@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/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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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;
|