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.
- package/dist/adapters/codex-commit-check.d.ts +4 -3
- package/dist/adapters/codex-commit-check.js +42 -28
- package/dist/adapters/codex.d.ts +2 -2
- package/dist/adapters/codex.js +38 -32
- package/dist/adapters/grok.js +5 -9
- package/dist/adapters/model-lints.js +4 -3
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +10 -1
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/approve.js +9 -28
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +38 -20
- package/dist/cli/commands/status.js +9 -6
- 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/gsd.js +4 -3
- package/dist/compile/native.js +8 -6
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/baseline.js +51 -10
- package/dist/gates/cache.js +2 -4
- package/dist/graph/schema.js +3 -1
- package/dist/run/daemon.js +147 -35
- package/dist/run/git-trust.d.ts +11 -0
- package/dist/run/git-trust.js +1 -1
- package/dist/run/git.d.ts +18 -1
- package/dist/run/git.js +73 -2
- package/dist/run/journal.d.ts +15 -1
- package/dist/run/journal.js +43 -6
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +2 -2
- 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";
|
|
@@ -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")
|
|
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
|
-
|
|
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
|
-
|
|
5264
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
6046
|
-
// conclusion without it
|
|
6047
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
6287
|
-
//
|
|
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
|
-
|
|
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 &&
|
|
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;
|
package/dist/run/git-trust.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/run/git-trust.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|