@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.
Files changed (132) hide show
  1. package/bin/morph-spec.js +283 -8
  2. package/bin/validate.js +4 -4
  3. package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
  4. package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
  5. package/docs/next-generation/EXECUTION-FLOW.md +274 -0
  6. package/docs/next-generation/META-PROMPTS.md +235 -0
  7. package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
  8. package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
  9. package/package.json +5 -5
  10. package/src/commands/agents/agents-fuse.js +96 -0
  11. package/src/commands/agents/micro-agent.js +112 -0
  12. package/src/commands/agents/spawn-team.js +69 -4
  13. package/src/commands/agents/squad-template.js +146 -0
  14. package/src/commands/analytics/analytics.js +176 -0
  15. package/src/commands/context/context-prime.js +63 -0
  16. package/src/commands/context/core-four.js +54 -0
  17. package/src/commands/mcp/mcp.js +102 -0
  18. package/src/commands/project/detect-agents.js +1 -1
  19. package/src/commands/project/doctor.js +573 -356
  20. package/src/commands/project/init.js +1 -1
  21. package/src/commands/project/update.js +1 -1
  22. package/src/commands/state/advance-phase.js +433 -416
  23. package/src/commands/templates/template-render.js +80 -1
  24. package/src/commands/threads/thread-template.js +103 -0
  25. package/src/commands/threads/threads.js +261 -0
  26. package/src/commands/trust/trust.js +205 -0
  27. package/src/{orchestrator.js → core/orchestrator.js} +8 -8
  28. package/src/core/state/state-manager.js +18 -2
  29. package/src/core/workflows/workflow-detector.js +100 -2
  30. package/src/lib/agents/micro-agent-factory.js +161 -0
  31. package/src/lib/analytics/analytics-engine.js +345 -0
  32. package/src/lib/checkpoints/checkpoint-hooks.js +293 -258
  33. package/src/lib/context/context-bundler.js +240 -0
  34. package/src/lib/context/context-optimizer.js +212 -0
  35. package/src/lib/context/context-tracker.js +273 -0
  36. package/src/lib/context/core-four-tracker.js +201 -0
  37. package/src/lib/context/mcp-optimizer.js +200 -0
  38. package/src/lib/execution/fusion-executor.js +304 -0
  39. package/src/lib/execution/parallel-executor.js +270 -0
  40. package/src/lib/generators/context-generator.js +3 -3
  41. package/src/lib/generators/recap-generator.js +2 -2
  42. package/src/lib/hooks/hook-executor.js +169 -0
  43. package/src/lib/hooks/stop-hook-executor.js +286 -0
  44. package/src/lib/hops/hop-composer.js +221 -0
  45. package/src/lib/threads/thread-coordinator.js +238 -0
  46. package/src/lib/threads/thread-manager.js +317 -0
  47. package/src/lib/tracking/artifact-trail.js +202 -0
  48. package/src/lib/trust/trust-manager.js +269 -0
  49. package/src/lib/validators/design-system/design-system-validator.js +2 -2
  50. package/src/lib/validators/validation-runner.js +6 -6
  51. package/stacks/blazor-azure/.morph/config/agents.json +72 -3
  52. package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
  53. package/CLAUDE.md +0 -993
  54. package/docs/llm-interaction-config.md +0 -735
  55. package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
  56. package/src/commands/utils/migrate-state.js +0 -158
  57. package/src/commands/utils/upgrade.js +0 -346
  58. package/src/lib/validators/architecture-validator.js +0 -60
  59. package/src/lib/validators/content-validator.js +0 -164
  60. package/src/lib/validators/package-validator.js +0 -61
  61. package/src/lib/validators/ui-contrast-validator.js +0 -44
  62. package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
  63. package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
  64. package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
  65. package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
  66. package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
  67. package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
  68. package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
  69. package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
  70. package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
  71. package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
  72. package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
  73. package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
  74. package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
  75. package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
  76. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
  77. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
  78. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
  79. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
  80. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
  81. package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
  82. package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
  83. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
  84. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
  85. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
  86. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
  87. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
  88. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
  89. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
  90. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
  91. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
  92. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
  93. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
  94. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
  95. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
  96. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
  97. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
  98. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
  99. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
  100. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
  101. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
  102. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
  103. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
  104. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
  105. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
  106. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
  107. package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
  108. package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
  109. package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
  110. package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
  111. package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
  112. package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
  113. package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
  114. package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
  115. package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
  116. package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
  117. package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
  118. package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
  119. package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
  120. package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
  121. package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
  122. package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
  123. package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
  124. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
  125. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
  126. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
  127. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
  128. /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
  129. /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
  130. /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
  131. /package/docs/{v3.0 → next-generation}/README.md +0 -0
  132. /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: "2.1.1",
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/lib/ → root)
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
+ }