mindforge-cc 2.0.0-alpha.8 → 2.0.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 (63) hide show
  1. package/.agent/CLAUDE.md +26 -0
  2. package/.agent/mindforge/execute-phase.md +23 -0
  3. package/.agent/mindforge/install-skill.md +20 -11
  4. package/.agent/mindforge/learn.md +142 -0
  5. package/.agent/mindforge/marketplace.md +120 -0
  6. package/.agent/mindforge/new-runtime.md +19 -0
  7. package/.agent/mindforge/remember.md +16 -4
  8. package/.claude/CLAUDE.md +26 -0
  9. package/.claude/commands/mindforge/execute-phase.md +23 -0
  10. package/.claude/commands/mindforge/learn.md +142 -0
  11. package/.claude/commands/mindforge/marketplace.md +120 -0
  12. package/.claude/commands/mindforge/new-runtime.md +19 -0
  13. package/.mindforge/distribution/marketplace.md +53 -0
  14. package/.mindforge/org/skills/MANIFEST.md +1 -0
  15. package/.mindforge/production/production-checklist.md +34 -123
  16. package/.mindforge/skills-builder/auto-capture-protocol.md +88 -0
  17. package/.mindforge/skills-builder/learn-protocol.md +161 -0
  18. package/.mindforge/skills-builder/quality-scoring.md +120 -0
  19. package/.planning/AUDIT.jsonl +1 -0
  20. package/.planning/decisions/ADR-036-learn-command-docs-as-skill-source.md +26 -0
  21. package/.planning/decisions/ADR-037-auto-capture-frequency-threshold.md +26 -0
  22. package/.planning/decisions/ADR-038-skill-quality-minimum-60.md +27 -0
  23. package/CHANGELOG.md +59 -1
  24. package/MINDFORGE.md +9 -0
  25. package/README.md +42 -5
  26. package/bin/autonomous/auto-runner.js +12 -0
  27. package/bin/install.js +9 -3
  28. package/bin/installer-core.js +141 -27
  29. package/bin/migrations/1.0.0-to-2.0.0.js +115 -0
  30. package/bin/migrations/schema-versions.js +12 -0
  31. package/bin/mindforge-cc.sh +5 -0
  32. package/bin/mindforge-cli.js +35 -0
  33. package/bin/review/cross-review-engine.js +11 -0
  34. package/bin/skill-registry.js +167 -0
  35. package/bin/skill-validator.js +144 -0
  36. package/bin/skills-builder/learn-cli.js +57 -0
  37. package/bin/skills-builder/marketplace-cli.js +54 -0
  38. package/bin/skills-builder/marketplace-client.js +198 -0
  39. package/bin/skills-builder/pattern-detector.js +144 -0
  40. package/bin/skills-builder/skill-generator.js +258 -0
  41. package/bin/skills-builder/skill-registrar.js +107 -0
  42. package/bin/skills-builder/skill-scorer.js +263 -0
  43. package/bin/skills-builder/source-loader.js +268 -0
  44. package/bin/wizard/environment-detector.js +1 -0
  45. package/docs/architecture/README.md +6 -1
  46. package/docs/architecture/adr-039-multi-runtime-support.md +20 -0
  47. package/docs/architecture/adr-040-additive-schema-migration.md +21 -0
  48. package/docs/architecture/adr-041-stable-runtime-interface-contract.md +20 -0
  49. package/docs/architecture/decision-records-index.md +3 -0
  50. package/docs/commands-reference.md +3 -0
  51. package/docs/mindforge-md-reference.md +4 -0
  52. package/docs/skills-authoring-guide.md +29 -0
  53. package/docs/skills-publishing-guide.md +3 -2
  54. package/docs/testing-current-version.md +3 -3
  55. package/docs/upgrade.md +16 -2
  56. package/docs/user-guide.md +57 -8
  57. package/docs/usp-features.md +21 -6
  58. package/package.json +1 -1
  59. package/.mindforge/memory/decision-library.jsonl +0 -0
  60. package/.mindforge/memory/knowledge-base.jsonl +0 -7
  61. package/.mindforge/memory/pattern-library.jsonl +0 -1
  62. package/.mindforge/memory/team-preferences.jsonl +0 -4
  63. package/.planning/browser-daemon.log +0 -32
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MindForge Skill Validator — v1.0.0
4
+ * Validates SKILL.md files against schema and content requirements.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ARGS = process.argv.slice(2);
13
+ const NO_COLOR = ARGS.includes('--no-color');
14
+
15
+ const colors = {
16
+ reset: NO_COLOR ? '' : '\x1b[0m',
17
+ red: NO_COLOR ? '' : '\x1b[31m',
18
+ green: NO_COLOR ? '' : '\x1b[32m',
19
+ yellow: NO_COLOR ? '' : '\x1b[33m',
20
+ cyan: NO_COLOR ? '' : '\x1b[36m',
21
+ bold: NO_COLOR ? '' : '\x1b[1m'
22
+ };
23
+
24
+ function main() {
25
+ const target = ARGS[0];
26
+ if (!target || ARGS.includes('--help') || ARGS.includes('-h')) {
27
+ console.log('\nUsage: mindforge-cc validate-skill <path-to-SKILL.md>\n');
28
+ process.exit(0);
29
+ }
30
+
31
+ const filePath = path.resolve(process.cwd(), target);
32
+ if (!fs.existsSync(filePath)) {
33
+ console.error(`${colors.red}❌ File not found: ${filePath}${colors.reset}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log(`\n${colors.bold}MindForge Skill Validator — ${path.basename(filePath)}${colors.reset}`);
38
+ console.log('─'.repeat(60));
39
+
40
+ const content = fs.readFileSync(filePath, 'utf8');
41
+ const results = { schema: [], content: [], quality: [], valid: true };
42
+
43
+ // ── Level 1: Schema ─────────────────────────────────────────────────────────
44
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
45
+ if (!fmMatch) {
46
+ results.schema.push({ ok: false, msg: 'Missing frontmatter (--- delimiters)' });
47
+ results.valid = false;
48
+ } else {
49
+ results.schema.push({ ok: true, msg: 'Frontmatter delimiters present' });
50
+ const fmLines = fmMatch[1].split('\n');
51
+ const fm = {};
52
+ fmLines.forEach(line => {
53
+ const parts = line.split(':');
54
+ if (parts.length >= 2) fm[parts[0].trim()] = parts.slice(1).join(':').trim();
55
+ });
56
+
57
+ // name
58
+ if (!fm.name) {
59
+ results.schema.push({ ok: false, msg: 'Missing field: name' });
60
+ results.valid = false;
61
+ } else if (!/^[a-z][a-z0-9-]+$/.test(fm.name)) {
62
+ results.schema.push({ ok: false, msg: `Invalid name format: ${fm.name} (must be kebab-case)` });
63
+ results.valid = false;
64
+ } else {
65
+ results.schema.push({ ok: true, msg: `name: ${fm.name}` });
66
+ }
67
+
68
+ // version
69
+ if (!fm.version) {
70
+ results.schema.push({ ok: false, msg: 'Missing field: version' });
71
+ results.valid = false;
72
+ } else if (!/^\d+\.\d+\.\d+$/.test(fm.version)) {
73
+ results.schema.push({ ok: false, msg: `Invalid version: ${fm.version} (must be x.y.z)` });
74
+ results.valid = false;
75
+ } else {
76
+ results.schema.push({ ok: true, msg: `version: ${fm.version}` });
77
+ }
78
+
79
+ // status
80
+ const validStatuses = ['stable', 'beta', 'alpha', 'deprecated'];
81
+ if (!fm.status || !validStatuses.includes(fm.status)) {
82
+ results.schema.push({ ok: false, msg: `Invalid status: ${fm.status}` });
83
+ results.valid = false;
84
+ } else {
85
+ results.schema.push({ ok: true, msg: `status: ${fm.status}` });
86
+ }
87
+
88
+ // triggers
89
+ if (!fm.triggers) {
90
+ results.schema.push({ ok: false, msg: 'Missing field: triggers' });
91
+ results.valid = false;
92
+ } else {
93
+ const triggers = fm.triggers.split(',').map(t => t.trim()).filter(Boolean);
94
+ if (triggers.length < 5) {
95
+ results.schema.push({ ok: false, msg: `Too few triggers: ${triggers.length} (min: 5)` });
96
+ results.valid = false;
97
+ } else {
98
+ results.schema.push({ ok: true, msg: `triggers: ${triggers.length} keywords` });
99
+ }
100
+ }
101
+ }
102
+
103
+ // ── Level 2: Content ────────────────────────────────────────────────────────
104
+ results.content.push({
105
+ ok: content.length >= 1024 && content.length <= 200 * 1024,
106
+ msg: `File size: ${(content.length / 1024).toFixed(1)}KB (1KB-200KB)`
107
+ });
108
+
109
+ results.content.push({
110
+ ok: /##\s+(Mandatory actions|When this skill is active)/i.test(content),
111
+ msg: 'Mandatory actions section present'
112
+ });
113
+
114
+ results.content.push({
115
+ ok: /- \[ \]/.test(content),
116
+ msg: 'Self-check/checklist items found'
117
+ });
118
+
119
+ results.content.push({
120
+ ok: !/IGNORE ALL PREVIOUS/i.test(content),
121
+ msg: 'No injection patterns detected'
122
+ });
123
+
124
+ // ── Output ──────────────────────────────────────────────────────────────────
125
+ console.log(`${colors.cyan}${colors.bold}Schema validation:${colors.reset}`);
126
+ results.schema.forEach(r => console.log(` ${r.ok ? colors.green + '✅' : colors.red + '❌'} ${r.msg}`));
127
+
128
+ console.log(`\n${colors.cyan}${colors.bold}Content validation:${colors.reset}`);
129
+ results.content.forEach(r => {
130
+ if (!r.ok) results.valid = false;
131
+ console.log(` ${r.ok ? colors.green + '✅' : colors.red + '❌'} ${r.msg}`);
132
+ });
133
+
134
+ console.log('─'.repeat(60));
135
+ if (results.valid) {
136
+ console.log(`${colors.green}${colors.bold}Result: VALID${colors.reset}`);
137
+ process.exit(0);
138
+ } else {
139
+ console.log(`${colors.red}${colors.bold}Result: INVALID${colors.reset}`);
140
+ process.exit(1);
141
+ }
142
+ }
143
+
144
+ main();
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MindForge v2 — Learn Skill CLI
4
+ * Ingests content from a source and generates a validated SKILL.md.
5
+ */
6
+ 'use strict';
7
+
8
+ const Loader = require('./source-loader');
9
+ const Generator = require('./skill-generator');
10
+ const Scorer = require('./skill-scorer');
11
+ const Registrar = require('./skill-registrar');
12
+
13
+ const ARGS = process.argv.slice(2);
14
+ const SOURCE = ARGS[0];
15
+ const NAME = ARGS[1] || 'new-skill';
16
+
17
+ if (!SOURCE) {
18
+ console.log('Usage: node bin/skills-builder/learn-cli.js <source-url-or-path> [skill-name]');
19
+ process.exit(1);
20
+ }
21
+
22
+ async function main() {
23
+ try {
24
+ console.log(`\n📚 Learning skill: ${NAME} from ${SOURCE}`);
25
+
26
+ // 1. Load
27
+ const { content, metadata } = await Loader.load(SOURCE);
28
+
29
+ // 2. Generate
30
+ const result = await Generator.generate({
31
+ skillName: NAME,
32
+ content,
33
+ sourceMetadata: metadata,
34
+ sessionId: 'cli-learn'
35
+ });
36
+
37
+ // 3. Score
38
+ const scoreResult = Scorer.score(result.skillPath);
39
+ console.log(`\n✅ Skill generated: ${result.skillPath}`);
40
+ console.log(`⭐ Quality Score: ${scoreResult.quality_score}/100 (${scoreResult.threshold_status})`);
41
+
42
+ // 4. Register if acceptable
43
+ if (scoreResult.can_register) {
44
+ Registrar.register(result.skillPath, 'project');
45
+ console.log('📝 Registered in MANIFEST.md');
46
+ } else {
47
+ console.log('⚠️ Score too low for automatic registration.');
48
+ scoreResult.improvement_suggestions.forEach(s => console.log(` - ${s}`));
49
+ }
50
+
51
+ } catch (err) {
52
+ console.error(`\n❌ Error: ${err.message}`);
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ main();
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MindForge v2 — Marketplace CLI
4
+ * Searched and manages community skills via npm registry.
5
+ */
6
+ 'use strict';
7
+
8
+ const Marketplace = require('./marketplace-client');
9
+
10
+ const ARGS = process.argv.slice(2);
11
+ const CMD = ARGS[0];
12
+ const QUERY = ARGS[1];
13
+
14
+ if (!CMD) {
15
+ console.log('Usage: node bin/skills-builder/marketplace-cli.js <search|featured|trending|install> [query]');
16
+ process.exit(1);
17
+ }
18
+
19
+ async function main() {
20
+ try {
21
+ switch (CMD) {
22
+ case 'search':
23
+ const results = await Marketplace.search(QUERY);
24
+ console.table(results.map(r => ({
25
+ name: r.name,
26
+ version: r.version,
27
+ score: r.quality?.score || 'N/A',
28
+ description: r.description.slice(0, 50) + '...'
29
+ })));
30
+ break;
31
+
32
+ case 'featured':
33
+ case 'trending':
34
+ const list = await Marketplace.getFeatured();
35
+ console.table(list);
36
+ break;
37
+
38
+ case 'install':
39
+ if (!QUERY) throw new Error('Package name required for install');
40
+ await Marketplace.install(QUERY);
41
+ console.log(`✅ Successfully installed ${QUERY}`);
42
+ break;
43
+
44
+ default:
45
+ console.error(`Unknown command: ${CMD}`);
46
+ process.exit(1);
47
+ }
48
+ } catch (err) {
49
+ console.error(`\n❌ Error: ${err.message}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ main();
@@ -0,0 +1,198 @@
1
+ /**
2
+ * MindForge v2 — Marketplace Client
3
+ * Interface to the MindForge Community Skills Marketplace.
4
+ *
5
+ * The marketplace is a curated layer on top of the npm registry.
6
+ * Skills are npm packages with the `mindforge-skill-` prefix.
7
+ *
8
+ * For this implementation: uses npm registry API directly.
9
+ * A dedicated marketplace API (registry.mindforge.dev) would be used
10
+ * when it becomes available.
11
+ */
12
+ 'use strict';
13
+
14
+ const https = require('https');
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+
18
+ const SKILL_PREFIX = 'mindforge-skill-';
19
+ const NPM_REGISTRY = 'https://registry.npmjs.org';
20
+ const NPM_SEARCH_API = 'https://registry.npmjs.org/-/v1/search';
21
+ const MINDFORGEMD_PATH = path.join(process.cwd(), 'MINDFORGE.md');
22
+
23
+ // ── Config reader ─────────────────────────────────────────────────────────────
24
+ function getConfig() {
25
+ const defaults = {
26
+ MARKETPLACE_REGISTRY: NPM_SEARCH_API,
27
+ MARKETPLACE_DAILY_FETCH_LIMIT: 50,
28
+ };
29
+ if (!fs.existsSync(MINDFORGEMD_PATH)) return defaults;
30
+ const content = fs.readFileSync(MINDFORGEMD_PATH, 'utf8');
31
+ for (const [key, defaultVal] of Object.entries(defaults)) {
32
+ const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));
33
+ if (match) defaults[key] = match[1].trim();
34
+ }
35
+ return defaults;
36
+ }
37
+
38
+ // ── HTTP helper ───────────────────────────────────────────────────────────────
39
+ function httpsGet(url) {
40
+ return new Promise((resolve, reject) => {
41
+ const req = https.get(url, {
42
+ headers: { 'User-Agent': 'MindForge-Marketplace/2.0', 'Accept': 'application/json' },
43
+ timeout: 15_000,
44
+ }, res => {
45
+ let body = '';
46
+ res.on('data', c => (body += c));
47
+ res.on('end', () => {
48
+ try { resolve(JSON.parse(body)); }
49
+ catch { reject(new Error(`Invalid JSON from ${url.slice(0, 80)}`)); }
50
+ });
51
+ });
52
+ req.on('error', reject);
53
+ req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout: ${url}`)); });
54
+ });
55
+ }
56
+
57
+ // ── Search ────────────────────────────────────────────────────────────────────
58
+ async function search(query, limit = 10) {
59
+ const encoded = encodeURIComponent(`${SKILL_PREFIX} ${query}`);
60
+ const url = `${NPM_SEARCH_API}?text=${encoded}&size=${Math.min(limit, 50)}`;
61
+
62
+ const data = await httpsGet(url);
63
+ const objects = data.objects || [];
64
+
65
+ return objects
66
+ .filter(o => o.package?.name?.startsWith(SKILL_PREFIX))
67
+ .map(o => ({
68
+ name: o.package.name,
69
+ display_name: o.package.name.replace(SKILL_PREFIX, '').replace(/-/g, ' '),
70
+ description: o.package.description || '',
71
+ version: o.package.version,
72
+ author: o.package.publisher?.username || o.package.author?.name || 'unknown',
73
+ date: o.package.date,
74
+ keywords: o.package.keywords || [],
75
+ links: o.package.links || {},
76
+ // Quality signals from npm (proxy until dedicated marketplace)
77
+ download_count: o.downloads?.weekly || null,
78
+ }));
79
+ }
80
+
81
+ // ── Featured skills ───────────────────────────────────────────────────────────
82
+ const FEATURED_SKILLS = [
83
+ { name: `${SKILL_PREFIX}db-postgres-advanced`, category: 'Database', description: 'Advanced PostgreSQL patterns, indexes, partitioning, and query optimisation' },
84
+ { name: `${SKILL_PREFIX}api-graphql`, category: 'API', description: 'GraphQL schema design, N+1 prevention, pagination, and subscriptions' },
85
+ { name: `${SKILL_PREFIX}frontend-react-patterns`, category: 'Frontend', description: 'React composition patterns, memo/callback, Suspense, and Server Components' },
86
+ { name: `${SKILL_PREFIX}infra-terraform`, category: 'Infra', description: 'Terraform module structure, state management, and production best practices' },
87
+ { name: `${SKILL_PREFIX}fintech-pci-compliance`, category: 'Compliance', description: 'PCI DSS Level 1 implementation requirements for payment processing' },
88
+ { name: `${SKILL_PREFIX}healthtech-hipaa`, category: 'Compliance', description: 'HIPAA Security Rule technical safeguards for PHI handling' },
89
+ { name: `${SKILL_PREFIX}ecommerce-stripe`, category: 'Payments', description: 'Stripe Elements, webhooks, idempotency, and subscription lifecycle' },
90
+ ];
91
+
92
+ async function getFeatured() {
93
+ // Try to fetch actual data for each featured skill
94
+ const results = [];
95
+ for (const skill of FEATURED_SKILLS) {
96
+ try {
97
+ const url = `${NPM_REGISTRY}/${encodeURIComponent(skill.name)}`;
98
+ const data = await httpsGet(url);
99
+ results.push({
100
+ ...skill,
101
+ version: data['dist-tags']?.latest || '1.0.0',
102
+ date: data.time?.modified,
103
+ exists: true,
104
+ });
105
+ } catch {
106
+ // Skill not yet published — show as coming soon
107
+ results.push({ ...skill, exists: false, version: 'coming soon' });
108
+ }
109
+ }
110
+ return results;
111
+ }
112
+
113
+ // ── Trending ──────────────────────────────────────────────────────────────────
114
+ async function getTrending(limit = 10) {
115
+ // Use npm search sorted by popularity
116
+ const url = `${NPM_SEARCH_API}?text=${encodeURIComponent(SKILL_PREFIX)}&size=${limit}&ranking=popularity`;
117
+ const data = await httpsGet(url);
118
+ return (data.objects || [])
119
+ .filter(o => o.package?.name?.startsWith(SKILL_PREFIX))
120
+ .map(o => ({
121
+ name: o.package.name,
122
+ description: o.package.description || '',
123
+ version: o.package.version,
124
+ date: o.package.date,
125
+ score: o.score?.final || 0,
126
+ }))
127
+ .sort((a, b) => b.score - a.score);
128
+ }
129
+
130
+ // ── Install from marketplace ──────────────────────────────────────────────────
131
+ async function install(skillName, tier = 'project') {
132
+ // Ensure the package name has the skill prefix
133
+ const packageName = skillName.startsWith(SKILL_PREFIX)
134
+ ? skillName
135
+ : `${SKILL_PREFIX}${skillName}`;
136
+
137
+ // Verify package exists on npm first
138
+ try {
139
+ const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
140
+ await httpsGet(url);
141
+ } catch {
142
+ throw new Error(`Skill not found on marketplace: ${packageName}`);
143
+ }
144
+
145
+ // Use MindForge's existing install-skill command machinery
146
+ return {
147
+ install_command: `/mindforge:install-skill ${packageName} --tier ${tier}`,
148
+ package_name: packageName,
149
+ message: `Run the install command above, or execute: npm install ${packageName}`,
150
+ };
151
+ }
152
+
153
+ // ── Format results for display ────────────────────────────────────────────────
154
+ function formatSearchResults(results, query) {
155
+ if (results.length === 0) {
156
+ return `🔍 No marketplace skills found for "${query}"\n\n` +
157
+ `Try broader terms, or create your own with:\n /mindforge:learn [url|path]`;
158
+ }
159
+
160
+ const lines = [`🏪 Marketplace results for "${query}" (${results.length} found)\n`];
161
+ results.forEach((r, i) => {
162
+ const name = r.display_name || r.name.replace(SKILL_PREFIX, '').replace(/-/g, ' ');
163
+ lines.push(` ${i + 1}. ${name} (${r.version})`);
164
+ lines.push(` ${r.description.slice(0, 100)}`);
165
+ if (r.download_count) lines.push(` ${r.download_count} downloads/week`);
166
+ lines.push('');
167
+ });
168
+ lines.push(`Install: /mindforge:marketplace install [name] [--tier project|org]`);
169
+ return lines.join('\n');
170
+ }
171
+
172
+ function formatFeatured(featured) {
173
+ const lines = ['🏪 MindForge Community Skills Marketplace\n Featured Skills\n'];
174
+
175
+ const byCategory = {};
176
+ featured.forEach(s => {
177
+ if (!byCategory[s.category]) byCategory[s.category] = [];
178
+ byCategory[s.category].push(s);
179
+ });
180
+
181
+ for (const [cat, skills] of Object.entries(byCategory)) {
182
+ lines.push(` ${cat}:`);
183
+ skills.forEach(s => {
184
+ const status = s.exists ? `v${s.version}` : '(coming soon)';
185
+ lines.push(` ${s.name.replace(SKILL_PREFIX, '')} ${status}`);
186
+ lines.push(` ${s.description.slice(0, 90)}`);
187
+ });
188
+ lines.push('');
189
+ }
190
+
191
+ lines.push('Commands:');
192
+ lines.push(' /mindforge:marketplace search [query]');
193
+ lines.push(' /mindforge:marketplace trending');
194
+ lines.push(' /mindforge:marketplace install [name]');
195
+ return lines.join('\n');
196
+ }
197
+
198
+ module.exports = { search, getFeatured, getTrending, install, formatSearchResults, formatFeatured };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * MindForge v2 — Pattern Detector
3
+ * Analyses phase SUMMARY files to find patterns that appeared
4
+ * across 2+ tasks and are worth capturing as skills.
5
+ *
6
+ * Used by the AUTO_CAPTURE_SKILLS=true hook in execute-phase.
7
+ */
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const ModelClient = require('../models/model-client');
13
+ const Router = require('../models/model-router');
14
+
15
+ const PLANNING_DIR = path.join(process.cwd(), '.planning');
16
+
17
+ const PATTERN_DETECTION_SYSTEM = `You are an expert at analysing software development sessions
18
+ to find reusable patterns worth capturing as team knowledge.
19
+
20
+ You will receive SUMMARY files from a completed development phase.
21
+ Find patterns that:
22
+ 1. Appeared in 2+ tasks (frequency = evidence of importance)
23
+ 2. Are technology-specific (not generic like "wrote tests" or "handled errors")
24
+ 3. Would be hard to know without having done this before
25
+ 4. Would meaningfully help future agents starting similar work
26
+
27
+ For each pattern found, provide:
28
+ - pattern_name: Short name (≤ 50 chars, kebab-case)
29
+ - display_name: Human-readable name
30
+ - frequency: Number of tasks where this pattern appeared
31
+ - generality: "high"|"medium"|"low" (would this help in other projects?)
32
+ - difficulty: "high"|"medium"|"low" (hard to get right without knowing it?)
33
+ - evidence: List of which plan files show this pattern
34
+ - summary: 2-3 sentence description of the pattern
35
+ - suggested_skill_name: Kebab-case name for the skill (e.g., "prisma-relations")
36
+
37
+ Return ONLY valid JSON. Array of pattern objects. Maximum 5 patterns.
38
+ Minimum capture bar: frequency >= 2 OR (frequency == 1 AND difficulty == "high" AND generality != "low")`;
39
+
40
+ async function detectPatterns(phaseNum, options = {}) {
41
+ const { sessionId = 'pattern-detect', minFrequency = 2 } = options;
42
+
43
+ const phaseDir = path.join(PLANNING_DIR, 'phases', String(phaseNum));
44
+ if (!fs.existsSync(phaseDir)) return { patterns: [], phase: phaseNum };
45
+
46
+ // Load all SUMMARY files
47
+ const summaryFiles = fs.readdirSync(phaseDir)
48
+ .filter(f => f.startsWith('SUMMARY-') && f.endsWith('.md'))
49
+ .sort();
50
+
51
+ if (summaryFiles.length < 2) {
52
+ return { patterns: [], phase: phaseNum, reason: 'Need at least 2 SUMMARY files for pattern detection' };
53
+ }
54
+
55
+ let combinedContent = `# Phase ${phaseNum} SUMMARY Analysis\n\n`;
56
+ for (const f of summaryFiles) {
57
+ const text = fs.readFileSync(path.join(phaseDir, f), 'utf8');
58
+ combinedContent += `## ${f}\n${text.slice(0, 8_000)}\n\n`;
59
+ }
60
+
61
+ // Also include HANDOFF.json implicit knowledge if available
62
+ const handoffPath = path.join(PLANNING_DIR, 'HANDOFF.json');
63
+ if (fs.existsSync(handoffPath)) {
64
+ try {
65
+ const handoff = JSON.parse(fs.readFileSync(handoffPath, 'utf8'));
66
+ const implicit = (handoff.implicit_knowledge || []).filter(i => (i.confidence || 0) >= 0.7);
67
+ if (implicit.length > 0) {
68
+ combinedContent += `## Implicit Knowledge (from compaction)\n`;
69
+ implicit.forEach(i => { combinedContent += `- ${i.topic || ''}: ${i.content || i.text || ''}\n`; });
70
+ }
71
+ } catch { /* ignore */ }
72
+ }
73
+
74
+ const model = Router.getAllSettings()?.EXECUTOR_MODEL || 'claude-3-5-sonnet';
75
+ process.stdout.write(` 🔍 Detecting patterns in Phase ${phaseNum} (${summaryFiles.length} tasks)... `);
76
+
77
+ const result = await ModelClient.complete({
78
+ model,
79
+ systemPrompt: PATTERN_DETECTION_SYSTEM,
80
+ userMessage: combinedContent.slice(0, 100_000),
81
+ maxTokens: 2048,
82
+ temperature: 0.1,
83
+ taskName: `pattern-detect-phase${phaseNum}`,
84
+ sessionId,
85
+ });
86
+
87
+ console.log('done');
88
+
89
+ const text = result.content.trim().replace(/^```json\n?/, '').replace(/\n?```$/, '');
90
+ let patterns;
91
+ try {
92
+ patterns = JSON.parse(text);
93
+ if (!Array.isArray(patterns)) throw new Error('Not an array');
94
+ } catch (err) {
95
+ return { patterns: [], phase: phaseNum, error: `Pattern detection returned invalid JSON: ${err.message}` };
96
+ }
97
+
98
+ // Filter to minimum bar
99
+ const filtered = patterns
100
+ .filter(p => {
101
+ const freq = p.frequency || 1;
102
+ const generality = p.generality || 'low';
103
+ const difficulty = p.difficulty || 'medium';
104
+ return freq >= minFrequency || (freq >= 1 && difficulty === 'high' && generality !== 'low');
105
+ })
106
+ .slice(0, 5);
107
+
108
+ return { patterns: filtered, phase: phaseNum, tasks_analysed: summaryFiles.length };
109
+ }
110
+
111
+ /**
112
+ * Format detected patterns for user presentation.
113
+ */
114
+ function formatForPresentation(detectionResult) {
115
+ const { patterns, phase, tasks_analysed } = detectionResult;
116
+
117
+ if (!patterns || patterns.length === 0) {
118
+ return `\n🔍 Auto-capture: No reusable patterns found in Phase ${phase}\n` +
119
+ ` (${tasks_analysed} tasks analysed — need patterns appearing in 2+ tasks)\n`;
120
+ }
121
+
122
+ const lines = [
123
+ `\n🎯 Auto-capture: ${patterns.length} reusable pattern${patterns.length > 1 ? 's' : ''} found in Phase ${phase}`,
124
+ ` (${tasks_analysed} tasks analysed)\n`,
125
+ ];
126
+
127
+ patterns.forEach((p, i) => {
128
+ const stars = p.generality === 'high' ? '★★★' : p.generality === 'medium' ? '★★' : '★';
129
+ const freq = p.frequency > 1 ? `appeared in ${p.frequency} tasks` : `1 task (high difficulty)`;
130
+ lines.push(` ${i + 1}. ${p.display_name || p.pattern_name} (${stars} ${p.generality} generality)`);
131
+ lines.push(` ${freq}`);
132
+ lines.push(` "${p.summary?.slice(0, 120) || ''}"`);
133
+ lines.push('');
134
+ });
135
+
136
+ const choices = patterns.length === 1
137
+ ? '[ y=save ] [ n=skip ]'
138
+ : `[ y=all ] [ ${patterns.map((_, i) => `${i+1}=only #${i+1}`).join(' ] [ ')} ] [ n=skip ]`;
139
+
140
+ lines.push(`Save as skills? ${choices}`);
141
+ return lines.join('\n');
142
+ }
143
+
144
+ module.exports = { detectPatterns, formatForPresentation };