tickmarkr 2.5.6 → 2.5.8
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/dist/adapters/prompt.js +21 -1
- package/dist/cli/commands/approve.js +13 -6
- package/dist/cli/commands/compile.js +7 -0
- package/dist/cli/commands/plan.d.ts +5 -0
- package/dist/cli/commands/plan.js +28 -23
- package/dist/cli/commands/resume.js +1 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +6 -4
- package/dist/compile/native.js +68 -7
- package/dist/compile/retired-literals.d.ts +22 -0
- package/dist/compile/retired-literals.js +271 -0
- package/dist/drivers/index.d.ts +4 -2
- package/dist/drivers/index.js +54 -6
- package/dist/gates/baseline.d.ts +8 -3
- package/dist/gates/baseline.js +6 -3
- package/dist/gates/review.js +12 -1
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +67 -9
- package/dist/gates/test-manifest.d.ts +12 -0
- package/dist/gates/test-manifest.js +29 -3
- package/dist/gates/test-reporter.js +28 -2
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +5 -0
- package/dist/graph/schema.d.ts +28 -0
- package/dist/graph/schema.js +13 -1
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/daemon.d.ts +28 -0
- package/dist/run/daemon.js +466 -110
- package/dist/run/git.d.ts +6 -1
- package/dist/run/git.js +63 -11
- package/dist/run/journal.d.ts +57 -5
- package/dist/run/journal.js +103 -9
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +1 -0
- package/dist/run/operator-page-summary.d.ts +56 -0
- package/dist/run/operator-page-summary.js +68 -0
- package/dist/run/operator-summary.d.ts +69 -0
- package/dist/run/operator-summary.js +77 -0
- package/dist/run/protocol.d.ts +71 -0
- package/dist/run/protocol.js +32 -0
- package/dist/run/repair-disposition.d.ts +41 -0
- package/dist/run/repair-disposition.js +77 -0
- package/dist/tui/cockpit/board.d.ts +9 -0
- package/dist/tui/cockpit/board.js +10 -0
- package/dist/tui/cockpit/derive.d.ts +35 -0
- package/dist/tui/cockpit/derive.js +152 -10
- package/dist/tui/cockpit/evidence-view.d.ts +2 -0
- package/dist/tui/cockpit/evidence-view.js +42 -12
- package/dist/tui/cockpit/home-view.js +45 -30
- package/dist/tui/cockpit/live-store.d.ts +18 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +8 -1
- package/dist/tui/cockpit/run-cockpit.js +95 -1
- package/dist/tui/cockpit/run-view.d.ts +37 -0
- package/dist/tui/cockpit/run-view.js +189 -2
- package/package.json +1 -1
- package/schema/rungraph.schema.json +54 -0
- package/skills/tickmarkr-overseer/SKILL.md +162 -3
package/dist/run/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
3
4
|
export { ROUTING_ENV_SEAMS };
|
|
@@ -150,10 +151,14 @@ export interface ShellOptions {
|
|
|
150
151
|
signal?: AbortSignal;
|
|
151
152
|
onSpawn?: (pid: number | undefined) => void;
|
|
152
153
|
onTimeout?: () => void;
|
|
154
|
+
/** Independent of the legacy pid callback; started means the child emitted spawn. */
|
|
155
|
+
onReceipt?: (receipt: ShellReceipt) => void;
|
|
156
|
+
/** Called once per invocation (1-based), including each pre-spawn retry. */
|
|
157
|
+
receiptAttribution?: (invocation: number) => CommandReceiptAttribution;
|
|
153
158
|
}
|
|
154
159
|
/** Shared command seam, including invocation-bound manifested runners. */
|
|
155
160
|
export declare function shell(cmd: string, cwd: string, timeoutMs: number, login?: boolean, options?: ShellOptions): Promise<ShResult>;
|
|
156
|
-
export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
161
|
+
export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options?: ShellOptions): Promise<ShResult>;
|
|
157
162
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
158
163
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
159
164
|
export declare function shGitOk(cmd: string, cwd: string): Promise<string>;
|
package/dist/run/git.js
CHANGED
|
@@ -214,11 +214,43 @@ const SHELL_REAP_GRACE_MS = 2000;
|
|
|
214
214
|
export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
215
215
|
const inherited = executionSignal();
|
|
216
216
|
const signal = inherited && options.signal ? AbortSignal.any([inherited, options.signal]) : inherited ?? options.signal;
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
let attribution;
|
|
218
|
+
let confirmedStart = false;
|
|
219
|
+
let terminal = false;
|
|
220
|
+
const begin = (invocation) => {
|
|
221
|
+
attribution = options.receiptAttribution?.(invocation);
|
|
222
|
+
confirmedStart = false;
|
|
223
|
+
terminal = false;
|
|
224
|
+
};
|
|
225
|
+
const emit = (receipt) => {
|
|
226
|
+
if (terminal)
|
|
227
|
+
return;
|
|
228
|
+
if (receipt.outcome === "started")
|
|
229
|
+
confirmedStart = true;
|
|
230
|
+
else
|
|
231
|
+
terminal = true;
|
|
232
|
+
// Observational callbacks must not change process cleanup, retries or cancellation.
|
|
233
|
+
try {
|
|
234
|
+
options.onReceipt?.({ ...receipt, confirmedStart, ...(attribution ? { attribution: { ...attribution } } : {}) });
|
|
235
|
+
}
|
|
236
|
+
catch { /* receipt sinks are observational */ }
|
|
237
|
+
};
|
|
238
|
+
const checkAbort = () => {
|
|
239
|
+
if (signal?.aborted)
|
|
240
|
+
emit({ outcome: "cancelled", exitCode: null, signal: null });
|
|
241
|
+
signal?.throwIfAborted();
|
|
242
|
+
};
|
|
243
|
+
begin(1);
|
|
244
|
+
checkAbort();
|
|
245
|
+
return withCommandLease(cmd, () => executeShell(cmd, cwd, timeoutMs, login, { ...options, signal }, { begin, emit, checkAbort }))
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
if (signal?.aborted)
|
|
248
|
+
emit({ outcome: "cancelled", exitCode: null, signal: null });
|
|
249
|
+
throw error;
|
|
250
|
+
});
|
|
219
251
|
}
|
|
220
|
-
function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
221
|
-
|
|
252
|
+
function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
|
|
253
|
+
observation.checkAbort();
|
|
222
254
|
// OBS-74: scrub tickmarkr's own routing env seams from every child — a daemon carrying
|
|
223
255
|
// TICKMARKR_QUALITY leaked it into baseline/gate/tip-verify children, turning a dogfood
|
|
224
256
|
// repo's route() tests red inside the gates. Scrub a copy at this one choke point so
|
|
@@ -245,13 +277,22 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
245
277
|
// detached: bash gets its own process group so a timeout can kill the whole tree —
|
|
246
278
|
// SIGKILLing bash alone orphans grandchildren (codex/pi) that hold the stdio pipes
|
|
247
279
|
// open, so "close" never fires and the promise wedges forever (v1.33.1 init hang).
|
|
248
|
-
|
|
280
|
+
let p;
|
|
281
|
+
try {
|
|
282
|
+
p = (spawnChild ?? spawn)("bash", [login ? "-lc" : "-c", cmd], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
249
288
|
let stdout = "", stderr = "";
|
|
250
289
|
const stdoutDecoder = new StringDecoder("utf8");
|
|
251
290
|
const stderrDecoder = new StringDecoder("utf8");
|
|
252
291
|
let timedOut = false, reapedGroup = false, done = false, started = false, outputSeen = false;
|
|
253
292
|
let reapError, exitedCode;
|
|
254
293
|
let signalExit = false;
|
|
294
|
+
let exitSignal = null;
|
|
295
|
+
let spawnError;
|
|
255
296
|
let reapTimer;
|
|
256
297
|
let drainTimer;
|
|
257
298
|
const finish = (code, err) => {
|
|
@@ -264,6 +305,12 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
264
305
|
options.signal?.removeEventListener("abort", abort);
|
|
265
306
|
stdout += stdoutDecoder.end();
|
|
266
307
|
stderr += stderrDecoder.end();
|
|
308
|
+
observation.emit({
|
|
309
|
+
outcome: options.signal?.aborted ? "cancelled" : timedOut ? "timed-out" : spawnError ? "spawn-failed" : "completed",
|
|
310
|
+
pid: p.pid, exitCode: signalExit || spawnError ? null : code,
|
|
311
|
+
signal: exitSignal, ...(spawnError ? { error: spawnError } : {}),
|
|
312
|
+
durationMs: Date.now() - startedAt,
|
|
313
|
+
});
|
|
267
314
|
resolve({
|
|
268
315
|
code,
|
|
269
316
|
...(signalExit ? { signalExit: true } : {}),
|
|
@@ -316,7 +363,7 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
316
363
|
options.onSpawn?.(p.pid);
|
|
317
364
|
if (options.signal?.aborted)
|
|
318
365
|
abort();
|
|
319
|
-
p.on("spawn", () => { started = true; }); // the command exists from here on — never retryable past it
|
|
366
|
+
p.on("spawn", () => { started = true; observation.emit({ outcome: "started", pid: p.pid }); }); // the command exists from here on — never retryable past it
|
|
320
367
|
// OBS-716: one stateful decoder per stream carries an incomplete UTF-8 sequence into that
|
|
321
368
|
// stream's next pipe chunk; decoding each chunk through string concatenation corrupts bytes at
|
|
322
369
|
// kernel-chosen boundaries. A deterministic fixture proves this decoder correct rather than
|
|
@@ -333,21 +380,24 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
333
380
|
stderr += stderrDecoder.write(d);
|
|
334
381
|
});
|
|
335
382
|
p.on("error", (e) => {
|
|
383
|
+
spawnError = String(e);
|
|
336
384
|
if (!done && !started && !outputSeen && e.code === RETRYABLE_SPAWN_CODE) {
|
|
337
385
|
done = true;
|
|
338
386
|
clearTimeout(timer);
|
|
339
387
|
options.signal?.removeEventListener("abort", abort);
|
|
388
|
+
observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(e), durationMs: Date.now() - startedAt });
|
|
340
389
|
resolve({ refused: e });
|
|
341
390
|
return;
|
|
342
391
|
}
|
|
343
392
|
finish(127, String(e));
|
|
344
393
|
});
|
|
345
|
-
p.on("close", (code) => { signalExit = code === null; finish(code ?? 1); });
|
|
394
|
+
p.on("close", (code, signal) => { signalExit = code === null; exitSignal = signal; finish(code ?? 1); });
|
|
346
395
|
// "close" waits for stdio to drain. Once bash exits normally, give descendants a bounded grace
|
|
347
396
|
// to exit with it; a survivor still in bash's detached group is then reaped so its inherited pipe
|
|
348
397
|
// cannot hold this promise until the command ceiling. A real timeout wins first and is never
|
|
349
398
|
// reclassified as a grace reap.
|
|
350
|
-
p.on("exit", (code) => {
|
|
399
|
+
p.on("exit", (code, signal) => {
|
|
400
|
+
exitSignal = signal;
|
|
351
401
|
signalExit = code === null;
|
|
352
402
|
exitedCode = code ?? 1;
|
|
353
403
|
// Allow a short pipe drain after killing, bounded even for an escaped descendant.
|
|
@@ -373,7 +423,9 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
373
423
|
return (async () => {
|
|
374
424
|
const startedAt = Date.now();
|
|
375
425
|
for (let n = 1;; n++) {
|
|
376
|
-
|
|
426
|
+
if (n > 1)
|
|
427
|
+
observation.begin(n);
|
|
428
|
+
observation.checkAbort();
|
|
377
429
|
const r = await attempt();
|
|
378
430
|
if (!("refused" in r))
|
|
379
431
|
return r;
|
|
@@ -386,8 +438,8 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
386
438
|
}
|
|
387
439
|
})();
|
|
388
440
|
}
|
|
389
|
-
export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
|
390
|
-
return shell(cmd, cwd, timeoutMs, true);
|
|
441
|
+
export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, options = {}) {
|
|
442
|
+
return shell(cmd, cwd, timeoutMs, true, options);
|
|
391
443
|
}
|
|
392
444
|
// Git plumbing never needs an operator profile; skip login-shell startup and its side effects.
|
|
393
445
|
export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -132,6 +132,42 @@ export declare function repairReachSinceApproval(events: JournalEvent[], taskId:
|
|
|
132
132
|
export declare function repairsSinceApproval(events: JournalEvent[], taskId: string): number;
|
|
133
133
|
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
134
134
|
export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
135
|
+
/**
|
|
136
|
+
* OBS-1075: what an accepted approval still authorises, typed by its release. `worker` funds one
|
|
137
|
+
* dispatch (plain, attempt-cap, review-upheld, scope-request); `battery` funds the recheck battery
|
|
138
|
+
* and no worker; `waiver` satisfies exactly its named gate; `inert` authorises nothing.
|
|
139
|
+
*/
|
|
140
|
+
export type PendingApprovalAction = {
|
|
141
|
+
taskId: string;
|
|
142
|
+
ts: string;
|
|
143
|
+
authority: "worker";
|
|
144
|
+
release: "plain" | typeof ATTEMPT_CAP_RELEASE | typeof REVIEW_UPHELD_RELEASE | "scope-request";
|
|
145
|
+
} | {
|
|
146
|
+
taskId: string;
|
|
147
|
+
ts: string;
|
|
148
|
+
authority: "battery";
|
|
149
|
+
release: typeof RECHECK_RELEASE;
|
|
150
|
+
} | {
|
|
151
|
+
taskId: string;
|
|
152
|
+
ts: string;
|
|
153
|
+
authority: "waiver";
|
|
154
|
+
release: typeof GATE_SATISFIED_RELEASE;
|
|
155
|
+
gate: GateName;
|
|
156
|
+
} | {
|
|
157
|
+
taskId: string;
|
|
158
|
+
ts: string;
|
|
159
|
+
authority: "inert";
|
|
160
|
+
release: unknown;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Per task, the newest task-approved row no later enactment has consumed. Pure: events in, typed
|
|
164
|
+
* actions out. Acceptance is not enactment and a run boundary is not enactment — only the row the
|
|
165
|
+
* release causes consumes it (ENACTED_BY), so an approval accepted just before an abnormal exit
|
|
166
|
+
* survives every restart until it is enacted. A later approval supersedes an earlier one. An unknown
|
|
167
|
+
* release, or a waiver without a known gate, is inert: it supersedes, authorises nothing, and no row
|
|
168
|
+
* enacts it.
|
|
169
|
+
*/
|
|
170
|
+
export declare function pendingApprovalActions(events: JournalEvent[]): Map<string, PendingApprovalAction>;
|
|
135
171
|
/**
|
|
136
172
|
* Why the last attempt failed, one row per journaled cause, in the daemon's own `source: details`
|
|
137
173
|
* shape. The daemon builds that brief in a loop-local variable, which dies with the process: a resumed
|
|
@@ -144,8 +180,11 @@ export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
|
144
180
|
* `task-dispatch`: everything between the two — worktree recreation, setup, prompt write, slot
|
|
145
181
|
* allocation, the launch itself — can still die with no worker having read a word, and clearing at
|
|
146
182
|
* task-dispatch meant `--retry-failed` after exactly that death rebuilt the prompt without the gate
|
|
147
|
-
* failures OR the delivery failure that preceded it.
|
|
148
|
-
*
|
|
183
|
+
* failures OR the delivery failure that preceded it. Of the approvals, only a WAIVE clears (the operator
|
|
184
|
+
* retired the findings by fiat — the uphold case re-derives its own brief separately). OBS-1074: a
|
|
185
|
+
* plain approve, a scope grant or a recheck re-funds an attempt that must still see why the last one
|
|
186
|
+
* parked, plus the operator's stated reason — v2.5.7's T11 looped four times on one hygiene oracle
|
|
187
|
+
* because every approval erased exactly the finding the fresh attempt was funded to fix.
|
|
149
188
|
*/
|
|
150
189
|
export declare function journaledFailureBrief(events: JournalEvent[], taskId: string): string[];
|
|
151
190
|
/**
|
|
@@ -237,6 +276,7 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
237
276
|
"gate-fail": "gate-fail";
|
|
238
277
|
quota: "quota";
|
|
239
278
|
infra: "infra";
|
|
279
|
+
"scope-request": "scope-request";
|
|
240
280
|
dispatch: "dispatch";
|
|
241
281
|
"human-gate": "human-gate";
|
|
242
282
|
"reroute-exhausted": "reroute-exhausted";
|
|
@@ -244,7 +284,6 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
244
284
|
"merge-conflict": "merge-conflict";
|
|
245
285
|
"tip-moved": "tip-moved";
|
|
246
286
|
authoring: "authoring";
|
|
247
|
-
"scope-request": "scope-request";
|
|
248
287
|
"diff-cap": "diff-cap";
|
|
249
288
|
}>>;
|
|
250
289
|
tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
@@ -300,6 +339,13 @@ export declare function gateResultJournalData(gate: string, pass: boolean, detai
|
|
|
300
339
|
signalQuality: number;
|
|
301
340
|
} & Record<string, unknown>;
|
|
302
341
|
export declare function recordedGraphDefinitionHash(events: JournalEvent[]): string | undefined;
|
|
342
|
+
/**
|
|
343
|
+
* OBS-1073 residual: the `--graph-changed` release belongs to the ENGAGEMENT, not to the launch call. The
|
|
344
|
+
* daemon writes it on the run-resume row; every replay that runs inside that engagement from another
|
|
345
|
+
* process (the approve CLI) reads it back here. Without it the first in-run approval after a released
|
|
346
|
+
* launch re-ran the whole-graph asserts the launch had waived and killed the daemon (v2.5.7 run …152220).
|
|
347
|
+
*/
|
|
348
|
+
export declare function engagementReleased(events: JournalEvent[]): boolean;
|
|
303
349
|
/** An approval is the durable authority; graph.json is only its materialized projection. */
|
|
304
350
|
export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
305
351
|
from: z.ZodString;
|
|
@@ -307,10 +353,16 @@ export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
|
307
353
|
beforeFiles: z.ZodArray<z.ZodString>;
|
|
308
354
|
files: z.ZodArray<z.ZodString>;
|
|
309
355
|
parkLine: z.ZodNumber;
|
|
356
|
+
definition: z.ZodOptional<z.ZodString>;
|
|
310
357
|
}, z.core.$strip>;
|
|
311
|
-
|
|
358
|
+
/**
|
|
359
|
+
* `release` is the operator's audited `--graph-changed`. `approvedDefinitions` supplies, per amended task,
|
|
360
|
+
* the definition fingerprint the amendment was granted against when the row itself carries none (rows
|
|
361
|
+
* older than v2.5.7): the caller reads it from the run's materialized graph snapshot.
|
|
362
|
+
*/
|
|
363
|
+
export declare function replayScopeAmendments(graph: RunGraph, events: JournalEvent[], release?: boolean, approvedDefinitions?: ReadonlyMap<string, string>): RunGraph;
|
|
312
364
|
/** Publish/recover the audit before dispatch, even after a crash between approval and rehash. */
|
|
313
|
-
export declare function applyScopeAmendments(graph: RunGraph, journal: Journal, auditReplay?: boolean): RunGraph;
|
|
365
|
+
export declare function applyScopeAmendments(graph: RunGraph, journal: Journal, auditReplay?: boolean, release?: boolean): RunGraph;
|
|
314
366
|
export type EngagementCompare = {
|
|
315
367
|
comparable: true;
|
|
316
368
|
recorded: string;
|
package/dist/run/journal.js
CHANGED
|
@@ -4,7 +4,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
7
|
-
import { graphDefinitionHash, stateDirName, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
|
|
7
|
+
import { graphDefinitionHash, stateDirName, taskContentDigest, taskDefinitionFingerprint, tickmarkrDir } from "../graph/graph.js";
|
|
8
8
|
import { GATE_NAMES, TIERS } from "../graph/schema.js";
|
|
9
9
|
import { channelRouteIdentity } from "../route/preference.js";
|
|
10
10
|
import { buildProfile, classify } from "../route/profile.js";
|
|
@@ -620,6 +620,49 @@ export function pendingRechecks(events) {
|
|
|
620
620
|
}
|
|
621
621
|
return pending;
|
|
622
622
|
}
|
|
623
|
+
const ENACTED_BY = {
|
|
624
|
+
worker: ["task-dispatch", "worker-launch"],
|
|
625
|
+
battery: ["recheck-battery"],
|
|
626
|
+
waiver: ["worktree-recreation"],
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Per task, the newest task-approved row no later enactment has consumed. Pure: events in, typed
|
|
630
|
+
* actions out. Acceptance is not enactment and a run boundary is not enactment — only the row the
|
|
631
|
+
* release causes consumes it (ENACTED_BY), so an approval accepted just before an abnormal exit
|
|
632
|
+
* survives every restart until it is enacted. A later approval supersedes an earlier one. An unknown
|
|
633
|
+
* release, or a waiver without a known gate, is inert: it supersedes, authorises nothing, and no row
|
|
634
|
+
* enacts it.
|
|
635
|
+
*/
|
|
636
|
+
export function pendingApprovalActions(events) {
|
|
637
|
+
const pending = new Map();
|
|
638
|
+
for (const e of events) {
|
|
639
|
+
if (!e.taskId)
|
|
640
|
+
continue;
|
|
641
|
+
if (e.event === "task-approved") {
|
|
642
|
+
pending.set(e.taskId, approvalAction(e.taskId, e));
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const action = pending.get(e.taskId);
|
|
646
|
+
if (action && action.authority !== "inert" && ENACTED_BY[action.authority].includes(e.event))
|
|
647
|
+
pending.delete(e.taskId);
|
|
648
|
+
}
|
|
649
|
+
return pending;
|
|
650
|
+
}
|
|
651
|
+
function approvalAction(taskId, e) {
|
|
652
|
+
const { release, gate } = e.data;
|
|
653
|
+
const base = { taskId, ts: e.ts };
|
|
654
|
+
if (release === undefined)
|
|
655
|
+
return { ...base, authority: "worker", release: "plain" };
|
|
656
|
+
if (release === ATTEMPT_CAP_RELEASE || release === REVIEW_UPHELD_RELEASE || release === "scope-request") {
|
|
657
|
+
return { ...base, authority: "worker", release };
|
|
658
|
+
}
|
|
659
|
+
if (release === RECHECK_RELEASE)
|
|
660
|
+
return { ...base, authority: "battery", release };
|
|
661
|
+
if (release === GATE_SATISFIED_RELEASE && typeof gate === "string" && GATE_NAMES.includes(gate)) {
|
|
662
|
+
return { ...base, authority: "waiver", release, gate: gate };
|
|
663
|
+
}
|
|
664
|
+
return { ...base, authority: "inert", release };
|
|
665
|
+
}
|
|
623
666
|
// Both retry decisions below govern exactly ONE dispatch: the next one. So both are read back from the
|
|
624
667
|
// journal at the moment that dispatch is built, never carried in a process variable — a stop between
|
|
625
668
|
// the decision and the dispatch (OBS-254's shape, one layer up) would otherwise send a normal prompt
|
|
@@ -656,16 +699,25 @@ function decisionForNextDispatch(events, taskId, event) {
|
|
|
656
699
|
* `task-dispatch`: everything between the two — worktree recreation, setup, prompt write, slot
|
|
657
700
|
* allocation, the launch itself — can still die with no worker having read a word, and clearing at
|
|
658
701
|
* task-dispatch meant `--retry-failed` after exactly that death rebuilt the prompt without the gate
|
|
659
|
-
* failures OR the delivery failure that preceded it.
|
|
660
|
-
*
|
|
702
|
+
* failures OR the delivery failure that preceded it. Of the approvals, only a WAIVE clears (the operator
|
|
703
|
+
* retired the findings by fiat — the uphold case re-derives its own brief separately). OBS-1074: a
|
|
704
|
+
* plain approve, a scope grant or a recheck re-funds an attempt that must still see why the last one
|
|
705
|
+
* parked, plus the operator's stated reason — v2.5.7's T11 looped four times on one hygiene oracle
|
|
706
|
+
* because every approval erased exactly the finding the fresh attempt was funded to fix.
|
|
661
707
|
*/
|
|
662
708
|
export function journaledFailureBrief(events, taskId) {
|
|
663
709
|
let rows = [];
|
|
664
710
|
for (const e of events) {
|
|
665
711
|
if (e.taskId !== taskId)
|
|
666
712
|
continue;
|
|
667
|
-
if (e.event === "worker-launch"
|
|
713
|
+
if (e.event === "worker-launch")
|
|
668
714
|
rows = [];
|
|
715
|
+
else if (e.event === "task-approved") {
|
|
716
|
+
if (e.data.release === GATE_SATISFIED_RELEASE)
|
|
717
|
+
rows = [];
|
|
718
|
+
else if (typeof e.data.reason === "string" && e.data.reason.trim())
|
|
719
|
+
rows.push(`approval: ${e.data.reason.trim()}`);
|
|
720
|
+
}
|
|
669
721
|
else if (e.event === "gate-result" && e.data.pass === false && e.data.skipped !== true
|
|
670
722
|
&& typeof e.data.details === "string")
|
|
671
723
|
rows.push(`${e.data.gate}: ${e.data.details}`);
|
|
@@ -983,12 +1035,37 @@ export function recordedGraphDefinitionHash(events) {
|
|
|
983
1035
|
}
|
|
984
1036
|
return recorded ?? undefined;
|
|
985
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* OBS-1073 residual: the `--graph-changed` release belongs to the ENGAGEMENT, not to the launch call. The
|
|
1040
|
+
* daemon writes it on the run-resume row; every replay that runs inside that engagement from another
|
|
1041
|
+
* process (the approve CLI) reads it back here. Without it the first in-run approval after a released
|
|
1042
|
+
* launch re-ran the whole-graph asserts the launch had waived and killed the daemon (v2.5.7 run …152220).
|
|
1043
|
+
*/
|
|
1044
|
+
export function engagementReleased(events) {
|
|
1045
|
+
const engagement = events.filter((e) => e.event === "run-start" || e.event === "run-resume").at(-1);
|
|
1046
|
+
return engagement?.data.graphChanged === true;
|
|
1047
|
+
}
|
|
1048
|
+
/** The run's own graph.json (copied by the daemon at every start/resume) — the definitions the last engagement ran on. */
|
|
1049
|
+
function snapshotDefinitions(journal) {
|
|
1050
|
+
const path = join(journal.dir, "graph.json");
|
|
1051
|
+
if (!existsSync(path))
|
|
1052
|
+
return new Map();
|
|
1053
|
+
const snapshot = JSON.parse(readFileSync(path, "utf8"));
|
|
1054
|
+
return new Map((snapshot.tasks ?? []).map((t) => [t.id, taskDefinitionFingerprint(t)]));
|
|
1055
|
+
}
|
|
986
1056
|
/** An approval is the durable authority; graph.json is only its materialized projection. */
|
|
987
1057
|
export const ScopeAmendmentSchema = z.object({
|
|
988
1058
|
from: z.string(), to: z.string(), beforeFiles: z.array(z.string()), files: z.array(z.string()),
|
|
989
1059
|
parkLine: z.number().int().positive(),
|
|
1060
|
+
/** OBS-1073: the amended task's definition minus files[] at approval; absent on rows older than v2.5.7. */
|
|
1061
|
+
definition: z.string().optional(),
|
|
990
1062
|
});
|
|
991
|
-
|
|
1063
|
+
/**
|
|
1064
|
+
* `release` is the operator's audited `--graph-changed`. `approvedDefinitions` supplies, per amended task,
|
|
1065
|
+
* the definition fingerprint the amendment was granted against when the row itself carries none (rows
|
|
1066
|
+
* older than v2.5.7): the caller reads it from the run's materialized graph snapshot.
|
|
1067
|
+
*/
|
|
1068
|
+
export function replayScopeAmendments(graph, events, release = false, approvedDefinitions = new Map()) {
|
|
992
1069
|
const amendments = events.filter((e) => e.event === "task-approved" && e.data.release === "scope-request")
|
|
993
1070
|
.map((event) => ({ event, amendment: ScopeAmendmentSchema.parse(event.data.amendment) }));
|
|
994
1071
|
if (!amendments.length)
|
|
@@ -1007,20 +1084,37 @@ export function replayScopeAmendments(graph, events) {
|
|
|
1007
1084
|
}
|
|
1008
1085
|
result = { ...result, tasks: result.tasks.map((t) => t.id === id ? { ...t, files: original } : t) };
|
|
1009
1086
|
}
|
|
1087
|
+
// OBS-1073: `from`/`to` bind the WHOLE graph, so a later spec repair of ANOTHER task (a pin-sweep miss
|
|
1088
|
+
// found mid-run, D-57) makes them unmatchable forever and `--graph-changed` — applied only after this
|
|
1089
|
+
// replay — never reaches its own release. Under that audited release the amended task's files[]
|
|
1090
|
+
// recognition above still holds and the caller journals the rehash; only the whole-graph identity
|
|
1091
|
+
// asserts are waived. Without the release nothing changes: fail closed.
|
|
1010
1092
|
for (const { event, amendment } of amendments) {
|
|
1011
|
-
if (
|
|
1093
|
+
if (release) {
|
|
1094
|
+
// The release waives the WHOLE-GRAPH identity (another task may lawfully have moved), never the
|
|
1095
|
+
// amended task's own definition: goal, acceptance, deps, floor must still be what was approved.
|
|
1096
|
+
const approved = amendment.definition ?? approvedDefinitions.get(event.taskId ?? "");
|
|
1097
|
+
const current = result.tasks.find((t) => t.id === event.taskId);
|
|
1098
|
+
if (approved === undefined || current === undefined) {
|
|
1099
|
+
throw new Error(`refusing scope amendment replay under --graph-changed: no approved definition fingerprint for ${event.taskId}`);
|
|
1100
|
+
}
|
|
1101
|
+
if (taskDefinitionFingerprint(current) !== approved) {
|
|
1102
|
+
throw new Error(`refusing scope amendment replay: ${event.taskId} definition changed beyond the approved amendment (goal, acceptance, deps or routing)`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (!release && graphDefinitionHash(result) !== amendment.from) {
|
|
1012
1106
|
throw new Error(`refusing scope amendment replay: graph task definition changed beyond approval for ${event.taskId}`);
|
|
1013
1107
|
}
|
|
1014
1108
|
result = { ...result, tasks: result.tasks.map((t) => t.id === event.taskId ? { ...t, files: amendment.files } : t) };
|
|
1015
|
-
if (graphDefinitionHash(result) !== amendment.to)
|
|
1109
|
+
if (!release && graphDefinitionHash(result) !== amendment.to)
|
|
1016
1110
|
throw new Error("refusing invalid scope amendment hash");
|
|
1017
1111
|
}
|
|
1018
1112
|
return result;
|
|
1019
1113
|
}
|
|
1020
1114
|
/** Publish/recover the audit before dispatch, even after a crash between approval and rehash. */
|
|
1021
|
-
export function applyScopeAmendments(graph, journal, auditReplay = false) {
|
|
1115
|
+
export function applyScopeAmendments(graph, journal, auditReplay = false, release = false) {
|
|
1022
1116
|
const events = journal.read();
|
|
1023
|
-
const result = replayScopeAmendments(graph, events); // validate all before moving any identity
|
|
1117
|
+
const result = replayScopeAmendments(graph, events, release, release ? snapshotDefinitions(journal) : new Map()); // validate all before moving any identity
|
|
1024
1118
|
const approvals = events.filter((e) => e.event === "task-approved" && e.data.release === "scope-request");
|
|
1025
1119
|
for (const approval of approvals) {
|
|
1026
1120
|
const amendment = ScopeAmendmentSchema.parse(approval.data.amendment);
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface TipVerifyResult {
|
|
|
12
12
|
spawnedCommand?: string;
|
|
13
13
|
/** Q121s: nonzero exit whose failures are ALL baseline-recorded — forgiven exactly as the battery forgives. */
|
|
14
14
|
forgiven?: boolean;
|
|
15
|
+
/** D-131: carried from a persisted per-gate verdict — this cycle did NOT execute the command. */
|
|
16
|
+
reused?: true;
|
|
15
17
|
/**
|
|
16
18
|
* OBS-534: what a nonzero exit is evidence OF, taken from the battery's own readers — `ceilingKillResult`
|
|
17
19
|
* for a kill, the shared runner classifier for everything else. `infra` means nothing was verified.
|
package/dist/run/merge.js
CHANGED
|
@@ -184,6 +184,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
|
|
|
184
184
|
gate,
|
|
185
185
|
cmd,
|
|
186
186
|
pass: hit.pass,
|
|
187
|
+
reused: true,
|
|
187
188
|
exitCode: hit.exitCode ?? 0,
|
|
188
189
|
fingerprints: hit.meta?.fingerprints ?? [],
|
|
189
190
|
...(hit.meta?.forgiven ? { forgiven: true } : {}),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure operator-page grouping over the `operator-page` rows the daemon journals (one row per
|
|
3
|
+
* delivered page, carrying the producer's `suppressed` count). This module consumes that count and
|
|
4
|
+
* never throttles a second time: it only coalesces EQUIVALENT rows for display while keeping every
|
|
5
|
+
* source line, both evidence timestamps, and the producer's suppressed count separate from what was
|
|
6
|
+
* actually observed. Folding is incremental and pure; `fold(fold(empty, a), b)` equals
|
|
7
|
+
* `fold(empty, [...a, ...b])` and no journal byte is touched.
|
|
8
|
+
*/
|
|
9
|
+
import type { JournalEvent } from "./journal.js";
|
|
10
|
+
export interface OperatorPageRow {
|
|
11
|
+
/** Source reference (journal line number or any stable locator). */
|
|
12
|
+
readonly line: number;
|
|
13
|
+
readonly ts: string;
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly taskId: string;
|
|
16
|
+
/** Current park identity (e.g. park kind + attempt); part of the group key. */
|
|
17
|
+
readonly park: string;
|
|
18
|
+
/** Decision state (e.g. blocked / idle / failed / resolved); part of the group key. */
|
|
19
|
+
readonly status: string;
|
|
20
|
+
/** Producer-suppressed page count; absent on rows written before suppression metadata existed. */
|
|
21
|
+
readonly suppressed?: number;
|
|
22
|
+
readonly blocker?: string;
|
|
23
|
+
readonly owner?: string;
|
|
24
|
+
readonly requiredAction?: string;
|
|
25
|
+
readonly permittedActions?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export interface OperatorPageGroup {
|
|
28
|
+
readonly key: string;
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
readonly taskId: string;
|
|
31
|
+
readonly park: string;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
readonly blocker?: string;
|
|
34
|
+
readonly owner?: string;
|
|
35
|
+
readonly requiredAction?: string;
|
|
36
|
+
readonly permittedActions: readonly string[];
|
|
37
|
+
readonly firstEvidenceAt: string;
|
|
38
|
+
readonly lastEvidenceAt: string;
|
|
39
|
+
/** Rows actually present in the journal. */
|
|
40
|
+
readonly observedCount: number;
|
|
41
|
+
/** Sum of the producer's `suppressed` counts; never added to observedCount. */
|
|
42
|
+
readonly suppressedCount: number;
|
|
43
|
+
/** True when at least one row lacked suppression metadata (pre-metadata journal). */
|
|
44
|
+
readonly rawOnly: boolean;
|
|
45
|
+
readonly lines: readonly number[];
|
|
46
|
+
}
|
|
47
|
+
export interface OperatorPageSummary {
|
|
48
|
+
readonly groups: readonly OperatorPageGroup[];
|
|
49
|
+
}
|
|
50
|
+
export declare const EMPTY_OPERATOR_PAGE_SUMMARY: OperatorPageSummary;
|
|
51
|
+
/** Projects a journal event onto a row; returns undefined for anything that is not an operator page. */
|
|
52
|
+
export declare function operatorPageRow(event: JournalEvent, line: number, runId: string): OperatorPageRow | undefined;
|
|
53
|
+
/** Folds rows into a summary. Pure: returns a new summary, never mutates inputs. */
|
|
54
|
+
export declare function foldOperatorPages(prior: OperatorPageSummary, rows: readonly OperatorPageRow[]): OperatorPageSummary;
|
|
55
|
+
/** Full replay from journal events. */
|
|
56
|
+
export declare function summarizeOperatorPages(events: readonly JournalEvent[], runId: string): OperatorPageSummary;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const EMPTY_OPERATOR_PAGE_SUMMARY = { groups: [] };
|
|
2
|
+
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
3
|
+
/** Projects a journal event onto a row; returns undefined for anything that is not an operator page. */
|
|
4
|
+
export function operatorPageRow(event, line, runId) {
|
|
5
|
+
if (event.event !== "operator-page" || event.taskId === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
const d = event.data;
|
|
8
|
+
const permitted = Array.isArray(d.permittedActions) ? d.permittedActions.filter((a) => typeof a === "string") : undefined;
|
|
9
|
+
return {
|
|
10
|
+
line, ts: event.ts, runId, taskId: event.taskId,
|
|
11
|
+
park: str(d.park) ?? `${str(d.slot) ?? ""}#${typeof d.attempt === "number" ? d.attempt : ""}`,
|
|
12
|
+
status: str(d.status) ?? "unknown",
|
|
13
|
+
...(typeof d.suppressed === "number" ? { suppressed: d.suppressed } : {}),
|
|
14
|
+
...(str(d.blocker) !== undefined ? { blocker: str(d.blocker) } : {}),
|
|
15
|
+
...(str(d.owner) !== undefined ? { owner: str(d.owner) } : {}),
|
|
16
|
+
...(str(d.requiredAction) !== undefined ? { requiredAction: str(d.requiredAction) } : {}),
|
|
17
|
+
...(permitted !== undefined ? { permittedActions: permitted } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const groupKey = (r) => [r.runId, r.taskId, r.park, r.status].join("\u0000");
|
|
21
|
+
const openKey = (r) => `${r.runId}\u0000${r.taskId}`;
|
|
22
|
+
/** Permitted actions compare as a SET: order and duplicates never open a new group. */
|
|
23
|
+
const actionSet = (a) => [...new Set(a ?? [])].sort().join("\u0000");
|
|
24
|
+
const sameDecision = (g, r) => g.key === groupKey(r)
|
|
25
|
+
&& g.blocker === r.blocker && g.owner === r.owner && g.requiredAction === r.requiredAction
|
|
26
|
+
&& actionSet(g.permittedActions) === actionSet(r.permittedActions);
|
|
27
|
+
/** Folds rows into a summary. Pure: returns a new summary, never mutates inputs. */
|
|
28
|
+
export function foldOperatorPages(prior, rows) {
|
|
29
|
+
const groups = [...prior.groups];
|
|
30
|
+
// One open group per run/task: ANY change in park, status or decision closes it, so a later
|
|
31
|
+
// transition can never merge backward into an earlier group across an intervening change.
|
|
32
|
+
const open = new Map();
|
|
33
|
+
groups.forEach((g, i) => open.set(openKey(g), i));
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const key = groupKey(row);
|
|
36
|
+
const idx = open.get(openKey(row));
|
|
37
|
+
const g = idx === undefined ? undefined : groups[idx];
|
|
38
|
+
if (g !== undefined && sameDecision(g, row)) {
|
|
39
|
+
groups[idx] = {
|
|
40
|
+
...g,
|
|
41
|
+
firstEvidenceAt: row.ts < g.firstEvidenceAt ? row.ts : g.firstEvidenceAt,
|
|
42
|
+
lastEvidenceAt: row.ts > g.lastEvidenceAt ? row.ts : g.lastEvidenceAt,
|
|
43
|
+
observedCount: g.observedCount + 1,
|
|
44
|
+
suppressedCount: g.suppressedCount + (row.suppressed ?? 0),
|
|
45
|
+
rawOnly: g.rawOnly || row.suppressed === undefined,
|
|
46
|
+
lines: [...g.lines, row.line],
|
|
47
|
+
};
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
groups.push({
|
|
51
|
+
key, runId: row.runId, taskId: row.taskId, park: row.park, status: row.status,
|
|
52
|
+
...(row.blocker !== undefined ? { blocker: row.blocker } : {}),
|
|
53
|
+
...(row.owner !== undefined ? { owner: row.owner } : {}),
|
|
54
|
+
...(row.requiredAction !== undefined ? { requiredAction: row.requiredAction } : {}),
|
|
55
|
+
permittedActions: [...(row.permittedActions ?? [])],
|
|
56
|
+
firstEvidenceAt: row.ts, lastEvidenceAt: row.ts,
|
|
57
|
+
observedCount: 1, suppressedCount: row.suppressed ?? 0,
|
|
58
|
+
rawOnly: row.suppressed === undefined, lines: [row.line],
|
|
59
|
+
});
|
|
60
|
+
open.set(openKey(row), groups.length - 1);
|
|
61
|
+
}
|
|
62
|
+
return { groups };
|
|
63
|
+
}
|
|
64
|
+
/** Full replay from journal events. */
|
|
65
|
+
export function summarizeOperatorPages(events, runId) {
|
|
66
|
+
const rows = events.flatMap((e, i) => { const r = operatorPageRow(e, i + 1, runId); return r ? [r] : []; });
|
|
67
|
+
return foldOperatorPages(EMPTY_OPERATOR_PAGE_SUMMARY, rows);
|
|
68
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Labels describe blockers, not journal park kinds or commands. */
|
|
2
|
+
export declare const BLOCKER_KINDS: readonly ["human-decision", "retry", "cooldown", "dependency-wait", "stale-receipt", "missing-capability", "unknown"];
|
|
3
|
+
export type BlockerKind = (typeof BLOCKER_KINDS)[number];
|
|
4
|
+
/**
|
|
5
|
+
* Structural boundary: callers compute park with newestPark and verbs with
|
|
6
|
+
* permittedDecisionVerbs in cli/commands/approve. RunDecision also fits this
|
|
7
|
+
* shape. This module neither imports a surface nor repeats its decision table.
|
|
8
|
+
* Supply only current parks, from the same snapshot as the task statuses.
|
|
9
|
+
*/
|
|
10
|
+
export interface OperatorDecisionSnapshot {
|
|
11
|
+
readonly taskId: string;
|
|
12
|
+
readonly park: {
|
|
13
|
+
readonly kind: string | undefined;
|
|
14
|
+
readonly tombstone: boolean;
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
};
|
|
17
|
+
readonly verbs: readonly string[];
|
|
18
|
+
readonly diagnostic?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RecordedResponsibility {
|
|
21
|
+
readonly role?: string;
|
|
22
|
+
readonly agent?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Current, already-reduced evidence supplied by the reader. Optional facts stay
|
|
26
|
+
* absent: a wait duration is not a wake timestamp, a channel is not an owner,
|
|
27
|
+
* and elapsed wall time alone cannot establish that a receipt is stale.
|
|
28
|
+
*/
|
|
29
|
+
export interface OperatorSummaryTask {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly status: string;
|
|
32
|
+
readonly deps: readonly string[];
|
|
33
|
+
readonly phase?: string;
|
|
34
|
+
readonly lastEvidenceAt?: string;
|
|
35
|
+
readonly responsible?: RecordedResponsibility;
|
|
36
|
+
readonly wait?: {
|
|
37
|
+
readonly kind: "retry" | "cooldown";
|
|
38
|
+
readonly wakeAt?: string;
|
|
39
|
+
readonly diagnostic?: string;
|
|
40
|
+
};
|
|
41
|
+
readonly staleReceipt?: {
|
|
42
|
+
readonly diagnostic: string;
|
|
43
|
+
};
|
|
44
|
+
readonly missingCapability?: {
|
|
45
|
+
readonly capability: string;
|
|
46
|
+
readonly diagnostic?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface OperatorBlocker {
|
|
50
|
+
kind: BlockerKind;
|
|
51
|
+
permittedActions: string[];
|
|
52
|
+
humanInterventionRequired: boolean;
|
|
53
|
+
decisionRequired: boolean;
|
|
54
|
+
diagnostic?: string;
|
|
55
|
+
nextAction: string | null;
|
|
56
|
+
prerequisites?: string[];
|
|
57
|
+
wakeAt?: string;
|
|
58
|
+
capability?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface OperatorTaskSummary {
|
|
61
|
+
taskId: string;
|
|
62
|
+
phase: string | null;
|
|
63
|
+
lastEvidenceAt: string | null;
|
|
64
|
+
/** null explicitly means unknown; never inferred from a park or wait. */
|
|
65
|
+
responsible: RecordedResponsibility | null;
|
|
66
|
+
blocker: OperatorBlocker | null;
|
|
67
|
+
}
|
|
68
|
+
/** Pure projection in current-task order; no clock, host access, or retained state. */
|
|
69
|
+
export declare function projectOperatorSummary(tasks: readonly OperatorSummaryTask[], decisions: readonly OperatorDecisionSnapshot[]): OperatorTaskSummary[];
|