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,75 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { canonicalSerialize } from '@chati/core';
4
+ import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
5
+ import { verifyRailEvidenceAuthorityCertificate } from './rail-evidence-authority.js';
6
+ import { verifyClickUpMcpAuthorityCertificate } from './clickup-runtime.js';
7
+ import {
8
+ persistImmutableInstallationFile, readSafeInstallationStorageFile, replaceInstallationStorageFile,
9
+ } from '../installer-v2/catalog-client.js';
10
+
11
+ const MAX_CERTIFICATE_BYTES = 32 * 1024;
12
+ const CERTIFICATE_TYPES = Object.freeze({
13
+ 'chati-rail-evidence-authority': Object.freeze({
14
+ path: '.chati/v2/rail/evidence-authority.json', verify: verifyRailEvidenceAuthorityCertificate,
15
+ }),
16
+ 'chati-clickup-mcp-authority': Object.freeze({
17
+ path: '.chati/v2/tracking/clickup-mcp-authority.json', verify: verifyClickUpMcpAuthorityCertificate,
18
+ }),
19
+ });
20
+
21
+ function fail(code, message) { throw Object.assign(new Error(message), { code }); }
22
+
23
+ /** Public-certificate enrollment only. Never generates or trusts an issuer key. */
24
+ export function enrollProjectEvidenceAuthority({ projectDir, certificate, replace = false, clock = () => new Date() } = {}) {
25
+ const artifact = loadRuntimeInstallationV2(projectDir, { clock });
26
+ if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'evidence authority enrollment requires a verified v2 installation');
27
+ if (typeof replace !== 'boolean') fail('INVALID_AUTHORITY_ENROLLMENT', 'replace must be an explicit boolean');
28
+ const definition = Object.hasOwn(CERTIFICATE_TYPES, certificate?.certificate_type ?? '')
29
+ ? CERTIFICATE_TYPES[certificate.certificate_type] : null;
30
+ if (!definition) fail('INVALID_AUTHORITY_CERTIFICATE_TYPE', 'expected a CHATI RAIL evidence or ClickUp MCP authority certificate');
31
+ const rootPublicKey = readFileSync(new URL('../installer/signing-public-key.pem', import.meta.url), 'utf8');
32
+ // Verify the original closed-schema object before serializing or mutating storage.
33
+ definition.verify({ certificate, expected_project_id: artifact.project_id, root_public_key: rootPublicKey, clock });
34
+ // createPublicKey also accepts private PEM and ignores trailing PEM blocks.
35
+ // Do not persist such input or normalize a signed certificate into new bytes.
36
+ if (typeof certificate.public_key !== 'string'
37
+ || !/^-----BEGIN PUBLIC KEY-----\r?\n(?:[A-Za-z0-9+/=]+\r?\n)+-----END PUBLIC KEY-----$/.test(certificate.public_key.trim())) {
38
+ fail('AUTHORITY_PUBLIC_KEY_REQUIRED', 'certificate public_key must contain exactly one public SPKI PEM block');
39
+ }
40
+ const content = `${canonicalSerialize(certificate)}\n`;
41
+ if (Buffer.byteLength(content) > MAX_CERTIFICATE_BYTES) fail('AUTHORITY_CERTIFICATE_SIZE_LIMIT', 'authority certificate exceeds 32 KiB');
42
+ const path = join(projectDir, definition.path);
43
+ const previous = readSafeInstallationStorageFile(path, {
44
+ allowMissing: true, label: 'evidence authority certificate', maxBytes: MAX_CERTIFICATE_BYTES,
45
+ unsafeCode: 'UNSAFE_AUTHORITY_CERTIFICATE_STORAGE', sizeCode: 'AUTHORITY_CERTIFICATE_SIZE_LIMIT',
46
+ });
47
+ let written = false;
48
+ if (previous === null) {
49
+ written = persistImmutableInstallationFile(path, content, {
50
+ label: 'evidence authority certificate', maxBytes: MAX_CERTIFICATE_BYTES,
51
+ conflictCode: 'AUTHORITY_CERTIFICATE_CONFLICT',
52
+ });
53
+ } else if (previous.content !== content) {
54
+ if (!replace) fail('AUTHORITY_CERTIFICATE_CONFLICT', 'another certificate is enrolled; use --replace explicitly to rotate it');
55
+ replaceInstallationStorageFile(path, content, previous);
56
+ written = true;
57
+ }
58
+ return Object.freeze({
59
+ ok: true, action: 'rail_authority_enrolled', written,
60
+ project_id: artifact.project_id, issuer_id: certificate.issuer_id,
61
+ certificate_type: certificate.certificate_type, certificate_ref: definition.path,
62
+ expires_at: certificate.expires_at,
63
+ });
64
+ }
65
+
66
+ export function enrollProjectEvidenceAuthorityFile({ projectDir, certificatePath, replace = false } = {}) {
67
+ const source = readSafeInstallationStorageFile(certificatePath, {
68
+ label: 'enrollment certificate input', maxBytes: MAX_CERTIFICATE_BYTES,
69
+ unsafeCode: 'UNSAFE_AUTHORITY_CERTIFICATE_INPUT', sizeCode: 'AUTHORITY_CERTIFICATE_SIZE_LIMIT',
70
+ });
71
+ let certificate;
72
+ try { certificate = JSON.parse(source.content); }
73
+ catch { fail('INVALID_AUTHORITY_CERTIFICATE_JSON', 'enrollment certificate must be valid JSON'); }
74
+ return enrollProjectEvidenceAuthority({ projectDir, certificate, replace });
75
+ }
@@ -0,0 +1,46 @@
1
+ import { canonicalSerialize, sha256 } from '@chati/core';
2
+ import { createPublicKey, verify } from 'node:crypto';
3
+ import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
4
+ import { delegatedAuthorityPublicIdentity, signDelegatedAuthorityStatement } from '../installer-v2/installation-authority.js';
5
+
6
+ const REQUEST_DOMAIN = 'chati.evidence-authority-certification-request.v1';
7
+ const CERTIFICATE_TYPES = Object.freeze({
8
+ 'rail-evidence': 'chati-rail-evidence-authority', 'clickup-mcp': 'chati-clickup-mcp-authority',
9
+ });
10
+
11
+ function fail(code, message) { throw Object.assign(new Error(message), { code }); }
12
+
13
+ /** Creates a public request, never an enrolled certificate or a trusted verdict. */
14
+ export function createEvidenceAuthorityRequest({ projectDir, kind, validForDays = 30, clock = () => new Date(), authorityDirectory } = {}) {
15
+ if (!Object.hasOwn(CERTIFICATE_TYPES, kind ?? '')) fail('INVALID_AUTHORITY_REQUEST_KIND', 'kind must be rail-evidence or clickup-mcp');
16
+ if (!Number.isInteger(validForDays) || validForDays < 1 || validForDays > 90) fail('INVALID_AUTHORITY_REQUEST_LIFETIME', 'requested validity must be 1 to 90 days');
17
+ const artifact = loadRuntimeInstallationV2(projectDir, { clock });
18
+ if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'a certification request requires a verified v2 installation');
19
+ const observed = clock();
20
+ const now = (observed instanceof Date ? observed : new Date(observed)).getTime();
21
+ if (!Number.isFinite(now)) fail('INVALID_AUTHORITY_REQUEST_TIME', 'certification request requires a valid time');
22
+ const identityOptions = { projectDir, projectId: artifact.project_id, kind, authorityDirectory };
23
+ const identity = delegatedAuthorityPublicIdentity({ ...identityOptions, create: true });
24
+ const request = {
25
+ schema_version: 1, request_type: REQUEST_DOMAIN,
26
+ requested_issuer_implementation: 'chati-local-evidence-observer-v1',
27
+ project_binding_digest: identity.project_binding_digest,
28
+ certificate_payload: {
29
+ schema_version: 1, certificate_type: CERTIFICATE_TYPES[kind], project_id: artifact.project_id,
30
+ issuer_id: identity.issuer_id, public_key: identity.public_key,
31
+ not_before: new Date(now).toISOString(), expires_at: new Date(now + validForDays * 86400000).toISOString(),
32
+ },
33
+ };
34
+ const proof = signDelegatedAuthorityStatement(request, identityOptions);
35
+ // A concurrent key replacement must not produce a request with mismatched proof.
36
+ if (proof.issuer_id !== identity.issuer_id
37
+ || !verify(null, Buffer.from(canonicalSerialize(request)), createPublicKey(identity.public_key), Buffer.from(proof.signature, 'base64'))) {
38
+ fail('AUTHORITY_REQUEST_IDENTITY_CHANGED', 'delegated identity changed while creating its public request');
39
+ }
40
+ return Object.freeze({
41
+ ok: true, action: 'rail_authority_certification_requested', certified: false,
42
+ request_id: `authority-request-${sha256(request)}`, request,
43
+ proof_of_possession: proof.signature,
44
+ next_required_action: 'authorized_root_reviews_issuer_and_signs_certificate_then_enroll',
45
+ });
46
+ }
@@ -10,9 +10,10 @@ import { readAgentMemory } from '../memory/agent-memory.js';
10
10
  import { getRelevantGotchas } from '../memory/gotchas.js';
11
11
  import { updateProjectContext } from '../memory/magic-docs.js';
12
12
  import { generateContextFiles } from '../config/context-file-generator.js';
13
- import { existsSync, readdirSync } from 'fs';
13
+ import { existsSync } from 'fs';
14
14
  import { join } from 'path';
15
15
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
16
+ import { listHandoffAgents } from '../utils/handoff-paths.js';
16
17
 
17
18
  /**
18
19
  * Execute a full agent-to-agent handoff.
@@ -228,17 +229,9 @@ export function loadHandoffContext(projectDir, fromAgent) {
228
229
  * @returns {object[]} Array of handoff summaries
229
230
  */
230
231
  export function getHandoffHistory(projectDir) {
231
- const handoffsDir = join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs');
232
-
233
- if (!existsSync(handoffsDir)) {
234
- return [];
235
- }
236
-
237
232
  try {
238
- const files = readdirSync(handoffsDir)
239
- .filter(f => f.endsWith('-handoff.md'))
240
- .map(f => {
241
- const agentName = f.replace('-handoff.md', '');
233
+ const files = listHandoffAgents(projectDir)
234
+ .map(agentName => {
242
235
  const result = loadHandoff(projectDir, agentName);
243
236
 
244
237
  if (!result.loaded) {
@@ -138,6 +138,24 @@ function loadQaThresholds(projectDir) {
138
138
  return out;
139
139
  }
140
140
 
141
+ export function getQaThreshold(agent, projectDir) {
142
+ return loadQaThresholds(projectDir)[agent] ?? null;
143
+ }
144
+
145
+ /** A controller may explicitly reopen completed Planning dependencies, never advance. */
146
+ export function assertQaCorrectionTarget(state, agent, results) {
147
+ if (!results.correctionTarget) return;
148
+ const rejected = results.qaVerdict === 'needs_correction'
149
+ || (Number.isFinite(results.score) && results.score < getQaThreshold(agent, state.targetDir));
150
+ if (agent !== 'qa-planning' || !rejected
151
+ || !['detail', 'architect', 'ux'].includes(results.correctionTarget)
152
+ || !state.completedAgents.includes(results.correctionTarget)
153
+ || !results.handoffData?.decision_trail_entries?.some(entry => entry.resolved === false)) {
154
+ throw Object.assign(new Error('An explicit QA correction target requires a completed negative review, unresolved findings, and a completed upstream Planning agent.'),
155
+ { code: 'INVALID_QA_CORRECTION_TARGET' });
156
+ }
157
+ }
158
+
141
159
  /**
142
160
  * Route a failing QA to the correct rework target via Fault Vector Protocol.
143
161
  *
@@ -396,6 +414,7 @@ export function initStandardFlowPipeline(options = {}) {
396
414
  * @returns {{ state: object, nextAction: string, nextAgent: string|null, needsModeSwitch: boolean, contextBracket: object }}
397
415
  */
398
416
  export function advancePipeline(pipelineState, completedAgent, results = {}) {
417
+ assertQaCorrectionTarget(pipelineState, completedAgent, results);
399
418
  const newState = { ...pipelineState };
400
419
 
401
420
  // Update completed agent status
@@ -406,7 +425,7 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
406
425
  newState.agents[completedAgent] = {
407
426
  ...newState.agents[completedAgent],
408
427
  status: AGENT_STATUS.COMPLETED,
409
- score: results.score || null,
428
+ score: results.score ?? null,
410
429
  completedAt: new Date().toISOString(),
411
430
  };
412
431
 
@@ -426,15 +445,30 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
426
445
  agent: completedAgent,
427
446
  action: 'completed',
428
447
  timestamp: new Date().toISOString(),
429
- score: results.score || null,
448
+ score: results.score ?? null,
430
449
  });
431
450
 
432
451
  // Special handling for QA agents - check if they meet thresholds
433
452
  const isQAAgent = completedAgent === 'qa-planning' || completedAgent === 'qa-implementation' || completedAgent === 'qa-visual';
434
453
  if (isQAAgent) {
435
454
  const transitionCheck = checkPhaseTransition(newState);
455
+ const rejected = results.qaVerdict === 'needs_correction'
456
+ || !Number.isFinite(results.score)
457
+ || results.score < getQaThreshold(completedAgent, newState.targetDir);
458
+
459
+ // QA completion precedes Phases/Tasks in Team Mode. Missing later agents
460
+ // prevent a phase transition, not an otherwise approved QA result.
461
+ if (completedAgent === 'qa-planning' && newState.phase === 'plan'
462
+ && !rejected && !transitionCheck.canAdvance
463
+ && ['phases', 'tasks'].some(agent => newState.agents[agent]?.status !== AGENT_STATUS.COMPLETED)) {
464
+ return {
465
+ state: newState, nextAction: 'continue',
466
+ nextAgent: getNextAgent(completedAgent, newState.completedAgents).next,
467
+ needsModeSwitch: false, contextBracket,
468
+ };
469
+ }
436
470
 
437
- if (transitionCheck.canAdvance) {
471
+ if (!rejected && transitionCheck.canAdvance) {
438
472
  // User Preview gate: when the final QA gate (qa-visual) passes in build phase,
439
473
  // don't advance to deploy yet — let user preview first. (4.3.0+: qa-visual is
440
474
  // the final build gate; previously this was qa-implementation.)
@@ -491,11 +525,15 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
491
525
  // QA failed — trigger backward transition via Fault Vector Protocol.
492
526
  // We re-open the rework target, NOT wait for manual intervention.
493
527
  const faultOrigin = (results.faultOrigin || extractPrimaryFaultOrigin(results.handoffData)).toUpperCase();
494
- const { nextAgent: correctionTarget, reason: routingReason } = routeByFaultVector(
528
+ const routed = routeByFaultVector(
495
529
  completedAgent,
496
530
  faultOrigin,
497
531
  newState,
498
532
  );
533
+ const correctionTarget = results.correctionTarget || routed.nextAgent;
534
+ const routingReason = results.correctionTarget
535
+ ? `${completedAgent} requires correction; the controller selected upstream ${correctionTarget} from the preserved findings.`
536
+ : routed.reason;
499
537
 
500
538
  // DEFER or unroutable — log and stay in wait (legacy behavior)
501
539
  if (!correctionTarget) {