@relipa/ai-flow-kit 0.1.2 → 0.1.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/bin/aiflow.js +89 -2
- 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/ba-skills/skill-ba-phan-tich-ban-dau-v1.md +44 -0
- package/custom/skills/ba-skills/skill-ba-prototype-v1.md +35 -0
- package/custom/skills/ba-skills/skill-ba-qa-template-v1.md +21 -0
- package/custom/skills/ba-skills/skill-ba-qa-v1.md +35 -0
- package/custom/skills/ba-skills/skill-ba-template-phan-tich-ban-dau-v1.md +30 -0
- package/custom/skills/ba-skills/skill-ba-template-uc-v1.md +83 -0
- package/custom/skills/ba-skills/skill-ba-ve-luong-mermaid-v1.md +35 -0
- package/custom/skills/ba-skills/skill-ba-viet-spec-uc-v1.md +38 -0
- package/custom/skills/ba-skills/skill-ba-xay-dung-business-rules-v1.md +39 -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 +499 -0
- package/custom/skills/execute-flow/templates/playwright.config.ts +39 -0
- package/custom/skills/gate-review/SKILL.md +195 -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 +350 -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 +300 -0
- package/custom/skills/test-skills/categories/00-core/00.01.requirement-analysis.md +111 -0
- package/custom/skills/test-skills/categories/00-core/00.02.risk-analysis.md +24 -0
- package/custom/skills/test-skills/categories/00-core/00.03.test-scenario-builder.md +90 -0
- package/custom/skills/test-skills/categories/00-core/00.04.testcase-review.md +68 -0
- package/custom/skills/test-skills/categories/01-test-design/01.01.boundary-value-analysis.md +30 -0
- package/custom/skills/test-skills/categories/01-test-design/01.02.equivalence.partitioning.md +27 -0
- package/custom/skills/test-skills/categories/01-test-design/01.03.decision-table.md +26 -0
- package/custom/skills/test-skills/categories/01-test-design/01.04.state.transition.md +26 -0
- package/custom/skills/test-skills/categories/01-test-design/01.05.pairwise.md +25 -0
- package/custom/skills/test-skills/categories/01-test-design/01.06.error-guessing.md +28 -0
- package/custom/skills/test-skills/categories/02-ui-testing/02.01.UI-layout.md +29 -0
- package/custom/skills/test-skills/categories/02-ui-testing/02.02.form-validation.md +57 -0
- package/custom/skills/test-skills/categories/02-ui-testing/02.03.navigation.md +27 -0
- package/custom/skills/test-skills/categories/02-ui-testing/02.04.localization.md +27 -0
- package/custom/skills/test-skills/categories/02-ui-testing/02.05.accessibility.md +27 -0
- package/custom/skills/test-skills/categories/03-business-testing/03.01.CRUD-testing.md +51 -0
- package/custom/skills/test-skills/categories/03-business-testing/03.02.workflow-testing.md +44 -0
- package/custom/skills/test-skills/categories/03-business-testing/03.03.permission-testing.md +41 -0
- package/custom/skills/test-skills/categories/03-business-testing/03.04.dependency-validation.md +40 -0
- package/custom/skills/test-skills/categories/03-business-testing/03.05.notification-testing.md +43 -0
- package/custom/skills/test-skills/categories/03-business-testing/03.06.calculation.testing.md +44 -0
- package/custom/skills/test-skills/categories/04-data-testing/04.01.database-testing.md +43 -0
- package/custom/skills/test-skills/categories/04-data-testing/04.02.import-export-testing.md +43 -0
- package/custom/skills/test-skills/categories/04-data-testing/04.03.data-mapping.md +39 -0
- package/custom/skills/test-skills/categories/04-data-testing/04.04.duplicate-handling.md +39 -0
- package/custom/skills/test-skills/categories/04-data-testing/04.05.migration.md +42 -0
- package/custom/skills/test-skills/categories/05-integration/05.01.API-testing.md +37 -0
- package/custom/skills/test-skills/categories/05-integration/05.02.third-party-integration.md +36 -0
- package/custom/skills/test-skills/categories/05-integration/05.03.batch-processing.md +38 -0
- package/custom/skills/test-skills/categories/05-integration/05.04.file-upload-download.md +40 -0
- package/custom/skills/test-skills/categories/08-domain-skills/CMS.md +34 -0
- package/custom/skills/test-skills/categories/08-domain-skills/Ecommerce.md +35 -0
- package/custom/skills/test-skills/categories/99-review/99.01.coverage-review.md +35 -0
- package/custom/skills/test-skills/categories/99-review/99.02.knowledge-learning.md +34 -0
- package/custom/skills/test-skills/categories/99-review/99.03.repository-update.md +33 -0
- package/custom/skills/test-skills/categories/99-review/99.04.change-history.md +38 -0
- package/custom/skills/test-skills/rules/Directory_and_Naming_Convention.md +257 -0
- package/custom/skills/test-skills/rules/QA_Writing_Standards.md +49 -0
- package/custom/skills/test-skills/template/template_testcase.md +92 -0
- package/custom/templates/shared/create-spec-workflow.md +441 -0
- package/custom/templates/shared/create-testcase-workflow.md +470 -0
- package/custom/templates/shared/gate-workflow.md +421 -11
- 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 +2 -2
- package/scripts/create-score-excel.js +37 -14
- package/scripts/detect.js +10 -0
- package/scripts/guide.js +9 -0
- package/scripts/init.js +48 -1
- package/scripts/prompt.js +36 -0
- package/scripts/review.js +91 -0
- package/scripts/scaffold-playwright.js +106 -0
- package/scripts/task.js +21 -7
- package/scripts/update.js +127 -124
- package/scripts/use.js +36 -12
|
@@ -164,16 +164,25 @@ function buildRow(email, u) {
|
|
|
164
164
|
const ticketsStr = ticketsCompleted.length ? ticketsCompleted.join(', ') : '';
|
|
165
165
|
|
|
166
166
|
// Notes
|
|
167
|
+
const onlyExploreCmds = [...rawCmds].every(c => EXPLORE_CMDS.has(c));
|
|
167
168
|
let notes = '';
|
|
168
|
-
if (score === 0) notes = 'Không có
|
|
169
|
-
else if (score === 1) notes = 'Chỉ
|
|
169
|
+
if (score === 0) notes = 'Không có hoạt động';
|
|
170
|
+
else if (score === 1) notes = 'Chỉ dùng lệnh khám phá';
|
|
170
171
|
else if (score === 2) {
|
|
171
|
-
if (ticketsStarted.length > 0)
|
|
172
|
-
|
|
172
|
+
if (ticketsStarted.length > 0)
|
|
173
|
+
notes = `${ticketsStarted.length} ticket(s) trong tuần; Có gate.start nhưng chưa hoàn thành ticket`;
|
|
174
|
+
else if (onlyExploreCmds)
|
|
175
|
+
notes = 'Chỉ dùng lệnh khám phá';
|
|
173
176
|
}
|
|
174
|
-
else if (score === 3)
|
|
175
|
-
|
|
176
|
-
else if (score ===
|
|
177
|
+
else if (score === 3)
|
|
178
|
+
notes = `${ticketsStarted.length} ticket(s) trong tuần; Có gate.start nhưng chưa hoàn thành ticket`;
|
|
179
|
+
else if (score === 4) {
|
|
180
|
+
notes = `Gate 3-5 approved; ${ticketsCompleted.length} ticket(s) hoàn thành`;
|
|
181
|
+
if (ticketsStarted.length > ticketsCompleted.length)
|
|
182
|
+
notes += `; ${ticketsStarted.length} ticket(s) trong tuần`;
|
|
183
|
+
}
|
|
184
|
+
else if (score === 5)
|
|
185
|
+
notes = `Gate 3-5 approved; ${ticketsCompleted.length} ticket(s) hoàn thành; ${ticketsStarted.length} ticket(s) trong tuần`;
|
|
177
186
|
|
|
178
187
|
return {
|
|
179
188
|
email,
|
|
@@ -193,9 +202,9 @@ function buildRow(email, u) {
|
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
// ── Excel writer ───────────────────────────────────────────────────────────
|
|
196
|
-
async function writeExcel(rows, outputPath) {
|
|
205
|
+
async function writeExcel(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
197
206
|
const workbook = new ExcelJS.Workbook();
|
|
198
|
-
const sheet = workbook.addWorksheet(
|
|
207
|
+
const sheet = workbook.addWorksheet(`Score ${sheetLabel}`);
|
|
199
208
|
|
|
200
209
|
sheet.columns = [
|
|
201
210
|
{ key: 'no', width: 5 },
|
|
@@ -220,7 +229,7 @@ async function writeExcel(rows, outputPath) {
|
|
|
220
229
|
};
|
|
221
230
|
|
|
222
231
|
// Title row
|
|
223
|
-
sheet.addRow([
|
|
232
|
+
sheet.addRow([`AI Flow Kit — Weekly Score Report: ${fromLabel} – ${toLabel}`]);
|
|
224
233
|
sheet.mergeCells('A1:N1');
|
|
225
234
|
const title = sheet.getCell('A1');
|
|
226
235
|
title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
|
|
@@ -296,7 +305,7 @@ const CANONICAL_USERS = [
|
|
|
296
305
|
'nguyenlt@relipasoft.com',
|
|
297
306
|
'thuongvv@relipasoft.com',
|
|
298
307
|
'yenvtb@relipasoft.com',
|
|
299
|
-
'
|
|
308
|
+
'tructh@relipasoft.com',
|
|
300
309
|
'thainq@relipasoft.com',
|
|
301
310
|
'tuoittx@relipasoft.com',
|
|
302
311
|
'hungdv@relipasoft.com',
|
|
@@ -315,6 +324,7 @@ const CANONICAL_USERS = [
|
|
|
315
324
|
const LOG_EMAIL_ALIAS = {
|
|
316
325
|
'tructh@relipasoft.com': 'tructth@relipasoft.com',
|
|
317
326
|
'duongnt1@relipasoft.com': 'duongnt@relipasoft.com',
|
|
327
|
+
'anhtt2@relipasoft.com': 'anhtt@relipasoft.com',
|
|
318
328
|
};
|
|
319
329
|
|
|
320
330
|
const EMPTY_USER = (email) => ({
|
|
@@ -323,8 +333,21 @@ const EMPTY_USER = (email) => ({
|
|
|
323
333
|
|
|
324
334
|
// ── Main ───────────────────────────────────────────────────────────────────
|
|
325
335
|
(async () => {
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
// Usage: node create-score-excel.js [log-file-stem]
|
|
337
|
+
// e.g.: node create-score-excel.js log_08-06-2026_14-06-2026
|
|
338
|
+
const stem = process.argv[2] || 'log_18-05-2026_24-05-2026';
|
|
339
|
+
|
|
340
|
+
// Derive display dates from stem: log_DD-MM-YYYY_DD-MM-YYYY → DD/MM/YYYY – DD/MM/YYYY
|
|
341
|
+
const dateMatch = stem.match(/log_(\d{2}-\d{2}-\d{4})_(\d{2}-\d{2}-\d{4})/);
|
|
342
|
+
const [fromLabel, toLabel] = dateMatch
|
|
343
|
+
? [dateMatch[1].replace(/-/g, '/'), dateMatch[2].replace(/-/g, '/')]
|
|
344
|
+
: ['?', '?'];
|
|
345
|
+
const sheetLabel = dateMatch
|
|
346
|
+
? `${dateMatch[1].slice(0,5).replace(/-/g,'.')}-${dateMatch[2].slice(0,5).replace(/-/g,'.')}.${dateMatch[1].slice(6,10)}`
|
|
347
|
+
: stem;
|
|
348
|
+
|
|
349
|
+
const logFile = path.join(__dirname, '..', 'tellog', stem);
|
|
350
|
+
const outputFile = path.join(__dirname, '..', 'tellog', `${stem}.xlsx`);
|
|
328
351
|
|
|
329
352
|
const rawUsers = parseLog(logFile);
|
|
330
353
|
|
|
@@ -350,5 +373,5 @@ const EMPTY_USER = (email) => ({
|
|
|
350
373
|
return buildRow(email, u);
|
|
351
374
|
});
|
|
352
375
|
|
|
353
|
-
await writeExcel(rows, outputFile);
|
|
376
|
+
await writeExcel(rows, outputFile, sheetLabel, fromLabel, toLabel);
|
|
354
377
|
})();
|
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/init.js
CHANGED
|
@@ -285,6 +285,37 @@ async function verifySuperpowersSkills(versionDir) {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Install Claude Code custom commands from shared workflow templates.
|
|
290
|
+
* Creates .claude/commands/<name>.md for each shared workflow template.
|
|
291
|
+
* Users can invoke these with /<name> in Claude Code.
|
|
292
|
+
*/
|
|
293
|
+
async function setupClaudeCommands(projectDir) {
|
|
294
|
+
const commandsDir = path.join(projectDir, '.claude', 'commands');
|
|
295
|
+
await fs.ensureDir(commandsDir);
|
|
296
|
+
|
|
297
|
+
const commands = [
|
|
298
|
+
{
|
|
299
|
+
src: path.join(PKG_DIR, 'custom', 'templates', 'shared', 'create-spec-workflow.md'),
|
|
300
|
+
dest: 'create-spec.md',
|
|
301
|
+
description: 'BA 4-Gate Spec Creation Workflow — converts raw requirements (Jira/Backlog/file) into complete UC Spec documents following the UCflow process: Gate 1 (analysis & Q&A), Gate 2 (Q&A loop until all Confirmed), Gate 3 (HTML/CSS prototype design), Gate 4 (full UC Spec). Reads BA sub-skills from .claude/skills/ba-skills/.',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
src: path.join(PKG_DIR, 'custom', 'templates', 'shared', 'create-testcase-workflow.md'),
|
|
305
|
+
dest: 'create-testcase.md',
|
|
306
|
+
description: 'QA 4-Gate TestCase Creation Workflow — converts raw requirements (SRS/Backlog/Jira/Spec file) into a complete, execution-ready Test Case set following the QA Workflow standard: Gate 1 (requirement & risk analysis), Gate 2 (scenario building), Gate 3 (detailed test case design), Gate 4 (review & optimization). Reads QA sub-skills from .claude/skills/test-skills/.',
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
for (const cmd of commands) {
|
|
311
|
+
if (!(await fs.pathExists(cmd.src))) continue;
|
|
312
|
+
const body = await fs.readFile(cmd.src, 'utf-8');
|
|
313
|
+
const content = `---\ndescription: ${cmd.description}\n---\n\n${body}`;
|
|
314
|
+
await fs.writeFile(path.join(commandsDir, cmd.dest), content);
|
|
315
|
+
console.log(chalk.green(`✓ Installed /${path.basename(cmd.dest, '.md')} command → .claude/commands/${cmd.dest}`));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
288
319
|
|
|
289
320
|
async function generateSkillRegistry(projectDir) {
|
|
290
321
|
const skillsDir = path.join(projectDir, '.claude', 'skills');
|
|
@@ -332,10 +363,24 @@ async function setupFramework(projectDir, framework, multi = false, selectedTool
|
|
|
332
363
|
const frameworkContent = await fs.readFile(frameworkTemplatePath, 'utf-8');
|
|
333
364
|
const workflowFile = resolveWorkflowFile(framework);
|
|
334
365
|
const workflowPath = path.join(PKG_DIR, 'custom', 'templates', 'shared', workflowFile);
|
|
335
|
-
const
|
|
366
|
+
const gateWorkflowContent = (await fs.pathExists(workflowPath))
|
|
336
367
|
? await fs.readFile(workflowPath, 'utf-8')
|
|
337
368
|
: '';
|
|
338
369
|
|
|
370
|
+
// Load BA spec creation workflow (appended to all instruction files)
|
|
371
|
+
const createSpecPath = path.join(PKG_DIR, 'custom', 'templates', 'shared', 'create-spec-workflow.md');
|
|
372
|
+
const createSpecContent = (await fs.pathExists(createSpecPath))
|
|
373
|
+
? await fs.readFile(createSpecPath, 'utf-8')
|
|
374
|
+
: '';
|
|
375
|
+
|
|
376
|
+
// Load QA testcase creation workflow (appended to all instruction files)
|
|
377
|
+
const createTestcasePath = path.join(PKG_DIR, 'custom', 'templates', 'shared', 'create-testcase-workflow.md');
|
|
378
|
+
const createTestcaseContent = (await fs.pathExists(createTestcasePath))
|
|
379
|
+
? await fs.readFile(createTestcasePath, 'utf-8')
|
|
380
|
+
: '';
|
|
381
|
+
|
|
382
|
+
const workflowContent = [gateWorkflowContent, createSpecContent, createTestcaseContent].filter(Boolean).join('\n\n---\n\n');
|
|
383
|
+
|
|
339
384
|
const separator = `\n\n---\n\n`;
|
|
340
385
|
const skillRegistry = await generateSkillRegistry(projectDir);
|
|
341
386
|
|
|
@@ -1047,6 +1092,7 @@ async function init(options) {
|
|
|
1047
1092
|
await verifySuperpowersSkills(versionDir);
|
|
1048
1093
|
await updateSymlinks(projectDir, versionDir);
|
|
1049
1094
|
await copyDocsToProject(projectDir);
|
|
1095
|
+
await setupClaudeCommands(projectDir);
|
|
1050
1096
|
|
|
1051
1097
|
const selectedTools = options.aiTools || Object.keys(AI_TOOL_FILES);
|
|
1052
1098
|
for (const fw of frameworks) {
|
|
@@ -1103,6 +1149,7 @@ module.exports.AI_TOOL_FILES = AI_TOOL_FILES;
|
|
|
1103
1149
|
module.exports.detectRtk = detectRtk;
|
|
1104
1150
|
module.exports.isRtkHookConfigured = isRtkHookConfigured;
|
|
1105
1151
|
module.exports.isGitNexusConfigured = isGitNexusConfigured;
|
|
1152
|
+
module.exports.setupClaudeCommands = setupClaudeCommands;
|
|
1106
1153
|
module.exports.setupFramework = setupFramework;
|
|
1107
1154
|
module.exports.ensureAiflowGitignored = ensureAiflowGitignored;
|
|
1108
1155
|
module.exports.resolveWorkflowFile = resolveWorkflowFile;
|
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,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const REVIEW_DIR = '.aiflow/review';
|
|
7
|
+
|
|
8
|
+
function reviewFilePath(gate, ticketId) {
|
|
9
|
+
return path.join(process.cwd(), REVIEW_DIR, `gate-${gate}-${ticketId}.md`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a gate review file and return checkbox state.
|
|
14
|
+
*
|
|
15
|
+
* @param {number} gate
|
|
16
|
+
* @param {string} ticketId
|
|
17
|
+
* @returns {Promise<{exists: boolean, passed?: boolean, total?: number, unchecked?: string[], commented?: Array<{item: string, comment: string}>}>}
|
|
18
|
+
*/
|
|
19
|
+
async function checkReviewFile(gate, ticketId) {
|
|
20
|
+
const filePath = reviewFilePath(gate, ticketId);
|
|
21
|
+
|
|
22
|
+
if (!(await fs.pathExists(filePath))) {
|
|
23
|
+
return { exists: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
27
|
+
const lines = content.split('\n');
|
|
28
|
+
|
|
29
|
+
const unchecked = [];
|
|
30
|
+
const commented = [];
|
|
31
|
+
let total = 0;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const line = lines[i];
|
|
35
|
+
const checkboxMatch = line.match(/^- \[([ xX])\] (.+)/);
|
|
36
|
+
if (!checkboxMatch) continue;
|
|
37
|
+
|
|
38
|
+
total++;
|
|
39
|
+
const isChecked = checkboxMatch[1].toLowerCase() === 'x';
|
|
40
|
+
const itemText = checkboxMatch[2].trim();
|
|
41
|
+
|
|
42
|
+
if (!isChecked) {
|
|
43
|
+
let comment = null;
|
|
44
|
+
let commentBuffer = '';
|
|
45
|
+
let inComment = false;
|
|
46
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
47
|
+
const nextLine = lines[j].trim();
|
|
48
|
+
if (!inComment && nextLine.match(/^- \[/)) break;
|
|
49
|
+
if (!inComment && nextLine === '---') break;
|
|
50
|
+
if (!inComment) {
|
|
51
|
+
const singleLineMatch = nextLine.match(/<!--([\s\S]+?)-->/);
|
|
52
|
+
if (singleLineMatch) {
|
|
53
|
+
comment = singleLineMatch[1].trim();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (nextLine.includes('<!--')) {
|
|
57
|
+
inComment = true;
|
|
58
|
+
commentBuffer = nextLine.replace(/^.*<!--/, '');
|
|
59
|
+
if (commentBuffer.includes('-->')) {
|
|
60
|
+
comment = commentBuffer.replace(/-->.*$/, '').trim();
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
if (nextLine.includes('-->')) {
|
|
67
|
+
commentBuffer += ' ' + nextLine.replace(/-->.*$/, '');
|
|
68
|
+
comment = commentBuffer.trim();
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
commentBuffer += ' ' + nextLine;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (comment) {
|
|
75
|
+
commented.push({ item: itemText, comment });
|
|
76
|
+
} else {
|
|
77
|
+
unchecked.push(itemText);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
exists: true,
|
|
84
|
+
passed: unchecked.length === 0 && commented.length === 0,
|
|
85
|
+
total,
|
|
86
|
+
unchecked,
|
|
87
|
+
commented,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { checkReviewFile, reviewFilePath };
|
|
@@ -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,
|