@relipa/ai-flow-kit 0.1.5-beta.1 → 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.
Files changed (42) hide show
  1. package/bin/aiflow.js +34 -0
  2. package/custom/rules/ml-conventions.md +11 -8
  3. package/custom/rules/project-conventions.md +30 -14
  4. package/custom/skills/design-experiment/SKILL.md +2 -2
  5. package/custom/skills/evaluate-model/SKILL.md +2 -2
  6. package/custom/skills/explore-data/SKILL.md +1 -1
  7. package/custom/skills/figma-to-component/SKILL.md +222 -20
  8. package/custom/skills/frame-ml-problem/SKILL.md +1 -1
  9. package/custom/skills/gate-review/SKILL.md +1 -1
  10. package/custom/skills/generate-spec/SKILL.md +22 -3
  11. package/custom/skills/read-study-requirement/SKILL.md +74 -6
  12. package/custom/skills/review-plan/SKILL.md +26 -3
  13. package/custom/templates/shared/coding-workflow.md +0 -0
  14. package/custom/templates/shared/create-spec-workflow.md +55 -0
  15. package/custom/templates/shared/create-testcase-workflow.md +55 -0
  16. package/custom/templates/shared/gate-workflow.md +131 -19
  17. package/custom/templates/shared/ml-gate-workflow.md +16 -9
  18. package/custom/templates/tools/claude.md +1 -1
  19. package/custom/templates/tools/copilot.md +1 -1
  20. package/custom/templates/tools/cursor.md +1 -1
  21. package/custom/templates/tools/gemini.md +1 -1
  22. package/custom/templates/tools/generic.md +1 -1
  23. package/docs/common/AIFLOW.md +21 -11
  24. package/docs/common/CHANGELOG.md +43 -0
  25. package/docs/common/ai-integration.md +2 -2
  26. package/docs/common/cli-reference.md +3 -1
  27. package/docs/common/workflows/bug-fix.md +2 -2
  28. package/docs/common/workflows/feature.md +2 -2
  29. package/docs/common/workflows/figma.md +176 -105
  30. package/package.json +2 -2
  31. package/scripts/create-score-excel.js +135 -14
  32. package/scripts/detect.js +11 -0
  33. package/scripts/docs-branch.js +264 -0
  34. package/scripts/docs-repo.js +49 -0
  35. package/scripts/hooks/figma-rate-limit.js +83 -0
  36. package/scripts/hooks/session-start.js +49 -14
  37. package/scripts/init.js +33 -2
  38. package/scripts/link-resolver.js +0 -0
  39. package/scripts/prompt.js +56 -11
  40. package/scripts/task.js +39 -23
  41. package/scripts/update.js +4 -0
  42. package/scripts/use.js +6 -5
@@ -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,49 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+
6
+ // AK-Docs (per-project docs) and Shared-Docs (cross-project templates) are
7
+ // fixed-name folders living directly under the workspace root — the folder
8
+ // opened in the editor, which contains one or more source repos plus these
9
+ // two docs repos as siblings. `ak` is run from that root, so projectDir
10
+ // (process.cwd()) already IS the root — see docs/internal/Project-Structure.md.
11
+ const DOC_REPOS = [
12
+ { name: 'AK-Docs', purpose: 'tài liệu dự án (Requirements, Specs, Test Cases, Coding docs...)' },
13
+ { name: 'Shared-Docs', purpose: 'template dùng chung cho tất cả dự án (Spec, QA, Test Case, Test Report...)' },
14
+ ];
15
+
16
+ function resolveDocsRepoPath(projectDir, repoName) {
17
+ return path.join(projectDir, repoName);
18
+ }
19
+
20
+ async function syncDocsRepo(projectDir, repoName, purpose) {
21
+ const repoPath = resolveDocsRepoPath(projectDir, repoName);
22
+
23
+ if (!(await fs.pathExists(repoPath))) {
24
+ console.log(chalk.yellow(`⚠ ${repoName}/ chưa tồn tại tại ${repoPath} (${purpose}). Hãy clone repo này vào thư mục root.`));
25
+ return;
26
+ }
27
+
28
+ if (!(await fs.pathExists(path.join(repoPath, '.git')))) {
29
+ console.log(chalk.yellow(`⚠ ${repoName}/ tồn tại tại ${repoPath} nhưng không phải git repo — bỏ qua pull.`));
30
+ return;
31
+ }
32
+
33
+ console.log(chalk.cyan(`\n⟳ Pulling latest ${repoName}...`));
34
+ try {
35
+ execSync('git pull', { cwd: repoPath, stdio: 'inherit' });
36
+ console.log(chalk.green(`✓ ${repoName} đã cập nhật mới nhất.`));
37
+ } catch (err) {
38
+ console.log(chalk.red(`✗ Không thể pull ${repoName}: ${err.message}`));
39
+ }
40
+ }
41
+
42
+ async function syncDocsRepos(projectDir) {
43
+ console.log(chalk.blue('\nĐồng bộ AK-Docs / Shared-Docs...'));
44
+ for (const repo of DOC_REPOS) {
45
+ await syncDocsRepo(projectDir, repo.name, repo.purpose);
46
+ }
47
+ }
48
+
49
+ module.exports = { syncDocsRepos, resolveDocsRepoPath, DOC_REPOS };
@@ -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,21 +210,32 @@ 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
- lines.push(` 04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
219
- lines.push(` ([functionId] — look it up in 04.Coding/00.Overview/_Index.md or the folder containing ${ctx.taskId}.md)`);
223
+ lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
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
- lines.push(` 04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
226
- lines.push(` 04.Coding/02.Plans/[functionId]/${ctx.taskId}.md`);
227
- lines.push(` ([functionId] — look it up in 04.Coding/00.Overview/_Index.md)`);
236
+ lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
237
+ lines.push(` AK-Docs/04.Coding/02.Plans/[functionId]/${ctx.taskId}.md`);
238
+ lines.push(` ([functionId] — look it up in AK-Docs/04.Coding/00.Overview/_Index.md)`);
228
239
  lines.push('');
229
240
  lines.push('Proceed to Gate 3: implement code following the plan (TDD).');
230
241
  lines.push(buildModeInstruction(mode, 3));
@@ -251,20 +262,28 @@ function buildContextPrompt(ctx, taskState) {
251
262
  lines.push('- Pre-flight: determine functionId first (from context / input UC Spec file / ask DEV to confirm) — see the DEV workflow Pre-flight step');
252
263
  lines.push('- Read ticket + identify changed files only (no deep source scan)');
253
264
  lines.push('- Skip Q&A unless there is a critical blocking ambiguity');
254
- lines.push('- Output a lite requirement doc (Sections 1, 3, 5 only) to 04.Coding/01.Requirements/[functionId]/' + ctx.taskId + '.md');
265
+ lines.push('- Output a lite requirement doc (Sections 1, 3, 5 only) to AK-Docs/04.Coding/01.Requirements/[functionId]/' + ctx.taskId + '.md');
255
266
  lines.push('- Target: Gate 1 complete in < 5 minutes');
256
267
  } else {
257
268
  lines.push('Gate 1 process:');
258
- lines.push('0. Pre-flight: determine functionId (from context / input UC Spec file / ask DEV to confirm) + ensure 04.Coding/ tree exists — see the DEV workflow Pre-flight step. DO NOT continue without functionId.');
269
+ lines.push('0. Pre-flight: determine functionId (from context / input UC Spec file / ask DEV to confirm) + ensure AK-Docs/04.Coding/ tree exists — see the DEV workflow Pre-flight step. DO NOT continue without functionId.');
259
270
  lines.push('1. Read the ticket context above');
260
271
  lines.push('2. Read CLAUDE.md — understand project architecture and conventions');
261
272
  lines.push('3. Read source code — identify related files, data flow, patterns');
262
273
  lines.push('4. If anything is unclear — ask ONE question at a time');
263
- lines.push('5. Output 04.Coding/01.Requirements/[functionId]/' + ctx.taskId + '.md');
274
+ lines.push('5. Output AK-Docs/04.Coding/01.Requirements/[functionId]/' + ctx.taskId + '.md');
264
275
  lines.push('6. Display GATE 1 prompt and wait for APPROVED');
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
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const chalk = require('chalk');
5
5
  const { input, checkbox, confirm } = require('@inquirer/prompts');
6
+ const { syncDocsRepos } = require('./docs-repo');
6
7
 
7
8
  const PKG_DIR = path.join(__dirname, '..');
8
9
  const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
@@ -213,10 +214,20 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
213
214
  const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
214
215
  await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
215
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
+
216
226
  // Copy telemetry module so hooks can require('../telemetry/record')
217
227
  const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
218
228
  const telemetryDest = path.join(claudeDir, 'telemetry');
219
229
  await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
230
+ await fs.writeJson(path.join(telemetryDest, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
220
231
 
221
232
  const settingsFile = path.join(claudeDir, 'settings.json');
222
233
  let settings = {};
@@ -272,8 +283,26 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
272
283
  ]
273
284
  });
274
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
+
275
304
  await fs.writeJson(settingsFile, settings, { spaces: 2 });
276
- 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`));
277
306
  }
278
307
 
279
308
  async function verifySuperpowersSkills(versionDir) {
@@ -752,7 +781,6 @@ async function ensureAiflowGitignored(projectDir) {
752
781
  const gitignorePath = path.join(projectDir, '.gitignore');
753
782
  const entries = [
754
783
  '.aiflow/',
755
- 'plan/',
756
784
  '.claude/',
757
785
  '.rules/',
758
786
  '.mcp.json',
@@ -1048,6 +1076,9 @@ async function init(options) {
1048
1076
  try {
1049
1077
  console.log(chalk.blue(`Initializing ai-flow-kit (v${PKG_VERSION})...`));
1050
1078
 
1079
+ // ── Sync AK-Docs / Shared-Docs sibling repos ──────────────────
1080
+ await syncDocsRepos(projectDir);
1081
+
1051
1082
  // ── Auto-detect or prompt for framework if not supplied ──────
1052
1083
  if (frameworks.length === 0) {
1053
1084
  const stateFilePath = path.join(aiflowDir, 'state.json');
File without changes