@sagentlab/navarch-runtime 0.1.58 → 0.1.60
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/delivery.cjs +10 -2
- package/dist/git-worktree.cjs +136 -1
- package/dist/prompt.cjs +23 -0
- package/dist/session.cjs +33 -7
- package/package.json +1 -1
package/dist/delivery.cjs
CHANGED
|
@@ -54,13 +54,21 @@ async function probeWorktreeDelivery(args) {
|
|
|
54
54
|
const parsed = Number(raw.trim());
|
|
55
55
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : -1;
|
|
56
56
|
};
|
|
57
|
+
// Both counts skip empty commits: `--full-history -- .` keeps every commit
|
|
58
|
+
// that changes a file (merges included) and drops the ones that change
|
|
59
|
+
// nothing. The runtime opens every new task branch with an empty
|
|
60
|
+
// `chore(navarch): claim task branch` commit (git-worktree.cts); counting it
|
|
61
|
+
// failed sessions that correctly delivered nothing — a superseded task, say —
|
|
62
|
+
// as "1 commit never pushed" and pushed their agent to open an empty PR.
|
|
57
63
|
const [branchRaw, statusRaw, aheadRaw, unpushedRaw] = await Promise.all([
|
|
58
64
|
git(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
59
65
|
git(["status", "--porcelain"]),
|
|
60
|
-
args.baseRef
|
|
66
|
+
args.baseRef
|
|
67
|
+
? git(["rev-list", "--count", "--full-history", `${args.baseRef}..HEAD`, "--", "."])
|
|
68
|
+
: Promise.resolve(null),
|
|
61
69
|
// Commits no origin ref carries. Independent of the base ref, so it still
|
|
62
70
|
// answers "was this pushed?" when the base cannot be resolved at all.
|
|
63
|
-
git(["rev-list", "--count", "HEAD", "--not", "--remotes=origin"]),
|
|
71
|
+
git(["rev-list", "--count", "--full-history", "HEAD", "--not", "--remotes=origin", "--", "."]),
|
|
64
72
|
]);
|
|
65
73
|
const branch = branchRaw?.trim() ?? "";
|
|
66
74
|
return {
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.GitWorktree = void 0;
|
|
7
7
|
exports.branchSlug = branchSlug;
|
|
8
|
+
exports.hostedCheckpointRef = hostedCheckpointRef;
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
|
|
10
11
|
const node_fs_1 = require("node:fs");
|
|
@@ -21,6 +22,10 @@ class GitWorktree {
|
|
|
21
22
|
worktreePath;
|
|
22
23
|
repositoryPath;
|
|
23
24
|
branch;
|
|
25
|
+
/** Set by prepare() when the session resumed a hosted attempt's checkpoint. */
|
|
26
|
+
restoredHostedCheckpoint = null;
|
|
27
|
+
/** Why an existing hosted checkpoint could not be restored, for the session log. */
|
|
28
|
+
hostedCheckpointRestoreFailure = null;
|
|
24
29
|
runner;
|
|
25
30
|
checkpointRoot;
|
|
26
31
|
cloneUrl;
|
|
@@ -30,6 +35,8 @@ class GitWorktree {
|
|
|
30
35
|
legacyTaskBranchSuffix;
|
|
31
36
|
taskOwnershipTrailer;
|
|
32
37
|
sessionTokenMarker;
|
|
38
|
+
hostedCheckpointRef;
|
|
39
|
+
hostedCheckpointLocalRef;
|
|
33
40
|
constructor(options) {
|
|
34
41
|
const projectKey = safePathSegment(options.projectId);
|
|
35
42
|
const sessionKey = safePathSegment(options.sessionId);
|
|
@@ -53,6 +60,8 @@ class GitWorktree {
|
|
|
53
60
|
this.cloneUrl = options.cloneUrl;
|
|
54
61
|
this.githubToken = options.githubToken;
|
|
55
62
|
this.repoLocalGithubToken = options.repoLocalGithubToken;
|
|
63
|
+
this.hostedCheckpointRef = options.restoreHostedCheckpoint ? hostedCheckpointRef(options.taskId) : null;
|
|
64
|
+
this.hostedCheckpointLocalRef = `refs/navarch/restored-checkpoints/${taskKey}`;
|
|
56
65
|
}
|
|
57
66
|
async prepare() {
|
|
58
67
|
await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.repositoryPath), { recursive: true });
|
|
@@ -145,6 +154,10 @@ class GitWorktree {
|
|
|
145
154
|
// `worktree add -b` would fail on it, dooming every remaining attempt.
|
|
146
155
|
const staleTip = await this.reclaimStaleLocalBranch();
|
|
147
156
|
const baseRef = taskRef ?? (await this.resolveStartRef());
|
|
157
|
+
// A hosted attempt never pushes the allocated branch until it delivers; an
|
|
158
|
+
// interrupted one leaves its work only in a checkpoint ref. Without this a
|
|
159
|
+
// self-hosted retry starts from the default branch and silently discards it.
|
|
160
|
+
const checkpoint = !taskRef && !staleTip ? await this.fetchHostedCheckpoint() : null;
|
|
148
161
|
// The stale local tip may hold commits the killed session never pushed;
|
|
149
162
|
// resume it unless the pushed task branch has moved past it (then the
|
|
150
163
|
// remote is the durable record).
|
|
@@ -152,7 +165,9 @@ class GitWorktree {
|
|
|
152
165
|
? taskRef && !(await this.isAncestor(taskRef, staleTip))
|
|
153
166
|
? taskRef
|
|
154
167
|
: staleTip
|
|
155
|
-
:
|
|
168
|
+
: checkpoint
|
|
169
|
+
? checkpoint.parent
|
|
170
|
+
: baseRef;
|
|
156
171
|
if (!startRef) {
|
|
157
172
|
// Empty remote (no commits yet): there is nothing to base the session on,
|
|
158
173
|
// so bootstrap an orphan branch the session can push as the first commit.
|
|
@@ -172,6 +187,91 @@ class GitWorktree {
|
|
|
172
187
|
], false);
|
|
173
188
|
if (!taskRef && !staleTip)
|
|
174
189
|
await this.createTaskOwnershipCommit();
|
|
190
|
+
if (checkpoint)
|
|
191
|
+
await this.applyHostedCheckpoint(checkpoint);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Fetches the hosted checkpoint for this task into the cache, or returns
|
|
195
|
+
* null when there is none. Best-effort: a checkpoint that cannot be read
|
|
196
|
+
* costs only the work a fresh start would have lost anyway.
|
|
197
|
+
*/
|
|
198
|
+
async fetchHostedCheckpoint() {
|
|
199
|
+
const ref = this.hostedCheckpointRef;
|
|
200
|
+
if (!ref)
|
|
201
|
+
return null;
|
|
202
|
+
try {
|
|
203
|
+
const listed = await this.runGit(["--git-dir", this.repositoryPath, "ls-remote", "origin", ref], true);
|
|
204
|
+
if (!listed.stdout.trim())
|
|
205
|
+
return null;
|
|
206
|
+
await this.runGit(["--git-dir", this.repositoryPath, "fetch", "--quiet", "--no-tags", "origin", `+${ref}:${this.hostedCheckpointLocalRef}`], true);
|
|
207
|
+
const [sha, parent] = await Promise.all([this.hostedCheckpointLocalRef, `${this.hostedCheckpointLocalRef}^`].map(async (rev) => (await this.runGit(["--git-dir", this.repositoryPath, "rev-parse", "--verify", `${rev}^{commit}`], false))
|
|
208
|
+
.stdout.trim()));
|
|
209
|
+
if (!/^[0-9a-f]{40,64}$/i.test(sha) || !/^[0-9a-f]{40,64}$/i.test(parent)) {
|
|
210
|
+
throw new Error("the checkpoint ref did not resolve to a snapshot commit with a parent");
|
|
211
|
+
}
|
|
212
|
+
return { ref, sha: sha, parent: parent };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
this.hostedCheckpointRestoreFailure = errorMessage(error);
|
|
216
|
+
await this.dropHostedCheckpointLocalRef();
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Puts the snapshot's files in the working tree as uncommitted edits on top
|
|
222
|
+
* of the branch, which already sits on the hosted agent's HEAD: the state the
|
|
223
|
+
* interrupted agent was in. `read-tree -u --reset` rewrites the index and
|
|
224
|
+
* files (deletions included) without moving HEAD; the mixed reset then puts
|
|
225
|
+
* the index back on HEAD so the edits show as unstaged, as they were.
|
|
226
|
+
*/
|
|
227
|
+
async applyHostedCheckpoint(checkpoint) {
|
|
228
|
+
try {
|
|
229
|
+
const meta = await this.runGit(["--git-dir", this.repositoryPath, "show", "-s", "--format=%cI%x00%B", checkpoint.sha], false);
|
|
230
|
+
const [savedAt = "", body = ""] = meta.stdout.split("\0");
|
|
231
|
+
const hostedBranch = body
|
|
232
|
+
.split("\n")
|
|
233
|
+
.map((line) => line.match(/^Navarch-Checkpoint-Branch:\s*(\S+)\s*$/)?.[1])
|
|
234
|
+
.find(Boolean) ?? "";
|
|
235
|
+
await this.runGit(["-C", this.worktreePath, "read-tree", "-u", "--reset", checkpoint.sha], false);
|
|
236
|
+
await this.runGit(["-C", this.worktreePath, "reset", "-q"], false);
|
|
237
|
+
const status = await this.runGit(["-C", this.worktreePath, "status", "--porcelain"], false);
|
|
238
|
+
this.restoredHostedCheckpoint = {
|
|
239
|
+
ref: checkpoint.ref,
|
|
240
|
+
sha: checkpoint.sha,
|
|
241
|
+
hostedBranch,
|
|
242
|
+
savedAt: savedAt.trim(),
|
|
243
|
+
changedFiles: status.stdout.split("\n").filter((line) => line.trim()).length,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// The branch still carries the hosted agent's commits; only its
|
|
248
|
+
// uncommitted edits are lost. Leave a clean tree, not a half-applied one.
|
|
249
|
+
this.hostedCheckpointRestoreFailure = errorMessage(error);
|
|
250
|
+
await this.runner
|
|
251
|
+
.run("git", ["-C", this.worktreePath, "reset", "-q", "--hard", "HEAD"])
|
|
252
|
+
.catch(() => undefined);
|
|
253
|
+
await this.runner.run("git", ["-C", this.worktreePath, "clean", "-fdq"]).catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
await this.dropHostedCheckpointLocalRef();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async dropHostedCheckpointLocalRef() {
|
|
260
|
+
await this.runner
|
|
261
|
+
.run("git", ["--git-dir", this.repositoryPath, "update-ref", "-d", this.hostedCheckpointLocalRef])
|
|
262
|
+
.catch(() => undefined);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Removes the restored hosted checkpoint from origin once a pull request
|
|
266
|
+
* carries its work, so a later hosted attempt does not resume a stale
|
|
267
|
+
* snapshot. Best-effort, like the hosted side's own delete.
|
|
268
|
+
*/
|
|
269
|
+
async deleteRestoredHostedCheckpoint() {
|
|
270
|
+
const restored = this.restoredHostedCheckpoint;
|
|
271
|
+
if (!restored)
|
|
272
|
+
return;
|
|
273
|
+
await this.runGit(["--git-dir", this.repositoryPath, "push", "--quiet", "origin", `:${restored.ref}`], true);
|
|
274
|
+
this.restoredHostedCheckpoint = null;
|
|
175
275
|
}
|
|
176
276
|
/**
|
|
177
277
|
* Releases a leftover local task branch (and any dead worktree holding it)
|
|
@@ -287,6 +387,14 @@ class GitWorktree {
|
|
|
287
387
|
.split("\n")
|
|
288
388
|
.some((line) => line.trim() === this.taskOwnershipTrailer);
|
|
289
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* The ownership line a push to the delivery branch must keep, for the
|
|
392
|
+
* pre-push guard (`NAVARCH_DELIVERY_TASK_MARKER`). Null for a resumed legacy
|
|
393
|
+
* full-UUID branch, whose name alone proves ownership.
|
|
394
|
+
*/
|
|
395
|
+
get deliveryTaskMarker() {
|
|
396
|
+
return this.branch.endsWith(this.legacyTaskBranchSuffix) ? null : this.taskOwnershipTrailer;
|
|
397
|
+
}
|
|
290
398
|
/** Records full task ownership without lengthening the human-facing branch name. */
|
|
291
399
|
async createTaskOwnershipCommit() {
|
|
292
400
|
await this.runGit([
|
|
@@ -557,6 +665,11 @@ async function pathExists(value) {
|
|
|
557
665
|
* the allocated `navarch/...` branch (for example onto Codex's default
|
|
558
666
|
* `codex/...` prefix). `--no-verify` bypasses any hook, so
|
|
559
667
|
* `resolveDeliveryHead` also repairs the worktree before completion.
|
|
668
|
+
*
|
|
669
|
+
* With `NAVARCH_DELIVERY_TASK_MARKER` it also refuses a delivery-branch push
|
|
670
|
+
* whose history lost the task's ownership commit (an amend, reset, or rebase
|
|
671
|
+
* drops the empty claim commit). A retry only resumes a short-suffix branch
|
|
672
|
+
* that carries the marker, so an unmarked push strands the task.
|
|
560
673
|
*/
|
|
561
674
|
const DELIVERY_BRANCH_GUARD_HOOK = `#!/bin/sh
|
|
562
675
|
# Managed by @sagentlab/navarch-runtime (git-worktree.cts).
|
|
@@ -572,6 +685,18 @@ while read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
572
685
|
if [ "$branch" != "$allowed" ]; then
|
|
573
686
|
printf '%s\\n' "navarch: refusing to push '$branch'; this task's delivery branch is '$allowed'." >&2
|
|
574
687
|
status=1
|
|
688
|
+
continue
|
|
689
|
+
fi
|
|
690
|
+
marker="\${NAVARCH_DELIVERY_TASK_MARKER:-}"
|
|
691
|
+
[ -n "$marker" ] || continue
|
|
692
|
+
case "$local_sha" in
|
|
693
|
+
*[!0]*) ;;
|
|
694
|
+
*) continue ;;
|
|
695
|
+
esac
|
|
696
|
+
if [ -z "$(git log -1 --format=%H --fixed-strings --grep="$marker" "$local_sha" 2>/dev/null)" ]; then
|
|
697
|
+
printf '%s\\n' "navarch: refusing to push '$branch'; its history no longer contains the task ownership line '$marker' (the empty 'chore(navarch): claim task branch' commit was amended, reset, or rebased away)." >&2
|
|
698
|
+
printf '%s\\n' "navarch: restore it with: git commit --allow-empty -m 'chore(navarch): claim task branch' -m '$marker' && git push" >&2
|
|
699
|
+
status=1
|
|
575
700
|
fi
|
|
576
701
|
done
|
|
577
702
|
exit "$status"
|
|
@@ -602,6 +727,16 @@ function branchSlug(options) {
|
|
|
602
727
|
.replace(/^-+|-+$/g, "");
|
|
603
728
|
return idSlug || "task";
|
|
604
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* The hidden per-task ref hosted runs checkpoint to
|
|
732
|
+
* (worker/hosted-agent/checkpoint.ts `checkpointRef`), or null for an id that
|
|
733
|
+
* is unsafe in a refname.
|
|
734
|
+
*/
|
|
735
|
+
function hostedCheckpointRef(taskId) {
|
|
736
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId) && !taskId.includes("..")
|
|
737
|
+
? `refs/navarch/checkpoints/${taskId}`
|
|
738
|
+
: null;
|
|
739
|
+
}
|
|
605
740
|
function safePathSegment(value) {
|
|
606
741
|
const safe = value.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
|
|
607
742
|
if (!safe)
|
package/dist/prompt.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderPrompt = renderPrompt;
|
|
4
4
|
exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
|
|
5
|
+
exports.renderHostedCheckpointNote = renderHostedCheckpointNote;
|
|
5
6
|
const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
|
|
6
7
|
/**
|
|
7
8
|
* Renders the four-layer context bundle (project-plan.md §3.8: steering docs,
|
|
@@ -63,3 +64,25 @@ function renderGuidanceCorrectionPrompt(originalPrompt, guidance) {
|
|
|
63
64
|
"",
|
|
64
65
|
].join("\n");
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Tells the agent its worktree starts from a hosted attempt's checkpoint
|
|
69
|
+
* (GitWorktree.restoredHostedCheckpoint), mirroring the hosted side's own
|
|
70
|
+
* resume note (worker/hosted-agent/checkpoint.ts `buildCheckpointRestoreNote`).
|
|
71
|
+
*/
|
|
72
|
+
function renderHostedCheckpointNote(restored, deliveryBranch) {
|
|
73
|
+
const changes = restored.changedFiles > 0
|
|
74
|
+
? `${restored.changedFiles} file${restored.changedFiles === 1 ? "" : "s"} with uncommitted changes are in the working tree.`
|
|
75
|
+
: "There are no uncommitted changes.";
|
|
76
|
+
const hosted = restored.hostedBranch
|
|
77
|
+
? ` The hosted agent was working on \`${restored.hostedBranch}\`; its commits are already on \`${deliveryBranch}\`, so deliver from \`${deliveryBranch}\` and do not push \`${restored.hostedBranch}\`.`
|
|
78
|
+
: "";
|
|
79
|
+
return [
|
|
80
|
+
"## Resuming a hosted attempt",
|
|
81
|
+
"",
|
|
82
|
+
"A hosted Navarch agent worked on this task before you and was cut off before it delivered.",
|
|
83
|
+
`Navarch checkpointed its checkout${restored.savedAt ? ` at ${restored.savedAt}` : ""}, and this worktree was restored from it. ${changes}${hosted}`,
|
|
84
|
+
"",
|
|
85
|
+
"Start by inspecting that work with `git status`, `git log --oneline`, and `git diff`, then continue from it",
|
|
86
|
+
"rather than starting over. Discard it only if it is clearly wrong for the task.",
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
package/dist/session.cjs
CHANGED
|
@@ -136,7 +136,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
136
136
|
const workDir = node_path_1.default.join(config.workspaceRoot, "sessions", sessionId);
|
|
137
137
|
await node_fs_1.promises.mkdir(workDir, { recursive: true });
|
|
138
138
|
await (0, worktree_janitor_cjs_1.markSessionWorkspaceActive)(workDir);
|
|
139
|
-
|
|
139
|
+
let promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
|
|
140
140
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
141
141
|
// Secrets: initially fetched once and held in the registry + child env map.
|
|
142
142
|
// The broker-issued github-pat is additionally installed in the project-local
|
|
@@ -188,6 +188,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
190
|
const githubToken = secrets[bundle.repository?.credential_secret_name ?? "github-pat"];
|
|
191
|
+
// A root task owns one delivery branch, so the pre-push guard and the
|
|
192
|
+
// completion-time reconciliation keep the agent on the allocated
|
|
193
|
+
// `navarch/...` branch. PR-continuation tasks are the exception: their agent
|
|
194
|
+
// must check out and push the reviewed PR's existing head branch.
|
|
195
|
+
const allowBranchRename = Boolean(task.review_of_task_id);
|
|
191
196
|
const gitWorktree = new git_worktree_cjs_1.GitWorktree({
|
|
192
197
|
workspaceRoot: config.workspaceRoot,
|
|
193
198
|
projectId: task.project_id,
|
|
@@ -198,12 +203,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
198
203
|
cloneUrl,
|
|
199
204
|
githubToken,
|
|
200
205
|
repoLocalGithubToken: secrets["github-pat"],
|
|
206
|
+
// Hosted runs skip checkpoints for verify tasks, which deliver a verdict.
|
|
207
|
+
restoreHostedCheckpoint: !allowBranchRename && task.task_type !== "verify",
|
|
201
208
|
});
|
|
202
|
-
// A root task owns one delivery branch, so the pre-push guard and the
|
|
203
|
-
// completion-time reconciliation keep the agent on the allocated
|
|
204
|
-
// `navarch/...` branch. PR-continuation tasks are the exception: their agent
|
|
205
|
-
// must check out and push the reviewed PR's existing head branch.
|
|
206
|
-
const allowBranchRename = Boolean(task.review_of_task_id);
|
|
207
209
|
const knownGuidanceIds = new Set((bundle.guidance ?? []).map((entry) => entry.id));
|
|
208
210
|
const deliveredGuidance = [...(bundle.guidance ?? [])];
|
|
209
211
|
let guidanceCursor = bundle.guidance_cursor ?? null;
|
|
@@ -406,9 +408,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
406
408
|
lifecycle.enteredMainSessionTry = true;
|
|
407
409
|
try {
|
|
408
410
|
await gitWorktree.prepare();
|
|
411
|
+
if (gitWorktree.hostedCheckpointRestoreFailure) {
|
|
412
|
+
log.warn(`hosted checkpoint for ${leaseId} could not be restored: ` +
|
|
413
|
+
`${(0, redact_cjs_1.redactText)(gitWorktree.hostedCheckpointRestoreFailure, registry.list())}; starting without it.`);
|
|
414
|
+
}
|
|
415
|
+
const restoredCheckpoint = gitWorktree.restoredHostedCheckpoint;
|
|
416
|
+
if (restoredCheckpoint) {
|
|
417
|
+
log.info(`resumed hosted checkpoint ${restoredCheckpoint.sha.slice(0, 12)} for ${leaseId} ` +
|
|
418
|
+
`(${restoredCheckpoint.changedFiles} uncommitted file(s)).`);
|
|
419
|
+
promptText = `${promptText}\n\n${(0, prompt_cjs_1.renderHostedCheckpointNote)(restoredCheckpoint, gitWorktree.branch)}`;
|
|
420
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
421
|
+
}
|
|
409
422
|
// Preparation may resume an existing task branch with an older slug or ID format.
|
|
410
|
-
if (!allowBranchRename)
|
|
423
|
+
if (!allowBranchRename) {
|
|
411
424
|
sessionEnv.NAVARCH_DELIVERY_BRANCH = gitWorktree.branch;
|
|
425
|
+
const marker = gitWorktree.deliveryTaskMarker;
|
|
426
|
+
if (marker)
|
|
427
|
+
sessionEnv.NAVARCH_DELIVERY_TASK_MARKER = marker;
|
|
428
|
+
}
|
|
412
429
|
if (sandbox) {
|
|
413
430
|
await sandbox.create();
|
|
414
431
|
// Security-policy refusals (a mount outside the allowlist, an egress
|
|
@@ -432,6 +449,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
432
449
|
let nextPrompt = null;
|
|
433
450
|
let completionRemediationRetries = 0;
|
|
434
451
|
let deliveryCorrectionRetries = 0;
|
|
452
|
+
let deliveredPullRequest = false;
|
|
435
453
|
while (true) {
|
|
436
454
|
// Guidance can arrive while the worktree/sandbox is being prepared.
|
|
437
455
|
// It is already included in deliveredGuidance, so clear the pending
|
|
@@ -561,6 +579,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
561
579
|
catch (err) {
|
|
562
580
|
delivery = { status: "unknown", reason: `the delivery probe failed: ${String(err)}` };
|
|
563
581
|
}
|
|
582
|
+
deliveredPullRequest = delivery.status === "delivered";
|
|
564
583
|
if (delivery.status === "unknown") {
|
|
565
584
|
log.warn(`delivery for ${leaseId} could not be verified: ${delivery.reason}; completing anyway.`);
|
|
566
585
|
}
|
|
@@ -641,6 +660,13 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
641
660
|
};
|
|
642
661
|
try {
|
|
643
662
|
await api.completeLease(leaseId, completion);
|
|
663
|
+
if (deliveredPullRequest && completion.status === "completed" && gitWorktree.restoredHostedCheckpoint) {
|
|
664
|
+
// The pull request carries the hosted attempt's work now; a later
|
|
665
|
+
// hosted attempt must not resume the stale snapshot.
|
|
666
|
+
await gitWorktree.deleteRestoredHostedCheckpoint().catch((err) => {
|
|
667
|
+
log.warn(`hosted checkpoint cleanup failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(String(err), knownSecrets)}`);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
644
670
|
break;
|
|
645
671
|
}
|
|
646
672
|
catch (err) {
|
package/package.json
CHANGED