specrails-core 6.0.1 → 6.1.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 (164) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-runtime/capabilities.js +3 -1
  3. package/dist/agent-runtime/cli-executor.d.ts +3 -0
  4. package/dist/agent-runtime/cli-executor.js +50 -13
  5. package/dist/agent-runtime/cli-process.js +6 -2
  6. package/dist/agent-runtime/cli.d.ts +15 -0
  7. package/dist/agent-runtime/cli.js +43 -9
  8. package/dist/agent-runtime/compact/chat-client.js +2 -1
  9. package/dist/agent-runtime/config.d.ts +4 -1
  10. package/dist/agent-runtime/config.js +41 -3
  11. package/dist/agent-runtime/core-host.d.ts +9 -3
  12. package/dist/agent-runtime/core-host.js +21 -18
  13. package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
  14. package/dist/agent-runtime/efficiency-summary.js +30 -27
  15. package/dist/agent-runtime/efficiency-types.d.ts +4 -4
  16. package/dist/agent-runtime/efficiency.d.ts +1 -1
  17. package/dist/agent-runtime/efficiency.js +7 -6
  18. package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
  19. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
  20. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
  21. package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
  22. package/dist/agent-runtime/engine/budget.d.ts +9 -0
  23. package/dist/agent-runtime/engine/budget.js +33 -0
  24. package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
  25. package/dist/agent-runtime/engine/canonical-json.js +162 -0
  26. package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
  27. package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
  28. package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
  29. package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
  30. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
  31. package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
  32. package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
  33. package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
  34. package/dist/agent-runtime/engine/cli.d.ts +10 -0
  35. package/dist/agent-runtime/engine/cli.js +172 -0
  36. package/dist/agent-runtime/engine/compiler.d.ts +23 -0
  37. package/dist/agent-runtime/engine/compiler.js +261 -0
  38. package/dist/agent-runtime/engine/composition.d.ts +16 -0
  39. package/dist/agent-runtime/engine/composition.js +28 -0
  40. package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
  41. package/dist/agent-runtime/engine/concurrency.js +55 -0
  42. package/dist/agent-runtime/engine/contracts.d.ts +273 -0
  43. package/dist/agent-runtime/engine/contracts.js +11 -0
  44. package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
  45. package/dist/agent-runtime/engine/definition-schema.js +313 -0
  46. package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
  47. package/dist/agent-runtime/engine/definition-types.js +3 -0
  48. package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
  49. package/dist/agent-runtime/engine/definition-validator.js +187 -0
  50. package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
  51. package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
  52. package/dist/agent-runtime/engine/events.d.ts +19 -0
  53. package/dist/agent-runtime/engine/events.js +88 -0
  54. package/dist/agent-runtime/engine/execution.d.ts +29 -0
  55. package/dist/agent-runtime/engine/execution.js +121 -0
  56. package/dist/agent-runtime/engine/expressions.d.ts +11 -0
  57. package/dist/agent-runtime/engine/expressions.js +172 -0
  58. package/dist/agent-runtime/engine/fork.d.ts +28 -0
  59. package/dist/agent-runtime/engine/fork.js +211 -0
  60. package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
  61. package/dist/agent-runtime/engine/graph-description.js +16 -0
  62. package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
  63. package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
  64. package/dist/agent-runtime/engine/index.d.ts +14 -0
  65. package/dist/agent-runtime/engine/index.js +14 -0
  66. package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
  67. package/dist/agent-runtime/engine/invocation-context.js +128 -0
  68. package/dist/agent-runtime/engine/otel.d.ts +15 -0
  69. package/dist/agent-runtime/engine/otel.js +132 -0
  70. package/dist/agent-runtime/engine/output.d.ts +4 -0
  71. package/dist/agent-runtime/engine/output.js +73 -0
  72. package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
  73. package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
  74. package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
  75. package/dist/agent-runtime/engine/piece-registry.js +62 -0
  76. package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
  77. package/dist/agent-runtime/engine/pieces/assign.js +35 -0
  78. package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
  79. package/dist/agent-runtime/engine/pieces/control.js +49 -0
  80. package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
  81. package/dist/agent-runtime/engine/pieces/decider.js +70 -0
  82. package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
  83. package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
  84. package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
  85. package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
  86. package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
  87. package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
  88. package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
  89. package/dist/agent-runtime/engine/pieces/index.js +22 -0
  90. package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
  91. package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
  92. package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
  93. package/dist/agent-runtime/engine/pieces/ports.js +1 -0
  94. package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
  95. package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
  96. package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
  97. package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
  98. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
  99. package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
  100. package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
  101. package/dist/agent-runtime/engine/pieces/shared.js +40 -0
  102. package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
  103. package/dist/agent-runtime/engine/pieces/shell.js +47 -0
  104. package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
  105. package/dist/agent-runtime/engine/pieces/verify.js +52 -0
  106. package/dist/agent-runtime/engine/preflight.d.ts +27 -0
  107. package/dist/agent-runtime/engine/preflight.js +54 -0
  108. package/dist/agent-runtime/engine/regex.d.ts +3 -0
  109. package/dist/agent-runtime/engine/regex.js +19 -0
  110. package/dist/agent-runtime/engine/run-status.d.ts +563 -0
  111. package/dist/agent-runtime/engine/run-status.js +61 -0
  112. package/dist/agent-runtime/engine/runs.d.ts +605 -0
  113. package/dist/agent-runtime/engine/runs.js +247 -0
  114. package/dist/agent-runtime/engine/state.d.ts +64 -0
  115. package/dist/agent-runtime/engine/state.js +98 -0
  116. package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
  117. package/dist/agent-runtime/engine/steering/inbox.js +113 -0
  118. package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
  119. package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
  120. package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
  121. package/dist/agent-runtime/engine/storage/private-path.js +74 -0
  122. package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
  123. package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
  124. package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
  125. package/dist/agent-runtime/evaluation-corpus.js +4 -0
  126. package/dist/agent-runtime/evaluation.d.ts +9 -3
  127. package/dist/agent-runtime/evaluation.js +50 -8
  128. package/dist/agent-runtime/executor-types.d.ts +53 -3
  129. package/dist/agent-runtime/executor-types.js +35 -2
  130. package/dist/agent-runtime/executors.js +2 -2
  131. package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
  132. package/dist/agent-runtime/graph/artifacts.js +82 -9
  133. package/dist/agent-runtime/graph/nodes.d.ts +2 -0
  134. package/dist/agent-runtime/graph/nodes.js +2 -2
  135. package/dist/agent-runtime/graph/roles.d.ts +6 -0
  136. package/dist/agent-runtime/graph/roles.js +23 -17
  137. package/dist/agent-runtime/kimi-acp.js +6 -6
  138. package/dist/agent-runtime/openai-executor.js +17 -12
  139. package/dist/agent-runtime/openspec.d.ts +17 -2
  140. package/dist/agent-runtime/openspec.js +54 -20
  141. package/dist/agent-runtime/prompts.d.ts +4 -3
  142. package/dist/agent-runtime/prompts.js +26 -5
  143. package/dist/agent-runtime/recovery.d.ts +1 -1
  144. package/dist/agent-runtime/repository-context.js +1 -1
  145. package/dist/agent-runtime/role-routing.js +4 -2
  146. package/dist/agent-runtime/role-state.d.ts +5 -0
  147. package/dist/agent-runtime/verification-plan.d.ts +2 -0
  148. package/dist/agent-runtime/verification-plan.js +10 -0
  149. package/dist/agent-runtime/workflow-types.d.ts +2 -0
  150. package/dist/agent-runtime/workflow.d.ts +3 -1
  151. package/dist/agent-runtime/workflow.js +7 -3
  152. package/dist/agent-runtime/workspace-tools.d.ts +2 -2
  153. package/dist/agent-runtime/workspace-tools.js +6 -6
  154. package/dist/installer/cli.js +5 -0
  155. package/dist/installer/phases/scaffold.d.ts +2 -2
  156. package/dist/installer/phases/scaffold.js +3 -7
  157. package/dist/pipeline/pipeline-state.d.ts +38 -4
  158. package/dist/pipeline/pipeline-state.js +94 -21
  159. package/integration-contract.json +83 -11
  160. package/package.json +12 -5
  161. package/schemas/agent-runtime.schema.json +93 -1
  162. package/schemas/workflow-definition.schema.json +312 -0
  163. package/templates/kimi/specrails/run-skill.mjs +15 -1
  164. package/templates/commands/specrails/batch-implement.md +0 -32
@@ -10,6 +10,13 @@ import { ExecutorRegistry } from './executors.js';
10
10
  import { OpenSpecTools } from './openspec.js';
11
11
  import { validatePipelineContext } from '../pipeline/pipeline-state.js';
12
12
  import { runtimeEfficiency } from './efficiency.js';
13
+ import { validateWorkflowDefinition } from './engine/definition-validator.js';
14
+ import { validationPieceRegistry } from './engine/pieces/index.js';
15
+ import { configuredRoles } from './engine/preflight.js';
16
+ import { createRun, definitionRunDirectory } from './engine/runs.js';
17
+ import { observeLedger } from './engine/run-status.js';
18
+ import { RunDatabase } from './engine/checkpoint/database.js';
19
+ import { ledgerWorkflowState } from './engine/invocation-context.js';
13
20
  function oracle(test, file) {
14
21
  return spawnSync(process.execPath, ['-e', 'const assert = require("node:assert/strict"); const api = require(process.argv[1]);' + test.oracle, file], { encoding: 'utf8', timeout: 10000 }).status === 0;
15
22
  }
@@ -26,12 +33,13 @@ function prepare(test, root) {
26
33
  });
27
34
  return validatePipelineContext({ schemaVersion: 1, runId: randomUUID(), backlogRoot: root, artifactRoot: repositories[0].path, artifactRepositoryId: repositories[0].id, repositories, ownership: { git: 'host', backlog: 'host', worktrees: 'host' }, specs: [{ id: 1, title: test.id, description: test.description + ' Implement behavior in implementation.cjs in every selected repository.', repositoryIds: test.repositories, acceptanceCriteria: [test.description] }] });
28
35
  }
29
- function fixtureRegistry(test, context) {
36
+ function fixtureRegistry(test, context, onPrompt) {
30
37
  let developers = 0, reviews = 0;
31
38
  const sessions = new Map();
32
39
  return new ExecutorRegistry().register('fixture', {
33
40
  capabilities: () => ({ transport: 'offline-fixture', continuation: 'supported', effortSupport: 'unsupported', supportedEfforts: [], observedModel: false, observedEffort: false }),
34
41
  async execute(request) {
42
+ onPrompt?.(request);
35
43
  const sessionId = 'fixture-' + request.role;
36
44
  if (request.resumeSessionId && !sessions.has(request.resumeSessionId))
37
45
  throw new Error('Fixture session was not restored');
@@ -57,11 +65,11 @@ function fixtureRegistry(test, context) {
57
65
  }
58
66
  else {
59
67
  await tools.execute({ action: 'instructions', artifact: 'apply' });
60
- if (request.role === 'developer') {
68
+ if (request.role === 'developer' || request.role === 'fixer') {
61
69
  developers++;
62
70
  for (const repo of context.repositories)
63
71
  writeFileSync(path.join(repo.path, 'implementation.cjs'), test.correction && developers === 1 ? test.defects[0] : test.solution);
64
- writeFileSync(path.join(context.artifactRoot, 'openspec/changes/evaluation-change/tasks.md'), '## 1. Implementation\n- [x] 1.1 Implement and test the requested behavior\n');
72
+ await tools.execute({ action: 'write_artifact', path: 'tasks.md', content: '## 1. Implementation\n- [x] 1.1 Implement and test the requested behavior\n' });
65
73
  output = { summary: 'Implemented fixture behavior', files: ['implementation.cjs'], tests: [], verification: 'Core executes the frozen checks', incomplete: [] };
66
74
  }
67
75
  else {
@@ -75,18 +83,23 @@ function fixtureRegistry(test, context) {
75
83
  });
76
84
  }
77
85
  export async function runEvaluation(options) {
86
+ if (options.capturePrompts && options.real)
87
+ throw new Error('Prompt capture is limited to offline fixture evaluation');
78
88
  const repetitions = options.repetitions ?? 1;
79
89
  if (!Number.isSafeInteger(repetitions) || repetitions < 1 || repetitions > 20)
80
90
  throw new Error('Evaluation repetitions must be 1–20');
81
91
  if (options.real && (!options.config || !Number.isFinite(options.maxCostUsd) || options.maxCostUsd <= 0 || Object.values(options.config.agents).some(role => !role.model)))
82
92
  throw new Error('Real evaluation requires explicit per-role models and a positive aggregate spend limit');
93
+ const corpus = options.caseIds ? EVALUATION_CORPUS.filter(item => options.caseIds.includes(item.id)) : EVALUATION_CORPUS;
94
+ if (!corpus.length || options.caseIds?.some(id => !corpus.some(item => item.id === id)))
95
+ throw new Error('Unknown or empty evaluation case selection');
83
96
  const identity = coreRuntimeIdentity();
84
97
  const observations = [];
85
- let spend = 0, stopReason = null;
98
+ let spend = 0, stopReason = null, evaluatedDefinitionVersion = null;
86
99
  const root = mkdtempSync(path.join(tmpdir(), 'specrails-evaluation-'));
87
100
  try {
88
101
  for (let repeat = 0; repeat < repetitions && !stopReason; repeat++)
89
- for (const test of EVALUATION_CORPUS) {
102
+ for (const test of corpus) {
90
103
  // Check the independent oracle itself against defective candidates.
91
104
  const probe = path.join(root, 'oracle-probe.cjs');
92
105
  writeFileSync(probe, test.solution);
@@ -113,7 +126,33 @@ export async function runEvaluation(options) {
113
126
  const started = Date.now();
114
127
  let state;
115
128
  try {
116
- state = await runCoreWorkflow({ context, change: 'evaluation-change', config, ...(options.real ? {} : { registry: fixtureRegistry(test, context) }) });
129
+ let promptNumber = 0;
130
+ const capture = options.capturePrompts ? (request) => {
131
+ const directory = path.join(options.output, 'prompts', test.id + '-' + repeat + '-' + mode);
132
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
133
+ const filename = String(++promptNumber).padStart(2, '0') + '-' + request.role + (request.stance ? '-' + request.stance : '');
134
+ writeFileSync(path.join(directory, filename + '.txt'), request.prompt, { mode: 0o600 });
135
+ writeFileSync(path.join(directory, filename + '.json'), JSON.stringify({ role: request.role, stance: request.stance ?? null,
136
+ model: request.model ?? null, resumeSessionId: request.resumeSessionId ?? null, promptBytes: Buffer.byteLength(request.prompt),
137
+ sections: request.prompt.split(/(?=^#{1,4} )/m).map(section => ({ heading: section.split('\n')[0].slice(0, 150), bytes: Buffer.byteLength(section) })) }, null, 2) + '\n', { mode: 0o600 });
138
+ } : undefined;
139
+ const registry = options.real ? undefined : fixtureRegistry(test, context, capture);
140
+ if (options.definition === undefined)
141
+ state = await runCoreWorkflow({ context, change: 'evaluation-change', config, ...(registry ? { registry } : {}) });
142
+ else {
143
+ const validation = validateWorkflowDefinition(options.definition, validationPieceRegistry(), configuredRoles(config), { published: true });
144
+ if (!validation.ok)
145
+ throw new Error('Evaluation definition failed validation: ' + JSON.stringify(validation.errors));
146
+ evaluatedDefinitionVersion = validation.version;
147
+ await createRun({ context, change: 'evaluation-change', config, definition: validation.definition, ...(registry ? { registry } : {}) });
148
+ const database = await RunDatabase.open(path.join(definitionRunDirectory(context), 'run.sqlite'), { readOnly: true });
149
+ try {
150
+ state = ledgerWorkflowState(observeLedger(database), validation.definition, '*');
151
+ }
152
+ finally {
153
+ database.close();
154
+ }
155
+ }
117
156
  }
118
157
  catch (error) {
119
158
  observations.push({ caseId: test.id, repeat, mode, status: 'blocked', independentAccepted: false, error: error instanceof Error ? error.message : String(error), runtimeIdentity: identity, taskHash: fingerprint(test), oracleHash: fingerprint(test.oracle), repositories: context.repositories.map(({ id, baseSha }) => ({ id, baseSha })), configHash: fingerprint(config) });
@@ -145,7 +184,7 @@ export async function runEvaluation(options) {
145
184
  return [mode, { samples: rows.length, independentlyAccepted: accepted, costPerAcceptedUsd: options.real && knownCost !== null && accepted ? knownCost / accepted : null, medianActiveDurationMs: median(durations), minActiveDurationMs: durations.length ? Math.min(...durations) : null, maxActiveDurationMs: durations.length ? Math.max(...durations) : null, standardDeviationMs: average === null ? null : Math.sqrt(durations.reduce((sum, value) => sum + (value - average) ** 2, 0) / durations.length) }];
146
185
  }));
147
186
  const full = groups.full, optimized = groups.optimized;
148
- const paired = observations.length === EVALUATION_CORPUS.length * repetitions * 2 && full.samples === optimized.samples;
187
+ const paired = observations.length === corpus.length * repetitions * 2 && full.samples === optimized.samples;
149
188
  const noObservedQualityDrop = paired && optimized.independentlyAccepted >= full.independentlyAccepted;
150
189
  const promptPairs = observations.filter(row => row.mode === 'full' && row.caseId === 'verification-correction').map(row => {
151
190
  const other = observations.find(item => item.mode === 'optimized' && item.caseId === row.caseId && item.repeat === row.repeat);
@@ -158,7 +197,10 @@ export async function runEvaluation(options) {
158
197
  return Array.isArray(row.invocations) && Array.isArray(other?.invocations) && other.invocations.length <= row.invocations.length;
159
198
  });
160
199
  const monetaryConclusion = options.real && paired && full.costPerAcceptedUsd !== null && optimized.costPerAcceptedUsd !== null && full.costPerAcceptedUsd > 0 ? optimized.costPerAcceptedUsd <= full.costPerAcceptedUsd * 0.8 && noObservedQualityDrop ? 'target-met-in-this-sample' : 'target-not-met-in-this-sample' : 'inconclusive';
161
- const report = { schemaVersion: 1, experiment: Object.values(options.config?.agents ?? {}).some(role => role.escalation) ? 'routing' : 'same-model', noExtraInvocations, mode: options.real ? 'real' : 'offline', runtimeIdentity: identity, corpusHash: fingerprint(EVALUATION_CORPUS), measurement: options.real ? 'reported-provider-usage' : 'synthetic-fixture-usage; no actual AI savings measured', monetaryTarget: 0.2, monetaryConclusion, groups, noObservedQualityDrop, promptPairs, correctionPromptTargetMet: promptPairs.length > 0 && promptPairs.every(pair => pair.reduction !== null && pair.reduction >= 0.4), sampleLimitations: 'Small descriptive paired sample; does not establish a universal quality or savings guarantee.', stopReason, reportedSpendUsd: options.real ? spend : 0, observations };
200
+ const report = { schemaVersion: 1, isDefinitionEvaluation: options.definition !== undefined,
201
+ ...(options.definition === undefined ? {} : { definitionHash: evaluatedDefinitionVersion }),
202
+ acceptedCases: observations.filter(row => row.independentAccepted).length,
203
+ allCasesAccepted: paired && observations.every(row => row.independentAccepted), experiment: Object.values(options.config?.agents ?? {}).some(role => role.escalation) ? 'routing' : 'same-model', noExtraInvocations, mode: options.real ? 'real' : 'offline', runtimeIdentity: identity, corpusHash: fingerprint(corpus), measurement: options.real ? 'reported-provider-usage' : 'synthetic-fixture-usage; no actual AI savings measured', monetaryTarget: 0.2, monetaryConclusion, groups, noObservedQualityDrop, promptPairs, correctionPromptTargetMet: promptPairs.length > 0 && promptPairs.every(pair => pair.reduction !== null && pair.reduction >= 0.4), sampleLimitations: 'Small descriptive paired sample; does not establish a universal quality or savings guarantee.', stopReason, reportedSpendUsd: options.real ? spend : 0, observations };
162
204
  mkdirSync(options.output, { recursive: true });
163
205
  writeFileSync(path.join(options.output, 'evaluation.json'), JSON.stringify(report, null, 2) + '\n');
164
206
  writeFileSync(path.join(options.output, 'evaluation.md'), `# Implementation efficiency evaluation\n\nMode: ${report.mode}. ${report.measurement}.\n\nMonetary target: 20% lower aggregate cost per independently accepted output. Conclusion: ${monetaryConclusion}.\n\n${JSON.stringify(groups, null, 2)}\n\nCorrection prompt comparison: ${JSON.stringify(promptPairs)}\n\n${observations.map(row => `- ${row.caseId}, ${row.mode}: ${row.status}; independent acceptance ${row.independentAccepted}; ${row.activeDurationMs} ms`).join('\n')}\n\n${stopReason ?? (options.real ? report.sampleLimitations : 'No paid benchmark has established monetary savings.')}\n`);
@@ -3,7 +3,31 @@ import type { CacheTokenUsage } from './efficiency-types.js';
3
3
  import type { GuardrailSettings } from './guardrails.js';
4
4
  export declare const DEFAULT_AGENT_TIMEOUT_MS: number;
5
5
  export declare const DEFAULT_AGENT_IDLE_TIMEOUT_MS: number;
6
- export type AgentRole = 'architect' | 'developer' | 'reviewer';
6
+ export declare const BUILTIN_ROLES: readonly ["architect", "developer", "reviewer"];
7
+ export type BuiltinAgentRole = typeof BUILTIN_ROLES[number];
8
+ export type AgentRole = string;
9
+ export type AgentAccess = 'read' | 'write';
10
+ export type ArtifactAccess = 'none' | 'tasks-checkboxes' | 'all';
11
+ export type OpenSpecSkill = 'openspec-ff-change' | 'openspec-apply-change' | 'openspec-verify-change';
12
+ export declare const ROLE_ID: RegExp;
13
+ export declare function isBuiltinRole(role: string): role is BuiltinAgentRole;
14
+ export declare const BUILTIN_ROLE_POLICY: {
15
+ readonly architect: {
16
+ readonly access: "read";
17
+ readonly artifacts: "all";
18
+ readonly openspecSkill: "openspec-ff-change";
19
+ };
20
+ readonly developer: {
21
+ readonly access: "write";
22
+ readonly artifacts: "tasks-checkboxes";
23
+ readonly openspecSkill: "openspec-apply-change";
24
+ };
25
+ readonly reviewer: {
26
+ readonly access: "read";
27
+ readonly artifacts: "none";
28
+ readonly openspecSkill: "openspec-verify-change";
29
+ };
30
+ };
7
31
  /** Who an observer sees acting: a pipeline role, or the FIXER stance the developer step takes on a correction round. */
8
32
  export type AgentEventRole = AgentRole | 'fixer';
9
33
  export type CliProvider = 'claude' | 'codex' | 'gemini' | 'kimi';
@@ -37,6 +61,15 @@ export interface RuntimeAgentConfig {
37
61
  effort?: string;
38
62
  };
39
63
  }
64
+ export interface RuntimeRoleConfig extends RuntimeAgentConfig {
65
+ access: AgentAccess;
66
+ artifacts: ArtifactAccess;
67
+ prompt?: string;
68
+ openspecSkill?: OpenSpecSkill;
69
+ }
70
+ export interface RoleDescriptor extends RuntimeRoleConfig {
71
+ id: AgentRole;
72
+ }
40
73
  export interface EfficiencyPolicy {
41
74
  schemaVersion: 1;
42
75
  contextMode?: 'full' | 'incremental';
@@ -64,7 +97,8 @@ export interface RuntimeConfig {
64
97
  efficiency?: EfficiencyPolicy;
65
98
  enabled: boolean;
66
99
  providers: RuntimeProviderConfig[];
67
- agents: Record<AgentRole, RuntimeAgentConfig>;
100
+ agents: Record<BuiltinAgentRole, RuntimeAgentConfig>;
101
+ roles?: Record<AgentRole, RuntimeRoleConfig>;
68
102
  /** Optional engine for correction rounds (after a failed verification or a rejected review); unset ⇒ the developer corrects. */
69
103
  fixer?: RuntimeAgentConfig;
70
104
  limits?: {
@@ -110,6 +144,14 @@ export interface AgentEvent {
110
144
  export interface AgentRequest {
111
145
  openspec?: import('./openspec.js').OpenSpecRoleContext;
112
146
  role: AgentRole;
147
+ /** Optional only for backward-compatible built-in requests. Custom roles require explicit policy. */
148
+ access?: AgentAccess;
149
+ artifacts?: ArtifactAccess;
150
+ instructions?: 'role' | 'none';
151
+ nativeCommand?: {
152
+ id: string;
153
+ args?: string;
154
+ };
113
155
  prompt: string;
114
156
  cwd: string;
115
157
  allowedRoots: string[];
@@ -118,8 +160,9 @@ export interface AgentRequest {
118
160
  /** Local engines: private thinking on/off for this invocation (see RuntimeAgentConfig.thinking). */
119
161
  thinking?: 'on' | 'off';
120
162
  maxTurns?: number;
163
+ /** Total invocation limit; zero disables this timer without disabling cancellation or run budgets. */
121
164
  timeoutMs?: number;
122
- /** Maximum silence from the provider; independent of the total invocation limit. */
165
+ /** Maximum silence from the provider; zero disables the idle timer. */
123
166
  idleTimeoutMs?: number;
124
167
  maxTokens?: number;
125
168
  maxCostUsd?: number;
@@ -157,3 +200,10 @@ export declare class AgentExecutionError extends Error {
157
200
  }
158
201
  export declare function unknownUsage(): AgentUsage;
159
202
  export declare function validateAgentRequest(request: AgentRequest): void;
203
+ /** Resolve compatible defaults without changing the caller's frozen request. */
204
+ export declare function requestPolicy(request: Pick<AgentRequest, 'role' | 'access' | 'artifacts' | 'instructions'>): {
205
+ access: AgentAccess;
206
+ artifacts: ArtifactAccess;
207
+ instructions: 'role' | 'none';
208
+ };
209
+ export declare function normalizeAgentRequest(request: AgentRequest): AgentRequest & ReturnType<typeof requestPolicy>;
@@ -1,5 +1,13 @@
1
1
  export const DEFAULT_AGENT_TIMEOUT_MS = 60 * 60_000;
2
2
  export const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 15 * 60_000;
3
+ export const BUILTIN_ROLES = ['architect', 'developer', 'reviewer'];
4
+ export const ROLE_ID = /^[a-z][a-z0-9-]{0,63}$/;
5
+ export function isBuiltinRole(role) { return BUILTIN_ROLES.includes(role); }
6
+ export const BUILTIN_ROLE_POLICY = {
7
+ architect: { access: 'read', artifacts: 'all', openspecSkill: 'openspec-ff-change' },
8
+ developer: { access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change' },
9
+ reviewer: { access: 'read', artifacts: 'none', openspecSkill: 'openspec-verify-change' },
10
+ };
3
11
  export class AgentExecutionError extends Error {
4
12
  code;
5
13
  usage;
@@ -15,13 +23,21 @@ export class AgentExecutionError extends Error {
15
23
  }
16
24
  export function unknownUsage() { return { inputTokens: null, outputTokens: null, costUsd: null }; }
17
25
  export function validateAgentRequest(request) {
18
- if (!['architect', 'developer', 'reviewer'].includes(request.role) || typeof request.prompt !== 'string' || !request.prompt.trim() || request.prompt.includes('\0'))
26
+ if (typeof request.role !== 'string' || !ROLE_ID.test(request.role) || typeof request.prompt !== 'string' || request.prompt.includes('\0') || (!request.nativeCommand && !request.prompt.trim()))
19
27
  throw new AgentExecutionError('Invalid role request', 'invalid_request');
28
+ const policy = requestPolicy(request);
29
+ if (request.nativeCommand !== undefined) {
30
+ const command = request.nativeCommand;
31
+ if (!command || typeof command !== 'object' || Array.isArray(command) || Object.keys(command).some(key => !['id', 'args'].includes(key)) || typeof command.id !== 'string' || !/^[a-z][a-z0-9:_-]{0,63}$/.test(command.id) || (command.args !== undefined && (typeof command.args !== 'string' || command.args.includes('\0'))) || request.prompt.trim() || policy.instructions !== 'none' || request.openspec)
32
+ throw new AgentExecutionError('Invalid native command request', 'invalid_request');
33
+ }
34
+ if (policy.instructions === 'none' && request.openspec)
35
+ throw new AgentExecutionError('Free prompts cannot bind role instructions', 'invalid_request');
20
36
  if (request.model !== undefined && (typeof request.model !== 'string' || !request.model.trim() || request.model.length > 256 || /^-/.test(request.model) || /[\0\r\n]/.test(request.model)))
21
37
  throw new AgentExecutionError('Invalid model identifier', 'invalid_model');
22
38
  for (const key of ['maxTurns', 'timeoutMs', 'idleTimeoutMs', 'maxTokens', 'maxCostUsd']) {
23
39
  const value = request[key];
24
- if (value !== undefined && (!Number.isFinite(value) || value <= 0 || (key !== 'maxCostUsd' && !Number.isSafeInteger(value))))
40
+ if (value !== undefined && (!Number.isFinite(value) || (value < 0 || (value === 0 && key !== 'timeoutMs' && key !== 'idleTimeoutMs')) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value))))
25
41
  throw new AgentExecutionError(`Invalid ${key}`, 'invalid_limit');
26
42
  }
27
43
  if (request.resumeSessionId !== undefined && (typeof request.resumeSessionId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(request.resumeSessionId)))
@@ -31,3 +47,20 @@ export function validateAgentRequest(request) {
31
47
  if (request.signal?.aborted)
32
48
  throw new AgentExecutionError('Agent cancelled', 'aborted');
33
49
  }
50
+ /** Resolve compatible defaults without changing the caller's frozen request. */
51
+ export function requestPolicy(request) {
52
+ const legacy = isBuiltinRole(request.role) ? BUILTIN_ROLE_POLICY[request.role] : undefined;
53
+ const access = request.access ?? legacy?.access;
54
+ const artifacts = request.artifacts ?? legacy?.artifacts;
55
+ const instructions = request.instructions ?? (legacy ? 'role' : undefined);
56
+ if ((access !== 'read' && access !== 'write') || (artifacts !== 'none' && artifacts !== 'tasks-checkboxes' && artifacts !== 'all') || (instructions !== 'role' && instructions !== 'none'))
57
+ throw new AgentExecutionError('Explicit access, artifacts and instructions are required', 'invalid_request');
58
+ return { access, artifacts, instructions };
59
+ }
60
+ export function normalizeAgentRequest(request) {
61
+ validateAgentRequest(request);
62
+ const policy = requestPolicy(request);
63
+ const legacy = isBuiltinRole(request.role) ? BUILTIN_ROLE_POLICY[request.role] : undefined;
64
+ const unchangedBinding = request.openspec && request.openspec.role === request.role && request.openspec.access === undefined && request.openspec.artifacts === undefined && policy.access === legacy?.access && policy.artifacts === legacy?.artifacts;
65
+ return { ...request, ...policy, ...(request.openspec && !unchangedBinding ? { openspec: { ...request.openspec, access: policy.access, artifacts: policy.artifacts } } : {}) };
66
+ }
@@ -1,4 +1,4 @@
1
- import { AgentExecutionError, validateAgentRequest } from './executor-types.js';
1
+ import { AgentExecutionError, normalizeAgentRequest } from './executor-types.js';
2
2
  import { validateRuntimeConfig } from './config.js';
3
3
  import { CliExecutor } from './cli-executor.js';
4
4
  import { OpenAICompatibleExecutor } from './openai-executor.js';
@@ -24,7 +24,7 @@ export class ExecutorRegistry {
24
24
  async capabilities(id, model) { return await this.get(id).capabilities?.(model) ?? unknownCapabilities('registered:' + id); }
25
25
  validateLimits(id, limits) { this.get(id).validateLimits?.(limits); }
26
26
  async execute(id, request) {
27
- validateAgentRequest(request);
27
+ request = normalizeAgentRequest(request);
28
28
  if (request.effort !== undefined)
29
29
  assertEffortSupported(request, await this.capabilities(id, request.model));
30
30
  const result = await this.get(id).execute(request);
@@ -1,6 +1,8 @@
1
1
  import { type PipelineContext, type PipelineState, type VerificationCommand } from '../../pipeline/pipeline-state.js';
2
2
  import type { DesignConfidence } from './state.js';
3
3
  export declare const SLUG: RegExp;
4
+ /** Historical target lookup, not an archive or verification receipt. */
5
+ export declare function archivedOpenSpecChange(context: PipelineContext, change: string): string | undefined;
4
6
  export declare function object(value: unknown): Record<string, unknown>;
5
7
  /** Accepts the object wherever the model put it: bare, fenced, or surrounded by commentary. */
6
8
  export declare function parseAgentObject(text: string): Record<string, unknown>;
@@ -23,4 +25,9 @@ export declare function writeDesignConfidence(context: PipelineContext, change:
23
25
  }): void;
24
26
  /** Commands the architect proposed for repositories the configuration leaves uncovered. */
25
27
  export declare function proposedVerification(context: PipelineContext, configured: VerificationCommand[], verification: unknown): VerificationCommand[];
28
+ /** Recoverable pinned-CLI archive for ledger-only runs; no implementation journal. */
29
+ export declare function archiveOpenSpecChange(context: PipelineContext, change: string, adapter: {
30
+ directory: string;
31
+ beforePublish(): void;
32
+ }, signal?: AbortSignal): Promise<void>;
26
33
  export declare function archive(context: PipelineContext, change: string, signal?: AbortSignal): Promise<void>;
@@ -5,6 +5,23 @@ import path from 'node:path';
5
5
  import { checkArchive, pipelineStateDirectory, transitionPipeline, validateVerificationRequest, } from '../../pipeline/pipeline-state.js';
6
6
  import { assertProposalInScope, withScopeDefault } from '../change-scope.js';
7
7
  export const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ /** Historical target lookup, not an archive or verification receipt. */
9
+ export function archivedOpenSpecChange(context, change) {
10
+ if (!SLUG.test(change) || change.length > 64)
11
+ throw new Error('Invalid OpenSpec change identifier');
12
+ if (existsSync(artifactPath(context.artifactRoot, 'openspec/changes/' + change)))
13
+ return undefined;
14
+ const relative = 'openspec/changes/archive', directory = artifactPath(context.artifactRoot, relative);
15
+ if (!existsSync(directory))
16
+ return undefined;
17
+ const candidate = readdirSync(directory, { withFileTypes: true }).filter(entry => entry.isDirectory() &&
18
+ (entry.name === change || /^\d{4}-\d{2}-\d{2}-/.test(entry.name) && entry.name.slice(11) === change))
19
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'))[0];
20
+ if (!candidate)
21
+ return undefined;
22
+ artifactPath(context.artifactRoot, relative + '/' + candidate.name);
23
+ return relative + '/' + candidate.name;
24
+ }
8
25
  export function object(value) {
9
26
  if (!value || typeof value !== 'object' || Array.isArray(value))
10
27
  throw new Error('Expected a structured JSON object from agent');
@@ -114,8 +131,8 @@ export function proposedVerification(context, configured, verification) {
114
131
  }
115
132
  /** Publish an archive prepared by the real CLI. The durable write set makes a
116
133
  * crash between main-spec updates recoverable without applying a delta twice. */
117
- async function archiveWithOpenSpec(context, change, active, signal) {
118
- const directory = pipelineStateDirectory(context);
134
+ async function archiveWithOpenSpec(context, change, active, signal, adapter) {
135
+ const directory = adapter?.directory ?? pipelineStateDirectory(context);
119
136
  const receipt = path.join(directory, 'openspec-archive.json');
120
137
  const files = (root, prefix = '') => {
121
138
  if (!existsSync(root))
@@ -127,7 +144,8 @@ async function archiveWithOpenSpec(context, change, active, signal) {
127
144
  return entry.isDirectory() ? files(path.join(root, entry.name), relative + '/') : [relative];
128
145
  });
129
146
  };
130
- const activeHash = hash(JSON.stringify(files(active).sort().map(file => [file, readFileSync(artifactPath(active, file), 'utf8')])));
147
+ const treeHash = (root) => hash(JSON.stringify(files(root).sort().map(file => [file, readFileSync(artifactPath(root, file), 'utf8')])));
148
+ const activeHash = treeHash(active);
131
149
  let plan;
132
150
  if (existsSync(receipt))
133
151
  plan = JSON.parse(readFileSync(receipt, 'utf8'));
@@ -137,6 +155,22 @@ async function archiveWithOpenSpec(context, change, active, signal) {
137
155
  const source = artifactPath(context.artifactRoot, 'openspec');
138
156
  files(source); // Reject symlinks before copying or invoking the external CLI.
139
157
  cpSync(source, path.join(staging, 'openspec'), { recursive: true });
158
+ // A fork prepares its delta against checkpoint-owned main specs. Publish
159
+ // still compares all current preimages below; no source files are rewound.
160
+ const forkBase = path.join(directory, 'openspec-archive-base.json');
161
+ let beforeRoot = path.join(source, 'specs');
162
+ let sourceChange;
163
+ if (existsSync(forkBase)) {
164
+ const baseline = object(JSON.parse(readFileSync(forkBase, 'utf8')));
165
+ if (baseline.schemaVersion !== 1 || baseline.directory !== 'agent-workflow/openspec-base-specs' || typeof baseline.snapshot !== 'string' || !/^[a-f0-9]{64}$/.test(baseline.snapshot)
166
+ || typeof baseline.sourceChange !== 'string' || baseline.sourceChange.length > 64 || !SLUG.test(baseline.sourceChange))
167
+ throw new Error('Invalid fork archive baseline');
168
+ sourceChange = baseline.sourceChange;
169
+ beforeRoot = child(directory, baseline.directory);
170
+ files(beforeRoot);
171
+ rmSync(path.join(staging, 'openspec/specs'), { recursive: true, force: true });
172
+ cpSync(beforeRoot, path.join(staging, 'openspec/specs'), { recursive: true });
173
+ }
140
174
  const output = await runOpenSpec(resolveOpenSpecCli(), staging, ['archive', change, '--yes'], signal);
141
175
  const archiveRoot = path.join(staging, 'openspec/changes/archive');
142
176
  const archives = existsSync(archiveRoot) ? readdirSync(archiveRoot).filter(name => name.endsWith('-' + change)) : [];
@@ -151,20 +185,40 @@ async function archiveWithOpenSpec(context, change, active, signal) {
151
185
  const destination = 'openspec/changes/archive/' + archives[0];
152
186
  if (existsSync(artifactPath(context.artifactRoot, destination)))
153
187
  throw new Error('OpenSpec archive destination already exists');
154
- const beforeRoot = path.join(source, 'specs'), afterRoot = path.join(staging, 'openspec/specs');
188
+ const afterRoot = path.join(staging, 'openspec/specs');
155
189
  const names = new Set([...files(beforeRoot), ...files(afterRoot)]);
156
190
  const contents = (root, name) => existsSync(path.join(root, name)) ? readFileSync(path.join(root, name), 'utf8') : null;
157
- plan = { activeHash, destination, writes: [...names].map(name => ({ path: 'openspec/specs/' + name, before: contents(beforeRoot, name), after: contents(afterRoot, name) })).filter(item => item.before !== item.after) };
191
+ plan = { activeHash, destination, writes: [...names].map(name => {
192
+ const before = contents(beforeRoot, name);
193
+ let after = contents(afterRoot, name);
194
+ // Pinned OpenSpec inserts a change ID into the placeholder for a newly
195
+ // created specification. Preserve that provenance during fork replay;
196
+ // authored text and all byte-for-byte publication guards stay intact.
197
+ if (sourceChange && before === null && after !== null)
198
+ after = after.replace(`\n## Purpose\nTBD - created by archiving change ${change}. Update Purpose after archive.\n## Requirements\n`, `\n## Purpose\nTBD - created by archiving change ${sourceChange}. Update Purpose after archive.\n## Requirements\n`);
199
+ return { path: 'openspec/specs/' + name, before, after };
200
+ }).filter(item => item.before !== item.after) };
158
201
  write(receipt, JSON.stringify(plan));
159
202
  }
160
203
  finally {
161
204
  rmSync(staging, { recursive: true, force: true });
162
205
  }
163
206
  }
164
- if (plan.activeHash !== activeHash)
165
- throw new Error('Reviewed artifacts changed since the OpenSpec archive was prepared');
166
207
  if (!new RegExp('^openspec/changes/archive/[0-9]{4}-[0-9]{2}-[0-9]{2}-' + change + '$').test(plan.destination))
167
208
  throw new Error('Invalid OpenSpec archive receipt');
209
+ if (adapter && !existsSync(active)) {
210
+ const archived = artifactPath(context.artifactRoot, plan.destination);
211
+ if (!existsSync(archived) || treeHash(archived) !== plan.activeHash || plan.writes.some(item => {
212
+ if (!item.path.startsWith('openspec/specs/'))
213
+ throw new Error('Invalid OpenSpec archive write');
214
+ const target = artifactPath(context.artifactRoot, item.path);
215
+ return (existsSync(target) ? readFileSync(target, 'utf8') : null) !== item.after;
216
+ }))
217
+ throw new Error('OpenSpec archive recovery does not match the prepared write set');
218
+ return;
219
+ }
220
+ if (plan.activeHash !== activeHash)
221
+ throw new Error('Reviewed artifacts changed since the OpenSpec archive was prepared');
168
222
  const targets = plan.writes.map(item => {
169
223
  if (!item.path.startsWith('openspec/specs/'))
170
224
  throw new Error('Invalid OpenSpec archive write');
@@ -172,16 +226,23 @@ async function archiveWithOpenSpec(context, change, active, signal) {
172
226
  const current = existsSync(target) ? readFileSync(target, 'utf8') : null;
173
227
  if (current !== item.before && current !== item.after)
174
228
  throw new Error('Main specification changed during archive; refusing to overwrite: ' + item.path);
175
- return { ...item, target };
229
+ return { ...item, target, unchanged: current === item.after };
176
230
  });
177
231
  // The external CLI ran asynchronously; recheck the candidate and artifacts before publishing.
178
232
  signal?.throwIfAborted();
179
- checkArchive(context);
233
+ if (treeHash(active) !== activeHash)
234
+ throw new Error('Reviewed artifacts changed during OpenSpec archive preparation');
235
+ if (adapter)
236
+ adapter.beforePublish();
237
+ else
238
+ checkArchive(context);
180
239
  // Validate every path and preimage before publishing the first output.
181
240
  const destination = artifactPath(context.artifactRoot, plan.destination);
182
241
  if (existsSync(destination))
183
242
  throw new Error('Archive destination already exists');
184
243
  for (const item of targets) {
244
+ if (item.unchanged)
245
+ continue;
185
246
  if (item.after === null)
186
247
  rmSync(item.target, { force: true });
187
248
  else
@@ -190,6 +251,18 @@ async function archiveWithOpenSpec(context, change, active, signal) {
190
251
  mkdirSync(path.dirname(destination), { recursive: true });
191
252
  renameSync(active, destination);
192
253
  }
254
+ /** Recoverable pinned-CLI archive for ledger-only runs; no implementation journal. */
255
+ export async function archiveOpenSpecChange(context, change, adapter, signal) {
256
+ if (!SLUG.test(change) || change.length > 64)
257
+ throw new Error('Invalid OpenSpec change identifier');
258
+ mkdirSync(adapter.directory, { recursive: true, mode: 0o700 });
259
+ if (lstatSync(adapter.directory).isSymbolicLink())
260
+ throw new Error('Refusing symlink archive adapter directory');
261
+ const active = artifactPath(context.artifactRoot, 'openspec/changes/' + change);
262
+ if (!existsSync(active) && !existsSync(path.join(adapter.directory, 'openspec-archive.json')))
263
+ throw new Error('OpenSpec active change does not exist');
264
+ await archiveWithOpenSpec(context, change, active, signal, adapter);
265
+ }
193
266
  export async function archive(context, change, signal) {
194
267
  const state = journal(context);
195
268
  if (state.phases.archive.status !== 'done') {
@@ -9,6 +9,8 @@ import type { CoreNodeId, CoreStateType, DeveloperRecord, VerificationRecord } f
9
9
  /** Autonomous investigation passes before a low-confidence design asks or proceeds. */
10
10
  export declare const MAX_DEEPEN_PASSES = 1;
11
11
  export interface CoreNodeDeps {
12
+ /** V2 presentation omits framework dispatch frames; original evidence stays complete. */
13
+ focusedCorrectionEvidence?: boolean;
12
14
  archiveApproved?: () => boolean;
13
15
  context: PipelineContext;
14
16
  config: RuntimeConfig;
@@ -214,7 +214,7 @@ async function implementationVisit(deps, state, step, mode) {
214
214
  kind: visits > 1 ? 'correction' : 'initial',
215
215
  ...(fixer ? { agentOverride: fixer } : {}), ...(fixing ? { stance: 'fixer' } : {}),
216
216
  ...(resumable
217
- ? { prompt: correctionInstructions('developer', feedback, { ...(changes ? { changeSet: changes } : {}), developer: state.development }), resumeSessionId: previous.sessionId, fallbackPrompt: full }
217
+ ? { prompt: correctionInstructions('developer', feedback, { ...(changes ? { changeSet: changes } : {}), developer: state.development, focusedEvidence: deps.focusedCorrectionEvidence && config.efficiency?.contextMode !== 'full' }), resumeSessionId: previous.sessionId, fallbackPrompt: full }
218
218
  : { prompt: full }),
219
219
  structured: true, lenient: true, outputSchema: DEVELOPER_OUTPUT_SCHEMA,
220
220
  }, (output, text, result) => {
@@ -513,7 +513,7 @@ function reviewerNode(deps) {
513
513
  const changes = measuredChange(context, change);
514
514
  const prompt = roleInstructions('reviewer', context, change, { definition: config.rolePrompts?.reviewer, feedback: feedbackFor(state), verification: state.plan, policy, criteria, developer: state.development, ...(changes ? { changeSet: changes } : {}), ...(reReview ? { reReview } : {}) });
515
515
  const incremental = config.efficiency?.reviewMode !== 'full' && delta.mode === 'incremental' && state.review?.sessionId;
516
- const followup = correctionInstructions('reviewer', feedbackFor(state)) + '\nChanges since YOUR previous reviewed candidate:\n' + JSON.stringify(delta.changes)
516
+ const followup = correctionInstructions('reviewer', feedbackFor(state), { focusedEvidence: deps.focusedCorrectionEvidence && config.efficiency?.contextMode !== 'full' }) + '\nChanges since YOUR previous reviewed candidate:\n' + JSON.stringify(delta.changes)
517
517
  + (changes ? '\nThe complete change under review (from git, against the run\'s base) — review these files only:\n' + changes.join('\n') : '')
518
518
  + '\nRecertify EVERY current acceptance criterion; previous met results are not current evidence:\n' + JSON.stringify(criteria)
519
519
  + '\nCurrent developer handoff:\n' + JSON.stringify(state.development);
@@ -1,3 +1,4 @@
1
+ import { type RoleStatePort } from '../role-state.js';
1
2
  import { type InvocationKind } from '../role-routing.js';
2
3
  import { type PipelineContext } from '../../pipeline/pipeline-state.js';
3
4
  import type { ExecutorRegistry } from '../executors.js';
@@ -17,6 +18,9 @@ export interface InvokeOptions {
17
18
  /** Run this invocation on another configured engine (the fixer) with the matching stance; sessions never carry across engines. */
18
19
  agentOverride?: RuntimeAgentConfig;
19
20
  stance?: 'fixer';
21
+ /** Per-piece invocation bounds; explicit zero disables only that timer. */
22
+ timeoutMs?: number;
23
+ idleTimeoutMs?: number;
20
24
  }
21
25
  export type InvokeOutcome<T> = {
22
26
  ok: true;
@@ -36,6 +40,8 @@ export interface RoleInvokerDeps {
36
40
  config: RuntimeConfig;
37
41
  registry: ExecutorRegistry;
38
42
  openspec?: Record<AgentRole, import('../openspec.js').OpenSpecRoleContext>;
43
+ /** Engine hosts isolate sessions/routes by scope and node instead of a role-global file. */
44
+ roleState?: RoleStatePort;
39
45
  /** `fixer` labels the developer step's events during a correction round on the fixer stance, so a log reader sees who is acting. */
40
46
  onAgentEvent?: (role: AgentEventRole, event: AgentEvent) => void;
41
47
  }