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/bin/init.mjs
CHANGED
|
@@ -11,18 +11,17 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The bash installers remain the stable curl-able entrypoints for existing bookmarks;
|
|
13
13
|
// this is the `npx` front door. Both produce the same layout, and drift between them is
|
|
14
|
-
// a bug (
|
|
14
|
+
// a bug (the structural suite keeps the shared bits honest).
|
|
15
15
|
//
|
|
16
16
|
// Usage:
|
|
17
|
-
// npx shapeup-sdlc init [-d <dir>] [-y] [-o]
|
|
17
|
+
// npx shapeup-sdlc init [-d <dir>] [-y] [-o]
|
|
18
18
|
//
|
|
19
19
|
// What it configures (identical to install-harness.sh):
|
|
20
20
|
// AGENTS.md harness block · Claude Code plugin (CLI or settings.json merge) ·
|
|
21
|
-
//
|
|
22
|
-
// import · .gitignore rules · shapeup/metrics/ · Tier C templates
|
|
21
|
+
// CLAUDE.md @AGENTS.md import · .gitignore rules · shapeup/metrics/ · Tier C templates
|
|
23
22
|
|
|
24
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
25
|
-
import { resolve, join, dirname
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, appendFileSync } from "node:fs";
|
|
24
|
+
import { resolve, join, dirname } from "node:path";
|
|
26
25
|
import { fileURLToPath } from "node:url";
|
|
27
26
|
import { spawnSync } from "node:child_process";
|
|
28
27
|
import { createInterface } from "node:readline";
|
|
@@ -35,31 +34,24 @@ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
35
34
|
const REPO = "nguyenvanphituoc/shapeup-sdlc-plugin";
|
|
36
35
|
const MARKETPLACE_KEY = "nvptuoc-marketplace";
|
|
37
36
|
const PLUGIN_KEY = "shapeup-sdlc-plugin@nvptuoc-marketplace";
|
|
38
|
-
const ALL_CLIS = ["claude", "antigravity", "codex"];
|
|
39
37
|
|
|
40
38
|
// ---- args -------------------------------------------------------------------
|
|
41
39
|
const argv = process.argv.slice(2);
|
|
42
40
|
const usage = `Usage: npx shapeup-sdlc init [options]
|
|
43
41
|
Options:
|
|
44
42
|
-d, --directory <path> Target project directory (default: current directory)
|
|
45
|
-
--cli <list> Comma-separated: claude,antigravity,codex or "all" (default: all)
|
|
46
43
|
-o, --override Overwrite existing files in target
|
|
47
44
|
-y, --yes Run unattended (answer yes to all prompts)
|
|
48
45
|
-h, --help Print this help`;
|
|
49
46
|
|
|
50
|
-
let targetDir = ".", yes = false, override = false
|
|
47
|
+
let targetDir = ".", yes = false, override = false;
|
|
51
48
|
const positional = [];
|
|
52
49
|
for (let i = 0; i < argv.length; i++) {
|
|
53
50
|
const a = argv[i];
|
|
54
51
|
if (a === "-d" || a === "--directory") targetDir = argv[++i];
|
|
55
52
|
else if (a === "-y" || a === "--yes") yes = true;
|
|
56
53
|
else if (a === "-o" || a === "--override") override = true;
|
|
57
|
-
else if (a === "--
|
|
58
|
-
const v = argv[++i] || "";
|
|
59
|
-
clis = v === "all" ? [...ALL_CLIS] : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
60
|
-
const bad = clis.filter((c) => !ALL_CLIS.includes(c));
|
|
61
|
-
if (bad.length) { console.error(`Unknown CLI(s): ${bad.join(", ")}. Valid: ${ALL_CLIS.join(", ")}, all`); process.exit(1); }
|
|
62
|
-
} else if (a === "-h" || a === "--help") { console.log(usage); process.exit(0); }
|
|
54
|
+
else if (a === "-h" || a === "--help") { console.log(usage); process.exit(0); }
|
|
63
55
|
else if (a.startsWith("-")) { console.error(`Unknown option: ${a}\n${usage}`); process.exit(1); }
|
|
64
56
|
else positional.push(a);
|
|
65
57
|
}
|
|
@@ -71,7 +63,6 @@ if (positional.length && positional[0] !== "init") {
|
|
|
71
63
|
const target = resolve(targetDir);
|
|
72
64
|
if (!existsSync(target)) { console.error(`Target directory does not exist: ${target}`); process.exit(1); }
|
|
73
65
|
console.log(`Installing Shape Up SDLC Harness into: ${target}`);
|
|
74
|
-
console.log(`CLIs: ${clis.join(", ")}`);
|
|
75
66
|
|
|
76
67
|
// ---- confirmation -----------------------------------------------------------
|
|
77
68
|
if (!yes) {
|
|
@@ -116,21 +107,16 @@ if (!existsSync(agentsSrc)) {
|
|
|
116
107
|
}
|
|
117
108
|
}
|
|
118
109
|
|
|
119
|
-
// ---- 1.
|
|
120
|
-
|
|
121
|
-
if (cli === "claude") installClaude();
|
|
122
|
-
else replaceSkills(cli);
|
|
123
|
-
}
|
|
110
|
+
// ---- 1. Claude Code install -------------------------------------------------
|
|
111
|
+
installClaude();
|
|
124
112
|
|
|
125
|
-
// ---- 2. wire
|
|
126
|
-
|
|
127
|
-
if (clis.includes("antigravity")) ensureAgentImport(join(target, ".agents", "AGENTS.md"), ".agents/AGENTS.md", "auto");
|
|
128
|
-
if (clis.includes("codex")) ensureAgentImport(join(target, ".codex", "AGENTS.md"), ".codex/AGENTS.md", "auto");
|
|
113
|
+
// ---- 2. wire Claude Code to the root AGENTS.md ------------------------------
|
|
114
|
+
ensureAgentImport(join(target, "CLAUDE.md"), "CLAUDE.md");
|
|
129
115
|
|
|
130
116
|
// ---- 3. .gitignore ----------------------------------------------------------
|
|
131
|
-
// Both roots are listed, deliberately. A project may be
|
|
132
|
-
//
|
|
133
|
-
// the tier split exists to prevent. Ignoring a directory that does not exist costs nothing.
|
|
117
|
+
// Both roots are listed, deliberately. A project may still be carrying the pre-ADR-0001 local root
|
|
118
|
+
// alongside the current one, and a run trace committed by accident from either is exactly the
|
|
119
|
+
// mistake the tier split exists to prevent. Ignoring a directory that does not exist costs nothing.
|
|
134
120
|
const GITIGNORE_RULE = `# Shape Up SDLC run workspace
|
|
135
121
|
${LOCAL}/
|
|
136
122
|
${LEGACY_LOCAL}/
|
|
@@ -244,7 +230,7 @@ function installClaude() {
|
|
|
244
230
|
/**
|
|
245
231
|
* Pre-approve the harness's OWN pipeline scripts, and nothing else.
|
|
246
232
|
*
|
|
247
|
-
* WHY THIS EXISTS (
|
|
233
|
+
* WHY THIS EXISTS (observed, not theorized).
|
|
248
234
|
*
|
|
249
235
|
* Every load-bearing step of a run is a Node script that ships with the plugin and therefore
|
|
250
236
|
* lives OUTSIDE the project — `${CLAUDE_PLUGIN_ROOT}/skills/**\/scripts/*.mjs`. Under any
|
|
@@ -252,10 +238,10 @@ function installClaude() {
|
|
|
252
238
|
* directory needs approval. In an interactive session you click once and forget it. In a headless
|
|
253
239
|
* one there is nobody to click, and the run cannot take its first step.
|
|
254
240
|
*
|
|
255
|
-
* That is not hypothetical.
|
|
241
|
+
* That is not hypothetical. Without the grant, the run receipt step (`init-run.mjs`) gets
|
|
256
242
|
* attempted six different ways in a single session — direct, via a heredoc, via two hand-written
|
|
257
|
-
* wrapper scripts, via a sub-agent — and every one
|
|
258
|
-
* The agent
|
|
243
|
+
* wrapper scripts, via a sub-agent — and every one comes back "This command requires approval".
|
|
244
|
+
* The agent then gives up on the harness and builds the feature by hand. It is the failure the
|
|
259
245
|
* receipt was designed to make visible, arriving through the door the receipt itself opened.
|
|
260
246
|
*
|
|
261
247
|
* Scope is deliberately narrow: `node <plugin>/skills/.../scripts/*.mjs`, by prefix. This grants
|
|
@@ -270,7 +256,7 @@ function installClaude() {
|
|
|
270
256
|
* put every call site back outside the grant — the exact mismatch this fix exists to remove. The
|
|
271
257
|
* unquoted prefix stays for older prose and for anything a user has already typed.
|
|
272
258
|
*
|
|
273
|
-
*
|
|
259
|
+
* The structural suite asserts that every documented call site is in a form
|
|
274
260
|
* one of these prefixes actually matches, so the two can never drift apart again.
|
|
275
261
|
*
|
|
276
262
|
* @param {object} settings - Parsed settings.json, mutated in place.
|
|
@@ -288,42 +274,11 @@ function mergePipelinePermissions(settings) {
|
|
|
288
274
|
settings.permissions.allow = [...allow];
|
|
289
275
|
}
|
|
290
276
|
|
|
291
|
-
function
|
|
292
|
-
const src = join(PKG_ROOT, "skills");
|
|
293
|
-
const dest = join(target, cli === "antigravity" ? ".agents" : ".codex", "skills");
|
|
294
|
-
mkdirSync(dest, { recursive: true });
|
|
295
|
-
let n = 0;
|
|
296
|
-
for (const name of readdirSync(src)) {
|
|
297
|
-
const skillPath = join(src, name);
|
|
298
|
-
if (!existsSync(join(skillPath, "SKILL.md"))) continue; // skip empty stubs
|
|
299
|
-
rmSync(join(dest, name), { recursive: true, force: true });
|
|
300
|
-
cpSync(skillPath, join(dest, name), { recursive: true });
|
|
301
|
-
n++;
|
|
302
|
-
}
|
|
303
|
-
console.log(` [${cli}] ${n} skills replaced in ${rel(dest)}`);
|
|
304
|
-
|
|
305
|
-
if (cli === "antigravity") {
|
|
306
|
-
const distSub = join(PKG_ROOT, "dist", "antigravity", "subagents");
|
|
307
|
-
if (existsSync(distSub)) {
|
|
308
|
-
const subDest = join(target, ".agents", "subagents");
|
|
309
|
-
mkdirSync(subDest, { recursive: true });
|
|
310
|
-
cpSync(distSub, subDest, { recursive: true });
|
|
311
|
-
const idx = join(PKG_ROOT, "dist", "antigravity", "subagents.json");
|
|
312
|
-
if (existsSync(idx)) cpSync(idx, join(target, ".agents", "subagents.json"));
|
|
313
|
-
console.log(` [antigravity] subagent configs replaced in ${rel(subDest)}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function ensureAgentImport(file, label, mode) {
|
|
277
|
+
function ensureAgentImport(file, label) {
|
|
319
278
|
mkdirSync(dirname(file), { recursive: true });
|
|
320
279
|
if (!existsSync(file)) writeFileSync(file, "");
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
} else console.log(`@AGENTS.md import tag already present in ${label}`);
|
|
326
|
-
} else {
|
|
327
|
-
console.log(`${label} ready (root AGENTS.md auto-discovered)`);
|
|
328
|
-
}
|
|
280
|
+
if (!readFileSync(file, "utf8").includes("@AGENTS.md")) {
|
|
281
|
+
appendFileSync(file, "\n@AGENTS.md\n");
|
|
282
|
+
console.log(`Appended @AGENTS.md import tag to ${label}`);
|
|
283
|
+
} else console.log(`@AGENTS.md import tag already present in ${label}`);
|
|
329
284
|
}
|
package/commands/build.md
CHANGED
|
@@ -12,3 +12,22 @@ If no task ID was given, read the board (`.shapeup/<slug>/tasks/_index.md`) and
|
|
|
12
12
|
next `ready` task, stating which one you picked. Respect the substrate: if scope contracts
|
|
13
13
|
exist, writes outside the active scope's whitelist will be denied by the sandbox hook — that is
|
|
14
14
|
the harness working, not an error to route around.
|
|
15
|
+
|
|
16
|
+
## Building a whole scoped feature is not this command
|
|
17
|
+
|
|
18
|
+
This command builds **one task**. A full BUILD round — every scope, the per-scope attempt loop,
|
|
19
|
+
T0 verification, the inner circuit breaker, then the single EVAL — is a workflow-script launch, and
|
|
20
|
+
it belongs to the orchestrator:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/run-workflow.mjs" \
|
|
24
|
+
"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js" \
|
|
25
|
+
--args-file .shapeup/<slug>/run-args.json --run-dir .shapeup/<slug>/workflow-run
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Reach it through `/ship` (or the `tech-lead` skill), which opens the run properly — `init-run.mjs`
|
|
29
|
+
first, so the receipt exists. Do not hand-roll the round by calling this command once per task: the
|
|
30
|
+
attempt loop, the T0 ratchet and the breakers are branches in that script, not steps a caller can
|
|
31
|
+
be trusted to reproduce, and a session that rebuilds them by hand is the prose lane the cutover
|
|
32
|
+
replaced. On a `--tiny` run or a spec with no committed `scopes/*.md`, the prose loop in
|
|
33
|
+
`skills/tech-lead/references/round-protocol.md` still applies, unchanged and by design.
|
package/commands/eval.md
CHANGED
|
@@ -9,7 +9,8 @@ The single judge. Two modes, chosen by the arguments:
|
|
|
9
9
|
- `--spec <folder> --feature <slug> --single-pass` — the once-per-round verdict on the whole
|
|
10
10
|
board.
|
|
11
11
|
|
|
12
|
-
Round mode is
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
do not
|
|
12
|
+
Round mode is watched: a PreToolUse hook (GATE L2) **warns** while any task on the board is
|
|
13
|
+
unfinished, naming the offenders — advisory since ADR-0001, so the call proceeds, but a verdict
|
|
14
|
+
taken now grades a partial board and the warning is recorded. The correct response is to route
|
|
15
|
+
back to `/build` and finish them — do not shrug the warning off, and do not use `--task` as a
|
|
16
|
+
loophole to simulate a round verdict piecemeal.
|
package/commands/scopes.md
CHANGED
|
@@ -5,10 +5,11 @@ This is step 8 (Map Scopes), which is two skills in sequence:
|
|
|
5
5
|
|
|
6
6
|
1. Use the **ba-pitch-analyzer** skill on $ARGUMENTS — pitch → linked DDD spec tree (domain
|
|
7
7
|
model → use cases → tasks) with BDD scenarios and the derived Test Surface. Pass through an
|
|
8
|
-
operation when the user names one: `analyze
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
operation when the user names one: `analyze` (the default — spec tree + board), `reconcile`
|
|
9
|
+
(fold discovered-ledger items back into the board and UC invariants), `retrofit-surface`
|
|
10
|
+
(append a Test Surface to a spec written before there was one), or `coverage` (extract the
|
|
11
|
+
shared `requirements.md` registry that anchors covers-closure).
|
|
12
|
+
2. Then use the **scope-architect** skill (`map-scopes`) to write the committed scope contracts
|
|
11
13
|
(`scopes/*.md`) — import-graph slicing by flow, write-whitelist substrates, fixtures.
|
|
12
|
-
Operations: `map-scopes` (default), `remap`, `split-scope`.
|
|
13
14
|
|
|
14
15
|
If the user asked for only one half ("just analyze", "just the contracts"), run only that half.
|
package/commands/shape.md
CHANGED
|
@@ -3,7 +3,7 @@ description: Shape a raw idea into a pitch (boundaries → breadboard → spike
|
|
|
3
3
|
---
|
|
4
4
|
Use the **shapeup** skill on $ARGUMENTS.
|
|
5
5
|
|
|
6
|
-
This is Phase 1 of the pipeline — it runs *before* any code and produces the `
|
|
6
|
+
This is Phase 1 of the pipeline — it runs *before* any code and produces the `shaping.md` and `breadboard.md` files (the pitch) that the
|
|
7
7
|
Betting Table decides on. Default to the full sequence (`full`); when the user names a single
|
|
8
8
|
step, pass it through as the sub-command: `shaping`, `breadboarding`, `spike`, `framing-doc`,
|
|
9
9
|
`kickoff-doc`, or `breadboard-reflection`.
|
package/commands/ship.md
CHANGED
|
@@ -12,6 +12,39 @@ loop; do not skip gates by default.
|
|
|
12
12
|
a session that dispatches the orchestrator and leaves no receipt is blocked at `Stop` by
|
|
13
13
|
`hooks/gate-zerowork.mjs`.
|
|
14
14
|
|
|
15
|
+
## How the run actually executes
|
|
16
|
+
|
|
17
|
+
On a spec with committed `scopes/*.md` — the common case — `tech-lead` holds the L0 intake
|
|
18
|
+
conversation, writes `project-profile.md`, then hands the whole pipeline to a single background
|
|
19
|
+
launch and does not drive it turn by turn:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/scripts/run-workflow.mjs" \
|
|
23
|
+
"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js" \
|
|
24
|
+
--args-file .shapeup/<slug>/run-args.json --run-dir .shapeup/<slug>/workflow-run
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
ORIENT → L1a → ANALYZE → WIRE → L1a.5 → MAP SCOPES → L1b → rounds of BUILD/L2/EVAL → QA → GATE H
|
|
28
|
+
all run inside it. Three things follow, and they are the point of the cutover rather than trivia:
|
|
29
|
+
|
|
30
|
+
- **A gate pause is a return value, not a stop.** The launch returns `{status: "paused", paused_at,
|
|
31
|
+
block}`; emit `block` **verbatim**, get the PO's decision, write it to
|
|
32
|
+
`.shapeup/<slug>/gate-answers.json`, and **relaunch the same call with the same args**. The
|
|
33
|
+
fast-forward re-derives position from disk and re-dispatches nothing already finished.
|
|
34
|
+
- **A killed session loses nothing.** Resume state comes off disk, never from context, so a fresh
|
|
35
|
+
session picks the run up where it died — the property the whole launch shape exists to buy.
|
|
36
|
+
- **Headless runs need `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` in the environment.** Without it
|
|
37
|
+
`claude -p` cuts the background wait at 600 s and **exits 0**, reporting a truncated run as a
|
|
38
|
+
clean one. Set it for any `--unattended` or CI invocation.
|
|
39
|
+
- **Never launch this with the `Workflow` tool.** That call needs an interactive confirmation, so it
|
|
40
|
+
is denied in every headless session, and the only grant that unblocks it is unscoped. Left to it,
|
|
41
|
+
the script executes **zero** times and the agent improvises instead — a session can reach GATE L4
|
|
42
|
+
with a valid receipt while the pipeline never started. `run-workflow.mjs` runs the same script
|
|
43
|
+
under the path-scoped grant `npx shapeup-sdlc init` already writes.
|
|
44
|
+
|
|
45
|
+
`--tiny`, and any spec with no committed `scopes/*.md` yet, take the unchanged prose lane in
|
|
46
|
+
`skills/tech-lead/references/round-protocol.md` instead — non-regression, by design.
|
|
47
|
+
|
|
15
48
|
Only run headless/auto if the user explicitly asks for it in their message:
|
|
16
49
|
- `--auto` → advance low-risk gates automatically, still pause at L4 (Ship sign-off).
|
|
17
50
|
Implies `--gate-answers guarded` unless a set is named.
|
|
@@ -22,8 +55,8 @@ Only run headless/auto if the user explicitly asks for it in their message:
|
|
|
22
55
|
into GATE L0 in the same turn.
|
|
23
56
|
|
|
24
57
|
> Why this is spelled out: asking for confirmation here made `--unattended` unusable for the
|
|
25
|
-
> only job it has. In a non-interactive invocation (`claude -p …`, a CI step
|
|
26
|
-
>
|
|
58
|
+
> only job it has. In a non-interactive invocation (`claude -p …`, a CI step) there is no second
|
|
59
|
+
> turn in which to answer, so the run spent its turn requesting permission and
|
|
27
60
|
> exited having written nothing. A headless flag that cannot complete a headless run is a defect,
|
|
28
61
|
> not a safety feature — and the warning, which is the part that carries the safety value, is
|
|
29
62
|
> still printed. `--auto` remains the middle setting that pauses at L4.
|
|
@@ -39,9 +72,9 @@ Additional flags, pass through to `tech-lead` only when the user names them:
|
|
|
39
72
|
decision's **source** becomes the answer set instead of a live human, and the ledger says so.
|
|
40
73
|
Generate one with `gate-answers.mjs --init --preset ci --by "<name>"`. This is what makes a
|
|
41
74
|
headless lane finish: without it an unattended run waits at the first ⏸ until the wall-clock
|
|
42
|
-
budget expires
|
|
75
|
+
budget expires, having built nothing.
|
|
43
76
|
- `--wall-clock-budget <seconds>` → arm the deadline breaker. Off by default. Set it in any lane
|
|
44
|
-
with a hard clock (CI,
|
|
77
|
+
with a hard clock (CI, an overnight run) and set it *below* the external kill, so
|
|
45
78
|
the harness trips its own breaker first: past the deadline `hooks/gate-deadline.mjs` denies new
|
|
46
79
|
`task-executor` work and routes to GATE H, where scope-hammer ships whatever is green. A run
|
|
47
80
|
killed from outside ships nothing — including the scopes that already passed T0.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Anti-rationalization — advisory Stop hook (v1.2
|
|
2
|
+
// Anti-rationalization — advisory Stop hook (v1.2).
|
|
3
3
|
//
|
|
4
4
|
// When the session's final message claims completion ("done", "all tests pass", "ready to
|
|
5
5
|
// ship") while the harness's own mechanical facts disagree (unfinished board tasks, a red T0
|
|
6
|
-
// verdict
|
|
6
|
+
// verdict), this hook says so — to the user, out loud, with the facts.
|
|
7
7
|
//
|
|
8
8
|
// ADVISORY ONLY, by architectural invariant: "QA is a level-up, not a gate." This hook exits
|
|
9
9
|
// 0 always and emits at most { systemMessage } — never { decision: "block" }, never exit 2.
|
|
@@ -59,9 +59,9 @@ export function activeSlug(cwd) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Does the text claim the work is finished — or promise that it is about to be?
|
|
61
61
|
*
|
|
62
|
-
* The past-tense half is the original detector. The future-tense half was added after
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* The past-tense half is the original detector. The future-tense half was added after a transcript
|
|
63
|
+
* this hook should have caught and structurally could not: the session ended on "The tech-lead
|
|
64
|
+
* skill is orchestrating the full harness. It will: 1…".
|
|
65
65
|
* A promise at the END of a session is a completion claim wearing different grammar — the run is
|
|
66
66
|
* over, and the thing it says it will do is never going to happen. Matching only past tense meant
|
|
67
67
|
* the emptiest failures were the least detectable, which is backwards.
|
|
@@ -93,7 +93,7 @@ export function isFutureClaim(claim) {
|
|
|
93
93
|
/** Read-only mechanical facts about the run — the evidence the claim is checked against. */
|
|
94
94
|
export function gatherFacts(cwd, slug) {
|
|
95
95
|
const root = localRoot(cwd, slug);
|
|
96
|
-
const facts = { unfinished: [], red_t0: null,
|
|
96
|
+
const facts = { unfinished: [], red_t0: null, run_status: null, final_verdict: null };
|
|
97
97
|
|
|
98
98
|
const tasksDir = join(root, "tasks");
|
|
99
99
|
if (existsSync(tasksDir)) {
|
|
@@ -123,11 +123,6 @@ export function gatherFacts(cwd, slug) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const escDir = join(root, "escalates");
|
|
127
|
-
if (existsSync(escDir)) {
|
|
128
|
-
facts.open_escalates = readdirSync(escDir).filter((f) => f.endsWith(".json")).length;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
126
|
const runPath = join(root, "harness-run.md");
|
|
132
127
|
if (existsSync(runPath)) {
|
|
133
128
|
try {
|
|
@@ -143,7 +138,7 @@ export function gatherFacts(cwd, slug) {
|
|
|
143
138
|
* The facts that contradict a claim, as human-readable fragments.
|
|
144
139
|
*
|
|
145
140
|
* TENSE DECIDES WHICH FACTS COUNT, and getting this wrong is what makes a hook get disabled.
|
|
146
|
-
* An unfinished board
|
|
141
|
+
* An unfinished board and a red T0 both contradict "it is done". NEITHER of
|
|
147
142
|
* them contradict "I am about to run the evaluator" — they are that sentence's PREMISE. Checking
|
|
148
143
|
* a promise against them meant every healthy build round ended with the hook announcing that the
|
|
149
144
|
* turn's own plan "disagrees with the facts", naming as evidence the very work the plan exists to
|
|
@@ -175,7 +170,6 @@ export function contradictions(claim, facts) {
|
|
|
175
170
|
out.push(`${facts.unfinished.length} board task(s) not done (${named}${facts.unfinished.length > 5 ? ", …" : ""})`);
|
|
176
171
|
}
|
|
177
172
|
if (facts.red_t0) out.push(`latest T0 verdict ${facts.red_t0} is red`);
|
|
178
|
-
if (facts.open_escalates > 0) out.push(`${facts.open_escalates} escalate(s) unanswered`);
|
|
179
173
|
if (facts.final_verdict === "fail") out.push("harness-run records final_verdict: fail");
|
|
180
174
|
if (/ship/i.test(claim || "") && MID_RUN.has(facts.run_status)) out.push(`run status is still "${facts.run_status}"`);
|
|
181
175
|
return out;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Compact snapshot — PreCompact hook (v1.2
|
|
2
|
+
// Compact snapshot — PreCompact hook (v1.2).
|
|
3
3
|
//
|
|
4
4
|
// PreCompact provably CANNOT inject context (no additionalContext/systemMessage channel), so
|
|
5
5
|
// this hook is a pure side effect: when a harness run is mid-flight, freeze a RunSnapshot to
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Contract: PreCompact stdin JSON { cwd, trigger: "manual"|"auto" }.
|
|
13
13
|
|
|
14
|
-
// RECEIPTS (v1.5). This hook is the sharpest case for `hooks/lib/decision.mjs`:
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// RECEIPTS (v1.5). This hook is the sharpest case for `hooks/lib/decision.mjs`: it is routinely
|
|
15
|
+
// scored `Unfired` — 0 `PreCompact` events observed — and that score is UNOBTAINABLE, because
|
|
16
|
+
// "never had to fire" and "never ran" produce identical evidence (exit 0, no output). With a
|
|
17
|
+
// decision row per invocation the two become separable facts.
|
|
18
18
|
|
|
19
19
|
import { deriveSnapshot, writeSnapshot } from "../skills/tech-lead/scripts/run-snapshot.mjs";
|
|
20
20
|
import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
|
package/hooks/gate-deadline.mjs
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GATE D — DEADLINE. PreToolUse hook. The wall-clock breaker, enforced rather than requested.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (and it corrects an earlier diagnosis).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// A run killed at an external time cap looks exactly like a stall from outside: no acceptance, no
|
|
7
|
+
// verdict, nothing to show. The natural reading is "it hung at a gate". Often the opposite is
|
|
8
|
+
// true — the run was working, steadily, and was still working when the clock ran out.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// Both existing breakers count EVENTS (`round_budget` per round, `attempt_budget` per T0
|
|
11
|
+
// attempt), so neither can observe that a single round has been running for half an hour. A run
|
|
12
|
+
// can burn its whole budget with both breakers untouched.
|
|
12
13
|
//
|
|
13
|
-
//
|
|
14
|
-
// the clock ran out. Both existing breakers count EVENTS (`round_budget` per round,
|
|
15
|
-
// `attempt_budget` per T0 attempt), so neither can observe that a single round has been running
|
|
16
|
-
// for twenty-nine minutes. The run burned its whole budget with both breakers untouched.
|
|
17
|
-
//
|
|
18
|
-
// The cost of that is not the DNF row. It is that an externally killed run ships NOTHING, not
|
|
14
|
+
// The cost of that is not the missing verdict. It is that an externally killed run ships NOTHING, not
|
|
19
15
|
// even the scopes that were already green. A breaker that trips from the inside routes to GATE H
|
|
20
16
|
// instead: census, baseline comparison, ship the part that works. Same clock, different ending.
|
|
21
17
|
//
|
|
22
18
|
// WHAT IT DENIES, AND WHAT IT DELIBERATELY DOES NOT. Past the deadline this denies dispatches
|
|
23
|
-
// that START NEW WORK — `task-executor`. It never denies `spec-evaluator`, `scope-hammer
|
|
24
|
-
// `qa-edge-hunter
|
|
19
|
+
// that START NEW WORK — `task-executor`. It never denies `spec-evaluator`, `scope-hammer` or
|
|
20
|
+
// `qa-edge-hunter`, because a run past its deadline must still be able to
|
|
25
21
|
// judge, hammer and close. A breaker that also blocked the exit would strand the run with green
|
|
26
22
|
// scopes it could not ship, which is the failure it exists to prevent.
|
|
27
23
|
//
|
|
@@ -132,8 +128,8 @@ const reason = [
|
|
|
132
128
|
"against the ideal), and produces a cut list. Ship what is green; the rest becomes a raw idea for",
|
|
133
129
|
"the next Betting Table.",
|
|
134
130
|
"",
|
|
135
|
-
"spec-evaluator, scope-hammer
|
|
136
|
-
"
|
|
131
|
+
"spec-evaluator, scope-hammer and qa-edge-hunter are all still permitted — a run past its",
|
|
132
|
+
"deadline must still be able to judge, hammer, and close.",
|
|
137
133
|
...repeat,
|
|
138
134
|
].join("\n");
|
|
139
135
|
|
package/hooks/gate-intake.mjs
CHANGED
|
@@ -3,16 +3,14 @@
|
|
|
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
|
|
@@ -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
|
|
|
@@ -68,10 +66,11 @@ 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
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;
|
package/hooks/gate-l2.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// GATE L2 — board-green check on the once-per-round EVAL. PreToolUse hook
|
|
2
|
+
// GATE L2 — board-green check on the once-per-round EVAL. PreToolUse hook.
|
|
3
3
|
//
|
|
4
4
|
// ADVISORY SINCE ADR-0001, and the downgrade was a deliberate product decision rather than a
|
|
5
5
|
// retreat. This hook used to hard-DENY the EVAL delegation (tech-lead → spec-evaluator) while the
|
|
@@ -82,8 +82,8 @@ if (!roundMode) defer(hasTask ? "per-task eval — the board-green rule is about
|
|
|
82
82
|
|
|
83
83
|
// 4. Locate the board. Since v0.4.0 (Local Tasks Architecture) it lives under the LOCAL
|
|
84
84
|
// gitignored root `.shapeup/<slug>/tasks/`, NOT the committed spec dir — resolving only
|
|
85
|
-
// `<spec>/tasks/` made this hook silently fail-open on every v0.4.0+ run
|
|
86
|
-
//
|
|
85
|
+
// `<spec>/tasks/` made this hook silently fail-open on every v0.4.0+ run — EVAL proceeded with
|
|
86
|
+
// most of the board still `status: ready`.
|
|
87
87
|
// <slug> comes from --feature (the round invocation always carries it: tech-lead's eval plan
|
|
88
88
|
// is `--spec <path> --feature <slug> --single-pass`), falling back to the spec-path
|
|
89
89
|
// convention shapeup/<slug>/spec → parent dir name. `<spec>/tasks/` is kept as the
|