@amistio/cli 0.1.34 → 0.1.36
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 +1 -1
- package/dist/index.js +279 -10
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
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(),
|
|
@@ -5773,6 +5790,25 @@ var canonicalProjectContextCitationSources = /* @__PURE__ */ new Map([
|
|
|
5773
5790
|
["runnerstate", "runnerState"],
|
|
5774
5791
|
["mixed", "mixed"]
|
|
5775
5792
|
]);
|
|
5793
|
+
var canonicalProjectContextFreshnessValues = /* @__PURE__ */ new Map([
|
|
5794
|
+
["fresh", "fresh"],
|
|
5795
|
+
["current", "fresh"],
|
|
5796
|
+
["uptodate", "fresh"],
|
|
5797
|
+
["accurate", "fresh"],
|
|
5798
|
+
["stale", "stale"],
|
|
5799
|
+
["outdated", "stale"],
|
|
5800
|
+
["outofdate", "stale"],
|
|
5801
|
+
["old", "stale"],
|
|
5802
|
+
["historical", "stale"],
|
|
5803
|
+
["partial", "partial"],
|
|
5804
|
+
["partiallyfresh", "partial"],
|
|
5805
|
+
["incomplete", "partial"],
|
|
5806
|
+
["mixed", "partial"],
|
|
5807
|
+
["missing", "missing"],
|
|
5808
|
+
["absent", "missing"],
|
|
5809
|
+
["notfound", "missing"],
|
|
5810
|
+
["notpresent", "missing"]
|
|
5811
|
+
]);
|
|
5776
5812
|
function createImplementationVerificationPrompt(workItem) {
|
|
5777
5813
|
return [
|
|
5778
5814
|
"# Amistio Implementation Verification",
|
|
@@ -6065,6 +6101,7 @@ function createProjectContextRefreshPrompt(workItem, context) {
|
|
|
6065
6101
|
"- Use only these exact singular slice kind values: overview, architecture, domain, data, api, frontend, backend, cli, workflow, operations, security, testing, unknown.",
|
|
6066
6102
|
"- Capture entities and relations that explain how the app is put together and where future work should look first.",
|
|
6067
6103
|
"- Prefer summaries, repository-relative paths, short citations, tags, and freshness status over raw source excerpts.",
|
|
6104
|
+
"- Use only these exact freshness values for coverage.status and slices[].freshness: fresh, stale, partial, missing.",
|
|
6068
6105
|
"- Use only these exact citation source values: projectBrain, localSource, runnerState, mixed. Approved project-brain records use projectBrain, not approvedBrain.",
|
|
6069
6106
|
"- Mark stale or missing areas explicitly instead of guessing.",
|
|
6070
6107
|
"- Keep coverage.missingAreas entries concise noun phrases under 160 characters.",
|
|
@@ -6811,6 +6848,11 @@ function normalizeProjectContextRefreshEnums(value) {
|
|
|
6811
6848
|
return value;
|
|
6812
6849
|
}
|
|
6813
6850
|
const normalized = { ...value };
|
|
6851
|
+
if (isObjectRecord(normalized.coverage)) {
|
|
6852
|
+
const normalizedCoverage = { ...normalized.coverage };
|
|
6853
|
+
normalizedCoverage.status = normalizeProjectContextFreshness(normalizedCoverage.status);
|
|
6854
|
+
normalized.coverage = normalizedCoverage;
|
|
6855
|
+
}
|
|
6814
6856
|
if (Array.isArray(normalized.slices)) {
|
|
6815
6857
|
normalized.slices = normalized.slices.map((slice) => {
|
|
6816
6858
|
if (!isObjectRecord(slice)) {
|
|
@@ -6818,6 +6860,7 @@ function normalizeProjectContextRefreshEnums(value) {
|
|
|
6818
6860
|
}
|
|
6819
6861
|
const normalizedSlice = { ...slice };
|
|
6820
6862
|
normalizedSlice.kind = normalizeProjectContextSliceKind(slice.kind);
|
|
6863
|
+
normalizedSlice.freshness = normalizeProjectContextFreshness(slice.freshness);
|
|
6821
6864
|
return normalizedSlice;
|
|
6822
6865
|
});
|
|
6823
6866
|
}
|
|
@@ -6928,6 +6971,26 @@ function normalizeProjectContextSliceKind(value) {
|
|
|
6928
6971
|
}
|
|
6929
6972
|
return "unknown";
|
|
6930
6973
|
}
|
|
6974
|
+
function normalizeProjectContextFreshness(value) {
|
|
6975
|
+
if (typeof value !== "string") {
|
|
6976
|
+
return value;
|
|
6977
|
+
}
|
|
6978
|
+
const trimmed = value.trim();
|
|
6979
|
+
if (!trimmed) {
|
|
6980
|
+
return value;
|
|
6981
|
+
}
|
|
6982
|
+
const direct = canonicalProjectContextFreshnessValues.get(normalizeEnumKey(trimmed));
|
|
6983
|
+
if (direct) {
|
|
6984
|
+
return direct;
|
|
6985
|
+
}
|
|
6986
|
+
for (const segment of trimmed.split(/[\/,|]+/)) {
|
|
6987
|
+
const segmentFreshness = canonicalProjectContextFreshnessValues.get(normalizeEnumKey(segment));
|
|
6988
|
+
if (segmentFreshness) {
|
|
6989
|
+
return segmentFreshness;
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
return "partial";
|
|
6993
|
+
}
|
|
6931
6994
|
function normalizeProjectContextRefreshBoundedText(value) {
|
|
6932
6995
|
if (!isObjectRecord(value)) {
|
|
6933
6996
|
return value;
|
|
@@ -7744,6 +7807,14 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
|
7744
7807
|
const preparedLocalEnvironmentFileCount = await prepareLocalWorktreeEnvironment(repoRoot, worktreePath);
|
|
7745
7808
|
return { ...identity, baseRevision, worktreePath, ...preparedLocalEnvironmentFileCount ? { preparedLocalEnvironmentFileCount } : {} };
|
|
7746
7809
|
}
|
|
7810
|
+
async function resolveExistingGitWorktreeIsolation(rootDir, workItem) {
|
|
7811
|
+
const identity = resolveWorktreeIdentity(workItem);
|
|
7812
|
+
const repoRoot = await gitOutput(rootDir, ["rev-parse", "--show-toplevel"]);
|
|
7813
|
+
const baseRevision = await gitOutput(repoRoot, ["rev-parse", "HEAD"]);
|
|
7814
|
+
const worktreePath = localWorktreePath(repoRoot, identity.worktreeKey);
|
|
7815
|
+
await assertExistingWorktree(worktreePath, identity.branch);
|
|
7816
|
+
return { ...identity, baseRevision, worktreePath };
|
|
7817
|
+
}
|
|
7747
7818
|
function localWorktreePath(repoRoot, worktreeKey) {
|
|
7748
7819
|
const repoName = path14.basename(repoRoot);
|
|
7749
7820
|
const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
|
|
@@ -7875,16 +7946,37 @@ async function completeImplementationHandoff(input) {
|
|
|
7875
7946
|
}
|
|
7876
7947
|
const unmergedFiles = await gitOutput2(run, input.worktreePath, ["diff", "--name-only", "--diff-filter=U"]);
|
|
7877
7948
|
if (unmergedFiles.trim()) {
|
|
7878
|
-
return blockedHandoff({
|
|
7949
|
+
return blockedHandoff({
|
|
7950
|
+
...common,
|
|
7951
|
+
artifacts: artifactResult,
|
|
7952
|
+
message: "Implementation handoff is blocked because the worktree has unresolved merge conflicts.",
|
|
7953
|
+
recovery: handoffRecovery(input, {
|
|
7954
|
+
category: "unresolvedConflicts",
|
|
7955
|
+
availableActions: ["requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
|
|
7956
|
+
cleanupEligible: false,
|
|
7957
|
+
conflictFiles: safeRepoPaths(unmergedFiles),
|
|
7958
|
+
rebaseAbortStatus: "notApplicable",
|
|
7959
|
+
summary: "Resolve the local conflicts manually or requeue a fresh attempt after reviewing the preserved worktree."
|
|
7960
|
+
})
|
|
7961
|
+
});
|
|
7879
7962
|
}
|
|
7880
7963
|
const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
7881
7964
|
if (!status) {
|
|
7965
|
+
const cleanup2 = input.worktreeIsolation ? await cleanupWorktree(run, input) : { status: "notApplicable" };
|
|
7882
7966
|
return {
|
|
7883
7967
|
...common,
|
|
7884
7968
|
status: "noChanges",
|
|
7885
|
-
cleanupStatus:
|
|
7969
|
+
cleanupStatus: cleanup2.status,
|
|
7970
|
+
...cleanup2.message ? { cleanupMessage: cleanup2.message } : {},
|
|
7886
7971
|
artifacts: artifactResult,
|
|
7887
|
-
|
|
7972
|
+
recovery: handoffRecovery(input, {
|
|
7973
|
+
category: cleanup2.status === "completed" ? "noChangeCleaned" : cleanup2.status === "failed" ? "cleanupRetryAvailable" : "manualReview",
|
|
7974
|
+
availableActions: cleanup2.status === "failed" ? ["retryCleanup", "markSupersededNoChanges", "exportHandoffDetails"] : ["markSupersededNoChanges", "exportHandoffDetails"],
|
|
7975
|
+
cleanupEligible: cleanup2.status === "failed",
|
|
7976
|
+
rebaseAbortStatus: "notApplicable",
|
|
7977
|
+
summary: cleanup2.status === "completed" ? "No repository changes were found and the runner-created worktree was removed." : "No repository changes were found for handoff."
|
|
7978
|
+
}),
|
|
7979
|
+
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."
|
|
7888
7980
|
};
|
|
7889
7981
|
}
|
|
7890
7982
|
const remoteName = await resolveRemoteName(run, input.worktreePath);
|
|
@@ -7896,12 +7988,25 @@ async function completeImplementationHandoff(input) {
|
|
|
7896
7988
|
provider,
|
|
7897
7989
|
remoteName,
|
|
7898
7990
|
artifacts: artifactResult,
|
|
7899
|
-
message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository."
|
|
7991
|
+
message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository.",
|
|
7992
|
+
recovery: handoffRecovery(input, {
|
|
7993
|
+
category: "nonGithubProvider",
|
|
7994
|
+
availableActions: ["requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
|
|
7995
|
+
cleanupEligible: false,
|
|
7996
|
+
rebaseAbortStatus: "notApplicable",
|
|
7997
|
+
summary: "Automated retry is unavailable until this project uses a GitHub repository link."
|
|
7998
|
+
})
|
|
7900
7999
|
});
|
|
7901
8000
|
}
|
|
7902
8001
|
await gitOutput2(run, input.worktreePath, ["add", "-A"]);
|
|
7903
8002
|
await gitOutput2(run, input.worktreePath, ["commit", "-m", commitSubject(input.workItem), "-m", commitBody(input)]);
|
|
7904
|
-
await
|
|
8003
|
+
const preRebaseCommitSha = await gitOutput2(run, input.worktreePath, ["rev-parse", "HEAD"]);
|
|
8004
|
+
await fetchRemoteBase(run, input.worktreePath, { baseBranch, remoteName });
|
|
8005
|
+
try {
|
|
8006
|
+
await rebaseBranchFromRemoteBase(run, input.worktreePath);
|
|
8007
|
+
} catch (error) {
|
|
8008
|
+
return blockedRebaseHandoff({ artifactResult, baseBranch, error, headBranch, input, preRebaseCommitSha, remoteName, run });
|
|
8009
|
+
}
|
|
7905
8010
|
const commitSha = await gitOutput2(run, input.worktreePath, ["rev-parse", "HEAD"]);
|
|
7906
8011
|
await gitOutput2(run, input.worktreePath, ["push", "--set-upstream", remoteName, headBranch]);
|
|
7907
8012
|
const pullRequest = await ensureGithubPullRequest(run, input.worktreePath, { artifacts: artifactResult, baseBranch, headBranch, workItem: input.workItem, ...input.verificationSummary ? { verificationSummary: input.verificationSummary } : {} });
|
|
@@ -7917,13 +8022,60 @@ async function completeImplementationHandoff(input) {
|
|
|
7917
8022
|
prUrl: pullRequest.url,
|
|
7918
8023
|
cleanupStatus: cleanup.status,
|
|
7919
8024
|
...cleanup.message ? { cleanupMessage: cleanup.message } : {},
|
|
8025
|
+
...cleanup.status === "failed" ? {
|
|
8026
|
+
recovery: handoffRecovery(input, {
|
|
8027
|
+
category: "cleanupRetryAvailable",
|
|
8028
|
+
availableActions: ["retryCleanup", "exportHandoffDetails"],
|
|
8029
|
+
cleanupEligible: true,
|
|
8030
|
+
rebaseAbortStatus: "notApplicable",
|
|
8031
|
+
summary: "The pull request is durable, but local worktree cleanup needs another runner-local attempt."
|
|
8032
|
+
})
|
|
8033
|
+
} : {},
|
|
7920
8034
|
artifacts: artifactResult,
|
|
7921
8035
|
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."
|
|
7922
8036
|
};
|
|
7923
8037
|
} catch (error) {
|
|
7924
|
-
return blockedHandoff({
|
|
8038
|
+
return blockedHandoff({
|
|
8039
|
+
...common,
|
|
8040
|
+
message: "Implementation handoff is blocked and the local worktree was preserved for recovery.",
|
|
8041
|
+
error: safeErrorMessage(error),
|
|
8042
|
+
recovery: handoffRecovery(input, {
|
|
8043
|
+
category: "providerBlocked",
|
|
8044
|
+
availableActions: ["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
|
|
8045
|
+
cleanupEligible: false,
|
|
8046
|
+
rebaseAbortStatus: "notAttempted",
|
|
8047
|
+
summary: "Retry handoff after fixing local Git, remote, or GitHub CLI access; requeue only if a fresh attempt is preferable."
|
|
8048
|
+
})
|
|
8049
|
+
});
|
|
7925
8050
|
}
|
|
7926
8051
|
}
|
|
8052
|
+
async function cleanupImplementationWorktree(input) {
|
|
8053
|
+
const run = input.commandRunner ?? defaultCommandRunner;
|
|
8054
|
+
return cleanupWorktree(run, input);
|
|
8055
|
+
}
|
|
8056
|
+
async function blockedRebaseHandoff(input) {
|
|
8057
|
+
const conflictFiles = await safeConflictFiles(input.run, input.input.worktreePath);
|
|
8058
|
+
const rebaseAbortStatus = await abortRebaseForRecovery(input.run, input.input.worktreePath, input.preRebaseCommitSha);
|
|
8059
|
+
return blockedHandoff({
|
|
8060
|
+
provider: "github",
|
|
8061
|
+
status: "blocked",
|
|
8062
|
+
baseBranch: input.baseBranch,
|
|
8063
|
+
headBranch: input.headBranch,
|
|
8064
|
+
remoteName: input.remoteName,
|
|
8065
|
+
commitSha: input.preRebaseCommitSha,
|
|
8066
|
+
artifacts: input.artifactResult,
|
|
8067
|
+
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.",
|
|
8068
|
+
error: safeErrorMessage(input.error),
|
|
8069
|
+
recovery: handoffRecovery(input.input, {
|
|
8070
|
+
category: "rebaseConflict",
|
|
8071
|
+
availableActions: ["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
|
|
8072
|
+
cleanupEligible: false,
|
|
8073
|
+
conflictFiles,
|
|
8074
|
+
rebaseAbortStatus,
|
|
8075
|
+
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."
|
|
8076
|
+
})
|
|
8077
|
+
});
|
|
8078
|
+
}
|
|
7927
8079
|
async function materializeApprovedArtifacts(input) {
|
|
7928
8080
|
const { selected: artifacts, skipped } = selectApprovedWorkArtifacts(input.workItem, input.approvedArtifacts ?? []);
|
|
7929
8081
|
if (!artifacts.length) {
|
|
@@ -7941,6 +8093,38 @@ async function materializeApprovedArtifacts(input) {
|
|
|
7941
8093
|
blocked: materialized.conflicts.map((conflict) => artifactStatus(findArtifactByPath(artifacts, conflictRepoPath(conflict)), "blocked", conflict))
|
|
7942
8094
|
};
|
|
7943
8095
|
}
|
|
8096
|
+
function handoffRecovery(input, recovery) {
|
|
8097
|
+
const worktreeKey = input.worktreeIsolation?.worktreeKey ?? input.workItem.executionWorktreeKey;
|
|
8098
|
+
return {
|
|
8099
|
+
...recovery,
|
|
8100
|
+
conflictFiles: recovery.conflictFiles ?? [],
|
|
8101
|
+
...worktreeKey ? { worktreeKey } : {}
|
|
8102
|
+
};
|
|
8103
|
+
}
|
|
8104
|
+
async function safeConflictFiles(run, cwd) {
|
|
8105
|
+
const unmergedFiles = await gitOutput2(run, cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "");
|
|
8106
|
+
return safeRepoPaths(unmergedFiles);
|
|
8107
|
+
}
|
|
8108
|
+
async function abortRebaseForRecovery(run, cwd, expectedHead) {
|
|
8109
|
+
try {
|
|
8110
|
+
await gitOutput2(run, cwd, ["rebase", "--abort"]);
|
|
8111
|
+
const currentHead = await gitOutput2(run, cwd, ["rev-parse", "HEAD"]);
|
|
8112
|
+
return currentHead === expectedHead ? "succeeded" : "failed";
|
|
8113
|
+
} catch {
|
|
8114
|
+
return "failed";
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
8117
|
+
function safeRepoPaths(output) {
|
|
8118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8119
|
+
for (const value of output.split(/\0|\r?\n/g)) {
|
|
8120
|
+
const repoPath = value.trim();
|
|
8121
|
+
if (!repoPath || repoPath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(repoPath) || repoPath.split(/[\\/]+/).includes("..")) {
|
|
8122
|
+
continue;
|
|
8123
|
+
}
|
|
8124
|
+
seen.add(repoPath);
|
|
8125
|
+
}
|
|
8126
|
+
return [...seen].slice(0, 25);
|
|
8127
|
+
}
|
|
7944
8128
|
function selectApprovedWorkArtifacts(workItem, documents) {
|
|
7945
8129
|
const draftId = workItem.generatedDraftId;
|
|
7946
8130
|
const explicitDocumentIds = new Set([workItem.reviewDocumentId, workItem.impactDocumentId].filter((value) => Boolean(value)));
|
|
@@ -8017,8 +8201,10 @@ async function resolveRemoteName(run, cwd) {
|
|
|
8017
8201
|
}
|
|
8018
8202
|
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
8019
8203
|
}
|
|
8020
|
-
async function
|
|
8204
|
+
async function fetchRemoteBase(run, cwd, input) {
|
|
8021
8205
|
await gitOutput2(run, cwd, ["fetch", input.remoteName, input.baseBranch]);
|
|
8206
|
+
}
|
|
8207
|
+
async function rebaseBranchFromRemoteBase(run, cwd) {
|
|
8022
8208
|
await gitOutput2(run, cwd, ["rebase", "FETCH_HEAD"]);
|
|
8023
8209
|
}
|
|
8024
8210
|
async function gitOutput2(run, cwd, args) {
|
|
@@ -9671,7 +9857,7 @@ async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
|
9671
9857
|
}
|
|
9672
9858
|
await updateRunnerCommandStatus(apiClient, context, command, "acknowledged", "Command acknowledged by local runner.");
|
|
9673
9859
|
await updateRunnerCommandStatus(apiClient, context, command, "running", `Running ${runnerCommandLabel(command.commandKind)} command.`);
|
|
9674
|
-
const result = await executeRunnerCommand(command, context);
|
|
9860
|
+
const result = await executeRunnerCommand(apiClient, command, context);
|
|
9675
9861
|
await updateRunnerCommandStatus(apiClient, context, command, result.succeeded ? "completed" : "failed", result.message, result.error);
|
|
9676
9862
|
if (command.commandKind === "remove" && result.succeeded) {
|
|
9677
9863
|
await new LocalCredentialStore().delete(credentialKey(context.accountId, context.projectId, context.repositoryLinkId));
|
|
@@ -9692,18 +9878,100 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
|
|
|
9692
9878
|
});
|
|
9693
9879
|
return result.command;
|
|
9694
9880
|
}
|
|
9695
|
-
async function executeRunnerCommand(command, context) {
|
|
9881
|
+
async function executeRunnerCommand(apiClient, command, context) {
|
|
9696
9882
|
if (command.commandKind === "remove") {
|
|
9697
9883
|
return { succeeded: true, stopRunner: true, message: "Runner credential revoked. This local runner will stop after removing its stored credential." };
|
|
9698
9884
|
}
|
|
9699
9885
|
if (command.commandKind === "restart") {
|
|
9700
9886
|
return restartCurrentRunner(context);
|
|
9701
9887
|
}
|
|
9888
|
+
if (command.commandKind === "implementationHandoffRecovery") {
|
|
9889
|
+
return executeImplementationHandoffRecoveryCommand(apiClient, command, context);
|
|
9890
|
+
}
|
|
9702
9891
|
return runOfficialCliUpdateWithRuntimeRefresh({
|
|
9703
9892
|
mode: currentRunnerMode(),
|
|
9704
9893
|
restartBackgroundRunner: () => restartCurrentRunner(context, { useUpdatedCliExecutable: true })
|
|
9705
9894
|
});
|
|
9706
9895
|
}
|
|
9896
|
+
async function executeImplementationHandoffRecoveryCommand(apiClient, command, context) {
|
|
9897
|
+
if (!command.workItemId || !command.handoffRecoveryAction) {
|
|
9898
|
+
return { succeeded: false, message: "Handoff recovery command is missing a scoped work item or action." };
|
|
9899
|
+
}
|
|
9900
|
+
const workItem = await findRecoveryWorkItem(apiClient, context.projectId, command.workItemId);
|
|
9901
|
+
if (!workItem) {
|
|
9902
|
+
return { succeeded: false, message: "Handoff recovery work item was not found." };
|
|
9903
|
+
}
|
|
9904
|
+
if (workItem.repositoryLinkId && workItem.repositoryLinkId !== context.repositoryLinkId) {
|
|
9905
|
+
return { succeeded: false, message: "Handoff recovery command is not scoped to this repository link." };
|
|
9906
|
+
}
|
|
9907
|
+
if (!workItem.implementationHandoff?.recovery?.availableActions.includes(command.handoffRecoveryAction)) {
|
|
9908
|
+
return { succeeded: false, message: "Handoff recovery action is no longer available for this work item." };
|
|
9909
|
+
}
|
|
9910
|
+
try {
|
|
9911
|
+
if (command.handoffRecoveryAction === "retryHandoff") {
|
|
9912
|
+
return retryImplementationHandoff(apiClient, context, workItem);
|
|
9913
|
+
}
|
|
9914
|
+
if (isCleanupHandoffRecoveryAction(command.handoffRecoveryAction)) {
|
|
9915
|
+
return retryImplementationHandoffCleanup(apiClient, context, workItem);
|
|
9916
|
+
}
|
|
9917
|
+
return { succeeded: false, message: "Handoff recovery action is not a runner-local command." };
|
|
9918
|
+
} catch (error) {
|
|
9919
|
+
return { succeeded: false, message: "Handoff recovery command failed locally.", error: errorMessage3(error) };
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
async function retryImplementationHandoff(apiClient, context, workItem) {
|
|
9923
|
+
const worktreeIsolation = await resolveExistingGitWorktreeIsolation(context.root, workItem);
|
|
9924
|
+
const [{ repositoryLinks }, { documents }] = await Promise.all([apiClient.listRepositoryLinks(context.projectId), apiClient.listBrainDocuments(context.projectId)]);
|
|
9925
|
+
const repositoryLink = repositoryLinks.find((link) => link.repositoryLinkId === context.repositoryLinkId);
|
|
9926
|
+
const handoff = await completeImplementationHandoff({ approvedArtifacts: documents, primaryRepoRoot: context.root, ...repositoryLink ? { repositoryLink } : {}, workItem, worktreeIsolation, worktreePath: worktreeIsolation.worktreePath });
|
|
9927
|
+
await submitRecoveredHandoff(apiClient, context, workItem, handoff, "retryHandoff");
|
|
9928
|
+
return {
|
|
9929
|
+
succeeded: handoff.status === "prReady" || handoff.status === "noChanges",
|
|
9930
|
+
message: handoff.message ?? handoffStatusMessage(handoff),
|
|
9931
|
+
...handoff.error ? { error: handoff.error } : {}
|
|
9932
|
+
};
|
|
9933
|
+
}
|
|
9934
|
+
async function retryImplementationHandoffCleanup(apiClient, context, workItem) {
|
|
9935
|
+
const worktreeIsolation = await resolveExistingGitWorktreeIsolation(context.root, workItem);
|
|
9936
|
+
const cleanup = await cleanupImplementationWorktree({ primaryRepoRoot: context.root, worktreePath: worktreeIsolation.worktreePath });
|
|
9937
|
+
const previousHandoff = workItem.implementationHandoff;
|
|
9938
|
+
const handoff = {
|
|
9939
|
+
...previousHandoff ?? { status: "noChanges" },
|
|
9940
|
+
cleanupStatus: cleanup.status,
|
|
9941
|
+
...cleanup.message ? { cleanupMessage: cleanup.message } : {},
|
|
9942
|
+
message: cleanup.status === "completed" ? "Local handoff worktree cleanup completed." : "Local handoff worktree cleanup still needs attention.",
|
|
9943
|
+
...cleanup.status === "failed" && previousHandoff?.recovery ? { recovery: previousHandoff.recovery } : {}
|
|
9944
|
+
};
|
|
9945
|
+
await submitRecoveredHandoff(apiClient, context, workItem, handoff, "retryCleanup");
|
|
9946
|
+
return { succeeded: cleanup.status === "completed", message: handoff.message ?? handoffStatusMessage(handoff), ...cleanup.message ? { error: cleanup.message } : {} };
|
|
9947
|
+
}
|
|
9948
|
+
async function findRecoveryWorkItem(apiClient, projectId, workItemId) {
|
|
9949
|
+
const { workItems } = await apiClient.listWorkItems(projectId);
|
|
9950
|
+
return workItems.find((item) => item.workItemId === workItemId);
|
|
9951
|
+
}
|
|
9952
|
+
async function submitRecoveredHandoff(apiClient, context, workItem, handoff, action) {
|
|
9953
|
+
await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID()}`, context.runnerId, {
|
|
9954
|
+
implementationHandoff: handoff,
|
|
9955
|
+
...handoff.message ? { message: handoff.message } : {},
|
|
9956
|
+
...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
|
|
9957
|
+
...workItem.implementationScopeId ? { implementationScopeId: workItem.implementationScopeId } : {},
|
|
9958
|
+
...workItem.executionBranch ? { executionBranch: workItem.executionBranch } : {},
|
|
9959
|
+
...workItem.executionWorktreeKey ? { executionWorktreeKey: workItem.executionWorktreeKey } : {},
|
|
9960
|
+
...workItem.isolationMode ? { isolationMode: workItem.isolationMode } : {},
|
|
9961
|
+
...workItem.repositoryLockId ? { repositoryLockId: workItem.repositoryLockId } : {}
|
|
9962
|
+
});
|
|
9963
|
+
}
|
|
9964
|
+
function recoveredHandoffWorkStatus(handoff) {
|
|
9965
|
+
return handoff.status === "prReady" || handoff.status === "noChanges" ? "completed" : "blocked";
|
|
9966
|
+
}
|
|
9967
|
+
function handoffStatusMessage(handoff) {
|
|
9968
|
+
if (handoff.status === "prReady") return "GitHub pull request is ready for review.";
|
|
9969
|
+
if (handoff.status === "noChanges") return "No repository changes were found.";
|
|
9970
|
+
return "Implementation handoff remains blocked.";
|
|
9971
|
+
}
|
|
9972
|
+
function isCleanupHandoffRecoveryAction(action) {
|
|
9973
|
+
return action === "retryCleanup" || action === "cleanNoChangeWorktree";
|
|
9974
|
+
}
|
|
9707
9975
|
async function restartCurrentRunner(context, options = {}) {
|
|
9708
9976
|
if (currentRunnerMode() !== "background") {
|
|
9709
9977
|
return { succeeded: false, message: "Foreground runners cannot be restarted remotely. Stop and start the local command manually." };
|
|
@@ -9722,6 +9990,7 @@ async function restartCurrentRunner(context, options = {}) {
|
|
|
9722
9990
|
function runnerCommandLabel(commandKind) {
|
|
9723
9991
|
if (commandKind === "update") return "update";
|
|
9724
9992
|
if (commandKind === "restart") return "restart";
|
|
9993
|
+
if (commandKind === "implementationHandoffRecovery") return "handoff recovery";
|
|
9725
9994
|
return "remove";
|
|
9726
9995
|
}
|
|
9727
9996
|
async function replayPendingBrainGenerationFinalizations({ accountId, apiClient, projectId, repositoryLinkId, runnerId }) {
|