tickmarkr 2.8.5 → 2.8.7

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.
Files changed (38) hide show
  1. package/dist/adapters/codex-commit-check.d.ts +4 -3
  2. package/dist/adapters/codex-commit-check.js +42 -28
  3. package/dist/adapters/codex.d.ts +2 -2
  4. package/dist/adapters/codex.js +38 -32
  5. package/dist/adapters/grok.js +5 -9
  6. package/dist/adapters/model-lints.js +4 -3
  7. package/dist/adapters/prompt.js +9 -1
  8. package/dist/adapters/types.d.ts +10 -1
  9. package/dist/adapters/types.js +12 -0
  10. package/dist/cli/commands/approve.js +9 -28
  11. package/dist/cli/commands/init.d.ts +10 -0
  12. package/dist/cli/commands/init.js +38 -20
  13. package/dist/cli/commands/status.js +9 -6
  14. package/dist/cli/commands/version.d.ts +1 -1
  15. package/dist/cli/commands/version.js +36 -2
  16. package/dist/cli/help.d.ts +1 -0
  17. package/dist/cli/help.js +6 -3
  18. package/dist/compile/gsd.js +4 -3
  19. package/dist/compile/native.js +8 -6
  20. package/dist/drivers/subprocess.js +9 -4
  21. package/dist/gates/baseline.js +51 -10
  22. package/dist/gates/cache.js +2 -4
  23. package/dist/graph/schema.js +3 -1
  24. package/dist/run/daemon.js +147 -35
  25. package/dist/run/git-trust.d.ts +11 -0
  26. package/dist/run/git-trust.js +1 -1
  27. package/dist/run/git.d.ts +18 -1
  28. package/dist/run/git.js +73 -2
  29. package/dist/run/journal.d.ts +15 -1
  30. package/dist/run/journal.js +43 -6
  31. package/dist/run/repair-disposition.d.ts +1 -1
  32. package/dist/run/repair-disposition.js +2 -2
  33. package/dist/run/stall.d.ts +2 -0
  34. package/dist/run/stall.js +12 -4
  35. package/package.json +1 -1
  36. package/skills/tickmarkr-auto/SKILL.md +10 -8
  37. package/skills/tickmarkr-loop/SKILL.md +10 -8
  38. package/skills/tickmarkr-overseer/SKILL.md +10 -8
@@ -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";
@@ -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, 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, 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"); a worker that is merely thinking still burns CPU and is never concluded here.
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. */
@@ -1145,6 +1149,48 @@ export function integrationAnchor(events) {
1145
1149
  return { unreadable: `the newest merge row's from is ${show(merge.data.from)}, not an object id` };
1146
1150
  return { anchor: merge.data.commit };
1147
1151
  }
1152
+ /**
1153
+ * Queue row 220 (D-2111): prove the spec's declared base against the commit this run starts from — the
1154
+ * checkout HEAD for a fresh run (recorded as its run-start baseRef), the recorded run-start baseRef for a
1155
+ * resume, never the checkout's current HEAD: a resume never re-bases, and no run rebases onto the declared
1156
+ * ref. Drift and an unresolvable answer throw; runDaemon calls this before it writes anything.
1157
+ */
1158
+ async function proveDeclaredBase(repoRoot, runId, ref, resume) {
1159
+ const action = resume ? `resume ${runId}` : "run";
1160
+ const remedy = `Remedy: check out a branch that contains ${ref}, or correct base: in the spec and recompile.`;
1161
+ const unresolvable = (against, why) => new Error(`refusing to ${action}: declared base ${ref} is unresolvable against ${against} — ${why}. ${remedy}`);
1162
+ let start;
1163
+ let against;
1164
+ if (resume) {
1165
+ const recorded = Journal.open(repoRoot, runId).read().find((e) => e.event === "run-start")?.data.baseRef;
1166
+ if (typeof recorded !== "string" || !recorded)
1167
+ throw unresolvable("the recorded run-start base", "the journal's run-start records no baseRef");
1168
+ start = recorded;
1169
+ against = `the recorded run-start base ${short(start)}`;
1170
+ }
1171
+ else {
1172
+ const head = await shGit("git rev-parse --verify HEAD^{commit}", repoRoot);
1173
+ if (head.code !== 0)
1174
+ throw unresolvable("HEAD", `git said for HEAD: ${(head.stderr || head.stdout).trim()}`);
1175
+ start = head.stdout.trim();
1176
+ against = `HEAD ${short(start)}`;
1177
+ }
1178
+ const probe = await declaredBaseContainment(repoRoot, ref, start);
1179
+ if (probe.result === "contained")
1180
+ return { ref, via: probe.via, start };
1181
+ if (probe.result === "unresolvable")
1182
+ throw unresolvable(against, `git said for ${probe.ref}: ${probe.evidence}`);
1183
+ const lines = await Promise.all(probe.missing.map(async (m) => {
1184
+ const subject = await shGit(`git log -1 --format=%s ${shq(m.commit)}`, repoRoot);
1185
+ // a drift refusal must name the subject: an unreadable one refuses as unresolvable, quoting git
1186
+ if (subject.code !== 0)
1187
+ throw unresolvable(against, `git said for missing commit ${short(m.commit)}: ${(subject.stderr || subject.stdout).trim()}`);
1188
+ return ` ${short(m.commit)} ${subject.stdout.trim()}${m.merge ? " (merge)" : ""}`
1189
+ + ` — ${m.paths.length > 0 ? m.paths.join(", ") : "no paths"}`;
1190
+ }));
1191
+ throw new Error(`refusing to ${action}: declared base ${ref} has drifted from ${against} — `
1192
+ + `${probe.missing.length} declared commit(s) missing:\n${lines.join("\n")}\n${remedy}`);
1193
+ }
1148
1194
  /** The ref legs' details: the leg, both commits, the symbolic target and the repair text. */
1149
1195
  export function identityDetails(leg, state, branch, runId, expected) {
1150
1196
  const found = state.kind === "regular" || state.kind === "symbolic" ? state.commit : undefined;
@@ -2105,6 +2151,12 @@ export async function runDaemon(repoRoot, opts = {}) {
2105
2151
  let baselineFailed = false;
2106
2152
  try {
2107
2153
  graph = loadGraph(repoRoot);
2154
+ // Queue row 220 (D-2111): a declared base is proven HERE — before any journal row (lock-reclaimed and
2155
+ // exit-cause included), the resume anchor checks, a worktree, the integration ref or baseline capture —
2156
+ // so a refusal leaves nothing; the finally releases the lock and repository claim. No base: unchanged.
2157
+ const declaredBase = graph.spec.base === undefined ? undefined
2158
+ : await proveDeclaredBase(repoRoot, runId, graph.spec.base, opts.resume === true);
2159
+ const declaredBaseRow = declaredBase ? { declaredBase: { ref: declaredBase.ref, via: declaredBase.via } } : {};
2108
2160
  // One bounded snapshot supplies every dispatch. On resume the current journal still participates
2109
2161
  // in the fold so its task-done/ordinary-approval events can retire older evidence, but findings it
2110
2162
  // produced are suppressed: same-run feedback already replays those bytes and must not deliver them
@@ -2943,6 +2995,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2943
2995
  ...(replayedExclusions.size > 0 ? { excludedChannels: [...replayedExclusions].sort() } : {}),
2944
2996
  ...(opts.retryFailed ? { retryFailed: true } : {}),
2945
2997
  ...(opts.graphChanged ? { graphChanged: true } : {}), // OBS-1073: the release is the engagement's
2998
+ ...declaredBaseRow,
2946
2999
  });
2947
3000
  runStarted = true;
2948
3001
  await placeBoard();
@@ -2993,7 +3046,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2993
3046
  }
2994
3047
  }
2995
3048
  else {
2996
- baseRef = await gitHead(repoRoot);
3049
+ baseRef = declaredBase?.start ?? await gitHead(repoRoot); // row 220: the proven commit is the run's base
2997
3050
  journal.append("baseline-start", undefined, { baseRef, commands, capacity: resolvedCapacity() });
2998
3051
  await placeBoard();
2999
3052
  baselinePending = true;
@@ -3034,6 +3087,7 @@ export async function runDaemon(repoRoot, opts = {}) {
3034
3087
  branch, graphDefinitionHash: graphDefinitionHash(graph),
3035
3088
  integrationAnchor: "journal", // 2.8.1 T2: this run creates its branch at baseRef and journals every publication with `from`
3036
3089
  ...(prior ? { supersedes: prior.runId } : {}),
3090
+ ...declaredBaseRow,
3037
3091
  }); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
3038
3092
  runStarted = true;
3039
3093
  // v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
@@ -3185,11 +3239,6 @@ export async function runDaemon(repoRoot, opts = {}) {
3185
3239
  ];
3186
3240
  return `${identity} — release with ${commands.map((command) => `\`${command}\``).join(" or ")}`;
3187
3241
  };
3188
- // Lexing never starts in the middle of a word. Resolution is separate so missing hints
3189
- // remain visible evidence without becoming executable approval advice.
3190
- const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
3191
- .map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
3192
- .filter((path) => !path.split("/").includes(".."));
3193
3242
  const diffPaths = async (base, wt) => {
3194
3243
  const out = await shGit(`git diff --name-only -z ${shq(base)}..HEAD`, wt);
3195
3244
  return new Set(out.code === 0 ? out.stdout.split("\0").filter(Boolean) : []);
@@ -3476,7 +3525,20 @@ export async function runDaemon(repoRoot, opts = {}) {
3476
3525
  const ref = await preserveWorktree(priorWt, producer);
3477
3526
  if (ref)
3478
3527
  journal.append("worktree-preserved", t.id, { ref, ...producerFields(producer) });
3479
- return driver.worktree(repoRoot, taskBranch, taskBase);
3528
+ // Queue row 223 (D-2118): only this typed add race buys retries, with three adds total.
3529
+ const addWorktree = () => {
3530
+ return driver.worktree(repoRoot, taskBranch, taskBase);
3531
+ };
3532
+ for (let attempt = 1;; attempt++) {
3533
+ try {
3534
+ return await addWorktree();
3535
+ }
3536
+ catch (error) {
3537
+ if (!(error instanceof WorktreeAdminRaceError) || attempt >= 3)
3538
+ throw error;
3539
+ journal.append("worktree-add-retry", t.id, { attempt, nextAttempt: attempt + 1, evidence: error.evidence });
3540
+ }
3541
+ }
3480
3542
  };
3481
3543
  // A dead worker with a clean checkout still needs a durable recovery handle: there may be no
3482
3544
  // pane left to identify even the commit it was dispatched from. preserveWorktree deliberately
@@ -5186,7 +5248,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5186
5248
  const icmd = retryMode === "resume"
5187
5249
  ? adapter.resumeCommand(sessionId, promptFile, assignment.model, assignment.effort)
5188
5250
  : cfg.visibility.worker === "interactive" && driver.interactive
5189
- ? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort)
5251
+ ? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort, { cwd: wt, branch: taskBranch, repoRoot })
5190
5252
  : null;
5191
5253
  // v1.69 T6: adapters that declare interactiveSeed launch the real TUI and inject the prompt as a
5192
5254
  // user turn; they do NOT need the argv-seeding surface that interactiveCommand represents.
@@ -5203,7 +5265,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5203
5265
  // timing can never interleave a `$(…)` with what follows it (the codex corruption class).
5204
5266
  const workerCmd = interactive
5205
5267
  ? (hasSeed ? ":" : icmd)
5206
- : adapter.invoke(t, wt, assignment, { promptFile }).command;
5268
+ : adapter.invoke(t, wt, assignment, { promptFile, branch: taskBranch, repoRoot }).command;
5207
5269
  const dispatchScript = promptFile.replace(/\.md$/, ".sh");
5208
5270
  // Approval can reset the attempt counter while the old pane remains retained.
5209
5271
  // Its ownership claim must survive a new engagement reusing the script path.
@@ -5260,17 +5322,19 @@ export async function runDaemon(repoRoot, opts = {}) {
5260
5322
  let cpuFlat;
5261
5323
  let cpuAccountant;
5262
5324
  let cpuGapCount = 0;
5263
- let unmeasurableNoted = false;
5264
- // One line per attempt, whichever way the CPU leg turns out to be unmeasurable. A triad that
5325
+ const unmeasurableNoted = new Set();
5326
+ // Queue row 231 (D-2119): one line per reading per attempt, including the connection leg.
5327
+ // One CPU line per attempt, whichever way that leg turns out to be unmeasurable. A triad that
5265
5328
  // can never conclude is this feature silently ABSENT — on a host whose `ps` the probe cannot
5266
5329
  // read, every stall would ride its whole window out again with nothing saying why. Named once
5267
5330
  // per attempt, not per slice: the condition is structural, and a per-slice line would bury it.
5268
- const noteUnmeasurable = (reason) => {
5269
- if (unmeasurableNoted)
5331
+ const noteUnmeasurable = (reason, reading = "cpu") => {
5332
+ if (unmeasurableNoted.has(reading))
5270
5333
  return;
5271
- unmeasurableNoted = true;
5334
+ unmeasurableNoted.add(reading);
5272
5335
  journal.append("worker-harvest-unmeasurable", t.id, { slot: slot.name, attempt, reason });
5273
5336
  };
5337
+ let harvestHeldNoted = false;
5274
5338
  // The CPU leg needs a marker in the worker's own argv, and every launch path puts this
5275
5339
  // attempt's dispatch script there EXCEPT interactiveSeed: runInteractiveSeed launches the
5276
5340
  // TUI directly, by a command the ADAPTER owns (seed.launch(model)) which tickmarkr cannot
@@ -5332,6 +5396,39 @@ export async function runDaemon(repoRoot, opts = {}) {
5332
5396
  const carried = await commitsAheadOf(taskBase, wt);
5333
5397
  if (carried.length === 0)
5334
5398
  return false; // nothing landed: not this branch's population
5399
+ if (!interactive) {
5400
+ // Queue row 231 (D-2119): query afresh only after the other legs hold. lsof ORs selections
5401
+ // without -a; -g includes connector children in the owned group, not just the CLI root.
5402
+ // No environment inspection. One async PATH lookup, bounded from invocation like the
5403
+ // reap census (15 s); SIGKILL also retires a probe that ignores SIGTERM.
5404
+ const group = readOwnedProcessGroup(groupFile);
5405
+ const connection = group === undefined ? "unreadable" : await new Promise((resolve) => {
5406
+ try {
5407
+ 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) => {
5408
+ if (!error && stdout.trim())
5409
+ resolve("connected");
5410
+ else if (error && error.code === 1 && !stdout && !stderr)
5411
+ resolve("none");
5412
+ else
5413
+ resolve("unreadable");
5414
+ });
5415
+ }
5416
+ catch {
5417
+ resolve("unreadable");
5418
+ }
5419
+ });
5420
+ if (connection === "unreadable") {
5421
+ noteUnmeasurable("the worker ESTABLISHED TCP connection reading could not be read", "connection");
5422
+ return false;
5423
+ }
5424
+ if (connection === "connected") {
5425
+ if (!harvestHeldNoted) {
5426
+ harvestHeldNoted = true;
5427
+ journal.append("worker-harvest-held", t.id, { slot: slot.name, attempt, reason: "established-tcp-connection", processGroup: group });
5428
+ }
5429
+ return false;
5430
+ }
5431
+ }
5335
5432
  journal.append("worker-harvest", t.id, {
5336
5433
  slot: slot.name, attempt, commits: carried.length,
5337
5434
  silentMs, cpuMs: leg.cpu.ms, cpuResolutionMs: leg.cpu.resolutionMs,
@@ -5810,13 +5907,17 @@ export async function runDaemon(repoRoot, opts = {}) {
5810
5907
  lastStatus = st;
5811
5908
  journal.append("worker-status", t.id, { slot: slot.name, status: st, attempt });
5812
5909
  }
5813
- if (st === "blocked" || st === "idle") {
5910
+ // Queue row 215 (D-2118 add.1): an unhooked CLI can report unknown at its startup modal.
5911
+ // Bound that status to worker-launch's startup window, including time spent reading.
5912
+ const withinTrustStartup = () => workerLaunchedAt !== undefined
5913
+ && Date.now() - workerLaunchedAt <= workerStartupWindowMs;
5914
+ if (st === "blocked" || st === "idle" || (st === "unknown" && withinTrustStartup())) {
5814
5915
  // T5: once-per-slot auto-answer when the adapter declares a trust dialog and the pane
5815
5916
  // text matches. tickmarkr created the worktree from the operator's own repo — safe by construction.
5816
5917
  if (!trustAnswered && adapter.trustDialog && driver.sendKey) {
5817
5918
  try {
5818
- const paneText = await workerTransport.read(slot, 80);
5819
- if (matchesTrustDialog(paneText, adapter.trustDialog)) {
5919
+ const paneText = await (slot.readScreen ? slot.readScreen() : workerTransport.read(slot, 80));
5920
+ if ((st !== "unknown" || withinTrustStartup()) && matchesTrustDialog(paneText, adapter.trustDialog)) {
5820
5921
  trustAnswered = true;
5821
5922
  trustAnsweredSlots.add(slot.id);
5822
5923
  // v1.25 T1: audit trail for live runs — prove the dialog appeared and was answered.
@@ -6042,9 +6143,9 @@ export async function runDaemon(repoRoot, opts = {}) {
6042
6143
  }
6043
6144
  // OBS-548: the FOURTH leg, and the one the daemon already measured. The three legs above
6044
6145
  // are all channel-side: they say a pane stopped talking. The tree's CPU says whether
6045
- // anything is still WORKING, and the harvest triad forty lines up refuses exactly this
6046
- // conclusion without it — "a worker that is merely thinking still burns CPU and is never
6047
- // concluded here". A live CPU delta is a live worker, by construction; an UNMEASURABLE
6146
+ // anything is still WORKING, and the harvest triad forty lines up refuses this
6147
+ // conclusion without it. Queue row 231 (D-2119): print-mode harvest additionally checks
6148
+ // connections because a remote model turn can have flat CPU. An UNMEASURABLE
6048
6149
  // reading keeps the fail-open contract the probe already states (undefined is never
6049
6150
  // evidence a worker stopped), so on a host whose `ps` cannot be read the fast-kill stands
6050
6151
  // down and the fixed attempt ceiling bounds the hold.
@@ -6231,13 +6332,15 @@ export async function runDaemon(repoRoot, opts = {}) {
6231
6332
  }
6232
6333
  await noteLaunched();
6233
6334
  // OBS-54: headless workers have the same output-inactivity budget as visible panes.
6234
- // v1.76: same monotonic-progress measure as the interactive site; harvest stays raw.
6335
+ // Queue row 233 (D-2130): print progress is the driver's received-byte count, never a row
6336
+ // high-water over the bounded tail; a driver reporting no count keeps the row measure.
6235
6337
  const stallWindowMs = taskTimeoutMinutes * 60_000;
6338
+ const hardDeadline = attemptStart + (attemptHardTimeoutMs ?? stallWindowMs * 4);
6236
6339
  const initialPane = await workerTransport.read(slot, 500);
6237
6340
  startupFailure = startupFailureInWindow(initialPane, workerLaunchedAt);
6238
6341
  let everHadOutput = initialPane.length > 0;
6239
6342
  const stallProgress = new StallProgressTracker();
6240
- stallProgress.observe({ paneText: initialPane, seedSubmitted: true, contextTokens });
6343
+ stallProgress.observe({ paneText: initialPane, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, seedSubmitted: true, contextTokens });
6241
6344
  let lastProgressAt = Date.now();
6242
6345
  finished = false;
6243
6346
  // T2 review (material): the exit marker proves the PROCESS EXITED, never that the worker
@@ -6247,10 +6350,10 @@ export async function runDaemon(repoRoot, opts = {}) {
6247
6350
  // worker-result-harvested row. The interactive site has always kept them apart (`finished`
6248
6351
  // there is the trailer regex; the exit marker only sets exitCode), and the cause taxonomy
6249
6352
  // already names this shape "clean-exit-no-trailer" — unreachable in print mode until now.
6250
- while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs) {
6353
+ while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs && Date.now() < hardDeadline) {
6251
6354
  fatalStop.signal.throwIfAborted();
6252
6355
  executionSignal()?.throwIfAborted();
6253
- const remaining = stallWindowMs - (Date.now() - lastProgressAt);
6356
+ const remaining = Math.min(stallWindowMs - (Date.now() - lastProgressAt), hardDeadline - Date.now());
6254
6357
  let slice = Math.min(BLOCKED_POLL_MS, Math.max(100, Math.min(stallWindowMs / 2, remaining)));
6255
6358
  const startupLeft = workerLaunchedAt === undefined
6256
6359
  ? 0
@@ -6281,11 +6384,10 @@ export async function runDaemon(repoRoot, opts = {}) {
6281
6384
  break;
6282
6385
  }
6283
6386
  await sampleContext();
6284
- if (stallProgress.observe({ paneText, contextTokens }))
6387
+ if (stallProgress.observe({ paneText, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, contextTokens }))
6285
6388
  lastProgressAt = Date.now();
6286
- // T2 (OBS-264): the same liveness triad the interactive loop runs. A headless worker that
6287
- // committed and went quiet is finished work too, and before this it rode the entire
6288
- // window out before anything looked at its commits.
6389
+ // Queue row 231 (D-2119): print mode adds a fresh owned-group connection reading to the
6390
+ // silence/commits/CPU triad; a quiet remote model turn can hold a connection with flat CPU.
6289
6391
  // No nudge hold here, deliberately: print mode has no nudge surface at all (no pane to
6290
6392
  // steer, driver.nudge is never consulted on this path), so there is no pending daemon
6291
6393
  // action for the triad to preempt — the asymmetry with the interactive call site above is
@@ -6300,7 +6402,7 @@ export async function runDaemon(repoRoot, opts = {}) {
6300
6402
  // without one is finished:false with a non-null exitCode — the harvest synthesis then owns
6301
6403
  // it when the worktree carries work, and classifyWorkerResultCause names it otherwise.
6302
6404
  finished = new RegExp(trailerPattern(nonce)).test(output);
6303
- timedOut = !processExited && !finished && Date.now() - lastProgressAt >= stallWindowMs;
6405
+ timedOut = !processExited && !finished && (Date.now() - lastProgressAt >= stallWindowMs || Date.now() >= hardDeadline);
6304
6406
  }
6305
6407
  }
6306
6408
  finally {
@@ -7051,7 +7153,8 @@ export async function runDaemon(repoRoot, opts = {}) {
7051
7153
  // Budget spent means the FRESH LADDER owns this failure — including a review-only one, whose
7052
7154
  // same-channel fix retry is exactly the round the budget just declared too expensive to repeat.
7053
7155
  const repairExhausted = repairable && !repair;
7054
- if (repair && !capStep) {
7156
+ const fundedRepair = repair && !capStep;
7157
+ if (fundedRepair) {
7055
7158
  journal.append("repair-attempt", t.id, {
7056
7159
  repair: repairHistory.length + 1, charge: repairsDrawn + 1, of: MAX_REPAIRS,
7057
7160
  gates: failing.map((g) => g.gate),
@@ -7076,8 +7179,13 @@ export async function runDaemon(repoRoot, opts = {}) {
7076
7179
  });
7077
7180
  await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
7078
7181
  }
7182
+ // Queue row 224(b) (D-2118): pinned tasks park on the first unfunded owned deterministic red
7183
+ // at or after the second, so this climb fires at most once. Unpinned tasks climb on exactly
7184
+ // the second owned deterministic red, even when it funds a repair.
7079
7185
  const climbCause = repairExhausted ? "repair-exhausted"
7080
- : deterministic && ownedGateFailures === 2 ? "gate-fail" : undefined;
7186
+ : deterministic && (t.routingHints?.pin
7187
+ ? ownedGateFailures >= 2 && !fundedRepair
7188
+ : ownedGateFailures === 2) ? "gate-fail" : undefined;
7081
7189
  if (climbCause) {
7082
7190
  const climbed = await climb(climbCause, deterministic ?? failing[0], attempt + 1);
7083
7191
  if (climbed === "parked")
@@ -7190,6 +7298,10 @@ export async function runDaemon(repoRoot, opts = {}) {
7190
7298
  if (fatalStop.signal.aborted)
7191
7299
  return;
7192
7300
  const cleanupEvidence = cleanupErrors.length ? { cleanupErrors } : {};
7301
+ if (err instanceof WorktreeAdminRaceError) {
7302
+ await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { evidence: err.evidence, ...cleanupEvidence });
7303
+ return;
7304
+ }
7193
7305
  if (err instanceof HostDegradedError) {
7194
7306
  await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { disposition: "host-degraded", ...cleanupEvidence });
7195
7307
  return;
@@ -4,11 +4,21 @@ export declare class GitTrustRefusal extends Error {
4
4
  readonly detail: string;
5
5
  constructor(path: string, detail: string, options?: ErrorOptions);
6
6
  }
7
+ interface Authority {
8
+ gitdir: string;
9
+ common: string;
10
+ linked: boolean;
11
+ }
7
12
  /**
8
13
  * The canonical, independently expected common git directory for the checkout enclosing `cwd` — never what
9
14
  * a mutable commondir says. Throws GitTrustRefusal naming the authority path when it cannot be established.
10
15
  */
11
16
  export declare function trustedCommonDir(cwd: string): string;
17
+ /** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
18
+ export declare function checkedAuthority(cwd: string): (Authority & {
19
+ worktree: string;
20
+ stamp?: string;
21
+ }) | undefined;
12
22
  /**
13
23
  * The uncached pre-spawn check for a covered git invocation at `cwd`. Outside any checkout it is a no-op
14
24
  * (unrelated shell commands stay compatible). Inside one it refuses, naming the file, when the commondir git
@@ -119,3 +129,4 @@ export declare const INERT_HOOKS_PATH = "/dev/null";
119
129
  * children (shGit/shGitOk) receive it; payload shells (gate commands, suites) keep the operator's git config.
120
130
  */
121
131
  export declare function protectedGitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
132
+ export {};
@@ -151,7 +151,7 @@ catch {
151
151
  return false;
152
152
  } };
153
153
  /** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
154
- function checkedAuthority(cwd) {
154
+ export function checkedAuthority(cwd) {
155
155
  const dotGit = dotGitOf(cwd);
156
156
  if (!dotGit)
157
157
  return undefined;
package/dist/run/git.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
2
- import { spawn } from "node:child_process";
2
+ import { spawn, type SpawnSyncReturns } from "node:child_process";
3
3
  import { ROUTING_ENV_SEAMS } from "../route/router.js";
4
4
  import { type OwnGitCapability } from "./git-trust.js";
5
5
  export { ROUTING_ENV_SEAMS };
@@ -187,6 +187,23 @@ export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options
187
187
  export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions, options?: Pick<ShellOptions, "bytes">): Promise<ShResult>;
188
188
  export declare function shOk(cmd: string, cwd: string): Promise<string>;
189
189
  export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions): Promise<string>;
190
+ /**
191
+ * Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
192
+ * It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
193
+ * checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
194
+ * targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
195
+ * inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
196
+ * after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
197
+ * fail-open answer for an ordinary git failure is unchanged.
198
+ */
199
+ export declare function gitSync(dir: string, args: readonly string[], { env }?: {
200
+ env?: NodeJS.ProcessEnv;
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
+ }
190
207
  /**
191
208
  * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
192
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
@@ -2,7 +2,7 @@ import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEn
2
2
  import { executionSignal, withoutExecutionBudget } from "./execution-budget.js";
3
3
  import { commandLeaseEnvironment, repositoryLeaseEnvironment, registerRepositoryChild, withCommandLease } from "./lease.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
- import { execFile, execFileSync, spawn } from "node:child_process";
5
+ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
6
6
  import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
7
7
  import { availableParallelism, homedir, tmpdir } from "node:os";
8
8
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -600,6 +600,62 @@ export async function shGitOk(cmd, cwd, own = {}) {
600
600
  throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
601
601
  return r.stdout;
602
602
  }
603
+ /**
604
+ * Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
605
+ * It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
606
+ * checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
607
+ * targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
608
+ * inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
609
+ * after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
610
+ * fail-open answer for an ordinary git failure is unchanged.
611
+ */
612
+ export function gitSync(dir, args, { env = process.env } = {}) {
613
+ const forced = protectedGitEnv(env);
614
+ const pin = ownGitPin(dir, forced);
615
+ try {
616
+ const r = spawnSync("git", args, { cwd: dir, env: pin?.env ?? forced, encoding: "utf8", maxBuffer: 1 << 28, stdio: ["ignore", "pipe", "pipe"] });
617
+ pin?.recheck();
618
+ return pin && typeof r.stdout === "string" ? { ...r, stdout: pin.unview(r.stdout), stderr: pin.unview(r.stderr) } : r;
619
+ }
620
+ finally {
621
+ pin?.release();
622
+ }
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
+ };
603
659
  /**
604
660
  * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
605
661
  * verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
@@ -612,6 +668,9 @@ export async function shGitOk(cmd, cwd, own = {}) {
612
668
  * back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
613
669
  */
614
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) {
615
674
  // 2.8.1 T1 (ANL-023 §7.1): `logHead` writes the new checkout's HEAD reflog whatever the repository's
616
675
  // core.logAllRefUpdates says, so a later HEAD move by any command is appended to an established witness.
617
676
  const git = logHead ? "git -c core.logAllRefUpdates=always" : "git";
@@ -661,6 +720,9 @@ function rollbackFailed(error, failure) {
661
720
  * step bounded by `timeoutMs`; every step is tried, and any that failed is thrown, never swallowed.
662
721
  */
663
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) {
664
726
  const failed = await withoutExecutionBudget(async () => {
665
727
  const run = async (cmd) => {
666
728
  try {
@@ -838,6 +900,9 @@ export function checkoutIncarnation(worktree) {
838
900
  }
839
901
  }
840
902
  export async function createWorktree(repo, branch, baseRef) {
903
+ return withWorktreeAdmin(() => createWorktreeHeld(repo, branch, baseRef));
904
+ }
905
+ async function createWorktreeHeld(repo, branch, baseRef) {
841
906
  branch = await resolveTaskBranch(repo, branch);
842
907
  const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
843
908
  if (existsSync(dir))
@@ -845,7 +910,13 @@ export async function createWorktree(repo, branch, baseRef) {
845
910
  // the worktree capability: a branch add, whose symbolic HEAD keeps the ref-store pin (see OwnGitCapability); a raced
846
911
  // add is rolled back, its branch restored to the value read here or deleted when it is new
847
912
  const prior = (await shGit(`git rev-parse --verify -q ${shq(`refs/heads/${branch}`)}`, repo)).stdout.trim() || undefined;
848
- await shGitOk(`git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo, { createsWorktree: "branch", rollback: { dir, branch: { name: branch, prior } } });
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
+ }
849
920
  linkNodeModules(repo, dir);
850
921
  return dir;
851
922
  }
@@ -42,7 +42,14 @@ export declare const bindingToken: (binding: DecisionBinding) => string;
42
42
  export declare function parseBindingToken(token: string): DecisionBinding | undefined;
43
43
  /** The binding a task-approved row recorded under `park` or `failure`, when well formed. */
44
44
  export declare function recordedBinding(value: unknown): DecisionBinding | undefined;
45
- /** The newest failed gate before a park row — the gate a waive of that park would satisfy. */
45
+ /**
46
+ * The newest failed gate before a park row — the gate a waive of that park would satisfy.
47
+ * Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
48
+ * unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
49
+ * review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
50
+ * an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
51
+ * does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
52
+ */
46
53
  export declare function failedGateBeforePark(events: readonly JournalEvent[], taskId: string, parkIndex: number): GateName | undefined;
47
54
  /** The physical journal line of `events[i]` (see SOURCE_LINE). */
48
55
  export declare const physicalLine: (events: readonly JournalEvent[], i: number) => number;
@@ -96,6 +103,13 @@ export interface PreservedRef {
96
103
  export declare function preservedRefsByTask(events: JournalEvent[]): Map<string, PreservedRef[]>;
97
104
  export declare function reviewRoundsSinceApproval(events: JournalEvent[], taskId: string, rounds?: (decided: JournalEvent[]) => JournalEvent[]): number;
98
105
  export declare function upheldFeedbackByTask(events: JournalEvent[]): Map<string, string>;
106
+ /**
107
+ * Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
108
+ * active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
109
+ * that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
110
+ * `decided` is already read through the decision fold (effectiveEvents).
111
+ */
112
+ export declare function restoresUpheldFeedback(decided: JournalEvent[], taskId: string): boolean;
99
113
  export interface StructuredFinding {
100
114
  class: string;
101
115
  path: string;