@relipa/ai-flow-kit 0.1.5 → 0.1.6

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.
@@ -0,0 +1,264 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { confirm } = require('@inquirer/prompts');
6
+ const { resolveDocsRepoPath, DOC_REPOS } = require('./docs-repo');
7
+
8
+ // Implements the branch + Merge Request half of docs/internal/Docs-Management-Flow.md:
9
+ // PM reviews & merges into `main`; every other role updates docs on a
10
+ // `feature/<functionId>/<taskId>` branch created from `main`, then submits a MR.
11
+ //
12
+ // Safety model (mirrors the rest of the kit's "AI drafts — human approves" rule,
13
+ // see scripts/hooks/block-git-write.js): AI must never call these with --yes until
14
+ // the developer has explicitly approved the plan in chat. Without --yes and without
15
+ // an interactive TTY, both commands only print the plan and make no changes.
16
+
17
+ const PROJECT_DIR = process.cwd();
18
+
19
+ function sh(cmd, cwd) {
20
+ return execSync(cmd, { cwd, encoding: 'utf-8' }).trim();
21
+ }
22
+
23
+ // Same as sh(), but swallows stderr — for existence checks expected to fail often/normally.
24
+ function shQuiet(cmd, cwd) {
25
+ return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
26
+ }
27
+
28
+ function isKnownRepoName(name) {
29
+ return DOC_REPOS.some((r) => r.name === name);
30
+ }
31
+
32
+ function resolveRepo(repoName) {
33
+ if (!isKnownRepoName(repoName)) {
34
+ console.log(chalk.red(`✗ Repo không hợp lệ: "${repoName}". Chỉ chấp nhận: ${DOC_REPOS.map((r) => r.name).join(', ')}`));
35
+ return null;
36
+ }
37
+ const repoPath = resolveDocsRepoPath(PROJECT_DIR, repoName);
38
+ if (!fs.pathExistsSync(repoPath) || !fs.pathExistsSync(path.join(repoPath, '.git'))) {
39
+ console.log(chalk.red(`✗ ${repoName}/ không tồn tại hoặc không phải git repo tại ${repoPath}.`));
40
+ console.log(chalk.gray(` Hãy clone repo này vào thư mục root trước.`));
41
+ return null;
42
+ }
43
+ return repoPath;
44
+ }
45
+
46
+ function branchName(functionId, taskId) {
47
+ return `feature/${functionId}/${taskId}`;
48
+ }
49
+
50
+ function currentBranch(repoPath) {
51
+ return sh('git rev-parse --abbrev-ref HEAD', repoPath);
52
+ }
53
+
54
+ function hasUncommittedChanges(repoPath) {
55
+ return sh('git status --porcelain', repoPath).length > 0;
56
+ }
57
+
58
+ function localBranchExists(repoPath, name) {
59
+ try {
60
+ shQuiet(`git rev-parse --verify refs/heads/${name}`, repoPath);
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function remoteBranchExists(repoPath, name) {
68
+ try {
69
+ return shQuiet(`git ls-remote --heads origin ${name}`, repoPath).length > 0;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // Prints the plan and gates on explicit approval — either an interactive "y"
76
+ // (developer running the command directly) or a pre-confirmed --yes flag
77
+ // (AI, only after the developer approved the same plan in chat).
78
+ async function ensureApproval(planLines, opts) {
79
+ console.log(chalk.yellow('\n📋 Kế hoạch thực hiện:'));
80
+ planLines.forEach((l) => console.log(chalk.gray(` • ${l}`)));
81
+
82
+ if (opts.yes) return true;
83
+
84
+ if (!process.stdin.isTTY) {
85
+ console.log(chalk.red('\n✗ Cần xác nhận trước khi thực hiện.'));
86
+ console.log(chalk.gray(' AI: hỏi user đồng ý trong chat trước, rồi chạy lại lệnh này kèm --yes.'));
87
+ console.log(chalk.gray(' Người dùng: chạy lại lệnh này trong terminal để được hỏi xác nhận trực tiếp.'));
88
+ return false;
89
+ }
90
+
91
+ return confirm({ message: '\nTiếp tục thực hiện các bước trên?', default: false });
92
+ }
93
+
94
+ async function createBranch(options) {
95
+ const repoName = options.repo || 'AK-Docs';
96
+ const repoPath = resolveRepo(repoName);
97
+ if (!repoPath) { process.exitCode = 1; return null; }
98
+
99
+ if (!options.functionId || !options.taskId) {
100
+ console.log(chalk.red('✗ Cần cả --function-id và --task-id (hoặc tham số vị trí <functionId> <taskId>).'));
101
+ process.exitCode = 1;
102
+ return null;
103
+ }
104
+
105
+ const base = options.base || 'main';
106
+ const branch = branchName(options.functionId, options.taskId);
107
+
108
+ if (localBranchExists(repoPath, branch)) {
109
+ console.log(chalk.green(`✓ Branch ${branch} đã tồn tại trong ${repoName} — checkout.`));
110
+ execSync(`git checkout ${branch}`, { cwd: repoPath, stdio: 'inherit' });
111
+ return { repoPath, repoName, branch, created: false };
112
+ }
113
+
114
+ if (hasUncommittedChanges(repoPath)) {
115
+ console.log(chalk.red(`✗ ${repoName} đang có thay đổi chưa commit — xử lý (commit/stash) trước khi tạo branch mới.`));
116
+ process.exitCode = 1;
117
+ return null;
118
+ }
119
+
120
+ const ok = await ensureApproval([
121
+ `Repo: ${repoName}`,
122
+ `git checkout ${base} && git pull origin ${base} --ff-only`,
123
+ `git checkout -b ${branch}`,
124
+ `git push -u origin ${branch}`,
125
+ ], options);
126
+ if (!ok) { process.exitCode = 1; return null; }
127
+
128
+ try {
129
+ console.log(chalk.cyan(`\n⟳ Cập nhật ${base} mới nhất...`));
130
+ execSync(`git checkout ${base}`, { cwd: repoPath, stdio: 'inherit' });
131
+ execSync(`git pull origin ${base} --ff-only`, { cwd: repoPath, stdio: 'inherit' });
132
+
133
+ console.log(chalk.cyan(`⟳ Tạo branch ${branch}...`));
134
+ execSync(`git checkout -b ${branch}`, { cwd: repoPath, stdio: 'inherit' });
135
+ execSync(`git push -u origin ${branch}`, { cwd: repoPath, stdio: 'inherit' });
136
+
137
+ console.log(chalk.green(`\n✓ Đã tạo và đẩy branch ${branch} từ ${base} (repo ${repoName}).`));
138
+ return { repoPath, repoName, branch, created: true };
139
+ } catch (err) {
140
+ console.log(chalk.red(`\n✗ Lỗi khi tạo branch: ${err.message}`));
141
+ process.exitCode = 1;
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function detectProvider(repoPath) {
147
+ let remoteUrl = '';
148
+ try { remoteUrl = sh('git remote get-url origin', repoPath); } catch { /* no remote */ }
149
+ if (/gitlab/i.test(remoteUrl)) return { provider: 'gitlab', remoteUrl };
150
+ if (/github/i.test(remoteUrl)) return { provider: 'github', remoteUrl };
151
+ return { provider: 'unknown', remoteUrl };
152
+ }
153
+
154
+ function hasCli(bin) {
155
+ try {
156
+ execSync(`${bin} --version`, { stdio: 'ignore' });
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ function webUrlFromRemote(remoteUrl) {
164
+ const sshMatch = remoteUrl.match(/^git@([^:]+):(.+?)(\.git)?$/);
165
+ if (sshMatch) return `https://${sshMatch[1]}/${sshMatch[2]}`;
166
+ return remoteUrl.replace(/\.git$/, '');
167
+ }
168
+
169
+ function openMergeRequest(repoPath, { branch, base, title, description }) {
170
+ const { provider, remoteUrl } = detectProvider(repoPath);
171
+
172
+ if (provider === 'gitlab' && hasCli('glab')) {
173
+ execSync(
174
+ `glab mr create --source-branch "${branch}" --target-branch "${base}" ` +
175
+ `--title ${JSON.stringify(title)} --description ${JSON.stringify(description || '')} --yes`,
176
+ { cwd: repoPath, stdio: 'inherit' }
177
+ );
178
+ return;
179
+ }
180
+ if (provider === 'github' && hasCli('gh')) {
181
+ execSync(
182
+ `gh pr create --base "${base}" --head "${branch}" ` +
183
+ `--title ${JSON.stringify(title)} --body ${JSON.stringify(description || '')}`,
184
+ { cwd: repoPath, stdio: 'inherit' }
185
+ );
186
+ return;
187
+ }
188
+
189
+ if (!remoteUrl) {
190
+ console.log(chalk.yellow('\n⚠ Không tìm thấy remote "origin" — hãy tự tạo Merge Request thủ công.'));
191
+ return;
192
+ }
193
+
194
+ const webBase = webUrlFromRemote(remoteUrl);
195
+ let url;
196
+ if (provider === 'gitlab') {
197
+ url = `${webBase}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${encodeURIComponent(branch)}` +
198
+ `&merge_request%5Btarget_branch%5D=${encodeURIComponent(base)}` +
199
+ `&merge_request%5Btitle%5D=${encodeURIComponent(title)}`;
200
+ } else if (provider === 'github') {
201
+ url = `${webBase}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1&title=${encodeURIComponent(title)}`;
202
+ } else {
203
+ url = webBase;
204
+ }
205
+ console.log(chalk.yellow(`\n⚠ Không tìm thấy 'glab'/'gh' CLI — mở link sau để tạo Merge Request thủ công:`));
206
+ console.log(chalk.cyan(` ${url}\n`));
207
+ }
208
+
209
+ async function submitDocs(options) {
210
+ const repoName = options.repo || 'AK-Docs';
211
+ const repoPath = resolveRepo(repoName);
212
+ if (!repoPath) { process.exitCode = 1; return null; }
213
+
214
+ if (!options.title) {
215
+ console.log(chalk.red('✗ Cần --title (tiêu đề commit / Merge Request).'));
216
+ process.exitCode = 1;
217
+ return null;
218
+ }
219
+
220
+ const base = options.base || 'main';
221
+ const branch = currentBranch(repoPath);
222
+ if (branch === base) {
223
+ console.log(chalk.red(`✗ Đang ở nhánh ${base} — tạo branch feature trước bằng "ak docs branch <functionId> <taskId>".`));
224
+ process.exitCode = 1;
225
+ return null;
226
+ }
227
+
228
+ const dirty = hasUncommittedChanges(repoPath);
229
+ if (!dirty && !remoteBranchExists(repoPath, branch)) {
230
+ console.log(chalk.yellow(`Không có thay đổi để submit và branch ${branch} chưa từng được push.`));
231
+ process.exitCode = 1;
232
+ return null;
233
+ }
234
+ if (!dirty) {
235
+ console.log(chalk.yellow('Không có thay đổi mới để commit — sẽ chỉ đảm bảo branch đã push và mở Merge Request.'));
236
+ }
237
+
238
+ const planLines = [`Repo: ${repoName}, branch: ${branch} → ${base}`];
239
+ if (dirty) planLines.push('git add -A', `git commit -m ${JSON.stringify(options.title)}`);
240
+ planLines.push(`git push -u origin ${branch}`, `Mở Merge Request: "${options.title}" (${branch} → ${base})`);
241
+
242
+ const ok = await ensureApproval(planLines, options);
243
+ if (!ok) { process.exitCode = 1; return null; }
244
+
245
+ try {
246
+ if (dirty) {
247
+ execSync('git add -A', { cwd: repoPath, stdio: 'inherit' });
248
+ execSync(`git commit -m ${JSON.stringify(options.title)}`, { cwd: repoPath, stdio: 'inherit' });
249
+ }
250
+ execSync(`git push -u origin ${branch}`, { cwd: repoPath, stdio: 'inherit' });
251
+
252
+ console.log(chalk.cyan('\n⟳ Mở Merge Request...'));
253
+ openMergeRequest(repoPath, { branch, base, title: options.title, description: options.description || '' });
254
+
255
+ console.log(chalk.green(`\n✓ Đã submit ${branch} → ${base} (repo ${repoName}). Chờ PM review.`));
256
+ return { repoPath, repoName, branch };
257
+ } catch (err) {
258
+ console.log(chalk.red(`\n✗ Lỗi khi submit: ${err.message}`));
259
+ process.exitCode = 1;
260
+ return null;
261
+ }
262
+ }
263
+
264
+ module.exports = { createBranch, submitDocs, branchName, currentBranch, hasUncommittedChanges };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse hook: throttle mcp__figma__* calls to stay under Figma's Tier-1 REST
4
+ * budget (10/min on a Starter-plan Full seat). Sleeps instead of blocking, so the
5
+ * tool call always proceeds — just never faster than the budget allows.
6
+ *
7
+ * Budget is counted in "units" ≈ underlying Figma REST calls
8
+ * (verified against figma-developer-mcp v0.13.2):
9
+ * get_figma_data → 1 unit (one /v1/files/:key[/nodes] call)
10
+ * download_figma_images → 3 units (fills map + PNG render + SVG render, worst case)
11
+ * Override with FIGMA_RATE_LIMIT_UNITS (default 6/60s — safety margin under 10/min).
12
+ */
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+
19
+ const WINDOW = 60; // seconds
20
+ const MAX_UNITS = parseInt(process.env.FIGMA_RATE_LIMIT_UNITS || '6', 10) || 6;
21
+ const STATE = path.join(
22
+ os.tmpdir(),
23
+ `figma-mcp-rate-limit-${typeof process.getuid === 'function' ? process.getuid() : 'user'}.log`
24
+ );
25
+
26
+ function readStdin() {
27
+ return new Promise((resolve) => {
28
+ let data = '';
29
+ process.stdin.setEncoding('utf8');
30
+ process.stdin.on('data', (c) => (data += c));
31
+ process.stdin.on('end', () => resolve(data));
32
+ process.stdin.on('error', () => resolve(data));
33
+ });
34
+ }
35
+
36
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
37
+
38
+ function readStamps() {
39
+ try {
40
+ return fs
41
+ .readFileSync(STATE, 'utf8')
42
+ .split('\n')
43
+ .map((l) => parseInt(l, 10))
44
+ .filter((n) => Number.isFinite(n))
45
+ .sort((a, b) => a - b);
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ (async () => {
52
+ let tool = '';
53
+ try {
54
+ tool = JSON.parse(await readStdin()).tool_name || '';
55
+ } catch {
56
+ /* no/invalid stdin — treat as 1 unit */
57
+ }
58
+ const units = tool.includes('download_figma_images') ? 3 : 1;
59
+
60
+ let slept = 0;
61
+ for (;;) {
62
+ const now = Math.floor(Date.now() / 1000);
63
+ const recent = readStamps().filter((t) => t > now - WINDOW);
64
+ // recent.length === 0 escape hatch: a single oversized call must never deadlock
65
+ if (recent.length + units <= MAX_UNITS || recent.length === 0) {
66
+ for (let i = 0; i < units; i++) recent.push(now);
67
+ fs.writeFileSync(STATE, recent.join('\n') + '\n');
68
+ break;
69
+ }
70
+ const wait = Math.max(1, recent[0] + WINDOW - now + 1);
71
+ await sleep(wait * 1000);
72
+ slept += wait;
73
+ }
74
+
75
+ if (slept > 0) {
76
+ console.log(
77
+ JSON.stringify({
78
+ systemMessage: `Figma rate-limit guard: delayed ${slept}s to stay under ${MAX_UNITS} calls/min`,
79
+ })
80
+ );
81
+ }
82
+ process.exit(0);
83
+ })();
@@ -210,16 +210,27 @@ function buildContextPrompt(ctx, taskState) {
210
210
  }
211
211
  lines.push(`- Gate ${currentGate}: 🔄 IN PROGRESS`);
212
212
  lines.push('');
213
- lines.push(`**DO NOT restart from Gate 1.** Resume directly at Gate ${currentGate} (${gateLabel(currentGate)}).`);
213
+ const isGenDoc = ctx.taskType === 'gen-doc';
214
+ lines.push(`**DO NOT restart from Gate 1.** Resume directly at Gate ${currentGate} (${gateLabel(currentGate, ctx.taskType)}).`);
214
215
  lines.push('');
215
216
 
216
- if (currentGate === 2) {
217
+ if (isGenDoc && currentGate >= 3) {
218
+ lines.push(`This is a **gen-doc** task (2-gate flow). The document workflow is complete.`);
219
+ lines.push(`Expected output: \`plan/${ctx.taskId}/output.md\` (or path in requirement.md).`);
220
+ lines.push('DO NOT start Gate 3, DO NOT create plan.md, DO NOT run TDD.');
221
+ } else if (currentGate === 2) {
217
222
  lines.push('Gate 1 is already APPROVED. The requirement document is at:');
218
223
  lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
219
224
  lines.push(` ([functionId] — look it up in AK-Docs/04.Coding/00.Overview/_Index.md or the folder containing ${ctx.taskId}.md)`);
220
225
  lines.push('');
221
- lines.push('Proceed to Gate 2: create the implementation plan.');
222
- lines.push(buildModeInstruction(mode, 2));
226
+ if (isGenDoc) {
227
+ lines.push('This is a **gen-doc** task. DO NOT create plan.md or run TDD.');
228
+ lines.push('Proceed to Gate 2: read the approved requirement.md and generate the output document immediately.');
229
+ lines.push('After generating, self-review + create task-summary.md, then run `ak gate 2 approved`.');
230
+ } else {
231
+ lines.push('Proceed to Gate 2: create the implementation plan.');
232
+ }
233
+ lines.push(buildModeInstruction(mode, 2, ctx.taskType));
223
234
  } else if (currentGate === 3) {
224
235
  lines.push('Gates 1 and 2 are already APPROVED. Plans are at:');
225
236
  lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
@@ -265,6 +276,14 @@ function buildContextPrompt(ctx, taskState) {
265
276
  }
266
277
 
267
278
  lines.push('');
279
+ if (ctx.taskType === 'gen-doc') {
280
+ lines.push('**This is a gen-doc task (2-gate flow):**');
281
+ lines.push('- Gate 1: Create requirement.md → wait for APPROVED');
282
+ lines.push('- Gate 2 (after APPROVED): Generate output document immediately + self-review + task-summary.md');
283
+ lines.push('- NO plan.md, NO TDD, NO Gate 3/4/5');
284
+ lines.push('');
285
+ }
286
+
268
287
  lines.push('DO NOT wait for the developer to ask. START NOW.');
269
288
  lines.push('If you have not started automatically, begin as soon as the developer types **"start"**.');
270
289
  lines.push('(If a later ticket switch does not seem to trigger this — e.g. `ak use` was run in another terminal while this chat was already open — the developer can run **`/coding`** to load it manually.)');
@@ -274,7 +293,11 @@ function buildContextPrompt(ctx, taskState) {
274
293
  return lines.join('\n');
275
294
  }
276
295
 
277
- function gateLabel(n) {
296
+ function gateLabel(n, taskType = '') {
297
+ if (taskType === 'gen-doc') {
298
+ if (n === 2) return 'Generate Document';
299
+ if (n >= 3) return 'Done';
300
+ }
278
301
  const labels = {
279
302
  1: 'AI Analyze Requirement',
280
303
  2: 'Implementation Plan',
@@ -285,9 +308,21 @@ function gateLabel(n) {
285
308
  return labels[n] || `Gate ${n}`;
286
309
  }
287
310
 
288
- function buildModeInstruction(mode, gate) {
311
+ function buildModeInstruction(mode, gate, taskType = '') {
289
312
  if (mode !== 'fast') return '';
290
313
 
314
+ if (gate === 2 && taskType === 'gen-doc') {
315
+ return [
316
+ '',
317
+ '**MODE: fast — Gate 2 (gen-doc):**',
318
+ '- Re-read `AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md` → generate output document immediately',
319
+ '- Save to `AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId].md` (or the custom path/format noted in the requirement doc)',
320
+ '- Self-review + create `AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId]-summary.md`',
321
+ '- Submit AK-Docs via MR (show title/description, wait for explicit confirmation, then `ak docs submit --title "..." --description "..." --yes`)',
322
+ '- Run `ak gate 2 approved` immediately — NO extra approval prompt for this step',
323
+ ].join('\n');
324
+ }
325
+
291
326
  const instructions = {
292
327
  2: [
293
328
  '**MODE: fast** — Gate 2 fast track:',
package/scripts/init.js CHANGED
@@ -214,10 +214,20 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
214
214
  const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
215
215
  await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
216
216
 
217
+ const hookFigmaRateSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'figma-rate-limit.js');
218
+ const hookFigmaRateDest = path.join(hooksDir, 'figma-rate-limit.js');
219
+ await fs.copy(hookFigmaRateSrc, hookFigmaRateDest, { overwrite: true });
220
+
221
+ // Hooks are CommonJS (.js + require). In an ESM project ("type": "module" in the
222
+ // project's package.json) Node would treat them as ES modules and crash on require().
223
+ // A nested package.json pins the interpretation for the kit-owned dirs only.
224
+ await fs.writeJson(path.join(hooksDir, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
225
+
217
226
  // Copy telemetry module so hooks can require('../telemetry/record')
218
227
  const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
219
228
  const telemetryDest = path.join(claudeDir, 'telemetry');
220
229
  await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
230
+ await fs.writeJson(path.join(telemetryDest, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
221
231
 
222
232
  const settingsFile = path.join(claudeDir, 'settings.json');
223
233
  let settings = {};
@@ -273,8 +283,26 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
273
283
  ]
274
284
  });
275
285
 
286
+ // Throttle Figma MCP calls to stay under Figma's Tier-1 REST budget (anti-429).
287
+ // Sleeps when the 60s window is full — may hold a call up to ~60s, hence timeout 120.
288
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
289
+ h => !(h._aiflowKitFigmaRateLimit)
290
+ );
291
+ settings.hooks.PreToolUse.push({
292
+ _aiflowKitFigmaRateLimit: true,
293
+ matcher: 'mcp__figma__.*',
294
+ hooks: [
295
+ {
296
+ type: 'command',
297
+ command: `node "${hookFigmaRateDest.replace(/\\/g, '/')}"`,
298
+ timeout: 120,
299
+ statusMessage: 'Figma rate-limit guard: checking call budget…',
300
+ }
301
+ ]
302
+ });
303
+
276
304
  await fs.writeJson(settingsFile, settings, { spaces: 2 });
277
- console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write) configured`));
305
+ console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write, PreToolUse:figma-rate-limit) configured`));
278
306
  }
279
307
 
280
308
  async function verifySuperpowersSkills(versionDir) {
package/scripts/prompt.js CHANGED
@@ -281,6 +281,51 @@ Write documentation based on approved outline.
281
281
  **INVOKE:** \`superpowers:verification-before-completion\`
282
282
  Verify docs match code. No outdated examples.
283
283
  Display: "GATE 3: Documentation complete. Type APPROVED."
284
+ `,
285
+ },
286
+ 'gen-doc': {
287
+ header: 'Generate Document',
288
+ instruction: 'Generate a document from source code / feature analysis as described below.',
289
+ skillWorkflow: `
290
+ ## STRICT GATE WORKFLOW — Generate Document (2-Gate)
291
+
292
+ > ⚡ gen-doc uses a **2-gate flow**: Gate 1 (Requirement) → APPROVED → Gate 2 (Generate + Auto Review).
293
+ > Gate 2 runs immediately after APPROVED — no plan.md, no TDD, no extra coding gates.
294
+
295
+ > gen-doc reuses the same \`AK-Docs/04.Coding/\` folders as the standard Dev workflow (see \`custom/rules/project-conventions.md\`) — only Gate 1–2, no separate section.
296
+
297
+ ### GATE 1 — AI Analyze & Plan Document
298
+ **INVOKE:** \`read-study-requirement\` skill
299
+ 1. Load ticket context + read source code / files to understand scope
300
+ 2. Confirm \`functionId\` (BẮT BUỘC — see "functionId & ticketId" in \`project-conventions.md\`; if it can't be derived for an ad-hoc task, ask directly) and ensure AK-Docs is on branch \`feature/[functionId]/[ticketId]\` (create via \`ak docs branch [functionId] [ticketId] --yes\` only after explicit user confirmation)
301
+ 3. Understand the document goal, target audience, and output format
302
+ 4. If unclear → ask ONE question at a time, wait for reply
303
+ 5. Output \`AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md\` with:
304
+ - Document scope and target audience
305
+ - Content outline (sections to cover)
306
+ - Source references (files, features, flows to analyze)
307
+ - Output format: Markdown or Excel (note template path if applicable)
308
+ - Effort estimate
309
+ 6. Display "GATE 1: Document plan ready" → wait for **APPROVED**
310
+
311
+ > **Telemetry:** Run \`ak gate 1 start --ticket [ticket-id]\` when starting.
312
+ > Run \`ak gate 1 approved --ticket [ticket-id]\` immediately when APPROVED is received.
313
+
314
+ ### GATE 2 — Generate Document + Auto Review (runs immediately after APPROVED)
315
+
316
+ **DO NOT create plan.md. DO NOT run TDD. Proceed directly to document generation.**
317
+
318
+ 1. Re-read \`AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md\` (the approved outline)
319
+ 2. Read source code, trace data flows, analyze features as needed
320
+ 3. Generate the output document:
321
+ - **Markdown:** Save to \`AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId].md\` (or the custom path/format noted in the requirement doc — still write a short pointer + summary into this file)
322
+ 4. Self-review: verify content completeness against the approved requirement outline
323
+ 5. Create \`AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId]-summary.md\` with a brief summary of what was generated
324
+ 6. Submit AK-Docs via Merge Request: show the commit/MR title+description, wait for explicit user confirmation, then run \`ak docs submit --title "..." --description "..." --yes\` — PM reviews & merges into \`main\` (never merge it yourself)
325
+ 7. Run: \`ak gate 2 approved --ticket [ticket-id]\` to close the task
326
+ 8. Display: \`GATE 2 DONE: Document generated at [path]\`
327
+
328
+ **Do NOT wait for extra approval before generating the document — only the branch/MR confirmations above gate on the user; run the gate 2 approved command immediately after.**
284
329
  `,
285
330
  },
286
331
  };
package/scripts/task.js CHANGED
@@ -331,17 +331,22 @@ async function nextGate(taskId) {
331
331
  const nextGateNum = currentGate + 1;
332
332
 
333
333
  // Save context snapshot if current task is active
334
+ let taskType = null;
334
335
  if (await fs.pathExists(CURRENT_FILE)) {
335
336
  const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
336
337
  if (ctx && ctx.taskId === resolvedId) {
338
+ taskType = ctx.taskType || null;
337
339
  await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
338
340
  }
339
341
  }
340
342
 
343
+ const isGenDoc = taskType === 'gen-doc';
344
+ const maxGate = isGenDoc ? 2 : 5;
345
+
341
346
  const taskState = {
342
347
  ...existing,
343
348
  taskId: resolvedId,
344
- status: 'pending',
349
+ status: nextGateNum > maxGate ? 'done' : 'pending',
345
350
  updatedAt: now,
346
351
  pausedAt: now,
347
352
  currentGate: nextGateNum,
@@ -354,20 +359,26 @@ async function nextGate(taskId) {
354
359
 
355
360
  // Generate cumulative task-summary.md (task-internal state — lives in .aiflow/, not gate docs)
356
361
  const summaryPath = path.join(TASKS_DIR, resolvedId, 'task-summary.md');
357
- const summaryContent = await generateMarkdownSummary(taskState);
362
+ const summaryContent = await generateMarkdownSummary(taskState, taskType);
358
363
  await fs.writeFile(summaryPath, summaryContent, 'utf-8');
359
364
 
360
- const taskType = existing.taskType || 'feature';
361
365
  console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
362
366
  console.log(chalk.gray(` Summary saved to: .aiflow/tasks/${resolvedId}/task-summary.md`));
363
- const nextLabel = gateLabel(nextGateNum, taskType);
364
- console.log(chalk.white(`\n Next: Gate ${nextGateNum} ${nextLabel}`));
365
- console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
366
- console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
367
- console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
368
- console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
369
- console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in the Gate 2 plan doc (AK-Docs/04.Coding/02.Plans/).`));
370
- console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
367
+
368
+ if (isGenDoc && currentGate >= maxGate) {
369
+ console.log(chalk.green('\n Document workflow complete. Task is done.'));
370
+ } else {
371
+ const nextLabel = gateLabel(nextGateNum, taskType);
372
+ console.log(chalk.white(`\n Next: Gate ${nextGateNum} ${nextLabel}`));
373
+ console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
374
+ console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
375
+ console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
376
+ console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
377
+ if (!isGenDoc) {
378
+ console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in the Gate 2 plan doc (AK-Docs/04.Coding/02.Plans/).`));
379
+ console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
380
+ }
381
+ }
371
382
  console.log();
372
383
  }
373
384
 
@@ -466,9 +477,9 @@ async function detectCurrentGate(taskId) {
466
477
  return 1;
467
478
  }
468
479
 
469
- async function generateMarkdownSummary(taskState) {
470
- const taskType = taskState.taskType || 'feature';
471
- const maxGate = taskType === 'testing' ? 4 : 5;
480
+ async function generateMarkdownSummary(taskState, taskType = null) {
481
+ taskType = taskType || taskState.taskType || 'feature';
482
+ const maxGate = taskType === 'testing' ? 4 : (taskType === 'gen-doc' ? 2 : 5);
472
483
  const lines = [];
473
484
  lines.push(`# Task Summary: ${taskState.taskId}`);
474
485
  lines.push(`**Title:** ${taskState.title}`);
@@ -492,7 +503,7 @@ async function generateMarkdownSummary(taskState) {
492
503
  return lines.join('\n');
493
504
  }
494
505
 
495
- function gateLabel(n, taskType) {
506
+ function gateLabel(n, taskType = null) {
496
507
  if (taskType === 'testing') {
497
508
  const labels = {
498
509
  1: 'Phân tích & Confirm',
@@ -502,6 +513,10 @@ function gateLabel(n, taskType) {
502
513
  };
503
514
  return labels[n] || `Gate ${n}`;
504
515
  }
516
+ if (taskType === 'gen-doc') {
517
+ if (n === 2) return 'Generate Document';
518
+ if (n >= 3) return 'Done';
519
+ }
505
520
  const labels = {
506
521
  1: 'AI Analyze Requirement',
507
522
  2: 'Implementation Plan',
package/scripts/use.js CHANGED
@@ -234,7 +234,7 @@ async function loadFromBacklog(issueKey, options = {}) {
234
234
  options.cto != null ||
235
235
  options.commentsTo != null ||
236
236
  options["comments-to"] != null;
237
-
237
+
238
238
  if (loadComments) {
239
239
  console.log(chalk.gray(" ℹ Comments requested..."));
240
240
  }
@@ -349,13 +349,13 @@ async function fetchBacklogComments(domain, apiKey, issueKey) {
349
349
  if (minId) {
350
350
  url += `&minId=${minId}`;
351
351
  }
352
-
352
+
353
353
  const batch = await backlogGet(url);
354
354
  if (!batch || batch.length === 0) break;
355
-
355
+
356
356
  all.push(...batch);
357
357
  if (batch.length < 100) break;
358
-
358
+
359
359
  // Use the last ID as minId for the next batch
360
360
  minId = batch[batch.length - 1].id;
361
361
  }
@@ -467,7 +467,7 @@ async function loadFromJira(issueKey, options = {}) {
467
467
  options.cto != null ||
468
468
  options.commentsTo != null ||
469
469
  options["comments-to"] != null;
470
-
470
+
471
471
  if (loadComments) {
472
472
  console.log(chalk.gray(" ℹ Comments requested..."));
473
473
  }
@@ -709,6 +709,7 @@ async function manualContext(prefillId = "") {
709
709
  { name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
710
710
  { name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
711
711
  { name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
712
+ { name: td("📝 Generate Doc 2 Gate", "Document", "Generate new documentation"), value: "gen-doc" },
712
713
  ],
713
714
  default: existing.taskType || undefined,
714
715
  });