0xray 3.4.10 → 3.5.1
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 +36 -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 +53 -32
- 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/integrations/hermes-agent/__init__.py +50 -3
- package/dist/integrations/hermes-agent/bridge.mjs +78 -9
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +1 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +7 -0
- package/dist/mcps/orchestrator/handlers/task-handler.js +20 -3
- package/dist/nucleus/delegation-gate.d.ts +86 -0
- package/dist/nucleus/delegation-gate.js +275 -0
- 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/dist/plugin/xray-codex-injection.js +33 -0
- package/package.json +9 -2
- package/scripts/mjs/delegation-gate-fixture.mjs +84 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
- package/scripts/mjs/verify-grok-delegation-gate.mjs +113 -0
- package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
- package/scripts/mjs/verify-orchestrator-behavior.mjs +47 -1
- package/scripts/node/universal-version-manager.js +1 -1
- package/src/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +8 -1
- package/src/mcps/orchestrator/handlers/task-handler.ts +53 -3
- package/xray/features.json +2 -1
|
@@ -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>>;
|
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,9 @@ import { getOrCreateLogger } from "./plugin-logger.js";
|
|
|
7
7
|
import { resolveStateDir, importSystemPromptGenerator, loadXrayComponents, getProcessorManager, getXrayStateManager, getFeaturesConfigLoader, getDetectTaskType, getSystemPromptGenerator, } from "./plugin-modules.js";
|
|
8
8
|
import { getFrameworkIdentity, classifyTaskType, isWriteEditOperation, isPublishOperation, resolveAgentName, registerAllProcessors, registerAfterPostProcessors, logPreProcessorResults, logPostProcessorResults, logTestAutoCreationResult, spawnPromise, } from "./plugin-helpers.js";
|
|
9
9
|
import { runEnforcerQualityGate } from "./plugin-quality-gate.js";
|
|
10
|
+
import { evaluatePostToolSpawn, evaluatePreToolGate, isSubagentTool, loadDelegationGateFeatures, } from "../nucleus/delegation-gate.js";
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
10
13
|
// Re-export all public API for test compatibility
|
|
11
14
|
export { classifyTaskType } from "./plugin-helpers.js";
|
|
12
15
|
export { validateModulePath } from "./plugin-modules.js";
|
|
@@ -81,6 +84,25 @@ export default async function xrayCodexPlugin(input) {
|
|
|
81
84
|
throw new Error(`ENFORCER BLOCKED: ${qualityGateResult.violations.join("; ")}`);
|
|
82
85
|
}
|
|
83
86
|
logger.log(`✅ Quality gate passed for ${tool}`);
|
|
87
|
+
const features = loadDelegationGateFeatures(directory);
|
|
88
|
+
let sessionId = process.env.GROK_SESSION_ID ?? null;
|
|
89
|
+
try {
|
|
90
|
+
const boot = JSON.parse(readFileSync(join(directory, ".xray", "state", "session-boot.json"), "utf8"));
|
|
91
|
+
sessionId = boot.sessionId ?? sessionId;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* no session boot */
|
|
95
|
+
}
|
|
96
|
+
const delegationGate = evaluatePreToolGate(tool, (args ?? {}), {
|
|
97
|
+
projectRoot: directory,
|
|
98
|
+
sessionId,
|
|
99
|
+
features,
|
|
100
|
+
host: "opencode",
|
|
101
|
+
});
|
|
102
|
+
if (!delegationGate.allow) {
|
|
103
|
+
logger.error(`🚫 Delegation gate: ${delegationGate.reason}`);
|
|
104
|
+
throw new Error(`DELEGATION GATE: ${delegationGate.reason}`);
|
|
105
|
+
}
|
|
84
106
|
if (isWriteEditOperation(tool)) {
|
|
85
107
|
const _ProcessorManager = getProcessorManager();
|
|
86
108
|
const _XrayStateManager = getXrayStateManager();
|
|
@@ -158,6 +180,17 @@ export default async function xrayCodexPlugin(input) {
|
|
|
158
180
|
const logger = await getOrCreateLogger(directory);
|
|
159
181
|
await loadXrayComponents();
|
|
160
182
|
const { tool, args, result } = input;
|
|
183
|
+
if (isSubagentTool(tool)) {
|
|
184
|
+
try {
|
|
185
|
+
const spawnResult = evaluatePostToolSpawn(tool, (args ?? {}), directory);
|
|
186
|
+
if (spawnResult.satisfied.length > 0) {
|
|
187
|
+
logger.log(`🔗 Delegation gate post: satisfied ${spawnResult.satisfied.map((d) => d.id).join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
logger.error("Delegation gate post-hook error", error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
161
194
|
try {
|
|
162
195
|
const { routingOutcomeTracker } = await import("../delegation/analytics/outcome-tracker.js");
|
|
163
196
|
const { TOOL_AGENT_MAP } = await import("./plugin-helpers.js");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0xray",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
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,10 @@
|
|
|
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",
|
|
131
|
+
"verify:hermes-delegation-gate": "npm run build && node scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
132
|
+
"verify:delegation-gate": "npm run build && node scripts/mjs/verify-delegation-gate-core.mjs --host=grok && node scripts/mjs/verify-delegation-gate-core.mjs --host=hermes && node scripts/mjs/verify-delegation-gate-core.mjs --host=opencode && node scripts/mjs/verify-grok-delegation-gate.mjs && node scripts/mjs/verify-hermes-delegation-gate.mjs"
|
|
130
133
|
},
|
|
131
134
|
"files": [
|
|
132
135
|
"dist/",
|
|
@@ -139,6 +142,10 @@
|
|
|
139
142
|
"scripts/mjs/validate-mcp-connectivity.cjs",
|
|
140
143
|
"scripts/mjs/validate-postinstall-config.mjs",
|
|
141
144
|
"scripts/mjs/verify-orchestrator-behavior.mjs",
|
|
145
|
+
"scripts/mjs/verify-grok-delegation-gate.mjs",
|
|
146
|
+
"scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
147
|
+
"scripts/mjs/verify-delegation-gate-core.mjs",
|
|
148
|
+
"scripts/mjs/delegation-gate-fixture.mjs",
|
|
142
149
|
"src/opencode/",
|
|
143
150
|
"src/opencode/codex.codex",
|
|
144
151
|
"src/opencode/commands/",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared 4/4 delegation gate fixture — seeds consumer tmp workspace.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const FIXTURE_SESSION_ID = 'verify-gate';
|
|
8
|
+
|
|
9
|
+
export function seedDelegationGateFixture(tmp) {
|
|
10
|
+
mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
|
|
11
|
+
writeFileSync(
|
|
12
|
+
join(tmp, '.xray', 'features.json'),
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
multi_agent_orchestration: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
lead_dev_mode: true,
|
|
17
|
+
auto_chain_delegations: true,
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
active: true,
|
|
25
|
+
persistedAt: new Date().toISOString(),
|
|
26
|
+
phases: [],
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(tmp, '.xray', 'state', 'pending-delegations.json'),
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
sessionId: FIXTURE_SESSION_ID,
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
ttlMs: 14400000,
|
|
35
|
+
delegations: [
|
|
36
|
+
{
|
|
37
|
+
id: 'del-verify',
|
|
38
|
+
taskId: 'impl-1',
|
|
39
|
+
agent: 'backend-engineer',
|
|
40
|
+
taskDescription: 'implement',
|
|
41
|
+
taskType: 'implement',
|
|
42
|
+
sessionId: FIXTURE_SESSION_ID,
|
|
43
|
+
planTodoId: '2.1',
|
|
44
|
+
planTodoTask: 'swap deps',
|
|
45
|
+
matchMethod: 'agent-only',
|
|
46
|
+
matchConfidence: 0.5,
|
|
47
|
+
status: 'pending',
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
satisfiedAt: null,
|
|
50
|
+
spawnHint: {
|
|
51
|
+
tool: 'Task',
|
|
52
|
+
subagent_type: 'backend-engineer',
|
|
53
|
+
description: 'Lead-dev delegation 2.1',
|
|
54
|
+
planTodoId: '2.1',
|
|
55
|
+
delegationId: 'del-verify',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function seedSpawnTodoPlan(tmp) {
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
active: true,
|
|
68
|
+
persistedAt: new Date().toISOString(),
|
|
69
|
+
phases: [
|
|
70
|
+
{
|
|
71
|
+
id: 'phase-2',
|
|
72
|
+
todos: [
|
|
73
|
+
{
|
|
74
|
+
id: '2.1',
|
|
75
|
+
task: 'swap deps',
|
|
76
|
+
subagent: 'backend-engineer',
|
|
77
|
+
status: 'pending',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|