@amistio/cli 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Runner lifecycle controls in the web app, such as update, restart, and remove, a
15
15
 
16
16
  Runner Update installs the official `@amistio/cli` package and then refreshes the runner runtime. Background runners attempt a replacement restart so the next heartbeat reports the new CLI version. If replacement restart metadata is missing or restart fails after a successful install, the old runner still stops and reports manual restart guidance instead of continuing to heartbeat the stale runtime. Foreground `amistio run --watch` sessions stop after a successful install with restart guidance; start the command again to load the updated package.
17
17
 
18
- Current runners advertise the work kinds they can claim. Older runners that do not send this capability can continue legacy brain generation, implementation, and plan revision work, but they will skip source-aware assistant, impact-preview, semantic brain consolidation, project-context refresh, issue-diagnosis, app-evaluation, security-posture, Test-quality, implementation-Test-gate, and implementation-verification work until updated.
18
+ Current runners advertise the work kinds they can claim. Older runners that do not send this capability can continue legacy brain generation, implementation, and plan revision work, but they will skip source-aware assistant, impact-preview, semantic brain consolidation, project-context refresh, issue-diagnosis, app-evaluation, security-posture, Test-quality, implementation-Test-gate, and implementation-verification work until updated. Normal runner polling also refreshes review-only self-maintenance health in the Evaluate panel with bounded counts, trend, and safe record IDs for operational drift; it does not upload source, full document bodies, secrets, commands, local paths, or mutate cleanup targets.
19
19
 
20
20
  Tool session reuse is bounded. One-shot tool sessions close after successful completion, failed runs are blocked, active sessions are treated as already in use, and reusable sessions idle for more than six hours are closed before the next claim selects context. This keeps follow-up work from inheriting stale local AI context while preserving recent reusable sessions for related work.
21
21
 
@@ -53,7 +53,7 @@ When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until
53
53
 
54
54
  The runner advertises its supported work kinds in heartbeats. Current runners can claim read-only `projectContextRefresh` jobs from the workspace Context panel and create due runner-driven refreshes when no fresh approved map exists. Context refreshes inspect the paired checkout locally without modifying files and submit only bounded summaries, slices, entities, relations, safe citations, confidence, freshness, and repo-relative paths. If a submitted context refresh contains unsafe evidence, unsafe paths, or a map too large to store safely, Amistio marks the refresh failed with a safe reason instead of storing the rejected raw result. Approved maps are reused as context packs for source-aware assistant and impact-preview work. Current runners can also claim read-only issue diagnosis jobs from the web Issues panel, generate root-cause analysis and a proposed fix, and submit that result without modifying source. They can claim manual read-only `appEvaluationScan` jobs from the workspace Evaluate panel and create at most one due hourly evaluation during normal watch/background polling when app evaluation is enabled for the repository link. Evaluation results contain bounded summaries, safe evidence, suggested actions, lifecycle proposals, and repo-relative paths only. Current runners can also claim manual read-only `securityPostureScan` jobs from the workspace Security panel and create due daily posture checks during normal watch/background polling. Security scan results contain bounded summaries, standard references, safe evidence, and repo-relative paths only. Current runners can claim manual read-only `testQualityScan` jobs from the workspace Test panel and create one due daily Test scan per repository when Test quality is enabled. Test scans run only existing lint, typecheck, test, coverage, build, or verify commands and submit bounded command summaries, coverage summaries, safe findings, blocked reasons, warnings, and repo-relative paths. Missing tests, missing coverage, low coverage, failing checks, flaky tests, and test gaps create reviewable plan-backed findings in the app. Current runners also claim `implementationTestGate` jobs before implementation completion, PR handoff, or runner-managed push; a passing gate is required unless the web Test panel records an audited override. Blocked implementation Test gates submit structured Test findings, such as `blockedEnvironment`, with safe evidence, a suggested action, and a verification plan. Current runners can claim read-only `implementationVerification` jobs from Tasks to prove whether completed implementation work actually landed; verification submits bounded acceptance-criteria evidence, checks, gaps, outcome, and recommendation without mutating source. Source, secrets, environment variables, command lines, process lists, credentials, provider sessions, and arbitrary local paths stay local. Implementation or cleanup is queued separately only after the user approves an issue analysis, app evaluation finding, security remediation plan, or Test quality plan in the app.
55
55
 
56
- Approved implementation work uses Git as the handoff boundary. During worktree preflight, the runner locally copies eligible ignored root dotenv files such as `.env.local` or `.env.test.local` from the paired checkout into the implementation worktree when the target is missing and ignored, so local tests can use the same machine configuration. Dotenv values, variable names, file contents, and local paths are not uploaded to Amistio, and copied dotenv files stay ignored so PR handoff does not commit them. After the local tool completes successfully, the runner materializes approved Markdown, MDX, and HTML project-brain artifacts for the same work scope into the isolated worktree before final Git status. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; no-change completion requires no source changes and no approved artifact changes. Prepare the runner machine with Git commit identity, fetch/push permission to the linked remote, and `gh auth status`. If artifact materialization, commit, fetch/rebase, push, or PR creation fails, the work item is blocked and the branch/worktree stay on disk for manual recovery; source files and patches are not uploaded to Amistio.
56
+ Approved implementation work uses Git as the handoff boundary. During worktree preflight, the runner locally copies eligible ignored root dotenv files such as `.env.local` or `.env.test.local` from the paired checkout into the implementation worktree when the target is missing and ignored, so local tests can use the same machine configuration. Dotenv values, variable names, file contents, and local paths are not uploaded to Amistio, and copied dotenv files stay ignored so PR handoff does not commit them. After the local tool completes successfully, the runner materializes approved Markdown, MDX, and HTML project-brain artifacts for the same work scope into the isolated worktree before final Git status. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; no-change completion requires no source changes and no approved artifact changes, and runner-created no-change worktrees are removed after final clean checks. Prepare the runner machine with Git commit identity, fetch/push permission to the linked remote, and `gh auth status`. If artifact materialization, commit, fetch/rebase, push, or PR creation fails, the work item is blocked with safe recovery choices; source files and patches are not uploaded to Amistio. The Work panel can queue scoped Retry handoff or Retry cleanup commands to the paired runner for the same work item, branch, and worktree key. Rebase conflicts capture bounded repo-relative conflict files and try `git rebase --abort` so the implementation branch can be retried or manually reviewed without leaving an active rebase. Dirty, unmerged, or ambiguous worktrees are preserved rather than discarded.
57
57
 
58
58
  Failed or stale work can be requeued from the web Tasks panel. Requeue creates a new linked work attempt and preserves the original terminal attempt for audit history; it is blocked while equivalent work is already active or when the paired runner does not advertise the needed work kind. Completed implementation status is separate from proof: queue `implementationVerification` from Tasks when a plan needs source-aware evidence before cleanup or implementation status decisions.
59
59
 
package/dist/index.js CHANGED
@@ -148,6 +148,9 @@ var autopilotGuardCheckSchema = z.object({
148
148
  });
149
149
  var implementationHandoffStatusSchema = z.enum(["notStarted", "noChanges", "prReady", "blocked", "failed"]);
150
150
  var implementationHandoffCleanupStatusSchema = z.enum(["notApplicable", "pending", "completed", "failed"]);
151
+ var implementationHandoffRecoveryCategorySchema = z.enum(["noChangeCleaned", "rebaseConflict", "dirtyWorktree", "unresolvedConflicts", "providerBlocked", "cleanupRetryAvailable", "manualReview", "artifactBlocked", "nonGithubProvider"]);
152
+ var implementationHandoffRecoveryActionSchema = z.enum(["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "cleanNoChangeWorktree", "markSupersededNoChanges", "exportHandoffDetails", "retryCleanup"]);
153
+ var implementationHandoffRebaseAbortStatusSchema = z.enum(["notAttempted", "succeeded", "failed", "notApplicable"]);
151
154
  var safeRepoPathSchema = z.string().trim().min(1).max(300).refine((value) => {
152
155
  if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)) {
153
156
  return false;
@@ -168,6 +171,15 @@ var implementationHandoffArtifactsSchema = z.object({
168
171
  skipped: z.array(implementationHandoffArtifactSchema).max(50).default([]),
169
172
  blocked: z.array(implementationHandoffArtifactSchema).max(50).default([])
170
173
  }).strict();
174
+ var implementationHandoffRecoverySchema = z.object({
175
+ category: implementationHandoffRecoveryCategorySchema,
176
+ availableActions: z.array(implementationHandoffRecoveryActionSchema).max(8).default([]),
177
+ cleanupEligible: z.boolean().optional(),
178
+ conflictFiles: z.array(safeRepoPathSchema).max(25).default([]),
179
+ rebaseAbortStatus: implementationHandoffRebaseAbortStatusSchema.optional(),
180
+ summary: z.string().trim().min(1).max(600).optional(),
181
+ worktreeKey: z.string().trim().min(1).max(200).optional()
182
+ }).strict();
171
183
  var implementationVerificationOutcomeSchema = z.enum(["verifiedImplemented", "partiallyImplemented", "notImplemented", "inconclusive", "verificationBlocked"]);
172
184
  var implementationVerificationStatusSchema = z.enum(["queued", "running", "completed", "failed", "blocked", "stale"]);
173
185
  var implementationProofStatusSchema = z.enum(["unverified", "verificationQueued", "verificationRunning", "verifiedImplemented", "partiallyImplemented", "notImplemented", "inconclusive", "verificationBlocked", "humanOverride"]);
@@ -183,6 +195,7 @@ var implementationHandoffSchema = z.object({
183
195
  cleanupStatus: implementationHandoffCleanupStatusSchema.optional(),
184
196
  cleanupMessage: z.string().trim().min(1).max(600).optional(),
185
197
  artifacts: implementationHandoffArtifactsSchema.optional(),
198
+ recovery: implementationHandoffRecoverySchema.optional(),
186
199
  message: z.string().trim().min(1).max(600).optional(),
187
200
  error: z.string().trim().min(1).max(1200).optional()
188
201
  }).strict();
@@ -905,7 +918,7 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
905
918
  lastUsedAt: isoDateTimeSchema.optional(),
906
919
  status: z.enum(["active", "revoked"]).default("active")
907
920
  });
908
- var runnerCommandKindSchema = z.enum(["update", "restart", "remove"]);
921
+ var runnerCommandKindSchema = z.enum(["update", "restart", "remove", "implementationHandoffRecovery"]);
909
922
  var runnerCommandStatusSchema = z.enum(["pending", "acknowledged", "running", "completed", "failed", "expired", "cancelled"]);
910
923
  var runnerCommandItemSchema = baseItemSchema.extend({
911
924
  type: z.literal("runnerCommand"),
@@ -915,6 +928,10 @@ var runnerCommandItemSchema = baseItemSchema.extend({
915
928
  status: runnerCommandStatusSchema,
916
929
  runnerId: z.string().min(1),
917
930
  repositoryLinkId: z.string().min(1),
931
+ workItemId: z.string().min(1).optional(),
932
+ handoffRecoveryAction: implementationHandoffRecoveryActionSchema.optional(),
933
+ executionBranch: z.string().min(1).optional(),
934
+ executionWorktreeKey: z.string().min(1).optional(),
918
935
  requestedByUserId: z.string().min(1),
919
936
  idempotencyKey: z.string().min(1),
920
937
  lastStatusIdempotencyKey: z.string().min(1).optional(),
@@ -6188,6 +6205,7 @@ function createAppEvaluationScanPrompt(workItem, context) {
6188
6205
  "- Treat intentionally in-progress feature tracks as still-active work when their controlling plan/feature has unchecked requirements or explicit follow-up gaps. For example, a completed first implementation prompt does not make the broader feature stale if PLAN/FEAT evidence says remaining lifecycle work is still open; return proposedLifecycleAction keepActive with evidence instead of cleanup.",
6189
6206
  "- Treat prompt frontmatter status Ready as an active execution backlog state by default, not as stale review debt. Only flag a Ready prompt for metadata correction when its controlling plan, feature, prompt index, or verification evidence unambiguously proves the prompt has already completed or been superseded.",
6190
6207
  "- Treat implemented umbrella plans that explicitly label unchecked checklist items as deferred follow-ups, future candidates, roadmap backlog, or split-out hardening phases as valid deferred backlog. Do not mark the umbrella incomplete or stale solely because those deferred items remain unchecked; use keepActive or no cleanup, and recommend a fresh focused plan only when a concrete deferred slice has current evidence and approval.",
6208
+ "- Treat reusable hygiene tooling and recurring health-loop prompts/plans as active operational backlog when the one-time cleanup or prevention pass has executed but the repeatable utility or recurring workflow remains explicitly Ready, Proposed, or unchecked. Do not archive them just because the initial cleanup succeeded; use keepActive unless controlling evidence proves the reusable work was completed, superseded, or rejected.",
6191
6209
  "- When lifecycle metadata disagrees across indexes, frontmatter, feature specs, ADRs, and implementation evidence, cite the conflict and propose a metadata correction or verification step instead of archival/removal.",
6192
6210
  "- Check missing memory or workflow updates when repeated lessons or operational rules are visible.",
6193
6211
  "- Check release readiness, UX, accessibility, performance, reliability, and security-posture follow-through at a summary level.",
@@ -7743,6 +7761,14 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
7743
7761
  const preparedLocalEnvironmentFileCount = await prepareLocalWorktreeEnvironment(repoRoot, worktreePath);
7744
7762
  return { ...identity, baseRevision, worktreePath, ...preparedLocalEnvironmentFileCount ? { preparedLocalEnvironmentFileCount } : {} };
7745
7763
  }
7764
+ async function resolveExistingGitWorktreeIsolation(rootDir, workItem) {
7765
+ const identity = resolveWorktreeIdentity(workItem);
7766
+ const repoRoot = await gitOutput(rootDir, ["rev-parse", "--show-toplevel"]);
7767
+ const baseRevision = await gitOutput(repoRoot, ["rev-parse", "HEAD"]);
7768
+ const worktreePath = localWorktreePath(repoRoot, identity.worktreeKey);
7769
+ await assertExistingWorktree(worktreePath, identity.branch);
7770
+ return { ...identity, baseRevision, worktreePath };
7771
+ }
7746
7772
  function localWorktreePath(repoRoot, worktreeKey) {
7747
7773
  const repoName = path14.basename(repoRoot);
7748
7774
  const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
@@ -7874,16 +7900,37 @@ async function completeImplementationHandoff(input) {
7874
7900
  }
7875
7901
  const unmergedFiles = await gitOutput2(run, input.worktreePath, ["diff", "--name-only", "--diff-filter=U"]);
7876
7902
  if (unmergedFiles.trim()) {
7877
- return blockedHandoff({ ...common, artifacts: artifactResult, message: "Implementation handoff is blocked because the worktree has unresolved merge conflicts." });
7903
+ return blockedHandoff({
7904
+ ...common,
7905
+ artifacts: artifactResult,
7906
+ message: "Implementation handoff is blocked because the worktree has unresolved merge conflicts.",
7907
+ recovery: handoffRecovery(input, {
7908
+ category: "unresolvedConflicts",
7909
+ availableActions: ["requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
7910
+ cleanupEligible: false,
7911
+ conflictFiles: safeRepoPaths(unmergedFiles),
7912
+ rebaseAbortStatus: "notApplicable",
7913
+ summary: "Resolve the local conflicts manually or requeue a fresh attempt after reviewing the preserved worktree."
7914
+ })
7915
+ });
7878
7916
  }
7879
7917
  const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
7880
7918
  if (!status) {
7919
+ const cleanup2 = input.worktreeIsolation ? await cleanupWorktree(run, input) : { status: "notApplicable" };
7881
7920
  return {
7882
7921
  ...common,
7883
7922
  status: "noChanges",
7884
- cleanupStatus: "notApplicable",
7923
+ cleanupStatus: cleanup2.status,
7924
+ ...cleanup2.message ? { cleanupMessage: cleanup2.message } : {},
7885
7925
  artifacts: artifactResult,
7886
- message: "Local execution completed with no repository changes to hand off."
7926
+ recovery: handoffRecovery(input, {
7927
+ category: cleanup2.status === "completed" ? "noChangeCleaned" : cleanup2.status === "failed" ? "cleanupRetryAvailable" : "manualReview",
7928
+ availableActions: cleanup2.status === "failed" ? ["retryCleanup", "markSupersededNoChanges", "exportHandoffDetails"] : ["markSupersededNoChanges", "exportHandoffDetails"],
7929
+ cleanupEligible: cleanup2.status === "failed",
7930
+ rebaseAbortStatus: "notApplicable",
7931
+ summary: cleanup2.status === "completed" ? "No repository changes were found and the runner-created worktree was removed." : "No repository changes were found for handoff."
7932
+ }),
7933
+ message: cleanup2.status === "completed" ? "Local execution completed with no repository changes to hand off and the local worktree was cleaned up." : "Local execution completed with no repository changes to hand off."
7887
7934
  };
7888
7935
  }
7889
7936
  const remoteName = await resolveRemoteName(run, input.worktreePath);
@@ -7895,12 +7942,25 @@ async function completeImplementationHandoff(input) {
7895
7942
  provider,
7896
7943
  remoteName,
7897
7944
  artifacts: artifactResult,
7898
- message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository."
7945
+ message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository.",
7946
+ recovery: handoffRecovery(input, {
7947
+ category: "nonGithubProvider",
7948
+ availableActions: ["requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
7949
+ cleanupEligible: false,
7950
+ rebaseAbortStatus: "notApplicable",
7951
+ summary: "Automated retry is unavailable until this project uses a GitHub repository link."
7952
+ })
7899
7953
  });
7900
7954
  }
7901
7955
  await gitOutput2(run, input.worktreePath, ["add", "-A"]);
7902
7956
  await gitOutput2(run, input.worktreePath, ["commit", "-m", commitSubject(input.workItem), "-m", commitBody(input)]);
7903
- await rebaseBranchFromRemoteBase(run, input.worktreePath, { baseBranch, remoteName });
7957
+ const preRebaseCommitSha = await gitOutput2(run, input.worktreePath, ["rev-parse", "HEAD"]);
7958
+ await fetchRemoteBase(run, input.worktreePath, { baseBranch, remoteName });
7959
+ try {
7960
+ await rebaseBranchFromRemoteBase(run, input.worktreePath);
7961
+ } catch (error) {
7962
+ return blockedRebaseHandoff({ artifactResult, baseBranch, error, headBranch, input, preRebaseCommitSha, remoteName, run });
7963
+ }
7904
7964
  const commitSha = await gitOutput2(run, input.worktreePath, ["rev-parse", "HEAD"]);
7905
7965
  await gitOutput2(run, input.worktreePath, ["push", "--set-upstream", remoteName, headBranch]);
7906
7966
  const pullRequest = await ensureGithubPullRequest(run, input.worktreePath, { artifacts: artifactResult, baseBranch, headBranch, workItem: input.workItem, ...input.verificationSummary ? { verificationSummary: input.verificationSummary } : {} });
@@ -7916,13 +7976,60 @@ async function completeImplementationHandoff(input) {
7916
7976
  prUrl: pullRequest.url,
7917
7977
  cleanupStatus: cleanup.status,
7918
7978
  ...cleanup.message ? { cleanupMessage: cleanup.message } : {},
7979
+ ...cleanup.status === "failed" ? {
7980
+ recovery: handoffRecovery(input, {
7981
+ category: "cleanupRetryAvailable",
7982
+ availableActions: ["retryCleanup", "exportHandoffDetails"],
7983
+ cleanupEligible: true,
7984
+ rebaseAbortStatus: "notApplicable",
7985
+ summary: "The pull request is durable, but local worktree cleanup needs another runner-local attempt."
7986
+ })
7987
+ } : {},
7919
7988
  artifacts: artifactResult,
7920
7989
  message: cleanup.status === "completed" ? "GitHub pull request is ready for review and the local worktree was cleaned up." : "GitHub pull request is ready for review; local worktree cleanup needs attention."
7921
7990
  };
7922
7991
  } catch (error) {
7923
- return blockedHandoff({ ...common, message: "Implementation handoff is blocked and the local worktree was preserved for recovery.", error: safeErrorMessage(error) });
7992
+ return blockedHandoff({
7993
+ ...common,
7994
+ message: "Implementation handoff is blocked and the local worktree was preserved for recovery.",
7995
+ error: safeErrorMessage(error),
7996
+ recovery: handoffRecovery(input, {
7997
+ category: "providerBlocked",
7998
+ availableActions: ["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
7999
+ cleanupEligible: false,
8000
+ rebaseAbortStatus: "notAttempted",
8001
+ summary: "Retry handoff after fixing local Git, remote, or GitHub CLI access; requeue only if a fresh attempt is preferable."
8002
+ })
8003
+ });
7924
8004
  }
7925
8005
  }
8006
+ async function cleanupImplementationWorktree(input) {
8007
+ const run = input.commandRunner ?? defaultCommandRunner;
8008
+ return cleanupWorktree(run, input);
8009
+ }
8010
+ async function blockedRebaseHandoff(input) {
8011
+ const conflictFiles = await safeConflictFiles(input.run, input.input.worktreePath);
8012
+ const rebaseAbortStatus = await abortRebaseForRecovery(input.run, input.input.worktreePath, input.preRebaseCommitSha);
8013
+ return blockedHandoff({
8014
+ provider: "github",
8015
+ status: "blocked",
8016
+ baseBranch: input.baseBranch,
8017
+ headBranch: input.headBranch,
8018
+ remoteName: input.remoteName,
8019
+ commitSha: input.preRebaseCommitSha,
8020
+ artifacts: input.artifactResult,
8021
+ message: rebaseAbortStatus === "succeeded" ? "Implementation handoff hit a rebase conflict. Conflict files were captured and the branch was restored for recovery." : "Implementation handoff hit a rebase conflict and the local worktree was preserved for recovery.",
8022
+ error: safeErrorMessage(input.error),
8023
+ recovery: handoffRecovery(input.input, {
8024
+ category: "rebaseConflict",
8025
+ availableActions: ["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
8026
+ cleanupEligible: false,
8027
+ conflictFiles,
8028
+ rebaseAbortStatus,
8029
+ summary: rebaseAbortStatus === "succeeded" ? "The failed rebase was aborted after conflict metadata was captured, so the implementation branch can be retried or reviewed manually." : "The failed rebase could not be safely aborted; review the preserved worktree before retrying."
8030
+ })
8031
+ });
8032
+ }
7926
8033
  async function materializeApprovedArtifacts(input) {
7927
8034
  const { selected: artifacts, skipped } = selectApprovedWorkArtifacts(input.workItem, input.approvedArtifacts ?? []);
7928
8035
  if (!artifacts.length) {
@@ -7940,6 +8047,38 @@ async function materializeApprovedArtifacts(input) {
7940
8047
  blocked: materialized.conflicts.map((conflict) => artifactStatus(findArtifactByPath(artifacts, conflictRepoPath(conflict)), "blocked", conflict))
7941
8048
  };
7942
8049
  }
8050
+ function handoffRecovery(input, recovery) {
8051
+ const worktreeKey = input.worktreeIsolation?.worktreeKey ?? input.workItem.executionWorktreeKey;
8052
+ return {
8053
+ ...recovery,
8054
+ conflictFiles: recovery.conflictFiles ?? [],
8055
+ ...worktreeKey ? { worktreeKey } : {}
8056
+ };
8057
+ }
8058
+ async function safeConflictFiles(run, cwd) {
8059
+ const unmergedFiles = await gitOutput2(run, cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "");
8060
+ return safeRepoPaths(unmergedFiles);
8061
+ }
8062
+ async function abortRebaseForRecovery(run, cwd, expectedHead) {
8063
+ try {
8064
+ await gitOutput2(run, cwd, ["rebase", "--abort"]);
8065
+ const currentHead = await gitOutput2(run, cwd, ["rev-parse", "HEAD"]);
8066
+ return currentHead === expectedHead ? "succeeded" : "failed";
8067
+ } catch {
8068
+ return "failed";
8069
+ }
8070
+ }
8071
+ function safeRepoPaths(output) {
8072
+ const seen = /* @__PURE__ */ new Set();
8073
+ for (const value of output.split(/\0|\r?\n/g)) {
8074
+ const repoPath = value.trim();
8075
+ if (!repoPath || repoPath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(repoPath) || repoPath.split(/[\\/]+/).includes("..")) {
8076
+ continue;
8077
+ }
8078
+ seen.add(repoPath);
8079
+ }
8080
+ return [...seen].slice(0, 25);
8081
+ }
7943
8082
  function selectApprovedWorkArtifacts(workItem, documents) {
7944
8083
  const draftId = workItem.generatedDraftId;
7945
8084
  const explicitDocumentIds = new Set([workItem.reviewDocumentId, workItem.impactDocumentId].filter((value) => Boolean(value)));
@@ -8016,8 +8155,10 @@ async function resolveRemoteName(run, cwd) {
8016
8155
  }
8017
8156
  return remotes.includes("origin") ? "origin" : remotes[0];
8018
8157
  }
8019
- async function rebaseBranchFromRemoteBase(run, cwd, input) {
8158
+ async function fetchRemoteBase(run, cwd, input) {
8020
8159
  await gitOutput2(run, cwd, ["fetch", input.remoteName, input.baseBranch]);
8160
+ }
8161
+ async function rebaseBranchFromRemoteBase(run, cwd) {
8021
8162
  await gitOutput2(run, cwd, ["rebase", "FETCH_HEAD"]);
8022
8163
  }
8023
8164
  async function gitOutput2(run, cwd, args) {
@@ -9670,7 +9811,7 @@ async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
9670
9811
  }
9671
9812
  await updateRunnerCommandStatus(apiClient, context, command, "acknowledged", "Command acknowledged by local runner.");
9672
9813
  await updateRunnerCommandStatus(apiClient, context, command, "running", `Running ${runnerCommandLabel(command.commandKind)} command.`);
9673
- const result = await executeRunnerCommand(command, context);
9814
+ const result = await executeRunnerCommand(apiClient, command, context);
9674
9815
  await updateRunnerCommandStatus(apiClient, context, command, result.succeeded ? "completed" : "failed", result.message, result.error);
9675
9816
  if (command.commandKind === "remove" && result.succeeded) {
9676
9817
  await new LocalCredentialStore().delete(credentialKey(context.accountId, context.projectId, context.repositoryLinkId));
@@ -9691,18 +9832,100 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
9691
9832
  });
9692
9833
  return result.command;
9693
9834
  }
9694
- async function executeRunnerCommand(command, context) {
9835
+ async function executeRunnerCommand(apiClient, command, context) {
9695
9836
  if (command.commandKind === "remove") {
9696
9837
  return { succeeded: true, stopRunner: true, message: "Runner credential revoked. This local runner will stop after removing its stored credential." };
9697
9838
  }
9698
9839
  if (command.commandKind === "restart") {
9699
9840
  return restartCurrentRunner(context);
9700
9841
  }
9842
+ if (command.commandKind === "implementationHandoffRecovery") {
9843
+ return executeImplementationHandoffRecoveryCommand(apiClient, command, context);
9844
+ }
9701
9845
  return runOfficialCliUpdateWithRuntimeRefresh({
9702
9846
  mode: currentRunnerMode(),
9703
9847
  restartBackgroundRunner: () => restartCurrentRunner(context, { useUpdatedCliExecutable: true })
9704
9848
  });
9705
9849
  }
9850
+ async function executeImplementationHandoffRecoveryCommand(apiClient, command, context) {
9851
+ if (!command.workItemId || !command.handoffRecoveryAction) {
9852
+ return { succeeded: false, message: "Handoff recovery command is missing a scoped work item or action." };
9853
+ }
9854
+ const workItem = await findRecoveryWorkItem(apiClient, context.projectId, command.workItemId);
9855
+ if (!workItem) {
9856
+ return { succeeded: false, message: "Handoff recovery work item was not found." };
9857
+ }
9858
+ if (workItem.repositoryLinkId && workItem.repositoryLinkId !== context.repositoryLinkId) {
9859
+ return { succeeded: false, message: "Handoff recovery command is not scoped to this repository link." };
9860
+ }
9861
+ if (!workItem.implementationHandoff?.recovery?.availableActions.includes(command.handoffRecoveryAction)) {
9862
+ return { succeeded: false, message: "Handoff recovery action is no longer available for this work item." };
9863
+ }
9864
+ try {
9865
+ if (command.handoffRecoveryAction === "retryHandoff") {
9866
+ return retryImplementationHandoff(apiClient, context, workItem);
9867
+ }
9868
+ if (isCleanupHandoffRecoveryAction(command.handoffRecoveryAction)) {
9869
+ return retryImplementationHandoffCleanup(apiClient, context, workItem);
9870
+ }
9871
+ return { succeeded: false, message: "Handoff recovery action is not a runner-local command." };
9872
+ } catch (error) {
9873
+ return { succeeded: false, message: "Handoff recovery command failed locally.", error: errorMessage3(error) };
9874
+ }
9875
+ }
9876
+ async function retryImplementationHandoff(apiClient, context, workItem) {
9877
+ const worktreeIsolation = await resolveExistingGitWorktreeIsolation(context.root, workItem);
9878
+ const [{ repositoryLinks }, { documents }] = await Promise.all([apiClient.listRepositoryLinks(context.projectId), apiClient.listBrainDocuments(context.projectId)]);
9879
+ const repositoryLink = repositoryLinks.find((link) => link.repositoryLinkId === context.repositoryLinkId);
9880
+ const handoff = await completeImplementationHandoff({ approvedArtifacts: documents, primaryRepoRoot: context.root, ...repositoryLink ? { repositoryLink } : {}, workItem, worktreeIsolation, worktreePath: worktreeIsolation.worktreePath });
9881
+ await submitRecoveredHandoff(apiClient, context, workItem, handoff, "retryHandoff");
9882
+ return {
9883
+ succeeded: handoff.status === "prReady" || handoff.status === "noChanges",
9884
+ message: handoff.message ?? handoffStatusMessage(handoff),
9885
+ ...handoff.error ? { error: handoff.error } : {}
9886
+ };
9887
+ }
9888
+ async function retryImplementationHandoffCleanup(apiClient, context, workItem) {
9889
+ const worktreeIsolation = await resolveExistingGitWorktreeIsolation(context.root, workItem);
9890
+ const cleanup = await cleanupImplementationWorktree({ primaryRepoRoot: context.root, worktreePath: worktreeIsolation.worktreePath });
9891
+ const previousHandoff = workItem.implementationHandoff;
9892
+ const handoff = {
9893
+ ...previousHandoff ?? { status: "noChanges" },
9894
+ cleanupStatus: cleanup.status,
9895
+ ...cleanup.message ? { cleanupMessage: cleanup.message } : {},
9896
+ message: cleanup.status === "completed" ? "Local handoff worktree cleanup completed." : "Local handoff worktree cleanup still needs attention.",
9897
+ ...cleanup.status === "failed" && previousHandoff?.recovery ? { recovery: previousHandoff.recovery } : {}
9898
+ };
9899
+ await submitRecoveredHandoff(apiClient, context, workItem, handoff, "retryCleanup");
9900
+ return { succeeded: cleanup.status === "completed", message: handoff.message ?? handoffStatusMessage(handoff), ...cleanup.message ? { error: cleanup.message } : {} };
9901
+ }
9902
+ async function findRecoveryWorkItem(apiClient, projectId, workItemId) {
9903
+ const { workItems } = await apiClient.listWorkItems(projectId);
9904
+ return workItems.find((item) => item.workItemId === workItemId);
9905
+ }
9906
+ async function submitRecoveredHandoff(apiClient, context, workItem, handoff, action) {
9907
+ await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID()}`, context.runnerId, {
9908
+ implementationHandoff: handoff,
9909
+ ...handoff.message ? { message: handoff.message } : {},
9910
+ ...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
9911
+ ...workItem.implementationScopeId ? { implementationScopeId: workItem.implementationScopeId } : {},
9912
+ ...workItem.executionBranch ? { executionBranch: workItem.executionBranch } : {},
9913
+ ...workItem.executionWorktreeKey ? { executionWorktreeKey: workItem.executionWorktreeKey } : {},
9914
+ ...workItem.isolationMode ? { isolationMode: workItem.isolationMode } : {},
9915
+ ...workItem.repositoryLockId ? { repositoryLockId: workItem.repositoryLockId } : {}
9916
+ });
9917
+ }
9918
+ function recoveredHandoffWorkStatus(handoff) {
9919
+ return handoff.status === "prReady" || handoff.status === "noChanges" ? "completed" : "blocked";
9920
+ }
9921
+ function handoffStatusMessage(handoff) {
9922
+ if (handoff.status === "prReady") return "GitHub pull request is ready for review.";
9923
+ if (handoff.status === "noChanges") return "No repository changes were found.";
9924
+ return "Implementation handoff remains blocked.";
9925
+ }
9926
+ function isCleanupHandoffRecoveryAction(action) {
9927
+ return action === "retryCleanup" || action === "cleanNoChangeWorktree";
9928
+ }
9706
9929
  async function restartCurrentRunner(context, options = {}) {
9707
9930
  if (currentRunnerMode() !== "background") {
9708
9931
  return { succeeded: false, message: "Foreground runners cannot be restarted remotely. Stop and start the local command manually." };
@@ -9721,6 +9944,7 @@ async function restartCurrentRunner(context, options = {}) {
9721
9944
  function runnerCommandLabel(commandKind) {
9722
9945
  if (commandKind === "update") return "update";
9723
9946
  if (commandKind === "restart") return "restart";
9947
+ if (commandKind === "implementationHandoffRecovery") return "handoff recovery";
9724
9948
  return "remove";
9725
9949
  }
9726
9950
  async function replayPendingBrainGenerationFinalizations({ accountId, apiClient, projectId, repositoryLinkId, runnerId }) {