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.
- package/.xray/features.json +5 -1
- package/.xray/features.schema.json +56 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +11 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +11 -1
- package/dist/CHANGELOG.md +54 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/cli/commands/hermes-install.js +3 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/features-config.d.ts +15 -0
- package/dist/governance/governance-service.d.ts +1 -0
- package/dist/governance/governance-service.js +81 -6
- package/dist/integrations/enforcement-gate.js +15 -5
- package/dist/integrations/grok/grok-cli.js +1 -10
- package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
- package/dist/integrations/grok/hooks/post-tool-use.js +48 -14
- package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
- package/dist/integrations/grok/hooks/session-start.js +28 -0
- package/dist/integrations/hermes-agent/__init__.py +11 -1
- package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/bridge.mjs +73 -10
- package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
- package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
- package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
- package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
- package/dist/mcps/architect-tools.server.js +6 -1
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
- package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
- package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
- package/dist/mcps/orchestrator/server.js +54 -5
- package/dist/memory-routing/provider-loader.js +30 -2
- package/dist/nucleus/aside-worktree.d.ts +41 -0
- package/dist/nucleus/aside-worktree.js +164 -0
- package/dist/nucleus/confer.d.ts +57 -0
- package/dist/nucleus/confer.js +308 -0
- package/dist/nucleus/delegation-gate.d.ts +12 -1
- package/dist/nucleus/delegation-gate.js +113 -13
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
- package/dist/nucleus/lead-dev-plan-persistence.js +222 -18
- package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
- package/dist/nucleus/spawn-plan-resolution.js +42 -0
- package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
- package/dist/nucleus/synthesis-consult-receipt.js +171 -0
- package/dist/nucleus/synthesis-context.js +1 -1
- package/dist/nucleus/user-aside.d.ts +70 -0
- package/dist/nucleus/user-aside.js +393 -0
- package/dist/plugin/xray-codex-injection.js +1 -1
- package/dist/postprocessor/PostProcessor.d.ts +1 -0
- package/dist/postprocessor/PostProcessor.js +11 -0
- package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
- package/dist/processors/implementations/session-summary-processor.js +9 -6
- package/package.json +14 -5
- package/scripts/hooks/install-hooks.cjs +3 -3
- package/scripts/hooks/run-hook.js +171 -86
- package/scripts/mjs/verify-confer-core.mjs +103 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
- package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
- package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
- package/scripts/mjs/verify-user-aside-core.mjs +137 -0
- package/scripts/node/bridge-mcp-wiring.cjs +10 -0
- package/scripts/node/install-bridges.cjs +3 -11
- package/scripts/node/load-reflection-config.mjs +2 -2
- package/scripts/node/release-gate.mjs +8 -4
- package/src/mcps/architect-tools.server.ts +6 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
- package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
- package/src/mcps/orchestrator/server.ts +56 -5
- package/xray/features.json +5 -1
- 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
|
|
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
|
|
163
|
-
if (
|
|
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:
|
|
178
|
-
|
|
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
|
|
191
|
-
|
|
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:**
|
|
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;
|