@amistio/cli 0.1.56 → 0.1.58
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 +5 -3
- package/dist/index.js +78 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,15 +95,17 @@ 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.
|
|
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. If finalization replay finds that the stored work claim belongs to a forgotten, missing, stale, offline, or lease-expired runner, the CLI requests abandoned-claim recovery and retries only after the server safely adopts the claim.
|
|
99
99
|
|
|
100
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.
|
|
101
101
|
|
|
102
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.
|
|
103
103
|
|
|
104
|
-
Runner watch mode defaults to bounded parallel claim lanes, capped at
|
|
104
|
+
Runner watch mode defaults to bounded parallel claim lanes, capped at 8. `--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. Before local tool execution, the runner records a bounded user-level active claim with work item, lane, lease, implementation scope, and worktree key, then releases it on completion or failure; if another local lane already owns the same work or worktree, the runner skips execution and releases the server claim when possible.
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Current runners recover abandoned server claims without impersonating the old owner. If a work item is still claimed by a forgotten, missing, offline, stale, or lease-expired runner, a compatible runner asks the API to release or adopt that claim through an audited recovery path before normal claiming or pending finalization replay continues. Fresh live claims still reject non-owner renewals, releases, and finalization attempts. If a project has no compatible runner, work waits for setup until one is paired and heartbeating.
|
|
107
|
+
|
|
108
|
+
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. Unchanged idle-ready status is printed once and then stays quiet until the next action changes.
|
|
107
109
|
|
|
108
110
|
Known validation failures such as `unsafe_context_path` are printed with attention-needed next steps. For project-context refresh path-safety failures, deploy the latest web/API fix, update and restart the runner when applicable, retry the refresh, and capture only bounded non-secret output if it repeats.
|
|
109
111
|
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Command } from "commander";
|
|
|
9
9
|
|
|
10
10
|
// ../shared/src/schemas.ts
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
var runnerMaxConcurrentWorkLimit = 8;
|
|
12
13
|
var isoDateTimeSchema = z.string().datetime({ offset: true });
|
|
13
14
|
var itemTypeSchema = z.enum([
|
|
14
15
|
"account",
|
|
@@ -601,6 +602,14 @@ var runnerResourceUsageSchema = z.object({
|
|
|
601
602
|
systemLoadAverage15m: z.number().nonnegative().optional()
|
|
602
603
|
});
|
|
603
604
|
var runnerClaimLaneIdSchema = z.string().trim().min(1).max(80);
|
|
605
|
+
var abandonedRunnerClaimRecoveryReasonSchema = z.enum([
|
|
606
|
+
"claimOwnerRemoved",
|
|
607
|
+
"claimOwnerMissing",
|
|
608
|
+
"claimOwnerOffline",
|
|
609
|
+
"claimOwnerStale",
|
|
610
|
+
"leaseExpired",
|
|
611
|
+
"pendingFinalizationOwnershipLost"
|
|
612
|
+
]);
|
|
604
613
|
var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
|
|
605
614
|
var runnerExecutionEnvironmentProfileSchema = z.enum(["hostWorktree", "hostWorktreeWithSetup", "dockerWorkspace", "cloudSandbox"]);
|
|
606
615
|
var runnerEnvironmentBlockerReasonSchema = z.enum([
|
|
@@ -900,6 +909,10 @@ var workItemSchema = baseItemSchema.extend({
|
|
|
900
909
|
claimLeaseId: z.string().min(1).optional(),
|
|
901
910
|
claimAttempt: z.number().int().nonnegative().optional(),
|
|
902
911
|
leaseExpiresAt: isoDateTimeSchema.optional(),
|
|
912
|
+
previousClaimedByRunnerId: z.string().min(1).optional(),
|
|
913
|
+
claimRecoveryReason: abandonedRunnerClaimRecoveryReasonSchema.optional(),
|
|
914
|
+
claimRecoveredByRunnerId: z.string().min(1).optional(),
|
|
915
|
+
claimRecoveredAt: isoDateTimeSchema.optional(),
|
|
903
916
|
controllingAdrId: z.string().min(1).optional(),
|
|
904
917
|
implementationScopeId: z.string().min(1).optional(),
|
|
905
918
|
executionBranch: z.string().min(1).optional(),
|
|
@@ -948,8 +961,8 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
948
961
|
supportedExecutionEnvironmentProfiles: z.array(runnerExecutionEnvironmentProfileSchema).optional(),
|
|
949
962
|
currentExecutionEnvironmentProfile: runnerExecutionEnvironmentProfileSchema.optional(),
|
|
950
963
|
environmentReadiness: runnerEnvironmentReadinessSchema.optional(),
|
|
951
|
-
maxConcurrentWork: z.number().int().min(1).max(
|
|
952
|
-
activeClaimLaneIds: z.array(runnerClaimLaneIdSchema).max(
|
|
964
|
+
maxConcurrentWork: z.number().int().min(1).max(runnerMaxConcurrentWorkLimit).optional(),
|
|
965
|
+
activeClaimLaneIds: z.array(runnerClaimLaneIdSchema).max(runnerMaxConcurrentWorkLimit).optional(),
|
|
953
966
|
currentWorkItemId: z.string().min(1).optional(),
|
|
954
967
|
currentImplementationScopeId: z.string().min(1).optional(),
|
|
955
968
|
currentWorktreeKey: z.string().min(1).optional(),
|
|
@@ -1902,11 +1915,20 @@ var implementationTestGateItemSchema = baseItemSchema.extend({
|
|
|
1902
1915
|
source: implementationTestGateSourceSchema.default("system"),
|
|
1903
1916
|
sourceWorkItemId: z.string().min(1),
|
|
1904
1917
|
gateWorkItemId: z.string().min(1).optional(),
|
|
1918
|
+
generatedDraftId: z.string().min(1).optional(),
|
|
1919
|
+
changeId: z.string().min(1).optional(),
|
|
1905
1920
|
repositoryLinkId: z.string().min(1).optional(),
|
|
1921
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
1906
1922
|
runnerId: z.string().min(1).optional(),
|
|
1907
1923
|
planDocumentId: z.string().min(1).optional(),
|
|
1908
1924
|
planDocumentRevision: z.number().int().nonnegative().optional(),
|
|
1909
1925
|
testProfileId: z.string().min(1).optional(),
|
|
1926
|
+
autopilotAuthorization: autopilotAuthorizationSchema.optional(),
|
|
1927
|
+
autopilotAuthorizationId: z.string().min(1).optional(),
|
|
1928
|
+
autopilotCandidateId: z.string().min(1).optional(),
|
|
1929
|
+
autopilotCandidateType: autopilotCandidateTypeSchema.optional(),
|
|
1930
|
+
autopilotClassificationOutcome: autopilotClassificationOutcomeSchema.optional(),
|
|
1931
|
+
autopilotPolicyVersion: z.string().trim().min(1).max(80).optional(),
|
|
1910
1932
|
outcome: implementationTestGateOutcomeSchema.optional(),
|
|
1911
1933
|
result: implementationTestGateResultSchema.optional(),
|
|
1912
1934
|
summary: z.string().trim().min(1).max(2e3).optional(),
|
|
@@ -2890,6 +2912,9 @@ var AmistioApiError = class extends Error {
|
|
|
2890
2912
|
function isForgottenRunnerApiError(error) {
|
|
2891
2913
|
return error instanceof AmistioApiError && error.status === 403 && error.detail.includes("This runner was forgotten");
|
|
2892
2914
|
}
|
|
2915
|
+
function isWorkItemNotClaimedByRunnerApiError(error) {
|
|
2916
|
+
return error instanceof AmistioApiError && error.status === 403 && error.detail.includes("Work item is not claimed by this runner.");
|
|
2917
|
+
}
|
|
2893
2918
|
function isRetryableApiError(error) {
|
|
2894
2919
|
if (error instanceof AmistioApiError) {
|
|
2895
2920
|
return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
@@ -3146,6 +3171,21 @@ var ApiClient = class {
|
|
|
3146
3171
|
}
|
|
3147
3172
|
);
|
|
3148
3173
|
}
|
|
3174
|
+
async recoverAbandonedWorkClaim(projectId, workItemId, input) {
|
|
3175
|
+
return this.request(
|
|
3176
|
+
`/projects/${projectId}/work-items/${workItemId}/claim-recovery`,
|
|
3177
|
+
z3.object({
|
|
3178
|
+
status: z3.enum(["recovered", "notNeeded"]),
|
|
3179
|
+
workItem: workItemSchema,
|
|
3180
|
+
message: z3.string(),
|
|
3181
|
+
recoveryReason: abandonedRunnerClaimRecoveryReasonSchema.optional()
|
|
3182
|
+
}),
|
|
3183
|
+
{
|
|
3184
|
+
method: "POST",
|
|
3185
|
+
body: JSON.stringify(input)
|
|
3186
|
+
}
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3149
3189
|
async submitBrainGenerationResult(projectId, workItemId, result) {
|
|
3150
3190
|
return this.request(
|
|
3151
3191
|
`/projects/${projectId}/work-items/${workItemId}/generation-result`,
|
|
@@ -5822,7 +5862,7 @@ function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateR
|
|
|
5822
5862
|
if (!previous || previous.key !== key) {
|
|
5823
5863
|
return true;
|
|
5824
5864
|
}
|
|
5825
|
-
if (action.kind === "workCompleted") {
|
|
5865
|
+
if (action.kind === "workCompleted" || action.kind === "idle") {
|
|
5826
5866
|
return false;
|
|
5827
5867
|
}
|
|
5828
5868
|
return nowMs - previous.printedAtMs >= reminderMs;
|
|
@@ -11221,7 +11261,7 @@ var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
|
|
|
11221
11261
|
var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
|
|
11222
11262
|
var RUNNER_WORK_LEASE_SECONDS = 300;
|
|
11223
11263
|
var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
|
|
11224
|
-
var MAX_CONCURRENT_RUNNER_WORK =
|
|
11264
|
+
var MAX_CONCURRENT_RUNNER_WORK = runnerMaxConcurrentWorkLimit;
|
|
11225
11265
|
var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "promptBatch", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "brainConsolidationScan", "projectContextRefresh", "implementationVerification", "testQualityScan", "implementationTestGate"];
|
|
11226
11266
|
program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
|
|
11227
11267
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
@@ -13512,13 +13552,24 @@ async function submitImplementationFinalizationEntry(apiClient, entry, options =
|
|
|
13512
13552
|
...entry.telemetry,
|
|
13513
13553
|
message: message2,
|
|
13514
13554
|
blockerReason: message2,
|
|
13515
|
-
releaseClaim: true
|
|
13555
|
+
releaseClaim: true,
|
|
13556
|
+
releaseReason: "implementationTestGatePending"
|
|
13516
13557
|
}).catch((releaseError) => {
|
|
13517
13558
|
console.error(`Could not release implementation work ${entry.workItemId} while waiting for its test gate: ${truncateLogExcerpt(errorMessage7(releaseError))}`);
|
|
13518
13559
|
});
|
|
13519
13560
|
console.log(message2);
|
|
13520
13561
|
return { status: "deferred", message: message2, retryable: true };
|
|
13521
13562
|
}
|
|
13563
|
+
if (isWorkItemNotClaimedByRunnerApiError(error)) {
|
|
13564
|
+
const recovered = await recoverPendingImplementationFinalizationClaim(apiClient, entry);
|
|
13565
|
+
if (recovered) {
|
|
13566
|
+
return submitImplementationFinalizationEntry(apiClient, entry, options);
|
|
13567
|
+
}
|
|
13568
|
+
await markImplementationFinalizationRetry(entry, "pending_finalization_ownership_lost");
|
|
13569
|
+
const message2 = `Pending implementation finalization ${entry.workItemId} is waiting for abandoned claim recovery before replay can continue.`;
|
|
13570
|
+
console.log(message2);
|
|
13571
|
+
return { status: "deferred", message: message2, retryable: true };
|
|
13572
|
+
}
|
|
13522
13573
|
const detail = truncateLogExcerpt(errorMessage7(error));
|
|
13523
13574
|
if (isRetryableApiError(error)) {
|
|
13524
13575
|
const updated = await markImplementationFinalizationRetry(entry, detail);
|
|
@@ -13548,6 +13599,28 @@ async function submitImplementationFinalizationEntry(apiClient, entry, options =
|
|
|
13548
13599
|
}
|
|
13549
13600
|
return { status: "completed", workItem: response.workItem };
|
|
13550
13601
|
}
|
|
13602
|
+
async function recoverPendingImplementationFinalizationClaim(apiClient, entry) {
|
|
13603
|
+
console.log(`Work claim owner is gone for ${entry.workItemId}; requesting abandoned-claim recovery before retrying finalization.`);
|
|
13604
|
+
try {
|
|
13605
|
+
const recovery = await apiClient.recoverAbandonedWorkClaim(entry.projectId, entry.workItemId, {
|
|
13606
|
+
runnerId: entry.runnerId,
|
|
13607
|
+
repositoryLinkId: entry.repositoryLinkId,
|
|
13608
|
+
reason: "pendingFinalizationOwnershipLost",
|
|
13609
|
+
mode: "adopt",
|
|
13610
|
+
...entry.telemetry.claimLaneId ? { claimLaneId: entry.telemetry.claimLaneId } : {},
|
|
13611
|
+
...entry.telemetry.machineId ? { machineId: entry.telemetry.machineId } : {},
|
|
13612
|
+
leaseSeconds: RUNNER_WORK_LEASE_SECONDS,
|
|
13613
|
+
supportedWorkKinds: runnerSupportedWorkKinds,
|
|
13614
|
+
supportsBranchIsolation: true,
|
|
13615
|
+
supportsGitWorktreeIsolation: true,
|
|
13616
|
+
maxConcurrentWork: 1
|
|
13617
|
+
});
|
|
13618
|
+
return recovery.workItem.claimedByRunnerId === entry.runnerId;
|
|
13619
|
+
} catch (error) {
|
|
13620
|
+
console.error(`Could not recover abandoned claim for ${entry.workItemId}: ${truncateLogExcerpt(errorMessage7(error))}`);
|
|
13621
|
+
return false;
|
|
13622
|
+
}
|
|
13623
|
+
}
|
|
13551
13624
|
function runnerMilestoneStatusFromWorkStatus(status) {
|
|
13552
13625
|
if (status === "completed" || status === "failed" || status === "running" || status === "blocked") return status;
|
|
13553
13626
|
if (status === "approved" || status === "drafted") return "queued";
|