tickmarkr 2.6.0 → 2.6.2
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 +14 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +3 -1
- package/dist/adapters/prompt.js +21 -3
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +13 -4
- package/dist/adapters/types.js +16 -0
- package/dist/cli/commands/approve.d.ts +5 -1
- package/dist/cli/commands/approve.js +66 -23
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +71 -8
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +37 -22
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.js +11 -2
- package/dist/config/config.d.ts +20 -0
- package/dist/config/config.js +47 -8
- package/dist/config/fleet-overlay.d.ts +1 -0
- package/dist/config/fleet-overlay.js +56 -0
- package/dist/drivers/orca.d.ts +26 -1
- package/dist/drivers/orca.js +199 -61
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +2 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +12 -1
- package/dist/gates/baseline.js +11 -4
- package/dist/gates/cache.d.ts +3 -1
- package/dist/gates/cache.js +10 -3
- package/dist/gates/llm.d.ts +5 -4
- package/dist/gates/llm.js +17 -14
- package/dist/gates/review.d.ts +8 -0
- package/dist/gates/review.js +47 -15
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +34 -17
- package/dist/gates/test-manifest.d.ts +14 -0
- package/dist/gates/test-manifest.js +33 -6
- package/dist/graph/graph.d.ts +6 -2
- package/dist/graph/graph.js +15 -4
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +39 -16
- package/dist/run/consult.d.ts +13 -1
- package/dist/run/consult.js +19 -14
- package/dist/run/daemon.d.ts +46 -2
- package/dist/run/daemon.js +964 -190
- package/dist/run/git.d.ts +44 -1
- package/dist/run/git.js +103 -5
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +126 -3
- package/dist/run/journal.js +418 -36
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +31 -1
- package/dist/run/protocol.js +3 -1
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +30 -22
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +17 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/live-store.d.ts +1 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +28 -2
- package/dist/tui/cockpit/run-view.d.ts +11 -7
- package/dist/tui/cockpit/run-view.js +69 -15
- package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
- package/dist/tui/cockpit/setup-cockpit.js +6 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +2 -1
- package/schema/config.schema.json +818 -0
- package/skills/tickmarkr-loop/SKILL.md +8 -2
- package/skills/tickmarkr-overseer/SKILL.md +85 -6
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
package/dist/run/git.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
|
4
4
|
export { ROUTING_ENV_SEAMS };
|
|
5
5
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
6
6
|
export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
7
|
+
/** OBS-1173 add.2: only ps's clean "no such process" (exit 1, nothing printed) means gone (""). A
|
|
8
|
+
* timeout, a signal, a spawn error or any other exit is a FAILED probe (null) that says nothing. */
|
|
9
|
+
export declare function classifyIdentityProbe(error: {
|
|
10
|
+
code?: number | string | null;
|
|
11
|
+
killed?: boolean;
|
|
12
|
+
signal?: string | null;
|
|
13
|
+
} | null, stdout: string): string | null;
|
|
14
|
+
/** `undefined` = the pid is gone; `null` = the probe failed and proves nothing (never gone, never a match). */
|
|
15
|
+
export declare function processIdentity(pid: number): Promise<string | null | undefined>;
|
|
16
|
+
export declare const activeShellIdentities: () => ReadonlyMap<number, string | undefined>;
|
|
17
|
+
/** A process that started after its receipt is a pid reuse, never that receipt's child. */
|
|
18
|
+
export declare function resolveActiveShellIdentities(): Promise<void>;
|
|
7
19
|
export declare const DEFAULT_FORK_CAP = "6";
|
|
8
20
|
/** Verification uses command leases; workers keep their concurrency budget. */
|
|
9
21
|
export declare const runWithVerificationBudget: <T>(capacity: RunCapacity, fn: () => Promise<T>) => Promise<T>;
|
|
@@ -174,6 +186,23 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
|
|
|
174
186
|
removeTaskIds: string[];
|
|
175
187
|
}): Promise<void>;
|
|
176
188
|
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
|
189
|
+
/**
|
|
190
|
+
* The attempt whose worker last wrote in a checkout, or explicit "unknown" when no worker launch is
|
|
191
|
+
* on record for the current tree (gate-only recreations, standalone verify, legacy callers). The
|
|
192
|
+
* label is what the preserve commit trailer and every `worktree-preserved`/refusal row carry, so a
|
|
193
|
+
* preserved engine commit is never credited to whichever seat is dispatched next.
|
|
194
|
+
*/
|
|
195
|
+
export type PreserveProducer = {
|
|
196
|
+
channel: string;
|
|
197
|
+
attempt: number;
|
|
198
|
+
} | "unknown";
|
|
199
|
+
export declare const PRESERVE_PRODUCER_TRAILER = "Tickmarkr-Producer";
|
|
200
|
+
/** Subject of every engine preserve commit; only such commits are owned by their preserve row. */
|
|
201
|
+
export declare const PRESERVE_COMMIT_SUBJECT = "tickmarkr: preserve uncommitted worktree";
|
|
202
|
+
export declare const producerFields: (producer: PreserveProducer | undefined) => {
|
|
203
|
+
producer: string;
|
|
204
|
+
producerAttempt?: number;
|
|
205
|
+
};
|
|
177
206
|
/**
|
|
178
207
|
* Preserve the bytes an existing checkout holds before recreation removes it.
|
|
179
208
|
*
|
|
@@ -188,7 +217,13 @@ export declare function resolveIntegrationBranch(_repo: string, branch: string):
|
|
|
188
217
|
* neither a commit nor a ref, keeping meaningful deaths visible rather than minting one ref per
|
|
189
218
|
* ordinary dispatch.
|
|
190
219
|
*/
|
|
191
|
-
export declare function preserveWorktree(cwd: string): Promise<string | undefined>;
|
|
220
|
+
export declare function preserveWorktree(cwd: string, producer?: PreserveProducer): Promise<string | undefined>;
|
|
221
|
+
/**
|
|
222
|
+
* Identity of the physical checkout, including recreation at the same path. Unlike mtime/ctime,
|
|
223
|
+
* birthtime does not change when builds add outputs. Read from disk on every call so process
|
|
224
|
+
* restarts preserve reuse. Filesystems without birthtime cannot prove build-output continuity.
|
|
225
|
+
*/
|
|
226
|
+
export declare function checkoutIncarnation(worktree: string): string | undefined;
|
|
192
227
|
export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
193
228
|
export interface DependencyLink {
|
|
194
229
|
/** Package link, relative to node_modules (including @scope/name). */
|
|
@@ -242,6 +277,14 @@ export type BaseContainment = {
|
|
|
242
277
|
* future `git cherry` ever emitted a `-` line for a merge.
|
|
243
278
|
*/
|
|
244
279
|
export declare function declaredBaseContainment(cwd: string, declaredRef: string, targetRef?: string): Promise<BaseContainment>;
|
|
280
|
+
/**
|
|
281
|
+
* OBS-1107: does `dest` already hold everything `commit` changed? Replays the commit's OWN
|
|
282
|
+
* parent-to-commit change onto `dest` as a three-way merge; a clean merge whose tree IS `dest`'s tree
|
|
283
|
+
* brings nothing the destination lacks — a content-empty commit, or a patch already represented there
|
|
284
|
+
* under another hash. Content, never hash identity. Any git failure (a root commit, a git without
|
|
285
|
+
* `merge-tree --merge-base`) answers false: unproven work stays unaccounted.
|
|
286
|
+
*/
|
|
287
|
+
export declare function changeRepresented(cwd: string, commit: string, dest?: string): Promise<boolean>;
|
|
245
288
|
export declare function removeWorktree(repo: string, dir: string): Promise<void>;
|
|
246
289
|
export declare const REFS_PREFLIGHT_PREFIX = "refs/tickmarkr/preflight";
|
|
247
290
|
export declare const REFS_PROBE_REMEDY = "run from the main repository or a full clone; a sandbox that denies writes under that path cannot host a run";
|
package/dist/run/git.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { executionSignal } from "./execution-budget.js";
|
|
2
2
|
import { commandLeaseEnvironment, withCommandLease } from "./lease.js";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
7
7
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -16,6 +16,52 @@ export { ROUTING_ENV_SEAMS };
|
|
|
16
16
|
// as argv (OBS-55) so child test oracles stay intact. The operator's own export wins.
|
|
17
17
|
export const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
18
18
|
export const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
19
|
+
/** A pid is an address, not an identity. Keep this evidence out of ShellReceipt. */
|
|
20
|
+
const linuxIdentity = (pid) => {
|
|
21
|
+
try {
|
|
22
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
23
|
+
const start = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
|
|
24
|
+
return start && /^\d+$/.test(start) ? `${pid}:${start}` : undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** OBS-1173 add.2: only ps's clean "no such process" (exit 1, nothing printed) means gone (""). A
|
|
31
|
+
* timeout, a signal, a spawn error or any other exit is a FAILED probe (null) that says nothing. */
|
|
32
|
+
export function classifyIdentityProbe(error, stdout) {
|
|
33
|
+
const start = stdout.trim().replace(/\s+/g, " ");
|
|
34
|
+
if (!error)
|
|
35
|
+
return start || null;
|
|
36
|
+
return !error.killed && !error.signal && error.code === 1 && !start ? "" : null;
|
|
37
|
+
}
|
|
38
|
+
/** `undefined` = the pid is gone; `null` = the probe failed and proves nothing (never gone, never a match). */
|
|
39
|
+
export async function processIdentity(pid) {
|
|
40
|
+
if (process.platform === "linux")
|
|
41
|
+
return linuxIdentity(pid);
|
|
42
|
+
// Async and bounded like every other ps probe: a loaded host is slow, not unreadable.
|
|
43
|
+
const start = await new Promise((done) => execFile("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
44
|
+
encoding: "utf8", timeout: 15_000, maxBuffer: 4096,
|
|
45
|
+
}, (error, stdout) => done(classifyIdentityProbe(error, stdout))));
|
|
46
|
+
if (start === null)
|
|
47
|
+
return null;
|
|
48
|
+
return start ? `${pid}:${start}` : undefined;
|
|
49
|
+
}
|
|
50
|
+
// Started receipts own these live pids; terminal receipts retire exactly that invocation. The
|
|
51
|
+
// shell hot path never probes: Linux reads /proc in place, other hosts resolve on demand below.
|
|
52
|
+
const activeShells = new Map();
|
|
53
|
+
export const activeShellIdentities = () => new Map([...activeShells].map(([pid, live]) => [pid, live.identity ?? undefined]));
|
|
54
|
+
/** A process that started after its receipt is a pid reuse, never that receipt's child. */
|
|
55
|
+
export async function resolveActiveShellIdentities() {
|
|
56
|
+
await Promise.all([...activeShells].filter(([, live]) => live.identity === undefined).map(async ([pid, live]) => {
|
|
57
|
+
const identity = await processIdentity(pid);
|
|
58
|
+
if (identity === null)
|
|
59
|
+
return; // a failed probe resolves nothing; the next resolve retries
|
|
60
|
+
const started = identity ? Date.parse(identity.slice(identity.indexOf(":") + 1)) : NaN;
|
|
61
|
+
if (live.identity === undefined)
|
|
62
|
+
live.identity = identity && started <= live.startedMs + 1_000 ? identity : null;
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
19
65
|
export const DEFAULT_FORK_CAP = "6";
|
|
20
66
|
/** Worker concurrency is resolved once per run and isolated from other async runs. */
|
|
21
67
|
const forkBudget = new AsyncLocalStorage();
|
|
@@ -217,6 +263,8 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
|
217
263
|
let attribution;
|
|
218
264
|
let confirmedStart = false;
|
|
219
265
|
let terminal = false;
|
|
266
|
+
let activePid;
|
|
267
|
+
let activeInvocation;
|
|
220
268
|
const begin = (invocation) => {
|
|
221
269
|
attribution = options.receiptAttribution?.(invocation);
|
|
222
270
|
confirmedStart = false;
|
|
@@ -225,10 +273,21 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
|
225
273
|
const emit = (receipt) => {
|
|
226
274
|
if (terminal)
|
|
227
275
|
return;
|
|
228
|
-
if (receipt.outcome === "started")
|
|
276
|
+
if (receipt.outcome === "started") {
|
|
229
277
|
confirmedStart = true;
|
|
230
|
-
|
|
278
|
+
activePid = receipt.pid;
|
|
279
|
+
if (activePid !== undefined) {
|
|
280
|
+
activeInvocation = Symbol("shell invocation");
|
|
281
|
+
activeShells.set(activePid, { startedMs: Date.now(), invocation: activeInvocation,
|
|
282
|
+
...(process.platform === "linux" ? { identity: linuxIdentity(activePid) ?? null } : {}) });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
231
286
|
terminal = true;
|
|
287
|
+
if (activePid !== undefined && activeShells.get(activePid)?.invocation === activeInvocation)
|
|
288
|
+
activeShells.delete(activePid);
|
|
289
|
+
activePid = undefined;
|
|
290
|
+
}
|
|
232
291
|
// Observational callbacks must not change process cleanup, retries or cancellation.
|
|
233
292
|
try {
|
|
234
293
|
options.onReceipt?.({ ...receipt, confirmedStart, ...(attribution ? { attribution: { ...attribution } } : {}) });
|
|
@@ -540,6 +599,14 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
540
599
|
const integration = await resolveIntegrationBranch(repo, branch.slice(0, split));
|
|
541
600
|
return `${integration}${branch.slice(split)}`;
|
|
542
601
|
};
|
|
602
|
+
export const PRESERVE_PRODUCER_TRAILER = "Tickmarkr-Producer";
|
|
603
|
+
/** Subject of every engine preserve commit; only such commits are owned by their preserve row. */
|
|
604
|
+
export const PRESERVE_COMMIT_SUBJECT = "tickmarkr: preserve uncommitted worktree";
|
|
605
|
+
export const producerFields = (producer) => producer && producer !== "unknown" ? { producer: producer.channel, producerAttempt: producer.attempt } : { producer: "unknown" };
|
|
606
|
+
const producerLabel = (producer) => {
|
|
607
|
+
const f = producerFields(producer);
|
|
608
|
+
return f.producerAttempt === undefined ? f.producer : `${f.producer} attempt ${f.producerAttempt}`;
|
|
609
|
+
};
|
|
543
610
|
/**
|
|
544
611
|
* Preserve the bytes an existing checkout holds before recreation removes it.
|
|
545
612
|
*
|
|
@@ -554,7 +621,7 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
554
621
|
* neither a commit nor a ref, keeping meaningful deaths visible rather than minting one ref per
|
|
555
622
|
* ordinary dispatch.
|
|
556
623
|
*/
|
|
557
|
-
export async function preserveWorktree(cwd) {
|
|
624
|
+
export async function preserveWorktree(cwd, producer = "unknown") {
|
|
558
625
|
if (!existsSync(cwd))
|
|
559
626
|
return undefined;
|
|
560
627
|
const scratch = mkdtempSync(join(tmpdir(), "tickmarkr-preserve-index-"));
|
|
@@ -571,7 +638,7 @@ export async function preserveWorktree(cwd) {
|
|
|
571
638
|
// not an authored project commit. Its HEAD parent makes the preserved tree directly inspectable.
|
|
572
639
|
const identity = "GIT_AUTHOR_NAME=tickmarkr GIT_AUTHOR_EMAIL=tickmarkr@localhost "
|
|
573
640
|
+ "GIT_COMMITTER_NAME=tickmarkr GIT_COMMITTER_EMAIL=tickmarkr@localhost";
|
|
574
|
-
const commit = (await shGitOk(`${identity} git commit-tree ${shq(tree)} -p HEAD -m ${shq(
|
|
641
|
+
const commit = (await shGitOk(`${identity} git commit-tree ${shq(tree)} -p HEAD -m ${shq(PRESERVE_COMMIT_SUBJECT)} -m ${shq(`${PRESERVE_PRODUCER_TRAILER}: ${producerLabel(producer)}`)}`, cwd)).trim();
|
|
575
642
|
const ref = `refs/tickmarkr/preserved/${commit}`;
|
|
576
643
|
await shGitOk(`git update-ref ${shq(ref)} ${shq(commit)}`, cwd);
|
|
577
644
|
return ref;
|
|
@@ -580,6 +647,23 @@ export async function preserveWorktree(cwd) {
|
|
|
580
647
|
rmSync(scratch, { recursive: true, force: true });
|
|
581
648
|
}
|
|
582
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Identity of the physical checkout, including recreation at the same path. Unlike mtime/ctime,
|
|
652
|
+
* birthtime does not change when builds add outputs. Read from disk on every call so process
|
|
653
|
+
* restarts preserve reuse. Filesystems without birthtime cannot prove build-output continuity.
|
|
654
|
+
*/
|
|
655
|
+
export function checkoutIncarnation(worktree) {
|
|
656
|
+
try {
|
|
657
|
+
const root = realpathSync(worktree);
|
|
658
|
+
const stat = statSync(root, { bigint: true });
|
|
659
|
+
if (!stat.isDirectory() || stat.birthtimeNs <= 0n)
|
|
660
|
+
return undefined;
|
|
661
|
+
return `${root}:${stat.dev}:${stat.ino}:${stat.birthtimeNs}`;
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
583
667
|
export async function createWorktree(repo, branch, baseRef) {
|
|
584
668
|
branch = await resolveTaskBranch(repo, branch);
|
|
585
669
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
@@ -779,6 +863,20 @@ export async function declaredBaseContainment(cwd, declaredRef, targetRef = "HEA
|
|
|
779
863
|
}
|
|
780
864
|
return missing.length > 0 ? { result: "drifted", missing } : { result: "contained", via: "patch-id" };
|
|
781
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* OBS-1107: does `dest` already hold everything `commit` changed? Replays the commit's OWN
|
|
868
|
+
* parent-to-commit change onto `dest` as a three-way merge; a clean merge whose tree IS `dest`'s tree
|
|
869
|
+
* brings nothing the destination lacks — a content-empty commit, or a patch already represented there
|
|
870
|
+
* under another hash. Content, never hash identity. Any git failure (a root commit, a git without
|
|
871
|
+
* `merge-tree --merge-base`) answers false: unproven work stays unaccounted.
|
|
872
|
+
*/
|
|
873
|
+
export async function changeRepresented(cwd, commit, dest = "HEAD") {
|
|
874
|
+
const merged = await shGit(`git merge-tree --write-tree --merge-base=${shq(`${commit}^`)} ${shq(dest)} ${shq(commit)}`, cwd);
|
|
875
|
+
if (merged.code !== 0)
|
|
876
|
+
return false;
|
|
877
|
+
const tree = await shGit(`git rev-parse ${shq(`${dest}^{tree}`)}`, cwd);
|
|
878
|
+
return tree.code === 0 && merged.stdout.split("\n")[0].trim() === tree.stdout.trim();
|
|
879
|
+
}
|
|
782
880
|
export async function removeWorktree(repo, dir) {
|
|
783
881
|
await shGit(`git worktree remove --force ${shq(dir)}`, repo); // best-effort; stale dirs are re-added with -B
|
|
784
882
|
await shGit(`rm -rf ${shq(dir)}`, repo);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export declare const HOST_PROBE_SAMPLES = 3;
|
|
3
|
+
export declare const HOST_PROBE_SAMPLE_MS = 1000;
|
|
4
|
+
export declare const HOST_LATENCY_FLOOR_MS = 50;
|
|
5
|
+
export declare const HOST_LATENCY_RATIO = 3;
|
|
6
|
+
export declare class HostDegradedError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export declare const setHostProbeSpawnForTests: (fn: typeof spawn) => void;
|
|
9
|
+
export declare const resetHostProbeSpawnForTests: () => void;
|
|
10
|
+
declare const execLatency: (signal: AbortSignal, timeoutMs: number) => Promise<number | null>;
|
|
11
|
+
export declare const setHostLatencySampleForTests: (fn: typeof execLatency) => void;
|
|
12
|
+
export declare const resetHostLatencySampleForTests: () => void;
|
|
13
|
+
export interface HostObservation {
|
|
14
|
+
samplesMs: Array<number | null>;
|
|
15
|
+
medianMs: number | null;
|
|
16
|
+
}
|
|
17
|
+
/** Three sequential samples, each bounded by 1s and the remaining observation budget. */
|
|
18
|
+
export declare function observeHost(signal: AbortSignal, budgetMs?: number): Promise<HostObservation>;
|
|
19
|
+
export declare const hostDegraded: (observation: HostObservation, referenceMs: number | undefined) => boolean;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { FILE_HANG_SLACK } from "../gates/test-manifest.js";
|
|
4
|
+
export const HOST_PROBE_SAMPLES = 3;
|
|
5
|
+
export const HOST_PROBE_SAMPLE_MS = 1_000;
|
|
6
|
+
export const HOST_LATENCY_FLOOR_MS = 50;
|
|
7
|
+
export const HOST_LATENCY_RATIO = FILE_HANG_SLACK;
|
|
8
|
+
export class HostDegradedError extends Error {
|
|
9
|
+
}
|
|
10
|
+
// A probe owns just this child: no shell, descendants, inherited Node options, or host-wide kills.
|
|
11
|
+
let probeSpawn = spawn;
|
|
12
|
+
export const setHostProbeSpawnForTests = (fn) => { probeSpawn = fn; };
|
|
13
|
+
export const resetHostProbeSpawnForTests = () => { probeSpawn = spawn; };
|
|
14
|
+
const execLatency = (signal, timeoutMs) => new Promise((resolve, reject) => {
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
const start = performance.now();
|
|
17
|
+
const env = { ...process.env };
|
|
18
|
+
delete env.NODE_OPTIONS;
|
|
19
|
+
let child;
|
|
20
|
+
try {
|
|
21
|
+
child = probeSpawn(process.execPath, ["-e", ""], { stdio: "ignore", env });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
resolve(null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let unreadable = false;
|
|
28
|
+
const retire = () => { unreadable = true; child.kill("SIGKILL"); };
|
|
29
|
+
const timer = setTimeout(retire, timeoutMs);
|
|
30
|
+
signal.addEventListener("abort", retire, { once: true });
|
|
31
|
+
child.once("error", () => { unreadable = true; });
|
|
32
|
+
// Resolve only after close: cancellation and timeout must reap the owned child before returning.
|
|
33
|
+
child.once("close", code => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal.removeEventListener("abort", retire);
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
reject(signal.reason);
|
|
38
|
+
else {
|
|
39
|
+
const elapsed = performance.now() - start;
|
|
40
|
+
resolve(!unreadable && code === 0 && elapsed <= timeoutMs ? elapsed : null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (signal.aborted)
|
|
44
|
+
retire();
|
|
45
|
+
});
|
|
46
|
+
let sample = execLatency;
|
|
47
|
+
export const setHostLatencySampleForTests = (fn) => { sample = fn; };
|
|
48
|
+
export const resetHostLatencySampleForTests = () => { sample = execLatency; };
|
|
49
|
+
/** Three sequential samples, each bounded by 1s and the remaining observation budget. */
|
|
50
|
+
export async function observeHost(signal, budgetMs = HOST_PROBE_SAMPLES * HOST_PROBE_SAMPLE_MS) {
|
|
51
|
+
const deadline = performance.now() + budgetMs;
|
|
52
|
+
const samplesMs = [];
|
|
53
|
+
for (let i = 0; i < HOST_PROBE_SAMPLES; i++) {
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
const remaining = deadline - performance.now();
|
|
56
|
+
const value = remaining > 0 ? await sample(signal, Math.min(HOST_PROBE_SAMPLE_MS, remaining)) : null;
|
|
57
|
+
samplesMs.push(value !== null && Number.isFinite(value) && value > 0 ? value : null);
|
|
58
|
+
}
|
|
59
|
+
const medianMs = samplesMs.every((n) => n !== null)
|
|
60
|
+
? [...samplesMs].sort((a, b) => a - b)[1] : null;
|
|
61
|
+
return { samplesMs, medianMs };
|
|
62
|
+
}
|
|
63
|
+
export const hostDegraded = (observation, referenceMs) => observation.medianMs === null || referenceMs === undefined
|
|
64
|
+
|| observation.medianMs >= Math.max(HOST_LATENCY_FLOOR_MS, referenceMs * HOST_LATENCY_RATIO);
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface ResumeState {
|
|
|
18
18
|
tried: string[];
|
|
19
19
|
lastAssignment?: Assignment;
|
|
20
20
|
upheldFeedback?: string;
|
|
21
|
+
escalated?: Record<string, string>;
|
|
22
|
+
escalatedAdapters?: string[];
|
|
21
23
|
}
|
|
22
24
|
export interface CurrentAttemptGateReplay {
|
|
23
25
|
commit: string;
|
|
@@ -29,12 +31,70 @@ export declare const ATTEMPT_CAP_RELEASE: "attempt-cap";
|
|
|
29
31
|
export declare const GATE_SATISFIED_RELEASE: "gate-satisfied";
|
|
30
32
|
export declare const REVIEW_UPHELD_RELEASE: "review-upheld";
|
|
31
33
|
export declare const RECHECK_RELEASE: "recheck";
|
|
34
|
+
export declare const APPROVAL_REFUSED: "approval-refused";
|
|
35
|
+
/** OBS-1178: the journal row a decision answers — its physical 1-based line plus its timestamp. */
|
|
36
|
+
export interface DecisionBinding {
|
|
37
|
+
line: number;
|
|
38
|
+
ts: string;
|
|
39
|
+
}
|
|
40
|
+
/** The token every surface prints and `approve --park` accepts: `<line>@<ts>`. */
|
|
41
|
+
export declare const bindingToken: (binding: DecisionBinding) => string;
|
|
42
|
+
export declare function parseBindingToken(token: string): DecisionBinding | undefined;
|
|
43
|
+
/** The binding a task-approved row recorded under `park` or `failure`, when well formed. */
|
|
44
|
+
export declare function recordedBinding(value: unknown): DecisionBinding | undefined;
|
|
45
|
+
/** The newest failed gate before a park row — the gate a waive of that park would satisfy. */
|
|
46
|
+
export declare function failedGateBeforePark(events: readonly JournalEvent[], taskId: string, parkIndex: number): GateName | undefined;
|
|
47
|
+
/** The physical journal line of `events[i]` (see SOURCE_LINE). */
|
|
48
|
+
export declare const physicalLine: (events: readonly JournalEvent[], i: number) => number;
|
|
49
|
+
/** A row parsed outside this module (a cockpit capture) names its own physical line to the decision fold. */
|
|
50
|
+
export declare const withPhysicalLine: <T extends JournalEvent>(row: T, line: number) => T;
|
|
51
|
+
/** OBS-1178: an approval neither enacted nor refused yet, and — when it may not be enacted — why. */
|
|
52
|
+
export interface OpenDecision {
|
|
53
|
+
taskId: string;
|
|
54
|
+
line: number;
|
|
55
|
+
stale?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* OBS-1178: THE decision fold. Every reader of task-approved rows — the replay folds, the daemon's
|
|
59
|
+
* startup, sweep and pending-action folds, scope-amendment replay and its audits, status and both
|
|
60
|
+
* cockpits — reads decisions through it, so no two surfaces can disagree on whether one happened.
|
|
61
|
+
*
|
|
62
|
+
* `effective` holds the PHYSICAL lines of the task-approved rows whose effects may apply: a row that
|
|
63
|
+
* was sound when an enactment row consumed it (bound by line and timestamp to the task's newest park,
|
|
64
|
+
* a waive also to that park's failed gate, or a recheck to the newest failure with no park after it;
|
|
65
|
+
* a legacy row carrying no binding only when no park or failure landed between it and that
|
|
66
|
+
* enactment), plus a row still open that is sound NOW. A refused row is never effective, and neither is an open row that no
|
|
67
|
+
* longer binds — a newer park or failure landed, or it names none — whatever unrelated rows landed
|
|
68
|
+
* around it. An enactment row also consumes the task's park and failure: a decision naming either
|
|
69
|
+
* after the task moved on is stale, while the decisions that enactment already consumed stay effective.
|
|
70
|
+
* `open` lists every unenacted, unrefused row with why it may not be enacted, and `refused` the lines
|
|
71
|
+
* an approval-refused row answered.
|
|
72
|
+
*/
|
|
73
|
+
export declare function foldDecisions(events: readonly JournalEvent[]): {
|
|
74
|
+
effective: Set<number>;
|
|
75
|
+
open: OpenDecision[];
|
|
76
|
+
refused: Set<number>;
|
|
77
|
+
};
|
|
78
|
+
/** The physical lines of the task-approved rows whose effects may apply (see foldDecisions). */
|
|
79
|
+
export declare const effectiveDecisions: (events: readonly JournalEvent[]) => Set<number>;
|
|
80
|
+
/**
|
|
81
|
+
* The journal as every decision reader must see it: task-approved rows that are not effective removed,
|
|
82
|
+
* every other row (and its physical line) kept. Order-only folds iterate this instead of skipping rows
|
|
83
|
+
* on their own.
|
|
84
|
+
*/
|
|
85
|
+
export declare function effectiveEvents(events: readonly JournalEvent[]): JournalEvent[];
|
|
86
|
+
/** Per task, the open decisions that may not be enacted now — what the daemon refuses before enactment. */
|
|
87
|
+
export interface StaleApprovals {
|
|
88
|
+
reason: string;
|
|
89
|
+
lines: number[];
|
|
90
|
+
}
|
|
91
|
+
export declare function staleApprovals(events: readonly JournalEvent[]): Map<string, StaleApprovals>;
|
|
32
92
|
export interface PreservedRef {
|
|
33
93
|
ref: string;
|
|
34
94
|
diffCommand: string;
|
|
35
95
|
}
|
|
36
96
|
export declare function preservedRefsByTask(events: JournalEvent[]): Map<string, PreservedRef[]>;
|
|
37
|
-
export declare function reviewRoundsSinceApproval(events: JournalEvent[], taskId: string): number;
|
|
97
|
+
export declare function reviewRoundsSinceApproval(events: JournalEvent[], taskId: string, rounds?: (decided: JournalEvent[]) => JournalEvent[]): number;
|
|
38
98
|
export declare function upheldFeedbackByTask(events: JournalEvent[]): Map<string, string>;
|
|
39
99
|
export interface StructuredFinding {
|
|
40
100
|
class: string;
|
|
@@ -177,6 +237,7 @@ export type PendingApprovalAction = {
|
|
|
177
237
|
* enacts it.
|
|
178
238
|
*/
|
|
179
239
|
export declare function pendingApprovalActions(events: JournalEvent[]): Map<string, PendingApprovalAction>;
|
|
240
|
+
export declare function approvalAction(taskId: string, e: JournalEvent): PendingApprovalAction;
|
|
180
241
|
/**
|
|
181
242
|
* Why the last attempt failed, one row per journaled cause, in the daemon's own `source: details`
|
|
182
243
|
* shape. The daemon builds that brief in a loop-local variable, which dies with the process: a resumed
|
|
@@ -192,10 +253,20 @@ export declare function pendingApprovalActions(events: JournalEvent[]): Map<stri
|
|
|
192
253
|
* failures OR the delivery failure that preceded it. Of the approvals, only a WAIVE clears (the operator
|
|
193
254
|
* retired the findings by fiat — the uphold case re-derives its own brief separately). OBS-1074: a
|
|
194
255
|
* plain approve, a scope grant or a recheck re-funds an attempt that must still see why the last one
|
|
195
|
-
* parked
|
|
196
|
-
*
|
|
256
|
+
* parked — v2.5.7's T11 looped four times on one hygiene oracle because every approval erased exactly
|
|
257
|
+
* the finding the fresh attempt was funded to fix. The operator's stated reasons ride after those rows
|
|
258
|
+
* as `standingRulings` (OBS-1150): no launch and no waive resets them.
|
|
197
259
|
*/
|
|
198
260
|
export declare function journaledFailureBrief(events: JournalEvent[], taskId: string): string[];
|
|
261
|
+
/**
|
|
262
|
+
* OBS-1150: an operator's approval reason is a ruling on the TASK, not on the attempt it released.
|
|
263
|
+
* The failure rows above are spent at the next worker-launch and the review context once bound only
|
|
264
|
+
* the newest reason, so ruling A vanished at the first launch after it and a later ruling B replaced
|
|
265
|
+
* it. Every effective reason therefore stands, oldest first, for the whole run; a repeat is carried
|
|
266
|
+
* once. A gate-satisfied release accepts a gate's verdict rather than ruling on the work, so its
|
|
267
|
+
* reason adds no standing ruling — and, being no ruling, it retires none either.
|
|
268
|
+
*/
|
|
269
|
+
export declare function standingRulings(events: JournalEvent[], taskId: string): string[];
|
|
199
270
|
/** Never infer a cross-path alias from a symbol, sentence, or hash. */
|
|
200
271
|
export declare function reviewFingerprintMatches(candidate: unknown, fingerprint: string): boolean;
|
|
201
272
|
export declare function observedReviewFingerprints(finding: StructuredFinding): string[];
|
|
@@ -256,6 +327,32 @@ export type WorkerResultCause = (typeof WORKER_RESULT_CAUSES)[number];
|
|
|
256
327
|
export declare function isQualityFailureParkKind(kind: ParkKind): boolean;
|
|
257
328
|
export declare function classifyTaskFailure(taskEvents: JournalEvent[]): ParkKind;
|
|
258
329
|
export declare function recordedTaskFailureKind(events: JournalEvent[], taskId: string): ParkKind | undefined;
|
|
330
|
+
export declare const RESUME_HARVEST_SOURCE: "resume";
|
|
331
|
+
export interface InterruptedAttempt {
|
|
332
|
+
attempt: number;
|
|
333
|
+
/** The producing dispatch's own assignment — the author the harvested work is gated under. */
|
|
334
|
+
assignment: Assignment;
|
|
335
|
+
/** Owned evidence still to read: nothing of this attempt's harvest is recorded yet. */
|
|
336
|
+
launch?: {
|
|
337
|
+
nonce: string;
|
|
338
|
+
dispatchScript: string;
|
|
339
|
+
slot: {
|
|
340
|
+
id: string;
|
|
341
|
+
name: string;
|
|
342
|
+
cwd: string;
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
/** A worker-result with no harvest row after it: finish that record. `reaped` only when a resume wrote
|
|
346
|
+
* it — a resume records the result after its owned-process cleanup; a live daemon records it BEFORE
|
|
347
|
+
* handling a failed reap, so its result alone never proves cleanup and `launch` must be reaped again. */
|
|
348
|
+
result?: {
|
|
349
|
+
finished: boolean;
|
|
350
|
+
summary: string;
|
|
351
|
+
reaped: boolean;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
export declare function resumeHarvestAuthor(events: JournalEvent[], taskId: string): Assignment | undefined;
|
|
355
|
+
export declare function interruptedAttempt(events: JournalEvent[], taskId: string): InterruptedAttempt | undefined;
|
|
259
356
|
export declare function runHasEnded(events: JournalEvent[]): boolean;
|
|
260
357
|
/** OBS-53: classify worker-result failures so retries and routing see the true signal, not one lumped bucket. */
|
|
261
358
|
export declare function classifyWorkerResultCause(opts: {
|
|
@@ -392,6 +489,8 @@ export type EngagementCompare = {
|
|
|
392
489
|
export declare function engagementComparable(events: JournalEvent[], loadedHash: string): EngagementCompare;
|
|
393
490
|
export declare function newRunId(now?: Date): string;
|
|
394
491
|
export declare function parseRunId(runId: string): string;
|
|
492
|
+
/** The journal reader rule over bytes in hand: skip blanks, drop a torn line, keep each row's physical line. */
|
|
493
|
+
export declare function parseJournalText(raw: string): JournalEvent[];
|
|
395
494
|
export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?: {
|
|
396
495
|
after?: string;
|
|
397
496
|
}): (TelemetryRow & {
|
|
@@ -402,6 +501,23 @@ export declare const PRIOR_JOURNAL_RUN_WINDOW = 50;
|
|
|
402
501
|
export declare function readPriorRunEvidence(repoRoot: string, tasks: readonly Pick<Task, "id" | "goal" | "files" | "acceptance">[], opts?: {
|
|
403
502
|
suppressRunId?: string;
|
|
404
503
|
}): PriorRunEvidence;
|
|
504
|
+
export declare const REVIEW_NO_VERDICT_RUN_WINDOW = 10;
|
|
505
|
+
export declare const REVIEW_NO_VERDICT_ADVISORY_AT = 2;
|
|
506
|
+
export interface ReviewNoVerdictHistory {
|
|
507
|
+
/** completed runs measured, oldest first */
|
|
508
|
+
runs: string[];
|
|
509
|
+
/** window journals with a row that did not parse — their events are unknown, never zero */
|
|
510
|
+
unreadable: string[];
|
|
511
|
+
/** reviewer channel → review-no-verdict events; every channel seen reviewing is present, 0 included */
|
|
512
|
+
counts: Map<string, number>;
|
|
513
|
+
}
|
|
514
|
+
export declare function readReviewNoVerdictHistory(repoRoot: string, window?: number): ReviewNoVerdictHistory;
|
|
515
|
+
/** One row per measured channel (plus one naming unreadable journals); doctor prints all, Fleet the warn rows. */
|
|
516
|
+
export declare function reviewNoVerdictRows(history: ReviewNoVerdictHistory): {
|
|
517
|
+
channel: string;
|
|
518
|
+
verdict: "pass" | "warn";
|
|
519
|
+
value: string;
|
|
520
|
+
}[];
|
|
405
521
|
export declare function readProfileCursor(repoRoot: string): string | undefined;
|
|
406
522
|
export declare function profileDiscountsPath(repoRoot: string): string;
|
|
407
523
|
export declare function readProfileDiscounts(repoRoot: string): ProfileDiscount[];
|
|
@@ -424,6 +540,13 @@ export declare class Journal {
|
|
|
424
540
|
append(event: string, taskId?: string, data?: Record<string, unknown>): void;
|
|
425
541
|
phaseStart(taskId: string, phase: TaskPhase, data?: Record<string, unknown>): void;
|
|
426
542
|
read(): JournalEvent[];
|
|
543
|
+
/** Parsed rows paired with their physical 1-based journal lines (OBS-1178 bindings name lines). */
|
|
544
|
+
readSourced(): {
|
|
545
|
+
events: JournalEvent[];
|
|
546
|
+
lines: number[];
|
|
547
|
+
};
|
|
548
|
+
/** OBS-1178: the binding of a task's newest park (or `task-failed`) row — what a decision on it names. */
|
|
549
|
+
newestBinding(taskId: string, event?: "task-human" | "task-failed"): DecisionBinding | undefined;
|
|
427
550
|
readTracked(): TrackedJournalRow[];
|
|
428
551
|
replayStatuses(): Map<string, TaskStatus>;
|
|
429
552
|
/**
|