specrails-core 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-runtime/capabilities.js +3 -1
  3. package/dist/agent-runtime/change-scope.d.ts +63 -0
  4. package/dist/agent-runtime/change-scope.js +297 -0
  5. package/dist/agent-runtime/cli-executor.d.ts +3 -0
  6. package/dist/agent-runtime/cli-executor.js +50 -13
  7. package/dist/agent-runtime/cli-process.js +6 -2
  8. package/dist/agent-runtime/cli.d.ts +15 -0
  9. package/dist/agent-runtime/cli.js +43 -9
  10. package/dist/agent-runtime/compact/chat-client.js +2 -1
  11. package/dist/agent-runtime/compact/environment.d.ts +12 -0
  12. package/dist/agent-runtime/compact/environment.js +31 -1
  13. package/dist/agent-runtime/compact/test-reachability.js +6 -2
  14. package/dist/agent-runtime/config.d.ts +4 -1
  15. package/dist/agent-runtime/config.js +41 -3
  16. package/dist/agent-runtime/core-host.d.ts +10 -4
  17. package/dist/agent-runtime/core-host.js +22 -19
  18. package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
  19. package/dist/agent-runtime/efficiency-summary.js +30 -27
  20. package/dist/agent-runtime/efficiency-types.d.ts +4 -4
  21. package/dist/agent-runtime/efficiency.d.ts +1 -1
  22. package/dist/agent-runtime/efficiency.js +7 -6
  23. package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
  24. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
  25. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
  26. package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
  27. package/dist/agent-runtime/engine/budget.d.ts +9 -0
  28. package/dist/agent-runtime/engine/budget.js +33 -0
  29. package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
  30. package/dist/agent-runtime/engine/canonical-json.js +162 -0
  31. package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
  32. package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
  33. package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
  34. package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
  35. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
  36. package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
  37. package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
  38. package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
  39. package/dist/agent-runtime/engine/cli.d.ts +10 -0
  40. package/dist/agent-runtime/engine/cli.js +172 -0
  41. package/dist/agent-runtime/engine/compiler.d.ts +23 -0
  42. package/dist/agent-runtime/engine/compiler.js +261 -0
  43. package/dist/agent-runtime/engine/composition.d.ts +16 -0
  44. package/dist/agent-runtime/engine/composition.js +28 -0
  45. package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
  46. package/dist/agent-runtime/engine/concurrency.js +55 -0
  47. package/dist/agent-runtime/engine/contracts.d.ts +273 -0
  48. package/dist/agent-runtime/engine/contracts.js +11 -0
  49. package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
  50. package/dist/agent-runtime/engine/definition-schema.js +313 -0
  51. package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
  52. package/dist/agent-runtime/engine/definition-types.js +3 -0
  53. package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
  54. package/dist/agent-runtime/engine/definition-validator.js +187 -0
  55. package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
  56. package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
  57. package/dist/agent-runtime/engine/events.d.ts +19 -0
  58. package/dist/agent-runtime/engine/events.js +88 -0
  59. package/dist/agent-runtime/engine/execution.d.ts +29 -0
  60. package/dist/agent-runtime/engine/execution.js +121 -0
  61. package/dist/agent-runtime/engine/expressions.d.ts +11 -0
  62. package/dist/agent-runtime/engine/expressions.js +172 -0
  63. package/dist/agent-runtime/engine/fork.d.ts +28 -0
  64. package/dist/agent-runtime/engine/fork.js +211 -0
  65. package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
  66. package/dist/agent-runtime/engine/graph-description.js +16 -0
  67. package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
  68. package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
  69. package/dist/agent-runtime/engine/index.d.ts +14 -0
  70. package/dist/agent-runtime/engine/index.js +14 -0
  71. package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
  72. package/dist/agent-runtime/engine/invocation-context.js +128 -0
  73. package/dist/agent-runtime/engine/otel.d.ts +15 -0
  74. package/dist/agent-runtime/engine/otel.js +132 -0
  75. package/dist/agent-runtime/engine/output.d.ts +4 -0
  76. package/dist/agent-runtime/engine/output.js +73 -0
  77. package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
  78. package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
  79. package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
  80. package/dist/agent-runtime/engine/piece-registry.js +62 -0
  81. package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
  82. package/dist/agent-runtime/engine/pieces/assign.js +35 -0
  83. package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
  84. package/dist/agent-runtime/engine/pieces/control.js +49 -0
  85. package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
  86. package/dist/agent-runtime/engine/pieces/decider.js +70 -0
  87. package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
  88. package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
  89. package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
  90. package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
  91. package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
  92. package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
  93. package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
  94. package/dist/agent-runtime/engine/pieces/index.js +22 -0
  95. package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
  96. package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
  97. package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
  98. package/dist/agent-runtime/engine/pieces/ports.js +1 -0
  99. package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
  100. package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
  101. package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
  102. package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
  103. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
  104. package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
  105. package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
  106. package/dist/agent-runtime/engine/pieces/shared.js +40 -0
  107. package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
  108. package/dist/agent-runtime/engine/pieces/shell.js +47 -0
  109. package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
  110. package/dist/agent-runtime/engine/pieces/verify.js +52 -0
  111. package/dist/agent-runtime/engine/preflight.d.ts +27 -0
  112. package/dist/agent-runtime/engine/preflight.js +54 -0
  113. package/dist/agent-runtime/engine/regex.d.ts +3 -0
  114. package/dist/agent-runtime/engine/regex.js +19 -0
  115. package/dist/agent-runtime/engine/run-status.d.ts +563 -0
  116. package/dist/agent-runtime/engine/run-status.js +61 -0
  117. package/dist/agent-runtime/engine/runs.d.ts +605 -0
  118. package/dist/agent-runtime/engine/runs.js +247 -0
  119. package/dist/agent-runtime/engine/state.d.ts +64 -0
  120. package/dist/agent-runtime/engine/state.js +98 -0
  121. package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
  122. package/dist/agent-runtime/engine/steering/inbox.js +113 -0
  123. package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
  124. package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
  125. package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
  126. package/dist/agent-runtime/engine/storage/private-path.js +74 -0
  127. package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
  128. package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
  129. package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
  130. package/dist/agent-runtime/evaluation-corpus.js +4 -0
  131. package/dist/agent-runtime/evaluation.d.ts +9 -3
  132. package/dist/agent-runtime/evaluation.js +50 -8
  133. package/dist/agent-runtime/executor-types.d.ts +53 -3
  134. package/dist/agent-runtime/executor-types.js +35 -2
  135. package/dist/agent-runtime/executors.js +2 -2
  136. package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
  137. package/dist/agent-runtime/graph/artifacts.js +86 -10
  138. package/dist/agent-runtime/graph/convergence.d.ts +26 -0
  139. package/dist/agent-runtime/graph/convergence.js +62 -0
  140. package/dist/agent-runtime/graph/nodes.d.ts +2 -0
  141. package/dist/agent-runtime/graph/nodes.js +171 -24
  142. package/dist/agent-runtime/graph/roles.d.ts +6 -0
  143. package/dist/agent-runtime/graph/roles.js +23 -17
  144. package/dist/agent-runtime/graph/state.d.ts +5 -0
  145. package/dist/agent-runtime/graph/state.js +2 -0
  146. package/dist/agent-runtime/kimi-acp.js +6 -6
  147. package/dist/agent-runtime/openai-executor.js +17 -12
  148. package/dist/agent-runtime/openspec.d.ts +17 -2
  149. package/dist/agent-runtime/openspec.js +54 -20
  150. package/dist/agent-runtime/prompts.d.ts +11 -5
  151. package/dist/agent-runtime/prompts.js +71 -20
  152. package/dist/agent-runtime/recovery.d.ts +1 -1
  153. package/dist/agent-runtime/repository-context.js +20 -1
  154. package/dist/agent-runtime/role-routing.js +4 -2
  155. package/dist/agent-runtime/role-state.d.ts +5 -0
  156. package/dist/agent-runtime/verification-plan.d.ts +2 -0
  157. package/dist/agent-runtime/verification-plan.js +18 -3
  158. package/dist/agent-runtime/workflow-types.d.ts +2 -0
  159. package/dist/agent-runtime/workflow.d.ts +3 -1
  160. package/dist/agent-runtime/workflow.js +7 -3
  161. package/dist/agent-runtime/workspace-tools.d.ts +2 -2
  162. package/dist/agent-runtime/workspace-tools.js +6 -6
  163. package/dist/installer/cli.js +5 -0
  164. package/dist/installer/phases/scaffold.d.ts +2 -2
  165. package/dist/installer/phases/scaffold.js +3 -7
  166. package/dist/pipeline/pipeline-state.d.ts +49 -4
  167. package/dist/pipeline/pipeline-state.js +150 -23
  168. package/integration-contract.json +84 -12
  169. package/package.json +12 -5
  170. package/schemas/agent-runtime.schema.json +93 -1
  171. package/schemas/workflow-definition.schema.json +312 -0
  172. package/templates/kimi/specrails/run-skill.mjs +15 -1
  173. package/templates/commands/specrails/batch-implement.md +0 -32
@@ -0,0 +1,91 @@
1
+ import { Annotation, END, interrupt, isGraphInterrupt, START, StateGraph } from '@langchain/langgraph';
2
+ import { EphemeralValue } from '@langchain/langgraph/channels';
3
+ import { CORE_NODE_ORDER, CoreState } from '../graph/state.js';
4
+ import { EngineError } from './contracts.js';
5
+ import { boundPieceOutput } from './output.js';
6
+ import { json } from './pieces/shared.js';
7
+ function implementationSchema() {
8
+ return Annotation.Root({ ...CoreState.spec,
9
+ $scope: Annotation({ reducer: (_old, value) => value }),
10
+ $outer: Annotation({ reducer: (_old, value) => value }),
11
+ $next: Annotation({ reducer: (_old, value) => value, default: () => 'architect' }),
12
+ $nodeResult: Annotation({ reducer: (_old, value) => value, default: () => null }),
13
+ $commit: new EphemeralValue(false),
14
+ });
15
+ }
16
+ export function implementationInput(parent, frame) {
17
+ return { $outer: structuredClone(parent), $scope: { id: frame.scope.id + '/' + frame.visitId, nodePathPrefix: frame.nodePath,
18
+ ...(frame.scope.branchId ? { branchId: frame.scope.branchId } : {}), ...(frame.scope.limits ? { limits: frame.scope.limits } : {}) } };
19
+ }
20
+ export function implementationContext(state, frame, signal, execution) {
21
+ return { state: structuredClone(state), frame, signal,
22
+ interrupt: request => interrupt(request),
23
+ progress: event => execution.progress({ ...event, payload: {
24
+ ...(event.payload !== null && typeof event.payload === 'object' && !Array.isArray(event.payload) ? event.payload : { payload: event.payload }),
25
+ runId: frame.runId, nodePath: frame.nodePath, scopeId: frame.scope.id,
26
+ ...(frame.scope.branchId ? { branch: frame.scope.branchId } : {}), attemptId: frame.attemptId, attempt: frame.attempt, visit: frame.visit,
27
+ } }), };
28
+ }
29
+ /** Six actual Core nodes use the parent's saver; every state update shares its terminal commit. */
30
+ export function compileImplementationGraph(adapter, execution) {
31
+ const schema = implementationSchema(), graph = new StateGraph(schema);
32
+ for (const id of CORE_NODE_ORDER) {
33
+ const node = adapter.nodes[id], effect = node.effect ?? 'write';
34
+ graph.addNode(id, async (state, config) => {
35
+ const info = config.executionInfo;
36
+ if (!info?.threadId)
37
+ throw new EngineError('execution_identity_missing', 'Implementation child requires a public checkpoint identity');
38
+ const frame = await execution.enter({ nodePath: state.$scope.nodePathPrefix + '/' + id, scope: state.$scope,
39
+ kind: id === 'verify' ? 'verify' : id === 'archive' ? 'openspec-archive' : 'role-turn', effect,
40
+ requiresAI: !['verify', 'archive'].includes(id), acceptsSteering: !['verify', 'archive'].includes(id),
41
+ task: { checkpointThreadId: info.threadId, checkpointId: info.checkpointId, taskCheckpointNs: info.checkpointNs, taskId: info.taskId },
42
+ retry: { maxAttempts: node.maxAttempts ?? 1, retrySafe: node.retrySafe === true },
43
+ });
44
+ let result;
45
+ try {
46
+ result = await execution.execute(frame, effect, async (signal) => {
47
+ const context = implementationContext(state.$outer, frame, signal, execution);
48
+ const response = await adapter.runNode(id, state, context);
49
+ const next = response.status === 'succeeded' ? response.next === undefined ? node.ends[0] ?? null : response.next : null;
50
+ if (next !== null && !CORE_NODE_ORDER.includes(next))
51
+ throw new EngineError('invalid_piece_outcome', `Implementation ${id} returned an unknown successor`);
52
+ const update = response.update === undefined ? {} : json(response.update);
53
+ const childUpdate = { update, next, response: json({ ...response, update: undefined }), journal: json(adapter.snapshot(context)) };
54
+ if (Buffer.byteLength(JSON.stringify(childUpdate)) > 1_750_000)
55
+ throw new EngineError('implementation_snapshot_invalid', 'Implementation child checkpoint exceeds its bounded state budget');
56
+ return { outcome: next ?? (response.status === 'succeeded' ? 'next' : response.status), status: response.status,
57
+ childUpdate,
58
+ ...(response.output === undefined ? {} : { output: boundPieceOutput(json(response.output)) }),
59
+ ...(response.error ? { error: { code: response.status === 'blocked' ? 'implementation_blocked' : 'implementation_failed', message: response.error } } : {}),
60
+ completesRun: false };
61
+ });
62
+ }
63
+ catch (error) {
64
+ if (isGraphInterrupt(error)) {
65
+ await execution.interrupted(frame, error);
66
+ throw error;
67
+ }
68
+ const code = error instanceof EngineError || (error && typeof error === 'object' && 'code' in error) ? String(error.code) : 'implementation_failed';
69
+ if (effect === 'write' && ['aborted', 'timeout', 'idle_timeout', 'lease_lost'].includes(code)) {
70
+ await execution.interrupted(frame, error);
71
+ throw error;
72
+ }
73
+ await execution.failed(frame, error, { retry: false });
74
+ result = { outcome: 'failed', status: 'failed', error: { code, message: error instanceof Error ? error.message : String(error) },
75
+ childUpdate: { update: {}, next: null, response: { status: 'failed', error: error instanceof Error ? error.message : String(error) } }, completesRun: false };
76
+ }
77
+ const marker = execution.terminal(frame, result);
78
+ const saved = marker.result.childUpdate;
79
+ if (!saved)
80
+ throw new EngineError('terminal_mismatch', 'Implementation terminal is missing its child state update');
81
+ return { ...saved.update, $next: saved.next, $nodeResult: saved.response, $commit: marker };
82
+ });
83
+ }
84
+ const edges = graph;
85
+ edges.addEdge(START, adapter.entry);
86
+ const destinations = Object.fromEntries(CORE_NODE_ORDER.map(id => [id, id]));
87
+ destinations.__end__ = END;
88
+ for (const id of CORE_NODE_ORDER)
89
+ edges.addConditionalEdges(id, state => state.$next ?? '__end__', destinations);
90
+ return edges.compile();
91
+ }
@@ -0,0 +1,14 @@
1
+ /** Published definitions and run lifecycle. Infrastructure remains per execution. */
2
+ export { createRun, resumeRun, signalRun, cancelRun, definitionRunDirectory, type RunObservers, type ResumeDefinitionOptions } from './runs.js';
3
+ export { forkRun, type ForkRunOptions } from './fork.js';
4
+ export { statusRun } from './run-status.js';
5
+ export { preflightDefinition, configuredRoles, type DefinitionRunInput, type DefinitionRunRequest } from './preflight.js';
6
+ export { validateWorkflowDefinition } from './definition-validator.js';
7
+ export { workflowDefinitionSchema } from './definition-schema.js';
8
+ export { definitionVersion, canonicalJson } from './canonical-json.js';
9
+ export { validationPieceRegistry } from './pieces/index.js';
10
+ export { PieceRegistry } from './piece-registry.js';
11
+ export { compileWorkflowDefinition } from './compiler.js';
12
+ export { describeDefinition } from './graph-description.js';
13
+ export * from './definition-types.js';
14
+ export * from './contracts.js';
@@ -0,0 +1,14 @@
1
+ /** Published definitions and run lifecycle. Infrastructure remains per execution. */
2
+ export { createRun, resumeRun, signalRun, cancelRun, definitionRunDirectory } from './runs.js';
3
+ export { forkRun } from './fork.js';
4
+ export { statusRun } from './run-status.js';
5
+ export { preflightDefinition, configuredRoles } from './preflight.js';
6
+ export { validateWorkflowDefinition } from './definition-validator.js';
7
+ export { workflowDefinitionSchema } from './definition-schema.js';
8
+ export { definitionVersion, canonicalJson } from './canonical-json.js';
9
+ export { validationPieceRegistry } from './pieces/index.js';
10
+ export { PieceRegistry } from './piece-registry.js';
11
+ export { compileWorkflowDefinition } from './compiler.js';
12
+ export { describeDefinition } from './graph-description.js';
13
+ export * from './definition-types.js';
14
+ export * from './contracts.js';
@@ -0,0 +1,11 @@
1
+ import type { WorkflowState, WorkflowStepContext } from '../workflow-types.js';
2
+ import { type JsonValue, type PieceExecutionContext, type WorkflowBudget } from './contracts.js';
3
+ import type { RunLedger } from './checkpoint/ledger.js';
4
+ import type { WorkflowDefinition, DefinitionNode } from './definition-types.js';
5
+ /** Resolve declared paths without interpreting LangGraph's opaque namespaces. */
6
+ export declare function definitionNodeAt(definition: WorkflowDefinition, nodePath: string): DefinitionNode | undefined;
7
+ export declare function roleAt(definition: WorkflowDefinition, nodePath: string): string;
8
+ export declare function ledgerBudget(ledger: RunLedger): WorkflowBudget;
9
+ export declare function ledgerWorkflowState(ledger: RunLedger, definition: WorkflowDefinition, scopeId?: string): WorkflowState;
10
+ /** Adapter to the existing efficient role invoker; SQLite remains the sole journal. */
11
+ export declare function invocationStepContext(ledger: RunLedger, definition: WorkflowDefinition, context: PieceExecutionContext, input?: JsonValue): WorkflowStepContext;
@@ -0,0 +1,128 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { EngineError } from './contracts.js';
3
+ import { remainingBudget } from './budget.js';
4
+ import { projectDurableEvent, traceIdFor } from './events.js';
5
+ import { contentDigest } from './canonical-json.js';
6
+ import { ControlInbox, renderOperatorSteering } from './steering/inbox.js';
7
+ /** Resolve declared paths without interpreting LangGraph's opaque namespaces. */
8
+ export function definitionNodeAt(definition, nodePath) {
9
+ let nodes = definition.nodes, node;
10
+ for (const segment of nodePath.split('/')) {
11
+ node = nodes[segment];
12
+ if (!node)
13
+ return undefined;
14
+ const reference = node.kind === 'component' ? node.params.ref : node.kind === 'map' ? node.params.body : undefined;
15
+ nodes = typeof reference === 'string' ? definition.components?.[reference]?.nodes ?? {} : {};
16
+ }
17
+ return node;
18
+ }
19
+ export function roleAt(definition, nodePath) {
20
+ const node = definitionNodeAt(definition, nodePath);
21
+ if (node?.kind === 'role-turn' || node?.kind === 'decider')
22
+ return String(node.params.roleId);
23
+ return node?.kind === 'prompt' ? 'prompt' : nodePath.split('/').at(-1);
24
+ }
25
+ export function ledgerBudget(ledger) {
26
+ const row = ledger.db.get('budget', { run_id: ledger.runId });
27
+ return { ...(row.max_cost_usd === null ? {} : { maxCostUsd: Number(row.max_cost_usd) }),
28
+ ...(row.max_tokens === null ? {} : { maxTokens: Number(row.max_tokens) }),
29
+ ...(row.max_duration_ms === null ? {} : { maxDurationMs: Number(row.max_duration_ms) }) };
30
+ }
31
+ export function ledgerWorkflowState(ledger, definition, scopeId = 'root') {
32
+ const row = ledger.run(), snapshot = ledger.scopeSnapshot(scopeId), usage = snapshot.usage;
33
+ const attempts = new Map(ledger.db.sqlite.prepare("SELECT a.*,v.effect FROM attempts a JOIN visits v ON a.visit_id=v.visit_id WHERE a.run_id=? AND (?='*' OR a.scope_id=?)").all(ledger.runId, scopeId, scopeId).map(item => [String(item.attempt_id), item]));
34
+ const invocations = new Map();
35
+ for (const item of ledger.db.sqlite.prepare('SELECT * FROM invocations WHERE run_id=? ORDER BY started_at,ordinal').all(ledger.runId)) {
36
+ const key = String(item.attempt_id), entries = invocations.get(key) ?? [];
37
+ entries.push(item);
38
+ invocations.set(key, entries);
39
+ }
40
+ const history = snapshot.attempts.map(attempt => {
41
+ const details = attempts.get(attempt.frame.attemptId), rows = invocations.get(attempt.frame.attemptId) ?? [];
42
+ const completed = rows.filter(item => item.result_json).map(item => JSON.parse(String(item.result_json)));
43
+ const sum = (key) => rows.some(item => !item.result_json) || completed.some(item => item.usage[key] == null) ? null : completed.reduce((total, item) => total + (item.usage[key] ?? 0), 0);
44
+ return { id: attempt.frame.attemptId, stepId: roleAt(definition, attempt.frame.nodePath), attempt: attempt.frame.attempt, visit: attempt.frame.visit,
45
+ status: attempt.status, startedAt: String(details.started_at),
46
+ ...(details.ended_at ? { completedAt: String(details.ended_at) } : {}),
47
+ ...(attempt.result?.output === undefined ? {} : { output: attempt.result.output }),
48
+ ...(attempt.error ? { error: attempt.error.message } : {}),
49
+ ...(rows.length ? { usage: { costUsd: sum('costUsd'), inputTokens: sum('inputTokens'), outputTokens: sum('outputTokens') } } : {}),
50
+ invocations: completed, pendingInvocations: rows.filter(item => !item.result_json).map(item => ({
51
+ invocationId: String(item.invocation_id), ordinal: Number(item.ordinal), provider: String(item.provider), ...(item.model ? { model: String(item.model) } : {}),
52
+ })), };
53
+ });
54
+ const steps = {};
55
+ for (const attempt of snapshot.attempts) {
56
+ const details = attempts.get(attempt.frame.attemptId);
57
+ steps[attempt.frame.nodePath] = { id: attempt.frame.nodePath, status: attempt.status,
58
+ effect: details.effect, visits: attempt.frame.visit, attempt: attempt.frame.attempt, attemptId: attempt.frame.attemptId,
59
+ startedAt: String(details.started_at), ...(details.ended_at ? { completedAt: String(details.ended_at) } : {}),
60
+ ...(attempt.result?.output === undefined ? {} : { output: attempt.result.output }),
61
+ ...(attempt.result?.childUpdate ? { update: attempt.result.childUpdate } : {}), ...(attempt.error ? { error: attempt.error.message } : {}) };
62
+ }
63
+ const interruptions = ledger.db.sqlite.prepare("SELECT * FROM interrupts WHERE run_id=? AND (?='*' OR scope_id=?) ORDER BY requested_at DESC,interrupt_id DESC").all(ledger.runId, scopeId, scopeId);
64
+ const pending = interruptions.find(item => !item.answered_at) ?? interruptions[0];
65
+ const request = pending ? JSON.parse(String(pending.payload_json)) : undefined;
66
+ const answer = pending?.answer_json ? JSON.parse(String(pending.answer_json)) : undefined;
67
+ const events = ledger.events().filter(event => scopeId === '*' || !event.scopeId || event.scopeId === scopeId).flatMap(event => {
68
+ const projected = projectDurableEvent(event);
69
+ return projected.type === 'workflow-event' ? [projected.event] : [];
70
+ });
71
+ return { schemaVersion: 2, runId: ledger.runId, traceId: traceIdFor(ledger.runId), workflowId: definition.id,
72
+ workflowVersion: definition.version, workflowFingerprint: definition.version, inputFingerprint: contentDigest(JSON.parse(String(row.request_json))),
73
+ status: String(row.status), createdAt: String(row.created_at), updatedAt: String(row.updated_at),
74
+ nextStep: pending && !pending.answered_at ? String(pending.node_path) : [...snapshot.attempts].reverse().find(attempt => ['running', 'interrupted', 'failed'].includes(attempt.status))?.frame.nodePath ?? null,
75
+ nextAttempt: history.length + 1, transitions: snapshot.transitions, executionCount: history.length, steps, history, events, budget: ledgerBudget(ledger),
76
+ ...(pending && request?.kind === 'question' ? { pendingQuestion: { stepId: String(pending.node_path), requestedAt: String(pending.requested_at), question: request.prompt,
77
+ ...(pending.answered_at ? { answeredAt: String(pending.answered_at), answer: typeof answer === 'string' ? answer : answer?.answer } : {}) } } : {}),
78
+ ...(pending && ['approval', 'gate'].includes(request?.kind ?? '') ? { pendingApproval: { stepId: String(pending.node_path), requestedAt: String(pending.requested_at), reason: request.prompt,
79
+ ...(pending.answered_at ? { grantedAt: String(pending.answered_at) } : {}) } } : {}),
80
+ usage: { costUsd: usage.costUsd, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, knownCostUsd: usage.knownCostUsd,
81
+ knownTokens: usage.knownInputTokens + usage.knownOutputTokens, durationMs: ledger.activeDurationMs() }, };
82
+ }
83
+ /** Adapter to the existing efficient role invoker; SQLite remains the sole journal. */
84
+ export function invocationStepContext(ledger, definition, context, input = null) {
85
+ const budget = ledgerBudget(ledger);
86
+ const messages = new ControlInbox(ledger.db, ledger.runId).assigned(context.frame);
87
+ const pendingRow = ledger.db.sqlite.prepare('SELECT payload_json FROM interrupts WHERE run_id=? AND attempt_id=? ORDER BY requested_at DESC,interrupt_id DESC LIMIT 1').get(ledger.runId, context.frame.attemptId);
88
+ const pending = pendingRow ? JSON.parse(String(pendingRow.payload_json)) : undefined;
89
+ let offered;
90
+ const headroom = () => {
91
+ const live = ledger.db.sqlite.prepare('SELECT COUNT(*) count,COALESCE(SUM(max_tokens),0) tokens,COALESCE(SUM(max_cost_usd),0) cost FROM reservations WHERE run_id=?').get(ledger.runId);
92
+ const remaining = remainingBudget(ledger.usage(), budget, ledger.activeDurationMs(), { costUsd: Number(live.cost), tokens: Number(live.tokens) });
93
+ // Each concurrent caller receives a bounded share of unreserved capacity.
94
+ // Actual usage returns the unused share immediately at invocation settlement.
95
+ const parallelism = Math.min(definition.policies?.concurrency ?? 1, context.frame.scope.limits?.[0]?.concurrency ?? definition.policies?.concurrency ?? 1);
96
+ const slots = Math.max(1, parallelism - Number(live.count));
97
+ offered = { ...remaining,
98
+ ...(remaining.maxTokens === undefined ? {} : { maxTokens: Math.floor(remaining.maxTokens / slots) }),
99
+ ...(remaining.maxCostUsd === undefined ? {} : { maxCostUsd: remaining.maxCostUsd / slots }),
100
+ };
101
+ return offered;
102
+ };
103
+ return {
104
+ runId: ledger.runId, stepId: context.frame.nodePath, attemptId: context.frame.attemptId, attempt: context.frame.attempt,
105
+ input, ...(messages.length ? { operatorSteering: renderOperatorSteering(messages) } : {}),
106
+ ...(pending?.kind === 'question' ? { pending: { kind: 'question', question: pending.prompt } } : pending && ['approval', 'gate'].includes(pending.kind) ? { pending: { kind: 'approval', reason: pending.prompt } } : {}),
107
+ signal: context.signal, checkpoint: ledgerWorkflowState(ledger, definition, context.frame.scope.id), remainingBudget: headroom,
108
+ interrupt: request => context.interrupt({ kind: request.kind, prompt: request.kind === 'approval' ? request.reason : request.question,
109
+ nodePath: context.frame.nodePath, scopeId: context.frame.scope.id, attemptId: context.frame.attemptId }),
110
+ // The existing invoker calls this immediately before its finally settlement.
111
+ // Never charge twice: ProviderInvocation is the durable source of usage.
112
+ reportUsage: () => { },
113
+ reportInvocationStarted: async (measurement) => {
114
+ const invocationId = randomUUID(), limits = offered ?? headroom();
115
+ if (limits.maxTokens === 0 || limits.maxCostUsd === 0)
116
+ throw new EngineError('budget_exhausted', 'No unreserved budget remains for a provider invocation');
117
+ const ordinal = ledger.startInvocation(context.frame, { ...measurement, invocationId, role: roleAt(definition, context.frame.nodePath) }, limits);
118
+ offered = undefined;
119
+ return { invocationId, ordinal };
120
+ },
121
+ reportInvocation: async (invocation) => {
122
+ if (!invocation.invocationId)
123
+ throw new EngineError('invocation_mismatch', 'Provider settlement requires its durable invocation ID');
124
+ ledger.settleInvocation(context.frame, { ...invocation, invocationId: invocation.invocationId });
125
+ },
126
+ reportEfficiencyActivity: async (kind, payload) => context.progress({ type: 'runtime-efficiency-event', payload: { kind, ...payload } }),
127
+ };
128
+ }
@@ -0,0 +1,15 @@
1
+ import { type DurableEngineEvent } from './contracts.js';
2
+ export interface EngineTelemetry {
3
+ observe(event: DurableEngineEvent): void;
4
+ flush(): Promise<void>;
5
+ close(): Promise<void>;
6
+ }
7
+ interface TelemetryOptions {
8
+ endpoint?: string;
9
+ onError?: (error: unknown) => void;
10
+ timeoutMs?: number;
11
+ maxQueue?: number;
12
+ }
13
+ /** Explicitly enabled OTLP/HTTP observer. Lifecycle metadata only; never a source of execution truth. */
14
+ export declare function createEngineTelemetry(options?: TelemetryOptions): EngineTelemetry | undefined;
15
+ export {};
@@ -0,0 +1,132 @@
1
+ import { EngineError } from './contracts.js';
2
+ import { spanIdFor, traceIdFor } from './events.js';
3
+ /** Explicitly enabled OTLP/HTTP observer. Lifecycle metadata only; never a source of execution truth. */
4
+ export function createEngineTelemetry(options = {}) {
5
+ const configured = options.endpoint ?? process.env.SPECRAILS_OTEL_ENDPOINT;
6
+ if (!configured)
7
+ return undefined;
8
+ const report = (error) => { try {
9
+ options.onError?.(error);
10
+ }
11
+ catch { /* Optional diagnostics cannot fail a run. */ } };
12
+ let endpoint;
13
+ try {
14
+ endpoint = new URL(configured);
15
+ if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash)
16
+ throw new Error('Telemetry endpoint must be an HTTP(S) URL without credentials, query or fragment');
17
+ endpoint.pathname = endpoint.pathname.replace(/\/$/, '').endsWith('/v1/traces') ? endpoint.pathname.replace(/\/$/, '') : endpoint.pathname.replace(/\/$/, '') + '/v1/traces';
18
+ }
19
+ catch (error) {
20
+ report(error);
21
+ return undefined;
22
+ }
23
+ const maximum = options.maxQueue ?? 512, timeoutMs = options.timeoutMs ?? 3000;
24
+ if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 4096 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000) {
25
+ report(new EngineError('invalid_arguments', 'Telemetry queue or timeout exceeds its bound'));
26
+ return undefined;
27
+ }
28
+ const queue = [];
29
+ // Every observer belongs to one running stream. Cursor storage stays bounded across long executions.
30
+ const cursors = new Map();
31
+ let closed = false, draining, timer, dropped = 0;
32
+ async function send(spans) {
33
+ const response = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(timeoutMs),
34
+ body: JSON.stringify({ resourceSpans: [{ resource: { attributes: [{ key: 'service.name', value: { stringValue: 'specrails-core' } }] }, scopeSpans: [{ scope: { name: 'specrails.agent-engine', version: '2' }, spans }] }] }) });
35
+ if (!response.ok) {
36
+ await response.body?.cancel();
37
+ throw new Error(`Telemetry collector returned HTTP ${response.status}`);
38
+ }
39
+ const reader = response.body?.getReader();
40
+ if (!reader)
41
+ return;
42
+ let bytes = 0;
43
+ const chunks = [];
44
+ try {
45
+ for (;;) {
46
+ const chunk = await reader.read();
47
+ if (chunk.done)
48
+ break;
49
+ bytes += chunk.value.byteLength;
50
+ if (bytes > 64 * 1024)
51
+ throw new Error('Telemetry collector response exceeds 64 KiB');
52
+ chunks.push(chunk.value);
53
+ }
54
+ }
55
+ finally {
56
+ await reader.cancel().catch(() => { });
57
+ reader.releaseLock();
58
+ }
59
+ if (bytes) {
60
+ const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
61
+ if (Number(result.partialSuccess?.rejectedSpans ?? 0) > 0)
62
+ throw new Error('Telemetry collector rejected spans');
63
+ }
64
+ }
65
+ const flush = () => {
66
+ if (timer) {
67
+ clearTimeout(timer);
68
+ timer = undefined;
69
+ }
70
+ if (draining)
71
+ return draining;
72
+ const work = async () => {
73
+ while (queue.length) {
74
+ const batch = queue.splice(0, 64);
75
+ try {
76
+ await send(batch);
77
+ }
78
+ catch (error) {
79
+ report(error);
80
+ }
81
+ }
82
+ if (dropped) {
83
+ report(new Error(`Telemetry queue discarded ${dropped} events`));
84
+ dropped = 0;
85
+ }
86
+ };
87
+ draining = work().finally(() => { draining = undefined; });
88
+ return draining;
89
+ };
90
+ return {
91
+ observe(event) {
92
+ if (closed)
93
+ return;
94
+ try {
95
+ if (!Number.isSafeInteger(event.sequence) || event.sequence < 1)
96
+ throw new Error('Telemetry event sequence is invalid');
97
+ if ((cursors.get(event.runId) ?? 0) >= event.sequence)
98
+ return;
99
+ if (!cursors.has(event.runId) && cursors.size >= 16)
100
+ throw new Error('Telemetry observer exceeds its run bound');
101
+ cursors.set(event.runId, event.sequence);
102
+ if (queue.length >= maximum) {
103
+ dropped++;
104
+ return;
105
+ }
106
+ const millis = Date.parse(event.timestamp);
107
+ if (!Number.isFinite(millis) || millis < 0)
108
+ throw new Error('Telemetry event timestamp is invalid');
109
+ const nano = (BigInt(millis) * 1000000n).toString();
110
+ const attributes = Object.entries({ 'specrails.run.id': event.runId, 'specrails.event.type': event.type,
111
+ ...(event.nodePath ? { 'specrails.node.path': event.nodePath } : {}), ...(event.scopeId ? { 'specrails.scope.id': event.scopeId } : {}),
112
+ ...(event.attemptId ? { 'specrails.attempt.id': event.attemptId } : {}), ...(event.branchId ? { 'specrails.branch.id': event.branchId } : {}),
113
+ }).map(([key, value]) => ({ key, value: { stringValue: value.slice(0, 512) } }));
114
+ // Event spans are points in durable history, so no provider duration is inferred from them.
115
+ queue.push({ traceId: traceIdFor(event.runId), spanId: spanIdFor(event.runId, event.sequence), name: event.type.slice(0, 128), kind: 1,
116
+ startTimeUnixNano: nano, endTimeUnixNano: nano, attributes: [...attributes, { key: 'specrails.event.sequence', value: { intValue: String(event.sequence) } }],
117
+ status: { code: /_(failed|interrupted|blocked)$/.test(event.type) ? 2 : 0 },
118
+ });
119
+ if (queue.length >= 64)
120
+ void flush();
121
+ else if (!timer && !draining) {
122
+ timer = setTimeout(() => { void flush(); }, 1000);
123
+ timer.unref();
124
+ }
125
+ }
126
+ catch (error) {
127
+ report(error);
128
+ }
129
+ }, flush,
130
+ async close() { closed = true; await flush(); },
131
+ };
132
+ }
@@ -0,0 +1,4 @@
1
+ import { type JsonValue } from './contracts.js';
2
+ export declare const MAX_PIECE_OUTPUT_BYTES = 262144;
3
+ /** Preserve JSON shape while bounding untrusted provider/command output before checkpointing. */
4
+ export declare function boundPieceOutput(value: JsonValue, maxBytes?: number): JsonValue;
@@ -0,0 +1,73 @@
1
+ import { EngineError } from './contracts.js';
2
+ export const MAX_PIECE_OUTPUT_BYTES = 262_144;
3
+ const truncation = '[truncated]';
4
+ /** Preserve JSON shape while bounding untrusted provider/command output before checkpointing. */
5
+ export function boundPieceOutput(value, maxBytes = MAX_PIECE_OUTPUT_BYTES) {
6
+ let remaining = maxBytes;
7
+ const active = new Set();
8
+ function string(input) {
9
+ const budget = Math.max(16, remaining - 64);
10
+ const cap = Math.max(0, Math.min(32_000, budget - 16));
11
+ const prefix = (text, length) => text.slice(0, length).replace(/[\uD800-\uDBFF]$/, '');
12
+ let candidate = input.length > cap ? prefix(input, cap) + truncation : input;
13
+ while (Buffer.byteLength(JSON.stringify(candidate)) > budget && candidate.length > truncation.length)
14
+ candidate = prefix(candidate, Math.floor(candidate.length / 2)) + truncation;
15
+ remaining -= Buffer.byteLength(JSON.stringify(candidate));
16
+ return candidate;
17
+ }
18
+ function visit(input, depth) {
19
+ if (depth > 32 || remaining < 40) {
20
+ remaining -= truncation.length + 2;
21
+ return truncation;
22
+ }
23
+ if (typeof input === 'string')
24
+ return string(input);
25
+ if (input === null || typeof input === 'boolean' || typeof input === 'number') {
26
+ if (typeof input === 'number' && !Number.isFinite(input))
27
+ throw new EngineError('invalid_piece_output', 'Piece output must be finite JSON');
28
+ remaining -= String(input).length;
29
+ return input;
30
+ }
31
+ if (typeof input !== 'object' || active.has(input))
32
+ throw new EngineError('invalid_piece_output', 'Piece output must be acyclic JSON');
33
+ active.add(input);
34
+ remaining -= 2;
35
+ let result;
36
+ if (Array.isArray(input)) {
37
+ const entries = [];
38
+ for (const entry of input) {
39
+ if (remaining < 80) {
40
+ entries.push(truncation);
41
+ remaining -= truncation.length + 3;
42
+ break;
43
+ }
44
+ remaining -= 1;
45
+ entries.push(visit(entry, depth + 1));
46
+ }
47
+ result = entries;
48
+ }
49
+ else {
50
+ const entries = Object.create(null);
51
+ for (const key of Object.keys(input)) {
52
+ const overhead = Buffer.byteLength(JSON.stringify(key)) + 2;
53
+ if (remaining < overhead + 80) {
54
+ entries._coreTruncated = true;
55
+ remaining -= 22;
56
+ break;
57
+ }
58
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
59
+ if (!('value' in descriptor))
60
+ throw new EngineError('invalid_piece_output', 'Piece output cannot contain accessors');
61
+ remaining -= overhead;
62
+ entries[key] = visit(descriptor.value, depth + 1);
63
+ }
64
+ result = entries;
65
+ }
66
+ active.delete(input);
67
+ return result;
68
+ }
69
+ const bounded = visit(value, 0);
70
+ if (Buffer.byteLength(JSON.stringify(bounded)) > maxBytes)
71
+ throw new EngineError('invalid_piece_output', 'Piece output exceeds the bounded JSON budget');
72
+ return bounded;
73
+ }
@@ -0,0 +1,50 @@
1
+ import { type PipelineContext, type VerificationEvidencePort } from '../../pipeline/pipeline-state.js';
2
+ import type { RuntimeConfig } from '../executor-types.js';
3
+ import type { ExecutorRegistry } from '../executors.js';
4
+ import { type OpenSpecRoleContext } from '../openspec.js';
5
+ import type { ProviderInvocation } from '../efficiency-types.js';
6
+ import type { RoleStatePort } from '../role-state.js';
7
+ import { type AttemptFrame, type CandidateState, type EngineEffect, type JsonValue, type PieceExecutionContext, type PieceResult } from './contracts.js';
8
+ import type { RunLedger } from './checkpoint/ledger.js';
9
+ import type { WorkflowDefinition } from './definition-types.js';
10
+ import type { PieceDependencies, PieceStatePort } from './pieces/ports.js';
11
+ import type { ProjectMemory, SqliteProjectStore } from './store/sqlite-store.js';
12
+ import { type ImplementationBinding } from './pieces/implementation-binding.js';
13
+ /** Adapters are bound to one frozen project, definition and execution lease. */
14
+ export declare class RunPieceDependencies implements PieceDependencies {
15
+ readonly context: PipelineContext;
16
+ readonly config: RuntimeConfig;
17
+ readonly registry: ExecutorRegistry;
18
+ private readonly definition;
19
+ private readonly ledger;
20
+ private readonly directory;
21
+ private readonly change?;
22
+ private readonly projectMemory?;
23
+ readonly policies: PieceDependencies['policies'];
24
+ constructor(context: PipelineContext, config: RuntimeConfig, registry: ExecutorRegistry, definition: WorkflowDefinition, ledger: RunLedger, directory: string, change?: string | undefined, projectMemory?: SqliteProjectStore | undefined);
25
+ memory(context: PieceExecutionContext): ProjectMemory;
26
+ private implementationBindings;
27
+ bindImplementation(context: PieceExecutionContext, change: string): ImplementationBinding;
28
+ private candidateScope;
29
+ private inheritedCandidateScope;
30
+ private candidate;
31
+ implementationExclusions(_context: PieceExecutionContext): {
32
+ runtimeExclusions: string[];
33
+ repositoryExclusions: Readonly<Record<string, readonly string[]>>;
34
+ };
35
+ fingerprint(): string;
36
+ initializeCandidate(): void;
37
+ executionSnapshot(context: PieceExecutionContext): {
38
+ candidate: CandidateState;
39
+ verified: import("./contracts.js").VerifiedState | null;
40
+ };
41
+ /** Only the execution owner proposes protected candidate/receipt channels. */
42
+ finalize(frame: AttemptFrame, result: PieceResult, effect: EngineEffect): PieceResult;
43
+ memo(context: PieceExecutionContext): PieceStatePort;
44
+ roleState(context: PieceExecutionContext): RoleStatePort;
45
+ stepContext(context: PieceExecutionContext): import("../workflow-types.js").WorkflowStepContext;
46
+ settleResult(context: PieceExecutionContext, key: string, value: JsonValue, invocation: ProviderInvocation): void;
47
+ artifactDirectory(context: PieceExecutionContext): string;
48
+ openspec(context: PieceExecutionContext): Record<string, OpenSpecRoleContext>;
49
+ verification(context: PieceExecutionContext): VerificationEvidencePort;
50
+ }