shapeup-sdlc 3.18.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.example.json +2 -3
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +115 -56
- package/README.md +101 -143
- package/SECURITY.md +16 -25
- package/agents/orient.md +14 -0
- package/agents/qa-edge-hunter.md +13 -0
- package/agents/spec-evaluator.md +14 -0
- package/agents/task-executor.md +14 -0
- package/bin/init.mjs +13 -16
- package/bin/lib/grant.mjs +8 -19
- package/commands/build.md +9 -14
- package/commands/eval.md +3 -3
- package/commands/hammer.md +9 -5
- package/commands/retro.md +12 -11
- package/commands/scopes.md +1 -12
- package/commands/ship.md +45 -88
- package/commands/wire.md +1 -8
- package/hooks/dispatch-receipt.mjs +94 -8
- package/hooks/hooks.json +0 -41
- package/hooks/lib/decision.mjs +6 -9
- package/hooks/lib/tier.mjs +50 -0
- package/hooks/sandbox-guard.mjs +140 -64
- package/kernel/gate.mjs +203 -22
- package/kernel/harness.mjs +20 -67
- package/kernel/init/run.mjs +175 -523
- package/kernel/lib/contract.mjs +9 -93
- package/kernel/lib/floor.mjs +64 -0
- package/kernel/lib/paths.mjs +39 -112
- package/kernel/order.mjs +157 -0
- package/kernel/probe/digest.mjs +2 -38
- package/kernel/schemas/domain.schema.json +146 -1125
- package/kernel/schemas/work-order.schema.json +8 -4
- package/kernel/schemas/work-result.schema.json +1 -1
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +27 -8
- package/kernel/verify/t0.mjs +2 -2
- package/package.json +4 -2
- package/skills/building/SKILL.md +144 -0
- package/skills/building/resources/gates.md +76 -0
- package/skills/building/resources/guidance.md +23 -0
- package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
- package/skills/building/resources/intake.md +157 -0
- package/skills/building/resources/orders.md +69 -0
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
- package/skills/building/resources/plan-scopes.md +95 -0
- package/skills/building/resources/plan-spec.md +102 -0
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
- package/skills/building/resources/plan-wire.md +82 -0
- package/skills/building/resources/plan.md +98 -0
- package/skills/building/resources/retro-research.md +60 -0
- package/skills/building/resources/retro-scan.md +49 -0
- package/skills/building/resources/retro.md +225 -0
- package/skills/building/resources/rounds.md +78 -0
- package/skills/building/resources/stop.md +127 -0
- package/skills/orient/SKILL.md +37 -37
- package/skills/qa-edge-hunter/SKILL.md +14 -14
- package/skills/shapeup/SKILL.md +5 -5
- package/skills/shapeup/resources/breadboarding.md +1 -1
- package/skills/shapeup/resources/context-compaction.md +5 -5
- package/skills/shapeup/resources/shaping.md +1 -1
- package/skills/spec-evaluator/SKILL.md +17 -21
- package/skills/spec-evaluator/references/dimension-contract.md +1 -1
- package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
- package/skills/task-executor/SKILL.md +11 -15
- package/commands/hill.md +0 -10
- package/hooks/gate-intake.mjs +0 -109
- package/hooks/gate-zerowork.mjs +0 -349
- package/hooks/tier-guard.mjs +0 -162
- package/kernel/compile.mjs +0 -1419
- package/kernel/init/fit.mjs +0 -199
- package/kernel/init/run-args.mjs +0 -206
- package/kernel/lib/breadboard.mjs +0 -165
- package/kernel/probe/attempts.mjs +0 -161
- package/kernel/probe/concurrency.mjs +0 -535
- package/kernel/probe/eval.mjs +0 -405
- package/kernel/probe/hunt.mjs +0 -104
- package/kernel/probe/leg.mjs +0 -229
- package/kernel/probe/owner.mjs +0 -142
- package/kernel/probe/requirements.mjs +0 -328
- package/kernel/probe/resume.mjs +0 -1110
- package/kernel/probe/rounds.mjs +0 -115
- package/kernel/probe/staged.mjs +0 -83
- package/kernel/probe/stats.mjs +0 -477
- package/kernel/probe/t0.mjs +0 -115
- package/kernel/reduce/board.mjs +0 -350
- package/kernel/reduce/graph.mjs +0 -482
- package/kernel/reduce/hill.mjs +0 -322
- package/kernel/reduce/ingest.mjs +0 -767
- package/kernel/reduce/leftovers.mjs +0 -159
- package/kernel/reduce/ship.mjs +0 -529
- package/kernel/reduce/snapshot.mjs +0 -291
- package/kernel/reduce/verdict.mjs +0 -172
- package/kernel/report/export.mjs +0 -384
- package/kernel/report/facts.mjs +0 -234
- package/kernel/verify/budget.mjs +0 -153
- package/kernel/verify/build.mjs +0 -428
- package/kernel/verify/dispatch.mjs +0 -114
- package/kernel/verify/spec.mjs +0 -975
- package/kernel/verify/trace.mjs +0 -604
- package/skills/ba-pitch-analyzer/SKILL.md +0 -204
- package/skills/coach/SKILL.md +0 -397
- package/skills/hill-chart/SKILL.md +0 -154
- package/skills/hill-chart/assets/dashboard.template.html +0 -1432
- package/skills/scope-architect/SKILL.md +0 -174
- package/skills/scope-hammer/SKILL.md +0 -231
- package/skills/solution-architect/SKILL.md +0 -158
- package/skills/tech-lead/SKILL.md +0 -151
- package/skills/tech-lead/references/gates.md +0 -605
- package/skills/tech-lead/references/protocol.md +0 -889
- package/skills/tech-lead/references/tiny-lane.md +0 -53
- package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
- package/skills/translator/SKILL.md +0 -258
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// probe concurrency — "how many legs actually ran at once, and what did that buy?"
|
|
3
|
-
//
|
|
4
|
-
// WHY IT EXISTS. Fanning the scope loop out has three acceptance questions — did two scopes ever
|
|
5
|
-
// run at the same time, did shared state survive it, did wall-clock improve — and all three are
|
|
6
|
-
// MEASUREMENTS. The pipeline emitted no measurement: the first claim that legs ran in parallel came
|
|
7
|
-
// from a probe that counted green results, which is satisfied by three legs run one after another.
|
|
8
|
-
// This is the missing instrument. It writes nothing and answers only from records on disk.
|
|
9
|
-
//
|
|
10
|
-
// THE RULE IT IS BUILT AROUND. A predicate that an ABSENCE can satisfy must report that absence in
|
|
11
|
-
// the same value. `max_concurrent: 1` over four legs and one usable record is not a small number,
|
|
12
|
-
// it is a false one — and it reads exactly like a run that was genuinely sequential. So every
|
|
13
|
-
// figure here travels with the completeness of the record set it came from and with a `bound`
|
|
14
|
-
// saying whether it is exact or a floor, and a speedup that cannot be supported is `null` with the
|
|
15
|
-
// reason, never a plausible-looking ratio.
|
|
16
|
-
//
|
|
17
|
-
// WHERE THE TWO ENDS OF A LEG COME FROM.
|
|
18
|
-
//
|
|
19
|
-
// start `receipts/dispatch.jsonl` — a PostToolUse row, written when the Skill RESOLVED. Measured
|
|
20
|
-
// across real runs it lands 1.8–47 s after the order was compiled, on legs that then ran
|
|
21
|
-
// for minutes: it is a start, and being hook-written and append-only it is the one start
|
|
22
|
-
// that survives a relaunch re-writing the order file.
|
|
23
|
-
// end `legs.jsonl` — the reducer's leg-completion row. EXACT.
|
|
24
|
-
// Falling back, for a run recorded before that ledger existed: the last T0 trial the leg
|
|
25
|
-
// wrote inside its own dispatch window. That is a landmark near the end, not the end, so
|
|
26
|
-
// every figure derived through it is marked a LOWER bound.
|
|
27
|
-
//
|
|
28
|
-
// WHAT IS DELIBERATELY NOT USED: result-file mtimes. A trace that has been copied carries the copy
|
|
29
|
-
// time on every file, so a measurement built on them is fabricated rather than approximate.
|
|
30
|
-
//
|
|
31
|
-
// Usage: node kernel/harness.mjs probe concurrency --slug <slug> [--cwd <dir>] [--run-root <dir>]
|
|
32
|
-
// [--round N] [--gap-s N] [--format json|table]
|
|
33
|
-
// [--require-run-args]
|
|
34
|
-
// Exit: 0 = a report was produced with at least one usable leg · 1 = ran, and no leg in scope had
|
|
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).
|
|
38
|
-
|
|
39
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
40
|
-
import { join, resolve } from "node:path";
|
|
41
|
-
import { runArgs } from "../lib/argv.mjs";
|
|
42
|
-
import { localRoot, runIdFromRoot, RECEIPT_FILE, RUN_ARGS_FILE } from "../lib/paths.mjs";
|
|
43
|
-
|
|
44
|
-
/** The round-addressed order id forms `<scope>-r<N>-a<M>` and `<phase>-r<N>`. */
|
|
45
|
-
const ROUND_SUFFIX = /^(.*?)-r(\d+)(?:-a(\d+))?$/;
|
|
46
|
-
|
|
47
|
-
/** Default fan-out width when a run's `run-args.json` declares none — mirrors the orchestrator. */
|
|
48
|
-
export const DEFAULT_MAX_PARALLEL_SCOPES = 4;
|
|
49
|
-
|
|
50
|
-
/** Default relaunch gap, in seconds. A run resumed after this long is a different launch. */
|
|
51
|
-
export const DEFAULT_GAP_S = 900;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Read a JSONL ledger into rows, skipping torn lines rather than throwing.
|
|
55
|
-
* @param {string} path - Path to a `.jsonl` file.
|
|
56
|
-
* @returns {{rows:Array<object>, malformed:number, present:boolean}} Parsed rows and how many
|
|
57
|
-
* lines could not be parsed — reported, because a ledger half of which is unreadable must not
|
|
58
|
-
* look like a short one.
|
|
59
|
-
*/
|
|
60
|
-
export function readLedger(path) {
|
|
61
|
-
if (!existsSync(path)) return { rows: [], malformed: 0, present: false };
|
|
62
|
-
let malformed = 0;
|
|
63
|
-
const rows = [];
|
|
64
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
65
|
-
if (!line.trim()) continue;
|
|
66
|
-
try { rows.push(JSON.parse(line)); } catch { malformed++; }
|
|
67
|
-
}
|
|
68
|
-
return { rows, malformed, present: true };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Split an order id into the scope/phase name and the round-attempt address it encodes.
|
|
73
|
-
* @param {string} orderId - e.g. `todo-cli/foundation-r2-a1`.
|
|
74
|
-
* @returns {{name:(string|null), round:(number|null), attempt:(number|null)}}
|
|
75
|
-
*/
|
|
76
|
-
export function addressOf(orderId) {
|
|
77
|
-
const suffix = String(orderId ?? "").split("/").slice(1).join("/");
|
|
78
|
-
if (!suffix) return { name: null, round: null, attempt: null };
|
|
79
|
-
const m = suffix.match(ROUND_SUFFIX);
|
|
80
|
-
if (!m) return { name: suffix, round: null, attempt: null };
|
|
81
|
-
return { name: m[1], round: Number(m[2]), attempt: m[3] === undefined ? null : Number(m[3]) };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Every build dispatch's start, from the hook-written receipt ledger.
|
|
86
|
-
*
|
|
87
|
-
* BUILD LEGS ONLY, and the count of what was skipped travels with the answer. ORIENT, ANALYZE,
|
|
88
|
-
* WIRE, EVAL and GATE H are sequential phases in every lane; folding them into a concurrency figure
|
|
89
|
-
* would dilute exactly the number the fan-out is judged on. A leg qualifies by carrying an attempt
|
|
90
|
-
* address (`-r<N>-a<M>`), which only a scope build order does.
|
|
91
|
-
*
|
|
92
|
-
* @param {Array<object>} receipts - Rows from `receipts/dispatch.jsonl`.
|
|
93
|
-
* @returns {{starts:Array<object>, phase_dispatches:number}} Starts sorted by time.
|
|
94
|
-
*/
|
|
95
|
-
export function startsFrom(receipts) {
|
|
96
|
-
const starts = [];
|
|
97
|
-
let phase = 0;
|
|
98
|
-
for (const r of receipts) {
|
|
99
|
-
const { name, round, attempt } = addressOf(r.order_id);
|
|
100
|
-
const at = Date.parse(r.at);
|
|
101
|
-
if (attempt === null || !Number.isFinite(at)) { phase++; continue; }
|
|
102
|
-
starts.push({ order_id: r.order_id, scope_id: name, round, attempt, start: at, start_source: "dispatch-receipt", run_id: r.run_id ?? null });
|
|
103
|
-
}
|
|
104
|
-
starts.sort((a, b) => a.start - b.start || String(a.order_id).localeCompare(String(b.order_id)));
|
|
105
|
-
return { starts, phase_dispatches: phase };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Attach an end to every start.
|
|
110
|
-
*
|
|
111
|
-
* THE PAIRING PROBLEM THIS SOLVES. One order id is dispatched many times — a relaunch re-uses the
|
|
112
|
-
* path verbatim — so "the completion row for this order" is ambiguous and picking the newest would
|
|
113
|
-
* hand every earlier dispatch the last one's end. Each start therefore claims only what falls
|
|
114
|
-
* inside its own window: from its own instant up to the NEXT dispatch OF THE SAME SCOPE AND ROUND.
|
|
115
|
-
*
|
|
116
|
-
* The window is keyed by scope+round rather than by order id, and the difference is not cosmetic.
|
|
117
|
-
* Windowing by order id let one archived leg claim a T0 artifact written by a LATER dispatch of the
|
|
118
|
-
* same scope under a different attempt number — a leg that had actually died came back with a
|
|
119
|
-
* seventeen-minute duration. A leg for scope S in round R is over the moment anything else is
|
|
120
|
-
* dispatched for S in R, whatever attempt it is addressed by.
|
|
121
|
-
*
|
|
122
|
-
* Exact ends come from the leg ledger. When a start's window holds none — every run recorded before
|
|
123
|
-
* that ledger existed, and every leg that died before ingest — the last T0 trial the scope wrote
|
|
124
|
-
* inside the same window stands in, and the leg is marked a lower bound rather than a fact.
|
|
125
|
-
*
|
|
126
|
-
* @param {Array<object>} starts - From {@link startsFrom}.
|
|
127
|
-
* @param {Array<object>} legRows - Rows from `legs.jsonl`.
|
|
128
|
-
* @param {Array<object>} trials - Rows from `t0/trials.jsonl`.
|
|
129
|
-
* @returns {Array<object>} One leg per start, `end` null when nothing closed it.
|
|
130
|
-
*/
|
|
131
|
-
export function pairLegs(starts, legRows, trials) {
|
|
132
|
-
const sameLeg = (a, b) => a.scope_id === b.scope_id && a.round === b.round;
|
|
133
|
-
const nextOf = new Map();
|
|
134
|
-
for (let i = 0; i < starts.length; i++) {
|
|
135
|
-
for (let j = i + 1; j < starts.length; j++) {
|
|
136
|
-
if (sameLeg(starts[j], starts[i])) { nextOf.set(i, starts[j].start); break; }
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return starts.map((s, i) => {
|
|
140
|
-
const until = nextOf.get(i) ?? Infinity;
|
|
141
|
-
const inWindow = (t) => Number.isFinite(t) && t >= s.start && t < until;
|
|
142
|
-
|
|
143
|
-
const landmarks = trials
|
|
144
|
-
.filter((t) => t.scope_id === s.scope_id && Number(t.round) === s.round && inWindow(Date.parse(t.at)))
|
|
145
|
-
.map((t) => Date.parse(t.at)).sort((a, b) => a - b);
|
|
146
|
-
// The INNER end, kept alongside the outer one rather than used only as a fallback. A T0 trial
|
|
147
|
-
// was written BY THE LEG, mid-flight, so `[start, inner_end]` is an interval the leg provably
|
|
148
|
-
// occupied. `ingested_at` is when the WRITER ran, which is normally the leg's last act and is
|
|
149
|
-
// sometimes not the leg at all — an out-of-band ingest (a repair, a `--no-receipt-check` rescue,
|
|
150
|
-
// a relaunch re-applying a result) stamps a later instant onto the same row and stretches the
|
|
151
|
-
// leg over time it was not running. That is not hypothetical: one leg ingested out of band, minutes
|
|
152
|
-
// after its own T0 landmark while its neighbours closed within seconds of theirs, is enough to
|
|
153
|
-
// report two legs overlapping on a run whose dial was 1 and which built one scope at a time.
|
|
154
|
-
const innerEnd = landmarks.length ? landmarks[landmarks.length - 1] : null;
|
|
155
|
-
|
|
156
|
-
const exact = legRows
|
|
157
|
-
.filter((r) => r.order_id === s.order_id && inWindow(Date.parse(r.ingested_at)))
|
|
158
|
-
.map((r) => Date.parse(r.ingested_at)).sort((a, b) => a - b);
|
|
159
|
-
if (exact.length) {
|
|
160
|
-
return { ...s, end: exact[exact.length - 1], end_source: "leg-record", bound: "exact", inner_end: innerEnd };
|
|
161
|
-
}
|
|
162
|
-
if (innerEnd !== null) {
|
|
163
|
-
return { ...s, end: innerEnd, end_source: "t0-landmark", bound: "lower", inner_end: innerEnd };
|
|
164
|
-
}
|
|
165
|
-
return { ...s, end: null, end_source: null, bound: null, inner_end: null };
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Split legs into launches.
|
|
171
|
-
*
|
|
172
|
-
* A run keeps one `run_id` across every relaunch — the key is derived from the receipt, and a
|
|
173
|
-
* resumed run reads the same receipt — so a span taken over all of a run's legs fuses launches that
|
|
174
|
-
* were hours apart. Measured on one archived run: four launches under one key, 17:18 to 03:35, of
|
|
175
|
-
* which the great majority is a laptop being closed.
|
|
176
|
-
*
|
|
177
|
-
* There is no launch id in the record set, so this is a GAP HEURISTIC and it says so: the rule and
|
|
178
|
-
* its threshold are in the report, because a heuristic that hides is indistinguishable from a fact.
|
|
179
|
-
*
|
|
180
|
-
* @param {Array<object>} legs - Legs with a usable interval, sorted by start.
|
|
181
|
-
* @param {number} gapMs - A quiet period longer than this starts a new launch.
|
|
182
|
-
* @returns {Array<Array<object>>} One group per launch.
|
|
183
|
-
*/
|
|
184
|
-
export function segment(legs, gapMs) {
|
|
185
|
-
if (!legs.length) return [];
|
|
186
|
-
const out = [[legs[0]]];
|
|
187
|
-
let high = legs[0].end;
|
|
188
|
-
for (const leg of legs.slice(1)) {
|
|
189
|
-
if (leg.start - high > gapMs) { out.push([leg]); } else { out[out.length - 1].push(leg); }
|
|
190
|
-
high = Math.max(high, leg.end);
|
|
191
|
-
}
|
|
192
|
-
return out;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* The greatest number of legs open at one instant, and when it happened.
|
|
197
|
-
*
|
|
198
|
-
* Ends are processed before starts at the same millisecond, so a leg that closes exactly as the
|
|
199
|
-
* next one opens is not counted as an overlap. Two legs are concurrent only if they were both
|
|
200
|
-
* genuinely running, and a tie at the boundary is the case where that is least certain.
|
|
201
|
-
*
|
|
202
|
-
* @param {Array<object>} legs - Legs with `start` and `end`.
|
|
203
|
-
* @param {string} [endField] - Which end to measure against: `end` (the outer end, an ingest) or
|
|
204
|
-
* `inner_end` (the last T0 trial the leg wrote, provably mid-flight). Legs lacking the named field
|
|
205
|
-
* are dropped, so the caller must read the returned `legs` count before trusting `max`.
|
|
206
|
-
* @returns {{max:(number|null), at:(number|null), legs:number}} `null` when there is nothing to
|
|
207
|
-
* measure — never `0` or `1`, which a reader would take for a sequential run.
|
|
208
|
-
*/
|
|
209
|
-
export function maxConcurrent(legs, endField = "end") {
|
|
210
|
-
const usable = legs.filter((l) => Number.isFinite(l[endField]));
|
|
211
|
-
if (!usable.length) return { max: null, at: null, legs: 0 };
|
|
212
|
-
const events = [];
|
|
213
|
-
for (const l of usable) { events.push([l.start, 1]); events.push([l[endField], -1]); }
|
|
214
|
-
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
215
|
-
let open = 0, max = 0, at = null;
|
|
216
|
-
for (const [t, delta] of events) {
|
|
217
|
-
open += delta;
|
|
218
|
-
if (open > max) { max = open; at = t; }
|
|
219
|
-
}
|
|
220
|
-
return { max, at, legs: usable.length };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* The waves the run actually ran, derived from the timings rather than from the scheduler.
|
|
225
|
-
*
|
|
226
|
-
* A wave is a connected component of overlapping intervals: legs that ran together, bounded by
|
|
227
|
-
* quiet moments where nothing was in flight. This is an INDEPENDENT reading of the schedule — if
|
|
228
|
-
* the scheduler claims a dependency ordering, this is where it either shows up or does not.
|
|
229
|
-
*
|
|
230
|
-
* @param {Array<object>} legs - Legs with a usable interval, sorted by start.
|
|
231
|
-
* @returns {Array<{from:string, to:string, scopes:string[]}>} Waves in start order.
|
|
232
|
-
*/
|
|
233
|
-
export function wavesObserved(legs) {
|
|
234
|
-
const waves = [];
|
|
235
|
-
for (const leg of legs) {
|
|
236
|
-
const last = waves[waves.length - 1];
|
|
237
|
-
if (last && leg.start < last.endMs) {
|
|
238
|
-
last.members.push(leg);
|
|
239
|
-
last.endMs = Math.max(last.endMs, leg.end);
|
|
240
|
-
} else {
|
|
241
|
-
waves.push({ startMs: leg.start, endMs: leg.end, members: [leg] });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return waves.map((w) => ({
|
|
245
|
-
from: new Date(w.startMs).toISOString(),
|
|
246
|
-
to: new Date(w.endMs).toISOString(),
|
|
247
|
-
scopes: [...new Set(w.members.map((m) => m.scope_id))].sort(),
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Summarise one launch: overlap, span, work, and whether a speedup can honestly be stated.
|
|
253
|
-
*
|
|
254
|
-
* WHY A SPEEDUP IS REFUSED ON ANY LOWER-BOUND LEG, and it is not caution. Truncating every leg at
|
|
255
|
-
* its T0 landmark shortens the numerator once per leg and the denominator only for whichever leg
|
|
256
|
-
* finished last, so the ratio moves in a direction that depends on the shape of the round. Measured
|
|
257
|
-
* on a real round that demonstrably ran four scopes at once, the truncated figure is 0.90 — a
|
|
258
|
-
* "speedup" below 1. A ratio of two lower bounds is not a bound on the ratio.
|
|
259
|
-
*
|
|
260
|
-
* @param {number} index - 1-based launch number.
|
|
261
|
-
* @param {Array<object>} legs - The launch's legs, each with a usable interval.
|
|
262
|
-
* @returns {object} The launch block of the report.
|
|
263
|
-
*/
|
|
264
|
-
export function summarise(index, legs) {
|
|
265
|
-
const { max, at } = maxConcurrent(legs);
|
|
266
|
-
// The same question asked of the inner intervals — dispatch to the leg's own T0 trial. Two legs
|
|
267
|
-
// overlapping THERE were both provably mid-flight at one instant; two legs overlapping only on the
|
|
268
|
-
// outer intervals may be one leg plus somebody else's later ingest of it.
|
|
269
|
-
const inner = maxConcurrent(legs, "inner_end");
|
|
270
|
-
const exact = legs.every((l) => l.bound === "exact");
|
|
271
|
-
const from = Math.min(...legs.map((l) => l.start));
|
|
272
|
-
const to = Math.max(...legs.map((l) => l.end));
|
|
273
|
-
const span = to - from;
|
|
274
|
-
const sum = legs.reduce((a, l) => a + (l.end - l.start), 0);
|
|
275
|
-
const bound = exact ? "exact" : "lower";
|
|
276
|
-
return {
|
|
277
|
-
launch: index,
|
|
278
|
-
from: new Date(from).toISOString(),
|
|
279
|
-
to: new Date(to).toISOString(),
|
|
280
|
-
legs: legs.length,
|
|
281
|
-
max_concurrent: max,
|
|
282
|
-
max_concurrent_at: at === null ? null : new Date(at).toISOString(),
|
|
283
|
-
max_concurrent_bound: bound,
|
|
284
|
-
// Reported beside the outer figure, never instead of it, and with its own coverage count: an
|
|
285
|
-
// inner figure over 2 of 6 legs is not evidence the other four ran alone.
|
|
286
|
-
max_concurrent_inner: inner.max,
|
|
287
|
-
max_concurrent_inner_legs: inner.legs,
|
|
288
|
-
// The two ends disagreeing is a fact about the RECORD, not about the run, and it is the only
|
|
289
|
-
// signal that separates a leg that ran long from a leg something ingested later.
|
|
290
|
-
ends_disputed: max !== null && inner.max !== null && inner.legs === legs.length && max > inner.max,
|
|
291
|
-
span_ms: span,
|
|
292
|
-
span_bound: bound,
|
|
293
|
-
sum_leg_ms: sum,
|
|
294
|
-
sum_leg_bound: bound,
|
|
295
|
-
speedup: exact && span > 0 ? Math.round((sum / span) * 100) / 100 : null,
|
|
296
|
-
speedup_refused_because: exact
|
|
297
|
-
? (span > 0 ? null : "the launch's legs span zero milliseconds — nothing to divide by")
|
|
298
|
-
: `${legs.filter((l) => l.bound !== "exact").length} of ${legs.length} leg(s) end at a T0 landmark rather than a completion record, so both the span and the work sum are floors; their ratio bounds nothing`,
|
|
299
|
-
waves_observed: wavesObserved(legs),
|
|
300
|
-
scopes: [...new Set(legs.map((l) => l.scope_id))].sort(),
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Which fan-out width the run was launched with — read, never assumed.
|
|
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.
|
|
312
|
-
*
|
|
313
|
-
* @param {string} runRoot - The run's LOCAL root.
|
|
314
|
-
* @returns {{max_parallel_scopes:number, source:string}} The value and where it came from.
|
|
315
|
-
*/
|
|
316
|
-
export function dialFrom(runRoot) {
|
|
317
|
-
try {
|
|
318
|
-
const a = JSON.parse(readFileSync(join(runRoot, RUN_ARGS_FILE), "utf8"));
|
|
319
|
-
const n = Number(a?.maxParallelScopes);
|
|
320
|
-
if (Number.isFinite(n) && n >= 1) return { max_parallel_scopes: n, source: "run-args" };
|
|
321
|
-
return { max_parallel_scopes: DEFAULT_MAX_PARALLEL_SCOPES, source: "default (run-args.json declares none)" };
|
|
322
|
-
} catch {
|
|
323
|
-
return { max_parallel_scopes: DEFAULT_MAX_PARALLEL_SCOPES, source: "default (no run-args.json)" };
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* The whole report for one run root.
|
|
329
|
-
*
|
|
330
|
-
* @param {string} runRoot - The run's LOCAL root (`<cwd>/.shapeup/<slug>`, or an archived copy).
|
|
331
|
-
* @param {object} [opts] - `{round, gapS}`.
|
|
332
|
-
* @returns {object} A byte-stable report — no clock reading anywhere in it, arrays sorted, key
|
|
333
|
-
* order fixed by this literal — so two runs over one tree produce identical bytes.
|
|
334
|
-
*/
|
|
335
|
-
export function report(runRoot, { round = null, gapS = DEFAULT_GAP_S } = {}) {
|
|
336
|
-
const receipts = readLedger(join(runRoot, "receipts", "dispatch.jsonl"));
|
|
337
|
-
const legLedger = readLedger(join(runRoot, "legs.jsonl"));
|
|
338
|
-
const trials = readLedger(join(runRoot, "t0", "trials.jsonl"));
|
|
339
|
-
|
|
340
|
-
const { starts, phase_dispatches } = startsFrom(receipts.rows);
|
|
341
|
-
const scoped = round === null ? starts : starts.filter((s) => s.round === round);
|
|
342
|
-
const legs = pairLegs(scoped, legLedger.rows, trials.rows);
|
|
343
|
-
const usable = legs.filter((l) => l.end !== null);
|
|
344
|
-
|
|
345
|
-
const warnings = [];
|
|
346
|
-
if (!receipts.present) {
|
|
347
|
-
warnings.push("no receipts/dispatch.jsonl — nothing recorded a dispatch, so no leg has a start. " +
|
|
348
|
-
"An empty report here means the record set is empty, not that the run was sequential.");
|
|
349
|
-
}
|
|
350
|
-
if (!legLedger.present) {
|
|
351
|
-
warnings.push("no legs.jsonl — this run was recorded before leg completions were written, so every " +
|
|
352
|
-
"end falls back to the T0 landmark and every figure below is a floor.");
|
|
353
|
-
}
|
|
354
|
-
for (const l of [["receipts/dispatch.jsonl", receipts], ["legs.jsonl", legLedger], ["t0/trials.jsonl", trials]]) {
|
|
355
|
-
if (l[1].malformed) warnings.push(`${l[0]}: ${l[1].malformed} unparseable line(s) skipped`);
|
|
356
|
-
}
|
|
357
|
-
const unkeyed = legs.filter((l) => !l.run_id).length;
|
|
358
|
-
if (unkeyed) {
|
|
359
|
-
warnings.push(`${unkeyed} of ${legs.length} leg(s) carry no run_id — order_id alone repeats across ` +
|
|
360
|
-
"runs of one slug, so those rows cannot be attributed to this run rather than another.");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const launches = segment(usable, gapS * 1000).map((g, i) => summarise(i + 1, g));
|
|
364
|
-
|
|
365
|
-
const best = launches.reduce((a, b) => (b.max_concurrent > (a?.max_concurrent ?? -1) ? b : a), null);
|
|
366
|
-
// The launch the headline verdict is answered from, chosen on the INNER figure. Taking the inner
|
|
367
|
-
// number off `best` would compare two different launches: the widest outer overlap and the widest
|
|
368
|
-
// inner one need not be the same launch, and mixing them would let one launch's ingest artefact
|
|
369
|
-
// stand beside another launch's honest evidence.
|
|
370
|
-
const bestInner = launches.reduce((a, b) => (b.max_concurrent_inner > (a?.max_concurrent_inner ?? -1) ? b : a), null);
|
|
371
|
-
return {
|
|
372
|
-
schema_version: 1,
|
|
373
|
-
run_root: runRoot,
|
|
374
|
-
run_id: runIdFromRoot(runRoot),
|
|
375
|
-
receipt_present: existsSync(join(runRoot, RECEIPT_FILE)),
|
|
376
|
-
round,
|
|
377
|
-
dial: dialFrom(runRoot),
|
|
378
|
-
completeness: {
|
|
379
|
-
// The absence, in the same value as the number. `max_concurrent` below is meaningless without
|
|
380
|
-
// these four counts, so they are not an appendix.
|
|
381
|
-
legs_total: legs.length,
|
|
382
|
-
legs_exact: legs.filter((l) => l.bound === "exact").length,
|
|
383
|
-
legs_lower_bound: legs.filter((l) => l.bound === "lower").length,
|
|
384
|
-
no_completion_record: legs.filter((l) => l.end === null).length,
|
|
385
|
-
legs_unkeyed: unkeyed,
|
|
386
|
-
phase_dispatches_excluded: phase_dispatches,
|
|
387
|
-
end_sources: {
|
|
388
|
-
"leg-record": legs.filter((l) => l.end_source === "leg-record").length,
|
|
389
|
-
"t0-landmark": legs.filter((l) => l.end_source === "t0-landmark").length,
|
|
390
|
-
none: legs.filter((l) => l.end_source === null).length,
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
concurrency: {
|
|
394
|
-
max_concurrent: best ? best.max_concurrent : null,
|
|
395
|
-
bound: best ? best.max_concurrent_bound : null,
|
|
396
|
-
at: best ? best.max_concurrent_at : null,
|
|
397
|
-
launch: best ? best.launch : null,
|
|
398
|
-
max_concurrent_inner: bestInner ? bestInner.max_concurrent_inner : null,
|
|
399
|
-
// The acceptance predicate, answered rather than left to the reader — and `null` when the
|
|
400
|
-
// record set cannot answer it, which is a third state and not a "no".
|
|
401
|
-
//
|
|
402
|
-
// ANSWERED FROM THE INNER INTERVALS WHEN THEY COVER EVERY LEG, because that is the only form
|
|
403
|
-
// of the evidence an out-of-band ingest cannot fabricate. A run with dial 1 has been observed
|
|
404
|
-
// reporting `max_concurrent: 2 (exact)` because one leg was ingested long after it finished; the
|
|
405
|
-
// inner intervals said 1, which was the truth. Where the two disagree the answer is
|
|
406
|
-
// `"disputed"` rather than either number — a fourth state, and the honest one.
|
|
407
|
-
two_or_more_concurrent: !best ? null
|
|
408
|
-
: (bestInner && bestInner.max_concurrent_inner_legs === bestInner.legs)
|
|
409
|
-
? (bestInner.max_concurrent_inner >= 2 ? true : (best.max_concurrent >= 2 ? "disputed" : false))
|
|
410
|
-
: best.max_concurrent >= 2,
|
|
411
|
-
disputed_because: (best && bestInner && bestInner.max_concurrent_inner_legs === bestInner.legs
|
|
412
|
-
&& bestInner.max_concurrent_inner < 2 && best.max_concurrent >= 2)
|
|
413
|
-
? "the overlap exists only between intervals that END AT AN INGEST; measured against each leg's own " +
|
|
414
|
-
"T0 trial — an instant the leg provably occupied — no two legs were open together. An ingest run " +
|
|
415
|
-
"outside its leg (a repair, a --no-receipt-check rescue, a relaunch re-applying a result) stamps a " +
|
|
416
|
-
"later end onto the row and manufactures exactly this overlap."
|
|
417
|
-
: null,
|
|
418
|
-
},
|
|
419
|
-
launches,
|
|
420
|
-
segmentation: {
|
|
421
|
-
rule: "a quiet period longer than gap_s starts a new launch",
|
|
422
|
-
gap_s: gapS,
|
|
423
|
-
launches: launches.length,
|
|
424
|
-
note: "heuristic: nothing in the record set identifies a launch, and one run_id spans every relaunch",
|
|
425
|
-
},
|
|
426
|
-
legs: legs.map((l) => ({
|
|
427
|
-
order_id: l.order_id,
|
|
428
|
-
scope_id: l.scope_id,
|
|
429
|
-
round: l.round,
|
|
430
|
-
attempt: l.attempt,
|
|
431
|
-
run_id: l.run_id,
|
|
432
|
-
started_at: new Date(l.start).toISOString(),
|
|
433
|
-
start_source: l.start_source,
|
|
434
|
-
ended_at: l.end === null ? null : new Date(l.end).toISOString(),
|
|
435
|
-
end_source: l.end_source,
|
|
436
|
-
duration_ms: l.end === null ? null : l.end - l.start,
|
|
437
|
-
bound: l.bound,
|
|
438
|
-
// The leg's own mid-flight landmark and how far its recorded end sits past it. Reported for the
|
|
439
|
-
// reader to judge rather than thresholded, deliberately: a leg that closed inside itself and one
|
|
440
|
-
// ingested out of band differ by orders of magnitude on any single machine, and by nothing
|
|
441
|
-
// reliable ACROSS machines — so no constant that separates them here would survive slower
|
|
442
|
-
// hardware. The lag is the fact; what counts as suspicious is the reader's call.
|
|
443
|
-
inner_ended_at: l.inner_end === null ? null : new Date(l.inner_end).toISOString(),
|
|
444
|
-
end_lag_ms: (l.end === null || l.inner_end === null) ? null : l.end - l.inner_end,
|
|
445
|
-
})),
|
|
446
|
-
warnings,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Render the report as a table for a human, from the already-built object.
|
|
452
|
-
* @param {object} r - A {@link report}.
|
|
453
|
-
* @returns {string} The human view.
|
|
454
|
-
*/
|
|
455
|
-
export function table(r) {
|
|
456
|
-
const c = r.completeness;
|
|
457
|
-
const lines = [
|
|
458
|
-
`run_root ${r.run_root}`,
|
|
459
|
-
`run_id ${r.run_id ?? "(none — records cannot be attributed to a run)"}`,
|
|
460
|
-
`dial maxParallelScopes=${r.dial.max_parallel_scopes} (${r.dial.source})`,
|
|
461
|
-
`legs ${c.legs_total} total · ${c.legs_exact} exact · ${c.legs_lower_bound} lower-bound · ${c.no_completion_record} with NO completion record` +
|
|
462
|
-
`${c.phase_dispatches_excluded ? ` (+${c.phase_dispatches_excluded} non-build dispatch(es) excluded)` : ""}`,
|
|
463
|
-
`concurrency ${r.concurrency.max_concurrent ?? "unmeasurable"}` +
|
|
464
|
-
`${r.concurrency.max_concurrent === null ? "" : ` (${r.concurrency.bound}) at ${r.concurrency.at}`}` +
|
|
465
|
-
`${r.concurrency.max_concurrent_inner === null ? "" : ` · ${r.concurrency.max_concurrent_inner} measured inside the legs' own T0 windows`}`,
|
|
466
|
-
`≥2 concurrent ${r.concurrency.two_or_more_concurrent === null ? "UNKNOWN — no leg in scope has a usable interval" : r.concurrency.two_or_more_concurrent}`,
|
|
467
|
-
...(r.concurrency.disputed_because ? [` ! ${r.concurrency.disputed_because}`] : []),
|
|
468
|
-
];
|
|
469
|
-
for (const l of r.launches) {
|
|
470
|
-
lines.push(` launch ${l.launch} ${l.from} → ${l.to} legs=${l.legs} max=${l.max_concurrent} (${l.max_concurrent_bound})` +
|
|
471
|
-
` span=${(l.span_ms / 1000).toFixed(1)}s work=${(l.sum_leg_ms / 1000).toFixed(1)}s speedup=${l.speedup ?? "refused"}`);
|
|
472
|
-
if (l.speedup === null) lines.push(` refused: ${l.speedup_refused_because}`);
|
|
473
|
-
for (const w of l.waves_observed) lines.push(` wave ${w.scopes.join(" + ")}`);
|
|
474
|
-
}
|
|
475
|
-
for (const w of r.warnings) lines.push(` ! ${w}`);
|
|
476
|
-
return lines.join("\n");
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
480
|
-
export const ARGV_SPEC = {
|
|
481
|
-
usage: "harness.mjs probe concurrency (--slug <slug> | --run-root <dir>) [--cwd <dir>] [--round N] " +
|
|
482
|
-
"[--gap-s N] [--format json|table] [--require-run-args]",
|
|
483
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
484
|
-
slug: { type: "str" },
|
|
485
|
-
// The archived-trace and post-export cases, and the same escape `verify t0 --out` has: a caller
|
|
486
|
-
// that already holds the run root should not have to reconstruct a slug from a directory name.
|
|
487
|
-
"run-root": { type: "path" },
|
|
488
|
-
cwd: { type: "path" },
|
|
489
|
-
round: { type: "int", min: 1 },
|
|
490
|
-
"gap-s": { type: "int", min: 1, default: DEFAULT_GAP_S },
|
|
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" },
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Report a run's leg concurrency, span and observed waves.
|
|
500
|
-
*
|
|
501
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
502
|
-
* @returns {void} Exits 0 when at least one leg had a usable interval, 1 when none did — and the
|
|
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.
|
|
512
|
-
*/
|
|
513
|
-
export function cli(rawArgv) {
|
|
514
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
515
|
-
if (!args.slug && !args.runRoot) {
|
|
516
|
-
process.stderr.write(JSON.stringify({ error: "missing_required", flag: "--slug", expected: "a slug, or --run-root <dir>" }) + "\n");
|
|
517
|
-
process.stderr.write(`usage: ${ARGV_SPEC.usage}\n`);
|
|
518
|
-
process.exit(2);
|
|
519
|
-
}
|
|
520
|
-
const runRoot = args.runRoot
|
|
521
|
-
? resolve(args.runRoot)
|
|
522
|
-
: localRoot(resolve(args.cwd || process.cwd()), args.slug);
|
|
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
|
-
|
|
532
|
-
const r = report(runRoot, { round: args.round ?? null, gapS: args.gapS });
|
|
533
|
-
console.log(args.format === "table" ? table(r) : JSON.stringify(r));
|
|
534
|
-
process.exit(r.launches.length ? 0 : 1);
|
|
535
|
-
}
|