shapeup-sdlc 1.6.2 → 1.7.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 (85) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/AGENTS.md +36 -103
  3. package/README.md +60 -67
  4. package/SECURITY.md +31 -16
  5. package/bin/init.mjs +24 -69
  6. package/commands/build.md +19 -0
  7. package/commands/eval.md +5 -4
  8. package/commands/scopes.md +5 -4
  9. package/commands/shape.md +1 -1
  10. package/commands/ship.md +37 -4
  11. package/hooks/anti-rationalization.mjs +7 -13
  12. package/hooks/compact-snapshot.mjs +5 -5
  13. package/hooks/gate-deadline.mjs +12 -16
  14. package/hooks/gate-intake.mjs +13 -14
  15. package/hooks/gate-l2.mjs +3 -3
  16. package/hooks/gate-zerowork.mjs +97 -16
  17. package/hooks/lib/decision.mjs +21 -13
  18. package/hooks/safety-spine.mjs +1 -1
  19. package/hooks/sandbox-guard.mjs +83 -47
  20. package/hooks/session-rehydrate.mjs +8 -9
  21. package/hooks/slop-cleaner.mjs +1 -1
  22. package/oracles/_shared.mjs +1 -1
  23. package/oracles/http-oracle.mjs +1 -1
  24. package/oracles/index.mjs +1 -1
  25. package/oracles/process-oracle.mjs +1 -1
  26. package/oracles/snapshot-oracle.mjs +1 -1
  27. package/oracles/test-oracle.mjs +1 -1
  28. package/package.json +3 -7
  29. package/skills/ba-pitch-analyzer/SKILL.md +20 -15
  30. package/skills/ba-pitch-analyzer/references/doc-schemas.md +1 -1
  31. package/skills/ba-pitch-analyzer/references/task-generation.md +6 -6
  32. package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
  33. package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +5 -5
  34. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +2 -2
  35. package/skills/coach/SKILL.md +3 -3
  36. package/skills/orient/SKILL.md +2 -1
  37. package/skills/qa-edge-hunter/SKILL.md +9 -39
  38. package/skills/scope-architect/SKILL.md +17 -12
  39. package/skills/scope-hammer/SKILL.md +5 -5
  40. package/skills/shapeup/SKILL.md +3 -3
  41. package/skills/shapeup/resources/context-compaction.md +4 -3
  42. package/skills/solution-architect/SKILL.md +18 -11
  43. package/skills/spec-evaluator/SKILL.md +12 -5
  44. package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
  45. package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
  46. package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
  47. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
  48. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
  49. package/skills/spec-evaluator/references/probing.md +42 -23
  50. package/skills/spec-evaluator/references/report-schema.md +2 -2
  51. package/skills/spec-evaluator/scripts/verdict-ledger.mjs +1 -1
  52. package/skills/task-executor/SKILL.md +25 -12
  53. package/skills/tech-lead/SKILL.md +115 -438
  54. package/skills/tech-lead/references/delegation.md +38 -31
  55. package/skills/tech-lead/references/gates.md +75 -33
  56. package/skills/tech-lead/references/hard-rules.md +34 -0
  57. package/skills/tech-lead/references/invocation.md +2 -2
  58. package/skills/tech-lead/references/ledger-schema.md +17 -18
  59. package/skills/tech-lead/references/round-protocol.md +77 -27
  60. package/skills/tech-lead/references/state-model.md +1 -1
  61. package/skills/tech-lead/schemas/domain.schema.json +320 -148
  62. package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
  63. package/skills/tech-lead/schemas/work-result.schema.json +55 -17
  64. package/skills/tech-lead/scripts/aegis-digest.mjs +1 -1
  65. package/skills/tech-lead/scripts/budget-check.mjs +7 -13
  66. package/skills/tech-lead/scripts/compile-order.mjs +69 -22
  67. package/skills/tech-lead/scripts/fit-check.mjs +10 -10
  68. package/skills/tech-lead/scripts/gate-answers.mjs +11 -12
  69. package/skills/tech-lead/scripts/hill-derive.mjs +133 -0
  70. package/skills/tech-lead/scripts/ingest-result.mjs +7 -19
  71. package/skills/tech-lead/scripts/init-run.mjs +60 -18
  72. package/skills/tech-lead/scripts/lib/argv.mjs +1 -1
  73. package/skills/tech-lead/scripts/lib/contract-md.mjs +10 -10
  74. package/skills/tech-lead/scripts/lib/is-main.mjs +11 -12
  75. package/skills/tech-lead/scripts/lib/paths.mjs +15 -13
  76. package/skills/tech-lead/scripts/resume-state.mjs +387 -0
  77. package/skills/tech-lead/scripts/run-snapshot.mjs +1 -6
  78. package/skills/tech-lead/scripts/run-workflow.mjs +381 -0
  79. package/skills/tech-lead/scripts/ship-report.mjs +33 -2
  80. package/skills/tech-lead/scripts/stats.mjs +9 -10
  81. package/skills/tech-lead/scripts/t0-verify.mjs +4 -4
  82. package/skills/tech-lead/scripts/trace-lint.mjs +2 -2
  83. package/skills/tech-lead/workflows/shapeup-run.js +911 -0
  84. package/skills/translator/SKILL.md +2 -2
  85. package/skills/advisor-protocol/SKILL.md +0 -171
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://github.com/nguyenvanphituoc/shapeup-sdlc-plugin/schemas/gate-answers.schema.json",
4
4
  "title": "GateAnswerSet",
5
- "description": "A pre-recorded set of PO decisions, one per gate, that lets a run cross its gates without a human in the loop. This is NOT 'gates off' — every gate still emits its block and still records a decision in the ledger; the decision's SOURCE becomes this file instead of a live human. That distinction is the whole design: an unattended run with no answer set stalls at the first ⏸ and burns its wall-clock budget (measured: shapeup-sdlc DNF at 1800s on benchmark F3), while an unattended run with one produces the same audit trail an interactive run does.",
5
+ "description": "A pre-recorded set of PO decisions, one per gate, that lets a run cross its gates without a human in the loop. This is NOT 'gates off' — every gate still emits its block and still records a decision in the ledger; the decision's SOURCE becomes this file instead of a live human. That distinction is the whole design: an unattended run with no answer set stalls at the first ⏸ and burns its whole wall-clock budget having built nothing, while an unattended run with one produces the same audit trail an interactive run does.",
6
6
  "type": "object",
7
7
  "required": ["version", "preset", "answers"],
8
8
  "additionalProperties": false,
@@ -26,7 +26,7 @@
26
26
  "escalation": {
27
27
  "type": "object",
28
28
  "additionalProperties": false,
29
- "description": "Policy for a worker's structured ESCALATE (advisor-protocol), which is a gate in all but name.",
29
+ "description": "RESERVED. Policy for a worker-raised question, which would be a gate in all but name. Inert today: WorkResult carries no escalates field, so no worker can raise one. Kept as a declared key because the answer-set schema forbids additional properties, and removing it would invalidate every gate-answers.json already on disk.",
30
30
  "properties": {
31
31
  "default": {
32
32
  "type": "string",
@@ -3,38 +3,76 @@
3
3
  "title": "WorkResult",
4
4
  "description": "The worker → orchestrator envelope (pure-skill architecture v1.0). Everything a worker used to write into shared files it now RETURNS as data; skills/tech-lead/scripts/ingest-result.mjs performs the writes (tick AC boxes, flip board status, append execution log, propagate unblocks, append discoveries) deterministically, in one place. Path: .shapeup/<slug>/results/r<N>-a<M>.json. Every record type is DEFINED CENTRALLY in domain.schema.json — this file only shapes the envelope.",
5
5
  "type": "object",
6
- "required": ["schema_version", "order_id", "status"],
6
+ "required": [
7
+ "schema_version",
8
+ "order_id",
9
+ "status"
10
+ ],
7
11
  "properties": {
8
- "schema_version": { "type": "integer", "enum": [1] },
9
- "order_id": { "type": "string", "description": "Copied verbatim from the answered WorkOrder — the 1:1 join key." },
10
- "worker": { "type": "string" },
11
- "status": { "type": "string", "enum": ["done", "partial", "escalated", "failed"] },
12
+ "schema_version": {
13
+ "type": "integer",
14
+ "enum": [
15
+ 1
16
+ ]
17
+ },
18
+ "order_id": {
19
+ "type": "string",
20
+ "description": "Copied verbatim from the answered WorkOrder — the 1:1 join key."
21
+ },
22
+ "worker": {
23
+ "type": "string"
24
+ },
25
+ "status": {
26
+ "type": "string",
27
+ "enum": [
28
+ "done",
29
+ "partial",
30
+ "escalated",
31
+ "failed"
32
+ ]
33
+ },
12
34
  "task_results": {
13
35
  "type": "array",
14
36
  "description": "Per-task outcome. ingest-result flips the board row, ticks GATE-verified AC boxes, and appends the Execution Log from these entries.",
15
- "items": { "$ref": "domain.schema.json#/$defs/TaskResult" }
37
+ "items": {
38
+ "$ref": "domain.schema.json#/$defs/TaskResult"
39
+ }
16
40
  },
17
41
  "files_touched": {
18
42
  "type": "array",
19
- "items": { "$ref": "domain.schema.json#/$defs/FileTouched" }
20
- },
21
- "escalates": {
22
- "type": "array",
23
- "description": "advisor-protocol ESCALATE blocks — the worker's one outward port.",
24
- "items": { "$ref": "domain.schema.json#/$defs/Escalate" }
43
+ "items": {
44
+ "$ref": "domain.schema.json#/$defs/FileTouched"
45
+ }
25
46
  },
26
47
  "discoveries": {
27
48
  "type": "array",
28
49
  "description": "Raw discovered lines. ingest-result appends them to the discovery ledger — the worker never touches the ledger itself.",
29
- "items": { "$ref": "domain.schema.json#/$defs/Discovery" }
50
+ "items": {
51
+ "$ref": "domain.schema.json#/$defs/Discovery"
52
+ }
53
+ },
54
+ "verdict": {
55
+ "$ref": "domain.schema.json#/$defs/Verdict"
30
56
  },
31
- "verdict": { "$ref": "domain.schema.json#/$defs/Verdict" },
32
57
  "artifacts": {
33
58
  "type": "array",
34
59
  "description": "Domain artifacts the worker wrote inside its substrate (spec docs, orient/ files, reports).",
35
- "items": { "type": "string" }
60
+ "items": {
61
+ "type": "string"
62
+ }
36
63
  },
37
- "assumptions": { "type": "array", "items": { "type": "string" } },
38
- "deviations": { "type": "array", "items": { "type": "string" } }
64
+ "assumptions": {
65
+ "type": "array",
66
+ "items": {
67
+ "type": "string"
68
+ }
69
+ },
70
+ "deviations": {
71
+ "type": "array",
72
+ "items": {
73
+ "type": "string"
74
+ },
75
+ "description": "When status is \"escalated\", the FIRST entry is the blocker: one specific, answerable question with the context needed to answer it. There is no escalates[] field, so this is the only channel a blocked worker has."
76
+ }
39
77
  }
40
78
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // AEGIS digester (design spec v1.1 §3.4/§4.5, PA6 countermeasure).
2
+ // AEGIS digester compresses a failed verification into the triples the next attempt reads.
3
3
  //
4
4
  // Distills raw build/test/Playwright logs into {file, line, core_message} triples so a
5
5
  // task-executor's next attempt gets a few lines of signal instead of a full stack-trace dump.
@@ -1,21 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // THE THIRD BREAKER — wall clock. Derived, never claimed; checked at every round boundary.
3
3
  //
4
- // WHY THIS EXISTS (measured, and it corrects an earlier diagnosis).
4
+ // WHY THIS EXISTS (and it corrects an earlier diagnosis).
5
5
  //
6
- // SDD harness benchmark, F3 (wiring trap, Sonnet 5): this harness was killed at the declared
7
- // 1800 s cap and published as a DNF. The obvious reading the one the first write-up implied —
8
- // was that it had stalled at a gate with no human to sign off.
6
+ // A run killed at an external time cap produces nothing scoreable, and the obvious reading is that
7
+ // it stalled at a gate with no human to sign off.
9
8
  //
10
- // Re-reading the retained transcript with transcript-level metrics says otherwise, and the
11
- // numbers are not close:
12
- //
13
- // 327 assistant turns · 262 tool calls · 130 work calls · 37 file writes
14
- // 19 gate markers, last gate reached L3 · narration_ratio 0.047 · stall_signals 0
15
- //
16
- // It was not waiting. It was the LEAST talkative shapeup run in the whole matrix and one of the
17
- // busiest rows in the dataset, and it got killed mid-loop at GATE L3 — Verdict & Loop. It ran
18
- // out of clock while genuinely working.
9
+ // Reading the transcript with transcript-level metrics often says otherwise, and not narrowly:
10
+ // steady turns, steady tool calls, steady file writes, gate markers advancing, a low narration
11
+ // ratio and zero stall signals — a run killed mid-loop at GATE L3, Verdict & Loop. It was not
12
+ // waiting. It ran out of clock while genuinely working.
19
13
  //
20
14
  // That makes the defect specific: **both existing breakers count events, not time.**
21
15
  // `round_budget` decrements once per round; `attempt_budget` decrements once per T0 attempt.
@@ -4,7 +4,7 @@
4
4
  // The orchestrator's pipeline sub-layer: assembles the structured input envelope a worker is
5
5
  // dispatched with. Replaces tech-lead's hand-assembled `isolated_brief()` prose step and every
6
6
  // worker's GATE A/B plumbing (path resolution, run-state parse, board glob-matching, dependency
7
- // reads, mode detection). Deterministic, zero LLM tokens (DD-7).
7
+ // reads, mode detection). Deterministic, zero LLM tokens.
8
8
  //
9
9
  // A worker depends on its ORDER, never on filesystem topology — moving a directory again
10
10
  // (the v3.2 lesson) touches this script, zero skills.
@@ -39,6 +39,7 @@ import {
39
39
  relShared, globLocal, globShared, relKnowledgeBase,
40
40
  } from "./lib/paths.mjs";
41
41
  import { readContract, SCOPE_CONTRACT } from "./lib/contract-md.mjs";
42
+ import { writeActiveOrder } from "./resume-state.mjs";
42
43
 
43
44
  const HERE = dirname(fileURLToPath(import.meta.url));
44
45
  const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "../schemas/work-order.schema.json"), "utf8"));
@@ -145,9 +146,9 @@ export function ledgerDecisions(ledgerText, scopeId) {
145
146
 
146
147
  /**
147
148
  * Resolve the write-contract (sandbox substrate) for an operation — one whitelist template per
148
- * operation, so mode/flag differences are enforced by the sandbox hook, not trusted to prose.
149
- * @param {string} operation - The order's operation (execute|analyze|generate-board|reconcile|
150
- * retrofit-surface|coverage|map-scopes|remap|split-scope|wire|evaluate|hunt|recheck|orient|…).
149
+ * operation, so mode/flag differences are enforced by the sandbox hook reading the order's substrate, not trusted to prose.
150
+ * @param {string} operation - The order's operation (execute|fix|spike|analyze|reconcile|
151
+ * retrofit-surface|coverage|map-scopes|wire|evaluate|orient|hunt|translate|hammer|coach).
151
152
  * @param {{slug?:string, specDir?:string, scope?:object}} [ctx] - slug (names LOCAL/SHARED roots),
152
153
  * specDir (overrides the default spec path), scope (contract supplying allowed/shared substrates).
153
154
  * @returns {{allowed:string[], shared?:string[], frozen?:string[], append_only?:string[]}} The
@@ -171,11 +172,7 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
171
172
  };
172
173
  case "analyze":
173
174
  return { allowed: [`${spec}/**`, `${local}/**`], frozen: [] };
174
- case "generate-board":
175
- return {
176
- allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
177
- frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`],
178
- };
175
+
179
176
  case "reconcile":
180
177
  return {
181
178
  allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
@@ -184,14 +181,14 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
184
181
  };
185
182
  case "retrofit-surface":
186
183
  return { allowed: [], append_only: [`${spec}/usecases/*.md#Test Surface`], frozen: FROZEN_SPEC_CORE };
184
+
187
185
  case "coverage":
188
- // ba-pitch-analyzer writes the SHARED requirement registry only the REQ source and the
189
- // spec core stay frozen (the registry is a separate derived file, never an edit of the source).
190
- return {
191
- allowed: [globShared(slug, "requirements.md")],
192
- frozen: [...FROZEN_SPEC_CORE, `${scopesDir}/**`, `${local}/tasks/**`],
193
- };
194
- case "map-scopes": case "remap": case "split-scope":
186
+ // The covers-closure input truth. Writes ONLY the derived registry: the REQ source it
187
+ // extracts from is frozen alongside the spec core, because a planner that may edit the
188
+ // requirements it is being measured against is not measuring anything.
189
+ return { allowed: [globShared(slug, "requirements.md")], frozen: FROZEN_SPEC_CORE };
190
+
191
+ case "map-scopes":
195
192
  return {
196
193
  allowed: [`${scopesDir}/*.md`, globShared(slug, "scope-board.md")],
197
194
  frozen: [...FROZEN_SPEC_CORE, `${local}/tasks/**`],
@@ -205,10 +202,16 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
205
202
  };
206
203
  case "evaluate":
207
204
  return { allowed: [`${local}/evaluation/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
208
- case "hunt": case "recheck":
205
+ case "hunt":
209
206
  return { allowed: [`${local}/qa/**`], frozen: [`${spec}/**`, `${local}/tasks/**`] };
210
207
  case "orient":
211
208
  return { allowed: [`${local}/orient/**`], frozen: [`${spec}/**`] };
209
+ case "translate":
210
+ return { allowed: [globShared(slug, "shaping/*.md"), globShared(slug, "glossary.md")] };
211
+ case "hammer":
212
+ return { allowed: [globShared(slug, "REPORT.md"), `${local}/reports/**`] };
213
+ case "coach":
214
+ return { allowed: [relKnowledgeBase("*")] };
212
215
  default:
213
216
  return { allowed: [`${local}/**`] };
214
217
  }
@@ -325,7 +328,28 @@ export function compileOrder({
325
328
  slug, worker, mode = "orchestrated", operation, round, attempt,
326
329
  scope, tasks, decisions, digestedErrors, trialHistory, testCmd, payloadExtra, specDir, interaction,
327
330
  }) {
328
- const suffix = round && attempt ? `r${round}-a${attempt}` : round ? `${operation}-r${round}` : operation;
331
+ // A build order's id carries its SCOPE. Without it, `r<round>-a<attempt>` is the same name for
332
+ // every scope in a round, so scope 2's order file overwrites scope 1's the moment it is compiled
333
+ // — measured on the kill/resume probe, where it made `orders/ minus results/` read EMPTY on a run
334
+ // that was re-dispatching a completed phase. The contract row watching that property therefore
335
+ // passed on the exact failure it exists to catch. `t0-verify.mjs`'s verdict artifacts have always
336
+ // been self-identifying this way (`r<R>-a<A>-t<T>.json`, `wx`-created); orders now match, so
337
+ // `orders/` is an audit trail of dispatches rather than a rolling buffer of the last one.
338
+ //
339
+ // The shape stays `<slug>/<suffix>`: every consumer splits on the FIRST "/" (ingest-result.mjs
340
+ // reads [0] as the slug and [1] as the file stem), and a scope id is already filename-safe
341
+ // because it is a scope contract's own basename.
342
+ // `scope` arrives as the PARSED contract object (the CLI reads the .md and embeds it), so the id
343
+ // comes from its own `scope_id` field; a caller that passes a path instead still works. The id is
344
+ // lowercased and stripped to the character class `work-order.schema.json` allows after the "/" —
345
+ // an order that cannot pass its own schema is refused before it is written, and a naming
346
+ // improvement must not be able to cause that.
347
+ const rawScopeId = scope && typeof scope === "object"
348
+ ? scope.scope_id
349
+ : (scope ? String(scope).split("/").pop().replace(/\.(md|json)$/, "") : null);
350
+ const scopeId = String(rawScopeId || "").toLowerCase().replace(/[^a-z0-9.-]/g, "-").replace(/^[^a-z0-9]+/, "");
351
+ const buildSuffix = round && attempt ? (scopeId ? `${scopeId}-r${round}-a${attempt}` : `r${round}-a${attempt}`) : null;
352
+ const suffix = buildSuffix || (round ? `${operation}-r${round}` : operation);
329
353
  const order = {
330
354
  schema_version: 1,
331
355
  order_id: `${slug}/${suffix}`,
@@ -412,12 +436,12 @@ if (isMainModule) {
412
436
  // Operation → owning worker (mirrors domain.schema.json $defs/Operation ownership). Lets a
413
437
  // non-build dispatch resolve its worker from the operation alone, without a redundant --worker.
414
438
  const OP_OWNER = {
415
- analyze: "ba-pitch-analyzer", "generate-board": "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
439
+ analyze: "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
416
440
  "retrofit-surface": "ba-pitch-analyzer", coverage: "ba-pitch-analyzer",
417
- "map-scopes": "scope-architect", remap: "scope-architect", "split-scope": "scope-architect",
441
+ "map-scopes": "scope-architect",
418
442
  wire: "solution-architect", evaluate: "spec-evaluator", orient: "orient",
419
- hunt: "qa-edge-hunter", recheck: "qa-edge-hunter", translate: "translator",
420
- hammer: "scope-hammer", coach: "coach", adjudicate: "advisor-protocol",
443
+ hunt: "qa-edge-hunter", translate: "translator",
444
+ hammer: "scope-hammer", coach: "coach",
421
445
  };
422
446
  let operation = flag("operation") || (scopePath || flag("task") || has("next") ? "execute" : null);
423
447
  const worker = flag("worker")
@@ -501,6 +525,29 @@ if (isMainModule) {
501
525
  writeFileSync(outPath, JSON.stringify(order, null, 2) + "\n");
502
526
  console.log(outPath);
503
527
 
528
+ // POINT THE SANDBOX AT THIS ORDER, HERE, because this is the only place every lane passes
529
+ // through.
530
+ //
531
+ // `hooks/sandbox-guard.mjs` enforces the order's own `substrate` block — allowed/shared,
532
+ // append_only, frozen — and it finds the order through `.shapeup/active-order`. Until this
533
+ // write existed the pointer had exactly one author, the workflow script, so the guard fenced
534
+ // the workflow lane and DEFERRED everywhere else: `--tiny`, the prose round loop, and a
535
+ // standalone `/build` all compiled an order carrying a write contract that nothing enforced.
536
+ // A substrate that is only enforced on the lane that also happens to be the most supervised
537
+ // one is the wrong way round.
538
+ //
539
+ // Compiling an order is the moment the write contract comes into existence, so it is the
540
+ // correct moment to publish it. The workflow script still sets the pointer explicitly before
541
+ // dispatch (it interleaves phases and must be exact about which order is live); this write
542
+ // makes the SAME mechanism cover callers that never reach that code.
543
+ //
544
+ // Best-effort, on stderr, and never fatal: a compiled order that cannot publish its pointer is
545
+ // still a valid order, and stdout belongs to the order path the caller consumes. The guard
546
+ // fails open on a missing pointer by design, so the failure mode is "unfenced", which is
547
+ // exactly what a warning is for.
548
+ const ptr = writeActiveOrder(cwd, slug, outPath);
549
+ if (!ptr.ok) console.error(`compile-order: warning — ${ptr.reason} (this order's substrate will not be enforced)`);
550
+
504
551
  // The stagnation breaker reports on stderr, never on stdout: stdout is the order path the
505
552
  // orchestrator consumes, and a breaker that corrupts the pipeline's own output would be worse
506
553
  // than the flailing it detects. It advises; the orchestrator queues the GATE H proposal.
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // GATE L0.3 — FIT CHECK. Which lane does this change belong in? Decided by measurement.
3
3
  //
4
- // WHY THIS EXISTS (measured, and it is the root cause the other three fixes did not touch).
4
+ // WHY THIS EXISTS (and it is the root cause the other three fixes did not touch).
5
5
  //
6
- // On the SDD harness benchmark, F3 ("add a `summary` command" to a six-file CLI — one new module
7
- // plus one dispatcher wiring) was run through the full eleven-gate pipeline and never finished.
8
- // Not once, across four attempts. It was killed at the 1800s cap mid-build; then, with a
9
- // wall-clock breaker fitted, it reached ship-triage and was killed there; then it produced an
10
- // honest report saying its two must-haves were 0% started.
6
+ // A change of the size "add a `summary` command to a six-file CLI" — one new module plus one
7
+ // dispatcher wiring run through the full eleven-gate pipeline does not finish. Not once, across
8
+ // repeated attempts: killed at the time cap mid-build; then, with a wall-clock breaker fitted,
9
+ // killed at ship-triage; then producing an honest report saying its two must-haves were 0%
10
+ // started.
11
11
  //
12
12
  // Every one of those fixes made the FAILURE better. None of them made the RUN finish, because
13
13
  // none addressed why a three-file change was consuming half an hour: **the ceremony was not
14
14
  // sized to the change.**
15
15
  //
16
- // The harness already knew. In the pilot transcripts, tech-lead identified F1 at GATE L0 as
16
+ // The harness already knew. In those transcripts tech-lead identified the change at GATE L0 as
17
17
  // "about as small as they come… squarely inside the --tiny lane" — and then ran the full pipeline
18
18
  // anyway, because the lane was a judgment the model was free to talk itself out of. That is the
19
19
  // same class of defect as narration and as prose consent: an invariant living somewhere a model
@@ -121,7 +121,7 @@ export function decideLane({ intake, files }) {
121
121
  };
122
122
 
123
123
  // CONJUNCTIVE, and it must be. The first version asked "is there evidence this is BIG?" and
124
- // defaulted to tiny when it found none — which classified all three benchmark features as tiny,
124
+ // defaulted to tiny when it found none — which classified every trial feature as tiny,
125
125
  // including the five-seam one that genuinely needs the pipeline and used two evaluation rounds
126
126
  // to pass. A router that confident and that wrong is worse than no router: it would skip review
127
127
  // on exactly the change that needed it.
@@ -152,7 +152,7 @@ export function decideLane({ intake, files }) {
152
152
  // Say it on every invocation rather than burying it in a doc. These thresholds are calibrated
153
153
  // against THREE features. That is enough to stop the router being obviously wrong and nowhere
154
154
  // near enough to trust it silently, so it recommends and never denies.
155
- calibration: "advisory — thresholds fitted on 3 benchmark features (n=3); `full` is the default and the PO may override with --lane",
155
+ calibration: "advisory — thresholds are coarse and `full` is the default; the PO may override with --lane",
156
156
  };
157
157
  }
158
158
 
@@ -186,7 +186,7 @@ export function main() {
186
186
  ...result,
187
187
  // The sentence the orchestrator must act on, so the decision is not re-derived from the JSON.
188
188
  action: result.lane === "tiny"
189
- ? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is the measured cause of a benchmark run that never finished, four attempts running."
189
+ ? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is why a run burns its whole clock without finishing."
190
190
  : "FULL LANE. Run the complete pipeline: ORIENT → WIRE → MAP SCOPES → BUILD → EVAL → QA → GATE H → L4.",
191
191
  }, null, 2));
192
192
  }
@@ -1,21 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  // GATE ANSWER SET — cross a gate with a pre-recorded decision instead of a live human.
3
3
  //
4
- // WHY THIS EXISTS (measured, not theorized).
4
+ // WHY THIS EXISTS (observed, not theorized).
5
5
  //
6
6
  // This harness pauses at every ⏸ gate for PO sign-off by default. That is the point of it. But
7
- // on the SDD harness benchmark it produced two distinct failures, both of which look like the
8
- // harness being slow or broken rather than the harness being safe:
7
+ // unattended it produces two distinct failures, both of which look like the harness being slow or
8
+ // broken rather than the harness being safe:
9
9
  //
10
- // 1. STALL → TIMEOUT. F3 (Sonnet 5): the run was killed at the declared 1800s cap having
11
- // produced nothing scoreable, while the no-harness control finished the same feature in
12
- // 51 seconds. A run with no human at the keyboard sitting at a gate does not fail — it
13
- // waits, and a wait is indistinguishable from work until the budget runs out.
10
+ // 1. STALL → TIMEOUT. The run is killed at its time cap having produced nothing scoreable,
11
+ // on a feature that takes minutes to build by hand. A run with no human at the keyboard
12
+ // sitting at a gate does not fail it waits, and a wait is indistinguishable from work
13
+ // until the budget runs out.
14
14
  // 2. CONSENT-BY-PROSE. The workaround was a paragraph of English in the prompt ("treat this
15
- // message as advance sign-off for every gate"). On Sonnet that worked. On Haiku 4.5 the
16
- // model read the paragraph, read the 450-line gate list, and narrated the pipeline instead
17
- // of running it 29% acceptance, n=5, zero variance. Consent carried in prose is consent
18
- // that can be re-summarized instead of acted on.
15
+ // message as advance sign-off for every gate"). Some models act on it. Others read the
16
+ // paragraph, read the gate list, and narrate the pipeline instead of running it. Consent
17
+ // carried in prose is consent that can be re-summarized instead of acted on.
19
18
  //
20
19
  // The organising rule of this project is that every invariant that matters lives in the runtime,
21
20
  // not in a prompt. Gate sign-off was the last big one still living in a prompt. So it becomes a
@@ -79,7 +78,7 @@ export const VALID_BY_GATE = {
79
78
  const CI_NOTE = "Pre-approved for a headless lane. No human is present; the decision is recorded here so the ledger still names a source.";
80
79
 
81
80
  export const PRESETS = {
82
- // Everything pre-approved. The lane a CI step or a benchmark runs in.
81
+ // Everything pre-approved. The lane a CI step runs in.
83
82
  ci: {
84
83
  version: 1,
85
84
  preset: "ci",
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // Hill Phase Derivation — phases are derived from facts, never authored
3
+ // Mechanical derivation of a scope's hill position based on facts (T0 verdicts, T1 evaluation, and discovery ledger).
4
+ // Writes to shapeup/<slug>/hill/<scope-id>.yml
5
+
6
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
7
+ import { resolve, join } from "node:path";
8
+ import { isMain } from "./lib/is-main.mjs";
9
+ import { runArgs } from "./lib/argv.mjs";
10
+ import { scopesDir, hillDir, verdictsDir, evaluationDir, discoveryLedger } from "./lib/paths.mjs";
11
+ import { readAllContracts, SCOPE_CONTRACT } from "./lib/contract-md.mjs";
12
+
13
+ /**
14
+ * Derive and write the hill phase for all scopes mechanically based on T0, T1, and ledger facts.
15
+ *
16
+ * The derived phase follows these progression rules (facts move dots, not authors):
17
+ * - UPHILL_UNKNOWN: open unknowns > 0 in the ledger for this scope
18
+ * - UPHILL_SOLVED: unknowns 0, no T0-green yet
19
+ * - DOWNHILL_EXECUTION: ≥1 T0-green; T1/seesaw pending
20
+ * - FINISHED: T1 PASS ∧ seesaw green
21
+ *
22
+ * @param {string} cwd - The project root directory.
23
+ * @param {string} slug - The feature slug being built.
24
+ * @returns {Array<{scope_id: string, phase: string, changed: boolean}>} A report of all scopes processed, their derived phase, and whether the hill shard on disk was modified.
25
+ * Side effects: writes to `shapeup/<slug>/hill/<scope-id>.yml` for each scope.
26
+ */
27
+ export function deriveHill(cwd, slug) {
28
+ const scopes = readAllContracts(scopesDir(cwd, slug), SCOPE_CONTRACT).map((x) => x.contract);
29
+ const vDir = verdictsDir(cwd, slug);
30
+ const evalDir = evaluationDir(cwd, slug);
31
+ const ledgerPath = discoveryLedger(cwd, slug);
32
+ const hDir = hillDir(cwd, slug);
33
+
34
+ if (!existsSync(hDir)) mkdirSync(hDir, { recursive: true });
35
+
36
+ // 1. Check if T1 Evaluation passed (spec-conformance === PASS for the most recent run)
37
+ let t1Pass = false;
38
+ const evalFile = join(evalDir, ".verdicts-run.jsonl");
39
+ if (existsSync(evalFile)) {
40
+ const lines = readFileSync(evalFile, "utf8").trim().split(/\n/).filter(Boolean);
41
+ let maxRun = 0;
42
+ for (const line of lines) {
43
+ try {
44
+ const parsed = JSON.parse(line);
45
+ if (parsed.run >= maxRun) {
46
+ maxRun = parsed.run;
47
+ if (parsed.dimension === "spec-conformance") {
48
+ t1Pass = (parsed.verdict === "PASS");
49
+ }
50
+ }
51
+ } catch (e) {
52
+ // ignore parse errors
53
+ }
54
+ }
55
+ }
56
+
57
+ // 2. T0 facts per scope: has it achieved a green overall verdict? was seesaw also green?
58
+ const t0Facts = {};
59
+ if (existsSync(vDir)) {
60
+ for (const f of readdirSync(vDir)) {
61
+ if (!f.endsWith(".json")) continue;
62
+ try {
63
+ const b = JSON.parse(readFileSync(join(vDir, f), "utf8"));
64
+ if (!t0Facts[b.scope_id]) t0Facts[b.scope_id] = { hasGreen: false, seesawGreen: false };
65
+ if (b.overall === "green") {
66
+ t0Facts[b.scope_id].hasGreen = true;
67
+ // If regression is false on a green T0, it means seesaw was also green
68
+ if (b.regression === false) {
69
+ t0Facts[b.scope_id].seesawGreen = true;
70
+ }
71
+ }
72
+ } catch (e) {
73
+ // ignore parse errors
74
+ }
75
+ }
76
+ }
77
+
78
+ // 3. Ledger unknowns per scope
79
+ const scopeUnknowns = {};
80
+ if (existsSync(ledgerPath)) {
81
+ const lines = readFileSync(ledgerPath, "utf8").split("\n");
82
+ let currentScope = null;
83
+ for (const line of lines) {
84
+ const m = line.match(/^## Discovered — .*?:([\w.-]+)-a\d+/);
85
+ if (m) {
86
+ currentScope = m[1];
87
+ }
88
+ if (currentScope && line.startsWith("~ ")) {
89
+ scopeUnknowns[currentScope] = (scopeUnknowns[currentScope] || 0) + 1;
90
+ }
91
+ }
92
+ }
93
+
94
+ const report = [];
95
+ for (const s of scopes) {
96
+ const id = s.scope_id;
97
+ const t0 = t0Facts[id] || { hasGreen: false, seesawGreen: false };
98
+ const unknowns = scopeUnknowns[id] || 0;
99
+
100
+ let phase = "UPHILL_UNKNOWN";
101
+ if (t1Pass && t0.hasGreen && t0.seesawGreen) {
102
+ phase = "FINISHED";
103
+ } else if (t0.hasGreen) {
104
+ phase = "DOWNHILL_EXECUTION";
105
+ } else if (unknowns === 0) {
106
+ phase = "UPHILL_SOLVED";
107
+ }
108
+
109
+ const yaml = `scope_id: ${id}\nphase: ${phase}\n`;
110
+ const out = join(hDir, `${id}.yml`);
111
+ let changed = false;
112
+ if (!existsSync(out) || readFileSync(out, "utf8") !== yaml) {
113
+ writeFileSync(out, yaml);
114
+ changed = true;
115
+ }
116
+ report.push({ scope_id: id, phase, changed });
117
+ }
118
+ return report;
119
+ }
120
+
121
+ export const ARGV_SPEC = {
122
+ usage: "hill-derive.mjs --slug <slug> [--cwd <dir>]",
123
+ _: { arity: 0, max: 0, name: "(no positional operands)" },
124
+ slug: { type: "str", required: true },
125
+ cwd: { type: "path" },
126
+ };
127
+
128
+ if (isMain(import.meta.url)) {
129
+ const args = runArgs(ARGV_SPEC);
130
+ const cwd = resolve(args.cwd || process.cwd());
131
+ const report = deriveHill(cwd, args.slug);
132
+ console.log(JSON.stringify(report, null, 2));
133
+ }
@@ -10,7 +10,6 @@
10
10
  // discoveries[] → append to .shapeup/<slug>/discovery/ledger.md (old P3.7 / QA H.3)
11
11
  // verdict.criteria[] → append evaluation/.verdicts-<target>.jsonl (old evaluator B.0)
12
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
13
  //
15
14
  // Zero dependencies, zero network, schema-validated input (a malformed result never mutates
16
15
  // the board). Single-writer becomes mechanically true, not aspirational.
@@ -108,21 +107,18 @@ export function updateBoardRow(indexBody, taskId, done) {
108
107
  /**
109
108
  * Apply one validated WorkResult to the working tree — the single-writer step (D6): ticks AC
110
109
  * 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[]).
110
+ * writes the verdict ledger + un-ticks refuted boxes.
111
+ * verdict{criteria[],refuted[]}).
114
112
  * @param {{cwd:string}} opts - cwd: working-directory root every LOCAL path resolves against.
115
113
  * @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.
114
+ * discoveries_appended:number, refuted_unticked:number, verdict_lines:number}} A summary of every write performed.
118
115
  * @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>/`.
116
+ * `evaluation/.verdicts-*.jsonl` under `.shapeup/<slug>/`.
121
117
  */
122
118
  export function applyResult(result, { cwd }) {
123
119
  const slug = result.order_id.split("/")[0];
124
120
  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 };
121
+ const summary = { slug, tasks_updated: [], acs_ticked: 0, unblocked: [], discoveries_appended: 0, refuted_unticked: 0, verdict_lines: 0 };
126
122
 
127
123
  // 1. Task results → task files + board (old task-executor P3.1/P3.2/P3.6).
128
124
  const boardIndex = join(local, "tasks", "_index.md");
@@ -228,14 +224,7 @@ export function applyResult(result, { cwd }) {
228
224
  }
229
225
  }
230
226
 
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
- }
227
+
239
228
 
240
229
  return summary;
241
230
  }
@@ -265,6 +254,5 @@ if (isMainModule) {
265
254
  process.exit(1);
266
255
  }
267
256
  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
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}`);
270
258
  }