0xray 3.5.4 → 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 (73) 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 +44 -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 +21 -0
  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/pipeline-hook-runtime.mjs +211 -0
  29. package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
  30. package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
  31. package/dist/mcps/architect-tools.server.js +6 -1
  32. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
  33. package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
  34. package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
  35. package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
  36. package/dist/mcps/orchestrator/server.js +54 -5
  37. package/dist/memory-routing/provider-loader.js +30 -2
  38. package/dist/nucleus/aside-worktree.d.ts +41 -0
  39. package/dist/nucleus/aside-worktree.js +164 -0
  40. package/dist/nucleus/confer.d.ts +57 -0
  41. package/dist/nucleus/confer.js +308 -0
  42. package/dist/nucleus/delegation-gate.d.ts +4 -0
  43. package/dist/nucleus/delegation-gate.js +90 -11
  44. package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
  45. package/dist/nucleus/lead-dev-plan-persistence.js +201 -19
  46. package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
  47. package/dist/nucleus/spawn-plan-resolution.js +42 -0
  48. package/dist/nucleus/synthesis-consult-receipt.js +18 -2
  49. package/dist/nucleus/synthesis-context.js +1 -1
  50. package/dist/nucleus/user-aside.d.ts +70 -0
  51. package/dist/nucleus/user-aside.js +393 -0
  52. package/dist/postprocessor/PostProcessor.d.ts +1 -0
  53. package/dist/postprocessor/PostProcessor.js +11 -0
  54. package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
  55. package/dist/processors/implementations/session-summary-processor.js +9 -6
  56. package/package.json +14 -5
  57. package/scripts/hooks/install-hooks.cjs +3 -3
  58. package/scripts/hooks/run-hook.js +171 -86
  59. package/scripts/mjs/verify-confer-core.mjs +103 -0
  60. package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
  61. package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
  62. package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
  63. package/scripts/mjs/verify-user-aside-core.mjs +137 -0
  64. package/scripts/node/bridge-mcp-wiring.cjs +10 -0
  65. package/scripts/node/install-bridges.cjs +3 -11
  66. package/scripts/node/load-reflection-config.mjs +2 -2
  67. package/scripts/node/release-gate.mjs +8 -4
  68. package/src/mcps/architect-tools.server.ts +6 -1
  69. package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
  70. package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
  71. package/src/mcps/orchestrator/server.ts +56 -5
  72. package/xray/features.json +5 -1
  73. package/xray/features.schema.json +56 -0
@@ -7,10 +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, } from './synthesis-consult-receipt.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;
11
15
  export function leadDevPlanStatePath(projectRoot = process.cwd()) {
12
16
  return path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
13
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
+ }
14
149
  export function loadPersistedLeadDevPlan(projectRoot = process.cwd()) {
15
150
  const planPath = leadDevPlanStatePath(projectRoot);
16
151
  if (!fs.existsSync(planPath))
@@ -52,6 +187,8 @@ export function hasValidLeadDevPlanForSpawn(projectRoot = process.cwd(), _sessio
52
187
  const plan = loadPersistedLeadDevPlan(projectRoot);
53
188
  if (!plan?.active)
54
189
  return false;
190
+ if (isLeadDevPlanStale(plan, projectRoot))
191
+ return false;
55
192
  const outstanding = getOutstandingTodos(plan);
56
193
  if (outstanding.length > 0)
57
194
  return true;
@@ -98,7 +235,10 @@ function tryCompleteSynthesisCheckpointAfterTodo(projectRoot, sessionId) {
98
235
  void import('./synthesis-completion.js').then(({ runSynthesisCheckpointSideEffects }) => runSynthesisCheckpointSideEffects(projectRoot, sid, plan, completed));
99
236
  }
100
237
  }
101
- 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
+ }
102
242
  const plan = loadPersistedLeadDevPlan(projectRoot);
103
243
  if (!plan)
104
244
  return false;
@@ -122,6 +262,10 @@ export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd()
122
262
  if (!hasValidSynthesisConsultReceipt(todoId, projectRoot, receiptExpected)) {
123
263
  return false;
124
264
  }
265
+ const receipt = loadSynthesisConsultReceipt(todoId, projectRoot);
266
+ if (receipt?.verdict === 'FAIL') {
267
+ return false;
268
+ }
125
269
  }
126
270
  let updated = false;
127
271
  for (const phase of plan.phases) {
@@ -173,16 +317,41 @@ function agentsAlign(expected, actual) {
173
317
  const a = normalizeAgent(actual);
174
318
  return e === a || e.includes(a) || a.includes(e);
175
319
  }
176
- export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(), expectedTodo) {
177
- 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;
178
347
  const nextTodo = expectedTodo ?? (plan ? getNextRequiredTodo(plan) : null);
179
348
  if (!nextTodo) {
180
349
  return { valid: true };
181
350
  }
182
351
  const prompt = String(toolInput.prompt || toolInput.description || toolInput.task || '').toLowerCase();
183
352
  const subagent = String(toolInput.subagent_type || toolInput.agent || '');
184
- const explicitTodo = toolInput.planTodoId || toolInput.delegationId;
185
- if (explicitTodo && explicitTodo === nextTodo.id) {
353
+ const explicitMatch = toolInput.planTodoId || toolInput.delegationId;
354
+ if (explicitMatch && explicitMatch === nextTodo.id) {
186
355
  return { valid: true, expectedTodoId: nextTodo.id };
187
356
  }
188
357
  if (prompt.includes(nextTodo.id.toLowerCase())) {
@@ -194,25 +363,38 @@ export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(),
194
363
  if (subagent && agentsAlign(nextTodo.subagent, subagent) && prompt.length > 20) {
195
364
  return { valid: true, expectedTodoId: nextTodo.id };
196
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
+ }
197
381
  return {
198
382
  valid: false,
199
- reason: `Spawn must target plan todo ${nextTodo.id} before other work`,
200
- 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',
201
387
  expectedTodoId: nextTodo.id,
202
- hint: {
203
- tool: 'Task',
204
- subagent_type: nextTodo.subagent,
205
- planTodoId: nextTodo.id,
206
- description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. ` +
207
- `Include plan todo id in Task prompt.`,
208
- },
388
+ hint,
209
389
  };
210
390
  }
211
- export function markTodoInProgressOnSpawn(toolInput, projectRoot = process.cwd()) {
212
- const plan = loadPersistedLeadDevPlan(projectRoot);
213
- 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)
214
396
  return null;
215
- const validation = validateSpawnMatchesTodo(toolInput, projectRoot);
397
+ const validation = validateSpawnMatchesTodo(toolInput, projectRoot, undefined, sessionId);
216
398
  if (!validation.valid || !validation.expectedTodoId)
217
399
  return null;
218
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
+ }
@@ -74,11 +74,27 @@ export function hasValidSynthesisConsultReceipt(todoId, projectRoot = process.cw
74
74
  }
75
75
  export function parseConsultVerdictFromText(text) {
76
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
+ }
77
93
  if (/\bCONDITIONAL(\s+PASS)?\b/.test(normalized))
78
94
  return 'CONDITIONAL';
79
- if (/\b(?:PASS|SHIP)\b/.test(normalized))
95
+ if (/\b(?:PASS|SHIP|APPROVE)\b/.test(normalized))
80
96
  return 'PASS';
81
- if (/\bFAIL\b/.test(normalized))
97
+ if (/\b(?:FAIL|REJECT)\b/.test(normalized))
82
98
  return 'FAIL';
83
99
  return null;
84
100
  }
@@ -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;