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