0xray 4.0.41 → 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 (115) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +12 -0
  3. package/.xray/features.json +1 -1
  4. package/.xray/features.schema.json +1 -1
  5. package/AGENTS-consumer.md +2 -0
  6. package/AGENTS.md +2 -0
  7. package/README.md +52 -12
  8. package/SKILLS.md +2 -0
  9. package/dist/AGENTS.md +2 -0
  10. package/dist/CHANGELOG.md +13 -0
  11. package/dist/README.md +52 -12
  12. package/dist/SKILLS.md +2 -0
  13. package/dist/cli/commands/openclaw-install.js +1 -1
  14. package/dist/cli/index.js +12 -0
  15. package/dist/enforcement/index.d.ts +4 -0
  16. package/dist/enforcement/index.js +4 -0
  17. package/dist/enforcement/validators/code-quality-validators.js +6 -4
  18. package/dist/governance/llm-governance-provider.d.ts +7 -1
  19. package/dist/governance/llm-governance-provider.js +72 -14
  20. package/dist/governance/local-confer.d.ts +4 -1
  21. package/dist/governance/local-confer.js +22 -1
  22. package/dist/integrations/grok/grok-cli.js +10 -37
  23. package/dist/integrations/grok/hooks/grok-hook-utils.js +8 -1
  24. package/dist/integrations/hooks/confer-hook-runtime.mjs +23 -1
  25. package/dist/integrations/hooks/goggles-planes.json +4 -1
  26. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  27. package/dist/mcps/architect-tools.server.d.ts +6 -0
  28. package/dist/mcps/architect-tools.server.js +19 -2
  29. package/dist/mcps/config/server-config-registry.js +8 -6
  30. package/dist/mcps/governance.server.d.ts +1 -0
  31. package/dist/mcps/governance.server.js +46 -0
  32. package/dist/mcps/knowledge-skills/code-review.server.js +7 -0
  33. package/dist/mcps/knowledge-skills/security-audit.server.js +5 -3
  34. package/dist/mcps/mcp-client.js +1 -1
  35. package/dist/mcps/orchestrator/handlers/complexity-handler.js +2 -2
  36. package/dist/mcps/researcher.server.js +12 -3
  37. package/dist/nucleus/autonomy-kernel.d.ts +2 -0
  38. package/dist/nucleus/autonomy-kernel.js +17 -13
  39. package/dist/nucleus/confer-host.d.ts +35 -0
  40. package/dist/nucleus/confer-host.js +55 -0
  41. package/dist/nucleus/confer.d.ts +19 -3
  42. package/dist/nucleus/confer.js +111 -55
  43. package/dist/nucleus/lead-dev-plan-persistence.js +4 -2
  44. package/dist/nucleus/suit-temperament.d.ts +1 -1
  45. package/dist/nucleus/suit-temperament.js +2 -2
  46. package/dist/nucleus/synthesis-consult-receipt.d.ts +7 -1
  47. package/dist/nucleus/synthesis-consult-receipt.js +86 -35
  48. package/dist/reporting/framework-reporting-system.js +2 -5
  49. package/dist/reporting/log-parser.d.ts +2 -0
  50. package/dist/reporting/log-parser.js +7 -6
  51. package/dist/skills/orchestrator/SKILL.md +12 -0
  52. package/docs-site/docs/plates/activity-log.md +1 -1
  53. package/docs-site/docs/plates/boot.md +1 -1
  54. package/docs-site/docs/plates/centrifuge.md +1 -1
  55. package/docs-site/docs/plates/cursor-compact.md +1 -1
  56. package/docs-site/docs/plates/glossary.md +1 -1
  57. package/docs-site/docs/plates/goggles.md +1 -1
  58. package/docs-site/docs/plates/governance.md +1 -1
  59. package/docs-site/docs/plates/grok-compact.md +1 -1
  60. package/docs-site/docs/plates/grokbot.md +1 -1
  61. package/docs-site/docs/plates/host-pack.md +1 -1
  62. package/docs-site/docs/plates/house.md +1 -1
  63. package/docs-site/docs/plates/index.md +1 -1
  64. package/docs-site/docs/plates/inference-files.md +1 -1
  65. package/docs-site/docs/plates/kept-line.md +1 -1
  66. package/docs-site/docs/plates/kits.md +1 -1
  67. package/docs-site/docs/plates/lens-gate.md +1 -1
  68. package/docs-site/docs/plates/lens-page.md +1 -1
  69. package/docs-site/docs/plates/live-mesh.md +1 -1
  70. package/docs-site/docs/plates/memory-recall.md +1 -1
  71. package/docs-site/docs/plates/notes-page.md +1 -1
  72. package/docs-site/docs/plates/orchestration.md +1 -1
  73. package/docs-site/docs/plates/package-face.md +1 -1
  74. package/docs-site/docs/plates/payload-heat.md +1 -1
  75. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  76. package/docs-site/docs/plates/pre-tool.md +1 -1
  77. package/docs-site/docs/plates/processor.md +1 -1
  78. package/docs-site/docs/plates/record-map.md +1 -1
  79. package/docs-site/docs/plates/reflection-page.md +1 -1
  80. package/docs-site/docs/plates/reporting.md +1 -1
  81. package/docs-site/docs/plates/review.md +1 -1
  82. package/docs-site/docs/plates/routing.md +1 -1
  83. package/docs-site/docs/plates/session-capture.md +1 -1
  84. package/docs-site/docs/plates/site-manual.md +1 -1
  85. package/docs-site/docs/plates/stamp-plate.md +1 -1
  86. package/docs-site/docs/plates/station-card.md +1 -1
  87. package/docs-site/docs/plates/station-heat.md +1 -1
  88. package/docs-site/docs/plates/suit-organs.md +1 -1
  89. package/docs-site/docs/plates/suit-settings.md +1 -1
  90. package/docs-site/docs/plates/suit-wear.md +1 -1
  91. package/docs-site/docs/plates/suit.md +1 -1
  92. package/docs-site/docs/plates/trail-state.md +1 -1
  93. package/docs-site/docs/plates/work-fresh.md +1 -1
  94. package/docs-site/docs/plates/write-home.md +1 -1
  95. package/llms.txt +2 -0
  96. package/package.json +2 -2
  97. package/scripts/mjs/verify-confer-core.mjs +153 -29
  98. package/scripts/mjs/verify-user-aside-core.mjs +3 -4
  99. package/scripts/node/bridge-mcp-wiring.cjs +236 -5
  100. package/scripts/node/confer-switch.cjs +66 -0
  101. package/scripts/node/install-bridges.cjs +16 -41
  102. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  103. package/src/mcps/architect-tools.server.ts +26 -2
  104. package/src/mcps/config/__tests__/server-config-registry.test.ts +15 -6
  105. package/src/mcps/config/server-config-registry.ts +9 -6
  106. package/src/mcps/governance.server.ts +49 -0
  107. package/src/mcps/knowledge-skills/code-review.server.ts +8 -0
  108. package/src/mcps/knowledge-skills/security-audit.server.ts +11 -12
  109. package/src/mcps/mcp-client.ts +1 -1
  110. package/src/mcps/orchestrator/handlers/complexity-handler.ts +2 -1
  111. package/src/mcps/researcher.server.ts +19 -10
  112. package/src/skills/orchestrator/SKILL.md +12 -0
  113. package/vendor/@0xray/repertoire/scripts/run-live-synthesis-checkpoint.mjs +5 -51
  114. package/xray/features.json +1 -1
  115. package/xray/features.schema.json +1 -1
@@ -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
  }
@@ -32,4 +32,4 @@ export declare function maybeHeatHostStation(projectRoot: string, host: SuitHost
32
32
  export declare function writeSuitSessionBoot(projectRoot: string, host: SuitHost, extra?: Record<string, unknown>): string;
33
33
  export declare function ceremonyForProfile(profile: SuitProfile): CeremonyLevel;
34
34
  export declare function spawnPlanModeForProfile(profile: SuitProfile): SpawnPlanMode;
35
- export declare function conferDefaultForProfile(profile: SuitProfile): boolean;
35
+ export declare function conferDefaultForProfile(_profile: SuitProfile): boolean;
@@ -170,6 +170,6 @@ export function spawnPlanModeForProfile(profile) {
170
170
  return 'warn';
171
171
  return 'deny';
172
172
  }
173
- export function conferDefaultForProfile(profile) {
174
- return profile !== 'frontier';
173
+ export function conferDefaultForProfile(_profile) {
174
+ return false;
175
175
  }
@@ -2,7 +2,7 @@
2
2
  * Synthesis consult receipt SSOT — outcome-based completion for s.N todos.
3
3
  * Receipts live at `.xray/state/synthesis-consult-{todoId}.json`.
4
4
  */
5
- export type SynthesisConsultVerdict = 'PASS' | 'CONDITIONAL' | 'FAIL';
5
+ export type SynthesisConsultVerdict = 'PASS' | 'CONDITIONAL' | 'FAIL' | 'UNREVIEWED';
6
6
  export interface SynthesisConsultReceipt {
7
7
  sessionId: string;
8
8
  subagent: string;
@@ -11,18 +11,24 @@ export interface SynthesisConsultReceipt {
11
11
  hardeningNote: string;
12
12
  todoId?: string;
13
13
  recordedAt?: string;
14
+ /** Synthesis plan generation this receipt belongs to. A previous cycle's PASS cannot clear the next s.N. */
15
+ cycleId?: string;
14
16
  }
15
17
  export declare function isSynthesisConsultTodoId(todoId: string): boolean;
16
18
  export declare function synthesisConsultReceiptPath(todoId: string, projectRoot?: string): string;
17
19
  export declare function loadSynthesisConsultReceipt(todoId: string, projectRoot?: string): SynthesisConsultReceipt | null;
18
20
  export declare function writeSynthesisConsultReceipt(todoId: string, receipt: SynthesisConsultReceipt, projectRoot?: string): string;
21
+ /** Only a real approve (PASS) completes a consult todo. */
22
+ export declare function consultVerdictBlocksCompletion(verdict: SynthesisConsultVerdict | null | undefined): boolean;
19
23
  export declare function validateSynthesisConsultReceipt(receipt: SynthesisConsultReceipt, todoId: string, expected?: {
20
24
  sessionId?: string | null;
21
25
  subagent?: string;
26
+ cycleId?: string;
22
27
  }): boolean;
23
28
  export declare function hasValidSynthesisConsultReceipt(todoId: string, projectRoot?: string, expected?: {
24
29
  sessionId?: string | null;
25
30
  subagent?: string;
31
+ cycleId?: string;
26
32
  }): boolean;
27
33
  export declare function parseConsultVerdictFromText(text: string): SynthesisConsultVerdict | null;
28
34
  export declare function extractTopRisksFromText(text: string, max?: number): string[];
@@ -25,14 +25,30 @@ export function loadSynthesisConsultReceipt(todoId, projectRoot = process.cwd())
25
25
  export function writeSynthesisConsultReceipt(todoId, receipt, projectRoot = process.cwd()) {
26
26
  const receiptPath = synthesisConsultReceiptPath(todoId, projectRoot);
27
27
  fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
28
+ const cycleId = receipt.cycleId ?? readPlanConsultCycleId(projectRoot);
28
29
  const payload = {
29
30
  ...receipt,
30
31
  todoId,
31
32
  recordedAt: receipt.recordedAt ?? new Date().toISOString(),
33
+ ...(cycleId ? { cycleId } : {}),
32
34
  };
33
35
  fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2));
34
36
  return receiptPath;
35
37
  }
38
+ function readPlanConsultCycleId(projectRoot) {
39
+ const planPath = path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
40
+ if (!fs.existsSync(planPath))
41
+ return undefined;
42
+ try {
43
+ const data = JSON.parse(fs.readFileSync(planPath, 'utf8'));
44
+ return typeof data.consultCycleId === 'string' && data.consultCycleId.trim()
45
+ ? data.consultCycleId
46
+ : undefined;
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
36
52
  function normalizeSubagent(agent) {
37
53
  const key = agent.toLowerCase().trim();
38
54
  const aliases = {
@@ -47,7 +63,11 @@ function subagentsAlign(expected, actual) {
47
63
  return e === a || e.includes(a) || a.includes(e);
48
64
  }
49
65
  function isValidVerdict(value) {
50
- return value === 'PASS' || value === 'CONDITIONAL' || value === 'FAIL';
66
+ return value === 'PASS' || value === 'CONDITIONAL' || value === 'FAIL' || value === 'UNREVIEWED';
67
+ }
68
+ /** Only a real approve (PASS) completes a consult todo. */
69
+ export function consultVerdictBlocksCompletion(verdict) {
70
+ return verdict !== 'PASS';
51
71
  }
52
72
  export function validateSynthesisConsultReceipt(receipt, todoId, expected) {
53
73
  if (!receipt.sessionId || !receipt.subagent || !isValidVerdict(receipt.verdict)) {
@@ -62,6 +82,8 @@ export function validateSynthesisConsultReceipt(receipt, todoId, expected) {
62
82
  if (expected?.subagent && !subagentsAlign(expected.subagent, receipt.subagent)) {
63
83
  return false;
64
84
  }
85
+ if (expected?.cycleId && receipt.cycleId !== expected.cycleId)
86
+ return false;
65
87
  if (receipt.todoId && receipt.todoId !== todoId)
66
88
  return false;
67
89
  return true;
@@ -72,52 +94,81 @@ export function hasValidSynthesisConsultReceipt(todoId, projectRoot = process.cw
72
94
  return false;
73
95
  return validateSynthesisConsultReceipt(receipt, todoId, expected);
74
96
  }
75
- function verdictFromToken(token) {
76
- const value = token.toLowerCase();
77
- if (value === 'approve' || value === 'approved' || value === 'pass')
78
- return 'PASS';
79
- if (value === 'reject' || value === 'rejected' || value === 'fail')
80
- return 'FAIL';
81
- if (value === 'abstain' ||
82
- value === 'needs_revision' ||
83
- value === 'conditional' ||
84
- value === 'revise') {
97
+ const EXPLICIT_VERDICT_LINE = /^Verdict:\s*(PASS|CONDITIONAL|FAIL|UNREVIEWED)\s*$/i;
98
+ const VERDICT_OPTION_ECHO = /\bPASS\s*\|\s*CONDITIONAL\s*\|\s*FAIL\b/i;
99
+ const DECISION_LINE = /^DECISION:\s*(approve|reject|abstain|approved|rejected|pass|fail|needs_revision|conditional|revise)\s*$/i;
100
+ function lineWithoutBullet(rawLine) {
101
+ return rawLine.trim().replace(/^[-*•]\s*/, '');
102
+ }
103
+ function splitAtReasoning(text) {
104
+ const lines = text.split('\n');
105
+ const idx = lines.findIndex((rawLine) => /^REASONING:/i.test(lineWithoutBullet(rawLine)));
106
+ if (idx < 0)
107
+ return { header: text, reasoning: null };
108
+ return {
109
+ header: lines.slice(0, idx).join('\n'),
110
+ reasoning: lines.slice(idx).join('\n'),
111
+ };
112
+ }
113
+ /** Abstain because no governance model ran. A bare DECISION: abstain stays CONDITIONAL. */
114
+ function isAbstainWithoutModel(text) {
115
+ return /nested LLM not configured|did not produce a vote|returned no vote/i.test(text);
116
+ }
117
+ /** First exact DECISION line before REASONING. A menu or a line inside the reasoning body is not a vote. */
118
+ function firstHeaderDecision(text) {
119
+ const { header } = splitAtReasoning(text);
120
+ for (const rawLine of header.split('\n')) {
121
+ const match = lineWithoutBullet(rawLine).match(DECISION_LINE);
122
+ const decision = match?.[1]?.toLowerCase();
123
+ if (!decision)
124
+ continue;
125
+ if (decision === 'approve' || decision === 'approved' || decision === 'pass')
126
+ return 'PASS';
127
+ if (decision === 'reject' || decision === 'rejected' || decision === 'fail')
128
+ return 'FAIL';
129
+ if (decision === 'abstain' && isAbstainWithoutModel(text))
130
+ return 'UNREVIEWED';
85
131
  return 'CONDITIONAL';
86
132
  }
87
133
  return null;
88
134
  }
89
- /** The last single Verdict or DECISION line wins. A menu line that lists every word does not. */
90
- function lastExplicitVerdict(text) {
91
- let found = null;
92
- for (const line of text.split(/\r?\n/)) {
93
- const trimmed = line.trim().replace(/^[-*•]\s*/, '');
94
- const match = trimmed.match(/^(?:Verdict|DECISION):\s*(\w+)/i);
95
- const token = match?.[1];
96
- if (!token || trimmed.includes('|'))
135
+ function hasAmbiguousVerdictLine(text) {
136
+ for (const rawLine of text.split('\n')) {
137
+ const line = lineWithoutBullet(rawLine);
138
+ if (!/^Verdict:/i.test(line))
139
+ continue;
140
+ if (EXPLICIT_VERDICT_LINE.test(line))
97
141
  continue;
98
- const verdict = verdictFromToken(token);
99
- if (verdict)
100
- found = verdict;
142
+ return true;
143
+ }
144
+ return false;
145
+ }
146
+ function lastExplicitVerdictLine(text) {
147
+ let found = null;
148
+ for (const rawLine of text.split('\n')) {
149
+ const match = lineWithoutBullet(rawLine).match(EXPLICIT_VERDICT_LINE);
150
+ if (match?.[1])
151
+ found = match[1].toUpperCase();
101
152
  }
102
153
  return found;
103
154
  }
104
155
  export function parseConsultVerdictFromText(text) {
105
- const explicit = lastExplicitVerdict(text);
156
+ const decision = firstHeaderDecision(text);
157
+ if (decision)
158
+ return decision;
159
+ // Verdict lines inside REASONING are model text, not the vote.
160
+ const { header, reasoning } = splitAtReasoning(text);
161
+ const scanTarget = reasoning === null ? text : header;
162
+ const explicit = lastExplicitVerdictLine(scanTarget);
106
163
  if (explicit)
107
164
  return explicit;
108
- const decisionToken = text.match(/\bDECISION:\s*(\w+)/i)?.[1];
109
- if (decisionToken) {
110
- const mapped = verdictFromToken(decisionToken);
111
- if (mapped)
112
- return mapped;
165
+ if (!text.trim())
166
+ return null;
167
+ if (hasAmbiguousVerdictLine(text) || VERDICT_OPTION_ECHO.test(text))
168
+ return 'UNREVIEWED';
169
+ if (/\b(?:PASS|FAIL|CONDITIONAL|UNREVIEWED|SHIP|APPROVE|REJECT)\b/i.test(text)) {
170
+ return 'UNREVIEWED';
113
171
  }
114
- const normalized = text.toUpperCase();
115
- if (/\bCONDITIONAL(\s+PASS)?\b/.test(normalized))
116
- return 'CONDITIONAL';
117
- if (/\b(?:PASS|SHIP|APPROVE)\b/.test(normalized))
118
- return 'PASS';
119
- if (/\b(?:FAIL|REJECT)\b/.test(normalized))
120
- return 'FAIL';
121
172
  return null;
122
173
  }
123
174
  export function extractTopRisksFromText(text, max = 5) {
@@ -1,8 +1,7 @@
1
1
  import { frameworkLogger } from "../core/framework-logger.js";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { fileURLToPath } from "node:url";
5
- import { getComprehensiveLogs, filterLogsByConfig, frameworkLogToParsedEntry, } from "./log-parser.js";
4
+ import { getComprehensiveLogs, filterLogsByConfig, frameworkLogToParsedEntry, activityLogPath, } from "./log-parser.js";
6
5
  import { calculateTimeRange, calculateMetrics, calculatePeakActivity, calculateHealthScore, generateInsights, generateRecommendations, generateAlerts, } from "./metrics.js";
7
6
  import { formatReport } from "./report-formatter.js";
8
7
  export class FrameworkReportingSystem {
@@ -106,9 +105,7 @@ const report = await reportingSystem.generateCustomReport('${template.name}');
106
105
  }
107
106
  // Same activity.log path readCurrentLogFile rebuilds from size and mtime.
108
107
  activityLogFile() {
109
- const currentFilePath = fileURLToPath(import.meta.url);
110
- const projectRoot = path.resolve(path.dirname(currentFilePath), "../../");
111
- return path.join(projectRoot, "logs", "framework", "activity.log");
108
+ return activityLogPath();
112
109
  }
113
110
  currentLogStamp() {
114
111
  try {