tickmarkr 2.2.1 → 2.3.0
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 -9
- package/dist/adapters/types.d.ts +20 -1
- package/dist/adapters/types.js +42 -2
- package/dist/cli/commands/approve.js +5 -4
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/doctor.d.ts +5 -1
- package/dist/cli/commands/doctor.js +67 -7
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +16 -2
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +140 -25
- package/dist/compile/collateral.js +15 -9
- package/dist/compile/native.js +3 -3
- package/dist/config/config.js +1 -1
- package/dist/drivers/index.d.ts +6 -0
- package/dist/drivers/index.js +19 -4
- package/dist/drivers/orca.d.ts +22 -1
- package/dist/drivers/orca.js +147 -6
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +21 -4
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +25 -9
- package/dist/gates/review.d.ts +3 -1
- package/dist/gates/review.js +40 -12
- package/dist/gates/run-gates.js +17 -10
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/run/consult.js +5 -1
- package/dist/run/daemon.d.ts +12 -0
- package/dist/run/daemon.js +218 -25
- package/dist/run/git.d.ts +1 -0
- package/dist/run/git.js +4 -0
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +70 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +77 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
package/dist/run/daemon.js
CHANGED
|
@@ -23,9 +23,9 @@ import { GATE_NAMES } from "../graph/schema.js";
|
|
|
23
23
|
import { distFingerprint } from "../cli/commands/version.js";
|
|
24
24
|
import { augmentRetryBrief, consult, renderRetryGuidance } from "./consult.js";
|
|
25
25
|
import { runEnvironment } from "./environment.js";
|
|
26
|
-
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, resolvedCapacity, runWithForkBudget, sameCapacity, sh, shGit, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
26
|
+
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, resolvedCapacity, runWithForkBudget, sameCapacity, sh, shGit, SUITE_PARENT_ENV, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
27
27
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
28
|
-
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, renderStructuredReviewFinding, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
28
|
+
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingRechecks, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
29
29
|
import { isDiffCapPark } from "../gates/review.js";
|
|
30
30
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
31
31
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
@@ -69,12 +69,13 @@ export function resolveRunMode(repoRoot, opts = {}) {
|
|
|
69
69
|
return { cfg: resolved.cfg, mode: resolved.mode, source, ...(conflict ? { conflict } : {}) };
|
|
70
70
|
}
|
|
71
71
|
// T14: the events that prove an approval was ENACTED — narrowly causal, never merely subsequent.
|
|
72
|
-
//
|
|
73
|
-
//
|
|
72
|
+
// Ordinary, attempt-cap and review-upheld approvals buy a WORKER, so their proof is a dispatch;
|
|
73
|
+
// recheck has its own battery enactment below. Generic terminal events are deliberately NOT proof: an approved human-gate
|
|
74
74
|
// task can fail in routing before task-dispatch, and the catch appends task-failed — treating that
|
|
75
75
|
// as enactment reports "complete" over an approval that never ran (the exact silent-completion this
|
|
76
76
|
// task exists to kill).
|
|
77
77
|
const DISPATCH_ENACTMENT = new Set(["task-dispatch", "repair-dispatch"]);
|
|
78
|
+
const RECHECK_ENACTMENT = "recheck-battery";
|
|
78
79
|
// The ONE approval that enacts without buying a worker: GATE_SATISFIED_RELEASE resumes from the
|
|
79
80
|
// persisted task branch after the approved gate (execTask's satisfiedGate branch), whose first act
|
|
80
81
|
// for the task is worktree-recreation. That event is causal for this path, not incidental.
|
|
@@ -97,9 +98,13 @@ export function outstandingApprovals(events) {
|
|
|
97
98
|
newest.set(e.taskId, i); });
|
|
98
99
|
return [...newest]
|
|
99
100
|
.filter(([taskId, i]) => {
|
|
100
|
-
const
|
|
101
|
+
const release = events[i].data.release;
|
|
102
|
+
const noWorker = release === GATE_SATISFIED_RELEASE;
|
|
103
|
+
const recheck = release === RECHECK_RELEASE;
|
|
101
104
|
return !events.slice(i + 1).some((e) => e.taskId === taskId
|
|
102
|
-
&& (DISPATCH_ENACTMENT.has(e.event)
|
|
105
|
+
&& (DISPATCH_ENACTMENT.has(e.event)
|
|
106
|
+
|| (noWorker && e.event === GATE_SATISFIED_ENACTMENT)
|
|
107
|
+
|| (recheck && e.event === RECHECK_ENACTMENT)));
|
|
103
108
|
})
|
|
104
109
|
.map(([taskId]) => taskId)
|
|
105
110
|
.sort();
|
|
@@ -165,6 +170,19 @@ const isOracleFailure = (g) => g.details.startsWith("oracle failed:");
|
|
|
165
170
|
*/
|
|
166
171
|
export const gateSatisfied = (g) => (g.pass || g.meta?.skipped === true) && g.meta?.infra !== true;
|
|
167
172
|
const gateFailed = (g) => !gateSatisfied(g);
|
|
173
|
+
const SIGNAL_EXIT_RE = /\b(?:SIGTERM|SIGKILL|signal\s+(?:9|15)|exit(?:s|ed|\s+code)?\s+(?:137|143))\b/i;
|
|
174
|
+
const FAILURE_IDENTITY_RE = /\b(?:AssertionError|FAIL\s+\S|Tests?\s+\d+\s+failed|expected\s+.+\s+to\s+)\b/i;
|
|
175
|
+
/** A signalled test runner with no failure identity produced no verdict about HEAD. Baseline owns the
|
|
176
|
+
* ordinary infra vocabulary; this daemon-only rider handles the signal-shaped non-verdict before its
|
|
177
|
+
* journal row and repair accounting are written. */
|
|
178
|
+
function classifySignalOnlyTest(g) {
|
|
179
|
+
if (g.gate !== "test" || g.pass || g.meta?.infra === true || !SIGNAL_EXIT_RE.test(g.details))
|
|
180
|
+
return;
|
|
181
|
+
const named = Array.isArray(g.meta?.failingTests) && g.meta.failingTests.length > 0;
|
|
182
|
+
if (named || FAILURE_IDENTITY_RE.test(g.details))
|
|
183
|
+
return;
|
|
184
|
+
g.meta = { ...g.meta, classification: "infra", infra: true, retryable: false, kind: "signal-exit" };
|
|
185
|
+
}
|
|
168
186
|
// v1.85 T3: the gates whose failure IS a deterministic measurement — a machine re-ran a command over a
|
|
169
187
|
// tree and printed the same bytes. Those are the failures the fingerprint cap governs (the ruling names
|
|
170
188
|
// it a "deterministic-gate" cap): a third identical answer to a question already answered twice is the
|
|
@@ -270,6 +288,8 @@ function approvedReviewRoundCeiling(events, taskId) {
|
|
|
270
288
|
return undefined;
|
|
271
289
|
}
|
|
272
290
|
const BLOCKED_POLL_MS = 30_000; // between trailer-wait slices, check whether the pane is blocked on a prompt
|
|
291
|
+
export const SUITE_POLL_MS = 250;
|
|
292
|
+
export const APPROVAL_POLL_MS = 250;
|
|
273
293
|
const PROVIDER_DEATH_REQUEUE_CAP = 2; // v1.46 T1: requeue same assignment twice, then fall through to the normal ladder
|
|
274
294
|
const PROVIDER_DEATH_BACKOFF_MS = 500; // short backoff before provider-death requeue
|
|
275
295
|
const NO_TRAILER_DEMOTION_STREAK = 2; // OBS-57: consecutive no-trailer windows demote a channel for the rest of the run
|
|
@@ -558,6 +578,106 @@ async function observeWorkerProcessTree(marker, cwd) {
|
|
|
558
578
|
}
|
|
559
579
|
return tree.size === 0 ? "empty" : "running";
|
|
560
580
|
}
|
|
581
|
+
const SUITE_COMMAND_RE = /(?:^|[\s/])(vitest(?:\.mjs)?|jest|mocha)(?:[\s/]|$)|\bnpm(?:\s+run)?\s+test\b/i;
|
|
582
|
+
function processCwd(pid) {
|
|
583
|
+
try {
|
|
584
|
+
return realpathSync(readlinkSync(`/proc/${pid}/cwd`));
|
|
585
|
+
}
|
|
586
|
+
catch { /* Darwin has no /proc */ }
|
|
587
|
+
try {
|
|
588
|
+
const out = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
589
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
|
|
590
|
+
});
|
|
591
|
+
const path = out.split("\n").find((line) => line.startsWith("n"))?.slice(1);
|
|
592
|
+
return path ? realpathSync(path) : undefined;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function processSuiteParent(pid) {
|
|
599
|
+
try {
|
|
600
|
+
const env = readFileSync(`/proc/${pid}/environ`, "utf8").split("\0");
|
|
601
|
+
const value = env.find((entry) => entry.startsWith(`${SUITE_PARENT_ENV}=`))?.slice(SUITE_PARENT_ENV.length + 1);
|
|
602
|
+
return value && /^\d+$/.test(value) ? Number(value) : undefined;
|
|
603
|
+
}
|
|
604
|
+
catch { /* Darwin has no /proc process environments */ }
|
|
605
|
+
try {
|
|
606
|
+
const out = execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], {
|
|
607
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
|
|
608
|
+
});
|
|
609
|
+
const value = new RegExp(`(?:^|\\s)${SUITE_PARENT_ENV}=(\\d+)(?:\\s|$)`).exec(out)?.[1];
|
|
610
|
+
return value ? Number(value) : undefined;
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const pathAtOrBelow = (root, candidate) => {
|
|
617
|
+
const rel = relative(root, candidate);
|
|
618
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
619
|
+
};
|
|
620
|
+
/** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
|
|
621
|
+
* rules remain testable on hosts that forbid process inspection; production supplies cwd and the
|
|
622
|
+
* inherited TICKMARKR_SUITE_PARENT marker from the process itself. */
|
|
623
|
+
export function countLiveSuites(snapshot, repoRoot, daemonPid = process.pid, cwdForPid = processCwd, suiteParentForPid = processSuiteParent) {
|
|
624
|
+
const rows = [];
|
|
625
|
+
for (const line of snapshot.split("\n")) {
|
|
626
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
627
|
+
if (match && !match[3].startsWith("Z")) {
|
|
628
|
+
rows.push({ pid: Number(match[1]), ppid: Number(match[2]), command: match[4] });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const byPid = new Map(rows.map((row) => [row.pid, row]));
|
|
632
|
+
const ancestors = new Set();
|
|
633
|
+
for (let pid = daemonPid; pid && !ancestors.has(pid); pid = byPid.get(pid)?.ppid ?? 0)
|
|
634
|
+
ancestors.add(pid);
|
|
635
|
+
const descendants = new Set([daemonPid]);
|
|
636
|
+
for (let grew = true; grew;) {
|
|
637
|
+
grew = false;
|
|
638
|
+
for (const row of rows)
|
|
639
|
+
if (!descendants.has(row.pid) && descendants.has(row.ppid)) {
|
|
640
|
+
descendants.add(row.pid);
|
|
641
|
+
grew = true;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const root = realpathSync(repoRoot);
|
|
645
|
+
const candidates = rows.filter((row) => !ancestors.has(row.pid) && SUITE_COMMAND_RE.test(row.command));
|
|
646
|
+
const attributable = new Set(candidates.filter((row) => {
|
|
647
|
+
if (descendants.has(row.pid))
|
|
648
|
+
return true;
|
|
649
|
+
const cwd = cwdForPid(row.pid);
|
|
650
|
+
if (cwd !== undefined && pathAtOrBelow(root, cwd))
|
|
651
|
+
return true;
|
|
652
|
+
const suiteParent = suiteParentForPid(row.pid);
|
|
653
|
+
if (suiteParent === daemonPid)
|
|
654
|
+
return true;
|
|
655
|
+
const parentCwd = suiteParent === undefined ? undefined : cwdForPid(suiteParent);
|
|
656
|
+
return parentCwd !== undefined && pathAtOrBelow(root, parentCwd);
|
|
657
|
+
}).map((row) => row.pid));
|
|
658
|
+
// npm/npx + vitest + pool workers are one suite. Count only attributable suite processes with no
|
|
659
|
+
// attributable suite ancestor, while still following ordinary non-suite parents between them.
|
|
660
|
+
return [...attributable].filter((pid) => {
|
|
661
|
+
for (let parent = byPid.get(pid)?.ppid; parent; parent = byPid.get(parent)?.ppid) {
|
|
662
|
+
if (attributable.has(parent))
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
return true;
|
|
666
|
+
}).length;
|
|
667
|
+
}
|
|
668
|
+
let liveSuiteCountForTests;
|
|
669
|
+
export const setLiveSuiteCountForTests = (probe) => {
|
|
670
|
+
liveSuiteCountForTests = probe;
|
|
671
|
+
};
|
|
672
|
+
export const resetLiveSuiteCountForTests = () => { liveSuiteCountForTests = undefined; };
|
|
673
|
+
/** Live full-suite roots attributable to this repository or this daemon. Ancestors are excluded so
|
|
674
|
+
* a daemon invoked by vitest does not wait on its own test harness forever. */
|
|
675
|
+
export async function liveSuiteCount(repoRoot) {
|
|
676
|
+
if (liveSuiteCountForTests)
|
|
677
|
+
return liveSuiteCountForTests(repoRoot);
|
|
678
|
+
const snapshot = await shGit("ps -Aww -o pid=,ppid=,state=,command=", repoRoot, 15_000);
|
|
679
|
+
return snapshot.code === 0 ? countLiveSuites(snapshot.stdout, repoRoot) : 0;
|
|
680
|
+
}
|
|
561
681
|
const OBSERVE_CHUNK_BYTES = 64 * 1024;
|
|
562
682
|
const OBSERVE_BUDGET_BYTES = 256 * 1024 * 1024;
|
|
563
683
|
let observeBudgetBytes = OBSERVE_BUDGET_BYTES;
|
|
@@ -1386,6 +1506,41 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1386
1506
|
mergeChain = next.catch(() => undefined);
|
|
1387
1507
|
return next;
|
|
1388
1508
|
};
|
|
1509
|
+
// OBS-829/OBS-854: one full-suite verdict round at a time in this run, and do not begin beside an
|
|
1510
|
+
// externally live suite attributable to this repository. The process scan catches nested scratch
|
|
1511
|
+
// suites through daemon parentage even after cwd stops naming a worktree.
|
|
1512
|
+
let suiteChain = Promise.resolve();
|
|
1513
|
+
let suitePending = 0;
|
|
1514
|
+
const withSuiteWindow = async (taskId, enabled, run) => {
|
|
1515
|
+
if (!enabled)
|
|
1516
|
+
return run();
|
|
1517
|
+
const previous = suiteChain;
|
|
1518
|
+
let release;
|
|
1519
|
+
suiteChain = new Promise((resolve) => { release = resolve; });
|
|
1520
|
+
const queued = suitePending++ > 0;
|
|
1521
|
+
if (queued) {
|
|
1522
|
+
const count = Math.max(1, await liveSuiteCount(repoRoot));
|
|
1523
|
+
journal.append("suite-wait", taskId, { count });
|
|
1524
|
+
}
|
|
1525
|
+
await previous;
|
|
1526
|
+
try {
|
|
1527
|
+
let lastCount = -1;
|
|
1528
|
+
for (;;) {
|
|
1529
|
+
const count = await liveSuiteCount(repoRoot);
|
|
1530
|
+
if (count === 0)
|
|
1531
|
+
break;
|
|
1532
|
+
if (count !== lastCount)
|
|
1533
|
+
journal.append("suite-wait", taskId, { count });
|
|
1534
|
+
lastCount = count;
|
|
1535
|
+
await new Promise((wake) => setTimeout(wake, SUITE_POLL_MS));
|
|
1536
|
+
}
|
|
1537
|
+
return await run();
|
|
1538
|
+
}
|
|
1539
|
+
finally {
|
|
1540
|
+
suitePending--;
|
|
1541
|
+
release();
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1389
1544
|
// gateFails/consults are execTask-scoped counters passed in so a park row is a rich verified-failure
|
|
1390
1545
|
// observation (e.g. ladder-exhausted + gateFails:4); every task-human row has a closed kind, never prose alone.
|
|
1391
1546
|
const gateFailApprovalReason = (taskId, identity, includeUphold = false) => {
|
|
@@ -1853,21 +2008,24 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1853
2008
|
// contiguous green prefix whose recorded commit is still the task branch tip.
|
|
1854
2009
|
const satisfiedGate = satisfiedGates.get(t.id);
|
|
1855
2010
|
const replayedGates = replayedGateResults.get(t.id);
|
|
1856
|
-
|
|
2011
|
+
const recheck = pendingRechecks(journal.read()).has(t.id);
|
|
2012
|
+
resumeGateReplay: if (satisfiedGate || replayedGates || recheck) {
|
|
1857
2013
|
const taskBase = await integrationHead(intWt);
|
|
1858
2014
|
const taskBranch = `${branch}--${t.id}`;
|
|
1859
2015
|
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
1860
2016
|
if (!existsSync(priorWt)) {
|
|
1861
|
-
if (satisfiedGate)
|
|
1862
|
-
throw new Error(`approved gate ${satisfiedGate} cannot resume: task worktree is missing`);
|
|
2017
|
+
if (satisfiedGate || recheck)
|
|
2018
|
+
throw new Error(`${recheck ? "recheck" : `approved gate ${satisfiedGate}`} cannot resume: task worktree is missing`);
|
|
1863
2019
|
// Observed passes are an optimization, never authority: without the task worktree there is no
|
|
1864
2020
|
// commit to compare and no landed work to gate, so fall through to the ordinary worker path.
|
|
1865
2021
|
replayedGateResults.delete(t.id);
|
|
1866
2022
|
break resumeGateReplay;
|
|
1867
2023
|
}
|
|
1868
|
-
const resumeReason =
|
|
1869
|
-
?
|
|
1870
|
-
:
|
|
2024
|
+
const resumeReason = recheck
|
|
2025
|
+
? "operator recheck"
|
|
2026
|
+
: satisfiedGate
|
|
2027
|
+
? `approved gate ${satisfiedGate}`
|
|
2028
|
+
: `recorded gates on ${replayedGates.commit.slice(0, 10)}`;
|
|
1871
2029
|
const priorTaskTip = await gitHead(priorWt);
|
|
1872
2030
|
const priorTaskSubject = await gateCommitSubject(taskBase, priorTaskTip, priorWt);
|
|
1873
2031
|
const commitsToCarry = await commitsAheadOf(taskBase, priorWt);
|
|
@@ -1919,7 +2077,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1919
2077
|
: [],
|
|
1920
2078
|
raw: "",
|
|
1921
2079
|
};
|
|
1922
|
-
const
|
|
2080
|
+
const parkedAuthor = recheck
|
|
2081
|
+
? [...journal.read()].reverse().find((e) => e.event === "task-dispatch" && e.taskId === t.id)?.data.assignment
|
|
2082
|
+
: undefined;
|
|
2083
|
+
const gateAuthor = parkedAuthor ?? rs?.lastAssignment ?? assignment;
|
|
1923
2084
|
const satisfiedIndex = satisfiedGate ? GATE_NAMES.indexOf(satisfiedGate) : -1;
|
|
1924
2085
|
// The serial pipeline could have at most one blocking result, so "everything after the
|
|
1925
2086
|
// approved gate" was enough. v1.85 can record both verdict siblings red in one round, and a
|
|
@@ -1943,7 +2104,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1943
2104
|
priorResults.set(e.data.gate, e);
|
|
1944
2105
|
}
|
|
1945
2106
|
let remainingGates;
|
|
1946
|
-
if (
|
|
2107
|
+
if (recheck) {
|
|
2108
|
+
remainingGates = GATE_NAMES.filter((gate) => t.gates.includes(gate));
|
|
2109
|
+
}
|
|
2110
|
+
else if (satisfiedGate) {
|
|
1947
2111
|
remainingGates = t.gates.filter((gate) => {
|
|
1948
2112
|
if (gate === satisfiedGate)
|
|
1949
2113
|
return false;
|
|
@@ -2001,10 +2165,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2001
2165
|
// This suffix is re-measured to decide whether resume may advance, but the interrupted
|
|
2002
2166
|
// attempt already paid for its red result. The next worker-backed round remains the next
|
|
2003
2167
|
// deterministic-fingerprint occurrence/review round for budget accounting.
|
|
2004
|
-
...(!satisfiedGate ? { replayMeasurement: true } : {}),
|
|
2168
|
+
...(!satisfiedGate && !recheck ? { replayMeasurement: true } : {}),
|
|
2005
2169
|
};
|
|
2006
2170
|
journal.phaseStart(t.id, "gates");
|
|
2007
|
-
const { results } = await runGates(resumedTask, {
|
|
2171
|
+
const { results } = await withSuiteWindow(t.id, resumedTask.gates.includes("test") && commands.test !== undefined, () => runGates(resumedTask, {
|
|
2008
2172
|
worktree: wt, baseRef: taskBase, result: priorResult, author: gateAuthor,
|
|
2009
2173
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
2010
2174
|
collateral: collateral.get(t.id) ?? [],
|
|
@@ -2031,6 +2195,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2031
2195
|
return;
|
|
2032
2196
|
}
|
|
2033
2197
|
const g = e.result;
|
|
2198
|
+
classifySignalOnlyTest(g);
|
|
2034
2199
|
inParallelOrder(g.gate, () => {
|
|
2035
2200
|
journalGateResult(g);
|
|
2036
2201
|
noteReviewRetry(g);
|
|
@@ -2040,7 +2205,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2040
2205
|
}
|
|
2041
2206
|
});
|
|
2042
2207
|
},
|
|
2043
|
-
});
|
|
2208
|
+
}));
|
|
2209
|
+
results.forEach(classifySignalOnlyTest);
|
|
2210
|
+
if (pendingRechecks(journal.read()).has(t.id)) {
|
|
2211
|
+
journal.append("recheck-battery", t.id, {
|
|
2212
|
+
commit: gateSubject.commit,
|
|
2213
|
+
gates: resumedTask.gates,
|
|
2214
|
+
pass: results.every(gateSatisfied),
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2044
2217
|
const approvedCommits = await commitsAheadOf(taskBase, wt);
|
|
2045
2218
|
graph = addEvidence(graph, t.id, { commits: approvedCommits, gateResults: results });
|
|
2046
2219
|
saveGraph(repoRoot, graph);
|
|
@@ -2375,7 +2548,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2375
2548
|
const adapter = getAdapter(assignment.adapter, adapters);
|
|
2376
2549
|
// VIS-04: workers share one role tab. T2: `owned` names the pane canonically (ownership contract);
|
|
2377
2550
|
// the legacy name stays the fallback for drivers without owned handling (subprocess spies).
|
|
2378
|
-
const
|
|
2551
|
+
const workerSlotOpts = {
|
|
2552
|
+
group: "workers",
|
|
2553
|
+
owned: { role: "worker", taskId: t.id, attempt, runId },
|
|
2554
|
+
};
|
|
2555
|
+
// Keep the established enumerable slot-options shape consumed by legacy drivers while making
|
|
2556
|
+
// the adapter hook available as an own request field to execution surfaces such as Orca.
|
|
2557
|
+
Object.defineProperty(workerSlotOpts, "agent", { value: assignment.adapter });
|
|
2558
|
+
const slot = await trackedDriver.slot(wt, `${t.id}-worker-${assignment.adapter}-a${attempt}-${runTag}`, workerSlotOpts);
|
|
2379
2559
|
const sessionId = retryMode === "resume" ? priorSession.id : slot.name;
|
|
2380
2560
|
const icmd = retryMode === "resume"
|
|
2381
2561
|
? adapter.resumeCommand(sessionId, promptFile, assignment.model)
|
|
@@ -3478,6 +3658,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3478
3658
|
return;
|
|
3479
3659
|
}
|
|
3480
3660
|
const g = e.result;
|
|
3661
|
+
classifySignalOnlyTest(g);
|
|
3481
3662
|
inParallelOrder(g.gate, () => {
|
|
3482
3663
|
// GATE-09 (ROADMAP SC-4): journal every judge retry as an attributable event — which gate flaked,
|
|
3483
3664
|
// which channel flaked, which channel retried — so `tickmarkr journal`/report can distinguish "judge
|
|
@@ -3510,7 +3691,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3510
3691
|
const gated = await gitHead(wt);
|
|
3511
3692
|
gateSubject = { commit: await gateCommitSubject(taskBase, gated, wt), attempt };
|
|
3512
3693
|
journal.phaseStart(t.id, "gates");
|
|
3513
|
-
({ results, commits } = await runGates(t, {
|
|
3694
|
+
({ results, commits } = await withSuiteWindow(t.id, t.gates.includes("test") && commands.test !== undefined, () => runGates(t, {
|
|
3514
3695
|
worktree: wt, baseRef: taskBase, result, author: assignment,
|
|
3515
3696
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
3516
3697
|
collateral: collateral.get(t.id) ?? [],
|
|
@@ -3533,7 +3714,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3533
3714
|
: undefined,
|
|
3534
3715
|
excludeReviewers: badReviewers,
|
|
3535
3716
|
onGate,
|
|
3536
|
-
}));
|
|
3717
|
+
})));
|
|
3718
|
+
results.forEach(classifySignalOnlyTest);
|
|
3537
3719
|
graph = addEvidence(graph, t.id, { commits, gateResults: results, artifacts: [promptFile] });
|
|
3538
3720
|
saveGraph(repoRoot, graph);
|
|
3539
3721
|
if (results.some((g) => g.gate === "test" && !g.pass))
|
|
@@ -3630,6 +3812,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3630
3812
|
// failing gate, and `feedback` still carries every one of them to the next attempt.
|
|
3631
3813
|
const repairBattery = failing.some((g) => g.gate !== "review") ? failing.filter((g) => g.gate !== "review") : failing;
|
|
3632
3814
|
const repairable = narrowRepairBattery(repairBattery) && lostCommits.length === 0 && landed.length > 0;
|
|
3815
|
+
const repairHistory = repairReachSinceApproval(journal.read(), t.id);
|
|
3633
3816
|
const repairsDrawn = repairsSinceApproval(journal.read(), t.id);
|
|
3634
3817
|
const repair = repairable && repairsDrawn < MAX_REPAIRS;
|
|
3635
3818
|
// v1.85 T3: the fingerprint cap. Two normalized-identical failures of one DETERMINISTIC gate on
|
|
@@ -3699,13 +3882,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3699
3882
|
const repairExhausted = repairable && !repair;
|
|
3700
3883
|
if (repair && !capStep) {
|
|
3701
3884
|
journal.append("repair-attempt", t.id, {
|
|
3702
|
-
repair:
|
|
3885
|
+
repair: repairHistory.length + 1, charge: repairsDrawn + 1, of: MAX_REPAIRS,
|
|
3886
|
+
gates: failing.map((g) => g.gate),
|
|
3703
3887
|
commits: landed.length,
|
|
3704
3888
|
findings: feedback, // the failure bytes this repair must carry, replayable across a resume
|
|
3705
3889
|
});
|
|
3706
3890
|
}
|
|
3707
3891
|
else if (repairExhausted && !capStep) {
|
|
3708
|
-
journal.append("repair-exhausted", t.id, {
|
|
3892
|
+
journal.append("repair-exhausted", t.id, {
|
|
3893
|
+
repairs: repairsDrawn, of: MAX_REPAIRS, gates: failing.map((g) => g.gate),
|
|
3894
|
+
reached: repairHistory,
|
|
3895
|
+
});
|
|
3709
3896
|
}
|
|
3710
3897
|
const step = capStep ?? (repair || (reviewFixRetry && !repairExhausted)
|
|
3711
3898
|
? "retry"
|
|
@@ -3714,7 +3901,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3714
3901
|
journal.append("escalation", t.id, {
|
|
3715
3902
|
step, attempt: attempt + 1,
|
|
3716
3903
|
...(reviewFixRetry && !repairExhausted ? { reviewFix: true } : {}),
|
|
3717
|
-
...(repair ? { repair: repairsDrawn + 1 } : {}),
|
|
3904
|
+
...(repair ? { repair: repairHistory.length + 1, repairCharge: repairsDrawn + 1 } : {}),
|
|
3718
3905
|
});
|
|
3719
3906
|
await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
|
|
3720
3907
|
}
|
|
@@ -3769,7 +3956,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3769
3956
|
}
|
|
3770
3957
|
if (inflight.size === 0)
|
|
3771
3958
|
break;
|
|
3772
|
-
|
|
3959
|
+
const waiters = [...inflight.values(), aborted];
|
|
3960
|
+
// A free slot is itself a scheduling boundary: poll the append-only approval stream instead of
|
|
3961
|
+
// sleeping until an unrelated long-running task settles.
|
|
3962
|
+
if (inflight.size < concurrency) {
|
|
3963
|
+
waiters.push(new Promise((wake) => setTimeout(wake, APPROVAL_POLL_MS)));
|
|
3964
|
+
}
|
|
3965
|
+
await Promise.race(waiters); // aborted rejects on termination — unwinds the run
|
|
3773
3966
|
}
|
|
3774
3967
|
// D-07: the sweep now closes only what's LEFT in keptSlots — done-closed worker slots were removed
|
|
3775
3968
|
// (no double-close) and self-cleaned LLM/consult panes were never added under keepLlm:false. This
|
|
@@ -3794,7 +3987,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3794
3987
|
// OBS-34: post-merge integration-tip verify — strict exit codes, no baseline forgiveness.
|
|
3795
3988
|
const lastMergedTask = [...journal.read()].reverse().find((e) => e.event === "merge" && e.taskId)?.taskId;
|
|
3796
3989
|
if (summary.done.length > 0 && Object.keys(commands).length > 0) {
|
|
3797
|
-
const tipFailed = await verifyIntegrationTipCached(intWt, commands, journal, { lastMergedTask, baseline });
|
|
3990
|
+
const tipFailed = await withSuiteWindow(undefined, commands.test !== undefined, () => verifyIntegrationTipCached(intWt, commands, journal, { lastMergedTask, baseline }));
|
|
3798
3991
|
summary.tipVerify = tipFailed ? "failed" : "passed";
|
|
3799
3992
|
if (tipFailed && lastMergedTask)
|
|
3800
3993
|
summary.lastMergedTask = lastMergedTask;
|
package/dist/run/git.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
3
3
|
export { ROUTING_ENV_SEAMS };
|
|
4
4
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
5
|
+
export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
5
6
|
export declare const DEFAULT_FORK_CAP = "6";
|
|
6
7
|
/**
|
|
7
8
|
* OBS-618: how many PROCESSES one vitest fork can hold at its peak — the fork, a daemon it spawns,
|
package/dist/run/git.js
CHANGED
|
@@ -13,6 +13,7 @@ export { ROUTING_ENV_SEAMS };
|
|
|
13
13
|
// Default to a modest cap through the environment so vitest honors it natively; never pass it
|
|
14
14
|
// as argv (OBS-55) so child test oracles stay intact. The operator's own export wins.
|
|
15
15
|
export const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
16
|
+
export const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
16
17
|
export const DEFAULT_FORK_CAP = "6";
|
|
17
18
|
/**
|
|
18
19
|
* The fork budget belongs to ONE run: how many gate suites can be in flight at once is exactly that
|
|
@@ -150,6 +151,9 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
150
151
|
// OBS-110: apply the run's own fork cap only when the operator has not already set one.
|
|
151
152
|
if (!(FORK_CAP_ENV in env))
|
|
152
153
|
env[FORK_CAP_ENV] = resolvedForkCap();
|
|
154
|
+
// OBS-854: descendants can leave the checkout (nested fixture suites do), so cwd alone cannot
|
|
155
|
+
// attribute them. Every daemon shell exports the daemon pid as their durable parentage marker.
|
|
156
|
+
env[SUITE_PARENT_ENV] = String(process.pid);
|
|
153
157
|
// T7: the capacity every result of this shell carries, read HERE — off the environment the child
|
|
154
158
|
// is about to receive, after the precedence above has settled. An operator export is already in
|
|
155
159
|
// `env`, so what gets recorded is the operator's number, which is the case a release was re-taken
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -116,8 +116,21 @@ export declare function formatPriorFindingEvidence(evidence: PriorFindingEvidenc
|
|
|
116
116
|
export declare function normalizeGateFailure(details: string): string;
|
|
117
117
|
export declare const GATE_FINGERPRINT_CAP = 2;
|
|
118
118
|
export declare function identicalGateFailures(events: JournalEvent[], taskId: string, gate: string, normalized: string): number;
|
|
119
|
-
/**
|
|
119
|
+
/** What each funded repair's next battery actually reached. A repair spends its budget only
|
|
120
|
+
* when that battery reaches one of the gates it was funded to fix; an earlier red is evidence that
|
|
121
|
+
* this repair never got its funded turn, not a charge against the repair ladder. */
|
|
122
|
+
export interface RepairReach {
|
|
123
|
+
repair: number;
|
|
124
|
+
funded: string[];
|
|
125
|
+
reached: string[];
|
|
126
|
+
diedAt?: string;
|
|
127
|
+
charged: boolean;
|
|
128
|
+
}
|
|
129
|
+
export declare function repairReachSinceApproval(events: JournalEvent[], taskId: string): RepairReach[];
|
|
130
|
+
/** Repair slots actually spent this engagement — journal-derived, so a resume inherits it. */
|
|
120
131
|
export declare function repairsSinceApproval(events: JournalEvent[], taskId: string): number;
|
|
132
|
+
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
133
|
+
export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
121
134
|
/**
|
|
122
135
|
* Why the last attempt failed, one row per journaled cause, in the daemon's own `source: details`
|
|
123
136
|
* shape. The daemon builds that brief in a loop-local variable, which dies with the process: a resumed
|
|
@@ -242,9 +255,9 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
242
255
|
quotaFailover: z.ZodOptional<z.ZodLiteral<true>>;
|
|
243
256
|
overrun: z.ZodOptional<z.ZodLiteral<true>>;
|
|
244
257
|
retryMode: z.ZodOptional<z.ZodEnum<{
|
|
258
|
+
repair: "repair";
|
|
245
259
|
resume: "resume";
|
|
246
260
|
fresh: "fresh";
|
|
247
|
-
repair: "repair";
|
|
248
261
|
}>>;
|
|
249
262
|
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
250
263
|
signalBasis: z.ZodOptional<z.ZodEnum<{
|
package/dist/run/journal.js
CHANGED
|
@@ -72,8 +72,8 @@ export const REVIEW_UPHELD_RELEASE = "review-upheld";
|
|
|
72
72
|
// OBS-203: the third decision a gate-fail park needs. Plain approve WAIVES the failed gate (and every
|
|
73
73
|
// gate before it — daemon.ts remainingGates), which is wrong when the gate failed against a stale task
|
|
74
74
|
// DECLARATION rather than a bad diff: amend the spec's files[], recompile, and the gate now passes
|
|
75
|
-
// honestly. This release
|
|
76
|
-
//
|
|
75
|
+
// honestly. This release runs the whole gate suite over the parked commit before any worker and
|
|
76
|
+
// marks no gate satisfied: green merges directly, red returns to the ladder. Budget semantics match attempt-cap
|
|
77
77
|
// (fresh attempts, tried survives) because the park cost the task its remaining budget.
|
|
78
78
|
export const RECHECK_RELEASE = "recheck";
|
|
79
79
|
// OBS-738: one authority for every recovery surface. The ref is accepted only from the row that
|
|
@@ -547,18 +547,65 @@ export function identicalGateFailures(events, taskId, gate, normalized) {
|
|
|
547
547
|
}
|
|
548
548
|
return n;
|
|
549
549
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let n = 0;
|
|
550
|
+
export function repairReachSinceApproval(events, taskId) {
|
|
551
|
+
const repairs = [];
|
|
553
552
|
for (const e of events) {
|
|
554
553
|
if (e.taskId !== taskId)
|
|
555
554
|
continue;
|
|
556
|
-
if (e.event === "task-approved")
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
555
|
+
if (e.event === "task-approved") {
|
|
556
|
+
repairs.length = 0;
|
|
557
|
+
}
|
|
558
|
+
else if (e.event === "repair-attempt") {
|
|
559
|
+
const funded = Array.isArray(e.data.gates)
|
|
560
|
+
? e.data.gates.filter((gate) => typeof gate === "string")
|
|
561
|
+
: [];
|
|
562
|
+
repairs.push({
|
|
563
|
+
repair: typeof e.data.repair === "number" ? e.data.repair : repairs.length + 1,
|
|
564
|
+
funded,
|
|
565
|
+
reached: [],
|
|
566
|
+
charged: false,
|
|
567
|
+
launched: false,
|
|
568
|
+
closed: false,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const repair = repairs.at(-1);
|
|
573
|
+
if (!repair || repair.closed)
|
|
574
|
+
continue;
|
|
575
|
+
if (e.event === "worker-launch") {
|
|
576
|
+
if (repair.launched)
|
|
577
|
+
repair.closed = true;
|
|
578
|
+
else
|
|
579
|
+
repair.launched = true;
|
|
580
|
+
}
|
|
581
|
+
else if (repair.launched && e.event === "gate-result" && typeof e.data.gate === "string") {
|
|
582
|
+
if (!repair.reached.includes(e.data.gate))
|
|
583
|
+
repair.reached.push(e.data.gate);
|
|
584
|
+
if (e.data.pass === false && repair.diedAt === undefined)
|
|
585
|
+
repair.diedAt = e.data.gate;
|
|
586
|
+
if (repair.funded.includes(e.data.gate))
|
|
587
|
+
repair.charged = true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
560
590
|
}
|
|
561
|
-
return
|
|
591
|
+
return repairs.map(({ launched: _launched, closed: _closed, ...repair }) => repair);
|
|
592
|
+
}
|
|
593
|
+
/** Repair slots actually spent this engagement — journal-derived, so a resume inherits it. */
|
|
594
|
+
export function repairsSinceApproval(events, taskId) {
|
|
595
|
+
return repairReachSinceApproval(events, taskId).filter((repair) => repair.charged).length;
|
|
596
|
+
}
|
|
597
|
+
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
598
|
+
export function pendingRechecks(events) {
|
|
599
|
+
const pending = new Set();
|
|
600
|
+
for (const e of events) {
|
|
601
|
+
if (!e.taskId)
|
|
602
|
+
continue;
|
|
603
|
+
if (e.event === "task-approved" && e.data.release === RECHECK_RELEASE)
|
|
604
|
+
pending.add(e.taskId);
|
|
605
|
+
else if (e.event === "recheck-battery" || e.event === "worker-launch")
|
|
606
|
+
pending.delete(e.taskId);
|
|
607
|
+
}
|
|
608
|
+
return pending;
|
|
562
609
|
}
|
|
563
610
|
// Both retry decisions below govern exactly ONE dispatch: the next one. So both are read back from the
|
|
564
611
|
// journal at the moment that dispatch is built, never carried in a process variable — a stop between
|
|
@@ -695,8 +742,19 @@ export function pendingRepairFindings(events, taskId) {
|
|
|
695
742
|
* a channel it is no longer using, and a later unrelated failure is not parked under a stale reason.
|
|
696
743
|
*/
|
|
697
744
|
export function activeRetryBan(events, taskId, channel) {
|
|
698
|
-
|
|
699
|
-
|
|
745
|
+
let pending;
|
|
746
|
+
for (const e of events) {
|
|
747
|
+
if (e.taskId !== taskId)
|
|
748
|
+
continue;
|
|
749
|
+
if (e.event === "gate-fingerprint-cap")
|
|
750
|
+
pending = e;
|
|
751
|
+
else if (e.event === DECISION_SPENT
|
|
752
|
+
|| (e.event === "task-approved" && e.data.release === RECHECK_RELEASE))
|
|
753
|
+
pending = undefined;
|
|
754
|
+
}
|
|
755
|
+
return pending && pending.data.channel === channel && typeof pending.data.gate === "string"
|
|
756
|
+
? pending.data.gate
|
|
757
|
+
: undefined;
|
|
700
758
|
}
|
|
701
759
|
// Fail-closed shape for a dispatched assignment (journal.ts:75-90 posture): a malformed assignment in
|
|
702
760
|
// one dispatch degrades that single task toward today's behavior — counts toward attempts, contributes
|
|
@@ -7,6 +7,12 @@ export declare const SUPERVISION_FUTURE_GRACE_MS = 1000;
|
|
|
7
7
|
export declare const SUPERVISION_DEFAULT_THRESHOLD_PCT = 75;
|
|
8
8
|
export declare const SUPERVISION_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch"];
|
|
9
9
|
export type SupervisionTier = (typeof SUPERVISION_TIERS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a one-shot supervision writer to the repository state it is claiming to supervise.
|
|
12
|
+
* The nearest state directory wins; reaching a git root without one is a refusal, not permission
|
|
13
|
+
* to create fresh state there or to wander into an enclosing repository. This function is read-only.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveSupervisionRoot(cwd: string): string;
|
|
10
16
|
export declare const SUPERVISION_SEAT_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context"];
|
|
11
17
|
export type SeatTier = (typeof SUPERVISION_SEAT_TIERS)[number];
|
|
12
18
|
/** Does this tier's record have to name the seat it speaks for? */
|
package/dist/run/supervision.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { stateDirName, tickmarkrDir } from "../graph/graph.js";
|
|
5
5
|
// SUP-01: supervision liveness as FILE STATE, not as a report — lock.ts's proven shape, one file per
|
|
6
6
|
// tier. A watcher beats its OWN file; every other seat derives that tier's state by stat()ing it. So
|
|
@@ -41,6 +41,34 @@ export const SUPERVISION_DEFAULT_THRESHOLD_PCT = 75;
|
|
|
41
41
|
export const SUPERVISION_TIERS = [
|
|
42
42
|
"orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch",
|
|
43
43
|
];
|
|
44
|
+
const pathKind = (path) => {
|
|
45
|
+
try {
|
|
46
|
+
return statSync(path).isDirectory() ? "directory" : "present";
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "absent";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a one-shot supervision writer to the repository state it is claiming to supervise.
|
|
54
|
+
* The nearest state directory wins; reaching a git root without one is a refusal, not permission
|
|
55
|
+
* to create fresh state there or to wander into an enclosing repository. This function is read-only.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveSupervisionRoot(cwd) {
|
|
58
|
+
let current = resolve(cwd);
|
|
59
|
+
for (;;) {
|
|
60
|
+
if (pathKind(join(current, stateDirName(current))) === "directory")
|
|
61
|
+
return current;
|
|
62
|
+
if (pathKind(join(current, ".git")) !== "absent") {
|
|
63
|
+
throw new Error(`missing ${stateDirName(current)}/ state dir at repository root ${current}`);
|
|
64
|
+
}
|
|
65
|
+
const parent = dirname(current);
|
|
66
|
+
if (parent === current) {
|
|
67
|
+
throw new Error(`missing ${stateDirName(current)}/ state dir — no tickmarkr repository found from ${resolve(cwd)}`);
|
|
68
|
+
}
|
|
69
|
+
current = parent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
44
72
|
// The tiers whose records must NAME the seat behind them. A one-shot `tickmarkr beat` records a pid
|
|
45
73
|
// that has already exited by the time anyone reads it, and an instant — nothing a reader can attribute
|
|
46
74
|
// to a seat. Measured 2026-08-26: a consult seat of another tier ran the documented beat loop and the
|