specrails-core 6.0.0 → 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 (173) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-runtime/capabilities.js +3 -1
  3. package/dist/agent-runtime/change-scope.d.ts +63 -0
  4. package/dist/agent-runtime/change-scope.js +297 -0
  5. package/dist/agent-runtime/cli-executor.d.ts +3 -0
  6. package/dist/agent-runtime/cli-executor.js +50 -13
  7. package/dist/agent-runtime/cli-process.js +6 -2
  8. package/dist/agent-runtime/cli.d.ts +15 -0
  9. package/dist/agent-runtime/cli.js +43 -9
  10. package/dist/agent-runtime/compact/chat-client.js +2 -1
  11. package/dist/agent-runtime/compact/environment.d.ts +12 -0
  12. package/dist/agent-runtime/compact/environment.js +31 -1
  13. package/dist/agent-runtime/compact/test-reachability.js +6 -2
  14. package/dist/agent-runtime/config.d.ts +4 -1
  15. package/dist/agent-runtime/config.js +41 -3
  16. package/dist/agent-runtime/core-host.d.ts +10 -4
  17. package/dist/agent-runtime/core-host.js +22 -19
  18. package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
  19. package/dist/agent-runtime/efficiency-summary.js +30 -27
  20. package/dist/agent-runtime/efficiency-types.d.ts +4 -4
  21. package/dist/agent-runtime/efficiency.d.ts +1 -1
  22. package/dist/agent-runtime/efficiency.js +7 -6
  23. package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
  24. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
  25. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
  26. package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
  27. package/dist/agent-runtime/engine/budget.d.ts +9 -0
  28. package/dist/agent-runtime/engine/budget.js +33 -0
  29. package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
  30. package/dist/agent-runtime/engine/canonical-json.js +162 -0
  31. package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
  32. package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
  33. package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
  34. package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
  35. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
  36. package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
  37. package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
  38. package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
  39. package/dist/agent-runtime/engine/cli.d.ts +10 -0
  40. package/dist/agent-runtime/engine/cli.js +172 -0
  41. package/dist/agent-runtime/engine/compiler.d.ts +23 -0
  42. package/dist/agent-runtime/engine/compiler.js +261 -0
  43. package/dist/agent-runtime/engine/composition.d.ts +16 -0
  44. package/dist/agent-runtime/engine/composition.js +28 -0
  45. package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
  46. package/dist/agent-runtime/engine/concurrency.js +55 -0
  47. package/dist/agent-runtime/engine/contracts.d.ts +273 -0
  48. package/dist/agent-runtime/engine/contracts.js +11 -0
  49. package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
  50. package/dist/agent-runtime/engine/definition-schema.js +313 -0
  51. package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
  52. package/dist/agent-runtime/engine/definition-types.js +3 -0
  53. package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
  54. package/dist/agent-runtime/engine/definition-validator.js +187 -0
  55. package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
  56. package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
  57. package/dist/agent-runtime/engine/events.d.ts +19 -0
  58. package/dist/agent-runtime/engine/events.js +88 -0
  59. package/dist/agent-runtime/engine/execution.d.ts +29 -0
  60. package/dist/agent-runtime/engine/execution.js +121 -0
  61. package/dist/agent-runtime/engine/expressions.d.ts +11 -0
  62. package/dist/agent-runtime/engine/expressions.js +172 -0
  63. package/dist/agent-runtime/engine/fork.d.ts +28 -0
  64. package/dist/agent-runtime/engine/fork.js +211 -0
  65. package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
  66. package/dist/agent-runtime/engine/graph-description.js +16 -0
  67. package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
  68. package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
  69. package/dist/agent-runtime/engine/index.d.ts +14 -0
  70. package/dist/agent-runtime/engine/index.js +14 -0
  71. package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
  72. package/dist/agent-runtime/engine/invocation-context.js +128 -0
  73. package/dist/agent-runtime/engine/otel.d.ts +15 -0
  74. package/dist/agent-runtime/engine/otel.js +132 -0
  75. package/dist/agent-runtime/engine/output.d.ts +4 -0
  76. package/dist/agent-runtime/engine/output.js +73 -0
  77. package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
  78. package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
  79. package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
  80. package/dist/agent-runtime/engine/piece-registry.js +62 -0
  81. package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
  82. package/dist/agent-runtime/engine/pieces/assign.js +35 -0
  83. package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
  84. package/dist/agent-runtime/engine/pieces/control.js +49 -0
  85. package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
  86. package/dist/agent-runtime/engine/pieces/decider.js +70 -0
  87. package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
  88. package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
  89. package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
  90. package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
  91. package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
  92. package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
  93. package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
  94. package/dist/agent-runtime/engine/pieces/index.js +22 -0
  95. package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
  96. package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
  97. package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
  98. package/dist/agent-runtime/engine/pieces/ports.js +1 -0
  99. package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
  100. package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
  101. package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
  102. package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
  103. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
  104. package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
  105. package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
  106. package/dist/agent-runtime/engine/pieces/shared.js +40 -0
  107. package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
  108. package/dist/agent-runtime/engine/pieces/shell.js +47 -0
  109. package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
  110. package/dist/agent-runtime/engine/pieces/verify.js +52 -0
  111. package/dist/agent-runtime/engine/preflight.d.ts +27 -0
  112. package/dist/agent-runtime/engine/preflight.js +54 -0
  113. package/dist/agent-runtime/engine/regex.d.ts +3 -0
  114. package/dist/agent-runtime/engine/regex.js +19 -0
  115. package/dist/agent-runtime/engine/run-status.d.ts +563 -0
  116. package/dist/agent-runtime/engine/run-status.js +61 -0
  117. package/dist/agent-runtime/engine/runs.d.ts +605 -0
  118. package/dist/agent-runtime/engine/runs.js +247 -0
  119. package/dist/agent-runtime/engine/state.d.ts +64 -0
  120. package/dist/agent-runtime/engine/state.js +98 -0
  121. package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
  122. package/dist/agent-runtime/engine/steering/inbox.js +113 -0
  123. package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
  124. package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
  125. package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
  126. package/dist/agent-runtime/engine/storage/private-path.js +74 -0
  127. package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
  128. package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
  129. package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
  130. package/dist/agent-runtime/evaluation-corpus.js +4 -0
  131. package/dist/agent-runtime/evaluation.d.ts +9 -3
  132. package/dist/agent-runtime/evaluation.js +50 -8
  133. package/dist/agent-runtime/executor-types.d.ts +53 -3
  134. package/dist/agent-runtime/executor-types.js +35 -2
  135. package/dist/agent-runtime/executors.js +2 -2
  136. package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
  137. package/dist/agent-runtime/graph/artifacts.js +86 -10
  138. package/dist/agent-runtime/graph/convergence.d.ts +26 -0
  139. package/dist/agent-runtime/graph/convergence.js +62 -0
  140. package/dist/agent-runtime/graph/nodes.d.ts +2 -0
  141. package/dist/agent-runtime/graph/nodes.js +171 -24
  142. package/dist/agent-runtime/graph/roles.d.ts +6 -0
  143. package/dist/agent-runtime/graph/roles.js +23 -17
  144. package/dist/agent-runtime/graph/state.d.ts +5 -0
  145. package/dist/agent-runtime/graph/state.js +2 -0
  146. package/dist/agent-runtime/kimi-acp.js +6 -6
  147. package/dist/agent-runtime/openai-executor.js +17 -12
  148. package/dist/agent-runtime/openspec.d.ts +17 -2
  149. package/dist/agent-runtime/openspec.js +54 -20
  150. package/dist/agent-runtime/prompts.d.ts +11 -5
  151. package/dist/agent-runtime/prompts.js +71 -20
  152. package/dist/agent-runtime/recovery.d.ts +1 -1
  153. package/dist/agent-runtime/repository-context.js +20 -1
  154. package/dist/agent-runtime/role-routing.js +4 -2
  155. package/dist/agent-runtime/role-state.d.ts +5 -0
  156. package/dist/agent-runtime/verification-plan.d.ts +2 -0
  157. package/dist/agent-runtime/verification-plan.js +18 -3
  158. package/dist/agent-runtime/workflow-types.d.ts +2 -0
  159. package/dist/agent-runtime/workflow.d.ts +3 -1
  160. package/dist/agent-runtime/workflow.js +7 -3
  161. package/dist/agent-runtime/workspace-tools.d.ts +2 -2
  162. package/dist/agent-runtime/workspace-tools.js +6 -6
  163. package/dist/installer/cli.js +5 -0
  164. package/dist/installer/phases/scaffold.d.ts +2 -2
  165. package/dist/installer/phases/scaffold.js +3 -7
  166. package/dist/pipeline/pipeline-state.d.ts +49 -4
  167. package/dist/pipeline/pipeline-state.js +150 -23
  168. package/integration-contract.json +84 -12
  169. package/package.json +12 -5
  170. package/schemas/agent-runtime.schema.json +93 -1
  171. package/schemas/workflow-definition.schema.json +312 -0
  172. package/templates/kimi/specrails/run-skill.mjs +15 -1
  173. package/templates/commands/specrails/batch-implement.md +0 -32
@@ -1,3 +1,5 @@
1
+ import { isDefinitionCommand, NODE_KINDS_VERSION, runDefinitionCommand } from './engine/cli.js';
2
+ import { validationPieceRegistry } from './engine/pieces/index.js';
1
3
  import { GUARDRAIL_CATALOG } from './guardrails.js';
2
4
  import { runEvaluation } from './evaluation.js';
3
5
  import { efficiencySummary } from './efficiency-summary.js';
@@ -15,6 +17,15 @@ import { runtimeEfficiency } from './efficiency.js';
15
17
  import { configuredCapabilities } from './capabilities.js';
16
18
  import { createExecutorRegistry } from './executors.js';
17
19
  import { runRecovery } from './recovery.js';
20
+ import { EngineError } from './engine/contracts.js';
21
+ /** Both runtime entry points emit the same machine-readable fatal result. */
22
+ export function runtimeFailure(error) {
23
+ return { type: 'runtime-result', status: 'failed', error: error instanceof EngineError
24
+ ? { code: error.code, message: error.message, ...(error.details === undefined ? {} : { details: error.details }) }
25
+ : error instanceof Error ? error.message : String(error) };
26
+ }
27
+ /** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
28
+ export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'help'];
18
29
  function read(file) { return JSON.parse(readFileSync(file, 'utf8')); }
19
30
  async function readStdin() {
20
31
  const chunks = [];
@@ -87,8 +98,15 @@ function invocationUsage(state, priorCount) {
87
98
  ? null : attempts.reduce((total, a) => total + (a.usage?.[key] ?? 0), 0);
88
99
  return { costUsd: sum('costUsd'), inputTokens: sum('inputTokens'), outputTokens: sum('outputTokens') };
89
100
  }
101
+ /** Engines this package can launch: the legacy built-in pipeline (1) and workflow definitions (2).
102
+ * Core 7 advertises [2] so hosts refuse unconverted legacy loops before admission. */
103
+ export const RUNTIME_ENGINES = [1, 2];
90
104
  export async function runRuntimeCommand(flags, positionals, emit = value => process.stdout.write(JSON.stringify(value) + '\n')) {
91
105
  const command = flags.help === true ? 'help' : positionals[0] ?? 'help';
106
+ if (isDefinitionCommand(flags, command))
107
+ return runDefinitionCommand(flags, positionals, emit);
108
+ if (!RUNTIME_CLI_OPERATIONS.some(operation => operation === command))
109
+ throw new Error('Unknown runtime operation: ' + command);
92
110
  if (command === 'prompts') {
93
111
  emit({ type: 'runtime-role-prompts', defaults: rolePromptDefaults() });
94
112
  return 0;
@@ -105,28 +123,42 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
105
123
  if (command === 'evaluate') {
106
124
  if (flags.real !== undefined && flags.real !== true)
107
125
  throw new Error('--real is a boolean flag');
108
- const report = await runEvaluation({ output: stringFlag(flags, 'output'), real: flags.real === true, ...(flags.config === undefined ? {} : { config: validateRuntimeConfig(read(stringFlag(flags, 'config'))) }), ...(flags['max-cost-usd'] === undefined ? {} : { maxCostUsd: Number(stringFlag(flags, 'max-cost-usd')) }), ...(flags.repetitions === undefined ? {} : { repetitions: Number(stringFlag(flags, 'repetitions')) }) });
109
- emit({ type: 'runtime-evaluation', mode: report.mode, observations: report.observations.length, output: stringFlag(flags, 'output'), monetaryConclusion: report.monetaryConclusion, correctionPromptTargetMet: report.correctionPromptTargetMet, noObservedQualityDrop: report.noObservedQualityDrop, stopReason: report.stopReason });
110
- return report.stopReason || (!flags.real && (!report.noObservedQualityDrop || !report.noExtraInvocations || !report.correctionPromptTargetMet)) ? 1 : 0;
126
+ const report = await runEvaluation({ output: stringFlag(flags, 'output'), real: flags.real === true,
127
+ ...(flags.definition === undefined ? {} : { definition: readFileSync(stringFlag(flags, 'definition')) }),
128
+ ...(flags.config === undefined ? {} : { config: validateRuntimeConfig(read(stringFlag(flags, 'config'))) }), ...(flags['max-cost-usd'] === undefined ? {} : { maxCostUsd: Number(stringFlag(flags, 'max-cost-usd')) }), ...(flags.repetitions === undefined ? {} : { repetitions: Number(stringFlag(flags, 'repetitions')) }) });
129
+ emit({ type: 'runtime-evaluation', mode: report.mode, observations: report.observations.length, output: stringFlag(flags, 'output'),
130
+ isDefinitionEvaluation: report.isDefinitionEvaluation, allCasesAccepted: report.allCasesAccepted, acceptedCases: report.acceptedCases,
131
+ monetaryConclusion: report.monetaryConclusion, correctionPromptTargetMet: report.correctionPromptTargetMet, noObservedQualityDrop: report.noObservedQualityDrop, noExtraInvocations: report.noExtraInvocations, stopReason: report.stopReason });
132
+ return report.stopReason || !report.allCasesAccepted || (!flags.real && (!report.noObservedQualityDrop || !report.noExtraInvocations || !report.correctionPromptTargetMet)) ? 1 : 0;
111
133
  }
112
134
  if (command === 'help') {
113
135
  emit({ usage: [
114
136
  'specrails-core runtime api',
115
- 'specrails-core runtime evaluate --output <directory> [--repetitions <1..20>] [--real --config <json> --max-cost-usd <limit>]',
137
+ 'specrails-core runtime evaluate --output <directory> [--definition <json>] [--repetitions <1..20>] [--real --config <json> --max-cost-usd <limit>]',
116
138
  'specrails-core runtime capabilities --config <json>',
117
139
  'specrails-core runtime evidence --context <json> [--id <opaqueId>] [--section summary|stdout|stderr|source] [--source-id <opaqueId>] [--cursor <cursor>] [--limit <1..100>]',
118
140
  'specrails-core runtime prompts',
119
141
  'specrails-core runtime validate --config <json>',
120
142
  'specrails-core runtime validate --stdin',
121
143
  'specrails-core runtime run --context <json> --config <json> --change <kebab-case>',
144
+ 'specrails-core runtime run --context <json> --config <json> --definition <json> [--change <kebab-case>]',
145
+ 'specrails-core runtime workflows list',
146
+ 'specrails-core runtime workflows validate --stdin [--config <json> | --structural]',
122
147
  'specrails-core runtime status --context <json> [--compact]',
123
148
  'specrails-core runtime resume --context <json> [--approve archive] [--answer <text>] [--recover developer] [--invalidate verify]',
124
149
  'specrails-core runtime recovery --context <json> --stdin',
150
+ 'specrails-core runtime fork --context <json> --from <nodePath> --run-id <newRunId> [--scope-id <scope>] [--visit <number>] [--state <json>]',
151
+ 'specrails-core runtime signal --context <json> --stdin [--request-id <id>]',
152
+ 'specrails-core runtime cancel --context <json> [--request-id <id>]',
125
153
  ] });
126
154
  return 0;
127
155
  }
128
156
  if (command === 'api') {
129
- emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION], capabilities: { scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1 }, guardrails: GUARDRAIL_CATALOG });
157
+ // Engine v2 fields come from the same validation registry that admits definitions; test-only pieces are never registered there.
158
+ emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
159
+ engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
160
+ capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
161
+ engineV2: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1 }, guardrails: GUARDRAIL_CATALOG });
130
162
  return 0;
131
163
  }
132
164
  if (command === 'validate') {
@@ -163,8 +195,6 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
163
195
  process.off('SIGTERM', abort);
164
196
  }
165
197
  }
166
- if (!['run', 'status', 'resume'].includes(command))
167
- throw new Error('Unknown runtime operation: ' + command);
168
198
  const context = validatePipelineContext(read(stringFlag(flags, 'context')));
169
199
  const directory = path.join(pipelineStateDirectory(context), 'agent-workflow');
170
200
  const previous = await readWorkflowState(directory, context.runId);
@@ -178,6 +208,8 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
178
208
  const requestFile = path.join(pipelineStateDirectory(context), 'agent-runtime-request.json');
179
209
  let request;
180
210
  if (command === 'resume') {
211
+ if (flags.definition !== undefined)
212
+ throw new EngineError('invalid_arguments', 'Resume uses the frozen definition; start a new run to change it');
181
213
  if (!previous)
182
214
  throw new Error('No programmatic run exists for this context');
183
215
  request = read(requestFile);
@@ -189,7 +221,9 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
189
221
  else {
190
222
  if (flags.answer !== undefined || flags.approve !== undefined)
191
223
  throw new Error('Answers and approvals apply to runtime resume');
192
- request = { change: stringFlag(flags, 'change'), config: normalizeRuntimeConfig(read(stringFlag(flags, 'config'))), runtimeIdentity: coreRuntimeIdentity() };
224
+ // The built-in workflow has no published definition hash; its resume identity is the checkpoint's workflowFingerprint.
225
+ request = { change: stringFlag(flags, 'change'), config: normalizeRuntimeConfig(read(stringFlag(flags, 'config'))), runtimeIdentity: coreRuntimeIdentity(),
226
+ workflow: { id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, source: 'builtin', definitionHash: null, engine: 1 } };
193
227
  await preflightCoreWorkflow({ context, change: request.change, config: request.config });
194
228
  const serialized = JSON.stringify(request, null, 2) + '\n';
195
229
  mkdirSync(path.dirname(requestFile), { recursive: true, mode: 0o700 });
@@ -237,7 +271,7 @@ if (process.argv[1] && existsSync(process.argv[1]) && import.meta.url === pathTo
237
271
  process.exitCode = await runRuntimeCommand(flags, [subcommand ?? 'help', ...positionals]);
238
272
  }
239
273
  catch (error) {
240
- process.stdout.write(JSON.stringify({ type: 'runtime-result', status: 'failed', error: error instanceof Error ? error.message : String(error) }) + '\n');
274
+ process.stdout.write(JSON.stringify(runtimeFailure(error)) + '\n');
241
275
  process.exitCode = 1;
242
276
  }
243
277
  }
@@ -1,3 +1,4 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
1
2
  import { AgentExecutionError } from '../executor-types.js';
2
3
  import { sumCacheUsage } from '../efficiency-types.js';
3
4
  /**
@@ -49,7 +50,7 @@ export function patientFetchInit() {
49
50
  }
50
51
  const RETRY_DELAY_MS = 3000;
51
52
  function sleep(ms, signal) {
52
- return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true }); });
53
+ return delay(ms, undefined, { signal });
53
54
  }
54
55
  export function record(value) {
55
56
  return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
@@ -5,12 +5,24 @@ interface EnvironmentInstall {
5
5
  command: string;
6
6
  args: string[];
7
7
  }
8
+ /** `precondition`: the install failed for a reason only the host can repair (see hostPreconditionFailure). */
8
9
  interface InstallOutcome extends EnvironmentInstall {
9
10
  ok: boolean;
10
11
  detail: string;
12
+ precondition?: string;
11
13
  }
12
14
  /** True when a verification failure looks like a missing toolchain/dependency rather than a code defect. */
13
15
  export declare function isEnvironmentFailure(exitCode: number | null | undefined, output: string): boolean;
16
+ /**
17
+ * Failures that no edit to the change can repair, because the verification
18
+ * process itself lacks something only the host can grant: an environment
19
+ * variable a configuration file references, registry credentials, registry
20
+ * network access, git credentials for a dependency. Feeding them to a fixer
21
+ * makes it edit package-manager configuration to route around the missing
22
+ * secret (observed: `.yarnrc.yml` rewritten, reverted by review, rewritten
23
+ * again). Returns a short host-facing reason, or undefined.
24
+ */
25
+ export declare function hostPreconditionFailure(output: string): string | undefined;
14
26
  /** Packages a failure output names explicitly (`Try \`npm i --save-dev @types/jest\``); the host installs exactly those. */
15
27
  export declare function suggestedPackages(output: string): string[];
16
28
  /** Dependencies package.json declares that node_modules does not hold (bounded). */
@@ -19,6 +19,8 @@ const ENVIRONMENT_SIGNATURES = [
19
19
  /jest: not found|vitest: not found|mocha: not found|tsc: not found|pytest: not found/i,
20
20
  /TS2582|Do you need to install type definitions|Try `npm i --save-dev @types\//i,
21
21
  /Module (?:[\w@./-]+) in the \w+ option was not found|Preset [\w@./-]+ not found|Cannot find module '(?:ts-jest|babel-jest|@swc\/jest|ts-node|tsx)'/i,
22
+ // Yarn Berry refuses to run scripts in a checkout that was never installed.
23
+ /doesn't seem to have been installed - running an install there might help|Couldn't find the node_modules state file - running an install might help/i,
22
24
  ];
23
25
  /** True when a verification failure looks like a missing toolchain/dependency rather than a code defect. */
24
26
  export function isEnvironmentFailure(exitCode, output) {
@@ -27,6 +29,33 @@ export function isEnvironmentFailure(exitCode, output) {
27
29
  const tail = output.slice(-6000);
28
30
  return ENVIRONMENT_SIGNATURES.some(pattern => pattern.test(tail));
29
31
  }
32
+ const REGISTRY = String.raw `(?:registry|npmjs\.org|yarnpkg\.com|npm\.pkg\.github\.com|pkgs\.dev\.azure\.com|jfrog|artifactory|nexus|pypi\.org|crates\.io|proxy\.golang\.org)`;
33
+ /**
34
+ * Failures that no edit to the change can repair, because the verification
35
+ * process itself lacks something only the host can grant: an environment
36
+ * variable a configuration file references, registry credentials, registry
37
+ * network access, git credentials for a dependency. Feeding them to a fixer
38
+ * makes it edit package-manager configuration to route around the missing
39
+ * secret (observed: `.yarnrc.yml` rewritten, reverted by review, rewritten
40
+ * again). Returns a short host-facing reason, or undefined.
41
+ */
42
+ export function hostPreconditionFailure(output) {
43
+ const text = output.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '').slice(-16_000);
44
+ const variable = /Environment variable not found \(([A-Za-z_][A-Za-z0-9_]*)\)/.exec(text);
45
+ if (variable)
46
+ return `the environment variable ${variable[1]} is not available to verification commands, and a package-manager configuration file requires it`;
47
+ if (/\bYN0041\b/.test(text) || /YN0035[\s\S]{0,400}?Response Code: 40[13]\b/.test(text) || /Response code 40[13] \((?:Unauthorized|Forbidden)\)/i.test(text)
48
+ || /npm (?:ERR!|error) code (?:E401|E403|ENEEDAUTH)\b/.test(text) || /ERR_PNPM_FETCH_40[13]\b/.test(text)
49
+ || new RegExp(String.raw `\b40[13] (?:Unauthorized|Forbidden)\b[^\n]{0,80}https?://[^\s]*${REGISTRY}`, 'i').test(text)
50
+ || /\b(?:401|403) Client Error: (?:Unauthorized|Forbidden) for url/.test(text))
51
+ return 'the package registry rejected the credentials available to verification commands (HTTP 401/403)';
52
+ if (/npm (?:ERR!|error) code (?:ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT|ECONNRESET)\b/.test(text) || /ERR_PNPM_META_FETCH_FAIL\b/.test(text)
53
+ || new RegExp(String.raw `\b(?:ENOTFOUND|EAI_AGAIN)\b[^\n]{0,200}${REGISTRY}|${REGISTRY}[^\n]{0,200}\b(?:ENOTFOUND|EAI_AGAIN)\b`, 'i').test(text))
54
+ return 'the package registry cannot be reached from the verification environment';
55
+ if (/fatal: could not read (?:Username|Password) for/.test(text) || /Permission denied \(publickey\)/.test(text))
56
+ return 'git credentials needed to fetch a dependency are not available to verification commands';
57
+ return undefined;
58
+ }
30
59
  /** Packages a failure output names explicitly (`Try \`npm i --save-dev @types/jest\``); the host installs exactly those. */
31
60
  export function suggestedPackages(output) {
32
61
  const packages = new Set();
@@ -155,7 +184,8 @@ export function installEnvironment(roots, io = {}) {
155
184
  const ok = result.status === 0;
156
185
  const detail = ok ? `installed ${plan.ecosystem} dependencies in ${path.basename(root)}` : `${plan.command} ${plan.args.join(' ')} failed in ${path.basename(root)}: ${String(result.stderr || result.error?.message || `exit ${result.status}`).trim().slice(0, 400)}`;
157
186
  io.onEvent?.({ kind: 'text', text: `Environment: ${detail}` });
158
- outcomes.push({ ...plan, ok, detail });
187
+ const precondition = ok ? undefined : hostPreconditionFailure(`${result.stdout ?? ''}\n${result.stderr ?? ''}`);
188
+ outcomes.push({ ...plan, ok, detail, ...(precondition ? { precondition } : {}) });
159
189
  }
160
190
  }
161
191
  return outcomes;
@@ -98,7 +98,11 @@ function tokenReaches(token, file, root, discovery) {
98
98
  * file. Unknown ⇒ true: only an enumerating command can prove absence.
99
99
  */
100
100
  export function commandReachesTest(root, command, file) {
101
- const target = posix(file);
101
+ // Path arguments are relative to the directory the command runs in (a
102
+ // package inside a larger checkout), while reported files are relative to
103
+ // the checkout: compare both from the command's directory.
104
+ const base = path.resolve(root, command.cwd ?? '.');
105
+ const target = posix(path.relative(base, path.resolve(root, file)));
102
106
  let judged = false;
103
107
  for (const tokens of segments(expandCommandText(root, command))) {
104
108
  if (!tokens.length)
@@ -117,7 +121,7 @@ export function commandReachesTest(root, command, file) {
117
121
  if (!paths.some(token => TEST_FILE_PATTERN.test(posix(token)) || token.includes('*') || token.endsWith('...')))
118
122
  continue;
119
123
  judged = true;
120
- if (paths.some(token => tokenReaches(token, target, root, discovery)))
124
+ if (paths.some(token => tokenReaches(token, target, base, discovery)))
121
125
  return true;
122
126
  }
123
127
  return !judged;
@@ -1,4 +1,4 @@
1
- import type { RuntimeConfig } from './executor-types.js';
1
+ import { type RoleDescriptor, type RuntimeConfig } from './executor-types.js';
2
2
  export declare function validateRuntimeConfig(input: unknown, options?: {
3
3
  registeredProviderIds?: string[];
4
4
  }): RuntimeConfig;
@@ -9,3 +9,6 @@ export declare function loadRuntimeConfig(file: string, options?: {
9
9
  export declare function normalizeRuntimeConfig(input: unknown, options?: {
10
10
  registeredProviderIds?: string[];
11
11
  }): RuntimeConfig;
12
+ /** Built-ins stay ordered and custom roles are declared data, never inferred from a prompt. */
13
+ export declare function roleIds(config: RuntimeConfig): string[];
14
+ export declare function resolveRoleDescriptor(config: RuntimeConfig, id: string): RoleDescriptor;
@@ -1,7 +1,9 @@
1
+ import { isDeepStrictEqual } from 'node:util';
1
2
  import { validateGuardrailSettings } from './guardrails.js';
2
3
  import { readFileSync } from 'node:fs';
4
+ import { BUILTIN_ROLES, BUILTIN_ROLE_POLICY, ROLE_ID, isBuiltinRole } from './executor-types.js';
3
5
  import { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS } from './graph/review-policy.js';
4
- const ROLES = ['architect', 'developer', 'reviewer'];
6
+ const ROLES = [...BUILTIN_ROLES];
5
7
  const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6
8
  function fail(field, why) { throw new Error(`Invalid runtime config ${field}: ${why}`); }
7
9
  function object(value, field) {
@@ -60,7 +62,7 @@ function score(value, field, floor) {
60
62
  }
61
63
  export function validateRuntimeConfig(input, options = {}) {
62
64
  const config = object(input, '$');
63
- keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'fixer', 'limits', 'verification', 'approvalBeforeArchive', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
65
+ keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'roles', 'fixer', 'limits', 'verification', 'approvalBeforeArchive', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
64
66
  if (config.schemaVersion !== 1)
65
67
  fail('schemaVersion', 'expected 1');
66
68
  if (config.enabled !== undefined && typeof config.enabled !== 'boolean')
@@ -141,6 +143,30 @@ export function validateRuntimeConfig(input, options = {}) {
141
143
  return structuredClone(agent);
142
144
  };
143
145
  const agents = Object.fromEntries(ROLES.map(role => [role, validateAgent(rawAgents[role], `agents.${role}`)]));
146
+ let roles;
147
+ if (config.roles !== undefined) {
148
+ const raw = object(config.roles, 'roles');
149
+ roles = {};
150
+ for (const [id, value] of Object.entries(raw)) {
151
+ if (!ROLE_ID.test(id) || id === 'fixer')
152
+ fail(`roles.${id}`, 'expected a role id; fixer is reserved');
153
+ const descriptor = object(value, `roles.${id}`);
154
+ const { access, artifacts, prompt, openspecSkill, ...assignment } = descriptor;
155
+ const selected = validateAgent(assignment, `roles.${id}`);
156
+ choices(access, ['read', 'write'], `roles.${id}.access`);
157
+ choices(artifacts, ['none', 'tasks-checkboxes', 'all'], `roles.${id}.artifacts`);
158
+ if (prompt !== undefined && string(prompt, `roles.${id}.prompt`).length > 20000)
159
+ fail(`roles.${id}.prompt`, 'maximum 20000 characters');
160
+ if (openspecSkill !== undefined)
161
+ choices(openspecSkill, ['openspec-ff-change', 'openspec-apply-change', 'openspec-verify-change'], `roles.${id}.openspecSkill`);
162
+ if (isBuiltinRole(id)) {
163
+ const policy = BUILTIN_ROLE_POLICY[id];
164
+ if (access !== policy.access || artifacts !== policy.artifacts || openspecSkill !== policy.openspecSkill || prompt !== undefined || !isDeepStrictEqual(selected, agents[id]))
165
+ fail(`roles.${id}`, 'built-in descriptors must match their implicit assignment and policy');
166
+ }
167
+ roles[id] = { ...selected, access, artifacts, ...(prompt === undefined ? {} : { prompt }), ...(openspecSkill === undefined ? {} : { openspecSkill }) };
168
+ }
169
+ }
144
170
  // The optional fixer: the engine correction rounds run on (verify failure or
145
171
  // review rejection); absent ⇒ the developer corrects its own work.
146
172
  const fixer = config.fixer === undefined ? undefined : validateAgent(config.fixer, 'fixer');
@@ -259,7 +285,7 @@ export function validateRuntimeConfig(input, options = {}) {
259
285
  let rolePrompts;
260
286
  if (config.rolePrompts !== undefined) {
261
287
  const raw = object(config.rolePrompts, 'rolePrompts');
262
- keys(raw, [...ROLES, 'fixer'], 'rolePrompts');
288
+ keys(raw, [...ROLES, 'fixer', ...Object.keys(roles ?? {})], 'rolePrompts');
263
289
  rolePrompts = {};
264
290
  for (const [role, value] of Object.entries(raw)) {
265
291
  const text = string(value, `rolePrompts.${role}`);
@@ -269,6 +295,7 @@ export function validateRuntimeConfig(input, options = {}) {
269
295
  }
270
296
  }
271
297
  return { schemaVersion: 1, enabled: true, providers, agents, verification,
298
+ ...(roles === undefined ? {} : { roles }),
272
299
  ...(config.efficiency === undefined ? {} : { efficiency: structuredClone(config.efficiency) }),
273
300
  ...(rolePrompts === undefined ? {} : { rolePrompts }),
274
301
  ...(config.limits === undefined ? {} : { limits: { ...config.limits } }),
@@ -284,9 +311,20 @@ export function loadRuntimeConfig(file, options) {
284
311
  /** New admissions normalize once; validation of a saved document is lossless. */
285
312
  export function normalizeRuntimeConfig(input, options) {
286
313
  const config = JSON.parse(JSON.stringify(validateRuntimeConfig(input, options)));
314
+ config.roles = Object.fromEntries(roleIds(config).map(id => { const { id: _id, ...descriptor } = resolveRoleDescriptor(config, id); return [id, descriptor]; }));
287
315
  config.efficiency = {
288
316
  schemaVersion: 1, contextMode: 'incremental', reviewMode: 'incremental', planning: 'proportional', acceptDeveloperChecks: true,
289
317
  ...config.efficiency, verification: { maxConcurrency: 1, ...config.efficiency?.verification },
290
318
  };
291
319
  return config;
292
320
  }
321
+ /** Built-ins stay ordered and custom roles are declared data, never inferred from a prompt. */
322
+ export function roleIds(config) { return [...new Set([...BUILTIN_ROLES, ...Object.keys(config.roles ?? {})])]; }
323
+ export function resolveRoleDescriptor(config, id) {
324
+ if (isBuiltinRole(id))
325
+ return { ...config.agents[id], ...BUILTIN_ROLE_POLICY[id], id };
326
+ const role = Object.hasOwn(config.roles ?? {}, id) ? config.roles[id] : undefined;
327
+ if (!role)
328
+ throw new Error(`Unknown runtime role '${id}'`);
329
+ return { ...role, ...(config.rolePrompts?.[id] === undefined ? {} : { prompt: config.rolePrompts[id] }), id };
330
+ }
@@ -2,10 +2,11 @@ import { type PipelineContext } from '../pipeline/pipeline-state.js';
2
2
  import { type ExecutorRegistry } from './executors.js';
3
3
  import type { AgentEvent, RuntimeConfig, AgentEventRole } from './executor-types.js';
4
4
  import { parseAgentObject } from './graph/artifacts.js';
5
+ import { type CoreNodeId, type CoreStateType } from './graph/state.js';
5
6
  import { type RuntimeIdentity } from './runtime-identity.js';
6
- import type { WorkflowEvent, WorkflowSpan, WorkflowState } from './workflow-types.js';
7
+ import type { WorkflowDefinition, WorkflowEvent, WorkflowNode, WorkflowSpan, WorkflowState } from './workflow-types.js';
7
8
  export declare const RUNTIME_API_VERSION = 1;
8
- export declare const CORE_WORKFLOW_VERSION = "6";
9
+ export declare const CORE_WORKFLOW_VERSION = "7";
9
10
  export declare const CORE_PACKAGE_VERSION: string;
10
11
  export declare function coreRuntimeIdentity(): RuntimeIdentity;
11
12
  export { parseAgentObject };
@@ -32,9 +33,14 @@ export declare function preflightCoreWorkflow(options: Pick<CoreWorkflowOptions,
32
33
  config: RuntimeConfig;
33
34
  registry: ExecutorRegistry;
34
35
  }>;
36
+ /** The legacy graph's resume identity; extracting it must never change its fingerprint. */
37
+ export declare function implementationWorkflowDefinition(nodes: Record<CoreNodeId, WorkflowNode<CoreStateType>>, { attempts, compactDeveloper }: {
38
+ attempts: number;
39
+ compactDeveloper: boolean;
40
+ }): WorkflowDefinition<CoreStateType>;
35
41
  /**
36
- * Runs the Core implementation graph: architect → developer → verify → review (→ fixer → verify → review on corrections)er → archive,
37
- * with bounded corrections routed back to the developer, an autonomous investigation pass
42
+ * Runs the Core implementation graph: architect → developer → verify → reviewer → archive,
43
+ * with bounded corrections through fixer → verify → reviewer, an autonomous investigation pass
38
44
  * before a low-confidence design asks the requester, real verification receipts, acceptance
39
45
  * evidence bound to the exact candidate, and an optional approval before archive.
40
46
  */
@@ -3,7 +3,7 @@ import { prepareOpenSpec, roleOpenSpecContext } from './openspec.js';
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fingerprintCandidate, initializePipeline, inspectPipeline, pipelineStateDirectory, validatePipelineContext, validateVerificationRequest, } from '../pipeline/pipeline-state.js';
6
- import { normalizeRuntimeConfig } from './config.js';
6
+ import { normalizeRuntimeConfig, resolveRoleDescriptor, roleIds } from './config.js';
7
7
  import { createExecutorRegistry } from './executors.js';
8
8
  import { archive, child, journal, parseAgentObject, SLUG } from './graph/artifacts.js';
9
9
  import { coreNodes } from './graph/nodes.js';
@@ -15,10 +15,10 @@ import { assertEffortSupported } from './capabilities.js';
15
15
  import { runtimePackageIntegrity } from './runtime-identity.js';
16
16
  import { readWorkflowState, runWorkflow } from './workflow.js';
17
17
  export const RUNTIME_API_VERSION = 1;
18
- export const CORE_WORKFLOW_VERSION = '6';
18
+ export const CORE_WORKFLOW_VERSION = '7';
19
19
  export const CORE_PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
20
20
  export function coreRuntimeIdentity() {
21
- return { packageVersion: CORE_PACKAGE_VERSION, workflowVersion: CORE_WORKFLOW_VERSION, instructionsVersion: String(ROLE_INSTRUCTIONS_VERSION), packageIntegrity: runtimePackageIntegrity(), apiVersion: 1 };
21
+ return { packageVersion: CORE_PACKAGE_VERSION, workflowVersion: CORE_WORKFLOW_VERSION, instructionsVersion: String(ROLE_INSTRUCTIONS_VERSION), packageIntegrity: runtimePackageIntegrity(), apiVersion: RUNTIME_API_VERSION };
22
22
  }
23
23
  export { parseAgentObject };
24
24
  /** Read-only admission: validate scope, limits and actual effort support before freezing a request. */
@@ -36,7 +36,7 @@ export async function preflightCoreWorkflow(options) {
36
36
  if (config.verification.length)
37
37
  validateVerificationRequest(context, { kind: 'scoped', commands: config.verification });
38
38
  const registry = options.registry ?? createExecutorRegistry(config);
39
- for (const selected of Object.values(config.agents)) {
39
+ for (const selected of roleIds(config).map(id => resolveRoleDescriptor(config, id))) {
40
40
  registry.validateLimits(selected.provider, config.limits ?? {});
41
41
  for (const tier of [selected, ...(selected.escalation ? [selected.escalation] : [])]) {
42
42
  if (tier.effort !== undefined)
@@ -45,9 +45,19 @@ export async function preflightCoreWorkflow(options) {
45
45
  }
46
46
  return { context, config, registry };
47
47
  }
48
+ /** The legacy graph's resume identity; extracting it must never change its fingerprint. */
49
+ export function implementationWorkflowDefinition(nodes, { attempts, compactDeveloper }) {
50
+ return {
51
+ id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, schema: CoreState, entry: 'architect',
52
+ // The compact developer finishes task groups over developer→verify passes
53
+ // that do not count as corrections (bounded: +2 passes each).
54
+ maxTransitions: attempts * 3 + 3 + (compactDeveloper ? attempts * 2 * 2 : 0),
55
+ nodes: Object.fromEntries(CORE_NODE_ORDER.map(id => [id, nodes[id]])),
56
+ };
57
+ }
48
58
  /**
49
- * Runs the Core implementation graph: architect → developer → verify → review (→ fixer → verify → review on corrections)er → archive,
50
- * with bounded corrections routed back to the developer, an autonomous investigation pass
59
+ * Runs the Core implementation graph: architect → developer → verify → reviewer → archive,
60
+ * with bounded corrections through fixer → verify → reviewer, an autonomous investigation pass
51
61
  * before a low-confidence design asks the requester, real verification receipts, acceptance
52
62
  * evidence bound to the exact candidate, and an optional approval before archive.
53
63
  */
@@ -68,12 +78,12 @@ export async function runCoreWorkflow(options) {
68
78
  }
69
79
  }
70
80
  const prepared = prepareOpenSpec(context.artifactRoot, options.change, directory);
71
- const openspec = Object.fromEntries(['architect', 'developer', 'reviewer'].map(role => {
72
- const provider = config.providers.find(item => item.id === config.agents[role].provider);
73
- return [role, roleOpenSpecContext(prepared, context.artifactRoot, options.change, directory, role, provider?.kind === 'cli' ? provider.cli : 'claude')];
81
+ const openspec = Object.fromEntries(roleIds(config).filter(role => resolveRoleDescriptor(config, role).openspecSkill).map(role => {
82
+ const provider = config.providers.find(item => item.id === resolveRoleDescriptor(config, role).provider);
83
+ return [role, roleOpenSpecContext(prepared, context.artifactRoot, options.change, directory, resolveRoleDescriptor(config, role), provider?.kind === 'cli' ? provider.cli : 'claude')];
74
84
  }));
75
- for (const role of ['architect', 'developer', 'reviewer'])
76
- await registry.get(config.agents[role].provider).validateOpenSpec?.(openspec[role]);
85
+ for (const role of Object.keys(openspec))
86
+ await registry.get(resolveRoleDescriptor(config, role).provider).validateOpenSpec?.(openspec[role]);
77
87
  const attempts = config.limits?.maxAttempts ?? 3;
78
88
  const developerProvider = config.providers.find(item => item.id === config.agents.developer.provider);
79
89
  const compactDeveloper = developerProvider?.kind === 'openai-compatible' && (developerProvider.agentLoop ?? 'compact') === 'compact';
@@ -127,13 +137,6 @@ export async function runCoreWorkflow(options) {
127
137
  return inspection.phases.archive.status === 'done';
128
138
  return true;
129
139
  },
130
- workflow: {
131
- id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, schema: CoreState, entry: 'architect',
132
- // The compact (small-model) developer finishes its task groups over
133
- // several developer→verify passes that do not count as corrections, so
134
- // the transition budget must cover them too (bounded: +2 passes each).
135
- maxTransitions: attempts * 3 + 3 + (compactDeveloper ? attempts * 2 * 2 : 0),
136
- nodes: Object.fromEntries(CORE_NODE_ORDER.map(id => [id, nodes[id]])),
137
- },
140
+ workflow: implementationWorkflowDefinition(nodes, { attempts, compactDeveloper }),
138
141
  });
139
142
  }
@@ -13,6 +13,9 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
13
13
  notRunEvidenceIds?: string[];
14
14
  };
15
15
  };
16
+ }, durableEvidence?: {
17
+ items: Array<Record<string, unknown>>;
18
+ available: boolean;
16
19
  }): {
17
20
  schemaVersion: 1;
18
21
  currentEvidenceIds: string[];
@@ -24,7 +27,7 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
24
27
  archive: import("../pipeline/pipeline-state.js").PhaseStatus;
25
28
  delivery: "pending" | "complete" | "pending-host";
26
29
  roles: {
27
- role: "architect" | "developer" | "reviewer";
30
+ role: string;
28
31
  provider: string | null;
29
32
  model: string | null;
30
33
  effort: string | null;
@@ -1,36 +1,39 @@
1
+ import { resolveRoleDescriptor, roleIds } from './config.js';
2
+ import { BUILTIN_ROLES } from './executor-types.js';
1
3
  import { readVerificationEvidence } from '../pipeline/pipeline-state.js';
2
4
  import { runtimeEfficiency } from './efficiency.js';
3
5
  /** Compact derived view. Durable invocation/evidence IDs remain the accounting authority. */
4
- export function efficiencySummary(state, context, config, completion, identity) {
6
+ export function efficiencySummary(state, context, config, completion, identity, durableEvidence) {
5
7
  const invocations = state.history.flatMap(attempt => [...(attempt.invocations ?? []), ...(attempt.pendingInvocations ?? [])].map(call => ({ ...call, role: attempt.stepId, attemptId: attempt.id })));
6
8
  const kinds = ['initial', 'correction', 'repair', 'deepen', 'session-fallback'];
7
9
  const byKind = Object.fromEntries(kinds.map(kind => [kind, invocations.filter(call => call.kind === kind).length]));
8
- const measured = state.history.every(attempt => !attempt.pendingInvocations?.length) && state.history.every(attempt => !['architect', 'developer', 'reviewer'].includes(attempt.stepId) || attempt.invocations !== undefined) && invocations.every(call => call.kind !== undefined && call.promptBytes !== undefined);
10
+ const measured = state.history.every(attempt => !attempt.pendingInvocations?.length) && state.history.every(attempt => !(config ? roleIds(config) : [...BUILTIN_ROLES]).includes(attempt.stepId) || attempt.invocations !== undefined) && invocations.every(call => call.kind !== undefined && call.promptBytes !== undefined);
9
11
  const bytes = (key) => invocations.every(call => call[key] !== undefined) ? invocations.reduce((sum, call) => sum + call[key], 0) : null;
10
12
  const checkIds = new Set();
11
- let checks = [], evidenceAvailable = true;
12
- try {
13
- let cursor;
14
- do {
15
- const page = readVerificationEvidence(context, { limit: 100, ...(cursor ? { cursor } : {}) });
16
- if (!page.available) {
17
- evidenceAvailable = false;
18
- break;
19
- }
20
- for (const row of page.items ?? [])
21
- if (typeof row.id === 'string' && !checkIds.has(row.id)) {
22
- checkIds.add(row.id);
23
- checks.push(row);
13
+ let checks = durableEvidence?.items ?? [], evidenceAvailable = durableEvidence?.available ?? true;
14
+ if (!durableEvidence)
15
+ try {
16
+ let cursor;
17
+ do {
18
+ const page = readVerificationEvidence(context, { limit: 100, ...(cursor ? { cursor } : {}) });
19
+ if (!page.available) {
20
+ evidenceAvailable = false;
21
+ break;
24
22
  }
25
- if (checkIds.size > 10000)
26
- throw new Error('Evidence summary limit');
27
- cursor = 'nextCursor' in page ? page.nextCursor : undefined;
28
- } while (cursor);
29
- }
30
- catch {
31
- checks = [];
32
- evidenceAvailable = false;
33
- }
23
+ for (const row of page.items ?? [])
24
+ if (typeof row.id === 'string' && !checkIds.has(row.id)) {
25
+ checkIds.add(row.id);
26
+ checks.push(row);
27
+ }
28
+ if (checkIds.size > 10000)
29
+ throw new Error('Evidence summary limit');
30
+ cursor = 'nextCursor' in page ? page.nextCursor : undefined;
31
+ } while (cursor);
32
+ }
33
+ catch {
34
+ checks = [];
35
+ evidenceAvailable = false;
36
+ }
34
37
  const checksComplete = evidenceAvailable && checks.every(check => check.status !== 'interrupted' && typeof check.durationMs === 'number');
35
38
  const escalatedRoles = new Set();
36
39
  const escalations = invocations.filter(call => { if (call.tier !== 'escalation' || escalatedRoles.has(call.role))
@@ -39,13 +42,13 @@ export function efficiencySummary(state, context, config, completion, identity)
39
42
  schemaVersion: 1, currentEvidenceIds: [...(identity?.verification?.receipt?.commands.flatMap(command => command.evidenceId ? [command.evidenceId] : []) ?? []), ...(identity?.verification?.receipt?.notRunEvidenceIds ?? [])].slice(0, 100), planHash: identity?.planHash ?? null, candidateHash: identity?.candidateHash ?? null, runId: state.runId, workflowVersion: state.workflowVersion,
40
43
  technicalAcceptance: completion?.validation === 'verified' ? 'validated' : completion?.validation === 'with-exceptions' ? 'with-exceptions' : completion?.validation === 'blocked' ? 'blocked' : 'pending',
41
44
  archive: completion?.archive ?? 'pending', delivery: completion?.delivery ?? 'pending',
42
- roles: ['architect', 'developer', 'reviewer'].map(role => {
43
- const selected = config?.agents[role], latest = invocations.filter(call => call.role === role).at(-1);
45
+ roles: (config ? roleIds(config) : [...BUILTIN_ROLES]).map(role => {
46
+ const selected = (config ? resolveRoleDescriptor(config, role) : undefined), latest = invocations.filter(call => call.role === role).at(-1);
44
47
  return { role, provider: latest?.provider ?? selected?.provider ?? null, model: latest?.model ?? selected?.model ?? null, effort: latest ? latest.requestedEffort ?? null : selected?.effort ?? null, observedModel: null, observedEffort: null, origin: 'frozen-runtime-config', tier: latest?.tier ?? 'base' };
45
48
  }),
46
49
  invocations: { byKind, total: invocations.length, complete: measured, promptBytes: bytes('promptBytes'), contextBytes: bytes('contextBytes'), handoffBytes: bytes('handoffBytes'), fullContexts: invocations.filter(call => call.contextMode === 'full').length, incrementalContexts: invocations.filter(call => call.contextMode === 'incremental').length },
47
50
  escalations: escalations.slice(-16), escalationsTruncated: escalations.length > 16,
48
51
  checks: { invalidated: identity && evidenceAvailable ? checks.filter(check => check.planHash !== identity.planHash || check.candidateHash !== identity.candidateHash || check.reuseReason === 'snapshot-inputs-changed-during-verification').length : null, available: evidenceAvailable, complete: checksComplete, executed: checksComplete ? checks.filter(check => check.disposition === 'executed').length : null, reused: evidenceAvailable ? checks.filter(check => check.disposition === 'reused').length : null, notRun: evidenceAvailable ? checks.filter(check => check.disposition === 'not-run').length : null, durationMs: checksComplete ? checks.reduce((sum, check) => sum + (typeof check.durationMs === 'number' ? check.durationMs : 0), 0) : null },
49
- metrics: runtimeEfficiency(state),
52
+ metrics: runtimeEfficiency(state, config ? roleIds(config) : undefined),
50
53
  };
51
54
  }
@@ -18,10 +18,10 @@ export interface ProviderInvocation {
18
18
  provider: string;
19
19
  /** Requested model; absent means the provider selected its default. */
20
20
  model?: string;
21
- status: 'succeeded' | 'failed';
22
- /** Entire executor wall time, including its native tools and process startup. */
23
- durationMs: number;
24
- toolCalls: number;
21
+ status: 'succeeded' | 'failed' | 'interrupted';
22
+ /** Entire executor wall time; unknown after losing an executor process. */
23
+ durationMs: number | null;
24
+ toolCalls: number | null;
25
25
  usage: CacheTokenUsage & {
26
26
  inputTokens: number | null;
27
27
  outputTokens: number | null;
@@ -2,4 +2,4 @@ import type { WorkflowState } from './workflow-types.js';
2
2
  import { type RuntimeEfficiency } from './efficiency-types.js';
3
3
  export * from './efficiency-types.js';
4
4
  /** Read-only projection: failed/superseded attempts remain spend, polling never changes it. */
5
- export declare function runtimeEfficiency(state: WorkflowState): RuntimeEfficiency;
5
+ export declare function runtimeEfficiency(state: WorkflowState, roleIds?: readonly string[]): RuntimeEfficiency;