specrails-core 6.6.5 → 6.7.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 (118) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +5 -2
  2. package/dist/agent-runtime/capabilities.js +8 -2
  3. package/dist/agent-runtime/change-scope.d.ts +2 -0
  4. package/dist/agent-runtime/change-scope.js +5 -0
  5. package/dist/agent-runtime/cli-executor.js +7 -0
  6. package/dist/agent-runtime/cli.d.ts +6 -1
  7. package/dist/agent-runtime/cli.js +15 -4
  8. package/dist/agent-runtime/compact/reviewer.js +3 -1
  9. package/dist/agent-runtime/config.js +16 -5
  10. package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
  11. package/dist/agent-runtime/efficiency-summary.js +7 -1
  12. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
  13. package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
  14. package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
  15. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
  16. package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
  17. package/dist/agent-runtime/engine/cli.d.ts +12 -0
  18. package/dist/agent-runtime/engine/cli.js +15 -4
  19. package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
  20. package/dist/agent-runtime/engine/definition-schema.js +1 -0
  21. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  22. package/dist/agent-runtime/engine/definition-types.js +1 -1
  23. package/dist/agent-runtime/engine/definition-validator.js +8 -0
  24. package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
  25. package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
  26. package/dist/agent-runtime/engine/invocation-context.js +2 -0
  27. package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
  28. package/dist/agent-runtime/engine/piece-registry.js +1 -1
  29. package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
  30. package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
  31. package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
  32. package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
  33. package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
  34. package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
  35. package/dist/agent-runtime/engine/pieces/index.js +2 -1
  36. package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
  37. package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
  38. package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
  39. package/dist/agent-runtime/engine/preflight.d.ts +2 -1
  40. package/dist/agent-runtime/engine/preflight.js +3 -1
  41. package/dist/agent-runtime/engine/regex.d.ts +12 -2
  42. package/dist/agent-runtime/engine/regex.js +25 -11
  43. package/dist/agent-runtime/engine/run-status.d.ts +34 -0
  44. package/dist/agent-runtime/engine/run-status.js +7 -1
  45. package/dist/agent-runtime/engine/runs.d.ts +56 -0
  46. package/dist/agent-runtime/engine/runs.js +88 -5
  47. package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
  48. package/dist/agent-runtime/engine/steering/inbox.js +21 -1
  49. package/dist/agent-runtime/engine/storage/private-path.js +7 -1
  50. package/dist/agent-runtime/evaluation.js +1 -1
  51. package/dist/agent-runtime/executor-types.d.ts +19 -0
  52. package/dist/agent-runtime/executor-types.js +10 -0
  53. package/dist/agent-runtime/failure-classification.d.ts +51 -0
  54. package/dist/agent-runtime/failure-classification.js +110 -0
  55. package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
  56. package/dist/agent-runtime/graph/artifacts.js +5 -1
  57. package/dist/agent-runtime/graph/baseline.d.ts +50 -0
  58. package/dist/agent-runtime/graph/baseline.js +145 -0
  59. package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
  60. package/dist/agent-runtime/graph/capability-map.js +65 -0
  61. package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
  62. package/dist/agent-runtime/graph/clarifications.js +158 -0
  63. package/dist/agent-runtime/graph/nodes.js +178 -19
  64. package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
  65. package/dist/agent-runtime/graph/review-brief-record.js +26 -0
  66. package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
  67. package/dist/agent-runtime/graph/review-brief.js +127 -0
  68. package/dist/agent-runtime/graph/roles.js +5 -3
  69. package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
  70. package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
  71. package/dist/agent-runtime/graph/state.d.ts +8 -0
  72. package/dist/agent-runtime/index.d.ts +7 -0
  73. package/dist/agent-runtime/index.js +7 -0
  74. package/dist/agent-runtime/openspec.d.ts +1 -0
  75. package/dist/agent-runtime/openspec.js +1 -1
  76. package/dist/agent-runtime/project-rules.d.ts +139 -0
  77. package/dist/agent-runtime/project-rules.js +683 -0
  78. package/dist/agent-runtime/prompts.d.ts +32 -1
  79. package/dist/agent-runtime/prompts.js +180 -7
  80. package/dist/agent-runtime/role-routing.d.ts +2 -1
  81. package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
  82. package/dist/agent-runtime/session/application/active-session.js +163 -7
  83. package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
  84. package/dist/agent-runtime/session/domain/descriptor.js +5 -0
  85. package/dist/agent-runtime/session/domain/interruption.js +1 -0
  86. package/dist/agent-runtime/session/domain/policy.js +2 -0
  87. package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
  88. package/dist/agent-runtime/session/domain/redaction.js +71 -0
  89. package/dist/agent-runtime/session/domain/types.d.ts +17 -1
  90. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
  91. package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
  92. package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
  93. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
  94. package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
  95. package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
  96. package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
  97. package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
  98. package/dist/agent-runtime/session/host/protocol.js +10 -2
  99. package/dist/agent-runtime/session/host/server.js +6 -5
  100. package/dist/agent-runtime/session/index.d.ts +1 -1
  101. package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
  102. package/dist/agent-runtime/session/ports.d.ts +6 -2
  103. package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
  104. package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
  105. package/dist/installer/cli.js +10 -1
  106. package/dist/installer/commands/init.js +14 -3
  107. package/dist/installer/phases/scaffold.js +56 -5
  108. package/dist/pipeline/pipeline-state.d.ts +15 -0
  109. package/dist/pipeline/pipeline-state.js +36 -3
  110. package/dist/pipeline/spec-trace.d.ts +134 -0
  111. package/dist/pipeline/spec-trace.js +392 -0
  112. package/dist/shared/secret-redaction.d.ts +42 -0
  113. package/dist/shared/secret-redaction.js +184 -0
  114. package/integration-contract.json +177 -7
  115. package/package.json +7 -7
  116. package/schemas/agent-runtime.schema.json +3 -1
  117. package/schemas/workflow-definition.schema.json +1 -0
  118. package/templates/settings/codex-config.toml +3 -2
@@ -2,8 +2,11 @@ import { type CliProcessRunner } from './cli-process.js';
2
2
  import { type CliProvider, type ExecutorCapabilities, type RuntimeAgentConfig, type RuntimeConfig } from './executor-types.js';
3
3
  import type { ExecutorRegistry } from './executors.js';
4
4
  export declare function unknownCapabilities(transport: string): ExecutorCapabilities;
5
- /** Introspection never calls an inference endpoint. Only the explicit Codex
6
- * exec-resume contract is recognized; its possible cold fallback needs full context. */
5
+ /** Introspection never calls an inference endpoint. Two resume contracts are
6
+ * recognized: Codex `exec resume`, whose possible cold fallback needs full
7
+ * context, and Claude `-p --resume <id>`, which either continues the stored
8
+ * conversation of that ID or fails with "No conversation found" (mapped to
9
+ * `session_not_found`, so the role falls back to a fresh full turn). */
7
10
  export declare function cliCapabilities(provider: CliProvider, model: string | undefined, options?: {
8
11
  runProcess?: CliProcessRunner;
9
12
  env?: NodeJS.ProcessEnv;
@@ -7,8 +7,11 @@ import { AgentExecutionError } from './executor-types.js';
7
7
  export function unknownCapabilities(transport) {
8
8
  return { transport, continuation: 'unknown', effortSupport: 'unknown', supportedEfforts: null, observedModel: false, observedEffort: false };
9
9
  }
10
- /** Introspection never calls an inference endpoint. Only the explicit Codex
11
- * exec-resume contract is recognized; its possible cold fallback needs full context. */
10
+ /** Introspection never calls an inference endpoint. Two resume contracts are
11
+ * recognized: Codex `exec resume`, whose possible cold fallback needs full
12
+ * context, and Claude `-p --resume <id>`, which either continues the stored
13
+ * conversation of that ID or fails with "No conversation found" (mapped to
14
+ * `session_not_found`, so the role falls back to a fresh full turn). */
12
15
  export async function cliCapabilities(provider, model, options = {}) {
13
16
  const result = unknownCapabilities(provider === 'kimi' ? 'kimi-acp' : `${provider}-cli`);
14
17
  if (provider === 'kimi')
@@ -30,6 +33,9 @@ export async function cliCapabilities(provider, model, options = {}) {
30
33
  }
31
34
  }
32
35
  if (provider === 'claude') {
36
+ // Claude Code 2.1.309 (recorded): an unknown ID fails before inference with "No conversation found"; it never starts cold.
37
+ if (/^\s*(?:-r, )?--resume\b/m.test(help.stdout))
38
+ result.continuation = 'supported';
33
39
  const section = /--effort\s+<[^>]+>([\s\S]*?)(?=\n\s+--|$)/.exec(help.stdout)?.[1];
34
40
  const levels = section?.match(/\(([^)]+)\)/)?.[1].split(',').map(value => value.trim());
35
41
  if (levels?.length && levels.every(value => /^[a-z][a-z0-9_-]{0,31}$/.test(value)))
@@ -27,6 +27,8 @@ export declare function assertProposalInScope(context: PipelineContext, command:
27
27
  }, what?: string): void;
28
28
  /** Base commit per repository id; the first call records it. */
29
29
  export declare function changeBases(context: PipelineContext): Record<string, string>;
30
+ /** The commit HEAD names in a repository checkout, or null when it has none. */
31
+ export declare function repositoryHead(root: string): string | null;
30
32
  /** Every path that differs from the base: tracked edits (staged or not), deletions and new untracked files. */
31
33
  export declare function repositoryChanges(context: PipelineContext, repository: PipelineRepository, base: string, change?: string): ChangedFile[];
32
34
  /** What this run changed, per repository and bounded: untracked files that predate the run are not part of it. */
@@ -119,6 +119,11 @@ function baseRecords(context) {
119
119
  export function changeBases(context) {
120
120
  return Object.fromEntries(Object.entries(baseRecords(context)).map(([id, record]) => [id, record.base]));
121
121
  }
122
+ /** The commit HEAD names in a repository checkout, or null when it has none. */
123
+ export function repositoryHead(root) {
124
+ const head = git(root, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], true);
125
+ return head.ok && /^[a-f0-9]{40,64}$/.test(head.stdout.trim()) ? head.stdout.trim() : null;
126
+ }
122
127
  /** Host and runtime state, the change's own OpenSpec artifacts and generated trees are never part of the code change. */
123
128
  function ignored(context, repository, change, file, fresh) {
124
129
  if (file === '.specrails' || file.startsWith('.specrails/'))
@@ -14,6 +14,8 @@ import { canonicalWorkspace } from './workspace-tools.js';
14
14
  import { parseStructuredText } from './openai-executor.js';
15
15
  import { executeKimiReadonlyAcp } from './kimi-acp.js';
16
16
  import { assertGeminiAdminPolicyAvailable, GEMINI_READONLY_POLICY } from './gemini-policy.js';
17
+ /** Claude's print-mode answer to `--resume <id>` when no transcript exists for that ID in this project. */
18
+ const CLAUDE_SESSION_MISSING = /No conversation found with session ID/i;
17
19
  /** Tools a Claude developer may not use: nested agents and platform skills would start a second, unobserved workflow. */
18
20
  const CLAUDE_DEVELOPER_DISALLOWED = 'Agent,Task,Skill';
19
21
  /**
@@ -419,6 +421,11 @@ export class CliExecutor {
419
421
  throw new AgentExecutionError(message, parsed.failureKind, parsed.usage);
420
422
  }
421
423
  if (result.exitCode !== 0 || parsed.failed) {
424
+ // A resumed Claude session whose transcript is gone fails before any
425
+ // inference; the role layer then starts a fresh full turn.
426
+ if (this.provider === 'claude' && request.resumeSessionId && CLAUDE_SESSION_MISSING.test(result.stdout + '\n' + result.stderr)) {
427
+ throw new AgentExecutionError('claude has no stored conversation for the resumed session', 'session_not_found', parsed.usage);
428
+ }
422
429
  const diagnostic = providerDiagnostic(result.stdout, result.stderr, { ...process.env, ...this.options.env });
423
430
  throw new AgentExecutionError(`${this.provider} execution failed${result.exitCode !== 0 ? ` (exit ${result.exitCode})` : ''}.${diagnostic ? ' ' + diagnostic : ' The provider did not report a diagnostic.'}`, 'provider_execution_error', parsed.usage);
424
431
  }
@@ -1,3 +1,4 @@
1
+ import { type FailureClassification } from './failure-classification.js';
1
2
  import type { WorkflowState } from './workflow-types.js';
2
3
  /** Both runtime entry points emit the same machine-readable fatal result. */
3
4
  export declare function runtimeFailure(error: unknown): {
@@ -10,7 +11,11 @@ export declare function runtimeFailure(error: unknown): {
10
11
  };
11
12
  };
12
13
  /** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
13
- export declare const RUNTIME_CLI_OPERATIONS: readonly ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'host', 'help'];
14
+ export declare const RUNTIME_CLI_OPERATIONS: readonly ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'pause', 'host', 'help'];
15
+ /** `failure` (contract 5.8): the structured classification of a failed or blocked engine 1 run; absent otherwise. */
16
+ export declare function failureField(state: WorkflowState | null): {
17
+ failure?: FailureClassification;
18
+ };
14
19
  /** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
15
20
  export declare function compactState(state: WorkflowState | null): {
16
21
  runId: string;
@@ -1,3 +1,4 @@
1
+ import { classifyFailure } from './failure-classification.js';
1
2
  import { isDefinitionCommand, NODE_KINDS_VERSION, runDefinitionCommand } from './engine/cli.js';
2
3
  import { validationPieceRegistry } from './engine/pieces/index.js';
3
4
  import { GUARDRAIL_CATALOG } from './guardrails.js';
@@ -14,6 +15,10 @@ import { CORE_PACKAGE_VERSION, CORE_WORKFLOW_VERSION, RUNTIME_API_VERSION, coreR
14
15
  import { sameRuntimeIdentity } from './runtime-identity.js';
15
16
  import { readWorkflowState } from './workflow.js';
16
17
  import { runtimeEfficiency } from './efficiency.js';
18
+ import { readReviewBriefRecord } from './graph/review-brief-record.js';
19
+ import { readClarificationsRecord } from './graph/clarifications.js';
20
+ import { readSpecTraceReport } from './graph/spec-trace-record.js';
21
+ import { readBaselineRecord } from './graph/baseline.js';
17
22
  import { configuredCapabilities } from './capabilities.js';
18
23
  import { createExecutorRegistry } from './executors.js';
19
24
  import { runRecovery } from './recovery.js';
@@ -69,7 +74,7 @@ export function runtimeFailure(error) {
69
74
  : error instanceof Error ? error.message : String(error) };
70
75
  }
71
76
  /** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
72
- export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'host', 'help'];
77
+ export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'pause', 'host', 'help'];
73
78
  function read(file) { return JSON.parse(readFileSync(file, 'utf8')); }
74
79
  async function readStdin() {
75
80
  const chunks = [];
@@ -83,6 +88,11 @@ async function readStdin() {
83
88
  }
84
89
  return JSON.parse(Buffer.concat(chunks).toString('utf8'));
85
90
  }
91
+ /** `failure` (contract 5.8): the structured classification of a failed or blocked engine 1 run; absent otherwise. */
92
+ export function failureField(state) {
93
+ const failure = state ? classifyFailure({ status: state.status, error: state.error, steps: state.steps, nextStep: state.nextStep }) : undefined;
94
+ return failure ? { failure } : {};
95
+ }
86
96
  /** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
87
97
  export function compactState(state) {
88
98
  if (!state)
@@ -194,6 +204,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
194
204
  'specrails-core runtime fork --context <json> --from <nodePath> --run-id <newRunId> [--scope-id <scope>] [--visit <number>] [--state <json>]',
195
205
  'specrails-core runtime signal --context <json> --stdin [--request-id <id>]',
196
206
  'specrails-core runtime cancel --context <json> [--request-id <id>]',
207
+ 'specrails-core runtime pause --context <json> [--request-id <id>] [--reason requested|budget|<token>]',
197
208
  'specrails-core runtime host --stdio [--scope <projectKey|global>]',
198
209
  ] });
199
210
  return 0;
@@ -203,7 +214,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
203
214
  emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
204
215
  engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
205
216
  capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
206
- engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1, sessions: 1 }, guardrails: GUARDRAIL_CATALOG });
217
+ engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1, sessions: 1, flowSessions: 1, reviewBrief: 1, deriveChange: 1, architectClarifications: 1, specTrace: 1, baselineOnTouch: 1, rulesInventory: 1, pauseRuns: 1, failureClassification: 1, findingLocations: 1 }, guardrails: GUARDRAIL_CATALOG });
207
218
  return 0;
208
219
  }
209
220
  if (command === 'host')
@@ -249,7 +260,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
249
260
  const pipeline = previous ? inspectPipeline(context) : null;
250
261
  const requestPath = path.join(pipelineStateDirectory(context), 'agent-runtime-request.json');
251
262
  const config = existsSync(requestPath) ? read(requestPath).config : undefined;
252
- emit({ type: 'runtime-status', state: flags.compact ? compactState(previous) : previous, pipeline: flags.compact ? compactPipeline(pipeline) : pipeline, ...(previous ? { metrics: runtimeEfficiency(previous), efficiencySummary: efficiencySummary(previous, context, config, pipeline?.completion, pipeline ?? undefined) } : {}) });
263
+ emit({ type: 'runtime-status', ...failureField(previous), state: flags.compact ? compactState(previous) : previous, pipeline: flags.compact ? compactPipeline(pipeline) : pipeline, ...(previous ? { metrics: runtimeEfficiency(previous), efficiencySummary: efficiencySummary(previous, context, config, pipeline?.completion, pipeline ?? undefined) } : {}) });
253
264
  return 0;
254
265
  }
255
266
  const requestFile = path.join(pipelineStateDirectory(context), 'agent-runtime-request.json');
@@ -304,7 +315,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
304
315
  inspection = inspectPipeline(context);
305
316
  }
306
317
  catch { /* A failed scope inspection must not erase recorded spend. */ }
307
- emit({ type: 'runtime-result', runId: state.runId, traceId: state.traceId, status: state.status, nextStep: state.nextStep, error: state.error, pendingApproval: state.pendingApproval, pendingQuestion: state.pendingQuestion, usage: state.usage, invocationUsage: invocationUsage(state, previous?.history.length ?? 0), metrics: runtimeEfficiency(state), efficiencySummary: efficiencySummary(state, context, request.config, inspection?.completion, inspection) });
318
+ emit({ type: 'runtime-result', runId: state.runId, traceId: state.traceId, status: state.status, nextStep: state.nextStep, error: state.error, pendingApproval: state.pendingApproval, pendingQuestion: state.pendingQuestion, usage: state.usage, invocationUsage: invocationUsage(state, previous?.history.length ?? 0), metrics: runtimeEfficiency(state), efficiencySummary: efficiencySummary(state, context, request.config, inspection?.completion, inspection), reviewBrief: readReviewBriefRecord(context), clarifications: readClarificationsRecord(context), specTrace: readSpecTraceReport(context), baseline: readBaselineRecord(context), ...failureField(state) });
308
319
  return state.status === 'succeeded' ? 0 : state.status === 'paused' ? 2 : 1;
309
320
  }
310
321
  finally {
@@ -171,7 +171,9 @@ export async function runCompactReviewer(env, options = {}) {
171
171
  approved: (raw.approved === true || resolved) && !uncertified && issues.length === 0,
172
172
  summary: text(raw.summary).slice(0, 8000), issues, score: lift(raw.score, gate?.minScore),
173
173
  aspects: Object.fromEntries(REVIEW_ASPECTS.map(name => [name, lift(aspectsRaw[name], gate?.aspects[name])])),
174
- acceptance: { criteria: certified, checks: [], findings: strings(acceptanceRaw.findings, 50, 1000) },
174
+ acceptance: { criteria: certified, checks: [], findings: strings(acceptanceRaw.findings, 50, 1000), ...(Array.isArray(acceptanceRaw.findingLocations) ? { findingLocations: acceptanceRaw.findingLocations } : {}) },
175
+ // The optional risk brief passes through untouched; the reviewer node validates it.
176
+ ...(raw.brief !== undefined && raw.brief !== null ? { brief: raw.brief } : {}),
175
177
  };
176
178
  return { text: JSON.stringify(structured), usage: env.client.usage, structured };
177
179
  }
@@ -1,7 +1,7 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { validateGuardrailSettings } from './guardrails.js';
3
3
  import { readFileSync } from 'node:fs';
4
- import { BUILTIN_ROLES, BUILTIN_ROLE_POLICY, ROLE_ID, isBuiltinRole } from './executor-types.js';
4
+ import { BUILTIN_ROLES, BUILTIN_ROLE_POLICY, ENGINE_ROLE_POLICY, ROLE_ID, isBuiltinRole, isEngineRole } from './executor-types.js';
5
5
  import { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS } from './graph/review-policy.js';
6
6
  const ROLES = [...BUILTIN_ROLES];
7
7
  const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
@@ -119,7 +119,7 @@ function uniqueKeys(commands, list) {
119
119
  }
120
120
  export function validateRuntimeConfig(input, options = {}) {
121
121
  const config = object(input, '$');
122
- keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'roles', 'fixer', 'limits', 'verification', 'setup', 'approvalBeforeArchive', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
122
+ keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'roles', 'fixer', 'limits', 'verification', 'setup', 'approvalBeforeArchive', 'baselineOnTouch', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
123
123
  if (config.schemaVersion !== 1)
124
124
  fail('schemaVersion', 'expected 1');
125
125
  if (config.enabled !== undefined && typeof config.enabled !== 'boolean')
@@ -205,8 +205,8 @@ export function validateRuntimeConfig(input, options = {}) {
205
205
  const raw = object(config.roles, 'roles');
206
206
  roles = {};
207
207
  for (const [id, value] of Object.entries(raw)) {
208
- if (!ROLE_ID.test(id) || id === 'fixer')
209
- fail(`roles.${id}`, 'expected a role id; fixer is reserved');
208
+ if (!ROLE_ID.test(id) || id === 'fixer' || isEngineRole(id))
209
+ fail(`roles.${id}`, 'expected a role id; fixer and Core engine roles (deriver) are reserved');
210
210
  const descriptor = object(value, `roles.${id}`);
211
211
  const { access, artifacts, prompt, openspecSkill, ...assignment } = descriptor;
212
212
  const selected = validateAgent(assignment, `roles.${id}`);
@@ -272,6 +272,8 @@ export function validateRuntimeConfig(input, options = {}) {
272
272
  }
273
273
  if (config.approvalBeforeArchive !== undefined && typeof config.approvalBeforeArchive !== 'boolean')
274
274
  fail('approvalBeforeArchive', 'expected boolean');
275
+ if (config.baselineOnTouch !== undefined && typeof config.baselineOnTouch !== 'boolean')
276
+ fail('baselineOnTouch', 'expected boolean');
275
277
  let review;
276
278
  if (config.review !== undefined) {
277
279
  const raw = object(config.review, 'review');
@@ -288,13 +290,18 @@ export function validateRuntimeConfig(input, options = {}) {
288
290
  let architect;
289
291
  if (config.architect !== undefined) {
290
292
  const raw = object(config.architect, 'architect');
291
- keys(raw, ['onLowConfidence'], 'architect');
293
+ keys(raw, ['onLowConfidence', 'clarifications'], 'architect');
292
294
  architect = {};
293
295
  if (raw.onLowConfidence !== undefined) {
294
296
  if (raw.onLowConfidence !== 'ask' && raw.onLowConfidence !== 'proceed')
295
297
  fail('architect.onLowConfidence', 'expected "ask" or "proceed"');
296
298
  architect.onLowConfidence = raw.onLowConfidence;
297
299
  }
300
+ if (raw.clarifications !== undefined) {
301
+ if (raw.clarifications !== 'ask-on-high' && raw.clarifications !== 'always-proceed')
302
+ fail('architect.clarifications', 'expected "ask-on-high" or "always-proceed"');
303
+ architect.clarifications = raw.clarifications;
304
+ }
298
305
  }
299
306
  let rolePrompts;
300
307
  if (config.rolePrompts !== undefined) {
@@ -315,6 +322,7 @@ export function validateRuntimeConfig(input, options = {}) {
315
322
  ...(rolePrompts === undefined ? {} : { rolePrompts }),
316
323
  ...(config.limits === undefined ? {} : { limits: { ...config.limits } }),
317
324
  ...(config.approvalBeforeArchive === undefined ? {} : { approvalBeforeArchive: config.approvalBeforeArchive }),
325
+ ...(config.baselineOnTouch === undefined ? {} : { baselineOnTouch: config.baselineOnTouch }),
318
326
  ...(review === undefined ? {} : { review }),
319
327
  ...(architect === undefined ? {} : { architect }),
320
328
  ...(guardrails === undefined ? {} : { guardrails }),
@@ -338,6 +346,9 @@ export function roleIds(config) { return [...new Set([...BUILTIN_ROLES, ...Objec
338
346
  export function resolveRoleDescriptor(config, id) {
339
347
  if (isBuiltinRole(id))
340
348
  return { ...config.agents[id], ...BUILTIN_ROLE_POLICY[id], id };
349
+ // Engine roles reuse the architect's assignment; their instructions are Core's (prompts.ts), never configuration.
350
+ if (isEngineRole(id))
351
+ return { ...config.agents.architect, ...ENGINE_ROLE_POLICY[id], id };
341
352
  const role = Object.hasOwn(config.roles ?? {}, id) ? config.roles[id] : undefined;
342
353
  if (!role)
343
354
  throw new Error(`Unknown runtime role '${id}'`);
@@ -48,6 +48,13 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
48
48
  fullContexts: number;
49
49
  incrementalContexts: number;
50
50
  };
51
+ baseline: {
52
+ invocations: number;
53
+ roles: string[];
54
+ costUsd: number | null;
55
+ inputTokens: number | null;
56
+ outputTokens: number | null;
57
+ };
51
58
  escalations: {
52
59
  role: string;
53
60
  attemptId: string;
@@ -5,7 +5,7 @@ import { runtimeEfficiency } from './efficiency.js';
5
5
  /** Compact derived view. Durable invocation/evidence IDs remain the accounting authority. */
6
6
  export function efficiencySummary(state, context, config, completion, identity, durableEvidence) {
7
7
  const invocations = state.history.flatMap(attempt => [...(attempt.invocations ?? []), ...(attempt.pendingInvocations ?? [])].map(call => ({ ...call, role: attempt.stepId, attemptId: attempt.id })));
8
- const kinds = ['initial', 'correction', 'repair', 'deepen', 'session-fallback'];
8
+ const kinds = ['initial', 'correction', 'repair', 'deepen', 'session-fallback', 'baseline'];
9
9
  const byKind = Object.fromEntries(kinds.map(kind => [kind, invocations.filter(call => call.kind === kind).length]));
10
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);
11
11
  const bytes = (key) => invocations.every(call => call[key] !== undefined) ? invocations.reduce((sum, call) => sum + call[key], 0) : null;
@@ -47,8 +47,14 @@ export function efficiencySummary(state, context, config, completion, identity,
47
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' };
48
48
  }),
49
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 },
50
+ baseline: baselineSpend(invocations.filter(call => call.kind === 'baseline')),
50
51
  escalations: escalations.slice(-16), escalationsTruncated: escalations.length > 16,
51
52
  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 },
52
53
  metrics: runtimeEfficiency(state, config ? roleIds(config) : undefined),
53
54
  };
54
55
  }
56
+ /** Spend of the baseline-on-touch turns, kept apart from the change's own cost; unknown values stay unknown. */
57
+ function baselineSpend(calls) {
58
+ const sum = (key) => calls.some(call => call.usage?.[key] == null) ? null : calls.reduce((total, call) => total + call.usage[key], 0);
59
+ return { invocations: calls.length, roles: [...new Set(calls.map(call => call.role))], costUsd: sum('costUsd'), inputTokens: sum('inputTokens'), outputTokens: sum('outputTokens') };
60
+ }
@@ -34,7 +34,7 @@ export function implementationFixture(approval = false) {
34
34
  await tools.execute({ action: 'new' });
35
35
  const files = [
36
36
  ['proposal', 'proposal.md', '# Why\nReturn two.\n# What Changes\nUpdate the function.\n# Capabilities\n## New Capabilities\n- feature: Return the required value.\n# Impact\nOne function.'],
37
- ['design', 'design.md', '# Design\nUpdate code.cjs and verify the return value.'],
37
+ ['design', 'design.md', '# Design\nUpdate code.cjs and verify the return value.\n## Non-Goals\n- Nothing outside code.cjs.\n'],
38
38
  ['specs', `specs/${capability}/spec.md`, '## ADDED Requirements\n### Requirement: Return the required value\nThe function SHALL return 2.\n#### Scenario: Calling the function\n- **WHEN** code.cjs is loaded\n- **THEN** it returns 2\n'],
39
39
  ['tasks', 'tasks.md', '- [ ] 1. Implement the return value and verify behavior\n'],
40
40
  ];
@@ -22,6 +22,8 @@ declare const keys: {
22
22
  readonly writes: readonly ['thread_id', 'checkpoint_ns', 'checkpoint_id', 'task_id', 'idx'];
23
23
  };
24
24
  export type RevisionTable = keyof typeof keys;
25
+ /** Current run database schema. Version 1 lacks the 'pause' control kind and stays readable. */
26
+ export declare const RUN_DATABASE_SCHEMA = 2;
25
27
  /** Short synchronous transactions own graph writes and ledger evidence together. */
26
28
  export declare class RunDatabase {
27
29
  readonly filename: string;
@@ -34,6 +36,13 @@ export declare class RunDatabase {
34
36
  create?: boolean;
35
37
  readOnly?: boolean;
36
38
  }): Promise<RunDatabase>;
39
+ get schemaVersion(): number;
40
+ /**
41
+ * Upgrades a version 1 database in place. Only callers that already proved the
42
+ * run belongs to this Core package call it: a retained older Core must keep
43
+ * opening the databases of its own runs.
44
+ */
45
+ upgradeSchema(): void;
37
46
  get revision(): number;
38
47
  get transactionRevision(): number;
39
48
  /** Undefined reason is reserved for leases/read reservations, which do not change graph history. */
@@ -61,7 +61,7 @@ CREATE TABLE interrupts (
61
61
  attempt_id TEXT NOT NULL REFERENCES attempts(attempt_id), node_path TEXT NOT NULL, scope_id TEXT NOT NULL, branch_id TEXT,
62
62
  kind TEXT NOT NULL, requested_at TEXT NOT NULL, payload_json TEXT NOT NULL, answered_at TEXT, answer_json TEXT);
63
63
  CREATE TABLE control_inbox (
64
- request_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), kind TEXT NOT NULL CHECK(kind IN ('cancel','steer')),
64
+ request_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), kind TEXT NOT NULL CHECK(kind IN ('cancel','steer','pause')),
65
65
  payload_json TEXT NOT NULL, created_at TEXT NOT NULL, received_sequence INTEGER NOT NULL DEFAULT 0, consumed_by_attempt_id TEXT, consumed_at TEXT);
66
66
  CREATE TABLE role_sessions (
67
67
  session_key TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), scope_id TEXT NOT NULL, node_path TEXT NOT NULL,
@@ -95,7 +95,22 @@ CREATE INDEX visits_run ON visits(run_id,node_path,scope_id,local_visit);
95
95
  CREATE INDEX attempts_visit ON attempts(visit_id,attempt);
96
96
  CREATE INDEX events_cursor ON events(run_id,sequence);
97
97
  CREATE INDEX revisions_cut ON row_versions(revision,table_name);
98
- PRAGMA user_version=1;
98
+ PRAGMA user_version=2;
99
+ `;
100
+ /** Current run database schema. Version 1 lacks the 'pause' control kind and stays readable. */
101
+ export const RUN_DATABASE_SCHEMA = 2;
102
+ /**
103
+ * Version 1 → 2 only widens the control_inbox kind CHECK to admit 'pause'.
104
+ * SQLite cannot alter a CHECK constraint, so the table is rebuilt in place.
105
+ */
106
+ const migrateV1ToV2 = `
107
+ CREATE TABLE control_inbox_v2 (
108
+ request_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), kind TEXT NOT NULL CHECK(kind IN ('cancel','steer','pause')),
109
+ payload_json TEXT NOT NULL, created_at TEXT NOT NULL, received_sequence INTEGER NOT NULL DEFAULT 0, consumed_by_attempt_id TEXT, consumed_at TEXT);
110
+ INSERT INTO control_inbox_v2 SELECT request_id,run_id,kind,payload_json,created_at,received_sequence,consumed_by_attempt_id,consumed_at FROM control_inbox;
111
+ DROP TABLE control_inbox;
112
+ ALTER TABLE control_inbox_v2 RENAME TO control_inbox;
113
+ PRAGMA user_version=2;
99
114
  `;
100
115
  function encodeRow(row) {
101
116
  return JSON.stringify(Object.fromEntries(Object.entries(row).map(([key, value]) => [key,
@@ -137,7 +152,7 @@ export class RunDatabase {
137
152
  throw error;
138
153
  }
139
154
  }
140
- else if (version !== 1)
155
+ else if (version !== 1 && version !== RUN_DATABASE_SCHEMA)
141
156
  throw new PersistenceError('resume_incompatible', `Unsupported run database schema ${version}`);
142
157
  return new RunDatabase(target, sqlite);
143
158
  }
@@ -146,6 +161,28 @@ export class RunDatabase {
146
161
  throw error;
147
162
  }
148
163
  }
164
+ get schemaVersion() { return Number(this.sqlite.prepare('PRAGMA user_version').get()?.user_version); }
165
+ /**
166
+ * Upgrades a version 1 database in place. Only callers that already proved the
167
+ * run belongs to this Core package call it: a retained older Core must keep
168
+ * opening the databases of its own runs.
169
+ */
170
+ upgradeSchema() {
171
+ if (this.schemaVersion === RUN_DATABASE_SCHEMA)
172
+ return;
173
+ if (this.inTransaction)
174
+ throw new PersistenceError('internal', 'Schema upgrades cannot run inside a transaction');
175
+ this.sqlite.exec('BEGIN IMMEDIATE');
176
+ try {
177
+ if (this.schemaVersion === 1)
178
+ this.sqlite.exec(migrateV1ToV2);
179
+ this.sqlite.exec('COMMIT');
180
+ }
181
+ catch (error) {
182
+ this.sqlite.exec('ROLLBACK');
183
+ throw error;
184
+ }
185
+ }
149
186
  get revision() { return Number(this.sqlite.prepare('SELECT COALESCE(MAX(revision),0) AS value FROM durable_revisions').get().value); }
150
187
  get transactionRevision() {
151
188
  if (this.activeRevision === undefined)
@@ -33,6 +33,7 @@ export declare class RunLedger {
33
33
  private event;
34
34
  /** Public node namespace differs from the parent saver namespace; never parse opaque namespace strings. */
35
35
  private bindPendingTask;
36
+ /** `pause` only at attempt admission: an admitted attempt finishes all of its invocations. */
36
37
  private checkAdmission;
37
38
  enter(input: NodeAdmission): AttemptFrame;
38
39
  resultFor(frame: AttemptFrame): PieceResult | undefined;
@@ -70,10 +70,15 @@ export class RunLedger {
70
70
  this.db.put('visits', { ...visit, saver_checkpoint_ns: task.taskCheckpointNs });
71
71
  return visit;
72
72
  }
73
- checkAdmission() {
73
+ /** `pause` only at attempt admission: an admitted attempt finishes all of its invocations. */
74
+ checkAdmission(options = {}) {
74
75
  const row = this.run(), budget = this.db.get('budget', { run_id: this.runId });
75
76
  if (this.db.sqlite.prepare("SELECT 1 FROM control_inbox WHERE run_id=? AND kind='cancel' AND consumed_at IS NULL").get(this.runId))
76
77
  throw new EngineError('aborted', 'Cancellation requested');
78
+ // A cooperative pause lets the current attempts settle and refuses the next one.
79
+ const pause = options.pause ? new ControlInbox(this.db, this.runId).pauseRequested() : undefined;
80
+ if (pause)
81
+ throw new EngineError('paused', 'Pause requested', { requestId: pause.requestId, reason: pause.reason });
77
82
  if (this.options.failFast && Number(row.consecutive_failures) >= this.options.failFast)
78
83
  throw new EngineError('fail_fast', 'Consecutive AI failure limit reached');
79
84
  const duration = this.activeDurationMs();
@@ -112,7 +117,7 @@ export class RunLedger {
112
117
  this.db.put('runs', { ...this.run(), status: 'running', active_started_at: this.run().active_started_at ?? at });
113
118
  return frame;
114
119
  }
115
- this.checkAdmission();
120
+ this.checkAdmission({ pause: true });
116
121
  if (prior?.status === 'running' && prior.lease_epoch === this.token.epoch && !prior.inherited)
117
122
  throw new EngineError('attempt_running', 'The task is already executing');
118
123
  if (prior && (prior.status === 'running' || prior.status === 'interrupted') && input.effect === 'write' && !prior.recovery_authorized)
@@ -7,4 +7,16 @@ type Flags = Record<string, string | boolean>;
7
7
  * created before its request was materialized) still resolves to this engine.
8
8
  */
9
9
  export declare function isDefinitionCommand(flags: Flags, command: string): boolean;
10
+ /** `failure` (contract 5.8): the structured classification of a failed or blocked definition run; absent otherwise. */
11
+ export declare function definitionFailure(state: {
12
+ status: string;
13
+ steps?: unknown;
14
+ recentFailures?: unknown;
15
+ nextStep?: unknown;
16
+ error?: unknown;
17
+ }): {
18
+ failure: import("../failure-classification.js").FailureClassification;
19
+ } | {
20
+ failure?: undefined;
21
+ };
10
22
  export declare function runDefinitionCommand(flags: Flags, positionals: string[], emit: (value: unknown) => void): Promise<number>;
@@ -12,8 +12,9 @@ import { validateWorkflowDefinition } from './definition-validator.js';
12
12
  import { configuredRoles } from './preflight.js';
13
13
  import { validationPieceRegistry } from './pieces/index.js';
14
14
  import { NODE_KINDS_VERSION } from './piece-registry.js';
15
- import { cancelRun, createRun, definitionRunDirectory, freezeRunRequest, frozenRequestEngine, frozenRequestFile, resumeRun, signalRun } from './runs.js';
15
+ import { cancelRun, createRun, pauseRun, definitionRunDirectory, freezeRunRequest, frozenRequestEngine, frozenRequestFile, resumeRun, signalRun } from './runs.js';
16
16
  import { statusRun } from './run-status.js';
17
+ import { classifyFailure } from '../failure-classification.js';
17
18
  import { forkRun } from './fork.js';
18
19
  export { NODE_KINDS_VERSION };
19
20
  const required = (flags, key) => {
@@ -48,7 +49,7 @@ function directory(flags) {
48
49
  * created before its request was materialized) still resolves to this engine.
49
50
  */
50
51
  export function isDefinitionCommand(flags, command) {
51
- if (['workflows', 'fork', 'signal', 'cancel'].includes(command) || flags.definition !== undefined && command === 'run')
52
+ if (['workflows', 'fork', 'signal', 'cancel', 'pause'].includes(command) || flags.definition !== undefined && command === 'run')
52
53
  return true;
53
54
  if (!['resume', 'status'].includes(command))
54
55
  return false;
@@ -63,6 +64,11 @@ export function isDefinitionCommand(flags, command) {
63
64
  }
64
65
  /** Public workflow identity carried by status and results, matching the frozen request block. */
65
66
  const workflowIdentity = (workflow) => ({ ...workflow, definitionHash: workflow.version, engine: 2 });
67
+ /** `failure` (contract 5.8): the structured classification of a failed or blocked definition run; absent otherwise. */
68
+ export function definitionFailure(state) {
69
+ const failure = classifyFailure({ status: state.status, error: state.error, steps: state.steps, recentFailures: state.recentFailures, nextStep: state.nextStep });
70
+ return failure ? { failure } : {};
71
+ }
66
72
  export async function runDefinitionCommand(flags, positionals, emit) {
67
73
  const command = positionals[0];
68
74
  if (command === 'workflows') {
@@ -82,7 +88,7 @@ export async function runDefinitionCommand(flags, positionals, emit) {
82
88
  }
83
89
  if (command === 'status') {
84
90
  const status = await statusRun(directory(flags), flags.compact === true);
85
- emit({ ...status, workflow: workflowIdentity(status.workflow) });
91
+ emit({ ...status, ...definitionFailure(status.state), workflow: workflowIdentity(status.workflow) });
86
92
  return 0;
87
93
  }
88
94
  if (command === 'signal') {
@@ -93,6 +99,11 @@ export async function runDefinitionCommand(flags, positionals, emit) {
93
99
  emit({ type: 'runtime-signal-accepted', id: message.id, acceptedAt: message.createdAt });
94
100
  return 0;
95
101
  }
102
+ if (command === 'pause') {
103
+ // Exit 0 for every outcome: a terminal or already stopped run is a reported no-op.
104
+ emit({ type: 'runtime-pause-result', ...await pauseRun(directory(flags), { requestId: optional(flags, 'request-id'), reason: optional(flags, 'reason') }) });
105
+ return 0;
106
+ }
96
107
  if (command === 'cancel') {
97
108
  emit({ type: 'runtime-cancellation-accepted', ...await cancelRun(directory(flags), optional(flags, 'request-id')) });
98
109
  return 0;
@@ -160,7 +171,7 @@ export async function runDefinitionCommand(flags, positionals, emit) {
160
171
  const current = result.state.usage[key], prior = previous ? previous.state.usage[key] : 0;
161
172
  return current === null || prior === null ? null : Math.max(0, current - prior);
162
173
  };
163
- emit({ type: 'runtime-result', engineVersion: 2, ...result.state, revision: result.revision, eventCursor: result.eventCursor, workflow: workflowIdentity(result.workflow), completion: result.completion,
174
+ emit({ type: 'runtime-result', engineVersion: 2, ...result.state, ...definitionFailure({ ...result.state, ...('error' in result ? { error: result.error } : {}) }), revision: result.revision, eventCursor: result.eventCursor, workflow: workflowIdentity(result.workflow), completion: result.completion,
164
175
  ...('forkOf' in result ? { forkOf: result.forkOf } : {}), ...('error' in result ? { error: result.error } : {}),
165
176
  usage: result.state.usage, invocationUsage: { costUsd: delta('costUsd'), inputTokens: delta('inputTokens'), outputTokens: delta('outputTokens') }, metrics: result.metrics, efficiencySummary: result.efficiencySummary });
166
177
  return result.state.status === 'succeeded' ? 0 : result.state.status === 'paused' ? 2 : 1;
@@ -192,7 +192,7 @@ export declare const workflowDefinitionSchema: {
192
192
  readonly required: readonly ["kind", "params", "ends"];
193
193
  readonly properties: {
194
194
  readonly kind: {
195
- readonly enum: readonly ["prompt", "role-turn", "decider", "condition", "assign", "verify", "shell", "openspec-validate", "openspec-archive", "approval", "question", "gate", "map", "join", "component", "implementation", "implementation-step", "artifact-contract", "end"];
195
+ readonly enum: readonly ["prompt", "role-turn", "decider", "condition", "assign", "verify", "shell", "openspec-validate", "openspec-archive", "approval", "question", "gate", "map", "join", "component", "implementation", "implementation-step", "derive-change", "artifact-contract", "end"];
196
196
  };
197
197
  readonly params: {
198
198
  readonly type: "object";
@@ -252,6 +252,7 @@ export const workflowDefinitionSchema = {
252
252
  "component",
253
253
  "implementation",
254
254
  "implementation-step",
255
+ "derive-change",
255
256
  "artifact-contract",
256
257
  "end"
257
258
  ]
@@ -1,5 +1,5 @@
1
1
  import type { EngineEffect, JsonObject, WorkflowBudget } from './contracts.js';
2
- export declare const PIECE_KINDS: readonly ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell', 'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join', 'component', 'implementation', 'implementation-step', 'end'];
2
+ export declare const PIECE_KINDS: readonly ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell', 'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join', 'component', 'implementation', 'implementation-step', 'end', 'derive-change'];
3
3
  export type PieceKind = typeof PIECE_KINDS[number];
4
4
  export interface DefinitionNode {
5
5
  kind: PieceKind;
@@ -1,3 +1,3 @@
1
1
  export const PIECE_KINDS = ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell',
2
2
  'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join',
3
- 'component', 'implementation', 'implementation-step', 'end'];
3
+ 'component', 'implementation', 'implementation-step', 'end', 'derive-change'];
@@ -116,6 +116,14 @@ export function validateWorkflowDefinition(input, registry, roles = {}, options
116
116
  add('role_not_found', path + '/params/roleId', 'Used role is not configured');
117
117
  if (node.kind === 'decider' && typeof roleId === 'string' && roles[roleId]?.access !== 'read')
118
118
  add('invalid_role_access', path + '/params/roleId', 'Decider role must be read-only');
119
+ // A correction round edits code. Portable (structural) validation cannot know an unresolved role's access; execution validates again.
120
+ if (node.kind === 'role-turn' && node.params.stance === 'fixer' && typeof roleId === 'string' && Object.hasOwn(roles, roleId) && !unresolved.includes(roleId) && roles[roleId]?.access !== 'write')
121
+ add('invalid_role_access', path + '/params/stance', 'The fixer stance requires a role with write access');
122
+ }
123
+ // derive-change always invokes Core's engine role `deriver`.
124
+ if (node.kind === 'derive-change') {
125
+ if (!definition.roles.includes('deriver'))
126
+ add('role_undeclared', path, 'derive-change requires the deriver role in definition.roles');
119
127
  }
120
128
  const effect = effectOf(node);
121
129
  writes ||= effect === 'write';
@@ -33,6 +33,13 @@ export declare function definitionEfficiencySummary(ledger: RunLedger, state: Wo
33
33
  fullContexts: number;
34
34
  incrementalContexts: number;
35
35
  };
36
+ baseline: {
37
+ invocations: number;
38
+ roles: string[];
39
+ costUsd: number | null;
40
+ inputTokens: number | null;
41
+ outputTokens: number | null;
42
+ };
36
43
  escalations: {
37
44
  role: string;
38
45
  attemptId: string;