tickmarkr 2.5.6 → 2.5.7
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 +5 -5
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +6 -4
- 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 +4 -1
- package/dist/gates/run-gates.js +65 -9
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +20 -2
- package/dist/gates/test-reporter.js +6 -1
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +5 -0
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/daemon.d.ts +9 -0
- package/dist/run/daemon.js +200 -46
- package/dist/run/git.d.ts +6 -1
- package/dist/run/git.js +63 -11
- package/dist/run/journal.d.ts +20 -4
- package/dist/run/journal.js +60 -9
- 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/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/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/skills/tickmarkr-overseer/SKILL.md +55 -3
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
|
@@ -144,8 +144,11 @@ export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
|
144
144
|
* `task-dispatch`: everything between the two — worktree recreation, setup, prompt write, slot
|
|
145
145
|
* allocation, the launch itself — can still die with no worker having read a word, and clearing at
|
|
146
146
|
* 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
|
-
*
|
|
147
|
+
* failures OR the delivery failure that preceded it. Of the approvals, only a WAIVE clears (the operator
|
|
148
|
+
* retired the findings by fiat — the uphold case re-derives its own brief separately). OBS-1074: a
|
|
149
|
+
* plain approve, a scope grant or a recheck re-funds an attempt that must still see why the last one
|
|
150
|
+
* parked, plus the operator's stated reason — v2.5.7's T11 looped four times on one hygiene oracle
|
|
151
|
+
* because every approval erased exactly the finding the fresh attempt was funded to fix.
|
|
149
152
|
*/
|
|
150
153
|
export declare function journaledFailureBrief(events: JournalEvent[], taskId: string): string[];
|
|
151
154
|
/**
|
|
@@ -300,6 +303,13 @@ export declare function gateResultJournalData(gate: string, pass: boolean, detai
|
|
|
300
303
|
signalQuality: number;
|
|
301
304
|
} & Record<string, unknown>;
|
|
302
305
|
export declare function recordedGraphDefinitionHash(events: JournalEvent[]): string | undefined;
|
|
306
|
+
/**
|
|
307
|
+
* OBS-1073 residual: the `--graph-changed` release belongs to the ENGAGEMENT, not to the launch call. The
|
|
308
|
+
* daemon writes it on the run-resume row; every replay that runs inside that engagement from another
|
|
309
|
+
* process (the approve CLI) reads it back here. Without it the first in-run approval after a released
|
|
310
|
+
* launch re-ran the whole-graph asserts the launch had waived and killed the daemon (v2.5.7 run …152220).
|
|
311
|
+
*/
|
|
312
|
+
export declare function engagementReleased(events: JournalEvent[]): boolean;
|
|
303
313
|
/** An approval is the durable authority; graph.json is only its materialized projection. */
|
|
304
314
|
export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
305
315
|
from: z.ZodString;
|
|
@@ -307,10 +317,16 @@ export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
|
307
317
|
beforeFiles: z.ZodArray<z.ZodString>;
|
|
308
318
|
files: z.ZodArray<z.ZodString>;
|
|
309
319
|
parkLine: z.ZodNumber;
|
|
320
|
+
definition: z.ZodOptional<z.ZodString>;
|
|
310
321
|
}, z.core.$strip>;
|
|
311
|
-
|
|
322
|
+
/**
|
|
323
|
+
* `release` is the operator's audited `--graph-changed`. `approvedDefinitions` supplies, per amended task,
|
|
324
|
+
* the definition fingerprint the amendment was granted against when the row itself carries none (rows
|
|
325
|
+
* older than v2.5.7): the caller reads it from the run's materialized graph snapshot.
|
|
326
|
+
*/
|
|
327
|
+
export declare function replayScopeAmendments(graph: RunGraph, events: JournalEvent[], release?: boolean, approvedDefinitions?: ReadonlyMap<string, string>): RunGraph;
|
|
312
328
|
/** 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;
|
|
329
|
+
export declare function applyScopeAmendments(graph: RunGraph, journal: Journal, auditReplay?: boolean, release?: boolean): RunGraph;
|
|
314
330
|
export type EngagementCompare = {
|
|
315
331
|
comparable: true;
|
|
316
332
|
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";
|
|
@@ -656,16 +656,25 @@ function decisionForNextDispatch(events, taskId, event) {
|
|
|
656
656
|
* `task-dispatch`: everything between the two — worktree recreation, setup, prompt write, slot
|
|
657
657
|
* allocation, the launch itself — can still die with no worker having read a word, and clearing at
|
|
658
658
|
* 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
|
-
*
|
|
659
|
+
* failures OR the delivery failure that preceded it. Of the approvals, only a WAIVE clears (the operator
|
|
660
|
+
* retired the findings by fiat — the uphold case re-derives its own brief separately). OBS-1074: a
|
|
661
|
+
* plain approve, a scope grant or a recheck re-funds an attempt that must still see why the last one
|
|
662
|
+
* parked, plus the operator's stated reason — v2.5.7's T11 looped four times on one hygiene oracle
|
|
663
|
+
* because every approval erased exactly the finding the fresh attempt was funded to fix.
|
|
661
664
|
*/
|
|
662
665
|
export function journaledFailureBrief(events, taskId) {
|
|
663
666
|
let rows = [];
|
|
664
667
|
for (const e of events) {
|
|
665
668
|
if (e.taskId !== taskId)
|
|
666
669
|
continue;
|
|
667
|
-
if (e.event === "worker-launch"
|
|
670
|
+
if (e.event === "worker-launch")
|
|
668
671
|
rows = [];
|
|
672
|
+
else if (e.event === "task-approved") {
|
|
673
|
+
if (e.data.release === GATE_SATISFIED_RELEASE)
|
|
674
|
+
rows = [];
|
|
675
|
+
else if (typeof e.data.reason === "string" && e.data.reason.trim())
|
|
676
|
+
rows.push(`approval: ${e.data.reason.trim()}`);
|
|
677
|
+
}
|
|
669
678
|
else if (e.event === "gate-result" && e.data.pass === false && e.data.skipped !== true
|
|
670
679
|
&& typeof e.data.details === "string")
|
|
671
680
|
rows.push(`${e.data.gate}: ${e.data.details}`);
|
|
@@ -983,12 +992,37 @@ export function recordedGraphDefinitionHash(events) {
|
|
|
983
992
|
}
|
|
984
993
|
return recorded ?? undefined;
|
|
985
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* OBS-1073 residual: the `--graph-changed` release belongs to the ENGAGEMENT, not to the launch call. The
|
|
997
|
+
* daemon writes it on the run-resume row; every replay that runs inside that engagement from another
|
|
998
|
+
* process (the approve CLI) reads it back here. Without it the first in-run approval after a released
|
|
999
|
+
* launch re-ran the whole-graph asserts the launch had waived and killed the daemon (v2.5.7 run …152220).
|
|
1000
|
+
*/
|
|
1001
|
+
export function engagementReleased(events) {
|
|
1002
|
+
const engagement = events.filter((e) => e.event === "run-start" || e.event === "run-resume").at(-1);
|
|
1003
|
+
return engagement?.data.graphChanged === true;
|
|
1004
|
+
}
|
|
1005
|
+
/** The run's own graph.json (copied by the daemon at every start/resume) — the definitions the last engagement ran on. */
|
|
1006
|
+
function snapshotDefinitions(journal) {
|
|
1007
|
+
const path = join(journal.dir, "graph.json");
|
|
1008
|
+
if (!existsSync(path))
|
|
1009
|
+
return new Map();
|
|
1010
|
+
const snapshot = JSON.parse(readFileSync(path, "utf8"));
|
|
1011
|
+
return new Map((snapshot.tasks ?? []).map((t) => [t.id, taskDefinitionFingerprint(t)]));
|
|
1012
|
+
}
|
|
986
1013
|
/** An approval is the durable authority; graph.json is only its materialized projection. */
|
|
987
1014
|
export const ScopeAmendmentSchema = z.object({
|
|
988
1015
|
from: z.string(), to: z.string(), beforeFiles: z.array(z.string()), files: z.array(z.string()),
|
|
989
1016
|
parkLine: z.number().int().positive(),
|
|
1017
|
+
/** OBS-1073: the amended task's definition minus files[] at approval; absent on rows older than v2.5.7. */
|
|
1018
|
+
definition: z.string().optional(),
|
|
990
1019
|
});
|
|
991
|
-
|
|
1020
|
+
/**
|
|
1021
|
+
* `release` is the operator's audited `--graph-changed`. `approvedDefinitions` supplies, per amended task,
|
|
1022
|
+
* the definition fingerprint the amendment was granted against when the row itself carries none (rows
|
|
1023
|
+
* older than v2.5.7): the caller reads it from the run's materialized graph snapshot.
|
|
1024
|
+
*/
|
|
1025
|
+
export function replayScopeAmendments(graph, events, release = false, approvedDefinitions = new Map()) {
|
|
992
1026
|
const amendments = events.filter((e) => e.event === "task-approved" && e.data.release === "scope-request")
|
|
993
1027
|
.map((event) => ({ event, amendment: ScopeAmendmentSchema.parse(event.data.amendment) }));
|
|
994
1028
|
if (!amendments.length)
|
|
@@ -1007,20 +1041,37 @@ export function replayScopeAmendments(graph, events) {
|
|
|
1007
1041
|
}
|
|
1008
1042
|
result = { ...result, tasks: result.tasks.map((t) => t.id === id ? { ...t, files: original } : t) };
|
|
1009
1043
|
}
|
|
1044
|
+
// OBS-1073: `from`/`to` bind the WHOLE graph, so a later spec repair of ANOTHER task (a pin-sweep miss
|
|
1045
|
+
// found mid-run, D-57) makes them unmatchable forever and `--graph-changed` — applied only after this
|
|
1046
|
+
// replay — never reaches its own release. Under that audited release the amended task's files[]
|
|
1047
|
+
// recognition above still holds and the caller journals the rehash; only the whole-graph identity
|
|
1048
|
+
// asserts are waived. Without the release nothing changes: fail closed.
|
|
1010
1049
|
for (const { event, amendment } of amendments) {
|
|
1011
|
-
if (
|
|
1050
|
+
if (release) {
|
|
1051
|
+
// The release waives the WHOLE-GRAPH identity (another task may lawfully have moved), never the
|
|
1052
|
+
// amended task's own definition: goal, acceptance, deps, floor must still be what was approved.
|
|
1053
|
+
const approved = amendment.definition ?? approvedDefinitions.get(event.taskId ?? "");
|
|
1054
|
+
const current = result.tasks.find((t) => t.id === event.taskId);
|
|
1055
|
+
if (approved === undefined || current === undefined) {
|
|
1056
|
+
throw new Error(`refusing scope amendment replay under --graph-changed: no approved definition fingerprint for ${event.taskId}`);
|
|
1057
|
+
}
|
|
1058
|
+
if (taskDefinitionFingerprint(current) !== approved) {
|
|
1059
|
+
throw new Error(`refusing scope amendment replay: ${event.taskId} definition changed beyond the approved amendment (goal, acceptance, deps or routing)`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (!release && graphDefinitionHash(result) !== amendment.from) {
|
|
1012
1063
|
throw new Error(`refusing scope amendment replay: graph task definition changed beyond approval for ${event.taskId}`);
|
|
1013
1064
|
}
|
|
1014
1065
|
result = { ...result, tasks: result.tasks.map((t) => t.id === event.taskId ? { ...t, files: amendment.files } : t) };
|
|
1015
|
-
if (graphDefinitionHash(result) !== amendment.to)
|
|
1066
|
+
if (!release && graphDefinitionHash(result) !== amendment.to)
|
|
1016
1067
|
throw new Error("refusing invalid scope amendment hash");
|
|
1017
1068
|
}
|
|
1018
1069
|
return result;
|
|
1019
1070
|
}
|
|
1020
1071
|
/** Publish/recover the audit before dispatch, even after a crash between approval and rehash. */
|
|
1021
|
-
export function applyScopeAmendments(graph, journal, auditReplay = false) {
|
|
1072
|
+
export function applyScopeAmendments(graph, journal, auditReplay = false, release = false) {
|
|
1022
1073
|
const events = journal.read();
|
|
1023
|
-
const result = replayScopeAmendments(graph, events); // validate all before moving any identity
|
|
1074
|
+
const result = replayScopeAmendments(graph, events, release, release ? snapshotDefinitions(journal) : new Map()); // validate all before moving any identity
|
|
1024
1075
|
const approvals = events.filter((e) => e.event === "task-approved" && e.data.release === "scope-request");
|
|
1025
1076
|
for (const approval of approvals) {
|
|
1026
1077
|
const amendment = ScopeAmendmentSchema.parse(approval.data.amendment);
|
|
@@ -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[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Labels describe blockers, not journal park kinds or commands. */
|
|
2
|
+
export const BLOCKER_KINDS = [
|
|
3
|
+
"human-decision", "retry", "cooldown", "dependency-wait", "stale-receipt",
|
|
4
|
+
"missing-capability", "unknown",
|
|
5
|
+
];
|
|
6
|
+
const SETTLED = new Set(["done", "completed", "merged"]);
|
|
7
|
+
const emptyBlocker = (kind) => ({
|
|
8
|
+
kind, permittedActions: [], humanInterventionRequired: false,
|
|
9
|
+
decisionRequired: false, nextAction: null,
|
|
10
|
+
});
|
|
11
|
+
/** Pure projection in current-task order; no clock, host access, or retained state. */
|
|
12
|
+
export function projectOperatorSummary(tasks, decisions) {
|
|
13
|
+
const statuses = new Map(tasks.map(task => [task.id, task.status]));
|
|
14
|
+
const parks = new Map(decisions.map(decision => [decision.taskId, decision]));
|
|
15
|
+
return tasks.map(task => {
|
|
16
|
+
let blocker = null;
|
|
17
|
+
const decision = task.status === "human" ? parks.get(task.id) : undefined;
|
|
18
|
+
const terminal = SETTLED.has(task.status) || task.status === "failed";
|
|
19
|
+
if (decision) {
|
|
20
|
+
const { park, verbs, diagnostic } = decision;
|
|
21
|
+
blocker = {
|
|
22
|
+
...emptyBlocker(park.tombstone ? "unknown" : "human-decision"),
|
|
23
|
+
permittedActions: [...verbs],
|
|
24
|
+
humanInterventionRequired: !park.tombstone,
|
|
25
|
+
decisionRequired: verbs.length > 0,
|
|
26
|
+
...(diagnostic !== undefined || park.reason !== undefined
|
|
27
|
+
? { diagnostic: diagnostic ?? park.reason } : {}),
|
|
28
|
+
nextAction: verbs.length > 0 ? `Choose a decision: ${verbs.join(", ")}` : null,
|
|
29
|
+
};
|
|
30
|
+
if (!park.tombstone && park.kind === "infra" && task.missingCapability) {
|
|
31
|
+
const { capability, diagnostic: capabilityDiagnostic } = task.missingCapability;
|
|
32
|
+
blocker.kind = "missing-capability";
|
|
33
|
+
blocker.capability = capability;
|
|
34
|
+
blocker.nextAction = `Diagnose host capability: ${capability}`;
|
|
35
|
+
if (diagnostic === undefined && capabilityDiagnostic !== undefined)
|
|
36
|
+
blocker.diagnostic = capabilityDiagnostic;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (task.status === "human") {
|
|
40
|
+
// Missing decision evidence must not make a parked task look automatic.
|
|
41
|
+
blocker = { ...emptyBlocker("unknown"), humanInterventionRequired: true };
|
|
42
|
+
}
|
|
43
|
+
else if (!terminal) {
|
|
44
|
+
const unmet = task.status === "pending" ? task.deps.filter(id => !SETTLED.has(statuses.get(id))) : [];
|
|
45
|
+
if (unmet.length > 0) {
|
|
46
|
+
blocker = {
|
|
47
|
+
...emptyBlocker("dependency-wait"), prerequisites: unmet,
|
|
48
|
+
nextAction: `Wait for prerequisites: ${unmet.join(", ")}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else if (task.staleReceipt) {
|
|
52
|
+
blocker = {
|
|
53
|
+
...emptyBlocker("stale-receipt"), diagnostic: task.staleReceipt.diagnostic,
|
|
54
|
+
nextAction: "Inspect the stale receipt and refresh its evidence",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else if (task.wait) {
|
|
58
|
+
blocker = {
|
|
59
|
+
...emptyBlocker(task.wait.kind),
|
|
60
|
+
...(task.wait.diagnostic === undefined ? {} : { diagnostic: task.wait.diagnostic }),
|
|
61
|
+
...(task.wait.wakeAt === undefined ? {} : { wakeAt: task.wait.wakeAt }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else if (task.status === "blocked" || task.status === "unknown") {
|
|
65
|
+
blocker = emptyBlocker("unknown");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const owner = task.responsible;
|
|
69
|
+
return {
|
|
70
|
+
taskId: task.id, phase: task.phase ?? null, lastEvidenceAt: task.lastEvidenceAt ?? null,
|
|
71
|
+
responsible: owner?.role !== undefined || owner?.agent !== undefined
|
|
72
|
+
? { ...(owner.role === undefined ? {} : { role: owner.role }),
|
|
73
|
+
...(owner.agent === undefined ? {} : { agent: owner.agent }) } : null,
|
|
74
|
+
blocker,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
package/dist/run/protocol.d.ts
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type GateName } from "../graph/schema.js";
|
|
3
3
|
import { type GateOutcome } from "./outcome.js";
|
|
4
|
+
/** Shared lifecycle vocabulary for shell evidence and later build-result emitters. */
|
|
5
|
+
export declare const COMMAND_RECEIPT_OUTCOMES: readonly ["started", "completed", "spawn-failed", "timed-out", "cancelled", "reused-result", "skipped", "refused"];
|
|
6
|
+
export declare const CommandReceiptAttributionSchema: z.ZodObject<{
|
|
7
|
+
runId: z.ZodString;
|
|
8
|
+
taskId: z.ZodString;
|
|
9
|
+
attempt: z.ZodNumber;
|
|
10
|
+
gateRound: z.ZodNumber;
|
|
11
|
+
invocation: z.ZodString;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type CommandReceiptAttribution = z.infer<typeof CommandReceiptAttributionSchema>;
|
|
14
|
+
export declare const ShellReceiptSchema: z.ZodObject<{
|
|
15
|
+
outcome: z.ZodEnum<{
|
|
16
|
+
skipped: "skipped";
|
|
17
|
+
started: "started";
|
|
18
|
+
completed: "completed";
|
|
19
|
+
"spawn-failed": "spawn-failed";
|
|
20
|
+
"timed-out": "timed-out";
|
|
21
|
+
cancelled: "cancelled";
|
|
22
|
+
"reused-result": "reused-result";
|
|
23
|
+
refused: "refused";
|
|
24
|
+
}>;
|
|
25
|
+
confirmedStart: z.ZodBoolean;
|
|
26
|
+
attribution: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
runId: z.ZodString;
|
|
28
|
+
taskId: z.ZodString;
|
|
29
|
+
attempt: z.ZodNumber;
|
|
30
|
+
gateRound: z.ZodNumber;
|
|
31
|
+
invocation: z.ZodString;
|
|
32
|
+
}, z.core.$strict>>;
|
|
33
|
+
pid: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
exitCode: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
35
|
+
signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
36
|
+
error: z.ZodOptional<z.ZodString>;
|
|
37
|
+
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
export type ShellReceipt = z.infer<typeof ShellReceiptSchema>;
|
|
40
|
+
/** A task-build row must carry the entire caller-owned correlation, even before spawn. */
|
|
41
|
+
export declare const CommandReceiptSchema: z.ZodObject<{
|
|
42
|
+
outcome: z.ZodEnum<{
|
|
43
|
+
skipped: "skipped";
|
|
44
|
+
started: "started";
|
|
45
|
+
completed: "completed";
|
|
46
|
+
"spawn-failed": "spawn-failed";
|
|
47
|
+
"timed-out": "timed-out";
|
|
48
|
+
cancelled: "cancelled";
|
|
49
|
+
"reused-result": "reused-result";
|
|
50
|
+
refused: "refused";
|
|
51
|
+
}>;
|
|
52
|
+
confirmedStart: z.ZodBoolean;
|
|
53
|
+
pid: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
exitCode: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
55
|
+
signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
56
|
+
error: z.ZodOptional<z.ZodString>;
|
|
57
|
+
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
attribution: z.ZodObject<{
|
|
59
|
+
runId: z.ZodString;
|
|
60
|
+
taskId: z.ZodString;
|
|
61
|
+
attempt: z.ZodNumber;
|
|
62
|
+
gateRound: z.ZodNumber;
|
|
63
|
+
invocation: z.ZodString;
|
|
64
|
+
}, z.core.$strict>;
|
|
65
|
+
}, z.core.$strict>;
|
|
66
|
+
export type CommandReceipt = z.infer<typeof CommandReceiptSchema>;
|
|
67
|
+
export declare function readCommandReceipt(raw: unknown): {
|
|
68
|
+
kind: "receipt";
|
|
69
|
+
receipt: CommandReceipt;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "protocol-issue";
|
|
72
|
+
issues: string[];
|
|
73
|
+
raw: unknown;
|
|
74
|
+
};
|
|
4
75
|
/** The T31 outcome vocabulary, made executable and strict at the persistence boundary. */
|
|
5
76
|
export declare const GateOutcomeSchema: z.ZodType<GateOutcome>;
|
|
6
77
|
export declare const ROLE_INVOCATION_ROLES: readonly ["worker", "judge", "review", "consult"];
|