@tianhai/pi-workflow-kit 2.8.0 β†’ 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The progress file's structural slots, written in one place.
4
+ *
5
+ * Every command validates the whole table before touching the file and writes atomically
6
+ * (a temp file plus a rename), so a refusal leaves the file byte-identical. There is no
7
+ * bypass flag: the point of the helper is that a hand edit is no longer needed.
8
+ *
9
+ * Usage:
10
+ * node progress.mjs set --file <progress.md> --phase <value>
11
+ * node progress.mjs set-setup --file <progress.md> --value <pending|done|n/a>
12
+ * node progress.mjs set-row --file <progress.md> --requirement <n> [--done <glyph>] [--commit <hash>]
13
+ * node progress.mjs append-summary --file <progress.md> --requirement <n> --built <text> [--deviated <text>] [--evidence <text>]
14
+ */
15
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { parseArgs } from "node:util";
17
+
18
+ const DONE_GLYPHS = ["⬜", "πŸ”„", "βœ…", "❌", "⏭"];
19
+ const SETUP_VALUES = ["pending", "done", "n/a"];
20
+ const PHASE = /^(e2e-written|reviewing|ship-paused|done|implementing \((\d+)\/(\d+)\))$/;
21
+
22
+ const REQUIREMENT_HEADER = "| # | Done | Requirement | Per-req ceremony | Commit |";
23
+ const SUMMARY_HEADER = "| R# | Requirement | How it was built | Deviated? | Evidence |";
24
+
25
+ /** Every row of a markdown table, with its line index and its trimmed cells. */
26
+ function tableRows(lines, header) {
27
+ const start = lines.indexOf(header);
28
+ if (start === -1) return null;
29
+ const rows = [];
30
+ for (let i = start + 2; i < lines.length; i += 1) {
31
+ const line = lines[i];
32
+ if (!line.startsWith("|")) break;
33
+ rows.push({
34
+ index: i,
35
+ // Split on unescaped pipes only, so prose the helper escaped as \| survives a reparse.
36
+ cells: line
37
+ .split(/(?<!\\)\|/)
38
+ .slice(1, -1)
39
+ .map((cell) => cell.trim()),
40
+ });
41
+ }
42
+ return rows;
43
+ }
44
+
45
+ function fail(message) {
46
+ console.error(`progress.mjs: ${message}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ /** Parse and validate the whole file before any command runs. */
51
+ function load(filePath) {
52
+ let text;
53
+ try {
54
+ text = readFileSync(filePath, "utf8");
55
+ } catch (error) {
56
+ fail(`cannot read ${filePath}: ${error.message}`);
57
+ }
58
+ const lines = text.split("\n");
59
+ const phaseIndex = lines.findIndex((line) => line.startsWith("Feature phase:"));
60
+ if (phaseIndex === -1) fail(`${filePath}: no Feature phase line`);
61
+ const setupIndex = lines.findIndex((line) => line.startsWith("Setup:"));
62
+ if (setupIndex === -1) fail(`${filePath}: no Setup line`);
63
+ const lastUpdatedIndex = lines.findIndex((line) => line.startsWith("Last updated:"));
64
+ if (lastUpdatedIndex === -1) fail(`${filePath}: no Last updated line`);
65
+ const requirements = tableRows(lines, REQUIREMENT_HEADER);
66
+ if (!requirements) fail(`${filePath}: no Requirements table with its header row`);
67
+ const summary = tableRows(lines, SUMMARY_HEADER);
68
+ if (!summary) fail(`${filePath}: no Execution summary table with its header row`);
69
+ for (const row of requirements) {
70
+ if (row.cells.length !== 5) {
71
+ fail(`${filePath}: line ${row.index + 1} is not a five-cell requirement row`);
72
+ }
73
+ }
74
+ for (const row of summary) {
75
+ if (row.cells.length !== 5) {
76
+ fail(`${filePath}: line ${row.index + 1} is not a five-cell summary row`);
77
+ }
78
+ }
79
+ return { filePath, lines, phaseIndex, setupIndex, lastUpdatedIndex, requirements, summary };
80
+ }
81
+
82
+ const findRow = (rows, requirement, filePath, label) => {
83
+ const row = rows.find((candidate) => candidate.cells[0] === String(requirement));
84
+ if (!row) fail(`${filePath}: no ${label} row for requirement ${requirement}`);
85
+ return row;
86
+ };
87
+
88
+ const requirementRow = (cells) => `| ${cells.join(" | ")} |`;
89
+ const stamp = () => new Date().toISOString();
90
+
91
+ function save(state) {
92
+ state.lines[state.lastUpdatedIndex] = `Last updated: ${stamp()}`;
93
+ const temporary = `${state.filePath}.tmp`;
94
+ writeFileSync(temporary, state.lines.join("\n"));
95
+ renameSync(temporary, state.filePath);
96
+ }
97
+
98
+ const { values, positionals } = (() => {
99
+ try {
100
+ return parseArgs({
101
+ args: process.argv.slice(2),
102
+ allowPositionals: true,
103
+ options: {
104
+ file: { type: "string" },
105
+ phase: { type: "string" },
106
+ value: { type: "string" },
107
+ requirement: { type: "string" },
108
+ done: { type: "string" },
109
+ commit: { type: "string" },
110
+ built: { type: "string" },
111
+ deviated: { type: "string" },
112
+ evidence: { type: "string" },
113
+ },
114
+ });
115
+ } catch (error) {
116
+ fail(error.message);
117
+ }
118
+ })();
119
+
120
+ const command = positionals[0];
121
+ if (!command || !values.file) {
122
+ fail("usage: progress.mjs <set|set-setup|set-row|append-summary> --file <progress.md> …");
123
+ }
124
+ const state = load(values.file);
125
+
126
+ if (command === "set") {
127
+ const phase = values.phase ?? "";
128
+ const match = PHASE.exec(phase);
129
+ if (!match) {
130
+ fail(
131
+ `${values.file}: phase "${phase}" is not one of e2e-written, implementing (k/N), reviewing, ship-paused, done`,
132
+ );
133
+ }
134
+ if (match[2] !== undefined && Number(match[2]) > Number(match[3])) {
135
+ fail(
136
+ `${values.file}: phase "implementing (${match[2]}/${match[3]})" requires k <= N β€” accepted: e2e-written, implementing (k/N), reviewing, ship-paused, done`,
137
+ );
138
+ }
139
+ state.lines[state.phaseIndex] = `Feature phase: ${phase}`;
140
+ } else if (command === "set-setup") {
141
+ const value = values.value ?? "";
142
+ if (!SETUP_VALUES.includes(value)) {
143
+ fail(`${values.file}: setup "${value}" is not one of ${SETUP_VALUES.join(", ")}`);
144
+ }
145
+ state.lines[state.setupIndex] = `Setup: ${value}`;
146
+ } else if (command === "set-row") {
147
+ const row = findRow(state.requirements, values.requirement, values.file, "requirement");
148
+ const cells = [...row.cells];
149
+ if (values.done !== undefined) {
150
+ if (!DONE_GLYPHS.includes(values.done)) {
151
+ fail(`${values.file}: done "${values.done}" is not one of ${DONE_GLYPHS.join(" ")}`);
152
+ }
153
+ cells[1] = values.done;
154
+ }
155
+ if (values.commit !== undefined) {
156
+ const existing = cells[4] === "β€”" ? "" : cells[4];
157
+ const recorded = existing.split(/\s+/).filter(Boolean);
158
+ if (recorded.includes(values.commit)) fail(`${values.file}: commit ${values.commit} is already recorded`);
159
+ cells[4] = [...recorded, values.commit].join(" ");
160
+ }
161
+ state.lines[row.index] = requirementRow(cells);
162
+ } else if (command === "append-summary") {
163
+ const row = findRow(state.summary, values.requirement, values.file, "execution summary");
164
+ const [, , built, deviated, evidence] = row.cells;
165
+ if (built || deviated || evidence) {
166
+ fail(`${values.file}: requirement ${values.requirement} already has an execution summary`);
167
+ }
168
+ const cellText = (text) => String(text ?? "β€”").replace(/\|/g, "\\|");
169
+ state.lines[row.index] = requirementRow([
170
+ row.cells[0],
171
+ row.cells[1],
172
+ cellText(values.built),
173
+ cellText(values.deviated),
174
+ cellText(values.evidence),
175
+ ]);
176
+ } else {
177
+ fail(`unknown command "${command}"`);
178
+ }
179
+
180
+ save(state);
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Print one anchored section of a reference file, so a step loads the section it needs
4
+ * instead of the whole document.
5
+ *
6
+ * The anchor is the heading slug: lowercased, spaces to dashes, punctuation dropped.
7
+ * The section runs from its heading to the next heading of the same or higher level.
8
+ *
9
+ * Usage: node ref.mjs <reference-path>#<anchor>
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+
13
+ const HEADING = /^(#{1,6})\s+(.+?)\s*$/;
14
+
15
+ /** The heading slug a markdown link anchor is written against. */
16
+ export function slug(heading) {
17
+ return heading
18
+ .toLowerCase()
19
+ .replace(/[^\p{L}\p{N}\s-]/gu, "")
20
+ .trim()
21
+ .replace(/\s+/g, "-");
22
+ }
23
+
24
+ /** Every heading in a document, with its level, slug, and line index. */
25
+ export function headings(lines) {
26
+ const found = [];
27
+ for (const [index, line] of lines.entries()) {
28
+ const match = HEADING.exec(line);
29
+ if (match) found.push({ index, level: match[1].length, slug: slug(match[2]), text: match[2] });
30
+ }
31
+ return found;
32
+ }
33
+
34
+ export function section(lines, anchor) {
35
+ const all = headings(lines);
36
+ const start = all.find((heading) => heading.slug === anchor);
37
+ if (!start) return null;
38
+ const next = all.find((heading) => heading.index > start.index && heading.level <= start.level);
39
+ const body = lines.slice(start.index, next ? next.index : lines.length);
40
+ while (body.length && body[body.length - 1].trim() === "") body.pop();
41
+ return { text: body.join("\n"), available: all.map((heading) => heading.slug) };
42
+ }
43
+
44
+ function fail(message) {
45
+ console.error(`ref.mjs: ${message}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
50
+ const target = process.argv[2];
51
+ if (!target) fail("usage: ref.mjs <reference-path>#<anchor>");
52
+ const hash = target.lastIndexOf("#");
53
+ if (hash === -1 || hash === target.length - 1) fail(`${target}: pass an anchor, as <reference-path>#<anchor>`);
54
+ const path = target.slice(0, hash);
55
+ const anchor = target.slice(hash + 1);
56
+ let text;
57
+ try {
58
+ text = readFileSync(path, "utf8");
59
+ } catch (error) {
60
+ fail(`cannot read ${path}: ${error.message}`);
61
+ }
62
+ const found = section(text.split("\n"), anchor);
63
+ if (!found) {
64
+ const available =
65
+ headings(text.split("\n"))
66
+ .map((heading) => heading.slug)
67
+ .join(", ") || "(none)";
68
+ fail(`${path}: no heading matches "#${anchor}" β€” available: ${available}`);
69
+ }
70
+ console.log(found.text);
71
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail when the working tree holds changes outside the plan folder.
4
+ *
5
+ * The reviewed diff is a commit range, so anything uncommitted outside docs/plans/ is
6
+ * invisible to the review and can ship unreviewed. The plan folder is exempt: the design,
7
+ * the progress file, and the generated packet are legitimately dirty while a step is in
8
+ * flight. A root that cannot be verified is a failure, never a silent pass.
9
+ *
10
+ * Usage: node stage-check.mjs [root]
11
+ */
12
+ import { spawnSync } from "node:child_process";
13
+ import { resolve } from "node:path";
14
+
15
+ const ALLOWED = "docs/plans/";
16
+ const root = resolve(process.argv[2] ?? process.cwd());
17
+
18
+ const probe = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, encoding: "utf8" });
19
+ if (probe.status !== 0 || probe.stdout.trim() !== "true") {
20
+ console.error(`stage-check.mjs: ${root} is not a git worktree β€” nothing can be verified`);
21
+ process.exit(1);
22
+ }
23
+
24
+ // -uall: git otherwise collapses an untracked directory to "dir/", and the point of the
25
+ // failure is to name the file that would miss the reviewed diff.
26
+ const status = spawnSync("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8" });
27
+ if (status.status !== 0) {
28
+ console.error(`stage-check.mjs: git status failed in ${root}: ${status.stderr.trim()}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const unquote = (path) => path.replace(/^"(.*)"$/, "$1");
33
+ const offenders = [];
34
+ for (const line of status.stdout.split("\n")) {
35
+ if (!line.trim()) continue;
36
+ const entry = unquote(line.slice(3).trim());
37
+ const path = entry.includes(" -> ") ? entry.split(" -> ").at(-1).trim() : entry;
38
+ if (!path.startsWith(ALLOWED)) offenders.push(path);
39
+ }
40
+
41
+ if (offenders.length) {
42
+ for (const path of offenders) console.error(`stage-check.mjs: uncommitted outside ${ALLOWED} β€” ${path}`);
43
+ console.error(
44
+ `stage-check.mjs: ${offenders.length} path(s) would be missing from the reviewed diff; commit or remove them first`,
45
+ );
46
+ process.exit(1);
47
+ }
48
+ console.log(`stage-check.mjs: nothing uncommitted outside ${ALLOWED}`);
@@ -7,35 +7,44 @@ description: "Use after all requirements are complete to archive or delete consu
7
7
 
8
8
  Ship the completed work.
9
9
 
10
- Any command or tool recipe in this skill is an example, not the requirement β€” when your host grants a better tool for an operation (a sandboxed batch executor, a structured file API), use the better tool and treat the recipe as a fallback; the only exact exceptions are this kit's `scripts/` helpers and plain `git` invocations, whose deterministic output is the point.
10
+ Any command or tool recipe in this skill is an example, not the requirement β€” when your host grants a better tool for an operation (a sandboxed batch executor β€” e.g. `codemode` on pi β‰₯ 1.0.0 β€” a structured file API), use the better tool and treat the recipe as a fallback; the only exact exceptions are this kit's `scripts/` helpers and plain `git` invocations, whose deterministic output is the point.
11
11
 
12
12
  ## Pre-finalization checks
13
13
 
14
14
  1. **Verify the repo root** β€” run `pwd` (or your shell's equivalent) and `git rev-parse --show-toplevel`; mismatch β†’ report both paths and stop; never `cd` (a worktree root counts).
15
- 2. **Run the FULL test suite** β€” every test must pass, and only a green suite ships. Resume spans sessions; re-run the suite yourself rather than trust the previous session's ending state. Anything failing β†’ send the user back to `/skill:pwk-executing-tasks`.
16
- 3. Read **every** relevant progress file β€” for a multi-leaf topic that's each leaf's progress file β€” discovered by listing `docs/plans` recursively, excluding docs/plans/completed/, for `*-progress.md` (topic docs live in `docs/plans/<date>-<topic>/` folders β€” archived work is not in flight). Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`. More than one topic in flight? Ask which one this finalize ships β€” finalizing touches only its own topic, and another topic still being designed or executed never blocks this run. Then match, in the chosen topic's file(s), the `Feature phase:` line (e.g. `grep -m1 '^Feature phase:' <file>`) and read only the matching ❌/⏭ verdict rows in the Requirements table (e.g. `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep '^| [0-9].*❌'` / `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep '^| [0-9].*⏭'` β€” scoped to Requirements-table rows because the Execution summary rows share the same row shape, with reasons); the sections after the tables carry nothing the gate needs. Apply the gates below to the chosen topic's file(s) and no others:
17
- - **Any `❌ failed`** (in any leaf of the topic) β†’ **block**. Present counts and reasons; continue only when the user sends the task back to executing-tasks, or explicitly types `--force-failed` to acknowledge shipping with incomplete requirements.
15
+ 2. **Run the FULL test suite** β€” only a green suite ships; re-run it yourself rather than trusting the previous session's ending state. Anything failing β†’ send the user back to `/skill:pwk-executing-tasks`.
16
+ 3. Read **every** relevant progress file β€” for a multi-leaf topic that's each leaf's progress file β€” discovered by listing `docs/plans` recursively, excluding docs/plans/completed/, for `*-progress.md` (topic docs live in `docs/plans/<date>-<topic>/` folders β€” archived work is not in flight). Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`. With a sandboxed batch executor (e.g. `codemode` on pi β‰₯ 1.0.0) the listing and every topic’s `Feature phase:` extraction are one call, returning only the summary lines (adapt the paths to the verified root when your cwd differs):
17
+
18
+ ```javascript
19
+ const designs = (await tools.bash({ command: "find docs/plans -name '*-design.md' -not -path '*/completed/*' | sort", timeout: 15 })).output.trim();
20
+ const files = designs ? designs.split('\n') : [];
21
+ const shellq = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
22
+ const heads = await Promise.all(files.map(d =>
23
+ tools.bash({ command: "grep -m1 '^Feature phase:' " + shellq(d.replace('-design.md', '-progress.md')), timeout: 15 }).then(r => r.output.trim() || 'design')));
24
+ return text(files.map((d, i) => d + ' β€” ' + heads[i]).join('\n') || 'no in-flight topics');
25
+ ```
26
+
27
+ More than one topic in flight? Ask which one this finalize ships β€” finalizing touches only its own topic, and another topic being designed or executed never blocks this run. Then match the chosen topic's `Feature phase:` line (e.g. `grep -m1 '^Feature phase:' <file>`) and read only its ❌/⏭ verdict rows in the Requirements table (e.g. `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep '^| [0-9].*❌'` / `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep '^| [0-9].*⏭'` β€” scoped to Requirements-table rows because the Execution summary rows share the same row shape); the sections after the tables carry nothing the gate needs. Apply the gates below to the chosen topic's file(s) and no others:
28
+ - **Any `❌ failed`** (in any leaf of the topic) β†’ **block**. Present counts and reasons; continue only when the user sends the task back to executing-tasks, or explicitly types `--force-failed`.
18
29
  - **Only `⏭ skipped`** β†’ warn and confirm ("Requirement N was skipped. Continue, or go back?").
19
- - **`Feature phase` must be `done`** in every progress file of the topic β€” any other value (`e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`) means that topic is still in flight: its ship checkpoint has not been approved. Send the user back to `/skill:pwk-executing-tasks` instead of finalizing.
20
- 4. **Cross-check the roster β€” fail closed on unstarted work.** Find the topic folder locally: the progress file's `Design:` ref names the design doc, and that directory is the topic folder. An `overview.md` **beside the design doc** (same folder β€” never a repo-wide `docs/plans/**/overview.md`, or a sibling topic's overview would route this finalize) means a multi-leaf topic. Read that overview's `## Parts (build order)` roster and require one `<leaf>-progress.md` per leaf. A roster leaf with no progress file is **unstarted** β€” **block**, naming the leaf, and send the user back to `/skill:pwk-executing-tasks` to build it; never dispose an unstarted design unread. (A leaf with a progress file that is not `done` is already stopped by the gate above.)
30
+ - **`Feature phase` must be `done`** in every progress file of the topic β€” any other value (`e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`) means its ship checkpoint has not been approved: send the user back to `/skill:pwk-executing-tasks` instead of finalizing.
31
+ 4. **Cross-check the roster β€” fail closed on unstarted work.** The progress file's `Design:` ref names the design doc, and that directory is the topic folder. An `overview.md` **beside the design doc** (same folder β€” never a repo-wide `docs/plans/**/overview.md`) means a multi-leaf topic: read that overview's `## Parts (build order)` roster and require one `<leaf>-progress.md` per leaf. A roster leaf with no progress file is **unstarted** β€” **block**, naming the leaf, and send the user back to `/skill:pwk-executing-tasks` to build it; never dispose an unstarted design unread. (A leaf whose progress file is not `done` is already stopped by the gate above.)
21
32
 
22
33
  ## Process
23
34
 
24
35
  1. **Derive the topic set** β€”
25
- - **Multi-leaf topic** (an `overview.md` sits beside the design doc, inside the same `docs/plans/<date>-<topic>/` folder β€” never a repo-wide `docs/plans/**/overview.md`, and always excluding docs/plans/completed/, so an archived or sibling topic never routes): read its roster; the set is the topic folder itself, disposed as one unit.
36
+ - **Multi-leaf topic** (an `overview.md` beside the design doc, inside the same `docs/plans/<date>-<topic>/` folder β€” never a repo-wide `docs/plans/**/overview.md`, and always excluding docs/plans/completed/, so an archived or sibling topic never routes): the set is the topic folder itself, disposed as one unit.
26
37
  - **Single-leaf topic**: progress file β†’ `Design:` ref β†’ that design doc's own folder β†’ `<topic>`, which **is the folder slug** β€” the topic directory's name, and the same value `pwk-executing-tasks` uses for the branch and worktree (never a leaf filename). One folder.
27
38
 
28
39
  Ambiguous with several designs in flight? Ask.
29
- 2. **Run the learning sweep β€” before any disposal command.** The planning docs are about to be destroyed; extract the durable knowledge first, while its container still exists. Read each design doc's At-a-glance key-decision bullets **and its `Approaches considered` section** (the full forks with their reasoning β€” every leaf's, for an umbrella), each progress file's `Deviated?` entries (including any deviation decision-records), and each Code digest's `[ALERT]` entries. (A trivial doc has no `## At a glance` and no `Approaches considered` β€” a trivial slice's sweep is expected to come up honest-empty.) Then:
30
- - **Offer an ADR** (in `docs/adr/`) for each item that passes the three gates β€” **hard to reverse**, **surprising without context**, **a real trade-off** β€” informed by how the decision actually played out during execution, not just how it looked at design time.
40
+ 2. **Run the learning sweep β€” before any disposal command.** The planning docs are about to be destroyed, so extract the durable knowledge first, while its container still exists: each design doc's At-a-glance key-decision bullets **and its `Approaches considered` section** (every leaf's, for an umbrella), each progress file's `Deviated?` entries, and each Code digest's `[ALERT]` entries. (A trivial doc has no `## At a glance` and no `Approaches considered` β€” expect an honest-empty sweep.) Then:
41
+ - **Offer an ADR** (in `docs/adr/`) for each item that passes the three gates β€” **hard to reverse**, **surprising without context**, **a real trade-off**.
31
42
  - **Append generic rules** to `docs/lessons.md` (strip domain specifics).
32
- - Both outputs are **honest-empty**: most features qualify for nothing β€” say so and move on; never manufacture. When an item passes the gates but the recorded material is too thin to draft a credible ADR, **ask the human rather than fabricating** context β€” the human was there.
33
- - The sweep is file-based by necessity: executing and finalizing usually run in fresh sessions with no memory of the brainstorm conversation β€” everything the sweep needs must already be on disk.
34
- 3. **Dispose of consumed plan docs β€” ask archive or delete** β€” for **every topic** in the set, dispose its topic folder `docs/plans/<date>-<topic>/` **as one unit**: every leaf's `-design.md`, `-progress.md`, and `-review-packet*.md`, plus `overview.md` for an umbrella. The digest sections (`## At a glance`, `## Execution summary`) live inside their host docs and ride the folder β€” no separate disposal. The folder path is taken verbatim from discovery, and the `????-??-??-` dated shape is the topic's identity β€” a bare `*<topic>*` pattern would over-match unrelated topics. Present both options and let the human choose:
43
+ - Both outputs are **honest-empty**: most features qualify for nothing β€” say so and move on; never manufacture. When an item passes the gates but the recorded material is too thin for a credible ADR, **ask the human rather than fabricating**.
44
+ - The sweep is file-based by necessity: executing and finalizing usually run in fresh sessions with no memory of the brainstorm, so everything the sweep needs must already be on disk.
45
+ 3. **Dispose of consumed plan docs β€” ask archive or delete** β€” for **every topic** in the set, dispose its topic folder `docs/plans/<date>-<topic>/` **as one unit**: every leaf's `-design.md`, `-progress.md`, and `-review-packet*.md`, plus `overview.md` for an umbrella. The digest sections and the Evidence cells live inside their host docs and ride the folder. The folder path is taken verbatim from discovery, and the `????-??-??-` dated shape is the topic's identity β€” a bare `*<topic>*` pattern would over-match unrelated topics. Present both options and let the human choose:
35
46
 
36
- - **Delete (default)** β€” code + tests are the source of truth; removing the scaffold prevents stale plan docs from misleading future sessions:
37
-
38
- The invariant: the topic folder `docs/plans/<date>-<topic>/` is the disposal unit β€” delete it entirely, nothing more and nothing less; any single operation achieving exactly this is correct, and the bash below is one canonical example.
47
+ - **Delete (default)** β€” code + tests are the source of truth; removing the scaffold prevents stale plan docs from misleading future sessions. The invariant: the topic folder is the disposal unit β€” delete it entirely, nothing more and nothing less; any single operation achieving exactly this is correct, and the bash below is one canonical example.
39
48
 
40
49
  ```bash
41
50
  # the topic folder goes as one unit (every leaf, plus overview.md if it has one).
@@ -47,9 +56,7 @@ Any command or tool recipe in this skill is an example, not the requirement β€”
47
56
  # uncommitted drafts never ride the disposal commit.
48
57
  ```
49
58
 
50
- - **Archive** β€” keep the planning history for future readers (e.g. a complex design worth preserving) by moving the artifacts into `docs/plans/completed/`:
51
-
52
- The invariant: the topic folder `docs/plans/<date>-<topic>/` moves whole into `docs/plans/completed/` and the destination is verified before committing; any single operation achieving exactly this is correct, and the bash below is one canonical example.
59
+ - **Archive** β€” keep the planning history for future readers by moving the artifacts into `docs/plans/completed/`. The invariant: the topic folder moves whole into `docs/plans/completed/` and the destination is verified before committing; any single operation achieving exactly this is correct, and the bash below is one canonical example.
53
60
 
54
61
  ```bash
55
62
  mkdir -p docs/plans/completed
@@ -64,23 +71,17 @@ Any command or tool recipe in this skill is an example, not the requirement β€”
64
71
  git add -A docs/plans/<date>-<topic>/ docs/plans/completed/ && git commit -m "chore: archive planning docs for <topic>"
65
72
  ```
66
73
 
67
- Verify with `ls docs/plans/` before and after. Both paths commit the disposal so the shipped branch is clean. Neither path touches `docs/adr/`, `docs/lessons.md`, `CHANGELOG.md`, or `README.md` β€” those are permanent.
68
- 4. **Curate lessons (Agile Scrum Master hat)** β€” if `docs/lessons.md` exists: add missed lessons, generalize domain-specific rules into generic patterns, de-duplicate, categorize, retire stale rules. None exists but lessons were learned? Create it. (The learning sweep above feeds this; curation then shapes the whole file.)
74
+ Verify with `ls docs/plans/` before and after. Both paths commit the disposal so the shipped branch is clean. Neither touches `docs/adr/`, `docs/lessons.md`, `CHANGELOG.md`, or `README.md` β€” those are permanent.
75
+ 4. **Curate lessons (Agile Scrum Master hat)** β€” if `docs/lessons.md` exists: add missed lessons, generalize domain-specific rules, de-duplicate, categorize, retire stale rules. None exists but lessons were learned? Create it. (The learning sweep feeds this; curation then shapes the whole file.)
69
76
  5. **Update documentation** β€” if the API or surface changed: `README.md`, `CHANGELOG.md`, any inline docs. Bump the package version (major for breaking changes).
70
- 6. **Choose a merge strategy** β€” ask the human:
77
+ 6. **Choose a merge strategy** β€” ask the human: 1. **Create PR** (`git push origin <branch>` then `gh pr create`). 2. **Rebase & merge** *(recommended)* β€” rebase onto parent, `--ff-only` merge, push parent, delete branch. 3. **Squash & merge**. 4. **Merge commit** (`--no-ff`).
71
78
 
72
- 1. **Create PR** β€” `git push origin <branch>` then `gh pr create`.
73
- 2. **Rebase & merge** *(recommended)* β€” rebase onto parent, `--ff-only` merge, push parent, delete branch.
74
- 3. **Squash & merge** β€” squash onto parent, push, delete branch.
75
- 4. **Merge commit** β€” `--no-ff` merge, push parent, delete branch.
76
-
77
- For 2–4, confirm the detected parent branch before proceeding. When a worktree was used, the parent-side commands (merge onto parent, push parent, branch deletion) run from the main working tree β€” the topic worktree holds the feature-branch checkout, so the parent cannot be checked out there (`git -C ../<repo> …` drives the main side without moving the session; the root check's never-cd rule still holds) β€” and step 7's worktree removal comes **first**: a branch checked out in a worktree cannot be deleted.
78
- 7. **Clean up** β€” if a worktree was used, verify presence first (`git worktree list`): when the topic's worktree exists, remove it (`git worktree remove ../<repo>-<topic>`, where `<topic>` is the folder slug β€” the same one `pwk-executing-tasks` created the branch under, never a leaf slug). The `../` path is written from the main working tree, and the removal runs from there β€” `git -C ../<repo> worktree remove ../<repo>-<topic>` when the session stands inside the worktree, which git refuses to remove under itself. When absent, cleanup is a silent no-op β€” never a failure.
79
+ For 2–4, confirm the detected parent branch first. When a worktree was used, the parent-side commands (merge onto parent, push parent, branch deletion) run from the main working tree β€” the worktree holds the feature branch, so the parent cannot be checked out there (`git -C ../<repo> …`; the never-cd rule still holds) β€” and step 7's worktree removal comes **first**: a branch checked out in a worktree cannot be deleted.
80
+ 7. **Clean up** β€” if a worktree was used, check `git worktree list` first: when the topic's worktree exists, remove it (`git worktree remove ../<repo>-<topic>`, `<topic>` being the folder slug, never a leaf slug) from the main working tree β€” `git -C ../<repo> worktree remove ../<repo>-<topic>` when the session stands inside the worktree, which git refuses to remove under itself. Absent β†’ a silent no-op, never a failure.
79
81
 
80
82
  ## Principles
81
83
 
82
84
  - The learning sweep runs before disposal and touches only `docs/adr/` and `docs/lessons.md` β€” the two permanent stores.
83
-
84
- - Dispose of the active work's artifacts only (archive or delete, the human's choice) β€” the unit is the topic folder `docs/plans/<date>-<topic>/`, one folder whether the topic has a single leaf or an umbrella's many. Unrelated topics stay in `docs/plans/`.
85
- - ADRs are permanent institutional memory β€” they stay out of archive/rotation forever.
86
- - Bump the package version if this is a published change (major for breaking changes).
85
+ - Dispose of the active work's artifacts only, the human's choice β€” the unit is the topic folder `docs/plans/<date>-<topic>/`, one folder whether a single leaf or an umbrella's many. Unrelated topics stay in `docs/plans/`.
86
+ - ADRs are permanent institutional memory β€” never archived or rotated.
87
+ - Bump the package version if this is a published change (major for breaking changes).
@@ -5,24 +5,34 @@ description: "Show all active pipeline topics and their phase/progress. Use when
5
5
 
6
6
  # Status
7
7
 
8
- Report on in-flight pipelines in this working tree (a worktree has its own `docs/plans/`, so run status in each). Read-only. This skill **does not unlock the guard** β€” it needs no writes, so it runs fine inside the design read-only phase. If you want source edits after a status check, invoke the skill for the next phase yourself (the guard follows the skill).
8
+ Report on in-flight pipelines in this working tree (a worktree has its own `docs/plans/`, so run status in each). Read-only: this skill **does not unlock the guard**, so it runs fine inside the design read-only phase β€” invoke the next phase's skill yourself if you want source edits (the guard follows the skill).
9
9
 
10
- Any command or tool recipe in this skill is an example, not the requirement β€” when your host grants a better tool for an operation (a sandboxed batch executor, a structured file API), use the better tool and treat the recipe as a fallback; the only exact exceptions are this kit's `scripts/` helpers and plain `git` invocations, whose deterministic output is the point.
10
+ Any command or tool recipe in this skill is an example, not the requirement β€” when your host grants a better tool for an operation (a sandboxed batch executor β€” e.g. `codemode` on pi β‰₯ 1.0.0 β€” a structured file API), use the better tool and treat the recipe as a fallback; the only exact exceptions are this kit's `scripts/` helpers and plain `git` invocations, whose deterministic output is the point.
11
11
 
12
12
  ## Process
13
13
 
14
14
  0. **Verify the root** β€” run `pwd` (or your shell's equivalent) and `git rev-parse --show-toplevel`; mismatch β†’ report both paths and stop; never `cd` (a worktree root counts).
15
- 1. **Discover** β€” List `docs/plans` recursively, excluding docs/plans/completed/, for `*-design.md`, `*-progress.md`, and `overview.md` (topic docs live in `docs/plans/<date>-<topic>/` folders β€” archived work is not in flight) β€” this working tree only. Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`.
16
- 2. **State per topic/part β€” extract, never ingest.** For each progress file take the `Feature phase:` line by matching it (e.g. `grep -m1 '^Feature phase:' <file>` β€” wherever the template puts it); the body (execution summary, review reports, code digest) carries nothing status needs, and neither do design docs. A header reading `Setup: pending` renders the topic as **`awaiting setup`** β€” the setup checkpoint was never approved (the slot is only ever written when the design doc has a `## Setup` section, so the header alone decides β€” no design-doc read) β€” ahead of any phase-derived state below. Map the line to the displayed state:
17
- - `done` β†’ **`done`** β€” terminal, never shown as in-flight; every row is resolved (`βœ…` passed, `❌` failed, `⏭` skipped β€” resolved, not necessarily passed). Append the tally as `N/N` when wanted β€” the Requirements-table row count, scoped to that table because the Execution summary rows share the same `| <n> |` row shape and a bare grep would double it (e.g. `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9]'`) β€” and when that same scoped range finds any β€” `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9].*❌'` / `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9].*⏭'` (the same scoping, so Execution-summary rows β€” which share the row shape β€” and prose mentions of the glyphs do not inflate the count) β€” render the counts, e.g. `done (1 ❌ Β· 1 ⏭)`, noting finalizing will require `--force-failed` while `❌` rows stand
15
+ 1. **Discover** β€” List `docs/plans` recursively, excluding docs/plans/completed/, for `*-design.md`, `*-progress.md`, and `overview.md` (topic docs live in `docs/plans/<date>-<topic>/` folders β€” archived work is not in flight) β€” this working tree only; take each topic’s `Feature phase:` line header-only (`grep -m1 '^Feature phase:' <file>`). Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`. With a sandboxed batch executor (e.g. `codemode` on pi β‰₯ 1.0.0) the listing and every phase extraction are one call, returning only the summary lines (adapt the paths to the verified root when your cwd differs):
16
+
17
+ ```javascript
18
+ const designs = (await tools.bash({ command: "find docs/plans -name '*-design.md' -not -path '*/completed/*' | sort", timeout: 15 })).output.trim();
19
+ const files = designs ? designs.split('\n') : [];
20
+ const shellq = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
21
+ const heads = await Promise.all(files.map(d =>
22
+ tools.bash({ command: "grep -m1 '^Feature phase:' " + shellq(d.replace('-design.md', '-progress.md')), timeout: 15 }).then(r => r.output.trim() || 'design')));
23
+ return text(files.map((d, i) => d + ' β€” ' + heads[i]).join('\n') || 'no in-flight topics');
24
+ ```
25
+
26
+ 2. **State per topic/part β€” map the header.** The body (execution summary, review reports, code digest) carries nothing status needs, and neither do design docs. A header reading `Setup: pending` renders the topic as **`awaiting setup`** β€” the setup checkpoint was never approved (the slot is only ever written when the design doc has a `## Setup` section, so the header alone decides β€” no design-doc read) β€” ahead of any phase-derived state below. Map the line to the displayed state:
27
+ - `done` β†’ **`done`** β€” terminal, never shown as in-flight; every row is resolved (`βœ…` passed, `❌` failed, `⏭` skipped β€” resolved, not necessarily passed). Append the tally as `N/N` when wanted β€” the Requirements-table row count, scoped to that table because the Execution summary rows share the same `| <n> |` row shape and a bare grep would double it (e.g. `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9]'`). Counting `❌`/`⏭` rows over that same scoped range (`sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9].*❌'` / `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9].*⏭'` β€” the same scoping, so Execution-summary rows and prose mentions of the glyphs do not inflate the count) renders the counts, e.g. `done (1 ❌ Β· 1 ⏭)`, noting finalizing will require `--force-failed` while `❌` rows stand
18
28
  - `implementing (k/N)` β†’ `execute k/N` (the tally rides on the line itself β€” no extra read)
19
29
  - `e2e-written` β†’ `execute 0/N` (the E2E is written and the run continues into implementation β€” never shown as a paused state; N comes from the same Requirements-table-scoped row count as `done` β€” so no tally renders without a defined source)
20
30
  - `reviewing` β†’ `review`
21
- - `ship-paused` β†’ `ship-paused` β€” next: `/skill:pwk-executing-tasks` (approve the ship checkpoint). Never hint finalizing: only `done` routes there.
31
+ - `ship-paused` β†’ `ship-paused` β€” next: `/skill:pwk-executing-tasks` (approve the ship checkpoint); only `done` routes to finalizing.
22
32
  - no progress file, only `*-design.md` β†’ `design` β€” next: `/skill:pwk-executing-tasks`
23
33
  - roster-only (named in the overview, no artifacts) β†’ `not started`
24
34
  - no parseable `Feature phase` line β†’ `execute` (with tally if parseable).
25
- 3. **Group by umbrella** β€” for each `overview.md`, read its **`## Parts (build order)`** roster (the heading and the `N. <part> β€” <scope>` item shape are lint-pinned) and roll the parts up by state (the overview carries no status): done parts, in-flight parts (design, execute, review, ship-paused, awaiting setup), not-started parts. A part sitting at the feature-E2E notice (`e2e-written`) rolls up as execute β€” the notice is a routing moment, not a bucket of its own. Print one roll-up line β€” `<topic> (umbrella): n done Β· n in-flight Β· n not-started` β€” and when every part is `done` append `β€” all parts done, ready for /skill:pwk-finalizing`. A `done` single-leaf topic gets the same hint. Once the topic finalizes, its folder β€” overview included β€” is disposed, so it no longer appears here. Topics without an overview print flat.
35
+ 3. **Group by umbrella** β€” for each `overview.md`, read its **`## Parts (build order)`** roster (the heading and the `N. <part> β€” <scope>` item shape are lint-pinned) and roll the parts up by state (the overview carries no status): done, in-flight (design, execute, review, ship-paused, awaiting setup), not-started. A part at the feature-E2E notice (`e2e-written`) rolls up as execute β€” a routing moment, not a bucket of its own. Print one roll-up line β€” `<topic> (umbrella): n done Β· n in-flight Β· n not-started` β€” and when every part is `done` append `β€” all parts done, ready for /skill:pwk-finalizing`; a `done` single-leaf topic gets the same hint. Once the topic finalizes its folder β€” overview included β€” is disposed, so it no longer appears here. Topics without an overview print flat.
26
36
  4. Print a compact table, grouped under any umbrellas, e.g.:
27
37
 
28
38
  ```text
@@ -7,7 +7,7 @@ description: "Generate a detailed, file-referenced walkthrough of a shipped feat
7
7
 
8
8
  Explain a shipped feature in code-level detail, for a human reading with the files open.
9
9
 
10
- Any command or tool recipe in this skill is an example, not the requirement β€” when your host grants a better tool for an operation (a sandboxed batch executor, a structured file API), use the better tool and treat the recipe as a fallback; the only exact exceptions are this kit's `scripts/` helpers and plain `git` invocations, whose deterministic output is the point.
10
+ Any command or tool recipe in this skill is an example, not the requirement β€” when your host grants a better tool for an operation (a sandboxed batch executor β€” e.g. `codemode` on pi β‰₯ 1.0.0 β€” a structured file API), use the better tool and treat the recipe as a fallback; the only exact exceptions are this kit's `scripts/` helpers and plain `git` invocations, whose deterministic output is the point.
11
11
 
12
12
  **Not a pipeline phase** β€” never auto-run, never blocks anything. The skill is generated on demand and invoking it exits any gated design phase (it writes outside `docs/plans/`).
13
13