@relipa/ai-flow-kit 0.2.0-beta.2 → 0.2.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.
@@ -133,17 +133,41 @@ Nếu người dùng yêu cầu sửa (thêm/bớt ý, đổi log đích, sửa
133
133
  - **Chỉ là tham khảo** → hiển thị: `ℹ️ Nội dung này không phát sinh task — chỉ lưu làm tài liệu tham khảo. Gate 3 kết thúc.` rồi chạy `ak gate 3 skip --ticket [ticket-id] --reason "<1 câu lý do>"` (không cần `ak gate 3 start` trước) và dừng, không hỏi thêm. Việc này đóng gate đúng cách — dashboard (ai-flow-ex) hiển thị "⏭ Gate 3 skipped — \<lý do\>" thay vì để task treo mãi ở trạng thái "sẵn sàng chạy Gate 3".
134
134
  - **Có hành động cụ thể** → tiếp tục Bước 3. Khi không chắc (ví dụ nội dung vừa có FYI vừa có 1 ý cần sửa code) — nghiêng về đề xuất task cho phần cần hành động, bỏ qua phần FYI, không tự bịa task cho phần không rõ.
135
135
 
136
+ **Riêng khi nguồn là `Confirmations-Log`** (khách hàng CHỐT chính thức 1 thay đổi requirement) — đây là trường hợp dễ đề xuất thiếu task nhất nếu chỉ dựa vào Action items viết sẵn trong nội dung (vd chỉ thấy 1 bug fix được nêu tên, bỏ sót toàn bộ phần spec/testcase phát sinh từ chính rule vừa chốt). Trước khi sang Bước 3, tự chạy qua đủ checklist tối thiểu sau — không hiển thị checklist này cho PM, chỉ dùng để tự rà soát không bỏ sót:
137
+
138
+ 1. **UC Spec** — functionId này đã có UC Spec chưa, và rule vừa chốt đã phản ánh trong Spec chưa? Chưa → task `spec`.
139
+ 2. **System Requirement** — chỉ cần nếu có task `coding` theo sau (Gate 1 coding sẽ tự chặn nếu System Requirement thiếu/lệch version, xem `CLAUDE.md` pre-check) → task `system-requirement`.
140
+ 3. **Impact analysis** — thay đổi có đủ phức tạp/rủi ro (ảnh hưởng nhiều module/feature khác, hoặc PM cần thấy effort/scope trước khi giao việc) để cần 1 tài liệu đánh giá riêng, tách khỏi Gate 1 coding không? Cần → task `impact-analysis`.
141
+ 4. **Coding** — có thay đổi hành vi hệ thống cần code không? Cần → task `coding` (tách thành nhiều task nhỏ nếu phạm vi lớn, mỗi task 1 title riêng).
142
+ 5. **Data migration/backfill** — rule mới có áp dụng hồi tố lên dữ liệu đã tồn tại không (khác với chỉ áp dụng từ nay về sau)? Có → thêm 1 task `coding` **riêng** cho việc migration/backfill, không gộp chung với task coding chính (2 rủi ro/effort khác nhau, cần review riêng).
143
+ 6. **Create/Update testcase** — cần → task `test`.
144
+ 7. **Execute test** — cần → task `execute-test`.
145
+
146
+ Không phải mục nào cũng luôn có (case rule chỉ áp dụng tương lai thì mục 5 = không cần) — nhưng phải đi qua đủ 7 mục và có lý do trước khi kết luận không cần, không bỏ qua mục nào chỉ vì nội dung entry không viết thành Action item rõ ràng.
147
+
136
148
  ### 5.3 Bước 3 — Đề xuất danh sách task
137
149
 
138
150
  Mỗi task gồm:
139
151
 
140
152
  | Trường | Ghi chú |
141
153
  |---|---|
142
- | `type` | `spec` (tạo/update UC Spec) · `coding` · `test` · `other` (update tài liệu không thuộc BA/QA/Dev, vd Rules) |
154
+ | `type` | `spec` (tạo/update UC Spec) · `system-requirement` (tạo/update System Requirement) · `impact-analysis` (đánh giá phạm vi ảnh hưởng, tách riêng khỏi Gate 1 coding) · `coding` (bao gồm cả migration/backfill, tách task riêng nếu cần) · `test` (tạo/update testcase) · `execute-test` (thực thi testcase có sẵn) · `other` (update tài liệu không thuộc BA/QA/Dev, vd Rules) |
143
155
  | `title` | Ngắn, hành động rõ (vd "Cập nhật flow OTP theo feedback khách — tăng thời hạn 30s → 90s") |
144
- | `track` | BA / Dev / QA — để PM biết ai sẽ nhận task này |
156
+ | `track` | BA / Dev / QA / Analyst — để PM biết ai sẽ nhận task này |
145
157
  | `description` | Theo đúng template mục 6.1 `PM Workflow_v1.0.md`: |
146
158
 
159
+ Mỗi `type` map thẳng vào 1 task type có sẵn khi người nhận chạy `ak use TICKET-XXX` (không cần Gate workflow mới — xem `scripts/use.js` cho danh sách đầy đủ):
160
+
161
+ | `type` task | Task type chọn ở `ak use` |
162
+ |---|---|
163
+ | `spec` | 📋 Create Spec |
164
+ | `system-requirement` | 📐 Create System Requirement |
165
+ | `impact-analysis` | 📊 Impact Analysis |
166
+ | `coding` | 🐛 Bug Fix / ✨ Feature / 🔄 Refactor (tuỳ nội dung task) |
167
+ | `test` | ✅ Create TestCase |
168
+ | `execute-test` | ▶️ Execute Test |
169
+ | `other` | Không map task type nào — tuỳ nội dung, người nhận tự xử lý ngoài Gate workflow |
170
+
147
171
  ```text
148
172
  ## Nội dung task
149
173
  [Mô tả task do AI đề xuất]
@@ -190,33 +214,57 @@ Check LOCAL trước, chỉ hỏi khi thật sự chưa biết (Vấn đề 5.1)
190
214
  2. Đã có → dùng luôn, hiển thị rõ cho PM biết đang dùng project nào + nguồn (đã lưu từ lần trước).
191
215
  3. Chưa có → hiển thị danh sách project vừa fetch được, PM chọn 1 → lưu lại bằng `ak backlog-set-default-project <id> <key>` hoặc `ak jira-set-default-project <key>` — để lần sau không hỏi lại.
192
216
 
217
+ ### 5.6b Bước 6b — Ticket cha (chỉ hỏi khi danh sách có ≥ 2 task)
218
+
219
+ Nếu danh sách chỉ có 1 task, không có gì để gộp — coi `parentTicket.mode = "none"` và bỏ qua bước này.
220
+
221
+ Nếu ≥ 2 task, hỏi đúng 1 câu:
222
+
223
+ ```text
224
+ Gộp N task này dưới 1 ticket cha?
225
+ 1. Tạo mới ticket cha
226
+ 2. Dùng ticket cha có sẵn — cho biết ticket ID
227
+ 3. Không cần ticket cha
228
+ ```
229
+
230
+ - **(1)** → hỏi PM 1 tiêu đề ngắn cho ticket cha (gợi ý: dùng lại "Tổng quan nội dung thay đổi" đã soạn cho task `spec` ở Bước 3, nếu có; không có thì tự tóm tắt 1 câu). `parentTicket = { "mode": "create", "title": "...", "description": "..." }`.
231
+ - **(2)** → PM nhập ticket ID có sẵn. `parentTicket = { "mode": "existing", "existingId": "<ID do PM nhập>" }`. Việc xác minh ticket đó có tồn tại thật diễn ra ở Bước 7 (khi gọi `ak tasks create-tickets`) — không tự coi là đúng trước khi có kết quả lệnh.
232
+ - **(3)** → `parentTicket = { "mode": "none" }`.
233
+
234
+ ⚠️ **Chưa xác minh trên instance thật:** liên kết cha-con cần Backlog project đã bật tính năng phân cấp issue (Subtasking), hoặc Jira project hỗ trợ field `parent` trực tiếp (rõ nhất với project dạng team-managed; company-managed có thể cần issue type `Subtask` riêng — chưa test). Nếu Bước 7 báo lỗi `parent-create-failed`/`parent-not-found`, hoặc 1 task con lỗi do gán parent thất bại — hiển thị nguyên lỗi cho PM, hỏi PM muốn: thử lại với ticket cha khác, tiếp tục tạo các ticket con không gắn parent (`mode: "none"`), hay dừng lại để PM xử lý cấu hình Backlog/Jira trước.
235
+
193
236
  ### 5.7 Bước 7 — Tạo ticket
194
237
 
195
238
  1. Ghi danh sách task đã duyệt ra 1 file JSON tạm (vd `.aiflow/tmp/tasks-[ticketId].json`), đúng shape mà `ak tasks create-tickets` đọc (xem `scripts/ticket-writer.js`):
196
239
  ```json
197
240
  {
198
241
  "target": "backlog",
242
+ "parentTicket": { "mode": "create", "title": "...", "description": "..." },
199
243
  "tasks": [
200
244
  { "type": "coding", "title": "...", "description": "..." }
201
245
  ]
202
246
  }
203
247
  ```
248
+ `parentTicket` lấy nguyên từ kết quả Bước 6b (bỏ field này hoặc `{"mode": "none"}` nếu không gộp ticket cha).
204
249
  2. Chạy `ak tasks create-tickets <file> --json`.
205
250
  3. Đọc kết quả JSON trả về:
206
251
  - `{"error":"missing-write-credentials", "field": "...", "message": "..."}` → **đây không phải lỗi hệ thống** — hiển thị đúng `message` cho PM, hỏi PM nhập giá trị key ngay trong hội thoại (dùng luôn khung chat làm nơi PM "nhập & submit"), nhận giá trị → chạy `ak credentials set <field> "<giá trị PM vừa nhập>"` → chạy lại bước 2 (retry đúng 1 lần; nếu vẫn lỗi, báo PM key có thể sai/chưa đủ quyền, không tự thử lại vô hạn).
207
252
  - `{"error":"missing-project", ...}` → quay lại Bước 6 (chưa xác định được project).
208
- - Thành công (`ok: true`)mỗi task `ticketId` + `url` riêng; task nào `ok: false` (lỗi phía Backlog/Jira, vd thiếu field) báo cho PM, các task khác đã tạo vẫn giữ nguyên (không rollback).
253
+ - `{"error":"parent-not-found", "message": "..."}` → ticket cha PM cung cấp Bước 6b (2) không tồn tại/không đọc được báo PM, quay lại Bước 6b để nhập ID khác hoặc chọn phương án khác.
254
+ - `{"error":"parent-create-failed", "message": "..."}` → tạo ticket cha thất bại (thường do project chưa hỗ trợ phân cấp issue) — hiển thị nguyên lỗi, hỏi PM có muốn tạo lại không gắn ticket cha (`mode: "none"`) hay dừng lại.
255
+ - Thành công (`ok: true`) → mỗi task có `ticketId` + `url` riêng, có thêm `parent` (ticket cha vừa tạo/dùng, nếu có); task nào `ok: false` (lỗi phía Backlog/Jira, vd thiếu field) → báo rõ cho PM, các task khác đã tạo vẫn giữ nguyên (không rollback).
209
256
 
210
257
  ### 5.8 Bước 8 — Ghi ngược liên kết + đóng Gate
211
258
 
212
- 1. Append vào cuối đúng entry gốc (Meetings-Log/QnA-Log/Confirmations-Log) 1 dòng:
259
+ 1. Append vào cuối đúng entry gốc (Meetings-Log/QnA-Log/Confirmations-Log) 1 dòng, kèm ticket cha nếu có:
213
260
  ```text
214
- → Tasks created: TICKET-101 (coding), TICKET-102 (test)
261
+ → Tasks created: TICKET-100 (parent), TICKET-101 (coding), TICKET-102 (test)
215
262
  ```
216
263
  Việc sửa file này đi qua đúng branch đang dùng ở Gate 2 (không tạo MR riêng — gộp vào cùng thay đổi, hoặc nếu MR đã mở/merge thì tạo 1 commit nhỏ tiếp theo trên cùng branch/1 MR mới tuỳ trạng thái branch lúc đó).
217
264
  2. Hiển thị tổng kết:
218
265
  ```text
219
266
  ✅ GATE 3 DONE — Đã tạo N/N ticket trên [Backlog/Jira]:
267
+ - Ticket cha: TICKET-100 — <url> (bỏ dòng này nếu không gộp ticket cha)
220
268
  - TICKET-101 (coding) — <url>
221
269
  - TICKET-102 (test) — <url>
222
270
  → Dev/QA chạy `ak use TICKET-XXX` trên từng ticket để bắt đầu Gate tương ứng (Coding/QA workflow hiện có).
@@ -18,6 +18,8 @@
18
18
 
19
19
  Nếu `AK-Docs/` hoặc `Shared-Docs/` chưa tồn tại tại workspace root, hoặc không phải git repo → bỏ qua bước tương ứng, không cảnh báo.
20
20
 
21
+ > ❌ **KHÔNG** tự ý liệt kê (`git branch -a`), checkout, hoặc đọc/diff nhiều branch trong repo source để tự dò tìm "nhánh đang phát triển" của feature — rất tốn token và thời gian, và dễ đọc nhầm code chưa hoàn chỉnh/chưa merge. Bước 1 chỉ `git pull` đúng branch hiện tại đang checkout (thường là `main`/`develop`). Nếu cần biết chính xác code nào đã thay đổi cho ticket này → xem bước **Dev Artifacts Check** (Bước 1.5, Gate 1, bên dưới): hỏi TESTER cung cấp PR link hoặc commit SHA/branch name cụ thể, rồi fetch/diff **đúng** phạm vi đó qua skill `pr-impact-analysis`.
22
+
21
23
  **Nếu bất kỳ lệnh `git pull` nào ở Bước 1–3 thất bại** → **KHÔNG dừng workflow** — hiển thị cảnh báo và tiếp tục gate với dữ liệu local hiện có:
22
24
 
23
25
  ```
@@ -33,6 +35,7 @@ Các skill sau đây được cài tự động vào `.claude/skills/test-skills
33
35
 
34
36
  | Skill | File |
35
37
  |---|---|
38
+ | `PR Impact Analysis` | `.claude/skills/pr-impact-analysis/SKILL.md` |
36
39
  | `QA Writing Standards` | `.claude/skills/test-skills/rules/qa-writing-standards.md` |
37
40
  | `Directory & Naming Convention` | `.claude/skills/test-skills/rules/directory-and-naming-convention.md` |
38
41
  | `Template TestCase` | `.claude/skills/test-skills/template/testcase-template.md` |
@@ -204,6 +207,26 @@ Nếu tất cả điều kiện thỏa mãn → thông báo ngắn rồi tiếp
204
207
  - Nếu có `supplementaryContext[]` → đọc từng item (SRS file, Figma link, API spec, spec MD file)
205
208
  - Nếu có file yêu cầu thô được chỉ định → đọc file đó
206
209
 
210
+ #### Bước 1.5: Dev Artifacts Check (PR/Commit-based)
211
+
212
+ **Mục tiêu:** xác định **chính xác** phạm vi code đã thay đổi cho ticket này, để mở rộng coverage (regression scope) mà **không** phải tự dò/đọc toàn bộ branch đang phát triển của feature (tốn token, tốn thời gian, dễ đọc nhầm code chưa xong).
213
+
214
+ 1. Kiểm tra ticket/context có PR link đính kèm không (`.aiflow/context/current.json`, mô tả ticket).
215
+ 2. **Có PR link, hoặc TESTER đã khai báo `PR: <url>` trong chat** → **INVOKE** skill `pr-impact-analysis` ngay, dùng PR đó.
216
+ 3. **Chưa có PR/commit nào được biết** → hỏi TESTER **một câu duy nhất**:
217
+
218
+ ```text
219
+ Để xác định đúng phạm vi ảnh hưởng code (tránh phải rà toàn bộ branch), bạn cung cấp giúp PR link hoặc commit SHA/branch name của thay đổi cho ticket này (có thể nhiều PR/commit nếu multi-repo). Nếu chưa có, gõ "chưa có" để bỏ qua bước này.
220
+ ```
221
+
222
+ 4. Xử lý câu trả lời của TESTER:
223
+ - Cung cấp PR/commit/branch → **INVOKE** skill `pr-impact-analysis` với thông tin đó (dùng fallback `git fetch` + `git diff main...<branch>` nếu không phải PR).
224
+ - Trả lời "chưa có" → ghi nhận vào `test-plan/impact-analysis.md` (hoặc tương đương) "chưa có dev artifacts — sẽ re-check ở Gate 3", **không block Gate 1**.
225
+ 5. Kết quả `pr-impact-analysis` (màn hình ảnh hưởng trực tiếp/gián tiếp, đề xuất regression TCs) được dùng làm input bổ sung cho Bước 2 và Gate 2 (Scenario Building) — **không** dùng để xác định expected result (vẫn lấy từ ticket/UC Spec/System Requirement).
226
+
227
+ > ❌ **KHÔNG** tự ý `git branch -a`, checkout, hoặc đọc lần lượt nhiều branch để tìm "code đang phát triển" — luôn đi qua PR/commit cụ thể mà TESTER xác nhận, qua skill `pr-impact-analysis`.
228
+ > Ở Gate 3 (trước khi thiết kế TC chi tiết), re-run bước này nếu đã có `impact-analysis.md` từ Gate 1 — dùng Delta Detection của `pr-impact-analysis` (so SHA cũ/mới) để bắt commit mới, không đọc lại toàn bộ diff.
229
+
207
230
  #### Bước 2: Phân tích yêu cầu và đánh giá rủi ro
208
231
  - **READ skill:** `.claude/skills/test-skills/rules/qa-writing-standards.md` — đọc trước để nắm quy ước chung
209
232
  - **READ skill:** `.claude/skills/test-skills/categories/00-core/00.01.requirement-analysis.md`
@@ -357,6 +380,10 @@ Nếu tất cả điều kiện thỏa mãn → thông báo ngắn rồi tiếp
357
380
 
358
381
  **Pre-flight (bắt buộc, chạy trước Bước 1):** chạy [Pre-flight — Đồng bộ Source & Docs](#pre-flight-bắt-buộc--đồng-bộ-source--docs-đầu-mỗi-gate) ở đầu file. Lỗi → hiển thị ⚠️ cảnh báo, không dừng gate.
359
382
 
383
+ #### Bước 0.7: Dev Artifacts Check — Delta Detection
384
+
385
+ Nếu `test-plan/impact-analysis.md` đã có PR/commit từ Gate 1 (Bước 1.5) → re-run `pr-impact-analysis` (Delta Detection): so SHA đã ghi với SHA hiện tại, nếu có commit mới thì chỉ đọc diff phần thêm mới, bổ sung TC nếu cần. Không có gì mới hoặc chưa từng có PR → bỏ qua, không cần hỏi lại TESTER.
386
+
360
387
  #### Bước 1: Đọc template và quy ước bắt buộc
361
388
  - **READ template:** `.claude/skills/test-skills/template/testcase-template.md` — **BẮT BUỘC** tuân thủ 100%
362
389
  - **READ skill:** `.claude/skills/test-skills/rules/qa-writing-standards.md`
@@ -545,15 +572,16 @@ Sau khi Gate 4 đã APPROVED (bộ Test Case hoàn thành):
545
572
 
546
573
  | Gate | Skills đọc | Output |
547
574
  |---|---|---|
548
- | Gate 1 | `QA_Writing_Standards`, `00.01 Requirement Analysis`, `00.02 Risk Analysis` | `01_Requirement_Analysis_Result.md`, `01_QA.md` |
575
+ | Gate 1 | `pr-impact-analysis` (Bước 1.5), `QA_Writing_Standards`, `00.01 Requirement Analysis`, `00.02 Risk Analysis` | `01_Requirement_Analysis_Result.md`, `01_QA.md` |
549
576
  | Gate 2 | `00.03 Scenario Builder`, `01.06 Error Guessing`, `03.02 Workflow Testing`, Domain skills | `02_Test_Scenarios_Result.md`, `02_QA.md` |
550
- | Gate 3 | `testcase-template`, `01.01 BVA`, `01.02 EP`, `01.03 Decision Table`, `01.06 Error Guessing`, `02.01`–`02.04`, `03.01`, `03.03`, `04.01`, `04.04` | `03_Test_Cases_Draft_Result.md`, `03_QA.md` |
577
+ | Gate 3 | `pr-impact-analysis` (Delta Detection nếu có PR từ Gate 1), `testcase-template`, `01.01 BVA`, `01.02 EP`, `01.03 Decision Table`, `01.06 Error Guessing`, `02.01`–`02.04`, `03.01`, `03.03`, `04.01`, `04.04` | `03_Test_Cases_Draft_Result.md`, `03_QA.md` |
551
578
  | Gate 4 | `00.04 TC Review`, `99.01 Coverage Review` | `04_Final_Result.md`, `04_QA.md`, `[functionId]_TestCase.md` |
552
579
 
553
580
  ---
554
581
 
555
582
  ### Quy tắc bắt buộc
556
583
 
584
+ - ❌ **KHÔNG** tự ý liệt kê/checkout/đọc nhiều branch trong repo source để dò tìm thay đổi code — xác định phạm vi ảnh hưởng code phải qua PR link hoặc commit SHA/branch cụ thể do TESTER cung cấp (Bước 1.5, Gate 1) + skill `pr-impact-analysis`
557
585
  - ❌ **KHÔNG** bỏ qua thứ tự Gate — luôn đi từ Gate 1 → 2 → 3 → 4
558
586
  - ❌ **KHÔNG** tự suy diễn nghiệp vụ — ghi rõ Assumption và TBD khi thiếu thông tin
559
587
  - ❌ **KHÔNG** sinh `04_Final_Result.md` mà thiếu `[functionId]_TestCase.md` (hoặc ngược lại)
@@ -725,12 +725,13 @@ Bugs logged: [N] | Skipped: [N]
725
725
  > Implements "Vấn đề 5" trong `docs/internal/PM Workflow_v1.0.md`. **Không chờ APPROVED để bắt đầu** — chạy tự động ngay sau Gate 2 Bước 7, không cần lệnh riêng và không cần chờ PM merge MR (merge là việc độc lập của PM). `INVOKE`: `ingest-data` skill § 5 cho toàn bộ logic phân tích/đề xuất — mục này chỉ nêu gate mechanics.
726
726
 
727
727
  1. Đọc lại entry vừa ghi + điều tra source/docs liên quan (skill § 5.1).
728
- 2. Quyết định content này có cần task không (skill § 5.2). Nếu KHÔNG cần task → hiển thị thông báo tương ứng, chạy `ak gate 3 skip --ticket [ticket-id] --reason "<1 câu lý do>"` (không chạy `ak gate 3 start`), và **kết thúc luôn, không có Gate 3 Display**. `skip` vẫn đóng gate đúng cách (advance `currentGate` giống `approved`) — task hiện đúng "done" trên dashboard, không treo ở trạng thái "sẵn sàng chạy Gate 3" mãi.
728
+ 2. Quyết định content này có cần task không (skill § 5.2 — với entry từ `Confirmations-Log`, chạy qua checklist tối thiểu thay vì chỉ dựa Action items viết sẵn). Nếu KHÔNG cần task → hiển thị thông báo tương ứng, chạy `ak gate 3 skip --ticket [ticket-id] --reason "<1 câu lý do>"` (không chạy `ak gate 3 start`), và **kết thúc luôn, không có Gate 3 Display**. `skip` vẫn đóng gate đúng cách (advance `currentGate` giống `approved`) — task hiện đúng "done" trên dashboard, không treo ở trạng thái "sẵn sàng chạy Gate 3" mãi.
729
729
  3. Nếu CẦN task → chạy `ak gate 3 start --ticket [ticket-id]`, rồi đề xuất danh sách task (skill § 5.3), cho PM sửa/thêm/xoá (skill § 5.4), lặp tới khi PM hài lòng.
730
730
  4. Điểm dừng xác nhận trước khi tạo ticket (skill § 5.5) — ❌ không tự thêm `--yes`, phải có xác nhận rõ ràng trong hội thoại.
731
731
  5. Xác định project đích trên Backlog/Jira (skill § 5.6, dùng `ak backlog-projects`/`ak jira-projects` + `ak backlog-set-default-project`/`ak jira-set-default-project`).
732
- 6. Tạo ticket bằng `ak tasks create-tickets <file>.json --json` (skill § 5.7). Nếu thiếu WRITE credentials, hỏi PM nhập ngay trong chat rồi `ak credentials set <field> "<value>"` thử lại đúng 1 lần.
733
- 7. Ghi ngược link ticket vào entry gốc + hiển thị tổng kết (skill § 5.8).
732
+ 6. Nếu danh sách 2 task, hỏi PM muốn gộp dưới 1 ticket cha không tạo mới / dùng sẵn / không cần (skill § 5.6b).
733
+ 7. Tạo ticket bằng `ak tasks create-tickets <file>.json --json` (skill § 5.7), kèm `parentTicket` nếu Bước 6 xác định có. Nếu thiếu WRITE credentials, hỏi PM nhập ngay trong chat rồi `ak credentials set <field> "<value>"` và thử lại đúng 1 lần. Nếu lỗi `parent-not-found`/`parent-create-failed`, hiển thị nguyên lỗi và hỏi PM muốn thử ticket cha khác, bỏ qua ticket cha, hay dừng lại.
734
+ 8. Ghi ngược link ticket vào entry gốc + hiển thị tổng kết (skill § 5.8).
734
735
  8. Run: `ak gate 3 approved --ticket [ticket-id]` để đóng task.
735
736
 
736
737
  > **Telemetry:** Run `ak gate 3 start --ticket [ticket-id]` chỉ khi Bước 2 kết luận CẦN task. Run `ak gate 3 approved --ticket [ticket-id]` khi Gate 3 hoàn tất, hoặc `ak gate 3 skip --ticket [ticket-id] --reason "..."` khi Bước 2 kết luận KHÔNG cần task.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relipa/ai-flow-kit",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0",
4
4
  "description": "All-in-one AI Flow Kit for team development with Claude AI - skills, templates, and MCP adapters",
5
5
  "author": "Example Team",
6
6
  "publishConfig": {
@@ -197,24 +197,41 @@ async function fetchBacklogPriorities(domain, apiKey) {
197
197
  return httpsGet(`https://${domain}/api/v2/priorities?apiKey=${apiKey}`);
198
198
  }
199
199
 
200
+ /**
201
+ * Fetches a single Backlog issue — used to validate a PM-supplied "ticket cha có sẵn" before
202
+ * using it as `parentIssueId` for a batch of child issues (PM Workflow_v1.0.md Vấn đề 5, ticket cha).
203
+ * Returns the internal numeric `id` (needed for `parentIssueId`, which is NOT the issueKey).
204
+ */
205
+ async function getBacklogIssue(domain, apiKey, issueIdOrKey) {
206
+ const issue = await httpsGet(`https://${domain}/api/v2/issues/${issueIdOrKey}?apiKey=${apiKey}`);
207
+ return { id: issue.id, issueKey: issue.issueKey, projectId: issue.projectId, summary: issue.summary || '' };
208
+ }
209
+
200
210
  /**
201
211
  * Creates a Backlog issue. Requires an apiKey with WRITE permission — distinct from the
202
212
  * read-only key used elsewhere in this file for `ak fetch-links`/`ak use`.
203
213
  * `projectId`, `summary`, `issueTypeId`, `priorityId` are mandatory per Backlog's API.
214
+ *
215
+ * `parentIssueId` (optional) — Backlog's internal numeric issue `id` (not the `issueKey`) to
216
+ * link this as a child issue. ⚠️ NOT verified against a live space: requires the target project
217
+ * to have issue hierarchy ("Subtasking") enabled — if it isn't, Backlog rejects the request and
218
+ * the caller should surface the raw error rather than assume the link succeeded.
204
219
  */
205
- async function createBacklogIssue(domain, apiKeyWrite, { projectId, summary, description, issueTypeId, priorityId }) {
220
+ async function createBacklogIssue(domain, apiKeyWrite, { projectId, summary, description, issueTypeId, priorityId, parentIssueId }) {
221
+ const payload = {
222
+ projectId: String(projectId),
223
+ summary,
224
+ issueTypeId: String(issueTypeId),
225
+ priorityId: String(priorityId),
226
+ description: description || '',
227
+ };
228
+ if (parentIssueId) payload.parentIssueId = String(parentIssueId);
206
229
  const issue = await httpsPost(
207
230
  `https://${domain}/api/v2/issues?apiKey=${apiKeyWrite}`,
208
- {
209
- projectId: String(projectId),
210
- summary,
211
- issueTypeId: String(issueTypeId),
212
- priorityId: String(priorityId),
213
- description: description || '',
214
- },
231
+ payload,
215
232
  { bodyType: 'form' }
216
233
  );
217
- return { ticketId: issue.issueKey, url: `https://${domain}/view/${issue.issueKey}`, raw: issue };
234
+ return { ticketId: issue.issueKey, internalId: issue.id, url: `https://${domain}/view/${issue.issueKey}`, raw: issue };
218
235
  }
219
236
 
220
237
  function jiraAuth(email, token) {
@@ -262,25 +279,44 @@ async function fetchJiraComment(domain, email, token, ticketId, commentId) {
262
279
 
263
280
  // ── Jira WRITE — issue creation (PM Workflow_v1.0.md "Vấn đề 5") ────────────
264
281
 
282
+ /**
283
+ * Fetches a single Jira issue — used to validate a PM-supplied "ticket cha có sẵn" before
284
+ * using its key as `parent` for a batch of child issues.
285
+ */
286
+ async function getJiraIssue(domain, email, token, ticketId) {
287
+ const issue = await httpsGet(
288
+ `https://${domain}/rest/api/3/issue/${ticketId}`,
289
+ { Authorization: jiraAuth(email, token), Accept: 'application/json' }
290
+ );
291
+ const fields = issue.fields || {};
292
+ return { key: issue.key, projectKey: fields.project ? fields.project.key : null, summary: fields.summary || '' };
293
+ }
294
+
265
295
  /**
266
296
  * Creates a Jira issue. `apiTokenWrite`/`emailWrite` are distinct from the read-only
267
297
  * credentials used elsewhere in this file — same "separate WRITE key" policy as Backlog.
298
+ *
299
+ * `parentKey` (optional) — Jira issue key to link this as a child via `fields.parent`.
300
+ * ⚠️ NOT verified against a live instance: this field works directly for team-managed
301
+ * ("next-gen") projects; company-managed ("classic") projects may require the child's
302
+ * `issuetype` to be a dedicated `Subtask` type instead — if Jira rejects the request,
303
+ * surface the raw error rather than assume the link succeeded.
268
304
  */
269
- async function createJiraIssue(domain, emailWrite, apiTokenWrite, { projectKey, summary, description, issueTypeName }) {
305
+ async function createJiraIssue(domain, emailWrite, apiTokenWrite, { projectKey, summary, description, issueTypeName, parentKey }) {
306
+ const fields = {
307
+ project: { key: projectKey },
308
+ summary,
309
+ issuetype: { name: issueTypeName || 'Task' },
310
+ description: {
311
+ type: 'doc',
312
+ version: 1,
313
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: description || '' }] }],
314
+ },
315
+ };
316
+ if (parentKey) fields.parent = { key: parentKey };
270
317
  const issue = await httpsPost(
271
318
  `https://${domain}/rest/api/3/issue`,
272
- {
273
- fields: {
274
- project: { key: projectKey },
275
- summary,
276
- issuetype: { name: issueTypeName || 'Task' },
277
- description: {
278
- type: 'doc',
279
- version: 1,
280
- content: [{ type: 'paragraph', content: [{ type: 'text', text: description || '' }] }],
281
- },
282
- },
283
- },
319
+ { fields },
284
320
  { headers: { Authorization: jiraAuth(emailWrite, apiTokenWrite), Accept: 'application/json' }, bodyType: 'json' }
285
321
  );
286
322
  return { ticketId: issue.key, url: `https://${domain}/browse/${issue.key}`, raw: issue };
@@ -365,6 +401,6 @@ module.exports = {
365
401
  fetchBacklogTicket, fetchBacklogComment,
366
402
  fetchBacklogDocument, fetchBacklogDocumentComment, fetchBacklogWiki,
367
403
  fetchJiraTicket, fetchJiraComment,
368
- fetchBacklogIssueTypes, fetchBacklogPriorities, createBacklogIssue,
369
- createJiraIssue,
404
+ fetchBacklogIssueTypes, fetchBacklogPriorities, createBacklogIssue, getBacklogIssue,
405
+ createJiraIssue, getJiraIssue,
370
406
  };
@@ -24,7 +24,9 @@ const {
24
24
  fetchBacklogIssueTypes,
25
25
  fetchBacklogPriorities,
26
26
  createBacklogIssue,
27
+ getBacklogIssue,
27
28
  createJiraIssue,
29
+ getJiraIssue,
28
30
  } = linkResolver;
29
31
 
30
32
  // Only these keys can be written via `ak credentials set` — dedicated adapter credentials
@@ -104,13 +106,23 @@ async function pickBacklogDefaults(domain, apiKeyRead, projectId) {
104
106
  * "target": "backlog" | "jira",
105
107
  * "projectId": "12345", // optional — Backlog only, overrides BACKLOG_DEFAULT_PROJECT_ID
106
108
  * "projectKey": "PROJ", // optional — Jira only, overrides JIRA_DEFAULT_PROJECT_KEY
109
+ * "parentTicket": { // optional — ingest-data SKILL.md § 5.6b "Ticket cha"
110
+ * "mode": "create" | "existing" | "none",
111
+ * "title": "...", // required when mode === "create"
112
+ * "description": "...", // optional when mode === "create"
113
+ * "existingId": "..." // required when mode === "existing" (Backlog key/id or Jira key)
114
+ * },
107
115
  * "tasks": [
108
- * { "type": "spec|coding|test|other", "title": "...", "description": "..." }
116
+ * { "type": "spec|system-requirement|impact-analysis|coding|test|execute-test|other", "title": "...", "description": "..." }
109
117
  * ]
110
118
  * }
111
119
  *
112
120
  * `description` is expected to already be fully composed by the AI per the ticket template in
113
121
  * PM Workflow_v1.0.md §6.1 (Nội dung task + Nguồn tham chiếu) — this command posts it as-is.
122
+ *
123
+ * When `parentTicket.mode` is "create" or "existing", every task in `tasks` is created as a
124
+ * child of that parent (Backlog `parentIssueId` / Jira `parent`) — see link-resolver.js for the
125
+ * caveat that this depends on the target project supporting issue hierarchy/subtasks.
114
126
  */
115
127
  async function createTicketsCommand(inputFile, options = {}) {
116
128
  const emit = (obj) => {
@@ -119,6 +131,7 @@ async function createTicketsCommand(inputFile, options = {}) {
119
131
  console.log(chalk.red(`✗ ${obj.message || obj.error}`));
120
132
  } else {
121
133
  console.log(chalk.green(`✓ ${obj.results.filter(r => r.ok).length}/${obj.results.length} ticket(s) created on ${obj.target}.`));
134
+ if (obj.parent) console.log(` ${chalk.cyan('↳ parent:')} ${obj.parent.ticketId} ${chalk.gray(obj.parent.url)}`);
122
135
  for (const r of obj.results) {
123
136
  console.log(r.ok ? ` ${chalk.green('✓')} ${r.ticketId} ${chalk.gray(r.title)}` : ` ${chalk.red('✗')} ${chalk.gray(r.title)} — ${r.error}`);
124
137
  }
@@ -173,6 +186,34 @@ async function createTicketsCommand(inputFile, options = {}) {
173
186
  return;
174
187
  }
175
188
 
189
+ const parentTicket = input.parentTicket || { mode: 'none' };
190
+ let parentRef = null;
191
+ if (parentTicket.mode === 'create') {
192
+ try {
193
+ const createdParent = await createBacklogIssue(write.domain, write.apiKeyWrite, {
194
+ projectId,
195
+ summary: parentTicket.title,
196
+ description: parentTicket.description || '',
197
+ issueTypeId: defaults.defaultIssueTypeId,
198
+ priorityId: defaults.defaultPriorityId,
199
+ });
200
+ parentRef = { ticketId: createdParent.ticketId, internalId: createdParent.internalId, url: createdParent.url };
201
+ } catch (err) {
202
+ emit({ ok: false, error: 'parent-create-failed', message: `Không tạo được ticket cha trên Backlog: ${err.message}` });
203
+ process.exitCode = 1;
204
+ return;
205
+ }
206
+ } else if (parentTicket.mode === 'existing') {
207
+ try {
208
+ const existing = await getBacklogIssue(write.domain, write.apiKeyRead, parentTicket.existingId);
209
+ parentRef = { ticketId: existing.issueKey, internalId: existing.id, url: `https://${write.domain}/view/${existing.issueKey}` };
210
+ } catch (err) {
211
+ emit({ ok: false, error: 'parent-not-found', message: `Không tìm thấy ticket cha "${parentTicket.existingId}" trên Backlog: ${err.message}` });
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ }
216
+
176
217
  const results = [];
177
218
  for (const task of tasks) {
178
219
  try {
@@ -182,13 +223,14 @@ async function createTicketsCommand(inputFile, options = {}) {
182
223
  description: task.description,
183
224
  issueTypeId: task.issueTypeId || defaults.defaultIssueTypeId,
184
225
  priorityId: task.priorityId || defaults.defaultPriorityId,
226
+ parentIssueId: parentRef ? parentRef.internalId : undefined,
185
227
  });
186
228
  results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
187
229
  } catch (err) {
188
230
  results.push({ type: task.type, title: task.title, ok: false, error: err.message });
189
231
  }
190
232
  }
191
- emit({ ok: true, target, projectId, results });
233
+ emit({ ok: true, target, projectId, parent: parentRef, results });
192
234
  return;
193
235
  }
194
236
 
@@ -206,6 +248,32 @@ async function createTicketsCommand(inputFile, options = {}) {
206
248
  return;
207
249
  }
208
250
 
251
+ const parentTicket = input.parentTicket || { mode: 'none' };
252
+ let parentRef = null;
253
+ if (parentTicket.mode === 'create') {
254
+ try {
255
+ const createdParent = await createJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, {
256
+ projectKey,
257
+ summary: parentTicket.title,
258
+ description: parentTicket.description || '',
259
+ });
260
+ parentRef = { ticketId: createdParent.ticketId, url: createdParent.url };
261
+ } catch (err) {
262
+ emit({ ok: false, error: 'parent-create-failed', message: `Không tạo được ticket cha trên Jira: ${err.message}` });
263
+ process.exitCode = 1;
264
+ return;
265
+ }
266
+ } else if (parentTicket.mode === 'existing') {
267
+ try {
268
+ const existing = await getJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, parentTicket.existingId);
269
+ parentRef = { ticketId: existing.key, url: `https://${write.domain}/browse/${existing.key}` };
270
+ } catch (err) {
271
+ emit({ ok: false, error: 'parent-not-found', message: `Không tìm thấy ticket cha "${parentTicket.existingId}" trên Jira: ${err.message}` });
272
+ process.exitCode = 1;
273
+ return;
274
+ }
275
+ }
276
+
209
277
  const results = [];
210
278
  for (const task of tasks) {
211
279
  try {
@@ -214,13 +282,14 @@ async function createTicketsCommand(inputFile, options = {}) {
214
282
  summary: task.title,
215
283
  description: task.description,
216
284
  issueTypeName: task.issueType || 'Task',
285
+ parentKey: parentRef ? parentRef.ticketId : undefined,
217
286
  });
218
287
  results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
219
288
  } catch (err) {
220
289
  results.push({ type: task.type, title: task.title, ok: false, error: err.message });
221
290
  }
222
291
  }
223
- emit({ ok: true, target, projectKey, results });
292
+ emit({ ok: true, target, projectKey, parent: parentRef, results });
224
293
  }
225
294
 
226
295
  /**