@relipa/ai-flow-kit 0.0.6 → 0.0.7-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +33 -13
  2. package/bin/aiflow.js +58 -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/read-study-requirement/SKILL.md +11 -0
  7. package/custom/skills/review-plan/SKILL.md +15 -0
  8. package/docs/common/AIFLOW.md +12 -6
  9. package/docs/common/CHANGELOG.md +49 -5
  10. package/docs/common/QUICK_START.md +13 -11
  11. package/docs/common/cli-reference.md +23 -0
  12. package/package.json +2 -7
  13. package/scripts/checkpoint.js +46 -0
  14. package/scripts/doctor.js +111 -8
  15. package/scripts/gitnexus-worker.js +94 -0
  16. package/scripts/guide.js +42 -51
  17. package/scripts/hooks/session-start.js +35 -5
  18. package/scripts/hooks/session-stop.js +55 -0
  19. package/scripts/init.js +293 -18
  20. package/scripts/prompt.js +2 -2
  21. package/scripts/remove.js +54 -0
  22. package/scripts/task.js +62 -0
  23. package/scripts/update.js +14 -4
  24. package/scripts/use.js +41 -0
  25. package/upstream/.claude-plugin/marketplace.json +4 -4
  26. package/upstream/.claude-plugin/plugin.json +2 -4
  27. package/upstream/.cursor-plugin/plugin.json +2 -4
  28. package/upstream/docs/plans/2025-11-22-opencode-support-design.md +1 -1
  29. package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +2 -2
  30. package/upstream/docs/testing.md +2 -2
  31. package/upstream/skills/subagent-driven-development/SKILL.md +5 -6
  32. package/upstream/skills/subagent-driven-development/implementer-prompt.md +2 -3
  33. package/upstream/skills/systematic-debugging/CREATION-LOG.md +1 -1
  34. package/upstream/skills/systematic-debugging/root-cause-tracing.md +1 -1
  35. package/upstream/skills/tdd-lean/SKILL.md +127 -0
  36. package/upstream/skills/using-git-worktrees/SKILL.md +4 -5
  37. package/upstream/skills/writing-plans/SKILL.md +3 -9
  38. package/upstream/tests/brainstorm-server/package-lock.json +36 -0
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "@relipa/ai-flow-kit",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-beta.0",
4
4
  "description": "All-in-one AI Flow Kit for team development with Claude AI - skills, templates, and MCP adapters",
5
- "author": "Relipa AI Team",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://gitlab.relipa.vn/ai/ai-flow-kit.git"
9
- },
10
- "homepage": "https://gitlab.relipa.vn/ai/ai-flow-kit#readme",
5
+ "author": "Example Team",
11
6
  "publishConfig": {
12
7
  "access": "public",
13
8
  "registry": "https://registry.npmjs.com/"
@@ -0,0 +1,46 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ const PROJECT_DIR = process.cwd();
6
+ const AIFLOW_DIR = path.join(PROJECT_DIR, '.aiflow');
7
+ const CONTEXT_FILE = path.join(AIFLOW_DIR, 'context', 'current.json');
8
+
9
+ const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
10
+
11
+ module.exports = async function checkpoint(options = {}) {
12
+ const { gate, step, tokens, ticket: ticketOpt } = options;
13
+
14
+ if (!gate || !step || tokens === undefined) {
15
+ console.log(chalk.yellow('Usage: aiflow checkpoint --gate <N> --step <name> --tokens <N>'));
16
+ console.log(chalk.gray(' Example: aiflow checkpoint --gate 3 --step "tests-written" --tokens 3200'));
17
+ return;
18
+ }
19
+
20
+ // Resolve ticket ID
21
+ let ticketId = ticketOpt;
22
+ if (!ticketId) {
23
+ const ctx = await fs.readJson(CONTEXT_FILE).catch(() => null);
24
+ ticketId = ctx && ctx.taskId ? ctx.taskId : 'unknown';
25
+ }
26
+
27
+ const taskDir = path.join(AIFLOW_DIR, 'tasks', ticketId);
28
+ await fs.ensureDir(taskDir);
29
+
30
+ const checkpointPath = path.join(taskDir, 'checkpoints.json');
31
+ const existing = (await fs.pathExists(checkpointPath))
32
+ ? await fs.readJson(checkpointPath).catch(() => ({ checkpoints: [] }))
33
+ : { checkpoints: [] };
34
+
35
+ const entry = {
36
+ gate: parseInt(gate),
37
+ step,
38
+ tokens: parseInt(tokens),
39
+ timestamp: new Date().toISOString(),
40
+ };
41
+ existing.checkpoints.push(entry);
42
+ await fs.writeJson(checkpointPath, existing, { spaces: 2 });
43
+
44
+ const label = GATE_LABELS[parseInt(gate)] || `Gate ${gate}`;
45
+ console.log(chalk.green(`✓ Checkpoint: Gate ${gate} (${label}) — ${step} — ~${parseInt(tokens).toLocaleString()} tokens`));
46
+ };
package/scripts/doctor.js CHANGED
@@ -1,15 +1,71 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
- const { detectRtk, isRtkHookConfigured } = require('./init');
4
+ const { detectRtk, isRtkHookConfigured, isGitNexusConfigured } = require('./init');
5
5
 
6
- module.exports = async function doctor() {
6
+ function formatAgo(isoString) {
7
+ const ms = Date.now() - new Date(isoString).getTime();
8
+ const mins = Math.floor(ms / 60000);
9
+ if (mins < 1) return 'just now';
10
+ if (mins < 60) return `${mins}m ago`;
11
+ const hrs = Math.floor(mins / 60);
12
+ if (hrs < 24) return `${hrs}h ago`;
13
+ return `${Math.floor(hrs / 24)}d ago`;
14
+ }
15
+
16
+ async function showTokenBreakdown(ticketId) {
17
+ const tasksDir = path.join(process.cwd(), '.aiflow', 'tasks');
18
+ const checkpointsFile = path.join(tasksDir, ticketId, 'checkpoints.json');
19
+ if (!(await fs.pathExists(checkpointsFile))) return;
20
+
21
+ const data = await fs.readJson(checkpointsFile).catch(() => ({ checkpoints: [] }));
22
+ if (!data.checkpoints || data.checkpoints.length === 0) return;
23
+
24
+ // Aggregate tokens per gate (taking the max/latest)
25
+ const gateTokens = {};
26
+ for (const cp of data.checkpoints) {
27
+ if (!gateTokens[cp.gate] || cp.tokens > gateTokens[cp.gate]) {
28
+ gateTokens[cp.gate] = cp.tokens;
29
+ }
30
+ }
31
+
32
+ const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
33
+ const gates = Object.keys(gateTokens).sort();
34
+ const total = gates.reduce((sum, g) => sum + gateTokens[g], 0);
35
+
36
+ console.log('');
37
+ console.log(chalk.bold(`Token usage for task ${ticketId}:`));
38
+ for (const g of gates) {
39
+ const label = GATE_LABELS[parseInt(g)] || `Gate ${g}`;
40
+ const t = gateTokens[g];
41
+ const bar = '█'.repeat(Math.round((t / total) * 20)).padEnd(20, '░');
42
+ const pct = Math.round((t / total) * 100);
43
+ console.log(` Gate ${g} ${label.padEnd(8)} ~${t.toLocaleString().padStart(6)} ${chalk.gray(bar)} ${pct}%`);
44
+ }
45
+ console.log(` Total estimated: ~${total.toLocaleString()} tokens`);
46
+ }
47
+
48
+ module.exports = async function doctor(options = {}) {
7
49
  const projectDir = process.cwd();
8
50
  const errors = [];
9
51
  const warnings = [];
10
52
 
11
53
  console.log(chalk.blue('Running health check for AI Flow Kit...\n'));
12
54
 
55
+ // ── Token breakdown ────────────────────────────────────────
56
+ let ticketId = options.ticket;
57
+ if (!ticketId) {
58
+ const contextFile = path.join(projectDir, '.aiflow', 'context', 'current.json');
59
+ if (await fs.pathExists(contextFile)) {
60
+ const ctx = await fs.readJson(contextFile).catch(() => ({}));
61
+ ticketId = ctx.taskId;
62
+ }
63
+ }
64
+
65
+ if (ticketId) {
66
+ await showTokenBreakdown(ticketId);
67
+ }
68
+
13
69
  // ── Core setup ─────────────────────────────────────────────
14
70
  if (!(await fs.pathExists(path.join(projectDir, '.claude', 'skills')))) {
15
71
  errors.push('Missing `.claude/skills` directory. Did you run `aiflow init`?');
@@ -55,20 +111,67 @@ module.exports = async function doctor() {
55
111
  warnings.push('Missing .claude/settings.json — run `aiflow init`.');
56
112
  }
57
113
 
58
- // ── RTK token compression ──────────────────────────────────
114
+ // ── Token savings tools ────────────────────────────────────
59
115
  console.log('');
116
+ console.log(chalk.bold('Token savings:'));
117
+
118
+ // RTK
60
119
  const rtk = await detectRtk();
61
120
  if (rtk.installed) {
62
121
  const hookOk = await isRtkHookConfigured(projectDir);
63
122
  if (hookOk) {
64
- console.log(chalk.green(`✓ RTK installed (${rtk.version}) — token compression active`));
123
+ console.log(chalk.green(` ✓ RTK (${rtk.version}) — bash output compression active`));
124
+ console.log(chalk.gray(' Estimated: 60–90% reduction on bash command outputs (test runs, git, npm...)'));
65
125
  } else {
66
- console.log(chalk.green(`✓ RTK installed (${rtk.version})`));
67
- warnings.push('RTK is installed but hook not configured. Run `aiflow init` to enable token compression.');
126
+ console.log(chalk.yellow(` ⚠ RTK installed (${rtk.version}) but hook not configured`));
127
+ warnings.push('RTK hook not configured. Run `aiflow init` to enable token compression.');
128
+ }
129
+ } else {
130
+ console.log(chalk.gray(' ○ RTK not installed — bash output compression inactive'));
131
+ console.log(chalk.gray(' Install: cargo install rtk | Enable: aiflow init --with-rtk'));
132
+ }
133
+
134
+ // GitNexus
135
+ const gnConfigured = await isGitNexusConfigured(projectDir);
136
+ const gnStatusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
137
+ if (gnConfigured) {
138
+ let statusLine = chalk.green(' ✓ GitNexus MCP configured');
139
+ try {
140
+ if (await fs.pathExists(gnStatusPath)) {
141
+ const gn = await fs.readJson(gnStatusPath);
142
+ const GITNEXUS_STALE_MS = 3 * 60 * 60 * 1000; // 3 hours
143
+ if (gn.status === 'done') {
144
+ const ago = gn.completedAt
145
+ ? chalk.gray(` — indexed ${formatAgo(gn.completedAt)}`)
146
+ : '';
147
+ statusLine = chalk.green(` ✓ GitNexus MCP configured — index ready${ago}`);
148
+ console.log(statusLine);
149
+ console.log(chalk.gray(' Estimated: ~50% fewer tokens at Gate 1 (context queries vs file reads)'));
150
+ console.log(chalk.gray(' Estimated: ~70% fewer tokens at Gate 4 (impact() vs grep codebase)'));
151
+ } else if (gn.status === 'running') {
152
+ const age = Date.now() - new Date(gn.startedAt).getTime();
153
+ if (age > GITNEXUS_STALE_MS) {
154
+ console.log(chalk.red(' ✗ GitNexus MCP configured — indexing timed out (stale)'));
155
+ console.log(chalk.gray(` Started ${formatAgo(gn.startedAt)} but never finished. Process may have crashed.`));
156
+ // console.log(chalk.gray(' Retry: aiflow init --with-gitnexus --wait | Log: .aiflow/gitnexus-analyze.log'));
157
+ } else {
158
+ console.log(chalk.yellow(' ⟳ GitNexus MCP configured — indexing in progress...'));
159
+ console.log(chalk.gray(' Log: .aiflow/gitnexus-analyze.log'));
160
+ }
161
+ } else if (gn.status === 'error') {
162
+ console.log(chalk.red(' ✗ GitNexus MCP configured — last index failed'));
163
+ console.log(chalk.gray(' Retry: npx gitnexus analyze | Log: .aiflow/gitnexus-analyze.log'));
164
+ }
165
+ } else {
166
+ console.log(chalk.yellow(' ⚠ GitNexus MCP configured but not yet indexed'));
167
+ console.log(chalk.gray(' Run: npx gitnexus analyze'));
168
+ }
169
+ } catch (_) {
170
+ console.log(statusLine);
68
171
  }
69
172
  } else {
70
- console.log(chalk.gray('○ RTK not installed (optional) token compression inactive'));
71
- console.log(chalk.gray(' Install: cargo install rtk | More: https://github.com/rtk-ai/rtk'));
173
+ console.log(chalk.gray(' GitNexus not configuredcode intelligence inactive'));
174
+ // console.log(chalk.gray(' Enable: aiflow init --with-gitnexus'));
72
175
  }
73
176
 
74
177
  // ── Summary ────────────────────────────────────────────────
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Background worker: runs `npx gitnexus analyze` and writes result to
4
+ * .aiflow/gitnexus-status.json so session-start.js / use.js can notify the developer.
5
+ *
6
+ * Spawned as a detached process by init.js — never run directly.
7
+ * Usage: node gitnexus-worker.js <projectDir>
8
+ */
9
+
10
+ const { spawn, execSync } = require('child_process');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ function commandExists(cmd) {
15
+ try {
16
+ const isWin = process.platform === 'win32';
17
+ const checkCmd = isWin ? `where ${cmd}` : `which ${cmd}`;
18
+ execSync(checkCmd, { stdio: 'ignore' });
19
+ return true;
20
+ } catch (_) {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ const projectDir = process.argv[2];
26
+ if (!projectDir) process.exit(1);
27
+
28
+ const statusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
29
+ const logPath = path.join(projectDir, '.aiflow', 'gitnexus-analyze.log');
30
+
31
+ // Preserve startedAt written by init.js
32
+ let startedAt = new Date().toISOString();
33
+ try {
34
+ if (fs.existsSync(statusPath)) {
35
+ const existing = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
36
+ if (existing.startedAt) startedAt = existing.startedAt;
37
+ }
38
+ } catch (_) {}
39
+
40
+ // gitnexus requires Node >=20
41
+ const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
42
+ if (nodeMajor < 20) {
43
+ try {
44
+ fs.writeFileSync(statusPath, JSON.stringify({
45
+ status: 'error',
46
+ startedAt,
47
+ completedAt: new Date().toISOString(),
48
+ error: `Node.js version >=20 required (current: ${process.version})`,
49
+ }, null, 2));
50
+ } catch (_) {}
51
+ process.exit(0);
52
+ }
53
+
54
+ // Status path and log path already defined above
55
+
56
+ const logStream = fs.createWriteStream(logPath, { flags: 'w' });
57
+
58
+ // On Windows use shell:true — avoids EINVAL when spawning .cmd scripts
59
+ const isWin = process.platform === 'win32';
60
+
61
+ const hasGlobalGitNexus = commandExists('gitnexus');
62
+ const cmd = hasGlobalGitNexus ? 'gitnexus' : 'npx';
63
+ const args = hasGlobalGitNexus ? ['analyze'] : ['-y', 'gitnexus@latest', 'analyze'];
64
+
65
+ const child = spawn(cmd, args, {
66
+ cwd: projectDir,
67
+ shell: isWin,
68
+ windowsHide: true,
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ });
71
+
72
+ child.stdout.pipe(logStream);
73
+ child.stderr.pipe(logStream);
74
+
75
+ child.on('close', (code) => {
76
+ const status = code === 0
77
+ ? { status: 'done', startedAt, completedAt: new Date().toISOString() }
78
+ : { status: 'error', startedAt, completedAt: new Date().toISOString(), exitCode: code };
79
+ try {
80
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
81
+ } catch (_) {}
82
+ });
83
+
84
+ child.on('error', (err) => {
85
+ try {
86
+ fs.writeFileSync(statusPath, JSON.stringify({
87
+ status: 'error',
88
+ startedAt,
89
+ completedAt: new Date().toISOString(),
90
+ exitCode: null,
91
+ error: err.message,
92
+ }, null, 2));
93
+ } catch (_) {}
94
+ });
package/scripts/guide.js CHANGED
@@ -1,3 +1,5 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const chalk = require('chalk');
2
4
 
3
5
  // ──────────────────────────────────────────────────────────────
@@ -40,56 +42,42 @@ function showHeader() {
40
42
  // ──────────────────────────────────────────────────────────────
41
43
 
42
44
  function showFlow() {
43
- console.log(chalk.bold.cyan(' 📐 ARCHITECTURE & FLOW\n'));
45
+ console.log(chalk.bold.cyan(' ⛩️ WORKFLOW OVERVIEW (AIFLOW.md)\n'));
44
46
 
45
- console.log(chalk.gray(' ┌──────────────────────────────────────────────────────┐'));
46
- console.log(chalk.gray(' │') + chalk.bold.yellow(' TECH LEAD (One-time setup)') + chalk.gray(' │'));
47
- console.log(chalk.gray(' │') + chalk.white(' 1. Configure custom/rules/, custom/templates/') + chalk.gray(' │'));
48
- console.log(chalk.gray(' │') + chalk.white(' 2. npm publish → developer: npm install -g pkg') + chalk.gray(' │'));
49
- console.log(chalk.gray(' └─────────────────────────┬────────────────────────────┘'));
50
- console.log(chalk.gray(' │'));
51
- console.log(chalk.gray(' ┌─────────────▼──────────────┐'));
52
- console.log(chalk.gray(' │') + chalk.bold.green(' aiflow init') + chalk.gray(' │'));
53
- console.log(chalk.gray(' │') + chalk.white(' --framework spring-boot') + chalk.gray(''));
54
- console.log(chalk.gray(' │') + chalk.white(' --adapter backlog') + chalk.gray(' │'));
55
- console.log(chalk.gray(' └─────────────┬──────────────┘'));
56
- console.log(chalk.gray(' │ generates'));
57
- console.log(chalk.gray(' ┌─────────────▼──────────────────────────┐'));
58
- console.log(chalk.gray(' │') + chalk.white(' .claude/skills/ ← superpowers + custom') + chalk.gray(' │'));
59
- console.log(chalk.gray(' │') + chalk.white(' .claude/hooks/ ← SessionStart hook') + chalk.gray(' │'));
60
- console.log(chalk.gray(' │') + chalk.white(' .rules/ ← team coding rules') + chalk.gray(' │'));
61
- console.log(chalk.gray(' │') + chalk.white(' CLAUDE.md ← framework AI prompt') + chalk.gray(' │'));
62
- console.log(chalk.gray(' │') + chalk.white(' .mcp.json ← MCP adapter config') + chalk.gray(' │'));
63
- console.log(chalk.gray(' └─────────────┬──────────────────────────┘'));
64
- console.log(chalk.gray(''));
65
- console.log(chalk.gray(' ┌──────────────────▼───────────────────┐'));
66
- console.log(chalk.gray(' │') + chalk.bold.green(' Developer Workflow (per task)') + chalk.gray(' │'));
67
- console.log(chalk.gray(' │') + chalk.white(' │'));
68
- console.log(chalk.gray(' │') + chalk.cyan(' 1. aiflow use PROJ-33') + chalk.gray(' │'));
69
- console.log(chalk.gray(' │') + chalk.gray(' └─ pull title, desc, comments from │'));
70
- console.log(chalk.gray(' │') + chalk.gray(' Backlog/Jira → current.json │'));
71
- console.log(chalk.gray(' │') + chalk.white(' │'));
72
- console.log(chalk.gray(' │') + chalk.cyan(' 2. aiflow prompt bug-fix') + chalk.gray(' │'));
73
- console.log(chalk.gray(' │') + chalk.gray(' └─ generate AI prompt from context │'));
74
- console.log(chalk.gray(' │') + chalk.gray(' + team rules paste into Claude │'));
75
- console.log(chalk.gray(' │') + chalk.white(' │'));
76
- console.log(chalk.gray(' │') + chalk.cyan(' 3. claude (open Claude terminal)') + chalk.gray(' │'));
77
- console.log(chalk.gray(' │') + chalk.gray(' └─ "Fix bug PROJ-33" (1 line) │'));
78
- console.log(chalk.gray(' └──────────────────┬───────────────────┘'));
79
- console.log(chalk.gray(' │'));
80
- console.log(chalk.gray(' ┌──────────────────▼───────────────────┐'));
81
- console.log(chalk.gray(' │') + chalk.bold.magenta(' Claude AI (automatic)') + chalk.gray(' │'));
82
- console.log(chalk.gray(' │') + chalk.white(' │'));
83
- console.log(chalk.gray(' │') + chalk.gray(' SessionStart hook injected superpowers │'));
84
- console.log(chalk.gray(' │') + chalk.gray(' ↓ │'));
85
- console.log(chalk.gray(' │') + chalk.gray(' Read CLAUDE.md (framework rules) │'));
86
- console.log(chalk.gray(' │') + chalk.gray(' ↓ │'));
87
- console.log(chalk.gray(' │') + chalk.gray(' Read context/current.json (ticket) │'));
88
- console.log(chalk.gray(' │') + chalk.gray(' ↓ │'));
89
- console.log(chalk.gray(' │') + chalk.gray(' Auto-detect task type → select skill │'));
90
- console.log(chalk.gray(' │') + chalk.gray(' ↓ │'));
91
- console.log(chalk.gray(' │') + chalk.gray(' Implement with team rules │'));
92
- console.log(chalk.gray(' └────────────────────────────────────────┘'));
47
+ try {
48
+ const aiflowPath = path.join(__dirname, '../docs/common/AIFLOW.md');
49
+ if (!fs.existsSync(aiflowPath)) {
50
+ console.log(chalk.red(' Error: AIFLOW.md not found.'));
51
+ return;
52
+ }
53
+
54
+ const content = fs.readFileSync(aiflowPath, 'utf8');
55
+ const lines = content.split('\n');
56
+
57
+ let inSection = false;
58
+ let sectionLines = [];
59
+
60
+ for (const line of lines) {
61
+ if (line.trim() === '## Workflow Overview') {
62
+ inSection = true;
63
+ continue;
64
+ }
65
+ if (inSection) {
66
+ if (line.startsWith('## ') || line.trim() === '---') {
67
+ break;
68
+ }
69
+ sectionLines.push(line);
70
+ }
71
+ }
72
+
73
+ if (sectionLines.length > 0) {
74
+ console.log(chalk.white(sectionLines.join('\n')));
75
+ } else {
76
+ console.log(chalk.yellow(' (Workflow Overview section not found in AIFLOW.md)'));
77
+ }
78
+ } catch (error) {
79
+ console.log(chalk.red(' Error reading AIFLOW.md: ' + error.message));
80
+ }
93
81
  console.log();
94
82
  }
95
83
 
@@ -159,11 +147,14 @@ function showCommands() {
159
147
  items: [
160
148
  ['aiflow init', '--framework <fw> --adapter <ad>', 'Initialize project'],
161
149
  ['aiflow init', '--env <tools>', 'AI tools: cursor,gemini,copilot'],
162
- ['aiflow init', '--with-rtk | --no-rtk', 'Enable/disable RTK compression'],
163
- ['aiflow doctor', '', 'Check setup health'],
150
+ ['aiflow init', '--with-rtk | --no-rtk', 'RTK bash compression (60–90% saving)'],
151
+ // ['aiflow init', '--with-gitnexus', 'GitNexus code intelligence (waits for index)'],
152
+ // ['aiflow init', '--with-gitnexus --no-wait', 'GitNexus: index in background'],
153
+ ['aiflow doctor', '', 'Check setup health + token savings status'],
164
154
  ['aiflow doctor', '--verbose', 'Detailed health check output'],
165
155
  ['aiflow update', '', 'Update to the latest version'],
166
156
  ['aiflow update', '--force', 'Force update even if already latest'],
157
+ ['aiflow sync-skills', '', 'Manually sync AI Instruction files'],
167
158
  ['aiflow remove', '', 'Remove ai-flow-kit from project'],
168
159
  ['aiflow remove', '--version <ver>', 'Remove a cached version'],
169
160
  ['aiflow remove', '--global', 'Uninstall global package'],
@@ -41,7 +41,37 @@ try {
41
41
  // Context missing or invalid — continue without it
42
42
  }
43
43
 
44
- // ── 3. Combine and output ──────────────────────────────────────
44
+ // ── 3. Check GitNexus background indexing status ───────────────
45
+ const GITNEXUS_STALE_MS = 3 * 60 * 60 * 1000; // 3 hours
46
+ const gitNexusStatusPath = path.join(projectRoot, '.aiflow', 'gitnexus-status.json');
47
+ try {
48
+ if (fs.existsSync(gitNexusStatusPath)) {
49
+ const gn = JSON.parse(fs.readFileSync(gitNexusStatusPath, 'utf-8'));
50
+ if (!gn.notified) {
51
+ // Detect stale "running" — process was killed without updating status
52
+ if (gn.status === 'running' && gn.startedAt) {
53
+ const age = Date.now() - new Date(gn.startedAt).getTime();
54
+ if (age > GITNEXUS_STALE_MS) {
55
+ gn.status = 'error';
56
+ gn.error = 'timed out — process may have been killed or ran out of memory';
57
+ }
58
+ }
59
+ if (gn.status === 'done') {
60
+ process.stderr.write('[aiflow] ✓ GitNexus indexing complete — code intelligence tools are ready.\n');
61
+ } else if (gn.status === 'error') {
62
+ const detail = gn.error ? ` (${gn.error})` : gn.exitCode != null ? ` (exit ${gn.exitCode})` : '';
63
+ process.stderr.write(`[aiflow] ✗ GitNexus indexing failed${detail}.\n`);
64
+ // process.stderr.write('[aiflow] Retry: aiflow init --with-gitnexus\n');
65
+ }
66
+ if (gn.status === 'done' || gn.status === 'error') {
67
+ gn.notified = true;
68
+ fs.writeFileSync(gitNexusStatusPath, JSON.stringify(gn, null, 2));
69
+ }
70
+ }
71
+ }
72
+ } catch (_) {}
73
+
74
+ // ── 4. Combine and output ──────────────────────────────────────
45
75
  const parts = [];
46
76
 
47
77
  if (skillContent) {
@@ -222,12 +252,12 @@ function buildModeInstruction(mode, gate) {
222
252
  ],
223
253
  3: [
224
254
  '**MODE: fast** — Gate 3 fast track:',
255
+ '- INVOKE `tdd-lean` skill (NOT superpowers:test-driven-development)',
256
+ '- tdd-lean uses Batch Red-Green-Refactor: 2–3 test runs total, never per-test',
257
+ '- Test Output Discipline: keep only summary line after each run, discard stack traces',
225
258
  '- DO NOT use `superpowers:subagent-driven-development`',
226
259
  '- DO NOT spawn Spec Reviewer or Code Quality Reviewer subagents',
227
- '- Implement ALL tasks directly in this session',
228
- '- Write tests and implementation in the same pass per task',
229
- '- Run the test suite ONCE at the end of all tasks (not after each step)',
230
- '- If tests fail: fix inline, do not spawn a subagent',
260
+ '- Call `aiflow checkpoint` at each milestone (tests-written, code-done, tests-pass)',
231
261
  ],
232
262
  4: [
233
263
  '**MODE: fast** — Gate 4 fast track:',
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStop hook for ai-flow-kit
4
+ * Reads checkpoints.json for the active task and prints a token usage summary.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { record } = require('../telemetry/record');
10
+
11
+ record('session.stop');
12
+
13
+ const projectRoot = path.resolve(__dirname, '..', '..');
14
+ const contextFile = path.join(projectRoot, '.aiflow', 'context', 'current.json');
15
+ const tasksDir = path.join(projectRoot, '.aiflow', 'tasks');
16
+
17
+ const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
18
+
19
+ try {
20
+ if (!fs.existsSync(contextFile)) process.exit(0);
21
+ const ctx = JSON.parse(fs.readFileSync(contextFile, 'utf-8'));
22
+ const ticketId = ctx && ctx.taskId;
23
+ if (!ticketId) process.exit(0);
24
+
25
+ const checkpointPath = path.join(tasksDir, ticketId, 'checkpoints.json');
26
+ if (!fs.existsSync(checkpointPath)) process.exit(0);
27
+
28
+ const data = JSON.parse(fs.readFileSync(checkpointPath, 'utf-8'));
29
+ const checkpoints = data.checkpoints || [];
30
+ if (checkpoints.length === 0) process.exit(0);
31
+
32
+ // Aggregate latest token value per gate
33
+ const gateTokens = {};
34
+ for (const cp of checkpoints) {
35
+ const g = cp.gate;
36
+ if (!gateTokens[g] || cp.tokens > gateTokens[g]) {
37
+ gateTokens[g] = cp.tokens;
38
+ }
39
+ }
40
+
41
+ const gates = Object.keys(gateTokens).sort();
42
+ const total = gates.reduce((sum, g) => sum + gateTokens[g], 0);
43
+
44
+ process.stderr.write('\n[aiflow] Token usage this session:\n');
45
+ for (const g of gates) {
46
+ const label = GATE_LABELS[parseInt(g)] || `Gate ${g}`;
47
+ const t = gateTokens[g];
48
+ const bar = '█'.repeat(Math.round((t / total) * 20)).padEnd(20, '░');
49
+ const pct = Math.round((t / total) * 100);
50
+ process.stderr.write(`[aiflow] Gate ${g} ${label.padEnd(8)} ~${t.toLocaleString().padStart(6)} ${bar} ${pct}%\n`);
51
+ }
52
+ process.stderr.write(`[aiflow] Total estimated: ~${total.toLocaleString()} tokens\n\n`);
53
+ } catch (_) {}
54
+
55
+ process.exit(0);