Skip to content

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.

LocationRole today
plugin/entities/task/template.md#files-to-touchShips ## Files to touch as a free-form bulleted list (- path — note)
plugin/entities/task/template.md#todayShips ## Today as prose with inline src/foo/bar.rs:42 citations
plugin/skills/task-ensure-ready/SKILL.md#evaluate-the-contractExtracts paths via regex `[\w/-.]+.(rs
plugin/skills/task-review/SKILL.mdVerifies “for line numbers cited, code is at (or near) that line” — encodes the brittleness as a feature
plugin/entities/task/schema.jsonv2; no touchpoints_verified_at field
plugin/entities/task/migrations/v1-to-v2.pyFrontmatter-only transform. Interface: migrate(fm) -> new_fm. Cannot rewrite body sections
plugin/skills/entities-migrate/migrate_entities.pyRunner 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.

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):

FormExampleMeaning
Fileplugin/entities/task/schema.jsonSingle file
File + symbolplugin/skills/foo/bar.py#parse_rowSymbol within file (durable, preferred)
File + lineplugin/skills/foo/bar.py:42Line within file (brittle hint; allowed but discouraged)
Directoryplugin/skills/task-ensure-ready/Directory (trailing slash required)
Globplugin/skills/task-*/SKILL.mdGlob (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.

This is a schema bump v2→v3. The five canonical questions from plugin/conventions/schema-bump-checklist.md are answered first:

QAnswer
1. Missing schema_versionRoute 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=3Pass-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 pathPer-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 behaviorschema_version: "3" stamped via the existing canonical-order helper that validate_frontmatter.py uses (no change to placement)

Implementation steps:

  1. Extend migrate_entities.py to support body-aware transforms. Today’s runner only passes frontmatter. Add a calling convention where a transform module may declare a migrate(fm, body) -> (new_fm, new_body) signature instead of migrate(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.

  2. Write plugin/entities/task/migrations/v2-to-v3.py as the first body-aware transform:

    • Parse ## Files to touch bulleted block. Split each row on the first (em-dash) or - separator into path + note.
    • Default kind: modify. Set kind: new if note matches \b(new|new file|new module|create)\b (case-insensitive, conservative).
    • Build a Location | Kind | Change table.
    • For ## Today: best-effort — if the section contains path-bearing rows that parse, convert to table; otherwise leave as prose and flag via definition_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_gap on the frontmatter and (unless in-progress*) downshift status to planning/needs-definition.
    • Stamp schema_version: "3".
    • Co-located tests at plugin/entities/task/migrations/test_v2_to_v3.py covering each of the five schema-bump questions plus the mechanical-with-flagging branch points.
  3. Bump plugin/entities/task/schema.json to version: 3. Add the touchpoints_verified_at field with the same shape as readiness_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).

  4. Update plugin/entities/task/template.md. Replace the bulleted ## Files to touch skeleton with a table skeleton. Replace the prose ## Today example with a table skeleton. Add a short header comment documenting the five Location forms.

  5. 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.

  6. 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.py to 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.
  7. Update related skills. task-review and task-define reference the bulleted shape — update their SKILL.md and test fixtures (tests/run_evals.py in both) to use tables.

  8. Document the “mechanical-with-flagging” pattern inline in entities-migrate/SKILL.md (deferred promotion to plugin/conventions/entity-evolution.md until the second user, per the repo’s co-locate-first principle).

  9. Run the sweep. Invoke /sdlc:entities-migrate over the corpus. Verify: closed tasks land at schema_version: "0"; active tasks land at "3"; ambiguous-row tasks carry definition_gap and planning/needs-definition; in-progress tasks preserve status; report counts in stdout.

  10. Refresh per-skill docs under docs/skills/ for any SKILL.md that changed (use /sdlc:update-skill-doc per skill).

LocationKindChange
plugin/entities/task/schema.jsonmodifybump version to 3; add touchpoints_verified_at field declared last
plugin/entities/task/template.mdmodifyreplace bulleted Files-to-touch with table skeleton; convert Today example to table; document five Location forms in header comment
plugin/entities/task/implementation-ready.mdmodifytable shape required; update disqualifier list to describe kind-driven resolution rules
plugin/entities/task/migrations/v2-to-v3.pynewbody-aware transform; parses bulleted Files-to-touch + Today; emits tables; flags ambiguous rows with definition_gap
plugin/entities/task/migrations/test_v2_to_v3.pynewfixtures covering the five schema-bump questions + each mechanical-with-flagging branch
plugin/entities/task/migrations/fixtures/newmissing-stamp/, already-v3/, ambiguous-row/, in-progress-carve-out/, unknown-legacy/
plugin/skills/entities-migrate/migrate_entities.pymodifydetect migrate(fm) vs migrate(fm, body) shapes; route accordingly
plugin/skills/entities-migrate/SKILL.mdmodifydocument mechanical-with-flagging behavior inline (deferred promotion to a convention doc)
plugin/skills/task-ensure-ready/parse_touchpoints.pynewtable-row parser; returns (location, kind, note) tuples for both Today and Files-to-touch
plugin/skills/task-ensure-ready/SKILL.mdmodifydescribe table-driven check; remove regex + separate symbol-grep prose
plugin/skills/task-ensure-ready/scan_placeholders.pymodifyrecognize empty/<...> table cells as placeholders
plugin/skills/task-ensure-ready/tests/run_evals.pymodifyconvert fixtures to table shape
plugin/skills/task-review/SKILL.mdmodifyupdate Files-to-touch references and the “line numbers cited” verification step
plugin/skills/task-define/SKILL.mdmodifyupdate prompts that fill the Files-to-touch section
plugin/skills/task-define/tests/run_evals.pymodifyconvert fixtures to table shape
docs/skills/task-ensure-ready.mdmodifyrefresh per house-style
docs/skills/entities-migrate.mdmodifyrefresh per house-style
docs/skills/task-review.mdmodifyrefresh per house-style
docs/skills/task-define.mdmodifyrefresh per house-style
docs/planning/tasks/**/*.mdmodifyswept by entities-migrate; NOT hand-edited
  • AC-1: plugin/entities/task/schema.json declares "version": 3 and includes touchpoints_verified_at field; validators/validate_frontmatter.py accepts a task with the field set to a valid ISO 8601 UTC datetime and rejects malformed values.
  • AC-2: task-ensure-ready parses ## Files to touch as a markdown table with columns Location | 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.py converts a v2 task with a bulleted Files-to-touch block into a v3 task with the equivalent table, defaulting kind: modify and setting kind: new when 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_gap set in frontmatter and status: planning/needs-definition, except when it was already in-progress or in-progress/blocked — then the gap is set but status is preserved.
  • AC-6: Running /sdlc:entities-migrate over the live corpus leaves closed tasks untouched (whatever schema_version they had, they keep; no body re-serialization), migrates active tasks to schema_version: "3", and reports a count of tasks downshifted to planning/needs-definition in its stdout summary.
  • AC-7: Re-running /sdlc:entities-migrate against 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 under plugin/entities/task/migrations/fixtures/.
  • AC-9: entities-migrate/SKILL.md documents 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.
  • Auto-upgrading existing path:line citations to path#symbol. Requires line→symbol resolution (LSP territory). The new grammar accepts path:line as 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 #symbol on 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 in entities-migrate/SKILL.md only; 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_version is 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=false or filter the input set; tightening that default is out of scope.
  • Cross-entity rollout (backlog/epic/milestone tables). Only task gets the table treatment in this work; other entity types stay as-is until a concrete need surfaces.
  • none

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.

Captured by /sdlc:task-work on 2026-05-23. PR: pending.

  • AC-1: auto — validate_frontmatter.py accepts/rejects per the new schema; fixtures in plugin/entities/task/migrations/fixtures/already-v3/.
  • AC-2: auto — plugin/skills/task-ensure-ready/tests/run_evals.py::tp/bulleted-legacy-rejected confirms 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-flagged cover 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_gap was 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 commits bdc31e9 and a8ba8eb which restored closed tasks to their origin/main state and reverted the runner’s closed-entity short-circuit behavior). Final counts post-correction: closed tasks untouched (still at whatever schema_version they were on origin/main, typically '2'), 53 active tasks at "3", 5 active tasks downshifted to planning/needs-definition, 1 in-progress task preserved (this one).
  • AC-7: agent-manual — sub-agent ran migrate_entities.py --dry-run --type task post-sweep; returned would fix: 0 file(s) with exit 0.
  • AC-8: auto — five-question table present in this task’s ## Approach; fixtures exist at plugin/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.md carries 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.
  • 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, and status: in-progress was 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_fm vs migrate(fm, body) -> (new_fm, new_body)) kept v1→v2 untouched while enabling v2→v3’s richer mutation surface.
  • The Step 5b rebase hit a frontmatter conflict on first run — start_task.py wrote the verify commit’s new readiness_verified_at value 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 new last_reviewed line). Resolved by git 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_gap set and no readiness_verified_at stamp, 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 at schema_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 to migrate_entities.py (commit 8f18871) 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 of completion_note from block to inline scalar). Caught in PR review; reverted via bdc31e9 (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.

← Back to Tasks