@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 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 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. 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.
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
- 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.
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(4).optional(),
952
- activeClaimLaneIds: z.array(runnerClaimLaneIdSchema).max(4).optional(),
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 = 4;
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amistio/cli",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",