shapeup-sdlc 1.7.0 → 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 +17 -3
- package/README.md +124 -103
- package/SECURITY.md +37 -30
- package/bin/init.mjs +77 -47
- package/bin/lib/grant.mjs +145 -0
- package/commands/build.md +7 -6
- package/commands/ship.md +29 -16
- package/commands/wire.md +1 -1
- package/hooks/dispatch-receipt.mjs +195 -0
- package/hooks/gate-intake.mjs +3 -3
- package/hooks/gate-zerowork.mjs +25 -24
- package/hooks/hooks.json +9 -48
- package/hooks/lib/decision.mjs +19 -8
- package/hooks/safety-spine.mjs +3 -3
- package/hooks/sandbox-guard.mjs +84 -40
- package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
- package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
- package/kernel/harness.mjs +134 -0
- package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
- package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
- package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
- package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
- 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} +9 -10
- package/kernel/probe/eval.mjs +77 -0
- package/kernel/probe/leg.mjs +125 -0
- package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
- package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
- package/kernel/probe/t0.mjs +66 -0
- package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
- package/kernel/reduce/graph.mjs +437 -0
- package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
- 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} +61 -13
- package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
- package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
- 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} +15 -12
- 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} +134 -24
- package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +11 -9
- package/skills/ba-pitch-analyzer/SKILL.md +5 -5
- 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 +2 -0
- package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
- package/skills/qa-edge-hunter/SKILL.md +7 -3
- package/skills/scope-architect/SKILL.md +40 -6
- package/skills/solution-architect/SKILL.md +19 -4
- package/skills/spec-evaluator/SKILL.md +12 -2
- package/skills/task-executor/SKILL.md +7 -6
- package/skills/tech-lead/SKILL.md +50 -38
- package/skills/tech-lead/references/gates.md +69 -20
- package/skills/tech-lead/references/protocol.md +832 -0
- package/skills/tech-lead/schemas/domain.schema.json +341 -66
- package/skills/tech-lead/schemas/work-order.schema.json +11 -2
- package/skills/tech-lead/schemas/work-result.schema.json +1 -1
- package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
- package/hooks/anti-rationalization.mjs +0 -238
- package/hooks/compact-snapshot.mjs +0 -47
- package/hooks/gate-deadline.mjs +0 -147
- package/hooks/gate-l2.mjs +0 -161
- package/hooks/session-rehydrate.mjs +0 -108
- 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 -261
- package/skills/tech-lead/references/hard-rules.md +0 -34
- package/skills/tech-lead/references/invocation.md +0 -45
- package/skills/tech-lead/references/ledger-schema.md +0 -213
- package/skills/tech-lead/references/round-protocol.md +0 -234
- package/skills/tech-lead/references/state-model.md +0 -66
- package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
- package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
- package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
- package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
- package/skills/translator/README.md +0 -66
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Usage:
|
|
15
15
|
// Scope attempt (isolated attempt loop):
|
|
16
|
-
// node
|
|
16
|
+
// node kernel/harness.mjs compile --scope shapeup/<slug>/scopes/<id>.md \
|
|
17
17
|
// --round N --attempt M [--cwd <dir>] [--test-cmd "<cmd>"]
|
|
18
18
|
// Single task (no scope contracts — pre-v0.3.0 boards):
|
|
19
|
-
// node
|
|
20
|
-
// node
|
|
19
|
+
// node kernel/harness.mjs compile --task TASK-003 --slug <slug> [--worker task-executor]
|
|
20
|
+
// node kernel/harness.mjs compile --next --slug <slug>
|
|
21
21
|
// Non-build operation (planner/judge/QA dispatches; flag surface → operation + whitelist):
|
|
22
|
-
// node
|
|
22
|
+
// node kernel/harness.mjs compile --operation reconcile --slug <slug> [--round N]
|
|
23
23
|
// [--worker ba-pitch-analyzer] [--payload '<json>']
|
|
24
24
|
//
|
|
25
25
|
// Output: .shapeup/<slug>/orders/<order-file>.json (schema-validated before write; a
|
|
@@ -28,21 +28,25 @@
|
|
|
28
28
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
|
|
29
29
|
import { resolve, join, dirname, basename } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
|
-
import { validate } from "./
|
|
32
|
-
import { readTrials } from "./t0
|
|
33
|
-
import { isMain } from "./lib/is-main.mjs";
|
|
31
|
+
import { validate } from "./verify/envelope.mjs";
|
|
32
|
+
import { readTrials } from "./verify/t0.mjs";
|
|
34
33
|
import { runArgs } from "./lib/argv.mjs";
|
|
34
|
+
import { readRunId } from "./lib/paths.mjs";
|
|
35
35
|
// `specDir` is aliased: this module has a local `let specDir` holding the resolved, possibly
|
|
36
36
|
// --spec-overridden directory, and the import is the convention-derived default.
|
|
37
37
|
import {
|
|
38
38
|
tasksDir, specDir as defaultSpecDir, roundLedger, trials, verdictsDir, ordersDir,
|
|
39
|
-
relShared, globLocal, globShared, relKnowledgeBase,
|
|
39
|
+
relShared, globLocal, globShared, relKnowledgeBase, resultsDir, scopesDir,
|
|
40
40
|
} from "./lib/paths.mjs";
|
|
41
|
-
import { readContract, SCOPE_CONTRACT } from "./lib/contract
|
|
42
|
-
import { writeActiveOrder } from "./resume
|
|
41
|
+
import { readContract, tasksForScope, SCOPE_CONTRACT } from "./lib/contract.mjs";
|
|
42
|
+
import { writeActiveOrder } from "./probe/resume.mjs";
|
|
43
|
+
// The SAME matcher the sandbox hook enforces with. "Is this cited file inside this scope's
|
|
44
|
+
// substrate" has to mean exactly what the guard means, or a bug is addressed to a scope that is
|
|
45
|
+
// then denied the write that fixes it.
|
|
46
|
+
import { matchesAny } from "../hooks/sandbox-guard.mjs";
|
|
43
47
|
|
|
44
48
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "
|
|
49
|
+
const ORDER_SCHEMA = JSON.parse(readFileSync(resolve(HERE, "./../skills/tech-lead/schemas/work-order.schema.json"), "utf8"));
|
|
46
50
|
|
|
47
51
|
// --- tiny frontmatter reader (scalar keys + [a, b] inline lists) --------------------------
|
|
48
52
|
/**
|
|
@@ -72,7 +76,7 @@ export function frontmatter(md) {
|
|
|
72
76
|
* Parse one TASK-NNN.md file into the task entry a WorkOrder carries.
|
|
73
77
|
* @param {string} path - Absolute path to the task Markdown file.
|
|
74
78
|
* @returns {{id:string, title:string, status:string, priority:number, depends_on:string[],
|
|
75
|
-
* use_case_refs:string[], body_path:string, acceptance_criteria:Array<string|{text:string,
|
|
79
|
+
* use_case_refs:string[], scope_id:string, body_path:string, acceptance_criteria:Array<string|{text:string,
|
|
76
80
|
* covers:string[]}>}} The task entry. Each acceptance criterion is the checkbox text
|
|
77
81
|
* byte-identical (so ingest can tick it back); a trailing `(covers: REQ-…)` clause yields
|
|
78
82
|
* {text, covers} instead of a plain string. `priority` defaults to 999 when unset.
|
|
@@ -99,6 +103,7 @@ export function parseTaskFile(path) {
|
|
|
99
103
|
priority: Number(fm.priority) || 999,
|
|
100
104
|
depends_on: Array.isArray(fm.depends_on) ? fm.depends_on : [],
|
|
101
105
|
use_case_refs: Array.isArray(fm.use_case_refs) ? fm.use_case_refs : [],
|
|
106
|
+
scope_id: fm.scope_id || "",
|
|
102
107
|
body_path: path,
|
|
103
108
|
acceptance_criteria,
|
|
104
109
|
};
|
|
@@ -144,6 +149,25 @@ export function ledgerDecisions(ledgerText, scopeId) {
|
|
|
144
149
|
return decisions;
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Operation → owning worker (mirrors `domain.schema.json#/$defs/Operation` ownership). Lets a
|
|
154
|
+
* non-build dispatch resolve its worker from the operation alone, without a redundant `--worker`.
|
|
155
|
+
*
|
|
156
|
+
* Exported and at module scope so it is ONE table. Every value here must be a member of
|
|
157
|
+
* `WorkerName`: an operation mapped to a name the enum does not carry compiles an order that fails
|
|
158
|
+
* its own schema, and the dispatch is then denied by the order gate for a reason that names the
|
|
159
|
+
* envelope rather than the typo. That parity is checked by the suite, which can only read a table
|
|
160
|
+
* it can import.
|
|
161
|
+
*/
|
|
162
|
+
export const OP_OWNER = {
|
|
163
|
+
analyze: "ba-pitch-analyzer", reconcile: "ba-pitch-analyzer",
|
|
164
|
+
"retrofit-surface": "ba-pitch-analyzer", coverage: "ba-pitch-analyzer",
|
|
165
|
+
"map-scopes": "scope-architect",
|
|
166
|
+
wire: "solution-architect", evaluate: "spec-evaluator", orient: "orient",
|
|
167
|
+
hunt: "qa-edge-hunter", translate: "translator",
|
|
168
|
+
hammer: "scope-hammer", coach: "coach",
|
|
169
|
+
};
|
|
170
|
+
|
|
147
171
|
/**
|
|
148
172
|
* Resolve the write-contract (sandbox substrate) for an operation — one whitelist template per
|
|
149
173
|
* operation, so mode/flag differences are enforced by the sandbox hook reading the order's substrate, not trusted to prose.
|
|
@@ -281,7 +305,7 @@ export function selectTrialHistory(trials, { scopeId, round } = {}) {
|
|
|
281
305
|
}
|
|
282
306
|
|
|
283
307
|
/**
|
|
284
|
-
* The stagnation term of the inner circuit breaker
|
|
308
|
+
* The stagnation term of the inner circuit breaker.
|
|
285
309
|
*
|
|
286
310
|
* `attempt_budget` counts attempts; it cannot see that the last two produced nothing. One term
|
|
287
311
|
* joins it: `no_progress_k` consecutive non-`kept` trials ends the scope early and queues the
|
|
@@ -302,6 +326,162 @@ export function stagnation(trials, k = 2) {
|
|
|
302
326
|
return { stagnant: streak >= k && k > 0, streak, k };
|
|
303
327
|
}
|
|
304
328
|
|
|
329
|
+
// --- the FAIL verdict's bugs, carried into the round that must fix them ----------------------
|
|
330
|
+
//
|
|
331
|
+
// WHY THE KERNEL OWNS THIS AND NOT THE ORCHESTRATOR (measured twice, two different ways).
|
|
332
|
+
//
|
|
333
|
+
// `WorkOrderPayload.bugs` is defined as "the EVAL report's bug entries for this task — touch
|
|
334
|
+
// nothing else", and AGENTS.md states the regression rule as "bugs + full Test Surface of touched
|
|
335
|
+
// UC". Nothing ever populated it. The orchestrator's first fix threaded the evaluator's findings
|
|
336
|
+
// from memory into the build leg's `payload`, and it could not work, for two independent reasons:
|
|
337
|
+
//
|
|
338
|
+
// 1. A BUILD ORDER TAKES NO PAYLOAD FROM THE CALLER. Build legs override the compile line
|
|
339
|
+
// (`compile --scope … --round … --attempt …`) because an order is addressed by scope, round
|
|
340
|
+
// and attempt; only the generic fallback line serialises `--payload`. So the payload object
|
|
341
|
+
// the orchestrator built for a build leg was constructed, filtered, and dropped on the floor.
|
|
342
|
+
// 2. IT LIVED ONLY IN MEMORY. Round r's verdict is what round r+1 must act on, and a run killed
|
|
343
|
+
// after EVAL and relaunched — the normal case here — starts round r+1 in a fresh process with
|
|
344
|
+
// an empty variable. The verdict was on disk the whole time.
|
|
345
|
+
//
|
|
346
|
+
// Both vanish if the evidence is read where the order is written: the same read on a fresh round
|
|
347
|
+
// and a resumed one, no process boundary to survive, and no model in the path — an LLM courier
|
|
348
|
+
// transcribing a repro string is a repro string that no longer reproduces.
|
|
349
|
+
//
|
|
350
|
+
// The shape is also RICHER than the in-memory channel ever was: the ledgered verdict carries
|
|
351
|
+
// severity, file:line, repro, expected and actual per bug, where the structured return carried
|
|
352
|
+
// only `{id, criterion, evidence}`.
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The previous round's cited defects, if that round returned FAIL.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} cwd - Project root.
|
|
358
|
+
* @param {string} slug - Feature slug.
|
|
359
|
+
* @param {number} [round] - The round being compiled; round 1 has no predecessor.
|
|
360
|
+
* @returns {Array<object>} Bug entries, minus any the adversarial check overturned. Empty for a
|
|
361
|
+
* first round, a PASS, or an absent/unreadable result — a fix round with nothing to fix is a
|
|
362
|
+
* normal build round, never an error.
|
|
363
|
+
*/
|
|
364
|
+
export function verdictBugs(cwd, slug, round) {
|
|
365
|
+
if (!round || round < 2) return [];
|
|
366
|
+
const p = join(resultsDir(cwd, slug), `evaluate-r${round - 1}.json`);
|
|
367
|
+
if (!existsSync(p)) return [];
|
|
368
|
+
let v;
|
|
369
|
+
try { v = JSON.parse(readFileSync(p, "utf8"))?.verdict; } catch { return []; }
|
|
370
|
+
if (v?.overall !== "FAIL" || !Array.isArray(v.bugs)) return [];
|
|
371
|
+
// A refuted acceptance criterion is one the judge withdrew. Re-dispatching it would send a
|
|
372
|
+
// worker to "fix" behaviour that was found correct.
|
|
373
|
+
const refuted = new Set(
|
|
374
|
+
(Array.isArray(v.refuted) ? v.refuted : [])
|
|
375
|
+
.flatMap((r) => [r?.id, r?.ac_id, r?.criterion, typeof r === "string" ? r : null])
|
|
376
|
+
.filter(Boolean).map(String),
|
|
377
|
+
);
|
|
378
|
+
return v.bugs.filter((b) => !refuted.has(String(b?.id)) && !refuted.has(String(b?.criterion)));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Every repo-relative file a bug is cited against.
|
|
383
|
+
*
|
|
384
|
+
* A LOCATOR NAMES MORE THAN ONE SITE, ROUTINELY. The judge writes what it found, and what it finds
|
|
385
|
+
* is often the same defect at several lines, sometimes across files:
|
|
386
|
+
* `"bin/todo.js:53, bin/todo.js:85"`, or `"lib/parse-index.js:5, :13, :21 (rendered by
|
|
387
|
+
* bin/todo.js:66)"`. A parser that accepts only a lone `file:line` returns nothing for those, and
|
|
388
|
+
* "nothing" routes the bug to every scope as unowned — the safe direction, but it throws away an
|
|
389
|
+
* address the judge did supply. On the measured round-1 verdict that was two of five bugs.
|
|
390
|
+
*
|
|
391
|
+
* @param {object} bug - One bug entry.
|
|
392
|
+
* @returns {string[]} Distinct paths, `:line` suffixes and `./` prefixes stripped, in first-seen
|
|
393
|
+
* order. Empty when the entry carries no locator this can read.
|
|
394
|
+
*/
|
|
395
|
+
export function bugLocations(bug) {
|
|
396
|
+
const raw = String(bug?.location ?? bug?.file ?? "").trim();
|
|
397
|
+
// A path-looking token: at least one dot-extension, and no whitespace. The extension is what
|
|
398
|
+
// keeps prose out ("exit 1", "Node 20") without needing to know the project's layout.
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const m of raw.matchAll(/(?:^|[\s(,[])([\w@][\w./-]*\.[A-Za-z][A-Za-z0-9]{0,5})(?::\d+)?/g)) {
|
|
401
|
+
const p = m[1].replace(/^\.\//, "");
|
|
402
|
+
if (!out.includes(p)) out.push(p);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Elect the ONE scope that should fix a defect cited against a given file.
|
|
409
|
+
*
|
|
410
|
+
* OWNERSHIP IS BY SUBSTRATE, because that is what the sandbox enforces: a scope is exactly the set
|
|
411
|
+
* of files its worker may write, so a scope whose substrate excludes the cited line cannot fix it
|
|
412
|
+
* however well it understands the bug.
|
|
413
|
+
*
|
|
414
|
+
* BUT A MATCH IS NOT AN ELECTION. An entry point is routinely SHARED — on the measured run
|
|
415
|
+
* `bin/todo.js` sits in five scopes' substrate at once — so "address it to every scope that
|
|
416
|
+
* matches" hands the same one-line fix to five workers building concurrently against one file.
|
|
417
|
+
* That is a write race the harness sets up itself, and four of the five fixes are waste even when
|
|
418
|
+
* it resolves. So: prefer a scope that owns the file EXCLUSIVELY (allowed, not shared), and among
|
|
419
|
+
* equals take the lowest scope id — a rule that needs no coordination to agree with itself, since
|
|
420
|
+
* each leg compiles its own order in its own process.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} path - Repo-relative file the bug cites.
|
|
423
|
+
* @param {Array<{scope_id:string, allowed:string[], shared:string[]}>} scopes - Every scope.
|
|
424
|
+
* @returns {string|null} The elected scope id, or null when no scope may write that file.
|
|
425
|
+
*/
|
|
426
|
+
export function electOwner(path, scopes) {
|
|
427
|
+
const can = (scopes || []).filter((s) => matchesAny(path, s.allowed));
|
|
428
|
+
if (!can.length) return null;
|
|
429
|
+
const exclusive = can.filter((s) => !matchesAny(path, s.shared || []));
|
|
430
|
+
return (exclusive.length ? exclusive : can).map((s) => s.scope_id).sort()[0];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Address each bug to the scope that must fix it.
|
|
435
|
+
*
|
|
436
|
+
* AN UNOWNED BUG GOES TO EVERYONE, MARKED. A cited defect matching no scope's substrate — or
|
|
437
|
+
* carrying no locator this can read — has no owner, and dropping it silently is precisely how a
|
|
438
|
+
* judged defect survives a fix round to reappear in the next verdict. Better one scope reads a bug
|
|
439
|
+
* it turns out not to own than the run forgets a defect it already paid to find.
|
|
440
|
+
*
|
|
441
|
+
* @param {Array<object>} bugs - The previous verdict's bugs (see {@link verdictBugs}).
|
|
442
|
+
* @param {string|undefined} scopeId - The scope being compiled for.
|
|
443
|
+
* @param {Array<{scope_id:string, allowed:string[], shared:string[]}>} scopes - Every scope.
|
|
444
|
+
* @returns {Array<object>} The subset this order should carry; unowned entries get `unowned: true`.
|
|
445
|
+
*/
|
|
446
|
+
export function bugsForScope(bugs, scopeId, scopes) {
|
|
447
|
+
const out = [];
|
|
448
|
+
for (const b of bugs || []) {
|
|
449
|
+
if (b?.scope_id) {
|
|
450
|
+
if (b.scope_id === scopeId) out.push(b);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const owners = bugLocations(b).map((p) => electOwner(p, scopes)).filter(Boolean);
|
|
454
|
+
if (owners.includes(scopeId)) out.push(b);
|
|
455
|
+
else if (!owners.length) out.push({ ...b, unowned: true });
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Every scope's write substrate, for the election in {@link electOwner}.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} cwd - Project root.
|
|
464
|
+
* @param {string} slug - Feature slug.
|
|
465
|
+
* @returns {Array<{scope_id:string, allowed:string[], shared:string[]}>} One entry per readable
|
|
466
|
+
* contract; `[]` when the scopes directory is absent or unreadable.
|
|
467
|
+
*/
|
|
468
|
+
export function scopeSubstrates(cwd, slug) {
|
|
469
|
+
const dir = scopesDir(cwd, slug);
|
|
470
|
+
let files;
|
|
471
|
+
try { files = readdirSync(dir).filter((f) => f.endsWith(".md") || f.endsWith(".json")); } catch { return []; }
|
|
472
|
+
const out = [];
|
|
473
|
+
for (const f of files) {
|
|
474
|
+
const c = readContract(join(dir, f), SCOPE_CONTRACT)?.contract;
|
|
475
|
+
if (!c?.scope_id || !Array.isArray(c.allowed_file_substrate)) continue;
|
|
476
|
+
out.push({
|
|
477
|
+
scope_id: c.scope_id,
|
|
478
|
+
allowed: c.allowed_file_substrate,
|
|
479
|
+
shared: Array.isArray(c.shared_substrate) ? c.shared_substrate : [],
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
305
485
|
/**
|
|
306
486
|
* Assemble a WorkOrder envelope. Pure given its inputs — the CLI wrapper does the disk reads.
|
|
307
487
|
* @param {object} opts - The order inputs (destructured):
|
|
@@ -316,27 +496,32 @@ export function stagnation(trials, k = 2) {
|
|
|
316
496
|
* @param {Array<{id:string,answer:string}>} [opts.decisions] - This scope's advisor answers.
|
|
317
497
|
* @param {Array<object>} [opts.digestedErrors] - Prior-attempt AEGIS triples (payload.digested_errors).
|
|
318
498
|
* @param {Array<object>} [opts.trialHistory] - Compacted trial rows (payload.trial_history).
|
|
499
|
+
* @param {Array<object>} [opts.bugs] - The previous round's cited defects (payload.bugs).
|
|
319
500
|
* @param {string} [opts.testCmd] - Verify command, recorded under payload.verify.test_cmd.
|
|
320
501
|
* @param {object} [opts.payloadExtra] - Extra payload fields merged last (spec_folder, feature, …).
|
|
321
502
|
* @param {string} [opts.specDir] - Spec directory, threaded into the substrate template.
|
|
322
503
|
* @param {object} [opts.interaction] - Interaction flags (e.g. {pause_gates}).
|
|
323
|
-
* @
|
|
324
|
-
*
|
|
325
|
-
*
|
|
504
|
+
* @param {string} [opts.runId] - The run key (`mintRunId` in `lib/paths.mjs`); omitted when no receipt is readable.
|
|
505
|
+
* @param {string} [opts.compiledAt] - ISO compile time; omitted rather than invented.
|
|
506
|
+
* @returns {object} A WorkOrder: {schema_version, order_id ("<slug>/<suffix>"), run_id?,
|
|
507
|
+
* compiled_at?, worker, mode, operation?, interaction?, substrate (from {@link substrateFor}),
|
|
508
|
+
* payload{…}}. A coachable worker also gets payload.kb_rules_path. Not validated here — the CLI
|
|
509
|
+
* validates before writing.
|
|
326
510
|
*/
|
|
327
511
|
export function compileOrder({
|
|
328
512
|
slug, worker, mode = "orchestrated", operation, round, attempt,
|
|
329
|
-
scope, tasks, decisions, digestedErrors, trialHistory, testCmd, payloadExtra, specDir, interaction,
|
|
513
|
+
scope, tasks, decisions, digestedErrors, trialHistory, bugs, testCmd, payloadExtra, specDir, interaction,
|
|
514
|
+
runId, compiledAt,
|
|
330
515
|
}) {
|
|
331
516
|
// A build order's id carries its SCOPE. Without it, `r<round>-a<attempt>` is the same name for
|
|
332
517
|
// every scope in a round, so scope 2's order file overwrites scope 1's the moment it is compiled
|
|
333
518
|
// — measured on the kill/resume probe, where it made `orders/ minus results/` read EMPTY on a run
|
|
334
519
|
// that was re-dispatching a completed phase. The contract row watching that property therefore
|
|
335
|
-
// passed on the exact failure it exists to catch.
|
|
520
|
+
// passed on the exact failure it exists to catch. ``harness verify t0``'s verdict artifacts have always
|
|
336
521
|
// been self-identifying this way (`r<R>-a<A>-t<T>.json`, `wx`-created); orders now match, so
|
|
337
522
|
// `orders/` is an audit trail of dispatches rather than a rolling buffer of the last one.
|
|
338
523
|
//
|
|
339
|
-
// The shape stays `<slug>/<suffix>`: every consumer splits on the FIRST "/" (ingest
|
|
524
|
+
// The shape stays `<slug>/<suffix>`: every consumer splits on the FIRST "/" (`harness reduce ingest`
|
|
340
525
|
// reads [0] as the slug and [1] as the file stem), and a scope id is already filename-safe
|
|
341
526
|
// because it is a scope contract's own basename.
|
|
342
527
|
// `scope` arrives as the PARSED contract object (the CLI reads the .md and embeds it), so the id
|
|
@@ -349,10 +534,36 @@ export function compileOrder({
|
|
|
349
534
|
: (scope ? String(scope).split("/").pop().replace(/\.(md|json)$/, "") : null);
|
|
350
535
|
const scopeId = String(rawScopeId || "").toLowerCase().replace(/[^a-z0-9.-]/g, "-").replace(/^[^a-z0-9]+/, "");
|
|
351
536
|
const buildSuffix = round && attempt ? (scopeId ? `${scopeId}-r${round}-a${attempt}` : `r${round}-a${attempt}`) : null;
|
|
352
|
-
|
|
537
|
+
// A non-BUILD order was suffixed by operation+round alone, with no per-leg discriminator — so two
|
|
538
|
+
// concurrent non-BUILD legs of the SAME operation and round, dispatched for different scopes
|
|
539
|
+
// (e.g. two scopes both running `evaluate` in the same round), compiled to the identical suffix
|
|
540
|
+
// and the second write clobbered the first order file on disk while the run read green. `scopeId`
|
|
541
|
+
// is already derived above for any operation, not only BUILD; folding it in here — only when a
|
|
542
|
+
// scope was actually passed — closes that collision. Operation-level dispatches (orient, analyze,
|
|
543
|
+
// wire, map-scopes, evaluate, hunt, hammer) never pass a scope, so `scopeId` is empty for them and
|
|
544
|
+
// this reduces to the unchanged `${operation}-r${round}` / `${operation}` shape.
|
|
545
|
+
const scopedRoundSuffix = scopeId
|
|
546
|
+
? (round ? `${operation}-${scopeId}-r${round}` : `${operation}-${scopeId}`)
|
|
547
|
+
: (round ? `${operation}-r${round}` : operation);
|
|
548
|
+
const suffix = buildSuffix || scopedRoundSuffix;
|
|
353
549
|
const order = {
|
|
354
550
|
schema_version: 1,
|
|
355
551
|
order_id: `${slug}/${suffix}`,
|
|
552
|
+
// THE TWO ANALYTIC FIELDS, and why they are on the ORDER rather than the result.
|
|
553
|
+
//
|
|
554
|
+
// `order_id` identifies a dispatch within a run and repeats across runs of the same slug, so
|
|
555
|
+
// the dispatch record needed a run key to be groupable at all. It is stamped here because this
|
|
556
|
+
// is the one place every lane passes through — the same reason the active-order pointer is
|
|
557
|
+
// published here — so the workflow lane, `--tiny`, the prose round loop and a standalone
|
|
558
|
+
// dispatch all carry it without four separate stamps to keep in step.
|
|
559
|
+
//
|
|
560
|
+
// The RESULT deliberately gets neither. It is written by the worker, and a field a worker has
|
|
561
|
+
// to remember to copy is a field that goes missing under exactly the conditions you most want
|
|
562
|
+
// the record: the run that went wrong. Results join to orders on `order_id`, which they already
|
|
563
|
+
// echo and `validate-envelope` already checks — so the key reaches the result leg through a
|
|
564
|
+
// join that is enforced, instead of through a worker's cooperation.
|
|
565
|
+
...(runId ? { run_id: runId } : {}),
|
|
566
|
+
...(compiledAt ? { compiled_at: compiledAt } : {}),
|
|
356
567
|
worker,
|
|
357
568
|
mode,
|
|
358
569
|
...(operation ? { operation } : {}),
|
|
@@ -364,6 +575,9 @@ export function compileOrder({
|
|
|
364
575
|
...(decisions?.length ? { decisions } : {}),
|
|
365
576
|
...(digestedErrors?.length ? { digested_errors: digestedErrors } : {}),
|
|
366
577
|
...(trialHistory?.length ? { trial_history: trialHistory } : {}),
|
|
578
|
+
// Before `payloadExtra`, so an explicit `--payload '{"bugs":…}'` still wins: an operator
|
|
579
|
+
// hand-addressing a defect outranks the derivation.
|
|
580
|
+
...(bugs?.length ? { bugs } : {}),
|
|
367
581
|
...(testCmd ? { verify: { test_cmd: testCmd, env: [] } } : {}),
|
|
368
582
|
...(payloadExtra || {}),
|
|
369
583
|
},
|
|
@@ -376,7 +590,7 @@ export function compileOrder({
|
|
|
376
590
|
// ---------------------------------------------------------------------------
|
|
377
591
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
378
592
|
export const ARGV_SPEC = {
|
|
379
|
-
usage: "
|
|
593
|
+
usage: "harness.mjs compile (--scope <contract.json> --round N --attempt M | --task TASK-NNN --slug <slug> " +
|
|
380
594
|
"| --next --slug <slug> | --operation <op> --slug <slug>) [--worker <skill>] [--payload '<json>'] " +
|
|
381
595
|
"[--spec <dir>] [--test-cmd \"<cmd>\"] [--cwd <dir>] [--pause-gates]",
|
|
382
596
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
@@ -395,9 +609,15 @@ export const ARGV_SPEC = {
|
|
|
395
609
|
"pause-gates": { type: "flag" },
|
|
396
610
|
};
|
|
397
611
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Compile a WorkOrder for one dispatch and publish the substrate pointer that fences it.
|
|
614
|
+
*
|
|
615
|
+
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
616
|
+
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
617
|
+
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
618
|
+
*/
|
|
619
|
+
export async function cli(rawArgv) {
|
|
620
|
+
const argv = runArgs(ARGV_SPEC, rawArgv);
|
|
401
621
|
/**
|
|
402
622
|
* Read a parsed flag's value, normalising "absent" to null (the shape the body below expects).
|
|
403
623
|
* @param {string} name - Flag name without leading dashes.
|
|
@@ -433,17 +653,26 @@ if (isMainModule) {
|
|
|
433
653
|
|
|
434
654
|
const round = flag("round");
|
|
435
655
|
const attempt = flag("attempt");
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
656
|
+
|
|
657
|
+
// The fix round's inbound evidence. Derived here, from the ledgered verdict, for every lane —
|
|
658
|
+
// the workflow, `--tiny`, the prose round loop and a standalone `/build` all compile through
|
|
659
|
+
// this line, and none of them can pass a payload to a build order (see the banner above).
|
|
660
|
+
const bugs = scope ? bugsForScope(verdictBugs(cwd, slug, round), scope.scope_id, scopeSubstrates(cwd, slug)) : [];
|
|
661
|
+
|
|
662
|
+
// A ROUND CARRYING CITED DEFECTS IS A `fix`, AND THE ORDER HAS TO SAY SO.
|
|
663
|
+
//
|
|
664
|
+
// `payload.bugs` is the only field task-executor's input contract binds to a specific operation:
|
|
665
|
+
// "`fix` (only the bugs in `payload.bugs` — touch nothing else)". Dispatching a fix round as
|
|
666
|
+
// `execute` hands the worker a field its own contract associates with a different operation,
|
|
667
|
+
// against a Zero-memory rule that says to treat anything not in the order as unknown. Delivering
|
|
668
|
+
// evidence the receiving contract does not claim is half a delivery.
|
|
669
|
+
//
|
|
670
|
+
// Free of blast radius by construction: `substrateFor` returns the same whitelist for
|
|
671
|
+
// execute/fix/spike, so the sandbox contract is byte-identical, and the order id is addressed by
|
|
672
|
+
// scope/round/attempt rather than by operation, so no filename moves. An explicit `--operation`
|
|
673
|
+
// still wins — an operator naming the operation outranks the derivation.
|
|
674
|
+
let operation = flag("operation")
|
|
675
|
+
|| (scopePath || flag("task") || has("next") ? (bugs.length ? "fix" : "execute") : null);
|
|
447
676
|
const worker = flag("worker")
|
|
448
677
|
|| (scopePath || flag("task") || has("next") ? "task-executor" : null)
|
|
449
678
|
|| (operation ? OP_OWNER[operation] : null);
|
|
@@ -460,10 +689,18 @@ if (isMainModule) {
|
|
|
460
689
|
tasks = board.filter((t) => t.status === "ready" && t.depends_on.every((d) => doneIds.has(d))).slice(0, 1);
|
|
461
690
|
if (!tasks.length) { console.error("compile-order: no ready task with satisfied dependencies"); process.exit(2); }
|
|
462
691
|
} else if (scope) {
|
|
463
|
-
// Scope attempt: the
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
692
|
+
// Scope attempt: the board tasks anchored to this scope's use cases, else every not-done task
|
|
693
|
+
// on the board (the attempt loop owns sequencing, not the worker).
|
|
694
|
+
//
|
|
695
|
+
// The contract names USE CASES, never task ids: it is committed and the board is not, so a
|
|
696
|
+
// contract holding `TASK-004` pointed into a gitignored per-machine tier and resolved to
|
|
697
|
+
// nothing on a fresh clone — silently, because an empty filter and an absent board are the
|
|
698
|
+
// same empty array. A scope that anchors no use case still falls back to the whole open board
|
|
699
|
+
// rather than dispatching nothing; spec-lint's SCOPE-ANCHOR is what reports the missing anchor.
|
|
700
|
+
const anchored = tasksForScope(board, scope);
|
|
701
|
+
tasks = anchored.length || (scope.use_cases || []).length
|
|
702
|
+
? anchored
|
|
703
|
+
: board.filter((t) => t.status !== "done");
|
|
467
704
|
}
|
|
468
705
|
|
|
469
706
|
// Ledger decisions for this scope.
|
|
@@ -474,7 +711,7 @@ if (isMainModule) {
|
|
|
474
711
|
|
|
475
712
|
// inspect() — the attempt loop's history (see selectTrialHistory above).
|
|
476
713
|
//
|
|
477
|
-
// NON-REGRESSION
|
|
714
|
+
// NON-REGRESSION. With no `trials.jsonl` on disk this falls back to exactly the pre-ratchet
|
|
478
715
|
// read — the previous attempt's verdict artifact, AEGIS triples only, byte-for-byte the same
|
|
479
716
|
// order. Every new arm is skipped when its artifact is absent, per the ✦/✚ convention.
|
|
480
717
|
let digestedErrors = [];
|
|
@@ -508,9 +745,14 @@ if (isMainModule) {
|
|
|
508
745
|
if (!payloadExtra.feature) payloadExtra.feature = slug;
|
|
509
746
|
|
|
510
747
|
const order = compileOrder({
|
|
511
|
-
slug, worker, operation, round, attempt, scope, tasks, decisions, digestedErrors, trialHistory,
|
|
748
|
+
slug, worker, operation, round, attempt, scope, tasks, decisions, digestedErrors, trialHistory, bugs,
|
|
512
749
|
testCmd: flag("test-cmd"), payloadExtra, specDir,
|
|
513
750
|
interaction: has("pause-gates") ? { pause_gates: true } : { pause_gates: false },
|
|
751
|
+
// Read off the receipt rather than passed in: a standalone `compile-order` invocation gets the
|
|
752
|
+
// same key as one the workflow drove, and a dispatch in a workspace with no open run simply
|
|
753
|
+
// carries no key instead of failing — the analytic field must never be able to block a build.
|
|
754
|
+
runId: readRunId(cwd, slug),
|
|
755
|
+
compiledAt: new Date().toISOString(),
|
|
514
756
|
});
|
|
515
757
|
|
|
516
758
|
const { valid, errors } = validate(order, ORDER_SCHEMA);
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
// pre-recorded answer") rather than a silent wait that gets reported as a slow harness.
|
|
33
33
|
//
|
|
34
34
|
// USAGE
|
|
35
|
-
// node gate
|
|
36
|
-
// node gate
|
|
37
|
-
// node gate
|
|
38
|
-
// 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]
|
|
39
39
|
//
|
|
40
40
|
// RESOLUTION ORDER for --resolve / --list / --verify, first hit wins:
|
|
41
41
|
// 1. --file <path>
|
|
@@ -50,11 +50,10 @@
|
|
|
50
50
|
// 5 no answer and on_missing=abort → the run aborts here, attributably
|
|
51
51
|
// 2 usage / validation error
|
|
52
52
|
|
|
53
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
53
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
54
54
|
import { join, dirname } from "node:path";
|
|
55
|
-
import { isMain } from "./lib/is-main.mjs";
|
|
56
55
|
import { runArgs } from "./lib/argv.mjs";
|
|
57
|
-
import { gateAnswerCandidates, LOCAL } from "./lib/paths.mjs";
|
|
56
|
+
import { gateAnswerCandidates, gates as gatesPath, LOCAL } from "./lib/paths.mjs";
|
|
58
57
|
|
|
59
58
|
export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
|
|
60
59
|
|
|
@@ -204,6 +203,33 @@ export function resolve(set, gate, source) {
|
|
|
204
203
|
};
|
|
205
204
|
}
|
|
206
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
|
+
|
|
207
233
|
/** Gates this lane will actually hit — used by --verify to catch a set that stalls halfway. */
|
|
208
234
|
export function requiredGates({ autoLevel = "unattended", tiny = false, qa = true } = {}) {
|
|
209
235
|
if (tiny) return ["L0", "L4"];
|
|
@@ -239,9 +265,9 @@ export function discover({ cwd = process.cwd(), file = null, preset = null, slug
|
|
|
239
265
|
|
|
240
266
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
241
267
|
export const ARGV_SPEC = {
|
|
242
|
-
usage: "
|
|
268
|
+
usage: "harness.mjs gate (--init | --list | --verify | --resolve <gate-id>) [--preset <name>] " +
|
|
243
269
|
"[--file <path>] [--slug <slug>] [--cwd <dir>] [--out <path>] [--by <who>] " +
|
|
244
|
-
"[--auto-level <level>] [--tiny] [--no-qa]",
|
|
270
|
+
"[--auto-level <level>] [--tiny] [--no-qa] [--round <n>]",
|
|
245
271
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
246
272
|
cwd: { type: "path" },
|
|
247
273
|
init: { type: "flag" },
|
|
@@ -256,6 +282,10 @@ export const ARGV_SPEC = {
|
|
|
256
282
|
"auto-level": { type: "str" },
|
|
257
283
|
tiny: { type: "flag" },
|
|
258
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 },
|
|
259
289
|
};
|
|
260
290
|
|
|
261
291
|
function out(obj, code = 0) {
|
|
@@ -267,8 +297,15 @@ function die(msg, code = 2) {
|
|
|
267
297
|
process.exit(code);
|
|
268
298
|
}
|
|
269
299
|
|
|
270
|
-
|
|
271
|
-
|
|
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);
|
|
272
309
|
const cwd = args.cwd || process.cwd();
|
|
273
310
|
|
|
274
311
|
if (args.init) {
|
|
@@ -327,11 +364,18 @@ export function main() {
|
|
|
327
364
|
|
|
328
365
|
const r = resolve(found.set, gate, found.source);
|
|
329
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
|
+
}
|
|
330
377
|
if (r.status === "ask") out({ ...r, ok: false }, 4);
|
|
331
378
|
if (r.status === "abort") out({ ...r, ok: false }, 5);
|
|
332
379
|
out({ ...r, ok: true }, 0);
|
|
333
380
|
}
|
|
334
381
|
|
|
335
|
-
if (isMain(import.meta.url)) {
|
|
336
|
-
main();
|
|
337
|
-
}
|