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.
- package/README.md +8 -3
- package/bin/chati.js +124 -66
- package/framework/agents/build/dev.md +5 -5
- package/framework/agents/deploy/devops.md +7 -7
- package/framework/agents/discover/brief.md +4 -4
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +2 -2
- package/framework/agents/plan/tasks.md +2 -2
- package/framework/agents/plan/ux.md +2 -2
- package/framework/agents/quality/qa-implementation.md +11 -10
- package/framework/agents/quality/qa-planning.md +3 -3
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +18 -18
- package/framework/context/protocols.md +1 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +4 -4
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/orchestrator.yaml +2 -2
- package/framework/domains/constitution.yaml +1 -1
- package/framework/hooks/advance-trigger.js +4 -6
- package/framework/hooks/git-push-authority.js +45 -37
- package/framework/hooks/mode-governance.js +149 -40
- package/framework/hooks/model-governance.js +13 -20
- package/framework/hooks/prism-engine.js +74 -92
- package/framework/hooks/reasoning-escalator.js +23 -40
- package/framework/hooks/session-digest.js +12 -13
- package/framework/hooks/session-reader.js +224 -0
- package/framework/hooks/session-writer.js +195 -0
- package/framework/hooks/team-quality-gate.js +34 -24
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +4 -4
- package/framework/intelligence/memory-layer.md +1 -1
- package/framework/manifest.json +129 -119
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +252 -25
- package/framework/orchestrator/chati.md +238 -70
- package/framework/schemas/session.schema.json +1 -1
- package/framework/tasks/orchestrator-deviation.md +1 -1
- package/framework/tasks/orchestrator-escalate.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +6 -6
- package/framework/tasks/orchestrator-health.md +5 -9
- package/framework/tasks/orchestrator-mode-switch.md +3 -7
- package/framework/tasks/orchestrator-resume.md +10 -14
- package/framework/tasks/orchestrator-route.md +3 -3
- package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
- package/framework/tasks/orchestrator-status.md +9 -9
- package/framework/tasks/orchestrator-suggest-mode.md +1 -1
- package/framework/tasks/qa-impl-consolidate.md +2 -2
- package/framework/tasks/qa-impl-performance-test.md +4 -4
- package/framework/tasks/qa-impl-regression-check.md +4 -4
- package/framework/tasks/qa-impl-sast-scan.md +1 -1
- package/framework/tasks/qa-impl-test-execute.md +1 -1
- package/framework/tasks/qa-impl-verdict.md +2 -2
- package/framework/tasks/qa-planning-consolidate.md +3 -3
- package/framework/tasks/qa-planning-coverage-plan.md +2 -2
- package/framework/tasks/qa-planning-gate-define.md +4 -4
- package/framework/tasks/qa-planning-risk-matrix.md +4 -4
- package/framework/tasks/qa-planning-test-strategy.md +2 -2
- package/node_modules/@chati/browser-capability/src/index.js +12 -2
- package/node_modules/@chati/planning/src/index.js +24 -4
- package/node_modules/@chati/provider-registry/src/index.js +11 -0
- package/node_modules/@chati/rail/src/index.js +1967 -83
- package/node_modules/@chati/release-lane/README.md +12 -8
- package/node_modules/@chati/release-lane/package.json +1 -1
- package/node_modules/@chati/release-lane/src/index.js +1426 -58
- package/node_modules/@chati/review-council/src/index.js +63 -0
- package/node_modules/@chati/tracking-clickup/README.md +13 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
- package/package-artifact-manifest.json +1 -0
- package/package-artifact-manifest.sig +1 -0
- package/package.json +16 -7
- package/scripts/verify-real-harness-e2e.js +1581 -0
- package/src/config/framework-adapter.js +4 -4
- package/src/installer/core.js +148 -53
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +65 -20
- package/src/installer-v2/catalog-client.js +531 -23
- package/src/installer-v2/index.js +91 -34
- package/src/installer-v2/installation-authority.js +327 -0
- package/src/installer-v2/provider-executable.js +235 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1545 -174
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +16 -3
- package/src/orchestrator/index.js +16 -0
- package/src/orchestrator/planning-runtime.js +20 -2
- package/src/orchestrator/rail-adjudication-evidence.js +234 -0
- package/src/orchestrator/rail-evidence-authority.js +147 -0
- package/src/orchestrator/rail-execution-evidence.js +45 -0
- package/src/orchestrator/rail-runtime.js +836 -56
- package/src/orchestrator/release-runtime.js +65 -6
- package/src/orchestrator/review-runtime.js +186 -41
- package/src/orchestrator/runtime-installation-v2.js +236 -20
- package/src/orchestrator/session-manager.js +1167 -53
- package/src/terminal/adapters/claude-adapter.js +3 -1
- package/src/terminal/adapters/codex-adapter.js +2 -0
- package/src/terminal/adapters/grok-adapter.js +7 -4
- package/src/terminal/handoff-parser.js +19 -1
- package/src/terminal/prompt-builder.js +9 -1
- package/src/terminal/provider-preflight.js +36 -3
- package/src/terminal/rail-execution-worktree.js +213 -0
- package/src/terminal/rail-prompts.js +169 -0
- package/src/terminal/rail-readonly-workspace.js +324 -0
- package/src/terminal/run-agent.js +384 -24
- package/src/terminal/run-parallel.js +5 -0
- package/src/terminal/run-rail-adjudication.js +323 -0
- package/src/terminal/run-rail-review.js +291 -0
- package/src/terminal/run-rail-rework.js +325 -0
- package/src/terminal/run-rail-task.js +380 -0
- package/src/terminal/run-team.js +5 -0
- package/src/terminal/spawner.js +1225 -77
- package/src/wizard/index.js +3 -2
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
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 (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
|
149
|
-
const path = join(directory, filename);
|
|
357
|
+
for (const path of listClickUpDocumentPaths(directory)) {
|
|
150
358
|
let intent;
|
|
151
|
-
try { intent =
|
|
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
|
|
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(
|
|
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,
|
|
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
|
|
285
|
-
|
|
286
|
-
const
|
|
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 =
|
|
573
|
+
const invocationBase = resolveRuntimeRailInvocationV2({
|
|
299
574
|
artifact: installation,
|
|
300
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
364
|
-
|
|
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
|
|
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({
|
|
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. */
|