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.
- package/.xray/features.json +2 -1
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +32 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/core/features-config.d.ts +2 -0
- package/dist/integrations/grok/hooks/grok-hook-utils.js +390 -16
- package/dist/integrations/grok/hooks/post-tool-use.js +62 -0
- package/dist/integrations/grok/hooks/pre-tool-use.js +43 -5
- package/dist/integrations/grok/hooks/session-start.js +8 -0
- package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
- package/dist/mcps/orchestrator/config/agent-capabilities.d.ts +1 -1
- package/dist/mcps/orchestrator/config/agent-capabilities.js +23 -1
- package/dist/mcps/orchestrator/execution/execution-planner.js +2 -2
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +1 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +8 -1
- package/dist/mcps/orchestrator/handlers/task-handler.js +20 -3
- package/dist/nucleus/autonomy-kernel.d.ts +1 -1
- package/dist/nucleus/autonomy-kernel.js +5 -2
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +43 -0
- package/dist/nucleus/lead-dev-plan-persistence.js +155 -0
- package/dist/nucleus/pending-delegations.d.ts +71 -0
- package/dist/nucleus/pending-delegations.js +285 -0
- package/package.json +6 -2
- package/scripts/mjs/verify-grok-delegation-gate.mjs +173 -0
- package/scripts/mjs/verify-orchestrator-behavior.mjs +168 -0
- package/src/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
- package/src/mcps/orchestrator/config/agent-capabilities.ts +26 -0
- package/src/mcps/orchestrator/execution/execution-planner.ts +2 -0
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +9 -1
- package/src/mcps/orchestrator/handlers/task-handler.ts +53 -3
- 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(
|
|
49
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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>>;
|