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.
Files changed (118) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +54 -107
  4. package/README.md +165 -151
  5. package/SECURITY.md +49 -27
  6. package/bin/init.mjs +93 -108
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +20 -0
  9. package/commands/eval.md +5 -4
  10. package/commands/scopes.md +5 -4
  11. package/commands/shape.md +1 -1
  12. package/commands/ship.md +53 -7
  13. package/commands/wire.md +1 -1
  14. package/hooks/dispatch-receipt.mjs +195 -0
  15. package/hooks/gate-intake.mjs +16 -17
  16. package/hooks/gate-zerowork.mjs +107 -25
  17. package/hooks/hooks.json +9 -48
  18. package/hooks/lib/decision.mjs +38 -19
  19. package/hooks/safety-spine.mjs +4 -4
  20. package/hooks/sandbox-guard.mjs +130 -50
  21. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
  22. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
  23. package/kernel/harness.mjs +134 -0
  24. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
  25. package/kernel/init/run.mjs +489 -0
  26. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  27. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
  28. package/kernel/lib/paths.mjs +491 -0
  29. package/kernel/probe/concurrency.mjs +510 -0
  30. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
  31. package/kernel/probe/eval.mjs +77 -0
  32. package/kernel/probe/leg.mjs +125 -0
  33. package/kernel/probe/resume.mjs +528 -0
  34. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
  35. package/kernel/probe/t0.mjs +66 -0
  36. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
  37. package/kernel/reduce/graph.mjs +437 -0
  38. package/kernel/reduce/hill.mjs +152 -0
  39. package/kernel/reduce/ingest.mjs +633 -0
  40. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  41. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
  42. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
  43. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
  44. package/kernel/report/export.mjs +325 -0
  45. package/kernel/report/facts.mjs +347 -0
  46. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
  47. package/kernel/verify/dispatch.mjs +114 -0
  48. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  49. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  50. package/kernel/verify/skills.mjs +125 -0
  51. package/kernel/verify/spec.mjs +559 -0
  52. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
  53. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
  54. package/oracles/_shared.mjs +1 -1
  55. package/oracles/http-oracle.mjs +2 -2
  56. package/oracles/index.mjs +1 -1
  57. package/oracles/process-oracle.mjs +2 -2
  58. package/oracles/snapshot-oracle.mjs +2 -2
  59. package/oracles/test-oracle.mjs +2 -2
  60. package/package.json +11 -13
  61. package/skills/ba-pitch-analyzer/SKILL.md +24 -19
  62. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  63. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  64. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  65. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  66. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  67. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  68. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  69. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  70. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  71. package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
  72. package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
  73. package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
  74. package/skills/coach/SKILL.md +3 -3
  75. package/skills/orient/SKILL.md +2 -1
  76. package/skills/qa-edge-hunter/SKILL.md +15 -41
  77. package/skills/scope-architect/SKILL.md +57 -18
  78. package/skills/scope-hammer/SKILL.md +5 -5
  79. package/skills/shapeup/SKILL.md +3 -3
  80. package/skills/shapeup/resources/context-compaction.md +4 -3
  81. package/skills/solution-architect/SKILL.md +37 -15
  82. package/skills/spec-evaluator/SKILL.md +24 -7
  83. package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
  84. package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
  85. package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
  86. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
  87. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
  88. package/skills/spec-evaluator/references/probing.md +42 -23
  89. package/skills/spec-evaluator/references/report-schema.md +2 -2
  90. package/skills/task-executor/SKILL.md +32 -18
  91. package/skills/tech-lead/SKILL.md +127 -438
  92. package/skills/tech-lead/references/gates.md +140 -49
  93. package/skills/tech-lead/references/protocol.md +832 -0
  94. package/skills/tech-lead/schemas/domain.schema.json +645 -198
  95. package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
  96. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  97. package/skills/tech-lead/schemas/work-result.schema.json +56 -18
  98. package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
  99. package/skills/translator/SKILL.md +2 -2
  100. package/hooks/anti-rationalization.mjs +0 -244
  101. package/hooks/compact-snapshot.mjs +0 -47
  102. package/hooks/gate-deadline.mjs +0 -151
  103. package/hooks/gate-l2.mjs +0 -161
  104. package/hooks/session-rehydrate.mjs +0 -109
  105. package/skills/advisor-protocol/SKILL.md +0 -171
  106. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  107. package/skills/spec-evaluator/README.md +0 -93
  108. package/skills/tech-lead/README.md +0 -71
  109. package/skills/tech-lead/references/delegation.md +0 -254
  110. package/skills/tech-lead/references/invocation.md +0 -45
  111. package/skills/tech-lead/references/ledger-schema.md +0 -214
  112. package/skills/tech-lead/references/round-protocol.md +0 -184
  113. package/skills/tech-lead/references/state-model.md +0 -66
  114. package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
  115. package/skills/tech-lead/scripts/init-run.mjs +0 -326
  116. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
  117. package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
  118. package/skills/translator/README.md +0 -66
package/bin/init.mjs CHANGED
@@ -11,22 +11,22 @@
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 (tests/structural keeps the shared bits honest).
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] [--cli claude,antigravity,codex|all]
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
- // Antigravity .agents/skills + subagents · Codex .codex/skills · CLAUDE.md @AGENTS.md
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, rmSync, cpSync, readdirSync, appendFileSync } from "node:fs";
25
- import { resolve, join, dirname, basename } from "node:path";
23
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, appendFileSync, realpathSync } 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";
29
- import { LOCAL, LEGACY, metricsDir } from "../skills/tech-lead/scripts/lib/paths.mjs";
28
+ import { LOCAL, LEGACY, metricsDir } from "../kernel/lib/paths.mjs";
29
+ import { mergePipelinePermissions as mergeGrant, isWorkspaceTrusted, WORKFLOW_RULE } from "./lib/grant.mjs";
30
30
 
31
31
  /** The root a project migrating off the pre-ADR-0001 layout may still be carrying. */
32
32
  const LEGACY_LOCAL = LEGACY.local;
@@ -35,31 +35,33 @@ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
35
35
  const REPO = "nguyenvanphituoc/shapeup-sdlc-plugin";
36
36
  const MARKETPLACE_KEY = "nvptuoc-marketplace";
37
37
  const PLUGIN_KEY = "shapeup-sdlc-plugin@nvptuoc-marketplace";
38
- const ALL_CLIS = ["claude", "antigravity", "codex"];
39
38
 
40
39
  // ---- args -------------------------------------------------------------------
41
40
  const argv = process.argv.slice(2);
42
41
  const usage = `Usage: npx shapeup-sdlc init [options]
43
42
  Options:
44
43
  -d, --directory <path> Target project directory (default: current directory)
45
- --cli <list> Comma-separated: claude,antigravity,codex or "all" (default: all)
46
44
  -o, --override Overwrite existing files in target
47
45
  -y, --yes Run unattended (answer yes to all prompts)
48
- -h, --help Print this help`;
46
+ --no-native-workflow Do not grant the unscoped "Workflow" permission (see below)
47
+ -h, --help Print this help
49
48
 
50
- let targetDir = ".", yes = false, override = false, clis = [...ALL_CLIS];
49
+ The grant this writes is two Bash rules for the harness kernel, plus unless
50
+ --no-native-workflow is given — the "Workflow" token that lets the tech-lead
51
+ launch its run script without approving each launch. That token is UNSCOPED: it
52
+ authorises every dynamic workflow script in the project, not only this plugin's.
53
+ Declining it leaves the harness fully functional in an interactive session; only
54
+ the unattended lane needs the pre-approval.`;
55
+
56
+ let targetDir = ".", yes = false, override = false, nativeWorkflow = true;
51
57
  const positional = [];
52
58
  for (let i = 0; i < argv.length; i++) {
53
59
  const a = argv[i];
54
60
  if (a === "-d" || a === "--directory") targetDir = argv[++i];
55
61
  else if (a === "-y" || a === "--yes") yes = true;
56
62
  else if (a === "-o" || a === "--override") override = true;
57
- else if (a === "--cli") {
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); }
63
+ else if (a === "--no-native-workflow") nativeWorkflow = false;
64
+ else if (a === "-h" || a === "--help") { console.log(usage); process.exit(0); }
63
65
  else if (a.startsWith("-")) { console.error(`Unknown option: ${a}\n${usage}`); process.exit(1); }
64
66
  else positional.push(a);
65
67
  }
@@ -71,7 +73,6 @@ if (positional.length && positional[0] !== "init") {
71
73
  const target = resolve(targetDir);
72
74
  if (!existsSync(target)) { console.error(`Target directory does not exist: ${target}`); process.exit(1); }
73
75
  console.log(`Installing Shape Up SDLC Harness into: ${target}`);
74
- console.log(`CLIs: ${clis.join(", ")}`);
75
76
 
76
77
  // ---- confirmation -----------------------------------------------------------
77
78
  if (!yes) {
@@ -116,21 +117,17 @@ if (!existsSync(agentsSrc)) {
116
117
  }
117
118
  }
118
119
 
119
- // ---- 1. per-CLI install -----------------------------------------------------
120
- for (const cli of clis) {
121
- if (cli === "claude") installClaude();
122
- else replaceSkills(cli);
123
- }
120
+ // ---- 1. Claude Code install -------------------------------------------------
121
+ installClaude();
122
+ warnIfUntrusted(target);
124
123
 
125
- // ---- 2. wire each CLI to the root AGENTS.md ---------------------------------
126
- if (clis.includes("claude")) ensureAgentImport(join(target, "CLAUDE.md"), "CLAUDE.md", "claude");
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");
124
+ // ---- 2. wire Claude Code to the root AGENTS.md ------------------------------
125
+ ensureAgentImport(join(target, "CLAUDE.md"), "CLAUDE.md");
129
126
 
130
127
  // ---- 3. .gitignore ----------------------------------------------------------
131
- // Both roots are listed, deliberately. A project may be mid-migration (0006 moves `.shapeup/`
132
- // to `.shapeup/`), and a run trace committed by accident during that window is exactly the mistake
133
- // the tier split exists to prevent. Ignoring a directory that does not exist costs nothing.
128
+ // Both roots are listed, deliberately. A project may still be carrying the pre-ADR-0001 local root
129
+ // alongside the current one, and a run trace committed by accident from either is exactly the
130
+ // mistake the tier split exists to prevent. Ignoring a directory that does not exist costs nothing.
134
131
  const GITIGNORE_RULE = `# Shape Up SDLC run workspace
135
132
  ${LOCAL}/
136
133
  ${LEGACY_LOCAL}/
@@ -162,9 +159,23 @@ for (const [srcRel, note] of [
162
159
  const src = join(PKG_ROOT, srcRel);
163
160
  if (existsSync(src)) {
164
161
  const dst = join(target, srcRel);
165
- mkdirSync(dirname(dst), { recursive: true });
166
- cpSync(src, dst);
167
- console.log(`Installed ${srcRel} (${note})`);
162
+ // Self-install (the target IS the plugin's own checkout — dogfooding, or a worktree loaded
163
+ // with --plugin-dir and initialized against itself) makes src and dst the same file. cpSync
164
+ // throws ERR_FS_CP_EINVAL on that rather than treating "already there" as done, which crashed
165
+ // the installer AFTER its one load-bearing step (the permission grant, step 2) had already
166
+ // succeeded — so the failure looked worse than it was, but it was still a crash. A plain
167
+ // string compare isn't enough: `target` can reach the same file through a symlinked temp root
168
+ // (macOS's `/tmp` -> `/private/tmp`, `/var/folders/...`) that `PKG_ROOT` — resolved from
169
+ // `import.meta.url`, which Node already canonicalizes — does not share textually. Comparing
170
+ // real paths is what makes the check hold under both forms.
171
+ const samePath = existsSync(dst) && realpathSync(src) === realpathSync(dst);
172
+ if (src === dst || samePath) {
173
+ console.log(`${srcRel} already present at the install target (self-install) — nothing to copy`);
174
+ } else {
175
+ mkdirSync(dirname(dst), { recursive: true });
176
+ cpSync(src, dst);
177
+ console.log(`Installed ${srcRel} (${note})`);
178
+ }
168
179
  }
169
180
  }
170
181
 
@@ -193,7 +204,7 @@ function installClaude() {
193
204
  : add;
194
205
  if (ins.status === 0) {
195
206
  // The CLI registers the marketplace and enables the plugin. It does NOT know about the
196
- // pipeline permission grant, so this path has to add it — and until v1.6.1 it did not,
207
+ // kernel permission grant, so this path has to add it — and until v1.6.1 it did not,
197
208
  // while the comment below claimed both paths merged it. Measured on a fresh `npx
198
209
  // shapeup-sdlc init`: `permissions.allow` came out EMPTY on every machine with the claude
199
210
  // CLI installed, which is the common case and the one that prints success. That is FC-02
@@ -207,13 +218,13 @@ function installClaude() {
207
218
  try { written = JSON.parse(readFileSync(settingsFile, "utf8")); }
208
219
  catch (e) {
209
220
  console.error(` [claude] plugin installed, but ${rel(settingsFile)} is not valid JSON (${e.message}) —`);
210
- console.error(" the pipeline permission grant was NOT added. Copy it from .claude/settings.local.example.json.");
221
+ console.error(" the kernel permission grant was NOT added. Copy it from .claude/settings.local.example.json.");
211
222
  return;
212
223
  }
213
224
  }
214
225
  mergePipelinePermissions(written);
215
226
  writeFileSync(settingsFile, JSON.stringify(written, null, 2) + "\n");
216
- console.log(" [claude] plugin installed at project scope + pipeline permissions granted — run /reload-plugins to activate in a live session");
227
+ console.log(" [claude] plugin installed at project scope + kernel permissions granted — run /reload-plugins to activate in a live session");
217
228
  return;
218
229
  }
219
230
  console.log(" [claude] Warning: claude CLI failed — falling back to writing settings.json directly");
@@ -237,93 +248,67 @@ function installClaude() {
237
248
  settings.enabledPlugins[PLUGIN_KEY] = true;
238
249
  mergePipelinePermissions(settings);
239
250
  writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
240
- console.log(` [claude] merged marketplace + plugin + pipeline permissions into ${rel(settingsFile)}`);
251
+ console.log(` [claude] merged marketplace + plugin + kernel permissions into ${rel(settingsFile)}`);
241
252
  console.log(" [claude] the plugin auto-enables on the next session opened in this directory");
242
253
  }
243
254
 
244
255
  /**
245
- * Pre-approve the harness's OWN pipeline scripts, and nothing else.
246
- *
247
- * WHY THIS EXISTS (measured, on this project's own benchmark).
248
- *
249
- * Every load-bearing step of a run is a Node script that ships with the plugin and therefore
250
- * lives OUTSIDE the project — `${CLAUDE_PLUGIN_ROOT}/skills/**\/scripts/*.mjs`. Under any
251
- * permission mode short of `bypassPermissions`, executing a script from outside the working
252
- * directory needs approval. In an interactive session you click once and forget it. In a headless
253
- * one there is nobody to click, and the run cannot take its first step.
256
+ * Pre-approve the harness's OWN kernel, and nothing else.
254
257
  *
255
- * That is not hypothetical. On `sdd-harness-bench`, the run receipt step (`init-run.mjs`) was
256
- * 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 came back "This command requires approval".
258
- * The agent eventually gave up on the harness and built the feature by hand. It is the failure the
259
- * receipt was designed to make visible, arriving through the door the receipt itself opened.
258
+ * WHY THIS EXISTS. Every deterministic step of a run is a Node subcommand that ships with the
259
+ * plugin and therefore lives OUTSIDE the project. Under any permission mode short of
260
+ * `bypassPermissions`, executing a script from outside the working directory needs approval. In an
261
+ * interactive session you click once and forget it. In a headless one there is nobody to click,
262
+ * and the run cannot take its first step — measured, without a working grant, as the receipt step
263
+ * being attempted six different ways in one session and denied every time, after which the agent
264
+ * abandons the harness and builds the feature by hand.
260
265
  *
261
- * Scope is deliberately narrow: `node <plugin>/skills/.../scripts/*.mjs`, by prefix. This grants
262
- * the harness the right to run its own deterministic, dependency-free, network-free scripts. It
263
- * grants no general `Bash(node:*)`, which would be a much larger ask for a much smaller reason.
264
- *
265
- * BOTH SPELLINGS ARE GRANTED, and that is the point of v1.5's leg-2 fix. The skills now write
266
- * every invocation in the QUOTED literal form — `node "${CLAUDE_PLUGIN_ROOT}/skills/…"` — because
267
- * the unquoted form breaks the moment the plugin is installed under a path with a space in it
268
- * (`~/Library/Application Support/…`), which `lib/is-main.mjs` documents as a measured case, not a
269
- * hypothetical. A prefix rule is a literal string match, so the quote character would otherwise
270
- * put every call site back outside the grant — the exact mismatch this fix exists to remove. The
271
- * unquoted prefix stays for older prose and for anything a user has already typed.
272
- *
273
- * `tests/structural/14-invocation-paths.mjs` asserts that every documented call site is in a form
274
- * one of these prefixes actually matches, so the two can never drift apart again.
266
+ * The rule form and the measurements behind it live in `bin/lib/grant.mjs`. It is a separate module
267
+ * so the structural suite can IMPORT the generator instead of regex-parsing this file for the rule
268
+ * shape the proxy that let a grant matching no command at all ship green for three releases.
275
269
  *
276
270
  * @param {object} settings - Parsed settings.json, mutated in place.
277
271
  * @returns {void}
278
272
  */
279
273
  function mergePipelinePermissions(settings) {
280
- const OWNERS = ["tech-lead", "ba-pitch-analyzer", "spec-evaluator"];
281
- const PREFIXES = OWNERS.flatMap((o) => [
282
- `node \${CLAUDE_PLUGIN_ROOT}/skills/${o}/scripts/`,
283
- `node "\${CLAUDE_PLUGIN_ROOT}/skills/${o}/scripts/`,
284
- ]);
285
- settings.permissions = settings.permissions || {};
286
- const allow = new Set(settings.permissions.allow || []);
287
- for (const p of PREFIXES) allow.add(`Bash(${p}:*)`);
288
- settings.permissions.allow = [...allow];
289
- }
290
-
291
- function replaceSkills(cli) {
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++;
274
+ mergeGrant(settings, { nativeWorkflow });
275
+ if (!nativeWorkflow) {
276
+ console.log(` [claude] --no-native-workflow: "${WORKFLOW_RULE}" not granted — launch the run`);
277
+ console.log(" script interactively and approve it, or re-run init without the flag.");
302
278
  }
303
- console.log(` [${cli}] ${n} skills replaced in ${rel(dest)}`);
279
+ }
304
280
 
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
- }
281
+ /**
282
+ * Say so when the grant we just wrote will be ignored anyway.
283
+ *
284
+ * Writing a correct rule into an untrusted workspace produces no error at install, no error at
285
+ * session start, and a denial at the first dispatch — the same invisible failure shape that let a
286
+ * grant matching no command at all ship for three releases. The one thing this project cannot
287
+ * afford is another enforcement point that is silent when it is not working, so this prints.
288
+ *
289
+ * It reports rather than repairs: trusting a directory authorises executing code from it, which is
290
+ * the user's call, not a decision for a package running under `npx`.
291
+ *
292
+ * @param {string} projectDir - The directory just installed into.
293
+ * @returns {void}
294
+ */
295
+ function warnIfUntrusted(projectDir) {
296
+ const trusted = isWorkspaceTrusted(projectDir);
297
+ if (trusted === true || trusted === null) return;
298
+ console.log("");
299
+ console.log(" [claude] ⚠ This workspace is not trusted yet, so Claude Code will IGNORE the");
300
+ console.log(" permission grant just written and the harness will stop at its first");
301
+ console.log(" dispatch. Fix it in one of two ways:");
302
+ console.log(" • open Claude Code here interactively once and accept the trust prompt, or");
303
+ console.log(` • set projects[${JSON.stringify(resolve(projectDir))}].hasTrustDialogAccepted`);
304
+ console.log(" to true in ~/.claude.json (this is what a CI image should bake in).");
316
305
  }
317
306
 
318
- function ensureAgentImport(file, label, mode) {
307
+ function ensureAgentImport(file, label) {
319
308
  mkdirSync(dirname(file), { recursive: true });
320
309
  if (!existsSync(file)) writeFileSync(file, "");
321
- if (mode === "claude") {
322
- if (!readFileSync(file, "utf8").includes("@AGENTS.md")) {
323
- appendFileSync(file, "\n@AGENTS.md\n");
324
- console.log(`Appended @AGENTS.md import tag to ${label}`);
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
- }
310
+ if (!readFileSync(file, "utf8").includes("@AGENTS.md")) {
311
+ appendFileSync(file, "\n@AGENTS.md\n");
312
+ console.log(`Appended @AGENTS.md import tag to ${label}`);
313
+ } else console.log(`@AGENTS.md import tag already present in ${label}`);
329
314
  }
@@ -0,0 +1,145 @@
1
+ // grant — the permission rules that let the harness run its own kernel.
2
+ //
3
+ // WHY THIS IS ITS OWN MODULE. The installer writes these rules and the structural suite has to
4
+ // check them. Checking them by regex-parsing `bin/init.mjs`'s source is how a grant that matched
5
+ // no command at all once stayed green for three releases: the test re-derived what it expected
6
+ // instead of asking the code what it emits. Exporting the generator is the fix — every caller gets
7
+ // the same strings from the same function, or fails.
8
+ //
9
+ // HOW A BASH RULE ACTUALLY MATCHES (measured 2026-08-14 against Claude Code 2.1.232; every row was
10
+ // a real session whose verdict was whether the target script's marker file landed on disk, never
11
+ // what the model said about it). There are TWO rule syntaxes and they do not behave alike:
12
+ //
13
+ // Bash(<prefix>:*) PREFIX match. Compared literally; a `*` inside the prefix is an ordinary
14
+ // asterisk. Matching is at COMPLETE ARGUMENT BOUNDARIES — the command must
15
+ // equal the prefix or begin with `<prefix> `. A prefix ending mid-argument
16
+ // (`…/scripts/:*`) therefore grants NOTHING.
17
+ // Bash(<pattern> *) GLOB match, anchored end to end. Here `*` expands, and it crosses `/`.
18
+ //
19
+ // Two further facts shape what we emit:
20
+ // - A rule's `${CLAUDE_PLUGIN_ROOT}` is NOT expanded. Rules are read from the user's project
21
+ // settings, where that token has no meaning; measured DENIED against an expanded command.
22
+ // - A SKILL's `${CLAUDE_PLUGIN_ROOT}` IS expanded, at skill-load time, before the model reads it.
23
+ // So the command that reaches the matcher already carries an absolute, quoted path.
24
+ //
25
+ // WHAT WE EMIT — the whole grant, two lines:
26
+ //
27
+ // Bash(node "*/kernel/harness.mjs" *)
28
+ // Bash(node "*/kernel/harness.mjs")
29
+ //
30
+ // - GLOB syntax, not prefix, because the installer CANNOT know the path the rule must match.
31
+ // `npx shapeup-sdlc init` runs from an npm tarball; Claude Code loads the plugin from
32
+ // `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/`. A glob spans both, and the `*`
33
+ // is what makes the grant survive a plugin UPGRADE — an exact path would silently un-grant the
34
+ // harness on every version bump.
35
+ // - The QUOTED form is the one the skills emit, so an install path containing a space does not
36
+ // break the command. Closing the quote INSIDE the rule is what makes it match.
37
+ // - TWO rules, because the trailing ` *` requires at least one argument: a bare
38
+ // `node "<path>"` is DENIED by the ` *` form and needs the bare form as well.
39
+ // - The root is a bare `*`, NOT anchored on the plugin's directory name. Anchoring breaks every
40
+ // development and local install (a `--plugin-dir` checkout is named whatever the user cloned
41
+ // it to) and buys no security: anyone able to plant a script at `/tmp/x/kernel/harness.mjs`
42
+ // can equally plant one at `/tmp/x/shapeup-sdlc-plugin/9.9.9/kernel/harness.mjs`.
43
+ //
44
+ // WHY TWO LINES AND NOT FORTY. Through v1.8 the grant enumerated every pipeline script — two rules
45
+ // each, regenerated whenever a script was added, renamed or removed, and silently wrong whenever
46
+ // that regeneration was missed. v2.0 gives the deterministic half of the harness ONE executable
47
+ // (`kernel/harness.mjs`, subcommands beneath it), so the grant is a constant a person can read and
48
+ // verify by eye. Scope is unchanged in kind and narrower in fact: one dependency-free, network-free
49
+ // script that ships with the plugin. It grants no general `Bash(node:*)`.
50
+ //
51
+ // THE WORKFLOW GRANT is separate and optional (see {@link workflowRule}). The `Workflow` permission
52
+ // token is UNSCOPED — it authorises every dynamic workflow script in the project, not just this
53
+ // plugin's — so the installer states that plainly and lets `--no-native-workflow` decline it. A
54
+ // project that declines runs the harness interactively, approving the launch each time.
55
+
56
+ import { existsSync, readFileSync } from "node:fs";
57
+ import { join, resolve } from "node:path";
58
+ import { homedir } from "node:os";
59
+
60
+ /** The kernel's path inside the installed plugin — the one executable the grant has to cover. */
61
+ export const KERNEL_ENTRY = "kernel/harness.mjs";
62
+
63
+ /**
64
+ * The unscoped permission token that lets the tech-lead launch its Workflow script.
65
+ *
66
+ * Opt-in at install. Named here rather than spelled at each call site so the installer, the docs
67
+ * check and the structural suite all mean the same string.
68
+ */
69
+ export const WORKFLOW_RULE = "Workflow";
70
+
71
+ /**
72
+ * The `permissions.allow` rules for the harness kernel.
73
+ *
74
+ * Constant — deliberately not derived from the filesystem. A grant computed from a directory
75
+ * listing is a grant that changes silently when the listing does; with one entry point there is
76
+ * nothing left to enumerate.
77
+ *
78
+ * @returns {string[]} The two Bash rules, sorted.
79
+ */
80
+ export function pipelineRules() {
81
+ const pattern = `node "*/${KERNEL_ENTRY}"`;
82
+ return [`Bash(${pattern} *)`, `Bash(${pattern})`].sort();
83
+ }
84
+
85
+ /**
86
+ * True for a rule this plugin has ever written and no longer wants in a user's settings.
87
+ *
88
+ * Two generations are purged: the v1.5–v1.8 prefix rules (which granted nothing), and the
89
+ * per-script glob rules v1.8 replaced them with (superseded by the single kernel entry point).
90
+ * Leaving either behind turns a user's settings into a museum of dead grants and hides which rules
91
+ * are live, so the installer purges rather than merging alongside.
92
+ *
93
+ * @param {string} rule - One entry from `permissions.allow`.
94
+ * @returns {boolean} Whether the installer should drop it.
95
+ */
96
+ export function isSupersededRule(rule) {
97
+ return /^Bash\(node "?\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/[a-z-]+\/scripts\/:\*\)$/.test(rule)
98
+ || /^Bash\(node "\*\/skills\/[a-z-]+\/scripts\/[\w.-]+\.mjs"( \*)?\)$/.test(rule);
99
+ }
100
+
101
+ /**
102
+ * Whether Claude Code will honour a PROJECT-scoped grant in this directory at all.
103
+ *
104
+ * A third failure layer, above the two in the banner, and the one that bites hardest in exactly the
105
+ * case the grant exists for. Measured 2026-08-14 (CC 2.1.232): in an untrusted workspace the CLI
106
+ * prints `Ignoring N permissions.allow entries from .claude/settings.json: this workspace has not
107
+ * been trusted.` and drops every one of them. A fresh clone in CI is untrusted by definition, so a
108
+ * perfectly correct rule set still grants nothing there. `-p` skips the trust *dialog*; it does not
109
+ * confer trust.
110
+ *
111
+ * This returns a fact, not a fix. The installer reports it; it deliberately does NOT write the
112
+ * trust flag, because trusting a directory is a decision about executing code from it and belongs
113
+ * to the person, not to a package running under `npx`.
114
+ *
115
+ * @param {string} projectDir - The project directory being installed into.
116
+ * @returns {(boolean|null)} True/false when `~/.claude.json` is readable, null when it is not.
117
+ */
118
+ export function isWorkspaceTrusted(projectDir) {
119
+ const cfg = join(homedir(), ".claude.json");
120
+ if (!existsSync(cfg)) return null;
121
+ try {
122
+ const projects = JSON.parse(readFileSync(cfg, "utf8"))?.projects || {};
123
+ return projects[resolve(projectDir)]?.hasTrustDialogAccepted === true;
124
+ } catch { return null; }
125
+ }
126
+
127
+ /**
128
+ * Merge the harness grant into a parsed settings object, in place.
129
+ *
130
+ * @param {object} settings - Parsed settings.json.
131
+ * @param {object} [opts] - Options.
132
+ * @param {boolean} [opts.nativeWorkflow=true] - Also grant the unscoped `Workflow` token, which is
133
+ * what lets the tech-lead launch its run script without a per-launch approval. `false` removes it
134
+ * if a previous install added it, so `--no-native-workflow` is a real opt-out and not a no-op.
135
+ * @returns {void}
136
+ */
137
+ export function mergePipelinePermissions(settings, { nativeWorkflow = true } = {}) {
138
+ settings.permissions = settings.permissions || {};
139
+ const allow = new Set(settings.permissions.allow || []);
140
+ for (const r of [...allow]) if (isSupersededRule(r)) allow.delete(r);
141
+ for (const r of pipelineRules()) allow.add(r);
142
+ if (nativeWorkflow) allow.add(WORKFLOW_RULE);
143
+ else allow.delete(WORKFLOW_RULE);
144
+ settings.permissions.allow = [...allow].sort();
145
+ }
package/commands/build.md CHANGED
@@ -12,3 +12,23 @@ 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
+ ```
23
+ Workflow({
24
+ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js",
25
+ args: <the RunArgs object>
26
+ })
27
+ ```
28
+
29
+ Reach it through `/ship` (or the `tech-lead` skill), which opens the run properly — `harness init run`
30
+ first, so the receipt exists. Do not hand-roll the round by calling this command once per task: the
31
+ attempt loop, the T0 ratchet and the breakers are branches in that script, not steps a caller can
32
+ be trusted to reproduce, and a session that rebuilds them by hand is the prose lane the cutover
33
+ replaced. On a `--tiny` run or a spec with no committed `scopes/*.md`, the prose loop in
34
+ `skills/tech-lead/references/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 gated: a PreToolUse hook (GATE L2) will **deny** the dispatch while any task on
13
- the board is unfinished, naming the offenders. If that happens, the correct response is to
14
- route back to `/build` and finish them do not retry the eval, do not argue with the hook, and
15
- do not use `--task` as a loophole to simulate a round verdict piecemeal.
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.
@@ -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`, `generate-board`, `reconcile`,
9
- `retrofit-surface`, or `coverage` (which writes the shared `requirements.md` registry).
10
- 2. Then use the **scope-architect** skill to write the committed scope contracts
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 `pitch.md` 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
@@ -8,22 +8,59 @@ sign-off — especially the Ship gate (L4). The harness's safety depends on the
8
8
  loop; do not skip gates by default.
9
9
 
10
10
  **Before anything else, dispatch `tech-lead` and let it open the run** — its first action is
11
- `scripts/init-run.mjs`, which writes the run receipt. Do not summarise what the harness will do;
11
+ `harness init run`, which writes the run receipt. Do not summarise what the harness will do;
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
+ ```
22
+ Workflow({
23
+ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js",
24
+ args: <the RunArgs object>
25
+ })
26
+ ```
27
+
28
+ ORIENT → L1a → ANALYZE → WIRE → L1a.5 → MAP SCOPES → L1b → rounds of BUILD/L2/EVAL → QA → GATE H
29
+ all run inside it. Three things follow, and they are the point of the cutover rather than trivia:
30
+
31
+ - **A gate pause is a return value, not a stop.** The launch returns `{status: "paused", paused_at,
32
+ block}`; emit `block` **verbatim**, get the PO's decision, write it to
33
+ `.shapeup/<slug>/gate-answers.json`, and **relaunch the same call with the same args**. The
34
+ fast-forward re-derives position from disk and re-dispatches nothing already finished.
35
+ - **A killed session loses nothing.** Resume state comes off disk, never from context, so a fresh
36
+ session picks the run up where it died — the property the whole launch shape exists to buy.
37
+ - **The launch is the `Workflow` tool**, which is what gives the run resume-from-journal, worktree
38
+ isolation, and sub-agents that share the session's prompt cache instead of paying a cold start
39
+ each. `npx shapeup-sdlc init` writes the `"Workflow"` grant it needs. That grant is unscoped — it
40
+ authorises every dynamic workflow script in the project — so an install may decline it with
41
+ `--no-native-workflow`, in which case the launch asks for approval once per session and the
42
+ unattended lane is unavailable.
43
+
44
+ `--tiny`, and any spec with no committed `scopes/*.md` yet, take the unchanged prose lane in
45
+ `skills/tech-lead/references/protocol.md` instead — non-regression, by design.
46
+
15
47
  Only run headless/auto if the user explicitly asks for it in their message:
16
48
  - `--auto` → advance low-risk gates automatically, still pause at L4 (Ship sign-off).
17
49
  Implies `--gate-answers guarded` unless a set is named.
18
50
  - `--unattended` → fully headless, `max_rounds 3`. Intended for CI, not day-to-day local runs.
19
51
  Implies `--gate-answers ci` unless a set is named.
52
+ **In a `claude -p` invocation, set `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` first.** Print mode
53
+ terminates a session's background tasks after 600 s, and the entire pipeline is one background
54
+ launch, so without it the run is killed roughly ten minutes in — mid-phase, with no error beyond
55
+ the CLI's own "background tasks still running; terminating". Resume state is on disk, so a
56
+ relaunch fast-forwards past whatever finished; the cost is a relaunch, not the run.
20
57
  **Typing the flag IS the confirmation — do not stop to ask for another one.** Emit the warning
21
58
  that no human will review the verdict before ship as the run's first line, then proceed straight
22
59
  into GATE L0 in the same turn.
23
60
 
24
61
  > 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, a benchmark probe)
26
- > there is no second turn in which to answer, so the run spent its turn requesting permission and
62
+ > only job it has. In a non-interactive invocation (`claude -p …`, a CI step) there is no second
63
+ > turn in which to answer, so the run spent its turn requesting permission and
27
64
  > exited having written nothing. A headless flag that cannot complete a headless run is a defect,
28
65
  > not a safety feature — and the warning, which is the part that carries the safety value, is
29
66
  > still printed. `--auto` remains the middle setting that pauses at L4.
@@ -37,14 +74,23 @@ Additional flags, pass through to `tech-lead` only when the user names them:
37
74
  - `--gate-answers <ci|guarded|interactive|path.json>` → the pre-recorded PO decisions this run
38
75
  crosses its gates with. Gates still emit their blocks and still record a decision; the
39
76
  decision's **source** becomes the answer set instead of a live human, and the ledger says so.
40
- Generate one with `gate-answers.mjs --init --preset ci --by "<name>"`. This is what makes a
77
+ Generate one with `harness gate --init --preset ci --by "<name>"`. This is what makes a
41
78
  headless lane finish: without it an unattended run waits at the first ⏸ until the wall-clock
42
- budget expires (measured: a benchmark DNF at 1800s on a feature the control finished in 51s).
79
+ budget expires, having built nothing.
43
80
  - `--wall-clock-budget <seconds>` → arm the deadline breaker. Off by default. Set it in any lane
44
- with a hard clock (CI, a benchmark, an overnight run) and set it *below* the external kill, so
45
- the harness trips its own breaker first: past the deadline `hooks/gate-deadline.mjs` denies new
81
+ with a hard clock (CI, an overnight run) and set it *below* the external kill, so
82
+ the harness trips its own breaker first: past the deadline `harness verify budget` denies new
46
83
  `task-executor` work and routes to GATE H, where scope-hammer ships whatever is green. A run
47
84
  killed from outside ships nothing — including the scopes that already passed T0.
85
+ - `--parallel-scopes N` → how many scopes build at once (default 4). Concurrency is a cost
86
+ question before it is a speed one — every extra leg is another worker's full context — so this
87
+ is a dial rather than a constant. `1` builds scopes one at a time, which is the setting for a
88
+ project whose workers are not safe to run beside each other.
89
+ - `--adversarial-verify` → before a FAIL finding costs a whole fix round, spend one independent
90
+ skeptic on it, prompted to refute and to default to "real" when it cannot. Off by default; it
91
+ does not touch the single-judge invariant, because a refutation retracts a finding rather than
92
+ issuing a verdict.
93
+ - `--no-qa` → skip the QA Edge Hunt. QA is a level-up, not a gate.
48
94
  - `--rounds N` → override the outer circuit breaker (build+eval cycles, default 3).
49
95
  - `--attempts N` → override the inner circuit breaker (per-scope T0 attempts, default 5;
50
96
  no-op on specs without scope contracts).
package/commands/wire.md CHANGED
@@ -5,7 +5,7 @@ Use the **solution-architect** skill (operation `wire`) on $ARGUMENTS.
5
5
 
6
6
  This is gate L1a.5 — it front-loads the integration seam so no engine ships orphaned. The skill
7
7
  is the sole writer of the committed `wiring-map.md`, resolved against `project-profile.md`'s
8
- `entry_point`; `trace-lint.mjs` later checks reachability against it.
8
+ `entry_point`; `harness verify trace` later checks reachability against it.
9
9
 
10
10
  It needs the spec folder (for the use cases) and the project profile. If either is missing, say
11
11
  which one rather than inventing it.