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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
3
|
+
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { buildSpawnCommand, getTerminalLaunchAuthorityReceipt, getTerminalCompletionAuthorityReceipt, killTerminal, spawnTerminal, waitForTerminal } from './spawner.js';
|
|
8
|
+
import { checkProviderReadiness } from './provider-preflight.js';
|
|
9
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
10
|
+
import {
|
|
11
|
+
buildCouncilGatePrompt,
|
|
12
|
+
cleanupCouncilGateWorkspace,
|
|
13
|
+
createCouncilGatePlan,
|
|
14
|
+
createCouncilGateWorkspace,
|
|
15
|
+
persistCouncilGateDecision,
|
|
16
|
+
assertCouncilGateApproved,
|
|
17
|
+
} from '../orchestrator/council-gate-runtime.js';
|
|
18
|
+
import { resolveRuntimeExecutableBindingV2 } from '../orchestrator/runtime-installation-v2.js';
|
|
19
|
+
|
|
20
|
+
function fail(code, message, details = {}) {
|
|
21
|
+
const error = new Error(message);
|
|
22
|
+
error.code = code;
|
|
23
|
+
error.details = details;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function terminalOutput(handle) {
|
|
28
|
+
return [...(handle.stdout || []), ...(handle.stderr || [])].join('');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseCouncilPayload(stdout, launch_nonce, provider) {
|
|
32
|
+
// Grok's machine-readable transport carries the final response in top-level
|
|
33
|
+
// text. Never scan the raw wrapper (escaped JSON), metadata or echoed prompts
|
|
34
|
+
// for a vote, and never fall back to text if the requested transport failed.
|
|
35
|
+
if (provider === 'grok') {
|
|
36
|
+
let transport;
|
|
37
|
+
try { transport = JSON.parse(stdout); }
|
|
38
|
+
catch { return fail('COUNCIL_GATE_TRANSPORT_INVALID', 'Grok council output is not valid transport JSON'); }
|
|
39
|
+
if (!transport || Array.isArray(transport) || typeof transport.text !== 'string') {
|
|
40
|
+
return fail('COUNCIL_GATE_TRANSPORT_INVALID', 'Grok council output requires a top-level text response');
|
|
41
|
+
}
|
|
42
|
+
stdout = transport.text;
|
|
43
|
+
}
|
|
44
|
+
const matches = [...String(stdout).matchAll(/<CHATI_COUNCIL_REVIEW>\s*([\s\S]*?)\s*<\/CHATI_COUNCIL_REVIEW>/g)];
|
|
45
|
+
if (matches.length === 0) return fail('COUNCIL_GATE_OUTPUT_MISSING', 'council member did not return the required result envelope');
|
|
46
|
+
if (matches.length !== 1) return fail('COUNCIL_GATE_OUTPUT_AMBIGUOUS', 'council member output contains more than one result envelope');
|
|
47
|
+
let payload;
|
|
48
|
+
try { payload = JSON.parse(matches[0][1]); }
|
|
49
|
+
catch { return fail('COUNCIL_GATE_OUTPUT_INVALID', 'council member result envelope is not valid JSON'); }
|
|
50
|
+
if (payload?.launch_nonce !== launch_nonce) {
|
|
51
|
+
return fail('COUNCIL_GATE_OUTPUT_NONCE_MISMATCH', 'council member result is not bound to its authority launch nonce');
|
|
52
|
+
}
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function executeMember({ projectDir, workspace, plan, member, language, timeout, artifact, spawn, kill, readinessCheck, limiterFor, control, transport }) {
|
|
57
|
+
const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
|
|
58
|
+
artifact,
|
|
59
|
+
provider_id: member.provider_id,
|
|
60
|
+
harness_id: member.harness_id,
|
|
61
|
+
});
|
|
62
|
+
const readiness = readinessCheck(member.harness_id, {
|
|
63
|
+
workingDir: workspace,
|
|
64
|
+
executableBinding: providerExecutableBinding,
|
|
65
|
+
});
|
|
66
|
+
if (!readiness.ready) {
|
|
67
|
+
fail('COUNCIL_MEMBER_UNAVAILABLE', `selected council member ${member.provider_id}/${member.model_pin.model_id} is unavailable: ${readiness.detail}`, {
|
|
68
|
+
member_id: member.member_id,
|
|
69
|
+
provider_id: member.provider_id,
|
|
70
|
+
code: readiness.code,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const authorityContext = {
|
|
74
|
+
authority_kind: 'council_gate',
|
|
75
|
+
invocation_id: transport?.invocationId ?? `council-${plan.gate_id}-${member.member_id}`,
|
|
76
|
+
launch_nonce: transport?.launchNonce ?? randomUUID(),
|
|
77
|
+
provider_id: member.provider_id,
|
|
78
|
+
};
|
|
79
|
+
const config = {
|
|
80
|
+
agent: `council-${plan.stage}`,
|
|
81
|
+
taskId: plan.gate_id,
|
|
82
|
+
provider: member.harness_id,
|
|
83
|
+
providerId: member.provider_id,
|
|
84
|
+
model: member.model_pin.model_id,
|
|
85
|
+
reasoningConfiguration: member.model_pin.reasoning_configuration,
|
|
86
|
+
catalogSnapshotRef: member.model_pin.catalog_snapshot_ref,
|
|
87
|
+
providerExecutableBinding,
|
|
88
|
+
prompt: transport?.prompt ?? buildCouncilGatePrompt({ plan, member, material: control.material, launch_nonce: authorityContext.launch_nonce, language }),
|
|
89
|
+
workingDir: workspace,
|
|
90
|
+
timeout,
|
|
91
|
+
writeScope: [],
|
|
92
|
+
auditProjectDir: projectDir,
|
|
93
|
+
maskedReadPaths: [resolve(projectDir)],
|
|
94
|
+
councilToolIsolation: true,
|
|
95
|
+
// A council receives a private frozen workspace. Use the same authority
|
|
96
|
+
// containment as RAIL review. The source project is hidden from reviewers;
|
|
97
|
+
// on Darwin the workspace is retained because scoped group completion does
|
|
98
|
+
// not prove detached descendants have ended.
|
|
99
|
+
authorityContext,
|
|
100
|
+
};
|
|
101
|
+
const built = buildSpawnCommand(config);
|
|
102
|
+
if (built.actualProvider !== config.provider || built.actualModel !== config.model
|
|
103
|
+
|| built.actualReasoningConfiguration !== config.reasoningConfiguration) {
|
|
104
|
+
fail('COUNCIL_MEMBER_ROUTE_MISMATCH', 'terminal adapter changed the selected council binding', { member_id: member.member_id });
|
|
105
|
+
}
|
|
106
|
+
await limiterFor(member.harness_id).waitForSlot();
|
|
107
|
+
if (control.aborted) {
|
|
108
|
+
fail('COUNCIL_MEMBER_CANCELLED', `council member ${member.member_id} was cancelled after another member failed`, {
|
|
109
|
+
member_id: member.member_id,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const handle = spawn(config);
|
|
113
|
+
control.handles.add(handle);
|
|
114
|
+
try {
|
|
115
|
+
await confirmCouncilMemberCompletion(control, handle);
|
|
116
|
+
if (handle.exitCode !== 0) {
|
|
117
|
+
fail('COUNCIL_MEMBER_FAILED', `council member ${member.member_id} exited with status ${handle.exitCode}`, {
|
|
118
|
+
member_id: member.member_id,
|
|
119
|
+
output: terminalOutput(handle).slice(0, 10_000),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (transport) {
|
|
123
|
+
// Reuse containment, not council semantics. The caller must parse and
|
|
124
|
+
// attest its distinct protocol before any authorization can be consumed.
|
|
125
|
+
return Object.freeze({ stdout: (handle.stdout || []).join(''), stderr: (handle.stderr || []).join(''),
|
|
126
|
+
launchReceipt: getTerminalLaunchAuthorityReceipt(handle),
|
|
127
|
+
completionReceipt: getTerminalCompletionAuthorityReceipt(handle) });
|
|
128
|
+
}
|
|
129
|
+
const payload = parseCouncilPayload((handle.stdout || []).join(''), authorityContext.launch_nonce, member.harness_id);
|
|
130
|
+
return {
|
|
131
|
+
member_id: member.member_id,
|
|
132
|
+
reviewer_identity: member.reviewer_identity,
|
|
133
|
+
verdict: payload.verdict,
|
|
134
|
+
findings: payload.findings ?? [],
|
|
135
|
+
};
|
|
136
|
+
} finally {
|
|
137
|
+
if (handle.status === 'running' || (control.darwin && !control.completed.has(handle))) {
|
|
138
|
+
await terminateCouncilMember(control, handle, kill);
|
|
139
|
+
}
|
|
140
|
+
control.handles.delete(handle);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function confirmCouncilMemberCompletion(control, handle) {
|
|
145
|
+
await control.wait(handle);
|
|
146
|
+
if (control.darwin) getTerminalCompletionAuthorityReceipt(handle);
|
|
147
|
+
control.completed.add(handle);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function terminateCouncilMember(control, handle, kill) {
|
|
151
|
+
const prior = control.terminations.get(handle);
|
|
152
|
+
if (prior) return prior;
|
|
153
|
+
const termination = (async () => {
|
|
154
|
+
if (handle.status === 'running') await kill(handle);
|
|
155
|
+
if (handle.status === 'running') {
|
|
156
|
+
fail('COUNCIL_MEMBER_TERMINATION_UNCONFIRMED', 'council member process did not confirm termination before workspace cleanup');
|
|
157
|
+
}
|
|
158
|
+
if (control.darwin && !control.completed.has(handle)) await confirmCouncilMemberCompletion(control, handle);
|
|
159
|
+
})();
|
|
160
|
+
control.terminations.set(handle, termination);
|
|
161
|
+
return termination;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function cancelCouncilMembers(control, kill) {
|
|
165
|
+
control.aborted = true;
|
|
166
|
+
if (!control.cancellation) {
|
|
167
|
+
const active = [...control.handles];
|
|
168
|
+
control.cancellation = Promise.allSettled(active.map((handle) => terminateCouncilMember(control, handle, kill))).then((settled) => {
|
|
169
|
+
const failed = settled.find((result) => result.status === 'rejected');
|
|
170
|
+
if (failed) {
|
|
171
|
+
control.workspaceCleanupSafe = false;
|
|
172
|
+
fail('COUNCIL_MEMBER_TERMINATION_UNCONFIRMED', 'a council member could not be confirmed stopped, so its private workspace was retained', {
|
|
173
|
+
cause_code: failed.reason?.code,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
await control.cancellation;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Single no-tools reviewer transport, with no vote parsing or gate persistence. */
|
|
182
|
+
export async function executeCouncilReviewTransport(options = {}) {
|
|
183
|
+
return withUpgradeMaintenance(options.projectDir, async () => {
|
|
184
|
+
const { projectDir, current, member, prompt, invocationId, launchNonce, timeout = 600_000,
|
|
185
|
+
spawn = spawnTerminal, wait = waitForTerminal, kill = killTerminal,
|
|
186
|
+
readinessCheck = checkProviderReadiness, limiterFor = getRateLimiter } = options;
|
|
187
|
+
if (!current?.artifact || !current?.plan || !member?.member_id
|
|
188
|
+
|| !current.plan.selection?.members?.some(item => item.member_id === member.member_id
|
|
189
|
+
&& JSON.stringify(item) === JSON.stringify(member))
|
|
190
|
+
|| typeof prompt !== 'string' || !prompt || typeof invocationId !== 'string' || !invocationId
|
|
191
|
+
|| typeof launchNonce !== 'string' || !launchNonce
|
|
192
|
+
|| !Number.isInteger(timeout) || timeout < 1 || timeout > 1_800_000) {
|
|
193
|
+
fail('COUNCIL_REVIEW_TRANSPORT_INVALID', 'Single reviewer transport requires an exact selected member, prompt, invocation and finite timeout.');
|
|
194
|
+
}
|
|
195
|
+
const workspace = createCouncilGateWorkspace(current);
|
|
196
|
+
const control = { aborted: false, darwin: process.platform === 'darwin', wait,
|
|
197
|
+
handles: new Set(), completed: new Set(), terminations: new Map(), cancellation: null,
|
|
198
|
+
workspaceCleanupSafe: process.platform !== 'darwin', material: current.material };
|
|
199
|
+
try {
|
|
200
|
+
return await executeMember({ projectDir, workspace, plan: current.plan, member,
|
|
201
|
+
timeout, artifact: current.artifact, spawn, kill, readinessCheck, limiterFor, control,
|
|
202
|
+
transport: { prompt, invocationId, launchNonce } });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
await cancelCouncilMembers(control, kill);
|
|
205
|
+
throw error;
|
|
206
|
+
} finally {
|
|
207
|
+
if (control.handles.size) await cancelCouncilMembers(control, kill);
|
|
208
|
+
if (control.workspaceCleanupSafe) cleanupCouncilGateWorkspace(workspace);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Executes every selected council member against the same private frozen
|
|
215
|
+
* workspace. It persists only the normalized decision, never raw transcripts.
|
|
216
|
+
*/
|
|
217
|
+
export async function executeCouncilGate(options = {}) {
|
|
218
|
+
return withUpgradeMaintenance(options?.projectDir, () => executeCouncilGateUnderMaintenance(options));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function executeCouncilGateUnderMaintenance({
|
|
222
|
+
projectDir,
|
|
223
|
+
stage,
|
|
224
|
+
artifact,
|
|
225
|
+
builder_identity,
|
|
226
|
+
policy,
|
|
227
|
+
handoff,
|
|
228
|
+
language = 'en',
|
|
229
|
+
timeout = 600_000,
|
|
230
|
+
clock = () => new Date(),
|
|
231
|
+
spawn = spawnTerminal,
|
|
232
|
+
wait = waitForTerminal,
|
|
233
|
+
kill = killTerminal,
|
|
234
|
+
readinessCheck = checkProviderReadiness,
|
|
235
|
+
limiterFor = getRateLimiter,
|
|
236
|
+
} = {}) {
|
|
237
|
+
const current = createCouncilGatePlan({
|
|
238
|
+
projectDir, stage, artifact, builder_identity, policy,
|
|
239
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
240
|
+
clock,
|
|
241
|
+
});
|
|
242
|
+
const workspace = createCouncilGateWorkspace(current);
|
|
243
|
+
const control = {
|
|
244
|
+
aborted: false,
|
|
245
|
+
darwin: process.platform === 'darwin',
|
|
246
|
+
wait,
|
|
247
|
+
handles: new Set(),
|
|
248
|
+
completed: new Set(),
|
|
249
|
+
terminations: new Map(),
|
|
250
|
+
cancellation: null,
|
|
251
|
+
workspaceCleanupSafe: process.platform !== 'darwin',
|
|
252
|
+
material: current.material,
|
|
253
|
+
};
|
|
254
|
+
let memberPromises = [];
|
|
255
|
+
try {
|
|
256
|
+
memberPromises = current.plan.selection.members.map((member) => executeMember({
|
|
257
|
+
projectDir,
|
|
258
|
+
workspace,
|
|
259
|
+
plan: current.plan,
|
|
260
|
+
member,
|
|
261
|
+
language,
|
|
262
|
+
timeout,
|
|
263
|
+
artifact: current.artifact,
|
|
264
|
+
spawn,
|
|
265
|
+
kill,
|
|
266
|
+
readinessCheck,
|
|
267
|
+
limiterFor,
|
|
268
|
+
control,
|
|
269
|
+
}).catch(async (error) => {
|
|
270
|
+
await cancelCouncilMembers(control, kill);
|
|
271
|
+
throw error;
|
|
272
|
+
}));
|
|
273
|
+
const reviews = await Promise.all(memberPromises);
|
|
274
|
+
const persisted = persistCouncilGateDecision({ projectDir, plan: current.plan, reviews });
|
|
275
|
+
const approved = assertCouncilGateApproved({
|
|
276
|
+
projectDir,
|
|
277
|
+
stage,
|
|
278
|
+
artifact: current.artifact,
|
|
279
|
+
...(policy ? { policy } : {}),
|
|
280
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
281
|
+
clock,
|
|
282
|
+
});
|
|
283
|
+
if (approved.plan.gate_id !== current.plan.gate_id) {
|
|
284
|
+
fail('COUNCIL_GATE_DECISION_STALE', 'council artifacts changed while member reviews were running');
|
|
285
|
+
}
|
|
286
|
+
return Object.freeze({
|
|
287
|
+
ok: approved.decision.verdict === 'accept',
|
|
288
|
+
action: 'council_gate_recorded',
|
|
289
|
+
stage: current.plan.stage,
|
|
290
|
+
gate_id: current.plan.gate_id,
|
|
291
|
+
member_count: current.plan.selection.member_count,
|
|
292
|
+
decision: approved.decision,
|
|
293
|
+
decision_path: persisted.path,
|
|
294
|
+
next_required_action: approved.decision.verdict === 'accept' ? 'advance_pipeline' : 'planning_rework_required',
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
await cancelCouncilMembers(control, kill);
|
|
298
|
+
await Promise.allSettled(memberPromises);
|
|
299
|
+
throw error;
|
|
300
|
+
} finally {
|
|
301
|
+
if (control.handles.size > 0) {
|
|
302
|
+
await cancelCouncilMembers(control, kill);
|
|
303
|
+
await Promise.allSettled(memberPromises);
|
|
304
|
+
}
|
|
305
|
+
if (control.workspaceCleanupSafe) cleanupCouncilGateWorkspace(workspace);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function parseArgs(argv) {
|
|
310
|
+
const args = {};
|
|
311
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
312
|
+
const token = argv[index];
|
|
313
|
+
if (!token.startsWith('--')) continue;
|
|
314
|
+
const next = argv[index + 1];
|
|
315
|
+
if (next && !next.startsWith('--')) { args[token.slice(2)] = next; index += 1; }
|
|
316
|
+
else args[token.slice(2)] = true;
|
|
317
|
+
}
|
|
318
|
+
return args;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function main() {
|
|
322
|
+
const args = parseArgs(process.argv);
|
|
323
|
+
try {
|
|
324
|
+
const result = await executeCouncilGate({
|
|
325
|
+
projectDir: resolve(args['project-dir'] || process.cwd()),
|
|
326
|
+
stage: args.stage,
|
|
327
|
+
...(args.language ? { language: args.language } : {}),
|
|
328
|
+
...(args.timeout ? { timeout: Number(args.timeout) } : {}),
|
|
329
|
+
});
|
|
330
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
331
|
+
if (!result.ok) process.exitCode = 1;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
process.stdout.write(`${JSON.stringify({ ok: false, action: 'error', code: error.code || 'COUNCIL_GATE_FAILED', error: error.message })}\n`);
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) await main();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
2
|
+
import { loadSession, reservePlanningReconciliation, startPlanningReconciliation,
|
|
3
|
+
settlePlanningReconciliation, recordPlanningReconciliationFailure } from '../orchestrator/session-manager.js';
|
|
4
|
+
import { pinPlanningReconciliation, buildPlanningReconciliationPrompt, planningReconciliationInvocationId,
|
|
5
|
+
persistPlanningReconciliationDecision, readPlanningReconciliationDecision } from '../orchestrator/planning-reconciliation.js';
|
|
6
|
+
import { executeCouncilReviewTransport } from './run-council-gate.js';
|
|
7
|
+
|
|
8
|
+
/** One bounded installed reviewer, durable one-shot admission and atomic QA reservation. */
|
|
9
|
+
export async function executePlanningReconciliation({ projectDir, match, timeout = 600_000,
|
|
10
|
+
reviewTransport = executeCouncilReviewTransport } = {}) {
|
|
11
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 1_800_000) return {
|
|
12
|
+
settled: false, code: 'PLANNING_RECONCILIATION_TIMEOUT_INVALID', error: 'Reconciliation requires a finite timeout of at most 30 minutes.' };
|
|
13
|
+
return withUpgradeMaintenance(projectDir, async () => {
|
|
14
|
+
const session = loadSession(projectDir).session;
|
|
15
|
+
const previous = session?.agents?.['qa-planning']?.planning_reconciliations?.find(entry =>
|
|
16
|
+
entry.request.held_qa.claim_id === match?.claimId && entry.request.held_qa.finished_at === match?.finishedAt
|
|
17
|
+
&& entry.request.source_gate_id === match?.gateId && entry.authorization);
|
|
18
|
+
if (previous) return settlePlanningReconciliation(projectDir, { ...match,
|
|
19
|
+
reconciliationId: previous.request.reconciliation_id, launchNonce: previous.launch_nonce });
|
|
20
|
+
const reserved = reservePlanningReconciliation(projectDir, match);
|
|
21
|
+
if (!reserved.reserved) return { ...reserved, settled: false };
|
|
22
|
+
const entry = reserved.reservation;
|
|
23
|
+
const exact = { ...match, reconciliationId: entry.request.reconciliation_id, launchNonce: entry.launch_nonce };
|
|
24
|
+
const subject = pinPlanningReconciliation(projectDir, match, loadSession(projectDir).session);
|
|
25
|
+
try {
|
|
26
|
+
// A crash after persisting authenticated output needs no new inference.
|
|
27
|
+
const recorded = readPlanningReconciliationDecision(subject, { allowMissing: true });
|
|
28
|
+
if (recorded) return settlePlanningReconciliation(projectDir, exact);
|
|
29
|
+
if (entry.status !== 'reserved') return { settled: false,
|
|
30
|
+
code: entry.status === 'outcome_unknown' ? 'PLANNING_RECONCILIATION_OUTCOME_UNKNOWN' : 'PLANNING_RECONCILIATION_IN_FLIGHT',
|
|
31
|
+
error: 'This assessment already started. No retry or replacement is authorized without its authenticated completed result.' };
|
|
32
|
+
const started = startPlanningReconciliation(projectDir, exact);
|
|
33
|
+
if (!started.started) return { ...started, settled: false };
|
|
34
|
+
try {
|
|
35
|
+
subject.assertCurrent();
|
|
36
|
+
const transport = await reviewTransport({ projectDir, current: subject, member: subject.request.reviewer,
|
|
37
|
+
prompt: buildPlanningReconciliationPrompt({ request: subject.request, material: subject.material, launchNonce: entry.launch_nonce }),
|
|
38
|
+
invocationId: planningReconciliationInvocationId(subject.request), launchNonce: entry.launch_nonce, timeout });
|
|
39
|
+
subject.assertCurrent();
|
|
40
|
+
persistPlanningReconciliationDecision({ ...transport, subject });
|
|
41
|
+
return settlePlanningReconciliation(projectDir, exact);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const failure = recordPlanningReconciliationFailure(projectDir, exact, error.code);
|
|
44
|
+
return { settled: false, code: error.code || 'PLANNING_RECONCILIATION_FAILED', error: error.message,
|
|
45
|
+
outcome_unknown: true, failure_recorded: failure.recorded === true };
|
|
46
|
+
}
|
|
47
|
+
} finally { subject.close(); }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
2
3
|
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { resolve } from 'node:path';
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
buildSpawnCommand, getTerminalCompletionAuthorityReceipt, getTerminalLaunchAuthorityReceipt,
|
|
11
12
|
isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
|
|
12
13
|
} from './spawner.js';
|
|
14
|
+
import { waitForObservedRailProcess } from './rail-process-observer.js';
|
|
13
15
|
import { checkProviderReadiness } from './provider-preflight.js';
|
|
14
16
|
import { getRateLimiter } from './rate-limiter.js';
|
|
15
17
|
import { startClaimHeartbeat } from './run-rail-task.js';
|
|
@@ -135,7 +137,7 @@ async function invokeAdjudicator({ projectDir, artifact, context, plan, review_r
|
|
|
135
137
|
};
|
|
136
138
|
let { handle, exactInvocation } = await spawnAdjudicationAttempt(0);
|
|
137
139
|
let retries = 0;
|
|
138
|
-
await
|
|
140
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id: context.attempt.attempt_id });
|
|
139
141
|
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL adjudication' });
|
|
140
142
|
while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
|
|
141
143
|
assertLease();
|
|
@@ -143,7 +145,7 @@ async function invokeAdjudicator({ projectDir, artifact, context, plan, review_r
|
|
|
143
145
|
await new Promise((done) => setTimeout(done, 2_000 * (2 ** (retries - 1))));
|
|
144
146
|
assertLease();
|
|
145
147
|
({ handle, exactInvocation } = await spawnAdjudicationAttempt(retries));
|
|
146
|
-
await
|
|
148
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id: context.attempt.attempt_id });
|
|
147
149
|
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL adjudication retry' });
|
|
148
150
|
}
|
|
149
151
|
const adjudicationOutput = (handle.stdout || []).join('');
|
|
@@ -162,7 +164,11 @@ function unanimous(entries) {
|
|
|
162
164
|
return adjudicationEntriesAgree(entries);
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
export async function executeRailAdjudication({
|
|
167
|
+
export async function executeRailAdjudication(options = {}) {
|
|
168
|
+
return withUpgradeMaintenance(options?.projectDir, () => executeRailAdjudicationUnderMaintenance(options));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function executeRailAdjudicationUnderMaintenance({
|
|
166
172
|
projectDir, handoff_id, attempt_id,
|
|
167
173
|
timeout = 30 * 60 * 1000, max_retries = 2, lease_ttl_ms = 5 * 60 * 1000,
|
|
168
174
|
} = {}) {
|
|
@@ -180,6 +186,9 @@ export async function executeRailAdjudication({
|
|
|
180
186
|
context = ownership.context;
|
|
181
187
|
const renewed = ownership.renewed;
|
|
182
188
|
let activeHandle = null;
|
|
189
|
+
// No local process needs reconciliation initially (including observation-only
|
|
190
|
+
// resume). Each returned handle invalidates this until its completion is read.
|
|
191
|
+
let invocationCompletionProven = true;
|
|
183
192
|
let attemptClosed = false;
|
|
184
193
|
let reviewWorkspace = null;
|
|
185
194
|
try {
|
|
@@ -224,11 +233,14 @@ export async function executeRailAdjudication({
|
|
|
224
233
|
review_ref: plan.review_ref, priorDecisions, round, timeout, max_retries,
|
|
225
234
|
reviewWorkspace,
|
|
226
235
|
reviewSubject: immutableReviewSubject,
|
|
227
|
-
onHandle: (handle) => { activeHandle = handle; },
|
|
236
|
+
onHandle: (handle) => { activeHandle = handle; invocationCompletionProven = false; },
|
|
228
237
|
assertLease: () => {
|
|
229
238
|
if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'adjudication lost its RAIL claim heartbeat');
|
|
230
239
|
},
|
|
231
240
|
});
|
|
241
|
+
// invokeAdjudicator only returns after reading the trusted completion.
|
|
242
|
+
// Preserve that proof separately from the heartbeat's active handle.
|
|
243
|
+
invocationCompletionProven = true;
|
|
232
244
|
activeHandle = null;
|
|
233
245
|
recordRailAdjudicationObservation({
|
|
234
246
|
projectDir, handoff_id, attempt_id, review_ref: plan.review_ref, entry,
|
|
@@ -274,12 +286,22 @@ export async function executeRailAdjudication({
|
|
|
274
286
|
next_required_action: next,
|
|
275
287
|
});
|
|
276
288
|
} finally {
|
|
277
|
-
|
|
289
|
+
// A Darwin process group is not recursive descendant quiescence. Keep the
|
|
290
|
+
// readonly workspace and require private completion before claim release.
|
|
291
|
+
const retainWorkspace = process.platform === 'darwin';
|
|
292
|
+
let cleanupProven = !activeHandle && (!retainWorkspace || invocationCompletionProven);
|
|
278
293
|
try {
|
|
279
|
-
if (activeHandle) {
|
|
294
|
+
if (activeHandle) {
|
|
295
|
+
await killTerminal(activeHandle);
|
|
296
|
+
if (retainWorkspace) {
|
|
297
|
+
await waitForTerminal(activeHandle);
|
|
298
|
+
getTerminalCompletionAuthorityReceipt(activeHandle);
|
|
299
|
+
}
|
|
300
|
+
cleanupProven = true;
|
|
301
|
+
}
|
|
280
302
|
} finally {
|
|
281
303
|
try {
|
|
282
|
-
if (cleanupProven && reviewWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
|
|
304
|
+
if (cleanupProven && reviewWorkspace && !retainWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
|
|
283
305
|
} finally {
|
|
284
306
|
if (cleanupProven && !attemptClosed) {
|
|
285
307
|
relinquishRailTask({
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { observeRailInterruptedExecution, preserveRailInterruptedExecution, restoreRailInterruptedExecution, reconcileRailObservedExecution, resolveRailAttemptContext } from '../orchestrator/rail-runtime.js';
|
|
2
|
+
import { executeReadyRailTask } from './run-rail-task.js';
|
|
3
|
+
import { executeRailRework } from './run-rail-rework.js';
|
|
4
|
+
import { attemptJournalRevision } from '@chati/rail';
|
|
5
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
6
|
+
import { reviewNextRequiredAction } from './run-rail-review.js';
|
|
7
|
+
|
|
8
|
+
const REFRESH_AFTER_RECONCILIATION = Symbol('refresh-after-reconciliation');
|
|
9
|
+
|
|
10
|
+
/** Resume the canonical stage, never blindly replay a completed initial build. */
|
|
11
|
+
export async function recoverAndResumeRailTask(options = {}) {
|
|
12
|
+
return withUpgradeMaintenance(options?.projectDir, () => recoverUnderMaintenance(options));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function recoverUnderMaintenance({ projectDir, handoff_id, attempt_id, operator_id, termination_attestation, ...options } = {}) {
|
|
16
|
+
// An explicit operator attestation only feeds the interruption observation
|
|
17
|
+
// (Darwin same-boot recovery); it never reaches dispatch or rework options.
|
|
18
|
+
const observation = termination_attestation ? { termination_attestation } : {};
|
|
19
|
+
let changed;
|
|
20
|
+
// Retry only when the engine proves the observed attempt revision changed,
|
|
21
|
+
// before any ownership write. Unchanged errors never cause another launch.
|
|
22
|
+
// Observation, checkpoint and restoration each append one canonical fact.
|
|
23
|
+
// Leave bounded room for those steps plus concurrent revision refreshes.
|
|
24
|
+
for (let refresh = 0; refresh < 6; refresh += 1) {
|
|
25
|
+
try {
|
|
26
|
+
const result = await resumeFromSnapshot({ projectDir, handoff_id, attempt_id, operator_id, observation, ...options });
|
|
27
|
+
if (result !== REFRESH_AFTER_RECONCILIATION) return result;
|
|
28
|
+
}
|
|
29
|
+
catch (error) { if (error.code !== 'RECOVERY_CONTEXT_CHANGED') throw error; changed = error; }
|
|
30
|
+
}
|
|
31
|
+
throw changed ?? Object.assign(new Error('attempt kept changing during completion reconciliation'), { code: 'RECOVERY_CONTEXT_CHANGED' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function resumeFromSnapshot({ projectDir, handoff_id, attempt_id, operator_id, observation = {}, ...options }) {
|
|
35
|
+
const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id });
|
|
36
|
+
const records = context.records.filter((record) => record.payload?.attempt_id === attempt_id);
|
|
37
|
+
const executions = records.filter((record) => record.type === 'execution_recorded').map((record) => record.payload.execution_evidence);
|
|
38
|
+
const completedNonces = new Set(executions.map((entry) => entry.receipt_nonce));
|
|
39
|
+
const notStartedNonces = new Set(records.filter((record) => record.type === 'attempt_recovered')
|
|
40
|
+
.flatMap((record) => [...(record.payload.reconciliation?.execution_admissions ?? []),
|
|
41
|
+
...(record.payload.reconciliation?.execution_dispatches ?? [])])
|
|
42
|
+
.filter((entry) => ['not_started', 'interrupted_restored'].includes(entry.status)).map((entry) => entry.launch_nonce));
|
|
43
|
+
const pending = records.filter((record) => ['execution_launch_reserved', 'rework_launch_reserved', 'execution_dispatched'].includes(record.type)
|
|
44
|
+
&& !completedNonces.has(record.payload.invocation.launch_nonce)
|
|
45
|
+
&& !notStartedNonces.has(record.payload.invocation.launch_nonce));
|
|
46
|
+
const successful = executions.filter((entry) => entry.execution.exit_status === 0 && typeof entry.result_commit_ref === 'string');
|
|
47
|
+
const control = records.filter((record) => ['review_decided', 'adjudication_decided'].includes(record.type)).at(-1);
|
|
48
|
+
const source = control?.type === 'review_decided' ? control.payload.review_ref : control?.payload.adjudication_ref;
|
|
49
|
+
const reworkAuthorized = control?.payload.decision?.decision === 'normal-rework'
|
|
50
|
+
|| control?.payload.decision?.outcome === 'bounded-correction';
|
|
51
|
+
const reworkNeeded = reworkAuthorized && !successful.some((entry) => entry.source_decision_ref === source);
|
|
52
|
+
if (context.state !== 'active') {
|
|
53
|
+
const next = { ready_to_merge: 'rail_merge_required', merged: 'rail_release_readiness',
|
|
54
|
+
cancelled: 'planning_handoff_revision_required', blocked: 'rail_authority_resolution_required' };
|
|
55
|
+
return Object.freeze({ ok: true, action: 'rail_attempt_state_observed', handoff_id, attempt_id,
|
|
56
|
+
task_id: context.task.task_id, state: context.state, next_required_action: next[context.state] });
|
|
57
|
+
}
|
|
58
|
+
const pendingRework = pending.at(-1)?.type === 'rework_launch_reserved'
|
|
59
|
+
|| pending.at(-1)?.payload.run_kind === 'rework';
|
|
60
|
+
const expected_attempt_event_id = attemptJournalRevision(context.records, attempt_id);
|
|
61
|
+
const observedDispatch = pending.find((record) => record.type === 'execution_dispatched'
|
|
62
|
+
&& records.some((entry) => entry.type === 'provider_process_observed'
|
|
63
|
+
&& entry.payload.dispatch_event_id === record.event_id && entry.payload.terminal_output));
|
|
64
|
+
if (observedDispatch) {
|
|
65
|
+
reconcileRailObservedExecution({ projectDir, handoff_id, attempt_id,
|
|
66
|
+
launch_nonce: observedDispatch.payload.invocation.launch_nonce, expected_attempt_event_id });
|
|
67
|
+
return REFRESH_AFTER_RECONCILIATION;
|
|
68
|
+
}
|
|
69
|
+
const interruptedDispatch = pending.find((record) => record.type === 'execution_dispatched');
|
|
70
|
+
if (interruptedDispatch && !records.some((record) => record.type === 'execution_worktree_restored'
|
|
71
|
+
&& record.payload.dispatch_event_id === interruptedDispatch.event_id)) {
|
|
72
|
+
const observed = observeRailInterruptedExecution({ projectDir, handoff_id, attempt_id,
|
|
73
|
+
launch_nonce: interruptedDispatch.payload.invocation.launch_nonce, expected_attempt_event_id, ...observation });
|
|
74
|
+
if (!observed.replay) return REFRESH_AFTER_RECONCILIATION;
|
|
75
|
+
const checkpoint = preserveRailInterruptedExecution({ projectDir, handoff_id, attempt_id,
|
|
76
|
+
launch_nonce: interruptedDispatch.payload.invocation.launch_nonce, expected_attempt_event_id });
|
|
77
|
+
if (!checkpoint.replay) return REFRESH_AFTER_RECONCILIATION;
|
|
78
|
+
restoreRailInterruptedExecution({ projectDir, handoff_id, attempt_id,
|
|
79
|
+
launch_nonce: interruptedDispatch.payload.invocation.launch_nonce, expected_attempt_event_id });
|
|
80
|
+
return REFRESH_AFTER_RECONCILIATION;
|
|
81
|
+
}
|
|
82
|
+
if (pendingRework || (pending.length === 0 && reworkNeeded)) {
|
|
83
|
+
return executeRailRework({ projectDir, handoff_id, attempt_id, operator_id, ...options, expected_attempt_event_id });
|
|
84
|
+
}
|
|
85
|
+
if (pending.length || successful.length === 0) {
|
|
86
|
+
return executeReadyRailTask({ projectDir, handoff_id, attempt_id, operator_id,
|
|
87
|
+
task_id: context.task.task_id, ...options, recover_expired: true, expected_attempt_event_id });
|
|
88
|
+
}
|
|
89
|
+
const observed = successful.at(-1);
|
|
90
|
+
const next = control?.type === 'review_decided' && !reworkAuthorized
|
|
91
|
+
? reviewNextRequiredAction(control.payload.decision.decision)
|
|
92
|
+
: control?.payload.decision?.outcome === 'blocked-authority' ? 'human_authority_required' : 'independent_review';
|
|
93
|
+
return Object.freeze({ ok: true, action: 'rail_execution_already_recorded', handoff_id, attempt_id,
|
|
94
|
+
task_id: context.task.task_id, result_commit_ref: observed.result_commit_ref,
|
|
95
|
+
execution_evidence_ref: `rail-execution://${handoff_id}/${attempt_id}/${observed.evidence_id}`,
|
|
96
|
+
next_required_action: next });
|
|
97
|
+
}
|