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/hooks/sandbox-guard.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Sandbox guard — PreToolUse hook (the PA3 countermeasure: writes outside the order's substrate).
|
|
3
3
|
//
|
|
4
|
-
// Blocks Edit/Write/MultiEdit calls that
|
|
5
|
-
//
|
|
6
|
-
// the model cannot talk past.
|
|
4
|
+
// Blocks Edit/Write/MultiEdit calls that no LIVE ORDER's `substrate` block permits. Turns "a worker
|
|
5
|
+
// only writes what the run authorised" from prose into a precondition the model cannot talk past.
|
|
7
6
|
//
|
|
8
|
-
// IT ENFORCES THE ORDER, NOT THE SCOPE CONTRACT, and that is the whole design.
|
|
7
|
+
// IT ENFORCES THE ORDER, NOT THE SCOPE CONTRACT, and that is the whole design. ``harness compile``
|
|
9
8
|
// already stamps a write contract onto every order from `substrateFor(operation)` — allowed,
|
|
10
9
|
// shared, append_only, frozen. Resolving the scope contract instead covered exactly one operation,
|
|
11
10
|
// the build, because only build orders carry a scope; every other dispatch (`analyze`, `wire`,
|
|
@@ -14,12 +13,21 @@
|
|
|
14
13
|
// and the contract the hook enforces the same object, for every operation, with no per-operation
|
|
15
14
|
// code here.
|
|
16
15
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
16
|
+
// IT READS EVERY LIVE ORDER, NOT A POINTER TO ONE. `.shapeup/active-order` still names the run and
|
|
17
|
+
// seeds the search — `harness compile` publishes it as it writes each order, which is what fences
|
|
18
|
+
// the lanes that never reach the workflow (`--tiny`, the prose round loop, a standalone `/build`).
|
|
19
|
+
// But a single pointer cannot survive concurrency: with scopes building side by side the last
|
|
20
|
+
// compile wins the pointer, and a write from scope A is then judged against scope B's contract —
|
|
21
|
+
// a false block or a false permit depending on which way the race fell. So the candidate set is
|
|
22
|
+
// every order under `orders/` with no matching file under `results/` (compiled, not yet ingested),
|
|
23
|
+
// and a write is permitted when SOME live contract covers it.
|
|
21
24
|
//
|
|
22
|
-
//
|
|
25
|
+
// That is the same question as "the writer's own contract" because scope substrates are disjoint by
|
|
26
|
+
// construction — `harness verify spec`'s DISJOINT rule fails a spec where two scopes claim the same
|
|
27
|
+
// path, and it runs at GATE L1b before any build starts. `frozen` is checked across all of them, so
|
|
28
|
+
// a path one scope froze stays frozen while another scope is in flight.
|
|
29
|
+
//
|
|
30
|
+
// Design (deliberately conservative, mirrors the GATE L2 block):
|
|
23
31
|
// • Fail-OPEN whenever there is nothing to enforce: no active-order pointer (not running inside
|
|
24
32
|
// a harness dispatch), pointer names an order that doesn't exist or is unparsable, the order
|
|
25
33
|
// declares no boundaries at all, or the tool call carries no resolvable file path. A guard
|
|
@@ -34,18 +42,18 @@
|
|
|
34
42
|
// execution logs, the P3.7 discovery ledger). Substrate globs whitelist product code and
|
|
35
43
|
// never list the run-trace, so without the carve-out a scoped round leaves its own task files
|
|
36
44
|
// stale. Deliberately narrow: only the active slug's root. The pointers at the `.shapeup/`
|
|
37
|
-
// root — `active-order` (this guard's own) and `active-scope` — sit OUTSIDE
|
|
38
|
-
// construction, so a worker cannot widen its own sandbox by rewriting the
|
|
39
|
-
// it.
|
|
45
|
+
// root — `active-order` (this guard's own) and `active-scope` (the run pointer) — sit OUTSIDE
|
|
46
|
+
// the carve-out by construction, so a worker cannot widen its own sandbox by rewriting the
|
|
47
|
+
// thing that defines it.
|
|
40
48
|
// • Every denial is also appended to the metrics pathology log (telemetry, not just defense).
|
|
41
49
|
//
|
|
42
50
|
// Contract: PreToolUse stdin JSON { tool_name, tool_input:{file_path | edits[].file_path}, cwd }.
|
|
43
51
|
// Deny via { hookSpecificOutput: { hookEventName, permissionDecision:"deny", permissionDecisionReason } }.
|
|
44
52
|
|
|
45
|
-
import { readFileSync, existsSync, appendFileSync, mkdirSync } from "node:fs";
|
|
53
|
+
import { readFileSync, existsSync, appendFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
46
54
|
import { resolve, join, relative, dirname, sep } from "node:path";
|
|
47
|
-
import { isMain } from "../
|
|
48
|
-
import { LOCAL, activeOrder, metricsShard } from "../
|
|
55
|
+
import { isMain } from "../kernel/lib/argv.mjs";
|
|
56
|
+
import { LOCAL, activeOrder, ordersDir, resultsDir, metricsShard } from "../kernel/lib/paths.mjs";
|
|
49
57
|
import { runHook, readStdin, settle } from "./lib/decision.mjs";
|
|
50
58
|
|
|
51
59
|
// --- tiny glob matcher: supports *, **, ? — enough for substrate globs, zero dependencies ---
|
|
@@ -80,6 +88,31 @@ function readJSON(p) {
|
|
|
80
88
|
try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Every order for this run that has been compiled and not yet ingested.
|
|
93
|
+
*
|
|
94
|
+
* "Not yet ingested" is read off the filesystem — an order with a same-named file under `results/`
|
|
95
|
+
* has finished — because that is the only signal that survives a killed session. The pointer's own
|
|
96
|
+
* order is always included, even when its result has landed, so the single-order lane behaves
|
|
97
|
+
* exactly as it did before concurrency existed.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} cwd - Project root.
|
|
100
|
+
* @param {string} slug - The active run's slug.
|
|
101
|
+
* @param {string} pointerOrder - Absolute path of the order the pointer names.
|
|
102
|
+
* @returns {object[]} Parsed orders; unreadable files are skipped, never treated as permissive.
|
|
103
|
+
*/
|
|
104
|
+
function liveOrders(cwd, slug, pointerOrder) {
|
|
105
|
+
const dir = ordersDir(cwd, slug);
|
|
106
|
+
const done = new Set(existsSync(resultsDir(cwd, slug)) ? readdirSync(resultsDir(cwd, slug)) : []);
|
|
107
|
+
const paths = new Set(existsSync(pointerOrder) ? [pointerOrder] : []);
|
|
108
|
+
if (existsSync(dir)) {
|
|
109
|
+
for (const f of readdirSync(dir)) {
|
|
110
|
+
if (f.endsWith(".json") && !done.has(f)) paths.add(join(dir, f));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [...paths].map(readJSON).filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
function extractPaths(toolInput) {
|
|
84
117
|
const paths = [];
|
|
85
118
|
if (toolInput?.file_path) paths.push(toolInput.file_path);
|
|
@@ -118,20 +151,30 @@ async function main() {
|
|
|
118
151
|
const active = readJSON(activeOrderPath);
|
|
119
152
|
if (!active?.slug || !active?.order_path) defer("active-order pointer is unreadable or incomplete", "bad-pointer");
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
// EVERY LIVE ORDER, not just the pointer's. The pointer names one order, and with scopes building
|
|
155
|
+
// CONCURRENTLY the last compile wins it — so a write from scope A would be judged against scope
|
|
156
|
+
// B's contract, which is either a false block or a false permit depending on which way the race
|
|
157
|
+
// fell. Scope substrates are disjoint by construction (spec-lint's DISJOINT rule fails a spec
|
|
158
|
+
// where they are not), so "is this write inside SOME live contract" and "is it inside the
|
|
159
|
+
// writer's own contract" are the same question — and only the first can be asked without a
|
|
160
|
+
// shared mutable pointer.
|
|
161
|
+
//
|
|
162
|
+
// Live = compiled and not yet ingested. An order whose result is on disk has finished; leaving it
|
|
163
|
+
// in the candidate set would keep a finished scope's substrate open for the rest of the run.
|
|
164
|
+
const orders = liveOrders(cwd, active.slug, resolve(cwd, active.order_path));
|
|
165
|
+
if (orders.length === 0) defer(`no live order for ${active.slug}`, "no-order");
|
|
166
|
+
|
|
167
|
+
const withSubstrate = orders.filter((o) => o.substrate);
|
|
168
|
+
if (withSubstrate.length === 0) defer("no live order declares a substrate block", "no-substrate");
|
|
169
|
+
|
|
170
|
+
const contracts = withSubstrate.map((o) => ({
|
|
171
|
+
order_id: o.order_id,
|
|
172
|
+
allowed: [...(o.substrate.allowed || []), ...(o.substrate.shared || [])],
|
|
173
|
+
appendOnly: o.substrate.append_only || [],
|
|
174
|
+
frozen: o.substrate.frozen || [],
|
|
175
|
+
})).filter((c) => c.allowed.length || c.appendOnly.length || c.frozen.length);
|
|
176
|
+
|
|
177
|
+
if (contracts.length === 0) defer("no live order declares write/append/frozen boundaries", "no-whitelist");
|
|
135
178
|
|
|
136
179
|
const targetPaths = extractPaths(p.tool_input);
|
|
137
180
|
if (targetPaths.length === 0) defer("no writable path in the tool input", "no-target");
|
|
@@ -145,19 +188,20 @@ async function main() {
|
|
|
145
188
|
const abs = resolve(cwd, raw);
|
|
146
189
|
const rel = relative(cwd, abs);
|
|
147
190
|
if (rel.startsWith(runTracePrefix)) continue;
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
191
|
+
|
|
192
|
+
// Frozen takes absolute precedence, and it is checked across EVERY live contract: a path one
|
|
193
|
+
// scope froze stays frozen while another scope is in flight, which is the whole point of
|
|
194
|
+
// declaring it.
|
|
195
|
+
const freezer = contracts.find((c) => matchesAny(rel, c.frozen));
|
|
196
|
+
if (freezer) {
|
|
151
197
|
violations.push(rel);
|
|
152
|
-
blockReasons.push(`${rel} is frozen`);
|
|
198
|
+
blockReasons.push(`${rel} is frozen by ${freezer.order_id}`);
|
|
153
199
|
continue;
|
|
154
200
|
}
|
|
155
201
|
|
|
156
|
-
if (matchesAny(rel, allowed))
|
|
157
|
-
continue; // OK
|
|
158
|
-
}
|
|
202
|
+
if (contracts.some((c) => matchesAny(rel, c.allowed))) continue; // inside a live contract
|
|
159
203
|
|
|
160
|
-
if (matchesAny(rel, appendOnly)) {
|
|
204
|
+
if (contracts.some((c) => matchesAny(rel, c.appendOnly))) {
|
|
161
205
|
if (p.tool_name === "Write") {
|
|
162
206
|
violations.push(rel);
|
|
163
207
|
blockReasons.push(`${rel} is append-only (Write overwrites, use Edit)`);
|
|
@@ -166,13 +210,13 @@ async function main() {
|
|
|
166
210
|
}
|
|
167
211
|
|
|
168
212
|
violations.push(rel);
|
|
169
|
-
blockReasons.push(`${rel} is outside allowed scopes`);
|
|
213
|
+
blockReasons.push(`${rel} is outside every live order's allowed scopes`);
|
|
170
214
|
}
|
|
171
215
|
|
|
172
216
|
// Inside the substrate — the "inspected and permitted" row. Previously byte-identical to
|
|
173
217
|
// "this hook never ran", which is how 26 enforcement points sat inert behind 610 green checks.
|
|
174
218
|
if (violations.length === 0) {
|
|
175
|
-
defer(`${targetPaths.length} path(s) inside order substrate — permitted`, "in-substrate");
|
|
219
|
+
defer(`${targetPaths.length} path(s) inside a live order's substrate (${contracts.length} live) — permitted`, "in-substrate");
|
|
176
220
|
}
|
|
177
221
|
|
|
178
222
|
logPathology(metricsPath, {
|
|
@@ -194,7 +238,7 @@ async function main() {
|
|
|
194
238
|
hookEventName: "PreToolUse",
|
|
195
239
|
permissionDecision: "deny",
|
|
196
240
|
permissionDecisionReason:
|
|
197
|
-
`Sandbox guard (PA3) —
|
|
241
|
+
`Sandbox guard (PA3) — no live order's substrate covers these writes:\n` +
|
|
198
242
|
`${blockReasons.join("\n")}\n` +
|
|
199
243
|
`If this write legitimately crosses scopes, the order's substrate needs to be expanded (e.g. via ba --remap).`,
|
|
200
244
|
},
|