@relipa/ai-flow-kit 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aiflow.js +34 -0
- 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 +23 -0
- package/custom/templates/shared/create-spec-workflow.md +55 -0
- package/custom/templates/shared/create-testcase-workflow.md +55 -0
- package/custom/templates/shared/gate-workflow.md +115 -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 +41 -6
- package/scripts/init.js +29 -1
- package/scripts/prompt.js +45 -0
- package/scripts/task.js +30 -15
- 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,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse hook: throttle mcp__figma__* calls to stay under Figma's Tier-1 REST
|
|
4
|
+
* budget (10/min on a Starter-plan Full seat). Sleeps instead of blocking, so the
|
|
5
|
+
* tool call always proceeds — just never faster than the budget allows.
|
|
6
|
+
*
|
|
7
|
+
* Budget is counted in "units" ≈ underlying Figma REST calls
|
|
8
|
+
* (verified against figma-developer-mcp v0.13.2):
|
|
9
|
+
* get_figma_data → 1 unit (one /v1/files/:key[/nodes] call)
|
|
10
|
+
* download_figma_images → 3 units (fills map + PNG render + SVG render, worst case)
|
|
11
|
+
* Override with FIGMA_RATE_LIMIT_UNITS (default 6/60s — safety margin under 10/min).
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const WINDOW = 60; // seconds
|
|
20
|
+
const MAX_UNITS = parseInt(process.env.FIGMA_RATE_LIMIT_UNITS || '6', 10) || 6;
|
|
21
|
+
const STATE = path.join(
|
|
22
|
+
os.tmpdir(),
|
|
23
|
+
`figma-mcp-rate-limit-${typeof process.getuid === 'function' ? process.getuid() : 'user'}.log`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function readStdin() {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let data = '';
|
|
29
|
+
process.stdin.setEncoding('utf8');
|
|
30
|
+
process.stdin.on('data', (c) => (data += c));
|
|
31
|
+
process.stdin.on('end', () => resolve(data));
|
|
32
|
+
process.stdin.on('error', () => resolve(data));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
37
|
+
|
|
38
|
+
function readStamps() {
|
|
39
|
+
try {
|
|
40
|
+
return fs
|
|
41
|
+
.readFileSync(STATE, 'utf8')
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map((l) => parseInt(l, 10))
|
|
44
|
+
.filter((n) => Number.isFinite(n))
|
|
45
|
+
.sort((a, b) => a - b);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
(async () => {
|
|
52
|
+
let tool = '';
|
|
53
|
+
try {
|
|
54
|
+
tool = JSON.parse(await readStdin()).tool_name || '';
|
|
55
|
+
} catch {
|
|
56
|
+
/* no/invalid stdin — treat as 1 unit */
|
|
57
|
+
}
|
|
58
|
+
const units = tool.includes('download_figma_images') ? 3 : 1;
|
|
59
|
+
|
|
60
|
+
let slept = 0;
|
|
61
|
+
for (;;) {
|
|
62
|
+
const now = Math.floor(Date.now() / 1000);
|
|
63
|
+
const recent = readStamps().filter((t) => t > now - WINDOW);
|
|
64
|
+
// recent.length === 0 escape hatch: a single oversized call must never deadlock
|
|
65
|
+
if (recent.length + units <= MAX_UNITS || recent.length === 0) {
|
|
66
|
+
for (let i = 0; i < units; i++) recent.push(now);
|
|
67
|
+
fs.writeFileSync(STATE, recent.join('\n') + '\n');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const wait = Math.max(1, recent[0] + WINDOW - now + 1);
|
|
71
|
+
await sleep(wait * 1000);
|
|
72
|
+
slept += wait;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (slept > 0) {
|
|
76
|
+
console.log(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
systemMessage: `Figma rate-limit guard: delayed ${slept}s to stay under ${MAX_UNITS} calls/min`,
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
process.exit(0);
|
|
83
|
+
})();
|
|
@@ -210,16 +210,27 @@ function buildContextPrompt(ctx, taskState) {
|
|
|
210
210
|
}
|
|
211
211
|
lines.push(`- Gate ${currentGate}: 🔄 IN PROGRESS`);
|
|
212
212
|
lines.push('');
|
|
213
|
-
|
|
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
|
|
217
|
+
if (isGenDoc && currentGate >= 3) {
|
|
218
|
+
lines.push(`This is a **gen-doc** task (2-gate flow). The document workflow is complete.`);
|
|
219
|
+
lines.push(`Expected output: \`plan/${ctx.taskId}/output.md\` (or path in requirement.md).`);
|
|
220
|
+
lines.push('DO NOT start Gate 3, DO NOT create plan.md, DO NOT run TDD.');
|
|
221
|
+
} else if (currentGate === 2) {
|
|
217
222
|
lines.push('Gate 1 is already APPROVED. The requirement document is at:');
|
|
218
223
|
lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
|
|
219
224
|
lines.push(` ([functionId] — look it up in AK-Docs/04.Coding/00.Overview/_Index.md or the folder containing ${ctx.taskId}.md)`);
|
|
220
225
|
lines.push('');
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
if (isGenDoc) {
|
|
227
|
+
lines.push('This is a **gen-doc** task. DO NOT create plan.md or run TDD.');
|
|
228
|
+
lines.push('Proceed to Gate 2: read the approved requirement.md and generate the output document immediately.');
|
|
229
|
+
lines.push('After generating, self-review + create task-summary.md, then run `ak gate 2 approved`.');
|
|
230
|
+
} else {
|
|
231
|
+
lines.push('Proceed to Gate 2: create the implementation plan.');
|
|
232
|
+
}
|
|
233
|
+
lines.push(buildModeInstruction(mode, 2, ctx.taskType));
|
|
223
234
|
} else if (currentGate === 3) {
|
|
224
235
|
lines.push('Gates 1 and 2 are already APPROVED. Plans are at:');
|
|
225
236
|
lines.push(` AK-Docs/04.Coding/01.Requirements/[functionId]/${ctx.taskId}.md`);
|
|
@@ -265,6 +276,14 @@ function buildContextPrompt(ctx, taskState) {
|
|
|
265
276
|
}
|
|
266
277
|
|
|
267
278
|
lines.push('');
|
|
279
|
+
if (ctx.taskType === 'gen-doc') {
|
|
280
|
+
lines.push('**This is a gen-doc task (2-gate flow):**');
|
|
281
|
+
lines.push('- Gate 1: Create requirement.md → wait for APPROVED');
|
|
282
|
+
lines.push('- Gate 2 (after APPROVED): Generate output document immediately + self-review + task-summary.md');
|
|
283
|
+
lines.push('- NO plan.md, NO TDD, NO Gate 3/4/5');
|
|
284
|
+
lines.push('');
|
|
285
|
+
}
|
|
286
|
+
|
|
268
287
|
lines.push('DO NOT wait for the developer to ask. START NOW.');
|
|
269
288
|
lines.push('If you have not started automatically, begin as soon as the developer types **"start"**.');
|
|
270
289
|
lines.push('(If a later ticket switch does not seem to trigger this — e.g. `ak use` was run in another terminal while this chat was already open — the developer can run **`/coding`** to load it manually.)');
|
|
@@ -274,7 +293,11 @@ function buildContextPrompt(ctx, taskState) {
|
|
|
274
293
|
return lines.join('\n');
|
|
275
294
|
}
|
|
276
295
|
|
|
277
|
-
function gateLabel(n) {
|
|
296
|
+
function gateLabel(n, taskType = '') {
|
|
297
|
+
if (taskType === 'gen-doc') {
|
|
298
|
+
if (n === 2) return 'Generate Document';
|
|
299
|
+
if (n >= 3) return 'Done';
|
|
300
|
+
}
|
|
278
301
|
const labels = {
|
|
279
302
|
1: 'AI Analyze Requirement',
|
|
280
303
|
2: 'Implementation Plan',
|
|
@@ -285,9 +308,21 @@ function gateLabel(n) {
|
|
|
285
308
|
return labels[n] || `Gate ${n}`;
|
|
286
309
|
}
|
|
287
310
|
|
|
288
|
-
function buildModeInstruction(mode, gate) {
|
|
311
|
+
function buildModeInstruction(mode, gate, taskType = '') {
|
|
289
312
|
if (mode !== 'fast') return '';
|
|
290
313
|
|
|
314
|
+
if (gate === 2 && taskType === 'gen-doc') {
|
|
315
|
+
return [
|
|
316
|
+
'',
|
|
317
|
+
'**MODE: fast — Gate 2 (gen-doc):**',
|
|
318
|
+
'- Re-read `AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md` → generate output document immediately',
|
|
319
|
+
'- Save to `AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId].md` (or the custom path/format noted in the requirement doc)',
|
|
320
|
+
'- Self-review + create `AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId]-summary.md`',
|
|
321
|
+
'- Submit AK-Docs via MR (show title/description, wait for explicit confirmation, then `ak docs submit --title "..." --description "..." --yes`)',
|
|
322
|
+
'- Run `ak gate 2 approved` immediately — NO extra approval prompt for this step',
|
|
323
|
+
].join('\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
291
326
|
const instructions = {
|
|
292
327
|
2: [
|
|
293
328
|
'**MODE: fast** — Gate 2 fast track:',
|
package/scripts/init.js
CHANGED
|
@@ -214,10 +214,20 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
214
214
|
const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
|
|
215
215
|
await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
|
|
216
216
|
|
|
217
|
+
const hookFigmaRateSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'figma-rate-limit.js');
|
|
218
|
+
const hookFigmaRateDest = path.join(hooksDir, 'figma-rate-limit.js');
|
|
219
|
+
await fs.copy(hookFigmaRateSrc, hookFigmaRateDest, { overwrite: true });
|
|
220
|
+
|
|
221
|
+
// Hooks are CommonJS (.js + require). In an ESM project ("type": "module" in the
|
|
222
|
+
// project's package.json) Node would treat them as ES modules and crash on require().
|
|
223
|
+
// A nested package.json pins the interpretation for the kit-owned dirs only.
|
|
224
|
+
await fs.writeJson(path.join(hooksDir, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
|
|
225
|
+
|
|
217
226
|
// Copy telemetry module so hooks can require('../telemetry/record')
|
|
218
227
|
const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
|
|
219
228
|
const telemetryDest = path.join(claudeDir, 'telemetry');
|
|
220
229
|
await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
|
|
230
|
+
await fs.writeJson(path.join(telemetryDest, 'package.json'), { type: 'commonjs' }, { spaces: 2 });
|
|
221
231
|
|
|
222
232
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
223
233
|
let settings = {};
|
|
@@ -273,8 +283,26 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
273
283
|
]
|
|
274
284
|
});
|
|
275
285
|
|
|
286
|
+
// Throttle Figma MCP calls to stay under Figma's Tier-1 REST budget (anti-429).
|
|
287
|
+
// Sleeps when the 60s window is full — may hold a call up to ~60s, hence timeout 120.
|
|
288
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
289
|
+
h => !(h._aiflowKitFigmaRateLimit)
|
|
290
|
+
);
|
|
291
|
+
settings.hooks.PreToolUse.push({
|
|
292
|
+
_aiflowKitFigmaRateLimit: true,
|
|
293
|
+
matcher: 'mcp__figma__.*',
|
|
294
|
+
hooks: [
|
|
295
|
+
{
|
|
296
|
+
type: 'command',
|
|
297
|
+
command: `node "${hookFigmaRateDest.replace(/\\/g, '/')}"`,
|
|
298
|
+
timeout: 120,
|
|
299
|
+
statusMessage: 'Figma rate-limit guard: checking call budget…',
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
});
|
|
303
|
+
|
|
276
304
|
await fs.writeJson(settingsFile, settings, { spaces: 2 });
|
|
277
|
-
console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write) configured`));
|
|
305
|
+
console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write, PreToolUse:figma-rate-limit) configured`));
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
async function verifySuperpowersSkills(versionDir) {
|
package/scripts/prompt.js
CHANGED
|
@@ -281,6 +281,51 @@ Write documentation based on approved outline.
|
|
|
281
281
|
**INVOKE:** \`superpowers:verification-before-completion\`
|
|
282
282
|
Verify docs match code. No outdated examples.
|
|
283
283
|
Display: "GATE 3: Documentation complete. Type APPROVED."
|
|
284
|
+
`,
|
|
285
|
+
},
|
|
286
|
+
'gen-doc': {
|
|
287
|
+
header: 'Generate Document',
|
|
288
|
+
instruction: 'Generate a document from source code / feature analysis as described below.',
|
|
289
|
+
skillWorkflow: `
|
|
290
|
+
## STRICT GATE WORKFLOW — Generate Document (2-Gate)
|
|
291
|
+
|
|
292
|
+
> ⚡ gen-doc uses a **2-gate flow**: Gate 1 (Requirement) → APPROVED → Gate 2 (Generate + Auto Review).
|
|
293
|
+
> Gate 2 runs immediately after APPROVED — no plan.md, no TDD, no extra coding gates.
|
|
294
|
+
|
|
295
|
+
> gen-doc reuses the same \`AK-Docs/04.Coding/\` folders as the standard Dev workflow (see \`custom/rules/project-conventions.md\`) — only Gate 1–2, no separate section.
|
|
296
|
+
|
|
297
|
+
### GATE 1 — AI Analyze & Plan Document
|
|
298
|
+
**INVOKE:** \`read-study-requirement\` skill
|
|
299
|
+
1. Load ticket context + read source code / files to understand scope
|
|
300
|
+
2. Confirm \`functionId\` (BẮT BUỘC — see "functionId & ticketId" in \`project-conventions.md\`; if it can't be derived for an ad-hoc task, ask directly) and ensure AK-Docs is on branch \`feature/[functionId]/[ticketId]\` (create via \`ak docs branch [functionId] [ticketId] --yes\` only after explicit user confirmation)
|
|
301
|
+
3. Understand the document goal, target audience, and output format
|
|
302
|
+
4. If unclear → ask ONE question at a time, wait for reply
|
|
303
|
+
5. Output \`AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md\` with:
|
|
304
|
+
- Document scope and target audience
|
|
305
|
+
- Content outline (sections to cover)
|
|
306
|
+
- Source references (files, features, flows to analyze)
|
|
307
|
+
- Output format: Markdown or Excel (note template path if applicable)
|
|
308
|
+
- Effort estimate
|
|
309
|
+
6. Display "GATE 1: Document plan ready" → wait for **APPROVED**
|
|
310
|
+
|
|
311
|
+
> **Telemetry:** Run \`ak gate 1 start --ticket [ticket-id]\` when starting.
|
|
312
|
+
> Run \`ak gate 1 approved --ticket [ticket-id]\` immediately when APPROVED is received.
|
|
313
|
+
|
|
314
|
+
### GATE 2 — Generate Document + Auto Review (runs immediately after APPROVED)
|
|
315
|
+
|
|
316
|
+
**DO NOT create plan.md. DO NOT run TDD. Proceed directly to document generation.**
|
|
317
|
+
|
|
318
|
+
1. Re-read \`AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md\` (the approved outline)
|
|
319
|
+
2. Read source code, trace data flows, analyze features as needed
|
|
320
|
+
3. Generate the output document:
|
|
321
|
+
- **Markdown:** Save to \`AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId].md\` (or the custom path/format noted in the requirement doc — still write a short pointer + summary into this file)
|
|
322
|
+
4. Self-review: verify content completeness against the approved requirement outline
|
|
323
|
+
5. Create \`AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId]-summary.md\` with a brief summary of what was generated
|
|
324
|
+
6. Submit AK-Docs via Merge Request: show the commit/MR title+description, wait for explicit user confirmation, then run \`ak docs submit --title "..." --description "..." --yes\` — PM reviews & merges into \`main\` (never merge it yourself)
|
|
325
|
+
7. Run: \`ak gate 2 approved --ticket [ticket-id]\` to close the task
|
|
326
|
+
8. Display: \`GATE 2 DONE: Document generated at [path]\`
|
|
327
|
+
|
|
328
|
+
**Do NOT wait for extra approval before generating the document — only the branch/MR confirmations above gate on the user; run the gate 2 approved command immediately after.**
|
|
284
329
|
`,
|
|
285
330
|
},
|
|
286
331
|
};
|
package/scripts/task.js
CHANGED
|
@@ -331,17 +331,22 @@ async function nextGate(taskId) {
|
|
|
331
331
|
const nextGateNum = currentGate + 1;
|
|
332
332
|
|
|
333
333
|
// Save context snapshot if current task is active
|
|
334
|
+
let taskType = null;
|
|
334
335
|
if (await fs.pathExists(CURRENT_FILE)) {
|
|
335
336
|
const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
|
|
336
337
|
if (ctx && ctx.taskId === resolvedId) {
|
|
338
|
+
taskType = ctx.taskType || null;
|
|
337
339
|
await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
const isGenDoc = taskType === 'gen-doc';
|
|
344
|
+
const maxGate = isGenDoc ? 2 : 5;
|
|
345
|
+
|
|
341
346
|
const taskState = {
|
|
342
347
|
...existing,
|
|
343
348
|
taskId: resolvedId,
|
|
344
|
-
status: 'pending',
|
|
349
|
+
status: nextGateNum > maxGate ? 'done' : 'pending',
|
|
345
350
|
updatedAt: now,
|
|
346
351
|
pausedAt: now,
|
|
347
352
|
currentGate: nextGateNum,
|
|
@@ -354,20 +359,26 @@ async function nextGate(taskId) {
|
|
|
354
359
|
|
|
355
360
|
// Generate cumulative task-summary.md (task-internal state — lives in .aiflow/, not gate docs)
|
|
356
361
|
const summaryPath = path.join(TASKS_DIR, resolvedId, 'task-summary.md');
|
|
357
|
-
const summaryContent = await generateMarkdownSummary(taskState);
|
|
362
|
+
const summaryContent = await generateMarkdownSummary(taskState, taskType);
|
|
358
363
|
await fs.writeFile(summaryPath, summaryContent, 'utf-8');
|
|
359
364
|
|
|
360
|
-
const taskType = existing.taskType || 'feature';
|
|
361
365
|
console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
|
|
362
366
|
console.log(chalk.gray(` Summary saved to: .aiflow/tasks/${resolvedId}/task-summary.md`));
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
|
|
368
|
+
if (isGenDoc && currentGate >= maxGate) {
|
|
369
|
+
console.log(chalk.green('\n ✅ Document workflow complete. Task is done.'));
|
|
370
|
+
} else {
|
|
371
|
+
const nextLabel = gateLabel(nextGateNum, taskType);
|
|
372
|
+
console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${nextLabel}`));
|
|
373
|
+
console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
|
|
374
|
+
console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
|
|
375
|
+
console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
|
|
376
|
+
console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
|
|
377
|
+
if (!isGenDoc) {
|
|
378
|
+
console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in the Gate 2 plan doc (AK-Docs/04.Coding/02.Plans/).`));
|
|
379
|
+
console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
371
382
|
console.log();
|
|
372
383
|
}
|
|
373
384
|
|
|
@@ -466,9 +477,9 @@ async function detectCurrentGate(taskId) {
|
|
|
466
477
|
return 1;
|
|
467
478
|
}
|
|
468
479
|
|
|
469
|
-
async function generateMarkdownSummary(taskState) {
|
|
470
|
-
|
|
471
|
-
const maxGate
|
|
480
|
+
async function generateMarkdownSummary(taskState, taskType = null) {
|
|
481
|
+
taskType = taskType || taskState.taskType || 'feature';
|
|
482
|
+
const maxGate = taskType === 'testing' ? 4 : (taskType === 'gen-doc' ? 2 : 5);
|
|
472
483
|
const lines = [];
|
|
473
484
|
lines.push(`# Task Summary: ${taskState.taskId}`);
|
|
474
485
|
lines.push(`**Title:** ${taskState.title}`);
|
|
@@ -492,7 +503,7 @@ async function generateMarkdownSummary(taskState) {
|
|
|
492
503
|
return lines.join('\n');
|
|
493
504
|
}
|
|
494
505
|
|
|
495
|
-
function gateLabel(n, taskType) {
|
|
506
|
+
function gateLabel(n, taskType = null) {
|
|
496
507
|
if (taskType === 'testing') {
|
|
497
508
|
const labels = {
|
|
498
509
|
1: 'Phân tích & Confirm',
|
|
@@ -502,6 +513,10 @@ function gateLabel(n, taskType) {
|
|
|
502
513
|
};
|
|
503
514
|
return labels[n] || `Gate ${n}`;
|
|
504
515
|
}
|
|
516
|
+
if (taskType === 'gen-doc') {
|
|
517
|
+
if (n === 2) return 'Generate Document';
|
|
518
|
+
if (n >= 3) return 'Done';
|
|
519
|
+
}
|
|
505
520
|
const labels = {
|
|
506
521
|
1: 'AI Analyze Requirement',
|
|
507
522
|
2: 'Implementation Plan',
|
package/scripts/use.js
CHANGED
|
@@ -234,7 +234,7 @@ async function loadFromBacklog(issueKey, options = {}) {
|
|
|
234
234
|
options.cto != null ||
|
|
235
235
|
options.commentsTo != null ||
|
|
236
236
|
options["comments-to"] != null;
|
|
237
|
-
|
|
237
|
+
|
|
238
238
|
if (loadComments) {
|
|
239
239
|
console.log(chalk.gray(" ℹ Comments requested..."));
|
|
240
240
|
}
|
|
@@ -349,13 +349,13 @@ async function fetchBacklogComments(domain, apiKey, issueKey) {
|
|
|
349
349
|
if (minId) {
|
|
350
350
|
url += `&minId=${minId}`;
|
|
351
351
|
}
|
|
352
|
-
|
|
352
|
+
|
|
353
353
|
const batch = await backlogGet(url);
|
|
354
354
|
if (!batch || batch.length === 0) break;
|
|
355
|
-
|
|
355
|
+
|
|
356
356
|
all.push(...batch);
|
|
357
357
|
if (batch.length < 100) break;
|
|
358
|
-
|
|
358
|
+
|
|
359
359
|
// Use the last ID as minId for the next batch
|
|
360
360
|
minId = batch[batch.length - 1].id;
|
|
361
361
|
}
|
|
@@ -467,7 +467,7 @@ async function loadFromJira(issueKey, options = {}) {
|
|
|
467
467
|
options.cto != null ||
|
|
468
468
|
options.commentsTo != null ||
|
|
469
469
|
options["comments-to"] != null;
|
|
470
|
-
|
|
470
|
+
|
|
471
471
|
if (loadComments) {
|
|
472
472
|
console.log(chalk.gray(" ℹ Comments requested..."));
|
|
473
473
|
}
|
|
@@ -709,6 +709,7 @@ async function manualContext(prefillId = "") {
|
|
|
709
709
|
{ name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
|
|
710
710
|
{ name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
|
|
711
711
|
{ name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
|
|
712
|
+
{ name: td("📝 Generate Doc 2 Gate", "Document", "Generate new documentation"), value: "gen-doc" },
|
|
712
713
|
],
|
|
713
714
|
default: existing.taskType || undefined,
|
|
714
715
|
});
|