@relipa/ai-flow-kit 0.1.9 → 0.2.0-beta.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.
- package/README.md +65 -0
- package/bin/aiflow.js +18 -4
- package/custom/rules/project-conventions.md +16 -0
- package/custom/skills/create-system-requirement/SKILL.md +248 -0
- package/custom/skills/ingest-data/SKILL.md +112 -0
- package/custom/skills/read-study-requirement/SKILL.md +35 -2
- package/custom/templates/shared/gate-workflow.md +49 -0
- package/docs/common/BA-Specs-Structure.md +3 -0
- package/docs/common/CHANGELOG.md +26 -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/detect.js +22 -0
- package/scripts/hooks/session-start.js +1 -0
- package/scripts/link-resolver.js +101 -7
- package/scripts/prompt.js +144 -0
- package/scripts/task.js +22 -6
- package/scripts/use.js +261 -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
|
+
}
|
|
219
|
+
|
|
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
|
+
}
|
|
203
269
|
|
|
204
|
-
|
|
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
|
|
|
@@ -395,6 +428,8 @@ function buildContextFromBacklog(issue, comments, issueKey, domain) {
|
|
|
395
428
|
|
|
396
429
|
return {
|
|
397
430
|
taskId: issueKey,
|
|
431
|
+
projectId: issue.projectId ?? null,
|
|
432
|
+
projectKey: projectKeyFromIssueKey(issueKey),
|
|
398
433
|
taskType: type,
|
|
399
434
|
title: issue.summary || "",
|
|
400
435
|
description: issue.description || "",
|
|
@@ -430,13 +465,67 @@ function buildContextFromBacklog(issue, comments, issueKey, domain) {
|
|
|
430
465
|
// Jira context loader
|
|
431
466
|
// ──────────────────────────────────────────────────────────────
|
|
432
467
|
|
|
433
|
-
|
|
434
|
-
|
|
468
|
+
/** Resolves Jira credentials from env/config, or null if unset. */
|
|
469
|
+
function resolveJiraCredentials(creds) {
|
|
435
470
|
const apiToken = process.env.JIRA_API_TOKEN || creds.JIRA_API_TOKEN;
|
|
436
471
|
const email = process.env.JIRA_EMAIL || creds.JIRA_EMAIL;
|
|
437
472
|
const domain = process.env.JIRA_DOMAIN || creds.JIRA_DOMAIN;
|
|
473
|
+
if (!apiToken || !email || !domain) return null;
|
|
474
|
+
return { apiToken, email, domain };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Pure fetch: resolves a Jira issue into internal context format.
|
|
479
|
+
* Throws on missing credentials or fetch failure — never prompts, never saves.
|
|
480
|
+
* Used by both `loadFromJira` (interactive) and `previewTarget` (headless).
|
|
481
|
+
*/
|
|
482
|
+
async function fetchJiraContext(issueKey, options = {}) {
|
|
483
|
+
const creds = await loadCredentials();
|
|
484
|
+
const resolved = resolveJiraCredentials(creds);
|
|
485
|
+
if (!resolved) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
"Jira credentials not set. Set JIRA_API_TOKEN, JIRA_EMAIL, JIRA_DOMAIN.",
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const { apiToken, email, domain } = resolved;
|
|
491
|
+
|
|
492
|
+
const loadComments =
|
|
493
|
+
options.coms ||
|
|
494
|
+
options.withComs ||
|
|
495
|
+
options.withComments ||
|
|
496
|
+
options["with-comments"] ||
|
|
497
|
+
options.with_comments ||
|
|
498
|
+
options.cid != null ||
|
|
499
|
+
options.commentId != null ||
|
|
500
|
+
options["comment-id"] != null ||
|
|
501
|
+
options.clast != null ||
|
|
502
|
+
options.commentsLast != null ||
|
|
503
|
+
options.cfrom != null ||
|
|
504
|
+
options.commentsFrom != null ||
|
|
505
|
+
options.cto != null ||
|
|
506
|
+
options.commentsTo != null ||
|
|
507
|
+
options["comments-to"] != null;
|
|
508
|
+
|
|
509
|
+
if (loadComments) {
|
|
510
|
+
console.log(chalk.gray(" ℹ Comments requested..."));
|
|
511
|
+
}
|
|
512
|
+
const [issue, rawComments] = await Promise.all([
|
|
513
|
+
fetchJiraIssue(domain, email, apiToken, issueKey),
|
|
514
|
+
loadComments
|
|
515
|
+
? fetchJiraComments(domain, email, apiToken, issueKey)
|
|
516
|
+
: Promise.resolve([]),
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
const comments = filterComments(rawComments, options);
|
|
520
|
+
if (loadComments) {
|
|
521
|
+
console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
|
|
522
|
+
}
|
|
523
|
+
return buildContextFromJira(issue, comments, issueKey, domain);
|
|
524
|
+
}
|
|
438
525
|
|
|
439
|
-
|
|
526
|
+
async function loadFromJira(issueKey, options = {}) {
|
|
527
|
+
const creds = await loadCredentials();
|
|
528
|
+
if (!resolveJiraCredentials(creds)) {
|
|
440
529
|
console.log(chalk.yellow("⚠ Jira credentials not set."));
|
|
441
530
|
console.log(chalk.gray("Run: aiflow init --adapter jira"));
|
|
442
531
|
console.log(
|
|
@@ -445,45 +534,14 @@ async function loadFromJira(issueKey, options = {}) {
|
|
|
445
534
|
),
|
|
446
535
|
);
|
|
447
536
|
console.log(chalk.gray("\nFalling back to manual entry...\n"));
|
|
448
|
-
return await manualContext(issueKey);
|
|
537
|
+
return await manualContext(issueKey, options);
|
|
449
538
|
}
|
|
450
539
|
|
|
451
540
|
console.log(chalk.blue(`Fetching context from Jira: ${issueKey}...`));
|
|
452
541
|
|
|
453
542
|
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);
|
|
543
|
+
const context = await fetchJiraContext(issueKey, options);
|
|
544
|
+
context.taskType = await promptForTaskType(context.taskType, options.taskType);
|
|
487
545
|
context.mode = options.full ? "full" : "fast";
|
|
488
546
|
await saveContext(context, options.save);
|
|
489
547
|
printContextSummary(context);
|
|
@@ -491,7 +549,7 @@ async function loadFromJira(issueKey, options = {}) {
|
|
|
491
549
|
} catch (err) {
|
|
492
550
|
console.log(chalk.yellow(`⚠ Could not fetch from Jira: ${err.message}`));
|
|
493
551
|
console.log(chalk.gray("Falling back to manual entry...\n"));
|
|
494
|
-
await manualContext(issueKey);
|
|
552
|
+
await manualContext(issueKey, options);
|
|
495
553
|
}
|
|
496
554
|
}
|
|
497
555
|
|
|
@@ -590,6 +648,8 @@ function buildContextFromJira(issue, comments, issueKey, domain) {
|
|
|
590
648
|
|
|
591
649
|
return {
|
|
592
650
|
taskId: issueKey,
|
|
651
|
+
projectId: fields.project?.id ?? null,
|
|
652
|
+
projectKey: fields.project?.key ?? projectKeyFromIssueKey(issueKey),
|
|
593
653
|
taskType: type,
|
|
594
654
|
title: fields.summary || "",
|
|
595
655
|
description:
|
|
@@ -623,7 +683,13 @@ function buildContextFromJira(issue, comments, issueKey, domain) {
|
|
|
623
683
|
// Task type selector — shared by all loaders
|
|
624
684
|
// ──────────────────────────────────────────────────────────────
|
|
625
685
|
|
|
626
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Returns `explicit` immediately (no prompt) when the caller already knows the
|
|
688
|
+
* task type — e.g. `--task-type` was passed by a headless caller. Otherwise
|
|
689
|
+
* falls back to the interactive `select()` prompt as before.
|
|
690
|
+
*/
|
|
691
|
+
async function promptForTaskType(detectedDefault, explicit) {
|
|
692
|
+
if (explicit != null && explicit !== "") return explicit;
|
|
627
693
|
return await select({
|
|
628
694
|
message: "Task type:",
|
|
629
695
|
choices: [
|
|
@@ -633,10 +699,12 @@ async function promptForTaskType(detectedDefault) {
|
|
|
633
699
|
{ name: td("▶️ Execute Test", "Testing", "Run existing TC scripts (4-gate flow)"), value: "execute" },
|
|
634
700
|
{ name: td("📋 Create Spec", "BA", "Create UC Spec from raw requirements (4-gate BA)"), value: "create-spec" },
|
|
635
701
|
{ name: td("✅ Create TestCase", "QA", "Create Test Cases from requirements (4-gate QA)"), value: "create-testcase" },
|
|
702
|
+
{ name: td("📐 Create System Requirement", "Dev", "Bridge UC Spec → System Requirement (blocks coding Gate 1, 2-gate)"), value: "create-system-requirement" },
|
|
636
703
|
{ name: td("🔍 Investigation", "Analysis", "Investigate, analyze root cause"), value: "investigation" },
|
|
637
704
|
{ name: td("📊 Impact Analysis", "Analysis", "Assess scope and risk of changes"), value: "impact-analysis" },
|
|
638
705
|
{ name: td("📖 Documentation", "Document", "Write docs, README, API reference"), value: "documentation" },
|
|
639
706
|
{ name: td("📝 Generate Doc/Spec", "Document", "Generate new documentation/Spec (2-gate)"), value: "gen-doc" },
|
|
707
|
+
{ name: td("📥 Ingest Data", "PM", "Ingest Backlog/Jira/SharePoint link or pasted text into AK-Docs logs (2-gate)"), value: "ingest-data" },
|
|
640
708
|
],
|
|
641
709
|
default: detectedDefault || "feature",
|
|
642
710
|
});
|
|
@@ -646,7 +714,7 @@ async function promptForTaskType(detectedDefault) {
|
|
|
646
714
|
// Manual context entry
|
|
647
715
|
// ──────────────────────────────────────────────────────────────
|
|
648
716
|
|
|
649
|
-
async function manualContext(prefillId = "") {
|
|
717
|
+
async function manualContext(prefillId = "", options = {}) {
|
|
650
718
|
// Load existing context for pre-fill (Edit mode)
|
|
651
719
|
let existing = {};
|
|
652
720
|
const currentPath = path.join(CONTEXT_DIR, "current.json");
|
|
@@ -655,65 +723,69 @@ async function manualContext(prefillId = "") {
|
|
|
655
723
|
}
|
|
656
724
|
|
|
657
725
|
const isEdit = !!existing.taskId;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
726
|
+
// Headless callers (extension popup, scripts) pass fields as flags — skip
|
|
727
|
+
// the corresponding inquirer prompt entirely when a field is supplied.
|
|
728
|
+
if (options.title == null && options.description == null && options.taskType == null) {
|
|
729
|
+
if (isEdit) {
|
|
730
|
+
console.log(chalk.cyan("\nEdit Context\n"));
|
|
731
|
+
console.log(
|
|
732
|
+
chalk.gray("Press Enter to keep existing value shown in [brackets]\n"),
|
|
733
|
+
);
|
|
734
|
+
} else {
|
|
735
|
+
console.log(chalk.cyan("\nManual Context Entry\n"));
|
|
736
|
+
}
|
|
665
737
|
}
|
|
666
738
|
|
|
667
739
|
// Ticket ID
|
|
668
|
-
const defaultId = prefillId || existing.taskId || "";
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
740
|
+
const defaultId = options.ticketId || prefillId || existing.taskId || "";
|
|
741
|
+
let taskId;
|
|
742
|
+
if (options.ticketId != null) {
|
|
743
|
+
taskId = options.ticketId;
|
|
744
|
+
} else {
|
|
745
|
+
const idHint = defaultId ? chalk.gray(` [${defaultId}]`) : "";
|
|
746
|
+
const idInput = await input({
|
|
747
|
+
message: `Ticket ID (e.g. PROJ-33)${idHint}:`,
|
|
748
|
+
default: "",
|
|
749
|
+
});
|
|
750
|
+
taskId = idInput.trim() || defaultId;
|
|
751
|
+
}
|
|
675
752
|
|
|
676
753
|
// Title
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
754
|
+
let title;
|
|
755
|
+
if (options.title != null) {
|
|
756
|
+
title = options.title;
|
|
757
|
+
} else {
|
|
758
|
+
const defaultTitle = existing.title || "";
|
|
759
|
+
const titlePreview =
|
|
760
|
+
defaultTitle.length > 50
|
|
761
|
+
? defaultTitle.substring(0, 50) + "…"
|
|
762
|
+
: defaultTitle;
|
|
763
|
+
const titleHint = defaultTitle ? chalk.gray(` [${titlePreview}]`) : "";
|
|
764
|
+
const titleInput = await input({
|
|
765
|
+
message: `Title${titleHint}:`,
|
|
766
|
+
default: "",
|
|
767
|
+
});
|
|
768
|
+
title = titleInput.trim() || defaultTitle;
|
|
769
|
+
}
|
|
688
770
|
|
|
689
771
|
// Description
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
772
|
+
let description;
|
|
773
|
+
if (options.description != null) {
|
|
774
|
+
description = options.description;
|
|
775
|
+
} else {
|
|
776
|
+
const defaultDesc = existing.description || "";
|
|
777
|
+
const descPreview =
|
|
778
|
+
defaultDesc.length > 60 ? defaultDesc.substring(0, 60) + "…" : defaultDesc;
|
|
779
|
+
const descHint = defaultDesc ? chalk.gray(` [${descPreview}]`) : "";
|
|
780
|
+
const descInput = await input({
|
|
781
|
+
message: `Description (brief)${descHint}:`,
|
|
782
|
+
default: "",
|
|
783
|
+
});
|
|
784
|
+
description = descInput.trim() || defaultDesc;
|
|
785
|
+
}
|
|
699
786
|
|
|
700
787
|
// Task type — pre-select existing value if available
|
|
701
|
-
const taskType = await
|
|
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
|
-
});
|
|
788
|
+
const taskType = await promptForTaskType(existing.taskType, options.taskType);
|
|
717
789
|
|
|
718
790
|
const context = {
|
|
719
791
|
taskId,
|
|
@@ -765,13 +837,51 @@ async function loadFromFile(filePath, options = {}) {
|
|
|
765
837
|
context.mode = options.full ? "full" : "fast";
|
|
766
838
|
|
|
767
839
|
// Prompt for task type
|
|
768
|
-
context.taskType = await promptForTaskType(context.taskType);
|
|
840
|
+
context.taskType = await promptForTaskType(context.taskType, options.taskType);
|
|
769
841
|
|
|
770
842
|
await saveContext(context, options.save);
|
|
771
843
|
printContextSummary(context);
|
|
772
844
|
await suggestNextStep();
|
|
773
845
|
}
|
|
774
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Preview mode (`ak use <target> --preview`): fetches a ticket and prints its
|
|
849
|
+
* resolved context as JSON to stdout — no save, no prompts, no other output.
|
|
850
|
+
* Lets a GUI caller (e.g. the VS Code extension's popup) show fetched
|
|
851
|
+
* Title/Description/Project ID/Key/detected Task Type before committing.
|
|
852
|
+
*/
|
|
853
|
+
async function previewTarget(target, options = {}) {
|
|
854
|
+
if (!target) {
|
|
855
|
+
process.stderr.write("ak use --preview requires a ticket ID or Backlog/Jira URL.\n");
|
|
856
|
+
process.exitCode = 1;
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
let issueKey = target;
|
|
861
|
+
let adapter;
|
|
862
|
+
if (isTicketId(target)) {
|
|
863
|
+
adapter = await resolveTicketAdapter();
|
|
864
|
+
} else if (isBacklogUrl(target)) {
|
|
865
|
+
issueKey = extractBacklogId(target);
|
|
866
|
+
adapter = "backlog";
|
|
867
|
+
} else {
|
|
868
|
+
process.stderr.write(`ak use --preview only supports a ticket ID or Backlog URL, got: ${target}\n`);
|
|
869
|
+
process.exitCode = 1;
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
try {
|
|
874
|
+
const context =
|
|
875
|
+
adapter === "jira"
|
|
876
|
+
? await fetchJiraContext(issueKey, options)
|
|
877
|
+
: await fetchBacklogContext(issueKey, options);
|
|
878
|
+
process.stdout.write(JSON.stringify(context) + "\n");
|
|
879
|
+
} catch (err) {
|
|
880
|
+
process.stderr.write(`Error fetching preview: ${err.message}\n`);
|
|
881
|
+
process.exitCode = 1;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
775
885
|
/**
|
|
776
886
|
* Build a minimal context object from arbitrary plain-text file content.
|
|
777
887
|
* Tries to detect ticket ID (e.g. PROJ-33, APP-123) and a title from the first non-blank line.
|
|
@@ -939,6 +1049,8 @@ async function saveContext(context, saveName) {
|
|
|
939
1049
|
const state = await fs.readJson(STATE_FILE);
|
|
940
1050
|
state.current_context = context.taskId;
|
|
941
1051
|
state.current_context_type = context.taskType;
|
|
1052
|
+
state.current_context_project_id = context.projectId ?? null;
|
|
1053
|
+
state.current_context_project_key = context.projectKey ?? null;
|
|
942
1054
|
await fs.writeJson(STATE_FILE, state);
|
|
943
1055
|
}
|
|
944
1056
|
}
|
|
@@ -985,9 +1097,20 @@ function extractRelatedTickets(text) {
|
|
|
985
1097
|
return [...new Set(matches)];
|
|
986
1098
|
}
|
|
987
1099
|
|
|
1100
|
+
/** Both Backlog and Jira issue keys are PROJECTKEY-NUMBER — strip the trailing -NUMBER. */
|
|
1101
|
+
function projectKeyFromIssueKey(issueKey) {
|
|
1102
|
+
const match = String(issueKey || "").match(/^([A-Z][A-Z0-9_]+)-\d+$/);
|
|
1103
|
+
return match ? match[1] : null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
988
1106
|
function printContextSummary(context) {
|
|
989
1107
|
console.log(chalk.green("\n✓ Context loaded\n"));
|
|
990
1108
|
console.log(` ${chalk.white("Ticket:")} ${context.taskId}`);
|
|
1109
|
+
if (context.projectKey || context.projectId != null) {
|
|
1110
|
+
console.log(
|
|
1111
|
+
` ${chalk.white("Project:")} ${context.projectKey || "(unknown key)"}${context.projectId != null ? chalk.gray(` (id: ${context.projectId})`) : ""}`,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
991
1114
|
console.log(` ${chalk.white("Type:")} ${context.taskType}`);
|
|
992
1115
|
console.log(
|
|
993
1116
|
` ${chalk.white("Title:")} ${context.title.substring(0, 70)}`,
|
|
@@ -1227,3 +1350,6 @@ async function enrichWithSupplementary(supplementaryTargets) {
|
|
|
1227
1350
|
module.exports = useCommand;
|
|
1228
1351
|
module.exports.loadCredentials = loadCredentials;
|
|
1229
1352
|
module.exports.detectTaskTypeFromString = detectTaskTypeFromString;
|
|
1353
|
+
module.exports.manualContext = manualContext;
|
|
1354
|
+
module.exports.promptForTaskType = promptForTaskType;
|
|
1355
|
+
module.exports.previewTarget = previewTarget;
|