0xray 3.4.10 → 3.5.0

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.
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Pending delegations SSOT — written by task-handler on delegate-deferred.
3
+ * Grok hooks read the same file; PostToolUse never re-parses MCP output.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { featuresConfigLoader } from '../core/features-config.js';
8
+ import { updatePlanTodoStatus } from './lead-dev-plan-persistence.js';
9
+ export const DEFAULT_PENDING_TTL_MS = 4 * 60 * 60 * 1000;
10
+ const AGENT_ALIASES = {
11
+ 'bug-triage-specialist': 'bug-triage',
12
+ 'code-reviewer': 'code-review',
13
+ 'security-auditor': 'security-audit',
14
+ };
15
+ const TASK_TYPE_BY_AGENT = {
16
+ 'backend-engineer': 'backend-engineer',
17
+ 'frontend-engineer': 'frontend-engineer',
18
+ 'bug-triage': 'bug-triage',
19
+ 'bug-triage-specialist': 'bug-triage',
20
+ strategist: 'strategist',
21
+ researcher: 'generalPurpose',
22
+ 'architect-tools': 'generalPurpose',
23
+ 'code-review': 'code-review',
24
+ 'code-reviewer': 'code-review',
25
+ };
26
+ export function pendingDelegationsPath(projectRoot = process.cwd()) {
27
+ return path.join(projectRoot, '.xray', 'state', 'pending-delegations.json');
28
+ }
29
+ export function leadDevPlanPath(projectRoot = process.cwd()) {
30
+ return path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
31
+ }
32
+ export function isAutoChainDelegationsEnabled(projectRoot = process.cwd()) {
33
+ try {
34
+ const localPath = path.join(projectRoot, '.xray', 'features.json');
35
+ if (fs.existsSync(localPath)) {
36
+ const data = JSON.parse(fs.readFileSync(localPath, 'utf8'));
37
+ const cfg = data.multi_agent_orchestration ?? {};
38
+ if (cfg.enabled === false || cfg.lead_dev_mode === false)
39
+ return false;
40
+ return cfg.auto_chain_delegations !== false;
41
+ }
42
+ const cfg = featuresConfigLoader.loadConfig().multi_agent_orchestration;
43
+ if (!cfg.enabled || cfg.lead_dev_mode === false)
44
+ return false;
45
+ return cfg.auto_chain_delegations !== false;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function normalizeAgent(agent) {
52
+ const key = agent.toLowerCase().trim();
53
+ return AGENT_ALIASES[key] ?? key;
54
+ }
55
+ function loadLeadDevPlan(projectRoot) {
56
+ const planPath = leadDevPlanPath(projectRoot);
57
+ if (!fs.existsSync(planPath))
58
+ return null;
59
+ try {
60
+ return JSON.parse(fs.readFileSync(planPath, 'utf8'));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function allPlanTodos(plan) {
67
+ return plan.phases.flatMap((p) => p.todos);
68
+ }
69
+ export function matchPlanTodo(agent, taskDescription, taskId, plan) {
70
+ if (!plan) {
71
+ return { todo: null, method: 'none', confidence: 0 };
72
+ }
73
+ const normalizedAgent = normalizeAgent(agent);
74
+ const pendingTodos = allPlanTodos(plan).filter((t) => t.status === 'pending');
75
+ const byTaskId = pendingTodos.find((t) => t.id === taskId || t.task.includes(taskId));
76
+ if (byTaskId) {
77
+ return { todo: byTaskId, method: 'taskId', confidence: 0.95 };
78
+ }
79
+ const desc = taskDescription.toLowerCase().trim();
80
+ if (desc) {
81
+ const byDesc = pendingTodos.find((t) => {
82
+ const todoText = t.task.toLowerCase();
83
+ return todoText.includes(desc) || desc.includes(todoText.slice(0, 40));
84
+ });
85
+ if (byDesc) {
86
+ return { todo: byDesc, method: 'description', confidence: 0.75 };
87
+ }
88
+ }
89
+ const byAgent = pendingTodos.find((t) => normalizeAgent(t.subagent) === normalizedAgent);
90
+ if (byAgent) {
91
+ return { todo: byAgent, method: 'agent-only', confidence: 0.5 };
92
+ }
93
+ return { todo: null, method: 'none', confidence: 0 };
94
+ }
95
+ export function mapSubagentToTaskType(agent) {
96
+ const key = agent.toLowerCase().trim();
97
+ return TASK_TYPE_BY_AGENT[key] ?? TASK_TYPE_BY_AGENT[normalizeAgent(key)] ?? 'generalPurpose';
98
+ }
99
+ export function buildSpawnHint(delegation) {
100
+ const todoLine = delegation.planTodoTask
101
+ ? `[plan todo ${delegation.planTodoId}] ${delegation.planTodoTask}`
102
+ : delegation.taskDescription;
103
+ return {
104
+ tool: 'Task',
105
+ subagent_type: mapSubagentToTaskType(delegation.agent),
106
+ description: `Lead-dev implementation delegation. ${todoLine}. ` +
107
+ `Complete this todo per .xray/state/lead-dev-plan.json; run per-suite tests; report results.`,
108
+ planTodoId: delegation.planTodoId,
109
+ delegationId: delegation.id,
110
+ };
111
+ }
112
+ export function loadPendingDelegationsState(projectRoot = process.cwd()) {
113
+ const filePath = pendingDelegationsPath(projectRoot);
114
+ if (!fs.existsSync(filePath))
115
+ return null;
116
+ try {
117
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ export function savePendingDelegationsState(state, projectRoot = process.cwd()) {
124
+ const filePath = pendingDelegationsPath(projectRoot);
125
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
126
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
127
+ return filePath;
128
+ }
129
+ function newDelegationId() {
130
+ return `del-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
131
+ }
132
+ export function recordDeferredDelegations(sessionId, deferrals, projectRoot = process.cwd()) {
133
+ if (!isAutoChainDelegationsEnabled(projectRoot) || deferrals.length === 0) {
134
+ return [];
135
+ }
136
+ const plan = loadLeadDevPlan(projectRoot);
137
+ const existing = loadPendingDelegationsState(projectRoot);
138
+ const now = new Date().toISOString();
139
+ const base = existing && existing.sessionId === sessionId
140
+ ? existing
141
+ : {
142
+ sessionId,
143
+ createdAt: now,
144
+ ttlMs: DEFAULT_PENDING_TTL_MS,
145
+ delegations: [],
146
+ };
147
+ const created = [];
148
+ for (const d of deferrals) {
149
+ const { todo, method, confidence } = matchPlanTodo(d.agent, d.description, d.taskId, plan);
150
+ const partial = {
151
+ id: newDelegationId(),
152
+ taskId: d.taskId,
153
+ agent: d.agent,
154
+ taskDescription: d.description,
155
+ taskType: d.type ?? 'implement',
156
+ sessionId,
157
+ planTodoId: todo?.id ?? null,
158
+ planTodoTask: todo?.task ?? null,
159
+ matchMethod: method,
160
+ matchConfidence: confidence,
161
+ status: 'pending',
162
+ createdAt: now,
163
+ satisfiedAt: null,
164
+ };
165
+ created.push({
166
+ ...partial,
167
+ spawnHint: buildSpawnHint(partial),
168
+ });
169
+ }
170
+ base.delegations = [
171
+ ...base.delegations.filter((d) => d.status === 'pending'),
172
+ ...created,
173
+ ];
174
+ if (base.sessionId !== sessionId) {
175
+ base.sessionId = sessionId;
176
+ base.createdAt = now;
177
+ base.delegations = created;
178
+ }
179
+ savePendingDelegationsState(base, projectRoot);
180
+ return created;
181
+ }
182
+ export function isPendingStateActive(state, sessionId) {
183
+ if (!state)
184
+ return false;
185
+ if (!sessionId || state.sessionId !== sessionId)
186
+ return false;
187
+ const age = Date.now() - new Date(state.createdAt).getTime();
188
+ if (age > state.ttlMs)
189
+ return false;
190
+ return state.delegations.some((d) => d.status === 'pending');
191
+ }
192
+ export function getActivePendingDelegations(sessionId, projectRoot = process.cwd()) {
193
+ const state = loadPendingDelegationsState(projectRoot);
194
+ if (!isPendingStateActive(state, sessionId))
195
+ return [];
196
+ return state.delegations.filter((d) => d.status === 'pending');
197
+ }
198
+ export function satisfyDelegation(input, projectRoot = process.cwd()) {
199
+ const state = loadPendingDelegationsState(projectRoot);
200
+ if (!state)
201
+ return { satisfied: [], clearedAll: false };
202
+ const pending = state.delegations.filter((d) => d.status === 'pending');
203
+ if (pending.length === 0)
204
+ return { satisfied: [], clearedAll: false };
205
+ const prompt = (input.toolPrompt ?? '').toLowerCase();
206
+ let matches = [];
207
+ if (input.delegationId) {
208
+ matches = pending.filter((d) => d.id === input.delegationId);
209
+ }
210
+ else if (input.planTodoId) {
211
+ matches = pending.filter((d) => d.planTodoId === input.planTodoId || prompt.includes(input.planTodoId.toLowerCase()));
212
+ }
213
+ else if (input.taskId) {
214
+ matches = pending.filter((d) => d.taskId === input.taskId || prompt.includes(input.taskId.toLowerCase()));
215
+ }
216
+ else if (input.agent) {
217
+ const norm = normalizeAgent(input.agent);
218
+ matches = pending.filter((d) => normalizeAgent(d.agent) === norm);
219
+ }
220
+ if (matches.length === 0 && prompt) {
221
+ for (const d of pending) {
222
+ if (d.planTodoId && prompt.includes(d.planTodoId.toLowerCase())) {
223
+ matches.push(d);
224
+ break;
225
+ }
226
+ if (prompt.includes(d.taskId.toLowerCase())) {
227
+ matches.push(d);
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ if (matches.length === 0 && pending.length === 1) {
233
+ const only = pending[0];
234
+ const hint = only.spawnHint.description.toLowerCase();
235
+ if (prompt && hint && prompt.includes(hint.slice(0, 40))) {
236
+ matches = [only];
237
+ }
238
+ }
239
+ if (matches.length === 0) {
240
+ return { satisfied: [], clearedAll: false };
241
+ }
242
+ const now = new Date().toISOString();
243
+ const matchIds = new Set(matches.map((m) => m.id));
244
+ for (const d of state.delegations) {
245
+ if (matchIds.has(d.id) && d.status === 'pending') {
246
+ d.status = 'satisfied';
247
+ d.satisfiedAt = now;
248
+ }
249
+ }
250
+ savePendingDelegationsState(state, projectRoot);
251
+ for (const m of matches) {
252
+ if (m.planTodoId) {
253
+ updatePlanTodoStatus(m.planTodoId, 'in_progress', projectRoot);
254
+ }
255
+ }
256
+ const stillPending = state.delegations.some((d) => d.status === 'pending');
257
+ return { satisfied: matches, clearedAll: !stillPending };
258
+ }
259
+ export function clearPendingDelegations(projectRoot = process.cwd()) {
260
+ const filePath = pendingDelegationsPath(projectRoot);
261
+ if (!fs.existsSync(filePath))
262
+ return false;
263
+ fs.unlinkSync(filePath);
264
+ return true;
265
+ }
266
+ export function clearPendingDelegationsForSessionChange(newSessionId, projectRoot = process.cwd()) {
267
+ const state = loadPendingDelegationsState(projectRoot);
268
+ if (!state)
269
+ return false;
270
+ if (newSessionId && state.sessionId === newSessionId)
271
+ return false;
272
+ fs.unlinkSync(pendingDelegationsPath(projectRoot));
273
+ return true;
274
+ }
275
+ export function delegationSummaryForResponse(delegations) {
276
+ return delegations.map((d) => ({
277
+ id: d.id,
278
+ taskId: d.taskId,
279
+ agent: d.agent,
280
+ planTodoId: d.planTodoId,
281
+ matchMethod: d.matchMethod,
282
+ matchConfidence: d.matchConfidence,
283
+ spawnHint: d.spawnHint,
284
+ }));
285
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "3.4.10",
3
+ "version": "3.5.0",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -126,7 +126,8 @@
126
126
  "safe-publish": "npm run prepare-consumer && npm run build",
127
127
  "publish": "npm run release:gate && npm run safe-publish && npm publish --access public",
128
128
  "release:patch": "node scripts/node/release.mjs patch",
129
- "verify:orchestrator-behavior": "npm run build && node scripts/mjs/verify-orchestrator-behavior.mjs"
129
+ "verify:orchestrator-behavior": "npm run build && node scripts/mjs/verify-orchestrator-behavior.mjs",
130
+ "verify:grok-delegation-gate": "npm run build && node scripts/mjs/verify-grok-delegation-gate.mjs"
130
131
  },
131
132
  "files": [
132
133
  "dist/",
@@ -139,6 +140,7 @@
139
140
  "scripts/mjs/validate-mcp-connectivity.cjs",
140
141
  "scripts/mjs/validate-postinstall-config.mjs",
141
142
  "scripts/mjs/verify-orchestrator-behavior.mjs",
143
+ "scripts/mjs/verify-grok-delegation-gate.mjs",
142
144
  "src/opencode/",
143
145
  "src/opencode/codex.codex",
144
146
  "src/opencode/commands/",
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hook fixture verify — PreToolUse delegation gate with seeded pending-delegations.json.
4
+ * Run: node scripts/mjs/verify-grok-delegation-gate.mjs
5
+ */
6
+ import { execSync } from 'node:child_process';
7
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const packageRoot = join(__dirname, '../..');
14
+ const preToolHook = join(packageRoot, 'dist/integrations/grok/hooks/pre-tool-use.js');
15
+
16
+ let failed = 0;
17
+ function pass(n) {
18
+ console.log(`✅ ${n}`);
19
+ }
20
+ function fail(n, d = '') {
21
+ failed++;
22
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
23
+ }
24
+
25
+ function runHook(fixture, env = {}) {
26
+ const payload = JSON.stringify(fixture);
27
+ const out = execSync(`printf '%s' '${payload.replace(/'/g, "'\\''")}' | node "${preToolHook}"`, {
28
+ encoding: 'utf8',
29
+ env: { ...process.env, ...env },
30
+ });
31
+ return JSON.parse(out.trim());
32
+ }
33
+
34
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-gate-verify-'));
35
+ mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
36
+ writeFileSync(
37
+ join(tmp, '.xray', 'features.json'),
38
+ JSON.stringify({
39
+ multi_agent_orchestration: {
40
+ enabled: true,
41
+ lead_dev_mode: true,
42
+ auto_chain_delegations: true,
43
+ },
44
+ }),
45
+ );
46
+ writeFileSync(
47
+ join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
48
+ JSON.stringify({ active: true }),
49
+ );
50
+ writeFileSync(
51
+ join(tmp, '.xray', 'state', 'pending-delegations.json'),
52
+ JSON.stringify({
53
+ sessionId: 'verify-gate',
54
+ createdAt: new Date().toISOString(),
55
+ ttlMs: 14400000,
56
+ delegations: [
57
+ {
58
+ id: 'del-verify',
59
+ taskId: 'impl-1',
60
+ agent: 'backend-engineer',
61
+ taskDescription: 'implement',
62
+ taskType: 'implement',
63
+ sessionId: 'verify-gate',
64
+ planTodoId: '2.1',
65
+ planTodoTask: 'swap deps',
66
+ matchMethod: 'agent-only',
67
+ matchConfidence: 0.5,
68
+ status: 'pending',
69
+ createdAt: new Date().toISOString(),
70
+ satisfiedAt: null,
71
+ spawnHint: {
72
+ tool: 'Task',
73
+ subagent_type: 'backend-engineer',
74
+ description: 'Lead-dev delegation 2.1',
75
+ planTodoId: '2.1',
76
+ delegationId: 'del-verify',
77
+ },
78
+ },
79
+ ],
80
+ }),
81
+ );
82
+
83
+ const baseEnv = {
84
+ GROK_WORKSPACE_ROOT: tmp,
85
+ GROK_SESSION_ID: 'verify-gate',
86
+ };
87
+
88
+ try {
89
+ const deny = runHook(
90
+ {
91
+ toolName: 'search_replace',
92
+ workspaceRoot: tmp,
93
+ sessionId: 'verify-gate',
94
+ toolInput: { path: 'src/a.ts', new_string: 'x' },
95
+ },
96
+ baseEnv,
97
+ );
98
+ if (deny.decision === 'deny' && deny.gate === 'auto-chain-pending') {
99
+ pass('PreToolUse denies write while pending');
100
+ } else {
101
+ fail('write deny', JSON.stringify(deny));
102
+ }
103
+
104
+ const allowRead = runHook(
105
+ {
106
+ toolName: 'read_file',
107
+ workspaceRoot: tmp,
108
+ sessionId: 'verify-gate',
109
+ toolInput: { path: 'src/a.ts' },
110
+ },
111
+ baseEnv,
112
+ );
113
+ if (allowRead.decision === 'allow') pass('PreToolUse allows read while pending');
114
+ else fail('read allow', JSON.stringify(allowRead));
115
+
116
+ const allowTask = runHook(
117
+ {
118
+ toolName: 'Task',
119
+ workspaceRoot: tmp,
120
+ sessionId: 'verify-gate',
121
+ toolInput: { prompt: 'plan todo 2.1 swap deps', subagent_type: 'backend-engineer' },
122
+ },
123
+ baseEnv,
124
+ );
125
+ if (allowTask.decision === 'allow') pass('PreToolUse allows Task while pending');
126
+ else fail('Task allow', JSON.stringify(allowTask));
127
+
128
+ writeFileSync(
129
+ join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
130
+ JSON.stringify({
131
+ active: true,
132
+ persistedAt: new Date().toISOString(),
133
+ phases: [
134
+ {
135
+ id: 'phase-2',
136
+ todos: [
137
+ {
138
+ id: '2.1',
139
+ task: 'swap deps',
140
+ subagent: 'backend-engineer',
141
+ status: 'pending',
142
+ },
143
+ ],
144
+ },
145
+ ],
146
+ }),
147
+ );
148
+
149
+ const denyWrongTodo = runHook(
150
+ {
151
+ toolName: 'Task',
152
+ workspaceRoot: tmp,
153
+ sessionId: 'verify-gate',
154
+ toolInput: { prompt: 'explore only', subagent_type: 'explore' },
155
+ },
156
+ baseEnv,
157
+ );
158
+ if (denyWrongTodo.decision === 'deny' && denyWrongTodo.gate === 'spawn-todo-persistence') {
159
+ pass('PreToolUse denies spawn skipping required plan todo');
160
+ } else {
161
+ fail('spawn todo persistence', JSON.stringify(denyWrongTodo));
162
+ }
163
+ } finally {
164
+ rmSync(tmp, { recursive: true, force: true });
165
+ }
166
+
167
+ console.log(
168
+ '\n' +
169
+ (failed === 0
170
+ ? '🎉 Grok delegation gate verify passed (4/4).'
171
+ : `⚠️ ${failed} gate check(s) failed.`),
172
+ );
173
+ process.exit(failed === 0 ? 0 : 1);
@@ -3,6 +3,7 @@
3
3
  * Consumer behavior verify — orchestrator honesty (ships in 0xray tarball).
4
4
  * Run: node node_modules/0xray/scripts/mjs/verify-orchestrator-behavior.mjs
5
5
  */
6
+ import { existsSync, readFileSync, unlinkSync } from 'fs';
6
7
  import { dirname, join } from 'path';
7
8
  import { fileURLToPath, pathToFileURL } from 'url';
8
9
 
@@ -113,10 +114,55 @@ if (text.includes('Deferred') && text.includes('❌ FAILED')) {
113
114
  fail('defer honesty', text.slice(0, 200));
114
115
  }
115
116
 
117
+ // 6. delegations[] block in response
118
+ if (text.includes('delegations') && text.includes('spawnHint')) {
119
+ pass('orchestrate-task includes delegations[] JSON block');
120
+ } else {
121
+ fail('delegations block', 'missing JSON delegations in response');
122
+ }
123
+
124
+ // 7. pending-delegations.json written on defer (consumer cwd, not package root)
125
+ const pendingPath = join(process.cwd(), '.xray', 'state', 'pending-delegations.json');
126
+ if (existsSync(pendingPath)) {
127
+ try {
128
+ const pending = JSON.parse(readFileSync(pendingPath, 'utf8'));
129
+ if (pending.delegations?.some((d) => d.status === 'pending' && d.agent === 'backend-engineer')) {
130
+ pass('pending-delegations.json recorded on defer');
131
+ } else {
132
+ fail('pending file content', 'no pending backend-engineer delegation');
133
+ }
134
+ } catch (e) {
135
+ fail('pending file parse', e.message);
136
+ }
137
+ } else {
138
+ fail('pending-delegations.json', 'file missing after defer');
139
+ }
140
+
141
+ // 8. spawnHint includes Task tool
142
+ try {
143
+ const match = text.match(/```json\n([\s\S]*?)\n```/);
144
+ const block = match ? JSON.parse(match[1]) : null;
145
+ const hint = block?.delegations?.[0]?.spawnHint;
146
+ if (hint?.tool === 'Task' && hint?.subagent_type) {
147
+ pass('delegations spawnHint routes to host Task');
148
+ } else {
149
+ fail('spawnHint', JSON.stringify(hint));
150
+ }
151
+ } catch (e) {
152
+ fail('spawnHint parse', e.message);
153
+ }
154
+
155
+ try {
156
+ if (existsSync(pendingPath)) unlinkSync(pendingPath);
157
+ } catch {
158
+ /* cleanup best-effort */
159
+ }
160
+
161
+ const total = 8;
116
162
  console.log(
117
163
  '\n' +
118
164
  (failed === 0
119
- ? '🎉 Orchestrator behavior verify passed (5/5).'
165
+ ? `🎉 Orchestrator behavior verify passed (${total}/${total}).`
120
166
  : `⚠️ ${failed} behavior check(s) failed.`),
121
167
  );
122
168
  process.exit(failed === 0 ? 0 : 1);
@@ -42,6 +42,21 @@
42
42
  }
43
43
  ]
44
44
  }
45
+ ],
46
+ "PostToolUse": [
47
+ {
48
+ "matcher": ".*",
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "node",
53
+ "args": [
54
+ "${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/post-tool-use.js"
55
+ ],
56
+ "timeout": 15000
57
+ }
58
+ ]
59
+ }
45
60
  ]
46
61
  }
47
62
  }
@@ -10,6 +10,8 @@ import {
10
10
  isLeadDevModeActive,
11
11
  persistLeadDevPlan,
12
12
  } from '../../../nucleus/autonomy-kernel.js';
13
+ import { bindPlanToSession } from '../../../nucleus/lead-dev-plan-persistence.js';
14
+ import { clearPendingDelegations } from '../../../nucleus/pending-delegations.js';
13
15
  import { getExecutionPlanner } from '../execution/execution-planner.js';
14
16
  import { addObservations, extractComplexityObservations } from '../aside-context.js';
15
17
  import type { OrchestrationTask, ComplexityAnalysis } from '../types.js';
@@ -65,7 +67,7 @@ export class ComplexityHandler {
65
67
  * Handle analyze-complexity request
66
68
  */
67
69
  async handleAnalyzeComplexity(
68
- args: { tasks: AnalyzeComplexityInput[] },
70
+ args: { tasks: AnalyzeComplexityInput[]; sessionId?: string },
69
71
  asideId?: string,
70
72
  ): Promise<{ content: Array<{ type: string; text: string }> }> {
71
73
  const tasks = normalizeAnalyzeTasks(args.tasks ?? []);
@@ -102,13 +104,18 @@ export class ComplexityHandler {
102
104
 
103
105
  if (leadDevPlan) {
104
106
  try {
107
+ const sessionId = args.sessionId ?? `analyze-${Date.now()}`;
108
+ clearPendingDelegations();
105
109
  const planPath = persistLeadDevPlan(leadDevPlan);
110
+ const bound = bindPlanToSession(sessionId);
106
111
  await frameworkLogger.log(
107
112
  'orchestrator.server',
108
113
  'lead-dev-plan-persisted',
109
114
  'info',
110
115
  {
111
116
  planPath,
117
+ sessionId,
118
+ planGeneration: bound?.planGeneration,
112
119
  phaseCount: leadDevPlan.phases.length,
113
120
  todoCount: leadDevPlan.phases.reduce((n, p) => n + p.todos.length, 0),
114
121
  },