specrails-core 6.4.0 → 6.6.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.d.ts +2 -2
- package/dist/agent-runtime/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +4 -4
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +50 -4
- package/dist/agent-runtime/compact/environment.js +183 -6
- package/dist/agent-runtime/compact/step.d.ts +1 -1
- package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
- package/dist/agent-runtime/efficiency-types.d.ts +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
- package/dist/agent-runtime/engine/contracts.d.ts +2 -1
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
- package/dist/agent-runtime/engine/fork.d.ts +1 -1
- package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
- package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
- package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
- package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
- package/dist/agent-runtime/engine/pieces/verify.js +46 -13
- package/dist/agent-runtime/engine/run-status.d.ts +165 -165
- package/dist/agent-runtime/engine/runs.d.ts +168 -168
- package/dist/agent-runtime/engine/state.d.ts +16 -16
- package/dist/agent-runtime/evaluation.d.ts +3 -3
- package/dist/agent-runtime/executor-types.d.ts +10 -10
- package/dist/agent-runtime/graph/convergence.d.ts +2 -2
- package/dist/agent-runtime/graph/convergence.js +11 -0
- package/dist/agent-runtime/graph/nodes.js +35 -4
- package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
- package/dist/agent-runtime/graph/state.d.ts +6 -2
- package/dist/agent-runtime/guardrails.d.ts +1 -1
- package/dist/agent-runtime/guardrails.js +4 -1
- package/dist/agent-runtime/index.d.ts +3 -2
- package/dist/agent-runtime/index.js +1 -0
- package/dist/agent-runtime/openai-executor.d.ts +9 -9
- package/dist/agent-runtime/openspec.d.ts +4 -4
- package/dist/agent-runtime/prompts.d.ts +6 -0
- package/dist/agent-runtime/prompts.js +32 -8
- package/dist/agent-runtime/recovery.d.ts +12 -12
- 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/agent-runtime/verification-repair.d.ts +31 -2
- package/dist/agent-runtime/verification-repair.js +58 -3
- package/dist/installer/commands/init.d.ts +1 -1
- package/dist/installer/phases/install-config.js +10 -3
- package/dist/installer/util/paths.d.ts +25 -1
- package/dist/installer/util/registry.js +3 -3
- package/dist/pipeline/pipeline-state.d.ts +42 -11
- package/dist/pipeline/pipeline-state.js +47 -8
- 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 +23 -11
- package/schemas/agent-runtime.schema.json +1 -1
|
@@ -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,9 +56,10 @@ 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
|
-
const
|
|
61
|
+
const adopted = adoptedVerificationOutputs(context.state.$outputs);
|
|
62
|
+
const full = roleInstructions(descriptor, deps.context, openspec?.[roleId]?.change, { definition: descriptor.prompt, verification, ...(adopted.length ? { adoptedOutputs: adopted } : {}) }) + '\n## Current workflow task\n' + task;
|
|
51
63
|
const result = await invoke(roleId, deps.stepContext(context), { prompt: previous ? task : full, ...(previous ? { resumeSessionId: previous, fallbackPrompt: full } : {}),
|
|
52
64
|
structured: schema !== undefined, lenient: options.normalizeStructuredOutput !== undefined, outputSchema: schema, timeoutMs: params.timeoutMs, idleTimeoutMs: params.idleTimeoutMs }, (output, responseText) => {
|
|
53
65
|
const normalized = options.normalizeStructuredOutput ? options.normalizeStructuredOutput(output, responseText) : output;
|
|
@@ -60,6 +72,19 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
60
72
|
throw new AgentExecutionError(result.error, result.code);
|
|
61
73
|
return { outcome: schema ? 'invalid' : 'failed', status: 'failed', output: { error: boundedText(result.error) }, error: { code: schema ? 'invalid_role_output' : 'role_failed', message: result.error } };
|
|
62
74
|
}
|
|
75
|
+
// A writer that still hit a missing tool reports a structured blocker. When
|
|
76
|
+
// the host can install it, it does so and reruns the turn once instead of
|
|
77
|
+
// ending the run on something it can fix itself.
|
|
78
|
+
const blocker = hostRepairs && !options.environmentRetry ? repairableBlocker(result.value) : undefined;
|
|
79
|
+
if (blocker) {
|
|
80
|
+
const roots = deps.context.repositories.map(repository => repository.path);
|
|
81
|
+
const io = environmentIo(deps, note);
|
|
82
|
+
const installs = [...installEnvironment(roots, { ...io, failureOutput: blocker }), ...prepareEnvironment(roots, io)];
|
|
83
|
+
if (installs.some(install => install.ok)) {
|
|
84
|
+
note('[environment] The agent reported a missing dependency or tool; the host installed it and runs the turn again.');
|
|
85
|
+
return executeRoleTurn(deps, params, context, { ...options, environmentRetry: { installs } });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
63
88
|
const currentState = roleState.read(), session = currentState.sessions[roleId];
|
|
64
89
|
const finalIdentity = { ...memoryIdentity, tier: currentState.routes[roleId]?.tier ?? 'base' }, finalKey = contentDigest(finalIdentity);
|
|
65
90
|
await advisoryMemory(context, async () => {
|
|
@@ -73,34 +98,51 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
73
98
|
return { outcome: 'next', output: { candidateHash: deps.executionSnapshot(context).candidate?.hash ?? null, text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
74
99
|
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
75
100
|
}
|
|
101
|
+
/** Files any committed `verify` output reports as host-adopted verification output (sticky for the run), so no role is told to revert them. */
|
|
102
|
+
function adoptedVerificationOutputs(outputs) {
|
|
103
|
+
return Object.values(outputs).flatMap(output => {
|
|
104
|
+
const listed = output && typeof output === 'object' && !Array.isArray(output) ? output.adoptedOutputs : undefined;
|
|
105
|
+
return Array.isArray(listed) ? listed.flatMap(item => item && typeof item === 'object' && !Array.isArray(item) && typeof item.path === 'string'
|
|
106
|
+
? [{ ...(typeof item.repositoryId === 'string' ? { repositoryId: item.repositoryId } : {}), path: item.path }] : []) : [];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const REPAIRABLE_BLOCKERS = new Set(['toolchain', 'setup', 'environment']);
|
|
110
|
+
/** The text of a structured `blocker` the host may be able to install away; undefined for other kinds (credentials, network, scope). */
|
|
111
|
+
function repairableBlocker(value) {
|
|
112
|
+
const blocker = value?.blocker;
|
|
113
|
+
if (!blocker || typeof blocker !== 'object' || !REPAIRABLE_BLOCKERS.has(String(blocker.kind)))
|
|
114
|
+
return undefined;
|
|
115
|
+
return ['evidence', 'reason', 'command', 'requiredAction'].map(field => typeof blocker[field] === 'string' ? blocker[field] : '').join('\n').slice(0, 8_000);
|
|
116
|
+
}
|
|
117
|
+
function environmentIo(deps, note) {
|
|
118
|
+
return { lockfileRepair: guardrailEnabled(deps.config.guardrails, 'lockfile-repair'),
|
|
119
|
+
onEvent: (event) => note(event.kind === 'text' ? event.text ?? '' : `[environment] ${event.tool ?? ''} ${event.detail ?? ''}`.trim()) };
|
|
120
|
+
}
|
|
121
|
+
/** What the host installed (or could not) for this turn, as task context; empty when nothing was needed. */
|
|
122
|
+
function environmentNote(installs, when) {
|
|
123
|
+
if (!installs.length)
|
|
124
|
+
return '';
|
|
125
|
+
const done = installs.filter(install => install.ok).map(install => install.installs ?? `${install.ecosystem} dependencies`);
|
|
126
|
+
const failed = installs.filter(install => !install.ok).map(install => install.detail);
|
|
127
|
+
return '\n\n## Host environment\n' + (when === 'retry' ? 'Your previous turn reported an environment blocker. ' : '')
|
|
128
|
+
+ (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.'} ` : '')
|
|
129
|
+
+ (failed.length ? `It could not install: ${failed.join('; ')}. ` : '')
|
|
130
|
+
+ 'Do not install these yourself; if something is still missing, return the structured blocker.';
|
|
131
|
+
}
|
|
76
132
|
/** The complete plan the host will run for the repositories in scope: configured checks plus, when `verificationProposalsFrom`
|
|
77
|
-
* names a committed output,
|
|
133
|
+
* names a committed output, the proposals the verify piece admits (see admitVerificationProposals). */
|
|
78
134
|
function hostVerificationPlan(deps, context, params) {
|
|
79
135
|
const inScope = new Set(deps.context.repositories.map(repository => repository.id));
|
|
80
136
|
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
137
|
if (typeof params.verificationProposalsFrom === 'string') {
|
|
84
138
|
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) {
|
|
139
|
+
const proposals = (Array.isArray(source?.structured?.verification) ? source.structured.verification.slice(0, 20) : []).filter((proposal) => {
|
|
87
140
|
if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal))
|
|
88
|
-
|
|
141
|
+
return false;
|
|
89
142
|
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
|
-
}
|
|
143
|
+
return typeof raw.repositoryId === 'string' && inScope.has(raw.repositoryId) && typeof raw.command === 'string' && Array.isArray(raw.args) && raw.args.every(arg => typeof arg === 'string');
|
|
144
|
+
});
|
|
145
|
+
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
146
|
}
|
|
102
147
|
return commands;
|
|
103
148
|
}
|
|
104
|
-
function planKey(command) {
|
|
105
|
-
return JSON.stringify([command.repositoryId, command.cwd ?? '', command.command, command.args]);
|
|
106
|
-
}
|
|
@@ -14,14 +14,14 @@ export declare const positiveInteger: {
|
|
|
14
14
|
};
|
|
15
15
|
export declare const invocationTimers: {
|
|
16
16
|
timeoutMs: {
|
|
17
|
-
minimum: number;
|
|
18
17
|
type: string;
|
|
19
18
|
maximum: number;
|
|
19
|
+
minimum: number;
|
|
20
20
|
};
|
|
21
21
|
idleTimeoutMs: {
|
|
22
|
-
minimum: number;
|
|
23
22
|
type: string;
|
|
24
23
|
maximum: number;
|
|
24
|
+
minimum: number;
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
export declare const variableNameSchema: {
|
|
@@ -1,13 +1,14 @@
|
|
|
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';
|
|
4
|
-
import { executeVerification, validateVerificationRequest } from '../../../pipeline/pipeline-state.js';
|
|
5
|
+
import { describeModifiedFiles, executeVerification, validateVerificationRequest } from '../../../pipeline/pipeline-state.js';
|
|
5
6
|
import { withScopeDefault } from '../../change-scope.js';
|
|
6
7
|
import { verificationDiagnosticPriority, verificationFailureSummary } from '../../verification-diagnostics.js';
|
|
7
8
|
import { boundedText, json, paramsSchema, positiveInteger } from './shared.js';
|
|
8
|
-
import {
|
|
9
|
+
import { describeDrift } from '../../compact/environment.js';
|
|
9
10
|
import { guardrailEnabled } from '../../guardrails.js';
|
|
10
|
-
import { checkoutRelative, preconditionBlock, repairEnvironment } from '../../verification-repair.js';
|
|
11
|
+
import { NONDETERMINISTIC_OUTPUT_CODE, adoptedOutputNote, checkoutRelative, environmentFailure, nondeterministicOutputBlocker, preconditionBlock, repairEnvironment, selfMutationOnly } from '../../verification-repair.js';
|
|
11
12
|
/** Keep real subprocess diagnostics in committed outputs for subsequent agents.
|
|
12
13
|
* The full command evidence remains in the receipt; prioritize failed checks
|
|
13
14
|
* when the output budget is exhausted. */
|
|
@@ -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
|
}
|
|
@@ -171,10 +169,16 @@ export function verifyPiece(bindings) {
|
|
|
171
169
|
// fresh worktree has no node_modules whatever wrote the code. Install
|
|
172
170
|
// once where the failing command runs and verify again; only the second
|
|
173
171
|
// receipt becomes feedback.
|
|
172
|
+
// Installed versions outside their declared ranges (a stale warm-linked
|
|
173
|
+
// tree) count as environmental whatever the failing output says.
|
|
174
174
|
let environmentRepair;
|
|
175
|
-
|
|
175
|
+
const environment = !blocker && !receipt.valid && guardrailEnabled(deps.config.guardrails, 'environment-repair') ? environmentFailure(deps.context, receipt) : undefined;
|
|
176
|
+
if (environment) {
|
|
177
|
+
const drift = environment.drift.flatMap(item => item.drift.map(entry => ({ root: checkoutRelative(deps.context, item.root), ...entry }))).slice(0, 20);
|
|
178
|
+
if (drift.length)
|
|
179
|
+
progress(`[environment] Installed dependencies do not satisfy their declared ranges (${describeDrift(drift)}); the host reinstalls them before judging the failure.`);
|
|
176
180
|
const repair = repairEnvironment(deps.context, receipt, deps.config.guardrails, progress);
|
|
177
|
-
environmentRepair = { attempted: true, reverified: false, installs: repair.installs.map(install => ({ command: install.command, args: install.args, root: checkoutRelative(deps.context, install.root), ok: install.ok, detail: boundedText(install.detail, 1_000) })) };
|
|
181
|
+
environmentRepair = { attempted: true, reverified: false, installs: repair.installs.map(install => ({ command: install.command, args: install.args, root: checkoutRelative(deps.context, install.root), ok: install.ok, detail: boundedText(install.detail, 1_000) })), ...(drift.length ? { drift } : {}) };
|
|
178
182
|
if (repair.refused)
|
|
179
183
|
blocker = repair.refused;
|
|
180
184
|
else if (repair.installs.some(install => install.ok)) {
|
|
@@ -184,6 +188,29 @@ export function verifyPiece(bindings) {
|
|
|
184
188
|
blocker = receipt.valid ? undefined : preconditionBlock(deps.context, receipt);
|
|
185
189
|
}
|
|
186
190
|
}
|
|
191
|
+
// Verification that rewrote candidate files itself (a lint pre-step
|
|
192
|
+
// regenerating a tracked mapping) is not a code failure: reverting the
|
|
193
|
+
// file only makes the next run regenerate it. Keep it in the candidate
|
|
194
|
+
// and verify once more; output that changes again is the repository's
|
|
195
|
+
// or operator's to fix, never a correction round.
|
|
196
|
+
let adopted, nondeterministic;
|
|
197
|
+
if (!blocker && selfMutationOnly(receipt) && !context.signal.aborted && (verificationDeadline(deps, context) ?? Infinity) > Date.now()) {
|
|
198
|
+
const first = receipt.selfMutation;
|
|
199
|
+
if (!guardrailEnabled(deps.config.guardrails, 'verification-output-adoption'))
|
|
200
|
+
nondeterministic = nondeterministicOutputBlocker(deps.context, receipt, false);
|
|
201
|
+
else {
|
|
202
|
+
progress(`[verification] Verification commands modified ${describeModifiedFiles(first)}; keeping them in the candidate and verifying once more.`);
|
|
203
|
+
receipt = await run(request, concurrency);
|
|
204
|
+
if (receipt.valid) {
|
|
205
|
+
adopted = first.files;
|
|
206
|
+
progress(adoptedOutputNote(first));
|
|
207
|
+
}
|
|
208
|
+
else if (selfMutationOnly(receipt))
|
|
209
|
+
nondeterministic = nondeterministicOutputBlocker(deps.context, receipt, true);
|
|
210
|
+
else
|
|
211
|
+
blocker = preconditionBlock(deps.context, receipt);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
187
214
|
await advisoryMemory(context, () => deps.memory(context).put(['verification', 'known-commands'], memoryKey, {
|
|
188
215
|
...identity, observations: Math.min(1_000_000, typeof known?.value.observations === 'number' ? known.value.observations + 1 : 1),
|
|
189
216
|
lastAttemptId: context.frame.attemptId, lastReceiptId: receipt.id, lastValid: receipt.valid,
|
|
@@ -195,12 +222,18 @@ export function verifyPiece(bindings) {
|
|
|
195
222
|
const fingerprint = contentDigest(json({ candidateHash: identity.candidateHash,
|
|
196
223
|
failures: receipt.commands.filter(command => command.exitCode !== 0).map(command => ({ repositoryId: command.repositoryId, command: command.command, args: command.args, cwd: command.cwd, exitCode: command.exitCode })) }));
|
|
197
224
|
const previous = context.state.$vars[progressKey];
|
|
225
|
+
// Adopted output stays adopted for the rest of the run: a later pass that no longer mutates must not invite its revert.
|
|
226
|
+
const adoptedOutputs = [...new Map([...(Array.isArray(previous?.adopted) ? previous.adopted : []), ...(adopted ?? [])].map(file => [file.repositoryId + '\0' + file.path, file])).values()].slice(0, 200);
|
|
198
227
|
const count = receipt.valid ? 0 : previous?.fingerprint === fingerprint ? (previous.count ?? 0) + 1 : 1;
|
|
199
228
|
const stalled = !infrastructure && !receipt.valid && count >= 3;
|
|
200
229
|
const verified = receipt.valid && receipt.commands.length > 0 && !receipt.unverifiedRepositories?.length;
|
|
201
|
-
const output = { receiptId: receipt.id, valid: receipt.valid, candidateHash: identity.candidateHash, noProgressCount: count, ...(receipt.reason ? { reason: receipt.reason } : {}), ...(stalled ? { reason: 'Verification failed three times without candidate changes' } : {}),
|
|
202
|
-
...(setup ? { setup } : {}), ...(environmentRepair ? { environmentRepair } : {}), ...
|
|
203
|
-
|
|
230
|
+
const output = { receiptId: receipt.id, valid: receipt.valid, candidateHash: adopted ? receipt.candidateHash : identity.candidateHash, noProgressCount: count, ...(receipt.reason ? { reason: receipt.reason } : {}), ...(stalled ? { reason: 'Verification failed three times without candidate changes' } : {}),
|
|
231
|
+
...(setup ? { setup } : {}), ...(environmentRepair ? { environmentRepair } : {}), ...(receipt.selfMutation ? { selfMutation: json(receipt.selfMutation) } : {}),
|
|
232
|
+
...(adoptedOutputs.length ? { adoptedOutputs: json(adoptedOutputs) } : {}), ...verificationDiagnostics(receipt) };
|
|
233
|
+
const vars = { [progressKey]: { fingerprint, count, ...(adoptedOutputs.length ? { adopted: json(adoptedOutputs) } : {}) } };
|
|
234
|
+
if (nondeterministic)
|
|
235
|
+
return { outcome: params.hostBlockers === true ? 'blocked' : 'failed', status: params.hostBlockers === true ? 'blocked' : 'failed', vars, receipt: receiptEvidence(receipt), verified: null,
|
|
236
|
+
error: { code: NONDETERMINISTIC_OUTPUT_CODE, message: `${nondeterministic.reason}. ${nondeterministic.requiredAction}` }, output: { ...output, valid: false, blocker: json(nondeterministic) } };
|
|
204
237
|
// Without the opt-in outcome a blocker still travels in the output, and the no-progress stop keeps bounding the loop.
|
|
205
238
|
if (blocker && (params.hostBlockers === true || !stalled))
|
|
206
239
|
return { ...blocked(blocker, output), vars, receipt: receiptEvidence(receipt) };
|