shapeup-sdlc 3.7.3 → 3.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -2
- package/kernel/compile.mjs +23 -2
- package/kernel/gate.mjs +34 -2
- package/kernel/lib/paths.mjs +10 -0
- package/kernel/probe/resume.mjs +25 -4
- package/kernel/schemas/domain.schema.json +18 -1
- package/package.json +1 -1
- package/skills/ba-pitch-analyzer/SKILL.md +3 -2
- package/skills/scope-hammer/SKILL.md +13 -0
- package/skills/tech-lead/references/gates.md +6 -3
- package/skills/tech-lead/workflows/shapeup-run.js +111 -13
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shapeup-sdlc-plugin",
|
|
3
3
|
"displayName": "ShapeUp SDLC Plugin",
|
|
4
|
-
"version": "3.7.
|
|
4
|
+
"version": "3.7.5",
|
|
5
5
|
"description": "Shape Up SDLC harness for Claude Code: shaping, intake, orient, scope-mapping, building (T0-verified, sandboxed, scope-contracted), evaluation and QA skills orchestrated by a tech-lead.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Liberty Nguyen",
|
package/AGENTS.md
CHANGED
|
@@ -32,7 +32,7 @@ Betting Table: PO decides; rejected pitches loop back to raw idea.
|
|
|
32
32
|
| Kick-off | ⏸ **L0** — Intake & Config (L0.8 model/budget matrix) + worker roster ✧ | `/translator` if non-English |
|
|
33
33
|
| Orient (Scout) | ⏸ **L1a** — Orient Review | `/orient` |
|
|
34
34
|
| Requirements | — (reviewed at L1b) | `/ba-pitch-analyzer` (`coverage`): the pitch's clauses → committed `requirements.md`, one atomic clause per `REQ-<n>` row naming the pitch clause it came from, ids assigned once and frozen; dispatched once, ahead of Analyze because the acceptance criteria are what cite its ids. A registry already on disk is not re-dispatched |
|
|
35
|
-
| Analyze | — (reviewed at L1b) | `/ba-pitch-analyzer` (`analyze`): spec tree + board (UC + Invariants + Test Surface ★); before Wire (needs its use cases). An acceptance criterion that grades a requirement carries `(covers: REQ-…)` — that clause is the edge the verdict travels back along |
|
|
35
|
+
| Analyze | — (reviewed at L1b) | `/ba-pitch-analyzer` (`analyze`): spec tree + board (UC + Invariants + Test Surface ★); before Wire (needs its use cases). The tree is committed and the board is per-machine: a run that finds the tree on disk and no board dispatches `board`, which regenerates the board from the tree without re-deriving it — GATE L2 refuses `proceed` over a board with zero tasks. An acceptance criterion that grades a requirement carries `(covers: REQ-…)` — that clause is the edge the verdict travels back along |
|
|
36
36
|
| Wire | ⏸ **L1a.5** — Wiring Review ✚ | `/solution-architect` (`wire`): sole writer of committed `wiring-map.md` — per-UC engine → seam → entry-point call site → affordance, per `project-profile.md` |
|
|
37
37
|
| Map Scopes | ⏸ **L1b** — Board Review (+ substrate disjointness lint) | `/scope-architect` (scope contracts ✦ — sole writer); traceability oracle advisory ✚. A registered requirement that no acceptance criterion grades and no scope claims is **red** here, and L1b prints the `REQ → AC` table: the two ways out are an AC carrying `(covers: REQ-…)` or the PO marking the clause `CUT (PO-approved)`. Red only where the plan is still cheap to change — after L1b nobody re-reads the pitch |
|
|
38
38
|
| Build Vertically | ⏸ **L2** — Board 100% ✅ + T0-green ✦ | per dispatch: compile order → `/task-executor` (--order) → ingest result; T0-verified per attempt (fixtures + DB probe ✦; the seesaw regression arm is declared and not yet wired), substrate-sandboxed ✦. Scopes build **concurrently** ✦ — `--parallel-scopes N` caps it (default 4), a scope is released the moment its own dependencies are green, and a scope green in this round is skipped rather than rebuilt. Then the **round build gate** ⚙: the ledger's run command, then the profile's `build_probe` and `launch_probe`, run once per round before EVAL — a red gate ends the round with no verdict and its failing step is compiled into the next round's orders as bugs; a `mobile` profile with no `launch_probe` is warned about every round, so the install/launch risk has an owner |
|
|
@@ -48,7 +48,7 @@ Betting Table: PO decides; rejected pitches loop back to raw idea.
|
|
|
48
48
|
**Q0** Preflight → **Q1** Charter (6 lenses − EVAL-covered) → **Hunt** (repro required, findings `~` → ledger) → report (no verdict, no score). Skip with `--no-qa`.
|
|
49
49
|
|
|
50
50
|
### Ship & Triage
|
|
51
|
-
- **SHIP S.0 / GATE H** — `/scope-hammer`: census (QA findings + discovered ledger + attempt-budget proposals ✦; every "no scope owns X" cites `probe owner`, which derives ownership from the contracts) → baseline comparison (never the ideal) → cut list; TL/PO promotes selected items only.
|
|
51
|
+
- **SHIP S.0 / GATE H** — `/scope-hammer`: census (QA findings + discovered ledger + attempt-budget proposals ✦; every "no scope owns X" cites `probe owner`, which derives ownership from the contracts) → baseline comparison (never the ideal) → cut list; TL/PO promotes selected items only. The census is written as data beside the report, and GATE L4 reads that file — and nothing else — before any answer set may say `ship`. A breaker does not end the run: the run itself dispatches the census, crosses GATE H, writes the ship report with the verdict as it is, crosses L4, and only then closes — `shipped` with the cut list when the census clears it, `escalated` naming the census when it does not.
|
|
52
52
|
- ⏸ **L4** — Ship Sign-off (shows QA status ★).
|
|
53
53
|
- **Coach retro** — L4 feedback → `/coach`; GATE COACH-1 asks the PO which skill owns each rule (never assumes) → committed `shapeup/knowledge-base/<skill>.md` (team inherits on pull). Coachable: `/task-executor`, `/ba-pitch-analyzer`, `/qa-edge-hunter`, `/orient`, `/scope-architect`, `/solution-architect`, and `/tech-lead` (workflow guidance read at L0, plus suggested L0 values it confirms before pinning); `/spec-evaluator` is not (single judge) and neither is `/scope-hammer` (its census cites `probe owner`). **Guidance never decides a gate**: a rule may add a question or a check to a gate block, never an answer, a skip or a wider substrate. `/retro --scan` seeds the same files from the project on disk before the first run, and `/retro --research <stack>` from the platform's official documentation when there is nothing on disk yet (a source, never a verification: nothing it reads runs until the tech lead pins it at L0), optional, every rule confirmed at COACH-1. Mechanism defects file to `knowledge-base/harness-defects.md` as Betting Table raw ideas, never worker steering.
|
|
54
54
|
- Post-fix: `eval --single-pass` → remaining `~` + new feedback → new raw idea.
|
package/kernel/compile.mjs
CHANGED
|
@@ -175,6 +175,9 @@ export function ledgerDecisions(ledgerText, scopeId) {
|
|
|
175
175
|
*/
|
|
176
176
|
export const OP_OWNER = {
|
|
177
177
|
analyze: "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
|
|
178
|
+
// `board` regenerates the per-machine board from a committed spec tree without re-deriving the
|
|
179
|
+
// tree — the half of ANALYZE that does not survive a clone. Same worker, narrower write surface.
|
|
180
|
+
board: "ba-pitch-analyzer",
|
|
178
181
|
"retrofit-surface": "ba-pitch-analyzer", coverage: "ba-pitch-analyzer",
|
|
179
182
|
"map-scopes": "scope-architect",
|
|
180
183
|
wire: "solution-architect", evaluate: "spec-evaluator", orient: "orient",
|
|
@@ -259,6 +262,14 @@ export function substrateFor(operation, { slug, specDir, scope } = {}) {
|
|
|
259
262
|
case "analyze":
|
|
260
263
|
return { allowed: [`${spec}/**`, `${local}/**`], frozen: [...FROZEN_INTAKE] };
|
|
261
264
|
|
|
265
|
+
case "board":
|
|
266
|
+
// The spec is READ, never written: this operation exists because the tree is already on disk
|
|
267
|
+
// and the board is not. Task ids renumber per machine by design, so nothing here is committed.
|
|
268
|
+
return {
|
|
269
|
+
allowed: [`${local}/tasks/**`, `${working}/**`],
|
|
270
|
+
frozen: [...FROZEN_SPEC_CORE, ...FROZEN_INTAKE, `${spec}/usecases/*.md`, `${spec}/scope-summary.md`],
|
|
271
|
+
};
|
|
272
|
+
|
|
262
273
|
case "reconcile":
|
|
263
274
|
return {
|
|
264
275
|
allowed: [`${local}/tasks/**`, `${spec}/scope-summary.md`, `${working}/**`],
|
|
@@ -857,12 +868,22 @@ export async function cli(rawArgv) {
|
|
|
857
868
|
// receipts ledger EXISTING while carrying no row for the previous attempt: a lane that does not
|
|
858
869
|
// attest dispatches at all (no ledger on disk) cannot be judged by this rule and is waved
|
|
859
870
|
// through, so `--tiny`, a prose round loop and a standalone build are untouched.
|
|
860
|
-
|
|
871
|
+
//
|
|
872
|
+
// THE RUN KEY IS PART OF THE QUESTION. `attemptEvidence` matches a receipt to an attempt by
|
|
873
|
+
// `order_id` AND `run_id` — deliberately, so a previous run's receipt over the same slug cannot
|
|
874
|
+
// answer for this one — and this call used to omit the key. With no key nothing matches, so every
|
|
875
|
+
// previous attempt read "unattested" and every attempt 2 was refused as unanswered, on every run,
|
|
876
|
+
// with the receipt, the leg row and the result all on disk. Measured on a live run: one attempt
|
|
877
|
+
// spent of five, the census reading it spent, and the gate refusing to open the next one three
|
|
878
|
+
// times over. The ratchet was one attempt deep for as long as the gate has existed. A run with no
|
|
879
|
+
// readable receipt has no key to ask with and is waved through, like a lane with no ledger.
|
|
880
|
+
const myRunId = (scope?.scope_id && round && attempt > 1) ? readRunId(cwd, slug) : null;
|
|
881
|
+
if (scope?.scope_id && round && attempt > 1 && myRunId) {
|
|
861
882
|
const receiptsPath = dispatchReceipts(cwd, slug);
|
|
862
883
|
if (existsSync(receiptsPath)) {
|
|
863
884
|
const prev = attemptEvidence(
|
|
864
885
|
cwd, slug, scope.scope_id, round, attempt - 1,
|
|
865
|
-
readReceipts(receiptsPath), readLegs(legLedger(cwd, slug)),
|
|
886
|
+
readReceipts(receiptsPath), readLegs(legLedger(cwd, slug)), myRunId,
|
|
866
887
|
);
|
|
867
888
|
if (prev.state !== "spent") {
|
|
868
889
|
const why = prev.state === "unattested"
|
package/kernel/gate.mjs
CHANGED
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
// 2 usage / validation error
|
|
52
52
|
|
|
53
53
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
54
|
+
import { parseBoard } from "./reduce/board.mjs";
|
|
54
55
|
import { join, dirname } from "node:path";
|
|
55
56
|
import { runArgs } from "./lib/argv.mjs";
|
|
56
|
-
import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir } from "./lib/paths.mjs";
|
|
57
|
+
import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, tasksDir, hammerCensus } from "./lib/paths.mjs";
|
|
57
58
|
|
|
58
59
|
export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
|
|
59
60
|
|
|
@@ -110,7 +111,7 @@ export const PRESETS = {
|
|
|
110
111
|
"L1a": { decision: "proceed", note: "Orient review — advisory read." },
|
|
111
112
|
"L1a.5": { decision: "proceed", note: "Wiring review — checked by trace-lint." },
|
|
112
113
|
"L1b": { decision: "ask", note: "Board review is where scope is actually decided. Not pre-approvable." },
|
|
113
|
-
"L2": { decision: "proceed", note: "The board facts travel in the gate block itself — green_scopes and hammer_proposals — so a preset answering here is not answering blind.
|
|
114
|
+
"L2": { decision: "proceed", note: "The board facts travel in the gate block itself — green_scopes and hammer_proposals — so a preset answering here is not answering blind. A board with zero tasks is refused here regardless of the answer — the resolver narrows `proceed` to `ask` over an empty board." },
|
|
114
115
|
"L3": { decision: "loop", max_rounds: 3, note: "Loop on FAIL; the breaker ends it." },
|
|
115
116
|
"QA": { decision: "run" },
|
|
116
117
|
"H": { decision: "ask", note: "The cut list changes what ships." },
|
|
@@ -186,6 +187,17 @@ export const HAMMER_VERDICTS = ["ship-now", "ship-after-fixes", "cannot-ship"];
|
|
|
186
187
|
*/
|
|
187
188
|
export function censusVerdict(cwd, slug) {
|
|
188
189
|
if (!slug) return null;
|
|
190
|
+
// The census artifact — written by the hammer at a path its order's substrate permits. The
|
|
191
|
+
// WorkResult fallback below is kept for a lane that wrote one the old way; a WorkResult may not
|
|
192
|
+
// carry a verdict under the schema, so in practice the artifact is the only source.
|
|
193
|
+
try {
|
|
194
|
+
const c = hammerCensus(cwd, slug);
|
|
195
|
+
if (existsSync(c)) {
|
|
196
|
+
const r = JSON.parse(readFileSync(c, "utf8"));
|
|
197
|
+
const v = r?.verdict ?? null;
|
|
198
|
+
if (HAMMER_VERDICTS.includes(v)) return v;
|
|
199
|
+
}
|
|
200
|
+
} catch { /* unreadable census proves nothing — fall through */ }
|
|
189
201
|
try {
|
|
190
202
|
const p = join(resultsDir(cwd, slug), "hammer.json");
|
|
191
203
|
if (!existsSync(p)) return null;
|
|
@@ -218,6 +230,26 @@ export function censusVerdict(cwd, slug) {
|
|
|
218
230
|
* @returns {object} The result, or an `ask` carrying why `ship` was not available.
|
|
219
231
|
*/
|
|
220
232
|
export function narrowToEvidence(r, cwd, slug) {
|
|
233
|
+
// GATE L2's question is "is the board done", and a preset used to answer it over a board with
|
|
234
|
+
// zero tasks — 100% of nothing. Measured on a consumer: a committed spec fast-forwarded ANALYZE,
|
|
235
|
+
// the per-machine board was never regenerated, L2 crossed `proceed` from the `ci` preset and the
|
|
236
|
+
// frozen report printed `Board 0/0 tasks done`. Same rule as L4 below: an answer set chooses
|
|
237
|
+
// among allowed answers and cannot supply the evidence that makes one allowed.
|
|
238
|
+
if (r?.gate === "L2" && r?.status === "ok" && r?.decision === "proceed" && slug) {
|
|
239
|
+
let tasks = 0;
|
|
240
|
+
try { tasks = parseBoard(tasksDir(cwd, slug)).length; } catch { tasks = 0; }
|
|
241
|
+
if (tasks === 0) {
|
|
242
|
+
return {
|
|
243
|
+
gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
|
|
244
|
+
refused: "proceed", board_tasks: 0,
|
|
245
|
+
reason: `GATE L2 cannot be answered "proceed": the board has no tasks, so "board 100%" is a ` +
|
|
246
|
+
`hundred percent of nothing. A committed spec with no per-machine board needs the board ` +
|
|
247
|
+
`regenerated (operation \`board\`) before this gate means anything. Put the block to ` +
|
|
248
|
+
`the PO, or regenerate the board first.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return r;
|
|
252
|
+
}
|
|
221
253
|
if (r?.gate !== "L4" || r?.status !== "ok" || r?.decision !== "ship") return r;
|
|
222
254
|
const verdict = censusVerdict(cwd, slug);
|
|
223
255
|
if (verdict === "ship-now" || verdict === "ship-after-fixes") return r;
|
package/kernel/lib/paths.mjs
CHANGED
|
@@ -306,6 +306,16 @@ export const activeOrder = (cwd) => join(localDir(cwd), "active-order");
|
|
|
306
306
|
*/
|
|
307
307
|
export const lastRun = (cwd) => join(localDir(cwd), "last-run");
|
|
308
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Scope-hammer's census as DATA — `{verdict, cut_list, …}` — the one artifact GATE L4's resolver
|
|
311
|
+
* reads before it lets an answer set say `ship`. The hammer's WorkResult may not carry a verdict
|
|
312
|
+
* (its census is a proposal, never envelope data ingest acts on), and its printed H0/H1/H2 blocks
|
|
313
|
+
* are prose; this file is the same proposal, written by the same hand, in a shape a gate can read.
|
|
314
|
+
* Measured before it existed: a green census returned as the worker's report, and L4 could only
|
|
315
|
+
* record `ask`, because nothing on disk said so.
|
|
316
|
+
*/
|
|
317
|
+
export const hammerCensus = (cwd, slug) => join(localRoot(cwd, slug), "reports", "hammer-census.json");
|
|
318
|
+
|
|
309
319
|
/**
|
|
310
320
|
* Where the run scripts are staged for launch, inside the project.
|
|
311
321
|
*
|
package/kernel/probe/resume.mjs
CHANGED
|
@@ -61,7 +61,8 @@ import { dirname, join, resolve } from "node:path";
|
|
|
61
61
|
import { runArgs } from "../lib/argv.mjs";
|
|
62
62
|
import { splitFrontmatter, uncoerce } from "../lib/contract.mjs";
|
|
63
63
|
import { globToRegExp } from "../verify/spec.mjs";
|
|
64
|
-
import {
|
|
64
|
+
import { parseBoard } from "../reduce/board.mjs";
|
|
65
|
+
import { intake, harnessRun, wiringMap, projectProfile, scopesDir, resultsDir, ordersDir, orientDir, activeOrder, activeScope, usecasesDir, breadboard, receipt, readReceipt, requirements, exportRunDir, lastRun, readRunId, tasksDir } from "../lib/paths.mjs";
|
|
65
66
|
import { evalVerdict } from "./eval.mjs";
|
|
66
67
|
import { collectRun, writeRun } from "../report/export.mjs";
|
|
67
68
|
|
|
@@ -371,6 +372,21 @@ export function usecasesPath(cwd, slug, specFolder) {
|
|
|
371
372
|
* @param {string|null} specFolder - The ledger's `spec_folder`, if it names one.
|
|
372
373
|
* @returns {boolean} True when at least one use case (not the index) is on disk.
|
|
373
374
|
*/
|
|
375
|
+
/**
|
|
376
|
+
* Is the per-machine board on disk with at least one task? ANALYZE writes two artifacts in two
|
|
377
|
+
* tiers — the spec tree, committed, and the board, gitignored — and a fast-forward that asked only
|
|
378
|
+
* about the committed half walked every later run on a machine, and every run in a fresh checkout,
|
|
379
|
+
* into a build with a spec and no board: GATE L2 crossed over 0/0 tasks, the evaluator could read no
|
|
380
|
+
* `covers:` clause, and the requirements projection printed "no evidence" on a round whose static
|
|
381
|
+
* criteria all passed. Measured on a live consumer, fourth run of one pitch.
|
|
382
|
+
* @param {string} cwd - Project root.
|
|
383
|
+
* @param {string} slug - Feature slug.
|
|
384
|
+
* @returns {boolean} True when the board directory holds at least one task file.
|
|
385
|
+
*/
|
|
386
|
+
export function hasBoard(cwd, slug) {
|
|
387
|
+
try { return parseBoard(tasksDir(cwd, slug)).length > 0; } catch { return false; }
|
|
388
|
+
}
|
|
389
|
+
|
|
374
390
|
export function hasSpecTree(cwd, slug, specFolder) {
|
|
375
391
|
const dir = usecasesPath(cwd, slug, specFolder);
|
|
376
392
|
if (!existsSync(dir)) return false;
|
|
@@ -385,7 +401,10 @@ export function hasSpecTree(cwd, slug, specFolder) {
|
|
|
385
401
|
*/
|
|
386
402
|
export const PHASE_ARTIFACT = {
|
|
387
403
|
orient: { fact: "has_orient_artifacts", artifact: "orient/{code-surface,discovered-seed,hill-signal}.md + spike-*.md" },
|
|
388
|
-
|
|
404
|
+
// BOTH HALVES. The spec tree is committed and survives a clone; the board is per-machine and does
|
|
405
|
+
// not. A phase is complete only when both are on disk — a committed spec with no board resumes AT
|
|
406
|
+
// analyze, where the workflow dispatches the board-only operation rather than the whole phase.
|
|
407
|
+
analyze: { fact: "has_spec_tree", also: "has_board", artifact: "spec/usecases/*.md + tasks/TASK-*.md" },
|
|
389
408
|
wire: { fact: "has_wiring_map", artifact: "wiring-map.md" },
|
|
390
409
|
"map-scopes": { fact: "scope_files", artifact: "scopes/*.md" },
|
|
391
410
|
};
|
|
@@ -399,8 +418,9 @@ export const PHASES = Object.keys(PHASE_ARTIFACT);
|
|
|
399
418
|
* @returns {boolean} True when the phase's artifact exists.
|
|
400
419
|
*/
|
|
401
420
|
export function phaseSatisfied(state, phase) {
|
|
402
|
-
const v = state[
|
|
403
|
-
|
|
421
|
+
const present = (k) => { const v = state[k]; return Array.isArray(v) ? v.length > 0 : Boolean(v); };
|
|
422
|
+
const p = PHASE_ARTIFACT[phase];
|
|
423
|
+
return present(p.fact) && (!p.also || present(p.also));
|
|
404
424
|
}
|
|
405
425
|
|
|
406
426
|
/**
|
|
@@ -461,6 +481,7 @@ export function deriveResumeState(cwd, slug) {
|
|
|
461
481
|
orient_dir: `.shapeup/${slug}/orient/`,
|
|
462
482
|
has_orient_artifacts: hasOrientArtifacts(cwd, slug),
|
|
463
483
|
has_spec_tree: hasSpecTree(cwd, slug, hr.spec_folder || null),
|
|
484
|
+
has_board: hasBoard(cwd, slug),
|
|
464
485
|
// A PLAIN FACT, DELIBERATELY NOT A PHASE. The requirements registry is dispatched once, before
|
|
465
486
|
// ANALYZE, and the orchestrator guards that one dispatch on this boolean. It is NOT an entry in
|
|
466
487
|
// PHASE_ARTIFACT, and adding it there would be a migration hazard rather than a tidier shape:
|
|
@@ -494,7 +494,7 @@
|
|
|
494
494
|
"execute",
|
|
495
495
|
"fix",
|
|
496
496
|
"spike",
|
|
497
|
-
"analyze",
|
|
497
|
+
"analyze", "board",
|
|
498
498
|
"reconcile",
|
|
499
499
|
"retrofit-surface",
|
|
500
500
|
"coverage",
|
|
@@ -2537,6 +2537,22 @@
|
|
|
2537
2537
|
}
|
|
2538
2538
|
}
|
|
2539
2539
|
},
|
|
2540
|
+
"HammerCensus": {
|
|
2541
|
+
"description": "Scope-hammer's census as data — the proposal GATE L4's resolver reads before it lets an answer set say `ship`. Written by the hammer at `.shapeup/<slug>/reports/hammer-census.json`, the only path its order's substrate permits besides the committed report. A proposal, never a decision: promotion and shipping stay the PO's call, and ingest never acts on it.",
|
|
2542
|
+
"x-tier": "LOCAL",
|
|
2543
|
+
"x-location": ".shapeup/<slug>/reports/hammer-census.json",
|
|
2544
|
+
"type": "object",
|
|
2545
|
+
"required": ["schema_version", "verdict", "cut_list"],
|
|
2546
|
+
"properties": {
|
|
2547
|
+
"schema_version": { "const": 1 },
|
|
2548
|
+
"order_id": { "type": "string" },
|
|
2549
|
+
"verdict": { "type": "string", "enum": ["ship-now", "ship-after-fixes", "cannot-ship"] },
|
|
2550
|
+
"cut_list": { "type": "array", "items": { "type": "string" } },
|
|
2551
|
+
"ship_blocking": { "type": "array", "items": { "type": "string" }, "description": "Must-have items that failed the baseline comparison — non-empty exactly when the verdict is cannot-ship." },
|
|
2552
|
+
"breaker": { "type": ["string", "null"] },
|
|
2553
|
+
"baseline": { "type": ["string", "null"], "description": "The baseline the comparison ran against, or null when the pitch's problem statement stood in for it." }
|
|
2554
|
+
}
|
|
2555
|
+
},
|
|
2540
2556
|
"ResumeState": {
|
|
2541
2557
|
"description": "The fast-forward derivation: which phase a launch resumes at, derived from artifacts on disk and NEVER from stored state or conversation memory. Produced by kernel/probe/resume.mjs on stdout and consumed by shapeup-run.js's preamble on every launch, fresh or relaunch alike. Every phase predicate is an artifact test — `has_orient_artifacts` exists because the ORIENT branch was once gated on the ledger's stored `status` instead, which a silent courier failure left stale, and a completed ORIENT phase was re-dispatched on resume. `next_phase` is a convenience derived from the same booleans, which all travel too: a caller is never forced to trust a summary it cannot re-derive.",
|
|
2542
2558
|
"x-tier": "EMBEDDED",
|
|
@@ -2635,6 +2651,7 @@
|
|
|
2635
2651
|
"type": "boolean",
|
|
2636
2652
|
"description": "ANALYZE finished: the spec folder's usecases/ carries at least one use case that is not _index.md. WIRE reads these — one wiring-map entry per use case — which is why ANALYZE precedes WIRE in the phase chain: dispatched against an empty spec folder, WIRE escalates on every launch."
|
|
2637
2653
|
},
|
|
2654
|
+
"has_board": { "type": "boolean", "description": "The per-machine board (tasks/TASK-*.md) holds at least one task. ANALYZE is complete only with both the committed spec tree and this; a committed tree with no board resumes at analyze, where the board-only operation regenerates it." },
|
|
2638
2655
|
"has_requirements": {
|
|
2639
2656
|
"type": "boolean",
|
|
2640
2657
|
"description": "The requirements registry is on disk: shapeup/<slug>/requirements.md exists. A PLAIN FACT, not a phase — the orchestrator guards its single `coverage` dispatch on this boolean, and it is deliberately absent from kernel/probe/resume.mjs's PHASE_ARTIFACT map, which doubles as nextPhase()'s ordered list: an entry there would fast-forward every run recorded before the registry existed to the registry instead of to build."
|
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ Invoked as `--order <path>`. Fields you may rely on (absent = unknown; surface i
|
|
|
24
24
|
|
|
25
25
|
| Field | What it is |
|
|
26
26
|
|---|---|
|
|
27
|
-
| `operation` | `analyze` (pitch → full spec tree + board) · `reconcile` (fold discovered-ledger items into the board + UC invariants) · `retrofit-surface` (append `## Test Surface` to a pre-surface spec) · `coverage` (extract atomic requirement clauses → the SHARED `requirements.md` registry) |
|
|
27
|
+
| `operation` | `analyze` (pitch → full spec tree + board) · `board` (committed spec tree → the per-machine board only; the tree is frozen) · `reconcile` (fold discovered-ledger items into the board + UC invariants) · `retrofit-surface` (append `## Test Surface` to a pre-surface spec) · `coverage` (extract atomic requirement clauses → the SHARED `requirements.md` registry) |
|
|
28
28
|
| `payload.pitch` | The pitch/PRD path (analyze) |
|
|
29
29
|
| `payload.breadboard` | The breadboard (analyze): its Places are your screens; its U# and N# are the affordances you place and cite. Absent = none separate; never inferred |
|
|
30
30
|
| `payload.requirements` | (coverage) the REQ source to extract atomic clauses from — pitch / a customer-requirements doc / the use-case bodies. Absent → default to the pitch and record the choice in `assumptions[]` |
|
|
@@ -115,10 +115,11 @@ covered AC is a requirement the run can be measured against.
|
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
118
|
-
## The other
|
|
118
|
+
## The other four operations — same craft, different payload + whitelist
|
|
119
119
|
|
|
120
120
|
| Operation | Essence | Never |
|
|
121
121
|
|---|---|---|
|
|
122
|
+
| `board` | The spec tree is already on disk and committed; the board is per-machine and is not. Regenerate `tasks/` from the use cases as they are — same task derivation as `analyze`, every AC carrying its `(covers: REQ-…)` clause, ids numbered fresh for this machine | touch the spec folder (frozen), re-derive or "improve" a use case, invent a task no UC step or Test Surface row sources |
|
|
122
123
|
| `reconcile` | Verify `ledger.feature == payload.feature` (mismatch → STOP). Map each `[+]` Keep item → its owning UC; new task continues numbering (never renumber); `~`/Cut → synthesis "Hammered Out" row, no file. A Keep item asserting a new invariant → APPEND `[INV-NN]` + TS-INV row to that UC (append-only sections in your substrate). A new actor/action with no UC → `status: "escalated"` + a `deviations[]` spec-ambiguity entry: spawning a UC mid-cycle is silent re-shaping, the PO decides. Finish with board-derive (appetite overflow → report) + spec-lint | re-run phases 1–5; edit UC Steps; resolve the appetite HAMMER yourself |
|
|
123
124
|
| `retrofit-surface` | Append `## Test Surface` (derived rows only, after Error Cases) to each UC of a pre-surface spec; an all-sources-empty UC gets the explicit empty-sources line | touch anything else — append-only substrate |
|
|
124
125
|
| `coverage` | Extract **atomic** customer requirement clauses from `payload.requirements` (default: the pitch) and write the SHARED `shapeup/<slug>/requirements.md` registry: one `\| REQ-id \| clause (verbatim) \| source \| status \| note \|` row per clause. **The `source` cell, and any other prose in this file, cites the committed pitch or shaping doc — never the gitignored run-tier path** (`.shapeup/<slug>/intake.md`, or any other `.shapeup/` path): spec-lint's `TIER-DIRECTION` rule reds a committed file naming that path in ANY form, a bare path in a sentence exactly as much as a `[[tasks/...]]` wikilink, because it dangles on every other clone. When the intake has no committed original to name, describe the run tier without a path (`"the pitch staged for this run"`) rather than citing where it actually lives. Split compound sentences into one testable clause each — a clause lost *inside* a bigger sentence is a requirement nothing can be traced to. **Assign REQ-ids ONCE and freeze them** (they behave like scope_id, never TASK-NNN — every `covers:` link rots otherwise): re-running, append new clauses with fresh ids, mark a removed clause `CUT (PO-approved)`, never renumber or delete. Status starts `covered` (a live requirement); only the PO sets `CUT`. The REQ source itself is frozen — the registry is a separate derived file. **Numbering.** A source clause already carrying an `R<n>` keeps its number — `R12` → `REQ-12` — and its `source` cell records where it came from verbatim (`shaping.md R12`), because that cell is the only thing that survives a re-run. A clause with no R-id takes the next free number ABOVE the highest `R<n>` in the source, so it can never collide with one added later. Splitting a compound clause keeps `REQ-12` for the first atomic part and records `shaping.md R12 (split 2/3)` for the rest — a requirement graded in parts is why splitting matters at all. On a re-run, match an existing id by its frozen `source` cell and clause text, **never** by re-deriving the number from the source's current order | edit the REQ source; renumber existing REQ-ids; delete a dropped clause instead of marking it CUT; invent a requirement not in the source; re-point an existing REQ-id because the source's R-numbers shifted |
|
|
@@ -171,6 +171,19 @@ The WorkResult may carry only `files_touched`, `artifacts`, `assumptions`, `devi
|
|
|
171
171
|
(`x-result-by-worker`): the census, cut list, and ship verdict live in the report artifact as a
|
|
172
172
|
**proposal** — promotion and shipping stay a human call, never envelope data ingest acts on.
|
|
173
173
|
|
|
174
|
+
**The census is also written as data**, at `.shapeup/<slug>/reports/hammer-census.json` (inside
|
|
175
|
+
this operation's substrate), shape `HammerCensus` in the domain registry:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{ "schema_version": 1, "order_id": "<the order>", "verdict": "ship-now | ship-after-fixes | cannot-ship",
|
|
179
|
+
"cut_list": ["…"], "ship_blocking": ["…"], "breaker": "outer | inner | deadline | null", "baseline": "<path or null>" }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
GATE L4's resolver reads this file — and nothing else — before it lets any answer set say `ship`.
|
|
183
|
+
It is the same proposal as the H2 block, written by the same hand, in a shape a gate can read:
|
|
184
|
+
a census that lives only in printed blocks cannot be read by a gate, and a run that reached L4
|
|
185
|
+
with a green census could only be recorded as `ask`. Still a proposal: the file grants nothing.
|
|
186
|
+
|
|
174
187
|
---
|
|
175
188
|
|
|
176
189
|
## Invocation
|
|
@@ -560,9 +560,12 @@ and without it the trace holds no record of that decision at all: `node
|
|
|
560
560
|
"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" gate --resolve L4 --slug <slug>
|
|
561
561
|
[--file <path>|--preset <name>]`. Exit 0 (`decision=ship|hold`) — render the block above and close
|
|
562
562
|
the run: `node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" probe resume --slug <slug> --close shipped
|
|
563
|
-
--cause "verdict=<verdict> rounds=<r> decision=<ship|hold>"`.
|
|
564
|
-
|
|
565
|
-
|
|
563
|
+
--cause "verdict=<verdict> rounds=<r> decision=<ship|hold>"`. **This applies to the prose lane
|
|
564
|
+
only.** A launched run (`shapeup-run.js`) resolves L4 itself, on both the PASS path and a breaker
|
|
565
|
+
path: it dispatches the census, crosses GATE H, writes the ship report with the verdict as it is,
|
|
566
|
+
crosses L4, and only then closes — `shipped` when the census and L4 clear it, `escalated` naming
|
|
567
|
+
the census when they do not. A run that returned `{status: "shipped"}` or `{status: "gate_h"}` is
|
|
568
|
+
already closed; do not close it again, and do not run the census by hand after it.
|
|
566
569
|
The close itself is a once-only fact IN THE KERNEL (`closeRun`'s own guard reads a `closed_status:`
|
|
567
570
|
line that only `closeRun` ever writes — never the mutable `status:` line every phase rewrites, this
|
|
568
571
|
call included), not a conditional this instruction has to get right: if this run_id was NOT already
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
// { status: "paused", paused_at, block, valid_decisions, context }
|
|
43
43
|
// { status: "aborted", aborted_at, reason }
|
|
44
44
|
// { status: "gate_h", breaker: "outer"|"attempt_budget"|"none"|"deadline", hammer_proposals, green_scopes,
|
|
45
|
-
// tripped_scopes?, unapplied_results? }
|
|
45
|
+
// tripped_scopes?, unapplied_results?, census?, cut_list?, l4? }
|
|
46
|
+
// — returned only when the census said cannot-ship, L4 said hold, or the ship report was
|
|
47
|
+
// refused; a breaker whose census clears the run ends as `shipped` with `after: "gate_h"`.
|
|
46
48
|
|
|
47
49
|
// meta must be a PURE LITERAL — the runtime parses it statically, before the body ever runs, and
|
|
48
50
|
// rejects the whole script on anything it has to evaluate. A `+`-joined description is a
|
|
@@ -411,6 +413,7 @@ const RESUME = {
|
|
|
411
413
|
eval_dimensions: { type: "array", items: { type: "string" } },
|
|
412
414
|
has_orient_artifacts: { type: "boolean" },
|
|
413
415
|
has_spec_tree: { type: "boolean" },
|
|
416
|
+
has_board: { type: "boolean" },
|
|
414
417
|
// The requirements registry — a fact, not a phase. See the COVERAGE block below for why it is
|
|
415
418
|
// guarded on this bare boolean and never asked about through `probe resume --require`.
|
|
416
419
|
has_requirements: { type: "boolean" },
|
|
@@ -624,6 +627,69 @@ const HAMMER = {
|
|
|
624
627
|
required: ["ok", "verdict", "cut_list"],
|
|
625
628
|
};
|
|
626
629
|
|
|
630
|
+
/** What every hammer dispatch is told, on the PASS path and the breaker path alike. */
|
|
631
|
+
const HAMMER_EXTRA =
|
|
632
|
+
"Run the census, compare against the BASELINE and never the ideal, and produce the cut list. " +
|
|
633
|
+
"Write the census as data to `reports/hammer-census.json` under the run's local root — the " +
|
|
634
|
+
"`reports/**` entry in your order's substrate names the directory — with schema_version 1, " +
|
|
635
|
+
"order_id, verdict (ship-now | ship-after-fixes | cannot-ship), cut_list, ship_blocking, breaker, " +
|
|
636
|
+
"baseline — beside the human-readable report. GATE L4's resolver reads that file and nothing " +
|
|
637
|
+
"else before it lets any answer set say `ship`; a census that lives only in your printed blocks " +
|
|
638
|
+
"cannot be read by a gate.";
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* A breaker routed the run to GATE H. This used to be a terminal return: the loop handed back
|
|
642
|
+
* `{status: "gate_h"}`, the close-out stamped the ledger `escalated` and retired the pointers, and
|
|
643
|
+
* the census, GATE H, the ship report and GATE L4 all happened AFTER the close, in the tech
|
|
644
|
+
* lead's prose — so the census reached no artifact, `gates.jsonl` held no H and no L4, and a later
|
|
645
|
+
* `--close shipped` was refused over the `escalated` fact already on the ledger. Measured on three
|
|
646
|
+
* consumer runs. AGENTS.md has always said what a breaker means: "ship what's green, never kill the
|
|
647
|
+
* run from outside". So the run does that itself, in this launch: the hammer census (an artifact),
|
|
648
|
+
* GATE H (a row), the ship report (with the verdict as it is, FAIL or not-evaluated included), and
|
|
649
|
+
* GATE L4 (a row) — and only then a close, `shipped` or `escalated`, that names the census.
|
|
650
|
+
*
|
|
651
|
+
* QA never ran on this path (it sits after a PASS), so the census is told so plainly. `qaFindings`
|
|
652
|
+
* is declared after the round loop and must not be read from here.
|
|
653
|
+
*
|
|
654
|
+
* @param {object} ret - The gate_h return the round loop built.
|
|
655
|
+
* @returns {Promise<object>} The RunReturn that actually ends the run.
|
|
656
|
+
*/
|
|
657
|
+
async function settleAtGateH(ret) {
|
|
658
|
+
phase("Ship");
|
|
659
|
+
const h = await worker({
|
|
660
|
+
skill: "scope-hammer", operation: "hammer", schema: HAMMER, phase: "Ship", label: "hammer",
|
|
661
|
+
payload: { feature: slug, qa_findings: 0, hammer_proposals: ret.hammer_proposals || [], breaker: ret.breaker ?? null },
|
|
662
|
+
extra: HAMMER_EXTRA,
|
|
663
|
+
});
|
|
664
|
+
if (h.__failed) return await withWarnings(diedAt("H", h));
|
|
665
|
+
{
|
|
666
|
+
const g = await crossGate("H", "Ship", ["accept-cut-list", "ship-all", "ask"],
|
|
667
|
+
{ verdict: h.verdict, cut_list: h.cut_list, breaker: ret.breaker ?? null, green_scopes: ret.green_scopes, hammer_proposals: ret.hammer_proposals });
|
|
668
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
669
|
+
}
|
|
670
|
+
if (h.verdict === "cannot-ship") {
|
|
671
|
+
return await withWarnings({ ...ret, census: "cannot-ship", cut_list: h.cut_list });
|
|
672
|
+
}
|
|
673
|
+
const hVerdict = verdict === "pass" ? "PASS" : verdict === "fail" ? "FAIL" : "not-evaluated";
|
|
674
|
+
const ship = await cmd(`reduce ship --slug ${slug} --verdict ${hVerdict} --qa skipped`, "Ship", "ship-report");
|
|
675
|
+
if (!ship.ok) {
|
|
676
|
+
return await withWarnings({ ...ret, census: h.verdict, cut_list: h.cut_list, ship_report: `refused: ${ship.detail || `exit ${ship.exit_code}`}` });
|
|
677
|
+
}
|
|
678
|
+
{
|
|
679
|
+
const g = await crossGate("L4", "Ship", ["ship", "hold", "ask"], { verdict: hVerdict, census: h.verdict, cut_list: h.cut_list, breaker: ret.breaker ?? null });
|
|
680
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
681
|
+
if (g.decision === "hold") return await withWarnings({ ...ret, census: h.verdict, cut_list: h.cut_list, l4: "hold" });
|
|
682
|
+
}
|
|
683
|
+
await advisory(`report export --slug ${slug}`, "Ship", "export-run");
|
|
684
|
+
await setRunStatus("shipped", "Ship");
|
|
685
|
+
return await withWarnings({
|
|
686
|
+
status: "shipped", verdict, rounds_used: round, after: "gate_h", breaker: ret.breaker ?? null,
|
|
687
|
+
census: h.verdict, cut_list: h.cut_list, green_scopes: ret.green_scopes,
|
|
688
|
+
unapplied_results: ret.unapplied_results || [], qa_findings: 0,
|
|
689
|
+
report: ship.detail || `shapeup/${slug}/REPORT.md`,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
627
693
|
// ---------------------------------------------------------------------------------------------
|
|
628
694
|
// DISPATCH — three shapes, and none of them parses text.
|
|
629
695
|
//
|
|
@@ -859,13 +925,15 @@ const attest = (phaseKey, phaseName, label) =>
|
|
|
859
925
|
* rather than folding the gap into a phase that "completed".
|
|
860
926
|
*
|
|
861
927
|
* @param {string} gate - The gate name to report the abort under.
|
|
862
|
-
* @param {string} phaseKey - The phase
|
|
928
|
+
* @param {string} phaseKey - The phase.
|
|
863
929
|
* @param {string} phaseName - Progress group.
|
|
930
|
+
* @param {string} [orderStem=phaseKey] - The order's file stem, when the phase was dispatched
|
|
931
|
+
* under a different operation (a board-only ANALYZE compiles `board.json`).
|
|
864
932
|
* @returns {Promise<(object|null)>} An aborted RunReturn, or null when the leg closed (or the
|
|
865
933
|
* question could not be asked — a probe that did not run proves nothing, and is logged as such).
|
|
866
934
|
*/
|
|
867
|
-
async function requireLeg(gate, phaseKey, phaseName) {
|
|
868
|
-
const ask = () => query(`probe leg --slug ${slug} --order "${
|
|
935
|
+
async function requireLeg(gate, phaseKey, phaseName, orderStem = phaseKey) {
|
|
936
|
+
const ask = () => query(`probe leg --slug ${slug} --order "${orderStem}"`, ORDERLEG, phaseName, `legcheck:${orderStem}`);
|
|
869
937
|
let leg = await ask();
|
|
870
938
|
if (!leg || !leg.found) { log(`${gate} — could not ask the leg ledger about "${phaseKey}" (probe returned ${leg ? "no order" : "nothing"}); proceeding on the artifact alone.`); return null; }
|
|
871
939
|
if (!leg.has_result || leg.applied) return null;
|
|
@@ -891,9 +959,9 @@ async function requireLeg(gate, phaseKey, phaseName) {
|
|
|
891
959
|
* @param {string} phaseName - Progress group.
|
|
892
960
|
* @returns {Promise<(object|null)>} An aborted RunReturn, or null when the artifact is there.
|
|
893
961
|
*/
|
|
894
|
-
async function requirePhase(gate, phaseKey, phaseName) {
|
|
962
|
+
async function requirePhase(gate, phaseKey, phaseName, orderStem = phaseKey) {
|
|
895
963
|
const r = await attest(phaseKey, phaseName, `require:${phaseKey}`);
|
|
896
|
-
if (r.exit_code === 0) return await requireLeg(gate, phaseKey, phaseName);
|
|
964
|
+
if (r.exit_code === 0) return await requireLeg(gate, phaseKey, phaseName, orderStem);
|
|
897
965
|
// Exit 6 is `probe resume --require`'s OWN documented code for "the artifact really is not on
|
|
898
966
|
// disk" (kernel/probe/resume.mjs banner). Any other value — including -1, the courier's sentinel
|
|
899
967
|
// for a tool call that never ran — is not that predicate answering "no"; it is the predicate never
|
|
@@ -1069,7 +1137,9 @@ async function closeIfTerminal(ret) {
|
|
|
1069
1137
|
+ (Array.isArray(ret.tripped_scopes) ? ` tripped_scopes=${ret.tripped_scopes.length}` : "")
|
|
1070
1138
|
// A result the single writer never applied is named at the close, not folded into "not green".
|
|
1071
1139
|
+ (Array.isArray(ret.unapplied_results) && ret.unapplied_results.length ? ` unapplied_results=${ret.unapplied_results.length}` : "")
|
|
1072
|
-
|
|
1140
|
+
+ (ret.census ? ` census=${ret.census}` : "") + (ret.l4 ? ` l4=${ret.l4}` : "") + (ret.ship_report ? ` ship_report=${ret.ship_report}` : "")
|
|
1141
|
+
: `verdict=${ret.verdict ?? "?"} rounds=${ret.rounds_used ?? "?"} qa_findings=${ret.qa_findings ?? "?"}`
|
|
1142
|
+
+ (ret.after ? ` after=${ret.after} breaker=${ret.breaker ?? "?"} census=${ret.census ?? "?"} cut_list=${Array.isArray(ret.cut_list) ? ret.cut_list.length : "?"}` : "");
|
|
1073
1143
|
// `--close-arm` hands the kernel the arm itself (not a status this file decided was terminal) —
|
|
1074
1144
|
// a non-terminal arm (`paused`, `ok`) still exits 0 with no "decision" key, so the branches below
|
|
1075
1145
|
// stay silent for it exactly as they did when this file's own guard returned early.
|
|
@@ -1254,8 +1324,28 @@ if (!rs.has_spec_tree) {
|
|
|
1254
1324
|
const post = await requirePhase("ANALYZE", "analyze", "Analyze");
|
|
1255
1325
|
if (post) return await withWarnings(post);
|
|
1256
1326
|
await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:analyze");
|
|
1327
|
+
} else if (!rs.has_board) {
|
|
1328
|
+
// THE HALF THAT DOES NOT SURVIVE. The spec tree is committed; the board is per-machine and
|
|
1329
|
+
// gitignored, and ANALYZE writes both. A run after the first on a machine — and every run in a
|
|
1330
|
+
// fresh checkout — used to fast-forward on the committed half alone and build over no board:
|
|
1331
|
+
// GATE L2 crossed 0/0 tasks, the evaluator could read no `covers:` clause, and the requirements
|
|
1332
|
+
// projection said "no evidence" for a round whose static criteria all passed. The board-only
|
|
1333
|
+
// operation regenerates it from the tree without re-deriving the tree.
|
|
1334
|
+
log(`ANALYZE — spec tree on disk, no board: dispatching the board-only operation (slug ${slug})`);
|
|
1335
|
+
await setRunStatus("mapping", "Analyze");
|
|
1336
|
+
const b = await worker({
|
|
1337
|
+
skill: "ba-pitch-analyzer", operation: "board", schema: PHASE_OK, phase: "Analyze", label: "board",
|
|
1338
|
+
payload: { spec_folder: specFolder, feature: slug, lens: rs.lens },
|
|
1339
|
+
extra: "The spec tree is committed and FROZEN for this dispatch. Regenerate the per-machine board " +
|
|
1340
|
+
"under the run's tasks/ directory from the use cases on disk — every acceptance criterion " +
|
|
1341
|
+
"carrying its `(covers: REQ-…)` clause — and write nothing under the spec folder.",
|
|
1342
|
+
});
|
|
1343
|
+
if (b.__failed) return await withWarnings(diedAt("ANALYZE", b));
|
|
1344
|
+
const post = await requirePhase("ANALYZE", "analyze", "Analyze", "board");
|
|
1345
|
+
if (post) return await withWarnings(post);
|
|
1346
|
+
await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:board");
|
|
1257
1347
|
} else {
|
|
1258
|
-
const post = await fastForward("ANALYZE", "analyze", "Analyze", "spec tree already on disk");
|
|
1348
|
+
const post = await fastForward("ANALYZE", "analyze", "Analyze", "spec tree and board already on disk");
|
|
1259
1349
|
if (post) return await withWarnings(post);
|
|
1260
1350
|
}
|
|
1261
1351
|
|
|
@@ -1476,7 +1566,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1476
1566
|
const budget = await cmd(`verify budget --slug ${slug} --strict`, "Build", `budget:r${round}`);
|
|
1477
1567
|
if (budget.exit_code === 6) {
|
|
1478
1568
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1479
|
-
return await
|
|
1569
|
+
return await settleAtGateH({ status: "gate_h", breaker: "deadline", unapplied_results: allUnapplied, hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1480
1570
|
}
|
|
1481
1571
|
|
|
1482
1572
|
log(`BUILD round ${round} — ${scopes.length} scope(s), up to ${maxParallelScopes} at once, attempt budget ${attemptBudget}`);
|
|
@@ -1640,7 +1730,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1640
1730
|
if (census?.tripped) tripped.push(sid);
|
|
1641
1731
|
}
|
|
1642
1732
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1643
|
-
return await
|
|
1733
|
+
return await settleAtGateH({
|
|
1644
1734
|
status: "gate_h", breaker: tripped.length ? "attempt_budget" : "none", stalled: "no_green",
|
|
1645
1735
|
tripped_scopes: tripped, unapplied_results: allUnapplied,
|
|
1646
1736
|
hammer_proposals: allHammer, green_scopes: allGreen,
|
|
@@ -1755,13 +1845,13 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1755
1845
|
// SHIP" once EVAL is skipped, never spends another round waiting on a verdict nobody is producing.
|
|
1756
1846
|
if (verdict === "pass" || verdict === "not-evaluated") break; // → QA → GATE H → ship
|
|
1757
1847
|
if (g3.decision === "stop" || round >= maxRounds) {
|
|
1758
|
-
return await
|
|
1848
|
+
return await settleAtGateH({ status: "gate_h", breaker: "outer", unapplied_results: allUnapplied, hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1759
1849
|
}
|
|
1760
1850
|
round += 1;
|
|
1761
1851
|
}
|
|
1762
1852
|
|
|
1763
1853
|
if (verdict !== "pass" && verdict !== "not-evaluated") {
|
|
1764
|
-
return await
|
|
1854
|
+
return await settleAtGateH({ status: "gate_h", breaker: "outer", unapplied_results: allUnapplied, hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1765
1855
|
}
|
|
1766
1856
|
|
|
1767
1857
|
// ---- QA (post-PASS, pre-ship) — a level-up, never a gate. `--no-qa` answers it "skip". --------
|
|
@@ -1786,7 +1876,7 @@ phase("Ship");
|
|
|
1786
1876
|
const h = await worker({
|
|
1787
1877
|
skill: "scope-hammer", operation: "hammer", schema: HAMMER, phase: "Ship", label: "hammer",
|
|
1788
1878
|
payload: { feature: slug, qa_findings: qaFindings, hammer_proposals: allHammer },
|
|
1789
|
-
extra:
|
|
1879
|
+
extra: HAMMER_EXTRA,
|
|
1790
1880
|
});
|
|
1791
1881
|
if (h.__failed) return await withWarnings(diedAt("H", h));
|
|
1792
1882
|
if (h.verdict === "cannot-ship") {
|
|
@@ -1804,6 +1894,14 @@ if (h.verdict === "cannot-ship") {
|
|
|
1804
1894
|
// default). Hardcoding PASS here is exactly the silent upgrade protocol.md's Rules forbid.
|
|
1805
1895
|
const shipVerdict = verdict === "not-evaluated" ? "not-evaluated" : "PASS";
|
|
1806
1896
|
const ship = await cmd(`reduce ship --slug ${slug} --verdict ${shipVerdict} --qa ${qaRan ? "run" : "skipped"}`, "Ship", "ship-report");
|
|
1897
|
+
// GATE L4 HAS A CALL SITE. It used to be a line of prose in the tech lead's references — "resolve
|
|
1898
|
+
// the gate itself before any of the above" — and a run reached its ship decision with no ledger row
|
|
1899
|
+
// for it. The resolver narrows `ship` on the census artifact the hammer just wrote.
|
|
1900
|
+
{
|
|
1901
|
+
const g = await crossGate("L4", "Ship", ["ship", "hold", "ask"], { verdict: shipVerdict, census: h.verdict, cut_list: h.cut_list });
|
|
1902
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1903
|
+
if (g.decision === "hold") return await withWarnings({ status: "aborted", aborted_at: "L4", reason: `GATE L4 answered "hold" (census ${h.verdict}, verdict ${shipVerdict})` });
|
|
1904
|
+
}
|
|
1807
1905
|
await advisory(`report export --slug ${slug}`, "Ship", "export-run");
|
|
1808
1906
|
// The run's own concurrency, printed once where the records are complete and before the next run
|
|
1809
1907
|
// supersedes the trace. It is a projection over `receipts/dispatch.jsonl` and `legs.jsonl`, so it
|