shapeup-sdlc 1.6.3 → 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 (118) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +54 -107
  4. package/README.md +165 -151
  5. package/SECURITY.md +49 -27
  6. package/bin/init.mjs +93 -108
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +20 -0
  9. package/commands/eval.md +5 -4
  10. package/commands/scopes.md +5 -4
  11. package/commands/shape.md +1 -1
  12. package/commands/ship.md +53 -7
  13. package/commands/wire.md +1 -1
  14. package/hooks/dispatch-receipt.mjs +195 -0
  15. package/hooks/gate-intake.mjs +16 -17
  16. package/hooks/gate-zerowork.mjs +107 -25
  17. package/hooks/hooks.json +9 -48
  18. package/hooks/lib/decision.mjs +38 -19
  19. package/hooks/safety-spine.mjs +4 -4
  20. package/hooks/sandbox-guard.mjs +130 -50
  21. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
  22. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
  23. package/kernel/harness.mjs +134 -0
  24. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
  25. package/kernel/init/run.mjs +489 -0
  26. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  27. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
  28. package/kernel/lib/paths.mjs +491 -0
  29. package/kernel/probe/concurrency.mjs +510 -0
  30. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
  31. package/kernel/probe/eval.mjs +77 -0
  32. package/kernel/probe/leg.mjs +125 -0
  33. package/kernel/probe/resume.mjs +528 -0
  34. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
  35. package/kernel/probe/t0.mjs +66 -0
  36. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
  37. package/kernel/reduce/graph.mjs +437 -0
  38. package/kernel/reduce/hill.mjs +152 -0
  39. package/kernel/reduce/ingest.mjs +633 -0
  40. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  41. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
  42. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
  43. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
  44. package/kernel/report/export.mjs +325 -0
  45. package/kernel/report/facts.mjs +347 -0
  46. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
  47. package/kernel/verify/dispatch.mjs +114 -0
  48. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  49. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  50. package/kernel/verify/skills.mjs +125 -0
  51. package/kernel/verify/spec.mjs +559 -0
  52. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
  53. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
  54. package/oracles/_shared.mjs +1 -1
  55. package/oracles/http-oracle.mjs +2 -2
  56. package/oracles/index.mjs +1 -1
  57. package/oracles/process-oracle.mjs +2 -2
  58. package/oracles/snapshot-oracle.mjs +2 -2
  59. package/oracles/test-oracle.mjs +2 -2
  60. package/package.json +11 -13
  61. package/skills/ba-pitch-analyzer/SKILL.md +24 -19
  62. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  63. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  64. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  65. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  66. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  67. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  68. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  69. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  70. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  71. package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
  72. package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
  73. package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
  74. package/skills/coach/SKILL.md +3 -3
  75. package/skills/orient/SKILL.md +2 -1
  76. package/skills/qa-edge-hunter/SKILL.md +15 -41
  77. package/skills/scope-architect/SKILL.md +57 -18
  78. package/skills/scope-hammer/SKILL.md +5 -5
  79. package/skills/shapeup/SKILL.md +3 -3
  80. package/skills/shapeup/resources/context-compaction.md +4 -3
  81. package/skills/solution-architect/SKILL.md +37 -15
  82. package/skills/spec-evaluator/SKILL.md +24 -7
  83. package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
  84. package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
  85. package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
  86. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
  87. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
  88. package/skills/spec-evaluator/references/probing.md +42 -23
  89. package/skills/spec-evaluator/references/report-schema.md +2 -2
  90. package/skills/task-executor/SKILL.md +32 -18
  91. package/skills/tech-lead/SKILL.md +127 -438
  92. package/skills/tech-lead/references/gates.md +140 -49
  93. package/skills/tech-lead/references/protocol.md +832 -0
  94. package/skills/tech-lead/schemas/domain.schema.json +645 -198
  95. package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
  96. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  97. package/skills/tech-lead/schemas/work-result.schema.json +56 -18
  98. package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
  99. package/skills/translator/SKILL.md +2 -2
  100. package/hooks/anti-rationalization.mjs +0 -244
  101. package/hooks/compact-snapshot.mjs +0 -47
  102. package/hooks/gate-deadline.mjs +0 -151
  103. package/hooks/gate-l2.mjs +0 -161
  104. package/hooks/session-rehydrate.mjs +0 -109
  105. package/skills/advisor-protocol/SKILL.md +0 -171
  106. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  107. package/skills/spec-evaluator/README.md +0 -93
  108. package/skills/tech-lead/README.md +0 -71
  109. package/skills/tech-lead/references/delegation.md +0 -254
  110. package/skills/tech-lead/references/invocation.md +0 -45
  111. package/skills/tech-lead/references/ledger-schema.md +0 -214
  112. package/skills/tech-lead/references/round-protocol.md +0 -184
  113. package/skills/tech-lead/references/state-model.md +0 -66
  114. package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
  115. package/skills/tech-lead/scripts/init-run.mjs +0 -326
  116. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
  117. package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
  118. package/skills/translator/README.md +0 -66
@@ -1,184 +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
- ```
12
- round r = 1
13
- loop:
14
- BUILD(r) # see r=1 vs r>1 below
15
- assert board 100% done # GATE L2 — advisory: the hook warns, you decide
16
- verdict, bugs = EVAL(r) # ONE spec-evaluator --feature pass
17
- if verdict == PASS:
18
- SHIP; break
19
- if r >= max_rounds:
20
- ESCALATE(bugs); break # honest stop — no infinite loop
21
- r = r + 1 # next round builds bugs only
22
- ```
23
-
24
- ## BUILD(r) semantics
25
-
26
- | | r = 1 (initial build) | r > 1 (fix build) |
27
- |--|----------------------|-------------------|
28
- | Input | the whole task board | the bug list from EVAL(r-1) |
29
- | Scope | every ready task, dependency/layer order, until board all ✅ | only the tasks/areas named by bugs |
30
- | Command | compile-order `--next` → task-executor `--order` → ingest, looped | compile-order `--task <id> --operation fix` → dispatch → ingest, per bug |
31
- | Passing areas | n/a | never touched |
32
- | SPIKEs | resolved first (they block) | only if a bug is a SPIKE finding |
33
-
34
- Re-opening tasks in r>1: the fix order's WorkResult reports the task `partial` while failing
35
- and `done` when re-verified; ingest-result flips the board accordingly. The board reflects the
36
- churn so the next EVAL sees a green board again.
37
-
38
- Discovered Tasks:
39
- If WorkResults carry `discoveries[]` during BUILD, ingest-result appends them to the discovery
40
- ledger (`.shapeup/<slug>/discovery/ledger.md`) and the build loop pauses after the current
41
- tasks are done. Compile + dispatch a reconcile order (ba-pitch-analyzer, operation: reconcile).
42
- This reconciles them into new tasks and invariants and updates the board; the tech lead bumps
43
- `discovered_rounds` in harness-run.md, then routes back to GATE L1b (Board Review) for PO
44
- approval of the new tasks and estimates before resuming the BUILD loop.
45
-
46
- ## The EVAL timing rule (the core constraint)
47
-
48
- EVAL fires **once** per round and **only** when GATE L2 has confirmed the board is 100% done.
49
- It is never:
50
- - called per task,
51
- - called inside the BUILD loop,
52
- - called on a partial board.
53
-
54
- ## Regression rule (r > 1) — QA-meeting Bước 1c
55
-
56
- A fix round changes code; a fix can break what passed. Therefore EVAL(r>1) scope is **not**
57
- just the fixed bugs:
58
-
59
- ```
60
- EVAL(r) for r > 1:
61
- touched_UCs = every UC referenced (use_case_refs) by a task re-opened in BUILD(r)
62
- scope = fixed bugs' criteria
63
- + FULL re-run of `## Test Surface` rows for every touched UC
64
- (test-surface-conformance dimension, when active)
65
- + completeness re-check (cheap, static)
66
- untouched UCs' surfaces: NOT re-run (their code didn't change; re-probing everything
67
- every round would turn cheap end-of-round QA into a full-suite tax)
68
- ```
69
-
70
- Pre-v2.9 specs (no Test Surface anywhere): the rule degrades to bug-criteria-only, as
71
- before — and the verdict report notes `regression coverage: none (no test surface)`.
72
- Honest reporting over silent coverage claims.
73
-
74
- ## QA edge hunt (post-PASS, pre-ship)
75
-
76
- When EVAL(r) returns PASS for the **first** time in a run, the orchestrator delegates one
77
- `/qa-edge-hunter` pass before SHIP (skippable via `--no-qa`, same spirit as `--no-eval`).
78
- QA is a pure worker: no verdict, no score, no gate — it writes `~` findings to
79
- `discovery/ledger.md` and a `qa/hunt-report.md`. Triage happens at SHIP/GATE L4:
80
- - all findings stay `~` → SHIP; findings carry over as raw ideas (debt-free).
81
- - PO/TL promote any to must-have → a fix round r+1 (those items only) → EVAL
82
- `--single-pass` on them → `/qa-edge-hunter --recheck` (re-probe ONLY the promoted items;
83
- never a second full hunt) → back to L4.
84
- - Circuit breaker applies: out of rounds/appetite → ship with `~` findings recorded.
85
- QA never runs on a FAIL round — a build that hasn't passed conformance isn't worth
86
- edge-hunting yet.
87
-
88
- Rationale (from the long-running harness work): a single end-of-round QA pass over the
89
- running feature is cheap relative to the build (minutes vs hours) and catches the
90
- last-mile defects, whereas grading every task multiplies evaluator cost for little gain
91
- once the generator is competent. If the build round didn't finish, there is nothing
92
- coherent to evaluate yet.
93
-
94
- ## Stop conditions
95
- 1. **PASS** — EVAL(r) verdict is PASS → SHIP.
96
- 2. **max_rounds (OUTER breaker)** — r would exceed `--max-rounds` (default 3) without PASS →
97
- ESCALATE: print the residual bug list, the rounds used, and hand the decision to the PO
98
- (scope contracts present: also `/scope-hammer --breaker outer`). Do not start another
99
- build round automatically.
100
- 3. **attempt_budget (INNER breaker, scope contracts only)** — a single scope's T0 attempt
101
- loop exhausts `--attempts` (default 5) without a green result → does NOT stop the round;
102
- queues a hammer PROPOSAL for GATE H and moves to the next scope in sequence. See
103
- "Three-level circuit breaker" below.
104
- 4. **wall_clock_budget (DEADLINE breaker, opt-in)** — elapsed seconds since the run receipt
105
- exceed `--wall-clock-budget` → do NOT start another build round or another scope; go
106
- straight to GATE H (`/scope-hammer --breaker deadline`). Checked with
107
- `scripts/budget-check.mjs` at every round boundary and enforced by `hooks/gate-deadline.mjs`,
108
- which denies a `task-executor` dispatch past the deadline while leaving `spec-evaluator`,
109
- `scope-hammer`, `qa-edge-hunter` and `advisor-protocol` reachable — a run past its deadline
110
- must still be able to judge, hammer, and close. Off unless configured.
111
- 5. **Hard error** — a sub-skill fails irrecoverably (e.g. spec folder gone, app won't
112
- build at all) → stop and report; do not retry blindly.
113
- 6. **User halt** — at any L-gate the user can stop the run; the ledger preserves state for
114
- `--from` resume.
115
-
116
- ## Three-level circuit breaker
117
-
118
- ```
119
- OUTER round_budget (max_rounds) — the six-week-timebox analog. Decremented once per
120
- round at GATE L2, regardless of how many scopes it
121
- covered. Hitting 0 → GATE H immediately (§ above).
122
- INNER attempt_budget (per scope) — decremented once per T0 attempt inside BUILD round r.
123
- Hitting its cap WITHOUT a T0-green result trips the
124
- inner breaker for that scope only: the scope is queued
125
- as a hammer PROPOSAL (not a hard stop) and the round
126
- moves on to the next scope in the L1b sequence.
127
- DEADLINE wall_clock_budget_s — elapsed seconds since the run receipt. Opt-in; off
128
- unless set at L0. Tripping routes to GATE H with
129
- --breaker deadline. Enforced by hooks/gate-deadline.mjs.
130
- ```
131
-
132
- **Why the third one exists — it was measured, and it corrected an earlier diagnosis.** On the
133
- SDD harness benchmark (F3, Sonnet 5) this harness was killed at the declared 1800 s cap and
134
- published as a DNF. The natural reading was that it had stalled at a gate. Re-reading the
135
- retained transcript says otherwise: **327 turns, 262 tool calls, 37 file writes, 19 gate markers,
136
- last gate L3, and zero stall signals** — the least talkative shapeup run in the whole matrix. It
137
- was working when the clock ran out.
138
-
139
- Both existing breakers count *events*, not time: `round_budget` moves once per round,
140
- `attempt_budget` once per T0 attempt. Neither can observe that round 1 has been running for
141
- twenty-nine minutes, so a run can burn its entire wall clock with both breakers untouched. The
142
- cost is not the DNF row — it is that a run killed from *outside* ships nothing, not even the
143
- scopes that were already green. A breaker that trips from the inside routes to GATE H, where
144
- scope-hammer compares the shippable subset against the baseline and ships what works. Same clock,
145
- different ending.
146
- Nesting rationale (DD-9): a struggling scope should not freeze every other scope's progress
147
- in the same round — only running out of *rounds* (the real six-week analog) stops the whole
148
- run. A scope that trips its inner breaker still gets judged fairly at GATE H: scope-hammer
149
- compares "ship without this scope" against the baseline, same as any other cut candidate — it
150
- is never silently dropped, and it is never allowed to block scopes that ARE working.
151
-
152
- ## Isolated attempt loop — one T0 attempt, in detail (scope contracts only)
153
-
154
- ```
155
- compile-order --scope … --round N --attempt M
156
- → zero-memory WorkOrder (scope contract + this scope's
157
- tasks + digested errors + ledger decisions — compiled
158
- facts, no chat history by construction)
159
- dispatch task-executor --order … → code within substrate; WorkResult in results/
160
- ingest-result <result> → board/ledger writes; escalates[] queued
161
- handle_escalations(≤3/scope/round) → /advisor-protocol; answer promoted to round-ledger.md
162
- immediately (must survive the NEXT attempt's fresh
163
- context — this is what "zero-memory" is compatible with
164
- escalation memory means, DD-8)
165
- t0-verify.mjs → fixtures + DB probe + (on green) seesaw
166
- green → attempt loop breaks; scope reaches DOWNHILL_EXECUTION
167
- red, regression → git stash (never a hard discard) + retry; a FINISHED scope's fixture
168
- broke (PA5) — the whole point of running seesaw before declaring green
169
- red, own fixture → AEGIS-digest the failure into {file, line, core_message} triples,
170
- feed them into the NEXT attempt's brief; loop
171
- ```
172
- This replaces the old flat per-task loop for any scope that has a contract;
173
- scopes/specs without one keep the v0.2.6 behavior verbatim (see BUILD(r) table above).
174
-
175
- ## --no-eval (skip evaluation)
176
- A tech-lead judgment, surfaced at GATE L2: if the feature is clearly within what the model
177
- builds reliably solo, the evaluator is optional overhead. With `--no-eval`, after GATE L2
178
- the run goes straight to SHIP with verdict `not-evaluated` recorded in the ledger and a
179
- clear note that nothing was verified beyond the build's own task-executor GATE D checks.
180
-
181
- ## Round-cost intuition
182
- Build dominates; eval is cheap. Expect each EVAL round to cost a small fraction of a BUILD
183
- round. This is why running eval once per round (not per task) is the right trade: you pay a
184
- little QA at the end of each build and keep the expensive build coherent in between.
@@ -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 advisor-protocol ESCALATE answer,
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,270 +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
- // escalates[] → queue .shapeup/<slug>/escalates/<order>.json for the orchestrator
14
- //
15
- // Zero dependencies, zero network, schema-validated input (a malformed result never mutates
16
- // the board). Single-writer becomes mechanically true, not aspirational.
17
- //
18
- // Usage: node skills/tech-lead/scripts/ingest-result.mjs <result.json> [--cwd <dir>]
19
- // Exit: 0 = ingested, 1 = result rejected (schema) or a write failed.
20
-
21
- import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
22
- import { resolve, join, dirname } from "node:path";
23
- import { fileURLToPath } from "node:url";
24
- import { validate } from "./validate-envelope.mjs";
25
- import { isMain } from "./lib/is-main.mjs";
26
- import { runArgs } from "./lib/argv.mjs";
27
- import { tasksDir, localRoot } from "./lib/paths.mjs";
28
-
29
- const HERE = dirname(fileURLToPath(import.meta.url));
30
- const RESULT_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "../schemas/work-result.schema.json"), "utf8"));
31
-
32
- /**
33
- * @returns {string} Today's date as an ISO `YYYY-MM-DD` string (UTC), for log/frontmatter stamps.
34
- */
35
- const today = () => new Date().toISOString().slice(0, 10);
36
-
37
- /**
38
- * Locate a task file on the LOCAL board by id.
39
- * @param {string} cwd - Working-directory root.
40
- * @param {string} slug - Feature slug.
41
- * @param {string} taskId - Task id prefix to match (e.g. "TASK-001").
42
- * @returns {string|null} Absolute path of the first `<taskId>*.md` file, or null when the tasks
43
- * directory or a matching file does not exist.
44
- */
45
- export function findTaskFile(cwd, slug, taskId) {
46
- const dir = tasksDir(cwd, slug);
47
- if (!existsSync(dir)) return null;
48
- const f = readdirSync(dir).find((n) => n.startsWith(taskId) && n.endsWith(".md"));
49
- return f ? join(dir, f) : null;
50
- }
51
-
52
- /**
53
- * Set a scalar frontmatter field in a task-file body, adding it when absent.
54
- * @param {string} body - Full task-file text.
55
- * @param {string} key - Frontmatter key to set.
56
- * @param {(string|number)} value - Value to write (stringified inline).
57
- * @returns {string} The body with `key: value` set; returned unchanged when the body has no
58
- * frontmatter block.
59
- */
60
- export function setFrontmatter(body, key, value) {
61
- const m = body.match(/^---\r?\n([\s\S]*?)\r?\n---/);
62
- if (!m) return body;
63
- const re = new RegExp(`^${key}:.*$`, "m");
64
- const fm = re.test(m[1]) ? m[1].replace(re, `${key}: ${value}`) : `${m[1]}\n${key}: ${value}`;
65
- return body.replace(m[1], fm);
66
- }
67
-
68
- /**
69
- * Tick or un-tick the first acceptance-criterion checkbox whose text matches `ac`.
70
- * @param {string} body - Full task-file text.
71
- * @param {string} ac - Criterion text to match (case/space-insensitive substring, either direction).
72
- * @param {boolean} checked - true to write `[x]`, false to write `[ ]`.
73
- * @returns {{body:string, hit:boolean}} The updated body and whether a checkbox matched (only the
74
- * first match is changed).
75
- */
76
- export function setCheckbox(body, ac, checked) {
77
- const needle = ac.toLowerCase().replace(/\s+/g, " ").trim();
78
- const lines = body.split(/\r?\n/);
79
- let hit = false;
80
- const out = lines.map((line) => {
81
- const m = line.match(/^(\s*- \[)([ x])(\]\s+)(.*)$/);
82
- if (!m || hit) return line;
83
- const text = m[4].toLowerCase().replace(/\s+/g, " ").trim();
84
- if (text.includes(needle) || needle.includes(text)) {
85
- hit = true;
86
- return `${m[1]}${checked ? "x" : " "}${m[3]}${m[4]}`;
87
- }
88
- return line;
89
- });
90
- return { body: out.join("\n"), hit };
91
- }
92
-
93
- /**
94
- * Flip a task's row in `tasks/_index.md` to a done state.
95
- * @param {string} indexBody - Full board-index text.
96
- * @param {string} taskId - Task id whose row to update.
97
- * @param {boolean} done - When true, rewrite the row's status emoji/word to done; false is a no-op.
98
- * @returns {string} The board text with the matching row updated (unchanged when no row matches).
99
- */
100
- export function updateBoardRow(indexBody, taskId, done) {
101
- return indexBody.split(/\r?\n/).map((line) => {
102
- if (!line.includes(taskId) || !line.includes("|")) return line;
103
- if (done) return line.replace(/⬜|🔄|⏳|🚫/g, "✅").replace(/\b(ready|in-progress|blocked)\b/gi, "done");
104
- return line;
105
- }).join("\n");
106
- }
107
-
108
- /**
109
- * Apply one validated WorkResult to the working tree — the single-writer step (D6): ticks AC
110
- * boxes, flips task status, appends the Execution Log, propagates unblocks, appends discoveries,
111
- * writes the verdict ledger + un-ticks refuted boxes, and queues escalates.
112
- * @param {object} result - A schema-valid WorkResult (order_id, task_results[], discoveries[],
113
- * verdict{criteria[],refuted[]}, escalates[]).
114
- * @param {{cwd:string}} opts - cwd: working-directory root every LOCAL path resolves against.
115
- * @returns {{slug:string, tasks_updated:string[], acs_ticked:number, unblocked:string[],
116
- * discoveries_appended:number, refuted_unticked:number, verdict_lines:number,
117
- * escalates_queued:number}} A summary of every write performed.
118
- * @throws {Error} If a task/board/ledger file it must write is not writable (fs error propagates).
119
- * Side effects: writes task files, `tasks/_index.md`, `discovery/ledger.md`,
120
- * `evaluation/.verdicts-*.jsonl`, and `escalates/*.json` under `.shapeup/<slug>/`.
121
- */
122
- export function applyResult(result, { cwd }) {
123
- const slug = result.order_id.split("/")[0];
124
- const local = localRoot(cwd, slug);
125
- const summary = { slug, tasks_updated: [], acs_ticked: 0, unblocked: [], discoveries_appended: 0, refuted_unticked: 0, verdict_lines: 0, escalates_queued: 0 };
126
-
127
- // 1. Task results → task files + board (old task-executor P3.1/P3.2/P3.6).
128
- const boardIndex = join(local, "tasks", "_index.md");
129
- for (const tr of result.task_results || []) {
130
- const path = findTaskFile(cwd, slug, tr.task_id);
131
- if (!path) continue;
132
- let body = readFileSync(path, "utf8");
133
- for (const acr of tr.ac_results || []) {
134
- if (acr.result === "pass") {
135
- const r = setCheckbox(body, acr.ac, true);
136
- body = r.body;
137
- if (r.hit) summary.acs_ticked++;
138
- }
139
- }
140
- if (tr.status === "done") {
141
- body = setFrontmatter(body, "status", "done");
142
- body = setFrontmatter(body, "completed_at", today());
143
- } else if (tr.status === "partial" || tr.status === "failed") {
144
- body = setFrontmatter(body, "status", "in-progress");
145
- }
146
- // Execution Log (append; the checkbox list must never disagree with it).
147
- const logLines = (tr.ac_results || []).map((a) => `- ${a.ac}: ${a.result}${a.evidence ? ` (${a.evidence})` : ""}`).join("\n");
148
- 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`;
149
- writeFileSync(path, body);
150
- summary.tasks_updated.push(tr.task_id);
151
- if (tr.status === "done" && existsSync(boardIndex)) {
152
- writeFileSync(boardIndex, updateBoardRow(readFileSync(boardIndex, "utf8"), tr.task_id, true));
153
- }
154
- }
155
-
156
- // 2. Unblock propagation (old P3.4): any blocked task whose dependencies are all done → ready.
157
- const tasksDir = join(local, "tasks");
158
- if (existsSync(tasksDir)) {
159
- const files = readdirSync(tasksDir).filter((f) => /^TASK-[\w.-]+\.md$/i.test(f));
160
- const statusOf = {};
161
- const parsed = files.map((f) => {
162
- const body = readFileSync(join(tasksDir, f), "utf8");
163
- const id = (body.match(/^id:\s*(TASK-[\w.-]+)/im) || [])[1] || f.replace(/\.md$/, "");
164
- const status = (body.match(/^status:\s*(\S+)/im) || [])[1] || "unknown";
165
- const deps = (body.match(/^depends_on:\s*\[([^\]]*)\]/im) || [, ""])[1]
166
- .split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
167
- statusOf[id] = status;
168
- return { f, id, status, deps, body };
169
- });
170
- for (const t of parsed) {
171
- if (t.status === "blocked" && t.deps.length && t.deps.every((d) => statusOf[d] === "done")) {
172
- writeFileSync(join(tasksDir, t.f), setFrontmatter(t.body, "status", "ready"));
173
- summary.unblocked.push(t.id);
174
- if (existsSync(boardIndex)) {
175
- const idx = readFileSync(boardIndex, "utf8").split(/\r?\n/).map((line) =>
176
- line.includes(t.id) && line.includes("|")
177
- ? line.replace(/🚫|⏳/g, "⬜").replace(/\bblocked\b/gi, "ready")
178
- : line).join("\n");
179
- writeFileSync(boardIndex, idx);
180
- }
181
- }
182
- }
183
- }
184
-
185
- // 3. Discoveries → the ledger (old P3.7 / QA H.3). Single writer: this script.
186
- if (result.discoveries?.length) {
187
- const ledgerDir = join(local, "discovery");
188
- mkdirSync(ledgerDir, { recursive: true });
189
- const ledger = join(ledgerDir, "ledger.md");
190
- if (!existsSync(ledger)) writeFileSync(ledger, `---\nfeature: ${slug}\n---\n# Discovery Ledger — ${slug}\n`);
191
- const lines = result.discoveries.map((d) => {
192
- 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);
193
- 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("")}`;
194
- }).join("\n");
195
- appendFileSync(ledger, `\n## Discovered — ${result.order_id} (${today()})\n${lines}\n`);
196
- summary.discoveries_appended = result.discoveries.length;
197
- }
198
-
199
- // 4. Verdict bookkeeping (old evaluator B.0/B.2/B.2b) — judge returns data, ingest writes.
200
- if (result.verdict) {
201
- const evalDir = join(local, "evaluation");
202
- mkdirSync(evalDir, { recursive: true });
203
- if (result.verdict.criteria?.length) {
204
- const target = result.order_id.split("/")[1] || "run";
205
- const ledger = join(evalDir, `.verdicts-${target}.jsonl`);
206
- let run = 1;
207
- if (existsSync(ledger)) {
208
- const prior = readFileSync(ledger, "utf8").trim().split(/\n/).filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
209
- run = prior.reduce((mx, r) => Math.max(mx, r.run || 0), 0) + 1;
210
- }
211
- const lines = result.verdict.criteria.map((c) => JSON.stringify({
212
- run, dimension: c.dimension || "spec-conformance", criterion: c.criterion,
213
- verdict: c.verdict, confidence: c.confidence, reprobed: !!c.reprobed,
214
- evidence: c.evidence || "", at: new Date().toISOString(),
215
- })).join("\n");
216
- appendFileSync(ledger, lines + "\n");
217
- summary.verdict_lines = result.verdict.criteria.length;
218
- }
219
- for (const ref of result.verdict.refuted || []) {
220
- const path = findTaskFile(cwd, slug, ref.task_id);
221
- if (!path) continue;
222
- let body = readFileSync(path, "utf8");
223
- const r = setCheckbox(body, ref.ac, false);
224
- if (r.hit) summary.refuted_unticked++;
225
- body = setFrontmatter(r.body, "eval_verdict", "fail");
226
- body = setFrontmatter(body, "eval_at", today());
227
- writeFileSync(path, body);
228
- }
229
- }
230
-
231
- // 5. Escalates → a queue file the orchestrator adjudicates via advisor-protocol.
232
- if (result.escalates?.length) {
233
- const escDir = join(local, "escalates");
234
- mkdirSync(escDir, { recursive: true });
235
- const out = join(escDir, `${result.order_id.split("/")[1] || "run"}.json`);
236
- writeFileSync(out, JSON.stringify(result.escalates, null, 2) + "\n");
237
- summary.escalates_queued = result.escalates.length;
238
- }
239
-
240
- return summary;
241
- }
242
-
243
- // ---------------------------------------------------------------------------
244
- /** The typed argv contract (see `./lib/argv.mjs`). */
245
- export const ARGV_SPEC = {
246
- usage: "ingest-result.mjs <result.json> [--cwd <dir>]",
247
- _: { arity: 1, max: 1, name: "result.json" },
248
- cwd: { type: "path" },
249
- };
250
-
251
- const isMainModule = isMain(import.meta.url);
252
- if (isMainModule) {
253
- const args = runArgs(ARGV_SPEC);
254
- const file = args._[0];
255
- const cwd = resolve(args.cwd || process.cwd());
256
-
257
- let result;
258
- try { result = JSON.parse(readFileSync(resolve(file), "utf8")); }
259
- catch (e) { console.error(` ✗ result unreadable: ${e.message}`); process.exit(1); }
260
-
261
- const { valid, errors } = validate(result, RESULT_SCHEMA);
262
- if (!valid) {
263
- console.error("ingest-result: result rejected — a malformed result never mutates the board:");
264
- for (const e of errors) console.error(` ✗ ${e}`);
265
- process.exit(1);
266
- }
267
- const s = applyResult(result, { cwd });
268
- 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} · escalates queued: ${s.escalates_queued}`);
269
- if (s.escalates_queued) process.exitCode = 0; // escalates are data, not failure
270
- }