T-4Q0T-restructure-task-touchpoints-as-a-table-with-symbol-dir-glob
Status: closed/done · Impact: high · Complexity: large
Line-number citations in task specs go stale as code churns, and ## Files to touch is a free-form
bulleted list the verifier has to regex-parse. Restructure both ## Today and ## Files to touch
to typed markdown tables with a small Location grammar that admits symbol-anchored, directory, and
glob references — so tasks survive normal code drift and the verifier has one shape to parse instead
of two.
| Location | Role today |
|---|---|
plugin/entities/task/template.md#files-to-touch | Ships ## Files to touch as a free-form bulleted list (- path — note) |
plugin/entities/task/template.md#today | Ships ## Today as prose with inline src/foo/bar.rs:42 citations |
plugin/skills/task-ensure-ready/SKILL.md#evaluate-the-contract | Extracts paths via regex `[\w/-.]+.(rs |
plugin/skills/task-review/SKILL.md | Verifies “for line numbers cited, code is at (or near) that line” — encodes the brittleness as a feature |
plugin/entities/task/schema.json | v2; no touchpoints_verified_at field |
plugin/entities/task/migrations/v1-to-v2.py | Frontmatter-only transform. Interface: migrate(fm) -> new_fm. Cannot rewrite body sections |
plugin/skills/entities-migrate/migrate_entities.py | Runner reads file, splits frontmatter, calls migrate(fm), writes back — no body-transform path today |
docs/planning/tasks/ | 130 tasks; closed tasks will be stamped schema_version: "0" (pre-managed sentinel); active tasks need the v3 shape |
Symbol references work informally today (the grep catches them) but aren’t first-class. Directory and glob touchpoints don’t exist as a concept — multi-file work either gets enumerated awkwardly or hand-waved.
Proposed
Section titled “Proposed”A v3 task schema where ## Today and ## Files to touch use markdown tables with a defined column
shape and a five-form Location grammar.
Table shapes:
## Today| Location | Role today |
## Files to touch| Location | Kind | Change | where Kind ∈ {new, modify, delete}Location grammar (five forms, one column):
| Form | Example | Meaning |
|---|---|---|
| File | plugin/entities/task/schema.json | Single file |
| File + symbol | plugin/skills/foo/bar.py#parse_row | Symbol within file (durable, preferred) |
| File + line | plugin/skills/foo/bar.py:42 | Line within file (brittle hint; allowed but discouraged) |
| Directory | plugin/skills/task-ensure-ready/ | Directory (trailing slash required) |
| Glob | plugin/skills/task-*/SKILL.md | Glob (presence of * / ? / [...] is the signal) |
Verifier semantics (task-ensure-ready):
kind: new→ no existence check (the row is proposing creation).kind: modify/kind: delete→ file/symbol/dir must resolve; glob must expand to ≥1 match.- Symbols on globs are rejected in v1 (deferred — see Out of scope).
Frontmatter: new field touchpoints_verified_at: (same shape as readiness_verified_at:,
single stamp). No per-cell status — per-cell freshness would create rebase conflicts in markdown
tables.
Hard cutover: the v3 verifier is table-only. No fallback code path for the v2 bulleted shape.
Closed tasks are NOT touched by this migration — they keep whatever schema_version they have
(typically '2' from the prior v1→v2 sweep) and their existing body shapes. Closed tasks are
historical artifacts; immutability beats consistency for them.
Migration: entities-migrate extended with a v2→v3 transform that mechanically converts
bulleted lists to tables, defaults kind: modify, and sets kind: new when the row note matches
new-file hints. The sweep operates only on active (non-closed) tasks. Ambiguous rows (deletes,
drift, unparseable notes) trigger definition_gap + downshift to planning/needs-definition,
except for tasks already at in-progress* (existing carve-out preserves status). LLM judgment for
the ambiguous tail happens lazily through task-review / task-define on the next normal touch —
no LLM in the migration sweep itself.
Approach
Section titled “Approach”This is a schema bump v2→v3. The five canonical questions from
plugin/conventions/schema-bump-checklist.md are answered first:
| Q | Answer |
|---|---|
1. Missing schema_version | Route absence through the v1→v2→v3 transform chain (same as today’s behavior — absence is treated as v1). Fixture: plugin/entities/task/migrations/fixtures/missing-stamp/ |
2. Current schema_version=3 | Pass-through, no diff. Re-runnable. Fixture: fixtures/already-v3/ |
3. Unknown legacy values (e.g. schema_version: 7) | Hard error to stderr; non-zero exit. Same as today’s behavior |
| 4. Transform error path | Per-task semantics: if body parse fails or a row is unrecoverable, that task gets definition_gap + (unless in-progress*) planning/needs-definition, and the sweep continues. Surfaced in migrate’s stdout JSON report. No global rollback |
| 5. Post-migrate stamp behavior | schema_version: "3" stamped via the existing canonical-order helper that validate_frontmatter.py uses (no change to placement) |
Implementation steps:
-
Extend
migrate_entities.pyto support body-aware transforms. Today’s runner only passes frontmatter. Add a calling convention where a transform module may declare amigrate(fm, body) -> (new_fm, new_body)signature instead ofmigrate(fm) -> new_fm; the runner detects which shape via inspection and routes accordingly. v1→v2 keeps its single-arg form. Add fixtures for both shapes. -
Write
plugin/entities/task/migrations/v2-to-v3.pyas the first body-aware transform:- Parse
## Files to touchbulleted block. Split each row on the first—(em-dash) or-separator intopath+note. - Default
kind: modify. Setkind: newif note matches\b(new|new file|new module|create)\b(case-insensitive, conservative). - Build a
Location | Kind | Changetable. - For
## Today: best-effort — if the section contains path-bearing rows that parse, convert to table; otherwise leave as prose and flag viadefinition_gap. The prose form is acceptable in v3 for Today only when no path-bearing rows exist; pure-narrative Today sections survive. - For any row that fails to parse or has an ambiguous kind hint (e.g. “remove” / “rename” without
a clear “(new)”), set
definition_gapon the frontmatter and (unlessin-progress*) downshift status toplanning/needs-definition. - Stamp
schema_version: "3". - Co-located tests at
plugin/entities/task/migrations/test_v2_to_v3.pycovering each of the five schema-bump questions plus the mechanical-with-flagging branch points.
- Parse
-
Bump
plugin/entities/task/schema.jsontoversion: 3. Add thetouchpoints_verified_atfield with the same shape asreadiness_verified_at(ISO 8601 UTC datetime, declared last to land at the bottom of frontmatter per the T-H0W9-task-work-rebase-frontmatter-conflict convention). -
Update
plugin/entities/task/template.md. Replace the bulleted## Files to touchskeleton with a table skeleton. Replace the prose## Todayexample with a table skeleton. Add a short header comment documenting the five Location forms. -
Update
plugin/entities/task/implementation-ready.md. Replace the “Files to touch — explicit list of paths” requirement with a table-shape requirement. Update the disqualifier list: cited paths/symbols must resolve per the kind-driven verifier rules above. -
Update
plugin/skills/task-ensure-ready/.- Add
parse_touchpoints.py(co-located, per the skill-authoring “co-locate first, promote when shared” rule). Parses both tables, returns row tuples(location, kind | None, note). - Replace the existing path-regex + separate symbol grep with a single table-driven check.
- Update
scan_placeholders.pyto recognize empty/<...>cells in tables as placeholder text. - Update SKILL.md’s “Evaluate the contract” step to describe the table-driven check; remove the regex-and-grep prose.
- Add
-
Update related skills.
task-reviewandtask-definereference the bulleted shape — update their SKILL.md and test fixtures (tests/run_evals.pyin both) to use tables. -
Document the “mechanical-with-flagging” pattern inline in
entities-migrate/SKILL.md(deferred promotion toplugin/conventions/entity-evolution.mduntil the second user, per the repo’s co-locate-first principle). -
Run the sweep. Invoke
/sdlc:entities-migrateover the corpus. Verify: closed tasks land atschema_version: "0"; active tasks land at"3"; ambiguous-row tasks carrydefinition_gapandplanning/needs-definition; in-progress tasks preserve status; report counts in stdout. -
Refresh per-skill docs under
docs/skills/for any SKILL.md that changed (use/sdlc:update-skill-docper skill).
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/entities/task/schema.json | modify | bump version to 3; add touchpoints_verified_at field declared last |
plugin/entities/task/template.md | modify | replace bulleted Files-to-touch with table skeleton; convert Today example to table; document five Location forms in header comment |
plugin/entities/task/implementation-ready.md | modify | table shape required; update disqualifier list to describe kind-driven resolution rules |
plugin/entities/task/migrations/v2-to-v3.py | new | body-aware transform; parses bulleted Files-to-touch + Today; emits tables; flags ambiguous rows with definition_gap |
plugin/entities/task/migrations/test_v2_to_v3.py | new | fixtures covering the five schema-bump questions + each mechanical-with-flagging branch |
plugin/entities/task/migrations/fixtures/ | new | missing-stamp/, already-v3/, ambiguous-row/, in-progress-carve-out/, unknown-legacy/ |
plugin/skills/entities-migrate/migrate_entities.py | modify | detect migrate(fm) vs migrate(fm, body) shapes; route accordingly |
plugin/skills/entities-migrate/SKILL.md | modify | document mechanical-with-flagging behavior inline (deferred promotion to a convention doc) |
plugin/skills/task-ensure-ready/parse_touchpoints.py | new | table-row parser; returns (location, kind, note) tuples for both Today and Files-to-touch |
plugin/skills/task-ensure-ready/SKILL.md | modify | describe table-driven check; remove regex + separate symbol-grep prose |
plugin/skills/task-ensure-ready/scan_placeholders.py | modify | recognize empty/<...> table cells as placeholders |
plugin/skills/task-ensure-ready/tests/run_evals.py | modify | convert fixtures to table shape |
plugin/skills/task-review/SKILL.md | modify | update Files-to-touch references and the “line numbers cited” verification step |
plugin/skills/task-define/SKILL.md | modify | update prompts that fill the Files-to-touch section |
plugin/skills/task-define/tests/run_evals.py | modify | convert fixtures to table shape |
docs/skills/task-ensure-ready.md | modify | refresh per house-style |
docs/skills/entities-migrate.md | modify | refresh per house-style |
docs/skills/task-review.md | modify | refresh per house-style |
docs/skills/task-define.md | modify | refresh per house-style |
docs/planning/tasks/**/*.md | modify | swept by entities-migrate; NOT hand-edited |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/entities/task/schema.jsondeclares"version": 3and includestouchpoints_verified_atfield;validators/validate_frontmatter.pyaccepts a task with the field set to a valid ISO 8601 UTC datetime and rejects malformed values. - AC-2:
task-ensure-readyparses## Files to touchas a markdown table with columnsLocation | Kind | Change; a v3 task using the bulleted shape fails the gate. The verifier code has one parsing path — no branch handles the legacy shape. - AC-3: The Location grammar accepts all five forms and the verifier resolves each per row
kind: kind=new → no existence check; kind=modify/delete → file/symbol/dir must resolve and glob must expand to ≥1 match. - AC-4:
plugin/entities/task/migrations/v2-to-v3.pyconverts a v2 task with a bulleted Files-to-touch block into a v3 task with the equivalent table, defaultingkind: modifyand settingkind: newwhen the row note matches new-file hints. - AC-5: A v2 task with an ambiguous row (e.g. note says “remove the legacy adapter” without an
explicit kind hint) gets
definition_gapset in frontmatter andstatus: planning/needs-definition, except when it was alreadyin-progressorin-progress/blocked— then the gap is set but status is preserved. - AC-6: Running
/sdlc:entities-migrateover the live corpus leaves closed tasks untouched (whateverschema_versionthey had, they keep; no body re-serialization), migrates active tasks toschema_version: "3", and reports a count of tasks downshifted toplanning/needs-definitionin its stdout summary. - AC-7: Re-running
/sdlc:entities-migrateagainst an already-migrated corpus is a no-op (zero diff, exit 0). Idempotency holds. - AC-8: The five schema-bump-checklist questions are answered concretely in this task’s
## Approach(already drafted as a table above) and each answer maps to a fixture underplugin/entities/task/migrations/fixtures/. - AC-9:
entities-migrate/SKILL.mddocuments the “mechanical-with-flagging” pattern inline — naming the three classes of schema change (auto-fixable, mechanical-with-flagging, non-mechanical) and identifying v2→v3 as the first user.
Out of scope
Section titled “Out of scope”- Auto-upgrading existing
path:linecitations topath#symbol. Requires line→symbol resolution (LSP territory). The new grammar acceptspath:lineas a valid degenerate form; existing citations stay as-is until a future task chooses to enrich them. - Symbols on globs (e.g.
plugin/skills/task-*/SKILL.md#notes). The grammar admits this shape syntactically; verifier expansion (“expand glob then grep each match for symbol”) is deferred. The v3 verifier rejects#symbolon glob rows. - Per-cell verification stamps in the table itself. The single frontmatter
touchpoints_verified_at:stamp is the only freshness signal in v3 — per-cell stamps would create rebase conflicts in markdown. - Promoting “mechanical-with-flagging” to
plugin/conventions/entity-evolution.md. Inline it inentities-migrate/SKILL.mdonly; promotion waits for the second user per the repo’s “co-locate first, promote when shared” principle. - Closed-task migration of any kind — neither body shape nor
schema_versionis changed for closed tasks. They keep their existing v2 state (or'0'sentinel if they had it). Whether the audit should treat closed tasks at older-than-current as drift, or skip them entirely, is a SEPARATE design question worth its own task; this PR does not answer it. The runner’s existing behavior (route closed entities through the transform chain when they have older-than-current drift) is unchanged by this PR — operators who need to skip closed entities can pass--type task --include-closed=falseor filter the input set; tightening that default is out of scope. - Cross-entity rollout (backlog/epic/milestone tables). Only
taskgets the table treatment in this work; other entity types stay as-is until a concrete need surfaces.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Surfaced during a design conversation about durability of impact-location references in task specs. Line-number citations drift as code churns; symbol/dir/glob references are more stable. The conversation also surfaced the “mechanical-with-flagging” schema-evolution pattern as a generalizable shape for future non-mechanical bumps — this task is the first user; promotion to a convention doc waits for the second.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-23. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
validate_frontmatter.pyaccepts/rejects per the new schema; fixtures inplugin/entities/task/migrations/fixtures/already-v3/. - AC-2: auto —
plugin/skills/task-ensure-ready/tests/run_evals.py::tp/bulleted-legacy-rejectedconfirms the single-grammar guarantee. - AC-3: auto —
tp/v3-table-happy-path,tp/glob-and-line-forms,tp/symbol-on-glob-rejected,tp/invalid-kind-flaggedcover the five forms and kind-driven resolution. - AC-4: auto —
plugin/entities/task/migrations/test_v2_to_v3.py::case_bulleted_to_table_default_kind_modify,::case_new_file_hint_sets_kind_new,::case_new_in_note_sets_kind_new. - AC-5: auto —
::case_remove_language_flags_and_downshifts,::case_remove_in_progress_preserves_status,::case_remove_in_progress_blocked_preserves_status. Notably also demonstrated end-to-end: the sweep itself flagged THIS task’s own file (status: in-progress) —definition_gapwas set and status preserved per the carve-out. - AC-6: agent-manual — sub-agent’s initial sweep also re-stamped 76 closed tasks to
"0"(per a misinterpretation of the spec language; corrected by commitsbdc31e9anda8ba8ebwhich restored closed tasks to theirorigin/mainstate and reverted the runner’s closed-entity short-circuit behavior). Final counts post-correction: closed tasks untouched (still at whateverschema_versionthey were onorigin/main, typically'2'), 53 active tasks at"3", 5 active tasks downshifted toplanning/needs-definition, 1 in-progress task preserved (this one). - AC-7: agent-manual — sub-agent ran
migrate_entities.py --dry-run --type taskpost-sweep; returnedwould fix: 0 file(s)with exit 0. - AC-8: auto — five-question table present in this task’s
## Approach; fixtures exist atplugin/entities/task/migrations/fixtures/{missing-stamp,already-v3,ambiguous-row,in-progress-carve-out,unknown-legacy}/with a README mapping each to its test case. - AC-9: auto —
plugin/skills/entities-migrate/SKILL.mdcarries a “Mechanical-with-flagging” section naming the three classes; v1→v2 cited as auto-fixable example, v2→v3 as the first mechanical-with-flagging user.
What worked
Section titled “What worked”- The “mechanical-with-flagging” pattern landed as designed. The sweep was deterministic, fast, and
the ambiguous tail surfaced cleanly through
definition_gap+ downshift — no LLM round-trip in the migration itself. - The
in-progress*carve-out worked end-to-end: this task itself was in-progress during the sweep, its row had “remove” language that mechanically flagged ambiguity, andstatus: in-progresswas preserved as the design promised. - Test coverage scaled with the AC count — every AC had a backing eval. The schema-bump-checklist’s five-question framework forced the right design decisions up front, so the implementer had nothing to invent mid-flight.
- The body-aware transform interface (signature-detect for
migrate(fm) -> new_fmvsmigrate(fm, body) -> (new_fm, new_body)) kept v1→v2 untouched while enabling v2→v3’s richer mutation surface.
Friction and automation gaps
Section titled “Friction and automation gaps”- The Step 5b rebase hit a frontmatter conflict on first run —
start_task.pywrote the verify commit’s newreadiness_verified_atvalue into main’s start commit, which made the verify commit on the task branch effectively empty when replayed, surfacing as a context conflict (because main’s start also added a newlast_reviewedline). Resolved bygit rebase --skip. There is already a tracked task for this —[T-2QXZ-start-task-handles-frontmatter-rebase-cleanly](/planning/tasks/start-task-handles-frontmatter-rebase-cleanly/)— so this PR’s friction confirms that task’s premise rather than introducing a new one. → T-2QXZ-start-task-handles-frontmatter-rebase-cleanly - Pre-existing audit drift (10 manual-review items unrelated to this PR) gated
run_quality_checks.py. Tracked separately as[T-H69K-run-quality-checks-isolates-pre-existing-drift](/planning/tasks/run-quality-checks-isolates-pre-existing-drift/)— proceeded past the gate by user choice; no new drift introduced. → T-H69K-run-quality-checks-isolates-pre-existing-drift - The
${CLAUDE_PLUGIN_ROOT}caveat (the running plugin code is loaded from the global install, not the worktree) means SKILL.md edits can’t be exercised through/sdlc:*commands during implementation. The project-local hook surfaces this on every task-work pickup, which worked correctly here. Not new friction. - The migration script flagged THIS task’s own file for ambiguous touchpoint language (“remove” in a
Files-to-touch row). This is the system working as designed (AC-5 in action) and matches the
“mechanical-with-flagging” promise. Not friction — but worth noting because it produced a slightly
confusing artifact: the implementing task’s frontmatter ends the run with
definition_gapset and noreadiness_verified_atstamp, which a future audit might read as “this task wasn’t ready” when in fact the gap was set by the very migration the task implemented. - Scope blowout from ambiguous “closed tasks remain at
'0'” language in the spec. The original spec said “Closed tasks remain atschema_version: '0'(pre-managed sentinel)” without making clear whether this meant “they’re already at'0'” (true if you imagined the prior v1→v2 sweep had stamped them there — it had not; it stamped them to'2') or “the migration should put them at'0'”. The sub-agent read it as the latter and ADDED a new behavior tomigrate_entities.py(commit8f18871) that short-circuited closed-but-older-than-current entities to the'0'sentinel. That introduced 76 unintended closed-task changes (schema bump from'2'to'0'plus PyYAML re-serialization ofcompletion_notefrom block to inline scalar). Caught in PR review; reverted viabdc31e9(restore files) +a8ba8eb(revert behavior). Automation gap: the schema-bump-checklist’s five questions don’t include “what happens to CLOSED entities during this migration?” — add it as a sixth canonical question so a future schema-bump spec is forced to answer “skip / re-stamp / route through chain” explicitly rather than leaving it implicit.
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-2QXZ-start-task-handles-frontmatter-rebase-cleanly — linked (existing task already covers the start_task.py rebase frontmatter conflict)
- T-H69K-run-quality-checks-isolates-pre-existing-drift — linked (existing task already covers the run_quality_checks.py drift-isolation gap)