shapeup-sdlc 1.6.3 → 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 +54 -107
- package/README.md +165 -151
- package/SECURITY.md +49 -27
- package/bin/init.mjs +93 -108
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +20 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +53 -7
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +16 -17
- package/hooks/gate-zerowork.mjs +107 -25
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +38 -19
- package/hooks/safety-spine.mjs +4 -4
- package/hooks/sandbox-guard.mjs +130 -50
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
- package/kernel/init/run.mjs +489 -0
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
- 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} +10 -11
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/kernel/probe/resume.mjs +528 -0
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
- package/kernel/reduce/graph.mjs +437 -0
- package/kernel/reduce/hill.mjs +152 -0
- 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} +93 -14
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
- 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} +22 -25
- 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} +138 -28
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +2 -2
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +2 -2
- package/oracles/snapshot-oracle.mjs +2 -2
- package/oracles/test-oracle.mjs +2 -2
- package/package.json +11 -13
- package/skills/ba-pitch-analyzer/SKILL.md +24 -19
- 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 +3 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +15 -41
- package/skills/scope-architect/SKILL.md +57 -18
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +37 -15
- package/skills/spec-evaluator/SKILL.md +24 -7
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/task-executor/SKILL.md +32 -18
- package/skills/tech-lead/SKILL.md +127 -438
- package/skills/tech-lead/references/gates.md +140 -49
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +645 -198
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +56 -18
- package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
- package/skills/translator/SKILL.md +2 -2
- package/hooks/anti-rationalization.mjs +0 -244
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -151
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -109
- package/skills/advisor-protocol/SKILL.md +0 -171
- 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 -254
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -214
- package/skills/tech-lead/references/round-protocol.md +0 -184
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
- package/skills/tech-lead/scripts/init-run.mjs +0 -326
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
- 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
|
@@ -3,23 +3,21 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Denies a `tech-lead` dispatch that carries no pitch, no spec folder, and no requirement text.
|
|
5
5
|
//
|
|
6
|
-
// WHY THIS EXISTS (
|
|
7
|
-
// (`sdd-harness-bench`, feature F2, Haiku 4.5, n=3, zero variance) the orchestrator was reached as
|
|
6
|
+
// WHY THIS EXISTS (reproduced, not theorized). The orchestrator was reached as
|
|
8
7
|
//
|
|
9
8
|
// Skill(tech-lead, args: "--unattended")
|
|
10
9
|
//
|
|
11
10
|
// — the flag survived the hand-off, the requirement text did not. With nothing to orchestrate,
|
|
12
|
-
// the run printed
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// cheaper one did not, three times out of three.
|
|
11
|
+
// the run printed the gate names, a confident plan, and wrote no code, while *looking* like a
|
|
12
|
+
// successful run. The same harness invoked with --pitch/--spec built the feature. Whether a model
|
|
13
|
+
// happens to inline the text and recover is a property of the model, not of the harness.
|
|
16
14
|
//
|
|
17
15
|
// That is the "agent claims done" failure this project exists to prevent, happening at the
|
|
18
16
|
// project's own front door. Every other invariant that matters here lives in the runtime; this one
|
|
19
17
|
// was living in a prompt, and a prompt is exactly what gets dropped on a hand-off. So it becomes a
|
|
20
18
|
// hook, like GATE L2.
|
|
21
19
|
//
|
|
22
|
-
// Design mirrors
|
|
20
|
+
// Design mirrors the GATE L2 block deliberately:
|
|
23
21
|
// • Scope — only `Skill` → tech-lead. Anything else defers instantly.
|
|
24
22
|
// • Fail-CLOSED only on a provably empty intake, with an actionable re-invocation in the reason.
|
|
25
23
|
// • Fail-OPEN on anything ambiguous (unparseable payload, unknown arg shape, an envelope
|
|
@@ -29,10 +27,10 @@
|
|
|
29
27
|
// Contract: PreToolUse stdin JSON { tool_name, tool_input:{skill_name|skill, skill_args|args}, ... }
|
|
30
28
|
// Deny via { hookSpecificOutput: { hookEventName, permissionDecision:"deny", permissionDecisionReason } }.
|
|
31
29
|
|
|
32
|
-
// RECEIPTS (v1.5). This gate
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// RECEIPTS (v1.5). This gate has been scored `No effect` on a re-run where the acceptance rate did
|
|
31
|
+
// not move — because the hook CORRECTLY never fired and the cause was elsewhere. That distinction
|
|
32
|
+
// is unprovable from a hook that answers "inspected and permitted" and "never ran" with the same
|
|
33
|
+
// silence. Every decision below is now recorded (hooks/lib/decision.mjs).
|
|
36
34
|
|
|
37
35
|
import { runHook, readStdin, settle } from "./lib/decision.mjs";
|
|
38
36
|
|
|
@@ -57,7 +55,7 @@ const args = String(p.tool_input?.skill_args ?? p.tool_input?.args ?? "");
|
|
|
57
55
|
const skill = String(skillRaw).split(":").pop();
|
|
58
56
|
if (skill !== "tech-lead") defer(`Skill(${skill}) is not the orchestrator — out of scope`);
|
|
59
57
|
|
|
60
|
-
// The envelope port supplies its own intake;
|
|
58
|
+
// The envelope port supplies its own intake; `harness verify envelope` owns that path.
|
|
61
59
|
if (/--order\b/.test(args)) defer("envelope dispatch — validate-envelope owns this path", "--order");
|
|
62
60
|
|
|
63
61
|
// Intake is satisfied by ANY of: a pitch path, a spec folder, or free requirement text.
|
|
@@ -68,13 +66,14 @@ const hasResume = /--from\s+\S/.test(args); // resuming an existing run has its
|
|
|
68
66
|
// Free text = anything left once flags and their values are removed.
|
|
69
67
|
// Every flag that TAKES A VALUE must be listed here. A flag whose value is not stripped reads as
|
|
70
68
|
// free requirement text, and the gate then defers on an empty intake — the exact dispatch it
|
|
71
|
-
// exists to deny. This is a live failure mode, not a hypothetical: adding `--gate-answers
|
|
72
|
-
// `--wall-clock-budget
|
|
73
|
-
// tech-lead as `args:"--unattended --gate-answers ci --wall-clock-budget 2400"` — no
|
|
74
|
-
// text at all — and was waved straight through, because "ci" and "2400" counted as
|
|
69
|
+
// exists to deny. This is a live failure mode, not a hypothetical: adding `--gate-answers` and
|
|
70
|
+
// `--wall-clock-budget` without listing them here silently blinded the gate, and the very next run
|
|
71
|
+
// reached tech-lead as `args:"--unattended --gate-answers ci --wall-clock-budget 2400"` — no
|
|
72
|
+
// requirement text at all — and was waved straight through, because "ci" and "2400" counted as
|
|
73
|
+
// the spec.
|
|
75
74
|
// Adding a valued flag anywhere in the harness means adding it here, and structural test §39
|
|
76
75
|
// enforces exactly that against commands/ship.md.
|
|
77
|
-
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;
|
|
78
77
|
const BARE_FLAGS = /--[a-z0-9-]+/g;
|
|
79
78
|
const freeText = args.replace(VALUED_FLAGS, " ").replace(BARE_FLAGS, " ").trim();
|
|
80
79
|
|
package/hooks/gate-zerowork.mjs
CHANGED
|
@@ -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 (
|
|
4
|
+
// WHY THIS EXISTS (reproduced, not theorized).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
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. …"
|
|
@@ -14,7 +13,7 @@
|
|
|
14
13
|
// That is the "agent claims done" pathology this project exists to prevent, reproduced by the
|
|
15
14
|
// project, at its own front door.
|
|
16
15
|
//
|
|
17
|
-
// THE TWO STRUCTURAL MISSES IT EXPOSED.
|
|
16
|
+
// THE TWO STRUCTURAL MISSES IT EXPOSED. the ship report's census is the guard for exactly
|
|
18
17
|
// this class of failure, and it could not see this one for two independent reasons:
|
|
19
18
|
//
|
|
20
19
|
// 1. SCOPE. It defers unless a run is active (`activeSlug()` → `.shapeup/<slug>/`).
|
|
@@ -30,10 +29,10 @@
|
|
|
30
29
|
//
|
|
31
30
|
// the session dispatched the orchestrator AND the run left no receipt
|
|
32
31
|
//
|
|
33
|
-
//
|
|
32
|
+
// ``harness init run`` writes that receipt as the orchestrator's first tool call. Its absence is the
|
|
34
33
|
// fact. Nothing here parses intent, so nothing here can be talked past.
|
|
35
34
|
//
|
|
36
|
-
// 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,
|
|
37
36
|
// not a gate" — no second judge behind `spec-evaluator`. That governs quality JUDGMENTS. This
|
|
38
37
|
// hook makes no judgment: it reports that no work exists to judge. Blocking is also uniquely
|
|
39
38
|
// safe here, because a session with no artifacts has nothing to lose by continuing, and a
|
|
@@ -50,25 +49,77 @@
|
|
|
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
|
|
54
|
-
// every gate
|
|
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";
|
|
59
|
-
import { isMain } from "../
|
|
60
|
-
import { localDir, globLocal } from "../
|
|
58
|
+
import { isMain } from "../kernel/lib/argv.mjs";
|
|
59
|
+
import { localDir, globLocal } from "../kernel/lib/paths.mjs";
|
|
61
60
|
import { runHook, readStdin, settle, decisionsPath } from "./lib/decision.mjs";
|
|
62
61
|
|
|
63
62
|
const MAX_TRANSCRIPT_BYTES = 20 * 1024 * 1024;
|
|
64
63
|
|
|
65
|
-
/**
|
|
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
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Is this block a launch of the orchestrator's own workflow script, by either surface?
|
|
76
|
+
*
|
|
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
|
+
*
|
|
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: a node launcher AND an orchestrator script. A launcher 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 /\bnode\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;
|
|
@@ -110,7 +161,7 @@ export function workCensus(events) {
|
|
|
110
161
|
return census;
|
|
111
162
|
}
|
|
112
163
|
|
|
113
|
-
/** 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. */
|
|
114
165
|
export function findReceipts(cwd) {
|
|
115
166
|
const root = localDir(cwd);
|
|
116
167
|
if (!existsSync(root)) return [];
|
|
@@ -178,26 +229,39 @@ 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
|
|
239
|
+
"Check the plugin install — a symlinked path or a path with a space is the usual cause."
|
|
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
|
|
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:",
|
|
191
247
|
"",
|
|
192
248
|
" # write the requirement to a file first — inlining multi-line text into a shell",
|
|
193
249
|
" # argument is where this step goes wrong",
|
|
194
|
-
" node \"${CLAUDE_PLUGIN_ROOT}/
|
|
250
|
+
" node \"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs\" init run \\",
|
|
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
|
|
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
|
+
"",
|
|
257
|
+
" Workflow({",
|
|
258
|
+
" scriptPath: \"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js\",",
|
|
259
|
+
" args: <the RunArgs object>",
|
|
260
|
+
" })",
|
|
199
261
|
"",
|
|
200
|
-
"
|
|
262
|
+
"Resolve each gate the run pauses at with:",
|
|
263
|
+
"",
|
|
264
|
+
" node \"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs\" gate --resolve <gate-id> …",
|
|
201
265
|
"",
|
|
202
266
|
"If the command comes back \"requires approval\", say so and stop — the harness's scripts ship with",
|
|
203
267
|
"the plugin and need a one-time permission grant (`npx shapeup-sdlc init` writes it). Do NOT route",
|
|
@@ -231,18 +295,36 @@ async function main() {
|
|
|
231
295
|
// not a harness session → not our business
|
|
232
296
|
if (!dispatchedOrchestrator(events)) defer("session never dispatched the orchestrator — not a harness run", "no-dispatch");
|
|
233
297
|
|
|
234
|
-
// 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
|
|
235
299
|
// the evaluator's; this hook only asks whether anything started at all.
|
|
236
300
|
const receipts = findReceipts(cwd);
|
|
237
301
|
if (receipts.length > 0) defer(`${receipts.length} run receipt(s) on disk — the run started`, "receipt-present");
|
|
238
302
|
|
|
239
303
|
const census = workCensus(events);
|
|
240
304
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
305
|
+
// ── The `work-done` fail-open is GONE, and its removal is the point.
|
|
306
|
+
//
|
|
307
|
+
// WHAT IT USED TO DO: `if (census.work_calls > 2) defer("the session did work by other means")`.
|
|
308
|
+
// The stated rationale was that a user may have run the harness steps by hand, or be on a
|
|
309
|
+
// pre-receipt version of the plugin.
|
|
310
|
+
//
|
|
311
|
+
// WHAT IT ACTUALLY DID, observed. `Skill(tech-lead)` was dispatched, the lane could not start,
|
|
312
|
+
// the agent hand-built the whole feature, and the hook ALLOWED the Stop on `"37 work calls — the
|
|
313
|
+
// session did work by other means"`. That session passed its own oracle and read like a clean
|
|
314
|
+
// run, with no board, no T0, no verdict and no receipt. A busy session was the one thing that
|
|
315
|
+
// could switch this gate off — so the harder the agent worked around the harness, the safer it
|
|
316
|
+
// was from the gate watching for exactly that.
|
|
317
|
+
//
|
|
318
|
+
// BOTH HALVES OF THE RATIONALE FAIL ON INSPECTION. Running the harness steps by hand starts with
|
|
319
|
+
// ``harness init run``, which writes the receipt — such a session already deferred at `receipt-present`
|
|
320
|
+
// two lines up and never reached here. And a pre-receipt plugin cannot be the one executing this
|
|
321
|
+
// hook: ``harness init run`` ships in the same install, beside the `lib/` this file imports from.
|
|
322
|
+
// Neither case needs an escape, so no replacement escape is added — an escape that cannot fire
|
|
323
|
+
// is the "row that cannot fail" this project keeps catching.
|
|
324
|
+
//
|
|
325
|
+
// WHAT KEEPS THIS SAFE is not a threshold, it is the loop guard: `stop_hook_active` defers
|
|
326
|
+
// unconditionally, so a session that genuinely did the work outside the harness costs one extra
|
|
327
|
+
// turn and then stops. One nudge, never a hang. That is the trade this gate is worth.
|
|
246
328
|
const message = typeof p.last_assistant_message === "string" ? p.last_assistant_message : "";
|
|
247
329
|
const narration = detectNarration(message);
|
|
248
330
|
const enforcement = enforcementCensus(cwd);
|
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
|
}
|