@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
|
@@ -90,7 +90,7 @@ export function initState(options = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const initialState = {
|
|
93
|
-
version: "
|
|
93
|
+
version: "3.0.0",
|
|
94
94
|
project: {
|
|
95
95
|
name: projectName,
|
|
96
96
|
type: projectType,
|
|
@@ -98,6 +98,7 @@ export function initState(options = {}) {
|
|
|
98
98
|
updatedAt: new Date().toISOString()
|
|
99
99
|
},
|
|
100
100
|
features: {},
|
|
101
|
+
threads: {},
|
|
101
102
|
metadata: {
|
|
102
103
|
totalFeatures: 0,
|
|
103
104
|
completedFeatures: 0,
|
|
@@ -201,7 +202,22 @@ async function ensureFeature(featureName, options = {}) {
|
|
|
201
202
|
inProgress: 0,
|
|
202
203
|
pending: 0
|
|
203
204
|
},
|
|
204
|
-
checkpoints: []
|
|
205
|
+
checkpoints: [],
|
|
206
|
+
threadMetrics: {
|
|
207
|
+
totalThreads: 0,
|
|
208
|
+
parallelPeak: 0,
|
|
209
|
+
avgDuration: 0,
|
|
210
|
+
checkpointPassRate: 100
|
|
211
|
+
},
|
|
212
|
+
trustConfig: {
|
|
213
|
+
level: 'low',
|
|
214
|
+
history: [],
|
|
215
|
+
autoApprove: {
|
|
216
|
+
design: false,
|
|
217
|
+
tasks: false
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
contextBundles: []
|
|
205
221
|
};
|
|
206
222
|
|
|
207
223
|
state.metadata.totalFeatures++;
|
|
@@ -27,8 +27,8 @@ function getFrameworkRoot(projectPath) {
|
|
|
27
27
|
return npmPath;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Fallback to local development path (from src/
|
|
31
|
-
return join(__dirname, '..', '..');
|
|
30
|
+
// Fallback to local development path (from src/core/workflows/ → root)
|
|
31
|
+
return join(__dirname, '..', '..', '..');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -352,3 +352,101 @@ export function listWorkflows(projectPath = '.') {
|
|
|
352
352
|
description: w.description
|
|
353
353
|
}));
|
|
354
354
|
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// Parallel, Fusion, Long-Running, Zero-Touch Detection
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Detect if parallel execution mode should be used for a feature.
|
|
362
|
+
* Based on dependency graph analysis: if feature has independent task squads,
|
|
363
|
+
* recommend parallel mode.
|
|
364
|
+
*
|
|
365
|
+
* @param {string} feature - Feature name
|
|
366
|
+
* @param {string} [projectPath='.'] - Project path
|
|
367
|
+
* @returns {Object} { useParallel: boolean, reason: string, squads: Array }
|
|
368
|
+
*/
|
|
369
|
+
export async function detectParallelMode(feature, projectPath = '.') {
|
|
370
|
+
try {
|
|
371
|
+
const { getExecutionPlan } = await import('../../lib/threads/thread-coordinator.js').catch(() => {
|
|
372
|
+
// thread-coordinator may not exist yet — graceful fallback
|
|
373
|
+
return { getExecutionPlan: null };
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (!getExecutionPlan) {
|
|
377
|
+
return { useParallel: false, reason: 'thread-coordinator not available', squads: [] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const plan = getExecutionPlan(feature);
|
|
381
|
+
|
|
382
|
+
if (!plan.valid) {
|
|
383
|
+
return { useParallel: false, reason: plan.error, squads: [] };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const parallelPhases = plan.phases.filter(p => p.canRunParallel);
|
|
387
|
+
const useParallel = parallelPhases.length > 0 && plan.stats.parallelizationRatio > 30;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
useParallel,
|
|
391
|
+
reason: useParallel
|
|
392
|
+
? `${plan.stats.parallelizationRatio}% of tasks are parallelizable across ${parallelPhases.length} phases`
|
|
393
|
+
: 'Tasks are mostly sequential — parallel mode would not improve throughput',
|
|
394
|
+
parallelizationRatio: plan.stats.parallelizationRatio,
|
|
395
|
+
parallelPhases: parallelPhases.length,
|
|
396
|
+
squads: parallelPhases.map(p => p.tasks)
|
|
397
|
+
};
|
|
398
|
+
} catch {
|
|
399
|
+
return { useParallel: false, reason: 'Dependency analysis failed', squads: [] };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Detect if a request needs fusion workflow (uncertainty → run N agents → best-of-N)
|
|
405
|
+
* @param {string} userRequest
|
|
406
|
+
* @returns {boolean}
|
|
407
|
+
*/
|
|
408
|
+
export function detectFusionNeed(userRequest) {
|
|
409
|
+
const fusionKeywords = [
|
|
410
|
+
'uncertain', 'uncertainty', 'critical decision', 'compare approaches',
|
|
411
|
+
'best approach', 'prototype', 'explore options', 'which is better',
|
|
412
|
+
'not sure how', 'multiple solutions', 'benchmark'
|
|
413
|
+
];
|
|
414
|
+
const req = userRequest.toLowerCase();
|
|
415
|
+
return fusionKeywords.some(kw => req.includes(kw));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Detect if a request needs long-running workflow (L-Thread with stop hooks)
|
|
420
|
+
* @param {string} userRequest
|
|
421
|
+
* @returns {boolean}
|
|
422
|
+
*/
|
|
423
|
+
export function detectLongRunningNeed(userRequest) {
|
|
424
|
+
const longRunningKeywords = [
|
|
425
|
+
'autonomous', 'large scope', 'full system', 'end to end', 'e2e',
|
|
426
|
+
'complete implementation', 'no interruption', 'long running', 'unattended'
|
|
427
|
+
];
|
|
428
|
+
const req = userRequest.toLowerCase();
|
|
429
|
+
const fileCountMatch = req.match(/(\d+)\+?\s+files?/);
|
|
430
|
+
const fileCount = fileCountMatch ? parseInt(fileCountMatch[1]) : 0;
|
|
431
|
+
return longRunningKeywords.some(kw => req.includes(kw)) || fileCount > 10;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Check zero-touch eligibility for a feature (requires trust-manager)
|
|
436
|
+
* @param {string} feature
|
|
437
|
+
* @returns {Promise<boolean>}
|
|
438
|
+
*/
|
|
439
|
+
export async function checkZeroTouchEligibility(feature) {
|
|
440
|
+
try {
|
|
441
|
+
const { getTrustLevel } = await import('../../lib/trust/trust-manager.js').catch(() => ({
|
|
442
|
+
getTrustLevel: null
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
if (!getTrustLevel) return false;
|
|
446
|
+
|
|
447
|
+
const level = getTrustLevel(feature);
|
|
448
|
+
return level === 'maximum';
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Micro Agent Factory — Create ultra-specialized agents
|
|
3
|
+
*
|
|
4
|
+
* Micro-agents are stripped-down versions of base agents with:
|
|
5
|
+
* - Only the standards they need (1-3 files max)
|
|
6
|
+
* - A single focused mission
|
|
7
|
+
* - Minimal context overhead (~2-5K tokens vs 20-50K for full agent)
|
|
8
|
+
*
|
|
9
|
+
* Saved to: .morph/micro-agents/{name}.json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const MICRO_AGENTS_DIR = join(process.cwd(), '.morph/micro-agents');
|
|
16
|
+
const AGENTS_CONFIG = join(process.cwd(), '.morph/config/agents.json');
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Agent Creation
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a micro-agent from a base agent with a standards subset
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {string} opts.name - Micro-agent name (e.g., 'jwt-validator')
|
|
26
|
+
* @param {string} opts.baseAgent - Base agent ID (e.g., 'security-expert')
|
|
27
|
+
* @param {string[]} opts.standards - Relevant standards file paths (1-3 files)
|
|
28
|
+
* @param {string} opts.mission - Focused mission description
|
|
29
|
+
* @param {string[]} [opts.tools] - Tools this micro-agent uses (default: minimal set)
|
|
30
|
+
* @param {Object} [opts.constraints] - Additional constraints
|
|
31
|
+
* @returns {Object} Micro-agent config object
|
|
32
|
+
*/
|
|
33
|
+
export function createMicroAgent({ name, baseAgent, standards, mission, tools = null, constraints = {} }) {
|
|
34
|
+
if (!name || !baseAgent || !mission) {
|
|
35
|
+
throw new Error('name, baseAgent, and mission are required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (standards && standards.length > 5) {
|
|
39
|
+
throw new Error('Micro-agents should load at most 5 standards files (ideally 1-3)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const baseAgentConfig = loadBaseAgent(baseAgent);
|
|
43
|
+
|
|
44
|
+
const microAgent = {
|
|
45
|
+
id: name,
|
|
46
|
+
type: 'micro-agent',
|
|
47
|
+
baseAgent,
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
mission,
|
|
50
|
+
tier: (baseAgentConfig?.tier || 3) + 1, // micro-agents are tier+1 (more specialized)
|
|
51
|
+
domain: baseAgentConfig?.domain || 'general',
|
|
52
|
+
standards: standards || [],
|
|
53
|
+
tools: tools || ['Read', 'Write', 'Edit'],
|
|
54
|
+
constraints: {
|
|
55
|
+
maxStandards: 3,
|
|
56
|
+
maxFiles: 10,
|
|
57
|
+
focusOnly: mission,
|
|
58
|
+
...constraints
|
|
59
|
+
},
|
|
60
|
+
contextEstimate: {
|
|
61
|
+
standards: (standards || []).length * 2000,
|
|
62
|
+
overhead: 1500,
|
|
63
|
+
total: ((standards || []).length * 2000) + 1500
|
|
64
|
+
},
|
|
65
|
+
prompt: generateMicroAgentPrompt({ name, baseAgent, standards, mission, constraints, baseAgentConfig })
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return microAgent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Save a micro-agent to disk
|
|
73
|
+
* @param {Object} microAgent - Micro-agent config (from createMicroAgent)
|
|
74
|
+
* @returns {string} Path to saved file
|
|
75
|
+
*/
|
|
76
|
+
export function saveMicroAgent(microAgent) {
|
|
77
|
+
if (!existsSync(MICRO_AGENTS_DIR)) {
|
|
78
|
+
mkdirSync(MICRO_AGENTS_DIR, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filePath = join(MICRO_AGENTS_DIR, `${microAgent.id}.json`);
|
|
82
|
+
writeFileSync(filePath, JSON.stringify(microAgent, null, 2), 'utf8');
|
|
83
|
+
return filePath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* List all micro-agents
|
|
88
|
+
* @returns {Array} Array of micro-agent summaries
|
|
89
|
+
*/
|
|
90
|
+
export function listMicroAgents() {
|
|
91
|
+
if (!existsSync(MICRO_AGENTS_DIR)) return [];
|
|
92
|
+
|
|
93
|
+
return readdirSync(MICRO_AGENTS_DIR)
|
|
94
|
+
.filter(f => f.endsWith('.json'))
|
|
95
|
+
.map(f => {
|
|
96
|
+
try {
|
|
97
|
+
const agent = JSON.parse(readFileSync(join(MICRO_AGENTS_DIR, f), 'utf8'));
|
|
98
|
+
return {
|
|
99
|
+
id: agent.id,
|
|
100
|
+
baseAgent: agent.baseAgent,
|
|
101
|
+
mission: agent.mission,
|
|
102
|
+
standards: agent.standards?.length || 0,
|
|
103
|
+
contextEstimate: agent.contextEstimate?.total || 0,
|
|
104
|
+
createdAt: agent.createdAt
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get a specific micro-agent
|
|
115
|
+
* @param {string} name - Micro-agent name
|
|
116
|
+
* @returns {Object|null} Micro-agent config or null
|
|
117
|
+
*/
|
|
118
|
+
export function getMicroAgent(name) {
|
|
119
|
+
const filePath = join(MICRO_AGENTS_DIR, `${name}.json`);
|
|
120
|
+
if (!existsSync(filePath)) return null;
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Helpers
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
function loadBaseAgent(agentId) {
|
|
133
|
+
if (!existsSync(AGENTS_CONFIG)) return null;
|
|
134
|
+
try {
|
|
135
|
+
const config = JSON.parse(readFileSync(AGENTS_CONFIG, 'utf8'));
|
|
136
|
+
return config.agents?.find(a => a.id === agentId) || null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generateMicroAgentPrompt({ name, baseAgent, standards, mission, constraints, baseAgentConfig }) {
|
|
143
|
+
const standardsList = standards?.map(s => `- framework/standards/${s}`).join('\n') || '(none)';
|
|
144
|
+
|
|
145
|
+
return `You are a MICRO-AGENT: ${name}
|
|
146
|
+
Base: ${baseAgent} (${baseAgentConfig?.description || 'specialist'})
|
|
147
|
+
|
|
148
|
+
MISSION (single focus):
|
|
149
|
+
${mission}
|
|
150
|
+
|
|
151
|
+
STANDARDS TO LOAD (${standards?.length || 0} files):
|
|
152
|
+
${standardsList}
|
|
153
|
+
|
|
154
|
+
CONSTRAINTS:
|
|
155
|
+
- Focus ONLY on your mission above
|
|
156
|
+
- Do NOT load additional standards or context
|
|
157
|
+
- Maximum ${constraints.maxFiles || 10} files to read/write
|
|
158
|
+
- Report completion when done — do NOT start new tasks
|
|
159
|
+
|
|
160
|
+
This is a minimal-context agent. Stay focused.`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Engine — Metrics collection and JSONL storage
|
|
3
|
+
*
|
|
4
|
+
* Records events to append-only JSONL files:
|
|
5
|
+
* - .morph/analytics/threads-log.jsonl
|
|
6
|
+
* - .morph/analytics/context-log.jsonl
|
|
7
|
+
* - .morph/analytics/trust-log.jsonl
|
|
8
|
+
*
|
|
9
|
+
* Provides aggregation, ASCII chart generation, and 30-day dashboards.
|
|
10
|
+
* Auto-prunes entries older than 90 days.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
|
|
16
|
+
const ANALYTICS_DIR = join(process.cwd(), '.morph/analytics');
|
|
17
|
+
const THREADS_LOG = join(ANALYTICS_DIR, 'threads-log.jsonl');
|
|
18
|
+
const CONTEXT_LOG = join(ANALYTICS_DIR, 'context-log.jsonl');
|
|
19
|
+
const TRUST_LOG = join(ANALYTICS_DIR, 'trust-log.jsonl');
|
|
20
|
+
const PRUNE_DAYS = 90;
|
|
21
|
+
const DASHBOARD_DAYS = 30;
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Internal Helpers
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
function ensureAnalyticsDir() {
|
|
28
|
+
if (!existsSync(ANALYTICS_DIR)) {
|
|
29
|
+
mkdirSync(ANALYTICS_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendJSONL(filePath, record) {
|
|
34
|
+
ensureAnalyticsDir();
|
|
35
|
+
appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readJSONL(filePath) {
|
|
39
|
+
if (!existsSync(filePath)) return [];
|
|
40
|
+
|
|
41
|
+
const lines = readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean);
|
|
42
|
+
const records = [];
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
try {
|
|
46
|
+
records.push(JSON.parse(line));
|
|
47
|
+
} catch {
|
|
48
|
+
// Skip malformed lines
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return records;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pruneJSONL(filePath) {
|
|
55
|
+
if (!existsSync(filePath)) return;
|
|
56
|
+
|
|
57
|
+
const cutoff = new Date();
|
|
58
|
+
cutoff.setDate(cutoff.getDate() - PRUNE_DAYS);
|
|
59
|
+
|
|
60
|
+
const records = readJSONL(filePath);
|
|
61
|
+
const fresh = records.filter(r => {
|
|
62
|
+
const ts = new Date(r.timestamp || r.createdAt || 0);
|
|
63
|
+
return ts > cutoff;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (fresh.length < records.length) {
|
|
67
|
+
writeFileSync(filePath, fresh.map(r => JSON.stringify(r)).join('\n') + '\n', 'utf8');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function recentRecords(records, days = DASHBOARD_DAYS) {
|
|
72
|
+
const cutoff = new Date();
|
|
73
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
74
|
+
return records.filter(r => {
|
|
75
|
+
const ts = new Date(r.timestamp || r.createdAt || 0);
|
|
76
|
+
return ts > cutoff;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Event Recording
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Record a thread analytics event
|
|
86
|
+
* @param {Object} event
|
|
87
|
+
* @param {string} event.type - Event type (thread_created|thread_completed|thread_failed|checkpoint_passed|etc.)
|
|
88
|
+
* @param {string} event.feature - Feature name
|
|
89
|
+
* @param {string} [event.threadId] - Thread ID
|
|
90
|
+
* @param {string} [event.agent] - Agent name
|
|
91
|
+
* @param {Object} [event.data] - Additional event data
|
|
92
|
+
*/
|
|
93
|
+
export function recordEvent(event) {
|
|
94
|
+
const record = {
|
|
95
|
+
...event,
|
|
96
|
+
timestamp: new Date().toISOString()
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Route to appropriate log
|
|
100
|
+
if (event.type?.startsWith('context_') || event.type === 'token_usage') {
|
|
101
|
+
appendJSONL(CONTEXT_LOG, record);
|
|
102
|
+
} else if (event.type?.startsWith('trust_')) {
|
|
103
|
+
appendJSONL(TRUST_LOG, record);
|
|
104
|
+
} else {
|
|
105
|
+
appendJSONL(THREADS_LOG, record);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Record a context event (token usage, optimization, etc.)
|
|
111
|
+
* @param {Object} event
|
|
112
|
+
*/
|
|
113
|
+
export function recordContextEvent(event) {
|
|
114
|
+
recordEvent({ ...event, type: event.type || 'context_usage' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Record a trust level change event
|
|
119
|
+
* @param {string} feature
|
|
120
|
+
* @param {string} level - new trust level
|
|
121
|
+
* @param {string} reason
|
|
122
|
+
*/
|
|
123
|
+
export function recordTrustEvent(feature, level, reason = '') {
|
|
124
|
+
appendJSONL(TRUST_LOG, {
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
type: 'trust_level_changed',
|
|
127
|
+
feature,
|
|
128
|
+
level,
|
|
129
|
+
reason
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Record auto-approval event (gate approved by trust)
|
|
135
|
+
* @param {string} feature
|
|
136
|
+
* @param {string} gate - 'design' | 'tasks' | 'proposal'
|
|
137
|
+
* @param {string} trustLevel
|
|
138
|
+
* @param {number} passRate
|
|
139
|
+
*/
|
|
140
|
+
export function recordAutoApproval(feature, gate, trustLevel, passRate) {
|
|
141
|
+
appendJSONL(TRUST_LOG, {
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
type: 'trust_auto_approved',
|
|
144
|
+
feature,
|
|
145
|
+
gate,
|
|
146
|
+
trustLevel,
|
|
147
|
+
passRate,
|
|
148
|
+
timeSavedMinutes: 5 // estimated minutes saved per auto-approval
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get trust progression for a feature
|
|
154
|
+
* @param {string} featureName
|
|
155
|
+
* @returns {Array} Trust level history
|
|
156
|
+
*/
|
|
157
|
+
export function getTrustProgression(featureName) {
|
|
158
|
+
const events = readJSONL(TRUST_LOG)
|
|
159
|
+
.filter(e => e.feature === featureName && e.type === 'trust_level_changed')
|
|
160
|
+
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
161
|
+
return events;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Feature Analytics
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get analytics for a specific feature
|
|
170
|
+
* @param {string} feature - Feature name
|
|
171
|
+
* @returns {Object} Feature analytics
|
|
172
|
+
*/
|
|
173
|
+
export function getFeatureAnalytics(feature) {
|
|
174
|
+
const allThreadEvents = readJSONL(THREADS_LOG).filter(r => r.feature === feature);
|
|
175
|
+
const allContextEvents = readJSONL(CONTEXT_LOG).filter(r => r.feature === feature);
|
|
176
|
+
|
|
177
|
+
const threadsByStatus = allThreadEvents
|
|
178
|
+
.filter(e => e.type === 'thread_completed' || e.type === 'thread_failed')
|
|
179
|
+
.reduce((acc, e) => {
|
|
180
|
+
const status = e.type === 'thread_completed' ? 'completed' : 'failed';
|
|
181
|
+
acc[status] = (acc[status] || 0) + 1;
|
|
182
|
+
return acc;
|
|
183
|
+
}, {});
|
|
184
|
+
|
|
185
|
+
const checkpoints = allThreadEvents.filter(e =>
|
|
186
|
+
e.type === 'checkpoint_passed' || e.type === 'checkpoint_failed'
|
|
187
|
+
);
|
|
188
|
+
const checkpointPassRate = checkpoints.length > 0
|
|
189
|
+
? Math.round(checkpoints.filter(e => e.type === 'checkpoint_passed').length / checkpoints.length * 100)
|
|
190
|
+
: 100;
|
|
191
|
+
|
|
192
|
+
const totalDuration = allThreadEvents
|
|
193
|
+
.filter(e => e.type === 'thread_completed' && e.data?.duration)
|
|
194
|
+
.reduce((sum, e) => sum + e.data.duration, 0);
|
|
195
|
+
|
|
196
|
+
const toolCallEvents = allThreadEvents.filter(e => e.type === 'tool_call');
|
|
197
|
+
const tokenEvents = allContextEvents.filter(e => e.data?.tokensUsed);
|
|
198
|
+
const totalTokens = tokenEvents.reduce((sum, e) => sum + (e.data.tokensUsed || 0), 0);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
feature,
|
|
202
|
+
threads: threadsByStatus,
|
|
203
|
+
checkpointPassRate,
|
|
204
|
+
totalDuration,
|
|
205
|
+
totalToolCalls: toolCallEvents.length,
|
|
206
|
+
totalTokensUsed: totalTokens,
|
|
207
|
+
optimizationEvents: allContextEvents.filter(e => e.type === 'context_optimized').length
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get project-wide analytics dashboard (30-day summary)
|
|
213
|
+
* @returns {Object} Project analytics
|
|
214
|
+
*/
|
|
215
|
+
export function getProjectAnalytics() {
|
|
216
|
+
const allThreadEvents = recentRecords(readJSONL(THREADS_LOG));
|
|
217
|
+
const allContextEvents = recentRecords(readJSONL(CONTEXT_LOG));
|
|
218
|
+
const allTrustEvents = recentRecords(readJSONL(TRUST_LOG));
|
|
219
|
+
|
|
220
|
+
const features = [...new Set(allThreadEvents.map(e => e.feature).filter(Boolean))];
|
|
221
|
+
|
|
222
|
+
const completedThreads = allThreadEvents.filter(e => e.type === 'thread_completed');
|
|
223
|
+
const failedThreads = allThreadEvents.filter(e => e.type === 'thread_failed');
|
|
224
|
+
const checkpoints = allThreadEvents.filter(e =>
|
|
225
|
+
e.type === 'checkpoint_passed' || e.type === 'checkpoint_failed'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const parallelEvents = allThreadEvents.filter(e => e.data?.type === 'parallel');
|
|
229
|
+
const avgParallel = parallelEvents.length > 0
|
|
230
|
+
? parallelEvents.reduce((sum, e) => sum + (e.data?.concurrency || 1), 0) / parallelEvents.length
|
|
231
|
+
: 1;
|
|
232
|
+
|
|
233
|
+
const autoApprovalEvents = allTrustEvents.filter(e => e.type === 'trust_auto_approved');
|
|
234
|
+
const avgTokens = allContextEvents.filter(e => e.data?.tokensUsed).length > 0
|
|
235
|
+
? Math.round(allContextEvents.reduce((sum, e) => sum + (e.data?.tokensUsed || 0), 0) / allContextEvents.filter(e => e.data?.tokensUsed).length)
|
|
236
|
+
: 0;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
period: `${DASHBOARD_DAYS} days`,
|
|
240
|
+
features: features.length,
|
|
241
|
+
threads: {
|
|
242
|
+
total: completedThreads.length + failedThreads.length,
|
|
243
|
+
completed: completedThreads.length,
|
|
244
|
+
failed: failedThreads.length
|
|
245
|
+
},
|
|
246
|
+
checkpoints: {
|
|
247
|
+
total: checkpoints.length,
|
|
248
|
+
passed: checkpoints.filter(e => e.type === 'checkpoint_passed').length,
|
|
249
|
+
passRate: checkpoints.length > 0
|
|
250
|
+
? Math.round(checkpoints.filter(e => e.type === 'checkpoint_passed').length / checkpoints.length * 100)
|
|
251
|
+
: 100
|
|
252
|
+
},
|
|
253
|
+
parallelization: {
|
|
254
|
+
avgConcurrency: Math.round(avgParallel * 10) / 10,
|
|
255
|
+
parallelThreads: parallelEvents.length
|
|
256
|
+
},
|
|
257
|
+
trust: {
|
|
258
|
+
autoApprovals: autoApprovalEvents.length,
|
|
259
|
+
trustChanges: allTrustEvents.filter(e => e.type === 'trust_level_changed').length
|
|
260
|
+
},
|
|
261
|
+
context: {
|
|
262
|
+
avgTokensPerSession: avgTokens,
|
|
263
|
+
optimizations: allContextEvents.filter(e => e.type === 'context_optimized').length
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// ASCII Charts
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Generate an ASCII bar chart
|
|
274
|
+
* @param {Object} data - { label: value }
|
|
275
|
+
* @param {Object} [opts]
|
|
276
|
+
* @param {number} [opts.width=40] - Chart width in chars
|
|
277
|
+
* @param {string} [opts.title] - Chart title
|
|
278
|
+
* @returns {string} ASCII chart string
|
|
279
|
+
*/
|
|
280
|
+
export function generateAsciiChart(data, { width = 40, title = '' } = {}) {
|
|
281
|
+
const entries = Object.entries(data);
|
|
282
|
+
if (entries.length === 0) return ' (no data)';
|
|
283
|
+
|
|
284
|
+
const maxValue = Math.max(...entries.map(([, v]) => v), 1);
|
|
285
|
+
const maxLabelLen = Math.max(...entries.map(([k]) => k.length));
|
|
286
|
+
const lines = [];
|
|
287
|
+
|
|
288
|
+
if (title) {
|
|
289
|
+
lines.push(` ${title}`);
|
|
290
|
+
lines.push(' ' + '─'.repeat(width + maxLabelLen + 5));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const [label, value] of entries) {
|
|
294
|
+
const barLen = Math.round((value / maxValue) * width);
|
|
295
|
+
const bar = '█'.repeat(barLen);
|
|
296
|
+
const paddedLabel = label.padStart(maxLabelLen);
|
|
297
|
+
lines.push(` ${paddedLabel} │${bar} ${value}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return lines.join('\n');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generate thread timeline ASCII chart
|
|
305
|
+
* @param {Array} events - Array of thread events with timestamps
|
|
306
|
+
* @param {string} label - Chart label
|
|
307
|
+
* @returns {string} ASCII timeline
|
|
308
|
+
*/
|
|
309
|
+
export function generateTimelineChart(events, label = 'Timeline') {
|
|
310
|
+
if (events.length === 0) return ' (no data)';
|
|
311
|
+
|
|
312
|
+
const now = new Date();
|
|
313
|
+
const days = Array.from({ length: 7 }, (_, i) => {
|
|
314
|
+
const d = new Date(now);
|
|
315
|
+
d.setDate(d.getDate() - (6 - i));
|
|
316
|
+
return d.toLocaleDateString('en-US', { weekday: 'short' });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const counts = new Array(7).fill(0);
|
|
320
|
+
for (const event of events) {
|
|
321
|
+
const ts = new Date(event.timestamp);
|
|
322
|
+
const daysAgo = Math.floor((now - ts) / (1000 * 60 * 60 * 24));
|
|
323
|
+
if (daysAgo < 7) {
|
|
324
|
+
counts[6 - daysAgo]++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const data = {};
|
|
329
|
+
days.forEach((d, i) => { data[d] = counts[i]; });
|
|
330
|
+
|
|
331
|
+
return generateAsciiChart(data, { title: label });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Maintenance
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Prune all analytics logs (remove entries older than 90 days)
|
|
340
|
+
*/
|
|
341
|
+
export function pruneAnalytics() {
|
|
342
|
+
pruneJSONL(THREADS_LOG);
|
|
343
|
+
pruneJSONL(CONTEXT_LOG);
|
|
344
|
+
pruneJSONL(TRUST_LOG);
|
|
345
|
+
}
|