tickmarkr 2.6.3 → 2.6.5
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 +10 -2
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +25 -0
- package/dist/adapters/codex-commit-check.js +108 -0
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +2 -1
- package/dist/adapters/model-lints.js +52 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +13 -0
- package/dist/cli/commands/approve.js +37 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +20 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +279 -95
- package/dist/cli/commands/init.js +10 -4
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/report.d.ts +39 -2
- package/dist/cli/commands/report.js +227 -10
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +30 -3
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +356 -123
- package/dist/compile/native.js +130 -0
- package/dist/config/config.d.ts +25 -7
- package/dist/config/config.js +92 -11
- package/dist/config/fleet-overlay.d.ts +2 -1
- package/dist/config/fleet-overlay.js +77 -4
- package/dist/drivers/orca.d.ts +19 -1
- package/dist/drivers/orca.js +161 -31
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +3 -2
- package/dist/gates/run-gates.js +91 -50
- package/dist/route/router.d.ts +11 -0
- package/dist/route/router.js +29 -8
- package/dist/run/daemon.js +158 -22
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +101 -1
- package/dist/run/journal.js +352 -2
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +30 -1
- package/dist/tui/cockpit/live-store.js +134 -18
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +12 -6
- package/dist/tui/ink/fleet-app.js +17 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +33 -3
- package/skills/tickmarkr-loop/SKILL.md +55 -5
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
package/dist/run/journal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -6,9 +7,10 @@ import { z } from "zod";
|
|
|
6
7
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
7
8
|
import { graphDefinitionHash, stateDirName, taskContentDigest, taskDefinitionFingerprint, tickmarkrDir } from "../graph/graph.js";
|
|
8
9
|
import { GATE_NAMES, TIERS } from "../graph/schema.js";
|
|
9
|
-
import { channelRouteIdentity } from "../route/preference.js";
|
|
10
|
+
import { channelRouteIdentity, modelProvider } from "../route/preference.js";
|
|
10
11
|
import { buildProfile, classify } from "../route/profile.js";
|
|
11
12
|
import { DecisionEventSchema, trackJournalRows, } from "./protocol.js";
|
|
13
|
+
import { PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER } from "./git.js";
|
|
12
14
|
import { normalizeGateOutcome } from "./outcome.js";
|
|
13
15
|
import { redactSecrets } from "./redact.js";
|
|
14
16
|
export function phaseForGate(gate) {
|
|
@@ -1936,7 +1938,11 @@ export class Journal {
|
|
|
1936
1938
|
const reducedData = event === "run-end"
|
|
1937
1939
|
? (() => {
|
|
1938
1940
|
const preservedRefs = [...preservedRefsByTask(priorEvents)].flatMap(([preservedTaskId, refs]) => refs.map(({ ref, diffCommand }) => ({ taskId: preservedTaskId, ref, diffCommand })));
|
|
1939
|
-
|
|
1941
|
+
const reduced = preservedRefs.length > 0 ? { ...inputData, preservedRefs } : inputData;
|
|
1942
|
+
// C1: the terminal record carries the owed-check fold as of this append, revalidated from
|
|
1943
|
+
// each discharge's artifact — a reopened journal re-reduces, it never copies a prior total.
|
|
1944
|
+
// Canonical (validated) rows are strict and keep their schema; the live producer is the tuple.
|
|
1945
|
+
return decisionRow ? reduced : { ...reduced, owedChecks: foldOwedChecks(priorEvents, join(this.dir, "..", "..", "..")) };
|
|
1940
1946
|
})()
|
|
1941
1947
|
: event === "resume-restore" && rowTaskId && upheldFeedbackByTask(effectiveEvents(priorEvents)).has(rowTaskId)
|
|
1942
1948
|
? {
|
|
@@ -2383,3 +2389,347 @@ export class Journal {
|
|
|
2383
2389
|
.filter((row) => row && typeof row === "object" && "kind" in row && row.kind === "judge");
|
|
2384
2390
|
}
|
|
2385
2391
|
}
|
|
2392
|
+
// ── C1 (v2.6.4): owed checks ─────────────────────────────────────────────────────────────────────
|
|
2393
|
+
// A waiver accepts risk on ONE immutable subject. The same task-approved row that satisfies the gate
|
|
2394
|
+
// carries the obligation it leaves behind: run, task, gate, the exact base..head range and its
|
|
2395
|
+
// canonical subject, the acceptance criteria, the run-derived lifetime patch authors, the cause and
|
|
2396
|
+
// the evidence that would discharge it. The fold below is the only reader. It keeps every waiver as
|
|
2397
|
+
// accepted-risk history, subtracts a discharge only while its proof still revalidates, and answers
|
|
2398
|
+
// "unknown" — never zero — for a waiver without a well-formed obligation or a proof that no longer holds.
|
|
2399
|
+
export const OWED_CHECK_VERSION = 1;
|
|
2400
|
+
export const OWED_DISCHARGE_EVENT = "owed-check-discharged";
|
|
2401
|
+
const owedGit = (cwd, args, input) => execFileSync("git", args, {
|
|
2402
|
+
cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...(input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input }),
|
|
2403
|
+
});
|
|
2404
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
2405
|
+
export const owedCriteria = (acceptance) => sha256(JSON.stringify(acceptance));
|
|
2406
|
+
/** The daemon's canonical gate subject for base..head (daemon.ts gateCommitSubject), byte for byte. */
|
|
2407
|
+
export const owedSubject = (cwd, base, head) => sha256(owedGit(cwd, ["log", "--reverse", "--format=%T%x00%an%x00%ae%x00%cn%x00%ce%x00%B%x1e", `${base}..${head}`]));
|
|
2408
|
+
// Ownership joins on --stable ids (whitespace-blind: a re-indented carry keeps its author, which only
|
|
2409
|
+
// widens exclusions). Range identity uses --verbatim: re-indented content is a different program.
|
|
2410
|
+
const patchIdOf = (cwd, diff, mode = "--stable") => owedGit(cwd, ["patch-id", mode], diff).trim().split(/\s+/)[0] || undefined;
|
|
2411
|
+
/** Ordered verbatim patch ids of the commits in base..head; an empty commit authored no patch. */
|
|
2412
|
+
export const rangePatches = (cwd, base, head) => owedGit(cwd, ["rev-list", "--reverse", `${base}..${head}`]).trim().split("\n").filter(Boolean)
|
|
2413
|
+
.map((commit) => patchIdOf(cwd, owedGit(cwd, ["show", "--format=", "--binary", commit]), "--verbatim"))
|
|
2414
|
+
.filter((id) => id !== undefined);
|
|
2415
|
+
/** Verbatim patch identity of the whole change base..head introduces. */
|
|
2416
|
+
export const rangePatch = (cwd, base, head) => patchIdOf(cwd, owedGit(cwd, ["diff", "--binary", base, head]), "--verbatim") ?? "empty";
|
|
2417
|
+
const COMMIT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
2418
|
+
/**
|
|
2419
|
+
* `head` is the task's own recorded integration merge of the obligation's range — exactly. Its first
|
|
2420
|
+
* parent is `mergeBase`; its second parent replays the waived history source tree for source tree from
|
|
2421
|
+
* a base with the waived base's tree (a carry may mint new commit objects, never new content — and
|
|
2422
|
+
* patch ids alone ignore where a hunk lands); its tree is git's own clean merge of the two (no
|
|
2423
|
+
* resolution, no amend); and it introduced the same ordered verbatim patch series and total patch.
|
|
2424
|
+
* Squashed, reordered, relocated, re-indented, reverted or conflict-resolved history fails one of
|
|
2425
|
+
* these and is refused as ambiguous; containment never counts.
|
|
2426
|
+
* Both verify (before discharging) and the fold (re-reading every discharge) decide through here.
|
|
2427
|
+
*/
|
|
2428
|
+
export function integrationMapped(cwd, events, o, mergeBase, head, memo) {
|
|
2429
|
+
if (typeof mergeBase !== "string" || typeof head !== "string" || !COMMIT_ID.test(mergeBase) || !COMMIT_ID.test(head))
|
|
2430
|
+
return false;
|
|
2431
|
+
if (!events.some((e) => e.event === "merge" && e.taskId === o.taskId && e.data.commit === head))
|
|
2432
|
+
return false;
|
|
2433
|
+
// Only the git component is memoised, and only a completed proof: commit ids name immutable history,
|
|
2434
|
+
// so the key is the obligation's id and recorded range facts plus the claimed mergeBase..head. A refusal
|
|
2435
|
+
// or git failure (a shallow or partial object store can refuse a range it later proves) is never cached
|
|
2436
|
+
// and is re-proved next fold. The merge-row eligibility above is rechecked on every fold.
|
|
2437
|
+
const key = JSON.stringify([o.id, mergeBase, head, o.base, o.head, o.patch, o.patches]);
|
|
2438
|
+
if (memo?.has(key))
|
|
2439
|
+
return true;
|
|
2440
|
+
const proved = integrationProved(cwd, o, mergeBase, head);
|
|
2441
|
+
if (proved)
|
|
2442
|
+
memo?.add(key);
|
|
2443
|
+
return proved;
|
|
2444
|
+
}
|
|
2445
|
+
function integrationProved(cwd, o, mergeBase, head) {
|
|
2446
|
+
try {
|
|
2447
|
+
const parents = owedGit(cwd, ["show", "-s", "--format=%P", head]).trim().split(" ");
|
|
2448
|
+
if (parents.length !== 2 || parents[0] !== mergeBase)
|
|
2449
|
+
return false;
|
|
2450
|
+
const [, carried] = parents;
|
|
2451
|
+
const tree = (rev) => owedGit(cwd, ["rev-parse", `${rev}^{tree}`]).trim();
|
|
2452
|
+
const trees = (from, to) => owedGit(cwd, ["log", "--reverse", "--format=%T", `${from}..${to}`]).trim();
|
|
2453
|
+
if (tree(owedGit(cwd, ["merge-base", mergeBase, carried]).trim()) !== tree(o.base) || trees(mergeBase, carried) !== trees(o.base, o.head))
|
|
2454
|
+
return false;
|
|
2455
|
+
// A conflicted merge-tree exits 1 and throws; a clean one prints its tree first.
|
|
2456
|
+
if (owedGit(cwd, ["merge-tree", "--write-tree", mergeBase, carried]).trim().split("\n")[0] !== tree(head))
|
|
2457
|
+
return false;
|
|
2458
|
+
const series = rangePatches(cwd, mergeBase, carried);
|
|
2459
|
+
return series.length === o.patches.length && series.every((id, i) => id === o.patches[i])
|
|
2460
|
+
&& rangePatch(cwd, mergeBase, head) === o.patch;
|
|
2461
|
+
}
|
|
2462
|
+
catch {
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Lifetime patch authors of base..head, read from the run: the daemon's subjectAuthors fold
|
|
2468
|
+
* (daemon.ts, not exported and not this task's to edit) restated synchronously so the waive row is
|
|
2469
|
+
* written without yielding. Recreation rows name SOURCE hashes, so ownership joins by stable patch
|
|
2470
|
+
* identity across changed commit objects; a gate-only restore (resume-restore's seat, a recreation
|
|
2471
|
+
* after work started) never becomes an author. Unreadable history names an unknown author.
|
|
2472
|
+
*/
|
|
2473
|
+
export function owedAuthors(events, taskId, cwd, base, head) {
|
|
2474
|
+
const cache = new Map();
|
|
2475
|
+
const patches = (commits) => {
|
|
2476
|
+
const ids = new Set();
|
|
2477
|
+
for (const commit of commits) {
|
|
2478
|
+
if (!cache.has(commit))
|
|
2479
|
+
cache.set(commit, patchIdOf(cwd, owedGit(cwd, ["show", "--format=", "--binary", commit])));
|
|
2480
|
+
const id = cache.get(commit);
|
|
2481
|
+
if (id)
|
|
2482
|
+
ids.add(id);
|
|
2483
|
+
}
|
|
2484
|
+
return ids;
|
|
2485
|
+
};
|
|
2486
|
+
let current;
|
|
2487
|
+
let previous;
|
|
2488
|
+
let awaitingCarry = false;
|
|
2489
|
+
const owners = new Map();
|
|
2490
|
+
const preservedOwner = new Map();
|
|
2491
|
+
const attribute = (ids, attempt) => {
|
|
2492
|
+
for (const id of ids) {
|
|
2493
|
+
if (attempt?.incoming.has(id))
|
|
2494
|
+
continue;
|
|
2495
|
+
const authors = owners.get(id) ?? new Set();
|
|
2496
|
+
authors.add(preservedOwner.get(id) ?? attempt?.author ?? "unknown author (missing task-dispatch assignment)");
|
|
2497
|
+
owners.set(id, authors);
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
try {
|
|
2501
|
+
for (const row of events) {
|
|
2502
|
+
if (row.taskId !== taskId)
|
|
2503
|
+
continue;
|
|
2504
|
+
const preserved = [row.data.ref, row.data.preservedRef].find((v) => typeof v === "string" && v.startsWith("refs/tickmarkr/preserved/"));
|
|
2505
|
+
if (preserved) {
|
|
2506
|
+
const [commit, subject, trailer] = owedGit(cwd, ["show", "-s", `--format=%H%n%s%n%(trailers:key=${PRESERVE_PRODUCER_TRAILER},valueonly)`, `${preserved}^{commit}`]).trim().split("\n");
|
|
2507
|
+
if (commit && subject === PRESERVE_COMMIT_SUBJECT) {
|
|
2508
|
+
const owner = typeof row.data.producer === "string" ? row.data.producer
|
|
2509
|
+
: trailer?.trim().replace(/ attempt \d+$/, "") || "unknown author (legacy unattributed preservation)";
|
|
2510
|
+
for (const id of patches([commit])) {
|
|
2511
|
+
const prior = preservedOwner.get(id);
|
|
2512
|
+
if (prior === undefined || prior === "unknown" || prior.startsWith("unknown author"))
|
|
2513
|
+
preservedOwner.set(id, owner);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (row.event === "task-dispatch") {
|
|
2518
|
+
previous = current;
|
|
2519
|
+
const a = row.data.assignment;
|
|
2520
|
+
current = { author: typeof a?.adapter === "string" && typeof a?.model === "string"
|
|
2521
|
+
? `${a.adapter}:${a.model}` : "unknown author (missing task-dispatch assignment)", incoming: new Set() };
|
|
2522
|
+
awaitingCarry = true;
|
|
2523
|
+
}
|
|
2524
|
+
else if (row.event === "worktree-recreation") {
|
|
2525
|
+
const carried = patches(Array.isArray(row.data.carried) ? row.data.carried : []);
|
|
2526
|
+
attribute(carried, awaitingCarry ? previous : current);
|
|
2527
|
+
if (awaitingCarry && current)
|
|
2528
|
+
current.incoming = carried;
|
|
2529
|
+
awaitingCarry = false;
|
|
2530
|
+
}
|
|
2531
|
+
else if (["worker-launch", "worker-result", "gate-result", "task-human"].includes(row.event)) {
|
|
2532
|
+
awaitingCarry = false;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const subject = patches(owedGit(cwd, ["rev-list", "--reverse", `${base}..${head}`]).trim().split("\n").filter(Boolean));
|
|
2536
|
+
attribute(subject, current);
|
|
2537
|
+
return [...new Set([...subject].flatMap((id) => [...(owners.get(id) ?? [])]))].sort();
|
|
2538
|
+
}
|
|
2539
|
+
catch (error) {
|
|
2540
|
+
return [`unknown author (${String(error).split("\n")[0]})`];
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* The obligation a waive of `gate` leaves on the task branch's current range. Synchronous on purpose:
|
|
2545
|
+
* approve writes it into the waive row under the approval serialization without yielding, so a live
|
|
2546
|
+
* daemon's boundary sees the same single append it always did. Anything not bound exactly — the
|
|
2547
|
+
* waived row measured another subject, unreadable refs, unresolved authors, unreadable criteria — is
|
|
2548
|
+
* recorded, never guessed, as known: false.
|
|
2549
|
+
*/
|
|
2550
|
+
export function captureOwedCheck(input) {
|
|
2551
|
+
const { cwd, runId, taskId, gate, events } = input;
|
|
2552
|
+
let base = "unknown", head = "unknown", subject = "unknown", patch = "unknown";
|
|
2553
|
+
let patches = [];
|
|
2554
|
+
let authors = [];
|
|
2555
|
+
let unknownReason;
|
|
2556
|
+
try {
|
|
2557
|
+
head = owedGit(cwd, ["rev-parse", "--verify", `${input.taskRef}^{commit}`]).trim();
|
|
2558
|
+
base = owedGit(cwd, ["merge-base", input.integrationRef, head]).trim();
|
|
2559
|
+
subject = owedSubject(cwd, base, head);
|
|
2560
|
+
const measured = [...events].reverse().find((e) => e.taskId === taskId && e.event === "gate-result" && e.data.gate === gate);
|
|
2561
|
+
if (measured?.data.commit !== subject) {
|
|
2562
|
+
throw new Error(`the waived ${gate} row measured ${String(measured?.data.commit ?? "no subject")}, not ${input.taskRef}'s range`);
|
|
2563
|
+
}
|
|
2564
|
+
patches = rangePatches(cwd, base, head);
|
|
2565
|
+
patch = rangePatch(cwd, base, head);
|
|
2566
|
+
authors = owedAuthors(events, taskId, cwd, base, head);
|
|
2567
|
+
if (!authors.length || authors.some((a) => a.startsWith("unknown")))
|
|
2568
|
+
throw new Error(`unresolved patch authors: ${authors.join(", ") || "none"}`);
|
|
2569
|
+
}
|
|
2570
|
+
catch (error) {
|
|
2571
|
+
unknownReason = (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
2572
|
+
}
|
|
2573
|
+
if (!Array.isArray(input.acceptance))
|
|
2574
|
+
unknownReason ??= "task acceptance criteria unreadable";
|
|
2575
|
+
const files = Array.isArray(input.files) && input.files.every((f) => typeof f === "string") ? input.files : undefined;
|
|
2576
|
+
if (!files)
|
|
2577
|
+
unknownReason ??= "task file scope unreadable";
|
|
2578
|
+
if (!input.declared?.length)
|
|
2579
|
+
unknownReason ??= "declared channels unreadable";
|
|
2580
|
+
const criteria = Array.isArray(input.acceptance) ? owedCriteria(input.acceptance) : "unknown";
|
|
2581
|
+
return {
|
|
2582
|
+
version: OWED_CHECK_VERSION,
|
|
2583
|
+
id: sha256(JSON.stringify([runId, taskId, gate, base, head, subject, criteria])).slice(0, 16),
|
|
2584
|
+
runId, taskId, gate, base, head, subject, patch, patches, criteria, files: files ?? [], authors,
|
|
2585
|
+
declared: [...(input.declared ?? [])].map(({ key, vendor }) => ({ key, vendor })), cause: input.cause,
|
|
2586
|
+
evidence: `tickmarkr verify --base ${base} --task ${taskId} --record ${runId}: ${gate} passing on ${base}..${head} or on its recorded integration merge, reviewed outside the authors' vendors`,
|
|
2587
|
+
disposition: "accepted-risk",
|
|
2588
|
+
known: unknownReason === undefined,
|
|
2589
|
+
...(unknownReason === undefined ? {} : { unknownReason }),
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
/** A bound fact: a non-empty string that capture did not record as unknown. */
|
|
2593
|
+
const isBound = (v) => typeof v === "string" && v !== "" && !v.startsWith("unknown");
|
|
2594
|
+
/** Every required field present and well-typed; anything else is malformed evidence, never a known check. */
|
|
2595
|
+
const isOwedCheck = (value, taskId, gate) => {
|
|
2596
|
+
if (typeof value !== "object" || value === null)
|
|
2597
|
+
return false;
|
|
2598
|
+
const o = value;
|
|
2599
|
+
return o.version === OWED_CHECK_VERSION && o.known === true && o.disposition === "accepted-risk"
|
|
2600
|
+
&& o.taskId === taskId && o.gate === gate && GATE_NAMES.includes(o.gate)
|
|
2601
|
+
&& [o.id, o.runId, o.taskId, o.base, o.head, o.subject, o.patch, o.criteria].every(isBound)
|
|
2602
|
+
&& typeof o.cause === "string" && typeof o.evidence === "string"
|
|
2603
|
+
&& Array.isArray(o.patches) && o.patches.every(isBound)
|
|
2604
|
+
&& Array.isArray(o.authors) && o.authors.length > 0 && o.authors.every(isBound)
|
|
2605
|
+
&& Array.isArray(o.files) && o.files.every((f) => typeof f === "string")
|
|
2606
|
+
&& Array.isArray(o.declared) && o.declared.length > 0 && o.declared.every(isChannelFact);
|
|
2607
|
+
};
|
|
2608
|
+
const providerOf = (fact) => modelProvider(fact.key.slice(fact.key.indexOf(":") + 1), fact.vendor);
|
|
2609
|
+
/** A resolved channel: an `adapter:model` key and a bound vendor — never an empty or placeholder claim. */
|
|
2610
|
+
const isChannelFact = (f) => typeof f === "object" && f !== null
|
|
2611
|
+
&& typeof f.key === "string" && /^[^:\s]+:\S+$/.test(f.key) && isBound(f.vendor);
|
|
2612
|
+
/**
|
|
2613
|
+
* The trusted channel identities, key → vendors: what this repository's adapters declared under its config
|
|
2614
|
+
* when the waive was written, recorded on the obligation by approve. Declaration, not doctor health — a
|
|
2615
|
+
* later logout does not revoke a proof, but a seat not declared (or declared under another vendor) never
|
|
2616
|
+
* resolved. Read from the row, the fold resolves identically in every process and every checkout.
|
|
2617
|
+
*/
|
|
2618
|
+
const declaredVendors = (o) => {
|
|
2619
|
+
const vendors = new Map();
|
|
2620
|
+
for (const c of o.declared)
|
|
2621
|
+
vendors.set(c.key, (vendors.get(c.key) ?? new Set()).add(c.vendor));
|
|
2622
|
+
return vendors;
|
|
2623
|
+
};
|
|
2624
|
+
/** Why a discharge row no longer proves its obligation, re-read from its artifact; undefined when it does. */
|
|
2625
|
+
function dischargeProblem(row, o, events, cwd, memo) {
|
|
2626
|
+
const d = row.data;
|
|
2627
|
+
if (row.taskId !== o.taskId)
|
|
2628
|
+
return "discharge names another task";
|
|
2629
|
+
if (d.criteria !== o.criteria)
|
|
2630
|
+
return "discharge proved other criteria";
|
|
2631
|
+
const exact = d.mapping === "exact" && d.mergeBase === o.base && d.head === o.head;
|
|
2632
|
+
// The integration mapping is re-proved from git on every fold, never taken from the row's claims.
|
|
2633
|
+
const integration = d.mapping === "integration" && integrationMapped(cwd, events, o, d.mergeBase, d.head, memo);
|
|
2634
|
+
if (!exact && !integration)
|
|
2635
|
+
return "discharge range does not bind the obligation";
|
|
2636
|
+
let artifact;
|
|
2637
|
+
try {
|
|
2638
|
+
const bytes = readFileSync(String(d.artifactPath));
|
|
2639
|
+
if (createHash("sha256").update(bytes).digest("hex") !== d.artifactSha256)
|
|
2640
|
+
return "artifact hash mismatch";
|
|
2641
|
+
artifact = JSON.parse(bytes.toString("utf8")) ?? {};
|
|
2642
|
+
}
|
|
2643
|
+
catch {
|
|
2644
|
+
return "artifact unavailable";
|
|
2645
|
+
}
|
|
2646
|
+
if (artifact.head !== d.head || artifact.mergeBase !== d.mergeBase)
|
|
2647
|
+
return "artifact measured another range";
|
|
2648
|
+
if (artifact.green !== true)
|
|
2649
|
+
return "artifact verdict is not green";
|
|
2650
|
+
// The criteria the hash-bound evidence itself measured: the row's claim alone only repeats the obligation.
|
|
2651
|
+
if (artifact.criteria !== o.criteria)
|
|
2652
|
+
return "artifact measured other criteria";
|
|
2653
|
+
// The scope the waived task declared, exactly: a wider allowlist (--files '*') is weakened evidence.
|
|
2654
|
+
if (JSON.stringify(artifact.files) !== JSON.stringify(o.files))
|
|
2655
|
+
return "artifact gated another file scope";
|
|
2656
|
+
const rows = (Array.isArray(artifact.gateRows) ? artifact.gateRows : []);
|
|
2657
|
+
const gateRow = rows.find((r) => r?.gate === o.gate && r.pass === true && r.meta?.skipped !== true);
|
|
2658
|
+
if (!gateRow)
|
|
2659
|
+
return `artifact holds no passing ${o.gate} row`;
|
|
2660
|
+
if (o.gate === "review") {
|
|
2661
|
+
const reviewer = d.reviewer;
|
|
2662
|
+
const authors = Array.isArray(d.authorChannels) ? d.authorChannels : [];
|
|
2663
|
+
// The row's reviewer must be the seat the hash-bound review row recorded — key, vendor and provider.
|
|
2664
|
+
if (!isChannelFact(reviewer) || gateRow.meta?.reviewer !== reviewer.key || gateRow.meta.vendor !== reviewer.vendor
|
|
2665
|
+
|| gateRow.meta.provider !== providerOf(reviewer))
|
|
2666
|
+
return "unresolved reviewer";
|
|
2667
|
+
// ...and a channel the waive recorded as declared under exactly that vendor: agreement between the row
|
|
2668
|
+
// and the artifact only repeats a claim, so identity and vendor resolve against the obligation's own.
|
|
2669
|
+
const declared = declaredVendors(o);
|
|
2670
|
+
const resolves = (f) => { const v = declared.get(f.key); return v?.size === 1 && v.has(f.vendor); };
|
|
2671
|
+
// An author key stays excluded whatever vendor any row claims for it.
|
|
2672
|
+
if (o.authors.includes(reviewer.key))
|
|
2673
|
+
return "same-vendor reviewer (the reviewer is a recorded patch author)";
|
|
2674
|
+
if (!resolves(reviewer))
|
|
2675
|
+
return "unresolved reviewer (no declared channel with that identity and vendor)";
|
|
2676
|
+
if (!authors.every(isChannelFact) || !o.authors.every((key) => authors.some((a) => a.key === key)))
|
|
2677
|
+
return "unresolved author channel";
|
|
2678
|
+
if (authors.some((a) => authors.some((b) => a.key === b.key && a.vendor !== b.vendor)))
|
|
2679
|
+
return "unresolved author channel (inconsistent vendor claims)";
|
|
2680
|
+
if (!authors.every(resolves))
|
|
2681
|
+
return "unresolved author channel (no declared channel with that identity and vendor)";
|
|
2682
|
+
if (authors.some((a) => a.vendor === reviewer.vendor || providerOf(a) === providerOf(reviewer)))
|
|
2683
|
+
return "same-vendor reviewer";
|
|
2684
|
+
}
|
|
2685
|
+
return undefined;
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* The CURRENT owed-check fold: accepted-risk history, outstanding debt, and whether it is known.
|
|
2689
|
+
* `cwd` is any checkout of the run's repository (integration discharges are re-proved from git).
|
|
2690
|
+
* It never throws: a fold that cannot be computed is unknown debt, not a crash and never zero.
|
|
2691
|
+
* `memo` (I2, optional): a store-scoped table reusing the immutable git re-proof of an integration mapping
|
|
2692
|
+
* across folds; every artifact, hash, criteria and reviewer condition is still re-read on every fold.
|
|
2693
|
+
*/
|
|
2694
|
+
export function foldOwedChecks(events, cwd, memo) {
|
|
2695
|
+
try {
|
|
2696
|
+
return foldOwedChecksOrThrow(events, cwd, memo);
|
|
2697
|
+
}
|
|
2698
|
+
catch (error) {
|
|
2699
|
+
return { known: false, debt: "unknown", outstanding: [], acceptedRisk: [], discharged: [], unknown: [{ reason: `owed-check fold failed: ${String(error).split("\n")[0]}` }] };
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
function foldOwedChecksOrThrow(events, cwd, memo) {
|
|
2703
|
+
const accepted = new Map();
|
|
2704
|
+
const discharged = new Set();
|
|
2705
|
+
const unknown = [];
|
|
2706
|
+
for (const e of effectiveEvents(events)) {
|
|
2707
|
+
if (e.event === "task-approved" && e.data.release === GATE_SATISFIED_RELEASE) {
|
|
2708
|
+
const o = e.data.obligation;
|
|
2709
|
+
if (isOwedCheck(o, e.taskId, e.data.gate))
|
|
2710
|
+
accepted.set(o.id, o);
|
|
2711
|
+
else {
|
|
2712
|
+
const recorded = typeof o === "object" && o !== null ? o.unknownReason : undefined;
|
|
2713
|
+
unknown.push({ taskId: e.taskId, gate: String(e.data.gate), reason: o === undefined ? "legacy waiver without an owed-check obligation"
|
|
2714
|
+
: typeof recorded === "string" ? recorded : "malformed owed-check obligation" });
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
else if (e.event === OWED_DISCHARGE_EVENT) {
|
|
2718
|
+
const ids = Array.isArray(e.data.ids) ? e.data.ids : [];
|
|
2719
|
+
const problems = ids.map((id) => {
|
|
2720
|
+
const o = accepted.get(String(id));
|
|
2721
|
+
return o ? dischargeProblem(e, o, events, cwd, memo) : "discharge names no recorded obligation";
|
|
2722
|
+
});
|
|
2723
|
+
const problem = ids.length ? problems.find((p) => p !== undefined) : "discharge names no obligation";
|
|
2724
|
+
if (problem)
|
|
2725
|
+
unknown.push({ taskId: e.taskId, reason: problem });
|
|
2726
|
+
else
|
|
2727
|
+
for (const id of ids)
|
|
2728
|
+
discharged.add(String(id));
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
const acceptedRisk = [...accepted.values()];
|
|
2732
|
+
const outstanding = acceptedRisk.filter((o) => !discharged.has(o.id));
|
|
2733
|
+
const known = unknown.length === 0;
|
|
2734
|
+
return { known, debt: known ? outstanding.length : "unknown", outstanding, acceptedRisk, discharged: [...discharged], unknown };
|
|
2735
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { AuthHealth, LaunchServicesRecord } from "../adapters/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* v2.6.5 T10 (A1, D-787/D-789): did launchservicesd restart since tickmarkr last looked?
|
|
5
|
+
*
|
|
6
|
+
* On 2026-09-30 a heavy local suite exhausted launchservicesd's Mach port space (0x1000600b); the daemon
|
|
7
|
+
* crashed, respawned with an empty app registry, and every GUI app launched before it read "not open
|
|
8
|
+
* anymore". The tell is the daemon's own identity: a young pid/start beside an old Dock and Finder.
|
|
9
|
+
* doctor and standalone verify read that identity with one bounded /bin/ps call, compare it with the
|
|
10
|
+
* one the FIRST doctor.json record holds (registry order — no new state file or adapter row), and
|
|
11
|
+
* persist one advisory on a change. Unreadable evidence is unknown: the prior identity is kept, never
|
|
12
|
+
* replaced by a guess. Advisory only — verdicts, retries and routing never read it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const LAUNCHSERVICES_PROBE_CEILING_MS = 5000;
|
|
15
|
+
export interface LaunchServicesIdentity {
|
|
16
|
+
pid: number;
|
|
17
|
+
start: string;
|
|
18
|
+
}
|
|
19
|
+
export type LaunchServicesObservation = {
|
|
20
|
+
status: "non-darwin";
|
|
21
|
+
} | {
|
|
22
|
+
status: "read";
|
|
23
|
+
identity: LaunchServicesIdentity;
|
|
24
|
+
} | {
|
|
25
|
+
status: "unreadable";
|
|
26
|
+
detail: string;
|
|
27
|
+
};
|
|
28
|
+
/** The closed identity table; "first" is the baseline row when no prior identity is held. */
|
|
29
|
+
export type LaunchServicesStatus = "first" | "unchanged" | "changed-pid" | "changed-start" | "unreadable" | "non-darwin" | "skipped";
|
|
30
|
+
/** Exactly one launchservicesd row, or null (absent, ambiguous or malformed output is unknown). */
|
|
31
|
+
export declare function parsePsIdentity(stdout: string): LaunchServicesIdentity | null;
|
|
32
|
+
export type ProbeTimer = (fire: () => void, ms: number) => () => void;
|
|
33
|
+
interface ProbeSeam {
|
|
34
|
+
platform: NodeJS.Platform;
|
|
35
|
+
spawn: typeof spawn;
|
|
36
|
+
setTimer: ProbeTimer;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
export declare const setLaunchServicesProbeForTests: (s: Partial<ProbeSeam>) => void;
|
|
40
|
+
export declare const resetLaunchServicesProbeForTests: () => void;
|
|
41
|
+
export declare function observeLaunchServices(s?: ProbeSeam): Promise<LaunchServicesObservation>;
|
|
42
|
+
export declare function launchServicesAdvisory(prior: LaunchServicesIdentity, now: LaunchServicesIdentity): string;
|
|
43
|
+
/** Pure: the table row for one observation against the held record, and the record to hold next. */
|
|
44
|
+
export declare function classifyLaunchServices(prior: Partial<LaunchServicesRecord> | undefined, observed: LaunchServicesObservation): {
|
|
45
|
+
status: LaunchServicesStatus;
|
|
46
|
+
next?: LaunchServicesRecord;
|
|
47
|
+
line?: string;
|
|
48
|
+
};
|
|
49
|
+
/** Observe, classify against the first record of `prior`, and hold the result on the first record of `health`.
|
|
50
|
+
* Returns `health` itself when nothing changed, else a copy — the argument is never mutated. */
|
|
51
|
+
export declare function recordLaunchServices(prior: Record<string, AuthHealth> | null, health: Record<string, AuthHealth>): Promise<{
|
|
52
|
+
status: LaunchServicesStatus;
|
|
53
|
+
health: Record<string, AuthHealth>;
|
|
54
|
+
line?: string;
|
|
55
|
+
}>;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* v2.6.5 T10 (A1, D-787/D-789): did launchservicesd restart since tickmarkr last looked?
|
|
5
|
+
*
|
|
6
|
+
* On 2026-09-30 a heavy local suite exhausted launchservicesd's Mach port space (0x1000600b); the daemon
|
|
7
|
+
* crashed, respawned with an empty app registry, and every GUI app launched before it read "not open
|
|
8
|
+
* anymore". The tell is the daemon's own identity: a young pid/start beside an old Dock and Finder.
|
|
9
|
+
* doctor and standalone verify read that identity with one bounded /bin/ps call, compare it with the
|
|
10
|
+
* one the FIRST doctor.json record holds (registry order — no new state file or adapter row), and
|
|
11
|
+
* persist one advisory on a change. Unreadable evidence is unknown: the prior identity is kept, never
|
|
12
|
+
* replaced by a guess. Advisory only — verdicts, retries and routing never read it.
|
|
13
|
+
*/
|
|
14
|
+
export const LAUNCHSERVICES_PROBE_CEILING_MS = 5_000;
|
|
15
|
+
// lstart in the C locale: "Wed Sep 30 21:54:33 2026"; comm is the executable path on Darwin.
|
|
16
|
+
const PS_ARGS = ["-A", "-o", "pid=,lstart=,comm="];
|
|
17
|
+
const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/;
|
|
18
|
+
const MAX_PS_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
/** Exactly one launchservicesd row, or null (absent, ambiguous or malformed output is unknown). */
|
|
20
|
+
export function parsePsIdentity(stdout) {
|
|
21
|
+
const rows = stdout.split("\n").flatMap((line) => {
|
|
22
|
+
const m = PS_LINE.exec(line);
|
|
23
|
+
return m && basename(m[3]) === "launchservicesd" ? [{ pid: Number(m[1]), start: m[2].replace(/\s+/g, " ") }] : [];
|
|
24
|
+
});
|
|
25
|
+
return rows.length === 1 ? rows[0] : null;
|
|
26
|
+
}
|
|
27
|
+
const production = () => ({
|
|
28
|
+
platform: process.platform,
|
|
29
|
+
spawn,
|
|
30
|
+
setTimer: (fire, ms) => { const t = setTimeout(fire, ms); return () => clearTimeout(t); },
|
|
31
|
+
});
|
|
32
|
+
// ponytail: default-off under the test runner (the catalogRefreshAllowed precedent) so an unowned doctor
|
|
33
|
+
// test never takes a real host reading or changes its doctor.json bytes; tests opt in through the seam.
|
|
34
|
+
let seam;
|
|
35
|
+
export const setLaunchServicesProbeForTests = (s) => { seam = { ...production(), ...s }; };
|
|
36
|
+
export const resetLaunchServicesProbeForTests = () => { seam = undefined; };
|
|
37
|
+
const activeSeam = () => seam ?? (process.env.VITEST === "true" ? undefined : production());
|
|
38
|
+
/** One /bin/ps call that owns only its child, killed at the ceiling or on cancellation and reaped before it resolves. */
|
|
39
|
+
function readIdentity(s) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const unknown = (detail) => resolve({ status: "unreadable", detail });
|
|
42
|
+
if (s.signal?.aborted) {
|
|
43
|
+
unknown("probe cancelled");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let child;
|
|
47
|
+
try {
|
|
48
|
+
child = s.spawn("/bin/ps", PS_ARGS, { stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, LC_ALL: "C" } });
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
unknown(`/bin/ps did not start (${e instanceof Error ? e.message : String(e)})`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let out = "";
|
|
55
|
+
let why;
|
|
56
|
+
const retire = (reason) => { why ??= reason; child.kill("SIGKILL"); };
|
|
57
|
+
const clear = s.setTimer(() => retire(`/bin/ps exceeded the ${LAUNCHSERVICES_PROBE_CEILING_MS} ms kill ceiling`), LAUNCHSERVICES_PROBE_CEILING_MS);
|
|
58
|
+
const cancel = () => retire("probe cancelled");
|
|
59
|
+
s.signal?.addEventListener("abort", cancel, { once: true });
|
|
60
|
+
child.stdout?.on("data", (chunk) => {
|
|
61
|
+
out += chunk.toString("utf8");
|
|
62
|
+
if (out.length > MAX_PS_BYTES)
|
|
63
|
+
retire("/bin/ps output exceeded its bound");
|
|
64
|
+
});
|
|
65
|
+
child.once("error", (e) => { why ??= `/bin/ps failed (${e.message})`; });
|
|
66
|
+
child.once("close", (code) => {
|
|
67
|
+
clear();
|
|
68
|
+
s.signal?.removeEventListener("abort", cancel);
|
|
69
|
+
if (why)
|
|
70
|
+
return unknown(why);
|
|
71
|
+
if (code !== 0)
|
|
72
|
+
return unknown(`/bin/ps exited ${code}`);
|
|
73
|
+
const identity = parsePsIdentity(out);
|
|
74
|
+
return identity ? resolve({ status: "read", identity }) : unknown("no single launchservicesd row in /bin/ps output");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function observeLaunchServices(s = production()) {
|
|
79
|
+
return s.platform === "darwin" ? readIdentity(s) : Promise.resolve({ status: "non-darwin" });
|
|
80
|
+
}
|
|
81
|
+
// doctor.json is never schema-validated: a malformed held record is no identity at all.
|
|
82
|
+
const knownPrior = (prior) => typeof prior?.pid === "number" && typeof prior.start === "string";
|
|
83
|
+
export function launchServicesAdvisory(prior, now) {
|
|
84
|
+
return `launchservicesd restarted: pid ${prior.pid} → ${now.pid}, started ${prior.start} → ${now.start}. `
|
|
85
|
+
+ `GUI apps launched before ${now.start} may read "not open anymore" (D-787: the daemon crashed and respawned with an empty app registry). `
|
|
86
|
+
+ `Remedy: quit and relaunch each affected app, or log out / restart the Mac (a restart ends agent sessions and wipes /private/tmp); `
|
|
87
|
+
+ `confirm with /usr/bin/log show (launchservicesd 0x1000600b near ${now.start}) and ~/Library/Logs/DiagnosticReports/ExcUserFault_node-*.ips (advisory — verdicts unchanged)`;
|
|
88
|
+
}
|
|
89
|
+
/** Pure: the table row for one observation against the held record, and the record to hold next. */
|
|
90
|
+
export function classifyLaunchServices(prior, observed) {
|
|
91
|
+
const kept = knownPrior(prior) ? prior : undefined;
|
|
92
|
+
if (observed.status === "non-darwin")
|
|
93
|
+
return { status: "non-darwin", next: kept };
|
|
94
|
+
if (observed.status === "unreadable") {
|
|
95
|
+
const held = kept ? `pid ${kept.pid} started ${kept.start}` : "none";
|
|
96
|
+
return { status: "unreadable", next: kept, line: `launchservicesd identity unknown — ${observed.detail}; prior identity (${held}) retained (advisory — verdicts unchanged)` };
|
|
97
|
+
}
|
|
98
|
+
const now = observed.identity;
|
|
99
|
+
if (!knownPrior(prior))
|
|
100
|
+
return { status: "first", next: now };
|
|
101
|
+
const status = prior.pid !== now.pid ? "changed-pid" : prior.start !== now.start ? "changed-start" : "unchanged";
|
|
102
|
+
if (status === "unchanged")
|
|
103
|
+
return { status, next: prior };
|
|
104
|
+
const advisory = launchServicesAdvisory(prior, now);
|
|
105
|
+
return { status, next: { ...now, advisory }, line: advisory };
|
|
106
|
+
}
|
|
107
|
+
/** Observe, classify against the first record of `prior`, and hold the result on the first record of `health`.
|
|
108
|
+
* Returns `health` itself when nothing changed, else a copy — the argument is never mutated. */
|
|
109
|
+
export async function recordLaunchServices(prior, health) {
|
|
110
|
+
const held = prior ? Object.values(prior)[0]?.launchServices : undefined;
|
|
111
|
+
const s = activeSeam();
|
|
112
|
+
const r = s ? classifyLaunchServices(held, await observeLaunchServices(s)) : { status: "skipped", next: knownPrior(held) ? held : undefined };
|
|
113
|
+
const holder = Object.keys(health)[0];
|
|
114
|
+
if (!holder || !r.next || JSON.stringify(health[holder].launchServices) === JSON.stringify(r.next))
|
|
115
|
+
return { ...r, health };
|
|
116
|
+
return { ...r, health: { ...health, [holder]: { ...health[holder], launchServices: r.next } } };
|
|
117
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RunGraph } from "../graph/schema.js";
|
|
2
|
-
import { type JournalEvent } from "./journal.js";
|
|
2
|
+
import { type JournalEvent, type OwedFold } from "./journal.js";
|
|
3
3
|
/** An evidence identity is a physical journal line, never a filtered row ordinal. */
|
|
4
4
|
export interface EvidenceIdentity {
|
|
5
5
|
source: string;
|
|
@@ -74,6 +74,15 @@ export interface OperatorSnapshot {
|
|
|
74
74
|
};
|
|
75
75
|
latestRunEnd?: EvidenceIdentity;
|
|
76
76
|
approvedResumeRequired: boolean;
|
|
77
|
+
/** I2: CURRENT owed-check debt from the authority this observation supplied; "unknown" when absent or stale. */
|
|
78
|
+
debt: number | "unknown";
|
|
79
|
+
outstanding: readonly string[];
|
|
80
|
+
}
|
|
81
|
+
/** I2: foldOwedChecks over the complete effective journal, tagged with the journal basis it read. A result
|
|
82
|
+
* whose basis is not the observation's own is stale and counts as unknown debt, never as zero. */
|
|
83
|
+
export interface OwedAuthority {
|
|
84
|
+
basis: string;
|
|
85
|
+
fold: OwedFold;
|
|
77
86
|
}
|
|
78
87
|
/** Author channels recorded on a task-done row. A row written before authors were recorded carries none and
|
|
79
88
|
* projects nothing (never the dispatch); unattributed preserved work arrives as an explicit "unknown" entry. */
|
|
@@ -99,12 +108,20 @@ export declare class OperatorStateFold {
|
|
|
99
108
|
private total;
|
|
100
109
|
apply(record: OperatorRecord): void;
|
|
101
110
|
private task;
|
|
102
|
-
|
|
111
|
+
/** A run-end is the latest lifecycle boundary: the only state whose headline consults owed-check debt. */
|
|
112
|
+
get ended(): boolean;
|
|
113
|
+
/** `daemonDead`: this run's lock holder was observed provably dead (lock.ts liveness). It is an observation,
|
|
114
|
+
* not a journal row, so it settles the projection only and the fold keeps no trace of it.
|
|
115
|
+
* `owed`/`basis` (I2): COMPLETE needs a known-empty debt computed over this very journal basis. */
|
|
116
|
+
snapshot({ graph, sequence, observedAt, readable, graphAvailability, daemonDead, owed, basis }?: {
|
|
103
117
|
graph?: RunGraph;
|
|
104
118
|
sequence?: number;
|
|
105
119
|
observedAt?: number;
|
|
106
120
|
readable?: boolean;
|
|
107
121
|
graphAvailability?: OperatorSnapshot["graphAvailability"];
|
|
122
|
+
daemonDead?: boolean;
|
|
123
|
+
owed?: OwedAuthority;
|
|
124
|
+
basis?: string;
|
|
108
125
|
}): OperatorSnapshot;
|
|
109
126
|
private comparableTo;
|
|
110
127
|
}
|
|
@@ -116,4 +133,7 @@ export declare function readOperatorState({ events, source, ...options }: {
|
|
|
116
133
|
sequence?: number;
|
|
117
134
|
observedAt?: number;
|
|
118
135
|
readable?: boolean;
|
|
136
|
+
daemonDead?: boolean;
|
|
137
|
+
owed?: OwedAuthority;
|
|
138
|
+
basis?: string;
|
|
119
139
|
}): OperatorSnapshot;
|