tickmarkr 2.6.1 → 2.6.3
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 +16 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +1 -0
- package/dist/adapters/prompt.js +14 -6
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +12 -4
- package/dist/adapters/types.js +6 -0
- package/dist/cli/commands/approve.d.ts +11 -4
- package/dist/cli/commands/approve.js +82 -27
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +87 -11
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +2 -1
- package/dist/cli/commands/report.js +74 -8
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +43 -20
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/compile/native.js +7 -0
- package/dist/config/config.d.ts +35 -2
- package/dist/config/config.js +86 -10
- package/dist/config/fleet-overlay.d.ts +13 -2
- package/dist/config/fleet-overlay.js +60 -0
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +35 -2
- package/dist/drivers/orca.js +222 -67
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.js +31 -4
- package/dist/gates/baseline.d.ts +32 -2
- package/dist/gates/baseline.js +111 -24
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +11 -4
- package/dist/gates/llm.js +40 -21
- package/dist/gates/review.d.ts +14 -1
- package/dist/gates/review.js +160 -34
- package/dist/gates/run-gates.d.ts +56 -4
- package/dist/gates/run-gates.js +358 -58
- package/dist/gates/test-manifest.d.ts +45 -1
- package/dist/gates/test-manifest.js +78 -12
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/router.d.ts +12 -1
- package/dist/route/router.js +56 -24
- package/dist/run/consult.d.ts +15 -1
- package/dist/run/consult.js +18 -7
- package/dist/run/daemon.d.ts +38 -2
- package/dist/run/daemon.js +895 -192
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +152 -3
- package/dist/run/journal.js +551 -50
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +46 -1
- package/dist/run/protocol.js +14 -2
- 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/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/board.js +3 -3
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.js +13 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +27 -2
- package/dist/tui/cockpit/run-view.d.ts +9 -2
- package/dist/tui/cockpit/run-view.js +66 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
- package/dist/tui/cockpit/setup-cockpit.js +10 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +3 -1
- package/schema/config.schema.json +825 -0
- package/skills/tickmarkr-loop/SKILL.md +15 -3
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { gateDeclined } from "../report/bundle.js";
|
|
2
|
+
/**
|
|
3
|
+
* OBS-1201: where a run's wall time went. Every instant of the engagement window lands in exactly ONE
|
|
4
|
+
* bucket — the first of WALL_PRIORITY with a span covering it — and what no span covers is residual,
|
|
5
|
+
* so the exposed buckets always sum to the wall. Task-time sums every span as recorded, so concurrent
|
|
6
|
+
* work (two workers, four suites waiting on one lease) stays visible without inflating the wall.
|
|
7
|
+
*
|
|
8
|
+
* Only journaled observations become spans: a fresh gate row's measured duration less the matched
|
|
9
|
+
* suite waits inside it, a worker launch matched to its result, a suite wait matched to its admission,
|
|
10
|
+
* and the gap a restart left between one engagement's last row and the next run-resume. A copy of
|
|
11
|
+
* evidence — a cache reuse, a journal replay, a row naming an invocation already counted — never adds
|
|
12
|
+
* a span; a reused full suite's row still times the fresh screen it carries. A row without a duration, a launch, wait or gate start with no matching end, and one cut by
|
|
13
|
+
* a restart are counted, never timed. Residual
|
|
14
|
+
* is unattributed time, not a claim of idleness. Pure: journal rows in, numbers out.
|
|
15
|
+
*/
|
|
16
|
+
export const WALL_PRIORITY = ["interruption", "test", "semantics", "worker", "queue", "other-gate"];
|
|
17
|
+
const perBucket = () => Object.fromEntries(WALL_PRIORITY.map((bucket) => [bucket, 0]));
|
|
18
|
+
const at = (e) => Date.parse(e.ts);
|
|
19
|
+
const measured = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
20
|
+
const bucketOf = (gate) => gate === "test" ? "test" : gate === "acceptance" || gate === "review" ? "semantics" : "other-gate";
|
|
21
|
+
// Rows another process writes around a restart — the operator's approval, the resuming daemon's
|
|
22
|
+
// reclaim and rehash audit — so the prior engagement's own last row is the one before them.
|
|
23
|
+
// ponytail: a closed list; a new out-of-engagement writer lengthens the interruption it precedes.
|
|
24
|
+
const OUTSIDE_ENGAGEMENT = new Set(["task-approved", "graph-rehash", "lock-reclaimed", "superseded"]);
|
|
25
|
+
const outsideEngagement = (e) => OUTSIDE_ENGAGEMENT.has(e.event) || (e.event === "exit-cause" && e.data.cause === "unclean");
|
|
26
|
+
const WORKER_END = new Set(["worker-result", "worker-dead", "worker-hard-timeout"]);
|
|
27
|
+
const invocationOf = (data) => {
|
|
28
|
+
if (typeof data.nonce === "string")
|
|
29
|
+
return data.nonce;
|
|
30
|
+
const receipt = data.evidenceReceipt;
|
|
31
|
+
return typeof receipt?.invocationId === "string" ? receipt.invocationId : undefined;
|
|
32
|
+
};
|
|
33
|
+
/** Undefined when the journal names no measurable window (no run-start, or unreadable timestamps). */
|
|
34
|
+
export function wallBudget(events) {
|
|
35
|
+
const first = events.findIndex((e) => e.event === "run-start");
|
|
36
|
+
if (first < 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const lastOf = (event) => events.map((e) => e.event).lastIndexOf(event);
|
|
39
|
+
const lastEnd = lastOf("run-end");
|
|
40
|
+
const lastResume = lastOf("run-resume");
|
|
41
|
+
const from = at(events[first]);
|
|
42
|
+
// A closed engagement ends at its run-end; an open or crashed one at its newest row.
|
|
43
|
+
const to = at(events[lastEnd > lastResume ? lastEnd : events.length - 1]);
|
|
44
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from)
|
|
45
|
+
return undefined;
|
|
46
|
+
const spans = [];
|
|
47
|
+
const span = (bucket, a, b) => {
|
|
48
|
+
const s = Math.max(a, from);
|
|
49
|
+
const t = Math.min(b, to);
|
|
50
|
+
if (Number.isFinite(s) && Number.isFinite(t) && t > s)
|
|
51
|
+
spans.push({ bucket, from: s, to: t });
|
|
52
|
+
};
|
|
53
|
+
const evidence = { fresh: 0, replay: 0, reuse: 0, unknown: 0 };
|
|
54
|
+
const untimed = Object.fromEntries(WALL_PRIORITY.map((bucket) => [bucket, { unknown: 0, unmatched: 0, interrupted: 0 }]));
|
|
55
|
+
const counted = new Set();
|
|
56
|
+
// A gate row is journaled when its whole round settles (acceptance, review and test often share one
|
|
57
|
+
// timestamp), so its measured duration is anchored at the gate's FIRST phase-start in the round.
|
|
58
|
+
const gateStarts = new Map();
|
|
59
|
+
const workers = new Map();
|
|
60
|
+
// A dispatch that never journals a launch (every pre-launch-row journal) ran a worker nobody timed.
|
|
61
|
+
const dispatched = new Set();
|
|
62
|
+
// A wait is owned by the gate whose command was admitted (the daemon names it when the phase is
|
|
63
|
+
// unambiguous; a parallel sibling leaves it absent). Its measurement started before admission, so
|
|
64
|
+
// a matched wait inside that gate's own span is queue, not service, and is cut out before priority
|
|
65
|
+
// applies. Sibling gates keep executing through it, so nothing is cut from them. A wait naming no
|
|
66
|
+
// gate belongs to the suite lease, which is the test gate's.
|
|
67
|
+
const owner = (task, gate) => `${task}\0${typeof gate === "string" ? gate : "test"}`;
|
|
68
|
+
const waits = new Map();
|
|
69
|
+
const queued = new Map();
|
|
70
|
+
const admit = (task, gate, ts) => {
|
|
71
|
+
const key = owner(task, gate);
|
|
72
|
+
const opened = waits.get(key);
|
|
73
|
+
if (opened === undefined)
|
|
74
|
+
return;
|
|
75
|
+
span("queue", opened, ts);
|
|
76
|
+
// One open wait per owner, closed in journal order: the list stays sorted and non-overlapping.
|
|
77
|
+
queued.set(key, [...(queued.get(key) ?? []), [opened, ts]]);
|
|
78
|
+
waits.delete(key);
|
|
79
|
+
};
|
|
80
|
+
const service = (gate, task, a, b) => {
|
|
81
|
+
const bucket = bucketOf(gate);
|
|
82
|
+
// One linear sweep over the sorted waits; span() drops any empty or inverted piece.
|
|
83
|
+
let cursor = a;
|
|
84
|
+
for (const [ws, we] of queued.get(owner(task, gate)) ?? []) {
|
|
85
|
+
if (we <= cursor || ws >= b)
|
|
86
|
+
continue;
|
|
87
|
+
span(bucket, cursor, ws);
|
|
88
|
+
cursor = Math.max(cursor, we);
|
|
89
|
+
}
|
|
90
|
+
span(bucket, cursor, b);
|
|
91
|
+
};
|
|
92
|
+
// A gate that started and never journaled its result is evidence without timing, never a zero.
|
|
93
|
+
const untimedStarts = (starts, kind) => {
|
|
94
|
+
for (const gate of starts?.keys() ?? [])
|
|
95
|
+
untimed[bucketOf(gate)][kind]++;
|
|
96
|
+
};
|
|
97
|
+
for (let i = first + 1; i < events.length; i++) {
|
|
98
|
+
const e = events[i];
|
|
99
|
+
const task = e.taskId ?? "";
|
|
100
|
+
const ts = at(e);
|
|
101
|
+
switch (e.event) {
|
|
102
|
+
case "run-resume": {
|
|
103
|
+
let last = i - 1;
|
|
104
|
+
while (last > first && outsideEngagement(events[last]))
|
|
105
|
+
last--;
|
|
106
|
+
span("interruption", at(events[last]), ts);
|
|
107
|
+
untimed.worker.interrupted += workers.size;
|
|
108
|
+
untimed.worker.unknown += dispatched.size;
|
|
109
|
+
untimed.queue.interrupted += waits.size;
|
|
110
|
+
for (const starts of gateStarts.values())
|
|
111
|
+
untimedStarts(starts, "interrupted");
|
|
112
|
+
workers.clear();
|
|
113
|
+
dispatched.clear();
|
|
114
|
+
waits.clear();
|
|
115
|
+
gateStarts.clear();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "phase-start": {
|
|
119
|
+
if (e.data.phase === "gates") {
|
|
120
|
+
untimedStarts(gateStarts.get(task), "unmatched");
|
|
121
|
+
gateStarts.delete(task);
|
|
122
|
+
}
|
|
123
|
+
else if (typeof e.data.gate === "string") {
|
|
124
|
+
const starts = gateStarts.get(task) ?? new Map();
|
|
125
|
+
if (!starts.has(e.data.gate))
|
|
126
|
+
starts.set(e.data.gate, ts);
|
|
127
|
+
gateStarts.set(task, starts);
|
|
128
|
+
}
|
|
129
|
+
if (e.data.admitted === true)
|
|
130
|
+
admit(task, e.data.gate, ts);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "suite-admitted":
|
|
134
|
+
admit(task, undefined, ts);
|
|
135
|
+
break;
|
|
136
|
+
case "suite-wait":
|
|
137
|
+
// Repeated rows while a wait is open re-report its count; they do not open a second wait.
|
|
138
|
+
if (!waits.has(owner(task, e.data.gate)))
|
|
139
|
+
waits.set(owner(task, e.data.gate), ts);
|
|
140
|
+
break;
|
|
141
|
+
case "task-dispatch":
|
|
142
|
+
if (dispatched.has(task))
|
|
143
|
+
untimed.worker.unknown++;
|
|
144
|
+
dispatched.add(task);
|
|
145
|
+
break;
|
|
146
|
+
case "worker-launch":
|
|
147
|
+
if (workers.has(task))
|
|
148
|
+
untimed.worker.unmatched++;
|
|
149
|
+
workers.set(task, ts);
|
|
150
|
+
dispatched.delete(task);
|
|
151
|
+
break;
|
|
152
|
+
case "gate-provisioned":
|
|
153
|
+
if (measured(e.data.durationMs))
|
|
154
|
+
service(typeof e.data.gate === "string" ? e.data.gate : "build", task, ts - e.data.durationMs, ts);
|
|
155
|
+
break;
|
|
156
|
+
case "gate-result": {
|
|
157
|
+
const gate = e.data.gate;
|
|
158
|
+
if (typeof gate !== "string")
|
|
159
|
+
break;
|
|
160
|
+
const anchor = gateStarts.get(task)?.get(gate);
|
|
161
|
+
gateStarts.get(task)?.delete(gate);
|
|
162
|
+
if (gateDeclined(e.data))
|
|
163
|
+
break;
|
|
164
|
+
const bucket = bucketOf(gate);
|
|
165
|
+
const invocation = invocationOf(e.data);
|
|
166
|
+
const identity = invocation === undefined ? undefined : `${gate}\0${invocation}`;
|
|
167
|
+
if (e.data.reused === true) {
|
|
168
|
+
evidence.reuse++;
|
|
169
|
+
// A held screen runs fresh before its full suite is carried from the cache, and the one
|
|
170
|
+
// merge-candidate row holds both: the screen's measured interval is fresh execution, the
|
|
171
|
+
// carried suite adds none. A replay's copy is not a measurement of this attempt.
|
|
172
|
+
const screen = e.data.fullSuite === true && e.data.replayedFromAttempt === undefined ? e.data.selectedDurationMs : undefined;
|
|
173
|
+
if (!measured(screen) || screen <= 0)
|
|
174
|
+
break;
|
|
175
|
+
evidence.fresh++;
|
|
176
|
+
const screenFrom = anchor ?? ts - screen;
|
|
177
|
+
service(gate, task, screenFrom, Math.min(screenFrom + screen, ts));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (e.data.replayedFromAttempt !== undefined || (identity !== undefined && counted.has(identity))) {
|
|
181
|
+
evidence.replay++;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (identity !== undefined)
|
|
185
|
+
counted.add(identity);
|
|
186
|
+
const d = e.data.durationMs;
|
|
187
|
+
if (!measured(d)) {
|
|
188
|
+
evidence.unknown++;
|
|
189
|
+
untimed[bucket].unknown++;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
evidence.fresh++;
|
|
193
|
+
const full = e.data.fullSuite === true && measured(e.data.fullDurationMs) && measured(e.data.selectedDurationMs);
|
|
194
|
+
if (anchor === undefined)
|
|
195
|
+
service(gate, task, ts - d, ts);
|
|
196
|
+
else if (full) {
|
|
197
|
+
// The full suite ends at the row; the selected run began at the gate's first phase-start.
|
|
198
|
+
const fullFrom = ts - e.data.fullDurationMs;
|
|
199
|
+
service(gate, task, anchor, Math.min(anchor + e.data.selectedDurationMs, fullFrom));
|
|
200
|
+
service(gate, task, fullFrom, ts);
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
service(gate, task, anchor, Math.min(anchor + d, ts));
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
if (WORKER_END.has(e.event) && workers.has(task)) {
|
|
208
|
+
span("worker", workers.get(task), ts);
|
|
209
|
+
workers.delete(task);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
untimed.worker.unmatched += workers.size;
|
|
214
|
+
untimed.worker.unknown += dispatched.size;
|
|
215
|
+
untimed.queue.unmatched += waits.size;
|
|
216
|
+
for (const starts of gateStarts.values())
|
|
217
|
+
untimedStarts(starts, "unmatched");
|
|
218
|
+
const taskMs = perBucket();
|
|
219
|
+
for (const s of spans)
|
|
220
|
+
taskMs[s.bucket] += s.to - s.from;
|
|
221
|
+
// One sweep over span edges: each segment goes to the highest-priority bucket still open over it.
|
|
222
|
+
const rank = (bucket) => WALL_PRIORITY.indexOf(bucket);
|
|
223
|
+
const edges = spans.flatMap((s) => [[s.from, rank(s.bucket), 1], [s.to, rank(s.bucket), -1]])
|
|
224
|
+
.sort((a, b) => a[0] - b[0]);
|
|
225
|
+
const open = WALL_PRIORITY.map(() => 0);
|
|
226
|
+
const exposedMs = perBucket();
|
|
227
|
+
let cursor = from;
|
|
228
|
+
for (const [t, r, delta] of edges) {
|
|
229
|
+
if (t > cursor) {
|
|
230
|
+
const top = open.findIndex((n) => n > 0);
|
|
231
|
+
if (top >= 0)
|
|
232
|
+
exposedMs[WALL_PRIORITY[top]] += t - cursor;
|
|
233
|
+
cursor = t;
|
|
234
|
+
}
|
|
235
|
+
open[r] += delta;
|
|
236
|
+
}
|
|
237
|
+
const wallMs = to - from;
|
|
238
|
+
const residualMs = wallMs - WALL_PRIORITY.reduce((sum, bucket) => sum + exposedMs[bucket], 0);
|
|
239
|
+
return { wallMs, exposedMs, residualMs, taskMs, evidence, untimed };
|
|
240
|
+
}
|
|
241
|
+
/** Whole seconds past a minute, one decimal below ten seconds — never a rounded-away "0s" for a real span. */
|
|
242
|
+
export const formatSpan = (ms) => {
|
|
243
|
+
if (ms > 0 && ms < 10_000)
|
|
244
|
+
return `${(Math.ceil(ms / 100) / 10).toString()}s`;
|
|
245
|
+
const s = Math.round(ms / 1_000);
|
|
246
|
+
if (s < 60)
|
|
247
|
+
return `${s}s`;
|
|
248
|
+
if (s < 3_600)
|
|
249
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
250
|
+
return `${Math.floor(s / 3_600)}h ${Math.floor((s % 3_600) / 60)}m`;
|
|
251
|
+
};
|
|
252
|
+
const plural = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
253
|
+
/**
|
|
254
|
+
* One fact per bucket, then residual and the evidence tally — the words every surface prints. A
|
|
255
|
+
* bucket whose only evidence is untimed reads `unknown`, never a zero it did not measure.
|
|
256
|
+
*/
|
|
257
|
+
export function wallBudgetFacts(b) {
|
|
258
|
+
const pct = (ms) => b.wallMs > 0 ? ` (${Math.round((100 * ms) / b.wallMs)}%)` : "";
|
|
259
|
+
const facts = WALL_PRIORITY.map((bucket) => {
|
|
260
|
+
const { unknown, unmatched, interrupted } = b.untimed[bucket];
|
|
261
|
+
const notes = [
|
|
262
|
+
...(unknown ? [bucket === "worker"
|
|
263
|
+
? `${unknown} ${unknown === 1 ? "dispatch" : "dispatches"} without a launch row`
|
|
264
|
+
: `${plural(unknown, "row")} without a duration`] : []),
|
|
265
|
+
...(unmatched ? [`${unmatched} unmatched`] : []),
|
|
266
|
+
...(interrupted ? [`${interrupted} cut by a restart`] : []),
|
|
267
|
+
];
|
|
268
|
+
if (!b.exposedMs[bucket] && !b.taskMs[bucket] && notes.length)
|
|
269
|
+
return [bucket, `unknown — ${notes.join(" · ")}`];
|
|
270
|
+
const timed = `${formatSpan(b.exposedMs[bucket])}${pct(b.exposedMs[bucket])} · task-time ${formatSpan(b.taskMs[bucket])}`;
|
|
271
|
+
return [bucket, [timed, ...notes.map((note) => `${note}, untimed`)].join(" · ")];
|
|
272
|
+
});
|
|
273
|
+
const { fresh, replay, reuse, unknown } = b.evidence;
|
|
274
|
+
return [
|
|
275
|
+
...facts,
|
|
276
|
+
["residual", `${formatSpan(b.residualMs)}${pct(b.residualMs)} — unattributed, not proven idle`],
|
|
277
|
+
["gate evidence", `fresh ${fresh} · replay ${replay} · reuse ${reuse} · unknown ${unknown}`],
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
export const WALL_PRIORITY_TEXT = [...WALL_PRIORITY, "residual"].join(" › ");
|
|
@@ -47,9 +47,9 @@ const AREAS = [
|
|
|
47
47
|
["FLEET", [/^src\/(route|config|adapters)\//]],
|
|
48
48
|
["RUN", [/^src\/(run|drivers|eval)\//]],
|
|
49
49
|
["GATE", [/^src\/gates\//]],
|
|
50
|
-
["UI", [/^src\/(cli|tui|report)\//, /^src\/brand\.ts
|
|
51
|
-
["DOCS", [/^(docs|skills)\//]],
|
|
52
|
-
["REPO", [/^(scripts|schema)\//, /^\.github\//, /^tests\/(lint|repo|helpers)\//, /^(vitest\.config|package\.json|tsconfig)/]],
|
|
50
|
+
["UI", [/^src\/(cli|tui|report)\//, /^src\/brand\.ts/, /^tests\/cockpit\//]],
|
|
51
|
+
["DOCS", [/^(docs|skills)\//, /^\.claude\/skills\//]],
|
|
52
|
+
["REPO", [/^(scripts|schema)\//, /^\.github\//, /^tests\/(lint|repo|helpers|fixtures)\//, /^(vitest\.config|package\.json|tsconfig)/]],
|
|
53
53
|
];
|
|
54
54
|
export const BOARD_AREA_ORDER = AREAS.map(([n]) => n);
|
|
55
55
|
const expandBraces = (p) => {
|
|
@@ -25,6 +25,10 @@ export interface DecisionCommand {
|
|
|
25
25
|
readonly reason?: string;
|
|
26
26
|
/** `--review-rounds`: the review-round ceiling the release carries. */
|
|
27
27
|
readonly reviewRounds?: number;
|
|
28
|
+
/** OBS-1178: `--park <line>@<ts>` — the displayed park this decision binds to. */
|
|
29
|
+
readonly park?: string;
|
|
30
|
+
/** OBS-1178: `--gate <gate>` — the failed gate a waive binds to. */
|
|
31
|
+
readonly gate?: string;
|
|
28
32
|
}
|
|
29
33
|
/** The exact argv the production command receives — built only from the closed verb set. */
|
|
30
34
|
export declare function decisionArgv(command: DecisionCommand, { runId, by }: {
|
|
@@ -57,13 +61,11 @@ export declare const journalFileFor: (cwd: string, runId: string) => string;
|
|
|
57
61
|
* the named verb is outside the park's table, so a confirm inset can never promise a decision the
|
|
58
62
|
* command would refuse. The enactor is read from the run lock now, and read again at the receipt.
|
|
59
63
|
*/
|
|
60
|
-
export declare function previewDecision(
|
|
64
|
+
export declare function previewDecision(named: DecisionCommand, { cwd, runId, by }: {
|
|
61
65
|
cwd: string;
|
|
62
66
|
runId: string;
|
|
63
67
|
by: string;
|
|
64
68
|
}): DecisionPreviewResult;
|
|
65
|
-
/** Who enacts it — the three lock states, each stated by the command's own sentence. */
|
|
66
|
-
export declare function enactmentLines(run: ApprovalRunOwner, disposition: ApprovalDisposition): string[];
|
|
67
69
|
/** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
|
|
68
70
|
export declare function decisionConfirmLines(preview: DecisionPreview): readonly string[];
|
|
69
71
|
/** The task-approved row the file gained, located by its physical line. */
|
|
@@ -135,8 +137,7 @@ export interface DecisionKeyResult {
|
|
|
135
137
|
/**
|
|
136
138
|
* The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
|
|
137
139
|
* selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
|
|
138
|
-
* verb
|
|
139
|
-
* cancel the deepest layer. Enter on a confirm is inert by construction.
|
|
140
|
+
* verb opens the confirm inset; `y` names the write; `n`/Esc cancel the deepest layer. Enter on a confirm is inert.
|
|
140
141
|
*/
|
|
141
142
|
export declare function applyDecisionKey(session: DecisionSession, event: DecisionKeyEvent, selected: RunDecision | undefined): DecisionKeyResult;
|
|
142
143
|
/** Store the preview a picked verb produced — or its refusal as the notice. */
|
|
@@ -145,3 +146,5 @@ export declare function withDecisionPreview(session: DecisionSession, result: De
|
|
|
145
146
|
export declare function withDecisionReceipt(session: DecisionSession, receipt: DecisionReceipt): DecisionSession;
|
|
146
147
|
/** The verbs the keybar may advertise for the current layer — nothing the flow cannot honour. */
|
|
147
148
|
export declare function decisionKeybar(session: DecisionSession, selected: RunDecision | undefined): string;
|
|
149
|
+
/** Who enacts it — the three lock states, each stated by the command's own sentence. */
|
|
150
|
+
export declare function enactmentLines(run: ApprovalRunOwner, disposition: ApprovalDisposition): string[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
|
|
2
|
+
import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, parkToken, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
|
|
3
3
|
import { stateDirName } from "../../graph/graph.js";
|
|
4
|
-
import { Journal } from "../../run/journal.js";
|
|
4
|
+
import { bindingToken, effectiveDecisions, Journal, physicalLine, recordedBinding } from "../../run/journal.js";
|
|
5
5
|
const parkDiagnostic = (park) => {
|
|
6
6
|
if (park.kind === "scope-request")
|
|
7
7
|
return park.approveCommand ?? "scope-request requires tickmarkr approve with --files <glob,…>";
|
|
@@ -50,6 +50,8 @@ export function decisionArgv(command, { runId, by }) {
|
|
|
50
50
|
const flag = { approve: [], waive: ["--waive"], uphold: ["--uphold"], recheck: ["--recheck"] }[command.verb];
|
|
51
51
|
return [
|
|
52
52
|
runId, command.taskId, ...flag,
|
|
53
|
+
...(command.park === undefined ? [] : ["--park", command.park]),
|
|
54
|
+
...(command.gate === undefined ? [] : ["--gate", command.gate]),
|
|
53
55
|
...(command.reviewRounds === undefined ? [] : ["--review-rounds", String(command.reviewRounds)]),
|
|
54
56
|
"--by", by,
|
|
55
57
|
...(command.reason === undefined ? [] : ["--reason", command.reason]),
|
|
@@ -62,10 +64,9 @@ export const journalFileFor = (cwd, runId) => join(stateDirName(cwd), "runs", ru
|
|
|
62
64
|
* the named verb is outside the park's table, so a confirm inset can never promise a decision the
|
|
63
65
|
* command would refuse. The enactor is read from the run lock now, and read again at the receipt.
|
|
64
66
|
*/
|
|
65
|
-
export function previewDecision(
|
|
66
|
-
let argv;
|
|
67
|
+
export function previewDecision(named, { cwd, runId, by }) {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
decisionArgv(named, { runId, by });
|
|
69
70
|
}
|
|
70
71
|
catch (e) {
|
|
71
72
|
return { ok: false, refusal: e.message };
|
|
@@ -78,15 +79,29 @@ export function previewDecision(command, { cwd, runId, by }) {
|
|
|
78
79
|
catch (e) {
|
|
79
80
|
return { ok: false, refusal: e.message };
|
|
80
81
|
}
|
|
81
|
-
const park = newestPark(events,
|
|
82
|
+
const park = newestPark(events, named.taskId, sourceIndexes);
|
|
82
83
|
if (!park)
|
|
83
|
-
return { ok: false, refusal: `task ${
|
|
84
|
+
return { ok: false, refusal: `task ${named.taskId} has no park in run ${runId} — nothing to decide` };
|
|
85
|
+
// OBS-1178: the argv binds the park this preview displays; approve refuses it once another opens.
|
|
86
|
+
const token = parkToken(park);
|
|
87
|
+
if (named.park !== undefined && named.park !== token)
|
|
88
|
+
return { ok: false, refusal: `task ${named.taskId}'s open park is ${token ?? "untimestamped"}, not ${named.park} — refresh and decide the open park` };
|
|
89
|
+
if (named.gate !== undefined && named.gate !== park.failedGate)
|
|
90
|
+
return { ok: false, refusal: `refusing mismatched gate for ${named.taskId}: --gate ${named.gate} but park ${token} failed ${park.failedGate ?? "no gate"} — nothing appended` };
|
|
91
|
+
const command = { ...named, ...(token === undefined ? {} : { park: token }), ...(named.verb === "waive" && named.gate === undefined && park.failedGate !== undefined ? { gate: park.failedGate } : {}) };
|
|
92
|
+
let argv;
|
|
93
|
+
try {
|
|
94
|
+
argv = decisionArgv(command, { runId, by });
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return { ok: false, refusal: e.message };
|
|
98
|
+
}
|
|
84
99
|
const verbs = permittedDecisionVerbs(park);
|
|
85
100
|
if (!verbs.includes(command.verb)) {
|
|
86
101
|
const why = parkDiagnostic(park) ?? `permitted: ${verbs.join(", ")}`;
|
|
87
102
|
return { ok: false, refusal: `${command.verb} is not a decision for ${command.taskId}'s ${park.kind ?? "unknown"} park (${why})` };
|
|
88
103
|
}
|
|
89
|
-
const released = events
|
|
104
|
+
const released = releaseSince(events, park.index, command.taskId);
|
|
90
105
|
if (released) {
|
|
91
106
|
return { ok: false, refusal: `task ${command.taskId} was already released at #L${sourceIndexes[events.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} — nothing to confirm` };
|
|
92
107
|
}
|
|
@@ -100,28 +115,12 @@ export function previewDecision(command, { cwd, runId, by }) {
|
|
|
100
115
|
};
|
|
101
116
|
}
|
|
102
117
|
const parkLine = (park) => `#L${park.line} ${park.kind ?? "unknown kind"}${park.failedGate ? ` · failed gate ${park.failedGate}` : ""}${park.reason ? ` · ${park.reason}` : ""}`;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (verb === "waive")
|
|
108
|
-
lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
|
|
109
|
-
if (verb === "recheck")
|
|
110
|
-
lines.push(" marks no gate satisfied; the whole declared battery re-runs");
|
|
111
|
-
if (verb === "uphold")
|
|
112
|
-
lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
|
|
113
|
-
lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
|
|
114
|
-
return lines;
|
|
115
|
-
}
|
|
116
|
-
/** Who enacts it — the three lock states, each stated by the command's own sentence. */
|
|
117
|
-
export function enactmentLines(run, disposition) {
|
|
118
|
-
const owner = run.live
|
|
119
|
-
? "enactment matching live daemon — pending daemon enactment at its next task boundary"
|
|
120
|
-
: run.blockingRunId
|
|
121
|
-
? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
|
|
122
|
-
: `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
|
|
123
|
-
return [owner, ` ${approvalEnactment(disposition, run)}`];
|
|
118
|
+
/** The decision already open on this park, if any. OBS-1178: a refused or unsound row released nothing. */
|
|
119
|
+
function releaseSince(events, parkIndex, taskId) {
|
|
120
|
+
const effective = effectiveDecisions(events);
|
|
121
|
+
return events.find((e, i) => i > parkIndex && e.event === "task-approved" && e.taskId === taskId && effective.has(physicalLine(events, i)));
|
|
124
122
|
}
|
|
123
|
+
const quoteArg = (arg) => (/^[\w.:/@=-]+$/u.test(arg) ? arg : JSON.stringify(arg));
|
|
125
124
|
/** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
|
|
126
125
|
export function decisionConfirmLines(preview) {
|
|
127
126
|
return [
|
|
@@ -157,7 +156,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
|
|
|
157
156
|
if (!park || park.index !== preview.park.index || park.line !== preview.park.line || park.kind !== preview.park.kind) {
|
|
158
157
|
return { ok: false, preview, stale: true, refusal: `task ${named.taskId}'s park changed since preview (${park ? `now #L${park.line} ${park.kind ?? "unknown"}` : "no park now"}) — refresh and preview again` };
|
|
159
158
|
}
|
|
160
|
-
const released = before
|
|
159
|
+
const released = releaseSince(before, park.index, named.taskId);
|
|
161
160
|
if (released) {
|
|
162
161
|
return { ok: false, preview, stale: true, refusal: `task ${named.taskId} was released at #L${beforeSourceIndexes[before.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} after this preview — no second decision appended; refresh` };
|
|
163
162
|
}
|
|
@@ -208,6 +207,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
|
|
|
208
207
|
via: "cli",
|
|
209
208
|
gate: named.verb === "waive" ? park.failedGate : named.verb === "uphold" ? "review" : undefined,
|
|
210
209
|
reviewRoundCeiling: named.reviewRounds,
|
|
210
|
+
park: parkToken(park),
|
|
211
211
|
};
|
|
212
212
|
const actual = {
|
|
213
213
|
release,
|
|
@@ -216,6 +216,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
|
|
|
216
216
|
via: appended.event.data.via,
|
|
217
217
|
gate: appended.event.data.gate,
|
|
218
218
|
reviewRoundCeiling: appended.event.data.reviewRoundCeiling,
|
|
219
|
+
park: recordedBinding(appended.event.data.park) === undefined ? undefined : bindingToken(recordedBinding(appended.event.data.park)),
|
|
219
220
|
};
|
|
220
221
|
const mismatch = Object.keys(expected)
|
|
221
222
|
.find((field) => actual[field] !== expected[field]);
|
|
@@ -264,8 +265,7 @@ export const initialDecisionSession = () => ({ menu: null, confirming: null, rec
|
|
|
264
265
|
/**
|
|
265
266
|
* The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
|
|
266
267
|
* selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
|
|
267
|
-
* verb
|
|
268
|
-
* cancel the deepest layer. Enter on a confirm is inert by construction.
|
|
268
|
+
* verb opens the confirm inset; `y` names the write; `n`/Esc cancel the deepest layer. Enter on a confirm is inert.
|
|
269
269
|
*/
|
|
270
270
|
export function applyDecisionKey(session, event, selected) {
|
|
271
271
|
if (session.confirming !== null) {
|
|
@@ -315,3 +315,26 @@ export function decisionKeybar(session, selected) {
|
|
|
315
315
|
return session.menu.verbs.length === 0 ? "Esc Close" : "↑↓ Choose · Enter Preview · Esc Close";
|
|
316
316
|
return selected === undefined ? "" : "a Actions";
|
|
317
317
|
}
|
|
318
|
+
// Consequence and enactor lines, shared by the confirm inset and the receipt above. Kept below the key
|
|
319
|
+
// grammar so the operator walkthrough's line citations stay put.
|
|
320
|
+
/** What a confirmed write buys, from the command's own table — never a phrase of this file's own. */
|
|
321
|
+
function consequenceLines(verb, park, release, disposition) {
|
|
322
|
+
const lines = [`consequence disposition ${disposition}; appends one task-approved${release ? ` with release ${release}` : " with no release"}`];
|
|
323
|
+
if (verb === "waive")
|
|
324
|
+
lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
|
|
325
|
+
if (verb === "recheck")
|
|
326
|
+
lines.push(" marks no gate satisfied; the whole declared battery re-runs");
|
|
327
|
+
if (verb === "uphold")
|
|
328
|
+
lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
|
|
329
|
+
lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
332
|
+
/** Who enacts it — the three lock states, each stated by the command's own sentence. */
|
|
333
|
+
export function enactmentLines(run, disposition) {
|
|
334
|
+
const owner = run.live
|
|
335
|
+
? "enactment matching live daemon — pending daemon enactment at its next task boundary"
|
|
336
|
+
: run.blockingRunId
|
|
337
|
+
? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
|
|
338
|
+
: `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
|
|
339
|
+
return [owner, ` ${approvalEnactment(disposition, run)}`];
|
|
340
|
+
}
|
|
@@ -5,6 +5,7 @@ import { channelKey } from "../../adapters/types.js";
|
|
|
5
5
|
import { newestPark, permittedDecisionVerbs } from "../../cli/commands/approve.js";
|
|
6
6
|
import { parseOwnedName } from "../../drivers/types.js";
|
|
7
7
|
import { projectActivity } from "../../run/activity.js";
|
|
8
|
+
import { effectiveDecisions, withPhysicalLine } from "../../run/journal.js";
|
|
8
9
|
import { isPidLive } from "../../run/lock.js";
|
|
9
10
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
10
11
|
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
@@ -79,6 +80,15 @@ function parseSource(raw) {
|
|
|
79
80
|
}
|
|
80
81
|
return { events, defects };
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* OBS-1178: the rows the task-state folds read. A refused or unsound decision released nothing, so its
|
|
85
|
+
* task-approved row is dropped here; the one decision fold keys every row by its physical line. The
|
|
86
|
+
* journal rows the cockpit draws keep every line as evidence.
|
|
87
|
+
*/
|
|
88
|
+
function decidedEvents(events) {
|
|
89
|
+
const effective = effectiveDecisions(events.map((event) => withPhysicalLine(event, event.line)));
|
|
90
|
+
return events.filter((event) => event.event !== "task-approved" || effective.has(event.line));
|
|
91
|
+
}
|
|
82
92
|
function assignmentFrom(value) {
|
|
83
93
|
const assignment = recordValue(value);
|
|
84
94
|
return assignment
|
|
@@ -844,7 +854,8 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
844
854
|
// alive in the lock — the cockpit called a live run interrupted.
|
|
845
855
|
: pid !== undefined && (options.isDaemonAlive ?? isPidLive)(pid);
|
|
846
856
|
const interrupted = !alive;
|
|
847
|
-
const
|
|
857
|
+
const decided = decidedEvents(events);
|
|
858
|
+
const tasks = deriveTasks(decided, interrupted);
|
|
848
859
|
const taskFacts = [...tasks.values()];
|
|
849
860
|
const done = taskFacts.filter((task) => task.state === "done").length;
|
|
850
861
|
const failed = taskFacts.filter((task) => task.state === "failed").length;
|
|
@@ -873,7 +884,7 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
873
884
|
// run-end — the run's own closing statement is enough.
|
|
874
885
|
const runStatus = failed > 0
|
|
875
886
|
? "failed"
|
|
876
|
-
: human > 0 || unresolvedSummaryParks(
|
|
887
|
+
: human > 0 || unresolvedSummaryParks(decided).length > 0
|
|
877
888
|
? "parked"
|
|
878
889
|
: lifecycle === "superseded" || hasInterruptedTask
|
|
879
890
|
? "interrupted"
|
|
@@ -46,5 +46,15 @@ export interface ConsolidatedOptions {
|
|
|
46
46
|
environment?: NodeJS.ProcessEnv;
|
|
47
47
|
diagnostics?: readonly HomeNeedsYouTarget[];
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
|
|
51
|
+
* decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
|
|
52
|
+
* fold shows now, since a later run-end restores a park but not the gate evidence a rejected recheck
|
|
53
|
+
* cleared — the operator snapshot is refolded over the decided rows, each keeping its physical line and
|
|
54
|
+
* the store's evidence identity.
|
|
55
|
+
* ponytail: a whole-journal read per observed journal change (parsed only when it holds an approval);
|
|
56
|
+
* move the decision fold into OperatorStateFold if that ever measures slow.
|
|
57
|
+
*/
|
|
58
|
+
export declare function decidedLiveStore(store: LiveStore): LiveStore;
|
|
49
59
|
/** The live entry mounts the dependency-owned leaves, using only C2 snapshots/history/page. */
|
|
50
60
|
export declare function runConsolidatedCockpit(options: ConsolidatedOptions): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { existsSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { renderMarkdownRecord } from "../../cli/commands/report.js";
|
|
5
5
|
import { loadConfig } from "../../config/config.js";
|
|
@@ -20,9 +20,10 @@ import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF }
|
|
|
20
20
|
import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPreview, withDecisionReceipt, decisionConfirmLines } from "./decision-actions.js";
|
|
21
21
|
import { approvalRunOwner } from "../../cli/commands/approve.js";
|
|
22
22
|
import { GLYPHS } from "../../brand.js";
|
|
23
|
-
import { Journal } from "../../run/journal.js";
|
|
23
|
+
import { effectiveEvents, Journal, parseJournalText, physicalLine } from "../../run/journal.js";
|
|
24
24
|
import { observeNamedRun, WATCH_OWNER_ENV } from "../../run/supervision.js";
|
|
25
25
|
import { formatOwnedName } from "../../drivers/types.js";
|
|
26
|
+
import { readOperatorState } from "../../run/operator-state.js";
|
|
26
27
|
import { cellWidth, wrapCells } from "./width.js";
|
|
27
28
|
import { resolveShellColourMode } from "./theme.js";
|
|
28
29
|
let timelineUsers = 0;
|
|
@@ -58,6 +59,52 @@ export function borrowRuntimeTimeline() {
|
|
|
58
59
|
}
|
|
59
60
|
export const SHELL_TERMINAL_ENTER = "\x1b[?1049h\x1b[22;0t\x1b]0;tickmarkr cockpit\x07" + POINTER_TRACKING_ON;
|
|
60
61
|
export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25h\x1b[?1049l";
|
|
62
|
+
/**
|
|
63
|
+
* OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
|
|
64
|
+
* decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
|
|
65
|
+
* fold shows now, since a later run-end restores a park but not the gate evidence a rejected recheck
|
|
66
|
+
* cleared — the operator snapshot is refolded over the decided rows, each keeping its physical line and
|
|
67
|
+
* the store's evidence identity.
|
|
68
|
+
* ponytail: a whole-journal read per observed journal change (parsed only when it holds an approval);
|
|
69
|
+
* move the decision fold into OperatorStateFold if that ever measures slow.
|
|
70
|
+
*/
|
|
71
|
+
export function decidedLiveStore(store) {
|
|
72
|
+
let key;
|
|
73
|
+
let operator;
|
|
74
|
+
const decided = (snap) => {
|
|
75
|
+
const { journal, graph } = snap;
|
|
76
|
+
const observed = `${journal.source}:${journal.generation}:${journal.offset}:${graph.identity}:${graph.status}`;
|
|
77
|
+
if (observed !== key) {
|
|
78
|
+
key = observed;
|
|
79
|
+
operator = undefined;
|
|
80
|
+
let bytes;
|
|
81
|
+
try {
|
|
82
|
+
bytes = readFileSync(journal.source).subarray(0, journal.offset);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return snap;
|
|
86
|
+
}
|
|
87
|
+
if (bytes.includes("task-approved")) {
|
|
88
|
+
const rows = parseJournalText(bytes.subarray(0, bytes.lastIndexOf(10) + 1).toString("utf8"))
|
|
89
|
+
.filter(e => e !== null && typeof e === "object" && typeof e.event === "string" && typeof e.data === "object" && e.data !== null);
|
|
90
|
+
const kept = effectiveEvents(rows);
|
|
91
|
+
if (kept.length !== rows.length) {
|
|
92
|
+
const events = kept.map((event, i) => {
|
|
93
|
+
const line = physicalLine(kept, i);
|
|
94
|
+
return { source: journal.source, line, id: `${journal.source}#L${line}`, generation: journal.generation, event };
|
|
95
|
+
});
|
|
96
|
+
operator = readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
|
|
97
|
+
readable: journal.status === "readable" && journal.backlogBytes === 0 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!operator)
|
|
102
|
+
return snap;
|
|
103
|
+
const { sequence, observedAt, graphAvailability } = snap.operator;
|
|
104
|
+
return { ...snap, operator: { ...operator, sequence, observedAt, graphAvailability } };
|
|
105
|
+
};
|
|
106
|
+
return { ...store, snapshot: () => decided(store.snapshot()), refresh: () => decided(store.refresh()) };
|
|
107
|
+
}
|
|
61
108
|
/** Preserve a policy-disabled gate from C2 even when C5's generic event
|
|
62
109
|
* mapper calls an unpassed result unknown. Lead with the gate name so a long
|
|
63
110
|
* task identity cannot clip the label that explains the disabled glyph. */
|
|
@@ -97,7 +144,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
97
144
|
output.write(SHELL_TERMINAL_ENTER);
|
|
98
145
|
}
|
|
99
146
|
store = createLiveStore({ cwd, runId, now: options.now });
|
|
100
|
-
const source = store;
|
|
147
|
+
const source = decidedLiveStore(store);
|
|
101
148
|
if (options.observeRun !== false)
|
|
102
149
|
observation = observeNamedRun(cwd, runId, options.environment);
|
|
103
150
|
// BD-1 (RULING-231-19 §2): the daemon-placed board carries the watch owner token and mounts
|
|
@@ -34,6 +34,9 @@ export declare function deriveRunViewRows(data: RunCockpitData, viewId: RunViewI
|
|
|
34
34
|
* task row — identity, phase, build evidence, blocker, next action — each unrecorded field saying so,
|
|
35
35
|
* each recorded one citing the journal line it was read from, and the OBS-1048 stall marker when the
|
|
36
36
|
* newest attempt's harvest is suspect. No attempt label is printed here: the ruler is the row's own.
|
|
37
|
+
* A task with no recorded dispatch folds identity, phase, and build into one missing-evidence clause
|
|
38
|
+
* (OBS-1104). A recorded blocker or next action — a pre-dispatch park, a dependency wait — stays,
|
|
39
|
+
* cited to the journal row that produced it.
|
|
37
40
|
*/
|
|
38
41
|
export declare function taskProjectionText(row: TaskRow, journal?: readonly JournalRow[]): string;
|
|
39
42
|
/**
|