0xray 4.0.40 → 4.0.42

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 (190) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/agents/api-design.yml +31 -0
  3. package/.opencode/agents/architect.yml +15 -0
  4. package/.opencode/agents/architecture-patterns.yml +14 -0
  5. package/.opencode/agents/auto-format.yml +14 -0
  6. package/.opencode/agents/backend-engineer.yml +12 -0
  7. package/.opencode/agents/boot-orchestrator.yml +14 -0
  8. package/.opencode/agents/bug-triage-specialist.yml +15 -0
  9. package/.opencode/agents/code-analyzer.yml +12 -0
  10. package/.opencode/agents/code-reviewer.yml +14 -0
  11. package/.opencode/agents/content-creator.yml +12 -0
  12. package/.opencode/agents/database-engineer.yml +12 -0
  13. package/.opencode/agents/devops-engineer.yml +12 -0
  14. package/.opencode/agents/framework-compliance-audit.yml +14 -0
  15. package/.opencode/agents/frontend-engineer.yml +12 -0
  16. package/.opencode/agents/frontend-ui-ux-engineer.yml +12 -0
  17. package/.opencode/agents/git-workflow.yml +15 -0
  18. package/.opencode/agents/growth-strategist.yml +12 -0
  19. package/.opencode/agents/hermes-agent.yml +12 -0
  20. package/.opencode/agents/inference-improve.yml +12 -0
  21. package/.opencode/agents/lint.yml +14 -0
  22. package/.opencode/agents/log-monitor.yml +12 -0
  23. package/.opencode/agents/mobile-developer.yml +12 -0
  24. package/.opencode/agents/model-health-check.yml +12 -0
  25. package/.opencode/agents/multimodal-looker.yml +12 -0
  26. package/.opencode/agents/performance-analysis.yml +12 -0
  27. package/.opencode/agents/performance-engineer.yml +12 -0
  28. package/.opencode/agents/performance-optimization.yml +12 -0
  29. package/.opencode/agents/processor-pipeline.yml +14 -0
  30. package/.opencode/agents/project-analysis.yml +12 -0
  31. package/.opencode/agents/refactorer.yml +12 -0
  32. package/.opencode/agents/researcher.yml +12 -0
  33. package/.opencode/agents/security-auditor.yml +12 -0
  34. package/.opencode/agents/security-scan.yml +12 -0
  35. package/.opencode/agents/seo-consultant.yml +12 -0
  36. package/.opencode/agents/session-management.yml +12 -0
  37. package/.opencode/agents/state-manager.yml +12 -0
  38. package/.opencode/agents/storyteller.yml +12 -0
  39. package/.opencode/agents/strategist.yml +12 -0
  40. package/.opencode/agents/tech-writer.yml +12 -0
  41. package/.opencode/agents/testing-best-practices.yml +12 -0
  42. package/.opencode/agents/testing-lead.yml +12 -0
  43. package/.opencode/agents/ui-ux-design.yml +12 -0
  44. package/.opencode/codex.codex +8 -0
  45. package/.opencode/commands/auto-format.md +99 -0
  46. package/.opencode/commands/auto-summary-capture.md +90 -0
  47. package/.opencode/commands/dependency-audit.md +184 -0
  48. package/.opencode/commands/enforcer-daily-scan.md +137 -0
  49. package/.opencode/commands/framework-compliance-audit.md +205 -0
  50. package/.opencode/commands/interactive-validator.md +75 -0
  51. package/.opencode/commands/job-summary-logger.md +68 -0
  52. package/.opencode/commands/lint.md +11 -0
  53. package/.opencode/commands/mode-switch.md +95 -0
  54. package/.opencode/commands/model-health-check.md +186 -0
  55. package/.opencode/commands/performance-analysis.md +144 -0
  56. package/.opencode/commands/pre-commit-introspection.md +185 -0
  57. package/.opencode/commands/pre-commit-introspection.sh +133 -0
  58. package/.opencode/commands/security-scan.md +157 -0
  59. package/.opencode/commands/sisyphus-validation.md +128 -0
  60. package/.opencode/commands/summary-logger.md +83 -0
  61. package/.opencode/enforcer-config.json +282 -0
  62. package/.opencode/skills/orchestrator/SKILL.md +12 -0
  63. package/.opencode/workflows/post-deployment-audit.yml +123 -0
  64. package/.xray/features.json +1 -1
  65. package/.xray/features.schema.json +1 -1
  66. package/AGENTS-consumer.md +2 -0
  67. package/AGENTS.md +2 -0
  68. package/README.md +52 -12
  69. package/SKILLS.md +2 -0
  70. package/dist/AGENTS.md +2 -0
  71. package/dist/CHANGELOG.md +29 -0
  72. package/dist/README.md +52 -12
  73. package/dist/SKILLS.md +2 -0
  74. package/dist/cli/commands/openclaw-install.js +1 -1
  75. package/dist/cli/index.js +12 -0
  76. package/dist/enforcement/index.d.ts +4 -0
  77. package/dist/enforcement/index.js +4 -0
  78. package/dist/enforcement/validators/code-quality-validators.js +6 -4
  79. package/dist/governance/llm-governance-provider.d.ts +7 -1
  80. package/dist/governance/llm-governance-provider.js +72 -14
  81. package/dist/governance/local-confer.d.ts +4 -1
  82. package/dist/governance/local-confer.js +22 -1
  83. package/dist/integrations/grok/grok-cli.js +10 -37
  84. package/dist/integrations/grok/hooks/grok-hook-utils.js +29 -2
  85. package/dist/integrations/grok/hooks/session-start.js +2 -20
  86. package/dist/integrations/hooks/confer-hook-runtime.mjs +23 -1
  87. package/dist/integrations/hooks/goggles-mcp.mjs +36 -4
  88. package/dist/integrations/hooks/goggles-pipeline.mjs +55 -15
  89. package/dist/integrations/hooks/goggles-planes.json +13 -4
  90. package/dist/integrations/hooks/plates.cjs +22 -0
  91. package/dist/integrations/hooks/session-note-index.cjs +298 -0
  92. package/dist/integrations/hooks/session-note-index.mjs +22 -0
  93. package/dist/integrations/hooks/sleeve.cjs +335 -0
  94. package/dist/integrations/hooks/sleeve.mjs +18 -0
  95. package/dist/integrations/hooks/station-hook-runtime.cjs +205 -9
  96. package/dist/integrations/hooks/wake-cascade.cjs +102 -0
  97. package/dist/integrations/hooks/wake-cascade.mjs +16 -0
  98. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  99. package/dist/mcps/architect-tools.server.d.ts +6 -0
  100. package/dist/mcps/architect-tools.server.js +19 -2
  101. package/dist/mcps/config/server-config-registry.js +8 -6
  102. package/dist/mcps/governance.server.d.ts +1 -0
  103. package/dist/mcps/governance.server.js +46 -0
  104. package/dist/mcps/knowledge-skills/code-review.server.js +7 -0
  105. package/dist/mcps/knowledge-skills/security-audit.server.js +5 -3
  106. package/dist/mcps/mcp-client.js +1 -1
  107. package/dist/mcps/orchestrator/handlers/complexity-handler.js +2 -2
  108. package/dist/mcps/researcher.server.js +12 -3
  109. package/dist/memory-routing/plates.d.ts +1 -1
  110. package/dist/memory-routing/plates.js +1 -0
  111. package/dist/nucleus/autonomy-kernel.d.ts +2 -0
  112. package/dist/nucleus/autonomy-kernel.js +17 -13
  113. package/dist/nucleus/confer-host.d.ts +35 -0
  114. package/dist/nucleus/confer-host.js +55 -0
  115. package/dist/nucleus/confer.d.ts +19 -3
  116. package/dist/nucleus/confer.js +111 -55
  117. package/dist/nucleus/lead-dev-plan-persistence.js +4 -2
  118. package/dist/nucleus/suit-temperament.d.ts +1 -1
  119. package/dist/nucleus/suit-temperament.js +2 -2
  120. package/dist/nucleus/synthesis-consult-receipt.d.ts +7 -1
  121. package/dist/nucleus/synthesis-consult-receipt.js +86 -35
  122. package/dist/reporting/framework-reporting-system.js +2 -5
  123. package/dist/reporting/log-parser.d.ts +2 -0
  124. package/dist/reporting/log-parser.js +7 -6
  125. package/dist/skills/orchestrator/SKILL.md +12 -0
  126. package/docs-site/docs/plates/activity-log.md +1 -1
  127. package/docs-site/docs/plates/boot.md +2 -0
  128. package/docs-site/docs/plates/centrifuge.md +30 -0
  129. package/docs-site/docs/plates/cursor-compact.md +1 -1
  130. package/docs-site/docs/plates/glossary.md +1 -2
  131. package/docs-site/docs/plates/goggles.md +4 -0
  132. package/docs-site/docs/plates/governance.md +2 -0
  133. package/docs-site/docs/plates/grok-compact.md +1 -1
  134. package/docs-site/docs/plates/grokbot.md +1 -1
  135. package/docs-site/docs/plates/host-pack.md +2 -0
  136. package/docs-site/docs/plates/house.md +1 -1
  137. package/docs-site/docs/plates/index.md +3 -1
  138. package/docs-site/docs/plates/inference-files.md +1 -1
  139. package/docs-site/docs/plates/kept-line.md +1 -1
  140. package/docs-site/docs/plates/kits.md +2 -0
  141. package/docs-site/docs/plates/lens-gate.md +1 -1
  142. package/docs-site/docs/plates/lens-page.md +1 -1
  143. package/docs-site/docs/plates/live-mesh.md +1 -1
  144. package/docs-site/docs/plates/memory-recall.md +4 -0
  145. package/docs-site/docs/plates/notes-page.md +2 -1
  146. package/docs-site/docs/plates/orchestration.md +2 -0
  147. package/docs-site/docs/plates/package-face.md +1 -1
  148. package/docs-site/docs/plates/payload-heat.md +1 -1
  149. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  150. package/docs-site/docs/plates/pre-tool.md +1 -1
  151. package/docs-site/docs/plates/processor.md +2 -0
  152. package/docs-site/docs/plates/record-map.md +2 -1
  153. package/docs-site/docs/plates/reflection-page.md +1 -1
  154. package/docs-site/docs/plates/reporting.md +2 -0
  155. package/docs-site/docs/plates/review.md +1 -1
  156. package/docs-site/docs/plates/routing.md +2 -0
  157. package/docs-site/docs/plates/session-capture.md +1 -1
  158. package/docs-site/docs/plates/site-manual.md +1 -1
  159. package/docs-site/docs/plates/stamp-plate.md +1 -1
  160. package/docs-site/docs/plates/station-card.md +4 -1
  161. package/docs-site/docs/plates/station-heat.md +1 -1
  162. package/docs-site/docs/plates/suit-organs.md +1 -1
  163. package/docs-site/docs/plates/suit-settings.md +1 -1
  164. package/docs-site/docs/plates/suit-wear.md +3 -1
  165. package/docs-site/docs/plates/suit.md +2 -0
  166. package/docs-site/docs/plates/trail-state.md +1 -1
  167. package/docs-site/docs/plates/work-fresh.md +1 -1
  168. package/docs-site/docs/plates/write-home.md +1 -1
  169. package/llms.txt +2 -0
  170. package/package.json +2 -2
  171. package/scripts/mjs/verify-confer-core.mjs +153 -29
  172. package/scripts/mjs/verify-user-aside-core.mjs +3 -4
  173. package/scripts/node/bridge-mcp-wiring.cjs +273 -9
  174. package/scripts/node/confer-switch.cjs +66 -0
  175. package/scripts/node/install-bridges.cjs +188 -60
  176. package/scripts/node/setup.cjs +2 -46
  177. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  178. package/src/mcps/architect-tools.server.ts +26 -2
  179. package/src/mcps/config/__tests__/server-config-registry.test.ts +15 -6
  180. package/src/mcps/config/server-config-registry.ts +9 -6
  181. package/src/mcps/governance.server.ts +49 -0
  182. package/src/mcps/knowledge-skills/code-review.server.ts +8 -0
  183. package/src/mcps/knowledge-skills/security-audit.server.ts +11 -12
  184. package/src/mcps/mcp-client.ts +1 -1
  185. package/src/mcps/orchestrator/handlers/complexity-handler.ts +2 -1
  186. package/src/mcps/researcher.server.ts +19 -10
  187. package/src/skills/orchestrator/SKILL.md +12 -0
  188. package/vendor/@0xray/repertoire/scripts/run-live-synthesis-checkpoint.mjs +5 -51
  189. package/xray/features.json +1 -1
  190. package/xray/features.schema.json +1 -1
@@ -7,7 +7,7 @@
7
7
  import * as fs from "fs";
8
8
  import * as path from "path";
9
9
  import { XrayKnowledgeSkillBase } from "../shared/knowledge-skill-base.js";
10
- import { isGovernanceLlmConfigured, tryLLMGovernance, } from "../../governance/llm-governance-provider.js";
10
+ import { attemptLLMGovernance } from "../../governance/llm-governance-provider.js";
11
11
  import { formatGovernanceVoteText, localConferVote } from "../../governance/local-confer.js";
12
12
  class XraySecurityAuditServer extends XrayKnowledgeSkillBase {
13
13
  constructor() {
@@ -661,10 +661,12 @@ class XraySecurityAuditServer extends XrayKnowledgeSkillBase {
661
661
  */
662
662
  async analyzeProposal(args) {
663
663
  const { proposalTitle = "", proposalDescription = "", evidence = [], proposalType = "" } = args;
664
- const vote = (await tryLLMGovernance("security-audit", proposalTitle, proposalDescription, evidence, proposalType)) ??
664
+ const attempt = await attemptLLMGovernance("security-audit", proposalTitle, proposalDescription, evidence, proposalType);
665
+ const vote = attempt.vote ??
665
666
  localConferVote({
666
667
  role: "security-audit",
667
- llmConfigured: isGovernanceLlmConfigured(),
668
+ llmConfigured: attempt.miss !== "not-configured",
669
+ ...(attempt.miss ? { miss: attempt.miss } : {}),
668
670
  });
669
671
  return {
670
672
  content: [
@@ -319,7 +319,7 @@ export class MCPClient extends EventEmitter {
319
319
  };
320
320
  this.emit('tool.before', beforeEvent);
321
321
  const serverName = this.config.serverName;
322
- const isGovernanceServer = ['code-review', 'security-audit', 'researcher', 'governance'].includes(serverName);
322
+ const isGovernanceServer = ['code-review', 'security-audit', 'researcher', 'architect', 'governance'].includes(serverName);
323
323
  const isGovernanceTool = toolName === 'analyze_proposal';
324
324
  const preferReal = this.isPureMcpMode || isGovernanceServer || isGovernanceTool;
325
325
  try {
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { frameworkLogger } from '../../../core/framework-logger.js';
7
7
  import { buildLeadDevPlan, buildSynthesisCheckpointPlan, isLeadDevModeActive, persistLeadDevPlan, } from '../../../nucleus/autonomy-kernel.js';
8
- import { formatConferQuorumReport, isConferEnabled, runConferQuorum, } from '../../../nucleus/confer.js';
8
+ import { conferFixtureAllowed, formatConferQuorumReport, isConferEnabled, runConferQuorum, } from '../../../nucleus/confer.js';
9
9
  import { bindPlanToSession } from '../../../nucleus/lead-dev-plan-persistence.js';
10
10
  import { clearPendingDelegations } from '../../../nucleus/pending-delegations.js';
11
11
  import { isAutoProvisionWorktreeEnabled, provisionGitWorktree, } from '../../../nucleus/aside-worktree.js';
@@ -177,7 +177,7 @@ export class ComplexityHandler {
177
177
  args.sessionId) {
178
178
  const conferOpts = {
179
179
  dueReason: args.synthesisDueReason ?? null,
180
- fixture: args.conferFixture === true,
180
+ fixture: conferFixtureAllowed() && args.conferFixture === true,
181
181
  };
182
182
  if (args.collocatedText)
183
183
  conferOpts.collocatedText = args.collocatedText;
@@ -10,10 +10,11 @@
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
12
  import { frameworkLogger } from "../core/framework-logger.js";
13
- import { isGovernanceLlmConfigured, tryLLMGovernance, } from "../governance/llm-governance-provider.js";
13
+ import { attemptLLMGovernance, } from "../governance/llm-governance-provider.js";
14
14
  import { formatGovernanceVoteText, localConferVote } from "../governance/local-confer.js";
15
15
  import { initializeMemoryRouting } from "../memory-routing/index.js";
16
16
  import { buildMemoryRoutingEvidence, formatMemoryRoutingBlock, resolveResearcherMemoryContext, } from "./researcher-confidence.js";
17
+ import { buildHostConferEvidence } from "../nucleus/confer-host.js";
17
18
  import { XrayKnowledgeSkillBase } from "./shared/knowledge-skill-base.js";
18
19
  class XrayLibrarianServer extends XrayKnowledgeSkillBase {
19
20
  constructor() {
@@ -382,6 +383,12 @@ class XrayLibrarianServer extends XrayKnowledgeSkillBase {
382
383
  */
383
384
  async analyzeProposal(args) {
384
385
  const { proposalTitle = "", proposalDescription = "", evidence = [], proposalType = "" } = args || {};
386
+ if (proposalType === "synthesis-confer") {
387
+ const packet = [proposalTitle, proposalDescription, ...(evidence || [])].filter(Boolean).join("\n\n");
388
+ return {
389
+ content: [{ type: "text", text: buildHostConferEvidence("researcher", packet) }],
390
+ };
391
+ }
385
392
  const memoryContext = await resolveResearcherMemoryContext({
386
393
  proposalTitle,
387
394
  proposalDescription,
@@ -391,12 +398,14 @@ class XrayLibrarianServer extends XrayKnowledgeSkillBase {
391
398
  if (memoryContext) {
392
399
  enrichedEvidence.push(...buildMemoryRoutingEvidence(memoryContext));
393
400
  }
394
- const vote = (await tryLLMGovernance("researcher", proposalTitle || "", proposalDescription || "", enrichedEvidence, proposalType || "")) ??
401
+ const attempt = await attemptLLMGovernance("researcher", proposalTitle || "", proposalDescription || "", enrichedEvidence, proposalType || "");
402
+ const vote = attempt.vote ??
395
403
  localConferVote({
396
404
  role: "researcher",
397
- llmConfigured: isGovernanceLlmConfigured(),
405
+ llmConfigured: attempt.miss !== "not-configured",
398
406
  highConfidenceTrapPresent: memoryContext?.confidence.highConfidenceTrapPresent,
399
407
  recommendedAgent: memoryContext?.recommendedAgent,
408
+ ...(attempt.miss ? { miss: attempt.miss } : {}),
400
409
  });
401
410
  const memoryRoutingBlock = memoryContext
402
411
  ? `\n${formatMemoryRoutingBlock(memoryContext)}`
@@ -1,4 +1,4 @@
1
- export declare const PLATE_IDS: readonly ["routing", "governance", "boot", "orchestration", "processor", "reporting", "memory-recall", "stamp-plate", "house", "review", "goggles", "suit", "kits", "host-pack", "glossary", "record-map", "write-home", "activity-log", "session-capture", "suit-wear", "suit-organs", "station-card", "notes-page", "reflection-page", "site-manual", "package-face", "suit-settings", "trail-state", "inference-files", "grok-compact", "payload-heat", "station-heat", "pickup-stamp", "cursor-compact", "work-fresh", "kept-line", "lens-gate", "pre-tool", "lens-page"];
1
+ export declare const PLATE_IDS: readonly ["routing", "governance", "boot", "orchestration", "processor", "reporting", "memory-recall", "stamp-plate", "house", "review", "goggles", "suit", "kits", "host-pack", "glossary", "record-map", "write-home", "activity-log", "session-capture", "suit-wear", "suit-organs", "station-card", "notes-page", "reflection-page", "site-manual", "package-face", "suit-settings", "trail-state", "inference-files", "grok-compact", "payload-heat", "station-heat", "pickup-stamp", "cursor-compact", "work-fresh", "kept-line", "lens-gate", "pre-tool", "lens-page", "centrifuge"];
2
2
  export type PlateId = (typeof PLATE_IDS)[number];
3
3
  export interface PlateStamp {
4
4
  id: PlateId;
@@ -45,6 +45,7 @@ export const PLATE_IDS = [
45
45
  'lens-gate',
46
46
  'pre-tool',
47
47
  'lens-page',
48
+ 'centrifuge',
48
49
  ];
49
50
  function isPlateId(value) {
50
51
  return PLATE_IDS.includes(value);
@@ -38,6 +38,8 @@ export interface LeadDevPlan {
38
38
  fullSuiteGate: boolean;
39
39
  hint: string;
40
40
  };
41
+ /** New id each synthesis plan. Receipts from an older id cannot complete s.1–s.3. */
42
+ consultCycleId?: string;
41
43
  }
42
44
  export declare function isLeadDevModeActive(): boolean;
43
45
  export declare function routeSubagent(taskType: string): SubagentRoute;
@@ -2,11 +2,11 @@
2
2
  * Lead-dev plan builder — internal implementation for orchestrator + hooks.
3
3
  * Config: features.json → multi_agent_orchestration (lead_dev_mode), NOT a separate surface.
4
4
  */
5
+ import { randomUUID } from 'node:crypto';
5
6
  import * as fs from 'fs';
6
7
  import * as path from 'path';
7
8
  import { featuresConfigLoader } from '../core/features-config.js';
8
9
  import { scoreComplexity } from './thin-dispatch.js';
9
- import { conferDefaultForProfile, resolveRuntimeSuitProfile } from './suit-temperament.js';
10
10
  export const LEAD_DEV_RULES = [
11
11
  'Phased plan + detailed todos; assign best subagent; monitor output; iterate fully',
12
12
  'Lead dev takes helm — loop test fix until complete; no permission pings',
@@ -33,6 +33,19 @@ const TASK_TYPE_ROUTES = {
33
33
  security: 'security-audit',
34
34
  governance: 'enforcer',
35
35
  };
36
+ function conferExplicitlyEnabled(projectRoot) {
37
+ const featuresPath = path.join(projectRoot, '.xray', 'features.json');
38
+ if (!fs.existsSync(featuresPath))
39
+ return false;
40
+ try {
41
+ const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
42
+ const orch = data.multi_agent_orchestration ?? {};
43
+ return orch.confer?.enabled === true || orch.confer_on_synthesis === true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
36
49
  function orchestrationConfig() {
37
50
  try {
38
51
  return featuresConfigLoader.loadConfig().multi_agent_orchestration;
@@ -80,12 +93,7 @@ export function buildSynthesisCheckpointPlan(dueReason, projectRoot = process.cw
80
93
  if (!isLeadDevModeActive())
81
94
  return null;
82
95
  const cfg = orchestrationConfig();
83
- const profile = resolveRuntimeSuitProfile(projectRoot);
84
- const autoConsult = profile === 'frontier'
85
- ? cfg.confer?.enabled === true || cfg.confer_on_synthesis === true
86
- : profile === 'strict'
87
- ? true
88
- : cfg.auto_consult_major_work !== false;
96
+ const autoConsult = conferExplicitlyEnabled(projectRoot);
89
97
  const mandatoryConsults = autoConsult ? [...MANDATORY_MAJOR_CONSULTS] : [];
90
98
  if (mandatoryConsults.length === 0)
91
99
  return null;
@@ -118,13 +126,13 @@ export function buildSynthesisCheckpointPlan(dueReason, projectRoot = process.cw
118
126
  fullSuiteGate: false,
119
127
  hint: 'Synthesis checkpoint — consult mandatory agents before resuming gated work',
120
128
  },
129
+ consultCycleId: randomUUID(),
121
130
  };
122
131
  }
123
132
  export function buildLeadDevPlan(description, taskTypes = ['implement'], taskInputs = [], mcpOverallComplexity, projectRoot = process.cwd()) {
124
133
  if (!isLeadDevModeActive())
125
134
  return null;
126
135
  const cfg = orchestrationConfig();
127
- const profile = resolveRuntimeSuitProfile(projectRoot);
128
136
  const threshold = cfg.phased_plan_threshold ?? 25;
129
137
  const score = scoreComplexity(description, { taskTypes });
130
138
  const mcpScore = mcpOverallComplexity !== undefined && Number.isFinite(mcpOverallComplexity)
@@ -132,11 +140,7 @@ export function buildLeadDevPlan(description, taskTypes = ['implement'], taskInp
132
140
  : 0;
133
141
  const complexity = Math.max(score.score, mcpScore);
134
142
  const requiresPhasedPlan = complexity > threshold || taskInputs.length > 1;
135
- const autoConsult = profile === 'frontier'
136
- ? cfg.confer?.enabled === true || cfg.confer_on_synthesis === true
137
- : profile === 'strict' || conferDefaultForProfile(profile)
138
- ? cfg.auto_consult_major_work !== false
139
- : false;
143
+ const autoConsult = conferExplicitlyEnabled(projectRoot);
140
144
  const mandatoryConsults = autoConsult && requiresPhasedPlan ? [...MANDATORY_MAJOR_CONSULTS] : [];
141
145
  const phases = requiresPhasedPlan
142
146
  ? [
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Confer host port. The wearing TUI already spawned the MCP servers.
3
+ * This port writes the ask or returns the host's answer. It does not spawn a server.
4
+ */
5
+ export interface ConferHostSeat {
6
+ todoId: string;
7
+ subagent: string;
8
+ server: string;
9
+ tool: string;
10
+ args: Record<string, unknown>;
11
+ sessionId: string;
12
+ prompt: string;
13
+ dueReason: string | null;
14
+ }
15
+ export type ConferHostCall = {
16
+ status: 'pending';
17
+ askPath: string;
18
+ } | {
19
+ status: 'answered';
20
+ text: string;
21
+ };
22
+ export interface ConferHostPort {
23
+ call(seat: ConferHostSeat, projectRoot: string): Promise<ConferHostCall>;
24
+ }
25
+ export declare function conferAskPath(todoId: string, projectRoot: string): string;
26
+ /** Evidence for the host model. The schema line is not a vote. */
27
+ export declare function buildHostConferEvidence(role: string, evidence: string): string;
28
+ export declare class SessionConferHost implements ConferHostPort {
29
+ call(seat: ConferHostSeat, projectRoot: string): Promise<ConferHostCall>;
30
+ }
31
+ export declare class InProcessConferHost implements ConferHostPort {
32
+ private readonly answers;
33
+ constructor(answers: Record<string, string>);
34
+ call(seat: ConferHostSeat): Promise<ConferHostCall>;
35
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Confer host port. The wearing TUI already spawned the MCP servers.
3
+ * This port writes the ask or returns the host's answer. It does not spawn a server.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ export function conferAskPath(todoId, projectRoot) {
8
+ return path.join(projectRoot, '.xray', 'state', `confer-ask-${todoId}.json`);
9
+ }
10
+ /** Evidence for the host model. The schema line is not a vote. */
11
+ export function buildHostConferEvidence(role, evidence) {
12
+ const body = evidence.trim();
13
+ return [
14
+ `## Confer evidence — ${role}`,
15
+ '',
16
+ body || 'No local evidence. The wearing TUI has the confer ask.',
17
+ '',
18
+ 'The wearing TUI writes the verdict on its own line after this evidence.',
19
+ '',
20
+ 'Verdict: PASS | CONDITIONAL | FAIL',
21
+ '- Top risks (bulleted)',
22
+ '- Hardening note / recommendations',
23
+ ].join('\n');
24
+ }
25
+ export class SessionConferHost {
26
+ async call(seat, projectRoot) {
27
+ const askPath = conferAskPath(seat.todoId, projectRoot);
28
+ fs.mkdirSync(path.dirname(askPath), { recursive: true });
29
+ const payload = {
30
+ todoId: seat.todoId,
31
+ subagent: seat.subagent,
32
+ server: seat.server,
33
+ tool: seat.tool,
34
+ args: seat.args,
35
+ sessionId: seat.sessionId,
36
+ prompt: seat.prompt,
37
+ dueReason: seat.dueReason,
38
+ };
39
+ fs.writeFileSync(askPath, `${JSON.stringify(payload, null, 2)}\n`);
40
+ return { status: 'pending', askPath };
41
+ }
42
+ }
43
+ export class InProcessConferHost {
44
+ answers;
45
+ constructor(answers) {
46
+ this.answers = answers;
47
+ }
48
+ async call(seat) {
49
+ const text = this.answers[seat.todoId] ?? this.answers[seat.subagent];
50
+ if (text === undefined) {
51
+ return { status: 'pending', askPath: '' };
52
+ }
53
+ return { status: 'answered', text };
54
+ }
55
+ }
@@ -3,6 +3,9 @@
3
3
  * Triggered automatically at synthesis checkpoint; records consult receipts + plan todos.
4
4
  */
5
5
  import { type SynthesisConsultReceipt } from './synthesis-consult-receipt.js';
6
+ import { InProcessConferHost, SessionConferHost, type ConferHostPort, type ConferHostSeat } from './confer-host.js';
7
+ export { InProcessConferHost, SessionConferHost };
8
+ export type { ConferHostPort, ConferHostSeat };
6
9
  export declare const CONFER_AGENTS: readonly ["researcher", "architect-tools", "code-review"];
7
10
  export interface ConferConfig {
8
11
  enabled: boolean;
@@ -27,12 +30,16 @@ export interface ConferAgentResult {
27
30
  error?: string;
28
31
  }
29
32
  export interface ConferQuorumResult {
30
- status: 'completed' | 'partial' | 'skipped' | 'failed';
33
+ status: 'completed' | 'partial' | 'skipped' | 'failed' | 'pending';
31
34
  agents: ConferAgentResult[];
32
35
  message: string;
33
36
  }
34
37
  export declare function conferCheckpointPath(projectRoot?: string): string;
35
38
  export declare function defaultConferConfig(): ConferConfig;
39
+ /** Fixture PASS is a test harness. Live orchestrate-task and XRAY_CONFER_FIXTURE cannot reach it. */
40
+ export declare const CONFER_FIXTURE_UNREACHABLE = "Confer fixture is unreachable outside tests";
41
+ export declare function conferFixtureAllowed(): boolean;
42
+ /** A suit profile does not enable Confer. Missing confer key stays off, including strict. */
36
43
  export declare function loadConferConfig(projectRoot?: string): ConferConfig;
37
44
  export declare function isConferEnabled(projectRoot?: string): boolean;
38
45
  export declare function loadConferCheckpoint(projectRoot?: string): ConferCheckpointState | null;
@@ -46,12 +53,21 @@ export declare function conferAgentMcpTarget(subagent: string): {
46
53
  tool: string;
47
54
  args: (prompt: string, projectRoot: string) => Record<string, unknown>;
48
55
  };
49
- export declare function invokeConferAgent(subagent: string, prompt: string, projectRoot?: string): Promise<string>;
56
+ export declare function invokeConferAgent(subagent: string, prompt: string, projectRoot?: string, host?: ConferHostPort, sessionId?: string, todoId?: string, dueReason?: string | null): Promise<string>;
50
57
  export declare function applyConferConsultResult(todoId: string, subagent: string, sessionId: string, outputText: string, projectRoot?: string): ConferAgentResult;
51
- export declare function writeFixtureConferReceipt(todoId: string, subagent: string, sessionId: string, projectRoot?: string): ConferAgentResult;
58
+ export declare function buildFixtureConferOutput(subagent: string, verdict?: SynthesisConsultReceipt['verdict'], noLlm?: boolean): string;
59
+ export declare function writeFixtureConferReceipt(todoId: string, subagent: string, sessionId: string, projectRoot?: string, verdict?: SynthesisConsultReceipt['verdict'], noLlm?: boolean): ConferAgentResult;
52
60
  export declare function runConferQuorum(projectRoot: string | undefined, sessionId: string, options?: {
53
61
  collocatedText?: string;
54
62
  dueReason?: string | null;
55
63
  fixture?: boolean;
64
+ /** Used only when fixture is true. Defaults to PASS. FAIL and UNREVIEWED do not complete the todo. */
65
+ fixtureVerdict?: SynthesisConsultReceipt['verdict'];
66
+ /** Fixture text is the real no-model localConferVote (UNREVIEWED), not a hardcoded PASS. */
67
+ fixtureNoLlm?: boolean;
68
+ /** Used only when fixture is true. Replaces the generated fixture text for every consult. */
69
+ fixtureOutput?: string;
70
+ /** Live path. Default writes an ask for the wearing TUI and does not spawn a server. */
71
+ host?: ConferHostPort;
56
72
  }): Promise<ConferQuorumResult>;
57
73
  export declare function formatConferQuorumReport(result: ConferQuorumResult): string;
@@ -6,45 +6,41 @@ import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { MANDATORY_MAJOR_CONSULTS } from './autonomy-kernel.js';
8
8
  import { areSynthesisConsultTodosComplete, getNextRequiredTodo, getSynthesisConsultTodos, isSynthesisRealignmentPlan, loadPersistedLeadDevPlan, updatePlanTodoStatus, } from './lead-dev-plan-persistence.js';
9
- import { buildReceiptFromConsultOutput, tryRecordSynthesisConsultReceipt, parseConsultVerdictFromText, } from './synthesis-consult-receipt.js';
9
+ import { buildReceiptFromConsultOutput, consultVerdictBlocksCompletion, loadSynthesisConsultReceipt, tryRecordSynthesisConsultReceipt, parseConsultVerdictFromText, } from './synthesis-consult-receipt.js';
10
+ import { formatGovernanceVoteText, localConferVote } from '../governance/local-confer.js';
10
11
  import { isSynthesisCheckpointDue } from './synthesis.js';
11
- import { conferDefaultForProfile, resolveRuntimeSuitProfile } from './suit-temperament.js';
12
+ import { InProcessConferHost, SessionConferHost, } from './confer-host.js';
13
+ export { InProcessConferHost, SessionConferHost };
12
14
  export const CONFER_AGENTS = [...MANDATORY_MAJOR_CONSULTS];
13
15
  const STATE_VERSION = 1;
14
16
  export function conferCheckpointPath(projectRoot = process.cwd()) {
15
17
  return path.join(projectRoot, '.xray', 'state', 'confer-checkpoint.json');
16
18
  }
17
19
  export function defaultConferConfig() {
18
- return { enabled: true, on_synthesis: true };
20
+ return { enabled: false, on_synthesis: false };
19
21
  }
22
+ /** Fixture PASS is a test harness. Live orchestrate-task and XRAY_CONFER_FIXTURE cannot reach it. */
23
+ export const CONFER_FIXTURE_UNREACHABLE = 'Confer fixture is unreachable outside tests';
24
+ export function conferFixtureAllowed() {
25
+ return process.env.NODE_ENV === 'test' || Boolean(process.env.VITEST);
26
+ }
27
+ /** A suit profile does not enable Confer. Missing confer key stays off, including strict. */
20
28
  export function loadConferConfig(projectRoot = process.cwd()) {
21
29
  const featuresPath = path.join(projectRoot, '.xray', 'features.json');
22
- const profile = resolveRuntimeSuitProfile(projectRoot);
23
- if (profile === 'strict') {
24
- return { enabled: true, on_synthesis: true };
25
- }
26
- if (!fs.existsSync(featuresPath)) {
27
- return conferDefaultForProfile(profile)
28
- ? defaultConferConfig()
29
- : { enabled: false, on_synthesis: false };
30
- }
30
+ if (!fs.existsSync(featuresPath))
31
+ return defaultConferConfig();
31
32
  try {
32
33
  const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
33
34
  const orch = data.multi_agent_orchestration ?? {};
34
35
  const raw = orch.confer ?? {};
35
- if (profile === 'frontier') {
36
- const optedIn = raw.enabled === true || orch.confer_on_synthesis === true;
37
- return { enabled: optedIn, on_synthesis: optedIn };
38
- }
36
+ const optedIn = raw.enabled === true || orch.confer_on_synthesis === true;
39
37
  return {
40
- enabled: raw.enabled !== false && orch.confer_on_synthesis !== false,
41
- on_synthesis: raw.on_synthesis !== false && orch.confer_on_synthesis !== false,
38
+ enabled: optedIn,
39
+ on_synthesis: optedIn && raw.on_synthesis !== false,
42
40
  };
43
41
  }
44
42
  catch {
45
- return conferDefaultForProfile(profile)
46
- ? defaultConferConfig()
47
- : { enabled: false, on_synthesis: false };
43
+ return defaultConferConfig();
48
44
  }
49
45
  }
50
46
  export function isConferEnabled(projectRoot = process.cwd()) {
@@ -116,7 +112,7 @@ export function conferAgentMcpTarget(subagent) {
116
112
  switch (subagent) {
117
113
  case 'architect-tools':
118
114
  return {
119
- server: 'architect-tools',
115
+ server: 'xray-architect-tools',
120
116
  tool: 'architecture-assessment',
121
117
  args: (prompt, projectRoot) => ({
122
118
  projectRoot,
@@ -127,7 +123,7 @@ export function conferAgentMcpTarget(subagent) {
127
123
  };
128
124
  case 'researcher':
129
125
  return {
130
- server: 'researcher',
126
+ server: 'xray-researcher',
131
127
  tool: 'analyze_proposal',
132
128
  args: (prompt) => ({
133
129
  proposalTitle: 'Synthesis confer — researcher',
@@ -139,7 +135,7 @@ export function conferAgentMcpTarget(subagent) {
139
135
  case 'code-review':
140
136
  default:
141
137
  return {
142
- server: 'code-review',
138
+ server: 'xray-code-review',
143
139
  tool: 'analyze_proposal',
144
140
  args: (prompt) => ({
145
141
  proposalTitle: 'Synthesis confer — code-review',
@@ -150,38 +146,37 @@ export function conferAgentMcpTarget(subagent) {
150
146
  };
151
147
  }
152
148
  }
153
- function extractMcpText(result) {
154
- const content = result?.content;
155
- if (Array.isArray(content)) {
156
- return content.map((c) => c.text ?? '').join('\n');
157
- }
158
- if (typeof result === 'string')
159
- return result;
160
- try {
161
- return JSON.stringify(result);
162
- }
163
- catch {
164
- return String(result);
165
- }
166
- }
167
- export async function invokeConferAgent(subagent, prompt, projectRoot = process.cwd()) {
168
- const { mcpClientManager } = await import('../mcps/mcp-client.js');
149
+ function conferHostSeat(subagent, prompt, projectRoot, sessionId, todoId, dueReason) {
169
150
  const target = conferAgentMcpTarget(subagent);
170
- const args = target.args(prompt, projectRoot);
171
- const result = await mcpClientManager.callServerTool(target.server, target.tool, args);
172
- const text = extractMcpText(result);
151
+ return {
152
+ todoId,
153
+ subagent,
154
+ server: target.server,
155
+ tool: target.tool,
156
+ args: target.args(prompt, projectRoot),
157
+ sessionId,
158
+ prompt,
159
+ dueReason,
160
+ };
161
+ }
162
+ export async function invokeConferAgent(subagent, prompt, projectRoot = process.cwd(), host = new SessionConferHost(), sessionId = '', todoId = subagent, dueReason = null) {
163
+ const call = await host.call(conferHostSeat(subagent, prompt, projectRoot, sessionId, todoId, dueReason), projectRoot);
164
+ if (call.status === 'pending') {
165
+ throw new Error(`Confer ask pending for ${subagent} at ${call.askPath}`);
166
+ }
167
+ const text = call.text;
173
168
  if (!text.trim()) {
174
169
  throw new Error(`Empty confer response from ${subagent}`);
175
170
  }
176
171
  if (!parseConsultVerdictFromText(text)) {
177
- throw new Error(`Confer response from ${subagent} missing parseable verdict (expected Verdict: PASS|CONDITIONAL|FAIL or DECISION: approve|reject|abstain)`);
172
+ throw new Error(`Confer response from ${subagent} missing parseable verdict (expected an explicit Verdict: PASS|CONDITIONAL|FAIL|UNREVIEWED line, or DECISION: approve|reject|abstain)`);
178
173
  }
179
174
  return text;
180
175
  }
181
176
  export function applyConferConsultResult(todoId, subagent, sessionId, outputText, projectRoot = process.cwd()) {
182
177
  const receipt = tryRecordSynthesisConsultReceipt(todoId, subagent, sessionId, outputText, projectRoot);
183
178
  let todoCompleted = false;
184
- if (receipt && receipt.verdict !== 'FAIL') {
179
+ if (receipt && !consultVerdictBlocksCompletion(receipt.verdict)) {
185
180
  todoCompleted = updatePlanTodoStatus(todoId, 'completed', projectRoot);
186
181
  }
187
182
  return {
@@ -193,11 +188,24 @@ export function applyConferConsultResult(todoId, subagent, sessionId, outputText
193
188
  verdict: receipt?.verdict ?? buildReceiptFromConsultOutput(todoId, subagent, sessionId, outputText)?.verdict ?? null,
194
189
  };
195
190
  }
196
- export function writeFixtureConferReceipt(todoId, subagent, sessionId, projectRoot = process.cwd()) {
197
- const output = `Verdict: PASS\nTop risks: none\nHardening: confer fixture quorum for ${subagent}`;
198
- return applyConferConsultResult(todoId, subagent, sessionId, output, projectRoot);
191
+ export function buildFixtureConferOutput(subagent, verdict = 'PASS', noLlm = false) {
192
+ if (noLlm) {
193
+ const role = subagent === 'code-review' ? 'code-review' : 'researcher';
194
+ return formatGovernanceVoteText(localConferVote({ role, llmConfigured: false }));
195
+ }
196
+ return `Verdict: ${verdict}\nTop risks: none\nHardening: confer fixture quorum for ${subagent}`;
197
+ }
198
+ export function writeFixtureConferReceipt(todoId, subagent, sessionId, projectRoot = process.cwd(), verdict = 'PASS', noLlm = false) {
199
+ return applyConferConsultResult(todoId, subagent, sessionId, buildFixtureConferOutput(subagent, verdict, noLlm), projectRoot);
199
200
  }
200
201
  export async function runConferQuorum(projectRoot = process.cwd(), sessionId, options = {}) {
202
+ if (options.fixture && !conferFixtureAllowed()) {
203
+ return {
204
+ status: 'failed',
205
+ agents: [],
206
+ message: CONFER_FIXTURE_UNREACHABLE,
207
+ };
208
+ }
201
209
  const cfg = loadConferConfig(projectRoot);
202
210
  if (!cfg.enabled || !cfg.on_synthesis) {
203
211
  return {
@@ -214,7 +222,7 @@ export async function runConferQuorum(projectRoot = process.cwd(), sessionId, op
214
222
  message: 'No synthesis realignment plan — confer not applicable',
215
223
  };
216
224
  }
217
- if (areSynthesisConsultTodosComplete(plan)) {
225
+ if (areSynthesisConsultTodosComplete(plan) && consultReceiptsAreRealApproves(plan, sessionId, projectRoot)) {
218
226
  return {
219
227
  status: 'completed',
220
228
  agents: [],
@@ -228,17 +236,36 @@ export async function runConferQuorum(projectRoot = process.cwd(), sessionId, op
228
236
  '# Synthesis confer\n\nReview checkpoint context and realign plan.';
229
237
  const agents = [];
230
238
  for (const todo of getSynthesisConsultTodos(plan)) {
231
- if (todo.status === 'completed')
239
+ if (todo.status === 'completed' && consultReceiptIsRealPass(todo.id, sessionId, plan, projectRoot)) {
232
240
  continue;
241
+ }
242
+ if (todo.status === 'completed') {
243
+ updatePlanTodoStatus(todo.id, 'pending', projectRoot);
244
+ }
233
245
  try {
234
246
  let agentResult;
235
247
  if (options.fixture) {
236
- agentResult = writeFixtureConferReceipt(todo.id, todo.subagent, sessionId, projectRoot);
248
+ agentResult =
249
+ options.fixtureOutput !== undefined
250
+ ? applyConferConsultResult(todo.id, todo.subagent, sessionId, options.fixtureOutput, projectRoot)
251
+ : writeFixtureConferReceipt(todo.id, todo.subagent, sessionId, projectRoot, options.fixtureVerdict ?? 'PASS', options.fixtureNoLlm === true);
237
252
  }
238
253
  else {
239
254
  const prompt = buildConferPrompt(todo.subagent, todo.task, collocated, options.dueReason ?? null);
240
- const outputText = await invokeConferAgent(todo.subagent, prompt, projectRoot);
241
- agentResult = applyConferConsultResult(todo.id, todo.subagent, sessionId, outputText, projectRoot);
255
+ const host = options.host ?? new SessionConferHost();
256
+ const call = await host.call(conferHostSeat(todo.subagent, prompt, projectRoot, sessionId, todo.id, options.dueReason ?? null), projectRoot);
257
+ if (call.status === 'pending') {
258
+ agents.push({
259
+ todoId: todo.id,
260
+ subagent: todo.subagent,
261
+ outputText: '',
262
+ receiptRecorded: false,
263
+ todoCompleted: false,
264
+ verdict: null,
265
+ });
266
+ continue;
267
+ }
268
+ agentResult = applyConferConsultResult(todo.id, todo.subagent, sessionId, call.text, projectRoot);
242
269
  }
243
270
  agents.push(agentResult);
244
271
  if (agentResult.receiptRecorded && agentResult.todoCompleted) {
@@ -247,7 +274,9 @@ export async function runConferQuorum(projectRoot = process.cwd(), sessionId, op
247
274
  }
248
275
  else {
249
276
  state.status = 'failed';
250
- state.lastError = `Receipt or todo completion failed for ${todo.id}`;
277
+ state.lastError = consultVerdictBlocksCompletion(agentResult.verdict)
278
+ ? `${agentResult.verdict} blocks ${todo.id}`
279
+ : `Receipt or todo completion failed for ${todo.id}`;
251
280
  saveConferCheckpoint(state, projectRoot);
252
281
  return {
253
282
  status: 'partial',
@@ -273,10 +302,21 @@ export async function runConferQuorum(projectRoot = process.cwd(), sessionId, op
273
302
  return { status: 'failed', agents, message };
274
303
  }
275
304
  }
305
+ if (agents.some((agent) => agent.verdict === null && !agent.receiptRecorded && !agent.error)) {
306
+ state.status = 'pending';
307
+ delete state.lastError;
308
+ saveConferCheckpoint(state, projectRoot);
309
+ return {
310
+ status: 'pending',
311
+ agents,
312
+ message: 'Confer asks written. The wearing TUI calls its MCP servers and records each verdict.',
313
+ };
314
+ }
276
315
  const refreshed = loadPersistedLeadDevPlan(projectRoot);
277
316
  const done = refreshed &&
278
317
  isSynthesisRealignmentPlan(refreshed) &&
279
- areSynthesisConsultTodosComplete(refreshed);
318
+ areSynthesisConsultTodosComplete(refreshed) &&
319
+ consultReceiptsAreRealApproves(refreshed, sessionId, projectRoot);
280
320
  state.status = done ? 'completed' : 'failed';
281
321
  if (!done)
282
322
  state.lastError = 'Consult todos remain after confer loop';
@@ -289,6 +329,22 @@ export async function runConferQuorum(projectRoot = process.cwd(), sessionId, op
289
329
  : (state.lastError ?? 'Confer incomplete'),
290
330
  };
291
331
  }
332
+ function consultReceiptIsRealPass(todoId, sessionId, plan, projectRoot) {
333
+ const receipt = loadSynthesisConsultReceipt(todoId, projectRoot);
334
+ if (receipt?.verdict !== 'PASS')
335
+ return false;
336
+ if (sessionId && receipt.sessionId !== sessionId)
337
+ return false;
338
+ if (plan.consultCycleId && receipt.cycleId !== plan.consultCycleId)
339
+ return false;
340
+ return true;
341
+ }
342
+ function consultReceiptsAreRealApproves(plan, sessionId, projectRoot) {
343
+ const todos = getSynthesisConsultTodos(plan);
344
+ if (todos.length === 0)
345
+ return false;
346
+ return todos.every((todo) => consultReceiptIsRealPass(todo.id, sessionId, plan, projectRoot));
347
+ }
292
348
  const CONFER_AGENT_EMOJI = {
293
349
  researcher: '🔍',
294
350
  'architect-tools': '🏗️',
@@ -7,7 +7,7 @@ import * as path from 'path';
7
7
  import { SYNTHESIS_REALIGNMENT_PHASE_ID } from './autonomy-kernel.js';
8
8
  import { DEFAULT_PENDING_TTL_MS } from './pending-delegations.js';
9
9
  import { completeSynthesisCheckpoint, getSynthesisCheckpointSessionId, isSynthesisCheckpointDue, recordExecutionSlice, } from './synthesis.js';
10
- import { hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, } from './synthesis-consult-receipt.js';
10
+ import { consultVerdictBlocksCompletion, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, } from './synthesis-consult-receipt.js';
11
11
  import { resolveSpawnPlan } from './spawn-plan-resolution.js';
12
12
  import { isAsideActiveForSession, isUserAsideTodoId, isUserAsidesEnabled, parseAsideIdFromTodoId, findAsideContainingTodo, updateUserAsideTodoStatus, } from './user-aside.js';
13
13
  export const DEFAULT_PLAN_STALE_MS = 8 * 60 * 60 * 1000;
@@ -259,11 +259,13 @@ export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd()
259
259
  receiptExpected.sessionId = plan.sessionId;
260
260
  if (targetTodo?.subagent)
261
261
  receiptExpected.subagent = targetTodo.subagent;
262
+ if (plan.consultCycleId)
263
+ receiptExpected.cycleId = plan.consultCycleId;
262
264
  if (!hasValidSynthesisConsultReceipt(todoId, projectRoot, receiptExpected)) {
263
265
  return false;
264
266
  }
265
267
  const receipt = loadSynthesisConsultReceipt(todoId, projectRoot);
266
- if (receipt?.verdict === 'FAIL') {
268
+ if (consultVerdictBlocksCompletion(receipt?.verdict)) {
267
269
  return false;
268
270
  }
269
271
  }