shapeup-sdlc 1.7.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.example.json +5 -5
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +17 -3
- package/README.md +124 -103
- package/SECURITY.md +37 -30
- package/bin/init.mjs +77 -47
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +7 -6
- package/commands/ship.md +29 -16
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +3 -3
- package/hooks/gate-zerowork.mjs +25 -24
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +19 -8
- package/hooks/safety-spine.mjs +3 -3
- package/hooks/sandbox-guard.mjs +84 -40
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
- package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
- package/kernel/lib/paths.mjs +491 -0
- package/kernel/probe/concurrency.mjs +510 -0
- package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +14 -9
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
- package/kernel/reduce/graph.mjs +437 -0
- package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
- package/kernel/reduce/ingest.mjs +633 -0
- package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
- package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
- package/kernel/report/export.mjs +321 -0
- package/kernel/report/facts.mjs +209 -0
- package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
- package/kernel/verify/dispatch.mjs +114 -0
- package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
- package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
- package/kernel/verify/skills.mjs +125 -0
- package/kernel/verify/spec.mjs +559 -0
- package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +11 -9
- package/skills/ba-pitch-analyzer/SKILL.md +5 -5
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
- package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
- package/skills/qa-edge-hunter/SKILL.md +7 -3
- package/skills/scope-architect/SKILL.md +40 -6
- package/skills/solution-architect/SKILL.md +19 -4
- package/skills/spec-evaluator/SKILL.md +12 -2
- package/skills/task-executor/SKILL.md +7 -6
- package/skills/tech-lead/SKILL.md +50 -38
- package/skills/tech-lead/references/gates.md +71 -20
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +341 -66
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +1 -1
- package/skills/tech-lead/workflows/shapeup-run.js +1327 -750
- package/hooks/anti-rationalization.mjs +0 -238
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -147
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -108
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
- package/skills/spec-evaluator/README.md +0 -93
- package/skills/tech-lead/README.md +0 -71
- package/skills/tech-lead/references/delegation.md +0 -261
- package/skills/tech-lead/references/hard-rules.md +0 -34
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -213
- package/skills/tech-lead/references/round-protocol.md +0 -234
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
- package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
- package/skills/translator/README.md +0 -66
package/bin/init.mjs
CHANGED
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
// AGENTS.md harness block · Claude Code plugin (CLI or settings.json merge) ·
|
|
21
21
|
// CLAUDE.md @AGENTS.md import · .gitignore rules · shapeup/metrics/ · Tier C templates
|
|
22
22
|
|
|
23
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, appendFileSync } from "node:fs";
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, appendFileSync, realpathSync } from "node:fs";
|
|
24
24
|
import { resolve, join, dirname } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { createInterface } from "node:readline";
|
|
28
|
-
import { LOCAL, LEGACY, metricsDir } from "../
|
|
28
|
+
import { LOCAL, LEGACY, metricsDir } from "../kernel/lib/paths.mjs";
|
|
29
|
+
import { mergePipelinePermissions as mergeGrant, isWorkspaceTrusted, WORKFLOW_RULE } from "./lib/grant.mjs";
|
|
29
30
|
|
|
30
31
|
/** The root a project migrating off the pre-ADR-0001 layout may still be carrying. */
|
|
31
32
|
const LEGACY_LOCAL = LEGACY.local;
|
|
@@ -42,15 +43,24 @@ Options:
|
|
|
42
43
|
-d, --directory <path> Target project directory (default: current directory)
|
|
43
44
|
-o, --override Overwrite existing files in target
|
|
44
45
|
-y, --yes Run unattended (answer yes to all prompts)
|
|
45
|
-
|
|
46
|
+
--no-native-workflow Do not grant the unscoped "Workflow" permission (see below)
|
|
47
|
+
-h, --help Print this help
|
|
46
48
|
|
|
47
|
-
|
|
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;
|
|
48
57
|
const positional = [];
|
|
49
58
|
for (let i = 0; i < argv.length; i++) {
|
|
50
59
|
const a = argv[i];
|
|
51
60
|
if (a === "-d" || a === "--directory") targetDir = argv[++i];
|
|
52
61
|
else if (a === "-y" || a === "--yes") yes = true;
|
|
53
62
|
else if (a === "-o" || a === "--override") override = true;
|
|
63
|
+
else if (a === "--no-native-workflow") nativeWorkflow = false;
|
|
54
64
|
else if (a === "-h" || a === "--help") { console.log(usage); process.exit(0); }
|
|
55
65
|
else if (a.startsWith("-")) { console.error(`Unknown option: ${a}\n${usage}`); process.exit(1); }
|
|
56
66
|
else positional.push(a);
|
|
@@ -109,6 +119,7 @@ if (!existsSync(agentsSrc)) {
|
|
|
109
119
|
|
|
110
120
|
// ---- 1. Claude Code install -------------------------------------------------
|
|
111
121
|
installClaude();
|
|
122
|
+
warnIfUntrusted(target);
|
|
112
123
|
|
|
113
124
|
// ---- 2. wire Claude Code to the root AGENTS.md ------------------------------
|
|
114
125
|
ensureAgentImport(join(target, "CLAUDE.md"), "CLAUDE.md");
|
|
@@ -148,9 +159,23 @@ for (const [srcRel, note] of [
|
|
|
148
159
|
const src = join(PKG_ROOT, srcRel);
|
|
149
160
|
if (existsSync(src)) {
|
|
150
161
|
const dst = join(target, srcRel);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
179
|
}
|
|
155
180
|
}
|
|
156
181
|
|
|
@@ -179,7 +204,7 @@ function installClaude() {
|
|
|
179
204
|
: add;
|
|
180
205
|
if (ins.status === 0) {
|
|
181
206
|
// The CLI registers the marketplace and enables the plugin. It does NOT know about the
|
|
182
|
-
//
|
|
207
|
+
// kernel permission grant, so this path has to add it — and until v1.6.1 it did not,
|
|
183
208
|
// while the comment below claimed both paths merged it. Measured on a fresh `npx
|
|
184
209
|
// shapeup-sdlc init`: `permissions.allow` came out EMPTY on every machine with the claude
|
|
185
210
|
// CLI installed, which is the common case and the one that prints success. That is FC-02
|
|
@@ -193,13 +218,13 @@ function installClaude() {
|
|
|
193
218
|
try { written = JSON.parse(readFileSync(settingsFile, "utf8")); }
|
|
194
219
|
catch (e) {
|
|
195
220
|
console.error(` [claude] plugin installed, but ${rel(settingsFile)} is not valid JSON (${e.message}) —`);
|
|
196
|
-
console.error(" the
|
|
221
|
+
console.error(" the kernel permission grant was NOT added. Copy it from .claude/settings.local.example.json.");
|
|
197
222
|
return;
|
|
198
223
|
}
|
|
199
224
|
}
|
|
200
225
|
mergePipelinePermissions(written);
|
|
201
226
|
writeFileSync(settingsFile, JSON.stringify(written, null, 2) + "\n");
|
|
202
|
-
console.log(" [claude] plugin installed at project scope +
|
|
227
|
+
console.log(" [claude] plugin installed at project scope + kernel permissions granted — run /reload-plugins to activate in a live session");
|
|
203
228
|
return;
|
|
204
229
|
}
|
|
205
230
|
console.log(" [claude] Warning: claude CLI failed — falling back to writing settings.json directly");
|
|
@@ -223,55 +248,60 @@ function installClaude() {
|
|
|
223
248
|
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
224
249
|
mergePipelinePermissions(settings);
|
|
225
250
|
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
226
|
-
console.log(` [claude] merged marketplace + plugin +
|
|
251
|
+
console.log(` [claude] merged marketplace + plugin + kernel permissions into ${rel(settingsFile)}`);
|
|
227
252
|
console.log(" [claude] the plugin auto-enables on the next session opened in this directory");
|
|
228
253
|
}
|
|
229
254
|
|
|
230
255
|
/**
|
|
231
|
-
* Pre-approve the harness's OWN
|
|
232
|
-
*
|
|
233
|
-
* WHY THIS EXISTS (observed, not theorized).
|
|
256
|
+
* Pre-approve the harness's OWN kernel, and nothing else.
|
|
234
257
|
*
|
|
235
|
-
* Every
|
|
236
|
-
* lives OUTSIDE the project
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
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.
|
|
240
265
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* The agent then gives up on the harness and builds the feature by hand. It is the failure the
|
|
245
|
-
* receipt was designed to make visible, arriving through the door the receipt itself opened.
|
|
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.
|
|
246
269
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
270
|
+
* @param {object} settings - Parsed settings.json, mutated in place.
|
|
271
|
+
* @returns {void}
|
|
272
|
+
*/
|
|
273
|
+
function mergePipelinePermissions(settings) {
|
|
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.");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Say so when the grant we just wrote will be ignored anyway.
|
|
250
283
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* hypothetical. A prefix rule is a literal string match, so the quote character would otherwise
|
|
256
|
-
* put every call site back outside the grant — the exact mismatch this fix exists to remove. The
|
|
257
|
-
* unquoted prefix stays for older prose and for anything a user has already typed.
|
|
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.
|
|
258
288
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
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`.
|
|
261
291
|
*
|
|
262
|
-
* @param {
|
|
292
|
+
* @param {string} projectDir - The directory just installed into.
|
|
263
293
|
* @returns {void}
|
|
264
294
|
*/
|
|
265
|
-
function
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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).");
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
function ensureAgentImport(file, label) {
|
|
@@ -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
|
@@ -19,15 +19,16 @@ This command builds **one task**. A full BUILD round — every scope, the per-sc
|
|
|
19
19
|
T0 verification, the inner circuit breaker, then the single EVAL — is a workflow-script launch, and
|
|
20
20
|
it belongs to the orchestrator:
|
|
21
21
|
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js"
|
|
25
|
-
|
|
22
|
+
```
|
|
23
|
+
Workflow({
|
|
24
|
+
scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js",
|
|
25
|
+
args: <the RunArgs object>
|
|
26
|
+
})
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
Reach it through `/ship` (or the `tech-lead` skill), which opens the run properly — `init
|
|
29
|
+
Reach it through `/ship` (or the `tech-lead` skill), which opens the run properly — `harness init run`
|
|
29
30
|
first, so the receipt exists. Do not hand-roll the round by calling this command once per task: the
|
|
30
31
|
attempt loop, the T0 ratchet and the breakers are branches in that script, not steps a caller can
|
|
31
32
|
be trusted to reproduce, and a session that rebuilds them by hand is the prose lane the cutover
|
|
32
33
|
replaced. On a `--tiny` run or a spec with no committed `scopes/*.md`, the prose loop in
|
|
33
|
-
`skills/tech-lead/references/
|
|
34
|
+
`skills/tech-lead/references/protocol.md` still applies, unchanged and by design.
|
package/commands/ship.md
CHANGED
|
@@ -8,7 +8,7 @@ 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
|
-
`
|
|
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
|
|
|
@@ -18,10 +18,11 @@ On a spec with committed `scopes/*.md` — the common case — `tech-lead` holds
|
|
|
18
18
|
conversation, writes `project-profile.md`, then hands the whole pipeline to a single background
|
|
19
19
|
launch and does not drive it turn by turn:
|
|
20
20
|
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
"${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js"
|
|
24
|
-
|
|
21
|
+
```
|
|
22
|
+
Workflow({
|
|
23
|
+
scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/tech-lead/workflows/shapeup-run.js",
|
|
24
|
+
args: <the RunArgs object>
|
|
25
|
+
})
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
ORIENT → L1a → ANALYZE → WIRE → L1a.5 → MAP SCOPES → L1b → rounds of BUILD/L2/EVAL → QA → GATE H
|
|
@@ -33,23 +34,26 @@ all run inside it. Three things follow, and they are the point of the cutover ra
|
|
|
33
34
|
fast-forward re-derives position from disk and re-dispatches nothing already finished.
|
|
34
35
|
- **A killed session loses nothing.** Resume state comes off disk, never from context, so a fresh
|
|
35
36
|
session picks the run up where it died — the property the whole launch shape exists to buy.
|
|
36
|
-
- **
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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.
|
|
44
43
|
|
|
45
44
|
`--tiny`, and any spec with no committed `scopes/*.md` yet, take the unchanged prose lane in
|
|
46
|
-
`skills/tech-lead/references/
|
|
45
|
+
`skills/tech-lead/references/protocol.md` instead — non-regression, by design.
|
|
47
46
|
|
|
48
47
|
Only run headless/auto if the user explicitly asks for it in their message:
|
|
49
48
|
- `--auto` → advance low-risk gates automatically, still pause at L4 (Ship sign-off).
|
|
50
49
|
Implies `--gate-answers guarded` unless a set is named.
|
|
51
50
|
- `--unattended` → fully headless, `max_rounds 3`. Intended for CI, not day-to-day local runs.
|
|
52
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.
|
|
53
57
|
**Typing the flag IS the confirmation — do not stop to ask for another one.** Emit the warning
|
|
54
58
|
that no human will review the verdict before ship as the run's first line, then proceed straight
|
|
55
59
|
into GATE L0 in the same turn.
|
|
@@ -70,14 +74,23 @@ Additional flags, pass through to `tech-lead` only when the user names them:
|
|
|
70
74
|
- `--gate-answers <ci|guarded|interactive|path.json>` → the pre-recorded PO decisions this run
|
|
71
75
|
crosses its gates with. Gates still emit their blocks and still record a decision; the
|
|
72
76
|
decision's **source** becomes the answer set instead of a live human, and the ledger says so.
|
|
73
|
-
Generate one with `gate
|
|
77
|
+
Generate one with `harness gate --init --preset ci --by "<name>"`. This is what makes a
|
|
74
78
|
headless lane finish: without it an unattended run waits at the first ⏸ until the wall-clock
|
|
75
79
|
budget expires, having built nothing.
|
|
76
80
|
- `--wall-clock-budget <seconds>` → arm the deadline breaker. Off by default. Set it in any lane
|
|
77
81
|
with a hard clock (CI, an overnight run) and set it *below* the external kill, so
|
|
78
|
-
the harness trips its own breaker first: past the deadline `
|
|
82
|
+
the harness trips its own breaker first: past the deadline `harness verify budget` denies new
|
|
79
83
|
`task-executor` work and routes to GATE H, where scope-hammer ships whatever is green. A run
|
|
80
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.
|
|
81
94
|
- `--rounds N` → override the outer circuit breaker (build+eval cycles, default 3).
|
|
82
95
|
- `--attempts N` → override the inner circuit breaker (per-scope T0 attempts, default 5;
|
|
83
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
|
|
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.
|