shapeup-sdlc 3.4.0 → 3.6.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-plugin/plugin.json +1 -1
- package/AGENTS.md +16 -4
- package/README.md +7 -3
- package/SECURITY.md +1 -1
- package/hooks/sandbox-guard.mjs +69 -6
- package/kernel/compile.mjs +33 -12
- package/kernel/harness.mjs +10 -4
- package/kernel/init/run-args.mjs +206 -0
- package/kernel/init/run.mjs +10 -0
- package/kernel/lib/contract.mjs +68 -1
- package/kernel/lib/paths.mjs +10 -0
- package/kernel/probe/concurrency.mjs +31 -6
- package/kernel/probe/digest.mjs +15 -1
- package/kernel/probe/owner.mjs +4 -1
- package/kernel/probe/requirements.mjs +296 -0
- package/kernel/probe/resume.mjs +195 -6
- package/kernel/probe/rounds.mjs +104 -0
- package/kernel/reduce/graph.mjs +5 -2
- package/kernel/reduce/ingest.mjs +69 -15
- package/kernel/reduce/ship.mjs +52 -31
- package/kernel/reduce/snapshot.mjs +23 -2
- package/kernel/report/export.mjs +54 -2
- package/kernel/report/facts.mjs +24 -2
- package/{skills/tech-lead → kernel}/schemas/domain.schema.json +20 -12
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +1 -1
- package/kernel/verify/spec.mjs +130 -4
- package/kernel/verify/trace.mjs +16 -7
- package/package.json +1 -1
- package/skills/ba-pitch-analyzer/SKILL.md +16 -1
- package/skills/coach/SKILL.md +8 -2
- package/skills/hill-chart/SKILL.md +3 -4
- package/skills/scope-architect/SKILL.md +16 -1
- package/skills/scope-hammer/SKILL.md +11 -2
- package/skills/spec-evaluator/SKILL.md +12 -1
- package/skills/tech-lead/SKILL.md +10 -10
- package/skills/tech-lead/references/gates.md +70 -12
- package/skills/tech-lead/references/protocol.md +4 -2
- package/skills/tech-lead/workflows/shapeup-run.js +176 -38
- package/skills/translator/SKILL.md +1 -1
- /package/{skills/tech-lead → kernel}/schemas/gate-answers.schema.json +0 -0
- /package/{skills/tech-lead → kernel}/schemas/work-order.schema.json +0 -0
- /package/{skills/tech-lead → kernel}/schemas/work-result.schema.json +0 -0
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
//
|
|
34
34
|
// args — RunArgs (domain.schema.json $defs/RunArgs):
|
|
35
35
|
// slug, autoLevel (interactive|auto|unattended), answers (preset name or path),
|
|
36
|
-
// models {exec, eval, qa?}, budgets {maxRounds, attemptBudget
|
|
36
|
+
// models {exec, eval, qa?}, budgets {maxRounds, attemptBudget}, pluginRoot,
|
|
37
37
|
// startedAt, and the optional switches noEval / noQa / adversarialVerify /
|
|
38
38
|
// maxParallelScopes (default 4).
|
|
39
39
|
//
|
|
@@ -407,6 +407,9 @@ const RESUME = {
|
|
|
407
407
|
eval_dimensions: { type: "array", items: { type: "string" } },
|
|
408
408
|
has_orient_artifacts: { type: "boolean" },
|
|
409
409
|
has_spec_tree: { type: "boolean" },
|
|
410
|
+
// The requirements registry — a fact, not a phase. See the COVERAGE block below for why it is
|
|
411
|
+
// guarded on this bare boolean and never asked about through `probe resume --require`.
|
|
412
|
+
has_requirements: { type: "boolean" },
|
|
410
413
|
has_wiring_map: { type: "boolean" },
|
|
411
414
|
has_project_profile: { type: "boolean" },
|
|
412
415
|
// THE SHAPE THE KERNEL WRITES, not a convenient one. `probe resume` emits `{scope_id, path}` per
|
|
@@ -488,7 +491,7 @@ const ORIENT = {
|
|
|
488
491
|
required: ["ok", "artifact_written", "spiked_area", "spike_result"],
|
|
489
492
|
};
|
|
490
493
|
|
|
491
|
-
/** analyze / wire — "did the artifact land?" is all a gate needs from them. */
|
|
494
|
+
/** coverage / analyze / wire — "did the artifact land?" is all a gate needs from them. */
|
|
492
495
|
const PHASE_OK = {
|
|
493
496
|
type: "object",
|
|
494
497
|
properties: { ok: { type: "boolean" }, artifact_written: { type: "boolean" }, detail: { type: "string" } },
|
|
@@ -898,6 +901,47 @@ async function fastForward(gate, phaseKey, phaseName, what) {
|
|
|
898
901
|
`${phaseKey} artifact by hand before assuming it regressed, then relaunch.`);
|
|
899
902
|
}
|
|
900
903
|
|
|
904
|
+
/**
|
|
905
|
+
* The positive enforcer for GATE L0.9b's launch record: the RunArgs object this run was
|
|
906
|
+
* configured with, kernel-written to the run's own local root, is on disk before this file
|
|
907
|
+
* dispatches anything past Preflight.
|
|
908
|
+
*
|
|
909
|
+
* Nothing upstream of this call can be trusted to have written it. `SKILL.md` Step 2 tells the
|
|
910
|
+
* orchestrating session to run `harness init run-args` right before `Workflow(...)` is invoked, but
|
|
911
|
+
* that is prose the session reads, not a check anything runs — and a session that launches this
|
|
912
|
+
* script without having done it left no trace anywhere else: the receipt, `intake.md` and
|
|
913
|
+
* `harness-run.md` all exist regardless, and `dialFrom()` (`kernel/probe/concurrency.mjs`) answers
|
|
914
|
+
* every reader with a silent default rather than an error. So the record's existence would depend
|
|
915
|
+
* entirely on whether a prior step of prose was followed — exactly the shape this file's own header
|
|
916
|
+
* exists to retire (no rule with no enforcer). `probe concurrency --require-run-args` is a presence
|
|
917
|
+
* check, not a measurement: it shares `dialFrom()`'s run-root resolution and file path so there is
|
|
918
|
+
* one definition of where the record lives, not two.
|
|
919
|
+
*
|
|
920
|
+
* @returns {Promise<(object|null)>} An aborted RunReturn, or null when the record is there.
|
|
921
|
+
*/
|
|
922
|
+
async function requireLaunchRecord() {
|
|
923
|
+
const r = await cmd(`probe concurrency --slug ${slug} --require-run-args`, "Preflight", "launch-record");
|
|
924
|
+
if (r.exit_code === 0) return null;
|
|
925
|
+
// Same distinction requirePhase/fastForward draw: only exit 6 is the predicate genuinely
|
|
926
|
+
// answering "not there". Anything else means the check itself did not run.
|
|
927
|
+
if (r.exit_code === 6) {
|
|
928
|
+
return aborted("preflight",
|
|
929
|
+
`the run's launch record does not exist — GATE L0.9b's RunArgs (the model matrix, budgets and ` +
|
|
930
|
+
`every operator switch this run was typed with) was never written to disk before this launch, ` +
|
|
931
|
+
`so nothing downstream can be attested against what the operator actually configured. Run ` +
|
|
932
|
+
`\`harness init run-args\` (references/gates.md L0.9b names every flag it resolves — \`SKILL.md\` ` +
|
|
933
|
+
`Step 2 runs it right before this workflow launches) and relaunch.`);
|
|
934
|
+
}
|
|
935
|
+
return aborted("preflight",
|
|
936
|
+
`the launch-record check did not run to a verdict — exit_code ${r.exit_code}, not the 0 (present) ` +
|
|
937
|
+
`or 6 (absent) \`probe concurrency --require-run-args\` documents.` +
|
|
938
|
+
`${r.detail ? ` Courier reported: ${r.detail}.` : ""} This is NOT evidence the record is missing — ` +
|
|
939
|
+
`most often a Bash call denied above this plugin's own hooks and permission grant (an untrusted ` +
|
|
940
|
+
`workspace, or Claude Code's auto-mode classifier). Verify the run's launch record by hand — ` +
|
|
941
|
+
`\`harness probe concurrency --slug ${slug} --require-run-args\` — before assuming it is absent, ` +
|
|
942
|
+
`then relaunch.`);
|
|
943
|
+
}
|
|
944
|
+
|
|
901
945
|
// The ledger's `status` field is bookkeeping, not this file's resume oracle — the fast-forward reads
|
|
902
946
|
// artifacts. It survives because `reduce snapshot` and the the ship report's census hook read it to tell
|
|
903
947
|
// a run in flight from a finished one. A lost write is a degraded digest, not a corrupted build, so
|
|
@@ -914,7 +958,57 @@ async function setRunStatus(status, phaseName) {
|
|
|
914
958
|
stateWarnings.push(`status="${status}" did not take: ${why}`);
|
|
915
959
|
}
|
|
916
960
|
}
|
|
917
|
-
|
|
961
|
+
|
|
962
|
+
// A free-text reason, made safe to spell into a sub-agent's shell instruction: no quotes, no
|
|
963
|
+
// newlines, no backticks or `$` — every one of those risks breaking the command the sub-agent
|
|
964
|
+
// itself constructs from this file's own instruction text (see `cmd()`'s banner: every kernel call
|
|
965
|
+
// in this script is spelled into a prompt, not spawned directly). Truncated, not elided: the exit
|
|
966
|
+
// code and the ledger's own close_cause line are still the source of record — this is only what
|
|
967
|
+
// crosses the prompt boundary.
|
|
968
|
+
const causeArg = (s) => (String(s ?? "").replace(/[`"'$\\\n\r]/g, " ").replace(/\s+/g, " ").trim().slice(0, 300) || "no reason recorded");
|
|
969
|
+
|
|
970
|
+
// A terminal RunReturn closes the run's own ledger — a terminal status, its cause, and a close
|
|
971
|
+
// timestamp, in one write (`probe resume --close`). `aborted` and `shipped` are the two statuses
|
|
972
|
+
// this script itself ends a run on; `paused` resumes on relaunch and `gate_h` hands the rest of the
|
|
973
|
+
// run to the tech-lead skill's own orchestration (GATE H's census, then Ship) — neither is this
|
|
974
|
+
// script's close to make. Best-effort, the same discipline as `setRunStatus` above: a lost write
|
|
975
|
+
// degrades the trace's own record of why the run ended, it does not change what this return reports.
|
|
976
|
+
//
|
|
977
|
+
// A close can also come back `ok:true` and still be a degraded outcome: `closeRun`
|
|
978
|
+
// (kernel/probe/resume.mjs) refuses a DIFFERENT terminal status outright (that failure already hits
|
|
979
|
+
// the `!r.ok` branch below, exit 3), but the SAME status with a DIFFERENT cause — a run_id closed
|
|
980
|
+
// more than once across relaunches, e.g. `aborted` at Preflight then `aborted` again for a different
|
|
981
|
+
// reason after a later relaunch — is *superseded* rather than refused: `ok:true`, the new cause
|
|
982
|
+
// folded together with the old one on disk, and `decision:"superseded"` (the one bare token `cmd()`
|
|
983
|
+
// relays verbatim across the courier boundary — see its own banner) naming that outcome. Reporting
|
|
984
|
+
// that as a clean, silent success would be a state fact the trace needs with nothing surfacing it —
|
|
985
|
+
// the same shape as leaving a run's ledger open after it ended. It costs nothing this return itself
|
|
986
|
+
// reports — the ledger already folded the prior cause in — but the run's own RunReturn must say the
|
|
987
|
+
// trace is degraded.
|
|
988
|
+
async function closeIfTerminal(ret) {
|
|
989
|
+
if (ret.status !== "aborted" && ret.status !== "shipped") return;
|
|
990
|
+
const cause = ret.status === "aborted"
|
|
991
|
+
? `${ret.aborted_at || "?"}: ${ret.reason || "no reason recorded"}`
|
|
992
|
+
: `verdict=${ret.verdict ?? "?"} rounds=${ret.rounds_used ?? "?"} qa_findings=${ret.qa_findings ?? "?"}`;
|
|
993
|
+
const r = await cmd(`probe resume --slug ${slug} --close ${ret.status} --cause "${causeArg(cause)}"`, "Ship", `close:${ret.status}`);
|
|
994
|
+
if (!r.ok) {
|
|
995
|
+
const why = (r.detail || `exit ${r.exit_code}`).trim();
|
|
996
|
+
log(`RUN STATE — close(${ret.status}) did not take: ${why}. This return's own status and reason still ` +
|
|
997
|
+
`stand; only the ledger's own closed_at/close_cause record is degraded.`);
|
|
998
|
+
stateWarnings.push(`close(${ret.status}) did not take: ${why}`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (String(r.decision ?? "").trim() === "superseded") {
|
|
1002
|
+
log(`RUN STATE — close(${ret.status}) superseded an earlier close recorded under the same status ` +
|
|
1003
|
+
`but a different cause — this run_id was closed more than once. Both causes are on the ledger's ` +
|
|
1004
|
+
`own close_cause line; this return's trace is degraded, not corrupted.`);
|
|
1005
|
+
stateWarnings.push(`close(${ret.status}) superseded an earlier close of this run_id — see harness-run.md's close_cause for both reasons`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const withWarnings = async (ret) => {
|
|
1009
|
+
await closeIfTerminal(ret);
|
|
1010
|
+
return stateWarnings.length ? { ...ret, state_warnings: stateWarnings } : ret;
|
|
1011
|
+
};
|
|
918
1012
|
|
|
919
1013
|
// =============================================================================================
|
|
920
1014
|
// THE RUN
|
|
@@ -947,20 +1041,25 @@ await agent(
|
|
|
947
1041
|
);
|
|
948
1042
|
const canary = await cmd(`verify dispatch --skill ${canarySkill} --within 900`, "Preflight", "canary-evidence");
|
|
949
1043
|
if (!canary.ok) {
|
|
950
|
-
return aborted("preflight",
|
|
1044
|
+
return await withWarnings(aborted("preflight",
|
|
951
1045
|
`the ${canarySkill} skill did not resolve in this session — no dispatch reached the hook layer. ` +
|
|
952
1046
|
`A run would report phases completing while the sub-agents improvised every worker's craft. ` +
|
|
953
1047
|
`Load the plugin (\`claude --plugin-dir <repo>\`, or install and enable it) and relaunch. ` +
|
|
954
|
-
`(${canary.detail || `exit ${canary.exit_code}`})`);
|
|
1048
|
+
`(${canary.detail || `exit ${canary.exit_code}`})`));
|
|
955
1049
|
}
|
|
956
1050
|
|
|
1051
|
+
// GATE L0.9b's launch record must exist before anything past Preflight dispatches; see
|
|
1052
|
+
// requireLaunchRecord()'s own banner for why this cannot be left to Step 2's prose alone.
|
|
1053
|
+
const launchRecordAbort = await requireLaunchRecord();
|
|
1054
|
+
if (launchRecordAbort) return await withWarnings(launchRecordAbort);
|
|
1055
|
+
|
|
957
1056
|
phase("Orient");
|
|
958
1057
|
|
|
959
1058
|
const rs = await query(`probe resume --slug ${slug}`, RESUME, "Orient", "resume-state");
|
|
960
1059
|
// A probe that produced nothing is not an EMPTY run — it is an unknown one. Treating it as empty
|
|
961
1060
|
// would re-dispatch every phase from the top, over a run that may be in progress.
|
|
962
1061
|
if (!rs) {
|
|
963
|
-
return aborted("probe", "the fast-forward derivation returned no state — refusing to re-dispatch a run that may already be in progress");
|
|
1062
|
+
return await withWarnings(aborted("probe", "the fast-forward derivation returned no state — refusing to re-dispatch a run that may already be in progress"));
|
|
964
1063
|
}
|
|
965
1064
|
|
|
966
1065
|
const specFolder = rs.spec_folder || `shapeup/${slug}/spec/`;
|
|
@@ -988,14 +1087,14 @@ if (!rs.has_orient_artifacts) {
|
|
|
988
1087
|
"when the risk scan came back rank 0). Any other filename leaves the phase incomplete and " +
|
|
989
1088
|
"the run aborts, however good the contents are.",
|
|
990
1089
|
});
|
|
991
|
-
if (o.__failed) return diedAt("ORIENT", o);
|
|
1090
|
+
if (o.__failed) return await withWarnings(diedAt("ORIENT", o));
|
|
992
1091
|
const post = await requirePhase("ORIENT", "orient", "Orient");
|
|
993
|
-
if (post) return withWarnings(post);
|
|
1092
|
+
if (post) return await withWarnings(post);
|
|
994
1093
|
await advisory(`reduce graph --slug ${slug}`, "Orient", "graph:orient");
|
|
995
1094
|
spikedArea = o.spiked_area; spikeResult = o.spike_result; riskiest = o.riskiest_unknowns || [];
|
|
996
1095
|
} else {
|
|
997
1096
|
const post = await fastForward("ORIENT", "orient", "Orient", "artifacts already on disk");
|
|
998
|
-
if (post) return withWarnings(post);
|
|
1097
|
+
if (post) return await withWarnings(post);
|
|
999
1098
|
}
|
|
1000
1099
|
|
|
1001
1100
|
{
|
|
@@ -1003,11 +1102,50 @@ if (!rs.has_orient_artifacts) {
|
|
|
1003
1102
|
// downstream artifact reads the same whether or not the pitch's second half reached the run.
|
|
1004
1103
|
const g = await crossGate("L1a", "Orient", ["proceed", "ask", "abort"],
|
|
1005
1104
|
{ breadboard: rs.breadboard_source ?? "none", spiked_area: spikedArea, spike_result: spikeResult, riskiest_unknowns: riskiest });
|
|
1006
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1105
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1007
1106
|
}
|
|
1008
1107
|
|
|
1009
|
-
// ----
|
|
1108
|
+
// ---- COVERAGE (the requirements registry) — ahead of ANALYZE, whose ACs cite its ids ----------
|
|
1109
|
+
//
|
|
1110
|
+
// WHY IT RUNS AT ALL, and why here. The pitch's own requirement list is the one statement of what
|
|
1111
|
+
// the run was asked for, and until it is extracted into `shapeup/<slug>/requirements.md` there is
|
|
1112
|
+
// no stable key an acceptance criterion, a scope contract or a verdict can point back to. Measured
|
|
1113
|
+
// on a full run: the planner produced the requirement edge on the board and the judge never saw
|
|
1114
|
+
// it, because nothing on either side shared a key space. So the registry is written BEFORE the
|
|
1115
|
+
// board, not beside it — ANALYZE's acceptance criteria cite `REQ-<n>` ids, which have to exist
|
|
1116
|
+
// before they can be cited.
|
|
1117
|
+
//
|
|
1118
|
+
// IT IS NOT A PHASE, AND THAT IS THE WHOLE DESIGN OF THIS BLOCK.
|
|
1119
|
+
// · No `phase("Coverage")`: the dispatch belongs to the planning stretch the Analyze group
|
|
1120
|
+
// already covers (`setRunStatus("mapping")` spans it), so it never renders as an empty group
|
|
1121
|
+
// on a relaunch — the failure a per-phase progress box would otherwise have to pay a leg to
|
|
1122
|
+
// avoid, and there is no leg to spend here (see the next point).
|
|
1123
|
+
// · No `requirePhase()` / no `fastForward()`: both route to `probe resume --require`, whose
|
|
1124
|
+
// `--require` is an ENUM over `PHASE_ARTIFACT`'s keys. `coverage` is not one, so the call exits
|
|
1125
|
+
// 2, and this file reads any exit other than 6 as "the predicate was never asked" — a dispatch
|
|
1126
|
+
// that worked would abort the run. The skip is therefore narrated, and guarded on the bare
|
|
1127
|
+
// `has_requirements` boolean the resume state carries.
|
|
1128
|
+
// · Not in `PHASE_ARTIFACT` either: that map is also `nextPhase()`'s ordered list, so adding it
|
|
1129
|
+
// would fast-forward every pre-registry run to the registry instead of to `build`.
|
|
1010
1130
|
phase("Analyze");
|
|
1131
|
+
if (!rs.has_requirements) {
|
|
1132
|
+
log(`COVERAGE — dispatching (slug ${slug})`);
|
|
1133
|
+
await setRunStatus("mapping", "Analyze");
|
|
1134
|
+
const c = await worker({
|
|
1135
|
+
skill: "ba-pitch-analyzer", operation: "coverage", schema: PHASE_OK, phase: "Analyze", label: "coverage",
|
|
1136
|
+
payload: { requirements: rs.intake_path, feature: slug },
|
|
1137
|
+
extra:
|
|
1138
|
+
"Extract the pitch's requirement clauses into the SHARED requirements registry, one atomic " +
|
|
1139
|
+
"clause per row. A clause carrying an R-id keeps its number as REQ-<n> and records the R-id " +
|
|
1140
|
+
"verbatim in its source cell; ids are assigned once and never renumbered.",
|
|
1141
|
+
});
|
|
1142
|
+
if (c.__failed) return await withWarnings(diedAt("COVERAGE", c));
|
|
1143
|
+
await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:coverage");
|
|
1144
|
+
} else {
|
|
1145
|
+
log(`COVERAGE — a requirements registry is already on disk; not re-dispatching it`);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// ---- ANALYZE (spec tree + board) — ahead of WIRE, which reads its use cases -------------------
|
|
1011
1149
|
if (!rs.has_spec_tree) {
|
|
1012
1150
|
log(`ANALYZE — dispatching (slug ${slug})`);
|
|
1013
1151
|
// "mapping", not "analyzing": the kernel's RUN_STATUSES enum is deliberately COARSER than this
|
|
@@ -1021,13 +1159,13 @@ if (!rs.has_spec_tree) {
|
|
|
1021
1159
|
payload: { pitch: rs.intake_path, breadboard: rs.breadboard_path, spec_folder: specFolder, feature: slug, lens: rs.lens, orient_dir: rs.orient_dir },
|
|
1022
1160
|
extra: "Write the spec tree and the board from the orient artifacts — do not re-scan the code.",
|
|
1023
1161
|
});
|
|
1024
|
-
if (a.__failed) return diedAt("ANALYZE", a);
|
|
1162
|
+
if (a.__failed) return await withWarnings(diedAt("ANALYZE", a));
|
|
1025
1163
|
const post = await requirePhase("ANALYZE", "analyze", "Analyze");
|
|
1026
|
-
if (post) return withWarnings(post);
|
|
1164
|
+
if (post) return await withWarnings(post);
|
|
1027
1165
|
await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:analyze");
|
|
1028
1166
|
} else {
|
|
1029
1167
|
const post = await fastForward("ANALYZE", "analyze", "Analyze", "spec tree already on disk");
|
|
1030
|
-
if (post) return withWarnings(post);
|
|
1168
|
+
if (post) return await withWarnings(post);
|
|
1031
1169
|
}
|
|
1032
1170
|
|
|
1033
1171
|
// ---- WIRE + GATE L1a.5 ------------------------------------------------------------------------
|
|
@@ -1041,7 +1179,7 @@ if (!rs.has_wiring_map) {
|
|
|
1041
1179
|
// stays false and every relaunch re-dispatches and re-escalates identically. The orchestrator
|
|
1042
1180
|
// holds the state a gate needs; it should not hand the check to the LLM it is about to pay for.
|
|
1043
1181
|
if (!rs.has_project_profile) {
|
|
1044
|
-
return withWarnings(aborted("WIRE",
|
|
1182
|
+
return await withWarnings(aborted("WIRE",
|
|
1045
1183
|
`missing SHARED project-profile.md at ${rs.project_profile_path} — GATE L0 writes it ` +
|
|
1046
1184
|
`({schema_version:1, archetype, entry_point}; references/gates.md GATE L0 §PROFILE) before ` +
|
|
1047
1185
|
`this workflow launches. WIRE cannot resolve an entry_call_site without an entry_point to ` +
|
|
@@ -1053,18 +1191,18 @@ if (!rs.has_wiring_map) {
|
|
|
1053
1191
|
payload: { feature: slug, spec_folder: specFolder, project_profile: rs.project_profile_path, breadboard: rs.breadboard_path },
|
|
1054
1192
|
extra: "Write the wiring map: per use case, engine → seam → entry-point call site → affordance.",
|
|
1055
1193
|
});
|
|
1056
|
-
if (w.__failed) return diedAt("WIRE", w);
|
|
1194
|
+
if (w.__failed) return await withWarnings(diedAt("WIRE", w));
|
|
1057
1195
|
const post = await requirePhase("WIRE", "wire", "Wire");
|
|
1058
|
-
if (post) return withWarnings(post);
|
|
1196
|
+
if (post) return await withWarnings(post);
|
|
1059
1197
|
await advisory(`reduce graph --slug ${slug}`, "Wire", "graph:wire");
|
|
1060
1198
|
} else {
|
|
1061
1199
|
const post = await fastForward("WIRE", "wire", "Wire", "wiring map already on disk");
|
|
1062
|
-
if (post) return withWarnings(post);
|
|
1200
|
+
if (post) return await withWarnings(post);
|
|
1063
1201
|
}
|
|
1064
1202
|
|
|
1065
1203
|
{
|
|
1066
1204
|
const g = await crossGate("L1a.5", "Wire", ["proceed", "ask", "abort"], { wiring_map: "written" });
|
|
1067
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1205
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1068
1206
|
}
|
|
1069
1207
|
|
|
1070
1208
|
// ---- MAP SCOPES + GATE L1b --------------------------------------------------------------------
|
|
@@ -1099,15 +1237,15 @@ if (scopes.length === 0) {
|
|
|
1099
1237
|
"with prose appended to it is not runnable. A scope whose fixtures do not parse has nothing " +
|
|
1100
1238
|
"to verify it and is refused at the board review.",
|
|
1101
1239
|
});
|
|
1102
|
-
if (m.__failed) return diedAt("MAP SCOPES", m);
|
|
1240
|
+
if (m.__failed) return await withWarnings(diedAt("MAP SCOPES", m));
|
|
1103
1241
|
const post = await requirePhase("MAP SCOPES", "map-scopes", "MapScopes");
|
|
1104
|
-
if (post) return withWarnings(post);
|
|
1242
|
+
if (post) return await withWarnings(post);
|
|
1105
1243
|
await advisory(`reduce graph --slug ${slug}`, "MapScopes", "graph:map-scopes");
|
|
1106
1244
|
scopes = m.scopes;
|
|
1107
1245
|
} else {
|
|
1108
1246
|
const post = await fastForward("MAP SCOPES", "map-scopes", "MapScopes",
|
|
1109
1247
|
`${scopes.length} scope contract(s) already on disk`);
|
|
1110
|
-
if (post) return withWarnings(post);
|
|
1248
|
+
if (post) return await withWarnings(post);
|
|
1111
1249
|
}
|
|
1112
1250
|
|
|
1113
1251
|
// DEPENDENCY ORDER — a scope is never built beside a scope it consumes.
|
|
@@ -1172,14 +1310,14 @@ if (waves.length > 1 || excluded.added || ceiling < maxParallelScopes) {
|
|
|
1172
1310
|
// has more than one kind of red, and the detail says which.
|
|
1173
1311
|
const specLint = await cmd(`verify spec --slug ${slug}`, "MapScopes", "spec-lint");
|
|
1174
1312
|
if (!specLint.ok) {
|
|
1175
|
-
return aborted("L1b", `spec-lint reported red findings before BUILD: ${specLint.detail || `exit ${specLint.exit_code}`}`);
|
|
1313
|
+
return await withWarnings(aborted("L1b", `spec-lint reported red findings before BUILD: ${specLint.detail || `exit ${specLint.exit_code}`}`));
|
|
1176
1314
|
}
|
|
1177
1315
|
await advisory(`verify trace --slug ${slug} --quiet`, "MapScopes", "trace-lint");
|
|
1178
1316
|
await advisory(`reduce hill --slug ${slug}`, "MapScopes", "hill-derive");
|
|
1179
1317
|
|
|
1180
1318
|
{
|
|
1181
1319
|
const g = await crossGate("L1b", "MapScopes", ["proceed", "ask", "abort"], { scopes: scopes.map((s) => s.scope_id) });
|
|
1182
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1320
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1183
1321
|
}
|
|
1184
1322
|
|
|
1185
1323
|
// =============================================================================================
|
|
@@ -1246,7 +1384,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1246
1384
|
const budget = await cmd(`verify budget --slug ${slug} --strict`, "Build", `budget:r${round}`);
|
|
1247
1385
|
if (budget.exit_code === 6) {
|
|
1248
1386
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1249
|
-
return withWarnings({ status: "gate_h", breaker: "deadline", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1387
|
+
return await withWarnings({ status: "gate_h", breaker: "deadline", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1250
1388
|
}
|
|
1251
1389
|
|
|
1252
1390
|
log(`BUILD round ${round} — ${scopes.length} scope(s), up to ${maxParallelScopes} at once, attempt budget ${attemptBudget}`);
|
|
@@ -1382,7 +1520,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1382
1520
|
// INNER breaker: nothing green and something queued → GATE H. The census is scope-hammer's job.
|
|
1383
1521
|
if (roundGreen.length === 0 && roundHammer.length > 0) {
|
|
1384
1522
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1385
|
-
return withWarnings({ status: "gate_h", breaker: "inner", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1523
|
+
return await withWarnings({ status: "gate_h", breaker: "inner", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1386
1524
|
}
|
|
1387
1525
|
|
|
1388
1526
|
// ---- ROUND BUILD GATE — the feature builds and launches, measured before anyone is asked --------
|
|
@@ -1419,7 +1557,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1419
1557
|
{
|
|
1420
1558
|
const g = await crossGate("L2", "Build", ["proceed", "ask", "abort"],
|
|
1421
1559
|
{ round, green_scopes: roundGreen, hammer_proposals: roundHammer, build_gate: buildGate });
|
|
1422
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1560
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1423
1561
|
}
|
|
1424
1562
|
|
|
1425
1563
|
// ---- EVAL — exactly one feature-level pass per round (the single-judge invariant) ------------
|
|
@@ -1444,16 +1582,16 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1444
1582
|
payload: { dimensions: evalDims, run_cmd: rs.run_cmd, round },
|
|
1445
1583
|
extra: "Evaluate the running feature against every acceptance criterion and Done-when. One feature-level pass; cite every artifact the order lists under t0_artifacts, re-hashing each yourself.",
|
|
1446
1584
|
});
|
|
1447
|
-
if (e.__failed) return diedAt("L3", e);
|
|
1585
|
+
if (e.__failed) return await withWarnings(diedAt("L3", e));
|
|
1448
1586
|
// The pass/fail branch is decided from the WorkResult on disk, not from the dispatching
|
|
1449
1587
|
// agent's own summary of it (`e.overall`) — see EVAL_VERDICT's comment for why.
|
|
1450
1588
|
const ev = await query(`probe eval --slug ${slug} --round ${round}`, EVAL_VERDICT, "Eval", `verdict:r${round}`);
|
|
1451
|
-
if (!ev) return diedAt("L3", nullFail(`verdict:r${round}`));
|
|
1589
|
+
if (!ev) return await withWarnings(diedAt("L3", nullFail(`verdict:r${round}`)));
|
|
1452
1590
|
// A round with no verdict to act on is NOT a dead worker. An evaluator that refused the round
|
|
1453
1591
|
// wrote a result saying why, and `probe eval` carries it as `reason`; reported as "died after
|
|
1454
1592
|
// retries", the one sentence naming the cause stayed in a file nobody was pointed at.
|
|
1455
1593
|
if (!ev.ok || !ev.overall) {
|
|
1456
|
-
return diedAt("L3", { __failed: `verdict:r${round}: no verdict this round can act on — ${ev.reason || `status ${ev.status || "unknown"}`}` });
|
|
1594
|
+
return await withWarnings(diedAt("L3", { __failed: `verdict:r${round}: no verdict this round can act on — ${ev.reason || `status ${ev.status || "unknown"}`}` }));
|
|
1457
1595
|
}
|
|
1458
1596
|
verdict = ev.overall === "PASS" ? "pass" : "fail";
|
|
1459
1597
|
findings = e.findings || [];
|
|
@@ -1483,24 +1621,24 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1483
1621
|
await advisory(`reduce graph --slug ${slug}`, "Eval", `graph:eval-r${round}`);
|
|
1484
1622
|
await advisory(`reduce hill --slug ${slug}`, "Eval", "hill-derive");
|
|
1485
1623
|
const g3 = await crossGate("L3", "Eval", ["loop", "stop", "ask"], { round, verdict, build_gate: buildGate });
|
|
1486
|
-
if (g3.stop) return withWarnings(g3.stop);
|
|
1624
|
+
if (g3.stop) return await withWarnings(g3.stop);
|
|
1487
1625
|
|
|
1488
1626
|
if (verdict === "pass") break; // → QA → GATE H → ship
|
|
1489
1627
|
if (g3.decision === "stop" || round >= maxRounds) {
|
|
1490
|
-
return withWarnings({ status: "gate_h", breaker: "outer", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1628
|
+
return await withWarnings({ status: "gate_h", breaker: "outer", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1491
1629
|
}
|
|
1492
1630
|
round += 1;
|
|
1493
1631
|
}
|
|
1494
1632
|
|
|
1495
1633
|
if (verdict !== "pass") {
|
|
1496
|
-
return withWarnings({ status: "gate_h", breaker: "outer", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1634
|
+
return await withWarnings({ status: "gate_h", breaker: "outer", hammer_proposals: allHammer, green_scopes: allGreen });
|
|
1497
1635
|
}
|
|
1498
1636
|
|
|
1499
1637
|
// ---- QA (post-PASS, pre-ship) — a level-up, never a gate. `--no-qa` answers it "skip". --------
|
|
1500
1638
|
phase("QA");
|
|
1501
1639
|
let qaFindings = 0;
|
|
1502
1640
|
const qaG = await crossGate("QA", "QA", ["run", "skip", "ask"], { round, verdict });
|
|
1503
|
-
if (qaG.stop) return withWarnings(qaG.stop);
|
|
1641
|
+
if (qaG.stop) return await withWarnings(qaG.stop);
|
|
1504
1642
|
const qaRan = !args.noQa && qaG.decision === "run";
|
|
1505
1643
|
if (qaRan) {
|
|
1506
1644
|
const q = await worker({
|
|
@@ -1520,14 +1658,14 @@ const h = await worker({
|
|
|
1520
1658
|
payload: { feature: slug, qa_findings: qaFindings, hammer_proposals: allHammer },
|
|
1521
1659
|
extra: "Run the census, compare against the BASELINE and never the ideal, and produce the cut list.",
|
|
1522
1660
|
});
|
|
1523
|
-
if (h.__failed) return diedAt("H", h);
|
|
1661
|
+
if (h.__failed) return await withWarnings(diedAt("H", h));
|
|
1524
1662
|
if (h.verdict === "cannot-ship") {
|
|
1525
|
-
return withWarnings(aborted("H", `scope-hammer: CANNOT SHIP — ${h.cut_list.join(", ") || "a must-have failed"}`));
|
|
1663
|
+
return await withWarnings(aborted("H", `scope-hammer: CANNOT SHIP — ${h.cut_list.join(", ") || "a must-have failed"}`));
|
|
1526
1664
|
}
|
|
1527
1665
|
|
|
1528
1666
|
{
|
|
1529
1667
|
const g = await crossGate("H", "Ship", ["accept-cut-list", "ship-all", "ask"], { verdict: h.verdict, cut_list: h.cut_list });
|
|
1530
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1668
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1531
1669
|
}
|
|
1532
1670
|
|
|
1533
1671
|
const ship = await cmd(`reduce ship --slug ${slug} --verdict PASS --qa ${qaRan ? "run" : "skipped"}`, "Ship", "ship-report");
|
|
@@ -1543,7 +1681,7 @@ await setRunStatus("shipped", "Ship");
|
|
|
1543
1681
|
const ALL_DIMS = ["spec-conformance", "tdd-surface", "integration", "completeness",
|
|
1544
1682
|
"test-surface-conformance", "security", "performance"];
|
|
1545
1683
|
|
|
1546
|
-
return withWarnings({
|
|
1684
|
+
return await withWarnings({
|
|
1547
1685
|
status: "shipped",
|
|
1548
1686
|
verdict: "pass",
|
|
1549
1687
|
rounds_used: round,
|
|
@@ -188,7 +188,7 @@ shared vocabulary → write it to `shapeup/<slug>/shaping/glossary.md`.
|
|
|
188
188
|
Orchestrated, this skill is dispatched like every worker: a **WorkOrder** in (`--order <path>`,
|
|
189
189
|
operation `translate`), a **WorkResult** out. The standalone arguments below map 1:1 onto the
|
|
190
190
|
payload fields registered for this worker in the central domain registry
|
|
191
|
-
(`
|
|
191
|
+
(`kernel/schemas/domain.schema.json`, `x-payload-by-worker`):
|
|
192
192
|
|
|
193
193
|
| Payload field | Standalone form | Meaning |
|
|
194
194
|
|---|---|---|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|