@amistio/cli 0.1.52 → 0.1.53
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 +2 -0
- package/dist/index.js +336 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,8 @@ Approved implementation work uses Git as the handoff boundary. During worktree p
|
|
|
95
95
|
|
|
96
96
|
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; Requeue safe sends one backend batch that recomputes safe candidates, reports already-active and skipped rows, and still uses linked attempts. Requeue is blocked while equivalent work is already active, when the paired runner does not advertise the needed work kind, or when the latest linked attempt repeats the same sanitized blocker fingerprint. Repeated runner setup, handoff, policy, verification, and worktree blockers require root-cause repair before another linked attempt. Completed implementation status is separate from proof: queue `implementationVerification` from Tasks when a plan needs source-aware evidence before cleanup or implementation status decisions.
|
|
97
97
|
|
|
98
|
+
Patch-context failures are typed recovery states. If a local tool reports a stale `apply_patch` context miss, such as an expected-lines mismatch, the runner marks the work with patch-drift recovery metadata and safe next actions instead of uploading raw patch output or leaving a generic failed row. Retryable API failures during implementation finalization are staged in the user-level Amistio finalization outbox and replayed before the runner claims more work, so a transient `500` does not require rerunning the local AI tool. When completion queues a required implementation Test gate, the source-work finalization remains pending locally, compatible runners claim the gate before unrelated implementation work, and finalization replays after the gate passes.
|
|
99
|
+
|
|
98
100
|
Runner setup and local-tool execution use bounded failure controls. During Git worktree preflight, `amistio run --watch` repairs safe stale Git registrations when the target worktree directory is missing and Git marks the registration prunable; dirty, present, or ambiguous worktrees are preserved. Other Git worktree preflight failures are retried by releasing the claim for another attempt, then fail the work item after `--max-preflight-attempts` attempts, defaulting to 3. Active local-tool runs renew the work lease, and `--tool-timeout-seconds` caps tool execution, defaulting to 1800 seconds.
|
|
99
101
|
|
|
100
102
|
The environment doctor blocks work before local AI/tool execution when `git`, `node`, Corepack, the package manager, required package scripts, dependencies, setup allowlist, Docker, or the requested execution profile is not ready. Foreground, background, and startup-service runners accept `--execution-profile`; use `--setup-package-manager-install` with `hostWorktreeWithSetup` when the runner may run the fixed package-manager install step in the execution worktree. Work and Runner surfaces receive sanitized profile/readiness metadata only; raw host paths, command lines, environment values, and secrets are not uploaded. Watch mode also performs a Git PATH preflight before auto-sync or work claiming. If the runner reports that Git is not available to the runner PATH, install Git or restart the foreground, background, or startup-service runner from an environment where `git --version` works. On macOS, service and GUI-launched runner environments may not inherit the same PATH as an interactive shell, so restart or reinstall the service after changing PATH.
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,7 @@ var autopilotGuardCheckSchema = z.object({
|
|
|
157
157
|
var implementationExpectedOutcomeSchema = z.enum(["sourceImplementation", "docsOnly", "analysisOnly", "verificationOnly"]);
|
|
158
158
|
var implementationHandoffStatusSchema = z.enum(["notStarted", "noChanges", "prReady", "blocked", "failed", "noImplementationProduced"]);
|
|
159
159
|
var implementationHandoffCleanupStatusSchema = z.enum(["notApplicable", "pending", "completed", "failed"]);
|
|
160
|
-
var implementationHandoffRecoveryCategorySchema = z.enum(["noChangeCleaned", "rebaseConflict", "dirtyWorktree", "unresolvedConflicts", "providerBlocked", "cleanupRetryAvailable", "manualReview", "artifactBlocked", "nonGithubProvider"]);
|
|
160
|
+
var implementationHandoffRecoveryCategorySchema = z.enum(["noChangeCleaned", "rebaseConflict", "dirtyWorktree", "unresolvedConflicts", "providerBlocked", "cleanupRetryAvailable", "manualReview", "artifactBlocked", "nonGithubProvider", "patchDrift"]);
|
|
161
161
|
var implementationHandoffRecoveryActionSchema = z.enum(["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "cleanNoChangeWorktree", "markSupersededNoChanges", "exportHandoffDetails", "retryCleanup"]);
|
|
162
162
|
var implementationHandoffRebaseAbortStatusSchema = z.enum(["notAttempted", "succeeded", "failed", "notApplicable"]);
|
|
163
163
|
var safeRepoPathSchema = z.string().trim().min(1).max(300).refine((value) => {
|
|
@@ -3538,6 +3538,47 @@ var durableResultFinalizationEntrySchema = z4.object({
|
|
|
3538
3538
|
updatedAt: z4.string().min(1),
|
|
3539
3539
|
lastError: z4.string().optional()
|
|
3540
3540
|
}).and(durableResultMutationSchema);
|
|
3541
|
+
var implementationFinalizationTelemetrySchema = z4.object({
|
|
3542
|
+
message: z4.string().trim().min(1).max(1e3).optional(),
|
|
3543
|
+
tool: z4.string().trim().min(1).max(120).optional(),
|
|
3544
|
+
durationMs: z4.number().int().nonnegative().optional(),
|
|
3545
|
+
sessionPolicy: sessionPolicySchema.optional(),
|
|
3546
|
+
sessionGroupKey: z4.string().trim().min(1).max(200).optional(),
|
|
3547
|
+
toolSessionId: z4.string().trim().min(1).max(200).optional(),
|
|
3548
|
+
sessionDecision: sessionDecisionSchema.optional(),
|
|
3549
|
+
sessionDecisionReason: z4.string().trim().min(1).max(600).optional(),
|
|
3550
|
+
claimLaneId: z4.string().trim().min(1).max(80).optional(),
|
|
3551
|
+
claimLeaseId: z4.string().trim().min(1).max(200).optional(),
|
|
3552
|
+
controllingAdrId: z4.string().trim().min(1).max(200).optional(),
|
|
3553
|
+
implementationScopeId: z4.string().trim().min(1).max(200).optional(),
|
|
3554
|
+
executionBranch: z4.string().trim().min(1).max(200).optional(),
|
|
3555
|
+
executionWorktreeKey: z4.string().trim().min(1).max(200).optional(),
|
|
3556
|
+
isolationMode: z4.enum(["none", "gitWorktree"]).optional(),
|
|
3557
|
+
machineId: z4.string().trim().min(1).max(200).optional(),
|
|
3558
|
+
repositoryLockId: z4.string().trim().min(1).max(200).optional(),
|
|
3559
|
+
blockerReason: z4.string().trim().min(1).max(1e3).optional(),
|
|
3560
|
+
error: z4.string().trim().min(1).max(1200).optional(),
|
|
3561
|
+
implementationHandoff: implementationHandoffSchema.optional()
|
|
3562
|
+
}).strict();
|
|
3563
|
+
var implementationFinalizationEntrySchema = z4.object({
|
|
3564
|
+
schemaVersion: z4.literal(1),
|
|
3565
|
+
kind: z4.literal("implementationFinalization"),
|
|
3566
|
+
status: z4.enum(["pending", "terminal"]),
|
|
3567
|
+
accountId: z4.string().min(1),
|
|
3568
|
+
projectId: z4.string().min(1),
|
|
3569
|
+
repositoryLinkId: z4.string().min(1),
|
|
3570
|
+
runnerId: z4.string().min(1),
|
|
3571
|
+
workItemId: z4.string().min(1),
|
|
3572
|
+
workKind: z4.enum(["implementation", "promptBatch"]),
|
|
3573
|
+
attempt: z4.number().int().nonnegative(),
|
|
3574
|
+
idempotencyKey: z4.string().min(1),
|
|
3575
|
+
finalStatus: workStatusSchema,
|
|
3576
|
+
telemetry: implementationFinalizationTelemetrySchema,
|
|
3577
|
+
retryCount: z4.number().int().nonnegative(),
|
|
3578
|
+
createdAt: z4.string().min(1),
|
|
3579
|
+
updatedAt: z4.string().min(1),
|
|
3580
|
+
lastError: z4.string().optional()
|
|
3581
|
+
});
|
|
3541
3582
|
function defaultResultFinalizationOutboxDir() {
|
|
3542
3583
|
return path5.join(os2.homedir(), ".config", "amistio", "result-finalizations");
|
|
3543
3584
|
}
|
|
@@ -3580,6 +3621,26 @@ function createDurableResultFinalizationEntry(input, now = (/* @__PURE__ */ new
|
|
|
3580
3621
|
updatedAt: now
|
|
3581
3622
|
});
|
|
3582
3623
|
}
|
|
3624
|
+
function createImplementationFinalizationEntry(input, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3625
|
+
return implementationFinalizationEntrySchema.parse({
|
|
3626
|
+
schemaVersion: 1,
|
|
3627
|
+
kind: "implementationFinalization",
|
|
3628
|
+
status: "pending",
|
|
3629
|
+
accountId: input.accountId,
|
|
3630
|
+
projectId: input.projectId,
|
|
3631
|
+
repositoryLinkId: input.repositoryLinkId,
|
|
3632
|
+
runnerId: input.runnerId,
|
|
3633
|
+
workItemId: input.workItemId,
|
|
3634
|
+
workKind: input.workKind,
|
|
3635
|
+
attempt: input.attempt,
|
|
3636
|
+
idempotencyKey: input.idempotencyKey,
|
|
3637
|
+
finalStatus: input.finalStatus,
|
|
3638
|
+
telemetry: input.telemetry,
|
|
3639
|
+
retryCount: 0,
|
|
3640
|
+
createdAt: now,
|
|
3641
|
+
updatedAt: now
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3583
3644
|
async function upsertBrainGenerationFinalizationEntry(entry, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3584
3645
|
await mkdir5(outboxDir, { recursive: true });
|
|
3585
3646
|
const filePath = brainGenerationFinalizationEntryPath(entry, outboxDir);
|
|
@@ -3620,6 +3681,14 @@ async function upsertDurableResultFinalizationEntry(entry, outboxDir = defaultRe
|
|
|
3620
3681
|
`, "utf8");
|
|
3621
3682
|
await rename(tempPath, filePath);
|
|
3622
3683
|
}
|
|
3684
|
+
async function upsertImplementationFinalizationEntry(entry, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3685
|
+
await mkdir5(outboxDir, { recursive: true });
|
|
3686
|
+
const filePath = implementationFinalizationEntryPath(entry, outboxDir);
|
|
3687
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
3688
|
+
await writeFile4(tempPath, `${JSON.stringify(entry, null, 2)}
|
|
3689
|
+
`, "utf8");
|
|
3690
|
+
await rename(tempPath, filePath);
|
|
3691
|
+
}
|
|
3623
3692
|
async function listPendingDurableResultFinalizations(scope, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3624
3693
|
const entries = await readdir2(outboxDir, { withFileTypes: true }).catch((error) => {
|
|
3625
3694
|
if (error.code === "ENOENT") return [];
|
|
@@ -3644,6 +3713,30 @@ async function listPendingDurableResultFinalizations(scope, outboxDir = defaultR
|
|
|
3644
3713
|
}
|
|
3645
3714
|
return pending.sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt));
|
|
3646
3715
|
}
|
|
3716
|
+
async function listPendingImplementationFinalizations(scope, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3717
|
+
const entries = await readdir2(outboxDir, { withFileTypes: true }).catch((error) => {
|
|
3718
|
+
if (error.code === "ENOENT") return [];
|
|
3719
|
+
throw error;
|
|
3720
|
+
});
|
|
3721
|
+
const pending = [];
|
|
3722
|
+
for (const entry of entries) {
|
|
3723
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3724
|
+
const filePath = path5.join(outboxDir, entry.name);
|
|
3725
|
+
let raw;
|
|
3726
|
+
try {
|
|
3727
|
+
raw = JSON.parse(await readFile3(filePath, "utf8"));
|
|
3728
|
+
} catch {
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
const parsed = implementationFinalizationEntrySchema.safeParse(raw);
|
|
3732
|
+
if (!parsed.success) continue;
|
|
3733
|
+
const value = parsed.data;
|
|
3734
|
+
if (value.status !== "pending") continue;
|
|
3735
|
+
if (value.accountId !== scope.accountId || value.projectId !== scope.projectId || value.repositoryLinkId !== scope.repositoryLinkId || value.runnerId !== scope.runnerId) continue;
|
|
3736
|
+
pending.push(value);
|
|
3737
|
+
}
|
|
3738
|
+
return pending.sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt));
|
|
3739
|
+
}
|
|
3647
3740
|
async function markBrainGenerationFinalizationRetry(entry, error, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3648
3741
|
const updated = brainGenerationFinalizationEntrySchema.parse({
|
|
3649
3742
|
...entry,
|
|
@@ -3689,9 +3782,33 @@ async function markDurableResultFinalizationTerminal(entry, error, outboxDir = d
|
|
|
3689
3782
|
await upsertDurableResultFinalizationEntry(updated, outboxDir);
|
|
3690
3783
|
return updated;
|
|
3691
3784
|
}
|
|
3785
|
+
async function markImplementationFinalizationRetry(entry, error, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3786
|
+
const updated = implementationFinalizationEntrySchema.parse({
|
|
3787
|
+
...entry,
|
|
3788
|
+
status: "pending",
|
|
3789
|
+
retryCount: entry.retryCount + 1,
|
|
3790
|
+
lastError: truncateLocalError(error),
|
|
3791
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3792
|
+
});
|
|
3793
|
+
await upsertImplementationFinalizationEntry(updated, outboxDir);
|
|
3794
|
+
return updated;
|
|
3795
|
+
}
|
|
3796
|
+
async function markImplementationFinalizationTerminal(entry, error, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3797
|
+
const updated = implementationFinalizationEntrySchema.parse({
|
|
3798
|
+
...entry,
|
|
3799
|
+
status: "terminal",
|
|
3800
|
+
lastError: truncateLocalError(error),
|
|
3801
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3802
|
+
});
|
|
3803
|
+
await upsertImplementationFinalizationEntry(updated, outboxDir);
|
|
3804
|
+
return updated;
|
|
3805
|
+
}
|
|
3692
3806
|
async function deleteDurableResultFinalizationEntry(entry, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3693
3807
|
await rm(durableResultFinalizationEntryPath(entry, outboxDir), { force: true });
|
|
3694
3808
|
}
|
|
3809
|
+
async function deleteImplementationFinalizationEntry(entry, outboxDir = defaultResultFinalizationOutboxDir()) {
|
|
3810
|
+
await rm(implementationFinalizationEntryPath(entry, outboxDir), { force: true });
|
|
3811
|
+
}
|
|
3695
3812
|
function brainGenerationFinalizationEntryPath(entry, outboxDir) {
|
|
3696
3813
|
return path5.join(outboxDir, `${brainGenerationFinalizationEntryKey(entry)}.json`);
|
|
3697
3814
|
}
|
|
@@ -3706,6 +3823,13 @@ function durableResultFinalizationEntryKey(entry) {
|
|
|
3706
3823
|
const hash = createHash2("sha256").update([entry.accountId, entry.projectId, entry.repositoryLinkId, entry.runnerId, entry.workItemId, String(entry.attempt), entry.idempotencyKey, entry.resultKind].join("\0")).digest("hex").slice(0, 32);
|
|
3707
3824
|
return `runner-result-${hash}`;
|
|
3708
3825
|
}
|
|
3826
|
+
function implementationFinalizationEntryPath(entry, outboxDir) {
|
|
3827
|
+
return path5.join(outboxDir, `${implementationFinalizationEntryKey(entry)}.json`);
|
|
3828
|
+
}
|
|
3829
|
+
function implementationFinalizationEntryKey(entry) {
|
|
3830
|
+
const hash = createHash2("sha256").update([entry.accountId, entry.projectId, entry.repositoryLinkId, entry.runnerId, entry.workItemId, String(entry.attempt), entry.idempotencyKey].join("\0")).digest("hex").slice(0, 32);
|
|
3831
|
+
return `implementation-finalization-${hash}`;
|
|
3832
|
+
}
|
|
3709
3833
|
function truncateLocalError(error) {
|
|
3710
3834
|
const trimmed = error.trim();
|
|
3711
3835
|
return trimmed.length > 600 ? `${trimmed.slice(0, 600)}...` : trimmed;
|
|
@@ -5099,11 +5223,20 @@ async function detectRunnerProviderCatalog(adapter) {
|
|
|
5099
5223
|
return mergeProviderCatalogs(adapter.providerCatalog, localOpencodeConfigCatalog);
|
|
5100
5224
|
}
|
|
5101
5225
|
function localOpencodeProviderConfigPaths() {
|
|
5102
|
-
|
|
5226
|
+
const configPaths = [
|
|
5103
5227
|
path7.join(os3.homedir(), ".config", "opencode", "opencode.json"),
|
|
5104
|
-
path7.join(os3.homedir(), ".config", "opencode", "config.json")
|
|
5105
|
-
path7.join(process.cwd(), "opencode.json")
|
|
5228
|
+
path7.join(os3.homedir(), ".config", "opencode", "config.json")
|
|
5106
5229
|
];
|
|
5230
|
+
const cwd = safeProcessCwd();
|
|
5231
|
+
if (cwd) configPaths.push(path7.join(cwd, "opencode.json"));
|
|
5232
|
+
return configPaths;
|
|
5233
|
+
}
|
|
5234
|
+
function safeProcessCwd() {
|
|
5235
|
+
try {
|
|
5236
|
+
return process.cwd();
|
|
5237
|
+
} catch {
|
|
5238
|
+
return void 0;
|
|
5239
|
+
}
|
|
5107
5240
|
}
|
|
5108
5241
|
async function loadLocalOpencodeProviderConfigCatalog(configPaths = localOpencodeProviderConfigPaths()) {
|
|
5109
5242
|
for (const configPath of configPaths) {
|
|
@@ -6112,6 +6245,30 @@ async function checkRunnerWatchGitPreflight(options) {
|
|
|
6112
6245
|
return { status: "blocked", exitCode: 1, message, readiness: readiness2 };
|
|
6113
6246
|
}
|
|
6114
6247
|
|
|
6248
|
+
// src/runner-patch-drift.ts
|
|
6249
|
+
var patchDriftPatterns = [
|
|
6250
|
+
{ signature: "applyPatchVerificationFailed", pattern: /apply_patch\s+verification\s+failed/i },
|
|
6251
|
+
{ signature: "expectedLinesMissing", pattern: /failed\s+to\s+find\s+expected\s+lines/i },
|
|
6252
|
+
{ signature: "patchContextMismatch", pattern: /patch\s+context\s+(?:mismatch|drift)|stale\s+patch\s+context/i }
|
|
6253
|
+
];
|
|
6254
|
+
function classifyPatchDriftToolResult(result) {
|
|
6255
|
+
if (result.exitCode === 0) return void 0;
|
|
6256
|
+
return classifyPatchDriftText(`${result.stderr}
|
|
6257
|
+
${result.stdout}`);
|
|
6258
|
+
}
|
|
6259
|
+
function classifyPatchDriftText(output) {
|
|
6260
|
+
for (const candidate of patchDriftPatterns) {
|
|
6261
|
+
if (candidate.pattern.test(output)) {
|
|
6262
|
+
return {
|
|
6263
|
+
signature: candidate.signature,
|
|
6264
|
+
message: "Patch context drift was detected; refresh context or queue a fresh linked attempt before retrying this implementation.",
|
|
6265
|
+
summary: `Local patch application failed with ${candidate.signature}. The runner preserved bounded recovery metadata instead of storing raw patch output.`
|
|
6266
|
+
};
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
return void 0;
|
|
6270
|
+
}
|
|
6271
|
+
|
|
6115
6272
|
// src/runner-watch-errors.ts
|
|
6116
6273
|
var forgottenRunnerWatchMessage = "This runner was forgotten by the server. Start or pair a new runner from the Runner panel; this runner ID cannot heartbeat or claim work anymore.";
|
|
6117
6274
|
async function handleRunnerWatchError(options) {
|
|
@@ -12270,6 +12427,25 @@ async function runNextWorkItem({
|
|
|
12270
12427
|
let implementationHandoff;
|
|
12271
12428
|
let finalMessage = `${preview.toolName} exited with code ${toolResult.exitCode}.`;
|
|
12272
12429
|
let finalError = failureExcerpt ?? promptBatchFailureReason(promptBatchResult);
|
|
12430
|
+
const patchDrift = finalStatus !== "completed" ? classifyPatchDriftToolResult(toolResult) : void 0;
|
|
12431
|
+
if (patchDrift) {
|
|
12432
|
+
finalStatus = "blocked";
|
|
12433
|
+
finalMessage = patchDrift.message;
|
|
12434
|
+
finalError = patchDrift.summary;
|
|
12435
|
+
implementationHandoff = {
|
|
12436
|
+
status: "blocked",
|
|
12437
|
+
cleanupStatus: "pending",
|
|
12438
|
+
message: patchDrift.message,
|
|
12439
|
+
error: patchDrift.summary,
|
|
12440
|
+
recovery: {
|
|
12441
|
+
category: "patchDrift",
|
|
12442
|
+
availableActions: ["requeueFreshAttempt", "exportHandoffDetails"],
|
|
12443
|
+
conflictFiles: [],
|
|
12444
|
+
summary: "The local patch did not match the current worktree context. Refresh context or queue a fresh linked attempt before retrying.",
|
|
12445
|
+
...isolationTelemetry.executionWorktreeKey ? { worktreeKey: isolationTelemetry.executionWorktreeKey } : {}
|
|
12446
|
+
}
|
|
12447
|
+
};
|
|
12448
|
+
}
|
|
12273
12449
|
if (promptBatchResult && finalStatus !== "completed" && toolResult.exitCode === 0) {
|
|
12274
12450
|
finalMessage = "Prompt batch reported a failed or blocked child prompt.";
|
|
12275
12451
|
}
|
|
@@ -12320,33 +12496,59 @@ async function runNextWorkItem({
|
|
|
12320
12496
|
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
|
|
12321
12497
|
});
|
|
12322
12498
|
let statusResult;
|
|
12499
|
+
const finalizationIdempotencyKey = `run_${result.workItem.workItemId}_${randomUUID3()}`;
|
|
12500
|
+
const finalizationTelemetry = {
|
|
12501
|
+
tool: preview.toolName,
|
|
12502
|
+
...toolResult.model ? { model: toolResult.model } : {},
|
|
12503
|
+
durationMs,
|
|
12504
|
+
message: finalMessage,
|
|
12505
|
+
...isolationTelemetry,
|
|
12506
|
+
...finalStatus === "blocked" ? { blockerReason: finalMessage } : {},
|
|
12507
|
+
sessionPolicy: sessionContext.policy,
|
|
12508
|
+
sessionDecision: sessionContext.decision,
|
|
12509
|
+
sessionDecisionReason: sessionContext.reason,
|
|
12510
|
+
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
12511
|
+
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {},
|
|
12512
|
+
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
12513
|
+
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
12514
|
+
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {},
|
|
12515
|
+
...implementationHandoff ? { implementationHandoff } : {},
|
|
12516
|
+
...result.workItem.workKind === "promptBatch" ? { promptBatch: finalizePromptBatchMetadata(result.workItem, finalStatus, finalError, promptBatchResult) } : {},
|
|
12517
|
+
...finalError ? { error: finalError } : {}
|
|
12518
|
+
};
|
|
12323
12519
|
try {
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
result.workItem.
|
|
12520
|
+
const implementationFinalization = await submitStagedImplementationFinalization(apiClient, {
|
|
12521
|
+
accountId: commandContext.accountId,
|
|
12522
|
+
attempt: result.workItem.attempt,
|
|
12327
12523
|
finalStatus,
|
|
12328
|
-
|
|
12524
|
+
idempotencyKey: finalizationIdempotencyKey,
|
|
12525
|
+
projectId,
|
|
12526
|
+
repositoryLinkId,
|
|
12329
12527
|
runnerId,
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
sessionDecision: sessionContext.decision,
|
|
12339
|
-
sessionDecisionReason: sessionContext.reason,
|
|
12340
|
-
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
12341
|
-
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {},
|
|
12342
|
-
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
12343
|
-
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
12344
|
-
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {},
|
|
12345
|
-
...implementationHandoff ? { implementationHandoff } : {},
|
|
12346
|
-
...result.workItem.workKind === "promptBatch" ? { promptBatch: finalizePromptBatchMetadata(result.workItem, finalStatus, finalError, promptBatchResult) } : {},
|
|
12347
|
-
...finalError ? { error: finalError } : {}
|
|
12528
|
+
telemetry: implementationFinalizationTelemetry(finalizationTelemetry),
|
|
12529
|
+
workItemId: result.workItem.workItemId,
|
|
12530
|
+
workKind: result.workItem.workKind === "promptBatch" ? "promptBatch" : "implementation"
|
|
12531
|
+
});
|
|
12532
|
+
if (implementationFinalization.status === "failed") {
|
|
12533
|
+
if (implementationFinalization.retryable) {
|
|
12534
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig, currentRunnerMode(), heartbeatConcurrency)).catch(() => void 0);
|
|
12535
|
+
return { status: "blocked", exitCode: 0, message: implementationFinalization.message };
|
|
12348
12536
|
}
|
|
12349
|
-
|
|
12537
|
+
throw new Error(implementationFinalization.message);
|
|
12538
|
+
}
|
|
12539
|
+
if (implementationFinalization.status === "deferred") {
|
|
12540
|
+
const gateMessage = "Implementation test gate was queued and must pass before completion or PR handoff is finalized.";
|
|
12541
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
12542
|
+
status: "queued",
|
|
12543
|
+
summary: gateMessage,
|
|
12544
|
+
idempotencyKey: `runner_milestone_test_gate_required_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
12545
|
+
metadata: { tool: preview.toolName, durationMs, executionWorktreeKey: isolationTelemetry.executionWorktreeKey ?? "", executionBranch: isolationTelemetry.executionBranch ?? "" }
|
|
12546
|
+
});
|
|
12547
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig, currentRunnerMode(), heartbeatConcurrency));
|
|
12548
|
+
console.log(gateMessage);
|
|
12549
|
+
return { status: "blocked", exitCode: 0, message: gateMessage };
|
|
12550
|
+
}
|
|
12551
|
+
statusResult = { workItem: implementationFinalization.workItem };
|
|
12350
12552
|
} catch (error) {
|
|
12351
12553
|
if (error instanceof AmistioApiError && error.status === 409 && error.detail.includes("implementation_test_gate_required")) {
|
|
12352
12554
|
const gateMessage = "Implementation test gate was queued and must pass before completion or PR handoff is finalized.";
|
|
@@ -12895,10 +13097,12 @@ async function replayPendingResultFinalizations({ accountId, apiClient, projectI
|
|
|
12895
13097
|
return brainReplay;
|
|
12896
13098
|
}
|
|
12897
13099
|
const pendingEntries = await listPendingDurableResultFinalizations({ accountId, projectId, repositoryLinkId, runnerId });
|
|
12898
|
-
|
|
13100
|
+
const pendingImplementationEntries = await listPendingImplementationFinalizations({ accountId, projectId, repositoryLinkId, runnerId });
|
|
13101
|
+
if (!pendingEntries.length && !pendingImplementationEntries.length) {
|
|
12899
13102
|
return void 0;
|
|
12900
13103
|
}
|
|
12901
13104
|
let completedCount = 0;
|
|
13105
|
+
let deferredCount = 0;
|
|
12902
13106
|
for (const entry of pendingEntries) {
|
|
12903
13107
|
const replay = await submitDurableResultFinalizationEntry(apiClient, entry, { recordReplayTelemetry: true });
|
|
12904
13108
|
if (replay.status === "completed") {
|
|
@@ -12907,7 +13111,24 @@ async function replayPendingResultFinalizations({ accountId, apiClient, projectI
|
|
|
12907
13111
|
}
|
|
12908
13112
|
return { status: "failed", exitCode: 1, message: replay.message };
|
|
12909
13113
|
}
|
|
12910
|
-
const
|
|
13114
|
+
for (const entry of pendingImplementationEntries) {
|
|
13115
|
+
const replay = await submitImplementationFinalizationEntry(apiClient, entry, { recordReplayTelemetry: true });
|
|
13116
|
+
if (replay.status === "completed") {
|
|
13117
|
+
completedCount += 1;
|
|
13118
|
+
continue;
|
|
13119
|
+
}
|
|
13120
|
+
if (replay.status === "deferred") {
|
|
13121
|
+
deferredCount += 1;
|
|
13122
|
+
continue;
|
|
13123
|
+
}
|
|
13124
|
+
return { status: "failed", exitCode: 1, message: replay.message };
|
|
13125
|
+
}
|
|
13126
|
+
if (!completedCount && deferredCount) {
|
|
13127
|
+
const message2 = `Deferred ${deferredCount} pending implementation finalization${deferredCount === 1 ? "" : "s"} until required implementation test gates pass.`;
|
|
13128
|
+
console.log(message2);
|
|
13129
|
+
return void 0;
|
|
13130
|
+
}
|
|
13131
|
+
const message = `Replayed ${completedCount} pending runner finalization${completedCount === 1 ? "" : "s"}.`;
|
|
12911
13132
|
console.log(message);
|
|
12912
13133
|
return { status: "completed", exitCode: 0, message };
|
|
12913
13134
|
}
|
|
@@ -12980,6 +13201,59 @@ async function submitDurableResultFinalizationEntry(apiClient, entry, options =
|
|
|
12980
13201
|
}
|
|
12981
13202
|
return { status: "completed", workItem, response };
|
|
12982
13203
|
}
|
|
13204
|
+
async function submitImplementationFinalizationEntry(apiClient, entry, options = {}) {
|
|
13205
|
+
let response;
|
|
13206
|
+
try {
|
|
13207
|
+
response = await apiClient.updateWorkStatus(entry.projectId, entry.workItemId, entry.finalStatus, entry.idempotencyKey, entry.runnerId, entry.telemetry);
|
|
13208
|
+
} catch (error) {
|
|
13209
|
+
if (isImplementationTestGateRequiredError(error)) {
|
|
13210
|
+
await markImplementationFinalizationRetry(entry, "implementation_test_gate_required");
|
|
13211
|
+
const message2 = `Pending implementation finalization ${entry.workItemId} is waiting for the required implementation test gate to pass.`;
|
|
13212
|
+
await apiClient.updateWorkStatus(entry.projectId, entry.workItemId, "blocked", `${entry.idempotencyKey}_gate_pending`, entry.runnerId, {
|
|
13213
|
+
...entry.telemetry,
|
|
13214
|
+
message: message2,
|
|
13215
|
+
blockerReason: message2,
|
|
13216
|
+
releaseClaim: true
|
|
13217
|
+
}).catch((releaseError) => {
|
|
13218
|
+
console.error(`Could not release implementation work ${entry.workItemId} while waiting for its test gate: ${truncateLogExcerpt(errorMessage7(releaseError))}`);
|
|
13219
|
+
});
|
|
13220
|
+
console.log(message2);
|
|
13221
|
+
return { status: "deferred", message: message2, retryable: true };
|
|
13222
|
+
}
|
|
13223
|
+
const detail = truncateLogExcerpt(errorMessage7(error));
|
|
13224
|
+
if (isRetryableApiError(error)) {
|
|
13225
|
+
const updated = await markImplementationFinalizationRetry(entry, detail);
|
|
13226
|
+
const message2 = `Pending implementation finalization ${entry.workItemId} could not be replayed yet (${updated.retryCount} attempt${updated.retryCount === 1 ? "" : "s"}): ${detail}`;
|
|
13227
|
+
console.error(message2);
|
|
13228
|
+
return { status: "failed", message: message2, retryable: true };
|
|
13229
|
+
}
|
|
13230
|
+
await markImplementationFinalizationTerminal(entry, detail);
|
|
13231
|
+
const message = `Pending implementation finalization ${entry.workItemId} reached a terminal API failure: ${detail}`;
|
|
13232
|
+
console.error(message);
|
|
13233
|
+
return { status: "failed", message, retryable: false };
|
|
13234
|
+
}
|
|
13235
|
+
await deleteImplementationFinalizationEntry(entry).catch((error) => {
|
|
13236
|
+
console.error(`delete pending implementation finalization ${entry.workItemId} failed: ${errorMessage7(error)}`);
|
|
13237
|
+
});
|
|
13238
|
+
if (options.recordReplayTelemetry) {
|
|
13239
|
+
await recordRunnerMilestone(apiClient, entry.projectId, response.workItem, entry.runnerId, entry.repositoryLinkId, {
|
|
13240
|
+
status: runnerMilestoneStatusFromWorkStatus(entry.finalStatus),
|
|
13241
|
+
summary: `Replayed pending implementation finalization as ${entry.finalStatus}.`,
|
|
13242
|
+
idempotencyKey: `runner_milestone_implementation_replayed_${entry.workItemId}_${response.workItem.idempotencyKey}`,
|
|
13243
|
+
metadata: { replayedFinalization: true, workKind: entry.workKind }
|
|
13244
|
+
});
|
|
13245
|
+
await apiClient.sendRunnerHeartbeat(entry.projectId, entry.runnerId, entry.repositoryLinkId, "online", {
|
|
13246
|
+
...runnerHeartbeatMetadata(),
|
|
13247
|
+
preferenceMessage: "Pending implementation finalization replayed."
|
|
13248
|
+
}).catch(() => void 0);
|
|
13249
|
+
}
|
|
13250
|
+
return { status: "completed", workItem: response.workItem };
|
|
13251
|
+
}
|
|
13252
|
+
function runnerMilestoneStatusFromWorkStatus(status) {
|
|
13253
|
+
if (status === "completed" || status === "failed" || status === "running" || status === "blocked") return status;
|
|
13254
|
+
if (status === "approved" || status === "drafted") return "queued";
|
|
13255
|
+
return "info";
|
|
13256
|
+
}
|
|
12983
13257
|
async function submitDurableResultMutation(apiClient, entry) {
|
|
12984
13258
|
if (entry.resultKind === "assistantResult") {
|
|
12985
13259
|
return apiClient.submitAssistantResult(entry.projectId, entry.workItemId, entry.result);
|
|
@@ -13033,6 +13307,39 @@ async function submitStagedDurableResultFinalization(apiClient, input) {
|
|
|
13033
13307
|
await upsertDurableResultFinalizationEntry(entry);
|
|
13034
13308
|
return submitDurableResultFinalizationEntry(apiClient, entry);
|
|
13035
13309
|
}
|
|
13310
|
+
async function submitStagedImplementationFinalization(apiClient, input) {
|
|
13311
|
+
const entry = createImplementationFinalizationEntry(input);
|
|
13312
|
+
await upsertImplementationFinalizationEntry(entry);
|
|
13313
|
+
return submitImplementationFinalizationEntry(apiClient, entry);
|
|
13314
|
+
}
|
|
13315
|
+
function implementationFinalizationTelemetry(telemetry) {
|
|
13316
|
+
const isolationMode = telemetry.isolationMode === "none" || telemetry.isolationMode === "gitWorktree" ? telemetry.isolationMode : void 0;
|
|
13317
|
+
return {
|
|
13318
|
+
...telemetry.message ? { message: telemetry.message } : {},
|
|
13319
|
+
...telemetry.tool ? { tool: telemetry.tool } : {},
|
|
13320
|
+
...telemetry.durationMs !== void 0 ? { durationMs: telemetry.durationMs } : {},
|
|
13321
|
+
...telemetry.sessionPolicy ? { sessionPolicy: telemetry.sessionPolicy } : {},
|
|
13322
|
+
...telemetry.sessionGroupKey ? { sessionGroupKey: telemetry.sessionGroupKey } : {},
|
|
13323
|
+
...telemetry.toolSessionId ? { toolSessionId: telemetry.toolSessionId } : {},
|
|
13324
|
+
...telemetry.sessionDecision ? { sessionDecision: telemetry.sessionDecision } : {},
|
|
13325
|
+
...telemetry.sessionDecisionReason ? { sessionDecisionReason: telemetry.sessionDecisionReason } : {},
|
|
13326
|
+
...telemetry.claimLaneId ? { claimLaneId: telemetry.claimLaneId } : {},
|
|
13327
|
+
...telemetry.claimLeaseId ? { claimLeaseId: telemetry.claimLeaseId } : {},
|
|
13328
|
+
...telemetry.controllingAdrId ? { controllingAdrId: telemetry.controllingAdrId } : {},
|
|
13329
|
+
...telemetry.implementationScopeId ? { implementationScopeId: telemetry.implementationScopeId } : {},
|
|
13330
|
+
...telemetry.executionBranch ? { executionBranch: telemetry.executionBranch } : {},
|
|
13331
|
+
...telemetry.executionWorktreeKey ? { executionWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
13332
|
+
...isolationMode ? { isolationMode } : {},
|
|
13333
|
+
...telemetry.machineId ? { machineId: telemetry.machineId } : {},
|
|
13334
|
+
...telemetry.repositoryLockId ? { repositoryLockId: telemetry.repositoryLockId } : {},
|
|
13335
|
+
...telemetry.blockerReason ? { blockerReason: telemetry.blockerReason } : {},
|
|
13336
|
+
...telemetry.error ? { error: telemetry.error } : {},
|
|
13337
|
+
...telemetry.implementationHandoff ? { implementationHandoff: telemetry.implementationHandoff } : {}
|
|
13338
|
+
};
|
|
13339
|
+
}
|
|
13340
|
+
function isImplementationTestGateRequiredError(error) {
|
|
13341
|
+
return error instanceof AmistioApiError && error.status === 409 && error.detail.includes("implementation_test_gate_required");
|
|
13342
|
+
}
|
|
13036
13343
|
async function submitPrimaryRunnerResult(apiClient, input) {
|
|
13037
13344
|
const replay = await submitStagedDurableResultFinalization(apiClient, input);
|
|
13038
13345
|
if (replay.status === "failed") {
|