@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.
- package/README.md +65 -0
- package/bin/aiflow.js +74 -4
- package/custom/rules/output-language.md +36 -0
- package/custom/rules/project-conventions.md +16 -0
- package/custom/skills/automation-testing/SKILL.md +1 -1
- package/custom/skills/ba-skills/skill-ba-build-business-rules-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-initial-analysis-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-mermaid-flowchart-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-qna-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-ui-prototype-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-write-uc-spec-v1.md +2 -0
- package/custom/skills/coverage-check/SKILL.md +2 -0
- package/custom/skills/create-system-requirement/SKILL.md +252 -0
- package/custom/skills/deploy-model/SKILL.md +2 -0
- package/custom/skills/design-experiment/SKILL.md +2 -0
- package/custom/skills/evaluate-model/SKILL.md +2 -0
- package/custom/skills/evidence-aggregation/SKILL.md +2 -0
- package/custom/skills/execute-flow/SKILL.md +2 -0
- package/custom/skills/explore-data/SKILL.md +2 -0
- package/custom/skills/frame-ml-problem/SKILL.md +2 -0
- package/custom/skills/gate-review/SKILL.md +2 -0
- package/custom/skills/generate-spec/SKILL.md +2 -0
- package/custom/skills/generate-test-report/SKILL.md +2 -0
- package/custom/skills/generate-testcase/SKILL.md +2 -0
- package/custom/skills/impact-analysis/SKILL.md +2 -0
- package/custom/skills/improve-algorithm/SKILL.md +2 -0
- package/custom/skills/ingest-data/SKILL.md +226 -0
- package/custom/skills/log-bug/SKILL.md +2 -0
- package/custom/skills/pr-impact-analysis/SKILL.md +2 -0
- package/custom/skills/read-study-requirement/SKILL.md +41 -2
- package/custom/skills/report-customer/SKILL.md +6 -2
- package/custom/skills/retest-orchestration/SKILL.md +2 -0
- package/custom/skills/review-plan/SKILL.md +2 -0
- package/custom/skills/script-sync/SKILL.md +1 -1
- package/custom/skills/test-analysis/SKILL.md +2 -0
- package/custom/skills/test-skills/rules/qa-writing-standards.md +2 -2
- package/custom/skills/train-model/SKILL.md +2 -0
- package/custom/templates/shared/create-testcase-workflow.md +32 -1
- package/custom/templates/shared/gate-workflow.md +64 -0
- package/docs/common/BA-Specs-Structure.md +3 -0
- package/docs/common/CHANGELOG.md +39 -0
- package/docs/common/Docs-Management-Flow.md +1 -1
- package/docs/common/INDEX.md +1 -0
- package/docs/common/Memory-Architecture-v1.0.md +1 -0
- package/docs/common/Project-Structure.md +5 -2
- package/package.json +1 -1
- package/scripts/create-score-excel.js +4 -111
- package/scripts/detect.js +22 -0
- package/scripts/hooks/session-start.js +1 -0
- package/scripts/init.js +24 -1
- package/scripts/link-resolver.js +194 -7
- package/scripts/prompt.js +144 -0
- package/scripts/task.js +22 -6
- package/scripts/ticket-writer.js +247 -0
- package/scripts/use.js +402 -135
package/scripts/link-resolver.js
CHANGED
|
@@ -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
|
|
134
|
-
|
|
135
|
-
|
|
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 = {
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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 (
|
|
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 (!
|
|
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',
|