@cleocode/core 2026.3.72 → 2026.3.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/cleo.d.ts.map +1 -1
  2. package/dist/hooks/handlers/agent-hooks.d.ts +48 -0
  3. package/dist/hooks/handlers/agent-hooks.d.ts.map +1 -0
  4. package/dist/hooks/handlers/context-hooks.d.ts +53 -0
  5. package/dist/hooks/handlers/context-hooks.d.ts.map +1 -0
  6. package/dist/hooks/handlers/error-hooks.d.ts +4 -4
  7. package/dist/hooks/handlers/error-hooks.d.ts.map +1 -1
  8. package/dist/hooks/handlers/file-hooks.d.ts +3 -3
  9. package/dist/hooks/handlers/file-hooks.d.ts.map +1 -1
  10. package/dist/hooks/handlers/index.d.ts +8 -1
  11. package/dist/hooks/handlers/index.d.ts.map +1 -1
  12. package/dist/hooks/handlers/mcp-hooks.d.ts +29 -7
  13. package/dist/hooks/handlers/mcp-hooks.d.ts.map +1 -1
  14. package/dist/hooks/handlers/session-hooks.d.ts +5 -5
  15. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  16. package/dist/hooks/handlers/task-hooks.d.ts +5 -5
  17. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  18. package/dist/hooks/handlers/work-capture-hooks.d.ts +7 -7
  19. package/dist/hooks/handlers/work-capture-hooks.d.ts.map +1 -1
  20. package/dist/hooks/payload-schemas.d.ts +177 -11
  21. package/dist/hooks/payload-schemas.d.ts.map +1 -1
  22. package/dist/hooks/provider-hooks.d.ts +33 -7
  23. package/dist/hooks/provider-hooks.d.ts.map +1 -1
  24. package/dist/hooks/registry.d.ts +26 -6
  25. package/dist/hooks/registry.d.ts.map +1 -1
  26. package/dist/hooks/types.d.ts +132 -38
  27. package/dist/hooks/types.d.ts.map +1 -1
  28. package/dist/index.js +818 -233
  29. package/dist/index.js.map +4 -4
  30. package/dist/nexus/index.d.ts +2 -0
  31. package/dist/nexus/index.d.ts.map +1 -1
  32. package/dist/nexus/workspace.d.ts +128 -0
  33. package/dist/nexus/workspace.d.ts.map +1 -0
  34. package/dist/sessions/snapshot.d.ts.map +1 -1
  35. package/package.json +6 -6
  36. package/src/cleo.ts +14 -0
  37. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +634 -0
  38. package/src/hooks/handlers/agent-hooks.ts +148 -0
  39. package/src/hooks/handlers/context-hooks.ts +156 -0
  40. package/src/hooks/handlers/error-hooks.ts +8 -5
  41. package/src/hooks/handlers/file-hooks.ts +6 -4
  42. package/src/hooks/handlers/index.ts +12 -1
  43. package/src/hooks/handlers/mcp-hooks.ts +74 -9
  44. package/src/hooks/handlers/session-hooks.ts +7 -7
  45. package/src/hooks/handlers/task-hooks.ts +7 -7
  46. package/src/hooks/handlers/work-capture-hooks.ts +12 -12
  47. package/src/hooks/payload-schemas.ts +96 -26
  48. package/src/hooks/provider-hooks.ts +50 -9
  49. package/src/hooks/registry.ts +86 -23
  50. package/src/hooks/types.ts +175 -39
  51. package/src/nexus/index.ts +15 -0
  52. package/src/nexus/workspace.ts +508 -0
  53. package/src/sessions/index.ts +4 -4
  54. package/src/sessions/snapshot.ts +4 -2
  55. package/src/store/json.ts +2 -2
  56. package/src/task-work/index.ts +4 -4
@@ -0,0 +1,508 @@
1
+ /**
2
+ * NEXUS Workspace — Cross-project orchestration operations.
3
+ *
4
+ * Implements ORCH-PLAN Phase B:
5
+ * - B.2: nexus.route(directiveEvent) — dispatch Conduit directives to the correct project
6
+ * - B.3: nexus.workspace.status() — aggregated cross-project task view
7
+ * - B.4: nexus.workspace.agents() — cross-project agent registry view
8
+ *
9
+ * Security: project-level ACL enforced on all routing operations (HIGH-02).
10
+ * Rate limiting: per-agent throttling on route operations (MEDIUM-05).
11
+ * Audit: all routing operations logged (LOW-06).
12
+ *
13
+ * @module nexus/workspace
14
+ */
15
+
16
+ import type { ConduitMessage } from '@cleocode/contracts';
17
+ import { ExitCode } from '@cleocode/contracts';
18
+ import { CleoError } from '../errors.js';
19
+ import type { DataAccessor } from '../store/data-accessor.js';
20
+ import { getAccessor } from '../store/data-accessor.js';
21
+ import { type NexusProject, nexusList } from './registry.js';
22
+
23
+ // ============================================================================
24
+ // Types
25
+ // ============================================================================
26
+
27
+ /** Parsed directive from a Conduit message. */
28
+ export interface ParsedDirective {
29
+ /** The directive verb (claim, done, blocked, action, etc.). */
30
+ verb: string;
31
+ /** Task references extracted from the message (e.g., T042, T1234). */
32
+ taskRefs: string[];
33
+ /** The agent ID that sent the directive. */
34
+ agentId: string;
35
+ /** Original message ID for audit trail. */
36
+ messageId: string;
37
+ /** Timestamp of the directive. */
38
+ timestamp: string;
39
+ }
40
+
41
+ /** Result of routing a directive to a project. */
42
+ export interface RouteResult {
43
+ /** Whether the routing succeeded. */
44
+ success: boolean;
45
+ /** The project that was routed to. */
46
+ project: string;
47
+ /** The project's filesystem path. */
48
+ projectPath: string;
49
+ /** The task that was affected. */
50
+ taskId: string;
51
+ /** What operation was performed. */
52
+ operation: string;
53
+ /** Error message if routing failed. */
54
+ error?: string;
55
+ }
56
+
57
+ /** Aggregated task status across all projects. */
58
+ export interface WorkspaceStatus {
59
+ /** Total projects in the workspace. */
60
+ projectCount: number;
61
+ /** Per-project task summaries. */
62
+ projects: WorkspaceProjectSummary[];
63
+ /** Aggregated totals. */
64
+ totals: {
65
+ pending: number;
66
+ active: number;
67
+ done: number;
68
+ total: number;
69
+ };
70
+ /** When the status was computed. */
71
+ computedAt: string;
72
+ }
73
+
74
+ /** Task summary for a single project. */
75
+ export interface WorkspaceProjectSummary {
76
+ /** Project name. */
77
+ name: string;
78
+ /** Project path. */
79
+ path: string;
80
+ /** Task counts by status. */
81
+ counts: {
82
+ pending: number;
83
+ active: number;
84
+ done: number;
85
+ total: number;
86
+ };
87
+ /** Health status from the Nexus registry. */
88
+ health: string;
89
+ /** Last sync time. */
90
+ lastSync: string;
91
+ }
92
+
93
+ /** Agent info aggregated across projects. */
94
+ export interface WorkspaceAgent {
95
+ /** Agent instance ID. */
96
+ agentId: string;
97
+ /** Agent type. */
98
+ agentType: string;
99
+ /** Current status. */
100
+ status: string;
101
+ /** Which project this agent is registered in. */
102
+ project: string;
103
+ /** Current task (if any). */
104
+ taskId: string | null;
105
+ /** Last heartbeat. */
106
+ lastHeartbeat: string;
107
+ }
108
+
109
+ /** Project-level ACL entry. */
110
+ export interface ProjectACL {
111
+ /** Agent IDs with write access. */
112
+ authorizedAgents: string[];
113
+ }
114
+
115
+ // ============================================================================
116
+ // Rate limiting (MEDIUM-05)
117
+ // ============================================================================
118
+
119
+ const RATE_LIMIT_WINDOW_MS = 60_000;
120
+ const RATE_LIMIT_MAX_OPS = 100;
121
+
122
+ /** Per-agent operation counters for rate limiting. */
123
+ const rateLimitCounters = new Map<string, { count: number; windowStart: number }>();
124
+
125
+ /** Check if an agent is rate-limited. Throws if exceeded. */
126
+ function checkRateLimit(agentId: string): void {
127
+ const now = Date.now();
128
+ const entry = rateLimitCounters.get(agentId);
129
+
130
+ if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
131
+ rateLimitCounters.set(agentId, { count: 1, windowStart: now });
132
+ return;
133
+ }
134
+
135
+ entry.count++;
136
+ if (entry.count > RATE_LIMIT_MAX_OPS) {
137
+ throw new CleoError(
138
+ ExitCode.GENERAL_ERROR,
139
+ `Agent '${agentId}' exceeded rate limit: ${RATE_LIMIT_MAX_OPS} routing ops per ${RATE_LIMIT_WINDOW_MS / 1000}s`,
140
+ );
141
+ }
142
+ }
143
+
144
+ // ============================================================================
145
+ // ACL (HIGH-02)
146
+ // ============================================================================
147
+
148
+ /** Default ACL: all agents have access (open by default, restrict as needed). */
149
+ const DEFAULT_ACL: ProjectACL = { authorizedAgents: ['*'] };
150
+
151
+ /**
152
+ * Load ACL for a project. Reads from .cleo/config.json authorizedAgents field.
153
+ * Falls back to open access if not configured.
154
+ */
155
+ async function loadProjectACL(projectPath: string): Promise<ProjectACL> {
156
+ try {
157
+ const { loadConfig } = await import('../config.js');
158
+ const config = await loadConfig(projectPath);
159
+ const agents = (config as unknown as Record<string, unknown>)?.authorizedAgents;
160
+ if (Array.isArray(agents) && agents.length > 0) {
161
+ return { authorizedAgents: agents as string[] };
162
+ }
163
+ } catch {
164
+ // Config load failure = open access
165
+ }
166
+ return DEFAULT_ACL;
167
+ }
168
+
169
+ /** Check if an agent is authorized to mutate a project. */
170
+ function isAuthorized(acl: ProjectACL, agentId: string): boolean {
171
+ if (acl.authorizedAgents.includes('*')) return true;
172
+ return acl.authorizedAgents.includes(agentId);
173
+ }
174
+
175
+ // ============================================================================
176
+ // B.2: nexus.route() — Directive routing
177
+ // ============================================================================
178
+
179
+ /** Task reference pattern: T followed by digits (T001, T42, T1234). */
180
+ const TASK_REF_PATTERN = /\bT(\d+)\b/g;
181
+
182
+ /**
183
+ * Parse a Conduit message into a structured directive.
184
+ *
185
+ * Extracts directive verbs (/claim, /done, /blocked) and task references
186
+ * from the message content and metadata.
187
+ */
188
+ export function parseDirective(message: ConduitMessage): ParsedDirective | null {
189
+ const content = message.content;
190
+
191
+ // Extract directive verb (first /word in content)
192
+ const verbMatch = content.match(/^\/(\w+)/);
193
+ if (!verbMatch) return null;
194
+
195
+ const verb = verbMatch[1];
196
+
197
+ // Extract task refs from content
198
+ const taskRefs: string[] = [];
199
+ const pattern = new RegExp(TASK_REF_PATTERN.source, 'g');
200
+ for (const m of content.matchAll(pattern)) {
201
+ taskRefs.push(`T${m[1]}`);
202
+ }
203
+
204
+ // Also check metadata.taskRefs from SignalDock extraction (Phase A.5)
205
+ const metaRefs = (message.metadata as Record<string, unknown>)?.taskRefs;
206
+ if (Array.isArray(metaRefs)) {
207
+ for (const ref of metaRefs) {
208
+ if (typeof ref === 'string' && !taskRefs.includes(ref)) {
209
+ taskRefs.push(ref);
210
+ }
211
+ }
212
+ }
213
+
214
+ if (taskRefs.length === 0) return null;
215
+
216
+ return {
217
+ verb,
218
+ taskRefs,
219
+ agentId: message.from,
220
+ messageId: message.id,
221
+ timestamp: message.timestamp,
222
+ };
223
+ }
224
+
225
+ /** Map directive verbs to CLEO task operations. */
226
+ const VERB_TO_OPERATION: Record<string, string> = {
227
+ claim: 'tasks.start',
228
+ done: 'tasks.complete',
229
+ complete: 'tasks.complete',
230
+ blocked: 'tasks.update', // Update status to blocked
231
+ start: 'tasks.start',
232
+ stop: 'tasks.stop',
233
+ };
234
+
235
+ /**
236
+ * Route a Conduit directive to the correct project's CLEO instance.
237
+ *
238
+ * Resolves which project owns the referenced task, checks ACL,
239
+ * and dispatches the appropriate CLEO operation.
240
+ *
241
+ * @param directive - Parsed directive from a Conduit message
242
+ * @returns Array of route results (one per task reference)
243
+ */
244
+ export async function routeDirective(directive: ParsedDirective): Promise<RouteResult[]> {
245
+ // Rate limit check (MEDIUM-05)
246
+ checkRateLimit(directive.agentId);
247
+
248
+ const results: RouteResult[] = [];
249
+ const operation = VERB_TO_OPERATION[directive.verb];
250
+
251
+ if (!operation) {
252
+ // Unknown verb — not routable, just skip
253
+ return results;
254
+ }
255
+
256
+ const projects = await nexusList();
257
+
258
+ for (const taskRef of directive.taskRefs) {
259
+ const result = await routeSingleTask(taskRef, directive, operation, projects);
260
+ results.push(result);
261
+ }
262
+
263
+ return results;
264
+ }
265
+
266
+ /** Route a single task reference to its project and execute the operation. */
267
+ async function routeSingleTask(
268
+ taskId: string,
269
+ directive: ParsedDirective,
270
+ operation: string,
271
+ projects: NexusProject[],
272
+ ): Promise<RouteResult> {
273
+ // Find which project owns this task
274
+ let targetProject: NexusProject | null = null;
275
+ let targetAccessor: DataAccessor | null = null;
276
+
277
+ for (const project of projects) {
278
+ try {
279
+ const acc = await getAccessor(project.path);
280
+ const { tasks } = await acc.queryTasks({});
281
+ const task = tasks.find((t) => t.id === taskId);
282
+ if (task) {
283
+ targetProject = project;
284
+ targetAccessor = acc;
285
+ break;
286
+ }
287
+ } catch {}
288
+ }
289
+
290
+ if (!targetProject || !targetAccessor) {
291
+ return {
292
+ success: false,
293
+ project: 'unknown',
294
+ projectPath: '',
295
+ taskId,
296
+ operation,
297
+ error: `Task ${taskId} not found in any registered project`,
298
+ };
299
+ }
300
+
301
+ // ACL check (HIGH-02)
302
+ const acl = await loadProjectACL(targetProject.path);
303
+ if (!isAuthorized(acl, directive.agentId)) {
304
+ return {
305
+ success: false,
306
+ project: targetProject.name,
307
+ projectPath: targetProject.path,
308
+ taskId,
309
+ operation,
310
+ error: `Agent '${directive.agentId}' not authorized to mutate project '${targetProject.name}'`,
311
+ };
312
+ }
313
+
314
+ // Execute the operation
315
+ try {
316
+ await executeOperation(operation, taskId, targetProject.path, targetAccessor, directive);
317
+
318
+ // Audit log (LOW-06)
319
+ await logRouteAudit(directive, targetProject.name, taskId, operation, true);
320
+
321
+ return {
322
+ success: true,
323
+ project: targetProject.name,
324
+ projectPath: targetProject.path,
325
+ taskId,
326
+ operation,
327
+ };
328
+ } catch (err) {
329
+ const errorMsg = err instanceof Error ? err.message : String(err);
330
+
331
+ // Audit log for failures too
332
+ await logRouteAudit(directive, targetProject.name, taskId, operation, false, errorMsg);
333
+
334
+ return {
335
+ success: false,
336
+ project: targetProject.name,
337
+ projectPath: targetProject.path,
338
+ taskId,
339
+ operation,
340
+ error: errorMsg,
341
+ };
342
+ }
343
+ }
344
+
345
+ /** Execute a CLEO operation on a project's task. */
346
+ async function executeOperation(
347
+ operation: string,
348
+ taskId: string,
349
+ projectPath: string,
350
+ accessor: DataAccessor,
351
+ directive: ParsedDirective,
352
+ ): Promise<void> {
353
+ switch (operation) {
354
+ case 'tasks.start': {
355
+ const { startTask } = await import('../task-work/index.js');
356
+ await startTask(taskId, projectPath, accessor);
357
+ break;
358
+ }
359
+ case 'tasks.complete': {
360
+ const { completeTask } = await import('../tasks/complete.js');
361
+ await completeTask(
362
+ { taskId, notes: `Completed via Conduit directive from ${directive.agentId}` },
363
+ projectPath,
364
+ accessor,
365
+ );
366
+ break;
367
+ }
368
+ case 'tasks.stop': {
369
+ const { stopTask } = await import('../task-work/index.js');
370
+ await stopTask(projectPath, accessor);
371
+ break;
372
+ }
373
+ case 'tasks.update': {
374
+ const { updateTask } = await import('../tasks/update.js');
375
+ await updateTask(
376
+ { taskId, notes: `Marked blocked via Conduit directive from ${directive.agentId}` },
377
+ projectPath,
378
+ accessor,
379
+ );
380
+ break;
381
+ }
382
+ }
383
+ }
384
+
385
+ /** Log a routing operation to the audit trail. */
386
+ async function logRouteAudit(
387
+ directive: ParsedDirective,
388
+ projectName: string,
389
+ taskId: string,
390
+ operation: string,
391
+ success: boolean,
392
+ error?: string,
393
+ ): Promise<void> {
394
+ try {
395
+ const { getLogger } = await import('../logger.js');
396
+ const log = getLogger('nexus.route');
397
+ const level = success ? 'info' : 'warn';
398
+ log[level](
399
+ {
400
+ directive: directive.verb,
401
+ agentId: directive.agentId,
402
+ messageId: directive.messageId,
403
+ project: projectName,
404
+ taskId,
405
+ operation,
406
+ success,
407
+ error,
408
+ },
409
+ `Conduit directive routed: ${directive.verb} ${taskId} → ${projectName} (${success ? 'OK' : 'FAILED'})`,
410
+ );
411
+ } catch {
412
+ // Audit logging is best-effort
413
+ }
414
+ }
415
+
416
+ // ============================================================================
417
+ // B.3: nexus.workspace.status() — Aggregated view
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Get aggregated task status across all registered projects.
422
+ *
423
+ * Returns per-project task counts and workspace-wide totals.
424
+ * Respects project permissions — only includes readable projects.
425
+ */
426
+ export async function workspaceStatus(): Promise<WorkspaceStatus> {
427
+ const projects = await nexusList();
428
+ const summaries: WorkspaceProjectSummary[] = [];
429
+ const totals = { pending: 0, active: 0, done: 0, total: 0 };
430
+
431
+ for (const project of projects) {
432
+ try {
433
+ const acc = await getAccessor(project.path);
434
+ const { tasks } = await acc.queryTasks({});
435
+
436
+ const counts = {
437
+ pending: tasks.filter((t) => t.status === 'pending').length,
438
+ active: tasks.filter((t) => t.status === 'active').length,
439
+ done: tasks.filter((t) => t.status === 'done').length,
440
+ total: tasks.length,
441
+ };
442
+
443
+ summaries.push({
444
+ name: project.name,
445
+ path: project.path,
446
+ counts,
447
+ health: project.healthStatus,
448
+ lastSync: project.lastSync,
449
+ });
450
+
451
+ totals.pending += counts.pending;
452
+ totals.active += counts.active;
453
+ totals.done += counts.done;
454
+ totals.total += counts.total;
455
+ } catch {
456
+ // Skip unreachable projects
457
+ summaries.push({
458
+ name: project.name,
459
+ path: project.path,
460
+ counts: { pending: 0, active: 0, done: 0, total: 0 },
461
+ health: 'unreachable',
462
+ lastSync: project.lastSync,
463
+ });
464
+ }
465
+ }
466
+
467
+ return {
468
+ projectCount: projects.length,
469
+ projects: summaries,
470
+ totals,
471
+ computedAt: new Date().toISOString(),
472
+ };
473
+ }
474
+
475
+ // ============================================================================
476
+ // B.4: nexus.workspace.agents() — Cross-project agent view
477
+ // ============================================================================
478
+
479
+ /**
480
+ * Get all agents registered across all projects.
481
+ *
482
+ * Queries each project's agent_instances table and returns a unified list.
483
+ */
484
+ export async function workspaceAgents(): Promise<WorkspaceAgent[]> {
485
+ const projects = await nexusList();
486
+ const agents: WorkspaceAgent[] = [];
487
+
488
+ for (const project of projects) {
489
+ try {
490
+ const { listAgentInstances } = await import('../agents/registry.js');
491
+ const instances = await listAgentInstances(undefined, project.path);
492
+ for (const inst of instances) {
493
+ agents.push({
494
+ agentId: inst.id,
495
+ agentType: inst.agentType,
496
+ status: inst.status,
497
+ project: project.name,
498
+ taskId: inst.taskId ?? null,
499
+ lastHeartbeat: inst.lastHeartbeat,
500
+ });
501
+ }
502
+ } catch {
503
+ // Skip projects without agent support
504
+ }
505
+ }
506
+
507
+ return agents;
508
+ }
@@ -170,10 +170,10 @@ export async function startSession(
170
170
  });
171
171
  }
172
172
 
173
- // Dispatch onSessionStart hook (best-effort, don't await)
173
+ // Dispatch SessionStart hook (best-effort, don't await)
174
174
  const { hooks } = await import('../hooks/registry.js');
175
175
  hooks
176
- .dispatch('onSessionStart', cwd ?? process.cwd(), {
176
+ .dispatch('SessionStart', cwd ?? process.cwd(), {
177
177
  timestamp: new Date().toISOString(),
178
178
  sessionId: session.id,
179
179
  name: options.name,
@@ -233,10 +233,10 @@ export async function endSession(
233
233
 
234
234
  const duration = Math.floor((Date.now() - new Date(session.startedAt).getTime()) / 1000);
235
235
 
236
- // Dispatch onSessionEnd hook (best-effort, don't await)
236
+ // Dispatch SessionEnd hook (best-effort, don't await)
237
237
  const { hooks } = await import('../hooks/registry.js');
238
238
  hooks
239
- .dispatch('onSessionEnd', cwd ?? process.cwd(), {
239
+ .dispatch('SessionEnd', cwd ?? process.cwd(), {
240
240
  timestamp: new Date().toISOString(),
241
241
  sessionId: session.id,
242
242
  duration,
@@ -209,7 +209,9 @@ export async function serializeSession(
209
209
  status: task.status,
210
210
  priority: task.priority ?? 'medium',
211
211
  description: desc.length > maxDescLen ? desc.slice(0, maxDescLen) + '...' : desc,
212
- acceptance: Array.isArray(task.acceptance) ? task.acceptance.join('\n') : (task.acceptance ?? undefined),
212
+ acceptance: Array.isArray(task.acceptance)
213
+ ? task.acceptance.join('\n')
214
+ : (task.acceptance ?? undefined),
213
215
  };
214
216
  }
215
217
  } catch {
@@ -324,7 +326,7 @@ export async function restoreSession(
324
326
  // Dispatch hook (best-effort)
325
327
  try {
326
328
  const { hooks } = await import('../hooks/registry.js');
327
- await hooks.dispatch('onSessionStart', projectRoot, {
329
+ await hooks.dispatch('SessionStart', projectRoot, {
328
330
  timestamp: new Date().toISOString(),
329
331
  sessionId: restoredSession.id,
330
332
  name: restoredSession.name,
package/src/store/json.ts CHANGED
@@ -100,10 +100,10 @@ export async function saveJson(
100
100
  // Atomic write
101
101
  await atomicWriteJson(filePath, data, { indent: options?.indent });
102
102
 
103
- // Dispatch onFileChange hook (best-effort, fire-and-forget)
103
+ // Dispatch Notification hook (best-effort, fire-and-forget)
104
104
  import('../hooks/registry.js')
105
105
  .then(({ hooks: h }) =>
106
- h.dispatch('onFileChange', process.cwd(), {
106
+ h.dispatch('Notification', process.cwd(), {
107
107
  timestamp: new Date().toISOString(),
108
108
  filePath,
109
109
  changeType: 'write' as const,
@@ -123,10 +123,10 @@ export async function startTask(
123
123
  accessor,
124
124
  );
125
125
 
126
- // Dispatch onToolStart hook (best-effort, don't await)
126
+ // Dispatch PreToolUse hook (best-effort, don't await)
127
127
  const { hooks } = await import('../hooks/registry.js');
128
128
  hooks
129
- .dispatch('onToolStart', cwd ?? process.cwd(), {
129
+ .dispatch('PreToolUse', cwd ?? process.cwd(), {
130
130
  timestamp: new Date().toISOString(),
131
131
  taskId,
132
132
  taskTitle: task.title,
@@ -169,11 +169,11 @@ export async function stopTask(
169
169
 
170
170
  const now = new Date().toISOString();
171
171
 
172
- // Dispatch onToolComplete hook (best-effort, don't await)
172
+ // Dispatch PostToolUse hook (best-effort, don't await)
173
173
  if (taskId && task) {
174
174
  const { hooks } = await import('../hooks/registry.js');
175
175
  hooks
176
- .dispatch('onToolComplete', cwd ?? process.cwd(), {
176
+ .dispatch('PostToolUse', cwd ?? process.cwd(), {
177
177
  timestamp: now,
178
178
  taskId,
179
179
  taskTitle: task.title,