specrails-core 6.4.0 → 6.6.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 (131) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +2 -2
  2. package/dist/agent-runtime/capabilities.js +22 -5
  3. package/dist/agent-runtime/cli-executor.js +8 -3
  4. package/dist/agent-runtime/cli.d.ts +4 -4
  5. package/dist/agent-runtime/cli.js +50 -3
  6. package/dist/agent-runtime/compact/environment.d.ts +50 -4
  7. package/dist/agent-runtime/compact/environment.js +183 -6
  8. package/dist/agent-runtime/compact/step.d.ts +1 -1
  9. package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
  10. package/dist/agent-runtime/efficiency-types.d.ts +1 -1
  11. package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
  12. package/dist/agent-runtime/engine/contracts.d.ts +2 -1
  13. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  14. package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
  15. package/dist/agent-runtime/engine/fork.d.ts +1 -1
  16. package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
  17. package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
  18. package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
  19. package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
  20. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
  21. package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
  22. package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
  23. package/dist/agent-runtime/engine/pieces/verify.js +46 -13
  24. package/dist/agent-runtime/engine/run-status.d.ts +165 -165
  25. package/dist/agent-runtime/engine/runs.d.ts +168 -168
  26. package/dist/agent-runtime/engine/state.d.ts +16 -16
  27. package/dist/agent-runtime/evaluation.d.ts +3 -3
  28. package/dist/agent-runtime/executor-types.d.ts +10 -10
  29. package/dist/agent-runtime/graph/convergence.d.ts +2 -2
  30. package/dist/agent-runtime/graph/convergence.js +11 -0
  31. package/dist/agent-runtime/graph/nodes.js +35 -4
  32. package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
  33. package/dist/agent-runtime/graph/state.d.ts +6 -2
  34. package/dist/agent-runtime/guardrails.d.ts +1 -1
  35. package/dist/agent-runtime/guardrails.js +4 -1
  36. package/dist/agent-runtime/index.d.ts +3 -2
  37. package/dist/agent-runtime/index.js +1 -0
  38. package/dist/agent-runtime/openai-executor.d.ts +9 -9
  39. package/dist/agent-runtime/openspec.d.ts +4 -4
  40. package/dist/agent-runtime/prompts.d.ts +6 -0
  41. package/dist/agent-runtime/prompts.js +32 -8
  42. package/dist/agent-runtime/recovery.d.ts +12 -12
  43. package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
  44. package/dist/agent-runtime/session/application/active-session.js +701 -0
  45. package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
  46. package/dist/agent-runtime/session/application/delegation.js +171 -0
  47. package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
  48. package/dist/agent-runtime/session/application/output-buffer.js +65 -0
  49. package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
  50. package/dist/agent-runtime/session/application/session-service.js +256 -0
  51. package/dist/agent-runtime/session/application/timers.d.ts +15 -0
  52. package/dist/agent-runtime/session/application/timers.js +35 -0
  53. package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
  54. package/dist/agent-runtime/session/domain/descriptor.js +32 -0
  55. package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
  56. package/dist/agent-runtime/session/domain/errors.js +16 -0
  57. package/dist/agent-runtime/session/domain/events.d.ts +147 -0
  58. package/dist/agent-runtime/session/domain/events.js +24 -0
  59. package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
  60. package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
  61. package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
  62. package/dist/agent-runtime/session/domain/interruption.js +39 -0
  63. package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
  64. package/dist/agent-runtime/session/domain/policy.js +104 -0
  65. package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
  66. package/dist/agent-runtime/session/domain/snapshot.js +234 -0
  67. package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
  68. package/dist/agent-runtime/session/domain/state-machines.js +41 -0
  69. package/dist/agent-runtime/session/domain/types.d.ts +130 -0
  70. package/dist/agent-runtime/session/domain/types.js +15 -0
  71. package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
  72. package/dist/agent-runtime/session/domain/usage.js +86 -0
  73. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
  74. package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
  75. package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
  76. package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
  77. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
  78. package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
  79. package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
  80. package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
  81. package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
  82. package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
  83. package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
  84. package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
  85. package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
  86. package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
  87. package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
  88. package/dist/agent-runtime/session/drivers/common/process.js +116 -0
  89. package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
  90. package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
  91. package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
  92. package/dist/agent-runtime/session/drivers/registry.js +38 -0
  93. package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
  94. package/dist/agent-runtime/session/host/protocol.js +88 -0
  95. package/dist/agent-runtime/session/host/server.d.ts +57 -0
  96. package/dist/agent-runtime/session/host/server.js +229 -0
  97. package/dist/agent-runtime/session/index.d.ts +45 -0
  98. package/dist/agent-runtime/session/index.js +74 -0
  99. package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
  100. package/dist/agent-runtime/session/journal/schema.js +44 -0
  101. package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
  102. package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
  103. package/dist/agent-runtime/session/ports.d.ts +192 -0
  104. package/dist/agent-runtime/session/ports.js +1 -0
  105. package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
  106. package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
  107. package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
  108. package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
  109. package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
  110. package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
  111. package/dist/agent-runtime/session/testing/index.d.ts +9 -0
  112. package/dist/agent-runtime/session/testing/index.js +9 -0
  113. package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
  114. package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
  115. package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
  116. package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
  117. package/dist/agent-runtime/verification-proposals.d.ts +9 -0
  118. package/dist/agent-runtime/verification-proposals.js +69 -0
  119. package/dist/agent-runtime/verification-repair.d.ts +31 -2
  120. package/dist/agent-runtime/verification-repair.js +58 -3
  121. package/dist/installer/commands/init.d.ts +1 -1
  122. package/dist/installer/phases/install-config.js +10 -3
  123. package/dist/installer/util/paths.d.ts +25 -1
  124. package/dist/installer/util/registry.js +3 -3
  125. package/dist/pipeline/pipeline-state.d.ts +42 -11
  126. package/dist/pipeline/pipeline-state.js +47 -8
  127. package/dist/shared/specrails-home.d.ts +21 -0
  128. package/dist/shared/specrails-home.js +36 -0
  129. package/integration-contract.json +42 -1
  130. package/package.json +23 -11
  131. package/schemas/agent-runtime.schema.json +1 -1
@@ -13,9 +13,9 @@ export declare function configuredCapabilities(config: RuntimeConfig, registry:
13
13
  schemaVersion: 1;
14
14
  roles: {
15
15
  transport: string;
16
- continuation: "supported" | "unsupported" | "unknown";
16
+ continuation: 'supported' | 'unsupported' | 'unknown';
17
17
  resumeRequiresFullContext?: boolean;
18
- effortSupport: "supported" | "unsupported" | "unknown";
18
+ effortSupport: 'supported' | 'unsupported' | 'unknown';
19
19
  supportedEfforts: string[] | null;
20
20
  observedModel: boolean;
21
21
  observedEffort: boolean;
@@ -36,6 +36,16 @@ export async function cliCapabilities(provider, model, options = {}) {
36
36
  return { ...result, effortSupport: 'supported', supportedEfforts: levels };
37
37
  }
38
38
  else if (model && help.stdout.includes('--config')) {
39
+ // Ask the installed client for its own model catalog. Other Codex clients
40
+ // (the desktop app's app-server daemon, IDE extensions) share
41
+ // models_cache.json and rewrite it with their own version, so the cache
42
+ // is only a fallback for clients without `codex debug models`.
43
+ const catalog = await runner({ command: provider, args: ['debug', 'models'] }, { cwd: tmpdir(), env, timeoutMs: 10_000 }).catch(() => null);
44
+ if (catalog?.exitCode === 0 && catalog.stdout.length <= 2 * 1024 * 1024) {
45
+ const levels = codexModelEfforts(JSON.parse(catalog.stdout), model);
46
+ if (levels)
47
+ return { ...result, effortSupport: 'supported', supportedEfforts: levels };
48
+ }
39
49
  // Use the installed client's own bounded, fresh model catalog. Do not
40
50
  // infer reasoning support from a model prefix or another provider.
41
51
  const file = path.join(env.CODEX_HOME ?? path.join(env.HOME ?? homedir(), '.codex'), 'models_cache.json');
@@ -48,16 +58,23 @@ export async function cliCapabilities(provider, model, options = {}) {
48
58
  const version = await runner({ command: provider, args: ['--version'] }, { cwd: tmpdir(), env, timeoutMs: 10_000 });
49
59
  if (version.exitCode !== 0 || version.stdout.trim() !== `codex-cli ${cache.client_version}`)
50
60
  return result;
51
- const entry = Array.isArray(cache.models) ? cache.models.find((item) => item?.slug === model) : undefined;
52
- const levels = entry?.supported_reasoning_levels?.map((item) => item?.effort);
53
- if (Array.isArray(levels) && levels.length && levels.every(value => typeof value === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(value))) {
54
- return { ...result, effortSupport: 'supported', supportedEfforts: [...new Set(levels)] };
55
- }
61
+ const levels = codexModelEfforts(cache, model);
62
+ if (levels)
63
+ return { ...result, effortSupport: 'supported', supportedEfforts: levels };
56
64
  }
57
65
  }
58
66
  catch { /* Missing/stale metadata is unknown, never optimistic support. */ }
59
67
  return result;
60
68
  }
69
+ /** Reasoning levels a Codex catalog (`{ models: [...] }`) declares for one model; null when unknown or malformed. */
70
+ function codexModelEfforts(catalog, model) {
71
+ const models = catalog?.models;
72
+ const entry = Array.isArray(models) ? models.find((item) => item?.slug === model) : undefined;
73
+ const levels = entry?.supported_reasoning_levels?.map?.((item) => item?.effort);
74
+ if (!Array.isArray(levels) || !levels.length || !levels.every(value => typeof value === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(value)))
75
+ return null;
76
+ return [...new Set(levels)];
77
+ }
61
78
  export function assertEffortSupported(selection, capability) {
62
79
  if (selection.effort !== undefined && (capability.effortSupport !== 'supported' || !capability.supportedEfforts?.includes(selection.effort))) {
63
80
  throw new AgentExecutionError(`Requested effort '${selection.effort}' is not confirmed for ${capability.transport}. Select a supported effort or provider default.`, 'provider_capability_unsupported');
@@ -32,7 +32,7 @@ export function buildCliInvocation(provider, request, options = {}) {
32
32
  }
33
33
  const readOnly = requestPolicy(request).access === 'read';
34
34
  // Pin the product alias; explicit IDs remain reproducible across releases.
35
- const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5' };
35
+ const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' };
36
36
  const modelId = provider === 'claude' && request.model ? claudeModelIds[request.model] ?? request.model : request.model;
37
37
  const model = modelId ? ['--model', modelId] : [];
38
38
  const extraRoots = request.allowedRoots.filter(root => root !== request.cwd);
@@ -54,14 +54,19 @@ export function buildCliInvocation(provider, request, options = {}) {
54
54
  ...(resume ? ['--resume', resume] : []),
55
55
  ] };
56
56
  case 'codex': {
57
- const sandbox = readOnly ? 'read-only' : 'workspace-write';
57
+ // Writers run without Codex's OS sandbox, like the Claude developer
58
+ // (--dangerously-skip-permissions) and Desktop's legacy rail runs: the
59
+ // workspace-write seatbelt denies what real verification needs (a
60
+ // localhost listen, Chromium's Mach bootstrap registration, the browser
61
+ // cache). Read-only roles keep the read-only sandbox.
62
+ const sandbox = readOnly ? 'read-only' : 'danger-full-access';
58
63
  const common = ['--json', '--skip-git-repo-check', '-c', 'approval_policy="never"', ...model,
59
64
  ...(request.effort === undefined ? [] : ['-c', 'model_reasoning_effort=' + JSON.stringify(request.effort)]),
60
65
  ...(options.openspecBridge ? ['-c', 'mcp_servers.specrails_openspec.command=' + JSON.stringify(options.openspecBridge.command), '-c', 'mcp_servers.specrails_openspec.args=' + JSON.stringify(options.openspecBridge.args), '-c', 'mcp_servers.specrails_openspec.default_tools_approval_mode="approve"', '-c', 'mcp_servers.specrails_openspec.required=true'] : []),
61
66
  ];
62
67
  // `codex exec resume` has no --sandbox flag; the same policy travels as a config override.
63
68
  if (resume)
64
- return { command: 'codex', stdin: request.prompt, args: ['exec', 'resume', ...common, '-c', `sandbox_mode="${sandbox}"`, '-c', 'sandbox_workspace_write.writable_roots=' + JSON.stringify(readOnly ? [] : request.allowedRoots), resume, '-'] };
69
+ return { command: 'codex', stdin: request.prompt, args: ['exec', 'resume', ...common, '-c', `sandbox_mode="${sandbox}"`, ...(readOnly ? ['-c', 'sandbox_workspace_write.writable_roots=[]'] : []), resume, '-'] };
65
70
  return { command: 'codex', stdin: request.prompt, args: [
66
71
  'exec', ...common, '--sandbox', sandbox,
67
72
  ...(options.codexSchemaFile ? ['--output-schema', options.codexSchemaFile] : []),
@@ -4,13 +4,13 @@ export declare function runtimeFailure(error: unknown): {
4
4
  type: string;
5
5
  status: string;
6
6
  error: string | {
7
- details?: import("./workflow-types.js").JsonValue | undefined;
8
7
  code: string;
9
8
  message: string;
9
+ details?: import("./workflow-types.js").JsonValue | undefined;
10
10
  };
11
11
  };
12
12
  /** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
13
- export declare const RUNTIME_CLI_OPERATIONS: readonly ["api", "validate", "run", "status", "resume", "prompts", "capabilities", "evidence", "recovery", "evaluate", "workflows", "fork", "signal", "cancel", "help"];
13
+ export declare const RUNTIME_CLI_OPERATIONS: readonly ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'host', 'help'];
14
14
  /** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
15
15
  export declare function compactState(state: WorkflowState | null): {
16
16
  runId: string;
@@ -42,13 +42,13 @@ export declare function compactState(state: WorkflowState | null): {
42
42
  };
43
43
  steps: {
44
44
  [k: string]: {
45
- status: "pending" | "running" | "interrupted" | import("./workflow-types.js").StepStatus;
45
+ status: "interrupted" | "pending" | "running" | import("./workflow-types.js").StepStatus;
46
46
  visits: number;
47
47
  };
48
48
  };
49
49
  recentFailures: {
50
50
  stepId: string;
51
- status: "running" | "interrupted" | import("./workflow-types.js").StepStatus;
51
+ status: "interrupted" | "running" | import("./workflow-types.js").StepStatus;
52
52
  attempt: number;
53
53
  visit: number;
54
54
  at: string;
@@ -18,14 +18,58 @@ import { configuredCapabilities } from './capabilities.js';
18
18
  import { createExecutorRegistry } from './executors.js';
19
19
  import { runRecovery } from './recovery.js';
20
20
  import { EngineError } from './engine/contracts.js';
21
+ /**
22
+ * Long-lived agent session host (docs/agent-sessions/protocol.md). Loaded lazily
23
+ * so batch operations never initialize session drivers or the journal.
24
+ */
25
+ async function runSessionHost(flags) {
26
+ if (flags.stdio !== true)
27
+ throw new Error('runtime host requires --stdio');
28
+ if (flags.scope !== undefined && typeof flags.scope !== 'string')
29
+ throw new Error('--scope requires a value');
30
+ const [{ createSessionRuntime, GLOBAL_SESSION_SCOPE }, { SessionHost }, { CliExecutor }] = await Promise.all([
31
+ import('./session/index.js'),
32
+ import('./session/host/server.js'),
33
+ import('./cli-executor.js'),
34
+ ]);
35
+ const scope = flags.scope ?? GLOBAL_SESSION_SCOPE;
36
+ let host = null;
37
+ const runtime = await createSessionRuntime({
38
+ scope,
39
+ drivers: {
40
+ executors: [
41
+ { id: 'gemini', displayName: 'Gemini CLI', executor: new CliExecutor('gemini') },
42
+ { id: 'kimi', displayName: 'Kimi Code', executor: new CliExecutor('kimi') },
43
+ ],
44
+ },
45
+ onLeaseLost: () => host?.leaseLost(),
46
+ });
47
+ host = new SessionHost({ service: runtime.service, scope, identity: { ...coreRuntimeIdentity(), coreVersion: CORE_PACKAGE_VERSION }, close: () => runtime.close() }, { input: process.stdin, output: process.stdout });
48
+ const stop = () => { void host?.stop(); };
49
+ process.on('SIGINT', stop);
50
+ process.on('SIGTERM', stop);
51
+ try {
52
+ await host.start();
53
+ return 0;
54
+ }
55
+ finally {
56
+ process.off('SIGINT', stop);
57
+ process.off('SIGTERM', stop);
58
+ // A stopped host must not be kept alive by its still-open stdin pipe.
59
+ process.stdin.destroy();
60
+ }
61
+ }
21
62
  /** Both runtime entry points emit the same machine-readable fatal result. */
22
63
  export function runtimeFailure(error) {
23
64
  return { type: 'runtime-result', status: 'failed', error: error instanceof EngineError
24
65
  ? { code: error.code, message: error.message, ...(error.details === undefined ? {} : { details: error.details }) }
25
- : error instanceof Error ? error.message : String(error) };
66
+ // Session errors keep their code so hosts can react (e.g. `journal_locked` must not be retried).
67
+ : error instanceof Error && error.name === 'SessionError' && typeof error.code === 'string'
68
+ ? { code: error.code, message: error.message }
69
+ : error instanceof Error ? error.message : String(error) };
26
70
  }
27
71
  /** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
28
- export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'help'];
72
+ export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'host', 'help'];
29
73
  function read(file) { return JSON.parse(readFileSync(file, 'utf8')); }
30
74
  async function readStdin() {
31
75
  const chunks = [];
@@ -150,6 +194,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
150
194
  'specrails-core runtime fork --context <json> --from <nodePath> --run-id <newRunId> [--scope-id <scope>] [--visit <number>] [--state <json>]',
151
195
  'specrails-core runtime signal --context <json> --stdin [--request-id <id>]',
152
196
  'specrails-core runtime cancel --context <json> [--request-id <id>]',
197
+ 'specrails-core runtime host --stdio [--scope <projectKey|global>]',
153
198
  ] });
154
199
  return 0;
155
200
  }
@@ -158,9 +203,11 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
158
203
  emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
159
204
  engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
160
205
  capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
161
- engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1 }, guardrails: GUARDRAIL_CATALOG });
206
+ engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1, sessions: 1 }, guardrails: GUARDRAIL_CATALOG });
162
207
  return 0;
163
208
  }
209
+ if (command === 'host')
210
+ return runSessionHost(flags);
164
211
  if (command === 'validate') {
165
212
  if (flags.stdin !== undefined && flags.stdin !== true)
166
213
  throw new Error('--stdin is a boolean flag');
@@ -1,5 +1,5 @@
1
1
  import type { spawnSync } from 'node:child_process';
2
- /** `timeoutMs` overrides the default bound for one plan (a browser download is far larger than a dependency install). */
2
+ /** `timeoutMs` overrides the default bound for one plan (a browser download is far larger than a dependency install). `drift`: installed direct dependencies the install must bring back inside their declared ranges. */
3
3
  export interface EnvironmentInstall {
4
4
  ecosystem: 'node' | 'python' | 'go' | 'rust';
5
5
  root: string;
@@ -7,12 +7,20 @@ export interface EnvironmentInstall {
7
7
  args: string[];
8
8
  timeoutMs?: number;
9
9
  installs?: string;
10
+ drift?: DriftedDependency[];
10
11
  }
11
- /** `precondition`: the install failed for a reason only the host can repair (see hostPreconditionFailure). */
12
+ /** `precondition`: the install failed for a reason only the host can repair (see hostPreconditionFailure). `driftPersists`: drifted packages a successful install left drifted (the lockfile pins them). */
12
13
  export interface InstallOutcome extends EnvironmentInstall {
13
14
  ok: boolean;
14
15
  detail: string;
15
16
  precondition?: string;
17
+ driftPersists?: DriftedDependency[];
18
+ }
19
+ /** An installed direct dependency whose version does not satisfy the range package.json declares. */
20
+ export interface DriftedDependency {
21
+ name: string;
22
+ declared: string;
23
+ installed: string;
16
24
  }
17
25
  export type HostPreconditionKind = 'network' | 'credential' | 'environment-variable' | 'toolchain' | 'setup' | 'environment';
18
26
  export interface HostPrecondition {
@@ -44,10 +52,20 @@ export declare function hostPrecondition(output: string): HostPrecondition | und
44
52
  export declare function suggestedPackages(output: string): string[];
45
53
  /** Dependencies package.json declares that node_modules does not hold (bounded). */
46
54
  export declare function missingNodeDependencies(root: string): string[];
55
+ /**
56
+ * Installed direct dependencies whose version fails the range package.json
57
+ * declares (bounded, never throws): a tree installed for other inputs, such as
58
+ * a warm link from a base checkout or a manifest edited after the install,
59
+ * passes `missingNodeDependencies` and then fails lint on an untouched file.
60
+ * Absent packages are `missingNodeDependencies`' to report.
61
+ */
62
+ export declare function driftedNodeDependencies(root: string): DriftedDependency[];
63
+ /** `@busuu/experiments 5.23.0 does not satisfy ^5.24.0, …` for up to five packages. */
64
+ export declare function describeDrift(drift: readonly DriftedDependency[]): string;
47
65
  /** The installs a repository root needs, judged from its manifests and the absence of their install output. */
48
66
  export declare function plannedInstalls(root: string, failureOutput?: string): EnvironmentInstall[];
49
67
  /** Runs each planned install once, bounded; never throws. */
50
- export declare function installEnvironment(roots: readonly string[], io?: {
68
+ export interface InstallIo {
51
69
  spawn?: typeof spawnSync;
52
70
  timeoutMs?: number;
53
71
  failureOutput?: string;
@@ -58,7 +76,35 @@ export declare function installEnvironment(roots: readonly string[], io?: {
58
76
  detail?: string;
59
77
  text?: string;
60
78
  }) => void;
61
- }): InstallOutcome[];
79
+ /** The installs for one root; the failure-driven plan by default. */
80
+ plan?: (root: string) => EnvironmentInstall[];
81
+ }
82
+ export declare function installEnvironment(roots: readonly string[], io?: InstallIo): InstallOutcome[];
83
+ type PlaywrightBrowser = 'chromium' | 'firefox' | 'webkit';
84
+ /** Browsers the repository's Playwright config runs (by project names, devices or browserName); chromium when it names none. */
85
+ export declare function configuredPlaywrightBrowsers(root: string): PlaywrightBrowser[];
86
+ /**
87
+ * The configured browsers when any build the installed Playwright pins is
88
+ * missing from the host cache. `install --dry-run` names the exact install
89
+ * locations without downloading; an unreadable answer means "unknown", never
90
+ * an install.
91
+ */
92
+ export declare function missingPlaywrightBrowsers(root: string, spawn?: typeof spawnSync): PlaywrightBrowser[];
93
+ /**
94
+ * Host preparation before an agent edits a repository: the dependencies its
95
+ * manifest declares but are not installed, then the browser builds its
96
+ * Playwright pins. Agents run sandboxed (Codex cannot write the browser cache
97
+ * or reach it at all), so what they need must already be there. Only cheap,
98
+ * idempotent, project-local work happens here; other ecosystems keep their
99
+ * failure-driven repair at verification.
100
+ */
101
+ export declare function prepareEnvironment(roots: readonly string[], io?: Omit<InstallIo, 'plan' | 'failureOutput'>): InstallOutcome[];
102
+ /**
103
+ * A Node preload that answers IPv6 lookups with nothing when the host has an
104
+ * IPv4 address, so dual-stack clients connect over IPv4. Written once to the
105
+ * temp directory; undefined when it cannot be written.
106
+ */
107
+ export declare function ipv4FirstPreload(): string | undefined;
62
108
  /** The repository's own test command, judged from its manifest (npm/pnpm/yarn `test` script, pytest, go, cargo); undefined when none. */
63
109
  export declare function detectCheckCommand(root: string): {
64
110
  command: string;
@@ -1,6 +1,8 @@
1
1
  import crossSpawn from 'cross-spawn';
2
2
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
3
4
  import path from 'node:path';
5
+ import semver from 'semver';
4
6
  // npm/pnpm/yarn are `.cmd` shims on Windows: a bare `spawnSync('npm')` is
5
7
  // ENOENT there, which read as an environment failure of its own and sent the
6
8
  // install/check round in circles. cross-spawn resolves the shim on every OS.
@@ -132,6 +134,56 @@ export function missingNodeDependencies(root) {
132
134
  }
133
135
  return [...declared].filter(name => !existsSync(path.join(root, 'node_modules', name))).slice(0, 20);
134
136
  }
137
+ // Specifiers with no installed-version contract to check locally; reporting
138
+ // them would plan an install on every run.
139
+ const NON_SEMVER_SPECIFIER = /^(?:workspace:|file:|link:|portal:|patch:|npm:|catalog:|git[+:]|github:|gitlab:|bitbucket:|https?:|ssh:)|\//;
140
+ /**
141
+ * Installed direct dependencies whose version fails the range package.json
142
+ * declares (bounded, never throws): a tree installed for other inputs, such as
143
+ * a warm link from a base checkout or a manifest edited after the install,
144
+ * passes `missingNodeDependencies` and then fails lint on an untouched file.
145
+ * Absent packages are `missingNodeDependencies`' to report.
146
+ */
147
+ export function driftedNodeDependencies(root) {
148
+ let manifest;
149
+ try {
150
+ manifest = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
151
+ }
152
+ catch {
153
+ return [];
154
+ }
155
+ const drifted = new Map();
156
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
157
+ const block = manifest[field];
158
+ if (!block || typeof block !== 'object' || Array.isArray(block))
159
+ continue;
160
+ for (const [name, declared] of Object.entries(block)) {
161
+ if (drifted.has(name) || typeof declared !== 'string' || NON_SEMVER_SPECIFIER.test(declared.trim()))
162
+ continue;
163
+ const range = semver.validRange(declared);
164
+ if (!range)
165
+ continue;
166
+ let installed;
167
+ try {
168
+ installed = JSON.parse(readFileSync(path.join(root, 'node_modules', name, 'package.json'), 'utf8')).version;
169
+ }
170
+ catch {
171
+ continue;
172
+ }
173
+ if (typeof installed !== 'string' || !semver.valid(installed))
174
+ continue;
175
+ if (!semver.satisfies(installed, range, { includePrerelease: false }))
176
+ drifted.set(name, { name, declared, installed });
177
+ if (drifted.size >= 20)
178
+ return [...drifted.values()];
179
+ }
180
+ }
181
+ return [...drifted.values()];
182
+ }
183
+ /** `@busuu/experiments 5.23.0 does not satisfy ^5.24.0, …` for up to five packages. */
184
+ export function describeDrift(drift) {
185
+ return drift.slice(0, 5).map(item => `${item.name} ${item.installed} does not satisfy ${item.declared}`).join(', ') + (drift.length > 5 ? ` and ${drift.length - 5} more` : '');
186
+ }
135
187
  /** Browser builds are ~150 MB each; the default 5-minute install bound is too short on a slow link. */
136
188
  const PLAYWRIGHT_INSTALL_TIMEOUT_MS = 10 * 60_000;
137
189
  /** True when package.json declares `@playwright/test` or `playwright`; only those repositories get a browser install plan. */
@@ -160,9 +212,12 @@ export function plannedInstalls(root, failureOutput = '') {
160
212
  // A manifest edited after the first install (a later task group adds
161
213
  // jest-extended) leaves node_modules present but incomplete; verify then
162
214
  // fails on the environment, not the code. Declared-but-absent ⇒ reinstall.
163
- if (has('package.json') && (!has('node_modules') || missingNodeDependencies(root).length)) {
215
+ // A version outside its declared range (a stale warm-linked tree) is the
216
+ // same kind of gap: the same install brings it back in range.
217
+ const drift = has('package.json') && has('node_modules') ? driftedNodeDependencies(root) : [];
218
+ if (has('package.json') && (!has('node_modules') || missingNodeDependencies(root).length || drift.length)) {
164
219
  const runner = has('pnpm-lock.yaml') ? 'pnpm' : has('yarn.lock') ? 'yarn' : 'npm';
165
- plans.push({ ecosystem: 'node', root, command: runner, args: runner === 'npm' ? ['install', '--no-audit', '--no-fund', '--loglevel=error'] : ['install'] });
220
+ plans.push({ ecosystem: 'node', root, command: runner, args: runner === 'npm' ? ['install', '--no-audit', '--no-fund', '--loglevel=error'] : ['install'], ...(drift.length ? { drift } : {}) });
166
221
  }
167
222
  // After the dependency plan: a fresh worktree needs node_modules before
168
223
  // `playwright install` can run at all.
@@ -208,12 +263,11 @@ function relaxManifestPin(root, name, version) {
208
263
  writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
209
264
  return changed;
210
265
  }
211
- /** Runs each planned install once, bounded; never throws. */
212
266
  export function installEnvironment(roots, io = {}) {
213
267
  const spawn = io.spawn ?? defaultSpawn;
214
268
  const outcomes = [];
215
269
  for (const root of roots) {
216
- for (const plan of plannedInstalls(root, io.failureOutput ?? '')) {
270
+ for (const plan of io.plan ? io.plan(root) : plannedInstalls(root, io.failureOutput ?? '')) {
217
271
  io.onEvent?.({ kind: 'tool-start', tool: plan.command, detail: `${plan.args.join(' ')} (${path.basename(root)})` });
218
272
  const run = () => {
219
273
  try {
@@ -245,16 +299,139 @@ export function installEnvironment(roots, io = {}) {
245
299
  io.onEvent?.({ kind: 'text', text: `Environment: ${unpublished[1]}@${unpublished[2]} is not published; relaxed the pin in package.json and retrying ${plan.command} ${plan.args.join(' ')}` });
246
300
  result = run();
247
301
  }
302
+ // Playwright's downloader tries IPv6 first; on a host whose DNS returns
303
+ // an AAAA record without an IPv6 route, Node's family auto-selection
304
+ // times the request out before IPv4 is tried. Retry once preferring IPv4.
305
+ if (result.status !== 0 && plan.installs?.startsWith('Playwright') && NETWORK_TIMEOUT.test(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)) {
306
+ const preload = ipv4FirstPreload();
307
+ if (preload) {
308
+ io.onEvent?.({ kind: 'text', text: `Environment: the browser download timed out; retrying ${plan.command} ${plan.args.join(' ')} over IPv4` });
309
+ try {
310
+ result = spawn(plan.command, plan.args, { cwd: root, encoding: 'utf8', timeout: plan.timeoutMs ?? io.timeoutMs ?? 5 * 60_000, windowsHide: true, env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --require ${JSON.stringify(preload)}`.trim() } });
311
+ }
312
+ catch (error) {
313
+ result = { status: null, error: error, stdout: '', stderr: '', pid: 0, output: [], signal: null };
314
+ }
315
+ }
316
+ }
248
317
  io.onEvent?.({ kind: 'tool-end', tool: plan.command });
249
318
  const ok = result.status === 0;
250
- const detail = ok ? `installed ${plan.installs ?? `${plan.ecosystem} dependencies`} in ${path.basename(root)}` : `${plan.command} ${plan.args.join(' ')} failed in ${path.basename(root)}: ${String(result.stderr || result.error?.message || `exit ${result.status}`).trim().slice(0, 400)}`;
319
+ const detail = ok ? `installed ${plan.installs ?? `${plan.ecosystem} dependencies`} in ${path.basename(root)}${plan.drift?.length ? ` (${describeDrift(plan.drift)})` : ''}` : `${plan.command} ${plan.args.join(' ')} failed in ${path.basename(root)}: ${String(result.stderr || result.error?.message || `exit ${result.status}`).trim().slice(0, 400)}`;
251
320
  io.onEvent?.({ kind: 'text', text: `Environment: ${detail}` });
321
+ // An install that leaves the drift in place means the lockfile itself
322
+ // resolves the old version: a code change, never a second install.
323
+ const persisting = ok && plan.drift?.length ? driftedNodeDependencies(root).filter(item => plan.drift.some(before => before.name === item.name)) : [];
324
+ if (persisting.length)
325
+ io.onEvent?.({ kind: 'text', text: `Environment: ${describeDrift(persisting)} after installing in ${path.basename(root)}; the lockfile still resolves the drifted version, so update the lockfile or the declared range.` });
252
326
  const precondition = ok ? undefined : hostPreconditionFailure(`${result.stdout ?? ''}\n${result.stderr ?? ''}`);
253
- outcomes.push({ ...plan, ok, detail, ...(precondition ? { precondition } : {}) });
327
+ outcomes.push({ ...plan, ok, detail, ...(precondition ? { precondition } : {}), ...(persisting.length ? { driftPersists: persisting } : {}) });
254
328
  }
255
329
  }
256
330
  return outcomes;
257
331
  }
332
+ const PLAYWRIGHT_CONFIGS = ['playwright.config.ts', 'playwright.config.mts', 'playwright.config.cts', 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.cjs'];
333
+ /** Browsers the repository's Playwright config runs (by project names, devices or browserName); chromium when it names none. */
334
+ export function configuredPlaywrightBrowsers(root) {
335
+ const file = PLAYWRIGHT_CONFIGS.map(name => path.join(root, name)).find(candidate => existsSync(candidate));
336
+ let source = '';
337
+ try {
338
+ if (file)
339
+ source = readFileSync(file, 'utf8').slice(0, 200_000);
340
+ }
341
+ catch { /* unreadable config: the default browser */ }
342
+ const browsers = [];
343
+ if (/\b(?:chromium|chrome|edge|pixel|galaxy)\b/i.test(source))
344
+ browsers.push('chromium');
345
+ if (/\bfirefox\b/i.test(source))
346
+ browsers.push('firefox');
347
+ if (/\b(?:webkit|safari|iphone|ipad)\b/i.test(source))
348
+ browsers.push('webkit');
349
+ return browsers.length ? browsers : ['chromium'];
350
+ }
351
+ /** How to run the repository's own Playwright CLI; undefined until its package is installed (never downloads one). */
352
+ function playwrightCli(root) {
353
+ const bin = path.join(root, 'node_modules', '.bin');
354
+ if (!['playwright', 'playwright.cmd'].some(name => existsSync(path.join(bin, name))))
355
+ return undefined;
356
+ if (existsSync(path.join(root, 'pnpm-lock.yaml')))
357
+ return { command: 'pnpm', args: ['exec', 'playwright'] };
358
+ if (existsSync(path.join(root, 'yarn.lock')))
359
+ return { command: 'yarn', args: ['playwright'] };
360
+ return { command: 'npx', args: ['--no', 'playwright'] };
361
+ }
362
+ /**
363
+ * The configured browsers when any build the installed Playwright pins is
364
+ * missing from the host cache. `install --dry-run` names the exact install
365
+ * locations without downloading; an unreadable answer means "unknown", never
366
+ * an install.
367
+ */
368
+ export function missingPlaywrightBrowsers(root, spawn = defaultSpawn) {
369
+ if (!declaresPlaywright(root))
370
+ return [];
371
+ const cli = playwrightCli(root);
372
+ if (!cli)
373
+ return [];
374
+ const browsers = configuredPlaywrightBrowsers(root);
375
+ let result;
376
+ try {
377
+ result = spawn(cli.command, [...cli.args, 'install', '--dry-run', ...browsers], { cwd: root, encoding: 'utf8', timeout: 30_000, windowsHide: true, env: process.env });
378
+ }
379
+ catch {
380
+ return [];
381
+ }
382
+ if (result.status !== 0)
383
+ return [];
384
+ const locations = [...String(result.stdout ?? '').matchAll(/Install location:\s+(.+?)\s*$/gm)].map(match => match[1]);
385
+ return locations.length && locations.some(location => !existsSync(location)) ? browsers : [];
386
+ }
387
+ /**
388
+ * Host preparation before an agent edits a repository: the dependencies its
389
+ * manifest declares but are not installed, then the browser builds its
390
+ * Playwright pins. Agents run sandboxed (Codex cannot write the browser cache
391
+ * or reach it at all), so what they need must already be there. Only cheap,
392
+ * idempotent, project-local work happens here; other ecosystems keep their
393
+ * failure-driven repair at verification.
394
+ */
395
+ export function prepareEnvironment(roots, io = {}) {
396
+ const spawn = io.spawn ?? defaultSpawn;
397
+ const dependencies = installEnvironment(roots, { ...io, plan: root => plannedInstalls(root).filter(plan => plan.ecosystem === 'node') });
398
+ const browsers = installEnvironment(roots, { ...io, plan: root => {
399
+ const missing = missingPlaywrightBrowsers(root, spawn);
400
+ const cli = missing.length ? playwrightCli(root) : undefined;
401
+ return cli ? [{ ecosystem: 'node', root, command: cli.command, args: [...cli.args, 'install', ...missing], timeoutMs: PLAYWRIGHT_INSTALL_TIMEOUT_MS, installs: `Playwright ${missing.join(', ')}` }] : [];
402
+ } });
403
+ return [...dependencies, ...browsers];
404
+ }
405
+ const NETWORK_TIMEOUT = /timed out after \d+ ?ms|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH/i;
406
+ /**
407
+ * A Node preload that answers IPv6 lookups with nothing when the host has an
408
+ * IPv4 address, so dual-stack clients connect over IPv4. Written once to the
409
+ * temp directory; undefined when it cannot be written.
410
+ */
411
+ export function ipv4FirstPreload() {
412
+ const file = path.join(tmpdir(), 'specrails-ipv4-first-v1.cjs');
413
+ if (existsSync(file))
414
+ return file;
415
+ try {
416
+ writeFileSync(file, [
417
+ "const dns = require('node:dns')",
418
+ 'const lookup = dns.promises.lookup.bind(dns.promises)',
419
+ 'dns.promises.lookup = async (hostname, options) => {',
420
+ " const family = typeof options === 'number' ? options : options && options.family",
421
+ ' if (family === 6) {',
422
+ ' const v4 = await lookup(hostname, { all: true, family: 4 }).catch(() => [])',
423
+ " if (v4.length) { if (options && typeof options === 'object' && options.all) return []; throw Object.assign(new Error('IPv6 skipped: ' + hostname), { code: 'ENOTFOUND' }) }",
424
+ ' }',
425
+ ' return lookup(hostname, options)',
426
+ '}',
427
+ '',
428
+ ].join('\n'));
429
+ return file;
430
+ }
431
+ catch {
432
+ return undefined;
433
+ }
434
+ }
258
435
  /** The repository's own test command, judged from its manifest (npm/pnpm/yarn `test` script, pytest, go, cargo); undefined when none. */
259
436
  export function detectCheckCommand(root) {
260
437
  const has = (file) => existsSync(path.join(root, file));
@@ -40,7 +40,7 @@ export declare function openspecCall(env: CompactEnv, input: Parameters<OpenSpec
40
40
  export declare const PLANNING_CONTROLS: {
41
41
  readonly temperature: 0.2;
42
42
  readonly maxOutputTokens: 16384;
43
- readonly reasoningEffort: "high";
43
+ readonly reasoningEffort: 'high';
44
44
  };
45
45
  export declare function structuredStep(env: CompactEnv, name: string, system: string, user: string, schema: Record<string, unknown>, validate?: (value: Record<string, unknown>) => string | undefined, controls?: {
46
46
  temperature?: number;
@@ -25,7 +25,7 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
25
25
  workflowVersion: string;
26
26
  technicalAcceptance: string;
27
27
  archive: import("../pipeline/pipeline-state.js").PhaseStatus;
28
- delivery: "pending" | "complete" | "pending-host";
28
+ delivery: "complete" | "pending" | "pending-host";
29
29
  roles: {
30
30
  role: string;
31
31
  provider: string | null;
@@ -28,7 +28,7 @@ export interface ProviderInvocation {
28
28
  costUsd: number | null;
29
29
  };
30
30
  }
31
- export declare const CACHE_TOKEN_KEYS: readonly ["uncachedInputTokens", "cacheReadInputTokens", "cacheWriteInputTokens"];
31
+ export declare const CACHE_TOKEN_KEYS: readonly ['uncachedInputTokens', 'cacheReadInputTokens', 'cacheWriteInputTokens'];
32
32
  export declare function sumCacheUsage(values: CacheTokenUsage[]): CacheTokenUsage;
33
33
  export interface EfficiencyTotals {
34
34
  attempts: number;
@@ -5,21 +5,21 @@ export type DatabaseRow = Record<string, SQLOutputValue>;
5
5
  export type WriteRow = Record<string, SQLInputValue>;
6
6
  /** Every identifier interpolated into SQL comes from this closed adapter-owned map. */
7
7
  declare const keys: {
8
- readonly runs: readonly ["run_id"];
9
- readonly visits: readonly ["visit_id"];
10
- readonly steps: readonly ["run_id", "node_path", "scope_id"];
11
- readonly attempts: readonly ["attempt_id"];
12
- readonly invocations: readonly ["invocation_id"];
13
- readonly receipts: readonly ["receipt_id"];
14
- readonly budget: readonly ["run_id"];
15
- readonly interrupts: readonly ["interrupt_id"];
16
- readonly control_inbox: readonly ["request_id"];
17
- readonly role_sessions: readonly ["session_key"];
18
- readonly piece_state: readonly ["run_id", "scope_id", "node_path", "key"];
19
- readonly reservations: readonly ["reservation_id"];
20
- readonly events: readonly ["run_id", "sequence"];
21
- readonly checkpoints: readonly ["thread_id", "checkpoint_ns", "checkpoint_id"];
22
- readonly writes: readonly ["thread_id", "checkpoint_ns", "checkpoint_id", "task_id", "idx"];
8
+ readonly runs: readonly ['run_id'];
9
+ readonly visits: readonly ['visit_id'];
10
+ readonly steps: readonly ['run_id', 'node_path', 'scope_id'];
11
+ readonly attempts: readonly ['attempt_id'];
12
+ readonly invocations: readonly ['invocation_id'];
13
+ readonly receipts: readonly ['receipt_id'];
14
+ readonly budget: readonly ['run_id'];
15
+ readonly interrupts: readonly ['interrupt_id'];
16
+ readonly control_inbox: readonly ['request_id'];
17
+ readonly role_sessions: readonly ['session_key'];
18
+ readonly piece_state: readonly ['run_id', 'scope_id', 'node_path', 'key'];
19
+ readonly reservations: readonly ['reservation_id'];
20
+ readonly events: readonly ['run_id', 'sequence'];
21
+ readonly checkpoints: readonly ['thread_id', 'checkpoint_ns', 'checkpoint_id'];
22
+ readonly writes: readonly ['thread_id', 'checkpoint_ns', 'checkpoint_id', 'task_id', 'idx'];
23
23
  };
24
24
  export type RevisionTable = keyof typeof keys;
25
25
  /** Short synchronous transactions own graph writes and ledger evidence together. */
@@ -103,7 +103,8 @@ export interface ReceiptEvidence {
103
103
  * is reserved for a correction role that diagnoses the cause outside the change.
104
104
  */
105
105
  export interface HostBlocker {
106
- kind: 'network' | 'credential' | 'environment-variable' | 'toolchain' | 'setup' | 'environment' | 'scope';
106
+ /** `nondeterministic-output`: verification rewrites candidate files differently on every run (error code `verification_nondeterministic_output`). */
107
+ kind: 'network' | 'credential' | 'environment-variable' | 'toolchain' | 'setup' | 'environment' | 'scope' | 'nondeterministic-output';
107
108
  reason: string;
108
109
  command: string;
109
110
  args: string[];
@@ -1,5 +1,5 @@
1
1
  import type { EngineEffect, JsonObject, WorkflowBudget } from './contracts.js';
2
- export declare const PIECE_KINDS: readonly ["prompt", "role-turn", "decider", "condition", "assign", "verify", "shell", "artifact-contract", "openspec-validate", "openspec-archive", "approval", "question", "gate", "map", "join", "component", "implementation", "implementation-step", "end"];
2
+ export declare const PIECE_KINDS: readonly ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell', 'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join', 'component', 'implementation', 'implementation-step', 'end'];
3
3
  export type PieceKind = typeof PIECE_KINDS[number];
4
4
  export interface DefinitionNode {
5
5
  kind: PieceKind;