chati-dev 4.5.16 → 4.5.27

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 (122) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +1 -1
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +235 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. package/src/wizard/index.js +3 -2
@@ -1,13 +1,28 @@
1
- import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import {
2
+ closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, mkdirSync, openSync,
3
+ readSync, unlinkSync, writeSync, realpathSync,
4
+ } from 'node:fs';
5
+ import { basename, dirname, join, parse, resolve, sep } from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
3
7
  import { sha256 } from '@chati/core';
4
- import { RailEngine } from '@chati/rail';
8
+ import { adjudicationAttemptState, adjudicationEntriesAgree, RailEngine } from '@chati/rail';
5
9
  import { validateSealedHandoff } from '@chati/planning';
6
- import { loadRuntimeInstallationV2, resolveRuntimeInvocationV2 } from './runtime-installation-v2.js';
10
+ import { validateAdjudicationDecision } from '@chati/review-council';
11
+ import { loadRuntimeInstallationV2, resolveRuntimeRailInvocationV2 } from './runtime-installation-v2.js';
7
12
  import { createProjectBrowserSession, launchProjectBrowserSession } from './browser-runtime.js';
8
13
  import { enqueueClickUpProjectionFromState } from './clickup-projection.js';
14
+ import { verifyTerminalAuthorityReceipt } from '../terminal/spawner.js';
15
+ import { buildRailAdjudicationPrompt, buildRailReworkPrompt, buildRailTaskPrompt } from '../terminal/rail-prompts.js';
16
+ import { readSessionSafe } from './session-manager.js';
17
+ import { assertRailReadOnlyCommitWorkspaceUnchanged } from '../terminal/rail-readonly-workspace.js';
18
+ import { loadProjectRailEvidenceAuthority, verifyRailEvidenceReceipt } from './rail-evidence-authority.js';
19
+ import {
20
+ listClickUpDocumentPaths, persistImmutableClickUpDocument,
21
+ readClickUpDocument, removeImmutableClickUpDocument,
22
+ } from '@chati/tracking-clickup';
9
23
 
10
24
  export const RAIL_V2_DIR = '.chati/v2/rail';
25
+ const HANDOFF_MAX_BYTES = 4 * 1024 * 1024;
11
26
 
12
27
  function fail(code, message, cause) {
13
28
  const error = new Error(message, cause ? { cause } : undefined);
@@ -15,6 +30,173 @@ function fail(code, message, cause) {
15
30
  throw error;
16
31
  }
17
32
 
33
+ function projectRailLanguage(projectDir) {
34
+ const loaded = readSessionSafe(projectDir);
35
+ if (!loaded.loaded) fail('RAIL_SESSION_LANGUAGE_UNAVAILABLE', `RAIL requires canonical session language: ${loaded.error}`);
36
+ const language = loaded.session?.language;
37
+ if (!['en', 'pt', 'es', 'fr'].includes(language)) {
38
+ fail('RAIL_SESSION_LANGUAGE_INVALID', `unsupported canonical RAIL language: ${language}`);
39
+ }
40
+ return language;
41
+ }
42
+
43
+ function parseAuthorizedAdjudicationOutput(text) {
44
+ if (typeof text !== 'string') fail('RAIL_ADJUDICATION_OUTPUT_INVALID', 'adjudication_output must be a string');
45
+ const startToken = '<CHATI_RAIL_ADJUDICATION>';
46
+ const endToken = '</CHATI_RAIL_ADJUDICATION>';
47
+ const starts = [...text.matchAll(new RegExp(startToken, 'g'))];
48
+ const ends = [...text.matchAll(new RegExp(endToken, 'g'))];
49
+ if (starts.length !== 1 || ends.length !== 1 || ends[0].index < starts[0].index) {
50
+ fail('RAIL_ADJUDICATION_OUTPUT_INVALID', 'adjudication_output must contain exactly one ordered envelope');
51
+ }
52
+ try {
53
+ return validateAdjudicationDecision(JSON.parse(text.slice(
54
+ starts[0].index + startToken.length,
55
+ ends[0].index,
56
+ ).trim()));
57
+ } catch (error) {
58
+ return fail(error.code || 'RAIL_ADJUDICATION_OUTPUT_INVALID', error.message, error);
59
+ }
60
+ }
61
+
62
+ function sameIdentity(left, right) {
63
+ return left?.dev === right?.dev && left?.ino === right?.ino;
64
+ }
65
+
66
+ function sameSnapshot(left, right) {
67
+ return sameIdentity(left, right)
68
+ && left?.mode === right?.mode
69
+ && left?.uid === right?.uid
70
+ && left?.gid === right?.gid
71
+ && left?.nlink === right?.nlink
72
+ && left?.size === right?.size
73
+ && left?.mtimeMs === right?.mtimeMs
74
+ && left?.ctimeMs === right?.ctimeMs;
75
+ }
76
+
77
+ function runSecurityTestHook(name, payload) {
78
+ const hook = globalThis[Symbol.for('chati.rail-runtime.security-test-hooks')]?.[name];
79
+ if (typeof hook === 'function') hook(payload);
80
+ }
81
+
82
+ function readDescriptorBounded(descriptor, maxBytes) {
83
+ const bytes = Buffer.allocUnsafe(maxBytes + 1);
84
+ let offset = 0;
85
+ while (offset < bytes.length) {
86
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
87
+ if (count === 0) break;
88
+ offset += count;
89
+ }
90
+ return bytes.subarray(0, offset);
91
+ }
92
+
93
+ 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');
96
+ }
97
+ const absolute = resolve(path);
98
+ const root = parse(absolute).root;
99
+ const segments = absolute.slice(root.length).split(sep).filter(Boolean);
100
+ let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
101
+ try {
102
+ for (const segment of segments) {
103
+ const childPath = join(`/proc/self/fd/${descriptor}`, segment);
104
+ let childDescriptor;
105
+ try {
106
+ childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
107
+ } catch (error) {
108
+ if (error?.code !== 'ENOENT' || !create) {
109
+ if (['ELOOP', 'ENOTDIR'].includes(error?.code)) {
110
+ fail('UNSAFE_HANDOFF_STORAGE', 'handoff directory chain must contain only real directories');
111
+ }
112
+ throw error;
113
+ }
114
+ try { mkdirSync(childPath, { mode: 0o700 }); }
115
+ catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
116
+ childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
117
+ }
118
+ const stat = fstatSync(childDescriptor);
119
+ if (!stat.isDirectory()) {
120
+ closeSync(childDescriptor);
121
+ fail('UNSAFE_HANDOFF_STORAGE', 'handoff directory chain must contain only real directories');
122
+ }
123
+ closeSync(descriptor);
124
+ descriptor = childDescriptor;
125
+ }
126
+ return Object.freeze({
127
+ absolute,
128
+ descriptor,
129
+ identity: fstatSync(descriptor),
130
+ reference: `/proc/self/fd/${descriptor}`,
131
+ });
132
+ } catch (error) {
133
+ closeSync(descriptor);
134
+ throw error;
135
+ }
136
+ }
137
+
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
+ function assertPinnedHandoffDirectory(directory) {
146
+ const descriptorStat = fstatSync(directory.descriptor);
147
+ if (!descriptorStat.isDirectory() || !sameIdentity(descriptorStat, directory.identity)) {
148
+ fail('UNSAFE_HANDOFF_STORAGE', 'pinned handoff directory descriptor changed');
149
+ }
150
+ let current;
151
+ try { current = openPinnedHandoffDirectory(directory.absolute); }
152
+ catch (error) {
153
+ if (error?.code === 'UNSAFE_HANDOFF_STORAGE') throw error;
154
+ fail('UNSAFE_HANDOFF_STORAGE', 'handoff directory chain changed during storage', error);
155
+ }
156
+ try {
157
+ if (!sameIdentity(current.identity, directory.identity)) {
158
+ fail('UNSAFE_HANDOFF_STORAGE', 'handoff directory chain changed during storage');
159
+ }
160
+ } finally { closeSync(current.descriptor); }
161
+ }
162
+
163
+ function readHandoffContent(path, { allowMissing = false, directory = null } = {}) {
164
+ let activeDirectory = directory;
165
+ const ownedDirectory = activeDirectory === null;
166
+ if (ownedDirectory) {
167
+ try { activeDirectory = openPinnedHandoffDirectory(dirname(path)); }
168
+ catch (error) { if (allowMissing && error?.code === 'ENOENT') return null; throw error; }
169
+ }
170
+ const entry = handoffEntryPath(activeDirectory, basename(path));
171
+ runSecurityTestHook('beforeHandoffOpen', { path, directory: activeDirectory.absolute });
172
+ let descriptor;
173
+ try {
174
+ descriptor = openSync(entry, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
175
+ } catch (error) {
176
+ if (ownedDirectory) closeSync(activeDirectory.descriptor);
177
+ if (allowMissing && error?.code === 'ENOENT') return null;
178
+ if (['ELOOP', 'ENXIO', 'EISDIR'].includes(error?.code)) fail('UNSAFE_HANDOFF_STORAGE', 'handoff must not be a symlink, FIFO or directory');
179
+ throw error;
180
+ }
181
+ try {
182
+ const before = fstatSync(descriptor);
183
+ if (!before.isFile() || before.nlink !== 1) fail('UNSAFE_HANDOFF_STORAGE', 'handoff must be a regular single-link file');
184
+ if (before.size > HANDOFF_MAX_BYTES) fail('HANDOFF_SIZE_LIMIT', 'handoff exceeds the 4 MiB safety limit');
185
+ runSecurityTestHook('afterHandoffStat', { path, descriptor });
186
+ const content = readDescriptorBounded(descriptor, HANDOFF_MAX_BYTES);
187
+ if (content.length > HANDOFF_MAX_BYTES) fail('HANDOFF_SIZE_LIMIT', 'handoff exceeds the 4 MiB safety limit');
188
+ const after = fstatSync(descriptor);
189
+ if (!after.isFile() || after.nlink !== 1 || !sameSnapshot(before, after)) {
190
+ fail('HANDOFF_STORAGE_CONFLICT', 'handoff changed while it was being read');
191
+ }
192
+ assertPinnedHandoffDirectory(activeDirectory);
193
+ return content.toString('utf8');
194
+ } finally {
195
+ closeSync(descriptor);
196
+ if (ownedDirectory) closeSync(activeDirectory.descriptor);
197
+ }
198
+ }
199
+
18
200
  function handoffPath(projectDir, handoffId) {
19
201
  if (typeof handoffId !== 'string' || handoffId.trim() === '') fail('INVALID_HANDOFF_ID', 'handoff_id is required');
20
202
  if (!/^[A-Za-z0-9._-]+$/.test(handoffId)) fail('INVALID_HANDOFF_ID', 'handoff_id contains unsupported path characters');
@@ -41,33 +223,57 @@ export function persistRailHandoff({ projectDir, handoff }) {
41
223
  const validated = validateSealedHandoff(handoff);
42
224
  const path = handoffPath(projectDir, validated.handoff_id);
43
225
  const content = `${JSON.stringify(validated, null, 2)}\n`;
44
- if (existsSync(path)) {
45
- const existing = readFileSync(path, 'utf8');
46
- if (existing === content) return Object.freeze({ path, handoff: validated, written: false });
47
- fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} already exists with different material`);
48
- }
49
- mkdirSync(getRailPaths(projectDir).handoffs, { recursive: true, mode: 0o700 });
50
- const temporary = `${path}.${process.pid}.tmp`;
226
+ if (Buffer.byteLength(content) > HANDOFF_MAX_BYTES) fail('HANDOFF_SIZE_LIMIT', 'sealed handoff exceeds the 4 MiB safety limit');
227
+ const directory = openPinnedHandoffDirectory(getRailPaths(projectDir).handoffs, { create: true });
228
+ const name = basename(path);
229
+ const temporaryName = `${name}.${process.pid}.${randomUUID()}.tmp`;
230
+ const temporary = handoffEntryPath(directory, temporaryName);
231
+ const destination = handoffEntryPath(directory, name);
232
+ let temporaryDescriptor;
51
233
  try {
52
- writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
53
- linkSync(temporary, path);
54
- } catch (error) {
55
- if (error?.code === 'EEXIST' && existsSync(path) && readFileSync(path, 'utf8') === content) {
56
- return Object.freeze({ path, handoff: validated, written: false });
234
+ const existing = readHandoffContent(path, { allowMissing: true, directory });
235
+ assertPinnedHandoffDirectory(directory);
236
+ if (existing !== null) {
237
+ if (existing === content) return Object.freeze({ path, handoff: validated, written: false });
238
+ fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} already exists with different material`);
57
239
  }
58
- if (error?.code === 'EEXIST') fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} already exists`, error);
59
- throw error;
240
+ 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);
242
+ runSecurityTestHook('afterHandoffTempOpen', { path, temporary, directory: directory.absolute });
243
+ const bytes = Buffer.from(content);
244
+ let offset = 0;
245
+ while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
246
+ fsyncSync(temporaryDescriptor);
247
+ closeSync(temporaryDescriptor);
248
+ temporaryDescriptor = undefined;
249
+ assertPinnedHandoffDirectory(directory);
250
+ runSecurityTestHook('beforeHandoffCommit', { path, temporary, directory: directory.absolute });
251
+ try { linkSync(temporary, destination); }
252
+ catch (error) {
253
+ if (error?.code !== 'EEXIST') throw error;
254
+ const raced = readHandoffContent(path, { directory });
255
+ if (raced === content) return Object.freeze({ path, handoff: validated, written: false });
256
+ fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} raced with different material`, error);
257
+ }
258
+ runSecurityTestHook('afterHandoffCommit', { path, temporary, directory: directory.absolute });
259
+ assertPinnedHandoffDirectory(directory);
260
+ fsyncSync(directory.descriptor);
261
+ return Object.freeze({ path, handoff: validated, written: true });
60
262
  } finally {
61
- if (existsSync(temporary)) unlinkSync(temporary);
263
+ if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
264
+ try {
265
+ try { unlinkSync(temporary); fsyncSync(directory.descriptor); }
266
+ catch { /* a descriptor-bound temporary may remain after a failed operation */ }
267
+ } finally { closeSync(directory.descriptor); }
62
268
  }
63
- return Object.freeze({ path, handoff: validated, written: true });
64
269
  }
65
270
 
66
271
  export function loadRailHandoff({ projectDir, handoff_id }) {
67
272
  const path = handoffPath(projectDir, handoff_id);
68
- if (!existsSync(path)) fail('HANDOFF_NOT_FOUND', `no sealed handoff exists for ${handoff_id}`);
69
273
  try {
70
- return Object.freeze({ path, handoff: validateSealedHandoff(JSON.parse(readFileSync(path, 'utf8'))) });
274
+ const content = readHandoffContent(path, { allowMissing: true });
275
+ if (content === null) fail('HANDOFF_NOT_FOUND', `no sealed handoff exists for ${handoff_id}`);
276
+ return Object.freeze({ path, handoff: validateSealedHandoff(JSON.parse(content)) });
71
277
  } catch (error) {
72
278
  if (error?.code) throw error;
73
279
  return fail('INVALID_STORED_HANDOFF', `sealed handoff ${handoff_id} cannot be read`, error);
@@ -75,12 +281,23 @@ export function loadRailHandoff({ projectDir, handoff_id }) {
75
281
  }
76
282
 
77
283
  export function createProjectRailEngine({ projectDir, clock = () => new Date() }) {
78
- return new RailEngine({ journal_path: getRailPaths(projectDir).journal, clock });
284
+ let evidenceAuthority;
285
+ return new RailEngine({
286
+ journal_path: getRailPaths(projectDir).journal,
287
+ clock,
288
+ authority_verifier: ({ kind, receipt, expected }) => {
289
+ if (['ci_evidence', 'acceptance_evidence', 'browser_evidence'].includes(kind)) {
290
+ evidenceAuthority ??= loadProjectRailEvidenceAuthority({ projectDir, clock });
291
+ return verifyRailEvidenceReceipt(evidenceAuthority, receipt, expected, clock);
292
+ }
293
+ return verifyTerminalAuthorityReceipt(receipt, expected);
294
+ },
295
+ });
79
296
  }
80
297
 
81
298
  function loadProjectRail({ projectDir, handoff_id, clock }) {
82
299
  const stored = loadRailHandoff({ projectDir, handoff_id });
83
- return Object.freeze({ handoff: stored.handoff, engine: createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) }) });
300
+ return Object.freeze({ handoff: stored.handoff, handoff_path: stored.path, engine: createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) }) });
84
301
  }
85
302
 
86
303
  function internalTrackingInstallation(projectDir, clock) {
@@ -120,15 +337,7 @@ function trackingIntentPath(projectDir, intent) {
120
337
  function prepareTrackingIntent(projectDir, intent) {
121
338
  const normalized = Object.freeze({ schema_version: 1, ...intent });
122
339
  const path = trackingIntentPath(projectDir, normalized);
123
- const content = `${JSON.stringify(normalized, null, 2)}\n`;
124
- mkdirSync(trackingIntentDirectory(projectDir), { recursive: true, mode: 0o700 });
125
- if (existsSync(path)) {
126
- if (readFileSync(path, 'utf8') !== content) fail('TRACKING_INTENT_CONFLICT', 'tracking intent path contains different material');
127
- return path;
128
- }
129
- const temporary = `${path}.${process.pid}.tmp`;
130
- writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
131
- renameSync(temporary, path);
340
+ persistImmutableClickUpDocument(path, normalized, { maxBytes: 64 * 1024 });
132
341
  return path;
133
342
  }
134
343
 
@@ -137,7 +346,7 @@ function discardTrackingIntentWhenMutationAbsent({ intentPath, engine, matchesMu
137
346
  // If append reached the journal but checkpointing failed, the durable intent
138
347
  // must survive so recovery can reconcile the two local stores. Only discard
139
348
  // it when the in-memory canonical record set proves no mutation was appended.
140
- if (!engine.records.some(matchesMutation)) unlinkSync(intentPath);
349
+ if (!engine.records.some(matchesMutation)) removeImmutableClickUpDocument(intentPath, { maxBytes: 64 * 1024 });
141
350
  }
142
351
 
143
352
  /** Replays durable local projection intents from canonical RAIL journal state. */
@@ -145,10 +354,9 @@ export function replayPendingClickUpProjections({ projectDir, handoff_id, clock
145
354
  const directory = trackingIntentDirectory(projectDir);
146
355
  if (!existsSync(directory)) return Object.freeze([]);
147
356
  const outcomes = [];
148
- for (const filename of readdirSync(directory).filter((name) => name.endsWith('.json')).sort()) {
149
- const path = join(directory, filename);
357
+ for (const path of listClickUpDocumentPaths(directory)) {
150
358
  let intent;
151
- try { intent = JSON.parse(readFileSync(path, 'utf8')); } catch (error) {
359
+ try { intent = readClickUpDocument(path, { maxBytes: 64 * 1024 }); } catch (error) {
152
360
  outcomes.push(Object.freeze({ path, state: 'blocked', error_code: 'TRACKING_INTENT_CORRUPT', message: error.message }));
153
361
  continue;
154
362
  }
@@ -217,6 +425,9 @@ function attemptIdsForHandoff(records, handoffId) {
217
425
 
218
426
  function taskStates(engine, handoffId) {
219
427
  const attemptIds = attemptIdsForHandoff(engine.records, handoffId);
428
+ const taskIdsByAttempt = new Map(engine.records
429
+ .filter((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.handoff_id === handoffId)
430
+ .map((record) => [record.payload.attempt.attempt_id, record.payload.attempt.task_id]));
220
431
  const states = new Map();
221
432
  for (const record of engine.records) {
222
433
  if (record.type === 'attempt_claimed' && record.payload?.attempt?.handoff_id === handoffId) {
@@ -226,6 +437,26 @@ function taskStates(engine, handoffId) {
226
437
  const completion = record.payload.completion;
227
438
  states.set(completion.task_id, { task_id: completion.task_id, attempt_id: completion.attempt_id, state: completion.state });
228
439
  }
440
+ const attemptId = record.payload?.attempt_id;
441
+ const taskId = taskIdsByAttempt.get(attemptId);
442
+ if (taskId && record.type === 'attempt_blocked') {
443
+ states.set(taskId, { task_id: taskId, attempt_id: attemptId, state: 'blocked' });
444
+ }
445
+ if (taskId && record.type === 'adjudication_decided'
446
+ && adjudicationAttemptState(record.payload?.decision) === 'cancelled') {
447
+ states.set(taskId, {
448
+ task_id: taskId, attempt_id: attemptId, state: 'cancelled',
449
+ adjudication_ref: record.payload.adjudication_ref,
450
+ handoff_revision_ref: record.payload.decision.handoff_revision_ref,
451
+ });
452
+ }
453
+ if (taskId && record.type === 'attempt_cancelled') {
454
+ states.set(taskId, {
455
+ task_id: taskId, attempt_id: attemptId, state: 'cancelled',
456
+ adjudication_ref: record.payload.adjudication_ref,
457
+ handoff_revision_ref: record.payload.handoff_revision_ref,
458
+ });
459
+ }
229
460
  }
230
461
  return states;
231
462
  }
@@ -239,13 +470,23 @@ export function selectReadyRailTasks({ projectDir, handoff_id, clock } = {}) {
239
470
  const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
240
471
  const states = taskStates(engine, handoff.handoff_id);
241
472
  const merged = new Set([...states.values()].filter((item) => item.state === 'merged').map((item) => item.task_id));
242
- const ready = handoff.tasks.filter((task) => !states.has(task.task_id)
473
+ const cancelled = [...states.values()].filter((item) => item.state === 'cancelled');
474
+ const handoffRevisionRequired = cancelled.length > 0;
475
+ const ready = handoffRevisionRequired ? [] : handoff.tasks.filter((task) => !states.has(task.task_id)
243
476
  && task.dependency_ids.every((dependency) => merged.has(dependency)));
477
+ const blocked = handoffRevisionRequired
478
+ ? handoff.tasks.filter((task) => !merged.has(task.task_id))
479
+ : handoff.tasks.filter((task) => !states.has(task.task_id) && !ready.includes(task));
244
480
  return Object.freeze({
245
481
  handoff_id: handoff.handoff_id,
246
482
  completed_task_ids: Object.freeze([...merged].sort()),
247
483
  ready_task_ids: Object.freeze(ready.map((task) => task.task_id)),
248
- blocked_task_ids: Object.freeze(handoff.tasks.filter((task) => !states.has(task.task_id) && !ready.includes(task)).map((task) => task.task_id)),
484
+ blocked_task_ids: Object.freeze(blocked.map((task) => task.task_id)),
485
+ ...(handoffRevisionRequired ? {
486
+ next_required_action: 'planning_handoff_revision_required',
487
+ cancelled_task_ids: Object.freeze(cancelled.map((item) => item.task_id).sort()),
488
+ handoff_revision_refs: Object.freeze([...new Set(cancelled.map((item) => item.handoff_revision_ref).filter(Boolean))].sort()),
489
+ } : {}),
249
490
  });
250
491
  }
251
492
 
@@ -277,13 +518,47 @@ export function selectRailReleaseReadiness({ projectDir, handoff_id, clock } = {
277
518
  * harness adapter with the installation-selected binding. There is no provider
278
519
  * fallback and no direct CLI invocation in this boundary.
279
520
  */
280
- export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, harness_process_id, adapter, browser_request, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
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 } = {}) {
281
522
  if (!adapter || typeof adapter.invoke !== 'function') fail('HARNESS_ADAPTER_UNAVAILABLE', 'RAIL dispatch requires an injected harness adapter');
282
523
  const installation = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
283
524
  if (!installation) fail('V2_INSTALLATION_REQUIRED', 'RAIL dispatch requires a v2 installation');
284
- const selection = selectReadyRailTasks({ projectDir, handoff_id, clock });
285
- if (!selection.ready_task_ids.includes(task_id)) fail('TASK_NOT_DISPATCHABLE', 'task is not dependency-ready for dispatch', { task_id, ready_task_ids: selection.ready_task_ids });
286
- const { handoff } = loadProjectRail({ projectDir, handoff_id, clock });
525
+ const language = projectRailLanguage(projectDir);
526
+ const { handoff, handoff_path, engine } = loadProjectRail({ projectDir, handoff_id, clock });
527
+ const priorClaim = engine.records.find((record) => record.type === 'attempt_claimed'
528
+ && record.payload?.attempt?.attempt_id === attempt_id);
529
+ let resumedContext = null;
530
+ let selection = null;
531
+ if (priorClaim) {
532
+ resumedContext = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
533
+ if (resumedContext.task.task_id !== task_id) fail('ATTEMPT_TASK_MISMATCH', 'retry attempt is bound to a different task');
534
+ if (resumedContext.state !== 'active') fail('RAIL_RETRY_ATTEMPT_INACTIVE', `attempt is ${resumedContext.state}`);
535
+ if (engine.executionEvidenceForAttempt(attempt_id, { successful_only: true }).length > 0) {
536
+ fail('RAIL_EXECUTION_ALREADY_SUCCEEDED', 'attempt already has a successful canonical execution');
537
+ }
538
+ const currentProcessId = `process://chati/${process.pid}`;
539
+ if (resumedContext.claim.harness_process_id !== currentProcessId) {
540
+ if (!recover_expired) {
541
+ fail('RAIL_CLAIM_PROCESS_MISMATCH', 'retry requires the same live supervisor process; use the atomic recovery runner after lease expiry', {
542
+ attempt_id,
543
+ claim_process_id: resumedContext.claim.harness_process_id,
544
+ supervisor_process_id: currentProcessId,
545
+ });
546
+ }
547
+ engine.recoverAttempt({
548
+ attempt_id,
549
+ operator_id,
550
+ harness_process_id: currentProcessId,
551
+ git_dir,
552
+ ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
553
+ });
554
+ resumedContext = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
555
+ } else if (resumedContext.claim.operator_id !== operator_id) {
556
+ fail('CLAIM_OWNER_MISMATCH', 'retry requires the active claim owner');
557
+ }
558
+ } else {
559
+ selection = selectReadyRailTasks({ projectDir, handoff_id, clock });
560
+ if (!selection.ready_task_ids.includes(task_id)) fail('TASK_NOT_DISPATCHABLE', 'task is not dependency-ready for dispatch', { task_id, ready_task_ids: selection.ready_task_ids });
561
+ }
287
562
  const task = handoff.tasks.find((item) => item.task_id === task_id);
288
563
  if (!task.execution_binding || typeof task.execution_binding !== 'object'
289
564
  || ['provider_id', 'harness_id', 'model_id', 'reasoning_configuration']
@@ -295,33 +570,100 @@ export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt
295
570
  ? launchProjectBrowserSession({ session: createProjectBrowserSession({ task, request: browser_request }), request: browser_request })
296
571
  : null;
297
572
  const selectedBinding = { provider_id: task.execution_binding.provider_id, harness_id: task.execution_binding.harness_id };
298
- const invocationBase = resolveRuntimeInvocationV2({
573
+ const invocationBase = resolveRuntimeRailInvocationV2({
299
574
  artifact: installation,
300
- agent: 'dev',
575
+ action: 'build',
301
576
  binding: selectedBinding,
302
577
  model_id: task.execution_binding.model_id,
303
578
  reasoning_configuration: task.execution_binding.reasoning_configuration,
304
579
  ...(clock === undefined ? {} : { clock }),
305
580
  });
306
581
  if (adapter.harness_id && adapter.harness_id !== invocationBase.harness_id) fail('HARNESS_MISMATCH', 'selected installation binding does not match the injected adapter');
307
- const claim = claimRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, harness_process_id, completed_task_ids: selection.completed_task_ids, git_dir, ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }), ...(clock === undefined ? {} : { clock }) });
308
- const invocation = Object.freeze({
582
+ let claim;
583
+ if (resumedContext) {
584
+ const renewed = engine.heartbeat({
585
+ attempt_id,
586
+ claim_id: resumedContext.claim.claim_id,
587
+ operator_id,
588
+ harness_process_id: `process://chati/${process.pid}`,
589
+ ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
590
+ });
591
+ claim = Object.freeze({
592
+ attempt: resumedContext.attempt,
593
+ claim: Object.freeze({ ...resumedContext.claim, lease: renewed.lease }),
594
+ event_id: renewed.event_id,
595
+ resumed: true,
596
+ });
597
+ } else {
598
+ claim = claimRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, completed_task_ids: selection.completed_task_ids, git_dir, ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }), ...(clock === undefined ? {} : { clock }) });
599
+ }
600
+ const invocationDraft = {
309
601
  ...invocationBase,
310
602
  invocation_id: `rail-invocation-${attempt_id}`,
311
603
  attempt_id,
604
+ launch_nonce: randomUUID(),
312
605
  tool_policy_ref: task.routing_constraints_ref,
313
606
  input_ref: task.execution_scope_ref,
314
607
  expected_output_schema: 'rail-task-execution/v1',
608
+ language,
609
+ };
610
+ const prompt = buildRailTaskPrompt({
611
+ projectDir,
612
+ stored: { path: handoff_path, handoff },
613
+ task,
614
+ invocation: invocationDraft,
615
+ browserManifest: browser?.manifest ?? null,
616
+ language,
617
+ });
618
+ const invocation = Object.freeze({ ...invocationDraft, prompt_digest: sha256(prompt) });
619
+ let launchReserved = false;
620
+ const reserve_launch = () => {
621
+ if (launchReserved) fail('EXECUTION_LAUNCH_RESERVATION_REUSED', 'terminal attempted to reserve the same provider launch more than once');
622
+ engine.reserveExecutionLaunch({
623
+ attempt_id,
624
+ claim_id: claim.claim.claim_id,
625
+ operator_id: claim.claim.operator_id,
626
+ harness_process_id: `process://chati/${process.pid}`,
627
+ invocation,
628
+ });
629
+ launchReserved = true;
630
+ };
631
+ const receipt = adapter.invoke({
632
+ installation: installation.installation,
633
+ snapshot: installation.capability_snapshot,
634
+ invocation,
635
+ prompt,
636
+ browser_manifest: browser?.manifest ?? null,
637
+ reserve_launch,
638
+ });
639
+ if (!launchReserved) fail('EXECUTION_LAUNCH_NOT_RESERVED', 'terminal authority must reserve the provider launch at the final pre-spawn boundary');
640
+ if (!verifyTerminalAuthorityReceipt(receipt, {
641
+ purpose: 'terminal_launch', authority_kind: 'rail_execution',
642
+ invocation_id: invocation.invocation_id, launch_nonce: invocation.launch_nonce,
643
+ provider_id: invocation.provider_id, harness_id: invocation.harness_id,
644
+ model_id: invocation.model_pin.model_id,
645
+ reasoning_configuration: invocation.model_pin.reasoning_configuration,
646
+ task_id: task.task_id,
647
+ working_dir: resolve(projectDir),
648
+ prompt_digest: invocation.prompt_digest,
649
+ })) 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,
315
658
  });
316
- const receipt = adapter.invoke({ installation: installation.installation, snapshot: installation.capability_snapshot, invocation });
317
- return Object.freeze({ claim, invocation, receipt, task_id, handoff_id, browser_manifest: browser?.manifest ?? null });
659
+ return Object.freeze({ claim, invocation, receipt, dispatch, task_id, handoff_id, resumed: Boolean(resumedContext), browser_manifest: browser?.manifest ?? null });
318
660
  }
319
661
 
320
662
  /**
321
663
  * Entry point used by the CHATI runtime after a sealed planning handoff is
322
664
  * present. It creates an auditable RAIL claim before any task executor runs.
323
665
  */
324
- export function claimRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, harness_process_id, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
666
+ export function claimRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
325
667
  const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
326
668
  const states = taskStates(engine, handoff.handoff_id);
327
669
  const completed_task_ids = [...states.values()].filter((item) => item.state === 'merged').map((item) => item.task_id);
@@ -332,7 +674,7 @@ export function claimRailTask({ projectDir, handoff_id, task_id, attempt_id, ope
332
674
  let claim;
333
675
  try {
334
676
  claim = engine.claimTask({
335
- handoff, task_id, attempt_id, operator_id, harness_process_id,
677
+ handoff, task_id, attempt_id, operator_id, harness_process_id: `process://chati/${process.pid}`,
336
678
  completed_task_ids, git_dir, ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
337
679
  });
338
680
  } catch (error) {
@@ -356,25 +698,449 @@ export function heartbeatRailTask({ projectDir, handoff_id, attempt_id, claim_id
356
698
  const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
357
699
  return engine.heartbeat({
358
700
  attempt_id, claim_id, operator_id,
701
+ harness_process_id: `process://chati/${process.pid}`,
359
702
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
360
703
  });
361
704
  }
362
705
 
363
- /** Recover an expired attempt only after the caller supplies explicit reconciliation evidence. */
364
- export function recoverRailTask({ projectDir, handoff_id, attempt_id, operator_id, harness_process_id, reconciliation, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
706
+ /**
707
+ * Acquires the attempt for the current long-lived action supervisor. An
708
+ * explicitly relinquished claim can move immediately; an abrupt exit still
709
+ * requires both lease expiry and proof that the prior supervisor stopped.
710
+ */
711
+ export function acquireRailTaskSupervisor({
712
+ projectDir, handoff_id, attempt_id, operator_id, git_dir = projectDir,
713
+ lease_ttl_ms, clock,
714
+ } = {}) {
715
+ let context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
716
+ const effectiveOperator = operator_id ?? context.claim.operator_id;
717
+ const currentProcessId = `process://chati/${process.pid}`;
718
+ const observedNow = typeof clock === 'function' ? clock() : new Date();
719
+ const observedNowMs = observedNow instanceof Date ? observedNow.getTime() : Date.parse(observedNow);
720
+ let recovered = false;
721
+ if (context.claim.harness_process_id !== currentProcessId
722
+ || Date.parse(context.claim.lease.expires_at) <= observedNowMs) {
723
+ recoverRailTask({
724
+ projectDir, handoff_id, attempt_id, operator_id: effectiveOperator, git_dir,
725
+ ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
726
+ ...(clock === undefined ? {} : { clock }),
727
+ });
728
+ context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
729
+ recovered = true;
730
+ }
731
+ const renewed = heartbeatRailTask({
732
+ projectDir, handoff_id, attempt_id,
733
+ claim_id: context.claim.claim_id, operator_id: effectiveOperator,
734
+ ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
735
+ ...(clock === undefined ? {} : { clock }),
736
+ });
737
+ context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
738
+ return Object.freeze({ context, renewed, recovered, operator_id: effectiveOperator });
739
+ }
740
+
741
+ /** Resolve the latest canonical claim and task binding for an active attempt. */
742
+ export function resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock } = {}) {
743
+ const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
744
+ const claimed = engine.records.find((record) => record.type === 'attempt_claimed'
745
+ && record.payload?.attempt?.attempt_id === attempt_id);
746
+ if (!claimed) fail('RAIL_ATTEMPT_NOT_FOUND', `attempt ${attempt_id} is absent from the RAIL journal`);
747
+ if (claimed.payload.attempt.handoff_id !== handoff_id) fail('RAIL_ATTEMPT_HANDOFF_MISMATCH', 'attempt does not belong to the requested handoff');
748
+ let claim = claimed.payload.claim;
749
+ let state = 'active';
750
+ for (const record of engine.records) {
751
+ if (record.payload?.attempt_id === attempt_id && record.type === 'attempt_heartbeat') claim = { ...claim, lease: record.payload.lease };
752
+ if (record.payload?.attempt_id === attempt_id && record.type === 'attempt_relinquished') claim = { ...claim, lease: record.payload.lease };
753
+ if (record.payload?.attempt_id === attempt_id && record.type === 'attempt_recovered') { claim = record.payload.claim; state = record.payload.resume_state ?? 'active'; }
754
+ if (record.type === 'attempt_completed' && record.payload?.completion?.attempt_id === attempt_id) state = record.payload.completion.state;
755
+ if (record.payload?.attempt_id === attempt_id && record.type === 'attempt_blocked') state = 'blocked';
756
+ if (record.payload?.attempt_id === attempt_id && record.type === 'adjudication_decided'
757
+ && adjudicationAttemptState(record.payload?.decision) === 'cancelled') state = 'cancelled';
758
+ if (record.payload?.attempt_id === attempt_id && record.type === 'attempt_cancelled') state = 'cancelled';
759
+ }
760
+ const task = handoff.tasks.find((item) => item.task_id === claimed.payload.attempt.task_id);
761
+ if (!task) fail('TASK_NOT_FOUND', 'attempt task is absent from the sealed handoff');
762
+ return Object.freeze({
763
+ handoff,
764
+ task,
765
+ attempt: claimed.payload.attempt,
766
+ claim: Object.freeze(claim),
767
+ state,
768
+ records: engine.records,
769
+ language: projectRailLanguage(projectDir),
770
+ });
771
+ }
772
+
773
+ /** Expire the current supervisor lease without transferring process ownership. */
774
+ export function relinquishRailTask({ projectDir, handoff_id, attempt_id, claim_id, operator_id, reason, clock } = {}) {
775
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
776
+ return engine.relinquishAttempt({
777
+ attempt_id, claim_id, operator_id,
778
+ harness_process_id: `process://chati/${process.pid}`,
779
+ ...(reason === undefined ? {} : { reason }),
780
+ });
781
+ }
782
+
783
+ /** Atomically reserves one mutating rework for the active claim generation. */
784
+ export function reserveRailRework({
785
+ projectDir, handoff_id, attempt_id, source_decision_ref, reservation_id, clock,
786
+ } = {}) {
787
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
788
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
789
+ return engine.reserveRework({
790
+ attempt_id,
791
+ claim_id: context.claim.claim_id,
792
+ operator_id: context.claim.operator_id,
793
+ harness_process_id: `process://chati/${process.pid}`,
794
+ source_decision_ref,
795
+ reservation_id,
796
+ });
797
+ }
798
+
799
+ /** Persist the fail-closed rework launch intent before a provider process is created. */
800
+ export function reserveRailReworkLaunch({
801
+ projectDir, handoff_id, attempt_id, source_decision_ref, reservation_id, invocation, clock,
802
+ } = {}) {
803
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
804
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
805
+ return engine.reserveReworkLaunch({
806
+ attempt_id,
807
+ claim_id: context.claim.claim_id,
808
+ operator_id: context.claim.operator_id,
809
+ harness_process_id: `process://chati/${process.pid}`,
810
+ source_decision_ref,
811
+ reservation_id,
812
+ invocation,
813
+ });
814
+ }
815
+
816
+ /** Releases a rework reservation only after this supervisor stopped its provider. */
817
+ export function releaseRailReworkReservation({
818
+ projectDir, handoff_id, attempt_id, source_decision_ref, reservation_id, reason, clock,
819
+ } = {}) {
820
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
821
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
822
+ return engine.releaseReworkReservation({
823
+ attempt_id,
824
+ claim_id: context.claim.claim_id,
825
+ operator_id: context.claim.operator_id,
826
+ harness_process_id: `process://chati/${process.pid}`,
827
+ source_decision_ref,
828
+ reservation_id,
829
+ ...(reason === undefined ? {} : { reason }),
830
+ });
831
+ }
832
+
833
+ /** Persist executor output only through the claim-bound canonical RAIL journal. */
834
+ export function recordRailExecutionEvidence({
835
+ projectDir, handoff_id, attempt_id, invocation, execution, base_commit_ref,
836
+ result_commit_ref = null, run_kind = 'initial', source_decision_ref = null,
837
+ clock,
838
+ } = {}) {
839
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
840
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
841
+ if (!verifyTerminalAuthorityReceipt(execution?.terminal_authority_receipt, {
842
+ purpose: 'terminal_completion', authority_kind: 'rail_execution',
843
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
844
+ terminal_id: execution?.terminal_id, provider_id: invocation?.provider_id,
845
+ harness_id: invocation?.harness_id, model_id: invocation?.model_pin?.model_id,
846
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
847
+ exit_status: execution?.exit_status,
848
+ execution_base_commit_ref: base_commit_ref,
849
+ })) fail('UNTRUSTED_EXECUTION_EVIDENCE', 'execution evidence requires a completion receipt minted by the terminal authority');
850
+ if (typeof execution?.stdout !== 'string' || typeof execution?.stderr !== 'string'
851
+ || execution.terminal_authority_receipt.stdout_digest !== sha256(execution.stdout)
852
+ || execution.terminal_authority_receipt.stderr_digest !== sha256(execution.stderr)) {
853
+ fail('EXECUTION_TERMINAL_OUTPUT_MISMATCH', 'execution logs must be the exact stdout and stderr signed by the terminal authority');
854
+ }
855
+ const evidence = engine.recordExecutionEvidence({
856
+ attempt_id,
857
+ claim_id: context.claim.claim_id,
858
+ operator_id: context.claim.operator_id,
859
+ invocation,
860
+ execution,
861
+ base_commit_ref,
862
+ result_commit_ref,
863
+ run_kind,
864
+ source_decision_ref,
865
+ receipt_nonce: invocation?.launch_nonce,
866
+ git_dir: projectDir,
867
+ });
868
+ return Object.freeze({
869
+ ...evidence,
870
+ evidence_ref: `rail-execution://${handoff_id}/${attempt_id}/${evidence.evidence_id}`,
871
+ });
872
+ }
873
+
874
+ /** Persist the fail-closed initial launch intent before a provider process is created. */
875
+ export function reserveRailExecutionLaunch({
876
+ projectDir, handoff_id, attempt_id, invocation, clock,
877
+ } = {}) {
878
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
879
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
880
+ return engine.reserveExecutionLaunch({
881
+ attempt_id,
882
+ claim_id: context.claim.claim_id,
883
+ operator_id: context.claim.operator_id,
884
+ harness_process_id: `process://chati/${process.pid}`,
885
+ invocation,
886
+ });
887
+ }
888
+
889
+ /** Journal the exact launched terminal binding before any output may become evidence. */
890
+ export function recordRailExecutionDispatch({
891
+ projectDir, handoff_id, attempt_id, invocation, launch_receipt, prompt,
892
+ browser_manifest = null, run_kind = 'initial', clock,
893
+ } = {}) {
894
+ const { engine, handoff, handoff_path } = loadProjectRail({ projectDir, handoff_id, clock });
895
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
896
+ let canonicalPrompt;
897
+ let canonicalBaseCommitRef;
898
+ if (run_kind === 'rework') {
899
+ const latestControl = context.records.filter((record) => ['review_decided', 'adjudication_decided'].includes(record.type)
900
+ && record.payload?.attempt_id === attempt_id).at(-1);
901
+ const reviewRef = latestControl?.type === 'review_decided' ? latestControl.payload.review_ref : latestControl?.payload?.review_ref;
902
+ const sourceReview = engine.reviewEvidenceForAttempt(attempt_id)
903
+ .find((entry) => `rail-review://${handoff_id}/${attempt_id}/${entry.review?.review_id}` === reviewRef)?.review;
904
+ const sourceAdjudication = latestControl?.type === 'adjudication_decided'
905
+ ? {
906
+ adjudication_ref: latestControl.payload.adjudication_ref,
907
+ review_ref: latestControl.payload.review_ref,
908
+ decision: latestControl.payload.decision,
909
+ evidence: latestControl.payload.evidence,
910
+ }
911
+ : null;
912
+ const executionHistory = engine.executionEvidenceForAttempt(attempt_id);
913
+ const baseCommitRef = executionHistory.filter((entry) => typeof entry.result_commit_ref === 'string').at(-1)?.result_commit_ref;
914
+ if (!sourceReview || !baseCommitRef) fail('NONCANONICAL_EXECUTION_PROMPT', 'rework prompt requires its canonical review and base execution');
915
+ canonicalBaseCommitRef = baseCommitRef;
916
+ canonicalPrompt = buildRailReworkPrompt({
917
+ projectDir, context, sourceReview, sourceAdjudication, executionHistory, baseCommitRef, invocation,
918
+ });
919
+ } else {
920
+ canonicalBaseCommitRef = context.attempt.git_commit_ref;
921
+ canonicalPrompt = buildRailTaskPrompt({
922
+ projectDir, stored: { path: handoff_path, handoff }, task: context.task, invocation,
923
+ browserManifest: browser_manifest, language: invocation?.language ?? context.language,
924
+ });
925
+ }
926
+ if (prompt !== canonicalPrompt || invocation?.prompt_digest !== sha256(canonicalPrompt)) {
927
+ fail('NONCANONICAL_EXECUTION_PROMPT', 'execution dispatch must use the exact canonical prompt for this attempt');
928
+ }
929
+ if (!verifyTerminalAuthorityReceipt(launch_receipt, {
930
+ purpose: 'terminal_launch', authority_kind: 'rail_execution',
931
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
932
+ provider_id: invocation?.provider_id, harness_id: invocation?.harness_id,
933
+ model_id: invocation?.model_pin?.model_id,
934
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
935
+ task_id: context.task.task_id,
936
+ working_dir: resolve(projectDir),
937
+ prompt_digest: invocation.prompt_digest,
938
+ execution_base_commit_ref: canonicalBaseCommitRef,
939
+ execution_launch_head_ref: canonicalBaseCommitRef,
940
+ execution_worktree_clean: true,
941
+ })) fail('UNTRUSTED_EXECUTION_DISPATCH', 'execution dispatch requires a launch receipt minted by the terminal authority');
942
+ if (!/^[a-f0-9]{64}$/.test(launch_receipt.execution_repository_identity_digest || '')
943
+ || !/^[a-f0-9]{64}$/.test(launch_receipt.execution_preexisting_commits_digest || '')) {
944
+ fail('UNTRUSTED_EXECUTION_DISPATCH', 'execution dispatch requires signed repository and preexisting-object baselines');
945
+ }
946
+ return engine.recordExecutionDispatch({
947
+ attempt_id,
948
+ claim_id: context.claim.claim_id,
949
+ operator_id: context.claim.operator_id,
950
+ invocation,
951
+ launch_receipt,
952
+ project_dir: projectDir,
953
+ browser_manifest,
954
+ run_kind,
955
+ ...(run_kind === 'rework' ? {
956
+ source_decision_ref: invocation?.source_decision_ref,
957
+ rework_reservation_id: invocation?.rework_reservation_id,
958
+ } : {}),
959
+ });
960
+ }
961
+
962
+ export function listRailExecutionEvidence({ projectDir, attempt_id, successful_only = false, clock } = {}) {
963
+ const engine = createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) });
964
+ return engine.executionEvidenceForAttempt(attempt_id, { successful_only });
965
+ }
966
+
967
+ export function loadLatestRailExecutionEvidence({ projectDir, attempt_id, clock } = {}) {
968
+ const records = listRailExecutionEvidence({ projectDir, attempt_id, successful_only: true, clock });
969
+ if (records.length === 0) fail('RAIL_EXECUTION_EVIDENCE_REQUIRED', `attempt ${attempt_id} has no successful canonical execution evidence`);
970
+ return records.at(-1);
971
+ }
972
+
973
+ /** 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 } = {}) {
365
975
  const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
366
976
  return engine.recoverAttempt({
367
- attempt_id, operator_id, harness_process_id, reconciliation, git_dir,
977
+ attempt_id, operator_id, harness_process_id: `process://chati/${process.pid}`, git_dir,
368
978
  ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
369
979
  });
370
980
  }
371
981
 
982
+ function canonicalAdjudicationRound(records, attemptId, reviewRef, selected) {
983
+ const observations = records
984
+ .filter((record) => record.type === 'adjudication_observed'
985
+ && record.payload?.attempt_id === attemptId
986
+ && record.payload?.review_ref === reviewRef)
987
+ .map((record) => record.payload.observation);
988
+ const orderedRound = (round) => selected.map((candidate) => observations.find((entry) =>
989
+ entry.round === round
990
+ && entry.invocation.provider_id === candidate.provider_id
991
+ && entry.invocation.harness_id === candidate.harness_id
992
+ && sha256(entry.invocation.model_pin) === sha256(candidate.model_pin))).filter(Boolean);
993
+ const roundOne = orderedRound(1);
994
+ if (roundOne.length < selected.length) return { round: 1, prior_decisions: [] };
995
+ if (adjudicationEntriesAgree(roundOne)) {
996
+ fail('ADJUDICATION_ROUND_COMPLETE', 'the canonical first adjudication round already reached consensus');
997
+ }
998
+ const roundTwo = orderedRound(2);
999
+ if (roundTwo.length === selected.length) fail('ADJUDICATION_ROUND_COMPLETE', 'the canonical second adjudication round is complete');
1000
+ return {
1001
+ round: 2,
1002
+ prior_decisions: roundOne.map((entry, index) => ({
1003
+ anonymous_ref: `independent-decision-${index + 1}`,
1004
+ decision: entry.decision,
1005
+ })),
1006
+ };
1007
+ }
1008
+
1009
+ /** Records only a unanimous, selected-model adjudication against an active attempt. */
1010
+ export function recordRailAdjudicationDispatch({
1011
+ projectDir, handoff_id, attempt_id, review_ref, invocation, launch_receipt,
1012
+ prompt, reviewWorkspace, clock,
1013
+ } = {}) {
1014
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
1015
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
1016
+ const plan = context.records.filter((record) => record.type === 'review_decided'
1017
+ && record.payload?.attempt_id === attempt_id).at(-1)?.payload?.decision;
1018
+ if (plan?.decision !== 'AI_ADJUDICATION' || !Array.isArray(plan.adjudicators) || plan.adjudicators.length === 0) {
1019
+ fail('RAIL_ADJUDICATION_NOT_AUTHORIZED', 'the latest canonical review decision does not authorize adjudication');
1020
+ }
1021
+ const roundState = canonicalAdjudicationRound(
1022
+ engine.records, attempt_id, review_ref, plan?.adjudicators || [],
1023
+ );
1024
+ const execution = loadLatestRailExecutionEvidence({ projectDir, attempt_id, clock });
1025
+ if (!reviewWorkspace
1026
+ || reviewWorkspace.canonical_project_dir !== realpathSync(resolve(projectDir))
1027
+ || reviewWorkspace.result_commit_ref !== execution.result_commit_ref) {
1028
+ fail('RAIL_ADJUDICATION_WORKSPACE_MISMATCH', 'adjudication requires an immutable workspace for the exact result commit');
1029
+ }
1030
+ const workspaceState = assertRailReadOnlyCommitWorkspaceUnchanged({
1031
+ workspace: reviewWorkspace,
1032
+ label: 'RAIL adjudication dispatch',
1033
+ });
1034
+ const reviewSubject = {
1035
+ result_commit_ref: execution.result_commit_ref,
1036
+ workspace_digest: sha256(workspaceState),
1037
+ };
1038
+ const canonicalPrompt = buildRailAdjudicationPrompt({
1039
+ projectDir: reviewWorkspace.subject_dir, context, plan, invocation,
1040
+ reviews: engine.reviewEvidenceForAttempt(attempt_id),
1041
+ executions: engine.executionEvidenceForAttempt(attempt_id),
1042
+ priorDecisions: roundState.prior_decisions,
1043
+ round: roundState.round,
1044
+ reviewSubject,
1045
+ });
1046
+ if (prompt !== canonicalPrompt || invocation?.prompt_digest !== sha256(canonicalPrompt)
1047
+ || invocation?.adjudication_round !== roundState.round
1048
+ || invocation?.prior_decisions_digest !== sha256(roundState.prior_decisions)) {
1049
+ fail('NONCANONICAL_ADJUDICATION_PROMPT', 'adjudication dispatch must use the exact canonical prompt for this attempt and round');
1050
+ }
1051
+ if (!verifyTerminalAuthorityReceipt(launch_receipt, {
1052
+ purpose: 'terminal_launch', authority_kind: 'rail_adjudication',
1053
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
1054
+ provider_id: invocation?.provider_id, harness_id: invocation?.harness_id,
1055
+ model_id: invocation?.model_pin?.model_id,
1056
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
1057
+ task_id: context.task.task_id,
1058
+ working_dir: resolve(reviewWorkspace.subject_dir),
1059
+ prompt_digest: invocation.prompt_digest,
1060
+ })) fail('UNTRUSTED_ADJUDICATION_LAUNCH', 'adjudication dispatch requires a terminal-authority launch receipt');
1061
+ return engine.recordAdjudicationDispatch({
1062
+ attempt_id, review_ref, invocation, launch_receipt,
1063
+ project_dir: projectDir,
1064
+ working_dir: reviewWorkspace.subject_dir,
1065
+ });
1066
+ }
1067
+
1068
+ export function recordRailAdjudicationObservation({
1069
+ projectDir, handoff_id, attempt_id, review_ref, entry, clock,
1070
+ } = {}) {
1071
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
1072
+ const parsedDecision = parseAuthorizedAdjudicationOutput(entry?.adjudication_output);
1073
+ if (sha256(parsedDecision) !== sha256(validateAdjudicationDecision(entry?.decision))) {
1074
+ fail('RAIL_ADJUDICATION_PAYLOAD_MISMATCH', 'caller decision differs from the terminal-authorized adjudication output');
1075
+ }
1076
+ const receipt = entry?.terminal_authority_receipt;
1077
+ const invocation = entry?.invocation;
1078
+ if (!verifyTerminalAuthorityReceipt(receipt, {
1079
+ purpose: 'terminal_completion', authority_kind: 'rail_adjudication',
1080
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
1081
+ provider_id: invocation?.provider_id, harness_id: invocation?.harness_id,
1082
+ model_id: invocation?.model_pin?.model_id,
1083
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
1084
+ exit_status: 0,
1085
+ }) || receipt.stdout_digest !== sha256(entry.adjudication_output)) {
1086
+ fail('UNTRUSTED_ADJUDICATION_EVIDENCE', 'adjudication observation is not bound to an authorized terminal completion');
1087
+ }
1088
+ return engine.recordAdjudicationObservation({
1089
+ attempt_id, review_ref, invocation, decision: parsedDecision,
1090
+ adjudication_output: entry.adjudication_output,
1091
+ terminal_authority_receipt: receipt,
1092
+ });
1093
+ }
1094
+
1095
+ export function recordRailAdjudication({ projectDir, handoff_id, attempt_id, review_ref, evidence, clock } = {}) {
1096
+ const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
1097
+ if (!Array.isArray(evidence) || evidence.length === 0) fail('INVALID_ADJUDICATION_RECORD', 'adjudication evidence must be a non-empty array');
1098
+ const authorizedEvidence = evidence.map((entry) => {
1099
+ const invocation = entry?.invocation;
1100
+ const receipt = entry?.terminal_authority_receipt;
1101
+ const parsedDecision = parseAuthorizedAdjudicationOutput(entry?.adjudication_output);
1102
+ if (sha256(parsedDecision) !== sha256(validateAdjudicationDecision(entry?.decision))) {
1103
+ fail('RAIL_ADJUDICATION_PAYLOAD_MISMATCH', 'caller decision differs from the terminal-authorized adjudication output');
1104
+ }
1105
+ const expected = {
1106
+ purpose: 'terminal_completion', authority_kind: 'rail_adjudication',
1107
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
1108
+ provider_id: invocation?.provider_id, harness_id: invocation?.harness_id,
1109
+ model_id: invocation?.model_pin?.model_id,
1110
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
1111
+ exit_status: 0,
1112
+ };
1113
+ if (!verifyTerminalAuthorityReceipt(receipt, expected)
1114
+ || receipt.stdout_digest !== sha256(entry.adjudication_output)) {
1115
+ fail('UNTRUSTED_ADJUDICATION_EVIDENCE', 'adjudication evidence is not bound to a successful authorized terminal completion');
1116
+ }
1117
+ return Object.freeze({
1118
+ invocation,
1119
+ decision: parsedDecision,
1120
+ adjudication_output: entry.adjudication_output,
1121
+ terminal_authority_receipt: receipt,
1122
+ });
1123
+ });
1124
+ return engine.recordAdjudicationDecision({ attempt_id, review_ref, evidence: authorizedEvidence });
1125
+ }
1126
+
372
1127
  /**
373
1128
  * Write an immutable C-06 completion record. Git, CI, review and acceptance
374
1129
  * evidence are validated by RailEngine against the project-local journal.
375
1130
  */
376
- export function completeRailTask({ projectDir, handoff_id, completion_id, attempt_id, claim_id, operator_id, state, git, ci, reviews, acceptance_evidence_refs, completed_at, metrics, clock } = {}) {
1131
+ export function completeRailTask({
1132
+ projectDir, handoff_id, completion_id, attempt_id, claim_id, operator_id, state,
1133
+ git, ci, reviews, acceptance_evidence_refs, acceptance_authority_receipt,
1134
+ browser_evidence, completed_at, metrics, clock,
1135
+ } = {}) {
377
1136
  const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
1137
+ const ownership = acquireRailTaskSupervisor({
1138
+ projectDir, handoff_id, attempt_id, operator_id,
1139
+ ...(clock === undefined ? {} : { clock }),
1140
+ });
1141
+ if (claim_id !== ownership.context.claim.claim_id) fail('CLAIM_OWNER_MISMATCH', 'completion claim_id does not match the canonical attempt claim');
1142
+ let shouldRelinquish = true;
1143
+ try {
378
1144
  const trackingRequired = internalTrackingInstallation(projectDir, clock);
379
1145
  if (trackingRequired && state === 'merged') assertCompletionMetricsEvidence(metrics, reviews, acceptance_evidence_refs, engine.records, attempt_id);
380
1146
  const taskId = engine.records.find((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.attempt_id === attempt_id)?.payload?.attempt?.task_id;
@@ -384,7 +1150,9 @@ export function completeRailTask({ projectDir, handoff_id, completion_id, attemp
384
1150
  let completion;
385
1151
  try {
386
1152
  completion = engine.completeAttempt({
387
- completion_id, attempt_id, claim_id, operator_id, state, git: { ...git, git_dir: projectDir }, ci, reviews, acceptance_evidence_refs,
1153
+ completion_id, attempt_id, claim_id, operator_id: ownership.operator_id, state, git: { ...git, git_dir: projectDir }, ci, reviews, acceptance_evidence_refs,
1154
+ acceptance_authority_receipt,
1155
+ ...(browser_evidence === undefined ? {} : { browser_evidence }),
388
1156
  ...(completed_at === undefined ? {} : { completed_at }), ...(metrics === undefined ? {} : { metrics }), handoff,
389
1157
  });
390
1158
  } catch (error) {
@@ -401,7 +1169,19 @@ export function completeRailTask({ projectDir, handoff_id, completion_id, attemp
401
1169
  const tracking_projection = trackingRequired
402
1170
  ? replayPendingClickUpProjections({ projectDir, handoff_id, ...(clock === undefined ? {} : { clock }) }).find((item) => item.path === intentPath)
403
1171
  : undefined;
1172
+ if (['merged', 'blocked', 'cancelled'].includes(completion.state)) shouldRelinquish = false;
404
1173
  return Object.freeze({ ...result, ...(tracking_projection ? { tracking_projection } : {}) });
1174
+ } finally {
1175
+ if (shouldRelinquish) {
1176
+ relinquishRailTask({
1177
+ projectDir, handoff_id, attempt_id,
1178
+ claim_id: ownership.context.claim.claim_id,
1179
+ operator_id: ownership.operator_id,
1180
+ reason: 'action_complete',
1181
+ ...(clock === undefined ? {} : { clock }),
1182
+ });
1183
+ }
1184
+ }
405
1185
  }
406
1186
 
407
1187
  /** Resolves only a completion that exists in the immutable project-local journal. */