@amistio/cli 0.1.51 → 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 +14 -5
- package/dist/index.js +342 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ Tool session reuse is bounded. One-shot tool sessions close after successful com
|
|
|
37
37
|
|
|
38
38
|
Repository brain auto-sync is disabled until the repository link option is enabled in the app. After pairing, run `amistio sync watch` from the paired checkout to push recognized external brain Markdown/MDX files and explicit HTML artifacts under `docs/html/<area>/`, including local ADRs, plans, prompts, workflows, memory, context, architecture, and feature docs, to the app for review. Markdown is the default generation format; HTML appears only when a runner or user explicitly generated an HTML artifact. `amistio run --watch` also runs the same cycle between work polls when the option is enabled. The CLI skips templates, unsupported paths, oversized files, unchanged managed docs, and conflicts instead of silently overwriting web state.
|
|
39
39
|
|
|
40
|
-
Repository
|
|
40
|
+
Repository autonomy is disabled until the repository link option is enabled in the app. When enabled, Amistio can attach an audited low-risk autonomy-contract authorization to eligible runner work, including generated brain approval, impact preview, issue diagnosis, security posture scan, app evaluation cleanup, Test scans, implementation Test gates, low-risk implementation handoff, safe requeue, and implementation verification. The Runner panel shows and updates safe work scopes, allowed candidate types, max risk, optional runner binding, daily/concurrent/failure budgets, expiry/review/cooldown windows, and pause state. The CLI shows authorization id, candidate id/type, outcome, policy version, and work kind in `amistio work list`, claim logs, runner prompts, and milestone activity. Autonomy does not widen local runner permissions: pairing, supported work kinds, runner identity, Git worktree isolation, redaction, local-tool permission controls, and unsafe/review-required/blocked/repeated-blocker/paused/budget stops still apply.
|
|
41
|
+
Organization workspace autonomy contracts can be changed only by `org:admin` members.
|
|
41
42
|
|
|
42
43
|
After pairing, confirm that at least one local AI tool is available:
|
|
43
44
|
|
|
@@ -59,7 +60,7 @@ amistio run --watch --execution-profile hostWorktree
|
|
|
59
60
|
amistio run --watch --execution-profile hostWorktreeWithSetup --setup-package-manager-install
|
|
60
61
|
amistio run --watch --execution-profile dockerWorkspace
|
|
61
62
|
amistio run --watch --tool opencode --provider anthropic --model-id claude-opus-4.6 --reasoning-effort xhigh
|
|
62
|
-
amistio run --watch --max-concurrent-work
|
|
63
|
+
amistio run --watch --max-concurrent-work 1 --tool opencode
|
|
63
64
|
amistio run --watch --background --tool opencode
|
|
64
65
|
AMISTIO_HOST_HELPER_PATH=$(command -v amistio-host-helper) amistio host-helper status
|
|
65
66
|
AMISTIO_HOST_HELPER_PATH=$(command -v amistio-host-helper) amistio host-helper conformance
|
|
@@ -82,17 +83,25 @@ When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until
|
|
|
82
83
|
|
|
83
84
|
`amistio runner smoke-session-lifecycle` runs a local no-claim smoke for the runner tool-session lifecycle. It does not contact the API, claim production work, inspect source, or mutate local runner state; it verifies that completed one-shot sessions close, active sessions are treated as in use, stale sessions are not selected for reuse, and fresh related reusable sessions can still continue.
|
|
84
85
|
|
|
85
|
-
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.
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
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 adaptive runner-driven evaluations during normal watch/background polling when app evaluation is enabled for the repository link; fresh clean evidence is skipped when there is no active app-evaluation finding to act on. 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.
|
|
89
|
+
|
|
90
|
+
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. Under a repository autonomy contract, `testQualityScan` and `implementationTestGate` are delegated evidence work instead of another approval interruption. 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 autonomous repair work when delegated or reviewable plan-backed findings when not. 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 when a repository autonomy contract or explicit approval authorizes it.
|
|
91
|
+
|
|
92
|
+
Runner-driven app evaluation, Test quality, and Security posture signals skip fresh clean evidence when no active finding, failed/blocked gate, or exception needs action. If an implementation Test gate fails or blocks under an enabled repository autonomy contract, Amistio queues one deduped low-risk test-repair implementation work item so the runner can continue from validation failure into repair instead of waiting for another approval.
|
|
86
93
|
|
|
87
94
|
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. Before local AI/tool execution starts, implementation work checks PR handoff readiness: GitHub remote support, default-branch fetch, Git commit identity, local `gh` authentication, and repository visibility. 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, then classifies the final diff. Source-implementation work must include source, config, test, or other implementation-affecting changes before the runner opens or reuses a PR. If only project-brain or documentation artifacts changed, Amistio completes the work as no implementation produced instead of opening a misleading implementation PR. App-evaluation proof and lifecycle-cleanup actions are queued with explicit docs-only expected outcomes, so safe proof notes and plan metadata updates can create docs-only PRs without being mislabeled as source implementation. Other explicit docs-only work can also create docs-only PRs. 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 only to the runner that owns the preserved worktree for the same work item, branch, and worktree key. Retry handoff can publish a clean preserved local-only implementation commit without rerunning the implementation prompt. 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.
|
|
88
95
|
|
|
89
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.
|
|
90
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
|
+
|
|
91
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.
|
|
92
101
|
|
|
93
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.
|
|
94
103
|
|
|
95
|
-
Runner watch mode defaults to
|
|
104
|
+
Runner watch mode defaults to bounded parallel claim lanes, capped at 4. `--max-concurrent-work <count>` lowers or explicitly sets the advertised lane count; use `--max-concurrent-work 1` only when you intentionally want serial execution. The server enforces one active lease per runner lane, honors the advertised capacity, and keeps equivalent implementation scopes serialized through Git worktree locks; use separate lanes for independent work, not multiple attempts at the same ADR scope.
|
|
96
105
|
|
|
97
106
|
Watch mode prints a completed-work success once per work item, keeps fresh completion visible briefly, and returns old completed work to the ready state when no queued, running, blocked, failed, review, or runner-readiness action needs attention.
|
|
98
107
|
|
|
@@ -102,7 +111,7 @@ If watch mode reports that the runner was forgotten by the server, run `amistio
|
|
|
102
111
|
|
|
103
112
|
App-evaluation and impact-preview result finalization rejections print safe validation paths and preserve the local finalization evidence without exposing raw source or secrets. If a structured app-evaluation or impact-preview result is rejected, update and restart the runner, confirm the web/API deployment is current, and retry before acting on cleanup, implementation, or risk recommendations. Accepted impact-preview results that cannot be stored safely are marked failed with a bounded reason instead of uploading raw source or secrets.
|
|
104
113
|
|
|
105
|
-
When a newer manual or
|
|
114
|
+
When a newer manual or adaptive app evaluation is queued for the same repository, Amistio marks older queued or running app-evaluation scans stale and closes their scan work without deleting historical scans, findings, or generated plans. Runner-driven evaluation skips fresh clean evidence instead of spending another hourly scan when there is no active app-evaluation finding to act on.
|
|
106
115
|
|
|
107
116
|
When brain generation or plan revision output is parsed but the Amistio API is temporarily unavailable during finalization, the runner keeps a safe pending result envelope in user-level Amistio config and replays it before claiming more work. The envelope uses a stable idempotency key and does not store raw tool stdout, provider sessions, credentials, or arbitrary local paths.
|
|
108
117
|
|
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) {
|
|
@@ -7904,6 +8061,7 @@ function createAppEvaluationScanPrompt(workItem, context) {
|
|
|
7904
8061
|
"- 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.",
|
|
7905
8062
|
"- Check missing memory or workflow updates when repeated lessons or operational rules are visible.",
|
|
7906
8063
|
"- Check release readiness, UX, accessibility, performance, reliability, and security-posture follow-through at a summary level.",
|
|
8064
|
+
"- For security posture, do not treat implemented controls, documentation, remediation notes, or the presence of the Security workflow as proof that current live posture is fresh. Require a recent completed securityPostureScan/security snapshot or explicit runner-backed scan evidence; if it is missing, report a securityPosture finding that recommends refreshing posture evidence rather than source implementation.",
|
|
7907
8065
|
"- For release, deploy, or publish readiness, do not treat the mere presence of a workflow, script, or documented verification gate as proof. If fresh successful root verification evidence is missing for the current release candidate, report releaseReadiness and recommend refreshing the gate before release actions.",
|
|
7908
8066
|
"- Prefer repository-documented verification commands over ad hoc package-script inference.",
|
|
7909
8067
|
"- For this Amistio monorepo, if plain Corepack pnpm fails before scripts with spawnSync pnpm ENOENT, retry the documented command corepack pnpm --config.verify-deps-before-run=false verify before declaring whole-app verification blocked.",
|
|
@@ -9501,6 +9659,7 @@ var exactLocalEnvironmentFiles = /* @__PURE__ */ new Set([".env", ".env.local",
|
|
|
9501
9659
|
var localEnvironmentFilePattern = /^\.env\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*\.local$/;
|
|
9502
9660
|
function needsGitWorktreeIsolation(workItem) {
|
|
9503
9661
|
const workKind = workItem.workKind ?? "implementation";
|
|
9662
|
+
if (workItem.implementationExpectedOutcome === "analysisOnly" || workItem.implementationExpectedOutcome === "verificationOnly") return false;
|
|
9504
9663
|
return workKind === "implementation" || workKind === "promptBatch";
|
|
9505
9664
|
}
|
|
9506
9665
|
function resolveWorktreeIdentity(workItem) {
|
|
@@ -11281,7 +11440,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
11281
11440
|
process.exitCode = result.exitCode;
|
|
11282
11441
|
}
|
|
11283
11442
|
});
|
|
11284
|
-
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--execution-profile <profile>", "Runner execution environment profile: hostWorktree, hostWorktreeWithSetup, or dockerWorkspace", parseRunnerExecutionEnvironmentProfile, defaultRunnerExecutionEnvironmentProfile).option("--setup-package-manager-install", "Allow hostWorktreeWithSetup to run the approved package-manager install command").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--max-concurrent-work <count>", "Maximum approved work items to run in parallel in --watch mode", parsePositiveInteger,
|
|
11443
|
+
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--execution-profile <profile>", "Runner execution environment profile: hostWorktree, hostWorktreeWithSetup, or dockerWorkspace", parseRunnerExecutionEnvironmentProfile, defaultRunnerExecutionEnvironmentProfile).option("--setup-package-manager-install", "Allow hostWorktreeWithSetup to run the approved package-manager install command").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--max-concurrent-work <count>", "Maximum approved work items to run in parallel in --watch mode", parsePositiveInteger, MAX_CONCURRENT_RUNNER_WORK).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
|
|
11285
11444
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
11286
11445
|
if (!context) {
|
|
11287
11446
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -11510,7 +11669,7 @@ runner.command("stop").description("Stop a background runner for the paired repo
|
|
|
11510
11669
|
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
11511
11670
|
});
|
|
11512
11671
|
var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
|
|
11513
|
-
runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--execution-profile <profile>", "Runner execution environment profile for the service runner", parseRunnerExecutionEnvironmentProfile, defaultRunnerExecutionEnvironmentProfile).option("--setup-package-manager-install", "Allow hostWorktreeWithSetup to run the approved package-manager install command").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--max-concurrent-work <count>", "Maximum approved work items to run in parallel in --watch mode", parsePositiveInteger,
|
|
11672
|
+
runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--provider <providerId>", "Provider id for provider-backed model configuration").option("--model-id <modelId>", "Provider catalog model id to request").option("--model-variant <variant>", "Provider catalog model variant to request").option("--reasoning-effort <effort>", "Reasoning effort: auto, low, medium, high, or xhigh", parseReasoningEffort).option("--execution-profile <profile>", "Runner execution environment profile for the service runner", parseRunnerExecutionEnvironmentProfile, defaultRunnerExecutionEnvironmentProfile).option("--setup-package-manager-install", "Allow hostWorktreeWithSetup to run the approved package-manager install command").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--max-concurrent-work <count>", "Maximum approved work items to run in parallel in --watch mode", parsePositiveInteger, MAX_CONCURRENT_RUNNER_WORK).option("--max-preflight-attempts <count>", "Fail setup/preflight failures after this many claimed attempts", parsePositiveInteger, DEFAULT_MAX_PREFLIGHT_ATTEMPTS).option("--tool-timeout-seconds <seconds>", "Fail local tool execution after this many seconds", parsePositiveInteger, DEFAULT_TOOL_TIMEOUT_SECONDS).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
|
|
11514
11673
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
11515
11674
|
if (!context) {
|
|
11516
11675
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -11689,9 +11848,6 @@ async function runWatchIteration({ command, context, options, runnerId }) {
|
|
|
11689
11848
|
function claimLaneIds(maxConcurrentWork) {
|
|
11690
11849
|
return Array.from({ length: maxConcurrentWork }, (_, index) => index === 0 ? "default" : `lane_${index + 1}`);
|
|
11691
11850
|
}
|
|
11692
|
-
function supportsConcurrentLocalToolExecution(toolConfig) {
|
|
11693
|
-
return toolConfig.tool === "none" || toolConfig.effectiveInvocationChannel === "command" || toolConfig.effectiveTool === "custom";
|
|
11694
|
-
}
|
|
11695
11851
|
function aggregateRunnerLaneResults(results) {
|
|
11696
11852
|
const stopResult = results.find((result) => result.stopRunner);
|
|
11697
11853
|
if (stopResult) return stopResult;
|
|
@@ -11802,7 +11958,7 @@ async function runNextWorkItem({
|
|
|
11802
11958
|
...explicitTool ? { explicitTool } : {},
|
|
11803
11959
|
...toolCommand ? { toolCommand } : {}
|
|
11804
11960
|
});
|
|
11805
|
-
const effectiveMaxConcurrentWork =
|
|
11961
|
+
const effectiveMaxConcurrentWork = Math.min(Math.max(maxConcurrentWork, 1), MAX_CONCURRENT_RUNNER_WORK);
|
|
11806
11962
|
const effectiveActiveClaimLaneIds = claimLaneIds(effectiveMaxConcurrentWork);
|
|
11807
11963
|
const heartbeatConcurrency = { maxConcurrentWork: effectiveMaxConcurrentWork, activeClaimLaneIds: effectiveActiveClaimLaneIds };
|
|
11808
11964
|
if (claimLaneId !== "default" && effectiveMaxConcurrentWork === 1) {
|
|
@@ -12271,6 +12427,25 @@ async function runNextWorkItem({
|
|
|
12271
12427
|
let implementationHandoff;
|
|
12272
12428
|
let finalMessage = `${preview.toolName} exited with code ${toolResult.exitCode}.`;
|
|
12273
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
|
+
}
|
|
12274
12449
|
if (promptBatchResult && finalStatus !== "completed" && toolResult.exitCode === 0) {
|
|
12275
12450
|
finalMessage = "Prompt batch reported a failed or blocked child prompt.";
|
|
12276
12451
|
}
|
|
@@ -12321,33 +12496,59 @@ async function runNextWorkItem({
|
|
|
12321
12496
|
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
|
|
12322
12497
|
});
|
|
12323
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
|
+
};
|
|
12324
12519
|
try {
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
result.workItem.
|
|
12520
|
+
const implementationFinalization = await submitStagedImplementationFinalization(apiClient, {
|
|
12521
|
+
accountId: commandContext.accountId,
|
|
12522
|
+
attempt: result.workItem.attempt,
|
|
12328
12523
|
finalStatus,
|
|
12329
|
-
|
|
12524
|
+
idempotencyKey: finalizationIdempotencyKey,
|
|
12525
|
+
projectId,
|
|
12526
|
+
repositoryLinkId,
|
|
12330
12527
|
runnerId,
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
sessionDecision: sessionContext.decision,
|
|
12340
|
-
sessionDecisionReason: sessionContext.reason,
|
|
12341
|
-
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
12342
|
-
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {},
|
|
12343
|
-
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
12344
|
-
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
12345
|
-
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {},
|
|
12346
|
-
...implementationHandoff ? { implementationHandoff } : {},
|
|
12347
|
-
...result.workItem.workKind === "promptBatch" ? { promptBatch: finalizePromptBatchMetadata(result.workItem, finalStatus, finalError, promptBatchResult) } : {},
|
|
12348
|
-
...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 };
|
|
12349
12536
|
}
|
|
12350
|
-
|
|
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 };
|
|
12351
12552
|
} catch (error) {
|
|
12352
12553
|
if (error instanceof AmistioApiError && error.status === 409 && error.detail.includes("implementation_test_gate_required")) {
|
|
12353
12554
|
const gateMessage = "Implementation test gate was queued and must pass before completion or PR handoff is finalized.";
|
|
@@ -12394,6 +12595,7 @@ function artifactHandoffMetadata(artifacts) {
|
|
|
12394
12595
|
}
|
|
12395
12596
|
function isImplementationHandoffWork(workItem) {
|
|
12396
12597
|
const workKind = workItem.workKind ?? "implementation";
|
|
12598
|
+
if (workItem.implementationExpectedOutcome === "analysisOnly" || workItem.implementationExpectedOutcome === "verificationOnly") return false;
|
|
12397
12599
|
return workKind === "implementation" || workKind === "promptBatch";
|
|
12398
12600
|
}
|
|
12399
12601
|
async function loadWorkItemRepositoryLink(apiClient, projectId, repositoryLinkId) {
|
|
@@ -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") {
|