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.
- 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 +44 -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 +21 -0
- 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/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 +4 -0
- package/dist/nucleus/delegation-gate.js +90 -11
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
- package/dist/nucleus/lead-dev-plan-persistence.js +201 -19
- 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.js +18 -2
- 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/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,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
|
|
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
|
|
185
|
-
if (
|
|
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:
|
|
200
|
-
|
|
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
|
|
213
|
-
|
|
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 (/\
|
|
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:**
|
|
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;
|