shapeup-sdlc 3.18.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.claude/settings.local.example.json +2 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +115 -56
  4. package/README.md +101 -143
  5. package/SECURITY.md +16 -25
  6. package/agents/orient.md +14 -0
  7. package/agents/qa-edge-hunter.md +13 -0
  8. package/agents/spec-evaluator.md +14 -0
  9. package/agents/task-executor.md +14 -0
  10. package/bin/init.mjs +13 -16
  11. package/bin/lib/grant.mjs +8 -19
  12. package/commands/build.md +9 -14
  13. package/commands/eval.md +3 -3
  14. package/commands/hammer.md +9 -5
  15. package/commands/retro.md +12 -11
  16. package/commands/scopes.md +1 -12
  17. package/commands/ship.md +45 -88
  18. package/commands/wire.md +1 -8
  19. package/hooks/dispatch-receipt.mjs +94 -8
  20. package/hooks/hooks.json +0 -41
  21. package/hooks/lib/decision.mjs +6 -9
  22. package/hooks/lib/tier.mjs +50 -0
  23. package/hooks/sandbox-guard.mjs +140 -64
  24. package/kernel/gate.mjs +203 -22
  25. package/kernel/harness.mjs +20 -67
  26. package/kernel/init/run.mjs +175 -523
  27. package/kernel/lib/contract.mjs +9 -93
  28. package/kernel/lib/floor.mjs +64 -0
  29. package/kernel/lib/paths.mjs +39 -112
  30. package/kernel/order.mjs +157 -0
  31. package/kernel/probe/digest.mjs +2 -38
  32. package/kernel/schemas/domain.schema.json +146 -1125
  33. package/kernel/schemas/work-order.schema.json +8 -4
  34. package/kernel/schemas/work-result.schema.json +1 -1
  35. package/kernel/verify/envelope.mjs +2 -2
  36. package/kernel/verify/skills.mjs +27 -8
  37. package/kernel/verify/t0.mjs +2 -2
  38. package/package.json +4 -2
  39. package/skills/building/SKILL.md +144 -0
  40. package/skills/building/resources/gates.md +76 -0
  41. package/skills/building/resources/guidance.md +23 -0
  42. package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
  43. package/skills/building/resources/intake.md +157 -0
  44. package/skills/building/resources/orders.md +69 -0
  45. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
  46. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
  47. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
  48. package/skills/building/resources/plan-scopes.md +95 -0
  49. package/skills/building/resources/plan-spec.md +102 -0
  50. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
  51. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
  52. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
  53. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
  54. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
  55. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
  56. package/skills/building/resources/plan-wire.md +82 -0
  57. package/skills/building/resources/plan.md +98 -0
  58. package/skills/building/resources/retro-research.md +60 -0
  59. package/skills/building/resources/retro-scan.md +49 -0
  60. package/skills/building/resources/retro.md +225 -0
  61. package/skills/building/resources/rounds.md +78 -0
  62. package/skills/building/resources/stop.md +127 -0
  63. package/skills/orient/SKILL.md +37 -37
  64. package/skills/qa-edge-hunter/SKILL.md +14 -14
  65. package/skills/shapeup/SKILL.md +5 -5
  66. package/skills/shapeup/resources/breadboarding.md +1 -1
  67. package/skills/shapeup/resources/context-compaction.md +5 -5
  68. package/skills/shapeup/resources/shaping.md +1 -1
  69. package/skills/spec-evaluator/SKILL.md +17 -21
  70. package/skills/spec-evaluator/references/dimension-contract.md +1 -1
  71. package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
  72. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
  73. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
  74. package/skills/task-executor/SKILL.md +11 -15
  75. package/commands/hill.md +0 -10
  76. package/hooks/gate-intake.mjs +0 -109
  77. package/hooks/gate-zerowork.mjs +0 -349
  78. package/hooks/tier-guard.mjs +0 -162
  79. package/kernel/compile.mjs +0 -1419
  80. package/kernel/init/fit.mjs +0 -199
  81. package/kernel/init/run-args.mjs +0 -206
  82. package/kernel/lib/breadboard.mjs +0 -165
  83. package/kernel/probe/attempts.mjs +0 -161
  84. package/kernel/probe/concurrency.mjs +0 -535
  85. package/kernel/probe/eval.mjs +0 -405
  86. package/kernel/probe/hunt.mjs +0 -104
  87. package/kernel/probe/leg.mjs +0 -229
  88. package/kernel/probe/owner.mjs +0 -142
  89. package/kernel/probe/requirements.mjs +0 -328
  90. package/kernel/probe/resume.mjs +0 -1110
  91. package/kernel/probe/rounds.mjs +0 -115
  92. package/kernel/probe/staged.mjs +0 -83
  93. package/kernel/probe/stats.mjs +0 -477
  94. package/kernel/probe/t0.mjs +0 -115
  95. package/kernel/reduce/board.mjs +0 -350
  96. package/kernel/reduce/graph.mjs +0 -482
  97. package/kernel/reduce/hill.mjs +0 -322
  98. package/kernel/reduce/ingest.mjs +0 -767
  99. package/kernel/reduce/leftovers.mjs +0 -159
  100. package/kernel/reduce/ship.mjs +0 -529
  101. package/kernel/reduce/snapshot.mjs +0 -291
  102. package/kernel/reduce/verdict.mjs +0 -172
  103. package/kernel/report/export.mjs +0 -384
  104. package/kernel/report/facts.mjs +0 -234
  105. package/kernel/verify/budget.mjs +0 -153
  106. package/kernel/verify/build.mjs +0 -428
  107. package/kernel/verify/dispatch.mjs +0 -114
  108. package/kernel/verify/spec.mjs +0 -975
  109. package/kernel/verify/trace.mjs +0 -604
  110. package/skills/ba-pitch-analyzer/SKILL.md +0 -204
  111. package/skills/coach/SKILL.md +0 -397
  112. package/skills/hill-chart/SKILL.md +0 -154
  113. package/skills/hill-chart/assets/dashboard.template.html +0 -1432
  114. package/skills/scope-architect/SKILL.md +0 -174
  115. package/skills/scope-hammer/SKILL.md +0 -231
  116. package/skills/solution-architect/SKILL.md +0 -158
  117. package/skills/tech-lead/SKILL.md +0 -151
  118. package/skills/tech-lead/references/gates.md +0 -605
  119. package/skills/tech-lead/references/protocol.md +0 -889
  120. package/skills/tech-lead/references/tiny-lane.md +0 -53
  121. package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
  122. package/skills/translator/SKILL.md +0 -258
  123. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
  124. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
  125. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
  126. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
  127. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
  128. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
  129. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
  130. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
  131. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
  132. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
  133. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
  134. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
  135. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
  136. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
  137. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
  138. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
  139. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
  140. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
  141. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
@@ -1,6 +1,6 @@
1
1
  // contract — read and write the committed contracts as markdown.
2
2
  //
3
- // WHY (ADR-0001). Scope contracts, the wiring map and the project profile are *low-level design*:
3
+ // WHY. Scope contracts, the wiring map and the project profile are *low-level design*:
4
4
  // which files a slice may touch, which seam each use case attaches to, where the app starts. A
5
5
  // teammate should be able to read them in a pull request. As `.json` they were machine artifacts
6
6
  // sitting in the tier meant for prose, and nobody read them.
@@ -12,7 +12,7 @@
12
12
  //
13
13
  // • scalars and string arrays → frontmatter, in the `key: value` / `key: [a, b]` dialect the
14
14
  // repo already parses in `compile-order.frontmatter`;
15
- // • arrays of objects → markdown TABLES, which `trace-lint.parseRequirements` and
15
+ // • arrays of objects → markdown TABLES, which `requirements.parseRequirements` and
16
16
  // `compile-order.ledgerDecisions` already demonstrate reading;
17
17
  // • everything else in the file is prose, and is preserved on round-trip.
18
18
  //
@@ -40,7 +40,7 @@ import { join } from "node:path";
40
40
  // `signatures` names the columns that identify a table as THAT field's, and it exists because of
41
41
  // The heading match is exact, so a table written under `# Wiring map — <slug>` instead of
42
42
  // `## Wiring` parsed as ABSENT, and every reader downstream treats absent as "none declared". The
43
- // observed consequence was `trace-lint` reporting `🟢 green · 0/0 engines reach src/cli/main.js`
43
+ // observed consequence was a wiring check reporting green with 0/0 engines reached
44
44
  // for a committed map holding six correct rows — the gate whose whole purpose is that no engine
45
45
  // ships orphaned, failing OPEN.
46
46
  //
@@ -60,7 +60,7 @@ export const SCOPE_CONTRACT = {
60
60
  * `WiringMap` — per use case: engine → seam → entry-point call site → affordance.
61
61
  *
62
62
  * `perSection` is a MIGRATION READER, and it exists because the canonical shape was never written.
63
- * `solution-architect` was told to produce a `{schema_version, feature, entry_point, entries[]}`
63
+ * The wiring author was told to produce a `{schema_version, feature, entry_point, entries[]}`
64
64
  * OBJECT and never told the markdown layout, so every run authored its own: a `## Entries` section
65
65
  * with one `### UC-xx — title` subsection per use case, each holding a vertical `| Field | Value |`
66
66
  * table. Under this spec that parses as no table at all — and the vertical layout carries no
@@ -68,8 +68,8 @@ export const SCOPE_CONTRACT = {
68
68
  * reports as perfectly readable while yielding nothing.
69
69
  *
70
70
  * Measured across every completed run in the corpus: 9 of 9 committed wiring maps parsed to ZERO
71
- * entries with `unreadableReason() === null`, and `trace-lint` certified `🟢 green · 0/0 engines
72
- * reach <entry point>` against a deliverable whose engines were on disk — the exact failure quoted
71
+ * entries with `unreadableReason() === null`, and the wiring check certified green with 0/0 engines
72
+ * reached against a deliverable whose engines were on disk — the exact failure quoted
73
73
  * in the banner above, recurred in a shape its own fix could not detect.
74
74
  *
75
75
  * The horizontal `## Wiring` table stays canonical: it is what `renderContract` emits, so a map
@@ -161,7 +161,7 @@ export function coerce(raw) {
161
161
  // These contracts are MARKDOWN on disk, and a code span is the idiomatic way to write a path or
162
162
  // an identifier in one; this repo's own prose backticks every path it names. Read literally,
163
163
  // `` `src/capture/add.js` `` is a filename with two backticks in it, which is on no disk
164
- // anywhere — so `trace-lint` reported "engine file not on disk" and then "reachability is not
164
+ // anywhere — so the wiring check reported "engine file not on disk" and then "reachability is not
165
165
  // demonstrated" for a wiring map whose engines all resolve AND all reach the entry point. The
166
166
  // gate that exists so no engine ships orphaned failing CLOSED, on a correct map, is the same
167
167
  // silent-format family as the parser defects above, arriving from the other direction.
@@ -222,7 +222,7 @@ export function uncoerce(v) {
222
222
  * Split a `[a, b]` list body on commas that are NOT inside quotes.
223
223
  *
224
224
  * The old implementation was `body.split(",")`, and it shredded any member carrying a
225
- * comma — even a correctly quoted one. Measured: a `scope-architect` run probed the running CLI,
225
+ * comma — even a correctly quoted one. Measured: a scope-contract run probed the running CLI,
226
226
  * confirmed `tag` was unimplemented, and wrote the honest entry its own SKILL.md asks for —
227
227
  * ["TBD — `tag` is not in dispatch.js's TABLE (exits 1, confirmed against the running CLI). A
228
228
  * fixture asserting the spec'd behaviour (attach/remove a tag, idempotent double-tag) can only
@@ -324,7 +324,7 @@ export function splitFrontmatter(md) {
324
324
  // An indented run beneath a key is a YAML BLOCK SEQUENCE, and it used to be skipped
325
325
  // entirely — so `e2e_verification_fixtures:` followed by two `- "node …"` lines parsed to
326
326
  // null, the members vanished, and no reader could tell "declared nothing" from "declared
327
- // something I discarded". Measured: a scope-architect run wrote three scopes of researched
327
+ // something I discarded". Measured: a scope-contract run wrote three scopes of researched
328
328
  // fixtures in this form and every one evaporated. It is ACCEPTED now, because it is the form a
329
329
  // model reaches for by default and the one that carries prose members without the quoting
330
330
  // problem — and anything indented that is NOT a block sequence is reported rather than dropped.
@@ -683,39 +683,6 @@ export function readContract(path, spec = SCOPE_CONTRACT) {
683
683
  return null;
684
684
  }
685
685
 
686
- /**
687
- * Read every contract in a directory, markdown and legacy JSON alike.
688
- *
689
- * A `.md` and a `.json` for the same id is a half-finished migration, not two scopes — the
690
- * markdown wins and the JSON is ignored, so a stale leftover cannot resurrect an old substrate.
691
- *
692
- * @param {string} dir - Directory of contracts (typically `scopes/`).
693
- * @param {{tables:Object<string,string>}} [spec=SCOPE_CONTRACT] - Field → heading map.
694
- * @returns {Array<{contract:Object, path:string, format:string, id:string}>} One entry per
695
- * contract, sorted by id; [] when the directory is absent. Unparseable files are skipped.
696
- */
697
- export function readAllContracts(dir, spec = SCOPE_CONTRACT) {
698
- if (!existsSync(dir)) return [];
699
- let names;
700
- try { names = readdirSync(dir); } catch { return []; }
701
- const ids = new Map();
702
- for (const f of names) {
703
- const m = f.match(/^(.+)\.(md|json)$/);
704
- if (!m) continue;
705
- // markdown wins over a legacy sibling of the same id
706
- if (m[2] === "json" && ids.has(m[1])) continue;
707
- if (m[2] === "md" || !ids.has(m[1])) ids.set(m[1], f);
708
- }
709
- const out = [];
710
- for (const [id, file] of [...ids].sort(([a], [b]) => a.localeCompare(b))) {
711
- try {
712
- const found = readContract(join(dir, file), spec);
713
- if (found) out.push({ ...found, id });
714
- } catch { /* an unparseable contract is reported by spec-lint, not here */ }
715
- }
716
- return out;
717
- }
718
-
719
686
  // ---------------------------------------------------------------------------
720
687
  // The scope ↔ spec join
721
688
  // ---------------------------------------------------------------------------
@@ -734,35 +701,6 @@ export function ucId(ref) {
734
701
  return String(ref ?? "").trim().replace(/^\[\[|\]\]$/g, "").replace(/^usecases\//, "").replace(/\.md$/, "").trim();
735
702
  }
736
703
 
737
- /**
738
- * Normalise one requirement reference to the registry's key space (`REQ-<n>`).
739
- *
740
- * TWO KEY SPACES FOR ONE THING, and the measurement is what decided this. A pitch numbers its
741
- * requirements `R1…R21`; the registry, every AC's `covers:` clause and every `traces_to[]` key off
742
- * `REQ-<n>`. Measured across two runs of one pitch, the scope contracts cited `R<n>` — 20 of 21
743
- * requirements had a scope claiming them, and every one of those links resolved to nothing,
744
- * reported 23 times a run as a shape warning nobody could act on. So the edge WAS produced; it was
745
- * severed by spelling alone.
746
- *
747
- * Normalising here rather than teaching the planner to emit `REQ-<n>` is deliberate: `covers[]` is
748
- * an OPTIONAL contract field, so a fix that depends on a planner choosing to comply converts
749
- * whatever links the next run happens to write, while this converts the links on contracts already
750
- * committed, with no worker behaviour change. The craft still asks for `REQ-<n>` going forward.
751
- *
752
- * THIS IS A MAPPING PERFORMED BEFORE THE PATTERN, NOT A LOOSENING OF IT. `^REQ-[0-9]+$` is
753
- * unchanged everywhere it appears; a reference this function does not recognise is returned
754
- * verbatim, so it still fails that pattern and is still reported.
755
- *
756
- * @param {string} ref - A requirement reference (`REQ-12`, `R12`, `R-12`, `[[REQ-12]]`, any case).
757
- * @returns {string} The canonical `REQ-<n>` id, or the trimmed input unchanged when it is not a
758
- * numbered requirement reference at all.
759
- */
760
- export function reqId(ref) {
761
- const s = String(ref ?? "").trim().replace(/^\[\[|\]\]$/g, "").trim();
762
- const m = s.match(/^(?:REQ|R)-?([0-9]+)$/i);
763
- return m ? `REQ-${m[1]}` : s;
764
- }
765
-
766
704
  /**
767
705
  * The tasks on a LOCAL board that belong to a scope, joined through the COMMITTED spec.
768
706
  *
@@ -809,28 +747,6 @@ function byUseCase(tasks, contract) {
809
747
  return (tasks || []).filter((t) => (t.use_case_refs || []).some((r) => mine.has(ucId(r))));
810
748
  }
811
749
 
812
- /**
813
- * Tasks that more than one scope would build — the ambiguity {@link tasksForScope} cannot resolve.
814
- *
815
- * Dispatch has to be a partition: a task built by two scopes is built twice, and the second builder
816
- * is denied by the sandbox for writing outside its own substrate, so the cost lands as burnt
817
- * attempts and escalations rather than as corruption. Measured on the corpus's four-scope,
818
- * one-use-case cut: every task claimed by every scope.
819
- *
820
- * @param {Array<{id:string, scope_id?:string, use_case_refs?:string[]}>} tasks - The parsed board.
821
- * @param {Array<{scope_id:string, use_cases?:string[]}>} contracts - Every scope in the run.
822
- * @returns {Array<{task_id:string, scopes:string[]}>} One entry per contested task; [] when the
823
- * assignment is already a partition.
824
- */
825
- export function scopePartitionConflicts(tasks, contracts) {
826
- const out = [];
827
- for (const t of tasks || []) {
828
- const owners = (contracts || []).filter((c) => tasksForScope([t], c).length).map((c) => c.scope_id);
829
- if (owners.length > 1) out.push({ task_id: t.id, scopes: owners });
830
- }
831
- return out;
832
- }
833
-
834
750
  /**
835
751
  * Extract the prose a regeneration must not destroy — everything in the body except the sections
836
752
  * this spec owns.
@@ -0,0 +1,64 @@
1
+ // MODEL FLOOR AND LAUNCH-RECORD CHECK — what a run may be launched with, decided in code.
2
+ //
3
+ // The model floor is a denylist of cheap tiers refused for the roles that write code and grade it
4
+ // (`exec`, `eval`). It is a denylist on purpose, and fails open: an id this list has never heard of
5
+ // is allowed, because refusing an unknown future model would stop a run for a reason nobody can
6
+ // act on. `qa` is not checked — a hunt discovers and never decides.
7
+ //
8
+ // WHY THE KERNEL OWNS IT. A rule that only a launch script applied was a rule a different launch
9
+ // path never met. Here it is one function, read by whoever opens the run and by the loop that
10
+ // drives it, so the two cannot disagree about what is below the floor.
11
+
12
+ const BELOW_FLOOR = [/ha[i1]ku/i, /\binstant\b/i, /\bmini\b/i, /\bnano\b/i, /\blite\b/i, /\bsmall\b/i];
13
+
14
+ /**
15
+ * Whether a model id names a tier below the floor.
16
+ * @param {*} m - A model id or alias.
17
+ * @returns {boolean} True for an empty value or a denylisted tier.
18
+ */
19
+ export function belowFloor(m) {
20
+ const s = String(m ?? "").trim();
21
+ return s === "" || BELOW_FLOOR.some((re) => re.test(s));
22
+ }
23
+
24
+ /** The auto levels a launch may name. */
25
+ export const AUTO_LEVELS = ["interactive", "auto", "unattended"];
26
+
27
+ /**
28
+ * The roles whose model sits below the floor, as sentences naming the role. `qa` is never checked, and
29
+ * `eval` is not under `--no-eval`: a run that skips evaluation never resolves a judge.
30
+ *
31
+ * @param {object} models - The role -> model matrix (`exec`, `eval`, `qa`).
32
+ * @param {{noEval?: boolean}} [opts] - Whether evaluation is skipped.
33
+ * @returns {string[]} One sentence per offending role; empty when the matrix clears the floor.
34
+ */
35
+ export function floorProblems(models, { noEval = false } = {}) {
36
+ const out = [];
37
+ for (const role of ["exec", "eval"]) {
38
+ if (role === "eval" && noEval) continue;
39
+ if (belowFloor(models?.[role])) {
40
+ const m = String(models?.[role] ?? "").trim();
41
+ out.push(`models.${role} ${m ? `"${m}" ` : ""}is below the model floor (sonnet or above)`);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ /**
48
+ * Everything wrong with a launch record, as sentences. Empty means launchable.
49
+ *
50
+ * @param {object} a - The parsed launch record (`run-args.json`).
51
+ * @returns {string[]} Problems, empty when the record may run.
52
+ */
53
+ export function launchProblems(a) {
54
+ const problems = [];
55
+ for (const k of ["slug", "autoLevel", "models", "budgets"]) {
56
+ if (!a || a[k] === undefined || a[k] === null || a[k] === "") problems.push(`launch record is missing "${k}"`);
57
+ }
58
+ if (a?.autoLevel && !AUTO_LEVELS.includes(a.autoLevel)) problems.push(`autoLevel must be one of ${AUTO_LEVELS.join(" | ")}`);
59
+ if (a?.budgets && !(a.budgets.maxRounds && a.budgets.attemptBudget)) {
60
+ problems.push("budgets must carry maxRounds and attemptBudget");
61
+ }
62
+ if (a?.models) problems.push(...floorProblems(a.models, { noEval: Boolean(a.noEval) }));
63
+ return problems;
64
+ }
@@ -5,7 +5,7 @@
5
5
  // syntaxes (a string literal and a `join(cwd, "…", …)` chain) is a rename that no single search
6
6
  // completes.
7
7
  //
8
- // TIER DISCIPLINE (ADR-0001), the reason there are two roots:
8
+ // TIER DISCIPLINE, the reason there are two roots:
9
9
  // SHARED (committed) — prose a teammate reads: shaping, spec, contracts, requirements, report.
10
10
  // LOCAL (gitignored) — run state, envelopes, verification artifacts, machine policy.
11
11
  //
@@ -17,12 +17,12 @@
17
17
  // touching no filesystem. The run-key half reads receipts and fails open (`null`, never a throw),
18
18
  // because hooks call it and telemetry must never break a tool call.
19
19
 
20
- import { join } from "node:path";
20
+ import { join, resolve, sep } from "node:path";
21
21
  import { readFileSync } from "node:fs";
22
22
  import { createHash } from "node:crypto";
23
23
 
24
24
  // ---------------------------------------------------------------------------
25
- // The two roots. Renaming a root is these two lines plus migration 0006.
25
+ // The two roots. Renaming a root is these two lines.
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
28
  // Both roots are POSIX strings, deliberately. They are used two ways — fed to `join()` (which
@@ -35,7 +35,7 @@ import { createHash } from "node:crypto";
35
35
  *
36
36
  * Was `docs/shapeup-sdlc`. Moved out of `docs/` because many projects publish that directory
37
37
  * through a static-site generator, which either publishes the spec tree by accident or fails the
38
- * site build on it (ADR-0001).
38
+ * site build on it.
39
39
  */
40
40
  export const SHARED = "shapeup";
41
41
 
@@ -43,8 +43,8 @@ export const SHARED = "shapeup";
43
43
  export const LOCAL = ".shapeup";
44
44
 
45
45
  /**
46
- * The pre-ADR-0001 roots. Migration `0006` moves a project from these to the pair above;
47
- * `bin/init.mjs` writes ignore rules covering both so a half-migrated checkout cannot commit a
46
+ * The earlier roots. Nothing migrates a project off them any more;
47
+ * `bin/init.mjs` writes ignore rules covering both so a checkout still carrying one cannot commit a
48
48
  * run trace. Nothing else should read these.
49
49
  */
50
50
  export const LEGACY = { shared: "docs/shapeup-sdlc", local: ".shapeup-sdlc" };
@@ -81,6 +81,18 @@ export const sharedRoot = (cwd, slug) => join(cwd, SHARED, slug);
81
81
  * @param {string} slug - Feature slug.
82
82
  * @returns {string} `<cwd>/.shapeup/<slug>`.
83
83
  */
84
+ /**
85
+ * Whether a working directory is inside a run trace (a `.shapeup/` folder).
86
+ *
87
+ * A kernel command resolves every path from the working directory, so run from inside the trace it
88
+ * would open a second, empty run nested in the first and write its records there — a real run did
89
+ * exactly that. The commands that write run state refuse instead.
90
+ *
91
+ * @param {string} cwd - The directory the command was run from.
92
+ * @returns {boolean} True when `.shapeup` is one of its path segments.
93
+ */
94
+ export const insideRunTrace = (cwd) => resolve(cwd).split(sep).includes(LOCAL);
95
+
84
96
  export const localRoot = (cwd, slug) => join(cwd, LOCAL, slug);
85
97
 
86
98
  // ---------------------------------------------------------------------------
@@ -93,7 +105,7 @@ export const specDir = (cwd, slug) => join(sharedRoot(cwd, slug), "spec");
93
105
  export const usecasesDir = (cwd, slug) => join(specDir(cwd, slug), "usecases");
94
106
  /** Shaping artifacts — pitch, framing, breadboard, baseline, glossary. `init run` looks here for a breadboard. */
95
107
  export const shapingDir = (cwd, slug) => join(sharedRoot(cwd, slug), "shaping");
96
- // The three contracts are markdown on disk and JSON on the wire (ADR-0001) — see
108
+ // The three contracts are markdown on disk and JSON on the wire — see
97
109
  // `lib/contract.mjs`. `readContract()` accepts either extension, so a project mid-migration
98
110
  // keeps working; these builders name the form the harness WRITES.
99
111
 
@@ -103,20 +115,18 @@ export const scopesDir = (cwd, slug) => join(sharedRoot(cwd, slug), "scopes");
103
115
  export const scopeContract = (cwd, slug, id) => join(scopesDir(cwd, slug), `${id}.md`);
104
116
  /** The wiring map — engine → seam → entry-point call site → affordance. */
105
117
  export const wiringMap = (cwd, slug) => join(sharedRoot(cwd, slug), "wiring-map.md");
106
- /** Archetype + entry_point; gates the reachability arm of trace-lint. */
118
+ /** Archetype + entry_point + the build and launch probes. */
107
119
  export const projectProfile = (cwd, slug) => join(sharedRoot(cwd, slug), "project-profile.md");
108
120
  /** The REQ clause registry that covers-closure checks against. */
109
121
  export const requirements = (cwd, slug) => join(sharedRoot(cwd, slug), "requirements.md");
110
- /** Hill shards — mechanical phase per scope. */
111
- export const hillDir = (cwd, slug) => join(sharedRoot(cwd, slug), "hill");
112
122
  /** The frozen ship report, written once at GATE L4. */
113
123
  export const report = (cwd, slug) => join(sharedRoot(cwd, slug), "REPORT.md");
114
- /** Team-shared coaching rules, read back by the coachable workers and by the tech lead at GATE L0. */
124
+ /** Team-shared coaching rules, read back by the coachable workers, the plan activity and the loop at GATE L0. */
115
125
  export const knowledgeBaseDir = (cwd) => join(sharedDir(cwd), "knowledge-base");
116
- /** One worker's coaching file. */
126
+ /** One reader's coaching file (a coachable worker, or the `plan` / `building` activity). */
117
127
  export const knowledgeBase = (cwd, skill) => join(knowledgeBaseDir(cwd), `${skill}.md`);
118
128
  // `.harness-version` and `.harness-migrations` were resolved here, committed by necessity as
119
- // ADR-0001's third exception. Their sole writer was the upgrade path's data-migration runner, which
129
+ // The third exception to the tier rule. Their sole writer was the upgrade path's data-migration runner, which
120
130
  // has been removed; no code in the plugin has written either file since. The helpers went with
121
131
  // them rather than remaining as resolvable paths to files nothing produces — a path helper is a
122
132
  // claim that the file is part of the layout, and this one would have been false.
@@ -142,22 +152,18 @@ export const intake = (cwd, slug) => join(localRoot(cwd, slug), "intake.md");
142
152
  export const breadboard = (cwd, slug) => join(localRoot(cwd, slug), "breadboard.md");
143
153
  /**
144
154
  * The launch record's filename, as a constant rather than a literal at each call site — the
145
- * writer (`harness init run-args`) and every reader (`probe concurrency`'s `dialFrom()`) resolve
155
+ * writer (`harness init run`) and every reader resolve
146
156
  * the same path through this constant, the same way {@link runIdFromRoot} resolves `RECEIPT_FILE`
147
157
  * against a bare run root rather than through {@link receipt}'s `(cwd, slug)` form: a caller that
148
158
  * already holds the run root (an archived trace, `--run-root <dir>`) has no slug to reconstruct.
149
159
  */
150
160
  export const RUN_ARGS_FILE = "run-args.json";
151
- /** The launch record — the RunArgs object a run was launched with (GATE L0.9b), kernel-written. */
161
+ /** The launch record — the RunArgs object a run was launched with (GATE L0.9b), written by `init run`. */
152
162
  export const runArgsPath = (cwd, slug) => join(localRoot(cwd, slug), RUN_ARGS_FILE);
153
163
  /** The run ledger — rounds, decisions, status frontmatter. */
154
164
  export const harnessRun = (cwd, slug) => join(localRoot(cwd, slug), "harness-run.md");
155
- /** File-derived mid-run digest, frozen by `reduce snapshot --write` as an audit anchor. */
156
- export const runSnapshot = (cwd, slug) => join(localRoot(cwd, slug), "run-snapshot.json");
157
165
  /** The task board directory. */
158
166
  export const tasksDir = (cwd, slug) => join(localRoot(cwd, slug), "tasks");
159
- /** The board index. */
160
- export const boardIndex = (cwd, slug) => join(tasksDir(cwd, slug), "_index.md");
161
167
  /** Compiled WorkOrders. */
162
168
  export const ordersDir = (cwd, slug) => join(localRoot(cwd, slug), "orders");
163
169
  /** Returned WorkResults. */
@@ -167,7 +173,7 @@ export const resultsDir = (cwd, slug) => join(localRoot(cwd, slug), "results");
167
173
  *
168
174
  * IT HAS ITS OWN DIRECTORY, and that is not tidiness. A receipt filed as a sibling of the order it
169
175
  * answers (`orders/orient.receipt.json`) would land inside a directory three readers enumerate —
170
- * `probe resume`, `reduce graph` and `report export` all `readdirSync` `orders/` — where it survives
176
+ * `probe resume` and the order set both `readdirSync` `orders/` — where it survives
171
177
  * today only because each of them happens to filter `.json`. `orient.receipt.json` passes that
172
178
  * filter and becomes a bogus `Order` node in the run graph. A separate directory means no future
173
179
  * reader has to know this file exists.
@@ -180,27 +186,6 @@ export const resultsDir = (cwd, slug) => join(localRoot(cwd, slug), "results");
180
186
  * (`receipt`, above, is the RUN receipt — a different fact, hence the different name.)
181
187
  */
182
188
  export const dispatchReceipts = (cwd, slug) => join(localRoot(cwd, slug), "receipts", "dispatch.jsonl");
183
- /**
184
- * Leg-completion rows — the moment a dispatched leg was closed by the reducer.
185
- *
186
- * WHY IT IS A SEPARATE LEDGER FROM {@link dispatchReceipts}. A dispatch receipt is written by a
187
- * `PostToolUse` hook, which fires when the skill RESOLVES — measured across archived runs, 1.8 to
188
- * 47 seconds after the order was compiled, on legs that then ran for minutes. So the receipt is a
189
- * hook-attested START and there is no end anywhere in the record set: concurrency and span are
190
- * guesses without one. This ledger is the end, written by the one component that closes a leg.
191
- *
192
- * WHY IT SNAPSHOTS RATHER THAN POINTS. A row carries the order's `compiled_at` as a VALUE, not a
193
- * path to read it from later. Order paths are reused verbatim on relaunch, so the `compiled_at` in
194
- * `orders/<id>.json` is the LAST compile's; a row that deferred to the file would silently re-date
195
- * itself to a dispatch that is not the one it recorded.
196
- *
197
- * APPEND-ONLY JSONL, for the same two reasons `dispatch.jsonl` is: order ids repeat across
198
- * relaunches and rounds, and scopes close concurrently. One `O_APPEND` line needs no lock.
199
- *
200
- * A LEG THAT DIES LEAVES NO ROW, deliberately. The absence is the fact — a reader compares starts
201
- * to completions and reports the hole rather than averaging over it.
202
- */
203
- export const legLedger = (cwd, slug) => join(localRoot(cwd, slug), "legs.jsonl");
204
189
  /** T0 verification artifacts. */
205
190
  export const t0Dir = (cwd, slug) => join(localRoot(cwd, slug), "t0");
206
191
  /** Immutable per-attempt verdict artifacts the evaluator must cite. */
@@ -216,45 +201,22 @@ export const trials = (cwd, slug) => join(t0Dir(cwd, slug), "trials.jsonl");
216
201
  * {@link decisions}, one small file with one writer.
217
202
  */
218
203
  export const gates = (cwd, slug) => join(localRoot(cwd, slug), "gates.jsonl");
219
- /**
220
- * The round build gate's verdicts — one immutable artifact per gate run, `r<N>-t<T>.json`.
221
- *
222
- * A separate directory from `t0/`, on purpose. A T0 verdict is one scope's fixtures inside that
223
- * scope's substrate; this is the FEATURE's build, launched the way the ledger's `run_cmd` and the
224
- * profile's probes say a user would, and it belongs to no scope. Written by `harness verify build`,
225
- * read by `reduce hill` (a green T0 in a round whose build is red moves no dot) and by
226
- * `harness compile` (a red gate is the next round's bug list).
227
- */
228
- export const roundBuildDir = (cwd, slug) => join(localRoot(cwd, slug), "build");
229
204
  /** Evaluator output — report, evidence, verdict ledger. */
230
205
  export const evaluationDir = (cwd, slug) => join(localRoot(cwd, slug), "evaluation");
231
- /** The workflow launcher's run directory — `journal.jsonl` and the launch `result.json`. */
232
- export const workflowRunDir = (cwd, slug) => join(localRoot(cwd, slug), "workflow-run");
233
206
  /** QA hunt output. */
234
207
  export const qaDir = (cwd, slug) => join(localRoot(cwd, slug), "qa");
235
208
  /** Scout recon — code-surface map, spikes, discovered seed. */
236
209
  export const orientDir = (cwd, slug) => join(localRoot(cwd, slug), "orient");
237
210
  /** Time-boxed spike workspace. */
238
211
  export const spikesDir = (cwd, slug) => join(localRoot(cwd, slug), "spikes");
239
- /** Covers-closure + reachability run trace. */
240
- export const traceDir = (cwd, slug) => join(localRoot(cwd, slug), "trace");
241
212
  /** The discovered-task ledger every discovery flow appends to. */
242
213
  export const discoveryLedger = (cwd, slug) => join(localRoot(cwd, slug), "discovery", "ledger.md");
243
- /**
244
- * Adjudicated decisions, read back by `compile-order` as binding precedent.
245
- *
246
- * LOCAL since ADR-0001. It was committed, and it is appended to DURING a build round — so a run
247
- * left the working tree dirty in the deliverable tier while it was still building. Its
248
- * conclusions reach the team in `REPORT.md` at GATE L4 instead, frozen once.
249
- */
250
- export const roundLedger = (cwd, slug) => join(localRoot(cwd, slug), "round-ledger.md");
251
-
252
214
  /**
253
215
  * Spec WORKING NOTES — analysis that informed the contract but is not the contract.
254
216
  *
255
217
  * `synthesis.md`, `assess-report.md`, `feedback.md`, `api-feasibility.md`, `integration.md`. The
256
- * committed `spec/` keeps only what the evaluator grades against and a reviewer needs (ADR-0001
257
- * "contract vs working artifact").
218
+ * committed `spec/` keeps only what the evaluator grades against and a reviewer needs (the
219
+ * "contract vs working artifact" rule).
258
220
  */
259
221
  export const workingDir = (cwd, slug) => join(localRoot(cwd, slug), "working");
260
222
 
@@ -272,8 +234,7 @@ export const workingDir = (cwd, slug) => join(localRoot(cwd, slug), "working");
272
234
  *
273
235
  * What is left is written once, at run open, and never moved again, so it is not a concurrency
274
236
  * hazard. Its readers all degrade rather than fail without it: the budget check and the snapshot
275
- * both fall back to scanning for a receipt or a mid-run ledger, and `report export` refuses with
276
- * instructions rather than guessing. The one thing it uniquely supplies is the slug that
237
+ * both fall back to scanning for a receipt or a mid-run ledger. The one thing it uniquely supplies is the slug that
277
238
  * {@link resolveRunId} turns into a `run_id` for callers that were handed no slug — hook decision
278
239
  * rows, above all, which without it are written unjoinable to any run.
279
240
  */
@@ -296,7 +257,7 @@ export const activeOrder = (cwd) => join(localDir(cwd), "active-order");
296
257
  * close in the same act that retires the run pointers, and superseded by the next close.
297
258
  *
298
259
  * Why it exists: retiring the pointers is right (a finished run must stop fencing the checkout),
299
- * but the close lands BEFORE the stretch that decides the most — the scope-hammer census, the ship
260
+ * but the close lands BEFORE the stretch that decides the most — the stop census, the ship
300
261
  * phase, the export — and every hook decision in that stretch resolved its run through the pointer
301
262
  * that was just removed. Measured on a live run: every decision row after `closed_at` carried
302
263
  * `run_id: null`, the census dispatch included. The breadcrumb keeps that join without keeping the
@@ -305,30 +266,14 @@ export const activeOrder = (cwd) => join(localDir(cwd), "active-order");
305
266
  export const lastRun = (cwd) => join(localDir(cwd), "last-run");
306
267
 
307
268
  /**
308
- * Scope-hammer's census as DATA — `{verdict, cut_list, …}` — the one artifact GATE L4's resolver
309
- * reads before it lets an answer set say `ship`. The hammer's WorkResult may not carry a verdict
310
- * (its census is a proposal, never envelope data ingest acts on), and its printed H0/H1/H2 blocks
311
- * are prose; this file is the same proposal, written by the same hand, in a shape a gate can read.
312
- * Measured before it existed: a green census returned as the worker's report, and L4 could only
313
- * record `ask`, because nothing on disk said so.
269
+ * The stop step's census as DATA — `{verdict, cut_list, …}` — the one artifact GATE L4's resolver
270
+ * reads before it lets an answer set say `ship`. The loop's printed H0/H1/H2 blocks are prose; this
271
+ * file is the same proposal, written by the same hand, in a shape a gate can read. Measured before
272
+ * it existed: a green census returned as the worker's report, and L4 could only record `ask`,
273
+ * because nothing on disk said so. (The file keeps its name from when a worker wrote it.)
314
274
  */
315
275
  export const hammerCensus = (cwd, slug) => join(localRoot(cwd, slug), "reports", "hammer-census.json");
316
276
 
317
- /**
318
- * Where the run scripts are staged for launch, inside the project.
319
- *
320
- * The orchestrator's Workflow scripts ship with the plugin, which lives OUTSIDE the project — and
321
- * the Workflow tool will only load a script it is already allowed to read, meaning the working
322
- * directory or a directory the operator added. An install path therefore fails the launch outright,
323
- * with no permission rule that can fix it: the grant that authorises the tool does not widen where
324
- * it may read from. So ``harness init run`` copies the shipped scripts here, and the launch names
325
- * this project-local path. LOCAL, because a staged copy is a build artifact of the run, not a
326
- * source file anybody should review or commit.
327
- */
328
- export const workflowsStage = (cwd) => join(localDir(cwd), "workflows");
329
-
330
- /** The staged run script as the LAUNCH names it — project-relative, `/`-joined on every platform. */
331
- export const globWorkflowsStage = (...parts) => [LOCAL, "workflows", ...parts].join("/");
332
277
  /** Hook receipts — one row per evaluation, so `allow` carries evidence. */
333
278
  export const decisions = (cwd) => join(localDir(cwd), "decisions.jsonl");
334
279
  /** Human-authored safety escape hatch. LOCAL so no PR can widen another machine's envelope. */
@@ -336,9 +281,9 @@ export const safetyOverrides = (cwd) => join(localDir(cwd), "safety-overrides.js
336
281
  /**
337
282
  * Telemetry shards.
338
283
  *
339
- * LOCAL since ADR-0001. Committed, they put `process.env.HOSTNAME` — a person's laptop name —
284
+ * LOCAL. Committed, they put `process.env.HOSTNAME` — a person's laptop name —
340
285
  * into the repository, and append-only JSONL in git only grows. The cost, stated plainly: with
341
- * these local, ``harness probe stats`` becomes a personal tool and "is the KB flywheel working across the
286
+ * these local, the shards are a personal record, and "is the KB flywheel working across the
342
287
  * team?" is no longer answerable from the repo.
343
288
  */
344
289
  export const metricsDir = (cwd) => join(localDir(cwd), "metrics");
@@ -346,28 +291,10 @@ export const metricsDir = (cwd) => join(localDir(cwd), "metrics");
346
291
  export const metricsShard = (cwd, id = process.env.HOSTNAME || "local") =>
347
292
  join(metricsDir(cwd), `${id}.jsonl`);
348
293
 
349
- /**
350
- * The default landing tier for exported run records — the analysis plane's staging area.
351
- *
352
- * DELIBERATELY LOCAL, and the trade-off is stated rather than dodged. The run trace it reads from
353
- * (`.shapeup/<slug>/`) is regenerable and gets wiped per-slug; an export keyed by run id survives
354
- * that, which is the failure this directory closes. What it does NOT do is cross a machine
355
- * boundary: making it SHARED would put per-run structured data and a machine id back in the
356
- * repository, which is exactly what ADR-0001 moved the metrics shards out of git to prevent. A
357
- * cross-machine warehouse is therefore an explicit `--out <dir>` to a destination the operator
358
- * owns, never a default that commits telemetry on their behalf.
359
- */
360
- export const exportsDir = (cwd) => join(localDir(cwd), "exports");
361
-
362
- /** One exported run's table directory, keyed by the run id (see {@link mintRunId}). */
363
- export const exportRunDir = (cwd, runId) => join(exportsDir(cwd), String(runId ?? "unkeyed"));
364
- /** Archived pitches. */
365
- export const pitchArchiveDir = (cwd) => join(localDir(cwd), "pitch-archive");
366
-
367
294
  /**
368
295
  * Gate answer sets, in resolution order (first hit wins).
369
296
  *
370
- * ONE TIER ONLY, since ADR-0001. There used to be a third candidate — a COMMITTED
297
+ * ONE TIER ONLY. There used to be a third candidate — a COMMITTED
371
298
  * `gate-answers.json`, auto-discovered with no flag — so a file with `preset: ci` pre-approved
372
299
  * GATE L4 ship sign-off for everyone who pulled the repo, and the same filename meant "my
373
300
  * personal lane" at one path and "team policy" at another. Consent is now per-machine by
@@ -426,7 +353,7 @@ export const globShared = (slug, ...parts) => [SHARED, slug, ...parts].join("/")
426
353
 
427
354
  /**
428
355
  * The coaching file a coachable worker reads, as a repo-relative path for the WorkOrder payload.
429
- * @param {string} skill - A coachable worker name (`COACHABLE` in compile.mjs), or `tech-lead`.
356
+ * @param {string} skill - A coachable worker name (`COACHABLE` in compile.mjs), or an activity name (`plan`, `building`).
430
357
  * @returns {string} e.g. `shapeup/knowledge-base/task-executor.md`.
431
358
  */
432
359
  export const relKnowledgeBase = (skill) => [SHARED, "knowledge-base", `${skill}.md`].join("/");