@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
package/scripts/use.js CHANGED
@@ -8,11 +8,13 @@ const { select, input } = require("@inquirer/prompts");
8
8
  const stringWidth = require("string-width");
9
9
  const CATEGORY_COLOR = {
10
10
  Coding: chalk.cyan,
11
+ Dev: chalk.blueBright,
11
12
  Testing: chalk.green,
12
13
  Analysis: chalk.yellow,
13
14
  Document: chalk.magenta,
14
15
  BA: chalk.blue,
15
16
  QA: chalk.greenBright,
17
+ PM: chalk.redBright,
16
18
  };
17
19
  const td = (label, cat, desc) => {
18
20
  const pad = " ".repeat(Math.max(0, 20 - stringWidth(label)));
@@ -32,6 +34,15 @@ const useCommand = async function use(targets, options = {}) {
32
34
  // Normalize: accept string (legacy) or array (new multi-target)
33
35
  const targetList = Array.isArray(targets) ? targets.filter(Boolean) : (targets ? [targets] : []);
34
36
  const primaryTarget = targetList[0] || null;
37
+
38
+ // ── Preview mode: fetch + print JSON to stdout, nothing else — no save, no
39
+ // prompts, no other console output (a caller like a GUI parses stdout
40
+ // as JSON, so nothing may print ahead of it). Must run before every
41
+ // other check/notice below, all of which write informational stdout. ──
42
+ if (options.preview) {
43
+ return await previewTarget(primaryTarget, options);
44
+ }
45
+
35
46
  const supplementaryTargets = targetList.slice(1);
36
47
 
37
48
  if (!(await fs.pathExists(STATE_FILE))) {
@@ -56,7 +67,7 @@ const useCommand = async function use(targets, options = {}) {
56
67
 
57
68
  // ── Manual entry ──
58
69
  if (options.manual) {
59
- return await manualContext();
70
+ return await manualContext("", options);
60
71
  }
61
72
 
62
73
  // ── Load from local file ──
@@ -92,7 +103,7 @@ const useCommand = async function use(targets, options = {}) {
92
103
  console.log(
93
104
  chalk.gray("Supported: PROJ-33, APP-123, version number, --manual"),
94
105
  );
95
- return await manualContext();
106
+ return await manualContext("", options);
96
107
  };
97
108
 
98
109
  // ──────────────────────────────────────────────────────────────
@@ -192,65 +203,87 @@ async function switchVersion(versionReq) {
192
203
  // Backlog context loader
193
204
  // ──────────────────────────────────────────────────────────────
194
205
 
195
- async function loadFromBacklog(issueKey, options = {}) {
196
- const creds = await loadCredentials();
206
+ /** Resolves Backlog credentials from env/config, or null if unset. */
207
+ function resolveBacklogCredentials(creds) {
197
208
  const apiKey = process.env.BACKLOG_API_KEY || creds.BACKLOG_API_KEY;
198
209
  const spaceKey =
199
210
  process.env.BACKLOG_SPACE_KEY ||
200
211
  creds.BACKLOG_SPACE_KEY ||
201
212
  process.env.BACKLOG_DOMAIN ||
202
213
  creds.BACKLOG_DOMAIN;
214
+ if (!apiKey || !spaceKey) return null;
215
+ // spaceKey can be full domain (mycompany.backlog.com) or just the space (mycompany)
216
+ const domain = spaceKey.includes(".") ? spaceKey : `${spaceKey}.backlog.com`;
217
+ return { apiKey, domain };
218
+ }
203
219
 
204
- if (!apiKey || !spaceKey) {
220
+ /**
221
+ * Pure fetch: resolves a Backlog issue into internal context format.
222
+ * Throws on missing credentials or fetch failure — never prompts, never saves.
223
+ * Used by both `loadFromBacklog` (interactive) and `previewTarget` (headless).
224
+ */
225
+ async function fetchBacklogContext(issueKey, options = {}) {
226
+ const creds = await loadCredentials();
227
+ const resolved = resolveBacklogCredentials(creds);
228
+ if (!resolved) {
229
+ throw new Error(
230
+ "Backlog credentials not set. Run `aiflow init --adapter backlog` or set BACKLOG_API_KEY / BACKLOG_SPACE_KEY.",
231
+ );
232
+ }
233
+ const { apiKey, domain } = resolved;
234
+
235
+ // Default: description only. Comments loaded only when explicitly requested.
236
+ const loadComments =
237
+ options.coms ||
238
+ options.withComs ||
239
+ options.withComments ||
240
+ options["with-comments"] ||
241
+ options.with_comments ||
242
+ options.cid != null ||
243
+ options.commentId != null ||
244
+ options["comment-id"] != null ||
245
+ options.clast != null ||
246
+ options.commentsLast != null ||
247
+ options.cfrom != null ||
248
+ options.commentsFrom != null ||
249
+ options.cto != null ||
250
+ options.commentsTo != null ||
251
+ options["comments-to"] != null;
252
+
253
+ if (loadComments) {
254
+ console.log(chalk.gray(" ℹ Comments requested..."));
255
+ }
256
+ const [issue, rawComments] = await Promise.all([
257
+ fetchBacklogIssue(domain, apiKey, issueKey),
258
+ loadComments
259
+ ? fetchBacklogComments(domain, apiKey, issueKey)
260
+ : Promise.resolve([]),
261
+ ]);
262
+
263
+ const comments = filterComments(rawComments, options);
264
+ if (loadComments) {
265
+ console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
266
+ }
267
+ return buildContextFromBacklog(issue, comments, issueKey, domain);
268
+ }
269
+
270
+ async function loadFromBacklog(issueKey, options = {}) {
271
+ const creds = await loadCredentials();
272
+ if (!resolveBacklogCredentials(creds)) {
205
273
  console.log(chalk.yellow("⚠ Backlog credentials not set."));
206
274
  console.log(chalk.gray("Run: aiflow init --adapter backlog"));
207
275
  console.log(chalk.gray("Or set environment variables:"));
208
276
  console.log(chalk.gray(" BACKLOG_API_KEY=your-api-key"));
209
277
  console.log(chalk.gray(" BACKLOG_SPACE_KEY=your-space (e.g. mycompany)"));
210
278
  console.log(chalk.gray("\nFalling back to manual entry...\n"));
211
- return await manualContext(issueKey);
279
+ return await manualContext(issueKey, options);
212
280
  }
213
281
 
214
- // spaceKey can be full domain (mycompany.backlog.com) or just the space (mycompany)
215
- const domain = spaceKey.includes(".") ? spaceKey : `${spaceKey}.backlog.com`;
216
-
217
282
  console.log(chalk.blue(`Fetching context from Backlog: ${issueKey}...`));
218
283
 
219
284
  try {
220
- // Default: description only. Comments loaded only when explicitly requested.
221
- const loadComments =
222
- options.coms ||
223
- options.withComs ||
224
- options.withComments ||
225
- options["with-comments"] ||
226
- options.with_comments ||
227
- options.cid != null ||
228
- options.commentId != null ||
229
- options["comment-id"] != null ||
230
- options.clast != null ||
231
- options.commentsLast != null ||
232
- options.cfrom != null ||
233
- options.commentsFrom != null ||
234
- options.cto != null ||
235
- options.commentsTo != null ||
236
- options["comments-to"] != null;
237
-
238
- if (loadComments) {
239
- console.log(chalk.gray(" ℹ Comments requested..."));
240
- }
241
- const [issue, rawComments] = await Promise.all([
242
- fetchBacklogIssue(domain, apiKey, issueKey),
243
- loadComments
244
- ? fetchBacklogComments(domain, apiKey, issueKey)
245
- : Promise.resolve([]),
246
- ]);
247
-
248
- const comments = filterComments(rawComments, options);
249
- if (loadComments) {
250
- console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
251
- }
252
- const context = buildContextFromBacklog(issue, comments, issueKey, domain);
253
- context.taskType = await promptForTaskType(context.taskType);
285
+ const context = await fetchBacklogContext(issueKey, options);
286
+ context.taskType = await promptForTaskType(context.taskType, options.taskType);
254
287
  context.mode = options.full ? "full" : "fast";
255
288
  await saveContext(context, options.save);
256
289
  printContextSummary(context);
@@ -258,7 +291,7 @@ async function loadFromBacklog(issueKey, options = {}) {
258
291
  } catch (err) {
259
292
  console.log(chalk.yellow(`⚠ Could not fetch from Backlog: ${err.message}`));
260
293
  console.log(chalk.gray("Falling back to manual entry...\n"));
261
- await manualContext(issueKey);
294
+ await manualContext(issueKey, options);
262
295
  }
263
296
  }
264
297
 
@@ -327,6 +360,14 @@ function backlogGet(url) {
327
360
  });
328
361
  }
329
362
 
363
+ /**
364
+ * Fetch the list of projects in the Backlog space — used to let the user pick a default
365
+ * project for tasks created manually (no ticket to derive a project from).
366
+ */
367
+ function fetchBacklogProjects(domain, apiKey) {
368
+ return backlogGet(`https://${domain}/api/v2/projects?apiKey=${apiKey}`);
369
+ }
370
+
330
371
  /**
331
372
  * Fetch issue detail from Backlog REST API
332
373
  */
@@ -395,6 +436,8 @@ function buildContextFromBacklog(issue, comments, issueKey, domain) {
395
436
 
396
437
  return {
397
438
  taskId: issueKey,
439
+ projectId: issue.projectId ?? null,
440
+ projectKey: projectKeyFromIssueKey(issueKey),
398
441
  taskType: type,
399
442
  title: issue.summary || "",
400
443
  description: issue.description || "",
@@ -430,13 +473,67 @@ function buildContextFromBacklog(issue, comments, issueKey, domain) {
430
473
  // Jira context loader
431
474
  // ──────────────────────────────────────────────────────────────
432
475
 
433
- async function loadFromJira(issueKey, options = {}) {
434
- const creds = await loadCredentials();
476
+ /** Resolves Jira credentials from env/config, or null if unset. */
477
+ function resolveJiraCredentials(creds) {
435
478
  const apiToken = process.env.JIRA_API_TOKEN || creds.JIRA_API_TOKEN;
436
479
  const email = process.env.JIRA_EMAIL || creds.JIRA_EMAIL;
437
480
  const domain = process.env.JIRA_DOMAIN || creds.JIRA_DOMAIN;
481
+ if (!apiToken || !email || !domain) return null;
482
+ return { apiToken, email, domain };
483
+ }
484
+
485
+ /**
486
+ * Pure fetch: resolves a Jira issue into internal context format.
487
+ * Throws on missing credentials or fetch failure — never prompts, never saves.
488
+ * Used by both `loadFromJira` (interactive) and `previewTarget` (headless).
489
+ */
490
+ async function fetchJiraContext(issueKey, options = {}) {
491
+ const creds = await loadCredentials();
492
+ const resolved = resolveJiraCredentials(creds);
493
+ if (!resolved) {
494
+ throw new Error(
495
+ "Jira credentials not set. Set JIRA_API_TOKEN, JIRA_EMAIL, JIRA_DOMAIN.",
496
+ );
497
+ }
498
+ const { apiToken, email, domain } = resolved;
499
+
500
+ const loadComments =
501
+ options.coms ||
502
+ options.withComs ||
503
+ options.withComments ||
504
+ options["with-comments"] ||
505
+ options.with_comments ||
506
+ options.cid != null ||
507
+ options.commentId != null ||
508
+ options["comment-id"] != null ||
509
+ options.clast != null ||
510
+ options.commentsLast != null ||
511
+ options.cfrom != null ||
512
+ options.commentsFrom != null ||
513
+ options.cto != null ||
514
+ options.commentsTo != null ||
515
+ options["comments-to"] != null;
516
+
517
+ if (loadComments) {
518
+ console.log(chalk.gray(" ℹ Comments requested..."));
519
+ }
520
+ const [issue, rawComments] = await Promise.all([
521
+ fetchJiraIssue(domain, email, apiToken, issueKey),
522
+ loadComments
523
+ ? fetchJiraComments(domain, email, apiToken, issueKey)
524
+ : Promise.resolve([]),
525
+ ]);
526
+
527
+ const comments = filterComments(rawComments, options);
528
+ if (loadComments) {
529
+ console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
530
+ }
531
+ return buildContextFromJira(issue, comments, issueKey, domain);
532
+ }
438
533
 
439
- if (!apiToken || !email || !domain) {
534
+ async function loadFromJira(issueKey, options = {}) {
535
+ const creds = await loadCredentials();
536
+ if (!resolveJiraCredentials(creds)) {
440
537
  console.log(chalk.yellow("⚠ Jira credentials not set."));
441
538
  console.log(chalk.gray("Run: aiflow init --adapter jira"));
442
539
  console.log(
@@ -445,45 +542,14 @@ async function loadFromJira(issueKey, options = {}) {
445
542
  ),
446
543
  );
447
544
  console.log(chalk.gray("\nFalling back to manual entry...\n"));
448
- return await manualContext(issueKey);
545
+ return await manualContext(issueKey, options);
449
546
  }
450
547
 
451
548
  console.log(chalk.blue(`Fetching context from Jira: ${issueKey}...`));
452
549
 
453
550
  try {
454
- const loadComments =
455
- options.coms ||
456
- options.withComs ||
457
- options.withComments ||
458
- options["with-comments"] ||
459
- options.with_comments ||
460
- options.cid != null ||
461
- options.commentId != null ||
462
- options["comment-id"] != null ||
463
- options.clast != null ||
464
- options.commentsLast != null ||
465
- options.cfrom != null ||
466
- options.commentsFrom != null ||
467
- options.cto != null ||
468
- options.commentsTo != null ||
469
- options["comments-to"] != null;
470
-
471
- if (loadComments) {
472
- console.log(chalk.gray(" ℹ Comments requested..."));
473
- }
474
- const [issue, rawComments] = await Promise.all([
475
- fetchJiraIssue(domain, email, apiToken, issueKey),
476
- loadComments
477
- ? fetchJiraComments(domain, email, apiToken, issueKey)
478
- : Promise.resolve([]),
479
- ]);
480
-
481
- const comments = filterComments(rawComments, options);
482
- if (loadComments) {
483
- console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
484
- }
485
- const context = buildContextFromJira(issue, comments, issueKey, domain);
486
- context.taskType = await promptForTaskType(context.taskType);
551
+ const context = await fetchJiraContext(issueKey, options);
552
+ context.taskType = await promptForTaskType(context.taskType, options.taskType);
487
553
  context.mode = options.full ? "full" : "fast";
488
554
  await saveContext(context, options.save);
489
555
  printContextSummary(context);
@@ -491,7 +557,7 @@ async function loadFromJira(issueKey, options = {}) {
491
557
  } catch (err) {
492
558
  console.log(chalk.yellow(`⚠ Could not fetch from Jira: ${err.message}`));
493
559
  console.log(chalk.gray("Falling back to manual entry...\n"));
494
- await manualContext(issueKey);
560
+ await manualContext(issueKey, options);
495
561
  }
496
562
  }
497
563
 
@@ -526,6 +592,64 @@ function fetchJiraIssue(domain, email, apiToken, issueKey) {
526
592
  });
527
593
  }
528
594
 
595
+ /**
596
+ * `ak jira-projects [--json]` — lists the projects visible to the configured Jira account, so the
597
+ * user can pick a default project for tasks created from Meeting Minutes/QnA (see Vấn đề 5.1 in
598
+ * PM Workflow_v1.0.md). Mirrors `listBacklogProjectsCommand` above.
599
+ */
600
+ function fetchJiraProjects(domain, email, apiToken) {
601
+ return new Promise((resolve, reject) => {
602
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
603
+ const url = `https://${domain}.atlassian.net/rest/api/3/project`;
604
+ https
605
+ .get(url, { headers: { Authorization: `Basic ${auth}`, Accept: "application/json" } }, (res) => {
606
+ let data = "";
607
+ res.on("data", (chunk) => (data += chunk));
608
+ res.on("end", () => {
609
+ if (res.statusCode !== 200) {
610
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
611
+ return;
612
+ }
613
+ try { resolve(JSON.parse(data)); }
614
+ catch (e) { reject(new Error("Invalid JSON response from Jira")); }
615
+ });
616
+ })
617
+ .on("error", reject);
618
+ });
619
+ }
620
+
621
+ async function listJiraProjectsCommand(options = {}) {
622
+ const creds = await loadCredentials();
623
+ const resolved = resolveJiraCredentials(creds);
624
+ if (!resolved) {
625
+ const msg = "Jira credentials not set. Run `aiflow init --adapter jira` first.";
626
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: msg })); return; }
627
+ console.log(chalk.yellow(`⚠ ${msg}`));
628
+ return;
629
+ }
630
+ try {
631
+ const projects = await fetchJiraProjects(resolved.domain, resolved.email, resolved.apiToken);
632
+ const list = projects.map((p) => ({ id: p.id, key: p.key, name: p.name }));
633
+ if (options.json) {
634
+ process.stdout.write(JSON.stringify({ ok: true, projects: list, defaultProjectKey: creds.JIRA_DEFAULT_PROJECT_KEY ?? null }));
635
+ return;
636
+ }
637
+ console.log(chalk.cyan(`\nJira projects (${list.length}):`));
638
+ for (const p of list) {
639
+ const isDefault = p.key === creds.JIRA_DEFAULT_PROJECT_KEY;
640
+ console.log(` ${isDefault ? chalk.green("●") : " "} ${p.key} ${chalk.gray(p.name)} ${chalk.gray(`(id: ${p.id})`)}`);
641
+ }
642
+ } catch (err) {
643
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: err.message })); return; }
644
+ console.log(chalk.red(`✗ Could not fetch Jira projects: ${err.message}`));
645
+ }
646
+ }
647
+
648
+ async function setDefaultJiraProjectCommand(key) {
649
+ await saveMcpCredentials({ JIRA_DEFAULT_PROJECT_KEY: key });
650
+ console.log(chalk.green(`✓ Default Jira project set to ${key}.`));
651
+ }
652
+
529
653
  /**
530
654
  * Fetch ALL comments of an issue from Jira REST API.
531
655
  */
@@ -590,6 +714,8 @@ function buildContextFromJira(issue, comments, issueKey, domain) {
590
714
 
591
715
  return {
592
716
  taskId: issueKey,
717
+ projectId: fields.project?.id ?? null,
718
+ projectKey: fields.project?.key ?? projectKeyFromIssueKey(issueKey),
593
719
  taskType: type,
594
720
  title: fields.summary || "",
595
721
  description:
@@ -623,7 +749,13 @@ function buildContextFromJira(issue, comments, issueKey, domain) {
623
749
  // Task type selector — shared by all loaders
624
750
  // ──────────────────────────────────────────────────────────────
625
751
 
626
- async function promptForTaskType(detectedDefault) {
752
+ /**
753
+ * Returns `explicit` immediately (no prompt) when the caller already knows the
754
+ * task type — e.g. `--task-type` was passed by a headless caller. Otherwise
755
+ * falls back to the interactive `select()` prompt as before.
756
+ */
757
+ async function promptForTaskType(detectedDefault, explicit) {
758
+ if (explicit != null && explicit !== "") return explicit;
627
759
  return await select({
628
760
  message: "Task type:",
629
761
  choices: [
@@ -633,10 +765,12 @@ async function promptForTaskType(detectedDefault) {
633
765
  { name: td("▶️ Execute Test", "Testing", "Run existing TC scripts (4-gate flow)"), value: "execute" },
634
766
  { name: td("📋 Create Spec", "BA", "Create UC Spec from raw requirements (4-gate BA)"), value: "create-spec" },
635
767
  { name: td("✅ Create TestCase", "QA", "Create Test Cases from requirements (4-gate QA)"), value: "create-testcase" },
768
+ { name: td("📐 Create System Requirement", "Dev", "Bridge UC Spec → System Requirement (blocks coding Gate 1, 2-gate)"), value: "create-system-requirement" },
636
769
  { name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
637
770
  { name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
638
771
  { name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
639
772
  { name: td("📝 Generate Doc/Spec", "Document", "Generate new documentation/Spec (2-gate)"), value: "gen-doc" },
773
+ { name: td("📥 Ingest Data", "PM", "Ingest Backlog/Jira/SharePoint link or pasted text into AK-Docs logs (2-gate)"), value: "ingest-data" },
640
774
  ],
641
775
  default: detectedDefault || "feature",
642
776
  });
@@ -646,7 +780,7 @@ async function promptForTaskType(detectedDefault) {
646
780
  // Manual context entry
647
781
  // ──────────────────────────────────────────────────────────────
648
782
 
649
- async function manualContext(prefillId = "") {
783
+ async function manualContext(prefillId = "", options = {}) {
650
784
  // Load existing context for pre-fill (Edit mode)
651
785
  let existing = {};
652
786
  const currentPath = path.join(CONTEXT_DIR, "current.json");
@@ -655,71 +789,84 @@ async function manualContext(prefillId = "") {
655
789
  }
656
790
 
657
791
  const isEdit = !!existing.taskId;
658
- if (isEdit) {
659
- console.log(chalk.cyan("\nEdit Context\n"));
660
- console.log(
661
- chalk.gray("Press Enter to keep existing value shown in [brackets]\n"),
662
- );
663
- } else {
664
- console.log(chalk.cyan("\nManual Context Entry\n"));
792
+ // Headless callers (extension popup, scripts) pass fields as flags — skip
793
+ // the corresponding inquirer prompt entirely when a field is supplied.
794
+ if (options.title == null && options.description == null && options.taskType == null) {
795
+ if (isEdit) {
796
+ console.log(chalk.cyan("\nEdit Context\n"));
797
+ console.log(
798
+ chalk.gray("Press Enter to keep existing value shown in [brackets]\n"),
799
+ );
800
+ } else {
801
+ console.log(chalk.cyan("\nManual Context Entry\n"));
802
+ }
665
803
  }
666
804
 
667
805
  // Ticket ID
668
- const defaultId = prefillId || existing.taskId || "";
669
- const idHint = defaultId ? chalk.gray(` [${defaultId}]`) : "";
670
- const idInput = await input({
671
- message: `Ticket ID (e.g. PROJ-33)${idHint}:`,
672
- default: "",
673
- });
674
- const taskId = idInput.trim() || defaultId;
806
+ const defaultId = options.ticketId || prefillId || existing.taskId || "";
807
+ let taskId;
808
+ if (options.ticketId != null) {
809
+ taskId = options.ticketId;
810
+ } else {
811
+ const idHint = defaultId ? chalk.gray(` [${defaultId}]`) : "";
812
+ const idInput = await input({
813
+ message: `Ticket ID (e.g. PROJ-33)${idHint}:`,
814
+ default: "",
815
+ });
816
+ taskId = idInput.trim() || defaultId;
817
+ }
675
818
 
676
819
  // Title
677
- const defaultTitle = existing.title || "";
678
- const titlePreview =
679
- defaultTitle.length > 50
680
- ? defaultTitle.substring(0, 50) + "…"
681
- : defaultTitle;
682
- const titleHint = defaultTitle ? chalk.gray(` [${titlePreview}]`) : "";
683
- const titleInput = await input({
684
- message: `Title${titleHint}:`,
685
- default: "",
686
- });
687
- const title = titleInput.trim() || defaultTitle;
820
+ let title;
821
+ if (options.title != null) {
822
+ title = options.title;
823
+ } else {
824
+ const defaultTitle = existing.title || "";
825
+ const titlePreview =
826
+ defaultTitle.length > 50
827
+ ? defaultTitle.substring(0, 50) + "…"
828
+ : defaultTitle;
829
+ const titleHint = defaultTitle ? chalk.gray(` [${titlePreview}]`) : "";
830
+ const titleInput = await input({
831
+ message: `Title${titleHint}:`,
832
+ default: "",
833
+ });
834
+ title = titleInput.trim() || defaultTitle;
835
+ }
688
836
 
689
837
  // Description
690
- const defaultDesc = existing.description || "";
691
- const descPreview =
692
- defaultDesc.length > 60 ? defaultDesc.substring(0, 60) + "…" : defaultDesc;
693
- const descHint = defaultDesc ? chalk.gray(` [${descPreview}]`) : "";
694
- const descInput = await input({
695
- message: `Description (brief)${descHint}:`,
696
- default: "",
697
- });
698
- const description = descInput.trim() || defaultDesc;
838
+ let description;
839
+ if (options.description != null) {
840
+ description = options.description;
841
+ } else {
842
+ const defaultDesc = existing.description || "";
843
+ const descPreview =
844
+ defaultDesc.length > 60 ? defaultDesc.substring(0, 60) + "" : defaultDesc;
845
+ const descHint = defaultDesc ? chalk.gray(` [${descPreview}]`) : "";
846
+ const descInput = await input({
847
+ message: `Description (brief)${descHint}:`,
848
+ default: "",
849
+ });
850
+ description = descInput.trim() || defaultDesc;
851
+ }
699
852
 
700
853
  // Task type — pre-select existing value if available
701
- const taskType = await select({
702
- message: "Task type:",
703
- choices: [
704
- { name: td("🐛 Bug Fix", "Coding", "Fix bugs, crashes, regressions"), value: "bug-fix" },
705
- { name: td("✨ Feature", "Coding", "Build new features, user stories"), value: "feature" },
706
- { name: td("🔄 Refactor", "Coding", "Improve code without behavior change"), value: "refactor" },
707
- { name: td("▶️ Execute Test", "Testing", "Run existing TC scripts (4-gate flow)"), value: "execute" },
708
- { name: td("📋 Create Spec", "BA", "Create UC Spec from raw requirements (4-gate BA)"), value: "create-spec" },
709
- { name: td("✅ Create TestCase", "QA", "Create Test Cases from requirements (4-gate QA)"), value: "create-testcase" },
710
- { name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
711
- { name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
712
- { name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
713
- { name: td("📝 Generate Doc/Spec", "Document", "Generate new documentation/Spec (2-gate)"), value: "gen-doc" },
714
- ],
715
- default: existing.taskType || undefined,
716
- });
854
+ const taskType = await promptForTaskType(existing.taskType, options.taskType);
855
+
856
+ // Stamp the default Backlog project (`ak backlog-set-default-project`), if one was ever set —
857
+ // manual tasks have no ticket to derive a project from otherwise. Kept for later use: syncing a
858
+ // locally-created task up to Backlog as a new issue needs to know which project it belongs to.
859
+ const creds = await loadCredentials();
860
+ const projectId = existing.projectId ?? creds.BACKLOG_DEFAULT_PROJECT_ID ?? null;
861
+ const projectKey = existing.projectKey ?? creds.BACKLOG_DEFAULT_PROJECT_KEY ?? null;
717
862
 
718
863
  const context = {
719
864
  taskId,
720
865
  taskType,
721
866
  title,
722
867
  description,
868
+ projectId,
869
+ projectKey,
723
870
  status: existing.status || "In Progress",
724
871
  mode: existing.mode || "auto",
725
872
  acceptanceCriteria: existing.acceptanceCriteria || [],
@@ -765,13 +912,51 @@ async function loadFromFile(filePath, options = {}) {
765
912
  context.mode = options.full ? "full" : "fast";
766
913
 
767
914
  // Prompt for task type
768
- context.taskType = await promptForTaskType(context.taskType);
915
+ context.taskType = await promptForTaskType(context.taskType, options.taskType);
769
916
 
770
917
  await saveContext(context, options.save);
771
918
  printContextSummary(context);
772
919
  await suggestNextStep();
773
920
  }
774
921
 
922
+ /**
923
+ * Preview mode (`ak use <target> --preview`): fetches a ticket and prints its
924
+ * resolved context as JSON to stdout — no save, no prompts, no other output.
925
+ * Lets a GUI caller (e.g. the VS Code extension's popup) show fetched
926
+ * Title/Description/Project ID/Key/detected Task Type before committing.
927
+ */
928
+ async function previewTarget(target, options = {}) {
929
+ if (!target) {
930
+ process.stderr.write("ak use --preview requires a ticket ID or Backlog/Jira URL.\n");
931
+ process.exitCode = 1;
932
+ return;
933
+ }
934
+
935
+ let issueKey = target;
936
+ let adapter;
937
+ if (isTicketId(target)) {
938
+ adapter = await resolveTicketAdapter();
939
+ } else if (isBacklogUrl(target)) {
940
+ issueKey = extractBacklogId(target);
941
+ adapter = "backlog";
942
+ } else {
943
+ process.stderr.write(`ak use --preview only supports a ticket ID or Backlog URL, got: ${target}\n`);
944
+ process.exitCode = 1;
945
+ return;
946
+ }
947
+
948
+ try {
949
+ const context =
950
+ adapter === "jira"
951
+ ? await fetchJiraContext(issueKey, options)
952
+ : await fetchBacklogContext(issueKey, options);
953
+ process.stdout.write(JSON.stringify(context) + "\n");
954
+ } catch (err) {
955
+ process.stderr.write(`Error fetching preview: ${err.message}\n`);
956
+ process.exitCode = 1;
957
+ }
958
+ }
959
+
775
960
  /**
776
961
  * Build a minimal context object from arbitrary plain-text file content.
777
962
  * Tries to detect ticket ID (e.g. PROJ-33, APP-123) and a title from the first non-blank line.
@@ -857,6 +1042,63 @@ async function loadCredentials() {
857
1042
  return {};
858
1043
  }
859
1044
 
1045
+ /** Always the global file (`~/.aiflow/credentials.json`) — same path `loadCredentials` reads
1046
+ * first — so a value written here is picked up by every later `loadCredentials()` call. */
1047
+ function globalCredentialsPath() {
1048
+ return path.join(os.homedir(), ".aiflow", "credentials.json");
1049
+ }
1050
+
1051
+ /** Merges `patch` into the saved `.mcp` credentials block, creating the file if needed. */
1052
+ async function saveMcpCredentials(patch) {
1053
+ const file = globalCredentialsPath();
1054
+ await fs.ensureDir(path.dirname(file));
1055
+ const data = (await fs.pathExists(file)) ? await fs.readJson(file).catch(() => ({})) : {};
1056
+ data.mcp = { ...(data.mcp || {}), ...patch };
1057
+ await fs.writeJson(file, data, { spaces: 2 });
1058
+ }
1059
+
1060
+ /**
1061
+ * `ak backlog-projects [--json]` — lists the projects in the configured Backlog space, so the
1062
+ * user (or a headless caller like the VS Code extension) can pick one as the default project for
1063
+ * manually-created tasks (see `manualContext` below, which stamps that default onto new tasks).
1064
+ */
1065
+ async function listBacklogProjectsCommand(options = {}) {
1066
+ const creds = await loadCredentials();
1067
+ const resolved = resolveBacklogCredentials(creds);
1068
+ if (!resolved) {
1069
+ const msg = "Backlog credentials not set. Run `aiflow init --adapter backlog` first.";
1070
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: msg })); return; }
1071
+ console.log(chalk.yellow(`⚠ ${msg}`));
1072
+ return;
1073
+ }
1074
+ try {
1075
+ const projects = await fetchBacklogProjects(resolved.domain, resolved.apiKey);
1076
+ const list = projects.map((p) => ({ id: p.id, projectKey: p.projectKey, name: p.name }));
1077
+ if (options.json) {
1078
+ process.stdout.write(JSON.stringify({ ok: true, projects: list, defaultProjectId: creds.BACKLOG_DEFAULT_PROJECT_ID ?? null }));
1079
+ return;
1080
+ }
1081
+ console.log(chalk.cyan(`\nBacklog projects (${list.length}):`));
1082
+ for (const p of list) {
1083
+ const isDefault = String(p.id) === String(creds.BACKLOG_DEFAULT_PROJECT_ID);
1084
+ console.log(` ${isDefault ? chalk.green("●") : " "} ${p.projectKey} ${chalk.gray(p.name)} ${chalk.gray(`(id: ${p.id})`)}`);
1085
+ }
1086
+ } catch (err) {
1087
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: err.message })); return; }
1088
+ console.log(chalk.red(`✗ Could not fetch Backlog projects: ${err.message}`));
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * `ak backlog-set-default-project <id> <key>` — saves the chosen default project, so future
1094
+ * manually-created tasks (`ak use --manual`) get stamped with it automatically. Non-interactive —
1095
+ * safe to spawn without a TTY (see New-ex-architecture_v1.0.md §1.1 for why that distinction matters).
1096
+ */
1097
+ async function setDefaultBacklogProjectCommand(id, key) {
1098
+ await saveMcpCredentials({ BACKLOG_DEFAULT_PROJECT_ID: id, BACKLOG_DEFAULT_PROJECT_KEY: key });
1099
+ console.log(chalk.green(`✓ Default Backlog project set to ${key} (id: ${id}).`));
1100
+ }
1101
+
860
1102
  // ──────────────────────────────────────────────────────────────
861
1103
  // Context persistence
862
1104
  // ──────────────────────────────────────────────────────────────
@@ -939,6 +1181,8 @@ async function saveContext(context, saveName) {
939
1181
  const state = await fs.readJson(STATE_FILE);
940
1182
  state.current_context = context.taskId;
941
1183
  state.current_context_type = context.taskType;
1184
+ state.current_context_project_id = context.projectId ?? null;
1185
+ state.current_context_project_key = context.projectKey ?? null;
942
1186
  await fs.writeJson(STATE_FILE, state);
943
1187
  }
944
1188
  }
@@ -985,9 +1229,20 @@ function extractRelatedTickets(text) {
985
1229
  return [...new Set(matches)];
986
1230
  }
987
1231
 
1232
+ /** Both Backlog and Jira issue keys are PROJECTKEY-NUMBER — strip the trailing -NUMBER. */
1233
+ function projectKeyFromIssueKey(issueKey) {
1234
+ const match = String(issueKey || "").match(/^([A-Z][A-Z0-9_]+)-\d+$/);
1235
+ return match ? match[1] : null;
1236
+ }
1237
+
988
1238
  function printContextSummary(context) {
989
1239
  console.log(chalk.green("\n✓ Context loaded\n"));
990
1240
  console.log(` ${chalk.white("Ticket:")} ${context.taskId}`);
1241
+ if (context.projectKey || context.projectId != null) {
1242
+ console.log(
1243
+ ` ${chalk.white("Project:")} ${context.projectKey || "(unknown key)"}${context.projectId != null ? chalk.gray(` (id: ${context.projectId})`) : ""}`,
1244
+ );
1245
+ }
991
1246
  console.log(` ${chalk.white("Type:")} ${context.taskType}`);
992
1247
  console.log(
993
1248
  ` ${chalk.white("Title:")} ${context.title.substring(0, 70)}`,
@@ -1227,3 +1482,15 @@ async function enrichWithSupplementary(supplementaryTargets) {
1227
1482
  module.exports = useCommand;
1228
1483
  module.exports.loadCredentials = loadCredentials;
1229
1484
  module.exports.detectTaskTypeFromString = detectTaskTypeFromString;
1485
+ module.exports.manualContext = manualContext;
1486
+ module.exports.promptForTaskType = promptForTaskType;
1487
+ module.exports.previewTarget = previewTarget;
1488
+ module.exports.resolveBacklogCredentials = resolveBacklogCredentials;
1489
+ module.exports.fetchBacklogProjects = fetchBacklogProjects;
1490
+ module.exports.listBacklogProjectsCommand = listBacklogProjectsCommand;
1491
+ module.exports.setDefaultBacklogProjectCommand = setDefaultBacklogProjectCommand;
1492
+ module.exports.resolveJiraCredentials = resolveJiraCredentials;
1493
+ module.exports.fetchJiraProjects = fetchJiraProjects;
1494
+ module.exports.listJiraProjectsCommand = listJiraProjectsCommand;
1495
+ module.exports.setDefaultJiraProjectCommand = setDefaultJiraProjectCommand;
1496
+ module.exports.saveMcpCredentials = saveMcpCredentials;