@amistio/cli 0.1.40 → 0.1.41

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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +478 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,8 @@ The package install provides the `amistio` command and the optional `amistio-hos
13
13
 
14
14
  For enterprise setup, use the web Runner panel as the primary guide. It shows repository, pairing code, GitHub access, AI provider, local runner, and verification readiness with plain next actions; advanced CLI logs remain secondary diagnostics. The panel also shows trust/privacy boundaries, cost forecast and budget posture, runner health, blockers, verification health, and runner-version distribution from safe runner metadata.
15
15
 
16
+ Runner execution profiles make runtime readiness explicit before local AI/tool execution. `amistio run --watch --execution-profile hostWorktree` keeps the default ADR-scoped Git worktree and checks Git, Node, Corepack, package manager, scripts, dependencies, and setup state. `--execution-profile hostWorktreeWithSetup --setup-package-manager-install` permits only the fixed package-manager install command inferred from package metadata, then rechecks readiness. `--execution-profile dockerWorkspace` validates Docker availability and a safe envelope mounted only to the worktree, rejecting privileged mode, host networking, extra mounts, and arbitrary environment injection; current runners still block approved work until containerized harness execution is enabled.
17
+
16
18
  Optional host helpers are configured outside the SaaS with `AMISTIO_HOST_HELPER_PATH`. The official npm package ships `amistio-host-helper` for Level 1 supervised process execution diagnostics; enable it with `AMISTIO_HOST_HELPER_PATH=$(command -v amistio-host-helper)` on reviewed runner machines and confirm `amistio host-helper status` plus `amistio host-helper conformance`. PTY and sandbox requests use a helper only when its versioned handshake advertises support; otherwise the CLI returns deterministic unsupported results instead of silently downgrading. The npm helper does not advertise PTY or sandbox support. Helper request envelopes include allowlisted environment values, explicit sandbox policy metadata, and bounded normalized output, and must never include provider tokens, OAuth material, runner API tokens, local auth files, or arbitrary SaaS-originated shell text.
17
19
 
18
20
  Before publishing the CLI, run the package release gate:
@@ -51,6 +53,9 @@ If no supported tool is available, install and authenticate one of the supported
51
53
  ```sh
52
54
  amistio sync watch
53
55
  amistio run --watch --tool opencode
56
+ amistio run --watch --execution-profile hostWorktree
57
+ amistio run --watch --execution-profile hostWorktreeWithSetup --setup-package-manager-install
58
+ amistio run --watch --execution-profile dockerWorkspace
54
59
  amistio run --watch --tool opencode --provider anthropic --model-id claude-opus-4.6 --reasoning-effort xhigh
55
60
  amistio run --watch --max-concurrent-work 2 --tool opencode
56
61
  amistio run --watch --background --tool opencode
@@ -83,6 +88,8 @@ Failed or stale work can be requeued from the web Tasks panel. Requeue creates a
83
88
 
84
89
  Runner setup and local-tool execution use bounded failure controls. `amistio run --watch` retries Git worktree preflight failures by releasing the claim for another attempt, then fails 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.
85
90
 
91
+ 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. Work and Runner surfaces receive sanitized profile/readiness metadata only; raw host paths, command lines, environment values, and secrets are not uploaded.
92
+
86
93
  Runner watch mode defaults to one claim lane. `--max-concurrent-work <count>` enables bounded parallel claim lanes for one paired runner, capped at 4. 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.
87
94
 
88
95
  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.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { createHash as createHash9, randomUUID as randomUUID3 } from "node:crypto";
5
5
  import { writeFile as writeFile12 } from "node:fs/promises";
6
6
  import os9 from "node:os";
7
- import path18 from "node:path";
7
+ import path19 from "node:path";
8
8
  import { Command } from "commander";
9
9
 
10
10
  // ../shared/src/schemas.ts
@@ -551,6 +551,45 @@ var runnerResourceUsageSchema = z.object({
551
551
  });
552
552
  var runnerClaimLaneIdSchema = z.string().trim().min(1).max(80);
553
553
  var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
554
+ var runnerExecutionEnvironmentProfileSchema = z.enum(["hostWorktree", "hostWorktreeWithSetup", "dockerWorkspace", "cloudSandbox"]);
555
+ var runnerEnvironmentBlockerReasonSchema = z.enum([
556
+ "missingToolchain",
557
+ "missingPackageManager",
558
+ "missingCorepack",
559
+ "missingScript",
560
+ "dependenciesNotReady",
561
+ "dockerUnavailable",
562
+ "dockerProfileInvalid",
563
+ "setupCommandNotAllowed",
564
+ "setupFailed",
565
+ "unsupportedProfile"
566
+ ]);
567
+ var runnerEnvironmentReadinessStatusSchema = z.enum(["ready", "blocked"]);
568
+ var runnerEnvironmentCheckStatusSchema = z.enum(["passed", "blocked", "warning", "skipped"]);
569
+ var runnerEnvironmentBlockerSchema = z.object({
570
+ reason: runnerEnvironmentBlockerReasonSchema,
571
+ summary: z.string().trim().min(1).max(600),
572
+ remediation: z.string().trim().min(1).max(600).optional(),
573
+ safePaths: z.array(safeRepoPathSchema).max(20).default([])
574
+ }).strict();
575
+ var runnerEnvironmentCheckSchema = z.object({
576
+ checkId: z.string().trim().min(1).max(80),
577
+ title: z.string().trim().min(1).max(120),
578
+ status: runnerEnvironmentCheckStatusSchema,
579
+ reason: runnerEnvironmentBlockerReasonSchema.optional(),
580
+ summary: z.string().trim().min(1).max(600).optional(),
581
+ safePaths: z.array(safeRepoPathSchema).max(20).default([])
582
+ }).strict();
583
+ var runnerEnvironmentReadinessSchema = z.object({
584
+ profile: runnerExecutionEnvironmentProfileSchema,
585
+ status: runnerEnvironmentReadinessStatusSchema,
586
+ summary: z.string().trim().min(1).max(1e3),
587
+ checks: z.array(runnerEnvironmentCheckSchema).max(40).default([]),
588
+ blockers: z.array(runnerEnvironmentBlockerSchema).max(20).default([]),
589
+ warnings: z.array(z.string().trim().min(1).max(600)).max(20).default([]),
590
+ safePaths: z.array(safeRepoPathSchema).max(30).default([]),
591
+ checkedAt: isoDateTimeSchema.optional()
592
+ }).strict();
554
593
  var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
555
594
  var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
556
595
  var repositoryBrainAutoSyncStatusSchema = z.enum(["disabled", "enabledInactive", "active", "synced", "failed", "blocked", "conflicted"]);
@@ -813,6 +852,8 @@ var workItemSchema = baseItemSchema.extend({
813
852
  executionBranch: z.string().min(1).optional(),
814
853
  executionWorktreeKey: z.string().min(1).optional(),
815
854
  isolationMode: workIsolationModeSchema.optional(),
855
+ executionEnvironmentProfile: runnerExecutionEnvironmentProfileSchema.optional(),
856
+ executionEnvironmentReadiness: runnerEnvironmentReadinessSchema.optional(),
816
857
  repositoryLockId: z.string().min(1).optional(),
817
858
  baseRevision: z.string().min(1).optional(),
818
859
  baseContentHash: z.string().min(1).optional(),
@@ -850,6 +891,9 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
850
891
  supportedWorkKinds: z.array(workKindSchema).optional(),
851
892
  supportsBranchIsolation: z.boolean().optional(),
852
893
  supportsGitWorktreeIsolation: z.boolean().optional(),
894
+ supportedExecutionEnvironmentProfiles: z.array(runnerExecutionEnvironmentProfileSchema).optional(),
895
+ currentExecutionEnvironmentProfile: runnerExecutionEnvironmentProfileSchema.optional(),
896
+ environmentReadiness: runnerEnvironmentReadinessSchema.optional(),
853
897
  maxConcurrentWork: z.number().int().min(1).max(4).optional(),
854
898
  activeClaimLaneIds: z.array(runnerClaimLaneIdSchema).max(4).optional(),
855
899
  currentWorkItemId: z.string().min(1).optional(),
@@ -1255,6 +1299,35 @@ var projectSystemDiagramGraphManifestSchema = z.object({
1255
1299
  unknowns: z.array(z.string().trim().min(1).max(300)).default([]),
1256
1300
  warnings: z.array(z.string().trim().min(1).max(600)).default([])
1257
1301
  });
1302
+ var projectSystemDiagramAutomationStatusSchema = z.enum(["current", "refreshing", "stale", "proposed", "degraded", "waitingForRunner", "blocked"]);
1303
+ var projectSystemDiagramProposalChangeKindSchema = z.enum(["node", "edge", "group", "boundary", "view", "mermaid", "metadata"]);
1304
+ var projectSystemDiagramProposalChangeSchema = z.object({
1305
+ proposalId: z.string().trim().min(1).max(200),
1306
+ kind: projectSystemDiagramProposalChangeKindSchema,
1307
+ title: z.string().trim().min(1).max(160),
1308
+ summary: z.string().trim().min(1).max(1200),
1309
+ viewIds: z.array(z.string().trim().min(1).max(160)).default([]),
1310
+ nodeIds: z.array(z.string().trim().min(1).max(200)).default([]),
1311
+ edgeIds: z.array(z.string().trim().min(1).max(220)).default([]),
1312
+ citations: z.array(projectContextCitationSchema).default([])
1313
+ });
1314
+ var projectSystemDiagramAutomationSchema = z.object({
1315
+ status: projectSystemDiagramAutomationStatusSchema,
1316
+ reason: z.string().trim().min(1).max(1e3).optional(),
1317
+ trigger: z.string().trim().min(1).max(80).optional(),
1318
+ sourceContextMapRevision: z.number().int().positive().optional(),
1319
+ sourceBrainRevision: z.number().int().nonnegative().optional(),
1320
+ lastCheckedAt: isoDateTimeSchema.optional(),
1321
+ lastRefreshAt: isoDateTimeSchema.optional(),
1322
+ lastRefreshResult: z.enum(["current", "regenerated", "proposalCreated", "missRecorded", "blocked", "degraded"]).optional(),
1323
+ staleEdgeCount: z.number().int().nonnegative().default(0),
1324
+ unreadableViewCount: z.number().int().nonnegative().default(0),
1325
+ waitingForRunner: z.boolean().default(false),
1326
+ proposedChangeCount: z.number().int().nonnegative().default(0),
1327
+ proposalChanges: z.array(projectSystemDiagramProposalChangeSchema).default([]),
1328
+ missCount: z.number().int().nonnegative().default(0),
1329
+ misses: z.array(z.string().trim().min(1).max(300)).default([])
1330
+ });
1258
1331
  var projectSystemDiagramItemSchema = baseItemSchema.extend({
1259
1332
  type: z.literal("projectSystemDiagram"),
1260
1333
  projectId: z.string().min(1),
@@ -1274,6 +1347,7 @@ var projectSystemDiagramItemSchema = baseItemSchema.extend({
1274
1347
  approvedAt: isoDateTimeSchema.optional(),
1275
1348
  generatedAt: isoDateTimeSchema,
1276
1349
  supersedesDiagramId: z.string().min(1).optional(),
1350
+ automation: projectSystemDiagramAutomationSchema.optional(),
1277
1351
  error: z.string().max(1e3).optional()
1278
1352
  });
1279
1353
  var projectContextMapItemSchema = baseItemSchema.extend({
@@ -2353,10 +2427,10 @@ function computeProjectNextAction(input) {
2353
2427
  const queuedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "approved"));
2354
2428
  const queuedImplementation = latestWorkItem(input.workItems.filter((item) => item.workKind !== "brainGeneration" && item.workKind !== "planRevision" && item.status === "approved"));
2355
2429
  const queuedWork = queuedBrainGeneration ?? queuedPlanRevision ?? queuedImplementation;
2356
- const readiness = getSharedRunnerReadiness(pairedRepositoryLinks, input.runnerHeartbeats, nowMs);
2357
- if (queuedWork && !readiness.ready) {
2430
+ const readiness2 = getSharedRunnerReadiness(pairedRepositoryLinks, input.runnerHeartbeats, nowMs);
2431
+ if (queuedWork && !readiness2.ready) {
2358
2432
  const title = queuedWork.workKind === "brainGeneration" ? "Brain generation is queued" : queuedWork.workKind === "planRevision" ? "Plan revision is queued" : "Implementation is queued";
2359
- return runnerWaitAction(readiness, title);
2433
+ return runnerWaitAction(readiness2, title);
2360
2434
  }
2361
2435
  if (queuedBrainGeneration) {
2362
2436
  return workAction(queuedBrainGeneration, "brainGenerationQueued", "runner", "warning", "Brain generation queued", "The local runner can claim this queued brain-generation work.");
@@ -2367,8 +2441,8 @@ function computeProjectNextAction(input) {
2367
2441
  if (queuedImplementation) {
2368
2442
  return workAction(queuedImplementation, "implementationQueued", "runner", "warning", "Implementation queued", "The local runner can claim approved implementation work.");
2369
2443
  }
2370
- if (!readiness.ready) {
2371
- return runnerWaitAction(readiness, "Runner setup needed");
2444
+ if (!readiness2.ready) {
2445
+ return runnerWaitAction(readiness2, "Runner setup needed");
2372
2446
  }
2373
2447
  const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed" && isFreshCompletedWork(item, nowMs)));
2374
2448
  if (completedWork) {
@@ -2380,38 +2454,38 @@ function computeProjectNextAction(input) {
2380
2454
  tone: "success",
2381
2455
  title: "Ready for the next task",
2382
2456
  message: "The repository and runner are ready for a new project-brain request.",
2383
- ...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
2384
- ...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
2385
- ...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
2457
+ ...readiness2.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness2.repositoryLink.repositoryLinkId } : {},
2458
+ ...readiness2.heartbeat?.runnerId ? { runnerId: readiness2.heartbeat.runnerId } : {},
2459
+ ...readiness2.heartbeat?.lastSeenAt ? { updatedAt: readiness2.heartbeat.lastSeenAt } : {}
2386
2460
  };
2387
2461
  }
2388
2462
  function formatProjectNextAction(action) {
2389
2463
  return `${action.title}: ${action.message}${action.detail ? ` ${action.detail}` : ""}`;
2390
2464
  }
2391
- function runnerWaitAction(readiness, fallbackTitle) {
2392
- const repositoryName = readiness.repositoryLink?.repoName ?? "the paired repository";
2393
- if (readiness.reason === "runnerOffline") {
2465
+ function runnerWaitAction(readiness2, fallbackTitle) {
2466
+ const repositoryName = readiness2.repositoryLink?.repoName ?? "the paired repository";
2467
+ if (readiness2.reason === "runnerOffline") {
2394
2468
  return {
2395
2469
  kind: "runnerOffline",
2396
2470
  actor: "user",
2397
2471
  tone: "danger",
2398
2472
  title: "Restart the runner",
2399
2473
  message: `The runner for ${repositoryName} is offline. Start amistio run --watch from the paired checkout.`,
2400
- ...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
2401
- ...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
2402
- ...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
2474
+ ...readiness2.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness2.repositoryLink.repositoryLinkId } : {},
2475
+ ...readiness2.heartbeat?.runnerId ? { runnerId: readiness2.heartbeat.runnerId } : {},
2476
+ ...readiness2.heartbeat?.lastSeenAt ? { updatedAt: readiness2.heartbeat.lastSeenAt } : {}
2403
2477
  };
2404
2478
  }
2405
- if (readiness.reason === "runnerStale") {
2479
+ if (readiness2.reason === "runnerStale") {
2406
2480
  return {
2407
2481
  kind: "runnerStale",
2408
2482
  actor: "user",
2409
2483
  tone: "warning",
2410
2484
  title: "Refresh the runner",
2411
2485
  message: `The runner for ${repositoryName} has not checked in recently. Restart amistio run --watch from the paired checkout.`,
2412
- ...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
2413
- ...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
2414
- ...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
2486
+ ...readiness2.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness2.repositoryLink.repositoryLinkId } : {},
2487
+ ...readiness2.heartbeat?.runnerId ? { runnerId: readiness2.heartbeat.runnerId } : {},
2488
+ ...readiness2.heartbeat?.lastSeenAt ? { updatedAt: readiness2.heartbeat.lastSeenAt } : {}
2415
2489
  };
2416
2490
  }
2417
2491
  return {
@@ -2420,7 +2494,7 @@ function runnerWaitAction(readiness, fallbackTitle) {
2420
2494
  tone: "warning",
2421
2495
  title: fallbackTitle,
2422
2496
  message: `Start amistio run --watch from ${repositoryName} so the local runner can claim work.`,
2423
- ...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {}
2497
+ ...readiness2.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness2.repositoryLink.repositoryLinkId } : {}
2424
2498
  };
2425
2499
  }
2426
2500
  function getSharedRunnerReadiness(repositoryLinks, runnerHeartbeats, nowMs) {
@@ -3145,8 +3219,8 @@ var toolSessionMutationSchema = z3.object({
3145
3219
  });
3146
3220
  function resolveApiUrl(apiUrl, urlPath) {
3147
3221
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
3148
- const path19 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
3149
- return new URL(`${base}${path19}`);
3222
+ const path20 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
3223
+ return new URL(`${base}${path20}`);
3150
3224
  }
3151
3225
 
3152
3226
  // src/orchestrator.ts
@@ -8729,6 +8803,12 @@ function buildBackgroundRunnerArgs(options) {
8729
8803
  if (options.reasoningEffort) {
8730
8804
  args.push("--reasoning-effort", options.reasoningEffort);
8731
8805
  }
8806
+ if (options.executionProfile) {
8807
+ args.push("--execution-profile", options.executionProfile);
8808
+ }
8809
+ if (options.setupPackageManagerInstall) {
8810
+ args.push("--setup-package-manager-install");
8811
+ }
8732
8812
  if (options.maxIterations !== void 0) {
8733
8813
  args.push("--max-iterations", String(options.maxIterations));
8734
8814
  }
@@ -9826,6 +9906,320 @@ function accountFingerprint(host, login) {
9826
9906
  return `sha256:${createHash8("sha256").update(`${host.toLowerCase()}\0${login.toLowerCase()}`).digest("hex")}`;
9827
9907
  }
9828
9908
 
9909
+ // src/runner-environment.ts
9910
+ import { constants as constants2 } from "node:fs";
9911
+ import { access as access2, readFile as readFile12 } from "node:fs/promises";
9912
+ import path18 from "node:path";
9913
+ var defaultRunnerExecutionEnvironmentProfile = "hostWorktree";
9914
+ var supportedRunnerExecutionEnvironmentProfiles = ["hostWorktree", "hostWorktreeWithSetup", "dockerWorkspace"];
9915
+ var commandLookupScript = String.raw`
9916
+ const fs = require("node:fs");
9917
+ const path = require("node:path");
9918
+ const command = process.argv[1];
9919
+ const pathEntries = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
9920
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
9921
+ for (const directory of pathEntries) {
9922
+ for (const extension of extensions) {
9923
+ const candidate = path.join(directory, process.platform === "win32" && !path.extname(command) ? command + extension : command);
9924
+ try {
9925
+ fs.accessSync(candidate, fs.constants.X_OK);
9926
+ process.exit(0);
9927
+ } catch {}
9928
+ }
9929
+ }
9930
+ process.exit(1);
9931
+ `;
9932
+ async function checkRunnerExecutionEnvironment(options) {
9933
+ const profile = options.profile ?? defaultRunnerExecutionEnvironmentProfile;
9934
+ const hostExecution = options.hostExecution ?? await getRuntimeHostExecutionPort();
9935
+ const env = options.env ?? process.env;
9936
+ const checks = [];
9937
+ const blockers = [];
9938
+ const warnings = [];
9939
+ const packageDetection = await detectRepositoryPackageManager(options.executionRoot);
9940
+ const requiredScripts = [.../* @__PURE__ */ new Set([...options.requiredScripts ?? requiredScriptsForWorkKind(options.workKind)])];
9941
+ if (profile === "cloudSandbox") {
9942
+ addBlockedCheck(checks, blockers, "execution-profile", "Execution profile", "unsupportedProfile", "Cloud sandbox execution is reserved and is not supported by this runner.", "Select hostWorktree or dockerWorkspace for local execution.");
9943
+ return readiness(profile, checks, blockers, warnings, options.executionRoot);
9944
+ }
9945
+ await checkExecutable({ checks, blockers, hostExecution, env, executionRoot: options.executionRoot, command: "git", title: "Git", reason: "missingToolchain", remediation: "Install Git and ensure it is available on the runner PATH." });
9946
+ await checkExecutable({ checks, blockers, hostExecution, env, executionRoot: options.executionRoot, command: "node", title: "Node.js", reason: "missingToolchain", remediation: "Install Node.js and ensure it is available on the runner PATH." });
9947
+ if (packageDetection.packageManager) {
9948
+ await checkPackageManager({ checks, blockers, hostExecution, env, executionRoot: options.executionRoot, packageDetection });
9949
+ } else if (packageDetection.lockfiles.length || packageDetection.scripts.length) {
9950
+ addBlockedCheck(checks, blockers, "package-manager", "Package manager", "missingPackageManager", "The repository has package metadata but no supported package manager could be detected.", "Add a packageManager field or a supported lockfile.", ["package.json", ...packageDetection.lockfiles]);
9951
+ } else {
9952
+ checks.push({ checkId: "package-manager", title: "Package manager", status: "skipped", summary: "No package.json or supported lockfile was detected.", safePaths: [] });
9953
+ }
9954
+ for (const scriptName of requiredScripts) {
9955
+ if (!packageDetection.scripts.includes(scriptName)) {
9956
+ addBlockedCheck(checks, blockers, `script-${scriptName}`, `Package script: ${scriptName}`, "missingScript", `Required package script is missing: ${scriptName}.`, `Add a ${scriptName} script or update the runner test profile.`, ["package.json"]);
9957
+ } else {
9958
+ checks.push({ checkId: `script-${scriptName}`, title: `Package script: ${scriptName}`, status: "passed", summary: `Required package script is declared: ${scriptName}.`, safePaths: ["package.json"] });
9959
+ }
9960
+ }
9961
+ if (profile === "hostWorktreeWithSetup") {
9962
+ await runSetupCommands({ checks, blockers, hostExecution, env, executionRoot: options.executionRoot, packageDetection, setupCommands: options.setupCommands ?? [], timeoutMs: options.setupTimeoutMs ?? 12e4 });
9963
+ } else if (options.setupCommands?.length) {
9964
+ addBlockedCheck(checks, blockers, "setup-profile", "Setup profile", "setupCommandNotAllowed", "Setup commands require the hostWorktreeWithSetup execution profile.", "Select hostWorktreeWithSetup or remove setup commands.");
9965
+ }
9966
+ if (packageDetection.packageManager && await fileExists2(path18.join(options.executionRoot, "package.json")) && !await fileExists2(path18.join(options.executionRoot, "node_modules"))) {
9967
+ const dependencySummary = profile === "hostWorktreeWithSetup" ? "Repository dependencies are not ready after setup." : "Repository dependencies are not ready in this worktree.";
9968
+ addBlockedCheck(checks, blockers, "dependencies", "Dependencies", "dependenciesNotReady", dependencySummary, "Run an approved setup profile or install dependencies in the execution worktree.", ["package.json"]);
9969
+ } else if (packageDetection.packageManager) {
9970
+ checks.push({ checkId: "dependencies", title: "Dependencies", status: "passed", summary: "Repository dependencies appear ready for local package scripts.", safePaths: ["package.json"] });
9971
+ }
9972
+ if (profile === "dockerWorkspace") {
9973
+ await checkDockerWorkspace({
9974
+ checks,
9975
+ blockers,
9976
+ hostExecution,
9977
+ env,
9978
+ executionRoot: options.executionRoot,
9979
+ ...options.primaryCheckoutRoot ? { primaryCheckoutRoot: options.primaryCheckoutRoot } : {},
9980
+ ...options.docker ? { docker: options.docker } : {}
9981
+ });
9982
+ }
9983
+ return readiness(profile, checks, blockers, warnings, options.executionRoot);
9984
+ }
9985
+ function createDockerWorkspaceCommandEnvelope(options) {
9986
+ const executionRoot = path18.resolve(options.executionRoot);
9987
+ const primaryCheckoutRoot = options.primaryCheckoutRoot ? path18.resolve(options.primaryCheckoutRoot) : void 0;
9988
+ const docker = options.docker ?? {};
9989
+ const network = docker.network ?? "none";
9990
+ const image = docker.image ?? "node:20-bookworm";
9991
+ if (primaryCheckoutRoot && executionRoot === primaryCheckoutRoot) {
9992
+ return { ok: false, reason: "dockerProfileInvalid", summary: "Docker workspace mode requires an ADR-scoped worktree, not the primary checkout." };
9993
+ }
9994
+ if (docker.privileged) {
9995
+ return { ok: false, reason: "dockerProfileInvalid", summary: "Docker workspace mode does not allow privileged containers." };
9996
+ }
9997
+ if (network === "host") {
9998
+ return { ok: false, reason: "dockerProfileInvalid", summary: "Docker workspace mode does not allow host networking." };
9999
+ }
10000
+ if (docker.mounts?.length) {
10001
+ return { ok: false, reason: "dockerProfileInvalid", summary: "Docker workspace mode only mounts the prepared execution worktree." };
10002
+ }
10003
+ if (docker.env && Object.keys(docker.env).length > 0) {
10004
+ return { ok: false, reason: "dockerProfileInvalid", summary: "Docker workspace mode does not accept unbounded environment injection." };
10005
+ }
10006
+ return {
10007
+ ok: true,
10008
+ envelope: {
10009
+ command: "docker",
10010
+ cwd: executionRoot,
10011
+ env: dockerSafeEnvironment(options.env ?? process.env),
10012
+ args: [
10013
+ "run",
10014
+ "--rm",
10015
+ "--network",
10016
+ network,
10017
+ "--workdir",
10018
+ "/workspace",
10019
+ "--mount",
10020
+ `type=bind,source=${executionRoot},target=/workspace`,
10021
+ image,
10022
+ "node",
10023
+ "--version"
10024
+ ]
10025
+ }
10026
+ };
10027
+ }
10028
+ function requiredScriptsForWorkKind(workKind) {
10029
+ if (workKind === "implementationTestGate" || workKind === "testQualityScan") {
10030
+ return ["test"];
10031
+ }
10032
+ if (workKind === "implementationVerification") {
10033
+ return ["typecheck"];
10034
+ }
10035
+ return [];
10036
+ }
10037
+ async function detectRepositoryPackageManager(executionRoot) {
10038
+ const policy = createBoundedToolAdapterPolicy({ executionRoot, mutationPolicy: "readOnly" });
10039
+ const result = await runPackageManagerDetectTool({ policy });
10040
+ if (result.status !== "completed") {
10041
+ return { lockfiles: [], scripts: [], profiles: [] };
10042
+ }
10043
+ const parsed = JSON.parse(result.stdout);
10044
+ if (!isPackageManagerDetection(parsed)) {
10045
+ return { lockfiles: [], scripts: [], profiles: [] };
10046
+ }
10047
+ return parsed;
10048
+ }
10049
+ async function checkPackageManager(options) {
10050
+ const packageManager = options.packageDetection.packageManager;
10051
+ if (!packageManager) return;
10052
+ if (packageManager === "pnpm" || packageManager === "yarn") {
10053
+ const corepackAvailable = await isCommandAvailable(options.hostExecution, "corepack", options.executionRoot, options.env);
10054
+ if (!corepackAvailable) {
10055
+ addBlockedCheck(options.checks, options.blockers, "corepack", "Corepack", "missingCorepack", `${packageManager} repositories require Corepack or an installed package-manager command.`, "Enable Corepack or install the package manager before rerunning work.", ["package.json", ...options.packageDetection.lockfiles]);
10056
+ } else {
10057
+ options.checks.push({ checkId: "corepack", title: "Corepack", status: "passed", summary: "Corepack is available to the runner PATH.", safePaths: [] });
10058
+ }
10059
+ }
10060
+ const directPackageManagerAvailable = await isCommandAvailable(options.hostExecution, packageManager, options.executionRoot, options.env);
10061
+ const corepackPackageManagerAvailable = packageManager === "pnpm" || packageManager === "yarn" ? await canRunCorepackPackageManager(options.hostExecution, packageManager, options.executionRoot, options.env) : false;
10062
+ if (!directPackageManagerAvailable && !corepackPackageManagerAvailable) {
10063
+ addBlockedCheck(options.checks, options.blockers, "package-manager", "Package manager", "missingPackageManager", `${packageManager} is required by repository package metadata but is not runnable in the runner environment.`, "Install the package manager or enable Corepack for this runner.", ["package.json", ...options.packageDetection.lockfiles]);
10064
+ return;
10065
+ }
10066
+ options.checks.push({ checkId: "package-manager", title: "Package manager", status: "passed", summary: `${packageManager} is runnable in the runner environment.`, safePaths: ["package.json", ...options.packageDetection.lockfiles] });
10067
+ }
10068
+ async function checkExecutable(options) {
10069
+ const available = await isCommandAvailable(options.hostExecution, options.command, options.executionRoot, options.env);
10070
+ if (!available) {
10071
+ addBlockedCheck(options.checks, options.blockers, `toolchain-${options.command}`, options.title, options.reason, `${options.title} is not available to the runner PATH.`, options.remediation);
10072
+ return;
10073
+ }
10074
+ options.checks.push({ checkId: `toolchain-${options.command}`, title: options.title, status: "passed", summary: `${options.title} is available to the runner PATH.`, safePaths: [] });
10075
+ }
10076
+ async function runSetupCommands(options) {
10077
+ if (!options.setupCommands.length) {
10078
+ options.checks.push({ checkId: "setup-profile", title: "Setup profile", status: "skipped", summary: "No approved setup commands were requested.", safePaths: [] });
10079
+ return;
10080
+ }
10081
+ for (const setupCommand of options.setupCommands) {
10082
+ const command = setupCommandForPackageManager(setupCommand.id, options.packageDetection.packageManager, options.packageDetection.lockfiles);
10083
+ if (!command) {
10084
+ addBlockedCheck(options.checks, options.blockers, `setup-${setupCommand.id}`, "Setup profile", "setupCommandNotAllowed", "Requested setup command is not allowed for the detected repository package manager.", "Use an approved package-manager setup command derived from repository metadata.", ["package.json", ...options.packageDetection.lockfiles]);
10085
+ continue;
10086
+ }
10087
+ const result = await options.hostExecution.executeCommand({
10088
+ command: command.command,
10089
+ args: command.args,
10090
+ cwd: options.executionRoot,
10091
+ env: options.env,
10092
+ shell: false,
10093
+ timeoutMs: options.timeoutMs,
10094
+ timeoutMessage: "Approved runner setup command timed out."
10095
+ });
10096
+ if (result.exitCode !== 0) {
10097
+ addBlockedCheck(options.checks, options.blockers, `setup-${setupCommand.id}`, "Setup profile", "setupFailed", "Approved runner setup command failed.", "Review setup output locally and rerun when dependencies are ready.", ["package.json", ...options.packageDetection.lockfiles]);
10098
+ continue;
10099
+ }
10100
+ options.checks.push({ checkId: `setup-${setupCommand.id}`, title: "Setup profile", status: "passed", summary: "Approved runner setup command completed.", safePaths: ["package.json", ...options.packageDetection.lockfiles] });
10101
+ }
10102
+ }
10103
+ async function checkDockerWorkspace(options) {
10104
+ const dockerAvailable = await isCommandAvailable(options.hostExecution, "docker", options.executionRoot, options.env);
10105
+ if (!dockerAvailable) {
10106
+ addBlockedCheck(options.checks, options.blockers, "docker", "Docker", "dockerUnavailable", "Docker is not available to the runner PATH.", "Install Docker Desktop or select hostWorktree execution.");
10107
+ return;
10108
+ }
10109
+ const envelope = createDockerWorkspaceCommandEnvelope(options);
10110
+ if (!envelope.ok) {
10111
+ addBlockedCheck(options.checks, options.blockers, "docker-profile", "Docker profile", envelope.reason, envelope.summary, "Use the default Docker workspace envelope mounted to the prepared worktree only.");
10112
+ return;
10113
+ }
10114
+ options.checks.push({ checkId: "docker", title: "Docker", status: "passed", summary: "Docker is available and the workspace envelope is valid.", safePaths: [] });
10115
+ }
10116
+ function setupCommandForPackageManager(commandId, packageManager, lockfiles) {
10117
+ if (commandId !== "packageManager.install" || !packageManager) {
10118
+ return void 0;
10119
+ }
10120
+ if (packageManager === "pnpm") {
10121
+ return { command: "corepack", args: ["pnpm", "--config.verify-deps-before-run=false", "install", "--frozen-lockfile"], displayName: "pnpm install", mutationPolicy: "mutating" };
10122
+ }
10123
+ if (packageManager === "yarn") {
10124
+ return { command: "corepack", args: ["yarn", "install", "--immutable"], displayName: "yarn install", mutationPolicy: "mutating" };
10125
+ }
10126
+ if (packageManager === "bun") {
10127
+ return { command: "bun", args: ["install", "--frozen-lockfile"], displayName: "bun install", mutationPolicy: "mutating" };
10128
+ }
10129
+ return lockfiles.includes("package-lock.json") ? { command: "npm", args: ["ci"], displayName: "npm ci", mutationPolicy: "mutating" } : { command: "npm", args: ["install"], displayName: "npm install", mutationPolicy: "mutating" };
10130
+ }
10131
+ async function isCommandAvailable(hostExecution, command, executionRoot, env) {
10132
+ if (env === process.env) {
10133
+ return hostExecution.commandExists(command);
10134
+ }
10135
+ const result = await hostExecution.executeCommand({
10136
+ command: process.execPath,
10137
+ args: ["-e", commandLookupScript, command],
10138
+ cwd: executionRoot,
10139
+ env,
10140
+ shell: false,
10141
+ timeoutMs: 5e3,
10142
+ timeoutMessage: "Command lookup timed out."
10143
+ });
10144
+ return result.exitCode === 0;
10145
+ }
10146
+ async function canRunCorepackPackageManager(hostExecution, packageManager, executionRoot, env) {
10147
+ if (packageManager !== "pnpm" && packageManager !== "yarn") {
10148
+ return false;
10149
+ }
10150
+ const corepackAvailable = await isCommandAvailable(hostExecution, "corepack", executionRoot, env);
10151
+ if (!corepackAvailable) {
10152
+ return false;
10153
+ }
10154
+ const result = await hostExecution.executeCommand({
10155
+ command: "corepack",
10156
+ args: [packageManager, "--version"],
10157
+ cwd: executionRoot,
10158
+ env,
10159
+ shell: false,
10160
+ timeoutMs: 1e4,
10161
+ timeoutMessage: "Package manager lookup through Corepack timed out."
10162
+ });
10163
+ return result.exitCode === 0;
10164
+ }
10165
+ function addBlockedCheck(checks, blockers, checkId, title, reason, summary, remediation, safePaths = []) {
10166
+ checks.push({ checkId, title, status: "blocked", reason, summary, safePaths: [...safePaths] });
10167
+ blockers.push({ reason, summary, remediation, safePaths: [...safePaths] });
10168
+ }
10169
+ function readiness(profile, checks, blockers, warnings, executionRoot) {
10170
+ const status = blockers.length ? "blocked" : "ready";
10171
+ const summary = status === "ready" ? `Runner execution environment ${profile} is ready.` : `Runner execution environment ${profile} is blocked: ${blockers.map((blocker) => blocker.reason).join(", ")}.`;
10172
+ return runnerEnvironmentReadinessSchema.parse({
10173
+ profile,
10174
+ status,
10175
+ summary: redactLocalPath(summary, executionRoot),
10176
+ checks: checks.map((check) => redactCheck(check, executionRoot)),
10177
+ blockers: blockers.map((blocker) => redactBlocker(blocker, executionRoot)),
10178
+ warnings: warnings.map((warning) => redactLocalPath(warning, executionRoot)),
10179
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
10180
+ });
10181
+ }
10182
+ function redactCheck(check, executionRoot) {
10183
+ return {
10184
+ ...check,
10185
+ ...check.summary ? { summary: redactLocalPath(check.summary, executionRoot) } : {},
10186
+ safePaths: check.safePaths ?? []
10187
+ };
10188
+ }
10189
+ function redactBlocker(blocker, executionRoot) {
10190
+ return {
10191
+ ...blocker,
10192
+ summary: redactLocalPath(blocker.summary, executionRoot),
10193
+ ...blocker.remediation ? { remediation: redactLocalPath(blocker.remediation, executionRoot) } : {},
10194
+ safePaths: blocker.safePaths ?? []
10195
+ };
10196
+ }
10197
+ function redactLocalPath(value, executionRoot) {
10198
+ return value.split(executionRoot).join("<execution-root>");
10199
+ }
10200
+ function dockerSafeEnvironment(env) {
10201
+ return Object.fromEntries(["HOME", "PATH", "TMPDIR", "TEMP", "TMP"].flatMap((envName) => {
10202
+ const value = env[envName];
10203
+ return value ? [[envName, value]] : [];
10204
+ }));
10205
+ }
10206
+ async function fileExists2(filePath) {
10207
+ try {
10208
+ await access2(filePath, constants2.F_OK);
10209
+ return true;
10210
+ } catch {
10211
+ return false;
10212
+ }
10213
+ }
10214
+ function isPackageManagerDetection(value) {
10215
+ if (!isRecord4(value)) return false;
10216
+ if (value.packageManager !== void 0 && !["pnpm", "npm", "yarn", "bun"].includes(String(value.packageManager))) return false;
10217
+ return Array.isArray(value.lockfiles) && Array.isArray(value.scripts) && Array.isArray(value.profiles);
10218
+ }
10219
+ function isRecord4(value) {
10220
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10221
+ }
10222
+
9829
10223
  // src/version.ts
9830
10224
  import { readFileSync } from "node:fs";
9831
10225
  function readCliPackageVersion() {
@@ -10233,7 +10627,7 @@ hostHelper.command("conformance").description("Run local compatibility checks ag
10233
10627
  process.exitCode = 1;
10234
10628
  }
10235
10629
  });
10236
- program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel, "auto").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("--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("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
10630
+ program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel, "auto").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("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
10237
10631
  const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
10238
10632
  const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
10239
10633
  if (options.promptOut) {
@@ -10297,7 +10691,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
10297
10691
  projectId: context.metadata.amistioProjectId,
10298
10692
  repositoryLinkId: context.metadata.repositoryLinkId,
10299
10693
  runnerId,
10300
- rootDir: path18.resolve(options.root),
10694
+ rootDir: path19.resolve(options.root),
10301
10695
  apiUrl: options.apiUrl,
10302
10696
  args: buildBackgroundRunnerArgs(resolvedOptions)
10303
10697
  });
@@ -10489,7 +10883,7 @@ runner.command("stop").description("Stop a background runner for the paired repo
10489
10883
  console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
10490
10884
  });
10491
10885
  var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
10492
- 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("--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, 1).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) => {
10886
+ 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, 1).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) => {
10493
10887
  const context = await loadPairedApiContext(options.root, options.apiUrl);
10494
10888
  if (!context) {
10495
10889
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -10518,7 +10912,7 @@ runnerService.command("install").description("Install a user-level startup servi
10518
10912
  projectId: context.metadata.amistioProjectId,
10519
10913
  repositoryLinkId: context.metadata.repositoryLinkId,
10520
10914
  runnerId,
10521
- rootDir: path18.resolve(options.root),
10915
+ rootDir: path19.resolve(options.root),
10522
10916
  apiUrl: options.apiUrl,
10523
10917
  args,
10524
10918
  platform
@@ -10607,6 +11001,8 @@ async function runWatchIteration({ command, context, options, runnerId }) {
10607
11001
  ...command.getOptionValueSource("modelVariant") === "cli" && options.modelVariant ? { explicitModelVariant: options.modelVariant } : {},
10608
11002
  ...command.getOptionValueSource("reasoningEffort") === "cli" && options.reasoningEffort ? { explicitReasoningEffort: options.reasoningEffort } : {},
10609
11003
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
11004
+ executionProfile: options.executionProfile,
11005
+ ...options.setupPackageManagerInstall ? { setupPackageManagerInstall: options.setupPackageManagerInstall } : {},
10610
11006
  dryRun: Boolean(options.dryRun),
10611
11007
  stream: options.stream,
10612
11008
  commandContext: {
@@ -10739,6 +11135,8 @@ async function runNextWorkItem({
10739
11135
  explicitModelVariant,
10740
11136
  explicitProviderId,
10741
11137
  explicitReasoningEffort,
11138
+ executionProfile,
11139
+ setupPackageManagerInstall,
10742
11140
  explicitInvocationChannel,
10743
11141
  explicitTool,
10744
11142
  maxPreflightAttempts,
@@ -10811,9 +11209,52 @@ async function runNextWorkItem({
10811
11209
  }
10812
11210
  const executionRoot = worktreeIsolation.isolation?.worktreePath ?? root;
10813
11211
  const isolationTelemetry = workItemIsolationTelemetry(result.workItem, worktreeIsolation.isolation);
11212
+ const environmentReadiness = await checkRunnerExecutionEnvironment({
11213
+ executionRoot,
11214
+ primaryCheckoutRoot: root,
11215
+ profile: executionProfile,
11216
+ setupCommands: runnerEnvironmentSetupCommands(setupPackageManagerInstall),
11217
+ ...result.workItem.workKind ? { workKind: result.workItem.workKind } : {},
11218
+ env: process.env
11219
+ });
11220
+ if (environmentReadiness.profile === "dockerWorkspace" && environmentReadiness.status === "ready") {
11221
+ environmentReadiness.status = "blocked";
11222
+ environmentReadiness.summary = "Runner execution environment dockerWorkspace is blocked: containerized harness execution is not enabled in this runner build.";
11223
+ environmentReadiness.blockers.push({ reason: "unsupportedProfile", summary: "Containerized harness execution is not enabled in this runner build.", remediation: "Select hostWorktree or hostWorktreeWithSetup until Docker workspace execution is enabled.", safePaths: [] });
11224
+ environmentReadiness.checks.push({ checkId: "docker-harness", title: "Docker harness", status: "blocked", reason: "unsupportedProfile", summary: "Docker workspace envelope is valid, but this runner cannot execute the harness inside the container yet.", safePaths: [] });
11225
+ }
11226
+ if (environmentReadiness.status === "blocked") {
11227
+ const statusResult2 = await apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "blocked", `environment_blocked_${result.workItem.workItemId}_${result.workItem.attempt}_${randomUUID3()}`, runnerId, {
11228
+ ...isolationTelemetry,
11229
+ executionEnvironmentProfile: environmentReadiness.profile,
11230
+ executionEnvironmentReadiness: environmentReadiness,
11231
+ blockerReason: environmentReadiness.summary,
11232
+ message: environmentReadiness.summary
11233
+ });
11234
+ await recordRunnerMilestone(apiClient, projectId, statusResult2.workItem, runnerId, repositoryLinkId, {
11235
+ status: "warning",
11236
+ summary: environmentReadiness.summary,
11237
+ idempotencyKey: `runner_milestone_environment_blocked_${result.workItem.workItemId}_${statusResult2.workItem.idempotencyKey}`,
11238
+ metadata: { executionEnvironmentProfile: environmentReadiness.profile, blockerReasons: environmentReadiness.blockers.map((blocker) => blocker.reason) }
11239
+ });
11240
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", {
11241
+ ...runnerHeartbeatMetadata(toolConfig, currentRunnerMode(), heartbeatConcurrency),
11242
+ currentWorkItemId: result.workItem.workItemId,
11243
+ currentExecutionEnvironmentProfile: environmentReadiness.profile,
11244
+ environmentReadiness,
11245
+ preferenceMessage: environmentReadiness.summary,
11246
+ ...isolationTelemetry.implementationScopeId ? { currentImplementationScopeId: isolationTelemetry.implementationScopeId } : {},
11247
+ ...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
11248
+ ...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
11249
+ });
11250
+ console.error(environmentReadiness.summary);
11251
+ return { status: "blocked", exitCode: 1, message: environmentReadiness.summary };
11252
+ }
10814
11253
  await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", {
10815
11254
  ...runnerHeartbeatMetadata(toolConfig, currentRunnerMode(), heartbeatConcurrency),
10816
11255
  currentWorkItemId: result.workItem.workItemId,
11256
+ currentExecutionEnvironmentProfile: environmentReadiness.profile,
11257
+ environmentReadiness,
10817
11258
  ...isolationTelemetry.implementationScopeId ? { currentImplementationScopeId: isolationTelemetry.implementationScopeId } : {},
10818
11259
  ...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
10819
11260
  ...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
@@ -13302,8 +13743,17 @@ function parseReasoningEffort(value) {
13302
13743
  }
13303
13744
  throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
13304
13745
  }
13746
+ function parseRunnerExecutionEnvironmentProfile(value) {
13747
+ if (value === "hostWorktree" || value === "hostWorktreeWithSetup" || value === "dockerWorkspace") {
13748
+ return value;
13749
+ }
13750
+ throw new Error(`Expected execution profile hostWorktree, hostWorktreeWithSetup, or dockerWorkspace; received ${value}.`);
13751
+ }
13752
+ function runnerEnvironmentSetupCommands(setupPackageManagerInstall) {
13753
+ return setupPackageManagerInstall ? [{ id: "packageManager.install" }] : [];
13754
+ }
13305
13755
  function inferRepoName(root) {
13306
- return path18.basename(path18.resolve(root)) || "repository";
13756
+ return path19.basename(path19.resolve(root)) || "repository";
13307
13757
  }
13308
13758
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
13309
13759
  return createHash9("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
@@ -13570,6 +14020,7 @@ function runnerIsolationCapabilityMetadata(laneMetadata = {}) {
13570
14020
  supportedWorkKinds: runnerSupportedWorkKinds,
13571
14021
  supportsBranchIsolation: true,
13572
14022
  supportsGitWorktreeIsolation: true,
14023
+ supportedExecutionEnvironmentProfiles: [...supportedRunnerExecutionEnvironmentProfiles],
13573
14024
  ...laneMetadata.claimLaneId ? { claimLaneId: laneMetadata.claimLaneId } : {},
13574
14025
  ...laneMetadata.maxConcurrentWork !== void 0 ? { maxConcurrentWork: laneMetadata.maxConcurrentWork } : {}
13575
14026
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amistio/cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",