shapeup-sdlc 3.5.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 +12 -2
- package/README.md +1 -1
- package/kernel/compile.mjs +18 -6
- package/kernel/harness.mjs +3 -2
- package/kernel/init/run-args.mjs +206 -0
- package/kernel/init/run.mjs +10 -0
- 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/resume.mjs +188 -5
- package/kernel/probe/rounds.mjs +104 -0
- package/kernel/reduce/ingest.mjs +53 -12
- package/kernel/reduce/ship.mjs +15 -30
- 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 +15 -12
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +1 -1
- package/package.json +1 -1
- package/skills/coach/SKILL.md +8 -2
- package/skills/hill-chart/SKILL.md +3 -4
- package/skills/scope-hammer/SKILL.md +1 -1
- package/skills/tech-lead/SKILL.md +10 -10
- package/skills/tech-lead/references/gates.md +48 -11
- package/skills/tech-lead/references/protocol.md +4 -2
- package/skills/tech-lead/workflows/shapeup-run.js +133 -37
- 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
|
//
|
|
@@ -901,6 +901,47 @@ async function fastForward(gate, phaseKey, phaseName, what) {
|
|
|
901
901
|
`${phaseKey} artifact by hand before assuming it regressed, then relaunch.`);
|
|
902
902
|
}
|
|
903
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
|
+
|
|
904
945
|
// The ledger's `status` field is bookkeeping, not this file's resume oracle — the fast-forward reads
|
|
905
946
|
// artifacts. It survives because `reduce snapshot` and the the ship report's census hook read it to tell
|
|
906
947
|
// a run in flight from a finished one. A lost write is a degraded digest, not a corrupted build, so
|
|
@@ -917,7 +958,57 @@ async function setRunStatus(status, phaseName) {
|
|
|
917
958
|
stateWarnings.push(`status="${status}" did not take: ${why}`);
|
|
918
959
|
}
|
|
919
960
|
}
|
|
920
|
-
|
|
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
|
+
};
|
|
921
1012
|
|
|
922
1013
|
// =============================================================================================
|
|
923
1014
|
// THE RUN
|
|
@@ -950,20 +1041,25 @@ await agent(
|
|
|
950
1041
|
);
|
|
951
1042
|
const canary = await cmd(`verify dispatch --skill ${canarySkill} --within 900`, "Preflight", "canary-evidence");
|
|
952
1043
|
if (!canary.ok) {
|
|
953
|
-
return aborted("preflight",
|
|
1044
|
+
return await withWarnings(aborted("preflight",
|
|
954
1045
|
`the ${canarySkill} skill did not resolve in this session — no dispatch reached the hook layer. ` +
|
|
955
1046
|
`A run would report phases completing while the sub-agents improvised every worker's craft. ` +
|
|
956
1047
|
`Load the plugin (\`claude --plugin-dir <repo>\`, or install and enable it) and relaunch. ` +
|
|
957
|
-
`(${canary.detail || `exit ${canary.exit_code}`})`);
|
|
1048
|
+
`(${canary.detail || `exit ${canary.exit_code}`})`));
|
|
958
1049
|
}
|
|
959
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
|
+
|
|
960
1056
|
phase("Orient");
|
|
961
1057
|
|
|
962
1058
|
const rs = await query(`probe resume --slug ${slug}`, RESUME, "Orient", "resume-state");
|
|
963
1059
|
// A probe that produced nothing is not an EMPTY run — it is an unknown one. Treating it as empty
|
|
964
1060
|
// would re-dispatch every phase from the top, over a run that may be in progress.
|
|
965
1061
|
if (!rs) {
|
|
966
|
-
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"));
|
|
967
1063
|
}
|
|
968
1064
|
|
|
969
1065
|
const specFolder = rs.spec_folder || `shapeup/${slug}/spec/`;
|
|
@@ -991,14 +1087,14 @@ if (!rs.has_orient_artifacts) {
|
|
|
991
1087
|
"when the risk scan came back rank 0). Any other filename leaves the phase incomplete and " +
|
|
992
1088
|
"the run aborts, however good the contents are.",
|
|
993
1089
|
});
|
|
994
|
-
if (o.__failed) return diedAt("ORIENT", o);
|
|
1090
|
+
if (o.__failed) return await withWarnings(diedAt("ORIENT", o));
|
|
995
1091
|
const post = await requirePhase("ORIENT", "orient", "Orient");
|
|
996
|
-
if (post) return withWarnings(post);
|
|
1092
|
+
if (post) return await withWarnings(post);
|
|
997
1093
|
await advisory(`reduce graph --slug ${slug}`, "Orient", "graph:orient");
|
|
998
1094
|
spikedArea = o.spiked_area; spikeResult = o.spike_result; riskiest = o.riskiest_unknowns || [];
|
|
999
1095
|
} else {
|
|
1000
1096
|
const post = await fastForward("ORIENT", "orient", "Orient", "artifacts already on disk");
|
|
1001
|
-
if (post) return withWarnings(post);
|
|
1097
|
+
if (post) return await withWarnings(post);
|
|
1002
1098
|
}
|
|
1003
1099
|
|
|
1004
1100
|
{
|
|
@@ -1006,7 +1102,7 @@ if (!rs.has_orient_artifacts) {
|
|
|
1006
1102
|
// downstream artifact reads the same whether or not the pitch's second half reached the run.
|
|
1007
1103
|
const g = await crossGate("L1a", "Orient", ["proceed", "ask", "abort"],
|
|
1008
1104
|
{ breadboard: rs.breadboard_source ?? "none", spiked_area: spikedArea, spike_result: spikeResult, riskiest_unknowns: riskiest });
|
|
1009
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1105
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1010
1106
|
}
|
|
1011
1107
|
|
|
1012
1108
|
// ---- COVERAGE (the requirements registry) — ahead of ANALYZE, whose ACs cite its ids ----------
|
|
@@ -1043,7 +1139,7 @@ if (!rs.has_requirements) {
|
|
|
1043
1139
|
"clause per row. A clause carrying an R-id keeps its number as REQ-<n> and records the R-id " +
|
|
1044
1140
|
"verbatim in its source cell; ids are assigned once and never renumbered.",
|
|
1045
1141
|
});
|
|
1046
|
-
if (c.__failed) return diedAt("COVERAGE", c);
|
|
1142
|
+
if (c.__failed) return await withWarnings(diedAt("COVERAGE", c));
|
|
1047
1143
|
await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:coverage");
|
|
1048
1144
|
} else {
|
|
1049
1145
|
log(`COVERAGE — a requirements registry is already on disk; not re-dispatching it`);
|
|
@@ -1063,13 +1159,13 @@ if (!rs.has_spec_tree) {
|
|
|
1063
1159
|
payload: { pitch: rs.intake_path, breadboard: rs.breadboard_path, spec_folder: specFolder, feature: slug, lens: rs.lens, orient_dir: rs.orient_dir },
|
|
1064
1160
|
extra: "Write the spec tree and the board from the orient artifacts — do not re-scan the code.",
|
|
1065
1161
|
});
|
|
1066
|
-
if (a.__failed) return diedAt("ANALYZE", a);
|
|
1162
|
+
if (a.__failed) return await withWarnings(diedAt("ANALYZE", a));
|
|
1067
1163
|
const post = await requirePhase("ANALYZE", "analyze", "Analyze");
|
|
1068
|
-
if (post) return withWarnings(post);
|
|
1164
|
+
if (post) return await withWarnings(post);
|
|
1069
1165
|
await advisory(`reduce graph --slug ${slug}`, "Analyze", "graph:analyze");
|
|
1070
1166
|
} else {
|
|
1071
1167
|
const post = await fastForward("ANALYZE", "analyze", "Analyze", "spec tree already on disk");
|
|
1072
|
-
if (post) return withWarnings(post);
|
|
1168
|
+
if (post) return await withWarnings(post);
|
|
1073
1169
|
}
|
|
1074
1170
|
|
|
1075
1171
|
// ---- WIRE + GATE L1a.5 ------------------------------------------------------------------------
|
|
@@ -1083,7 +1179,7 @@ if (!rs.has_wiring_map) {
|
|
|
1083
1179
|
// stays false and every relaunch re-dispatches and re-escalates identically. The orchestrator
|
|
1084
1180
|
// holds the state a gate needs; it should not hand the check to the LLM it is about to pay for.
|
|
1085
1181
|
if (!rs.has_project_profile) {
|
|
1086
|
-
return withWarnings(aborted("WIRE",
|
|
1182
|
+
return await withWarnings(aborted("WIRE",
|
|
1087
1183
|
`missing SHARED project-profile.md at ${rs.project_profile_path} — GATE L0 writes it ` +
|
|
1088
1184
|
`({schema_version:1, archetype, entry_point}; references/gates.md GATE L0 §PROFILE) before ` +
|
|
1089
1185
|
`this workflow launches. WIRE cannot resolve an entry_call_site without an entry_point to ` +
|
|
@@ -1095,18 +1191,18 @@ if (!rs.has_wiring_map) {
|
|
|
1095
1191
|
payload: { feature: slug, spec_folder: specFolder, project_profile: rs.project_profile_path, breadboard: rs.breadboard_path },
|
|
1096
1192
|
extra: "Write the wiring map: per use case, engine → seam → entry-point call site → affordance.",
|
|
1097
1193
|
});
|
|
1098
|
-
if (w.__failed) return diedAt("WIRE", w);
|
|
1194
|
+
if (w.__failed) return await withWarnings(diedAt("WIRE", w));
|
|
1099
1195
|
const post = await requirePhase("WIRE", "wire", "Wire");
|
|
1100
|
-
if (post) return withWarnings(post);
|
|
1196
|
+
if (post) return await withWarnings(post);
|
|
1101
1197
|
await advisory(`reduce graph --slug ${slug}`, "Wire", "graph:wire");
|
|
1102
1198
|
} else {
|
|
1103
1199
|
const post = await fastForward("WIRE", "wire", "Wire", "wiring map already on disk");
|
|
1104
|
-
if (post) return withWarnings(post);
|
|
1200
|
+
if (post) return await withWarnings(post);
|
|
1105
1201
|
}
|
|
1106
1202
|
|
|
1107
1203
|
{
|
|
1108
1204
|
const g = await crossGate("L1a.5", "Wire", ["proceed", "ask", "abort"], { wiring_map: "written" });
|
|
1109
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1205
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1110
1206
|
}
|
|
1111
1207
|
|
|
1112
1208
|
// ---- MAP SCOPES + GATE L1b --------------------------------------------------------------------
|
|
@@ -1141,15 +1237,15 @@ if (scopes.length === 0) {
|
|
|
1141
1237
|
"with prose appended to it is not runnable. A scope whose fixtures do not parse has nothing " +
|
|
1142
1238
|
"to verify it and is refused at the board review.",
|
|
1143
1239
|
});
|
|
1144
|
-
if (m.__failed) return diedAt("MAP SCOPES", m);
|
|
1240
|
+
if (m.__failed) return await withWarnings(diedAt("MAP SCOPES", m));
|
|
1145
1241
|
const post = await requirePhase("MAP SCOPES", "map-scopes", "MapScopes");
|
|
1146
|
-
if (post) return withWarnings(post);
|
|
1242
|
+
if (post) return await withWarnings(post);
|
|
1147
1243
|
await advisory(`reduce graph --slug ${slug}`, "MapScopes", "graph:map-scopes");
|
|
1148
1244
|
scopes = m.scopes;
|
|
1149
1245
|
} else {
|
|
1150
1246
|
const post = await fastForward("MAP SCOPES", "map-scopes", "MapScopes",
|
|
1151
1247
|
`${scopes.length} scope contract(s) already on disk`);
|
|
1152
|
-
if (post) return withWarnings(post);
|
|
1248
|
+
if (post) return await withWarnings(post);
|
|
1153
1249
|
}
|
|
1154
1250
|
|
|
1155
1251
|
// DEPENDENCY ORDER — a scope is never built beside a scope it consumes.
|
|
@@ -1214,14 +1310,14 @@ if (waves.length > 1 || excluded.added || ceiling < maxParallelScopes) {
|
|
|
1214
1310
|
// has more than one kind of red, and the detail says which.
|
|
1215
1311
|
const specLint = await cmd(`verify spec --slug ${slug}`, "MapScopes", "spec-lint");
|
|
1216
1312
|
if (!specLint.ok) {
|
|
1217
|
-
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}`}`));
|
|
1218
1314
|
}
|
|
1219
1315
|
await advisory(`verify trace --slug ${slug} --quiet`, "MapScopes", "trace-lint");
|
|
1220
1316
|
await advisory(`reduce hill --slug ${slug}`, "MapScopes", "hill-derive");
|
|
1221
1317
|
|
|
1222
1318
|
{
|
|
1223
1319
|
const g = await crossGate("L1b", "MapScopes", ["proceed", "ask", "abort"], { scopes: scopes.map((s) => s.scope_id) });
|
|
1224
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1320
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1225
1321
|
}
|
|
1226
1322
|
|
|
1227
1323
|
// =============================================================================================
|
|
@@ -1288,7 +1384,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1288
1384
|
const budget = await cmd(`verify budget --slug ${slug} --strict`, "Build", `budget:r${round}`);
|
|
1289
1385
|
if (budget.exit_code === 6) {
|
|
1290
1386
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1291
|
-
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 });
|
|
1292
1388
|
}
|
|
1293
1389
|
|
|
1294
1390
|
log(`BUILD round ${round} — ${scopes.length} scope(s), up to ${maxParallelScopes} at once, attempt budget ${attemptBudget}`);
|
|
@@ -1424,7 +1520,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1424
1520
|
// INNER breaker: nothing green and something queued → GATE H. The census is scope-hammer's job.
|
|
1425
1521
|
if (roundGreen.length === 0 && roundHammer.length > 0) {
|
|
1426
1522
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1427
|
-
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 });
|
|
1428
1524
|
}
|
|
1429
1525
|
|
|
1430
1526
|
// ---- ROUND BUILD GATE — the feature builds and launches, measured before anyone is asked --------
|
|
@@ -1461,7 +1557,7 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1461
1557
|
{
|
|
1462
1558
|
const g = await crossGate("L2", "Build", ["proceed", "ask", "abort"],
|
|
1463
1559
|
{ round, green_scopes: roundGreen, hammer_proposals: roundHammer, build_gate: buildGate });
|
|
1464
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1560
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1465
1561
|
}
|
|
1466
1562
|
|
|
1467
1563
|
// ---- EVAL — exactly one feature-level pass per round (the single-judge invariant) ------------
|
|
@@ -1486,16 +1582,16 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1486
1582
|
payload: { dimensions: evalDims, run_cmd: rs.run_cmd, round },
|
|
1487
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.",
|
|
1488
1584
|
});
|
|
1489
|
-
if (e.__failed) return diedAt("L3", e);
|
|
1585
|
+
if (e.__failed) return await withWarnings(diedAt("L3", e));
|
|
1490
1586
|
// The pass/fail branch is decided from the WorkResult on disk, not from the dispatching
|
|
1491
1587
|
// agent's own summary of it (`e.overall`) — see EVAL_VERDICT's comment for why.
|
|
1492
1588
|
const ev = await query(`probe eval --slug ${slug} --round ${round}`, EVAL_VERDICT, "Eval", `verdict:r${round}`);
|
|
1493
|
-
if (!ev) return diedAt("L3", nullFail(`verdict:r${round}`));
|
|
1589
|
+
if (!ev) return await withWarnings(diedAt("L3", nullFail(`verdict:r${round}`)));
|
|
1494
1590
|
// A round with no verdict to act on is NOT a dead worker. An evaluator that refused the round
|
|
1495
1591
|
// wrote a result saying why, and `probe eval` carries it as `reason`; reported as "died after
|
|
1496
1592
|
// retries", the one sentence naming the cause stayed in a file nobody was pointed at.
|
|
1497
1593
|
if (!ev.ok || !ev.overall) {
|
|
1498
|
-
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"}`}` }));
|
|
1499
1595
|
}
|
|
1500
1596
|
verdict = ev.overall === "PASS" ? "pass" : "fail";
|
|
1501
1597
|
findings = e.findings || [];
|
|
@@ -1525,24 +1621,24 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1525
1621
|
await advisory(`reduce graph --slug ${slug}`, "Eval", `graph:eval-r${round}`);
|
|
1526
1622
|
await advisory(`reduce hill --slug ${slug}`, "Eval", "hill-derive");
|
|
1527
1623
|
const g3 = await crossGate("L3", "Eval", ["loop", "stop", "ask"], { round, verdict, build_gate: buildGate });
|
|
1528
|
-
if (g3.stop) return withWarnings(g3.stop);
|
|
1624
|
+
if (g3.stop) return await withWarnings(g3.stop);
|
|
1529
1625
|
|
|
1530
1626
|
if (verdict === "pass") break; // → QA → GATE H → ship
|
|
1531
1627
|
if (g3.decision === "stop" || round >= maxRounds) {
|
|
1532
|
-
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 });
|
|
1533
1629
|
}
|
|
1534
1630
|
round += 1;
|
|
1535
1631
|
}
|
|
1536
1632
|
|
|
1537
1633
|
if (verdict !== "pass") {
|
|
1538
|
-
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 });
|
|
1539
1635
|
}
|
|
1540
1636
|
|
|
1541
1637
|
// ---- QA (post-PASS, pre-ship) — a level-up, never a gate. `--no-qa` answers it "skip". --------
|
|
1542
1638
|
phase("QA");
|
|
1543
1639
|
let qaFindings = 0;
|
|
1544
1640
|
const qaG = await crossGate("QA", "QA", ["run", "skip", "ask"], { round, verdict });
|
|
1545
|
-
if (qaG.stop) return withWarnings(qaG.stop);
|
|
1641
|
+
if (qaG.stop) return await withWarnings(qaG.stop);
|
|
1546
1642
|
const qaRan = !args.noQa && qaG.decision === "run";
|
|
1547
1643
|
if (qaRan) {
|
|
1548
1644
|
const q = await worker({
|
|
@@ -1562,14 +1658,14 @@ const h = await worker({
|
|
|
1562
1658
|
payload: { feature: slug, qa_findings: qaFindings, hammer_proposals: allHammer },
|
|
1563
1659
|
extra: "Run the census, compare against the BASELINE and never the ideal, and produce the cut list.",
|
|
1564
1660
|
});
|
|
1565
|
-
if (h.__failed) return diedAt("H", h);
|
|
1661
|
+
if (h.__failed) return await withWarnings(diedAt("H", h));
|
|
1566
1662
|
if (h.verdict === "cannot-ship") {
|
|
1567
|
-
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"}`));
|
|
1568
1664
|
}
|
|
1569
1665
|
|
|
1570
1666
|
{
|
|
1571
1667
|
const g = await crossGate("H", "Ship", ["accept-cut-list", "ship-all", "ask"], { verdict: h.verdict, cut_list: h.cut_list });
|
|
1572
|
-
if (g.stop) return withWarnings(g.stop);
|
|
1668
|
+
if (g.stop) return await withWarnings(g.stop);
|
|
1573
1669
|
}
|
|
1574
1670
|
|
|
1575
1671
|
const ship = await cmd(`reduce ship --slug ${slug} --verdict PASS --qa ${qaRan ? "run" : "skipped"}`, "Ship", "ship-report");
|
|
@@ -1585,7 +1681,7 @@ await setRunStatus("shipped", "Ship");
|
|
|
1585
1681
|
const ALL_DIMS = ["spec-conformance", "tdd-surface", "integration", "completeness",
|
|
1586
1682
|
"test-surface-conformance", "security", "performance"];
|
|
1587
1683
|
|
|
1588
|
-
return withWarnings({
|
|
1684
|
+
return await withWarnings({
|
|
1589
1685
|
status: "shipped",
|
|
1590
1686
|
verdict: "pass",
|
|
1591
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
|