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.
- package/.claude/settings.local.example.json +5 -5
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +54 -107
- package/README.md +165 -151
- package/SECURITY.md +49 -27
- package/bin/init.mjs +93 -108
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +20 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +53 -7
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +16 -17
- package/hooks/gate-zerowork.mjs +107 -25
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +38 -19
- package/hooks/safety-spine.mjs +4 -4
- package/hooks/sandbox-guard.mjs +130 -50
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
- package/kernel/init/run.mjs +489 -0
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
- package/kernel/lib/paths.mjs +491 -0
- package/kernel/probe/concurrency.mjs +510 -0
- package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/kernel/probe/resume.mjs +528 -0
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
- package/kernel/reduce/graph.mjs +437 -0
- package/kernel/reduce/hill.mjs +152 -0
- package/kernel/reduce/ingest.mjs +633 -0
- package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
- package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
- package/kernel/report/export.mjs +325 -0
- package/kernel/report/facts.mjs +347 -0
- package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
- package/kernel/verify/dispatch.mjs +114 -0
- package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
- package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
- package/kernel/verify/skills.mjs +125 -0
- package/kernel/verify/spec.mjs +559 -0
- package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +2 -2
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +2 -2
- package/oracles/snapshot-oracle.mjs +2 -2
- package/oracles/test-oracle.mjs +2 -2
- package/package.json +11 -13
- package/skills/ba-pitch-analyzer/SKILL.md +24 -19
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +15 -41
- package/skills/scope-architect/SKILL.md +57 -18
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +37 -15
- package/skills/spec-evaluator/SKILL.md +24 -7
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/task-executor/SKILL.md +32 -18
- package/skills/tech-lead/SKILL.md +127 -438
- package/skills/tech-lead/references/gates.md +140 -49
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +645 -198
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +56 -18
- package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
- package/skills/translator/SKILL.md +2 -2
- package/hooks/anti-rationalization.mjs +0 -244
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -151
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -109
- package/skills/advisor-protocol/SKILL.md +0 -171
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
- package/skills/spec-evaluator/README.md +0 -93
- package/skills/tech-lead/README.md +0 -71
- package/skills/tech-lead/references/delegation.md +0 -254
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -214
- package/skills/tech-lead/references/round-protocol.md +0 -184
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
- package/skills/tech-lead/scripts/init-run.mjs +0 -326
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
- 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
|
-
}
|