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.
Files changed (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +117 -12
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. 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 waitForTerminal(handle);
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 waitForTerminal(handle);
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
- let cleanupProven = !activeHandle;
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) { await killTerminal(activeHandle); cleanupProven = true; }
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
+ }