supermind-claude 2.1.0 → 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.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +21 -0
  2. package/README.md +34 -46
  3. package/agents/code-reviewer.md +81 -0
  4. package/cli/commands/doctor.js +415 -79
  5. package/cli/commands/install.js +17 -18
  6. package/cli/commands/skill.js +164 -0
  7. package/cli/commands/uninstall.js +32 -3
  8. package/cli/commands/update.js +27 -5
  9. package/cli/index.js +16 -4
  10. package/cli/lib/agents.js +413 -0
  11. package/cli/lib/executor.js +365 -0
  12. package/cli/lib/hooks.js +8 -1
  13. package/cli/lib/logger.js +1 -1
  14. package/cli/lib/mcp.js +25 -5
  15. package/cli/lib/planning.js +502 -0
  16. package/cli/lib/platform.js +4 -0
  17. package/cli/lib/plugin.js +127 -0
  18. package/cli/lib/settings.js +2 -40
  19. package/cli/lib/skills.js +39 -2
  20. package/cli/lib/templates.js +48 -1
  21. package/cli/lib/vendor-skills.js +594 -0
  22. package/hooks/bash-permissions.js +196 -176
  23. package/hooks/context-monitor.js +79 -0
  24. package/hooks/improvement-logger.js +94 -0
  25. package/hooks/pre-merge-checklist.js +102 -0
  26. package/hooks/session-start.js +109 -5
  27. package/hooks/statusline-command.js +123 -29
  28. package/package.json +4 -2
  29. package/skills/anti-rationalization/SKILL.md +38 -0
  30. package/skills/brainstorming/SKILL.md +165 -0
  31. package/skills/code-review/SKILL.md +144 -0
  32. package/skills/executing-plans/SKILL.md +138 -0
  33. package/skills/finishing-branches/SKILL.md +144 -0
  34. package/skills/project/SKILL.md +533 -0
  35. package/skills/quick/SKILL.md +178 -0
  36. package/skills/supermind/SKILL.md +58 -4
  37. package/skills/supermind-init/SKILL.md +48 -2
  38. package/skills/systematic-debugging/SKILL.md +129 -0
  39. package/skills/tdd/SKILL.md +179 -0
  40. package/skills/using-git-worktrees/SKILL.md +138 -0
  41. package/skills/verification-before-completion/SKILL.md +54 -0
  42. package/skills/writing-plans/SKILL.md +169 -0
  43. package/templates/CLAUDE.md +124 -61
  44. package/cli/lib/plugins.js +0 -23
@@ -0,0 +1,365 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Path safety — mirrors planning.js / vendor-skills.js safeJoin pattern
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function safeJoin(trustedBase, segment, label) {
12
+ if (typeof segment !== 'string' || segment.length === 0) {
13
+ throw new Error(`Invalid ${label}: must be a non-empty string`);
14
+ }
15
+ if (path.isAbsolute(segment)) {
16
+ throw new Error(`Invalid ${label}: must not be an absolute path`);
17
+ }
18
+ const parts = segment.split(/[\\/]/);
19
+ for (const part of parts) {
20
+ if (part === '..') {
21
+ throw new Error(`Invalid ${label}: path traversal sequences are not allowed`);
22
+ }
23
+ }
24
+ const resolved = trustedBase + path.sep + parts.join(path.sep);
25
+ if (!resolved.startsWith(trustedBase + path.sep) && resolved !== trustedBase) {
26
+ throw new Error(`Invalid ${label}: resolved path escapes base directory`);
27
+ }
28
+ return resolved;
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Constants
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
36
+ const SKILLS_DIR_NAME = 'skills';
37
+
38
+ /** Maps task types to the methodology skills injected into executor prompts. */
39
+ const SKILL_MAP = {
40
+ 'write-feature': ['tdd', 'verification-before-completion', 'anti-rationalization', 'using-git-worktrees'],
41
+ 'fix-bug': ['systematic-debugging', 'verification-before-completion', 'anti-rationalization', 'using-git-worktrees'],
42
+ 'refactor': ['verification-before-completion', 'anti-rationalization', 'using-git-worktrees'],
43
+ 'write-test': ['tdd', 'anti-rationalization'],
44
+ 'research': [],
45
+ };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // getSkillContent — reads SKILL.md from the skills directory
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Read a skill's SKILL.md content.
53
+ *
54
+ * Search order:
55
+ * 1. ~/.claude/skills/{skillName}/SKILL.md
56
+ * 2. {projectRoot}/.claude/skills/{skillName}/SKILL.md (project-level)
57
+ *
58
+ * @param {string} skillName — e.g. 'tdd', 'anti-rationalization'
59
+ * @param {string} [projectRoot] — project root for project-level fallback
60
+ * @returns {string | null} file content or null if not found
61
+ */
62
+ function getSkillContent(skillName, projectRoot) {
63
+ if (typeof skillName !== 'string' || skillName.length === 0) return null;
64
+ if (!/^[\w.-]+$/.test(skillName)) return null;
65
+
66
+ // Global: ~/.claude/skills/{skillName}/SKILL.md
67
+ const globalSkillsDir = safeJoin(CLAUDE_DIR, SKILLS_DIR_NAME, 'skills directory');
68
+ const globalPath = safeJoin(
69
+ safeJoin(globalSkillsDir, skillName, 'skill name'),
70
+ 'SKILL.md',
71
+ 'skill file',
72
+ );
73
+ try {
74
+ return fs.readFileSync(globalPath, 'utf-8');
75
+ } catch { /* fall through */ }
76
+
77
+ // Project-level: {projectRoot}/.claude/skills/{skillName}/SKILL.md
78
+ if (typeof projectRoot === 'string' && projectRoot.length > 0) {
79
+ const projectClaudeDir = safeJoin(projectRoot, '.claude', 'project claude dir');
80
+ const projectSkillsDir = safeJoin(projectClaudeDir, SKILLS_DIR_NAME, 'skills directory');
81
+ const projectPath = safeJoin(
82
+ safeJoin(projectSkillsDir, skillName, 'skill name'),
83
+ 'SKILL.md',
84
+ 'skill file',
85
+ );
86
+ try {
87
+ return fs.readFileSync(projectPath, 'utf-8');
88
+ } catch { /* fall through */ }
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // buildTaskPacket — assembles everything a subagent needs
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Assemble a self-contained task packet for a fresh-context executor.
100
+ *
101
+ * @param {object} task
102
+ * @param {string} task.id — unique task identifier
103
+ * @param {string} task.title — short task title
104
+ * @param {string} task.type — one of: write-feature, fix-bug, refactor, write-test, research
105
+ * @param {string} [task.description] — what to do (falls back to placeholder)
106
+ * @param {string[]} [task.files] — files the executor should read/modify
107
+ * @param {string[]} [task.acceptance] — acceptance criteria
108
+ * @param {string} [task.expectedOutput] — what the result should look like
109
+ * @param {string[]} [task.dependsOn] — IDs of tasks this depends on
110
+ * @param {object} [options]
111
+ * @param {string} [options.projectRoot] — for skill + context resolution
112
+ * @param {string} [options.branch] — current git branch
113
+ * @param {string} [options.recentCommits] — compact git log
114
+ * @param {string} [options.architectureExcerpt] — relevant ARCHITECTURE.md sections
115
+ * @param {string} [options.conventions] — key CLAUDE.md conventions
116
+ * @returns {string} a single prompt string ready for the Agent tool
117
+ */
118
+ function buildTaskPacket(task, options = {}) {
119
+ if (!task || !task.id || !task.title || !task.type) {
120
+ throw new Error('buildTaskPacket: task must have id, title, and type');
121
+ }
122
+ if (!SKILL_MAP.hasOwnProperty(task.type)) {
123
+ const valid = Object.keys(SKILL_MAP).join(', ');
124
+ throw new Error(`buildTaskPacket: invalid task type "${task.type}" (valid: ${valid})`);
125
+ }
126
+
127
+ const sections = [];
128
+
129
+ // --- Task spec ---
130
+ sections.push('# Task Spec');
131
+ sections.push(`**ID:** ${task.id}`);
132
+ sections.push(`**Title:** ${task.title}`);
133
+ sections.push(`**Type:** ${task.type}`);
134
+ sections.push('');
135
+ sections.push('## What to Do');
136
+ sections.push(task.description || '(no description provided)');
137
+
138
+ if (task.files && task.files.length > 0) {
139
+ sections.push('');
140
+ sections.push('## Files');
141
+ for (const f of task.files) {
142
+ sections.push(`- ${f}`);
143
+ }
144
+ }
145
+
146
+ if (task.acceptance && task.acceptance.length > 0) {
147
+ sections.push('');
148
+ sections.push('## Acceptance Criteria');
149
+ for (const a of task.acceptance) {
150
+ sections.push(`- [ ] ${a}`);
151
+ }
152
+ }
153
+
154
+ if (task.expectedOutput) {
155
+ sections.push('');
156
+ sections.push('## Expected Output');
157
+ sections.push(task.expectedOutput);
158
+ }
159
+
160
+ // --- Project context (compact) ---
161
+ const contextParts = [];
162
+ if (options.branch) {
163
+ contextParts.push(`**Branch:** ${options.branch}`);
164
+ }
165
+ if (options.recentCommits) {
166
+ contextParts.push(`**Recent commits:**\n${options.recentCommits}`);
167
+ }
168
+ if (options.architectureExcerpt) {
169
+ contextParts.push(`**Architecture:**\n${options.architectureExcerpt}`);
170
+ }
171
+ if (options.conventions) {
172
+ contextParts.push(`**Conventions:**\n${options.conventions}`);
173
+ }
174
+ if (contextParts.length > 0) {
175
+ sections.push('');
176
+ sections.push('# Project Context');
177
+ sections.push(contextParts.join('\n\n'));
178
+ }
179
+
180
+ // --- Injected skills ---
181
+ const skillNames = SKILL_MAP[task.type] || [];
182
+ const skillSections = [];
183
+ for (const name of skillNames) {
184
+ const content = getSkillContent(name, options.projectRoot);
185
+ if (content) {
186
+ skillSections.push(`<skill name="${name}">\n${content}\n</skill>`);
187
+ }
188
+ }
189
+ if (skillSections.length > 0) {
190
+ sections.push('');
191
+ sections.push('# Methodology Skills');
192
+ sections.push('Follow these skills strictly:');
193
+ sections.push(skillSections.join('\n\n'));
194
+ }
195
+
196
+ // --- Completion contract ---
197
+ sections.push('');
198
+ sections.push('# Completion Contract');
199
+ sections.push('You MUST follow these rules:');
200
+ sections.push('- Commit your work atomically when done (one commit per task)');
201
+ sections.push('- Report results: files changed, tests run, tests passed');
202
+ sections.push('- Stay in scope — only modify files related to this task');
203
+ sections.push('- NEVER merge branches or push to main/master');
204
+ sections.push('- NEVER skip tests or verification steps');
205
+ sections.push('- If you cannot complete the task, report what failed and why');
206
+
207
+ return sections.join('\n');
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // executeTask — builds the prompt an orchestrator passes to Agent tool
212
+ // ---------------------------------------------------------------------------
213
+
214
+ /**
215
+ * Build a structured execution request from a task packet.
216
+ *
217
+ * This does NOT call the Agent tool — the orchestrator skill does that.
218
+ * It returns the prompt string and metadata the orchestrator needs.
219
+ *
220
+ * @param {string} taskPacket — output of buildTaskPacket
221
+ * @param {object} [options]
222
+ * @param {boolean} [options.useWorktree] — whether executor should use a worktree
223
+ * @param {string} [options.model] — model override (e.g. 'sonnet', 'opus')
224
+ * @returns {{ prompt: string, description: string, model?: string, isolation?: string }}
225
+ */
226
+ function executeTask(taskPacket, options = {}) {
227
+ if (typeof taskPacket !== 'string' || taskPacket.length === 0) {
228
+ throw new Error('executeTask: taskPacket must be a non-empty string');
229
+ }
230
+
231
+ // Extract task title from the packet for the Agent description
232
+ const titleMatch = taskPacket.match(/^\*\*Title:\*\*\s*(.+)$/m);
233
+ const title = titleMatch ? titleMatch[1].trim() : 'Execute task';
234
+
235
+ // Truncate description to 5 words for the Agent tool's description field
236
+ const descWords = title.split(/\s+/).slice(0, 5).join(' ');
237
+
238
+ const result = {
239
+ prompt: taskPacket,
240
+ description: descWords,
241
+ };
242
+
243
+ if (options.model) {
244
+ result.model = options.model;
245
+ }
246
+
247
+ if (options.useWorktree) {
248
+ result.isolation = 'worktree';
249
+ }
250
+
251
+ return result;
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // buildWavePlan — topological sort + wave grouping
256
+ // ---------------------------------------------------------------------------
257
+
258
+ /**
259
+ * Group tasks into execution waves based on dependency graph.
260
+ *
261
+ * @param {Array<{ id: string, dependsOn?: string[] }>} tasks
262
+ * @returns {Array<{ wave: number, tasks: Array }>}
263
+ * @throws if circular dependencies are detected
264
+ */
265
+ function buildWavePlan(tasks) {
266
+ if (!Array.isArray(tasks) || tasks.length === 0) {
267
+ return [];
268
+ }
269
+
270
+ // Build lookup: taskId → task object, and pre-compute string deps
271
+ const taskMap = new Map();
272
+ const depsMap = new Map(); // taskId → string[] of dependency IDs
273
+
274
+ for (const task of tasks) {
275
+ const id = String(task.id);
276
+ taskMap.set(id, task);
277
+ depsMap.set(id, (task.dependsOn || []).map(String));
278
+ }
279
+
280
+ // Validate: all referenced dependencies must exist in the task list
281
+ for (const [id, deps] of depsMap) {
282
+ for (const dep of deps) {
283
+ if (!taskMap.has(dep)) {
284
+ throw new Error(
285
+ `Task "${id}" depends on "${dep}" which does not exist in the task list`,
286
+ );
287
+ }
288
+ }
289
+ }
290
+
291
+ const waves = [];
292
+ const resolved = new Set();
293
+ const remaining = new Set(taskMap.keys());
294
+
295
+ while (remaining.size > 0) {
296
+ // Collect tasks whose dependencies are all resolved
297
+ const ready = [];
298
+ for (const id of remaining) {
299
+ if (depsMap.get(id).every(d => resolved.has(d))) {
300
+ ready.push(id);
301
+ }
302
+ }
303
+
304
+ if (ready.length === 0) {
305
+ const stuck = Array.from(remaining).join(', ');
306
+ throw new Error(`Circular dependency detected among tasks: ${stuck}`);
307
+ }
308
+
309
+ const waveNum = waves.length + 1;
310
+ waves.push({ wave: waveNum, tasks: ready.map(id => taskMap.get(id)) });
311
+
312
+ for (const id of ready) {
313
+ resolved.add(id);
314
+ remaining.delete(id);
315
+ }
316
+ }
317
+
318
+ return waves;
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // formatWaveProgress — Markdown progress table
323
+ // ---------------------------------------------------------------------------
324
+
325
+ /**
326
+ * Render a Markdown progress table from a wave plan and results.
327
+ *
328
+ * @param {Array<{ wave: number, tasks: Array<{ id: string, title: string }> }>} wavePlan
329
+ * @param {Map<string, { status: string, commitHash?: string }>} [results] — taskId → result
330
+ * @returns {string} Markdown table
331
+ */
332
+ function formatWaveProgress(wavePlan, results) {
333
+ const resultMap = results || new Map();
334
+ const lines = [];
335
+
336
+ lines.push('| Wave | Task | Status | Commit |');
337
+ lines.push('|------|------|--------|--------|');
338
+
339
+ for (const wave of wavePlan) {
340
+ for (const task of wave.tasks) {
341
+ const id = String(task.id);
342
+ const r = resultMap.get(id);
343
+ const status = r ? r.status : 'pending';
344
+ const commit = (r && r.commitHash) ? r.commitHash.slice(0, 7) : '';
345
+ const safeTitle = (task.title || id).replace(/\|/g, '\\|');
346
+ lines.push(`| ${wave.wave} | ${safeTitle} | ${status} | ${commit} |`);
347
+ }
348
+ }
349
+
350
+ return lines.join('\n') + '\n';
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // Exports
355
+ // ---------------------------------------------------------------------------
356
+
357
+ module.exports = {
358
+ buildTaskPacket,
359
+ executeTask,
360
+ buildWavePlan,
361
+ formatWaveProgress,
362
+ getSkillContent,
363
+ // Exposed for testing
364
+ SKILL_MAP,
365
+ };
package/cli/lib/hooks.js CHANGED
@@ -33,10 +33,17 @@ function getHookSettings() {
33
33
  SessionStart: [{
34
34
  hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'session-start.js')}"`, statusMessage: 'Loading session context...' }],
35
35
  }],
36
+ PostToolUse: [{
37
+ matcher: 'Bash',
38
+ hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'pre-merge-checklist.js')}"`, timeout: 5 }],
39
+ }, {
40
+ hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'context-monitor.js')}"`, timeout: 3 }],
41
+ }],
36
42
  Stop: [{
37
43
  hooks: [
38
44
  { type: 'command', command: `node "${path.join(hooksDir, 'session-end.js')}"`, async: true },
39
45
  { type: 'command', command: `node "${path.join(hooksDir, 'cost-tracker.js')}"`, async: true },
46
+ { type: 'command', command: `node "${path.join(hooksDir, 'improvement-logger.js')}"`, async: true },
40
47
  ],
41
48
  }],
42
49
  },
@@ -48,7 +55,7 @@ function getHookSettings() {
48
55
  }
49
56
 
50
57
  // Fallback list if package source is unavailable
51
- const KNOWN_HOOKS = ['bash-permissions.js', 'session-start.js', 'session-end.js', 'cost-tracker.js', 'statusline-command.js'];
58
+ const KNOWN_HOOKS = ['bash-permissions.js', 'session-start.js', 'session-end.js', 'cost-tracker.js', 'statusline-command.js', 'pre-merge-checklist.js', 'improvement-logger.js', 'context-monitor.js'];
52
59
 
53
60
  function removeHooks() {
54
61
  if (!fs.existsSync(PATHS.hooksDir)) return;
package/cli/lib/logger.js CHANGED
@@ -35,4 +35,4 @@ function info(message) {
35
35
  console.log(` ${DIM}${message}${R}`);
36
36
  }
37
37
 
38
- module.exports = { banner, step, success, warn, error, info };
38
+ module.exports = { banner, step, success, warn, error, info, GREEN, YELLOW, RED, DIM, BOLD, CYAN, R };
package/cli/lib/mcp.js CHANGED
@@ -91,22 +91,42 @@ async function setupMcp(flags) {
91
91
  }
92
92
  if (!mode || mode === 'skip') {
93
93
  logger.info('Skipping MCP setup');
94
- return {};
94
+ return { mode: 'skip' };
95
95
  }
96
96
 
97
97
  if (mode === 'docker') {
98
98
  await setupDocker();
99
- return {}; // Docker mode uses AIRIS, not settings.json mcpServers
99
+ return { mode: 'docker' }; // Docker uses AIRIS gateway; mode drives template MCP section
100
100
  }
101
101
 
102
102
  if (mode === 'direct') {
103
103
  const apiKeys = await promptApiKeys(flags);
104
104
  const servers = setupDirect(apiKeys);
105
105
  logger.success(`Configured ${Object.keys(servers).length} MCP servers`);
106
- return { mcpServers: servers };
106
+ return { mode: 'direct', mcpServers: servers };
107
107
  }
108
108
 
109
- return {};
109
+ return { mode: 'skip' };
110
110
  }
111
111
 
112
- module.exports = { setupMcp };
112
+ // Infer MCP mode from installed artifacts. Used by update to re-render the template MCP section.
113
+ function detectMcpMode() {
114
+ if (fs.existsSync(path.join(PATHS.airisDir, 'docker-compose.yml'))) {
115
+ return 'docker';
116
+ }
117
+ // Any known direct-mode server in settings.json means direct install was used
118
+ try {
119
+ const settings = JSON.parse(fs.readFileSync(PATHS.settings, 'utf-8'));
120
+ const servers = settings.mcpServers || {};
121
+ if (servers.context7 || servers.playwright || servers.serena || servers.tavily) {
122
+ return 'direct';
123
+ }
124
+ } catch (err) {
125
+ if (err.code !== 'ENOENT') {
126
+ logger.warn(`Could not read settings.json to detect MCP mode: ${err.message}`);
127
+ }
128
+ }
129
+ return 'skip';
130
+ }
131
+
132
+ module.exports = { setupMcp, detectMcpMode };