specrails-core 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-runtime/capabilities.js +3 -1
  3. package/dist/agent-runtime/cli-executor.d.ts +3 -0
  4. package/dist/agent-runtime/cli-executor.js +50 -13
  5. package/dist/agent-runtime/cli-process.js +6 -2
  6. package/dist/agent-runtime/cli.d.ts +15 -0
  7. package/dist/agent-runtime/cli.js +43 -9
  8. package/dist/agent-runtime/compact/chat-client.js +2 -1
  9. package/dist/agent-runtime/config.d.ts +4 -1
  10. package/dist/agent-runtime/config.js +41 -3
  11. package/dist/agent-runtime/core-host.d.ts +9 -3
  12. package/dist/agent-runtime/core-host.js +21 -18
  13. package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
  14. package/dist/agent-runtime/efficiency-summary.js +30 -27
  15. package/dist/agent-runtime/efficiency-types.d.ts +4 -4
  16. package/dist/agent-runtime/efficiency.d.ts +1 -1
  17. package/dist/agent-runtime/efficiency.js +7 -6
  18. package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
  19. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
  20. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
  21. package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
  22. package/dist/agent-runtime/engine/budget.d.ts +9 -0
  23. package/dist/agent-runtime/engine/budget.js +33 -0
  24. package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
  25. package/dist/agent-runtime/engine/canonical-json.js +162 -0
  26. package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
  27. package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
  28. package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
  29. package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
  30. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
  31. package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
  32. package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
  33. package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
  34. package/dist/agent-runtime/engine/cli.d.ts +10 -0
  35. package/dist/agent-runtime/engine/cli.js +172 -0
  36. package/dist/agent-runtime/engine/compiler.d.ts +23 -0
  37. package/dist/agent-runtime/engine/compiler.js +261 -0
  38. package/dist/agent-runtime/engine/composition.d.ts +16 -0
  39. package/dist/agent-runtime/engine/composition.js +28 -0
  40. package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
  41. package/dist/agent-runtime/engine/concurrency.js +55 -0
  42. package/dist/agent-runtime/engine/contracts.d.ts +273 -0
  43. package/dist/agent-runtime/engine/contracts.js +11 -0
  44. package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
  45. package/dist/agent-runtime/engine/definition-schema.js +313 -0
  46. package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
  47. package/dist/agent-runtime/engine/definition-types.js +3 -0
  48. package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
  49. package/dist/agent-runtime/engine/definition-validator.js +187 -0
  50. package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
  51. package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
  52. package/dist/agent-runtime/engine/events.d.ts +19 -0
  53. package/dist/agent-runtime/engine/events.js +88 -0
  54. package/dist/agent-runtime/engine/execution.d.ts +29 -0
  55. package/dist/agent-runtime/engine/execution.js +121 -0
  56. package/dist/agent-runtime/engine/expressions.d.ts +11 -0
  57. package/dist/agent-runtime/engine/expressions.js +172 -0
  58. package/dist/agent-runtime/engine/fork.d.ts +28 -0
  59. package/dist/agent-runtime/engine/fork.js +211 -0
  60. package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
  61. package/dist/agent-runtime/engine/graph-description.js +16 -0
  62. package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
  63. package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
  64. package/dist/agent-runtime/engine/index.d.ts +14 -0
  65. package/dist/agent-runtime/engine/index.js +14 -0
  66. package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
  67. package/dist/agent-runtime/engine/invocation-context.js +128 -0
  68. package/dist/agent-runtime/engine/otel.d.ts +15 -0
  69. package/dist/agent-runtime/engine/otel.js +132 -0
  70. package/dist/agent-runtime/engine/output.d.ts +4 -0
  71. package/dist/agent-runtime/engine/output.js +73 -0
  72. package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
  73. package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
  74. package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
  75. package/dist/agent-runtime/engine/piece-registry.js +62 -0
  76. package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
  77. package/dist/agent-runtime/engine/pieces/assign.js +35 -0
  78. package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
  79. package/dist/agent-runtime/engine/pieces/control.js +49 -0
  80. package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
  81. package/dist/agent-runtime/engine/pieces/decider.js +70 -0
  82. package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
  83. package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
  84. package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
  85. package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
  86. package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
  87. package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
  88. package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
  89. package/dist/agent-runtime/engine/pieces/index.js +22 -0
  90. package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
  91. package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
  92. package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
  93. package/dist/agent-runtime/engine/pieces/ports.js +1 -0
  94. package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
  95. package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
  96. package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
  97. package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
  98. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
  99. package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
  100. package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
  101. package/dist/agent-runtime/engine/pieces/shared.js +40 -0
  102. package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
  103. package/dist/agent-runtime/engine/pieces/shell.js +47 -0
  104. package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
  105. package/dist/agent-runtime/engine/pieces/verify.js +52 -0
  106. package/dist/agent-runtime/engine/preflight.d.ts +27 -0
  107. package/dist/agent-runtime/engine/preflight.js +54 -0
  108. package/dist/agent-runtime/engine/regex.d.ts +3 -0
  109. package/dist/agent-runtime/engine/regex.js +19 -0
  110. package/dist/agent-runtime/engine/run-status.d.ts +563 -0
  111. package/dist/agent-runtime/engine/run-status.js +61 -0
  112. package/dist/agent-runtime/engine/runs.d.ts +605 -0
  113. package/dist/agent-runtime/engine/runs.js +247 -0
  114. package/dist/agent-runtime/engine/state.d.ts +64 -0
  115. package/dist/agent-runtime/engine/state.js +98 -0
  116. package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
  117. package/dist/agent-runtime/engine/steering/inbox.js +113 -0
  118. package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
  119. package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
  120. package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
  121. package/dist/agent-runtime/engine/storage/private-path.js +74 -0
  122. package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
  123. package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
  124. package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
  125. package/dist/agent-runtime/evaluation-corpus.js +4 -0
  126. package/dist/agent-runtime/evaluation.d.ts +9 -3
  127. package/dist/agent-runtime/evaluation.js +50 -8
  128. package/dist/agent-runtime/executor-types.d.ts +53 -3
  129. package/dist/agent-runtime/executor-types.js +35 -2
  130. package/dist/agent-runtime/executors.js +2 -2
  131. package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
  132. package/dist/agent-runtime/graph/artifacts.js +82 -9
  133. package/dist/agent-runtime/graph/nodes.d.ts +2 -0
  134. package/dist/agent-runtime/graph/nodes.js +2 -2
  135. package/dist/agent-runtime/graph/roles.d.ts +6 -0
  136. package/dist/agent-runtime/graph/roles.js +23 -17
  137. package/dist/agent-runtime/kimi-acp.js +6 -6
  138. package/dist/agent-runtime/openai-executor.js +17 -12
  139. package/dist/agent-runtime/openspec.d.ts +17 -2
  140. package/dist/agent-runtime/openspec.js +54 -20
  141. package/dist/agent-runtime/prompts.d.ts +4 -3
  142. package/dist/agent-runtime/prompts.js +26 -5
  143. package/dist/agent-runtime/recovery.d.ts +1 -1
  144. package/dist/agent-runtime/repository-context.js +1 -1
  145. package/dist/agent-runtime/role-routing.js +4 -2
  146. package/dist/agent-runtime/role-state.d.ts +5 -0
  147. package/dist/agent-runtime/verification-plan.d.ts +2 -0
  148. package/dist/agent-runtime/verification-plan.js +10 -0
  149. package/dist/agent-runtime/workflow-types.d.ts +2 -0
  150. package/dist/agent-runtime/workflow.d.ts +3 -1
  151. package/dist/agent-runtime/workflow.js +7 -3
  152. package/dist/agent-runtime/workspace-tools.d.ts +2 -2
  153. package/dist/agent-runtime/workspace-tools.js +6 -6
  154. package/dist/installer/cli.js +5 -0
  155. package/dist/installer/phases/scaffold.d.ts +2 -2
  156. package/dist/installer/phases/scaffold.js +3 -7
  157. package/dist/pipeline/pipeline-state.d.ts +38 -4
  158. package/dist/pipeline/pipeline-state.js +94 -21
  159. package/integration-contract.json +83 -11
  160. package/package.json +12 -5
  161. package/schemas/agent-runtime.schema.json +93 -1
  162. package/schemas/workflow-definition.schema.json +312 -0
  163. package/templates/kimi/specrails/run-skill.mjs +15 -1
  164. package/templates/commands/specrails/batch-implement.md +0 -32
@@ -13,6 +13,11 @@ export interface RoleExecutionState {
13
13
  attemptId: string;
14
14
  }>>;
15
15
  }
16
+ /** Bound by the engine to one run/scope/node; the legacy adapter remains file-backed. */
17
+ export interface RoleStatePort {
18
+ read(): RoleExecutionState;
19
+ write(state: RoleExecutionState): void;
20
+ }
16
21
  /** Host-owned auxiliary state lives under the workflow's existing run lease.
17
22
  * A corrupted packet fails explicitly; it is never used as missing context. */
18
23
  export declare function readRoleState(directory: string): RoleExecutionState;
@@ -53,4 +53,6 @@ export declare function addDeveloperChecks(context: PipelineContext, proposals:
53
53
  export declare function expandedPlanCommands(context: PipelineContext, plan: VerificationPlan): VerificationCommand[];
54
54
  /** Bind the exact manifest and every source to lifecycle gates. */
55
55
  export declare function bindPlan(context: PipelineContext, plan: VerificationPlan): void;
56
+ /** Rebind a checkpoint-owned plan to a new run, retaining the exact admitted checks. */
57
+ export declare function forkVerificationPlan(context: PipelineContext, source: VerificationPlan, sourceContext: PipelineContext): VerificationPlan;
56
58
  export {};
@@ -208,3 +208,13 @@ export function bindPlan(context, plan) {
208
208
  }
209
209
  bindVerificationPlan(context, plan.planHash, files);
210
210
  }
211
+ /** Rebind a checkpoint-owned plan to a new run, retaining the exact admitted checks. */
212
+ export function forkVerificationPlan(context, source, sourceContext) {
213
+ const { integrity, ...payload } = source;
214
+ if (integrity !== fingerprint(payload) || source.scopeHash !== fingerprint(sourceContext) || source.runId !== sourceContext.runId
215
+ || source.planHash !== fingerprint({ policyVersion: 1, scopeHash: source.scopeHash, executionPolicy: source.executionPolicy, entries: source.entries }))
216
+ throw new Error('Fork verification plan failed its source integrity check');
217
+ const plan = savePlan(context, source.baseline, source.developer, null, source.executionPolicy);
218
+ bindPlan(context, plan);
219
+ return plan;
220
+ }
@@ -52,6 +52,8 @@ export interface WorkflowStepContext {
52
52
  attemptId: string;
53
53
  attempt: number;
54
54
  input: JsonValue;
55
+ /** Engine-owned, durably claimed operator text; cannot replace the frozen scope. */
56
+ operatorSteering?: string;
55
57
  signal: AbortSignal;
56
58
  /** A detached snapshot; modifying it cannot modify the persisted run. */
57
59
  checkpoint: WorkflowState;
@@ -1,10 +1,12 @@
1
- import type { RunWorkflowOptions, WorkflowState } from './workflow-types.js';
1
+ import type { RunWorkflowOptions, WorkflowDefinition, WorkflowState } from './workflow-types.js';
2
2
  export * from './workflow-types.js';
3
3
  export { readWorkflowState, readWorkflowEnvelope, writeWorkflowEnvelope, WorkflowStoreError, type WorkflowEnvelope } from './durable-store.js';
4
4
  export declare class WorkflowError extends Error {
5
5
  readonly code: 'INVALID_WORKFLOW' | 'ALREADY_EXISTS' | 'NOT_FOUND' | 'INCOMPATIBLE_RESUME';
6
6
  constructor(code: 'INVALID_WORKFLOW' | 'ALREADY_EXISTS' | 'NOT_FOUND' | 'INCOMPATIBLE_RESUME', message: string);
7
7
  }
8
+ /** Pure legacy resume identity; preserve node order, declared exits and default metadata. */
9
+ export declare function definitionFingerprint<S extends Record<string, unknown>>(workflow: WorkflowDefinition<S>): string;
8
10
  /**
9
11
  * Execute a LangGraph state graph with a durable host ledger at every effect
10
12
  * boundary. LangGraph owns traversal, state reducers, checkpoints, interrupts
@@ -63,9 +63,13 @@ function validateWorkflow(workflow) {
63
63
  if (!Number.isInteger(maxTransitions) || maxTransitions < 1 || maxTransitions > 10_000) {
64
64
  throw new WorkflowError('INVALID_WORKFLOW', 'maxTransitions must be an integer from 1 to 10000');
65
65
  }
66
+ return definitionFingerprint(workflow);
67
+ }
68
+ /** Pure legacy resume identity; preserve node order, declared exits and default metadata. */
69
+ export function definitionFingerprint(workflow) {
66
70
  return fingerprint({
67
- id: workflow.id, version: workflow.version, maxTransitions, entry: workflow.entry,
68
- nodes: ids.map(id => ({ id, effect: workflow.nodes[id].effect ?? 'read', maxAttempts: workflow.nodes[id].maxAttempts ?? 1, retrySafe: workflow.nodes[id].retrySafe ?? false, ends: workflow.nodes[id].ends })),
71
+ id: workflow.id, version: workflow.version, maxTransitions: workflow.maxTransitions ?? 100, entry: workflow.entry,
72
+ nodes: Object.keys(workflow.nodes).map(id => ({ id, effect: workflow.nodes[id].effect ?? 'read', maxAttempts: workflow.nodes[id].maxAttempts ?? 1, retrySafe: workflow.nodes[id].retrySafe ?? false, ends: workflow.nodes[id].ends })),
69
73
  });
70
74
  }
71
75
  function validateUsage(usage) {
@@ -406,7 +410,7 @@ export async function runWorkflow(options) {
406
410
  },
407
411
  reportInvocation: async (invocation) => {
408
412
  validateUsage(invocation.usage);
409
- if (!Number.isFinite(invocation.durationMs) || invocation.durationMs < 0 || !Number.isSafeInteger(invocation.toolCalls) || invocation.toolCalls < 0)
413
+ if (typeof invocation.durationMs !== 'number' || !Number.isFinite(invocation.durationMs) || invocation.durationMs < 0 || typeof invocation.toolCalls !== 'number' || !Number.isSafeInteger(invocation.toolCalls) || invocation.toolCalls < 0)
410
414
  throw new TypeError('Invalid invocation measurement');
411
415
  if (invocation.invocationId && history.invocations?.some(item => item.invocationId === invocation.invocationId))
412
416
  return;
@@ -13,11 +13,11 @@ export declare function canonicalWorkspace(cwd: string, allowedRoots: string[]):
13
13
  };
14
14
  /** Application tool boundary, not an OS sandbox for concurrent external processes. */
15
15
  export declare class WorkspaceTools {
16
- private readonly role;
16
+ private readonly access;
17
17
  readonly cwd: string;
18
18
  readonly roots: string[];
19
19
  private readonly rootAliases;
20
- constructor(cwd: string, roots: string[], role: AgentRole);
20
+ constructor(cwd: string, roots: string[], access: 'read' | 'write' | AgentRole);
21
21
  definitions(): WorkspaceToolDefinition[];
22
22
  private resolve;
23
23
  execute(name: string, input: unknown): string;
@@ -33,12 +33,12 @@ export function canonicalWorkspace(cwd, allowedRoots) {
33
33
  }
34
34
  /** Application tool boundary, not an OS sandbox for concurrent external processes. */
35
35
  export class WorkspaceTools {
36
- role;
36
+ access;
37
37
  cwd;
38
38
  roots;
39
39
  rootAliases;
40
- constructor(cwd, roots, role) {
41
- this.role = role;
40
+ constructor(cwd, roots, access) {
41
+ this.access = access;
42
42
  const scope = canonicalWorkspace(cwd, roots);
43
43
  this.cwd = scope.cwd;
44
44
  this.roots = scope.roots;
@@ -52,8 +52,8 @@ export class WorkspaceTools {
52
52
  definition('read_lines', 'Read numbered lines, up to 500 lines / 48 KiB, from a UTF-8 file up to 1 MiB. Returns a full-file SHA-256 for guarded edits.', { ...file, startLine: { type: 'integer', minimum: 1 }, endLine: { type: 'integer', minimum: 1 } }, ['path']),
53
53
  definition('search_text', 'Search literal text recursively in source files. Skips dependencies, build output, metadata, binaries and symlinks. Returns bounded numbered matches and a truncation flag.', { ...file, query: { type: 'string' }, maxResults: { type: 'integer', minimum: 1, maximum: 100 } }, ['path', 'query']),
54
54
  definition('get_diff', 'Read the staged and unstaged Git diff against HEAD for one file, up to 48 KiB. Untracked files are identified; use read_lines for their content.', file, ['path']),
55
- ...(this.role === 'developer' ? [definition('write_file', 'Create or replace a UTF-8 source file (max 256 KiB). Parent directories are created. Runtime metadata cannot be changed.', { ...file, content: { type: 'string' } }, ['path', 'content'])] : []),
56
- ...(this.role === 'developer' ? [definition('apply_patch', 'Atomically replace one exact, unique fragment in a UTF-8 file. Rejects missing or ambiguous matches. Optionally guard against stale reads with expectedHash from read_lines.', { ...file, oldText: { type: 'string', minLength: 1 }, newText: { type: 'string' }, expectedHash: { type: 'string' } }, ['path', 'oldText', 'newText'])] : []),
55
+ ...((this.access === 'write' || this.access === 'developer') ? [definition('write_file', 'Create or replace a UTF-8 source file (max 256 KiB). Parent directories are created. Runtime metadata cannot be changed.', { ...file, content: { type: 'string' } }, ['path', 'content'])] : []),
56
+ ...((this.access === 'write' || this.access === 'developer') ? [definition('apply_patch', 'Atomically replace one exact, unique fragment in a UTF-8 file. Rejects missing or ambiguous matches. Optionally guard against stale reads with expectedHash from read_lines.', { ...file, oldText: { type: 'string', minLength: 1 }, newText: { type: 'string' }, expectedHash: { type: 'string' } }, ['path', 'oldText', 'newText'])] : []),
57
57
  ];
58
58
  }
59
59
  resolve(raw, write) {
@@ -92,7 +92,7 @@ export class WorkspaceTools {
92
92
  throw new Error('Tool input must be a JSON object');
93
93
  const args = input;
94
94
  if (!this.definitions().some(tool => tool.function.name === name))
95
- throw new Error(`Tool '${name}' is unavailable for the ${this.role} role`);
95
+ throw new Error(`Tool '${name}' is unavailable for the ${this.access} role`);
96
96
  const allowed = name === 'write_file' ? ['path', 'content'] : name === 'apply_patch' ? ['path', 'oldText', 'newText', 'expectedHash'] : name === 'read_lines' ? ['path', 'startLine', 'endLine'] : name === 'search_text' ? ['path', 'query', 'maxResults'] : ['path'];
97
97
  if (Object.keys(args).some(key => !allowed.includes(key)) || typeof args.path !== 'string')
98
98
  throw new Error('Invalid tool arguments');
@@ -99,6 +99,11 @@ export async function main(argv = process.argv.slice(2)) {
99
99
  return await dispatch(subcommand, flags, positionals);
100
100
  }
101
101
  catch (err) {
102
+ if (subcommand === 'runtime') {
103
+ const { runtimeFailure } = await import('../agent-runtime/cli.js');
104
+ process.stdout.write(JSON.stringify(runtimeFailure(err)) + '\n');
105
+ return 1;
106
+ }
102
107
  if (isInstallerError(err)) {
103
108
  fatal(err.message);
104
109
  return err.exitCode;
@@ -4,8 +4,8 @@ import type { Provider } from './provider-detect.js';
4
4
  * ships. The implement pipeline depends on all three. Any additional agent
5
5
  * comes from a user-authored profile (`custom-*`), never the installer.
6
6
  *
7
- * Mirrors the `allOf` baseline in schemas/profile.v1.json — update
8
- * both files together if this set ever changes.
7
+ * Mirrors the required built-in roles in schemas/agent-runtime.schema.json
8
+ * (`agents`, with the sr- prefix in installed files); keep both in sync.
9
9
  */
10
10
  export declare const CORE_AGENTS: Set<string>;
11
11
  /**
@@ -11,8 +11,8 @@ import { buildManifest, writeManifestFiles } from './manifest.js';
11
11
  * ships. The implement pipeline depends on all three. Any additional agent
12
12
  * comes from a user-authored profile (`custom-*`), never the installer.
13
13
  *
14
- * Mirrors the `allOf` baseline in schemas/profile.v1.json — update
15
- * both files together if this set ever changes.
14
+ * Mirrors the required built-in roles in schemas/agent-runtime.schema.json
15
+ * (`agents`, with the sr- prefix in installed files); keep both in sync.
16
16
  */
17
17
  export const CORE_AGENTS = new Set([
18
18
  'sr-architect',
@@ -151,10 +151,6 @@ const SKILL_FROM_COMMAND = {
151
151
  command: 'implement',
152
152
  description: 'sr:implement — Full OpenSpec lifecycle with specialized agents: architect designs, developer implements, reviewer validates. Use for implementing frozen specs or feature descriptions.',
153
153
  },
154
- 'sr-batch-implement': {
155
- command: 'batch-implement',
156
- description: 'sr:batch-implement — Run multiple frozen specs as one programmatic agent workflow.',
157
- },
158
154
  };
159
155
  /**
160
156
  * Provider-static subtrees inside a providerDir that are SHARED via symlink from
@@ -1557,7 +1553,7 @@ function renderInitialAgentsMd(repoRoot) {
1557
1553
  '',
1558
1554
  'Implementation is coordinated by the Specrails programmatic agent runtime.',
1559
1555
  'Use the frozen scope and official OpenSpec workflow supplied for your role.',
1560
- 'Do not start another implement or batch-implement workflow inside a role.',
1556
+ 'Do not start another implement workflow inside a role.',
1561
1557
  '',
1562
1558
  '## Repository context',
1563
1559
  '',
@@ -187,6 +187,9 @@ export interface PipelineState {
187
187
  acceptanceHash: string;
188
188
  };
189
189
  artifactExclusions: string[];
190
+ /** Explicit engine-owned metadata; absent for all legacy journals. */
191
+ runtimeExclusions?: readonly string[];
192
+ repositoryExclusions?: Readonly<Record<string, readonly string[]>>;
190
193
  preview?: {
191
194
  baseHash: string;
192
195
  files: PreviewFile[];
@@ -207,6 +210,18 @@ export declare function withinRepositoryScope(repository: Pick<PipelineRepositor
207
210
  export declare function repositoryWorkingDirectory(repository: Pick<PipelineRepository, 'path' | 'scope'>): string;
208
211
  export declare function validatePipelineContext(input: unknown): PipelineContext;
209
212
  export declare function pipelineStateDirectory(context: PipelineContext): string;
213
+ export type CandidateScope = Pick<PipelineState, 'context' | 'scopeHash' | 'artifactExclusions'> & {
214
+ /** Engine-owned child journals and artifact paths, never user-defined exclusion patterns. */
215
+ runtimeExclusions?: readonly string[];
216
+ repositoryExclusions?: Readonly<Record<string, readonly string[]>>;
217
+ };
218
+ /** Bind only engine-owned metadata paths; callers supply declared run/branch ownership. */
219
+ export declare function bindImplementationExclusions(contextInput: PipelineContext, exclusions: {
220
+ runtimeExclusions: readonly string[];
221
+ repositoryExclusions: Readonly<Record<string, readonly string[]>>;
222
+ }): void;
223
+ /** Read the validated implementation journal's dynamic candidate exclusions. */
224
+ export declare function readCandidateScope(contextInput: unknown): CandidateScope;
210
225
  export interface CandidateManifest {
211
226
  schemaVersion: 1;
212
227
  scopeHash: string;
@@ -216,8 +231,8 @@ export interface CandidateManifest {
216
231
  files: Array<[string, string]>;
217
232
  }>;
218
233
  }
219
- export declare function candidateManifest(state: PipelineState): CandidateManifest;
220
- export declare function fingerprintCandidate(state: PipelineState): string;
234
+ export declare function candidateManifest(state: CandidateScope): CandidateManifest;
235
+ export declare function fingerprintCandidate(state: CandidateScope): string;
221
236
  export declare function initializePipeline(contextInput: unknown, change: string): PipelineState;
222
237
  /** Host-owned binding, including immutable harness bytes outside the candidate. */
223
238
  export declare function bindVerificationPlan(contextInput: PipelineContext, hash: string, files: Array<{
@@ -270,6 +285,11 @@ export declare function verificationInvocation(command: string, args: string[],
270
285
  };
271
286
  /** Shared persisted-output/provider diagnostic redaction; preserves line structure. */
272
287
  export declare function redactRuntimeText(text: string, env?: NodeJS.ProcessEnv): string;
288
+ /** Structured shell execution with the same scope, environment and tree-kill rules, without a receipt or journal. */
289
+ export declare function executeVerificationCommand(contextInput: unknown, raw: unknown, log?: (text: string) => void, signal?: AbortSignal, options?: {
290
+ deadline?: number;
291
+ idleTimeoutMs?: number;
292
+ }): Promise<CommandReceipt>;
273
293
  export interface VerificationSnapshot {
274
294
  eligible: boolean;
275
295
  reason: string;
@@ -280,12 +300,25 @@ export interface VerificationSnapshot {
280
300
  export declare function verificationSnapshot(context: PipelineContext, command: VerificationCommand): VerificationSnapshot;
281
301
  /** Contiguous waves preserve baseline order; an uncertain check is a barrier. */
282
302
  export declare function verificationWaves<T extends VerificationCommand>(commands: T[], maxConcurrency?: number): T[][];
283
- export declare function verifyPipeline(contextInput: unknown, raw: unknown, log?: (text: string) => void, signal?: AbortSignal, options?: {
303
+ export interface VerificationRunOptions {
284
304
  deadline?: number;
285
305
  maxConcurrency?: number;
286
306
  idleTimeoutMs?: number;
287
307
  onEvidence?: (kind: 'check-started' | 'check-finished' | 'check-reused' | 'check-invalidated', payload: Record<string, string | number | null>) => Promise<void>;
288
- }): Promise<VerificationReceipt>;
308
+ }
309
+ /** Persistence and candidate ownership are bound by the host, never inferred from a journal path. */
310
+ export interface VerificationEvidencePort {
311
+ candidateHash: string;
312
+ scopeHash: string;
313
+ planHash?: string;
314
+ previous?: VerificationReceipt;
315
+ isCurrent(): boolean;
316
+ persistCheck(result: CommandReceipt, planHash: string, candidateHash: string): void | Promise<void>;
317
+ commitReceipt(receipt: VerificationReceipt): VerificationReceipt | Promise<VerificationReceipt>;
318
+ }
319
+ export declare function verifyPipeline(contextInput: unknown, raw: unknown, log?: (text: string) => void, signal?: AbortSignal, options?: VerificationRunOptions): Promise<VerificationReceipt>;
320
+ /** Same deterministic command/evidence runner for legacy journals and ledger-only engine runs. */
321
+ export declare function executeVerification(contextInput: unknown, raw: unknown, evidence: VerificationEvidencePort, log?: (text: string) => void, signal?: AbortSignal, options?: VerificationRunOptions): Promise<VerificationReceipt>;
289
322
  /** Validate a check plan without creating state or executing commands. */
290
323
  export declare function validateVerificationRequest(contextInput: unknown, raw: unknown): void;
291
324
  export declare function preparePreview(contextInput: unknown, raw: unknown): PipelineState;
@@ -299,6 +332,7 @@ export interface VerificationEvidenceQuery {
299
332
  cursor?: string;
300
333
  limit?: number;
301
334
  }
335
+ export declare function persistCheckEvidence(context: PipelineContext, result: CommandReceipt, planHash: string, candidateHash: string): void;
302
336
  /** Historical read: no live repository validation, source execution or lifecycle mutation. */
303
337
  export declare function readVerificationEvidence(context: Pick<PipelineContext, 'backlogRoot' | 'runId'>, query?: VerificationEvidenceQuery): {
304
338
  nextCursor?: string | undefined;
@@ -273,10 +273,56 @@ function saveState(state) {
273
273
  atomicJson(stateFile(state.context), state);
274
274
  }
275
275
  function relativeUnix(value) { return value.split(path.sep).join('/'); }
276
+ /** Bind only engine-owned metadata paths; callers supply declared run/branch ownership. */
277
+ export function bindImplementationExclusions(contextInput, exclusions) {
278
+ const context = validatePipelineContext(contextInput);
279
+ const root = path.join(context.backlogRoot, '.specrails/pipeline');
280
+ const runtimeExclusions = [...new Set(exclusions.runtimeExclusions)].sort();
281
+ for (const item of runtimeExclusions) {
282
+ const relative = path.relative(root, item);
283
+ const memory = ['', '-wal', '-shm'].some(suffix => item === path.join(context.backlogRoot, '.specrails/engine-store.sqlite' + suffix));
284
+ if (memory) {
285
+ safeChild(context.backlogRoot, path.relative(context.backlogRoot, item));
286
+ continue;
287
+ }
288
+ if (!path.isAbsolute(item) || path.resolve(item) !== item || !ID.test(relative) || relative.includes(path.sep))
289
+ fail('Implementation runtime exclusion is not an admitted pipeline journal');
290
+ safeChild(root, relative);
291
+ }
292
+ const repositoryExclusions = {};
293
+ for (const [id, paths] of Object.entries(exclusions.repositoryExclusions)) {
294
+ const repository = context.repositories.find(repository => repository.id === id);
295
+ if (!repository)
296
+ continue; // A scoped child receives the parent union, filtered to its frozen repositories.
297
+ repositoryExclusions[id] = [...new Set(paths)].sort();
298
+ for (const item of repositoryExclusions[id]) {
299
+ if (!/^(?:[a-zA-Z0-9][a-zA-Z0-9._-]*\/)*openspec\/(?:changes|specs)\/[a-z0-9][a-z0-9./-]*$/.test(item))
300
+ fail('Implementation artifact exclusion is outside declared OpenSpec artifacts');
301
+ safeChild(repository.path, item);
302
+ }
303
+ }
304
+ locked(context, () => {
305
+ const state = readState(context);
306
+ if (canonical(state.runtimeExclusions ?? []) === canonical(runtimeExclusions) && canonical(state.repositoryExclusions ?? {}) === canonical(repositoryExclusions))
307
+ return;
308
+ state.runtimeExclusions = runtimeExclusions;
309
+ state.repositoryExclusions = repositoryExclusions;
310
+ saveState(state);
311
+ });
312
+ }
313
+ /** Read the validated implementation journal's dynamic candidate exclusions. */
314
+ export function readCandidateScope(contextInput) {
315
+ const state = readState(validatePipelineContext(contextInput));
316
+ return { context: state.context, scopeHash: state.scopeHash, artifactExclusions: [...state.artifactExclusions], ...(state.runtimeExclusions ? { runtimeExclusions: [...state.runtimeExclusions] } : {}), ...(state.repositoryExclusions ? { repositoryExclusions: structuredClone(state.repositoryExclusions) } : {}) };
317
+ }
276
318
  function excluded(state, repo, relative) {
277
319
  const absolute = path.join(repo.path, relative);
278
320
  if (within(pipelineStateDirectory(state.context), absolute))
279
321
  return true;
322
+ if (state.runtimeExclusions?.some(directory => within(directory, absolute)))
323
+ return true;
324
+ if (state.repositoryExclusions?.[repo.id]?.some(item => relative === item || relative.startsWith(item + '/')))
325
+ return true;
280
326
  if (relative === '.specrails/runtime' || relative.startsWith('.specrails/runtime/'))
281
327
  return true;
282
328
  if (repo.id !== state.context.artifactRepositoryId)
@@ -805,6 +851,12 @@ function validateCommand(context, raw) {
805
851
  export function verificationInvocation(command, args, cwd, platform = process.platform, env = process.env) {
806
852
  if (platform !== 'win32')
807
853
  return { command, args };
854
+ // An explicit shell line (`cmd /d /s /c <line>`) is already cmd syntax. Node's
855
+ // default quoting would escape its inner quotes with backslashes, which cmd does
856
+ // not understand. Pass it verbatim: /s strips exactly the outer quotes we add.
857
+ if (/(^|[\\/])cmd(\.exe)?$/i.test(command) && args.length === 4 && args[0].toLowerCase() === '/d' && args[1].toLowerCase() === '/s' && args[2].toLowerCase() === '/c') {
858
+ return { command, args: ['/d', '/s', '/c', '"' + args[3] + '"'], windowsVerbatimArguments: true };
859
+ }
808
860
  let resolved = command;
809
861
  if (!/\.(cmd|bat|exe|com)$/i.test(command)) {
810
862
  const pathValue = Object.entries(env).find(([key]) => key.toLowerCase() === 'path')?.[1] ?? '';
@@ -845,6 +897,10 @@ export function redactRuntimeText(text, env = process.env) {
845
897
  }
846
898
  /** Silence a verification command may keep before it counts as hung (env `SPECRAILS_VERIFY_IDLE_TIMEOUT_MS`, default 5 min). */
847
899
  function verificationIdleTimeoutMs() { const raw = Number(process.env.SPECRAILS_VERIFY_IDLE_TIMEOUT_MS); return Number.isFinite(raw) && raw >= 100 ? raw : 5 * 60_000; }
900
+ /** Structured shell execution with the same scope, environment and tree-kill rules, without a receipt or journal. */
901
+ export async function executeVerificationCommand(contextInput, raw, log = () => { }, signal, options = {}) {
902
+ return executeCheck(validateCommand(validatePipelineContext(contextInput), raw), log, signal, options.deadline, undefined, options.idleTimeoutMs);
903
+ }
848
904
  async function executeCheck(command, log, signal, deadline, evidenceId = digest(randomUUID()), idleTimeoutOverrideMs) {
849
905
  const started = Date.now();
850
906
  const overrides = normalizeVerificationEnvironment(command.env ?? {});
@@ -1078,7 +1134,7 @@ export function verificationWaves(commands, maxConcurrency = 1) {
1078
1134
  }
1079
1135
  export async function verifyPipeline(contextInput, raw, log = () => { }, signal, options = {}) {
1080
1136
  const context = validatePipelineContext(contextInput);
1081
- const { request, commands, unverifiedRepositories } = verificationPlan(context, raw);
1137
+ const { request } = verificationPlan(context, raw);
1082
1138
  let previous;
1083
1139
  const state = locked(context, () => {
1084
1140
  const current = readState(context);
@@ -1097,6 +1153,33 @@ export async function verifyPipeline(contextInput, raw, log = () => { }, signal,
1097
1153
  if (identityProblems.length)
1098
1154
  fail(identityProblems.join('; '));
1099
1155
  const candidateHash = fingerprintCandidate(state);
1156
+ const isCurrent = () => {
1157
+ const current = readState(context);
1158
+ return fingerprintCandidate(current) === candidateHash && current.revision === state.revision && planReasons(current, request.planHash).length === 0;
1159
+ };
1160
+ return executeVerification(context, raw, {
1161
+ candidateHash, scopeHash: state.scopeHash, planHash: state.verificationPlan?.hash, previous,
1162
+ isCurrent: () => locked(context, isCurrent),
1163
+ persistCheck: (result, planHash, candidate) => persistCheckEvidence(context, result, planHash, candidate),
1164
+ commitReceipt: receipt => locked(context, () => {
1165
+ const current = readState(context);
1166
+ if (!isCurrent())
1167
+ receipt = { ...receipt, valid: false, reason: 'Candidate changed during verification' };
1168
+ atomicJson(safeChild(pipelineStateDirectory(context), 'receipts/' + receipt.id + '.json'), receipt);
1169
+ if (receipt.kind === 'full' || !receipt.valid || !current.verification)
1170
+ current.verification = receipt;
1171
+ else if (previous?.valid && previous.candidateHash === candidateHash)
1172
+ current.verification = previous;
1173
+ saveState(current);
1174
+ return receipt;
1175
+ }),
1176
+ }, log, signal, options);
1177
+ }
1178
+ /** Same deterministic command/evidence runner for legacy journals and ledger-only engine runs. */
1179
+ export async function executeVerification(contextInput, raw, evidence, log = () => { }, signal, options = {}) {
1180
+ const context = validatePipelineContext(contextInput);
1181
+ const { request, commands, unverifiedRepositories } = verificationPlan(context, raw);
1182
+ const { candidateHash, previous } = evidence;
1100
1183
  const results = [];
1101
1184
  const controller = new AbortController();
1102
1185
  const abort = () => controller.abort();
@@ -1116,7 +1199,7 @@ export async function verifyPipeline(contextInput, raw, log = () => { }, signal,
1116
1199
  const evidenceId = digest(randomUUID());
1117
1200
  if (!reuse) {
1118
1201
  const pending = { repositoryId: command.repositoryId, key: command.key, label: command.label, command: command.command, args: command.args, cwd: command.cwd, environmentHash: '', environmentKeys: [], environmentOverrideKeys: [], environmentOverridesHash: '', evidenceId, pending: true, disposition: 'executed', outcome: 'interrupted', exitCode: -1, durationMs: 0, output: '' };
1119
- persistCheckEvidence(context, pending, request.planHash ?? digest(canonical(commands)), candidateHash);
1202
+ await evidence.persistCheck(pending, request.planHash ?? digest(canonical(commands)), candidateHash);
1120
1203
  await options.onEvidence?.('check-started', { executionId: evidenceId, repositoryId: command.repositoryId, checkId: command.key ?? '', label: command.label ?? command.command });
1121
1204
  }
1122
1205
  const result = reuse ? { ...old, evidenceId, disposition: 'reused', reusedFrom: old.evidenceId, durationMs: 0, snapshot, reuseReason: 'snapshot-local-identities-match' } : { ...await executeCheck(command, text => log(prefix + text), controller.signal, options.deadline, evidenceId, options.idleTimeoutMs), disposition: 'executed', snapshot, reuseReason: snapshot.eligible ? 'snapshot-local-no-current-match' : snapshot.reason };
@@ -1131,7 +1214,7 @@ export async function verifyPipeline(contextInput, raw, log = () => { }, signal,
1131
1214
  }
1132
1215
  if (result.exitCode !== 0)
1133
1216
  controller.abort();
1134
- persistCheckEvidence(context, result, request.planHash ?? digest(canonical(commands)), candidateHash);
1217
+ await evidence.persistCheck(result, request.planHash ?? digest(canonical(commands)), candidateHash);
1135
1218
  await options.onEvidence?.(reuse ? 'check-reused' : 'check-finished', { executionId: evidenceId, repositoryId: command.repositoryId, checkId: command.key ?? '', label: command.label ?? command.command, exitCode: result.exitCode, durationMs: result.durationMs, reason: result.reuseReason ?? null });
1136
1219
  return result;
1137
1220
  }
@@ -1160,25 +1243,15 @@ export async function verifyPipeline(contextInput, raw, log = () => { }, signal,
1160
1243
  for (const command of commands.slice(results.length)) {
1161
1244
  const id = digest(randomUUID());
1162
1245
  const skipped = { repositoryId: command.repositoryId, key: command.key, label: command.label, command: command.command, args: command.args, cwd: command.cwd, environmentHash: '', environmentKeys: [], environmentOverrideKeys: [], environmentOverridesHash: '', evidenceId: id, disposition: 'not-run', outcome: 'cancelled', exitCode: -1, durationMs: 0, output: 'Not run after cancellation or an earlier failed check' };
1163
- persistCheckEvidence(context, skipped, request.planHash ?? digest(canonical(commands)), candidateHash);
1246
+ await evidence.persistCheck(skipped, request.planHash ?? digest(canonical(commands)), candidateHash);
1164
1247
  notRunEvidenceIds.push(id);
1165
1248
  }
1166
- const receipt = locked(context, () => {
1167
- const current = readState(context);
1168
- const changed = fingerprintCandidate(current) !== candidateHash || current.revision !== state.revision || planReasons(current, request.planHash).length > 0;
1169
- const receipt = {
1170
- id: randomUUID(), ...(state.verificationPlan ? { planHash: state.verificationPlan.hash } : {}), kind: request.kind, scopeHash: state.scopeHash, candidateHash, commands: results, ...(notRunEvidenceIds.length ? { notRunEvidenceIds } : {}),
1171
- completedAt: new Date().toISOString(), valid: !changed && !signal?.aborted && (options.deadline === undefined || Date.now() <= options.deadline) && results.length === commands.length && results.every((result) => result.exitCode === 0),
1172
- ...(changed ? { reason: 'Candidate changed during verification' } : results.some((result) => result.exitCode !== 0) ? { reason: 'A verification command failed' } : {}),
1173
- ...(unverifiedRepositories.length ? { unverifiedRepositories } : {}),
1174
- };
1175
- atomicJson(safeChild(pipelineStateDirectory(context), 'receipts/' + receipt.id + '.json'), receipt);
1176
- if (receipt.kind === 'full' || !receipt.valid || !current.verification)
1177
- current.verification = receipt;
1178
- else if (previous?.valid && previous.candidateHash === candidateHash)
1179
- current.verification = previous;
1180
- saveState(current);
1181
- return receipt;
1249
+ const changed = !evidence.isCurrent();
1250
+ const receipt = await evidence.commitReceipt({
1251
+ id: randomUUID(), ...(evidence.planHash ? { planHash: evidence.planHash } : {}), kind: request.kind, scopeHash: evidence.scopeHash, candidateHash, commands: results, ...(notRunEvidenceIds.length ? { notRunEvidenceIds } : {}),
1252
+ completedAt: new Date().toISOString(), valid: !changed && !signal?.aborted && (options.deadline === undefined || Date.now() <= options.deadline) && results.length === commands.length && results.every((result) => result.exitCode === 0),
1253
+ ...(changed ? { reason: 'Candidate changed during verification' } : results.some((result) => result.exitCode !== 0) ? { reason: 'A verification command failed' } : {}),
1254
+ ...(unverifiedRepositories.length ? { unverifiedRepositories } : {}),
1182
1255
  });
1183
1256
  if (!receipt.valid)
1184
1257
  for (const result of results) {
@@ -1406,7 +1479,7 @@ function evidenceSummary(document) {
1406
1479
  const { stdout: _out, stderr: _err, sources, ...summary } = document;
1407
1480
  return { ...summary, sources: sources.map(({ text: _text, ...source }) => source) };
1408
1481
  }
1409
- function persistCheckEvidence(context, result, planHash, candidateHash) {
1482
+ export function persistCheckEvidence(context, result, planHash, candidateHash) {
1410
1483
  const id = result.evidenceId;
1411
1484
  const planPath = safeChild(pipelineStateDirectory(context), 'verification/plan.json');
1412
1485
  const plan = existsSync(planPath) ? object(readJson(planPath)) : undefined;
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": "5.0",
2
+ "schemaVersion": "5.1",
3
3
  "providers": {
4
4
  "claude": {
5
5
  "cli": {
@@ -13,7 +13,6 @@
13
13
  "initCommand": "init",
14
14
  "workflows": {
15
15
  "implement": "/specrails:implement",
16
- "batch-implement": "/specrails:batch-implement",
17
16
  "retry": "/specrails:retry"
18
17
  }
19
18
  },
@@ -29,7 +28,6 @@
29
28
  "initCommand": "init",
30
29
  "workflows": {
31
30
  "implement": "$implement",
32
- "batch-implement": "$batch-implement",
33
31
  "retry": "$retry"
34
32
  }
35
33
  },
@@ -45,7 +43,6 @@
45
43
  "initCommand": "init",
46
44
  "workflows": {
47
45
  "implement": "/specrails:implement",
48
- "batch-implement": "/specrails:batch-implement",
49
46
  "retry": "/specrails:retry"
50
47
  }
51
48
  },
@@ -85,12 +82,21 @@
85
82
  "<model-id>",
86
83
  "--args",
87
84
  "<arguments>"
85
+ ],
86
+ "renderOnlyArgs": [
87
+ ".kimi-code/specrails/run-skill.mjs",
88
+ "--skill",
89
+ "<skill-id>",
90
+ "--model",
91
+ "<model-id>",
92
+ "--args",
93
+ "<arguments>",
94
+ "--render-only"
88
95
  ]
89
96
  },
90
97
  "initCommand": "init",
91
98
  "workflows": {
92
99
  "implement": "/skill:specrails-implement",
93
- "batch-implement": "/skill:specrails-batch-implement",
94
100
  "retry": "/skill:specrails-retry"
95
101
  }
96
102
  }
@@ -229,9 +235,19 @@
229
235
  "validate",
230
236
  "run",
231
237
  "status",
232
- "resume"
238
+ "resume",
239
+ "prompts",
240
+ "capabilities",
241
+ "evidence",
242
+ "recovery",
243
+ "evaluate",
244
+ "workflows",
245
+ "fork",
246
+ "signal",
247
+ "cancel"
233
248
  ],
234
- "apiOperation": "runtime api emits {type: runtime-api, apiVersion, coreVersion}; does not invoke providers",
249
+ "cliPresentationOperations": ["help"],
250
+ "apiOperation": "runtime api emits {type: runtime-api, apiVersion, coreVersion, runtimeIdentity, workflowVersions, engineVersion, engines, nodeKindsVersion, nodeKinds, capabilities, guardrails}; does not invoke providers",
235
251
  "validationTransport": "runtime validate --stdin accepts UTF-8 configuration JSON up to 2 MiB, mutually exclusive with --config",
236
252
  "compactStatus": "runtime status --compact omits workflow history, events and outputs, frozen pipeline context and verification command output; retains identities (runId, traceId), phase statuses and visits, pendingApproval, pendingQuestion, usage, completion, acceptance summary and verification metadata",
237
253
  "interrupts": "A paused run (exit 2) carries pendingApproval (resume --approve <step>) or pendingQuestion (resume --answer <text>, nonempty, at most 20000 characters); the architect asks only after one autonomous investigation pass unless architect.onLowConfidence is proceed",
@@ -241,7 +257,7 @@
241
257
  "configSchemaPath": "schemas/agent-runtime.schema.json",
242
258
  "configSchemaExport": "specrails-core/schemas/agent-runtime.schema.json",
243
259
  "projectConfigPath": ".specrails/agent-runtime.json",
244
- "minimumNode": "20.19.0",
260
+ "minimumNode": "22.22.3",
245
261
  "platforms": [
246
262
  "darwin",
247
263
  "win32"
@@ -260,6 +276,7 @@
260
276
  "phases": [
261
277
  "architect",
262
278
  "developer",
279
+ "fixer",
263
280
  "verify",
264
281
  "reviewer",
265
282
  "archive"
@@ -277,14 +294,69 @@
277
294
  "specificationFormat": "Architect specs contain complete main-spec replacements, including unchanged requirements; archive does not merge partial delta snippets.",
278
295
  "compatibility": "Additive to integration schema 4 and execution context schema 1. Legacy workflows and profile v1 remain supported. Hosts must check RUNTIME_API_VERSION, not infer runtime support from Core major version.",
279
296
  "documentation": "docs/agent-runtime.md",
280
- "instructionsVersion": "3",
281
- "workflowVersion": "3",
297
+ "instructionsVersion": "10",
298
+ "workflowVersion": "7",
299
+ "engines": [1, 2],
300
+ "engine": {
301
+ "version": 2,
302
+ "definitionSchema": "schemas/workflow-definition.schema.json",
303
+ "nodeKindsVersion": 5
304
+ },
305
+ "nodeKinds": [
306
+ "prompt",
307
+ "role-turn",
308
+ "decider",
309
+ "verify",
310
+ "shell",
311
+ "openspec-validate",
312
+ "openspec-archive",
313
+ "condition",
314
+ "end",
315
+ "approval",
316
+ "question",
317
+ "gate",
318
+ "assign",
319
+ "component",
320
+ "map",
321
+ "implementation",
322
+ "join"
323
+ ],
324
+ "engineStatePath": "<backlogRoot>/.specrails/pipeline/<runId>/agent-workflow/run.sqlite",
325
+ "frozenRequest": "agent-runtime-request.json carries workflow {id, version, source: builtin|definition, definitionHash, engine: 1|2}; resume and status select the engine from workflow.engine and a request without the block is a legacy (engine 1) run",
326
+ "builtins": [
327
+ { "id": "specrails-implementation", "version": "7", "deprecated": false }
328
+ ],
282
329
  "developerTools": "The developer role runs with each CLI's autonomous edit+shell mode (claude --tools default --dangerously-skip-permissions with Agent/Task/Skill disallowed; codex workspace-write; gemini --yolo; kimi print mode). Architect and reviewer stay read-only. Claude roles load project settings only (--setting-sources project,local).",
283
330
  "verificationPolicy": "Configured commands are optional. The architect proposes commands for repositories without one; repositories with no automated check are admitted and recorded in the receipt as unverifiedRepositories. Unchecked tasks return to the developer as feedback instead of failing the run.",
284
331
  "sessionReuse": "Developer correction passes resume the previous provider session (claude --resume, codex exec resume, gemini --resume, kimi --session) with a short correction prompt; an unavailable session falls back to a fresh full turn. Structured roles get one repair turn in-session when their JSON is unusable.",
285
332
  "structuredOutput": "Claude receives --json-schema and Codex --output-schema for architect/reviewer replies; other providers are parsed leniently.",
286
333
  "resumeBudget": "An explicit resume resets the transition ceiling and grants the developer a fresh maxAttempts budget.",
287
- "defaultImplementationEngine": true
334
+ "defaultImplementationEngine": true,
335
+ "openRoles": {
336
+ "version": 1,
337
+ "builtinRoles": [
338
+ "architect",
339
+ "developer",
340
+ "reviewer"
341
+ ],
342
+ "configField": "roles",
343
+ "roleIdPattern": "^[a-z][a-z0-9-]{0,63}$",
344
+ "access": [
345
+ "read",
346
+ "write"
347
+ ],
348
+ "artifacts": [
349
+ "none",
350
+ "tasks-checkboxes",
351
+ "all"
352
+ ],
353
+ "instructions": [
354
+ "role",
355
+ "none"
356
+ ],
357
+ "customAgentLoop": "free",
358
+ "nativeCommandUnsupported": "native_command_unsupported"
359
+ }
288
360
  },
289
361
  "execution": {
290
362
  "schemaVersion": 1,