shapeup-sdlc 1.7.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.example.json +5 -5
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +17 -3
- package/README.md +124 -103
- package/SECURITY.md +37 -30
- package/bin/init.mjs +77 -47
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +7 -6
- package/commands/ship.md +29 -16
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +3 -3
- package/hooks/gate-zerowork.mjs +25 -24
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +19 -8
- package/hooks/safety-spine.mjs +3 -3
- package/hooks/sandbox-guard.mjs +84 -40
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
- package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
- package/kernel/lib/paths.mjs +491 -0
- package/kernel/probe/concurrency.mjs +510 -0
- package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
- package/kernel/reduce/graph.mjs +437 -0
- package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
- package/kernel/reduce/ingest.mjs +633 -0
- package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
- package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
- package/kernel/report/export.mjs +325 -0
- package/kernel/report/facts.mjs +347 -0
- package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
- package/kernel/verify/dispatch.mjs +114 -0
- package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
- package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
- package/kernel/verify/skills.mjs +125 -0
- package/kernel/verify/spec.mjs +559 -0
- package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +11 -9
- package/skills/ba-pitch-analyzer/SKILL.md +5 -5
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
- package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
- package/skills/qa-edge-hunter/SKILL.md +7 -3
- package/skills/scope-architect/SKILL.md +40 -6
- package/skills/solution-architect/SKILL.md +19 -4
- package/skills/spec-evaluator/SKILL.md +12 -2
- package/skills/task-executor/SKILL.md +7 -6
- package/skills/tech-lead/SKILL.md +50 -38
- package/skills/tech-lead/references/gates.md +69 -20
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +341 -66
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +1 -1
- package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
- package/hooks/anti-rationalization.mjs +0 -238
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -147
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -108
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
- package/skills/spec-evaluator/README.md +0 -93
- package/skills/tech-lead/README.md +0 -71
- package/skills/tech-lead/references/delegation.md +0 -261
- package/skills/tech-lead/references/hard-rules.md +0 -34
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -213
- package/skills/tech-lead/references/round-protocol.md +0 -234
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
- package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
- package/skills/translator/README.md +0 -66
|
@@ -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();
|
package/hooks/gate-intake.mjs
CHANGED
|
@@ -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
|
|
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;
|
|
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
|
|
package/hooks/gate-zerowork.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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
|
|
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 "../
|
|
59
|
-
import { localDir, globLocal } from "../
|
|
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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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:
|
|
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 /\
|
|
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
|
|
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
|
|
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}/
|
|
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
|
|
255
|
-
"
|
|
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
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
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}/
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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}/
|
|
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": "
|
|
45
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
77
46
|
"hooks": [
|
|
78
47
|
{
|
|
79
48
|
"type": "command",
|
|
80
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/
|
|
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": "
|
|
57
|
+
"matcher": "Skill|Agent",
|
|
87
58
|
"hooks": [
|
|
88
59
|
{
|
|
89
60
|
"type": "command",
|
|
90
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/
|
|
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
|
}
|
package/hooks/lib/decision.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
24
|
-
//
|
|
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
|
-
// `
|
|
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 "../../
|
|
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 —
|
|
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
|
package/hooks/safety-spine.mjs
CHANGED
|
@@ -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
|
|
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 "../
|
|
37
|
-
import { LOCAL, safetyOverrides, metricsShard } from "../
|
|
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
|
|