mindforge-cc 2.0.0-alpha.9 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/CLAUDE.md +26 -0
- package/.agent/mindforge/add-backlog.md +32 -0
- package/.agent/mindforge/agent.md +31 -0
- package/.agent/mindforge/do.md +31 -0
- package/.agent/mindforge/execute-phase.md +23 -0
- package/.agent/mindforge/install-skill.md +20 -11
- package/.agent/mindforge/learn.md +142 -0
- package/.agent/mindforge/marketplace.md +120 -0
- package/.agent/mindforge/new-runtime.md +19 -0
- package/.agent/mindforge/note.md +35 -0
- package/.agent/mindforge/plant-seed.md +31 -0
- package/.agent/mindforge/remember.md +16 -4
- package/.agent/mindforge/review-backlog.md +34 -0
- package/.agent/mindforge/session-report.md +39 -0
- package/.agent/mindforge/ui-phase.md +34 -0
- package/.agent/mindforge/ui-review.md +36 -0
- package/.agent/mindforge/validate-phase.md +31 -0
- package/.agent/mindforge/workstreams.md +35 -0
- package/.claude/CLAUDE.md +26 -0
- package/.claude/commands/mindforge/add-backlog.md +32 -0
- package/.claude/commands/mindforge/agent.md +31 -0
- package/.claude/commands/mindforge/approve.md +27 -15
- package/.claude/commands/mindforge/audit.md +30 -26
- package/.claude/commands/mindforge/auto.md +29 -18
- package/.claude/commands/mindforge/benchmark.md +26 -29
- package/.claude/commands/mindforge/browse.md +24 -22
- package/.claude/commands/mindforge/complete-milestone.md +28 -14
- package/.claude/commands/mindforge/costs.md +26 -9
- package/.claude/commands/mindforge/cross-review.md +27 -13
- package/.claude/commands/mindforge/dashboard.md +35 -98
- package/.claude/commands/mindforge/debug.md +34 -126
- package/.claude/commands/mindforge/discuss-phase.md +36 -138
- package/.claude/commands/mindforge/do.md +31 -0
- package/.claude/commands/mindforge/execute-phase.md +37 -167
- package/.claude/commands/mindforge/health.md +27 -17
- package/.claude/commands/mindforge/help.md +25 -19
- package/.claude/commands/mindforge/init-org.md +37 -131
- package/.claude/commands/mindforge/init-project.md +40 -155
- package/.claude/commands/mindforge/install-skill.md +32 -15
- package/.claude/commands/mindforge/learn.md +36 -0
- package/.claude/commands/mindforge/map-codebase.md +36 -298
- package/.claude/commands/mindforge/marketplace.md +33 -0
- package/.claude/commands/mindforge/metrics.md +29 -18
- package/.claude/commands/mindforge/migrate.md +33 -40
- package/.claude/commands/mindforge/milestone.md +35 -12
- package/.claude/commands/mindforge/new-runtime.md +29 -0
- package/.claude/commands/mindforge/next.md +34 -105
- package/.claude/commands/mindforge/note.md +35 -0
- package/.claude/commands/mindforge/plan-phase.md +34 -125
- package/.claude/commands/mindforge/plant-seed.md +31 -0
- package/.claude/commands/mindforge/plugins.md +30 -36
- package/.claude/commands/mindforge/pr-review.md +32 -41
- package/.claude/commands/mindforge/profile-team.md +26 -19
- package/.claude/commands/mindforge/publish-skill.md +28 -17
- package/.claude/commands/mindforge/qa.md +27 -12
- package/.claude/commands/mindforge/quick.md +35 -135
- package/.claude/commands/mindforge/release.md +27 -8
- package/.claude/commands/mindforge/remember.md +25 -10
- package/.claude/commands/mindforge/research.md +27 -9
- package/.claude/commands/mindforge/retrospective.md +28 -22
- package/.claude/commands/mindforge/review-backlog.md +34 -0
- package/.claude/commands/mindforge/review.md +37 -157
- package/.claude/commands/mindforge/security-scan.md +34 -233
- package/.claude/commands/mindforge/session-report.md +39 -0
- package/.claude/commands/mindforge/ship.md +34 -100
- package/.claude/commands/mindforge/skills.md +36 -141
- package/.claude/commands/mindforge/status.md +30 -104
- package/.claude/commands/mindforge/steer.md +25 -10
- package/.claude/commands/mindforge/sync-confluence.md +28 -9
- package/.claude/commands/mindforge/sync-jira.md +32 -12
- package/.claude/commands/mindforge/tokens.md +25 -6
- package/.claude/commands/mindforge/ui-phase.md +34 -0
- package/.claude/commands/mindforge/ui-review.md +36 -0
- package/.claude/commands/mindforge/update.md +33 -42
- package/.claude/commands/mindforge/validate-phase.md +31 -0
- package/.claude/commands/mindforge/verify-phase.md +30 -62
- package/.claude/commands/mindforge/workspace.md +28 -25
- package/.claude/commands/mindforge/workstreams.md +35 -0
- package/.mindforge/distribution/marketplace.md +53 -0
- package/.mindforge/org/skills/MANIFEST.md +1 -0
- package/.mindforge/personas/advisor-researcher.md +89 -0
- package/.mindforge/personas/analyst.md +112 -52
- package/.mindforge/personas/architect.md +100 -67
- package/.mindforge/personas/assumptions-analyzer-extend.md +87 -0
- package/.mindforge/personas/assumptions-analyzer.md +109 -0
- package/.mindforge/personas/codebase-mapper-extend.md +93 -0
- package/.mindforge/personas/codebase-mapper.md +770 -0
- package/.mindforge/personas/coverage-specialist.md +104 -0
- package/.mindforge/personas/debug-specialist.md +118 -52
- package/.mindforge/personas/debugger.md +97 -0
- package/.mindforge/personas/decision-architect.md +102 -0
- package/.mindforge/personas/developer.md +97 -85
- package/.mindforge/personas/executor.md +88 -0
- package/.mindforge/personas/integration-checker.md +92 -0
- package/.mindforge/personas/nyquist-auditor.md +84 -0
- package/.mindforge/personas/phase-researcher.md +107 -0
- package/.mindforge/personas/plan-checker.md +92 -0
- package/.mindforge/personas/planner.md +105 -0
- package/.mindforge/personas/project-researcher.md +99 -0
- package/.mindforge/personas/qa-engineer.md +113 -61
- package/.mindforge/personas/release-manager.md +102 -64
- package/.mindforge/personas/research-agent.md +108 -24
- package/.mindforge/personas/research-synthesizer.md +101 -0
- package/.mindforge/personas/roadmapper-extend.md +100 -0
- package/.mindforge/personas/roadmapper.md +103 -0
- package/.mindforge/personas/security-reviewer.md +114 -91
- package/.mindforge/personas/tech-writer.md +118 -51
- package/.mindforge/personas/ui-auditor.md +94 -0
- package/.mindforge/personas/ui-checker.md +89 -0
- package/.mindforge/personas/ui-researcher.md +99 -0
- package/.mindforge/personas/user-profiler.md +93 -0
- package/.mindforge/personas/verifier.md +101 -0
- package/.mindforge/production/production-checklist.md +34 -123
- package/.mindforge/skills-builder/auto-capture-protocol.md +88 -0
- package/.mindforge/skills-builder/learn-protocol.md +161 -0
- package/.mindforge/skills-builder/quality-scoring.md +120 -0
- package/.planning/AUDIT.jsonl +1 -0
- package/.planning/decisions/ADR-036-learn-command-docs-as-skill-source.md +26 -0
- package/.planning/decisions/ADR-037-auto-capture-frequency-threshold.md +26 -0
- package/.planning/decisions/ADR-038-skill-quality-minimum-60.md +27 -0
- package/CHANGELOG.md +78 -0
- package/MINDFORGE.md +11 -0
- package/README.md +80 -6
- package/bin/autonomous/auto-runner.js +12 -0
- package/bin/install.js +8 -2
- package/bin/installer-core.js +129 -26
- package/bin/migrations/1.0.0-to-2.0.0.js +115 -0
- package/bin/migrations/schema-versions.js +12 -0
- package/bin/mindforge-cli.js +35 -0
- package/bin/review/cross-review-engine.js +11 -0
- package/bin/skill-registry.js +167 -0
- package/bin/skill-validator.js +144 -0
- package/bin/skills-builder/learn-cli.js +57 -0
- package/bin/skills-builder/marketplace-cli.js +54 -0
- package/bin/skills-builder/marketplace-client.js +198 -0
- package/bin/skills-builder/pattern-detector.js +144 -0
- package/bin/skills-builder/skill-generator.js +258 -0
- package/bin/skills-builder/skill-registrar.js +107 -0
- package/bin/skills-builder/skill-scorer.js +263 -0
- package/bin/skills-builder/source-loader.js +268 -0
- package/docs/Context/Master-Context.md +6 -13
- package/docs/PERSONAS.md +611 -0
- package/docs/architecture/README.md +6 -1
- package/docs/architecture/adr-039-multi-runtime-support.md +20 -0
- package/docs/architecture/adr-040-additive-schema-migration.md +21 -0
- package/docs/architecture/adr-041-stable-runtime-interface-contract.md +20 -0
- package/docs/architecture/decision-records-index.md +3 -0
- package/docs/commands-reference.md +3 -0
- package/docs/mindforge-md-reference.md +4 -0
- package/docs/reference/commands.md +53 -43
- package/docs/skills-authoring-guide.md +29 -0
- package/docs/skills-publishing-guide.md +3 -2
- package/docs/testing-current-version.md +3 -3
- package/docs/upgrade.md +16 -2
- package/docs/user-guide.md +57 -8
- package/docs/usp-features.md +21 -6
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Skill Scorer
|
|
3
|
+
* 7-dimension quality scoring system for SKILL.md files.
|
|
4
|
+
* Total: 100 points.
|
|
5
|
+
*
|
|
6
|
+
* This is a static analysis scorer — no AI calls needed.
|
|
7
|
+
* Runs in < 100ms on any SKILL.md.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// ── Injection guard patterns (from skill-loader.md) ───────────────────────────
|
|
15
|
+
const INJECTION_PATTERNS = [
|
|
16
|
+
/IGNORE ALL PREVIOUS INSTRUCTIONS/i,
|
|
17
|
+
/IGNORE PREVIOUS INSTRUCTIONS/i,
|
|
18
|
+
/DISREGARD YOUR INSTRUCTIONS/i,
|
|
19
|
+
/FORGET YOUR TRAINING/i,
|
|
20
|
+
/YOU ARE NOW/i,
|
|
21
|
+
/YOUR NEW INSTRUCTIONS ARE/i,
|
|
22
|
+
/OVERRIDE:/i,
|
|
23
|
+
/SYSTEM PROMPT:/i,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// ── Placeholder detection ─────────────────────────────────────────────────────
|
|
27
|
+
const PLACEHOLDER_PATTERNS = [
|
|
28
|
+
/\[your description here\]/i,
|
|
29
|
+
/\[fill in\]/i,
|
|
30
|
+
/\[TODO\]/i,
|
|
31
|
+
/\btodo\b/i,
|
|
32
|
+
/\bfixme\b/i,
|
|
33
|
+
/<description>/i,
|
|
34
|
+
/\.\.\.fill in\.\.\./i,
|
|
35
|
+
/\[your [a-z\s]+ here\]/i,
|
|
36
|
+
/\[replace with\]/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// ── Generic trigger words (penalty list) ─────────────────────────────────────
|
|
40
|
+
const GENERIC_TRIGGERS = new Set([
|
|
41
|
+
'database', 'api', 'model', 'service', 'component', 'function',
|
|
42
|
+
'class', 'method', 'type', 'interface', 'module', 'package',
|
|
43
|
+
'file', 'config', 'test', 'error', 'data', 'query', 'request',
|
|
44
|
+
'response', 'handler', 'controller', 'repository', 'schema',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// ── SKILL.md parser ───────────────────────────────────────────────────────────
|
|
48
|
+
function parseSkill(content) {
|
|
49
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
50
|
+
const frontmatter = frontmatterMatch?.[1] || '';
|
|
51
|
+
|
|
52
|
+
// Extract triggers from frontmatter
|
|
53
|
+
const triggersSection = frontmatter.match(/^triggers:\n((?: - .+\n?)*)/m);
|
|
54
|
+
const triggers = (triggersSection?.[1] || '')
|
|
55
|
+
.split('\n')
|
|
56
|
+
.map(l => l.replace(/^\s*- /, '').trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
|
|
59
|
+
// Count code blocks with ≥ 3 lines (meaningful examples, not one-liners)
|
|
60
|
+
const codeBlockMatches = content.match(/```[\s\S]*?```/g) || [];
|
|
61
|
+
const codeBlocks = codeBlockMatches.length;
|
|
62
|
+
const meaningfulBlocks = codeBlockMatches.filter(block => {
|
|
63
|
+
const lines = block.split('\n').length;
|
|
64
|
+
return lines >= 4; // ``` + at least 3 content lines + ```
|
|
65
|
+
}).length;
|
|
66
|
+
|
|
67
|
+
// Count checklist items (both checked and unchecked)
|
|
68
|
+
const checklistItems = (content.match(/^- \[[ xX]\] /gm) || []).length;
|
|
69
|
+
|
|
70
|
+
// Has version history?
|
|
71
|
+
const hasVersionHistory = /## Version History/i.test(content);
|
|
72
|
+
const versionEntries = (content.match(/^### v\d+\.\d+\.\d+/gm) || []).length;
|
|
73
|
+
|
|
74
|
+
// Mandatory action counts
|
|
75
|
+
const alwaysRules = (content.match(/\b(Always|Must|Required|mandatory|MUST)\b/gi) || []).length;
|
|
76
|
+
const neverRules = (content.match(/\b(Never|Don't|Do not|Avoid|NEVER)\b/gi) || []).length;
|
|
77
|
+
const hasSecuritySection = /security|auth|SECURITY|AUTH/i.test(content);
|
|
78
|
+
const hasPerformanceSection = /performance|perf|optimiz|PERFORMANCE/i.test(content);
|
|
79
|
+
const hasErrorSection = /error handling|exception|catch|Error/i.test(content);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
triggers, codeBlocks, meaningfulBlocks, checklistItems,
|
|
83
|
+
hasVersionHistory, versionEntries,
|
|
84
|
+
alwaysRules, neverRules,
|
|
85
|
+
hasSecuritySection, hasPerformanceSection, hasErrorSection,
|
|
86
|
+
content,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Dimension scorers ─────────────────────────────────────────────────────────
|
|
91
|
+
function scoreTriggerCoverage(parsed) {
|
|
92
|
+
const { triggers } = parsed;
|
|
93
|
+
let score = 0;
|
|
94
|
+
|
|
95
|
+
if (triggers.length >= 25) score = 30;
|
|
96
|
+
else if (triggers.length >= 20) score = 24;
|
|
97
|
+
else if (triggers.length >= 15) score = 18;
|
|
98
|
+
else if (triggers.length >= 10) score = 12;
|
|
99
|
+
else if (triggers.length >= 5) score = 6;
|
|
100
|
+
|
|
101
|
+
// Penalty for generic triggers
|
|
102
|
+
const genericCount = triggers.filter(t => GENERIC_TRIGGERS.has(t.toLowerCase())).length;
|
|
103
|
+
const penalty = genericCount * 2;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
score: Math.max(0, score - penalty),
|
|
107
|
+
max: 30,
|
|
108
|
+
details: `${triggers.length} triggers, ${genericCount} generic (penalty: -${penalty})`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scoreMandatoryActions(parsed) {
|
|
113
|
+
const { alwaysRules, neverRules, hasSecuritySection, hasPerformanceSection, hasErrorSection } = parsed;
|
|
114
|
+
let score = 0;
|
|
115
|
+
|
|
116
|
+
if (alwaysRules >= 5) score += 5;
|
|
117
|
+
else if (alwaysRules >= 3) score += 3;
|
|
118
|
+
else if (alwaysRules >= 1) score += 1;
|
|
119
|
+
|
|
120
|
+
if (neverRules >= 3) score += 5;
|
|
121
|
+
else if (neverRules >= 2) score += 3;
|
|
122
|
+
else if (neverRules >= 1) score += 2;
|
|
123
|
+
|
|
124
|
+
if (hasSecuritySection) score += 5;
|
|
125
|
+
if (hasPerformanceSection) score += 5;
|
|
126
|
+
if (hasErrorSection) score += 5;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
score: Math.min(25, score),
|
|
130
|
+
max: 25,
|
|
131
|
+
details: `${alwaysRules} always-rules, ${neverRules} never-rules, security:${hasSecuritySection}, perf:${hasPerformanceSection}, errors:${hasErrorSection}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scoreCodeExamples(parsed) {
|
|
136
|
+
const { meaningfulBlocks, content } = parsed;
|
|
137
|
+
let score = 0;
|
|
138
|
+
|
|
139
|
+
if (meaningfulBlocks >= 5) score = 20;
|
|
140
|
+
else if (meaningfulBlocks >= 3) score = 14;
|
|
141
|
+
else if (meaningfulBlocks >= 1) score = 7;
|
|
142
|
+
|
|
143
|
+
// Bonus: side-by-side correct/incorrect examples
|
|
144
|
+
const hasSideBySide = content.includes('✅') && content.includes('❌');
|
|
145
|
+
if (hasSideBySide) score = Math.min(20, score + 2);
|
|
146
|
+
|
|
147
|
+
return { score: Math.min(20, score), max: 20, details: `${meaningfulBlocks} meaningful code blocks, side-by-side:${hasSideBySide}` };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function scoreSelfCheck(parsed) {
|
|
151
|
+
const { checklistItems } = parsed;
|
|
152
|
+
let score = 0;
|
|
153
|
+
|
|
154
|
+
if (checklistItems >= 10) score = 15;
|
|
155
|
+
else if (checklistItems >= 7) score = 10;
|
|
156
|
+
else if (checklistItems >= 4) score = 7;
|
|
157
|
+
else if (checklistItems >= 1) score = 3;
|
|
158
|
+
|
|
159
|
+
return { score, max: 15, details: `${checklistItems} checklist items` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function scoreInjectionSafe(parsed) {
|
|
163
|
+
const hasInjection = INJECTION_PATTERNS.some(p => p.test(parsed.content));
|
|
164
|
+
return {
|
|
165
|
+
score: hasInjection ? 0 : 10,
|
|
166
|
+
max: 10,
|
|
167
|
+
details: hasInjection ? 'INJECTION PATTERN DETECTED — score 0' : 'clean',
|
|
168
|
+
fail: hasInjection,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function scoreNoPlaceholders(parsed) {
|
|
173
|
+
const placeholderCount = PLACEHOLDER_PATTERNS.filter(p => p.test(parsed.content)).length;
|
|
174
|
+
let score = 0;
|
|
175
|
+
if (placeholderCount === 0) score = 10;
|
|
176
|
+
else if (placeholderCount <= 2) score = 5;
|
|
177
|
+
|
|
178
|
+
return { score, max: 10, details: `${placeholderCount} placeholder patterns found` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function scoreVersionHistory(parsed) {
|
|
182
|
+
const { hasVersionHistory, versionEntries } = parsed;
|
|
183
|
+
let score = 0;
|
|
184
|
+
if (hasVersionHistory && versionEntries >= 1) score = 10;
|
|
185
|
+
else if (versionEntries > 0) score = 5;
|
|
186
|
+
|
|
187
|
+
const bonus = versionEntries > 1 ? 2 : 0;
|
|
188
|
+
const MAX = 10;
|
|
189
|
+
return { score: Math.min(MAX, score + bonus), max: MAX, details: `${versionEntries} version entries` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Main score function ───────────────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Score a SKILL.md file.
|
|
195
|
+
* @param {string} skillPathOrContent - Path to SKILL.md or content string
|
|
196
|
+
* @returns {object} Full scoring result
|
|
197
|
+
*/
|
|
198
|
+
function score(skillPathOrContent) {
|
|
199
|
+
let content;
|
|
200
|
+
if (typeof skillPathOrContent === 'string' && fs.existsSync(skillPathOrContent)) {
|
|
201
|
+
content = fs.readFileSync(skillPathOrContent, 'utf8');
|
|
202
|
+
} else {
|
|
203
|
+
content = skillPathOrContent;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const parsed = parseSkill(content);
|
|
207
|
+
|
|
208
|
+
const dimensions = {
|
|
209
|
+
trigger_coverage: scoreTriggerCoverage(parsed),
|
|
210
|
+
mandatory_actions: scoreMandatoryActions(parsed),
|
|
211
|
+
code_examples: scoreCodeExamples(parsed),
|
|
212
|
+
self_check: scoreSelfCheck(parsed),
|
|
213
|
+
injection_safe: scoreInjectionSafe(parsed),
|
|
214
|
+
no_placeholders: scoreNoPlaceholders(parsed),
|
|
215
|
+
version_history: scoreVersionHistory(parsed),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const total = Object.values(dimensions).reduce((s, d) => s + d.score, 0);
|
|
219
|
+
|
|
220
|
+
// Determine thresholds
|
|
221
|
+
let threshold_status = 'insufficient';
|
|
222
|
+
if (total >= 90) threshold_status = 'excellent';
|
|
223
|
+
else if (total >= 80) threshold_status = 'good';
|
|
224
|
+
else if (total >= 70) threshold_status = 'acceptable';
|
|
225
|
+
else if (total >= 60) threshold_status = 'minimum';
|
|
226
|
+
|
|
227
|
+
const can_register = total >= 60 && !dimensions.injection_safe.fail;
|
|
228
|
+
const can_publish = total >= 80 && !dimensions.injection_safe.fail;
|
|
229
|
+
|
|
230
|
+
// Improvement suggestions
|
|
231
|
+
const suggestions = [];
|
|
232
|
+
if (dimensions.trigger_coverage.score < 24) {
|
|
233
|
+
suggestions.push(`Add ${25 - parsed.triggers.length} more triggers to reach 25+ (currently ${parsed.triggers.length})`);
|
|
234
|
+
}
|
|
235
|
+
if (dimensions.mandatory_actions.score < 20) {
|
|
236
|
+
if (!parsed.hasSecuritySection) suggestions.push('Add a security considerations section');
|
|
237
|
+
if (!parsed.hasPerformanceSection) suggestions.push('Add a performance considerations section');
|
|
238
|
+
if (!parsed.hasErrorSection) suggestions.push('Add an error handling section');
|
|
239
|
+
}
|
|
240
|
+
if (dimensions.code_examples.score < 14) {
|
|
241
|
+
suggestions.push(`Add ${5 - parsed.codeBlocks} more code examples (currently ${parsed.codeBlocks})`);
|
|
242
|
+
}
|
|
243
|
+
if (dimensions.self_check.score < 10) {
|
|
244
|
+
suggestions.push(`Add ${10 - parsed.checklistItems} more checklist items (currently ${parsed.checklistItems})`);
|
|
245
|
+
}
|
|
246
|
+
if (!dimensions.version_history.score) {
|
|
247
|
+
suggestions.push('Add a ## Version History section with a v1.0.0 entry');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
quality_score: total,
|
|
252
|
+
threshold_status,
|
|
253
|
+
can_register,
|
|
254
|
+
can_publish,
|
|
255
|
+
score_breakdown: Object.fromEntries(Object.entries(dimensions).map(([k, v]) => [k, v.score])),
|
|
256
|
+
dimension_details: Object.fromEntries(Object.entries(dimensions).map(([k, v]) => [k, v.details])),
|
|
257
|
+
improvement_suggestions: suggestions,
|
|
258
|
+
trigger_count: parsed.triggers.length,
|
|
259
|
+
injection_safe: !dimensions.injection_safe.fail,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { score, parseSkill, INJECTION_PATTERNS, PLACEHOLDER_PATTERNS, GENERIC_TRIGGERS };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Source Loader
|
|
3
|
+
* Loads documentation from URLs, local files/dirs, npm packages,
|
|
4
|
+
* and the current session (SUMMARY files + HANDOFF.json).
|
|
5
|
+
*
|
|
6
|
+
* All URL fetches have SSRF protection (Day 10 pattern).
|
|
7
|
+
* Local reads use walkDir() to safely enumerate files.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const dns = require('dns').promises;
|
|
14
|
+
|
|
15
|
+
const PLANNING_DIR = path.join(process.cwd(), '.planning');
|
|
16
|
+
const MINDFORGE_DIR = path.join(process.cwd(), '.mindforge');
|
|
17
|
+
|
|
18
|
+
// ── SSRF protection (reused from research-engine.js, Day 10) ─────────────────
|
|
19
|
+
const PRIVATE_RANGES = [
|
|
20
|
+
/^127\./,
|
|
21
|
+
/^10\./,
|
|
22
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
23
|
+
/^192\.168\./,
|
|
24
|
+
/^169\.254\./, // AWS metadata
|
|
25
|
+
/^::1$/,
|
|
26
|
+
/^fc00:/,
|
|
27
|
+
/^fe80:/,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
async function isSafeUrl(rawUrl) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try { parsed = new URL(rawUrl); } catch { return false; }
|
|
33
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const { address } = await dns.lookup(parsed.hostname);
|
|
37
|
+
if (PRIVATE_RANGES.some(r => r.test(address))) {
|
|
38
|
+
process.stderr.write(`[source-loader] SSRF blocked: ${rawUrl} → ${address}\n`);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
process.stderr.write(`[source-loader] DNS resolution failed for: ${parsed.hostname}\n`);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── URL fetcher ───────────────────────────────────────────────────────────────
|
|
49
|
+
async function fetchUrl(rawUrl, maxChars = 400_000, _redirectCount = 0) {
|
|
50
|
+
if (_redirectCount > 5) {
|
|
51
|
+
throw new Error(`Too many redirects (>5) for URL: ${rawUrl}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!await isSafeUrl(rawUrl)) {
|
|
55
|
+
throw new Error(`URL blocked by SSRF protection: ${rawUrl}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const protocol = rawUrl.startsWith('https') ? require('https') : require('http');
|
|
60
|
+
let settled = false;
|
|
61
|
+
const settle = (fn, val) => { if (!settled) { settled = true; fn(val); } };
|
|
62
|
+
|
|
63
|
+
const hardTimer = setTimeout(() => {
|
|
64
|
+
settle(reject, new Error(`Fetch timeout (30s): ${rawUrl}`));
|
|
65
|
+
}, 30_000);
|
|
66
|
+
|
|
67
|
+
const req = protocol.get(rawUrl, { headers: { 'User-Agent': 'MindForge-Learn/2.0' } }, res => {
|
|
68
|
+
// Follow redirects (up to 5)
|
|
69
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
70
|
+
clearTimeout(hardTimer);
|
|
71
|
+
// Pass incremented redirect count — SSRF re-checked in recursive call
|
|
72
|
+
// Comment: Each redirect hop calls isSafeUrl() — open redirect chains through private IPs are blocked
|
|
73
|
+
fetchUrl(res.headers.location, maxChars, _redirectCount + 1).then(settle.bind(null, resolve), settle.bind(null, reject));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (res.statusCode !== 200) {
|
|
77
|
+
clearTimeout(hardTimer);
|
|
78
|
+
settle(reject, new Error(`HTTP ${res.statusCode} for: ${rawUrl}`));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let body = '';
|
|
83
|
+
res.on('data', chunk => { body += chunk; if (body.length > maxChars) res.destroy(); });
|
|
84
|
+
res.on('end', () => { clearTimeout(hardTimer); settle(resolve, body.slice(0, maxChars)); });
|
|
85
|
+
res.on('error', err => { clearTimeout(hardTimer); settle(reject, err); });
|
|
86
|
+
});
|
|
87
|
+
req.on('error', err => { clearTimeout(hardTimer); settle(reject, err); });
|
|
88
|
+
req.end();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── HTML → text (strip tags for cleaner model context) ───────────────────────
|
|
93
|
+
function htmlToText(html) {
|
|
94
|
+
return html
|
|
95
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '') // Remove scripts
|
|
96
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '') // Remove styles
|
|
97
|
+
.replace(/<[^>]+>/g, ' ') // Strip remaining tags
|
|
98
|
+
.replace(/ /g, ' ')
|
|
99
|
+
.replace(/&/g, '&')
|
|
100
|
+
.replace(/</g, '<')
|
|
101
|
+
.replace(/>/g, '>')
|
|
102
|
+
.replace(/"/g, '"')
|
|
103
|
+
.replace(/'/g, "'")
|
|
104
|
+
.replace(/\s{3,}/g, '\n\n') // Collapse excessive whitespace
|
|
105
|
+
.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── npm package loader ────────────────────────────────────────────────────────
|
|
109
|
+
async function loadNpmPackage(packageName) {
|
|
110
|
+
// Sanitize package name (prevent path injection)
|
|
111
|
+
if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(packageName)) {
|
|
112
|
+
throw new Error(`Invalid npm package name: ${packageName}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
|
|
116
|
+
const raw = await fetchUrl(registryUrl, 500_000);
|
|
117
|
+
const data = JSON.parse(raw);
|
|
118
|
+
|
|
119
|
+
const latest = data['dist-tags']?.latest || Object.keys(data.versions || {}).pop();
|
|
120
|
+
const version = data.versions?.[latest];
|
|
121
|
+
const readme = data.readme || version?.readme || '';
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
name: packageName,
|
|
125
|
+
version: latest,
|
|
126
|
+
description: data.description || '',
|
|
127
|
+
homepage: data.homepage || '',
|
|
128
|
+
repository: version?.repository?.url || '',
|
|
129
|
+
readme: readme.slice(0, 200_000),
|
|
130
|
+
keywords: data.keywords || [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Local file/directory loader ───────────────────────────────────────────────
|
|
135
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.planning']);
|
|
136
|
+
const DOC_EXTENSIONS = new Set(['.md', '.mdx', '.txt', '.rst', '.html', '.json', '.yaml', '.yml']);
|
|
137
|
+
const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java']);
|
|
138
|
+
|
|
139
|
+
function walkDir(dir, extensions, maxFiles = 50) {
|
|
140
|
+
const results = [];
|
|
141
|
+
function walk(d) {
|
|
142
|
+
if (results.length >= maxFiles) return;
|
|
143
|
+
let entries;
|
|
144
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
145
|
+
for (const e of entries) {
|
|
146
|
+
if (results.length >= maxFiles) break;
|
|
147
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
148
|
+
const full = path.join(d, e.name);
|
|
149
|
+
if (e.isDirectory()) walk(full);
|
|
150
|
+
else if (extensions.has(path.extname(e.name).toLowerCase())) results.push(full);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
walk(dir);
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function loadLocal(localPath, maxCharsPerFile = 50_000) {
|
|
158
|
+
const resolved = path.resolve(localPath);
|
|
159
|
+
|
|
160
|
+
// Safety check: resolved path must be inside cwd or be absolute
|
|
161
|
+
const stat = fs.statSync(resolved);
|
|
162
|
+
let content = '';
|
|
163
|
+
|
|
164
|
+
if (stat.isDirectory()) {
|
|
165
|
+
const allExts = new Set([...DOC_EXTENSIONS, ...CODE_EXTENSIONS]);
|
|
166
|
+
const files = walkDir(resolved, allExts, 30);
|
|
167
|
+
for (const f of files) {
|
|
168
|
+
const text = fs.readFileSync(f, 'utf8').slice(0, maxCharsPerFile);
|
|
169
|
+
content += `\n\n### ${path.relative(process.cwd(), f)}\n${text}`;
|
|
170
|
+
if (content.length > 600_000) break;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
content = fs.readFileSync(resolved, 'utf8').slice(0, maxCharsPerFile);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { path: resolved, content: content.slice(0, 800_000) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Session loader ────────────────────────────────────────────────────────────
|
|
180
|
+
function loadSession(phaseNum = null) {
|
|
181
|
+
const phasesDir = path.join(PLANNING_DIR, 'phases');
|
|
182
|
+
if (!fs.existsSync(phasesDir)) return { content: '', sources: [] };
|
|
183
|
+
|
|
184
|
+
// Determine phase to analyse
|
|
185
|
+
let targetPhase = phaseNum;
|
|
186
|
+
if (!targetPhase) {
|
|
187
|
+
// Find the most recent phase with SUMMARY files
|
|
188
|
+
const phaseDirs = fs.readdirSync(phasesDir)
|
|
189
|
+
.filter(d => /^\d+$/.test(d))
|
|
190
|
+
.map(Number).sort((a, b) => b - a); // Descending
|
|
191
|
+
targetPhase = phaseDirs[0];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!targetPhase) return { content: '', sources: [] };
|
|
195
|
+
|
|
196
|
+
const phaseDir = path.join(phasesDir, String(targetPhase));
|
|
197
|
+
const sources = [];
|
|
198
|
+
let content = `# Session Analysis — Phase ${targetPhase}\n\n`;
|
|
199
|
+
|
|
200
|
+
// SUMMARY files
|
|
201
|
+
const summaryFiles = fs.existsSync(phaseDir)
|
|
202
|
+
? fs.readdirSync(phaseDir).filter(f => f.startsWith('SUMMARY-') && f.endsWith('.md'))
|
|
203
|
+
: [];
|
|
204
|
+
for (const f of summaryFiles) {
|
|
205
|
+
const text = fs.readFileSync(path.join(phaseDir, f), 'utf8');
|
|
206
|
+
content += `## ${f}\n${text.slice(0, 10_000)}\n\n`;
|
|
207
|
+
sources.push(f);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// HANDOFF.json implicit_knowledge
|
|
211
|
+
const handoffPath = path.join(PLANNING_DIR, 'HANDOFF.json');
|
|
212
|
+
if (fs.existsSync(handoffPath)) {
|
|
213
|
+
try {
|
|
214
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, 'utf8'));
|
|
215
|
+
const implicit = handoff.implicit_knowledge || [];
|
|
216
|
+
if (implicit.length > 0) {
|
|
217
|
+
content += `## Implicit Knowledge (from HANDOFF.json)\n`;
|
|
218
|
+
implicit
|
|
219
|
+
.filter(i => (i.confidence || 0) >= 0.65)
|
|
220
|
+
.forEach(i => { content += `- ${i.topic || i.text}: ${i.content || i.text}\n`; });
|
|
221
|
+
content += '\n';
|
|
222
|
+
sources.push('HANDOFF.json:implicit_knowledge');
|
|
223
|
+
}
|
|
224
|
+
} catch { /* ignore malformed HANDOFF */ }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ADR files from this phase
|
|
228
|
+
const decisionsDir = path.join(PLANNING_DIR, 'decisions');
|
|
229
|
+
if (fs.existsSync(decisionsDir)) {
|
|
230
|
+
const recentAdrs = fs.readdirSync(decisionsDir)
|
|
231
|
+
.filter(f => f.startsWith('ADR-') && f.endsWith('.md'))
|
|
232
|
+
.slice(-5); // Last 5 ADRs
|
|
233
|
+
for (const f of recentAdrs) {
|
|
234
|
+
const text = fs.readFileSync(path.join(decisionsDir, f), 'utf8');
|
|
235
|
+
content += `## ${f}\n${text.slice(0, 5_000)}\n\n`;
|
|
236
|
+
sources.push(f);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { content: content.slice(0, 800_000), sources, phase: targetPhase };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Main load function ────────────────────────────────────────────────────────
|
|
244
|
+
async function load(source) {
|
|
245
|
+
if (source === '--session') {
|
|
246
|
+
const result = loadSession();
|
|
247
|
+
return { type: 'session', content: result.content, metadata: { sources: result.sources, phase: result.phase } };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (source.startsWith('npm:')) {
|
|
251
|
+
const pkg = source.slice(4);
|
|
252
|
+
const result = await loadNpmPackage(pkg);
|
|
253
|
+
const content = `# ${result.name} v${result.version}\n${result.description}\n\n${result.readme}`;
|
|
254
|
+
return { type: 'npm', content, metadata: result };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
258
|
+
const raw = await fetchUrl(source);
|
|
259
|
+
const text = raw.includes('<html') || raw.includes('<HTML') ? htmlToText(raw) : raw;
|
|
260
|
+
return { type: 'url', content: text, metadata: { url: source, length: raw.length } };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Local path
|
|
264
|
+
const result = loadLocal(source);
|
|
265
|
+
return { type: 'local', content: result.content, metadata: { path: result.path } };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { load, fetchUrl, isSafeUrl, htmlToText, loadNpmPackage, loadLocal, loadSession };
|
|
@@ -34,15 +34,8 @@ anti-pattern detector, quality metrics, team profiling).
|
|
|
34
34
|
Built the entire structural foundation:
|
|
35
35
|
|
|
36
36
|
- **`.claude/CLAUDE.md`** — The agent entry point. Session start protocol, plan-first rule, quality gates, security auto-trigger, state artifact table. Mirrored identically to `.agent/CLAUDE.md` (Antigravity runtime).
|
|
37
|
-
- **
|
|
38
|
-
- `analyst.md`
|
|
39
|
-
- `architect.md` — System design, ADRs, technology decisions
|
|
40
|
-
- `developer.md` — Implementation with 5 common AI anti-pattern guards
|
|
41
|
-
- `qa-engineer.md` — Test strategy and verification
|
|
42
|
-
- `security-reviewer.md` — OWASP-aligned security review
|
|
43
|
-
- `tech-writer.md` — Documentation and changelog authoring
|
|
44
|
-
- `debug-specialist.md` — 10-step root cause analysis protocol
|
|
45
|
-
- `release-manager.md` — Deployment coordination
|
|
37
|
+
- **32 agent persona files** in `.mindforge/personas/`:
|
|
38
|
+
- `advisor-researcher.md`, `analyst.md`, `architect.md`, `assumptions-analyzer-extend.md`, `assumptions-analyzer.md`, `codebase-mapper-extend.md`, `codebase-mapper.md`, `coverage-specialist.md`, `debug-specialist.md`, `debugger.md`, `decision-architect.md`, `developer.md`, `executor.md`, `integration-checker.md`, `nyquist-auditor.md`, `phase-researcher.md`, `plan-checker.md`, `planner.md`, `project-researcher.md`, `qa-engineer.md`, `release-manager.md`, `research-agent.md`, `research-synthesizer.md`, `roadmapper-extend.md`, `roadmapper.md`, `security-reviewer.md`, `tech-writer.md`, `ui-auditor.md`, `ui-checker.md`, `ui-researcher.md`, `user-profiler.md`, `verifier.md`
|
|
46
39
|
- **5 initial core skill packs** in `.mindforge/skills/`:
|
|
47
40
|
- `security-review/SKILL.md` — 29 trigger keywords, OWASP A01-A10
|
|
48
41
|
- `code-quality/SKILL.md` — Complexity, naming, error handling
|
|
@@ -331,7 +324,7 @@ mindforge-cc/ ← npm package root
|
|
|
331
324
|
│ └── [36 .md command files]
|
|
332
325
|
│
|
|
333
326
|
├── .mindforge/
|
|
334
|
-
│ ├── personas/ ←
|
|
327
|
+
│ ├── personas/ ← 32 persona definitions + overrides/
|
|
335
328
|
│ ├── skills/ ← 10 core skill packs (SKILL.md each)
|
|
336
329
|
│ ├── engine/
|
|
337
330
|
│ │ ├── wave-executor.md ← Kahn's topological sort, parallel waves
|
|
@@ -490,9 +483,9 @@ mindforge-cc/ ← npm package root
|
|
|
490
483
|
9. `incident-response` — P0-P3, runbooks, postmortems
|
|
491
484
|
10. `database-patterns` — Compound cursor, UUIDv7, indexes
|
|
492
485
|
|
|
493
|
-
###
|
|
486
|
+
### 32 Agent Personas
|
|
494
487
|
|
|
495
|
-
analyst, architect, developer, qa-engineer, security-reviewer, tech-writer,
|
|
488
|
+
advisor-researcher, analyst, architect, assumptions-analyzer-extend, assumptions-analyzer, codebase-mapper-extend, codebase-mapper, coverage-specialist, debug-specialist, debugger, decision-architect, developer, executor, integration-checker, nyquist-auditor, phase-researcher, plan-checker, planner, project-researcher, qa-engineer, release-manager, research-agent, research-synthesizer, roadmapper-extend, roadmapper, security-reviewer, tech-writer, ui-auditor, ui-checker, ui-researcher, user-profiler, verifier
|
|
496
489
|
|
|
497
490
|
### 20 Architecture Decision Records
|
|
498
491
|
|
|
@@ -698,4 +691,4 @@ All prompt files are in `/mnt/user-data/outputs/`:
|
|
|
698
691
|
|
|
699
692
|
---
|
|
700
693
|
|
|
701
|
-
*State file generated at completion. MindForge v1.0.0 — 36 commands · 10 skills ·
|
|
694
|
+
*State file generated at completion. MindForge v1.0.0 — 36 commands · 10 skills · 32 personas · 20 ADRs · 15 test suites.*
|