claude-mycelium 2.0.0 → 2.2.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/.agent-meta/_inhibitors.ndjson +1287 -0
- package/.agent-meta/_quarantine.json +45 -0
- package/.agent-meta/config.json +9 -0
- package/.agent-meta/tasks/_active.json +4 -0
- package/.agent-meta/tasks/task_0657b028-05a0-4b0c-b0b9-a4eae3d66cd9.json +168 -0
- package/.claude/memory.db +0 -0
- package/.claude/settings.local.json +4 -1
- package/README.md +85 -233
- package/SECURITY.md +145 -0
- package/dist/agent/task-worker.d.ts +11 -0
- package/dist/agent/task-worker.d.ts.map +1 -0
- package/dist/agent/task-worker.js +173 -0
- package/dist/agent/task-worker.js.map +1 -0
- package/dist/agent/worker.d.ts +8 -0
- package/dist/agent/worker.d.ts.map +1 -0
- package/dist/agent/worker.js +97 -0
- package/dist/agent/worker.js.map +1 -0
- package/dist/bin.d.ts +7 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/cost.d.ts +10 -0
- package/dist/cli/cost.d.ts.map +1 -0
- package/dist/cli/cost.js +163 -0
- package/dist/cli/cost.js.map +1 -0
- package/dist/cli/gc.d.ts +10 -0
- package/dist/cli/gc.d.ts.map +1 -0
- package/dist/cli/gc.js +108 -0
- package/dist/cli/gc.js.map +1 -0
- package/dist/cli/gradients.d.ts +10 -0
- package/dist/cli/gradients.d.ts.map +1 -0
- package/dist/cli/gradients.js +70 -0
- package/dist/cli/gradients.js.map +1 -0
- package/dist/cli/grow.d.ts +17 -0
- package/dist/cli/grow.d.ts.map +1 -0
- package/dist/cli/grow.js +373 -0
- package/dist/cli/grow.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +74 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +97 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/status.d.ts +10 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +191 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/coordination/file-locks.d.ts +42 -0
- package/dist/coordination/file-locks.d.ts.map +1 -0
- package/dist/coordination/file-locks.js +269 -0
- package/dist/coordination/file-locks.js.map +1 -0
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +4 -0
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/inhibitors.d.ts +84 -0
- package/dist/coordination/inhibitors.d.ts.map +1 -0
- package/dist/coordination/inhibitors.js +290 -0
- package/dist/coordination/inhibitors.js.map +1 -0
- package/dist/coordination/process-manager.d.ts +73 -0
- package/dist/coordination/process-manager.d.ts.map +1 -0
- package/dist/coordination/process-manager.js +144 -0
- package/dist/coordination/process-manager.js.map +1 -0
- package/dist/core/agent-executor.d.ts +4 -1
- package/dist/core/agent-executor.d.ts.map +1 -1
- package/dist/core/agent-executor.js +38 -12
- package/dist/core/agent-executor.js.map +1 -1
- package/dist/core/change-applier.d.ts +29 -5
- package/dist/core/change-applier.d.ts.map +1 -1
- package/dist/core/change-applier.js +254 -24
- package/dist/core/change-applier.js.map +1 -1
- package/dist/core/signals/churn.d.ts.map +1 -1
- package/dist/core/signals/churn.js +6 -4
- package/dist/core/signals/churn.js.map +1 -1
- package/dist/core/signals/debt.d.ts.map +1 -1
- package/dist/core/signals/debt.js +4 -3
- package/dist/core/signals/debt.js.map +1 -1
- package/dist/cost/cost-tracker.d.ts.map +1 -1
- package/dist/cost/cost-tracker.js +2 -0
- package/dist/cost/cost-tracker.js.map +1 -1
- package/dist/gc/index.d.ts +17 -0
- package/dist/gc/index.d.ts.map +1 -0
- package/dist/gc/index.js +17 -0
- package/dist/gc/index.js.map +1 -0
- package/dist/gc/runner.d.ts +39 -0
- package/dist/gc/runner.d.ts.map +1 -0
- package/dist/gc/runner.js +277 -0
- package/dist/gc/runner.js.map +1 -0
- package/dist/gc/trace-compactor.d.ts +31 -0
- package/dist/gc/trace-compactor.d.ts.map +1 -0
- package/dist/gc/trace-compactor.js +162 -0
- package/dist/gc/trace-compactor.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/quarantine/explorer.d.ts +65 -0
- package/dist/quarantine/explorer.d.ts.map +1 -0
- package/dist/quarantine/explorer.js +175 -0
- package/dist/quarantine/explorer.js.map +1 -0
- package/dist/quarantine/index.d.ts +7 -0
- package/dist/quarantine/index.d.ts.map +1 -0
- package/dist/quarantine/index.js +7 -0
- package/dist/quarantine/index.js.map +1 -0
- package/dist/quarantine/manager.d.ts +75 -0
- package/dist/quarantine/manager.d.ts.map +1 -0
- package/dist/quarantine/manager.js +275 -0
- package/dist/quarantine/manager.js.map +1 -0
- package/dist/task/acceptance.d.ts +29 -0
- package/dist/task/acceptance.d.ts.map +1 -0
- package/dist/task/acceptance.js +228 -0
- package/dist/task/acceptance.js.map +1 -0
- package/dist/task/agent-coordinator.d.ts +40 -0
- package/dist/task/agent-coordinator.d.ts.map +1 -0
- package/dist/task/agent-coordinator.js +168 -0
- package/dist/task/agent-coordinator.js.map +1 -0
- package/dist/task/executor.d.ts +37 -0
- package/dist/task/executor.d.ts.map +1 -0
- package/dist/task/executor.js +462 -0
- package/dist/task/executor.js.map +1 -0
- package/dist/task/index.d.ts +12 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +12 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/planner.d.ts +21 -0
- package/dist/task/planner.d.ts.map +1 -0
- package/dist/task/planner.js +253 -0
- package/dist/task/planner.js.map +1 -0
- package/dist/task/storage.d.ts +46 -0
- package/dist/task/storage.d.ts.map +1 -0
- package/dist/task/storage.js +266 -0
- package/dist/task/storage.js.map +1 -0
- package/dist/trace/trace-event.d.ts +2 -18
- package/dist/trace/trace-event.d.ts.map +1 -1
- package/dist/trace/trace-event.js +6 -6
- package/dist/trace/trace-event.js.map +1 -1
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +54 -15
- package/dist/utils/file-utils.js.map +1 -1
- package/docs/PHASE5_IMPLEMENTATION.md +237 -0
- package/docs/PHASES-3-7-COMPLETE.md +177 -0
- package/docs/PHASE_4_COMPLETE.md +135 -0
- package/docs/PHASE_7_DELIVERABLES.md +295 -0
- package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
- package/docs/PHASE_7_SUMMARY.txt +195 -0
- package/docs/RELEASE-NOTES-v2.1.md +213 -0
- package/docs/ROADMAP.md +194 -107
- package/docs/SECURITY-AUDIT.md +387 -0
- package/docs/SNAPSHOT.md +59 -32
- package/docs/implementation/phase3-summary.md +220 -0
- package/package.json +27 -11
- package/src/agent/task-worker.ts +196 -0
- package/src/agent/worker.ts +111 -0
- package/src/bin.ts +13 -0
- package/src/cli/cost.ts +210 -0
- package/src/cli/gc.ts +138 -0
- package/src/cli/gradients.ts +97 -0
- package/src/cli/grow.ts +416 -0
- package/src/cli/index.ts +81 -0
- package/src/cli/init.ts +139 -0
- package/src/cli/status.ts +218 -0
- package/src/coordination/file-locks.ts +300 -0
- package/src/coordination/index.ts +4 -0
- package/src/coordination/inhibitors.ts +345 -0
- package/src/coordination/process-manager.ts +199 -0
- package/src/core/agent-executor.ts +37 -8
- package/src/core/signals/churn.ts +8 -5
- package/src/core/signals/debt.ts +4 -3
- package/src/cost/cost-tracker.ts +2 -0
- package/src/gc/index.ts +17 -0
- package/src/gc/runner.ts +314 -0
- package/src/gc/trace-compactor.ts +187 -0
- package/src/index.ts +7 -1
- package/src/prompts/index.ts +2 -1
- package/src/quarantine/explorer.ts +234 -0
- package/src/quarantine/index.ts +7 -0
- package/src/quarantine/manager.ts +336 -0
- package/src/task/acceptance.ts +267 -0
- package/src/task/agent-coordinator.ts +220 -0
- package/src/task/executor.ts +543 -0
- package/src/task/index.ts +38 -0
- package/src/task/planner.ts +294 -0
- package/src/task/storage.ts +332 -0
- package/src/trace/trace-event.ts +7 -26
- package/src/utils/file-utils.ts +61 -15
- package/tests/cli/gc.test.ts +206 -0
- package/tests/cli/init.test.ts +181 -0
- package/tests/cli/status.test.ts +282 -0
- package/tests/coordination/file-locks.test.ts +196 -0
- package/tests/coordination/inhibitors.test.ts +459 -0
- package/tests/coordination/integration.test.ts +195 -0
- package/tests/coordination/process-manager.test.ts +165 -0
- package/tests/gc/trace-compactor.test.ts +245 -0
- package/tests/integration/phase-7.test.ts +145 -0
- package/tests/quarantine/explorer.test.ts +381 -0
- package/tests/quarantine/manager.test.ts +399 -0
- package/tests/security/command-injection.test.ts +88 -0
- package/tests/security/path-traversal.test.ts +103 -0
- package/tests/task/acceptance.test.ts +411 -0
- package/tests/task/executor.test.ts +421 -0
- package/tests/task/planner.test.ts +359 -0
- package/tests/trace/trace-event.test.ts +62 -20
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Planning Implementation
|
|
3
|
+
* Per second-spec §12.1: Task Planning
|
|
4
|
+
*
|
|
5
|
+
* LLM-based task decomposition that generates a TaskPlan with steps,
|
|
6
|
+
* dependency analysis, and risk identification.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { callLLM } from '../llm/anthropic-client.js';
|
|
10
|
+
import { Task, TaskPlan, TaskStep } from '../types/index.js';
|
|
11
|
+
import { logDebug, logError } from '../utils/logger.js';
|
|
12
|
+
import fg from 'fast-glob';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Plan a task by using LLM to decompose it into steps
|
|
16
|
+
*/
|
|
17
|
+
export async function planTask(task: Task): Promise<TaskPlan> {
|
|
18
|
+
logDebug('Planning task', { taskId: task.id, description: task.description });
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Get existing files in target directory
|
|
22
|
+
const existingFiles = await getExistingFiles(task.target ?? 'src');
|
|
23
|
+
|
|
24
|
+
// Build prompts for LLM
|
|
25
|
+
const systemPrompt = buildPlanningSystemPrompt();
|
|
26
|
+
const userPrompt = buildPlanningUserPrompt(task, existingFiles);
|
|
27
|
+
|
|
28
|
+
// Call LLM to generate plan
|
|
29
|
+
const response = await callLLM({
|
|
30
|
+
prompt: userPrompt,
|
|
31
|
+
systemPrompt,
|
|
32
|
+
temperature: 0.3, // Lower temperature for more deterministic planning
|
|
33
|
+
maxTokens: 4000,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Parse JSON plan from response
|
|
37
|
+
const plan = parseTaskPlanFromResponse(response.content);
|
|
38
|
+
|
|
39
|
+
logDebug('Task plan generated', {
|
|
40
|
+
taskId: task.id,
|
|
41
|
+
steps: plan.steps.length,
|
|
42
|
+
complexity: plan.estimated_complexity,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return plan;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
48
|
+
logError('Failed to plan task', errorObj, { taskId: task.id });
|
|
49
|
+
throw new Error(`Task planning failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Analyze dependencies between task steps
|
|
55
|
+
*/
|
|
56
|
+
export function analyzeDependencies(steps: TaskStep[]): Map<number, number[]> {
|
|
57
|
+
const dependencies = new Map<number, number[]>();
|
|
58
|
+
|
|
59
|
+
for (const step of steps) {
|
|
60
|
+
// Direct dependencies from the step definition
|
|
61
|
+
dependencies.set(step.order, [...step.depends_on]);
|
|
62
|
+
|
|
63
|
+
// Detect implicit file dependencies
|
|
64
|
+
const implicitDeps = detectImplicitDependencies(step, steps);
|
|
65
|
+
const allDeps = new Set([...step.depends_on, ...implicitDeps]);
|
|
66
|
+
dependencies.set(step.order, Array.from(allDeps));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return dependencies;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Identify potential risks in the task plan
|
|
74
|
+
*/
|
|
75
|
+
export function identifyRisks(plan: TaskPlan): string[] {
|
|
76
|
+
const risks: string[] = [...plan.risks];
|
|
77
|
+
|
|
78
|
+
// Check for circular dependencies
|
|
79
|
+
if (hasCircularDependencies(plan.steps)) {
|
|
80
|
+
risks.push('Circular dependency detected in task steps');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for steps with many dependencies
|
|
84
|
+
const stepsWithManyDeps = plan.steps.filter(s => s.depends_on.length > 3);
|
|
85
|
+
if (stepsWithManyDeps.length > 0) {
|
|
86
|
+
risks.push(`${stepsWithManyDeps.length} step(s) have complex dependencies (>3 deps)`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for create mode on existing file patterns
|
|
90
|
+
const createSteps = plan.steps.filter(s => s.mode === 'create');
|
|
91
|
+
if (createSteps.length > 5) {
|
|
92
|
+
risks.push(`Many new files (${createSteps.length}) - may need more validation`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for high complexity tasks
|
|
96
|
+
if (plan.estimated_complexity === 'large' && plan.steps.length > 10) {
|
|
97
|
+
risks.push('Large task with many steps - consider breaking down further');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return risks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get existing files in the target directory
|
|
105
|
+
*/
|
|
106
|
+
async function getExistingFiles(target: string): Promise<string[]> {
|
|
107
|
+
try {
|
|
108
|
+
const pattern = `${target}/**/*.{ts,tsx,js,jsx}`;
|
|
109
|
+
const files = await fg(pattern, {
|
|
110
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/*.test.ts', '**/*.spec.ts'],
|
|
111
|
+
onlyFiles: true,
|
|
112
|
+
});
|
|
113
|
+
return files;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
116
|
+
logError('Failed to list existing files', errorObj, { target });
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build system prompt for task planning
|
|
123
|
+
*/
|
|
124
|
+
function buildPlanningSystemPrompt(): string {
|
|
125
|
+
return `You are a task planning agent.
|
|
126
|
+
|
|
127
|
+
Given a task description, create a step-by-step plan to accomplish it.
|
|
128
|
+
|
|
129
|
+
Output your plan as JSON in this exact format:
|
|
130
|
+
{
|
|
131
|
+
"summary": "Brief description of approach",
|
|
132
|
+
"steps": [
|
|
133
|
+
{
|
|
134
|
+
"order": 1,
|
|
135
|
+
"description": "What to do",
|
|
136
|
+
"target_file": "path/to/file.ts",
|
|
137
|
+
"mode": "create|error_reducer|complexity_reducer|debt_payer|stabilizer",
|
|
138
|
+
"depends_on": []
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"estimated_complexity": "small|medium|large",
|
|
142
|
+
"risks": ["potential risk 1", "potential risk 2"]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Rules:
|
|
146
|
+
- Prefer small, incremental steps over large changes
|
|
147
|
+
- Each step should be independently testable
|
|
148
|
+
- New files must include basic tests
|
|
149
|
+
- Follow existing code patterns
|
|
150
|
+
- mode "create" is for new files only
|
|
151
|
+
- Use error_reducer for fixing bugs
|
|
152
|
+
- Use complexity_reducer for simplifying code
|
|
153
|
+
- Use debt_payer for fixing lint/type issues
|
|
154
|
+
- Use stabilizer for adding tests/docs`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build user prompt for task planning
|
|
159
|
+
*/
|
|
160
|
+
function buildPlanningUserPrompt(task: Task, existingFiles: string[]): string {
|
|
161
|
+
const fileList = existingFiles.length > 50
|
|
162
|
+
? `${existingFiles.slice(0, 50).join('\n')}\n... and ${existingFiles.length - 50} more`
|
|
163
|
+
: existingFiles.join('\n') || '(no files found)';
|
|
164
|
+
|
|
165
|
+
const criteriaList = task.acceptance_criteria.length > 0
|
|
166
|
+
? task.acceptance_criteria.map(c => `- ${c.description}`).join('\n')
|
|
167
|
+
: '(none specified)';
|
|
168
|
+
|
|
169
|
+
return `## Task
|
|
170
|
+
${task.description}
|
|
171
|
+
|
|
172
|
+
## Target Directory
|
|
173
|
+
${task.target ?? 'src/'}
|
|
174
|
+
|
|
175
|
+
## Existing Files
|
|
176
|
+
${fileList}
|
|
177
|
+
|
|
178
|
+
## Acceptance Criteria
|
|
179
|
+
${criteriaList}
|
|
180
|
+
|
|
181
|
+
Create a plan to accomplish this task.`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse TaskPlan from LLM response
|
|
186
|
+
*/
|
|
187
|
+
function parseTaskPlanFromResponse(content: string): TaskPlan {
|
|
188
|
+
// Try to extract JSON from response
|
|
189
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
190
|
+
if (!jsonMatch) {
|
|
191
|
+
throw new Error('Failed to parse task plan from LLM response - no JSON found');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
196
|
+
|
|
197
|
+
// Validate required fields
|
|
198
|
+
if (!parsed.summary || !Array.isArray(parsed.steps) || !parsed.estimated_complexity) {
|
|
199
|
+
throw new Error('Invalid task plan structure - missing required fields');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Validate each step
|
|
203
|
+
for (const step of parsed.steps) {
|
|
204
|
+
if (typeof step.order !== 'number' ||
|
|
205
|
+
!step.description ||
|
|
206
|
+
!step.target_file ||
|
|
207
|
+
!step.mode) {
|
|
208
|
+
throw new Error('Invalid task step structure');
|
|
209
|
+
}
|
|
210
|
+
// Ensure depends_on is an array
|
|
211
|
+
step.depends_on = step.depends_on || [];
|
|
212
|
+
step.completed = false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Ensure risks is an array
|
|
216
|
+
parsed.risks = parsed.risks || [];
|
|
217
|
+
|
|
218
|
+
return parsed as TaskPlan;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new Error(`Failed to parse task plan JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Detect implicit dependencies between steps based on file relationships
|
|
226
|
+
*/
|
|
227
|
+
function detectImplicitDependencies(step: TaskStep, allSteps: TaskStep[]): number[] {
|
|
228
|
+
const implicitDeps: number[] = [];
|
|
229
|
+
|
|
230
|
+
// If this step modifies a file, it depends on the step that creates it
|
|
231
|
+
if (step.mode !== 'create') {
|
|
232
|
+
const createStep = allSteps.find(
|
|
233
|
+
s => s.mode === 'create' &&
|
|
234
|
+
s.target_file === step.target_file &&
|
|
235
|
+
s.order < step.order
|
|
236
|
+
);
|
|
237
|
+
if (createStep && !step.depends_on.includes(createStep.order)) {
|
|
238
|
+
implicitDeps.push(createStep.order);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// If this step creates a test file, it depends on the implementation file step
|
|
243
|
+
if (step.mode === 'create' && step.target_file.includes('.test.')) {
|
|
244
|
+
const implPath = step.target_file.replace(/\.test\./, '.');
|
|
245
|
+
const implStep = allSteps.find(
|
|
246
|
+
s => s.target_file === implPath && s.order < step.order
|
|
247
|
+
);
|
|
248
|
+
if (implStep && !step.depends_on.includes(implStep.order)) {
|
|
249
|
+
implicitDeps.push(implStep.order);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return implicitDeps;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if task steps have circular dependencies
|
|
258
|
+
*/
|
|
259
|
+
function hasCircularDependencies(steps: TaskStep[]): boolean {
|
|
260
|
+
const visited = new Set<number>();
|
|
261
|
+
const recursionStack = new Set<number>();
|
|
262
|
+
|
|
263
|
+
function hasCycle(stepOrder: number): boolean {
|
|
264
|
+
if (recursionStack.has(stepOrder)) {
|
|
265
|
+
return true; // Found a cycle
|
|
266
|
+
}
|
|
267
|
+
if (visited.has(stepOrder)) {
|
|
268
|
+
return false; // Already checked this path
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
visited.add(stepOrder);
|
|
272
|
+
recursionStack.add(stepOrder);
|
|
273
|
+
|
|
274
|
+
const step = steps.find(s => s.order === stepOrder);
|
|
275
|
+
if (step) {
|
|
276
|
+
for (const dep of step.depends_on) {
|
|
277
|
+
if (hasCycle(dep)) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
recursionStack.delete(stepOrder);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const step of steps) {
|
|
288
|
+
if (hasCycle(step.order)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Storage Implementation
|
|
3
|
+
* Per ROADMAP §Phase 5 Week 6 Day 7: Task Storage
|
|
4
|
+
*
|
|
5
|
+
* Stores tasks in .agent-meta/tasks/ with one file per task,
|
|
6
|
+
* tracks status transitions, and links to trace IDs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Task, TaskStatus } from '../types/index.js';
|
|
10
|
+
import {
|
|
11
|
+
fileExists,
|
|
12
|
+
readJsonFile,
|
|
13
|
+
writeJsonFile,
|
|
14
|
+
ensureDir,
|
|
15
|
+
deleteFile,
|
|
16
|
+
} from '../utils/file-utils.js';
|
|
17
|
+
import { logDebug, logError } from '../utils/logger.js';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
|
|
21
|
+
const TASKS_DIR = '.agent-meta/tasks';
|
|
22
|
+
const ACTIVE_TASKS_FILE = path.join(TASKS_DIR, '_active.json');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Active tasks index structure
|
|
26
|
+
*/
|
|
27
|
+
interface ActiveTasksIndex {
|
|
28
|
+
updated_at: string;
|
|
29
|
+
tasks: string[]; // Array of task IDs
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Store a task to disk
|
|
34
|
+
*/
|
|
35
|
+
export async function storeTask(task: Task): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
// Ensure tasks directory exists
|
|
38
|
+
ensureDir(TASKS_DIR);
|
|
39
|
+
|
|
40
|
+
// Write task to its own file
|
|
41
|
+
const taskFilePath = getTaskFilePath(task.id);
|
|
42
|
+
writeJsonFile(taskFilePath, task);
|
|
43
|
+
|
|
44
|
+
// Update active tasks index
|
|
45
|
+
if (task.status === 'pending' || task.status === 'planning' || task.status === 'in_progress' || task.status === 'validating') {
|
|
46
|
+
await addToActiveIndex(task.id);
|
|
47
|
+
} else {
|
|
48
|
+
await removeFromActiveIndex(task.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
logDebug('Task stored', {
|
|
52
|
+
taskId: task.id,
|
|
53
|
+
status: task.status,
|
|
54
|
+
filePath: taskFilePath,
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
58
|
+
logError('Failed to store task', errorObj, { taskId: task.id });
|
|
59
|
+
throw new Error(`Task storage failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load a task from disk
|
|
65
|
+
*/
|
|
66
|
+
export async function loadTask(taskId: string): Promise<Task | null> {
|
|
67
|
+
try {
|
|
68
|
+
const taskFilePath = getTaskFilePath(taskId);
|
|
69
|
+
|
|
70
|
+
if (!fileExists(taskFilePath)) {
|
|
71
|
+
logDebug('Task file not found', { taskId, filePath: taskFilePath });
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const task = readJsonFile<Task>(taskFilePath);
|
|
76
|
+
logDebug('Task loaded', { taskId, status: task.status });
|
|
77
|
+
return task;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
80
|
+
logError('Failed to load task', errorObj, { taskId });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* List all tasks, optionally filtered by status
|
|
87
|
+
*/
|
|
88
|
+
export async function listTasks(statusFilter?: TaskStatus): Promise<Task[]> {
|
|
89
|
+
try {
|
|
90
|
+
ensureDir(TASKS_DIR);
|
|
91
|
+
|
|
92
|
+
// Get all task files
|
|
93
|
+
const files = fs.readdirSync(TASKS_DIR).filter(f => f.startsWith('task_') && f.endsWith('.json'));
|
|
94
|
+
|
|
95
|
+
const tasks: Task[] = [];
|
|
96
|
+
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
try {
|
|
99
|
+
const taskFilePath = path.join(TASKS_DIR, file);
|
|
100
|
+
const task = readJsonFile<Task>(taskFilePath);
|
|
101
|
+
|
|
102
|
+
// Apply status filter if provided
|
|
103
|
+
if (!statusFilter || task.status === statusFilter) {
|
|
104
|
+
tasks.push(task);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
108
|
+
logError('Failed to load task file', errorObj, { file });
|
|
109
|
+
// Continue with other files
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sort by creation date (newest first)
|
|
114
|
+
tasks.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
115
|
+
|
|
116
|
+
logDebug('Tasks listed', {
|
|
117
|
+
total: tasks.length,
|
|
118
|
+
statusFilter,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return tasks;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
124
|
+
logError('Failed to list tasks', errorObj);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* List active tasks (from index for performance)
|
|
131
|
+
*/
|
|
132
|
+
export async function listActiveTasks(): Promise<Task[]> {
|
|
133
|
+
try {
|
|
134
|
+
const index = await getActiveIndex();
|
|
135
|
+
const tasks: Task[] = [];
|
|
136
|
+
|
|
137
|
+
for (const taskId of index.tasks) {
|
|
138
|
+
const task = await loadTask(taskId);
|
|
139
|
+
if (task) {
|
|
140
|
+
tasks.push(task);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return tasks;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
147
|
+
logError('Failed to list active tasks', errorObj);
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Delete a task from storage
|
|
154
|
+
*/
|
|
155
|
+
export async function deleteTask(taskId: string): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
const taskFilePath = getTaskFilePath(taskId);
|
|
158
|
+
|
|
159
|
+
if (fileExists(taskFilePath)) {
|
|
160
|
+
deleteFile(taskFilePath);
|
|
161
|
+
logDebug('Task deleted', { taskId });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Remove from active index
|
|
165
|
+
await removeFromActiveIndex(taskId);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
168
|
+
logError('Failed to delete task', errorObj, { taskId });
|
|
169
|
+
throw new Error(`Task deletion failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update task status and store
|
|
175
|
+
*/
|
|
176
|
+
export async function updateTaskStatus(
|
|
177
|
+
taskId: string,
|
|
178
|
+
newStatus: TaskStatus,
|
|
179
|
+
error?: string
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const task = await loadTask(taskId);
|
|
182
|
+
if (!task) {
|
|
183
|
+
throw new Error(`Task ${taskId} not found`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const oldStatus = task.status;
|
|
187
|
+
task.status = newStatus;
|
|
188
|
+
|
|
189
|
+
if (error) {
|
|
190
|
+
task.error = error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (newStatus === 'completed' || newStatus === 'failed') {
|
|
194
|
+
task.completed_at = new Date().toISOString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await storeTask(task);
|
|
198
|
+
|
|
199
|
+
logDebug('Task status updated', {
|
|
200
|
+
taskId,
|
|
201
|
+
oldStatus,
|
|
202
|
+
newStatus,
|
|
203
|
+
hasError: !!error,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get task file path
|
|
209
|
+
*/
|
|
210
|
+
function getTaskFilePath(taskId: string): string {
|
|
211
|
+
return path.join(TASKS_DIR, `task_${taskId}.json`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get active tasks index
|
|
216
|
+
*/
|
|
217
|
+
async function getActiveIndex(): Promise<ActiveTasksIndex> {
|
|
218
|
+
try {
|
|
219
|
+
if (fileExists(ACTIVE_TASKS_FILE)) {
|
|
220
|
+
return readJsonFile<ActiveTasksIndex>(ACTIVE_TASKS_FILE);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
224
|
+
logError('Failed to read active tasks index', errorObj);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Return empty index if file doesn't exist or can't be read
|
|
228
|
+
return {
|
|
229
|
+
updated_at: new Date().toISOString(),
|
|
230
|
+
tasks: [],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Save active tasks index
|
|
236
|
+
*/
|
|
237
|
+
async function saveActiveIndex(index: ActiveTasksIndex): Promise<void> {
|
|
238
|
+
index.updated_at = new Date().toISOString();
|
|
239
|
+
writeJsonFile(ACTIVE_TASKS_FILE, index);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Add task to active index
|
|
244
|
+
*/
|
|
245
|
+
async function addToActiveIndex(taskId: string): Promise<void> {
|
|
246
|
+
const index = await getActiveIndex();
|
|
247
|
+
|
|
248
|
+
if (!index.tasks.includes(taskId)) {
|
|
249
|
+
index.tasks.push(taskId);
|
|
250
|
+
await saveActiveIndex(index);
|
|
251
|
+
logDebug('Task added to active index', { taskId });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Remove task from active index
|
|
257
|
+
*/
|
|
258
|
+
async function removeFromActiveIndex(taskId: string): Promise<void> {
|
|
259
|
+
const index = await getActiveIndex();
|
|
260
|
+
|
|
261
|
+
const initialLength = index.tasks.length;
|
|
262
|
+
index.tasks = index.tasks.filter(id => id !== taskId);
|
|
263
|
+
|
|
264
|
+
if (index.tasks.length < initialLength) {
|
|
265
|
+
await saveActiveIndex(index);
|
|
266
|
+
logDebug('Task removed from active index', { taskId });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get task history for a file (all tasks that touched it)
|
|
272
|
+
*/
|
|
273
|
+
export async function getTaskHistoryForFile(filePath: string): Promise<Task[]> {
|
|
274
|
+
const allTasks = await listTasks();
|
|
275
|
+
|
|
276
|
+
return allTasks.filter(
|
|
277
|
+
task =>
|
|
278
|
+
task.files_created.includes(filePath) ||
|
|
279
|
+
task.files_modified.includes(filePath)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get task statistics
|
|
285
|
+
*/
|
|
286
|
+
export async function getTaskStatistics(): Promise<{
|
|
287
|
+
total: number;
|
|
288
|
+
byStatus: Record<TaskStatus, number>;
|
|
289
|
+
avgCompletionTime: number;
|
|
290
|
+
successRate: number;
|
|
291
|
+
}> {
|
|
292
|
+
const allTasks = await listTasks();
|
|
293
|
+
|
|
294
|
+
const byStatus: Record<TaskStatus, number> = {
|
|
295
|
+
pending: 0,
|
|
296
|
+
planning: 0,
|
|
297
|
+
in_progress: 0,
|
|
298
|
+
validating: 0,
|
|
299
|
+
completed: 0,
|
|
300
|
+
failed: 0,
|
|
301
|
+
blocked: 0,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let totalCompletionTime = 0;
|
|
305
|
+
let completedOrFailed = 0;
|
|
306
|
+
|
|
307
|
+
for (const task of allTasks) {
|
|
308
|
+
byStatus[task.status]++;
|
|
309
|
+
|
|
310
|
+
if (task.completed_at) {
|
|
311
|
+
const created = new Date(task.created_at).getTime();
|
|
312
|
+
const completed = new Date(task.completed_at).getTime();
|
|
313
|
+
totalCompletionTime += completed - created;
|
|
314
|
+
completedOrFailed++;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const avgCompletionTime = completedOrFailed > 0
|
|
319
|
+
? totalCompletionTime / completedOrFailed
|
|
320
|
+
: 0;
|
|
321
|
+
|
|
322
|
+
const successRate = completedOrFailed > 0
|
|
323
|
+
? byStatus.completed / completedOrFailed
|
|
324
|
+
: 0;
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
total: allTasks.length,
|
|
328
|
+
byStatus,
|
|
329
|
+
avgCompletionTime,
|
|
330
|
+
successRate,
|
|
331
|
+
};
|
|
332
|
+
}
|
package/src/trace/trace-event.ts
CHANGED
|
@@ -9,25 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { appendFile, readFile, fileExists, ensureDir } from '../utils/file-utils.js';
|
|
11
11
|
import { logInfo, logError, logDebug } from '../utils/logger.js';
|
|
12
|
-
|
|
13
|
-
export type Mode = 'error_reducer' | 'complexity_reducer' | 'debt_payer' | 'stabilizer' | 'explorer';
|
|
14
|
-
|
|
15
|
-
export interface TraceEvent {
|
|
16
|
-
id: string;
|
|
17
|
-
timestamp: string;
|
|
18
|
-
agent_id: string;
|
|
19
|
-
file_path: string;
|
|
20
|
-
mode: Mode;
|
|
21
|
-
gradient_before: number;
|
|
22
|
-
gradient_after: number;
|
|
23
|
-
gradient_delta: number;
|
|
24
|
-
changes_made: string[];
|
|
25
|
-
tokens_used: number;
|
|
26
|
-
cost_usd: number;
|
|
27
|
-
duration_ms: number;
|
|
28
|
-
success: boolean;
|
|
29
|
-
error?: string;
|
|
30
|
-
}
|
|
12
|
+
import type { TraceEvent, Mode } from '../types/index.js';
|
|
31
13
|
|
|
32
14
|
export interface TraceFilters {
|
|
33
15
|
file?: string;
|
|
@@ -64,13 +46,13 @@ export async function recordTrace(event: TraceEvent): Promise<void> {
|
|
|
64
46
|
appendFile(TRACES_FILE, line);
|
|
65
47
|
|
|
66
48
|
// Log summary
|
|
67
|
-
const costValue = Number(event.
|
|
49
|
+
const costValue = Number(event.cost.estimated_usd) || 0;
|
|
68
50
|
logInfo('Trace recorded', {
|
|
69
51
|
file: event.file_path,
|
|
70
52
|
mode: event.mode,
|
|
71
53
|
delta: event.gradient_delta.toFixed(3),
|
|
72
54
|
cost: `$${costValue.toFixed(4)}`,
|
|
73
|
-
|
|
55
|
+
ciPassed: event.ci_passed,
|
|
74
56
|
});
|
|
75
57
|
|
|
76
58
|
// Check if cleanup needed (every 100 traces)
|
|
@@ -172,7 +154,7 @@ export function calculateEfficiency(traces: TraceEvent[]): number {
|
|
|
172
154
|
}
|
|
173
155
|
|
|
174
156
|
const totalDelta = traces.reduce((sum, t) => sum + t.gradient_delta, 0);
|
|
175
|
-
const totalCost = traces.reduce((sum, t) => sum + t.
|
|
157
|
+
const totalCost = traces.reduce((sum, t) => sum + (t.cost.estimated_usd || 0), 0);
|
|
176
158
|
|
|
177
159
|
// Prevent division by zero
|
|
178
160
|
if (totalCost === 0) {
|
|
@@ -195,9 +177,9 @@ export function getTraceStats(traces: TraceEvent[]): TraceStats {
|
|
|
195
177
|
};
|
|
196
178
|
}
|
|
197
179
|
|
|
198
|
-
const totalCost = traces.reduce((sum, t) => sum + t.
|
|
180
|
+
const totalCost = traces.reduce((sum, t) => sum + (t.cost.estimated_usd || 0), 0);
|
|
199
181
|
const totalDelta = traces.reduce((sum, t) => sum + t.gradient_delta, 0);
|
|
200
|
-
const successCount = traces.filter(t => t.
|
|
182
|
+
const successCount = traces.filter(t => t.ci_passed).length;
|
|
201
183
|
|
|
202
184
|
return {
|
|
203
185
|
totalCost,
|
|
@@ -313,12 +295,11 @@ export function calculateCost(tokensIn: number, tokensOut: number, model: string
|
|
|
313
295
|
* Create a trace event with defaults
|
|
314
296
|
*/
|
|
315
297
|
export function createTraceEvent(
|
|
316
|
-
partial: Omit<TraceEvent, 'id' | 'timestamp'
|
|
298
|
+
partial: Omit<TraceEvent, 'id' | 'timestamp'>
|
|
317
299
|
): TraceEvent {
|
|
318
300
|
return {
|
|
319
301
|
id: generateTraceId(),
|
|
320
302
|
timestamp: new Date().toISOString(),
|
|
321
|
-
gradient_delta: partial.gradient_before - partial.gradient_after,
|
|
322
303
|
...partial,
|
|
323
304
|
};
|
|
324
305
|
}
|