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
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { validateAdjudicationDecision } from '@chati/review-council';
|
|
7
|
+
import { sha256 } from '@chati/core';
|
|
8
|
+
import { adjudicationEntriesAgree, deriveAdjudicationConsensus } from '@chati/rail';
|
|
9
|
+
import {
|
|
10
|
+
buildSpawnCommand, getTerminalCompletionAuthorityReceipt, getTerminalLaunchAuthorityReceipt,
|
|
11
|
+
isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
|
|
12
|
+
} from './spawner.js';
|
|
13
|
+
import { checkProviderReadiness } from './provider-preflight.js';
|
|
14
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
15
|
+
import { startClaimHeartbeat } from './run-rail-task.js';
|
|
16
|
+
import {
|
|
17
|
+
acquireRailTaskSupervisor, createProjectRailEngine, recordRailAdjudication,
|
|
18
|
+
recordRailAdjudicationDispatch, recordRailAdjudicationObservation,
|
|
19
|
+
relinquishRailTask, resolveRailAttemptContext,
|
|
20
|
+
} from '../orchestrator/rail-runtime.js';
|
|
21
|
+
import { listRailExecutionEvidence } from '../orchestrator/rail-runtime.js';
|
|
22
|
+
import { listRailReviewEvidence, resolveReviewGitSubject } from '../orchestrator/review-runtime.js';
|
|
23
|
+
import { persistRailAdjudicationEvidence } from '../orchestrator/rail-adjudication-evidence.js';
|
|
24
|
+
import { buildRailAdjudicationPrompt } from './rail-prompts.js';
|
|
25
|
+
import {
|
|
26
|
+
assertRailReadOnlyCommitWorkspaceUnchanged, cleanupRailReadOnlyCommitWorkspace,
|
|
27
|
+
createRailReadOnlyCommitWorkspace,
|
|
28
|
+
} from './rail-readonly-workspace.js';
|
|
29
|
+
import {
|
|
30
|
+
loadRuntimeInstallationV2, resolveRuntimeExecutableBindingV2,
|
|
31
|
+
} from '../orchestrator/runtime-installation-v2.js';
|
|
32
|
+
|
|
33
|
+
export { buildRailAdjudicationPrompt } from './rail-prompts.js';
|
|
34
|
+
|
|
35
|
+
function fail(code, message, details = {}) { const error = new Error(message); error.code = code; error.details = details; throw error; }
|
|
36
|
+
|
|
37
|
+
function parseDecision(text) {
|
|
38
|
+
const startToken = '<CHATI_RAIL_ADJUDICATION>';
|
|
39
|
+
const endToken = '</CHATI_RAIL_ADJUDICATION>';
|
|
40
|
+
const starts = [...text.matchAll(new RegExp(startToken, 'g'))];
|
|
41
|
+
const ends = [...text.matchAll(new RegExp(endToken, 'g'))];
|
|
42
|
+
if (starts.length !== 1 || ends.length !== 1) {
|
|
43
|
+
return fail('RAIL_ADJUDICATION_OUTPUT_INVALID', 'adjudicator must return exactly one required envelope');
|
|
44
|
+
}
|
|
45
|
+
const start = starts[0].index;
|
|
46
|
+
const end = text.indexOf(endToken, start + startToken.length);
|
|
47
|
+
if (end < start) return fail('RAIL_ADJUDICATION_OUTPUT_INVALID', 'adjudicator returned malformed envelope ordering');
|
|
48
|
+
try { return validateAdjudicationDecision(JSON.parse(text.slice(start + startToken.length, end).trim())); }
|
|
49
|
+
catch (error) { return fail(error.code || 'RAIL_ADJUDICATION_OUTPUT_INVALID', error.message); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function latestAdjudicationPlan(context) {
|
|
53
|
+
const latest = context.records.filter((record) => record.type === 'review_decided'
|
|
54
|
+
&& record.payload?.attempt_id === context.attempt.attempt_id).at(-1);
|
|
55
|
+
if (latest?.payload?.decision?.decision !== 'AI_ADJUDICATION') {
|
|
56
|
+
fail('RAIL_ADJUDICATION_NOT_AUTHORIZED', 'the latest canonical review decision does not authorize AI adjudication');
|
|
57
|
+
}
|
|
58
|
+
return latest.payload;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function invokeAdjudicator({ projectDir, artifact, context, plan, review_ref, invocation: selectedInvocation, reviews, executions, priorDecisions, round, timeout, max_retries, onHandle, reviewWorkspace, reviewSubject, assertLease = () => {} }) {
|
|
62
|
+
const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
|
|
63
|
+
artifact, provider_id: selectedInvocation.provider_id, harness_id: selectedInvocation.harness_id,
|
|
64
|
+
});
|
|
65
|
+
const readiness = checkProviderReadiness(selectedInvocation.harness_id, {
|
|
66
|
+
workingDir: projectDir, executableBinding: providerExecutableBinding,
|
|
67
|
+
});
|
|
68
|
+
if (!readiness.ready) fail(readiness.code || 'PROVIDER_NOT_READY', readiness.detail || `${selectedInvocation.harness_id} is not ready`);
|
|
69
|
+
const spawnAdjudicationAttempt = async (retry) => {
|
|
70
|
+
const invocationDraft = {
|
|
71
|
+
...selectedInvocation,
|
|
72
|
+
attempt_id: context.attempt.attempt_id,
|
|
73
|
+
invocation_id: `rail-adjudication-${context.attempt.attempt_id}-${round}-${selectedInvocation.provider_id}-${retry + 1}`,
|
|
74
|
+
launch_nonce: randomUUID(),
|
|
75
|
+
adjudication_round: round,
|
|
76
|
+
prior_decisions_digest: sha256(priorDecisions),
|
|
77
|
+
language: context.language,
|
|
78
|
+
};
|
|
79
|
+
const prompt = buildRailAdjudicationPrompt({
|
|
80
|
+
projectDir: reviewWorkspace.subject_dir,
|
|
81
|
+
context,
|
|
82
|
+
plan,
|
|
83
|
+
invocation: invocationDraft,
|
|
84
|
+
reviews,
|
|
85
|
+
executions,
|
|
86
|
+
priorDecisions,
|
|
87
|
+
round,
|
|
88
|
+
reviewSubject,
|
|
89
|
+
});
|
|
90
|
+
const exactInvocation = Object.freeze({ ...invocationDraft, prompt_digest: sha256(prompt) });
|
|
91
|
+
const config = {
|
|
92
|
+
agent: 'rail-adjudication', taskId: context.task.task_id,
|
|
93
|
+
provider: exactInvocation.harness_id, providerId: exactInvocation.provider_id,
|
|
94
|
+
model: exactInvocation.model_pin.model_id,
|
|
95
|
+
reasoningConfiguration: exactInvocation.model_pin.reasoning_configuration,
|
|
96
|
+
catalogSnapshotRef: exactInvocation.model_pin.catalog_snapshot_ref,
|
|
97
|
+
providerExecutableBinding,
|
|
98
|
+
prompt,
|
|
99
|
+
workingDir: reviewWorkspace.subject_dir, timeout, writeScope: [],
|
|
100
|
+
auditProjectDir: projectDir,
|
|
101
|
+
maskedReadPaths: [projectDir],
|
|
102
|
+
authorityContext: {
|
|
103
|
+
authority_kind: 'rail_adjudication', invocation_id: exactInvocation.invocation_id,
|
|
104
|
+
launch_nonce: exactInvocation.launch_nonce, provider_id: exactInvocation.provider_id,
|
|
105
|
+
},
|
|
106
|
+
beforeProcessSpawn: () => assertRailReadOnlyCommitWorkspaceUnchanged({
|
|
107
|
+
workspace: reviewWorkspace,
|
|
108
|
+
label: 'RAIL adjudication pre-spawn',
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
const built = buildSpawnCommand(config);
|
|
112
|
+
if (built.actualProvider !== config.provider || built.actualModel !== config.model
|
|
113
|
+
|| built.actualReasoningConfiguration !== config.reasoningConfiguration) {
|
|
114
|
+
fail('RAIL_ADJUDICATION_ROUTE_MISMATCH', 'terminal adapter changed a selected adjudicator binding');
|
|
115
|
+
}
|
|
116
|
+
await getRateLimiter(exactInvocation.harness_id).waitForSlot();
|
|
117
|
+
assertLease();
|
|
118
|
+
const handle = spawnTerminal(config);
|
|
119
|
+
// Publish the handle before the first fallible journal operation so outer
|
|
120
|
+
// cleanup never treats an unreturned spawned process as already absent.
|
|
121
|
+
onHandle?.(handle);
|
|
122
|
+
try {
|
|
123
|
+
recordRailAdjudicationDispatch({
|
|
124
|
+
projectDir, handoff_id: context.handoff.handoff_id,
|
|
125
|
+
attempt_id: context.attempt.attempt_id, review_ref, invocation: exactInvocation,
|
|
126
|
+
launch_receipt: getTerminalLaunchAuthorityReceipt(handle),
|
|
127
|
+
prompt,
|
|
128
|
+
reviewWorkspace,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await killTerminal(handle);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
return { handle, exactInvocation };
|
|
135
|
+
};
|
|
136
|
+
let { handle, exactInvocation } = await spawnAdjudicationAttempt(0);
|
|
137
|
+
let retries = 0;
|
|
138
|
+
await waitForTerminal(handle);
|
|
139
|
+
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL adjudication' });
|
|
140
|
+
while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
|
|
141
|
+
assertLease();
|
|
142
|
+
retries += 1;
|
|
143
|
+
await new Promise((done) => setTimeout(done, 2_000 * (2 ** (retries - 1))));
|
|
144
|
+
assertLease();
|
|
145
|
+
({ handle, exactInvocation } = await spawnAdjudicationAttempt(retries));
|
|
146
|
+
await waitForTerminal(handle);
|
|
147
|
+
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL adjudication retry' });
|
|
148
|
+
}
|
|
149
|
+
const adjudicationOutput = (handle.stdout || []).join('');
|
|
150
|
+
const terminalOutput = `${adjudicationOutput}${(handle.stderr || []).join('')}`;
|
|
151
|
+
if (handle.exitCode !== 0) fail('RAIL_ADJUDICATOR_FAILED', `${exactInvocation.harness_id} exited with status ${handle.exitCode}`, { output: terminalOutput.slice(0, 10_000) });
|
|
152
|
+
return Object.freeze({
|
|
153
|
+
invocation: exactInvocation,
|
|
154
|
+
decision: parseDecision(adjudicationOutput),
|
|
155
|
+
adjudication_output: adjudicationOutput,
|
|
156
|
+
terminal_authority_receipt: getTerminalCompletionAuthorityReceipt(handle),
|
|
157
|
+
retry_count: retries,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function unanimous(entries) {
|
|
162
|
+
return adjudicationEntriesAgree(entries);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function executeRailAdjudication({
|
|
166
|
+
projectDir, handoff_id, attempt_id,
|
|
167
|
+
timeout = 30 * 60 * 1000, max_retries = 2, lease_ttl_ms = 5 * 60 * 1000,
|
|
168
|
+
} = {}) {
|
|
169
|
+
if (!projectDir || !handoff_id || !attempt_id) fail('RAIL_ADJUDICATION_INPUT_REQUIRED', 'projectDir, handoff_id and attempt_id are required');
|
|
170
|
+
let context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id });
|
|
171
|
+
if (context.state !== 'active') fail('RAIL_ADJUDICATION_ATTEMPT_INACTIVE', `attempt is ${context.state}`);
|
|
172
|
+
if (context.records.some((record) => record.type === 'adjudication_decided' && record.payload?.attempt_id === attempt_id)) {
|
|
173
|
+
fail('RAIL_ADJUDICATION_ALREADY_RECORDED', 'this attempt already has an immutable adjudication decision');
|
|
174
|
+
}
|
|
175
|
+
const plan = latestAdjudicationPlan(context);
|
|
176
|
+
const artifact = loadRuntimeInstallationV2(projectDir);
|
|
177
|
+
const reviews = listRailReviewEvidence({ projectDir, attempt_id });
|
|
178
|
+
const executions = listRailExecutionEvidence({ projectDir, attempt_id });
|
|
179
|
+
const ownership = acquireRailTaskSupervisor({ projectDir, handoff_id, attempt_id, lease_ttl_ms });
|
|
180
|
+
context = ownership.context;
|
|
181
|
+
const renewed = ownership.renewed;
|
|
182
|
+
let activeHandle = null;
|
|
183
|
+
let attemptClosed = false;
|
|
184
|
+
let reviewWorkspace = null;
|
|
185
|
+
try {
|
|
186
|
+
const resolvedReviewSubject = resolveReviewGitSubject({ projectDir, attempt_id });
|
|
187
|
+
reviewWorkspace = createRailReadOnlyCommitWorkspace({
|
|
188
|
+
projectDir,
|
|
189
|
+
baseCommitRef: resolvedReviewSubject.base_commit_ref,
|
|
190
|
+
resultCommitRef: resolvedReviewSubject.result_commit_ref,
|
|
191
|
+
});
|
|
192
|
+
const immutableReviewSubject = Object.freeze({
|
|
193
|
+
base_commit_ref: resolvedReviewSubject.base_commit_ref,
|
|
194
|
+
result_commit_ref: resolvedReviewSubject.result_commit_ref,
|
|
195
|
+
workspace_digest: sha256(reviewWorkspace.baseline),
|
|
196
|
+
});
|
|
197
|
+
const heartbeat = startClaimHeartbeat({
|
|
198
|
+
projectDir, handoff_id, attempt_id, claim_id: context.claim.claim_id,
|
|
199
|
+
operator_id: ownership.operator_id, lease_ttl_ms, initial_lease: renewed.lease,
|
|
200
|
+
activeHandle: () => activeHandle,
|
|
201
|
+
});
|
|
202
|
+
const observed = createProjectRailEngine({ projectDir }).records
|
|
203
|
+
.filter((record) => record.type === 'adjudication_observed'
|
|
204
|
+
&& record.payload?.attempt_id === attempt_id
|
|
205
|
+
&& record.payload?.review_ref === plan.review_ref)
|
|
206
|
+
.map((record) => record.payload.observation);
|
|
207
|
+
const rounds = [];
|
|
208
|
+
try {
|
|
209
|
+
for (let round = 1; round <= 2; round += 1) {
|
|
210
|
+
const priorDecisions = round === 1 ? [] : rounds[0].map((entry, index) => ({
|
|
211
|
+
anonymous_ref: `independent-decision-${index + 1}`,
|
|
212
|
+
decision: entry.decision,
|
|
213
|
+
}));
|
|
214
|
+
const entries = plan.decision.adjudicators.map((candidate) => observed.find((entry) =>
|
|
215
|
+
entry.round === round
|
|
216
|
+
&& entry.invocation.provider_id === candidate.provider_id
|
|
217
|
+
&& entry.invocation.harness_id === candidate.harness_id
|
|
218
|
+
&& sha256(entry.invocation.model_pin) === sha256(candidate.model_pin))).filter(Boolean);
|
|
219
|
+
for (const invocation of plan.decision.adjudicators) {
|
|
220
|
+
if (entries.some((entry) => entry.invocation.provider_id === invocation.provider_id
|
|
221
|
+
&& entry.invocation.harness_id === invocation.harness_id)) continue;
|
|
222
|
+
const entry = await invokeAdjudicator({
|
|
223
|
+
projectDir, artifact, context, plan: plan.decision, invocation, reviews, executions,
|
|
224
|
+
review_ref: plan.review_ref, priorDecisions, round, timeout, max_retries,
|
|
225
|
+
reviewWorkspace,
|
|
226
|
+
reviewSubject: immutableReviewSubject,
|
|
227
|
+
onHandle: (handle) => { activeHandle = handle; },
|
|
228
|
+
assertLease: () => {
|
|
229
|
+
if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'adjudication lost its RAIL claim heartbeat');
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
activeHandle = null;
|
|
233
|
+
recordRailAdjudicationObservation({
|
|
234
|
+
projectDir, handoff_id, attempt_id, review_ref: plan.review_ref, entry,
|
|
235
|
+
});
|
|
236
|
+
entries.push(entry);
|
|
237
|
+
}
|
|
238
|
+
rounds.push(entries);
|
|
239
|
+
if (unanimous(entries)) break;
|
|
240
|
+
}
|
|
241
|
+
} finally { heartbeat.stop(); }
|
|
242
|
+
const heartbeatEvidence = heartbeat.evidence();
|
|
243
|
+
if (heartbeatEvidence.failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'adjudication lost its RAIL claim heartbeat');
|
|
244
|
+
const finalEntries = rounds.at(-1);
|
|
245
|
+
if (!unanimous(finalEntries)) {
|
|
246
|
+
fail('RAIL_ADJUDICATION_NO_CONSENSUS', 'selected top-tier adjudicators did not reach consensus after reconciliation', { rounds });
|
|
247
|
+
}
|
|
248
|
+
const expectedDecision = deriveAdjudicationConsensus({ handoff_id, attempt_id, evidence: finalEntries });
|
|
249
|
+
const persisted = persistRailAdjudicationEvidence({
|
|
250
|
+
projectDir, handoff_id, attempt_id, review_ref: plan.review_ref,
|
|
251
|
+
rounds, final_decision: expectedDecision,
|
|
252
|
+
});
|
|
253
|
+
const journal = recordRailAdjudication({
|
|
254
|
+
projectDir, handoff_id, attempt_id, review_ref: plan.review_ref, evidence: finalEntries,
|
|
255
|
+
});
|
|
256
|
+
attemptClosed = journal.decision.outcome === 'handoff-revision';
|
|
257
|
+
if (sha256(journal.decision) !== sha256(expectedDecision)) {
|
|
258
|
+
fail('RAIL_ADJUDICATION_DECISION_MISMATCH', 'persisted adjudication consensus differs from the canonical journal');
|
|
259
|
+
}
|
|
260
|
+
const next = {
|
|
261
|
+
'bounded-correction': 'rail_rework_required',
|
|
262
|
+
'handoff-revision': 'planning_handoff_revision_required',
|
|
263
|
+
'blocked-authority': 'human_authority_required',
|
|
264
|
+
}[journal.decision.outcome];
|
|
265
|
+
return Object.freeze({
|
|
266
|
+
ok: true, action: 'rail_adjudication_recorded', handoff_id,
|
|
267
|
+
task_id: context.task.task_id, attempt_id,
|
|
268
|
+
adjudication_ref: journal.adjudication_ref,
|
|
269
|
+
adjudication_evidence_ref: persisted.evidence_ref,
|
|
270
|
+
decision: journal.decision,
|
|
271
|
+
council_size: finalEntries.length, rounds: rounds.length,
|
|
272
|
+
journal_event_id: journal.event_id,
|
|
273
|
+
lease_heartbeat_count: heartbeatEvidence.heartbeat_count,
|
|
274
|
+
next_required_action: next,
|
|
275
|
+
});
|
|
276
|
+
} finally {
|
|
277
|
+
let cleanupProven = !activeHandle;
|
|
278
|
+
try {
|
|
279
|
+
if (activeHandle) { await killTerminal(activeHandle); cleanupProven = true; }
|
|
280
|
+
} finally {
|
|
281
|
+
try {
|
|
282
|
+
if (cleanupProven && reviewWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
|
|
283
|
+
} finally {
|
|
284
|
+
if (cleanupProven && !attemptClosed) {
|
|
285
|
+
relinquishRailTask({
|
|
286
|
+
projectDir, handoff_id, attempt_id,
|
|
287
|
+
claim_id: context.claim.claim_id,
|
|
288
|
+
operator_id: ownership.operator_id,
|
|
289
|
+
reason: 'action_complete',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseArgs(argv) {
|
|
298
|
+
const args = {};
|
|
299
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
300
|
+
const token = argv[index];
|
|
301
|
+
if (!token.startsWith('--')) continue;
|
|
302
|
+
const value = argv[index + 1];
|
|
303
|
+
if (value && !value.startsWith('--')) { args[token.slice(2)] = value; index += 1; } else args[token.slice(2)] = true;
|
|
304
|
+
}
|
|
305
|
+
return args;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function main() {
|
|
309
|
+
const args = parseArgs(process.argv);
|
|
310
|
+
try {
|
|
311
|
+
const result = await executeRailAdjudication({
|
|
312
|
+
projectDir: resolve(args['project-dir'] || process.cwd()),
|
|
313
|
+
handoff_id: args['handoff-id'], attempt_id: args['attempt-id'],
|
|
314
|
+
...(args.timeout ? { timeout: Number(args.timeout) } : {}),
|
|
315
|
+
});
|
|
316
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
process.stdout.write(`${JSON.stringify({ ok: false, action: 'error', code: error.code || 'RAIL_ADJUDICATION_FAILED', error: error.message, details: error.details })}\n`);
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) await main();
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { sha256 } from '@chati/core';
|
|
7
|
+
import {
|
|
8
|
+
buildSpawnCommand, getTerminalCompletionAuthorityReceipt, getTerminalLaunchAuthorityReceipt,
|
|
9
|
+
isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
|
|
10
|
+
} from './spawner.js';
|
|
11
|
+
import { checkProviderReadiness } from './provider-preflight.js';
|
|
12
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
13
|
+
import { startClaimHeartbeat } from './run-rail-task.js';
|
|
14
|
+
import {
|
|
15
|
+
acquireRailTaskSupervisor, relinquishRailTask, resolveRailAttemptContext,
|
|
16
|
+
} from '../orchestrator/rail-runtime.js';
|
|
17
|
+
import {
|
|
18
|
+
listRailReviewEvidence, recordRailReview, recordRailReviewDispatch, resolveReviewGitSubject,
|
|
19
|
+
} from '../orchestrator/review-runtime.js';
|
|
20
|
+
import {
|
|
21
|
+
loadRuntimeInstallationV2, resolveRuntimeExecutableBindingV2, resolveRuntimeRailReviewerV2,
|
|
22
|
+
} from '../orchestrator/runtime-installation-v2.js';
|
|
23
|
+
import { buildRailReviewPrompt } from './rail-prompts.js';
|
|
24
|
+
import {
|
|
25
|
+
assertRailReadOnlyCommitWorkspaceUnchanged, cleanupRailReadOnlyCommitWorkspace,
|
|
26
|
+
createRailReadOnlyCommitWorkspace,
|
|
27
|
+
} from './rail-readonly-workspace.js';
|
|
28
|
+
|
|
29
|
+
export { buildRailReviewPrompt } from './rail-prompts.js';
|
|
30
|
+
|
|
31
|
+
function fail(code, message, details = {}) { const error = new Error(message); error.code = code; error.details = details; throw error; }
|
|
32
|
+
|
|
33
|
+
function output(handle) { return [...(handle.stdout || []), ...(handle.stderr || [])].join(''); }
|
|
34
|
+
|
|
35
|
+
export function assertRailReviewIndependenceAvailable({ task, attained_independence_level } = {}) {
|
|
36
|
+
const required = task?.required_independence_level ?? 'A';
|
|
37
|
+
const order = { A: 3, B: 2, C: 1 };
|
|
38
|
+
if (!Object.hasOwn(order, required) || !Object.hasOwn(order, attained_independence_level)) {
|
|
39
|
+
fail('RAIL_REVIEW_INDEPENDENCE_POLICY_INVALID', 'review independence policy must declare A, B or C');
|
|
40
|
+
}
|
|
41
|
+
if (order[attained_independence_level] < order[required]) {
|
|
42
|
+
fail(
|
|
43
|
+
'RAIL_REVIEW_INDEPENDENCE_UNAVAILABLE',
|
|
44
|
+
`sealed routing policy requires independence ${required}, but the strongest eligible reviewer attains ${attained_independence_level}`,
|
|
45
|
+
{ routing_constraints_ref: task?.routing_constraints_ref, required, attained: attained_independence_level },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return required;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseReviewPayload(text) {
|
|
52
|
+
const startToken = '<CHATI_RAIL_REVIEW>';
|
|
53
|
+
const endToken = '</CHATI_RAIL_REVIEW>';
|
|
54
|
+
const start = text.lastIndexOf(startToken);
|
|
55
|
+
const end = text.indexOf(endToken, start + startToken.length);
|
|
56
|
+
if (start === -1 || end === -1) return fail('RAIL_REVIEW_OUTPUT_INVALID', 'reviewer did not return the required CHATI_RAIL_REVIEW envelope');
|
|
57
|
+
try { return JSON.parse(text.slice(start + startToken.length, end).trim()); }
|
|
58
|
+
catch (error) { return fail('RAIL_REVIEW_OUTPUT_INVALID', `reviewer returned invalid JSON: ${error.message}`); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function executeRailReview({
|
|
62
|
+
projectDir,
|
|
63
|
+
handoff_id,
|
|
64
|
+
attempt_id,
|
|
65
|
+
timeout = 30 * 60 * 1000,
|
|
66
|
+
max_retries = 2,
|
|
67
|
+
lease_ttl_ms = 5 * 60 * 1000,
|
|
68
|
+
} = {}) {
|
|
69
|
+
if (!projectDir || !handoff_id || !attempt_id) fail('RAIL_REVIEW_INPUT_REQUIRED', 'projectDir, handoff_id and attempt_id are required');
|
|
70
|
+
let context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id });
|
|
71
|
+
if (!['active', 'ready_to_merge'].includes(context.state)) fail('RAIL_REVIEW_ATTEMPT_INACTIVE', `attempt is ${context.state}`);
|
|
72
|
+
const artifact = loadRuntimeInstallationV2(projectDir);
|
|
73
|
+
const builder_identity = {
|
|
74
|
+
provider_id: context.task.execution_binding.provider_id,
|
|
75
|
+
model_id: context.task.execution_binding.model_id,
|
|
76
|
+
};
|
|
77
|
+
const selected = resolveRuntimeRailReviewerV2({ artifact, builder_identity });
|
|
78
|
+
const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
|
|
79
|
+
artifact, provider_id: selected.provider_id, harness_id: selected.harness_id,
|
|
80
|
+
});
|
|
81
|
+
const requiredIndependence = assertRailReviewIndependenceAvailable({
|
|
82
|
+
task: context.task,
|
|
83
|
+
attained_independence_level: selected.independence_level,
|
|
84
|
+
});
|
|
85
|
+
const existing = listRailReviewEvidence({ projectDir, attempt_id });
|
|
86
|
+
const round = existing.length + 1;
|
|
87
|
+
const readiness = checkProviderReadiness(selected.harness_id, {
|
|
88
|
+
workingDir: projectDir, executableBinding: providerExecutableBinding,
|
|
89
|
+
});
|
|
90
|
+
if (!readiness.ready) fail(readiness.code || 'PROVIDER_NOT_READY', readiness.detail || `${selected.harness_id} is not ready`);
|
|
91
|
+
const reviewSubject = resolveReviewGitSubject({ projectDir, attempt_id });
|
|
92
|
+
const ownership = acquireRailTaskSupervisor({ projectDir, handoff_id, attempt_id, lease_ttl_ms });
|
|
93
|
+
context = ownership.context;
|
|
94
|
+
const renewed = ownership.renewed;
|
|
95
|
+
let handle;
|
|
96
|
+
let reviewWorkspace;
|
|
97
|
+
try {
|
|
98
|
+
reviewWorkspace = createRailReadOnlyCommitWorkspace({
|
|
99
|
+
projectDir,
|
|
100
|
+
baseCommitRef: reviewSubject.base_commit_ref,
|
|
101
|
+
resultCommitRef: reviewSubject.result_commit_ref,
|
|
102
|
+
});
|
|
103
|
+
const immutableReviewSubject = Object.freeze({
|
|
104
|
+
...reviewSubject,
|
|
105
|
+
workspace_digest: sha256(reviewWorkspace.baseline),
|
|
106
|
+
});
|
|
107
|
+
const spawnReviewAttempt = async (retry, assertLease = () => {}) => {
|
|
108
|
+
const invocationDraft = {
|
|
109
|
+
provider_id: selected.provider_id, harness_id: selected.harness_id,
|
|
110
|
+
action: 'review', attempt_id, model_pin: selected.model_pin,
|
|
111
|
+
invocation_id: `rail-review-${attempt_id}-${round}-${retry + 1}`,
|
|
112
|
+
launch_nonce: randomUUID(),
|
|
113
|
+
language: context.language,
|
|
114
|
+
};
|
|
115
|
+
const prompt = buildRailReviewPrompt({
|
|
116
|
+
projectDir: reviewWorkspace.subject_dir,
|
|
117
|
+
context,
|
|
118
|
+
invocation: invocationDraft,
|
|
119
|
+
reviewSubject: immutableReviewSubject,
|
|
120
|
+
round,
|
|
121
|
+
});
|
|
122
|
+
const invocation = Object.freeze({ ...invocationDraft, prompt_digest: sha256(prompt) });
|
|
123
|
+
const config = {
|
|
124
|
+
agent: 'rail-review', taskId: context.task.task_id,
|
|
125
|
+
provider: invocation.harness_id, providerId: invocation.provider_id,
|
|
126
|
+
model: invocation.model_pin.model_id,
|
|
127
|
+
reasoningConfiguration: invocation.model_pin.reasoning_configuration,
|
|
128
|
+
catalogSnapshotRef: invocation.model_pin.catalog_snapshot_ref,
|
|
129
|
+
providerExecutableBinding,
|
|
130
|
+
prompt,
|
|
131
|
+
workingDir: reviewWorkspace.subject_dir, timeout, writeScope: [],
|
|
132
|
+
auditProjectDir: projectDir,
|
|
133
|
+
maskedReadPaths: [projectDir],
|
|
134
|
+
authorityContext: {
|
|
135
|
+
authority_kind: 'rail_review', invocation_id: invocation.invocation_id,
|
|
136
|
+
launch_nonce: invocation.launch_nonce, provider_id: invocation.provider_id,
|
|
137
|
+
},
|
|
138
|
+
beforeProcessSpawn: () => assertRailReadOnlyCommitWorkspaceUnchanged({
|
|
139
|
+
workspace: reviewWorkspace,
|
|
140
|
+
label: 'RAIL review pre-spawn',
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
const built = buildSpawnCommand(config);
|
|
144
|
+
if (built.actualProvider !== config.provider || built.actualModel !== config.model
|
|
145
|
+
|| built.actualReasoningConfiguration !== config.reasoningConfiguration) {
|
|
146
|
+
fail('RAIL_REVIEW_ROUTE_MISMATCH', 'terminal adapter changed the selected review binding');
|
|
147
|
+
}
|
|
148
|
+
await getRateLimiter(invocation.harness_id).waitForSlot();
|
|
149
|
+
assertLease();
|
|
150
|
+
handle = spawnTerminal(config);
|
|
151
|
+
try {
|
|
152
|
+
recordRailReviewDispatch({
|
|
153
|
+
projectDir, attempt_id, invocation,
|
|
154
|
+
launch_receipt: getTerminalLaunchAuthorityReceipt(handle),
|
|
155
|
+
prompt,
|
|
156
|
+
reviewWorkspace,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
await killTerminal(handle);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
return { handle, invocation };
|
|
163
|
+
};
|
|
164
|
+
let invocation;
|
|
165
|
+
const heartbeat = startClaimHeartbeat({
|
|
166
|
+
projectDir, handoff_id, attempt_id, claim_id: context.claim.claim_id,
|
|
167
|
+
operator_id: ownership.operator_id, lease_ttl_ms, initial_lease: renewed.lease,
|
|
168
|
+
activeHandle: () => handle,
|
|
169
|
+
});
|
|
170
|
+
let retries = 0;
|
|
171
|
+
try {
|
|
172
|
+
({ handle, invocation } = await spawnReviewAttempt(0, () => {
|
|
173
|
+
if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
|
|
174
|
+
}));
|
|
175
|
+
await waitForTerminal(handle);
|
|
176
|
+
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL review' });
|
|
177
|
+
while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
|
|
178
|
+
if (heartbeat.evidence().failure) break;
|
|
179
|
+
retries += 1;
|
|
180
|
+
await new Promise((done) => setTimeout(done, 2_000 * (2 ** (retries - 1))));
|
|
181
|
+
if (heartbeat.evidence().failure) break;
|
|
182
|
+
({ handle, invocation } = await spawnReviewAttempt(retries, () => {
|
|
183
|
+
if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
|
|
184
|
+
}));
|
|
185
|
+
await waitForTerminal(handle);
|
|
186
|
+
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL review retry' });
|
|
187
|
+
}
|
|
188
|
+
} finally { heartbeat.stop(); }
|
|
189
|
+
const heartbeatEvidence = heartbeat.evidence();
|
|
190
|
+
if (heartbeatEvidence.failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
|
|
191
|
+
if (handle.exitCode !== 0) fail('RAIL_REVIEW_PROVIDER_FAILED', `reviewer exited with status ${handle.exitCode}`, { output: output(handle).slice(0, 10_000) });
|
|
192
|
+
const payload = parseReviewPayload(output(handle));
|
|
193
|
+
const review_id = `review-${round}-${sha256(payload).slice(0, 16)}`;
|
|
194
|
+
const review = {
|
|
195
|
+
review_id,
|
|
196
|
+
attempt_id,
|
|
197
|
+
reviewer_invocation_ref: invocation.invocation_id,
|
|
198
|
+
rubric_ref: context.task.routing_constraints_ref,
|
|
199
|
+
review_round: round,
|
|
200
|
+
required_independence_level: requiredIndependence,
|
|
201
|
+
attained_independence_level: selected.independence_level,
|
|
202
|
+
builder_identity,
|
|
203
|
+
reviewer_identity: { provider_id: invocation.provider_id, model_id: invocation.model_pin.model_id },
|
|
204
|
+
clean_context: true,
|
|
205
|
+
degradation: { status: 'none' },
|
|
206
|
+
findings: payload.findings ?? [],
|
|
207
|
+
verdict: payload.verdict,
|
|
208
|
+
impasse: payload.impasse ?? { kind: 'none' },
|
|
209
|
+
...(payload.authority_reason ? { authority_reason: payload.authority_reason } : {}),
|
|
210
|
+
...(payload.authority_reason_ref ? { authority_reason_ref: payload.authority_reason_ref } : {}),
|
|
211
|
+
};
|
|
212
|
+
const recorded = recordRailReview({
|
|
213
|
+
projectDir,
|
|
214
|
+
review,
|
|
215
|
+
review_output: (handle.stdout || []).join(''),
|
|
216
|
+
terminal_authority_receipt: getTerminalCompletionAuthorityReceipt(handle),
|
|
217
|
+
invocation,
|
|
218
|
+
result_commit_ref: reviewSubject.result_commit_ref,
|
|
219
|
+
});
|
|
220
|
+
const next = {
|
|
221
|
+
accept: 'rail_completion_required',
|
|
222
|
+
'normal-rework': 'rail_rework_required',
|
|
223
|
+
AI_ADJUDICATION: 'rail_adjudication_required',
|
|
224
|
+
'required-human-decision': 'human_authority_required',
|
|
225
|
+
blocked: 'rail_blocked',
|
|
226
|
+
}[recorded.decision.decision] || 'rail_blocked';
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
ok: recorded.decision.decision === 'accept',
|
|
229
|
+
action: 'rail_review_recorded',
|
|
230
|
+
handoff_id,
|
|
231
|
+
task_id: context.task.task_id,
|
|
232
|
+
attempt_id,
|
|
233
|
+
invocation,
|
|
234
|
+
review_ref: recorded.review_ref,
|
|
235
|
+
decision: recorded.decision,
|
|
236
|
+
review: recorded.review,
|
|
237
|
+
retry_count: retries,
|
|
238
|
+
lease_heartbeat_count: heartbeatEvidence.heartbeat_count,
|
|
239
|
+
next_required_action: next,
|
|
240
|
+
});
|
|
241
|
+
} finally {
|
|
242
|
+
let cleanupProven = !handle;
|
|
243
|
+
try {
|
|
244
|
+
if (handle) { await killTerminal(handle); cleanupProven = true; }
|
|
245
|
+
} finally {
|
|
246
|
+
try {
|
|
247
|
+
if (cleanupProven && reviewWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
|
|
248
|
+
} finally {
|
|
249
|
+
if (cleanupProven) {
|
|
250
|
+
relinquishRailTask({
|
|
251
|
+
projectDir, handoff_id, attempt_id,
|
|
252
|
+
claim_id: context.claim.claim_id,
|
|
253
|
+
operator_id: ownership.operator_id,
|
|
254
|
+
reason: 'action_complete',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseArgs(argv) {
|
|
263
|
+
const args = {};
|
|
264
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
265
|
+
const token = argv[index];
|
|
266
|
+
if (!token.startsWith('--')) continue;
|
|
267
|
+
const value = argv[index + 1];
|
|
268
|
+
if (value && !value.startsWith('--')) { args[token.slice(2)] = value; index += 1; }
|
|
269
|
+
else args[token.slice(2)] = true;
|
|
270
|
+
}
|
|
271
|
+
return args;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function main() {
|
|
275
|
+
const args = parseArgs(process.argv);
|
|
276
|
+
try {
|
|
277
|
+
const result = await executeRailReview({
|
|
278
|
+
projectDir: resolve(args['project-dir'] || process.cwd()),
|
|
279
|
+
handoff_id: args['handoff-id'],
|
|
280
|
+
attempt_id: args['attempt-id'],
|
|
281
|
+
...(args.timeout ? { timeout: Number(args.timeout) } : {}),
|
|
282
|
+
});
|
|
283
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
284
|
+
if (!result.ok && !['rail_rework_required', 'rail_adjudication_required', 'human_authority_required'].includes(result.next_required_action)) process.exitCode = 1;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
process.stdout.write(`${JSON.stringify({ ok: false, action: 'error', code: error.code || 'RAIL_REVIEW_FAILED', error: error.message })}\n`);
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) await main();
|