@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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Vấn đề 5 (docs/internal/PM Workflow_v1.0.md) — turns an AI-proposed, PM-approved task list
|
|
3
|
+
// (born out of `ingest-data` Gate 3) into real tickets on Backlog/Jira.
|
|
4
|
+
//
|
|
5
|
+
// Read access (fetch-links, listing projects/issueTypes/priorities) reuses the existing
|
|
6
|
+
// read-only credentials already configured for `ak use`/`ak fetch-links`. Creating a ticket is a
|
|
7
|
+
// WRITE action and requires a SEPARATE credential (`BACKLOG_API_KEY_WRITE` / `JIRA_API_TOKEN_WRITE`)
|
|
8
|
+
// — same "distinct WRITE key" policy the PM Workflow doc calls for, kept apart from the read key so
|
|
9
|
+
// a compromised/misconfigured read key can never create tickets.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const useCommand = require('./use');
|
|
14
|
+
const linkResolver = require('./link-resolver');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
loadCredentials,
|
|
18
|
+
resolveBacklogCredentials,
|
|
19
|
+
resolveJiraCredentials,
|
|
20
|
+
saveMcpCredentials,
|
|
21
|
+
} = useCommand;
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
fetchBacklogIssueTypes,
|
|
25
|
+
fetchBacklogPriorities,
|
|
26
|
+
createBacklogIssue,
|
|
27
|
+
createJiraIssue,
|
|
28
|
+
} = linkResolver;
|
|
29
|
+
|
|
30
|
+
// Only these keys can be written via `ak credentials set` — dedicated adapter credentials
|
|
31
|
+
// (BACKLOG_API_KEY, JIRA_API_TOKEN, ...) still go through `ak init --adapter` on purpose.
|
|
32
|
+
const SETTABLE_CREDENTIAL_KEYS = [
|
|
33
|
+
'BACKLOG_API_KEY_WRITE',
|
|
34
|
+
'JIRA_API_TOKEN_WRITE',
|
|
35
|
+
'JIRA_EMAIL_WRITE',
|
|
36
|
+
'BACKLOG_DEFAULT_PROJECT_ID',
|
|
37
|
+
'BACKLOG_DEFAULT_PROJECT_KEY',
|
|
38
|
+
'JIRA_DEFAULT_PROJECT_KEY',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function jiraFullDomain(domain) {
|
|
42
|
+
return domain.includes('.') ? domain : `${domain}.atlassian.net`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function missingCredResult(target, field, message) {
|
|
46
|
+
return { ok: false, error: 'missing-write-credentials', target, field, message };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function resolveBacklogWrite(creds) {
|
|
50
|
+
const read = resolveBacklogCredentials(creds);
|
|
51
|
+
if (!read) {
|
|
52
|
+
return missingCredResult(
|
|
53
|
+
'backlog', 'BACKLOG_API_KEY / BACKLOG_SPACE_KEY',
|
|
54
|
+
'Chưa cấu hình credentials đọc Backlog. Chạy `ak init --adapter backlog` trước.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const apiKeyWrite = process.env.BACKLOG_API_KEY_WRITE || creds.BACKLOG_API_KEY_WRITE;
|
|
58
|
+
if (!apiKeyWrite) {
|
|
59
|
+
return missingCredResult(
|
|
60
|
+
'backlog', 'BACKLOG_API_KEY_WRITE',
|
|
61
|
+
'Cần API key Backlog có quyền tạo issue (khác key đọc hiện tại). Cung cấp API key này, mình sẽ lưu lại và tạo ticket ngay.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, domain: read.domain, apiKeyRead: read.apiKey, apiKeyWrite };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resolveJiraWrite(creds) {
|
|
68
|
+
const read = resolveJiraCredentials(creds);
|
|
69
|
+
if (!read) {
|
|
70
|
+
return missingCredResult(
|
|
71
|
+
'jira', 'JIRA_API_TOKEN / JIRA_EMAIL / JIRA_DOMAIN',
|
|
72
|
+
'Chưa cấu hình credentials đọc Jira. Chạy `ak init --adapter jira` trước.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const apiTokenWrite = process.env.JIRA_API_TOKEN_WRITE || creds.JIRA_API_TOKEN_WRITE;
|
|
76
|
+
if (!apiTokenWrite) {
|
|
77
|
+
return missingCredResult(
|
|
78
|
+
'jira', 'JIRA_API_TOKEN_WRITE',
|
|
79
|
+
'Cần Jira API token có quyền tạo issue (khác token đọc hiện tại). Cung cấp token này, mình sẽ lưu lại và tạo ticket ngay.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const emailWrite = process.env.JIRA_EMAIL_WRITE || creds.JIRA_EMAIL_WRITE || read.email;
|
|
83
|
+
return { ok: true, domain: jiraFullDomain(read.domain), emailWrite, apiTokenWrite };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function pickBacklogDefaults(domain, apiKeyRead, projectId) {
|
|
87
|
+
const [issueTypes, priorities] = await Promise.all([
|
|
88
|
+
fetchBacklogIssueTypes(domain, apiKeyRead, projectId),
|
|
89
|
+
fetchBacklogPriorities(domain, apiKeyRead),
|
|
90
|
+
]);
|
|
91
|
+
if (!issueTypes || !issueTypes.length) throw new Error(`Project ${projectId} không có issueType nào — không thể tạo issue.`);
|
|
92
|
+
const defaultIssueTypeId = issueTypes[0].id;
|
|
93
|
+
const normal = (priorities || []).find((p) => /normal|trung bình/i.test(p.name));
|
|
94
|
+
const defaultPriorityId = (normal || (priorities || [])[0] || {}).id;
|
|
95
|
+
if (!defaultPriorityId) throw new Error('Không lấy được danh sách priority từ Backlog.');
|
|
96
|
+
return { defaultIssueTypeId, defaultPriorityId };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `ak tasks create-tickets <file.json> [--json]`
|
|
101
|
+
*
|
|
102
|
+
* Input file shape:
|
|
103
|
+
* {
|
|
104
|
+
* "target": "backlog" | "jira",
|
|
105
|
+
* "projectId": "12345", // optional — Backlog only, overrides BACKLOG_DEFAULT_PROJECT_ID
|
|
106
|
+
* "projectKey": "PROJ", // optional — Jira only, overrides JIRA_DEFAULT_PROJECT_KEY
|
|
107
|
+
* "tasks": [
|
|
108
|
+
* { "type": "spec|coding|test|other", "title": "...", "description": "..." }
|
|
109
|
+
* ]
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* `description` is expected to already be fully composed by the AI per the ticket template in
|
|
113
|
+
* PM Workflow_v1.0.md §6.1 (Nội dung task + Nguồn tham chiếu) — this command posts it as-is.
|
|
114
|
+
*/
|
|
115
|
+
async function createTicketsCommand(inputFile, options = {}) {
|
|
116
|
+
const emit = (obj) => {
|
|
117
|
+
if (options.json) { process.stdout.write(JSON.stringify(obj)); return; }
|
|
118
|
+
if (obj.ok === false) {
|
|
119
|
+
console.log(chalk.red(`✗ ${obj.message || obj.error}`));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.green(`✓ ${obj.results.filter(r => r.ok).length}/${obj.results.length} ticket(s) created on ${obj.target}.`));
|
|
122
|
+
for (const r of obj.results) {
|
|
123
|
+
console.log(r.ok ? ` ${chalk.green('✓')} ${r.ticketId} ${chalk.gray(r.title)}` : ` ${chalk.red('✗')} ${chalk.gray(r.title)} — ${r.error}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log(JSON.stringify(obj));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let input;
|
|
130
|
+
try {
|
|
131
|
+
input = await fs.readJson(inputFile);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
emit({ ok: false, error: 'invalid-input-file', message: `Không đọc được ${inputFile}: ${err.message}` });
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const target = input.target;
|
|
139
|
+
const tasks = input.tasks || [];
|
|
140
|
+
if (!['backlog', 'jira'].includes(target)) {
|
|
141
|
+
emit({ ok: false, error: 'invalid-target', message: `target phải là "backlog" hoặc "jira", nhận được: ${target}` });
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!tasks.length) {
|
|
146
|
+
emit({ ok: false, error: 'empty-tasks', message: 'File input không có task nào trong "tasks".' });
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const creds = await loadCredentials();
|
|
152
|
+
|
|
153
|
+
if (target === 'backlog') {
|
|
154
|
+
const write = await resolveBacklogWrite(creds);
|
|
155
|
+
if (!write.ok) { emit(write); process.exitCode = 2; return; }
|
|
156
|
+
|
|
157
|
+
const projectId = input.projectId || creds.BACKLOG_DEFAULT_PROJECT_ID;
|
|
158
|
+
if (!projectId) {
|
|
159
|
+
emit({
|
|
160
|
+
ok: false, error: 'missing-project',
|
|
161
|
+
message: 'Chưa xác định projectId đích trên Backlog. Chạy `ak backlog-projects` để xem danh sách, rồi `ak backlog-set-default-project <id> <key>`.',
|
|
162
|
+
});
|
|
163
|
+
process.exitCode = 2;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let defaults;
|
|
168
|
+
try {
|
|
169
|
+
defaults = await pickBacklogDefaults(write.domain, write.apiKeyRead, projectId);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
emit({ ok: false, error: 'project-lookup-failed', message: err.message });
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const task of tasks) {
|
|
178
|
+
try {
|
|
179
|
+
const created = await createBacklogIssue(write.domain, write.apiKeyWrite, {
|
|
180
|
+
projectId,
|
|
181
|
+
summary: task.title,
|
|
182
|
+
description: task.description,
|
|
183
|
+
issueTypeId: task.issueTypeId || defaults.defaultIssueTypeId,
|
|
184
|
+
priorityId: task.priorityId || defaults.defaultPriorityId,
|
|
185
|
+
});
|
|
186
|
+
results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
results.push({ type: task.type, title: task.title, ok: false, error: err.message });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
emit({ ok: true, target, projectId, results });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// target === 'jira'
|
|
196
|
+
const write = await resolveJiraWrite(creds);
|
|
197
|
+
if (!write.ok) { emit(write); process.exitCode = 2; return; }
|
|
198
|
+
|
|
199
|
+
const projectKey = input.projectKey || creds.JIRA_DEFAULT_PROJECT_KEY;
|
|
200
|
+
if (!projectKey) {
|
|
201
|
+
emit({
|
|
202
|
+
ok: false, error: 'missing-project',
|
|
203
|
+
message: 'Chưa xác định project đích trên Jira. Chạy `ak jira-projects` để xem danh sách, rồi `ak jira-set-default-project <key>`.',
|
|
204
|
+
});
|
|
205
|
+
process.exitCode = 2;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const results = [];
|
|
210
|
+
for (const task of tasks) {
|
|
211
|
+
try {
|
|
212
|
+
const created = await createJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, {
|
|
213
|
+
projectKey,
|
|
214
|
+
summary: task.title,
|
|
215
|
+
description: task.description,
|
|
216
|
+
issueTypeName: task.issueType || 'Task',
|
|
217
|
+
});
|
|
218
|
+
results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
results.push({ type: task.type, title: task.title, ok: false, error: err.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
emit({ ok: true, target, projectKey, results });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* `ak credentials set <KEY> <VALUE>` — lets the AI persist a WRITE key the user pasted in chat
|
|
228
|
+
* (after being asked, per the missing-write-credentials error above) without a bespoke command
|
|
229
|
+
* per key. Restricted to an allowlist — dedicated adapter read-credentials still go through
|
|
230
|
+
* `ak init --adapter`, this is only for the pieces that flow introduced.
|
|
231
|
+
*/
|
|
232
|
+
async function setCredentialCommand(key, value) {
|
|
233
|
+
if (!SETTABLE_CREDENTIAL_KEYS.includes(key)) {
|
|
234
|
+
console.log(chalk.red(`✗ Unknown credential key: ${key}.`));
|
|
235
|
+
console.log(chalk.gray(` Allowed: ${SETTABLE_CREDENTIAL_KEYS.join(', ')}`));
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await saveMcpCredentials({ [key]: value });
|
|
240
|
+
console.log(chalk.green(`✓ Saved ${key} to ~/.aiflow/credentials.json`));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
createTicketsCommand,
|
|
245
|
+
setCredentialCommand,
|
|
246
|
+
SETTABLE_CREDENTIAL_KEYS,
|
|
247
|
+
};
|