tickmarkr 2.8.6 → 2.8.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/adapters/grok.js +5 -9
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +13 -3
- package/dist/cli/commands/report.js +21 -12
- package/dist/cli/commands/status.js +14 -2
- package/dist/cli/commands/verify.js +29 -5
- package/dist/cli/commands/version.d.ts +1 -1
- package/dist/cli/commands/version.js +36 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +6 -3
- package/dist/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- package/dist/gates/baseline.js +47 -7
- package/dist/gates/run-gates.js +31 -22
- package/dist/report/bundle.d.ts +5 -2
- package/dist/report/bundle.js +11 -6
- package/dist/report/compare.d.ts +9 -0
- package/dist/report/compare.js +58 -40
- package/dist/report/operator-record.js +4 -3
- package/dist/run/daemon.d.ts +7 -3
- package/dist/run/daemon.js +157 -58
- package/dist/run/git.d.ts +5 -0
- package/dist/run/git.js +51 -1
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +2 -2
- package/dist/run/repair-disposition.js +35 -4
- package/dist/run/stall.d.ts +2 -0
- package/dist/run/stall.js +12 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -8
- package/skills/tickmarkr-loop/SKILL.md +10 -8
- package/skills/tickmarkr-overseer/SKILL.md +10 -8
package/dist/run/daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HOST_PROBE_SAMPLE_MS, HOST_PROBE_SAMPLES, HostDegradedError, hostDegraded, observeHost } from "./host-health.js";
|
|
2
2
|
import { runWithInterruptionSink, VITEST_CACHE_ENV, worktreeVitestCache } from "../gates/test-manifest.js";
|
|
3
3
|
import { COMMAND_LEASE_TOKEN_ENV, commandLeaseEnvironment, CommandLeases, currentCommandLeaseToken, isRunnerCommand, runWithCommandLease, withCommandLease } from "./lease.js";
|
|
4
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { shq } from "../adapters/types.js";
|
|
7
7
|
import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -23,7 +23,7 @@ import { herdrSealShellPrefix, MAX_BUF, SubprocessDriver } from "../drivers/subp
|
|
|
23
23
|
import { formatOwnedName } from "../drivers/types.js";
|
|
24
24
|
import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
|
|
25
25
|
import { reobserveTestFiles, runGates } from "../gates/run-gates.js";
|
|
26
|
-
import { getVerdictStore, isInfraResult, resolveStateDir } from "../gates/cache.js";
|
|
26
|
+
import { environmentFingerprint, getVerdictStore, isInfraResult, resolveStateDir } from "../gates/cache.js";
|
|
27
27
|
import { RUNNER_INFRA_DIAGNOSTIC_RE, timeoutShaped } from "../gates/timeout-shaped.js";
|
|
28
28
|
import { filesGlob } from "../graph/files-glob.js";
|
|
29
29
|
import { addEvidence, attributeBlocked, batteryPriority, blockedTasks, getTask, graphDefinitionHash, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
|
|
@@ -34,10 +34,10 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
34
34
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
35
35
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
36
36
|
import { runEnvironment } from "./environment.js";
|
|
37
|
-
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, dependencyLinkRefusal, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WorktreeAdminRaceError, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
38
38
|
import { MASK } from "./redact.js";
|
|
39
39
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
40
|
-
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
40
|
+
import { classifyRepairDisposition, namedPaths, resolveScopeHints } from "./repair-disposition.js";
|
|
41
41
|
import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVEST_SOURCE, resumeHarvestAuthor, approvalAction, APPROVAL_REFUSED, bindingToken, effectiveEvents, foldDecisions, physicalLine, staleApprovals, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, readableExcerpt, receiptOrigin, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, priorJudgments, readPriorRunEvidence, recordedTaskFailureKind, reraisedReviewChains, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, standingRulings, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
42
42
|
import { carriedAuthorVendors, gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
|
|
43
43
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
@@ -855,7 +855,11 @@ export function resetDeadChannelFastKillMsForTests() {
|
|
|
855
855
|
// monotonic-tracker silence — CONCLUDES the wait. Conclude, never kill: the pane is harvested by
|
|
856
856
|
// the same tail a window expiry uses, and the carried worktree goes straight to gates. Set at the
|
|
857
857
|
// fast-kill's window on purpose (the OBS-264 arithmetic is "a ~36m stall + ~15m redo becomes a
|
|
858
|
-
// ~5m gate pass")
|
|
858
|
+
// ~5m gate pass"). Queue row 231 (D-2119): a headless worker waiting on its remote provider can be
|
|
859
|
+
// silent with flat CPU. Print mode also requires a fresh reading of no ESTABLISHED TCP connection
|
|
860
|
+
// in its owned process group; connected or unreadable holds the harvest. Interactive workers keep
|
|
861
|
+
// the triad: an idle pane holds connections too. Residual (D-2125): a truly hung headless worker
|
|
862
|
+
// holding a connection rides the stall window instead.
|
|
859
863
|
const HARVEST_SILENT_MS = 5 * 60_000;
|
|
860
864
|
let harvestSilentMs = HARVEST_SILENT_MS;
|
|
861
865
|
/** Test seam — shrink the harvest silence gate without minute-long sleeps. */
|
|
@@ -879,6 +883,32 @@ export const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries committ
|
|
|
879
883
|
export function commandsHash(commands) {
|
|
880
884
|
return createHash("sha256").update(JSON.stringify(Object.entries(commands).sort())).digest("hex").slice(0, 12);
|
|
881
885
|
}
|
|
886
|
+
// Queue row 24 (D-2134): refuse links before npm probes; an unreadable identity stays absent for the battery's infra row.
|
|
887
|
+
function measureTipEnvironment(worktree, capacity) {
|
|
888
|
+
if (dependencyLinkRefusal(worktree))
|
|
889
|
+
return undefined;
|
|
890
|
+
try {
|
|
891
|
+
return environmentFingerprint({ worktree, scope: "tip", capacity });
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Queue row 24 (D-2134): validate the parts too; a matching fingerprint alone cannot rescue a malformed stamp.
|
|
898
|
+
function sameTipEnvironment(recorded, current) {
|
|
899
|
+
if (!current || !recorded || typeof recorded !== "object" || Array.isArray(recorded))
|
|
900
|
+
return false;
|
|
901
|
+
const { fingerprint, parts } = recorded;
|
|
902
|
+
if (fingerprint !== current.fingerprint || !parts || typeof parts !== "object" || Array.isArray(parts))
|
|
903
|
+
return false;
|
|
904
|
+
const p = parts;
|
|
905
|
+
const verification = p.verification;
|
|
906
|
+
return p.nodeRuntime === current.parts.nodeRuntime && p.runner === current.parts.runner
|
|
907
|
+
&& p.lockfile === current.parts.lockfile && p.selectedSet === undefined
|
|
908
|
+
&& p.capacity !== undefined && sameCapacity(p.capacity, current.parts.capacity)
|
|
909
|
+
&& !!verification && (verification.source === "explicit" || verification.source === "npm-config")
|
|
910
|
+
&& sameVerification(verification, current.parts.verification);
|
|
911
|
+
}
|
|
882
912
|
/**
|
|
883
913
|
* T4 (OBS-266): the journal's LAST verification cycle — the (tip, cmdHash) pair the most recent run
|
|
884
914
|
* of the verify commands spoke for, the gates it got a pass from, and whether anything failed in it.
|
|
@@ -900,7 +930,7 @@ function lastVerifyCycle(events) {
|
|
|
900
930
|
if (e.event === "tip-verify-start") {
|
|
901
931
|
const { tip, cmdHash } = e.data;
|
|
902
932
|
cur = typeof tip === "string" && typeof cmdHash === "string"
|
|
903
|
-
? { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [e.data.capacity], evidenceByGate: new Map() }
|
|
933
|
+
? { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [e.data.capacity], tipEnvironments: [e.data.tipEnvironment], evidenceByGate: new Map() }
|
|
904
934
|
: undefined;
|
|
905
935
|
afterRunEnd = false;
|
|
906
936
|
continue;
|
|
@@ -923,7 +953,7 @@ function lastVerifyCycle(events) {
|
|
|
923
953
|
continue;
|
|
924
954
|
}
|
|
925
955
|
if (!cur || afterRunEnd || cur.tip !== tip || cur.cmdHash !== cmdHash) {
|
|
926
|
-
cur = { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [], evidenceByGate: new Map() };
|
|
956
|
+
cur = { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [], tipEnvironments: [], evidenceByGate: new Map() };
|
|
927
957
|
}
|
|
928
958
|
afterRunEnd = false;
|
|
929
959
|
// T7: EVERY verdict row's own capacity, not just the start row's. The start row is a statement of
|
|
@@ -931,6 +961,7 @@ function lastVerifyCycle(events) {
|
|
|
931
961
|
// these rows, so a row whose capacity differs from the session's — or which is malformed — has to
|
|
932
962
|
// be able to sink the cycle by itself.
|
|
933
963
|
cur.capacities.push(e.data.capacity);
|
|
964
|
+
cur.tipEnvironments.push(e.data.tipEnvironment);
|
|
934
965
|
if (e.event === "tip-verify-failed")
|
|
935
966
|
cur.failed = true;
|
|
936
967
|
else {
|
|
@@ -1200,13 +1231,13 @@ export function identityDetails(leg, state, branch, runId, expected) {
|
|
|
1200
1231
|
*/
|
|
1201
1232
|
export function appendIdentityFailure(journal, f) {
|
|
1202
1233
|
if (!f.started)
|
|
1203
|
-
journal.append("tip-verify-start", undefined, { tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, gates: [], cached: false });
|
|
1234
|
+
journal.append("tip-verify-start", undefined, { tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, tipEnvironment: f.tipEnvironment, gates: [], cached: false });
|
|
1204
1235
|
const found = f.state?.kind === "regular" || f.state?.kind === "symbolic" ? f.state.commit : undefined;
|
|
1205
1236
|
journal.append("tip-verify-failed", undefined, {
|
|
1206
1237
|
gate: "integration", cmd: "", exitCode: 1, fingerprints: [], details: f.details, ...(f.infra ? { infra: true } : {}),
|
|
1207
1238
|
integration: { branch: f.branch, expected: f.expected ?? null, found: found ?? null, leg: f.leg,
|
|
1208
1239
|
...(f.state?.kind === "symbolic" ? { target: f.state.target } : {}) },
|
|
1209
|
-
tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity,
|
|
1240
|
+
tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, tipEnvironment: f.tipEnvironment,
|
|
1210
1241
|
});
|
|
1211
1242
|
}
|
|
1212
1243
|
/** O: I(expected) — or, with no expected value, the ref must be regular and its commit becomes the expected. */
|
|
@@ -1267,16 +1298,12 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1267
1298
|
opts.signal?.throwIfAborted();
|
|
1268
1299
|
const cmdHash = commandsHash(commands);
|
|
1269
1300
|
const { identity } = opts;
|
|
1270
|
-
//
|
|
1271
|
-
// green must match, beside the tip and the command set. A cached verdict is the one place a green
|
|
1272
|
-
// crosses a session boundary with nothing re-run, and a session resumed at a different capacity
|
|
1273
|
-
// divides the machine by a different number: that green was established in another world, so it is
|
|
1274
|
-
// not carried forward and the commands run again. A pre-T7 cycle records no capacity and keeps
|
|
1275
|
-
// exactly the behaviour it has today.
|
|
1301
|
+
// Queue row 24 (D-2134): measure once against the repository whose installed packages the verifier receives.
|
|
1276
1302
|
const capacity = resolvedCapacity();
|
|
1303
|
+
const tipEnvironment = measureTipEnvironment(identity?.repo ?? intWt, capacity);
|
|
1277
1304
|
const identityFailure = (f) => {
|
|
1278
|
-
appendIdentityFailure(journal, { ...f, branch: identity.branch, cmdHash, capacity });
|
|
1279
|
-
return { failed: true, identityFailed: true, deferred: [] };
|
|
1305
|
+
appendIdentityFailure(journal, { ...f, branch: identity.branch, cmdHash, capacity, tipEnvironment });
|
|
1306
|
+
return { failed: true, identityFailed: true, deferred: [], tipEnvironment };
|
|
1280
1307
|
};
|
|
1281
1308
|
let tip;
|
|
1282
1309
|
let clean = true; // with `identity` the battery runs in a fresh checkout and K checks tracked content
|
|
@@ -1295,25 +1322,30 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1295
1322
|
const gates = Object.keys(commands).filter((g) => g !== "tipTest");
|
|
1296
1323
|
if (!gates.includes("test") && commands.tipTest)
|
|
1297
1324
|
gates.push("test");
|
|
1325
|
+
// Queue row 24 (D-2134 R2/Q6/Q7): carry requires tip, commands, every gate and capacity AND the stamped runner
|
|
1326
|
+
// and environment identity with a measured lifecycle on every row. An unstamped cycle re-verifies once.
|
|
1327
|
+
// Package content drift under equal lockfile and links is the declared residual, as in per-gate reuse.
|
|
1298
1328
|
const cached = last !== undefined && !last.failed && !last.forgiven && last.tip === tip && last.cmdHash === cmdHash
|
|
1299
1329
|
&& gates.every((g) => last.gates.has(g))
|
|
1300
|
-
&& last.capacities.every((recorded) => sameCapacity(recorded, capacity))
|
|
1330
|
+
&& last.capacities.every((recorded) => sameCapacity(recorded, capacity))
|
|
1331
|
+
&& last.tipEnvironments.every((recorded) => sameTipEnvironment(recorded, tipEnvironment))
|
|
1332
|
+
&& !dependencyLinkRefusal(identity?.repo ?? intWt);
|
|
1301
1333
|
// A pair can be verified red and then green without either SHA or command hash changing (for
|
|
1302
1334
|
// example, an external service or ignored fixture recovers). Delimit attempts explicitly so that
|
|
1303
1335
|
// the earlier red cannot remain latched into the later complete green cycle.
|
|
1304
|
-
journal.append("tip-verify-start", undefined, { tip, cmdHash, capacity, gates, cached: clean && cached });
|
|
1336
|
+
journal.append("tip-verify-start", undefined, { tip, cmdHash, capacity, tipEnvironment, gates, cached: clean && cached });
|
|
1305
1337
|
opts.signal?.throwIfAborted();
|
|
1306
1338
|
if (clean && cached) {
|
|
1307
|
-
journal.append("tip-verify-cached", undefined, { tip, cmdHash, capacity, gates });
|
|
1339
|
+
journal.append("tip-verify-cached", undefined, { tip, cmdHash, capacity, tipEnvironment, gates });
|
|
1308
1340
|
// The skip must not read as a red. Every surface derives the tip's verdict from this cycle's
|
|
1309
1341
|
// `tip-verify` events (cockpit derive.ts tipVerificationPassed: a run-end claiming "passed" with
|
|
1310
1342
|
// ZERO events is fail-closed to FALSE), so a carried-forward green still journals its per-gate
|
|
1311
1343
|
// pass — `cached: true` keeps it honest about not having re-run the command.
|
|
1312
1344
|
for (const gate of gates) {
|
|
1313
1345
|
const cmd = gate === "test" && commands.tipTest ? commands.tipTest : commands[gate];
|
|
1314
|
-
journal.append("tip-verify", undefined, { ...reusedTipEvidence(last.evidenceByGate.get(gate) ?? {}), gate, cmd, pass: true, exitCode: 0, cached: true, tip, cmdHash, capacity });
|
|
1346
|
+
journal.append("tip-verify", undefined, { ...reusedTipEvidence(last.evidenceByGate.get(gate) ?? {}), gate, cmd, pass: true, exitCode: 0, cached: true, tip, cmdHash, capacity, tipEnvironment });
|
|
1315
1347
|
}
|
|
1316
|
-
return identity ? { failed: false, expected: tip, identityFailed: false, deferred: [] } : false;
|
|
1348
|
+
return identity ? { failed: false, expected: tip, identityFailed: false, deferred: [], tipEnvironment } : false;
|
|
1317
1349
|
}
|
|
1318
1350
|
const battery = async (dir) => {
|
|
1319
1351
|
let tipFailed = false;
|
|
@@ -1336,7 +1368,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1336
1368
|
};
|
|
1337
1369
|
if (r.pass) {
|
|
1338
1370
|
// Q121s: a forgiven pass journals its fingerprints — honest about what was carried, never a silent green.
|
|
1339
|
-
journal.append("tip-verify", undefined, { ...evidence, 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, ...(r.baselineProvenance ? { baselineProvenance: r.baselineProvenance } : {}) } : {}), tip, cmdHash, capacity: measuredCapacity });
|
|
1371
|
+
journal.append("tip-verify", undefined, { ...evidence, 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, ...(r.baselineProvenance ? { baselineProvenance: r.baselineProvenance } : {}) } : {}), tip, cmdHash, capacity: measuredCapacity, tipEnvironment });
|
|
1340
1372
|
}
|
|
1341
1373
|
else {
|
|
1342
1374
|
journal.append("tip-verify-failed", undefined, {
|
|
@@ -1351,6 +1383,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1351
1383
|
tip,
|
|
1352
1384
|
cmdHash,
|
|
1353
1385
|
capacity: measuredCapacity,
|
|
1386
|
+
tipEnvironment,
|
|
1354
1387
|
});
|
|
1355
1388
|
tipFailed = true;
|
|
1356
1389
|
}
|
|
@@ -1368,7 +1401,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1368
1401
|
const changed = await checkoutChange(opened.dir, tip, opened.h0);
|
|
1369
1402
|
if (changed)
|
|
1370
1403
|
return identityFailure({ started: true, expected: tip, leg: "checkout", details: `verification checkout changed during the battery: ${changed}` });
|
|
1371
|
-
return { failed: tipFailed, expected: tip, identityFailed: false, deferred };
|
|
1404
|
+
return { failed: tipFailed, expected: tip, identityFailed: false, deferred, tipEnvironment };
|
|
1372
1405
|
}
|
|
1373
1406
|
finally {
|
|
1374
1407
|
if (checkout)
|
|
@@ -3235,11 +3268,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3235
3268
|
];
|
|
3236
3269
|
return `${identity} — release with ${commands.map((command) => `\`${command}\``).join(" or ")}`;
|
|
3237
3270
|
};
|
|
3238
|
-
// Lexing never starts in the middle of a word. Resolution is separate so missing hints
|
|
3239
|
-
// remain visible evidence without becoming executable approval advice.
|
|
3240
|
-
const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
|
|
3241
|
-
.map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
|
|
3242
|
-
.filter((path) => !path.split("/").includes(".."));
|
|
3243
3271
|
const diffPaths = async (base, wt) => {
|
|
3244
3272
|
const out = await shGit(`git diff --name-only -z ${shq(base)}..HEAD`, wt);
|
|
3245
3273
|
return new Set(out.code === 0 ? out.stdout.split("\0").filter(Boolean) : []);
|
|
@@ -3381,7 +3409,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3381
3409
|
const worker = journal.read().reverse().find((e) => e.taskId === t.id && e.event === "worker-result");
|
|
3382
3410
|
const disposition = classifyRepairDisposition({
|
|
3383
3411
|
// OBS-1072 add.2: hints are lexed from the readable excerpt, never from terminal control bytes.
|
|
3384
|
-
results: results.map((g) => ({ ...g, details: readableExcerpt(g.details)
|
|
3412
|
+
results: results.map((g) => ({ ...g, details: readableExcerpt(g.details),
|
|
3413
|
+
// Queue row 235 (D-2134): oracle output has the same readable boundary as gate details.
|
|
3414
|
+
...(Array.isArray(g.meta?.oracles) ? { meta: { ...g.meta, oracles: g.meta.oracles.map((oracle) => oracle && typeof oracle === "object" && typeof oracle.output === "string"
|
|
3415
|
+
? { ...oracle, output: readableExcerpt(oracle.output) } : oracle) } } : {}),
|
|
3416
|
+
})), files: t.files, inventory: changed,
|
|
3385
3417
|
refusalSummary: worker?.data.ok === false && typeof worker.data.summary === "string" ? worker.data.summary : undefined,
|
|
3386
3418
|
});
|
|
3387
3419
|
for (const diagnostic of disposition.diagnostics) {
|
|
@@ -3526,7 +3558,20 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3526
3558
|
const ref = await preserveWorktree(priorWt, producer);
|
|
3527
3559
|
if (ref)
|
|
3528
3560
|
journal.append("worktree-preserved", t.id, { ref, ...producerFields(producer) });
|
|
3529
|
-
|
|
3561
|
+
// Queue row 223 (D-2118): only this typed add race buys retries, with three adds total.
|
|
3562
|
+
const addWorktree = () => {
|
|
3563
|
+
return driver.worktree(repoRoot, taskBranch, taskBase);
|
|
3564
|
+
};
|
|
3565
|
+
for (let attempt = 1;; attempt++) {
|
|
3566
|
+
try {
|
|
3567
|
+
return await addWorktree();
|
|
3568
|
+
}
|
|
3569
|
+
catch (error) {
|
|
3570
|
+
if (!(error instanceof WorktreeAdminRaceError) || attempt >= 3)
|
|
3571
|
+
throw error;
|
|
3572
|
+
journal.append("worktree-add-retry", t.id, { attempt, nextAttempt: attempt + 1, evidence: error.evidence });
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3530
3575
|
};
|
|
3531
3576
|
// A dead worker with a clean checkout still needs a durable recovery handle: there may be no
|
|
3532
3577
|
// pane left to identify even the commit it was dispatched from. preserveWorktree deliberately
|
|
@@ -3883,8 +3928,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3883
3928
|
...(typeof g.meta.producerAttempt === "number" ? { producerAttempt: g.meta.producerAttempt } : {}),
|
|
3884
3929
|
} : {}),
|
|
3885
3930
|
...((cfg.executionPolicy || repairSelection) && !g.pass ? { disposition: failureDisposition(g) } : {}),
|
|
3886
|
-
...Object.fromEntries(["runnerInfraRerun", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
|
|
3887
|
-
"forgivenFingerprints", "freshFingerprints", "baselineProvenance", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
|
|
3931
|
+
...Object.fromEntries(["runnerInfraRerun", "judgeIndependent", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
|
|
3932
|
+
"forgivenFingerprints", "freshFingerprints", "baselineProvenance", "oracles", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
|
|
3888
3933
|
.filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
|
|
3889
3934
|
// OBS-540: preserve terminal-vs-retryable infra exactly. normalizeGateOutcome deliberately
|
|
3890
3935
|
// defaults a legacy infra row to retryable, so dropping an explicit false here reverses the
|
|
@@ -5310,17 +5355,19 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5310
5355
|
let cpuFlat;
|
|
5311
5356
|
let cpuAccountant;
|
|
5312
5357
|
let cpuGapCount = 0;
|
|
5313
|
-
|
|
5314
|
-
//
|
|
5358
|
+
const unmeasurableNoted = new Set();
|
|
5359
|
+
// Queue row 231 (D-2119): one line per reading per attempt, including the connection leg.
|
|
5360
|
+
// One CPU line per attempt, whichever way that leg turns out to be unmeasurable. A triad that
|
|
5315
5361
|
// can never conclude is this feature silently ABSENT — on a host whose `ps` the probe cannot
|
|
5316
5362
|
// read, every stall would ride its whole window out again with nothing saying why. Named once
|
|
5317
5363
|
// per attempt, not per slice: the condition is structural, and a per-slice line would bury it.
|
|
5318
|
-
const noteUnmeasurable = (reason) => {
|
|
5319
|
-
if (unmeasurableNoted)
|
|
5364
|
+
const noteUnmeasurable = (reason, reading = "cpu") => {
|
|
5365
|
+
if (unmeasurableNoted.has(reading))
|
|
5320
5366
|
return;
|
|
5321
|
-
unmeasurableNoted
|
|
5367
|
+
unmeasurableNoted.add(reading);
|
|
5322
5368
|
journal.append("worker-harvest-unmeasurable", t.id, { slot: slot.name, attempt, reason });
|
|
5323
5369
|
};
|
|
5370
|
+
let harvestHeldNoted = false;
|
|
5324
5371
|
// The CPU leg needs a marker in the worker's own argv, and every launch path puts this
|
|
5325
5372
|
// attempt's dispatch script there EXCEPT interactiveSeed: runInteractiveSeed launches the
|
|
5326
5373
|
// TUI directly, by a command the ADAPTER owns (seed.launch(model)) which tickmarkr cannot
|
|
@@ -5382,6 +5429,39 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5382
5429
|
const carried = await commitsAheadOf(taskBase, wt);
|
|
5383
5430
|
if (carried.length === 0)
|
|
5384
5431
|
return false; // nothing landed: not this branch's population
|
|
5432
|
+
if (!interactive) {
|
|
5433
|
+
// Queue row 231 (D-2119): query afresh only after the other legs hold. lsof ORs selections
|
|
5434
|
+
// without -a; -g includes connector children in the owned group, not just the CLI root.
|
|
5435
|
+
// No environment inspection. One async PATH lookup, bounded from invocation like the
|
|
5436
|
+
// reap census (15 s); SIGKILL also retires a probe that ignores SIGTERM.
|
|
5437
|
+
const group = readOwnedProcessGroup(groupFile);
|
|
5438
|
+
const connection = group === undefined ? "unreadable" : await new Promise((resolve) => {
|
|
5439
|
+
try {
|
|
5440
|
+
execFile("lsof", ["-nP", "-a", "-g", String(group), "-iTCP", "-sTCP:ESTABLISHED", "-Fp"], { encoding: "utf8", timeout: 15_000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
5441
|
+
if (!error && stdout.trim())
|
|
5442
|
+
resolve("connected");
|
|
5443
|
+
else if (error && error.code === 1 && !stdout && !stderr)
|
|
5444
|
+
resolve("none");
|
|
5445
|
+
else
|
|
5446
|
+
resolve("unreadable");
|
|
5447
|
+
});
|
|
5448
|
+
}
|
|
5449
|
+
catch {
|
|
5450
|
+
resolve("unreadable");
|
|
5451
|
+
}
|
|
5452
|
+
});
|
|
5453
|
+
if (connection === "unreadable") {
|
|
5454
|
+
noteUnmeasurable("the worker ESTABLISHED TCP connection reading could not be read", "connection");
|
|
5455
|
+
return false;
|
|
5456
|
+
}
|
|
5457
|
+
if (connection === "connected") {
|
|
5458
|
+
if (!harvestHeldNoted) {
|
|
5459
|
+
harvestHeldNoted = true;
|
|
5460
|
+
journal.append("worker-harvest-held", t.id, { slot: slot.name, attempt, reason: "established-tcp-connection", processGroup: group });
|
|
5461
|
+
}
|
|
5462
|
+
return false;
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5385
5465
|
journal.append("worker-harvest", t.id, {
|
|
5386
5466
|
slot: slot.name, attempt, commits: carried.length,
|
|
5387
5467
|
silentMs, cpuMs: leg.cpu.ms, cpuResolutionMs: leg.cpu.resolutionMs,
|
|
@@ -5860,13 +5940,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5860
5940
|
lastStatus = st;
|
|
5861
5941
|
journal.append("worker-status", t.id, { slot: slot.name, status: st, attempt });
|
|
5862
5942
|
}
|
|
5863
|
-
|
|
5943
|
+
// Queue row 215 (D-2118 add.1): an unhooked CLI can report unknown at its startup modal.
|
|
5944
|
+
// Bound that status to worker-launch's startup window, including time spent reading.
|
|
5945
|
+
const withinTrustStartup = () => workerLaunchedAt !== undefined
|
|
5946
|
+
&& Date.now() - workerLaunchedAt <= workerStartupWindowMs;
|
|
5947
|
+
if (st === "blocked" || st === "idle" || (st === "unknown" && withinTrustStartup())) {
|
|
5864
5948
|
// T5: once-per-slot auto-answer when the adapter declares a trust dialog and the pane
|
|
5865
5949
|
// text matches. tickmarkr created the worktree from the operator's own repo — safe by construction.
|
|
5866
5950
|
if (!trustAnswered && adapter.trustDialog && driver.sendKey) {
|
|
5867
5951
|
try {
|
|
5868
|
-
const paneText = await workerTransport.read(slot, 80);
|
|
5869
|
-
if (matchesTrustDialog(paneText, adapter.trustDialog)) {
|
|
5952
|
+
const paneText = await (slot.readScreen ? slot.readScreen() : workerTransport.read(slot, 80));
|
|
5953
|
+
if ((st !== "unknown" || withinTrustStartup()) && matchesTrustDialog(paneText, adapter.trustDialog)) {
|
|
5870
5954
|
trustAnswered = true;
|
|
5871
5955
|
trustAnsweredSlots.add(slot.id);
|
|
5872
5956
|
// v1.25 T1: audit trail for live runs — prove the dialog appeared and was answered.
|
|
@@ -6092,9 +6176,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6092
6176
|
}
|
|
6093
6177
|
// OBS-548: the FOURTH leg, and the one the daemon already measured. The three legs above
|
|
6094
6178
|
// are all channel-side: they say a pane stopped talking. The tree's CPU says whether
|
|
6095
|
-
// anything is still WORKING, and the harvest triad forty lines up refuses
|
|
6096
|
-
// conclusion without it
|
|
6097
|
-
//
|
|
6179
|
+
// anything is still WORKING, and the harvest triad forty lines up refuses this
|
|
6180
|
+
// conclusion without it. Queue row 231 (D-2119): print-mode harvest additionally checks
|
|
6181
|
+
// connections because a remote model turn can have flat CPU. An UNMEASURABLE
|
|
6098
6182
|
// reading keeps the fail-open contract the probe already states (undefined is never
|
|
6099
6183
|
// evidence a worker stopped), so on a host whose `ps` cannot be read the fast-kill stands
|
|
6100
6184
|
// down and the fixed attempt ceiling bounds the hold.
|
|
@@ -6281,13 +6365,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6281
6365
|
}
|
|
6282
6366
|
await noteLaunched();
|
|
6283
6367
|
// OBS-54: headless workers have the same output-inactivity budget as visible panes.
|
|
6284
|
-
//
|
|
6368
|
+
// Queue row 233 (D-2130): print progress is the driver's received-byte count, never a row
|
|
6369
|
+
// high-water over the bounded tail; a driver reporting no count keeps the row measure.
|
|
6285
6370
|
const stallWindowMs = taskTimeoutMinutes * 60_000;
|
|
6371
|
+
const hardDeadline = attemptStart + (attemptHardTimeoutMs ?? stallWindowMs * 4);
|
|
6286
6372
|
const initialPane = await workerTransport.read(slot, 500);
|
|
6287
6373
|
startupFailure = startupFailureInWindow(initialPane, workerLaunchedAt);
|
|
6288
6374
|
let everHadOutput = initialPane.length > 0;
|
|
6289
6375
|
const stallProgress = new StallProgressTracker();
|
|
6290
|
-
stallProgress.observe({ paneText: initialPane, seedSubmitted: true, contextTokens });
|
|
6376
|
+
stallProgress.observe({ paneText: initialPane, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, seedSubmitted: true, contextTokens });
|
|
6291
6377
|
let lastProgressAt = Date.now();
|
|
6292
6378
|
finished = false;
|
|
6293
6379
|
// T2 review (material): the exit marker proves the PROCESS EXITED, never that the worker
|
|
@@ -6297,10 +6383,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6297
6383
|
// worker-result-harvested row. The interactive site has always kept them apart (`finished`
|
|
6298
6384
|
// there is the trailer regex; the exit marker only sets exitCode), and the cause taxonomy
|
|
6299
6385
|
// already names this shape "clean-exit-no-trailer" — unreachable in print mode until now.
|
|
6300
|
-
while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs) {
|
|
6386
|
+
while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs && Date.now() < hardDeadline) {
|
|
6301
6387
|
fatalStop.signal.throwIfAborted();
|
|
6302
6388
|
executionSignal()?.throwIfAborted();
|
|
6303
|
-
const remaining = stallWindowMs - (Date.now() - lastProgressAt);
|
|
6389
|
+
const remaining = Math.min(stallWindowMs - (Date.now() - lastProgressAt), hardDeadline - Date.now());
|
|
6304
6390
|
let slice = Math.min(BLOCKED_POLL_MS, Math.max(100, Math.min(stallWindowMs / 2, remaining)));
|
|
6305
6391
|
const startupLeft = workerLaunchedAt === undefined
|
|
6306
6392
|
? 0
|
|
@@ -6331,11 +6417,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6331
6417
|
break;
|
|
6332
6418
|
}
|
|
6333
6419
|
await sampleContext();
|
|
6334
|
-
if (stallProgress.observe({ paneText, contextTokens }))
|
|
6420
|
+
if (stallProgress.observe({ paneText, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, contextTokens }))
|
|
6335
6421
|
lastProgressAt = Date.now();
|
|
6336
|
-
//
|
|
6337
|
-
//
|
|
6338
|
-
// window out before anything looked at its commits.
|
|
6422
|
+
// Queue row 231 (D-2119): print mode adds a fresh owned-group connection reading to the
|
|
6423
|
+
// silence/commits/CPU triad; a quiet remote model turn can hold a connection with flat CPU.
|
|
6339
6424
|
// No nudge hold here, deliberately: print mode has no nudge surface at all (no pane to
|
|
6340
6425
|
// steer, driver.nudge is never consulted on this path), so there is no pending daemon
|
|
6341
6426
|
// action for the triad to preempt — the asymmetry with the interactive call site above is
|
|
@@ -6350,7 +6435,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6350
6435
|
// without one is finished:false with a non-null exitCode — the harvest synthesis then owns
|
|
6351
6436
|
// it when the worktree carries work, and classifyWorkerResultCause names it otherwise.
|
|
6352
6437
|
finished = new RegExp(trailerPattern(nonce)).test(output);
|
|
6353
|
-
timedOut = !processExited && !finished && Date.now() - lastProgressAt >= stallWindowMs;
|
|
6438
|
+
timedOut = !processExited && !finished && (Date.now() - lastProgressAt >= stallWindowMs || Date.now() >= hardDeadline);
|
|
6354
6439
|
}
|
|
6355
6440
|
}
|
|
6356
6441
|
finally {
|
|
@@ -7101,7 +7186,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7101
7186
|
// Budget spent means the FRESH LADDER owns this failure — including a review-only one, whose
|
|
7102
7187
|
// same-channel fix retry is exactly the round the budget just declared too expensive to repeat.
|
|
7103
7188
|
const repairExhausted = repairable && !repair;
|
|
7104
|
-
|
|
7189
|
+
const fundedRepair = repair && !capStep;
|
|
7190
|
+
if (fundedRepair) {
|
|
7105
7191
|
journal.append("repair-attempt", t.id, {
|
|
7106
7192
|
repair: repairHistory.length + 1, charge: repairsDrawn + 1, of: MAX_REPAIRS,
|
|
7107
7193
|
gates: failing.map((g) => g.gate),
|
|
@@ -7126,8 +7212,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7126
7212
|
});
|
|
7127
7213
|
await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
|
|
7128
7214
|
}
|
|
7215
|
+
// Queue row 224(b) (D-2118): pinned tasks park on the first unfunded owned deterministic red
|
|
7216
|
+
// at or after the second, so this climb fires at most once. Unpinned tasks climb on exactly
|
|
7217
|
+
// the second owned deterministic red, even when it funds a repair.
|
|
7129
7218
|
const climbCause = repairExhausted ? "repair-exhausted"
|
|
7130
|
-
: deterministic &&
|
|
7219
|
+
: deterministic && (t.routingHints?.pin
|
|
7220
|
+
? ownedGateFailures >= 2 && !fundedRepair
|
|
7221
|
+
: ownedGateFailures === 2) ? "gate-fail" : undefined;
|
|
7131
7222
|
if (climbCause) {
|
|
7132
7223
|
const climbed = await climb(climbCause, deterministic ?? failing[0], attempt + 1);
|
|
7133
7224
|
if (climbed === "parked")
|
|
@@ -7240,6 +7331,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7240
7331
|
if (fatalStop.signal.aborted)
|
|
7241
7332
|
return;
|
|
7242
7333
|
const cleanupEvidence = cleanupErrors.length ? { cleanupErrors } : {};
|
|
7334
|
+
if (err instanceof WorktreeAdminRaceError) {
|
|
7335
|
+
await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { evidence: err.evidence, ...cleanupEvidence });
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7243
7338
|
if (err instanceof HostDegradedError) {
|
|
7244
7339
|
await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { disposition: "host-degraded", ...cleanupEvidence });
|
|
7245
7340
|
return;
|
|
@@ -7416,8 +7511,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7416
7511
|
summary.tipProof = { kind: "incomplete" };
|
|
7417
7512
|
}
|
|
7418
7513
|
else {
|
|
7419
|
-
|
|
7420
|
-
|
|
7514
|
+
const capacity = resolvedCapacity();
|
|
7515
|
+
const tipEnvironment = measureTipEnvironment(repoRoot, capacity);
|
|
7516
|
+
appendIdentityFailure(journal, { started: false, branch, ...opened, cmdHash: commandsHash(commands), capacity, tipEnvironment });
|
|
7517
|
+
tipClose = { failed: true, identityFailed: true, deferred: [], tipEnvironment };
|
|
7421
7518
|
summary.tipVerify = "failed";
|
|
7422
7519
|
summary.tipProof = runEndTipProof(journal.read());
|
|
7423
7520
|
}
|
|
@@ -7452,8 +7549,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7452
7549
|
if (!tipClose.identityFailed && tipClose.expected !== undefined) {
|
|
7453
7550
|
const closing = await checkIntegrationIdentity(repoRoot, branch, tipClose.expected);
|
|
7454
7551
|
if (!closing.ok) {
|
|
7552
|
+
const capacity = resolvedCapacity();
|
|
7553
|
+
const tipEnvironment = tipStarted ? tipClose.tipEnvironment : measureTipEnvironment(repoRoot, capacity);
|
|
7455
7554
|
appendIdentityFailure(journal, { started: tipStarted, branch, expected: tipClose.expected, leg: closing.leg, state: closing.state,
|
|
7456
|
-
details: identityDetails(closing.leg, closing.state, branch, runId, tipClose.expected), cmdHash: commandsHash(commands), capacity
|
|
7555
|
+
details: identityDetails(closing.leg, closing.state, branch, runId, tipClose.expected), cmdHash: commandsHash(commands), capacity, tipEnvironment });
|
|
7457
7556
|
summary.tipVerify = "failed";
|
|
7458
7557
|
summary.tipProof = runEndTipProof(journal.read());
|
|
7459
7558
|
}
|
package/dist/run/git.d.ts
CHANGED
|
@@ -199,6 +199,11 @@ export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions):
|
|
|
199
199
|
export declare function gitSync(dir: string, args: readonly string[], { env }?: {
|
|
200
200
|
env?: NodeJS.ProcessEnv;
|
|
201
201
|
}): SpawnSyncReturns<string>;
|
|
202
|
+
/** Queue row 223 (D-2118): git could not read a sibling's partially written admin file during add. */
|
|
203
|
+
export declare class WorktreeAdminRaceError extends Error {
|
|
204
|
+
readonly evidence: string;
|
|
205
|
+
constructor(evidence: string);
|
|
206
|
+
}
|
|
202
207
|
/**
|
|
203
208
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
204
209
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
package/dist/run/git.js
CHANGED
|
@@ -621,6 +621,41 @@ export function gitSync(dir, args, { env = process.env } = {}) {
|
|
|
621
621
|
pin?.release();
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
|
+
// Queue row 223 (D-2118): one process-wide administration queue; recreation and rollback retain
|
|
625
|
+
// their caller's hold, including trust rechecks and branch restoration, without queuing behind it.
|
|
626
|
+
let worktreeAdminTail = Promise.resolve();
|
|
627
|
+
const worktreeAdminHold = new AsyncLocalStorage();
|
|
628
|
+
async function withWorktreeAdmin(operation) {
|
|
629
|
+
if (worktreeAdminHold.getStore())
|
|
630
|
+
return operation();
|
|
631
|
+
const prior = worktreeAdminTail;
|
|
632
|
+
let release;
|
|
633
|
+
worktreeAdminTail = new Promise((done) => { release = done; });
|
|
634
|
+
await prior;
|
|
635
|
+
try {
|
|
636
|
+
return await worktreeAdminHold.run(true, operation);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
release();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/** Queue row 223 (D-2118): git could not read a sibling's partially written admin file during add. */
|
|
643
|
+
export class WorktreeAdminRaceError extends Error {
|
|
644
|
+
evidence;
|
|
645
|
+
constructor(evidence) {
|
|
646
|
+
super(`worktree add encountered a sibling admin read race (queue row 223, D-2118): ${evidence}`);
|
|
647
|
+
this.evidence = evidence;
|
|
648
|
+
this.name = "WorktreeAdminRaceError";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const siblingAdminReadFailed = (stderr) => {
|
|
652
|
+
for (const line of stderr.split("\n")) {
|
|
653
|
+
const read = /^(?:fatal: |error: )?failed to read (?:'([^'\r\n]+)'|"([^"\r\n]+)"|([^:\r\n]+))(?::|$)/.exec(line);
|
|
654
|
+
if (read && /(?:^|\/)worktrees\/[^/\r\n]+\/(?:commondir|gitdir|HEAD)$/.test(read[1] ?? read[2] ?? read[3]))
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
};
|
|
624
659
|
/**
|
|
625
660
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
626
661
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
|
@@ -633,6 +668,9 @@ export function gitSync(dir, args, { env = process.env } = {}) {
|
|
|
633
668
|
* back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
|
|
634
669
|
*/
|
|
635
670
|
export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, { logHead = false } = {}) {
|
|
671
|
+
return withWorktreeAdmin(() => addDetachedWorktreeHeld(cwd, dir, commitish, timeoutMs, logHead));
|
|
672
|
+
}
|
|
673
|
+
async function addDetachedWorktreeHeld(cwd, dir, commitish, timeoutMs, logHead) {
|
|
636
674
|
// 2.8.1 T1 (ANL-023 §7.1): `logHead` writes the new checkout's HEAD reflog whatever the repository's
|
|
637
675
|
// core.logAllRefUpdates says, so a later HEAD move by any command is appended to an established witness.
|
|
638
676
|
const git = logHead ? "git -c core.logAllRefUpdates=always" : "git";
|
|
@@ -682,6 +720,9 @@ function rollbackFailed(error, failure) {
|
|
|
682
720
|
* step bounded by `timeoutMs`; every step is tried, and any that failed is thrown, never swallowed.
|
|
683
721
|
*/
|
|
684
722
|
async function dropWorktree(store, { dir, branch }, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
|
723
|
+
return withWorktreeAdmin(() => dropWorktreeHeld(store, { dir, branch }, timeoutMs));
|
|
724
|
+
}
|
|
725
|
+
async function dropWorktreeHeld(store, { dir, branch }, timeoutMs) {
|
|
685
726
|
const failed = await withoutExecutionBudget(async () => {
|
|
686
727
|
const run = async (cmd) => {
|
|
687
728
|
try {
|
|
@@ -859,6 +900,9 @@ export function checkoutIncarnation(worktree) {
|
|
|
859
900
|
}
|
|
860
901
|
}
|
|
861
902
|
export async function createWorktree(repo, branch, baseRef) {
|
|
903
|
+
return withWorktreeAdmin(() => createWorktreeHeld(repo, branch, baseRef));
|
|
904
|
+
}
|
|
905
|
+
async function createWorktreeHeld(repo, branch, baseRef) {
|
|
862
906
|
branch = await resolveTaskBranch(repo, branch);
|
|
863
907
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
864
908
|
if (existsSync(dir))
|
|
@@ -866,7 +910,13 @@ export async function createWorktree(repo, branch, baseRef) {
|
|
|
866
910
|
// the worktree capability: a branch add, whose symbolic HEAD keeps the ref-store pin (see OwnGitCapability); a raced
|
|
867
911
|
// add is rolled back, its branch restored to the value read here or deleted when it is new
|
|
868
912
|
const prior = (await shGit(`git rev-parse --verify -q ${shq(`refs/heads/${branch}`)}`, repo)).stdout.trim() || undefined;
|
|
869
|
-
|
|
913
|
+
const cmd = `git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`;
|
|
914
|
+
const added = await shGit(cmd, repo, DEFAULT_SHELL_TIMEOUT_MS, { createsWorktree: "branch", rollback: { dir, branch: { name: branch, prior } } });
|
|
915
|
+
if (added.code !== 0) {
|
|
916
|
+
if (siblingAdminReadFailed(added.stderr))
|
|
917
|
+
throw new WorktreeAdminRaceError(added.stderr.trim());
|
|
918
|
+
throw new Error(`command failed (${added.code}): ${cmd}\n${added.stderr || added.stdout}`);
|
|
919
|
+
}
|
|
870
920
|
linkNodeModules(repo, dir);
|
|
871
921
|
return dir;
|
|
872
922
|
}
|
package/dist/run/journal.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
8
8
|
import { graphDefinitionHash, stateDirName, taskContentDigest, taskDefinitionFingerprint, tickmarkrDir } from "../graph/graph.js";
|
|
@@ -2744,7 +2744,16 @@ function dischargeProblem(row, o, events, cwd, memo) {
|
|
|
2744
2744
|
return "discharge range does not bind the obligation";
|
|
2745
2745
|
let artifact;
|
|
2746
2746
|
try {
|
|
2747
|
-
|
|
2747
|
+
let artifactPath = String(d.artifactPath);
|
|
2748
|
+
if (!isAbsolute(artifactPath)) {
|
|
2749
|
+
// Queue row 31 / D-2134: resolve within the obligation's own run, with common-root fallback
|
|
2750
|
+
// only when that run is absent locally; legacy absolute paths keep their original meaning.
|
|
2751
|
+
const id = parseRunId(o.runId);
|
|
2752
|
+
const localRun = join(runsDir(cwd), id);
|
|
2753
|
+
const runDir = existsSync(localRun) ? localRun : join(runsDir(dirname(trustedCommonDir(cwd))), id);
|
|
2754
|
+
artifactPath = join(runDir, artifactPath);
|
|
2755
|
+
}
|
|
2756
|
+
const bytes = readFileSync(artifactPath);
|
|
2748
2757
|
if (createHash("sha256").update(bytes).digest("hex") !== d.artifactSha256)
|
|
2749
2758
|
return "artifact hash mismatch";
|
|
2750
2759
|
artifact = JSON.parse(bytes.toString("utf8")) ?? {};
|