shapeup-sdlc 3.18.2 → 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 +29 -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,199 +0,0 @@
1
- #!/usr/bin/env node
2
- // GATE L0.3 — FIT CHECK. Which lane does this change belong in? Decided by measurement.
3
- //
4
- // WHY THIS EXISTS (and it is the root cause the other three fixes did not touch).
5
- //
6
- // A change of the size "add a `summary` command to a six-file CLI" — one new module plus one
7
- // dispatcher wiring — run through the full eleven-gate pipeline does not finish. Not once, across
8
- // repeated attempts: killed at the time cap mid-build; then, with a wall-clock breaker fitted,
9
- // killed at ship-triage; then producing an honest report saying its two must-haves were 0%
10
- // started.
11
- //
12
- // Every one of those fixes made the FAILURE better. None of them made the RUN finish, because
13
- // none addressed why a three-file change was consuming half an hour: **the ceremony was not
14
- // sized to the change.**
15
- //
16
- // The harness already knew. In those transcripts tech-lead identified the change at GATE L0 as
17
- // "about as small as they come… squarely inside the --tiny lane" — and then ran the full pipeline
18
- // anyway, because the lane was a judgment the model was free to talk itself out of. That is the
19
- // same class of defect as narration and as prose consent: an invariant living somewhere a model
20
- // can re-decide it. The project's rule is that such invariants move into the runtime.
21
- //
22
- // So the lane stops being a recommendation and becomes a computed value, recorded in the receipt
23
- // alongside the evidence for it. A PO can still override — with `--lane`, which is recorded AS an
24
- // override, so "we ran the heavy lane on a two-file change" is visible rather than accidental.
25
- //
26
- // WHAT IT MEASURES. Only things that are true before any work starts:
27
- // • the size of the tree being changed (a 6-file CLI is not a monorepo)
28
- // • how many distinct deliverables the intake asks for
29
- // • whether the intake introduces a new dependency, datastore, or external seam
30
- // • whether the intake itself signals breadth ("migration", "across", "end to end")
31
- //
32
- // It is deliberately CONSERVATIVE: `full` is the default and `tiny` must be earned. A wrong
33
- // `tiny` skips review on something that needed it; a wrong `full` only costs money. Those are not
34
- // symmetric, and the tie goes to the gates.
35
- //
36
- // USAGE
37
- // node `harness init fit` --intake-file <path> [--cwd <root>] [--json]
38
- // node `harness init fit` --intake-text "<requirement>" [--cwd <root>]
39
- //
40
- // Exit 0 always — this informs a decision, it does not deny a tool call. The orchestrator reads
41
- // `lane` and acts on it; ``harness init run`` records it in the receipt.
42
-
43
- import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
44
- import { join } from "node:path";
45
- import { LOCAL } from "../lib/paths.mjs";
46
- import { runArgs } from "../lib/argv.mjs";
47
-
48
- /** Directories that are never part of "the tree being changed". */
49
- const IGNORE_DIRS = new Set([".git", "node_modules", LOCAL, "dist", "build", ".next", "coverage", ".claude"]);
50
-
51
- /** Source extensions that count toward tree size. */
52
- const SOURCE_EXT = /\.(m?[jt]sx?|py|rb|go|rs|java|kt|swift|php|cs)$/i;
53
-
54
- /** Intake phrases that mean "this is bigger than it looks". */
55
- const BREADTH_SIGNALS = [
56
- /\bmigrat(e|ion)\b/i,
57
- /\bacross\b.*\b(files?|modules?|services?|commands?)\b/i,
58
- /\bend[- ]to[- ]end\b/i,
59
- /\brefactor\b/i,
60
- /\bbackfill\b/i,
61
- /\bschema\b/i,
62
- /\bbreaking change\b/i,
63
- /\bnew (dependency|package|library|service|datastore|database|table)\b/i,
64
- /\bauth(entication|orization)\b/i,
65
- /\bexternal (api|service|provider)\b/i,
66
- ];
67
-
68
- /** Countable deliverables: things the intake asks to exist that do not exist yet. */
69
- const DELIVERABLE_SIGNALS = [
70
- /\badd (?:a )?new\b/gi,
71
- /\badd (?:a|an|the)\b/gi,
72
- /\bnew (command|module|flag|endpoint|screen|field|option)\b/gi,
73
- /\bimplement\b/gi,
74
- /\bcreate\b/gi,
75
- /\bsupport\b/gi,
76
- ];
77
-
78
- /** Count source files in the tree, bounded so a huge repo cannot make this slow. */
79
- export function treeSize(root, cap = 400) {
80
- let files = 0;
81
- const walk = (dir, depth) => {
82
- if (files >= cap || depth > 6) return;
83
- let entries;
84
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
85
- for (const e of entries) {
86
- if (files >= cap) return;
87
- if (e.name.startsWith(".") && e.name !== ".claude") { if (IGNORE_DIRS.has(e.name)) continue; }
88
- if (IGNORE_DIRS.has(e.name)) continue;
89
- const p = join(dir, e.name);
90
- if (e.isDirectory()) walk(p, depth + 1);
91
- else if (SOURCE_EXT.test(e.name)) {
92
- try { if (statSync(p).size > 0) files++; } catch { /* unreadable */ }
93
- }
94
- }
95
- };
96
- walk(root, 0);
97
- return files;
98
- }
99
-
100
- /**
101
- * Decide the lane from measurable facts.
102
- *
103
- * @param {{intake: string, files: number}} input
104
- * @returns {{lane: "tiny"|"full", confidence: "clear"|"borderline", reasons: string[], signals: object}}
105
- */
106
- export function decideLane({ intake, files }) {
107
- const text = String(intake || "");
108
- const breadth = BREADTH_SIGNALS.filter((re) => re.test(text)).map((re) => String(re));
109
- let deliverables = 0;
110
- for (const re of DELIVERABLE_SIGNALS) {
111
- re.lastIndex = 0;
112
- deliverables += (text.match(re) || []).length;
113
- }
114
-
115
- const signals = {
116
- tree_source_files: files,
117
- intake_chars: text.length,
118
- deliverable_mentions: deliverables,
119
- breadth_signals: breadth.length,
120
- };
121
-
122
- // CONJUNCTIVE, and it must be. The first version asked "is there evidence this is BIG?" and
123
- // defaulted to tiny when it found none — which classified every trial feature as tiny,
124
- // including the five-seam one that genuinely needs the pipeline and used two evaluation rounds
125
- // to pass. A router that confident and that wrong is worse than no router: it would skip review
126
- // on exactly the change that needed it.
127
- //
128
- // So `tiny` now requires positive evidence of smallness on EVERY axis, and `full` is what you
129
- // get by default. A wrong `tiny` skips gates on something that needed them; a wrong `full` only
130
- // costs money. Those are not symmetric.
131
- const TINY = { files: 10, chars: 1500, deliverables: 2 };
132
- const checks = [
133
- { ok: files > 0 && files <= TINY.files, why: `tree has ${files} source file(s) (tiny needs 1–${TINY.files})` },
134
- { ok: text.length <= TINY.chars, why: `intake is ${text.length} chars (tiny needs ≤${TINY.chars})` },
135
- { ok: deliverables <= TINY.deliverables, why: `intake names ~${deliverables} deliverable(s) (tiny needs ≤${TINY.deliverables})` },
136
- { ok: breadth.length === 0, why: breadth.length ? `${breadth.length} breadth signal(s) present` : "no breadth signals" },
137
- ];
138
- const reasons = checks.map((c) => `${c.ok ? "✓" : "✗"} ${c.why}`);
139
- const tiny = checks.every((c) => c.ok);
140
- const failed = checks.filter((c) => !c.ok).length;
141
-
142
- // "Borderline" is not decoration: it is the signal that a human should look. One failed check
143
- // means the change sits on the boundary and the lane is a judgment, not a measurement.
144
- const confidence = tiny ? "clear" : failed === 1 ? "borderline" : "clear";
145
-
146
- return {
147
- lane: tiny ? "tiny" : "full",
148
- confidence,
149
- reasons,
150
- signals,
151
- // Say it on every invocation rather than burying it in a doc. These thresholds are calibrated
152
- // against THREE features. That is enough to stop the router being obviously wrong and nowhere
153
- // near enough to trust it silently, so it recommends and never denies.
154
- calibration: "advisory — thresholds are coarse and `full` is the default; the PO may override with --lane",
155
- };
156
- }
157
-
158
- // ---- CLI -------------------------------------------------------------------
159
-
160
- /** The typed argv contract (see `./lib/argv.mjs`). */
161
- export const ARGV_SPEC = {
162
- usage: 'harness.mjs init fit (--intake-file <path> | --intake-text "<requirement>") [--cwd <dir>]',
163
- _: { arity: 0, max: 0, name: "(no positional operands)" },
164
- cwd: { type: "path" },
165
- "intake-text": { type: "str" },
166
- "intake-file": { type: "path" },
167
- };
168
-
169
- /**
170
- * Decide the lane (full or tiny) for an intake and the tree it will build in.
171
- *
172
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
173
- * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
174
- * call `process.exit()` with the subcommand's documented code rather than returning.
175
- */
176
- export function cli(rawArgv) {
177
- const args = runArgs(ARGV_SPEC, rawArgv);
178
- const cwd = args.cwd || process.cwd();
179
- let intake = args.intakeText ?? null;
180
- const f = args.intakeFile ?? null;
181
- if (f) {
182
- const p = f.startsWith("/") ? f : join(cwd, f);
183
- if (!existsSync(p)) { console.error(`--intake-file not found: ${p}`); process.exit(2); }
184
- intake = readFileSync(p, "utf8");
185
- }
186
- if (!intake) { console.error("fit-check: pass --intake-file <path> or --intake-text \"<requirement>\""); process.exit(2); }
187
-
188
- const files = treeSize(cwd);
189
- const result = decideLane({ intake, files });
190
-
191
- console.log(JSON.stringify({
192
- ...result,
193
- // The sentence the orchestrator must act on, so the decision is not re-derived from the JSON.
194
- action: result.lane === "tiny"
195
- ? "TINY LANE. Run: orient (light) → single-task board → build → T0 → ⏸ L4. Skip WIRE, scope contracts, spec tree, EVAL and QA. The full pipeline on a change this size is why a run burns its whole clock without finishing."
196
- : "FULL LANE. Run the complete pipeline: ORIENT → WIRE → MAP SCOPES → BUILD → EVAL → QA → GATE H → L4.",
197
- }, null, 2));
198
- }
199
-
@@ -1,206 +0,0 @@
1
- #!/usr/bin/env node
2
- // GATE L0.9b — THE LAUNCH RECORD, GIVEN A WRITER.
3
- //
4
- // WHY THIS EXISTS. `domain.schema.json`'s `RunArgs` entry calls `.shapeup/<slug>/run-args.json`
5
- // "the only artifact that records what a run was configured with" and names tech-lead as the
6
- // writer — but "tech-lead" meant a paragraph of prose telling the orchestrating session to
7
- // assemble a JSON object by hand and `Write` it. Outside a structural-test fixture, nothing ever
8
- // did: no kernel module wrote the file, so `probe concurrency`'s `dialFrom()` read a fan-out dial
9
- // that was never recorded and reported the effective default every time, indistinguishable from a
10
- // run that genuinely chose it. A registry entry is not an instruction, and an instruction with no
11
- // enforcer is how a documented record becomes a file that simply never exists.
12
- //
13
- // THE FIX. This is the single writer. It takes the resolved GATE L0 values as flags, builds the
14
- // exact `RunArgs` object the schema describes, writes it to `.shapeup/<slug>/run-args.json`, and
15
- // prints that SAME object on stdout — so tech-lead passes the printed value straight to
16
- // `Workflow({args: ...})` rather than re-typing it a second time. One construction, not two: the
17
- // file on disk and the value the workflow actually launches with can no longer disagree.
18
- //
19
- // `wallClockS` is NOT one of this command's flags and never lands in the object it writes.
20
- // `--wall-clock-budget` is consumed earlier, by `harness init run` (see `RECEIPT_VERSION` and
21
- // `config.wall_clock_budget_s` in `./run.mjs`) — the deadline breaker reads that receipt field
22
- // directly and was never going to see this launch's `RunArgs` at all. Naming a third field here
23
- // that nothing reads is the exact defect this command exists to close, not one to reintroduce.
24
- //
25
- // USAGE
26
- // node `harness init run-args` --slug <slug> --auto-level interactive|auto|unattended \
27
- // --exec-model <name> [--eval-model <name>] [--qa-model <name>] \
28
- // --max-rounds N --attempts N --plugin-root <dir> \
29
- // [--run-id <id>] [--answers <preset|path>] [--lane full|tiny] \
30
- // [--no-eval] [--no-qa] [--adversarial-verify] [--parallel-scopes N] [--cwd <dir>]
31
- //
32
- // `--eval-model` is required unless `--no-eval` is set — an EVAL-skipping run never resolves one.
33
- // `--run-id`, given no explicit value, is read off the run's own receipt (the run must already be
34
- // open — see `harness init run`), never invented.
35
- //
36
- // Prints the written `RunArgs` object as JSON on stdout. Exit 0 on success, 2 on a usage error,
37
- // 3 when no run is open at `--slug` (the receipt is what makes this artifact meaningful at all).
38
-
39
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
40
- import { dirname, join, resolve } from "node:path";
41
- import { fileURLToPath } from "node:url";
42
- import { runArgs } from "../lib/argv.mjs";
43
- import { localRoot, RECEIPT_FILE, runArgsPath, runIdFromReceipt } from "../lib/paths.mjs";
44
-
45
- const HERE = dirname(fileURLToPath(import.meta.url));
46
- /** The plugin root, resolved from where this file actually is (`kernel/init/` → repo root). */
47
- export const PLUGIN_ROOT = resolve(HERE, "../..");
48
-
49
- /**
50
- * The `autoLevel` enum, read from the schema that defines it — never hand-typed here.
51
- *
52
- * `domain.schema.json`'s `$defs/RunArgs.properties.autoLevel.enum` is the one place this set is
53
- * declared. A `new Set([...])` beside it, inside the very module written to close run-argument
54
- * contract drift (see this file's own banner), would be that drift repeating one level down —
55
- * the same class of failure this module exists to close, applied here to an *enum's values*
56
- * instead of RunArgs *field* names. Mirrors `kernel/verify/skills.mjs`'s `roster()`, which
57
- * derives `WorkerName` the same way.
58
- *
59
- * @param {string} [root=PLUGIN_ROOT] - Plugin root the schema is read from — overridable so a test
60
- * can point this at a scratch copy of the schema and prove the result grows and shrinks with it,
61
- * never with an edit to this function.
62
- * @returns {string[]} The enum, in schema order.
63
- * @throws {Error} When the schema is missing or does not carry the enum.
64
- */
65
- export function autoLevels(root = PLUGIN_ROOT) {
66
- const schemaPath = join(root, "kernel/schemas/domain.schema.json");
67
- const schema = JSON.parse(readFileSync(schemaPath, "utf8"));
68
- const levels = schema?.$defs?.RunArgs?.properties?.autoLevel?.enum;
69
- if (!Array.isArray(levels) || !levels.length) {
70
- throw new Error(`${schemaPath} carries no $defs/RunArgs.properties.autoLevel.enum — the auto-level set cannot be derived`);
71
- }
72
- return levels;
73
- }
74
-
75
- /** The typed argv contract (see `../lib/argv.mjs`). */
76
- export const ARGV_SPEC = {
77
- usage: `harness.mjs init run-args --slug <slug> --auto-level ${autoLevels().join("|")} ` +
78
- '--exec-model <name> [--eval-model <name>] [--qa-model <name>] --max-rounds N --attempts N ' +
79
- '--plugin-root <dir> [--run-id <id>] [--answers <preset|path>] [--lane full|tiny] ' +
80
- '[--no-eval] [--no-qa] [--adversarial-verify] [--parallel-scopes N] [--cwd <dir>]',
81
- _: { arity: 0, max: 0, name: "(no positional operands)" },
82
- cwd: { type: "path" },
83
- slug: { type: "str", required: true },
84
- "run-id": { type: "str" },
85
- "auto-level": { type: "str", required: true },
86
- answers: { type: "str" },
87
- lane: { type: "str" },
88
- "exec-model": { type: "str", required: true },
89
- "eval-model": { type: "str" },
90
- "qa-model": { type: "str" },
91
- "max-rounds": { type: "int", min: 1, required: true },
92
- attempts: { type: "int", min: 1, required: true },
93
- "plugin-root": { type: "path", required: true },
94
- "no-eval": { type: "flag" },
95
- "no-qa": { type: "flag" },
96
- "adversarial-verify": { type: "flag" },
97
- "parallel-scopes": { type: "int", min: 1 },
98
- };
99
-
100
- function fail(code, msg) {
101
- console.error(msg);
102
- process.exit(code);
103
- }
104
-
105
- /**
106
- * Drop `undefined`-valued keys, one level deep on the named nested objects — so the JSON this
107
- * writes never carries a literal `"eval": undefined` for a skipped role or an unset switch.
108
- *
109
- * @param {object} o - The candidate RunArgs object.
110
- * @returns {object} The same shape with every `undefined` leaf and empty nested object removed.
111
- */
112
- function pruned(o) {
113
- const out = {};
114
- for (const [k, v] of Object.entries(o)) {
115
- if (v === undefined) continue;
116
- if (v && typeof v === "object" && !Array.isArray(v)) {
117
- const inner = pruned(v);
118
- if (Object.keys(inner).length) out[k] = inner;
119
- continue;
120
- }
121
- out[k] = v;
122
- }
123
- return out;
124
- }
125
-
126
- /**
127
- * Build the `RunArgs` object — pure, so the structural suite can assert its shape without a
128
- * filesystem. Mirrors `domain.schema.json` `$defs/RunArgs` exactly: this is the one place that
129
- * shape is constructed, so a field this function does not carry cannot reach the file either.
130
- *
131
- * @param {object} o - Resolved inputs (destructured); optional fields may be `undefined`.
132
- * @returns {object} The RunArgs object, pruned of unset optionals.
133
- */
134
- export function buildRunArgs({
135
- slug, runId, autoLevel, answers, lane, execModel, evalModel, qaModel,
136
- maxRounds, attemptBudget, pluginRoot, startedAt,
137
- noEval, noQa, adversarialVerify, maxParallelScopes,
138
- }) {
139
- return pruned({
140
- slug, runId, autoLevel, answers, lane,
141
- models: { exec: execModel, eval: evalModel, qa: qaModel },
142
- budgets: { maxRounds, attemptBudget },
143
- pluginRoot, startedAt,
144
- noEval, noQa, adversarialVerify, maxParallelScopes,
145
- });
146
- }
147
-
148
- /**
149
- * Write `.shapeup/<slug>/run-args.json`, or refuse (exit 3) when no run is open at `--slug`.
150
- *
151
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
152
- * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; every path
153
- * calls `process.exit()` with the subcommand's documented code rather than returning.
154
- */
155
- export function cli(rawArgv) {
156
- const args = runArgs(ARGV_SPEC, rawArgv);
157
- const cwd = args.cwd || process.cwd();
158
-
159
- if (!autoLevels().includes(args.autoLevel)) {
160
- fail(2, `--auto-level must be one of: ${autoLevels().join(", ")}`);
161
- }
162
- if (!args.noEval && !args.evalModel) {
163
- fail(2, "--eval-model is required unless --no-eval is set — an EVAL-skipping run never resolves one.");
164
- }
165
-
166
- const runRoot = localRoot(cwd, args.slug);
167
- const receiptPath = join(runRoot, RECEIPT_FILE);
168
- if (!existsSync(receiptPath)) {
169
- fail(3, [
170
- `✋ init run-args: no open run at ${runRoot} — run "harness init run" first (GATE L0.1).`,
171
- "",
172
- "`run-args.json` records what an OPEN run was launched with; writing one with no receipt",
173
- "behind it would leave a launch record for a run that, everywhere else in the harness, never",
174
- "started.",
175
- ].join("\n"));
176
- }
177
- let receipt = null;
178
- try { receipt = JSON.parse(readFileSync(receiptPath, "utf8")); } catch { /* handled below */ }
179
- const runId = args.runId ?? runIdFromReceipt(receipt) ?? undefined;
180
-
181
- const runArgsObj = buildRunArgs({
182
- slug: args.slug,
183
- runId,
184
- autoLevel: args.autoLevel,
185
- answers: args.answers ?? undefined,
186
- lane: args.lane ?? undefined,
187
- execModel: args.execModel,
188
- evalModel: args.evalModel ?? undefined,
189
- qaModel: args.qaModel ?? undefined,
190
- maxRounds: args.maxRounds,
191
- attemptBudget: args.attempts,
192
- pluginRoot: args.pluginRoot,
193
- startedAt: new Date().toISOString(),
194
- noEval: args.noEval || undefined,
195
- noQa: args.noQa || undefined,
196
- adversarialVerify: args.adversarialVerify || undefined,
197
- maxParallelScopes: args.parallelScopes ?? undefined,
198
- });
199
-
200
- mkdirSync(runRoot, { recursive: true });
201
- writeFileSync(runArgsPath(cwd, args.slug), JSON.stringify(runArgsObj, null, 2) + "\n", "utf8");
202
-
203
- // Printed, not just written — so the caller's `Workflow({args: ...})` reads this stdout instead
204
- // of re-assembling the object from the flags it just typed.
205
- console.log(JSON.stringify(runArgsObj, null, 2));
206
- }
@@ -1,165 +0,0 @@
1
- // breadboard — read a `/shapeup` breadboard's element ids out of its markdown tables.
2
- //
3
- // WHY THIS EXISTS. A pitch is two files: `shaping.md` (problem, requirements, parts) and
4
- // `breadboard.md` (Places, affordances, slices). The run used to take only the first, so a Place
5
- // that existed only in the breadboard — a new sheet, a new modal — reached no planning worker, and
6
- // the spec folded it into whichever screen the prose happened to mention. The ids read here are what
7
- // the run pins at open (the receipt's counts) and what spec-lint checks placement against.
8
- //
9
- // WHAT IT READS, and nothing more. An element id is the FIRST cell of a markdown table row —
10
- // `P1`, `P2.1`, `U3`, `N1b`, `S2`, `V4` — after stripping `**` and backticks, so both table layouts
11
- // the breadboarding guide ships parse the same way: a `#` first column and an `ID` first column. A
12
- // row's Places come from its `Place` column, every `P#` token in the cell (`P1 / P3`, `P2/P1`); a
13
- // table with no Place column gives its rows no Places. Places may also be listed as prose under a
14
- // `## Places` heading (`P1: Composer — …`), which is the guide's own output template.
15
- //
16
- // It skips fenced code blocks: a table inside a fence is an example of a breadboard, not this one.
17
- //
18
- // Tolerant by design. A layout this reader cannot parse yields zero ids, and every caller treats
19
- // zero ids as "nothing to check" plus a warning — never as a hard stop. Zero dependencies.
20
-
21
- /** A first-cell element id: a Place (`P1`, `P2.1`), or a UI/code affordance, store or slice. */
22
- export const ID_PATTERN = /^(P\d+(?:\.\d+)*|[UNSV]\d+[a-z]?)$/;
23
-
24
- /** Every Place reference inside a cell. Global — use with `match`, never `test`. */
25
- const PLACE_REF = /\bP\d+(?:\.\d+)*\b/g;
26
-
27
- /** A prose Place line under `## Places`: `P1: …`, `- P1 — …`, `**P2** – …`. */
28
- const PROSE_PLACE = /^\s*(?:[-*+]\s+)?\**(P\d+(?:\.\d+)*)\**\s*[:—–-]\s*(.*)$/;
29
-
30
- /**
31
- * Split one markdown table row into trimmed cells.
32
- * @param {string} line - A line that starts with `|`.
33
- * @returns {string[]} The cells between the outer pipes.
34
- */
35
- function cells(line) {
36
- const t = line.trim().replace(/^\|/, "").replace(/\|$/, "");
37
- return t.split("|").map((c) => c.trim());
38
- }
39
-
40
- /**
41
- * Strip the emphasis and code marks a breadboard author puts around an id.
42
- * @param {string} cell - A raw table cell.
43
- * @returns {string} The cell's text without `**` and backticks.
44
- */
45
- function bare(cell) {
46
- return String(cell ?? "").replace(/\*\*/g, "").replace(/`/g, "").trim();
47
- }
48
-
49
- /**
50
- * Is this line a table's header separator (`|---|:--:|`)?
51
- * @param {string} line - A table line.
52
- * @returns {boolean} True for a separator row.
53
- */
54
- function isSeparator(line) {
55
- return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line);
56
- }
57
-
58
- /**
59
- * Parse a breadboard's element ids.
60
- *
61
- * @param {string} text - The breadboard markdown (or a pitch that carries one inline).
62
- * @returns {{places: {id: string, name: string}[], ui: {id: string, places: string[]}[],
63
- * code: {id: string, places: string[]}[], stores: {id: string, places: string[]}[],
64
- * slices: {id: string}[]}} Every id found, de-duplicated by id; a repeated row merges its Places.
65
- */
66
- export function parseBreadboard(text) {
67
- const lines = String(text ?? "").split(/\r?\n/);
68
- const places = new Map();
69
- const byKind = { U: new Map(), N: new Map(), S: new Map(), V: new Map() };
70
-
71
- const addPlace = (id, name) => {
72
- if (!places.has(id)) places.set(id, { id, name: name || "" });
73
- else if (!places.get(id).name && name) places.get(id).name = name;
74
- };
75
- const addElement = (id, refs) => {
76
- const m = byKind[id[0]];
77
- if (!m.has(id)) m.set(id, new Set());
78
- for (const p of refs) m.get(id).add(p);
79
- };
80
-
81
- let inFence = false;
82
- let inPlacesSection = false;
83
- let header = null; // the current table's header cells, lower-cased
84
- let prevWasTable = false;
85
-
86
- for (let i = 0; i < lines.length; i++) {
87
- const line = lines[i];
88
- if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; prevWasTable = false; header = null; continue; }
89
- if (inFence) continue;
90
-
91
- const heading = line.match(/^\s*#{1,6}\s+(.*)$/);
92
- if (heading) {
93
- inPlacesSection = /^places\b/i.test(bare(heading[1]));
94
- prevWasTable = false; header = null;
95
- continue;
96
- }
97
-
98
- if (/^\s*\|/.test(line)) {
99
- if (!prevWasTable) {
100
- // A table starts here. Its first line is the header when the next line is a separator.
101
- header = isSeparator(lines[i + 1] ?? "") ? cells(line).map((c) => bare(c).toLowerCase()) : null;
102
- prevWasTable = true;
103
- if (header) continue;
104
- }
105
- if (isSeparator(line)) continue;
106
- const row = cells(line);
107
- const id = bare(row[0]);
108
- if (!ID_PATTERN.test(id)) continue;
109
- const placeCol = header ? header.indexOf("place") : -1;
110
- if (id[0] === "P") {
111
- const nameCol = placeCol > 0 ? placeCol : (header ? header.findIndex((h) => h === "name") : -1);
112
- addPlace(id, bare(row[nameCol > 0 ? nameCol : 1] ?? ""));
113
- } else {
114
- const cell = placeCol > 0 ? bare(row[placeCol] ?? "") : "";
115
- addElement(id, cell.match(PLACE_REF) ?? []);
116
- }
117
- continue;
118
- }
119
- prevWasTable = false; header = null;
120
-
121
- if (inPlacesSection) {
122
- const m = line.match(PROSE_PLACE);
123
- if (m) addPlace(m[1], bare(m[2]).split(/\s+[—–-]\s+/)[0]);
124
- }
125
- }
126
-
127
- const list = (m) => [...m].map(([id, refs]) => ({ id, places: [...refs] }));
128
- return {
129
- places: [...places.values()],
130
- ui: list(byKind.U),
131
- code: list(byKind.N),
132
- stores: list(byKind.S),
133
- slices: [...byKind.V.keys()].map((id) => ({ id })),
134
- };
135
- }
136
-
137
- /**
138
- * Does this text carry a breadboard of its own — at least one Place and one UI affordance?
139
- *
140
- * The test for a pitch that embeds its breadboard inline rather than as a second file. Both are
141
- * required: a shaping doc routinely has tables, and a Place with nothing to place is not a
142
- * breadboard anything downstream can check.
143
- *
144
- * @param {string} text - Markdown to inspect.
145
- * @returns {boolean} True when `parseBreadboard` finds a P# and a U#.
146
- */
147
- export function hasBreadboardTables(text) {
148
- const bb = parseBreadboard(text);
149
- return bb.places.length > 0 && bb.ui.length > 0;
150
- }
151
-
152
- /**
153
- * Count a parsed breadboard's ids by kind, the shape the run receipt records.
154
- * @param {ReturnType<typeof parseBreadboard>} parsed - A `parseBreadboard` result.
155
- * @returns {{P: number, U: number, N: number, S: number, V: number}} Ids per kind.
156
- */
157
- export function idCounts(parsed) {
158
- return {
159
- P: parsed.places.length,
160
- U: parsed.ui.length,
161
- N: parsed.code.length,
162
- S: parsed.stores.length,
163
- V: parsed.slices.length,
164
- };
165
- }