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.
Files changed (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/AGENTS.md +16 -4
  3. package/README.md +7 -3
  4. package/SECURITY.md +1 -1
  5. package/hooks/sandbox-guard.mjs +69 -6
  6. package/kernel/compile.mjs +33 -12
  7. package/kernel/harness.mjs +10 -4
  8. package/kernel/init/run-args.mjs +206 -0
  9. package/kernel/init/run.mjs +10 -0
  10. package/kernel/lib/contract.mjs +68 -1
  11. package/kernel/lib/paths.mjs +10 -0
  12. package/kernel/probe/concurrency.mjs +31 -6
  13. package/kernel/probe/digest.mjs +15 -1
  14. package/kernel/probe/owner.mjs +4 -1
  15. package/kernel/probe/requirements.mjs +296 -0
  16. package/kernel/probe/resume.mjs +195 -6
  17. package/kernel/probe/rounds.mjs +104 -0
  18. package/kernel/reduce/graph.mjs +5 -2
  19. package/kernel/reduce/ingest.mjs +69 -15
  20. package/kernel/reduce/ship.mjs +52 -31
  21. package/kernel/reduce/snapshot.mjs +23 -2
  22. package/kernel/report/export.mjs +54 -2
  23. package/kernel/report/facts.mjs +24 -2
  24. package/{skills/tech-lead → kernel}/schemas/domain.schema.json +20 -12
  25. package/kernel/verify/envelope.mjs +2 -2
  26. package/kernel/verify/skills.mjs +1 -1
  27. package/kernel/verify/spec.mjs +130 -4
  28. package/kernel/verify/trace.mjs +16 -7
  29. package/package.json +1 -1
  30. package/skills/ba-pitch-analyzer/SKILL.md +16 -1
  31. package/skills/coach/SKILL.md +8 -2
  32. package/skills/hill-chart/SKILL.md +3 -4
  33. package/skills/scope-architect/SKILL.md +16 -1
  34. package/skills/scope-hammer/SKILL.md +11 -2
  35. package/skills/spec-evaluator/SKILL.md +12 -1
  36. package/skills/tech-lead/SKILL.md +10 -10
  37. package/skills/tech-lead/references/gates.md +70 -12
  38. package/skills/tech-lead/references/protocol.md +4 -2
  39. package/skills/tech-lead/workflows/shapeup-run.js +176 -38
  40. package/skills/translator/SKILL.md +1 -1
  41. /package/{skills/tech-lead → kernel}/schemas/gate-answers.schema.json +0 -0
  42. /package/{skills/tech-lead → kernel}/schemas/work-order.schema.json +0 -0
  43. /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, 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
  //
@@ -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
- 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
+ };
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
- // ---- ANALYZE (spec tree + board) — ahead of WIRE, which reads its use cases -------------------
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
- (`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
  |---|---|---|