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
package/.xray/features.json
CHANGED
package/AGENTS-consumer.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Quick reference for the 0xRay AI orchestration framework (shipped to your project on `npm install 0xray`).
|
|
6
6
|
|
|
7
|
-
**v3.
|
|
7
|
+
**v3.5.0** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
|
|
8
8
|
|
|
9
9
|
## Postinstall (automatic)
|
|
10
10
|
|
package/AGENTS.md
CHANGED
package/README.md
CHANGED
package/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.
|
|
3
|
+
**v3.5.0** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
package/dist/AGENTS.md
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
|
|
6
6
|
|
|
7
|
+
## [3.5.0] - 2026-06-19
|
|
8
|
+
|
|
9
|
+
### ✨ Features
|
|
10
|
+
- **Auto-chain bridge (PR1)** — `pending-delegations.json` SSOT on `delegate-deferred`; PreToolUse surgical gate blocks writes while pending; PostToolUse logs + targeted clear on Task; session-bound TTL; `delegations[]` in orchestrate response
|
|
11
|
+
- **Spawn todo persistence (PR2)** — lead-dev plan stays enforced while outstanding todos remain (beyond 4h mtime); `checkSubagentGate` validates Task targets next plan todo; plan todo `in_progress` sync on matched spawn; new intake clears pending delegations + binds `sessionId`/`planGeneration`
|
|
12
|
+
- **`auto_chain_delegations: false`** — disables PR1 delegation gate and PR2 spawn-todo validation (opt-out of full auto-chain enforcement)
|
|
13
|
+
|
|
14
|
+
### 🐛 Bug Fixes
|
|
15
|
+
- **verify-orchestrator-behavior** — checks `pending-delegations.json` in consumer cwd (not package root)
|
|
16
|
+
|
|
17
|
+
### ✅ Tests
|
|
18
|
+
- `pending-delegations`, `lead-dev-plan-persistence`, `grok-pending-delegation-gate` unit tests; `verify-grok-delegation-gate.mjs` hook fixture (4/4); orchestrator behavior verify 8/8
|
|
19
|
+
|
|
20
|
+
### 📋 Deferred (3.5.1+)
|
|
21
|
+
- **#3 Observability** — full session trail (`sessionId` / `taskId` / `planTodoId` / gate deny count correlation)
|
|
22
|
+
- **#4 Rule 1 vs Rule 2** — lead-dev arbitration when read/triage conflicts with spawn-first gate
|
|
23
|
+
- **#5 Codex hard-fail** — silent fallback removal (independent of auto-chain)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [3.4.10] - 2026-06-19
|
|
28
|
+
|
|
29
|
+
### 🐛 Bug Fixes
|
|
30
|
+
- **Routing SSOT** — `selectAgentForTask` checks `routeSubagent(taskType)` before capability scoring or memory routing; `type: implement` → `backend-engineer` (not `code-reviewer`)
|
|
31
|
+
- **Unified complexity** — `buildLeadDevPlan` uses `max(thin-dispatch, MCP overallComplexity)`; `dependencyCount: 8` at complexity 70 forces phased plan
|
|
32
|
+
|
|
33
|
+
### ✅ Tests
|
|
34
|
+
- `verify-orchestrator-behavior.mjs` ships in tarball — 5 behavior assertions (NaN, multi-task todos, implement routing, phased plan, honest deferral)
|
|
35
|
+
- Unit coverage: `agent-capabilities-routing`, extended `autonomy-kernel` + `execution-planner-dependencies`
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
7
39
|
## [3.4.9] - 2026-06-19
|
|
8
40
|
|
|
9
41
|
### ⚠️ Breaking Changes
|
package/dist/README.md
CHANGED
package/dist/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.
|
|
3
|
+
**v3.5.0** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
|
@@ -63,6 +63,8 @@ export interface MultiAgentOrchestrationConfig {
|
|
|
63
63
|
auto_consult_major_work?: boolean;
|
|
64
64
|
/** Codex 69 — block new MCP/skill/handler files via PreToolUse (default true) */
|
|
65
65
|
no_new_surface?: boolean;
|
|
66
|
+
/** 3.5.0 — PreToolUse gate after delegate-deferred (default true when lead_dev_mode) */
|
|
67
|
+
auto_chain_delegations?: boolean;
|
|
66
68
|
}
|
|
67
69
|
export interface AutonomousReportingConfig {
|
|
68
70
|
enabled: boolean;
|
|
@@ -90,9 +90,15 @@ export function loadFeatures() {
|
|
|
90
90
|
lead_dev_mode: orch.enabled !== false && orch.lead_dev_mode !== false,
|
|
91
91
|
no_new_surface: orch.no_new_surface !== false,
|
|
92
92
|
per_suite_test_triage: orch.per_suite_test_triage !== false,
|
|
93
|
+
auto_chain_delegations: orch.auto_chain_delegations !== false,
|
|
93
94
|
};
|
|
94
95
|
} catch {
|
|
95
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
lead_dev_mode: true,
|
|
98
|
+
no_new_surface: true,
|
|
99
|
+
per_suite_triage: true,
|
|
100
|
+
auto_chain_delegations: true,
|
|
101
|
+
};
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
|
|
@@ -114,17 +120,134 @@ export function leadDevPlanPath(root = workspaceRoot()) {
|
|
|
114
120
|
return path.join(root, '.xray', 'state', 'lead-dev-plan.json');
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
const DEFAULT_PLAN_TTL_MS = 4 * 60 * 60 * 1000;
|
|
124
|
+
|
|
125
|
+
export function loadPersistedLeadDevPlan(root = workspaceRoot()) {
|
|
126
|
+
const planPath = leadDevPlanPath(root);
|
|
127
|
+
if (!fs.existsSync(planPath)) return null;
|
|
120
128
|
try {
|
|
121
|
-
|
|
122
|
-
if (Date.now() - stat.mtimeMs > maxAgeMs) return false;
|
|
123
|
-
const plan = JSON.parse(fs.readFileSync(planPath, 'utf8'));
|
|
124
|
-
return plan.active === true;
|
|
129
|
+
return JSON.parse(fs.readFileSync(planPath, 'utf8'));
|
|
125
130
|
} catch {
|
|
126
|
-
return
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function savePersistedLeadDevPlan(plan, root = workspaceRoot()) {
|
|
136
|
+
const planPath = leadDevPlanPath(root);
|
|
137
|
+
fs.mkdirSync(path.dirname(planPath), { recursive: true });
|
|
138
|
+
fs.writeFileSync(planPath, JSON.stringify(plan, null, 2));
|
|
139
|
+
return planPath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getOutstandingPlanTodos(plan) {
|
|
143
|
+
if (!plan?.phases) return [];
|
|
144
|
+
return plan.phases.flatMap((p) => p.todos).filter(
|
|
145
|
+
(t) => t.status === 'pending' || t.status === 'in_progress',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getNextRequiredPlanTodo(plan) {
|
|
150
|
+
if (!plan?.phases) return null;
|
|
151
|
+
for (const phase of plan.phases) {
|
|
152
|
+
const next = phase.todos.find(
|
|
153
|
+
(t) => t.status === 'pending' || t.status === 'in_progress',
|
|
154
|
+
);
|
|
155
|
+
if (next) return next;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Todo-level persistence: plan stays valid while outstanding todos exist */
|
|
161
|
+
export function hasValidLeadDevPlanForSpawn(root = workspaceRoot()) {
|
|
162
|
+
const plan = loadPersistedLeadDevPlan(root);
|
|
163
|
+
if (!plan?.active) return false;
|
|
164
|
+
const outstanding = getOutstandingPlanTodos(plan);
|
|
165
|
+
if (outstanding.length > 0) return true;
|
|
166
|
+
const persistedAt = plan.persistedAt;
|
|
167
|
+
if (!persistedAt) {
|
|
168
|
+
try {
|
|
169
|
+
const stat = fs.statSync(leadDevPlanPath(root));
|
|
170
|
+
return Date.now() - stat.mtimeMs <= DEFAULT_PLAN_TTL_MS;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Date.now() - new Date(persistedAt).getTime() <= DEFAULT_PLAN_TTL_MS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function hasFreshLeadDevPlan(maxAgeMs = DEFAULT_PLAN_TTL_MS) {
|
|
179
|
+
return hasValidLeadDevPlanForSpawn();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeSpawnAgent(agent) {
|
|
183
|
+
const key = String(agent || '').toLowerCase().trim();
|
|
184
|
+
const aliases = {
|
|
185
|
+
'bug-triage-specialist': 'bug-triage',
|
|
186
|
+
'code-reviewer': 'code-review',
|
|
187
|
+
};
|
|
188
|
+
return aliases[key] ?? key;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function agentsAlign(expected, actual) {
|
|
192
|
+
const e = normalizeSpawnAgent(expected);
|
|
193
|
+
const a = normalizeSpawnAgent(actual);
|
|
194
|
+
return e === a || e.includes(a) || a.includes(e);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function validateSpawnMatchesTodo(toolInput, root = workspaceRoot(), expectedTodo = null) {
|
|
198
|
+
const plan = loadPersistedLeadDevPlan(root);
|
|
199
|
+
const nextTodo = expectedTodo ?? (plan ? getNextRequiredPlanTodo(plan) : null);
|
|
200
|
+
if (!nextTodo) return { valid: true };
|
|
201
|
+
|
|
202
|
+
const prompt = String(
|
|
203
|
+
toolInput.prompt || toolInput.description || toolInput.task || '',
|
|
204
|
+
).toLowerCase();
|
|
205
|
+
const subagent = String(toolInput.subagent_type || toolInput.agent || '');
|
|
206
|
+
const explicitTodo = toolInput.planTodoId || toolInput.delegationId;
|
|
207
|
+
|
|
208
|
+
if (explicitTodo && explicitTodo === nextTodo.id) {
|
|
209
|
+
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
127
210
|
}
|
|
211
|
+
if (prompt.includes(nextTodo.id.toLowerCase())) {
|
|
212
|
+
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
213
|
+
}
|
|
214
|
+
if (prompt.includes(nextTodo.task.toLowerCase().slice(0, 30))) {
|
|
215
|
+
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
216
|
+
}
|
|
217
|
+
if (subagent && agentsAlign(nextTodo.subagent, subagent) && prompt.length > 20) {
|
|
218
|
+
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
expectedTodoId: nextTodo.id,
|
|
224
|
+
reason: `Spawn must target plan todo ${nextTodo.id} before other work`,
|
|
225
|
+
gate: 'spawn-todo-persistence',
|
|
226
|
+
hint: {
|
|
227
|
+
tool: 'Task',
|
|
228
|
+
subagent_type: nextTodo.subagent,
|
|
229
|
+
planTodoId: nextTodo.id,
|
|
230
|
+
description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. Include plan todo id in Task prompt.`,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function updatePlanTodoStatusInPlace(todoId, status, root = workspaceRoot()) {
|
|
236
|
+
const plan = loadPersistedLeadDevPlan(root);
|
|
237
|
+
if (!plan) return false;
|
|
238
|
+
let updated = false;
|
|
239
|
+
for (const phase of plan.phases) {
|
|
240
|
+
for (const todo of phase.todos) {
|
|
241
|
+
if (todo.id === todoId) {
|
|
242
|
+
todo.status = status;
|
|
243
|
+
updated = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (updated) {
|
|
248
|
+
fs.writeFileSync(leadDevPlanPath(root), JSON.stringify(plan, null, 2));
|
|
249
|
+
}
|
|
250
|
+
return updated;
|
|
128
251
|
}
|
|
129
252
|
|
|
130
253
|
export function extractToolContext(event) {
|
|
@@ -167,14 +290,39 @@ export function checkSurfaceArea(paths, root = workspaceRoot()) {
|
|
|
167
290
|
return null;
|
|
168
291
|
}
|
|
169
292
|
|
|
170
|
-
export function checkSubagentGate(toolName, features) {
|
|
293
|
+
export function checkSubagentGate(toolName, features, root = workspaceRoot(), sessionId = null, toolInput = {}) {
|
|
171
294
|
if (!features.lead_dev_mode) return null;
|
|
172
|
-
if (!
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
295
|
+
if (!isSubagentTool(toolName)) return null;
|
|
296
|
+
|
|
297
|
+
if (!hasValidLeadDevPlanForSpawn(root)) {
|
|
298
|
+
return {
|
|
299
|
+
reason:
|
|
300
|
+
'Codex 59/67: call xray-orchestrator analyze-complexity first — ' +
|
|
301
|
+
'writes .xray/state/lead-dev-plan.json required before spawn_subagent',
|
|
302
|
+
gate: 'spawn-plan-missing',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (features.auto_chain_delegations === false) return null;
|
|
307
|
+
|
|
308
|
+
const pending = getActivePendingDelegations(root, sessionId);
|
|
309
|
+
const expectedTodo =
|
|
310
|
+
pending[0]?.planTodoId && loadPersistedLeadDevPlan(root)
|
|
311
|
+
? loadPersistedLeadDevPlan(root).phases
|
|
312
|
+
.flatMap((p) => p.todos)
|
|
313
|
+
.find((t) => t.id === pending[0].planTodoId) ?? null
|
|
314
|
+
: null;
|
|
315
|
+
|
|
316
|
+
const validation = validateSpawnMatchesTodo(toolInput, root, expectedTodo);
|
|
317
|
+
if (!validation.valid) {
|
|
318
|
+
return {
|
|
319
|
+
reason: validation.reason,
|
|
320
|
+
gate: validation.gate,
|
|
321
|
+
hint: validation.hint,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return null;
|
|
178
326
|
}
|
|
179
327
|
|
|
180
328
|
export function checkFullTestSuite(cmd, features) {
|
|
@@ -258,4 +406,230 @@ export function ensureSessionBoot(root = workspaceRoot(), source = '0xray/grok-b
|
|
|
258
406
|
}
|
|
259
407
|
}
|
|
260
408
|
return writeSessionBoot(root, buildSessionBootPayload(root, source));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const READ_TOOLS = new Set([
|
|
412
|
+
'read_file',
|
|
413
|
+
'Read',
|
|
414
|
+
'grep',
|
|
415
|
+
'Grep',
|
|
416
|
+
'glob',
|
|
417
|
+
'Glob',
|
|
418
|
+
'list_dir',
|
|
419
|
+
'ListDir',
|
|
420
|
+
'web_search',
|
|
421
|
+
'WebSearch',
|
|
422
|
+
'codebase_search',
|
|
423
|
+
'SemanticSearch',
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
const ORCHESTRATOR_CONSULT_TOOLS = new Set([
|
|
427
|
+
'analyze-complexity',
|
|
428
|
+
'analyze_complexity',
|
|
429
|
+
'get-orchestration-status',
|
|
430
|
+
'get_orchestration_status',
|
|
431
|
+
'orchestrate-task',
|
|
432
|
+
'orchestrate_task',
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
export function pendingDelegationsPath(root = workspaceRoot()) {
|
|
436
|
+
return path.join(root, '.xray', 'state', 'pending-delegations.json');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function loadPendingDelegationsState(root = workspaceRoot()) {
|
|
440
|
+
const filePath = pendingDelegationsPath(root);
|
|
441
|
+
if (!fs.existsSync(filePath)) return null;
|
|
442
|
+
try {
|
|
443
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function savePendingDelegationsState(state, root = workspaceRoot()) {
|
|
450
|
+
const filePath = pendingDelegationsPath(root);
|
|
451
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
452
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function resolveSessionId(event) {
|
|
456
|
+
return (
|
|
457
|
+
event.sessionId ||
|
|
458
|
+
process.env.GROK_SESSION_ID ||
|
|
459
|
+
null
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function isPendingStateActive(state, sessionId) {
|
|
464
|
+
if (!state || !sessionId || state.sessionId !== sessionId) return false;
|
|
465
|
+
const age = Date.now() - new Date(state.createdAt).getTime();
|
|
466
|
+
if (age > (state.ttlMs ?? 4 * 60 * 60 * 1000)) return false;
|
|
467
|
+
return state.delegations?.some((d) => d.status === 'pending');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function getActivePendingDelegations(root, sessionId) {
|
|
471
|
+
const state = loadPendingDelegationsState(root);
|
|
472
|
+
if (!isPendingStateActive(state, sessionId)) return [];
|
|
473
|
+
return state.delegations.filter((d) => d.status === 'pending');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function clearPendingDelegationsForSessionChange(newSessionId, root = workspaceRoot()) {
|
|
477
|
+
const state = loadPendingDelegationsState(root);
|
|
478
|
+
if (!state) return false;
|
|
479
|
+
if (newSessionId && state.sessionId === newSessionId) return false;
|
|
480
|
+
try {
|
|
481
|
+
fs.unlinkSync(pendingDelegationsPath(root));
|
|
482
|
+
return true;
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function isFocusedTestCommand(cmd) {
|
|
489
|
+
const cmdText = (cmd || '').toLowerCase();
|
|
490
|
+
return (
|
|
491
|
+
/\b(npm\s+(run\s+)?test|vitest\s+run|pnpm\s+test|yarn\s+test)\b/.test(cmdText) &&
|
|
492
|
+
(/--\s+\S+\.test/.test(cmdText) || /vitest\s+run\s+\S+\.test/.test(cmdText))
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function isReadOnlyTool(toolName) {
|
|
497
|
+
return READ_TOOLS.has(toolName) || /^(read|grep|glob|list)/i.test(toolName);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function isSubagentTool(toolName) {
|
|
501
|
+
return SUBAGENT_TOOLS.has(toolName) || /^task$/i.test(toolName) || toolName === 'spawn_subagent';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function extractMcpToolName(toolInput) {
|
|
505
|
+
const candidates = [
|
|
506
|
+
toolInput.toolName,
|
|
507
|
+
toolInput.tool,
|
|
508
|
+
toolInput.name,
|
|
509
|
+
toolInput.mcpToolName,
|
|
510
|
+
];
|
|
511
|
+
for (const c of candidates) {
|
|
512
|
+
if (c) return String(c);
|
|
513
|
+
}
|
|
514
|
+
const server = String(toolInput.server || toolInput.mcpServer || '');
|
|
515
|
+
const args = toolInput.arguments ?? toolInput.args ?? {};
|
|
516
|
+
if (args.toolName) return String(args.toolName);
|
|
517
|
+
if (server.includes('orchestrator') && args.name) return String(args.name);
|
|
518
|
+
return '';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function isOrchestratorConsultMcp(toolName, toolInput) {
|
|
522
|
+
if (!/mcp|CallMcpTool/i.test(toolName)) return false;
|
|
523
|
+
const inner = extractMcpToolName(toolInput).toLowerCase();
|
|
524
|
+
if (!inner) return false;
|
|
525
|
+
for (const t of ORCHESTRATOR_CONSULT_TOOLS) {
|
|
526
|
+
if (inner.includes(t) || inner.includes(t.replace(/-/g, '_'))) return true;
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function isOrchestrateToolEvent(toolName, toolInput = {}) {
|
|
532
|
+
const inner = extractMcpToolName(toolInput).toLowerCase();
|
|
533
|
+
if (/orchestrate[-_]?task/.test(inner)) return true;
|
|
534
|
+
const blob = JSON.stringify(toolInput).toLowerCase();
|
|
535
|
+
return /orchestrate[-_]?task/.test(blob);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function satisfyDelegationsFromToolInput(toolInput, root = workspaceRoot()) {
|
|
539
|
+
const state = loadPendingDelegationsState(root);
|
|
540
|
+
if (!state) return { satisfied: [], clearedAll: false };
|
|
541
|
+
|
|
542
|
+
const prompt = String(
|
|
543
|
+
toolInput.prompt || toolInput.description || toolInput.task || '',
|
|
544
|
+
).toLowerCase();
|
|
545
|
+
const subagent = String(toolInput.subagent_type || toolInput.agent || '').toLowerCase();
|
|
546
|
+
const delegationId = toolInput.delegationId || toolInput.delegation_id;
|
|
547
|
+
|
|
548
|
+
const pending = state.delegations.filter((d) => d.status === 'pending');
|
|
549
|
+
if (pending.length === 0) return { satisfied: [], clearedAll: false };
|
|
550
|
+
|
|
551
|
+
let matches = [];
|
|
552
|
+
if (delegationId) {
|
|
553
|
+
matches = pending.filter((d) => d.id === delegationId);
|
|
554
|
+
} else {
|
|
555
|
+
for (const d of pending) {
|
|
556
|
+
if (d.planTodoId && prompt.includes(d.planTodoId.toLowerCase())) {
|
|
557
|
+
matches.push(d);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
if (prompt.includes(d.taskId.toLowerCase())) {
|
|
561
|
+
matches.push(d);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
if (subagent && d.agent.toLowerCase().includes(subagent)) {
|
|
565
|
+
matches.push(d);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (matches.length === 0 && pending.length === 1) {
|
|
572
|
+
const only = pending[0];
|
|
573
|
+
const hint = (only.spawnHint?.description || '').toLowerCase();
|
|
574
|
+
if (prompt && hint && prompt.includes(hint.slice(0, 30))) {
|
|
575
|
+
matches = [only];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (matches.length === 0) return { satisfied: [], clearedAll: false };
|
|
580
|
+
|
|
581
|
+
const now = new Date().toISOString();
|
|
582
|
+
const ids = new Set(matches.map((m) => m.id));
|
|
583
|
+
for (const d of state.delegations) {
|
|
584
|
+
if (ids.has(d.id) && d.status === 'pending') {
|
|
585
|
+
d.status = 'satisfied';
|
|
586
|
+
d.satisfiedAt = now;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
savePendingDelegationsState(state, root);
|
|
590
|
+
for (const m of matches) {
|
|
591
|
+
if (m.planTodoId) {
|
|
592
|
+
updatePlanTodoStatusInPlace(m.planTodoId, 'in_progress', root);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const clearedAll = !state.delegations.some((d) => d.status === 'pending');
|
|
596
|
+
return { satisfied: matches, clearedAll };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Surgical gate: block writes and unrelated work while pending delegations exist.
|
|
601
|
+
* Allows read-only tools, orchestrator consult MCP, focused test shell, and Task/spawn.
|
|
602
|
+
*/
|
|
603
|
+
export function checkPendingDelegationGate(toolName, toolInput, features, root, sessionId) {
|
|
604
|
+
if (!features.lead_dev_mode || features.auto_chain_delegations === false) return null;
|
|
605
|
+
if (!sessionId) return null;
|
|
606
|
+
|
|
607
|
+
const pending = getActivePendingDelegations(root, sessionId);
|
|
608
|
+
if (pending.length === 0) return null;
|
|
609
|
+
|
|
610
|
+
if (isSubagentTool(toolName)) return null;
|
|
611
|
+
|
|
612
|
+
if (isReadOnlyTool(toolName)) return null;
|
|
613
|
+
|
|
614
|
+
if (isOrchestratorConsultMcp(toolName, toolInput)) return null;
|
|
615
|
+
|
|
616
|
+
if (isShellTool(toolName)) {
|
|
617
|
+
const cmd = String(toolInput.command || '');
|
|
618
|
+
if (isFocusedTestCommand(cmd)) return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const primary = pending[0];
|
|
622
|
+
return {
|
|
623
|
+
reason: 'Pending implementation delegation — spawn host Task before other work',
|
|
624
|
+
gate: 'auto-chain-pending',
|
|
625
|
+
hint: primary.spawnHint ?? {
|
|
626
|
+
tool: 'Task',
|
|
627
|
+
subagent_type: primary.agent,
|
|
628
|
+
description: primary.taskDescription,
|
|
629
|
+
planTodoId: primary.planTodoId,
|
|
630
|
+
delegationId: primary.id,
|
|
631
|
+
},
|
|
632
|
+
pendingCount: pending.length,
|
|
633
|
+
delegationId: primary.id,
|
|
634
|
+
};
|
|
261
635
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Grok PostToolUse — auto-chain observability + targeted clear on Task spawn.
|
|
4
|
+
* Passive hook: stdout ignored by Grok; side effects only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
extractToolContext,
|
|
9
|
+
isOrchestrateToolEvent,
|
|
10
|
+
isSubagentTool,
|
|
11
|
+
readStdinJson,
|
|
12
|
+
resolveSessionId,
|
|
13
|
+
satisfyDelegationsFromToolInput,
|
|
14
|
+
validateSpawnMatchesTodo,
|
|
15
|
+
updatePlanTodoStatusInPlace,
|
|
16
|
+
workspaceRoot,
|
|
17
|
+
} from './grok-hook-utils.js';
|
|
18
|
+
import { appendHookActivity } from './grok-hook-activity.js';
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const root = workspaceRoot();
|
|
22
|
+
try {
|
|
23
|
+
const event = await readStdinJson();
|
|
24
|
+
const eventRoot = event.workspaceRoot || event.cwd || root;
|
|
25
|
+
const sessionId = resolveSessionId(event);
|
|
26
|
+
const { toolName, toolInput } = extractToolContext(event);
|
|
27
|
+
|
|
28
|
+
if (isOrchestrateToolEvent(toolName, toolInput)) {
|
|
29
|
+
appendHookActivity(eventRoot, 'grok-post-tool-use', 'auto-chain-pending', 'info', {
|
|
30
|
+
tool: toolName,
|
|
31
|
+
sessionId,
|
|
32
|
+
note: 'orchestrate-task completed — pending-delegations.json written by task-handler',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isSubagentTool(toolName)) {
|
|
37
|
+
const spawnCheck = validateSpawnMatchesTodo(toolInput, eventRoot);
|
|
38
|
+
if (spawnCheck.valid && spawnCheck.expectedTodoId) {
|
|
39
|
+
updatePlanTodoStatusInPlace(spawnCheck.expectedTodoId, 'in_progress', eventRoot);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = satisfyDelegationsFromToolInput(toolInput, eventRoot);
|
|
43
|
+
if (result.satisfied.length > 0) {
|
|
44
|
+
appendHookActivity(eventRoot, 'grok-post-tool-use', 'auto-chain-cleared', 'success', {
|
|
45
|
+
tool: toolName,
|
|
46
|
+
satisfied: result.satisfied.map((d) => d.id),
|
|
47
|
+
clearedAll: result.clearedAll,
|
|
48
|
+
planTodoId: spawnCheck.expectedTodoId ?? null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.exit(0);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
appendHookActivity(root, 'grok-post-tool-use', 'hook-error', 'error', {
|
|
56
|
+
error: err.message,
|
|
57
|
+
});
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|