@amistio/cli 0.1.39 → 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.
- package/README.md +9 -2
- package/dist/index.js +481 -27
- 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
|
|
@@ -77,12 +82,14 @@ When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until
|
|
|
77
82
|
|
|
78
83
|
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. 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 at most one due hourly evaluation during normal watch/background polling when app evaluation is enabled for the repository link. 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. 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. 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 reviewable plan-backed findings in the app. 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 after the user approves an issue analysis, app evaluation finding, security remediation plan, or Test quality plan in the app.
|
|
79
84
|
|
|
80
|
-
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. 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. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; 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 to the
|
|
85
|
+
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. 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. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; 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. 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.
|
|
81
86
|
|
|
82
|
-
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;
|
|
87
|
+
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 all 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 or when the paired runner does not advertise the needed work kind. Completed implementation status is separate from proof: queue `implementationVerification` from Tasks when a plan needs source-aware evidence before cleanup or implementation status decisions.
|
|
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
|
|
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
|
|
2357
|
-
if (queuedWork && !
|
|
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(
|
|
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 (!
|
|
2371
|
-
return runnerWaitAction(
|
|
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
|
-
...
|
|
2384
|
-
...
|
|
2385
|
-
...
|
|
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(
|
|
2392
|
-
const repositoryName =
|
|
2393
|
-
if (
|
|
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
|
-
...
|
|
2401
|
-
...
|
|
2402
|
-
...
|
|
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 (
|
|
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
|
-
...
|
|
2413
|
-
...
|
|
2414
|
-
...
|
|
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
|
-
...
|
|
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
|
|
3149
|
-
return new URL(`${base}${
|
|
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:
|
|
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:
|
|
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 } : {}
|
|
@@ -11549,6 +11990,9 @@ async function executeImplementationHandoffRecoveryCommand(apiClient, command, c
|
|
|
11549
11990
|
if (workItem.repositoryLinkId && workItem.repositoryLinkId !== context.repositoryLinkId) {
|
|
11550
11991
|
return { succeeded: false, message: "Handoff recovery command is not scoped to this repository link." };
|
|
11551
11992
|
}
|
|
11993
|
+
if (workItem.claimedByRunnerId && workItem.claimedByRunnerId !== context.runnerId) {
|
|
11994
|
+
return { succeeded: false, message: `Handoff recovery command belongs to runner ${workItem.claimedByRunnerId}. Restart that runner or use Requeue fresh attempt.` };
|
|
11995
|
+
}
|
|
11552
11996
|
if (!workItem.implementationHandoff?.recovery?.availableActions.includes(command.handoffRecoveryAction)) {
|
|
11553
11997
|
return { succeeded: false, message: "Handoff recovery action is no longer available for this work item." };
|
|
11554
11998
|
}
|
|
@@ -13299,8 +13743,17 @@ function parseReasoningEffort(value) {
|
|
|
13299
13743
|
}
|
|
13300
13744
|
throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
|
|
13301
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
|
+
}
|
|
13302
13755
|
function inferRepoName(root) {
|
|
13303
|
-
return
|
|
13756
|
+
return path19.basename(path19.resolve(root)) || "repository";
|
|
13304
13757
|
}
|
|
13305
13758
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
13306
13759
|
return createHash9("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
@@ -13567,6 +14020,7 @@ function runnerIsolationCapabilityMetadata(laneMetadata = {}) {
|
|
|
13567
14020
|
supportedWorkKinds: runnerSupportedWorkKinds,
|
|
13568
14021
|
supportsBranchIsolation: true,
|
|
13569
14022
|
supportsGitWorktreeIsolation: true,
|
|
14023
|
+
supportedExecutionEnvironmentProfiles: [...supportedRunnerExecutionEnvironmentProfiles],
|
|
13570
14024
|
...laneMetadata.claimLaneId ? { claimLaneId: laneMetadata.claimLaneId } : {},
|
|
13571
14025
|
...laneMetadata.maxConcurrentWork !== void 0 ? { maxConcurrentWork: laneMetadata.maxConcurrentWork } : {}
|
|
13572
14026
|
};
|