@relipa/ai-flow-kit 0.1.5 → 0.1.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 (49) hide show
  1. package/bin/aiflow.js +71 -33
  2. package/custom/rules/ml-conventions.md +11 -8
  3. package/custom/rules/project-conventions.md +18 -2
  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/generate-spec/SKILL.md +19 -0
  10. package/custom/skills/read-study-requirement/SKILL.md +69 -1
  11. package/custom/skills/review-plan/SKILL.md +42 -0
  12. package/custom/templates/memory/CODEOWNERS +8 -0
  13. package/custom/templates/memory/ci/memory-finalize.yml +9 -0
  14. package/custom/templates/memory/ci/memory-lint.yml +10 -0
  15. package/custom/templates/memory/gitlab/merge_request_templates/memory.md +18 -0
  16. package/custom/templates/memory/memory-item.md +25 -0
  17. package/custom/templates/memory/skeleton/00.Shared/architecture/_global/.gitkeep +0 -0
  18. package/custom/templates/memory/skeleton/00.Shared/decisions/.gitkeep +0 -0
  19. package/custom/templates/memory/skeleton/00.Shared/domain/_global/.gitkeep +0 -0
  20. package/custom/templates/memory/skeleton/00.Shared/glossary/.gitkeep +0 -0
  21. package/custom/templates/memory/skeleton/01.Lessons/ba/_global/.gitkeep +0 -0
  22. package/custom/templates/memory/skeleton/01.Lessons/dev/_global/.gitkeep +0 -0
  23. package/custom/templates/memory/skeleton/01.Lessons/pm/_global/.gitkeep +0 -0
  24. package/custom/templates/memory/skeleton/01.Lessons/qa/_global/.gitkeep +0 -0
  25. package/custom/templates/memory/skeleton/02.Instincts/approved/_global/.gitkeep +0 -0
  26. package/custom/templates/memory/skeleton/03.Retro/.gitkeep +0 -0
  27. package/custom/templates/memory/skeleton/MEMORY.md +7 -0
  28. package/custom/templates/memory/skeleton/_deprecated/.gitkeep +0 -0
  29. package/custom/templates/shared/create-spec-workflow.md +68 -1
  30. package/custom/templates/shared/create-testcase-workflow.md +67 -0
  31. package/custom/templates/shared/gate-workflow.md +122 -3
  32. package/custom/templates/shared/ml-gate-workflow.md +16 -9
  33. package/docs/common/AIFLOW.md +11 -1
  34. package/docs/common/CHANGELOG.md +32 -0
  35. package/docs/common/cli-reference.md +3 -1
  36. package/docs/common/workflows/figma.md +176 -105
  37. package/package.json +2 -2
  38. package/scripts/create-score-excel.js +135 -14
  39. package/scripts/detect.js +11 -0
  40. package/scripts/docs-branch.js +264 -0
  41. package/scripts/hooks/figma-rate-limit.js +83 -0
  42. package/scripts/hooks/session-start.js +146 -8
  43. package/scripts/init.js +53 -1
  44. package/scripts/memory-store.js +391 -0
  45. package/scripts/memory.js +176 -247
  46. package/scripts/prompt.js +45 -0
  47. package/scripts/task.js +30 -15
  48. package/scripts/update.js +12 -0
  49. package/scripts/use.js +7 -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,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
+ })();
@@ -9,6 +9,7 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const os = require('os');
12
+ const { execSync } = require('child_process');
12
13
 
13
14
  function loadTelemetry() {
14
15
  try { return require('../telemetry/record'); } catch (_) { }
@@ -29,7 +30,7 @@ let tasksDir = '';
29
30
  let raw = '';
30
31
  process.stdin.setEncoding('utf-8');
31
32
  process.stdin.on('data', chunk => { raw += chunk; });
32
- process.stdin.on('end', () => {
33
+ process.stdin.on('end', async () => {
33
34
  let hookData = {};
34
35
  try { hookData = JSON.parse(raw || '{}'); } catch (_) { }
35
36
 
@@ -59,9 +60,10 @@ process.stdin.on('end', () => {
59
60
 
60
61
  // ── 2. Load active ticket context ──────────────────────────────
61
62
  let contextBlock = '';
63
+ let ctx = null;
62
64
  try {
63
65
  if (fs.existsSync(contextPath)) {
64
- const ctx = JSON.parse(fs.readFileSync(contextPath, 'utf-8'));
66
+ ctx = JSON.parse(fs.readFileSync(contextPath, 'utf-8'));
65
67
  if (ctx.taskId && ctx.title) {
66
68
  const taskState = loadTaskState(ctx.taskId);
67
69
  contextBlock = buildContextPrompt(ctx, taskState);
@@ -97,6 +99,34 @@ process.stdin.on('end', () => {
97
99
  }
98
100
  } catch (_) { }
99
101
 
102
+ // ── 3.5 Load relevant memory (99.Memory/ Project Brain) ────────
103
+ // See docs/internal/Memory-Architecture-v1.0.md — Luồng 3 (Nạp/Sử dụng).
104
+ let memoryBlock = '';
105
+ try {
106
+ const workspaceRoot = resolveWorkspaceRoot(projectRoot);
107
+ const akDocsPath = path.join(workspaceRoot, 'AK-Docs');
108
+ if (fs.existsSync(akDocsPath)) {
109
+ if (fs.existsSync(path.join(akDocsPath, '.git'))) {
110
+ pullAkDocsWithTimeout(akDocsPath);
111
+ }
112
+ const memoryStore = require('../lib/memory-store');
113
+ const functionId = ctx ? (ctx.functionId || ctx.screenId) : null;
114
+ const workflow = inferWorkflow(ctx);
115
+ const tags = ctx ? extractQueryTags(ctx) : [];
116
+
117
+ const [indexContent, relevant] = await Promise.all([
118
+ memoryStore.readIndex(workspaceRoot),
119
+ memoryStore.loadRelevant(workspaceRoot, { functionId, workflow, tags }),
120
+ ]);
121
+
122
+ memoryBlock = buildMemoryBlock(indexContent, relevant);
123
+ // recordHits' ledger lives under THIS project's own .aiflow/ (projectRoot),
124
+ // not the workspace root — matches where context/tasks/ already live. Batched
125
+ // into one read-modify-write (see memory-store.js) instead of one call per id.
126
+ await memoryStore.recordHits(projectRoot, relevant.map(m => m.id));
127
+ }
128
+ } catch (_) { }
129
+
100
130
  // ── 4. Combine and output ──────────────────────────────────────
101
131
  const parts = [];
102
132
  if (skillContent) {
@@ -105,6 +135,9 @@ process.stdin.on('end', () => {
105
135
  if (contextBlock) {
106
136
  parts.push(contextBlock);
107
137
  }
138
+ if (memoryBlock) {
139
+ parts.push(memoryBlock);
140
+ }
108
141
 
109
142
  const combined = parts.join('\n\n');
110
143
  const escaped = combined
@@ -126,6 +159,76 @@ process.stdin.on('end', () => {
126
159
 
127
160
  // ── Helpers ────────────────────────────────────────────────────
128
161
 
162
+ // AK-Docs lives as a sibling of the project repo at the workspace root (see
163
+ // scripts/docs-repo.js), but this hook can't rely on process.cwd() — Claude Code's
164
+ // hook invocation cwd isn't guaranteed to be the workspace root. Try the project
165
+ // root itself first (project opened standalone with AK-Docs cloned alongside it
166
+ // at that same level), then its parent (the documented sibling-of-the-repo layout).
167
+ function resolveWorkspaceRoot(projectRoot) {
168
+ if (fs.existsSync(path.join(projectRoot, 'AK-Docs'))) return projectRoot;
169
+ const parent = path.resolve(projectRoot, '..');
170
+ if (fs.existsSync(path.join(parent, 'AK-Docs'))) return parent;
171
+ return projectRoot;
172
+ }
173
+
174
+ function pullAkDocsWithTimeout(akDocsPath, timeoutMs = 5000) {
175
+ try {
176
+ execSync('git pull', { cwd: akDocsPath, stdio: 'ignore', timeout: timeoutMs });
177
+ } catch (err) {
178
+ process.stderr.write(`[aiflow] ⚠ Không pull được AK-Docs (${String(err.message || err).split('\n')[0]}) — dùng bản memory hiện có trên máy.\n`);
179
+ }
180
+ }
181
+
182
+ // Map the active task's taskType to the workflow profile used by the memory scorer
183
+ // (doc §5.2 profile table). Unresolvable → null, which falls back to the doc's
184
+ // "gen-doc / không xác định" default profile inside memory-store's scoring.
185
+ function inferWorkflow(ctx) {
186
+ if (!ctx) return null;
187
+ const t = ctx.taskType;
188
+ if (t === 'gen-doc') return 'gen-doc';
189
+ if (t === 'testing') return 'create-testcase';
190
+ if (t === 'spec') return 'create-spec';
191
+ if (!t || ['feature', 'bug-fix', 'refactor', 'documentation', 'investigation'].includes(t)) return 'coding';
192
+ return null;
193
+ }
194
+
195
+ // Rough query-side tag extraction from the ticket title — the scorer only needs a
196
+ // handful of significant words to overlap against memory `tags:` (doc §5.2 formula).
197
+ function extractQueryTags(ctx) {
198
+ const text = `${ctx.title || ''} ${ctx.description || ''}`;
199
+ return [...new Set(
200
+ text.toLowerCase()
201
+ .replace(/[^a-z0-9\s-]/g, ' ')
202
+ .split(/\s+/)
203
+ .filter(w => w.length > 3)
204
+ )].slice(0, 8);
205
+ }
206
+
207
+ function buildMemoryBlock(indexContent, relevant) {
208
+ if (!indexContent && (!relevant || relevant.length === 0)) return '';
209
+ const lines = ['<PROJECT_MEMORY>'];
210
+ lines.push('**99.Memory/ index (Lớp 1 — luôn có; xem MEMORY.md để biết toàn bộ danh mục):**');
211
+ lines.push('```markdown');
212
+ lines.push((indexContent || '(chưa có memory nào được approve)').trim());
213
+ lines.push('```');
214
+ if (relevant && relevant.length > 0) {
215
+ lines.push('');
216
+ lines.push(`**Memory liên quan nhất tới task này (Lớp 2, ${relevant.length} mục):**`);
217
+ for (const m of relevant) {
218
+ const pendingTag = m.status === 'pending'
219
+ ? ' ⚠ CHƯA DUYỆT — chỉ máy này thấy, kiểm tra lại trước khi tin'
220
+ : '';
221
+ const firstLine = (m.content || '').trim().split('\n')[0];
222
+ lines.push(`- **${m.id}** (${m.type}, scope=${m.scope})${pendingTag}`);
223
+ lines.push(` ${firstLine}`);
224
+ }
225
+ }
226
+ lines.push('');
227
+ lines.push('Nếu memory mâu thuẫn với `.rules/`, `.rules/` thắng — báo cho developer biết.');
228
+ lines.push('</PROJECT_MEMORY>');
229
+ return lines.join('\n');
230
+ }
231
+
129
232
  function loadTaskState(taskId) {
130
233
  try {
131
234
  const statePath = path.join(tasksDir, taskId, 'task-state.json');
@@ -210,16 +313,27 @@ function buildContextPrompt(ctx, taskState) {
210
313
  }
211
314
  lines.push(`- Gate ${currentGate}: 🔄 IN PROGRESS`);
212
315
  lines.push('');
213
- lines.push(`**DO NOT restart from Gate 1.** Resume directly at Gate ${currentGate} (${gateLabel(currentGate)}).`);
316
+ const isGenDoc = ctx.taskType === 'gen-doc';
317
+ lines.push(`**DO NOT restart from Gate 1.** Resume directly at Gate ${currentGate} (${gateLabel(currentGate, ctx.taskType)}).`);
214
318
  lines.push('');
215
319
 
216
- if (currentGate === 2) {
320
+ if (isGenDoc && currentGate >= 3) {
321
+ lines.push(`This is a **gen-doc** task (2-gate flow). The document workflow is complete.`);
322
+ lines.push(`Expected output: \`plan/${ctx.taskId}/output.md\` (or path in requirement.md).`);
323
+ lines.push('DO NOT start Gate 3, DO NOT create plan.md, DO NOT run TDD.');
324
+ } else if (currentGate === 2) {
217
325
  lines.push('Gate 1 is already APPROVED. The requirement document is at:');
218
326
  lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
219
327
  lines.push(` ([functionId] — look it up in AK-Docs/04.Coding/00.Overview/_Index.md or the folder containing ${ctx.taskId}.md)`);
220
328
  lines.push('');
221
- lines.push('Proceed to Gate 2: create the implementation plan.');
222
- lines.push(buildModeInstruction(mode, 2));
329
+ if (isGenDoc) {
330
+ lines.push('This is a **gen-doc** task. DO NOT create plan.md or run TDD.');
331
+ lines.push('Proceed to Gate 2: read the approved requirement.md and generate the output document immediately.');
332
+ lines.push('After generating, self-review + create task-summary.md, then run `ak gate 2 approved`.');
333
+ } else {
334
+ lines.push('Proceed to Gate 2: create the implementation plan.');
335
+ }
336
+ lines.push(buildModeInstruction(mode, 2, ctx.taskType));
223
337
  } else if (currentGate === 3) {
224
338
  lines.push('Gates 1 and 2 are already APPROVED. Plans are at:');
225
339
  lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
@@ -265,6 +379,14 @@ function buildContextPrompt(ctx, taskState) {
265
379
  }
266
380
 
267
381
  lines.push('');
382
+ if (ctx.taskType === 'gen-doc') {
383
+ lines.push('**This is a gen-doc task (2-gate flow):**');
384
+ lines.push('- Gate 1: Create requirement.md → wait for APPROVED');
385
+ lines.push('- Gate 2 (after APPROVED): Generate output document immediately + self-review + task-summary.md');
386
+ lines.push('- NO plan.md, NO TDD, NO Gate 3/4/5');
387
+ lines.push('');
388
+ }
389
+
268
390
  lines.push('DO NOT wait for the developer to ask. START NOW.');
269
391
  lines.push('If you have not started automatically, begin as soon as the developer types **"start"**.');
270
392
  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 +396,11 @@ function buildContextPrompt(ctx, taskState) {
274
396
  return lines.join('\n');
275
397
  }
276
398
 
277
- function gateLabel(n) {
399
+ function gateLabel(n, taskType = '') {
400
+ if (taskType === 'gen-doc') {
401
+ if (n === 2) return 'Generate Document';
402
+ if (n >= 3) return 'Done';
403
+ }
278
404
  const labels = {
279
405
  1: 'AI Analyze Requirement',
280
406
  2: 'Implementation Plan',
@@ -285,9 +411,21 @@ function gateLabel(n) {
285
411
  return labels[n] || `Gate ${n}`;
286
412
  }
287
413
 
288
- function buildModeInstruction(mode, gate) {
414
+ function buildModeInstruction(mode, gate, taskType = '') {
289
415
  if (mode !== 'fast') return '';
290
416
 
417
+ if (gate === 2 && taskType === 'gen-doc') {
418
+ return [
419
+ '',
420
+ '**MODE: fast — Gate 2 (gen-doc):**',
421
+ '- Re-read `AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md` → generate output document immediately',
422
+ '- Save to `AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId].md` (or the custom path/format noted in the requirement doc)',
423
+ '- Self-review + create `AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId]-summary.md`',
424
+ '- Submit AK-Docs via MR (show title/description, wait for explicit confirmation, then `ak docs submit --title "..." --description "..." --yes`)',
425
+ '- Run `ak gate 2 approved` immediately — NO extra approval prompt for this step',
426
+ ].join('\n');
427
+ }
428
+
291
429
  const instructions = {
292
430
  2: [
293
431
  '**MODE: fast** — Gate 2 fast track:',
package/scripts/init.js CHANGED
@@ -4,6 +4,7 @@ const os = require('os');
4
4
  const chalk = require('chalk');
5
5
  const { input, checkbox, confirm } = require('@inquirer/prompts');
6
6
  const { syncDocsRepos } = require('./docs-repo');
7
+ const memoryStore = require('./memory-store');
7
8
 
8
9
  const PKG_DIR = path.join(__dirname, '..');
9
10
  const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
@@ -214,10 +215,30 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
214
215
  const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
215
216
  await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
216
217
 
218
+ const hookFigmaRateSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'figma-rate-limit.js');
219
+ const hookFigmaRateDest = path.join(hooksDir, 'figma-rate-limit.js');
220
+ await fs.copy(hookFigmaRateSrc, hookFigmaRateDest, { overwrite: true });
221
+
222
+ // Hooks are CommonJS (.js + require). In an ESM project ("type": "module" in the
223
+ // project's package.json) Node would treat them as ES modules and crash on require().
224
+ // A nested package.json pins the interpretation for the kit-owned dirs only.
225
+ await fs.writeJson(path.join(hooksDir, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
226
+
217
227
  // Copy telemetry module so hooks can require('../telemetry/record')
218
228
  const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
219
229
  const telemetryDest = path.join(claudeDir, 'telemetry');
220
230
  await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
231
+ await fs.writeJson(path.join(telemetryDest, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
232
+
233
+ // Copy memory-store.js so session-start.js can require('../lib/memory-store') for the
234
+ // 99.Memory/ Layer 1+2 load — same reason as telemetry above: only the hook file itself
235
+ // is deployed, not the rest of scripts/. memory-store.js is dependency-free by design
236
+ // (no fs-extra/chalk — see its own header comment) specifically so it can be copied alone.
237
+ // Own subdir + nested package.json for the same CJS-pinning reason as hooks/telemetry above.
238
+ const libDest = path.join(claudeDir, 'lib');
239
+ await fs.ensureDir(libDest);
240
+ await fs.copy(path.join(PKG_DIR, 'scripts', 'memory-store.js'), path.join(libDest, 'memory-store.js'), { overwrite: true });
241
+ await fs.writeJson(path.join(libDest, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
221
242
 
222
243
  const settingsFile = path.join(claudeDir, 'settings.json');
223
244
  let settings = {};
@@ -273,8 +294,26 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
273
294
  ]
274
295
  });
275
296
 
297
+ // Throttle Figma MCP calls to stay under Figma's Tier-1 REST budget (anti-429).
298
+ // Sleeps when the 60s window is full — may hold a call up to ~60s, hence timeout 120.
299
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
300
+ h => !(h._aiflowKitFigmaRateLimit)
301
+ );
302
+ settings.hooks.PreToolUse.push({
303
+ _aiflowKitFigmaRateLimit: true,
304
+ matcher: 'mcp__figma__.*',
305
+ hooks: [
306
+ {
307
+ type: 'command',
308
+ command: `node "${hookFigmaRateDest.replace(/\\/g, '/')}"`,
309
+ timeout: 120,
310
+ statusMessage: 'Figma rate-limit guard: checking call budget…',
311
+ }
312
+ ]
313
+ });
314
+
276
315
  await fs.writeJson(settingsFile, settings, { spaces: 2 });
277
- console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write) configured`));
316
+ console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write, PreToolUse:figma-rate-limit) configured`));
278
317
  }
279
318
 
280
319
  async function verifySuperpowersSkills(versionDir) {
@@ -1051,6 +1090,19 @@ async function init(options) {
1051
1090
  // ── Sync AK-Docs / Shared-Docs sibling repos ──────────────────
1052
1091
  await syncDocsRepos(projectDir);
1053
1092
 
1093
+ // ── Bootstrap 99.Memory/ skeleton (Luồng 1 — Khởi tạo) ────────
1094
+ // Free: no reads, no token cost — just an empty folder skeleton if AK-Docs
1095
+ // exists and doesn't have one yet (docs/internal/Memory-Architecture-v1.0.md §4).
1096
+ const memorySkeleton = await memoryStore.ensureSkeleton(projectDir);
1097
+ if (memorySkeleton.created) {
1098
+ console.log(chalk.green('✓ Đã tạo khung 99.Memory/ trong AK-Docs (chưa có ghi nhớ nào).'));
1099
+ const gitignoreResult = await memoryStore.ensureMemoryGitignored(projectDir);
1100
+ if (memorySkeleton.committed || gitignoreResult.committed) {
1101
+ console.log(chalk.yellow(' ⚠ Đã commit local trên nhánh hiện tại của AK-Docs — `main` là protected branch,'));
1102
+ console.log(chalk.yellow(' hãy tự `git push` khi sẵn sàng (không tự động push).'));
1103
+ }
1104
+ }
1105
+
1054
1106
  // ── Auto-detect or prompt for framework if not supplied ──────
1055
1107
  if (frameworks.length === 0) {
1056
1108
  const stateFilePath = path.join(aiflowDir, 'state.json');