diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c9ad269..4fd60be 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "progress-tracker", "displayName": "Progress Tracker", - "version": "1.7.1", + "version": "1.8.0", "description": "Tracks local development progress across scopes with consented, script-gated migration (a whole-document inventory audit, not just an eyeballed active-work section), and safe lifecycle scripts — for any project.", "author": { "name": "FWcloud916", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 972a128..1d18112 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "progress-tracker", - "version": "1.7.1", + "version": "1.8.0", "description": "Track local development progress across single- or multi-scope task lifecycles.", "author": { "name": "FWcloud916", diff --git a/AGENTS.md b/AGENTS.md index 88baed2..c44a753 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ single-line edits) can skip; do not pre-load all docs. | Changing lifecycle update, close-out, or audit behavior | [SKILL.md](skills/progress-tracker/SKILL.md) + [update_progress.py](skills/progress-tracker/scripts/update_progress.py) | | Changing the item template or INDEX shape | [PROGRESS.template.md](skills/progress-tracker/references/PROGRESS.template.md) + [INDEX.template.md](skills/progress-tracker/references/INDEX.template.md) | | Changing the status lifecycle | [SKILL.md](skills/progress-tracker/SKILL.md) §Status lifecycle + [workflow.md](skills/progress-tracker/references/workflow.md) | -| Changing migration-inventory/migration-audit behavior or the record shape | [SKILL.md](skills/progress-tracker/SKILL.md) §Before creating anything + [MIGRATION.template.md](skills/progress-tracker/references/MIGRATION.template.md) + [KNOWN-ISSUE.md](KNOWN-ISSUE.md) KI-001 | +| Changing migration-inventory/migration-audit behavior or the record shape | [migration.md](skills/progress-tracker/references/migration.md) + [MIGRATION.template.md](skills/progress-tracker/references/MIGRATION.template.md) + [KNOWN-ISSUE.md](KNOWN-ISSUE.md) KI-001 | | Changing eval scenarios or the trigger matrix | [evals/README.md](evals/README.md) | | Changing Codex plugin packaging or installation | [README.md](README.md) §Install + [docs/design-decisions.md](docs/design-decisions.md) Codex packaging decision | | Understanding why it's built this way | [docs/design-decisions.md](docs/design-decisions.md) | @@ -65,6 +65,23 @@ python3 evals/scripts/test_grade_scenarios.py - Requirement keywords (MUST/SHOULD/MAY) follow RFC 2119, uppercase. - English throughout — templates, field names, and section headings. +## SKILL.md editing checklist + +Before committing any change to `skills/progress-tracker/SKILL.md`, ask: + +1. **Premature completion** — does any sentence claim behavior that no script + or eval enforces yet? +2. **Duplication** — does the same rule now live in two places? Keep one + authoritative copy and point to it (only the gate and lifecycle marker + blocks are deliberately duplicated, byte-synced by verify.sh). +3. **Sediment** — is any sentence a leftover from a superseded design? +4. **Sprawl** — should single-branch detail move to a `references/` doc + behind a read-in-full pointer instead of staying top-level? +5. **No-op** — does each sentence change behavior relative to the model's + default? Delete failing sentences whole. +6. **Negation** — can a "do not" be restated as the positive target behavior? + (Hard guardrails stay negative.) + ## Docs maintenance When modifying any file under `docs/`, update its `> **Last updated:** diff --git a/README.md b/README.md index 29daac6..3e3de5e 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,13 @@ several — for any project. tracker directory's supporting files. - **Migration guard, script-gated** — before first use, inventories existing tracking documents and their pointers and asks whether to migrate. - `migration-inventory` scans a legacy source **whole-document** (never just - an "in progress" section), can scaffold an empty tracker before destination - creation, and `migration-audit` is the pre-deletion gate: it - fails while any actionable/ambiguous entry lacks a valid `migrated` or - `excluded` disposition and destination, migrated Evidence is absent or - non-unique, any generated record field changed, - any old path/name or changed link is unverified, or the pre-deletion human - sign-off checklist is incomplete. `migration-finalize` durably records the - user's retain/delete decision without deleting source files. + Migration is a two-phase commit: `migration-inventory` scans each legacy + source **whole-document** (never just an "in progress" section), + `migration-audit` is the prepare-phase gate that must pass before the + deletion question may even be asked, and `migration-finalize` durably + records the user's retain/delete decision without deleting source files. + The full contract lives in + [`references/migration.md`](skills/progress-tracker/references/migration.md). - **Multi-scope, tool-agnostic** — `--scope name[:branch[:ticket]]` accepts any free-form label (a service, a package, a sibling repo — not validated against a directory) and any ticket format (serial, `#123`, `JIRA-111`, a @@ -161,7 +159,7 @@ progress-tracker/ │ └── progress-tracker/ │ ├── SKILL.md # canonical entry point: lifecycle, args, status enum │ ├── agents/ # Codex UI metadata (openai.yaml) -│ ├── references/ # workflow spec + item/index templates + seed READMEs +│ ├── references/ # workflow spec + migration contract + templates + seed READMEs │ └── scripts/ # create/update/check CLIs + their pytest suites ├── agents/ # dedicated agent definition (preloads the skill) ├── AGENTS.md # maintainer guide for this repo (CLAUDE.md is a symlink to it) @@ -181,6 +179,7 @@ progress-tracker/ | [AGENTS.md](AGENTS.md) | Maintainer guide: hard constraints, the verify gate | | [docs/design-decisions.md](docs/design-decisions.md) | Decision log with rationale: generic scope model, script-first eval strategy | | [workflow.md](skills/progress-tracker/references/workflow.md) | Full workflow spec: folder structure, field semantics, cleanup policy | +| [migration.md](skills/progress-tracker/references/migration.md) | The migration contract in full: flow, command reference, Kind/disposition rules | | [PROGRESS.template.md](skills/progress-tracker/references/PROGRESS.template.md) | The item template | | [INDEX.template.md](skills/progress-tracker/references/INDEX.template.md) | The item-list seed | | [evals/README.md](evals/README.md) | Scenario + trigger-matrix strategy and how to run them | diff --git a/agents/progress-tracker.md b/agents/progress-tracker.md index 08eb090..dae6c7d 100644 --- a/agents/progress-tracker.md +++ b/agents/progress-tracker.md @@ -36,7 +36,8 @@ The preloaded **progress-tracker** skill is your operating manual. Follow it str "in progress" section, and defaults every unrecognized heading to blocking rather than assuming it is safe to ignore. Copy every actionable entry, dispose of every inventory row, update every pointer, and audit all old - path/name references afterward. + path/name references afterward. A migration was approved → read the skill's + `references/migration.md` in full before running any migration command. Migration is script-gated. The deletion question MUST NOT be asked until both diff --git a/docs/design-decisions.md b/docs/design-decisions.md index 6558696..ca6a370 100644 --- a/docs/design-decisions.md +++ b/docs/design-decisions.md @@ -2,13 +2,133 @@ > **Type:** Reference > **Audience:** Maintainers, AI agents -> **Last updated:** 2026-07-27 +> **Last updated:** 2026-07-28 A decision log with rationale, in chronological order. When a design choice seems arbitrary, check here before changing it. --- +## 2026-07-28 — Shared vocabulary lives in domain-models.md, not a repo-root CONTEXT.md + +The improvement plan's Phase 4 prescribed a repo-root `CONTEXT.md` for shared domain +vocabulary (scope, disposition, Kind, tracker-dir, preflight, two-phase commit). +Implemented instead as `docs/domain-models.md` §0 Shared Vocabulary: + +- The terms are structural domain terminology — they sit one section above the + entities that define them, so definitions and structure cannot drift apart. +- `AGENTS.md`'s task→doc table already routes "domain behavior" reads to + domain-models.md; a second repo-root file would split the same lookup across two + homes for no added recall. +- doc-architect 2.4.0 ships an opt-in `CONTEXT.md` glossary module; this repo + declines the module deliberately — revisit only if the vocabulary outgrows the + domain reference or needs rulings that contradict it (`_Avoid_` synonym lists, + cross-doc drift tripwires). + +## 2026-07-28 — SKILL.md defers argument semantics to the CLI interface + +Experiment: can SKILL.md's Key-arguments list and option prose be replaced +by the scripts' own `--help`, per the interface-design principle (the +interface teaches through its structure; errors teach at the moment of the +mistake)? Method: a fact-by-fact coverage matrix — every sentence proposed +for deletion was checked against live `--help` output and probed error +messages, keeping the eval philosophy's independent truth source (actual +CLI behavior, not the docs being edited). + +Result: adopted. `--help` fully covers slug format, `--scope` syntax and +defaults, verbatim tickets, `--plan` resolution, `--dir` containment, and +`--root` discovery; `choices=` enums expose the status values; and every +probed mistake (bad slug, bare plan filename, dir escape, optionless +update, invalid transition, unknown slug) fails with an actionable message +that names the fix. Those sentences left SKILL.md (180 → 162 lines) behind +a "read `--help` before first use" pointer. + +Two classes of prose stayed, by rule: **behavioral policy** the interface +cannot express (always pass `--plan` when a plan exists; back-fill `TBD`; +keep the two Status fields identical; `review` ≠ `done`), and **silent- +failure traps** the interface cannot catch at the right moment — the one +found: an unescaped comma in a `--scope` value used to split the entry in +two with no error. That pre-registered condition has since fired: the parser +now rejects whitespace-adjacent unescaped commas and empty entries, and both +CLIs echo the parsed scope in normal mode (see the amended 2026-07-24 +escaping entry), so the escaping sentence moved out of SKILL.md into +`--help`, leaving `--plan` as the sole interface-untaught rule. + +--- + +## 2026-07-28 — Eval suite is not tautological with SKILL.md + +Audited whether the graders judge with the same logic the skill documents — +which would let a docs change rubber-stamp itself. They do not: every +scenario runs the real CLIs in a disposable git repo, and +`grade_scenarios.py` asserts only on the files those runs produce +(`files_exist` / `files_absent` / `content_contains` / `content_not_contains` +/ `content_count`); no scenario, grader, or grader-regression test reads +`SKILL.md` or any `references/` doc. Expected values therefore come from an +independent truth source (script behavior), so the documentation refactor +cannot false-green the evals and the evals cannot vouch for prose claims — +`scripts/verify.sh` remains the gate for doc-level consistency. + +--- + +## 2026-07-28 — Invocation semantics stay single-skill + +Reviewed alongside the progressive-disclosure split: `check` (the tracker +audit) stays model-invoked inside this skill so agents run it before +review/close-out without a separate activation; migration stays in-skill too, +loaded only through the read-in-full pointer to `references/migration.md` +rather than split into a second skill — a split would spend an extra +description slot in every session's context for a branch most sessions never +take. With only two skills in this repository, a router skill is likewise +unjustified. The frontmatter description was rewritten in the same spirit: +one leading trigger per lifecycle branch (create / update / audit / close out +/ migrate), front-loaded with the preflight anchor. The activation boundary +is unchanged — the same prompts trigger and the same prompts do not — so +`evals/trigger-matrix.json` and its case counts stay as they are. + +--- + +## 2026-07-28 — Progressive disclosure for the migration contract + +Migration detail (~110 of SKILL.md's 302 lines) sat top-level in `SKILL.md` +even though only the migration branch ever needs it, and the same seven-step +contract was restated nearly in full in `references/workflow.md`. Both copies +now collapse to the shared preflight rules, the byte-synced +`MIGRATION_GATE` block, and an imperative pointer ("read +`references/migration.md` in full before running any migration command"); +`references/migration.md` is the single authoritative migration document — +discovery/consent, the KI-001 rationale, the merged step-by-step flow, the +command reference, and the Kind/disposition rules. + +Deliberate residuals: the gate block stays byte-identical in `SKILL.md`, +`workflow.md`, and `agents/progress-tracker.md` (verify.sh check 3b) and is +**not** duplicated into `migration.md`, which would be a fourth, unchecked +copy; `agents/progress-tracker.md` keeps its condensed migration paraphrase +(a subagent definition must be self-contained) plus the same read-in-full +pointer; and SKILL.md's preflight detection rules remain top-level because +every create runs them, not just the migration branch. `migration.md` is not +in verify.sh's scaffold-link map, so it is never copied into user projects — +the scaffolded seed docs already direct readers back to the installed skill. + +External corroboration (added the same day): Anthropic's post ["The new +rules of context engineering for Claude 5 generation +models"](https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models) +independently prescribes the same moves this refactor made — progressive +disclosure over upfront content, focused single-home guidance over +repetition, and pruning constraint prose in favor of model judgment +("unhobbling"). Two deliberate divergences stand: the migration gate's +MUST NOT wording stays, because it is an incident-driven (KI-001), +script-enforced guardrail — exactly the load-bearing minority of rules the +post's own framing says to keep; and SKILL.md retains its three CLI +examples plus the Key-arguments list, because verify.sh check 6 requires +the command strings and the examples are already one-per-lifecycle-stage +minimal. The post's "interface design" step — collapsing the Key-arguments list into +a `--help` pointer — was run as its own experiment and adopted the same day; +see the next entry for the coverage-matrix method and the one fact the +interface cannot teach. + +--- + ## 2026-07-27 — Package Codex directly from the repository root The repository root is the plugin root: `.codex-plugin/plugin.json` points to @@ -52,7 +172,10 @@ Allowed transitions reflect normal review rework and explicit termination: Scope labels remain free-form and filesystem-independent. The compact legacy syntax stays compatible, with backslash escaping added for literal commas, colons, and backslashes. The first two unescaped colons delimit fields and an -unescaped comma delimits scope entries. +unescaped comma delimits scope entries. Amended 2026-07-28: an unescaped +comma with adjacent whitespace is rejected as ambiguous and an empty entry +(trailing, leading, or doubled comma) is rejected — both previously failed +silently — and both CLIs echo the parsed scope names in normal-mode output. All CLI values written into Markdown tables are now rendered through shared helpers. Pipes and backslashes are escaped, code spans choose a delimiter that diff --git a/docs/domain-models.md b/docs/domain-models.md index ec983e6..87d71c8 100644 --- a/docs/domain-models.md +++ b/docs/domain-models.md @@ -2,14 +2,30 @@ > **Type:** Reference > **Audience:** Developers, AI assistants, code reviewers -> **Last updated:** 2026-07-27 +> **Last updated:** 2026-07-28 --- The project has no database models. Its domain consists of Markdown records, parsed in-memory dataclasses, and guarded transitions between filesystem states. The canonical -workflow contract is [`SKILL.md`](../skills/progress-tracker/SKILL.md); this document -maps that contract to the implementing functions in the two Python CLIs. +workflow contract is [`SKILL.md`](../skills/progress-tracker/SKILL.md) (with the +migration contract in +[`references/migration.md`](../skills/progress-tracker/references/migration.md)); this +document maps that contract to the implementing functions in the two Python CLIs. + +## 0. Shared Vocabulary + +Terms used with a fixed meaning across `SKILL.md`, the `references/` docs, +and this document: + +| Term | Definition | +|---|---| +| **scope** | A free-form label for one piece of a task (e.g. `api`, `payments-service`); never validated against any directory. | +| **tracker-dir** | The tracker's root directory inside the project — `progress/` by default, overridden by `--dir` / `$PROGRESS_TRACKER_DIR` — holding `INDEX.md`, item folders, `_template/`, `_plans/`, and `_migrations/`. | +| **preflight** | The mandatory pre-creation inspection for an existing tracking mechanism and the documents that point to it (`SKILL.md` §Before creating anything). | +| **two-phase commit** | The migration deletion model: `migration-audit` is the prepare phase, and deletion may proceed only after it exits 0 and `migration-finalize` records the user's explicit decision (the commit). | +| **Kind** | A migration-inventory row's generated classification: `actionable` and `ambiguous` block the audit; `done`, `empty`, and `historical` do not. | +| **disposition** | The reviewer's ruling on an inventory row: `migrated`, `excluded`, or `not-applicable`. | ## 1. Model Details @@ -295,6 +311,9 @@ Only then does the record enter `deleted`. - `require_project_descendant()` resolves paths before accepting them, including paths behind existing symlinks. - Scope labels are parsed text and never validated as directories. +- Ambiguous scope input fails closed: an unescaped comma touching whitespace + and empty scope entries are rejected at parse time, and both CLIs echo the + parsed scope names in normal-mode output. - Ticket values are trimmed and defaulted but never prefixed or reformatted. - Markdown table values escape pipes and backslashes; inline code chooses a delimiter longer than any backtick run in the value. diff --git a/docs/project-overview.md b/docs/project-overview.md index 1ddc822..6a56f31 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -2,7 +2,7 @@ > **Type:** Explanation > **Audience:** Developers, AI assistants, and tooling that needs project context -> **Last updated:** 2026-07-27 +> **Last updated:** 2026-07-28 > > A reusable agent skill and deterministic CLI toolkit for durable, local development-progress tracking. Related docs: [domain-models.md](domain-models.md), [coding-style.md](coding-style.md), and [design-decisions.md](design-decisions.md). @@ -30,7 +30,9 @@ runtime. The public behavior is specified in [`skills/progress-tracker/SKILL.md`](../skills/progress-tracker/SKILL.md). The longer operational rationale and field reference live in -[`workflow.md`](../skills/progress-tracker/references/workflow.md). +[`workflow.md`](../skills/progress-tracker/references/workflow.md); the migration +contract in full lives in +[`migration.md`](../skills/progress-tracker/references/migration.md). ### 1.2 Relationship with Other Systems @@ -90,7 +92,7 @@ The repository separates human/agent workflow guidance from deterministic mutati User or agent request | v -skills/progress-tracker/SKILL.md --------> references/workflow.md +skills/progress-tracker/SKILL.md --------> references/workflow.md + migration.md | | | chooses command | explains fields and policy v v @@ -143,7 +145,7 @@ progress-tracker/ └── skills/progress-tracker/ ├── SKILL.md # Canonical agent workflow and public command contract ├── agents/openai.yaml # Codex skill UI metadata - ├── references/ # Workflow, templates, and generated tracker seed docs + ├── references/ # Workflow, migration contract, templates, and seed docs └── scripts/ ├── new_progress.py # Scaffold and create CLI ├── update_progress.py # Update, close, audit, and migration CLI diff --git a/evals/README.md b/evals/README.md index 1aac254..70c9908 100644 --- a/evals/README.md +++ b/evals/README.md @@ -8,9 +8,9 @@ is a deterministic script, not a model judgment call). 1. **Lifecycle scenarios** run the real scaffold script end-to-end in a disposable git repository and grade filesystem/content invariants — - creation, multi-scope expansion, escaped input, slug-namespaced plan - snapshots, lifecycle-script close-out, contained custom `--dir`/`--root` - overrides, first-inventory tracker scaffolding, and the + creation, multi-scope expansion, escaped input, ambiguous-scope refusal, + slug-namespaced plan snapshots, lifecycle-script close-out, contained + custom `--dir`/`--root` overrides, first-inventory tracker scaffolding, and the migration-inventory/migration-audit gate, per-row Evidence, durable migration-finalize outcomes (including the KI-001 regression: an empty "in progress" section must not make the audit diff --git a/evals/scenarios/ambiguous-scope-refuse/scenario.json b/evals/scenarios/ambiguous-scope-refuse/scenario.json new file mode 100644 index 0000000..706d482 --- /dev/null +++ b/evals/scenarios/ambiguous-scope-refuse/scenario.json @@ -0,0 +1,12 @@ +{ + "description": "An unescaped comma with adjacent whitespace in --scope is rejected as ambiguous instead of silently splitting the entry, and nothing is written.", + "steps": [ + {"type": "run_expect_fail", "args": ["ambiguous-task", "--scope", "my label, with comma"], "expect_stderr_contains": "ambiguous"} + ], + "checks": { + "files_absent": [ + "progress", + "*-ambiguous-task" + ] + } +} diff --git a/progress/2026-07-28-skill-docs-progressive-disclosure/PROGRESS.md b/progress/2026-07-28-skill-docs-progressive-disclosure/PROGRESS.md new file mode 100644 index 0000000..d66946a --- /dev/null +++ b/progress/2026-07-28-skill-docs-progressive-disclosure/PROGRESS.md @@ -0,0 +1,59 @@ +# SKILL.md progressive-disclosure refactor + +**Slug:** skill-docs-progressive-disclosure +**Status:** review +**Ticket:** N/A +**Related plan:** [skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md](../_plans/skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md) +**Created:** 2026-07-28 +**Updated:** 2026-07-28 + +--- + +## Scope + +| Scope | Branch | Ticket | Notes | +|---|---|---|---| +| `progress-tracker` | TBD | TBD | | + +## Background & goals + +Execute the user-supplied `progress-tracker-improvement-plan.md` (per the +writing-great-skills review of `SKILL.md`): reduce context load, eliminate +duplication between `SKILL.md` and `references/workflow.md`, and strengthen +leading-word anchoring — using the repo's own evals (trigger matrix + +scenarios) and `scripts/verify.sh` as the regression net throughout. Expected +outcome: `SKILL.md` shrinks well below its 302-line starting point without +losing any enforced behavior, and the migration contract lives in exactly one +authoritative place. + +## Task list + +- [x] Phase 1 — split the migration contract into `references/migration.md`; prune no-op sentences +- [x] Phase 2 — rewrite the frontmatter description; record invocation-semantics decision +- [x] Phase 3 — anchor migration as a two-phase commit; negation scan +- [x] Phase 4 — shared vocabulary, editing checklist, eval-tautology audit, plugin version bump +- [x] Interface-design experiment — defer argument semantics to `--help` via a coverage matrix +- [x] Close the scope-comma silent-failure trap the experiment surfaced (ambiguity + empty-entry errors, scope echo) +- [x] Address PR #1 code review (explicit-empty-`--scope` handling, review-status honesty, plan deviation record) + +## Work log + +### 2026-07-28 + +- Phase 1: split migration contract into references/migration.md (single authoritative doc, absorbing workflow.md's parallel section), pruned SKILL.md 302 -> 195 lines, synced doc surfaces; verify.sh green. +- Phase 2: rewrote frontmatter description (one leading trigger per lifecycle branch, preflight anchor, 110 words) and recorded the single-skill invocation-semantics decision; SKILL.md at 178 lines. +- Phase 3: anchored the two-phase-commit leading sentence above the gate block (outside the byte-synced markers) and in migration.md's opening; negation scan kept hard guardrails, soft negations were already rewritten positively during the split. +- Phase 4: shared vocabulary in domain-models.md, SKILL.md editing checklist in AGENTS.md, eval-tautology audit recorded, plugin manifests bumped to 1.8.0; full gate green. Branch ready for review. +- Closed item as `done`. +- Reopened to `review` per PR #1 code review: an item is not `done` while its PR is + still open. Also addressed the review findings (scope-comma guard: explicit empty + `--scope` now rejected instead of silently ignored; `--help` names leading commas; + plan deviations recorded). + +## Outcome + +Executed the writing-great-skills refactor: SKILL.md 302 -> 180 lines via progressive disclosure into references/migration.md (single authoritative migration doc), description rewritten with one trigger per lifecycle branch, two-phase-commit leading anchor added, shared vocabulary + SKILL.md editing checklist + eval-tautology audit recorded; plugin 1.8.0. verify.sh, ruff, 14 scenarios, and grader regression all green. + +**Final status:** review — [PR #1](https://github.com/FWcloud916/skill-progress-tracker/pull/1) open; close as `done` after merge +**PR / Commit:** [PR #1](https://github.com/FWcloud916/skill-progress-tracker/pull/1) (branch claude/progress-tracker-improvement-plan-950330) +**Follow-ups:** None diff --git a/progress/INDEX.md b/progress/INDEX.md index daa01c9..67f00f9 100644 --- a/progress/INDEX.md +++ b/progress/INDEX.md @@ -10,6 +10,7 @@ workflow. | Status | Item | Folder | Scope | Ticket | Plan | Created | Notes | |---|---|---|---|---|---|---|---| | `done` | Progress Tracker Improvements | `progress/2026-07-24-progress-tracker-improvements/` | `progress-tracker` | N/A | _plans/progress-tracker-improvements-plan.md | 2026-07-24 | P0 verification truthfulness; P1 lifecycle automation and input hardening | +| `review` | SKILL.md progressive-disclosure refactor | `progress/2026-07-28-skill-docs-progressive-disclosure/` | `progress-tracker` | N/A | [skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md](_plans/skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md) | 2026-07-28 | | ## Status legend diff --git a/progress/_plans/skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md b/progress/_plans/skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md new file mode 100644 index 0000000..96ba227 --- /dev/null +++ b/progress/_plans/skill-docs-progressive-disclosure-progress-tracker-improvement-plan.md @@ -0,0 +1,124 @@ +# skill-progress-tracker 改善計劃 + +> **依據:** 對照 `mattpocock/skills` 的 `writing-great-skills` 原則,逐行審視現行 `skills/progress-tracker/SKILL.md`(309 行)後制定。 +> **核心目標:** 降低 context load、消除 duplication、用 leading words 強化行為錨定 — 全程以既有 evals(trigger matrix + scenarios)當回歸安全網。 +> **建立日期:** 2026-07-27 + +--- + +## Phase 1 — Pruning pass(優先度:最高) + +### 1a. Migration 段落 progressive disclosure + +**問題:** SKILL.md 309 行中 migration 佔約 120 行,但 migration 是只有單一 branch 會走到的路徑;大多數 session 只做 create / update / close。依 branch 判準(所有分支都需要的留頂層,只有部分分支會走到的推下去),這 120 行不該常駐頂層。 + +**做法:** + +1. SKILL.md 只保留: + - Preflight 偵測規則(每次 create 都要跑,屬於全分支內容) + - `MIGRATION_GATE_START/END` 硬約束區塊(verify.sh 已鏡射,維持 single source of truth 機制) + - 一行 context pointer +2. 以下整段搬到新檔 `references/migration.md`: + - 七步 migration 流程細節(步驟 3–7 的展開內容) + - 五種 `Kind` 值語義與 disposition 規則 + - v1 → v2 record 升級行為 + - Migration command reference 區塊 +3. **Pointer 措辭用命令式**,決定觸發率的是措辭不是目標: + - ✅ `A migration was approved → read references/migration.md in full before running any migration command.` + - ❌ `See references/migration.md for details.` + +### 1b. 消除 migration 指令的 duplication + +**問題:** migration 指令目前出現兩次 — 編號步驟內一次、「Migration command reference」區塊一次。Duplication 除了維護成本,還會虛增該內容在 information hierarchy 上的地位。 + +**做法:** 權威版本留在 `references/migration.md` 的 command reference,步驟內文改為指向。 + +### 1c. 逐句 no-op 測試 + +**規則:** 對每一句問「這句有沒有改變相對於模型預設的行為?」失敗的**整句刪除**,不修剪字詞。 + +**已識別候選:** + +- `Use --dry-run first to preview what would be created.` — flag 說明已含 preview 語意,近乎 no-op +- 各處「brief notes」「as needed」等修飾語逐一檢驗 + +**驗收:** 拆分 + 修剪後 SKILL.md 目標 ≤ 180 行,`bash scripts/verify.sh` 通過,evals 全綠。 + +--- + +## Phase 2 — Description 重寫 + invocation 語意 + +### 2a. Description 依「one trigger per branch」重寫 + +現行 description 的 trigger 條列品質不錯(含 negative trigger),但仍可依三規則收緊: + +1. **Front-load leading word** — 第一個詞就做觸發工作 +2. **每個 branch 只留一個 trigger** — create / update / audit / close-out / migrate 各一,同義改寫視為 duplication 合併 +3. **刪掉 body 已有的 identity 描述** + +Description 每一輪都躺在 context window 裡,是全 repo 最貴的文字。 + +### 2b. Invocation 語意決策(記錄進 design-decisions.md) + +- `check`(audit):保留 model-invoked — 期望 agent 在 close-out 前自動跑 +- Migration:1a 拆出後天然降級為「僅由主 skill 指路才載入」的層級,等效降低 context load,**不另拆成獨立 skill**(granularity 經濟學:每次 split 都花掉一種 load,目前拆不出對應價值) +- Router skill:目前僅兩個 skill,不需要 + +**驗收:** trigger-matrix.json 全數通過(description 改動直接影響觸發,這是它的安全網)。 + +--- + +## Phase 3 — Leading words 重構 + +### 3a. Migration → **two-phase commit** + +inventory → audit(= prepare)→ finalize(= commit)→ confirm-deleted 的結構天生就是 2PC。在 SKILL.md gate 區塊與 `references/migration.md` 開頭各錨定一次: + +> Migration is a two-phase commit: the audit is the prepare phase; nothing is deleted until it votes yes. + +徵召模型既有的 2PC 先驗 — 其語意模型中不存在「跳過 prepare 直接 commit」的路徑,比條列 MUST NOT 更穩。 + +### 3b. 強化既有 leading words + +- **preflight** — 已是好詞,擴大到 description 也使用,讓觸發與執行共用同一錨點 +- Status lifecycle 的 `blocked ↕` 圖已足夠緊湊,維持 + +### 3c. Negation 掃描 + +保留正當的 hard guardrail(「Never delete current tracker items automatically」屬此類,保留並已配對正向指示)。其餘 do not / never 逐一檢查能否改寫為正向陳述目標行為。 + +--- + +## Phase 4 — 功能性移植(挑選執行) + +1. **`CONTEXT.md`(共享語言)** — 集中定義 domain 詞彙:scope、disposition、Kind、tracker-dir、preflight、two-phase commit。AGENTS.md 與 SKILL.md 引用之,減少各處重複解釋。 +2. **Failure-mode review checklist** — 把六個 failure mode(premature completion / duplication / sediment / sprawl / no-op / negation)做成人工 checklist,納入 verify.sh 流程說明;之後每次改 SKILL.md 都過一遍。 +3. **Tautological eval 自查** — 檢查 grader 是否用與 skill 相同的邏輯判分;期望值必須來自獨立 truth source。 + +--- + +## 執行順序與驗收總表 + +| 順序 | 項目 | 產出 | 驗收 | +|---|---|---|---| +| 1 | 1a + 1b Migration 拆分 | `references/migration.md` + 縮短的 SKILL.md | verify.sh + evals 全綠 | +| 2 | 1c No-op 修剪 | SKILL.md ≤ 180 行 | 同上 | +| 3 | 2a Description 重寫 | 新 description | trigger matrix 全過 | +| 4 | 2b Invocation 決策 | design-decisions.md 新條目 | — | +| 5 | 3a–3c Leading words | 修訂後全文 | scenarios 全綠 | +| 6 | Phase 4 擇項 | CONTEXT.md 等 | verify.sh 通過 | + +**每個 phase 是一個獨立 commit / PR,eval 全綠才進下一個。** + +--- + +## 執行偏差記錄(2026-07-28,PR #1 review 後補記) + +1. **基線行數 309 → 302:** 本計劃撰寫時(2026-07-27)量測 SKILL.md 為 309 行; + 執行開始時(2026-07-28)main 上的 SKILL.md 為 302 行(期間 main 有其他變更落地)。 + 驗收指標「≤ 180 行」以執行起點 302 為基線計算,PROGRESS.md 記錄的 302 為準。 +2. **Phase 4 第 1 項改道:** 計劃寫 `CONTEXT.md`,實作裁決為 + `docs/domain-models.md` §0 Shared Vocabulary — 詞彙緊鄰其實體定義、 + AGENTS.md 的 task→doc 表已路由到該檔;獨立的 repo-root 檔案會把命名與 + agents 實際閱讀的 domain 參考拆成兩處。完整理由見 + `docs/design-decisions.md`「Shared vocabulary lives in domain-models.md」條目。 diff --git a/skills/progress-tracker/SKILL.md b/skills/progress-tracker/SKILL.md index 466d41c..3f2d329 100644 --- a/skills/progress-tracker/SKILL.md +++ b/skills/progress-tracker/SKILL.md @@ -1,30 +1,21 @@ --- name: progress-tracker description: >- - Manages local development progress tracking under a project's progress/ - directory. Trigger this skill when: (1) the user wants to start tracking a - development task that runs a full lifecycle (investigate → fix → test → - PR/MR), whether it touches one scope (service, package, repo) or spans - several — and wants a progress record; (2) the user asks to create, - update, audit/check, close out, or migrate a progress item or existing local - tracking documents; (3) the user explicitly invokes /progress-tracker. Do - NOT trigger for genuine one-off questions or trivial - edits with no lifecycle to track (a typo fix, a config tweak, answering a - question) — a single-scope bug fix that goes through investigate/fix/test/PR - still qualifies for (1). + Preflight-checked local development progress tracking under a project's + progress/ directory. Trigger when: (1) the user starts a development task + with a full lifecycle (investigate → fix → test → PR/MR) and wants a durable + record — create; (2) an existing item's status, scope, or work log needs + updating — update; (3) the tracker needs a consistency audit — audit; + (4) finished work needs its outcome recorded — close out; (5) existing local + tracking documents should be adopted — migrate; (6) the user invokes + /progress-tracker. Do NOT trigger for one-off questions or trivial edits + with no lifecycle (a typo, a config tweak); a single-scope fix with a full + lifecycle still qualifies. --- # Development Progress Tracker -Track development tasks locally under a `progress/` directory at the project -root (configurable — see below). Works for a single-scope project or one that -spans several scopes (services, packages, sibling repos); "scope" is a -free-form label, not validated against anything. - -Full spec: `references/workflow.md` -Item list: `/INDEX.md` -Template: `/_template/PROGRESS.md` -Migration record template: `references/MIGRATION.template.md` +Full spec: `references/workflow.md` · Migration contract: `references/migration.md` ## Before creating anything: existing-tracker preflight @@ -36,33 +27,16 @@ in `AGENTS.md`, `README.md`, or project docs. Judge by content, not filename alone. An existing `/INDEX.md` with this skill's structure is an already-adopted tracker, not a migration candidate. -If a separate tracking mechanism exists: - -1. **Do not scaffold or mutate anything yet.** Show the user the artifacts and - the documents/scripts that reference them. -2. **Ask whether to migrate.** An explicit answer is required; silence is not - consent. If the user declines, preserve the existing mechanism and do not - create a second tracker unless they explicitly choose coexistence. -3. If approved, run `migration-inventory` to produce a section-by-section - inventory of the source (see below) **before** editing anything. Do not - rely on an "in progress" or "current work" section alone to decide what is - actionable — inventory the whole document. Do not invent missing facts. - Keep the source unchanged during migration and audit. -4. Copy every actionable entry the inventory lists into the new item(s): - current status, scope, branches/tickets/plans, goals, unfinished tasks, - current work log, blockers, next actions, and live references. For each - entry, fill in the record's Disposition (`migrated`, `excluded`, or - `not-applicable`) and Destination. Every `migrated` row also needs a - non-trivial, row-specific Evidence locator copied from the destination item; verify - semantic equivalence against wherever it landed. -5. Update every live pointer to the old mechanism, including Markdown links, - path mentions, agent instructions, command examples, scripts, and - configuration. -6. Run `update_progress.py check`, search the whole project for every old path - or filename, and inspect all changed links. Classify any remaining match as - an intentional historical/compatibility reference. Tick the record's human - sign-off checklist only once each item has actually been verified. -7. Run `migration-audit` — the pre-deletion gate: +A separate tracking mechanism exists → show the user the artifacts and the +documents that reference them, then ask whether to migrate. An explicit answer +is required; silence is not consent. A declined migration preserves the +existing mechanism; a second tracker requires an explicit coexistence choice. + +A migration was approved → read `references/migration.md` in full before +running any migration command. + +Migration is a two-phase commit: the audit is the prepare phase; nothing is +deleted until it votes yes. Migration is script-gated. The deletion question MUST NOT be asked until both @@ -80,124 +54,36 @@ sign-offs cover work already performed. An empty WIP section is not evidence of an empty actionable set. - Only after `migration-audit` exits 0, show its output and ask whether to - delete the original tracking artifacts. Do not infer consent. Record a - declined deletion with `migration-finalize --decision retain`. If - deletion is approved, run `migration-finalize --decision delete`; - this reruns the audit, seals the exact sources and record fingerprint, and - **does not delete anything**. Remove only those approved sources, update - affected pointers, run pointer/link checks, then run - `migration-finalize --confirm-deleted`. - -Report the migration record's location, schema and final outcome, -`migration-audit`'s pass/fail result, the historical/reference entry counts it -disclosed, pointer files updated, and intentional legacy references. - -**Migration command reference:** - -```bash -# Scan the legacy source(s) and write/refresh /_migrations/.md. -# --source may repeat; a directory source is scanned recursively for *.md. -# If no tracker exists yet, this command scaffolds its support files without -# creating a progress item, so inventory still precedes destination creation. -# Re-running upgrades v1 records to v2 and preserves every compatible -# Disposition, Destination, and Evidence whose -# entry is unchanged. Sign-offs survive only an identical source + inventory; -# any inventory change resets all of them and prints that refresh result. -uv run /scripts/update_progress.py migration-inventory --source [--source ...] - -# Rescan and reconcile against the record — the pre-deletion gate. Exits non-zero -# while any actionable/ambiguous entry lacks a Disposition and Destination, -# any generated field was hand-edited, any migrated Destination isn't an existing item -# slug, its Evidence is absent/non-unique, or any human sign-off box is unticked. -uv run /scripts/update_progress.py migration-audit - -# Persist the user's choice. The delete decision never removes files itself. -uv run /scripts/update_progress.py migration-finalize --decision retain -uv run /scripts/update_progress.py migration-finalize --decision delete -uv run /scripts/update_progress.py migration-finalize --confirm-deleted -``` - -Every row in the generated record is one of five `Kind` values: -`actionable` and `ambiguous` block the audit until dispositioned; `done`, -`empty`, and `historical` are pre-filled and non-blocking. An unrecognized -heading becomes `ambiguous`, not `historical` — the scanner defaults to -blocking on what it doesn't recognize, never to treating it as already -covered. - -`actionable` and `ambiguous` rows accept only `migrated` or `excluded`. -`done` and `historical` rows are seeded `not-applicable` but may instead be -`migrated` or `excluded`; `empty` rows accept only `not-applicable`. Every -historical entry is retained as its own non-blocking record row. Retaining the -whole legacy source is a durable document-level outcome after the audit, not -an entry-level `archived` disposition. - --- ## Before starting work -Create the progress item with the scaffold script. Resolve `` to -the directory containing this `SKILL.md`; the script can then run from -anywhere inside the project because it locates the project root itself: +Create the progress item with the scaffold script — resolve `` to +this `SKILL.md`'s directory; the script locates the project root itself: ```bash -# Minimal — one scope entry (branch/ticket filled in later as TBD) -uv run /scripts/new_progress.py \ - --scope api \ - [--plan ] \ - [--title "Task title"] \ - [--dry-run] - -# Full — per-scope-entry branch and ticket; --ticket is the umbrella/epic reference uv run /scripts/new_progress.py \ --scope "api:feature/my-branch:JIRA-111,worker:feature/my-branch" \ --ticket EPIC-100 \ - --plan + --plan \ + [--title "Task title"] [--dry-run] ``` -Key arguments: -- `slug` — kebab-case identifier, e.g. `subscription-refund-flow` -- `--scope` — `name[:branch[:ticket]]`, comma-separated. `name` is a - free-form label — not validated against any directory. Escape a literal - comma, colon, or backslash as `\,`, `\:`, or `\\`. `branch` and - per-entry `ticket` default to `TBD` when omitted. Ticket values are kept - **verbatim** — this skill has no opinion on your tracker's numbering - convention (serial, `#123`, `JIRA-111`, a URL — all pass through as given). -- `--ticket` — umbrella/epic reference for the whole task (optional, `N/A` - if omitted). Kept verbatim. -- `--plan` — path to the associated plan file (optional but recommended when - a plan exists). The plan is **copied** into `/_plans/` as a - version-controlled `-` snapshot and linked via an explicit - relative Markdown link in `PROGRESS.md`. - - A path (absolute, or containing `/`) is validated by existence directly — - works with plan output from any tool or agent. - - A bare filename (e.g. `my-plan.md`) is resolved against - `$PROGRESS_TRACKER_PLANS_DIR`, if that env var is set. Without it, a bare - filename is an error asking for a path instead. -- `--title` — human-readable title (defaults to the slug title-cased) -- `--dir` — tracker directory path, relative to and strictly inside the - project root. Nested paths and dot-directories are allowed; absolute paths, - `.`, `..`, and symlinks that resolve outside the root are rejected. Defaults - to `$PROGRESS_TRACKER_DIR`, then `progress`. -- `--root` — project root directory. Defaults to the current git repository's - top level, falling back to the current working directory. - -If a plan for this task exists anywhere, **always** pass it via `--plan`. - -Use `--dry-run` first to preview what would be created. - -On first use in a project, the script scaffolds the tracker directory's -supporting files (`README.md`, `INDEX.md`, `_template/PROGRESS.md`, -`_plans/README.md`) from this skill's bundled references — nothing to set up -by hand. An approved migration normally scaffolds those files earlier through -`migration-inventory`, before the first destination item is created. +Argument semantics (slug format, `--scope` escaping, per-entry defaults, +`--plan` path resolution, `--dir` containment, `--root` discovery) live in +the script's `--help` — read it before first use. One rule the interface +cannot teach: if a plan for this task exists anywhere, **always** pass it +via `--plan`. + +On first use, the script scaffolds the tracker's supporting files +(`README.md`, `INDEX.md`, `_template/PROGRESS.md`, `_plans/README.md`). --- ## During work Use the lifecycle script so `PROGRESS.md` and `INDEX.md` are validated and -updated together. Preview with `--dry-run` when changing status or scope: +updated together: ```bash uv run /scripts/update_progress.py update \ @@ -208,30 +94,18 @@ uv run /scripts/update_progress.py update \ [--dry-run] ``` -All update options are optional individually, but at least one is required. -`--complete-task` may be repeated and matches the exact text of an unchecked -Task list entry. `--scope` replaces the full Scope table using the same escaped -syntax as `new_progress.py`. - -The script performs these lifecycle duties: - -1. **Back-fill `TBD` values** in `## Scope` as branches are created and tickets are opened -2. Tick off completed items in `## Task list` (`- [x]`) -3. Add a `### YYYY-MM-DD` entry under `## Work log` each day with brief notes -4. Update the **Updated** field to today -5. Update Status in both this `PROGRESS.md` and `/INDEX.md` per - the lifecycle below; the two values MUST stay identical +Option semantics live in `update --help`; invalid input fails with the +allowed values. Back-fill `TBD` scope values as branches and tickets appear, +log work daily, and keep Status identical in `PROGRESS.md` and +`/INDEX.md` per the lifecycle below. -Run an audit at any time (and before review/close-out): +Audit consistency at any time — before review/close-out and after any manual +edit: ```bash uv run /scripts/update_progress.py check [--dir ] [--root ] ``` -`check` detects invalid statuses, duplicate slugs/rows, missing or stale INDEX -rows, and status drift. Manual edits remain possible, but run `check` -afterward. - --- ## Status lifecycle (canonical) @@ -251,31 +125,18 @@ Any non-terminal status → abandoned ``` -| Status | Meaning | -|---|---| -| `planning` | Item created, implementation not started (scaffold-script default) | -| `in-progress` | Under active development | -| `review` | PR/MR opened, in code review / QA — **not** `done`; that comes after merge | -| `blocked` | Paused on an external dependency | -| `done` | Development complete (PR/MR merged) | -| `abandoned` | Stopped without completing | - -Allowed transitions are enforced by `update_progress.py`: - -| From | To | -|---|---| -| `planning` | `in-progress`, `abandoned` | -| `in-progress` | `review`, `blocked`, `abandoned` | -| `review` | `in-progress`, `done`, `abandoned` | -| `blocked` | `in-progress`, `abandoned` | -| `done`, `abandoned` | None (terminal) | +`planning` is the scaffold default. `review` means a PR/MR is open (code +review / QA) — **not** `done`, which comes only after merge. `blocked` is +paused on an external dependency; `abandoned` is stopped without completing. + +`update_progress.py` enforces exactly the transitions the diagram shows; +`done` and `abandoned` are terminal. --- ## After completing work -Close the item with the lifecycle script. `--outcome` is required; `--status` -defaults to `done` and may also be `abandoned`: +Close the item with the lifecycle script: ```bash uv run /scripts/update_progress.py close \ @@ -286,9 +147,7 @@ uv run /scripts/update_progress.py close \ [--dry-run] ``` -This fills `## Outcome`, appends a final Work log entry, updates **Updated**, -and changes both status sources in one validated operation. Run `check` after -close-out. +Run `check` after close-out. --- @@ -296,7 +155,5 @@ close-out. **Never delete current tracker items automatically.** Deleting current item folders or removing rows from `INDEX.md` is a manual human decision. A legacy -source may be deleted only after migration audits pass and the user explicitly -confirms the exact source target. Record approval with `migration-finalize ---decision delete`, remove only its confirmed sources, then record completion -with `migration-finalize --confirm-deleted` after pointer/link checks. +source may be deleted only through the migration flow's audited finalize +sequence — see `references/migration.md`. diff --git a/skills/progress-tracker/references/migration.md b/skills/progress-tracker/references/migration.md new file mode 100644 index 0000000..288aa25 --- /dev/null +++ b/skills/progress-tracker/references/migration.md @@ -0,0 +1,170 @@ +# Legacy-tracker migration + +Migration is a two-phase commit: the audit is the prepare phase; nothing is +deleted until it votes yes. + +> **Type:** How-to +> **Audience:** Developers, AI assistants +> **Last updated:** 2026-07-28 +> +> This document is the migration contract in full: discovery, consent, the +> step-by-step flow, the authoritative command reference, and the +> Kind/disposition rules. The normative gate block +> (`MIGRATION_GATE_START/END`) lives byte-identically in +> [`SKILL.md`](../SKILL.md), [`workflow.md`](workflow.md), and the bundled +> subagent definition; it is deliberately not duplicated here. +> +> **Terminology:** this document uses RFC 2119 keywords — **MUST**, +> **SHOULD**, **MAY**. + +--- + +## Discovery and consent + +Before first use, the agent **MUST** inspect the project for an existing local +tracking mechanism and for project documents, agent instructions, scripts, or +configuration that point to it (detection rules → [`SKILL.md`](../SKILL.md) +§Before creating anything). + +When a separate mechanism exists, the agent **MUST NOT** scaffold a parallel +tracker without first showing the discovered artifacts and asking whether the +user wants to migrate. Migration requires explicit approval; silence is not +consent. A declined migration preserves the existing mechanism; coexistence +also requires an explicit user choice. + +--- + +## Why the gate exists (KI-001) + +An approved migration is **script-gated**, not a judgment call: a real +migration trial once treated an empty "current work" section as proof a legacy +source held no actionable content, while a "Next steps" section and a backlog +it never inspected still held unmigrated work (see `KNOWN-ISSUE.md` KI-001 in +the repository root for the incident this contract was hardened against). +`migration-inventory` and `migration-audit` exist specifically so that +judgment call is no longer load-bearing. + +--- + +## Migration flow + +Command invocations for every step live in the +[command reference](#command-reference-authoritative) below. + +1. **Inventory first.** Run `migration-inventory` to produce a + section-by-section inventory record at `/_migrations/.md` + **before** editing anything. It scans the **whole document**, not just an + "in progress" or "current work" section, and defaults every unrecognized + heading to `ambiguous` (blocking). If the tracker does not exist yet, the + command scaffolds only its support files; create the destination item(s) + after the inventory exists. +2. **Agree on a source-to-destination mapping.** Copy only facts the source + states; leave unknowns explicit rather than inventing them. +3. **Copy, keeping the source unchanged.** Copy every actionable entry the + inventory lists into the new item(s): current status, scope, + branch/ticket/plan references, goals, unfinished tasks, current work log, + blockers, next actions, and live links. For each `actionable`/`ambiguous` + row, fill in its Disposition (`migrated`, `excluded`, or `not-applicable`) + and Destination. Every `migrated` row also needs a non-trivial, + row-specific Evidence locator copied from the destination item. +4. **Verify field by field.** For each `migrated` row, verify semantic + equivalence with wherever it landed, and tick the record's corresponding + sign-off box only once verified. Missing or unexplained active content — + any row still `TBD` — fails the audit. +5. **Update every live pointer** to the old mechanism: Markdown links, path + mentions, agent instructions, command examples, scripts, and configuration. +6. **Audit the project.** Run `update_progress.py check`, search the whole + project for every old path or filename, and verify every changed relative + link resolves. Classify any remaining match as an intentional + historical/compatibility reference, then tick the record's remaining + sign-off boxes (historical sections disclosed, pointer audit passed, link + audit passed). +7. **Run `migration-audit` — the pre-deletion gate** (the gate block in + `SKILL.md` is normative). Only after it exits 0, show its output and ask + whether to delete the original artifacts. An explicit answer is required. + Persist a declined deletion with `migration-finalize --decision retain`. + For an approved deletion, run `migration-finalize --decision delete`; it + reruns the audit, seals the exact sources and record fingerprint, and + **does not delete anything**. Remove only the approved sources, update + affected pointers, run old-reference/link audits, then run + `migration-finalize --confirm-deleted`. + +The migration is complete only when `migration-audit` exits 0, all active +content is accounted for, the two records agree, every changed pointer has +been reviewed, and the record outcome has left `pending`. The final report +lists the migration record's location, schema, and final outcome, +`migration-audit`'s pass/fail result, the historical/reference entry counts it +disclosed, pointer files updated, and intentional legacy references. + +--- + +## Command reference (authoritative) + +```bash +# Scan the legacy source(s) and write/refresh /_migrations/.md. +# --source may repeat; a directory source is scanned recursively for *.md. +# If no tracker exists yet, this command scaffolds its support files without +# creating a progress item, so inventory still precedes destination creation. +# Re-running upgrades v1 records to v2 and preserves every compatible +# Disposition, Destination, and Evidence whose +# entry is unchanged. Sign-offs survive only an identical source + inventory; +# any inventory change resets all of them and prints that refresh result. +uv run /scripts/update_progress.py migration-inventory --source [--source ...] + +# Rescan and reconcile against the record — the pre-deletion gate. Exits non-zero +# while any actionable/ambiguous entry lacks a Disposition and Destination, +# any generated field was hand-edited, any migrated Destination isn't an existing item +# slug, its Evidence is absent/non-unique, or any human sign-off box is unticked. +uv run /scripts/update_progress.py migration-audit + +# Persist the user's choice. The delete decision never removes files itself. +uv run /scripts/update_progress.py migration-finalize --decision retain +uv run /scripts/update_progress.py migration-finalize --decision delete +uv run /scripts/update_progress.py migration-finalize --confirm-deleted +``` + +--- + +## Kind values and disposition rules + +Every row in the generated record is one of five `Kind` values: +`actionable` and `ambiguous` block the audit until dispositioned; `done`, +`empty`, and `historical` are pre-filled and non-blocking. An unrecognized +heading becomes `ambiguous`, not `historical` — the scanner defaults to +blocking on what it doesn't recognize, never to treating it as already +covered. + +`actionable` and `ambiguous` rows accept only `migrated` or `excluded`. +`done` and `historical` rows are seeded `not-applicable` but may instead be +`migrated` or `excluded`; `empty` rows accept only `not-applicable`. Every +historical entry is retained as its own non-blocking record row. Retaining the +whole legacy source is a durable document-level outcome after the audit, not +an entry-level `archived` disposition. + +--- + +## ✅ Do / ❌ Don't + +✅ **Do** + +- Discover existing tracking artifacts and ask before migrating or scaffolding + a parallel tracker +- Run `migration-inventory` before copying anything — inventory the whole + source document, not just an "in progress" or "current work" section +- Dispose of every `actionable`/`ambiguous` inventory row and run + `migration-audit` before asking whether to delete the source +- Record the user's retain/delete choice with `migration-finalize` +- Audit every old path/name and changed link after an approved migration + +❌ **Don't** + +- Don't treat silence as migration consent or leave stale pointers to the old + tracking mechanism +- Don't ask to delete the source until `migration-audit` exits 0 — an empty + "in progress" section is not proof the rest of the document holds no + actionable content +- Don't hand-edit a migration record's `ID`, `Kind`, `Source`, `Loc`, + `Section`, or `Entry` cells; re-run `migration-inventory` instead +- Don't delete a `/_migrations/.md` record while its legacy + source still exists — it is the audit trail proving the migration was + complete diff --git a/skills/progress-tracker/references/workflow.md b/skills/progress-tracker/references/workflow.md index 691ef0b..74f1f88 100644 --- a/skills/progress-tracker/references/workflow.md +++ b/skills/progress-tracker/references/workflow.md @@ -2,12 +2,13 @@ > **Type:** How-to > **Audience:** Developers, AI assistants -> **Last updated:** 2026-07-26 +> **Last updated:** 2026-07-28 > > This document explains the **workflow and purpose** of the progress tracker > (why, folder structure, field semantics, cleanup policy). The single > source of truth for **operating mechanics** (script argument spec, `--plan` -> resolution rules, status enum) is [`../SKILL.md`](../SKILL.md). +> resolution rules, status enum) is [`../SKILL.md`](../SKILL.md); the +> migration contract in full is [`migration.md`](migration.md). > > **Terminology:** this document uses RFC 2119 keywords — **MUST**, > **SHOULD**, **MAY**. @@ -39,55 +40,13 @@ across sessions and machines. ## Existing-tracker discovery and migration -Before first use, the agent **MUST** inspect the project for an existing local -tracking mechanism and for project documents, agent instructions, scripts, or -configuration that point to it. Likely names include root-level `PROGRESS.md`, -`progress_note/`, `progress-notes/`, and `WORKLOG.md`, but content and documented -purpose determine whether an artifact is a tracker. A tracker already using -this skill's `/INDEX.md` structure is not a migration candidate. - -When a separate mechanism exists, the agent **MUST NOT** scaffold a parallel -tracker without first showing the discovered artifacts and asking whether the -user wants to migrate. Migration requires explicit approval. A declined -migration preserves the existing mechanism; coexistence also requires an -explicit user choice. - -An approved migration follows this contract. It is **script-gated**, not a -judgment call: a real migration trial once treated an empty "current work" -section as proof a legacy source held no actionable content, while a -"Next steps" section and a backlog it never inspected still held unmigrated -work (see `KNOWN-ISSUE.md` KI-001 in the repository root for the incident this -contract was hardened against). `migration-inventory` and `migration-audit` -exist specifically so that judgment call is no longer load-bearing. - -1. Inventory source artifacts and all live pointers to them. Run - `migration-inventory --source ` to produce a - section-by-section inventory record at `/_migrations/.md` - — this scans the **whole document**, not just an "in progress" or "current - work" section, and defaults every unrecognized heading to `ambiguous` - (blocking) rather than assuming it is safe to ignore. If the tracker does - not exist yet, this command scaffolds only its support files; create the - destination item(s) after the inventory exists. -2. Agree on a source-to-destination mapping; do not infer missing task facts. -3. Keep the source unchanged and copy every in-progress concern into the new - item(s): current status, scope, branch/ticket/plan references, goals, - unfinished tasks, current work log, blockers, next actions, and live links. - For each `actionable`/`ambiguous` row in the inventory record, fill in its - Disposition (`migrated`, `excluded`, or `not-applicable`) and - Destination. Every `migrated` row also gets a non-trivial Evidence locator - copied from the destination item. -4. Compare source and destination field by field. For each `migrated` row, - verify semantic equivalence with wherever it landed and tick the record's - corresponding sign-off box. Missing or unexplained active content — any - row still `TBD` — fails the audit. -5. Update every live pointer: links, path mentions, agent instructions, command - examples, scripts, and configuration. -6. Run `update_progress.py check`, search the project for each old path/name, - and verify every changed relative link resolves. Classify any remaining - match as an intentional historical/compatibility reference, then tick the - record's remaining sign-off boxes (historical sections disclosed, pointer - audit passed, link audit passed). -7. Run `migration-audit ` — the pre-deletion gate: +Before first use, the agent **MUST** run the existing-tracker preflight +(detection rules → [`SKILL.md`](../SKILL.md) §Before creating anything). + +A separate tracking mechanism was discovered → read +[`migration.md`](migration.md) in full before running any migration command. +That document owns discovery, consent, the KI-001 rationale, the step-by-step +flow, the authoritative command reference, and the Kind/disposition rules. Migration is script-gated. The deletion question MUST NOT be asked until both @@ -105,20 +64,6 @@ sign-offs cover work already performed. An empty WIP section is not evidence of an empty actionable set. - Only once `migration-audit` exits 0, show its output and ask whether to - delete the original artifacts. Persist a declined decision with - `migration-finalize --decision retain`. For an approved deletion, - run `migration-finalize --decision delete`; it reruns the audit, - seals the record and source list, and does not delete anything. Remove only - the approved sources, update affected pointers, run old-reference/link - audits, then use `migration-finalize --confirm-deleted`. - -The migration is not complete while `migration-audit` exits non-zero, active -content is missing, the two records disagree, an unreviewed stale pointer -remains, or the record outcome is still `pending`. The final report lists the -migration record's schema and outcome, `migration-audit`'s result, historical -entry counts, updated pointer files, and intentional legacy references. - --- ## Quick usage @@ -278,14 +223,9 @@ its own. - Scripts and AI agents **MUST NOT** proactively delete current item folders or INDEX rows. -- A migrated legacy source MAY be deleted only after `migration-audit` exits 0 - and `migration-finalize --decision delete` records the user's exact target - approval. That command does not delete files. After removal and pointer/link - checks, `migration-finalize --confirm-deleted` records completion only when - all approved sources are absent and the sealed record is unchanged. -- Do not delete a `/_migrations/.md` record while its - legacy source still exists — it is the audit trail proving the migration - was complete. +- A migrated legacy source MAY be deleted only through the audited finalize + sequence in [`migration.md`](migration.md), which also covers retention of + the migration record itself. - Other cleanup remains a **human decision**, performed manually as needed. - Consider keeping `done` / `abandoned` items around for at least one sprint for later reference and retrospectives. @@ -296,14 +236,8 @@ its own. ✅ **Do** -- Discover existing tracking artifacts and ask before migrating or scaffolding - a parallel tracker -- Run `migration-inventory` before copying anything — inventory the whole - source document, not just an "in progress" or "current work" section -- Dispose of every `actionable`/`ambiguous` inventory row and run - `migration-audit` before asking whether to delete the source -- Record the user's retain/delete choice with `migration-finalize` -- Audit every old path/name and changed link after an approved migration +- Follow [`migration.md`](migration.md) end-to-end when an existing tracking + mechanism is discovered - Preview with `--dry-run` before creating a task - Include **every scope entry involved** in `--scope`, even read-only ones - Fill in `TBD` for branch/ticket before they exist, then back-fill `## Scope` once known @@ -316,13 +250,6 @@ its own. ❌ **Don't** -- Don't treat silence as migration consent or leave stale pointers to the old - tracking mechanism -- Don't ask to delete the source until `migration-audit` exits 0 — an empty - "in progress" section is not proof the rest of the document holds no - actionable content -- Don't hand-edit a migration record's `ID`, `Kind`, `Source`, `Loc`, - `Section`, or `Entry` cells; re-run `migration-inventory` instead - Don't delete tracker-directory folders directly (update the INDEX status to `abandoned` first, then clean up manually) - Don't fill real content into `_template/PROGRESS.md` (the template should @@ -331,3 +258,5 @@ its own. - Don't pass a `slug` containing uppercase letters, underscores, or spaces (the script rejects it) - Don't use a separator other than comma between `--scope` entries (comma separates entries, colon separates name/branch/ticket within one) +- Don't put whitespace around a separating comma — the parser rejects it as + ambiguous (escape a literal comma inside one label as `\,`) diff --git a/skills/progress-tracker/scripts/new_progress.py b/skills/progress-tracker/scripts/new_progress.py index 943ff4e..db87d2e 100755 --- a/skills/progress-tracker/scripts/new_progress.py +++ b/skills/progress-tracker/scripts/new_progress.py @@ -208,13 +208,16 @@ def parse_scope(scope_arg: str) -> list[ScopeEntry]: - name is a free-form label; not validated against any directory - branch defaults to 'TBD' when omitted or empty - ticket defaults to 'TBD' when omitted; kept verbatim otherwise + - an unescaped separator comma with whitespace on either side is + rejected as ambiguous, and an empty entry (trailing, leading, or + doubled comma) is rejected — both would otherwise fail silently """ entries: list[ScopeEntry] = [] parsed_entries: list[list[str]] = [] segments = [""] escaped = False - for char in scope_arg: + for i, char in enumerate(scope_arg): if escaped: if char not in {",", ":", "\\"}: sys.exit( @@ -226,6 +229,15 @@ def parse_scope(scope_arg: str) -> list[ScopeEntry]: elif char == "\\": escaped = True elif char == ",": + prev_char = scope_arg[i - 1] if i else "" + next_char = scope_arg[i + 1] if i + 1 < len(scope_arg) else "" + if prev_char.isspace() or next_char.isspace(): + sys.exit( + f"ERROR: ambiguous --scope value {scope_arg!r}: whitespace " + "touches an unescaped comma. Entry separators take no " + "surrounding whitespace (api,worker); a literal comma inside " + "one label must be escaped (my label\\, with comma)." + ) parsed_entries.append(segments) segments = [""] elif char == ":" and len(segments) < 3: @@ -239,7 +251,16 @@ def parse_scope(scope_arg: str) -> list[ScopeEntry]: for segments in parsed_entries: if len(segments) == 1 and not segments[0].strip(): - continue + if len(parsed_entries) == 1: + sys.exit( + "ERROR: --scope produced no valid entries. " + "Provide at least one scope name." + ) + sys.exit( + f"ERROR: empty --scope entry in {scope_arg!r}: a trailing, " + "leading, or doubled comma creates an empty entry. Remove the " + "extra comma." + ) name = segments[0].strip() branch = segments[1].strip() if len(segments) > 1 else "" ticket_raw = segments[2].strip() if len(segments) > 2 else "" @@ -256,12 +277,16 @@ def parse_scope(scope_arg: str) -> list[ScopeEntry]: entries.append((name, branch, ticket)) - if not entries: - sys.exit("ERROR: --scope produced no valid entries. Provide at least one scope name.") - return entries +def scope_summary(entries: list[ScopeEntry]) -> str: + """One-line echo of the parsed scope names, so a wrong split is visible.""" + names = " · ".join(name for name, _, _ in entries) + unit = "entry" if len(entries) == 1 else "entries" + return f"{names} ({len(entries)} {unit})" + + def resolve_plan(plan_arg: str | None) -> tuple[str, Path | None]: """Resolve a plan argument to (plan_name, source_path). @@ -554,7 +579,9 @@ def build_arg_parser() -> argparse.ArgumentParser: "Comma-separated `name[:branch[:ticket]]` entries. " "name is a free-form label (service, package, repo — not validated). " "Escape literal commas, colons, and backslashes with a backslash. " - "branch defaults to TBD when omitted. " + "An unescaped comma with adjacent whitespace is rejected as " + "ambiguous, and an empty entry (leading, trailing, or doubled " + "comma) is rejected. branch defaults to TBD when omitted. " "ticket defaults to TBD when omitted; kept verbatim otherwise." ), ) @@ -712,6 +739,7 @@ def main() -> None: item_dir.mkdir(parents=True) progress_file.write_text(rendered, encoding="utf-8") print(f"Created: {progress_file}") + print(f"Scope: {scope_summary(scope_entries)}") if source_path: copy_plan(source_path, plan_name, plans_local_dir, dry_run) diff --git a/skills/progress-tracker/scripts/test_new_progress.py b/skills/progress-tracker/scripts/test_new_progress.py index 6b31894..c155270 100644 --- a/skills/progress-tracker/scripts/test_new_progress.py +++ b/skills/progress-tracker/scripts/test_new_progress.py @@ -110,8 +110,9 @@ def test_no_directory_validation(self, tmp_path, monkeypatch): assert entries[0][0] == "totally-made-up-service-name" def test_empty_scope_errors(self): - with pytest.raises(SystemExit): + with pytest.raises(SystemExit) as exc_info: np.parse_scope("") + assert "no valid entries" in str(exc_info.value) def test_empty_name_in_entry_errors(self): with pytest.raises(SystemExit): @@ -136,6 +137,39 @@ def test_unknown_escape_errors(self): with pytest.raises(SystemExit): np.parse_scope(r"api\q") + @pytest.mark.parametrize( + "value", + [ + "api, worker", # space after the separator + "api ,worker", # space before the separator + "api,\tworker", # tab counts as whitespace + "a:b, c", # inside a fielded entry + r"api\,, worker", # escaped comma, then an ambiguous real one + ], + ) + def test_whitespace_adjacent_to_comma_is_ambiguous(self, value): + with pytest.raises(SystemExit) as exc_info: + np.parse_scope(value) + assert "ambiguous" in str(exc_info.value) + + def test_padded_single_entry_is_stripped(self): + assert np.parse_scope(" api ") == [("api", "TBD", "TBD")] + + @pytest.mark.parametrize("value", ["api,", ",api", "api,,worker"]) + def test_empty_entry_from_extra_comma_errors(self, value): + with pytest.raises(SystemExit) as exc_info: + np.parse_scope(value) + assert "empty --scope entry" in str(exc_info.value) + + +class TestScopeSummary: + def test_single_entry(self): + assert np.scope_summary([("api", "TBD", "TBD")]) == "api (1 entry)" + + def test_multiple_entries(self): + entries = [("api", "feature/x", "JIRA-1"), ("worker", "TBD", "TBD")] + assert np.scope_summary(entries) == "api · worker (2 entries)" + class TestResolvePlan: def test_no_plan(self): @@ -349,6 +383,17 @@ def test_index_row_appended_with_planning_status(self, project): assert "| `planning` | Demo Task |" in index assert "`api`" in index + def test_scope_echo_in_normal_mode(self, project): + result = run_cli(["echo-task", "--scope", "api:feature/x,worker"], cwd=project) + assert result.returncode == 0, result.stderr + assert "Scope: api · worker (2 entries)" in result.stdout + + def test_ambiguous_scope_writes_nothing(self, project): + result = run_cli(["bad-task", "--scope", "api, worker"], cwd=project) + assert result.returncode != 0 + assert "ambiguous" in result.stderr + assert not (project / "progress").exists() + def test_scope_row_expansion_multi_scope(self, project): run_cli( ["multi-task", "--scope", "api:feature/x:JIRA-1,worker:feature/y"], diff --git a/skills/progress-tracker/scripts/test_update_progress.py b/skills/progress-tracker/scripts/test_update_progress.py index 308c100..254e335 100644 --- a/skills/progress-tracker/scripts/test_update_progress.py +++ b/skills/progress-tracker/scripts/test_update_progress.py @@ -127,6 +127,60 @@ def test_replaces_scope_table_with_escaped_values(self, project): assert "JIRA-1,JIRA-2" in content assert "`worker` | TBD | TBD" in content + def test_scope_replacement_echoes_parsed_entries(self, project): + result = run_script( + UPDATE_SCRIPT, + ["update", "demo-task", "--scope", "api:feature/x,worker"], + project, + ) + assert result.returncode == 0, result.stderr + assert "Scope: api · worker (2 entries)" in result.stdout + + def test_ambiguous_scope_leaves_files_unchanged(self, project): + before_progress = item_file(project).read_text() + before_index = (project / "progress" / "INDEX.md").read_text() + result = run_script( + UPDATE_SCRIPT, + ["update", "demo-task", "--scope", "api, worker"], + project, + ) + assert result.returncode != 0 + assert "ambiguous" in result.stderr + assert item_file(project).read_text() == before_progress + assert (project / "progress" / "INDEX.md").read_text() == before_index + + def test_empty_scope_entry_leaves_files_unchanged(self, project): + before_progress = item_file(project).read_text() + result = run_script( + UPDATE_SCRIPT, + ["update", "demo-task", "--scope", "api,"], + project, + ) + assert result.returncode != 0 + assert "empty --scope entry" in result.stderr + assert item_file(project).read_text() == before_progress + + def test_explicitly_empty_scope_is_rejected_not_ignored(self, project): + before_progress = item_file(project).read_text() + result = run_script( + UPDATE_SCRIPT, + ["update", "demo-task", "--scope", "", "--work-log", "note"], + project, + ) + assert result.returncode != 0 + assert "no valid entries" in result.stderr + assert item_file(project).read_text() == before_progress + + def test_empty_scope_alone_is_a_mutation_not_omitted(self, project): + result = run_script( + UPDATE_SCRIPT, + ["update", "demo-task", "--scope", ""], + project, + ) + assert result.returncode != 0 + assert "no valid entries" in result.stderr + assert "requires at least one" not in result.stderr + def test_dry_run_writes_nothing(self, project): before_progress = item_file(project).read_text() before_index = (project / "progress" / "INDEX.md").read_text() diff --git a/skills/progress-tracker/scripts/update_progress.py b/skills/progress-tracker/scripts/update_progress.py index 767076a..2d4958e 100755 --- a/skills/progress-tracker/scripts/update_progress.py +++ b/skills/progress-tracker/scripts/update_progress.py @@ -27,6 +27,7 @@ from new_progress import ( DEFAULT_TRACKER_DIRNAME, REFERENCES_DIR, + ScopeEntry, markdown_code, markdown_table_text, parse_scope, @@ -35,6 +36,7 @@ resolve_project_root, resolve_tracker_dir, scaffold_tracker_dir, + scope_summary, validate_scaffold, validate_single_line, validate_slug, @@ -409,8 +411,7 @@ def replace_metadata(content: str, field: str, value: str, path: Path) -> str: return updated -def replace_scope_table(content: str, scope_arg: str, path: Path) -> str: - entries = parse_scope(scope_arg) +def replace_scope_table(content: str, entries: list[ScopeEntry], path: Path) -> str: start = content.find(SCOPE_TABLE_HEADER) if start == -1: sys.exit(f"ERROR: Scope table header not found in {path}") @@ -566,7 +567,7 @@ def prepare_item(args: argparse.Namespace) -> tuple[Path, str, Path, str, list[s def run_mutation(args: argparse.Namespace) -> int: if args.command == "update" and not any( - (args.status, args.scope, args.work_log, args.complete_task) + (args.status, args.scope is not None, args.work_log, args.complete_task) ): sys.exit( "ERROR: update requires at least one of --status, --scope, " @@ -590,8 +591,10 @@ def run_mutation(args: argparse.Namespace) -> int: progress_after = progress_before if target_status != current_status: progress_after = replace_metadata(progress_after, "Status", target_status, progress_path) - if args.command == "update" and args.scope: - progress_after = replace_scope_table(progress_after, args.scope, progress_path) + scope_entries = None + if args.command == "update" and args.scope is not None: + scope_entries = parse_scope(args.scope) + progress_after = replace_scope_table(progress_after, scope_entries, progress_path) if args.command == "update" and args.complete_task: progress_after = complete_tasks(progress_after, args.complete_task, progress_path) work_log = args.work_log @@ -630,6 +633,8 @@ def run_mutation(args: argparse.Namespace) -> int: print(f"Updated: {progress_path}") if index_before != index_after: print(f"Updated: {index_path}") + if scope_entries is not None: + print(f"Scope: {scope_summary(scope_entries)}") print(f"Status: {target_status}") return 0