@relipa/ai-flow-kit 0.0.6 → 0.0.7-beta.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/README.md +41 -16
- package/bin/aiflow.js +65 -2
- package/custom/mcp-presets/gitnexus.json +8 -0
- package/custom/skills/generate-spec/SKILL.md +7 -0
- package/custom/skills/impact-analysis/SKILL.md +10 -0
- package/custom/skills/investigate-bug/SKILL.md +17 -0
- package/custom/skills/read-study-requirement/SKILL.md +11 -0
- package/custom/skills/review-plan/SKILL.md +15 -0
- package/custom/templates/php-plain.md +261 -0
- package/docs/common/AIFLOW.md +17 -6
- package/docs/common/CHANGELOG.md +63 -5
- package/docs/common/QUICK_START.md +33 -13
- package/docs/common/cli-reference.md +23 -0
- package/package.json +2 -7
- package/scripts/checkpoint.js +46 -0
- package/scripts/doctor.js +111 -8
- package/scripts/gitnexus-worker.js +94 -0
- package/scripts/guide.js +42 -51
- package/scripts/hooks/session-start.js +35 -5
- package/scripts/hooks/session-stop.js +55 -0
- package/scripts/init.js +299 -19
- package/scripts/prompt.js +2 -2
- package/scripts/remove.js +54 -0
- package/scripts/task.js +101 -0
- package/scripts/update.js +14 -4
- package/scripts/use.js +78 -6
- package/upstream/.claude-plugin/marketplace.json +4 -4
- package/upstream/.claude-plugin/plugin.json +2 -4
- package/upstream/.cursor-plugin/plugin.json +2 -4
- package/upstream/docs/plans/2025-11-22-opencode-support-design.md +1 -1
- package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +2 -2
- package/upstream/docs/testing.md +2 -2
- package/upstream/skills/subagent-driven-development/SKILL.md +5 -6
- package/upstream/skills/subagent-driven-development/implementer-prompt.md +2 -3
- package/upstream/skills/systematic-debugging/CREATION-LOG.md +1 -1
- package/upstream/skills/systematic-debugging/root-cause-tracing.md +1 -1
- package/upstream/skills/tdd-lean/SKILL.md +127 -0
- package/upstream/skills/using-git-worktrees/SKILL.md +4 -5
- package/upstream/skills/writing-plans/SKILL.md +3 -9
- package/upstream/tests/brainstorm-server/package-lock.json +36 -0
package/scripts/init.js
CHANGED
|
@@ -8,14 +8,28 @@ const PKG_DIR = path.join(__dirname, '..');
|
|
|
8
8
|
const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
|
|
9
9
|
const GLOBAL_CREDENTIALS_PATH = path.join(GLOBAL_AIFLOW_DIR, 'credentials.json');
|
|
10
10
|
const PKG_VERSION = require('../package.json').version;
|
|
11
|
+
const { execSync, spawn } = require('child_process');
|
|
12
|
+
|
|
13
|
+
function commandExists(cmd) {
|
|
14
|
+
try {
|
|
15
|
+
const isWin = process.platform === 'win32';
|
|
16
|
+
const checkCmd = isWin ? `where ${cmd}` : `which ${cmd}`;
|
|
17
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
18
|
+
return true;
|
|
19
|
+
} catch (_) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
// Map framework → language family for picking the right rules
|
|
13
25
|
const FRAMEWORK_LANGUAGE = {
|
|
14
26
|
'laravel': 'php',
|
|
27
|
+
'php-plain': 'php',
|
|
15
28
|
'spring-boot': 'java',
|
|
16
29
|
'reactjs': 'javascript',
|
|
17
30
|
'nextjs': 'javascript',
|
|
18
31
|
'vue-nuxt': 'javascript',
|
|
32
|
+
'nestjs': 'javascript',
|
|
19
33
|
};
|
|
20
34
|
|
|
21
35
|
const AI_TOOL_FILES = {
|
|
@@ -53,7 +67,7 @@ async function detectFramework(projectDir) {
|
|
|
53
67
|
if (deps['next']) return 'nextjs';
|
|
54
68
|
if (deps['nuxt']) return 'vue-nuxt';
|
|
55
69
|
if (deps['vue']) return 'vue-nuxt';
|
|
56
|
-
if (deps['@nestjs/core']) return '
|
|
70
|
+
if (deps['@nestjs/core']) return 'nestjs';
|
|
57
71
|
if (deps['react']) return 'reactjs';
|
|
58
72
|
if (deps['express'] || deps['fastify']) return 'nodejs-express';
|
|
59
73
|
}
|
|
@@ -66,6 +80,7 @@ async function detectFramework(projectDir) {
|
|
|
66
80
|
if (await fs.pathExists(composerPath)) {
|
|
67
81
|
const composer = await fs.readJson(composerPath).catch(() => ({}));
|
|
68
82
|
if ((composer.require || {})['laravel/framework']) return 'laravel';
|
|
83
|
+
return 'php-plain';
|
|
69
84
|
}
|
|
70
85
|
// Python
|
|
71
86
|
if (await fs.pathExists(path.join(projectDir, 'manage.py'))) return 'python-django';
|
|
@@ -84,6 +99,8 @@ const FRAMEWORK_CHOICES = [
|
|
|
84
99
|
{ name: 'Next.js', value: 'nextjs' },
|
|
85
100
|
{ name: 'Vue / Nuxt', value: 'vue-nuxt' },
|
|
86
101
|
{ name: 'Laravel (PHP)', value: 'laravel' },
|
|
102
|
+
{ name: 'PHP (no framework)', value: 'php-plain' },
|
|
103
|
+
{ name: 'NestJS', value: 'nestjs' },
|
|
87
104
|
{ name: 'Node.js / Express', value: 'nodejs-express' },
|
|
88
105
|
{ name: 'Python Django', value: 'python-django' },
|
|
89
106
|
{ name: 'Python FastAPI', value: 'python-fastapi' },
|
|
@@ -157,9 +174,13 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
157
174
|
const hooksDir = path.join(claudeDir, 'hooks');
|
|
158
175
|
await fs.ensureDir(hooksDir);
|
|
159
176
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
await fs.copy(
|
|
177
|
+
const hookStartSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-start.js');
|
|
178
|
+
const hookStartDest = path.join(hooksDir, 'session-start.js');
|
|
179
|
+
await fs.copy(hookStartSrc, hookStartDest, { overwrite: true });
|
|
180
|
+
|
|
181
|
+
const hookStopSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-stop.js');
|
|
182
|
+
const hookStopDest = path.join(hooksDir, 'session-stop.js');
|
|
183
|
+
await fs.copy(hookStopSrc, hookStopDest, { overwrite: true });
|
|
163
184
|
|
|
164
185
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
165
186
|
let settings = {};
|
|
@@ -169,6 +190,7 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
169
190
|
|
|
170
191
|
if (!settings.hooks) settings.hooks = {};
|
|
171
192
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
193
|
+
if (!settings.hooks.SessionStop) settings.hooks.SessionStop = [];
|
|
172
194
|
|
|
173
195
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
174
196
|
h => !(h._aiflowKit)
|
|
@@ -178,13 +200,26 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
178
200
|
hooks: [
|
|
179
201
|
{
|
|
180
202
|
type: 'command',
|
|
181
|
-
command: `node "${
|
|
203
|
+
command: `node "${hookStartDest.replace(/\\/g, '/')}"`,
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
settings.hooks.SessionStop = settings.hooks.SessionStop.filter(
|
|
209
|
+
h => !(h._aiflowKit)
|
|
210
|
+
);
|
|
211
|
+
settings.hooks.SessionStop.push({
|
|
212
|
+
_aiflowKit: true,
|
|
213
|
+
hooks: [
|
|
214
|
+
{
|
|
215
|
+
type: 'command',
|
|
216
|
+
command: `node "${hookStopDest.replace(/\\/g, '/')}"`,
|
|
182
217
|
}
|
|
183
218
|
]
|
|
184
219
|
});
|
|
185
220
|
|
|
186
221
|
await fs.writeJson(settingsFile, settings, { spaces: 2 });
|
|
187
|
-
console.log(chalk.green(`✓ Superpowers SessionStart
|
|
222
|
+
console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionStop) configured`));
|
|
188
223
|
}
|
|
189
224
|
|
|
190
225
|
async function verifySuperpowersSkills(versionDir) {
|
|
@@ -273,24 +308,66 @@ async function setupFramework(projectDir, framework, multi = false, selectedTool
|
|
|
273
308
|
finalContent += workflowContent + '\n\n';
|
|
274
309
|
}
|
|
275
310
|
|
|
276
|
-
|
|
311
|
+
const markerStart = '<!-- aiflow-kit-start -->';
|
|
312
|
+
const markerEnd = '<!-- aiflow-kit-end -->';
|
|
313
|
+
|
|
314
|
+
finalContent = `${markerStart}\n${finalContent.trim()}\n${markerEnd}`;
|
|
277
315
|
|
|
278
316
|
const fileExists = await fs.pathExists(targetPath);
|
|
279
317
|
|
|
280
318
|
if (!multi) {
|
|
281
319
|
const relPath = path.relative(projectDir, targetPath).replace(/\\/g, '/');
|
|
282
320
|
if (fileExists) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
321
|
+
const existingContent = await fs.readFile(targetPath, 'utf-8');
|
|
322
|
+
const hasStart = existingContent.includes(markerStart);
|
|
323
|
+
const hasEnd = existingContent.includes(markerEnd);
|
|
324
|
+
|
|
325
|
+
if (hasStart && hasEnd) {
|
|
326
|
+
const doUpdate = await confirm({
|
|
327
|
+
message: `File ${relPath} already contains an AI Flow block. Do you want to update this block? (Your custom rules will not be affected)`,
|
|
328
|
+
default: true
|
|
329
|
+
});
|
|
330
|
+
if (doUpdate) {
|
|
331
|
+
const regex = new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`, 'g');
|
|
332
|
+
const updatedContent = existingContent.replace(regex, finalContent);
|
|
333
|
+
await fs.writeFile(targetPath, updatedContent);
|
|
334
|
+
console.log(chalk.green(` ✓ Updated AI Flow block in ${relPath}`));
|
|
335
|
+
written.push(AI_TOOL_FILES[tool]);
|
|
336
|
+
} else {
|
|
337
|
+
const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
|
|
338
|
+
await fs.ensureDir(path.dirname(refPath));
|
|
339
|
+
await fs.writeFile(refPath, finalContent);
|
|
340
|
+
const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
|
|
341
|
+
console.log(chalk.gray(` ✓ Skipped ${relPath} — aiflow template saved to ${refRel}`));
|
|
342
|
+
skipped.push(relPath);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
const doOverwrite = await confirm({
|
|
346
|
+
message: `File ${relPath} is missing the new AI Flow marker block. Do you want to OVERWRITE it completely with the latest template?`,
|
|
347
|
+
default: true
|
|
348
|
+
});
|
|
349
|
+
if (doOverwrite) {
|
|
350
|
+
await fs.writeFile(targetPath, finalContent);
|
|
351
|
+
console.log(chalk.green(` ✓ Overwrote ${relPath} with latest AI Flow template`));
|
|
352
|
+
written.push(AI_TOOL_FILES[tool]);
|
|
353
|
+
} else {
|
|
354
|
+
const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
|
|
355
|
+
await fs.ensureDir(path.dirname(refPath));
|
|
356
|
+
await fs.writeFile(refPath, finalContent);
|
|
357
|
+
const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
|
|
358
|
+
console.log(chalk.gray(` ✓ Skipped ${relPath} — aiflow template saved to ${refRel}`));
|
|
359
|
+
skipped.push(relPath);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
291
362
|
} else {
|
|
292
|
-
await
|
|
293
|
-
|
|
363
|
+
const doCreate = await confirm({
|
|
364
|
+
message: `File ${relPath} does not exist. Do you want to create it using the latest AI Flow template?`,
|
|
365
|
+
default: true
|
|
366
|
+
});
|
|
367
|
+
if (doCreate) {
|
|
368
|
+
await fs.writeFile(targetPath, finalContent);
|
|
369
|
+
written.push(AI_TOOL_FILES[tool]);
|
|
370
|
+
}
|
|
294
371
|
}
|
|
295
372
|
} else {
|
|
296
373
|
if (fileExists) {
|
|
@@ -494,9 +571,19 @@ function verifyFigma(credentials) {
|
|
|
494
571
|
});
|
|
495
572
|
}
|
|
496
573
|
|
|
497
|
-
async function
|
|
574
|
+
async function ensureAiflowGitignored(projectDir) {
|
|
498
575
|
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
499
|
-
const entries = [
|
|
576
|
+
const entries = [
|
|
577
|
+
'.aiflow/',
|
|
578
|
+
'plan/',
|
|
579
|
+
'.claude/',
|
|
580
|
+
'.rules/',
|
|
581
|
+
'.mcp.json',
|
|
582
|
+
'CLAUDE.md',
|
|
583
|
+
'GEMINI.md',
|
|
584
|
+
'.cursorrules',
|
|
585
|
+
'.github/copilot-instructions.md'
|
|
586
|
+
];
|
|
500
587
|
let content = '';
|
|
501
588
|
if (await fs.pathExists(gitignorePath)) {
|
|
502
589
|
content = await fs.readFile(gitignorePath, 'utf-8');
|
|
@@ -523,6 +610,190 @@ function maskSecret(value) {
|
|
|
523
610
|
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
524
611
|
}
|
|
525
612
|
|
|
613
|
+
// ── GitNexus (code intelligence) helpers ─────────────────────────
|
|
614
|
+
|
|
615
|
+
const IGNORE_DIRS = new Set([
|
|
616
|
+
'.git', 'node_modules', 'target', 'build', '.aiflow',
|
|
617
|
+
'dist', 'out', '.next', '.nuxt', 'vendor', 'coverage', '.gradle',
|
|
618
|
+
]);
|
|
619
|
+
const SOURCE_EXT = /\.(java|kt|groovy|js|ts|jsx|tsx|mjs|cjs|php|py|go|rb|cs|cpp|c|h|rs|vue|svelte|xml|yml|yaml|sql|properties)$/i;
|
|
620
|
+
async function countSourceFiles(dir) {
|
|
621
|
+
let count = 0;
|
|
622
|
+
const scan = async (d) => {
|
|
623
|
+
let entries;
|
|
624
|
+
try { entries = await fs.readdir(d, { withFileTypes: true }); } catch (_) { return; }
|
|
625
|
+
for (const e of entries) {
|
|
626
|
+
if (e.isDirectory()) {
|
|
627
|
+
if (!IGNORE_DIRS.has(e.name) && !e.name.startsWith('.')) await scan(path.join(d, e.name));
|
|
628
|
+
} else if (SOURCE_EXT.test(e.name)) {
|
|
629
|
+
count++;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
await scan(dir);
|
|
634
|
+
return count;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function isGitNexusConfigured(projectDir) {
|
|
638
|
+
const mcpPath = path.join(projectDir, '.mcp.json');
|
|
639
|
+
if (!(await fs.pathExists(mcpPath))) return false;
|
|
640
|
+
const mcp = await fs.readJson(mcpPath).catch(() => ({}));
|
|
641
|
+
return !!(mcp.mcpServers && mcp.mcpServers.gitnexus);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function setupGitNexus(projectDir, wait = false) {
|
|
645
|
+
const { spawn } = require('child_process');
|
|
646
|
+
// On Windows, use shell:true so npm scripts (.cmd) resolve correctly — avoids EINVAL
|
|
647
|
+
const isWin = process.platform === 'win32';
|
|
648
|
+
const npxCmd = 'npx';
|
|
649
|
+
const spawnOpts = (extra = {}) => ({ shell: isWin, windowsHide: true, ...extra });
|
|
650
|
+
|
|
651
|
+
// 1. Inject MCP server config into .mcp.json (merge, don't overwrite)
|
|
652
|
+
const mcpPath = path.join(projectDir, '.mcp.json');
|
|
653
|
+
let existing = {};
|
|
654
|
+
if (await fs.pathExists(mcpPath)) {
|
|
655
|
+
existing = await fs.readJson(mcpPath).catch(() => ({}));
|
|
656
|
+
}
|
|
657
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
658
|
+
const mcpAlreadyPresent = !!(existing.mcpServers.gitnexus);
|
|
659
|
+
|
|
660
|
+
// 3. Detect if gitnexus is already available in PATH (preferred over npx)
|
|
661
|
+
const hasGlobalGitNexus = commandExists('gitnexus');
|
|
662
|
+
if (hasGlobalGitNexus) {
|
|
663
|
+
existing.mcpServers.gitnexus = { command: 'gitnexus', args: ['mcp'] };
|
|
664
|
+
console.log(chalk.gray(' ✓ Global gitnexus found — using it for MCP and indexing.'));
|
|
665
|
+
} else {
|
|
666
|
+
existing.mcpServers.gitnexus = { command: 'npx', args: ['-y', 'gitnexus@latest', 'mcp'] };
|
|
667
|
+
}
|
|
668
|
+
await fs.writeJson(mcpPath, existing, { spaces: 2 });
|
|
669
|
+
if (!mcpAlreadyPresent) {
|
|
670
|
+
console.log(chalk.green('✓ GitNexus MCP server configured (.mcp.json)'));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
|
|
674
|
+
if (nodeMajor < 20) {
|
|
675
|
+
console.log(chalk.yellow(`⚠ GitNexus indexing skipped — requires Node.js >=20.0.0 (current: ${process.version}).`));
|
|
676
|
+
console.log(chalk.gray(' MCP config saved. After upgrading Node.js, run: npx gitnexus analyze'));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const aiflowDir = path.join(projectDir, '.aiflow');
|
|
681
|
+
await fs.ensureDir(aiflowDir);
|
|
682
|
+
const statusPath = path.join(aiflowDir, 'gitnexus-status.json');
|
|
683
|
+
const startedAt = new Date().toISOString();
|
|
684
|
+
await fs.writeJson(statusPath, { status: 'running', startedAt });
|
|
685
|
+
|
|
686
|
+
if (!wait) {
|
|
687
|
+
const workerPath = path.join(__dirname, 'gitnexus-worker.js');
|
|
688
|
+
const child = spawn(process.execPath, [workerPath, projectDir],
|
|
689
|
+
spawnOpts({ detached: true, stdio: 'ignore' }));
|
|
690
|
+
child.unref();
|
|
691
|
+
console.log(chalk.cyan('⟳ GitNexus: indexing codebase in background...'));
|
|
692
|
+
console.log(chalk.gray(' Notification will appear when you run `aiflow use <TICKET>` next time.'));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const fileCount = await countSourceFiles(projectDir);
|
|
697
|
+
console.log(chalk.cyan(`⟳ GitNexus: indexing ${fileCount} source files (this may take 10–30 min on first run)...`));
|
|
698
|
+
console.log(chalk.gray(' Remove --wait to index in background instead.'));
|
|
699
|
+
|
|
700
|
+
await new Promise((resolve) => {
|
|
701
|
+
const runIndexing = (withMoreRam = false) => {
|
|
702
|
+
const currentCmd = hasGlobalGitNexus ? 'gitnexus' : 'npx';
|
|
703
|
+
const currentArgs = hasGlobalGitNexus ? ['analyze'] : ['-y', 'gitnexus@latest', 'analyze'];
|
|
704
|
+
const env = { ...process.env };
|
|
705
|
+
if (withMoreRam) {
|
|
706
|
+
const ramSize = withMoreRam === '8g' ? '8192' : '4096';
|
|
707
|
+
env.NODE_OPTIONS = `--max-old-space-size=${ramSize}`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const child = spawn(currentCmd, currentArgs,
|
|
711
|
+
spawnOpts({ cwd: projectDir, stdio: ['ignore', 'inherit', 'inherit'], env }));
|
|
712
|
+
|
|
713
|
+
child.on('close', async (code) => {
|
|
714
|
+
const completedAt = new Date().toISOString();
|
|
715
|
+
if (code === 0) {
|
|
716
|
+
await fs.writeJson(statusPath, { status: 'done', startedAt, completedAt, notified: true });
|
|
717
|
+
console.log(chalk.green(`✓ GitNexus: indexed ${fileCount} files — code intelligence is ready.`));
|
|
718
|
+
resolve();
|
|
719
|
+
} else {
|
|
720
|
+
console.log(chalk.red(`✗ GitNexus: indexing failed (exit ${code}).`));
|
|
721
|
+
|
|
722
|
+
// Detection of Segmentation Fault / Crash (139 on Linux/Mac, 3221225477 on Windows)
|
|
723
|
+
const isCrash = (code === 139 || code === 3221225477);
|
|
724
|
+
if (isCrash) {
|
|
725
|
+
console.log(chalk.yellow(`\n⚠ Indexing crashed (Segmentation Fault) at ${withMoreRam ? (withMoreRam === '8g' ? '8GB' : '4GB') : 'default'} RAM.`));
|
|
726
|
+
|
|
727
|
+
const nextRam = !withMoreRam ? '4g' : (withMoreRam === '4g' ? '8g' : null);
|
|
728
|
+
if (nextRam) {
|
|
729
|
+
const retry = await confirm({
|
|
730
|
+
message: `Would you like to retry indexing with increased memory (${nextRam.toUpperCase()})?`,
|
|
731
|
+
default: true
|
|
732
|
+
});
|
|
733
|
+
if (retry) {
|
|
734
|
+
return runIndexing(nextRam); // Recursion, don't resolve yet
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
console.log(chalk.red('⚠ Already tried with 8GB RAM. Please check if there are any extremely large files or corrupted database.'));
|
|
738
|
+
console.log(chalk.gray(' Try: rm -rf .aiflow/ladybug.db (if exists) or check your .gitignore'));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// If npx failed and gitnexus isn't global, offer to install it global (only if not a crash or if retry was rejected)
|
|
743
|
+
if (!hasGlobalGitNexus && !isCrash) {
|
|
744
|
+
console.log(chalk.yellow('\nTip: npx failed. Installing gitnexus globally often fixes environment issues.'));
|
|
745
|
+
const install = await confirm({ message: 'Would you like me to try installing gitnexus globally for you?', default: true });
|
|
746
|
+
if (install) {
|
|
747
|
+
try {
|
|
748
|
+
console.log(chalk.blue('Running: npm install -g gitnexus ...'));
|
|
749
|
+
execSync('npm install -g gitnexus', { stdio: 'inherit' });
|
|
750
|
+
console.log(chalk.green('✓ GitNexus installed globally. Please run `aiflow init` again to retry indexing.'));
|
|
751
|
+
} catch (err) {
|
|
752
|
+
console.log(chalk.red('Failed to install globally. Please try manually: npm install -g gitnexus'));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
await fs.writeJson(statusPath, { status: 'error', startedAt, completedAt, exitCode: code, notified: true });
|
|
758
|
+
resolve();
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
child.on('error', async (err) => {
|
|
763
|
+
await fs.writeJson(statusPath, {
|
|
764
|
+
status: 'error', startedAt, completedAt: new Date().toISOString(),
|
|
765
|
+
exitCode: null, error: err.message, notified: true,
|
|
766
|
+
});
|
|
767
|
+
console.log(chalk.red(`✗ GitNexus: failed to start — ${err.message}`));
|
|
768
|
+
resolve();
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
runIndexing();
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function maybeSetupGitNexus(projectDir, withGitNexus, wait = false) {
|
|
777
|
+
if (!withGitNexus) return;
|
|
778
|
+
|
|
779
|
+
// Check if indexing has already completed successfully (not stale)
|
|
780
|
+
const STALE_MS = 3 * 60 * 60 * 1000;
|
|
781
|
+
const statusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
|
|
782
|
+
let indexDone = false;
|
|
783
|
+
try {
|
|
784
|
+
const gn = await fs.readJson(statusPath);
|
|
785
|
+
const age = gn.startedAt ? Date.now() - new Date(gn.startedAt).getTime() : Infinity;
|
|
786
|
+
const staleRunning = gn.status === 'running' && age > STALE_MS;
|
|
787
|
+
indexDone = gn.status === 'done' && !staleRunning;
|
|
788
|
+
} catch (_) {}
|
|
789
|
+
|
|
790
|
+
if ((await isGitNexusConfigured(projectDir)) && indexDone) {
|
|
791
|
+
console.log(chalk.gray(' GitNexus already configured and indexed — skipping.'));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
await setupGitNexus(projectDir, wait);
|
|
795
|
+
}
|
|
796
|
+
|
|
526
797
|
// ── RTK (token compression) helpers ──────────────────────────────
|
|
527
798
|
|
|
528
799
|
async function detectRtk() {
|
|
@@ -657,6 +928,12 @@ async function init(options) {
|
|
|
657
928
|
await maybeSetupRtk(projectDir, options.withRtk || false);
|
|
658
929
|
}
|
|
659
930
|
|
|
931
|
+
// ── Optional: GitNexus code intelligence ─────────────────────
|
|
932
|
+
await maybeSetupGitNexus(projectDir, options.withGitnexus || false, options.wait || false);
|
|
933
|
+
|
|
934
|
+
// ── Ensure all aiflow files are gitignored ───────────────────
|
|
935
|
+
await ensureAiflowGitignored(projectDir);
|
|
936
|
+
|
|
660
937
|
console.log(chalk.green('\n✨ Initialized AI Flow Kit successfully!'));
|
|
661
938
|
console.log(chalk.blue('\n' + '─'.repeat(62)));
|
|
662
939
|
console.log(chalk.bold.white(' Next steps:'));
|
|
@@ -682,3 +959,6 @@ module.exports = init;
|
|
|
682
959
|
module.exports.AI_TOOL_FILES = AI_TOOL_FILES;
|
|
683
960
|
module.exports.detectRtk = detectRtk;
|
|
684
961
|
module.exports.isRtkHookConfigured = isRtkHookConfigured;
|
|
962
|
+
module.exports.isGitNexusConfigured = isGitNexusConfigured;
|
|
963
|
+
module.exports.setupFramework = setupFramework;
|
|
964
|
+
module.exports.ensureAiflowGitignored = ensureAiflowGitignored;
|
package/scripts/prompt.js
CHANGED
|
@@ -96,7 +96,7 @@ AI must:
|
|
|
96
96
|
Only runs after Gate 2 APPROVED.
|
|
97
97
|
1. Complex feature (3+ files): **INVOKE:** \`superpowers:subagent-driven-development\`
|
|
98
98
|
2. **INVOKE:** \`superpowers:test-driven-development\` — write failing test first
|
|
99
|
-
3. Implement to make tests pass —
|
|
99
|
+
3. Implement to make tests pass — verify each small step
|
|
100
100
|
|
|
101
101
|
### GATE 4 — AI Self-Review (wait for APPROVED)
|
|
102
102
|
**INVOKE:** \`review-plan\` skill
|
|
@@ -174,7 +174,7 @@ AI must:
|
|
|
174
174
|
Only runs after Gate 2 APPROVED.
|
|
175
175
|
1. **INVOKE:** \`superpowers:test-driven-development\` — ensure tests cover CURRENT behavior
|
|
176
176
|
2. Refactor incrementally — keep tests green at each step
|
|
177
|
-
3. Commit
|
|
177
|
+
3. Commit after manual verification
|
|
178
178
|
|
|
179
179
|
### GATE 4 — AI Self-Review (wait for APPROVED)
|
|
180
180
|
**INVOKE:** \`review-plan\` skill
|
package/scripts/remove.js
CHANGED
|
@@ -89,6 +89,60 @@ async function removeFromProject() {
|
|
|
89
89
|
console.log(chalk.green('✓ Removed .rules/'));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Clean up AI Tool files (CLAUDE.md, .cursorrules, etc.)
|
|
93
|
+
const { AI_TOOL_FILES } = require('./init');
|
|
94
|
+
const markerStart = '<!-- aiflow-kit-start -->';
|
|
95
|
+
const markerEnd = '<!-- aiflow-kit-end -->';
|
|
96
|
+
|
|
97
|
+
for (const tool in AI_TOOL_FILES) {
|
|
98
|
+
const filePath = path.join(PROJECT_DIR, AI_TOOL_FILES[tool]);
|
|
99
|
+
if (!(await fs.pathExists(filePath))) continue;
|
|
100
|
+
|
|
101
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
102
|
+
let cleaned = false;
|
|
103
|
+
|
|
104
|
+
if (content.includes(markerStart) && content.includes(markerEnd)) {
|
|
105
|
+
// Precise removal using markers
|
|
106
|
+
const startIndex = content.indexOf(markerStart);
|
|
107
|
+
const endIndex = content.indexOf(markerEnd) + markerEnd.length;
|
|
108
|
+
content = content.slice(0, startIndex).trim() + '\n' + content.slice(endIndex).trim();
|
|
109
|
+
content = content.trim();
|
|
110
|
+
cleaned = true;
|
|
111
|
+
} else if (content.includes('## AI Skill Registry (Superpowers)')) {
|
|
112
|
+
// Fallback for legacy installs: remove sections by headers
|
|
113
|
+
console.log(chalk.yellow(` ⚠ Found legacy aiflow sections in ${AI_TOOL_FILES[tool]}`));
|
|
114
|
+
const ok = await confirm({ message: `Remove ai-flow-kit sections from ${AI_TOOL_FILES[tool]}?`, default: true });
|
|
115
|
+
if (ok) {
|
|
116
|
+
// Heuristic: remove from "## AI Skill Registry" or "# Claude AI System Instructions"
|
|
117
|
+
// This is simple and covers most cases. For complex cases, user should manual clean.
|
|
118
|
+
const headers = [
|
|
119
|
+
'# Claude AI System Instructions',
|
|
120
|
+
'## AI Skill Registry (Superpowers)',
|
|
121
|
+
'## MANDATORY: Strict Gate Workflow'
|
|
122
|
+
];
|
|
123
|
+
let minIndex = content.length;
|
|
124
|
+
for (const h of headers) {
|
|
125
|
+
const idx = content.indexOf(h);
|
|
126
|
+
if (idx !== -1 && idx < minIndex) minIndex = idx;
|
|
127
|
+
}
|
|
128
|
+
if (minIndex < content.length) {
|
|
129
|
+
content = content.slice(0, minIndex).trim();
|
|
130
|
+
cleaned = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (cleaned) {
|
|
136
|
+
if (content.length === 0) {
|
|
137
|
+
await fs.remove(filePath);
|
|
138
|
+
console.log(chalk.green(`✓ Deleted ${AI_TOOL_FILES[tool]} (it was aiflow-only)`));
|
|
139
|
+
} else {
|
|
140
|
+
await fs.writeFile(filePath, content);
|
|
141
|
+
console.log(chalk.green(`✓ Cleaned up ai-flow-kit instructions from ${AI_TOOL_FILES[tool]}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
92
146
|
console.log(chalk.green('\n✨ ai-flow-kit removed from project.'));
|
|
93
147
|
console.log(chalk.gray('To reinstall: aiflow init'));
|
|
94
148
|
}
|
package/scripts/task.js
CHANGED
|
@@ -22,6 +22,7 @@ module.exports = async function task(action, options = {}) {
|
|
|
22
22
|
if (action === 'resume' && options.taskId) return await resumeTask(options.taskId);
|
|
23
23
|
if (action === 'reset' && options.taskId) return await resetTask(options.taskId);
|
|
24
24
|
if (action === 'remove' && options.taskId) return await removeTask(options.taskId);
|
|
25
|
+
if (action === 'next') return await nextGate(options.taskId);
|
|
25
26
|
|
|
26
27
|
console.log(chalk.yellow('Usage:'));
|
|
27
28
|
console.log(chalk.gray(' aiflow task status — show active task and pending list'));
|
|
@@ -32,6 +33,8 @@ module.exports = async function task(action, options = {}) {
|
|
|
32
33
|
console.log(chalk.gray(' aiflow task resume <ticket-id> — resume a paused task'));
|
|
33
34
|
console.log(chalk.gray(' aiflow task reset <ticket-id> — reset task to Gate 1 (keeps context)'));
|
|
34
35
|
console.log(chalk.gray(' aiflow task remove <ticket-id> — permanently delete all task data'));
|
|
36
|
+
console.log(chalk.gray(' aiflow task next — approve current gate + prep next session'));
|
|
37
|
+
console.log(chalk.gray(' aiflow task next --ticket <id> — same, specify ticket explicitly'));
|
|
35
38
|
} catch (err) {
|
|
36
39
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
37
40
|
}
|
|
@@ -293,6 +296,75 @@ async function removeTask(taskId) {
|
|
|
293
296
|
console.log(chalk.green(`✓ Task ${taskId} removed.`));
|
|
294
297
|
}
|
|
295
298
|
|
|
299
|
+
async function nextGate(taskId) {
|
|
300
|
+
// Resolve ticket ID
|
|
301
|
+
let resolvedId = taskId;
|
|
302
|
+
if (!resolvedId) {
|
|
303
|
+
const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
|
|
304
|
+
resolvedId = ctx && ctx.taskId ? ctx.taskId : null;
|
|
305
|
+
}
|
|
306
|
+
if (!resolvedId) {
|
|
307
|
+
console.log(chalk.yellow('No ticket ID provided and no active task found.'));
|
|
308
|
+
console.log(chalk.gray(' Usage: aiflow task next --ticket PROJ-123'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const taskDir = path.join(TASKS_DIR, resolvedId);
|
|
313
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
314
|
+
await fs.ensureDir(taskDir);
|
|
315
|
+
|
|
316
|
+
// Load or create task state
|
|
317
|
+
const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
|
|
318
|
+
const currentGate = existing.currentGate || await detectCurrentGate(resolvedId);
|
|
319
|
+
const now = new Date().toISOString();
|
|
320
|
+
|
|
321
|
+
// Record gate approval
|
|
322
|
+
const gateApprovals = existing.gateApprovals || {};
|
|
323
|
+
gateApprovals[String(currentGate)] = now;
|
|
324
|
+
const nextGateNum = currentGate + 1;
|
|
325
|
+
|
|
326
|
+
// Save context snapshot if current task is active
|
|
327
|
+
if (await fs.pathExists(CURRENT_FILE)) {
|
|
328
|
+
const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
|
|
329
|
+
if (ctx && ctx.taskId === resolvedId) {
|
|
330
|
+
await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const taskState = {
|
|
335
|
+
...existing,
|
|
336
|
+
taskId: resolvedId,
|
|
337
|
+
status: 'pending',
|
|
338
|
+
updatedAt: now,
|
|
339
|
+
pausedAt: now,
|
|
340
|
+
currentGate: nextGateNum,
|
|
341
|
+
gateApprovals,
|
|
342
|
+
};
|
|
343
|
+
await fs.writeJson(statePath, taskState, { spaces: 2 });
|
|
344
|
+
|
|
345
|
+
// Clear current context so no task is "active"
|
|
346
|
+
await fs.remove(CURRENT_FILE);
|
|
347
|
+
|
|
348
|
+
// Generate cumulative task-summary.md
|
|
349
|
+
const planDir = path.join(PROJECT_DIR, 'plan', resolvedId);
|
|
350
|
+
await fs.ensureDir(planDir);
|
|
351
|
+
const summaryPath = path.join(planDir, 'task-summary.md');
|
|
352
|
+
const summaryContent = await generateMarkdownSummary(taskState);
|
|
353
|
+
await fs.writeFile(summaryPath, summaryContent, 'utf-8');
|
|
354
|
+
|
|
355
|
+
const gateLabels = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
|
|
356
|
+
console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
|
|
357
|
+
console.log(chalk.gray(` Summary saved to: plan/${resolvedId}/task-summary.md`));
|
|
358
|
+
console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${gateLabels[nextGateNum] || 'Done'}`));
|
|
359
|
+
console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
|
|
360
|
+
console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
|
|
361
|
+
console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
|
|
362
|
+
console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
|
|
363
|
+
console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in plan.md.`));
|
|
364
|
+
console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
|
|
365
|
+
console.log();
|
|
366
|
+
}
|
|
367
|
+
|
|
296
368
|
// ──────────────────────────────────────────────────────────────
|
|
297
369
|
// Internal helpers
|
|
298
370
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -313,6 +385,12 @@ async function loadAllTaskStates() {
|
|
|
313
385
|
}
|
|
314
386
|
|
|
315
387
|
async function detectCurrentGate(taskId) {
|
|
388
|
+
const statePath = path.join(TASKS_DIR, taskId, 'task-state.json');
|
|
389
|
+
if (await fs.pathExists(statePath)) {
|
|
390
|
+
const state = await fs.readJson(statePath).catch(() => ({}));
|
|
391
|
+
if (state.currentGate) return state.currentGate;
|
|
392
|
+
}
|
|
393
|
+
|
|
316
394
|
const planDir = path.join(PROJECT_DIR, 'plan', taskId);
|
|
317
395
|
if (!(await fs.pathExists(planDir))) return 1;
|
|
318
396
|
if (await fs.pathExists(path.join(planDir, 'summary.md'))) return 5;
|
|
@@ -321,6 +399,29 @@ async function detectCurrentGate(taskId) {
|
|
|
321
399
|
return 1;
|
|
322
400
|
}
|
|
323
401
|
|
|
402
|
+
async function generateMarkdownSummary(taskState) {
|
|
403
|
+
const lines = [];
|
|
404
|
+
lines.push(`# Task Summary: ${taskState.taskId}`);
|
|
405
|
+
lines.push(`**Title:** ${taskState.title}`);
|
|
406
|
+
lines.push(`**Status:** ${taskState.status}`);
|
|
407
|
+
lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
|
|
408
|
+
lines.push(`**Updated At:** ${new Date().toLocaleString()}`);
|
|
409
|
+
lines.push(``);
|
|
410
|
+
lines.push(`## Gate History`);
|
|
411
|
+
lines.push(`| Gate | Status | Approved At |`);
|
|
412
|
+
lines.push(`|------|--------|-------------|`);
|
|
413
|
+
for (let i = 1; i <= 5; i++) {
|
|
414
|
+
const approvedAt = taskState.gateApprovals && taskState.gateApprovals[String(i)];
|
|
415
|
+
const status = approvedAt ? '✅ Approved' : (i === taskState.currentGate ? '⏳ In Progress' : '⚪ Pending');
|
|
416
|
+
const timeStr = approvedAt ? new Date(approvedAt).toLocaleString() : '-';
|
|
417
|
+
lines.push(`| Gate ${i} (${gateLabel(i)}) | ${status} | ${timeStr} |`);
|
|
418
|
+
}
|
|
419
|
+
lines.push(``);
|
|
420
|
+
lines.push(`---`);
|
|
421
|
+
lines.push(`*Auto-generated by aiflow task next*`);
|
|
422
|
+
return lines.join('\n');
|
|
423
|
+
}
|
|
424
|
+
|
|
324
425
|
function gateLabel(n) {
|
|
325
426
|
const labels = {
|
|
326
427
|
1: 'AI Analyze Requirement',
|
package/scripts/update.js
CHANGED
|
@@ -29,12 +29,11 @@ module.exports = async function update(options = {}) {
|
|
|
29
29
|
const currentVersion = state.current_version;
|
|
30
30
|
|
|
31
31
|
if (currentVersion === PKG_VERSION && !options.force) {
|
|
32
|
-
console.log(chalk.
|
|
33
|
-
|
|
32
|
+
console.log(chalk.cyan(`You are already on v${PKG_VERSION}. Syncing latest assets and instruction files...`));
|
|
33
|
+
} else {
|
|
34
|
+
console.log(chalk.blue(`Updating from v${currentVersion} to v${PKG_VERSION}...`));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
console.log(chalk.blue(`Updating from v${currentVersion} to v${PKG_VERSION}...`));
|
|
37
|
-
|
|
38
37
|
const newVersionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
|
|
39
38
|
await fs.ensureDir(newVersionDir);
|
|
40
39
|
|
|
@@ -107,5 +106,16 @@ module.exports = async function update(options = {}) {
|
|
|
107
106
|
state.current_version = PKG_VERSION;
|
|
108
107
|
await fs.writeJson(stateFile, state);
|
|
109
108
|
|
|
109
|
+
// Sync instruction files with the updated skills
|
|
110
|
+
const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('./init');
|
|
111
|
+
const frameworks = state.frameworks || [];
|
|
112
|
+
const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
|
|
113
|
+
for (const fw of frameworks) {
|
|
114
|
+
await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Ensure all aiflow files are gitignored (especially for projects upgrading from older versions)
|
|
118
|
+
await ensureAiflowGitignored(projectDir);
|
|
119
|
+
|
|
110
120
|
console.log(chalk.green(`\n✨ Update completed! Your project is now on v${PKG_VERSION}.`));
|
|
111
121
|
};
|