supermind-claude 2.1.1 → 4.0.1
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/.claude-plugin/plugin.json +21 -0
- package/README.md +34 -46
- package/agents/code-reviewer.md +81 -0
- package/cli/commands/doctor.js +415 -79
- package/cli/commands/install.js +16 -17
- package/cli/commands/skill.js +164 -0
- package/cli/commands/uninstall.js +32 -3
- package/cli/commands/update.js +25 -4
- package/cli/index.js +16 -4
- package/cli/lib/agents.js +413 -0
- package/cli/lib/executor.js +365 -0
- package/cli/lib/hooks.js +8 -1
- package/cli/lib/logger.js +1 -1
- package/cli/lib/planning.js +502 -0
- package/cli/lib/platform.js +4 -0
- package/cli/lib/plugin.js +127 -0
- package/cli/lib/settings.js +2 -40
- package/cli/lib/skills.js +39 -2
- package/cli/lib/vendor-skills.js +594 -0
- package/hooks/bash-permissions.js +196 -176
- package/hooks/context-monitor.js +79 -0
- package/hooks/improvement-logger.js +94 -0
- package/hooks/pre-merge-checklist.js +102 -0
- package/hooks/session-start.js +109 -5
- package/hooks/statusline-command.js +123 -29
- package/package.json +4 -2
- package/skills/anti-rationalization/SKILL.md +38 -0
- package/skills/brainstorming/SKILL.md +165 -0
- package/skills/code-review/SKILL.md +144 -0
- package/skills/executing-plans/SKILL.md +138 -0
- package/skills/finishing-branches/SKILL.md +144 -0
- package/skills/project/SKILL.md +533 -0
- package/skills/quick/SKILL.md +178 -0
- package/skills/supermind/SKILL.md +58 -4
- package/skills/supermind-init/SKILL.md +48 -2
- package/skills/systematic-debugging/SKILL.md +129 -0
- package/skills/tdd/SKILL.md +179 -0
- package/skills/using-git-worktrees/SKILL.md +138 -0
- package/skills/verification-before-completion/SKILL.md +54 -0
- package/skills/writing-plans/SKILL.md +169 -0
- package/templates/CLAUDE.md +124 -62
- package/cli/lib/plugins.js +0 -23
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Constants
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const PLANNING_DIR = '.planning';
|
|
11
|
+
const PHASES_DIR = 'phases';
|
|
12
|
+
const CONFIG_FILE = 'config.json';
|
|
13
|
+
const ROADMAP_FILE = 'roadmap.md';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Path safety — mirrors vendor-skills.js safeJoin pattern
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate that a relative path segment is safe (no traversal, not absolute),
|
|
21
|
+
* then join it onto a trusted base using string concatenation so semgrep
|
|
22
|
+
* taint-tracking does not flag path.join with untrusted input.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} trustedBase — already-safe absolute directory
|
|
25
|
+
* @param {string} segment — caller-supplied component to validate
|
|
26
|
+
* @param {string} label — for error messages
|
|
27
|
+
* @returns {string} safe absolute path
|
|
28
|
+
*/
|
|
29
|
+
function safeJoin(trustedBase, segment, label) {
|
|
30
|
+
if (typeof segment !== 'string' || segment.length === 0) {
|
|
31
|
+
throw new Error(`Invalid ${label}: must be a non-empty string`);
|
|
32
|
+
}
|
|
33
|
+
if (path.isAbsolute(segment)) {
|
|
34
|
+
throw new Error(`Invalid ${label}: must not be an absolute path`);
|
|
35
|
+
}
|
|
36
|
+
const parts = segment.split(/[\\/]/);
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (part === '..') {
|
|
39
|
+
throw new Error(`Invalid ${label}: path traversal sequences are not allowed`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const resolved = trustedBase + path.sep + parts.join(path.sep);
|
|
43
|
+
if (!resolved.startsWith(trustedBase + path.sep) && resolved !== trustedBase) {
|
|
44
|
+
throw new Error(`Invalid ${label}: resolved path escapes base directory`);
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate phaseNum is a positive integer and return the safe directory name.
|
|
51
|
+
*/
|
|
52
|
+
function safePhaseSegment(phaseNum) {
|
|
53
|
+
const n = Number(phaseNum);
|
|
54
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
55
|
+
throw new Error(`Invalid phase number: must be a positive integer, got ${phaseNum}`);
|
|
56
|
+
}
|
|
57
|
+
return `phase-${n}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a filename segment (agentName, taskId, planId) contains only safe chars.
|
|
62
|
+
* Allows alphanumeric, hyphens, underscores, and dots (no slashes or traversal).
|
|
63
|
+
*/
|
|
64
|
+
function safeFilenameSegment(value, label) {
|
|
65
|
+
const str = String(value);
|
|
66
|
+
if (str.length === 0) {
|
|
67
|
+
throw new Error(`Invalid ${label}: must be non-empty`);
|
|
68
|
+
}
|
|
69
|
+
if (!/^[\w.-]+$/.test(str)) {
|
|
70
|
+
throw new Error(`Invalid ${label}: contains disallowed characters`);
|
|
71
|
+
}
|
|
72
|
+
if (str === '.' || str === '..') {
|
|
73
|
+
throw new Error(`Invalid ${label}: traversal not allowed`);
|
|
74
|
+
}
|
|
75
|
+
return str;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Internal helpers — all paths built via safeJoin
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function planningRoot(projectRoot) {
|
|
83
|
+
// PLANNING_DIR is a constant, not user input — safe to join directly
|
|
84
|
+
return safeJoin(projectRoot, PLANNING_DIR, 'planning directory');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function phasePath(projectRoot, phaseNum) {
|
|
88
|
+
const root = planningRoot(projectRoot);
|
|
89
|
+
const phasesDir = safeJoin(root, PHASES_DIR, 'phases directory');
|
|
90
|
+
return safeJoin(phasesDir, safePhaseSegment(phaseNum), 'phase directory');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ensureDir(dirPath) {
|
|
94
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function safeReadFile(filePath) {
|
|
98
|
+
try {
|
|
99
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.code === 'ENOENT') return null;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function safeReadJson(filePath) {
|
|
107
|
+
const raw = safeReadFile(filePath);
|
|
108
|
+
if (raw === null) return null;
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(raw);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeFile(filePath, content) {
|
|
117
|
+
ensureDir(path.dirname(filePath));
|
|
118
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeJson(filePath, data) {
|
|
122
|
+
writeFile(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Roadmap parsing
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
const ROADMAP_HEADER = `# Roadmap
|
|
130
|
+
|
|
131
|
+
| Phase | Status | Description |
|
|
132
|
+
|-------|--------|-------------|`;
|
|
133
|
+
|
|
134
|
+
function parseRoadmapTable(content) {
|
|
135
|
+
const phases = [];
|
|
136
|
+
const lines = content.split('\n');
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
const match = line.match(/^\|\s*(\d+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*$/);
|
|
139
|
+
if (match && match[2].trim()) {
|
|
140
|
+
phases.push({
|
|
141
|
+
phase: parseInt(match[1], 10),
|
|
142
|
+
status: match[2].trim(),
|
|
143
|
+
description: match[3].trim(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return phases;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderRoadmapTable(phases) {
|
|
151
|
+
const rows = phases.map(p => `| ${p.phase} | ${p.status} | ${p.description} |`);
|
|
152
|
+
return ROADMAP_HEADER + '\n' + rows.join('\n') + '\n';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Progress parsing
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
const PROGRESS_HEADER = `# Progress
|
|
160
|
+
|
|
161
|
+
| Wave | Task | Status | Executor | Commit |
|
|
162
|
+
|------|------|--------|----------|--------|`;
|
|
163
|
+
|
|
164
|
+
function parseProgressTable(content) {
|
|
165
|
+
const entries = [];
|
|
166
|
+
const lines = content.split('\n');
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
const match = line.match(/^\|\s*(\d+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*$/);
|
|
169
|
+
if (match) {
|
|
170
|
+
entries.push({
|
|
171
|
+
wave: parseInt(match[1], 10),
|
|
172
|
+
task: match[2].trim(),
|
|
173
|
+
status: match[3].trim(),
|
|
174
|
+
executor: match[4].trim(),
|
|
175
|
+
commit: match[5].trim(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return entries;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderProgressTable(entries) {
|
|
183
|
+
const rows = entries.map(e =>
|
|
184
|
+
`| ${e.wave} | ${e.task} | ${e.status} | ${e.executor || ''} | ${e.commit || ''} |`
|
|
185
|
+
);
|
|
186
|
+
return PROGRESS_HEADER + '\n' + rows.join('\n') + '\n';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Public API
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create .planning/ structure with roadmap.md and config.json.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} projectRoot — absolute path to project root
|
|
197
|
+
* @param {object} config — { modelProfile, flags, ... }
|
|
198
|
+
* @returns {{ planningDir: string }}
|
|
199
|
+
*/
|
|
200
|
+
function initPlanning(projectRoot, config = {}) {
|
|
201
|
+
const root = planningRoot(projectRoot);
|
|
202
|
+
ensureDir(safeJoin(root, PHASES_DIR, 'phases directory'));
|
|
203
|
+
|
|
204
|
+
const configData = {
|
|
205
|
+
modelProfile: config.modelProfile || 'default',
|
|
206
|
+
flags: config.flags || {},
|
|
207
|
+
createdAt: new Date().toISOString(),
|
|
208
|
+
lastUpdated: new Date().toISOString(),
|
|
209
|
+
};
|
|
210
|
+
writeJson(safeJoin(root, CONFIG_FILE, 'config file'), configData);
|
|
211
|
+
writeFile(safeJoin(root, ROADMAP_FILE, 'roadmap file'), ROADMAP_HEADER + '\n');
|
|
212
|
+
|
|
213
|
+
return { planningDir: root };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create phases/phase-N/ with empty discussion.md, research/, plans/, tasks/, progress.md.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} projectRoot
|
|
220
|
+
* @param {number} phaseNum
|
|
221
|
+
* @returns {{ phaseDir: string }}
|
|
222
|
+
*/
|
|
223
|
+
function initPhase(projectRoot, phaseNum) {
|
|
224
|
+
const dir = phasePath(projectRoot, phaseNum);
|
|
225
|
+
|
|
226
|
+
ensureDir(safeJoin(dir, 'research', 'research directory'));
|
|
227
|
+
ensureDir(safeJoin(dir, 'plans', 'plans directory'));
|
|
228
|
+
ensureDir(safeJoin(dir, 'tasks', 'tasks directory'));
|
|
229
|
+
|
|
230
|
+
writeFile(
|
|
231
|
+
safeJoin(dir, 'discussion.md', 'discussion file'),
|
|
232
|
+
`# Phase ${phaseNum} — Discussion\n\n`,
|
|
233
|
+
);
|
|
234
|
+
writeFile(safeJoin(dir, 'progress.md', 'progress file'), PROGRESS_HEADER + '\n');
|
|
235
|
+
|
|
236
|
+
return { phaseDir: dir };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Read the latest progress state from a phase's progress.md.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} projectRoot
|
|
243
|
+
* @param {number} [phaseNum] — if omitted, finds the latest active phase
|
|
244
|
+
* @returns {{ phase: number, entries: Array, summary: { total: number, done: number, pending: number, failed: number, currentWave: number } } | null}
|
|
245
|
+
*/
|
|
246
|
+
function readProgress(projectRoot, phaseNum) {
|
|
247
|
+
if (phaseNum === undefined) {
|
|
248
|
+
const roadmap = readRoadmap(projectRoot);
|
|
249
|
+
if (!roadmap) return null;
|
|
250
|
+
const active = roadmap.find(p => p.status !== 'completed' && p.status !== 'skipped');
|
|
251
|
+
if (!active) return null;
|
|
252
|
+
phaseNum = active.phase;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const filePath = safeJoin(phasePath(projectRoot, phaseNum), 'progress.md', 'progress file');
|
|
256
|
+
const content = safeReadFile(filePath);
|
|
257
|
+
if (content === null) return null;
|
|
258
|
+
|
|
259
|
+
const entries = parseProgressTable(content);
|
|
260
|
+
const done = entries.filter(e => e.status === 'completed').length;
|
|
261
|
+
const failed = entries.filter(e => e.status === 'failed').length;
|
|
262
|
+
const pending = entries.length - done - failed;
|
|
263
|
+
const currentWave = entries.length > 0
|
|
264
|
+
? Math.max(...entries.filter(e => e.status !== 'completed').map(e => e.wave).concat([0]))
|
|
265
|
+
: 0;
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
phase: phaseNum,
|
|
269
|
+
entries,
|
|
270
|
+
summary: { total: entries.length, done, pending, failed, currentWave },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update progress.md with current wave execution state.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} projectRoot
|
|
278
|
+
* @param {number} phaseNum
|
|
279
|
+
* @param {Array<{ wave: number, task: string, status: string, executor?: string, commit?: string }>} progressData
|
|
280
|
+
*/
|
|
281
|
+
function writeProgress(projectRoot, phaseNum, progressData) {
|
|
282
|
+
const filePath = safeJoin(phasePath(projectRoot, phaseNum), 'progress.md', 'progress file');
|
|
283
|
+
writeFile(filePath, renderProgressTable(progressData));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse roadmap.md to get phase list with statuses.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} projectRoot
|
|
290
|
+
* @returns {Array<{ phase: number, status: string, description: string }> | null}
|
|
291
|
+
*/
|
|
292
|
+
function readRoadmap(projectRoot) {
|
|
293
|
+
const filePath = safeJoin(planningRoot(projectRoot), ROADMAP_FILE, 'roadmap file');
|
|
294
|
+
const content = safeReadFile(filePath);
|
|
295
|
+
if (content === null) return null;
|
|
296
|
+
return parseRoadmapTable(content);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Update a phase's status in roadmap.md.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} projectRoot
|
|
303
|
+
* @param {number} phaseNum
|
|
304
|
+
* @param {string} status — e.g. 'pending', 'active', 'completed', 'skipped'
|
|
305
|
+
* @returns {boolean} true if the phase was found and updated
|
|
306
|
+
*/
|
|
307
|
+
function updateRoadmap(projectRoot, phaseNum, status) {
|
|
308
|
+
const filePath = safeJoin(planningRoot(projectRoot), ROADMAP_FILE, 'roadmap file');
|
|
309
|
+
const phases = readRoadmap(projectRoot) || [];
|
|
310
|
+
const idx = phases.findIndex(p => p.phase === phaseNum);
|
|
311
|
+
if (idx < 0) return false;
|
|
312
|
+
phases[idx].status = status;
|
|
313
|
+
writeFile(filePath, renderRoadmapTable(phases));
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Read .planning/config.json.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} projectRoot
|
|
321
|
+
* @returns {object | null}
|
|
322
|
+
*/
|
|
323
|
+
function readConfig(projectRoot) {
|
|
324
|
+
return safeReadJson(safeJoin(planningRoot(projectRoot), CONFIG_FILE, 'config file'));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Write .planning/config.json.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} projectRoot
|
|
331
|
+
* @param {object} config
|
|
332
|
+
*/
|
|
333
|
+
function writeConfig(projectRoot, config) {
|
|
334
|
+
const toWrite = { ...config, lastUpdated: new Date().toISOString() };
|
|
335
|
+
writeJson(safeJoin(planningRoot(projectRoot), CONFIG_FILE, 'config file'), toWrite);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Append to discussion.md.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} projectRoot
|
|
342
|
+
* @param {number} phaseNum
|
|
343
|
+
* @param {string} content — markdown text to append
|
|
344
|
+
*/
|
|
345
|
+
function writeDiscussion(projectRoot, phaseNum, content) {
|
|
346
|
+
const filePath = safeJoin(phasePath(projectRoot, phaseNum), 'discussion.md', 'discussion file');
|
|
347
|
+
const existing = safeReadFile(filePath) || `# Phase ${phaseNum} — Discussion\n\n`;
|
|
348
|
+
writeFile(filePath, existing + content + '\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Write researcher output to research/.
|
|
353
|
+
*
|
|
354
|
+
* @param {string} projectRoot
|
|
355
|
+
* @param {number} phaseNum
|
|
356
|
+
* @param {string} agentName — e.g. 'stack', 'features', 'architecture', 'pitfalls'
|
|
357
|
+
* @param {string} content — markdown content
|
|
358
|
+
*/
|
|
359
|
+
function writeResearch(projectRoot, phaseNum, agentName, content) {
|
|
360
|
+
const safeName = safeFilenameSegment(agentName, 'agent name');
|
|
361
|
+
const researchDir = safeJoin(phasePath(projectRoot, phaseNum), 'research', 'research directory');
|
|
362
|
+
const filePath = safeJoin(researchDir, `${safeName}.md`, 'research file');
|
|
363
|
+
writeFile(filePath, content);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Write plan with dependency graph to plans/.
|
|
368
|
+
*
|
|
369
|
+
* @param {string} projectRoot
|
|
370
|
+
* @param {number} phaseNum
|
|
371
|
+
* @param {{ id: number, title: string, dependencies?: number[], tasks?: string[], waves?: Array, body?: string }} planData
|
|
372
|
+
*/
|
|
373
|
+
function writePlan(projectRoot, phaseNum, planData) {
|
|
374
|
+
const planId = safeFilenameSegment(String(planData.id || 1), 'plan id');
|
|
375
|
+
|
|
376
|
+
let content = `---\nid: ${planId}\ndependencies: [${(planData.dependencies || []).join(', ')}]\n---\n\n`;
|
|
377
|
+
content += `# Plan ${planId}: ${planData.title || 'Untitled'}\n\n`;
|
|
378
|
+
|
|
379
|
+
if (planData.waves) {
|
|
380
|
+
for (const wave of planData.waves) {
|
|
381
|
+
content += `## Wave ${wave.number}\n\n`;
|
|
382
|
+
for (const task of (wave.tasks || [])) {
|
|
383
|
+
content += `- ${task}\n`;
|
|
384
|
+
}
|
|
385
|
+
content += '\n';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (planData.body) {
|
|
390
|
+
content += planData.body + '\n';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const plansDir = safeJoin(phasePath(projectRoot, phaseNum), 'plans', 'plans directory');
|
|
394
|
+
const filePath = safeJoin(plansDir, `plan-${planId}.md`, 'plan file');
|
|
395
|
+
writeFile(filePath, content);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Write individual task spec to tasks/.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} projectRoot
|
|
402
|
+
* @param {number} phaseNum
|
|
403
|
+
* @param {string|number} taskId
|
|
404
|
+
* @param {{ title: string, description?: string, skills?: string[], files?: string[], acceptance?: string[], wave?: number }} taskSpec
|
|
405
|
+
*/
|
|
406
|
+
function writeTask(projectRoot, phaseNum, taskId, taskSpec) {
|
|
407
|
+
const safeId = safeFilenameSegment(String(taskId), 'task id');
|
|
408
|
+
|
|
409
|
+
let content = `---\nid: ${safeId}\nwave: ${taskSpec.wave || 1}\n`;
|
|
410
|
+
if (taskSpec.skills?.length) content += `skills: [${taskSpec.skills.join(', ')}]\n`;
|
|
411
|
+
content += `---\n\n`;
|
|
412
|
+
content += `# Task ${safeId}: ${taskSpec.title || 'Untitled'}\n\n`;
|
|
413
|
+
|
|
414
|
+
if (taskSpec.description) {
|
|
415
|
+
content += taskSpec.description + '\n\n';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (taskSpec.files?.length) {
|
|
419
|
+
content += `## Files\n\n`;
|
|
420
|
+
for (const f of taskSpec.files) content += `- ${f}\n`;
|
|
421
|
+
content += '\n';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (taskSpec.acceptance?.length) {
|
|
425
|
+
content += `## Acceptance Criteria\n\n`;
|
|
426
|
+
for (const a of taskSpec.acceptance) content += `- [ ] ${a}\n`;
|
|
427
|
+
content += '\n';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const tasksDir = safeJoin(phasePath(projectRoot, phaseNum), 'tasks', 'tasks directory');
|
|
431
|
+
const filePath = safeJoin(tasksDir, `task-${safeId}.md`, 'task file');
|
|
432
|
+
writeFile(filePath, content);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Walk up from startDir to find .planning/ (like how git finds .git/).
|
|
437
|
+
*
|
|
438
|
+
* @param {string} startDir — must be an absolute path
|
|
439
|
+
* @returns {string | null} — project root containing .planning/, or null
|
|
440
|
+
*/
|
|
441
|
+
function getPlanningRoot(startDir) {
|
|
442
|
+
if (typeof startDir !== 'string' || startDir.length === 0) {
|
|
443
|
+
throw new Error('getPlanningRoot: startDir must be a non-empty string');
|
|
444
|
+
}
|
|
445
|
+
// Require an absolute path — callers pass process.cwd() or PROJECT_DIR
|
|
446
|
+
if (!path.isAbsolute(startDir)) {
|
|
447
|
+
throw new Error('getPlanningRoot: startDir must be an absolute path');
|
|
448
|
+
}
|
|
449
|
+
let dir = startDir;
|
|
450
|
+
const root = path.parse(dir).root;
|
|
451
|
+
|
|
452
|
+
while (dir !== root) {
|
|
453
|
+
// PLANNING_DIR is a constant — safe to join
|
|
454
|
+
const candidate = safeJoin(dir, PLANNING_DIR, 'planning directory');
|
|
455
|
+
if (fs.existsSync(candidate)) {
|
|
456
|
+
return dir;
|
|
457
|
+
}
|
|
458
|
+
dir = path.dirname(dir);
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Check if there's an active (non-completed) planning session.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} projectRoot
|
|
467
|
+
* @returns {boolean}
|
|
468
|
+
*/
|
|
469
|
+
function isActive(projectRoot) {
|
|
470
|
+
const roadmap = readRoadmap(projectRoot);
|
|
471
|
+
if (!roadmap || roadmap.length === 0) {
|
|
472
|
+
return fs.existsSync(planningRoot(projectRoot));
|
|
473
|
+
}
|
|
474
|
+
return roadmap.some(p => p.status !== 'completed' && p.status !== 'skipped');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Exports
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
module.exports = {
|
|
482
|
+
initPlanning,
|
|
483
|
+
initPhase,
|
|
484
|
+
readProgress,
|
|
485
|
+
writeProgress,
|
|
486
|
+
readRoadmap,
|
|
487
|
+
updateRoadmap,
|
|
488
|
+
readConfig,
|
|
489
|
+
writeConfig,
|
|
490
|
+
writeDiscussion,
|
|
491
|
+
writeResearch,
|
|
492
|
+
writePlan,
|
|
493
|
+
writeTask,
|
|
494
|
+
getPlanningRoot,
|
|
495
|
+
isActive,
|
|
496
|
+
// Exposed for testing
|
|
497
|
+
parseRoadmapTable,
|
|
498
|
+
renderRoadmapTable,
|
|
499
|
+
parseProgressTable,
|
|
500
|
+
renderProgressTable,
|
|
501
|
+
PLANNING_DIR,
|
|
502
|
+
};
|
package/cli/lib/platform.js
CHANGED
|
@@ -12,11 +12,15 @@ const PATHS = {
|
|
|
12
12
|
settingsBackup: path.join(home, '.claude', 'settings.json.backup'),
|
|
13
13
|
hooksDir: path.join(home, '.claude', 'hooks'),
|
|
14
14
|
skillsDir: path.join(home, '.claude', 'skills'),
|
|
15
|
+
agentsDir: path.join(home, '.claude', 'agents'),
|
|
15
16
|
templatesDir: path.join(home, '.claude', 'templates'),
|
|
16
17
|
sessionsDir: path.join(home, '.claude', 'sessions'),
|
|
17
18
|
versionFile: path.join(home, '.claude', '.supermind-version'),
|
|
18
19
|
legacyHooksJson: path.join(home, '.claude', 'hooks.json'),
|
|
19
20
|
airisDir: path.join(home, '.claude', 'airis'),
|
|
21
|
+
improvementLog: path.join(home, '.claude', 'improvement-log.jsonl'),
|
|
22
|
+
skillsLock: path.join(home, '.claude', 'skills-lock.json'),
|
|
23
|
+
approvedCommands: path.join(home, '.claude', 'supermind-approved.json'),
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
function ensureDir(dirPath) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { PATHS, ensureDir, getPackageRoot } = require('./platform');
|
|
6
|
+
const logger = require('./logger');
|
|
7
|
+
const { version } = require('../../package.json');
|
|
8
|
+
|
|
9
|
+
const PLUGIN_NAME = 'supermind';
|
|
10
|
+
const PLUGIN_MARKETPLACE = 'local';
|
|
11
|
+
const PLUGIN_KEY = `${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}`;
|
|
12
|
+
|
|
13
|
+
function getPluginsDir() {
|
|
14
|
+
return path.join(PATHS.claudeHome, 'plugins');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getPluginCacheBase() {
|
|
18
|
+
return path.join(getPluginsDir(), 'cache', PLUGIN_MARKETPLACE, PLUGIN_NAME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPluginCacheDir() {
|
|
22
|
+
return path.join(getPluginCacheBase(), version);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getInstalledPluginsPath() {
|
|
26
|
+
return path.join(getPluginsDir(), 'installed_plugins.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readInstalledPlugins() {
|
|
30
|
+
const filePath = getInstalledPluginsPath();
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return { version: 2, plugins: {} };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeInstalledPlugins(data) {
|
|
39
|
+
const filePath = getInstalledPluginsPath();
|
|
40
|
+
ensureDir(path.dirname(filePath));
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build plugin.json with the current version from package.json.
|
|
45
|
+
// The template .claude-plugin/plugin.json uses a placeholder version
|
|
46
|
+
// that is always overridden here with the actual package.json version.
|
|
47
|
+
function buildPluginManifest() {
|
|
48
|
+
const templatePath = path.join(getPackageRoot(), '.claude-plugin', 'plugin.json');
|
|
49
|
+
const manifest = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
|
|
50
|
+
manifest.version = version;
|
|
51
|
+
return manifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Remove stale version directories from the plugin cache, keeping only the current version.
|
|
55
|
+
function cleanOldVersions() {
|
|
56
|
+
const cacheBase = getPluginCacheBase();
|
|
57
|
+
if (!fs.existsSync(cacheBase)) return;
|
|
58
|
+
for (const entry of fs.readdirSync(cacheBase)) {
|
|
59
|
+
if (entry !== version) {
|
|
60
|
+
const stale = path.join(cacheBase, entry);
|
|
61
|
+
try {
|
|
62
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
63
|
+
} catch { /* non-critical cleanup */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Register Supermind as a Claude Code plugin.
|
|
69
|
+
// This is a forward-looking registration stub — Supermind's skills, hooks, and agents
|
|
70
|
+
// are delivered through the traditional ~/.claude/ installation paths, not through the
|
|
71
|
+
// plugin cache. The registration enables future marketplace discovery and `/plugin update`
|
|
72
|
+
// support when Claude Code adds npm-based plugin sources.
|
|
73
|
+
function installPlugin() {
|
|
74
|
+
const cacheDir = getPluginCacheDir();
|
|
75
|
+
ensureDir(cacheDir);
|
|
76
|
+
|
|
77
|
+
// Write plugin manifest to cache
|
|
78
|
+
const pluginDir = path.join(cacheDir, '.claude-plugin');
|
|
79
|
+
ensureDir(pluginDir);
|
|
80
|
+
const manifest = buildPluginManifest();
|
|
81
|
+
fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
82
|
+
|
|
83
|
+
// Clean up old version directories
|
|
84
|
+
cleanOldVersions();
|
|
85
|
+
|
|
86
|
+
// Register in installed_plugins.json
|
|
87
|
+
const registry = readInstalledPlugins();
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const entry = {
|
|
90
|
+
scope: 'user',
|
|
91
|
+
installPath: cacheDir,
|
|
92
|
+
version: version,
|
|
93
|
+
installedAt: now,
|
|
94
|
+
lastUpdated: now,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Preserve original installedAt on re-install
|
|
98
|
+
const existing = registry.plugins[PLUGIN_KEY];
|
|
99
|
+
if (existing && Array.isArray(existing) && existing.length > 0) {
|
|
100
|
+
entry.installedAt = existing[0].installedAt || now;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
registry.plugins[PLUGIN_KEY] = [entry];
|
|
104
|
+
writeInstalledPlugins(registry);
|
|
105
|
+
|
|
106
|
+
logger.success(`Plugin registered as ${PLUGIN_KEY}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove plugin registration and cached files
|
|
110
|
+
function removePlugin() {
|
|
111
|
+
// Remove from installed_plugins.json
|
|
112
|
+
const registry = readInstalledPlugins();
|
|
113
|
+
if (registry.plugins[PLUGIN_KEY]) {
|
|
114
|
+
delete registry.plugins[PLUGIN_KEY];
|
|
115
|
+
writeInstalledPlugins(registry);
|
|
116
|
+
logger.success('Plugin registration removed');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove cached plugin directory
|
|
120
|
+
const cacheBase = getPluginCacheBase();
|
|
121
|
+
if (fs.existsSync(cacheBase)) {
|
|
122
|
+
fs.rmSync(cacheBase, { recursive: true, force: true });
|
|
123
|
+
logger.success('Plugin cache removed');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { installPlugin, removePlugin, PLUGIN_NAME, PLUGIN_KEY };
|