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.
- package/.claude/settings.local.example.json +2 -3
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +115 -56
- package/README.md +101 -143
- package/SECURITY.md +16 -25
- package/agents/orient.md +14 -0
- package/agents/qa-edge-hunter.md +13 -0
- package/agents/spec-evaluator.md +14 -0
- package/agents/task-executor.md +14 -0
- package/bin/init.mjs +13 -16
- package/bin/lib/grant.mjs +8 -19
- package/commands/build.md +9 -14
- package/commands/eval.md +3 -3
- package/commands/hammer.md +9 -5
- package/commands/retro.md +12 -11
- package/commands/scopes.md +1 -12
- package/commands/ship.md +45 -88
- package/commands/wire.md +1 -8
- package/hooks/dispatch-receipt.mjs +94 -8
- package/hooks/hooks.json +0 -41
- package/hooks/lib/decision.mjs +6 -9
- package/hooks/lib/tier.mjs +50 -0
- package/hooks/sandbox-guard.mjs +140 -64
- package/kernel/gate.mjs +203 -22
- package/kernel/harness.mjs +20 -67
- package/kernel/init/run.mjs +175 -523
- package/kernel/lib/contract.mjs +9 -93
- package/kernel/lib/floor.mjs +64 -0
- package/kernel/lib/paths.mjs +39 -112
- package/kernel/order.mjs +157 -0
- package/kernel/probe/digest.mjs +2 -38
- package/kernel/schemas/domain.schema.json +146 -1125
- package/kernel/schemas/work-order.schema.json +8 -4
- package/kernel/schemas/work-result.schema.json +1 -1
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +27 -8
- package/kernel/verify/t0.mjs +2 -2
- package/package.json +4 -2
- package/skills/building/SKILL.md +144 -0
- package/skills/building/resources/gates.md +76 -0
- package/skills/building/resources/guidance.md +23 -0
- package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
- package/skills/building/resources/intake.md +157 -0
- package/skills/building/resources/orders.md +69 -0
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
- package/skills/building/resources/plan-scopes.md +95 -0
- package/skills/building/resources/plan-spec.md +102 -0
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
- package/skills/building/resources/plan-wire.md +82 -0
- package/skills/building/resources/plan.md +98 -0
- package/skills/building/resources/retro-research.md +60 -0
- package/skills/building/resources/retro-scan.md +49 -0
- package/skills/building/resources/retro.md +225 -0
- package/skills/building/resources/rounds.md +78 -0
- package/skills/building/resources/stop.md +127 -0
- package/skills/orient/SKILL.md +37 -37
- package/skills/qa-edge-hunter/SKILL.md +29 -14
- package/skills/shapeup/SKILL.md +5 -5
- package/skills/shapeup/resources/breadboarding.md +1 -1
- package/skills/shapeup/resources/context-compaction.md +5 -5
- package/skills/shapeup/resources/shaping.md +1 -1
- package/skills/spec-evaluator/SKILL.md +17 -21
- package/skills/spec-evaluator/references/dimension-contract.md +1 -1
- package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
- package/skills/task-executor/SKILL.md +11 -15
- package/commands/hill.md +0 -10
- package/hooks/gate-intake.mjs +0 -109
- package/hooks/gate-zerowork.mjs +0 -349
- package/hooks/tier-guard.mjs +0 -162
- package/kernel/compile.mjs +0 -1419
- package/kernel/init/fit.mjs +0 -199
- package/kernel/init/run-args.mjs +0 -206
- package/kernel/lib/breadboard.mjs +0 -165
- package/kernel/probe/attempts.mjs +0 -161
- package/kernel/probe/concurrency.mjs +0 -535
- package/kernel/probe/eval.mjs +0 -405
- package/kernel/probe/hunt.mjs +0 -104
- package/kernel/probe/leg.mjs +0 -229
- package/kernel/probe/owner.mjs +0 -142
- package/kernel/probe/requirements.mjs +0 -328
- package/kernel/probe/resume.mjs +0 -1110
- package/kernel/probe/rounds.mjs +0 -115
- package/kernel/probe/staged.mjs +0 -83
- package/kernel/probe/stats.mjs +0 -477
- package/kernel/probe/t0.mjs +0 -115
- package/kernel/reduce/board.mjs +0 -350
- package/kernel/reduce/graph.mjs +0 -482
- package/kernel/reduce/hill.mjs +0 -322
- package/kernel/reduce/ingest.mjs +0 -767
- package/kernel/reduce/leftovers.mjs +0 -159
- package/kernel/reduce/ship.mjs +0 -529
- package/kernel/reduce/snapshot.mjs +0 -291
- package/kernel/reduce/verdict.mjs +0 -172
- package/kernel/report/export.mjs +0 -384
- package/kernel/report/facts.mjs +0 -234
- package/kernel/verify/budget.mjs +0 -153
- package/kernel/verify/build.mjs +0 -428
- package/kernel/verify/dispatch.mjs +0 -114
- package/kernel/verify/spec.mjs +0 -975
- package/kernel/verify/trace.mjs +0 -604
- package/skills/ba-pitch-analyzer/SKILL.md +0 -204
- package/skills/coach/SKILL.md +0 -397
- package/skills/hill-chart/SKILL.md +0 -154
- package/skills/hill-chart/assets/dashboard.template.html +0 -1432
- package/skills/scope-architect/SKILL.md +0 -174
- package/skills/scope-hammer/SKILL.md +0 -231
- package/skills/solution-architect/SKILL.md +0 -158
- package/skills/tech-lead/SKILL.md +0 -151
- package/skills/tech-lead/references/gates.md +0 -605
- package/skills/tech-lead/references/protocol.md +0 -889
- package/skills/tech-lead/references/tiny-lane.md +0 -53
- package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
- package/skills/translator/SKILL.md +0 -258
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
package/kernel/init/fit.mjs
DELETED
|
@@ -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
|
-
|
package/kernel/init/run-args.mjs
DELETED
|
@@ -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
|
-
}
|