@relipa/ai-flow-kit 0.1.2 → 0.1.3
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/README.md +26 -0
- package/bin/aiflow.js +40 -1
- package/custom/harness/playwright/.env.example +10 -0
- package/custom/harness/playwright/playwright.config.ts +34 -0
- package/custom/harness/playwright/tests/e2e/auth.setup.ts +25 -0
- package/custom/harness/playwright/tests/e2e/fixtures/test.ts +6 -0
- package/custom/harness/playwright/tests/e2e/pages/BasePage.ts +9 -0
- package/custom/harness/playwright/tests/e2e/support/auth.ts +5 -0
- package/custom/mcp-presets/playwright.json +8 -0
- package/custom/rules/test-patterns.md +70 -0
- package/custom/skills/automation-testing/SKILL.md +239 -0
- package/custom/skills/automation-testing/templates/BasePage.ts +29 -0
- package/custom/skills/automation-testing/templates/PageObject.example.ts +29 -0
- package/custom/skills/automation-testing/templates/playwright.config.ts +39 -0
- package/custom/skills/automation-testing/templates/spec.example.ts +29 -0
- package/custom/skills/coverage-check/SKILL.md +202 -0
- package/custom/skills/evidence-aggregation/SKILL.md +246 -0
- package/custom/skills/execute-flow/SKILL.md +479 -0
- package/custom/skills/execute-flow/templates/playwright.config.ts +39 -0
- package/custom/skills/generate-test-report/SKILL.md +99 -0
- package/custom/skills/generate-test-report/templates/test-report.md +58 -0
- package/custom/skills/generate-testcase/SKILL.md +303 -0
- package/custom/skills/generate-testcase/templates/testcase.md +88 -0
- package/custom/skills/log-bug/SKILL.md +131 -0
- package/custom/skills/pr-impact-analysis/SKILL.md +180 -0
- package/custom/skills/retest-orchestration/SKILL.md +191 -0
- package/custom/skills/script-sync/SKILL.md +208 -0
- package/custom/skills/test-analysis/SKILL.md +262 -0
- package/custom/templates/shared/gate-workflow.md +306 -2
- package/docs/common/CHANGELOG.md +52 -0
- package/docs/common/QUICK_START.md +30 -0
- package/docs/common/cli-reference.md +48 -7
- package/package.json +1 -1
- package/scripts/create-score-excel.js +20 -7
- package/scripts/detect.js +10 -0
- package/scripts/guide.js +9 -0
- package/scripts/prompt.js +36 -0
- package/scripts/scaffold-playwright.js +106 -0
- package/scripts/task.js +21 -7
- package/scripts/update.js +124 -124
- package/scripts/use.js +30 -12
|
@@ -17,13 +17,13 @@ ak <alias> --help # Also works with aliases
|
|
|
17
17
|
| Full command | Short | Full command | Short |
|
|
18
18
|
|---|---|---|---|
|
|
19
19
|
| `ak init` | `ak i` | `ak use` | `ak u` |
|
|
20
|
-
| `ak
|
|
21
|
-
| `ak
|
|
22
|
-
| `ak
|
|
23
|
-
| `ak
|
|
24
|
-
| `ak
|
|
25
|
-
| `ak
|
|
26
|
-
| `ak
|
|
20
|
+
| `ak execute` | `ak ex` | `ak prompt` | `ak p` |
|
|
21
|
+
| `ak detect` | `ak d` | `ak task` | `ak t` |
|
|
22
|
+
| `ak context` | `ak ctx` | `ak checkpoint` | `ak cp` |
|
|
23
|
+
| `ak validate` | `ak vl` | `ak memory` | `ak mem` |
|
|
24
|
+
| `ak guide` | `ak g` | `ak remove` | `ak rm` |
|
|
25
|
+
| `ak update` | `ak up` | `ak sync-skills` | `ak sync` |
|
|
26
|
+
| `ak doctor` | `ak dr` | `ak telemetry` | `ak tel` |
|
|
27
27
|
|
|
28
28
|
| `task` sub-command | Short | `memory` sub-command | Short |
|
|
29
29
|
|---|---|---|---|
|
|
@@ -156,6 +156,47 @@ $ aiflow use PROJ-33 PROJ-10 docs/arch.md
|
|
|
156
156
|
✓ Auto-resolved 2 link(s) from description
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
## execute (alias: `ex`)
|
|
160
|
+
|
|
161
|
+
Execute test cases via the 4-gate Executing Flow. Syncs Playwright scripts, runs them, collects evidence, and generates a test report.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
ak execute PROJ-44 # from ticket (taskType must be "execute")
|
|
165
|
+
ak execute ./testcases/AD10.md # from TC file
|
|
166
|
+
ak execute # manual — AI asks for TC file
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Arguments:**
|
|
170
|
+
- `[target]` — Optional. A ticket ID (`PROJ-44`), a path to a TC file (`./testcases/AD10.md`), or omitted for manual entry.
|
|
171
|
+
|
|
172
|
+
**What it does (4 gates):**
|
|
173
|
+
1. **Gate 1 — Pre-flight:** Parse TC file, verify `ak-test/{repo}/` and `playwright.config.ts` exist, check `BASE_URL`
|
|
174
|
+
2. **Gate 2 — Script Sync:** Hash-based sync — gen new TCs, update changed, skip unchanged. Uses Playwright MCP for real selectors (never fabricates).
|
|
175
|
+
3. **Gate 3 — Execute & Evidence:** Run Playwright, organize evidence into `run-{N}/`, update TC file R1/R2 columns (`✅ Pass` · `❌ Fail` · `⏭️ Untest` · `⏳ Pending`)
|
|
176
|
+
4. **Gate 4 — Report & Bug Logging:** Generate `testreport.md`, confirm each bug with TESTER before logging to Jira
|
|
177
|
+
|
|
178
|
+
**Output structure:**
|
|
179
|
+
```
|
|
180
|
+
ak-test/{repo}/
|
|
181
|
+
├── scripts/{screenId}/{ScreenID}.spec.ts
|
|
182
|
+
└── results/{screenId}/run-{N}/
|
|
183
|
+
├── {TC_ID}-{scenario}/ (screenshots, trace.zip, result.md)
|
|
184
|
+
├── bugs/BUG-NNN-{slug}.md
|
|
185
|
+
└── testreport.md
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
```bash
|
|
190
|
+
$ ak execute ./testcases/AD10.md
|
|
191
|
+
⏸️ GATE 1: EXECUTE FLOW READY
|
|
192
|
+
TC file: testcases/AD10.md
|
|
193
|
+
Repo: repo-fe | Screen: AD10 — Create Product
|
|
194
|
+
BASE_URL: http://localhost:3000
|
|
195
|
+
→ Type APPROVED to sync scripts (Gate 2)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
159
200
|
## fetch-links
|
|
160
201
|
|
|
161
202
|
Fetch a single Backlog/Jira URL and print a `SupplementaryContext` JSON object to stdout. Intended for AI runtime use inside the `read-study-requirement` skill — when AI encounters a link in the ticket description that wasn't already resolved by `ak use`, it can call this command and integrate the JSON output.
|
package/package.json
CHANGED
|
@@ -193,9 +193,9 @@ function buildRow(email, u) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// ── Excel writer ───────────────────────────────────────────────────────────
|
|
196
|
-
async function writeExcel(rows, outputPath) {
|
|
196
|
+
async function writeExcel(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
197
197
|
const workbook = new ExcelJS.Workbook();
|
|
198
|
-
const sheet = workbook.addWorksheet(
|
|
198
|
+
const sheet = workbook.addWorksheet(`Score ${sheetLabel}`);
|
|
199
199
|
|
|
200
200
|
sheet.columns = [
|
|
201
201
|
{ key: 'no', width: 5 },
|
|
@@ -220,7 +220,7 @@ async function writeExcel(rows, outputPath) {
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
// Title row
|
|
223
|
-
sheet.addRow([
|
|
223
|
+
sheet.addRow([`AI Flow Kit — Weekly Score Report: ${fromLabel} – ${toLabel}`]);
|
|
224
224
|
sheet.mergeCells('A1:N1');
|
|
225
225
|
const title = sheet.getCell('A1');
|
|
226
226
|
title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
|
|
@@ -296,7 +296,7 @@ const CANONICAL_USERS = [
|
|
|
296
296
|
'nguyenlt@relipasoft.com',
|
|
297
297
|
'thuongvv@relipasoft.com',
|
|
298
298
|
'yenvtb@relipasoft.com',
|
|
299
|
-
'
|
|
299
|
+
'tructh@relipasoft.com',
|
|
300
300
|
'thainq@relipasoft.com',
|
|
301
301
|
'tuoittx@relipasoft.com',
|
|
302
302
|
'hungdv@relipasoft.com',
|
|
@@ -323,8 +323,21 @@ const EMPTY_USER = (email) => ({
|
|
|
323
323
|
|
|
324
324
|
// ── Main ───────────────────────────────────────────────────────────────────
|
|
325
325
|
(async () => {
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
// Usage: node create-score-excel.js [log-file-stem]
|
|
327
|
+
// e.g.: node create-score-excel.js log_08-06-2026_14-06-2026
|
|
328
|
+
const stem = process.argv[2] || 'log_18-05-2026_24-05-2026';
|
|
329
|
+
|
|
330
|
+
// Derive display dates from stem: log_DD-MM-YYYY_DD-MM-YYYY → DD/MM/YYYY – DD/MM/YYYY
|
|
331
|
+
const dateMatch = stem.match(/log_(\d{2}-\d{2}-\d{4})_(\d{2}-\d{2}-\d{4})/);
|
|
332
|
+
const [fromLabel, toLabel] = dateMatch
|
|
333
|
+
? [dateMatch[1].replace(/-/g, '/'), dateMatch[2].replace(/-/g, '/')]
|
|
334
|
+
: ['?', '?'];
|
|
335
|
+
const sheetLabel = dateMatch
|
|
336
|
+
? `${dateMatch[1].slice(0,5).replace(/-/g,'.')}-${dateMatch[2].slice(0,5).replace(/-/g,'.')}.${dateMatch[1].slice(6,10)}`
|
|
337
|
+
: stem;
|
|
338
|
+
|
|
339
|
+
const logFile = path.join(__dirname, '..', 'tellog', stem);
|
|
340
|
+
const outputFile = path.join(__dirname, '..', 'tellog', `${stem}.xlsx`);
|
|
328
341
|
|
|
329
342
|
const rawUsers = parseLog(logFile);
|
|
330
343
|
|
|
@@ -350,5 +363,5 @@ const EMPTY_USER = (email) => ({
|
|
|
350
363
|
return buildRow(email, u);
|
|
351
364
|
});
|
|
352
365
|
|
|
353
|
-
await writeExcel(rows, outputFile);
|
|
366
|
+
await writeExcel(rows, outputFile, sheetLabel, fromLabel, toLabel);
|
|
354
367
|
})();
|
package/scripts/detect.js
CHANGED
|
@@ -64,6 +64,16 @@ class TaskDetector {
|
|
|
64
64
|
antiKeywords: ['code'],
|
|
65
65
|
icon: '📖',
|
|
66
66
|
description: 'Writing documentation'
|
|
67
|
+
},
|
|
68
|
+
'testing': {
|
|
69
|
+
keywords: [
|
|
70
|
+
'test', 'testing', 'unit test', 'integration test', 'e2e',
|
|
71
|
+
'qa', 'quality assurance', 'coverage', 'spec', 'assert',
|
|
72
|
+
'mock', 'stub', 'test case', 'test suite', 'automated test'
|
|
73
|
+
],
|
|
74
|
+
antiKeywords: [],
|
|
75
|
+
icon: '🧪',
|
|
76
|
+
description: 'Writing tests and QA'
|
|
67
77
|
}
|
|
68
78
|
};
|
|
69
79
|
}
|
package/scripts/guide.js
CHANGED
|
@@ -181,6 +181,14 @@ function showCommands() {
|
|
|
181
181
|
['aiflow detect "<description>"', '', 'Auto-detect task type'],
|
|
182
182
|
]
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
group: 'QA Execute Flow',
|
|
186
|
+
items: [
|
|
187
|
+
['aiflow execute <TICKET-ID>', '', 'Execute tests from ticket (taskType: execute)'],
|
|
188
|
+
['aiflow execute <TC-FILE>', '', 'Execute tests from TC file path'],
|
|
189
|
+
['aiflow execute', '', 'Execute tests — manual (AI asks for TC file)'],
|
|
190
|
+
]
|
|
191
|
+
},
|
|
184
192
|
{
|
|
185
193
|
group: 'Task management',
|
|
186
194
|
items: [
|
|
@@ -279,6 +287,7 @@ function showPromptGuide() {
|
|
|
279
287
|
['refactor', 'Improving code without changing behavior'],
|
|
280
288
|
['impact-analysis', 'Analyzing the impact of changes'],
|
|
281
289
|
['documentation', 'Writing docs, README, API reference'],
|
|
290
|
+
['testing', 'Writing tests, test coverage, QA'],
|
|
282
291
|
];
|
|
283
292
|
|
|
284
293
|
for (const [type, when] of types) {
|
package/scripts/prompt.js
CHANGED
|
@@ -215,6 +215,42 @@ AI must:
|
|
|
215
215
|
5. Display "GATE 1: Impact report complete" → wait for **APPROVED**
|
|
216
216
|
|
|
217
217
|
**Do NOT make any code changes.** Implementation needs a separate ticket.
|
|
218
|
+
`,
|
|
219
|
+
},
|
|
220
|
+
'testing': {
|
|
221
|
+
header: 'Testing',
|
|
222
|
+
instruction: 'Write tests for the feature or scenario described below.',
|
|
223
|
+
skillWorkflow: `
|
|
224
|
+
## STRICT GATE WORKFLOW — Testing
|
|
225
|
+
|
|
226
|
+
### GATE 1 — AI Analyze Requirement
|
|
227
|
+
**INVOKE:** \`read-study-requirement\` skill
|
|
228
|
+
AI must:
|
|
229
|
+
1. Load ticket context + read source code thoroughly
|
|
230
|
+
2. Understand what needs to be tested (unit, integration, e2e)
|
|
231
|
+
3. If unclear → ask ONE question at a time
|
|
232
|
+
4. Output \`plan/[ticket-id]/requirement.md\` with:
|
|
233
|
+
- Test scope and strategy
|
|
234
|
+
- Test cases to cover (happy path + edge cases + failure cases)
|
|
235
|
+
- Source code references
|
|
236
|
+
- Effort estimate
|
|
237
|
+
5. Display "GATE 1: Test plan ready" → wait for **APPROVED**
|
|
238
|
+
|
|
239
|
+
### GATE 2 — Implementation Plan
|
|
240
|
+
**INVOKE:** \`generate-spec\` skill, then \`superpowers:writing-plans\`
|
|
241
|
+
- Create detailed TDD test implementation plan.
|
|
242
|
+
- Output \`plan/[ticket-id]/plan.md\`.
|
|
243
|
+
- Display: "GATE 2 PAUSED: type APPROVED to start coding".
|
|
244
|
+
|
|
245
|
+
### GATE 3 — Write Tests (TDD)
|
|
246
|
+
Only runs after Gate 2 APPROVED.
|
|
247
|
+
**INVOKE:** \`superpowers:test-driven-development\`
|
|
248
|
+
- Write failing tests first → confirm FAIL → implement → confirm PASS.
|
|
249
|
+
|
|
250
|
+
### GATE 4 — AI Self-Review (wait for APPROVED)
|
|
251
|
+
**INVOKE:** \`superpowers:verification-before-completion\`
|
|
252
|
+
All tests must PASS. Check coverage meets requirements.
|
|
253
|
+
Display: "GATE 4 PAUSED: type APPROVED or BUG: [description]"
|
|
218
254
|
`,
|
|
219
255
|
},
|
|
220
256
|
'documentation': {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const PKG_DIR = path.join(__dirname, '..');
|
|
8
|
+
const HARNESS_SRC = path.join(PKG_DIR, 'custom', 'harness', 'playwright');
|
|
9
|
+
|
|
10
|
+
const HARNESS_FILES = [
|
|
11
|
+
'playwright.config.ts',
|
|
12
|
+
'.env.example',
|
|
13
|
+
'tests/e2e/pages/BasePage.ts',
|
|
14
|
+
'tests/e2e/fixtures/test.ts',
|
|
15
|
+
'tests/e2e/auth.setup.ts',
|
|
16
|
+
'tests/e2e/support/auth.ts',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const GITIGNORE_LINES = [
|
|
20
|
+
'# Playwright',
|
|
21
|
+
'playwright-report/',
|
|
22
|
+
'test-results/',
|
|
23
|
+
'tests/e2e/.auth/',
|
|
24
|
+
'.env',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const MCP_CONFIG = {
|
|
28
|
+
playwright: {
|
|
29
|
+
command: 'npx',
|
|
30
|
+
args: ['-y', '@playwright/mcp@latest', '--isolated', '--browser', 'chromium', '--headless'],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Scaffold Playwright E2E harness into a target project directory.
|
|
36
|
+
* @param {string} [targetDir] Destination directory (defaults to process.cwd())
|
|
37
|
+
* @param {{ force?: boolean, silent?: boolean }} [opts]
|
|
38
|
+
* @returns {Promise<{ copied: string[], skipped: string[], mcpUpdated: boolean }>}
|
|
39
|
+
*/
|
|
40
|
+
async function scaffoldPlaywright(targetDir, opts) {
|
|
41
|
+
if (targetDir === undefined) targetDir = process.cwd();
|
|
42
|
+
var force = opts && opts.force ? true : false;
|
|
43
|
+
var silent = opts && opts.silent ? true : false;
|
|
44
|
+
var log = silent ? function () {} : console.log;
|
|
45
|
+
|
|
46
|
+
var result = { copied: [], skipped: [], mcpUpdated: false };
|
|
47
|
+
|
|
48
|
+
// ── Harness files ─────────────────────────────────────────────
|
|
49
|
+
for (var i = 0; i < HARNESS_FILES.length; i++) {
|
|
50
|
+
var relPath = HARNESS_FILES[i];
|
|
51
|
+
var src = path.join(HARNESS_SRC, relPath);
|
|
52
|
+
var dest = path.join(targetDir, relPath);
|
|
53
|
+
var exists = await fs.pathExists(dest);
|
|
54
|
+
|
|
55
|
+
if (!force && exists) {
|
|
56
|
+
log(chalk.gray(' ↷ Skipped (already exists): ' + relPath));
|
|
57
|
+
result.skipped.push(relPath);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await fs.ensureDir(path.dirname(dest));
|
|
62
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
63
|
+
log(chalk.green(' ✓ ' + (exists ? 'Overwrote' : 'Created') + ': ' + relPath));
|
|
64
|
+
result.copied.push(relPath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── .mcp.json ─────────────────────────────────────────────────
|
|
68
|
+
var mcpFile = path.join(targetDir, '.mcp.json');
|
|
69
|
+
var mcpConfig = { mcpServers: {} };
|
|
70
|
+
if (await fs.pathExists(mcpFile)) {
|
|
71
|
+
mcpConfig = await fs.readJson(mcpFile).catch(function () { return { mcpServers: {} }; });
|
|
72
|
+
}
|
|
73
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
74
|
+
|
|
75
|
+
if (!force && mcpConfig.mcpServers.playwright) {
|
|
76
|
+
log(chalk.gray(' ↷ Skipped .mcp.json playwright entry (already exists)'));
|
|
77
|
+
} else {
|
|
78
|
+
mcpConfig.mcpServers.playwright = MCP_CONFIG.playwright;
|
|
79
|
+
await fs.writeJson(mcpFile, mcpConfig, { spaces: 2 });
|
|
80
|
+
log(chalk.green(' ✓ Updated .mcp.json with playwright MCP server'));
|
|
81
|
+
result.mcpUpdated = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── .gitignore ────────────────────────────────────────────────
|
|
85
|
+
var gitignoreFile = path.join(targetDir, '.gitignore');
|
|
86
|
+
var gitignoreContent = '';
|
|
87
|
+
if (await fs.pathExists(gitignoreFile)) {
|
|
88
|
+
gitignoreContent = await fs.readFile(gitignoreFile, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
var missingLines = GITIGNORE_LINES.filter(function (line) {
|
|
91
|
+
return !gitignoreContent.includes(line);
|
|
92
|
+
});
|
|
93
|
+
if (missingLines.length > 0) {
|
|
94
|
+
var addition = '\n' + missingLines.join('\n') + '\n';
|
|
95
|
+
await fs.appendFile(gitignoreFile, addition);
|
|
96
|
+
log(chalk.green(' ✓ Updated .gitignore'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
scaffoldPlaywright: scaffoldPlaywright,
|
|
104
|
+
HARNESS_FILES: HARNESS_FILES,
|
|
105
|
+
MCP_CONFIG: MCP_CONFIG,
|
|
106
|
+
};
|
package/scripts/task.js
CHANGED
|
@@ -189,7 +189,7 @@ async function resumeTask(taskId) {
|
|
|
189
189
|
await fs.writeJson(statePath, taskState, { spaces: 2 });
|
|
190
190
|
|
|
191
191
|
console.log(chalk.green(`✓ Resumed task: ${taskId}`));
|
|
192
|
-
console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
|
|
192
|
+
console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate, taskState.taskType)})`);
|
|
193
193
|
console.log(` ${chalk.white('Title:')} ${ctx.title.substring(0, 70)}`);
|
|
194
194
|
if (taskState.notes) console.log(` ${chalk.white('Note:')} ${taskState.notes}`);
|
|
195
195
|
console.log(chalk.gray('\n Open Claude to resume: claude'));
|
|
@@ -355,10 +355,11 @@ async function nextGate(taskId) {
|
|
|
355
355
|
const summaryContent = await generateMarkdownSummary(taskState);
|
|
356
356
|
await fs.writeFile(summaryPath, summaryContent, 'utf-8');
|
|
357
357
|
|
|
358
|
-
const
|
|
358
|
+
const taskType = existing.taskType || 'feature';
|
|
359
359
|
console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
|
|
360
360
|
console.log(chalk.gray(` Summary saved to: plan/${resolvedId}/task-summary.md`));
|
|
361
|
-
|
|
361
|
+
const nextLabel = gateLabel(nextGateNum, taskType);
|
|
362
|
+
console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${nextLabel}`));
|
|
362
363
|
console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
|
|
363
364
|
console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
|
|
364
365
|
console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
|
|
@@ -418,21 +419,24 @@ async function detectCurrentGate(taskId) {
|
|
|
418
419
|
}
|
|
419
420
|
|
|
420
421
|
async function generateMarkdownSummary(taskState) {
|
|
422
|
+
const taskType = taskState.taskType || 'feature';
|
|
423
|
+
const maxGate = taskType === 'testing' ? 4 : 5;
|
|
421
424
|
const lines = [];
|
|
422
425
|
lines.push(`# Task Summary: ${taskState.taskId}`);
|
|
423
426
|
lines.push(`**Title:** ${taskState.title}`);
|
|
427
|
+
lines.push(`**Type:** ${taskType}`);
|
|
424
428
|
lines.push(`**Status:** ${taskState.status}`);
|
|
425
|
-
lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
|
|
429
|
+
lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate, taskType)})`);
|
|
426
430
|
lines.push(`**Updated At:** ${new Date().toLocaleString()}`);
|
|
427
431
|
lines.push(``);
|
|
428
432
|
lines.push(`## Gate History`);
|
|
429
433
|
lines.push(`| Gate | Status | Approved At |`);
|
|
430
434
|
lines.push(`|------|--------|-------------|`);
|
|
431
|
-
for (let i = 1; i <=
|
|
435
|
+
for (let i = 1; i <= maxGate; i++) {
|
|
432
436
|
const approvedAt = taskState.gateApprovals && taskState.gateApprovals[String(i)];
|
|
433
437
|
const status = approvedAt ? '✅ Approved' : (i === taskState.currentGate ? '⏳ In Progress' : '⚪ Pending');
|
|
434
438
|
const timeStr = approvedAt ? new Date(approvedAt).toLocaleString() : '-';
|
|
435
|
-
lines.push(`| Gate ${i} (${gateLabel(i)}) | ${status} | ${timeStr} |`);
|
|
439
|
+
lines.push(`| Gate ${i} (${gateLabel(i, taskType)}) | ${status} | ${timeStr} |`);
|
|
436
440
|
}
|
|
437
441
|
lines.push(``);
|
|
438
442
|
lines.push(`---`);
|
|
@@ -440,7 +444,16 @@ async function generateMarkdownSummary(taskState) {
|
|
|
440
444
|
return lines.join('\n');
|
|
441
445
|
}
|
|
442
446
|
|
|
443
|
-
function gateLabel(n) {
|
|
447
|
+
function gateLabel(n, taskType) {
|
|
448
|
+
if (taskType === 'testing') {
|
|
449
|
+
const labels = {
|
|
450
|
+
1: 'Phân tích & Confirm',
|
|
451
|
+
2: 'Lập kế hoạch Test',
|
|
452
|
+
3: 'Thực thi & Evidence',
|
|
453
|
+
4: 'Báo cáo & Sign-off',
|
|
454
|
+
};
|
|
455
|
+
return labels[n] || `Gate ${n}`;
|
|
456
|
+
}
|
|
444
457
|
const labels = {
|
|
445
458
|
1: 'AI Analyze Requirement',
|
|
446
459
|
2: 'Implementation Plan',
|
|
@@ -494,6 +507,7 @@ module.exports.createOrActivateTaskState = async function createOrActivateTaskSt
|
|
|
494
507
|
...existing,
|
|
495
508
|
taskId,
|
|
496
509
|
title: ctx.title || '',
|
|
510
|
+
taskType: ctx.taskType || existing.taskType || 'feature',
|
|
497
511
|
status: 'active',
|
|
498
512
|
createdAt: existing.createdAt || now,
|
|
499
513
|
updatedAt: now,
|
package/scripts/update.js
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const { confirm, select } = require('@inquirer/prompts');
|
|
5
|
-
|
|
6
|
-
const PKG_DIR = path.join(__dirname, '..');
|
|
7
|
-
const PKG_VERSION = require('../package.json').version;
|
|
8
|
-
|
|
9
|
-
// Same language map as init.js
|
|
10
|
-
const FRAMEWORK_LANGUAGE = {
|
|
11
|
-
'laravel': 'php',
|
|
12
|
-
'spring-boot': 'java',
|
|
13
|
-
'reactjs': 'javascript',
|
|
14
|
-
'nextjs': 'javascript',
|
|
15
|
-
'vue-nuxt': 'javascript',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
module.exports = async function update(options = {}) {
|
|
19
|
-
const projectDir = process.cwd();
|
|
20
|
-
const aiflowDir = path.join(projectDir, '.aiflow');
|
|
21
|
-
const stateFile = path.join(aiflowDir, 'state.json');
|
|
22
|
-
|
|
23
|
-
if (!(await fs.pathExists(stateFile))) {
|
|
24
|
-
console.log(chalk.red('Project is not initialized. Please run `aiflow init` first.'));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const state = await fs.readJson(stateFile);
|
|
29
|
-
const currentVersion = state.current_version;
|
|
30
|
-
|
|
31
|
-
if (currentVersion === PKG_VERSION && !options.force) {
|
|
32
|
-
console.log(chalk.cyan(`You are already on v${PKG_VERSION}. Syncing latest assets and instruction files...`));
|
|
33
|
-
} else {
|
|
34
|
-
console.log(chalk.blue(`Updating from v${currentVersion} to v${PKG_VERSION}...`));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const newVersionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
|
|
38
|
-
await fs.ensureDir(newVersionDir);
|
|
39
|
-
|
|
40
|
-
// Merge skills
|
|
41
|
-
const upstreamSkills = path.join(PKG_DIR, 'upstream', 'skills');
|
|
42
|
-
const customSkills = path.join(PKG_DIR, 'custom', 'skills');
|
|
43
|
-
|
|
44
|
-
await fs.ensureDir(path.join(newVersionDir, 'skills'));
|
|
45
|
-
if (await fs.pathExists(upstreamSkills)) {
|
|
46
|
-
await fs.copy(upstreamSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
|
|
47
|
-
}
|
|
48
|
-
if (await fs.pathExists(customSkills)) {
|
|
49
|
-
await fs.copy(customSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Copy rules: common first, then language-specific overlay (mirrors init.js logic)
|
|
53
|
-
const rulesTarget = path.join(newVersionDir, 'rules');
|
|
54
|
-
const commonRules = path.join(PKG_DIR, 'custom', 'rules', 'common');
|
|
55
|
-
const allRules = path.join(PKG_DIR, 'custom', 'rules');
|
|
56
|
-
const primaryFramework = (state.frameworks || (state.framework ? [state.framework] : []))[0] || null;
|
|
57
|
-
|
|
58
|
-
await fs.ensureDir(rulesTarget);
|
|
59
|
-
if (await fs.pathExists(commonRules)) {
|
|
60
|
-
await fs.copy(commonRules, rulesTarget, { overwrite: true });
|
|
61
|
-
} else if (await fs.pathExists(allRules)) {
|
|
62
|
-
const entries = await fs.readdir(allRules, { withFileTypes: true });
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
65
|
-
await fs.copy(path.join(allRules, entry.name), path.join(rulesTarget, entry.name), { overwrite: true });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (primaryFramework) {
|
|
71
|
-
const lang = FRAMEWORK_LANGUAGE[primaryFramework];
|
|
72
|
-
if (lang) {
|
|
73
|
-
const langRules = path.join(PKG_DIR, 'custom', 'rules', lang);
|
|
74
|
-
if (await fs.pathExists(langRules)) {
|
|
75
|
-
await fs.copy(langRules, rulesTarget, { overwrite: true });
|
|
76
|
-
console.log(chalk.gray(` ✓ Applied ${lang} rules`));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
console.log(chalk.green(`✓ Downloaded assets for v${PKG_VERSION} into the project.`));
|
|
82
|
-
|
|
83
|
-
// Only offer to delete old version when it's a different version.
|
|
84
|
-
// When --force re-copies the same version, old === new === newVersionDir,
|
|
85
|
-
// so deleting "old" would immediately break the copy step below.
|
|
86
|
-
const isSameVersion = currentVersion === PKG_VERSION;
|
|
87
|
-
if (!isSameVersion) {
|
|
88
|
-
const deleteOld = await confirm({ message: `Do you want to delete the old version (v${currentVersion})?` });
|
|
89
|
-
if (deleteOld) {
|
|
90
|
-
await fs.remove(path.join(aiflowDir, 'versions', currentVersion));
|
|
91
|
-
console.log(chalk.gray(`Deleted v${currentVersion}.`));
|
|
92
|
-
} else {
|
|
93
|
-
console.log(chalk.gray(`Kept v${currentVersion}. You can switch back using 'aiflow use ${currentVersion}'.`));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Switch to new version — copy skills without wiping the whole .claude dir
|
|
98
|
-
// (wiping would destroy settings.json which holds the hooks configuration)
|
|
99
|
-
const claudeDir = path.join(projectDir, '.claude');
|
|
100
|
-
const claudeSkillsDir = path.join(claudeDir, 'skills');
|
|
101
|
-
await fs.emptyDir(claudeSkillsDir);
|
|
102
|
-
await fs.copy(path.join(newVersionDir, 'skills'), claudeSkillsDir, { overwrite: true });
|
|
103
|
-
|
|
104
|
-
const rulesDir = path.join(projectDir, '.rules');
|
|
105
|
-
await fs.emptyDir(rulesDir);
|
|
106
|
-
await fs.copy(path.join(newVersionDir, 'rules'), rulesDir, { overwrite: true });
|
|
107
|
-
|
|
108
|
-
state.current_version = PKG_VERSION;
|
|
109
|
-
await fs.writeJson(stateFile, state);
|
|
110
|
-
|
|
111
|
-
// Sync instruction files with the updated skills.
|
|
112
|
-
// Support both old 'framework' (singular string) and new 'frameworks' (array).
|
|
113
|
-
const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('./init');
|
|
114
|
-
const frameworks = state.frameworks || (state.framework ? [state.framework] : []);
|
|
115
|
-
const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
|
|
116
|
-
for (const fw of frameworks) {
|
|
117
|
-
await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools, { force: true });
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Ensure all aiflow files are gitignored (especially for projects upgrading from older versions)
|
|
121
|
-
await ensureAiflowGitignored(projectDir);
|
|
122
|
-
|
|
123
|
-
console.log(chalk.green(`\n✨ Update completed! Your project is now on v${PKG_VERSION}.`));
|
|
124
|
-
};
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { confirm, select } = require('@inquirer/prompts');
|
|
5
|
+
|
|
6
|
+
const PKG_DIR = path.join(__dirname, '..');
|
|
7
|
+
const PKG_VERSION = require('../package.json').version;
|
|
8
|
+
|
|
9
|
+
// Same language map as init.js
|
|
10
|
+
const FRAMEWORK_LANGUAGE = {
|
|
11
|
+
'laravel': 'php',
|
|
12
|
+
'spring-boot': 'java',
|
|
13
|
+
'reactjs': 'javascript',
|
|
14
|
+
'nextjs': 'javascript',
|
|
15
|
+
'vue-nuxt': 'javascript',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = async function update(options = {}) {
|
|
19
|
+
const projectDir = process.cwd();
|
|
20
|
+
const aiflowDir = path.join(projectDir, '.aiflow');
|
|
21
|
+
const stateFile = path.join(aiflowDir, 'state.json');
|
|
22
|
+
|
|
23
|
+
if (!(await fs.pathExists(stateFile))) {
|
|
24
|
+
console.log(chalk.red('Project is not initialized. Please run `aiflow init` first.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const state = await fs.readJson(stateFile);
|
|
29
|
+
const currentVersion = state.current_version;
|
|
30
|
+
|
|
31
|
+
if (currentVersion === PKG_VERSION && !options.force) {
|
|
32
|
+
console.log(chalk.cyan(`You are already on v${PKG_VERSION}. Syncing latest assets and instruction files...`));
|
|
33
|
+
} else {
|
|
34
|
+
console.log(chalk.blue(`Updating from v${currentVersion} to v${PKG_VERSION}...`));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const newVersionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
|
|
38
|
+
await fs.ensureDir(newVersionDir);
|
|
39
|
+
|
|
40
|
+
// Merge skills
|
|
41
|
+
const upstreamSkills = path.join(PKG_DIR, 'upstream', 'skills');
|
|
42
|
+
const customSkills = path.join(PKG_DIR, 'custom', 'skills');
|
|
43
|
+
|
|
44
|
+
await fs.ensureDir(path.join(newVersionDir, 'skills'));
|
|
45
|
+
if (await fs.pathExists(upstreamSkills)) {
|
|
46
|
+
await fs.copy(upstreamSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
|
|
47
|
+
}
|
|
48
|
+
if (await fs.pathExists(customSkills)) {
|
|
49
|
+
await fs.copy(customSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Copy rules: common first, then language-specific overlay (mirrors init.js logic)
|
|
53
|
+
const rulesTarget = path.join(newVersionDir, 'rules');
|
|
54
|
+
const commonRules = path.join(PKG_DIR, 'custom', 'rules', 'common');
|
|
55
|
+
const allRules = path.join(PKG_DIR, 'custom', 'rules');
|
|
56
|
+
const primaryFramework = (state.frameworks || (state.framework ? [state.framework] : []))[0] || null;
|
|
57
|
+
|
|
58
|
+
await fs.ensureDir(rulesTarget);
|
|
59
|
+
if (await fs.pathExists(commonRules)) {
|
|
60
|
+
await fs.copy(commonRules, rulesTarget, { overwrite: true });
|
|
61
|
+
} else if (await fs.pathExists(allRules)) {
|
|
62
|
+
const entries = await fs.readdir(allRules, { withFileTypes: true });
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
65
|
+
await fs.copy(path.join(allRules, entry.name), path.join(rulesTarget, entry.name), { overwrite: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (primaryFramework) {
|
|
71
|
+
const lang = FRAMEWORK_LANGUAGE[primaryFramework];
|
|
72
|
+
if (lang) {
|
|
73
|
+
const langRules = path.join(PKG_DIR, 'custom', 'rules', lang);
|
|
74
|
+
if (await fs.pathExists(langRules)) {
|
|
75
|
+
await fs.copy(langRules, rulesTarget, { overwrite: true });
|
|
76
|
+
console.log(chalk.gray(` ✓ Applied ${lang} rules`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(chalk.green(`✓ Downloaded assets for v${PKG_VERSION} into the project.`));
|
|
82
|
+
|
|
83
|
+
// Only offer to delete old version when it's a different version.
|
|
84
|
+
// When --force re-copies the same version, old === new === newVersionDir,
|
|
85
|
+
// so deleting "old" would immediately break the copy step below.
|
|
86
|
+
const isSameVersion = currentVersion === PKG_VERSION;
|
|
87
|
+
if (!isSameVersion) {
|
|
88
|
+
const deleteOld = await confirm({ message: `Do you want to delete the old version (v${currentVersion})?` });
|
|
89
|
+
if (deleteOld) {
|
|
90
|
+
await fs.remove(path.join(aiflowDir, 'versions', currentVersion));
|
|
91
|
+
console.log(chalk.gray(`Deleted v${currentVersion}.`));
|
|
92
|
+
} else {
|
|
93
|
+
console.log(chalk.gray(`Kept v${currentVersion}. You can switch back using 'aiflow use ${currentVersion}'.`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Switch to new version — copy skills without wiping the whole .claude dir
|
|
98
|
+
// (wiping would destroy settings.json which holds the hooks configuration)
|
|
99
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
100
|
+
const claudeSkillsDir = path.join(claudeDir, 'skills');
|
|
101
|
+
await fs.emptyDir(claudeSkillsDir);
|
|
102
|
+
await fs.copy(path.join(newVersionDir, 'skills'), claudeSkillsDir, { overwrite: true });
|
|
103
|
+
|
|
104
|
+
const rulesDir = path.join(projectDir, '.rules');
|
|
105
|
+
await fs.emptyDir(rulesDir);
|
|
106
|
+
await fs.copy(path.join(newVersionDir, 'rules'), rulesDir, { overwrite: true });
|
|
107
|
+
|
|
108
|
+
state.current_version = PKG_VERSION;
|
|
109
|
+
await fs.writeJson(stateFile, state);
|
|
110
|
+
|
|
111
|
+
// Sync instruction files with the updated skills.
|
|
112
|
+
// Support both old 'framework' (singular string) and new 'frameworks' (array).
|
|
113
|
+
const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('./init');
|
|
114
|
+
const frameworks = state.frameworks || (state.framework ? [state.framework] : []);
|
|
115
|
+
const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
|
|
116
|
+
for (const fw of frameworks) {
|
|
117
|
+
await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools, { force: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Ensure all aiflow files are gitignored (especially for projects upgrading from older versions)
|
|
121
|
+
await ensureAiflowGitignored(projectDir);
|
|
122
|
+
|
|
123
|
+
console.log(chalk.green(`\n✨ Update completed! Your project is now on v${PKG_VERSION}.`));
|
|
124
|
+
};
|