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
@@ -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 { randomUUID } from 'node:crypto';
@@ -8,6 +9,7 @@ import {
8
9
  buildSpawnCommand, getTerminalCompletionAuthorityReceipt, getTerminalLaunchAuthorityReceipt,
9
10
  isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
10
11
  } from './spawner.js';
12
+ import { waitForObservedRailProcess } from './rail-process-observer.js';
11
13
  import { checkProviderReadiness } from './provider-preflight.js';
12
14
  import { getRateLimiter } from './rate-limiter.js';
13
15
  import { startClaimHeartbeat } from './run-rail-task.js';
@@ -18,7 +20,8 @@ import {
18
20
  listRailReviewEvidence, recordRailReview, recordRailReviewDispatch, resolveReviewGitSubject,
19
21
  } from '../orchestrator/review-runtime.js';
20
22
  import {
21
- loadRuntimeInstallationV2, resolveRuntimeExecutableBindingV2, resolveRuntimeRailReviewerV2,
23
+ assertRuntimeTaskReviewBindingV2, loadRuntimeInstallationV2,
24
+ resolveRuntimeExecutableBindingV2, resolveRuntimeRailReviewerV2,
22
25
  } from '../orchestrator/runtime-installation-v2.js';
23
26
  import { buildRailReviewPrompt } from './rail-prompts.js';
24
27
  import {
@@ -58,7 +61,11 @@ function parseReviewPayload(text) {
58
61
  catch (error) { return fail('RAIL_REVIEW_OUTPUT_INVALID', `reviewer returned invalid JSON: ${error.message}`); }
59
62
  }
60
63
 
61
- export async function executeRailReview({
64
+ export async function executeRailReview(options = {}) {
65
+ return withUpgradeMaintenance(options?.projectDir, () => executeRailReviewUnderMaintenance(options));
66
+ }
67
+
68
+ async function executeRailReviewUnderMaintenance({
62
69
  projectDir,
63
70
  handoff_id,
64
71
  attempt_id,
@@ -74,7 +81,11 @@ export async function executeRailReview({
74
81
  provider_id: context.task.execution_binding.provider_id,
75
82
  model_id: context.task.execution_binding.model_id,
76
83
  };
77
- const selected = resolveRuntimeRailReviewerV2({ artifact, builder_identity });
84
+ // A v2 execution-design handoff seals its reviewer during planning. Legacy
85
+ // handoffs retain the prior catalog selection path for compatibility only.
86
+ const selected = context.task.review_binding
87
+ ? assertRuntimeTaskReviewBindingV2({ artifact, review_binding: context.task.review_binding, builder_identity })
88
+ : resolveRuntimeRailReviewerV2({ artifact, builder_identity });
78
89
  const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
79
90
  artifact, provider_id: selected.provider_id, harness_id: selected.harness_id,
80
91
  });
@@ -172,7 +183,7 @@ export async function executeRailReview({
172
183
  ({ handle, invocation } = await spawnReviewAttempt(0, () => {
173
184
  if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
174
185
  }));
175
- await waitForTerminal(handle);
186
+ await waitForObservedRailProcess({ handle, projectDir, attempt_id });
176
187
  assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL review' });
177
188
  while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
178
189
  if (heartbeat.evidence().failure) break;
@@ -182,7 +193,7 @@ export async function executeRailReview({
182
193
  ({ handle, invocation } = await spawnReviewAttempt(retries, () => {
183
194
  if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
184
195
  }));
185
- await waitForTerminal(handle);
196
+ await waitForObservedRailProcess({ handle, projectDir, attempt_id });
186
197
  assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL review retry' });
187
198
  }
188
199
  } finally { heartbeat.stop(); }
@@ -217,13 +228,7 @@ export async function executeRailReview({
217
228
  invocation,
218
229
  result_commit_ref: reviewSubject.result_commit_ref,
219
230
  });
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';
231
+ const next = reviewNextRequiredAction(recorded.decision.decision);
227
232
  return Object.freeze({
228
233
  ok: recorded.decision.decision === 'accept',
229
234
  action: 'rail_review_recorded',
@@ -239,12 +244,22 @@ export async function executeRailReview({
239
244
  next_required_action: next,
240
245
  });
241
246
  } finally {
242
- let cleanupProven = !handle;
247
+ // Darwin's valid group completion permits this readonly invocation to
248
+ // relinquish its claim, but does not prove that every detached reader died.
249
+ const retainWorkspace = process.platform === 'darwin';
250
+ let cleanupProven = !handle && !retainWorkspace;
243
251
  try {
244
- if (handle) { await killTerminal(handle); cleanupProven = true; }
252
+ if (handle) {
253
+ await killTerminal(handle);
254
+ if (retainWorkspace) {
255
+ await waitForTerminal(handle);
256
+ getTerminalCompletionAuthorityReceipt(handle);
257
+ }
258
+ cleanupProven = true;
259
+ }
245
260
  } finally {
246
261
  try {
247
- if (cleanupProven && reviewWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
262
+ if (cleanupProven && reviewWorkspace && !retainWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
248
263
  } finally {
249
264
  if (cleanupProven) {
250
265
  relinquishRailTask({
@@ -288,4 +303,10 @@ async function main() {
288
303
  }
289
304
  }
290
305
 
306
+ export function reviewNextRequiredAction(decision) {
307
+ return { accept: 'rail_completion_required', 'normal-rework': 'rail_rework_required',
308
+ AI_ADJUDICATION: 'rail_adjudication_required', 'required-human-decision': 'human_authority_required',
309
+ blocked: 'rail_blocked' }[decision] || 'rail_blocked';
310
+ }
311
+
291
312
  if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) await main();
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
2
3
 
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { resolve } from 'node:path';
6
7
  import {
7
8
  buildSpawnCommand, getTerminalCompletionAuthorityReceipt,
8
- getTerminalLaunchAuthorityReceipt, isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
9
+ isTransientFailure, killTerminal, spawnTerminal,
9
10
  } from './spawner.js';
11
+ import { waitForObservedRailProcess } from './rail-process-observer.js';
10
12
  import { checkProviderReadiness } from './provider-preflight.js';
11
13
  import { getRateLimiter } from './rate-limiter.js';
12
14
  import { startClaimHeartbeat } from './run-rail-task.js';
@@ -70,10 +72,17 @@ function latestReworkAuthorization(context) {
70
72
  return fail('RAIL_REWORK_NOT_AUTHORIZED', 'the latest canonical decision does not authorize bounded rework');
71
73
  }
72
74
 
73
- export async function executeRailRework({
75
+ export async function executeRailRework(options = {}) {
76
+ return withUpgradeMaintenance(options?.projectDir, () => executeRailReworkUnderMaintenance(options));
77
+ }
78
+
79
+ async function executeRailReworkUnderMaintenance({
74
80
  projectDir,
75
81
  handoff_id,
76
82
  attempt_id,
83
+ operator_id,
84
+ git_dir = projectDir,
85
+ expected_attempt_event_id,
77
86
  timeout = 30 * 60 * 1000,
78
87
  max_retries = 2,
79
88
  lease_ttl_ms = 5 * 60 * 1000,
@@ -111,7 +120,7 @@ export async function executeRailRework({
111
120
  if (!readiness.ready) fail(readiness.code || 'PROVIDER_NOT_READY', readiness.detail || `${binding.harness_id} is not ready`);
112
121
  const base_commit_ref = executionHistory.filter((entry) => typeof entry.result_commit_ref === 'string').at(-1)?.result_commit_ref;
113
122
  if (!base_commit_ref) fail('RAIL_REWORK_BASE_COMMIT_REQUIRED', 'latest immutable execution evidence has no result commit');
114
- const ownership = acquireRailTaskSupervisor({ projectDir, handoff_id, attempt_id, lease_ttl_ms });
123
+ const ownership = acquireRailTaskSupervisor({ projectDir, handoff_id, attempt_id, operator_id, git_dir, lease_ttl_ms, expected_attempt_event_id });
115
124
  context = ownership.context;
116
125
  const renewed = ownership.renewed;
117
126
  const reservation_id = `rework-reservation-${randomUUID()}`;
@@ -166,32 +175,34 @@ export async function executeRailRework({
166
175
  authority_kind: 'rail_execution', invocation_id: invocation.invocation_id,
167
176
  launch_nonce: invocation.launch_nonce, provider_id: invocation.provider_id,
168
177
  },
169
- beforeAuthoritySpawn: () => {
178
+ beforeAuthoritySpawn: ({ admission_receipt }) => {
170
179
  reserveRailReworkLaunch({
171
180
  projectDir, handoff_id, attempt_id,
172
181
  source_decision_ref: authorization.source_decision_ref,
173
182
  reservation_id,
174
183
  invocation,
184
+ admission_receipt,
175
185
  });
176
186
  // From the final pre-spawn durable launch intent onward, absence of
177
187
  // canonical completion evidence remains unresolved after a crash.
178
188
  dispatchAwaitingEvidence = true;
179
189
  },
190
+ afterAuthoritySpawn: (launch_receipt, pendingHandle) => {
191
+ handle = pendingHandle;
192
+ return recordRailExecutionDispatch({
193
+ projectDir, handoff_id, attempt_id, invocation, launch_receipt,
194
+ prompt, run_kind: 'rework',
195
+ });
196
+ },
180
197
  };
181
198
  const built = buildSpawnCommand(config);
182
199
  if (built.actualProvider !== config.provider || built.actualModel !== config.model
183
200
  || built.actualReasoningConfiguration !== config.reasoningConfiguration) {
184
201
  fail('RAIL_REWORK_ROUTE_MISMATCH', 'terminal adapter changed the sealed rework binding');
185
202
  }
186
- handle = spawnTerminal(config);
187
- try {
188
- recordRailExecutionDispatch({
189
- projectDir, handoff_id, attempt_id, invocation,
190
- launch_receipt: getTerminalLaunchAuthorityReceipt(handle),
191
- prompt, run_kind: 'rework',
192
- });
193
- } catch (error) {
194
- await killTerminal(handle);
203
+ try { handle = spawnTerminal(config); }
204
+ catch (error) {
205
+ if (handle) await killTerminal(handle);
195
206
  throw error;
196
207
  }
197
208
  return handle;
@@ -214,7 +225,7 @@ export async function executeRailRework({
214
225
  failedAttemptRefs.push(persisted.evidence_ref);
215
226
  };
216
227
  try {
217
- await waitForTerminal(handle);
228
+ await waitForObservedRailProcess({ handle, projectDir, attempt_id });
218
229
  while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
219
230
  if (heartbeat.evidence().failure) break;
220
231
  recordFailedAttempt();
@@ -224,7 +235,7 @@ export async function executeRailRework({
224
235
  await getRateLimiter(binding.harness_id).waitForSlot();
225
236
  if (heartbeat.evidence().failure) break;
226
237
  await launch();
227
- await waitForTerminal(handle);
238
+ await waitForObservedRailProcess({ handle, projectDir, attempt_id });
228
239
  }
229
240
  } finally { heartbeat.stop(); }
230
241
  const heartbeatEvidence = heartbeat.evidence();
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
2
3
 
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { resolve } from 'node:path';
7
+ import { attemptExecutionBase } from '@chati/rail';
6
8
  import {
7
9
  buildSpawnCommand, getTerminalCompletionAuthorityReceipt,
8
- getTerminalLaunchAuthorityReceipt, isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
10
+ getTerminalLaunchAuthorityReceipt, isTransientFailure, killTerminal, spawnTerminal,
9
11
  } from './spawner.js';
12
+ import { waitForObservedRailProcess } from './rail-process-observer.js';
10
13
  import { checkProviderReadiness } from './provider-preflight.js';
11
14
  import { getRateLimiter } from './rate-limiter.js';
12
15
  import {
@@ -139,7 +142,11 @@ export function startClaimHeartbeat({ projectDir, handoff_id, attempt_id, claim_
139
142
  * dispatchReadyRailTask and starts the exact selected CLI binding from inside
140
143
  * the injected adapter. Retries reuse that same binding and never downgrade.
141
144
  */
142
- export async function executeReadyRailTask({
145
+ export async function executeReadyRailTask(options = {}) {
146
+ return withUpgradeMaintenance(options?.projectDir, () => executeReadyRailTaskUnderMaintenance(options));
147
+ }
148
+
149
+ async function executeReadyRailTaskUnderMaintenance({
143
150
  projectDir,
144
151
  handoff_id,
145
152
  task_id,
@@ -149,11 +156,12 @@ export async function executeReadyRailTask({
149
156
  git_dir = projectDir,
150
157
  lease_ttl_ms,
151
158
  recover_expired = false,
159
+ expected_attempt_event_id,
152
160
  timeout = 30 * 60 * 1000,
153
161
  max_retries = 2,
154
162
  } = {}) {
155
163
  if (!projectDir || !handoff_id || !task_id) fail('RAIL_DISPATCH_INPUT_REQUIRED', 'projectDir, handoff_id and task_id are required');
156
- const { stored, task } = exactTask({ projectDir, handoff_id, task_id });
164
+ const { task } = exactTask({ projectDir, handoff_id, task_id });
157
165
  const binding = task.execution_binding;
158
166
  const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
159
167
  artifact: loadRuntimeInstallationV2(projectDir),
@@ -169,13 +177,17 @@ export async function executeReadyRailTask({
169
177
  let exactConfig = null;
170
178
  const adapter = Object.freeze({
171
179
  harness_id: binding.harness_id,
172
- invoke({ invocation, prompt, reserve_launch }) {
180
+ invoke({ attempt, invocation, prompt, reserve_launch, record_dispatch }) {
173
181
  exactConfig = Object.freeze({
174
182
  ...terminalConfig({
175
183
  projectDir, task, invocation, prompt, timeout, providerExecutableBinding,
176
- baseCommitRef: stored.handoff.seal.git_commit_ref,
184
+ baseCommitRef: attemptExecutionBase(attempt),
177
185
  }),
178
186
  beforeAuthoritySpawn: reserve_launch,
187
+ afterAuthoritySpawn: (receipt, pendingHandle) => {
188
+ handle = pendingHandle;
189
+ return record_dispatch(receipt);
190
+ },
179
191
  });
180
192
  assertExactTerminalRoute(exactConfig);
181
193
  handle = spawnTerminal(exactConfig);
@@ -191,6 +203,7 @@ export async function executeReadyRailTask({
191
203
  projectDir, handoff_id, task_id, attempt_id, operator_id,
192
204
  adapter, browser_request, git_dir, lease_ttl_ms: effectiveLeaseTtlMs,
193
205
  recover_expired,
206
+ expected_attempt_event_id,
194
207
  });
195
208
  } catch (error) {
196
209
  if (handle) await killTerminal(handle);
@@ -230,13 +243,13 @@ export async function executeReadyRailTask({
230
243
  projectDir, handoff_id, task_id, attempt_id,
231
244
  invocation: dispatch.invocation,
232
245
  execution: failedExecution,
233
- base_commit_ref: dispatch.claim.attempt.git_commit_ref,
246
+ base_commit_ref: attemptExecutionBase(dispatch.claim.attempt),
234
247
  result_commit_ref: null,
235
248
  });
236
249
  failedAttemptRefs.push(persisted.evidence_ref);
237
250
  };
238
251
  try {
239
- await waitForTerminal(handle);
252
+ await waitForObservedRailProcess({ handle, projectDir, attempt_id });
240
253
  while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
241
254
  if (heartbeat.evidence().failure) break;
242
255
  recordFailedAttempt();
@@ -253,7 +266,7 @@ export async function executeReadyRailTask({
253
266
  if (handle) await killTerminal(handle);
254
267
  throw error;
255
268
  }
256
- await waitForTerminal(handle);
269
+ await waitForObservedRailProcess({ handle, projectDir, attempt_id });
257
270
  }
258
271
  } finally {
259
272
  heartbeat.stop();
@@ -266,7 +279,7 @@ export async function executeReadyRailTask({
266
279
  );
267
280
  handle.retryCount = retries;
268
281
  const execution = terminalEvidence(handle);
269
- const base_commit_ref = dispatch.claim.attempt.git_commit_ref;
282
+ const base_commit_ref = attemptExecutionBase(dispatch.claim.attempt);
270
283
  const invalidOutput = execution.terminal_authority_receipt.execution_result_status === 'invalid_output';
271
284
  const envelope = execution.exit_status === 0 && !invalidOutput
272
285
  ? parseRailExecutionEnvelope((handle.stdout || []).join(''))
@@ -0,0 +1,85 @@
1
+ /** Read a native helper against the original publisher attestation. Never
2
+ * execute or derive trust from the helper's current pathname/hash alone. */
3
+ import { createPublicKey } from 'node:crypto';
4
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
5
+ import { basename, dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { PACKAGE_ARTIFACT_MANIFEST, PACKAGE_ARTIFACT_SIGNATURE, verifyPackageArtifactManifest } from '../installer/package-artifact.js';
8
+ import { digestProviderExecutable } from '../installer-v2/provider-executable.js';
9
+
10
+ const SOURCE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const INSTALLED = basename(SOURCE_ROOT) === '_cli';
12
+ const ARTIFACT_ROOT = INSTALLED ? SOURCE_ROOT : dirname(SOURCE_ROOT);
13
+ function fail(message) { throw Object.assign(new Error(message), { code: 'RUNTIME_NATIVE_HELPER_UNTRUSTED' }); }
14
+ function same(left, right) {
15
+ return ['dev', 'ino', 'mode', 'uid', 'gid', 'nlink', 'size', 'mtimeMs', 'ctimeMs']
16
+ .every((key) => left[key] === right[key]);
17
+ }
18
+
19
+ function readMetadata(path, maximum) {
20
+ let descriptor;
21
+ try {
22
+ if (realpathSync(path) !== path) fail('runtime attestation must use physical paths');
23
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
24
+ const before = fstatSync(descriptor);
25
+ if (!before.isFile() || before.nlink !== 1 || before.size < 1 || before.size > maximum) {
26
+ fail('runtime attestation must be a bounded single-link regular file');
27
+ }
28
+ const buffer = Buffer.alloc(maximum + 1);
29
+ let offset = 0;
30
+ while (offset < buffer.length) {
31
+ const count = readSync(descriptor, buffer, offset, buffer.length - offset, offset);
32
+ if (count === 0) break;
33
+ offset += count;
34
+ }
35
+ if (offset > maximum || offset !== before.size || !same(before, fstatSync(descriptor))
36
+ || !same(before, lstatSync(path))) fail('runtime attestation changed while being read');
37
+ return buffer.subarray(0, offset).toString('utf8');
38
+ } finally { if (descriptor !== undefined) closeSync(descriptor); }
39
+ }
40
+
41
+ export function openAttestedRuntimeNativeHelper(name) {
42
+ let descriptor;
43
+ try {
44
+ if (!['src', '_cli'].includes(basename(SOURCE_ROOT))) fail('unknown runtime source layout');
45
+ if (!['linux', 'darwin'].includes(process.platform) || !['x64', 'arm64'].includes(process.arch)
46
+ || !['execution-gate', 'darwin-launcher'].includes(name)
47
+ || (name === 'darwin-launcher' && process.platform !== 'darwin')) fail('unsupported native helper target');
48
+ const manifest = JSON.parse(readMetadata(join(ARTIFACT_ROOT, PACKAGE_ARTIFACT_MANIFEST), 4 * 1024 * 1024));
49
+ const signature = readMetadata(join(ARTIFACT_ROOT, PACKAGE_ARTIFACT_SIGNATURE), 4096);
50
+ // No caller-selected key or environment trust override. The reader belongs
51
+ // to this runtime, whose own embedded production root is preserved at copy.
52
+ const key = createPublicKey(readMetadata(join(SOURCE_ROOT, 'installer/signing-public-key.pem'), 16384));
53
+ const verified = verifyPackageArtifactManifest(manifest, signature, key);
54
+ if (!verified.valid) fail(`runtime package attestation rejected: ${verified.reason}`);
55
+ const metadata = JSON.parse(readMetadata(join(ARTIFACT_ROOT, INSTALLED ? 'version.json' : 'package.json'), 65536));
56
+ const expectedVersion = INSTALLED ? metadata.package_version : metadata.version;
57
+ if (manifest.package_name !== 'chati-dev' || typeof expectedVersion !== 'string'
58
+ || !expectedVersion || manifest.version !== expectedVersion
59
+ || (!INSTALLED && metadata.name !== 'chati-dev')) fail('runtime package attestation version or name mismatch');
60
+ const entryPath = `node_modules/@chati/core/native/prebuilds/${process.platform}-${process.arch}/${name}`;
61
+ const entry = manifest.files[entryPath];
62
+ if (!entry || !/^[a-f0-9]{64}$/.test(entry.hash) || !Number.isSafeInteger(entry.size)
63
+ || entry.size < 1 || entry.size > 64 * 1024 * 1024 || !Number.isInteger(entry.mode)
64
+ || entry.mode < 0 || entry.mode > 0o777 || (entry.mode & 0o111) === 0
65
+ || (entry.mode & 0o022) !== 0) fail('signed native helper entry is missing or invalid');
66
+ const path = join(ARTIFACT_ROOT, entryPath);
67
+ if (realpathSync(path) !== path) fail('native helper must use a physical path');
68
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
69
+ const before = fstatSync(descriptor);
70
+ if (!before.isFile() || before.nlink !== 1 || before.size !== entry.size
71
+ || (before.mode & 0o777) !== entry.mode || ![0, process.geteuid()].includes(before.uid)) {
72
+ fail('native helper file does not match signed shape or safe ownership');
73
+ }
74
+ const executable = digestProviderExecutable(descriptor);
75
+ if (executable.sha256 !== entry.hash || !same(before, fstatSync(descriptor))
76
+ || !same(before, lstatSync(path))) fail('native helper bytes differ from the publisher attestation');
77
+ const result = Object.freeze({ descriptor,
78
+ binding: Object.freeze({ executable_path: path, executable_sha256: entry.hash }), executable });
79
+ descriptor = undefined;
80
+ return result;
81
+ } catch (error) {
82
+ if (error.code === 'RUNTIME_NATIVE_HELPER_UNTRUSTED') throw error;
83
+ return fail(`native helper attestation is unavailable or invalid: ${error.code || error.name}`);
84
+ } finally { if (descriptor !== undefined) closeSync(descriptor); }
85
+ }