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.
Files changed (131) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +2 -2
  2. package/dist/agent-runtime/capabilities.js +22 -5
  3. package/dist/agent-runtime/cli-executor.js +8 -3
  4. package/dist/agent-runtime/cli.d.ts +4 -4
  5. package/dist/agent-runtime/cli.js +50 -3
  6. package/dist/agent-runtime/compact/environment.d.ts +50 -4
  7. package/dist/agent-runtime/compact/environment.js +183 -6
  8. package/dist/agent-runtime/compact/step.d.ts +1 -1
  9. package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
  10. package/dist/agent-runtime/efficiency-types.d.ts +1 -1
  11. package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
  12. package/dist/agent-runtime/engine/contracts.d.ts +2 -1
  13. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  14. package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
  15. package/dist/agent-runtime/engine/fork.d.ts +1 -1
  16. package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
  17. package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
  18. package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
  19. package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
  20. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
  21. package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
  22. package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
  23. package/dist/agent-runtime/engine/pieces/verify.js +46 -13
  24. package/dist/agent-runtime/engine/run-status.d.ts +165 -165
  25. package/dist/agent-runtime/engine/runs.d.ts +168 -168
  26. package/dist/agent-runtime/engine/state.d.ts +16 -16
  27. package/dist/agent-runtime/evaluation.d.ts +3 -3
  28. package/dist/agent-runtime/executor-types.d.ts +10 -10
  29. package/dist/agent-runtime/graph/convergence.d.ts +2 -2
  30. package/dist/agent-runtime/graph/convergence.js +11 -0
  31. package/dist/agent-runtime/graph/nodes.js +35 -4
  32. package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
  33. package/dist/agent-runtime/graph/state.d.ts +6 -2
  34. package/dist/agent-runtime/guardrails.d.ts +1 -1
  35. package/dist/agent-runtime/guardrails.js +4 -1
  36. package/dist/agent-runtime/index.d.ts +3 -2
  37. package/dist/agent-runtime/index.js +1 -0
  38. package/dist/agent-runtime/openai-executor.d.ts +9 -9
  39. package/dist/agent-runtime/openspec.d.ts +4 -4
  40. package/dist/agent-runtime/prompts.d.ts +6 -0
  41. package/dist/agent-runtime/prompts.js +32 -8
  42. package/dist/agent-runtime/recovery.d.ts +12 -12
  43. package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
  44. package/dist/agent-runtime/session/application/active-session.js +701 -0
  45. package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
  46. package/dist/agent-runtime/session/application/delegation.js +171 -0
  47. package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
  48. package/dist/agent-runtime/session/application/output-buffer.js +65 -0
  49. package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
  50. package/dist/agent-runtime/session/application/session-service.js +256 -0
  51. package/dist/agent-runtime/session/application/timers.d.ts +15 -0
  52. package/dist/agent-runtime/session/application/timers.js +35 -0
  53. package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
  54. package/dist/agent-runtime/session/domain/descriptor.js +32 -0
  55. package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
  56. package/dist/agent-runtime/session/domain/errors.js +16 -0
  57. package/dist/agent-runtime/session/domain/events.d.ts +147 -0
  58. package/dist/agent-runtime/session/domain/events.js +24 -0
  59. package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
  60. package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
  61. package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
  62. package/dist/agent-runtime/session/domain/interruption.js +39 -0
  63. package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
  64. package/dist/agent-runtime/session/domain/policy.js +104 -0
  65. package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
  66. package/dist/agent-runtime/session/domain/snapshot.js +234 -0
  67. package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
  68. package/dist/agent-runtime/session/domain/state-machines.js +41 -0
  69. package/dist/agent-runtime/session/domain/types.d.ts +130 -0
  70. package/dist/agent-runtime/session/domain/types.js +15 -0
  71. package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
  72. package/dist/agent-runtime/session/domain/usage.js +86 -0
  73. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
  74. package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
  75. package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
  76. package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
  77. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
  78. package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
  79. package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
  80. package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
  81. package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
  82. package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
  83. package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
  84. package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
  85. package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
  86. package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
  87. package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
  88. package/dist/agent-runtime/session/drivers/common/process.js +116 -0
  89. package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
  90. package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
  91. package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
  92. package/dist/agent-runtime/session/drivers/registry.js +38 -0
  93. package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
  94. package/dist/agent-runtime/session/host/protocol.js +88 -0
  95. package/dist/agent-runtime/session/host/server.d.ts +57 -0
  96. package/dist/agent-runtime/session/host/server.js +229 -0
  97. package/dist/agent-runtime/session/index.d.ts +45 -0
  98. package/dist/agent-runtime/session/index.js +74 -0
  99. package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
  100. package/dist/agent-runtime/session/journal/schema.js +44 -0
  101. package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
  102. package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
  103. package/dist/agent-runtime/session/ports.d.ts +192 -0
  104. package/dist/agent-runtime/session/ports.js +1 -0
  105. package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
  106. package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
  107. package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
  108. package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
  109. package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
  110. package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
  111. package/dist/agent-runtime/session/testing/index.d.ts +9 -0
  112. package/dist/agent-runtime/session/testing/index.js +9 -0
  113. package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
  114. package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
  115. package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
  116. package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
  117. package/dist/agent-runtime/verification-proposals.d.ts +9 -0
  118. package/dist/agent-runtime/verification-proposals.js +69 -0
  119. package/dist/agent-runtime/verification-repair.d.ts +31 -2
  120. package/dist/agent-runtime/verification-repair.js +58 -3
  121. package/dist/installer/commands/init.d.ts +1 -1
  122. package/dist/installer/phases/install-config.js +10 -3
  123. package/dist/installer/util/paths.d.ts +25 -1
  124. package/dist/installer/util/registry.js +3 -3
  125. package/dist/pipeline/pipeline-state.d.ts +42 -11
  126. package/dist/pipeline/pipeline-state.js +47 -8
  127. package/dist/shared/specrails-home.d.ts +21 -0
  128. package/dist/shared/specrails-home.js +36 -0
  129. package/integration-contract.json +42 -1
  130. package/package.json +23 -11
  131. 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 full = roleInstructions(descriptor, deps.context, openspec?.[roleId]?.change, { definition: descriptor.prompt, verification }) + '\n## Current workflow task\n' + task;
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, its proposals for repositories without a configured check (the rule the verify piece applies). */
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
- continue;
141
+ return false;
89
142
  const raw = proposal;
90
- if (typeof raw.repositoryId !== 'string' || typeof raw.command !== 'string' || !Array.isArray(raw.args) || !raw.args.every(arg => typeof arg === 'string'))
91
- continue;
92
- if (!inScope.has(raw.repositoryId) || configured.has(raw.repositoryId))
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 { isEnvironmentFailure } from '../../compact/environment.js';
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
- const raw = proposal;
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
- if (!blocker && !receipt.valid && guardrailEnabled(deps.config.guardrails, 'environment-repair') && receipt.commands.some(command => isEnvironmentFailure(command.exitCode, command.output))) {
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 } : {}), ...verificationDiagnostics(receipt) };
203
- const vars = { [progressKey]: { fingerprint, count } };
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) };