shapeup-sdlc 3.5.0 → 3.7.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-plugin/plugin.json +1 -1
- package/AGENTS.md +15 -4
- package/README.md +1 -1
- package/kernel/compile.mjs +55 -7
- package/kernel/harness.mjs +11 -4
- package/kernel/init/run-args.mjs +206 -0
- package/kernel/init/run.mjs +10 -0
- package/kernel/lib/paths.mjs +10 -0
- package/kernel/probe/attempts.mjs +135 -0
- package/kernel/probe/concurrency.mjs +31 -6
- package/kernel/probe/digest.mjs +15 -1
- package/kernel/probe/owner.mjs +4 -1
- package/kernel/probe/resume.mjs +314 -5
- package/kernel/probe/rounds.mjs +104 -0
- package/kernel/reduce/ingest.mjs +53 -12
- package/kernel/reduce/ship.mjs +15 -30
- package/kernel/reduce/snapshot.mjs +23 -2
- package/kernel/report/export.mjs +54 -2
- package/kernel/report/facts.mjs +24 -2
- package/{skills/tech-lead → kernel}/schemas/domain.schema.json +15 -12
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +1 -1
- package/package.json +1 -1
- package/skills/ba-pitch-analyzer/SKILL.md +1 -1
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +6 -0
- package/skills/coach/SKILL.md +8 -2
- package/skills/hill-chart/SKILL.md +3 -4
- package/skills/scope-hammer/SKILL.md +11 -3
- package/skills/tech-lead/SKILL.md +10 -10
- package/skills/tech-lead/references/gates.md +48 -11
- package/skills/tech-lead/references/protocol.md +4 -2
- package/skills/tech-lead/workflows/shapeup-run.js +164 -40
- package/skills/translator/SKILL.md +1 -1
- /package/{skills/tech-lead → kernel}/schemas/gate-answers.schema.json +0 -0
- /package/{skills/tech-lead → kernel}/schemas/work-order.schema.json +0 -0
- /package/{skills/tech-lead → kernel}/schemas/work-result.schema.json +0 -0
|
@@ -30,13 +30,16 @@
|
|
|
30
30
|
//
|
|
31
31
|
// Usage: node kernel/harness.mjs probe concurrency --slug <slug> [--cwd <dir>] [--run-root <dir>]
|
|
32
32
|
// [--round N] [--gap-s N] [--format json|table]
|
|
33
|
+
// [--require-run-args]
|
|
33
34
|
// Exit: 0 = a report was produced with at least one usable leg · 1 = ran, and no leg in scope had
|
|
34
35
|
// a usable interval (the report still prints, and says why) · 2 = malformed argv.
|
|
36
|
+
// With --require-run-args the report is skipped: 0 = run-args.json exists at this run root ·
|
|
37
|
+
// 6 = it does not (mirrors `probe resume --require`'s "artifact absent" convention).
|
|
35
38
|
|
|
36
39
|
import { existsSync, readFileSync } from "node:fs";
|
|
37
40
|
import { join, resolve } from "node:path";
|
|
38
41
|
import { runArgs } from "../lib/argv.mjs";
|
|
39
|
-
import { localRoot, runIdFromRoot, RECEIPT_FILE } from "../lib/paths.mjs";
|
|
42
|
+
import { localRoot, runIdFromRoot, RECEIPT_FILE, RUN_ARGS_FILE } from "../lib/paths.mjs";
|
|
40
43
|
|
|
41
44
|
/** The round-addressed order id forms `<scope>-r<N>-a<M>` and `<phase>-r<N>`. */
|
|
42
45
|
const ROUND_SUFFIX = /^(.*?)-r(\d+)(?:-a(\d+))?$/;
|
|
@@ -301,16 +304,18 @@ export function summarise(index, legs) {
|
|
|
301
304
|
/**
|
|
302
305
|
* Which fan-out width the run was launched with — read, never assumed.
|
|
303
306
|
*
|
|
304
|
-
* The dial is written into `run-args.json` by
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
+
* The dial is written into `run-args.json` by `harness init run-args` (GATE L0.9b), the kernel
|
|
308
|
+
* writer tech-lead invokes right before the launch. A run opened before that writer existed, or one
|
|
309
|
+
* whose launcher never passed `--parallel-scopes`, still has no file or no key — so the honest
|
|
310
|
+
* answer stays the effective default WITH the fact that it is a default: reporting `4` unqualified
|
|
311
|
+
* would assert an operator choice nobody made.
|
|
307
312
|
*
|
|
308
313
|
* @param {string} runRoot - The run's LOCAL root.
|
|
309
314
|
* @returns {{max_parallel_scopes:number, source:string}} The value and where it came from.
|
|
310
315
|
*/
|
|
311
316
|
export function dialFrom(runRoot) {
|
|
312
317
|
try {
|
|
313
|
-
const a = JSON.parse(readFileSync(join(runRoot,
|
|
318
|
+
const a = JSON.parse(readFileSync(join(runRoot, RUN_ARGS_FILE), "utf8"));
|
|
314
319
|
const n = Number(a?.maxParallelScopes);
|
|
315
320
|
if (Number.isFinite(n) && n >= 1) return { max_parallel_scopes: n, source: "run-args" };
|
|
316
321
|
return { max_parallel_scopes: DEFAULT_MAX_PARALLEL_SCOPES, source: "default (run-args.json declares none)" };
|
|
@@ -474,7 +479,7 @@ export function table(r) {
|
|
|
474
479
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
475
480
|
export const ARGV_SPEC = {
|
|
476
481
|
usage: "harness.mjs probe concurrency (--slug <slug> | --run-root <dir>) [--cwd <dir>] [--round N] " +
|
|
477
|
-
"[--gap-s N] [--format json|table]",
|
|
482
|
+
"[--gap-s N] [--format json|table] [--require-run-args]",
|
|
478
483
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
479
484
|
slug: { type: "str" },
|
|
480
485
|
// The archived-trace and post-export cases, and the same escape `verify t0 --out` has: a caller
|
|
@@ -484,6 +489,10 @@ export const ARGV_SPEC = {
|
|
|
484
489
|
round: { type: "int", min: 1 },
|
|
485
490
|
"gap-s": { type: "int", min: 1, default: DEFAULT_GAP_S },
|
|
486
491
|
format: { type: "enum", values: ["json", "table"], default: "json" },
|
|
492
|
+
// The positive enforcer for the launch record (see the banner below). Skips the concurrency
|
|
493
|
+
// report entirely — this is a presence check, not a measurement, and the two must not be
|
|
494
|
+
// confused by sharing an exit code.
|
|
495
|
+
"require-run-args": { type: "flag" },
|
|
487
496
|
};
|
|
488
497
|
|
|
489
498
|
/**
|
|
@@ -492,6 +501,14 @@ export const ARGV_SPEC = {
|
|
|
492
501
|
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
493
502
|
* @returns {void} Exits 0 when at least one leg had a usable interval, 1 when none did — and the
|
|
494
503
|
* report prints either way, because "nothing was measurable" is the answer, not an error.
|
|
504
|
+
*
|
|
505
|
+
* `--require-run-args` is a different question with its own exit convention, mirroring
|
|
506
|
+
* `probe resume --require`'s 0 (satisfied) / 6 (artifact absent): it never reaches the report at
|
|
507
|
+
* all. The launch record had exactly one reader (`dialFrom()`, below) and zero enforcers — a
|
|
508
|
+
* run missing it proceeded green with a silently substituted default, "indistinguishable from an
|
|
509
|
+
* operator choice" (`gates.md` L0.9b). This flag is what `shapeup-run.js` calls at Preflight, before
|
|
510
|
+
* ORIENT, so that state stops being invisible: this module already owns run-root resolution and the
|
|
511
|
+
* file's path, so the check is a few lines here rather than a new kernel entry point.
|
|
495
512
|
*/
|
|
496
513
|
export function cli(rawArgv) {
|
|
497
514
|
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
@@ -504,6 +521,14 @@ export function cli(rawArgv) {
|
|
|
504
521
|
? resolve(args.runRoot)
|
|
505
522
|
: localRoot(resolve(args.cwd || process.cwd()), args.slug);
|
|
506
523
|
|
|
524
|
+
if (args.requireRunArgs) {
|
|
525
|
+
const path = join(runRoot, RUN_ARGS_FILE);
|
|
526
|
+
const present = existsSync(path);
|
|
527
|
+
console.log(JSON.stringify({ ok: present, run_root: runRoot, run_args_path: path,
|
|
528
|
+
reason: present ? null : "no run-args.json at this run root — GATE L0.9b's launch record was never written before this launch" }));
|
|
529
|
+
process.exit(present ? 0 : 6);
|
|
530
|
+
}
|
|
531
|
+
|
|
507
532
|
const r = report(runRoot, { round: args.round ?? null, gapS: args.gapS });
|
|
508
533
|
console.log(args.format === "table" ? table(r) : JSON.stringify(r));
|
|
509
534
|
process.exit(r.launches.length ? 0 : 1);
|
package/kernel/probe/digest.mjs
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// Script-first by design: regex over known log formats is free (no model tokens); an
|
|
7
7
|
// unrecognized line becomes a "raw" triple (file/line unknown) rather than being silently
|
|
8
8
|
// dropped, so a Sonnet fallback (or a human) still has something to look at — this module never
|
|
9
|
-
// invents a file:line it didn't find in the text.
|
|
9
|
+
// invents a file:line it didn't find in the text. `file` and `line` are independent: a
|
|
10
|
+
// diagnostic that names a file but no line number (a resource-compiler error, for example)
|
|
11
|
+
// still yields its file — `line` stays null rather than being guessed at.
|
|
10
12
|
//
|
|
11
13
|
// Zero dependencies, zero network — same discipline as oracles/*.
|
|
12
14
|
|
|
@@ -19,6 +21,18 @@ const PATTERNS = [
|
|
|
19
21
|
{ re: /^(?:✗|not ok\b.*?)[^()]*\((.+?):(\d+)\)\s*$/, kind: "test-failure" },
|
|
20
22
|
// ESLint/tsc style: "path/to/file.ts:12:34 - error TS2345: message"
|
|
21
23
|
{ re: /^(.+?):(\d+):\d+\s*[-–]\s*(?:error|warning)\b.*$/, kind: "compiler-diagnostic" },
|
|
24
|
+
// File-level diagnostic with NO line number: "resource.xml: error: message" or
|
|
25
|
+
// "resource.xml - fatal error: message" (resource compilers and linkers report this way —
|
|
26
|
+
// the failure is the whole file, so there is no line to cite). The file must look like a
|
|
27
|
+
// path (ends in a dotted extension, no embedded whitespace/colon) so this stays anchored to
|
|
28
|
+
// real diagnostics rather than matching arbitrary prose that happens to contain "error:".
|
|
29
|
+
{ re: /^([^\s:]+?\.[A-Za-z0-9]{1,10})\s*[:\-–—]\s*(?:fatal\s+error|error|warning)\b.*$/i, kind: "compiler-diagnostic" },
|
|
30
|
+
// Bundler style: "ERROR in ./src/components/Foo.tsx" (webpack et al.) — file, no line. The
|
|
31
|
+
// captured token must look like a path — leads with "./"/"../", or ends in a dotted extension
|
|
32
|
+
// of 1-10 alnum chars (same anchor the sibling pattern above uses) — so prose after "ERROR in"
|
|
33
|
+
// ("ERROR in the build pipeline", "ERROR in test suite failed to run") is left unmatched
|
|
34
|
+
// instead of handing back a fabricated file.
|
|
35
|
+
{ re: /^(?:ERROR|WARNING)\s+in\s+(\.{1,2}\/[^\s:]*|[^\s:]+\.[A-Za-z0-9]{1,10})\b/i, kind: "compiler-diagnostic" },
|
|
22
36
|
// Generic "Error: message" line followed later by a stack — capture the message alone.
|
|
23
37
|
{ re: /^\s*(?:Error|TypeError|ReferenceError|AssertionError)\s*:\s*(.+)$/, kind: "error-message" },
|
|
24
38
|
];
|
package/kernel/probe/owner.mjs
CHANGED
|
@@ -46,8 +46,11 @@ import { matchesAny } from "../../hooks/sandbox-guard.mjs";
|
|
|
46
46
|
*/
|
|
47
47
|
export function ownership(path, scopes, cwd = null) {
|
|
48
48
|
const rel = String(path).replace(/^\.\//, "");
|
|
49
|
-
const writers = (scopes || []).filter((s) => matchesAny(rel, s.allowed)).map((s) => s.scope_id).sort();
|
|
50
49
|
const shared = (scopes || []).filter((s) => matchesAny(rel, s.shared || [])).map((s) => s.scope_id).sort();
|
|
50
|
+
// `writers` is admits-the-path, the same union the sandbox fence composes (allowed ++ shared) —
|
|
51
|
+
// a path declared only in a contract's `shared` list is still a scope this path may write, and
|
|
52
|
+
// must read as owned rather than UNOWNED. `shared_with` (below) stays the narrower subset.
|
|
53
|
+
const writers = (scopes || []).filter((s) => matchesAny(rel, s.allowed) || matchesAny(rel, s.shared || [])).map((s) => s.scope_id).sort();
|
|
51
54
|
return { path: rel, owner: electOwner(rel, scopes), writers, shared_with: shared, exists: cwd ? existsSync(join(cwd, rel)) : null };
|
|
52
55
|
}
|
|
53
56
|
|
package/kernel/probe/resume.mjs
CHANGED
|
@@ -59,18 +59,89 @@
|
|
|
59
59
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
60
60
|
import { dirname, join, resolve } from "node:path";
|
|
61
61
|
import { runArgs } from "../lib/argv.mjs";
|
|
62
|
-
import { splitFrontmatter } from "../lib/contract.mjs";
|
|
62
|
+
import { splitFrontmatter, uncoerce } from "../lib/contract.mjs";
|
|
63
63
|
import { globToRegExp } from "../verify/spec.mjs";
|
|
64
64
|
import {
|
|
65
65
|
intake, harnessRun, wiringMap, projectProfile, scopesDir, resultsDir, ordersDir,
|
|
66
66
|
orientDir, activeOrder, usecasesDir, breadboard, receipt, readReceipt, requirements,
|
|
67
|
+
exportRunDir,
|
|
67
68
|
} from "../lib/paths.mjs";
|
|
68
69
|
import { evalVerdict } from "./eval.mjs";
|
|
70
|
+
import { collectRun, writeRun } from "../report/export.mjs";
|
|
69
71
|
|
|
70
72
|
/** The run-state values `references/protocol.md` (Part 4 — State) defines. A typo'd status is a rejection,
|
|
71
73
|
* not a write — the whole point of this file is that a write nobody validates is a write nobody
|
|
72
|
-
* can trust.
|
|
73
|
-
|
|
74
|
+
* can trust. `aborted` is the terminal counterpart `escalated` already was: a gate
|
|
75
|
+
* resolving "abort", or a hard stop, ends the run the same way an operator-declared escalation
|
|
76
|
+
* does — neither resumes on relaunch — so both are TERMINAL_STATUSES below. */
|
|
77
|
+
export const RUN_STATUSES = ["orienting", "mapping", "building", "evaluating", "shipped", "escalated", "aborted"];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The statuses a run does not come back from. `closeRun` refuses every other member of
|
|
81
|
+
* {@link RUN_STATUSES} — `orienting`/`mapping`/`building`/`evaluating` are mid-flight, and writing
|
|
82
|
+
* a close over one of those would stamp `closed_at` on a run a relaunch is still meant to resume.
|
|
83
|
+
*/
|
|
84
|
+
export const TERMINAL_STATUSES = ["shipped", "aborted", "escalated"];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The RunReturn union (`kernel/schemas/domain.schema.json` `$defs/RunReturn.properties.status.enum`)
|
|
88
|
+
* mapped to what closing the run means for each arm — the derivation `closeIfTerminal`
|
|
89
|
+
* (`skills/tech-lead/workflows/shapeup-run.js`) now reads instead of a hand-typed
|
|
90
|
+
* `status !== "aborted" && status !== "shipped"` pair that referenced {@link TERMINAL_STATUSES}
|
|
91
|
+
* zero times and so could not see when a new arm went unhandled.
|
|
92
|
+
*
|
|
93
|
+
* A lookup answers one of three ways, and the distinction is load-bearing for what this map must
|
|
94
|
+
* catch: an arm ABSENT from this object (never listed as a key) returns `undefined` — an arm the
|
|
95
|
+
* schema carries that nobody has mapped, which a caller must treat as a defect, never as "fine to
|
|
96
|
+
* skip". An arm mapped to a terminal status closes the run as that status. An arm mapped to `null`
|
|
97
|
+
* is EXPLICITLY non-terminal — `paused` resumes on relaunch and `ok` is one inner round finishing,
|
|
98
|
+
* not the run — so it is a key with a falsy value, not an omission a reader could mistake for "not
|
|
99
|
+
* decided yet".
|
|
100
|
+
*
|
|
101
|
+
* `gate_h` → `escalated`: the breaker that tripped (`outer`/`inner`/`deadline`) travels in
|
|
102
|
+
* `close_cause`, never as a new member of {@link TERMINAL_STATUSES} or {@link RUN_STATUSES} — a
|
|
103
|
+
* circuit breaker tripping is not a new way a run ends, it is the reason an existing one
|
|
104
|
+
* (`escalated`) fires this time.
|
|
105
|
+
*
|
|
106
|
+
* @type {Object<string, (string|null)>}
|
|
107
|
+
*/
|
|
108
|
+
export const RUN_RETURN_CLOSE = {
|
|
109
|
+
shipped: "shipped",
|
|
110
|
+
aborted: "aborted",
|
|
111
|
+
gate_h: "escalated",
|
|
112
|
+
paused: null,
|
|
113
|
+
ok: null,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve one RunReturn arm to a close outcome and, when the arm is terminal, perform the close —
|
|
118
|
+
* the single call `closeIfTerminal` makes instead of deciding locally which arms are terminal.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} cwd - Project root.
|
|
121
|
+
* @param {string} slug - Feature slug.
|
|
122
|
+
* @param {string} arm - A `RunReturn.status` value.
|
|
123
|
+
* @param {(string|null)} [cause] - Why the run ended there; only used when `arm` is terminal.
|
|
124
|
+
* @param {boolean} [withExport] - Forwarded to {@link closeRun} — defaults true. The one caller
|
|
125
|
+
* that ever passes `false` is a test fixture proving the export assertion is real — a check that
|
|
126
|
+
* cannot fail did not happen; production call sites never set this.
|
|
127
|
+
* @returns {({ok:true, arm:string, terminal:false, reason:string} |
|
|
128
|
+
* {ok:false, arm:string, reason:string} |
|
|
129
|
+
* ({ok:boolean, arm:string, terminal:true} & ReturnType<typeof closeRun>))} `terminal:false` when
|
|
130
|
+
* `arm` maps to `null` (nothing closed, not an error). `ok:false` with no `terminal` field when
|
|
131
|
+
* `arm` is not a key of {@link RUN_RETURN_CLOSE} at all — a schema arm this map has not been
|
|
132
|
+
* taught, which must never be silently treated as non-terminal. Otherwise the {@link closeRun}
|
|
133
|
+
* outcome, tagged with the arm that produced it.
|
|
134
|
+
*/
|
|
135
|
+
export function closeArm(cwd, slug, arm, cause = null, withExport = true) {
|
|
136
|
+
if (!Object.hasOwn(RUN_RETURN_CLOSE, arm)) {
|
|
137
|
+
return { ok: false, arm, reason: `closeArm: "${arm}" is not a RunReturn arm this kernel maps — known arms: ${Object.keys(RUN_RETURN_CLOSE).join(", ")}` };
|
|
138
|
+
}
|
|
139
|
+
const status = RUN_RETURN_CLOSE[arm];
|
|
140
|
+
if (!status) {
|
|
141
|
+
return { ok: true, arm, terminal: false, reason: `"${arm}" is explicitly non-terminal — no close` };
|
|
142
|
+
}
|
|
143
|
+
return { ...closeRun(cwd, slug, { status, cause, withExport }), arm, terminal: true };
|
|
144
|
+
}
|
|
74
145
|
|
|
75
146
|
/** ORIENT's four artifacts (skills/orient/SKILL.md §Outputs): three by exact name, plus a spike
|
|
76
147
|
* whose filename carries the area it spiked (`spike-<area>.md`, or `spike-not-needed.md` when
|
|
@@ -462,6 +533,215 @@ export function setRunStatus(cwd, slug, status) {
|
|
|
462
533
|
return { ok: true, path: p, status };
|
|
463
534
|
}
|
|
464
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Rewrite `status:`, `closed_at:`, `closed_status:` and `close_cause:` together, in one pass,
|
|
538
|
+
* appending any of the last three that a pre-migration ledger carries no line for yet (the same
|
|
539
|
+
* tolerant-of-old-ledgers discipline `close_cause` itself shipped under).
|
|
540
|
+
*
|
|
541
|
+
* @param {string} body - The ledger's current text.
|
|
542
|
+
* @param {{status:string, closedAt:string, cause:(string|null)}} o - What to write. `cause` is
|
|
543
|
+
* already normalized prose (newlines collapsed, truncated) — this function only `uncoerce`s it.
|
|
544
|
+
* @returns {string} The rewritten text.
|
|
545
|
+
*/
|
|
546
|
+
function writeCloseLines(body, { status, closedAt, cause }) {
|
|
547
|
+
const causeLine = `close_cause: ${uncoerce(cause || null)}`;
|
|
548
|
+
const closedStatusLine = `closed_status: ${status}`;
|
|
549
|
+
let out = body
|
|
550
|
+
.replace(/^status:.*$/m, `status: ${status}`)
|
|
551
|
+
.replace(/^closed_at:.*$/m, `closed_at: ${closedAt}`);
|
|
552
|
+
out = /^closed_status:.*$/m.test(out)
|
|
553
|
+
? out.replace(/^closed_status:.*$/m, closedStatusLine)
|
|
554
|
+
// A ledger written before this field existed carries no line to replace — appended right after
|
|
555
|
+
// `closed_at:`, the one line every TERMINAL_STATUSES write also touches.
|
|
556
|
+
: out.replace(/^closed_at:.*$/m, (m) => `${m}\n${closedStatusLine}`);
|
|
557
|
+
out = /^close_cause:.*$/m.test(out)
|
|
558
|
+
? out.replace(/^close_cause:.*$/m, causeLine)
|
|
559
|
+
: out.replace(/^closed_at:.*$/m, (m) => `${m}\n${causeLine}`);
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Export a just-closed run's own records into fact tables, best-effort
|
|
565
|
+
* (`report export`, `kernel/report/export.mjs`). {@link closeRun} calls this for every terminal
|
|
566
|
+
* status except `"shipped"`: the Ship phase (`skills/tech-lead/workflows/shapeup-run.js`) already
|
|
567
|
+
* calls `report export` itself, several lines before this file's own close-out runs, and that call
|
|
568
|
+
* site is left untouched on purpose — the shipped path's export stays byte-comparable to what it
|
|
569
|
+
* wrote before Stage 2. `aborted`, `escalated` and `gate_h` (which closes as `escalated`, see
|
|
570
|
+
* {@link RUN_RETURN_CLOSE}) never reached that call site at all, because it sits inside a phase
|
|
571
|
+
* those endings never enter — so a run ending any of them left its whole trace (orders, results,
|
|
572
|
+
* T0 verdicts, hook decisions, `graph.jsonl`) in the gitignored LOCAL tier with nothing durable
|
|
573
|
+
* surviving the next `init run`'s wipe. This is the read that was missing for those endings, run at
|
|
574
|
+
* the one point every one of them passes through: the close itself.
|
|
575
|
+
*
|
|
576
|
+
* FAIL-OPEN, the hook discipline (CLAUDE.md) extended to a write that is not a hook: an export that
|
|
577
|
+
* cannot write — a blocked or missing exports directory, a full disk — must never turn a close that
|
|
578
|
+
* DID take into one that looks like it did not. The three facts {@link closeRun} just wrote
|
|
579
|
+
* (`closed_status`/`close_cause`/`closed_at`) are never touched by this function; a failure here is
|
|
580
|
+
* handed back to the caller as a warning string, never thrown.
|
|
581
|
+
*
|
|
582
|
+
* @param {string} cwd - Project root.
|
|
583
|
+
* @param {string} slug - Feature slug.
|
|
584
|
+
* @returns {(string|null)} A one-line warning when the export did not complete; `null` on success.
|
|
585
|
+
*/
|
|
586
|
+
function exportOnClose(cwd, slug) {
|
|
587
|
+
try {
|
|
588
|
+
const collected = collectRun(cwd, slug);
|
|
589
|
+
if (!collected) return `export on close: no readable receipt for "${slug}" — nothing to export`;
|
|
590
|
+
writeRun(collected, exportRunDir(cwd, collected.run_id ?? slug));
|
|
591
|
+
return null;
|
|
592
|
+
} catch (e) {
|
|
593
|
+
return `export on close: ${e.message}`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Close the run: a terminal status, its cause, and a close timestamp, written together in ONE
|
|
599
|
+
* pass.
|
|
600
|
+
*
|
|
601
|
+
* Measured: after an EVAL worker escalated and the run aborted, `harness-run.md` still read
|
|
602
|
+
* `status: evaluating`, `closed_at: ~`, with no cause recorded anywhere — a live EVAL and a dead
|
|
603
|
+
* one were indistinguishable from the trace alone. Two writers made that possible: `setRunStatus`
|
|
604
|
+
* above replaces the `status:` line and NOTHING ELSE, and `closed_at` was written exactly once, as
|
|
605
|
+
* the literal `~`, by `init run` — nothing ever replaced it. This function is the one call site
|
|
606
|
+
* that closes a run, so a terminal RunReturn cannot leave one of the three facts behind.
|
|
607
|
+
*
|
|
608
|
+
* Refuses rather than silently no-ops, the same discipline as `setRunStatus`: an absent ledger, one
|
|
609
|
+
* missing the lines this writes, or a non-terminal `status` (closing a run still `building` would
|
|
610
|
+
* stamp a live run as done) is a fact to act on, not a write to skip quietly.
|
|
611
|
+
*
|
|
612
|
+
* THE ONCE-ONLY GUARD READS `closed_status`, NEVER `status`. REWORK (round 2): the guard used to key
|
|
613
|
+
* on `before.status`, and `status:` is a LIVE field every phase rewrites via `setRunStatus` —
|
|
614
|
+
* including the product's own ship path, which stamps `status: shipped`
|
|
615
|
+
* (`skills/tech-lead/workflows/shapeup-run.js`'s Ship phase) immediately before this call runs. So a
|
|
616
|
+
* run closed `aborted` at Preflight on one launch, relaunched, and carried through to a `shipped`
|
|
617
|
+
* RunReturn on a later one had its `status:` line rewritten to `shipped` by that ordinary phase
|
|
618
|
+
* traffic BEFORE `closeIfTerminal` ever called this function — the guard read `before.status ===
|
|
619
|
+
* "shipped"`, matched the very close it was about to perform, and treated a run that was actually
|
|
620
|
+
* closed `aborted` as already closed `shipped`: `ok:true`, an "idempotent no-op" that silently kept
|
|
621
|
+
* the abort's own `closed_at`/`close_cause` under a `status:` line now reading `shipped`. `closed_status`
|
|
622
|
+
* is written ONLY here, exactly once per distinct close-writing call, so nothing between two calls to
|
|
623
|
+
* this function can move it — it is the one field that actually answers "has this run been closed,
|
|
624
|
+
* and to what" regardless of how many times `status:` has been rewritten since.
|
|
625
|
+
*
|
|
626
|
+
* WHAT A SECOND CLOSE MEANS, decided explicitly rather than left implicit. `run_id` is reused across
|
|
627
|
+
* relaunches by design (AGENTS.md), so "the run's close" and "this launch's own close" are two
|
|
628
|
+
* different facts a single `closed_at`/`close_cause` pair cannot both hold:
|
|
629
|
+
* - The IDENTICAL status and the IDENTICAL cause is the ordinary case a retried or duplicated call
|
|
630
|
+
* produces (the same `withWarnings` call, or a relaunch that re-executes an already-applied
|
|
631
|
+
* close) — a true no-op, `ok:true`, nothing rewritten.
|
|
632
|
+
* - The SAME terminal status but a DIFFERENT cause is a SECOND, real close — most often a later
|
|
633
|
+
* relaunch aborting again for its own reason, or shipping again after an earlier ship's close
|
|
634
|
+
* record was never superseded. Discarding it (the pre-rework behavior) silently drops the later
|
|
635
|
+
* launch's own reason with no trace of the loss. It is recorded instead: this call's cause
|
|
636
|
+
* becomes the ledger's `close_cause`, folded together with the prior cause it is superseding —
|
|
637
|
+
* the earlier fact survives inside the new line rather than the ledger simply losing it — and the
|
|
638
|
+
* return carries `superseded:true` so a caller (`closeIfTerminal`) can flag the trace as degraded
|
|
639
|
+
* rather than reporting a clean success.
|
|
640
|
+
* - A DIFFERENT terminal status altogether (aborted vs. shipped) is refused outright — flipping the
|
|
641
|
+
* actual OUTCOME of a run after the fact is not a fact a later launch gets to silently overwrite,
|
|
642
|
+
* so the original `closed_status`/`closed_at`/`close_cause` are left completely untouched and
|
|
643
|
+
* handed back to the caller.
|
|
644
|
+
*
|
|
645
|
+
* @param {string} cwd - Project root.
|
|
646
|
+
* @param {string} slug - Feature slug.
|
|
647
|
+
* @param {{status:string, cause:(string|null), withExport?:boolean}} o - The terminal status (one
|
|
648
|
+
* of {@link TERMINAL_STATUSES}) and why the run ended there. `cause` travels through `uncoerce`
|
|
649
|
+
* (the one dialect `harness-run.md`'s frontmatter is read and written in), so free prose — quotes
|
|
650
|
+
* and colons included — round-trips as one frontmatter line; an embedded newline is collapsed to
|
|
651
|
+
* a space first, because this dialect is line-based and could not carry one either way.
|
|
652
|
+
* `withExport` (default true) gates {@link exportOnClose} — every status except `"shipped"` runs
|
|
653
|
+
* it on a successful close; `false` exists only for a test fixture proving the export assertion
|
|
654
|
+
* is real, never for a production call site.
|
|
655
|
+
* @returns {{ok:boolean, path:string, status:string, closed_at?:string, cause?:(string|null),
|
|
656
|
+
* reason?:string, closed_status?:string, superseded?:boolean, decision?:string,
|
|
657
|
+
* prior_cause?:(string|null), prior_closed_at?:string, export_warning?:string}} Outcome. A refused
|
|
658
|
+
* overwrite (already closed with a DIFFERENT terminal status) carries
|
|
659
|
+
* `closed_status`/`closed_at`/`cause` naming what is actually on disk. A successful supersede
|
|
660
|
+
* (same status, different cause) carries `superseded:true`, `decision:"superseded"` (the
|
|
661
|
+
* one-token signal the courier boundary in `shapeup-run.js` relays verbatim — see its own
|
|
662
|
+
* `cmd()` banner) and the prior close it folded in. Any successful, non-`"shipped"` close carries
|
|
663
|
+
* `export_warning` when {@link exportOnClose} could not write the run's fact tables — the close
|
|
664
|
+
* itself still stands; this is advisory only.
|
|
665
|
+
*/
|
|
666
|
+
export function closeRun(cwd, slug, { status, cause = null, withExport = true } = {}) {
|
|
667
|
+
const p = harnessRun(cwd, slug);
|
|
668
|
+
if (!TERMINAL_STATUSES.includes(status)) {
|
|
669
|
+
return { ok: false, path: p, status, reason: `closeRun: "${status}" is not terminal — expected one of ${TERMINAL_STATUSES.join(" | ")}` };
|
|
670
|
+
}
|
|
671
|
+
if (!existsSync(p)) {
|
|
672
|
+
return { ok: false, path: p, status, reason: `no harness-run.md for slug "${slug}" — open the run with harness init run (GATE L0.1) before closing it` };
|
|
673
|
+
}
|
|
674
|
+
let body = readFileSync(p, "utf8");
|
|
675
|
+
if (!/^status:.*$/m.test(body) || !/^closed_at:.*$/m.test(body)) {
|
|
676
|
+
return { ok: false, path: p, status, reason: `harness-run.md carries no "status:"/"closed_at:" line to replace — the ledger's frontmatter is malformed (references/protocol.md)` };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// The Ship phase (`shapeup-run.js`) already exports a shipped run itself, before this call ever
|
|
680
|
+
// runs — Stage 2 adds the endings that wrote nothing, and leaves that path untouched.
|
|
681
|
+
const shouldExport = withExport && status !== "shipped";
|
|
682
|
+
const withExportWarning = (result) => {
|
|
683
|
+
if (!shouldExport) return result;
|
|
684
|
+
const warning = exportOnClose(cwd, slug);
|
|
685
|
+
return warning ? { ...result, export_warning: warning } : result;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// Truncated, not elided: a cause this long has already done its job in the run's own log — the
|
|
689
|
+
// ledger line is a pointer back to it, not the full transcript. Newlines are collapsed to spaces
|
|
690
|
+
// FIRST — this dialect is line-based, so a raw embedded newline would split one field into a value
|
|
691
|
+
// line and a stray, unparsed one.
|
|
692
|
+
const normCause = String(cause ?? "").replace(/\r?\n/g, " ").trim().slice(0, 4000) || null;
|
|
693
|
+
|
|
694
|
+
const before = parseFrontmatter(body);
|
|
695
|
+
const priorClosedStatus = before.closed_status && before.closed_status !== "~" ? before.closed_status : null;
|
|
696
|
+
const priorClosedAt = before.closed_at && before.closed_at !== "~" ? before.closed_at : null;
|
|
697
|
+
const priorCause = before.close_cause && before.close_cause !== "~" ? before.close_cause : null;
|
|
698
|
+
|
|
699
|
+
if (priorClosedStatus && priorClosedAt) {
|
|
700
|
+
if (priorClosedStatus === status && normCause === priorCause) {
|
|
701
|
+
// The identical fact, restated — a retried or duplicated call costs nothing.
|
|
702
|
+
return withExportWarning({ ok: true, path: p, status, closed_at: priorClosedAt, cause: priorCause, decision: "idempotent", reason: `already closed as "${status}" at ${priorClosedAt} — idempotent no-op` });
|
|
703
|
+
}
|
|
704
|
+
if (priorClosedStatus !== status) {
|
|
705
|
+
// A DIFFERENT terminal status over an already-closed run — refused outright, the original
|
|
706
|
+
// close left completely untouched so the caller can see what it was refused permission to
|
|
707
|
+
// destroy, rather than losing it silently.
|
|
708
|
+
return {
|
|
709
|
+
ok: false, path: p, status,
|
|
710
|
+
reason: `closeRun: this run is already closed as "${priorClosedStatus}" at ${priorClosedAt} (cause: ${JSON.stringify(priorCause)}) — refusing to overwrite it with "${status}". A terminal close is a once-only fact; the first cause is not destroyed.`,
|
|
711
|
+
closed_status: priorClosedStatus, closed_at: priorClosedAt, cause: priorCause,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
// SAME terminal status, a DIFFERENT cause — a second, real close (see the function banner's
|
|
715
|
+
// "what a second close means"). Superseded, not discarded: the prior cause is folded into the
|
|
716
|
+
// new line rather than lost, and the return says so explicitly.
|
|
717
|
+
const closedAt = new Date().toISOString();
|
|
718
|
+
const foldedCause = `${normCause || "no reason recorded"} — supersedes an earlier close recorded ${priorClosedAt} (cause: ${JSON.stringify(priorCause)})`.slice(0, 4000);
|
|
719
|
+
body = writeCloseLines(body, { status, closedAt, cause: foldedCause });
|
|
720
|
+
try { writeFileSync(p, body); } catch (e) {
|
|
721
|
+
return { ok: false, path: p, status, reason: `could not write the ledger: ${e.message}` };
|
|
722
|
+
}
|
|
723
|
+
const afterSup = parseFrontmatter(readFileSync(p, "utf8"));
|
|
724
|
+
if (afterSup.status !== status || !afterSup.closed_at || afterSup.closed_at === "~") {
|
|
725
|
+
return { ok: false, path: p, status, reason: `wrote the superseding close but the ledger reads back status="${afterSup.status}" closed_at="${afterSup.closed_at}" — the write did not take` };
|
|
726
|
+
}
|
|
727
|
+
return withExportWarning({
|
|
728
|
+
ok: true, path: p, status, closed_at: afterSup.closed_at, cause: afterSup.close_cause ?? null,
|
|
729
|
+
superseded: true, decision: "superseded", prior_cause: priorCause, prior_closed_at: priorClosedAt,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const closedAt = new Date().toISOString();
|
|
734
|
+
body = writeCloseLines(body, { status, closedAt, cause: normCause });
|
|
735
|
+
try { writeFileSync(p, body); } catch (e) {
|
|
736
|
+
return { ok: false, path: p, status, reason: `could not write the ledger: ${e.message}` };
|
|
737
|
+
}
|
|
738
|
+
const after = parseFrontmatter(readFileSync(p, "utf8"));
|
|
739
|
+
if (after.status !== status || !after.closed_at || after.closed_at === "~") {
|
|
740
|
+
return { ok: false, path: p, status, reason: `wrote the close but the ledger reads back status="${after.status}" closed_at="${after.closed_at}" — the write did not take` };
|
|
741
|
+
}
|
|
742
|
+
return withExportWarning({ ok: true, path: p, status, closed_at: after.closed_at, cause: after.close_cause ?? null, decision: "closed" });
|
|
743
|
+
}
|
|
744
|
+
|
|
465
745
|
/**
|
|
466
746
|
* Point the substrate pointer at the order about to be executed.
|
|
467
747
|
*
|
|
@@ -488,13 +768,21 @@ export function writeActiveOrder(cwd, slug, orderPath) {
|
|
|
488
768
|
|
|
489
769
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
490
770
|
export const ARGV_SPEC = {
|
|
491
|
-
usage: "harness.mjs probe resume --slug <slug> [--cwd <dir>]
|
|
771
|
+
usage: "harness.mjs probe resume --slug <slug> [--cwd <dir>] " +
|
|
772
|
+
"[--require <phase> | --set-status <status> | --set-active-order <path> | " +
|
|
773
|
+
"--close <status> | --close-arm <RunReturn.status> [--cause <text>]]",
|
|
492
774
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
493
775
|
slug: { type: "str", required: true },
|
|
494
776
|
cwd: { type: "path" },
|
|
495
777
|
require: { type: "enum", values: PHASES },
|
|
496
778
|
"set-status": { type: "enum", values: RUN_STATUSES },
|
|
497
779
|
"set-active-order": { type: "str" },
|
|
780
|
+
// The one call site that stamps a terminal status, its cause and closed_at together.
|
|
781
|
+
close: { type: "enum", values: TERMINAL_STATUSES },
|
|
782
|
+
// Not `--close`: the caller (shapeup-run.js's closeIfTerminal) hands over a RunReturn arm, never
|
|
783
|
+
// a status it decided was terminal itself — RUN_RETURN_CLOSE/closeArm above make that call.
|
|
784
|
+
"close-arm": { type: "str" },
|
|
785
|
+
cause: { type: "str" },
|
|
498
786
|
};
|
|
499
787
|
|
|
500
788
|
/**
|
|
@@ -508,11 +796,15 @@ export function cli(rawArgv) {
|
|
|
508
796
|
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
509
797
|
const cwd = args.cwd || process.cwd();
|
|
510
798
|
|
|
511
|
-
const ops = [args.require && "--require", args.setStatus && "--set-status", args.setActiveOrder && "--set-active-order"].filter(Boolean);
|
|
799
|
+
const ops = [args.require && "--require", args.setStatus && "--set-status", args.setActiveOrder && "--set-active-order", args.close && "--close", args.closeArm && "--close-arm"].filter(Boolean);
|
|
512
800
|
if (ops.length > 1) {
|
|
513
801
|
process.stderr.write(JSON.stringify({ error: "conflicting_flags", flags: ops, expected: "one operation per invocation" }) + "\n");
|
|
514
802
|
process.exit(2);
|
|
515
803
|
}
|
|
804
|
+
if (args.cause !== undefined && !args.close && !args.closeArm) {
|
|
805
|
+
process.stderr.write(JSON.stringify({ error: "conflicting_flags", flags: ["--cause"], expected: "--cause is only meaningful with --close or --close-arm" }) + "\n");
|
|
806
|
+
process.exit(2);
|
|
807
|
+
}
|
|
516
808
|
|
|
517
809
|
// The post-condition. It prints the SAME ResumeState the derivation prints — plus which phase was
|
|
518
810
|
// asked about and whether its artifact is there — so a caller that wants to act on the facts
|
|
@@ -541,6 +833,23 @@ export function cli(rawArgv) {
|
|
|
541
833
|
process.exit(r.ok ? 0 : 3);
|
|
542
834
|
}
|
|
543
835
|
|
|
836
|
+
if (args.close) {
|
|
837
|
+
const r = closeRun(cwd, args.slug, { status: args.close, cause: args.cause ?? null });
|
|
838
|
+
console.log(JSON.stringify(r));
|
|
839
|
+
process.exit(r.ok ? 0 : 3);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// The arm-derived close: the caller hands a RunReturn arm and this kernel decides — via
|
|
843
|
+
// RUN_RETURN_CLOSE/closeArm above — whether it is terminal and, if so, what status it closes as.
|
|
844
|
+
// Exit 0 for both a real close AND a correctly-declined non-terminal arm (`terminal:false`) —
|
|
845
|
+
// neither is an error the caller (shapeup-run.js's closeIfTerminal) should treat as failed; only
|
|
846
|
+
// an arm this map does not recognize at all, or a close `closeRun` itself refuses, exits non-zero.
|
|
847
|
+
if (args.closeArm) {
|
|
848
|
+
const r = closeArm(cwd, args.slug, args.closeArm, args.cause ?? null);
|
|
849
|
+
console.log(JSON.stringify(r));
|
|
850
|
+
process.exit(r.ok ? 0 : 3);
|
|
851
|
+
}
|
|
852
|
+
|
|
544
853
|
console.log(JSON.stringify(deriveResumeState(cwd, args.slug)));
|
|
545
854
|
process.exit(0);
|
|
546
855
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// rounds — how many BUILD/EVAL rounds a run actually reached, derived from disk.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS (measured, not theorized).
|
|
4
|
+
//
|
|
5
|
+
// `harness-run.md`'s `rounds_used` frontmatter is written ONCE, at GATE L0.1 (`init run`), as `0`,
|
|
6
|
+
// and nothing in the round loop ever rewrites it — the orchestrator's own RunReturn carries the
|
|
7
|
+
// real count, but that value lives in a JS variable a relaunch loses, and it never reached the
|
|
8
|
+
// ledger. `reduce ship`'s own derivation partly compensated by counting the highest
|
|
9
|
+
// `evaluate-r<N>.json` result on disk, which is right for "rounds the judge saw" and wrong for
|
|
10
|
+
// "rounds the run built": measured after two full BUILD rounds with neither reaching EVAL,
|
|
11
|
+
// `round_count: 0` and `rounds_used: 0` both — a run that did real work reported having done none.
|
|
12
|
+
//
|
|
13
|
+
// TWO NUMBERS, NOT ONE. A round can be built and die before EVAL ever sees it (a circuit breaker,
|
|
14
|
+
// a kill mid-round), so "rounds built" and "rounds judged" are different facts and collapsing them
|
|
15
|
+
// into a single field is exactly what made the fallback silently wrong. Both are returned here, and
|
|
16
|
+
// both are meant to survive to wherever a run's numbers are read — the ship report and the export.
|
|
17
|
+
//
|
|
18
|
+
// PURE. Reads the run's own trace, writes nothing — the same discipline as `reduce ship`'s other
|
|
19
|
+
// derivations (`t0Summary`, `boardCensus`, …). A run before any of these artifacts existed, or a
|
|
20
|
+
// lane with no round concept at all (`--tiny`), falls back to the caller-supplied ledger value,
|
|
21
|
+
// non-regression with the earlier, EVAL-only behaviour.
|
|
22
|
+
|
|
23
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { ordersDir, verdictsDir, roundBuildDir, resultsDir } from "../lib/paths.mjs";
|
|
26
|
+
|
|
27
|
+
/** Parse a JSON file, returning null rather than throwing — every reader here is best-effort. */
|
|
28
|
+
function readJson(p) {
|
|
29
|
+
try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The round a build-addressed order id encodes, e.g. `checkout/sc-01-r2-a1` → 2.
|
|
34
|
+
* @param {*} orderId - The order's own id, whatever shape it happens to be.
|
|
35
|
+
* @returns {(number|null)} The round, or null when the id carries none (`orient`, `wire`, `hammer`, …).
|
|
36
|
+
*/
|
|
37
|
+
export function orderRound(orderId) {
|
|
38
|
+
const suffix = String(orderId ?? "").split("/").slice(1).join("/");
|
|
39
|
+
const m = suffix.match(/-r(\d+)(?:-a\d+)?$/);
|
|
40
|
+
return m ? Number(m[1]) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const maxOf = (nums) => (nums.length ? Math.max(...nums) : null);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* How many rounds this run actually reached — built, and separately, judged.
|
|
47
|
+
*
|
|
48
|
+
* `rounds_used` is the highest round carrying ANY build evidence: a compiled order, a T0 verdict
|
|
49
|
+
* artifact, a round build-gate artifact, or an EVAL result — the brief's own list, plus EVAL results
|
|
50
|
+
* because a judged round is, by construction, a round that was also built. `rounds_judged` is the
|
|
51
|
+
* highest round EVAL actually returned a verdict for (`evaluate-r<N>.json` on disk), kept as its own
|
|
52
|
+
* field rather than folded into the only number a reader can see.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} cwd - Project root.
|
|
55
|
+
* @param {string} slug - Feature slug.
|
|
56
|
+
* @param {*} [fallback] - What to report for `rounds_used` when nothing on disk is derivable — the
|
|
57
|
+
* caller's own ledger value, passed through untouched (some callers hand this on already coerced
|
|
58
|
+
* to a number, some as the raw frontmatter string; this function does not care which).
|
|
59
|
+
* @returns {{rounds_used:*, rounds_judged:(number|null)}} Both counts.
|
|
60
|
+
*/
|
|
61
|
+
export function deriveRounds(cwd, slug, fallback) {
|
|
62
|
+
const orderRounds = [];
|
|
63
|
+
const oDir = ordersDir(cwd, slug);
|
|
64
|
+
if (existsSync(oDir)) {
|
|
65
|
+
for (const f of readdirSync(oDir)) {
|
|
66
|
+
if (!f.endsWith(".json")) continue;
|
|
67
|
+
const r = orderRound(readJson(join(oDir, f))?.order_id);
|
|
68
|
+
if (r !== null) orderRounds.push(r);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const verdictRounds = [];
|
|
73
|
+
const vDir = verdictsDir(cwd, slug);
|
|
74
|
+
if (existsSync(vDir)) {
|
|
75
|
+
for (const f of readdirSync(vDir).filter((x) => x.endsWith(".json"))) {
|
|
76
|
+
const v = readJson(join(vDir, f));
|
|
77
|
+
if (typeof v?.round === "number") verdictRounds.push(v.round);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const buildGateRounds = [];
|
|
82
|
+
const bDir = roundBuildDir(cwd, slug);
|
|
83
|
+
if (existsSync(bDir)) {
|
|
84
|
+
for (const f of readdirSync(bDir)) {
|
|
85
|
+
const m = f.match(/^r(\d+)-t\d+\.json$/);
|
|
86
|
+
if (m) buildGateRounds.push(Number(m[1]));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const evalRounds = [];
|
|
91
|
+
const rDir = resultsDir(cwd, slug);
|
|
92
|
+
if (existsSync(rDir)) {
|
|
93
|
+
for (const f of readdirSync(rDir)) {
|
|
94
|
+
const m = f.match(/^evaluate-r(\d+)\.json$/);
|
|
95
|
+
if (m) evalRounds.push(Number(m[1]));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const built = maxOf([...orderRounds, ...verdictRounds, ...buildGateRounds, ...evalRounds]);
|
|
100
|
+
return {
|
|
101
|
+
rounds_used: built !== null ? built : fallback,
|
|
102
|
+
rounds_judged: maxOf(evalRounds),
|
|
103
|
+
};
|
|
104
|
+
}
|