tickmarkr 2.8.7 → 2.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/cli/commands/compile.js +1 -1
- 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/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- 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 +221 -42
- package/dist/run/git.d.ts +7 -2
- package/dist/run/git.js +37 -4
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +33 -2
- package/dist/run/wall-budget.js +8 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +58 -3
- package/skills/tickmarkr-loop/SKILL.md +59 -5
- package/skills/tickmarkr-overseer/SKILL.md +134 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +145 -18
package/dist/run/daemon.js
CHANGED
|
@@ -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,7 +34,7 @@ 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, WorktreeAdminRaceError, 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
40
|
import { classifyRepairDisposition, namedPaths, resolveScopeHints } from "./repair-disposition.js";
|
|
@@ -883,6 +883,32 @@ export const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries committ
|
|
|
883
883
|
export function commandsHash(commands) {
|
|
884
884
|
return createHash("sha256").update(JSON.stringify(Object.entries(commands).sort())).digest("hex").slice(0, 12);
|
|
885
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
|
+
}
|
|
886
912
|
/**
|
|
887
913
|
* T4 (OBS-266): the journal's LAST verification cycle — the (tip, cmdHash) pair the most recent run
|
|
888
914
|
* of the verify commands spoke for, the gates it got a pass from, and whether anything failed in it.
|
|
@@ -904,7 +930,7 @@ function lastVerifyCycle(events) {
|
|
|
904
930
|
if (e.event === "tip-verify-start") {
|
|
905
931
|
const { tip, cmdHash } = e.data;
|
|
906
932
|
cur = typeof tip === "string" && typeof cmdHash === "string"
|
|
907
|
-
? { 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() }
|
|
908
934
|
: undefined;
|
|
909
935
|
afterRunEnd = false;
|
|
910
936
|
continue;
|
|
@@ -927,7 +953,7 @@ function lastVerifyCycle(events) {
|
|
|
927
953
|
continue;
|
|
928
954
|
}
|
|
929
955
|
if (!cur || afterRunEnd || cur.tip !== tip || cur.cmdHash !== cmdHash) {
|
|
930
|
-
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() };
|
|
931
957
|
}
|
|
932
958
|
afterRunEnd = false;
|
|
933
959
|
// T7: EVERY verdict row's own capacity, not just the start row's. The start row is a statement of
|
|
@@ -935,6 +961,7 @@ function lastVerifyCycle(events) {
|
|
|
935
961
|
// these rows, so a row whose capacity differs from the session's — or which is malformed — has to
|
|
936
962
|
// be able to sink the cycle by itself.
|
|
937
963
|
cur.capacities.push(e.data.capacity);
|
|
964
|
+
cur.tipEnvironments.push(e.data.tipEnvironment);
|
|
938
965
|
if (e.event === "tip-verify-failed")
|
|
939
966
|
cur.failed = true;
|
|
940
967
|
else {
|
|
@@ -1204,13 +1231,13 @@ export function identityDetails(leg, state, branch, runId, expected) {
|
|
|
1204
1231
|
*/
|
|
1205
1232
|
export function appendIdentityFailure(journal, f) {
|
|
1206
1233
|
if (!f.started)
|
|
1207
|
-
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 });
|
|
1208
1235
|
const found = f.state?.kind === "regular" || f.state?.kind === "symbolic" ? f.state.commit : undefined;
|
|
1209
1236
|
journal.append("tip-verify-failed", undefined, {
|
|
1210
1237
|
gate: "integration", cmd: "", exitCode: 1, fingerprints: [], details: f.details, ...(f.infra ? { infra: true } : {}),
|
|
1211
1238
|
integration: { branch: f.branch, expected: f.expected ?? null, found: found ?? null, leg: f.leg,
|
|
1212
1239
|
...(f.state?.kind === "symbolic" ? { target: f.state.target } : {}) },
|
|
1213
|
-
tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity,
|
|
1240
|
+
tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, tipEnvironment: f.tipEnvironment,
|
|
1214
1241
|
});
|
|
1215
1242
|
}
|
|
1216
1243
|
/** O: I(expected) — or, with no expected value, the ref must be regular and its commit becomes the expected. */
|
|
@@ -1271,16 +1298,12 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1271
1298
|
opts.signal?.throwIfAborted();
|
|
1272
1299
|
const cmdHash = commandsHash(commands);
|
|
1273
1300
|
const { identity } = opts;
|
|
1274
|
-
//
|
|
1275
|
-
// green must match, beside the tip and the command set. A cached verdict is the one place a green
|
|
1276
|
-
// crosses a session boundary with nothing re-run, and a session resumed at a different capacity
|
|
1277
|
-
// divides the machine by a different number: that green was established in another world, so it is
|
|
1278
|
-
// not carried forward and the commands run again. A pre-T7 cycle records no capacity and keeps
|
|
1279
|
-
// exactly the behaviour it has today.
|
|
1301
|
+
// Queue row 24 (D-2134): measure once against the repository whose installed packages the verifier receives.
|
|
1280
1302
|
const capacity = resolvedCapacity();
|
|
1303
|
+
const tipEnvironment = measureTipEnvironment(identity?.repo ?? intWt, capacity);
|
|
1281
1304
|
const identityFailure = (f) => {
|
|
1282
|
-
appendIdentityFailure(journal, { ...f, branch: identity.branch, cmdHash, capacity });
|
|
1283
|
-
return { failed: true, identityFailed: true, deferred: [] };
|
|
1305
|
+
appendIdentityFailure(journal, { ...f, branch: identity.branch, cmdHash, capacity, tipEnvironment });
|
|
1306
|
+
return { failed: true, identityFailed: true, deferred: [], tipEnvironment };
|
|
1284
1307
|
};
|
|
1285
1308
|
let tip;
|
|
1286
1309
|
let clean = true; // with `identity` the battery runs in a fresh checkout and K checks tracked content
|
|
@@ -1299,25 +1322,30 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1299
1322
|
const gates = Object.keys(commands).filter((g) => g !== "tipTest");
|
|
1300
1323
|
if (!gates.includes("test") && commands.tipTest)
|
|
1301
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.
|
|
1302
1328
|
const cached = last !== undefined && !last.failed && !last.forgiven && last.tip === tip && last.cmdHash === cmdHash
|
|
1303
1329
|
&& gates.every((g) => last.gates.has(g))
|
|
1304
|
-
&& 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);
|
|
1305
1333
|
// A pair can be verified red and then green without either SHA or command hash changing (for
|
|
1306
1334
|
// example, an external service or ignored fixture recovers). Delimit attempts explicitly so that
|
|
1307
1335
|
// the earlier red cannot remain latched into the later complete green cycle.
|
|
1308
|
-
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 });
|
|
1309
1337
|
opts.signal?.throwIfAborted();
|
|
1310
1338
|
if (clean && cached) {
|
|
1311
|
-
journal.append("tip-verify-cached", undefined, { tip, cmdHash, capacity, gates });
|
|
1339
|
+
journal.append("tip-verify-cached", undefined, { tip, cmdHash, capacity, tipEnvironment, gates });
|
|
1312
1340
|
// The skip must not read as a red. Every surface derives the tip's verdict from this cycle's
|
|
1313
1341
|
// `tip-verify` events (cockpit derive.ts tipVerificationPassed: a run-end claiming "passed" with
|
|
1314
1342
|
// ZERO events is fail-closed to FALSE), so a carried-forward green still journals its per-gate
|
|
1315
1343
|
// pass — `cached: true` keeps it honest about not having re-run the command.
|
|
1316
1344
|
for (const gate of gates) {
|
|
1317
1345
|
const cmd = gate === "test" && commands.tipTest ? commands.tipTest : commands[gate];
|
|
1318
|
-
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 });
|
|
1319
1347
|
}
|
|
1320
|
-
return identity ? { failed: false, expected: tip, identityFailed: false, deferred: [] } : false;
|
|
1348
|
+
return identity ? { failed: false, expected: tip, identityFailed: false, deferred: [], tipEnvironment } : false;
|
|
1321
1349
|
}
|
|
1322
1350
|
const battery = async (dir) => {
|
|
1323
1351
|
let tipFailed = false;
|
|
@@ -1340,7 +1368,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1340
1368
|
};
|
|
1341
1369
|
if (r.pass) {
|
|
1342
1370
|
// Q121s: a forgiven pass journals its fingerprints — honest about what was carried, never a silent green.
|
|
1343
|
-
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 });
|
|
1344
1372
|
}
|
|
1345
1373
|
else {
|
|
1346
1374
|
journal.append("tip-verify-failed", undefined, {
|
|
@@ -1355,6 +1383,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1355
1383
|
tip,
|
|
1356
1384
|
cmdHash,
|
|
1357
1385
|
capacity: measuredCapacity,
|
|
1386
|
+
tipEnvironment,
|
|
1358
1387
|
});
|
|
1359
1388
|
tipFailed = true;
|
|
1360
1389
|
}
|
|
@@ -1372,7 +1401,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1372
1401
|
const changed = await checkoutChange(opened.dir, tip, opened.h0);
|
|
1373
1402
|
if (changed)
|
|
1374
1403
|
return identityFailure({ started: true, expected: tip, leg: "checkout", details: `verification checkout changed during the battery: ${changed}` });
|
|
1375
|
-
return { failed: tipFailed, expected: tip, identityFailed: false, deferred };
|
|
1404
|
+
return { failed: tipFailed, expected: tip, identityFailed: false, deferred, tipEnvironment };
|
|
1376
1405
|
}
|
|
1377
1406
|
finally {
|
|
1378
1407
|
if (checkout)
|
|
@@ -2104,6 +2133,54 @@ function endedRunIdsForReconcile(repoRoot) {
|
|
|
2104
2133
|
}
|
|
2105
2134
|
return ended;
|
|
2106
2135
|
}
|
|
2136
|
+
// Queue row 240 (D-2138): ownership comes only from dispatch rows in this checkout's runs directory.
|
|
2137
|
+
async function removeJournaledTaskWorktrees(repo, owner, current, activeBranch) {
|
|
2138
|
+
let events;
|
|
2139
|
+
try {
|
|
2140
|
+
events = owner.read();
|
|
2141
|
+
}
|
|
2142
|
+
catch {
|
|
2143
|
+
return;
|
|
2144
|
+
} // Queue row 240 (D-2138): unreadable ownership proves no task checkout.
|
|
2145
|
+
const branch = activeBranch ?? events.find((e) => e.event === "run-start")?.data.branch;
|
|
2146
|
+
if (typeof branch !== "string")
|
|
2147
|
+
return;
|
|
2148
|
+
await cleanupRunWorktrees(repo, branch, {
|
|
2149
|
+
removeTaskIds: events.flatMap((e) => e.event === "task-dispatch" && e.taskId ? [e.taskId] : []),
|
|
2150
|
+
run: owner.runId, by: current.runId,
|
|
2151
|
+
producer: (task) => knownProducer(events, task),
|
|
2152
|
+
append: (event, task, data) => {
|
|
2153
|
+
// Queue row 240 (D-2138): the recovery handle retains its terminal task reason without reopening the decision.
|
|
2154
|
+
const terminal = task && [...events].reverse().find((e) => e.taskId === task
|
|
2155
|
+
&& ["task-human", "task-failed", "task-done", "task-dispatch"].includes(e.event));
|
|
2156
|
+
const reason = terminal && terminal.data.reason;
|
|
2157
|
+
owner.append(event, task, { ...data, ...(typeof reason === "string" ? { reason } : {}) });
|
|
2158
|
+
},
|
|
2159
|
+
failed: (data) => current.append("worktree-removal-failed", undefined, data),
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
async function sweepDeadRunWorktrees(repo, current) {
|
|
2163
|
+
let runs;
|
|
2164
|
+
try {
|
|
2165
|
+
runs = readdirSync(join(tickmarkrDir(repo), "runs"));
|
|
2166
|
+
}
|
|
2167
|
+
catch {
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
for (const run of runs) {
|
|
2171
|
+
if (run === current.runId)
|
|
2172
|
+
continue;
|
|
2173
|
+
try {
|
|
2174
|
+
const owner = Journal.open(repo, run);
|
|
2175
|
+
const lifecycle = [...owner.read()].reverse().find((e) => e.event === "run-start" || e.event === "run-resume");
|
|
2176
|
+
const pid = lifecycle?.data.pid;
|
|
2177
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || isPidLive(pid))
|
|
2178
|
+
continue;
|
|
2179
|
+
await removeJournaledTaskWorktrees(repo, owner, current);
|
|
2180
|
+
}
|
|
2181
|
+
catch { /* Queue row 240 (D-2138): unreadable ownership proves nothing and never refuses a start. */ }
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2107
2184
|
export async function runDaemon(repoRoot, opts = {}) {
|
|
2108
2185
|
// OBS-1045: a daemon launched from inside a leased command (a nested runner, an operator's test
|
|
2109
2186
|
// shell) inherits its ancestor's lease token through the environment, and every gate it ran would
|
|
@@ -3188,6 +3265,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3188
3265
|
// panes too. OBS-777: the one startup snapshot lets it reclaim only older runs proven ended, while
|
|
3189
3266
|
// every live or unknown run remains spared.
|
|
3190
3267
|
await reconcile();
|
|
3268
|
+
if (!keepForever)
|
|
3269
|
+
await sweepDeadRunWorktrees(repoRoot, journal);
|
|
3191
3270
|
// v1.4 self-reference guard: a random nonce on the worker trailer AND exit marker. Displayed
|
|
3192
3271
|
// source/diffs (e.g. a worker editing tickmarkr's own prompt.ts/daemon.ts) can't know it, so an echoed
|
|
3193
3272
|
// TICKMARKR_RESULT/TICKMARKR_EXIT literal can never premature-harvest the worker. Quote-split keeps the
|
|
@@ -3380,7 +3459,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3380
3459
|
const worker = journal.read().reverse().find((e) => e.taskId === t.id && e.event === "worker-result");
|
|
3381
3460
|
const disposition = classifyRepairDisposition({
|
|
3382
3461
|
// OBS-1072 add.2: hints are lexed from the readable excerpt, never from terminal control bytes.
|
|
3383
|
-
results: results.map((g) => ({ ...g, details: readableExcerpt(g.details)
|
|
3462
|
+
results: results.map((g) => ({ ...g, details: readableExcerpt(g.details),
|
|
3463
|
+
// Queue row 235 (D-2134): oracle output has the same readable boundary as gate details.
|
|
3464
|
+
...(Array.isArray(g.meta?.oracles) ? { meta: { ...g.meta, oracles: g.meta.oracles.map((oracle) => oracle && typeof oracle === "object" && typeof oracle.output === "string"
|
|
3465
|
+
? { ...oracle, output: readableExcerpt(oracle.output) } : oracle) } } : {}),
|
|
3466
|
+
})), files: t.files, inventory: changed,
|
|
3384
3467
|
refusalSummary: worker?.data.ok === false && typeof worker.data.summary === "string" ? worker.data.summary : undefined,
|
|
3385
3468
|
});
|
|
3386
3469
|
for (const diagnostic of disposition.diagnostics) {
|
|
@@ -3520,6 +3603,64 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3520
3603
|
// row is deliberately written before the later worktree-recreation row so the journal cannot
|
|
3521
3604
|
// describe only the commits it carried while omitting uncommitted work the removal destroyed.
|
|
3522
3605
|
const producerNow = () => knownProducer(journal.read(), t.id);
|
|
3606
|
+
const readTaskWork = async (taskBase, priorWt, events = journal.read()) => {
|
|
3607
|
+
if (existsSync(priorWt))
|
|
3608
|
+
return { head: await gitHead(priorWt), commits: await commitsAheadOf(taskBase, priorWt) };
|
|
3609
|
+
const taskBranch = `${branch}--${t.id}`;
|
|
3610
|
+
try {
|
|
3611
|
+
const read = async (command) => {
|
|
3612
|
+
const result = await shGit(command, repoRoot);
|
|
3613
|
+
if (result.code !== 0)
|
|
3614
|
+
throw new Error(result.stderr || result.stdout || `git exited ${result.code}`);
|
|
3615
|
+
return result.stdout.trim();
|
|
3616
|
+
};
|
|
3617
|
+
const ahead = async (head) => (await read(`git log --reverse --format=%H ${shq(taskBase)}..${shq(head)}`)).split("\n").filter(Boolean);
|
|
3618
|
+
const ancestor = async (base, head) => {
|
|
3619
|
+
const result = await shGit(`git merge-base --is-ancestor ${shq(base)} ${shq(head)}`, repoRoot);
|
|
3620
|
+
if (result.code !== 0 && result.code !== 1)
|
|
3621
|
+
throw new Error(result.stderr || result.stdout || `ancestry exited ${result.code}`);
|
|
3622
|
+
return result.code === 0;
|
|
3623
|
+
};
|
|
3624
|
+
const branchRef = `refs/heads/${taskBranch}`;
|
|
3625
|
+
const exists = await shGit(`git show-ref --verify --quiet ${shq(branchRef)}`, repoRoot);
|
|
3626
|
+
if (exists.code !== 0 && exists.code !== 1)
|
|
3627
|
+
throw new Error(exists.stderr || exists.stdout || `branch read exited ${exists.code}`);
|
|
3628
|
+
const tip = exists.code === 0 ? await read(`git rev-parse --verify ${shq(`${branchRef}^{commit}`)}`) : undefined;
|
|
3629
|
+
const removalAt = events.map((e) => e.event === "worktree-removed" && e.data.task === t.id).lastIndexOf(true);
|
|
3630
|
+
const removal = events[removalAt];
|
|
3631
|
+
if (removal && !events.slice(removalAt + 1).some((e) => e.event === "worktree-recreation" && e.taskId === t.id)
|
|
3632
|
+
&& typeof removal.data.head === "string") {
|
|
3633
|
+
const resolved = await shGit(`git rev-parse --verify ${shq(`${removal.data.head}^{commit}`)}`, repoRoot);
|
|
3634
|
+
const head = resolved.stdout.trim();
|
|
3635
|
+
if (resolved.code === 0 && head && (!tip || await ancestor(tip, head))) {
|
|
3636
|
+
const commits = await ahead(head);
|
|
3637
|
+
let snapshot;
|
|
3638
|
+
if (typeof removal.data.ref === "string") {
|
|
3639
|
+
const pinned = await read(`git rev-parse --verify ${shq(`${removal.data.ref}^{commit}`)}`);
|
|
3640
|
+
if (pinned !== head)
|
|
3641
|
+
snapshot = removal.data.ref;
|
|
3642
|
+
}
|
|
3643
|
+
return { head, commits, snapshot, rebuiltFrom: "removal" };
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
if (!tip)
|
|
3647
|
+
return { commits: [] };
|
|
3648
|
+
const commits = await ahead(tip);
|
|
3649
|
+
if (commits.length === 0)
|
|
3650
|
+
return { commits: [] };
|
|
3651
|
+
const dispatch = [...events].reverse().find((e) => e.event === "task-dispatch" && e.taskId === t.id);
|
|
3652
|
+
const base = dispatch?.data.base === undefined ? events.find((e) => e.event === "run-start")?.data.baseRef : dispatch.data.base;
|
|
3653
|
+
if (typeof base !== "string" || !await ancestor(base, tip))
|
|
3654
|
+
throw new Error("tip does not descend from its recorded dispatch base");
|
|
3655
|
+
const ref = `refs/tickmarkr/preserved/${tip}`;
|
|
3656
|
+
await read(`git update-ref ${shq(ref)} ${shq(tip)}`);
|
|
3657
|
+
journal.append("worktree-preserved", t.id, { ref, ...producerFields(producerNow()) });
|
|
3658
|
+
return { head: tip, commits, rebuiltFrom: "branch" };
|
|
3659
|
+
}
|
|
3660
|
+
catch (error) {
|
|
3661
|
+
return { commits: [], error: `Queue row 240 (D-2138): cannot rebuild task branch ${taskBranch}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3662
|
+
}
|
|
3663
|
+
};
|
|
3523
3664
|
const recreateTaskWorktree = async (taskBranch, taskBase, priorWt) => {
|
|
3524
3665
|
const producer = producerNow();
|
|
3525
3666
|
const ref = await preserveWorktree(priorWt, producer);
|
|
@@ -3895,8 +4036,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3895
4036
|
...(typeof g.meta.producerAttempt === "number" ? { producerAttempt: g.meta.producerAttempt } : {}),
|
|
3896
4037
|
} : {}),
|
|
3897
4038
|
...((cfg.executionPolicy || repairSelection) && !g.pass ? { disposition: failureDisposition(g) } : {}),
|
|
3898
|
-
...Object.fromEntries(["runnerInfraRerun", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
|
|
3899
|
-
"forgivenFingerprints", "freshFingerprints", "baselineProvenance", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
|
|
4039
|
+
...Object.fromEntries(["runnerInfraRerun", "judgeIndependent", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
|
|
4040
|
+
"forgivenFingerprints", "freshFingerprints", "baselineProvenance", "oracles", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
|
|
3900
4041
|
.filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
|
|
3901
4042
|
// OBS-540: preserve terminal-vs-retryable infra exactly. normalizeGateOutcome deliberately
|
|
3902
4043
|
// defaults a legacy infra row to retryable, so dropping an explicit false here reverses the
|
|
@@ -4345,7 +4486,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4345
4486
|
if (uncertain)
|
|
4346
4487
|
return decline(uncertain);
|
|
4347
4488
|
}
|
|
4348
|
-
const
|
|
4489
|
+
const work = await readTaskWork(published, wt);
|
|
4490
|
+
if (work.error) {
|
|
4491
|
+
await park(t, work.error, "infra", found.assignment, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4492
|
+
return "parked";
|
|
4493
|
+
}
|
|
4494
|
+
const commits = work.commits;
|
|
4349
4495
|
if (!found.result.finished && commits.length === 0)
|
|
4350
4496
|
return decline("unfinished");
|
|
4351
4497
|
recordHarvest(commits, found.result.finished, found.result.summary);
|
|
@@ -4402,6 +4548,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4402
4548
|
return found.assignment;
|
|
4403
4549
|
};
|
|
4404
4550
|
const harvestAuthor = rs && !satisfiedGate && !replayedGates && !recheck ? await harvestInterruptedAttempt() : undefined;
|
|
4551
|
+
if (harvestAuthor === "parked")
|
|
4552
|
+
return;
|
|
4405
4553
|
// OBS-1202: a recheck bound to a stall park that recorded a reapFailure re-verifies the parked
|
|
4406
4554
|
// attempt's owned census through the same reapWorker a live harvest uses, BEFORE any gate reads
|
|
4407
4555
|
// its tree. Only a complete, empty census releases the harvested commits to the battery; an
|
|
@@ -4454,7 +4602,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4454
4602
|
const preservedRef = recheck
|
|
4455
4603
|
? effectiveEvents(journal.read()).reverse().find((e) => e.taskId === t.id && e.event === "task-approved" && e.data.release === RECHECK_RELEASE)?.data.recheckedRef
|
|
4456
4604
|
: undefined;
|
|
4457
|
-
|
|
4605
|
+
const work = preservedRef ? undefined : await readTaskWork(taskBase, priorWt);
|
|
4606
|
+
if (work?.error) {
|
|
4607
|
+
await park(t, work.error, "infra", assignment, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
if (!work?.head && !preservedRef) {
|
|
4458
4611
|
if (satisfiedGate || recheck)
|
|
4459
4612
|
throw new Error(`${recheck ? "recheck" : `approved gate ${satisfiedGate}`} cannot resume: task worktree is missing`);
|
|
4460
4613
|
// Observed passes are an optimization, never authority: without the task worktree there is no
|
|
@@ -4469,9 +4622,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4469
4622
|
: satisfiedGate
|
|
4470
4623
|
? `approved gate ${satisfiedGate}`
|
|
4471
4624
|
: `recorded gates on ${replayedGates.commit.slice(0, 10)}`;
|
|
4472
|
-
const priorTaskTip = preservedRef ? (await shGit(`git rev-parse ${shq(preservedRef)}`, repoRoot)).stdout.trim() :
|
|
4473
|
-
const priorTaskSubject = await gateCommitSubject(taskBase, priorTaskTip, preservedRef ? repoRoot : priorWt);
|
|
4474
|
-
const commitsToCarry = preservedRef ? await commitsAheadOfRef(taskBase, priorTaskTip, repoRoot) :
|
|
4625
|
+
const priorTaskTip = preservedRef ? (await shGit(`git rev-parse ${shq(preservedRef)}`, repoRoot)).stdout.trim() : work.head;
|
|
4626
|
+
const priorTaskSubject = await gateCommitSubject(taskBase, priorTaskTip, preservedRef || work?.rebuiltFrom ? repoRoot : priorWt);
|
|
4627
|
+
const commitsToCarry = preservedRef ? await commitsAheadOfRef(taskBase, priorTaskTip, repoRoot) : [...work.commits];
|
|
4628
|
+
// Queue row 240 (D-2138): rechecking a dirt refusal gates the committed work as a present checkout did.
|
|
4629
|
+
// The refused bytes remain recoverable; an explicitly checked ref still selects its exact tree.
|
|
4630
|
+
const dirtRecheck = recheck && [...journal.read()].reverse()
|
|
4631
|
+
.find((e) => e.taskId === t.id && e.event === "gate-result")?.data.dirtyWorktree === true;
|
|
4632
|
+
if (work?.snapshot && !dirtRecheck)
|
|
4633
|
+
commitsToCarry.push(work.snapshot);
|
|
4475
4634
|
// 2.8.1 T2 (ANL-023 §7.2): a task with a recorded merge whose prior tip is PROVEN in E (`merge-base --is-ancestor`
|
|
4476
4635
|
// exit 0 — an empty or failed history read is no proof) keeps that tip as its gated subject — recreated on E it
|
|
4477
4636
|
// would be gated AT E, and a sibling's publication since its merge would hide the recorded merge. Nothing is gated
|
|
@@ -4505,6 +4664,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4505
4664
|
satisfiedGate = journal.replaySatisfiedGates(new Map([[t.id, currentTaskSubject]])).get(t.id);
|
|
4506
4665
|
}
|
|
4507
4666
|
journal.append("worktree-recreation", t.id, {
|
|
4667
|
+
...(work?.rebuiltFrom ? { rebuiltFrom: work.rebuiltFrom } : {}),
|
|
4508
4668
|
attempted: commitsToCarry, carried: carriedCommits, ...(accountedCommits.length > 0 ? { accounted: accountedCommits } : {}),
|
|
4509
4669
|
});
|
|
4510
4670
|
// OBS-212: same fail-closed rule as the dispatch path — but this path is worse, because it runs
|
|
@@ -5063,6 +5223,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5063
5223
|
});
|
|
5064
5224
|
}
|
|
5065
5225
|
lastContextTokens = undefined;
|
|
5226
|
+
const taskBase = published; // deps are merged → visible to this task (E, never the ref as it stands)
|
|
5227
|
+
const taskBranch = `${branch}--${t.id}`;
|
|
5228
|
+
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
5229
|
+
const recreating = existsSync(priorWt);
|
|
5230
|
+
const work = await readTaskWork(taskBase, priorWt, journaledSoFar);
|
|
5231
|
+
if (work.error) {
|
|
5232
|
+
await park(t, work.error, "infra", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
5233
|
+
return;
|
|
5234
|
+
}
|
|
5066
5235
|
graph = setStatus(graph, t.id, "running");
|
|
5067
5236
|
saveGraph(repoRoot, graph);
|
|
5068
5237
|
await trackedDriver.project?.(t.id, "in-progress");
|
|
@@ -5073,6 +5242,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5073
5242
|
const workerDispatchOrdinal = nextWorkerDispatchOrdinal++;
|
|
5074
5243
|
vendorEscalated.delete(channelKey(assignment)); // dispatched now: from here on it IS tried
|
|
5075
5244
|
journal.append("task-dispatch", t.id, {
|
|
5245
|
+
base: taskBase,
|
|
5076
5246
|
...(scopeApproval?.data.release === "scope-request" ? { files: t.files, graphDefinitionHash: graphDefinitionHash(graph) } : {}),
|
|
5077
5247
|
assignment, attempt, workerDispatchOrdinal, provenance: dispatchProvenance([
|
|
5078
5248
|
channelKey(assignment) === channelKey(r.assignment) ? r.provenance
|
|
@@ -5090,15 +5260,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5090
5260
|
...(carriedConsultGuidance ? { carriedConsultGuidance } : {}),
|
|
5091
5261
|
});
|
|
5092
5262
|
journal.phaseStart(t.id, "worker", { attempt, assignment });
|
|
5093
|
-
const taskBase = published; // deps are merged → visible to this task (E, never the ref as it stands)
|
|
5094
5263
|
taskBases.set(t.id, taskBase);
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
//
|
|
5099
|
-
|
|
5264
|
+
// Queue row 240 (D-2138): read a missing checkout like a present one: its recorded removal first,
|
|
5265
|
+
// with its preserved snapshot carried as a reaped worker's is; else a task branch proven by its
|
|
5266
|
+
// journaled dispatch base or the run-start base; else nothing. Never reset a branch holding commits unproven.
|
|
5267
|
+
// Queue row 240 (D-2138): like recheck, a dispatch after a dirt refusal carries committed work only;
|
|
5268
|
+
// the refused bytes remain recoverable from their ref, as when the prior checkout was present.
|
|
5269
|
+
const dirtRefusal = [...journaledSoFar].reverse()
|
|
5270
|
+
.find((e) => e.taskId === t.id && (e.event === "gate-result" || e.event === "task-dispatch"))?.data.dirtyWorktree === true;
|
|
5271
|
+
const preserved = recreating ? reapedWorktreeRef : dirtRefusal ? undefined : work.snapshot;
|
|
5100
5272
|
reapedWorktreeRef = undefined;
|
|
5101
|
-
const commitsToCarry =
|
|
5273
|
+
const commitsToCarry = [...work.commits];
|
|
5102
5274
|
if (preserved)
|
|
5103
5275
|
commitsToCarry.push(preserved);
|
|
5104
5276
|
const wt = await recreateTaskWorktree(taskBranch, taskBase, priorWt);
|
|
@@ -5107,8 +5279,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5107
5279
|
// orphan work a consult already verified as landed.
|
|
5108
5280
|
const { carried: carriedCommits, accounted: accountedCommits } = commitsToCarry.length > 0
|
|
5109
5281
|
? await cherryPickCommits(wt, commitsToCarry) : { carried: [], accounted: [] };
|
|
5110
|
-
if (recreating) {
|
|
5282
|
+
if (recreating || work.rebuiltFrom) {
|
|
5111
5283
|
journal.append("worktree-recreation", t.id, {
|
|
5284
|
+
...(work.rebuiltFrom ? { rebuiltFrom: work.rebuiltFrom } : {}),
|
|
5112
5285
|
attempted: commitsToCarry, carried: carriedCommits, ...(accountedCommits.length > 0 ? { accounted: accountedCommits } : {}),
|
|
5113
5286
|
});
|
|
5114
5287
|
}
|
|
@@ -7478,8 +7651,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7478
7651
|
summary.tipProof = { kind: "incomplete" };
|
|
7479
7652
|
}
|
|
7480
7653
|
else {
|
|
7481
|
-
|
|
7482
|
-
|
|
7654
|
+
const capacity = resolvedCapacity();
|
|
7655
|
+
const tipEnvironment = measureTipEnvironment(repoRoot, capacity);
|
|
7656
|
+
appendIdentityFailure(journal, { started: false, branch, ...opened, cmdHash: commandsHash(commands), capacity, tipEnvironment });
|
|
7657
|
+
tipClose = { failed: true, identityFailed: true, deferred: [], tipEnvironment };
|
|
7483
7658
|
summary.tipVerify = "failed";
|
|
7484
7659
|
summary.tipProof = runEndTipProof(journal.read());
|
|
7485
7660
|
}
|
|
@@ -7514,8 +7689,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7514
7689
|
if (!tipClose.identityFailed && tipClose.expected !== undefined) {
|
|
7515
7690
|
const closing = await checkIntegrationIdentity(repoRoot, branch, tipClose.expected);
|
|
7516
7691
|
if (!closing.ok) {
|
|
7692
|
+
const capacity = resolvedCapacity();
|
|
7693
|
+
const tipEnvironment = tipStarted ? tipClose.tipEnvironment : measureTipEnvironment(repoRoot, capacity);
|
|
7517
7694
|
appendIdentityFailure(journal, { started: tipStarted, branch, expected: tipClose.expected, leg: closing.leg, state: closing.state,
|
|
7518
|
-
details: identityDetails(closing.leg, closing.state, branch, runId, tipClose.expected), cmdHash: commandsHash(commands), capacity
|
|
7695
|
+
details: identityDetails(closing.leg, closing.state, branch, runId, tipClose.expected), cmdHash: commandsHash(commands), capacity, tipEnvironment });
|
|
7519
7696
|
summary.tipVerify = "failed";
|
|
7520
7697
|
summary.tipProof = runEndTipProof(journal.read());
|
|
7521
7698
|
}
|
|
@@ -7527,9 +7704,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7527
7704
|
}
|
|
7528
7705
|
journal.append("run-end", undefined, { ...summary });
|
|
7529
7706
|
await reconcile(); // run-end boundary: nothing in flight — full sweep (empty desired set)
|
|
7530
|
-
//
|
|
7707
|
+
// Queue row 240 (D-2138): every task checkout of this run is removed once its recovery ref is recorded,
|
|
7708
|
+
// unless keepPanes is forever. A dead run's leftovers go at the next daemon start in the same checkout.
|
|
7709
|
+
// Only journaled task checkouts are touched; no directory listing or in-use check exists.
|
|
7531
7710
|
if (!keepForever)
|
|
7532
|
-
await
|
|
7711
|
+
await removeJournaledTaskWorktrees(repoRoot, journal, journal, branch);
|
|
7533
7712
|
// VIS-02: name each blocked subtree by its nearest parked/failed root, e.g. "3 blocked behind P40-02".
|
|
7534
7713
|
const attribution = [...attributeBlocked(graph).entries()]
|
|
7535
7714
|
.sort(([a], [b]) => a.localeCompare(b))
|
package/dist/run/git.d.ts
CHANGED
|
@@ -234,11 +234,16 @@ export declare const sanitizeBranch: (branch: string) => string;
|
|
|
234
234
|
export declare const WORKTREES_DIR = "worktrees.noindex";
|
|
235
235
|
export declare const worktreePath: (repo: string, branch: string) => string;
|
|
236
236
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
237
|
+
* Queue row 240 (D-2138): remove only journaled task checkouts, after preserving their recovery tree.
|
|
238
|
+
* There is no in-use check. Branches and recovery refs remain; a failed operation keeps the checkout.
|
|
239
239
|
*/
|
|
240
240
|
export declare function cleanupRunWorktrees(repo: string, branch: string, opts: {
|
|
241
241
|
removeTaskIds: string[];
|
|
242
|
+
run: string;
|
|
243
|
+
by: string;
|
|
244
|
+
producer: (task: string) => PreserveProducer;
|
|
245
|
+
append: (event: string, task: string | undefined, data: Record<string, unknown>) => void;
|
|
246
|
+
failed: (data: Record<string, unknown>) => void;
|
|
242
247
|
}): Promise<void>;
|
|
243
248
|
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
|
244
249
|
/**
|
package/dist/run/git.js
CHANGED
|
@@ -817,12 +817,45 @@ const sanitize = sanitizeBranch;
|
|
|
817
817
|
export const WORKTREES_DIR = "worktrees.noindex";
|
|
818
818
|
export const worktreePath = (repo, branch) => join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
819
819
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
820
|
+
* Queue row 240 (D-2138): remove only journaled task checkouts, after preserving their recovery tree.
|
|
821
|
+
* There is no in-use check. Branches and recovery refs remain; a failed operation keeps the checkout.
|
|
822
822
|
*/
|
|
823
823
|
export async function cleanupRunWorktrees(repo, branch, opts) {
|
|
824
|
-
for (const
|
|
825
|
-
|
|
824
|
+
for (const task of new Set(opts.removeTaskIds)) {
|
|
825
|
+
const dir = worktreePath(repo, `${branch}--${task}`);
|
|
826
|
+
if (!existsSync(dir))
|
|
827
|
+
continue;
|
|
828
|
+
try {
|
|
829
|
+
const head = await gitHead(dir);
|
|
830
|
+
const producer = opts.producer(task);
|
|
831
|
+
let ref = await preserveWorktree(dir, producer);
|
|
832
|
+
if (!ref) {
|
|
833
|
+
const merged = await shGit(`git merge-base --is-ancestor ${shq(head)} ${shq(`refs/heads/${branch}`)}`, repo);
|
|
834
|
+
if (merged.code !== 0 && merged.code !== 1)
|
|
835
|
+
throw new Error(merged.stderr || merged.stdout || "ancestry comparison failed");
|
|
836
|
+
if (merged.code === 1) {
|
|
837
|
+
ref = `refs/tickmarkr/preserved/${head}`;
|
|
838
|
+
await shGitOk(`git update-ref ${shq(ref)} ${shq(head)}`, repo);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// Queue row 240 (D-2138 add.4): a disposable preservation index never sees staged-only bytes.
|
|
842
|
+
const staged = await shGit(`git diff-index --cached --name-only --diff-filter=ACMRT -z ${shq(ref ?? head)} --`, dir);
|
|
843
|
+
if (staged.code !== 0 || staged.stdout.length)
|
|
844
|
+
throw new Error(staged.stderr || "staged bytes are absent from the recovery tree");
|
|
845
|
+
if (ref)
|
|
846
|
+
opts.append("worktree-preserved", task, { ref, ...producerFields(producer), by: opts.by });
|
|
847
|
+
if (ref)
|
|
848
|
+
opts.append("worktree-removed", undefined, { task, dir, head, ref, by: opts.by });
|
|
849
|
+
// Queue row 240 (D-2138): unlike rollback, a refused deletion must never fall through to rm -rf.
|
|
850
|
+
await withWorktreeAdmin(() => shGitOk(`git worktree remove --force ${shq(dir)}`, repo));
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
try {
|
|
854
|
+
opts.failed({ run: opts.run, task, dir, error: error instanceof Error ? error.message : String(error) });
|
|
855
|
+
}
|
|
856
|
+
catch { /* Queue row 240 (D-2138): cleanup never replaces the run's outcome, even with an unwritable journal. */ }
|
|
857
|
+
}
|
|
858
|
+
}
|
|
826
859
|
}
|
|
827
860
|
export async function resolveIntegrationBranch(_repo, branch) {
|
|
828
861
|
return branch;
|
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")) ?? {};
|
|
@@ -4,7 +4,7 @@ import type { GateResult } from "../gates/types.js";
|
|
|
4
4
|
export type RepairDispositionKind = "fund-repair" | "scope-request" | "authoring" | "none";
|
|
5
5
|
export interface RepairDisposition {
|
|
6
6
|
kind: RepairDispositionKind;
|
|
7
|
-
/**
|
|
7
|
+
/** Queue row 235 (D-2134): validated inventory paths or explicitly declared oracle landings. */
|
|
8
8
|
paths: string[];
|
|
9
9
|
reason: string;
|
|
10
10
|
/** which evidence decided: a gate name, "worker" for a refusal, absent for the ordinary disposition */
|