0xray 3.4.9 → 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.
Files changed (36) hide show
  1. package/.xray/features.json +2 -1
  2. package/AGENTS-consumer.md +1 -1
  3. package/AGENTS.md +1 -1
  4. package/README.md +1 -1
  5. package/SKILLS.md +1 -1
  6. package/dist/AGENTS.md +1 -1
  7. package/dist/CHANGELOG.md +32 -0
  8. package/dist/README.md +1 -1
  9. package/dist/SKILLS.md +1 -1
  10. package/dist/core/features-config.d.ts +2 -0
  11. package/dist/integrations/grok/hooks/grok-hook-utils.js +390 -16
  12. package/dist/integrations/grok/hooks/post-tool-use.js +62 -0
  13. package/dist/integrations/grok/hooks/pre-tool-use.js +43 -5
  14. package/dist/integrations/grok/hooks/session-start.js +8 -0
  15. package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
  16. package/dist/mcps/orchestrator/config/agent-capabilities.d.ts +1 -1
  17. package/dist/mcps/orchestrator/config/agent-capabilities.js +23 -1
  18. package/dist/mcps/orchestrator/execution/execution-planner.js +2 -2
  19. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +1 -0
  20. package/dist/mcps/orchestrator/handlers/complexity-handler.js +8 -1
  21. package/dist/mcps/orchestrator/handlers/task-handler.js +20 -3
  22. package/dist/nucleus/autonomy-kernel.d.ts +1 -1
  23. package/dist/nucleus/autonomy-kernel.js +5 -2
  24. package/dist/nucleus/lead-dev-plan-persistence.d.ts +43 -0
  25. package/dist/nucleus/lead-dev-plan-persistence.js +155 -0
  26. package/dist/nucleus/pending-delegations.d.ts +71 -0
  27. package/dist/nucleus/pending-delegations.js +285 -0
  28. package/package.json +6 -2
  29. package/scripts/mjs/verify-grok-delegation-gate.mjs +173 -0
  30. package/scripts/mjs/verify-orchestrator-behavior.mjs +168 -0
  31. package/src/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
  32. package/src/mcps/orchestrator/config/agent-capabilities.ts +26 -0
  33. package/src/mcps/orchestrator/execution/execution-planner.ts +2 -0
  34. package/src/mcps/orchestrator/handlers/complexity-handler.ts +9 -1
  35. package/src/mcps/orchestrator/handlers/task-handler.ts +53 -3
  36. package/xray/features.json +2 -1
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  checkCodexPatterns,
9
9
  checkFullTestSuite,
10
+ checkPendingDelegationGate,
10
11
  checkSubagentGate,
11
12
  checkSurfaceArea,
12
13
  ensureSessionBoot,
@@ -14,18 +15,20 @@ import {
14
15
  isWriteTool,
15
16
  loadFeatures,
16
17
  readStdinJson,
18
+ resolveSessionId,
17
19
  workspaceRoot,
18
20
  } from './grok-hook-utils.js';
19
21
  import { appendHookActivity } from './grok-hook-activity.js';
20
22
 
21
- function finish(root, decision, reason, hint, toolName) {
22
- const out = { decision };
23
+ function finish(root, decision, reason, hint, toolName, extra = {}) {
24
+ const out = { decision, ...extra };
23
25
  if (reason) out.reason = reason;
24
26
  if (hint) out.hint = hint;
25
27
  console.log(JSON.stringify(out));
26
28
  appendHookActivity(root, 'grok-pre-tool-use', decision, decision === 'deny' ? 'error' : 'info', {
27
29
  tool: toolName,
28
30
  reason: reason || hint || null,
31
+ gate: extra.gate || null,
29
32
  livePath: true,
30
33
  });
31
34
  process.exit(0);
@@ -43,10 +46,44 @@ async function main() {
43
46
  const features = loadFeatures();
44
47
  const ctx = extractFromEvent(event);
45
48
  toolName = ctx.toolName;
46
- const { paths, content, cmd } = ctx;
49
+ const { paths, content, cmd, toolInput } = ctx;
50
+ const sessionId = resolveSessionId(event);
51
+
52
+ const delegationBlock = checkPendingDelegationGate(
53
+ toolName,
54
+ toolInput,
55
+ features,
56
+ eventRoot,
57
+ sessionId,
58
+ );
59
+ if (delegationBlock) {
60
+ finish(
61
+ eventRoot,
62
+ 'deny',
63
+ delegationBlock.reason,
64
+ delegationBlock.hint,
65
+ toolName,
66
+ { gate: delegationBlock.gate },
67
+ );
68
+ }
47
69
 
48
- const subagentBlock = checkSubagentGate(toolName, features);
49
- if (subagentBlock) finish(eventRoot, 'deny', subagentBlock, null, toolName);
70
+ const subagentBlock = checkSubagentGate(
71
+ toolName,
72
+ features,
73
+ eventRoot,
74
+ sessionId,
75
+ toolInput,
76
+ );
77
+ if (subagentBlock) {
78
+ finish(
79
+ eventRoot,
80
+ 'deny',
81
+ subagentBlock.reason || subagentBlock,
82
+ subagentBlock.hint || null,
83
+ toolName,
84
+ { gate: subagentBlock.gate || 'spawn-gate' },
85
+ );
86
+ }
50
87
 
51
88
  if (features.no_new_surface && isWriteTool(toolName) && paths.length) {
52
89
  const surfaceBlock = checkSurfaceArea(paths, eventRoot);
@@ -95,6 +132,7 @@ function extractFromEvent(event) {
95
132
 
96
133
  return {
97
134
  toolName,
135
+ toolInput,
98
136
  paths,
99
137
  content,
100
138
  cmd: String(toolInput.command || ''),
@@ -6,8 +6,10 @@
6
6
 
7
7
  import {
8
8
  buildSessionBootPayload,
9
+ clearPendingDelegationsForSessionChange,
9
10
  ensureSessionBoot,
10
11
  readStdinJson,
12
+ resolveSessionId,
11
13
  workspaceRoot,
12
14
  writeSessionBoot,
13
15
  } from './grok-hook-utils.js';
@@ -20,6 +22,12 @@ async function main() {
20
22
  try {
21
23
  const event = await readStdinJson();
22
24
  const eventRoot = event.workspaceRoot || event.cwd || root;
25
+ const sessionId = resolveSessionId(event);
26
+ if (clearPendingDelegationsForSessionChange(sessionId, eventRoot)) {
27
+ appendHookActivity(eventRoot, 'grok-session-start', 'stale-pending-cleared', 'info', {
28
+ sessionId,
29
+ });
30
+ }
23
31
  const source =
24
32
  HOOK_EVENT === 'user_prompt_submit'
25
33
  ? '0xray/grok-user-prompt-submit'
@@ -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
  }
@@ -29,7 +29,7 @@ export declare class AgentCapabilitiesManager {
29
29
  /**
30
30
  * Select the best agent for a task based on complexity, capabilities, and memory routing.
31
31
  */
32
- selectAgentForTask(requiredCapabilities: string[], complexity: number, operationDescription?: string): string | null;
32
+ selectAgentForTask(requiredCapabilities: string[], complexity: number, operationDescription?: string, taskType?: string): string | null;
33
33
  /**
34
34
  * Get all available agents for a given complexity level
35
35
  */
@@ -7,6 +7,22 @@
7
7
  import { AGENT_REGISTRY } from "../../../agents/registry.js";
8
8
  import { initializeMemoryRouting } from '../../../memory-routing/index.js';
9
9
  import { getProvider, toMemoryCapabilityMap, fromMemoryCapabilityMap, } from './memory-routing-bridge.js';
10
+ import { routeSubagent } from '../../../nucleus/autonomy-kernel.js';
11
+ /** routeSubagent names → AGENT_REGISTRY keys */
12
+ const ROUTE_AGENT_ALIASES = {
13
+ 'code-review': 'code-reviewer',
14
+ 'security-audit': 'security-auditor',
15
+ 'bug-triage': 'bug-triage-specialist',
16
+ 'architect-tools': 'architect',
17
+ };
18
+ function resolveRoutedAgent(routed, capabilities) {
19
+ const candidate = ROUTE_AGENT_ALIASES[routed] ?? routed;
20
+ if (capabilities.has(candidate))
21
+ return candidate;
22
+ if (capabilities.has(routed))
23
+ return routed;
24
+ return null;
25
+ }
10
26
  const DEFAULT_AGENT_CAPABILITIES = Object.fromEntries(Object.entries(AGENT_REGISTRY).map(([name, entry]) => [
11
27
  name,
12
28
  {
@@ -61,7 +77,13 @@ export class AgentCapabilitiesManager {
61
77
  /**
62
78
  * Select the best agent for a task based on complexity, capabilities, and memory routing.
63
79
  */
64
- selectAgentForTask(requiredCapabilities, complexity, operationDescription = '') {
80
+ selectAgentForTask(requiredCapabilities, complexity, operationDescription = '', taskType = '') {
81
+ if (taskType.trim()) {
82
+ const routed = routeSubagent(taskType);
83
+ const resolved = resolveRoutedAgent(routed, this.capabilities);
84
+ if (resolved)
85
+ return resolved;
86
+ }
65
87
  const provider = getProvider();
66
88
  if (provider.id !== 'null' && operationDescription) {
67
89
  const memoryCaps = toMemoryCapabilityMap(this.getAllCapabilities());
@@ -181,7 +181,7 @@ export class ExecutionPlanner {
181
181
  const operationDescription = task.metadata?.memoryHighConfidenceTrap
182
182
  ? `${task.description} TYPE: ontological-trap`
183
183
  : task.description;
184
- const agent = this.capabilitiesManager.selectAgentForTask(requiredCaps, complexity, operationDescription) || 'orchestrator';
184
+ const agent = this.capabilitiesManager.selectAgentForTask(requiredCaps, complexity, operationDescription, task.type) || 'orchestrator';
185
185
  if (!agentAssignments.has(agent)) {
186
186
  agentAssignments.set(agent, []);
187
187
  }
@@ -223,7 +223,7 @@ export class ExecutionPlanner {
223
223
  task.type,
224
224
  ...(task.metadata?.memorySignals ?? []),
225
225
  ];
226
- const agent = this.capabilitiesManager.selectAgentForTask(requiredCaps, complexity, task.description) || 'orchestrator';
226
+ const agent = this.capabilitiesManager.selectAgentForTask(requiredCaps, complexity, task.description, task.type) || 'orchestrator';
227
227
  if (!assignments.has(agent)) {
228
228
  assignments.set(agent, []);
229
229
  }
@@ -26,6 +26,7 @@ export declare class ComplexityHandler {
26
26
  */
27
27
  handleAnalyzeComplexity(args: {
28
28
  tasks: AnalyzeComplexityInput[];
29
+ sessionId?: string;
29
30
  }, asideId?: string): Promise<{
30
31
  content: Array<{
31
32
  type: string;
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { frameworkLogger } from '../../../core/framework-logger.js';
7
7
  import { buildLeadDevPlan, isLeadDevModeActive, persistLeadDevPlan, } from '../../../nucleus/autonomy-kernel.js';
8
+ import { bindPlanToSession } from '../../../nucleus/lead-dev-plan-persistence.js';
9
+ import { clearPendingDelegations } from '../../../nucleus/pending-delegations.js';
8
10
  import { getExecutionPlanner } from '../execution/execution-planner.js';
9
11
  import { addObservations, extractComplexityObservations } from '../aside-context.js';
10
12
  function normalizeAnalyzeTasks(raw) {
@@ -59,13 +61,18 @@ export class ComplexityHandler {
59
61
  .filter((t) => t.description?.trim())
60
62
  .map((t) => ({ description: t.description, type: t.type }));
61
63
  const leadDevPlan = isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
62
- ? buildLeadDevPlan(primaryDescription, taskTypes.length ? taskTypes : ['implement'], planTaskInputs)
64
+ ? buildLeadDevPlan(primaryDescription, taskTypes.length ? taskTypes : ['implement'], planTaskInputs, analysis.overallComplexity)
63
65
  : null;
64
66
  if (leadDevPlan) {
65
67
  try {
68
+ const sessionId = args.sessionId ?? `analyze-${Date.now()}`;
69
+ clearPendingDelegations();
66
70
  const planPath = persistLeadDevPlan(leadDevPlan);
71
+ const bound = bindPlanToSession(sessionId);
67
72
  await frameworkLogger.log('orchestrator.server', 'lead-dev-plan-persisted', 'info', {
68
73
  planPath,
74
+ sessionId,
75
+ planGeneration: bound?.planGeneration,
69
76
  phaseCount: leadDevPlan.phases.length,
70
77
  todoCount: leadDevPlan.phases.reduce((n, p) => n + p.todos.length, 0),
71
78
  });
@@ -8,6 +8,7 @@ import { getExecutionPlanner } from '../execution/execution-planner.js';
8
8
  import { mcpClientManager } from '../../../mcps/mcp-client.js';
9
9
  import { addObservations, extractOrchestrationObservations, spawnAside } from '../aside-context.js';
10
10
  import { getProvider } from '../config/memory-routing-bridge.js';
11
+ import { delegationSummaryForResponse, recordDeferredDelegations, } from '../../../nucleus/pending-delegations.js';
11
12
  /**
12
13
  * Task Handler
13
14
  * Processes orchestrate-task requests
@@ -31,6 +32,7 @@ export class TaskHandler {
31
32
  recommendations: [],
32
33
  };
33
34
  const startTime = Date.now();
35
+ let pendingDelegations = [];
34
36
  try {
35
37
  // Validate tasks
36
38
  const validation = this.planner.validateTasks(tasks);
@@ -50,6 +52,10 @@ export class TaskHandler {
50
52
  });
51
53
  }
52
54
  const results = await this.executePlan(executionPlan, sessionId, timeout);
55
+ if (results.deferredRecords.length > 0) {
56
+ pendingDelegations = recordDeferredDelegations(sessionId, results.deferredRecords);
57
+ await frameworkLogger.log('orchestrator.task-handler', 'pending-delegations-recorded', 'info', { count: pendingDelegations.length, sessionId });
58
+ }
53
59
  // Feed per-task outcomes back to memory provider (not aggregate session stats)
54
60
  if (memoryProvider.ingestFeedback) {
55
61
  const outcomeByKey = new Map(results.taskOutcomes.map((o) => [`${o.agent}:${o.taskId}`, o]));
@@ -100,7 +106,7 @@ export class TaskHandler {
100
106
  orchestrationResult.recommendations.push(`Orchestration error: ${error instanceof Error ? error.message : String(error)}`);
101
107
  }
102
108
  orchestrationResult.duration = Date.now() - startTime;
103
- const response = this.formatOrchestrationResponse(description, orchestrationResult, executionMode);
109
+ const response = this.formatOrchestrationResponse(description, orchestrationResult, executionMode, pendingDelegations);
104
110
  return { content: [{ type: 'text', text: response }] };
105
111
  }
106
112
  /**
@@ -114,6 +120,7 @@ export class TaskHandler {
114
120
  let failedTasks = 0;
115
121
  let deferredTasks = 0;
116
122
  const agentOutputs = {};
123
+ const deferredRecords = [];
117
124
  const taskOutcomes = [];
118
125
  const skillServers = new Set([
119
126
  'code-review', 'code-reviewer',
@@ -167,6 +174,12 @@ export class TaskHandler {
167
174
  else {
168
175
  for (const task of tasks) {
169
176
  deferredTasks++;
177
+ deferredRecords.push({
178
+ taskId: task.id,
179
+ agent,
180
+ description: task.description,
181
+ type: task.type,
182
+ });
170
183
  taskOutcomes.push({
171
184
  taskId: task.id,
172
185
  agent,
@@ -199,6 +212,7 @@ export class TaskHandler {
199
212
  bottlenecks,
200
213
  recommendations,
201
214
  agentOutputs,
215
+ deferredRecords,
202
216
  taskOutcomes,
203
217
  };
204
218
  }
@@ -232,7 +246,10 @@ export class TaskHandler {
232
246
  /**
233
247
  * Format orchestration response
234
248
  */
235
- formatOrchestrationResponse(description, result, executionMode) {
249
+ formatOrchestrationResponse(description, result, executionMode, pendingDelegations = []) {
250
+ const delegationsBlock = pendingDelegations.length > 0
251
+ ? `\n**Delegations (host Task required):**\n\`\`\`json\n${JSON.stringify({ delegations: delegationSummaryForResponse(pendingDelegations) }, null, 2)}\n\`\`\`\n`
252
+ : '';
236
253
  return `🎯 Orchestration Complete: "${description}"
237
254
 
238
255
  **Session ID:** ${result.sessionId}
@@ -257,6 +274,6 @@ ${result.recommendations.length > 0 ? result.recommendations.map((r) => `•
257
274
 
258
275
  ${result.agentOutputs && Object.keys(result.agentOutputs).length > 0
259
276
  ? `**Agent Outputs (real MCP responses):**\n${Object.entries(result.agentOutputs).map(([k, v]) => `• ${k}: ${v.substring(0, 200)}${v.length > 200 ? '...' : ''}`).join('\n')}\n`
260
- : ''}`;
277
+ : ''}${delegationsBlock}`;
261
278
  }
262
279
  }
@@ -41,7 +41,7 @@ export interface LeadDevPlan {
41
41
  export declare function isLeadDevModeActive(): boolean;
42
42
  export declare function routeSubagent(taskType: string): SubagentRoute;
43
43
  export declare function shouldFlagFullTestSuite(command: string): boolean;
44
- export declare function buildLeadDevPlan(description: string, taskTypes?: string[], taskInputs?: LeadDevPlanTaskInput[]): LeadDevPlan | null;
44
+ export declare function buildLeadDevPlan(description: string, taskTypes?: string[], taskInputs?: LeadDevPlanTaskInput[], mcpOverallComplexity?: number): LeadDevPlan | null;
45
45
  export declare function buildSessionBootContext(): Record<string, unknown>;
46
46
  /** Persist plan for PreToolUse spawn_subagent gate */
47
47
  export declare function persistLeadDevPlan(plan: LeadDevPlan, projectRoot?: string): string;
@@ -73,13 +73,16 @@ function buildImplementationTodos(description, taskTypes, taskInputs, idPrefix)
73
73
  status: 'pending',
74
74
  }));
75
75
  }
76
- export function buildLeadDevPlan(description, taskTypes = ['implement'], taskInputs = []) {
76
+ export function buildLeadDevPlan(description, taskTypes = ['implement'], taskInputs = [], mcpOverallComplexity) {
77
77
  if (!isLeadDevModeActive())
78
78
  return null;
79
79
  const cfg = orchestrationConfig();
80
80
  const threshold = cfg.phased_plan_threshold ?? 25;
81
81
  const score = scoreComplexity(description, { taskTypes });
82
- const complexity = score.score;
82
+ const mcpScore = mcpOverallComplexity !== undefined && Number.isFinite(mcpOverallComplexity)
83
+ ? Math.round(mcpOverallComplexity)
84
+ : 0;
85
+ const complexity = Math.max(score.score, mcpScore);
83
86
  const requiresPhasedPlan = complexity > threshold || taskInputs.length > 1;
84
87
  const mandatoryConsults = cfg.auto_consult_major_work !== false && requiresPhasedPlan
85
88
  ? [...MANDATORY_MAJOR_CONSULTS]
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Todo-level lead-dev plan persistence — spawn gate survives beyond 4h mtime
3
+ * while outstanding todos remain. SSOT for plan todo status sync.
4
+ */
5
+ import type { LeadDevPlan, LeadDevTodo } from './autonomy-kernel.js';
6
+ export interface PersistedLeadDevPlan extends LeadDevPlan {
7
+ persistedAt?: string;
8
+ sessionId?: string;
9
+ planGeneration?: number;
10
+ }
11
+ export interface SpawnTodoValidation {
12
+ valid: boolean;
13
+ reason?: string;
14
+ gate?: string;
15
+ hint?: Record<string, unknown>;
16
+ expectedTodoId?: string | null;
17
+ }
18
+ export declare function leadDevPlanStatePath(projectRoot?: string): string;
19
+ export declare function loadPersistedLeadDevPlan(projectRoot?: string): PersistedLeadDevPlan | null;
20
+ export declare function savePersistedLeadDevPlan(plan: PersistedLeadDevPlan, projectRoot?: string): string;
21
+ export declare function allPlanTodos(plan: PersistedLeadDevPlan): LeadDevTodo[];
22
+ export declare function getOutstandingTodos(plan: PersistedLeadDevPlan): LeadDevTodo[];
23
+ /** First outstanding todo in phase order */
24
+ export declare function getNextRequiredTodo(plan: PersistedLeadDevPlan): LeadDevTodo | null;
25
+ /**
26
+ * Plan valid for spawn when active and either:
27
+ * - outstanding todos remain (ignores 4h mtime), or
28
+ * - all todos complete and persistedAt within TTL
29
+ */
30
+ export declare function hasValidLeadDevPlanForSpawn(projectRoot?: string, _sessionId?: string | null): boolean;
31
+ export declare function updatePlanTodoStatus(todoId: string, status: LeadDevTodo['status'], projectRoot?: string): boolean;
32
+ export declare function bindPlanToSession(sessionId: string, projectRoot?: string): PersistedLeadDevPlan | null;
33
+ export interface SpawnToolInput {
34
+ prompt?: string;
35
+ description?: string;
36
+ task?: string;
37
+ subagent_type?: string;
38
+ agent?: string;
39
+ delegationId?: string;
40
+ planTodoId?: string;
41
+ }
42
+ export declare function validateSpawnMatchesTodo(toolInput: SpawnToolInput, projectRoot?: string, expectedTodo?: LeadDevTodo | null): SpawnTodoValidation;
43
+ export declare function markTodoInProgressOnSpawn(toolInput: SpawnToolInput, projectRoot?: string): string | null;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Todo-level lead-dev plan persistence — spawn gate survives beyond 4h mtime
3
+ * while outstanding todos remain. SSOT for plan todo status sync.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { DEFAULT_PENDING_TTL_MS } from './pending-delegations.js';
8
+ export function leadDevPlanStatePath(projectRoot = process.cwd()) {
9
+ return path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
10
+ }
11
+ export function loadPersistedLeadDevPlan(projectRoot = process.cwd()) {
12
+ const planPath = leadDevPlanStatePath(projectRoot);
13
+ if (!fs.existsSync(planPath))
14
+ return null;
15
+ try {
16
+ return JSON.parse(fs.readFileSync(planPath, 'utf8'));
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function savePersistedLeadDevPlan(plan, projectRoot = process.cwd()) {
23
+ const planPath = leadDevPlanStatePath(projectRoot);
24
+ fs.mkdirSync(path.dirname(planPath), { recursive: true });
25
+ fs.writeFileSync(planPath, JSON.stringify(plan, null, 2));
26
+ return planPath;
27
+ }
28
+ export function allPlanTodos(plan) {
29
+ return plan.phases.flatMap((p) => p.todos);
30
+ }
31
+ export function getOutstandingTodos(plan) {
32
+ return allPlanTodos(plan).filter((t) => t.status === 'pending' || t.status === 'in_progress');
33
+ }
34
+ /** First outstanding todo in phase order */
35
+ export function getNextRequiredTodo(plan) {
36
+ for (const phase of plan.phases) {
37
+ const next = phase.todos.find((t) => t.status === 'pending' || t.status === 'in_progress');
38
+ if (next)
39
+ return next;
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Plan valid for spawn when active and either:
45
+ * - outstanding todos remain (ignores 4h mtime), or
46
+ * - all todos complete and persistedAt within TTL
47
+ */
48
+ export function hasValidLeadDevPlanForSpawn(projectRoot = process.cwd(), _sessionId) {
49
+ const plan = loadPersistedLeadDevPlan(projectRoot);
50
+ if (!plan?.active)
51
+ return false;
52
+ const outstanding = getOutstandingTodos(plan);
53
+ if (outstanding.length > 0)
54
+ return true;
55
+ const persistedAt = plan.persistedAt;
56
+ if (!persistedAt) {
57
+ try {
58
+ const stat = fs.statSync(leadDevPlanStatePath(projectRoot));
59
+ return Date.now() - stat.mtimeMs <= DEFAULT_PENDING_TTL_MS;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ return Date.now() - new Date(persistedAt).getTime() <= DEFAULT_PENDING_TTL_MS;
66
+ }
67
+ export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd()) {
68
+ const plan = loadPersistedLeadDevPlan(projectRoot);
69
+ if (!plan)
70
+ return false;
71
+ let updated = false;
72
+ for (const phase of plan.phases) {
73
+ for (const todo of phase.todos) {
74
+ if (todo.id === todoId) {
75
+ todo.status = status;
76
+ updated = true;
77
+ }
78
+ }
79
+ }
80
+ if (updated)
81
+ savePersistedLeadDevPlan(plan, projectRoot);
82
+ return updated;
83
+ }
84
+ export function bindPlanToSession(sessionId, projectRoot = process.cwd()) {
85
+ const plan = loadPersistedLeadDevPlan(projectRoot);
86
+ if (!plan)
87
+ return null;
88
+ const generation = (plan.planGeneration ?? 0) + 1;
89
+ const next = {
90
+ ...plan,
91
+ sessionId,
92
+ planGeneration: generation,
93
+ persistedAt: new Date().toISOString(),
94
+ };
95
+ savePersistedLeadDevPlan(next, projectRoot);
96
+ return next;
97
+ }
98
+ function normalizeAgent(agent) {
99
+ const key = agent.toLowerCase().trim();
100
+ const aliases = {
101
+ 'bug-triage-specialist': 'bug-triage',
102
+ 'code-reviewer': 'code-review',
103
+ };
104
+ return aliases[key] ?? key;
105
+ }
106
+ function agentsAlign(expected, actual) {
107
+ const e = normalizeAgent(expected);
108
+ const a = normalizeAgent(actual);
109
+ return e === a || e.includes(a) || a.includes(e);
110
+ }
111
+ export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(), expectedTodo) {
112
+ const plan = loadPersistedLeadDevPlan(projectRoot);
113
+ const nextTodo = expectedTodo ?? (plan ? getNextRequiredTodo(plan) : null);
114
+ if (!nextTodo) {
115
+ return { valid: true };
116
+ }
117
+ const prompt = String(toolInput.prompt || toolInput.description || toolInput.task || '').toLowerCase();
118
+ const subagent = String(toolInput.subagent_type || toolInput.agent || '');
119
+ const explicitTodo = toolInput.planTodoId || toolInput.delegationId;
120
+ if (explicitTodo && explicitTodo === nextTodo.id) {
121
+ return { valid: true, expectedTodoId: nextTodo.id };
122
+ }
123
+ if (prompt.includes(nextTodo.id.toLowerCase())) {
124
+ return { valid: true, expectedTodoId: nextTodo.id };
125
+ }
126
+ if (prompt.includes(nextTodo.task.toLowerCase().slice(0, 30))) {
127
+ return { valid: true, expectedTodoId: nextTodo.id };
128
+ }
129
+ if (subagent && agentsAlign(nextTodo.subagent, subagent) && prompt.length > 20) {
130
+ return { valid: true, expectedTodoId: nextTodo.id };
131
+ }
132
+ return {
133
+ valid: false,
134
+ reason: `Spawn must target plan todo ${nextTodo.id} before other work`,
135
+ gate: 'spawn-todo-persistence',
136
+ expectedTodoId: nextTodo.id,
137
+ hint: {
138
+ tool: 'Task',
139
+ subagent_type: nextTodo.subagent,
140
+ planTodoId: nextTodo.id,
141
+ description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. ` +
142
+ `Include plan todo id in Task prompt.`,
143
+ },
144
+ };
145
+ }
146
+ export function markTodoInProgressOnSpawn(toolInput, projectRoot = process.cwd()) {
147
+ const plan = loadPersistedLeadDevPlan(projectRoot);
148
+ if (!plan)
149
+ return null;
150
+ const validation = validateSpawnMatchesTodo(toolInput, projectRoot);
151
+ if (!validation.valid || !validation.expectedTodoId)
152
+ return null;
153
+ updatePlanTodoStatus(validation.expectedTodoId, 'in_progress', projectRoot);
154
+ return validation.expectedTodoId;
155
+ }
@@ -0,0 +1,71 @@
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 type { LeadDevPlan, LeadDevTodo } from './autonomy-kernel.js';
6
+ export declare const DEFAULT_PENDING_TTL_MS: number;
7
+ export type DelegationMatchMethod = 'taskId' | 'description' | 'agent-only' | 'none';
8
+ export interface SpawnHint {
9
+ tool: 'Task';
10
+ subagent_type: string;
11
+ description: string;
12
+ planTodoId: string | null;
13
+ delegationId: string;
14
+ }
15
+ export interface PendingDelegation {
16
+ id: string;
17
+ taskId: string;
18
+ agent: string;
19
+ taskDescription: string;
20
+ taskType: string;
21
+ sessionId: string;
22
+ planTodoId: string | null;
23
+ planTodoTask: string | null;
24
+ matchMethod: DelegationMatchMethod;
25
+ matchConfidence: number;
26
+ status: 'pending' | 'satisfied';
27
+ createdAt: string;
28
+ satisfiedAt: string | null;
29
+ spawnHint: SpawnHint;
30
+ }
31
+ export interface PendingDelegationsState {
32
+ sessionId: string;
33
+ createdAt: string;
34
+ ttlMs: number;
35
+ delegations: PendingDelegation[];
36
+ }
37
+ export interface DeferredTaskRecord {
38
+ taskId: string;
39
+ agent: string;
40
+ description: string;
41
+ type?: string;
42
+ }
43
+ export declare function pendingDelegationsPath(projectRoot?: string): string;
44
+ export declare function leadDevPlanPath(projectRoot?: string): string;
45
+ export declare function isAutoChainDelegationsEnabled(projectRoot?: string): boolean;
46
+ export declare function matchPlanTodo(agent: string, taskDescription: string, taskId: string, plan: LeadDevPlan | null): {
47
+ todo: LeadDevTodo | null;
48
+ method: DelegationMatchMethod;
49
+ confidence: number;
50
+ };
51
+ export declare function mapSubagentToTaskType(agent: string): string;
52
+ export declare function buildSpawnHint(delegation: Pick<PendingDelegation, 'id' | 'agent' | 'taskDescription' | 'planTodoId' | 'planTodoTask'>): SpawnHint;
53
+ export declare function loadPendingDelegationsState(projectRoot?: string): PendingDelegationsState | null;
54
+ export declare function savePendingDelegationsState(state: PendingDelegationsState, projectRoot?: string): string;
55
+ export declare function recordDeferredDelegations(sessionId: string, deferrals: DeferredTaskRecord[], projectRoot?: string): PendingDelegation[];
56
+ export declare function isPendingStateActive(state: PendingDelegationsState | null, sessionId: string | null | undefined): boolean;
57
+ export declare function getActivePendingDelegations(sessionId: string | null | undefined, projectRoot?: string): PendingDelegation[];
58
+ export interface SatisfyDelegationInput {
59
+ delegationId?: string;
60
+ taskId?: string;
61
+ planTodoId?: string;
62
+ agent?: string;
63
+ toolPrompt?: string;
64
+ }
65
+ export declare function satisfyDelegation(input: SatisfyDelegationInput, projectRoot?: string): {
66
+ satisfied: PendingDelegation[];
67
+ clearedAll: boolean;
68
+ };
69
+ export declare function clearPendingDelegations(projectRoot?: string): boolean;
70
+ export declare function clearPendingDelegationsForSessionChange(newSessionId: string | null | undefined, projectRoot?: string): boolean;
71
+ export declare function delegationSummaryForResponse(delegations: PendingDelegation[]): Array<Record<string, unknown>>;