0xray 3.5.3 → 3.5.5

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 (76) hide show
  1. package/.xray/features.json +5 -1
  2. package/.xray/features.schema.json +56 -0
  3. package/AGENTS-consumer.md +1 -1
  4. package/AGENTS.md +11 -1
  5. package/README.md +1 -1
  6. package/SKILLS.md +1 -1
  7. package/dist/AGENTS.md +11 -1
  8. package/dist/CHANGELOG.md +54 -0
  9. package/dist/README.md +1 -1
  10. package/dist/SKILLS.md +1 -1
  11. package/dist/cli/commands/hermes-install.js +3 -1
  12. package/dist/cli/index.js +0 -0
  13. package/dist/core/features-config.d.ts +15 -0
  14. package/dist/governance/governance-service.d.ts +1 -0
  15. package/dist/governance/governance-service.js +81 -6
  16. package/dist/integrations/enforcement-gate.js +15 -5
  17. package/dist/integrations/grok/grok-cli.js +1 -10
  18. package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
  19. package/dist/integrations/grok/hooks/post-tool-use.js +48 -14
  20. package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
  21. package/dist/integrations/grok/hooks/session-start.js +28 -0
  22. package/dist/integrations/hermes-agent/__init__.py +11 -1
  23. package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
  25. package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
  26. package/dist/integrations/hermes-agent/bridge.mjs +73 -10
  27. package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
  28. package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
  29. package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
  30. package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
  31. package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
  32. package/dist/mcps/architect-tools.server.js +6 -1
  33. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
  34. package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
  35. package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
  36. package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
  37. package/dist/mcps/orchestrator/server.js +54 -5
  38. package/dist/memory-routing/provider-loader.js +30 -2
  39. package/dist/nucleus/aside-worktree.d.ts +41 -0
  40. package/dist/nucleus/aside-worktree.js +164 -0
  41. package/dist/nucleus/confer.d.ts +57 -0
  42. package/dist/nucleus/confer.js +308 -0
  43. package/dist/nucleus/delegation-gate.d.ts +12 -1
  44. package/dist/nucleus/delegation-gate.js +113 -13
  45. package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
  46. package/dist/nucleus/lead-dev-plan-persistence.js +222 -18
  47. package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
  48. package/dist/nucleus/spawn-plan-resolution.js +42 -0
  49. package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
  50. package/dist/nucleus/synthesis-consult-receipt.js +171 -0
  51. package/dist/nucleus/synthesis-context.js +1 -1
  52. package/dist/nucleus/user-aside.d.ts +70 -0
  53. package/dist/nucleus/user-aside.js +393 -0
  54. package/dist/plugin/xray-codex-injection.js +1 -1
  55. package/dist/postprocessor/PostProcessor.d.ts +1 -0
  56. package/dist/postprocessor/PostProcessor.js +11 -0
  57. package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
  58. package/dist/processors/implementations/session-summary-processor.js +9 -6
  59. package/package.json +14 -5
  60. package/scripts/hooks/install-hooks.cjs +3 -3
  61. package/scripts/hooks/run-hook.js +171 -86
  62. package/scripts/mjs/verify-confer-core.mjs +103 -0
  63. package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
  64. package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
  65. package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
  66. package/scripts/mjs/verify-user-aside-core.mjs +137 -0
  67. package/scripts/node/bridge-mcp-wiring.cjs +10 -0
  68. package/scripts/node/install-bridges.cjs +3 -11
  69. package/scripts/node/load-reflection-config.mjs +2 -2
  70. package/scripts/node/release-gate.mjs +8 -4
  71. package/src/mcps/architect-tools.server.ts +6 -1
  72. package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
  73. package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
  74. package/src/mcps/orchestrator/server.ts +56 -5
  75. package/xray/features.json +5 -1
  76. package/xray/features.schema.json +56 -0
@@ -7,9 +7,145 @@ 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';
11
+ import { resolveSpawnPlan } from './spawn-plan-resolution.js';
12
+ import { isAsideActiveForSession, isUserAsideTodoId, isUserAsidesEnabled, parseAsideIdFromTodoId, findAsideContainingTodo, updateUserAsideTodoStatus, } from './user-aside.js';
13
+ export const DEFAULT_PLAN_STALE_MS = 8 * 60 * 60 * 1000;
14
+ export const DEFAULT_PLAN_ARCHIVE_MARKER_MS = 24 * 60 * 60 * 1000;
10
15
  export function leadDevPlanStatePath(projectRoot = process.cwd()) {
11
16
  return path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
12
17
  }
18
+ export function loadLeadDevPlanStaleMs(projectRoot = process.cwd()) {
19
+ const featuresPath = path.join(projectRoot, '.xray', 'features.json');
20
+ if (!fs.existsSync(featuresPath))
21
+ return DEFAULT_PLAN_STALE_MS;
22
+ try {
23
+ const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
24
+ const hours = data.multi_agent_orchestration?.plan_stale_hours;
25
+ if (typeof hours === 'number' && hours > 0) {
26
+ return hours * 60 * 60 * 1000;
27
+ }
28
+ }
29
+ catch {
30
+ /* keep default */
31
+ }
32
+ return DEFAULT_PLAN_STALE_MS;
33
+ }
34
+ export function loadLeadDevPlanArchiveMarkerMs(projectRoot = process.cwd()) {
35
+ const featuresPath = path.join(projectRoot, '.xray', 'features.json');
36
+ if (!fs.existsSync(featuresPath))
37
+ return DEFAULT_PLAN_ARCHIVE_MARKER_MS;
38
+ try {
39
+ const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
40
+ const hours = data.multi_agent_orchestration?.plan_archive_marker_hours;
41
+ if (typeof hours === 'number' && hours > 0) {
42
+ return hours * 60 * 60 * 1000;
43
+ }
44
+ }
45
+ catch {
46
+ /* keep default */
47
+ }
48
+ return DEFAULT_PLAN_ARCHIVE_MARKER_MS;
49
+ }
50
+ function planAgeMs(plan, projectRoot) {
51
+ const persistedAt = plan.persistedAt;
52
+ if (persistedAt) {
53
+ const age = Date.now() - new Date(persistedAt).getTime();
54
+ if (Number.isFinite(age))
55
+ return age;
56
+ }
57
+ try {
58
+ const stat = fs.statSync(leadDevPlanStatePath(projectRoot));
59
+ return Date.now() - stat.mtimeMs;
60
+ }
61
+ catch {
62
+ return Number.POSITIVE_INFINITY;
63
+ }
64
+ }
65
+ /**
66
+ * Mandatory synthesis consults (s.1–s.3) must stay spawnable until complete —
67
+ * never treat an active realignment plan as stale while consult todos remain.
68
+ */
69
+ export function isProtectedSynthesisRealignmentPlan(plan) {
70
+ return isSynthesisRealignmentPlan(plan) && !areSynthesisConsultTodosComplete(plan);
71
+ }
72
+ /**
73
+ * Stale when every outstanding todo is still pending and plan age exceeds TTL.
74
+ * In-progress or completed todos imply active work — plan stays valid.
75
+ */
76
+ export function isLeadDevPlanStale(plan, projectRoot = process.cwd()) {
77
+ if (!plan.active)
78
+ return false;
79
+ if (isProtectedSynthesisRealignmentPlan(plan))
80
+ return false;
81
+ const outstanding = getOutstandingTodos(plan);
82
+ if (outstanding.length === 0)
83
+ return false;
84
+ const allStillPending = outstanding.every((t) => t.status === 'pending');
85
+ if (!allStillPending)
86
+ return false;
87
+ return planAgeMs(plan, projectRoot) > loadLeadDevPlanStaleMs(projectRoot);
88
+ }
89
+ export function archiveStaleLeadDevPlan(projectRoot = process.cwd()) {
90
+ const plan = loadPersistedLeadDevPlan(projectRoot);
91
+ if (!plan) {
92
+ return { archived: false };
93
+ }
94
+ if (isProtectedSynthesisRealignmentPlan(plan)) {
95
+ return { archived: false, reason: 'synthesis-realignment-active' };
96
+ }
97
+ if (!isLeadDevPlanStale(plan, projectRoot)) {
98
+ return { archived: false };
99
+ }
100
+ const stateDir = path.dirname(leadDevPlanStatePath(projectRoot));
101
+ fs.mkdirSync(stateDir, { recursive: true });
102
+ const archivePath = path.join(stateDir, `lead-dev-plan.archived-${Date.now()}.json`);
103
+ const payload = {
104
+ ...plan,
105
+ active: false,
106
+ archivedAt: new Date().toISOString(),
107
+ archiveReason: 'stale-unstarted-todos',
108
+ };
109
+ fs.writeFileSync(archivePath, JSON.stringify(payload, null, 2));
110
+ fs.unlinkSync(leadDevPlanStatePath(projectRoot));
111
+ return { archived: true, archivePath, reason: 'stale-unstarted-todos' };
112
+ }
113
+ /** Recent stale archival — keeps spawn-plan-stale gate after session-start cleanup. */
114
+ export function findRecentStalePlanArchive(projectRoot = process.cwd(), maxAgeMs = loadLeadDevPlanArchiveMarkerMs(projectRoot)) {
115
+ const stateDir = path.join(projectRoot, '.xray', 'state');
116
+ if (!fs.existsSync(stateDir))
117
+ return null;
118
+ const candidates = fs
119
+ .readdirSync(stateDir)
120
+ .filter((name) => name.startsWith('lead-dev-plan.archived-') && name.endsWith('.json'))
121
+ .map((name) => {
122
+ const archivePath = path.join(stateDir, name);
123
+ return { archivePath, mtimeMs: fs.statSync(archivePath).mtimeMs };
124
+ })
125
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
126
+ for (const candidate of candidates) {
127
+ try {
128
+ const data = JSON.parse(fs.readFileSync(candidate.archivePath, 'utf8'));
129
+ if (data.archiveReason !== 'stale-unstarted-todos')
130
+ continue;
131
+ const archivedAtMs = data.archivedAt
132
+ ? new Date(data.archivedAt).getTime()
133
+ : candidate.mtimeMs;
134
+ if (!Number.isFinite(archivedAtMs))
135
+ continue;
136
+ if (Date.now() - archivedAtMs > maxAgeMs)
137
+ continue;
138
+ return {
139
+ archivePath: candidate.archivePath,
140
+ archivedAt: data.archivedAt ?? new Date(archivedAtMs).toISOString(),
141
+ };
142
+ }
143
+ catch {
144
+ /* skip corrupt archive */
145
+ }
146
+ }
147
+ return null;
148
+ }
13
149
  export function loadPersistedLeadDevPlan(projectRoot = process.cwd()) {
14
150
  const planPath = leadDevPlanStatePath(projectRoot);
15
151
  if (!fs.existsSync(planPath))
@@ -51,6 +187,8 @@ export function hasValidLeadDevPlanForSpawn(projectRoot = process.cwd(), _sessio
51
187
  const plan = loadPersistedLeadDevPlan(projectRoot);
52
188
  if (!plan?.active)
53
189
  return false;
190
+ if (isLeadDevPlanStale(plan, projectRoot))
191
+ return false;
54
192
  const outstanding = getOutstandingTodos(plan);
55
193
  if (outstanding.length > 0)
56
194
  return true;
@@ -97,10 +235,38 @@ function tryCompleteSynthesisCheckpointAfterTodo(projectRoot, sessionId) {
97
235
  void import('./synthesis-completion.js').then(({ runSynthesisCheckpointSideEffects }) => runSynthesisCheckpointSideEffects(projectRoot, sid, plan, completed));
98
236
  }
99
237
  }
100
- export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd()) {
238
+ export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd(), sessionId) {
239
+ if (isUserAsidesEnabled(projectRoot) && isUserAsideTodoId(todoId)) {
240
+ return updateUserAsideTodoStatus(todoId, status, projectRoot, sessionId);
241
+ }
101
242
  const plan = loadPersistedLeadDevPlan(projectRoot);
102
243
  if (!plan)
103
244
  return false;
245
+ let targetTodo;
246
+ for (const phase of plan.phases) {
247
+ for (const todo of phase.todos) {
248
+ if (todo.id === todoId) {
249
+ targetTodo = todo;
250
+ break;
251
+ }
252
+ }
253
+ if (targetTodo)
254
+ break;
255
+ }
256
+ if (status === 'completed' && isSynthesisConsultTodoId(todoId)) {
257
+ const receiptExpected = {};
258
+ if (plan.sessionId !== undefined)
259
+ receiptExpected.sessionId = plan.sessionId;
260
+ if (targetTodo?.subagent)
261
+ receiptExpected.subagent = targetTodo.subagent;
262
+ if (!hasValidSynthesisConsultReceipt(todoId, projectRoot, receiptExpected)) {
263
+ return false;
264
+ }
265
+ const receipt = loadSynthesisConsultReceipt(todoId, projectRoot);
266
+ if (receipt?.verdict === 'FAIL') {
267
+ return false;
268
+ }
269
+ }
104
270
  let updated = false;
105
271
  for (const phase of plan.phases) {
106
272
  for (const todo of phase.todos) {
@@ -151,16 +317,41 @@ function agentsAlign(expected, actual) {
151
317
  const a = normalizeAgent(actual);
152
318
  return e === a || e.includes(a) || a.includes(e);
153
319
  }
154
- export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(), expectedTodo) {
155
- const plan = loadPersistedLeadDevPlan(projectRoot);
320
+ export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(), expectedTodo, sessionId) {
321
+ const explicitTodo = toolInput.planTodoId;
322
+ if (explicitTodo &&
323
+ isUserAsideTodoId(explicitTodo) &&
324
+ isUserAsidesEnabled(projectRoot)) {
325
+ const parsedId = parseAsideIdFromTodoId(explicitTodo);
326
+ const targetAside = parsedId
327
+ ? findAsideContainingTodo(explicitTodo, projectRoot, parsedId)
328
+ : null;
329
+ if (targetAside && !isAsideActiveForSession(targetAside.id, projectRoot, sessionId)) {
330
+ return {
331
+ valid: false,
332
+ reason: `Aside \`${targetAside.id}\` is not active for this session`,
333
+ gate: 'aside-session-mismatch',
334
+ expectedTodoId: explicitTodo,
335
+ hint: {
336
+ tool: 'orchestrate-task',
337
+ userAsideId: targetAside.id,
338
+ track: 'user-aside',
339
+ },
340
+ };
341
+ }
342
+ }
343
+ const resolved = isUserAsidesEnabled(projectRoot)
344
+ ? resolveSpawnPlan(toolInput, projectRoot, sessionId)
345
+ : { source: 'main', plan: loadPersistedLeadDevPlan(projectRoot) };
346
+ const plan = resolved.plan;
156
347
  const nextTodo = expectedTodo ?? (plan ? getNextRequiredTodo(plan) : null);
157
348
  if (!nextTodo) {
158
349
  return { valid: true };
159
350
  }
160
351
  const prompt = String(toolInput.prompt || toolInput.description || toolInput.task || '').toLowerCase();
161
352
  const subagent = String(toolInput.subagent_type || toolInput.agent || '');
162
- const explicitTodo = toolInput.planTodoId || toolInput.delegationId;
163
- if (explicitTodo && explicitTodo === nextTodo.id) {
353
+ const explicitMatch = toolInput.planTodoId || toolInput.delegationId;
354
+ if (explicitMatch && explicitMatch === nextTodo.id) {
164
355
  return { valid: true, expectedTodoId: nextTodo.id };
165
356
  }
166
357
  if (prompt.includes(nextTodo.id.toLowerCase())) {
@@ -172,25 +363,38 @@ export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(),
172
363
  if (subagent && agentsAlign(nextTodo.subagent, subagent) && prompt.length > 20) {
173
364
  return { valid: true, expectedTodoId: nextTodo.id };
174
365
  }
366
+ const hint = {
367
+ tool: 'Task',
368
+ subagent_type: nextTodo.subagent,
369
+ planTodoId: nextTodo.id,
370
+ description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. ` +
371
+ `Include plan todo id in Task prompt.`,
372
+ };
373
+ if (resolved.source === 'aside' && resolved.asideId) {
374
+ hint.asideId = resolved.asideId;
375
+ hint.track = 'user-aside';
376
+ if (resolved.worktree)
377
+ hint.worktree = resolved.worktree;
378
+ if (resolved.branch)
379
+ hint.branch = resolved.branch;
380
+ }
175
381
  return {
176
382
  valid: false,
177
- reason: `Spawn must target plan todo ${nextTodo.id} before other work`,
178
- gate: 'spawn-todo-persistence',
383
+ reason: resolved.source === 'aside'
384
+ ? `Spawn must target aside todo ${nextTodo.id} (aside ${resolved.asideId}) before other work`
385
+ : `Spawn must target plan todo ${nextTodo.id} before other work`,
386
+ gate: resolved.source === 'aside' ? 'aside-spawn-todo' : 'spawn-todo-persistence',
179
387
  expectedTodoId: nextTodo.id,
180
- hint: {
181
- tool: 'Task',
182
- subagent_type: nextTodo.subagent,
183
- planTodoId: nextTodo.id,
184
- description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. ` +
185
- `Include plan todo id in Task prompt.`,
186
- },
388
+ hint,
187
389
  };
188
390
  }
189
- export function markTodoInProgressOnSpawn(toolInput, projectRoot = process.cwd()) {
190
- const plan = loadPersistedLeadDevPlan(projectRoot);
191
- if (!plan)
391
+ export function markTodoInProgressOnSpawn(toolInput, projectRoot = process.cwd(), sessionId) {
392
+ const resolved = isUserAsidesEnabled(projectRoot)
393
+ ? resolveSpawnPlan(toolInput, projectRoot, sessionId)
394
+ : { source: 'main', plan: loadPersistedLeadDevPlan(projectRoot) };
395
+ if (!resolved.plan)
192
396
  return null;
193
- const validation = validateSpawnMatchesTodo(toolInput, projectRoot);
397
+ const validation = validateSpawnMatchesTodo(toolInput, projectRoot, undefined, sessionId);
194
398
  if (!validation.valid || !validation.expectedTodoId)
195
399
  return null;
196
400
  updatePlanTodoStatus(validation.expectedTodoId, 'in_progress', projectRoot);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Spawn plan resolution SSOT — main lead-dev plan vs active user aside.
3
+ * Extracted to avoid circular imports between user-aside and lead-dev-plan-persistence.
4
+ */
5
+ import { type PersistedLeadDevPlan } from './lead-dev-plan-persistence.js';
6
+ export interface SpawnPlanResolution {
7
+ source: 'main' | 'aside' | 'none';
8
+ asideId?: string;
9
+ plan: PersistedLeadDevPlan | null;
10
+ worktree?: string;
11
+ branch?: string;
12
+ }
13
+ export interface SpawnPlanToolInput {
14
+ planTodoId?: string;
15
+ prompt?: string;
16
+ description?: string;
17
+ task?: string;
18
+ }
19
+ export declare function resolveSpawnPlan(toolInput: SpawnPlanToolInput, projectRoot?: string, sessionId?: string | null): SpawnPlanResolution;
20
+ export declare function hasValidSpawnPlanContext(projectRoot?: string, sessionId?: string | null): boolean;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Spawn plan resolution SSOT — main lead-dev plan vs active user aside.
3
+ * Extracted to avoid circular imports between user-aside and lead-dev-plan-persistence.
4
+ */
5
+ import { getOutstandingTodos, hasValidLeadDevPlanForSpawn, loadPersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
6
+ import { asideToSpawnPlan, findAsideContainingTodo, isAsideActiveForSession, isUserAsideTodoId, isUserAsidesEnabled, loadActiveUserAside, parseAsideIdFromTodoId, } from './user-aside.js';
7
+ export function resolveSpawnPlan(toolInput, projectRoot = process.cwd(), sessionId) {
8
+ const explicitTodo = toolInput.planTodoId;
9
+ if (explicitTodo && isUserAsideTodoId(explicitTodo)) {
10
+ const parsedAsideId = parseAsideIdFromTodoId(explicitTodo);
11
+ const aside = findAsideContainingTodo(explicitTodo, projectRoot, parsedAsideId ?? undefined);
12
+ if (aside && isAsideActiveForSession(aside.id, projectRoot, sessionId)) {
13
+ return asideResolution(aside);
14
+ }
15
+ }
16
+ const active = loadActiveUserAside(projectRoot, sessionId);
17
+ if (active) {
18
+ return asideResolution(active);
19
+ }
20
+ const main = loadPersistedLeadDevPlan(projectRoot);
21
+ if (main?.active) {
22
+ return { source: 'main', plan: main };
23
+ }
24
+ return { source: 'none', plan: null };
25
+ }
26
+ function asideResolution(aside) {
27
+ return {
28
+ source: 'aside',
29
+ asideId: aside.id,
30
+ plan: asideToSpawnPlan(aside),
31
+ ...(aside.worktree ? { worktree: aside.worktree } : {}),
32
+ ...(aside.branch ? { branch: aside.branch } : {}),
33
+ };
34
+ }
35
+ export function hasValidSpawnPlanContext(projectRoot = process.cwd(), sessionId) {
36
+ const active = loadActiveUserAside(projectRoot, sessionId);
37
+ if (active && isUserAsidesEnabled(projectRoot)) {
38
+ const plan = asideToSpawnPlan(active);
39
+ return getOutstandingTodos(plan).length > 0 || plan.active;
40
+ }
41
+ return hasValidLeadDevPlanForSpawn(projectRoot, sessionId);
42
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Synthesis consult receipt SSOT — outcome-based completion for s.N todos.
3
+ * Receipts live at `.xray/state/synthesis-consult-{todoId}.json`.
4
+ */
5
+ export type SynthesisConsultVerdict = 'PASS' | 'CONDITIONAL' | 'FAIL';
6
+ export interface SynthesisConsultReceipt {
7
+ sessionId: string;
8
+ subagent: string;
9
+ verdict: SynthesisConsultVerdict;
10
+ topRisks: string[];
11
+ hardeningNote: string;
12
+ todoId?: string;
13
+ recordedAt?: string;
14
+ }
15
+ export declare function isSynthesisConsultTodoId(todoId: string): boolean;
16
+ export declare function synthesisConsultReceiptPath(todoId: string, projectRoot?: string): string;
17
+ export declare function loadSynthesisConsultReceipt(todoId: string, projectRoot?: string): SynthesisConsultReceipt | null;
18
+ export declare function writeSynthesisConsultReceipt(todoId: string, receipt: SynthesisConsultReceipt, projectRoot?: string): string;
19
+ export declare function validateSynthesisConsultReceipt(receipt: SynthesisConsultReceipt, todoId: string, expected?: {
20
+ sessionId?: string | null;
21
+ subagent?: string;
22
+ }): boolean;
23
+ export declare function hasValidSynthesisConsultReceipt(todoId: string, projectRoot?: string, expected?: {
24
+ sessionId?: string | null;
25
+ subagent?: string;
26
+ }): boolean;
27
+ export declare function parseConsultVerdictFromText(text: string): SynthesisConsultVerdict | null;
28
+ export declare function extractTopRisksFromText(text: string, max?: number): string[];
29
+ export declare function extractHardeningNoteFromText(text: string, maxChars?: number): string;
30
+ export declare function coerceToolOutputText(output: unknown): string;
31
+ export declare function buildReceiptFromConsultOutput(todoId: string, subagent: string, sessionId: string, outputText: string): SynthesisConsultReceipt | null;
32
+ export declare function tryRecordSynthesisConsultReceipt(todoId: string, subagent: string, sessionId: string, output: unknown, projectRoot?: string): SynthesisConsultReceipt | null;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Synthesis consult receipt SSOT — outcome-based completion for s.N todos.
3
+ * Receipts live at `.xray/state/synthesis-consult-{todoId}.json`.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ const CONSULT_TODO_PATTERN = /^s\.\d+$/;
8
+ export function isSynthesisConsultTodoId(todoId) {
9
+ return CONSULT_TODO_PATTERN.test(todoId);
10
+ }
11
+ export function synthesisConsultReceiptPath(todoId, projectRoot = process.cwd()) {
12
+ return path.join(projectRoot, '.xray', 'state', `synthesis-consult-${todoId}.json`);
13
+ }
14
+ export function loadSynthesisConsultReceipt(todoId, projectRoot = process.cwd()) {
15
+ const receiptPath = synthesisConsultReceiptPath(todoId, projectRoot);
16
+ if (!fs.existsSync(receiptPath))
17
+ return null;
18
+ try {
19
+ return JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function writeSynthesisConsultReceipt(todoId, receipt, projectRoot = process.cwd()) {
26
+ const receiptPath = synthesisConsultReceiptPath(todoId, projectRoot);
27
+ fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
28
+ const payload = {
29
+ ...receipt,
30
+ todoId,
31
+ recordedAt: receipt.recordedAt ?? new Date().toISOString(),
32
+ };
33
+ fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2));
34
+ return receiptPath;
35
+ }
36
+ function normalizeSubagent(agent) {
37
+ const key = agent.toLowerCase().trim();
38
+ const aliases = {
39
+ 'bug-triage-specialist': 'bug-triage',
40
+ 'code-reviewer': 'code-review',
41
+ };
42
+ return aliases[key] ?? key;
43
+ }
44
+ function subagentsAlign(expected, actual) {
45
+ const e = normalizeSubagent(expected);
46
+ const a = normalizeSubagent(actual);
47
+ return e === a || e.includes(a) || a.includes(e);
48
+ }
49
+ function isValidVerdict(value) {
50
+ return value === 'PASS' || value === 'CONDITIONAL' || value === 'FAIL';
51
+ }
52
+ export function validateSynthesisConsultReceipt(receipt, todoId, expected) {
53
+ if (!receipt.sessionId || !receipt.subagent || !isValidVerdict(receipt.verdict)) {
54
+ return false;
55
+ }
56
+ if (!Array.isArray(receipt.topRisks))
57
+ return false;
58
+ if (typeof receipt.hardeningNote !== 'string')
59
+ return false;
60
+ if (expected?.sessionId && receipt.sessionId !== expected.sessionId)
61
+ return false;
62
+ if (expected?.subagent && !subagentsAlign(expected.subagent, receipt.subagent)) {
63
+ return false;
64
+ }
65
+ if (receipt.todoId && receipt.todoId !== todoId)
66
+ return false;
67
+ return true;
68
+ }
69
+ export function hasValidSynthesisConsultReceipt(todoId, projectRoot = process.cwd(), expected) {
70
+ const receipt = loadSynthesisConsultReceipt(todoId, projectRoot);
71
+ if (!receipt)
72
+ return false;
73
+ return validateSynthesisConsultReceipt(receipt, todoId, expected);
74
+ }
75
+ export function parseConsultVerdictFromText(text) {
76
+ const normalized = text.toUpperCase();
77
+ const decisionMatch = text.match(/\bDECISION:\s*(\w+)/i);
78
+ if (decisionMatch) {
79
+ const decision = decisionMatch[1].toLowerCase();
80
+ if (decision === 'approve' || decision === 'approved' || decision === 'pass') {
81
+ return 'PASS';
82
+ }
83
+ if (decision === 'reject' || decision === 'rejected' || decision === 'fail') {
84
+ return 'FAIL';
85
+ }
86
+ if (decision === 'abstain' ||
87
+ decision === 'needs_revision' ||
88
+ decision === 'conditional' ||
89
+ decision === 'revise') {
90
+ return 'CONDITIONAL';
91
+ }
92
+ }
93
+ if (/\bCONDITIONAL(\s+PASS)?\b/.test(normalized))
94
+ return 'CONDITIONAL';
95
+ if (/\b(?:PASS|SHIP|APPROVE)\b/.test(normalized))
96
+ return 'PASS';
97
+ if (/\b(?:FAIL|REJECT)\b/.test(normalized))
98
+ return 'FAIL';
99
+ return null;
100
+ }
101
+ export function extractTopRisksFromText(text, max = 5) {
102
+ const risks = [];
103
+ const lines = text.split('\n');
104
+ for (const line of lines) {
105
+ const trimmed = line.trim();
106
+ if (!trimmed)
107
+ continue;
108
+ if (/^(?:[-*•]|\d+\.)\s*(?:risk|top\s*risk)/i.test(trimmed)) {
109
+ risks.push(trimmed.replace(/^[-*•\d.]+\s*/i, '').slice(0, 240));
110
+ }
111
+ else if (/^risk\s*:/i.test(trimmed)) {
112
+ risks.push(trimmed.replace(/^risk\s*:\s*/i, '').slice(0, 240));
113
+ }
114
+ if (risks.length >= max)
115
+ break;
116
+ }
117
+ return risks;
118
+ }
119
+ export function extractHardeningNoteFromText(text, maxChars = 500) {
120
+ const match = text.match(/(?:hardening|recommend(?:ation)?s?)\s*[:\n]([\s\S]{0,800})/i);
121
+ if (match?.[1])
122
+ return match[1].trim().slice(0, maxChars);
123
+ const lines = text.split('\n').filter((l) => l.trim().length > 0);
124
+ return lines.slice(-3).join(' ').slice(0, maxChars);
125
+ }
126
+ export function coerceToolOutputText(output) {
127
+ if (typeof output === 'string')
128
+ return output;
129
+ if (output == null)
130
+ return '';
131
+ if (typeof output === 'object') {
132
+ const obj = output;
133
+ if (typeof obj.text === 'string')
134
+ return obj.text;
135
+ if (typeof obj.content === 'string')
136
+ return obj.content;
137
+ if (typeof obj.message === 'string')
138
+ return obj.message;
139
+ try {
140
+ return JSON.stringify(output);
141
+ }
142
+ catch {
143
+ return String(output);
144
+ }
145
+ }
146
+ return String(output);
147
+ }
148
+ export function buildReceiptFromConsultOutput(todoId, subagent, sessionId, outputText) {
149
+ const text = outputText.trim();
150
+ if (!text)
151
+ return null;
152
+ const verdict = parseConsultVerdictFromText(text);
153
+ if (!verdict)
154
+ return null;
155
+ return {
156
+ sessionId,
157
+ subagent,
158
+ verdict,
159
+ topRisks: extractTopRisksFromText(text),
160
+ hardeningNote: extractHardeningNoteFromText(text),
161
+ todoId,
162
+ };
163
+ }
164
+ export function tryRecordSynthesisConsultReceipt(todoId, subagent, sessionId, output, projectRoot = process.cwd()) {
165
+ const receipt = buildReceiptFromConsultOutput(todoId, subagent, sessionId, coerceToolOutputText(output));
166
+ if (!receipt || !validateSynthesisConsultReceipt(receipt, todoId, { sessionId, subagent })) {
167
+ return null;
168
+ }
169
+ writeSynthesisConsultReceipt(todoId, receipt, projectRoot);
170
+ return receipt;
171
+ }
@@ -78,7 +78,7 @@ export function appendSynthesisContextToResponse(content, collatedText) {
78
78
 
79
79
  ${collatedText}
80
80
 
81
- **Next:** Spawn Task for each mandatory consult todo (s.1 researcher, s.2 architect-tools, s.3 code-review). Checkpoint clears only when all consult todos complete.`;
81
+ **Next:** Confer quorum runs automatically via analyze-complexity (researcher → architect-tools → code-review). If confer partial-fails, spawn Task for the next consult todo (s.1–s.3). Checkpoint clears when all consult todos complete.`;
82
82
  if (content.length === 0) {
83
83
  return [{ type: 'text', text: synthesisSection.trim() }];
84
84
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * User aside — parallel work tracks (worktree-like) declared by the user.
3
+ * SSOT: `.xray/state/asides/{id}.json` + `_active.json`.
4
+ * Todo ids: `{asideId}.a.{phase}.{todo}` — globally unique per aside.
5
+ */
6
+ import { type LeadDevPlan, type LeadDevPlanTaskInput, type LeadDevTodo } from './autonomy-kernel.js';
7
+ import { type PersistedLeadDevPlan } from './lead-dev-plan-persistence.js';
8
+ export declare const USER_ASIDE_ID_PATTERN: RegExp;
9
+ /** Namespaced aside todo: `suit-nft.a.1.1` */
10
+ export declare const USER_ASIDE_TODO_PATTERN: RegExp;
11
+ export declare const USER_ASIDE_STATE_VERSION: 1;
12
+ export type UserAsideStatus = 'active' | 'paused' | 'completed' | 'archived';
13
+ export interface UserAsideGovernance {
14
+ /** Session-boot hint to run confer before aside phase work (manual MCP — not auto confer). */
15
+ conferOnPhaseStart?: boolean;
16
+ /** Repertoire trap signal names surfaced in session-boot for aside work. */
17
+ trapSignals?: string[];
18
+ }
19
+ export interface UserAside {
20
+ version: typeof USER_ASIDE_STATE_VERSION;
21
+ id: string;
22
+ title: string;
23
+ status: UserAsideStatus;
24
+ owner?: string;
25
+ description: string;
26
+ definitionOfDone?: string;
27
+ worktree?: string;
28
+ branch?: string;
29
+ persistedAt?: string;
30
+ sessionId?: string;
31
+ governance?: UserAsideGovernance;
32
+ plan: LeadDevPlan;
33
+ }
34
+ export interface ActiveAsidePointer {
35
+ asideId: string;
36
+ activatedAt: string;
37
+ sessionId?: string;
38
+ }
39
+ export declare class UserAsideValidationError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ export declare function assertSafeAsideId(asideId: string): string;
43
+ export declare function asideTodoId(asideId: string, phaseIndex: number, todoIndex: number): string;
44
+ export declare function isUserAsideTodoId(todoId: string): boolean;
45
+ export declare function parseAsideIdFromTodoId(todoId: string): string | null;
46
+ export declare function userAsidesDir(projectRoot?: string): string;
47
+ export declare function userAsidePath(asideId: string, projectRoot?: string): string;
48
+ export declare function activeAsidePointerPath(projectRoot?: string): string;
49
+ export declare function isUserAsidesEnabled(projectRoot?: string): boolean;
50
+ /** Build a phased aside plan from task intake (mirrors lead-dev plan, namespaced todo ids). */
51
+ export declare function buildUserAsidePlan(asideId: string, title: string, description: string, taskInputs?: LeadDevPlanTaskInput[], mcpOverallComplexity?: number, projectRoot?: string): UserAside | null;
52
+ /** Merge new task intake into an existing aside (re-intake). */
53
+ export declare function mergeUserAsideIntake(existing: UserAside, description: string, taskInputs: LeadDevPlanTaskInput[], mcpOverallComplexity?: number, projectRoot?: string): UserAside;
54
+ export declare function loadUserAside(asideId: string, projectRoot?: string): UserAside | null;
55
+ export declare function saveUserAside(aside: UserAside, projectRoot?: string): string;
56
+ export declare function listUserAsideIds(projectRoot?: string): string[];
57
+ export declare function loadActiveAsidePointer(projectRoot?: string): ActiveAsidePointer | null;
58
+ export declare function getActiveAsideId(projectRoot?: string, sessionId?: string | null): string | null;
59
+ export declare function setActiveAsideId(asideId: string, projectRoot?: string, sessionId?: string | null): ActiveAsidePointer;
60
+ export declare function clearActiveAside(projectRoot?: string): boolean;
61
+ export declare function loadActiveUserAside(projectRoot?: string, sessionId?: string | null): UserAside | null;
62
+ /** PersistedLeadDevPlan view of an aside for spawn-gate reuse. */
63
+ export declare function asideToSpawnPlan(aside: UserAside): PersistedLeadDevPlan;
64
+ /** Aside routable when active for session, or pointer is unscoped (accepted exception). */
65
+ export declare function isAsideActiveForSession(asideId: string, projectRoot?: string, sessionId?: string | null): boolean;
66
+ export declare function findAsideContainingTodo(todoId: string, projectRoot?: string, preferredAsideId?: string): UserAside | null;
67
+ export declare function updateUserAsideTodoStatus(todoId: string, status: LeadDevTodo['status'], projectRoot?: string, sessionId?: string | null): boolean;
68
+ export declare function getNextAsideTodo(aside: UserAside): LeadDevTodo | null;
69
+ export declare function formatUserAsideSummary(aside: UserAside): string;
70
+ export declare function getUserAsideBootHints(projectRoot?: string, sessionId?: string | null): Record<string, unknown> | null;