shapeup-sdlc 1.7.0 → 3.0.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.
Files changed (98) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +17 -3
  4. package/README.md +124 -103
  5. package/SECURITY.md +37 -30
  6. package/bin/init.mjs +77 -47
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +7 -6
  9. package/commands/ship.md +29 -16
  10. package/commands/wire.md +1 -1
  11. package/hooks/dispatch-receipt.mjs +195 -0
  12. package/hooks/gate-intake.mjs +3 -3
  13. package/hooks/gate-zerowork.mjs +25 -24
  14. package/hooks/hooks.json +9 -48
  15. package/hooks/lib/decision.mjs +19 -8
  16. package/hooks/safety-spine.mjs +3 -3
  17. package/hooks/sandbox-guard.mjs +84 -40
  18. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
  19. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
  20. package/kernel/harness.mjs +134 -0
  21. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
  22. package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
  23. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  24. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
  25. package/kernel/lib/paths.mjs +491 -0
  26. package/kernel/probe/concurrency.mjs +510 -0
  27. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
  28. package/kernel/probe/eval.mjs +77 -0
  29. package/kernel/probe/leg.mjs +125 -0
  30. package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
  31. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
  32. package/kernel/probe/t0.mjs +66 -0
  33. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
  34. package/kernel/reduce/graph.mjs +437 -0
  35. package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
  36. package/kernel/reduce/ingest.mjs +633 -0
  37. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  38. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
  39. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
  40. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
  41. package/kernel/report/export.mjs +325 -0
  42. package/kernel/report/facts.mjs +347 -0
  43. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
  44. package/kernel/verify/dispatch.mjs +114 -0
  45. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  46. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  47. package/kernel/verify/skills.mjs +125 -0
  48. package/kernel/verify/spec.mjs +559 -0
  49. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
  50. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
  51. package/oracles/http-oracle.mjs +1 -1
  52. package/oracles/process-oracle.mjs +1 -1
  53. package/oracles/snapshot-oracle.mjs +1 -1
  54. package/oracles/test-oracle.mjs +1 -1
  55. package/package.json +11 -9
  56. package/skills/ba-pitch-analyzer/SKILL.md +5 -5
  57. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  58. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  59. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  60. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  61. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  62. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  63. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  64. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  65. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  66. package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
  67. package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
  68. package/skills/qa-edge-hunter/SKILL.md +7 -3
  69. package/skills/scope-architect/SKILL.md +40 -6
  70. package/skills/solution-architect/SKILL.md +19 -4
  71. package/skills/spec-evaluator/SKILL.md +12 -2
  72. package/skills/task-executor/SKILL.md +7 -6
  73. package/skills/tech-lead/SKILL.md +50 -38
  74. package/skills/tech-lead/references/gates.md +69 -20
  75. package/skills/tech-lead/references/protocol.md +832 -0
  76. package/skills/tech-lead/schemas/domain.schema.json +341 -66
  77. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  78. package/skills/tech-lead/schemas/work-result.schema.json +1 -1
  79. package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
  80. package/hooks/anti-rationalization.mjs +0 -238
  81. package/hooks/compact-snapshot.mjs +0 -47
  82. package/hooks/gate-deadline.mjs +0 -147
  83. package/hooks/gate-l2.mjs +0 -161
  84. package/hooks/session-rehydrate.mjs +0 -108
  85. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  86. package/skills/spec-evaluator/README.md +0 -93
  87. package/skills/tech-lead/README.md +0 -71
  88. package/skills/tech-lead/references/delegation.md +0 -261
  89. package/skills/tech-lead/references/hard-rules.md +0 -34
  90. package/skills/tech-lead/references/invocation.md +0 -45
  91. package/skills/tech-lead/references/ledger-schema.md +0 -213
  92. package/skills/tech-lead/references/round-protocol.md +0 -234
  93. package/skills/tech-lead/references/state-model.md +0 -66
  94. package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
  95. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
  96. package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
  97. package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
  98. package/skills/translator/README.md +0 -66
@@ -1,234 +0,0 @@
1
- # Round Protocol
2
-
3
- The orchestration loop in detail. A "round" is one BUILD phase followed by exactly one
4
- EVAL phase. The feature is done when an EVAL round returns PASS.
5
-
6
- > `tasks/_index.md` referenced throughout this file lives in the LOCAL gitignored root
7
- > (`.shapeup/<slug>/tasks/`, v3.2), not the
8
- > SHARED spec dir. See tech-lead SKILL.md GATE L1b for the bootstrap step that regenerates it
9
- > when missing.
10
-
11
- **On a spec with committed scope contracts, this loop is now CODE, not prose** —
12
- `skills/tech-lead/workflows/shapeup-run.js`'s own `while (round <= args.budgets.maxRounds)` loop
13
- implements exactly the shape below (PASS → ship; `max_rounds` exceeded → GATE H `breaker: outer`;
14
- else the next round builds bugs only), with the EVAL-timing rule, the regression rule, and the
15
- three-level breaker all enforced as branches in that script rather than described here for a model
16
- to follow. Read that script's own comments for the mechanics; this file keeps the historical
17
- rationale and the parts of the protocol the script does not cover (ESCALATE adjudication,
18
- discovered-task reconciliation mid-BUILD — see its banner
19
- for the full list) — and, unchanged, the loop below for a `--tiny` run or a spec with no scope
20
- contracts, which `shapeup-run.js` is out of scope for by design.
21
-
22
- ```
23
- round r = 1
24
- loop:
25
- BUILD(r) # see r=1 vs r>1 below
26
- assert board 100% done # GATE L2 — advisory: the hook warns, you decide
27
- verdict, bugs = EVAL(r) # ONE spec-evaluator --feature pass
28
- if verdict == PASS:
29
- SHIP; break
30
- if r >= max_rounds:
31
- ESCALATE(bugs); break # honest stop — no infinite loop
32
- r = r + 1 # next round builds bugs only
33
- ```
34
-
35
- ## BUILD(r) semantics
36
-
37
- | | r = 1 (initial build) | r > 1 (fix build) |
38
- |--|----------------------|-------------------|
39
- | Input | the whole task board | the bug list from EVAL(r-1) |
40
- | Scope | every ready task, dependency/layer order, until board all ✅ | only the tasks/areas named by bugs |
41
- | Command | compile-order `--next` → task-executor `--order` → ingest, looped | compile-order `--task <id> --operation fix` → dispatch → ingest, per bug |
42
- | Passing areas | n/a | never touched |
43
- | SPIKEs | resolved first (they block) | only if a bug is a SPIKE finding |
44
-
45
- Re-opening tasks in r>1: the fix order's WorkResult reports the task `partial` while failing
46
- and `done` when re-verified; ingest-result flips the board accordingly. The board reflects the
47
- churn so the next EVAL sees a green board again.
48
-
49
- Discovered Tasks:
50
- If WorkResults carry `discoveries[]` during BUILD, ingest-result appends them to the discovery
51
- ledger (`.shapeup/<slug>/discovery/ledger.md`) and the build loop pauses after the current
52
- tasks are done. Compile + dispatch a reconcile order (ba-pitch-analyzer, operation: reconcile).
53
- This reconciles them into new tasks and invariants and updates the board; the tech lead bumps
54
- `discovered_rounds` in harness-run.md, then routes back to GATE L1b (Board Review) for PO
55
- approval of the new tasks and estimates before resuming the BUILD loop.
56
-
57
- ## The EVAL timing rule (the core constraint)
58
-
59
- EVAL fires **once** per round and **only** when GATE L2 has confirmed the board is 100% done.
60
- It is never:
61
- - called per task,
62
- - called inside the BUILD loop,
63
- - called on a partial board.
64
-
65
- ## Regression rule (r > 1) — QA-meeting Bước 1c
66
-
67
- A fix round changes code; a fix can break what passed. Therefore EVAL(r>1) scope is **not**
68
- just the fixed bugs:
69
-
70
- ```
71
- EVAL(r) for r > 1:
72
- touched_UCs = every UC referenced (use_case_refs) by a task re-opened in BUILD(r)
73
- scope = fixed bugs' criteria
74
- + FULL re-run of `## Test Surface` rows for every touched UC
75
- (test-surface-conformance dimension, when active)
76
- + completeness re-check (cheap, static)
77
- untouched UCs' surfaces: NOT re-run (their code didn't change; re-probing everything
78
- every round would turn cheap end-of-round QA into a full-suite tax)
79
- ```
80
-
81
- Pre-v2.9 specs (no Test Surface anywhere): the rule degrades to bug-criteria-only, as
82
- before — and the verdict report notes `regression coverage: none (no test surface)`.
83
- Honest reporting over silent coverage claims.
84
-
85
- ## QA edge hunt (post-PASS, pre-ship)
86
-
87
- When EVAL(r) returns PASS for the **first** time in a run, the orchestrator delegates one
88
- `/qa-edge-hunter` pass before SHIP (skippable via `--no-qa`, same spirit as `--no-eval`).
89
- QA is a pure worker: no verdict, no score, no gate — it writes `~` findings to
90
- `discovery/ledger.md` and a `qa/hunt-report.md`. Triage happens at SHIP/GATE L4:
91
- - all findings stay `~` → SHIP; findings carry over as raw ideas (debt-free).
92
- - PO/TL promote any to must-have → a fix round r+1 (those items only) → EVAL
93
- never a second full hunt) → back to L4.
94
- - Circuit breaker applies: out of rounds/appetite → ship with `~` findings recorded.
95
- QA never runs on a FAIL round — a build that hasn't passed conformance isn't worth
96
- edge-hunting yet.
97
-
98
- Rationale (from the long-running harness work): a single end-of-round QA pass over the
99
- running feature is cheap relative to the build (minutes vs hours) and catches the
100
- last-mile defects, whereas grading every task multiplies evaluator cost for little gain
101
- once the generator is competent. If the build round didn't finish, there is nothing
102
- coherent to evaluate yet.
103
-
104
- ## Stop conditions
105
- 1. **PASS** — EVAL(r) verdict is PASS → SHIP.
106
- 2. **max_rounds (OUTER breaker)** — r would exceed `--max-rounds` (default 3) without PASS →
107
- ESCALATE: print the residual bug list, the rounds used, and hand the decision to the PO
108
- (scope contracts present: also `/scope-hammer --breaker outer`). Do not start another
109
- build round automatically.
110
- 3. **attempt_budget (INNER breaker, scope contracts only)** — a single scope's T0 attempt
111
- loop exhausts `--attempts` (default 5) without reaching a trial that is both `kept` and
112
- T0-green, **or** `no_progress_k` consecutive trials come back non-`kept` (the stagnation
113
- term, default 2 — `compile-order.mjs` prints it as a JSON breaker object on stderr) →
114
- does NOT stop the round; queues a hammer PROPOSAL for GATE H and moves to the next scope
115
- in sequence. See "Three-level circuit breaker" below.
116
- 4. **wall_clock_budget (DEADLINE breaker, opt-in)** — elapsed seconds since the run receipt
117
- exceed `--wall-clock-budget` → do NOT start another build round or another scope; go
118
- straight to GATE H (`/scope-hammer --breaker deadline`). Checked with
119
- `scripts/budget-check.mjs` at every round boundary and enforced by `hooks/gate-deadline.mjs`,
120
- which denies a `task-executor` dispatch past the deadline while leaving `spec-evaluator`,
121
- `scope-hammer` and `qa-edge-hunter` reachable — a run past its deadline
122
- must still be able to judge, hammer, and close. Off unless configured.
123
- 5. **Hard error** — a sub-skill fails irrecoverably (e.g. spec folder gone, app won't
124
- build at all) → stop and report; do not retry blindly.
125
- 6. **User halt** — at any L-gate the user can stop the run; the ledger preserves state for
126
- `--from` resume.
127
-
128
- ## Three-level circuit breaker
129
-
130
- ```
131
- OUTER round_budget (max_rounds) — the six-week-timebox analog. Decremented once per
132
- round at GATE L2, regardless of how many scopes it
133
- covered. Hitting 0 → GATE H immediately (§ above).
134
- INNER attempt_budget (per scope) — decremented once per T0 attempt inside BUILD round r.
135
- Hitting its cap WITHOUT a T0-green result trips the
136
- inner breaker for that scope only: the scope is queued
137
- as a hammer PROPOSAL (not a hard stop) and the round
138
- moves on to the next scope in the L1b sequence.
139
- no_progress_k (default 2) is COMPOSED INTO this same
140
- breaker rather than added beside it as a fourth budget:
141
- attempt_budget counts attempts and cannot see that the
142
- last two produced nothing, so k consecutive non-`kept`
143
- trials queue the same GATE H proposal early.
144
- DEADLINE wall_clock_budget_s — elapsed seconds since the run receipt. Opt-in; off
145
- unless set at L0. Tripping routes to GATE H with
146
- --breaker deadline. Enforced by hooks/gate-deadline.mjs.
147
- ```
148
-
149
- **Why the third one exists — and it corrects an earlier diagnosis.** A run killed at an external
150
- time cap looks like a stall from outside: no verdict, nothing to show. The natural reading is that
151
- it hung at a gate. Often the transcript says otherwise — steady turns, steady writes, gate markers
152
- advancing, zero stall signals. The run was working when the clock ran out.
153
-
154
- Both existing breakers count *events*, not time: `round_budget` moves once per round,
155
- `attempt_budget` once per T0 attempt. Neither can observe that round 1 has been running for
156
- twenty-nine minutes, so a run can burn its entire wall clock with both breakers untouched. The
157
- cost is not the missing verdict — it is that a run killed from *outside* ships nothing, not even the
158
- scopes that were already green. A breaker that trips from the inside routes to GATE H, where
159
- scope-hammer compares the shippable subset against the baseline and ships what works. Same clock,
160
- different ending.
161
- Nesting rationale: a struggling scope should not freeze every other scope's progress
162
- in the same round — only running out of *rounds* (the real six-week analog) stops the whole
163
- run. A scope that trips its inner breaker still gets judged fairly at GATE H: scope-hammer
164
- compares "ship without this scope" against the baseline, same as any other cut candidate — it
165
- is never silently dropped, and it is never allowed to block scopes that ARE working.
166
-
167
- ## Isolated attempt loop — one T0 attempt, in detail (scope contracts only)
168
-
169
- **This is `shapeup-run.js`'s inner per-scope loop, as code**, with one addition the prose below
170
- never had: before opening a scope's attempt loop, the script checks whether THIS round already
171
- has a green T0 verdict for that scope on disk, and skips it when it does — the resumability a
172
- mid-BUILD kill needs, that a session narrating this loop from memory could never guarantee.
173
-
174
- Whichever runs it, the loop is a **ratchet**: every attempt is scored against the last kept one,
175
- and the working tree moves forward only when the score strictly improves. `t0-verify.mjs` makes
176
- that decision and acts on the tree itself, inside the runtime — its caller reads the decision, it
177
- never makes it.
178
-
179
- ```
180
- compile-order --scope … --round N --attempt M
181
- → zero-memory WorkOrder (scope contract + this scope's
182
- tasks + digested errors + ledger decisions +
183
- trial_history: the last 8 trials for this scope, each
184
- with score, status, delta and top-3 digest, CROSSING the
185
- round boundary — compiled facts, no chat history by
186
- construction)
187
- dispatch task-executor --order … → code within substrate; WorkResult in results/
188
- ingest-result <result> → board/ledger writes
189
- (a worker cannot escalate) → WorkResult carries no escalates field. A phase that
190
- cannot finish leaves no artifact, the post-condition
191
- fails, and the run ABORTS naming the phase. Resolve it
192
- yourself and record the answer in round-ledger.md, which
193
- the NEXT attempt's fresh context reads back.
194
- t0-verify.mjs → fixtures + DB probe + (on green) seesaw, then scores the
195
- attempt against the baseline trial and snapshots or
196
- restores the tree. Branch on `status` from its stdout
197
- JSON — the tree action has ALREADY happened:
198
- kept strictly better, INCLUDING red-but-improved (2/5 → 4/5 fixtures — the whole point
199
- of the ratchet). Tree snapshotted to refs/shapeup/<scope_id>/kept.
200
- overall=green → the attempt loop breaks; scope reaches DOWNHILL_EXECUTION.
201
- Still red → loop, and attempt M+1 now builds ON attempt M.
202
- reverted not better — and a tie is not better. Tree already restored from the last kept
203
- snapshot. Subsumes the retired stash-and-retry branch: a FINISHED scope's broken
204
- fixture raises score.regressions and reverts through this same rule, which
205
- is why seesaw runs before anything is declared green.
206
- rather than the code. Tree kept, baseline reset. Not a verdict, not a failure.
207
- crash a fixture command failed to spawn or timed out; tree restored. Fix the fixture,
208
- not the code.
209
- (on any red, `discovered_tasks` carries the AEGIS {file, line, core_message} triples, which
210
- compile-order folds into the NEXT attempt's order as digested_errors — no separate dispatch)
211
- ```
212
-
213
- **The exit code is not the branch selector.** `t0-verify.mjs` exits 0 on T0-green and 1 on
214
- T0-red (2 on bad argv), mirroring the `oracles/*` convention — so a `kept` red-but-improved
215
- attempt, the exact case the ratchet exists for, exits 1. Branch on `status` from the stdout
216
- JSON; never on `$?`, and never wire this call into a `set -e` / `&&` chain that would read a
217
- non-zero exit as "stop".
218
-
219
- This replaces the old flat per-task loop for any scope that has a contract;
220
- scopes/specs without one keep the v0.2.6 behavior verbatim (see BUILD(r) table above).
221
-
222
- ## --no-eval (skip evaluation)
223
- A tech-lead judgment, surfaced at GATE L2: if the feature is clearly within what the model
224
- builds reliably solo, the evaluator is optional overhead. With `--no-eval`, after GATE L2
225
- the run goes straight to SHIP with verdict `not-evaluated` recorded in the ledger and a
226
- clear note that nothing was verified beyond task-executor's own per-AC evidence checks.
227
-
228
- ## Round-cost intuition
229
- Build dominates; eval is cheap. Expect each EVAL round to cost a small fraction of a BUILD
230
- round. This is why running eval once per round (not per task) is the right trade: you pay a
231
- little QA at the end of each build and keep the expensive build coherent in between.
232
-
233
- <!-- test requirement -->
234
- kept|reverted|rebased|crash, decided in t0-verify.mjs decideStatus()
@@ -1,66 +0,0 @@
1
- # State Model — who writes what
2
-
3
- Reference for the orchestrator. Moved out of `SKILL.md` so the front door stays a runbook: the
4
- first screen a model reads decides whether it acts or describes, and every line of architecture
5
- prose ahead of the first tool call is a line it can summarise instead of execute.
6
-
7
- ---
8
-
9
- ## State ownership (D6, mechanically closed in v1.0)
10
-
11
- Workers are stateless; the orchestrator layer is the **sole writer of ALL run-state**. Every
12
- worker receives a structured **WorkOrder** envelope (`.shapeup/<slug>/orders/`, compiled by
13
- `compile-order.mjs`) and returns a **WorkResult** envelope (`results/`); the deterministic
14
- `ingest-result.mjs` performs every shared-state write — board status, AC ticks, unblock
15
- propagation, discovery-ledger appends, verdict bookkeeping.
16
-
17
- No worker writes `run-state.md`, `tasks/_index.md`, or the ledger. Everything a worker used to
18
- write into shared files, it now returns as data.
19
-
20
- The tech lead owns `harness-run.md` — rounds, gate decisions, Hill positions, verdicts,
21
- `discovered_rounds`, config, language record. The board (`tasks/_index.md`, LOCAL root — v3.2) is
22
- **execution truth**, maintained exclusively through ingest.
23
-
24
- **The run receipt (v1.4).** `scripts/init-run.mjs` opens the run and writes
25
- `.shapeup/<slug>/receipt.json` plus `.shapeup/active-scope` before any gate. The
26
- receipt is the mechanical fact that a run *started* — distinct from every other artifact here,
27
- which records what a run *did*. That distinction is load-bearing: the guards that check a run's
28
- progress (`anti-rationalization.mjs`) are all scoped to an active run, so before the receipt
29
- existed, a run that never started was invisible to every one of them. `hooks/gate-zerowork.mjs`
30
- reads only the receipt's presence, which is why it can see a total failure that leaves no other
31
- trace. See `references/gates.md` — GATE L0.1.
32
-
33
- ## Central domain registry
34
-
35
- Every record type and payload field that crosses a skill boundary is defined exactly once in
36
- `skills/tech-lead/schemas/domain.schema.json` — the envelope schemas (`work-order.schema.json`,
37
- `work-result.schema.json`) only `$ref` it. The registry annotates each entity's tier
38
- (SHARED/LOCAL), location, sole writer, and readers, carries the machine-readable ERD (`x-erd`),
39
- and maps which payload fields each worker may rely on (`x-payload-by-worker`).
40
-
41
- A new cross-boundary field is added THERE first (structural test #24 enforces the map's
42
- consistency); a skill inventing its own undeclared field is a defect — the orchestrator, not the
43
- worker, owns the vocabulary.
44
-
45
- ## Two ledgers, split by promotion timing
46
-
47
- (Addendum §F.3 — only when scope contracts exist.)
48
-
49
- `harness-run.md` stays the LOCAL (`.shapeup/<slug>/`, gitignored) full run trace: it can be
50
- rebuilt or lost without consequence.
51
-
52
- A second, committed `round-ledger.md` (`shapeup/<slug>/round-ledger.md`, SHARED,
53
- Tier A) holds only what must survive a crash or a `.shapeup/` wipe:
54
-
55
- - the resolved model/budget matrix (L0.8/L0.9),
56
- - the **Decisions** table — every gate crossing and every PO answer to a blocked phase,
57
- promoted the instant it is given, never batched to round close.
58
-
59
- Gate crossings resolved from a **gate answer set** (`scripts/gate-answers.mjs`) are written here
60
- with their source — `preset:ci`, `file:.shapeup/gate-answers.json` — and the set's
61
- `authorized_by`. A headless run that ships must always be able to name the human behind its
62
- sign-off; that name lives here and nowhere else.
63
-
64
- The tech lead is the sole writer of both. `round-ledger.md` is simply the subset that must never
65
- live only in a session or a gitignored file. No scope contracts → `round-ledger.md` is not
66
- written, and `harness-run.md`'s "Decisions log" is the only ledger, exactly as in v0.2.6.
@@ -1,258 +0,0 @@
1
- #!/usr/bin/env node
2
- // WorkResult ingester (pure-skill architecture v1.0, plan P1).
3
- //
4
- // The other half of the orchestrator's pipeline sub-layer — and the mechanism that finally
5
- // closes D6: workers no longer write shared state; they RETURN data (a WorkResult envelope)
6
- // and this script performs every shared-state write, deterministically, in one place:
7
- //
8
- // task_results[] → tick AC boxes, flip task frontmatter status, append Execution Log,
9
- // update tasks/_index.md row, propagate unblocks (old P3.1–P3.6)
10
- // discoveries[] → append to .shapeup/<slug>/discovery/ledger.md (old P3.7 / QA H.3)
11
- // verdict.criteria[] → append evaluation/.verdicts-<target>.jsonl (old evaluator B.0)
12
- // verdict.refuted[] → un-tick refuted AC boxes + set eval_verdict frontmatter (old B.2/B.2b)
13
- //
14
- // Zero dependencies, zero network, schema-validated input (a malformed result never mutates
15
- // the board). Single-writer becomes mechanically true, not aspirational.
16
- //
17
- // Usage: node skills/tech-lead/scripts/ingest-result.mjs <result.json> [--cwd <dir>]
18
- // Exit: 0 = ingested, 1 = result rejected (schema) or a write failed.
19
-
20
- import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
21
- import { resolve, join, dirname } from "node:path";
22
- import { fileURLToPath } from "node:url";
23
- import { validate } from "./validate-envelope.mjs";
24
- import { isMain } from "./lib/is-main.mjs";
25
- import { runArgs } from "./lib/argv.mjs";
26
- import { tasksDir, localRoot } from "./lib/paths.mjs";
27
-
28
- const HERE = dirname(fileURLToPath(import.meta.url));
29
- const RESULT_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "../schemas/work-result.schema.json"), "utf8"));
30
-
31
- /**
32
- * @returns {string} Today's date as an ISO `YYYY-MM-DD` string (UTC), for log/frontmatter stamps.
33
- */
34
- const today = () => new Date().toISOString().slice(0, 10);
35
-
36
- /**
37
- * Locate a task file on the LOCAL board by id.
38
- * @param {string} cwd - Working-directory root.
39
- * @param {string} slug - Feature slug.
40
- * @param {string} taskId - Task id prefix to match (e.g. "TASK-001").
41
- * @returns {string|null} Absolute path of the first `<taskId>*.md` file, or null when the tasks
42
- * directory or a matching file does not exist.
43
- */
44
- export function findTaskFile(cwd, slug, taskId) {
45
- const dir = tasksDir(cwd, slug);
46
- if (!existsSync(dir)) return null;
47
- const f = readdirSync(dir).find((n) => n.startsWith(taskId) && n.endsWith(".md"));
48
- return f ? join(dir, f) : null;
49
- }
50
-
51
- /**
52
- * Set a scalar frontmatter field in a task-file body, adding it when absent.
53
- * @param {string} body - Full task-file text.
54
- * @param {string} key - Frontmatter key to set.
55
- * @param {(string|number)} value - Value to write (stringified inline).
56
- * @returns {string} The body with `key: value` set; returned unchanged when the body has no
57
- * frontmatter block.
58
- */
59
- export function setFrontmatter(body, key, value) {
60
- const m = body.match(/^---\r?\n([\s\S]*?)\r?\n---/);
61
- if (!m) return body;
62
- const re = new RegExp(`^${key}:.*$`, "m");
63
- const fm = re.test(m[1]) ? m[1].replace(re, `${key}: ${value}`) : `${m[1]}\n${key}: ${value}`;
64
- return body.replace(m[1], fm);
65
- }
66
-
67
- /**
68
- * Tick or un-tick the first acceptance-criterion checkbox whose text matches `ac`.
69
- * @param {string} body - Full task-file text.
70
- * @param {string} ac - Criterion text to match (case/space-insensitive substring, either direction).
71
- * @param {boolean} checked - true to write `[x]`, false to write `[ ]`.
72
- * @returns {{body:string, hit:boolean}} The updated body and whether a checkbox matched (only the
73
- * first match is changed).
74
- */
75
- export function setCheckbox(body, ac, checked) {
76
- const needle = ac.toLowerCase().replace(/\s+/g, " ").trim();
77
- const lines = body.split(/\r?\n/);
78
- let hit = false;
79
- const out = lines.map((line) => {
80
- const m = line.match(/^(\s*- \[)([ x])(\]\s+)(.*)$/);
81
- if (!m || hit) return line;
82
- const text = m[4].toLowerCase().replace(/\s+/g, " ").trim();
83
- if (text.includes(needle) || needle.includes(text)) {
84
- hit = true;
85
- return `${m[1]}${checked ? "x" : " "}${m[3]}${m[4]}`;
86
- }
87
- return line;
88
- });
89
- return { body: out.join("\n"), hit };
90
- }
91
-
92
- /**
93
- * Flip a task's row in `tasks/_index.md` to a done state.
94
- * @param {string} indexBody - Full board-index text.
95
- * @param {string} taskId - Task id whose row to update.
96
- * @param {boolean} done - When true, rewrite the row's status emoji/word to done; false is a no-op.
97
- * @returns {string} The board text with the matching row updated (unchanged when no row matches).
98
- */
99
- export function updateBoardRow(indexBody, taskId, done) {
100
- return indexBody.split(/\r?\n/).map((line) => {
101
- if (!line.includes(taskId) || !line.includes("|")) return line;
102
- if (done) return line.replace(/⬜|🔄|⏳|🚫/g, "✅").replace(/\b(ready|in-progress|blocked)\b/gi, "done");
103
- return line;
104
- }).join("\n");
105
- }
106
-
107
- /**
108
- * Apply one validated WorkResult to the working tree — the single-writer step (D6): ticks AC
109
- * boxes, flips task status, appends the Execution Log, propagates unblocks, appends discoveries,
110
- * writes the verdict ledger + un-ticks refuted boxes.
111
- * verdict{criteria[],refuted[]}).
112
- * @param {{cwd:string}} opts - cwd: working-directory root every LOCAL path resolves against.
113
- * @returns {{slug:string, tasks_updated:string[], acs_ticked:number, unblocked:string[],
114
- * discoveries_appended:number, refuted_unticked:number, verdict_lines:number}} A summary of every write performed.
115
- * @throws {Error} If a task/board/ledger file it must write is not writable (fs error propagates).
116
- * `evaluation/.verdicts-*.jsonl` under `.shapeup/<slug>/`.
117
- */
118
- export function applyResult(result, { cwd }) {
119
- const slug = result.order_id.split("/")[0];
120
- const local = localRoot(cwd, slug);
121
- const summary = { slug, tasks_updated: [], acs_ticked: 0, unblocked: [], discoveries_appended: 0, refuted_unticked: 0, verdict_lines: 0 };
122
-
123
- // 1. Task results → task files + board (old task-executor P3.1/P3.2/P3.6).
124
- const boardIndex = join(local, "tasks", "_index.md");
125
- for (const tr of result.task_results || []) {
126
- const path = findTaskFile(cwd, slug, tr.task_id);
127
- if (!path) continue;
128
- let body = readFileSync(path, "utf8");
129
- for (const acr of tr.ac_results || []) {
130
- if (acr.result === "pass") {
131
- const r = setCheckbox(body, acr.ac, true);
132
- body = r.body;
133
- if (r.hit) summary.acs_ticked++;
134
- }
135
- }
136
- if (tr.status === "done") {
137
- body = setFrontmatter(body, "status", "done");
138
- body = setFrontmatter(body, "completed_at", today());
139
- } else if (tr.status === "partial" || tr.status === "failed") {
140
- body = setFrontmatter(body, "status", "in-progress");
141
- }
142
- // Execution Log (append; the checkbox list must never disagree with it).
143
- const logLines = (tr.ac_results || []).map((a) => `- ${a.ac}: ${a.result}${a.evidence ? ` (${a.evidence})` : ""}`).join("\n");
144
- body += `\n\n## Execution Log — ${today()} (${result.order_id})\n- executor: ${result.worker || "task-executor"} via ingest-result\n- status: ${tr.status}\n${logLines}${tr.notes ? `\n- notes: ${tr.notes}` : ""}\n`;
145
- writeFileSync(path, body);
146
- summary.tasks_updated.push(tr.task_id);
147
- if (tr.status === "done" && existsSync(boardIndex)) {
148
- writeFileSync(boardIndex, updateBoardRow(readFileSync(boardIndex, "utf8"), tr.task_id, true));
149
- }
150
- }
151
-
152
- // 2. Unblock propagation (old P3.4): any blocked task whose dependencies are all done → ready.
153
- const tasksDir = join(local, "tasks");
154
- if (existsSync(tasksDir)) {
155
- const files = readdirSync(tasksDir).filter((f) => /^TASK-[\w.-]+\.md$/i.test(f));
156
- const statusOf = {};
157
- const parsed = files.map((f) => {
158
- const body = readFileSync(join(tasksDir, f), "utf8");
159
- const id = (body.match(/^id:\s*(TASK-[\w.-]+)/im) || [])[1] || f.replace(/\.md$/, "");
160
- const status = (body.match(/^status:\s*(\S+)/im) || [])[1] || "unknown";
161
- const deps = (body.match(/^depends_on:\s*\[([^\]]*)\]/im) || [, ""])[1]
162
- .split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
163
- statusOf[id] = status;
164
- return { f, id, status, deps, body };
165
- });
166
- for (const t of parsed) {
167
- if (t.status === "blocked" && t.deps.length && t.deps.every((d) => statusOf[d] === "done")) {
168
- writeFileSync(join(tasksDir, t.f), setFrontmatter(t.body, "status", "ready"));
169
- summary.unblocked.push(t.id);
170
- if (existsSync(boardIndex)) {
171
- const idx = readFileSync(boardIndex, "utf8").split(/\r?\n/).map((line) =>
172
- line.includes(t.id) && line.includes("|")
173
- ? line.replace(/🚫|⏳/g, "⬜").replace(/\bblocked\b/gi, "ready")
174
- : line).join("\n");
175
- writeFileSync(boardIndex, idx);
176
- }
177
- }
178
- }
179
- }
180
-
181
- // 3. Discoveries → the ledger (old P3.7 / QA H.3). Single writer: this script.
182
- if (result.discoveries?.length) {
183
- const ledgerDir = join(local, "discovery");
184
- mkdirSync(ledgerDir, { recursive: true });
185
- const ledger = join(ledgerDir, "ledger.md");
186
- if (!existsSync(ledger)) writeFileSync(ledger, `---\nfeature: ${slug}\n---\n# Discovery Ledger — ${slug}\n`);
187
- const lines = result.discoveries.map((d) => {
188
- const tags = [d.lens ? `[lens:${d.lens}]` : "", d.severity_hint ? `severity-hint: ${d.severity_hint}` : "", d.test_gap ? `test-gap: ${d.test_gap}` : "", d.contradicts ? `contradicts: ${d.contradicts}` : "", d.traces_to?.length ? `traces_to: ${d.traces_to.join(", ")}` : ""].filter(Boolean);
189
- return `${d.marker} ${d.lens ? tags[0] + " " : ""}${d.line}${d.repro ? `\n repro: ${d.repro}` : ""}${tags.slice(d.lens ? 1 : 0).map((t) => `\n ${t}`).join("")}`;
190
- }).join("\n");
191
- appendFileSync(ledger, `\n## Discovered — ${result.order_id} (${today()})\n${lines}\n`);
192
- summary.discoveries_appended = result.discoveries.length;
193
- }
194
-
195
- // 4. Verdict bookkeeping (old evaluator B.0/B.2/B.2b) — judge returns data, ingest writes.
196
- if (result.verdict) {
197
- const evalDir = join(local, "evaluation");
198
- mkdirSync(evalDir, { recursive: true });
199
- if (result.verdict.criteria?.length) {
200
- const target = result.order_id.split("/")[1] || "run";
201
- const ledger = join(evalDir, `.verdicts-${target}.jsonl`);
202
- let run = 1;
203
- if (existsSync(ledger)) {
204
- const prior = readFileSync(ledger, "utf8").trim().split(/\n/).filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
205
- run = prior.reduce((mx, r) => Math.max(mx, r.run || 0), 0) + 1;
206
- }
207
- const lines = result.verdict.criteria.map((c) => JSON.stringify({
208
- run, dimension: c.dimension || "spec-conformance", criterion: c.criterion,
209
- verdict: c.verdict, confidence: c.confidence, reprobed: !!c.reprobed,
210
- evidence: c.evidence || "", at: new Date().toISOString(),
211
- })).join("\n");
212
- appendFileSync(ledger, lines + "\n");
213
- summary.verdict_lines = result.verdict.criteria.length;
214
- }
215
- for (const ref of result.verdict.refuted || []) {
216
- const path = findTaskFile(cwd, slug, ref.task_id);
217
- if (!path) continue;
218
- let body = readFileSync(path, "utf8");
219
- const r = setCheckbox(body, ref.ac, false);
220
- if (r.hit) summary.refuted_unticked++;
221
- body = setFrontmatter(r.body, "eval_verdict", "fail");
222
- body = setFrontmatter(body, "eval_at", today());
223
- writeFileSync(path, body);
224
- }
225
- }
226
-
227
-
228
-
229
- return summary;
230
- }
231
-
232
- // ---------------------------------------------------------------------------
233
- /** The typed argv contract (see `./lib/argv.mjs`). */
234
- export const ARGV_SPEC = {
235
- usage: "ingest-result.mjs <result.json> [--cwd <dir>]",
236
- _: { arity: 1, max: 1, name: "result.json" },
237
- cwd: { type: "path" },
238
- };
239
-
240
- const isMainModule = isMain(import.meta.url);
241
- if (isMainModule) {
242
- const args = runArgs(ARGV_SPEC);
243
- const file = args._[0];
244
- const cwd = resolve(args.cwd || process.cwd());
245
-
246
- let result;
247
- try { result = JSON.parse(readFileSync(resolve(file), "utf8")); }
248
- catch (e) { console.error(` ✗ result unreadable: ${e.message}`); process.exit(1); }
249
-
250
- const { valid, errors } = validate(result, RESULT_SCHEMA);
251
- if (!valid) {
252
- console.error("ingest-result: result rejected — a malformed result never mutates the board:");
253
- for (const e of errors) console.error(` ✗ ${e}`);
254
- process.exit(1);
255
- }
256
- const s = applyResult(result, { cwd });
257
- console.log(`✅ ingested ${result.order_id} — tasks: [${s.tasks_updated.join(", ")}] · ACs ticked: ${s.acs_ticked} · unblocked: [${s.unblocked.join(", ")}] · discoveries: ${s.discoveries_appended} · verdict lines: ${s.verdict_lines} · refuted un-ticked: ${s.refuted_unticked}`);
258
- }
@@ -1,81 +0,0 @@
1
- // isMain — "was this module executed directly, or imported?"
2
- //
3
- // WHY THIS FILE EXISTS (observed, not theorized).
4
- //
5
- // Eighteen of this plugin's scripts and hooks decided whether to do anything at all with:
6
- //
7
- // if (import.meta.url === `file://${process.argv[1]}`) main();
8
- //
9
- // That comparison is FALSE — and the whole body silently does nothing, exit 0, no output —
10
- // whenever the path the process was invoked with is not byte-identical to the resolved module
11
- // URL. Two extremely common cases make it false:
12
- //
13
- // 1. A SYMLINKED DIRECTORY ANYWHERE IN THE PATH. Node resolves `import.meta.url` through
14
- // symlinks; `process.argv[1]` is the string as typed. On macOS `/var` is a symlink to
15
- // `/private/var`, so EVERY path under the system temp directory mismatches — which is where
16
- // an npm-packed install lands (`/var/folders/…/package`). nvm, pnpm's content store,
17
- // Homebrew and any symlinked checkout do the same thing on every platform.
18
- //
19
- // 2. A SPACE OR OTHER URL-RESERVED CHARACTER IN THE PATH. `import.meta.url` is percent-encoded
20
- // (`My%20Plugins`); the template literal is not (`My Plugins`). So a plugin installed under
21
- // `~/Library/Application Support/…` or any directory with a space in its name mismatches too.
22
- //
23
- // WHAT THAT COSTS. `init-run.mjs` is GATE L0.1 — the orchestrator's mandatory first tool call, the
24
- // script that writes the run receipt everything else is derived from. Under a `/var` path it
25
- // exited 0 with empty stdout and wrote no receipt. The orchestrator then cannot distinguish "the
26
- // run opened" from "nothing happened", and burns its budget on forensics against its own
27
- // bootstrap — retrying the script several ways, hitting permission refusals trying to capture an
28
- // exit code, and finally searching the filesystem for its own skill — while the artifact it needs
29
- // sits on disk the entire time.
30
- //
31
- // The same guard sits in seven hooks, including `gate-zerowork`, `safety-spine` and
32
- // `sandbox-guard`. This project's stated organising idea is that "every invariant that matters
33
- // lives in the runtime, not in a prompt" — and under a symlinked install the runtime half was
34
- // inert, while every gate still reported success. A silent no-op is the single worst failure mode
35
- // an enforcement layer can have, because it is indistinguishable from working.
36
- //
37
- // THE FIX. Compare resolved URL to resolved URL, using `pathToFileURL` so encoding matches and
38
- // `realpathSync` so symlinks match. The structural suite asserts that no file
39
- // reintroduces the fragile form, and actually executes each entry point through a symlinked
40
- // directory AND through a directory with a space in its name to prove the guard holds — because
41
- // this defect was invisible to every test that invoked scripts by their real path.
42
-
43
- import { realpathSync } from "node:fs";
44
- import { pathToFileURL } from "node:url";
45
-
46
- /**
47
- * True when `moduleUrl` belongs to the module Node was asked to execute.
48
- *
49
- * Usage, at the bottom of an entry point:
50
- *
51
- * import { isMain } from "./lib/is-main.mjs";
52
- * if (isMain(import.meta.url)) main();
53
- *
54
- * @param {string} moduleUrl - The caller's `import.meta.url`.
55
- * @returns {boolean} true if executed directly, false if imported (or if there is no entry point,
56
- * e.g. `node --eval`, where nothing should auto-run).
57
- */
58
- export function isMain(moduleUrl) {
59
- const entry = process.argv[1];
60
- if (!entry || !moduleUrl) return false;
61
-
62
- // Cheap path first: correct encoding, no filesystem access. Handles spaces and unicode.
63
- let entryUrl;
64
- try { entryUrl = pathToFileURL(entry).href; } catch { return false; }
65
- if (entryUrl === moduleUrl) return true;
66
-
67
- // Then resolve symlinks on both sides. `import.meta.url` is already realpath-resolved by Node,
68
- // but resolving it again is harmless and covers the reverse case (a caller passing an
69
- // unresolved URL) rather than assuming Node's behaviour never changes.
70
- try {
71
- const realEntry = pathToFileURL(realpathSync(entry)).href;
72
- if (realEntry === moduleUrl) return true;
73
- return realEntry === pathToFileURL(realpathSync(new URL(moduleUrl))).href;
74
- } catch {
75
- // An unreadable or deleted entry point is not this function's problem to report. Returning
76
- // false means "do not auto-run", which is the safe direction for an imported module and is
77
- // never the direction that silently skips a gate — a directly-invoked script whose own path
78
- // cannot be stat'd has larger problems that will surface immediately.
79
- return false;
80
- }
81
- }