specrails-core 6.4.0 → 6.5.0
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/dist/agent-runtime/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +1 -1
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +30 -2
- package/dist/agent-runtime/compact/environment.js +120 -2
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +54 -21
- package/dist/agent-runtime/engine/pieces/verify.js +3 -5
- package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
- package/dist/agent-runtime/session/application/active-session.js +701 -0
- package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
- package/dist/agent-runtime/session/application/delegation.js +171 -0
- package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
- package/dist/agent-runtime/session/application/output-buffer.js +65 -0
- package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
- package/dist/agent-runtime/session/application/session-service.js +256 -0
- package/dist/agent-runtime/session/application/timers.d.ts +15 -0
- package/dist/agent-runtime/session/application/timers.js +35 -0
- package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
- package/dist/agent-runtime/session/domain/descriptor.js +32 -0
- package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
- package/dist/agent-runtime/session/domain/errors.js +16 -0
- package/dist/agent-runtime/session/domain/events.d.ts +147 -0
- package/dist/agent-runtime/session/domain/events.js +24 -0
- package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
- package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
- package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
- package/dist/agent-runtime/session/domain/interruption.js +39 -0
- package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
- package/dist/agent-runtime/session/domain/policy.js +104 -0
- package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
- package/dist/agent-runtime/session/domain/snapshot.js +234 -0
- package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
- package/dist/agent-runtime/session/domain/state-machines.js +41 -0
- package/dist/agent-runtime/session/domain/types.d.ts +130 -0
- package/dist/agent-runtime/session/domain/types.js +15 -0
- package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
- package/dist/agent-runtime/session/domain/usage.js +86 -0
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
- package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
- package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
- package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
- package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
- package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
- package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
- package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
- package/dist/agent-runtime/session/drivers/common/process.js +116 -0
- package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
- package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
- package/dist/agent-runtime/session/drivers/registry.js +38 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
- package/dist/agent-runtime/session/host/protocol.js +88 -0
- package/dist/agent-runtime/session/host/server.d.ts +57 -0
- package/dist/agent-runtime/session/host/server.js +229 -0
- package/dist/agent-runtime/session/index.d.ts +45 -0
- package/dist/agent-runtime/session/index.js +74 -0
- package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
- package/dist/agent-runtime/session/journal/schema.js +44 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
- package/dist/agent-runtime/session/ports.d.ts +192 -0
- package/dist/agent-runtime/session/ports.js +1 -0
- package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
- package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
- package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
- package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
- package/dist/agent-runtime/session/testing/index.d.ts +9 -0
- package/dist/agent-runtime/session/testing/index.js +9 -0
- package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
- package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
- package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
- package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
- package/dist/agent-runtime/verification-proposals.d.ts +9 -0
- package/dist/agent-runtime/verification-proposals.js +69 -0
- package/dist/installer/util/registry.js +3 -3
- package/dist/shared/specrails-home.d.ts +21 -0
- package/dist/shared/specrails-home.js +36 -0
- package/integration-contract.json +42 -1
- package/package.json +11 -1
|
@@ -36,6 +36,16 @@ export async function cliCapabilities(provider, model, options = {}) {
|
|
|
36
36
|
return { ...result, effortSupport: 'supported', supportedEfforts: levels };
|
|
37
37
|
}
|
|
38
38
|
else if (model && help.stdout.includes('--config')) {
|
|
39
|
+
// Ask the installed client for its own model catalog. Other Codex clients
|
|
40
|
+
// (the desktop app's app-server daemon, IDE extensions) share
|
|
41
|
+
// models_cache.json and rewrite it with their own version, so the cache
|
|
42
|
+
// is only a fallback for clients without `codex debug models`.
|
|
43
|
+
const catalog = await runner({ command: provider, args: ['debug', 'models'] }, { cwd: tmpdir(), env, timeoutMs: 10_000 }).catch(() => null);
|
|
44
|
+
if (catalog?.exitCode === 0 && catalog.stdout.length <= 2 * 1024 * 1024) {
|
|
45
|
+
const levels = codexModelEfforts(JSON.parse(catalog.stdout), model);
|
|
46
|
+
if (levels)
|
|
47
|
+
return { ...result, effortSupport: 'supported', supportedEfforts: levels };
|
|
48
|
+
}
|
|
39
49
|
// Use the installed client's own bounded, fresh model catalog. Do not
|
|
40
50
|
// infer reasoning support from a model prefix or another provider.
|
|
41
51
|
const file = path.join(env.CODEX_HOME ?? path.join(env.HOME ?? homedir(), '.codex'), 'models_cache.json');
|
|
@@ -48,16 +58,23 @@ export async function cliCapabilities(provider, model, options = {}) {
|
|
|
48
58
|
const version = await runner({ command: provider, args: ['--version'] }, { cwd: tmpdir(), env, timeoutMs: 10_000 });
|
|
49
59
|
if (version.exitCode !== 0 || version.stdout.trim() !== `codex-cli ${cache.client_version}`)
|
|
50
60
|
return result;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return { ...result, effortSupport: 'supported', supportedEfforts: [...new Set(levels)] };
|
|
55
|
-
}
|
|
61
|
+
const levels = codexModelEfforts(cache, model);
|
|
62
|
+
if (levels)
|
|
63
|
+
return { ...result, effortSupport: 'supported', supportedEfforts: levels };
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
catch { /* Missing/stale metadata is unknown, never optimistic support. */ }
|
|
59
67
|
return result;
|
|
60
68
|
}
|
|
69
|
+
/** Reasoning levels a Codex catalog (`{ models: [...] }`) declares for one model; null when unknown or malformed. */
|
|
70
|
+
function codexModelEfforts(catalog, model) {
|
|
71
|
+
const models = catalog?.models;
|
|
72
|
+
const entry = Array.isArray(models) ? models.find((item) => item?.slug === model) : undefined;
|
|
73
|
+
const levels = entry?.supported_reasoning_levels?.map?.((item) => item?.effort);
|
|
74
|
+
if (!Array.isArray(levels) || !levels.length || !levels.every(value => typeof value === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(value)))
|
|
75
|
+
return null;
|
|
76
|
+
return [...new Set(levels)];
|
|
77
|
+
}
|
|
61
78
|
export function assertEffortSupported(selection, capability) {
|
|
62
79
|
if (selection.effort !== undefined && (capability.effortSupport !== 'supported' || !capability.supportedEfforts?.includes(selection.effort))) {
|
|
63
80
|
throw new AgentExecutionError(`Requested effort '${selection.effort}' is not confirmed for ${capability.transport}. Select a supported effort or provider default.`, 'provider_capability_unsupported');
|
|
@@ -32,7 +32,7 @@ export function buildCliInvocation(provider, request, options = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
const readOnly = requestPolicy(request).access === 'read';
|
|
34
34
|
// Pin the product alias; explicit IDs remain reproducible across releases.
|
|
35
|
-
const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5' };
|
|
35
|
+
const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' };
|
|
36
36
|
const modelId = provider === 'claude' && request.model ? claudeModelIds[request.model] ?? request.model : request.model;
|
|
37
37
|
const model = modelId ? ['--model', modelId] : [];
|
|
38
38
|
const extraRoots = request.allowedRoots.filter(root => root !== request.cwd);
|
|
@@ -54,14 +54,19 @@ export function buildCliInvocation(provider, request, options = {}) {
|
|
|
54
54
|
...(resume ? ['--resume', resume] : []),
|
|
55
55
|
] };
|
|
56
56
|
case 'codex': {
|
|
57
|
-
|
|
57
|
+
// Writers run without Codex's OS sandbox, like the Claude developer
|
|
58
|
+
// (--dangerously-skip-permissions) and Desktop's legacy rail runs: the
|
|
59
|
+
// workspace-write seatbelt denies what real verification needs (a
|
|
60
|
+
// localhost listen, Chromium's Mach bootstrap registration, the browser
|
|
61
|
+
// cache). Read-only roles keep the read-only sandbox.
|
|
62
|
+
const sandbox = readOnly ? 'read-only' : 'danger-full-access';
|
|
58
63
|
const common = ['--json', '--skip-git-repo-check', '-c', 'approval_policy="never"', ...model,
|
|
59
64
|
...(request.effort === undefined ? [] : ['-c', 'model_reasoning_effort=' + JSON.stringify(request.effort)]),
|
|
60
65
|
...(options.openspecBridge ? ['-c', 'mcp_servers.specrails_openspec.command=' + JSON.stringify(options.openspecBridge.command), '-c', 'mcp_servers.specrails_openspec.args=' + JSON.stringify(options.openspecBridge.args), '-c', 'mcp_servers.specrails_openspec.default_tools_approval_mode="approve"', '-c', 'mcp_servers.specrails_openspec.required=true'] : []),
|
|
61
66
|
];
|
|
62
67
|
// `codex exec resume` has no --sandbox flag; the same policy travels as a config override.
|
|
63
68
|
if (resume)
|
|
64
|
-
return { command: 'codex', stdin: request.prompt, args: ['exec', 'resume', ...common, '-c', `sandbox_mode="${sandbox}"`, '-c', 'sandbox_workspace_write.writable_roots='
|
|
69
|
+
return { command: 'codex', stdin: request.prompt, args: ['exec', 'resume', ...common, '-c', `sandbox_mode="${sandbox}"`, ...(readOnly ? ['-c', 'sandbox_workspace_write.writable_roots=[]'] : []), resume, '-'] };
|
|
65
70
|
return { command: 'codex', stdin: request.prompt, args: [
|
|
66
71
|
'exec', ...common, '--sandbox', sandbox,
|
|
67
72
|
...(options.codexSchemaFile ? ['--output-schema', options.codexSchemaFile] : []),
|
|
@@ -10,7 +10,7 @@ export declare function runtimeFailure(error: unknown): {
|
|
|
10
10
|
};
|
|
11
11
|
};
|
|
12
12
|
/** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
|
|
13
|
-
export declare const RUNTIME_CLI_OPERATIONS: readonly ["api", "validate", "run", "status", "resume", "prompts", "capabilities", "evidence", "recovery", "evaluate", "workflows", "fork", "signal", "cancel", "help"];
|
|
13
|
+
export declare const RUNTIME_CLI_OPERATIONS: readonly ["api", "validate", "run", "status", "resume", "prompts", "capabilities", "evidence", "recovery", "evaluate", "workflows", "fork", "signal", "cancel", "host", "help"];
|
|
14
14
|
/** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
|
|
15
15
|
export declare function compactState(state: WorkflowState | null): {
|
|
16
16
|
runId: string;
|
|
@@ -18,14 +18,58 @@ import { configuredCapabilities } from './capabilities.js';
|
|
|
18
18
|
import { createExecutorRegistry } from './executors.js';
|
|
19
19
|
import { runRecovery } from './recovery.js';
|
|
20
20
|
import { EngineError } from './engine/contracts.js';
|
|
21
|
+
/**
|
|
22
|
+
* Long-lived agent session host (docs/agent-sessions/protocol.md). Loaded lazily
|
|
23
|
+
* so batch operations never initialize session drivers or the journal.
|
|
24
|
+
*/
|
|
25
|
+
async function runSessionHost(flags) {
|
|
26
|
+
if (flags.stdio !== true)
|
|
27
|
+
throw new Error('runtime host requires --stdio');
|
|
28
|
+
if (flags.scope !== undefined && typeof flags.scope !== 'string')
|
|
29
|
+
throw new Error('--scope requires a value');
|
|
30
|
+
const [{ createSessionRuntime, GLOBAL_SESSION_SCOPE }, { SessionHost }, { CliExecutor }] = await Promise.all([
|
|
31
|
+
import('./session/index.js'),
|
|
32
|
+
import('./session/host/server.js'),
|
|
33
|
+
import('./cli-executor.js'),
|
|
34
|
+
]);
|
|
35
|
+
const scope = flags.scope ?? GLOBAL_SESSION_SCOPE;
|
|
36
|
+
let host = null;
|
|
37
|
+
const runtime = await createSessionRuntime({
|
|
38
|
+
scope,
|
|
39
|
+
drivers: {
|
|
40
|
+
executors: [
|
|
41
|
+
{ id: 'gemini', displayName: 'Gemini CLI', executor: new CliExecutor('gemini') },
|
|
42
|
+
{ id: 'kimi', displayName: 'Kimi Code', executor: new CliExecutor('kimi') },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
onLeaseLost: () => host?.leaseLost(),
|
|
46
|
+
});
|
|
47
|
+
host = new SessionHost({ service: runtime.service, scope, identity: { ...coreRuntimeIdentity(), coreVersion: CORE_PACKAGE_VERSION }, close: () => runtime.close() }, { input: process.stdin, output: process.stdout });
|
|
48
|
+
const stop = () => { void host?.stop(); };
|
|
49
|
+
process.on('SIGINT', stop);
|
|
50
|
+
process.on('SIGTERM', stop);
|
|
51
|
+
try {
|
|
52
|
+
await host.start();
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
process.off('SIGINT', stop);
|
|
57
|
+
process.off('SIGTERM', stop);
|
|
58
|
+
// A stopped host must not be kept alive by its still-open stdin pipe.
|
|
59
|
+
process.stdin.destroy();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
21
62
|
/** Both runtime entry points emit the same machine-readable fatal result. */
|
|
22
63
|
export function runtimeFailure(error) {
|
|
23
64
|
return { type: 'runtime-result', status: 'failed', error: error instanceof EngineError
|
|
24
65
|
? { code: error.code, message: error.message, ...(error.details === undefined ? {} : { details: error.details }) }
|
|
25
|
-
|
|
66
|
+
// Session errors keep their code so hosts can react (e.g. `journal_locked` must not be retried).
|
|
67
|
+
: error instanceof Error && error.name === 'SessionError' && typeof error.code === 'string'
|
|
68
|
+
? { code: error.code, message: error.message }
|
|
69
|
+
: error instanceof Error ? error.message : String(error) };
|
|
26
70
|
}
|
|
27
71
|
/** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
|
|
28
|
-
export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'help'];
|
|
72
|
+
export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'host', 'help'];
|
|
29
73
|
function read(file) { return JSON.parse(readFileSync(file, 'utf8')); }
|
|
30
74
|
async function readStdin() {
|
|
31
75
|
const chunks = [];
|
|
@@ -150,6 +194,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
150
194
|
'specrails-core runtime fork --context <json> --from <nodePath> --run-id <newRunId> [--scope-id <scope>] [--visit <number>] [--state <json>]',
|
|
151
195
|
'specrails-core runtime signal --context <json> --stdin [--request-id <id>]',
|
|
152
196
|
'specrails-core runtime cancel --context <json> [--request-id <id>]',
|
|
197
|
+
'specrails-core runtime host --stdio [--scope <projectKey|global>]',
|
|
153
198
|
] });
|
|
154
199
|
return 0;
|
|
155
200
|
}
|
|
@@ -158,9 +203,11 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
158
203
|
emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
|
|
159
204
|
engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
|
|
160
205
|
capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
|
|
161
|
-
engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
206
|
+
engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1, sessions: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
162
207
|
return 0;
|
|
163
208
|
}
|
|
209
|
+
if (command === 'host')
|
|
210
|
+
return runSessionHost(flags);
|
|
164
211
|
if (command === 'validate') {
|
|
165
212
|
if (flags.stdin !== undefined && flags.stdin !== true)
|
|
166
213
|
throw new Error('--stdin is a boolean flag');
|
|
@@ -47,7 +47,7 @@ export declare function missingNodeDependencies(root: string): string[];
|
|
|
47
47
|
/** The installs a repository root needs, judged from its manifests and the absence of their install output. */
|
|
48
48
|
export declare function plannedInstalls(root: string, failureOutput?: string): EnvironmentInstall[];
|
|
49
49
|
/** Runs each planned install once, bounded; never throws. */
|
|
50
|
-
export
|
|
50
|
+
export interface InstallIo {
|
|
51
51
|
spawn?: typeof spawnSync;
|
|
52
52
|
timeoutMs?: number;
|
|
53
53
|
failureOutput?: string;
|
|
@@ -58,7 +58,35 @@ export declare function installEnvironment(roots: readonly string[], io?: {
|
|
|
58
58
|
detail?: string;
|
|
59
59
|
text?: string;
|
|
60
60
|
}) => void;
|
|
61
|
-
|
|
61
|
+
/** The installs for one root; the failure-driven plan by default. */
|
|
62
|
+
plan?: (root: string) => EnvironmentInstall[];
|
|
63
|
+
}
|
|
64
|
+
export declare function installEnvironment(roots: readonly string[], io?: InstallIo): InstallOutcome[];
|
|
65
|
+
type PlaywrightBrowser = 'chromium' | 'firefox' | 'webkit';
|
|
66
|
+
/** Browsers the repository's Playwright config runs (by project names, devices or browserName); chromium when it names none. */
|
|
67
|
+
export declare function configuredPlaywrightBrowsers(root: string): PlaywrightBrowser[];
|
|
68
|
+
/**
|
|
69
|
+
* The configured browsers when any build the installed Playwright pins is
|
|
70
|
+
* missing from the host cache. `install --dry-run` names the exact install
|
|
71
|
+
* locations without downloading; an unreadable answer means "unknown", never
|
|
72
|
+
* an install.
|
|
73
|
+
*/
|
|
74
|
+
export declare function missingPlaywrightBrowsers(root: string, spawn?: typeof spawnSync): PlaywrightBrowser[];
|
|
75
|
+
/**
|
|
76
|
+
* Host preparation before an agent edits a repository: the dependencies its
|
|
77
|
+
* manifest declares but are not installed, then the browser builds its
|
|
78
|
+
* Playwright pins. Agents run sandboxed (Codex cannot write the browser cache
|
|
79
|
+
* or reach it at all), so what they need must already be there. Only cheap,
|
|
80
|
+
* idempotent, project-local work happens here; other ecosystems keep their
|
|
81
|
+
* failure-driven repair at verification.
|
|
82
|
+
*/
|
|
83
|
+
export declare function prepareEnvironment(roots: readonly string[], io?: Omit<InstallIo, 'plan' | 'failureOutput'>): InstallOutcome[];
|
|
84
|
+
/**
|
|
85
|
+
* A Node preload that answers IPv6 lookups with nothing when the host has an
|
|
86
|
+
* IPv4 address, so dual-stack clients connect over IPv4. Written once to the
|
|
87
|
+
* temp directory; undefined when it cannot be written.
|
|
88
|
+
*/
|
|
89
|
+
export declare function ipv4FirstPreload(): string | undefined;
|
|
62
90
|
/** The repository's own test command, judged from its manifest (npm/pnpm/yarn `test` script, pytest, go, cargo); undefined when none. */
|
|
63
91
|
export declare function detectCheckCommand(root: string): {
|
|
64
92
|
command: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crossSpawn from 'cross-spawn';
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
// npm/pnpm/yarn are `.cmd` shims on Windows: a bare `spawnSync('npm')` is
|
|
5
6
|
// ENOENT there, which read as an environment failure of its own and sent the
|
|
@@ -208,12 +209,11 @@ function relaxManifestPin(root, name, version) {
|
|
|
208
209
|
writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
|
|
209
210
|
return changed;
|
|
210
211
|
}
|
|
211
|
-
/** Runs each planned install once, bounded; never throws. */
|
|
212
212
|
export function installEnvironment(roots, io = {}) {
|
|
213
213
|
const spawn = io.spawn ?? defaultSpawn;
|
|
214
214
|
const outcomes = [];
|
|
215
215
|
for (const root of roots) {
|
|
216
|
-
for (const plan of plannedInstalls(root, io.failureOutput ?? '')) {
|
|
216
|
+
for (const plan of io.plan ? io.plan(root) : plannedInstalls(root, io.failureOutput ?? '')) {
|
|
217
217
|
io.onEvent?.({ kind: 'tool-start', tool: plan.command, detail: `${plan.args.join(' ')} (${path.basename(root)})` });
|
|
218
218
|
const run = () => {
|
|
219
219
|
try {
|
|
@@ -245,6 +245,21 @@ export function installEnvironment(roots, io = {}) {
|
|
|
245
245
|
io.onEvent?.({ kind: 'text', text: `Environment: ${unpublished[1]}@${unpublished[2]} is not published; relaxed the pin in package.json and retrying ${plan.command} ${plan.args.join(' ')}` });
|
|
246
246
|
result = run();
|
|
247
247
|
}
|
|
248
|
+
// Playwright's downloader tries IPv6 first; on a host whose DNS returns
|
|
249
|
+
// an AAAA record without an IPv6 route, Node's family auto-selection
|
|
250
|
+
// times the request out before IPv4 is tried. Retry once preferring IPv4.
|
|
251
|
+
if (result.status !== 0 && plan.installs?.startsWith('Playwright') && NETWORK_TIMEOUT.test(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)) {
|
|
252
|
+
const preload = ipv4FirstPreload();
|
|
253
|
+
if (preload) {
|
|
254
|
+
io.onEvent?.({ kind: 'text', text: `Environment: the browser download timed out; retrying ${plan.command} ${plan.args.join(' ')} over IPv4` });
|
|
255
|
+
try {
|
|
256
|
+
result = spawn(plan.command, plan.args, { cwd: root, encoding: 'utf8', timeout: plan.timeoutMs ?? io.timeoutMs ?? 5 * 60_000, windowsHide: true, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --require ${JSON.stringify(preload)}`.trim() } });
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
result = { status: null, error: error, stdout: '', stderr: '', pid: 0, output: [], signal: null };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
248
263
|
io.onEvent?.({ kind: 'tool-end', tool: plan.command });
|
|
249
264
|
const ok = result.status === 0;
|
|
250
265
|
const detail = ok ? `installed ${plan.installs ?? `${plan.ecosystem} dependencies`} in ${path.basename(root)}` : `${plan.command} ${plan.args.join(' ')} failed in ${path.basename(root)}: ${String(result.stderr || result.error?.message || `exit ${result.status}`).trim().slice(0, 400)}`;
|
|
@@ -255,6 +270,109 @@ export function installEnvironment(roots, io = {}) {
|
|
|
255
270
|
}
|
|
256
271
|
return outcomes;
|
|
257
272
|
}
|
|
273
|
+
const PLAYWRIGHT_CONFIGS = ['playwright.config.ts', 'playwright.config.mts', 'playwright.config.cts', 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.cjs'];
|
|
274
|
+
/** Browsers the repository's Playwright config runs (by project names, devices or browserName); chromium when it names none. */
|
|
275
|
+
export function configuredPlaywrightBrowsers(root) {
|
|
276
|
+
const file = PLAYWRIGHT_CONFIGS.map(name => path.join(root, name)).find(candidate => existsSync(candidate));
|
|
277
|
+
let source = '';
|
|
278
|
+
try {
|
|
279
|
+
if (file)
|
|
280
|
+
source = readFileSync(file, 'utf8').slice(0, 200_000);
|
|
281
|
+
}
|
|
282
|
+
catch { /* unreadable config: the default browser */ }
|
|
283
|
+
const browsers = [];
|
|
284
|
+
if (/\b(?:chromium|chrome|edge|pixel|galaxy)\b/i.test(source))
|
|
285
|
+
browsers.push('chromium');
|
|
286
|
+
if (/\bfirefox\b/i.test(source))
|
|
287
|
+
browsers.push('firefox');
|
|
288
|
+
if (/\b(?:webkit|safari|iphone|ipad)\b/i.test(source))
|
|
289
|
+
browsers.push('webkit');
|
|
290
|
+
return browsers.length ? browsers : ['chromium'];
|
|
291
|
+
}
|
|
292
|
+
/** How to run the repository's own Playwright CLI; undefined until its package is installed (never downloads one). */
|
|
293
|
+
function playwrightCli(root) {
|
|
294
|
+
const bin = path.join(root, 'node_modules', '.bin');
|
|
295
|
+
if (!['playwright', 'playwright.cmd'].some(name => existsSync(path.join(bin, name))))
|
|
296
|
+
return undefined;
|
|
297
|
+
if (existsSync(path.join(root, 'pnpm-lock.yaml')))
|
|
298
|
+
return { command: 'pnpm', args: ['exec', 'playwright'] };
|
|
299
|
+
if (existsSync(path.join(root, 'yarn.lock')))
|
|
300
|
+
return { command: 'yarn', args: ['playwright'] };
|
|
301
|
+
return { command: 'npx', args: ['--no', 'playwright'] };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The configured browsers when any build the installed Playwright pins is
|
|
305
|
+
* missing from the host cache. `install --dry-run` names the exact install
|
|
306
|
+
* locations without downloading; an unreadable answer means "unknown", never
|
|
307
|
+
* an install.
|
|
308
|
+
*/
|
|
309
|
+
export function missingPlaywrightBrowsers(root, spawn = defaultSpawn) {
|
|
310
|
+
if (!declaresPlaywright(root))
|
|
311
|
+
return [];
|
|
312
|
+
const cli = playwrightCli(root);
|
|
313
|
+
if (!cli)
|
|
314
|
+
return [];
|
|
315
|
+
const browsers = configuredPlaywrightBrowsers(root);
|
|
316
|
+
let result;
|
|
317
|
+
try {
|
|
318
|
+
result = spawn(cli.command, [...cli.args, 'install', '--dry-run', ...browsers], { cwd: root, encoding: 'utf8', timeout: 30_000, windowsHide: true, env: process.env });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
if (result.status !== 0)
|
|
324
|
+
return [];
|
|
325
|
+
const locations = [...String(result.stdout ?? '').matchAll(/Install location:\s+(.+?)\s*$/gm)].map(match => match[1]);
|
|
326
|
+
return locations.length && locations.some(location => !existsSync(location)) ? browsers : [];
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Host preparation before an agent edits a repository: the dependencies its
|
|
330
|
+
* manifest declares but are not installed, then the browser builds its
|
|
331
|
+
* Playwright pins. Agents run sandboxed (Codex cannot write the browser cache
|
|
332
|
+
* or reach it at all), so what they need must already be there. Only cheap,
|
|
333
|
+
* idempotent, project-local work happens here; other ecosystems keep their
|
|
334
|
+
* failure-driven repair at verification.
|
|
335
|
+
*/
|
|
336
|
+
export function prepareEnvironment(roots, io = {}) {
|
|
337
|
+
const spawn = io.spawn ?? defaultSpawn;
|
|
338
|
+
const dependencies = installEnvironment(roots, { ...io, plan: root => plannedInstalls(root).filter(plan => plan.ecosystem === 'node') });
|
|
339
|
+
const browsers = installEnvironment(roots, { ...io, plan: root => {
|
|
340
|
+
const missing = missingPlaywrightBrowsers(root, spawn);
|
|
341
|
+
const cli = missing.length ? playwrightCli(root) : undefined;
|
|
342
|
+
return cli ? [{ ecosystem: 'node', root, command: cli.command, args: [...cli.args, 'install', ...missing], timeoutMs: PLAYWRIGHT_INSTALL_TIMEOUT_MS, installs: `Playwright ${missing.join(', ')}` }] : [];
|
|
343
|
+
} });
|
|
344
|
+
return [...dependencies, ...browsers];
|
|
345
|
+
}
|
|
346
|
+
const NETWORK_TIMEOUT = /timed out after \d+ ?ms|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH/i;
|
|
347
|
+
/**
|
|
348
|
+
* A Node preload that answers IPv6 lookups with nothing when the host has an
|
|
349
|
+
* IPv4 address, so dual-stack clients connect over IPv4. Written once to the
|
|
350
|
+
* temp directory; undefined when it cannot be written.
|
|
351
|
+
*/
|
|
352
|
+
export function ipv4FirstPreload() {
|
|
353
|
+
const file = path.join(tmpdir(), 'specrails-ipv4-first-v1.cjs');
|
|
354
|
+
if (existsSync(file))
|
|
355
|
+
return file;
|
|
356
|
+
try {
|
|
357
|
+
writeFileSync(file, [
|
|
358
|
+
"const dns = require('node:dns')",
|
|
359
|
+
'const lookup = dns.promises.lookup.bind(dns.promises)',
|
|
360
|
+
'dns.promises.lookup = async (hostname, options) => {',
|
|
361
|
+
" const family = typeof options === 'number' ? options : options && options.family",
|
|
362
|
+
' if (family === 6) {',
|
|
363
|
+
' const v4 = await lookup(hostname, { all: true, family: 4 }).catch(() => [])',
|
|
364
|
+
" if (v4.length) { if (options && typeof options === 'object' && options.all) return []; throw Object.assign(new Error('IPv6 skipped: ' + hostname), { code: 'ENOTFOUND' }) }",
|
|
365
|
+
' }',
|
|
366
|
+
' return lookup(hostname, options)',
|
|
367
|
+
'}',
|
|
368
|
+
'',
|
|
369
|
+
].join('\n'));
|
|
370
|
+
return file;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
258
376
|
/** The repository's own test command, judged from its manifest (npm/pnpm/yarn `test` script, pytest, go, cargo); undefined when none. */
|
|
259
377
|
export function detectCheckCommand(root) {
|
|
260
378
|
const has = (file) => existsSync(path.join(root, file));
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { type InstallOutcome } from '../../compact/environment.js';
|
|
1
2
|
import { type JsonObject, type Piece, type PieceExecutionContext, type PieceResult } from '../contracts.js';
|
|
2
3
|
import type { PieceDependencies, PieceDependencyProvider } from './ports.js';
|
|
3
4
|
export declare function roleTurnPiece(bindings: PieceDependencyProvider): Piece;
|
|
4
5
|
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
5
6
|
export declare function executeRoleTurn(deps: PieceDependencies, params: JsonObject, context: PieceExecutionContext, options?: {
|
|
6
7
|
normalizeStructuredOutput?: (output: Record<string, unknown> | undefined, text: string) => Record<string, unknown> | undefined;
|
|
8
|
+
/** Set on the single rerun after the host repaired an environment blocker the agent reported. */
|
|
9
|
+
environmentRetry?: {
|
|
10
|
+
installs: InstallOutcome[];
|
|
11
|
+
};
|
|
7
12
|
}): Promise<PieceResult>;
|
|
@@ -5,6 +5,9 @@ import { withScopeDefault } from '../../change-scope.js';
|
|
|
5
5
|
import { resolveRoleDescriptor } from '../../config.js';
|
|
6
6
|
import { AgentExecutionError } from '../../executor-types.js';
|
|
7
7
|
import { createRoleInvoker } from '../../graph/roles.js';
|
|
8
|
+
import { admitVerificationProposals } from '../../verification-proposals.js';
|
|
9
|
+
import { guardrailEnabled } from '../../guardrails.js';
|
|
10
|
+
import { installEnvironment, prepareEnvironment } from '../../compact/environment.js';
|
|
8
11
|
import { roleInstructions } from '../../prompts.js';
|
|
9
12
|
import { EngineError } from '../contracts.js';
|
|
10
13
|
import { boundedText, historyEntry, idSchema, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
@@ -25,6 +28,14 @@ export function roleTurnPiece(bindings) {
|
|
|
25
28
|
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
26
29
|
export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
27
30
|
const roleId = text(params.roleId), descriptor = resolveRoleDescriptor(deps.config, roleId);
|
|
31
|
+
// Agents run sandboxed and cannot install what the repository needs (Codex
|
|
32
|
+
// cannot even write the Playwright browser cache): the host prepares it
|
|
33
|
+
// before any writer turn and says so in the task.
|
|
34
|
+
const hostRepairs = descriptor.access === 'write' && guardrailEnabled(deps.config.guardrails, 'environment-repair');
|
|
35
|
+
const note = (line) => context.progress({ type: 'verification-output', payload: { text: line } });
|
|
36
|
+
const environment = options.environmentRetry
|
|
37
|
+
? environmentNote(options.environmentRetry.installs, 'retry')
|
|
38
|
+
: hostRepairs ? environmentNote(prepareEnvironment(deps.context.repositories.map(repository => repository.path), environmentIo(deps, note)), 'prepared') : '';
|
|
28
39
|
const schema = params.structuredOutput;
|
|
29
40
|
const validate = schema ? ajv.compile(schema) : undefined;
|
|
30
41
|
const roleState = deps.roleState(context);
|
|
@@ -45,7 +56,7 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
45
56
|
});
|
|
46
57
|
const priorNote = !previous && descriptor.access === 'read' && saved?.session && typeof saved.notes?.value.text === 'string'
|
|
47
58
|
? saved.notes.value.text.slice(0, 4000) : '';
|
|
48
|
-
const task = text(params.prompt) + (priorNote ? '\n\n## Prior project review note\nTreat this bounded note as prior evidence to check against the current task; frozen requirements remain authoritative.\n' + JSON.stringify(priorNote) : '');
|
|
59
|
+
const task = text(params.prompt) + environment + (priorNote ? '\n\n## Prior project review note\nTreat this bounded note as prior evidence to check against the current task; frozen requirements remain authoritative.\n' + JSON.stringify(priorNote) : '');
|
|
49
60
|
const verification = descriptor.access === 'write' ? hostVerificationPlan(deps, context, params) : undefined;
|
|
50
61
|
const full = roleInstructions(descriptor, deps.context, openspec?.[roleId]?.change, { definition: descriptor.prompt, verification }) + '\n## Current workflow task\n' + task;
|
|
51
62
|
const result = await invoke(roleId, deps.stepContext(context), { prompt: previous ? task : full, ...(previous ? { resumeSessionId: previous, fallbackPrompt: full } : {}),
|
|
@@ -60,6 +71,19 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
60
71
|
throw new AgentExecutionError(result.error, result.code);
|
|
61
72
|
return { outcome: schema ? 'invalid' : 'failed', status: 'failed', output: { error: boundedText(result.error) }, error: { code: schema ? 'invalid_role_output' : 'role_failed', message: result.error } };
|
|
62
73
|
}
|
|
74
|
+
// A writer that still hit a missing tool reports a structured blocker. When
|
|
75
|
+
// the host can install it, it does so and reruns the turn once instead of
|
|
76
|
+
// ending the run on something it can fix itself.
|
|
77
|
+
const blocker = hostRepairs && !options.environmentRetry ? repairableBlocker(result.value) : undefined;
|
|
78
|
+
if (blocker) {
|
|
79
|
+
const roots = deps.context.repositories.map(repository => repository.path);
|
|
80
|
+
const io = environmentIo(deps, note);
|
|
81
|
+
const installs = [...installEnvironment(roots, { ...io, failureOutput: blocker }), ...prepareEnvironment(roots, io)];
|
|
82
|
+
if (installs.some(install => install.ok)) {
|
|
83
|
+
note('[environment] The agent reported a missing dependency or tool; the host installed it and runs the turn again.');
|
|
84
|
+
return executeRoleTurn(deps, params, context, { ...options, environmentRetry: { installs } });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
63
87
|
const currentState = roleState.read(), session = currentState.sessions[roleId];
|
|
64
88
|
const finalIdentity = { ...memoryIdentity, tier: currentState.routes[roleId]?.tier ?? 'base' }, finalKey = contentDigest(finalIdentity);
|
|
65
89
|
await advisoryMemory(context, async () => {
|
|
@@ -73,34 +97,43 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
73
97
|
return { outcome: 'next', output: { candidateHash: deps.executionSnapshot(context).candidate?.hash ?? null, text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
74
98
|
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
75
99
|
}
|
|
100
|
+
const REPAIRABLE_BLOCKERS = new Set(['toolchain', 'setup', 'environment']);
|
|
101
|
+
/** The text of a structured `blocker` the host may be able to install away; undefined for other kinds (credentials, network, scope). */
|
|
102
|
+
function repairableBlocker(value) {
|
|
103
|
+
const blocker = value?.blocker;
|
|
104
|
+
if (!blocker || typeof blocker !== 'object' || !REPAIRABLE_BLOCKERS.has(String(blocker.kind)))
|
|
105
|
+
return undefined;
|
|
106
|
+
return ['evidence', 'reason', 'command', 'requiredAction'].map(field => typeof blocker[field] === 'string' ? blocker[field] : '').join('\n').slice(0, 8_000);
|
|
107
|
+
}
|
|
108
|
+
function environmentIo(deps, note) {
|
|
109
|
+
return { lockfileRepair: guardrailEnabled(deps.config.guardrails, 'lockfile-repair'),
|
|
110
|
+
onEvent: (event) => note(event.kind === 'text' ? event.text ?? '' : `[environment] ${event.tool ?? ''} ${event.detail ?? ''}`.trim()) };
|
|
111
|
+
}
|
|
112
|
+
/** What the host installed (or could not) for this turn, as task context; empty when nothing was needed. */
|
|
113
|
+
function environmentNote(installs, when) {
|
|
114
|
+
if (!installs.length)
|
|
115
|
+
return '';
|
|
116
|
+
const done = installs.filter(install => install.ok).map(install => install.installs ?? `${install.ecosystem} dependencies`);
|
|
117
|
+
const failed = installs.filter(install => !install.ok).map(install => install.detail);
|
|
118
|
+
return '\n\n## Host environment\n' + (when === 'retry' ? 'Your previous turn reported an environment blocker. ' : '')
|
|
119
|
+
+ (done.length ? `The host installed ${done.join('; ')} for this workspace${when === 'retry' ? ': continue the remaining work and rerun the checks that were blocked.' : ' before this turn.'} ` : '')
|
|
120
|
+
+ (failed.length ? `It could not install: ${failed.join('; ')}. ` : '')
|
|
121
|
+
+ 'Do not install these yourself; if something is still missing, return the structured blocker.';
|
|
122
|
+
}
|
|
76
123
|
/** The complete plan the host will run for the repositories in scope: configured checks plus, when `verificationProposalsFrom`
|
|
77
|
-
* names a committed output,
|
|
124
|
+
* names a committed output, the proposals the verify piece admits (see admitVerificationProposals). */
|
|
78
125
|
function hostVerificationPlan(deps, context, params) {
|
|
79
126
|
const inScope = new Set(deps.context.repositories.map(repository => repository.id));
|
|
80
127
|
const commands = deps.config.verification.filter(command => inScope.has(command.repositoryId)).map(command => withScopeDefault(deps.context, command));
|
|
81
|
-
const configured = new Set(commands.map(command => command.repositoryId));
|
|
82
|
-
const seen = new Set(commands.map(planKey));
|
|
83
128
|
if (typeof params.verificationProposalsFrom === 'string') {
|
|
84
129
|
const source = context.state.$outputs[params.verificationProposalsFrom];
|
|
85
|
-
const proposals = Array.isArray(source?.structured?.verification) ? source.structured.verification.slice(0, 20) : []
|
|
86
|
-
for (const proposal of proposals) {
|
|
130
|
+
const proposals = (Array.isArray(source?.structured?.verification) ? source.structured.verification.slice(0, 20) : []).filter((proposal) => {
|
|
87
131
|
if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal))
|
|
88
|
-
|
|
132
|
+
return false;
|
|
89
133
|
const raw = proposal;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
continue;
|
|
94
|
-
const command = withScopeDefault(deps.context, { repositoryId: raw.repositoryId, command: raw.command, args: raw.args, ...(typeof raw.cwd === 'string' ? { cwd: raw.cwd } : {}) });
|
|
95
|
-
const key = planKey(command);
|
|
96
|
-
if (seen.has(key))
|
|
97
|
-
continue;
|
|
98
|
-
seen.add(key);
|
|
99
|
-
commands.push(command);
|
|
100
|
-
}
|
|
134
|
+
return typeof raw.repositoryId === 'string' && inScope.has(raw.repositoryId) && typeof raw.command === 'string' && Array.isArray(raw.args) && raw.args.every(arg => typeof arg === 'string');
|
|
135
|
+
});
|
|
136
|
+
commands.push(...admitVerificationProposals(deps.context, commands, proposals, raw => withScopeDefault(deps.context, { repositoryId: raw.repositoryId, command: raw.command, args: raw.args, ...(typeof raw.cwd === 'string' ? { cwd: raw.cwd } : {}) })));
|
|
101
137
|
}
|
|
102
138
|
return commands;
|
|
103
139
|
}
|
|
104
|
-
function planKey(command) {
|
|
105
|
-
return JSON.stringify([command.repositoryId, command.cwd ?? '', command.command, command.args]);
|
|
106
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { admitVerificationProposals } from '../../verification-proposals.js';
|
|
1
2
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
3
|
import { contentDigest } from '../canonical-json.js';
|
|
3
4
|
import { advisoryMemory } from './project-memory.js';
|
|
@@ -89,7 +90,6 @@ const verificationCommandList = { type: 'array', maxItems: 100, items: verificat
|
|
|
89
90
|
export function resolveVerificationPlan(deps, context, params) {
|
|
90
91
|
const configured = params.commands === undefined || params.commands === 'configured' ? deps.config.verification : params.commands;
|
|
91
92
|
const commands = configured.map(command => withScopeDefault(deps.context, command));
|
|
92
|
-
const hostRepositories = new Set(commands.map(command => command.repositoryId));
|
|
93
93
|
if (typeof params.additionalCommandsFrom === 'string') {
|
|
94
94
|
const source = context.state.$outputs[params.additionalCommandsFrom];
|
|
95
95
|
const proposals = source?.structured?.verification;
|
|
@@ -100,12 +100,10 @@ export function resolveVerificationPlan(deps, context, params) {
|
|
|
100
100
|
for (const proposal of proposals) {
|
|
101
101
|
if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal))
|
|
102
102
|
throw new Error('Invalid verification proposal');
|
|
103
|
-
|
|
104
|
-
if (!deps.context.repositories.some(repository => repository.id === raw.repositoryId))
|
|
103
|
+
if (!deps.context.repositories.some(repository => repository.id === proposal.repositoryId))
|
|
105
104
|
throw new Error('Unknown verification repository');
|
|
106
|
-
if (!hostRepositories.has(raw.repositoryId))
|
|
107
|
-
commands.push(withScopeDefault(deps.context, raw));
|
|
108
105
|
}
|
|
106
|
+
commands.push(...admitVerificationProposals(deps.context, commands, proposals, raw => withScopeDefault(deps.context, raw)));
|
|
109
107
|
}
|
|
110
108
|
return commands;
|
|
111
109
|
}
|