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.
- package/.claude/settings.local.example.json +5 -5
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +17 -3
- package/README.md +124 -103
- package/SECURITY.md +37 -30
- package/bin/init.mjs +77 -47
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +7 -6
- package/commands/ship.md +29 -16
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +3 -3
- package/hooks/gate-zerowork.mjs +25 -24
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +19 -8
- package/hooks/safety-spine.mjs +3 -3
- package/hooks/sandbox-guard.mjs +84 -40
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
- package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
- 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} +9 -10
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
- package/kernel/reduce/graph.mjs +437 -0
- package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
- 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} +61 -13
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
- 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} +15 -12
- 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} +134 -24
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +11 -9
- package/skills/ba-pitch-analyzer/SKILL.md +5 -5
- 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 +2 -0
- package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
- package/skills/qa-edge-hunter/SKILL.md +7 -3
- package/skills/scope-architect/SKILL.md +40 -6
- package/skills/solution-architect/SKILL.md +19 -4
- package/skills/spec-evaluator/SKILL.md +12 -2
- package/skills/task-executor/SKILL.md +7 -6
- package/skills/tech-lead/SKILL.md +50 -38
- package/skills/tech-lead/references/gates.md +69 -20
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +341 -66
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +1 -1
- package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
- package/hooks/anti-rationalization.mjs +0 -238
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -147
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -108
- 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 -261
- package/skills/tech-lead/references/hard-rules.md +0 -34
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -213
- package/skills/tech-lead/references/round-protocol.md +0 -234
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
- package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
- 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
|
-
}
|