@relipa/ai-flow-kit 0.1.9 → 0.2.0-beta.1

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 (55) hide show
  1. package/README.md +65 -0
  2. package/bin/aiflow.js +74 -4
  3. package/custom/rules/output-language.md +36 -0
  4. package/custom/rules/project-conventions.md +16 -0
  5. package/custom/skills/automation-testing/SKILL.md +1 -1
  6. package/custom/skills/ba-skills/skill-ba-build-business-rules-v1.md +2 -0
  7. package/custom/skills/ba-skills/skill-ba-initial-analysis-v1.md +2 -0
  8. package/custom/skills/ba-skills/skill-ba-mermaid-flowchart-v1.md +2 -0
  9. package/custom/skills/ba-skills/skill-ba-qna-v1.md +2 -0
  10. package/custom/skills/ba-skills/skill-ba-ui-prototype-v1.md +2 -0
  11. package/custom/skills/ba-skills/skill-ba-write-uc-spec-v1.md +2 -0
  12. package/custom/skills/coverage-check/SKILL.md +2 -0
  13. package/custom/skills/create-system-requirement/SKILL.md +252 -0
  14. package/custom/skills/deploy-model/SKILL.md +2 -0
  15. package/custom/skills/design-experiment/SKILL.md +2 -0
  16. package/custom/skills/evaluate-model/SKILL.md +2 -0
  17. package/custom/skills/evidence-aggregation/SKILL.md +2 -0
  18. package/custom/skills/execute-flow/SKILL.md +2 -0
  19. package/custom/skills/explore-data/SKILL.md +2 -0
  20. package/custom/skills/frame-ml-problem/SKILL.md +2 -0
  21. package/custom/skills/gate-review/SKILL.md +2 -0
  22. package/custom/skills/generate-spec/SKILL.md +2 -0
  23. package/custom/skills/generate-test-report/SKILL.md +2 -0
  24. package/custom/skills/generate-testcase/SKILL.md +2 -0
  25. package/custom/skills/impact-analysis/SKILL.md +2 -0
  26. package/custom/skills/improve-algorithm/SKILL.md +2 -0
  27. package/custom/skills/ingest-data/SKILL.md +226 -0
  28. package/custom/skills/log-bug/SKILL.md +2 -0
  29. package/custom/skills/pr-impact-analysis/SKILL.md +2 -0
  30. package/custom/skills/read-study-requirement/SKILL.md +41 -2
  31. package/custom/skills/report-customer/SKILL.md +6 -2
  32. package/custom/skills/retest-orchestration/SKILL.md +2 -0
  33. package/custom/skills/review-plan/SKILL.md +2 -0
  34. package/custom/skills/script-sync/SKILL.md +1 -1
  35. package/custom/skills/test-analysis/SKILL.md +2 -0
  36. package/custom/skills/test-skills/rules/qa-writing-standards.md +2 -2
  37. package/custom/skills/train-model/SKILL.md +2 -0
  38. package/custom/templates/shared/create-testcase-workflow.md +32 -1
  39. package/custom/templates/shared/gate-workflow.md +64 -0
  40. package/docs/common/BA-Specs-Structure.md +3 -0
  41. package/docs/common/CHANGELOG.md +39 -0
  42. package/docs/common/Docs-Management-Flow.md +1 -1
  43. package/docs/common/INDEX.md +1 -0
  44. package/docs/common/Memory-Architecture-v1.0.md +1 -0
  45. package/docs/common/Project-Structure.md +5 -2
  46. package/package.json +1 -1
  47. package/scripts/create-score-excel.js +4 -111
  48. package/scripts/detect.js +22 -0
  49. package/scripts/hooks/session-start.js +1 -0
  50. package/scripts/init.js +24 -1
  51. package/scripts/link-resolver.js +194 -7
  52. package/scripts/prompt.js +144 -0
  53. package/scripts/task.js +22 -6
  54. package/scripts/ticket-writer.js +247 -0
  55. package/scripts/use.js +402 -135
@@ -3,8 +3,14 @@ const https = require('https');
3
3
  const chalk = require('chalk');
4
4
 
5
5
  const BACKLOG_RE = /https?:\/\/([\w.-]+\.backlog(?:tool)?\.com)\/view\/([A-Z][A-Z0-9_]+-\d+)(?:#comment-(\d+))?/i;
6
+ // Backlog Document permalink — "Copy link" on a Document page gives /alias/document/{id}.
7
+ // NOTE: Nulab's Document REST API is new (2026) — path unverified against a live space, adjust if it 404s.
8
+ const BACKLOG_DOCUMENT_RE = /https?:\/\/([\w.-]+\.backlog(?:tool)?\.com)\/(?:alias\/)?document\/(\d+)(?:#comment-(\d+))?/i;
9
+ // Backlog Wiki permalink — "Copy link" on a Wiki page gives /alias/wiki/{id}.
10
+ const BACKLOG_WIKI_RE = /https?:\/\/([\w.-]+\.backlog(?:tool)?\.com)\/alias\/wiki\/(\d+)/i;
6
11
  const JIRA_RE = /https?:\/\/([\w.-]+\.atlassian\.net)\/browse\/([A-Z][A-Z0-9_]+-\d+)/i;
7
12
  const JIRA_COMMENT_RE = /[?&]focusedCommentId=(\d+)/;
13
+ const SHAREPOINT_RE = /https?:\/\/([\w.-]+\.sharepoint\.com)\/\S*/i;
8
14
  const URL_RE = /https?:\/\/[^\s\)\]"<>]+/g;
9
15
 
10
16
  const MAX_AUTO_LINKS = 5;
@@ -13,25 +19,43 @@ function classifyLink(url) {
13
19
  const bm = url.match(BACKLOG_RE);
14
20
  if (bm) {
15
21
  const [, domain, ticketId, commentId] = bm;
16
- return { adapter: 'backlog', domain, ticketId, commentId: commentId || null, isComment: !!commentId };
22
+ return { adapter: 'backlog', kind: 'ticket', domain, ticketId, commentId: commentId || null, isComment: !!commentId };
23
+ }
24
+ const dm = url.match(BACKLOG_DOCUMENT_RE);
25
+ if (dm) {
26
+ const [, domain, documentId, commentId] = dm;
27
+ return { adapter: 'backlog', kind: 'document', domain, documentId, commentId: commentId || null, isComment: !!commentId };
28
+ }
29
+ const wm = url.match(BACKLOG_WIKI_RE);
30
+ if (wm) {
31
+ const [, domain, wikiId] = wm;
32
+ return { adapter: 'backlog', kind: 'wiki', domain, wikiId };
17
33
  }
18
34
  const jm = url.match(JIRA_RE);
19
35
  if (jm) {
20
36
  const [, domain, ticketId] = jm;
21
37
  const cm = url.match(JIRA_COMMENT_RE);
22
38
  const commentId = cm ? cm[1] : null;
23
- return { adapter: 'jira', domain, ticketId, commentId, isComment: !!commentId };
39
+ return { adapter: 'jira', kind: 'ticket', domain, ticketId, commentId, isComment: !!commentId };
40
+ }
41
+ const sm = url.match(SHAREPOINT_RE);
42
+ if (sm) {
43
+ const [, domain] = sm;
44
+ return { adapter: 'sharepoint', kind: 'unsupported', domain };
24
45
  }
25
46
  return null;
26
47
  }
27
48
 
49
+ // Auto-resolve (from a ticket description, capped at MAX_AUTO_LINKS) only ever
50
+ // followed ticket/comment links. Document/Wiki/SharePoint are explicit, single-URL
51
+ // lookups only (`ak fetch-links <url>`) — never auto-fetched en masse.
28
52
  function scanLinks(text) {
29
53
  const found = [];
30
54
  let m;
31
55
  URL_RE.lastIndex = 0;
32
56
  while ((m = URL_RE.exec(text)) !== null) {
33
57
  const classified = classifyLink(m[0]);
34
- if (classified) found.push({ url: m[0], ...classified });
58
+ if (classified && classified.kind === 'ticket') found.push({ url: m[0], ...classified });
35
59
  }
36
60
  return found;
37
61
  }
@@ -54,6 +78,41 @@ function httpsGet(url, headers) {
54
78
  });
55
79
  }
56
80
 
81
+ // Generic POST — `bodyType` picks the wire format:
82
+ // 'form' → application/x-www-form-urlencoded (Backlog's write API)
83
+ // 'json' → application/json (Jira's write API)
84
+ function httpsPost(url, payload, { headers = {}, bodyType = 'json' } = {}) {
85
+ return new Promise((resolve, reject) => {
86
+ const body = bodyType === 'form'
87
+ ? new URLSearchParams(payload).toString()
88
+ : JSON.stringify(payload);
89
+ const reqHeaders = {
90
+ ...headers,
91
+ 'Content-Type': bodyType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json',
92
+ 'Content-Length': Buffer.byteLength(body),
93
+ };
94
+ const { hostname, pathname, search } = new URL(url);
95
+ const req = https.request(
96
+ { hostname, path: pathname + search, method: 'POST', headers: reqHeaders },
97
+ (res) => {
98
+ let data = '';
99
+ res.on('data', chunk => (data += chunk));
100
+ res.on('end', () => {
101
+ if (res.statusCode < 200 || res.statusCode >= 300) {
102
+ reject(new Error(`HTTP ${res.statusCode}: ${data.substring(0, 500)}`));
103
+ return;
104
+ }
105
+ try { resolve(data ? JSON.parse(data) : {}); }
106
+ catch (e) { reject(new Error('Invalid JSON from API')); }
107
+ });
108
+ }
109
+ );
110
+ req.on('error', reject);
111
+ req.write(body);
112
+ req.end();
113
+ });
114
+ }
115
+
57
116
  async function fetchBacklogTicket(domain, apiKey, ticketId) {
58
117
  const issue = await httpsGet(`https://${domain}/api/v2/issues/${ticketId}?apiKey=${apiKey}`);
59
118
  return {
@@ -83,6 +142,77 @@ async function fetchBacklogComment(domain, apiKey, ticketId, commentId) {
83
142
  };
84
143
  }
85
144
 
145
+ async function fetchBacklogDocument(domain, apiKey, documentId) {
146
+ const doc = await httpsGet(`https://${domain}/api/v2/documents/${documentId}?apiKey=${apiKey}`);
147
+ return {
148
+ sourceType: 'document',
149
+ sourceUrl: `https://${domain}/document/${documentId}`,
150
+ documentId: String(documentId),
151
+ title: doc.title || doc.name || '',
152
+ tag: (doc.tags || []).map(t => t.name).join(', '),
153
+ content: (doc.plainText || doc.content || '').substring(0, 4000),
154
+ };
155
+ }
156
+
157
+ async function fetchBacklogDocumentComment(domain, apiKey, documentId, commentId) {
158
+ const comments = await httpsGet(
159
+ `https://${domain}/api/v2/documents/${documentId}/comments?apiKey=${apiKey}&count=100&order=asc`
160
+ );
161
+ const comment = (comments || []).find(c => String(c.id) === String(commentId));
162
+ if (!comment) throw new Error(`Comment ${commentId} not found in document ${documentId}`);
163
+ const date = comment.created ? new Date(comment.created).toLocaleDateString('vi-VN') : '';
164
+ return {
165
+ sourceType: 'document-comment',
166
+ sourceUrl: `https://${domain}/document/${documentId}#comment-${commentId}`,
167
+ documentId: String(documentId),
168
+ commentId: String(commentId),
169
+ author: comment.createdUser?.name || 'Unknown',
170
+ date,
171
+ content: (comment.content || '').substring(0, 2000),
172
+ };
173
+ }
174
+
175
+ async function fetchBacklogWiki(domain, apiKey, wikiId) {
176
+ const wiki = await httpsGet(`https://${domain}/api/v2/wikis/${wikiId}?apiKey=${apiKey}`);
177
+ return {
178
+ sourceType: 'wiki',
179
+ sourceUrl: `https://${domain}/alias/wiki/${wikiId}`,
180
+ wikiId: String(wikiId),
181
+ title: wiki.name || '',
182
+ content: (wiki.content || '').substring(0, 4000),
183
+ };
184
+ }
185
+
186
+ // ── Backlog WRITE — issue creation (PM Workflow_v1.0.md "Vấn đề 5") ──────────
187
+
188
+ async function fetchBacklogIssueTypes(domain, apiKey, projectIdOrKey) {
189
+ return httpsGet(`https://${domain}/api/v2/projects/${projectIdOrKey}/issueTypes?apiKey=${apiKey}`);
190
+ }
191
+
192
+ async function fetchBacklogPriorities(domain, apiKey) {
193
+ return httpsGet(`https://${domain}/api/v2/priorities?apiKey=${apiKey}`);
194
+ }
195
+
196
+ /**
197
+ * Creates a Backlog issue. Requires an apiKey with WRITE permission — distinct from the
198
+ * read-only key used elsewhere in this file for `ak fetch-links`/`ak use`.
199
+ * `projectId`, `summary`, `issueTypeId`, `priorityId` are mandatory per Backlog's API.
200
+ */
201
+ async function createBacklogIssue(domain, apiKeyWrite, { projectId, summary, description, issueTypeId, priorityId }) {
202
+ const issue = await httpsPost(
203
+ `https://${domain}/api/v2/issues?apiKey=${apiKeyWrite}`,
204
+ {
205
+ projectId: String(projectId),
206
+ summary,
207
+ issueTypeId: String(issueTypeId),
208
+ priorityId: String(priorityId),
209
+ description: description || '',
210
+ },
211
+ { bodyType: 'form' }
212
+ );
213
+ return { ticketId: issue.issueKey, url: `https://${domain}/view/${issue.issueKey}`, raw: issue };
214
+ }
215
+
86
216
  function jiraAuth(email, token) {
87
217
  return `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
88
218
  }
@@ -126,17 +256,67 @@ async function fetchJiraComment(domain, email, token, ticketId, commentId) {
126
256
  };
127
257
  }
128
258
 
259
+ // ── Jira WRITE — issue creation (PM Workflow_v1.0.md "Vấn đề 5") ────────────
260
+
261
+ /**
262
+ * Creates a Jira issue. `apiTokenWrite`/`emailWrite` are distinct from the read-only
263
+ * credentials used elsewhere in this file — same "separate WRITE key" policy as Backlog.
264
+ */
265
+ async function createJiraIssue(domain, emailWrite, apiTokenWrite, { projectKey, summary, description, issueTypeName }) {
266
+ const issue = await httpsPost(
267
+ `https://${domain}/rest/api/3/issue`,
268
+ {
269
+ fields: {
270
+ project: { key: projectKey },
271
+ summary,
272
+ issuetype: { name: issueTypeName || 'Task' },
273
+ description: {
274
+ type: 'doc',
275
+ version: 1,
276
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: description || '' }] }],
277
+ },
278
+ },
279
+ },
280
+ { headers: { Authorization: jiraAuth(emailWrite, apiTokenWrite), Accept: 'application/json' }, bodyType: 'json' }
281
+ );
282
+ return { ticketId: issue.key, url: `https://${domain}/browse/${issue.key}`, raw: issue };
283
+ }
284
+
285
+ function linkDedupeKey(classified) {
286
+ if (classified.kind === 'document') return 'doc:' + classified.documentId + (classified.commentId ? '#' + classified.commentId : '');
287
+ if (classified.kind === 'wiki') return 'wiki:' + classified.wikiId;
288
+ if (classified.ticketId) return classified.ticketId + (classified.commentId ? '#' + classified.commentId : '');
289
+ return null;
290
+ }
291
+
129
292
  async function fetchLink(url, credentials = {}, alreadyLoadedIds = []) {
130
293
  const classified = classifyLink(url);
131
294
  if (!classified) return null;
132
295
 
133
- const { adapter, domain, ticketId, commentId, isComment } = classified;
134
- const dedupeKey = ticketId + (commentId ? '#' + commentId : '');
135
- if (alreadyLoadedIds.includes(dedupeKey) || alreadyLoadedIds.includes(ticketId)) return null;
296
+ const dedupeKey = linkDedupeKey(classified);
297
+ if (dedupeKey && (alreadyLoadedIds.includes(dedupeKey) || alreadyLoadedIds.includes(classified.ticketId))) return null;
298
+
299
+ const { adapter, kind, domain, ticketId, commentId, documentId, wikiId, isComment } = classified;
300
+
301
+ if (adapter === 'sharepoint') {
302
+ return {
303
+ sourceType: 'unsupported',
304
+ reason: 'sharepoint-not-configured',
305
+ sourceUrl: url,
306
+ message:
307
+ 'SharePoint chưa có connector (cần Microsoft Graph API + OAuth — xem "Vấn đề 4" trong PM Workflow_v1.0.md). ' +
308
+ 'Copy/paste nội dung comment/tài liệu trực tiếp vào chat thay vì dán link.',
309
+ };
310
+ }
136
311
 
137
312
  if (adapter === 'backlog') {
138
313
  const apiKey = credentials.BACKLOG_API_KEY;
139
314
  if (!apiKey) throw new Error('BACKLOG_API_KEY not set');
315
+ if (kind === 'document') {
316
+ if (isComment) return await fetchBacklogDocumentComment(domain, apiKey, documentId, commentId);
317
+ return await fetchBacklogDocument(domain, apiKey, documentId);
318
+ }
319
+ if (kind === 'wiki') return await fetchBacklogWiki(domain, apiKey, wikiId);
140
320
  if (isComment) return await fetchBacklogComment(domain, apiKey, ticketId, commentId);
141
321
  return await fetchBacklogTicket(domain, apiKey, ticketId);
142
322
  }
@@ -176,4 +356,11 @@ async function resolveLinks(description, credentials = {}, alreadyLoadedIds = []
176
356
  return results;
177
357
  }
178
358
 
179
- module.exports = { classifyLink, scanLinks, fetchBacklogTicket, fetchBacklogComment, fetchJiraTicket, fetchJiraComment, fetchLink, resolveLinks };
359
+ module.exports = {
360
+ classifyLink, scanLinks, fetchLink, resolveLinks,
361
+ fetchBacklogTicket, fetchBacklogComment,
362
+ fetchBacklogDocument, fetchBacklogDocumentComment, fetchBacklogWiki,
363
+ fetchJiraTicket, fetchJiraComment,
364
+ fetchBacklogIssueTypes, fetchBacklogPriorities, createBacklogIssue,
365
+ createJiraIssue,
366
+ };
package/scripts/prompt.js CHANGED
@@ -278,6 +278,150 @@ Write documentation based on approved outline.
278
278
  **INVOKE:** \`superpowers:verification-before-completion\`
279
279
  Verify docs match code. No outdated examples.
280
280
  Display: "GATE 3: Documentation complete. Type APPROVED."
281
+ `,
282
+ },
283
+ 'create-spec': {
284
+ header: 'Create UC Spec',
285
+ instruction: 'Create a complete UC Spec from the raw requirement described below, following the BA 4-Gate workflow.',
286
+ skillWorkflow: `
287
+ ## STRICT GATE WORKFLOW — [BA] 4-Gate Spec Creation
288
+
289
+ Follow gates IN ORDER. Do NOT skip or merge gates.
290
+
291
+ ### GATE 1 — Analyze Requirement & Draft Q&A (auto-start)
292
+ **INVOKE:** \`read-study-requirement\` skill, then the \`ba-skills\` sub-skills for analysis
293
+ 1. Confirm \`functionId\` and ensure AK-Docs is on branch \`feature/[functionId]/[ticketId]\`
294
+ 2. Read the raw requirement (ticket/backlog/file) + related source code
295
+ 3. Identify Gaps/Assumptions, draft a Q&A list — classify every item as Fact / Assumption / Gap
296
+ 4. Output \`AK-Docs/02.BA-Specs/01.QnA/[functionId]/[ticketId].md\`
297
+ 5. Display "GATE 1: Q&A draft ready" → wait for **APPROVED**
298
+
299
+ ### GATE 2 — Update Q&A (loop until all Confirmed)
300
+ Integrate BA/stakeholder answers into the analysis doc. Loop: unresolved items remain → keep asking ONE question at a time. All Confirmed → display "GATE 2: All items Confirmed" → wait for **APPROVED**.
301
+
302
+ ### GATE 3 — UI Prototype (HTML/CSS)
303
+ Only runs after Gate 2 APPROVED.
304
+ 1. List UI components from the confirmed requirement
305
+ 2. Design and write static HTML/CSS prototype (incl. error-state placeholders)
306
+ 3. Save under \`AK-Docs/02.BA-Specs/03.Prototypes/[functionId]/\`
307
+ 4. Display "GATE 3: Prototype ready" → wait for **APPROVED**
308
+
309
+ ### GATE 4 — Full UC Spec (wait for APPROVED)
310
+ Only runs after Gate 3 APPROVED.
311
+ 1. Fill the UC Spec template: Flow of Events (Main/Alternative/Exception), UI Components, Mermaid activity diagram, Business Rules
312
+ 2. Save \`AK-Docs/02.BA-Specs/04.UC-Specs/[functionId]/UC-Spec_v[N].md\`
313
+ 3. Retrospect — propose a memory draft for any new business rule/decision learned
314
+ 4. Submit AK-Docs via Merge Request (title/description shown, explicit user confirmation) — PM reviews & merges into \`main\`
315
+ 5. Display "GATE 4: UC Spec ready" → wait for **APPROVED**
316
+ `,
317
+ },
318
+ 'create-testcase': {
319
+ header: 'Create Test Cases',
320
+ instruction: 'Create a complete, execution-ready Test Case set from the requirement described below, following the QA 4-Gate workflow.',
321
+ skillWorkflow: `
322
+ ## STRICT GATE WORKFLOW — [QA] 4-Gate TestCase Creation
323
+
324
+ Follow gates IN ORDER. Do NOT skip or merge gates.
325
+
326
+ ### GATE 1 — Requirement & Risk Analysis (auto-start)
327
+ **INVOKE:** \`read-study-requirement\` skill, then the \`test-skills\` sub-skills
328
+ 1. Confirm \`functionId\` and ensure AK-Docs is on branch \`feature/[functionId]/[ticketId]\`
329
+ 2. Read the requirement (SRS/Backlog/Jira/UC Spec) and assess risk areas
330
+ 3. Clarify with QA/stakeholder if anything is ambiguous — ask ONE question at a time
331
+ 4. Output requirement + risk analysis under \`AK-Docs/03.QA-Specs/01.Requirements/[functionId]/[ticketId].md\`
332
+ 5. Display "GATE 1: Requirement & risk analysis ready" → wait for **APPROVED**
333
+
334
+ ### GATE 2 — Scenario Building
335
+ Only runs after Gate 1 APPROVED.
336
+ Apply scenario-building skills to derive test scenarios (happy path, edge case, negative). Output scenario list. Display "GATE 2: Scenarios ready" → wait for **APPROVED**.
337
+
338
+ ### GATE 3 — Detailed Test Case Design
339
+ Only runs after Gate 2 APPROVED.
340
+ 1. Apply TC design techniques (equivalence partitioning, boundary values, etc.)
341
+ 2. Design UI, business, and data test cases
342
+ 3. Output draft Test Cases under \`AK-Docs/03.QA-Specs/03.TestCases/[functionId]/\`
343
+ 4. Display "GATE 3: Draft Test Cases ready" → wait for **APPROVED**
344
+
345
+ ### GATE 4 — Review & Optimize (wait for APPROVED)
346
+ Only runs after Gate 3 APPROVED.
347
+ 1. Review coverage against requirements, optimize/dedupe test cases
348
+ 2. Confirm sign-off status, generate the final TestCase artifact
349
+ 3. Retrospect — propose a memory draft for any lesson learned
350
+ 4. Submit AK-Docs via Merge Request (title/description shown, explicit user confirmation) — PM reviews & merges into \`main\`
351
+ 5. Display "GATE 4: Final Test Cases ready" → wait for **APPROVED**
352
+ `,
353
+ },
354
+ 'create-system-requirement': {
355
+ header: 'Create System Requirement',
356
+ instruction: 'Bridge the UC Spec into a Dev-facing System Requirement, following the 2-Gate workflow.',
357
+ skillWorkflow: `
358
+ ## STRICT GATE WORKFLOW — Create System Requirement (2-Gate Bridge)
359
+
360
+ > Runs once per \`functionId\` per UC Spec version — NOT per ticket. Blocks coding Gate 1 until APPROVED.
361
+
362
+ ### GATE 1 — Investigate & Draft (auto-start)
363
+ **INVOKE:** \`create-system-requirement\` skill
364
+ 1. Resolve \`functionId\`; locate the current UC Spec (\`AK-Docs/02.BA-Specs/04.UC-Specs/[functionId]/UC-Spec_v[N].md\`) — not found → STOP, tell DEV the UC Spec must exist and be BA-signed-off first
365
+ 2. Read the full UC Spec, then investigate source code (existing error-handling conventions, data model/API surface)
366
+ 3. Translate UC → Functional/Non-Functional Requirements, Business Rules → Validation Rules, Exception & Error Handling, Acceptance Test Scenarios — every item traced to a UC reference, no invented content
367
+ 4. Classify every item as Fact / Assumption / Gap; ask ONE question at a time until every Gap is Confirmed
368
+ 5. Display "GATE 1: All Gaps Confirmed" → wait for **APPROVED**
369
+
370
+ ### GATE 2 — Finalize & Approve (wait for APPROVED)
371
+ Only runs after Gate 1 APPROVED.
372
+ 1. Decide single-file vs split (default: single file mirroring the UC Spec 1-1)
373
+ 2. Write \`AK-Docs/02.BA-Specs/00.Requirements/[functionId]/System-Requirement_v[N].md\` with the Traceability Matrix + all sections
374
+ 3. Display "GATE 2: System Requirement ready" with coverage counts (Open Gaps must be 0) → wait for **APPROVED**
375
+
376
+ **On APPROVED:** coding Gate 1 (\`read-study-requirement\`) unlocks for this functionId.
377
+ `,
378
+ },
379
+ 'execute': {
380
+ header: 'Execute Test Cases',
381
+ instruction: 'Execute the Test Case file described below against the running app, following the EXECUTE 4-Gate workflow.',
382
+ skillWorkflow: `
383
+ ## STRICT GATE WORKFLOW — [EXECUTE] Executing Flow
384
+
385
+ Follow gates IN ORDER. Do NOT skip or merge gates.
386
+
387
+ ### GATE 1 — Pre-flight & Work Plan (auto-start)
388
+ **INVOKE:** \`execute-flow\` skill
389
+ Check: MCP Playwright configured, TC file is parseable, \`ak-test/{repo}/\` scaffold exists, \`BASE_URL\` set, Playwright config exists. Display the work plan (TC file, repo, screen, BASE_URL) → wait for **APPROVED**.
390
+
391
+ ### GATE 2 — Script Sync
392
+ Only runs after Gate 1 APPROVED.
393
+ **INVOKE:** \`script-sync\` skill
394
+ Hash-based TC↔script sync: new TC → generate Playwright test via MCP snapshot/locator (never fabricate selectors); changed TC → update; unchanged → skip; manual/blocked → mark Untest/Pending. Display new/updated/skipped/manual counts → wait for **APPROVED**.
395
+
396
+ ### GATE 3 — Execute & Evidence
397
+ Only runs after Gate 2 APPROVED.
398
+ Run \`BASE_URL=<url> npx playwright test scripts/{screenId}/{ScreenID}.spec.ts\`, organize evidence (screenshots/trace/result.md) into \`ak-test/{repo}/results/{screenId}/run-{N}/\`, auto-draft bug reports for failures, update R1/R2 columns in the TC file. Supports \`RETEST: [TC_ID]\` and \`PR: [url]\`. Display pass/fail/untest/pending counts → wait for **APPROVED** (all Critical/High bugs resolved).
399
+
400
+ ### GATE 4 — Report & Bug Logging (wait for APPROVED)
401
+ Only runs after Gate 3 APPROVED.
402
+ Generate \`testreport.md\` (Go/No-Go: 0 unresolved Critical = Go), log each failed TC as a bug one at a time (ask Y/N/edit before logging). Display decision + bug counts → wait for **APPROVED** to sign off.
403
+ `,
404
+ },
405
+ 'ingest-data': {
406
+ header: 'Ingest Data',
407
+ instruction: 'Ingest the customer communication (Backlog/Jira/SharePoint link or pasted text) described below into AK-Docs logs, following the 2-Gate workflow.',
408
+ skillWorkflow: `
409
+ ## STRICT GATE WORKFLOW — Ingest Data (2-Gate)
410
+
411
+ **INVOKE:** \`ingest-data\` skill for both gates — it owns the classification rules, draft templates, and per-file format.
412
+
413
+ ### GATE 1 — Fetch, Classify, Draft (auto-start)
414
+ 1. Resolve the source: Backlog/Jira ticket/comment/Document/Wiki URL → \`ak fetch-links "<url>"\`; SharePoint → not yet connected, ask for pasted text instead; plain text → use as-is
415
+ 2. Classify into \`QnA-Log.md\` / \`Meetings-Log.md\` / \`Confirmations-Log.md\` — ask the user if ambiguous, never guess silently
416
+ 3. Draft the entry using the exact template for the target log file, including trace fields (Nguồn, functionId, Người tổng hợp, Người approve tại nguồn)
417
+ 4. Display the full draft → wait for **APPROVED** (or edit requests, looped until approved)
418
+
419
+ ### GATE 2 — Branch + Merge Request (runs immediately after APPROVED)
420
+ 1. Resolve \`functionId\` (tag from source → inferred from input → fallback \`general\`)
421
+ 2. Ensure AK-Docs is on branch \`feature/[functionId]/[ticketId]\` (create only after explicit confirmation)
422
+ 3. Append the approved entry to the target log file (create with header template if missing)
423
+ 4. Submit AK-Docs via Merge Request (title/description shown, explicit user confirmation) — PM reviews & merges into \`main\`
424
+ 5. Display "GATE 2 DONE: Entry written to AK-Docs/01.QnA/[file]"
281
425
  `,
282
426
  },
283
427
  'gen-doc': {
package/scripts/task.js CHANGED
@@ -331,21 +331,27 @@ async function nextGate(taskId) {
331
331
  const nextGateNum = currentGate + 1;
332
332
 
333
333
  // Save context snapshot if current task is active
334
- let taskType = null;
334
+ // taskType falls back to task-state.json's own stored value — current.json is removed
335
+ // at the end of this function, so on Gate 2+ of a multi-step task it's already gone.
336
+ let taskType = existing.taskType || null;
335
337
  if (await fs.pathExists(CURRENT_FILE)) {
336
338
  const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
337
339
  if (ctx && ctx.taskId === resolvedId) {
338
- taskType = ctx.taskType || null;
340
+ taskType = ctx.taskType || taskType;
339
341
  await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
340
342
  }
341
343
  }
342
344
 
343
345
  const isGenDoc = taskType === 'gen-doc';
344
- const maxGate = isGenDoc ? 2 : 5;
346
+ const isSystemRequirement = taskType === 'create-system-requirement';
347
+ const isIngestData = taskType === 'ingest-data';
348
+ const isTwoGateDoc = isGenDoc || isSystemRequirement || isIngestData;
349
+ const maxGate = isTwoGateDoc ? 2 : 5;
345
350
 
346
351
  const taskState = {
347
352
  ...existing,
348
353
  taskId: resolvedId,
354
+ taskType: taskType || existing.taskType || null,
349
355
  status: nextGateNum > maxGate ? 'done' : 'pending',
350
356
  updatedAt: now,
351
357
  pausedAt: now,
@@ -365,7 +371,7 @@ async function nextGate(taskId) {
365
371
  console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
366
372
  console.log(chalk.gray(` Summary saved to: .aiflow/tasks/${resolvedId}/task-summary.md`));
367
373
 
368
- if (isGenDoc && currentGate >= maxGate) {
374
+ if (isTwoGateDoc && currentGate >= maxGate) {
369
375
  console.log(chalk.green('\n ✅ Document workflow complete. Task is done.'));
370
376
  } else {
371
377
  const nextLabel = gateLabel(nextGateNum, taskType);
@@ -374,7 +380,7 @@ async function nextGate(taskId) {
374
380
  console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
375
381
  console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
376
382
  console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
377
- if (!isGenDoc) {
383
+ if (!isTwoGateDoc) {
378
384
  console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in the Gate 2 plan doc (AK-Docs/04.Coding/02.Plans/).`));
379
385
  console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
380
386
  }
@@ -479,7 +485,7 @@ async function detectCurrentGate(taskId) {
479
485
 
480
486
  async function generateMarkdownSummary(taskState, taskType = null) {
481
487
  taskType = taskType || taskState.taskType || 'feature';
482
- const maxGate = taskType === 'testing' ? 4 : (taskType === 'gen-doc' ? 2 : 5);
488
+ const maxGate = taskType === 'testing' ? 4 : ((taskType === 'gen-doc' || taskType === 'create-system-requirement' || taskType === 'ingest-data') ? 2 : 5);
483
489
  const lines = [];
484
490
  lines.push(`# Task Summary: ${taskState.taskId}`);
485
491
  lines.push(`**Title:** ${taskState.title}`);
@@ -517,6 +523,16 @@ function gateLabel(n, taskType = null) {
517
523
  if (n === 2) return 'Generate Document';
518
524
  if (n >= 3) return 'Done';
519
525
  }
526
+ if (taskType === 'create-system-requirement') {
527
+ if (n === 1) return 'Investigate UC & Draft';
528
+ if (n === 2) return 'Finalize & Approve';
529
+ if (n >= 3) return 'Done';
530
+ }
531
+ if (taskType === 'ingest-data') {
532
+ if (n === 1) return 'Fetch, Classify, Draft';
533
+ if (n === 2) return 'Branch + Merge Request';
534
+ if (n >= 3) return 'Done';
535
+ }
520
536
  const labels = {
521
537
  1: 'AI Analyze Requirement',
522
538
  2: 'Implementation Plan',