enhance(#2142): archive quick tasks at milestone close-out (#3592)

* test(#2142): failing-first coverage for quick-task archival at milestone close-out

* enhance(#2142): archive quick tasks at milestone close-out

* fix(#2142): resolve review findings — readme injection, move/reset ordering, owned state write

* fix(#2142): fold archival under milestone namespace, expose index IR, dedupe reset decision

* test(#2142): assert archive-dir-relative summary path in index IR

* docs(#2142): backfill changeset pr number to 3592

* test(#2142): skip newline-fixture injection test on windows (control chars illegal in path names)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-17 14:51:00 -04:00
committed by GitHub
parent b08af152e4
commit 98ecb2ba8c
18 changed files with 1776 additions and 22 deletions

View File

@@ -680,7 +680,10 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```bash
# Archive milestone
node gsd-tools.cjs milestone complete <version> [--name <name>] [--no-archive-phases] [--force] [--dry-run]
node gsd-tools.cjs milestone complete <version> [--name <name>] [--no-archive-phases] [--force] [--dry-run] [--archive-quick]
# Archive .planning/quick/* into milestones/<version>-quick/ WITHOUT the milestone complete close-out (#2142)
node gsd-tools.cjs milestone archive-quick <version> [--dry-run]
# Mark requirements as complete
node gsd-tools.cjs requirements mark-complete <ids>
@@ -694,13 +697,27 @@ node gsd-tools.cjs requirements mark-complete <ids>
| `<version>` | Milestone version label to archive (e.g. `v1.0`). |
| `--name <name>` | Display name for the MILESTONES.md entry. Defaults to `<version>`. |
| `--no-archive-phases` | Leave phase directories in place instead of moving them into `.planning/milestones/<version>-phases/`. |
| `--archive-quick` | Opt-in (default OFF, #2142): also move every directory under `.planning/quick/` into `.planning/milestones/<version>-quick/`, (re)write that archive directory's `README.md` index, and clear STATE.md's `### Quick Tasks Completed` table rows. See "`milestone archive-quick`" below for the narrower standalone form and the full behavior. |
| `--force` | Override the unstarted-phase guard (see below). |
| `--dry-run` | Print the archive plan (roadmap, requirements, phases to move) without mutating anything. |
| `--dry-run` | Print the archive plan (roadmap, requirements, phases, and — when `--archive-quick` is also passed — quick-task dirs to move) without mutating anything. |
**Unstarted-phase guard.** Before archiving, the command scans the ROADMAP scoped for `<version>` and refuses if any `### Phase N:` heading in that slice has no matching phase directory on disk (`disk_status: no_directory`). Phase 0 (pre-milestone) and Phase 999 (backlog) sentinels are excluded. The guard runs whenever `--force` is absent, independent of `STATE.md`'s `milestone:` field — if that field is present but does not match `<version>`, a WARNING naming both values is emitted to stderr and the scan still runs (#2946). Pass `--force` to override.
**Sentinel directories are never archived.** The phase-directory move performed when `--no-archive-phases` is absent is now filtered through the same canonical sentinel predicate as `phases list` and `phases clear`: `999.*` (backlog) and `0-*` (pre-milestone) directories are left in place rather than moved into `.planning/milestones/<version>-phases/`. Previously this path was scoped only by the milestone window, with no sentinel filter, so a sentinel directory sitting inside the window could be archived along with the milestone's real phases.
**`milestone archive-quick` (#2142 escalation)**
A narrower sibling of `milestone complete --archive-quick`, for callers that need to sweep `.planning/quick/*` WITHOUT the full milestone close-out — chiefly `gsd-core/workflows/cleanup.md`, which runs against milestones that are typically already completed.
| Flag | Description |
|------|-------------|
| `<version>` | Milestone version label to archive quick-task directories under (e.g. `v1.0`). Same validation as `milestone complete`'s `<version>` — letters/digits/`.`/`-`/`_` only, no path separators or `..`. |
| `--dry-run` | List what would move (`would_archive`) without mutating anything. |
It moves every directory under `.planning/quick/` into `.planning/milestones/<version>-quick/`, (re)writes that archive directory's `README.md` index, and clears STATE.md's `### Quick Tasks Completed` table rows — the same move/index/reset logic `milestone complete --archive-quick` uses. Unlike `milestone complete`, it never archives `ROADMAP.md`/`REQUIREMENTS.md`, never writes a `MILESTONES.md` entry, and runs neither the unstarted-phase guard nor the milestone-window refusal — so, unlike `milestone complete --archive-quick`, it can be safely re-run against an already-completed milestone. JSON result: `{ version, archived, entries, archive_dir, state_updated, warnings }`.
`milestone archive-quick` is a second subcommand of `milestone` (alongside `complete`) — it is not a separate top-level command.
---
## Agent Skills
@@ -771,6 +788,8 @@ node gsd-tools.cjs verify-path-exists <path>
# Append a row to STATE.md's "Quick Tasks Completed" table (schema-backed; #2133)
node gsd-tools.cjs quick-tasks-append --task "<description>"
# See "Milestone Commands" below for `milestone archive-quick` (#2142) — sweeps .planning/quick/* into
# milestones/<version>-quick/ and clears this table, without a full `milestone complete`.
# Aggregate all SUMMARY.md data
node gsd-tools.cjs history-digest

View File

@@ -500,6 +500,8 @@ The marker never overwrites the artifact's own `status:` field for the eight fro
> **Sentinel directories stay put.** Moving phase directories into the archive (the default, unless `--no-archive-phases` is passed) now excludes `999.*` (backlog) and `0-*` (pre-milestone) directories via the same sentinel predicate the unstarted-phase guard already uses. Previously the archive move was scoped only by the milestone window, so a sentinel directory sitting inside that window could be archived along with the milestone's own phases.
> **Quick-task archival (opt-in, default OFF, #2142).** Unlike phase archival above, quick-task archival does not run unless you say yes — doing nothing leaves `.planning/quick/` untouched. If `.planning/quick/` contains at least one directory, the workflow asks: `Archive completed quick tasks into this milestone too?` with options `Yes — archive quick tasks into v[X.Y]` / `Skip`. Choosing "Yes" passes `--archive-quick` to the underlying `gsd-tools milestone complete` call, which moves every directory under `.planning/quick/` into `.planning/milestones/v[X.Y]-quick/`, (re)writes that directory's `README.md` index (built by scanning the archive directory, not STATE.md), and clears the data rows of STATE.md's `### Quick Tasks Completed` table while preserving its header and column variant. **Known limit:** there is no on-disk record of which milestone a quick task belongs to, so archival buckets **all** remaining `.planning/quick/*` into the one milestone being completed — a task predating an earlier, unarchived milestone lands in the current bucket regardless. See [Archiving quick tasks](how-to/handle-quick-and-fast-tasks.md#archiving-quick-tasks) for the full walkthrough, including the retroactive path.
---
### `/gsd-milestone-summary`
@@ -1052,10 +1054,12 @@ covers only orphan worktrees, with the stale-worktree case moving to the new
### `/gsd-cleanup`
Archive accumulated phase directories from completed milestones and prune local branches whose upstream has been deleted.
Archive accumulated phase directories from completed milestones, prune local branches whose upstream has been deleted, and — when applicable — retroactively archive quick tasks (#2142).
**Behaviour:** Presents a dry-run summary of phase directories to archive (moved from `.planning/phases/` into `.planning/milestones/v{X.Y}-phases/`) and local branches whose upstream is gone (pruned via `git fetch --prune`). Requires confirmation before writing any changes. The currently checked-out branch is never pruned.
**Retroactive quick-task archival (opt-in, #2142).** When `.planning/quick/` contains at least one directory, `/gsd-cleanup` additionally offers to sweep it: `Archive ALL {N} quick-task directories into v{X.Y} — {Milestone Name}? This buckets every remaining quick task into this ONE milestone; there is no way to split them per-milestone.` with options `Yes — archive quick tasks into v{X.Y}` / `Skip`. The target is the single most recent completed milestone (from `MILESTONES.md`) that does not yet have a `v{X.Y}-quick` archive directory. If `.planning/quick/` is empty, this step is not offered at all. Confirming calls the narrower `gsd-tools milestone archive-quick <version>` command — the same move/README-index/table-reset logic `/gsd-complete-milestone`'s `--archive-quick` uses, but without touching `ROADMAP.md`, `REQUIREMENTS.md`, `MILESTONES.md`, or milestone-completion guards, since `/gsd-cleanup` typically targets a milestone that is already closed. See [Archiving quick tasks](how-to/handle-quick-and-fast-tasks.md#archiving-quick-tasks) for the full walkthrough and the silent/failure cases.
```bash
/gsd-cleanup
```

View File

@@ -182,6 +182,9 @@
- [External-Job Capability](#155-external-job-capability)
- [API-Coverage Gate](#156-api-coverage-gate)
- [State Rebuild & Configurable Graph Path](#157-state-rebuild--configurable-graph-path)
- [Broken-Windows Ledger](#158-broken-windows-ledger)
- [Complexity-Triggered Refactor](#159-complexity-triggered-refactor)
- [Archive Quick Tasks at Milestone Close](#160-archive-quick-tasks-at-milestone-close)
---
@@ -3384,3 +3387,24 @@ The load-bearing wire is the `plan-phase` lift into `must_haves.prohibitions`, s
**Config:** `refactor.trigger_enabled` (master gate, default `false`), `refactor.complexity_threshold` (default `15`), `refactor.complexity_jump_delta` (default `5`), `refactor.trigger_strict` (default `false`). See [Configuration Reference](CONFIGURATION.md#refactor-trigger-settings).
**Backward compatibility:** Off by default. When `refactor.trigger_enabled` is `false` the hook never runs and writes nothing; a project that never enables it is completely unaffected.
---
### 160. Archive Quick Tasks at Milestone Close
**Command:** `/gsd-complete-milestone` (forward path), `/gsd-cleanup` (retroactive path), `gsd-tools milestone complete --archive-quick` / `gsd-tools milestone archive-quick <version>` (#2142)
**Behavior:** `.planning/quick/` otherwise accumulates one directory per `/gsd-quick` task forever. `/gsd-complete-milestone` now offers a Yes/Skip prompt — when accepted, it moves every directory under `.planning/quick/` into `.planning/milestones/<version>-quick/`, (re)writes that archive directory's `README.md` (an index built by scanning the archive directory, one entry per task, linked to its `SUMMARY.md` when one exists), and clears the data rows of `STATE.md`'s `### Quick Tasks Completed` table while preserving its header and detected column variant. `/gsd-cleanup` offers the same archival retroactively, for milestones that were already closed before their quick tasks were swept, via the narrower `milestone archive-quick <version>` command — identical move/index/reset behavior, but without touching `ROADMAP.md`, `REQUIREMENTS.md`, `MILESTONES.md`, or milestone-completion guards, so it can be re-run safely against an already-completed milestone.
**Why opt-in.** Phase-directory archival is default-ON (#1871) — omitting a phase directory from an archive would silently leave stale execution history in the way of the next milestone's roadmap. Quick tasks carry no such downstream conflict, so archival here defaults OFF: a user who never passes `--archive-quick` sees zero behavior change. This is a deliberate asymmetry with phase archival, not an oversight.
**Why bucket-all, not per-milestone.** `.planning/quick/` is a flat directory with no on-disk record of which milestone a given task belongs to. Splitting tasks per milestone was considered and rejected — inferring provenance from dates (creation time vs. a milestone's shipped date) is a proxy, not a fact, and a wrong inference on a one-way `mv` is silently irreversible. Archival instead buckets everything currently in `.planning/quick/` into the one milestone being completed (or, on the retroactive path, the one milestone chosen), and says so in the confirmation prompt.
**Why the index is built from disk, not from `STATE.md`'s table.** The `### Quick Tasks Completed` table is a running log a workflow step appends to — it demonstrably drifts from what's actually in `.planning/quick/` (the motivating case: 53 rows against 49 directories, ~22 rows pointing at directories that no longer existed, 18 directories with no row at all). Building the archive's `README.md` index by scanning the archive directory itself, rather than trusting the table, means the index can never inherit that drift; a re-run's index also naturally includes entries a prior run already archived, since it's re-derived from what's physically present.
**Known limits:**
- No per-milestone provenance — bucket-all is the only option (see above).
- A `### Quick Tasks Completed` table whose columns match neither registered variant (with/without a Status column) is left untouched with a warning rather than reset, since clearing it would risk destroying rows under a schema GSD doesn't recognize.
- A `STATE.md` with no `### Quick Tasks Completed` section at all is a normal, silent no-op for the reset step — the section is created lazily by `/gsd-quick`, not present in the project template.
See [Archiving quick tasks](how-to/handle-quick-and-fast-tasks.md#archiving-quick-tasks) for the full walkthrough.

View File

@@ -113,6 +113,59 @@ The key distinction is subagent isolation. `/gsd-quick` spawns a fresh planner a
---
## Archiving quick tasks
`.planning/quick/` accumulates one directory per `/gsd-quick` task forever unless you archive it. Archival is **opt-in** (#2142) — it deliberately does not mirror phase-directory archival, which is default-ON. Do nothing and `.planning/quick/` stays exactly as it is.
There are two paths in, depending on when you archive:
### Forward path — archive at milestone close-out
When you run `/gsd-complete-milestone` and `.planning/quick/` has at least one directory, you are asked:
```text
Archive completed quick tasks into this milestone too?
Yes — archive quick tasks into v[X.Y]
Skip
```
Choosing "Yes" folds `--archive-quick` into the same `gsd-tools milestone complete` call that archives `ROADMAP.md`/`REQUIREMENTS.md` and phase directories — one command, one `STATE.md` write. "Skip" (or an empty `.planning/quick/`) leaves everything untouched.
### Retroactive path — archive an already-completed milestone
`/gsd-complete-milestone` only runs once per milestone, so if quick tasks piled up after you already closed one out, use `/gsd-cleanup` instead. When it finds `.planning/quick/` non-empty, it offers to sweep it into the most recent completed milestone that doesn't yet have a `-quick` archive:
```text
Archive ALL {N} quick-task directories into v{X.Y} — {Milestone Name}?
This buckets every remaining quick task into this ONE milestone;
there is no way to split them per-milestone.
Yes — archive quick tasks into v{X.Y}
Skip
```
Under the hood this calls the narrower `gsd-tools milestone archive-quick <version>` command — the same move/index/reset logic as `--archive-quick`, but without touching `ROADMAP.md`, `REQUIREMENTS.md`, `MILESTONES.md`, or milestone-completion guards, so it is safe to run against a milestone that's already closed.
### What both paths do
1. Move every directory under `.planning/quick/` into `.planning/milestones/<version>-quick/`.
2. (Re)write that archive directory's `README.md` — a plain list, one entry per archived task directory, linking to its `<dir>-SUMMARY.md` (or legacy bare `SUMMARY.md`) when one exists, listed unlinked when it doesn't. The index is built by scanning the archive directory itself, never from `STATE.md`'s table — the table is known to drift from disk, so the filesystem is the only source of truth here.
3. Clear the data rows of `STATE.md`'s `### Quick Tasks Completed` table, preserving its header row and whichever column variant (with or without a Status column) was detected.
**Bucket-all, not per-milestone.** Quick tasks carry no on-disk record of which milestone they belong to, so every remaining `.planning/quick/*` directory lands in the ONE milestone you're archiving into — including tasks that predate an earlier, unarchived milestone. There's no way to split them after the fact; this is a known limit, not a bug.
### Telling "nothing to archive" from "refused to touch it"
Four cases look similar from the outside but mean different things:
| What you see | Meaning |
|---|---|
| No archive prompt at all | `.planning/quick/` is absent or empty — nothing to archive, nothing reported |
| Archive prompt appears, directories move, but `STATE.md` doesn't change | Your `STATE.md` has no `### Quick Tasks Completed` section yet — normal, since `/gsd-quick` creates that section lazily on first completion, not the project template. This is a silent no-op, not a failure |
| Directories move, but you see a `warnings` entry naming `quick_tasks_table` | Your `### Quick Tasks Completed` table has a column set that matches neither registered variant. The reset is **refused** — every row is preserved untouched rather than risk destroying data under a schema GSD doesn't recognize |
| Fewer directories moved than existed in `.planning/quick/` | A rename failed partway through. The result's `archived` count reflects exactly what succeeded — never a false full count — and the directories that didn't move are still in `.planning/quick/` for a retry |
---
## Related
- [The phase loop](../explanation/the-phase-loop.md)