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.
Files changed (98) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +17 -3
  4. package/README.md +124 -103
  5. package/SECURITY.md +37 -30
  6. package/bin/init.mjs +77 -47
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +7 -6
  9. package/commands/ship.md +29 -16
  10. package/commands/wire.md +1 -1
  11. package/hooks/dispatch-receipt.mjs +195 -0
  12. package/hooks/gate-intake.mjs +3 -3
  13. package/hooks/gate-zerowork.mjs +25 -24
  14. package/hooks/hooks.json +9 -48
  15. package/hooks/lib/decision.mjs +19 -8
  16. package/hooks/safety-spine.mjs +3 -3
  17. package/hooks/sandbox-guard.mjs +84 -40
  18. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
  19. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
  20. package/kernel/harness.mjs +134 -0
  21. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
  22. package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
  23. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  24. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
  25. package/kernel/lib/paths.mjs +491 -0
  26. package/kernel/probe/concurrency.mjs +510 -0
  27. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
  28. package/kernel/probe/eval.mjs +77 -0
  29. package/kernel/probe/leg.mjs +125 -0
  30. package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
  31. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
  32. package/kernel/probe/t0.mjs +66 -0
  33. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
  34. package/kernel/reduce/graph.mjs +437 -0
  35. package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
  36. package/kernel/reduce/ingest.mjs +633 -0
  37. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  38. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
  39. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
  40. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
  41. package/kernel/report/export.mjs +325 -0
  42. package/kernel/report/facts.mjs +347 -0
  43. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
  44. package/kernel/verify/dispatch.mjs +114 -0
  45. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  46. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  47. package/kernel/verify/skills.mjs +125 -0
  48. package/kernel/verify/spec.mjs +559 -0
  49. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
  50. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
  51. package/oracles/http-oracle.mjs +1 -1
  52. package/oracles/process-oracle.mjs +1 -1
  53. package/oracles/snapshot-oracle.mjs +1 -1
  54. package/oracles/test-oracle.mjs +1 -1
  55. package/package.json +11 -9
  56. package/skills/ba-pitch-analyzer/SKILL.md +5 -5
  57. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  58. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  59. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  60. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  61. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  62. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  63. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  64. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  65. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  66. package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
  67. package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
  68. package/skills/qa-edge-hunter/SKILL.md +7 -3
  69. package/skills/scope-architect/SKILL.md +40 -6
  70. package/skills/solution-architect/SKILL.md +19 -4
  71. package/skills/spec-evaluator/SKILL.md +12 -2
  72. package/skills/task-executor/SKILL.md +7 -6
  73. package/skills/tech-lead/SKILL.md +50 -38
  74. package/skills/tech-lead/references/gates.md +69 -20
  75. package/skills/tech-lead/references/protocol.md +832 -0
  76. package/skills/tech-lead/schemas/domain.schema.json +341 -66
  77. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  78. package/skills/tech-lead/schemas/work-result.schema.json +1 -1
  79. package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
  80. package/hooks/anti-rationalization.mjs +0 -238
  81. package/hooks/compact-snapshot.mjs +0 -47
  82. package/hooks/gate-deadline.mjs +0 -147
  83. package/hooks/gate-l2.mjs +0 -161
  84. package/hooks/session-rehydrate.mjs +0 -108
  85. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  86. package/skills/spec-evaluator/README.md +0 -93
  87. package/skills/tech-lead/README.md +0 -71
  88. package/skills/tech-lead/references/delegation.md +0 -261
  89. package/skills/tech-lead/references/hard-rules.md +0 -34
  90. package/skills/tech-lead/references/invocation.md +0 -45
  91. package/skills/tech-lead/references/ledger-schema.md +0 -213
  92. package/skills/tech-lead/references/round-protocol.md +0 -234
  93. package/skills/tech-lead/references/state-model.md +0 -66
  94. package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
  95. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
  96. package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
  97. package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
  98. package/skills/translator/README.md +0 -66
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ // Dispatch receipt — PostToolUse hook. The attestation that the SHIPPED skill ran.
3
+ //
4
+ // WHAT IT DEFENDS AGAINST, measured live rather than imagined. A worker dispatch that fails —
5
+ // plugin absent, disabled, or the wrong version loaded — comes back as `<tool_use_error>Unknown
6
+ // skill</tool_use_error>`, and the sub-agent then does the craft ITSELF from the prose in its own
7
+ // prompt. Everything downstream accepts the result: the artifacts are on disk, in exactly the right
8
+ // place, so the phase post-condition passes and the run advances. Both existing walls fire correctly
9
+ // and neither can help — the order gate validates the ORDER, the sandbox guard validates WHERE
10
+ // writes land, and an improvised write satisfies both. Nothing in the system attested WHICH SKILL
11
+ // produced an artifact, so a green run was consistent with zero shipped craft having been applied.
12
+ // That is not a failure, it is a FALSE GREEN, and it defeats "measured, not claimed" at the root:
13
+ // the measurement was "is the artifact on disk", which cannot distinguish skill-produced from
14
+ // improvised.
15
+ //
16
+ // WHY PostToolUse AND NOT PreToolUse. `PreToolUse` fires BEFORE the tool runs, so it cannot separate
17
+ // "the Skill returned" from "the Skill errored and the sub-agent improvised". It appears to catch the
18
+ // observed instance only by coupling — the plugin was not loaded, so these hooks were not registered
19
+ // either, so there was no receipt. The moment the environment is repaired, a dispatch that errors
20
+ // still gets a `PreToolUse` receipt written before it fails, and the fix expires exactly when the
21
+ // thing it guards starts working.
22
+ //
23
+ // WHAT WAS MEASURED, in a real session with the plugin loaded and a SUB-AGENT making the calls
24
+ // (every dispatch in this pipeline is a `Skill(...)` call made by a workflow leg, never by the main
25
+ // session, so a hook that is blind there is blind exactly where it is needed):
26
+ //
27
+ // skill resolves and completes → PostToolUse fires, tool_response = {success, commandName}
28
+ // skill name unknown → NO hook fires at all; the host rejects the name upstream
29
+ // order missing → gate denies → PreToolUse deny, then nothing; a denied call never runs
30
+ //
31
+ // So the discriminator is stronger than a status field: A FAILED DISPATCH LEAVES NO ROW. The mere
32
+ // existence of a matching receipt is the wall; `dispatch_ok` is corroboration on top of it, recorded
33
+ // because a future host that reports failures through this event must not be able to satisfy the
34
+ // wall by firing at all.
35
+ //
36
+ // WHAT IT WILL NOT ATTEST. A receipt is written only when the tool result NAMES the skill that ran
37
+ // (`tool_response.commandName`). An `Agent` dispatch whose prompt merely mentions `--order` gets no
38
+ // receipt: the prompt is a plan to dispatch, not evidence of one, and minting attestation from it
39
+ // would forge the exact fact this file exists to establish.
40
+ //
41
+ // EVERY WRITE IS INSIDE try/catch AND THIS HOOK NEVER DENIES (`lib/decision.mjs`). An unguarded
42
+ // `writeFileSync` in a hook body becomes `verdict:"error"` → exit 0 → the dispatch proceeds WITHOUT
43
+ // a receipt, and the phase then dies at ingest instead — a receipt channel that can break a run
44
+ // gets the whole layer disabled, which is the outcome this file exists to prevent.
45
+ //
46
+ // Contract: PostToolUse stdin JSON { tool_name, tool_input:{skill,args}, tool_response, cwd }.
47
+
48
+ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
49
+ import { resolve, dirname } from "node:path";
50
+ import { isMain } from "../kernel/lib/argv.mjs";
51
+ import { dispatchReceipts } from "../kernel/lib/paths.mjs";
52
+ import { runHook, readStdin, settle } from "./lib/decision.mjs";
53
+
54
+ /**
55
+ * The `--order` matcher, character-for-character the one the PreToolUse order gate uses
56
+ * (`kernel/verify/envelope.mjs`). Two dispatch hooks that disagree about what an order path IS
57
+ * would gate one set of calls and attest another.
58
+ */
59
+ export const ORDER_RE = /--order(?:\s+|=)(?:"([^"]+)"|'([^']+)'|(\S+))/;
60
+
61
+ /**
62
+ * Pull the order path out of a dispatch's tool input.
63
+ * @param {object} toolInput - The `tool_input` block from the hook payload.
64
+ * @returns {string|null} The path as written, or null when no `--order` was threaded.
65
+ */
66
+ export function orderPathFrom(toolInput) {
67
+ const haystack = [toolInput?.skill_args, toolInput?.args, toolInput?.prompt].filter(Boolean).join(" ");
68
+ const m = haystack.match(ORDER_RE);
69
+ return m ? (m[1] || m[2] || m[3]) : null;
70
+ }
71
+
72
+ /**
73
+ * Which skill actually ran, from the tool result.
74
+ *
75
+ * The host reports it NAMESPACED (`shapeup-sdlc-plugin:orient`) while a WorkOrder's `worker` field
76
+ * carries the bare skill name (`orient`), so the comparison the ingest gate performs is only
77
+ * meaningful against the segment after the last `:`.
78
+ *
79
+ * @param {object} toolResponse - The `tool_response` block from the hook payload.
80
+ * @returns {string|null} The bare skill name, or null when the result names no command — which is
81
+ * the case for every dispatch that did not resolve to a skill.
82
+ */
83
+ export function skillInvokedFrom(toolResponse) {
84
+ const name = toolResponse?.commandName;
85
+ if (typeof name !== "string" || !name) return null;
86
+ return name.slice(name.lastIndexOf(":") + 1) || null;
87
+ }
88
+
89
+ /**
90
+ * Build the receipt row for one dispatch.
91
+ *
92
+ * Exported so the suite can assert the row's SHAPE without a live session — the hook body is
93
+ * otherwise reachable only through stdin.
94
+ *
95
+ * @param {object} order - The parsed WorkOrder the dispatch was aimed by.
96
+ * @param {string} skillInvoked - The bare skill name the host reported running.
97
+ * @param {object} payload - The whole hook payload, for the sub-agent provenance fields.
98
+ * @returns {object} The row appended to the run's dispatch ledger.
99
+ */
100
+ export function receiptRow(order, skillInvoked, payload) {
101
+ return {
102
+ at: new Date().toISOString(),
103
+ order_id: order.order_id ?? null,
104
+ run_id: order.run_id ?? null,
105
+ worker_declared: order.worker ?? null,
106
+ skill_invoked: skillInvoked,
107
+ // Corroboration, not the wall — see the header. `success` is absent on hosts that do not
108
+ // report one, and an absent field must not read as a successful dispatch.
109
+ dispatch_ok: payload?.tool_response?.success === true,
110
+ tool: payload?.tool_name ?? null,
111
+ // Present when the dispatch came from a sub-agent, absent when the operator called it directly.
112
+ // That is the difference between an orchestrated leg and someone driving a skill by hand.
113
+ agent_id: payload?.agent_id ?? null,
114
+ agent_type: payload?.agent_type ?? null,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Append one receipt to the run's dispatch ledger. Never throws.
120
+ * @param {object} row - A {@link receiptRow}.
121
+ * @param {string} cwd - Project root the LOCAL root resolves against.
122
+ * @returns {string|null} The ledger path when the row reached disk, else null.
123
+ */
124
+ export function writeReceipt(row, cwd) {
125
+ try {
126
+ const slug = String(row.order_id).split("/")[0];
127
+ if (!slug) return null;
128
+ const path = dispatchReceipts(cwd, slug);
129
+ mkdirSync(dirname(path), { recursive: true });
130
+ appendFileSync(path, JSON.stringify(row) + "\n");
131
+ return path;
132
+ } catch { return null; }
133
+ }
134
+
135
+ /**
136
+ * The hook body — attest a completed dispatch, defer on everything else.
137
+ * @returns {Promise<void>} Settles through {@link runHook}, which always exits 0.
138
+ */
139
+ export async function main() {
140
+ await runHook("dispatch-receipt", async () => {
141
+ /**
142
+ * Permit with the reason on the record. This hook has no deny path at all.
143
+ *
144
+ * THE SKILL NAME TRAVELS ON EVERY ROW, including the defers, and that is load-bearing rather
145
+ * than decorative. A dispatch whose skill name does not resolve fires no hook at all — measured,
146
+ * not assumed — so a row naming a skill is itself proof that THIS SESSION resolved THAT skill.
147
+ * That is the one question a file-existence check cannot answer, and `verify dispatch` answers
148
+ * it by reading these rows. Without the name here, the evidence exists and is unattributable.
149
+ *
150
+ * @param {string} reason - Why no receipt was written.
151
+ * @param {string} [rule] - Which defer condition matched.
152
+ * @returns {never} Does not return — settles the hook.
153
+ */
154
+ const defer = (reason, rule) => settle({
155
+ verdict: "allow", event: "PostToolUse", tool: p?.tool_name ?? null, cwd: p?.cwd, reason, rule,
156
+ subject: p?.tool_input?.skill ?? null,
157
+ });
158
+ const raw = await readStdin();
159
+ let p;
160
+ try { p = JSON.parse(raw || "{}"); }
161
+ catch (e) { settle({ verdict: "error", event: "PostToolUse", reason: `unparseable payload: ${e.message}` }); }
162
+
163
+ if (p.tool_name !== "Skill" && p.tool_name !== "Agent") {
164
+ defer(`${p.tool_name ?? "no tool_name"} is not a dispatch tool — out of scope`, "not-a-dispatch");
165
+ }
166
+ const cited = orderPathFrom(p.tool_input);
167
+ if (!cited) defer("no --order threaded — not an orchestrated dispatch", "no-order");
168
+
169
+ const skillInvoked = skillInvokedFrom(p.tool_response);
170
+ if (!skillInvoked) {
171
+ // The `Agent` case, and any host result that does not name what ran. Attesting here would
172
+ // mint the fact rather than record it.
173
+ defer("dispatch result names no resolved skill — nothing to attest", "no-skill-named");
174
+ }
175
+
176
+ const cwd = p.cwd || process.cwd();
177
+ const orderPath = resolve(cwd, cited);
178
+ let order;
179
+ try { order = JSON.parse(readFileSync(orderPath, "utf8")); }
180
+ catch (e) { defer(`order not readable (${e.message}) — cannot attest a dispatch it cannot identify`, "order-unreadable"); }
181
+ if (!order?.order_id) defer("order carries no order_id — nothing to key a receipt by", "order-unkeyed");
182
+
183
+ const row = receiptRow(order, skillInvoked, p);
184
+ const written = writeReceipt(row, cwd);
185
+ return {
186
+ verdict: "allow", event: "PostToolUse", tool: p.tool_name, cwd, subject: row.order_id,
187
+ rule: written ? "receipt-written" : "receipt-write-failed",
188
+ reason: written
189
+ ? `dispatch receipt: ${row.order_id} ran ${skillInvoked} (declared ${row.worker_declared}, ok=${row.dispatch_ok})`
190
+ : `dispatch receipt could not be written for ${row.order_id} — ingest will refuse this result unless --no-receipt-check`,
191
+ };
192
+ });
193
+ }
194
+
195
+ if (isMain(import.meta.url)) main();
@@ -17,7 +17,7 @@
17
17
  // was living in a prompt, and a prompt is exactly what gets dropped on a hand-off. So it becomes a
18
18
  // hook, like GATE L2.
19
19
  //
20
- // Design mirrors gate-l2.mjs deliberately:
20
+ // Design mirrors the GATE L2 block deliberately:
21
21
  // • Scope — only `Skill` → tech-lead. Anything else defers instantly.
22
22
  // • Fail-CLOSED only on a provably empty intake, with an actionable re-invocation in the reason.
23
23
  // • Fail-OPEN on anything ambiguous (unparseable payload, unknown arg shape, an envelope
@@ -55,7 +55,7 @@ const args = String(p.tool_input?.skill_args ?? p.tool_input?.args ?? "");
55
55
  const skill = String(skillRaw).split(":").pop();
56
56
  if (skill !== "tech-lead") defer(`Skill(${skill}) is not the orchestrator — out of scope`);
57
57
 
58
- // The envelope port supplies its own intake; validate-envelope.mjs owns that path.
58
+ // The envelope port supplies its own intake; `harness verify envelope` owns that path.
59
59
  if (/--order\b/.test(args)) defer("envelope dispatch — validate-envelope owns this path", "--order");
60
60
 
61
61
  // Intake is satisfied by ANY of: a pitch path, a spec folder, or free requirement text.
@@ -73,7 +73,7 @@ const hasResume = /--from\s+\S/.test(args); // resuming an existing run has its
73
73
  // the spec.
74
74
  // Adding a valued flag anywhere in the harness means adding it here, and structural test §39
75
75
  // enforces exactly that against commands/ship.md.
76
- const VALUED_FLAGS = /--(pitch|spec|from|lens|rounds|attempts|orch-model|exec-model|eval-model|qa-model|feature|task|gate-answers|wall-clock-budget|slug|auto-level|max-rounds|intake-file|intake-text|spec-folder|cwd|out|by|preset|file|order)\s+\S+/g;
76
+ const VALUED_FLAGS = /--(pitch|spec|from|lens|rounds|attempts|parallel-scopes|orch-model|exec-model|eval-model|qa-model|feature|task|gate-answers|wall-clock-budget|slug|auto-level|max-rounds|intake-file|intake-text|spec-folder|cwd|out|by|preset|file|order)\s+\S+/g;
77
77
  const BARE_FLAGS = /--[a-z0-9-]+/g;
78
78
  const freeText = args.replace(VALUED_FLAGS, " ").replace(BARE_FLAGS, " ").trim();
79
79
 
@@ -13,7 +13,7 @@
13
13
  // That is the "agent claims done" pathology this project exists to prevent, reproduced by the
14
14
  // project, at its own front door.
15
15
  //
16
- // THE TWO STRUCTURAL MISSES IT EXPOSED. `anti-rationalization.mjs` is the guard for exactly
16
+ // THE TWO STRUCTURAL MISSES IT EXPOSED. the ship report's census is the guard for exactly
17
17
  // this class of failure, and it could not see this one for two independent reasons:
18
18
  //
19
19
  // 1. SCOPE. It defers unless a run is active (`activeSlug()` → `.shapeup/<slug>/`).
@@ -29,10 +29,10 @@
29
29
  //
30
30
  // the session dispatched the orchestrator AND the run left no receipt
31
31
  //
32
- // `init-run.mjs` writes that receipt as the orchestrator's first tool call. Its absence is the
32
+ // ``harness init run`` writes that receipt as the orchestrator's first tool call. Its absence is the
33
33
  // fact. Nothing here parses intent, so nothing here can be talked past.
34
34
  //
35
- // WHY THIS ONE BLOCKS, WHEN anti-rationalization DOES NOT. The invariant is "QA is a level-up,
35
+ // WHY THIS ONE BLOCKS, WHEN the ship report's census DOES NOT. The invariant is "QA is a level-up,
36
36
  // not a gate" — no second judge behind `spec-evaluator`. That governs quality JUDGMENTS. This
37
37
  // hook makes no judgment: it reports that no work exists to judge. Blocking is also uniquely
38
38
  // safe here, because a session with no artifacts has nothing to lose by continuing, and a
@@ -55,8 +55,8 @@
55
55
 
56
56
  import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
57
57
  import { join } from "node:path";
58
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
59
- import { localDir, globLocal } from "../skills/tech-lead/scripts/lib/paths.mjs";
58
+ import { isMain } from "../kernel/lib/argv.mjs";
59
+ import { localDir, globLocal } from "../kernel/lib/paths.mjs";
60
60
  import { runHook, readStdin, settle, decisionsPath } from "./lib/decision.mjs";
61
61
 
62
62
  const MAX_TRANSCRIPT_BYTES = 20 * 1024 * 1024;
@@ -74,11 +74,11 @@ const WORK_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "Bash"
74
74
  /**
75
75
  * Is this block a launch of the orchestrator's own workflow script, by either surface?
76
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.
77
+ * The shipped front door is `Workflow({scriptPath})`; `npx shapeup-sdlc init` writes the grant it
78
+ * needs. The Bash arm below is kept because a project that declined that grant, or one still on a
79
+ * v1 install, launches the same script through a Bash runtime a gate that knew only one surface
80
+ * would go blind on the lane those users actually run, which is the same "the emptier the failure,
81
+ * the less of it there is to detect" hole the banner above describes.
82
82
  *
83
83
  * Matched on the BASENAME, anchored. `CLAUDE_PLUGIN_ROOT` itself ends in `shapeup-sdlc-plugin/` on
84
84
  * a normal install — so a substring match on the whole path would count EVERY workflow script that
@@ -91,10 +91,10 @@ const WORK_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "Bash"
91
91
  function launchedShapeupWorkflow(block) {
92
92
  if (block.name === "Bash") {
93
93
  const cmd = String(block.input?.command ?? "");
94
- // Both halves required: the launcher AND an orchestrator script. `run-workflow.mjs` carrying
94
+ // Both halves required: a node launcher AND an orchestrator script. A launcher carrying
95
95
  // somebody else's workflow is not a harness dispatch, and neither is a bare mention of the
96
96
  // script in an unrelated command (`ls`, `cat`).
97
- return /\brun-workflow\.mjs\b/.test(cmd) && /[\\/]shapeup-[\w.-]*\.[cm]?js\b/.test(cmd);
97
+ return /\bnode\b/.test(cmd) && /[\\/]shapeup-[\w.-]*\.[cm]?js\b/.test(cmd);
98
98
  }
99
99
  if (block.name !== "Workflow") return false;
100
100
  const scriptPath = String(block.input?.scriptPath ?? "");
@@ -161,7 +161,7 @@ export function workCensus(events) {
161
161
  return census;
162
162
  }
163
163
 
164
- /** Any run receipt on disk, from any run. Written by init-run.mjs as the run's first act. */
164
+ /** Any run receipt on disk, from any run. Written by `harness init run` as the run's first act. */
165
165
  export function findReceipts(cwd) {
166
166
  const root = localDir(cwd);
167
167
  if (!existsSync(root)) return [];
@@ -236,7 +236,7 @@ export function buildReason({ narration, census, enforcement }) {
236
236
  : null,
237
237
  enforcement && enforcement.readable && enforcement.rows === 0
238
238
  ? "AND the enforcement layer left zero decision rows — the gates did not merely permit this run, they never ran. " +
239
- "Check the plugin install — a symlinked or spaced path is the usual cause; see lib/is-main.mjs."
239
+ "Check the plugin install — a symlinked path or a path with a space is the usual cause."
240
240
  : null,
241
241
  "",
242
242
  "A run that describes its own pipeline and stops is the exact failure this harness exists to",
@@ -247,20 +247,21 @@ export function buildReason({ narration, census, enforcement }) {
247
247
  "",
248
248
  " # write the requirement to a file first — inlining multi-line text into a shell",
249
249
  " # argument is where this step goes wrong",
250
- " node \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/init-run.mjs\" \\",
250
+ " node \"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs\" init run \\",
251
251
  " --slug <slug> --intake-file <path/to/requirement.md> \\",
252
252
  " --auto-level <interactive|auto|unattended> [--gate-answers <preset|path>]",
253
253
  "",
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:",
254
+ "Then launch the lane itself with the Workflow tool (`npx shapeup-sdlc init` writes the grant",
255
+ "it needs, unless --no-native-workflow was given, in which case approve the launch once):",
256
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>",
257
+ " Workflow({",
258
+ " scriptPath: \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js\",",
259
+ " args: <the RunArgs object>",
260
+ " })",
260
261
  "",
261
262
  "Resolve each gate the run pauses at with:",
262
263
  "",
263
- " node \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/gate-answers.mjs\" --resolve <gate-id> …",
264
+ " node \"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs\" gate --resolve <gate-id> …",
264
265
  "",
265
266
  "If the command comes back \"requires approval\", say so and stop — the harness's scripts ship with",
266
267
  "the plugin and need a one-time permission grant (`npx shapeup-sdlc init` writes it). Do NOT route",
@@ -294,7 +295,7 @@ async function main() {
294
295
  // not a harness session → not our business
295
296
  if (!dispatchedOrchestrator(events)) defer("session never dispatched the orchestrator — not a harness run", "no-dispatch");
296
297
 
297
- // A receipt means the run started. What happens after that is anti-rationalization's job and
298
+ // A receipt means the run started. What happens after that is the ship report's census's job and
298
299
  // the evaluator's; this hook only asks whether anything started at all.
299
300
  const receipts = findReceipts(cwd);
300
301
  if (receipts.length > 0) defer(`${receipts.length} run receipt(s) on disk — the run started`, "receipt-present");
@@ -315,9 +316,9 @@ async function main() {
315
316
  // was from the gate watching for exactly that.
316
317
  //
317
318
  // 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
+ // ``harness init run``, which writes the receipt — such a session already deferred at `receipt-present`
319
320
  // 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
+ // hook: ``harness init run`` ships in the same install, beside the `lib/` this file imports from.
321
322
  // Neither case needs an escape, so no replacement escape is added — an escape that cannot fire
322
323
  // is the "row that cannot fail" this project keeps catching.
323
324
  //
package/hooks/hooks.json CHANGED
@@ -8,27 +8,6 @@
8
8
  "command": "echo \"shapeup-sdlc-plugin loaded from ${CLAUDE_PLUGIN_ROOT}\""
9
9
  }
10
10
  ]
11
- },
12
- {
13
- "matcher": "startup|compact|resume|clear",
14
- "hooks": [
15
- {
16
- "type": "command",
17
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-rehydrate.mjs\"",
18
- "timeout": 10
19
- }
20
- ]
21
- }
22
- ],
23
- "PreCompact": [
24
- {
25
- "hooks": [
26
- {
27
- "type": "command",
28
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/compact-snapshot.mjs\"",
29
- "timeout": 10
30
- }
31
- ]
32
11
  }
33
12
  ],
34
13
  "PreToolUse": [
@@ -42,16 +21,6 @@
42
21
  }
43
22
  ]
44
23
  },
45
- {
46
- "matcher": "Skill",
47
- "hooks": [
48
- {
49
- "type": "command",
50
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gate-l2.mjs\"",
51
- "timeout": 10
52
- }
53
- ]
54
- },
55
24
  {
56
25
  "matcher": "Skill",
57
26
  "hooks": [
@@ -63,31 +32,33 @@
63
32
  ]
64
33
  },
65
34
  {
66
- "matcher": "Skill",
35
+ "matcher": "Skill|Agent",
67
36
  "hooks": [
68
37
  {
69
38
  "type": "command",
70
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gate-deadline.mjs\"",
39
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs\" verify envelope",
71
40
  "timeout": 10
72
41
  }
73
42
  ]
74
43
  },
75
44
  {
76
- "matcher": "Skill|Agent",
45
+ "matcher": "Edit|Write|MultiEdit",
77
46
  "hooks": [
78
47
  {
79
48
  "type": "command",
80
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/validate-envelope.mjs\"",
49
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/sandbox-guard.mjs\"",
81
50
  "timeout": 10
82
51
  }
83
52
  ]
84
- },
53
+ }
54
+ ],
55
+ "PostToolUse": [
85
56
  {
86
- "matcher": "Edit|Write|MultiEdit",
57
+ "matcher": "Skill|Agent",
87
58
  "hooks": [
88
59
  {
89
60
  "type": "command",
90
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/sandbox-guard.mjs\"",
61
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/dispatch-receipt.mjs\"",
91
62
  "timeout": 10
92
63
  }
93
64
  ]
@@ -100,16 +71,6 @@
100
71
  "type": "command",
101
72
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gate-zerowork.mjs\"",
102
73
  "timeout": 10
103
- },
104
- {
105
- "type": "command",
106
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/anti-rationalization.mjs\"",
107
- "timeout": 10
108
- },
109
- {
110
- "type": "command",
111
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/slop-cleaner.mjs\"",
112
- "timeout": 15
113
74
  }
114
75
  ]
115
76
  }
@@ -6,13 +6,11 @@
6
6
  // input, every gate in this repo answered the same way:
7
7
  //
8
8
  // $ echo 'NOT JSON AT ALL {{{' | node hooks/<gate>.mjs
9
- // gate-l2 exit=0 stdout_len=0
10
9
  // gate-zerowork exit=0 stdout_len=0
11
10
  // sandbox-guard exit=0 stdout_len=0
12
11
  // safety-spine exit=0 stdout_len=0
13
- // gate-deadline exit=0 stdout_len=0
14
12
  // gate-intake exit=0 stdout_len=0
15
- // validate-envelope exit=0 stdout_len=0
13
+ // verify envelope exit=0 stdout_len=0
16
14
  //
17
15
  // exit 0 + silence = allow. But that is ALSO what "inspected the board and deferred" looks like,
18
16
  // and what "no rule matched" looks like, and what a thrown exception looks like, and what an inert
@@ -20,8 +18,8 @@
20
18
  // orchestrator or auditor can tell them apart, which is how a whole enforcement layer can sit
21
19
  // inert while every one of its checks reports success.
22
20
  //
23
- // FAIL-OPEN IS RETAINED, DELIBERATELY. `gate-l2.mjs` argues for it correctly in its own header: a
24
- // gate that breaks legitimate or standalone runs just gets disabled, and a disabled gate enforces
21
+ // FAIL-OPEN IS RETAINED, DELIBERATELY, and every hook here argues for it in its own header: a gate
22
+ // that breaks legitimate or standalone runs just gets disabled, and a disabled gate enforces
25
23
  // nothing. The defect was never the direction. It is that `allow` carried NO EVIDENCE.
26
24
  //
27
25
  // THE PREDICATE IS ALREADY INVENTED IN THIS REPO. The structural suite calls its helper `spoke()`
@@ -38,7 +36,7 @@
38
36
  // committed metrics shard, which `stats --hooks` aggregates into.
39
37
  //
40
38
  // 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
39
+ // ``harness probe stats` --hooks` reads through. This file used to hardcode the pre-ADR-0001 root, so every
42
40
  // hook wrote its receipts to `.shapeup-sdlc/` while the only reader looked in `.shapeup/`:
43
41
  // `stats --hooks` reported zero hook activity on every project, which is indistinguishable from
44
42
  // the inert-enforcement-layer failure this file exists to make visible. A telemetry channel with
@@ -50,7 +48,8 @@
50
48
 
51
49
  import { appendFileSync, mkdirSync } from "node:fs";
52
50
  import { dirname } from "node:path";
53
- import { decisions } from "../../skills/tech-lead/scripts/lib/paths.mjs";
51
+ import { decisions } from "../../kernel/lib/paths.mjs";
52
+ import { resolveRunId } from "../../kernel/lib/paths.mjs";
54
53
 
55
54
  /**
56
55
  * Where the receipts land.
@@ -168,6 +167,18 @@ export async function runHook(name, fn) {
168
167
  at: new Date().toISOString(),
169
168
  hook: name,
170
169
  pid: process.pid,
170
+ // WHICH RUN THIS DECISION BELONGS TO — resolved from the active-scope pointer, best-effort.
171
+ //
172
+ // The ledger is checkout-wide by design (a hook frequently fires with no `<slug>` to file
173
+ // under), which is exactly why the row needs the key: without it, "the enforcement layer denied
174
+ // 4 writes" cannot be attributed to a run, so a denial rate cannot be compared between runs and
175
+ // an inert layer in ONE run is invisible inside a healthy checkout-wide total.
176
+ //
177
+ // `null` is a real answer, not a failure: a hook firing outside any run genuinely belongs to no
178
+ // run, and recording that is what lets the export tier partition ambient decisions from run
179
+ // ones. Resolution reads two small files and swallows every error — a receipt must never be
180
+ // able to fail a tool call.
181
+ run_id: (() => { try { return resolveRunId(d.cwd || process.cwd()); } catch { return null; } })(),
171
182
  event: d.event ?? null,
172
183
  tool: d.tool ?? null,
173
184
  subject: d.subject ?? null,
@@ -176,7 +187,7 @@ export async function runHook(name, fn) {
176
187
  rule: d.rule ?? null,
177
188
  }, d.cwd);
178
189
  // Deny/block/warn payloads are emitted by definition. `emit: true` covers the hooks whose whole
179
- // job is to SAY something on an allow — session-rehydrate's additionalContext, for instance — so
190
+ // job is to SAY something on an allow — an injected context hint, for instance — so
180
191
  // a permitting hook can still write to stdout without pretending to be a denial.
181
192
  //
182
193
  // WHY `warn` IS ITS OWN VERDICT (ADR-0001). An advisory gate permits the call, so the obvious
@@ -17,7 +17,7 @@
17
17
  // the model must never widen its own safety envelope (same principle
18
18
  // as sandbox-guard's active-scope pointer rule)
19
19
  //
20
- // Deliberately conservative, mirrors gate-l2/sandbox-guard: fail-OPEN on unparseable input
20
+ // Deliberately conservative, mirrors the GATE L2 block/sandbox-guard: fail-OPEN on unparseable input
21
21
  // or an unmatched command (precision over reach — a spine that broke `rm -rf ./build` would
22
22
  // just get disabled), fail-CLOSED the moment a rule provably matches.
23
23
  //
@@ -33,8 +33,8 @@
33
33
  import { readFileSync, existsSync } from "node:fs";
34
34
  import { resolve, join, basename } from "node:path";
35
35
  import { globToRegExp, logPathology } from "./sandbox-guard.mjs";
36
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
37
- import { LOCAL, safetyOverrides, metricsShard } from "../skills/tech-lead/scripts/lib/paths.mjs";
36
+ import { isMain } from "../kernel/lib/argv.mjs";
37
+ import { LOCAL, safetyOverrides, metricsShard } from "../kernel/lib/paths.mjs";
38
38
 
39
39
  import { runHook, readStdin, settle } from "./lib/decision.mjs";
40
40