@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.
Files changed (94) hide show
  1. package/README.md +139 -0
  2. package/bin/aiflow.js +89 -2
  3. package/custom/harness/playwright/.env.example +10 -0
  4. package/custom/harness/playwright/playwright.config.ts +34 -0
  5. package/custom/harness/playwright/tests/e2e/auth.setup.ts +25 -0
  6. package/custom/harness/playwright/tests/e2e/fixtures/test.ts +6 -0
  7. package/custom/harness/playwright/tests/e2e/pages/BasePage.ts +9 -0
  8. package/custom/harness/playwright/tests/e2e/support/auth.ts +5 -0
  9. package/custom/mcp-presets/playwright.json +8 -0
  10. package/custom/rules/test-patterns.md +70 -0
  11. package/custom/skills/automation-testing/SKILL.md +239 -0
  12. package/custom/skills/automation-testing/templates/BasePage.ts +29 -0
  13. package/custom/skills/automation-testing/templates/PageObject.example.ts +29 -0
  14. package/custom/skills/automation-testing/templates/playwright.config.ts +39 -0
  15. package/custom/skills/automation-testing/templates/spec.example.ts +29 -0
  16. package/custom/skills/ba-skills/skill-ba-phan-tich-ban-dau-v1.md +44 -0
  17. package/custom/skills/ba-skills/skill-ba-prototype-v1.md +35 -0
  18. package/custom/skills/ba-skills/skill-ba-qa-template-v1.md +21 -0
  19. package/custom/skills/ba-skills/skill-ba-qa-v1.md +35 -0
  20. package/custom/skills/ba-skills/skill-ba-template-phan-tich-ban-dau-v1.md +30 -0
  21. package/custom/skills/ba-skills/skill-ba-template-uc-v1.md +83 -0
  22. package/custom/skills/ba-skills/skill-ba-ve-luong-mermaid-v1.md +35 -0
  23. package/custom/skills/ba-skills/skill-ba-viet-spec-uc-v1.md +38 -0
  24. package/custom/skills/ba-skills/skill-ba-xay-dung-business-rules-v1.md +39 -0
  25. package/custom/skills/coverage-check/SKILL.md +202 -0
  26. package/custom/skills/evidence-aggregation/SKILL.md +246 -0
  27. package/custom/skills/execute-flow/SKILL.md +499 -0
  28. package/custom/skills/execute-flow/templates/playwright.config.ts +39 -0
  29. package/custom/skills/gate-review/SKILL.md +195 -0
  30. package/custom/skills/generate-test-report/SKILL.md +99 -0
  31. package/custom/skills/generate-test-report/templates/test-report.md +58 -0
  32. package/custom/skills/generate-testcase/SKILL.md +350 -0
  33. package/custom/skills/generate-testcase/templates/testcase.md +88 -0
  34. package/custom/skills/log-bug/SKILL.md +131 -0
  35. package/custom/skills/pr-impact-analysis/SKILL.md +180 -0
  36. package/custom/skills/retest-orchestration/SKILL.md +191 -0
  37. package/custom/skills/script-sync/SKILL.md +208 -0
  38. package/custom/skills/test-analysis/SKILL.md +300 -0
  39. package/custom/skills/test-skills/categories/00-core/00.01.requirement-analysis.md +111 -0
  40. package/custom/skills/test-skills/categories/00-core/00.02.risk-analysis.md +24 -0
  41. package/custom/skills/test-skills/categories/00-core/00.03.test-scenario-builder.md +90 -0
  42. package/custom/skills/test-skills/categories/00-core/00.04.testcase-review.md +68 -0
  43. package/custom/skills/test-skills/categories/01-test-design/01.01.boundary-value-analysis.md +30 -0
  44. package/custom/skills/test-skills/categories/01-test-design/01.02.equivalence.partitioning.md +27 -0
  45. package/custom/skills/test-skills/categories/01-test-design/01.03.decision-table.md +26 -0
  46. package/custom/skills/test-skills/categories/01-test-design/01.04.state.transition.md +26 -0
  47. package/custom/skills/test-skills/categories/01-test-design/01.05.pairwise.md +25 -0
  48. package/custom/skills/test-skills/categories/01-test-design/01.06.error-guessing.md +28 -0
  49. package/custom/skills/test-skills/categories/02-ui-testing/02.01.UI-layout.md +29 -0
  50. package/custom/skills/test-skills/categories/02-ui-testing/02.02.form-validation.md +57 -0
  51. package/custom/skills/test-skills/categories/02-ui-testing/02.03.navigation.md +27 -0
  52. package/custom/skills/test-skills/categories/02-ui-testing/02.04.localization.md +27 -0
  53. package/custom/skills/test-skills/categories/02-ui-testing/02.05.accessibility.md +27 -0
  54. package/custom/skills/test-skills/categories/03-business-testing/03.01.CRUD-testing.md +51 -0
  55. package/custom/skills/test-skills/categories/03-business-testing/03.02.workflow-testing.md +44 -0
  56. package/custom/skills/test-skills/categories/03-business-testing/03.03.permission-testing.md +41 -0
  57. package/custom/skills/test-skills/categories/03-business-testing/03.04.dependency-validation.md +40 -0
  58. package/custom/skills/test-skills/categories/03-business-testing/03.05.notification-testing.md +43 -0
  59. package/custom/skills/test-skills/categories/03-business-testing/03.06.calculation.testing.md +44 -0
  60. package/custom/skills/test-skills/categories/04-data-testing/04.01.database-testing.md +43 -0
  61. package/custom/skills/test-skills/categories/04-data-testing/04.02.import-export-testing.md +43 -0
  62. package/custom/skills/test-skills/categories/04-data-testing/04.03.data-mapping.md +39 -0
  63. package/custom/skills/test-skills/categories/04-data-testing/04.04.duplicate-handling.md +39 -0
  64. package/custom/skills/test-skills/categories/04-data-testing/04.05.migration.md +42 -0
  65. package/custom/skills/test-skills/categories/05-integration/05.01.API-testing.md +37 -0
  66. package/custom/skills/test-skills/categories/05-integration/05.02.third-party-integration.md +36 -0
  67. package/custom/skills/test-skills/categories/05-integration/05.03.batch-processing.md +38 -0
  68. package/custom/skills/test-skills/categories/05-integration/05.04.file-upload-download.md +40 -0
  69. package/custom/skills/test-skills/categories/08-domain-skills/CMS.md +34 -0
  70. package/custom/skills/test-skills/categories/08-domain-skills/Ecommerce.md +35 -0
  71. package/custom/skills/test-skills/categories/99-review/99.01.coverage-review.md +35 -0
  72. package/custom/skills/test-skills/categories/99-review/99.02.knowledge-learning.md +34 -0
  73. package/custom/skills/test-skills/categories/99-review/99.03.repository-update.md +33 -0
  74. package/custom/skills/test-skills/categories/99-review/99.04.change-history.md +38 -0
  75. package/custom/skills/test-skills/rules/Directory_and_Naming_Convention.md +257 -0
  76. package/custom/skills/test-skills/rules/QA_Writing_Standards.md +49 -0
  77. package/custom/skills/test-skills/template/template_testcase.md +92 -0
  78. package/custom/templates/shared/create-spec-workflow.md +441 -0
  79. package/custom/templates/shared/create-testcase-workflow.md +470 -0
  80. package/custom/templates/shared/gate-workflow.md +421 -11
  81. package/docs/common/CHANGELOG.md +52 -0
  82. package/docs/common/QUICK_START.md +30 -0
  83. package/docs/common/cli-reference.md +48 -7
  84. package/package.json +2 -2
  85. package/scripts/create-score-excel.js +37 -14
  86. package/scripts/detect.js +10 -0
  87. package/scripts/guide.js +9 -0
  88. package/scripts/init.js +48 -1
  89. package/scripts/prompt.js +36 -0
  90. package/scripts/review.js +91 -0
  91. package/scripts/scaffold-playwright.js +106 -0
  92. package/scripts/task.js +21 -7
  93. package/scripts/update.js +127 -124
  94. 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ó event nào trong tuần';
169
- else if (score === 1) notes = 'Chỉ lệnh khám phá cơ bản (init/update/version/help)';
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) notes = `Có gate.start nhưng chưa gate4.approved, ${activeDays} ngày active`;
172
- else notes = `${displayCmds.slice(0, 3).join('/')}, ${activeDays} ngày active, chưa dùng gate workflow`;
172
+ if (ticketsStarted.length > 0)
173
+ notes = `${ticketsStarted.length} ticket(s) trong tuần; 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) notes = `Gate workflow bắt đầu, ${ticketsStarted.length} ticket(s), chưa hoàn thành`;
175
- else if (score === 4) notes = `${ticketsCompleted.length} ticket hoàn thành (gate4+)`;
176
- else if (score === 5) notes = `Gate 3-5 approved, ${ticketsCompleted.length} tickets hoàn thành`;
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('Score 18-24.05.2026');
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(['AI Flow Kit — Weekly Score Report: 18/05/202624/05/2026']);
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
- 'tructth@relipasoft.com',
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
- const logFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026');
327
- const outputFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026_new.xlsx');
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 workflowContent = (await fs.pathExists(workflowPath))
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 gateLabels = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
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
- console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${gateLabels[nextGateNum] || 'Done'}`));
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 <= 5; 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,