shapeup-sdlc 3.18.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.example.json +2 -3
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +115 -56
- package/README.md +101 -143
- package/SECURITY.md +16 -25
- package/agents/orient.md +14 -0
- package/agents/qa-edge-hunter.md +13 -0
- package/agents/spec-evaluator.md +14 -0
- package/agents/task-executor.md +14 -0
- package/bin/init.mjs +13 -16
- package/bin/lib/grant.mjs +8 -19
- package/commands/build.md +9 -14
- package/commands/eval.md +3 -3
- package/commands/hammer.md +9 -5
- package/commands/retro.md +12 -11
- package/commands/scopes.md +1 -12
- package/commands/ship.md +45 -88
- package/commands/wire.md +1 -8
- package/hooks/dispatch-receipt.mjs +94 -8
- package/hooks/hooks.json +0 -41
- package/hooks/lib/decision.mjs +6 -9
- package/hooks/lib/tier.mjs +50 -0
- package/hooks/sandbox-guard.mjs +140 -64
- package/kernel/gate.mjs +203 -22
- package/kernel/harness.mjs +20 -67
- package/kernel/init/run.mjs +175 -523
- package/kernel/lib/contract.mjs +9 -93
- package/kernel/lib/floor.mjs +64 -0
- package/kernel/lib/paths.mjs +39 -112
- package/kernel/order.mjs +157 -0
- package/kernel/probe/digest.mjs +2 -38
- package/kernel/schemas/domain.schema.json +146 -1125
- package/kernel/schemas/work-order.schema.json +8 -4
- package/kernel/schemas/work-result.schema.json +1 -1
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +27 -8
- package/kernel/verify/t0.mjs +2 -2
- package/package.json +4 -2
- package/skills/building/SKILL.md +144 -0
- package/skills/building/resources/gates.md +76 -0
- package/skills/building/resources/guidance.md +23 -0
- package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
- package/skills/building/resources/intake.md +157 -0
- package/skills/building/resources/orders.md +69 -0
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
- package/skills/building/resources/plan-scopes.md +95 -0
- package/skills/building/resources/plan-spec.md +102 -0
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
- package/skills/building/resources/plan-wire.md +82 -0
- package/skills/building/resources/plan.md +98 -0
- package/skills/building/resources/retro-research.md +60 -0
- package/skills/building/resources/retro-scan.md +49 -0
- package/skills/building/resources/retro.md +225 -0
- package/skills/building/resources/rounds.md +78 -0
- package/skills/building/resources/stop.md +127 -0
- package/skills/orient/SKILL.md +37 -37
- package/skills/qa-edge-hunter/SKILL.md +29 -14
- package/skills/shapeup/SKILL.md +5 -5
- package/skills/shapeup/resources/breadboarding.md +1 -1
- package/skills/shapeup/resources/context-compaction.md +5 -5
- package/skills/shapeup/resources/shaping.md +1 -1
- package/skills/spec-evaluator/SKILL.md +17 -21
- package/skills/spec-evaluator/references/dimension-contract.md +1 -1
- package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
- package/skills/task-executor/SKILL.md +11 -15
- package/commands/hill.md +0 -10
- package/hooks/gate-intake.mjs +0 -109
- package/hooks/gate-zerowork.mjs +0 -349
- package/hooks/tier-guard.mjs +0 -162
- package/kernel/compile.mjs +0 -1419
- package/kernel/init/fit.mjs +0 -199
- package/kernel/init/run-args.mjs +0 -206
- package/kernel/lib/breadboard.mjs +0 -165
- package/kernel/probe/attempts.mjs +0 -161
- package/kernel/probe/concurrency.mjs +0 -535
- package/kernel/probe/eval.mjs +0 -405
- package/kernel/probe/hunt.mjs +0 -104
- package/kernel/probe/leg.mjs +0 -229
- package/kernel/probe/owner.mjs +0 -142
- package/kernel/probe/requirements.mjs +0 -328
- package/kernel/probe/resume.mjs +0 -1110
- package/kernel/probe/rounds.mjs +0 -115
- package/kernel/probe/staged.mjs +0 -83
- package/kernel/probe/stats.mjs +0 -477
- package/kernel/probe/t0.mjs +0 -115
- package/kernel/reduce/board.mjs +0 -350
- package/kernel/reduce/graph.mjs +0 -482
- package/kernel/reduce/hill.mjs +0 -322
- package/kernel/reduce/ingest.mjs +0 -767
- package/kernel/reduce/leftovers.mjs +0 -159
- package/kernel/reduce/ship.mjs +0 -529
- package/kernel/reduce/snapshot.mjs +0 -291
- package/kernel/reduce/verdict.mjs +0 -172
- package/kernel/report/export.mjs +0 -384
- package/kernel/report/facts.mjs +0 -234
- package/kernel/verify/budget.mjs +0 -153
- package/kernel/verify/build.mjs +0 -428
- package/kernel/verify/dispatch.mjs +0 -114
- package/kernel/verify/spec.mjs +0 -975
- package/kernel/verify/trace.mjs +0 -604
- package/skills/ba-pitch-analyzer/SKILL.md +0 -204
- package/skills/coach/SKILL.md +0 -397
- package/skills/hill-chart/SKILL.md +0 -154
- package/skills/hill-chart/assets/dashboard.template.html +0 -1432
- package/skills/scope-architect/SKILL.md +0 -174
- package/skills/scope-hammer/SKILL.md +0 -231
- package/skills/solution-architect/SKILL.md +0 -158
- package/skills/tech-lead/SKILL.md +0 -151
- package/skills/tech-lead/references/gates.md +0 -605
- package/skills/tech-lead/references/protocol.md +0 -889
- package/skills/tech-lead/references/tiny-lane.md +0 -53
- package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
- package/skills/translator/SKILL.md +0 -258
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
package/kernel/gate.mjs
CHANGED
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
// 5 no answer and on_missing=abort → the run aborts here, attributably
|
|
51
51
|
// 2 usage / validation error
|
|
52
52
|
|
|
53
|
-
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
54
|
-
import { parseBoard } from "./reduce/board.mjs";
|
|
53
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
55
54
|
import { join, dirname } from "node:path";
|
|
56
55
|
import { runArgs } from "./lib/argv.mjs";
|
|
57
|
-
import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, tasksDir, hammerCensus, readRunId, harnessRun } from "./lib/paths.mjs";
|
|
56
|
+
import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, ordersDir, qaDir, tasksDir, hammerCensus, readRunId, harnessRun, insideRunTrace, runArgsPath, dispatchReceipts } from "./lib/paths.mjs";
|
|
57
|
+
import { belowFloor } from "./lib/floor.mjs";
|
|
58
58
|
|
|
59
59
|
export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
|
|
60
60
|
|
|
@@ -93,7 +93,7 @@ export const PRESETS = {
|
|
|
93
93
|
"L2": { decision: "proceed", note: CI_NOTE },
|
|
94
94
|
"L3": { decision: "loop", max_rounds: 3, note: "FAIL → fix round r+1, up to the run's max_rounds. The circuit breaker, not this file, ends the loop." },
|
|
95
95
|
"QA": { decision: "run", note: "QA is a level-up, not a gate — it discovers, it cannot block." },
|
|
96
|
-
"H": { decision: "accept-cut-list", note: "
|
|
96
|
+
"H": { decision: "accept-cut-list", note: "The census's cut list is accepted as proposed; baseline comparison still runs and is still recorded." },
|
|
97
97
|
"L4": { decision: "ship", note: "Ship sign-off pre-approved. THIS is the one a reviewer should look at first when auditing a headless run." },
|
|
98
98
|
"COACH-1": { decision: "skip", note: "No live PO to categorize feedback; retro rules are not filed from an unattended run." },
|
|
99
99
|
},
|
|
@@ -109,7 +109,7 @@ export const PRESETS = {
|
|
|
109
109
|
answers: {
|
|
110
110
|
"L0": { decision: "proceed", note: "Config gate — mechanical." },
|
|
111
111
|
"L1a": { decision: "proceed", note: "Orient review — advisory read." },
|
|
112
|
-
"L1a.5": { decision: "proceed", note: "Wiring review
|
|
112
|
+
"L1a.5": { decision: "proceed", note: "Wiring review." },
|
|
113
113
|
"L1b": { decision: "ask", note: "Board review is where scope is actually decided. Not pre-approvable." },
|
|
114
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." },
|
|
115
115
|
"L3": { decision: "loop", max_rounds: 3, note: "Loop on FAIL; the breaker ends it." },
|
|
@@ -169,17 +169,16 @@ export function validate(set) {
|
|
|
169
169
|
* Returns { gate, decision, source, note, status } where status ∈ ok | ask | abort.
|
|
170
170
|
* Pure: the CLI turns `status` into the exit code, nothing here exits.
|
|
171
171
|
*/
|
|
172
|
-
/** The three verdicts
|
|
172
|
+
/** The three verdicts the stop census may carry (`HammerCensus.verdict` in the domain registry). */
|
|
173
173
|
export const HAMMER_VERDICTS = ["ship-now", "ship-after-fixes", "cannot-ship"];
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
176
|
* The census verdict on disk for one run, or null when no census has run.
|
|
177
177
|
*
|
|
178
|
-
* Derived from the
|
|
179
|
-
* product of a
|
|
180
|
-
* believes. `null` is a real answer here — "
|
|
181
|
-
*
|
|
182
|
-
* hammer is dispatched.
|
|
178
|
+
* Derived from the census file the stop step wrote rather than taken from a caller: the verdict is the
|
|
179
|
+
* product of a step, and a gate that accepts it as an argument accepts whatever the caller
|
|
180
|
+
* believes. `null` is a real answer here — "the census has not run" — and is the state a run that
|
|
181
|
+
* routed to GATE H from a circuit breaker is in, until the stop step has written one.
|
|
183
182
|
*
|
|
184
183
|
* @param {string} cwd - Project root.
|
|
185
184
|
* @param {string} slug - Feature slug.
|
|
@@ -187,9 +186,9 @@ export const HAMMER_VERDICTS = ["ship-now", "ship-after-fixes", "cannot-ship"];
|
|
|
187
186
|
*/
|
|
188
187
|
export function censusVerdict(cwd, slug) {
|
|
189
188
|
if (!slug) return null;
|
|
190
|
-
// The census artifact — written by the
|
|
191
|
-
//
|
|
192
|
-
//
|
|
189
|
+
// The census artifact — written by the stop step. The WorkResult fallback below is kept for a lane
|
|
190
|
+
// that wrote one the old way; a WorkResult may not carry a verdict under the schema, so in practice
|
|
191
|
+
// the artifact is the only source.
|
|
193
192
|
try {
|
|
194
193
|
const c = hammerCensus(cwd, slug);
|
|
195
194
|
if (existsSync(c)) {
|
|
@@ -207,6 +206,135 @@ export function censusVerdict(cwd, slug) {
|
|
|
207
206
|
} catch { return null; }
|
|
208
207
|
}
|
|
209
208
|
|
|
209
|
+
/**
|
|
210
|
+
* The number of charters a hunt report says it ran.
|
|
211
|
+
*
|
|
212
|
+
* @param {(string|null)} text - The hunt report's text.
|
|
213
|
+
* @returns {(number|null)} The run count from its `charters: <run>/<approved>` line, or null.
|
|
214
|
+
*/
|
|
215
|
+
export function chartersRun(text) {
|
|
216
|
+
const m = String(text ?? "").match(/^charters:\s*(\d+)/m);
|
|
217
|
+
return m ? Number(m[1]) : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Why a run that owes a QA hunt cannot ship, or null when it owes none or paid it.
|
|
224
|
+
*
|
|
225
|
+
* Derived from files, never from the census (the loop writes that itself). A hunt is owed when the
|
|
226
|
+
* evaluator passed a round (or the round was not evaluated) and the run was not launched with
|
|
227
|
+
* `noQa` and no QA row of this run says `skip`. It is paid when `results/hunt.json` exists and, for
|
|
228
|
+
* a `done` result over an app its order could reach, the hunt report ran at least one charter. A
|
|
229
|
+
* `failed` hunt says it could not hunt, which is honest and accepted. A run that never reached a
|
|
230
|
+
* passing round owes nothing: a build that has not passed is not worth edge-hunting.
|
|
231
|
+
*
|
|
232
|
+
* The case this exists for is the one nothing else sees: QA answered `run` and the hunter never
|
|
233
|
+
* dispatched, so no result exists to be wrong. An explicit `skip` answer is the recorded way out.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} cwd - Project root.
|
|
236
|
+
* @param {string} slug - Feature slug.
|
|
237
|
+
* @returns {(string|null)} A reason phrased for the PO, or null.
|
|
238
|
+
*/
|
|
239
|
+
export function qaProblem(cwd, slug) {
|
|
240
|
+
if (!slug) return null;
|
|
241
|
+
if (readJson(runArgsPath(cwd, slug))?.noQa) return null;
|
|
242
|
+
const runId = readRunId(cwd, slug);
|
|
243
|
+
let rows = [];
|
|
244
|
+
try {
|
|
245
|
+
rows = readFileSync(gatesPath(cwd, slug), "utf8").split("\n").filter(Boolean)
|
|
246
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
247
|
+
.filter((r) => r && r.run_id === runId && r.status === "ok");
|
|
248
|
+
} catch { return null; }
|
|
249
|
+
const qa = rows.filter((r) => r.gate === "QA").at(-1);
|
|
250
|
+
if (qa?.decision === "skip") return null;
|
|
251
|
+
const owed = rows.some((r) => r.gate === "L3" && ["pass", "not-evaluated"].includes(r.verdict));
|
|
252
|
+
if (!qa) return owed ? "the round passed but GATE QA was never crossed" : null;
|
|
253
|
+
if (qa.decision !== "run") return null;
|
|
254
|
+
const hunt = join(resultsDir(cwd, slug), "hunt.json");
|
|
255
|
+
if (!existsSync(hunt)) return "GATE QA was answered run and no hunt was dispatched (no results/hunt.json)";
|
|
256
|
+
const result = readJson(hunt);
|
|
257
|
+
if (!result) return "the hunt result cannot be read";
|
|
258
|
+
if (result.status !== "done") return null;
|
|
259
|
+
const payload = readJson(join(ordersDir(cwd, slug), "hunt.json"))?.payload || {};
|
|
260
|
+
if (!payload.launch_cmd && !payload.app_url) return null;
|
|
261
|
+
let report = null;
|
|
262
|
+
try { report = readFileSync(join(qaDir(cwd, slug), "hunt-report.md"), "utf8"); } catch { /* no report */ }
|
|
263
|
+
const n = chartersRun(report);
|
|
264
|
+
if (n === null) return "the hunt returned done with no hunt report carrying a `charters:` line";
|
|
265
|
+
return n > 0 ? null : "the hunt returned done over an app its order could reach, having run 0 charters";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The launch-record role each worker runs under; `qa` is never held to the floor. */
|
|
269
|
+
export const WORKER_ROLE = { orient: "exec", "task-executor": "exec", "spec-evaluator": "eval", "qa-edge-hunter": "qa" };
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Whether a launch record's model names the model a receipt says ran.
|
|
273
|
+
*
|
|
274
|
+
* The record carries what the launcher typed — an alias (`sonnet`) or an id — and the receipt the id the
|
|
275
|
+
* host resolved (`claude-sonnet-5-5`), so equality is too strict and the alias is looked for inside the
|
|
276
|
+
* id. Nothing to compare on either side is not a mismatch: a mismatch is a claim, and it needs both names.
|
|
277
|
+
*
|
|
278
|
+
* @param {*} asked - The launch record's model for the role.
|
|
279
|
+
* @param {*} ran - The receipt's `usage.model`.
|
|
280
|
+
* @returns {boolean} True when they name the same model, or when one side is missing.
|
|
281
|
+
*/
|
|
282
|
+
export function sameModel(asked, ran) {
|
|
283
|
+
const a = String(asked ?? "").trim().toLowerCase();
|
|
284
|
+
const r = String(ran ?? "").trim().toLowerCase();
|
|
285
|
+
if (!a || !r) return true;
|
|
286
|
+
return a === r || r.includes(a) || a.includes(r);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The model each role ran on, read off this run's dispatch receipts, against the launch record.
|
|
291
|
+
*
|
|
292
|
+
* WHY THE GATE READS THIS. The floor is checked when the run opens, on the launch record — a request.
|
|
293
|
+
* The agents are defined `model: inherit`, so the model that ran is the launching session's unless the
|
|
294
|
+
* dispatch passed one, and the receipts are the only record of it. Across every archived run the record
|
|
295
|
+
* asked for `sonnet` and two runs that were compared against each other had run their workers on two
|
|
296
|
+
* different models, which no reader of the record could see. So the ship gate reads the receipts: an
|
|
297
|
+
* `exec` or `eval` worker that ran below the floor is the floor's own refusal, applied to what ran rather
|
|
298
|
+
* than to what was asked; any other difference is written down, because a cost or a verdict read against
|
|
299
|
+
* the wrong model is a figure about a different run.
|
|
300
|
+
*
|
|
301
|
+
* A receipt with no model (a background launch reports none) is counted as `unreported` and claims
|
|
302
|
+
* nothing. Rows of another run are ignored.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} cwd - Project root.
|
|
305
|
+
* @param {string} slug - Feature slug.
|
|
306
|
+
* @returns {{asked: object, ran: object, unreported: number, mismatch: string[], below_floor: string[]}}
|
|
307
|
+
* `ran` maps role → model id → receipt count; `mismatch` and `below_floor` are sentences for the PO.
|
|
308
|
+
*/
|
|
309
|
+
export function modelEvidence(cwd, slug) {
|
|
310
|
+
const asked = readJson(runArgsPath(cwd, slug))?.models ?? {};
|
|
311
|
+
const runId = readRunId(cwd, slug);
|
|
312
|
+
let rows = [];
|
|
313
|
+
try {
|
|
314
|
+
rows = readFileSync(dispatchReceipts(cwd, slug), "utf8").split("\n").filter(Boolean)
|
|
315
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
316
|
+
.filter((r) => r && (!runId || r.run_id === runId));
|
|
317
|
+
} catch { rows = []; }
|
|
318
|
+
const ran = { exec: {}, eval: {}, qa: {} };
|
|
319
|
+
let unreported = 0;
|
|
320
|
+
for (const r of rows) {
|
|
321
|
+
const role = WORKER_ROLE[r.skill_invoked] ?? null;
|
|
322
|
+
if (!role) continue;
|
|
323
|
+
const m = typeof r.usage?.model === "string" && r.usage.model ? r.usage.model : null;
|
|
324
|
+
if (!m) { unreported++; continue; }
|
|
325
|
+
ran[role][m] = (ran[role][m] ?? 0) + 1;
|
|
326
|
+
}
|
|
327
|
+
const n = (k) => `${k} receipt${k === 1 ? "" : "s"}`;
|
|
328
|
+
const mismatch = [], below = [];
|
|
329
|
+
for (const role of ["exec", "eval", "qa"]) {
|
|
330
|
+
for (const [m, k] of Object.entries(ran[role])) {
|
|
331
|
+
if (!sameModel(asked[role], m)) mismatch.push(`${role}: the launch record asked for ${asked[role] ?? "no model"}, ${n(k)} name${k === 1 ? "s" : ""} ${m}`);
|
|
332
|
+
if (role !== "qa" && belowFloor(m)) below.push(`${role} ran on ${m} in ${n(k)}, below the model floor (sonnet or above)`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { asked, ran, unreported, mismatch, below_floor: below };
|
|
336
|
+
}
|
|
337
|
+
|
|
210
338
|
/**
|
|
211
339
|
* Narrow a resolved gate answer to what the run's own evidence supports.
|
|
212
340
|
*
|
|
@@ -215,10 +343,10 @@ export function censusVerdict(cwd, slug) {
|
|
|
215
343
|
* census cleared the run, so `ship` is only on the menu when a census exists and did clear it.
|
|
216
344
|
*
|
|
217
345
|
* Measured on a consumer: a run whose dispatch receipts carried `orient` and `task-executor` and
|
|
218
|
-
* nothing else —
|
|
346
|
+
* nothing else — no census was ever written — recorded `H → accept-cut-list` and `L4 → ship`
|
|
219
347
|
* from the `ci` preset, while its own ledger read `status: escalated`, `final_verdict: ~`, every
|
|
220
|
-
* requirement at `no evidence`.
|
|
221
|
-
*
|
|
348
|
+
* requirement at `no evidence`. The run script did guard this, but only after the census
|
|
349
|
+
* step, so a run that returns `gate_h` from a breaker reaches L4 by a route the guard does not
|
|
222
350
|
* cover. A check on one call site is not a property.
|
|
223
351
|
*
|
|
224
352
|
* Absence of a census disqualifies `ship` on its own: "no one looked" and "someone looked and it
|
|
@@ -237,7 +365,7 @@ export function narrowToEvidence(r, cwd, slug) {
|
|
|
237
365
|
// among allowed answers and cannot supply the evidence that makes one allowed.
|
|
238
366
|
if (r?.gate === "L2" && r?.status === "ok" && r?.decision === "proceed" && slug) {
|
|
239
367
|
let tasks = 0;
|
|
240
|
-
try { tasks =
|
|
368
|
+
try { tasks = readdirSync(tasksDir(cwd, slug)).filter((f) => /^TASK-[\w.-]+\.md$/i.test(f)).length; } catch { tasks = 0; }
|
|
241
369
|
if (tasks === 0) {
|
|
242
370
|
return {
|
|
243
371
|
gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
|
|
@@ -252,10 +380,36 @@ export function narrowToEvidence(r, cwd, slug) {
|
|
|
252
380
|
}
|
|
253
381
|
if (r?.gate !== "L4" || r?.status !== "ok" || r?.decision !== "ship") return r;
|
|
254
382
|
const verdict = censusVerdict(cwd, slug);
|
|
255
|
-
if (verdict === "ship-now" || verdict === "ship-after-fixes")
|
|
383
|
+
if (verdict === "ship-now" || verdict === "ship-after-fixes") {
|
|
384
|
+
const qa = qaProblem(cwd, slug);
|
|
385
|
+
if (!qa) {
|
|
386
|
+
// The floor, applied to what ran. Opening the run checked the launch record; the receipts say
|
|
387
|
+
// which model each worker actually ran on, and `ship` over a build or a verdict produced below
|
|
388
|
+
// the floor is the same refusal one step later. A model that merely differs from the record is
|
|
389
|
+
// carried on the result and into the row, not refused: the PO may well have meant it.
|
|
390
|
+
const models = modelEvidence(cwd, slug);
|
|
391
|
+
if (models.below_floor.length) {
|
|
392
|
+
return {
|
|
393
|
+
gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
|
|
394
|
+
refused: "ship", census: verdict, models,
|
|
395
|
+
reason: `GATE L4 cannot be answered "ship": ${models.below_floor.join("; ")}. The floor was checked on ` +
|
|
396
|
+
`the launch record when the run opened; the dispatch receipts say what ran. Re-run the role on a ` +
|
|
397
|
+
`model at or above the floor, or put the block to the PO.`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return { ...r, models };
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
|
|
404
|
+
refused: "ship", census: verdict, qa,
|
|
405
|
+
reason: `GATE L4 cannot be answered "ship": ${qa}. Dispatch the hunt, or answer GATE QA \`skip\` ` +
|
|
406
|
+
`(a recorded decision) and resolve L4 again. An answer chooses among the answers a gate ` +
|
|
407
|
+
`allows; it cannot supply the evidence that makes one allowed.`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
256
410
|
const why = verdict === "cannot-ship"
|
|
257
|
-
? "
|
|
258
|
-
: "
|
|
411
|
+
? "the census returned CANNOT SHIP"
|
|
412
|
+
: "the census has not been written, so none exists";
|
|
259
413
|
return {
|
|
260
414
|
gate: r.gate, status: "ask", source: r.source, decision: "ask", note: r.note,
|
|
261
415
|
refused: "ship", census: verdict,
|
|
@@ -415,6 +569,11 @@ export const ARGV_SPEC = {
|
|
|
415
569
|
// before any verdict exists — "loop" on a FAIL — and its note says so; recorded without the
|
|
416
570
|
// verdict, a PASS round's row read "FAIL → fix round", the opposite of what happened.
|
|
417
571
|
verdict: { type: "str" },
|
|
572
|
+
// A decision the PO just gave in the session that reached the gate. It resolves THIS crossing with
|
|
573
|
+
// the source `PO (live)` and needs no answer file: the ledger row records who decided, and the
|
|
574
|
+
// evidence narrowing (`ship` needs a clearing census) still applies — an answer chooses among the
|
|
575
|
+
// answers a gate allows, never the evidence that makes one allowed.
|
|
576
|
+
answer: { type: "str" },
|
|
418
577
|
};
|
|
419
578
|
|
|
420
579
|
/**
|
|
@@ -434,6 +593,11 @@ export function gateNote(r, verdict) {
|
|
|
434
593
|
if (r.gate === "L3" && verdict === "pass") {
|
|
435
594
|
return `verdict PASS — the "${r.decision}" answer applies only to a failed round; the run goes on to QA and GATE H`;
|
|
436
595
|
}
|
|
596
|
+
// A ship over workers that ran on a model other than the launch record's says so in the row itself,
|
|
597
|
+
// where whoever reads the ledger for a cost or a verdict will see it.
|
|
598
|
+
if (r.gate === "L4" && r.decision === "ship" && r.models?.mismatch?.length) {
|
|
599
|
+
return `${own ?? ""}${own ? " — " : ""}model differs from the launch record: ${r.models.mismatch.join("; ")}`;
|
|
600
|
+
}
|
|
437
601
|
return own;
|
|
438
602
|
}
|
|
439
603
|
|
|
@@ -456,6 +620,10 @@ function die(msg, code = 2) {
|
|
|
456
620
|
export function cli(rawArgv) {
|
|
457
621
|
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
458
622
|
const cwd = args.cwd || process.cwd();
|
|
623
|
+
if (args.resolve && insideRunTrace(cwd)) {
|
|
624
|
+
console.error(`✋ gate: ${cwd} is inside a run trace (${LOCAL}/); run it from the project root, or its row lands in a nested ledger.`);
|
|
625
|
+
process.exit(2);
|
|
626
|
+
}
|
|
459
627
|
|
|
460
628
|
if (args.init) {
|
|
461
629
|
const presetName = args.preset ?? "ci";
|
|
@@ -472,7 +640,18 @@ export function cli(rawArgv) {
|
|
|
472
640
|
out({ ok: true, wrote: dest, preset: presetName, authorized_by: set.authorized_by, gates: Object.keys(set.answers).length });
|
|
473
641
|
}
|
|
474
642
|
|
|
475
|
-
|
|
643
|
+
// A live answer builds a one-gate set instead of discovering one. `ask` is not an answer.
|
|
644
|
+
let found;
|
|
645
|
+
if (args.answer !== undefined && args.resolve) {
|
|
646
|
+
const valid = VALID_BY_GATE[args.resolve];
|
|
647
|
+
if (!valid) die(`unknown gate "${args.resolve}" — known: ${GATE_IDS.join(", ")}`);
|
|
648
|
+
if (args.answer === "ask" || !valid.includes(args.answer)) {
|
|
649
|
+
die(`"${args.answer}" is not a decision GATE ${args.resolve} takes — valid: ${valid.filter((d) => d !== "ask").join(", ")}`);
|
|
650
|
+
}
|
|
651
|
+
found = { set: { version: 1, preset: "custom", answers: { [args.resolve]: { decision: args.answer, note: "answered live by the PO" } } }, source: "PO (live)" };
|
|
652
|
+
} else {
|
|
653
|
+
found = discover({ cwd, file: args.file ?? null, preset: args.preset ?? null, slug: args.slug ?? null });
|
|
654
|
+
}
|
|
476
655
|
if (found.error) die(found.error);
|
|
477
656
|
const errs = validate(found.set);
|
|
478
657
|
if (errs.length) die(`invalid gate answer set (${found.source}):\n - ${errs.join("\n - ")}`);
|
|
@@ -531,6 +710,8 @@ export function cli(rawArgv) {
|
|
|
531
710
|
source: r.source ?? found.source, note: gateNote(r, verdict),
|
|
532
711
|
round: args.round ?? null,
|
|
533
712
|
...(verdict ? { verdict } : {}),
|
|
713
|
+
// What ran, per role, beside what was asked — the machine-readable half of the note above.
|
|
714
|
+
...(r.models ? { models: { asked: r.models.asked, ran: r.models.ran, unreported: r.models.unreported, mismatch: r.models.mismatch } } : {}),
|
|
534
715
|
});
|
|
535
716
|
}
|
|
536
717
|
if (r.status === "ask") out({ ...r, ok: false }, 4);
|
package/kernel/harness.mjs
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// harness — the kernel's single entry point.
|
|
3
3
|
//
|
|
4
4
|
// WHAT THIS IS. Everything in the pipeline that must be DETERMINISTIC — measured rather than
|
|
5
|
-
// claimed,
|
|
6
|
-
// subcommand of this one script:
|
|
5
|
+
// claimed, an answer file rather than a vibe — is a subcommand of this one script:
|
|
7
6
|
//
|
|
8
7
|
// node ${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs <verb> [<action>] [flags]
|
|
9
8
|
//
|
|
@@ -13,66 +12,34 @@
|
|
|
13
12
|
// matches nothing is indistinguishable from a rule that works until the first dispatch fails.
|
|
14
13
|
// Subcommands are free; entry points are not.
|
|
15
14
|
//
|
|
16
|
-
// WHO CALLS IT.
|
|
17
|
-
//
|
|
18
|
-
// and never parses stdout.
|
|
15
|
+
// WHO CALLS IT. The loop that drives a run (the `building` skill, in the session), worker skills
|
|
16
|
+
// from their own shells, and the hooks.
|
|
19
17
|
//
|
|
20
18
|
// THE VERBS, and the invariant each one exists to keep:
|
|
21
19
|
//
|
|
22
|
-
// verify t0 ·
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// probe resume · t0 · stats · digest · Read-only queries over run state. `concurrency`
|
|
34
|
-
// concurrency · leg · eval · answers how many legs ran at once and what the
|
|
35
|
-
// owner · requirements · attempts ·
|
|
36
|
-
// hunt
|
|
37
|
-
// fan-out bought, and refuses a figure the record set
|
|
38
|
-
// cannot support rather than printing a plausible one.
|
|
39
|
-
// `leg` answers whether a scope's work reached the
|
|
40
|
-
// board — a green T0 says its fixtures passed, not that
|
|
41
|
-
// its result was ever applied, and a leg that skipped
|
|
42
|
-
// its own ingest step is indistinguishable from one that
|
|
43
|
-
// did not until something reads the writer's own record.
|
|
44
|
-
// `eval` answers what an EVAL round's WorkResult
|
|
45
|
-
// actually said, mechanically — the round-loop branch
|
|
46
|
-
// reads this instead of trusting a dispatching agent's
|
|
47
|
-
// own end-of-turn summary of its own verdict. `owner`
|
|
48
|
-
// answers which scope may write a path, elected from
|
|
49
|
-
// the contracts — so a census cites it instead of
|
|
50
|
-
// asserting ownership from memory. `requirements`
|
|
51
|
-
// answers which pitch clause a verdict reached, joined
|
|
52
|
-
// through the plan's own covers: edge — the L4 line and
|
|
53
|
-
// GATE H's census cite it for the same reason.
|
|
54
|
-
// `attempts` answers how many of a scope's attempts are
|
|
55
|
-
// ATTESTED (a dispatch receipt AND a leg row or a
|
|
56
|
-
// WorkResult), never the order set or the T0 verdict
|
|
57
|
-
// set alone — the round loop's inner breaker and
|
|
58
|
-
// scope-hammer's census both cite it, so they cannot
|
|
59
|
-
// disagree about the same exhaustion again.
|
|
60
|
-
// init run · fit · run-args Opens a run, or refuses it (exit 3). `run-args`
|
|
61
|
-
// writes GATE L0.9b's launch record and echoes it.
|
|
62
|
-
// report export Projects the run's records as fact tables.
|
|
63
|
-
// compile The WorkOrder: schema-valid or nothing is dispatched.
|
|
20
|
+
// verify t0 · envelope · skills Measured, not claimed. A model verifying itself is claiming;
|
|
21
|
+
// these read artifacts and re-hash them. `t0` runs a scope's
|
|
22
|
+
// fixtures and keeps or reverts the tree; `envelope` checks an
|
|
23
|
+
// order or a result against its schema (and is the dispatch
|
|
24
|
+
// gate when called as a hook); `skills` reads the worker roster
|
|
25
|
+
// off disk.
|
|
26
|
+
// gate An answer file with a source, not a vibe.
|
|
27
|
+
// init run Opens a run, or refuses it (exit 3). Writes the receipt and
|
|
28
|
+
// the launch record, and refuses a model below the floor.
|
|
29
|
+
// order open · close Arms and retires the write fence's pointer, and records how a
|
|
30
|
+
// run ended.
|
|
64
31
|
//
|
|
65
32
|
// EXIT CODES are the subcommand's own and are part of its contract: 0 success · 1 ran, answer is
|
|
66
33
|
// no · 2 malformed input, nothing ran (`lib/argv.mjs`) · 3 `init run` refused to open · 4/5 gate
|
|
67
|
-
// stop/abort
|
|
68
|
-
//
|
|
34
|
+
// stop/abort. This dispatcher adds exactly one of its own: 2 for an unknown verb, which is the same
|
|
35
|
+
// "the input was malformed" case.
|
|
69
36
|
|
|
70
37
|
import { isMain } from "./lib/argv.mjs";
|
|
71
38
|
|
|
72
39
|
/**
|
|
73
40
|
* verb → action → module, the whole routing table.
|
|
74
41
|
*
|
|
75
|
-
* A verb whose value is a string takes no action word (`gate`, `
|
|
42
|
+
* A verb whose value is a string takes no action word (`gate`, `order`); a verb whose value is an
|
|
76
43
|
* object requires one, and `_default` names the action taken when it is omitted.
|
|
77
44
|
*
|
|
78
45
|
* Exported so the structural suite enumerates the subcommands from THIS table rather than from a
|
|
@@ -80,25 +47,11 @@ import { isMain } from "./lib/argv.mjs";
|
|
|
80
47
|
*/
|
|
81
48
|
export const ROUTES = {
|
|
82
49
|
verify: {
|
|
83
|
-
t0: "./verify/t0.mjs",
|
|
84
|
-
trace: "./verify/trace.mjs", spec: "./verify/spec.mjs", skills: "./verify/skills.mjs",
|
|
85
|
-
dispatch: "./verify/dispatch.mjs", build: "./verify/build.mjs",
|
|
50
|
+
t0: "./verify/t0.mjs", envelope: "./verify/envelope.mjs", skills: "./verify/skills.mjs",
|
|
86
51
|
},
|
|
87
|
-
reduce: {
|
|
88
|
-
ingest: "./reduce/ingest.mjs", hill: "./reduce/hill.mjs", snapshot: "./reduce/snapshot.mjs",
|
|
89
|
-
ship: "./reduce/ship.mjs", board: "./reduce/board.mjs", verdict: "./reduce/verdict.mjs",
|
|
90
|
-
graph: "./reduce/graph.mjs",
|
|
91
|
-
},
|
|
92
|
-
probe: {
|
|
93
|
-
resume: "./probe/resume.mjs", t0: "./probe/t0.mjs", stats: "./probe/stats.mjs",
|
|
94
|
-
digest: "./probe/digest.mjs", concurrency: "./probe/concurrency.mjs",
|
|
95
|
-
leg: "./probe/leg.mjs", eval: "./probe/eval.mjs", owner: "./probe/owner.mjs",
|
|
96
|
-
requirements: "./probe/requirements.mjs", attempts: "./probe/attempts.mjs", hunt: "./probe/hunt.mjs",
|
|
97
|
-
},
|
|
98
|
-
init: { run: "./init/run.mjs", fit: "./init/fit.mjs", "run-args": "./init/run-args.mjs" },
|
|
99
|
-
report: { export: "./report/export.mjs", _default: "export" },
|
|
100
52
|
gate: "./gate.mjs",
|
|
101
|
-
|
|
53
|
+
init: { run: "./init/run.mjs" },
|
|
54
|
+
order: "./order.mjs",
|
|
102
55
|
};
|
|
103
56
|
|
|
104
57
|
/** Every `<verb> <action>` pair, for the usage text and the unknown-verb rejection. */
|