@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
package/scripts/memory.js CHANGED
@@ -1,247 +1,176 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const chalk = require('chalk');
4
-
5
- const MEMORY_DIR = path.join(process.cwd(), '.aiflow', 'memory');
6
-
7
- async function ensureMemoryDir() {
8
- await fs.ensureDir(MEMORY_DIR);
9
- }
10
-
11
- /**
12
- * Save knowledge to memory
13
- */
14
- async function saveMemory(key, value, metadata = {}) {
15
- await ensureMemoryDir();
16
-
17
- const fileName = `${sanitizeFileName(key)}.json`;
18
- const filePath = path.join(MEMORY_DIR, fileName);
19
-
20
- const memory = {
21
- key,
22
- value,
23
- metadata: {
24
- created: new Date().toISOString(),
25
- updated: new Date().toISOString(),
26
- ...metadata
27
- }
28
- };
29
-
30
- await fs.writeJson(filePath, memory, { spaces: 2 });
31
- console.log(chalk.green(`✓ Saved: ${key}`));
32
-
33
- return memory;
34
- }
35
-
36
- /**
37
- * Get memory by key
38
- */
39
- async function getMemory(key) {
40
- await ensureMemoryDir();
41
-
42
- const fileName = `${sanitizeFileName(key)}.json`;
43
- const filePath = path.join(MEMORY_DIR, fileName);
44
-
45
- if (!(await fs.pathExists(filePath))) {
46
- return null;
47
- }
48
-
49
- return await fs.readJson(filePath);
50
- }
51
-
52
- /**
53
- * List all memories
54
- */
55
- async function listMemories() {
56
- await ensureMemoryDir();
57
-
58
- const files = await fs.readdir(MEMORY_DIR);
59
- const memories = [];
60
-
61
- for (const file of files) {
62
- if (file.endsWith('.json')) {
63
- const memory = await fs.readJson(path.join(MEMORY_DIR, file));
64
- memories.push({
65
- key: memory.key,
66
- value: memory.value.substring(0, 100) + (memory.value.length > 100 ? '...' : ''),
67
- created: memory.metadata.created,
68
- updated: memory.metadata.updated
69
- });
70
- }
71
- }
72
-
73
- return memories.sort((a, b) => new Date(b.created) - new Date(a.created));
74
- }
75
-
76
- /**
77
- * Search memories by keyword
78
- */
79
- async function searchMemories(query) {
80
- await ensureMemoryDir();
81
-
82
- const files = await fs.readdir(MEMORY_DIR);
83
- const results = [];
84
-
85
- for (const file of files) {
86
- if (file.endsWith('.json')) {
87
- const memory = await fs.readJson(path.join(MEMORY_DIR, file));
88
- const matchesKey = memory.key.toLowerCase().includes(query.toLowerCase());
89
- const matchesValue = memory.value.toLowerCase().includes(query.toLowerCase());
90
-
91
- if (matchesKey || matchesValue) {
92
- results.push({
93
- key: memory.key,
94
- value: memory.value.substring(0, 150) + (memory.value.length > 150 ? '...' : ''),
95
- matches: matchesKey ? ['key'] : [],
96
- created: memory.metadata.created
97
- });
98
- }
99
- }
100
- }
101
-
102
- return results;
103
- }
104
-
105
- /**
106
- * Delete memory
107
- */
108
- async function deleteMemory(key) {
109
- await ensureMemoryDir();
110
-
111
- const fileName = `${sanitizeFileName(key)}.json`;
112
- const filePath = path.join(MEMORY_DIR, fileName);
113
-
114
- if (!(await fs.pathExists(filePath))) {
115
- console.log(chalk.yellow(`! Memory not found: ${key}`));
116
- return false;
117
- }
118
-
119
- await fs.remove(filePath);
120
- console.log(chalk.green(`✓ Deleted: ${key}`));
121
- return true;
122
- }
123
-
124
- /**
125
- * Clear all memories
126
- */
127
- async function clearAllMemories() {
128
- await fs.emptyDir(MEMORY_DIR);
129
- console.log(chalk.green(`✓ Cleared all memories`));
130
- }
131
-
132
- /**
133
- * Auto-load relevant memories for current context
134
- */
135
- async function loadRelevantMemories(contextDescription) {
136
- const keywords = contextDescription.split(/\s+/).filter(w => w.length > 3);
137
- const allMemories = await listMemories();
138
- const relevant = [];
139
-
140
- for (const memory of allMemories) {
141
- const score = keywords.filter(k =>
142
- memory.key.toLowerCase().includes(k.toLowerCase()) ||
143
- memory.value.toLowerCase().includes(k.toLowerCase())
144
- ).length;
145
-
146
- if (score > 0) {
147
- const fullMemory = await getMemory(memory.key);
148
- relevant.push({
149
- ...memory,
150
- score,
151
- content: fullMemory.value
152
- });
153
- }
154
- }
155
-
156
- return relevant.sort((a, b) => b.score - a.score);
157
- }
158
-
159
- /**
160
- * Display memory in CLI
161
- */
162
- async function displayMemory(memory) {
163
- console.log(chalk.cyan(`\n📝 ${memory.key}`));
164
- console.log(chalk.gray(`Created: ${new Date(memory.metadata.created).toLocaleDateString()}`));
165
- console.log(chalk.gray(`Updated: ${new Date(memory.metadata.updated).toLocaleDateString()}`));
166
- console.log(chalk.white(`\n${memory.value}\n`));
167
- }
168
-
169
- /**
170
- * Sanitize file name from key
171
- */
172
- function sanitizeFileName(key) {
173
- return key
174
- .toLowerCase()
175
- .replace(/[^\w\s-]/g, '')
176
- .replace(/\s+/g, '-')
177
- .replace(/-+/g, '-')
178
- .substring(0, 50);
179
- }
180
-
181
- module.exports = async function memory(command, args = {}) {
182
- try {
183
- if (command === 'save' && args.key && args.value) {
184
- return await saveMemory(args.key, args.value, args.metadata);
185
- }
186
-
187
- if (command === 'get' && args.key) {
188
- const m = await getMemory(args.key);
189
- if (m) {
190
- await displayMemory(m);
191
- return m;
192
- } else {
193
- console.log(chalk.yellow(`! Memory not found: ${args.key}`));
194
- }
195
- }
196
-
197
- if (command === 'list') {
198
- const memories = await listMemories();
199
- if (memories.length === 0) {
200
- console.log(chalk.gray('No memories saved yet'));
201
- return;
202
- }
203
- console.log(chalk.cyan('\n📚 Saved Memories:\n'));
204
- memories.forEach((m, i) => {
205
- console.log(`${i + 1}. ${chalk.white(m.key)}`);
206
- console.log(` ${chalk.gray(m.value)}`);
207
- console.log(` ${chalk.gray('Created: ' + new Date(m.created).toLocaleDateString())}\n`);
208
- });
209
- }
210
-
211
- if (command === 'search' && args.query) {
212
- const results = await searchMemories(args.query);
213
- if (results.length === 0) {
214
- console.log(chalk.gray(`No memories matching "${args.query}"`));
215
- return;
216
- }
217
- console.log(chalk.cyan(`\n🔍 Found ${results.length} memories matching "${args.query}":\n`));
218
- results.forEach((m, i) => {
219
- console.log(`${i + 1}. ${chalk.white(m.key)}`);
220
- console.log(` ${chalk.gray(m.value)}\n`);
221
- });
222
- }
223
-
224
- if (command === 'delete' && args.key) {
225
- return await deleteMemory(args.key);
226
- }
227
-
228
- if (command === 'clear') {
229
- return await clearAllMemories();
230
- }
231
-
232
- if (command === 'auto-load' && args.context) {
233
- return await loadRelevantMemories(args.context);
234
- }
235
-
236
- } catch (err) {
237
- console.error(chalk.red(`Error: ${err.message}`));
238
- }
239
- };
240
-
241
- module.exports.saveMemory = saveMemory;
242
- module.exports.getMemory = getMemory;
243
- module.exports.listMemories = listMemories;
244
- module.exports.searchMemories = searchMemories;
245
- module.exports.deleteMemory = deleteMemory;
246
- module.exports.clearAllMemories = clearAllMemories;
247
- module.exports.loadRelevantMemories = loadRelevantMemories;
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const store = require('./memory-store');
5
+ const docsBranchCommand = require('./docs-branch');
6
+
7
+ // CLI for the "99.Memory/" Project Brain (docs/internal/Memory-Architecture-v1.0.md).
8
+ // Thin dispatch layer: business logic lives in memory-store.js, git/MR logic is reused
9
+ // from docs-branch.js (same "AI drafts — human approves via MR" pattern as `ak docs`).
10
+ //
11
+ // Replaces the legacy JSON key/value store this file used to implement
12
+ // (`.aiflow/memory/*.json`, save/get/search/delete/clear) unrelated to 99.Memory/,
13
+ // retired outright (no backward-compat shim; nothing in the kit still reads that store).
14
+
15
+ const PROJECT_DIR = process.cwd();
16
+
17
+ module.exports = async function memory(action, options = {}) {
18
+ try {
19
+ if (action === 'draft') return await draftCmd(options);
20
+ if (action === 'list') return await listCmd(options);
21
+ if (action === 'submit') return await submitCmd(options);
22
+ if (action === 'remove') return await removeCmd(options);
23
+
24
+ console.log(chalk.yellow('Usage:'));
25
+ console.log(chalk.gray(' ak memory draft --category <cat> --slug <slug> --content <text> [options]'));
26
+ console.log(chalk.gray(' ak memory list [--approved]'));
27
+ console.log(chalk.gray(' ak memory submit <pendingPath> --title "..." [--yes]'));
28
+ console.log(chalk.gray(' ak memory remove <path> [--hard] [--reason "..."] --title "..." [--yes]'));
29
+ console.log(chalk.gray(`\n Categories: ${Object.keys(store.CATEGORIES).join(', ')}`));
30
+ } catch (err) {
31
+ console.error(chalk.red(`Error: ${err.message}`));
32
+ process.exitCode = 1;
33
+ }
34
+ };
35
+
36
+ async function draftCmd(options) {
37
+ if (!options.category || !options.slug || !options.content) {
38
+ console.log(chalk.red('✗ Cần --category, --slug, và --content.'));
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ const tags = splitList(options.tags);
43
+ const workflows = splitList(options.workflows, ['all']);
44
+
45
+ const result = await store.createDraft(PROJECT_DIR, {
46
+ category: options.category,
47
+ functionId: options.functionId,
48
+ scope: options.scope,
49
+ slug: options.slug,
50
+ content: options.content,
51
+ source: options.source || '',
52
+ tags, workflows,
53
+ confidence: options.confidence !== undefined ? Number(options.confidence) : 0.5,
54
+ });
55
+
56
+ if (result.conflict) {
57
+ console.log(chalk.yellow(`⚠ Memory tương tự đã có tại: ${result.existingPath}`));
58
+ console.log(chalk.gray(' Sửa file đó trực tiếp nếu muốn cập nhật, thay vì tạo bản mới.'));
59
+ return result;
60
+ }
61
+
62
+ console.log(chalk.green(`✓ Draft đã tạo: ${result.path}`));
63
+ console.log(chalk.gray(` id: ${result.id} (local — chỉ máy này recall được cho tới khi submit)`));
64
+ return result;
65
+ }
66
+
67
+ async function listCmd(options) {
68
+ const memories = options.approved
69
+ ? await store.listApproved(PROJECT_DIR)
70
+ : await store.listDrafts(PROJECT_DIR);
71
+
72
+ if (memories.length === 0) {
73
+ console.log(chalk.gray(options.approved ? 'Chưa memory nào được approve.' : 'Chưa có draft nào trong _pending/.'));
74
+ return memories;
75
+ }
76
+
77
+ console.log(chalk.cyan(`\n${options.approved ? 'Approved memories' : 'Pending drafts (local)'}:\n`));
78
+ for (const m of memories) {
79
+ console.log(` ${chalk.white(m.id || '(no id)')} ${chalk.gray(m.relPath)}`);
80
+ console.log(` ${chalk.gray(`type=${m.type} scope=${m.scope} workflows=${m.workflows}`)}`);
81
+ }
82
+ console.log();
83
+ return memories;
84
+ }
85
+
86
+ /**
87
+ * Submit a pending draft: create a branch in AK-Docs, move the file from _pending/
88
+ * to its destination path, commit, push, open an MR — same two-step CLI already
89
+ * used by `ak docs branch` + `ak docs submit --yes` (custom/templates/shared/*.md).
90
+ */
91
+ async function submitCmd(options) {
92
+ const pendingRelPath = options._positional && options._positional[0];
93
+ if (!pendingRelPath || !options.title) {
94
+ console.log(chalk.red('✗ Cần <pendingPath> (từ `ak memory list`) --title.'));
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+
99
+ const base = store.memoryDir(PROJECT_DIR);
100
+ const pendingAbs = path.join(base, pendingRelPath);
101
+ if (!(await fs.pathExists(pendingAbs))) {
102
+ console.log(chalk.red(`✗ Không tìm thấy draft: ${pendingRelPath}`));
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+
107
+ const { data } = store.parseFrontmatter(await fs.readFile(pendingAbs, 'utf-8'));
108
+ const destRelPath = pendingRelPath.replace(/^_pending[\\/]/, '');
109
+ const destAbs = path.join(base, destRelPath);
110
+
111
+ const slug = path.basename(destRelPath, '.md');
112
+ const branchResult = await docsBranchCommand.createBranch({
113
+ functionId: 'memory', taskId: slug, repo: 'AK-Docs', yes: options.yes,
114
+ });
115
+ if (!branchResult) return; // createBranch already printed the reason / plan
116
+
117
+ await fs.ensureDir(path.dirname(destAbs));
118
+ await fs.move(pendingAbs, destAbs, { overwrite: false });
119
+
120
+ return docsBranchCommand.submitDocs({
121
+ title: options.title,
122
+ description: options.description || `Memory: ${data.id || slug}`,
123
+ repo: 'AK-Docs', yes: options.yes,
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Propose removal (Luồng 4): same branch+MR mechanism as submit, but moves the
129
+ * target file into _deprecated/ (soft, default) or deletes it outright (--hard).
130
+ */
131
+ async function removeCmd(options) {
132
+ const targetRelPath = options._positional && options._positional[0];
133
+ if (!targetRelPath || !options.title) {
134
+ console.log(chalk.red('✗ Cần <path đã approve> (từ `ak memory list --approved`) và --title.'));
135
+ process.exitCode = 1;
136
+ return;
137
+ }
138
+
139
+ const base = store.memoryDir(PROJECT_DIR);
140
+ const targetAbs = path.join(base, targetRelPath);
141
+ if (!(await fs.pathExists(targetAbs))) {
142
+ console.log(chalk.red(`✗ Không tìm thấy memory: ${targetRelPath}`));
143
+ process.exitCode = 1;
144
+ return;
145
+ }
146
+
147
+ const slug = path.basename(targetRelPath, '.md');
148
+ const branchResult = await docsBranchCommand.createBranch({
149
+ functionId: 'memory', taskId: `remove-${slug}`, repo: 'AK-Docs', yes: options.yes,
150
+ });
151
+ if (!branchResult) return;
152
+
153
+ if (options.hard) {
154
+ await fs.remove(targetAbs);
155
+ console.log(chalk.yellow(`⚠ Hard-remove: đã xóa file khỏi working tree. Lý do: ${options.reason || '(không ghi lý do)'}`));
156
+ } else {
157
+ const deprecatedAbs = path.join(base, '_deprecated', path.basename(targetRelPath));
158
+ await fs.ensureDir(path.dirname(deprecatedAbs));
159
+ await fs.move(targetAbs, deprecatedAbs, { overwrite: false });
160
+ console.log(chalk.gray(` Soft-remove: chuyển vào _deprecated/${path.basename(targetRelPath)}`));
161
+ }
162
+
163
+ return docsBranchCommand.submitDocs({
164
+ title: options.title,
165
+ description: options.description || `Remove memory: ${slug}. Lý do: ${options.reason || '(không ghi lý do)'}`,
166
+ repo: 'AK-Docs', yes: options.yes,
167
+ });
168
+ }
169
+
170
+ function splitList(value, fallback = []) {
171
+ if (!value) return fallback;
172
+ if (Array.isArray(value)) return value;
173
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
174
+ }
175
+
176
+ module.exports.store = store;
package/scripts/prompt.js CHANGED
@@ -281,6 +281,51 @@ Write documentation based on approved outline.
281
281
  **INVOKE:** \`superpowers:verification-before-completion\`
282
282
  Verify docs match code. No outdated examples.
283
283
  Display: "GATE 3: Documentation complete. Type APPROVED."
284
+ `,
285
+ },
286
+ 'gen-doc': {
287
+ header: 'Generate Document',
288
+ instruction: 'Generate a document from source code / feature analysis as described below.',
289
+ skillWorkflow: `
290
+ ## STRICT GATE WORKFLOW — Generate Document (2-Gate)
291
+
292
+ > ⚡ gen-doc uses a **2-gate flow**: Gate 1 (Requirement) → APPROVED → Gate 2 (Generate + Auto Review).
293
+ > Gate 2 runs immediately after APPROVED — no plan.md, no TDD, no extra coding gates.
294
+
295
+ > gen-doc reuses the same \`AK-Docs/04.Coding/\` folders as the standard Dev workflow (see \`custom/rules/project-conventions.md\`) — only Gate 1–2, no separate section.
296
+
297
+ ### GATE 1 — AI Analyze & Plan Document
298
+ **INVOKE:** \`read-study-requirement\` skill
299
+ 1. Load ticket context + read source code / files to understand scope
300
+ 2. Confirm \`functionId\` (BẮT BUỘC — see "functionId & ticketId" in \`project-conventions.md\`; if it can't be derived for an ad-hoc task, ask directly) and ensure AK-Docs is on branch \`feature/[functionId]/[ticketId]\` (create via \`ak docs branch [functionId] [ticketId] --yes\` only after explicit user confirmation)
301
+ 3. Understand the document goal, target audience, and output format
302
+ 4. If unclear → ask ONE question at a time, wait for reply
303
+ 5. Output \`AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md\` with:
304
+ - Document scope and target audience
305
+ - Content outline (sections to cover)
306
+ - Source references (files, features, flows to analyze)
307
+ - Output format: Markdown or Excel (note template path if applicable)
308
+ - Effort estimate
309
+ 6. Display "GATE 1: Document plan ready" → wait for **APPROVED**
310
+
311
+ > **Telemetry:** Run \`ak gate 1 start --ticket [ticket-id]\` when starting.
312
+ > Run \`ak gate 1 approved --ticket [ticket-id]\` immediately when APPROVED is received.
313
+
314
+ ### GATE 2 — Generate Document + Auto Review (runs immediately after APPROVED)
315
+
316
+ **DO NOT create plan.md. DO NOT run TDD. Proceed directly to document generation.**
317
+
318
+ 1. Re-read \`AK-Docs/04.Coding/01.Requirements/[functionId]/[ticketId].md\` (the approved outline)
319
+ 2. Read source code, trace data flows, analyze features as needed
320
+ 3. Generate the output document:
321
+ - **Markdown:** Save to \`AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId].md\` (or the custom path/format noted in the requirement doc — still write a short pointer + summary into this file)
322
+ 4. Self-review: verify content completeness against the approved requirement outline
323
+ 5. Create \`AK-Docs/04.Coding/02.Plans/[functionId]/[ticketId]-summary.md\` with a brief summary of what was generated
324
+ 6. Submit AK-Docs via Merge Request: show the commit/MR title+description, wait for explicit user confirmation, then run \`ak docs submit --title "..." --description "..." --yes\` — PM reviews & merges into \`main\` (never merge it yourself)
325
+ 7. Run: \`ak gate 2 approved --ticket [ticket-id]\` to close the task
326
+ 8. Display: \`GATE 2 DONE: Document generated at [path]\`
327
+
328
+ **Do NOT wait for extra approval before generating the document — only the branch/MR confirmations above gate on the user; run the gate 2 approved command immediately after.**
284
329
  `,
285
330
  },
286
331
  };
package/scripts/task.js CHANGED
@@ -331,17 +331,22 @@ async function nextGate(taskId) {
331
331
  const nextGateNum = currentGate + 1;
332
332
 
333
333
  // Save context snapshot if current task is active
334
+ let taskType = null;
334
335
  if (await fs.pathExists(CURRENT_FILE)) {
335
336
  const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
336
337
  if (ctx && ctx.taskId === resolvedId) {
338
+ taskType = ctx.taskType || null;
337
339
  await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
338
340
  }
339
341
  }
340
342
 
343
+ const isGenDoc = taskType === 'gen-doc';
344
+ const maxGate = isGenDoc ? 2 : 5;
345
+
341
346
  const taskState = {
342
347
  ...existing,
343
348
  taskId: resolvedId,
344
- status: 'pending',
349
+ status: nextGateNum > maxGate ? 'done' : 'pending',
345
350
  updatedAt: now,
346
351
  pausedAt: now,
347
352
  currentGate: nextGateNum,
@@ -354,20 +359,26 @@ async function nextGate(taskId) {
354
359
 
355
360
  // Generate cumulative task-summary.md (task-internal state — lives in .aiflow/, not gate docs)
356
361
  const summaryPath = path.join(TASKS_DIR, resolvedId, 'task-summary.md');
357
- const summaryContent = await generateMarkdownSummary(taskState);
362
+ const summaryContent = await generateMarkdownSummary(taskState, taskType);
358
363
  await fs.writeFile(summaryPath, summaryContent, 'utf-8');
359
364
 
360
- const taskType = existing.taskType || 'feature';
361
365
  console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
362
366
  console.log(chalk.gray(` Summary saved to: .aiflow/tasks/${resolvedId}/task-summary.md`));
363
- const nextLabel = gateLabel(nextGateNum, taskType);
364
- console.log(chalk.white(`\n Next: Gate ${nextGateNum} ${nextLabel}`));
365
- console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
366
- console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
367
- console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
368
- console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
369
- console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in the Gate 2 plan doc (AK-Docs/04.Coding/02.Plans/).`));
370
- console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
367
+
368
+ if (isGenDoc && currentGate >= maxGate) {
369
+ console.log(chalk.green('\n Document workflow complete. Task is done.'));
370
+ } else {
371
+ const nextLabel = gateLabel(nextGateNum, taskType);
372
+ console.log(chalk.white(`\n Next: Gate ${nextGateNum} ${nextLabel}`));
373
+ console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
374
+ console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
375
+ console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
376
+ console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
377
+ if (!isGenDoc) {
378
+ console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in the Gate 2 plan doc (AK-Docs/04.Coding/02.Plans/).`));
379
+ console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
380
+ }
381
+ }
371
382
  console.log();
372
383
  }
373
384
 
@@ -466,9 +477,9 @@ async function detectCurrentGate(taskId) {
466
477
  return 1;
467
478
  }
468
479
 
469
- async function generateMarkdownSummary(taskState) {
470
- const taskType = taskState.taskType || 'feature';
471
- const maxGate = taskType === 'testing' ? 4 : 5;
480
+ async function generateMarkdownSummary(taskState, taskType = null) {
481
+ taskType = taskType || taskState.taskType || 'feature';
482
+ const maxGate = taskType === 'testing' ? 4 : (taskType === 'gen-doc' ? 2 : 5);
472
483
  const lines = [];
473
484
  lines.push(`# Task Summary: ${taskState.taskId}`);
474
485
  lines.push(`**Title:** ${taskState.title}`);
@@ -492,7 +503,7 @@ async function generateMarkdownSummary(taskState) {
492
503
  return lines.join('\n');
493
504
  }
494
505
 
495
- function gateLabel(n, taskType) {
506
+ function gateLabel(n, taskType = null) {
496
507
  if (taskType === 'testing') {
497
508
  const labels = {
498
509
  1: 'Phân tích & Confirm',
@@ -502,6 +513,10 @@ function gateLabel(n, taskType) {
502
513
  };
503
514
  return labels[n] || `Gate ${n}`;
504
515
  }
516
+ if (taskType === 'gen-doc') {
517
+ if (n === 2) return 'Generate Document';
518
+ if (n >= 3) return 'Done';
519
+ }
505
520
  const labels = {
506
521
  1: 'AI Analyze Requirement',
507
522
  2: 'Implementation Plan',
package/scripts/update.js CHANGED
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const { confirm, select } = require('@inquirer/prompts');
5
5
  const { syncDocsRepos } = require('./docs-repo');
6
+ const memoryStore = require('./memory-store');
6
7
 
7
8
  const PKG_DIR = path.join(__dirname, '..');
8
9
  const PKG_VERSION = require('../package.json').version;
@@ -29,6 +30,17 @@ module.exports = async function update(options = {}) {
29
30
  // ── Sync AK-Docs / Shared-Docs sibling repos ──────────────────
30
31
  await syncDocsRepos(projectDir);
31
32
 
33
+ // ── Bootstrap 99.Memory/ skeleton if AK-Docs was cloned after init ────
34
+ const memorySkeleton = await memoryStore.ensureSkeleton(projectDir);
35
+ if (memorySkeleton.created) {
36
+ console.log(chalk.green('✓ Đã tạo khung 99.Memory/ trong AK-Docs (chưa có ghi nhớ nào).'));
37
+ const gitignoreResult = await memoryStore.ensureMemoryGitignored(projectDir);
38
+ if (memorySkeleton.committed || gitignoreResult.committed) {
39
+ console.log(chalk.yellow(' ⚠ Đã commit local trên nhánh hiện tại của AK-Docs — `main` là protected branch,'));
40
+ console.log(chalk.yellow(' hãy tự `git push` khi sẵn sàng (không tự động push).'));
41
+ }
42
+ }
43
+
32
44
  const state = await fs.readJson(stateFile);
33
45
  const currentVersion = state.current_version;
34
46
 
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
  }
@@ -636,6 +636,7 @@ async function promptForTaskType(detectedDefault) {
636
636
  { name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
637
637
  { name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
638
638
  { name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
639
+ { name: td("📝 Generate Doc/Spec", "Document", "Generate new documentation/Spec (2-gate)"), value: "gen-doc" },
639
640
  ],
640
641
  default: detectedDefault || "feature",
641
642
  });
@@ -709,6 +710,7 @@ async function manualContext(prefillId = "") {
709
710
  { name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
710
711
  { name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
711
712
  { name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
713
+ { name: td("📝 Generate Doc/Spec", "Document", "Generate new documentation/Spec (2-gate)"), value: "gen-doc" },
712
714
  ],
713
715
  default: existing.taskType || undefined,
714
716
  });