tickmarkr 2.5.7 → 2.5.8
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/cli/commands/approve.js +8 -1
- package/dist/cli/commands/compile.js +7 -0
- package/dist/cli/commands/plan.d.ts +5 -0
- package/dist/cli/commands/plan.js +28 -23
- package/dist/cli/commands/resume.js +1 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/compile/native.js +68 -7
- package/dist/compile/retired-literals.d.ts +22 -0
- package/dist/compile/retired-literals.js +271 -0
- package/dist/drivers/index.d.ts +4 -2
- package/dist/drivers/index.js +54 -6
- package/dist/gates/run-gates.d.ts +2 -0
- package/dist/gates/run-gates.js +4 -2
- package/dist/gates/test-manifest.d.ts +9 -0
- package/dist/gates/test-manifest.js +9 -1
- package/dist/gates/test-reporter.js +22 -1
- package/dist/graph/schema.d.ts +28 -0
- package/dist/graph/schema.js +13 -1
- package/dist/run/daemon.d.ts +19 -0
- package/dist/run/daemon.js +280 -78
- package/dist/run/journal.d.ts +37 -1
- package/dist/run/journal.js +43 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +1 -0
- package/dist/run/repair-disposition.d.ts +41 -0
- package/dist/run/repair-disposition.js +77 -0
- package/dist/tui/cockpit/home-view.js +45 -30
- package/dist/tui/cockpit/live-store.d.ts +18 -0
- package/package.json +1 -1
- package/schema/rungraph.schema.json +54 -0
- package/skills/tickmarkr-overseer/SKILL.md +107 -0
package/dist/run/daemon.js
CHANGED
|
@@ -31,7 +31,8 @@ import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
|
31
31
|
import { runEnvironment } from "./environment.js";
|
|
32
32
|
import { cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
33
33
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
34
|
-
import {
|
|
34
|
+
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
35
|
+
import { applyScopeAmendments, activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
35
36
|
import { isDiffCapPark, pickReviewer } from "../gates/review.js";
|
|
36
37
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
37
38
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
@@ -175,6 +176,17 @@ const RECHECK_ENACTMENT = "recheck-battery";
|
|
|
175
176
|
// persisted task branch after the approved gate (execTask's satisfiedGate branch), whose first act
|
|
176
177
|
// for the task is worktree-recreation. That event is causal for this path, not incidental.
|
|
177
178
|
const GATE_SATISFIED_ENACTMENT = "worktree-recreation";
|
|
179
|
+
// Older daemons enacted rechecks through worker-launch, before recheck-battery existed.
|
|
180
|
+
// Preserve that consumption at every scheduling read without changing continuing permission.
|
|
181
|
+
function pendingDaemonApprovalActions(events) {
|
|
182
|
+
const actions = pendingApprovalActions(events);
|
|
183
|
+
const rechecks = pendingRechecks(events);
|
|
184
|
+
for (const [id, action] of actions) {
|
|
185
|
+
if (action.authority === "battery" && !rechecks.has(id))
|
|
186
|
+
actions.delete(id);
|
|
187
|
+
}
|
|
188
|
+
return actions;
|
|
189
|
+
}
|
|
178
190
|
/**
|
|
179
191
|
* T14, amended by v2.2 T3: approvals the run accepted and never acted on. `approved` above is still
|
|
180
192
|
* built ONCE at startup — replay determinism depends on it — but a live approval is no longer inert:
|
|
@@ -357,6 +369,14 @@ function repairDiffFilesCut(diff, cap, names) {
|
|
|
357
369
|
});
|
|
358
370
|
return files;
|
|
359
371
|
}
|
|
372
|
+
/** Assertion evidence is context, never part of the normalized failure identity. */
|
|
373
|
+
function repairFindingsBrief(results) {
|
|
374
|
+
return results.filter(gateFailed).map((g) => {
|
|
375
|
+
const evidence = g.meta?.failureEvidence;
|
|
376
|
+
return `${g.gate}: ${g.details}` + (Array.isArray(evidence) && evidence.length
|
|
377
|
+
? `\nAssertion evidence:\n${JSON.stringify(evidence, null, 2)}` : "");
|
|
378
|
+
}).join("\n\n");
|
|
379
|
+
}
|
|
360
380
|
/** The fix-only contract: the findings verbatim, then the diff content of the work already landed. */
|
|
361
381
|
function repairBrief(findings, diff, baseRef) {
|
|
362
382
|
const repair = { findings, diff };
|
|
@@ -688,6 +708,47 @@ function lastVerifyCycle(events) {
|
|
|
688
708
|
}
|
|
689
709
|
return cur;
|
|
690
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* OBS-1077 close rider: what the engagement's LATEST verification cycle proved — its
|
|
713
|
+
* `tip-verify-start` row and what followed, never a commit comparison. Exactly one kind per close:
|
|
714
|
+
* fresh (every tip command ran AND passed), reused (an eligible cached cycle carried forward),
|
|
715
|
+
* failed, or incomplete (cancelled, cut short, mixed or undelimited). Nothing before the start row
|
|
716
|
+
* is read, so an unfinished cycle inherits nothing from an earlier green one.
|
|
717
|
+
*/
|
|
718
|
+
export function runEndTipProof(events) {
|
|
719
|
+
const start = events.map((e) => e.event).lastIndexOf("tip-verify-start");
|
|
720
|
+
if (start < 0)
|
|
721
|
+
return { kind: "incomplete" };
|
|
722
|
+
const { tip: rawTip, gates: rawGates } = events[start].data;
|
|
723
|
+
const tip = typeof rawTip === "string" ? rawTip : undefined;
|
|
724
|
+
const proof = (kind) => ({ kind, ...(tip ? { tip } : {}) });
|
|
725
|
+
const after = events.slice(start + 1);
|
|
726
|
+
if (after.some((e) => e.event === "tip-verify-failed"))
|
|
727
|
+
return proof("failed");
|
|
728
|
+
if (after.some((e) => e.event === "tip-verify-cancelled"))
|
|
729
|
+
return proof("incomplete");
|
|
730
|
+
const rows = after.filter((e) => e.event === "tip-verify" && e.data.pass === true && e.data.tip === tip);
|
|
731
|
+
const gates = Array.isArray(rawGates) ? rawGates : [];
|
|
732
|
+
if (!tip || gates.length === 0 || !gates.every((g) => rows.some((r) => r.data.gate === g)))
|
|
733
|
+
return proof("incomplete");
|
|
734
|
+
const carried = rows.filter((r) => r.data.cached === true).length;
|
|
735
|
+
const cachedRow = after.some((e) => e.event === "tip-verify-cached");
|
|
736
|
+
// A whole-cycle carry must be declared by the start row, stated by the cache row and borne by every verdict row.
|
|
737
|
+
if ((events[start].data.cached === true) !== cachedRow || (cachedRow && carried !== rows.length))
|
|
738
|
+
return proof("incomplete");
|
|
739
|
+
// D-131: fresh only when EVERY command executed; one per-gate persisted verdict makes the cycle reused.
|
|
740
|
+
return carried > 0 ? proof("reused") : proof("fresh");
|
|
741
|
+
}
|
|
742
|
+
/** The close notification's statement of the proof — one clause per kind. */
|
|
743
|
+
export function formatTipProof(p) {
|
|
744
|
+
const at = p.tip ? ` ${p.tip.slice(0, 12)}` : "";
|
|
745
|
+
switch (p.kind) {
|
|
746
|
+
case "fresh": return `tip proof: fresh — every tip command ran and passed on${at || " the integration tip"}`;
|
|
747
|
+
case "reused": return `tip proof: reused — carried from verified commit${at}, commands not re-run`;
|
|
748
|
+
case "failed": return `tip proof: failed —${at ? ` commit${at}` : ""} latest verification cycle is red`;
|
|
749
|
+
case "incomplete": return `tip proof: incomplete —${at ? ` commit${at}` : ""} latest verification cycle did not finish`;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
691
752
|
// Isolate the battery so cancellation can stop its control flow as well as its shell children.
|
|
692
753
|
// The child uses the same verifier and capacity; only the daemon writes lifecycle verdict rows.
|
|
693
754
|
async function cancellableTipBattery(intWt, commands, runDir, baseline, signal) {
|
|
@@ -859,7 +920,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
859
920
|
const measuredCapacity = r.capacity ?? capacity;
|
|
860
921
|
if (r.pass) {
|
|
861
922
|
// Q121s: a forgiven pass journals its fingerprints — honest about what was carried, never a silent green.
|
|
862
|
-
journal.append("tip-verify", undefined, { gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details, ...(r.forgiven ? { forgiven: true, fingerprints: r.fingerprints } : {}), tip, cmdHash, capacity: measuredCapacity });
|
|
923
|
+
journal.append("tip-verify", undefined, { gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details, ...(r.reused ? { cached: true } : {}), ...(r.forgiven ? { forgiven: true, fingerprints: r.fingerprints } : {}), tip, cmdHash, capacity: measuredCapacity });
|
|
863
924
|
}
|
|
864
925
|
else {
|
|
865
926
|
journal.append("tip-verify-failed", undefined, {
|
|
@@ -1411,11 +1472,23 @@ const terminateTornJournalTail = (journalPath) => {
|
|
|
1411
1472
|
// a persistently unwritable sink reports a crash naming the journal path and carrying NO terminal
|
|
1412
1473
|
// record, rather than fabricating an ended run on evidence the harness could not write. (OBS-313:
|
|
1413
1474
|
// with the sink dead the crash CAUSE is unrecordable — recorded as an observation, not papered over.)
|
|
1414
|
-
function recordFatalRunEnd(journal, runId, branch, err) {
|
|
1475
|
+
export function recordFatalRunEnd(journal, runId, branch, err, graph, phase = "setup") {
|
|
1415
1476
|
const original = err instanceof Error ? err.message : String(err);
|
|
1477
|
+
// A fatal close never finished a cycle: fresh/reused green is withheld, a recorded red still stands.
|
|
1478
|
+
let tipProof = { kind: "incomplete" };
|
|
1416
1479
|
try {
|
|
1417
|
-
|
|
1418
|
-
|
|
1480
|
+
// Only the latest engagement can already own this terminal outcome.
|
|
1481
|
+
const events = journal.read();
|
|
1482
|
+
let from = events.length;
|
|
1483
|
+
while (from > 0) {
|
|
1484
|
+
const event = events[--from];
|
|
1485
|
+
if (event.event === "run-start" || event.event === "run-resume")
|
|
1486
|
+
break;
|
|
1487
|
+
if (event.event === "run-end")
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1490
|
+
const latest = runEndTipProof(events.slice(from));
|
|
1491
|
+
tipProof = { ...latest, kind: latest.kind === "failed" ? "failed" : "incomplete" };
|
|
1419
1492
|
}
|
|
1420
1493
|
catch (readErr) {
|
|
1421
1494
|
console.error(`tickmarkr ${runId}: journal read failed while recording the fatal run-end (${readErr instanceof Error ? readErr.message : String(readErr)}) — original error: ${original}`);
|
|
@@ -1423,18 +1496,19 @@ function recordFatalRunEnd(journal, runId, branch, err) {
|
|
|
1423
1496
|
const record = {
|
|
1424
1497
|
runId,
|
|
1425
1498
|
branch,
|
|
1426
|
-
done: [],
|
|
1427
|
-
failed: [],
|
|
1428
|
-
human: [],
|
|
1429
|
-
blocked: [],
|
|
1430
|
-
pending: [],
|
|
1431
|
-
phase
|
|
1499
|
+
done: graph?.tasks.filter((t) => t.status === "done").map((t) => t.id) ?? [],
|
|
1500
|
+
failed: graph?.tasks.filter((t) => t.status === "failed").map((t) => t.id) ?? [],
|
|
1501
|
+
human: graph?.tasks.filter((t) => t.status === "human").map((t) => t.id) ?? [],
|
|
1502
|
+
blocked: graph ? blockedTasks(graph).map((t) => t.id) : [],
|
|
1503
|
+
pending: graph ? pendingTasks(graph).map((t) => t.id) : [],
|
|
1504
|
+
phase,
|
|
1432
1505
|
fatal: true,
|
|
1433
1506
|
error: original,
|
|
1507
|
+
tipProof,
|
|
1434
1508
|
};
|
|
1435
1509
|
try {
|
|
1436
1510
|
journal.append("run-end", undefined, record);
|
|
1437
|
-
return;
|
|
1511
|
+
return tipProof;
|
|
1438
1512
|
}
|
|
1439
1513
|
catch {
|
|
1440
1514
|
// one retry below — the first failure is reported only if the retry also fails
|
|
@@ -1443,6 +1517,7 @@ function recordFatalRunEnd(journal, runId, branch, err) {
|
|
|
1443
1517
|
try {
|
|
1444
1518
|
terminateTornJournalTail(journalPath);
|
|
1445
1519
|
journal.append("run-end", undefined, record);
|
|
1520
|
+
return tipProof;
|
|
1446
1521
|
}
|
|
1447
1522
|
catch (retryErr) {
|
|
1448
1523
|
console.error(`tickmarkr ${runId}: run crashed — no terminal record written; journal sink unwritable at ${journalPath} (${retryErr instanceof Error ? retryErr.message : String(retryErr)}) — original error: ${original}`);
|
|
@@ -1512,12 +1587,18 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1512
1587
|
let journal;
|
|
1513
1588
|
let runStarted = false;
|
|
1514
1589
|
let taskLoopStarted = false;
|
|
1590
|
+
let graph;
|
|
1591
|
+
let fatalPhase = "setup";
|
|
1592
|
+
let deliberateTermination = false;
|
|
1593
|
+
const fatalStop = new AbortController();
|
|
1594
|
+
const inflight = new Map();
|
|
1595
|
+
let retireFatalSlots;
|
|
1515
1596
|
let branch = "";
|
|
1516
1597
|
let releaseApprovalSerialization;
|
|
1517
1598
|
let baselineCapture = Promise.resolve();
|
|
1518
1599
|
let baselineFailed = false;
|
|
1519
1600
|
try {
|
|
1520
|
-
|
|
1601
|
+
graph = loadGraph(repoRoot);
|
|
1521
1602
|
// One bounded snapshot supplies every dispatch. On resume the current journal still participates
|
|
1522
1603
|
// in the fold so its task-done/ordinary-approval events can retire older evidence, but findings it
|
|
1523
1604
|
// produced are suppressed: same-run feedback already replays those bytes and must not deliver them
|
|
@@ -1629,6 +1710,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1629
1710
|
await reapWorker(slot);
|
|
1630
1711
|
await driver.close(slot);
|
|
1631
1712
|
} }, s);
|
|
1713
|
+
retireFatalSlots = async () => {
|
|
1714
|
+
for (const slot of Array.from(liveSlots)) {
|
|
1715
|
+
try {
|
|
1716
|
+
await closeSlot(slot);
|
|
1717
|
+
}
|
|
1718
|
+
catch { /* preserve the original fatal error */ }
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1632
1721
|
const budgetSlots = new Map();
|
|
1633
1722
|
const trackedDriver = {
|
|
1634
1723
|
id: driver.id,
|
|
@@ -1637,19 +1726,24 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1637
1726
|
...(driver.describe ? { describe: driver.describe.bind(driver) } : {}),
|
|
1638
1727
|
...(driver.focus ? { focus: driver.focus.bind(driver) } : {}),
|
|
1639
1728
|
slot: async (cwd, name, o) => {
|
|
1729
|
+
fatalStop.signal.throwIfAborted();
|
|
1640
1730
|
const signal = executionSignal();
|
|
1641
1731
|
signal?.throwIfAborted();
|
|
1642
1732
|
const s = await driver.slot(cwd, name, o);
|
|
1643
1733
|
liveSlots.add(s);
|
|
1644
1734
|
if (signal)
|
|
1645
1735
|
budgetSlots.get(signal)?.add(s);
|
|
1646
|
-
if (signal?.aborted) {
|
|
1736
|
+
if (signal?.aborted || fatalStop.signal.aborted) {
|
|
1647
1737
|
await withoutExecutionBudget(() => closeSlot(s));
|
|
1648
|
-
signal.throwIfAborted();
|
|
1738
|
+
fatalStop.signal.throwIfAborted();
|
|
1739
|
+
signal?.throwIfAborted();
|
|
1649
1740
|
}
|
|
1650
1741
|
return s;
|
|
1651
1742
|
},
|
|
1652
|
-
run: (s, cmd) =>
|
|
1743
|
+
run: (s, cmd) => {
|
|
1744
|
+
fatalStop.signal.throwIfAborted();
|
|
1745
|
+
return driver.run(s, cmd);
|
|
1746
|
+
},
|
|
1653
1747
|
waitOutput: (s, p, ms, o) => driver.waitOutput(s, p, ms, o),
|
|
1654
1748
|
waitAgentStatus: (s, st, ms) => driver.waitAgentStatus(s, st, ms),
|
|
1655
1749
|
status: (s) => driver.status(s),
|
|
@@ -1692,6 +1786,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1692
1786
|
const exit = opts.exit ?? ((code) => process.exit(code));
|
|
1693
1787
|
onTermination = (sig) => {
|
|
1694
1788
|
termSignal = sig;
|
|
1789
|
+
deliberateTermination = true;
|
|
1695
1790
|
void (async () => {
|
|
1696
1791
|
if (!reaping) {
|
|
1697
1792
|
reaping = true;
|
|
@@ -1794,7 +1889,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1794
1889
|
// flag. Startup approvals seed the first scheduling pass; live approvals are folded at task
|
|
1795
1890
|
// boundaries below so a sibling can release parked work without waiting for run-end + resume.
|
|
1796
1891
|
const approvalStartupEvents = journal.read();
|
|
1797
|
-
|
|
1892
|
+
// Continuing human-gate permission survives enactment; malformed releases grant none.
|
|
1893
|
+
const validApproval = (e) => e.event === "task-approved" && e.taskId
|
|
1894
|
+
&& pendingApprovalActions([e]).get(e.taskId)?.authority !== "inert";
|
|
1895
|
+
const approved = new Set(approvalStartupEvents.filter(validApproval).map((e) => e.taskId));
|
|
1896
|
+
const startupActions = pendingDaemonApprovalActions(approvalStartupEvents);
|
|
1798
1897
|
let approvalSweepCursor = approvalStartupEvents.length;
|
|
1799
1898
|
const commands = detectGateCommands(repoRoot, cfg);
|
|
1800
1899
|
const watchName = formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId });
|
|
@@ -1900,6 +1999,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1900
1999
|
const startedAt = Date.now();
|
|
1901
2000
|
for (;;) {
|
|
1902
2001
|
signal?.throwIfAborted();
|
|
2002
|
+
fatalStop.signal.throwIfAborted();
|
|
1903
2003
|
executionSignal()?.throwIfAborted();
|
|
1904
2004
|
const count = await liveSuiteCount(repoRoot);
|
|
1905
2005
|
if (count === 0)
|
|
@@ -1941,11 +2041,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1941
2041
|
// Empty Map on fresh runs — every seed below conditions on resume.get(t.id), never on opts.resume (the
|
|
1942
2042
|
// GATE-08 lesson at the humanGate guard: condition on the data, not the code path). Dead-code
|
|
1943
2043
|
// equivalence to the router.ts:194 profile⇒undefined pattern: no map entry ⇒ today's literal.
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2044
|
+
const resume = opts.resume ? journal.replayResumeState() : new Map();
|
|
2045
|
+
const satisfiedGates = opts.resume ? journal.replaySatisfiedGates() : new Map();
|
|
2046
|
+
const replayedGateResults = resumeLifecycleOpen
|
|
1947
2047
|
? journal.replayCurrentAttemptGateResults()
|
|
1948
2048
|
: new Map();
|
|
2049
|
+
for (const [taskId, action] of startupActions) {
|
|
2050
|
+
if (action.authority === "worker")
|
|
2051
|
+
replayedGateResults.delete(taskId);
|
|
2052
|
+
}
|
|
1949
2053
|
// T7: the capacity THIS session resolved — read inside the run's fork budget, so it is the number
|
|
1950
2054
|
// every shell this run spawns will divide the machine by. Recorded evidence from another session
|
|
1951
2055
|
// is only reusable against this.
|
|
@@ -1998,7 +2102,29 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1998
2102
|
}
|
|
1999
2103
|
baseline = JSON.parse(readFileSync(join(journal.dir, "baseline.json"), "utf8"));
|
|
2000
2104
|
const replayEvents = journal.read();
|
|
2001
|
-
|
|
2105
|
+
// journal.replayStatuses predates typed releases and re-pends even inert rows. Keep that
|
|
2106
|
+
// legacy reader intact; scheduling accepts only the fold's recognised authorities.
|
|
2107
|
+
const statuses = new Map();
|
|
2108
|
+
for (const e of replayEvents) {
|
|
2109
|
+
if (!e.taskId)
|
|
2110
|
+
continue;
|
|
2111
|
+
if (e.event === "task-dispatch")
|
|
2112
|
+
statuses.set(e.taskId, "pending");
|
|
2113
|
+
else if (e.event === "task-done")
|
|
2114
|
+
statuses.set(e.taskId, "done");
|
|
2115
|
+
else if (e.event === "task-failed")
|
|
2116
|
+
statuses.set(e.taskId, "failed");
|
|
2117
|
+
else if (e.event === "task-human")
|
|
2118
|
+
statuses.set(e.taskId, "human");
|
|
2119
|
+
else if (validApproval(e))
|
|
2120
|
+
statuses.set(e.taskId, "pending");
|
|
2121
|
+
}
|
|
2122
|
+
for (const [id, action] of startupActions) {
|
|
2123
|
+
// Legacy gate-only approvals can finish and merge without a dispatch consuming funding.
|
|
2124
|
+
if (action.authority !== "inert" && statuses.get(id) !== "done")
|
|
2125
|
+
statuses.set(id, "pending");
|
|
2126
|
+
}
|
|
2127
|
+
for (const [id, st] of statuses) {
|
|
2002
2128
|
if (opts.retryFailed && st === "failed" && recordedTaskFailureKind(replayEvents, id) === "dispatch") {
|
|
2003
2129
|
graph = setStatus(graph, id, "pending");
|
|
2004
2130
|
// OBS-254: clear ATTEMPT AND CHANNEL STATE ONLY. Deleting the whole entry also deleted
|
|
@@ -2021,6 +2147,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2021
2147
|
...(opts.retryFailed ? { retryFailed: true } : {}),
|
|
2022
2148
|
...(opts.graphChanged ? { graphChanged: true } : {}), // OBS-1073: the release is the engagement's
|
|
2023
2149
|
});
|
|
2150
|
+
runStarted = true;
|
|
2024
2151
|
await placeBoard();
|
|
2025
2152
|
}
|
|
2026
2153
|
else {
|
|
@@ -2142,6 +2269,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2142
2269
|
let mergeChain = Promise.resolve();
|
|
2143
2270
|
const mergeSerial = (taskBranch, t, gated) => {
|
|
2144
2271
|
const next = mergeChain.then(() => {
|
|
2272
|
+
fatalStop.signal.throwIfAborted();
|
|
2145
2273
|
executionSignal()?.throwIfAborted();
|
|
2146
2274
|
journal.phaseStart(t.id, "merge");
|
|
2147
2275
|
return mergeTask(intWt, taskBranch, `tickmarkr: merge ${t.id} ${t.title}`, gated);
|
|
@@ -2173,15 +2301,22 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2173
2301
|
const tree = await shGit("git ls-files -z", wt);
|
|
2174
2302
|
return new Set([...(tree.code === 0 ? tree.stdout.split("\0").filter(Boolean) : []), ...await diffPaths(base, wt)]);
|
|
2175
2303
|
};
|
|
2304
|
+
// The base each task was dispatched on: the integration branch moves when a sibling merges, the pin does not.
|
|
2305
|
+
const taskBases = new Map();
|
|
2176
2306
|
const park = async (t, reason, kind, assignment, attempts, startMs, gateFails = 0, consults = 0, tokens, metered = 0, retryMode = "fresh", details = {}) => {
|
|
2177
2307
|
// OBS-979: a worker refusal can identify the missing authoring scope even when gates
|
|
2178
2308
|
// subsequently supply the park's disposition. Keep that actionable path on the park itself.
|
|
2179
2309
|
const worker = journal.read().reverse().find((e) => e.taskId === t.id && e.event === "worker-result");
|
|
2180
2310
|
if (!details.approveCommand && t.files.length > 0 && worker?.data.ok === false && typeof worker.data.summary === "string") {
|
|
2181
2311
|
const allowed = filesGlob(t.files);
|
|
2182
|
-
|
|
2312
|
+
// OBS-1072 residual: the same inventory as the main path — the task tree and its diff from the
|
|
2313
|
+
// task's PINNED base, never the main checkout and never the moving integration branch.
|
|
2314
|
+
const wt = worktreePath(repoRoot, `${branch}--${t.id}`);
|
|
2315
|
+
const inventory = existsSync(wt) ? await treeOrDiffPaths(taskBases.get(t.id) ?? "HEAD", wt) : new Set();
|
|
2316
|
+
const paths = resolveScopeHints(namedPaths(worker.data.summary).filter((path) => !allowed(path)), inventory)
|
|
2317
|
+
.resolved.filter((path) => !allowed(path));
|
|
2183
2318
|
if (paths.length)
|
|
2184
|
-
reason += ` — files[] repair hint: ${
|
|
2319
|
+
reason += ` — files[] repair hint: ${paths.join(", ")}`;
|
|
2185
2320
|
}
|
|
2186
2321
|
graph = setStatus(graph, t.id, "human");
|
|
2187
2322
|
saveGraph(repoRoot, graph);
|
|
@@ -2212,42 +2347,34 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2212
2347
|
const dispositionScopeRed = async (t, results, assignment, attempts, startMs, gateFails, consults, tokens, metered, retryMode, changed = new Set()) => {
|
|
2213
2348
|
const scopeRed = results.find((g) => g.gate === "scope" && gateFailed(g));
|
|
2214
2349
|
const verdict = scopeRed?.meta?.collateral;
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
const
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
journal.append("scope-request", t.id, classification);
|
|
2242
|
-
else
|
|
2243
|
-
journal.append("scope-authoring", t.id, classification);
|
|
2244
|
-
// OBS-547 (Leg-2 T3 material): unchargeable ⇒ metered 0, exactly as the predicted path below —
|
|
2245
|
-
// passing the physical count would write `meteredAttempts: 1` beside `attempts: 0`.
|
|
2246
|
-
const approveCommand = `tickmarkr approve ${runId} ${t.id} --files ${[...new Set(refusal.length ? refusal : paths)].map((path) => /^[\w./-]+$/.test(path) ? path : shq(path)).join(",")}`;
|
|
2247
|
-
await park(t, `${scopeRequest ? "scope request" : "authoring defect"} — files[] repair hint: ${[...new Set(refusal.length ? refusal : paths)].join(", ")}; current files[]: ${t.files.join(", ")}${scopeRequest ? `\n${approveCommand}` : ""}`, scopeRequest ? "scope-request" : "authoring", assignment, attempts, startMs, gateFails, consults, tokens, 0, retryMode, scopeRequest ? { paths: [...new Set(refusal.length ? refusal : paths)], approveCommand, graphDefinitionHash: graphDefinitionHash(graph) } : {});
|
|
2248
|
-
return true;
|
|
2249
|
-
}
|
|
2350
|
+
const worker = journal.read().reverse().find((e) => e.taskId === t.id && e.event === "worker-result");
|
|
2351
|
+
const disposition = classifyRepairDisposition({
|
|
2352
|
+
results, files: t.files, inventory: changed,
|
|
2353
|
+
refusalSummary: worker?.data.ok === false && typeof worker.data.summary === "string" ? worker.data.summary : undefined,
|
|
2354
|
+
});
|
|
2355
|
+
for (const diagnostic of disposition.diagnostics) {
|
|
2356
|
+
journal.append("scope-hint-unresolved", t.id, {
|
|
2357
|
+
paths: [diagnostic.candidate], reason: diagnostic.reason, diagnostic, attempt: attempts + 1,
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
if (disposition.blocker === "infra")
|
|
2361
|
+
return false;
|
|
2362
|
+
if (!disposition.blocker && (disposition.kind === "scope-request" || disposition.kind === "authoring")) {
|
|
2363
|
+
const paths = disposition.paths;
|
|
2364
|
+
const scopeRequest = disposition.kind === "scope-request";
|
|
2365
|
+
const classification = {
|
|
2366
|
+
gate: disposition.source, paths, repair: `files[] repair hint: ${paths.join(", ")}`,
|
|
2367
|
+
attempt: attempts + 1, chargeable: false, source: "diagnostic",
|
|
2368
|
+
};
|
|
2369
|
+
if (scopeRequest)
|
|
2370
|
+
journal.append("scope-request", t.id, classification);
|
|
2371
|
+
else
|
|
2372
|
+
journal.append("scope-authoring", t.id, classification);
|
|
2373
|
+
const approveCommand = `tickmarkr approve ${runId} ${t.id} --files ${paths.map((path) => /^[\w./-]+$/.test(path) ? path : shq(path)).join(",")}`;
|
|
2374
|
+
await park(t, `${scopeRequest ? "scope request" : "authoring defect"} — files[] repair hint: ${paths.join(", ")}; current files[]: ${t.files.join(", ")}${scopeRequest ? `\n${approveCommand}` : ""}`, scopeRequest ? "scope-request" : "authoring", assignment, attempts, startMs, gateFails, consults, tokens, 0, retryMode, scopeRequest ? { paths, approveCommand, graphDefinitionHash: graphDefinitionHash(graph) } : {});
|
|
2375
|
+
return true;
|
|
2250
2376
|
}
|
|
2377
|
+
// A detection site permits the ordinary chargeable policy; it does not itself grant budget.
|
|
2251
2378
|
if (!verdict)
|
|
2252
2379
|
return false;
|
|
2253
2380
|
if (verdict.authoring) {
|
|
@@ -2284,17 +2411,33 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2284
2411
|
approvalSweepCursor = events.length;
|
|
2285
2412
|
if (approvals.length === 0)
|
|
2286
2413
|
return;
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2414
|
+
// Replay every authority from this pass's single snapshot. In particular, helper reads must
|
|
2415
|
+
// neither rescan the file under load nor observe approvals beyond approvalSweepCursor.
|
|
2416
|
+
// Keep audit writes on the live journal so narration and persistence retain their owner.
|
|
2417
|
+
const sweepJournal = Journal.open(repoRoot, runId);
|
|
2418
|
+
sweepJournal.read = () => events;
|
|
2419
|
+
sweepJournal.append = journal.append.bind(journal);
|
|
2420
|
+
graph = applyScopeAmendments(graph, sweepJournal, false, opts.graphChanged === true); // OBS-1073: same release as launch
|
|
2421
|
+
const actions = pendingDaemonApprovalActions(events);
|
|
2422
|
+
const nextResume = sweepJournal.replayResumeState();
|
|
2423
|
+
const nextSatisfied = sweepJournal.replaySatisfiedGates();
|
|
2293
2424
|
let changed = false;
|
|
2294
|
-
for (const
|
|
2295
|
-
const
|
|
2425
|
+
for (const taskId of new Set(approvals.map((e) => e.taskId))) {
|
|
2426
|
+
const action = actions.get(taskId);
|
|
2427
|
+
if (!action || action.authority === "inert")
|
|
2428
|
+
continue;
|
|
2296
2429
|
approved.add(taskId);
|
|
2297
|
-
|
|
2430
|
+
const state = nextResume.get(taskId);
|
|
2431
|
+
if (state)
|
|
2432
|
+
resume.set(taskId, state);
|
|
2433
|
+
else
|
|
2434
|
+
resume.delete(taskId);
|
|
2435
|
+
const gate = nextSatisfied.get(taskId);
|
|
2436
|
+
if (gate)
|
|
2437
|
+
satisfiedGates.set(taskId, gate);
|
|
2438
|
+
else
|
|
2439
|
+
satisfiedGates.delete(taskId);
|
|
2440
|
+
if (action.authority !== "waiver")
|
|
2298
2441
|
replayedGateResults.delete(taskId);
|
|
2299
2442
|
try {
|
|
2300
2443
|
const task = getTask(graph, taskId);
|
|
@@ -2311,6 +2454,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2311
2454
|
saveGraph(repoRoot, graph);
|
|
2312
2455
|
};
|
|
2313
2456
|
const execTask = async (t) => {
|
|
2457
|
+
const approvalAction = pendingDaemonApprovalActions(journal.read()).get(t.id);
|
|
2458
|
+
const fundedRerun = approvalAction?.authority === "worker";
|
|
2314
2459
|
const startMs = Date.now();
|
|
2315
2460
|
let stallSeat;
|
|
2316
2461
|
let stallReaps = 0;
|
|
@@ -2540,7 +2685,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2540
2685
|
...(typeof g.meta.preservedRef === "string" ? { preservedRef: g.meta.preservedRef } : {}),
|
|
2541
2686
|
} : {}),
|
|
2542
2687
|
...(cfg.executionPolicy && !g.pass ? { disposition: failureDisposition(g) } : {}),
|
|
2543
|
-
...Object.fromEntries(["runnerInfraRerun", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision"]
|
|
2688
|
+
...Object.fromEntries(["runnerInfraRerun", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence"]
|
|
2544
2689
|
.filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
|
|
2545
2690
|
// OBS-540: preserve terminal-vs-retryable infra exactly. normalizeGateOutcome deliberately
|
|
2546
2691
|
// defaults a legacy infra row to retryable, so dropping an explicit false here reverses the
|
|
@@ -2816,10 +2961,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2816
2961
|
// An operator approval skips its exact failed gate by authority; observed results skip only the
|
|
2817
2962
|
// contiguous green prefix whose recorded commit is still the task branch tip.
|
|
2818
2963
|
const satisfiedGate = satisfiedGates.get(t.id);
|
|
2819
|
-
const replayedGates = replayedGateResults.get(t.id);
|
|
2820
|
-
const recheck =
|
|
2964
|
+
const replayedGates = fundedRerun ? undefined : replayedGateResults.get(t.id);
|
|
2965
|
+
const recheck = approvalAction?.authority === "battery";
|
|
2821
2966
|
resumeGateReplay: if (satisfiedGate || replayedGates || recheck) {
|
|
2822
2967
|
const taskBase = await integrationHead(intWt);
|
|
2968
|
+
taskBases.set(t.id, taskBase);
|
|
2823
2969
|
const taskBranch = `${branch}--${t.id}`;
|
|
2824
2970
|
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
2825
2971
|
// OBS-1022: the ref `approve --recheck` validated (commits ahead of the base) is the tree the
|
|
@@ -3016,6 +3162,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3016
3162
|
}
|
|
3017
3163
|
const resumedTask = { ...t, gates: remainingGates };
|
|
3018
3164
|
gateLoop: while (true) {
|
|
3165
|
+
fatalStop.signal.throwIfAborted();
|
|
3019
3166
|
executionSignal()?.throwIfAborted();
|
|
3020
3167
|
const gated = await gitHead(wt);
|
|
3021
3168
|
gateSubject = {
|
|
@@ -3073,7 +3220,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3073
3220
|
},
|
|
3074
3221
|
}, false));
|
|
3075
3222
|
results.forEach(classifySignalOnlyTest);
|
|
3076
|
-
if (
|
|
3223
|
+
if (pendingDaemonApprovalActions(journal.read()).get(t.id)?.authority === "battery") {
|
|
3077
3224
|
journal.append("recheck-battery", t.id, {
|
|
3078
3225
|
commit: gateSubject.commit,
|
|
3079
3226
|
gates: resumedTask.gates,
|
|
@@ -3125,6 +3272,26 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3125
3272
|
}
|
|
3126
3273
|
assignment = { adapter: seat.adapter, model: seat.model, channel: seat.channel, tier: seat.tier };
|
|
3127
3274
|
}
|
|
3275
|
+
// Carry completeness was verified before this replay battery. Apply the ordinary
|
|
3276
|
+
// repair bounds here too; a restored red must fund the same findings-bearing dispatch.
|
|
3277
|
+
// Funding is durable intent only: repairReachSinceApproval charges it after launch
|
|
3278
|
+
// reaches a funded gate, and a restart before launch must not duplicate that intent.
|
|
3279
|
+
const failing = results.filter(gateFailed);
|
|
3280
|
+
const battery = failing.some((g) => g.gate !== "review") ? failing.filter((g) => g.gate !== "review") : failing;
|
|
3281
|
+
const landed = await commitsAheadOf(taskBase, wt);
|
|
3282
|
+
const events = journal.read();
|
|
3283
|
+
const drawn = repairsSinceApproval(events, t.id);
|
|
3284
|
+
const repeated = failing.some((g) => isDeterministicFailure(g)
|
|
3285
|
+
&& identicalGateFailures(events, t.id, g.gate, normalizeGateFailure(g.details)) >= GATE_FINGERPRINT_CAP);
|
|
3286
|
+
if (narrowRepairBattery(battery) && landed.length > 0 && drawn < MAX_REPAIRS
|
|
3287
|
+
&& (rs?.attempts ?? 0) < MAX_ATTEMPTS && !repeated
|
|
3288
|
+
&& pendingRepairFindings(events, t.id) === undefined) {
|
|
3289
|
+
journal.append("repair-attempt", t.id, {
|
|
3290
|
+
repair: repairReachSinceApproval(events, t.id).length + 1, charge: drawn + 1, of: MAX_REPAIRS,
|
|
3291
|
+
gates: failing.map((g) => g.gate), commits: landed.length,
|
|
3292
|
+
findings: withCarriedEvidence(repairFindingsBrief(results)),
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3128
3295
|
break gateLoop;
|
|
3129
3296
|
}
|
|
3130
3297
|
await park(t, gateFailApprovalReason(t.id, "post-approval gate failed", results.some((g) => g.gate === "review" && gateFailed(g))), "gate-fail", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
@@ -3170,6 +3337,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3170
3337
|
// the same worktree. The next loop iteration retires it before reading/recreating the tree.
|
|
3171
3338
|
let supersededWorkerSlot;
|
|
3172
3339
|
attempts: for (let attempt = rs?.attempts ?? 0;; attempt++) {
|
|
3340
|
+
// Funding belongs to the next attempt. Later automatic repairs retain ordinary red reuse.
|
|
3341
|
+
const explicitlyFundedAttempt = fundedRerun && attempt === (rs?.attempts ?? 0);
|
|
3342
|
+
fatalStop.signal.throwIfAborted();
|
|
3173
3343
|
executionSignal()?.throwIfAborted();
|
|
3174
3344
|
if (attempt !== providerDeathAttempt) {
|
|
3175
3345
|
providerDeathRequeues = 0;
|
|
@@ -3344,6 +3514,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3344
3514
|
// T6: a dispatch that carries an outstanding finding says so, and names it. Without this the
|
|
3345
3515
|
// ledger cannot tell a carried dispatch from an amnesiac one — the exact question a run that
|
|
3346
3516
|
// spends two frontier attempts re-deriving a known defect has to be able to answer afterwards.
|
|
3517
|
+
fatalStop.signal.throwIfAborted();
|
|
3347
3518
|
journal.append("task-dispatch", t.id, {
|
|
3348
3519
|
...(scopeApproval?.data.release === "scope-request" ? { files: t.files, graphDefinitionHash: graphDefinitionHash(graph) } : {}),
|
|
3349
3520
|
assignment, attempt, provenance: dispatchProvenance([
|
|
@@ -3362,6 +3533,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3362
3533
|
});
|
|
3363
3534
|
journal.phaseStart(t.id, "worker", { attempt, assignment });
|
|
3364
3535
|
const taskBase = await integrationHead(intWt); // deps are merged → visible to this task
|
|
3536
|
+
taskBases.set(t.id, taskBase);
|
|
3365
3537
|
const taskBranch = `${branch}--${t.id}`; // "--": a ref can't nest under the existing integration branch (locked decision 10)
|
|
3366
3538
|
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
3367
3539
|
const recreating = existsSync(priorWt);
|
|
@@ -3869,6 +4041,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3869
4041
|
while (Date.now() < hardDeadline) {
|
|
3870
4042
|
if (termSignal)
|
|
3871
4043
|
throw new Error(`terminated by ${termSignal}`);
|
|
4044
|
+
fatalStop.signal.throwIfAborted();
|
|
3872
4045
|
executionSignal()?.throwIfAborted();
|
|
3873
4046
|
driverProbeFailed = false;
|
|
3874
4047
|
const sliceStart = Date.now();
|
|
@@ -4452,6 +4625,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4452
4625
|
// there is the trailer regex; the exit marker only sets exitCode), and the cause taxonomy
|
|
4453
4626
|
// already names this shape "clean-exit-no-trailer" — unreachable in print mode until now.
|
|
4454
4627
|
while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs) {
|
|
4628
|
+
fatalStop.signal.throwIfAborted();
|
|
4455
4629
|
executionSignal()?.throwIfAborted();
|
|
4456
4630
|
const remaining = stallWindowMs - (Date.now() - lastProgressAt);
|
|
4457
4631
|
let slice = Math.min(BLOCKED_POLL_MS, Math.max(100, Math.min(stallWindowMs / 2, remaining)));
|
|
@@ -4881,6 +5055,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4881
5055
|
let results = [];
|
|
4882
5056
|
let commits = [];
|
|
4883
5057
|
gateLoop: while (true) {
|
|
5058
|
+
fatalStop.signal.throwIfAborted();
|
|
4884
5059
|
executionSignal()?.throwIfAborted();
|
|
4885
5060
|
const gated = await gitHead(wt);
|
|
4886
5061
|
gateSubject = { commit: await gateCommitSubject(taskBase, gated, wt), attempt };
|
|
@@ -4895,7 +5070,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4895
5070
|
&& e.data.attempt === attempt - 1)
|
|
4896
5071
|
: [];
|
|
4897
5072
|
journal.phaseStart(t.id, "gates");
|
|
4898
|
-
const replay = previousRows.length > 0
|
|
5073
|
+
const replay = !explicitlyFundedAttempt && previousRows.length > 0
|
|
4899
5074
|
&& previousRows.every((e) => e.data.commit === gateSubject.commit)
|
|
4900
5075
|
&& previousRows.some((e) => e.data.pass === false && e.data.skipped !== true);
|
|
4901
5076
|
if (replay) {
|
|
@@ -4922,6 +5097,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4922
5097
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
4923
5098
|
collateral: collateral.get(t.id) ?? [],
|
|
4924
5099
|
selectTests: !testGateFailed,
|
|
5100
|
+
cachedRedBypass: explicitlyFundedAttempt ? "operator-rerun" : undefined,
|
|
4925
5101
|
via: cfg.visibility.llm === "pane"
|
|
4926
5102
|
? {
|
|
4927
5103
|
driver: trackedDriver,
|
|
@@ -4960,6 +5136,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4960
5136
|
// gate-fail brief is, so prior-RUN evidence (retired by its own rule, not by this reviewer)
|
|
4961
5137
|
// survives and only this run's settled findings go.
|
|
4962
5138
|
feedback = withCarriedEvidence("");
|
|
5139
|
+
fatalStop.signal.throwIfAborted();
|
|
4963
5140
|
executionSignal()?.throwIfAborted();
|
|
4964
5141
|
const m = await mergeSerial(taskBranch, t, gated);
|
|
4965
5142
|
if (m.tipMoved) {
|
|
@@ -5029,7 +5206,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5029
5206
|
// v1.53 T3: prefer the CLI's own session id captured from this attempt's output (kimi's resume
|
|
5030
5207
|
// trailer) over the harness slot name; absent hook or no capture keeps today's slot-name id.
|
|
5031
5208
|
retrySession = { channel: channelKey(assignment), id: adapter.sessionIdFrom?.(output) ?? sessionId, contextTokens };
|
|
5032
|
-
feedback = withCarriedEvidence(results
|
|
5209
|
+
feedback = withCarriedEvidence(repairFindingsBrief(results));
|
|
5033
5210
|
// OBS-189/G3 (park-economics patch): a request-changes review is a findings brief, not a worker
|
|
5034
5211
|
// defect — the fix attempt stays on the same channel with the findings as feedback and consumes
|
|
5035
5212
|
// no escalation-ladder rung. Bounded by the engagement round cap at the top of this loop.
|
|
@@ -5057,7 +5234,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5057
5234
|
const repairable = narrowRepairBattery(repairBattery) && lostCommits.length === 0 && landed.length > 0;
|
|
5058
5235
|
const repairHistory = repairReachSinceApproval(journal.read(), t.id);
|
|
5059
5236
|
const repairsDrawn = repairsSinceApproval(journal.read(), t.id);
|
|
5060
|
-
const repair = repairable && repairsDrawn < MAX_REPAIRS;
|
|
5237
|
+
const repair = repairable && repairsDrawn < MAX_REPAIRS && attempt + 1 < MAX_ATTEMPTS;
|
|
5061
5238
|
// v1.85 T3: the fingerprint cap. Two normalized-identical failures of one DETERMINISTIC gate on
|
|
5062
5239
|
// one task (volatile tokens — worktree prefixes, line refs, durations, run ids — are not
|
|
5063
5240
|
// information) mean the round about to be bought is a re-measurement: ~663m across 5 runs went to
|
|
@@ -5217,7 +5394,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5217
5394
|
});
|
|
5218
5395
|
};
|
|
5219
5396
|
taskLoopStarted = true;
|
|
5220
|
-
|
|
5397
|
+
fatalPhase = "scheduler";
|
|
5221
5398
|
// A settled worker can release a dependency or an approval in the same poll tick.
|
|
5222
5399
|
// Audit the current graph at every empty-flight boundary, never a prior ready snapshot.
|
|
5223
5400
|
// WB-1 rider: `end-condition-held` names a CLOSE the daemon refused, so only a close attempt journals
|
|
@@ -5236,6 +5413,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5236
5413
|
return true;
|
|
5237
5414
|
};
|
|
5238
5415
|
closeLoop: while (true) {
|
|
5416
|
+
fatalPhase = "scheduler";
|
|
5239
5417
|
let approvalDeadline;
|
|
5240
5418
|
while (true) {
|
|
5241
5419
|
// v1.54 T2: a signal that landed while nothing was racing `aborted` (empty inflight window)
|
|
@@ -5262,6 +5440,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5262
5440
|
cleanupErrors.push({ slot: slot.name, attempt: owner.attempt, error: String(error) });
|
|
5263
5441
|
}
|
|
5264
5442
|
}
|
|
5443
|
+
if (fatalStop.signal.aborted)
|
|
5444
|
+
return;
|
|
5265
5445
|
const cleanupEvidence = cleanupErrors.length ? { cleanupErrors } : {};
|
|
5266
5446
|
if (err instanceof HeldProbeExhausted) {
|
|
5267
5447
|
const wt = worktreePath(repoRoot, `${branch}--${t.id}`);
|
|
@@ -5364,7 +5544,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5364
5544
|
blocked: blockedTasks(graph).map((t) => t.id),
|
|
5365
5545
|
pending: pendingTasks(graph).map((t) => t.id),
|
|
5366
5546
|
};
|
|
5547
|
+
fatalPhase = "baseline";
|
|
5367
5548
|
await baselineCapture; // also drain capture when every worker parks or fails before its gates
|
|
5549
|
+
fatalPhase = "tip-verify";
|
|
5368
5550
|
// OBS-34: post-merge integration-tip verify — strict exit codes, no baseline forgiveness.
|
|
5369
5551
|
const lastMergedTask = [...journal.read()].reverse().find((e) => e.event === "merge" && e.taskId)?.taskId;
|
|
5370
5552
|
if (summary.done.length > 0 && Object.keys(commands).length > 0) {
|
|
@@ -5407,9 +5589,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5407
5589
|
clearTimeout(poll);
|
|
5408
5590
|
}
|
|
5409
5591
|
summary.tipVerify = tipFailed ? "failed" : "passed";
|
|
5592
|
+
summary.tipProof = runEndTipProof(journal.read());
|
|
5410
5593
|
if (tipFailed && lastMergedTask)
|
|
5411
5594
|
summary.lastMergedTask = lastMergedTask;
|
|
5412
5595
|
}
|
|
5596
|
+
else {
|
|
5597
|
+
// Exactly one proof per close: no cycle ran for THIS close, and an earlier one is not inherited.
|
|
5598
|
+
summary.tipProof = { kind: "incomplete" };
|
|
5599
|
+
}
|
|
5413
5600
|
// T14: read the journal, not the startup `approved` set — an approval appended DURING this run is
|
|
5414
5601
|
// exactly the one the set cannot see, and it is the one the record has to name.
|
|
5415
5602
|
//
|
|
@@ -5417,6 +5604,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5417
5604
|
// graph.lock release in the outer finally: an approval that wins first is included below, while an
|
|
5418
5605
|
// approval that loses cannot append until the live owner is gone and reports recorded-no-owner.
|
|
5419
5606
|
// Thus no accepted approval can land after this sample while still being attributed to this run.
|
|
5607
|
+
fatalPhase = "scheduler";
|
|
5420
5608
|
const approvalSerialization = await acquireApprovalSerialization(repoRoot, runId);
|
|
5421
5609
|
releaseApprovalSerialization = approvalSerialization.release;
|
|
5422
5610
|
// Close the last poll-to-run-end race while holding the same serializer as approve.
|
|
@@ -5448,7 +5636,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5448
5636
|
const tipFail = summary.tipVerify === "failed"
|
|
5449
5637
|
? ` — TIP VERIFY FAILED on ${summary.lastMergedTask ? `last merge ${summary.lastMergedTask}` : "integration tip"}`
|
|
5450
5638
|
: "";
|
|
5451
|
-
await driver.notify(`tickmarkr ${runId}: ${summary.done.length} done, ${summary.failed.length} failed, ${summary.human.length} awaiting human, ${summary.blocked.length} blocked, ${summary.pending.length} pending${attribution ? ` (${attribution})` : ""}${tipFail} — integration branch ${branch} (merge to main is yours)`, { tier: summary.tipVerify === "failed" ? "attention" : "routine" });
|
|
5639
|
+
await driver.notify(`tickmarkr ${runId}: ${summary.done.length} done, ${summary.failed.length} failed, ${summary.human.length} awaiting human, ${summary.blocked.length} blocked, ${summary.pending.length} pending${attribution ? ` (${attribution})` : ""}${tipFail} — ${formatTipProof(summary.tipProof)} — integration branch ${branch} (merge to main is yours)`, { tier: summary.tipVerify === "failed" ? "attention" : "routine" });
|
|
5452
5640
|
return summary;
|
|
5453
5641
|
}
|
|
5454
5642
|
});
|
|
@@ -5458,8 +5646,22 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5458
5646
|
// T7 (v1.86): guarded — a journal read/append failure while recording the fatal run-end is
|
|
5459
5647
|
// reported alongside err, never instead of it; recordFatalRunEnd never throws, so the original
|
|
5460
5648
|
// error (message, stack, cause) always reaches the caller verbatim.
|
|
5461
|
-
|
|
5462
|
-
|
|
5649
|
+
// Freeze failure-time buckets, stop new dispatch, and drain all journal writers before closing.
|
|
5650
|
+
if (!deliberateTermination) {
|
|
5651
|
+
const failedGraph = graph;
|
|
5652
|
+
const phase = taskLoopStarted ? (baselineFailed ? "baseline" : fatalPhase) : "setup";
|
|
5653
|
+
fatalStop.abort(err);
|
|
5654
|
+
await retireFatalSlots?.();
|
|
5655
|
+
await Promise.allSettled(inflight.values());
|
|
5656
|
+
await baselineCapture.catch(() => { });
|
|
5657
|
+
if (runStarted && !deliberateTermination) {
|
|
5658
|
+
const tipProof = recordFatalRunEnd(journal, runId, branch, err, taskLoopStarted || opts.resume ? failedGraph : undefined, phase);
|
|
5659
|
+
// The fatal close states its proof too; a dead notifier never replaces the original error.
|
|
5660
|
+
if (tipProof) {
|
|
5661
|
+
await driver.notify(`tickmarkr ${runId}: run crashed — ${formatTipProof(tipProof)} — integration branch ${branch}`, { tier: "attention" }).catch(() => { });
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5463
5665
|
throw err;
|
|
5464
5666
|
}
|
|
5465
5667
|
finally {
|