@relipa/ai-flow-kit 0.1.6 → 0.1.7

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 (32) hide show
  1. package/README.md +30 -4
  2. package/bin/aiflow.js +38 -34
  3. package/custom/skills/review-plan/SKILL.md +19 -0
  4. package/custom/templates/memory/CODEOWNERS +8 -0
  5. package/custom/templates/memory/ci/memory-finalize.yml +9 -0
  6. package/custom/templates/memory/ci/memory-lint.yml +10 -0
  7. package/custom/templates/memory/gitlab/merge_request_templates/memory.md +18 -0
  8. package/custom/templates/memory/memory-item.md +25 -0
  9. package/custom/templates/memory/skeleton/00.Shared/architecture/_global/.gitkeep +0 -0
  10. package/custom/templates/memory/skeleton/00.Shared/decisions/.gitkeep +0 -0
  11. package/custom/templates/memory/skeleton/00.Shared/domain/_global/.gitkeep +0 -0
  12. package/custom/templates/memory/skeleton/00.Shared/glossary/.gitkeep +0 -0
  13. package/custom/templates/memory/skeleton/01.Lessons/ba/_global/.gitkeep +0 -0
  14. package/custom/templates/memory/skeleton/01.Lessons/dev/_global/.gitkeep +0 -0
  15. package/custom/templates/memory/skeleton/01.Lessons/pm/_global/.gitkeep +0 -0
  16. package/custom/templates/memory/skeleton/01.Lessons/qa/_global/.gitkeep +0 -0
  17. package/custom/templates/memory/skeleton/02.Instincts/approved/_global/.gitkeep +0 -0
  18. package/custom/templates/memory/skeleton/03.Retro/.gitkeep +0 -0
  19. package/custom/templates/memory/skeleton/MEMORY.md +7 -0
  20. package/custom/templates/memory/skeleton/_deprecated/.gitkeep +0 -0
  21. package/custom/templates/shared/create-spec-workflow.md +13 -1
  22. package/custom/templates/shared/create-testcase-workflow.md +12 -0
  23. package/custom/templates/shared/gate-workflow.md +7 -0
  24. package/docs/common/CHANGELOG.md +16 -0
  25. package/package.json +1 -1
  26. package/scripts/create-score-excel.js +1 -1
  27. package/scripts/hooks/session-start.js +105 -2
  28. package/scripts/init.js +24 -0
  29. package/scripts/memory-store.js +391 -0
  30. package/scripts/memory.js +176 -247
  31. package/scripts/update.js +12 -0
  32. package/scripts/use.js +2 -1
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/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
@@ -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,7 +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" },
712
- { name: td("📝 Generate Doc 2 Gate", "Document", "Generate new documentation"), value: "gen-doc" },
713
+ { name: td("📝 Generate Doc/Spec", "Document", "Generate new documentation/Spec (2-gate)"), value: "gen-doc" },
713
714
  ],
714
715
  default: existing.taskType || undefined,
715
716
  });