tickmarkr 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code.js +18 -3
- package/dist/adapters/pi.d.ts +6 -0
- package/dist/adapters/pi.js +99 -18
- package/dist/cli/commands/beat.js +28 -8
- package/dist/cli/commands/init.js +28 -1
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.js +40 -16
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/gsd.js +21 -1
- package/dist/gates/baseline.d.ts +10 -0
- package/dist/gates/baseline.js +46 -3
- package/dist/run/daemon.d.ts +0 -1
- package/dist/run/daemon.js +54 -19
- package/dist/run/git.d.ts +20 -0
- package/dist/run/git.js +114 -8
- package/dist/run/journal.d.ts +24 -0
- package/dist/run/journal.js +62 -1
- package/dist/run/lock.d.ts +1 -0
- package/dist/run/lock.js +23 -13
- package/dist/run/supervision.d.ts +9 -3
- package/dist/run/supervision.js +185 -38
- package/dist/tui/cockpit/derive.js +5 -10
- package/fixtures/gsd-sample/07-live-check/07-03-SUMMARY.md +4 -0
- package/package.json +2 -2
- package/skills/tickmarkr-auto/SKILL.md +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +37 -14
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +80 -31
package/dist/run/daemon.js
CHANGED
|
@@ -19,16 +19,15 @@ import { addEvidence, attributeBlocked, blockedTasks, getTask, graphDefinitionHa
|
|
|
19
19
|
import { GATE_NAMES } from "../graph/schema.js";
|
|
20
20
|
import { augmentRetryBrief, consult, renderRetryGuidance } from "./consult.js";
|
|
21
21
|
import { runEnvironment } from "./environment.js";
|
|
22
|
-
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, runWithForkBudget, sh, shGit, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
22
|
+
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, runWithForkBudget, sh, shGit, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
23
23
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
24
|
-
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
24
|
+
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingReviewFindings, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
25
25
|
import { isDiffCapPark } from "../gates/review.js";
|
|
26
26
|
import { acquireApprovalSerialization, acquireRunLock, releaseRunLock } from "./lock.js";
|
|
27
27
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
28
28
|
import { nextChannel, route } from "../route/router.js";
|
|
29
29
|
import { desiredPanes } from "./reconcile.js";
|
|
30
30
|
import { harvestCpuFlatWindowMs, NUDGEABLE_ADAPTERS, PANE_READ_ROWS, StallProgressTracker, stallSnapshotBannerRows, WorkerTreeCpuAccountant, } from "./stall.js";
|
|
31
|
-
import { armSupervision } from "./supervision.js";
|
|
32
31
|
// Compatibility exports for the daemon liveness tests and existing consumers. The implementation
|
|
33
32
|
// lives in stall.ts so gate dispatch can depend on it without importing the daemon.
|
|
34
33
|
export { harvestCpuFlatWindowMs, resetHarvestCpuFlatMsForTests, setHarvestCpuFlatMsForTests, workerTreeCpuMs, } from "./stall.js";
|
|
@@ -721,13 +720,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
721
720
|
// subprocess, so the optional-chain open below is a no-op there). Cosmetic-only: any failure is
|
|
722
721
|
// swallowed (never affects the run); the operator closes a surviving watch pane.
|
|
723
722
|
const lock = acquireRunLock(repoRoot, runId);
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
// process owns the run) and held to the last, so the beat's span is the run's span. armSupervision
|
|
728
|
-
// never throws, so an unwritable beat can never take a run down; it is deregistered in BOTH exits
|
|
729
|
-
// below, because the signal reaper exits the process before the finally can run.
|
|
730
|
-
const supervision = opts.supervise === false ? undefined : armSupervision(repoRoot, "orchestrator");
|
|
723
|
+
// D10: the lock is this daemon's liveness record and already carries its pid; status prints that
|
|
724
|
+
// identity beside the supervision row. The `orchestrator` tier belongs exclusively to the seated
|
|
725
|
+
// supervisor, so a run never beats or stands down that seat's record on the daemon's behalf.
|
|
731
726
|
// v1.54 T2: declared before the try so the finally can always deregister (a throw before
|
|
732
727
|
// registration leaves it undefined — the guard below covers that path).
|
|
733
728
|
let onTermination;
|
|
@@ -855,7 +850,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
855
850
|
}
|
|
856
851
|
catch { /* cosmetic — visibility is never a gate */ }
|
|
857
852
|
}
|
|
858
|
-
supervision?.disarm(); // T16: same reason as the lock — this seat stood down, it did not die
|
|
859
853
|
releaseRunLock(repoRoot); // the process dies at exit() below — the finally never runs on this path
|
|
860
854
|
}
|
|
861
855
|
abortRun(new Error(`terminated by ${sig}`));
|
|
@@ -1145,6 +1139,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1145
1139
|
await park(t, `humanGate: "${t.title}" requires approval before dispatch`, "human-gate", null, 0, startMs);
|
|
1146
1140
|
return;
|
|
1147
1141
|
}
|
|
1142
|
+
// The driver owns how a checkout is created, but runDaemon owns the destructive transition:
|
|
1143
|
+
// every task-checkout recreation passes through this wrapper before any driver can remove the
|
|
1144
|
+
// old path. A preservation failure throws and therefore leaves the old checkout in place. The
|
|
1145
|
+
// row is deliberately written before the later worktree-recreation row so the journal cannot
|
|
1146
|
+
// describe only the commits it carried while omitting uncommitted work the removal destroyed.
|
|
1147
|
+
const recreateTaskWorktree = async (taskBranch, taskBase, priorWt) => {
|
|
1148
|
+
const ref = await preserveWorktree(priorWt);
|
|
1149
|
+
if (ref)
|
|
1150
|
+
journal.append("worktree-preserved", t.id, { ref });
|
|
1151
|
+
return driver.worktree(repoRoot, taskBranch, taskBase);
|
|
1152
|
+
};
|
|
1148
1153
|
const r = route(t, cfg, channels, profile, undefined, demotedChannels);
|
|
1149
1154
|
for (const lint of r.lints)
|
|
1150
1155
|
journal.append("routing-lint", t.id, { lint });
|
|
@@ -1457,7 +1462,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1457
1462
|
const priorTaskTip = await gitHead(priorWt);
|
|
1458
1463
|
const priorTaskSubject = await gateCommitSubject(taskBase, priorTaskTip, priorWt);
|
|
1459
1464
|
const commitsToCarry = await commitsAheadOf(taskBase, priorWt);
|
|
1460
|
-
const wt = await
|
|
1465
|
+
const wt = await recreateTaskWorktree(taskBranch, taskBase, priorWt);
|
|
1461
1466
|
const carriedCommits = await cherryPickCommits(wt, commitsToCarry);
|
|
1462
1467
|
// Reuse is about the tree the gates will actually inspect. The integration tip may have moved
|
|
1463
1468
|
// while the daemon was down, so compare after recreating the task on today's taskBase rather
|
|
@@ -1753,6 +1758,22 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1753
1758
|
const brief = journaledRows.join("\n\n");
|
|
1754
1759
|
feedback = feedback ? `${brief}\n\n${feedback}` : brief;
|
|
1755
1760
|
}
|
|
1761
|
+
// T6: both carries above are ATTEMPT-scoped — the funded repair is spent at the next
|
|
1762
|
+
// worker-launch (and budgeted at two), and the journaled brief is reset there too, so it hands
|
|
1763
|
+
// this dispatch only the LAST attempt's bytes. An unresolved review finding is a property of the
|
|
1764
|
+
// TASK: the moment one attempt fails for an unrelated reason — a red build, a refused tree, or a
|
|
1765
|
+
// death that journals no gate row at all — the finding is in neither carry and the next worker
|
|
1766
|
+
// re-derives the task from the spec and lands on the same gap the reviewer already anchored.
|
|
1767
|
+
// Re-derived from the journal on EVERY dispatch and retired only by a review that passes on this
|
|
1768
|
+
// task (journal.ts `outstandingReviewFindings`). Appended row-wise, because this round's own
|
|
1769
|
+
// feedback or a repair brief may already quote a finding and repeating it helps no worker.
|
|
1770
|
+
const outstandingFindings = outstandingReviewFindings(journaledSoFar, t.id);
|
|
1771
|
+
const unquoted = outstandingFindings.filter((f) => !feedback.includes(f.note));
|
|
1772
|
+
if (unquoted.length > 0) {
|
|
1773
|
+
const brief = ["## Outstanding review findings — a review has NOT passed on these yet",
|
|
1774
|
+
...unquoted.map((f) => `- ${f.path}: ${f.note}`)].join("\n");
|
|
1775
|
+
feedback = feedback ? `${feedback}\n\n${brief}` : brief;
|
|
1776
|
+
}
|
|
1756
1777
|
retryMode = repairFindings
|
|
1757
1778
|
? "repair"
|
|
1758
1779
|
: priorSession
|
|
@@ -1775,21 +1796,29 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1775
1796
|
lastContextTokens = undefined;
|
|
1776
1797
|
graph = setStatus(graph, t.id, "running");
|
|
1777
1798
|
saveGraph(repoRoot, graph);
|
|
1778
|
-
|
|
1799
|
+
// T6: a dispatch that carries an outstanding finding says so, and names it. Without this the
|
|
1800
|
+
// ledger cannot tell a carried dispatch from an amnesiac one — the exact question a run that
|
|
1801
|
+
// spends two frontier attempts re-deriving a known defect has to be able to answer afterwards.
|
|
1802
|
+
journal.append("task-dispatch", t.id, {
|
|
1803
|
+
assignment, attempt, provenance: dispatchProvenance(r.provenance), retryMode,
|
|
1804
|
+
...(outstandingFindings.length > 0 ? { carriedFindings: outstandingFindings } : {}),
|
|
1805
|
+
});
|
|
1779
1806
|
journal.phaseStart(t.id, "worker", { attempt, assignment });
|
|
1780
1807
|
const taskBase = await integrationHead(intWt); // deps are merged → visible to this task
|
|
1781
1808
|
const taskBranch = `${branch}--${t.id}`; // "--": a ref can't nest under the existing integration branch (locked decision 10)
|
|
1782
1809
|
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
1783
|
-
const
|
|
1784
|
-
const
|
|
1810
|
+
const recreating = existsSync(priorWt);
|
|
1811
|
+
const commitsToCarry = recreating ? await commitsAheadOf(taskBase, priorWt) : [];
|
|
1812
|
+
const wt = await recreateTaskWorktree(taskBranch, taskBase, priorWt);
|
|
1785
1813
|
// OBS-58: quota-failover and every retry recreate the task worktree from the integration tip —
|
|
1786
1814
|
// cherry-pick prior attempts' landed commits forward so a failover dispatch cannot silently
|
|
1787
1815
|
// orphan work a consult already verified as landed.
|
|
1788
1816
|
let carriedCommits = [];
|
|
1789
1817
|
if (commitsToCarry.length > 0) {
|
|
1790
1818
|
carriedCommits = await cherryPickCommits(wt, commitsToCarry);
|
|
1791
|
-
journal.append("worktree-recreation", t.id, { attempted: commitsToCarry, carried: carriedCommits });
|
|
1792
1819
|
}
|
|
1820
|
+
if (recreating)
|
|
1821
|
+
journal.append("worktree-recreation", t.id, { attempted: commitsToCarry, carried: carriedCommits });
|
|
1793
1822
|
// T2 review (material): harvest eligibility is "does this WORKTREE carry unverified work",
|
|
1794
1823
|
// measured against taskBase — the same base the fast-kill's delta probe and the gates
|
|
1795
1824
|
// themselves use. It was measured against this attempt's post-carry HEAD, which excluded
|
|
@@ -2892,6 +2921,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2892
2921
|
if (results.some((g) => g.gate === "test" && !g.pass))
|
|
2893
2922
|
testGateFailed = true;
|
|
2894
2923
|
if (results.every(gateSatisfied)) {
|
|
2924
|
+
// T6: every gate — the review included — is satisfied on this commit, so the failure brief
|
|
2925
|
+
// this loop is still holding describes nothing outstanding. It is dropped HERE, before the
|
|
2926
|
+
// merge, because a conflict below sends the task around the attempt loop again: a brief kept
|
|
2927
|
+
// across that retry hands the next worker findings a later review has since passed on, while
|
|
2928
|
+
// `carriedFindings` — re-derived from the journal, which retired them — is correctly empty,
|
|
2929
|
+
// leaving that dispatch's row indistinguishable from an amnesiac one. Rebuilt exactly as the
|
|
2930
|
+
// gate-fail brief is, so prior-RUN evidence (retired by its own rule, not by this reviewer)
|
|
2931
|
+
// survives and only this run's settled findings go.
|
|
2932
|
+
feedback = withCarriedEvidence("");
|
|
2895
2933
|
const m = await mergeSerial(taskBranch, t, gated);
|
|
2896
2934
|
if (m.tipMoved) {
|
|
2897
2935
|
journal.append("tip-moved", t.id, m.tipMoved);
|
|
@@ -3188,9 +3226,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3188
3226
|
process.removeListener("SIGINT", onTermination);
|
|
3189
3227
|
process.removeListener("SIGTERM", onTermination);
|
|
3190
3228
|
}
|
|
3191
|
-
// T16: every other exit — normal end, throw, termination unwind. disarm() is idempotent, so the
|
|
3192
|
-
// signal path having already stood the tier down changes nothing here.
|
|
3193
|
-
supervision?.disarm();
|
|
3194
3229
|
try {
|
|
3195
3230
|
releaseRunLock(repoRoot);
|
|
3196
3231
|
}
|
package/dist/run/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
2
3
|
export { ROUTING_ENV_SEAMS };
|
|
3
4
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
@@ -42,6 +43,10 @@ export interface ShResult {
|
|
|
42
43
|
timedOut?: boolean;
|
|
43
44
|
durationMs?: number;
|
|
44
45
|
}
|
|
46
|
+
export declare const setSpawnForTests: (fn: typeof spawn) => void;
|
|
47
|
+
export declare const resetSpawnForTests: () => void;
|
|
48
|
+
export declare const SPAWN_ATTEMPT_LIMIT = 4;
|
|
49
|
+
export declare const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
45
50
|
export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
46
51
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
47
52
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
@@ -58,6 +63,21 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
|
|
|
58
63
|
removeTaskIds: string[];
|
|
59
64
|
}): Promise<void>;
|
|
60
65
|
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Preserve the bytes an existing checkout holds before recreation removes it.
|
|
68
|
+
*
|
|
69
|
+
* `git stash create` cannot do this job: its apparent `-u` argument is accepted as a message and
|
|
70
|
+
* untracked files never enter the stash object. Build the snapshot through a disposable index
|
|
71
|
+
* instead. The index starts at HEAD (so no unrelated residue from the checkout's real index enters
|
|
72
|
+
* the tree), stages the complete working tree including ordinary untracked paths, and lives outside
|
|
73
|
+
* the repository so it cannot stage itself. None of these plumbing commands writes the checkout or
|
|
74
|
+
* its real index.
|
|
75
|
+
*
|
|
76
|
+
* The returned ref is the durable recovery handle. A clean checkout returns undefined and creates
|
|
77
|
+
* neither a commit nor a ref, keeping meaningful deaths visible rather than minting one ref per
|
|
78
|
+
* ordinary dispatch.
|
|
79
|
+
*/
|
|
80
|
+
export declare function preserveWorktree(cwd: string): Promise<string | undefined>;
|
|
61
81
|
export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
62
82
|
export declare function linkNodeModules(repo: string, dir: string, { force }?: {
|
|
63
83
|
force?: boolean | undefined;
|
package/dist/run/git.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { availableParallelism } from "node:os";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { availableParallelism, tmpdir } from "node:os";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
7
|
import { shq } from "../adapters/types.js";
|
|
7
8
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
8
9
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
@@ -64,6 +65,31 @@ export const runWithForkBudget = (concurrency, fn) => forkBudget.run(String(deri
|
|
|
64
65
|
export const resolvedForkCap = () => forkBudget.getStore() ?? DEFAULT_FORK_CAP;
|
|
65
66
|
/** The shipped shell ceiling: the fallback every caller gets when nothing measured a better one. */
|
|
66
67
|
export const DEFAULT_SHELL_TIMEOUT_MS = 600000;
|
|
68
|
+
/**
|
|
69
|
+
* OBS-688: every gate, every baseline capture and every tip verification reaches the machine through
|
|
70
|
+
* this one seam, and the seam took its spawn from the standard library directly — so the one failure
|
|
71
|
+
* it must handle, the kernel REFUSING the fork, was unreachable from a fixture and could only be
|
|
72
|
+
* described. It is injectable here for exactly that reason; production always holds `spawn` itself.
|
|
73
|
+
* Undefined-until-set, never captured at module load: the standard library binding stays read at
|
|
74
|
+
* CALL time, exactly as before this seam existed, so a suite that mocks `node:child_process` without
|
|
75
|
+
* a `spawn` export still imports this module (tests/adapters/pi-auth.test.ts does).
|
|
76
|
+
*/
|
|
77
|
+
let spawnChild;
|
|
78
|
+
export const setSpawnForTests = (fn) => { spawnChild = fn; };
|
|
79
|
+
export const resetSpawnForTests = () => { spawnChild = undefined; };
|
|
80
|
+
/**
|
|
81
|
+
* A refusal is NOT evidence about the work: the command never started, so nothing ran, and a retry
|
|
82
|
+
* cannot repeat a side effect. That argument is the whole safety case for retrying here, and it
|
|
83
|
+
* holds for exactly one closed case — the kernel refused the fork for a temporary resource shortage
|
|
84
|
+
* (EAGAIN: the fork table is full, which a gate burst does to a box twice in one night). Every
|
|
85
|
+
* other spawn error is a standing fact about the machine — a missing interpreter above all — and
|
|
86
|
+
* retrying it buys nothing while delaying every genuine failure by the whole backoff, so it returns
|
|
87
|
+
* on the first read. The retry is also gated on the child having produced NO byte and never having
|
|
88
|
+
* emitted `spawn`: past either, a command has run and re-running it is a side effect, never a retry.
|
|
89
|
+
*/
|
|
90
|
+
const RETRYABLE_SPAWN_CODE = "EAGAIN";
|
|
91
|
+
export const SPAWN_ATTEMPT_LIMIT = 4;
|
|
92
|
+
export const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
67
93
|
// stdin "ignore": same class as HARD-05 / SubprocessDriver — never leave an open pipe a child can block on
|
|
68
94
|
// (pi -p / codex exec wait for stdin EOF). timedOut distinguishes SIGKILL-timeout from a real nonzero exit.
|
|
69
95
|
function shell(cmd, cwd, timeoutMs, login) {
|
|
@@ -77,19 +103,23 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
77
103
|
// OBS-110: apply the run's own fork cap only when the operator has not already set one.
|
|
78
104
|
if (!(FORK_CAP_ENV in env))
|
|
79
105
|
env[FORK_CAP_ENV] = resolvedForkCap();
|
|
80
|
-
|
|
106
|
+
const attempt = () => new Promise((resolve) => {
|
|
81
107
|
const startedAt = Date.now();
|
|
82
108
|
// detached: bash gets its own process group so a timeout can kill the whole tree —
|
|
83
109
|
// SIGKILLing bash alone orphans grandchildren (codex/pi) that hold the stdio pipes
|
|
84
110
|
// open, so "close" never fires and the promise wedges forever (v1.33.1 init hang).
|
|
85
|
-
const p = spawn("bash", [login ? "-lc" : "-c", cmd], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
111
|
+
const p = (spawnChild ?? spawn)("bash", [login ? "-lc" : "-c", cmd], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
86
112
|
let stdout = "", stderr = "";
|
|
87
|
-
|
|
113
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
114
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
115
|
+
let timedOut = false, done = false, started = false, outputSeen = false;
|
|
88
116
|
const finish = (code, err) => {
|
|
89
117
|
if (done)
|
|
90
118
|
return;
|
|
91
119
|
done = true;
|
|
92
120
|
clearTimeout(timer);
|
|
121
|
+
stdout += stdoutDecoder.end();
|
|
122
|
+
stderr += stderrDecoder.end();
|
|
93
123
|
resolve({ code, stdout, stderr: err ?? stderr, timedOut, durationMs: Date.now() - startedAt });
|
|
94
124
|
};
|
|
95
125
|
const timer = setTimeout(() => {
|
|
@@ -101,14 +131,50 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
101
131
|
p.kill("SIGKILL");
|
|
102
132
|
}
|
|
103
133
|
}, timeoutMs);
|
|
104
|
-
p.
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
p.on("spawn", () => { started = true; }); // the command exists from here on — never retryable past it
|
|
135
|
+
// OBS-716: one stateful decoder per stream carries an incomplete UTF-8 sequence into that
|
|
136
|
+
// stream's next pipe chunk; decoding each chunk through string concatenation corrupts bytes at
|
|
137
|
+
// kernel-chosen boundaries. A deterministic fixture proves this decoder correct rather than
|
|
138
|
+
// proving every caller byte-safe: real chunk boundaries are the kernel's to choose, so timing
|
|
139
|
+
// still decides whether a chunk-local decoder exposes the defect in any particular run.
|
|
140
|
+
p.stdout.on("data", (d) => {
|
|
141
|
+
if (d.length > 0)
|
|
142
|
+
outputSeen = true;
|
|
143
|
+
stdout += stdoutDecoder.write(d);
|
|
144
|
+
});
|
|
145
|
+
p.stderr.on("data", (d) => {
|
|
146
|
+
if (d.length > 0)
|
|
147
|
+
outputSeen = true;
|
|
148
|
+
stderr += stderrDecoder.write(d);
|
|
149
|
+
});
|
|
150
|
+
p.on("error", (e) => {
|
|
151
|
+
if (!done && !started && !outputSeen && e.code === RETRYABLE_SPAWN_CODE) {
|
|
152
|
+
done = true;
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
resolve({ refused: e });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
finish(127, String(e));
|
|
158
|
+
});
|
|
107
159
|
p.on("close", (code) => finish(code ?? 1));
|
|
108
160
|
// "close" waits for stdio to drain; a surviving pipe-holder must not outlive the timeout
|
|
109
161
|
p.on("exit", (code) => { if (timedOut)
|
|
110
162
|
finish(code ?? 1); });
|
|
111
163
|
});
|
|
164
|
+
return (async () => {
|
|
165
|
+
const startedAt = Date.now();
|
|
166
|
+
for (let n = 1;; n++) {
|
|
167
|
+
const r = await attempt();
|
|
168
|
+
if (!("refused" in r))
|
|
169
|
+
return r;
|
|
170
|
+
// Bounded, and the bound is what makes a persisting shortage a REPORTED failure rather than a
|
|
171
|
+
// wedged daemon: past it the caller gets the refusal's own text under exit 127, as before.
|
|
172
|
+
if (n >= SPAWN_ATTEMPT_LIMIT) {
|
|
173
|
+
return { code: 127, stdout: "", stderr: String(r.refused), durationMs: Date.now() - startedAt };
|
|
174
|
+
}
|
|
175
|
+
await new Promise((wake) => setTimeout(wake, SPAWN_RETRY_BACKOFF_MS * n));
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
112
178
|
}
|
|
113
179
|
export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
|
114
180
|
return shell(cmd, cwd, timeoutMs, true);
|
|
@@ -212,6 +278,46 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
212
278
|
const integration = await resolveIntegrationBranch(repo, branch.slice(0, split));
|
|
213
279
|
return `${integration}${branch.slice(split)}`;
|
|
214
280
|
};
|
|
281
|
+
/**
|
|
282
|
+
* Preserve the bytes an existing checkout holds before recreation removes it.
|
|
283
|
+
*
|
|
284
|
+
* `git stash create` cannot do this job: its apparent `-u` argument is accepted as a message and
|
|
285
|
+
* untracked files never enter the stash object. Build the snapshot through a disposable index
|
|
286
|
+
* instead. The index starts at HEAD (so no unrelated residue from the checkout's real index enters
|
|
287
|
+
* the tree), stages the complete working tree including ordinary untracked paths, and lives outside
|
|
288
|
+
* the repository so it cannot stage itself. None of these plumbing commands writes the checkout or
|
|
289
|
+
* its real index.
|
|
290
|
+
*
|
|
291
|
+
* The returned ref is the durable recovery handle. A clean checkout returns undefined and creates
|
|
292
|
+
* neither a commit nor a ref, keeping meaningful deaths visible rather than minting one ref per
|
|
293
|
+
* ordinary dispatch.
|
|
294
|
+
*/
|
|
295
|
+
export async function preserveWorktree(cwd) {
|
|
296
|
+
if (!existsSync(cwd))
|
|
297
|
+
return undefined;
|
|
298
|
+
const scratch = mkdtempSync(join(tmpdir(), "tickmarkr-preserve-index-"));
|
|
299
|
+
const index = join(scratch, "index");
|
|
300
|
+
const withIndex = (command) => `GIT_INDEX_FILE=${shq(index)} ${command}`;
|
|
301
|
+
try {
|
|
302
|
+
await shGitOk(withIndex("git read-tree HEAD"), cwd);
|
|
303
|
+
await shGitOk(withIndex("git add -A -- ."), cwd);
|
|
304
|
+
const tree = (await shGitOk(withIndex("git write-tree"), cwd)).trim();
|
|
305
|
+
const headTree = (await shGitOk("git rev-parse 'HEAD^{tree}'", cwd)).trim();
|
|
306
|
+
if (tree === headTree)
|
|
307
|
+
return undefined;
|
|
308
|
+
// Do not depend on consumer-level identity configuration: this is an engine recovery object,
|
|
309
|
+
// not an authored project commit. Its HEAD parent makes the preserved tree directly inspectable.
|
|
310
|
+
const identity = "GIT_AUTHOR_NAME=tickmarkr GIT_AUTHOR_EMAIL=tickmarkr@localhost "
|
|
311
|
+
+ "GIT_COMMITTER_NAME=tickmarkr GIT_COMMITTER_EMAIL=tickmarkr@localhost";
|
|
312
|
+
const commit = (await shGitOk(`${identity} git commit-tree ${shq(tree)} -p HEAD -m ${shq("tickmarkr: preserve uncommitted worktree")}`, cwd)).trim();
|
|
313
|
+
const ref = `refs/tickmarkr/preserved/${commit}`;
|
|
314
|
+
await shGitOk(`git update-ref ${shq(ref)} ${shq(commit)}`, cwd);
|
|
315
|
+
return ref;
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
215
321
|
export async function createWorktree(repo, branch, baseRef) {
|
|
216
322
|
branch = await resolveTaskBranch(repo, branch);
|
|
217
323
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -104,6 +104,30 @@ export declare function repairsSinceApproval(events: JournalEvent[], taskId: str
|
|
|
104
104
|
* retires the findings it settled — the uphold case re-derives its own brief separately).
|
|
105
105
|
*/
|
|
106
106
|
export declare function journaledFailureBrief(events: JournalEvent[], taskId: string): string[];
|
|
107
|
+
/**
|
|
108
|
+
* T6: the review findings still OUTSTANDING on a task. A review finding is a property of the TASK,
|
|
109
|
+
* not of the attempt that drew it: it stays outstanding until a later review PASSES on the task (or
|
|
110
|
+
* an operator approval settles it), and it therefore travels on EVERY dispatch until then.
|
|
111
|
+
*
|
|
112
|
+
* The two carries beside this one are attempt-scoped by construction and both lose it. The funded
|
|
113
|
+
* repair (`pendingRepairFindings`) is spent at the next `worker-launch` and is budgeted at two per
|
|
114
|
+
* engagement; `journaledFailureBrief` is reset at that same launch, so it hands the next brief only
|
|
115
|
+
* the LAST attempt's bytes. The moment one attempt fails for an unrelated reason — a red build, a
|
|
116
|
+
* refused tree, or a death that produces no verdict at all and journals no gate row whatsoever — the
|
|
117
|
+
* outstanding finding is in neither carry, and the run re-derives the task from the spec and lands on
|
|
118
|
+
* the same gap the reviewer already anchored.
|
|
119
|
+
*
|
|
120
|
+
* Retirement is closed and narrow: a review that PASSED, or the one approval that accepts the review
|
|
121
|
+
* gate itself (`GATE_SATISFIED_RELEASE` stamped `gate: "review"` — the operator taking the diff the
|
|
122
|
+
* reviewer rejected). Every other approval RETAINS. `--uphold` funds an attempt to FIX the findings;
|
|
123
|
+
* `--recheck` and an attempt-cap release fund another dispatch and say nothing about the reviewer's
|
|
124
|
+
* objection; a plain human-gate approval predates any review; a gate-satisfied release naming some
|
|
125
|
+
* other gate settled that gate, not this one. Reading "approved" as "settled" is how a still-open
|
|
126
|
+
* finding was dropped at the exact moment the operator paid for another attempt to fix it. A review
|
|
127
|
+
* that DECLINED (`skipped`) is not a verdict and neither adds nor retires — fail closed. Findings are
|
|
128
|
+
* keyed by fingerprint, so a reviewer restating one across rounds carries it once, not once per round.
|
|
129
|
+
*/
|
|
130
|
+
export declare function outstandingReviewFindings(events: JournalEvent[], taskId: string): StructuredFinding[];
|
|
107
131
|
/** The findings a funded repair must carry into the next dispatch, or undefined if none is pending. */
|
|
108
132
|
export declare function pendingRepairFindings(events: JournalEvent[], taskId: string): string | undefined;
|
|
109
133
|
/**
|
package/dist/run/journal.js
CHANGED
|
@@ -87,6 +87,13 @@ export function upheldFeedbackByTask(events) {
|
|
|
87
87
|
&& typeof e.data.details === "string") {
|
|
88
88
|
lastReviewFail.set(e.taskId, e.data.details);
|
|
89
89
|
}
|
|
90
|
+
else if (e.event === "gate-result" && e.data.gate === "review" && e.data.pass === true
|
|
91
|
+
&& e.data.skipped !== true) {
|
|
92
|
+
// A later review pass settles the upheld finding. Retire both the active brief and the failed
|
|
93
|
+
// verdict it came from so a still-later approval cannot resurrect already-settled feedback.
|
|
94
|
+
upheld.delete(e.taskId);
|
|
95
|
+
lastReviewFail.delete(e.taskId);
|
|
96
|
+
}
|
|
90
97
|
else if (e.event === "task-approved") {
|
|
91
98
|
// any later approval supersedes: a plain accept-the-diff approval retires the uphold brief.
|
|
92
99
|
if (e.data.release === REVIEW_UPHELD_RELEASE) {
|
|
@@ -480,6 +487,49 @@ export function journaledFailureBrief(events, taskId) {
|
|
|
480
487
|
}
|
|
481
488
|
return rows;
|
|
482
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* T6: the review findings still OUTSTANDING on a task. A review finding is a property of the TASK,
|
|
492
|
+
* not of the attempt that drew it: it stays outstanding until a later review PASSES on the task (or
|
|
493
|
+
* an operator approval settles it), and it therefore travels on EVERY dispatch until then.
|
|
494
|
+
*
|
|
495
|
+
* The two carries beside this one are attempt-scoped by construction and both lose it. The funded
|
|
496
|
+
* repair (`pendingRepairFindings`) is spent at the next `worker-launch` and is budgeted at two per
|
|
497
|
+
* engagement; `journaledFailureBrief` is reset at that same launch, so it hands the next brief only
|
|
498
|
+
* the LAST attempt's bytes. The moment one attempt fails for an unrelated reason — a red build, a
|
|
499
|
+
* refused tree, or a death that produces no verdict at all and journals no gate row whatsoever — the
|
|
500
|
+
* outstanding finding is in neither carry, and the run re-derives the task from the spec and lands on
|
|
501
|
+
* the same gap the reviewer already anchored.
|
|
502
|
+
*
|
|
503
|
+
* Retirement is closed and narrow: a review that PASSED, or the one approval that accepts the review
|
|
504
|
+
* gate itself (`GATE_SATISFIED_RELEASE` stamped `gate: "review"` — the operator taking the diff the
|
|
505
|
+
* reviewer rejected). Every other approval RETAINS. `--uphold` funds an attempt to FIX the findings;
|
|
506
|
+
* `--recheck` and an attempt-cap release fund another dispatch and say nothing about the reviewer's
|
|
507
|
+
* objection; a plain human-gate approval predates any review; a gate-satisfied release naming some
|
|
508
|
+
* other gate settled that gate, not this one. Reading "approved" as "settled" is how a still-open
|
|
509
|
+
* finding was dropped at the exact moment the operator paid for another attempt to fix it. A review
|
|
510
|
+
* that DECLINED (`skipped`) is not a verdict and neither adds nor retires — fail closed. Findings are
|
|
511
|
+
* keyed by fingerprint, so a reviewer restating one across rounds carries it once, not once per round.
|
|
512
|
+
*/
|
|
513
|
+
export function outstandingReviewFindings(events, taskId) {
|
|
514
|
+
const open = new Map();
|
|
515
|
+
for (const e of events) {
|
|
516
|
+
if (e.taskId !== taskId)
|
|
517
|
+
continue;
|
|
518
|
+
if (e.event === "task-approved") {
|
|
519
|
+
if (e.data.release === GATE_SATISFIED_RELEASE && e.data.gate === "review")
|
|
520
|
+
open.clear();
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (e.event !== "gate-result" || e.data.gate !== "review" || e.data.skipped === true)
|
|
524
|
+
continue;
|
|
525
|
+
if (e.data.pass !== false)
|
|
526
|
+
open.clear(); // a later review PASSED on this task: nothing is outstanding
|
|
527
|
+
else
|
|
528
|
+
for (const finding of findingRows(e, "review"))
|
|
529
|
+
open.set(finding.fingerprint, finding);
|
|
530
|
+
}
|
|
531
|
+
return [...open.values()];
|
|
532
|
+
}
|
|
483
533
|
/** The findings a funded repair must carry into the next dispatch, or undefined if none is pending. */
|
|
484
534
|
export function pendingRepairFindings(events, taskId) {
|
|
485
535
|
const e = decisionForNextDispatch(events, taskId, "repair-attempt");
|
|
@@ -1055,12 +1105,16 @@ export class Journal {
|
|
|
1055
1105
|
}
|
|
1056
1106
|
replayResumeState() {
|
|
1057
1107
|
const m = new Map();
|
|
1108
|
+
const events = this.read();
|
|
1109
|
+
// Keep the legacy resume-state field aligned with the journal-authoritative prompt-time fold.
|
|
1110
|
+
// In particular, a review pass after an uphold must erase the fallback daemon.ts may consult.
|
|
1111
|
+
const activeUpheldFeedback = upheldFeedbackByTask(events);
|
|
1058
1112
|
const pendingReroute = new Set(); // reroute verdicts not yet cleared by a later dispatch
|
|
1059
1113
|
// OBS-547: what the last dispatch ADDED, so a scope-authoring event can take it back. An
|
|
1060
1114
|
// unchargeable dispatch must replay as if it never happened — no attempt counted, no channel burned.
|
|
1061
1115
|
const lastDispatch = new Map();
|
|
1062
1116
|
const lastReviewFail = new Map(); // OBS-189: newest failed review details per task
|
|
1063
|
-
for (const e of
|
|
1117
|
+
for (const e of events) {
|
|
1064
1118
|
if (!e.taskId)
|
|
1065
1119
|
continue;
|
|
1066
1120
|
if (e.event === "gate-result" && e.data.gate === "review" && e.data.pass === false
|
|
@@ -1160,6 +1214,13 @@ export class Journal {
|
|
|
1160
1214
|
st.lastAssignment = undefined;
|
|
1161
1215
|
}
|
|
1162
1216
|
}
|
|
1217
|
+
for (const [taskId, st] of m) {
|
|
1218
|
+
const feedback = activeUpheldFeedback.get(taskId);
|
|
1219
|
+
if (feedback)
|
|
1220
|
+
st.upheldFeedback = feedback;
|
|
1221
|
+
else
|
|
1222
|
+
delete st.upheldFeedback;
|
|
1223
|
+
}
|
|
1163
1224
|
return m;
|
|
1164
1225
|
}
|
|
1165
1226
|
// OBS-130: gate satisfaction is authority, not an inferred daemon state. Only an explicit
|
package/dist/run/lock.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface Inspection {
|
|
|
10
10
|
ino: number;
|
|
11
11
|
}
|
|
12
12
|
export declare function shouldRefuse(i: Pick<Inspection, "garbage" | "dead">): boolean;
|
|
13
|
+
export declare function isPidLive(pid: number): boolean;
|
|
13
14
|
export declare function acquireRunLock(repoRoot: string, runId: string): {
|
|
14
15
|
reclaimed?: {
|
|
15
16
|
pid: number;
|
package/dist/run/lock.js
CHANGED
|
@@ -45,6 +45,23 @@ process.once("exit", () => { if (heldPath)
|
|
|
45
45
|
export function shouldRefuse(i) {
|
|
46
46
|
return i.garbage || !i.dead;
|
|
47
47
|
}
|
|
48
|
+
// LOCK-04, PID-SCOPED: the same decision table, in the shape a caller holding only a pid can consume.
|
|
49
|
+
// Every other liveness export here takes a repository root, so a reader with a pid off a journal row
|
|
50
|
+
// had no seam to reach and wrote the four lines itself — twice. One copy was faithful; the other
|
|
51
|
+
// treated ANY thrown error as death, so a daemon owned by another user (EPERM) read dead there and
|
|
52
|
+
// alive here. A rule that forbids a second `process.kill(pid, 0)` without exporting a usable
|
|
53
|
+
// predicate produces exactly those copies, so this is the predicate. no throw ⇒ ALIVE; ESRCH ⇒ the
|
|
54
|
+
// only proof-positive death; ANY other errno (EPERM = alive-but-not-ours, EINVAL, …) ⇒ ALIVE,
|
|
55
|
+
// because none of them is evidence of death and this table fails closed toward alive.
|
|
56
|
+
export function isPidLive(pid) {
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid, 0);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch (k) {
|
|
62
|
+
return k.code !== "ESRCH";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
48
65
|
// statSync throws ENOENT when no lock exists — callers treat that as "not held".
|
|
49
66
|
function inspect(p) {
|
|
50
67
|
const st = statSync(p); // single stat: both the heartbeat mtime and the reclaim-guard inode
|
|
@@ -53,16 +70,8 @@ function inspect(p) {
|
|
|
53
70
|
const parsed = PayloadSchema.safeParse(readPayload(p));
|
|
54
71
|
const garbage = !parsed.success; // LOCK-01: its own state — shouldRefuse refuses it unconditionally; only `tickmarkr unlock` removes it
|
|
55
72
|
const pid = parsed.success ? parsed.data.pid : undefined;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
process.kill(pid, 0);
|
|
60
|
-
dead = false;
|
|
61
|
-
} // no throw ⇒ ALIVE
|
|
62
|
-
catch (k) {
|
|
63
|
-
dead = k.code === "ESRCH";
|
|
64
|
-
} // ESRCH ⇒ dead; EPERM ⇒ ALIVE
|
|
65
|
-
}
|
|
73
|
+
// harmless fallback for the garbage row — garbage short-circuits shouldRefuse before this is read
|
|
74
|
+
const dead = pid === undefined ? true : !isPidLive(pid);
|
|
66
75
|
return { pid, runId: parsed.success ? parsed.data.runId : undefined, garbage, dead, expired, mtimeMs, ino: st.ino };
|
|
67
76
|
}
|
|
68
77
|
function readPayload(p) {
|
|
@@ -222,9 +231,10 @@ export async function acquireApprovalSerialization(repoRoot, runId) {
|
|
|
222
231
|
}
|
|
223
232
|
}
|
|
224
233
|
// LOCK-04: the owner the decision table sees, read-only, for callers that need the pid as well as
|
|
225
|
-
// the answer. inspect() owns pid-liveness (ESRCH dead / EPERM alive / garbage fail-closed)
|
|
226
|
-
// `process.kill(pid, 0)` anywhere else would be a second copy of
|
|
227
|
-
//
|
|
234
|
+
// the answer. inspect() owns pid-liveness (ESRCH dead / EPERM alive / garbage fail-closed) and reads
|
|
235
|
+
// it from isPidLive above; a second `process.kill(pid, 0)` anywhere else would be a second copy of
|
|
236
|
+
// that rule, free to drift — a caller holding only a pid consumes isPidLive, never its own probe.
|
|
237
|
+
// undefined ⇒ no lock at all — never conflate that with a lock whose recorded owner is dead.
|
|
228
238
|
export function runLockOwner(repoRoot) {
|
|
229
239
|
let insp;
|
|
230
240
|
try {
|
|
@@ -3,24 +3,30 @@ export declare const SUPERVISION_BEAT_MS = 10000;
|
|
|
3
3
|
/** Ceiling before a beat reads STALE: SIX beats, lock.ts's ratio — five may be missed before alarm. */
|
|
4
4
|
export declare const SUPERVISION_STALE_MS: number;
|
|
5
5
|
export declare const SUPERVISION_FUTURE_GRACE_MS = 1000;
|
|
6
|
-
export declare const SUPERVISION_TIERS: readonly ["orchestrator", "overseer", "watch"];
|
|
6
|
+
export declare const SUPERVISION_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch"];
|
|
7
7
|
export type SupervisionTier = (typeof SUPERVISION_TIERS)[number];
|
|
8
|
+
export declare const SUPERVISION_SEAT_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context"];
|
|
9
|
+
export type SeatTier = (typeof SUPERVISION_SEAT_TIERS)[number];
|
|
10
|
+
/** Does this tier's record have to name the seat it speaks for? */
|
|
11
|
+
export declare const isSeatTier: (tier: string) => tier is SeatTier;
|
|
8
12
|
export type SupervisionState = "ABSENT" | "STALE" | "ARMED" | "UNREADABLE" | "DISARMED";
|
|
9
13
|
export interface TierLiveness {
|
|
10
14
|
tier: SupervisionTier;
|
|
11
15
|
state: SupervisionState;
|
|
12
16
|
/** Absent for ABSENT, UNREADABLE and DISARMED — no beat to age. Always present for STALE and ARMED. */
|
|
13
17
|
beatAgeMs?: number;
|
|
18
|
+
/** The seat the record names, when it names one. Always present on a seat tier that is not ABSENT. */
|
|
19
|
+
seat?: string;
|
|
14
20
|
}
|
|
15
21
|
export declare const supervisionBeatPath: (repoRoot: string, tier: SupervisionTier) => string;
|
|
16
22
|
/** Where a watcher records that it STOOD DOWN. Its own file: the beat keeps meaning only "alive". */
|
|
17
23
|
export declare const supervisionStandDownPath: (repoRoot: string, tier: SupervisionTier) => string;
|
|
18
|
-
export declare function beatSupervision(repoRoot: string, tier: SupervisionTier): void;
|
|
24
|
+
export declare function beatSupervision(repoRoot: string, tier: SupervisionTier, seat?: string): void;
|
|
19
25
|
/** Handle a watcher holds for as long as it is supervising; disarm stands it down and is idempotent. */
|
|
20
26
|
export interface ArmedSupervision {
|
|
21
27
|
disarm: () => void;
|
|
22
28
|
}
|
|
23
|
-
export declare function armSupervision(repoRoot: string, tier: SupervisionTier, beatMs?: number): ArmedSupervision;
|
|
29
|
+
export declare function armSupervision(repoRoot: string, tier: SupervisionTier, beatMs?: number, seat?: string): ArmedSupervision;
|
|
24
30
|
export declare function readTierLiveness(repoRoot: string, tier: SupervisionTier, now?: number): TierLiveness;
|
|
25
31
|
export declare function supervisionStatus(repoRoot: string, tier: SupervisionTier, now?: number): TierLiveness;
|
|
26
32
|
/** Every KNOWN tier, always — a tier omitted from this list would read as one that is fine. */
|