@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.
Files changed (40) hide show
  1. package/README.md +41 -16
  2. package/bin/aiflow.js +65 -2
  3. package/custom/mcp-presets/gitnexus.json +8 -0
  4. package/custom/skills/generate-spec/SKILL.md +7 -0
  5. package/custom/skills/impact-analysis/SKILL.md +10 -0
  6. package/custom/skills/investigate-bug/SKILL.md +17 -0
  7. package/custom/skills/read-study-requirement/SKILL.md +11 -0
  8. package/custom/skills/review-plan/SKILL.md +15 -0
  9. package/custom/templates/php-plain.md +261 -0
  10. package/docs/common/AIFLOW.md +17 -6
  11. package/docs/common/CHANGELOG.md +63 -5
  12. package/docs/common/QUICK_START.md +33 -13
  13. package/docs/common/cli-reference.md +23 -0
  14. package/package.json +2 -7
  15. package/scripts/checkpoint.js +46 -0
  16. package/scripts/doctor.js +111 -8
  17. package/scripts/gitnexus-worker.js +94 -0
  18. package/scripts/guide.js +42 -51
  19. package/scripts/hooks/session-start.js +35 -5
  20. package/scripts/hooks/session-stop.js +55 -0
  21. package/scripts/init.js +299 -19
  22. package/scripts/prompt.js +2 -2
  23. package/scripts/remove.js +54 -0
  24. package/scripts/task.js +101 -0
  25. package/scripts/update.js +14 -4
  26. package/scripts/use.js +78 -6
  27. package/upstream/.claude-plugin/marketplace.json +4 -4
  28. package/upstream/.claude-plugin/plugin.json +2 -4
  29. package/upstream/.cursor-plugin/plugin.json +2 -4
  30. package/upstream/docs/plans/2025-11-22-opencode-support-design.md +1 -1
  31. package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +2 -2
  32. package/upstream/docs/testing.md +2 -2
  33. package/upstream/skills/subagent-driven-development/SKILL.md +5 -6
  34. package/upstream/skills/subagent-driven-development/implementer-prompt.md +2 -3
  35. package/upstream/skills/systematic-debugging/CREATION-LOG.md +1 -1
  36. package/upstream/skills/systematic-debugging/root-cause-tracing.md +1 -1
  37. package/upstream/skills/tdd-lean/SKILL.md +127 -0
  38. package/upstream/skills/using-git-worktrees/SKILL.md +4 -5
  39. package/upstream/skills/writing-plans/SKILL.md +3 -9
  40. 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 'nodejs-express';
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 hookSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-start.js');
161
- const hookDest = path.join(hooksDir, 'session-start.js');
162
- await fs.copy(hookSrc, hookDest, { overwrite: true });
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 "${hookDest.replace(/\\/g, '/')}"`,
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 hook configured`));
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
- finalContent += frameworkContent;
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
- // File already exists — save aiflow template to .aiflow/reference/ for comparison.
284
- // Developer owns the file in root; we never overwrite without explicit action.
285
- const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
286
- await fs.ensureDir(path.dirname(refPath));
287
- await fs.writeFile(refPath, finalContent);
288
- const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
289
- console.log(chalk.gray(` ${relPath} exists aiflow template saved to ${refRel}`));
290
- skipped.push(relPath);
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 fs.writeFile(targetPath, finalContent);
293
- written.push(AI_TOOL_FILES[tool]);
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 ensureCredentialsGitignored(projectDir) {
574
+ async function ensureAiflowGitignored(projectDir) {
498
575
  const gitignorePath = path.join(projectDir, '.gitignore');
499
- const entries = ['.aiflow/no-telemetry'];
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 — commit each small step
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 each step
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.green(`Already on v${PKG_VERSION}. Use --force to re-copy assets anyway.`));
33
- return;
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
  };