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
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // GATE Z — ZERO-WORK. Blocking Stop hook. The detector for "the harness described itself".
3
3
  //
4
- // WHY THIS EXISTS (measured, not theorized).
4
+ // WHY THIS EXISTS (reproduced, not theorized).
5
5
  //
6
- // SDD harness benchmark, F2, Haiku 4.5, n=5, zero variance. The orchestrator was dispatched
7
- // with a valid spec and returned this, in full:
6
+ // Dispatched with a valid spec, the orchestrator returned this, in full:
8
7
  //
9
8
  // TOOL Skill(tech-lead, "--unattended --rounds 3\n\n# F2 — category budgets…")
10
9
  // TEXT "The tech-lead skill is orchestrating the full Shape Up harness. It will: 1. …"
@@ -50,9 +49,9 @@
50
49
  // SECOND CONDITION (v1.5). Since `hooks/lib/decision.mjs` gives every hook a receipt, this gate
51
50
  // gains a second, independent fact it can assert at `Stop`: the orchestrator was dispatched, and
52
51
  // `decisions.jsonl` holds ZERO rows for this pid — meaning the enforcement layer itself never ran.
53
- // That is F-16's class, not its instance: under a symlinked install every gate was inert while
54
- // every gate reported success. The detector for "the gates didn't run" now stops depending on the
55
- // gates running.
52
+ // That is a whole CLASS of failure, not one instance: under a symlinked install every gate can be
53
+ // inert while every gate reports success. The detector for "the gates didn't run" therefore stops
54
+ // depending on the gates running.
56
55
 
57
56
  import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
58
57
  import { join } from "node:path";
@@ -62,13 +61,65 @@ import { runHook, readStdin, settle, decisionsPath } from "./lib/decision.mjs";
62
61
 
63
62
  const MAX_TRANSCRIPT_BYTES = 20 * 1024 * 1024;
64
63
 
65
- /** Tool names that constitute doing something to the project, as opposed to looking at it. */
64
+ /**
65
+ * Tool names that constitute doing something to the project, as opposed to looking at it.
66
+ *
67
+ * The census these produce is now DESCRIPTIVE ONLY — it sharpens the block message and nothing
68
+ * decides on it. Until v1.7.1 a count above two was a fail-open ("the session did work by other
69
+ * means"), and it was retired for the reason spelled out at the `work-done` note below. `Workflow` stays absent because a launch is not work — it is the work this hook is
70
+ * asking about — and it is a DISPATCH signal instead (`dispatchedOrchestrator`).
71
+ */
66
72
  const WORK_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "Bash", "Task", "Agent"]);
67
73
 
68
- /** Did this session dispatch the orchestrator? Skill(tech-lead) in any of its surface spellings. */
74
+ /**
75
+ * Is this block a launch of the orchestrator's own workflow script, by either surface?
76
+ *
77
+ * TWO SURFACES, ONE INVARIANT. `Workflow({scriptPath})` is the tool form. The shipped
78
+ * front door is a Bash call — `node "…/scripts/run-workflow.mjs" "…/workflows/shapeup-run.js"` —
79
+ * because the tool form cannot be granted and is denied in every headless session. A gate that
80
+ * knew only the tool form would go blind on the lane users actually run, which is the same
81
+ * "the emptier the failure, the less of it there is to detect" hole the banner above describes.
82
+ *
83
+ * Matched on the BASENAME, anchored. `CLAUDE_PLUGIN_ROOT` itself ends in `shapeup-sdlc-plugin/` on
84
+ * a normal install — so a substring match on the whole path would count EVERY workflow script that
85
+ * happens to live under the plugin root, including one a user wrote for something else. The
86
+ * basename is the part the skill controls and the part `SKILL.md` names.
87
+ *
88
+ * @param {object} block - A `tool_use` content block.
89
+ * @returns {boolean} True when the block launches a `shapeup-*` workflow.
90
+ */
91
+ function launchedShapeupWorkflow(block) {
92
+ if (block.name === "Bash") {
93
+ const cmd = String(block.input?.command ?? "");
94
+ // Both halves required: the launcher AND an orchestrator script. `run-workflow.mjs` carrying
95
+ // somebody else's workflow is not a harness dispatch, and neither is a bare mention of the
96
+ // script in an unrelated command (`ls`, `cat`).
97
+ return /\brun-workflow\.mjs\b/.test(cmd) && /[\\/]shapeup-[\w.-]*\.[cm]?js\b/.test(cmd);
98
+ }
99
+ if (block.name !== "Workflow") return false;
100
+ const scriptPath = String(block.input?.scriptPath ?? "");
101
+ const base = scriptPath.split(/[\\/]/).pop().replace(/\.[cm]?js$/, "");
102
+ const named = String(block.input?.name ?? "");
103
+ return /^shapeup-/.test(base) || /^shapeup-/.test(named);
104
+ }
105
+
106
+ /**
107
+ * Did this session dispatch the orchestrator? Three surface spellings, all equivalent:
108
+ * `Skill(tech-lead)`, a leading `/ship` slash command, and — since the workflow cutover (v1.7) —
109
+ * a `Workflow` tool_use launching one of the orchestrator's own `shapeup-*` scripts.
110
+ *
111
+ * The third arm is a correctness repair, not a new detector. `SKILL.md`'s Step 2 makes the
112
+ * Workflow launch the scoped lane's front door, so a session can now reach the orchestrator
113
+ * without ever emitting `Skill(tech-lead)` — and before this arm existed such a session was
114
+ * invisible to the gate, exactly the "the emptier the failure, the less of it there is to detect"
115
+ * hole the banner above describes. It is a TRIGGER, never an escape: a launch that left no
116
+ * receipt is still a blocked stop, because a `Workflow` call that returned without starting a run
117
+ * is precisely the narration case wearing a tool call.
118
+ */
69
119
  export function dispatchedOrchestrator(events) {
70
120
  for (const ev of events) {
71
121
  for (const block of toolUses(ev)) {
122
+ if (launchedShapeupWorkflow(block)) return true;
72
123
  if (block.name !== "Skill") continue;
73
124
  const skill = String(block.input?.skill ?? block.input?.skill_name ?? "");
74
125
  if (skill.split(":").pop() === "tech-lead") return true;
@@ -178,13 +229,18 @@ export function buildReason({ narration, census, enforcement }) {
178
229
  `Mechanical facts: ${census.tool_calls} tool call(s), ${census.work_calls} of them work calls, ` +
179
230
  `${census.writes} file write(s), and no \`${globLocal("<slug>", "receipt.json")}\`.`,
180
231
  narration ? `The final message reads as a plan, not a result (matched: "${narration}").` : null,
232
+ census.work_calls > 2
233
+ ? `Those ${census.work_calls} work calls are why this block exists, not a reason to waive it: a busy ` +
234
+ "session used to switch this gate off. Work done AROUND the harness has no board, no T0 verdict and " +
235
+ "no receipt — a hand-built feature can pass its own oracle while the pipeline never ran."
236
+ : null,
181
237
  enforcement && enforcement.readable && enforcement.rows === 0
182
238
  ? "AND the enforcement layer left zero decision rows — the gates did not merely permit this run, they never ran. " +
183
- "Check the plugin install (a symlinked or spaced path was the measured cause; see lib/is-main.mjs)."
239
+ "Check the plugin install a symlinked or spaced path is the usual cause; see lib/is-main.mjs."
184
240
  : null,
185
241
  "",
186
242
  "A run that describes its own pipeline and stops is the exact failure this harness exists to",
187
- "prevent measured at 29% acceptance with 10 escaped defects while looking like a clean run.",
243
+ "prevent: it reads like a clean run and leaves escaped defects behind it.",
188
244
  "Loading the instructions is not running them.",
189
245
  "",
190
246
  "Do the work now, starting with the first step of the runbook:",
@@ -195,7 +251,14 @@ export function buildReason({ narration, census, enforcement }) {
195
251
  " --slug <slug> --intake-file <path/to/requirement.md> \\",
196
252
  " --auto-level <interactive|auto|unattended> [--gate-answers <preset|path>]",
197
253
  "",
198
- "Then proceed through the gates, resolving each one with:",
254
+ "Then launch the lane itself — a BACKGROUND Bash call, never the `Workflow` tool, which cannot be",
255
+ "granted and is denied outright in a headless session:",
256
+ "",
257
+ " node \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/run-workflow.mjs\" \\",
258
+ " \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js\" \\",
259
+ " --args-file <.shapeup/<slug>/run-args.json> --run-dir <.shapeup/<slug>/workflow-run>",
260
+ "",
261
+ "Resolve each gate the run pauses at with:",
199
262
  "",
200
263
  " node \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/gate-answers.mjs\" --resolve <gate-id> …",
201
264
  "",
@@ -238,11 +301,29 @@ async function main() {
238
301
 
239
302
  const census = workCensus(events);
240
303
 
241
- // Fail open when the session clearly did work by other means. A user may have run the harness
242
- // steps by hand, or be on a pre-receipt version of the plugin. Real narration has ~zero work
243
- // calls; this threshold keeps the hook off everything else.
244
- if (census.work_calls > 2) defer(`${census.work_calls} work calls the session did work by other means`, "work-done");
245
-
304
+ // ── The `work-done` fail-open is GONE, and its removal is the point.
305
+ //
306
+ // WHAT IT USED TO DO: `if (census.work_calls > 2) defer("the session did work by other means")`.
307
+ // The stated rationale was that a user may have run the harness steps by hand, or be on a
308
+ // pre-receipt version of the plugin.
309
+ //
310
+ // WHAT IT ACTUALLY DID, observed. `Skill(tech-lead)` was dispatched, the lane could not start,
311
+ // the agent hand-built the whole feature, and the hook ALLOWED the Stop on `"37 work calls — the
312
+ // session did work by other means"`. That session passed its own oracle and read like a clean
313
+ // run, with no board, no T0, no verdict and no receipt. A busy session was the one thing that
314
+ // could switch this gate off — so the harder the agent worked around the harness, the safer it
315
+ // was from the gate watching for exactly that.
316
+ //
317
+ // BOTH HALVES OF THE RATIONALE FAIL ON INSPECTION. Running the harness steps by hand starts with
318
+ // `init-run.mjs`, which writes the receipt — such a session already deferred at `receipt-present`
319
+ // two lines up and never reached here. And a pre-receipt plugin cannot be the one executing this
320
+ // hook: `init-run.mjs` ships in the same install, beside the `lib/` this file imports from.
321
+ // Neither case needs an escape, so no replacement escape is added — an escape that cannot fire
322
+ // is the "row that cannot fail" this project keeps catching.
323
+ //
324
+ // WHAT KEEPS THIS SAFE is not a threshold, it is the loop guard: `stop_hook_active` defers
325
+ // unconditionally, so a session that genuinely did the work outside the harness costs one extra
326
+ // turn and then stops. One nudge, never a hang. That is the trade this gate is worth.
246
327
  const message = typeof p.last_assistant_message === "string" ? p.last_assistant_message : "";
247
328
  const narration = detectNarration(message);
248
329
  const enforcement = enforcementCensus(cwd);
@@ -15,34 +15,42 @@
15
15
  // validate-envelope exit=0 stdout_len=0
16
16
  //
17
17
  // exit 0 + silence = allow. But that is ALSO what "inspected the board and deferred" looks like,
18
- // and what "no rule matched" looks like, and what a thrown exception looks like, and what F-16
19
- // looked like — a hook whose entire body silently never ran. Four states, one signature. No test,
20
- // orchestrator or auditor could tell them apart, which is how 26 enforcement points sat inert
21
- // behind 610 green checks while every one of them reported success.
18
+ // and what "no rule matched" looks like, and what a thrown exception looks like, and what an inert
19
+ // hook looks like — one whose entire body silently never ran. Four states, one signature. No test,
20
+ // orchestrator or auditor can tell them apart, which is how a whole enforcement layer can sit
21
+ // inert while every one of its checks reports success.
22
22
  //
23
23
  // FAIL-OPEN IS RETAINED, DELIBERATELY. `gate-l2.mjs` argues for it correctly in its own header: a
24
24
  // gate that breaks legitimate or standalone runs just gets disabled, and a disabled gate enforces
25
25
  // nothing. The defect was never the direction. It is that `allow` carried NO EVIDENCE.
26
26
  //
27
- // THE PREDICATE IS ALREADY INVENTED IN THIS REPO. `tests/structural/11-is-main.mjs` calls its
28
- // helper `spoke()` — did the script produce output? That is exactly the right question. It existed
29
- // only in the test harness, applied to entry points. This promotes it to runtime and applies it to
30
- // hooks, which closes F-16's whole CLASS rather than its instance:
27
+ // THE PREDICATE IS ALREADY INVENTED IN THIS REPO. The structural suite calls its helper `spoke()`
28
+ // — did the script produce output? That is exactly the right question. It existed only in the test
29
+ // harness, applied to entry points. This promotes it to runtime and applies it to
30
+ // hooks, which closes the whole CLASS rather than one instance of it:
31
31
  //
32
32
  // inspected-and-permitted · no-rule-matched · threw · never ran
33
33
  //
34
- // all four become distinguishable facts in `.shapeup-sdlc/decisions.jsonl`.
34
+ // all four become distinguishable facts in the decisions ledger.
35
35
  //
36
36
  // TIER: LOCAL, and checkout-wide rather than per-slug — hooks fire outside any run, so there is
37
37
  // frequently no `<slug>` to file under. Pure run-trace; the durable cross-machine record is the
38
38
  // committed metrics shard, which `stats --hooks` aggregates into.
39
39
  //
40
+ // THE PATH IS RESOLVED, NEVER SPELLED. It comes from `lib/paths.mjs` — the same resolver
41
+ // `stats.mjs --hooks` reads through. This file used to hardcode the pre-ADR-0001 root, so every
42
+ // hook wrote its receipts to `.shapeup-sdlc/` while the only reader looked in `.shapeup/`:
43
+ // `stats --hooks` reported zero hook activity on every project, which is indistinguishable from
44
+ // the inert-enforcement-layer failure this file exists to make visible. A telemetry channel with
45
+ // a hardcoded root is a telemetry channel with a silent disconnect in it.
46
+ //
40
47
  // THE RECEIPT IS BEST-EFFORT BY DESIGN. An unwritable `decisions.jsonl` must never turn into a
41
48
  // failed tool call: a receipt that can break a run would get the whole layer disabled, which is
42
49
  // the exact outcome this file exists to prevent. Every write here is inside a try/catch.
43
50
 
44
51
  import { appendFileSync, mkdirSync } from "node:fs";
45
- import { join, dirname } from "node:path";
52
+ import { dirname } from "node:path";
53
+ import { decisions } from "../../skills/tech-lead/scripts/lib/paths.mjs";
46
54
 
47
55
  /**
48
56
  * Where the receipts land.
@@ -54,11 +62,11 @@ import { join, dirname } from "node:path";
54
62
  * contaminates is not an instrument.
55
63
  *
56
64
  * @param {string} [cwd] - Project root; defaults to the process cwd.
57
- * @returns {string} The ledger path — `SHAPEUP_DECISIONS_PATH` when set, else
58
- * `<cwd>/.shapeup-sdlc/decisions.jsonl`.
65
+ * @returns {string} The ledger path — `SHAPEUP_DECISIONS_PATH` when set, else the LOCAL root's
66
+ * `decisions.jsonl`, resolved through `lib/paths.mjs`.
59
67
  */
60
68
  export function decisionsPath(cwd) {
61
- return process.env.SHAPEUP_DECISIONS_PATH || join(cwd || process.cwd(), ".shapeup-sdlc", "decisions.jsonl");
69
+ return process.env.SHAPEUP_DECISIONS_PATH || decisions(cwd || process.cwd());
62
70
  }
63
71
 
64
72
  /**
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Safety spine — PreToolUse hook (v1.2, absorb-audit P1).
2
+ // Safety spine — PreToolUse hook (v1.2).
3
3
  //
4
4
  // The three older hooks guard the PIPELINE (gate order, envelopes, substrates); this one
5
5
  // guards the MACHINE and the git remote — the substrate everything else sits on. It denies
@@ -1,26 +1,42 @@
1
1
  #!/usr/bin/env node
2
- // Sandbox guard — PreToolUse hook (design spec v1.1 §4.5/Blueprint E, PA3 countermeasure).
2
+ // Sandbox guard — PreToolUse hook (the PA3 countermeasure: writes outside the order's substrate).
3
3
  //
4
- // Blocks Edit/Write/MultiEdit calls that touch a file outside the active scope's
5
- // `allowed_file_substrate` (+ declared `shared_substrate`). Turns "generator only edits its
6
- // own scope" from prose into a precondition the model cannot talk past — the same pattern as
7
- // hooks/gate-l2.mjs for GATE L2.
4
+ // Blocks Edit/Write/MultiEdit calls that the ACTIVE ORDER's own `substrate` block does not
5
+ // permit. Turns "a worker only writes what its order authorised" from prose into a precondition
6
+ // the model cannot talk past.
7
+ //
8
+ // IT ENFORCES THE ORDER, NOT THE SCOPE CONTRACT, and that is the whole design. `compile-order.mjs`
9
+ // already stamps a write contract onto every order from `substrateFor(operation)` — allowed,
10
+ // shared, append_only, frozen. Resolving the scope contract instead covered exactly one operation,
11
+ // the build, because only build orders carry a scope; every other dispatch (`analyze`, `wire`,
12
+ // `evaluate`, `hunt`, `coach` …) ran unfenced, and the `frozen`/`append_only` surfaces the
13
+ // compiler emits had no enforcer at all. Reading the order makes the contract the compiler writes
14
+ // and the contract the hook enforces the same object, for every operation, with no per-operation
15
+ // code here.
16
+ //
17
+ // It finds the order through `.shapeup/active-order`, which `compile-order.mjs` publishes as it
18
+ // writes the order (and the workflow script re-points explicitly before each dispatch). Both
19
+ // authors matter: the compiler's write is what fences the lanes that never reach the workflow —
20
+ // `--tiny`, the prose round loop, a standalone `/build`.
8
21
  //
9
22
  // Design (deliberately conservative, mirrors gate-l2.mjs):
10
- // • Fail-OPEN whenever there is nothing to enforce: no active-scope pointer (not running
11
- // inside a scoped harness round), pointer names a scope contract that doesn't exist or is
12
- // unparsable, or the tool call carries no resolvable file path. A guard that breaks
13
- // legitimate non-harness edits would just get disabled.
14
- // • Fail-CLOSED the moment an active scope IS declared and the target path matches none of
15
- // its globs — deny, naming the substrate so the model can self-correct.
23
+ // • Fail-OPEN whenever there is nothing to enforce: no active-order pointer (not running inside
24
+ // a harness dispatch), pointer names an order that doesn't exist or is unparsable, the order
25
+ // declares no boundaries at all, or the tool call carries no resolvable file path. A guard
26
+ // that breaks legitimate non-harness edits would just get disabled.
27
+ // • Fail-CLOSED the moment an order IS live and the target is outside what it permits — deny,
28
+ // naming the reason so the model can self-correct. `frozen` outranks everything, including
29
+ // an `allowed` glob that would otherwise match; `append_only` permits Edit and denies Write,
30
+ // because Write overwrites what the append was supposed to preserve.
16
31
  // • Run-trace carve-out — writes under the ACTIVE feature's LOCAL gitignored root
17
32
  // (`.shapeup/<slug>/`) are always allowed: that root is harness bookkeeping the doer
18
33
  // is REQUIRED to write (task-executor P3 status/AC ticks + tasks/_index.md, run-state,
19
34
  // execution logs, the P3.7 discovery ledger). Substrate globs whitelist product code and
20
- // never list the run-trace, so without the carve-out every scoped round strands its own
21
- // board (island-escape shipped 16/20 task files stale this way). Deliberately narrow:
22
- // only the active slug's root — `.shapeup/active-scope` (this guard's own pointer)
23
- // and other features' roots remain subject to the substrate whitelist.
35
+ // never list the run-trace, so without the carve-out a scoped round leaves its own task files
36
+ // stale. Deliberately narrow: only the active slug's root. The pointers at the `.shapeup/`
37
+ // root — `active-order` (this guard's own) and `active-scope` — sit OUTSIDE the carve-out by
38
+ // construction, so a worker cannot widen its own sandbox by rewriting the thing that defines
39
+ // it.
24
40
  // • Every denial is also appended to the metrics pathology log (telemetry, not just defense).
25
41
  //
26
42
  // Contract: PreToolUse stdin JSON { tool_name, tool_input:{file_path | edits[].file_path}, cwd }.
@@ -29,8 +45,7 @@
29
45
  import { readFileSync, existsSync, appendFileSync, mkdirSync } from "node:fs";
30
46
  import { resolve, join, relative, dirname, sep } from "node:path";
31
47
  import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
32
- import { LOCAL, activeScope, scopeContract, metricsShard } from "../skills/tech-lead/scripts/lib/paths.mjs";
33
- import { readContract, SCOPE_CONTRACT } from "../skills/tech-lead/scripts/lib/contract-md.mjs";
48
+ import { LOCAL, activeOrder, metricsShard } from "../skills/tech-lead/scripts/lib/paths.mjs";
34
49
  import { runHook, readStdin, settle } from "./lib/decision.mjs";
35
50
 
36
51
  // --- tiny glob matcher: supports *, **, ? — enough for substrate globs, zero dependencies ---
@@ -97,46 +112,67 @@ async function main() {
97
112
  }
98
113
 
99
114
  const cwd = p.cwd || process.cwd();
100
- const activeScopePath = activeScope(cwd);
101
- // no harness round in progress don't enforce
102
- if (!existsSync(activeScopePath)) defer("no active-scope pointer — no harness round in progress", "no-round");
103
-
104
- const active = readJSON(activeScopePath);
105
- if (!active?.slug || !active?.scope_id) defer("active-scope pointer is unreadable or incomplete", "bad-pointer");
106
-
107
- // Markdown first, legacy JSON second (ADR-0001) a project mid-migration must stay sandboxed.
108
- let found = null;
109
- try { found = readContract(scopeContract(cwd, active.slug, active.scope_id), SCOPE_CONTRACT); }
110
- catch (e) { defer(`scope contract is unparseable (${e.message})`, "bad-contract"); }
111
- // pointer stale / contract not committed yet → don't break the run
112
- if (!found) defer(`no contract for ${active.scope_id} — pointer stale or not committed yet`, "no-contract");
113
- const contract = found.contract;
114
- if (!contract) defer("scope contract is unparseable", "bad-contract");
115
-
116
- const allowed = [...(contract.allowed_file_substrate || []), ...(contract.shared_substrate || [])];
117
- // no whitelist declared nothing to enforce
118
- if (allowed.length === 0) defer(`scope ${active.scope_id} declares no write whitelist`, "no-whitelist");
115
+ const activeOrderPath = activeOrder(cwd);
116
+ if (!existsSync(activeOrderPath)) defer("no active-order pointer no tracked task running", "no-round");
117
+
118
+ const active = readJSON(activeOrderPath);
119
+ if (!active?.slug || !active?.order_path) defer("active-order pointer is unreadable or incomplete", "bad-pointer");
120
+
121
+ const orderPathAbs = resolve(cwd, active.order_path);
122
+ if (!existsSync(orderPathAbs)) defer(`no order found at ${active.order_path}`, "no-order");
123
+
124
+ const order = readJSON(orderPathAbs);
125
+ if (!order?.substrate) defer("active order has no substrate block", "no-substrate");
126
+
127
+ const contract = order.substrate;
128
+ const allowed = [...(contract.allowed || []), ...(contract.shared || [])];
129
+ const appendOnly = contract.append_only || [];
130
+ const frozen = contract.frozen || [];
131
+
132
+ if (allowed.length === 0 && appendOnly.length === 0 && frozen.length === 0) {
133
+ defer(`order declares no write/append/frozen boundaries`, "no-whitelist");
134
+ }
119
135
 
120
136
  const targetPaths = extractPaths(p.tool_input);
121
137
  if (targetPaths.length === 0) defer("no writable path in the tool input", "no-target");
122
138
 
123
139
  const metricsPath = metricsShard(cwd);
124
- // Run-trace carve-out (see header): the active feature's LOCAL root only. The prefix ends
125
- // with a separator so a sibling `<local>/<slug>-other/` can't ride along, and the active-scope
126
- // pointer sits outside it by construction.
127
140
  const runTracePrefix = join(LOCAL, active.slug) + sep;
128
141
  const violations = [];
142
+ const blockReasons = [];
143
+
129
144
  for (const raw of targetPaths) {
130
145
  const abs = resolve(cwd, raw);
131
146
  const rel = relative(cwd, abs);
132
147
  if (rel.startsWith(runTracePrefix)) continue;
133
- if (!matchesAny(rel, allowed)) violations.push(rel);
148
+
149
+ // frozen takes absolute precedence
150
+ if (matchesAny(rel, frozen)) {
151
+ violations.push(rel);
152
+ blockReasons.push(`${rel} is frozen`);
153
+ continue;
154
+ }
155
+
156
+ if (matchesAny(rel, allowed)) {
157
+ continue; // OK
158
+ }
159
+
160
+ if (matchesAny(rel, appendOnly)) {
161
+ if (p.tool_name === "Write") {
162
+ violations.push(rel);
163
+ blockReasons.push(`${rel} is append-only (Write overwrites, use Edit)`);
164
+ }
165
+ continue;
166
+ }
167
+
168
+ violations.push(rel);
169
+ blockReasons.push(`${rel} is outside allowed scopes`);
134
170
  }
135
171
 
136
172
  // Inside the substrate — the "inspected and permitted" row. Previously byte-identical to
137
173
  // "this hook never ran", which is how 26 enforcement points sat inert behind 610 green checks.
138
174
  if (violations.length === 0) {
139
- defer(`${targetPaths.length} path(s) inside scope ${active.scope_id} substrate — permitted`, "in-substrate");
175
+ defer(`${targetPaths.length} path(s) inside order substrate — permitted`, "in-substrate");
140
176
  }
141
177
 
142
178
  logPathology(metricsPath, {
@@ -144,23 +180,23 @@ async function main() {
144
180
  at: new Date().toISOString(),
145
181
  kind: "pathology",
146
182
  pathology: "PA3",
147
- scope_id: active.scope_id,
183
+ order: active.order_path,
148
184
  slug: active.slug,
149
185
  blocked_paths: violations,
150
186
  });
151
187
 
152
188
  return {
153
- verdict: "deny", event: "PreToolUse", tool: p.tool_name, subject: active.scope_id, cwd,
189
+ verdict: "deny", event: "PreToolUse", tool: p.tool_name, subject: active.order_path, cwd,
154
190
  rule: "outside-substrate",
155
- reason: `${violations.length} write(s) outside the scope substrate: ${violations.join(", ")}`,
191
+ reason: `${violations.length} write(s) rejected by substrate boundaries: ${blockReasons.join("; ")}`,
156
192
  payload: {
157
193
  hookSpecificOutput: {
158
194
  hookEventName: "PreToolUse",
159
195
  permissionDecision: "deny",
160
196
  permissionDecisionReason:
161
- `Sandbox guard (PA3) — scope "${active.scope_id}" may only write ${JSON.stringify(allowed)}. ` +
162
- `Blocked: ${violations.join(", ")}. If this write legitimately crosses scopes, add the path to ` +
163
- `the contract's shared_substrate (via ba --remap) rather than editing outside the substrate.`,
197
+ `Sandbox guard (PA3) — active order substrate blocked these writes:\n` +
198
+ `${blockReasons.join("\n")}\n` +
199
+ `If this write legitimately crosses scopes, the order's substrate needs to be expanded (e.g. via ba --remap).`,
164
200
  },
165
201
  },
166
202
  };
@@ -16,13 +16,12 @@
16
16
  // `SessionStart:startup`, and the reflex whose entire purpose is "trust the files, not your
17
17
  // memory" did not fire in the one case where there IS no memory to distrust.
18
18
  //
19
- // The SDD harness benchmark measured the cost. Its F4 handoff design is exactly this scenario a
20
- // fresh `claude -p` in a workspace where a prior session was cut mid-build and across three
21
- // Sonnet rows every one recorded `hooks_fired: ["SessionStart:startup"]` (the plugin's load echo,
22
- // not this hook). With no pointer to the run in flight, the orchestrator re-entered at phase 1:
23
- // 82–120 turns before its first write, $4.57–$10.36 for the session, and **0/3 of the gap closed**
24
- // while the run receipt and board sat on disk the whole time. One row reached GATE L4 — ship
25
- // sign-off — having advanced the deliverable by zero criteria.
19
+ // The cost is concrete. A fresh session in a workspace where a prior one was cut mid-build sees
20
+ // only `SessionStart:startup` the plugin's load echo, not this hook. With no pointer to the run
21
+ // in flight, the orchestrator re-enters at phase 1 and spends its whole budget rebuilding a
22
+ // pipeline that is already on disk, closing none of the gap, while the run receipt and board sit
23
+ // there the entire time. A session can reach GATE L4 ship sign-off having advanced the
24
+ // deliverable by zero criteria.
26
25
  //
27
26
  // Firing on `startup` is free when there is nothing to say: `findRun` returns a run only for an
28
27
  // `active-scope` pointer or a `harness-run.md` whose status is mid-run, so an ordinary session in a
@@ -50,8 +49,8 @@ import { activeScope } from "../skills/tech-lead/scripts/lib/paths.mjs";
50
49
  //
51
50
  // compact / resume — the risk is acting on a lossy summary of work you remember doing.
52
51
  // startup / clear — the risk is not knowing the run exists, and OPENING IT AGAIN. That is the
53
- // failure the benchmark measured: a fresh session re-entered at phase 1 and
54
- // spent 82–120 turns rebuilding a pipeline that was already on disk.
52
+ // failure that actually happens: a fresh session re-enters at phase 1 and
53
+ // spends its budget rebuilding a pipeline that was already on disk.
55
54
  //
56
55
  // Naming the right failure is the whole value of the injection. A generic pointer to the files is
57
56
  // what a competent agent finds anyway; "there is a run open, do not re-open it" is not.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Slop cleaner — advisory Stop hook (v1.2, absorb-audit P2).
2
+ // Slop cleaner — advisory Stop hook (v1.2).
3
3
  //
4
4
  // Scans what actually changed this session (git diff of the working tree; fallback: the
5
5
  // newest WorkResult's files_touched) for the classic leftovers — TODO/FIXME markers,
@@ -1,4 +1,4 @@
1
- // Shared helpers for evaluation-contract oracle runners (Stage G of the audit).
1
+ // Shared helpers for evaluation-contract oracle runners.
2
2
  //
3
3
  // The `process` oracle (step 1–3) inlined these; the test/snapshot/http oracles (steps 4–5)
4
4
  // import them so the matching grammar is identical across the registry. Keeping one definition
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // `http` oracle for the evaluation contract (Stage G, step 5 of the audit).
2
+ // `http` oracle for the evaluation contract.
3
3
  //
4
4
  // Deliverable: a service / API. The oracle starts the server, waits until it is reachable,
5
5
  // sends each criterion's request, and grades the OBSERVED response (status + body) — never the
package/oracles/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Oracle registry for the evaluation contract (Stage G of the audit).
1
+ // Oracle registry for the evaluation contract.
2
2
  //
3
3
  // The single entry point `spec-evaluator` dispatches through when a criterion / Test-Surface row
4
4
  // carries an `oracle:` tag. Adding a new oracle type = registering one runner here (the spec's
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Shared `process` oracle for the evaluation contract (Stage G of the audit).
2
+ // Shared `process` oracle for the evaluation contract.
3
3
  //
4
4
  // This is the promoted, parameterized form of the todo-cli prototype: instead of hard-coding
5
5
  // criteria, it reads a *declarative contract* (a list of criteria, each with a `probe` and an
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // `snapshot` oracle for the evaluation contract (Stage G, step 4b of the audit).
2
+ // `snapshot` oracle for the evaluation contract.
3
3
  //
4
4
  // Deliverable: a generator or a pure refactor — something whose acceptance is "produces exactly
5
5
  // this output". The oracle runs the deliverable, captures its stdout, and diffs it against a
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // `test` oracle for the evaluation contract (Stage G, step 4a of the audit).
2
+ // `test` oracle for the evaluation contract.
3
3
  //
4
4
  // Deliverable: a library / module whose acceptance is "its own test suite is green".
5
5
  // The oracle runs the project's test command and grades the OBSERVED suite result —
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "shapeup-sdlc",
3
- "version": "1.6.2",
4
- "description": "Shape Up for coding agents with gates the agent can't talk its way past. Harness for Claude Code, compiled to Cursor, Antigravity and Codex.",
3
+ "version": "1.7.0",
4
+ "description": "Shape Up for coding agents \u2014 with gates the agent can't talk its way past. Harness for Claude Code.",
5
5
  "bin": {
6
6
  "shapeup-sdlc": "bin/init.mjs"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
10
10
  "skills/",
11
- "!skills/**/evals/**",
12
11
  "hooks/",
13
12
  "commands/",
14
13
  "oracles/",
@@ -34,14 +33,11 @@
34
33
  },
35
34
  "scripts": {
36
35
  "test": "node tests/structural.mjs",
37
- "demo": "node tools/demo/record-demo.mjs",
38
- "distribute": "node tools/distribute.js"
36
+ "demo": "node tools/demo/record-demo.mjs"
39
37
  },
40
38
  "keywords": [
41
39
  "shapeup",
42
40
  "sdlc",
43
- "cursor",
44
- "antigravity",
45
41
  "claude-code"
46
42
  ],
47
43
  "author": "Liberty Nguyen",