@tianhai/pi-workflow-kit 2.4.0 → 2.5.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.
- package/docs/workflow-phases.md +9 -0
- package/package.json +1 -1
- package/skills/pwk-brainstorming/SKILL.md +34 -80
- package/skills/pwk-brainstorming/references/design-format.md +83 -0
- package/skills/pwk-brainstorming/references/umbrella.md +26 -0
- package/skills/pwk-brainstorming/scripts/validate-design.py +194 -0
- package/skills/pwk-executing-tasks/SKILL.md +48 -106
- package/skills/pwk-executing-tasks/references/progress-format.md +81 -0
- package/skills/pwk-executing-tasks/references/resume.md +15 -0
- package/skills/pwk-executing-tasks/references/review.md +42 -0
- package/skills/pwk-executing-tasks/scripts/build-review-packet.py +318 -0
package/docs/workflow-phases.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Workflow Phases
|
|
2
2
|
|
|
3
|
+
Phase skills keep detailed contracts local to their skill directories:
|
|
4
|
+
`skills/pwk-brainstorming/references/umbrella.md`,
|
|
5
|
+
`skills/pwk-brainstorming/references/design-format.md`, and
|
|
6
|
+
`skills/pwk-brainstorming/scripts/validate-design.py`; plus
|
|
7
|
+
`skills/pwk-executing-tasks/references/progress-format.md`,
|
|
8
|
+
`skills/pwk-executing-tasks/references/resume.md`,
|
|
9
|
+
`skills/pwk-executing-tasks/references/review.md`, and
|
|
10
|
+
`skills/pwk-executing-tasks/scripts/build-review-packet.py`.
|
|
11
|
+
|
|
3
12
|
The kit enforces a design → execute → finalize workflow. Each phase below names the skill that drives it:
|
|
4
13
|
|
|
5
14
|
`pi-workflow-kit` has 4 pipeline skills plus 2 utility skills. You invoke each one explicitly with `/skill:`.
|
package/package.json
CHANGED
|
@@ -11,50 +11,25 @@ Read-only exploration of source code; every file you create or edit goes under `
|
|
|
11
11
|
|
|
12
12
|
Classify the change at the start.
|
|
13
13
|
|
|
14
|
-
- **Trivial** — typo or obvious bugfix with no open design questions, config/version bump, single-function change, or anything the human flags as trivial. Skip steps 3–6;
|
|
14
|
+
- **Trivial** — typo or obvious bugfix with no open design questions, config/version bump, single-function change, or anything the human flags as trivial. Skip steps 3–6; read the design-format reference and run the validator in step 7, writing a **minimal** design doc in one turn (an `In short:` one-liner — what + why + approach in plain words, plus a single `### R1:` requirement block with its criteria and tags, and an optional `## Production-risk areas` line — and deliberately **no `## Feature acceptance`**: a trivial slice's ship gate is the full existing suite, no E2E is manufactured for it), and hand off to `/skill:pwk-executing-tasks`. The guard still enforces read-only — trivial compresses the phase to one turn, it doesn't skip it.
|
|
15
15
|
- **Non-trivial** — open design questions, multiple approaches, cross-module impact, or new behavior. Run the full process below.
|
|
16
16
|
|
|
17
17
|
When unsure, ask: "This looks trivial — fast-path it, or full brainstorm?" Default to full.
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Scope and granularity
|
|
20
20
|
|
|
21
|
-
**One design doc = one PR; one requirement = one testable slice within it.** Most work is a single design doc.
|
|
21
|
+
**One design doc = one PR; one requirement = one testable slice within it.** Most work is a single design doc; several requirements serving one cohesive outcome do not force an umbrella.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Before drafting, assess whether a large request has distinct testable outcomes that need separate design/execute cycles. If so, propose an **umbrella** of vertical slices with one-line scopes and a build order, and get human approval before writing an overview or design. A layers-only split (database, API, UI) is not a vertical split: each slice delivers testable behavior. Design only the current slice; ground later designs in implemented code, not speculative sibling designs. The umbrella still ships as one PR on one branch, finalized once.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
- A requirement too big for one design doc but shipping as a single PR is an **umbrella** — multiple design docs decomposed under one overview, on one branch, finalized once. See [Umbrella](#umbrella) below.
|
|
25
|
+
**One folder per topic:** `docs/plans/<date>-<topic>/`, containing `<leaf>-design.md`, `<leaf>-progress.md`, and `<leaf>-review-packet*.md`; `<leaf>` is `<topic>` for a single-part topic. More than one leaf adds `overview.md`. The whole folder is the creation/discovery/disposal unit.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
An umbrella splits one large requirement into multiple design docs that ship together as **one PR**. It is the single-topic folder above with more than one leaf: one branch, one `pwk-finalizing` at the end, which disposes the folder as one unit. The split is intra-PR decomposition — a way to keep each design/execute cycle small and focused, not a multi-PR strategy.
|
|
31
|
-
|
|
32
|
-
**First brainstorm** (the requirement is too big for one design doc):
|
|
33
|
-
|
|
34
|
-
1. **Propose the split** — the parts, a one-line scope each, and build order. Get human approval before writing anything beyond discovery.
|
|
35
|
-
2. **Write the overview** — `docs/plans/<date>-<topic>/overview.md`, in the topic's own folder (every leaf lives beside it: `<leaf>-design.md`, `<leaf>-progress.md`, `<leaf>-review-packet*.md`), a **status-free roster**:
|
|
36
|
-
|
|
37
|
-
```markdown
|
|
38
|
-
# Overview: <topic>
|
|
39
|
-
|
|
40
|
-
Goal: <one line — what the whole requirement delivers>
|
|
41
|
-
|
|
42
|
-
## Parts (build order)
|
|
43
|
-
1. <leaf> — <one-line scope>
|
|
44
|
-
2. <leaf> — <one-line scope>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
The `## Parts (build order)` heading and the `N. <leaf> — <one-line scope>` item shape together are the **roster contract**: `pwk-finalizing` and `pwk-status` both read them, and both are lint-pinned so a drifted heading fails loudly instead of reading an empty roster. Goal, parts with one-line scopes, build order — **no status column**. No skill mutates the overview between this write and `pwk-finalizing` (which disposes it); part-completion is inferred from each leaf's own `*-progress.md`.
|
|
48
|
-
3. **Write the first part's design doc** — `docs/plans/<date>-<topic>/<leaf>-design.md` in the same folder, then hand off to `/skill:pwk-executing-tasks`.
|
|
49
|
-
|
|
50
|
-
**Later parts** — re-run `/skill:pwk-brainstorming` for the next part. Read the overview for big-picture/roster context (which slice is yours, which siblings exist), then report each prior leaf's `Feature phase:` line (matched header-only — `grep -m1 '^Feature phase:' <leaf>-progress.md` beside the overview — no body ingest; a leaf with no progress file yet reports `not started`) before designing the slice, then explore the codebase to design your slice **as brainstorm always does** — prior parts are just implemented code in the repo by then. Predecessor phases are visibility, not enforcement: design the slice regardless; cross-slice decisions that must persist go in an ADR, not the overview.
|
|
51
|
-
|
|
52
|
-
The whole umbrella is one branch and one PR: `pwk-executing-tasks` creates the branch in its pre-flight and reuses it for later parts, and suggests the next part (or finalize after the last); `pwk-finalizing` disposes the whole folder — overview + every leaf — and ships one PR.
|
|
27
|
+
**Umbrella trigger:** before proposing or writing a new umbrella, or when discovery finds an existing `overview.md` beside this design, read [references/umbrella.md](references/umbrella.md). It defines the status-free roster, first/later-slice procedures, and header-only predecessor reporting. Build order is advisory, not an enforcement gate. An existing roster means the split is already decided; design the current slice against it.
|
|
53
28
|
|
|
54
29
|
## Process
|
|
55
30
|
|
|
56
31
|
1. **Check git state** — `git status` + `git log --oneline -5`. Uncommitted work? Ask the user what to do first.
|
|
57
|
-
2. **Discovery** *(skip in a brand-new repo with no `docs/plans/`)* — first 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). Then 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). Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`; report in-flight topics and any active umbrella. For each in-flight topic with a progress file, report its `Feature phase:` line (matched header-only — `grep -m1 '^Feature phase:' <file>` — no body ingest); design-only topics report `design`. If the new idea continues an existing topic, ask whether to extend it or start fresh. Part of an umbrella? An existing `overview.md` **beside the design doc** means the split is already decided — read it for the roster and design this part's `<leaf>-design.md` against it (
|
|
32
|
+
2. **Discovery** *(skip in a brand-new repo with no `docs/plans/`)* — first 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). Then 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). Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`; report in-flight topics and any active umbrella. For each in-flight topic with a progress file, report its `Feature phase:` line (matched header-only — `grep -m1 '^Feature phase:' <file>` — no body ingest); design-only topics report `design`. If the new idea continues an existing topic, ask whether to extend it or start fresh. Part of an umbrella? An existing `overview.md` **beside the design doc** means the split is already decided — read it for the roster and design this part's `<leaf>-design.md` against it (read [references/umbrella.md](references/umbrella.md) before proceeding).
|
|
58
33
|
3. **Understand the idea** — read only enough code/docs/commits to ground the design. **Check `docs/lessons.md`** — known constraints may shape it. Ask questions in **frontier rounds**: build a question tree seeded by the dimension checklist, then ask in rounds. The **frontier** is every question whose prerequisites are already settled — ask the whole frontier in one round; a question whose answer depends on another still-open question waits for a later round. Number each question (`Q1`, `Q2`, …) and attach your recommended answer (`➡️ <recommendation>`) — the recommendation is your assumption surfaced up front; the human confirms, strikes, or corrects each in one reply. Recompute the frontier after each round of answers. Seed the tree by walking every checklist dimension — *Goal & scope · Data & state · Behavior & edge cases · Errors & failure · Integration · Non-functional* — printing `— nothing to ask` for groups with no questions (never skip silently). **Facts vs. decisions**: anything answerable from the codebase, docs, or tools is looked up — recon scout or inline — never asked of the human; a pending lookup is an unsettled prerequisite that holds only its downstream questions, while the rest of the frontier is asked now. Only decisions are asked. Major approvals stay single-decision — one question each, never batched: approach selection (step 5), umbrella split, design approval, ADR offer. The interview ends when the frontier is empty — every branch visited, nothing left silently assumed — not when you feel you understand. Then present a short summary and ask: "Should I proceed, or is there more?" The human decides when to move on.
|
|
59
34
|
4. **(skipped on trivial changes)** **Codebase recon** — for non-trivial topics with prior art, request the host’s `codebase-recon` capability using the logical `pwk-recon-scout` role. Require a fresh-context, read-only, bounded worker and pass the topic, one-line intent, and repo root. Use the returned 5-section codebase map (Relevant files, Existing patterns, Call sites, Test layout, Gotchas) as the grounding context for the next two steps instead of reading those files inline. The scout is observations only — no design recommendations. Skip this step on trivial changes (typo, version bump, single-function edit per the proportionality rule). If no compatible capability is available or the provider cannot enforce the requested constraints, report `Scout: unavailable` and do the same recon inline, preserving the five-section map and `file:line` citations; do not silently omit recon.
|
|
60
35
|
5. **Explore approaches** — propose 2–3, leading with your recommendation. Sketch the concrete interface (types, signatures, example caller) for each so the comparison is grounded in code, not abstractions.
|
|
@@ -63,60 +38,19 @@ The whole umbrella is one branch and one PR: `pwk-executing-tasks` creates the b
|
|
|
63
38
|
**Assumptions to confirm first** — before presenting, sweep the drafted design: every assumption it would bake in unconfirmed (business rules, defaults, edge-case resolutions) re-opens as a numbered question carrying your recommended answer; the human confirms, strikes, or corrects each in one reply. An honest empty gate ("no unconfirmed assumptions") when the draft is clean — never invent items. Hard rule: no business behavior enters the design doc on the agent's assumption. Confirmed facts are woven into the doc's existing sections — no new template section.
|
|
64
39
|
|
|
65
40
|
Identified a significant architectural decision? Offer an ADR in `docs/adr/`. Only when all three hold: **hard to reverse**, **surprising without context**, **a real trade-off**. Format: title + 1–3 sentences of context/decision/why. ADRs are permanent institutional memory — they stay out of archive/rotation forever. (Guard note: `docs/adr/` is outside the writable `docs/plans/`; the human writes the approved ADR themselves or briefly runs `/pwk-guard off` — the agent never writes it while the design phase is gated.)
|
|
66
|
-
7. **Write the design doc** — `docs/plans/<date>-<topic>/<leaf>-design.md`, creating the topic folder when absent
|
|
67
|
-
|
|
68
|
-
**Open with `## At a glance`** — the human's two-minute digest, immediately before `## Requirements`. It contains (1) a 2–4 sentence plain-language summary: what is wrong or needed, what will be built, the key approach in plain words; (2) **Key decisions** — one line each: decision + why, with a `(rejected: <alternative> — <reason>)` clause only when the fork was real and load-bearing (never manufactured — no fork, no clause); (3) a table `| R# | Requirement in one line | Risk |` with exactly one row per requirement, where **R# = the requirement's block number in the `## Requirements` section below** — this ID is what every later digest keys on (progress rows, execution summary, reviewer coverage table). Plain language only: short sentences, no jargon, no Given/When/Then — those live in the requirement blocks for the executor. An umbrella overview gains no at-a-glance section; its roster already serves that role.
|
|
69
|
-
|
|
70
|
-
Then **`## Requirements`** — one block per requirement, each one testable behavior:
|
|
71
|
-
|
|
72
|
-
```markdown
|
|
73
|
-
## Requirements
|
|
41
|
+
7. **Write the design doc** — validate its structure before handing off; before writing either a trivial or non-trivial doc, read [references/design-format.md](references/design-format.md) for the exact format, tag defaults, risk/Setup rules, and audit. Requirement blocks carry acceptance criteria plus `### Checkpoints` / `### Review` tags. Missing tags default to `none` / `skip`. Write `docs/plans/<date>-<topic>/<leaf>-design.md`, creating the topic folder when absent. It is descriptive, not a task list; the executor builds directly from it.
|
|
74
42
|
|
|
75
|
-
|
|
76
|
-
<one-line testable behavior — what the feature produces or changes, through its public interface>
|
|
43
|
+
**Completion gate:** Test observable behavior through the public interface. Audit every requirement's criteria, tags, and risk notes; confirm each appears exactly once. For non-trivial work, finish with end-to-end `## Feature acceptance` as the primary enforced spec. If writing a scenario step would require inventing behavior, that invention goes back through the assumption gate — an unwritable step means an unspecified requirement, not permission to guess. Risk notes never set the tag — the written `### Review` value stays `skip` unless the human sets it, and only the human tags a slice for review. Keep designing until the requirements compose into a coherent feature and the human approves the design, including feature acceptance. Trivial work retains the compressed one-turn path above, not another recurring approval stop.
|
|
77
44
|
|
|
78
|
-
**
|
|
79
|
-
- Given … When … Then …
|
|
80
|
-
- Given … When … Then … (edge case)
|
|
45
|
+
**Python prerequisite:** structural validation requires Python 3.9+ where commands execute, using only its standard library (no pip or project Node configuration). Confirm a compatible `python3` exists before invoking the helper. If Python is missing, unavailable, or too old, report the blocker and request a suitable runtime; stop at this gate. Do not auto-install Python, use a fallback implementation, or claim validation succeeded.
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
### Review: skip | parallel | inline
|
|
47
|
+
Resolve the helper from this installed skill's directory; pass explicit paths and quote paths with spaces. Relative input paths resolve from the caller's working directory, not the skill directory:
|
|
84
48
|
|
|
85
|
-
|
|
86
|
-
- <
|
|
87
|
-
|
|
88
|
-
### R2: <name>
|
|
89
|
-
…
|
|
49
|
+
```sh
|
|
50
|
+
python3 "<brainstorming-skill-dir>/scripts/validate-design.py" "<design-path>"
|
|
90
51
|
```
|
|
91
52
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- **No test-name lists.** The criteria are the test spec — the executor writes and names the actual tests red-green from them, so the doc contains no test-name lists and no R#-to-section mapping tables: the block structure is the map.
|
|
95
|
-
- **Tag every requirement** — `### Checkpoints` (how many human stops: `none` = no per-requirement stop, the default — the feature gate covers it; `full` = tests + complete stops) and `### Review` (per-requirement review: `skip` = none, the default; `parallel` = four delegated reviewers; `inline` = one `pwk-code-review` pass). Missing tags default to `none` / `skip`. Flag `full` only where complex logic or the main part of the feature makes a human look at the slice worth the stop.
|
|
96
|
-
- **Nothing is tagged silently here; only the human tags a slice for review.** A requirement that touches a production-risk area carries its `### Production-risk notes` in the block and is flagged `⚠ production-risk` in the At-a-glance Risk column (the column is derived, **display-only**: `⚠ production-risk` if and only if the block carries non-empty `### Production-risk notes`, else `—` — never normative, never an input; `auto` reads the underlying risk content — the `## Production-risk areas` section and the notes — never the display column), but its written `### Review` value stays `skip` unless the human sets it — propose the tag in prose, leave the field at `skip`. An explicit human tag always wins, in both directions.
|
|
97
|
-
- **No per-requirement spec stop, by design** — the acceptance criteria are approved right here, at design time; re-checking them mid-execution asks a question the human already answered.
|
|
98
|
-
- **Production-risk notes** — a requirement touching a production-risk area carries its notes inside the block. Risks involving schema migrations, new dependencies, external APIs, or seed data also get a `## Setup` section (dependencies, migrations, seed data, and how to verify setup worked) between `## Requirements` and `## Feature acceptance`.
|
|
99
|
-
- **Ordering** — dependencies come earlier in the list; the executor runs blocks in listed order with no dependency graph. Aim for vertical slices that merge cleanly on their own.
|
|
100
|
-
|
|
101
|
-
Then the narrative sections the executor reads as context — problem, approaches considered, architecture, components, data flow, error handling. Head the approaches section exactly `## Approaches considered` — finalizing's learning sweep reads it by that name. **`## Approaches considered` records every real fork and why each loser lost** — the executor needs the reasoning, and the finalize learning sweep harvests it for ADRs.
|
|
102
|
-
|
|
103
|
-
Touches a production-risk area (DB schema/migrations, auth, external APIs, concurrency/batch, uploads/large data flows, Redis/caching/queues)? Add a brief `## Production-risk areas` **immediately after the last requirement block** — inside the `### R1 → ## Feature acceptance` span, so the review packet carries it to reviewers verbatim — and mirror the risks into each touched block's `### Production-risk notes`. `pwk-code-review` audits it, per requirement when the slice is reviewed and at the feature review always; finalize's learning sweep reads it.
|
|
104
|
-
|
|
105
|
-
**End with `## Feature acceptance`** — one or more end-to-end `Given/When/Then` scenarios proving the requirements *compose* into the feature. This is the feature's definition-of-done; the human approves it as what "the feature works" means. `pwk-executing-tasks` writes it as the E2E test first and runs it as the **primary enforced spec** (the test it gates on first). The section carries the feature-level `### Feature review: auto | parallel | inline` tag — the one whole-feature review; default `auto`, which resolves on the design's own production-risk content to `parallel` (all four reviewers) when the design has any — a non-empty `## Production-risk areas` section, or a non-empty `### Production-risk notes` on any requirement — and to `inline` (one pass) otherwise. An explicit `parallel` or `inline` is written as-is, never resolved, so the human always overrides `auto`. Treat "I can write this scenario" as the green light to finish designing — if you can't, keep designing because the requirements don't yet compose into a coherent feature. If writing a scenario step would require inventing behavior, that invention goes back through the assumption gate — it may never be silently written into the scenario; an unwritable step means an unspecified requirement, so keep designing.
|
|
106
|
-
|
|
107
|
-
```markdown
|
|
108
|
-
## Feature acceptance
|
|
109
|
-
|
|
110
|
-
- Given <starting state>, When <trigger>, Then <end-to-end outcome the feature promises>.
|
|
111
|
-
|
|
112
|
-
### Feature review: auto | parallel | inline — the one whole-feature review; leave `auto` unless the human says otherwise.
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Example (rate limiting): "Given a new API consumer with no prior usage, When they exceed 100 requests/minute for 3 consecutive minutes, Then they're throttled, a `rate_limited` event is emitted, and further requests return 429."
|
|
116
|
-
|
|
117
|
-
**Audit before finishing:** every requirement block has acceptance criteria and both tags, and each requirement appears **exactly once** — none dropped, none duplicated. A `## Feature acceptance` section exists as the primary enforced spec — on every non-trivial doc; a trivial doc ends after its `### R1:` block and ships on the full suite instead. Production-risk areas are reflected in the requirement blocks that touch them.
|
|
118
|
-
|
|
119
|
-
**Splitting large issues:** if the requirement needs more than one design doc, propose an umbrella split (one `<topic>` per part) and get human approval first — see [Umbrella](#umbrella). On approval, write the status-free overview (`docs/plans/<date>-<topic>/overview.md`) and the **first part's** `-design.md` beside it, then hand off to `/skill:pwk-executing-tasks`. Later parts are brainstormed one by one (each re-reading the overview) — do not write every part's design doc up front.
|
|
53
|
+
Nonzero means validation did not succeed: report file-specific diagnostics, correct structural errors, and rerun. A missing local reference or helper is a blocker before its dependent action; do not silently skip it. Success verifies structure only, never semantic correctness, sound slicing, confirmed assumptions, or human approval.
|
|
120
54
|
|
|
121
55
|
The session stays read-only and uncommitted through brainstorm: branch creation and the design-doc commit happen at the start of `pwk-executing-tasks` (its pre-flight creates the feature branch).
|
|
122
56
|
|
|
@@ -131,3 +65,23 @@ The session stays read-only and uncommitted through brainstorm: branch creation
|
|
|
131
65
|
## After the design
|
|
132
66
|
|
|
133
67
|
Ask: "Ready to execute? Run `/skill:pwk-executing-tasks`"
|
|
68
|
+
|
|
69
|
+
## Contract index
|
|
70
|
+
|
|
71
|
+
The detailed design contract is in `references/design-format.md`; this compact index keeps
|
|
72
|
+
the pipeline markers visible to tools that inspect only the entrypoint:
|
|
73
|
+
`## At a glance` → **Key decisions** → `| R# | Requirement in one line | Risk |`;
|
|
74
|
+
`### R<n>: <name>` blocks contain Given/When/Then criteria, no test-name lists, and both
|
|
75
|
+
tags. The feature section ends with `### Feature review: auto | parallel | inline`.
|
|
76
|
+
Production-risk display is `⚠ production-risk` if and only if notes exist; alternatives
|
|
77
|
+
are never manufactured. The umbrella roster uses `## Parts (build order)` and
|
|
78
|
+
`N. <leaf> — <one-line scope>`.
|
|
79
|
+
The design format carries `Given/When/Then criteria`, `no test-name lists`, and tags;
|
|
80
|
+
the full contract also states `written \`### Review\` value stays \`skip\`` and records
|
|
81
|
+
architectural deviations `at deviation time`.
|
|
82
|
+
|
|
83
|
+
```markdown
|
|
84
|
+
## Feature acceptance
|
|
85
|
+
- Given <starting state>, When <trigger>, Then <outcome>.
|
|
86
|
+
### Feature review: auto | parallel | inline
|
|
87
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Design format
|
|
2
|
+
|
|
3
|
+
Read before writing any design doc. Keep the design descriptive, not a task list. It is the single buildable artifact; no separate implementation plan.
|
|
4
|
+
|
|
5
|
+
## Trivial format
|
|
6
|
+
|
|
7
|
+
A trivial design has an `In short:` one-liner (what + why + approach in plain words), a single `### R1:` block with acceptance criteria and tags, and optional `## Production-risk areas`. It has no `## At a glance`, `## Requirements`, or `## Feature acceptance`; its ship gate is the full existing suite, not a manufactured E2E. Use the block rules below for its criteria, tags, and risk notes.
|
|
8
|
+
|
|
9
|
+
```markdown
|
|
10
|
+
# Design: <topic>
|
|
11
|
+
|
|
12
|
+
In short: <what + why + approach>
|
|
13
|
+
|
|
14
|
+
### R1: <name>
|
|
15
|
+
<observable behavior>
|
|
16
|
+
|
|
17
|
+
**Acceptance criteria**
|
|
18
|
+
- Given <state>, When <action>, Then <observable outcome>.
|
|
19
|
+
|
|
20
|
+
### Checkpoints: none
|
|
21
|
+
### Review: skip
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Non-trivial format
|
|
25
|
+
|
|
26
|
+
**Open with `## At a glance`** — the human's two-minute digest, immediately before `## Requirements`. It contains (1) a 2–4 sentence plain-language summary: what is wrong or needed, what will be built, the key approach in plain words; (2) **Key decisions** — one line each: decision + why, with a `(rejected: <alternative> — <reason>)` clause only when the fork was real and load-bearing (never manufactured — no fork, no clause); (3) a table `| R# | Requirement in one line | Risk |` with exactly one row per requirement, where **R# = the requirement's block number in the `## Requirements` section below** — this ID is what every later digest keys on (progress rows, execution summary, reviewer coverage table). Plain language only: short sentences, no jargon, no Given/When/Then — those live in the requirement blocks for the executor. An umbrella overview gains no at-a-glance section; its roster already serves that role.
|
|
27
|
+
|
|
28
|
+
Then **`## Requirements`** — one `### R<n>: <name>` block per requirement, each one testable behavior:
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
### R1: <name>
|
|
34
|
+
<one-line testable behavior — what the feature produces or changes, through its public interface>
|
|
35
|
+
|
|
36
|
+
**Acceptance criteria** — Given/When/Then criteria defining "done". Test observable behavior — what the feature produces or changes through its public interface; not implementation steps. Cover edge and error cases.
|
|
37
|
+
- Given … When … Then …
|
|
38
|
+
- Given … When … Then … (edge case)
|
|
39
|
+
|
|
40
|
+
### Checkpoints: none | full
|
|
41
|
+
### Review: skip | parallel | inline
|
|
42
|
+
|
|
43
|
+
### Production-risk notes
|
|
44
|
+
- <only when the requirement touches a risk area>
|
|
45
|
+
|
|
46
|
+
### R2: <name>
|
|
47
|
+
…
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Block rules:
|
|
51
|
+
|
|
52
|
+
- **No test-name lists.** The criteria are the test spec — the executor writes and names the actual tests red-green from them, so the doc contains no test-name lists and no R#-to-section mapping tables: the block structure is the map.
|
|
53
|
+
- **Tag every requirement** — `### Checkpoints` (how many human stops: `none` = no per-requirement stop, the default — the feature gate covers it; `full` = tests + complete stops) and `### Review` (per-requirement review: `skip` = none, the default; `parallel` = four delegated reviewers; `inline` = one `pwk-code-review` pass). Missing tags default to `none` / `skip`. Flag `full` only where complex logic or the main part of the feature makes a human look at the slice worth the stop.
|
|
54
|
+
- **Nothing is tagged silently here; only the human tags a slice for review.** A requirement that touches a production-risk area carries its `### Production-risk notes` in the block and is flagged `⚠ production-risk` in the At-a-glance Risk column (the column is derived, **display-only**: `⚠ production-risk` if and only if the block carries non-empty `### Production-risk notes`, else `—` — never normative, never an input; `auto` reads the underlying risk content — the `## Production-risk areas` section and the notes — never the display column), but its written `### Review` value stays `skip` unless the human sets it — propose the tag in prose, leave the field at `skip`. An explicit human tag always wins, in both directions.
|
|
55
|
+
- **No per-requirement spec stop, by design** — the acceptance criteria are approved right here, at design time; re-checking them mid-execution asks a question the human already answered.
|
|
56
|
+
- **Production-risk notes** — a requirement touching a production-risk area carries its notes inside the block. Risks involving schema migrations, new dependencies, external APIs, or seed data also get a `## Setup` section (dependencies, migrations, seed data, and how to verify setup worked) between `## Requirements` and `## Feature acceptance`.
|
|
57
|
+
- **Ordering** — dependencies come earlier in the list; the executor runs blocks in listed order with no dependency graph. Aim for vertical slices that merge cleanly on their own.
|
|
58
|
+
|
|
59
|
+
Then the narrative sections the executor reads as context — problem, approaches considered, architecture, components, data flow, error handling. Head the approaches section exactly `## Approaches considered` — finalizing's learning sweep reads it by that name. **`## Approaches considered` records every real fork and why each loser lost** — the executor needs the reasoning, and the finalize learning sweep harvests it for ADRs.
|
|
60
|
+
|
|
61
|
+
Touches a production-risk area (DB schema/migrations, auth, external APIs, concurrency/batch, uploads/large data flows, Redis/caching/queues)? Add a brief `## Production-risk areas` **immediately after the last requirement block** — inside the `### R1 → ## Feature acceptance` span, so the review packet carries it to reviewers verbatim — and mirror the risks into each touched block's `### Production-risk notes`. `pwk-code-review` audits it, per requirement when the slice is reviewed and at the feature review always; finalize's learning sweep reads it.
|
|
62
|
+
|
|
63
|
+
**End with `## Feature acceptance`** — one or more end-to-end `Given/When/Then` scenarios proving the requirements *compose* into the feature. This is the feature's definition-of-done; the human approves it as what "the feature works" means. `pwk-executing-tasks` writes it as the E2E test first and runs it as the **primary enforced spec** (the test it gates on first). The section carries the feature-level `### Feature review: auto | parallel | inline` tag — the one whole-feature review; default `auto`, which resolves on the design's own production-risk content to `parallel` (all four reviewers) when the design has any — a non-empty `## Production-risk areas` section, or a non-empty `### Production-risk notes` on any requirement — and to `inline` (one pass) otherwise. An explicit `parallel` or `inline` is written as-is, never resolved, so the human always overrides `auto`. Treat "I can write this scenario" as the green light to finish designing — if you can't, keep designing because the requirements don't yet compose into a coherent feature. If writing a scenario step would require inventing behavior, that invention goes back through the assumption gate — it may never be silently written into the scenario; an unwritable step means an unspecified requirement, so keep designing.
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
## Feature acceptance
|
|
67
|
+
|
|
68
|
+
- Given <starting state>, When <trigger>, Then <end-to-end outcome the feature promises>.
|
|
69
|
+
|
|
70
|
+
### Feature review: auto | parallel | inline — the one whole-feature review; leave `auto` unless the human says otherwise.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Example (rate limiting): "Given a new API consumer with no prior usage, When they exceed 100 requests/minute for 3 consecutive minutes, Then they're throttled, a `rate_limited` event is emitted, and further requests return 429."
|
|
74
|
+
|
|
75
|
+
**Audit before finishing:** every requirement block has acceptance criteria and both tags, and each requirement appears **exactly once** — none dropped, none duplicated. A `## Feature acceptance` section exists as the primary enforced spec — on every non-trivial doc; a trivial doc ends after its `### R1:` block and ships on the full suite instead. Production-risk areas are reflected in the requirement blocks that touch them.
|
|
76
|
+
|
|
77
|
+
## Structural validator contract
|
|
78
|
+
|
|
79
|
+
Run `scripts/validate-design.py` as directed in the main skill. It reads UTF-8 Markdown without modifying it. Headings, tags, and labels inside backtick or tilde fenced examples do not count; close every fence. Real requirements use unique sequential `### R1: <name>`, `### R2: <name>`, … headings with nonempty names. Each block needs a standalone `**Acceptance criteria**` label in its main body, before any tag or risk-note heading (an explanatory suffix after an em dash, dash, or colon is allowed) and nonempty criteria before the next heading or bold section label. A bare bullet or content borrowed from risk notes is not a criterion section.
|
|
80
|
+
|
|
81
|
+
Write each selected tag as one value, not the template's pipe-separated alternatives. Missing optional tags retain `Checkpoints: none`, `Review: skip`, and `Feature review: auto`. Non-trivial designs require nonempty `## Feature acceptance` scenarios before their feature-review tag. Only the minimal trivial shape above receives the no-Feature-acceptance exception.
|
|
82
|
+
|
|
83
|
+
The helper checks structure, not meaning: it does not prove Given/When/Then quality, testability, complete edge/error coverage, good slicing, risk classification, confirmed assumptions, or human approval. Those remain the agent's audit and the human's judgment. Structural success does not complete the design gate by itself.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Umbrella mechanics
|
|
2
|
+
|
|
3
|
+
An umbrella splits one large requirement into multiple design docs that ship together as **one PR**. It is a single-topic folder with more than one leaf: one branch, one `pwk-finalizing` at the end, which disposes the folder as one unit. Design only the current slice; ground later slices in implemented predecessor code. The split is intra-PR decomposition — a way to keep each design/execute cycle small and focused, not a multi-PR strategy.
|
|
4
|
+
|
|
5
|
+
**First brainstorm** (the requirement is too big for one design doc):
|
|
6
|
+
|
|
7
|
+
1. **Propose the split** — the parts, a one-line scope each, and build order. Get human approval before writing anything beyond discovery.
|
|
8
|
+
2. **Write the overview** — `docs/plans/<date>-<topic>/overview.md`, in the topic's own folder (every leaf lives beside it: `<leaf>-design.md`, `<leaf>-progress.md`, `<leaf>-review-packet*.md`), a **status-free roster**:
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
# Overview: <topic>
|
|
12
|
+
|
|
13
|
+
Goal: <one line — what the whole requirement delivers>
|
|
14
|
+
|
|
15
|
+
## Parts (build order)
|
|
16
|
+
1. <leaf> — <one-line scope>
|
|
17
|
+
2. <leaf> — <one-line scope>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `## Parts (build order)` heading and the `N. <leaf> — <one-line scope>` item shape together are the **roster contract**: `pwk-finalizing` and `pwk-status` both read them, and both are lint-pinned so a drifted heading fails loudly instead of reading an empty roster. Goal, parts with one-line scopes, build order — **no status column**. No skill mutates the overview between this write and `pwk-finalizing` (which disposes it); part-completion is inferred from each leaf's own `*-progress.md`.
|
|
21
|
+
3. **Write the first part's design doc** — `docs/plans/<date>-<topic>/<leaf>-design.md` in the same folder, then hand off to `/skill:pwk-executing-tasks`.
|
|
22
|
+
|
|
23
|
+
**Later parts** — re-run `/skill:pwk-brainstorming` for the next part. Read the overview for big-picture/roster context (which slice is yours, which siblings exist), then report each prior leaf's `Feature phase:` line (matched header-only — `grep -m1 '^Feature phase:' <leaf>-progress.md` beside the overview — no body ingest; a leaf with no progress file yet reports `not started`) before designing the slice, then explore the codebase to design your slice **as brainstorm always does** — prior parts are just implemented code in the repo by then. Predecessor phases are visibility, not enforcement: design the slice regardless; cross-slice decisions that must persist go in an ADR, not the overview.
|
|
24
|
+
|
|
25
|
+
The whole umbrella is one branch and one PR: `pwk-executing-tasks` creates the branch in its pre-flight and reuses it for later parts, and suggests the next part (or finalize after the last); `pwk-finalizing` disposes the whole folder — overview + every leaf — and ships one PR.
|
|
26
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only validation of the workflow's Markdown design structure (Python 3.9+)."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TAG_VALUES = {
|
|
13
|
+
"Checkpoints": ("none", "full"),
|
|
14
|
+
"Review": ("skip", "parallel", "inline"),
|
|
15
|
+
"Feature review": ("auto", "parallel", "inline"),
|
|
16
|
+
}
|
|
17
|
+
HEADING = re.compile(r"^ {0,3}(#{1,6})[ \t]+(.+?)\s*#*\s*$")
|
|
18
|
+
REQUIREMENT = re.compile(r"^R([1-9][0-9]*):\s*(.*)$")
|
|
19
|
+
REQUIREMENT_LIKE = re.compile(r"^R(?:[0-9]|\s*:)")
|
|
20
|
+
CRITERIA = re.compile(r"^\*\*Acceptance criteria\*\*(?:\s*(?:—|–|-|:).*)?$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Heading:
|
|
25
|
+
level: int
|
|
26
|
+
title: str
|
|
27
|
+
index: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def visible_lines(text: str) -> Tuple[List[str], List[str]]:
|
|
31
|
+
"""Blank fenced examples, retaining indexes for diagnostics. Not a Markdown renderer."""
|
|
32
|
+
lines = []
|
|
33
|
+
errors = []
|
|
34
|
+
fence = ""
|
|
35
|
+
fence_line = 0
|
|
36
|
+
for number, line in enumerate(text.splitlines(), 1):
|
|
37
|
+
if fence:
|
|
38
|
+
if re.fullmatch(r" {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}[ \t]*", line):
|
|
39
|
+
fence = ""
|
|
40
|
+
lines.append("")
|
|
41
|
+
continue
|
|
42
|
+
opening = re.match(r"^ {0,3}(`{3,}|~{3,})(.*)$", line)
|
|
43
|
+
if opening and not (opening[1][0] == "`" and "`" in opening[2]):
|
|
44
|
+
fence = opening[1]
|
|
45
|
+
fence_line = number
|
|
46
|
+
lines.append("")
|
|
47
|
+
else:
|
|
48
|
+
lines.append(line)
|
|
49
|
+
if fence:
|
|
50
|
+
errors.append(f"line {fence_line}: unclosed code fence; close it before validating")
|
|
51
|
+
return lines, errors
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def has_content(lines: List[str]) -> bool:
|
|
55
|
+
# Check presence, not whether the assertion is true, testable, or sufficiently specific.
|
|
56
|
+
for line in lines:
|
|
57
|
+
text = re.sub(r"^\s*(?:[-+*]|[0-9]+[.)])\s*", "", line).strip()
|
|
58
|
+
if text and re.search(r"\w", text):
|
|
59
|
+
return True
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def criteria_body(lines: List[str], start: int, end: int) -> List[str]:
|
|
64
|
+
"""A heading or another bold section label terminates criteria, not just the next R ID."""
|
|
65
|
+
result = []
|
|
66
|
+
for line in lines[start:end]:
|
|
67
|
+
if HEADING.match(line) or re.match(r"^\s*\*\*[^*]+\*\*", line):
|
|
68
|
+
break
|
|
69
|
+
result.append(line)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate(text: str) -> Tuple[List[str], List[str]]:
|
|
74
|
+
lines, errors = visible_lines(text)
|
|
75
|
+
headings = []
|
|
76
|
+
for index, line in enumerate(lines):
|
|
77
|
+
match = HEADING.match(line)
|
|
78
|
+
if match:
|
|
79
|
+
headings.append(Heading(len(match[1]), match[2], index))
|
|
80
|
+
for heading in headings:
|
|
81
|
+
if REQUIREMENT_LIKE.match(heading.title) and heading.level != 3:
|
|
82
|
+
errors.append(f"line {heading.index + 1}: {heading.title} must use a ### requirement heading")
|
|
83
|
+
requirements = [h for h in headings if h.level == 3 and REQUIREMENT_LIKE.match(h.title)]
|
|
84
|
+
features = [h for h in headings if h.level == 2 and h.title == "Feature acceptance"]
|
|
85
|
+
summaries = []
|
|
86
|
+
|
|
87
|
+
if not requirements:
|
|
88
|
+
errors.append("required requirement block missing; add ### R1: <name> with Acceptance criteria")
|
|
89
|
+
seen = set()
|
|
90
|
+
for expected, heading in enumerate(requirements, 1):
|
|
91
|
+
match = REQUIREMENT.fullmatch(heading.title)
|
|
92
|
+
if not match:
|
|
93
|
+
errors.append(f"line {heading.index + 1}: malformed requirement '{heading.title}'; use ### R{expected}: <name>")
|
|
94
|
+
continue
|
|
95
|
+
number = int(match[1])
|
|
96
|
+
if number in seen:
|
|
97
|
+
errors.append(f"line {heading.index + 1}: duplicate requirement R{number}")
|
|
98
|
+
elif number != expected:
|
|
99
|
+
errors.append(f"line {heading.index + 1}: expected R{expected}, found R{number}; IDs must be sequential in document order")
|
|
100
|
+
seen.add(number)
|
|
101
|
+
if not match[2].strip():
|
|
102
|
+
errors.append(f"line {heading.index + 1}: R{number} requires a nonempty name")
|
|
103
|
+
|
|
104
|
+
intro_end = requirements[0].index if requirements else len(lines)
|
|
105
|
+
short = any(re.match(r"^In short:\s*\S.*$", line) for line in lines[:intro_end])
|
|
106
|
+
full_format = any(h.level == 2 and h.title in ("At a glance", "Requirements") for h in headings)
|
|
107
|
+
trivial = short and not full_format and len(requirements) == 1 and requirements[0].title.startswith("R1:") and not features
|
|
108
|
+
if not trivial and not features:
|
|
109
|
+
errors.append("## Feature acceptance is required for non-trivial designs; the trivial exception needs an In short: one-liner and a single R1 block without full-format sections")
|
|
110
|
+
if len(features) > 1:
|
|
111
|
+
errors.append("duplicate ## Feature acceptance section; keep exactly one")
|
|
112
|
+
|
|
113
|
+
# Record ownership so a valid-looking tag cannot silently land outside its section.
|
|
114
|
+
owned_tags = set()
|
|
115
|
+
|
|
116
|
+
def tags(start: int, end: int, names: Tuple[str, ...], owner: str) -> dict:
|
|
117
|
+
values = {name: TAG_VALUES[name][0] for name in names}
|
|
118
|
+
found = set()
|
|
119
|
+
for heading in headings:
|
|
120
|
+
if not start <= heading.index < end:
|
|
121
|
+
continue
|
|
122
|
+
for name in names:
|
|
123
|
+
if heading.level != 3 or not re.match(re.escape(name) + r"\b", heading.title):
|
|
124
|
+
continue
|
|
125
|
+
owned_tags.add(heading.index)
|
|
126
|
+
if name in found:
|
|
127
|
+
errors.append(f"line {heading.index + 1}: duplicate {name} tag in {owner}")
|
|
128
|
+
found.add(name)
|
|
129
|
+
value = heading.title[len(name):].strip()
|
|
130
|
+
value = value[1:].strip() if value.startswith(":") else ""
|
|
131
|
+
if value not in TAG_VALUES[name]:
|
|
132
|
+
allowed = " | ".join(TAG_VALUES[name])
|
|
133
|
+
errors.append(f"line {heading.index + 1}: invalid {name} tag in {owner}; expected {allowed}")
|
|
134
|
+
else:
|
|
135
|
+
values[name] = value
|
|
136
|
+
return values
|
|
137
|
+
|
|
138
|
+
for heading in requirements:
|
|
139
|
+
end = next((h.index for h in headings if h.index > heading.index and (h.level <= 2 or h in requirements)), len(lines))
|
|
140
|
+
criteria_end = next((h.index for h in headings if heading.index < h.index < end), end)
|
|
141
|
+
labels = [i for i in range(heading.index + 1, criteria_end) if CRITERIA.fullmatch(lines[i].strip())]
|
|
142
|
+
owner = heading.title.split(":", 1)[0]
|
|
143
|
+
if not labels:
|
|
144
|
+
errors.append(f"line {heading.index + 1}: {owner} needs a standalone **Acceptance criteria** section")
|
|
145
|
+
elif len(labels) > 1:
|
|
146
|
+
errors.append(f"line {labels[1] + 1}: {owner} has duplicate Acceptance criteria sections")
|
|
147
|
+
elif not has_content(criteria_body(lines, labels[0] + 1, end)):
|
|
148
|
+
errors.append(f"line {labels[0] + 1}: {owner} Acceptance criteria is empty; add a criterion before the next section")
|
|
149
|
+
values = tags(heading.index + 1, end, ("Checkpoints", "Review"), owner)
|
|
150
|
+
summaries.append(f"{owner}: Checkpoints={values['Checkpoints']}, Review={values['Review']}")
|
|
151
|
+
|
|
152
|
+
for heading in features:
|
|
153
|
+
end = next((h.index for h in headings if h.index > heading.index and h.level <= 2), len(lines))
|
|
154
|
+
if not has_content(criteria_body(lines, heading.index + 1, end)):
|
|
155
|
+
errors.append(f"line {heading.index + 1}: Feature acceptance is empty; add an end-to-end criterion before its review tag")
|
|
156
|
+
values = tags(heading.index + 1, end, ("Feature review",), "Feature acceptance")
|
|
157
|
+
summaries.append(f"Feature review={values['Feature review']}")
|
|
158
|
+
|
|
159
|
+
for heading in headings:
|
|
160
|
+
for name in TAG_VALUES:
|
|
161
|
+
if re.match(re.escape(name) + r"\b", heading.title) and heading.index not in owned_tags:
|
|
162
|
+
owner = "## Feature acceptance" if name == "Feature review" else "a ### R<n>: requirement block"
|
|
163
|
+
errors.append(f"line {heading.index + 1}: {name} must be a ### tag inside {owner}")
|
|
164
|
+
return errors, summaries
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
168
|
+
parser = argparse.ArgumentParser(
|
|
169
|
+
description="Read-only structural design validation. Requires Python 3.9+; standard library only. Semantic correctness is not verified.",
|
|
170
|
+
epilog="Pass an explicit design path. Absolute paths are used directly; relative paths resolve from the caller's working directory (cwd), not the installed skill directory.",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument("design", type=Path, help="UTF-8 Markdown design file (quote paths containing spaces)")
|
|
173
|
+
args = parser.parse_args(argv)
|
|
174
|
+
if sys.version_info < (3, 9):
|
|
175
|
+
print("Python 3.9+ is required; supply a compatible runtime before validation.", file=sys.stderr)
|
|
176
|
+
return 2
|
|
177
|
+
try:
|
|
178
|
+
text = args.design.read_text(encoding="utf-8")
|
|
179
|
+
except (OSError, UnicodeError, ValueError) as error:
|
|
180
|
+
print(f"{args.design}: cannot read UTF-8 design: {error}", file=sys.stderr)
|
|
181
|
+
return 2
|
|
182
|
+
errors, summaries = validate(text)
|
|
183
|
+
if errors:
|
|
184
|
+
for error in errors:
|
|
185
|
+
print(f"{args.design}: {error}", file=sys.stderr)
|
|
186
|
+
return 1
|
|
187
|
+
print(f"{args.design}: structural checks passed. Semantic correctness is not verified.")
|
|
188
|
+
for summary in summaries:
|
|
189
|
+
print(f" {summary}")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
sys.exit(main())
|
|
@@ -11,71 +11,16 @@ The feature-acceptance E2E test is the primary enforced gate and the primary enf
|
|
|
11
11
|
|
|
12
12
|
## Before you start
|
|
13
13
|
|
|
14
|
+
Resolve local references and scripts from this installed skill directory. If a required resource is missing, report the blocker and stop before the affected action; do not silently skip its instructions.
|
|
15
|
+
|
|
14
16
|
1. **Git state** — `git status` + `git log --oneline -5`; note uncommitted changes.
|
|
15
|
-
2. **Find the doc** — first 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). Then list `docs/plans` recursively, excluding docs/plans/completed/, for `*-design.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/*'`. If no doc at all, ask the user to run `/skill:pwk-brainstorming` first; if several, ask which. Report one line, e.g. `Found: design "auth" — feature-gate execute (E2E written, implementing 2/5)`. A matching `*-progress.md` means this is a **resume** (see [Resume](#resume)). An umbrella part whose prior leaves are not all at `done`: surface their `Feature phase:` lines (matched header-only from each `<leaf>-progress.md`) as advisory context in the pre-flight report — no refusal, no reorder; build order stays advisory.
|
|
16
|
-
3. **Workspace —
|
|
17
|
+
2. **Find the doc** — first 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). Then list `docs/plans` recursively, excluding docs/plans/completed/, for `*-design.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/*'`. If no doc at all, ask the user to run `/skill:pwk-brainstorming` first; if several, ask which. Report one line, e.g. `Found: design "auth" — feature-gate execute (E2E written, implementing 2/5)`. A matching `*-progress.md` means this is a **resume** (see [Resume and progress](#resume-and-progress)). An umbrella part whose prior leaves are not all at `done`: surface their `Feature phase:` lines (matched header-only from each `<leaf>-progress.md`) as advisory context in the pre-flight report — no refusal, no reorder; build order stays advisory.
|
|
18
|
+
3. **Workspace — choose this topic’s branch/worktree.** Before creating, switching, or adopting one, read [Workspace and resume details](references/resume.md#workspace). Reuse this topic’s branch (one umbrella = one branch); a different topic’s branch never hosts these commits. Uncommitted planning docs do not cross a worktree boundary: commit this topic folder on its branch before a new worktree handoff; when adopting an existing checkout with uncommitted docs here, copy the topic folder there, verify it, then remove the source copy. Read the reference's command order before acting. Offer/adopt only after checking existing worktrees; wait for the user's choice.
|
|
17
19
|
|
|
18
20
|
## First run
|
|
19
21
|
|
|
20
22
|
1. **Parse the design doc** — read every `### R<n>:` heading and its `### Checkpoints` / `### Review` tags (defaults `none` / `skip`), plus the feature-level `### Feature review` tag in the `## Feature acceptance` section. Requirements run in **listed order** (build order); do not reorder. Read the `## Feature acceptance` section — it is the E2E you gate on first. A trivial design doc (no `## Feature acceptance`) skips steps 5–6 below and goes straight into the implement phase.
|
|
21
|
-
2. **Create the progress file** `docs/plans/<date>-<topic>/<leaf>-progress.md
|
|
22
|
-
|
|
23
|
-
```markdown
|
|
24
|
-
# Progress: <topic>
|
|
25
|
-
|
|
26
|
-
Design: docs/plans/<date>-<topic>/<leaf>-design.md
|
|
27
|
-
Branch: <branch>
|
|
28
|
-
Setup: n/a
|
|
29
|
-
Started: <ISO timestamp>
|
|
30
|
-
Last updated: <ISO timestamp>
|
|
31
|
-
Feature phase: e2e-written
|
|
32
|
-
|
|
33
|
-
<!-- a trivial design doc (no `## Feature acceptance`) starts at `implementing (0/N)` instead — there is no E2E to write -->
|
|
34
|
-
|
|
35
|
-
## Requirements
|
|
36
|
-
| # | Done | Requirement | Per-req ceremony | Commit |
|
|
37
|
-
|---|------|-------------|-----------------|--------|
|
|
38
|
-
| 1 | ⬜ | <requirement name> | — | — |
|
|
39
|
-
|
|
40
|
-
## Execution summary
|
|
41
|
-
| R# | Requirement | How it was built | Deviated? |
|
|
42
|
-
|----|-------------|------------------|-----------|
|
|
43
|
-
| 1 | <requirement name> | | |
|
|
44
|
-
|
|
45
|
-
## Deviation records
|
|
46
|
-
|
|
47
|
-
<!-- One short paragraph per design-decision reversal (what changed, why, what was rejected), written at deviation time; leave this section empty when every Deviated? entry is mechanical. -->
|
|
48
|
-
|
|
49
|
-
## Code digest
|
|
50
|
-
|
|
51
|
-
<!-- Written once, after the feature review passes; never back-filled per requirement. -->
|
|
52
|
-
|
|
53
|
-
### Summary
|
|
54
|
-
### Flow
|
|
55
|
-
Spine
|
|
56
|
-
<entry point> -> [R1] <step> -> [R2] <step> -> <outcome>
|
|
57
|
-
Branches
|
|
58
|
-
<condition> -> <outcome> [R2]
|
|
59
|
-
<changed behavior> was: <previous behavior>
|
|
60
|
-
Side effects
|
|
61
|
-
reads: <what> writes: <what>
|
|
62
|
-
### Gotchas
|
|
63
|
-
### Key files
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The `## Code digest` is filled once, at the write point in the ship checkpoint — never per requirement. Fill rules: plain language, R# anchors where natural, no test names (the execution-summary rule). `### Summary` is 2–3 sentences: what the code now does differently, and why. `### Flow` uses `A -> B -> C` arrow chains. `### Gotchas` lifts real risks from the review findings — `[ALERT]` only for reviewer-confirmed issues, never invented; with no findings, write `none beyond review findings` and mean it. `### Key files` is capped at 5 pivotal files, one line each: what shifted inside them.
|
|
67
|
-
|
|
68
|
-
**Flow shape** — this is the ship stop's navigable map, so it is the one digest section that carries structure, not just prose:
|
|
69
|
-
|
|
70
|
-
- **Spine** — the happy path as `A -> B -> C` hops, each hop named by symbol or module, with no line numbers anywhere and no file paths: symbol names survive edits and stay greppable, so a stale anchor can never mislead. Tag each hop `[R<n>]` with the requirement it comes from — that is what lets the coverage table's verdict rows be traced into the code without opening the diff.
|
|
71
|
-
- **Branches** — every alternative and error path as `condition -> outcome` (a linear spine with no branches reads as "there are no edge cases", which is rarely true). Put ⚠️ on the genuinely surprising ones. Add a `was: <previous behavior>` clause for each requirement whose behavior changed; skip it for purely additive requirements — honest-empty, never invented.
|
|
72
|
-
- **Inline worked values** — where a mapping is non-obvious, show it concretely (`` `implementing (2/5)` -> `execute 2/5` ``), so a reader can falsify it without running anything.
|
|
73
|
-
- **Side effects** — a line naming what is read and written, only when the feature performs I/O, network, or migrations; omit it entirely otherwise rather than filling a placeholder.
|
|
74
|
-
- **Cap: roughly 15 lines.** If the change genuinely needs more, the ship checkpoint **offers `/skill:pwk-walkthrough`** as the deep read (it regenerates a `file:line`-anchored walkthrough on demand) instead of growing the digest — the digest is what the human reads at the one remaining stop, and an unbounded Flow recreates the digging it exists to prevent.
|
|
75
|
-
|
|
76
|
-
`Feature phase` is one of: `e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`, `done`.
|
|
77
|
-
|
|
78
|
-
The `Setup:` header slot takes one of `Setup: pending | done | n/a` — the header starts `Setup: pending` when the design doc has a `## Setup` section, else `Setup: n/a`; the setup checkpoint below flips it to `done` on approval. The phase line stays `e2e-written` through the setup stop — the Setup slot, not the phase, is what flags the not-yet-approved window, and the E2E is written after setup.
|
|
23
|
+
2. **Create the progress file** beside the design at `docs/plans/<date>-<topic>/<leaf>-progress.md`. Before creating it, read [Progress format](references/progress-format.md) for the exact template, header slots, and row vocabulary. Start at `Feature phase: e2e-written` (trivial doc: `implementing (0/N)`); `Setup: pending` when the design has `## Setup`, otherwise `Setup: n/a`.
|
|
79
24
|
|
|
80
25
|
3. **Setup pre-flight** *(only if the design doc has a `## Setup` section)* — install dependencies, apply migrations, seed data, then run the existing test suite. **⏸ CHECKPOINT: setup** — present results and wait for approval, then set `Setup: done` in the progress-file header so a resume can confirm it rather than assume it.
|
|
81
26
|
|
|
@@ -83,24 +28,15 @@ The feature-acceptance E2E test is the primary enforced gate and the primary enf
|
|
|
83
28
|
5. **Write the feature-acceptance E2E test (red).** Read the design doc's `## Feature acceptance` section and encode it as a real test file; run it; confirm it **fails** (it must — little or none of the feature exists yet). If it passes immediately, probe why before proceeding: the behavior may already exist, or the test may be asserting too little. If it still passes and the expected behavior looks wrong, **stop and present** rather than implementing against a wrong spec. Iterate it to green as you implement — fixing the implementation is the default, and you **stop and present only when you are genuinely blocked** (nothing you can do makes it pass, or the E2E itself is wrong). That stop is the reserved failure case; it is not the flow's default pause.
|
|
84
29
|
6. **Notice, not a stop — report the E2E and continue.** Post 1–2 plain-language lines stating **what the E2E proves** ("this test proves that …"), plus the E2E test and its failing output, then go straight into the implement phase **without waiting for approval** — the `## Feature acceptance` text was approved during brainstorm, so there is nothing new here for the human to sign off. The notice is the window to object *before* implementation starts: a human who sees the E2E asserting the wrong thing says so, and you revise, re-run, and re-notice. Keep `Feature phase: e2e-written`.
|
|
85
30
|
|
|
86
|
-
## Resume
|
|
87
|
-
|
|
88
|
-
Read the progress file's `Feature phase` (match the line — e.g. `grep -m1 '^Feature phase:' <file>`), the Requirements table (the first row whose Done cell is not terminal routes the next requirement — `⬜`, `🔄`, and blank all mean not-done; `✅`/`❌`/`⏭` are resolved and are skipped past), and the Execution summary rows (how prior parts were built); read from the top through the end of `## Execution summary` and stop — the sections after it (deviation records, code digest) carry nothing the resume needs, with one exception: the `ship-paused` route re-presents the ship checkpoint, so it also reads the `## Code digest` written below. Before routing: if the header reads `Setup: pending` and the design doc has a `## Setup` section, the setup checkpoint was never approved — re-run the setup verification and wait at it:
|
|
89
|
-
- `e2e-written` → write the E2E if not yet present, post the notice, and continue into the implement phase (no stop). A trivial design doc has no `## Feature acceptance` — skip both and go straight into the implement phase.
|
|
90
|
-
- `implementing (k/N)` → continue the next not-yet-terminal requirement.
|
|
91
|
-
- `reviewing` → continue/finish the feature review, then assemble the **ship** checkpoint.
|
|
92
|
-
- `ship-paused` → re-present the ship checkpoint and wait — the execution summary, verdict rows, and code digest re-read from the progress file; if the prior session's reviewer reports are gone, re-run the feature review per the tag first (the checkpoint is never presented without coverage).
|
|
31
|
+
## Resume and progress
|
|
93
32
|
|
|
94
|
-
|
|
33
|
+
`Feature phase` is one of: `e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`, `done`.
|
|
95
34
|
|
|
96
|
-
|
|
35
|
+
**Done cell:** `⬜` not started · `🔄` in progress · `✅` done · `❌` failed · `⏭` skipped. A row is **terminal** when its Done cell is `✅`, `❌`, or `⏭` — resolved, not necessarily passed. Only executing changes Done cells; a requirement under review stays `🔄` until a clean pass permits `✅`.
|
|
97
36
|
|
|
98
|
-
|
|
37
|
+
When a matching progress file exists, read [Resume](references/resume.md#resume) before routing. Read the header, Requirements table, and Execution summary only; `ship-paused` also needs Code digest. A pending setup checkpoint must be re-verified and approved before proceeding. Resume the first nonterminal row (`⬜`, `🔄`, blank); `✅`/`❌`/`⏭` are terminal, resolved rather than necessarily passed. Match `grep -m1 '^Feature phase:' <file>` for the phase header; `reviewing` resumes review; `ship-paused` re-presents and waits, never approves itself. Missing reviewer reports require review again before showing coverage.
|
|
99
38
|
|
|
100
|
-
|
|
101
|
-
- **Per-req ceremony cell** echoes the design doc's tag, not progress: `—` (default `none`/`skip`) · `⏸ full` (fires two stops — after the slice's tests and after it is complete) · `🔎 inline` · `🔎 parallel`.
|
|
102
|
-
|
|
103
|
-
**Execution summary rows are written in the same step as marking a requirement ✅** — never retrofitted at the end. "How it was built" = one or two plain sentences: what it does now + the approach actually taken; file names sparingly; **no test names, no code** (the human reads this at the ship checkpoint — big picture only). If the implementation departs from the design, fill the Deviated? column **at deviation time** (when the departure happens), with a one-line why — it is a log, not a stop. A departure that **reverses or alters a design decision** gets a **deviation decision-record**: a short paragraph (what changed, why, what was rejected) written into the progress file while the knowledge is fresh — mechanical deviations keep the one-liner. `pwk-finalizing`'s learning sweep harvests these records for ADRs before the docs are disposed.
|
|
39
|
+
Before any progress-row update or code-digest write, read [Progress format](references/progress-format.md). Update the matching row directly, `Last updated`, and `Feature phase`; write execution-summary content at the same time as ✅, and deviations at deviation time. A failed/skipped row carries its reason in the Requirement cell and reconciles its test coverage so the suite stays green.
|
|
104
40
|
|
|
105
41
|
## Implement phase
|
|
106
42
|
|
|
@@ -119,7 +55,7 @@ Set `Feature phase: implementing (0/N)` and work the requirements in listed orde
|
|
|
119
55
|
|
|
120
56
|
### Per-requirement review (opt-in)
|
|
121
57
|
|
|
122
|
-
If the requirement's `### Review` tag is `parallel` or `inline` (default `skip`), review that slice at step 7 —
|
|
58
|
+
If the requirement's `### Review` tag is `parallel` or `inline` (default `skip`), review that slice at step 7 — read [Review mechanics](references/review.md) first and use its helper with `--scope requirement --requirement N`, limited to the commits and criteria of that requirement, written to `<design doc's directory>/<leaf>-review-packet-r<N>.md` (requirement-suffixed, so per-requirement packets never overwrite the feature packet or each other). The row stays 🔄 through the review: the four delegated roles — or the one `pwk-code-review` pass on `inline` — report findings, you apply the smell fixes yourself and commit them (the roles are read-only reporters), and a clean pass returns to step 8, which marks the row ✅. With `skip`, no per-requirement review — step 8 fires directly, and the feature-level review covers it.
|
|
123
59
|
|
|
124
60
|
With the default `none`, no per-requirement stop fires — the per-requirement test still runs red→green, and the feature review covers implementation quality.
|
|
125
61
|
|
|
@@ -137,7 +73,7 @@ When every requirement's Done cell is terminal — `✅` (done), `❌` (failed,
|
|
|
137
73
|
1. **Run the FULL test suite** — a failure means one requirement regressed another; fix it now, in execute context.
|
|
138
74
|
2. **Run the feature-acceptance E2E** — the test you wrote at the start. It must be **green** now that all requirements have landed. (A `❌`/`⏭` requirement's assertions were reconciled when its row went terminal, so a still-red E2E still means a requirement is missing or wrong.) If it is still red, a requirement is missing or wrong — fix it before proceeding. (A trivial design doc has no `## Feature acceptance` — the full suite staying green is its gate instead.)
|
|
139
75
|
3. **Run the feature review** (below) per the design's `### Feature review` tag — set `Feature phase: reviewing` first, so a mid-review resume routes into this step instead of the implement loop. (A trivial design doc carries no `### Feature review` tag — the missing tag defaults to `auto`, which resolves on its risk content; a trivial diff is typically `inline`, one pass, and there is no E2E to re-green — the full suite is the gate.) The review runs **before** your final approval, so the pause is fully informed. Apply smell fixes yourself and re-green (full suite + E2E) before pausing.
|
|
140
|
-
4. **Write the code digest** into the progress file — the review has succeeded, findings are fixed, and the code is final: read the packet's `## Commits`, `## Changed files`, and `## Diff` sections and fill the progress file's `## Code digest` (
|
|
76
|
+
4. **Write the code digest** into the progress file — the review has succeeded, findings are fixed, and the code is final: read the packet's `## Commits`, `## Changed files`, and `## Diff` sections and fill the progress file's `## Code digest` using [Progress format](references/progress-format.md) and its fill rules. If the packet is stale or missing, read [Review mechanics](references/review.md) and re-run the helper before writing. A resumed `Feature phase: reviewing` that completes lands on this same write point before the checkpoint is assembled. Never a gate: it explains the change, it does not block shipping.
|
|
141
77
|
|
|
142
78
|
**Reconcile the Flow against reviewed reality before writing it.** The Flow is written from what was reviewed, not from recollection: in `parallel` mode every hop must correspond to a path the tracing reviewer's report names — no hop invented, no reported path dropped. In `inline` mode there is no separate tracing report — the inline pass itself performs code tracing — so check the Flow against that pass's tracing findings and spec-coverage result instead. A discrepancy you cannot resolve is **surfaced** to the human in the ship checkpoint's findings status, never smoothed over by writing a Flow that omits the path.
|
|
143
79
|
5. **Set `Feature phase: ship-paused`** and **⏸ CHECKPOINT: ship** — present, in this order:
|
|
@@ -159,40 +95,14 @@ The old "integration gate" is gone — the feature E2E at the ship checkpoint *i
|
|
|
159
95
|
|
|
160
96
|
This is step 3 of the [ship checkpoint](#ship-checkpoint-all-requirements-terminal--review-merged): it runs **before** the final human approval, so the pause is fully informed. Run **one** review over the **whole feature diff**, driven by the design doc's `### Feature review` tag — exactly one per part, never once per requirement. This is the single thorough review — per-requirement reviews, if any, only saw slices in isolation.
|
|
161
97
|
|
|
162
|
-
**Assemble the review packet first** —
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
PACKET="<design doc's directory>/<leaf>-review-packet.md" # beside the design doc, inside the topic folder — docs/plans/<date>-<topic>/<leaf>-review-packet.md
|
|
166
|
-
FEATURE_BASE="$(git rev-parse $(grep -m1 -o '^| [0-9]* | [^|]* | [^|]* | [^|]* | [0-9a-f]\{7,\}' <progress file> | grep -o '[0-9a-f]\{7,\}' | head -1)^)" # parent of the first recorded Commit-column entry
|
|
167
|
-
{
|
|
168
|
-
echo "# Review packet: <topic> — feature review"
|
|
169
|
-
echo
|
|
170
|
-
echo "## Commits"
|
|
171
|
-
git log --oneline $FEATURE_BASE..HEAD
|
|
172
|
-
echo
|
|
173
|
-
echo "## Changed files"
|
|
174
|
-
git diff --stat $FEATURE_BASE...HEAD
|
|
175
|
-
echo
|
|
176
|
-
echo "## Acceptance criteria (verbatim from the design doc)"
|
|
177
|
-
sed -n '/^### R1/,/^## Feature acceptance/p' <design-doc path> | sed '/^## Feature acceptance/,$d'
|
|
178
|
-
echo
|
|
179
|
-
echo "## Feature acceptance (verbatim)"
|
|
180
|
-
sed -n '/^## Feature acceptance/,/^### Feature review/p' <design-doc path> | sed '/^### Feature review/,$d'
|
|
181
|
-
echo
|
|
182
|
-
echo "## Production-risk notes (verbatim, if any)"
|
|
183
|
-
sed -nE '/^### Production-risk notes/,/^(## |### R[0-9])/p' <design-doc path> | sed -E '/^(## |### R[0-9])/d'
|
|
184
|
-
echo
|
|
185
|
-
echo "## Diff"
|
|
186
|
-
git diff $FEATURE_BASE...HEAD
|
|
187
|
-
} > "$PACKET"
|
|
188
|
-
```
|
|
98
|
+
**Assemble the review packet first** — read [Review mechanics](references/review.md), then invoke the installed skill’s `scripts/build-review-packet.py` with explicit `--project`, `--design`, `--progress`, and `--scope feature`. Check `python3 --version` for Python 3.9+ and helper availability before this step; if Python is missing or unavailable, report the blocker, request a suitable runtime, and stop. Do not auto-install a runtime, use a fallback implementation, or claim packet generation succeeded. No packet byte passes through model output. A failed helper is not usable evidence or approval; correct the inputs, never guess ranges. The helper writes `<leaf>-review-packet.md` beside the design; opt-in packets use `<leaf>-review-packet-r<N>.md`.
|
|
189
99
|
|
|
190
100
|
- **Resolve the tag first.** `auto` (the default, and what a missing tag means) resolves on the design's own production-risk content: if the design carries a non-empty `## Production-risk areas` section, or any requirement carries a non-empty `### Production-risk notes`, resolve to `parallel`; otherwise resolve to `inline`. An explicit `parallel` or `inline` is used as written and always wins over `auto` — explicit tag wins in both directions, so a human who wants one pass on a risky design gets one pass.
|
|
191
101
|
|
|
192
|
-
- **`parallel`**
|
|
193
|
-
|
|
102
|
+
- **`parallel`** — use the host’s `parallel-review` capability for four independent, fresh-context, read-only roles: `pwk-spec-reviewer`, `pwk-tracing-reviewer`, `pwk-smell-reviewer`, `pwk-hazard-reviewer`. Follow [Review mechanics](references/review.md#delegate-or-review-inline): a one-liner packet-file pointer with role framing last; the packet never appears in spawn arguments. Collect one outcome per role. Reviewers only report; you apply smell fixes, commit, and re-green full suite + E2E. Surface trace/spec/hazard findings to the human.
|
|
194
103
|
- **`inline`** — perform `/skill:pwk-code-review` over the whole diff as a single pass.
|
|
195
|
-
- **Fallback** — if
|
|
104
|
+
- **Fallback** — if compatible capability is absent, required read-only/fresh-context/bounded constraints cannot be proved, or delegation fails, do the missing work inline. Retain successful reports; required missing roles mean review is incomplete.
|
|
105
|
+
- **Refresh** — if commits land during review, use [Review mechanics](references/review.md) to refresh the packet before any replacement role or digest write. Append requirement-fix IDs before a requirement refresh; keep the initial ID. Packet creation never substitutes for review or approval.
|
|
196
106
|
|
|
197
107
|
On success, continue assembling the ship checkpoint; once the human approves it — knowing what failed — set `Feature phase: done`. (An umbrella part's gate is its own progress file; `pwk-finalizing` reads every part's file, and a `❌` in any part blocks the umbrella until the human sends the work back or explicitly types `--force-failed`.)
|
|
198
108
|
|
|
@@ -249,3 +159,35 @@ Feature phase: done
|
|
|
249
159
|
2. Check `git log` for context. Ask the user — clarify beats guessing.
|
|
250
160
|
3. Still stuck → discard uncommitted changes (`git restore .`); if already committed, also `git revert` the requirement's commit(s). **Never leave a failed requirement's partial work on the shipped branch.**
|
|
251
161
|
4. Set its Done cell `❌` (with the reason as a suffix in the Requirement cell), reconcile its test coverage, and move on. Check `docs/lessons.md` — a prior lesson may apply.
|
|
162
|
+
|
|
163
|
+
## Contract index
|
|
164
|
+
|
|
165
|
+
Detailed progress, resume, and review contracts live in `references/`. The following
|
|
166
|
+
entrypoint markers remain canonical: `Feature phase` is one of
|
|
167
|
+
`e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`, `done`; Done cells use
|
|
168
|
+
`⬜`, `🔄`, `✅`, `❌`, `⏭`, where `✅`/`❌`/`⏭` are resolved. Only executing changes
|
|
169
|
+
Done cells; a reviewed row flips only `🔄`→`✅`. `Setup: pending | done | n/a` is the
|
|
170
|
+
header slot, and the ceremony cell echoes the design doc's tag. Execution summary rows
|
|
171
|
+
are written in the same step as marking a requirement ✅, with deviations recorded when
|
|
172
|
+
the departure happens. Resume uses `re-run the setup verification`; packet bounds use
|
|
173
|
+
the `Commit column`, the `parent of the first recorded commit`, and the `previous
|
|
174
|
+
requirement's last commit`. Successful delegated reports are retained. The Flow has
|
|
175
|
+
`Spine`, `Branches`, `Side effects`, `was:`, and no line numbers; it is checked against
|
|
176
|
+
the tracing report and unresolved differences are surfaced. The digest is
|
|
177
|
+
`never back-filled per requirement`, with gotchas `none beyond review findings` and
|
|
178
|
+
key files capped `at 5`.
|
|
179
|
+
|
|
180
|
+
```markdown
|
|
181
|
+
# Progress: <topic>
|
|
182
|
+
Design: docs/plans/<date>-<topic>/<leaf>-design.md
|
|
183
|
+
Feature phase: e2e-written
|
|
184
|
+
## Requirements
|
|
185
|
+
| # | Done | Requirement | Per-req ceremony | Commit |
|
|
186
|
+
## Execution summary
|
|
187
|
+
| R# | Requirement | How it was built | Deviated? |
|
|
188
|
+
## Code digest
|
|
189
|
+
### Summary
|
|
190
|
+
### Flow
|
|
191
|
+
### Gotchas
|
|
192
|
+
### Key files
|
|
193
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Progress format and code digest
|
|
2
|
+
|
|
3
|
+
Read before creating or changing progress rows, or writing/re-presenting the code digest. This reference owns the unchanged progress artifact schema.
|
|
4
|
+
|
|
5
|
+
2. **Create the progress file** `docs/plans/<date>-<topic>/<leaf>-progress.md`, beside the design doc it belongs to (`<leaf>` is `<topic>` for a single-part topic; an umbrella part uses its roster slug), so `pwk-finalizing` finds it inside the topic folder:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# Progress: <topic>
|
|
9
|
+
|
|
10
|
+
Design: docs/plans/<date>-<topic>/<leaf>-design.md
|
|
11
|
+
Branch: <branch>
|
|
12
|
+
Setup: n/a
|
|
13
|
+
Started: <ISO timestamp>
|
|
14
|
+
Last updated: <ISO timestamp>
|
|
15
|
+
Feature phase: e2e-written
|
|
16
|
+
|
|
17
|
+
<!-- a trivial design doc (no `## Feature acceptance`) starts at `implementing (0/N)` instead — there is no E2E to write -->
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
| # | Done | Requirement | Per-req ceremony | Commit |
|
|
21
|
+
|---|------|-------------|-----------------|--------|
|
|
22
|
+
| 1 | ⬜ | <requirement name> | — | — |
|
|
23
|
+
|
|
24
|
+
## Execution summary
|
|
25
|
+
| R# | Requirement | How it was built | Deviated? |
|
|
26
|
+
|----|-------------|------------------|-----------|
|
|
27
|
+
| 1 | <requirement name> | | |
|
|
28
|
+
|
|
29
|
+
## Deviation records
|
|
30
|
+
|
|
31
|
+
<!-- One short paragraph per design-decision reversal (what changed, why, what was rejected), written at deviation time; leave this section empty when every Deviated? entry is mechanical. -->
|
|
32
|
+
|
|
33
|
+
## Code digest
|
|
34
|
+
|
|
35
|
+
<!-- Written once, after the feature review passes; never back-filled per requirement. -->
|
|
36
|
+
|
|
37
|
+
### Summary
|
|
38
|
+
### Flow
|
|
39
|
+
Spine
|
|
40
|
+
<entry point> -> [R1] <step> -> [R2] <step> -> <outcome>
|
|
41
|
+
Branches
|
|
42
|
+
<condition> -> <outcome> [R2]
|
|
43
|
+
<changed behavior> was: <previous behavior>
|
|
44
|
+
Side effects
|
|
45
|
+
reads: <what> writes: <what>
|
|
46
|
+
### Gotchas
|
|
47
|
+
### Key files
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `## Code digest` is filled once, at the write point in the ship checkpoint — never per requirement. Fill rules: plain language, R# anchors where natural, no test names (the execution-summary rule). `### Summary` is 2–3 sentences: what the code now does differently, and why. `### Flow` uses `A -> B -> C` arrow chains. `### Gotchas` lifts real risks from the review findings — `[ALERT]` only for reviewer-confirmed issues, never invented; with no findings, write `none beyond review findings` and mean it. `### Key files` is capped at 5 pivotal files, one line each: what shifted inside them.
|
|
51
|
+
|
|
52
|
+
**Flow shape** — this is the ship stop's navigable map, so it is the one digest section that carries structure, not just prose:
|
|
53
|
+
|
|
54
|
+
- **Spine** — the happy path as `A -> B -> C` hops, each hop named by symbol or module, with no line numbers anywhere and no file paths: symbol names survive edits and stay greppable, so a stale anchor can never mislead. Tag each hop `[R<n>]` with the requirement it comes from — that is what lets the coverage table's verdict rows be traced into the code without opening the diff.
|
|
55
|
+
- **Branches** — every alternative and error path as `condition -> outcome` (a linear spine with no branches reads as "there are no edge cases", which is rarely true). Put ⚠️ on the genuinely surprising ones. Add a `was: <previous behavior>` clause for each requirement whose behavior changed; skip it for purely additive requirements — honest-empty, never invented.
|
|
56
|
+
- **Inline worked values** — where a mapping is non-obvious, show it concretely (`` `implementing (2/5)` -> `execute 2/5` ``), so a reader can falsify it without running anything.
|
|
57
|
+
- **Side effects** — a line naming what is read and written, only when the feature performs I/O, network, or migrations; omit it entirely otherwise rather than filling a placeholder.
|
|
58
|
+
- **Cap: roughly 15 lines.** If the change genuinely needs more, the ship checkpoint **offers `/skill:pwk-walkthrough`** as the deep read (it regenerates a `file:line`-anchored walkthrough on demand) instead of growing the digest — the digest is what the human reads at the one remaining stop, and an unbounded Flow recreates the digging it exists to prevent.
|
|
59
|
+
|
|
60
|
+
`Feature phase` is one of: `e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`, `done`.
|
|
61
|
+
|
|
62
|
+
The `Setup:` header slot takes one of `Setup: pending | done | n/a` — the header starts `Setup: pending` when the design doc has a `## Setup` section, else `Setup: n/a`; the main skill's setup checkpoint flips it to `done` on approval. The phase line stays `e2e-written` through the setup stop — the Setup slot, not the phase, is what flags the not-yet-approved window, and the E2E is written after setup.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## Progress file
|
|
66
|
+
|
|
67
|
+
Update the matching requirement row directly (not via pattern matching that could corrupt the table). Update `Last updated` and `Feature phase` on every change.
|
|
68
|
+
|
|
69
|
+
**Row vocabulary (canonical — this file owns it):**
|
|
70
|
+
|
|
71
|
+
- **Done cell:** `⬜` not started · `🔄` in progress · `✅` done · `❌` failed · `⏭` skipped. A row is **terminal** when its Done cell is `✅`, `❌`, or `⏭` — resolved, not necessarily passed. A `❌`/`⏭` row carries its reason as a suffix in the Requirement cell (finalizing's verdict greps read it), and its test coverage is reconciled in the same step — assertions removed or skipped with the reason; the suite must stay green. This skill sets all five values. A requirement under review stays `🔄`: `pwk-code-review` reports findings and never edits the Done cell — the loop only ever flips `🔄`→`✅` when the review returns clean, and findings leave the row `🔄` for another pass.
|
|
72
|
+
- **Per-req ceremony cell** echoes the design doc's tag, not progress: `—` (default `none`/`skip`) · `⏸ full` (fires two stops — after the slice's tests and after it is complete) · `🔎 inline` · `🔎 parallel`.
|
|
73
|
+
|
|
74
|
+
**Execution summary rows are written in the same step as marking a requirement ✅** — never retrofitted at the end. "How it was built" = one or two plain sentences: what it does now + the approach actually taken; file names sparingly; **no test names, no code** (the human reads this at the ship checkpoint — big picture only). If the implementation departs from the design, fill the Deviated? column **at deviation time** (when the departure happens), with a one-line why — it is a log, not a stop. A departure that **reverses or alters a design decision** gets a **deviation decision-record**: a short paragraph (what changed, why, what was rejected) written into the progress file while the knowledge is fresh — mechanical deviations keep the one-liner. `pwk-finalizing`'s learning sweep harvests these records for ADRs before the docs are disposed.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Commit entries for packet assembly
|
|
78
|
+
|
|
79
|
+
Record immutable hexadecimal commit IDs (7–64 characters). A cell can hold one ID or an ordered, whitespace-separated list: initial implementation first, review-fix commits last. Preserve the first recorded commit when appending a fix; replacing it loses the feature base. Record a commit hash after committing, never amend a commit to include its own hash.
|
|
80
|
+
|
|
81
|
+
The helper reads only the Requirements table Commit column. A done or in-review row needs a recorded commit. Terminal `❌`/`⏭` rows may have `—` when no commit remains; this does not supply a bound for a requirement packet. If the selected or immediately previous requirement lacks a bound, stop and correct the record from Git evidence; never invent one.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Workspace and resume details
|
|
2
|
+
|
|
3
|
+
Read before choosing or adopting a topic branch/worktree, and whenever a matching progress file exists.
|
|
4
|
+
|
|
5
|
+
## Workspace
|
|
6
|
+
|
|
7
|
+
3. **Workspace — create the feature branch** — if you're already on **this topic's** feature branch, **reuse** it: a later umbrella part continues on the same umbrella branch as part 1. If on `main` — or on a different topic's feature branch, which stays that topic's own and never hosts this topic's commits — create or switch to this topic's branch: `git checkout <topic>` when it exists, else `git checkout -b <topic>` — the topic's folder slug (the `docs/plans/<date>-<topic>/` directory name, shared by every leaf of an umbrella), never a leaf slug. **`<topic>` is the folder slug** everywhere it appears in this skill, including the `../<repo>-<topic>` worktree convention below, so a standalone topic and an umbrella part name the same branch and the same worktree. For larger work, first check `git worktree list`: if a worktree already checks out this branch (the same `../<repo>-<topic>` convention), adopt it — resume the session there instead of offering a new `git worktree add`. One worktree per branch, created once: git refuses two checkouts of one branch, so a later umbrella part reuses part 1's worktree and a standalone resume reuses its own. Otherwise offer a worktree (`git worktree add ../<repo>-<topic> <topic>`) and hand off to a new session there so `pwd` is the worktree. **Uncommitted planning docs do not cross a worktree boundary**, and the design doc is still uncommitted (brainstorm never commits): when the user takes the worktree, commit the topic folder on this branch first — the same scoped add as the main skill's First-run commit — then switch this tree back to the branch it came from (e.g. `git checkout main` when it started there; git refuses `worktree add` of a branch that is still checked out here) and only then `git worktree add ../<repo>-<topic> <topic>` and hand off (the new session's First-run commit then has only its progress file to record). The adopt path above is safe only when the docs are already committed on the branch; if this tree holds uncommitted topic docs while another worktree holds the branch checkout, git refuses to commit to the branch from here — copy the topic folder into that worktree's `docs/plans/`, verify with `ls`, and remove the source copy before handing off. Wait for the user's choice.
|
|
8
|
+
|
|
9
|
+
## Resume
|
|
10
|
+
|
|
11
|
+
Read the progress file's `Feature phase` (match the line — e.g. `grep -m1 '^Feature phase:' <file>`), the Requirements table (the first row whose Done cell is not terminal routes the next requirement — `⬜`, `🔄`, and blank all mean not-done; `✅`/`❌`/`⏭` are resolved and are skipped past), and the Execution summary rows (how prior parts were built); read from the top through the end of `## Execution summary` and stop — the sections after it (deviation records, code digest) carry nothing the resume needs, with one exception: the `ship-paused` route re-presents the ship checkpoint, so it also reads the `## Code digest` written in the progress file. Before routing: if the header reads `Setup: pending` and the design doc has a `## Setup` section, the setup checkpoint was never approved — re-run the setup verification and wait at it:
|
|
12
|
+
- `e2e-written` → write the E2E if not yet present, post the notice, and continue into the implement phase (no stop). A trivial design doc has no `## Feature acceptance` — skip both and go straight into the implement phase.
|
|
13
|
+
- `implementing (k/N)` → continue the next not-yet-terminal requirement.
|
|
14
|
+
- `reviewing` → continue/finish the feature review, then assemble the **ship** checkpoint.
|
|
15
|
+
- `ship-paused` → re-present the ship checkpoint and wait — the execution summary, verdict rows, and code digest re-read from the progress file; if the prior session's reviewer reports are gone, re-run the feature review per the tag first (the checkpoint is never presented without coverage).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Review mechanics and packet helper
|
|
2
|
+
|
|
3
|
+
Read before any feature or opt-in requirement review, before refreshing a packet, and before writing the code digest from review evidence.
|
|
4
|
+
|
|
5
|
+
## Assemble evidence
|
|
6
|
+
|
|
7
|
+
**Assemble the review packet first** — once, by script, so that no packet byte passes through model output (spawn arguments are model output; file reads are not). The Commit column is the packet's source of truth for both spans: the feature packet base is `git rev-parse <first Commit-column entry>^` (the parent of the first recorded commit); a per-requirement packet spans from the previous requirement's last commit (exclusive) to this requirement's last commit (inclusive), with R1's exclusive bound being its own parent. No `merge-base` guessing, no default-branch knowledge. If commits land while the review is in flight, re-run the recipe before spawning any replacement role so the packet matches HEAD:
|
|
8
|
+
|
|
9
|
+
Use the installed skill directory, not a repository checkout of the kit. First verify `python3 --version` reports Python 3.9 or newer and that the helper exists. Missing runtime/resource → report the dependency and stop this step. No runtime auto-install or alternate-language fallback.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
python3 "<executing-skill-dir>/scripts/build-review-packet.py" \
|
|
13
|
+
--project "<repo-root>" \
|
|
14
|
+
--design "docs/plans/<date>-<topic>/<leaf>-design.md" \
|
|
15
|
+
--progress "docs/plans/<date>-<topic>/<leaf>-progress.md" \
|
|
16
|
+
--scope feature
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For an opt-in requirement review, use the same invocation with `--scope requirement --requirement 2` (substitute the selected number). The feature destination is `<design doc's directory>/<leaf>-review-packet.md`; the requirement destination is `<design doc's directory>/<leaf>-review-packet-r<N>.md`. Requirement-suffixed packets never overwrite each other or the feature packet.
|
|
20
|
+
|
|
21
|
+
`--project` resolves from the caller's cwd and must name the Git worktree root. Design/progress arguments are absolute or relative to that project, regardless of the current directory or installed skill location. Both must be matched leaf files inside one active `docs/plans/<topic>/` folder. The helper derives the destination; symlink destinations are refused. Use argument lists in programmatic invocations, never shell interpolation.
|
|
22
|
+
|
|
23
|
+
The packet retains these sections, in order:
|
|
24
|
+
- `## Commits`
|
|
25
|
+
- `## Changed files`
|
|
26
|
+
- `## Acceptance criteria (verbatim from the design doc)`
|
|
27
|
+
- `## Feature acceptance (verbatim)` (empty for a trivial design)
|
|
28
|
+
- `## Production-risk notes (verbatim, if any)`
|
|
29
|
+
- `## Diff`
|
|
30
|
+
|
|
31
|
+
Criteria and production-risk content remain verbatim, including all lines in multiple risk blocks; fenced template headings do not route extraction. Feature criteria span from the first real requirement to Feature acceptance (or document end); requirement criteria include only the selected requirement block. Design-level production-risk areas are retained as review context. The packet header records fixed `Base:` and `Head:` IDs: feature uses the first recorded commit's parent through current HEAD; requirement uses the previous requirement's last recorded commit exclusively through the selected last recorded commit inclusively. The helper excludes generated `*-review-packet*.md` files from diff/stat, including committed packets.
|
|
32
|
+
|
|
33
|
+
A Commit cell is one immutable hexadecimal ID or an ordered whitespace-separated list. Preserve the first ID; append review-fix IDs in order. Feature assembly permits missing commits only for terminal failed/skipped rows. Requirement assembly needs an explicit selected and previous bound; it refuses to guess across a failed/skipped row without a commit. Missing relevant rows, malformed/ambiguous entries, invalid revisions, or non-ancestral order → correct the record from evidence and rerun. The helper never edits progress, commits, switches branches, or marks anything approved. It finishes all parse/Git work before atomically replacing a complete packet. A nonzero exit is failure, not review evidence: report stderr and stop delegation; an older packet remains intact but may be stale.
|
|
34
|
+
|
|
35
|
+
After late review fixes, commit them and append their IDs to the selected row before refreshing a requirement packet; feature packets refresh HEAD automatically. Re-run the helper before any replacement reviewer and before digest writing if stale or missing. Packet generation is evidence assembly, not review approval. Re-green the full suite + E2E after fixes, then complete review and the human checkpoint.
|
|
36
|
+
|
|
37
|
+
## Delegate or review inline
|
|
38
|
+
|
|
39
|
+
- **`parallel`** (the `auto` resolution when risk content is present) — request the host’s `parallel-review` capability for four fresh-context, read-only logical roles: `pwk-spec-reviewer`, `pwk-tracing-reviewer`, `pwk-smell-reviewer`, and `pwk-hazard-reviewer`. Spawn each role with a **one-liner** — a pointer to the packet file with the role framing appended last: the checklist name of the role (`spec alignment`, `code tracing`, `code smells`, or `production hazards`). For example: `Read docs/plans/<date>-<topic>/<leaf>-review-packet.md. Your role: spec alignment.` The packet never appears in spawn arguments. Require independent execution and one collected outcome per role. The reviewer role contracts live in `agents/pwk-*-reviewer.md`; do not duplicate their checklists in the workflow instructions. Reviewers are read-only reporters; you apply smell fixes yourself (full suite + E2E must stay green, commit) and flag trace/spec/hazard findings as follow-ups for the human.
|
|
40
|
+
|
|
41
|
+
- **`inline`** — perform `/skill:pwk-code-review` over the whole diff as a single pass.
|
|
42
|
+
- **Fallback** — if the host has no compatible parallel-review capability, cannot prove the requested read-only/fresh-context/bounded constraints, or delegation fails, perform the missing review work inline. Retain successful delegated reports and do not mark the feature fully reviewed while a required role is missing.
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build review evidence from explicit, immutable Git bounds. Python 3.9+, stdlib only."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PacketError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Document:
|
|
18
|
+
"""Keep original lines for evidence; inspect headings only outside fenced examples."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, path):
|
|
21
|
+
with path.open(encoding='utf-8', newline='') as source:
|
|
22
|
+
self.lines = source.readlines()
|
|
23
|
+
self.headings = []
|
|
24
|
+
self.visible = set()
|
|
25
|
+
fence = None
|
|
26
|
+
for index, line in enumerate(self.lines):
|
|
27
|
+
marker = re.match(r'^ {0,3}(`{3,}|~{3,})(.*)$', line.rstrip('\r\n'))
|
|
28
|
+
if fence:
|
|
29
|
+
if marker and marker[1][0] == fence[0] and len(marker[1]) >= len(fence) and not marker[2].strip():
|
|
30
|
+
fence = None
|
|
31
|
+
continue
|
|
32
|
+
if marker:
|
|
33
|
+
fence = marker[1]
|
|
34
|
+
continue
|
|
35
|
+
self.visible.add(index)
|
|
36
|
+
heading = re.match(r'^ {0,3}(#{1,6})\s+(.+?)\s*#*\s*$', line.rstrip('\r\n'))
|
|
37
|
+
if heading:
|
|
38
|
+
self.headings.append((index, len(heading[1]), heading[2]))
|
|
39
|
+
if fence:
|
|
40
|
+
raise PacketError(f'{path.name}: unclosed code fence')
|
|
41
|
+
|
|
42
|
+
def section(self, title, level=2, required=False):
|
|
43
|
+
matches = [h for h in self.headings if h[1:] == (level, title)]
|
|
44
|
+
if len(matches) > 1 or (required and not matches):
|
|
45
|
+
raise PacketError(f'expected one {title} section; found {len(matches)}')
|
|
46
|
+
if not matches:
|
|
47
|
+
return None
|
|
48
|
+
start = matches[0][0]
|
|
49
|
+
end = next((i for i, depth, _ in self.headings if i > start and depth <= level), len(self.lines))
|
|
50
|
+
return start, end
|
|
51
|
+
|
|
52
|
+
def text(self, start, end):
|
|
53
|
+
return ''.join(self.lines[start:end])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def git(project, *args):
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
['git', '-C', str(project), *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
59
|
+
env={**os.environ, 'GIT_OPTIONAL_LOCKS': '0', 'GIT_TERMINAL_PROMPT': '0'},
|
|
60
|
+
)
|
|
61
|
+
if result.returncode:
|
|
62
|
+
invocation = ' '.join(['git', *args])
|
|
63
|
+
raise PacketError(f'{invocation} failed: {result.stderr.decode("utf-8", errors="replace").strip()}')
|
|
64
|
+
return result.stdout.decode('utf-8', errors='replace')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_input(project, value):
|
|
68
|
+
path = Path(value)
|
|
69
|
+
return (path if path.is_absolute() else project / path).resolve(strict=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def paths(args):
|
|
73
|
+
project = Path(args.project).resolve(strict=True)
|
|
74
|
+
if Path(git(project, 'rev-parse', '--show-toplevel').strip()).resolve() != project:
|
|
75
|
+
raise PacketError('--project must be the Git worktree root')
|
|
76
|
+
for value in (args.design, args.progress):
|
|
77
|
+
supplied = Path(value)
|
|
78
|
+
supplied = Path(os.path.abspath(supplied if supplied.is_absolute() else project / supplied))
|
|
79
|
+
lexical_project = Path(os.path.abspath(args.project))
|
|
80
|
+
try:
|
|
81
|
+
relative_input = supplied.relative_to(lexical_project)
|
|
82
|
+
except ValueError:
|
|
83
|
+
relative_input = supplied.relative_to(project)
|
|
84
|
+
current = project
|
|
85
|
+
for part in relative_input.parts:
|
|
86
|
+
current = current / part
|
|
87
|
+
if current.is_symlink():
|
|
88
|
+
raise PacketError('design/progress paths must not traverse symlinks')
|
|
89
|
+
design = resolve_input(project, args.design)
|
|
90
|
+
progress = resolve_input(project, args.progress)
|
|
91
|
+
relative = design.relative_to(project / 'docs' / 'plans')
|
|
92
|
+
if len(relative.parts) != 2 or relative.parts[0] == 'completed' or not design.name.endswith('-design.md'):
|
|
93
|
+
raise PacketError('design must be docs/plans/<topic>/<leaf>-design.md in an active topic')
|
|
94
|
+
leaf = design.name[:-len('-design.md')]
|
|
95
|
+
if not leaf or progress != design.with_name(f'{leaf}-progress.md'):
|
|
96
|
+
raise PacketError('progress must be the matching <leaf>-progress.md beside the design')
|
|
97
|
+
# Resolving inputs must not route a packet write through symlinked topic directories.
|
|
98
|
+
lexical_topic = project / 'docs' / 'plans' / relative.parts[0]
|
|
99
|
+
for directory in (project / 'docs', project / 'docs' / 'plans', lexical_topic):
|
|
100
|
+
if directory.is_symlink():
|
|
101
|
+
raise PacketError('symlink topic directories are not writable packet destinations')
|
|
102
|
+
suffix = '' if args.scope == 'feature' else f'-r{args.requirement}'
|
|
103
|
+
destination = design.with_name(f'{leaf}-review-packet{suffix}.md')
|
|
104
|
+
validate_destination(destination)
|
|
105
|
+
return project, design, progress, destination
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_destination(path):
|
|
109
|
+
if path.is_symlink() or (path.exists() and not path.is_file()):
|
|
110
|
+
raise PacketError('packet destination must be a regular file, not a symlink or directory')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def requirements(document):
|
|
114
|
+
region = document.section('Requirements', required=True)
|
|
115
|
+
headings = []
|
|
116
|
+
for index, depth, title in document.headings:
|
|
117
|
+
match = re.match(r'^R([1-9][0-9]*):\s*\S', title)
|
|
118
|
+
if depth == 3 and re.match(r'^R[0-9]', title) and not match:
|
|
119
|
+
raise PacketError('malformed requirement heading')
|
|
120
|
+
if depth == 3 and match:
|
|
121
|
+
if not region[0] < index < region[1]:
|
|
122
|
+
raise PacketError('requirement heading outside Requirements section')
|
|
123
|
+
headings.append((int(match[1]), index))
|
|
124
|
+
numbers = [number for number, _ in headings]
|
|
125
|
+
if not numbers or len(set(numbers)) != len(numbers):
|
|
126
|
+
raise PacketError('missing or duplicate requirement headings')
|
|
127
|
+
return headings, region
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def progress_rows(document, project, design):
|
|
131
|
+
first_heading = next((i for i, depth, _ in document.headings if depth == 2), len(document.lines))
|
|
132
|
+
pointers = [document.lines[i].strip()[8:] for i in document.visible
|
|
133
|
+
if i < first_heading and document.lines[i].startswith('Design: ')]
|
|
134
|
+
if len(pointers) != 1 or resolve_input(project, pointers[0]) != design:
|
|
135
|
+
raise PacketError('progress Design header must identify the selected design exactly once')
|
|
136
|
+
start, end = document.section('Requirements', required=True)
|
|
137
|
+
lines = [document.lines[i].strip() for i in range(start + 1, end)
|
|
138
|
+
if i in document.visible and document.lines[i].strip()]
|
|
139
|
+
if len(lines) < 3:
|
|
140
|
+
raise PacketError('missing Requirements table')
|
|
141
|
+
|
|
142
|
+
def cells(line):
|
|
143
|
+
if not line.startswith('|') or not line.endswith('|'):
|
|
144
|
+
raise PacketError('malformed Requirements table row')
|
|
145
|
+
return [cell.strip() for cell in re.split(r'(?<!\\)\|', line)[1:-1]]
|
|
146
|
+
|
|
147
|
+
header = cells(lines[0])
|
|
148
|
+
if header != ['#', 'Done', 'Requirement', 'Per-req ceremony', 'Commit']:
|
|
149
|
+
raise PacketError('Requirements table must have #, Done, Requirement, Per-req ceremony, Commit columns')
|
|
150
|
+
separator = cells(lines[1])
|
|
151
|
+
if len(separator) != 5 or not all(re.fullmatch(r':?-+:?', cell) for cell in separator):
|
|
152
|
+
raise PacketError('malformed Requirements table separator')
|
|
153
|
+
rows = {}
|
|
154
|
+
for line in lines[2:]:
|
|
155
|
+
row = cells(line)
|
|
156
|
+
if len(row) != 5 or not re.fullmatch(r'[1-9][0-9]*', row[0]):
|
|
157
|
+
raise PacketError('malformed Requirements table row')
|
|
158
|
+
number = int(row[0])
|
|
159
|
+
if number in rows:
|
|
160
|
+
raise PacketError(f'duplicate R{number} progress row')
|
|
161
|
+
if row[1] not in ('', '⬜', '🔄', '✅', '❌', '⏭'):
|
|
162
|
+
raise PacketError(f'R{number}: invalid Done cell')
|
|
163
|
+
rows[number] = row
|
|
164
|
+
return rows
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def bounds(project, rows, headings, args):
|
|
168
|
+
numbers = [number for number, _ in headings]
|
|
169
|
+
position = numbers.index(args.requirement) if args.scope == 'requirement' else None
|
|
170
|
+
relevant = numbers if position is None else numbers[max(0, position - 1):position + 1]
|
|
171
|
+
if set(rows) - set(numbers):
|
|
172
|
+
raise PacketError('progress contains requirements absent from the design')
|
|
173
|
+
recorded = {}
|
|
174
|
+
previous = None
|
|
175
|
+
current_head = git(project, 'rev-parse', '--verify', 'HEAD^{commit}').strip()
|
|
176
|
+
for number in relevant:
|
|
177
|
+
if number not in rows:
|
|
178
|
+
raise PacketError(f'R{number}: missing progress row')
|
|
179
|
+
row = rows[number]
|
|
180
|
+
value = row[4]
|
|
181
|
+
if value in ('', '—', '-'):
|
|
182
|
+
if row[1] not in ('❌', '⏭'):
|
|
183
|
+
raise PacketError(f'R{number}: missing Commit entry')
|
|
184
|
+
recorded[number] = []
|
|
185
|
+
continue
|
|
186
|
+
# An ordered whitespace-separated list supports review-fix commits without losing
|
|
187
|
+
# the first feature commit. Other formats are ambiguous, never mined for hashes.
|
|
188
|
+
entries = value.split()
|
|
189
|
+
if not all(re.fullmatch(r'[0-9a-fA-F]{7,64}', entry) for entry in entries):
|
|
190
|
+
raise PacketError(f'R{number}: Commit must contain only ordered hexadecimal commit IDs')
|
|
191
|
+
commits = []
|
|
192
|
+
for entry in entries:
|
|
193
|
+
commit = git(project, 'rev-parse', '--verify', f'{entry}^{{commit}}').strip()
|
|
194
|
+
if previous:
|
|
195
|
+
if previous == commit:
|
|
196
|
+
raise PacketError('duplicate or overlapping recorded commits')
|
|
197
|
+
git(project, 'merge-base', '--is-ancestor', previous, commit)
|
|
198
|
+
git(project, 'merge-base', '--is-ancestor', commit, current_head)
|
|
199
|
+
commits.append(commit)
|
|
200
|
+
previous = commit
|
|
201
|
+
recorded[number] = commits
|
|
202
|
+
if args.scope == 'feature':
|
|
203
|
+
first = next((commits[0] for commits in recorded.values() if commits), None)
|
|
204
|
+
if not first:
|
|
205
|
+
raise PacketError('no recorded commits; cannot infer a feature base')
|
|
206
|
+
return git(project, 'rev-parse', '--verify', f'{first}^').strip(), current_head
|
|
207
|
+
selected = recorded[args.requirement]
|
|
208
|
+
if not selected:
|
|
209
|
+
raise PacketError('selected requirement has no recorded bound')
|
|
210
|
+
if position == 0:
|
|
211
|
+
base = git(project, 'rev-parse', '--verify', f'{selected[0]}^').strip()
|
|
212
|
+
else:
|
|
213
|
+
prior = recorded[numbers[position - 1]]
|
|
214
|
+
if not prior:
|
|
215
|
+
raise PacketError('previous requirement has no recorded bound; cannot guess a range')
|
|
216
|
+
base = prior[-1]
|
|
217
|
+
return base, selected[-1]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def evidence(document, headings, region, args):
|
|
221
|
+
feature = document.section('Feature acceptance')
|
|
222
|
+
if feature and feature[0] < region[1]:
|
|
223
|
+
raise PacketError('Feature acceptance must follow Requirements')
|
|
224
|
+
if args.scope == 'feature':
|
|
225
|
+
start = headings[0][1]
|
|
226
|
+
end = feature[0] if feature else len(document.lines)
|
|
227
|
+
else:
|
|
228
|
+
position = next(i for i, (number, _) in enumerate(headings) if number == args.requirement)
|
|
229
|
+
start = headings[position][1]
|
|
230
|
+
end = headings[position + 1][1] if position + 1 < len(headings) else region[1]
|
|
231
|
+
criteria = document.text(start, end)
|
|
232
|
+
acceptance = ''
|
|
233
|
+
if feature:
|
|
234
|
+
stop = next((i for i, depth, title in document.headings
|
|
235
|
+
if feature[0] < i < feature[1] and depth == 3 and title.startswith('Feature review:')), feature[1])
|
|
236
|
+
acceptance = document.text(feature[0], stop)
|
|
237
|
+
notes = []
|
|
238
|
+
for index, depth, title in document.headings:
|
|
239
|
+
if depth == 3 and title == 'Production-risk notes' and start <= index < end:
|
|
240
|
+
stop = next((i for i, d, t in document.headings if i > index and
|
|
241
|
+
(d <= 2 or (d == 3 and re.match(r'^R[0-9]+:', t)))), len(document.lines))
|
|
242
|
+
notes.append(document.text(index, min(stop, end)))
|
|
243
|
+
risk = document.section('Production-risk areas')
|
|
244
|
+
if risk:
|
|
245
|
+
notes.append(document.text(*risk))
|
|
246
|
+
return criteria, acceptance, ''.join(notes)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build(args):
|
|
250
|
+
project, design_path, progress_path, destination = paths(args)
|
|
251
|
+
design = Document(design_path)
|
|
252
|
+
headings, region = requirements(design)
|
|
253
|
+
if args.scope == 'requirement' and args.requirement not in [n for n, _ in headings]:
|
|
254
|
+
raise PacketError('selected requirement is absent from the design')
|
|
255
|
+
rows = progress_rows(Document(progress_path), project, design_path)
|
|
256
|
+
base, head = bounds(project, rows, headings, args)
|
|
257
|
+
criteria, acceptance, notes = evidence(design, headings, region, args)
|
|
258
|
+
span = f'{base}..{head}'
|
|
259
|
+
pathspec = ['--', '.', ':(glob,exclude)**/*-review-packet*.md']
|
|
260
|
+
sections = [
|
|
261
|
+
('Commits', git(project, 'log', '--oneline', '--no-decorate', span)),
|
|
262
|
+
('Changed files', git(project, 'diff', '--no-ext-diff', '--no-textconv', '--stat', span, *pathspec)),
|
|
263
|
+
('Acceptance criteria (verbatim from the design doc)', criteria),
|
|
264
|
+
('Feature acceptance (verbatim)', acceptance),
|
|
265
|
+
('Production-risk notes (verbatim, if any)', notes),
|
|
266
|
+
('Diff', git(project, 'diff', '--no-ext-diff', '--no-textconv', '--no-color', span, *pathspec)),
|
|
267
|
+
]
|
|
268
|
+
label = 'feature review' if args.scope == 'feature' else f'requirement R{args.requirement} review'
|
|
269
|
+
packet = f'# Review packet: {design_path.parent.name} — {label}\n\nBase: {base}\nHead: {head}\n\n'
|
|
270
|
+
for title, content in sections:
|
|
271
|
+
packet += f'## {title}\n\n{content}\n'
|
|
272
|
+
# All parsing and Git reads finish before a temporary file exists. A failure in
|
|
273
|
+
# assembly leaves an earlier complete packet intact. Replacement is same-directory.
|
|
274
|
+
temporary = None
|
|
275
|
+
try:
|
|
276
|
+
validate_destination(destination)
|
|
277
|
+
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', newline='', dir=destination.parent,
|
|
278
|
+
prefix=f'.{destination.name}.', delete=False) as output:
|
|
279
|
+
temporary = Path(output.name)
|
|
280
|
+
output.write(packet)
|
|
281
|
+
output.flush()
|
|
282
|
+
os.fsync(output.fileno())
|
|
283
|
+
validate_destination(destination)
|
|
284
|
+
os.replace(temporary, destination)
|
|
285
|
+
temporary = None
|
|
286
|
+
finally:
|
|
287
|
+
if temporary is not None:
|
|
288
|
+
temporary.unlink()
|
|
289
|
+
print(f'Wrote {destination}\nRange: {base}..{head}')
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main():
|
|
293
|
+
parser = argparse.ArgumentParser(description=__doc__, epilog=(
|
|
294
|
+
'--project resolves from the caller cwd and must be a Git worktree root. '
|
|
295
|
+
'--design and --progress are absolute or relative to --project, not the skill or cwd. '
|
|
296
|
+
'Output is <leaf>-review-packet[-rN].md beside the matched docs in docs/plans/<topic>/. '
|
|
297
|
+
'Commit cells contain ordered whitespace-separated hexadecimal commit IDs, not revision expressions.'
|
|
298
|
+
))
|
|
299
|
+
parser.add_argument('--project', required=True)
|
|
300
|
+
parser.add_argument('--design', required=True)
|
|
301
|
+
parser.add_argument('--progress', required=True)
|
|
302
|
+
parser.add_argument('--scope', choices=['feature', 'requirement'], required=True)
|
|
303
|
+
parser.add_argument('--requirement', type=int)
|
|
304
|
+
args = parser.parse_args()
|
|
305
|
+
if (args.scope == 'requirement') != (args.requirement is not None):
|
|
306
|
+
parser.error('--requirement N is required only for --scope requirement')
|
|
307
|
+
try:
|
|
308
|
+
if sys.version_info < (3, 9):
|
|
309
|
+
raise PacketError('Python 3.9 or newer is required; install it explicitly before retrying')
|
|
310
|
+
build(args)
|
|
311
|
+
except (PacketError, OSError, ValueError) as error:
|
|
312
|
+
print(f'error: {error}', file=sys.stderr)
|
|
313
|
+
return 1
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == '__main__':
|
|
318
|
+
sys.exit(main())
|