shapeup-sdlc 1.6.3 → 3.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 +5 -5
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +54 -107
- package/README.md +165 -151
- package/SECURITY.md +49 -27
- package/bin/init.mjs +93 -108
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +20 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +53 -7
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +16 -17
- package/hooks/gate-zerowork.mjs +107 -25
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +38 -19
- package/hooks/safety-spine.mjs +4 -4
- package/hooks/sandbox-guard.mjs +130 -50
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
- package/kernel/init/run.mjs +489 -0
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
- package/kernel/lib/paths.mjs +491 -0
- package/kernel/probe/concurrency.mjs +510 -0
- package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/kernel/probe/resume.mjs +528 -0
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
- package/kernel/reduce/graph.mjs +437 -0
- package/kernel/reduce/hill.mjs +152 -0
- package/kernel/reduce/ingest.mjs +633 -0
- package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
- package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
- package/kernel/report/export.mjs +325 -0
- package/kernel/report/facts.mjs +347 -0
- package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
- package/kernel/verify/dispatch.mjs +114 -0
- package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
- package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
- package/kernel/verify/skills.mjs +125 -0
- package/kernel/verify/spec.mjs +559 -0
- package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +2 -2
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +2 -2
- package/oracles/snapshot-oracle.mjs +2 -2
- package/oracles/test-oracle.mjs +2 -2
- package/package.json +11 -13
- package/skills/ba-pitch-analyzer/SKILL.md +24 -19
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +15 -41
- package/skills/scope-architect/SKILL.md +57 -18
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +37 -15
- package/skills/spec-evaluator/SKILL.md +24 -7
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/task-executor/SKILL.md +32 -18
- package/skills/tech-lead/SKILL.md +127 -438
- package/skills/tech-lead/references/gates.md +140 -49
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +645 -198
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +56 -18
- package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
- package/skills/translator/SKILL.md +2 -2
- package/hooks/anti-rationalization.mjs +0 -244
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -151
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -109
- package/skills/advisor-protocol/SKILL.md +0 -171
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
- package/skills/spec-evaluator/README.md +0 -93
- package/skills/tech-lead/README.md +0 -71
- package/skills/tech-lead/references/delegation.md +0 -254
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -214
- package/skills/tech-lead/references/round-protocol.md +0 -184
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
- package/skills/tech-lead/scripts/init-run.mjs +0 -326
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
- package/skills/translator/README.md +0 -66
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GATE ANSWER SET — cross a gate with a pre-recorded decision instead of a live human.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (observed, not theorized).
|
|
5
5
|
//
|
|
6
6
|
// This harness pauses at every ⏸ gate for PO sign-off by default. That is the point of it. But
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// unattended it produces two distinct failures, both of which look like the harness being slow or
|
|
8
|
+
// broken rather than the harness being safe:
|
|
9
9
|
//
|
|
10
|
-
// 1. STALL → TIMEOUT.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// 1. STALL → TIMEOUT. The run is killed at its time cap having produced nothing scoreable,
|
|
11
|
+
// on a feature that takes minutes to build by hand. A run with no human at the keyboard
|
|
12
|
+
// sitting at a gate does not fail — it waits, and a wait is indistinguishable from work
|
|
13
|
+
// until the budget runs out.
|
|
14
14
|
// 2. CONSENT-BY-PROSE. The workaround was a paragraph of English in the prompt ("treat this
|
|
15
|
-
// message as advance sign-off for every gate").
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// that can be re-summarized instead of acted on.
|
|
15
|
+
// message as advance sign-off for every gate"). Some models act on it. Others read the
|
|
16
|
+
// paragraph, read the gate list, and narrate the pipeline instead of running it. Consent
|
|
17
|
+
// carried in prose is consent that can be re-summarized instead of acted on.
|
|
19
18
|
//
|
|
20
19
|
// The organising rule of this project is that every invariant that matters lives in the runtime,
|
|
21
20
|
// not in a prompt. Gate sign-off was the last big one still living in a prompt. So it becomes a
|
|
@@ -33,10 +32,10 @@
|
|
|
33
32
|
// pre-recorded answer") rather than a silent wait that gets reported as a slow harness.
|
|
34
33
|
//
|
|
35
34
|
// USAGE
|
|
36
|
-
// node gate
|
|
37
|
-
// node gate
|
|
38
|
-
// node gate
|
|
39
|
-
// node gate
|
|
35
|
+
// node `harness gate` --init [--preset ci|guarded|interactive] [--out <path>] [--by "<name>"]
|
|
36
|
+
// node `harness gate` --resolve <gate-id> [--file <path>|--preset <name>] [--slug <slug>]
|
|
37
|
+
// node `harness gate` --list [--file <path>|--preset <name>]
|
|
38
|
+
// node `harness gate` --verify [--file <path>|--preset <name>] [--auto-level unattended]
|
|
40
39
|
//
|
|
41
40
|
// RESOLUTION ORDER for --resolve / --list / --verify, first hit wins:
|
|
42
41
|
// 1. --file <path>
|
|
@@ -51,11 +50,10 @@
|
|
|
51
50
|
// 5 no answer and on_missing=abort → the run aborts here, attributably
|
|
52
51
|
// 2 usage / validation error
|
|
53
52
|
|
|
54
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
53
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
55
54
|
import { join, dirname } from "node:path";
|
|
56
|
-
import { isMain } from "./lib/is-main.mjs";
|
|
57
55
|
import { runArgs } from "./lib/argv.mjs";
|
|
58
|
-
import { gateAnswerCandidates, LOCAL } from "./lib/paths.mjs";
|
|
56
|
+
import { gateAnswerCandidates, gates as gatesPath, LOCAL } from "./lib/paths.mjs";
|
|
59
57
|
|
|
60
58
|
export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
|
|
61
59
|
|
|
@@ -79,7 +77,7 @@ export const VALID_BY_GATE = {
|
|
|
79
77
|
const CI_NOTE = "Pre-approved for a headless lane. No human is present; the decision is recorded here so the ledger still names a source.";
|
|
80
78
|
|
|
81
79
|
export const PRESETS = {
|
|
82
|
-
// Everything pre-approved. The lane a CI step
|
|
80
|
+
// Everything pre-approved. The lane a CI step runs in.
|
|
83
81
|
ci: {
|
|
84
82
|
version: 1,
|
|
85
83
|
preset: "ci",
|
|
@@ -205,6 +203,33 @@ export function resolve(set, gate, source) {
|
|
|
205
203
|
};
|
|
206
204
|
}
|
|
207
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Append one row to the per-slug gate-crossing ledger — `.shapeup/<slug>/gates.jsonl` ({@link
|
|
208
|
+
* gatesPath}, from `lib/paths.mjs`).
|
|
209
|
+
*
|
|
210
|
+
* WRITER RULE, same shape as {@link appendTrial} in `verify/t0.mjs` and the dispatch-receipt hook's
|
|
211
|
+
* ownership of `decisions.jsonl`: one small file, one writer, append-only JSONL so concurrent
|
|
212
|
+
* scopes crossing gates in the same round cannot clobber each other's row.
|
|
213
|
+
*
|
|
214
|
+
* Records every resolution that reached a real status — a crossing (`ok`), a recorded pause
|
|
215
|
+
* (`ask`), or an abort (`abort`) — because "the run stopped here" is exactly the kind of fact this
|
|
216
|
+
* ledger exists to hold. Best-effort: a ledger write that could fail a gate crossing would cost more
|
|
217
|
+
* than the row is worth, matching {@link appendTrial}'s own trade-off.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} cwd - Project root.
|
|
220
|
+
* @param {string} slug - Feature slug.
|
|
221
|
+
* @param {object} row - The ledger row (gate, decision, source, note, round, status).
|
|
222
|
+
* @returns {boolean} True when the row was written.
|
|
223
|
+
*/
|
|
224
|
+
export function appendGateLedger(cwd, slug, row) {
|
|
225
|
+
try {
|
|
226
|
+
const p = gatesPath(cwd, slug);
|
|
227
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
228
|
+
appendFileSync(p, JSON.stringify(row) + "\n");
|
|
229
|
+
return true;
|
|
230
|
+
} catch { return false; }
|
|
231
|
+
}
|
|
232
|
+
|
|
208
233
|
/** Gates this lane will actually hit — used by --verify to catch a set that stalls halfway. */
|
|
209
234
|
export function requiredGates({ autoLevel = "unattended", tiny = false, qa = true } = {}) {
|
|
210
235
|
if (tiny) return ["L0", "L4"];
|
|
@@ -240,9 +265,9 @@ export function discover({ cwd = process.cwd(), file = null, preset = null, slug
|
|
|
240
265
|
|
|
241
266
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
242
267
|
export const ARGV_SPEC = {
|
|
243
|
-
usage: "
|
|
268
|
+
usage: "harness.mjs gate (--init | --list | --verify | --resolve <gate-id>) [--preset <name>] " +
|
|
244
269
|
"[--file <path>] [--slug <slug>] [--cwd <dir>] [--out <path>] [--by <who>] " +
|
|
245
|
-
"[--auto-level <level>] [--tiny] [--no-qa]",
|
|
270
|
+
"[--auto-level <level>] [--tiny] [--no-qa] [--round <n>]",
|
|
246
271
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
247
272
|
cwd: { type: "path" },
|
|
248
273
|
init: { type: "flag" },
|
|
@@ -257,6 +282,10 @@ export const ARGV_SPEC = {
|
|
|
257
282
|
"auto-level": { type: "str" },
|
|
258
283
|
tiny: { type: "flag" },
|
|
259
284
|
"no-qa": { type: "flag" },
|
|
285
|
+
// The current BUILD round, when the caller has one — L2/L3 are crossed once per round, so the
|
|
286
|
+
// ledger row needs it to key a `GateDecision` node uniquely per crossing. Round-independent gates
|
|
287
|
+
// (L0, L1a, …) simply omit it and the row carries `round: null`.
|
|
288
|
+
round: { type: "int", min: 1 },
|
|
260
289
|
};
|
|
261
290
|
|
|
262
291
|
function out(obj, code = 0) {
|
|
@@ -268,8 +297,15 @@ function die(msg, code = 2) {
|
|
|
268
297
|
process.exit(code);
|
|
269
298
|
}
|
|
270
299
|
|
|
271
|
-
|
|
272
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Resolve, verify, list or initialise the gate answer set.
|
|
302
|
+
*
|
|
303
|
+
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
304
|
+
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
305
|
+
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
306
|
+
*/
|
|
307
|
+
export function cli(rawArgv) {
|
|
308
|
+
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
273
309
|
const cwd = args.cwd || process.cwd();
|
|
274
310
|
|
|
275
311
|
if (args.init) {
|
|
@@ -328,11 +364,18 @@ export function main() {
|
|
|
328
364
|
|
|
329
365
|
const r = resolve(found.set, gate, found.source);
|
|
330
366
|
if (r.status === "error") die(r.reason);
|
|
367
|
+
// A gate with no `--slug` (e.g. `--file` used ad hoc, outside any run) has nowhere to file a
|
|
368
|
+
// per-run ledger row — the same reasoning `resolveRunId` uses for "no run is active": absence is
|
|
369
|
+
// the correct answer, not an error, so the write is skipped rather than guessing a location.
|
|
370
|
+
if (args.slug) {
|
|
371
|
+
appendGateLedger(cwd, args.slug, {
|
|
372
|
+
gate: r.gate, status: r.status, decision: r.decision ?? null,
|
|
373
|
+
source: r.source ?? found.source, note: r.note ?? r.reason ?? null,
|
|
374
|
+
round: args.round ?? null,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
331
377
|
if (r.status === "ask") out({ ...r, ok: false }, 4);
|
|
332
378
|
if (r.status === "abort") out({ ...r, ok: false }, 5);
|
|
333
379
|
out({ ...r, ok: true }, 0);
|
|
334
380
|
}
|
|
335
381
|
|
|
336
|
-
if (isMain(import.meta.url)) {
|
|
337
|
-
main();
|
|
338
|
-
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// harness — the kernel's single entry point.
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS. Everything in the pipeline that must be DETERMINISTIC — measured rather than
|
|
5
|
+
// claimed, single-writer rather than negotiated, an answer file rather than a vibe — is a
|
|
6
|
+
// subcommand of this one script:
|
|
7
|
+
//
|
|
8
|
+
// node ${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs <verb> [<action>] [flags]
|
|
9
|
+
//
|
|
10
|
+
// WHY ONE ENTRY POINT. A permission rule is matched against a command string. With one executable
|
|
11
|
+
// the entire grant is two lines a person can read (`bin/lib/grant.mjs`); with twenty it is forty
|
|
12
|
+
// rules that must be regenerated on every add, rename or removal — and a rule that silently
|
|
13
|
+
// matches nothing is indistinguishable from a rule that works until the first dispatch fails.
|
|
14
|
+
// Subcommands are free; entry points are not.
|
|
15
|
+
//
|
|
16
|
+
// WHO CALLS IT. Worker skills, from their own shells, and the hooks. The control plane — the
|
|
17
|
+
// Workflow script — calls no shell at all: it branches on the structured return of an `agent()`
|
|
18
|
+
// and never parses stdout.
|
|
19
|
+
//
|
|
20
|
+
// THE VERBS, and the invariant each one exists to keep:
|
|
21
|
+
//
|
|
22
|
+
// verify t0 · budget · envelope · Measured, not claimed. A model verifying itself is
|
|
23
|
+
// trace · spec · skills · claiming; these read artifacts and re-hash them.
|
|
24
|
+
// dispatch `skills` reads the roster off disk; `dispatch` reads
|
|
25
|
+
// the hook layer's evidence that a skill really resolved
|
|
26
|
+
// in this session — the half a file check cannot answer.
|
|
27
|
+
// reduce ingest · hill · snapshot · Single writer. Shared state has exactly one author.
|
|
28
|
+
// ship · board · verdict · graph
|
|
29
|
+
// gate An answer file with a source, not a vibe.
|
|
30
|
+
// probe resume · t0 · stats · digest · Read-only queries over run state. `concurrency`
|
|
31
|
+
// concurrency · leg · eval answers how many legs ran at once and what the
|
|
32
|
+
// fan-out bought, and refuses a figure the record set
|
|
33
|
+
// cannot support rather than printing a plausible one.
|
|
34
|
+
// `leg` answers whether a scope's work reached the
|
|
35
|
+
// board — a green T0 says its fixtures passed, not that
|
|
36
|
+
// its result was ever applied, and a leg that skipped
|
|
37
|
+
// its own ingest step is indistinguishable from one that
|
|
38
|
+
// did not until something reads the writer's own record.
|
|
39
|
+
// `eval` answers what an EVAL round's WorkResult
|
|
40
|
+
// actually said, mechanically — the round-loop branch
|
|
41
|
+
// reads this instead of trusting a dispatching agent's
|
|
42
|
+
// own end-of-turn summary of its own verdict.
|
|
43
|
+
// init run · fit Opens a run, or refuses it (exit 3).
|
|
44
|
+
// report export Projects the run's records as fact tables.
|
|
45
|
+
// compile The WorkOrder: schema-valid or nothing is dispatched.
|
|
46
|
+
//
|
|
47
|
+
// EXIT CODES are the subcommand's own and are part of its contract: 0 success · 1 ran, answer is
|
|
48
|
+
// no · 2 malformed input, nothing ran (`lib/argv.mjs`) · 3 `init run` refused to open · 4/5 gate
|
|
49
|
+
// stop/abort · 6 budget tripped. This dispatcher adds exactly one of its own: 2 for an unknown
|
|
50
|
+
// verb, which is the same "the input was malformed" case.
|
|
51
|
+
|
|
52
|
+
import { isMain } from "./lib/argv.mjs";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* verb → action → module, the whole routing table.
|
|
56
|
+
*
|
|
57
|
+
* A verb whose value is a string takes no action word (`gate`, `compile`); a verb whose value is an
|
|
58
|
+
* object requires one, and `_default` names the action taken when it is omitted.
|
|
59
|
+
*
|
|
60
|
+
* Exported so the structural suite enumerates the subcommands from THIS table rather than from a
|
|
61
|
+
* hand-kept list beside it — a second copy of the routing is a second thing to forget.
|
|
62
|
+
*/
|
|
63
|
+
export const ROUTES = {
|
|
64
|
+
verify: {
|
|
65
|
+
t0: "./verify/t0.mjs", budget: "./verify/budget.mjs", envelope: "./verify/envelope.mjs",
|
|
66
|
+
trace: "./verify/trace.mjs", spec: "./verify/spec.mjs", skills: "./verify/skills.mjs",
|
|
67
|
+
dispatch: "./verify/dispatch.mjs",
|
|
68
|
+
},
|
|
69
|
+
reduce: {
|
|
70
|
+
ingest: "./reduce/ingest.mjs", hill: "./reduce/hill.mjs", snapshot: "./reduce/snapshot.mjs",
|
|
71
|
+
ship: "./reduce/ship.mjs", board: "./reduce/board.mjs", verdict: "./reduce/verdict.mjs",
|
|
72
|
+
graph: "./reduce/graph.mjs",
|
|
73
|
+
},
|
|
74
|
+
probe: {
|
|
75
|
+
resume: "./probe/resume.mjs", t0: "./probe/t0.mjs", stats: "./probe/stats.mjs",
|
|
76
|
+
digest: "./probe/digest.mjs", concurrency: "./probe/concurrency.mjs",
|
|
77
|
+
leg: "./probe/leg.mjs", eval: "./probe/eval.mjs",
|
|
78
|
+
},
|
|
79
|
+
init: { run: "./init/run.mjs", fit: "./init/fit.mjs" },
|
|
80
|
+
report: { export: "./report/export.mjs", _default: "export" },
|
|
81
|
+
gate: "./gate.mjs",
|
|
82
|
+
compile: "./compile.mjs",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Every `<verb> <action>` pair, for the usage text and the unknown-verb rejection. */
|
|
86
|
+
function usage() {
|
|
87
|
+
const lines = [];
|
|
88
|
+
for (const [verb, target] of Object.entries(ROUTES)) {
|
|
89
|
+
if (typeof target === "string") lines.push(` ${verb}`);
|
|
90
|
+
else lines.push(` ${verb} ${Object.keys(target).filter((a) => a !== "_default").join(" | ")}`);
|
|
91
|
+
}
|
|
92
|
+
return `usage: harness.mjs <verb> [<action>] [flags]\n${lines.join("\n")}\n\nEvery subcommand accepts --help through its own argv spec.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reject an invocation before anything runs, the same way `lib/argv.mjs` rejects a bad flag.
|
|
97
|
+
* @param {object} detail - Machine-readable rejection record.
|
|
98
|
+
* @returns {never} Exits 2.
|
|
99
|
+
*/
|
|
100
|
+
function reject(detail) {
|
|
101
|
+
process.stderr.write(JSON.stringify(detail) + "\n");
|
|
102
|
+
process.stderr.write(usage() + "\n");
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve `<verb> [<action>]` to a module path and the argv the subcommand should see.
|
|
108
|
+
*
|
|
109
|
+
* @param {string[]} argv - `process.argv.slice(2)`.
|
|
110
|
+
* @returns {{module: string, rest: string[]}} The module to import and its own arguments.
|
|
111
|
+
*/
|
|
112
|
+
export function route(argv) {
|
|
113
|
+
const [verb, ...rest] = argv;
|
|
114
|
+
if (!verb || verb === "--help" || verb === "-h") { process.stdout.write(usage() + "\n"); process.exit(0); }
|
|
115
|
+
const target = ROUTES[verb];
|
|
116
|
+
if (!target) reject({ error: "unknown_verb", verb, expected: Object.keys(ROUTES).join(" | ") });
|
|
117
|
+
if (typeof target === "string") return { module: target, rest };
|
|
118
|
+
|
|
119
|
+
const action = rest[0] && !rest[0].startsWith("-") ? rest[0] : target._default;
|
|
120
|
+
const module = action ? target[action] : null;
|
|
121
|
+
if (!module) {
|
|
122
|
+
reject({
|
|
123
|
+
error: action ? "unknown_action" : "missing_action", verb, action: action ?? null,
|
|
124
|
+
expected: Object.keys(target).filter((a) => a !== "_default").join(" | "),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { module, rest: rest[0] === action ? rest.slice(1) : rest };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isMain(import.meta.url)) {
|
|
131
|
+
const { module, rest } = route(process.argv.slice(2));
|
|
132
|
+
const mod = await import(module);
|
|
133
|
+
await mod.cli(rest);
|
|
134
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GATE L0.3 — FIT CHECK. Which lane does this change belong in? Decided by measurement.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (and it is the root cause the other three fixes did not touch).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
11
|
//
|
|
12
12
|
// Every one of those fixes made the FAILURE better. None of them made the RUN finish, because
|
|
13
13
|
// none addressed why a three-file change was consuming half an hour: **the ceremony was not
|
|
14
14
|
// sized to the change.**
|
|
15
15
|
//
|
|
16
|
-
// The harness already knew. In
|
|
16
|
+
// The harness already knew. In those transcripts tech-lead identified the change at GATE L0 as
|
|
17
17
|
// "about as small as they come… squarely inside the --tiny lane" — and then ran the full pipeline
|
|
18
18
|
// anyway, because the lane was a judgment the model was free to talk itself out of. That is the
|
|
19
19
|
// same class of defect as narration and as prose consent: an invariant living somewhere a model
|
|
@@ -34,17 +34,16 @@
|
|
|
34
34
|
// symmetric, and the tie goes to the gates.
|
|
35
35
|
//
|
|
36
36
|
// USAGE
|
|
37
|
-
// node fit
|
|
38
|
-
// node fit
|
|
37
|
+
// node `harness init fit` --intake-file <path> [--cwd <root>] [--json]
|
|
38
|
+
// node `harness init fit` --intake-text "<requirement>" [--cwd <root>]
|
|
39
39
|
//
|
|
40
40
|
// Exit 0 always — this informs a decision, it does not deny a tool call. The orchestrator reads
|
|
41
|
-
// `lane` and acts on it;
|
|
41
|
+
// `lane` and acts on it; ``harness init run`` records it in the receipt.
|
|
42
42
|
|
|
43
43
|
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
44
44
|
import { join } from "node:path";
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import { runArgs } from "./lib/argv.mjs";
|
|
45
|
+
import { LOCAL } from "../lib/paths.mjs";
|
|
46
|
+
import { runArgs } from "../lib/argv.mjs";
|
|
48
47
|
|
|
49
48
|
/** Directories that are never part of "the tree being changed". */
|
|
50
49
|
const IGNORE_DIRS = new Set([".git", "node_modules", LOCAL, "dist", "build", ".next", "coverage", ".claude"]);
|
|
@@ -121,7 +120,7 @@ export function decideLane({ intake, files }) {
|
|
|
121
120
|
};
|
|
122
121
|
|
|
123
122
|
// CONJUNCTIVE, and it must be. The first version asked "is there evidence this is BIG?" and
|
|
124
|
-
// defaulted to tiny when it found none — which classified
|
|
123
|
+
// defaulted to tiny when it found none — which classified every trial feature as tiny,
|
|
125
124
|
// including the five-seam one that genuinely needs the pipeline and used two evaluation rounds
|
|
126
125
|
// to pass. A router that confident and that wrong is worse than no router: it would skip review
|
|
127
126
|
// on exactly the change that needed it.
|
|
@@ -152,7 +151,7 @@ export function decideLane({ intake, files }) {
|
|
|
152
151
|
// Say it on every invocation rather than burying it in a doc. These thresholds are calibrated
|
|
153
152
|
// against THREE features. That is enough to stop the router being obviously wrong and nowhere
|
|
154
153
|
// near enough to trust it silently, so it recommends and never denies.
|
|
155
|
-
calibration: "advisory — thresholds
|
|
154
|
+
calibration: "advisory — thresholds are coarse and `full` is the default; the PO may override with --lane",
|
|
156
155
|
};
|
|
157
156
|
}
|
|
158
157
|
|
|
@@ -160,15 +159,22 @@ export function decideLane({ intake, files }) {
|
|
|
160
159
|
|
|
161
160
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
162
161
|
export const ARGV_SPEC = {
|
|
163
|
-
usage: '
|
|
162
|
+
usage: 'harness.mjs init fit (--intake-file <path> | --intake-text "<requirement>") [--cwd <dir>]',
|
|
164
163
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
165
164
|
cwd: { type: "path" },
|
|
166
165
|
"intake-text": { type: "str" },
|
|
167
166
|
"intake-file": { type: "path" },
|
|
168
167
|
};
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
|
|
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);
|
|
172
178
|
const cwd = args.cwd || process.cwd();
|
|
173
179
|
let intake = args.intakeText ?? null;
|
|
174
180
|
const f = args.intakeFile ?? null;
|
|
@@ -186,11 +192,8 @@ export function main() {
|
|
|
186
192
|
...result,
|
|
187
193
|
// The sentence the orchestrator must act on, so the decision is not re-derived from the JSON.
|
|
188
194
|
action: result.lane === "tiny"
|
|
189
|
-
? "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
|
|
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."
|
|
190
196
|
: "FULL LANE. Run the complete pipeline: ORIENT → WIRE → MAP SCOPES → BUILD → EVAL → QA → GATE H → L4.",
|
|
191
197
|
}, null, 2));
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
if (isMain(import.meta.url)) {
|
|
195
|
-
main();
|
|
196
|
-
}
|