chati-dev 4.5.27 → 4.5.29

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 (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +137 -20
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -1,17 +1,34 @@
1
1
  import {
2
- closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, mkdirSync, openSync,
3
- readSync, unlinkSync, writeSync, realpathSync,
2
+ closeSync, constants, existsSync, fstatSync, fsyncSync, openSync,
3
+ readSync, writeSync, realpathSync,
4
4
  } from 'node:fs';
5
5
  import { basename, dirname, join, parse, resolve, sep } from 'node:path';
6
6
  import { randomUUID } from 'node:crypto';
7
- import { sha256 } from '@chati/core';
8
- import { adjudicationAttemptState, adjudicationEntriesAgree, RailEngine } from '@chati/rail';
9
- import { validateSealedHandoff } from '@chati/planning';
7
+ import {
8
+ canonicalSerialize, canonicalize, directoryPathSync, linkAtSync, mkdirAtSync,
9
+ openAtSync, sha256, unlinkAtSync,
10
+ } from '@chati/core';
11
+ import { adjudicationAttemptState, adjudicationEntriesAgree, attemptExecutionBase, attemptJournalRevision, RailEngine } from '@chati/rail';
12
+ import { validatePlanningMaterial, validateSealedHandoff } from '@chati/planning';
10
13
  import { validateAdjudicationDecision } from '@chati/review-council';
11
- import { loadRuntimeInstallationV2, resolveRuntimeRailInvocationV2 } from './runtime-installation-v2.js';
14
+ import {
15
+ loadRuntimeInstallationV2,
16
+ resolveRuntimeCouncilGatePolicyV2,
17
+ resolveRuntimeRailInvocationV2,
18
+ } from './runtime-installation-v2.js';
19
+ import { assertCouncilGateApproved } from './council-gate-runtime.js';
20
+ import {
21
+ persistImmutableInstallationFile,
22
+ readSafeInstallationStorageFile,
23
+ } from '../installer-v2/catalog-client.js';
24
+ import { signHostAuthorityPayload, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
12
25
  import { createProjectBrowserSession, launchProjectBrowserSession } from './browser-runtime.js';
13
26
  import { enqueueClickUpProjectionFromState } from './clickup-projection.js';
14
27
  import { verifyTerminalAuthorityReceipt } from '../terminal/spawner.js';
28
+ import { captureRailWorktreeCheckpoint, readRailWorktreeCheckpoint } from '../terminal/rail-worktree-checkpoint.js';
29
+ import { restoreRailWorktreeCheckpoint, readRailWorktreeRestoration } from '../terminal/rail-worktree-restoration.js';
30
+ import { verifyHumanEffortReceipt } from './rail-human-effort-authority.js';
31
+ import { assertLocalBrowserArtifacts } from './browser-artifact-store.js';
15
32
  import { buildRailAdjudicationPrompt, buildRailReworkPrompt, buildRailTaskPrompt } from '../terminal/rail-prompts.js';
16
33
  import { readSessionSafe } from './session-manager.js';
17
34
  import { assertRailReadOnlyCommitWorkspaceUnchanged } from '../terminal/rail-readonly-workspace.js';
@@ -22,7 +39,9 @@ import {
22
39
  } from '@chati/tracking-clickup';
23
40
 
24
41
  export const RAIL_V2_DIR = '.chati/v2/rail';
42
+ export const RAIL_COUNCIL_BINDING_AUTHORITY_DOMAIN = 'chati.rail-execution-design-binding.v1';
25
43
  const HANDOFF_MAX_BYTES = 4 * 1024 * 1024;
44
+ const EXECUTION_DESIGN_TASK_GRAPH_PATH = '.chati/v2/planning/tasks.json';
26
45
 
27
46
  function fail(code, message, cause) {
28
47
  const error = new Error(message, cause ? { cause } : undefined);
@@ -91,8 +110,8 @@ function readDescriptorBounded(descriptor, maxBytes) {
91
110
  }
92
111
 
93
112
  function openPinnedHandoffDirectory(path, { create = false } = {}) {
94
- if (process.platform !== 'linux') {
95
- fail('UNSUPPORTED_HANDOFF_STORAGE_PLATFORM', 'RAIL handoff authority storage requires Linux descriptor-relative filesystem operations');
113
+ if (!['linux', 'darwin'].includes(process.platform)) {
114
+ fail('UNSUPPORTED_HANDOFF_STORAGE_PLATFORM', `RAIL handoff authority storage requires descriptor-relative operations; unsupported platform: ${process.platform}`);
96
115
  }
97
116
  const absolute = resolve(path);
98
117
  const root = parse(absolute).root;
@@ -100,10 +119,9 @@ function openPinnedHandoffDirectory(path, { create = false } = {}) {
100
119
  let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
101
120
  try {
102
121
  for (const segment of segments) {
103
- const childPath = join(`/proc/self/fd/${descriptor}`, segment);
104
122
  let childDescriptor;
105
123
  try {
106
- childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
124
+ childDescriptor = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
107
125
  } catch (error) {
108
126
  if (error?.code !== 'ENOENT' || !create) {
109
127
  if (['ELOOP', 'ENOTDIR'].includes(error?.code)) {
@@ -111,9 +129,9 @@ function openPinnedHandoffDirectory(path, { create = false } = {}) {
111
129
  }
112
130
  throw error;
113
131
  }
114
- try { mkdirSync(childPath, { mode: 0o700 }); }
132
+ try { mkdirAtSync(descriptor, segment, { mode: 0o700 }); }
115
133
  catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
116
- childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
134
+ childDescriptor = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
117
135
  }
118
136
  const stat = fstatSync(childDescriptor);
119
137
  if (!stat.isDirectory()) {
@@ -127,7 +145,6 @@ function openPinnedHandoffDirectory(path, { create = false } = {}) {
127
145
  absolute,
128
146
  descriptor,
129
147
  identity: fstatSync(descriptor),
130
- reference: `/proc/self/fd/${descriptor}`,
131
148
  });
132
149
  } catch (error) {
133
150
  closeSync(descriptor);
@@ -135,13 +152,6 @@ function openPinnedHandoffDirectory(path, { create = false } = {}) {
135
152
  }
136
153
  }
137
154
 
138
- function handoffEntryPath(directory, name) {
139
- if (basename(name) !== name || name === '.' || name === '..') {
140
- fail('UNSAFE_HANDOFF_STORAGE', 'handoff storage entry must be one path segment');
141
- }
142
- return join(directory.reference, name);
143
- }
144
-
145
155
  function assertPinnedHandoffDirectory(directory) {
146
156
  const descriptorStat = fstatSync(directory.descriptor);
147
157
  if (!descriptorStat.isDirectory() || !sameIdentity(descriptorStat, directory.identity)) {
@@ -167,11 +177,10 @@ function readHandoffContent(path, { allowMissing = false, directory = null } = {
167
177
  try { activeDirectory = openPinnedHandoffDirectory(dirname(path)); }
168
178
  catch (error) { if (allowMissing && error?.code === 'ENOENT') return null; throw error; }
169
179
  }
170
- const entry = handoffEntryPath(activeDirectory, basename(path));
171
180
  runSecurityTestHook('beforeHandoffOpen', { path, directory: activeDirectory.absolute });
172
181
  let descriptor;
173
182
  try {
174
- descriptor = openSync(entry, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
183
+ descriptor = openAtSync(activeDirectory.descriptor, basename(path), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
175
184
  } catch (error) {
176
185
  if (ownedDirectory) closeSync(activeDirectory.descriptor);
177
186
  if (allowMissing && error?.code === 'ENOENT') return null;
@@ -218,6 +227,143 @@ export function getRailPaths(projectDir) {
218
227
  });
219
228
  }
220
229
 
230
+ function railCouncilBindingPath(projectDir, handoff_id) {
231
+ return join(getRailPaths(projectDir).root, 'council', `${handoff_id}.json`);
232
+ }
233
+
234
+ function approvedTaskGraphReference(approval) {
235
+ const taskGraph = approval?.plan?.artifacts?.find((artifact) => artifact.path === EXECUTION_DESIGN_TASK_GRAPH_PATH);
236
+ if (!taskGraph || !/^[a-f0-9]{64}$/i.test(taskGraph.sha256)) {
237
+ fail('RAIL_COUNCIL_BINDING_INVALID', 'RAIL execution-design council approval has no valid frozen task graph digest');
238
+ }
239
+ return `local-artifact://sha256/${taskGraph.sha256}`;
240
+ }
241
+
242
+ function railCouncilBindingMaterial(handoff, approval) {
243
+ const validated = validateSealedHandoff(handoff);
244
+ if (!approval?.plan?.gate_id || !approval?.decision) {
245
+ fail('RAIL_COUNCIL_BINDING_INVALID', 'RAIL council binding requires an approved execution-design decision');
246
+ }
247
+ const task_graph_ref = approvedTaskGraphReference(approval);
248
+ if (validated.task_graph_ref !== task_graph_ref) {
249
+ fail('RAIL_COUNCIL_TASK_GRAPH_MISMATCH', 'RAIL handoff task graph is not the exact task graph approved by the execution-design council', {
250
+ handoff_task_graph_ref: validated.task_graph_ref,
251
+ council_task_graph_ref: task_graph_ref,
252
+ });
253
+ }
254
+ if (approval.plan.handoff_manifest_digest !== validated.seal.manifest_digest) {
255
+ fail('RAIL_COUNCIL_HANDOFF_MISMATCH', 'RAIL handoff is not the exact executable manifest reviewed by the execution-design council', {
256
+ handoff_manifest_digest: validated.seal.manifest_digest,
257
+ council_handoff_manifest_digest: approval.plan.handoff_manifest_digest,
258
+ });
259
+ }
260
+ return canonicalize({
261
+ record_type: 'chati-rail-execution-design-binding',
262
+ handoff_id: validated.handoff_id,
263
+ handoff_manifest_digest: validated.seal.manifest_digest,
264
+ task_graph_ref,
265
+ council_gate_id: approval.plan.gate_id,
266
+ council_decision_digest: sha256(canonicalSerialize(approval.decision)),
267
+ });
268
+ }
269
+
270
+ function signRailCouncilBinding(material, authorityDirectory) {
271
+ const signed = signHostAuthorityPayload(material, {
272
+ domain: RAIL_COUNCIL_BINDING_AUTHORITY_DOMAIN,
273
+ authorityDirectory,
274
+ });
275
+ return canonicalize({
276
+ ...material,
277
+ authority_receipt: {
278
+ schema_version: 1,
279
+ authority_id: signed.authority_id,
280
+ signature: signed.signature,
281
+ },
282
+ });
283
+ }
284
+
285
+ function verifyRailCouncilBinding(record, expected, authorityDirectory) {
286
+ const allowed = new Set([
287
+ 'record_type', 'handoff_id', 'handoff_manifest_digest', 'council_gate_id',
288
+ 'task_graph_ref', 'council_decision_digest', 'authority_receipt',
289
+ ]);
290
+ if (!record || typeof record !== 'object' || Array.isArray(record)
291
+ || Object.keys(record).some((key) => !allowed.has(key))) {
292
+ fail('RAIL_COUNCIL_BINDING_INVALID', 'RAIL council binding has an invalid closed schema');
293
+ }
294
+ const receipt = record.authority_receipt;
295
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)
296
+ || receipt.schema_version !== 1
297
+ || typeof receipt.authority_id !== 'string'
298
+ || typeof receipt.signature !== 'string') {
299
+ fail('RAIL_COUNCIL_BINDING_UNTRUSTED', 'RAIL council binding has no valid host authority receipt');
300
+ }
301
+ const canonical = canonicalize({
302
+ record_type: record.record_type,
303
+ handoff_id: record.handoff_id,
304
+ handoff_manifest_digest: record.handoff_manifest_digest,
305
+ task_graph_ref: record.task_graph_ref,
306
+ council_gate_id: record.council_gate_id,
307
+ council_decision_digest: record.council_decision_digest,
308
+ });
309
+ if (!verifyHostAuthorityPayload(canonical, {
310
+ domain: RAIL_COUNCIL_BINDING_AUTHORITY_DOMAIN,
311
+ authorityId: receipt.authority_id,
312
+ signature: receipt.signature,
313
+ authorityDirectory,
314
+ })) {
315
+ fail('RAIL_COUNCIL_BINDING_UNTRUSTED', 'RAIL council binding authority receipt is invalid');
316
+ }
317
+ if (canonicalSerialize(canonical) !== canonicalSerialize(expected)) {
318
+ fail('RAIL_COUNCIL_BINDING_STALE', 'RAIL handoff is not bound to the current approved execution-design council decision');
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Persists the host-signed link between an accepted execution-design council
324
+ * decision and the exact immutable RAIL handoff that the compiler produced.
325
+ */
326
+ export function persistRailHandoffCouncilBinding({
327
+ projectDir, handoff, artifact, policy, authorityDirectory, clock = () => new Date(),
328
+ } = {}) {
329
+ const validated = validateSealedHandoff(handoff);
330
+ const approval = assertCouncilGateApproved({
331
+ projectDir,
332
+ stage: 'execution-design',
333
+ artifact,
334
+ policy,
335
+ handoff: validated,
336
+ authorityDirectory,
337
+ clock,
338
+ });
339
+ const material = railCouncilBindingMaterial(validated, approval);
340
+ const record = signRailCouncilBinding(material, authorityDirectory);
341
+ const path = railCouncilBindingPath(projectDir, material.handoff_id);
342
+ persistImmutableInstallationFile(path, `${canonicalSerialize(record)}\n`, {
343
+ conflictCode: 'RAIL_COUNCIL_BINDING_CONFLICT',
344
+ label: 'RAIL execution-design council binding',
345
+ });
346
+ return Object.freeze({ path, record, approval });
347
+ }
348
+
349
+ /** Verifies the signed execution-design approval is bound to this exact handoff. */
350
+ export function assertRailHandoffCouncilBinding({ projectDir, handoff, approval, authorityDirectory } = {}) {
351
+ const expected = railCouncilBindingMaterial(handoff, approval);
352
+ const path = railCouncilBindingPath(projectDir, expected.handoff_id);
353
+ const source = readSafeInstallationStorageFile(path, {
354
+ allowMissing: true,
355
+ label: 'RAIL execution-design council binding',
356
+ });
357
+ if (source === null) {
358
+ fail('RAIL_COUNCIL_BINDING_MISSING', `RAIL handoff ${expected.handoff_id} has no execution-design council binding`);
359
+ }
360
+ let record;
361
+ try { record = JSON.parse(source.content); }
362
+ catch { fail('RAIL_COUNCIL_BINDING_INVALID', 'RAIL council binding is not valid JSON'); }
363
+ verifyRailCouncilBinding(record, expected, authorityDirectory);
364
+ return Object.freeze({ path, record: canonicalize(record) });
365
+ }
366
+
221
367
  /** Persist one sealed planning handoff. Conflicting material is never overwritten. */
222
368
  export function persistRailHandoff({ projectDir, handoff }) {
223
369
  const validated = validateSealedHandoff(handoff);
@@ -227,8 +373,8 @@ export function persistRailHandoff({ projectDir, handoff }) {
227
373
  const directory = openPinnedHandoffDirectory(getRailPaths(projectDir).handoffs, { create: true });
228
374
  const name = basename(path);
229
375
  const temporaryName = `${name}.${process.pid}.${randomUUID()}.tmp`;
230
- const temporary = handoffEntryPath(directory, temporaryName);
231
- const destination = handoffEntryPath(directory, name);
376
+ // Diagnostic/test-hook path only. Actual reads and mutations use *at ops.
377
+ const temporary = join(directoryPathSync(directory.descriptor), temporaryName);
232
378
  let temporaryDescriptor;
233
379
  try {
234
380
  const existing = readHandoffContent(path, { allowMissing: true, directory });
@@ -238,7 +384,7 @@ export function persistRailHandoff({ projectDir, handoff }) {
238
384
  fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} already exists with different material`);
239
385
  }
240
386
  runSecurityTestHook('beforeHandoffTempOpen', { path, temporary, directory: directory.absolute });
241
- temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
387
+ temporaryDescriptor = openAtSync(directory.descriptor, temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
242
388
  runSecurityTestHook('afterHandoffTempOpen', { path, temporary, directory: directory.absolute });
243
389
  const bytes = Buffer.from(content);
244
390
  let offset = 0;
@@ -248,7 +394,7 @@ export function persistRailHandoff({ projectDir, handoff }) {
248
394
  temporaryDescriptor = undefined;
249
395
  assertPinnedHandoffDirectory(directory);
250
396
  runSecurityTestHook('beforeHandoffCommit', { path, temporary, directory: directory.absolute });
251
- try { linkSync(temporary, destination); }
397
+ try { linkAtSync(directory.descriptor, temporaryName, directory.descriptor, name); }
252
398
  catch (error) {
253
399
  if (error?.code !== 'EEXIST') throw error;
254
400
  const raced = readHandoffContent(path, { directory });
@@ -262,7 +408,7 @@ export function persistRailHandoff({ projectDir, handoff }) {
262
408
  } finally {
263
409
  if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
264
410
  try {
265
- try { unlinkSync(temporary); fsyncSync(directory.descriptor); }
411
+ try { unlinkAtSync(directory.descriptor, temporaryName); fsyncSync(directory.descriptor); }
266
412
  catch { /* a descriptor-bound temporary may remain after a failed operation */ }
267
413
  } finally { closeSync(directory.descriptor); }
268
414
  }
@@ -280,15 +426,34 @@ export function loadRailHandoff({ projectDir, handoff_id }) {
280
426
  }
281
427
  }
282
428
 
283
- export function createProjectRailEngine({ projectDir, clock = () => new Date() }) {
429
+ export function createProjectRailEngine({ projectDir, clock = () => new Date(), humanAuthorityDirectory }) {
284
430
  let evidenceAuthority;
285
431
  return new RailEngine({
286
432
  journal_path: getRailPaths(projectDir).journal,
287
433
  clock,
288
- authority_verifier: ({ kind, receipt, expected }) => {
434
+ execution_checkpoint_writer: captureRailWorktreeCheckpoint,
435
+ execution_restoration_writer: restoreRailWorktreeCheckpoint,
436
+ authority_verifier: ({ kind, receipt, expected, context }) => {
437
+ if (kind === 'execution_restoration') {
438
+ readRailWorktreeRestoration({ projectDir, receipt, expected,
439
+ checkpointReceipt: context.checkpoint_receipt, current: context.current === true });
440
+ return true;
441
+ }
442
+ if (kind === 'execution_checkpoint') {
443
+ readRailWorktreeCheckpoint({ projectDir, receipt, expected });
444
+ return true;
445
+ }
446
+ if (kind === 'human_effort') return verifyHumanEffortReceipt(receipt, { projectDir,
447
+ authorityDirectory: humanAuthorityDirectory, expected });
289
448
  if (['ci_evidence', 'acceptance_evidence', 'browser_evidence'].includes(kind)) {
290
449
  evidenceAuthority ??= loadProjectRailEvidenceAuthority({ projectDir, clock });
291
- return verifyRailEvidenceReceipt(evidenceAuthority, receipt, expected, clock);
450
+ const valid = verifyRailEvidenceReceipt(evidenceAuthority, receipt, expected, clock);
451
+ if (valid && kind === 'browser_evidence') {
452
+ assertLocalBrowserArtifacts({ projectDir, projectId: receipt.project_id, artifacts: context.artifacts, clock,
453
+ expected: { attempt_id: expected.attempt_id, commit_ref: expected.commit_ref, policy_ref: context.policy_ref,
454
+ browser_session_id: context.browser_session_id, captured_at: context.captured_at } });
455
+ }
456
+ return valid;
292
457
  }
293
458
  return verifyTerminalAuthorityReceipt(receipt, expected);
294
459
  },
@@ -300,6 +465,39 @@ function loadProjectRail({ projectDir, handoff_id, clock }) {
300
465
  return Object.freeze({ handoff: stored.handoff, handoff_path: stored.path, engine: createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) }) });
301
466
  }
302
467
 
468
+ export function recordRailProviderProcessObservation({ projectDir, attempt_id, terminal_authority_receipt, terminal_output, clock } = {}) {
469
+ return createProjectRailEngine({ projectDir, clock }).recordProviderProcessObservation({ attempt_id, terminal_authority_receipt, terminal_output });
470
+ }
471
+
472
+ export function reconcileRailObservedExecution({ projectDir, handoff_id, attempt_id, launch_nonce, expected_attempt_event_id, clock } = {}) {
473
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
474
+ resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
475
+ return engine.reconcileObservedExecution({ attempt_id, launch_nonce, git_dir: projectDir, expected_attempt_event_id });
476
+ }
477
+
478
+ export function observeRailInterruptedExecution({ projectDir, handoff_id, attempt_id, launch_nonce, expected_attempt_event_id, clock, termination_attestation } = {}) {
479
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
480
+ resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
481
+ return engine.observeInterruptedExecution({ attempt_id, launch_nonce, git_dir: projectDir, expected_attempt_event_id,
482
+ ...(termination_attestation ? { termination_attestation } : {}) });
483
+ }
484
+
485
+ export function preserveRailInterruptedExecution({ projectDir, handoff_id, attempt_id, launch_nonce, expected_attempt_event_id, clock } = {}) {
486
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
487
+ resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
488
+ return engine.preserveInterruptedExecution({ attempt_id, launch_nonce, git_dir: projectDir, expected_attempt_event_id });
489
+ }
490
+
491
+ export function collectRailProviderProcessing({ projectDir, attempt_id, clock } = {}) {
492
+ return createProjectRailEngine({ projectDir, clock }).providerProcessingForAttempt(attempt_id);
493
+ }
494
+
495
+ export function restoreRailInterruptedExecution({ projectDir, handoff_id, attempt_id, launch_nonce, expected_attempt_event_id, clock } = {}) {
496
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
497
+ resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
498
+ return engine.restoreInterruptedExecution({ attempt_id, launch_nonce, git_dir: projectDir, expected_attempt_event_id });
499
+ }
500
+
303
501
  function internalTrackingInstallation(projectDir, clock) {
304
502
  const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
305
503
  return artifact?.installation?.external_integrations?.clickup === 'required';
@@ -518,15 +716,34 @@ export function selectRailReleaseReadiness({ projectDir, handoff_id, clock } = {
518
716
  * harness adapter with the installation-selected binding. There is no provider
519
717
  * fallback and no direct CLI invocation in this boundary.
520
718
  */
521
- export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, adapter, browser_request, git_dir = projectDir, lease_ttl_ms, recover_expired = false, clock } = {}) {
719
+ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, adapter, browser_request, git_dir = projectDir, lease_ttl_ms, recover_expired = false, clock, expected_attempt_event_id } = {}) {
522
720
  if (!adapter || typeof adapter.invoke !== 'function') fail('HARNESS_ADAPTER_UNAVAILABLE', 'RAIL dispatch requires an injected harness adapter');
523
721
  const installation = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
524
722
  if (!installation) fail('V2_INSTALLATION_REQUIRED', 'RAIL dispatch requires a v2 installation');
525
- const language = projectRailLanguage(projectDir);
526
723
  const { handoff, handoff_path, engine } = loadProjectRail({ projectDir, handoff_id, clock });
724
+ // Old reference-only handoffs remain readable for history/recovery, but must
725
+ // be regenerated before dispatch. Fail before a claim or provider side effect.
726
+ validatePlanningMaterial(handoff, { required: true });
727
+ const council = resolveRuntimeCouncilGatePolicyV2({ artifact: installation, stage: 'execution-design' });
728
+ if (!council.enabled && council.reason === 'GUIDED_COUNCIL_REQUIRED') {
729
+ fail('GUIDED_COUNCIL_REQUIRED', 'RAIL dispatch requires installation reconfiguration with two selected review providers');
730
+ }
731
+ const councilApproval = council.enabled
732
+ ? assertCouncilGateApproved({
733
+ projectDir,
734
+ stage: 'execution-design',
735
+ artifact: installation,
736
+ policy: council.policy,
737
+ handoff,
738
+ ...(clock === undefined ? {} : { clock }),
739
+ })
740
+ : null;
741
+ const language = projectRailLanguage(projectDir);
742
+ if (councilApproval) assertRailHandoffCouncilBinding({ projectDir, handoff, approval: councilApproval });
527
743
  const priorClaim = engine.records.find((record) => record.type === 'attempt_claimed'
528
744
  && record.payload?.attempt?.attempt_id === attempt_id);
529
745
  let resumedContext = null;
746
+ let expectedRevision = expected_attempt_event_id ?? attemptJournalRevision(engine.records, attempt_id);
530
747
  let selection = null;
531
748
  if (priorClaim) {
532
749
  resumedContext = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
@@ -544,13 +761,16 @@ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt
544
761
  supervisor_process_id: currentProcessId,
545
762
  });
546
763
  }
764
+ globalThis[Symbol.for('chati.rail.security-test-hooks')]?.beforeRecoveryAcquisition?.({ attempt_id });
547
765
  engine.recoverAttempt({
548
766
  attempt_id,
549
767
  operator_id,
550
768
  harness_process_id: currentProcessId,
551
769
  git_dir,
770
+ expected_attempt_event_id: expectedRevision,
552
771
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
553
772
  });
773
+ expectedRevision = undefined;
554
774
  resumedContext = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
555
775
  } else if (resumedContext.claim.operator_id !== operator_id) {
556
776
  fail('CLAIM_OWNER_MISMATCH', 'retry requires the active claim owner');
@@ -586,6 +806,7 @@ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt
586
806
  claim_id: resumedContext.claim.claim_id,
587
807
  operator_id,
588
808
  harness_process_id: `process://chati/${process.pid}`,
809
+ expected_attempt_event_id: expectedRevision,
589
810
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
590
811
  });
591
812
  claim = Object.freeze({
@@ -614,10 +835,11 @@ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt
614
835
  invocation: invocationDraft,
615
836
  browserManifest: browser?.manifest ?? null,
616
837
  language,
838
+ executionBaseCommitRef: attemptExecutionBase(claim.attempt),
617
839
  });
618
840
  const invocation = Object.freeze({ ...invocationDraft, prompt_digest: sha256(prompt) });
619
841
  let launchReserved = false;
620
- const reserve_launch = () => {
842
+ const reserve_launch = ({ admission_receipt = null } = {}) => {
621
843
  if (launchReserved) fail('EXECUTION_LAUNCH_RESERVATION_REUSED', 'terminal attempted to reserve the same provider launch more than once');
622
844
  engine.reserveExecutionLaunch({
623
845
  attempt_id,
@@ -625,16 +847,34 @@ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt
625
847
  operator_id: claim.claim.operator_id,
626
848
  harness_process_id: `process://chati/${process.pid}`,
627
849
  invocation,
850
+ project_dir: projectDir, admission_receipt,
628
851
  });
629
852
  launchReserved = true;
630
853
  };
854
+ let dispatch;
855
+ const record_dispatch = (receipt) => {
856
+ if (!launchReserved) fail('EXECUTION_LAUNCH_NOT_RESERVED', 'terminal authority must reserve before committing dispatch');
857
+ if (dispatch) fail('EXECUTION_DISPATCH_CALLBACK_REUSED', 'terminal must commit a launch exactly once');
858
+ dispatch = engine.recordExecutionDispatch({
859
+ attempt_id,
860
+ claim_id: claim.claim.claim_id,
861
+ operator_id: claim.claim.operator_id,
862
+ invocation,
863
+ launch_receipt: receipt,
864
+ project_dir: projectDir,
865
+ browser_manifest: browser?.manifest ?? null,
866
+ });
867
+ return dispatch;
868
+ };
631
869
  const receipt = adapter.invoke({
870
+ attempt: claim.attempt,
632
871
  installation: installation.installation,
633
872
  snapshot: installation.capability_snapshot,
634
873
  invocation,
635
874
  prompt,
636
875
  browser_manifest: browser?.manifest ?? null,
637
876
  reserve_launch,
877
+ record_dispatch,
638
878
  });
639
879
  if (!launchReserved) fail('EXECUTION_LAUNCH_NOT_RESERVED', 'terminal authority must reserve the provider launch at the final pre-spawn boundary');
640
880
  if (!verifyTerminalAuthorityReceipt(receipt, {
@@ -647,15 +887,10 @@ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt
647
887
  working_dir: resolve(projectDir),
648
888
  prompt_digest: invocation.prompt_digest,
649
889
  })) fail('UNTRUSTED_EXECUTION_DISPATCH', 'execution dispatch requires a launch receipt minted by the terminal authority');
650
- const dispatch = engine.recordExecutionDispatch({
651
- attempt_id,
652
- claim_id: claim.claim.claim_id,
653
- operator_id: claim.claim.operator_id,
654
- invocation,
655
- launch_receipt: receipt,
656
- project_dir: projectDir,
657
- browser_manifest: browser?.manifest ?? null,
658
- });
890
+ if (!dispatch) record_dispatch(receipt);
891
+ else if (sha256(dispatch.launch_receipt) !== sha256(receipt)) {
892
+ fail('EXECUTION_DISPATCH_RECEIPT_MISMATCH', 'terminal returned a different receipt after canonical admission');
893
+ }
659
894
  return Object.freeze({ claim, invocation, receipt, dispatch, task_id, handoff_id, resumed: Boolean(resumedContext), browser_manifest: browser?.manifest ?? null });
660
895
  }
661
896
 
@@ -694,10 +929,11 @@ export function claimRailTask({ projectDir, handoff_id, task_id, attempt_id, ope
694
929
  }
695
930
 
696
931
  /** Renew a claim lease against the local project journal and sealed handoff. */
697
- export function heartbeatRailTask({ projectDir, handoff_id, attempt_id, claim_id, operator_id, lease_ttl_ms, clock } = {}) {
932
+ export function heartbeatRailTask({ projectDir, handoff_id, attempt_id, claim_id, operator_id, lease_ttl_ms, clock, expected_attempt_event_id } = {}) {
698
933
  const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
699
934
  return engine.heartbeat({
700
935
  attempt_id, claim_id, operator_id,
936
+ expected_attempt_event_id,
701
937
  harness_process_id: `process://chati/${process.pid}`,
702
938
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
703
939
  });
@@ -710,7 +946,7 @@ export function heartbeatRailTask({ projectDir, handoff_id, attempt_id, claim_id
710
946
  */
711
947
  export function acquireRailTaskSupervisor({
712
948
  projectDir, handoff_id, attempt_id, operator_id, git_dir = projectDir,
713
- lease_ttl_ms, clock,
949
+ lease_ttl_ms, clock, expected_attempt_event_id,
714
950
  } = {}) {
715
951
  let context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
716
952
  const effectiveOperator = operator_id ?? context.claim.operator_id;
@@ -718,10 +954,12 @@ export function acquireRailTaskSupervisor({
718
954
  const observedNow = typeof clock === 'function' ? clock() : new Date();
719
955
  const observedNowMs = observedNow instanceof Date ? observedNow.getTime() : Date.parse(observedNow);
720
956
  let recovered = false;
957
+ const expectedRevision = expected_attempt_event_id ?? attemptJournalRevision(context.records, attempt_id);
721
958
  if (context.claim.harness_process_id !== currentProcessId
722
959
  || Date.parse(context.claim.lease.expires_at) <= observedNowMs) {
723
960
  recoverRailTask({
724
961
  projectDir, handoff_id, attempt_id, operator_id: effectiveOperator, git_dir,
962
+ expected_attempt_event_id: expectedRevision,
725
963
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
726
964
  ...(clock === undefined ? {} : { clock }),
727
965
  });
@@ -731,6 +969,7 @@ export function acquireRailTaskSupervisor({
731
969
  const renewed = heartbeatRailTask({
732
970
  projectDir, handoff_id, attempt_id,
733
971
  claim_id: context.claim.claim_id, operator_id: effectiveOperator,
972
+ ...(!recovered ? { expected_attempt_event_id: expectedRevision } : {}),
734
973
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
735
974
  ...(clock === undefined ? {} : { clock }),
736
975
  });
@@ -799,6 +1038,7 @@ export function reserveRailRework({
799
1038
  /** Persist the fail-closed rework launch intent before a provider process is created. */
800
1039
  export function reserveRailReworkLaunch({
801
1040
  projectDir, handoff_id, attempt_id, source_decision_ref, reservation_id, invocation, clock,
1041
+ admission_receipt = null,
802
1042
  } = {}) {
803
1043
  const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
804
1044
  const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
@@ -810,6 +1050,7 @@ export function reserveRailReworkLaunch({
810
1050
  source_decision_ref,
811
1051
  reservation_id,
812
1052
  invocation,
1053
+ project_dir: projectDir, admission_receipt,
813
1054
  });
814
1055
  }
815
1056
 
@@ -873,7 +1114,7 @@ export function recordRailExecutionEvidence({
873
1114
 
874
1115
  /** Persist the fail-closed initial launch intent before a provider process is created. */
875
1116
  export function reserveRailExecutionLaunch({
876
- projectDir, handoff_id, attempt_id, invocation, clock,
1117
+ projectDir, handoff_id, attempt_id, invocation, clock, admission_receipt = null,
877
1118
  } = {}) {
878
1119
  const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
879
1120
  const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
@@ -883,6 +1124,7 @@ export function reserveRailExecutionLaunch({
883
1124
  operator_id: context.claim.operator_id,
884
1125
  harness_process_id: `process://chati/${process.pid}`,
885
1126
  invocation,
1127
+ project_dir: projectDir, admission_receipt,
886
1128
  });
887
1129
  }
888
1130
 
@@ -917,10 +1159,11 @@ export function recordRailExecutionDispatch({
917
1159
  projectDir, context, sourceReview, sourceAdjudication, executionHistory, baseCommitRef, invocation,
918
1160
  });
919
1161
  } else {
920
- canonicalBaseCommitRef = context.attempt.git_commit_ref;
1162
+ canonicalBaseCommitRef = attemptExecutionBase(context.attempt);
921
1163
  canonicalPrompt = buildRailTaskPrompt({
922
1164
  projectDir, stored: { path: handoff_path, handoff }, task: context.task, invocation,
923
1165
  browserManifest: browser_manifest, language: invocation?.language ?? context.language,
1166
+ executionBaseCommitRef: canonicalBaseCommitRef,
924
1167
  });
925
1168
  }
926
1169
  if (prompt !== canonicalPrompt || invocation?.prompt_digest !== sha256(canonicalPrompt)) {
@@ -971,10 +1214,11 @@ export function loadLatestRailExecutionEvidence({ projectDir, attempt_id, clock
971
1214
  }
972
1215
 
973
1216
  /** Recover an expired attempt only from observations derived by the canonical engine. */
974
- export function recoverRailTask({ projectDir, handoff_id, attempt_id, operator_id, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
1217
+ export function recoverRailTask({ projectDir, handoff_id, attempt_id, operator_id, git_dir = projectDir, lease_ttl_ms, clock, expected_attempt_event_id } = {}) {
975
1218
  const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
976
1219
  return engine.recoverAttempt({
977
1220
  attempt_id, operator_id, harness_process_id: `process://chati/${process.pid}`, git_dir,
1221
+ expected_attempt_event_id,
978
1222
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
979
1223
  });
980
1224
  }