@polymorphism-tech/morph-spec 4.2.0 → 4.3.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/bin/morph-spec.js +283 -8
- package/bin/validate.js +4 -4
- package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
- package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
- package/docs/next-generation/EXECUTION-FLOW.md +274 -0
- package/docs/next-generation/META-PROMPTS.md +235 -0
- package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
- package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
- package/package.json +5 -5
- package/src/commands/agents/agents-fuse.js +96 -0
- package/src/commands/agents/micro-agent.js +112 -0
- package/src/commands/agents/spawn-team.js +69 -4
- package/src/commands/agents/squad-template.js +146 -0
- package/src/commands/analytics/analytics.js +176 -0
- package/src/commands/context/context-prime.js +63 -0
- package/src/commands/context/core-four.js +54 -0
- package/src/commands/mcp/mcp.js +102 -0
- package/src/commands/project/detect-agents.js +1 -1
- package/src/commands/project/doctor.js +573 -356
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/update.js +1 -1
- package/src/commands/state/advance-phase.js +433 -416
- package/src/commands/templates/template-render.js +80 -1
- package/src/commands/threads/thread-template.js +103 -0
- package/src/commands/threads/threads.js +261 -0
- package/src/commands/trust/trust.js +205 -0
- package/src/{orchestrator.js → core/orchestrator.js} +8 -8
- package/src/core/state/state-manager.js +18 -2
- package/src/core/workflows/workflow-detector.js +100 -2
- package/src/lib/agents/micro-agent-factory.js +161 -0
- package/src/lib/analytics/analytics-engine.js +345 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +293 -258
- package/src/lib/context/context-bundler.js +240 -0
- package/src/lib/context/context-optimizer.js +212 -0
- package/src/lib/context/context-tracker.js +273 -0
- package/src/lib/context/core-four-tracker.js +201 -0
- package/src/lib/context/mcp-optimizer.js +200 -0
- package/src/lib/execution/fusion-executor.js +304 -0
- package/src/lib/execution/parallel-executor.js +270 -0
- package/src/lib/generators/context-generator.js +3 -3
- package/src/lib/generators/recap-generator.js +2 -2
- package/src/lib/hooks/hook-executor.js +169 -0
- package/src/lib/hooks/stop-hook-executor.js +286 -0
- package/src/lib/hops/hop-composer.js +221 -0
- package/src/lib/threads/thread-coordinator.js +238 -0
- package/src/lib/threads/thread-manager.js +317 -0
- package/src/lib/tracking/artifact-trail.js +202 -0
- package/src/lib/trust/trust-manager.js +269 -0
- package/src/lib/validators/design-system/design-system-validator.js +2 -2
- package/src/lib/validators/validation-runner.js +6 -6
- package/stacks/blazor-azure/.morph/config/agents.json +72 -3
- package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
- package/CLAUDE.md +0 -993
- package/docs/llm-interaction-config.md +0 -735
- package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
- package/src/commands/utils/migrate-state.js +0 -158
- package/src/commands/utils/upgrade.js +0 -346
- package/src/lib/validators/architecture-validator.js +0 -60
- package/src/lib/validators/content-validator.js +0 -164
- package/src/lib/validators/package-validator.js +0 -61
- package/src/lib/validators/ui-contrast-validator.js +0 -44
- package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
- package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
- package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
- package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
- package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
- package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
- package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
- package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
- package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
- package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
- package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
- package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
- package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
- package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
- package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
- package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
- package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
- package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
- package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
- package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
- package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
- package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
- package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
- package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
- /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
- /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
- /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
- /package/docs/{v3.0 → next-generation}/README.md +0 -0
- /package/docs/{v3.0 → next-generation}/ROADMAP.md +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread Coordinator — Dependency analysis and wait strategies
|
|
3
|
+
*
|
|
4
|
+
* Parses tasks.json dependency graphs and identifies:
|
|
5
|
+
* - Tasks that can run in parallel (no dependencies on each other)
|
|
6
|
+
* - Tasks that must run sequentially
|
|
7
|
+
* - Circular dependencies
|
|
8
|
+
*
|
|
9
|
+
* Returns an execution plan: phases of parallel groups.
|
|
10
|
+
*
|
|
11
|
+
* Example output:
|
|
12
|
+
* Phase 1 (parallel): [T001, T002, T005] ← no dependencies
|
|
13
|
+
* Phase 2 (parallel): [T003, T004] ← depend only on phase 1
|
|
14
|
+
* Phase 3 (sequential): [T006] ← depends on both T003, T004
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Dependency Analysis
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a dependency graph from an in-memory task array
|
|
26
|
+
* @param {Array} tasks - Array of { id, dependencies: [] } task objects
|
|
27
|
+
* @returns {Object} { nodes: Map<id, task>, edges: Map<id, Set<depId>>, tasks }
|
|
28
|
+
*/
|
|
29
|
+
export function buildGraph(tasks) {
|
|
30
|
+
const nodes = new Map();
|
|
31
|
+
const edges = new Map();
|
|
32
|
+
|
|
33
|
+
for (const task of tasks) {
|
|
34
|
+
nodes.set(task.id, task);
|
|
35
|
+
edges.set(task.id, new Set(task.dependencies || []));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { nodes, edges, tasks };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse tasks.json and build a dependency graph
|
|
43
|
+
* @param {string} feature - Feature name
|
|
44
|
+
* @returns {Object} { nodes: Map<id, task>, edges: Map<id, Set<depId>> }
|
|
45
|
+
*/
|
|
46
|
+
export function analyzeDependencies(feature) {
|
|
47
|
+
const tasksPath = join(process.cwd(), `.morph/project/outputs/${feature}/tasks.json`);
|
|
48
|
+
|
|
49
|
+
if (!existsSync(tasksPath)) {
|
|
50
|
+
throw new Error(`tasks.json not found for feature: ${feature}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let tasksData;
|
|
54
|
+
try {
|
|
55
|
+
tasksData = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw new Error(`Failed to parse tasks.json: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return buildGraph(tasksData.tasks || []);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Identify tasks that can run in parallel (no dependencies between them)
|
|
65
|
+
* @param {Object} graph - { nodes, edges } from buildGraph/analyzeDependencies
|
|
66
|
+
* @returns {Array} Groups of task IDs that can run in parallel
|
|
67
|
+
*/
|
|
68
|
+
export function identifyParallelizable(graph) {
|
|
69
|
+
const { nodes, edges } = graph;
|
|
70
|
+
const plan = planExecution(nodes, edges);
|
|
71
|
+
return plan.filter(group => group.length > 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Identify tasks that must run sequentially
|
|
76
|
+
* @param {Object} graph - { nodes, edges } from buildGraph/analyzeDependencies
|
|
77
|
+
* @returns {Array} Task IDs that must run sequentially
|
|
78
|
+
*/
|
|
79
|
+
export function identifySequential(graph) {
|
|
80
|
+
const { nodes, edges } = graph;
|
|
81
|
+
const plan = planExecution(nodes, edges);
|
|
82
|
+
return plan.filter(group => group.length === 1).flat();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detect circular dependencies in the task graph
|
|
87
|
+
* @param {Map} nodes - Task nodes
|
|
88
|
+
* @param {Map} edges - Dependency edges
|
|
89
|
+
* @returns {Array} Array of circular dependency chains, or [] if none
|
|
90
|
+
*/
|
|
91
|
+
export function detectCircular(nodes, edges) {
|
|
92
|
+
const cycles = [];
|
|
93
|
+
const visited = new Set();
|
|
94
|
+
const recursionStack = new Set();
|
|
95
|
+
|
|
96
|
+
function dfs(nodeId, path) {
|
|
97
|
+
visited.add(nodeId);
|
|
98
|
+
recursionStack.add(nodeId);
|
|
99
|
+
|
|
100
|
+
for (const dep of (edges.get(nodeId) || [])) {
|
|
101
|
+
if (!nodes.has(dep)) continue;
|
|
102
|
+
|
|
103
|
+
if (!visited.has(dep)) {
|
|
104
|
+
dfs(dep, [...path, nodeId]);
|
|
105
|
+
} else if (recursionStack.has(dep)) {
|
|
106
|
+
// Found a cycle
|
|
107
|
+
const cycleStart = path.indexOf(dep);
|
|
108
|
+
const cycle = cycleStart >= 0
|
|
109
|
+
? [...path.slice(cycleStart), nodeId, dep]
|
|
110
|
+
: [...path, nodeId, dep];
|
|
111
|
+
cycles.push(cycle);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
recursionStack.delete(nodeId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const nodeId of nodes.keys()) {
|
|
119
|
+
if (!visited.has(nodeId)) {
|
|
120
|
+
dfs(nodeId, []);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return cycles;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Plan execution: return phases of parallel task groups
|
|
129
|
+
* @param {Map} nodes - Task nodes
|
|
130
|
+
* @param {Map} edges - Dependency edges
|
|
131
|
+
* @returns {Array} Array of phases, each phase is an array of task IDs
|
|
132
|
+
*
|
|
133
|
+
* Algorithm: Kahn's topological sort, grouping by "wave"
|
|
134
|
+
*/
|
|
135
|
+
export function planExecution(nodes, edges) {
|
|
136
|
+
// Check for cycles first
|
|
137
|
+
const cycles = detectCircular(nodes, edges);
|
|
138
|
+
if (cycles.length > 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Circular dependencies detected: ${cycles.map(c => c.join(' → ')).join('; ')}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build reverse edges (dependents)
|
|
145
|
+
const inDegree = new Map();
|
|
146
|
+
const dependents = new Map(); // id → Set of task IDs that depend on it
|
|
147
|
+
|
|
148
|
+
for (const [id] of nodes) {
|
|
149
|
+
inDegree.set(id, 0);
|
|
150
|
+
dependents.set(id, new Set());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const [id, deps] of edges) {
|
|
154
|
+
for (const dep of deps) {
|
|
155
|
+
if (!inDegree.has(dep)) continue;
|
|
156
|
+
inDegree.set(id, (inDegree.get(id) || 0) + 1);
|
|
157
|
+
dependents.get(dep).add(id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Correct in-degree: count only deps that exist in nodes
|
|
162
|
+
for (const [id, deps] of edges) {
|
|
163
|
+
let count = 0;
|
|
164
|
+
for (const dep of deps) {
|
|
165
|
+
if (nodes.has(dep)) count++;
|
|
166
|
+
}
|
|
167
|
+
inDegree.set(id, count);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const phases = [];
|
|
171
|
+
const remaining = new Set(nodes.keys());
|
|
172
|
+
|
|
173
|
+
while (remaining.size > 0) {
|
|
174
|
+
// Find all nodes with no unsatisfied dependencies
|
|
175
|
+
const ready = [...remaining].filter(id => {
|
|
176
|
+
const deps = edges.get(id) || new Set();
|
|
177
|
+
return [...deps].every(dep => !remaining.has(dep));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (ready.length === 0 && remaining.size > 0) {
|
|
181
|
+
// Shouldn't happen if no cycles, but safeguard
|
|
182
|
+
throw new Error(`Execution plan stalled with ${remaining.size} remaining tasks: ${[...remaining].join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
phases.push(ready);
|
|
186
|
+
for (const id of ready) {
|
|
187
|
+
remaining.delete(id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return phases;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Full Analysis
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get full parallel execution plan for a feature
|
|
200
|
+
* @param {string} feature - Feature name
|
|
201
|
+
* @returns {Object} Execution plan with phases, parallelizable groups, and stats
|
|
202
|
+
*/
|
|
203
|
+
export function getExecutionPlan(feature) {
|
|
204
|
+
const { nodes, edges, tasks } = analyzeDependencies(feature);
|
|
205
|
+
|
|
206
|
+
const cycles = detectCircular(nodes, edges);
|
|
207
|
+
if (cycles.length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
feature,
|
|
210
|
+
valid: false,
|
|
211
|
+
cycles,
|
|
212
|
+
error: `Circular dependencies found: ${cycles.map(c => c.join(' → ')).join('; ')}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const phases = planExecution(nodes, edges);
|
|
217
|
+
const parallelPhases = phases.filter(p => p.length > 1);
|
|
218
|
+
const totalParallelizable = parallelPhases.reduce((sum, p) => sum + p.length, 0);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
feature,
|
|
222
|
+
valid: true,
|
|
223
|
+
totalTasks: tasks.length,
|
|
224
|
+
phases: phases.map((group, i) => ({
|
|
225
|
+
phase: i + 1,
|
|
226
|
+
tasks: group,
|
|
227
|
+
canRunParallel: group.length > 1,
|
|
228
|
+
count: group.length
|
|
229
|
+
})),
|
|
230
|
+
stats: {
|
|
231
|
+
totalPhases: phases.length,
|
|
232
|
+
parallelPhases: parallelPhases.length,
|
|
233
|
+
parallelizableTasks: totalParallelizable,
|
|
234
|
+
sequentialTasks: tasks.length - totalParallelizable,
|
|
235
|
+
parallelizationRatio: Math.round(totalParallelizable / Math.max(tasks.length, 1) * 100)
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread Manager — Core thread lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Manages the full lifecycle of MORPH-SPEC threads:
|
|
5
|
+
* - Types: base, parallel, fusion, long-running, zero-touch
|
|
6
|
+
* - Status transitions: pending → running → completed | failed | killed
|
|
7
|
+
* - Persists thread state to state.json threads{} section
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
|
|
14
|
+
const STATE_PATH = join(process.cwd(), '.morph/state.json');
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Thread Types and Status
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export const THREAD_TYPES = {
|
|
21
|
+
BASE: 'base',
|
|
22
|
+
PARALLEL: 'parallel',
|
|
23
|
+
FUSION: 'fusion',
|
|
24
|
+
LONG_RUNNING: 'long-running',
|
|
25
|
+
ZERO_TOUCH: 'zero-touch'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const THREAD_STATUS = {
|
|
29
|
+
PENDING: 'pending',
|
|
30
|
+
RUNNING: 'running',
|
|
31
|
+
COMPLETED: 'completed',
|
|
32
|
+
FAILED: 'failed',
|
|
33
|
+
KILLED: 'killed'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// State I/O
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
function loadState() {
|
|
41
|
+
if (!existsSync(STATE_PATH)) {
|
|
42
|
+
throw new Error(`State file not found: ${STATE_PATH}`);
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(readFileSync(STATE_PATH, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveState(state) {
|
|
48
|
+
state.metadata = state.metadata || {};
|
|
49
|
+
state.metadata.lastUpdated = new Date().toISOString();
|
|
50
|
+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureThreadsSection(state) {
|
|
54
|
+
if (!state.threads) {
|
|
55
|
+
state.threads = {};
|
|
56
|
+
}
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Thread CRUD
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a new thread
|
|
66
|
+
* @param {Object} opts
|
|
67
|
+
* @param {string} opts.feature - Feature name this thread belongs to
|
|
68
|
+
* @param {string} opts.type - Thread type (base|parallel|fusion|long-running|zero-touch)
|
|
69
|
+
* @param {string} [opts.agent] - Agent assigned to this thread
|
|
70
|
+
* @param {string} [opts.mission] - Thread mission description
|
|
71
|
+
* @param {Object} [opts.meta] - Additional metadata
|
|
72
|
+
* @returns {Object} Created thread object
|
|
73
|
+
*/
|
|
74
|
+
export function createThread({ feature, type = THREAD_TYPES.BASE, agent = null, mission = '', meta = {} }) {
|
|
75
|
+
const state = ensureThreadsSection(loadState());
|
|
76
|
+
const id = randomUUID();
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
|
|
79
|
+
const thread = {
|
|
80
|
+
id,
|
|
81
|
+
feature,
|
|
82
|
+
type,
|
|
83
|
+
agent,
|
|
84
|
+
mission,
|
|
85
|
+
status: THREAD_STATUS.PENDING,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
startedAt: null,
|
|
88
|
+
completedAt: null,
|
|
89
|
+
duration: null,
|
|
90
|
+
events: [],
|
|
91
|
+
metrics: {
|
|
92
|
+
toolCalls: 0,
|
|
93
|
+
tokensUsed: 0,
|
|
94
|
+
checkpointsPassed: 0,
|
|
95
|
+
checkpointsFailed: 0
|
|
96
|
+
},
|
|
97
|
+
meta
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
state.threads[id] = thread;
|
|
101
|
+
saveState(state);
|
|
102
|
+
return thread;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Update thread fields
|
|
107
|
+
* @param {string} id - Thread ID
|
|
108
|
+
* @param {Object} updates - Fields to update
|
|
109
|
+
* @returns {Object} Updated thread
|
|
110
|
+
*/
|
|
111
|
+
export function updateThread(id, updates) {
|
|
112
|
+
const state = ensureThreadsSection(loadState());
|
|
113
|
+
|
|
114
|
+
if (!state.threads[id]) {
|
|
115
|
+
throw new Error(`Thread not found: ${id}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
state.threads[id] = { ...state.threads[id], ...updates };
|
|
119
|
+
saveState(state);
|
|
120
|
+
return state.threads[id];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get a thread by ID
|
|
125
|
+
* @param {string} id - Thread ID
|
|
126
|
+
* @returns {Object|null} Thread or null
|
|
127
|
+
*/
|
|
128
|
+
export function getThread(id) {
|
|
129
|
+
const state = ensureThreadsSection(loadState());
|
|
130
|
+
return state.threads[id] || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List threads, optionally filtered
|
|
135
|
+
* @param {Object} [filter]
|
|
136
|
+
* @param {string} [filter.feature] - Filter by feature name
|
|
137
|
+
* @param {string} [filter.status] - Filter by status
|
|
138
|
+
* @param {string} [filter.type] - Filter by type
|
|
139
|
+
* @returns {Array} Array of thread objects
|
|
140
|
+
*/
|
|
141
|
+
export function listThreads({ feature, status, type } = {}) {
|
|
142
|
+
const state = ensureThreadsSection(loadState());
|
|
143
|
+
let threads = Object.values(state.threads);
|
|
144
|
+
|
|
145
|
+
if (feature) threads = threads.filter(t => t.feature === feature);
|
|
146
|
+
if (status) threads = threads.filter(t => t.status === status);
|
|
147
|
+
if (type) threads = threads.filter(t => t.type === type);
|
|
148
|
+
|
|
149
|
+
return threads.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Start a thread (pending → running)
|
|
154
|
+
* @param {string} id - Thread ID
|
|
155
|
+
* @returns {Object} Updated thread
|
|
156
|
+
*/
|
|
157
|
+
export function startThread(id) {
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
return updateThread(id, {
|
|
160
|
+
status: THREAD_STATUS.RUNNING,
|
|
161
|
+
startedAt: now
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Complete a thread (running → completed)
|
|
167
|
+
* @param {string} id - Thread ID
|
|
168
|
+
* @param {Object} [result] - Optional result metadata
|
|
169
|
+
* @returns {Object} Updated thread
|
|
170
|
+
*/
|
|
171
|
+
export function completeThread(id, result = {}) {
|
|
172
|
+
const thread = getThread(id);
|
|
173
|
+
if (!thread) throw new Error(`Thread not found: ${id}`);
|
|
174
|
+
|
|
175
|
+
const now = new Date();
|
|
176
|
+
const startedAt = thread.startedAt ? new Date(thread.startedAt) : now;
|
|
177
|
+
const duration = Math.round((now - startedAt) / 1000); // seconds
|
|
178
|
+
|
|
179
|
+
return updateThread(id, {
|
|
180
|
+
status: THREAD_STATUS.COMPLETED,
|
|
181
|
+
completedAt: now.toISOString(),
|
|
182
|
+
duration,
|
|
183
|
+
result
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Mark a thread as failed
|
|
189
|
+
* @param {string} id - Thread ID
|
|
190
|
+
* @param {string} reason - Failure reason
|
|
191
|
+
* @returns {Object} Updated thread
|
|
192
|
+
*/
|
|
193
|
+
export function failThread(id, reason = '') {
|
|
194
|
+
const thread = getThread(id);
|
|
195
|
+
if (!thread) throw new Error(`Thread not found: ${id}`);
|
|
196
|
+
|
|
197
|
+
const now = new Date();
|
|
198
|
+
const startedAt = thread.startedAt ? new Date(thread.startedAt) : now;
|
|
199
|
+
const duration = Math.round((now - startedAt) / 1000);
|
|
200
|
+
|
|
201
|
+
return updateThread(id, {
|
|
202
|
+
status: THREAD_STATUS.FAILED,
|
|
203
|
+
completedAt: now.toISOString(),
|
|
204
|
+
duration,
|
|
205
|
+
failureReason: reason
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Kill (terminate) a running thread
|
|
211
|
+
* @param {string} id - Thread ID
|
|
212
|
+
* @returns {Object} Updated thread
|
|
213
|
+
*/
|
|
214
|
+
export function killThread(id) {
|
|
215
|
+
const thread = getThread(id);
|
|
216
|
+
if (!thread) throw new Error(`Thread not found: ${id}`);
|
|
217
|
+
if (thread.status === THREAD_STATUS.COMPLETED || thread.status === THREAD_STATUS.KILLED) {
|
|
218
|
+
throw new Error(`Cannot kill thread in status: ${thread.status}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const now = new Date();
|
|
222
|
+
const startedAt = thread.startedAt ? new Date(thread.startedAt) : now;
|
|
223
|
+
const duration = Math.round((now - startedAt) / 1000);
|
|
224
|
+
|
|
225
|
+
return updateThread(id, {
|
|
226
|
+
status: THREAD_STATUS.KILLED,
|
|
227
|
+
completedAt: now.toISOString(),
|
|
228
|
+
duration
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add an event to a thread's event log
|
|
234
|
+
* @param {string} id - Thread ID
|
|
235
|
+
* @param {string} type - Event type
|
|
236
|
+
* @param {Object} [data] - Event data
|
|
237
|
+
*/
|
|
238
|
+
export function addThreadEvent(id, type, data = {}) {
|
|
239
|
+
const state = ensureThreadsSection(loadState());
|
|
240
|
+
const thread = state.threads[id];
|
|
241
|
+
if (!thread) throw new Error(`Thread not found: ${id}`);
|
|
242
|
+
|
|
243
|
+
thread.events = thread.events || [];
|
|
244
|
+
thread.events.push({
|
|
245
|
+
type,
|
|
246
|
+
timestamp: new Date().toISOString(),
|
|
247
|
+
data
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
saveState(state);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Increment thread metrics counter
|
|
255
|
+
* @param {string} id - Thread ID
|
|
256
|
+
* @param {string} metric - Metric name (toolCalls|tokensUsed|checkpointsPassed|checkpointsFailed)
|
|
257
|
+
* @param {number} [amount=1] - Amount to increment
|
|
258
|
+
*/
|
|
259
|
+
export function incrementThreadMetric(id, metric, amount = 1) {
|
|
260
|
+
const state = ensureThreadsSection(loadState());
|
|
261
|
+
const thread = state.threads[id];
|
|
262
|
+
if (!thread) throw new Error(`Thread not found: ${id}`);
|
|
263
|
+
|
|
264
|
+
thread.metrics = thread.metrics || {};
|
|
265
|
+
thread.metrics[metric] = (thread.metrics[metric] || 0) + amount;
|
|
266
|
+
saveState(state);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Analytics
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get analytics for threads belonging to a feature
|
|
275
|
+
* @param {string} feature - Feature name
|
|
276
|
+
* @returns {Object} Analytics summary
|
|
277
|
+
*/
|
|
278
|
+
export function getThreadAnalytics(feature) {
|
|
279
|
+
const threads = listThreads({ feature });
|
|
280
|
+
|
|
281
|
+
const byStatus = threads.reduce((acc, t) => {
|
|
282
|
+
acc[t.status] = (acc[t.status] || 0) + 1;
|
|
283
|
+
return acc;
|
|
284
|
+
}, {});
|
|
285
|
+
|
|
286
|
+
const byType = threads.reduce((acc, t) => {
|
|
287
|
+
acc[t.type] = (acc[t.type] || 0) + 1;
|
|
288
|
+
return acc;
|
|
289
|
+
}, {});
|
|
290
|
+
|
|
291
|
+
const completed = threads.filter(t => t.status === THREAD_STATUS.COMPLETED);
|
|
292
|
+
const avgDuration = completed.length > 0
|
|
293
|
+
? Math.round(completed.reduce((sum, t) => sum + (t.duration || 0), 0) / completed.length)
|
|
294
|
+
: 0;
|
|
295
|
+
|
|
296
|
+
const totalToolCalls = threads.reduce((sum, t) => sum + (t.metrics?.toolCalls || 0), 0);
|
|
297
|
+
const parallelThreads = threads.filter(t => t.type === THREAD_TYPES.PARALLEL);
|
|
298
|
+
const avgParallel = parallelThreads.length;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
feature,
|
|
302
|
+
total: threads.length,
|
|
303
|
+
byStatus,
|
|
304
|
+
byType,
|
|
305
|
+
avgDuration,
|
|
306
|
+
totalToolCalls,
|
|
307
|
+
parallelEfficiency: {
|
|
308
|
+
avgParallel,
|
|
309
|
+
maxParallel: Math.max(...threads.map(() => parallelThreads.length), 0)
|
|
310
|
+
},
|
|
311
|
+
checkpointPassRate: threads.reduce((sum, t) => {
|
|
312
|
+
const p = t.metrics?.checkpointsPassed || 0;
|
|
313
|
+
const f = t.metrics?.checkpointsFailed || 0;
|
|
314
|
+
return p + f > 0 ? sum + (p / (p + f)) : sum;
|
|
315
|
+
}, 0) / Math.max(threads.filter(t => (t.metrics?.checkpointsPassed || 0) + (t.metrics?.checkpointsFailed || 0) > 0).length, 1)
|
|
316
|
+
};
|
|
317
|
+
}
|