@amistio/cli 0.1.36 → 0.1.37
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 +19 -1
- package/dist/host-helper.js +315 -0
- package/dist/index.js +1849 -320
- package/package.json +4 -2
- package/dist/index.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createHash as
|
|
5
|
-
import { writeFile as
|
|
4
|
+
import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
|
|
5
|
+
import { writeFile as writeFile11 } from "node:fs/promises";
|
|
6
6
|
import os8 from "node:os";
|
|
7
|
-
import
|
|
7
|
+
import path17 from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
// ../shared/src/schemas.ts
|
|
@@ -484,6 +484,33 @@ var runnerProviderConfigSchema = z.object({
|
|
|
484
484
|
models: z.record(runnerProviderModelIdSchema, runnerProviderModelSchema).default({})
|
|
485
485
|
}).strict();
|
|
486
486
|
var runnerProviderCatalogSchema = z.record(runnerProviderIdSchema, runnerProviderConfigSchema);
|
|
487
|
+
var runnerProviderClientIdSchema = z.string().trim().min(1).max(120);
|
|
488
|
+
var runnerProviderRouteTypeSchema = z.enum(["directProvider", "agentClient", "localRuntime"]);
|
|
489
|
+
var runnerProviderAuthStateSchema = z.enum(["notConfigured", "pendingUserAction", "authenticated", "expired", "revoked", "mismatch", "unavailable", "unsupported", "unknown"]);
|
|
490
|
+
var runnerProviderAuthStatusSchema = z.object({
|
|
491
|
+
providerId: runnerProviderIdSchema,
|
|
492
|
+
providerClientId: runnerProviderClientIdSchema,
|
|
493
|
+
routeType: runnerProviderRouteTypeSchema,
|
|
494
|
+
status: runnerProviderAuthStateSchema,
|
|
495
|
+
authMethodLabel: z.string().trim().min(1).max(120).optional(),
|
|
496
|
+
accountHost: z.string().trim().min(1).max(200).optional(),
|
|
497
|
+
accountLogin: z.string().trim().min(1).max(120).optional(),
|
|
498
|
+
accountFingerprint: z.string().trim().min(1).max(160).optional(),
|
|
499
|
+
modelCount: z.number().int().nonnegative().optional(),
|
|
500
|
+
checkedAt: isoDateTimeSchema,
|
|
501
|
+
expiresAt: isoDateTimeSchema.optional(),
|
|
502
|
+
message: z.string().max(400).optional(),
|
|
503
|
+
errorCode: z.string().trim().min(1).max(80).optional()
|
|
504
|
+
}).strict();
|
|
505
|
+
var runnerProviderAuthLinkRequestSchema = z.object({
|
|
506
|
+
providerId: runnerProviderIdSchema,
|
|
507
|
+
providerClientId: runnerProviderClientIdSchema,
|
|
508
|
+
routeType: runnerProviderRouteTypeSchema,
|
|
509
|
+
requestedAccountHost: z.string().trim().min(1).max(200).optional(),
|
|
510
|
+
requestedAccountLogin: z.string().trim().min(1).max(120).optional(),
|
|
511
|
+
requestedAccountFingerprint: z.string().trim().min(1).max(160).optional(),
|
|
512
|
+
intentExpiresAt: isoDateTimeSchema.optional()
|
|
513
|
+
}).strict();
|
|
487
514
|
var runnerToolModelPreferenceSchema = z.object({
|
|
488
515
|
tool: runnerToolSelectionSchema.optional(),
|
|
489
516
|
invocationChannel: runnerInvocationChannelSchema.optional(),
|
|
@@ -855,6 +882,7 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
855
882
|
autoSyncPushedCount: z.number().int().nonnegative().optional(),
|
|
856
883
|
autoSyncSkippedCount: z.number().int().nonnegative().optional(),
|
|
857
884
|
autoSyncConflictCount: z.number().int().nonnegative().optional(),
|
|
885
|
+
providerAuthStatuses: z.array(runnerProviderAuthStatusSchema).max(20).optional(),
|
|
858
886
|
lastSeenAt: isoDateTimeSchema
|
|
859
887
|
});
|
|
860
888
|
var runnerSettingsItemSchema = baseItemSchema.extend({
|
|
@@ -918,7 +946,7 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
|
918
946
|
lastUsedAt: isoDateTimeSchema.optional(),
|
|
919
947
|
status: z.enum(["active", "revoked"]).default("active")
|
|
920
948
|
});
|
|
921
|
-
var runnerCommandKindSchema = z.enum(["update", "restart", "remove", "implementationHandoffRecovery"]);
|
|
949
|
+
var runnerCommandKindSchema = z.enum(["update", "restart", "remove", "implementationHandoffRecovery", "providerAuthLinkRequested"]);
|
|
922
950
|
var runnerCommandStatusSchema = z.enum(["pending", "acknowledged", "running", "completed", "failed", "expired", "cancelled"]);
|
|
923
951
|
var runnerCommandItemSchema = baseItemSchema.extend({
|
|
924
952
|
type: z.literal("runnerCommand"),
|
|
@@ -930,6 +958,8 @@ var runnerCommandItemSchema = baseItemSchema.extend({
|
|
|
930
958
|
repositoryLinkId: z.string().min(1),
|
|
931
959
|
workItemId: z.string().min(1).optional(),
|
|
932
960
|
handoffRecoveryAction: implementationHandoffRecoveryActionSchema.optional(),
|
|
961
|
+
providerAuthLinkRequest: runnerProviderAuthLinkRequestSchema.optional(),
|
|
962
|
+
providerAuthStatus: runnerProviderAuthStatusSchema.optional(),
|
|
933
963
|
executionBranch: z.string().min(1).optional(),
|
|
934
964
|
executionWorktreeKey: z.string().min(1).optional(),
|
|
935
965
|
requestedByUserId: z.string().min(1),
|
|
@@ -3112,8 +3142,8 @@ var toolSessionMutationSchema = z3.object({
|
|
|
3112
3142
|
});
|
|
3113
3143
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
3114
3144
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
3115
|
-
const
|
|
3116
|
-
return new URL(`${base}${
|
|
3145
|
+
const path18 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
3146
|
+
return new URL(`${base}${path18}`);
|
|
3117
3147
|
}
|
|
3118
3148
|
|
|
3119
3149
|
// src/orchestrator.ts
|
|
@@ -3522,13 +3552,887 @@ function truncateLocalError(error) {
|
|
|
3522
3552
|
}
|
|
3523
3553
|
|
|
3524
3554
|
// src/local-tool-runner.ts
|
|
3525
|
-
import {
|
|
3526
|
-
import { mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
3555
|
+
import { mkdtemp, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
|
|
3527
3556
|
import os3 from "node:os";
|
|
3557
|
+
import path7 from "node:path";
|
|
3558
|
+
|
|
3559
|
+
// src/bounded-tool-adapter.ts
|
|
3560
|
+
import { constants } from "node:fs";
|
|
3561
|
+
import { access, mkdir as mkdir6, readdir as readdir3, readFile as readFile4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
|
|
3528
3562
|
import path6 from "node:path";
|
|
3563
|
+
|
|
3564
|
+
// src/host-execution.ts
|
|
3565
|
+
import { spawn } from "node:child_process";
|
|
3566
|
+
import { randomUUID } from "node:crypto";
|
|
3567
|
+
var hostExecutionProtocolVersion = "amistio.hostExecution.v1";
|
|
3568
|
+
var nativeHostHelperEnvironmentAllowlist = [
|
|
3569
|
+
"PATH",
|
|
3570
|
+
"Path",
|
|
3571
|
+
"HOME",
|
|
3572
|
+
"USERPROFILE",
|
|
3573
|
+
"TMPDIR",
|
|
3574
|
+
"TEMP",
|
|
3575
|
+
"TMP",
|
|
3576
|
+
"LANG",
|
|
3577
|
+
"LC_ALL",
|
|
3578
|
+
"CI",
|
|
3579
|
+
"NODE_ENV",
|
|
3580
|
+
"SystemRoot",
|
|
3581
|
+
"WINDIR",
|
|
3582
|
+
"ComSpec",
|
|
3583
|
+
"PATHEXT"
|
|
3584
|
+
];
|
|
3585
|
+
var defaultNativeHostOutputBudgetBytes = 64 * 1024;
|
|
3586
|
+
var nodeHostExecutionCapabilities = {
|
|
3587
|
+
protocolVersion: hostExecutionProtocolVersion,
|
|
3588
|
+
implementation: "node",
|
|
3589
|
+
processGroups: process.platform === "win32" ? { supported: false, reason: "Windows process-group cleanup needs a native helper or platform-specific implementation." } : { supported: true },
|
|
3590
|
+
signalEscalation: { supported: true },
|
|
3591
|
+
streamCapture: { supported: true },
|
|
3592
|
+
pty: { supported: false, reason: "PTY bridging is reserved for a future native helper." },
|
|
3593
|
+
sandbox: { supported: false, reason: "Sandboxed command execution is reserved for a future native helper." },
|
|
3594
|
+
outputEncoding: "utf8"
|
|
3595
|
+
};
|
|
3596
|
+
var defaultHostExecution = {
|
|
3597
|
+
capabilities: nodeHostExecutionCapabilities,
|
|
3598
|
+
executeCommand: executeNodeCommand,
|
|
3599
|
+
commandExists
|
|
3600
|
+
};
|
|
3601
|
+
var runtimeHostExecutionCacheKey;
|
|
3602
|
+
var runtimeHostExecutionPortPromise;
|
|
3603
|
+
function getRuntimeHostExecutionPort(env = process.env) {
|
|
3604
|
+
const helperConfig = nativeHostHelperConfigFromEnvironment(env);
|
|
3605
|
+
const cacheKey = helperConfig?.command ?? "";
|
|
3606
|
+
if (!runtimeHostExecutionPortPromise || runtimeHostExecutionCacheKey !== cacheKey) {
|
|
3607
|
+
runtimeHostExecutionCacheKey = cacheKey;
|
|
3608
|
+
runtimeHostExecutionPortPromise = createHostExecutionPort(helperConfig ? { nativeHelper: helperConfig } : {});
|
|
3609
|
+
}
|
|
3610
|
+
return runtimeHostExecutionPortPromise;
|
|
3611
|
+
}
|
|
3612
|
+
async function createHostExecutionPort(options = {}) {
|
|
3613
|
+
const fallback = options.fallback ?? defaultHostExecution;
|
|
3614
|
+
const helperConfig = options.nativeHelper ?? nativeHostHelperConfigFromEnvironment();
|
|
3615
|
+
if (!helperConfig) {
|
|
3616
|
+
return fallback;
|
|
3617
|
+
}
|
|
3618
|
+
const discovery = await discoverNativeHostHelper(helperConfig);
|
|
3619
|
+
return createCompositeHostExecutionPort(fallback, discovery);
|
|
3620
|
+
}
|
|
3621
|
+
function nativeHostHelperConfigFromEnvironment(env = process.env) {
|
|
3622
|
+
const command = env.AMISTIO_HOST_HELPER_PATH?.trim();
|
|
3623
|
+
if (!command) {
|
|
3624
|
+
return void 0;
|
|
3625
|
+
}
|
|
3626
|
+
return { command };
|
|
3627
|
+
}
|
|
3628
|
+
async function discoverNativeHostHelper(config) {
|
|
3629
|
+
const normalizedConfig = normalizeNativeHostHelperConfig(config);
|
|
3630
|
+
const result = await executeHelperProcess(normalizedConfig, "--amistio-host-helper-handshake", void 0, normalizedConfig.handshakeTimeoutMs);
|
|
3631
|
+
if (result.status === "spawnFailed") {
|
|
3632
|
+
return { ok: false, failure: { code: "helper_unavailable", message: result.error?.message ?? "Native host helper is unavailable.", stdout: result.stdout, stderr: result.stderr } };
|
|
3633
|
+
}
|
|
3634
|
+
if (result.status === "timedOut") {
|
|
3635
|
+
return { ok: false, failure: { code: "helper_unavailable", message: result.error?.message ?? "Native host helper handshake timed out.", stdout: result.stdout, stderr: result.stderr } };
|
|
3636
|
+
}
|
|
3637
|
+
if (result.exitCode !== 0) {
|
|
3638
|
+
return { ok: false, failure: { code: "helper_failed", message: `Native host helper handshake exited with code ${result.exitCode}.`, stdout: result.stdout, stderr: result.stderr } };
|
|
3639
|
+
}
|
|
3640
|
+
const parsed = parseJson(result.stdout);
|
|
3641
|
+
if (!isNativeHostHelperHandshake(parsed)) {
|
|
3642
|
+
return { ok: false, failure: { code: "protocol_mismatch", message: "Native host helper handshake did not match the Amistio host execution protocol.", stdout: result.stdout, stderr: result.stderr } };
|
|
3643
|
+
}
|
|
3644
|
+
return { ok: true, helper: { config: normalizedConfig, handshake: parsed } };
|
|
3645
|
+
}
|
|
3646
|
+
function createNativeHostCommandRequestEnvelope(request, requestId = randomUUID()) {
|
|
3647
|
+
const env = filterNativeHostHelperEnvironment(request.env ?? process.env);
|
|
3648
|
+
const sandbox = request.sandbox ?? "none";
|
|
3649
|
+
return {
|
|
3650
|
+
protocolVersion: hostExecutionProtocolVersion,
|
|
3651
|
+
requestId,
|
|
3652
|
+
command: request.command,
|
|
3653
|
+
args: [...request.args],
|
|
3654
|
+
cwd: request.cwd,
|
|
3655
|
+
shell: request.shell ?? false,
|
|
3656
|
+
timeoutMs: request.timeoutMs ?? 0,
|
|
3657
|
+
stdin: request.stdin ? "provided" : "none",
|
|
3658
|
+
env,
|
|
3659
|
+
envAllowlist: Object.keys(env).sort(),
|
|
3660
|
+
streamOutput: request.streamOutput ?? false,
|
|
3661
|
+
requirePty: request.requirePty ?? false,
|
|
3662
|
+
sandbox,
|
|
3663
|
+
sandboxPolicy: nativeHostSandboxPolicy(request.cwd, sandbox),
|
|
3664
|
+
outputBudgetBytes: positiveInteger(request.outputBudgetBytes) ?? defaultNativeHostOutputBudgetBytes
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
function createCompositeHostExecutionPort(fallback, discovery) {
|
|
3668
|
+
return {
|
|
3669
|
+
capabilities: discovery.ok ? discovery.helper.handshake.capabilities : fallback.capabilities,
|
|
3670
|
+
async executeCommand(request) {
|
|
3671
|
+
if (!discovery.ok) {
|
|
3672
|
+
if (request.requireNativeHelper) {
|
|
3673
|
+
return helperDiscoveryFailureResult(discovery.failure);
|
|
3674
|
+
}
|
|
3675
|
+
return fallback.executeCommand(request);
|
|
3676
|
+
}
|
|
3677
|
+
const helper = discovery.helper;
|
|
3678
|
+
const capabilityFailure = helperCapabilityFailure(request, helper.handshake.capabilities);
|
|
3679
|
+
if (capabilityFailure) {
|
|
3680
|
+
return capabilityFailure;
|
|
3681
|
+
}
|
|
3682
|
+
if (!canDelegateToNativeHelper(request)) {
|
|
3683
|
+
if (request.requireNativeHelper) {
|
|
3684
|
+
return unsupportedResult("helper_failed", "Native host helper delegation is unavailable for requests with stdin payloads.");
|
|
3685
|
+
}
|
|
3686
|
+
return fallback.executeCommand(request);
|
|
3687
|
+
}
|
|
3688
|
+
return executeNativeHelperCommand(helper, request);
|
|
3689
|
+
},
|
|
3690
|
+
commandExists: fallback.commandExists
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
function helperCapabilityFailure(request, capabilities) {
|
|
3694
|
+
if (request.requirePty && !capabilities.pty.supported) {
|
|
3695
|
+
return unsupportedResult("pty_unsupported", capabilities.pty.reason ?? "PTY command execution is not supported by the native host helper.");
|
|
3696
|
+
}
|
|
3697
|
+
if (request.sandbox && request.sandbox !== "none" && !capabilities.sandbox.supported) {
|
|
3698
|
+
return unsupportedResult("sandbox_unsupported", capabilities.sandbox.reason ?? `${request.sandbox} sandbox execution is not supported by the native host helper.`);
|
|
3699
|
+
}
|
|
3700
|
+
return void 0;
|
|
3701
|
+
}
|
|
3702
|
+
function canDelegateToNativeHelper(request) {
|
|
3703
|
+
return !request.stdin && !request.shell;
|
|
3704
|
+
}
|
|
3705
|
+
async function executeNativeHelperCommand(helper, request) {
|
|
3706
|
+
const requestId = randomUUID();
|
|
3707
|
+
const envelope = createNativeHostCommandRequestEnvelope(request, requestId);
|
|
3708
|
+
const result = await executeHelperProcess(helper.config, "--amistio-host-helper-execute", JSON.stringify(envelope), request.timeoutMs ?? helper.config.executeTimeoutMs);
|
|
3709
|
+
if (result.status === "spawnFailed") {
|
|
3710
|
+
return { status: "helperUnavailable", exitCode: 1, stdout: result.stdout, stderr: result.stderr, error: { code: "helper_unavailable", message: result.error?.message ?? "Native host helper is unavailable." } };
|
|
3711
|
+
}
|
|
3712
|
+
if (result.status === "timedOut") {
|
|
3713
|
+
return result;
|
|
3714
|
+
}
|
|
3715
|
+
if (result.exitCode !== 0) {
|
|
3716
|
+
return { status: "failed", exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, error: { code: "helper_failed", message: `Native host helper exited with code ${result.exitCode}.` } };
|
|
3717
|
+
}
|
|
3718
|
+
const parsed = parseJson(result.stdout);
|
|
3719
|
+
if (!isNativeHostCommandResultEnvelope(parsed) || parsed.requestId !== requestId) {
|
|
3720
|
+
return { status: "protocolMismatch", exitCode: 1, stdout: result.stdout, stderr: result.stderr, error: { code: "protocol_mismatch", message: "Native host helper command result did not match the Amistio host execution protocol." } };
|
|
3721
|
+
}
|
|
3722
|
+
return normalizeHostCommandResult(request, {
|
|
3723
|
+
status: parsed.status,
|
|
3724
|
+
exitCode: parsed.exitCode,
|
|
3725
|
+
stdout: parsed.stdout,
|
|
3726
|
+
stderr: parsed.stderr,
|
|
3727
|
+
...parsed.error ? { error: parsed.error } : {}
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3730
|
+
async function executeNodeCommand(request) {
|
|
3731
|
+
if (request.requirePty) {
|
|
3732
|
+
return unsupportedResult("pty_unsupported", "PTY command execution is not supported by the default Node host execution port.");
|
|
3733
|
+
}
|
|
3734
|
+
if (request.sandbox && request.sandbox !== "none") {
|
|
3735
|
+
return unsupportedResult("sandbox_unsupported", `${request.sandbox} sandbox execution is not supported by the default Node host execution port.`);
|
|
3736
|
+
}
|
|
3737
|
+
return new Promise((resolve) => {
|
|
3738
|
+
let child;
|
|
3739
|
+
try {
|
|
3740
|
+
child = spawn(request.command, [...request.args], {
|
|
3741
|
+
cwd: request.cwd,
|
|
3742
|
+
env: request.env ?? process.env,
|
|
3743
|
+
detached: process.platform !== "win32",
|
|
3744
|
+
shell: request.shell ?? false,
|
|
3745
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3746
|
+
});
|
|
3747
|
+
} catch (error) {
|
|
3748
|
+
resolve(spawnFailedResult(error));
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
let stdout = "";
|
|
3752
|
+
let stderr = "";
|
|
3753
|
+
let settled = false;
|
|
3754
|
+
let forceKillTimer;
|
|
3755
|
+
let timedOutError;
|
|
3756
|
+
const timeout = request.timeoutMs && request.timeoutMs > 0 ? setTimeout(() => {
|
|
3757
|
+
if (settled) return;
|
|
3758
|
+
const message = request.timeoutMessage ?? `Command timed out after ${request.timeoutMs}ms: ${request.command}`;
|
|
3759
|
+
stderr += `${message}
|
|
3760
|
+
`;
|
|
3761
|
+
timedOutError = { code: "timeout", message };
|
|
3762
|
+
killProcessTree(child, "SIGTERM");
|
|
3763
|
+
forceKillTimer = setTimeout(() => killProcessTree(child, "SIGKILL"), request.killGraceMs ?? 5e3);
|
|
3764
|
+
forceKillTimer.unref?.();
|
|
3765
|
+
}, request.timeoutMs) : void 0;
|
|
3766
|
+
timeout?.unref?.();
|
|
3767
|
+
const resolveOnce = (result) => {
|
|
3768
|
+
if (settled) return;
|
|
3769
|
+
settled = true;
|
|
3770
|
+
if (timeout) clearTimeout(timeout);
|
|
3771
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
3772
|
+
resolve(result);
|
|
3773
|
+
};
|
|
3774
|
+
child.on("error", (error) => {
|
|
3775
|
+
if (timedOutError) return;
|
|
3776
|
+
resolveOnce(spawnFailedResult(error, stdout, stderr));
|
|
3777
|
+
});
|
|
3778
|
+
child.stdout.setEncoding("utf8");
|
|
3779
|
+
child.stderr.setEncoding("utf8");
|
|
3780
|
+
child.stdout.on("data", (chunk) => {
|
|
3781
|
+
stdout += chunk;
|
|
3782
|
+
if (request.streamOutput) process.stdout.write(chunk);
|
|
3783
|
+
});
|
|
3784
|
+
child.stderr.on("data", (chunk) => {
|
|
3785
|
+
stderr += chunk;
|
|
3786
|
+
if (request.streamOutput) process.stderr.write(chunk);
|
|
3787
|
+
});
|
|
3788
|
+
child.stdin.on("error", () => void 0);
|
|
3789
|
+
if (request.stdin) {
|
|
3790
|
+
child.stdin.write(request.stdin);
|
|
3791
|
+
}
|
|
3792
|
+
child.stdin.end();
|
|
3793
|
+
child.on("close", (exitCode) => {
|
|
3794
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
3795
|
+
if (timedOutError) {
|
|
3796
|
+
resolveOnce({ status: "timedOut", exitCode: 1, stdout, stderr, error: timedOutError });
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
resolveOnce({ status: "completed", exitCode: exitCode ?? 1, stdout, stderr });
|
|
3800
|
+
});
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
function unsupportedResult(code, message) {
|
|
3804
|
+
return { status: "unsupported", exitCode: 1, stdout: "", stderr: "", error: { code, message } };
|
|
3805
|
+
}
|
|
3806
|
+
function helperDiscoveryFailureResult(failure) {
|
|
3807
|
+
const status = failure.code === "protocol_mismatch" ? "protocolMismatch" : failure.code === "helper_unavailable" ? "helperUnavailable" : "failed";
|
|
3808
|
+
return { status, exitCode: 1, stdout: failure.stdout ?? "", stderr: failure.stderr ?? "", error: { code: failure.code, message: failure.message } };
|
|
3809
|
+
}
|
|
3810
|
+
function spawnFailedResult(error, stdout = "", stderr = "") {
|
|
3811
|
+
return { status: "spawnFailed", exitCode: 1, stdout, stderr, error: { code: "spawn_failed", message: errorMessage(error) } };
|
|
3812
|
+
}
|
|
3813
|
+
function killProcessTree(child, signal) {
|
|
3814
|
+
if (child.pid && process.platform !== "win32") {
|
|
3815
|
+
try {
|
|
3816
|
+
process.kill(-child.pid, signal);
|
|
3817
|
+
return;
|
|
3818
|
+
} catch {
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
child.kill(signal);
|
|
3822
|
+
}
|
|
3823
|
+
async function commandExists(command) {
|
|
3824
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
|
3825
|
+
return new Promise((resolve) => {
|
|
3826
|
+
const lookup = spawn(lookupCommand, [command], { stdio: "ignore" });
|
|
3827
|
+
lookup.on("error", () => resolve(false));
|
|
3828
|
+
lookup.on("close", (exitCode) => resolve(exitCode === 0));
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
function normalizeNativeHostHelperConfig(config) {
|
|
3832
|
+
return {
|
|
3833
|
+
command: config.command,
|
|
3834
|
+
args: config.args ? [...config.args] : [],
|
|
3835
|
+
cwd: config.cwd ?? process.cwd(),
|
|
3836
|
+
env: filterNativeHostHelperEnvironment(config.env ?? process.env),
|
|
3837
|
+
handshakeTimeoutMs: positiveInteger(config.handshakeTimeoutMs) ?? 2e3,
|
|
3838
|
+
executeTimeoutMs: positiveInteger(config.executeTimeoutMs) ?? 3e4
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
function executeHelperProcess(config, mode, stdin, timeoutMs) {
|
|
3842
|
+
return new Promise((resolve) => {
|
|
3843
|
+
let child;
|
|
3844
|
+
try {
|
|
3845
|
+
child = spawn(config.command, [...config.args, mode], {
|
|
3846
|
+
cwd: config.cwd,
|
|
3847
|
+
env: config.env,
|
|
3848
|
+
shell: false,
|
|
3849
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3850
|
+
});
|
|
3851
|
+
} catch (error) {
|
|
3852
|
+
resolve(spawnFailedResult(error));
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
let stdout = "";
|
|
3856
|
+
let stderr = "";
|
|
3857
|
+
let settled = false;
|
|
3858
|
+
const timeout = timeoutMs > 0 ? setTimeout(() => {
|
|
3859
|
+
if (settled) return;
|
|
3860
|
+
const message = `Native host helper timed out after ${timeoutMs}ms.`;
|
|
3861
|
+
stderr += `${message}
|
|
3862
|
+
`;
|
|
3863
|
+
child.kill("SIGKILL");
|
|
3864
|
+
settled = true;
|
|
3865
|
+
resolve({ status: "timedOut", exitCode: 1, stdout, stderr, error: { code: "timeout", message } });
|
|
3866
|
+
}, timeoutMs) : void 0;
|
|
3867
|
+
timeout?.unref?.();
|
|
3868
|
+
const resolveOnce = (result) => {
|
|
3869
|
+
if (settled) return;
|
|
3870
|
+
settled = true;
|
|
3871
|
+
if (timeout) clearTimeout(timeout);
|
|
3872
|
+
resolve(result);
|
|
3873
|
+
};
|
|
3874
|
+
child.on("error", (error) => resolveOnce(spawnFailedResult(error, stdout, stderr)));
|
|
3875
|
+
child.stdout.setEncoding("utf8");
|
|
3876
|
+
child.stderr.setEncoding("utf8");
|
|
3877
|
+
child.stdout.on("data", (chunk) => {
|
|
3878
|
+
stdout += chunk;
|
|
3879
|
+
});
|
|
3880
|
+
child.stderr.on("data", (chunk) => {
|
|
3881
|
+
stderr += chunk;
|
|
3882
|
+
});
|
|
3883
|
+
child.stdin.on("error", () => void 0);
|
|
3884
|
+
if (stdin) {
|
|
3885
|
+
child.stdin.write(stdin);
|
|
3886
|
+
}
|
|
3887
|
+
child.stdin.end();
|
|
3888
|
+
child.on("close", (exitCode) => {
|
|
3889
|
+
resolveOnce({ status: "completed", exitCode: exitCode ?? 1, stdout, stderr });
|
|
3890
|
+
});
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
function filterNativeHostHelperEnvironment(env) {
|
|
3894
|
+
const allowedNames = new Set(nativeHostHelperEnvironmentAllowlist);
|
|
3895
|
+
const filtered = {};
|
|
3896
|
+
for (const [name, value] of Object.entries(env)) {
|
|
3897
|
+
if (!value || !allowedNames.has(name) || isSensitiveEnvironmentName(name)) {
|
|
3898
|
+
continue;
|
|
3899
|
+
}
|
|
3900
|
+
filtered[name] = value;
|
|
3901
|
+
}
|
|
3902
|
+
return filtered;
|
|
3903
|
+
}
|
|
3904
|
+
function nativeHostSandboxPolicy(cwd, mode) {
|
|
3905
|
+
return {
|
|
3906
|
+
mode,
|
|
3907
|
+
cwd,
|
|
3908
|
+
filesystem: mode === "filesystemReadOnly" ? "readOnly" : "default",
|
|
3909
|
+
network: mode === "networkDisabled" ? "disabled" : "default",
|
|
3910
|
+
envPolicy: "allowlist-only"
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
function normalizeHostCommandResult(request, result) {
|
|
3914
|
+
const outputBudgetBytes = positiveInteger(request.outputBudgetBytes) ?? defaultNativeHostOutputBudgetBytes;
|
|
3915
|
+
const normalize = request.requirePty ? normalizePtyTranscript : (value) => value;
|
|
3916
|
+
return {
|
|
3917
|
+
...result,
|
|
3918
|
+
stdout: capUtf8Text(normalize(result.stdout), outputBudgetBytes),
|
|
3919
|
+
stderr: capUtf8Text(normalize(result.stderr), outputBudgetBytes)
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
function normalizePtyTranscript(value) {
|
|
3923
|
+
return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
|
|
3924
|
+
}
|
|
3925
|
+
function capUtf8Text(value, budgetBytes) {
|
|
3926
|
+
if (Buffer.byteLength(value, "utf8") <= budgetBytes) {
|
|
3927
|
+
return value;
|
|
3928
|
+
}
|
|
3929
|
+
let capped = Buffer.from(value, "utf8").subarray(0, budgetBytes).toString("utf8");
|
|
3930
|
+
while (Buffer.byteLength(capped, "utf8") > budgetBytes && capped.length > 0) {
|
|
3931
|
+
capped = capped.slice(0, -1);
|
|
3932
|
+
}
|
|
3933
|
+
return capped;
|
|
3934
|
+
}
|
|
3935
|
+
function isSensitiveEnvironmentName(name) {
|
|
3936
|
+
return /(?:TOKEN|SECRET|PASSWORD|PASS|KEY|CREDENTIAL|AUTH|OAUTH|COOKIE|SESSION|PRIVATE|CERT|SSH)/i.test(name);
|
|
3937
|
+
}
|
|
3938
|
+
function isNativeHostHelperHandshake(value) {
|
|
3939
|
+
if (!isRecord(value) || value.protocolVersion !== hostExecutionProtocolVersion) return false;
|
|
3940
|
+
if (!isHostExecutionCapabilities(value.capabilities)) return false;
|
|
3941
|
+
if (!isRecord(value.secretBoundary)) return false;
|
|
3942
|
+
return value.secretBoundary.credentialPolicy === "never-send-credentials" && value.secretBoundary.environmentPolicy === "allowlist-only" && Array.isArray(value.secretBoundary.forbiddenInputs) && value.secretBoundary.forbiddenInputs.length > 0 && value.secretBoundary.forbiddenInputs.every((item) => typeof item === "string" && item.trim().length > 0);
|
|
3943
|
+
}
|
|
3944
|
+
function isHostExecutionCapabilities(value) {
|
|
3945
|
+
if (!isRecord(value)) return false;
|
|
3946
|
+
return value.protocolVersion === hostExecutionProtocolVersion && (value.implementation === "node" || value.implementation === "nativeHelper") && isCapabilitySupport(value.processGroups) && isCapabilitySupport(value.signalEscalation) && isCapabilitySupport(value.streamCapture) && isCapabilitySupport(value.pty) && isCapabilitySupport(value.sandbox) && value.outputEncoding === "utf8";
|
|
3947
|
+
}
|
|
3948
|
+
function isCapabilitySupport(value) {
|
|
3949
|
+
return isRecord(value) && typeof value.supported === "boolean" && (value.reason === void 0 || typeof value.reason === "string");
|
|
3950
|
+
}
|
|
3951
|
+
function isNativeHostCommandResultEnvelope(value) {
|
|
3952
|
+
if (!isRecord(value) || value.protocolVersion !== hostExecutionProtocolVersion) return false;
|
|
3953
|
+
return typeof value.requestId === "string" && isHostCommandStatus(value.status) && typeof value.exitCode === "number" && typeof value.stdout === "string" && typeof value.stderr === "string" && (value.error === void 0 || isHostCommandError(value.error));
|
|
3954
|
+
}
|
|
3955
|
+
function isHostCommandStatus(value) {
|
|
3956
|
+
return value === "completed" || value === "timedOut" || value === "spawnFailed" || value === "permissionDenied" || value === "helperUnavailable" || value === "protocolMismatch" || value === "killed" || value === "unsupported" || value === "failed";
|
|
3957
|
+
}
|
|
3958
|
+
function isHostCommandError(value) {
|
|
3959
|
+
return isRecord(value) && isHostCommandErrorCode(value.code) && typeof value.message === "string";
|
|
3960
|
+
}
|
|
3961
|
+
function isHostCommandErrorCode(value) {
|
|
3962
|
+
return value === "timeout" || value === "spawn_failed" || value === "permission_denied" || value === "helper_unavailable" || value === "protocol_mismatch" || value === "helper_failed" || value === "killed" || value === "pty_unsupported" || value === "sandbox_unsupported";
|
|
3963
|
+
}
|
|
3964
|
+
function isRecord(value) {
|
|
3965
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3966
|
+
}
|
|
3967
|
+
function parseJson(value) {
|
|
3968
|
+
try {
|
|
3969
|
+
return JSON.parse(value);
|
|
3970
|
+
} catch {
|
|
3971
|
+
return void 0;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
function positiveInteger(value) {
|
|
3975
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
3976
|
+
}
|
|
3977
|
+
function errorMessage(error) {
|
|
3978
|
+
return error instanceof Error ? error.message : String(error);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// src/bounded-tool-adapter.ts
|
|
3982
|
+
var boundedToolAdapterCatalog = [
|
|
3983
|
+
{ id: "filesystem.readFile", kind: "filesystem", displayName: "Read scoped file", implemented: true, mutating: false, concurrency: "parallelSafe", serializedResourceKinds: ["executionRoot"] },
|
|
3984
|
+
{ id: "filesystem.listFiles", kind: "filesystem", displayName: "List scoped files", implemented: true, mutating: false, concurrency: "parallelSafe", serializedResourceKinds: ["executionRoot"] },
|
|
3985
|
+
{ id: "filesystem.searchText", kind: "filesystem", displayName: "Search scoped text", implemented: true, mutating: false, concurrency: "parallelSafe", serializedResourceKinds: ["executionRoot"] },
|
|
3986
|
+
{ id: "filesystem.writeFile", kind: "filesystem", displayName: "Write scoped file", implemented: true, mutating: true, concurrency: "serialized", serializedResourceKinds: ["executionRoot"] },
|
|
3987
|
+
{ id: "shell.curatedCommand", kind: "shell", displayName: "Run runner-owned command preset", implemented: true, mutating: false, concurrency: "serialized", serializedResourceKinds: ["executionRoot", "process"] },
|
|
3988
|
+
{ id: "git.status", kind: "git", displayName: "Read Git status", implemented: true, mutating: false, concurrency: "parallelSafe", serializedResourceKinds: ["gitIndex"] },
|
|
3989
|
+
{ id: "git.diffNameOnly", kind: "git", displayName: "Read changed Git paths", implemented: true, mutating: false, concurrency: "parallelSafe", serializedResourceKinds: ["gitIndex"] },
|
|
3990
|
+
{ id: "packageManager.detect", kind: "packageManager", displayName: "Detect package manager", implemented: true, mutating: false, concurrency: "parallelSafe", serializedResourceKinds: ["packageManagerCache"] },
|
|
3991
|
+
{ id: "testRunner.profile", kind: "testRunner", displayName: "Run known verification profile", implemented: true, mutating: false, concurrency: "serialized", serializedResourceKinds: ["testRunner", "packageManagerCache"] },
|
|
3992
|
+
{ id: "browser.check", kind: "browser", displayName: "Run local browser smoke check", implemented: true, mutating: false, concurrency: "serialized", serializedResourceKinds: ["browserProfile"] },
|
|
3993
|
+
{ id: "modelClient.direct", kind: "modelClient", displayName: "Invoke direct model client", implemented: true, mutating: false, concurrency: "serialized", serializedResourceKinds: ["modelProvider"] },
|
|
3994
|
+
{ id: "agentClient.compatibilityBridge", kind: "agentClient", displayName: "Invoke optional local AI client", implemented: true, mutating: true, concurrency: "serialized", serializedResourceKinds: ["agentSession", "providerAuth", "executionRoot"] }
|
|
3995
|
+
];
|
|
3996
|
+
var testRunnerProfileIds = ["verify", "test", "typecheck", "lint", "build"];
|
|
3997
|
+
var packageLockfiles = {
|
|
3998
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
3999
|
+
npm: ["package-lock.json"],
|
|
4000
|
+
yarn: ["yarn.lock"],
|
|
4001
|
+
bun: ["bun.lockb", "bun.lock"]
|
|
4002
|
+
};
|
|
4003
|
+
var ignoredTraversalDirectories = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo", "coverage", ".cache"]);
|
|
4004
|
+
var defaultFilesystemListLimit = 200;
|
|
4005
|
+
var defaultFilesystemSearchLimit = 80;
|
|
4006
|
+
var filesystemSearchMaxFileBytes = 256e3;
|
|
4007
|
+
var defaultCuratedShellCommands = {
|
|
4008
|
+
"git.status.short": {
|
|
4009
|
+
command: "git",
|
|
4010
|
+
args: ["status", "--short"],
|
|
4011
|
+
displayName: "git status --short",
|
|
4012
|
+
mutationPolicy: "readOnly"
|
|
4013
|
+
},
|
|
4014
|
+
"git.diff.nameOnly": {
|
|
4015
|
+
command: "git",
|
|
4016
|
+
args: ["diff", "--name-only"],
|
|
4017
|
+
displayName: "git diff --name-only",
|
|
4018
|
+
mutationPolicy: "readOnly"
|
|
4019
|
+
},
|
|
4020
|
+
"package.detect": {
|
|
4021
|
+
command: process.execPath,
|
|
4022
|
+
args: ["-e", "const fs=require('fs');const files=['pnpm-lock.yaml','package-lock.json','yarn.lock','bun.lockb'];console.log(files.filter((file)=>fs.existsSync(file)).join('\\n'));"],
|
|
4023
|
+
displayName: "detect package manager lockfiles",
|
|
4024
|
+
mutationPolicy: "readOnly"
|
|
4025
|
+
}
|
|
4026
|
+
};
|
|
4027
|
+
function createBoundedToolAdapterPolicy(options) {
|
|
4028
|
+
const executionRoot = path6.resolve(options.executionRoot);
|
|
4029
|
+
return {
|
|
4030
|
+
executionRoot,
|
|
4031
|
+
mutationPolicy: options.mutationPolicy,
|
|
4032
|
+
timeoutMs: positiveInteger2(options.timeoutMs) ?? 3e4,
|
|
4033
|
+
outputBudgetBytes: positiveInteger2(options.outputBudgetBytes) ?? 64e3,
|
|
4034
|
+
allowedWorkKinds: options.allowedWorkKinds ?? ["assistantQuestion", "projectContextRefresh", "implementationVerification", "testQualityScan", "implementationTestGate", "implementation"],
|
|
4035
|
+
redaction: { localPaths: true, secretAssignments: true },
|
|
4036
|
+
concurrency: {
|
|
4037
|
+
mode: options.concurrencyMode ?? "serialized",
|
|
4038
|
+
resourceKey: options.resourceKey ?? executionRoot
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
4042
|
+
async function runFilesystemReadTool(options) {
|
|
4043
|
+
const resolved = resolveRelativePath(options.policy, options.relativePath);
|
|
4044
|
+
if (!resolved.ok) {
|
|
4045
|
+
return failedResult("filesystem.readFile", resolved.error, options.policy);
|
|
4046
|
+
}
|
|
4047
|
+
try {
|
|
4048
|
+
const content = await readFile4(resolved.path, "utf8");
|
|
4049
|
+
return completedResult("filesystem.readFile", content, "", options.policy);
|
|
4050
|
+
} catch (error) {
|
|
4051
|
+
return failedResult("filesystem.readFile", { code: "execution_failed", message: `File read failed: ${errorMessage2(error)}` }, options.policy);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
async function runFilesystemWriteTool(options) {
|
|
4055
|
+
const mutation = ensureMutationAllowed(options.policy, "mutating");
|
|
4056
|
+
if (mutation) {
|
|
4057
|
+
return failedResult("filesystem.writeFile", mutation, options.policy);
|
|
4058
|
+
}
|
|
4059
|
+
const resolved = resolveRelativePath(options.policy, options.relativePath);
|
|
4060
|
+
if (!resolved.ok) {
|
|
4061
|
+
return failedResult("filesystem.writeFile", resolved.error, options.policy);
|
|
4062
|
+
}
|
|
4063
|
+
try {
|
|
4064
|
+
await mkdir6(path6.dirname(resolved.path), { recursive: true });
|
|
4065
|
+
await writeFile5(resolved.path, options.content, "utf8");
|
|
4066
|
+
const summary = { relativePath: normalizeRelativePath(options.relativePath), bytes: Buffer.byteLength(options.content, "utf8") };
|
|
4067
|
+
return completedResult("filesystem.writeFile", JSON.stringify(summary, null, 2), "", options.policy);
|
|
4068
|
+
} catch (error) {
|
|
4069
|
+
return failedResult("filesystem.writeFile", { code: "execution_failed", message: `File write failed: ${errorMessage2(error)}` }, options.policy);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
async function runFilesystemListTool(options) {
|
|
4073
|
+
const relativeDirectory = normalizeRelativePath(options.relativeDirectory ?? ".");
|
|
4074
|
+
const resolved = resolveRelativePath(options.policy, relativeDirectory);
|
|
4075
|
+
if (!resolved.ok) {
|
|
4076
|
+
return failedResult("filesystem.listFiles", resolved.error, options.policy);
|
|
4077
|
+
}
|
|
4078
|
+
try {
|
|
4079
|
+
const maxFiles = positiveInteger2(options.maxFiles) ?? defaultFilesystemListLimit;
|
|
4080
|
+
const files = await listFiles(options.policy.executionRoot, resolved.path, maxFiles);
|
|
4081
|
+
const summary = { relativeDirectory, files: files.items, truncated: files.truncated };
|
|
4082
|
+
return completedResult("filesystem.listFiles", JSON.stringify(summary, null, 2), "", options.policy);
|
|
4083
|
+
} catch (error) {
|
|
4084
|
+
return failedResult("filesystem.listFiles", { code: "execution_failed", message: `File list failed: ${errorMessage2(error)}` }, options.policy);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
async function runFilesystemSearchTool(options) {
|
|
4088
|
+
const query = options.query.trim();
|
|
4089
|
+
if (!query) {
|
|
4090
|
+
return failedResult("filesystem.searchText", { code: "command_not_allowed", message: "Search query must not be empty." }, options.policy);
|
|
4091
|
+
}
|
|
4092
|
+
const relativeDirectory = normalizeRelativePath(options.relativeDirectory ?? ".");
|
|
4093
|
+
const resolved = resolveRelativePath(options.policy, relativeDirectory);
|
|
4094
|
+
if (!resolved.ok) {
|
|
4095
|
+
return failedResult("filesystem.searchText", resolved.error, options.policy);
|
|
4096
|
+
}
|
|
4097
|
+
try {
|
|
4098
|
+
const maxMatches = positiveInteger2(options.maxMatches) ?? defaultFilesystemSearchLimit;
|
|
4099
|
+
const matches = await searchText(options.policy, resolved.path, query, maxMatches);
|
|
4100
|
+
const summary = { query, matches: matches.items, truncated: matches.truncated };
|
|
4101
|
+
return completedResult("filesystem.searchText", JSON.stringify(summary, null, 2), "", options.policy);
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
return failedResult("filesystem.searchText", { code: "execution_failed", message: `Text search failed: ${errorMessage2(error)}` }, options.policy);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
async function runGitStatusTool(options) {
|
|
4107
|
+
return executeCuratedCommand(defaultCuratedShellCommands["git.status.short"], options.policy, "git.status");
|
|
4108
|
+
}
|
|
4109
|
+
async function runGitDiffNameOnlyTool(options) {
|
|
4110
|
+
return executeCuratedCommand(defaultCuratedShellCommands["git.diff.nameOnly"], options.policy, "git.diffNameOnly");
|
|
4111
|
+
}
|
|
4112
|
+
async function runPackageManagerDetectTool(options) {
|
|
4113
|
+
try {
|
|
4114
|
+
const detection = await detectPackageManager(options.policy.executionRoot);
|
|
4115
|
+
return completedResult("packageManager.detect", JSON.stringify(detection, null, 2), "", options.policy);
|
|
4116
|
+
} catch (error) {
|
|
4117
|
+
return failedResult("packageManager.detect", { code: "execution_failed", message: `Package manager detection failed: ${errorMessage2(error)}` }, options.policy);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
async function runTestRunnerProfileTool(options) {
|
|
4121
|
+
const profileId = options.profileId ?? "test";
|
|
4122
|
+
if (!testRunnerProfileIds.includes(profileId)) {
|
|
4123
|
+
return failedResult("testRunner.profile", { code: "command_not_allowed", message: "Verification profile is not allowed by this runner policy." }, options.policy);
|
|
4124
|
+
}
|
|
4125
|
+
let detection;
|
|
4126
|
+
try {
|
|
4127
|
+
detection = await detectPackageManager(options.policy.executionRoot);
|
|
4128
|
+
} catch (error) {
|
|
4129
|
+
return failedResult("testRunner.profile", { code: "execution_failed", message: `Verification profile discovery failed: ${errorMessage2(error)}` }, options.policy);
|
|
4130
|
+
}
|
|
4131
|
+
if (!detection.profiles.includes(profileId)) {
|
|
4132
|
+
return failedResult("testRunner.profile", { code: "command_not_allowed", message: `Verification profile is not defined in local package scripts: ${profileId}.` }, options.policy);
|
|
4133
|
+
}
|
|
4134
|
+
const command = options.commandCatalog?.[profileId] ?? testRunnerProfileCommand(options.packageManager ?? detection.packageManager ?? "npm", profileId);
|
|
4135
|
+
return executeCuratedCommand(command, options.policy, "testRunner.profile");
|
|
4136
|
+
}
|
|
4137
|
+
async function runBrowserCheckTool(options) {
|
|
4138
|
+
const target = parseBrowserCheckTarget(options.url, options.allowedHosts);
|
|
4139
|
+
if (!target.ok) {
|
|
4140
|
+
return failedResult("browser.check", target.error, options.policy);
|
|
4141
|
+
}
|
|
4142
|
+
const abortController = new AbortController();
|
|
4143
|
+
const timeout = setTimeout(() => abortController.abort(), options.policy.timeoutMs);
|
|
4144
|
+
timeout.unref?.();
|
|
4145
|
+
try {
|
|
4146
|
+
const response = await (options.fetchImpl ?? fetch)(target.url, { method: "GET", redirect: "manual", signal: abortController.signal });
|
|
4147
|
+
const body = await response.text();
|
|
4148
|
+
const summary = {
|
|
4149
|
+
url: target.url.toString(),
|
|
4150
|
+
status: response.status,
|
|
4151
|
+
ok: response.ok,
|
|
4152
|
+
...response.headers.get("content-type") ? { contentType: response.headers.get("content-type") } : {},
|
|
4153
|
+
...extractHtmlTitle(body) ? { title: extractHtmlTitle(body) } : {},
|
|
4154
|
+
bytes: Buffer.byteLength(body, "utf8")
|
|
4155
|
+
};
|
|
4156
|
+
const stdout = JSON.stringify(summary, null, 2);
|
|
4157
|
+
if (response.ok) {
|
|
4158
|
+
return completedResult("browser.check", stdout, "", options.policy);
|
|
4159
|
+
}
|
|
4160
|
+
return failedResult("browser.check", { code: "execution_failed", message: `Browser check failed with HTTP ${response.status}.` }, options.policy, stdout);
|
|
4161
|
+
} catch (error) {
|
|
4162
|
+
if (abortController.signal.aborted) {
|
|
4163
|
+
return failedResult("browser.check", { code: "timeout", message: `Browser check timed out after ${formatTimeoutDuration(options.policy.timeoutMs)}.` }, options.policy);
|
|
4164
|
+
}
|
|
4165
|
+
return failedResult("browser.check", { code: "execution_failed", message: `Browser check failed: ${errorMessage2(error)}` }, options.policy);
|
|
4166
|
+
} finally {
|
|
4167
|
+
clearTimeout(timeout);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
async function executeCuratedCommand(command, policy, adapterId) {
|
|
4171
|
+
const hostExecution = await getRuntimeHostExecutionPort();
|
|
4172
|
+
const result = await hostExecution.executeCommand({
|
|
4173
|
+
command: command.command,
|
|
4174
|
+
args: command.args,
|
|
4175
|
+
cwd: policy.executionRoot,
|
|
4176
|
+
env: process.env,
|
|
4177
|
+
shell: false,
|
|
4178
|
+
timeoutMs: policy.timeoutMs,
|
|
4179
|
+
timeoutMessage: `Tool adapter timed out after ${formatTimeoutDuration(policy.timeoutMs)}: ${command.displayName}`
|
|
4180
|
+
});
|
|
4181
|
+
if (result.status === "timedOut") {
|
|
4182
|
+
return failedResult(adapterId, { code: "timeout", message: result.error?.message ?? `Tool adapter timed out after ${formatTimeoutDuration(policy.timeoutMs)}: ${command.displayName}` }, policy, result.stdout, result.stderr);
|
|
4183
|
+
}
|
|
4184
|
+
if (result.status === "spawnFailed" || result.status === "unsupported") {
|
|
4185
|
+
return failedResult(adapterId, { code: "execution_failed", message: result.error?.message ?? `Command execution failed: ${command.displayName}` }, policy, result.stdout, result.stderr);
|
|
4186
|
+
}
|
|
4187
|
+
if (result.exitCode === 0) {
|
|
4188
|
+
return completedResult(adapterId, result.stdout, result.stderr, policy);
|
|
4189
|
+
}
|
|
4190
|
+
return failedResult(adapterId, { code: "execution_failed", message: `${command.displayName} exited with code ${result.exitCode}.` }, policy, result.stdout, result.stderr);
|
|
4191
|
+
}
|
|
4192
|
+
async function detectPackageManager(executionRoot) {
|
|
4193
|
+
const packageJson = await readPackageJson(executionRoot);
|
|
4194
|
+
const lockfiles = await detectPackageLockfiles(executionRoot);
|
|
4195
|
+
const scripts = Object.keys(packageJson?.scripts ?? {}).sort();
|
|
4196
|
+
const packageManager = packageManagerFromPackageManagerField(packageJson?.packageManager) ?? packageManagerFromLockfiles(lockfiles);
|
|
4197
|
+
const profiles = testRunnerProfileIds.filter((profileId) => scripts.includes(profileId));
|
|
4198
|
+
return {
|
|
4199
|
+
...packageManager ? { packageManager } : {},
|
|
4200
|
+
lockfiles,
|
|
4201
|
+
scripts,
|
|
4202
|
+
profiles
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
async function listFiles(executionRoot, directoryPath, maxFiles) {
|
|
4206
|
+
const items = [];
|
|
4207
|
+
let truncated = false;
|
|
4208
|
+
const visit = async (currentDirectory) => {
|
|
4209
|
+
if (truncated) return;
|
|
4210
|
+
const entries = await readdir3(currentDirectory, { withFileTypes: true });
|
|
4211
|
+
for (const entry of entries.sort((first, second) => first.name.localeCompare(second.name))) {
|
|
4212
|
+
if (truncated) return;
|
|
4213
|
+
if (entry.name.startsWith(".") && ignoredTraversalDirectories.has(entry.name)) continue;
|
|
4214
|
+
if (ignoredTraversalDirectories.has(entry.name)) continue;
|
|
4215
|
+
const absolutePath = path6.join(currentDirectory, entry.name);
|
|
4216
|
+
if (entry.isDirectory()) {
|
|
4217
|
+
await visit(absolutePath);
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
if (!entry.isFile()) continue;
|
|
4221
|
+
items.push(normalizeRelativePath(path6.relative(executionRoot, absolutePath)));
|
|
4222
|
+
if (items.length >= maxFiles) {
|
|
4223
|
+
truncated = true;
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
};
|
|
4228
|
+
await visit(directoryPath);
|
|
4229
|
+
return { items, truncated };
|
|
4230
|
+
}
|
|
4231
|
+
async function searchText(policy, directoryPath, query, maxMatches) {
|
|
4232
|
+
const listed = await listFiles(policy.executionRoot, directoryPath, Math.max(maxMatches * 20, maxMatches));
|
|
4233
|
+
const lowerQuery = query.toLowerCase();
|
|
4234
|
+
const matches = [];
|
|
4235
|
+
let truncated = listed.truncated;
|
|
4236
|
+
for (const relativePath of listed.items) {
|
|
4237
|
+
if (matches.length >= maxMatches) {
|
|
4238
|
+
truncated = true;
|
|
4239
|
+
break;
|
|
4240
|
+
}
|
|
4241
|
+
const absolutePath = path6.join(policy.executionRoot, relativePath);
|
|
4242
|
+
const stats = await stat3(absolutePath);
|
|
4243
|
+
if (!stats.isFile() || stats.size > filesystemSearchMaxFileBytes) continue;
|
|
4244
|
+
const content = await readFile4(absolutePath, "utf8").catch(() => void 0);
|
|
4245
|
+
if (content === void 0 || content.includes("\0")) continue;
|
|
4246
|
+
const lines = content.split(/\r?\n/);
|
|
4247
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
4248
|
+
if (!line.toLowerCase().includes(lowerQuery)) continue;
|
|
4249
|
+
matches.push({ relativePath, line: lineIndex + 1, text: line.slice(0, 500) });
|
|
4250
|
+
if (matches.length >= maxMatches) {
|
|
4251
|
+
truncated = true;
|
|
4252
|
+
break;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
return { items: matches, truncated };
|
|
4257
|
+
}
|
|
4258
|
+
async function readPackageJson(executionRoot) {
|
|
4259
|
+
const packageJsonPath = path6.join(executionRoot, "package.json");
|
|
4260
|
+
if (!await fileExists(packageJsonPath)) {
|
|
4261
|
+
return void 0;
|
|
4262
|
+
}
|
|
4263
|
+
const parsed = JSON.parse(await readFile4(packageJsonPath, "utf8"));
|
|
4264
|
+
if (!isRecord2(parsed)) {
|
|
4265
|
+
return void 0;
|
|
4266
|
+
}
|
|
4267
|
+
const scripts = isRecord2(parsed.scripts) ? Object.fromEntries(Object.entries(parsed.scripts).filter((entry) => typeof entry[1] === "string")) : void 0;
|
|
4268
|
+
return {
|
|
4269
|
+
...typeof parsed.packageManager === "string" ? { packageManager: parsed.packageManager } : {},
|
|
4270
|
+
...scripts ? { scripts } : {}
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
async function detectPackageLockfiles(executionRoot) {
|
|
4274
|
+
const lockfileCandidates = Object.values(packageLockfiles).flat();
|
|
4275
|
+
const presentLockfiles = [];
|
|
4276
|
+
for (const lockfile of lockfileCandidates) {
|
|
4277
|
+
if (await fileExists(path6.join(executionRoot, lockfile))) {
|
|
4278
|
+
presentLockfiles.push(lockfile);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
return presentLockfiles;
|
|
4282
|
+
}
|
|
4283
|
+
function packageManagerFromPackageManagerField(value) {
|
|
4284
|
+
const packageManagerName = value?.split("@")[0]?.trim();
|
|
4285
|
+
return isPackageManagerName(packageManagerName) ? packageManagerName : void 0;
|
|
4286
|
+
}
|
|
4287
|
+
function packageManagerFromLockfiles(lockfiles) {
|
|
4288
|
+
for (const packageManager of ["pnpm", "npm", "yarn", "bun"]) {
|
|
4289
|
+
if (packageLockfiles[packageManager].some((lockfile) => lockfiles.includes(lockfile))) {
|
|
4290
|
+
return packageManager;
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return void 0;
|
|
4294
|
+
}
|
|
4295
|
+
function testRunnerProfileCommand(packageManager, profileId) {
|
|
4296
|
+
if (packageManager === "pnpm") {
|
|
4297
|
+
return { command: "corepack", args: ["pnpm", "--config.verify-deps-before-run=false", "run", profileId], displayName: `pnpm run ${profileId}`, mutationPolicy: "readOnly" };
|
|
4298
|
+
}
|
|
4299
|
+
if (packageManager === "yarn") {
|
|
4300
|
+
return { command: "corepack", args: ["yarn", "run", profileId], displayName: `yarn run ${profileId}`, mutationPolicy: "readOnly" };
|
|
4301
|
+
}
|
|
4302
|
+
if (packageManager === "bun") {
|
|
4303
|
+
return { command: "bun", args: ["run", profileId], displayName: `bun run ${profileId}`, mutationPolicy: "readOnly" };
|
|
4304
|
+
}
|
|
4305
|
+
return { command: "npm", args: ["run", profileId], displayName: `npm run ${profileId}`, mutationPolicy: "readOnly" };
|
|
4306
|
+
}
|
|
4307
|
+
function parseBrowserCheckTarget(value, allowedHosts) {
|
|
4308
|
+
let url;
|
|
4309
|
+
try {
|
|
4310
|
+
url = new URL(value);
|
|
4311
|
+
} catch {
|
|
4312
|
+
return { ok: false, error: { code: "command_not_allowed", message: "Browser check target URL is invalid." } };
|
|
4313
|
+
}
|
|
4314
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
4315
|
+
return { ok: false, error: { code: "command_not_allowed", message: "Browser check target must use http or https." } };
|
|
4316
|
+
}
|
|
4317
|
+
if (url.username || url.password) {
|
|
4318
|
+
return { ok: false, error: { code: "command_not_allowed", message: "Browser check target must not include credentials." } };
|
|
4319
|
+
}
|
|
4320
|
+
const allowedHostSet = new Set((allowedHosts ?? []).map((host2) => host2.toLowerCase()));
|
|
4321
|
+
const hostname = url.hostname.toLowerCase();
|
|
4322
|
+
const host = url.host.toLowerCase();
|
|
4323
|
+
if (!isLoopbackHost(hostname) && !allowedHostSet.has(hostname) && !allowedHostSet.has(host)) {
|
|
4324
|
+
return { ok: false, error: { code: "command_not_allowed", message: "Browser check target must be loopback or explicitly allowed by the runner policy." } };
|
|
4325
|
+
}
|
|
4326
|
+
return { ok: true, url };
|
|
4327
|
+
}
|
|
4328
|
+
function isLoopbackHost(hostname) {
|
|
4329
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname.startsWith("127.") || hostname === "::1" || hostname === "[::1]" || hostname === "0.0.0.0";
|
|
4330
|
+
}
|
|
4331
|
+
function extractHtmlTitle(body) {
|
|
4332
|
+
const match = /<title[^>]*>([^<]*)<\/title>/i.exec(body);
|
|
4333
|
+
const title = match?.[1]?.replace(/\s+/g, " ").trim();
|
|
4334
|
+
return title || void 0;
|
|
4335
|
+
}
|
|
4336
|
+
async function fileExists(filePath) {
|
|
4337
|
+
try {
|
|
4338
|
+
await access(filePath, constants.F_OK);
|
|
4339
|
+
return true;
|
|
4340
|
+
} catch {
|
|
4341
|
+
return false;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
function isPackageManagerName(value) {
|
|
4345
|
+
return value === "pnpm" || value === "npm" || value === "yarn" || value === "bun";
|
|
4346
|
+
}
|
|
4347
|
+
function isRecord2(value) {
|
|
4348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4349
|
+
}
|
|
4350
|
+
function resolveRelativePath(policy, relativePath) {
|
|
4351
|
+
if (path6.isAbsolute(relativePath)) {
|
|
4352
|
+
return { ok: false, error: { code: "out_of_root", message: "Tool adapter rejected absolute local path input." } };
|
|
4353
|
+
}
|
|
4354
|
+
const resolved = path6.resolve(policy.executionRoot, relativePath);
|
|
4355
|
+
if (!isInsideRoot(policy.executionRoot, resolved)) {
|
|
4356
|
+
return { ok: false, error: { code: "out_of_root", message: "Tool adapter path is outside the execution root." } };
|
|
4357
|
+
}
|
|
4358
|
+
return { ok: true, path: resolved };
|
|
4359
|
+
}
|
|
4360
|
+
function normalizeRelativePath(relativePath) {
|
|
4361
|
+
const normalized = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
4362
|
+
return normalized || ".";
|
|
4363
|
+
}
|
|
4364
|
+
function ensureMutationAllowed(policy, requiredPolicy) {
|
|
4365
|
+
if (requiredPolicy === "mutating" && policy.mutationPolicy !== "mutating") {
|
|
4366
|
+
return { code: "mutation_denied", message: "Tool adapter mutation is not allowed by the current harness policy." };
|
|
4367
|
+
}
|
|
4368
|
+
return void 0;
|
|
4369
|
+
}
|
|
4370
|
+
function completedResult(adapterId, stdout, stderr, policy) {
|
|
4371
|
+
const output = normalizeOutput(stdout, stderr, policy);
|
|
4372
|
+
return { adapterId, status: "completed", exitCode: 0, stdout: output.stdout, stderr: output.stderr, truncated: output.truncated };
|
|
4373
|
+
}
|
|
4374
|
+
function failedResult(adapterId, error, policy, stdout = "", stderr = "") {
|
|
4375
|
+
const output = normalizeOutput(stdout, stderr, policy);
|
|
4376
|
+
return { adapterId, status: "failed", exitCode: 1, stdout: output.stdout, stderr: output.stderr, truncated: output.truncated, error };
|
|
4377
|
+
}
|
|
4378
|
+
function normalizeOutput(stdout, stderr, policy) {
|
|
4379
|
+
const redactedStdout = redactToolAdapterText(stdout, policy);
|
|
4380
|
+
const redactedStderr = redactToolAdapterText(stderr, policy);
|
|
4381
|
+
const stdoutBudget = Math.floor(policy.outputBudgetBytes / 2);
|
|
4382
|
+
const stderrBudget = policy.outputBudgetBytes - stdoutBudget;
|
|
4383
|
+
const normalizedStdout = truncateToBudget(redactedStdout, stdoutBudget);
|
|
4384
|
+
const normalizedStderr = truncateToBudget(redactedStderr, stderrBudget);
|
|
4385
|
+
return {
|
|
4386
|
+
stdout: normalizedStdout.value,
|
|
4387
|
+
stderr: normalizedStderr.value,
|
|
4388
|
+
truncated: normalizedStdout.truncated || normalizedStderr.truncated
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
function redactToolAdapterText(value, policy) {
|
|
4392
|
+
let result = value;
|
|
4393
|
+
if (policy.redaction.localPaths) {
|
|
4394
|
+
result = result.replaceAll(policy.executionRoot, "<execution-root>");
|
|
4395
|
+
}
|
|
4396
|
+
if (policy.redaction.secretAssignments) {
|
|
4397
|
+
result = result.replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|ACCESS_KEY)[A-Z0-9_]*)=([^\s"'`]+)/gi, "$1=<redacted>");
|
|
4398
|
+
}
|
|
4399
|
+
return result;
|
|
4400
|
+
}
|
|
4401
|
+
function truncateToBudget(value, budgetBytes) {
|
|
4402
|
+
if (budgetBytes <= 0) {
|
|
4403
|
+
return { value: "", truncated: Boolean(value) };
|
|
4404
|
+
}
|
|
4405
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
4406
|
+
if (bytes <= budgetBytes) {
|
|
4407
|
+
return { value, truncated: false };
|
|
4408
|
+
}
|
|
4409
|
+
const suffix = "\n[truncated by Amistio tool adapter output budget]";
|
|
4410
|
+
const contentBudget = Math.max(0, budgetBytes - Buffer.byteLength(suffix, "utf8"));
|
|
4411
|
+
return { value: `${Buffer.from(value, "utf8").subarray(0, contentBudget).toString("utf8")}${suffix}`, truncated: true };
|
|
4412
|
+
}
|
|
4413
|
+
function isInsideRoot(root, candidatePath) {
|
|
4414
|
+
const relative = path6.relative(root, candidatePath);
|
|
4415
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
4416
|
+
}
|
|
4417
|
+
function positiveInteger2(value) {
|
|
4418
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
4419
|
+
}
|
|
4420
|
+
function errorMessage2(error) {
|
|
4421
|
+
return error instanceof Error ? error.message : String(error);
|
|
4422
|
+
}
|
|
4423
|
+
function formatTimeoutDuration(timeoutMs) {
|
|
4424
|
+
if (timeoutMs < 1e3) {
|
|
4425
|
+
return `${timeoutMs}ms`;
|
|
4426
|
+
}
|
|
4427
|
+
const seconds = timeoutMs / 1e3;
|
|
4428
|
+
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// src/local-tool-runner.ts
|
|
3529
4432
|
var localToolNames = runnerToolNames;
|
|
3530
4433
|
var allReasoningEfforts = ["auto", "low", "medium", "high", "xhigh"];
|
|
3531
4434
|
var highReasoningEfforts = ["auto", "high", "xhigh"];
|
|
4435
|
+
var localToolOutputBudgetBytes = 32 * 1024;
|
|
3532
4436
|
var builtInProviderCatalogs = {
|
|
3533
4437
|
opencode: {
|
|
3534
4438
|
anthropic: {
|
|
@@ -3714,7 +4618,7 @@ async function detectLocalTools() {
|
|
|
3714
4618
|
return Promise.all(
|
|
3715
4619
|
localToolAdapters.map(async (adapter) => {
|
|
3716
4620
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
3717
|
-
const commandAvailable = adapter.executable ? await
|
|
4621
|
+
const commandAvailable = adapter.executable ? await commandExists2(adapter.executable) : false;
|
|
3718
4622
|
const providerCatalog = await detectRunnerProviderCatalog(adapter);
|
|
3719
4623
|
return {
|
|
3720
4624
|
name: adapter.name,
|
|
@@ -3732,9 +4636,9 @@ async function detectLocalTools() {
|
|
|
3732
4636
|
);
|
|
3733
4637
|
}
|
|
3734
4638
|
async function runLocalTool(options) {
|
|
3735
|
-
const promptTempDir = await mkdtemp(
|
|
3736
|
-
const promptFilePath =
|
|
3737
|
-
await
|
|
4639
|
+
const promptTempDir = await mkdtemp(path7.join(os3.tmpdir(), "amistio-prompt-"));
|
|
4640
|
+
const promptFilePath = path7.join(promptTempDir, "prompt.md");
|
|
4641
|
+
await writeFile6(promptFilePath, options.prompt, "utf8");
|
|
3738
4642
|
const modelConfig = normalizeModelOptions(options);
|
|
3739
4643
|
try {
|
|
3740
4644
|
const runnerOptions = {
|
|
@@ -3749,14 +4653,14 @@ async function runLocalTool(options) {
|
|
|
3749
4653
|
runnerOptions.toolCommand = options.toolCommand;
|
|
3750
4654
|
}
|
|
3751
4655
|
const runner2 = await createToolRunner(runnerOptions);
|
|
3752
|
-
const result = await executeToolRunner(runner2, {
|
|
4656
|
+
const result = normalizeToolExecutionResult(await executeToolRunner(runner2, {
|
|
3753
4657
|
rootDir: options.rootDir,
|
|
3754
4658
|
prompt: options.prompt,
|
|
3755
4659
|
promptFilePath,
|
|
3756
4660
|
streamOutput: Boolean(options.streamOutput),
|
|
3757
4661
|
...modelConfig,
|
|
3758
4662
|
...options.session ? { session: options.session } : {}
|
|
3759
|
-
}, options.timeoutMs);
|
|
4663
|
+
}, options.timeoutMs), options.rootDir);
|
|
3760
4664
|
return {
|
|
3761
4665
|
toolName: runner2.toolName,
|
|
3762
4666
|
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
@@ -3770,7 +4674,7 @@ async function runLocalTool(options) {
|
|
|
3770
4674
|
}
|
|
3771
4675
|
}
|
|
3772
4676
|
async function createToolRunPreview(options) {
|
|
3773
|
-
const promptFilePath =
|
|
4677
|
+
const promptFilePath = path7.join(os3.tmpdir(), "amistio-generated-prompt.md");
|
|
3774
4678
|
const modelConfig = normalizeModelOptions(options);
|
|
3775
4679
|
const runnerOptions = {
|
|
3776
4680
|
rootDir: options.rootDir,
|
|
@@ -3831,7 +4735,7 @@ async function createToolRunner(options) {
|
|
|
3831
4735
|
if (requiresModelSelection && !options.model) {
|
|
3832
4736
|
throw new Error("Provider-backed model configuration requires --model or --provider with --model-id.");
|
|
3833
4737
|
}
|
|
3834
|
-
const adapter = tool === "auto" ? await selectFirstAvailableAdapter(
|
|
4738
|
+
const adapter = tool === "auto" ? await selectFirstAvailableAdapter(options, options.invocationChannel) : await selectRequestedAdapter(tool, options.invocationChannel, options);
|
|
3835
4739
|
if (requiresModelSelection && !adapter.supportsModelSelection) {
|
|
3836
4740
|
throw new Error(`Model selection is not supported by ${adapter.name}. Remove model configuration or choose a model-aware adapter.`);
|
|
3837
4741
|
}
|
|
@@ -3844,7 +4748,7 @@ async function createToolRunner(options) {
|
|
|
3844
4748
|
allowCommandFallback: options.invocationChannel === "auto"
|
|
3845
4749
|
};
|
|
3846
4750
|
}
|
|
3847
|
-
if (options.invocationChannel !== "sdk" && adapter.buildInvocation && adapter.executable && await
|
|
4751
|
+
if (options.invocationChannel !== "sdk" && adapter.buildInvocation && adapter.executable && await commandExists2(adapter.executable)) {
|
|
3848
4752
|
return {
|
|
3849
4753
|
toolName: adapter.name,
|
|
3850
4754
|
kind: "command",
|
|
@@ -3866,10 +4770,10 @@ async function executeToolRunner(runner2, input, timeoutMs) {
|
|
|
3866
4770
|
try {
|
|
3867
4771
|
return await withTimeout(runner2.adapter.runWithSdk(input), timeoutMs, runner2.displayCommand);
|
|
3868
4772
|
} catch (error) {
|
|
3869
|
-
if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await
|
|
4773
|
+
if (runner2.allowCommandFallback && runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists2(runner2.adapter.executable)) {
|
|
3870
4774
|
const fallback = runner2.adapter.buildInvocation(input);
|
|
3871
4775
|
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput, timeoutMs);
|
|
3872
|
-
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${
|
|
4776
|
+
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage3(error)}`;
|
|
3873
4777
|
return {
|
|
3874
4778
|
...result,
|
|
3875
4779
|
stderr: result.stderr ? `${sdkFailure}
|
|
@@ -3885,31 +4789,48 @@ function normalizeToolRequest(value) {
|
|
|
3885
4789
|
}
|
|
3886
4790
|
throw new Error(`Unsupported local tool: ${value}. Supported tools: auto, none, ${localToolNames.join(", ")}.`);
|
|
3887
4791
|
}
|
|
3888
|
-
async function selectFirstAvailableAdapter(
|
|
4792
|
+
async function selectFirstAvailableAdapter(modelOptions, invocationChannel = "auto") {
|
|
4793
|
+
const requiresModelSelection = hasModelSelection(modelOptions);
|
|
4794
|
+
let modelSelectionFailure;
|
|
3889
4795
|
for (const adapter of localToolAdapters) {
|
|
3890
4796
|
if (requiresModelSelection && !adapter.supportsModelSelection) {
|
|
3891
4797
|
continue;
|
|
3892
4798
|
}
|
|
4799
|
+
const validationError = requiresModelSelection ? modelSelectionValidationError(adapter, modelOptions) : void 0;
|
|
4800
|
+
if (validationError) {
|
|
4801
|
+
modelSelectionFailure ??= validationError;
|
|
4802
|
+
continue;
|
|
4803
|
+
}
|
|
3893
4804
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
3894
|
-
const commandAvailable = adapter.executable ? await
|
|
4805
|
+
const commandAvailable = adapter.executable ? await commandExists2(adapter.executable) : false;
|
|
3895
4806
|
if (supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
|
|
3896
4807
|
return adapter;
|
|
3897
4808
|
}
|
|
3898
4809
|
}
|
|
3899
4810
|
if (invocationChannel !== "auto") {
|
|
4811
|
+
if (modelSelectionFailure) {
|
|
4812
|
+
throw new Error(`No installed local AI tool supports the selected provider/model for ${invocationChannel} invocation. ${modelSelectionFailure}`);
|
|
4813
|
+
}
|
|
3900
4814
|
throw new Error(`No installed local AI tool supports ${invocationChannel} invocation${requiresModelSelection ? " with model selection" : ""}. Select Auto or install a compatible tool.`);
|
|
3901
4815
|
}
|
|
4816
|
+
if (modelSelectionFailure) {
|
|
4817
|
+
throw new Error(`No installed local AI tool supports the selected provider/model. ${modelSelectionFailure}`);
|
|
4818
|
+
}
|
|
3902
4819
|
throw new Error(
|
|
3903
4820
|
requiresModelSelection ? "No installed local AI tool supports model selection. Remove --model or choose a model-aware adapter." : `No supported local AI tool was found. Install one of ${localToolNames.join(", ")} or pass --tool-command "your-tool --prompt-file {promptFile}".`
|
|
3904
4821
|
);
|
|
3905
4822
|
}
|
|
3906
|
-
async function selectRequestedAdapter(tool, invocationChannel = "auto") {
|
|
4823
|
+
async function selectRequestedAdapter(tool, invocationChannel = "auto", modelOptions) {
|
|
3907
4824
|
const adapter = localToolAdapters.find((candidate) => candidate.name === tool);
|
|
3908
4825
|
if (!adapter) {
|
|
3909
4826
|
throw new Error(`Unsupported local tool: ${tool}.`);
|
|
3910
4827
|
}
|
|
4828
|
+
if (modelOptions) {
|
|
4829
|
+
const validationError = modelSelectionValidationError(adapter, modelOptions);
|
|
4830
|
+
if (validationError) throw new Error(validationError);
|
|
4831
|
+
}
|
|
3911
4832
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
3912
|
-
const commandAvailable = adapter.executable ? await
|
|
4833
|
+
const commandAvailable = adapter.executable ? await commandExists2(adapter.executable) : false;
|
|
3913
4834
|
if (!supportsRequestedInvocationChannel({ sdkAvailable, commandAvailable }, invocationChannel)) {
|
|
3914
4835
|
if (invocationChannel === "sdk") {
|
|
3915
4836
|
throw new Error(`The ${tool} SDK was not found. Select Auto or Command invocation, install it, or pass --tool-command locally.`);
|
|
@@ -3925,18 +4846,78 @@ async function selectRequestedAdapter(tool, invocationChannel = "auto") {
|
|
|
3925
4846
|
if (invocationChannel !== "sdk" && commandAvailable) {
|
|
3926
4847
|
return adapter;
|
|
3927
4848
|
}
|
|
3928
|
-
throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
|
|
4849
|
+
throw new Error(`The ${tool} SDK or executable was not found. Install it or pass --tool-command.`);
|
|
4850
|
+
}
|
|
4851
|
+
function supportsRequestedInvocationChannel(capability, invocationChannel) {
|
|
4852
|
+
if (invocationChannel === "auto") return capability.sdkAvailable || capability.commandAvailable;
|
|
4853
|
+
if (invocationChannel === "sdk") return capability.sdkAvailable;
|
|
4854
|
+
return capability.commandAvailable;
|
|
4855
|
+
}
|
|
4856
|
+
function hasModelSelection(options) {
|
|
4857
|
+
return Boolean(options.model || options.providerId || options.modelId || options.modelVariant || options.reasoningEffort && options.reasoningEffort !== "auto");
|
|
4858
|
+
}
|
|
4859
|
+
function modelSelectionValidationError(adapter, options) {
|
|
4860
|
+
if (!hasModelSelection(options)) return void 0;
|
|
4861
|
+
if (!adapter.supportsModelSelection) {
|
|
4862
|
+
return `Model selection is not supported by ${adapter.name}. Remove model configuration or choose a model-aware adapter.`;
|
|
4863
|
+
}
|
|
4864
|
+
if (!adapter.providerCatalog) return void 0;
|
|
4865
|
+
const selection = resolveModelSelectionParts(options);
|
|
4866
|
+
if (!selection.providerId && !selection.modelId) return void 0;
|
|
4867
|
+
if (selection.providerId) {
|
|
4868
|
+
const provider = adapter.providerCatalog[selection.providerId];
|
|
4869
|
+
if (!provider) return `Provider ${selection.providerId} is not available on ${adapter.name}.`;
|
|
4870
|
+
if (!selection.modelId) return void 0;
|
|
4871
|
+
const model = provider.models[selection.modelId];
|
|
4872
|
+
if (!model) return `Model ${selection.modelId} is not available for provider ${selection.providerId} on ${adapter.name}.`;
|
|
4873
|
+
return modelCapabilityValidationError(adapter, selection.providerId, model, options);
|
|
4874
|
+
}
|
|
4875
|
+
if (selection.modelId) {
|
|
4876
|
+
const match = findCatalogModel(adapter.providerCatalog, selection.modelId);
|
|
4877
|
+
if (!match) return `Model ${selection.modelId} is not available on ${adapter.name}.`;
|
|
4878
|
+
return modelCapabilityValidationError(adapter, match.providerId, match.model, options);
|
|
4879
|
+
}
|
|
4880
|
+
return void 0;
|
|
4881
|
+
}
|
|
4882
|
+
function resolveModelSelectionParts(options) {
|
|
4883
|
+
const providerId = options.providerId?.trim();
|
|
4884
|
+
const modelId = options.modelId?.trim();
|
|
4885
|
+
if (providerId || modelId) {
|
|
4886
|
+
return {
|
|
4887
|
+
...providerId ? { providerId } : {},
|
|
4888
|
+
...modelId ? { modelId } : {}
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
const model = options.model?.trim();
|
|
4892
|
+
if (!model) return {};
|
|
4893
|
+
const separatorIndex = model.indexOf("/");
|
|
4894
|
+
if (separatorIndex > 0 && separatorIndex < model.length - 1) {
|
|
4895
|
+
return { providerId: model.slice(0, separatorIndex), modelId: model.slice(separatorIndex + 1) };
|
|
4896
|
+
}
|
|
4897
|
+
return { modelId: model };
|
|
3929
4898
|
}
|
|
3930
|
-
function
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4899
|
+
function findCatalogModel(catalog, modelId) {
|
|
4900
|
+
for (const [providerId, provider] of Object.entries(catalog)) {
|
|
4901
|
+
const model = provider.models[modelId];
|
|
4902
|
+
if (model) return { providerId, model };
|
|
4903
|
+
}
|
|
4904
|
+
return void 0;
|
|
4905
|
+
}
|
|
4906
|
+
function modelCapabilityValidationError(adapter, providerId, model, options) {
|
|
4907
|
+
const modelId = options.modelId ?? model.id;
|
|
4908
|
+
if (options.modelVariant && model.variants && !model.variants[options.modelVariant]) {
|
|
4909
|
+
return `Model variant ${options.modelVariant} is not available for ${providerId}/${modelId} on ${adapter.name}.`;
|
|
4910
|
+
}
|
|
4911
|
+
if (options.reasoningEffort && options.reasoningEffort !== "auto" && model.supportedReasoningEfforts?.length && !model.supportedReasoningEfforts.includes(options.reasoningEffort)) {
|
|
4912
|
+
return `${options.reasoningEffort} reasoning effort is not available for ${providerId}/${modelId} on ${adapter.name}.`;
|
|
4913
|
+
}
|
|
4914
|
+
return void 0;
|
|
3934
4915
|
}
|
|
3935
4916
|
async function isSdkAvailable(adapter) {
|
|
3936
4917
|
if (!adapter.sdkPackageName || !adapter.runWithSdk) {
|
|
3937
4918
|
return false;
|
|
3938
4919
|
}
|
|
3939
|
-
if (adapter.sdkRequiresExecutable && adapter.executable && !await
|
|
4920
|
+
if (adapter.sdkRequiresExecutable && adapter.executable && !await commandExists2(adapter.executable)) {
|
|
3940
4921
|
return false;
|
|
3941
4922
|
}
|
|
3942
4923
|
return packageAvailable(adapter.sdkPackageName);
|
|
@@ -3949,13 +4930,9 @@ async function packageAvailable(packageName) {
|
|
|
3949
4930
|
return false;
|
|
3950
4931
|
}
|
|
3951
4932
|
}
|
|
3952
|
-
async function
|
|
3953
|
-
const
|
|
3954
|
-
return
|
|
3955
|
-
const lookup = spawn(lookupCommand, [command], { stdio: "ignore" });
|
|
3956
|
-
lookup.on("error", () => resolve(false));
|
|
3957
|
-
lookup.on("close", (exitCode) => resolve(exitCode === 0));
|
|
3958
|
-
});
|
|
4933
|
+
async function commandExists2(command) {
|
|
4934
|
+
const hostExecution = await getRuntimeHostExecutionPort();
|
|
4935
|
+
return hostExecution.commandExists(command);
|
|
3959
4936
|
}
|
|
3960
4937
|
async function detectRunnerProviderCatalog(adapter) {
|
|
3961
4938
|
const localOpencodeConfigCatalog = adapter.name === "opencode" ? await loadLocalOpencodeProviderConfigCatalog() : void 0;
|
|
@@ -3963,16 +4940,16 @@ async function detectRunnerProviderCatalog(adapter) {
|
|
|
3963
4940
|
}
|
|
3964
4941
|
function localOpencodeProviderConfigPaths() {
|
|
3965
4942
|
return [
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4943
|
+
path7.join(os3.homedir(), ".config", "opencode", "opencode.json"),
|
|
4944
|
+
path7.join(os3.homedir(), ".config", "opencode", "config.json"),
|
|
4945
|
+
path7.join(process.cwd(), "opencode.json")
|
|
3969
4946
|
];
|
|
3970
4947
|
}
|
|
3971
4948
|
async function loadLocalOpencodeProviderConfigCatalog(configPaths = localOpencodeProviderConfigPaths()) {
|
|
3972
4949
|
for (const configPath of configPaths) {
|
|
3973
4950
|
try {
|
|
3974
|
-
const parsed = JSON.parse(await
|
|
3975
|
-
const providerValue =
|
|
4951
|
+
const parsed = JSON.parse(await readFile5(configPath, "utf8"));
|
|
4952
|
+
const providerValue = isRecord3(parsed) ? parsed.provider : void 0;
|
|
3976
4953
|
const catalog = sanitizeProviderCatalog(providerValue);
|
|
3977
4954
|
if (catalog) return catalog;
|
|
3978
4955
|
} catch {
|
|
@@ -3985,8 +4962,8 @@ function mergeProviderCatalogs(...catalogs) {
|
|
|
3985
4962
|
for (const catalog of catalogs) {
|
|
3986
4963
|
if (!catalog) continue;
|
|
3987
4964
|
for (const [providerId, provider] of Object.entries(catalog)) {
|
|
3988
|
-
const existing =
|
|
3989
|
-
const existingModels = existing &&
|
|
4965
|
+
const existing = isRecord3(merged[providerId]) ? merged[providerId] : void 0;
|
|
4966
|
+
const existingModels = existing && isRecord3(existing.models) ? existing.models : {};
|
|
3990
4967
|
merged[providerId] = {
|
|
3991
4968
|
...existing,
|
|
3992
4969
|
...provider,
|
|
@@ -3999,7 +4976,7 @@ function mergeProviderCatalogs(...catalogs) {
|
|
|
3999
4976
|
return parsed.success && Object.keys(parsed.data).length ? parsed.data : void 0;
|
|
4000
4977
|
}
|
|
4001
4978
|
function sanitizeProviderCatalog(value) {
|
|
4002
|
-
if (!
|
|
4979
|
+
if (!isRecord3(value)) return void 0;
|
|
4003
4980
|
const catalog = {};
|
|
4004
4981
|
for (const [providerId, providerValue] of Object.entries(value)) {
|
|
4005
4982
|
const provider = sanitizeProviderConfig(providerId, providerValue);
|
|
@@ -4009,7 +4986,7 @@ function sanitizeProviderCatalog(value) {
|
|
|
4009
4986
|
return parsed.success && Object.keys(parsed.data).length ? parsed.data : void 0;
|
|
4010
4987
|
}
|
|
4011
4988
|
function sanitizeProviderConfig(providerId, value) {
|
|
4012
|
-
if (!
|
|
4989
|
+
if (!isRecord3(value)) return void 0;
|
|
4013
4990
|
const models = sanitizeProviderModels(value.models);
|
|
4014
4991
|
if (!Object.keys(models).length) return void 0;
|
|
4015
4992
|
return {
|
|
@@ -4020,7 +4997,7 @@ function sanitizeProviderConfig(providerId, value) {
|
|
|
4020
4997
|
};
|
|
4021
4998
|
}
|
|
4022
4999
|
function sanitizeProviderModels(value) {
|
|
4023
|
-
if (!
|
|
5000
|
+
if (!isRecord3(value)) return {};
|
|
4024
5001
|
const models = {};
|
|
4025
5002
|
for (const [modelId, modelValue] of Object.entries(value)) {
|
|
4026
5003
|
const model = sanitizeProviderModel(modelId, modelValue);
|
|
@@ -4029,7 +5006,7 @@ function sanitizeProviderModels(value) {
|
|
|
4029
5006
|
return models;
|
|
4030
5007
|
}
|
|
4031
5008
|
function sanitizeProviderModel(modelId, value) {
|
|
4032
|
-
if (!
|
|
5009
|
+
if (!isRecord3(value)) return void 0;
|
|
4033
5010
|
const reasoning = booleanValue(value.reasoning);
|
|
4034
5011
|
const releaseDate = stringValue(value.release_date) ?? stringValue(value.releaseDate);
|
|
4035
5012
|
const toolCall = booleanValue(value.tool_call) ?? booleanValue(value.toolCall);
|
|
@@ -4055,7 +5032,7 @@ function sanitizeProviderModel(modelId, value) {
|
|
|
4055
5032
|
return model;
|
|
4056
5033
|
}
|
|
4057
5034
|
function sanitizeLimit(value) {
|
|
4058
|
-
if (!
|
|
5035
|
+
if (!isRecord3(value)) return void 0;
|
|
4059
5036
|
const limit = {
|
|
4060
5037
|
...numberValue(value.context) !== void 0 ? { context: numberValue(value.context) } : {},
|
|
4061
5038
|
...numberValue(value.input) !== void 0 ? { input: numberValue(value.input) } : {},
|
|
@@ -4064,7 +5041,7 @@ function sanitizeLimit(value) {
|
|
|
4064
5041
|
return Object.keys(limit).length ? limit : void 0;
|
|
4065
5042
|
}
|
|
4066
5043
|
function sanitizeModalities(value) {
|
|
4067
|
-
if (!
|
|
5044
|
+
if (!isRecord3(value)) return void 0;
|
|
4068
5045
|
const modalities = {
|
|
4069
5046
|
...stringArrayValue(value.input) ? { input: stringArrayValue(value.input) } : {},
|
|
4070
5047
|
...stringArrayValue(value.output) ? { output: stringArrayValue(value.output) } : {}
|
|
@@ -4072,10 +5049,10 @@ function sanitizeModalities(value) {
|
|
|
4072
5049
|
return Object.keys(modalities).length ? modalities : void 0;
|
|
4073
5050
|
}
|
|
4074
5051
|
function sanitizeVariants(value) {
|
|
4075
|
-
if (!
|
|
5052
|
+
if (!isRecord3(value)) return void 0;
|
|
4076
5053
|
const variants = {};
|
|
4077
5054
|
for (const [variantId, variantValue] of Object.entries(value)) {
|
|
4078
|
-
variants[variantId] =
|
|
5055
|
+
variants[variantId] = isRecord3(variantValue) && booleanValue(variantValue.disabled) !== void 0 ? { disabled: booleanValue(variantValue.disabled) } : {};
|
|
4079
5056
|
}
|
|
4080
5057
|
return Object.keys(variants).length ? variants : void 0;
|
|
4081
5058
|
}
|
|
@@ -4085,7 +5062,7 @@ function sanitizeReasoningEfforts(value) {
|
|
|
4085
5062
|
const efforts = values.filter((candidate) => allReasoningEfforts.includes(candidate));
|
|
4086
5063
|
return efforts.length ? efforts : void 0;
|
|
4087
5064
|
}
|
|
4088
|
-
function
|
|
5065
|
+
function isRecord3(value) {
|
|
4089
5066
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4090
5067
|
}
|
|
4091
5068
|
function stringValue(value) {
|
|
@@ -4101,66 +5078,38 @@ function stringArrayValue(value) {
|
|
|
4101
5078
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
4102
5079
|
}
|
|
4103
5080
|
async function executeToolInvocation(invocation, rootDir, streamOutput, timeoutMs) {
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
let forceKillTimer;
|
|
4115
|
-
const timeout = timeoutMs && timeoutMs > 0 ? setTimeout(() => {
|
|
4116
|
-
if (settled) return;
|
|
4117
|
-
stderr += `${toolTimeoutMessage(invocation.displayCommand, timeoutMs)}
|
|
4118
|
-
`;
|
|
4119
|
-
child.kill("SIGTERM");
|
|
4120
|
-
forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
4121
|
-
forceKillTimer.unref?.();
|
|
4122
|
-
rejectOnce(new Error(toolTimeoutMessage(invocation.displayCommand, timeoutMs)));
|
|
4123
|
-
}, timeoutMs) : void 0;
|
|
4124
|
-
timeout?.unref?.();
|
|
4125
|
-
const resolveOnce = (value) => {
|
|
4126
|
-
if (settled) return;
|
|
4127
|
-
settled = true;
|
|
4128
|
-
if (timeout) clearTimeout(timeout);
|
|
4129
|
-
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4130
|
-
resolve(value);
|
|
4131
|
-
};
|
|
4132
|
-
const rejectOnce = (error) => {
|
|
4133
|
-
if (settled) return;
|
|
4134
|
-
settled = true;
|
|
4135
|
-
if (timeout) clearTimeout(timeout);
|
|
4136
|
-
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4137
|
-
reject(error);
|
|
4138
|
-
};
|
|
4139
|
-
child.on("error", rejectOnce);
|
|
4140
|
-
child.stdout.setEncoding("utf8");
|
|
4141
|
-
child.stderr.setEncoding("utf8");
|
|
4142
|
-
child.stdout.on("data", (chunk) => {
|
|
4143
|
-
stdout += chunk;
|
|
4144
|
-
if (streamOutput) {
|
|
4145
|
-
process.stdout.write(chunk);
|
|
4146
|
-
}
|
|
4147
|
-
});
|
|
4148
|
-
child.stderr.on("data", (chunk) => {
|
|
4149
|
-
stderr += chunk;
|
|
4150
|
-
if (streamOutput) {
|
|
4151
|
-
process.stderr.write(chunk);
|
|
4152
|
-
}
|
|
4153
|
-
});
|
|
4154
|
-
child.stdin.on("error", () => void 0);
|
|
4155
|
-
if (invocation.stdin) {
|
|
4156
|
-
child.stdin.write(invocation.stdin);
|
|
4157
|
-
}
|
|
4158
|
-
child.stdin.end();
|
|
4159
|
-
child.on("close", (exitCode) => {
|
|
4160
|
-
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4161
|
-
resolveOnce({ exitCode: exitCode ?? 1, stdout, stderr });
|
|
4162
|
-
});
|
|
5081
|
+
const hostExecution = await getRuntimeHostExecutionPort();
|
|
5082
|
+
const result = await hostExecution.executeCommand({
|
|
5083
|
+
command: invocation.command,
|
|
5084
|
+
args: invocation.args,
|
|
5085
|
+
cwd: rootDir,
|
|
5086
|
+
env: process.env,
|
|
5087
|
+
shell: invocation.shell ?? false,
|
|
5088
|
+
...invocation.stdin ? { stdin: invocation.stdin } : {},
|
|
5089
|
+
...timeoutMs ? { timeoutMs, timeoutMessage: toolTimeoutMessage(invocation.displayCommand, timeoutMs) } : {},
|
|
5090
|
+
streamOutput
|
|
4163
5091
|
});
|
|
5092
|
+
if (result.status === "timedOut" || result.status === "spawnFailed" || result.status === "unsupported") {
|
|
5093
|
+
throw new Error(result.error?.message ?? `Command execution failed: ${invocation.displayCommand}`);
|
|
5094
|
+
}
|
|
5095
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
5096
|
+
}
|
|
5097
|
+
function normalizeToolExecutionResult(result, rootDir) {
|
|
5098
|
+
return {
|
|
5099
|
+
...result,
|
|
5100
|
+
stdout: normalizeLocalToolOutput(result.stdout, rootDir),
|
|
5101
|
+
stderr: normalizeLocalToolOutput(result.stderr, rootDir)
|
|
5102
|
+
};
|
|
5103
|
+
}
|
|
5104
|
+
function normalizeLocalToolOutput(value, rootDir) {
|
|
5105
|
+
const redacted = redactToolAdapterText(value, { executionRoot: rootDir, redaction: { localPaths: true, secretAssignments: true } });
|
|
5106
|
+
return truncateLocalToolOutput(redacted, localToolOutputBudgetBytes);
|
|
5107
|
+
}
|
|
5108
|
+
function truncateLocalToolOutput(value, budgetBytes) {
|
|
5109
|
+
if (Buffer.byteLength(value, "utf8") <= budgetBytes) return value;
|
|
5110
|
+
const suffix = "\n[truncated by Amistio local tool output budget]";
|
|
5111
|
+
const contentBudget = Math.max(0, budgetBytes - Buffer.byteLength(suffix, "utf8"));
|
|
5112
|
+
return `${Buffer.from(value, "utf8").subarray(0, contentBudget).toString("utf8")}${suffix}`;
|
|
4164
5113
|
}
|
|
4165
5114
|
async function withTimeout(promise, timeoutMs, displayCommand) {
|
|
4166
5115
|
if (!timeoutMs || timeoutMs <= 0) {
|
|
@@ -4182,9 +5131,9 @@ async function withTimeout(promise, timeoutMs, displayCommand) {
|
|
|
4182
5131
|
});
|
|
4183
5132
|
}
|
|
4184
5133
|
function toolTimeoutMessage(displayCommand, timeoutMs) {
|
|
4185
|
-
return `Local tool timed out after ${
|
|
5134
|
+
return `Local tool timed out after ${formatTimeoutDuration2(timeoutMs)}: ${displayCommand}`;
|
|
4186
5135
|
}
|
|
4187
|
-
function
|
|
5136
|
+
function formatTimeoutDuration2(timeoutMs) {
|
|
4188
5137
|
if (timeoutMs < 1e3) {
|
|
4189
5138
|
return `${timeoutMs}ms`;
|
|
4190
5139
|
}
|
|
@@ -4246,7 +5195,7 @@ async function runClaudeSdk(input) {
|
|
|
4246
5195
|
}
|
|
4247
5196
|
}
|
|
4248
5197
|
})) {
|
|
4249
|
-
if (
|
|
5198
|
+
if (isRecord3(message) && message.type === "result") {
|
|
4250
5199
|
if (message.subtype === "success" && typeof message.result === "string") {
|
|
4251
5200
|
stdout += message.result;
|
|
4252
5201
|
if (input.streamOutput) {
|
|
@@ -4332,9 +5281,9 @@ function extractTextParts(parts) {
|
|
|
4332
5281
|
if (!Array.isArray(parts)) {
|
|
4333
5282
|
return "";
|
|
4334
5283
|
}
|
|
4335
|
-
return parts.filter(
|
|
5284
|
+
return parts.filter(isRecord3).map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
|
|
4336
5285
|
}
|
|
4337
|
-
function
|
|
5286
|
+
function errorMessage3(error) {
|
|
4338
5287
|
return error instanceof Error ? error.message : String(error);
|
|
4339
5288
|
}
|
|
4340
5289
|
function shellQuote(value) {
|
|
@@ -4345,14 +5294,14 @@ function shellQuote(value) {
|
|
|
4345
5294
|
import { spawn as spawn2 } from "node:child_process";
|
|
4346
5295
|
import { createHash as createHash3 } from "node:crypto";
|
|
4347
5296
|
import { openSync } from "node:fs";
|
|
4348
|
-
import { mkdir as
|
|
5297
|
+
import { mkdir as mkdir7, readdir as readdir4, readFile as readFile6, writeFile as writeFile7 } from "node:fs/promises";
|
|
4349
5298
|
import os4 from "node:os";
|
|
4350
|
-
import
|
|
5299
|
+
import path8 from "node:path";
|
|
4351
5300
|
function currentRunnerMode() {
|
|
4352
5301
|
return process.env.AMISTIO_RUNNER_MODE === "background" ? "background" : "foreground";
|
|
4353
5302
|
}
|
|
4354
5303
|
function defaultRunnerMetadataDir() {
|
|
4355
|
-
return
|
|
5304
|
+
return path8.join(os4.homedir(), ".config", "amistio", "runners");
|
|
4356
5305
|
}
|
|
4357
5306
|
function updatedCliRunnerLaunchOptions() {
|
|
4358
5307
|
return { executablePath: "amistio", directExecutable: true };
|
|
@@ -4369,8 +5318,8 @@ async function startRunnerDaemon(input) {
|
|
|
4369
5318
|
if (existing?.status === "running" && isProcessRunning(existing.pid)) {
|
|
4370
5319
|
throw new Error(`Background runner ${existing.runnerId} is already running with PID ${existing.pid}.`);
|
|
4371
5320
|
}
|
|
4372
|
-
await
|
|
4373
|
-
const logPath =
|
|
5321
|
+
await mkdir7(metadataDir, { recursive: true });
|
|
5322
|
+
const logPath = path8.join(metadataDir, `${runnerDaemonKey(input)}.log`);
|
|
4374
5323
|
const logFd = openSync(logPath, "a");
|
|
4375
5324
|
const launch = resolveRunnerDaemonLaunch({
|
|
4376
5325
|
args: input.args,
|
|
@@ -4397,7 +5346,7 @@ async function startRunnerDaemon(input) {
|
|
|
4397
5346
|
projectId: input.projectId,
|
|
4398
5347
|
repositoryLinkId: input.repositoryLinkId,
|
|
4399
5348
|
runnerId: input.runnerId,
|
|
4400
|
-
rootDir:
|
|
5349
|
+
rootDir: path8.resolve(input.rootDir),
|
|
4401
5350
|
apiUrl: input.apiUrl,
|
|
4402
5351
|
pid: child.pid,
|
|
4403
5352
|
status: "running",
|
|
@@ -4411,8 +5360,8 @@ async function startRunnerDaemon(input) {
|
|
|
4411
5360
|
}
|
|
4412
5361
|
async function restartRunnerDaemonProcess(metadata, args, input = {}) {
|
|
4413
5362
|
const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
|
|
4414
|
-
await
|
|
4415
|
-
const logPath = metadata.logPath ??
|
|
5363
|
+
await mkdir7(metadataDir, { recursive: true });
|
|
5364
|
+
const logPath = metadata.logPath ?? path8.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
|
|
4416
5365
|
const logFd = openSync(logPath, "a");
|
|
4417
5366
|
const launch = resolveRunnerDaemonLaunch({
|
|
4418
5367
|
args,
|
|
@@ -4449,12 +5398,12 @@ async function restartRunnerDaemonProcess(metadata, args, input = {}) {
|
|
|
4449
5398
|
async function listRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
4450
5399
|
let entries;
|
|
4451
5400
|
try {
|
|
4452
|
-
entries = await
|
|
5401
|
+
entries = await readdir4(metadataDir);
|
|
4453
5402
|
} catch {
|
|
4454
5403
|
return [];
|
|
4455
5404
|
}
|
|
4456
5405
|
const records = await Promise.all(
|
|
4457
|
-
entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => readRunnerDaemonMetadataFile(
|
|
5406
|
+
entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => readRunnerDaemonMetadataFile(path8.join(metadataDir, entry)))
|
|
4458
5407
|
);
|
|
4459
5408
|
return records.filter((record) => Boolean(record)).filter((record) => record.accountId === input.accountId && record.projectId === input.projectId && record.repositoryLinkId === input.repositoryLinkId).filter((record) => !input.runnerId || record.runnerId === input.runnerId).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
4460
5409
|
}
|
|
@@ -4462,8 +5411,8 @@ async function readRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetada
|
|
|
4462
5411
|
return readRunnerDaemonMetadataFile(runnerDaemonMetadataPath(input, metadataDir));
|
|
4463
5412
|
}
|
|
4464
5413
|
async function writeRunnerDaemonMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
4465
|
-
await
|
|
4466
|
-
await
|
|
5414
|
+
await mkdir7(metadataDir, { recursive: true });
|
|
5415
|
+
await writeFile7(runnerDaemonMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
4467
5416
|
}
|
|
4468
5417
|
async function markRunnerDaemonStopped(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
4469
5418
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4525,14 +5474,14 @@ function runnerDaemonUptime(metadata, now = Date.now()) {
|
|
|
4525
5474
|
return `${seconds}s`;
|
|
4526
5475
|
}
|
|
4527
5476
|
function runnerDaemonMetadataPath(input, metadataDir) {
|
|
4528
|
-
return
|
|
5477
|
+
return path8.join(metadataDir, `${runnerDaemonKey(input)}.json`);
|
|
4529
5478
|
}
|
|
4530
5479
|
function runnerDaemonKey(input) {
|
|
4531
5480
|
return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
|
|
4532
5481
|
}
|
|
4533
5482
|
async function readRunnerDaemonMetadataFile(filePath) {
|
|
4534
5483
|
try {
|
|
4535
|
-
const parsed = JSON.parse(await
|
|
5484
|
+
const parsed = JSON.parse(await readFile6(filePath, "utf8"));
|
|
4536
5485
|
if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
|
|
4537
5486
|
return void 0;
|
|
4538
5487
|
}
|
|
@@ -4545,9 +5494,9 @@ async function readRunnerDaemonMetadataFile(filePath) {
|
|
|
4545
5494
|
// src/runner-service.ts
|
|
4546
5495
|
import { spawn as spawn3 } from "node:child_process";
|
|
4547
5496
|
import { createHash as createHash4 } from "node:crypto";
|
|
4548
|
-
import { mkdir as
|
|
5497
|
+
import { mkdir as mkdir8, readFile as readFile7, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
|
|
4549
5498
|
import os5 from "node:os";
|
|
4550
|
-
import
|
|
5499
|
+
import path9 from "node:path";
|
|
4551
5500
|
function detectRunnerServicePlatform(platform = process.platform) {
|
|
4552
5501
|
if (platform === "darwin") return "launchd";
|
|
4553
5502
|
if (platform === "linux") return "systemd";
|
|
@@ -4563,14 +5512,14 @@ function createRunnerServiceDescriptor(input) {
|
|
|
4563
5512
|
const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
|
|
4564
5513
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4565
5514
|
const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
|
|
4566
|
-
const logPath =
|
|
5515
|
+
const logPath = path9.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
|
|
4567
5516
|
const metadata = {
|
|
4568
5517
|
schemaVersion: 1,
|
|
4569
5518
|
accountId: input.accountId,
|
|
4570
5519
|
projectId: input.projectId,
|
|
4571
5520
|
repositoryLinkId: input.repositoryLinkId,
|
|
4572
5521
|
runnerId: input.runnerId,
|
|
4573
|
-
rootDir:
|
|
5522
|
+
rootDir: path9.resolve(input.rootDir),
|
|
4574
5523
|
apiUrl: input.apiUrl,
|
|
4575
5524
|
serviceName,
|
|
4576
5525
|
serviceFilePath,
|
|
@@ -4587,9 +5536,9 @@ function createRunnerServiceDescriptor(input) {
|
|
|
4587
5536
|
}
|
|
4588
5537
|
async function installRunnerService(input, options = {}) {
|
|
4589
5538
|
const descriptor = createRunnerServiceDescriptor(input);
|
|
4590
|
-
await
|
|
4591
|
-
await
|
|
4592
|
-
await
|
|
5539
|
+
await mkdir8(path9.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
|
|
5540
|
+
await mkdir8(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
|
|
5541
|
+
await writeFile8(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
|
|
4593
5542
|
await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
|
|
4594
5543
|
if (options.activate !== false) {
|
|
4595
5544
|
const activation = await activateRunnerService(descriptor.metadata);
|
|
@@ -4611,7 +5560,7 @@ async function removeRunnerService(input) {
|
|
|
4611
5560
|
}
|
|
4612
5561
|
async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
4613
5562
|
try {
|
|
4614
|
-
const parsed = JSON.parse(await
|
|
5563
|
+
const parsed = JSON.parse(await readFile7(runnerServiceMetadataPath(input, metadataDir), "utf8"));
|
|
4615
5564
|
if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
|
|
4616
5565
|
return void 0;
|
|
4617
5566
|
}
|
|
@@ -4621,8 +5570,8 @@ async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetad
|
|
|
4621
5570
|
}
|
|
4622
5571
|
}
|
|
4623
5572
|
async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
4624
|
-
await
|
|
4625
|
-
await
|
|
5573
|
+
await mkdir8(metadataDir, { recursive: true });
|
|
5574
|
+
await writeFile8(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
4626
5575
|
}
|
|
4627
5576
|
async function runnerServiceRuntimeStatus(metadata) {
|
|
4628
5577
|
if (metadata.platform === "launchd") {
|
|
@@ -4705,12 +5654,12 @@ WantedBy=default.target
|
|
|
4705
5654
|
}
|
|
4706
5655
|
function runnerServiceFilePath(platform, serviceName, homeDir) {
|
|
4707
5656
|
if (platform === "launchd") {
|
|
4708
|
-
return
|
|
5657
|
+
return path9.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
|
|
4709
5658
|
}
|
|
4710
|
-
return
|
|
5659
|
+
return path9.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
|
|
4711
5660
|
}
|
|
4712
5661
|
function runnerServiceMetadataPath(input, metadataDir) {
|
|
4713
|
-
return
|
|
5662
|
+
return path9.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
|
|
4714
5663
|
}
|
|
4715
5664
|
function runnerServiceName(input) {
|
|
4716
5665
|
return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
|
|
@@ -4769,7 +5718,7 @@ function completedToolSessionClosedReason(session) {
|
|
|
4769
5718
|
return "Completed one-shot tool run; this session is not reusable.";
|
|
4770
5719
|
}
|
|
4771
5720
|
function staleToolSessionClosedReason(session, now = /* @__PURE__ */ new Date()) {
|
|
4772
|
-
if (session.status !== "open") {
|
|
5721
|
+
if (session.status !== "open" && session.status !== "active") {
|
|
4773
5722
|
return void 0;
|
|
4774
5723
|
}
|
|
4775
5724
|
const lastActivityMs = Date.parse(session.lastActivityAt);
|
|
@@ -5045,8 +5994,8 @@ function createSmokeSession({ now, status, lastActivityAt }) {
|
|
|
5045
5994
|
// src/sync.ts
|
|
5046
5995
|
import { execFile as execFile3 } from "node:child_process";
|
|
5047
5996
|
import { createHash as createHash5 } from "node:crypto";
|
|
5048
|
-
import { mkdir as
|
|
5049
|
-
import
|
|
5997
|
+
import { mkdir as mkdir9, readdir as readdir5, readFile as readFile8, stat as stat4, writeFile as writeFile9 } from "node:fs/promises";
|
|
5998
|
+
import path10 from "node:path";
|
|
5050
5999
|
import { promisify as promisify3 } from "node:util";
|
|
5051
6000
|
var execFileAsync3 = promisify3(execFile3);
|
|
5052
6001
|
var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
@@ -5142,7 +6091,7 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
5142
6091
|
const documentFiles = await findBrainDocumentFiles(rootDir);
|
|
5143
6092
|
const documents = [];
|
|
5144
6093
|
for (const fullPath of documentFiles) {
|
|
5145
|
-
const raw = await
|
|
6094
|
+
const raw = await readFile8(fullPath, "utf8");
|
|
5146
6095
|
const repoPath = toRepoPath(rootDir, fullPath);
|
|
5147
6096
|
const parsed = parseSyncedDocument(raw, repoPath);
|
|
5148
6097
|
if (!parsed) {
|
|
@@ -5192,8 +6141,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
|
5192
6141
|
result.skipped.push(document.repoPath);
|
|
5193
6142
|
continue;
|
|
5194
6143
|
}
|
|
5195
|
-
await
|
|
5196
|
-
await
|
|
6144
|
+
await mkdir9(path10.dirname(fullPath), { recursive: true });
|
|
6145
|
+
await writeFile9(fullPath, createSyncedDocumentContent(document), "utf8");
|
|
5197
6146
|
result.written.push(document.repoPath);
|
|
5198
6147
|
}
|
|
5199
6148
|
return result;
|
|
@@ -5322,7 +6271,7 @@ function parseSyncedHtml(content) {
|
|
|
5322
6271
|
}
|
|
5323
6272
|
async function readExistingSyncedDocument(fullPath) {
|
|
5324
6273
|
try {
|
|
5325
|
-
const raw = await
|
|
6274
|
+
const raw = await readFile8(fullPath, "utf8");
|
|
5326
6275
|
const parsed = parseSyncedDocument(raw, fullPath);
|
|
5327
6276
|
if (!parsed) {
|
|
5328
6277
|
return { exists: true };
|
|
@@ -5347,7 +6296,7 @@ async function readExistingSyncedDocument(fullPath) {
|
|
|
5347
6296
|
async function findBrainDocumentFiles(rootDir) {
|
|
5348
6297
|
const files = [];
|
|
5349
6298
|
for (const syncRoot of [...syncRoots, htmlSyncRoot]) {
|
|
5350
|
-
const fullRoot =
|
|
6299
|
+
const fullRoot = path10.join(rootDir, syncRoot);
|
|
5351
6300
|
if (!await exists2(fullRoot)) {
|
|
5352
6301
|
continue;
|
|
5353
6302
|
}
|
|
@@ -5356,8 +6305,8 @@ async function findBrainDocumentFiles(rootDir) {
|
|
|
5356
6305
|
return files;
|
|
5357
6306
|
}
|
|
5358
6307
|
async function walkBrainDocumentFiles(directory, files) {
|
|
5359
|
-
for (const entry of await
|
|
5360
|
-
const fullPath =
|
|
6308
|
+
for (const entry of await readdir5(directory, { withFileTypes: true })) {
|
|
6309
|
+
const fullPath = path10.join(directory, entry.name);
|
|
5361
6310
|
if (entry.isDirectory()) {
|
|
5362
6311
|
await walkBrainDocumentFiles(fullPath, files);
|
|
5363
6312
|
} else if (entry.isFile() && /\.(md|mdx|html?)$/i.test(entry.name)) {
|
|
@@ -5366,23 +6315,23 @@ async function walkBrainDocumentFiles(directory, files) {
|
|
|
5366
6315
|
}
|
|
5367
6316
|
}
|
|
5368
6317
|
function safeRepoPath(rootDir, repoPath) {
|
|
5369
|
-
if (
|
|
6318
|
+
if (path10.isAbsolute(repoPath)) {
|
|
5370
6319
|
throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
|
|
5371
6320
|
}
|
|
5372
|
-
const normalized =
|
|
5373
|
-
if (normalized === ".." || normalized.startsWith(`..${
|
|
6321
|
+
const normalized = path10.normalize(repoPath);
|
|
6322
|
+
if (normalized === ".." || normalized.startsWith(`..${path10.sep}`)) {
|
|
5374
6323
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
5375
6324
|
}
|
|
5376
|
-
const root =
|
|
5377
|
-
const fullPath =
|
|
5378
|
-
if (!fullPath.startsWith(`${root}${
|
|
6325
|
+
const root = path10.resolve(rootDir);
|
|
6326
|
+
const fullPath = path10.resolve(root, normalized);
|
|
6327
|
+
if (!fullPath.startsWith(`${root}${path10.sep}`)) {
|
|
5379
6328
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
5380
6329
|
}
|
|
5381
6330
|
return fullPath;
|
|
5382
6331
|
}
|
|
5383
6332
|
function isControlPlanePath(repoPath) {
|
|
5384
|
-
const normalized =
|
|
5385
|
-
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${
|
|
6333
|
+
const normalized = path10.normalize(repoPath);
|
|
6334
|
+
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path10.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path10.sep}`);
|
|
5386
6335
|
}
|
|
5387
6336
|
function canonicalControlPlaneRepoPath(repoPath) {
|
|
5388
6337
|
const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
@@ -5393,16 +6342,16 @@ function canonicalControlPlaneRepoPath(repoPath) {
|
|
|
5393
6342
|
return normalized;
|
|
5394
6343
|
}
|
|
5395
6344
|
function toRepoPath(rootDir, fullPath) {
|
|
5396
|
-
return
|
|
6345
|
+
return path10.relative(rootDir, fullPath).split(path10.sep).join("/");
|
|
5397
6346
|
}
|
|
5398
6347
|
function inferTitle(content, repoPath) {
|
|
5399
6348
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
5400
6349
|
if (heading) return heading;
|
|
5401
6350
|
const htmlHeading = content.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
|
|
5402
|
-
return htmlHeading ||
|
|
6351
|
+
return htmlHeading || path10.basename(repoPath, path10.extname(repoPath));
|
|
5403
6352
|
}
|
|
5404
6353
|
async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
|
|
5405
|
-
const root =
|
|
6354
|
+
const root = path10.resolve(rootDir);
|
|
5406
6355
|
const maxBytes = (options.maxFileKb ?? defaultAutoSyncMaxFileKb) * 1024;
|
|
5407
6356
|
const syncedAt = options.syncedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
5408
6357
|
const existingById = new Map(existingDocuments.map((document) => [document.documentId, document]));
|
|
@@ -5418,7 +6367,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
|
|
|
5418
6367
|
continue;
|
|
5419
6368
|
}
|
|
5420
6369
|
const fullPath = safeRepoPath(root, normalizedRepoPath);
|
|
5421
|
-
const fileStat = await
|
|
6370
|
+
const fileStat = await stat4(fullPath).catch(() => void 0);
|
|
5422
6371
|
if (!fileStat?.isFile()) {
|
|
5423
6372
|
skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
|
|
5424
6373
|
continue;
|
|
@@ -5427,7 +6376,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
|
|
|
5427
6376
|
skipped.push({ repoPath: normalizedRepoPath, reason: "tooLarge" });
|
|
5428
6377
|
continue;
|
|
5429
6378
|
}
|
|
5430
|
-
const content = await
|
|
6379
|
+
const content = await readFile8(fullPath, "utf8").catch(() => void 0);
|
|
5431
6380
|
if (content === void 0) {
|
|
5432
6381
|
skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
|
|
5433
6382
|
continue;
|
|
@@ -5492,7 +6441,7 @@ async function listAutoSyncCandidatePaths(rootDir) {
|
|
|
5492
6441
|
}
|
|
5493
6442
|
const files = [];
|
|
5494
6443
|
for (const syncRoot of [...syncRoots, htmlSyncRoot, ...legacySyncRoots]) {
|
|
5495
|
-
const fullRoot =
|
|
6444
|
+
const fullRoot = path10.join(rootDir, syncRoot);
|
|
5496
6445
|
if (await exists2(fullRoot)) {
|
|
5497
6446
|
await walkAutoSyncFiles(rootDir, fullRoot, files);
|
|
5498
6447
|
}
|
|
@@ -5500,9 +6449,9 @@ async function listAutoSyncCandidatePaths(rootDir) {
|
|
|
5500
6449
|
return uniqueSortedRepoPaths(files);
|
|
5501
6450
|
}
|
|
5502
6451
|
async function walkAutoSyncFiles(rootDir, directory, files) {
|
|
5503
|
-
for (const entry of await
|
|
5504
|
-
const fullPath =
|
|
5505
|
-
const repoPath = normalizeRepoPath3(
|
|
6452
|
+
for (const entry of await readdir5(directory, { withFileTypes: true }).catch(() => [])) {
|
|
6453
|
+
const fullPath = path10.join(directory, entry.name);
|
|
6454
|
+
const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
|
|
5506
6455
|
if (entry.isDirectory()) {
|
|
5507
6456
|
if (!autoSyncExcludedDirectoryNames.has(entry.name)) {
|
|
5508
6457
|
await walkAutoSyncFiles(rootDir, fullPath, files);
|
|
@@ -5556,7 +6505,7 @@ function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
|
5556
6505
|
}
|
|
5557
6506
|
async function exists2(filePath) {
|
|
5558
6507
|
try {
|
|
5559
|
-
await
|
|
6508
|
+
await stat4(filePath);
|
|
5560
6509
|
return true;
|
|
5561
6510
|
} catch {
|
|
5562
6511
|
return false;
|
|
@@ -5564,9 +6513,9 @@ async function exists2(filePath) {
|
|
|
5564
6513
|
}
|
|
5565
6514
|
|
|
5566
6515
|
// src/tool-session-store.ts
|
|
5567
|
-
import { mkdir as
|
|
6516
|
+
import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile10 } from "node:fs/promises";
|
|
5568
6517
|
import os6 from "node:os";
|
|
5569
|
-
import
|
|
6518
|
+
import path11 from "node:path";
|
|
5570
6519
|
var LocalToolSessionStore = class {
|
|
5571
6520
|
constructor(filePath = defaultSessionStorePath()) {
|
|
5572
6521
|
this.filePath = filePath;
|
|
@@ -5580,12 +6529,12 @@ var LocalToolSessionStore = class {
|
|
|
5580
6529
|
async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
|
|
5581
6530
|
const data = await this.read();
|
|
5582
6531
|
data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5583
|
-
await
|
|
5584
|
-
await
|
|
6532
|
+
await mkdir10(path11.dirname(this.filePath), { recursive: true });
|
|
6533
|
+
await writeFile10(this.filePath, JSON.stringify(data, null, 2), "utf8");
|
|
5585
6534
|
}
|
|
5586
6535
|
async read() {
|
|
5587
6536
|
try {
|
|
5588
|
-
return JSON.parse(await
|
|
6537
|
+
return JSON.parse(await readFile9(this.filePath, "utf8"));
|
|
5589
6538
|
} catch {
|
|
5590
6539
|
return {};
|
|
5591
6540
|
}
|
|
@@ -5593,16 +6542,16 @@ var LocalToolSessionStore = class {
|
|
|
5593
6542
|
};
|
|
5594
6543
|
function defaultSessionStorePath() {
|
|
5595
6544
|
if (process.platform === "darwin") {
|
|
5596
|
-
return
|
|
6545
|
+
return path11.join(os6.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
|
|
5597
6546
|
}
|
|
5598
6547
|
if (process.platform === "win32") {
|
|
5599
|
-
return
|
|
6548
|
+
return path11.join(process.env.APPDATA ?? os6.homedir(), "Amistio", "tool-sessions.json");
|
|
5600
6549
|
}
|
|
5601
|
-
return
|
|
6550
|
+
return path11.join(process.env.XDG_STATE_HOME ?? path11.join(os6.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
|
|
5602
6551
|
}
|
|
5603
6552
|
|
|
5604
6553
|
// src/work-runner.ts
|
|
5605
|
-
import
|
|
6554
|
+
import path12 from "node:path";
|
|
5606
6555
|
var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
|
|
5607
6556
|
var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
|
|
5608
6557
|
var assistantAnswerStart = "AMISTIO_ASSISTANT_ANSWER_START";
|
|
@@ -7085,15 +8034,15 @@ function normalizeProjectContextRepoPath(value, options) {
|
|
|
7085
8034
|
if (!trimmed || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed) || /^file:/i.test(trimmed) || /^[A-Za-z]:($|[^\\/])/.test(trimmed)) {
|
|
7086
8035
|
throwUnsafeProjectContextPath();
|
|
7087
8036
|
}
|
|
7088
|
-
const absolute = trimmed.startsWith("/") || trimmed.startsWith("\\\\") ||
|
|
8037
|
+
const absolute = trimmed.startsWith("/") || trimmed.startsWith("\\\\") || path12.isAbsolute(trimmed) || path12.win32.isAbsolute(trimmed);
|
|
7089
8038
|
if (!absolute) {
|
|
7090
8039
|
return normalizeRelativeProjectContextPath(trimmed);
|
|
7091
8040
|
}
|
|
7092
8041
|
if (!options.repositoryRoot) {
|
|
7093
8042
|
throwUnsafeProjectContextPath();
|
|
7094
8043
|
}
|
|
7095
|
-
const useWindowsPathRules =
|
|
7096
|
-
const relativePath = useWindowsPathRules ?
|
|
8044
|
+
const useWindowsPathRules = path12.win32.isAbsolute(trimmed) && !trimmed.startsWith("/");
|
|
8045
|
+
const relativePath = useWindowsPathRules ? path12.win32.relative(path12.win32.resolve(options.repositoryRoot), path12.win32.resolve(trimmed)) : path12.relative(path12.resolve(options.repositoryRoot), path12.resolve(trimmed));
|
|
7097
8046
|
return normalizeRelativeProjectContextPath(relativePath);
|
|
7098
8047
|
}
|
|
7099
8048
|
function normalizeRelativeProjectContextPath(value) {
|
|
@@ -7298,8 +8247,8 @@ function roundNumber(value, digits) {
|
|
|
7298
8247
|
// src/importer.ts
|
|
7299
8248
|
import { execFile as execFile4 } from "node:child_process";
|
|
7300
8249
|
import { createHash as createHash7 } from "node:crypto";
|
|
7301
|
-
import { readdir as
|
|
7302
|
-
import
|
|
8250
|
+
import { readdir as readdir6, readFile as readFile10, stat as stat5 } from "node:fs/promises";
|
|
8251
|
+
import path13 from "node:path";
|
|
7303
8252
|
import { promisify as promisify4 } from "node:util";
|
|
7304
8253
|
var execFileAsync4 = promisify4(execFile4);
|
|
7305
8254
|
var defaultMaxFileKb = 256;
|
|
@@ -7320,12 +8269,12 @@ var documentFolderByType = {
|
|
|
7320
8269
|
workflow: "docs/workflows"
|
|
7321
8270
|
};
|
|
7322
8271
|
async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
7323
|
-
const requestedRoot =
|
|
8272
|
+
const requestedRoot = path13.resolve(rootDir);
|
|
7324
8273
|
const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
|
|
7325
8274
|
const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
|
|
7326
8275
|
const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
|
|
7327
8276
|
const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
|
|
7328
|
-
const repoName = (parsedCloneUrl?.repoName ??
|
|
8277
|
+
const repoName = (parsedCloneUrl?.repoName ?? path13.basename(root)) || "repository";
|
|
7329
8278
|
const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
|
|
7330
8279
|
return {
|
|
7331
8280
|
rootDir: root,
|
|
@@ -7337,7 +8286,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
|
7337
8286
|
};
|
|
7338
8287
|
}
|
|
7339
8288
|
async function scanLegacyDocuments(options) {
|
|
7340
|
-
const rootDir =
|
|
8289
|
+
const rootDir = path13.resolve(options.rootDir);
|
|
7341
8290
|
const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
|
|
7342
8291
|
const skipped = [];
|
|
7343
8292
|
const candidates = [];
|
|
@@ -7357,8 +8306,8 @@ async function scanLegacyDocuments(options) {
|
|
|
7357
8306
|
skipped.push({ repoPath, reason: "excluded" });
|
|
7358
8307
|
continue;
|
|
7359
8308
|
}
|
|
7360
|
-
const fullPath =
|
|
7361
|
-
const fileStat = await
|
|
8309
|
+
const fullPath = path13.join(rootDir, ...repoPath.split("/"));
|
|
8310
|
+
const fileStat = await stat5(fullPath).catch(() => void 0);
|
|
7362
8311
|
if (!fileStat?.isFile()) {
|
|
7363
8312
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
7364
8313
|
continue;
|
|
@@ -7367,7 +8316,7 @@ async function scanLegacyDocuments(options) {
|
|
|
7367
8316
|
skipped.push({ repoPath, reason: "tooLarge" });
|
|
7368
8317
|
continue;
|
|
7369
8318
|
}
|
|
7370
|
-
const content = await
|
|
8319
|
+
const content = await readFile10(fullPath, "utf8").catch(() => void 0);
|
|
7371
8320
|
if (content === void 0) {
|
|
7372
8321
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
7373
8322
|
continue;
|
|
@@ -7457,10 +8406,10 @@ async function listRepositoryPaths(rootDir) {
|
|
|
7457
8406
|
return files;
|
|
7458
8407
|
}
|
|
7459
8408
|
async function walkRepository(rootDir, directory, files) {
|
|
7460
|
-
const entries = await
|
|
8409
|
+
const entries = await readdir6(directory, { withFileTypes: true }).catch(() => []);
|
|
7461
8410
|
for (const entry of entries) {
|
|
7462
|
-
const fullPath =
|
|
7463
|
-
const repoPath = normalizeRepoPath4(
|
|
8411
|
+
const fullPath = path13.join(directory, entry.name);
|
|
8412
|
+
const repoPath = normalizeRepoPath4(path13.relative(rootDir, fullPath));
|
|
7464
8413
|
if (entry.isDirectory()) {
|
|
7465
8414
|
if (!excludedDirectoryNames.has(entry.name)) {
|
|
7466
8415
|
await walkRepository(rootDir, fullPath, files);
|
|
@@ -7528,9 +8477,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
|
7528
8477
|
usedPaths.add(basePath);
|
|
7529
8478
|
return basePath;
|
|
7530
8479
|
}
|
|
7531
|
-
const extension =
|
|
7532
|
-
const directory =
|
|
7533
|
-
const basename =
|
|
8480
|
+
const extension = path13.posix.extname(basePath) || ".md";
|
|
8481
|
+
const directory = path13.posix.dirname(basePath);
|
|
8482
|
+
const basename = path13.posix.basename(basePath, extension);
|
|
7534
8483
|
const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
|
|
7535
8484
|
usedPaths.add(uniquePath);
|
|
7536
8485
|
return uniquePath;
|
|
@@ -7603,7 +8552,7 @@ function inferTitle2(content, repoPath) {
|
|
|
7603
8552
|
if (heading) return heading;
|
|
7604
8553
|
const htmlHeading = body.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
|
|
7605
8554
|
if (htmlHeading) return htmlHeading;
|
|
7606
|
-
const basename =
|
|
8555
|
+
const basename = path13.posix.basename(repoPath, path13.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
|
|
7607
8556
|
return titleCase(basename || "Imported Document");
|
|
7608
8557
|
}
|
|
7609
8558
|
function stripFrontmatter(content) {
|
|
@@ -7637,7 +8586,7 @@ async function runGit2(args) {
|
|
|
7637
8586
|
|
|
7638
8587
|
// src/runner-actions.ts
|
|
7639
8588
|
import { spawn as spawn4 } from "node:child_process";
|
|
7640
|
-
import
|
|
8589
|
+
import path14 from "node:path";
|
|
7641
8590
|
function buildBackgroundRunnerArgs(options) {
|
|
7642
8591
|
const args = [
|
|
7643
8592
|
"run",
|
|
@@ -7647,7 +8596,7 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
7647
8596
|
"--runner-id",
|
|
7648
8597
|
options.runnerId,
|
|
7649
8598
|
"--root",
|
|
7650
|
-
|
|
8599
|
+
path14.resolve(options.root),
|
|
7651
8600
|
"--session",
|
|
7652
8601
|
options.session,
|
|
7653
8602
|
"--interval-seconds",
|
|
@@ -7765,8 +8714,8 @@ function truncateProcessOutput(value) {
|
|
|
7765
8714
|
|
|
7766
8715
|
// src/git-worktree.ts
|
|
7767
8716
|
import { execFile as execFile5 } from "node:child_process";
|
|
7768
|
-
import { copyFile, lstat, mkdir as
|
|
7769
|
-
import
|
|
8717
|
+
import { copyFile, lstat, mkdir as mkdir11, readdir as readdir7, stat as stat6 } from "node:fs/promises";
|
|
8718
|
+
import path15 from "node:path";
|
|
7770
8719
|
import { promisify as promisify5 } from "node:util";
|
|
7771
8720
|
var execFileAsync5 = promisify5(execFile5);
|
|
7772
8721
|
var exactLocalEnvironmentFiles = /* @__PURE__ */ new Set([".env", ".env.local", ".env.development", ".env.development.local", ".env.test", ".env.test.local", ".env.production", ".env.production.local"]);
|
|
@@ -7787,7 +8736,7 @@ function resolveWorktreeIdentity(workItem) {
|
|
|
7787
8736
|
async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
7788
8737
|
const identity = resolveWorktreeIdentity(workItem);
|
|
7789
8738
|
const repoRoot = await gitOutput(rootDir, ["rev-parse", "--show-toplevel"]).catch((error) => {
|
|
7790
|
-
throw new Error(`Git worktree isolation requires a paired Git checkout: ${
|
|
8739
|
+
throw new Error(`Git worktree isolation requires a paired Git checkout: ${errorMessage4(error)}`);
|
|
7791
8740
|
});
|
|
7792
8741
|
const currentHead = await gitOutput(repoRoot, ["rev-parse", "HEAD"]);
|
|
7793
8742
|
await assertBaseRevision(repoRoot, workItem.baseRevision, currentHead);
|
|
@@ -7798,11 +8747,11 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
|
7798
8747
|
const preparedLocalEnvironmentFileCount2 = await prepareLocalWorktreeEnvironment(repoRoot, worktreePath);
|
|
7799
8748
|
return { ...identity, baseRevision, worktreePath, ...preparedLocalEnvironmentFileCount2 ? { preparedLocalEnvironmentFileCount: preparedLocalEnvironmentFileCount2 } : {} };
|
|
7800
8749
|
}
|
|
7801
|
-
await
|
|
8750
|
+
await mkdir11(path15.dirname(worktreePath), { recursive: true });
|
|
7802
8751
|
const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
|
|
7803
8752
|
const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
|
|
7804
8753
|
await gitOutput(repoRoot, worktreeArgs).catch((error) => {
|
|
7805
|
-
throw new Error(`Could not create Git worktree ${identity.worktreeKey} on ${identity.branch}: ${
|
|
8754
|
+
throw new Error(`Could not create Git worktree ${identity.worktreeKey} on ${identity.branch}: ${errorMessage4(error)}`);
|
|
7806
8755
|
});
|
|
7807
8756
|
const preparedLocalEnvironmentFileCount = await prepareLocalWorktreeEnvironment(repoRoot, worktreePath);
|
|
7808
8757
|
return { ...identity, baseRevision, worktreePath, ...preparedLocalEnvironmentFileCount ? { preparedLocalEnvironmentFileCount } : {} };
|
|
@@ -7816,9 +8765,9 @@ async function resolveExistingGitWorktreeIsolation(rootDir, workItem) {
|
|
|
7816
8765
|
return { ...identity, baseRevision, worktreePath };
|
|
7817
8766
|
}
|
|
7818
8767
|
function localWorktreePath(repoRoot, worktreeKey) {
|
|
7819
|
-
const repoName =
|
|
8768
|
+
const repoName = path15.basename(repoRoot);
|
|
7820
8769
|
const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
|
|
7821
|
-
return
|
|
8770
|
+
return path15.join(path15.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
|
|
7822
8771
|
}
|
|
7823
8772
|
async function assertExistingWorktree(worktreePath, branch) {
|
|
7824
8773
|
await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -7844,8 +8793,8 @@ async function prepareLocalWorktreeEnvironment(repoRoot, worktreePath) {
|
|
|
7844
8793
|
const candidates = await localEnvironmentFileCandidates(repoRoot);
|
|
7845
8794
|
let preparedCount = 0;
|
|
7846
8795
|
for (const candidate of candidates) {
|
|
7847
|
-
const sourcePath =
|
|
7848
|
-
const targetPath =
|
|
8796
|
+
const sourcePath = path15.join(repoRoot, candidate);
|
|
8797
|
+
const targetPath = path15.join(worktreePath, candidate);
|
|
7849
8798
|
if (await pathExists(targetPath)) {
|
|
7850
8799
|
continue;
|
|
7851
8800
|
}
|
|
@@ -7866,7 +8815,7 @@ async function prepareLocalWorktreeEnvironment(repoRoot, worktreePath) {
|
|
|
7866
8815
|
}
|
|
7867
8816
|
async function localEnvironmentFileCandidates(repoRoot) {
|
|
7868
8817
|
const names = new Set(exactLocalEnvironmentFiles);
|
|
7869
|
-
for (const entry of await
|
|
8818
|
+
for (const entry of await readdir7(repoRoot)) {
|
|
7870
8819
|
if (isAllowedLocalEnvironmentFile(entry)) {
|
|
7871
8820
|
names.add(entry);
|
|
7872
8821
|
}
|
|
@@ -7876,7 +8825,7 @@ async function localEnvironmentFileCandidates(repoRoot) {
|
|
|
7876
8825
|
if (!isRootFileName(name)) {
|
|
7877
8826
|
continue;
|
|
7878
8827
|
}
|
|
7879
|
-
const source = await lstat(
|
|
8828
|
+
const source = await lstat(path15.join(repoRoot, name)).catch(() => void 0);
|
|
7880
8829
|
if (source?.isFile()) {
|
|
7881
8830
|
candidates.push(name);
|
|
7882
8831
|
}
|
|
@@ -7887,7 +8836,7 @@ function isAllowedLocalEnvironmentFile(name) {
|
|
|
7887
8836
|
return exactLocalEnvironmentFiles.has(name) || localEnvironmentFilePattern.test(name);
|
|
7888
8837
|
}
|
|
7889
8838
|
function isRootFileName(name) {
|
|
7890
|
-
return name ===
|
|
8839
|
+
return name === path15.basename(name) && !name.includes("/") && !name.includes("\\");
|
|
7891
8840
|
}
|
|
7892
8841
|
async function gitOutput(cwd, args) {
|
|
7893
8842
|
const { stdout } = await execFileAsync5("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
@@ -7897,7 +8846,7 @@ async function gitCommandSucceeds(cwd, args) {
|
|
|
7897
8846
|
return execFileAsync5("git", args, { cwd }).then(() => true, () => false);
|
|
7898
8847
|
}
|
|
7899
8848
|
async function pathExists(value) {
|
|
7900
|
-
return
|
|
8849
|
+
return stat6(value).then(() => true, () => false);
|
|
7901
8850
|
}
|
|
7902
8851
|
function workIsolationSlug(scopeId, title) {
|
|
7903
8852
|
const scopeSlug = slugify(scopeId);
|
|
@@ -7908,7 +8857,7 @@ function workIsolationSlug(scopeId, title) {
|
|
|
7908
8857
|
function slugify(value) {
|
|
7909
8858
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "work";
|
|
7910
8859
|
}
|
|
7911
|
-
function
|
|
8860
|
+
function errorMessage4(error) {
|
|
7912
8861
|
return error instanceof Error ? error.message : String(error);
|
|
7913
8862
|
}
|
|
7914
8863
|
function safeFileError(error) {
|
|
@@ -7920,7 +8869,7 @@ function safeFileError(error) {
|
|
|
7920
8869
|
|
|
7921
8870
|
// src/implementation-handoff.ts
|
|
7922
8871
|
import { execFile as execFile6 } from "node:child_process";
|
|
7923
|
-
import
|
|
8872
|
+
import path16 from "node:path";
|
|
7924
8873
|
import { promisify as promisify6 } from "node:util";
|
|
7925
8874
|
var execFileAsync6 = promisify6(execFile6);
|
|
7926
8875
|
async function completeImplementationHandoff(input) {
|
|
@@ -8188,7 +9137,7 @@ async function cleanupWorktree(run, input) {
|
|
|
8188
9137
|
return { status: "failed", message: "Cleanup skipped because the worktree is not clean after PR handoff." };
|
|
8189
9138
|
}
|
|
8190
9139
|
try {
|
|
8191
|
-
await gitOutput2(run, input.primaryRepoRoot ||
|
|
9140
|
+
await gitOutput2(run, input.primaryRepoRoot || path16.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
|
|
8192
9141
|
return { status: "completed" };
|
|
8193
9142
|
} catch (error) {
|
|
8194
9143
|
return { status: "failed", message: `Cleanup failed: ${safeErrorMessage(error)}` };
|
|
@@ -8300,6 +9249,481 @@ function redactLocalPaths(value) {
|
|
|
8300
9249
|
return value.replace(/(^|[\s'"`])(\/{1,2}(?:Users|home|private|tmp|var|Volumes)\/[^\s'"`]+)/g, "$1<local-path>").replace(/(^|[\s'"`])([A-Za-z]:\\[^\s'"`]+)/g, "$1<local-path>");
|
|
8301
9250
|
}
|
|
8302
9251
|
|
|
9252
|
+
// src/direct-model-client.ts
|
|
9253
|
+
var githubModelsProviderId = "github-models";
|
|
9254
|
+
var githubModelsEndpoint = "https://models.github.ai/inference/chat/completions";
|
|
9255
|
+
var githubModelsSupportedModelIds = ["openai/gpt-4.1", "openai/gpt-4o", "openai/gpt-5"];
|
|
9256
|
+
function shouldUseDirectModelClient(options) {
|
|
9257
|
+
if (options.toolCommand) return false;
|
|
9258
|
+
if (options.providerId !== githubModelsProviderId) return false;
|
|
9259
|
+
return options.tool === void 0 || options.tool === "auto" || options.tool === "none";
|
|
9260
|
+
}
|
|
9261
|
+
function createDirectModelClientPreview(options) {
|
|
9262
|
+
const modelId = requireSupportedGithubModelsModelId(options.modelId);
|
|
9263
|
+
return {
|
|
9264
|
+
toolName: "amistio-direct",
|
|
9265
|
+
displayCommand: "GitHub Models Direct API <selected model>",
|
|
9266
|
+
supportsSessionReuse: false,
|
|
9267
|
+
resumabilityScope: "none",
|
|
9268
|
+
model: `${githubModelsProviderId}/${modelId}`,
|
|
9269
|
+
providerId: githubModelsProviderId,
|
|
9270
|
+
modelId,
|
|
9271
|
+
...options.modelVariant ? { modelVariant: options.modelVariant } : {},
|
|
9272
|
+
...options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}
|
|
9273
|
+
};
|
|
9274
|
+
}
|
|
9275
|
+
function resolveDirectModelClientPreference(options) {
|
|
9276
|
+
if (options.tool && options.tool !== "auto" && options.tool !== "none") {
|
|
9277
|
+
return void 0;
|
|
9278
|
+
}
|
|
9279
|
+
const modelFromCombinedPreference = options.model?.startsWith(`${githubModelsProviderId}/`) ? options.model.slice(`${githubModelsProviderId}/`.length) : void 0;
|
|
9280
|
+
const providerId = options.providerId ?? (modelFromCombinedPreference ? githubModelsProviderId : void 0);
|
|
9281
|
+
if (providerId !== githubModelsProviderId) {
|
|
9282
|
+
return void 0;
|
|
9283
|
+
}
|
|
9284
|
+
if (options.requestedInvocationChannel === "command") {
|
|
9285
|
+
return { ready: false, status: "channelUnsupported", message: "GitHub Models direct provider uses the Amistio direct client and does not support command invocation." };
|
|
9286
|
+
}
|
|
9287
|
+
let modelId;
|
|
9288
|
+
try {
|
|
9289
|
+
modelId = requireSupportedGithubModelsModelId(options.modelId ?? modelFromCombinedPreference);
|
|
9290
|
+
} catch (error) {
|
|
9291
|
+
return { ready: false, status: "modelUnsupported", message: errorMessage5(error) };
|
|
9292
|
+
}
|
|
9293
|
+
return {
|
|
9294
|
+
ready: true,
|
|
9295
|
+
config: {
|
|
9296
|
+
model: `${githubModelsProviderId}/${modelId}`,
|
|
9297
|
+
providerId: githubModelsProviderId,
|
|
9298
|
+
modelId,
|
|
9299
|
+
...options.modelVariant ? { modelVariant: options.modelVariant } : {},
|
|
9300
|
+
...options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}
|
|
9301
|
+
}
|
|
9302
|
+
};
|
|
9303
|
+
}
|
|
9304
|
+
async function createGithubModelsChatCompletion(options) {
|
|
9305
|
+
const modelId = requireSupportedGithubModelsModelId(options.modelId);
|
|
9306
|
+
const token = resolveGithubModelsToken(options.env ?? process.env);
|
|
9307
|
+
const requestInit = {
|
|
9308
|
+
method: "POST",
|
|
9309
|
+
headers: {
|
|
9310
|
+
authorization: `Bearer ${token}`,
|
|
9311
|
+
"content-type": "application/json"
|
|
9312
|
+
},
|
|
9313
|
+
body: JSON.stringify({
|
|
9314
|
+
model: modelId,
|
|
9315
|
+
messages: options.messages,
|
|
9316
|
+
...options.tools?.length ? { tools: options.tools, tool_choice: "auto" } : {},
|
|
9317
|
+
...options.reasoningEffort && options.reasoningEffort !== "auto" ? { reasoning_effort: options.reasoningEffort } : {}
|
|
9318
|
+
}),
|
|
9319
|
+
...options.signal ? { signal: options.signal } : {}
|
|
9320
|
+
};
|
|
9321
|
+
const response = await (options.fetchImpl ?? fetch)(githubModelsEndpoint, requestInit);
|
|
9322
|
+
if (!response.ok) {
|
|
9323
|
+
throw new Error(`GitHub Models request failed with HTTP ${response.status}. Check local GitHub Models auth and selected model access.`);
|
|
9324
|
+
}
|
|
9325
|
+
return response.json();
|
|
9326
|
+
}
|
|
9327
|
+
function requireSupportedGithubModelsModelId(modelId) {
|
|
9328
|
+
const trimmed = modelId?.trim();
|
|
9329
|
+
if (!trimmed) {
|
|
9330
|
+
throw new Error("GitHub Models direct provider requires --provider github-models with --model-id.");
|
|
9331
|
+
}
|
|
9332
|
+
if (!githubModelsSupportedModelIds.includes(trimmed)) {
|
|
9333
|
+
throw new Error(`Unsupported GitHub Models direct model: ${trimmed}. Supported models: ${githubModelsSupportedModelIds.join(", ")}.`);
|
|
9334
|
+
}
|
|
9335
|
+
return trimmed;
|
|
9336
|
+
}
|
|
9337
|
+
function resolveGithubModelsToken(env) {
|
|
9338
|
+
const token = env.GITHUB_MODELS_TOKEN?.trim() || env.GITHUB_TOKEN?.trim();
|
|
9339
|
+
if (!token) {
|
|
9340
|
+
throw new Error("GitHub Models direct provider requires GITHUB_MODELS_TOKEN or GITHUB_TOKEN on this runner.");
|
|
9341
|
+
}
|
|
9342
|
+
return token;
|
|
9343
|
+
}
|
|
9344
|
+
function errorMessage5(error) {
|
|
9345
|
+
return error instanceof Error ? error.message : String(error);
|
|
9346
|
+
}
|
|
9347
|
+
|
|
9348
|
+
// src/amistio-direct-agent.ts
|
|
9349
|
+
var defaultMaxToolRounds = 8;
|
|
9350
|
+
async function runAmistioDirectAgent(options) {
|
|
9351
|
+
const preview = createDirectModelClientPreview(options);
|
|
9352
|
+
const messages = [
|
|
9353
|
+
{ role: "system", content: directAgentSystemPrompt(options.toolPolicy) },
|
|
9354
|
+
{ role: "user", content: options.prompt }
|
|
9355
|
+
];
|
|
9356
|
+
let tokensIn = 0;
|
|
9357
|
+
let tokensOut = 0;
|
|
9358
|
+
let lastAssistantContent = "";
|
|
9359
|
+
const maxToolRounds = positiveInteger3(options.maxToolRounds) ?? defaultMaxToolRounds;
|
|
9360
|
+
for (let round = 0; round <= maxToolRounds; round += 1) {
|
|
9361
|
+
const response = await createGithubModelsChatCompletion({
|
|
9362
|
+
modelId: preview.modelId,
|
|
9363
|
+
messages,
|
|
9364
|
+
tools: directAgentToolDefinitions,
|
|
9365
|
+
...options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {},
|
|
9366
|
+
...options.env ? { env: options.env } : {},
|
|
9367
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
|
|
9368
|
+
...options.signal ? { signal: options.signal } : {}
|
|
9369
|
+
});
|
|
9370
|
+
tokensIn += response.usage?.prompt_tokens ?? 0;
|
|
9371
|
+
tokensOut += response.usage?.completion_tokens ?? 0;
|
|
9372
|
+
const message = response.choices?.[0]?.message;
|
|
9373
|
+
const toolCalls = message?.tool_calls ?? [];
|
|
9374
|
+
lastAssistantContent = message?.content ?? lastAssistantContent;
|
|
9375
|
+
if (!toolCalls.length) {
|
|
9376
|
+
if (options.streamOutput && lastAssistantContent) {
|
|
9377
|
+
process.stdout.write(lastAssistantContent);
|
|
9378
|
+
}
|
|
9379
|
+
return {
|
|
9380
|
+
...preview,
|
|
9381
|
+
exitCode: 0,
|
|
9382
|
+
stdout: lastAssistantContent,
|
|
9383
|
+
stderr: "",
|
|
9384
|
+
...tokensIn ? { tokensIn } : {},
|
|
9385
|
+
...tokensOut ? { tokensOut } : {}
|
|
9386
|
+
};
|
|
9387
|
+
}
|
|
9388
|
+
messages.push({ role: "assistant", content: message?.content ?? null, tool_calls: toolCalls });
|
|
9389
|
+
for (const toolCall of toolCalls) {
|
|
9390
|
+
const execution = await executeDirectAgentTool(toolCall, options.toolPolicy);
|
|
9391
|
+
messages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(execution) });
|
|
9392
|
+
}
|
|
9393
|
+
}
|
|
9394
|
+
return {
|
|
9395
|
+
...preview,
|
|
9396
|
+
exitCode: 1,
|
|
9397
|
+
stdout: lastAssistantContent,
|
|
9398
|
+
stderr: `Amistio direct harness stopped after ${maxToolRounds} tool rounds without a final response.`,
|
|
9399
|
+
...tokensIn ? { tokensIn } : {},
|
|
9400
|
+
...tokensOut ? { tokensOut } : {}
|
|
9401
|
+
};
|
|
9402
|
+
}
|
|
9403
|
+
async function executeDirectAgentTool(toolCall, policy) {
|
|
9404
|
+
const args = parseToolArguments(toolCall.function.arguments);
|
|
9405
|
+
const toolName = toolCall.function.name;
|
|
9406
|
+
if (!args.ok) {
|
|
9407
|
+
return { toolName, result: toolFailure(toolName, "command_not_allowed", args.message) };
|
|
9408
|
+
}
|
|
9409
|
+
try {
|
|
9410
|
+
switch (toolName) {
|
|
9411
|
+
case "filesystem_read_file":
|
|
9412
|
+
return { toolName, result: await runFilesystemReadTool({ policy, relativePath: stringArg(args.value, "relativePath") }) };
|
|
9413
|
+
case "filesystem_list_files":
|
|
9414
|
+
return { toolName, result: await runFilesystemListTool({ policy, ...optionalStringField(args.value, "relativeDirectory"), ...optionalNumberField(args.value, "maxFiles") }) };
|
|
9415
|
+
case "filesystem_search_text":
|
|
9416
|
+
return { toolName, result: await runFilesystemSearchTool({ policy, query: stringArg(args.value, "query"), ...optionalStringField(args.value, "relativeDirectory"), ...optionalNumberField(args.value, "maxMatches") }) };
|
|
9417
|
+
case "filesystem_write_file":
|
|
9418
|
+
return { toolName, result: await runFilesystemWriteTool({ policy, relativePath: stringArg(args.value, "relativePath"), content: stringArg(args.value, "content") }) };
|
|
9419
|
+
case "git_status":
|
|
9420
|
+
return { toolName, result: await runGitStatusTool({ policy }) };
|
|
9421
|
+
case "git_diff_name_only":
|
|
9422
|
+
return { toolName, result: await runGitDiffNameOnlyTool({ policy }) };
|
|
9423
|
+
case "package_manager_detect":
|
|
9424
|
+
return { toolName, result: await runPackageManagerDetectTool({ policy }) };
|
|
9425
|
+
case "test_runner_profile":
|
|
9426
|
+
return { toolName, result: await runTestRunnerProfileTool({ policy, ...optionalTestProfileField(args.value, "profileId") }) };
|
|
9427
|
+
case "browser_check":
|
|
9428
|
+
return { toolName, result: await runBrowserCheckTool({ policy, url: stringArg(args.value, "url") }) };
|
|
9429
|
+
default:
|
|
9430
|
+
return { toolName, result: toolFailure(toolName, "command_not_allowed", `Unknown Amistio direct harness tool: ${toolName}.`) };
|
|
9431
|
+
}
|
|
9432
|
+
} catch (error) {
|
|
9433
|
+
return { toolName, result: toolFailure(toolName, "command_not_allowed", error instanceof Error ? error.message : String(error)) };
|
|
9434
|
+
}
|
|
9435
|
+
}
|
|
9436
|
+
var directAgentToolDefinitions = [
|
|
9437
|
+
toolDefinition("filesystem_read_file", "Read a UTF-8 file under the prepared execution root.", {
|
|
9438
|
+
type: "object",
|
|
9439
|
+
additionalProperties: false,
|
|
9440
|
+
required: ["relativePath"],
|
|
9441
|
+
properties: { relativePath: { type: "string" } }
|
|
9442
|
+
}),
|
|
9443
|
+
toolDefinition("filesystem_list_files", "List files under a directory within the prepared execution root.", {
|
|
9444
|
+
type: "object",
|
|
9445
|
+
additionalProperties: false,
|
|
9446
|
+
properties: { relativeDirectory: { type: "string" }, maxFiles: { type: "integer", minimum: 1, maximum: 500 } }
|
|
9447
|
+
}),
|
|
9448
|
+
toolDefinition("filesystem_search_text", "Search text under the prepared execution root with bounded output.", {
|
|
9449
|
+
type: "object",
|
|
9450
|
+
additionalProperties: false,
|
|
9451
|
+
required: ["query"],
|
|
9452
|
+
properties: { query: { type: "string" }, relativeDirectory: { type: "string" }, maxMatches: { type: "integer", minimum: 1, maximum: 200 } }
|
|
9453
|
+
}),
|
|
9454
|
+
toolDefinition("filesystem_write_file", "Write a UTF-8 file under the prepared execution root. Allowed only for mutating harness policy.", {
|
|
9455
|
+
type: "object",
|
|
9456
|
+
additionalProperties: false,
|
|
9457
|
+
required: ["relativePath", "content"],
|
|
9458
|
+
properties: { relativePath: { type: "string" }, content: { type: "string" } }
|
|
9459
|
+
}),
|
|
9460
|
+
toolDefinition("git_status", "Read git status --short for the execution root.", { type: "object", additionalProperties: false, properties: {} }),
|
|
9461
|
+
toolDefinition("git_diff_name_only", "Read changed Git paths for the execution root.", { type: "object", additionalProperties: false, properties: {} }),
|
|
9462
|
+
toolDefinition("package_manager_detect", "Detect local package manager lockfiles and declared verification scripts.", { type: "object", additionalProperties: false, properties: {} }),
|
|
9463
|
+
toolDefinition("test_runner_profile", "Run a declared local verification profile such as test, typecheck, lint, build, or verify.", {
|
|
9464
|
+
type: "object",
|
|
9465
|
+
additionalProperties: false,
|
|
9466
|
+
properties: { profileId: { type: "string", enum: ["verify", "test", "typecheck", "lint", "build"] } }
|
|
9467
|
+
}),
|
|
9468
|
+
toolDefinition("browser_check", "Run a loopback-only HTTP browser smoke check.", {
|
|
9469
|
+
type: "object",
|
|
9470
|
+
additionalProperties: false,
|
|
9471
|
+
required: ["url"],
|
|
9472
|
+
properties: { url: { type: "string" } }
|
|
9473
|
+
})
|
|
9474
|
+
];
|
|
9475
|
+
function directAgentSystemPrompt(policy) {
|
|
9476
|
+
return [
|
|
9477
|
+
"You are the built-in Amistio direct harness running on the user's local runner.",
|
|
9478
|
+
"Use the provided tools to inspect files, make permitted edits, run bounded checks, and then return the final Amistio result requested by the user prompt.",
|
|
9479
|
+
`Mutation policy: ${policy.mutationPolicy}.`,
|
|
9480
|
+
policy.mutationPolicy === "mutating" ? "You may write files only through filesystem_write_file and only with repository-relative paths inside the prepared execution root." : "Do not call filesystem_write_file. This work is read-only.",
|
|
9481
|
+
"Prefer targeted list/search/read operations before edits. Prefer known verification profiles over broad commands. Never ask for arbitrary shell execution, absolute local paths, secrets, provider credentials, or environment dumps.",
|
|
9482
|
+
"Tool outputs are normalized and may be truncated; if more detail is needed, make a narrower tool request.",
|
|
9483
|
+
"When finished, answer with the final task result only. Preserve any structured result contract included in the user prompt."
|
|
9484
|
+
].join("\n");
|
|
9485
|
+
}
|
|
9486
|
+
function toolDefinition(name, description, parameters) {
|
|
9487
|
+
return { type: "function", function: { name, description, parameters } };
|
|
9488
|
+
}
|
|
9489
|
+
function parseToolArguments(value) {
|
|
9490
|
+
try {
|
|
9491
|
+
const parsed = JSON.parse(value || "{}");
|
|
9492
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
9493
|
+
return { ok: true, value: parsed };
|
|
9494
|
+
}
|
|
9495
|
+
return { ok: false, message: "Tool arguments must be a JSON object." };
|
|
9496
|
+
} catch {
|
|
9497
|
+
return { ok: false, message: "Tool arguments must be valid JSON." };
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
function stringArg(args, key) {
|
|
9501
|
+
const value = args[key];
|
|
9502
|
+
if (typeof value !== "string") {
|
|
9503
|
+
throw new Error(`Tool argument ${key} must be a string.`);
|
|
9504
|
+
}
|
|
9505
|
+
return value;
|
|
9506
|
+
}
|
|
9507
|
+
function optionalStringArg(args, key) {
|
|
9508
|
+
const value = args[key];
|
|
9509
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
9510
|
+
}
|
|
9511
|
+
function optionalStringField(args, key) {
|
|
9512
|
+
const value = optionalStringArg(args, key);
|
|
9513
|
+
return value ? { [key]: value } : {};
|
|
9514
|
+
}
|
|
9515
|
+
function optionalNumberArg(args, key) {
|
|
9516
|
+
const value = args[key];
|
|
9517
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
9518
|
+
}
|
|
9519
|
+
function optionalNumberField(args, key) {
|
|
9520
|
+
const value = optionalNumberArg(args, key);
|
|
9521
|
+
return value === void 0 ? {} : { [key]: value };
|
|
9522
|
+
}
|
|
9523
|
+
function optionalTestProfileArg(args, key) {
|
|
9524
|
+
const value = args[key];
|
|
9525
|
+
return value === "verify" || value === "test" || value === "typecheck" || value === "lint" || value === "build" ? value : void 0;
|
|
9526
|
+
}
|
|
9527
|
+
function optionalTestProfileField(args, key) {
|
|
9528
|
+
const value = optionalTestProfileArg(args, key);
|
|
9529
|
+
return value ? { profileId: value } : {};
|
|
9530
|
+
}
|
|
9531
|
+
function toolFailure(toolName, code, message) {
|
|
9532
|
+
return { adapterId: toolName, status: "failed", exitCode: 1, stdout: "", stderr: "", truncated: false, error: { code, message } };
|
|
9533
|
+
}
|
|
9534
|
+
function positiveInteger3(value) {
|
|
9535
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
9536
|
+
}
|
|
9537
|
+
|
|
9538
|
+
// src/harness-adapter.ts
|
|
9539
|
+
var AMISTIO_HARNESS_ID = "amistio";
|
|
9540
|
+
function harnessMutationPolicyForWorkKind(workKind) {
|
|
9541
|
+
return workKind === "implementation" ? "mutating" : "readOnly";
|
|
9542
|
+
}
|
|
9543
|
+
function createHarnessConcurrencyMetadata(executionPolicy, preview) {
|
|
9544
|
+
const serializedResourceKeys = /* @__PURE__ */ new Set([
|
|
9545
|
+
`claim-lane:${executionPolicy.claimLaneId}`,
|
|
9546
|
+
`execution-root:${executionPolicy.executionRoot}`
|
|
9547
|
+
]);
|
|
9548
|
+
if (executionPolicy.mutationPolicy === "mutating") {
|
|
9549
|
+
serializedResourceKeys.add(`repository:${executionPolicy.executionRoot}`);
|
|
9550
|
+
}
|
|
9551
|
+
if (preview.providerId) {
|
|
9552
|
+
serializedResourceKeys.add(`provider:${preview.providerId}`);
|
|
9553
|
+
}
|
|
9554
|
+
if (preview.toolName && preview.toolName !== "custom") {
|
|
9555
|
+
serializedResourceKeys.add(`client:${preview.toolName}`);
|
|
9556
|
+
}
|
|
9557
|
+
return {
|
|
9558
|
+
claimLaneId: executionPolicy.claimLaneId,
|
|
9559
|
+
supportsConcurrentRuns: true,
|
|
9560
|
+
requiresExclusiveRepository: executionPolicy.mutationPolicy === "mutating",
|
|
9561
|
+
requiresExclusiveProviderAuth: Boolean(preview.providerId && preview.toolName !== "amistio-direct"),
|
|
9562
|
+
serializedResourceKeys: [...serializedResourceKeys]
|
|
9563
|
+
};
|
|
9564
|
+
}
|
|
9565
|
+
var builtinAmistioHarnessAdapter = {
|
|
9566
|
+
id: AMISTIO_HARNESS_ID,
|
|
9567
|
+
displayName: "Amistio",
|
|
9568
|
+
async createRunPreview({ executionPolicy, ...toolOptions }) {
|
|
9569
|
+
const preview = shouldUseDirectModelClient(toolOptions) ? createDirectModelClientPreview(toolOptions) : await createToolRunPreview(toolOptions);
|
|
9570
|
+
const concurrency = createHarnessConcurrencyMetadata(executionPolicy, preview);
|
|
9571
|
+
const toolAdapterPolicy = createBoundedToolAdapterPolicy({
|
|
9572
|
+
executionRoot: executionPolicy.executionRoot,
|
|
9573
|
+
mutationPolicy: executionPolicy.mutationPolicy,
|
|
9574
|
+
concurrencyMode: concurrency.requiresExclusiveRepository ? "serialized" : "parallelSafe",
|
|
9575
|
+
resourceKey: `claim-lane:${executionPolicy.claimLaneId}`
|
|
9576
|
+
});
|
|
9577
|
+
return { harnessId: AMISTIO_HARNESS_ID, displayName: "Amistio", executionPolicy, concurrency, toolAdapterPolicy, availableToolAdapters: boundedToolAdapterCatalog, preview };
|
|
9578
|
+
},
|
|
9579
|
+
async executeRun({ preparedRun, ...toolOptions }) {
|
|
9580
|
+
if (shouldUseDirectModelClient(toolOptions)) {
|
|
9581
|
+
return runAmistioDirectAgent({ ...toolOptions, toolPolicy: preparedRun.toolAdapterPolicy });
|
|
9582
|
+
}
|
|
9583
|
+
return runLocalTool(toolOptions);
|
|
9584
|
+
}
|
|
9585
|
+
};
|
|
9586
|
+
|
|
9587
|
+
// src/host-helper-conformance.ts
|
|
9588
|
+
async function runHostHelperConformance(config, cwd = process.cwd()) {
|
|
9589
|
+
const checks = [];
|
|
9590
|
+
const discovery = await discoverNativeHostHelper(config);
|
|
9591
|
+
checks.push({
|
|
9592
|
+
name: "handshake",
|
|
9593
|
+
passed: discovery.ok,
|
|
9594
|
+
detail: discovery.ok ? `protocol ${discovery.helper.handshake.protocolVersion}` : discovery.failure.message
|
|
9595
|
+
});
|
|
9596
|
+
if (!discovery.ok) {
|
|
9597
|
+
return { passed: false, checks };
|
|
9598
|
+
}
|
|
9599
|
+
const port = await createHostExecutionPort({ nativeHelper: config });
|
|
9600
|
+
const execution = await port.executeCommand({ command: process.execPath, args: ["-e", "console.log('amistio conformance')"], cwd, timeoutMs: 2e3, requireNativeHelper: true });
|
|
9601
|
+
checks.push(commandCheck("command execution", execution, (result) => result.status === "completed" && result.exitCode === 0 && result.stdout.includes("amistio conformance")));
|
|
9602
|
+
const boundedOutput = await port.executeCommand({ command: process.execPath, args: ["-e", "console.log('\u20AC\u20AC\u20AC\u20AC')"], cwd, timeoutMs: 2e3, requireNativeHelper: true, outputBudgetBytes: 7 });
|
|
9603
|
+
checks.push(commandCheck("bounded UTF-8 output", boundedOutput, (result) => result.status === "completed" && Buffer.byteLength(result.stdout, "utf8") <= 7 && !result.stdout.includes("\uFFFD")));
|
|
9604
|
+
if (!discovery.helper.handshake.capabilities.pty.supported) {
|
|
9605
|
+
const pty = await port.executeCommand({ command: process.execPath, args: ["-e", "console.log('pty')"], cwd, timeoutMs: 2e3, requireNativeHelper: true, requirePty: true });
|
|
9606
|
+
checks.push(commandCheck("PTY fail-closed", pty, (result) => result.status === "unsupported" && result.error?.code === "pty_unsupported"));
|
|
9607
|
+
} else {
|
|
9608
|
+
checks.push({ name: "PTY fail-closed", passed: true, detail: "Helper advertises PTY support; platform PTY enforcement must be verified by the helper's own tests." });
|
|
9609
|
+
}
|
|
9610
|
+
if (!discovery.helper.handshake.capabilities.sandbox.supported) {
|
|
9611
|
+
const sandbox = await port.executeCommand({ command: process.execPath, args: ["-e", "console.log('sandbox')"], cwd, timeoutMs: 2e3, requireNativeHelper: true, sandbox: "networkDisabled" });
|
|
9612
|
+
checks.push(commandCheck("sandbox fail-closed", sandbox, (result) => result.status === "unsupported" && result.error?.code === "sandbox_unsupported"));
|
|
9613
|
+
} else {
|
|
9614
|
+
checks.push({ name: "sandbox fail-closed", passed: true, detail: "Helper advertises sandbox support; OS enforcement must be verified by the helper's own platform tests." });
|
|
9615
|
+
}
|
|
9616
|
+
return { passed: checks.every((check) => check.passed), checks };
|
|
9617
|
+
}
|
|
9618
|
+
function commandCheck(name, result, predicate) {
|
|
9619
|
+
return {
|
|
9620
|
+
name,
|
|
9621
|
+
passed: predicate(result),
|
|
9622
|
+
detail: result.error?.message ?? `${result.status} exit ${result.exitCode}`
|
|
9623
|
+
};
|
|
9624
|
+
}
|
|
9625
|
+
|
|
9626
|
+
// src/provider-auth.ts
|
|
9627
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
9628
|
+
var githubCopilotProviderId = "github-copilot";
|
|
9629
|
+
var githubCopilotProviderClientId = "github-copilot-sdk";
|
|
9630
|
+
var githubCopilotRouteType = "agentClient";
|
|
9631
|
+
var githubModelsProviderClientId = "github-models-api";
|
|
9632
|
+
var githubModelsRouteType = "directProvider";
|
|
9633
|
+
async function checkProviderAuthLink(input) {
|
|
9634
|
+
if (isGithubModelsDirectRequest(input.request)) {
|
|
9635
|
+
return checkGithubModelsDirectAuth(input);
|
|
9636
|
+
}
|
|
9637
|
+
if (!isGithubCopilotSdkRequest(input.request)) {
|
|
9638
|
+
return providerAuthResult(input.request, "unsupported", "Provider link target is not supported by this runner.", input.now);
|
|
9639
|
+
}
|
|
9640
|
+
const checkedAt = input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9641
|
+
let client;
|
|
9642
|
+
try {
|
|
9643
|
+
client = await (input.createCopilotClient ?? createDefaultCopilotClient)({ cwd: input.root, logLevel: "error", useLoggedInUser: true });
|
|
9644
|
+
await client.start();
|
|
9645
|
+
const authStatus = await client.getAuthStatus();
|
|
9646
|
+
const modelCount = authStatus.isAuthenticated ? await listCopilotModelCount(client) : void 0;
|
|
9647
|
+
const status = {
|
|
9648
|
+
providerId: githubCopilotProviderId,
|
|
9649
|
+
providerClientId: githubCopilotProviderClientId,
|
|
9650
|
+
routeType: githubCopilotRouteType,
|
|
9651
|
+
status: authStatus.isAuthenticated ? "authenticated" : "pendingUserAction",
|
|
9652
|
+
authMethodLabel: authStatus.authType ? `GitHub ${authStatus.authType}` : "GitHub Copilot SDK",
|
|
9653
|
+
...authStatus.host ? { accountHost: authStatus.host } : {},
|
|
9654
|
+
...authStatus.login ? { accountLogin: authStatus.login } : {},
|
|
9655
|
+
...authStatus.host && authStatus.login ? { accountFingerprint: accountFingerprint(authStatus.host, authStatus.login) } : {},
|
|
9656
|
+
...modelCount !== void 0 ? { modelCount } : {},
|
|
9657
|
+
checkedAt,
|
|
9658
|
+
message: authStatus.isAuthenticated ? "GitHub Copilot SDK is authenticated on this runner." : "Complete GitHub Copilot auth locally on the runner machine, then check again."
|
|
9659
|
+
};
|
|
9660
|
+
return { succeeded: true, message: status.message, providerAuthStatus: status };
|
|
9661
|
+
} catch {
|
|
9662
|
+
return providerAuthResult(input.request, "unavailable", "GitHub Copilot SDK auth status is unavailable on this runner.", input.now, "copilot_sdk_unavailable");
|
|
9663
|
+
} finally {
|
|
9664
|
+
await client?.stop().catch(() => void 0);
|
|
9665
|
+
}
|
|
9666
|
+
}
|
|
9667
|
+
function isGithubCopilotSdkRequest(request) {
|
|
9668
|
+
return request.providerId === githubCopilotProviderId && request.providerClientId === githubCopilotProviderClientId && request.routeType === githubCopilotRouteType;
|
|
9669
|
+
}
|
|
9670
|
+
function isGithubModelsDirectRequest(request) {
|
|
9671
|
+
return request.providerId === githubModelsProviderId && request.providerClientId === githubModelsProviderClientId && request.routeType === githubModelsRouteType;
|
|
9672
|
+
}
|
|
9673
|
+
function checkGithubModelsDirectAuth(input) {
|
|
9674
|
+
const checkedAt = input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9675
|
+
const tokenSource = githubModelsTokenSource(input.env ?? process.env);
|
|
9676
|
+
const message = tokenSource ? `${tokenSource} is configured for GitHub Models direct execution on this runner.` : "Set GITHUB_MODELS_TOKEN or GITHUB_TOKEN on this runner to enable GitHub Models direct execution.";
|
|
9677
|
+
const status = {
|
|
9678
|
+
providerId: githubModelsProviderId,
|
|
9679
|
+
providerClientId: githubModelsProviderClientId,
|
|
9680
|
+
routeType: githubModelsRouteType,
|
|
9681
|
+
status: tokenSource ? "authenticated" : "notConfigured",
|
|
9682
|
+
authMethodLabel: tokenSource ?? "GITHUB_MODELS_TOKEN or GITHUB_TOKEN",
|
|
9683
|
+
...tokenSource ? { modelCount: githubModelsSupportedModelIds.length } : {},
|
|
9684
|
+
checkedAt,
|
|
9685
|
+
message
|
|
9686
|
+
};
|
|
9687
|
+
return { succeeded: true, message, providerAuthStatus: status };
|
|
9688
|
+
}
|
|
9689
|
+
function githubModelsTokenSource(env) {
|
|
9690
|
+
if (hasEnvValue(env.GITHUB_MODELS_TOKEN)) return "GITHUB_MODELS_TOKEN";
|
|
9691
|
+
if (hasEnvValue(env.GITHUB_TOKEN)) return "GITHUB_TOKEN";
|
|
9692
|
+
return void 0;
|
|
9693
|
+
}
|
|
9694
|
+
function hasEnvValue(value) {
|
|
9695
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
9696
|
+
}
|
|
9697
|
+
async function createDefaultCopilotClient(options) {
|
|
9698
|
+
const { CopilotClient } = await import("@github/copilot-sdk");
|
|
9699
|
+
return new CopilotClient(options);
|
|
9700
|
+
}
|
|
9701
|
+
async function listCopilotModelCount(client) {
|
|
9702
|
+
try {
|
|
9703
|
+
return (await client.listModels()).length;
|
|
9704
|
+
} catch {
|
|
9705
|
+
return void 0;
|
|
9706
|
+
}
|
|
9707
|
+
}
|
|
9708
|
+
function providerAuthResult(request, status, message, now, errorCode) {
|
|
9709
|
+
return {
|
|
9710
|
+
succeeded: status !== "unsupported",
|
|
9711
|
+
message,
|
|
9712
|
+
providerAuthStatus: {
|
|
9713
|
+
providerId: request.providerId,
|
|
9714
|
+
providerClientId: request.providerClientId,
|
|
9715
|
+
routeType: request.routeType,
|
|
9716
|
+
status,
|
|
9717
|
+
checkedAt: now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9718
|
+
message,
|
|
9719
|
+
...errorCode ? { errorCode } : {}
|
|
9720
|
+
}
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9723
|
+
function accountFingerprint(host, login) {
|
|
9724
|
+
return `sha256:${createHash8("sha256").update(`${host.toLowerCase()}\0${login.toLowerCase()}`).digest("hex")}`;
|
|
9725
|
+
}
|
|
9726
|
+
|
|
8303
9727
|
// src/version.ts
|
|
8304
9728
|
import { readFileSync } from "node:fs";
|
|
8305
9729
|
function readCliPackageVersion() {
|
|
@@ -8434,7 +9858,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
8434
9858
|
});
|
|
8435
9859
|
program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
|
|
8436
9860
|
const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
|
|
8437
|
-
let repositoryLinkId = options.repositoryLink ?? `repo_${
|
|
9861
|
+
let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID2()}`;
|
|
8438
9862
|
let credential = options.token;
|
|
8439
9863
|
if (options.pairingCode) {
|
|
8440
9864
|
const pairing = await new ApiClient({
|
|
@@ -8620,7 +10044,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
8620
10044
|
}
|
|
8621
10045
|
const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
|
|
8622
10046
|
if (options.out) {
|
|
8623
|
-
await
|
|
10047
|
+
await writeFile11(options.out, prompt, "utf8");
|
|
8624
10048
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
8625
10049
|
} else {
|
|
8626
10050
|
console.log(prompt);
|
|
@@ -8634,6 +10058,51 @@ program.command("tools").description("List local AI coding tools that the Amisti
|
|
|
8634
10058
|
}
|
|
8635
10059
|
console.log("custom - pass --tool-command to use any other local runner command.");
|
|
8636
10060
|
});
|
|
10061
|
+
var hostHelper = program.command("host-helper").description("Inspect the optional Amistio host helper used for stronger local execution primitives");
|
|
10062
|
+
hostHelper.command("status").description("Show configured host-helper protocol and capability status").option("--path <path>", "Helper executable path; defaults to AMISTIO_HOST_HELPER_PATH").action(async (options) => {
|
|
10063
|
+
const config = options.path?.trim() ? { command: options.path.trim() } : nativeHostHelperConfigFromEnvironment();
|
|
10064
|
+
console.log(`Protocol: ${hostExecutionProtocolVersion}`);
|
|
10065
|
+
if (!config) {
|
|
10066
|
+
console.log("Helper: not configured");
|
|
10067
|
+
console.log("Default execution: Node host port, no PTY, no sandbox.");
|
|
10068
|
+
console.log("Install @amistio/cli and set AMISTIO_HOST_HELPER_PATH to `amistio-host-helper` only on machines where the reviewed local helper is intended.");
|
|
10069
|
+
return;
|
|
10070
|
+
}
|
|
10071
|
+
console.log(`Helper: ${config.command}`);
|
|
10072
|
+
const discovery = await discoverNativeHostHelper(config);
|
|
10073
|
+
if (!discovery.ok) {
|
|
10074
|
+
console.log(`Status: ${discovery.failure.code}`);
|
|
10075
|
+
console.log(`Reason: ${discovery.failure.message}`);
|
|
10076
|
+
if (discovery.failure.stderr?.trim()) console.log(`stderr: ${truncateLogExcerpt(discovery.failure.stderr)}`);
|
|
10077
|
+
process.exitCode = 1;
|
|
10078
|
+
return;
|
|
10079
|
+
}
|
|
10080
|
+
const { capabilities, secretBoundary } = discovery.helper.handshake;
|
|
10081
|
+
console.log("Status: ready");
|
|
10082
|
+
console.log(`Implementation: ${capabilities.implementation}`);
|
|
10083
|
+
console.log(formatHostHelperCapability("Process groups", capabilities.processGroups));
|
|
10084
|
+
console.log(formatHostHelperCapability("Signal escalation", capabilities.signalEscalation));
|
|
10085
|
+
console.log(formatHostHelperCapability("Stream capture", capabilities.streamCapture));
|
|
10086
|
+
console.log(formatHostHelperCapability("PTY", capabilities.pty));
|
|
10087
|
+
console.log(formatHostHelperCapability("Sandbox", capabilities.sandbox));
|
|
10088
|
+
console.log(`Secret boundary: ${secretBoundary.credentialPolicy}, ${secretBoundary.environmentPolicy}`);
|
|
10089
|
+
});
|
|
10090
|
+
hostHelper.command("conformance").description("Run local compatibility checks against a host helper").option("--path <path>", "Helper executable path; defaults to AMISTIO_HOST_HELPER_PATH").action(async (options) => {
|
|
10091
|
+
const config = options.path?.trim() ? { command: options.path.trim() } : nativeHostHelperConfigFromEnvironment();
|
|
10092
|
+
if (!config) {
|
|
10093
|
+
console.log("No host helper configured. Set AMISTIO_HOST_HELPER_PATH or pass --path.");
|
|
10094
|
+
process.exitCode = 1;
|
|
10095
|
+
return;
|
|
10096
|
+
}
|
|
10097
|
+
const report = await runHostHelperConformance(config);
|
|
10098
|
+
console.log(`Host helper conformance: ${report.passed ? "passed" : "failed"}`);
|
|
10099
|
+
for (const check of report.checks) {
|
|
10100
|
+
console.log(` ${check.passed ? "ok" : "fail"} ${check.name}: ${check.detail}`);
|
|
10101
|
+
}
|
|
10102
|
+
if (!report.passed) {
|
|
10103
|
+
process.exitCode = 1;
|
|
10104
|
+
}
|
|
10105
|
+
});
|
|
8637
10106
|
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) => {
|
|
8638
10107
|
const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
|
|
8639
10108
|
const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
|
|
@@ -8657,7 +10126,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
8657
10126
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
8658
10127
|
...localModelConfig,
|
|
8659
10128
|
streamOutput: options.stream,
|
|
8660
|
-
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${
|
|
10129
|
+
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID2()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
|
|
8661
10130
|
});
|
|
8662
10131
|
if (!options.stream && result.stdout.trim()) {
|
|
8663
10132
|
console.log(result.stdout.trim());
|
|
@@ -8703,7 +10172,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
8703
10172
|
projectId: context.metadata.amistioProjectId,
|
|
8704
10173
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
8705
10174
|
runnerId,
|
|
8706
|
-
rootDir:
|
|
10175
|
+
rootDir: path17.resolve(options.root),
|
|
8707
10176
|
apiUrl: options.apiUrl,
|
|
8708
10177
|
args: buildBackgroundRunnerArgs(resolvedOptions)
|
|
8709
10178
|
});
|
|
@@ -8894,7 +10363,7 @@ runnerService.command("install").description("Install a user-level startup servi
|
|
|
8894
10363
|
projectId: context.metadata.amistioProjectId,
|
|
8895
10364
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
8896
10365
|
runnerId,
|
|
8897
|
-
rootDir:
|
|
10366
|
+
rootDir: path17.resolve(options.root),
|
|
8898
10367
|
apiUrl: options.apiUrl,
|
|
8899
10368
|
args,
|
|
8900
10369
|
platform
|
|
@@ -8910,7 +10379,7 @@ runnerService.command("install").description("Install a user-level startup servi
|
|
|
8910
10379
|
console.log(`Installed startup service ${metadata.serviceName}.`);
|
|
8911
10380
|
console.log(`Service file: ${metadata.serviceFilePath}`);
|
|
8912
10381
|
} catch (error) {
|
|
8913
|
-
console.error(
|
|
10382
|
+
console.error(errorMessage6(error));
|
|
8914
10383
|
process.exitCode = 1;
|
|
8915
10384
|
}
|
|
8916
10385
|
});
|
|
@@ -9053,12 +10522,12 @@ function supportsConcurrentLocalToolExecution(toolConfig) {
|
|
|
9053
10522
|
function aggregateRunnerLaneResults(results) {
|
|
9054
10523
|
const stopResult = results.find((result) => result.stopRunner);
|
|
9055
10524
|
if (stopResult) return stopResult;
|
|
9056
|
-
const
|
|
9057
|
-
if (
|
|
10525
|
+
const failedResult2 = results.find((result) => result.status === "failed");
|
|
10526
|
+
if (failedResult2) return failedResult2;
|
|
9058
10527
|
const blockedResult = results.find((result) => result.status === "blocked");
|
|
9059
10528
|
if (blockedResult) return blockedResult;
|
|
9060
|
-
const
|
|
9061
|
-
if (
|
|
10529
|
+
const completedResult2 = results.find((result) => result.status === "completed" || result.status === "preview");
|
|
10530
|
+
if (completedResult2) return completedResult2;
|
|
9062
10531
|
return results.find((result) => result.status === "idle") ?? { status: "idle", exitCode: 0 };
|
|
9063
10532
|
}
|
|
9064
10533
|
async function runAutoSyncCycle({ context, maxFileKb, quiet, quietDisabled, root, runnerId }) {
|
|
@@ -9110,7 +10579,7 @@ async function runAutoSyncCycle({ context, maxFileKb, quiet, quietDisabled, root
|
|
|
9110
10579
|
}
|
|
9111
10580
|
return { status, message, pushedCount, skippedCount, conflictCount, collection };
|
|
9112
10581
|
} catch (error) {
|
|
9113
|
-
const message = `Auto-sync failed: ${
|
|
10582
|
+
const message = `Auto-sync failed: ${errorMessage6(error)}`;
|
|
9114
10583
|
await context.client.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", { ...heartbeatBase, autoSyncStatus: "failed", autoSyncMessage: message, autoSyncLastFailureAt: startedAt }).catch(() => void 0);
|
|
9115
10584
|
return { status: "failed", message, pushedCount: 0, skippedCount: 0, conflictCount: 0 };
|
|
9116
10585
|
}
|
|
@@ -9213,7 +10682,20 @@ async function runNextWorkItem({
|
|
|
9213
10682
|
...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
|
|
9214
10683
|
});
|
|
9215
10684
|
const resolvedModelConfig = toolConfigModelOptions(toolConfig);
|
|
9216
|
-
const
|
|
10685
|
+
const preparedHarnessRun = await builtinAmistioHarnessAdapter.createRunPreview({
|
|
10686
|
+
rootDir: executionRoot,
|
|
10687
|
+
prompt,
|
|
10688
|
+
tool: toolConfig.tool,
|
|
10689
|
+
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
10690
|
+
...toolCommand ? { toolCommand } : {},
|
|
10691
|
+
...resolvedModelConfig,
|
|
10692
|
+
executionPolicy: {
|
|
10693
|
+
executionRoot,
|
|
10694
|
+
mutationPolicy: harnessMutationPolicyForWorkKind(result.workItem.workKind),
|
|
10695
|
+
claimLaneId
|
|
10696
|
+
}
|
|
10697
|
+
});
|
|
10698
|
+
const preview = preparedHarnessRun.preview;
|
|
9217
10699
|
const sessionContext = await prepareToolSession({
|
|
9218
10700
|
apiClient,
|
|
9219
10701
|
projectId,
|
|
@@ -9237,7 +10719,15 @@ async function runNextWorkItem({
|
|
|
9237
10719
|
status: "running",
|
|
9238
10720
|
summary: `Local runner started ${preview.toolName} execution.`,
|
|
9239
10721
|
idempotencyKey: `runner_milestone_started_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
9240
|
-
metadata: {
|
|
10722
|
+
metadata: {
|
|
10723
|
+
tool: preview.toolName,
|
|
10724
|
+
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
10725
|
+
claimLaneId: preparedHarnessRun.concurrency.claimLaneId,
|
|
10726
|
+
harnessSupportsConcurrentRuns: preparedHarnessRun.concurrency.supportsConcurrentRuns,
|
|
10727
|
+
harnessRequiresExclusiveRepository: preparedHarnessRun.concurrency.requiresExclusiveRepository,
|
|
10728
|
+
harnessRequiresExclusiveProviderAuth: preparedHarnessRun.concurrency.requiresExclusiveProviderAuth,
|
|
10729
|
+
harnessSerializedResourceKeys: preparedHarnessRun.concurrency.serializedResourceKeys
|
|
10730
|
+
}
|
|
9241
10731
|
});
|
|
9242
10732
|
const startedAt = Date.now();
|
|
9243
10733
|
const providerSessionStore = new LocalToolSessionStore();
|
|
@@ -9245,7 +10735,8 @@ async function runNextWorkItem({
|
|
|
9245
10735
|
let toolResult;
|
|
9246
10736
|
const stopLeaseRenewal = startWorkLeaseRenewal({ apiClient, projectId, repositoryLinkId, runnerId, toolConfig, workItem: result.workItem, telemetry: isolationTelemetry, heartbeatConcurrency });
|
|
9247
10737
|
try {
|
|
9248
|
-
toolResult = await
|
|
10738
|
+
toolResult = await builtinAmistioHarnessAdapter.executeRun({
|
|
10739
|
+
preparedRun: preparedHarnessRun,
|
|
9249
10740
|
rootDir: executionRoot,
|
|
9250
10741
|
prompt,
|
|
9251
10742
|
tool: toolConfig.tool,
|
|
@@ -9270,7 +10761,7 @@ async function runNextWorkItem({
|
|
|
9270
10761
|
const message = `${preview.toolName} failed before returning a result.`;
|
|
9271
10762
|
const settlements = await Promise.allSettled([
|
|
9272
10763
|
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig, currentRunnerMode(), heartbeatConcurrency)),
|
|
9273
|
-
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession,
|
|
10764
|
+
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage6(error)),
|
|
9274
10765
|
apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "failed", `run_failed_${result.workItem.workItemId}_${result.workItem.attempt}_${runnerId}`, runnerId, {
|
|
9275
10766
|
...isolationTelemetry,
|
|
9276
10767
|
tool: preview.toolName,
|
|
@@ -9564,7 +11055,7 @@ async function runNextWorkItem({
|
|
|
9564
11055
|
projectId,
|
|
9565
11056
|
result.workItem.workItemId,
|
|
9566
11057
|
finalStatus,
|
|
9567
|
-
`run_${result.workItem.workItemId}_${
|
|
11058
|
+
`run_${result.workItem.workItemId}_${randomUUID2()}`,
|
|
9568
11059
|
runnerId,
|
|
9569
11060
|
{
|
|
9570
11061
|
tool: preview.toolName,
|
|
@@ -9654,11 +11145,11 @@ async function prepareWorktreeForClaimedItem({ apiClient, heartbeatConcurrency,
|
|
|
9654
11145
|
});
|
|
9655
11146
|
return { status: "ready", isolation };
|
|
9656
11147
|
} catch (error) {
|
|
9657
|
-
const message =
|
|
11148
|
+
const message = errorMessage6(error);
|
|
9658
11149
|
const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
|
|
9659
11150
|
const finalAttempt = workItem.attempt >= maxPreflightAttempts;
|
|
9660
11151
|
const statusMessage = finalAttempt ? `Git worktree preflight failed after ${workItem.attempt}/${maxPreflightAttempts} attempts. ${message}` : `Git worktree preflight attempt ${workItem.attempt}/${maxPreflightAttempts} failed. Requeueing for retry. ${message}`;
|
|
9661
|
-
const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${
|
|
11152
|
+
const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, finalAttempt ? "failed" : "approved", `worktree_${finalAttempt ? "failed" : "retry"}_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`, runnerId, {
|
|
9662
11153
|
...telemetry,
|
|
9663
11154
|
message: statusMessage,
|
|
9664
11155
|
...finalAttempt ? { blockerReason: message } : { releaseClaim: true },
|
|
@@ -9726,8 +11217,8 @@ async function recordFinalizationFailure({ apiClient, durationMs, error, isolati
|
|
|
9726
11217
|
const message = `${toolName} completed, but Amistio could not finalize the result. ${safeFinalizationFailureSummary(error)}`;
|
|
9727
11218
|
const settlements = await Promise.allSettled([
|
|
9728
11219
|
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
9729
|
-
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession,
|
|
9730
|
-
apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${
|
|
11220
|
+
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage6(error)),
|
|
11221
|
+
apiClient.updateWorkStatus(projectId, workItem.workItemId, "failed", `finalize_failed_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`, runnerId, {
|
|
9731
11222
|
...isolationTelemetry,
|
|
9732
11223
|
tool: toolName,
|
|
9733
11224
|
durationMs,
|
|
@@ -9845,7 +11336,7 @@ function autopilotWorkMetadata2(workItem) {
|
|
|
9845
11336
|
function logRejectedSettlements(action, settlements) {
|
|
9846
11337
|
for (const settlement of settlements) {
|
|
9847
11338
|
if (settlement.status === "rejected") {
|
|
9848
|
-
console.error(`${action} failed: ${
|
|
11339
|
+
console.error(`${action} failed: ${errorMessage6(settlement.reason)}`);
|
|
9849
11340
|
}
|
|
9850
11341
|
}
|
|
9851
11342
|
}
|
|
@@ -9858,7 +11349,13 @@ async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
|
9858
11349
|
await updateRunnerCommandStatus(apiClient, context, command, "acknowledged", "Command acknowledged by local runner.");
|
|
9859
11350
|
await updateRunnerCommandStatus(apiClient, context, command, "running", `Running ${runnerCommandLabel(command.commandKind)} command.`);
|
|
9860
11351
|
const result = await executeRunnerCommand(apiClient, command, context);
|
|
9861
|
-
await updateRunnerCommandStatus(apiClient, context, command, result.succeeded ? "completed" : "failed", result.message, result.error);
|
|
11352
|
+
await updateRunnerCommandStatus(apiClient, context, command, result.succeeded ? "completed" : "failed", result.message, result.error, result.providerAuthStatus);
|
|
11353
|
+
if (result.providerAuthStatus) {
|
|
11354
|
+
await apiClient.sendRunnerHeartbeat(context.projectId, context.runnerId, context.repositoryLinkId, "online", {
|
|
11355
|
+
...heartbeatMetadata,
|
|
11356
|
+
providerAuthStatuses: mergeProviderAuthStatuses(heartbeatMetadata.providerAuthStatuses, result.providerAuthStatus)
|
|
11357
|
+
}).catch(() => void 0);
|
|
11358
|
+
}
|
|
9862
11359
|
if (command.commandKind === "remove" && result.succeeded) {
|
|
9863
11360
|
await new LocalCredentialStore().delete(credentialKey(context.accountId, context.projectId, context.repositoryLinkId));
|
|
9864
11361
|
}
|
|
@@ -9867,14 +11364,15 @@ async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
|
9867
11364
|
}
|
|
9868
11365
|
return { handled: true, succeeded: result.succeeded, message: result.message, ...result.stopRunner ? { stopRunner: true } : {} };
|
|
9869
11366
|
}
|
|
9870
|
-
async function updateRunnerCommandStatus(apiClient, context, command, status, message, error) {
|
|
11367
|
+
async function updateRunnerCommandStatus(apiClient, context, command, status, message, error, providerAuthStatus) {
|
|
9871
11368
|
const result = await apiClient.updateRunnerCommand(context.projectId, command.commandId, {
|
|
9872
11369
|
runnerId: context.runnerId,
|
|
9873
11370
|
repositoryLinkId: context.repositoryLinkId,
|
|
9874
11371
|
status,
|
|
9875
|
-
idempotencyKey: `runner_command_${command.commandId}_${status}_${
|
|
11372
|
+
idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
|
|
9876
11373
|
message,
|
|
9877
|
-
...error ? { error } : {}
|
|
11374
|
+
...error ? { error } : {},
|
|
11375
|
+
...providerAuthStatus ? { providerAuthStatus } : {}
|
|
9878
11376
|
});
|
|
9879
11377
|
return result.command;
|
|
9880
11378
|
}
|
|
@@ -9888,11 +11386,21 @@ async function executeRunnerCommand(apiClient, command, context) {
|
|
|
9888
11386
|
if (command.commandKind === "implementationHandoffRecovery") {
|
|
9889
11387
|
return executeImplementationHandoffRecoveryCommand(apiClient, command, context);
|
|
9890
11388
|
}
|
|
11389
|
+
if (command.commandKind === "providerAuthLinkRequested") {
|
|
11390
|
+
return executeProviderAuthLinkCommand(command, context);
|
|
11391
|
+
}
|
|
9891
11392
|
return runOfficialCliUpdateWithRuntimeRefresh({
|
|
9892
11393
|
mode: currentRunnerMode(),
|
|
9893
11394
|
restartBackgroundRunner: () => restartCurrentRunner(context, { useUpdatedCliExecutable: true })
|
|
9894
11395
|
});
|
|
9895
11396
|
}
|
|
11397
|
+
async function executeProviderAuthLinkCommand(command, context) {
|
|
11398
|
+
if (!command.providerAuthLinkRequest) {
|
|
11399
|
+
return { succeeded: false, message: "Provider link command is missing provider auth metadata." };
|
|
11400
|
+
}
|
|
11401
|
+
const result = await checkProviderAuthLink({ request: command.providerAuthLinkRequest, root: context.root });
|
|
11402
|
+
return { succeeded: result.succeeded, message: result.message, providerAuthStatus: result.providerAuthStatus };
|
|
11403
|
+
}
|
|
9896
11404
|
async function executeImplementationHandoffRecoveryCommand(apiClient, command, context) {
|
|
9897
11405
|
if (!command.workItemId || !command.handoffRecoveryAction) {
|
|
9898
11406
|
return { succeeded: false, message: "Handoff recovery command is missing a scoped work item or action." };
|
|
@@ -9916,7 +11424,7 @@ async function executeImplementationHandoffRecoveryCommand(apiClient, command, c
|
|
|
9916
11424
|
}
|
|
9917
11425
|
return { succeeded: false, message: "Handoff recovery action is not a runner-local command." };
|
|
9918
11426
|
} catch (error) {
|
|
9919
|
-
return { succeeded: false, message: "Handoff recovery command failed locally.", error:
|
|
11427
|
+
return { succeeded: false, message: "Handoff recovery command failed locally.", error: errorMessage6(error) };
|
|
9920
11428
|
}
|
|
9921
11429
|
}
|
|
9922
11430
|
async function retryImplementationHandoff(apiClient, context, workItem) {
|
|
@@ -9950,7 +11458,7 @@ async function findRecoveryWorkItem(apiClient, projectId, workItemId) {
|
|
|
9950
11458
|
return workItems.find((item) => item.workItemId === workItemId);
|
|
9951
11459
|
}
|
|
9952
11460
|
async function submitRecoveredHandoff(apiClient, context, workItem, handoff, action) {
|
|
9953
|
-
await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${
|
|
11461
|
+
await apiClient.updateWorkStatus(context.projectId, workItem.workItemId, recoveredHandoffWorkStatus(handoff), `handoff_recovery_${workItem.workItemId}_${action}_${randomUUID2()}`, context.runnerId, {
|
|
9954
11462
|
implementationHandoff: handoff,
|
|
9955
11463
|
...handoff.message ? { message: handoff.message } : {},
|
|
9956
11464
|
...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
|
|
@@ -9984,15 +11492,20 @@ async function restartCurrentRunner(context, options = {}) {
|
|
|
9984
11492
|
const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs, options.useUpdatedCliExecutable ? updatedCliRunnerLaunchOptions() : {});
|
|
9985
11493
|
return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
|
|
9986
11494
|
} catch (error) {
|
|
9987
|
-
return { succeeded: false, message: "Background restart failed.", error:
|
|
11495
|
+
return { succeeded: false, message: "Background restart failed.", error: errorMessage6(error) };
|
|
9988
11496
|
}
|
|
9989
11497
|
}
|
|
9990
11498
|
function runnerCommandLabel(commandKind) {
|
|
9991
11499
|
if (commandKind === "update") return "update";
|
|
9992
11500
|
if (commandKind === "restart") return "restart";
|
|
9993
11501
|
if (commandKind === "implementationHandoffRecovery") return "handoff recovery";
|
|
11502
|
+
if (commandKind === "providerAuthLinkRequested") return "provider auth link";
|
|
9994
11503
|
return "remove";
|
|
9995
11504
|
}
|
|
11505
|
+
function mergeProviderAuthStatuses(existingStatuses, nextStatus) {
|
|
11506
|
+
const statuses = existingStatuses?.filter((status) => status.providerId !== nextStatus.providerId || status.providerClientId !== nextStatus.providerClientId || status.routeType !== nextStatus.routeType) ?? [];
|
|
11507
|
+
return [...statuses, nextStatus].slice(-20);
|
|
11508
|
+
}
|
|
9996
11509
|
async function replayPendingBrainGenerationFinalizations({ accountId, apiClient, projectId, repositoryLinkId, runnerId }) {
|
|
9997
11510
|
const pendingEntries = await listPendingBrainGenerationFinalizations({ accountId, projectId, repositoryLinkId, runnerId });
|
|
9998
11511
|
if (!pendingEntries.length) {
|
|
@@ -10038,7 +11551,7 @@ async function submitBrainGenerationFinalizationEntry(apiClient, entry, options
|
|
|
10038
11551
|
try {
|
|
10039
11552
|
result = await apiClient.submitBrainGenerationResult(entry.projectId, entry.workItemId, entry.result);
|
|
10040
11553
|
} catch (error) {
|
|
10041
|
-
const detail = truncateLogExcerpt(
|
|
11554
|
+
const detail = truncateLogExcerpt(errorMessage6(error));
|
|
10042
11555
|
if (isRetryableApiError(error)) {
|
|
10043
11556
|
const updated = await markBrainGenerationFinalizationRetry(entry, detail);
|
|
10044
11557
|
const message2 = `Pending brain generation finalization ${entry.workItemId} could not be replayed yet (${updated.retryCount} attempt${updated.retryCount === 1 ? "" : "s"}): ${detail}`;
|
|
@@ -10051,7 +11564,7 @@ async function submitBrainGenerationFinalizationEntry(apiClient, entry, options
|
|
|
10051
11564
|
return { status: "failed", message, retryable: false };
|
|
10052
11565
|
}
|
|
10053
11566
|
await deleteBrainGenerationFinalizationEntry(entry).catch((error) => {
|
|
10054
|
-
console.error(`delete pending brain generation finalization ${entry.workItemId} failed: ${
|
|
11567
|
+
console.error(`delete pending brain generation finalization ${entry.workItemId} failed: ${errorMessage6(error)}`);
|
|
10055
11568
|
});
|
|
10056
11569
|
if (options.recordReplayTelemetry) {
|
|
10057
11570
|
await recordRunnerMilestone(apiClient, entry.projectId, result.workItem, entry.runnerId, entry.repositoryLinkId, {
|
|
@@ -10072,7 +11585,7 @@ async function submitDurableResultFinalizationEntry(apiClient, entry, options =
|
|
|
10072
11585
|
try {
|
|
10073
11586
|
response = await submitDurableResultMutation(apiClient, entry);
|
|
10074
11587
|
} catch (error) {
|
|
10075
|
-
const detail = truncateLogExcerpt(
|
|
11588
|
+
const detail = truncateLogExcerpt(errorMessage6(error));
|
|
10076
11589
|
if (isRetryableApiError(error)) {
|
|
10077
11590
|
const updated = await markDurableResultFinalizationRetry(entry, detail);
|
|
10078
11591
|
const message2 = `Pending ${runnerResultFinalizationLabel(entry)} finalization ${entry.workItemId} could not be replayed yet (${updated.retryCount} attempt${updated.retryCount === 1 ? "" : "s"}): ${detail}`;
|
|
@@ -10086,7 +11599,7 @@ async function submitDurableResultFinalizationEntry(apiClient, entry, options =
|
|
|
10086
11599
|
}
|
|
10087
11600
|
const workItem = durableResultResponseWorkItem(response);
|
|
10088
11601
|
await deleteDurableResultFinalizationEntry(entry).catch((error) => {
|
|
10089
|
-
console.error(`delete pending ${runnerResultFinalizationLabel(entry)} finalization ${entry.workItemId} failed: ${
|
|
11602
|
+
console.error(`delete pending ${runnerResultFinalizationLabel(entry)} finalization ${entry.workItemId} failed: ${errorMessage6(error)}`);
|
|
10090
11603
|
});
|
|
10091
11604
|
if (options.recordReplayTelemetry) {
|
|
10092
11605
|
await recordRunnerMilestone(apiClient, entry.projectId, workItem, entry.runnerId, entry.repositoryLinkId, {
|
|
@@ -10211,7 +11724,7 @@ async function finalizeBrainGenerationWork({
|
|
|
10211
11724
|
artifacts = parseBrainGenerationArtifacts(`${toolResult.stdout}
|
|
10212
11725
|
${toolResult.stderr}`);
|
|
10213
11726
|
} catch (error) {
|
|
10214
|
-
generationError =
|
|
11727
|
+
generationError = errorMessage6(error);
|
|
10215
11728
|
}
|
|
10216
11729
|
} else {
|
|
10217
11730
|
generationError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10229,7 +11742,7 @@ ${toolResult.stderr}`);
|
|
|
10229
11742
|
const resultMutation = {
|
|
10230
11743
|
status: "completed",
|
|
10231
11744
|
runnerId,
|
|
10232
|
-
idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${
|
|
11745
|
+
idempotencyKey: `generation_${workItem.workItemId}_${workItem.attempt}_${randomUUID2()}`,
|
|
10233
11746
|
artifacts,
|
|
10234
11747
|
tool: toolName,
|
|
10235
11748
|
durationMs,
|
|
@@ -10278,7 +11791,7 @@ ${toolResult.stderr}`);
|
|
|
10278
11791
|
metadata: { tool: toolName, durationMs, artifactCount: replay.documentCount, verificationSummary: "Generated artifacts were accepted by the Amistio API for review." }
|
|
10279
11792
|
});
|
|
10280
11793
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)).catch((error) => {
|
|
10281
|
-
console.error(`send generation completion heartbeat failed: ${
|
|
11794
|
+
console.error(`send generation completion heartbeat failed: ${errorMessage6(error)}`);
|
|
10282
11795
|
});
|
|
10283
11796
|
console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${replay.documentCount} brain artifact${replay.documentCount === 1 ? "" : "s"} for review.`);
|
|
10284
11797
|
return { status: "completed", exitCode: 0 };
|
|
@@ -10301,10 +11814,10 @@ ${toolResult.stderr}`);
|
|
|
10301
11814
|
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
10302
11815
|
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
10303
11816
|
};
|
|
10304
|
-
const
|
|
11817
|
+
const failedResult2 = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
10305
11818
|
status: "failed",
|
|
10306
11819
|
runnerId,
|
|
10307
|
-
idempotencyKey: `generation_${workItem.workItemId}_${
|
|
11820
|
+
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
|
|
10308
11821
|
tool: toolName,
|
|
10309
11822
|
durationMs,
|
|
10310
11823
|
...failedSessionTelemetry,
|
|
@@ -10314,7 +11827,7 @@ ${toolResult.stderr}`);
|
|
|
10314
11827
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10315
11828
|
status: "failed",
|
|
10316
11829
|
summary: generationError ?? `${toolName} did not produce valid brain artifacts.`,
|
|
10317
|
-
idempotencyKey: `runner_milestone_generation_failed_${workItem.workItemId}_${
|
|
11830
|
+
idempotencyKey: `runner_milestone_generation_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10318
11831
|
metadata: { tool: toolName, durationMs, verificationSummary: "Generation output could not be parsed into approved artifact JSON." }
|
|
10319
11832
|
});
|
|
10320
11833
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10343,7 +11856,7 @@ async function finalizeAssistantQuestionWork({
|
|
|
10343
11856
|
answerResult = parseAssistantAnswerResult(`${toolResult.stdout}
|
|
10344
11857
|
${toolResult.stderr}`);
|
|
10345
11858
|
} catch (error) {
|
|
10346
|
-
answerError =
|
|
11859
|
+
answerError = errorMessage6(error);
|
|
10347
11860
|
}
|
|
10348
11861
|
} else {
|
|
10349
11862
|
answerError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10354,7 +11867,7 @@ ${toolResult.stderr}`);
|
|
|
10354
11867
|
const resultMutation = {
|
|
10355
11868
|
status: "completed",
|
|
10356
11869
|
runnerId,
|
|
10357
|
-
idempotencyKey: `assistant_${workItem.workItemId}_${
|
|
11870
|
+
idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID2()}`,
|
|
10358
11871
|
answer: answerResult.answer,
|
|
10359
11872
|
sourceBoundary: answerResult.sourceBoundary,
|
|
10360
11873
|
citations: answerResult.citations,
|
|
@@ -10390,14 +11903,14 @@ ${toolResult.stderr}`);
|
|
|
10390
11903
|
const failedMutation = {
|
|
10391
11904
|
status: "failed",
|
|
10392
11905
|
runnerId,
|
|
10393
|
-
idempotencyKey: `assistant_${workItem.workItemId}_${
|
|
11906
|
+
idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID2()}`,
|
|
10394
11907
|
tool: toolName,
|
|
10395
11908
|
durationMs,
|
|
10396
11909
|
...sessionTelemetry,
|
|
10397
11910
|
message: `${toolName} did not produce a valid project knowledge answer.`,
|
|
10398
11911
|
...answerError ? { error: answerError } : {}
|
|
10399
11912
|
};
|
|
10400
|
-
const
|
|
11913
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10401
11914
|
accountId: workItem.accountId,
|
|
10402
11915
|
projectId,
|
|
10403
11916
|
repositoryLinkId,
|
|
@@ -10409,12 +11922,12 @@ ${toolResult.stderr}`);
|
|
|
10409
11922
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10410
11923
|
result: failedMutation
|
|
10411
11924
|
});
|
|
10412
|
-
if (!
|
|
11925
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
10413
11926
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
10414
11927
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10415
11928
|
status: "failed",
|
|
10416
11929
|
summary: answerError ?? `${toolName} did not produce a valid project knowledge answer.`,
|
|
10417
|
-
idempotencyKey: `runner_milestone_assistant_failed_${workItem.workItemId}_${
|
|
11930
|
+
idempotencyKey: `runner_milestone_assistant_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10418
11931
|
metadata: { tool: toolName, durationMs, verificationSummary: "Assistant output did not include a valid structured answer." }
|
|
10419
11932
|
});
|
|
10420
11933
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10441,7 +11954,7 @@ async function finalizeImpactPreviewWork({
|
|
|
10441
11954
|
report = parseImpactPreviewResult(`${toolResult.stdout}
|
|
10442
11955
|
${toolResult.stderr}`);
|
|
10443
11956
|
} catch (error) {
|
|
10444
|
-
previewError =
|
|
11957
|
+
previewError = errorMessage6(error);
|
|
10445
11958
|
}
|
|
10446
11959
|
} else {
|
|
10447
11960
|
previewError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10453,7 +11966,7 @@ ${toolResult.stderr}`);
|
|
|
10453
11966
|
const resultMutation = {
|
|
10454
11967
|
status: "completed",
|
|
10455
11968
|
runnerId,
|
|
10456
|
-
idempotencyKey: `impact_${workItem.workItemId}_${
|
|
11969
|
+
idempotencyKey: `impact_${workItem.workItemId}_${randomUUID2()}`,
|
|
10457
11970
|
report: {
|
|
10458
11971
|
...report,
|
|
10459
11972
|
analyzedRepoRevision: report.analyzedRepoRevision ?? metadata?.lastSyncedRevision
|
|
@@ -10490,14 +12003,14 @@ ${toolResult.stderr}`);
|
|
|
10490
12003
|
const failedMutation = {
|
|
10491
12004
|
status: "failed",
|
|
10492
12005
|
runnerId,
|
|
10493
|
-
idempotencyKey: `impact_${workItem.workItemId}_${
|
|
12006
|
+
idempotencyKey: `impact_${workItem.workItemId}_${randomUUID2()}`,
|
|
10494
12007
|
tool: toolName,
|
|
10495
12008
|
durationMs,
|
|
10496
12009
|
...sessionTelemetry,
|
|
10497
12010
|
message: `${toolName} did not produce a valid impact preview.`,
|
|
10498
12011
|
...previewError ? { error: previewError } : {}
|
|
10499
12012
|
};
|
|
10500
|
-
const
|
|
12013
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10501
12014
|
accountId: workItem.accountId,
|
|
10502
12015
|
projectId,
|
|
10503
12016
|
repositoryLinkId,
|
|
@@ -10509,12 +12022,12 @@ ${toolResult.stderr}`);
|
|
|
10509
12022
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10510
12023
|
result: failedMutation
|
|
10511
12024
|
});
|
|
10512
|
-
if (!
|
|
12025
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
10513
12026
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
10514
12027
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10515
12028
|
status: "failed",
|
|
10516
12029
|
summary: previewError ?? `${toolName} did not produce a valid impact preview.`,
|
|
10517
|
-
idempotencyKey: `runner_milestone_impact_failed_${workItem.workItemId}_${
|
|
12030
|
+
idempotencyKey: `runner_milestone_impact_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10518
12031
|
metadata: { tool: toolName, durationMs, verificationSummary: "Impact preview output did not include valid structured JSON." }
|
|
10519
12032
|
});
|
|
10520
12033
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10540,7 +12053,7 @@ async function finalizeIssueDiagnosisWork({
|
|
|
10540
12053
|
diagnosis = parseIssueDiagnosisResult(`${toolResult.stdout}
|
|
10541
12054
|
${toolResult.stderr}`);
|
|
10542
12055
|
} catch (error) {
|
|
10543
|
-
diagnosisError =
|
|
12056
|
+
diagnosisError = errorMessage6(error);
|
|
10544
12057
|
}
|
|
10545
12058
|
} else {
|
|
10546
12059
|
diagnosisError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10551,7 +12064,7 @@ ${toolResult.stderr}`);
|
|
|
10551
12064
|
const resultMutation = {
|
|
10552
12065
|
status: "completed",
|
|
10553
12066
|
runnerId,
|
|
10554
|
-
idempotencyKey: `issue_${workItem.workItemId}_${
|
|
12067
|
+
idempotencyKey: `issue_${workItem.workItemId}_${randomUUID2()}`,
|
|
10555
12068
|
diagnosis,
|
|
10556
12069
|
tool: toolName,
|
|
10557
12070
|
durationMs,
|
|
@@ -10585,14 +12098,14 @@ ${toolResult.stderr}`);
|
|
|
10585
12098
|
const failedMutation = {
|
|
10586
12099
|
status: "failed",
|
|
10587
12100
|
runnerId,
|
|
10588
|
-
idempotencyKey: `issue_${workItem.workItemId}_${
|
|
12101
|
+
idempotencyKey: `issue_${workItem.workItemId}_${randomUUID2()}`,
|
|
10589
12102
|
tool: toolName,
|
|
10590
12103
|
durationMs,
|
|
10591
12104
|
...sessionTelemetry,
|
|
10592
12105
|
message: `${toolName} did not produce a valid issue diagnosis.`,
|
|
10593
12106
|
...diagnosisError ? { error: diagnosisError } : {}
|
|
10594
12107
|
};
|
|
10595
|
-
const
|
|
12108
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10596
12109
|
accountId: workItem.accountId,
|
|
10597
12110
|
projectId,
|
|
10598
12111
|
repositoryLinkId,
|
|
@@ -10604,12 +12117,12 @@ ${toolResult.stderr}`);
|
|
|
10604
12117
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10605
12118
|
result: failedMutation
|
|
10606
12119
|
});
|
|
10607
|
-
if (!
|
|
12120
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
10608
12121
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
10609
12122
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10610
12123
|
status: "failed",
|
|
10611
12124
|
summary: diagnosisError ?? `${toolName} did not produce a valid issue diagnosis.`,
|
|
10612
|
-
idempotencyKey: `runner_milestone_issue_failed_${workItem.workItemId}_${
|
|
12125
|
+
idempotencyKey: `runner_milestone_issue_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10613
12126
|
metadata: { tool: toolName, durationMs, verificationSummary: "Issue diagnosis output did not include valid structured JSON." }
|
|
10614
12127
|
});
|
|
10615
12128
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10635,7 +12148,7 @@ async function finalizeSecurityPostureScanWork({
|
|
|
10635
12148
|
scanResult = parseSecurityPostureScanResult(`${toolResult.stdout}
|
|
10636
12149
|
${toolResult.stderr}`);
|
|
10637
12150
|
} catch (error) {
|
|
10638
|
-
scanError =
|
|
12151
|
+
scanError = errorMessage6(error);
|
|
10639
12152
|
}
|
|
10640
12153
|
} else {
|
|
10641
12154
|
scanError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10646,7 +12159,7 @@ ${toolResult.stderr}`);
|
|
|
10646
12159
|
const resultMutation = {
|
|
10647
12160
|
status: "completed",
|
|
10648
12161
|
runnerId,
|
|
10649
|
-
idempotencyKey: `security_${workItem.workItemId}_${
|
|
12162
|
+
idempotencyKey: `security_${workItem.workItemId}_${randomUUID2()}`,
|
|
10650
12163
|
result: scanResult,
|
|
10651
12164
|
tool: toolName,
|
|
10652
12165
|
durationMs,
|
|
@@ -10680,14 +12193,14 @@ ${toolResult.stderr}`);
|
|
|
10680
12193
|
const failedMutation = {
|
|
10681
12194
|
status: "failed",
|
|
10682
12195
|
runnerId,
|
|
10683
|
-
idempotencyKey: `security_${workItem.workItemId}_${
|
|
12196
|
+
idempotencyKey: `security_${workItem.workItemId}_${randomUUID2()}`,
|
|
10684
12197
|
tool: toolName,
|
|
10685
12198
|
durationMs,
|
|
10686
12199
|
...sessionTelemetry,
|
|
10687
12200
|
message: `${toolName} did not produce a valid security posture scan.`,
|
|
10688
12201
|
...scanError ? { error: scanError } : {}
|
|
10689
12202
|
};
|
|
10690
|
-
const
|
|
12203
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10691
12204
|
accountId: workItem.accountId,
|
|
10692
12205
|
projectId,
|
|
10693
12206
|
repositoryLinkId,
|
|
@@ -10699,12 +12212,12 @@ ${toolResult.stderr}`);
|
|
|
10699
12212
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10700
12213
|
result: failedMutation
|
|
10701
12214
|
});
|
|
10702
|
-
if (!
|
|
12215
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
10703
12216
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
10704
12217
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10705
12218
|
status: "failed",
|
|
10706
12219
|
summary: scanError ?? `${toolName} did not produce a valid security posture scan.`,
|
|
10707
|
-
idempotencyKey: `runner_milestone_security_failed_${workItem.workItemId}_${
|
|
12220
|
+
idempotencyKey: `runner_milestone_security_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10708
12221
|
metadata: { tool: toolName, durationMs, verificationSummary: "Security posture output did not include valid structured JSON." }
|
|
10709
12222
|
});
|
|
10710
12223
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10730,7 +12243,7 @@ async function finalizeAppEvaluationScanWork({
|
|
|
10730
12243
|
scanResult = parseAppEvaluationScanResult(`${toolResult.stdout}
|
|
10731
12244
|
${toolResult.stderr}`);
|
|
10732
12245
|
} catch (error) {
|
|
10733
|
-
scanError =
|
|
12246
|
+
scanError = errorMessage6(error);
|
|
10734
12247
|
}
|
|
10735
12248
|
} else {
|
|
10736
12249
|
scanError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10741,7 +12254,7 @@ ${toolResult.stderr}`);
|
|
|
10741
12254
|
const resultMutation = {
|
|
10742
12255
|
status: "completed",
|
|
10743
12256
|
runnerId,
|
|
10744
|
-
idempotencyKey: `app_evaluation_${workItem.workItemId}_${
|
|
12257
|
+
idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID2()}`,
|
|
10745
12258
|
result: scanResult,
|
|
10746
12259
|
tool: toolName,
|
|
10747
12260
|
durationMs,
|
|
@@ -10775,14 +12288,14 @@ ${toolResult.stderr}`);
|
|
|
10775
12288
|
const failedMutation = {
|
|
10776
12289
|
status: "failed",
|
|
10777
12290
|
runnerId,
|
|
10778
|
-
idempotencyKey: `app_evaluation_${workItem.workItemId}_${
|
|
12291
|
+
idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID2()}`,
|
|
10779
12292
|
tool: toolName,
|
|
10780
12293
|
durationMs,
|
|
10781
12294
|
...sessionTelemetry,
|
|
10782
12295
|
message: `${toolName} did not produce a valid app evaluation scan.`,
|
|
10783
12296
|
...scanError ? { error: scanError } : {}
|
|
10784
12297
|
};
|
|
10785
|
-
const
|
|
12298
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10786
12299
|
accountId: workItem.accountId,
|
|
10787
12300
|
projectId,
|
|
10788
12301
|
repositoryLinkId,
|
|
@@ -10794,12 +12307,12 @@ ${toolResult.stderr}`);
|
|
|
10794
12307
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10795
12308
|
result: failedMutation
|
|
10796
12309
|
});
|
|
10797
|
-
if (!
|
|
12310
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
10798
12311
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
10799
12312
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10800
12313
|
status: "failed",
|
|
10801
12314
|
summary: scanError ?? `${toolName} did not produce a valid app evaluation scan.`,
|
|
10802
|
-
idempotencyKey: `runner_milestone_app_evaluation_failed_${workItem.workItemId}_${
|
|
12315
|
+
idempotencyKey: `runner_milestone_app_evaluation_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10803
12316
|
metadata: { tool: toolName, durationMs, verificationSummary: "App evaluation output did not include valid structured JSON." }
|
|
10804
12317
|
});
|
|
10805
12318
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10825,7 +12338,7 @@ async function finalizeBrainConsolidationScanWork({
|
|
|
10825
12338
|
scanResult = parseBrainConsolidationScanResult(`${toolResult.stdout}
|
|
10826
12339
|
${toolResult.stderr}`);
|
|
10827
12340
|
} catch (error) {
|
|
10828
|
-
scanError =
|
|
12341
|
+
scanError = errorMessage6(error);
|
|
10829
12342
|
}
|
|
10830
12343
|
} else {
|
|
10831
12344
|
scanError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10836,7 +12349,7 @@ ${toolResult.stderr}`);
|
|
|
10836
12349
|
const resultMutation = {
|
|
10837
12350
|
status: "completed",
|
|
10838
12351
|
runnerId,
|
|
10839
|
-
idempotencyKey: `brain_consolidation_${workItem.workItemId}_${
|
|
12352
|
+
idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID2()}`,
|
|
10840
12353
|
result: scanResult,
|
|
10841
12354
|
tool: toolName,
|
|
10842
12355
|
durationMs,
|
|
@@ -10870,14 +12383,14 @@ ${toolResult.stderr}`);
|
|
|
10870
12383
|
const failedMutation = {
|
|
10871
12384
|
status: "failed",
|
|
10872
12385
|
runnerId,
|
|
10873
|
-
idempotencyKey: `brain_consolidation_${workItem.workItemId}_${
|
|
12386
|
+
idempotencyKey: `brain_consolidation_${workItem.workItemId}_${randomUUID2()}`,
|
|
10874
12387
|
tool: toolName,
|
|
10875
12388
|
durationMs,
|
|
10876
12389
|
...sessionTelemetry,
|
|
10877
12390
|
message: `${toolName} did not produce a valid semantic brain consolidation scan.`,
|
|
10878
12391
|
...scanError ? { error: scanError } : {}
|
|
10879
12392
|
};
|
|
10880
|
-
const
|
|
12393
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10881
12394
|
accountId: workItem.accountId,
|
|
10882
12395
|
projectId,
|
|
10883
12396
|
repositoryLinkId,
|
|
@@ -10889,12 +12402,12 @@ ${toolResult.stderr}`);
|
|
|
10889
12402
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10890
12403
|
result: failedMutation
|
|
10891
12404
|
});
|
|
10892
|
-
if (!
|
|
12405
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
10893
12406
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
10894
12407
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
10895
12408
|
status: "failed",
|
|
10896
12409
|
summary: scanError ?? `${toolName} did not produce a valid semantic brain consolidation scan.`,
|
|
10897
|
-
idempotencyKey: `runner_milestone_brain_consolidation_failed_${workItem.workItemId}_${
|
|
12410
|
+
idempotencyKey: `runner_milestone_brain_consolidation_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
10898
12411
|
metadata: { tool: toolName, durationMs, verificationSummary: "Brain consolidation output did not include valid structured JSON." }
|
|
10899
12412
|
});
|
|
10900
12413
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -10921,7 +12434,7 @@ async function finalizeProjectContextRefreshWork({
|
|
|
10921
12434
|
refreshResult = parseProjectContextRefreshResult(`${toolResult.stdout}
|
|
10922
12435
|
${toolResult.stderr}`, { repositoryRoot: executionRoot });
|
|
10923
12436
|
} catch (error) {
|
|
10924
|
-
refreshError =
|
|
12437
|
+
refreshError = errorMessage6(error);
|
|
10925
12438
|
}
|
|
10926
12439
|
} else {
|
|
10927
12440
|
refreshError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -10932,7 +12445,7 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
|
|
|
10932
12445
|
const resultMutation = {
|
|
10933
12446
|
status: "completed",
|
|
10934
12447
|
runnerId,
|
|
10935
|
-
idempotencyKey: `project_context_${workItem.workItemId}_${
|
|
12448
|
+
idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID2()}`,
|
|
10936
12449
|
result: refreshResult,
|
|
10937
12450
|
tool: toolName,
|
|
10938
12451
|
durationMs,
|
|
@@ -10978,14 +12491,14 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
|
|
|
10978
12491
|
const failedMutation = {
|
|
10979
12492
|
status: "failed",
|
|
10980
12493
|
runnerId,
|
|
10981
|
-
idempotencyKey: `project_context_${workItem.workItemId}_${
|
|
12494
|
+
idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID2()}`,
|
|
10982
12495
|
tool: toolName,
|
|
10983
12496
|
durationMs,
|
|
10984
12497
|
...sessionTelemetry,
|
|
10985
12498
|
message: `${toolName} did not produce a valid project context refresh.`,
|
|
10986
12499
|
...refreshError ? { error: refreshError } : {}
|
|
10987
12500
|
};
|
|
10988
|
-
const
|
|
12501
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
10989
12502
|
accountId: workItem.accountId,
|
|
10990
12503
|
projectId,
|
|
10991
12504
|
repositoryLinkId,
|
|
@@ -10997,12 +12510,12 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
|
|
|
10997
12510
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
10998
12511
|
result: failedMutation
|
|
10999
12512
|
});
|
|
11000
|
-
if (!
|
|
12513
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
11001
12514
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
11002
12515
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
11003
12516
|
status: "failed",
|
|
11004
12517
|
summary: refreshError ?? `${toolName} did not produce a valid project context refresh.`,
|
|
11005
|
-
idempotencyKey: `runner_milestone_project_context_failed_${workItem.workItemId}_${
|
|
12518
|
+
idempotencyKey: `runner_milestone_project_context_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
11006
12519
|
metadata: { tool: toolName, durationMs, verificationSummary: "Project context output did not include valid structured JSON." }
|
|
11007
12520
|
});
|
|
11008
12521
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -11028,7 +12541,7 @@ async function finalizeImplementationVerificationWork({
|
|
|
11028
12541
|
verificationResult = parseImplementationVerificationResult(`${toolResult.stdout}
|
|
11029
12542
|
${toolResult.stderr}`);
|
|
11030
12543
|
} catch (error) {
|
|
11031
|
-
verificationError =
|
|
12544
|
+
verificationError = errorMessage6(error);
|
|
11032
12545
|
}
|
|
11033
12546
|
} else {
|
|
11034
12547
|
verificationError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -11039,7 +12552,7 @@ ${toolResult.stderr}`);
|
|
|
11039
12552
|
const resultMutation = {
|
|
11040
12553
|
status: "completed",
|
|
11041
12554
|
runnerId,
|
|
11042
|
-
idempotencyKey: `implementation_verification_${workItem.workItemId}_${
|
|
12555
|
+
idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID2()}`,
|
|
11043
12556
|
result: verificationResult,
|
|
11044
12557
|
tool: toolName,
|
|
11045
12558
|
durationMs,
|
|
@@ -11073,14 +12586,14 @@ ${toolResult.stderr}`);
|
|
|
11073
12586
|
const failedMutation = {
|
|
11074
12587
|
status: "failed",
|
|
11075
12588
|
runnerId,
|
|
11076
|
-
idempotencyKey: `implementation_verification_${workItem.workItemId}_${
|
|
12589
|
+
idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID2()}`,
|
|
11077
12590
|
tool: toolName,
|
|
11078
12591
|
durationMs,
|
|
11079
12592
|
...sessionTelemetry,
|
|
11080
12593
|
message: `${toolName} did not produce valid implementation verification proof.`,
|
|
11081
12594
|
...verificationError ? { error: verificationError } : {}
|
|
11082
12595
|
};
|
|
11083
|
-
const
|
|
12596
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
11084
12597
|
accountId: workItem.accountId,
|
|
11085
12598
|
projectId,
|
|
11086
12599
|
repositoryLinkId,
|
|
@@ -11092,12 +12605,12 @@ ${toolResult.stderr}`);
|
|
|
11092
12605
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
11093
12606
|
result: failedMutation
|
|
11094
12607
|
});
|
|
11095
|
-
if (!
|
|
12608
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
11096
12609
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
11097
12610
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
11098
12611
|
status: "failed",
|
|
11099
12612
|
summary: verificationError ?? `${toolName} did not produce valid implementation verification proof.`,
|
|
11100
|
-
idempotencyKey: `runner_milestone_implementation_verification_failed_${workItem.workItemId}_${
|
|
12613
|
+
idempotencyKey: `runner_milestone_implementation_verification_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
11101
12614
|
metadata: { tool: toolName, durationMs, verificationSummary: "Implementation verification output did not include valid structured JSON." }
|
|
11102
12615
|
});
|
|
11103
12616
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -11123,7 +12636,7 @@ async function finalizeTestQualityScanWork({
|
|
|
11123
12636
|
scanResult = parseTestQualityScanResult(`${toolResult.stdout}
|
|
11124
12637
|
${toolResult.stderr}`);
|
|
11125
12638
|
} catch (error) {
|
|
11126
|
-
scanError =
|
|
12639
|
+
scanError = errorMessage6(error);
|
|
11127
12640
|
}
|
|
11128
12641
|
} else {
|
|
11129
12642
|
scanError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -11134,7 +12647,7 @@ ${toolResult.stderr}`);
|
|
|
11134
12647
|
const resultMutation = {
|
|
11135
12648
|
status: "completed",
|
|
11136
12649
|
runnerId,
|
|
11137
|
-
idempotencyKey: `test_quality_${workItem.workItemId}_${
|
|
12650
|
+
idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID2()}`,
|
|
11138
12651
|
result: scanResult,
|
|
11139
12652
|
tool: toolName,
|
|
11140
12653
|
durationMs,
|
|
@@ -11168,14 +12681,14 @@ ${toolResult.stderr}`);
|
|
|
11168
12681
|
const failedMutation = {
|
|
11169
12682
|
status: "failed",
|
|
11170
12683
|
runnerId,
|
|
11171
|
-
idempotencyKey: `test_quality_${workItem.workItemId}_${
|
|
12684
|
+
idempotencyKey: `test_quality_${workItem.workItemId}_${randomUUID2()}`,
|
|
11172
12685
|
tool: toolName,
|
|
11173
12686
|
durationMs,
|
|
11174
12687
|
...sessionTelemetry,
|
|
11175
12688
|
message: `${toolName} did not produce a valid test quality scan.`,
|
|
11176
12689
|
...scanError ? { error: scanError } : {}
|
|
11177
12690
|
};
|
|
11178
|
-
const
|
|
12691
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
11179
12692
|
accountId: workItem.accountId,
|
|
11180
12693
|
projectId,
|
|
11181
12694
|
repositoryLinkId,
|
|
@@ -11187,12 +12700,12 @@ ${toolResult.stderr}`);
|
|
|
11187
12700
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
11188
12701
|
result: failedMutation
|
|
11189
12702
|
});
|
|
11190
|
-
if (!
|
|
12703
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
11191
12704
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
11192
12705
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
11193
12706
|
status: "failed",
|
|
11194
12707
|
summary: scanError ?? `${toolName} did not produce a valid test quality scan.`,
|
|
11195
|
-
idempotencyKey: `runner_milestone_test_quality_failed_${workItem.workItemId}_${
|
|
12708
|
+
idempotencyKey: `runner_milestone_test_quality_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
11196
12709
|
metadata: { tool: toolName, durationMs, verificationSummary: "Test quality output did not include valid structured JSON." }
|
|
11197
12710
|
});
|
|
11198
12711
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -11218,7 +12731,7 @@ async function finalizeImplementationTestGateWork({
|
|
|
11218
12731
|
gateResult = parseImplementationTestGateResult(`${toolResult.stdout}
|
|
11219
12732
|
${toolResult.stderr}`);
|
|
11220
12733
|
} catch (error) {
|
|
11221
|
-
gateError =
|
|
12734
|
+
gateError = errorMessage6(error);
|
|
11222
12735
|
}
|
|
11223
12736
|
} else {
|
|
11224
12737
|
gateError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -11229,7 +12742,7 @@ ${toolResult.stderr}`);
|
|
|
11229
12742
|
const resultMutation = {
|
|
11230
12743
|
status: "completed",
|
|
11231
12744
|
runnerId,
|
|
11232
|
-
idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${
|
|
12745
|
+
idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID2()}`,
|
|
11233
12746
|
result: gateResult,
|
|
11234
12747
|
tool: toolName,
|
|
11235
12748
|
durationMs,
|
|
@@ -11263,14 +12776,14 @@ ${toolResult.stderr}`);
|
|
|
11263
12776
|
const failedMutation = {
|
|
11264
12777
|
status: "failed",
|
|
11265
12778
|
runnerId,
|
|
11266
|
-
idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${
|
|
12779
|
+
idempotencyKey: `implementation_test_gate_${workItem.workItemId}_${randomUUID2()}`,
|
|
11267
12780
|
tool: toolName,
|
|
11268
12781
|
durationMs,
|
|
11269
12782
|
...sessionTelemetry,
|
|
11270
12783
|
message: `${toolName} did not produce a valid implementation test gate result.`,
|
|
11271
12784
|
...gateError ? { error: gateError } : {}
|
|
11272
12785
|
};
|
|
11273
|
-
const
|
|
12786
|
+
const failedResult2 = await submitPrimaryRunnerResult(apiClient, {
|
|
11274
12787
|
accountId: workItem.accountId,
|
|
11275
12788
|
projectId,
|
|
11276
12789
|
repositoryLinkId,
|
|
@@ -11282,12 +12795,12 @@ ${toolResult.stderr}`);
|
|
|
11282
12795
|
idempotencyKey: failedMutation.idempotencyKey,
|
|
11283
12796
|
result: failedMutation
|
|
11284
12797
|
});
|
|
11285
|
-
if (!
|
|
12798
|
+
if (!failedResult2) return { status: "failed", exitCode: 1 };
|
|
11286
12799
|
await finalizeToolSessionBestEffort({ apiClient, projectId, runnerId, sessionContext, status: finalStatus, toolResult, workItemId: workItem.workItemId });
|
|
11287
12800
|
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
11288
12801
|
status: "failed",
|
|
11289
12802
|
summary: gateError ?? `${toolName} did not produce a valid implementation test gate result.`,
|
|
11290
|
-
idempotencyKey: `runner_milestone_implementation_test_gate_failed_${workItem.workItemId}_${
|
|
12803
|
+
idempotencyKey: `runner_milestone_implementation_test_gate_failed_${workItem.workItemId}_${failedResult2.workItem.idempotencyKey}`,
|
|
11291
12804
|
metadata: { tool: toolName, durationMs, verificationSummary: "Implementation test gate output did not include valid structured JSON." }
|
|
11292
12805
|
});
|
|
11293
12806
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
@@ -11492,7 +13005,7 @@ async function prepareToolSession({
|
|
|
11492
13005
|
});
|
|
11493
13006
|
return { ...selection, toolSession: toolSession2 };
|
|
11494
13007
|
}
|
|
11495
|
-
const toolSessionId = `tool_session_${
|
|
13008
|
+
const toolSessionId = `tool_session_${randomUUID2()}`;
|
|
11496
13009
|
const { toolSession } = await apiClient.createToolSession(projectId, {
|
|
11497
13010
|
toolSessionId,
|
|
11498
13011
|
repositoryLinkId,
|
|
@@ -11586,7 +13099,7 @@ function summarizeToolOutput(value) {
|
|
|
11586
13099
|
}
|
|
11587
13100
|
return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed;
|
|
11588
13101
|
}
|
|
11589
|
-
function
|
|
13102
|
+
function errorMessage6(error) {
|
|
11590
13103
|
return error instanceof Error ? error.message : String(error);
|
|
11591
13104
|
}
|
|
11592
13105
|
function errorDetail(error) {
|
|
@@ -11596,6 +13109,9 @@ function truncateLogExcerpt(value) {
|
|
|
11596
13109
|
const trimmed = value.trim();
|
|
11597
13110
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
11598
13111
|
}
|
|
13112
|
+
function formatHostHelperCapability(label, support) {
|
|
13113
|
+
return `${label}: ${support.supported ? "supported" : "unsupported"}${support.reason ? ` (${support.reason})` : ""}`;
|
|
13114
|
+
}
|
|
11599
13115
|
function collectRepeatedOption(value, previous) {
|
|
11600
13116
|
return [...previous, value];
|
|
11601
13117
|
}
|
|
@@ -11622,10 +13138,10 @@ function parseReasoningEffort(value) {
|
|
|
11622
13138
|
throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
|
|
11623
13139
|
}
|
|
11624
13140
|
function inferRepoName(root) {
|
|
11625
|
-
return
|
|
13141
|
+
return path17.basename(path17.resolve(root)) || "repository";
|
|
11626
13142
|
}
|
|
11627
13143
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
11628
|
-
return
|
|
13144
|
+
return createHash9("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
11629
13145
|
}
|
|
11630
13146
|
function defaultApiUrl() {
|
|
11631
13147
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -11660,6 +13176,13 @@ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, e
|
|
|
11660
13176
|
}
|
|
11661
13177
|
if (explicitTool === "none") {
|
|
11662
13178
|
const modelConfig2 = normalizeModelConfig({ model: explicitModel, providerId: explicitProviderId, modelId: explicitModelId, modelVariant: explicitModelVariant, reasoningEffort: explicitReasoningEffort });
|
|
13179
|
+
const directModelResolution = resolveDirectModelClientPreference({ ...modelConfig2, tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto" });
|
|
13180
|
+
if (directModelResolution) {
|
|
13181
|
+
if (!directModelResolution.ready) {
|
|
13182
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: directModelResolution.status, message: directModelResolution.message, tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...modelConfig2 });
|
|
13183
|
+
}
|
|
13184
|
+
return { ready: true, tool: "none", capabilities, source: "cli", status: "resolved", requestedInvocationChannel: explicitInvocationChannel ?? "auto", effectiveInvocationChannel: "sdk", ...directModelResolution.config, message: "Using Amistio direct model client." };
|
|
13185
|
+
}
|
|
11663
13186
|
if (hasModelConfig(modelConfig2)) {
|
|
11664
13187
|
return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "Model configuration cannot be used with --tool none.", tool: "none", requestedInvocationChannel: explicitInvocationChannel ?? "auto", ...modelConfig2 });
|
|
11665
13188
|
}
|
|
@@ -11684,6 +13207,13 @@ async function resolveRunnerToolConfig({ apiClient, explicitInvocationChannel, e
|
|
|
11684
13207
|
}
|
|
11685
13208
|
function resolveRequestedTool({ capabilities, modelConfig, requestedInvocationChannel, requestedTool, source }) {
|
|
11686
13209
|
const needsModelSelection = hasModelConfig(modelConfig);
|
|
13210
|
+
const directModelResolution = resolveDirectModelClientPreference({ ...modelConfig, tool: requestedTool, requestedInvocationChannel });
|
|
13211
|
+
if (directModelResolution) {
|
|
13212
|
+
if (!directModelResolution.ready) {
|
|
13213
|
+
return unavailableToolConfig({ capabilities, source, status: directModelResolution.status, requestedTool, requestedInvocationChannel, tool: requestedTool, ...modelConfig, message: directModelResolution.message });
|
|
13214
|
+
}
|
|
13215
|
+
return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, requestedInvocationChannel, effectiveInvocationChannel: "sdk", ...directModelResolution.config, message: "Using Amistio direct model client." };
|
|
13216
|
+
}
|
|
11687
13217
|
if (requestedTool === "auto") {
|
|
11688
13218
|
const candidate = capabilities.find((capability2) => capability2.available && capabilitySupportsInvocationChannel(capability2, requestedInvocationChannel) && (!needsModelSelection || capability2.supportsModelSelection));
|
|
11689
13219
|
if (!candidate) {
|
|
@@ -11910,7 +13440,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode(), concurr
|
|
|
11910
13440
|
};
|
|
11911
13441
|
}
|
|
11912
13442
|
function runnerMachineId() {
|
|
11913
|
-
return
|
|
13443
|
+
return createHash9("sha256").update(`${os8.hostname()}:${os8.platform()}:${os8.arch()}`).digest("hex").slice(0, 20);
|
|
11914
13444
|
}
|
|
11915
13445
|
async function delay(milliseconds) {
|
|
11916
13446
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -11919,4 +13449,3 @@ program.parseAsync().catch((error) => {
|
|
|
11919
13449
|
console.error(error instanceof Error ? error.message : String(error));
|
|
11920
13450
|
process.exitCode = 1;
|
|
11921
13451
|
});
|
|
11922
|
-
//# sourceMappingURL=index.js.map
|