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
  * CLI runner for single-agent terminal execution.
4
5
  *
@@ -18,9 +19,12 @@ import { randomUUID } from 'node:crypto';
18
19
  import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, rmSync } from 'fs';
19
20
  import { isAbsolute, relative, resolve } from 'path';
20
21
  import { buildAgentPrompt } from './prompt-builder.js';
21
- import { isTransientFailure, spawnTerminalWithRetry } from './spawner.js';
22
+ import { getTerminalAttemptDiagnostics, isTransientFailure, spawnTerminalWithRetry } from './spawner.js';
22
23
  import { getWriteScope } from './isolation.js';
23
- import { parseAgentOutput } from './handoff-parser.js';
24
+ import { normalizePlanningQaOutcome, parseAgentOutput } from './handoff-parser.js';
25
+ import { getQaThreshold } from '../orchestrator/pipeline-manager.js';
26
+ import { pinPlanningContractRevisionInputs, pinPlanningQaSubject } from '../orchestrator/council-gate-runtime.js';
27
+ import { AGENT_RETRY_DELAY_MS, createAgentAttemptBudget, parseAgentTimeout } from './agent-attempt-budget.js';
24
28
  import { checkProviderReadiness } from './provider-preflight.js';
25
29
  import { createCostTracker } from './cost-tracker.js';
26
30
  import { getRateLimiter } from './rate-limiter.js';
@@ -30,7 +34,7 @@ import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetr
30
34
  import {
31
35
  recordPendingInteraction, clearPendingInteraction,
32
36
  claimPendingInteraction, releasePendingInteraction,
33
- loadSession, startAgentExecution, holdAgentExecutionForAdvance, releaseAgentExecution,
37
+ loadSession, startAgentExecution, holdAgentExecutionForAdvance, releaseAgentExecution, validateInitialTaskContext,
34
38
  } from '../orchestrator/session-manager.js';
35
39
  import { recordEvent, EventType } from '../intelligence/timeline.js';
36
40
  import {
@@ -92,7 +96,11 @@ function parseArgs(argv) {
92
96
 
93
97
  async function main() {
94
98
  const args = parseArgs(process.argv);
99
+ const projectDir = args['project-dir'] || process.cwd();
100
+ return withUpgradeMaintenance(projectDir, () => runUnderMaintenance(args, projectDir));
101
+ }
95
102
 
103
+ async function runUnderMaintenance(args, projectDir) {
96
104
  // Validate required args
97
105
  if (!args.agent) {
98
106
  outputError('Missing required argument: --agent');
@@ -103,8 +111,14 @@ async function main() {
103
111
  process.exit(1);
104
112
  }
105
113
 
106
- const projectDir = args['project-dir'] || process.cwd();
107
- const timeout = parseInt(args.timeout, 10) || 600_000; // default 10 minutes
114
+ let timeout;
115
+ try { timeout = parseAgentTimeout(args.timeout, args.agent); }
116
+ catch (error) {
117
+ outputResult({ status: 'error', code: error.code, error: error.message, agent: args.agent });
118
+ process.exit(1);
119
+ }
120
+ let executionDeadline = null;
121
+ let contentReviewContext = null;
108
122
 
109
123
  const runtimeRoute = validateRuntimeCommand(projectDir, args);
110
124
  if (!runtimeRoute.ok) {
@@ -128,6 +142,10 @@ async function main() {
128
142
  const execution = startAgentExecution(projectDir, {
129
143
  agent: args.agent,
130
144
  claimId: args['execution-claim-id'] || null,
145
+ taskId: args['task-id'],
146
+ binding: { provider: args.provider || null, provider_id: args['provider-id'] || null,
147
+ model: args.model || null, reasoning_configuration: args['reasoning-configuration'] || null,
148
+ catalog_snapshot_ref: args['catalog-snapshot-ref'] || null },
131
149
  });
132
150
  if (!execution.saved || !execution.started) {
133
151
  outputResult({
@@ -139,6 +157,8 @@ async function main() {
139
157
  });
140
158
  process.exit(1);
141
159
  }
160
+ executionDeadline = execution.expiresAt;
161
+ contentReviewContext = execution.contentReviewContext;
142
162
  process.once('exit', () => {
143
163
  releaseAgentExecution(projectDir, {
144
164
  agent: args.agent,
@@ -159,11 +179,21 @@ async function main() {
159
179
  process.exit(1);
160
180
  }
161
181
  if (responseFile.response !== null) args['additional-context'] = responseFile.response;
182
+ // Extra task context belongs to this already validated fresh v2 claim. It
183
+ // is not an answer to a pending question and cannot authenticate a relay.
184
+ const freshTaskContext = Boolean(runtimeRoute.artifact && !args['interaction-id']);
185
+ if (freshTaskContext) {
186
+ try { validateInitialTaskContext(args['additional-context']); }
187
+ catch (error) {
188
+ outputResult({ status: 'error', code: error.code, error: error.message, agent: args.agent });
189
+ process.exit(1);
190
+ }
191
+ }
162
192
 
163
193
  // A session awaiting user input is a local state gate, not a provider
164
194
  // preflight concern. Check it before looking for a CLI so a missing or
165
195
  // empty relay response cannot be masked by a host-specific CLI absence.
166
- if (typeof args['additional-context'] !== 'string' || !args['additional-context'].trim()) {
196
+ if (freshTaskContext || typeof args['additional-context'] !== 'string' || !args['additional-context'].trim()) {
167
197
  const pendingGuard = claimPendingInteraction(projectDir, {
168
198
  interactionId: args['interaction-id'] || null,
169
199
  agent: args.agent,
@@ -173,7 +203,7 @@ async function main() {
173
203
  model: args.model || null,
174
204
  reasoningConfiguration: args['reasoning-configuration'] || null,
175
205
  catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
176
- response: args['additional-context'],
206
+ response: freshTaskContext ? undefined : args['additional-context'],
177
207
  reasoningEscalation: args['reasoning-escalation'] === 'true',
178
208
  });
179
209
  if (!pendingGuard.saved || !pendingGuard.allowed) {
@@ -208,28 +238,32 @@ async function main() {
208
238
  process.exit(1);
209
239
  }
210
240
 
211
- // Build the agent prompt
241
+ const promptConfig = {
242
+ agent: args.agent, taskId: args['task-id'], projectDir,
243
+ previousAgent: args['previous-agent'] || null, workflow: args.workflow || null,
244
+ sessionState, additionalContext: args['additional-context'] || null,
245
+ provider: args.provider || null, model: args.model || null,
246
+ providerId: args['provider-id'] || null,
247
+ reasoningConfiguration: args['reasoning-configuration'] || null,
248
+ catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
249
+ interactionId: args['interaction-id'] || null,
250
+ strictProvider: args['strict-provider'] === 'true',
251
+ executionClaimId: args['execution-claim-id'] || null,
252
+ contentReviewContext,
253
+ };
254
+
255
+ // Local preparation resolves the route, but never injects unclaimed history.
212
256
  let promptResult;
213
257
  try {
214
- promptResult = buildAgentPrompt({
215
- agent: args.agent,
216
- taskId: args['task-id'],
217
- projectDir,
218
- previousAgent: args['previous-agent'] || null,
219
- workflow: args.workflow || null,
220
- sessionState,
221
- additionalContext: args['additional-context'] || null,
222
- provider: args.provider || null,
223
- model: args.model || null,
224
- strictProvider: args['strict-provider'] === 'true',
225
- });
258
+ promptResult = buildAgentPrompt(promptConfig);
226
259
  } catch (err) {
227
260
  const relayState = releasePendingForFailedRelay(projectDir, args);
228
261
  if (!relayState.saved) {
229
262
  outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message });
230
263
  process.exit(1);
231
264
  }
232
- outputError(`Failed to build prompt: ${err.message}`);
265
+ outputResult({ status: 'error', code: err.code || 'PROMPT_BUILD_FAILED',
266
+ error: `Failed to build prompt: ${err.message}`, agent: args.agent });
233
267
  process.exit(1);
234
268
  }
235
269
 
@@ -270,6 +304,7 @@ async function main() {
270
304
 
271
305
  // Reserve the relay only after local preparation and rate limiting. The
272
306
  // lease therefore covers provider execution, not unbounded queue time.
307
+ const relayDeadline = new Date(Date.now() + (timeout * 2) + 5 * 60 * 1000).toISOString();
273
308
  const pendingClaim = claimPendingInteraction(projectDir, {
274
309
  interactionId: args['interaction-id'] || null,
275
310
  agent: args.agent,
@@ -279,9 +314,9 @@ async function main() {
279
314
  model: args.model || null,
280
315
  reasoningConfiguration: args['reasoning-configuration'] || null,
281
316
  catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
282
- response: args['additional-context'],
317
+ response: freshTaskContext ? undefined : args['additional-context'],
283
318
  reasoningEscalation: args['reasoning-escalation'] === 'true',
284
- leaseExpiresAt: new Date(Date.now() + (timeout * 2) + 5 * 60 * 1000).toISOString(),
319
+ leaseExpiresAt: relayDeadline,
285
320
  });
286
321
  if (!pendingClaim.saved || !pendingClaim.allowed) {
287
322
  cleanupRejectedResponseFile(responseFile.filePath, pendingClaim);
@@ -295,12 +330,43 @@ async function main() {
295
330
  process.exit(1);
296
331
  }
297
332
  if (pendingClaim.claimed) args['relay-claim-id'] = pendingClaim.claimId;
333
+ if (pendingClaim.claimed) {
334
+ try {
335
+ // The claim atomically validates the entire binding and snapshots its
336
+ // conversation. A pre-claim session read is not relay authority.
337
+ promptResult = buildAgentPrompt({ ...promptConfig, relayContext: pendingClaim.relayContext,
338
+ executionClaimId: pendingClaim.claimId, contentReviewContext: pendingClaim.contentReviewContext });
339
+ } catch (error) {
340
+ const released = releasePendingForFailedRelay(projectDir, args);
341
+ outputResult({ status: 'error', code: released.saved ? (error.code || 'RELAY_PROMPT_INVALID') : 'SESSION_WRITE_FAILED',
342
+ error: released.saved ? error.message : released.error, agent: args.agent });
343
+ process.exit(1);
344
+ }
345
+ }
298
346
 
299
347
  // Spawn the agent terminal
300
348
  const startTime = Date.now();
301
349
  let handle;
350
+ let retryBudgetExhausted = false;
351
+ let qaSubject;
302
352
 
303
353
  try {
354
+ const recoveryContext = pendingClaim.claimed ? pendingClaim.contentReviewContext : contentReviewContext;
355
+ const revalidation = recoveryContext?.planning_reconciliation || recoveryContext?.council_revalidation;
356
+ const contractRevision = recoveryContext?.planning_contract_revision;
357
+ if (contractRevision) {
358
+ // The executing owner may replace only its own output. Keep every other
359
+ // Planning input descriptor frozen across retries, interaction and hold.
360
+ qaSubject = pinPlanningContractRevisionInputs(projectDir, args.agent, contractRevision.execution_subject);
361
+ process.once('exit', () => qaSubject.close());
362
+ } else if (revalidation) {
363
+ // Retain the same input descriptors for the entire provider lifetime,
364
+ // including retries and final persistence. Hashes at two endpoints alone
365
+ // cannot detect a writer changing and restoring an input during review.
366
+ qaSubject = pinPlanningQaSubject(projectDir, revalidation.artifacts, { inputsOnly: true, excludeQaReport: true });
367
+ process.once('exit', () => qaSubject.close());
368
+ }
369
+ const budget = createAgentAttemptBudget(timeout, pendingClaim.claimed ? relayDeadline : executionDeadline);
304
370
  const spawnConfig = {
305
371
  agent: args.agent,
306
372
  taskId: args['task-id'],
@@ -326,22 +392,32 @@ async function main() {
326
392
  };
327
393
  handle = await spawnTerminalWithRetry(spawnConfig, {
328
394
  maxRetries: 1,
329
- baseDelay: 500,
395
+ baseDelay: AGENT_RETRY_DELAY_MS,
396
+ // This also runs after backoff, so a scheduler pause cannot turn an
397
+ // earlier admission decision into an expired claim launching a child.
398
+ beforeAttempt: () => { budget.assertCanStart(); qaSubject?.assertCurrent(); },
330
399
  // The orchestrator must execute the routed model exactly. A silent
331
400
  // downgrade would make the persisted binding and cost record false.
332
401
  enableModelFallback: false,
333
402
  shouldRetry(exitCode, output) {
334
403
  const combined = Array.isArray(output) ? output.join('') : String(output || '');
335
- return isTransientFailure(exitCode, combined) || /not logged in|not authenticated/i.test(combined);
404
+ const retryable = isTransientFailure(exitCode, combined) || /not logged in|not authenticated/i.test(combined);
405
+ if (retryable && !budget.canStart(AGENT_RETRY_DELAY_MS)) {
406
+ retryBudgetExhausted = true;
407
+ return false;
408
+ }
409
+ return retryable;
336
410
  },
337
411
  });
412
+ qaSubject?.assertCurrent();
338
413
  } catch (err) {
339
414
  const relayState = releasePendingForFailedRelay(projectDir, args);
415
+ const diagnostic = { elapsed: Date.now() - startTime, execution_diagnostics: getTerminalAttemptDiagnostics(err) };
340
416
  if (!relayState.saved) {
341
- outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message });
417
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message, ...diagnostic });
342
418
  process.exit(1);
343
419
  }
344
- outputError(`Failed to spawn terminal: ${err.message}`);
420
+ outputResult({ status: 'error', code: err.code || 'TERMINAL_SPAWN_FAILED', error: `Failed to spawn terminal: ${err.message}`, agent: args.agent, ...diagnostic });
345
421
  process.exit(1);
346
422
  }
347
423
 
@@ -399,7 +475,7 @@ async function main() {
399
475
  await flushAndSend(projectDir);
400
476
 
401
477
  // Parse the handoff from stdout
402
- const parsed = parseAgentOutput(stdout);
478
+ const parsed = parseAgentOutput(stdout, { strict: args.agent === 'qa-planning' });
403
479
  const recovered = recoverInteractiveHandoff(args.agent, stdout, handle.exitCode);
404
480
 
405
481
  if (handle.exitCode !== 0 && /not logged in|not authenticated/i.test(providerOutput)) {
@@ -415,6 +491,8 @@ async function main() {
415
491
  provider: spawnProvider,
416
492
  exitCode: handle.exitCode,
417
493
  retryCount: handle.retryCount || 0,
494
+ elapsed,
495
+ execution_diagnostics: getTerminalAttemptDiagnostics(handle),
418
496
  });
419
497
  process.exit(1);
420
498
  }
@@ -438,25 +516,58 @@ async function main() {
438
516
  exitCode: handle.exitCode,
439
517
  retryCount: handle.retryCount || 0,
440
518
  agent: args.agent,
519
+ elapsed,
520
+ execution_diagnostics: getTerminalAttemptDiagnostics(handle),
521
+ ...(retryBudgetExhausted ? { retry_suppressed: 'AGENT_EXECUTION_BUDGET_EXHAUSTED' } : {}),
441
522
  });
442
523
  process.exit(1);
443
524
  }
444
525
 
445
526
  if (parsed.found && parsed.valid) {
446
- const persistence = persistInteractiveState(projectDir, args, parsed.handoff, promptResult.model, spawnProvider);
447
- if (!persistence.saved) {
448
- outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
527
+ if (!['complete', 'needs_input'].includes(parsed.handoff.status)) {
528
+ // A zero OS exit is not a successful agent result. Keep the pending
529
+ // question and exact response recoverable, just as for process failure.
530
+ const relayState = releasePendingForFailedRelay(projectDir, args);
531
+ if (!relayState.saved) {
532
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: 'AGENT_RESULT_INCOMPLETE' });
533
+ process.exit(1);
534
+ }
535
+ outputResult({ status: 'error', code: 'AGENT_RESULT_INCOMPLETE',
536
+ error: `Agent returned ${parsed.handoff.status}; no successful result was held.`,
537
+ agent: args.agent, provider: spawnProvider, model: promptResult.model,
538
+ exitCode: handle.exitCode, handoff: parsed.handoff, elapsed, costEstimate });
449
539
  process.exit(1);
450
540
  }
451
- if (parsed.handoff.status !== 'needs_input' && handle.exitCode === 0) {
452
- const held = holdAgentExecutionForAdvance(projectDir, {
453
- agent: args.agent,
454
- claimId: args['interaction-id'] ? null : (args['execution-claim-id'] || null),
455
- });
456
- if (!held.saved || !held.held) {
457
- outputResult({ status: 'error', code: held.code || 'SESSION_WRITE_FAILED', error: held.error, agent: args.agent });
541
+ let qaResult;
542
+ if (args.agent === 'qa-planning' && parsed.handoff.status === 'complete') {
543
+ const normalized = normalizePlanningQaOutcome(parsed.handoff, getQaThreshold(args.agent, projectDir));
544
+ if (!normalized.ok) {
545
+ const released = releasePendingForFailedRelay(projectDir, args);
546
+ outputResult({ status: 'error', code: released.saved ? normalized.code : 'SESSION_WRITE_FAILED',
547
+ error: released.saved ? normalized.error : released.error, agent: args.agent });
458
548
  process.exit(1);
459
549
  }
550
+ qaResult = normalized.outcome;
551
+ }
552
+ const persistence = parsed.handoff.status === 'complete'
553
+ ? holdAgentExecutionForAdvance(projectDir, {
554
+ agent: args.agent, taskId: args['task-id'], resultStatus: parsed.handoff.status,
555
+ interactionId: args['interaction-id'] || null,
556
+ claimId: args['interaction-id'] ? args['relay-claim-id'] : (args['execution-claim-id'] || null),
557
+ binding: { provider: spawnProvider, provider_id: args['provider-id'] || null, model: promptResult.model,
558
+ reasoning_configuration: args['reasoning-configuration'] || null,
559
+ catalog_snapshot_ref: args['catalog-snapshot-ref'] || null },
560
+ ...(qaResult ? { qaResult } : {}),
561
+ ...(qaSubject ? { verifySubject: () => qaSubject.assertCurrent() } : {}),
562
+ })
563
+ : persistInteractiveState(projectDir, args, parsed.handoff, promptResult.model, spawnProvider);
564
+ if (!persistence.saved) {
565
+ outputResult({ status: 'error', code: persistence.code || 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
566
+ process.exit(1);
567
+ }
568
+ if (parsed.handoff.status === 'complete' && !persistence.held) {
569
+ outputResult({ status: 'error', code: persistence.code || 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
570
+ process.exit(1);
460
571
  }
461
572
  cleanupResponseFile(responseFile.filePath);
462
573
  outputResult({
@@ -517,6 +628,9 @@ async function main() {
517
628
  elapsed,
518
629
  costEstimate,
519
630
  });
631
+ // The provider's zero exit does not satisfy the handoff contract. Keep
632
+ // the relay/response recoverable, but report runner failure to its caller.
633
+ process.exit(1);
520
634
  }
521
635
 
522
636
  process.exit(handle.exitCode === 0 ? 0 : 1);
@@ -554,11 +668,18 @@ function persistInteractiveState(projectDir, args, handoff, model, provider) {
554
668
  model,
555
669
  reasoningConfiguration: args['reasoning-configuration'] || null,
556
670
  catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
557
- advanceRelay: Boolean(args['additional-context']?.trim()),
558
- });
559
- recordEvent(projectDir, EventType.AGENT_WAITING_FOR_INPUT, args.agent, {
560
- task_id: args['task-id'],
671
+ advanceRelay: Boolean(args['interaction-id'] && args['relay-claim-id']),
672
+ initialTaskContext: args['interaction-id'] ? undefined : args['additional-context'],
673
+ interactionId: args['interaction-id'] || null,
674
+ claimId: args['relay-claim-id'] || null,
561
675
  });
676
+ if (!result.saved) {
677
+ // Keep the original response recoverable on a rejected/failed commit.
678
+ // Exact claim ownership prevents this release from touching a newer relay.
679
+ releasePendingForFailedRelay(projectDir, args);
680
+ return result;
681
+ }
682
+ recordEvent(projectDir, EventType.AGENT_WAITING_FOR_INPUT, args.agent, { task_id: args['task-id'] });
562
683
  return result;
563
684
  }
564
685
  return clearPendingInteraction(projectDir, {
@@ -566,6 +687,7 @@ function persistInteractiveState(projectDir, args, handoff, model, provider) {
566
687
  taskId: args['task-id'],
567
688
  interactionId: args['interaction-id'] || null,
568
689
  claimId: args['relay-claim-id'] || null,
690
+ requireClaim: Boolean(args['interaction-id']),
569
691
  });
570
692
  }
571
693