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.
@@ -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, wallClockS?}, pluginRoot,
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
- const withWarnings = (ret) => (stateWarnings.length ? { ...ret, state_warnings: stateWarnings } : ret);
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
- (`skills/tech-lead/schemas/domain.schema.json`, `x-payload-by-worker`):
191
+ (`kernel/schemas/domain.schema.json`, `x-payload-by-worker`):
192
192
 
193
193
  | Payload field | Standalone form | Meaning |
194
194
  |---|---|---|