@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.
- package/bin/aiflow.js +71 -33
- package/custom/rules/ml-conventions.md +11 -8
- package/custom/rules/project-conventions.md +18 -2
- package/custom/skills/design-experiment/SKILL.md +2 -2
- package/custom/skills/evaluate-model/SKILL.md +2 -2
- package/custom/skills/explore-data/SKILL.md +1 -1
- package/custom/skills/figma-to-component/SKILL.md +222 -20
- package/custom/skills/frame-ml-problem/SKILL.md +1 -1
- package/custom/skills/generate-spec/SKILL.md +19 -0
- package/custom/skills/read-study-requirement/SKILL.md +69 -1
- package/custom/skills/review-plan/SKILL.md +42 -0
- package/custom/templates/memory/CODEOWNERS +8 -0
- package/custom/templates/memory/ci/memory-finalize.yml +9 -0
- package/custom/templates/memory/ci/memory-lint.yml +10 -0
- package/custom/templates/memory/gitlab/merge_request_templates/memory.md +18 -0
- package/custom/templates/memory/memory-item.md +25 -0
- package/custom/templates/memory/skeleton/00.Shared/architecture/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/00.Shared/decisions/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/00.Shared/domain/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/00.Shared/glossary/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/ba/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/dev/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/pm/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/qa/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/02.Instincts/approved/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/03.Retro/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/MEMORY.md +7 -0
- package/custom/templates/memory/skeleton/_deprecated/.gitkeep +0 -0
- package/custom/templates/shared/create-spec-workflow.md +68 -1
- package/custom/templates/shared/create-testcase-workflow.md +67 -0
- package/custom/templates/shared/gate-workflow.md +122 -3
- package/custom/templates/shared/ml-gate-workflow.md +16 -9
- package/docs/common/AIFLOW.md +11 -1
- package/docs/common/CHANGELOG.md +32 -0
- package/docs/common/cli-reference.md +3 -1
- package/docs/common/workflows/figma.md +176 -105
- package/package.json +2 -2
- package/scripts/create-score-excel.js +135 -14
- package/scripts/detect.js +11 -0
- package/scripts/docs-branch.js +264 -0
- package/scripts/hooks/figma-rate-limit.js +83 -0
- package/scripts/hooks/session-start.js +146 -8
- package/scripts/init.js +53 -1
- package/scripts/memory-store.js +391 -0
- package/scripts/memory.js +176 -247
- package/scripts/prompt.js +45 -0
- package/scripts/task.js +30 -15
- package/scripts/update.js +12 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
222
|
-
|
|
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');
|