tickmarkr 2.6.2 → 2.6.4
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/README.md +12 -2
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +48 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/types.d.ts +6 -0
- package/dist/cli/commands/approve.d.ts +6 -3
- package/dist/cli/commands/approve.js +43 -6
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +6 -1
- package/dist/cli/commands/fleet.js +264 -59
- package/dist/cli/commands/init.js +5 -3
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +40 -2
- package/dist/cli/commands/report.js +278 -11
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +49 -4
- package/dist/cli/commands/verify.js +332 -121
- package/dist/compile/native.js +137 -0
- package/dist/config/config.d.ts +40 -9
- package/dist/config/config.js +133 -15
- package/dist/config/fleet-overlay.d.ts +13 -3
- package/dist/config/fleet-overlay.js +12 -8
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +9 -1
- package/dist/drivers/orca.js +29 -7
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/gates/acceptance.d.ts +7 -0
- package/dist/gates/acceptance.js +27 -5
- package/dist/gates/baseline.d.ts +20 -1
- package/dist/gates/baseline.js +100 -20
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +27 -8
- package/dist/gates/review.d.ts +6 -1
- package/dist/gates/review.js +122 -32
- package/dist/gates/run-gates.d.ts +54 -3
- package/dist/gates/run-gates.js +331 -45
- package/dist/gates/test-manifest.d.ts +42 -0
- package/dist/gates/test-manifest.js +69 -10
- package/dist/route/router.d.ts +23 -1
- package/dist/route/router.js +54 -16
- package/dist/run/consult.d.ts +3 -1
- package/dist/run/consult.js +4 -2
- package/dist/run/daemon.d.ts +2 -1
- package/dist/run/daemon.js +349 -79
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +123 -1
- package/dist/run/journal.js +480 -17
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/protocol.d.ts +15 -0
- package/dist/run/protocol.js +11 -1
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/live-store.d.ts +6 -0
- package/dist/tui/cockpit/live-store.js +36 -11
- package/dist/tui/cockpit/run-cockpit.js +2 -2
- package/dist/tui/cockpit/run-view.d.ts +2 -1
- package/dist/tui/cockpit/run-view.js +13 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
- package/dist/tui/cockpit/setup-cockpit.js +4 -0
- package/package.json +2 -1
- package/schema/config.schema.json +8 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -2
- package/skills/tickmarkr-loop/SKILL.md +17 -5
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
|
@@ -11,9 +11,9 @@ import { batteryPriority, chainDepth, dispatchWaves, graphDefinitionHash, loadGr
|
|
|
11
11
|
import { filesGlob } from "../../graph/files-glob.js";
|
|
12
12
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
13
13
|
import { pendingDaemonApprovalActions, resolveRunMode } from "../../run/daemon.js";
|
|
14
|
-
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
14
|
+
import { disallowedBy, excludedChannels, exclusionLine, observedSeat, routingEntrySeatLines } from "../../route/preference.js";
|
|
15
15
|
import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
|
|
16
|
-
import { route, RoutingError } from "../../route/router.js";
|
|
16
|
+
import { resolvedFloor, route, RoutingError } from "../../route/router.js";
|
|
17
17
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
18
18
|
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
19
19
|
import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
|
|
@@ -237,16 +237,21 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
237
237
|
...(runLine ? [runLine] : []),
|
|
238
238
|
"",
|
|
239
239
|
];
|
|
240
|
-
|
|
240
|
+
// OBS-1186: the judge seat is judged under doctor's cached identity for that exact channel — the
|
|
241
|
+
// runtime judge's rule; unknown stays conservative (alias-family deny).
|
|
242
|
+
const judgeDeny = disallowedBy(observedSeat(health, cfg.judge.adapter, cfg.judge.model), cfg.routing, "judge");
|
|
241
243
|
if (judgeDeny?.by === "deny") {
|
|
242
244
|
lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
|
|
243
245
|
}
|
|
244
246
|
const entrySeats = routingEntrySeatLines(cfg);
|
|
245
247
|
if (entrySeats.length)
|
|
246
248
|
lines.push(...entrySeats, "");
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
// OBS-1185: the floor route() resolved for THIS task — a task hint over the configured/mode floor.
|
|
250
|
+
const derivation = (t) => {
|
|
251
|
+
const floor = resolvedFloor(t, cfg);
|
|
252
|
+
if (!floor)
|
|
253
|
+
return null;
|
|
254
|
+
return ` floor ${floor.tier} ← ${floor.source === "task" ? "task hint" : mode.provenance[t.shape] ?? "config floors"}`;
|
|
250
255
|
};
|
|
251
256
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
252
257
|
if (excluded.length)
|
|
@@ -408,7 +413,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
408
413
|
const review = reviewSeat(t, r.assignment);
|
|
409
414
|
cost += est + judge.cost + review.cost;
|
|
410
415
|
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
|
|
411
|
-
const d = derivation(t
|
|
416
|
+
const d = derivation(t);
|
|
412
417
|
if (d)
|
|
413
418
|
lines.push(d);
|
|
414
419
|
if (r.deviation) {
|
|
@@ -432,7 +437,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
432
437
|
throw e;
|
|
433
438
|
const msg = `${e.message}${exclusionReason(e.message)}`;
|
|
434
439
|
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}`, chainLine(t));
|
|
435
|
-
const d = derivation(t
|
|
440
|
+
const d = derivation(t);
|
|
436
441
|
if (d)
|
|
437
442
|
lines.push(d);
|
|
438
443
|
lints.push(`${t.id}: unroutable — ${msg}`);
|
|
@@ -1,4 +1,42 @@
|
|
|
1
|
+
import type { BaselineCommand } from "../../gates/baseline.js";
|
|
1
2
|
import { type ChannelCost } from "../../report/cost.js";
|
|
2
|
-
import { type JournalEvent, type TelemetryRow } from "../../run/journal.js";
|
|
3
|
-
export
|
|
3
|
+
import { type JournalEvent, type OwedFold, type TelemetryRow } from "../../run/journal.js";
|
|
4
|
+
export interface FirstPass {
|
|
5
|
+
passed: number;
|
|
6
|
+
known: number;
|
|
7
|
+
unknown: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* End-to-end first pass over the finished tasks of the SELECTED journal (no cross-run scan). A task
|
|
11
|
+
* closed done, merged, or named done by the newest run-end passes only as one dispatch at attempt 0
|
|
12
|
+
* straight to its merge. A task whose predecessor or dispatch evidence is missing — no run-start, a run
|
|
13
|
+
* that supersedes another, no dispatch, a first dispatch not at attempt 0 or not recording lifetime
|
|
14
|
+
* worker dispatch ordinal 0 — is unknown whatever else it shows: attempts reset at every release, so
|
|
15
|
+
* only the lifetime ordinal proves no earlier dispatch is missing. Otherwise any park, approval, failure, repair, restore or second dispatch is a known miss — a
|
|
16
|
+
* run-end summary's park or a restart before its merge included — and a would-be pass with no merge is
|
|
17
|
+
* unknown, never counted as a pass.
|
|
18
|
+
*/
|
|
19
|
+
export declare function endToEndFirstPass(events: readonly JournalEvent[]): FirstPass;
|
|
20
|
+
export declare const firstPassText: ({ passed, known, unknown }: FirstPass) => string;
|
|
21
|
+
/** The CURRENT owed-check fold. `cwd` is any checkout of the run's repository. */
|
|
22
|
+
export declare const currentOwed: (events: readonly JournalEvent[], cwd?: string) => OwedFold;
|
|
23
|
+
export declare const outstandingText: (fold: OwedFold) => string;
|
|
24
|
+
export declare const needsYouText: (events: readonly JournalEvent[], owed: OwedFold) => string;
|
|
25
|
+
/** The wall window alone, for the one-line form: the same partition, never task-time. */
|
|
26
|
+
export declare const wallText: (events: readonly JournalEvent[]) => string;
|
|
27
|
+
/**
|
|
28
|
+
* The three lead lines. `finished` is the surface's own done tally and tip reading (plus any
|
|
29
|
+
* comparability or supersession note); the rest is folded here from the journal and the current fold.
|
|
30
|
+
*/
|
|
31
|
+
export declare function leadLines(events: readonly JournalEvent[], owed: OwedFold, finished: {
|
|
32
|
+
tally: string;
|
|
33
|
+
tip: string;
|
|
34
|
+
notes?: string[];
|
|
35
|
+
}): [string, string, string];
|
|
36
|
+
/**
|
|
37
|
+
* The lead at `columns`: the first row of finished, time and needs you are the first three physical
|
|
38
|
+
* lines, and every continuation follows them, naming the lead it continues.
|
|
39
|
+
*/
|
|
40
|
+
export declare const leadRows: (lead: readonly string[], columns: number) => string[];
|
|
41
|
+
export declare function renderMarkdownRecord(runId: string, events: JournalEvent[], prices?: ChannelCost[], rows?: TelemetryRow[], suite?: BaselineCommand, cwd?: string): string;
|
|
4
42
|
export declare function report(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { parseArgs } from "node:util";
|
|
3
4
|
import { ttyVisual } from "../../adapters/model-lints.js";
|
|
4
5
|
import { addUsage } from "../../adapters/types.js";
|
|
@@ -10,9 +11,11 @@ import { compareRuns } from "../../report/compare.js";
|
|
|
10
11
|
import { estimateCosts } from "../../report/cost.js";
|
|
11
12
|
import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChannelTokens, formatTokenUsage as fields, totalTokens as total, } from "../../report/operator-record.js";
|
|
12
13
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
13
|
-
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
14
|
+
import { effectiveEvents, foldOwedChecks, Journal, loadRoutingProfile, OWED_DISCHARGE_EVENT, } from "../../run/journal.js";
|
|
14
15
|
import { baselineProvenanceOf, fingerprintsOf, forgivenFingerprints, forgivenGateRow, formatBaselineProvenance, formatFingerprints, formatForgiven, formatTipProof, runEndTipProof, } from "../../run/daemon.js";
|
|
16
|
+
import { formatSpan, WALL_PRIORITY, WALL_PRIORITY_TEXT, wallBudget, wallBudgetFacts } from "../../run/wall-budget.js";
|
|
15
17
|
import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
|
|
18
|
+
import { cellWidth, wrapCells } from "../../tui/cockpit/width.js";
|
|
16
19
|
const n = (x) => x.toLocaleString("en-US"); // explicit locale — CI/darwin flake guard
|
|
17
20
|
const EM = "—";
|
|
18
21
|
const firstLine = (s) => {
|
|
@@ -75,6 +78,44 @@ const wallClock = (start, end) => {
|
|
|
75
78
|
const minutes = Math.floor(seconds / 60);
|
|
76
79
|
return minutes ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
|
|
77
80
|
};
|
|
81
|
+
// OBS-1201: the disjoint wall budget, or why the journal cannot give one — never a zeroed table.
|
|
82
|
+
const wallFacts = (events) => {
|
|
83
|
+
const budget = wallBudget(events);
|
|
84
|
+
if (!budget)
|
|
85
|
+
return [["window", "not measurable — the journal names no run-start with a readable timestamp"]];
|
|
86
|
+
return [["window", `${formatSpan(budget.wallMs)} — each instant counted once, by priority ${WALL_PRIORITY_TEXT}; task-time sums concurrent spans`], ...wallBudgetFacts(budget)];
|
|
87
|
+
};
|
|
88
|
+
// OBS-634: the four numbers the baseline capture already measured for the test command. A capture that
|
|
89
|
+
// measured nothing says so; a missing number is not measurable, never a zero.
|
|
90
|
+
const suiteTelemetry = (entry) => {
|
|
91
|
+
if (!entry)
|
|
92
|
+
return "not recorded — this run's baseline holds no test capture";
|
|
93
|
+
if (entry.infra)
|
|
94
|
+
return `not measurable — the baseline capture returned no verdict (${entry.invalidCause ?? "infra"})`;
|
|
95
|
+
if (typeof entry.durationMs !== "number" || !Number.isFinite(entry.durationMs))
|
|
96
|
+
return "not recorded — the baseline predates capture timing";
|
|
97
|
+
const unmeasured = "not measurable (the runner named no per-file durations)";
|
|
98
|
+
const sum = entry.fileDurationSumMs;
|
|
99
|
+
const parallelism = entry.impliedParallelism;
|
|
100
|
+
const longest = entry.longestFile;
|
|
101
|
+
return [
|
|
102
|
+
`wall ${formatSpan(entry.durationMs)}`,
|
|
103
|
+
`file-sum ${typeof sum === "number" ? formatSpan(sum) : unmeasured}`,
|
|
104
|
+
`implied parallelism ${typeof parallelism === "number" ? parallelism.toFixed(2) : unmeasured}`,
|
|
105
|
+
`longest file ${longest ? `${longest.file} ${formatSpan(longest.durationMs)}` : unmeasured}`,
|
|
106
|
+
...(typeof entry.fileCount === "number" ? [`${entry.fileCount} files`] : []),
|
|
107
|
+
].join(" · ");
|
|
108
|
+
};
|
|
109
|
+
/** The run's own baseline.json test entry; absent or unreadable reads as not recorded. */
|
|
110
|
+
const baselineTestEntry = (runDir) => {
|
|
111
|
+
try {
|
|
112
|
+
const baseline = JSON.parse(readFileSync(join(runDir, "baseline.json"), "utf8"));
|
|
113
|
+
return baseline.commands?.test;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
78
119
|
const detail = (value) => typeof value === "string" || typeof value === "number" ? String(value) : EM;
|
|
79
120
|
// T11: a gate that declined never ran. Drop those before the comparison metrics so the pass rate
|
|
80
121
|
// counts only gates that actually ran — a declined gate is never a pass and never pads the base.
|
|
@@ -89,6 +130,192 @@ const supersession = (events) => {
|
|
|
89
130
|
const supersedes = typeof start?.data.supersedes === "string" ? start.data.supersedes : undefined;
|
|
90
131
|
return { ...(typeof by === "string" ? { supersededBy: by } : {}), ...(supersedes ? { supersedes } : {}) };
|
|
91
132
|
};
|
|
133
|
+
// ── CG1 (v2.6.4): the lead — what finished, where the wall went, what needs you NOW ──────────────
|
|
134
|
+
// Status and report open with the same three lines, folded here once. Finished work and first pass
|
|
135
|
+
// come from this journal's own lineage; time is the existing disjoint wall partition (never summed
|
|
136
|
+
// task-time); outstanding is the CURRENT owed-check fold, re-read on every call, never the run-end copy.
|
|
137
|
+
// Rows that prove a task's lineage was not one uninterrupted dispatch: a park, an approval, a failure,
|
|
138
|
+
// a repair, or a restart restoring it.
|
|
139
|
+
const INTERRUPTED = new Set(["task-human", "task-failed", "task-approved", "repair-dispatch", "resume-restore"]);
|
|
140
|
+
/**
|
|
141
|
+
* End-to-end first pass over the finished tasks of the SELECTED journal (no cross-run scan). A task
|
|
142
|
+
* closed done, merged, or named done by the newest run-end passes only as one dispatch at attempt 0
|
|
143
|
+
* straight to its merge. A task whose predecessor or dispatch evidence is missing — no run-start, a run
|
|
144
|
+
* that supersedes another, no dispatch, a first dispatch not at attempt 0 or not recording lifetime
|
|
145
|
+
* worker dispatch ordinal 0 — is unknown whatever else it shows: attempts reset at every release, so
|
|
146
|
+
* only the lifetime ordinal proves no earlier dispatch is missing. Otherwise any park, approval, failure, repair, restore or second dispatch is a known miss — a
|
|
147
|
+
* run-end summary's park or a restart before its merge included — and a would-be pass with no merge is
|
|
148
|
+
* unknown, never counted as a pass.
|
|
149
|
+
*/
|
|
150
|
+
export function endToEndFirstPass(events) {
|
|
151
|
+
const start = events.find((e) => e.event === "run-start");
|
|
152
|
+
const predecessorUnread = !start || typeof start.data.supersedes === "string";
|
|
153
|
+
const runEnd = [...events].reverse().find((e) => e.event === "run-end");
|
|
154
|
+
const finished = new Set([
|
|
155
|
+
...events.flatMap((e) => e.taskId && (e.event === "task-done" || e.event === "merge") ? [e.taskId] : []),
|
|
156
|
+
...(Array.isArray(runEnd?.data.done) ? runEnd.data.done.map(String) : []),
|
|
157
|
+
]);
|
|
158
|
+
const fp = { passed: 0, known: 0, unknown: 0 };
|
|
159
|
+
const names = (e, buckets, taskId) => e.event === "run-end"
|
|
160
|
+
&& buckets.some((b) => Array.isArray(e.data[b]) && e.data[b].map(String).includes(taskId));
|
|
161
|
+
for (const taskId of finished) {
|
|
162
|
+
const own = events.filter((e) => e.taskId === taskId);
|
|
163
|
+
const dispatches = own.filter((e) => e.event === "task-dispatch");
|
|
164
|
+
// Run-level rows name the task too: a run-end summary parking or failing it is a park wherever it
|
|
165
|
+
// sits, and between its first dispatch and its merge any not-done bucket or a restart means the
|
|
166
|
+
// lineage crossed an engagement boundary — never one uninterrupted dispatch.
|
|
167
|
+
const at = events.findIndex((e) => e.taskId === taskId && e.event === "task-dispatch");
|
|
168
|
+
const end = events.findIndex((e) => e.taskId === taskId && e.event === "merge");
|
|
169
|
+
const span = at < 0 ? [] : events.slice(at, end < 0 ? undefined : end);
|
|
170
|
+
const crossed = events.some((e) => names(e, ["human", "failed"], taskId))
|
|
171
|
+
|| span.some((e) => e.event === "run-resume" || names(e, ["blocked", "pending"], taskId));
|
|
172
|
+
// Missing predecessor or dispatch evidence is unknown BEFORE any outcome is classified: a park read
|
|
173
|
+
// without its initial dispatch (none, a first one past attempt 0, or one whose lifetime ordinal is
|
|
174
|
+
// unrecorded or past 0), or in a run continuing an unread predecessor, is no known miss either.
|
|
175
|
+
const origin = dispatches[0]?.data;
|
|
176
|
+
if (predecessorUnread || origin?.attempt !== 0 || origin.workerDispatchOrdinal !== 0)
|
|
177
|
+
fp.unknown++;
|
|
178
|
+
else if (dispatches.length > 1 || crossed || own.some((e) => INTERRUPTED.has(e.event)))
|
|
179
|
+
fp.known++;
|
|
180
|
+
else if (!own.some((e) => e.event === "merge"))
|
|
181
|
+
fp.unknown++;
|
|
182
|
+
else {
|
|
183
|
+
fp.passed++;
|
|
184
|
+
fp.known++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return fp;
|
|
188
|
+
}
|
|
189
|
+
export const firstPassText = ({ passed, known, unknown }) => known === 0 && unknown > 0
|
|
190
|
+
? `end-to-end first pass unknown (${unknown} without lineage)`
|
|
191
|
+
: `end-to-end first pass ${passed}/${known}${unknown ? ` + ${unknown} unknown` : ""}`;
|
|
192
|
+
/** The CURRENT owed-check fold. `cwd` is any checkout of the run's repository. */
|
|
193
|
+
export const currentOwed = (events, cwd = process.cwd()) => foldOwedChecks(events, cwd);
|
|
194
|
+
export const outstandingText = (fold) => fold.known
|
|
195
|
+
? `outstanding ${fold.debt}${fold.outstanding.length ? ` (${fold.outstanding.map((o) => `${o.taskId} ${o.gate}`).join(", ")})` : ""}`
|
|
196
|
+
: "outstanding unknown";
|
|
197
|
+
// A run-end summary's buckets read as the lifecycle row each names; later effective rows move them on.
|
|
198
|
+
const SUMMARY_STATE = { done: "task-done", failed: "task-failed", human: "task-human", blocked: "task-blocked", pending: "task-pending" };
|
|
199
|
+
/**
|
|
200
|
+
* Tasks the CURRENT lifecycle leaves waiting on the operator: every run-end summary sets the tasks its
|
|
201
|
+
* buckets name, and each effective lifecycle row after it moves its task on — so a summary naming a
|
|
202
|
+
* park or failure the task rows never wrote still counts, and a later approval or merge clears it.
|
|
203
|
+
*/
|
|
204
|
+
const waitingOnYou = (events) => {
|
|
205
|
+
const last = new Map();
|
|
206
|
+
for (const e of effectiveEvents(events)) {
|
|
207
|
+
if (e.event === "run-end") {
|
|
208
|
+
for (const [bucket, state] of Object.entries(SUMMARY_STATE)) {
|
|
209
|
+
if (Array.isArray(e.data[bucket]))
|
|
210
|
+
for (const id of e.data[bucket])
|
|
211
|
+
last.set(String(id), state);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (e.taskId && ["task-dispatch", "task-done", "task-failed", "task-human", "task-approved", "merge"].includes(e.event))
|
|
215
|
+
last.set(e.taskId, e.event);
|
|
216
|
+
}
|
|
217
|
+
const states = [...last.values()];
|
|
218
|
+
return { parked: states.filter((s) => s === "task-human").length, failed: states.filter((s) => s === "task-failed").length };
|
|
219
|
+
};
|
|
220
|
+
export const needsYouText = (events, owed) => {
|
|
221
|
+
const { parked, failed } = waitingOnYou(events);
|
|
222
|
+
const why = owed.known ? "" : ` · unknown: ${owed.unknown[0]?.reason ?? "owed checks unreadable"}`;
|
|
223
|
+
return `needs you: ${outstandingText(owed)} · ${parked} parked · ${failed} failed${why}`;
|
|
224
|
+
};
|
|
225
|
+
/** The wall window alone, for the one-line form: the same partition, never task-time. */
|
|
226
|
+
export const wallText = (events) => {
|
|
227
|
+
const budget = wallBudget(events);
|
|
228
|
+
return budget ? `${formatSpan(budget.wallMs)} wall` : "wall not measurable";
|
|
229
|
+
};
|
|
230
|
+
/** Time: the existing disjoint wall partition's window and its largest exposed buckets — never task-time. */
|
|
231
|
+
const timeLead = (events) => {
|
|
232
|
+
const budget = wallBudget(events);
|
|
233
|
+
if (!budget)
|
|
234
|
+
return "time not measurable — the journal names no run-start with a readable timestamp";
|
|
235
|
+
const top = [...WALL_PRIORITY.map((bucket) => [bucket, budget.exposedMs[bucket]]), ["residual", budget.residualMs]]
|
|
236
|
+
.filter(([, ms]) => ms > 0).sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
237
|
+
return `time ${formatSpan(budget.wallMs)} wall, each instant once${top.length ? `: ${top.map(([bucket, ms]) => `${bucket} ${formatSpan(ms)}`).join(" · ")}` : ""}`;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* The three lead lines. `finished` is the surface's own done tally and tip reading (plus any
|
|
241
|
+
* comparability or supersession note); the rest is folded here from the journal and the current fold.
|
|
242
|
+
*/
|
|
243
|
+
export function leadLines(events, owed, finished) {
|
|
244
|
+
return [
|
|
245
|
+
`finished ${[finished.tally, finished.tip, firstPassText(endToEndFirstPass(events)), ...(finished.notes ?? [])].join(" · ")}`,
|
|
246
|
+
timeLead(events),
|
|
247
|
+
needsYouText(events, owed),
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A lead line packed into `columns` at its ` · ` fact boundaries: a fact that does not fit beside the
|
|
252
|
+
* last moves whole to a `prefix`ed continuation row, so narrowing costs rows and never drops a fact.
|
|
253
|
+
*/
|
|
254
|
+
const wrapFacts = (line, columns, prefix) => {
|
|
255
|
+
const rows = [];
|
|
256
|
+
// Read fact by fact without a hand-split (the learning-section source pin forbids one here).
|
|
257
|
+
for (const [, fact] of line.matchAll(/(?:^| · )(.+?)(?= · |$)/gu)) {
|
|
258
|
+
const last = rows.at(-1);
|
|
259
|
+
if (last !== undefined && cellWidth(`${last} · ${fact}`) <= columns)
|
|
260
|
+
rows[rows.length - 1] = `${last} · ${fact}`;
|
|
261
|
+
else
|
|
262
|
+
rows.push(...wrapCells(last === undefined ? fact : `${prefix}${fact}`, columns, { continuationPrefix: " " }));
|
|
263
|
+
}
|
|
264
|
+
return rows;
|
|
265
|
+
};
|
|
266
|
+
const LEAD_LABELS = ["finished", "time", "needs you"];
|
|
267
|
+
/**
|
|
268
|
+
* The lead at `columns`: the first row of finished, time and needs you are the first three physical
|
|
269
|
+
* lines, and every continuation follows them, naming the lead it continues.
|
|
270
|
+
*/
|
|
271
|
+
export const leadRows = (lead, columns) => {
|
|
272
|
+
const wrapped = lead.map((line, i) => wrapFacts(line, columns, ` ${LEAD_LABELS[i]}: `));
|
|
273
|
+
return [...wrapped.map((rows) => rows[0]), ...wrapped.flatMap((rows) => rows.slice(1))];
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* The discharge rows the current fold validates ON THEIR OWN: each is folded alone, at its own journal
|
|
277
|
+
* position among every non-discharge row, so one valid proof never lends its validity to a refused
|
|
278
|
+
* sibling naming the same check, and a proof written before its obligation is never validated by it.
|
|
279
|
+
*/
|
|
280
|
+
const validatedDischarges = (events, cwd) => {
|
|
281
|
+
return events.filter((e) => e.event === OWED_DISCHARGE_EVENT).flatMap((row) => {
|
|
282
|
+
const ids = Array.isArray(row.data.ids) ? row.data.ids.map(String) : [];
|
|
283
|
+
// In journal order: a discharge that precedes its obligation stays where it was written, never moved after it.
|
|
284
|
+
const fold = currentOwed(events.filter((e) => e.event !== OWED_DISCHARGE_EVENT || e === row), cwd);
|
|
285
|
+
if (!ids.length || !ids.every((id) => fold.discharged.includes(id)))
|
|
286
|
+
return [];
|
|
287
|
+
return [{ row, gates: ids.map((id) => fold.acceptedRisk.find((o) => o.id === id)?.gate ?? "unknown") }];
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
/** A leg2 row's own reviewer fact: top-level, or under the verify gate row's meta it spreads. */
|
|
291
|
+
const leg2Seat = (data, key) => data[key] ?? (typeof data.meta === "object" && data.meta !== null ? data.meta[key] : undefined);
|
|
292
|
+
/**
|
|
293
|
+
* A review-leg2 row supersedes the daemon review only through its OWN validated discharge of a review
|
|
294
|
+
* check: a passing row whose artifact was captured, bound to that discharge's artifact and range, and
|
|
295
|
+
* naming the very reviewer seat (key and vendor) the discharge validated. A failed or capture-failed
|
|
296
|
+
* row, or one naming another seat, never borrows a sibling's proof.
|
|
297
|
+
*/
|
|
298
|
+
const leg2Discharge = (leg2, validated) => {
|
|
299
|
+
const d = leg2.data;
|
|
300
|
+
if (d.pass !== true || d.artifactAvailability !== "available" || typeof d.artifactSha256 !== "string")
|
|
301
|
+
return undefined;
|
|
302
|
+
return validated.find(({ row, gates }) => {
|
|
303
|
+
const seat = row.data.reviewer;
|
|
304
|
+
return row.taskId === leg2.taskId && gates.includes("review")
|
|
305
|
+
&& row.data.artifactSha256 === d.artifactSha256 && row.data.artifactPath === d.artifactPath
|
|
306
|
+
&& row.data.head === d.head && row.data.mergeBase === d.mergeBase
|
|
307
|
+
&& typeof seat?.key === "string" && leg2Seat(d, "reviewer") === seat.key && leg2Seat(d, "vendor") === seat.vendor;
|
|
308
|
+
})?.row;
|
|
309
|
+
};
|
|
310
|
+
const RUN_END_BUCKETS = ["done", "failed", "human", "blocked", "pending"];
|
|
311
|
+
/** The record's done tally: tasks closed done in the journal or its newest run-end, over every task it names. */
|
|
312
|
+
const recordTally = (events) => {
|
|
313
|
+
const runEnd = [...events].reverse().find((e) => e.event === "run-end");
|
|
314
|
+
const bucket = (key) => Array.isArray(runEnd?.data[key]) ? runEnd.data[key].map(String) : [];
|
|
315
|
+
const done = new Set([...bucket("done"), ...events.flatMap((e) => e.taskId && (e.event === "task-done" || e.event === "merge") ? [e.taskId] : [])]);
|
|
316
|
+
const all = new Set([...done, ...RUN_END_BUCKETS.flatMap(bucket), ...events.flatMap((e) => e.taskId && e.event.startsWith("task-") ? [e.taskId] : [])]);
|
|
317
|
+
return `${done.size}/${all.size} done`;
|
|
318
|
+
};
|
|
92
319
|
const taskIds = (events) => {
|
|
93
320
|
const seen = new Set();
|
|
94
321
|
const out = [];
|
|
@@ -194,7 +421,7 @@ const fingerprintClauses = (data) => {
|
|
|
194
421
|
+ `; forgiven vs baseline: ${formatFingerprints(forgiven)} — ${formatBaselineProvenance(baselineProvenanceOf(data.baselineProvenance))}`;
|
|
195
422
|
};
|
|
196
423
|
// VIS-07 / REC-01: derived only from the run journal, telemetry, and local configuration.
|
|
197
|
-
export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
424
|
+
export function renderMarkdownRecord(runId, events, prices = [], rows = [], suite, cwd) {
|
|
198
425
|
const runStart = events.find((e) => e.event === "run-start");
|
|
199
426
|
const runEnd = [...events].reverse().find((e) => e.event === "run-end");
|
|
200
427
|
const baseRef = typeof runStart?.data.baseRef === "string" ? runStart.data.baseRef : EM;
|
|
@@ -218,7 +445,11 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
218
445
|
if (!usageLines.length)
|
|
219
446
|
usageLines.push("- **not recorded:** attempts/windows: not measurable; tokens: not measurable; price: not measurable");
|
|
220
447
|
const sup = supersession(events);
|
|
448
|
+
const owed = currentOwed(events, cwd);
|
|
449
|
+
const validated = validatedDischarges(events, cwd);
|
|
221
450
|
const lines = [
|
|
451
|
+
...leadLines(events, owed, { tally: recordTally(events), tip: `tip verify ${verificationOf(runId, events)}` }).map((line) => `- ${line}`),
|
|
452
|
+
"",
|
|
222
453
|
`# tickmarkr engagement`,
|
|
223
454
|
"",
|
|
224
455
|
`- **runId:** ${runId}`,
|
|
@@ -237,11 +468,17 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
237
468
|
"",
|
|
238
469
|
...usageLines,
|
|
239
470
|
`- **wall-clock:** ${wallClock(runStart, runEnd)}`,
|
|
240
|
-
`- **
|
|
471
|
+
`- **end-to-end first pass:** ${firstPassText(endToEndFirstPass(events)).replace("end-to-end first pass ", "")} — this journal's dispatch, park, approval and merge lineage`,
|
|
472
|
+
`- **first-attempt rate (engagement-local telemetry):** ${firstAttemptRate} — attempts restart at every resume`,
|
|
241
473
|
`- **gate failures:** ${[...gateFailures.entries()].map(([gate, failures]) => `${gate}: ${failures}`).join(", ") || "none recorded"}`,
|
|
242
474
|
`- **consults:** ${events.filter((e) => e.event === "consult-verdict").length}`,
|
|
243
475
|
`- **escalations:** ${events.filter((e) => e.event === "escalation").length}`,
|
|
244
476
|
"",
|
|
477
|
+
"## Wall budget",
|
|
478
|
+
"",
|
|
479
|
+
...wallFacts(events).map(([label, fact]) => `- **${label}:** ${fact}`),
|
|
480
|
+
`- **suite telemetry (baseline test capture):** ${suiteTelemetry(suite)}`,
|
|
481
|
+
"",
|
|
245
482
|
];
|
|
246
483
|
lines.push("## Channels", "");
|
|
247
484
|
const operatorRecords = buildOperatorRecord(events, prices);
|
|
@@ -304,7 +541,12 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
304
541
|
const identityText = identity
|
|
305
542
|
? `reviewer ${identity.reviewer}${identity.vendor ? ` (vendor: ${identity.vendor}; provider: ${identity.provider})` : ` (provider: ${identity.provider})`}`
|
|
306
543
|
: firstLine(row.data.details);
|
|
307
|
-
|
|
544
|
+
// CG1: only this row's own validated discharge of a review check lets it supersede the daemon review.
|
|
545
|
+
const discharge = leg2Discharge(row, validated);
|
|
546
|
+
const claim = discharge
|
|
547
|
+
? `supersedes daemon review — validated discharge ${discharge.data.ids.map(String).join(", ")}`
|
|
548
|
+
: "unvalidated — no validated owed-check discharge; the daemon review stands";
|
|
549
|
+
lines.push(` - review-leg2: ${pass} (${claim}) — ${identityText}`);
|
|
308
550
|
}
|
|
309
551
|
}
|
|
310
552
|
else
|
|
@@ -326,7 +568,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
326
568
|
}
|
|
327
569
|
return lines.join("\n").trimEnd() + "\n";
|
|
328
570
|
}
|
|
329
|
-
function textReport(runId, events, rows, cwd) {
|
|
571
|
+
function textReport(runId, events, rows, cwd, suite) {
|
|
330
572
|
// one group per adapter:model, carrying channel + the folded usage across its rows
|
|
331
573
|
const groups = new Map();
|
|
332
574
|
for (const r of rows) {
|
|
@@ -377,17 +619,28 @@ function textReport(runId, events, rows, cwd) {
|
|
|
377
619
|
const consults = events.filter((e) => e.event === "consult-verdict").length;
|
|
378
620
|
const failovers = events.filter((e) => e.event === "quota-failover").length;
|
|
379
621
|
const sup = supersession(events);
|
|
622
|
+
// CG1: at a configured terminal width the lead wraps its continuations after the three lead lines;
|
|
623
|
+
// report() fits every other row of the complete output (fitColumns).
|
|
624
|
+
const lead = leadLines(events, currentOwed(events, cwd), { tally: recordTally(events), tip: `tip verify ${verificationOf(runId, events)}` });
|
|
625
|
+
const columns = process.stdout.columns;
|
|
380
626
|
return [
|
|
627
|
+
...(columns ? leadRows(lead, columns) : lead),
|
|
381
628
|
`tickmarkr engagement — ${runId}`,
|
|
382
629
|
...(sup.supersededBy ? [`superseded by ${sup.supersededBy}`] : []),
|
|
383
630
|
...(sup.supersedes ? [`supersedes ${sup.supersedes}`] : []),
|
|
384
631
|
"",
|
|
385
|
-
|
|
632
|
+
// CG1: the telemetry rows are engagement-local — attempts restart at every resume; the lead's
|
|
633
|
+
// end-to-end first pass is the lineage reading.
|
|
634
|
+
"engagement summary — audit trail: engagement-local telemetry, attempts restart at every resume",
|
|
386
635
|
...[...groups.entries()].map(([k, g]) => ` ${k.padEnd(30)} tasks ${g.rows.length}, attempts ${g.rows.reduce((s, r) => s + r.attempts, 0)}, done ${g.rows.filter((r) => r.outcome === "done").length}`),
|
|
387
636
|
"",
|
|
388
637
|
`tickmark rate: ${gateRan ? Math.round((100 * gatePass) / gateRan) : 0}% (${gatePass}/${gateRan})${declinedCount ? ` · declined: ${declinedCount}` : ""}`,
|
|
389
638
|
`escalations: ${escalations} · National Office consults: ${consults} · quota failovers: ${failovers}`,
|
|
390
639
|
"",
|
|
640
|
+
"wall budget — exposed wall, disjoint:",
|
|
641
|
+
...wallFacts(events).map(([label, fact]) => ` ${label.padEnd(14)} ${fact}`),
|
|
642
|
+
` ${"suite".padEnd(14)} ${suiteTelemetry(suite)}`,
|
|
643
|
+
"",
|
|
391
644
|
"spend — tokens (measured where observed):",
|
|
392
645
|
...tokenLines,
|
|
393
646
|
"spend — money:",
|
|
@@ -399,6 +652,14 @@ function textReport(runId, events, rows, cwd) {
|
|
|
399
652
|
` probes this run (route-deviation explore): ${probes}`,
|
|
400
653
|
].join("\n");
|
|
401
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* CG1: at a configured terminal width EVERY row of the complete text output fits it — the lead, the
|
|
657
|
+
* body, a --bundle receipt and --compare output alike: a row that overflows wraps beneath its own
|
|
658
|
+
* indent, no fact dropped; a row that already fits (the lead's own layout) is untouched.
|
|
659
|
+
*/
|
|
660
|
+
const fitColumns = (out, columns = process.stdout.columns) => columns
|
|
661
|
+
? out.replace(/^.*$/gmu, (line) => wrapCells(line, columns, { continuationPrefix: `${/^ */u.exec(line)[0]} ` }).join("\n"))
|
|
662
|
+
: out;
|
|
402
663
|
// T4 (v1.50): TTY-only brand pass over the text report — title frame + dim section chrome; row
|
|
403
664
|
// text and alignment untouched (the doctor/status system). Gated on ttyVisual(): the non-TTY
|
|
404
665
|
// surface returns untouched, and --md never styles (the record is a document surface).
|
|
@@ -406,8 +667,8 @@ const stylizeReport = (out) => {
|
|
|
406
667
|
if (!ttyVisual())
|
|
407
668
|
return out;
|
|
408
669
|
return out
|
|
409
|
-
.replace(
|
|
410
|
-
.replace(/^(engagement summary — audit trail
|
|
670
|
+
.replace(/^tickmarkr engagement — .*$/m, (head) => `${title(head)}\n${rule()}`) // non-global /m ⇒ the title line only
|
|
671
|
+
.replace(/^(engagement summary — audit trail:[^\n]*|wall budget — [^\n]*|spend — tokens[^\n]*|spend — money:|learning \([^\n]*)$/gm, (l) => dim(l));
|
|
411
672
|
};
|
|
412
673
|
export async function report(argv, cwd = process.cwd()) {
|
|
413
674
|
const { values, positionals } = parseArgs({
|
|
@@ -428,6 +689,7 @@ export async function report(argv, cwd = process.cwd()) {
|
|
|
428
689
|
const events = j.read();
|
|
429
690
|
const rows = j.readTelemetry();
|
|
430
691
|
const cfg = loadConfig(cwd);
|
|
692
|
+
const suite = baselineTestEntry(j.dir);
|
|
431
693
|
let bundleNote = "";
|
|
432
694
|
if (values.bundle) {
|
|
433
695
|
// Local-only write of the pure proof packet — no network path exists in buildProofBundle.
|
|
@@ -453,8 +715,13 @@ export async function report(argv, cwd = process.cwd()) {
|
|
|
453
715
|
throw new Error(outcome.reason);
|
|
454
716
|
comparison = "\n" + outcome.text;
|
|
455
717
|
}
|
|
718
|
+
// CG1: the bundle receipt follows the three lead lines and their wrapped continuations, never above
|
|
719
|
+
// them (in markdown, after the blank line that closes the lead list, so it never joins its last item).
|
|
720
|
+
const afterLead = (out, at = 3) => bundleNote
|
|
721
|
+
? out.replace(new RegExp(`^(?:[^\\n]*\\n){${at}}(?: [^\\n]*\\n)*`, "u"), (lead) => `${lead}${bundleNote.trimEnd()}\n`)
|
|
722
|
+
: out;
|
|
456
723
|
if (values.md) {
|
|
457
|
-
return
|
|
724
|
+
return afterLead(renderMarkdownRecord(runId, events, estimateCosts(rows, cfg.cost), rows, suite, cwd), 4) + comparison;
|
|
458
725
|
}
|
|
459
|
-
return
|
|
726
|
+
return fitColumns(afterLead(stylizeReport(textReport(runId, events, rows, cwd, suite))) + comparison);
|
|
460
727
|
}
|
|
@@ -5,10 +5,8 @@ import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } fr
|
|
|
5
5
|
import { loadGraph } from "../../graph/graph.js";
|
|
6
6
|
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
7
7
|
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
8
|
-
import { narrationSink, bindNarration } from "./run.js";
|
|
8
|
+
import { finalRail, narrationSink, bindNarration, summaryGreen } from "./run.js";
|
|
9
9
|
import { assertRefsWritable } from "../../run/git.js";
|
|
10
|
-
const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
11
|
-
&& s.tipVerify !== "failed";
|
|
12
10
|
export async function resume(argv, cwd = process.cwd()) {
|
|
13
11
|
const { values, positionals } = parseArgs({
|
|
14
12
|
args: argv,
|
|
@@ -56,6 +54,6 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
56
54
|
// the operator named, so a resumed run's lifecycle rows name THIS run and not a generic word.
|
|
57
55
|
narrate,
|
|
58
56
|
});
|
|
59
|
-
const out = `resumed ${s.runId} — ${formatSummary(s)}`;
|
|
57
|
+
const out = `resumed ${s.runId} — ${formatSummary(s)}${finalRail(s, cwd)}`;
|
|
60
58
|
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
61
59
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutorDriver } from "../../drivers/types.js";
|
|
2
|
+
import { type RunSummary } from "../../run/daemon.js";
|
|
2
3
|
import { type JournalEvent } from "../../run/journal.js";
|
|
3
4
|
/** Closed repetitive set the rail suppresses on a TTY. An ungated `phase-start` joins them below —
|
|
4
5
|
* it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
|
|
@@ -64,6 +65,16 @@ export declare const narrationSink: (runId: string) => (event: JournalEvent) =>
|
|
|
64
65
|
* wrap their driver here so neither can forget the binding.
|
|
65
66
|
*/
|
|
66
67
|
export declare function bindNarration<D extends ExecutorDriver>(driver: D, narrate: (event: JournalEvent) => void): D;
|
|
68
|
+
/** Execution green: the exit-code predicate run and resume share. Users' scripts read it; debt never moves it. */
|
|
69
|
+
export declare const summaryGreen: (s: RunSummary) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* CG2 (v2.6.4): the last line of a run or resume, read from the CURRENT owed-check fold after the
|
|
72
|
+
* daemon returns — never the run-end row's copy, which a later `verify --record` discharge leaves
|
|
73
|
+
* historical. "verified" requires execution green AND outstanding empty AND known; an execution-green
|
|
74
|
+
* run that owes a check, or whose debt is unknown, says so and is not green. A partial or failed run
|
|
75
|
+
* adds nothing: its summary keeps its own wording and is never called complete.
|
|
76
|
+
*/
|
|
77
|
+
export declare function finalRail(s: RunSummary, cwd: string): string;
|
|
67
78
|
export declare function run(argv: string[], cwd?: string): Promise<{
|
|
68
79
|
out: string;
|
|
69
80
|
code: number;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -6,10 +6,11 @@ import { loadGraph, onDiskSpecHash, readCompileRefusal } from "../../graph/graph
|
|
|
6
6
|
import { formatSummary, resolveRunMode, runDaemon } from "../../run/daemon.js";
|
|
7
7
|
import { isRunLockLive } from "../../run/lock.js";
|
|
8
8
|
import { route, NO_EXPLORE_ENV } from "../../route/router.js";
|
|
9
|
-
import { formatJournalNarration, loadRoutingProfile, newRunId } from "../../run/journal.js";
|
|
9
|
+
import { formatJournalNarration, Journal, loadRoutingProfile, newRunId } from "../../run/journal.js";
|
|
10
10
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
11
11
|
import { GLYPHS, LIVE } from "../../brand.js";
|
|
12
12
|
import { cellWidth, fitCells } from "../../tui/cockpit/width.js";
|
|
13
|
+
import { currentOwed, outstandingText } from "./report.js";
|
|
13
14
|
import { assertRefsWritable } from "../../run/git.js";
|
|
14
15
|
// ── the operator event rail (v1.99 T2) ──────────────────────────────────────────────────────────
|
|
15
16
|
// A run's TTY narration is an operator EVENT RAIL, not the journal dump it used to echo. The
|
|
@@ -166,17 +167,25 @@ const bucket = (value) => (Array.isArray(value) ? value.length : 0);
|
|
|
166
167
|
*
|
|
167
168
|
* a fatal crash, a failed task or a failed integration tip is a FAILURE;
|
|
168
169
|
* an incomplete run — parked, blocked or still-pending work — wants ATTENTION;
|
|
170
|
+
* so does a complete execution that owes a check, or whose owed-check fold is unknown (CG2);
|
|
169
171
|
* only a run with none of those is a pass.
|
|
170
172
|
*
|
|
171
173
|
* A rail that paints the terminal record by the same predicate the exit code uses can never show a
|
|
172
|
-
* green tickmark over a run whose exit code is 2
|
|
174
|
+
* green tickmark over a run whose exit code is 2 — and, since the exit code is execution-based, the
|
|
175
|
+
* debt clause is the one place the tone is stricter than it: exit 0 with an owed check is not green.
|
|
176
|
+
* The fold is the one this row carries, reduced at its own append: live, that IS the current fold.
|
|
173
177
|
*/
|
|
174
178
|
const runEndTone = (data) => {
|
|
175
179
|
if (data.fatal === true || bucket(data.failed) > 0 || data.tipVerify === "failed")
|
|
176
180
|
return "fail";
|
|
177
181
|
if (bucket(data.human) + bucket(data.blocked) + bucket(data.pending) > 0)
|
|
178
182
|
return "attention";
|
|
179
|
-
return "pass";
|
|
183
|
+
return recordedDebt(data) === 0 ? "pass" : "attention";
|
|
184
|
+
};
|
|
185
|
+
/** The outstanding count a run-end record's own fold carries; undefined when it carries none or an unknown one. */
|
|
186
|
+
const recordedDebt = (data) => {
|
|
187
|
+
const owed = data.owedChecks;
|
|
188
|
+
return owed?.known === true && Array.isArray(owed.outstanding) ? owed.outstanding.length : undefined;
|
|
180
189
|
};
|
|
181
190
|
/** A gate row's non-failure is spelled across `pass`, `skipped`, `verdict` and `infra`, and a row may
|
|
182
191
|
* state NONE of them (src/run/outcome.ts) — so the rail reads the canonical outcome rather than
|
|
@@ -330,6 +339,8 @@ const RAIL_PROJECTION = {
|
|
|
330
339
|
"tip-moved": (d) => typeof d.gatedCommit === "string" && typeof d.branchTip === "string"
|
|
331
340
|
? `gated ${d.gatedCommit.slice(0, 12)}, tip ${d.branchTip.slice(0, 12)}`
|
|
332
341
|
: undefined,
|
|
342
|
+
// CG2: the terminal row names its debt — a legacy record without a fold is unknown, never zero
|
|
343
|
+
"run-end": (d) => `outstanding ${recordedDebt(d) ?? "unknown"}`,
|
|
333
344
|
"trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
|
|
334
345
|
// SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
|
|
335
346
|
// would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
|
|
@@ -420,8 +431,25 @@ export function bindNarration(driver, narrate) {
|
|
|
420
431
|
driver.narrateWith?.(narrate);
|
|
421
432
|
return driver;
|
|
422
433
|
}
|
|
423
|
-
|
|
434
|
+
/** Execution green: the exit-code predicate run and resume share. Users' scripts read it; debt never moves it. */
|
|
435
|
+
export const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
424
436
|
&& s.tipVerify !== "failed";
|
|
437
|
+
/**
|
|
438
|
+
* CG2 (v2.6.4): the last line of a run or resume, read from the CURRENT owed-check fold after the
|
|
439
|
+
* daemon returns — never the run-end row's copy, which a later `verify --record` discharge leaves
|
|
440
|
+
* historical. "verified" requires execution green AND outstanding empty AND known; an execution-green
|
|
441
|
+
* run that owes a check, or whose debt is unknown, says so and is not green. A partial or failed run
|
|
442
|
+
* adds nothing: its summary keeps its own wording and is never called complete.
|
|
443
|
+
*/
|
|
444
|
+
export function finalRail(s, cwd) {
|
|
445
|
+
if (!summaryGreen(s))
|
|
446
|
+
return "";
|
|
447
|
+
const owed = currentOwed(Journal.open(cwd, s.runId).read(), cwd);
|
|
448
|
+
if (owed.known && owed.outstanding.length === 0)
|
|
449
|
+
return `\nverified — execution complete, ${outstandingText(owed)}`;
|
|
450
|
+
const why = owed.known ? "" : ` (${owed.unknown[0]?.reason ?? "owed checks unreadable"})`;
|
|
451
|
+
return `\nexecution complete; ${outstandingText(owed)}${why} — not green until outstanding is empty and known`;
|
|
452
|
+
}
|
|
425
453
|
// v1.51 T2: --quality is a pure compatibility alias for `--mode partner-led` (this run only). It
|
|
426
454
|
// carries no one-band floor raise of its own — and since the OBS-89 rip (v1.60) route() no longer
|
|
427
455
|
// reads the retired TICKMARKR_QUALITY env at all, so no downstream code can raise a floor on its
|
|
@@ -526,7 +554,7 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
526
554
|
supersedes: values.supersedes,
|
|
527
555
|
narrate,
|
|
528
556
|
});
|
|
529
|
-
const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
|
|
557
|
+
const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)${finalRail(s, cwd)}`;
|
|
530
558
|
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
531
559
|
}
|
|
532
560
|
finally {
|