@claudetools/tools 0.8.3 → 0.8.5
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/dist/cli.js +41 -0
- package/dist/context/deduplication.d.ts +72 -0
- package/dist/context/deduplication.js +77 -0
- package/dist/context/deduplication.test.d.ts +6 -0
- package/dist/context/deduplication.test.js +84 -0
- package/dist/context/emergency-eviction.d.ts +73 -0
- package/dist/context/emergency-eviction.example.d.ts +13 -0
- package/dist/context/emergency-eviction.example.js +94 -0
- package/dist/context/emergency-eviction.js +226 -0
- package/dist/context/eviction-engine.d.ts +76 -0
- package/dist/context/eviction-engine.example.d.ts +7 -0
- package/dist/context/eviction-engine.example.js +144 -0
- package/dist/context/eviction-engine.js +176 -0
- package/dist/context/example-usage.d.ts +1 -0
- package/dist/context/example-usage.js +128 -0
- package/dist/context/exchange-summariser.d.ts +80 -0
- package/dist/context/exchange-summariser.js +261 -0
- package/dist/context/health-monitor.d.ts +97 -0
- package/dist/context/health-monitor.example.d.ts +1 -0
- package/dist/context/health-monitor.example.js +164 -0
- package/dist/context/health-monitor.js +210 -0
- package/dist/context/importance-scorer.d.ts +94 -0
- package/dist/context/importance-scorer.example.d.ts +1 -0
- package/dist/context/importance-scorer.example.js +140 -0
- package/dist/context/importance-scorer.js +187 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.js +16 -0
- package/dist/context/session-helper.d.ts +10 -0
- package/dist/context/session-helper.js +51 -0
- package/dist/context/session-store.d.ts +94 -0
- package/dist/context/session-store.js +286 -0
- package/dist/context/usage-estimator.d.ts +131 -0
- package/dist/context/usage-estimator.js +260 -0
- package/dist/context/usage-estimator.test.d.ts +1 -0
- package/dist/context/usage-estimator.test.js +208 -0
- package/dist/context-cli.d.ts +16 -0
- package/dist/context-cli.js +309 -0
- package/dist/handlers/codedna-handlers.d.ts +1 -1
- package/dist/handlers/tool-handlers.js +215 -13
- package/dist/helpers/api-client.d.ts +5 -1
- package/dist/helpers/api-client.js +3 -1
- package/dist/helpers/circuit-breaker.d.ts +28 -0
- package/dist/helpers/circuit-breaker.js +97 -0
- package/dist/helpers/compact-formatter.d.ts +2 -0
- package/dist/helpers/compact-formatter.js +6 -0
- package/dist/helpers/error-tracking.js +1 -1
- package/dist/helpers/tasks-retry.d.ts +9 -0
- package/dist/helpers/tasks-retry.js +30 -0
- package/dist/helpers/tasks.d.ts +91 -5
- package/dist/helpers/tasks.js +261 -16
- package/dist/helpers/usage-analytics.js +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/post-tool-use-hook-cli.d.ts +2 -0
- package/dist/hooks/post-tool-use-hook-cli.js +34 -0
- package/dist/hooks/post-tool-use.d.ts +67 -0
- package/dist/hooks/post-tool-use.js +234 -0
- package/dist/hooks/stop-hook-cli.d.ts +2 -0
- package/dist/hooks/stop-hook-cli.js +34 -0
- package/dist/hooks/stop.d.ts +64 -0
- package/dist/hooks/stop.js +192 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +4 -0
- package/dist/setup.js +206 -2
- package/dist/tools.js +107 -2
- package/package.json +19 -18
- package/scripts/verify-prompt-compliance.sh +0 -0
package/dist/helpers/tasks.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// =============================================================================
|
|
4
4
|
import { apiRequest } from './api-client.js';
|
|
5
5
|
import { matchTaskToWorker, buildWorkerPrompt } from './workers.js';
|
|
6
|
+
import { isCircuitOpen } from './circuit-breaker.js';
|
|
6
7
|
export function parseJsonArray(value) {
|
|
7
8
|
if (!value)
|
|
8
9
|
return [];
|
|
@@ -51,11 +52,12 @@ export async function getTask(userId, projectId, taskId, full = false) {
|
|
|
51
52
|
const endpoint = full ? 'full' : '';
|
|
52
53
|
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}${endpoint ? `/${endpoint}` : ''}`);
|
|
53
54
|
}
|
|
54
|
-
export async function claimTask(userId, projectId, taskId, agentId, lockDurationMinutes
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lock_duration_minutes
|
|
58
|
-
}
|
|
55
|
+
export async function claimTask(userId, projectId, taskId, agentId, lockDurationMinutes) {
|
|
56
|
+
const body = { agent_id: agentId };
|
|
57
|
+
if (lockDurationMinutes !== undefined) {
|
|
58
|
+
body.lock_duration_minutes = lockDurationMinutes;
|
|
59
|
+
}
|
|
60
|
+
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/claim`, 'POST', body);
|
|
59
61
|
}
|
|
60
62
|
export async function releaseTask(userId, projectId, taskId, agentId, newStatus, workLog) {
|
|
61
63
|
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/release`, 'POST', {
|
|
@@ -90,11 +92,29 @@ export async function heartbeatTask(userId, projectId, taskId, agentId, extendMi
|
|
|
90
92
|
// -----------------------------------------------------------------------------
|
|
91
93
|
// Orchestration Functions
|
|
92
94
|
// -----------------------------------------------------------------------------
|
|
93
|
-
//
|
|
94
|
-
export const
|
|
95
|
+
// Effort weights for capacity planning
|
|
96
|
+
export const EFFORT_WEIGHTS = {
|
|
97
|
+
xs: 1,
|
|
98
|
+
s: 2,
|
|
99
|
+
m: 4,
|
|
100
|
+
l: 8,
|
|
101
|
+
xl: 16,
|
|
102
|
+
};
|
|
103
|
+
// Default lock duration by effort size (in minutes)
|
|
104
|
+
// Used when no explicit lockDurationMinutes is provided to claimTask
|
|
105
|
+
export const LOCK_DURATION_BY_EFFORT = {
|
|
106
|
+
xs: 15, // Extra small: 15 minutes
|
|
107
|
+
s: 30, // Small: 30 minutes (default)
|
|
108
|
+
m: 60, // Medium: 1 hour
|
|
109
|
+
l: 120, // Large: 2 hours
|
|
110
|
+
xl: 240, // Extra large: 4 hours
|
|
111
|
+
};
|
|
112
|
+
// Configuration: Default capacity budget in effort units
|
|
113
|
+
// 20 units allows: 20 xs tasks OR 1 xl + 1 m task OR 5 m tasks, etc.
|
|
114
|
+
export const DEFAULT_CAPACITY_BUDGET = 20;
|
|
95
115
|
/**
|
|
96
|
-
* Get
|
|
97
|
-
* Returns both in_progress and claimed (locked) task counts
|
|
116
|
+
* Get effort-weighted load of currently active tasks
|
|
117
|
+
* Returns both in_progress and claimed (locked) task counts, plus effort-weighted load
|
|
98
118
|
*/
|
|
99
119
|
export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
100
120
|
// Get all in_progress tasks
|
|
@@ -105,10 +125,15 @@ export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
|
105
125
|
});
|
|
106
126
|
let inProgressCount = 0;
|
|
107
127
|
let claimedCount = 0;
|
|
128
|
+
let effortLoad = 0;
|
|
108
129
|
if (inProgressResult.success && inProgressResult.data.length > 0) {
|
|
109
130
|
const now = new Date();
|
|
110
131
|
for (const task of inProgressResult.data) {
|
|
111
132
|
inProgressCount++;
|
|
133
|
+
// Calculate effort weight (default to 'm' = 4 units if not specified)
|
|
134
|
+
const effort = task.estimated_effort || 'm';
|
|
135
|
+
const weight = EFFORT_WEIGHTS[effort] || EFFORT_WEIGHTS.m;
|
|
136
|
+
effortLoad += weight;
|
|
112
137
|
// Check if task is also claimed (has valid lock)
|
|
113
138
|
if (task.assigned_to && task.lock_expires_at) {
|
|
114
139
|
const lockExpires = new Date(task.lock_expires_at);
|
|
@@ -122,6 +147,7 @@ export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
|
122
147
|
inProgress: inProgressCount,
|
|
123
148
|
claimed: claimedCount,
|
|
124
149
|
total: inProgressCount,
|
|
150
|
+
effortLoad,
|
|
125
151
|
};
|
|
126
152
|
}
|
|
127
153
|
/**
|
|
@@ -130,13 +156,13 @@ export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
|
130
156
|
* - Excludes already claimed tasks
|
|
131
157
|
* - Resolves dependencies (only returns unblocked tasks)
|
|
132
158
|
* - Matches each to appropriate expert worker
|
|
133
|
-
* - Respects
|
|
159
|
+
* - Respects effort-weighted capacity budget
|
|
134
160
|
*/
|
|
135
|
-
export async function getDispatchableTasks(userId, projectId, epicId,
|
|
136
|
-
// Get current active task
|
|
161
|
+
export async function getDispatchableTasks(userId, projectId, epicId, capacityBudget = DEFAULT_CAPACITY_BUDGET) {
|
|
162
|
+
// Get current active task effort load
|
|
137
163
|
const activeCount = await getActiveTaskCount(userId, projectId, epicId);
|
|
138
|
-
// Calculate remaining capacity
|
|
139
|
-
|
|
164
|
+
// Calculate remaining effort capacity
|
|
165
|
+
let remainingCapacity = capacityBudget - activeCount.effortLoad;
|
|
140
166
|
if (remainingCapacity <= 0) {
|
|
141
167
|
// No capacity available, return empty array
|
|
142
168
|
return [];
|
|
@@ -178,6 +204,13 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
|
|
|
178
204
|
continue; // Still blocked
|
|
179
205
|
}
|
|
180
206
|
}
|
|
207
|
+
// Calculate task effort weight
|
|
208
|
+
const effort = task.estimated_effort || 'm';
|
|
209
|
+
const taskEffort = EFFORT_WEIGHTS[effort] || EFFORT_WEIGHTS.m;
|
|
210
|
+
// Check if this task fits within remaining capacity
|
|
211
|
+
if (taskEffort > remainingCapacity) {
|
|
212
|
+
continue; // Task too large for remaining capacity
|
|
213
|
+
}
|
|
181
214
|
// Match to expert worker
|
|
182
215
|
const worker = matchTaskToWorker({
|
|
183
216
|
title: task.title,
|
|
@@ -185,6 +218,11 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
|
|
|
185
218
|
domain: task.domain || undefined,
|
|
186
219
|
tags: parseJsonArray(task.tags),
|
|
187
220
|
});
|
|
221
|
+
// Circuit breaker check: skip if worker circuit is open
|
|
222
|
+
if (isCircuitOpen(worker.id)) {
|
|
223
|
+
console.warn(`[Circuit Breaker] Skipping task ${task.id} - circuit OPEN for worker type: ${worker.id}`);
|
|
224
|
+
continue; // Skip this task
|
|
225
|
+
}
|
|
188
226
|
// Get task context
|
|
189
227
|
const fullTask = await getTask(userId, projectId, task.id, true);
|
|
190
228
|
const taskData = fullTask.data;
|
|
@@ -195,13 +233,48 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
|
|
|
195
233
|
parentContext: taskData.parent,
|
|
196
234
|
dependencies: [],
|
|
197
235
|
});
|
|
198
|
-
//
|
|
199
|
-
|
|
236
|
+
// Reduce remaining capacity by the effort of this task
|
|
237
|
+
remainingCapacity -= taskEffort;
|
|
238
|
+
// Stop if no capacity remains
|
|
239
|
+
if (remainingCapacity <= 0) {
|
|
200
240
|
break;
|
|
201
241
|
}
|
|
202
242
|
}
|
|
243
|
+
// Sort by priority: critical → high → medium → low
|
|
244
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
245
|
+
dispatchable.sort((a, b) => (priorityOrder[a.task.priority] ?? 2) - (priorityOrder[b.task.priority] ?? 2));
|
|
203
246
|
return dispatchable;
|
|
204
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Check if task lock is about to expire and return warning
|
|
250
|
+
*/
|
|
251
|
+
export function getLockExpiryWarning(lockExpiresAt) {
|
|
252
|
+
if (!lockExpiresAt) {
|
|
253
|
+
return { warning: false, minutes_remaining: 0 };
|
|
254
|
+
}
|
|
255
|
+
const now = new Date();
|
|
256
|
+
const expiryTime = new Date(lockExpiresAt);
|
|
257
|
+
const secondsRemaining = (expiryTime.getTime() - now.getTime()) / 1000;
|
|
258
|
+
const minutesRemaining = Math.floor(secondsRemaining / 60);
|
|
259
|
+
// Expired (lock time has passed)
|
|
260
|
+
if (secondsRemaining <= 0) {
|
|
261
|
+
return {
|
|
262
|
+
warning: true,
|
|
263
|
+
minutes_remaining: 0,
|
|
264
|
+
message: '🚨 Lock has EXPIRED. Task may be claimed by another agent.',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// Warn if lock expires within 5 minutes (but not yet expired)
|
|
268
|
+
if (minutesRemaining <= 5 && minutesRemaining >= 1) {
|
|
269
|
+
return {
|
|
270
|
+
warning: true,
|
|
271
|
+
minutes_remaining: minutesRemaining,
|
|
272
|
+
message: `⚠️ Lock expires in ${minutesRemaining} minute${minutesRemaining !== 1 ? 's' : ''}. Call task_heartbeat to extend.`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// Less than 1 minute but not expired - no warning (edge case)
|
|
276
|
+
return { warning: false, minutes_remaining: minutesRemaining };
|
|
277
|
+
}
|
|
205
278
|
/**
|
|
206
279
|
* Get full execution context for a worker agent
|
|
207
280
|
*/
|
|
@@ -251,6 +324,8 @@ export async function getExecutionContext(userId, projectId, taskId) {
|
|
|
251
324
|
status: s.status,
|
|
252
325
|
})),
|
|
253
326
|
});
|
|
327
|
+
// Check for lock expiry warning
|
|
328
|
+
const lockWarning = getLockExpiryWarning(task.lock_expires_at);
|
|
254
329
|
return {
|
|
255
330
|
task,
|
|
256
331
|
worker,
|
|
@@ -258,6 +333,7 @@ export async function getExecutionContext(userId, projectId, taskId) {
|
|
|
258
333
|
context: taskData.context,
|
|
259
334
|
parentTask: taskData.parent,
|
|
260
335
|
siblingTasks,
|
|
336
|
+
lockWarning,
|
|
261
337
|
};
|
|
262
338
|
}
|
|
263
339
|
/**
|
|
@@ -303,6 +379,81 @@ export async function resolveTaskDependencies(userId, projectId, completedTaskId
|
|
|
303
379
|
}
|
|
304
380
|
return newlyUnblocked;
|
|
305
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Aggregate work_log context from all tasks in an epic
|
|
384
|
+
* Used by orchestrator to synthesize final results
|
|
385
|
+
*/
|
|
386
|
+
export async function getEpicAggregate(userId, projectId, epicId, includePending = false) {
|
|
387
|
+
// Get the epic
|
|
388
|
+
const epicResult = await getTask(userId, projectId, epicId);
|
|
389
|
+
if (!epicResult.success) {
|
|
390
|
+
throw new Error(`Epic not found: ${epicId}`);
|
|
391
|
+
}
|
|
392
|
+
const epic = epicResult.data;
|
|
393
|
+
// Verify it's an epic
|
|
394
|
+
if (epic.type !== 'epic') {
|
|
395
|
+
throw new Error(`Task ${epicId} is not an epic (type: ${epic.type})`);
|
|
396
|
+
}
|
|
397
|
+
// Get all child tasks
|
|
398
|
+
const tasksResult = await listTasks(userId, projectId, {
|
|
399
|
+
parent_id: epicId,
|
|
400
|
+
limit: 100,
|
|
401
|
+
});
|
|
402
|
+
if (!tasksResult.success) {
|
|
403
|
+
return {
|
|
404
|
+
epic: {
|
|
405
|
+
id: epic.id,
|
|
406
|
+
title: epic.title,
|
|
407
|
+
description: epic.description,
|
|
408
|
+
status: epic.status,
|
|
409
|
+
},
|
|
410
|
+
tasks: [],
|
|
411
|
+
summary_stats: {
|
|
412
|
+
total: 0,
|
|
413
|
+
completed: 0,
|
|
414
|
+
in_progress: 0,
|
|
415
|
+
pending: 0,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const allTasks = tasksResult.data;
|
|
420
|
+
// Get work_log context for each task
|
|
421
|
+
const tasksWithLogs = await Promise.all(allTasks.map(async (task) => {
|
|
422
|
+
// Get full task data to access context
|
|
423
|
+
const fullTaskResult = await getTask(userId, projectId, task.id, true);
|
|
424
|
+
const fullTaskData = fullTaskResult.data;
|
|
425
|
+
// Find work_log context
|
|
426
|
+
const workLogContext = fullTaskData.context?.find(ctx => ctx.context_type === 'work_log');
|
|
427
|
+
return {
|
|
428
|
+
id: task.id,
|
|
429
|
+
title: task.title,
|
|
430
|
+
status: task.status,
|
|
431
|
+
work_log: workLogContext?.content || null,
|
|
432
|
+
completed_at: task.completed_at,
|
|
433
|
+
};
|
|
434
|
+
}));
|
|
435
|
+
// Filter based on includePending
|
|
436
|
+
const filteredTasks = includePending
|
|
437
|
+
? tasksWithLogs
|
|
438
|
+
: tasksWithLogs.filter(t => t.status === 'done' || t.status === 'in_progress');
|
|
439
|
+
// Calculate stats
|
|
440
|
+
const stats = {
|
|
441
|
+
total: allTasks.length,
|
|
442
|
+
completed: allTasks.filter(t => t.status === 'done').length,
|
|
443
|
+
in_progress: allTasks.filter(t => t.status === 'in_progress').length,
|
|
444
|
+
pending: allTasks.filter(t => t.status === 'ready' || t.status === 'backlog').length,
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
epic: {
|
|
448
|
+
id: epic.id,
|
|
449
|
+
title: epic.title,
|
|
450
|
+
description: epic.description,
|
|
451
|
+
status: epic.status,
|
|
452
|
+
},
|
|
453
|
+
tasks: filteredTasks,
|
|
454
|
+
summary_stats: stats,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
306
457
|
/**
|
|
307
458
|
* Get epic status with progress tracking
|
|
308
459
|
* Auto-completes epic if all child tasks are done
|
|
@@ -368,3 +519,97 @@ export async function getEpicStatus(userId, projectId, epicId) {
|
|
|
368
519
|
autoCompleted,
|
|
369
520
|
};
|
|
370
521
|
}
|
|
522
|
+
export async function handoffTask(userId, projectId, taskId, agentId, newWorkerType, reason) {
|
|
523
|
+
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/handoff`, 'POST', {
|
|
524
|
+
agent_id: agentId,
|
|
525
|
+
new_worker_type: newWorkerType,
|
|
526
|
+
reason,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Revise an existing epic by adding/removing/updating tasks
|
|
531
|
+
* Enables iterative planning without recreating the entire epic
|
|
532
|
+
*/
|
|
533
|
+
export async function reviseEpic(userId, projectId, epicId, changes) {
|
|
534
|
+
// Verify epic exists and is not done
|
|
535
|
+
const epicResult = await getTask(userId, projectId, epicId);
|
|
536
|
+
if (!epicResult.success) {
|
|
537
|
+
throw new Error(`Epic not found: ${epicId}`);
|
|
538
|
+
}
|
|
539
|
+
const epic = epicResult.data;
|
|
540
|
+
if (epic.type !== 'epic') {
|
|
541
|
+
throw new Error(`Task ${epicId} is not an epic (type: ${epic.type})`);
|
|
542
|
+
}
|
|
543
|
+
if (epic.status === 'done') {
|
|
544
|
+
throw new Error(`Cannot revise completed epic: ${epicId}`);
|
|
545
|
+
}
|
|
546
|
+
const added = [];
|
|
547
|
+
const removed = [];
|
|
548
|
+
const updated = [];
|
|
549
|
+
// Add new tasks
|
|
550
|
+
if (changes.add_tasks && changes.add_tasks.length > 0) {
|
|
551
|
+
for (const taskDef of changes.add_tasks) {
|
|
552
|
+
const taskResult = await createTask(userId, projectId, {
|
|
553
|
+
type: 'task',
|
|
554
|
+
title: taskDef.title,
|
|
555
|
+
description: taskDef.description,
|
|
556
|
+
parent_id: epicId,
|
|
557
|
+
priority: epic.priority,
|
|
558
|
+
estimated_effort: taskDef.effort,
|
|
559
|
+
blocked_by: taskDef.blocked_by,
|
|
560
|
+
domain: taskDef.domain,
|
|
561
|
+
});
|
|
562
|
+
added.push(taskResult.data);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Remove tasks (set to cancelled status)
|
|
566
|
+
if (changes.remove_task_ids && changes.remove_task_ids.length > 0) {
|
|
567
|
+
for (const taskId of changes.remove_task_ids) {
|
|
568
|
+
try {
|
|
569
|
+
await updateTaskStatus(userId, projectId, taskId, 'cancelled', 'system:epic-revision');
|
|
570
|
+
removed.push(taskId);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
// Log but continue - task may not exist
|
|
574
|
+
console.warn(`Failed to cancel task ${taskId}:`, error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Update existing tasks
|
|
579
|
+
// Note: The API doesn't support PATCH for tasks, so we can only update status
|
|
580
|
+
// For title/description/effort updates, we'll need to add context explaining the change
|
|
581
|
+
if (changes.update_tasks && changes.update_tasks.length > 0) {
|
|
582
|
+
for (const update of changes.update_tasks) {
|
|
583
|
+
try {
|
|
584
|
+
const taskResult = await getTask(userId, projectId, update.task_id);
|
|
585
|
+
if (!taskResult.success)
|
|
586
|
+
continue;
|
|
587
|
+
const task = taskResult.data;
|
|
588
|
+
// Add context about the updates since we can't modify fields directly
|
|
589
|
+
const updates = [];
|
|
590
|
+
if (update.title && update.title !== task.title) {
|
|
591
|
+
updates.push(`Title updated: "${task.title}" → "${update.title}"`);
|
|
592
|
+
}
|
|
593
|
+
if (update.description && update.description !== task.description) {
|
|
594
|
+
updates.push(`Description updated: "${update.description}"`);
|
|
595
|
+
}
|
|
596
|
+
if (update.effort && update.effort !== task.estimated_effort) {
|
|
597
|
+
updates.push(`Effort updated: ${task.estimated_effort || 'unset'} → ${update.effort}`);
|
|
598
|
+
}
|
|
599
|
+
if (updates.length > 0) {
|
|
600
|
+
await addTaskContext(userId, projectId, update.task_id, 'decision', updates.join('\n'), 'task_plan_revise', 'epic-revision');
|
|
601
|
+
updated.push(task);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.warn(`Failed to update task ${update.task_id}:`, error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
epic,
|
|
611
|
+
added,
|
|
612
|
+
removed,
|
|
613
|
+
updated,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
@@ -29,7 +29,7 @@ export class UsageAnalytics {
|
|
|
29
29
|
const userId = DEFAULT_USER_ID;
|
|
30
30
|
const projectId = event.projectId || resolveProjectId();
|
|
31
31
|
// Store as fact in memory system
|
|
32
|
-
await storeFact(projectId, 'CodeDNA', 'GENERATION_COMPLETED', event.generator || event.operation, JSON.stringify(usageEvent), userId);
|
|
32
|
+
await storeFact(projectId, 'CodeDNA', 'GENERATION_COMPLETED', event.generator || event.operation, JSON.stringify(usageEvent), { userId });
|
|
33
33
|
console.log('[CodeDNA Analytics]', {
|
|
34
34
|
operation: event.operation,
|
|
35
35
|
generator: event.generator,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { handleStopHook, sessionStore, usageEstimator } from './stop.js';
|
|
2
|
+
export type { StopHookInput, SessionData, ContextHealth } from './stop.js';
|
|
3
|
+
export { handlePostToolUseHook, sessionStore as postToolUseSessionStore, evictionEngine, } from './post-tool-use.js';
|
|
4
|
+
export type { PostToolUseInput, SessionData as PostToolUseSessionData, EvictionTrigger, } from './post-tool-use.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Claude Code Hooks - Index
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Exports hook handlers for Claude Code integration
|
|
5
|
+
export { handleStopHook, sessionStore, usageEstimator } from './stop.js';
|
|
6
|
+
export { handlePostToolUseHook, sessionStore as postToolUseSessionStore, evictionEngine, } from './post-tool-use.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Post-Tool-Use Hook CLI Entry Point
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Called by Claude Code when PostToolUse event occurs
|
|
6
|
+
// Reads hook input from stdin, processes it, and outputs to stdout/stderr
|
|
7
|
+
import { handlePostToolUseHook } from './post-tool-use.js';
|
|
8
|
+
async function main() {
|
|
9
|
+
try {
|
|
10
|
+
// Read JSON input from stdin
|
|
11
|
+
let inputData = '';
|
|
12
|
+
// Set up stdin reading
|
|
13
|
+
process.stdin.setEncoding('utf8');
|
|
14
|
+
for await (const chunk of process.stdin) {
|
|
15
|
+
inputData += chunk;
|
|
16
|
+
}
|
|
17
|
+
// Parse hook input
|
|
18
|
+
const hookInput = JSON.parse(inputData);
|
|
19
|
+
// Validate hook event
|
|
20
|
+
if (hookInput.hook_event_name !== 'PostToolUse') {
|
|
21
|
+
console.error(`Error: Expected PostToolUse event, got ${hookInput.hook_event_name}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
// Process the hook
|
|
25
|
+
await handlePostToolUseHook(hookInput);
|
|
26
|
+
// Success - exit code 0
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('Error processing PostToolUse hook:', error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
main();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
interface PostToolUseInput {
|
|
2
|
+
session_id?: string;
|
|
3
|
+
hook_event_name: 'PostToolUse';
|
|
4
|
+
tool_name: string;
|
|
5
|
+
tool_input?: Record<string, unknown>;
|
|
6
|
+
tool_output?: string;
|
|
7
|
+
post_tool_use_active?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface EvictionTrigger {
|
|
10
|
+
triggered: boolean;
|
|
11
|
+
level: 'none' | 'standard' | 'emergency';
|
|
12
|
+
estimated_fill: number;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
interface SessionData {
|
|
16
|
+
session_id: string;
|
|
17
|
+
tool_executions: number;
|
|
18
|
+
estimated_tokens: number;
|
|
19
|
+
context_limit: number;
|
|
20
|
+
estimated_fill: number;
|
|
21
|
+
last_updated: Date;
|
|
22
|
+
tool_output_tokens: number;
|
|
23
|
+
}
|
|
24
|
+
declare class SessionStore {
|
|
25
|
+
/**
|
|
26
|
+
* Get or create session data
|
|
27
|
+
*/
|
|
28
|
+
getSession(sessionId: string): SessionData;
|
|
29
|
+
/**
|
|
30
|
+
* Update session with tool output tokens
|
|
31
|
+
*/
|
|
32
|
+
updateSession(sessionId: string, outputTokens: number): SessionData;
|
|
33
|
+
/**
|
|
34
|
+
* Clear session (for cleanup)
|
|
35
|
+
*/
|
|
36
|
+
clearSession(sessionId: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get all sessions (for debugging)
|
|
39
|
+
*/
|
|
40
|
+
getAllSessions(): SessionData[];
|
|
41
|
+
}
|
|
42
|
+
declare const sessionStore: SessionStore;
|
|
43
|
+
declare class EvictionTriggerEngine {
|
|
44
|
+
/**
|
|
45
|
+
* Check if eviction should be triggered based on fill percentage
|
|
46
|
+
*/
|
|
47
|
+
checkEvictionTrigger(session: SessionData): EvictionTrigger;
|
|
48
|
+
/**
|
|
49
|
+
* Run standard eviction cycle
|
|
50
|
+
* NOTE: This is a placeholder - actual implementation would call into
|
|
51
|
+
* the context rotation/eviction system when it's built
|
|
52
|
+
*/
|
|
53
|
+
runStandardEviction(session: SessionData): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Run emergency eviction cycle
|
|
56
|
+
* More aggressive eviction for critical situations
|
|
57
|
+
*/
|
|
58
|
+
runEmergencyEviction(session: SessionData): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
declare const evictionEngine: EvictionTriggerEngine;
|
|
61
|
+
/**
|
|
62
|
+
* Handle PostToolUse hook event
|
|
63
|
+
* Updates session state with tool output and triggers eviction if needed
|
|
64
|
+
*/
|
|
65
|
+
export declare function handlePostToolUseHook(input: PostToolUseInput): Promise<void>;
|
|
66
|
+
export { sessionStore, evictionEngine };
|
|
67
|
+
export type { PostToolUseInput, SessionData, EvictionTrigger };
|