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