mrvn-cli 0.5.16 → 0.5.18
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/dist/index.d.ts +29 -8
- package/dist/index.js +1005 -175
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1001 -180
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1005 -175
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -14497,11 +14497,25 @@ function evaluateHealth(projectName, metrics) {
|
|
|
14497
14497
|
|
|
14498
14498
|
// src/storage/progress.ts
|
|
14499
14499
|
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
14500
|
+
var STATUS_PROGRESS_DEFAULTS = {
|
|
14501
|
+
done: 100,
|
|
14502
|
+
closed: 100,
|
|
14503
|
+
resolved: 100,
|
|
14504
|
+
obsolete: 100,
|
|
14505
|
+
"wont do": 100,
|
|
14506
|
+
cancelled: 100,
|
|
14507
|
+
review: 80,
|
|
14508
|
+
"in-progress": 40,
|
|
14509
|
+
ready: 5,
|
|
14510
|
+
blocked: 10,
|
|
14511
|
+
backlog: 0,
|
|
14512
|
+
open: 0
|
|
14513
|
+
};
|
|
14500
14514
|
function getEffectiveProgress(frontmatter) {
|
|
14501
14515
|
if (DONE_STATUSES.has(frontmatter.status)) return 100;
|
|
14502
14516
|
const raw = frontmatter.progress;
|
|
14503
14517
|
if (typeof raw === "number") return Math.max(0, Math.min(100, Math.round(raw)));
|
|
14504
|
-
return 0;
|
|
14518
|
+
return STATUS_PROGRESS_DEFAULTS[frontmatter.status] ?? 0;
|
|
14505
14519
|
}
|
|
14506
14520
|
function propagateProgressFromTask(store, taskId) {
|
|
14507
14521
|
const updated = [];
|
|
@@ -14580,6 +14594,14 @@ function calculateSprintCompletionPct(primaryDocs) {
|
|
|
14580
14594
|
|
|
14581
14595
|
// src/reports/sprint-summary/collector.ts
|
|
14582
14596
|
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
14597
|
+
var COMPLEXITY_WEIGHTS = {
|
|
14598
|
+
trivial: 1,
|
|
14599
|
+
simple: 2,
|
|
14600
|
+
moderate: 3,
|
|
14601
|
+
complex: 5,
|
|
14602
|
+
"very-complex": 8
|
|
14603
|
+
};
|
|
14604
|
+
var DEFAULT_WEIGHT = 3;
|
|
14583
14605
|
function collectSprintSummaryData(store, sprintId) {
|
|
14584
14606
|
const allDocs = store.list();
|
|
14585
14607
|
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -14665,12 +14687,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
14665
14687
|
for (const doc of workItemDocs) {
|
|
14666
14688
|
const about = doc.frontmatter.aboutArtifact;
|
|
14667
14689
|
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
14690
|
+
const complexity = doc.frontmatter.complexity;
|
|
14668
14691
|
const item = {
|
|
14669
14692
|
id: doc.frontmatter.id,
|
|
14670
14693
|
title: doc.frontmatter.title,
|
|
14671
14694
|
type: doc.frontmatter.type,
|
|
14672
14695
|
status: doc.frontmatter.status,
|
|
14673
14696
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
14697
|
+
weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
|
|
14674
14698
|
owner: doc.frontmatter.owner,
|
|
14675
14699
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
14676
14700
|
aboutArtifact: about,
|
|
@@ -22384,7 +22408,7 @@ function poBacklogPage(ctx) {
|
|
|
22384
22408
|
}
|
|
22385
22409
|
}
|
|
22386
22410
|
}
|
|
22387
|
-
const
|
|
22411
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
22388
22412
|
function featureTaskStats(featureId) {
|
|
22389
22413
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
22390
22414
|
let total = 0;
|
|
@@ -22393,7 +22417,7 @@ function poBacklogPage(ctx) {
|
|
|
22393
22417
|
for (const epic of fEpics) {
|
|
22394
22418
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
22395
22419
|
total++;
|
|
22396
|
-
if (
|
|
22420
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
22397
22421
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
22398
22422
|
}
|
|
22399
22423
|
}
|
|
@@ -22639,23 +22663,34 @@ function hashString(s) {
|
|
|
22639
22663
|
}
|
|
22640
22664
|
return Math.abs(h);
|
|
22641
22665
|
}
|
|
22666
|
+
var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
|
|
22667
|
+
var DEFAULT_WEIGHT2 = 3;
|
|
22642
22668
|
function countFocusStats(items) {
|
|
22643
22669
|
let total = 0;
|
|
22644
22670
|
let done = 0;
|
|
22645
22671
|
let inProgress = 0;
|
|
22646
|
-
|
|
22672
|
+
let totalWeight = 0;
|
|
22673
|
+
let weightedSum = 0;
|
|
22674
|
+
function walkStats(list) {
|
|
22647
22675
|
for (const w of list) {
|
|
22648
22676
|
if (w.type !== "contribution") {
|
|
22649
22677
|
total++;
|
|
22650
22678
|
const s = w.status.toLowerCase();
|
|
22651
|
-
if (s
|
|
22679
|
+
if (DONE_STATUS_SET.has(s)) done++;
|
|
22652
22680
|
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
22653
22681
|
}
|
|
22654
|
-
if (w.children)
|
|
22682
|
+
if (w.children) walkStats(w.children);
|
|
22655
22683
|
}
|
|
22656
22684
|
}
|
|
22657
|
-
|
|
22658
|
-
|
|
22685
|
+
walkStats(items);
|
|
22686
|
+
for (const w of items) {
|
|
22687
|
+
if (w.type === "contribution") continue;
|
|
22688
|
+
const weight = w.weight ?? DEFAULT_WEIGHT2;
|
|
22689
|
+
const progress = w.progress ?? (DONE_STATUS_SET.has(w.status.toLowerCase()) ? 100 : 0);
|
|
22690
|
+
totalWeight += weight;
|
|
22691
|
+
weightedSum += weight * progress;
|
|
22692
|
+
}
|
|
22693
|
+
return { total, done, inProgress, weightedProgress: totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 };
|
|
22659
22694
|
}
|
|
22660
22695
|
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
22661
22696
|
function ownerBadge2(owner) {
|
|
@@ -22704,7 +22739,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
22704
22739
|
for (const [focus, groupItems] of focusGroups) {
|
|
22705
22740
|
const color = focusColorMap.get(focus);
|
|
22706
22741
|
const stats = countFocusStats(groupItems);
|
|
22707
|
-
const pct = stats.
|
|
22742
|
+
const pct = stats.weightedProgress;
|
|
22708
22743
|
const summaryParts = [];
|
|
22709
22744
|
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
22710
22745
|
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
@@ -25338,35 +25373,62 @@ var DEFAULT_TASK_STATUS_MAP = {
|
|
|
25338
25373
|
blocked: ["Blocked"],
|
|
25339
25374
|
backlog: ["To Do", "Open", "Backlog", "New"]
|
|
25340
25375
|
};
|
|
25341
|
-
function
|
|
25342
|
-
|
|
25376
|
+
function isLegacyFormat(statusMap) {
|
|
25377
|
+
if (!statusMap || typeof statusMap !== "object") return false;
|
|
25378
|
+
const keys = Object.keys(statusMap);
|
|
25379
|
+
if (!keys.every((k) => k === "action" || k === "task")) return false;
|
|
25380
|
+
for (const key of keys) {
|
|
25381
|
+
const val = statusMap[key];
|
|
25382
|
+
if (typeof val !== "object" || val === null) return false;
|
|
25383
|
+
for (const innerVal of Object.values(val)) {
|
|
25384
|
+
if (!Array.isArray(innerVal)) return false;
|
|
25385
|
+
if (!innerVal.every((v) => typeof v === "string")) return false;
|
|
25386
|
+
}
|
|
25387
|
+
}
|
|
25388
|
+
return true;
|
|
25343
25389
|
}
|
|
25344
|
-
function
|
|
25345
|
-
const map2 = configMap ?? defaults;
|
|
25390
|
+
function buildLegacyLookup(legacyMap) {
|
|
25346
25391
|
const lookup = /* @__PURE__ */ new Map();
|
|
25347
|
-
for (const [marvinStatus,
|
|
25348
|
-
const
|
|
25349
|
-
for (const js of statuses) {
|
|
25392
|
+
for (const [marvinStatus, jiraStatuses] of Object.entries(legacyMap)) {
|
|
25393
|
+
for (const js of jiraStatuses) {
|
|
25350
25394
|
lookup.set(js.toLowerCase(), marvinStatus);
|
|
25351
25395
|
}
|
|
25352
25396
|
}
|
|
25353
|
-
|
|
25354
|
-
|
|
25355
|
-
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
|
|
25397
|
+
return lookup;
|
|
25398
|
+
}
|
|
25399
|
+
function buildFlatLookup(flatMap, inSprint) {
|
|
25400
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
25401
|
+
for (const [jiraStatus, value] of Object.entries(flatMap)) {
|
|
25402
|
+
if (typeof value === "string") {
|
|
25403
|
+
lookup.set(jiraStatus.toLowerCase(), value);
|
|
25404
|
+
} else {
|
|
25405
|
+
const resolved = inSprint && value.inSprint ? value.inSprint : value.default;
|
|
25406
|
+
lookup.set(jiraStatus.toLowerCase(), resolved);
|
|
25360
25407
|
}
|
|
25361
25408
|
}
|
|
25362
25409
|
return lookup;
|
|
25363
25410
|
}
|
|
25364
|
-
function
|
|
25365
|
-
|
|
25411
|
+
function normalizeStatusMap(statusMap) {
|
|
25412
|
+
if (!statusMap) return {};
|
|
25413
|
+
if (isLegacyFormat(statusMap)) {
|
|
25414
|
+
return { legacy: statusMap };
|
|
25415
|
+
}
|
|
25416
|
+
return { flat: statusMap };
|
|
25417
|
+
}
|
|
25418
|
+
function mapJiraStatusForAction(status, resolved, inSprint = false) {
|
|
25419
|
+
if (resolved.flat) {
|
|
25420
|
+
const lookup2 = buildFlatLookup(resolved.flat, inSprint);
|
|
25421
|
+
return lookup2.get(status.toLowerCase()) ?? "open";
|
|
25422
|
+
}
|
|
25423
|
+
const lookup = buildLegacyLookup(resolved.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP);
|
|
25366
25424
|
return lookup.get(status.toLowerCase()) ?? "open";
|
|
25367
25425
|
}
|
|
25368
|
-
function mapJiraStatusForTask(status,
|
|
25369
|
-
|
|
25426
|
+
function mapJiraStatusForTask(status, resolved, inSprint = false) {
|
|
25427
|
+
if (resolved.flat) {
|
|
25428
|
+
const lookup2 = buildFlatLookup(resolved.flat, inSprint);
|
|
25429
|
+
return lookup2.get(status.toLowerCase()) ?? "backlog";
|
|
25430
|
+
}
|
|
25431
|
+
const lookup = buildLegacyLookup(resolved.legacy?.task ?? DEFAULT_TASK_STATUS_MAP);
|
|
25370
25432
|
return lookup.get(status.toLowerCase()) ?? "backlog";
|
|
25371
25433
|
}
|
|
25372
25434
|
function isInActiveSprint(store, tags) {
|
|
@@ -25390,6 +25452,47 @@ function extractJiraKeyFromTags(tags) {
|
|
|
25390
25452
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
25391
25453
|
return tag ? tag.slice(5) : void 0;
|
|
25392
25454
|
}
|
|
25455
|
+
function collectLinkedIssues(issue2) {
|
|
25456
|
+
const linkedIssues = [];
|
|
25457
|
+
if (issue2.fields.subtasks) {
|
|
25458
|
+
for (const sub of issue2.fields.subtasks) {
|
|
25459
|
+
linkedIssues.push({
|
|
25460
|
+
key: sub.key,
|
|
25461
|
+
summary: sub.fields.summary,
|
|
25462
|
+
status: sub.fields.status.name,
|
|
25463
|
+
relationship: "subtask",
|
|
25464
|
+
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25465
|
+
});
|
|
25466
|
+
}
|
|
25467
|
+
}
|
|
25468
|
+
if (issue2.fields.issuelinks) {
|
|
25469
|
+
for (const link of issue2.fields.issuelinks) {
|
|
25470
|
+
if (link.outwardIssue) {
|
|
25471
|
+
linkedIssues.push({
|
|
25472
|
+
key: link.outwardIssue.key,
|
|
25473
|
+
summary: link.outwardIssue.fields.summary,
|
|
25474
|
+
status: link.outwardIssue.fields.status.name,
|
|
25475
|
+
relationship: link.type.outward,
|
|
25476
|
+
isDone: DONE_STATUSES14.has(
|
|
25477
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25478
|
+
)
|
|
25479
|
+
});
|
|
25480
|
+
}
|
|
25481
|
+
if (link.inwardIssue) {
|
|
25482
|
+
linkedIssues.push({
|
|
25483
|
+
key: link.inwardIssue.key,
|
|
25484
|
+
summary: link.inwardIssue.fields.summary,
|
|
25485
|
+
status: link.inwardIssue.fields.status.name,
|
|
25486
|
+
relationship: link.type.inward,
|
|
25487
|
+
isDone: DONE_STATUSES14.has(
|
|
25488
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25489
|
+
)
|
|
25490
|
+
});
|
|
25491
|
+
}
|
|
25492
|
+
}
|
|
25493
|
+
}
|
|
25494
|
+
return linkedIssues;
|
|
25495
|
+
}
|
|
25393
25496
|
function computeSubtaskProgress(subtasks) {
|
|
25394
25497
|
if (subtasks.length === 0) return 0;
|
|
25395
25498
|
const done = subtasks.filter(
|
|
@@ -25427,46 +25530,10 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
25427
25530
|
try {
|
|
25428
25531
|
const issue2 = await client.getIssueWithLinks(jiraKey);
|
|
25429
25532
|
const inSprint = isInActiveSprint(store, doc.frontmatter.tags);
|
|
25430
|
-
const
|
|
25533
|
+
const resolved = statusMap ?? {};
|
|
25534
|
+
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
|
|
25431
25535
|
const currentStatus = doc.frontmatter.status;
|
|
25432
|
-
const linkedIssues =
|
|
25433
|
-
if (issue2.fields.subtasks) {
|
|
25434
|
-
for (const sub of issue2.fields.subtasks) {
|
|
25435
|
-
linkedIssues.push({
|
|
25436
|
-
key: sub.key,
|
|
25437
|
-
summary: sub.fields.summary,
|
|
25438
|
-
status: sub.fields.status.name,
|
|
25439
|
-
relationship: "subtask",
|
|
25440
|
-
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25441
|
-
});
|
|
25442
|
-
}
|
|
25443
|
-
}
|
|
25444
|
-
if (issue2.fields.issuelinks) {
|
|
25445
|
-
for (const link of issue2.fields.issuelinks) {
|
|
25446
|
-
if (link.outwardIssue) {
|
|
25447
|
-
linkedIssues.push({
|
|
25448
|
-
key: link.outwardIssue.key,
|
|
25449
|
-
summary: link.outwardIssue.fields.summary,
|
|
25450
|
-
status: link.outwardIssue.fields.status.name,
|
|
25451
|
-
relationship: link.type.outward,
|
|
25452
|
-
isDone: DONE_STATUSES14.has(
|
|
25453
|
-
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25454
|
-
)
|
|
25455
|
-
});
|
|
25456
|
-
}
|
|
25457
|
-
if (link.inwardIssue) {
|
|
25458
|
-
linkedIssues.push({
|
|
25459
|
-
key: link.inwardIssue.key,
|
|
25460
|
-
summary: link.inwardIssue.fields.summary,
|
|
25461
|
-
status: link.inwardIssue.fields.status.name,
|
|
25462
|
-
relationship: link.type.inward,
|
|
25463
|
-
isDone: DONE_STATUSES14.has(
|
|
25464
|
-
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25465
|
-
)
|
|
25466
|
-
});
|
|
25467
|
-
}
|
|
25468
|
-
}
|
|
25469
|
-
}
|
|
25536
|
+
const linkedIssues = collectLinkedIssues(issue2);
|
|
25470
25537
|
const subtasks = issue2.fields.subtasks ?? [];
|
|
25471
25538
|
let proposedProgress;
|
|
25472
25539
|
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
@@ -25728,7 +25795,6 @@ function isWithinRange(timestamp, range) {
|
|
|
25728
25795
|
function isConfluenceUrl(url2) {
|
|
25729
25796
|
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
25730
25797
|
}
|
|
25731
|
-
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
25732
25798
|
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
25733
25799
|
const summary = {
|
|
25734
25800
|
dateRange,
|
|
@@ -25839,42 +25905,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
25839
25905
|
});
|
|
25840
25906
|
}
|
|
25841
25907
|
}
|
|
25842
|
-
const linkedIssues = [];
|
|
25843
|
-
if (issueWithLinks) {
|
|
25844
|
-
if (issueWithLinks.fields.subtasks) {
|
|
25845
|
-
for (const sub of issueWithLinks.fields.subtasks) {
|
|
25846
|
-
linkedIssues.push({
|
|
25847
|
-
key: sub.key,
|
|
25848
|
-
summary: sub.fields.summary,
|
|
25849
|
-
status: sub.fields.status.name,
|
|
25850
|
-
relationship: "subtask",
|
|
25851
|
-
isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
|
|
25852
|
-
});
|
|
25853
|
-
}
|
|
25854
|
-
}
|
|
25855
|
-
if (issueWithLinks.fields.issuelinks) {
|
|
25856
|
-
for (const link of issueWithLinks.fields.issuelinks) {
|
|
25857
|
-
if (link.outwardIssue) {
|
|
25858
|
-
linkedIssues.push({
|
|
25859
|
-
key: link.outwardIssue.key,
|
|
25860
|
-
summary: link.outwardIssue.fields.summary,
|
|
25861
|
-
status: link.outwardIssue.fields.status.name,
|
|
25862
|
-
relationship: link.type.outward,
|
|
25863
|
-
isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
25864
|
-
});
|
|
25865
|
-
}
|
|
25866
|
-
if (link.inwardIssue) {
|
|
25867
|
-
linkedIssues.push({
|
|
25868
|
-
key: link.inwardIssue.key,
|
|
25869
|
-
summary: link.inwardIssue.fields.summary,
|
|
25870
|
-
status: link.inwardIssue.fields.status.name,
|
|
25871
|
-
relationship: link.type.inward,
|
|
25872
|
-
isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
25873
|
-
});
|
|
25874
|
-
}
|
|
25875
|
-
}
|
|
25876
|
-
}
|
|
25877
|
-
}
|
|
25908
|
+
const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
|
|
25878
25909
|
const marvinArtifacts = [];
|
|
25879
25910
|
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
25880
25911
|
for (const doc of artifacts) {
|
|
@@ -25885,7 +25916,8 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
25885
25916
|
const jiraStatus = issue2.fields.status?.name;
|
|
25886
25917
|
if (jiraStatus) {
|
|
25887
25918
|
const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
|
|
25888
|
-
|
|
25919
|
+
const resolved = statusMap ?? {};
|
|
25920
|
+
proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
25889
25921
|
}
|
|
25890
25922
|
}
|
|
25891
25923
|
marvinArtifacts.push({
|
|
@@ -26003,36 +26035,23 @@ function generateProposedActions(issues) {
|
|
|
26003
26035
|
|
|
26004
26036
|
// src/skills/builtin/jira/sprint-progress.ts
|
|
26005
26037
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
26006
|
-
var
|
|
26038
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
26007
26039
|
var BATCH_SIZE = 5;
|
|
26040
|
+
var MAX_LINKED_ISSUES = 50;
|
|
26008
26041
|
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
26009
|
-
var
|
|
26042
|
+
var COMPLEXITY_WEIGHTS2 = {
|
|
26010
26043
|
trivial: 1,
|
|
26011
26044
|
simple: 2,
|
|
26012
26045
|
moderate: 3,
|
|
26013
26046
|
complex: 5,
|
|
26014
26047
|
"very-complex": 8
|
|
26015
26048
|
};
|
|
26016
|
-
var
|
|
26017
|
-
var STATUS_PROGRESS_DEFAULTS = {
|
|
26018
|
-
done: 100,
|
|
26019
|
-
closed: 100,
|
|
26020
|
-
resolved: 100,
|
|
26021
|
-
obsolete: 100,
|
|
26022
|
-
"wont do": 100,
|
|
26023
|
-
cancelled: 100,
|
|
26024
|
-
review: 80,
|
|
26025
|
-
"in-progress": 40,
|
|
26026
|
-
ready: 5,
|
|
26027
|
-
backlog: 0,
|
|
26028
|
-
open: 0
|
|
26029
|
-
};
|
|
26030
|
-
var BLOCKED_DEFAULT_PROGRESS = 10;
|
|
26049
|
+
var DEFAULT_WEIGHT3 = 3;
|
|
26031
26050
|
function resolveWeight(complexity) {
|
|
26032
|
-
if (complexity && complexity in
|
|
26033
|
-
return { weight:
|
|
26051
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
|
|
26052
|
+
return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
|
|
26034
26053
|
}
|
|
26035
|
-
return { weight:
|
|
26054
|
+
return { weight: DEFAULT_WEIGHT3, weightSource: "default" };
|
|
26036
26055
|
}
|
|
26037
26056
|
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
26038
26057
|
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
@@ -26043,9 +26062,6 @@ function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
|
26043
26062
|
return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
|
|
26044
26063
|
}
|
|
26045
26064
|
const status = frontmatter.status;
|
|
26046
|
-
if (status === "blocked") {
|
|
26047
|
-
return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
|
|
26048
|
-
}
|
|
26049
26065
|
const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
|
|
26050
26066
|
return { progress: defaultProgress, progressSource: "status-default" };
|
|
26051
26067
|
}
|
|
@@ -26124,6 +26140,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26124
26140
|
}
|
|
26125
26141
|
}
|
|
26126
26142
|
}
|
|
26143
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
26144
|
+
if (options.traverseLinks) {
|
|
26145
|
+
const visited = new Set(jiraIssues.keys());
|
|
26146
|
+
const queue = [];
|
|
26147
|
+
for (const [, data] of jiraIssues) {
|
|
26148
|
+
const links = collectLinkedIssues(data.issue);
|
|
26149
|
+
for (const link of links) {
|
|
26150
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
26151
|
+
visited.add(link.key);
|
|
26152
|
+
queue.push(link.key);
|
|
26153
|
+
}
|
|
26154
|
+
}
|
|
26155
|
+
}
|
|
26156
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
26157
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
26158
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
26159
|
+
const results = await Promise.allSettled(
|
|
26160
|
+
batch.map(async (key) => {
|
|
26161
|
+
const [issue2, comments] = await Promise.all([
|
|
26162
|
+
client.getIssueWithLinks(key),
|
|
26163
|
+
client.getComments(key)
|
|
26164
|
+
]);
|
|
26165
|
+
return { key, issue: issue2, comments };
|
|
26166
|
+
})
|
|
26167
|
+
);
|
|
26168
|
+
for (const result of results) {
|
|
26169
|
+
if (result.status === "fulfilled") {
|
|
26170
|
+
const { key, issue: issue2, comments } = result.value;
|
|
26171
|
+
linkedJiraIssues.set(key, { issue: issue2, comments });
|
|
26172
|
+
const newLinks = collectLinkedIssues(issue2);
|
|
26173
|
+
for (const link of newLinks) {
|
|
26174
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
26175
|
+
visited.add(link.key);
|
|
26176
|
+
queue.push(link.key);
|
|
26177
|
+
}
|
|
26178
|
+
}
|
|
26179
|
+
} else {
|
|
26180
|
+
const batchKey = batch[results.indexOf(result)];
|
|
26181
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
26182
|
+
}
|
|
26183
|
+
}
|
|
26184
|
+
}
|
|
26185
|
+
if (queue.length > 0) {
|
|
26186
|
+
errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
|
|
26187
|
+
}
|
|
26188
|
+
}
|
|
26127
26189
|
const proposedUpdates = [];
|
|
26128
26190
|
const itemReports = [];
|
|
26129
26191
|
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
@@ -26138,7 +26200,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26138
26200
|
if (jiraData) {
|
|
26139
26201
|
jiraStatus = jiraData.issue.fields.status.name;
|
|
26140
26202
|
const inSprint = isInActiveSprint(store, fm.tags);
|
|
26141
|
-
|
|
26203
|
+
const resolved = options.statusMap ?? {};
|
|
26204
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
26142
26205
|
const subtasks = jiraData.issue.fields.subtasks ?? [];
|
|
26143
26206
|
if (subtasks.length > 0) {
|
|
26144
26207
|
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
@@ -26169,11 +26232,42 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26169
26232
|
proposedValue: jiraSubtaskProgress,
|
|
26170
26233
|
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
26171
26234
|
});
|
|
26235
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
26236
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
26237
|
+
if (!hasExplicitProgress) {
|
|
26238
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
26239
|
+
if (proposedProgress !== currentProgress) {
|
|
26240
|
+
proposedUpdates.push({
|
|
26241
|
+
artifactId: fm.id,
|
|
26242
|
+
field: "progress",
|
|
26243
|
+
currentValue: currentProgress,
|
|
26244
|
+
proposedValue: proposedProgress,
|
|
26245
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
26246
|
+
});
|
|
26247
|
+
}
|
|
26248
|
+
}
|
|
26172
26249
|
}
|
|
26173
26250
|
const tags = fm.tags ?? [];
|
|
26174
26251
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
26175
26252
|
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
26176
26253
|
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
26254
|
+
let itemLinkedIssues = [];
|
|
26255
|
+
const itemLinkedIssueSignals = [];
|
|
26256
|
+
if (options.traverseLinks && jiraData) {
|
|
26257
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
26258
|
+
jiraData.issue,
|
|
26259
|
+
jiraIssues,
|
|
26260
|
+
linkedJiraIssues
|
|
26261
|
+
);
|
|
26262
|
+
itemLinkedIssues = allLinks;
|
|
26263
|
+
itemLinkedIssueSignals.push(...allSignals);
|
|
26264
|
+
analyzeLinkedIssueSignals(
|
|
26265
|
+
allLinks,
|
|
26266
|
+
fm,
|
|
26267
|
+
jiraKey,
|
|
26268
|
+
proposedUpdates
|
|
26269
|
+
);
|
|
26270
|
+
}
|
|
26177
26271
|
const report = {
|
|
26178
26272
|
id: fm.id,
|
|
26179
26273
|
title: fm.title,
|
|
@@ -26192,6 +26286,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26192
26286
|
progressDrift,
|
|
26193
26287
|
commentSignals,
|
|
26194
26288
|
commentSummary: null,
|
|
26289
|
+
linkedIssues: itemLinkedIssues,
|
|
26290
|
+
linkedIssueSignals: itemLinkedIssueSignals,
|
|
26195
26291
|
children: [],
|
|
26196
26292
|
owner: fm.owner ?? null,
|
|
26197
26293
|
focusArea: focusTag ? focusTag.slice(6) : null
|
|
@@ -26235,7 +26331,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26235
26331
|
const focusAreas = [];
|
|
26236
26332
|
for (const [name, items] of focusAreaMap) {
|
|
26237
26333
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
26238
|
-
const doneCount = allFlatItems.filter((i) =>
|
|
26334
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES15.has(i.marvinStatus)).length;
|
|
26239
26335
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
26240
26336
|
const progress = computeWeightedProgress(items);
|
|
26241
26337
|
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
@@ -26287,6 +26383,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26287
26383
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26288
26384
|
}
|
|
26289
26385
|
}
|
|
26386
|
+
if (options.traverseLinks) {
|
|
26387
|
+
try {
|
|
26388
|
+
const linkedSummaries = await analyzeLinkedIssueComments(
|
|
26389
|
+
itemReports,
|
|
26390
|
+
linkedJiraIssues
|
|
26391
|
+
);
|
|
26392
|
+
for (const [artifactId, signalSummaries] of linkedSummaries) {
|
|
26393
|
+
const report = itemReports.find((r) => r.id === artifactId);
|
|
26394
|
+
if (!report) continue;
|
|
26395
|
+
for (const [sourceKey, summary] of signalSummaries) {
|
|
26396
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === sourceKey);
|
|
26397
|
+
if (signal) {
|
|
26398
|
+
signal.commentSummary = summary;
|
|
26399
|
+
}
|
|
26400
|
+
}
|
|
26401
|
+
}
|
|
26402
|
+
} catch (err) {
|
|
26403
|
+
errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26404
|
+
}
|
|
26405
|
+
}
|
|
26290
26406
|
}
|
|
26291
26407
|
const appliedUpdates = [];
|
|
26292
26408
|
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
@@ -26403,6 +26519,155 @@ ${commentTexts}`);
|
|
|
26403
26519
|
}
|
|
26404
26520
|
return summaries;
|
|
26405
26521
|
}
|
|
26522
|
+
function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
|
|
26523
|
+
const allLinks = [];
|
|
26524
|
+
const allSignals = [];
|
|
26525
|
+
const visited = /* @__PURE__ */ new Set([primaryIssue.key]);
|
|
26526
|
+
const directLinks = collectLinkedIssues(primaryIssue).filter((l) => l.relationship !== "subtask");
|
|
26527
|
+
const queue = [...directLinks];
|
|
26528
|
+
for (const link of directLinks) {
|
|
26529
|
+
visited.add(link.key);
|
|
26530
|
+
}
|
|
26531
|
+
while (queue.length > 0) {
|
|
26532
|
+
const link = queue.shift();
|
|
26533
|
+
allLinks.push(link);
|
|
26534
|
+
const linkedData = linkedJiraIssues.get(link.key) ?? primaryIssues.get(link.key);
|
|
26535
|
+
if (!linkedData) continue;
|
|
26536
|
+
const linkedCommentSignals = [];
|
|
26537
|
+
for (const comment of linkedData.comments) {
|
|
26538
|
+
const text = extractCommentText(comment.body);
|
|
26539
|
+
const signals = detectCommentSignals(text);
|
|
26540
|
+
linkedCommentSignals.push(...signals);
|
|
26541
|
+
}
|
|
26542
|
+
if (linkedCommentSignals.length > 0 || linkedData.comments.length > 0) {
|
|
26543
|
+
allSignals.push({
|
|
26544
|
+
sourceKey: link.key,
|
|
26545
|
+
linkType: link.relationship,
|
|
26546
|
+
commentSignals: linkedCommentSignals,
|
|
26547
|
+
commentSummary: null
|
|
26548
|
+
});
|
|
26549
|
+
}
|
|
26550
|
+
const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
|
|
26551
|
+
for (const next of nextLinks) {
|
|
26552
|
+
visited.add(next.key);
|
|
26553
|
+
queue.push(next);
|
|
26554
|
+
}
|
|
26555
|
+
}
|
|
26556
|
+
return { allLinks, allSignals };
|
|
26557
|
+
}
|
|
26558
|
+
var BLOCKER_LINK_PATTERNS = ["blocks", "is blocked by"];
|
|
26559
|
+
var WONT_DO_STATUSES = /* @__PURE__ */ new Set(["wont do", "won't do", "cancelled"]);
|
|
26560
|
+
function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedUpdates) {
|
|
26561
|
+
if (linkedIssues.length === 0) return;
|
|
26562
|
+
const blockerLinks = linkedIssues.filter(
|
|
26563
|
+
(l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
|
|
26564
|
+
);
|
|
26565
|
+
if (blockerLinks.length > 0 && blockerLinks.every((l) => l.isDone) && frontmatter.status === "blocked") {
|
|
26566
|
+
proposedUpdates.push({
|
|
26567
|
+
artifactId: frontmatter.id,
|
|
26568
|
+
field: "status",
|
|
26569
|
+
currentValue: "blocked",
|
|
26570
|
+
proposedValue: "in-progress",
|
|
26571
|
+
reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
|
|
26572
|
+
});
|
|
26573
|
+
}
|
|
26574
|
+
const wontDoLinks = linkedIssues.filter(
|
|
26575
|
+
(l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
|
|
26576
|
+
);
|
|
26577
|
+
if (wontDoLinks.length > 0) {
|
|
26578
|
+
proposedUpdates.push({
|
|
26579
|
+
artifactId: frontmatter.id,
|
|
26580
|
+
field: "review",
|
|
26581
|
+
currentValue: null,
|
|
26582
|
+
proposedValue: "needs-review",
|
|
26583
|
+
reason: `Linked issue(s) cancelled/won't do: ${wontDoLinks.map((l) => `${l.key} "${l.summary}"`).join(", ")}`
|
|
26584
|
+
});
|
|
26585
|
+
}
|
|
26586
|
+
}
|
|
26587
|
+
var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
|
|
26588
|
+
|
|
26589
|
+
For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
|
|
26590
|
+
|
|
26591
|
+
Return your response as a JSON object mapping artifact IDs to objects mapping linked issue keys to summary strings.
|
|
26592
|
+
Example: {"T-001": {"PROJ-301": "DBA review scheduled for Thursday."}}
|
|
26593
|
+
|
|
26594
|
+
IMPORTANT: Only return the JSON object, no other text.`;
|
|
26595
|
+
async function analyzeLinkedIssueComments(items, linkedJiraIssues) {
|
|
26596
|
+
const results = /* @__PURE__ */ new Map();
|
|
26597
|
+
const promptParts = [];
|
|
26598
|
+
const itemsWithLinkedComments = [];
|
|
26599
|
+
for (const item of items) {
|
|
26600
|
+
if (item.linkedIssueSignals.length === 0) continue;
|
|
26601
|
+
const linkedParts = [];
|
|
26602
|
+
for (const signal of item.linkedIssueSignals) {
|
|
26603
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26604
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
26605
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
26606
|
+
const text = extractCommentText(c.body);
|
|
26607
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
26608
|
+
}).join("\n");
|
|
26609
|
+
linkedParts.push(` ### ${signal.sourceKey} (${signal.linkType})
|
|
26610
|
+
${commentTexts}`);
|
|
26611
|
+
}
|
|
26612
|
+
if (linkedParts.length > 0) {
|
|
26613
|
+
itemsWithLinkedComments.push(item);
|
|
26614
|
+
promptParts.push(`## ${item.id} \u2014 ${item.title}
|
|
26615
|
+
Linked issues:
|
|
26616
|
+
${linkedParts.join("\n")}`);
|
|
26617
|
+
}
|
|
26618
|
+
}
|
|
26619
|
+
if (promptParts.length === 0) return results;
|
|
26620
|
+
const prompt = promptParts.join("\n\n");
|
|
26621
|
+
const llmResult = query3({
|
|
26622
|
+
prompt,
|
|
26623
|
+
options: {
|
|
26624
|
+
systemPrompt: LINKED_COMMENT_ANALYSIS_PROMPT,
|
|
26625
|
+
maxTurns: 1,
|
|
26626
|
+
tools: [],
|
|
26627
|
+
allowedTools: []
|
|
26628
|
+
}
|
|
26629
|
+
});
|
|
26630
|
+
for await (const msg of llmResult) {
|
|
26631
|
+
if (msg.type === "assistant") {
|
|
26632
|
+
const textBlock = msg.message.content.find(
|
|
26633
|
+
(b) => b.type === "text"
|
|
26634
|
+
);
|
|
26635
|
+
if (textBlock) {
|
|
26636
|
+
const parsed = parseLlmJson(textBlock.text);
|
|
26637
|
+
if (parsed) {
|
|
26638
|
+
for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
|
|
26639
|
+
if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
|
|
26640
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
26641
|
+
for (const [key, summary] of Object.entries(linkedSummaries)) {
|
|
26642
|
+
if (typeof summary === "string") {
|
|
26643
|
+
signalMap.set(key, summary);
|
|
26644
|
+
}
|
|
26645
|
+
}
|
|
26646
|
+
if (signalMap.size > 0) {
|
|
26647
|
+
results.set(artifactId, signalMap);
|
|
26648
|
+
}
|
|
26649
|
+
}
|
|
26650
|
+
}
|
|
26651
|
+
}
|
|
26652
|
+
}
|
|
26653
|
+
}
|
|
26654
|
+
}
|
|
26655
|
+
return results;
|
|
26656
|
+
}
|
|
26657
|
+
function parseLlmJson(text) {
|
|
26658
|
+
try {
|
|
26659
|
+
return JSON.parse(text);
|
|
26660
|
+
} catch {
|
|
26661
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
26662
|
+
if (match) {
|
|
26663
|
+
try {
|
|
26664
|
+
return JSON.parse(match[1]);
|
|
26665
|
+
} catch {
|
|
26666
|
+
}
|
|
26667
|
+
}
|
|
26668
|
+
return null;
|
|
26669
|
+
}
|
|
26670
|
+
}
|
|
26406
26671
|
function formatProgressReport(report) {
|
|
26407
26672
|
const parts = [];
|
|
26408
26673
|
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
@@ -26486,7 +26751,7 @@ function formatProgressReport(report) {
|
|
|
26486
26751
|
}
|
|
26487
26752
|
function formatItemLine(parts, item, depth) {
|
|
26488
26753
|
const indent = " ".repeat(depth + 1);
|
|
26489
|
-
const statusIcon =
|
|
26754
|
+
const statusIcon = DONE_STATUSES15.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
26490
26755
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
26491
26756
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
26492
26757
|
const progressLabel = ` ${item.progress}%`;
|
|
@@ -26496,6 +26761,19 @@ function formatItemLine(parts, item, depth) {
|
|
|
26496
26761
|
if (item.commentSummary) {
|
|
26497
26762
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
26498
26763
|
}
|
|
26764
|
+
if (item.linkedIssues.length > 0) {
|
|
26765
|
+
parts.push(`${indent} \u{1F517} Linked Issues:`);
|
|
26766
|
+
for (const link of item.linkedIssues) {
|
|
26767
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
26768
|
+
const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
|
|
26769
|
+
const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
|
|
26770
|
+
parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
|
|
26771
|
+
const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
26772
|
+
if (signal?.commentSummary) {
|
|
26773
|
+
parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
|
|
26774
|
+
}
|
|
26775
|
+
}
|
|
26776
|
+
}
|
|
26499
26777
|
for (const child of item.children) {
|
|
26500
26778
|
formatItemLine(parts, child, depth + 1);
|
|
26501
26779
|
}
|
|
@@ -26505,6 +26783,506 @@ function progressBar6(pct) {
|
|
|
26505
26783
|
const empty = 10 - filled;
|
|
26506
26784
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
26507
26785
|
}
|
|
26786
|
+
var MAX_ARTIFACT_NODES = 50;
|
|
26787
|
+
var MAX_LLM_DEPTH = 3;
|
|
26788
|
+
var MAX_LLM_COMMENT_CHARS = 8e3;
|
|
26789
|
+
async function assessArtifact(store, client, host, options) {
|
|
26790
|
+
const visited = /* @__PURE__ */ new Set();
|
|
26791
|
+
return _assessArtifactRecursive(store, client, host, options, visited, 0);
|
|
26792
|
+
}
|
|
26793
|
+
async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
|
|
26794
|
+
const errors = [];
|
|
26795
|
+
if (visited.has(options.artifactId)) {
|
|
26796
|
+
return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
|
|
26797
|
+
}
|
|
26798
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
26799
|
+
return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
|
|
26800
|
+
}
|
|
26801
|
+
visited.add(options.artifactId);
|
|
26802
|
+
const doc = store.get(options.artifactId);
|
|
26803
|
+
if (!doc) {
|
|
26804
|
+
return emptyArtifactReport(options.artifactId, [`Artifact ${options.artifactId} not found`]);
|
|
26805
|
+
}
|
|
26806
|
+
const fm = doc.frontmatter;
|
|
26807
|
+
const jiraKey = fm.jiraKey ?? extractJiraKeyFromTags(fm.tags) ?? null;
|
|
26808
|
+
const tags = fm.tags ?? [];
|
|
26809
|
+
const sprintTag = tags.find((t) => t.startsWith("sprint:"));
|
|
26810
|
+
const sprint = sprintTag ? sprintTag.slice(7) : null;
|
|
26811
|
+
const parent = fm.aboutArtifact ?? null;
|
|
26812
|
+
let jiraStatus = null;
|
|
26813
|
+
let jiraAssignee = null;
|
|
26814
|
+
let proposedMarvinStatus = null;
|
|
26815
|
+
let jiraSubtaskProgress = null;
|
|
26816
|
+
const commentSignals = [];
|
|
26817
|
+
let commentSummary = null;
|
|
26818
|
+
let linkedIssues = [];
|
|
26819
|
+
let linkedIssueSignals = [];
|
|
26820
|
+
const proposedUpdates = [];
|
|
26821
|
+
const jiraIssues = /* @__PURE__ */ new Map();
|
|
26822
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
26823
|
+
if (jiraKey) {
|
|
26824
|
+
try {
|
|
26825
|
+
const [issue2, comments] = await Promise.all([
|
|
26826
|
+
client.getIssueWithLinks(jiraKey),
|
|
26827
|
+
client.getComments(jiraKey)
|
|
26828
|
+
]);
|
|
26829
|
+
jiraIssues.set(jiraKey, { issue: issue2, comments });
|
|
26830
|
+
jiraStatus = issue2.fields.status.name;
|
|
26831
|
+
jiraAssignee = issue2.fields.assignee?.displayName ?? null;
|
|
26832
|
+
const inSprint = isInActiveSprint(store, fm.tags);
|
|
26833
|
+
const resolved = options.statusMap ?? {};
|
|
26834
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
26835
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
26836
|
+
if (subtasks.length > 0) {
|
|
26837
|
+
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
26838
|
+
}
|
|
26839
|
+
for (const comment of comments) {
|
|
26840
|
+
const text = extractCommentText(comment.body);
|
|
26841
|
+
const signals2 = detectCommentSignals(text);
|
|
26842
|
+
commentSignals.push(...signals2);
|
|
26843
|
+
}
|
|
26844
|
+
const jiraVisited = /* @__PURE__ */ new Set([jiraKey]);
|
|
26845
|
+
const queue = [];
|
|
26846
|
+
const directLinks = collectLinkedIssues(issue2).filter((l) => l.relationship !== "subtask");
|
|
26847
|
+
for (const link of directLinks) {
|
|
26848
|
+
if (!jiraVisited.has(link.key)) {
|
|
26849
|
+
jiraVisited.add(link.key);
|
|
26850
|
+
queue.push(link.key);
|
|
26851
|
+
}
|
|
26852
|
+
}
|
|
26853
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
26854
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
26855
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
26856
|
+
const results = await Promise.allSettled(
|
|
26857
|
+
batch.map(async (key) => {
|
|
26858
|
+
const [li, lc] = await Promise.all([
|
|
26859
|
+
client.getIssueWithLinks(key),
|
|
26860
|
+
client.getComments(key)
|
|
26861
|
+
]);
|
|
26862
|
+
return { key, issue: li, comments: lc };
|
|
26863
|
+
})
|
|
26864
|
+
);
|
|
26865
|
+
for (const result of results) {
|
|
26866
|
+
if (result.status === "fulfilled") {
|
|
26867
|
+
const { key, issue: li, comments: lc } = result.value;
|
|
26868
|
+
linkedJiraIssues.set(key, { issue: li, comments: lc });
|
|
26869
|
+
const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
|
|
26870
|
+
for (const nl of newLinks) {
|
|
26871
|
+
jiraVisited.add(nl.key);
|
|
26872
|
+
queue.push(nl.key);
|
|
26873
|
+
}
|
|
26874
|
+
} else {
|
|
26875
|
+
const batchKey = batch[results.indexOf(result)];
|
|
26876
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
26877
|
+
}
|
|
26878
|
+
}
|
|
26879
|
+
}
|
|
26880
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
26881
|
+
issue2,
|
|
26882
|
+
jiraIssues,
|
|
26883
|
+
linkedJiraIssues
|
|
26884
|
+
);
|
|
26885
|
+
linkedIssues = allLinks;
|
|
26886
|
+
linkedIssueSignals = allSignals;
|
|
26887
|
+
analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
|
|
26888
|
+
} catch (err) {
|
|
26889
|
+
errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26890
|
+
}
|
|
26891
|
+
}
|
|
26892
|
+
const currentProgress = getEffectiveProgress(fm);
|
|
26893
|
+
const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
|
|
26894
|
+
const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
|
|
26895
|
+
if (statusDrift && proposedMarvinStatus) {
|
|
26896
|
+
proposedUpdates.push({
|
|
26897
|
+
artifactId: fm.id,
|
|
26898
|
+
field: "status",
|
|
26899
|
+
currentValue: fm.status,
|
|
26900
|
+
proposedValue: proposedMarvinStatus,
|
|
26901
|
+
reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
|
|
26902
|
+
});
|
|
26903
|
+
}
|
|
26904
|
+
if (progressDrift && jiraSubtaskProgress !== null) {
|
|
26905
|
+
proposedUpdates.push({
|
|
26906
|
+
artifactId: fm.id,
|
|
26907
|
+
field: "progress",
|
|
26908
|
+
currentValue: currentProgress,
|
|
26909
|
+
proposedValue: jiraSubtaskProgress,
|
|
26910
|
+
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
26911
|
+
});
|
|
26912
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
26913
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
26914
|
+
if (!hasExplicitProgress) {
|
|
26915
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
26916
|
+
if (proposedProgress !== currentProgress) {
|
|
26917
|
+
proposedUpdates.push({
|
|
26918
|
+
artifactId: fm.id,
|
|
26919
|
+
field: "progress",
|
|
26920
|
+
currentValue: currentProgress,
|
|
26921
|
+
proposedValue: proposedProgress,
|
|
26922
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
26923
|
+
});
|
|
26924
|
+
}
|
|
26925
|
+
}
|
|
26926
|
+
}
|
|
26927
|
+
const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
|
|
26928
|
+
if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
|
|
26929
|
+
const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
|
|
26930
|
+
if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
|
|
26931
|
+
try {
|
|
26932
|
+
const summary = await analyzeSingleArtifactComments(
|
|
26933
|
+
fm.id,
|
|
26934
|
+
fm.title,
|
|
26935
|
+
jiraKey,
|
|
26936
|
+
jiraStatus,
|
|
26937
|
+
jiraIssues,
|
|
26938
|
+
linkedJiraIssues,
|
|
26939
|
+
linkedIssueSignals
|
|
26940
|
+
);
|
|
26941
|
+
commentSummary = summary;
|
|
26942
|
+
} catch (err) {
|
|
26943
|
+
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26944
|
+
}
|
|
26945
|
+
}
|
|
26946
|
+
}
|
|
26947
|
+
const childIds = findChildIds(store, fm);
|
|
26948
|
+
const children = [];
|
|
26949
|
+
for (const childId of childIds) {
|
|
26950
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
26951
|
+
errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
|
|
26952
|
+
break;
|
|
26953
|
+
}
|
|
26954
|
+
const childReport = await _assessArtifactRecursive(
|
|
26955
|
+
store,
|
|
26956
|
+
client,
|
|
26957
|
+
host,
|
|
26958
|
+
{ ...options, artifactId: childId },
|
|
26959
|
+
visited,
|
|
26960
|
+
depth + 1
|
|
26961
|
+
);
|
|
26962
|
+
children.push(childReport);
|
|
26963
|
+
}
|
|
26964
|
+
const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
|
|
26965
|
+
const appliedUpdates = [];
|
|
26966
|
+
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
26967
|
+
for (const update of proposedUpdates) {
|
|
26968
|
+
if (update.field === "review") continue;
|
|
26969
|
+
try {
|
|
26970
|
+
store.update(update.artifactId, {
|
|
26971
|
+
[update.field]: update.proposedValue,
|
|
26972
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26973
|
+
});
|
|
26974
|
+
const updatedDoc = store.get(update.artifactId);
|
|
26975
|
+
if (updatedDoc) {
|
|
26976
|
+
if (updatedDoc.frontmatter.type === "task") {
|
|
26977
|
+
propagateProgressFromTask(store, update.artifactId);
|
|
26978
|
+
} else if (updatedDoc.frontmatter.type === "action") {
|
|
26979
|
+
propagateProgressToAction(store, update.artifactId);
|
|
26980
|
+
}
|
|
26981
|
+
}
|
|
26982
|
+
appliedUpdates.push(update);
|
|
26983
|
+
} catch (err) {
|
|
26984
|
+
errors.push(`Failed to apply update: ${err instanceof Error ? err.message : String(err)}`);
|
|
26985
|
+
}
|
|
26986
|
+
}
|
|
26987
|
+
}
|
|
26988
|
+
return {
|
|
26989
|
+
artifactId: fm.id,
|
|
26990
|
+
title: fm.title,
|
|
26991
|
+
type: fm.type,
|
|
26992
|
+
marvinStatus: fm.status,
|
|
26993
|
+
marvinProgress: currentProgress,
|
|
26994
|
+
sprint,
|
|
26995
|
+
parent,
|
|
26996
|
+
jiraKey,
|
|
26997
|
+
jiraStatus,
|
|
26998
|
+
jiraAssignee,
|
|
26999
|
+
jiraSubtaskProgress,
|
|
27000
|
+
proposedMarvinStatus,
|
|
27001
|
+
statusDrift,
|
|
27002
|
+
progressDrift,
|
|
27003
|
+
commentSignals,
|
|
27004
|
+
commentSummary,
|
|
27005
|
+
linkedIssues,
|
|
27006
|
+
linkedIssueSignals,
|
|
27007
|
+
children,
|
|
27008
|
+
proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
|
|
27009
|
+
appliedUpdates,
|
|
27010
|
+
signals,
|
|
27011
|
+
errors
|
|
27012
|
+
};
|
|
27013
|
+
}
|
|
27014
|
+
function findChildIds(store, fm) {
|
|
27015
|
+
if (fm.type === "action") {
|
|
27016
|
+
return store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === fm.id).map((d) => d.frontmatter.id);
|
|
27017
|
+
}
|
|
27018
|
+
if (fm.type === "epic") {
|
|
27019
|
+
const epicTag = `epic:${fm.id}`;
|
|
27020
|
+
const isLinked = (d) => {
|
|
27021
|
+
const le = d.frontmatter.linkedEpic;
|
|
27022
|
+
if (le?.includes(fm.id)) return true;
|
|
27023
|
+
const t = d.frontmatter.tags ?? [];
|
|
27024
|
+
return t.includes(epicTag);
|
|
27025
|
+
};
|
|
27026
|
+
return [
|
|
27027
|
+
...store.list({ type: "action" }).filter(isLinked),
|
|
27028
|
+
...store.list({ type: "task" }).filter(isLinked)
|
|
27029
|
+
].map((d) => d.frontmatter.id);
|
|
27030
|
+
}
|
|
27031
|
+
return [];
|
|
27032
|
+
}
|
|
27033
|
+
function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus) {
|
|
27034
|
+
const signals = [];
|
|
27035
|
+
const blockerSignals = commentSignals.filter((s) => s.type === "blocker");
|
|
27036
|
+
if (blockerSignals.length > 0) {
|
|
27037
|
+
for (const s of blockerSignals) {
|
|
27038
|
+
signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
|
|
27039
|
+
}
|
|
27040
|
+
}
|
|
27041
|
+
const blockingLinks = linkedIssues.filter(
|
|
27042
|
+
(l) => l.relationship.toLowerCase().includes("block")
|
|
27043
|
+
);
|
|
27044
|
+
const activeBlockers = blockingLinks.filter((l) => !l.isDone);
|
|
27045
|
+
const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
|
|
27046
|
+
if (activeBlockers.length > 0) {
|
|
27047
|
+
for (const b of activeBlockers) {
|
|
27048
|
+
signals.push(`\u{1F6AB} Blocker \u2014 ${b.relationship} ${b.key} "${b.summary}" [${b.status}]`);
|
|
27049
|
+
}
|
|
27050
|
+
}
|
|
27051
|
+
if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
|
|
27052
|
+
signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
|
|
27053
|
+
}
|
|
27054
|
+
const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
|
|
27055
|
+
for (const l of wontDoLinks) {
|
|
27056
|
+
signals.push(`\u{1F504} Superseded \u2014 ${l.key} "${l.summary}" is ${l.status}`);
|
|
27057
|
+
}
|
|
27058
|
+
const questionSignals = commentSignals.filter((s) => s.type === "question");
|
|
27059
|
+
for (const s of questionSignals) {
|
|
27060
|
+
signals.push(`\u23F3 Waiting \u2014 "${s.snippet}"`);
|
|
27061
|
+
}
|
|
27062
|
+
const relatedInProgress = linkedIssues.filter(
|
|
27063
|
+
(l) => l.relationship.toLowerCase().includes("relate") && !l.isDone
|
|
27064
|
+
);
|
|
27065
|
+
if (relatedInProgress.length > 0) {
|
|
27066
|
+
for (const l of relatedInProgress) {
|
|
27067
|
+
signals.push(`\u{1F4CB} Handoff \u2014 related work on ${l.key} "${l.summary}" [${l.status}]`);
|
|
27068
|
+
}
|
|
27069
|
+
}
|
|
27070
|
+
if (signals.length === 0) {
|
|
27071
|
+
if (statusDrift && proposedStatus) {
|
|
27072
|
+
signals.push(`\u26A0 Drift detected \u2014 Marvin and Jira statuses diverge`);
|
|
27073
|
+
} else {
|
|
27074
|
+
signals.push(`\u2705 No active blockers or concerns detected`);
|
|
27075
|
+
}
|
|
27076
|
+
}
|
|
27077
|
+
return signals;
|
|
27078
|
+
}
|
|
27079
|
+
function estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
27080
|
+
let total = 0;
|
|
27081
|
+
for (const [, data] of jiraIssues) {
|
|
27082
|
+
for (const c of data.comments) {
|
|
27083
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
27084
|
+
}
|
|
27085
|
+
}
|
|
27086
|
+
for (const signal of linkedIssueSignals) {
|
|
27087
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
27088
|
+
if (!linkedData) continue;
|
|
27089
|
+
for (const c of linkedData.comments) {
|
|
27090
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
27091
|
+
}
|
|
27092
|
+
}
|
|
27093
|
+
return total;
|
|
27094
|
+
}
|
|
27095
|
+
var SINGLE_ARTIFACT_COMMENT_PROMPT = `You are a delivery management assistant analyzing Jira comments for a single work item.
|
|
27096
|
+
|
|
27097
|
+
Produce a 2-3 sentence progress summary covering:
|
|
27098
|
+
- What work has been completed
|
|
27099
|
+
- What is pending or blocked
|
|
27100
|
+
- Any decisions, handoffs, or scheduling mentioned
|
|
27101
|
+
- Relevant context from linked issue comments (if provided)
|
|
27102
|
+
|
|
27103
|
+
Return ONLY the summary text, no JSON or formatting.`;
|
|
27104
|
+
async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraStatus, jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
27105
|
+
const promptParts = [];
|
|
27106
|
+
const primaryData = jiraIssues.get(jiraKey);
|
|
27107
|
+
if (primaryData && primaryData.comments.length > 0) {
|
|
27108
|
+
const commentTexts = primaryData.comments.map((c) => {
|
|
27109
|
+
const text = extractCommentText(c.body);
|
|
27110
|
+
return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
|
|
27111
|
+
}).join("\n");
|
|
27112
|
+
promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
|
|
27113
|
+
Comments:
|
|
27114
|
+
${commentTexts}`);
|
|
27115
|
+
}
|
|
27116
|
+
for (const signal of linkedIssueSignals) {
|
|
27117
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
27118
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
27119
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
27120
|
+
const text = extractCommentText(c.body);
|
|
27121
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
27122
|
+
}).join("\n");
|
|
27123
|
+
promptParts.push(`### Linked: ${signal.sourceKey} (${signal.linkType})
|
|
27124
|
+
${commentTexts}`);
|
|
27125
|
+
}
|
|
27126
|
+
if (promptParts.length === 0) return null;
|
|
27127
|
+
const prompt = promptParts.join("\n\n");
|
|
27128
|
+
const result = query3({
|
|
27129
|
+
prompt,
|
|
27130
|
+
options: {
|
|
27131
|
+
systemPrompt: SINGLE_ARTIFACT_COMMENT_PROMPT,
|
|
27132
|
+
maxTurns: 1,
|
|
27133
|
+
tools: [],
|
|
27134
|
+
allowedTools: []
|
|
27135
|
+
}
|
|
27136
|
+
});
|
|
27137
|
+
for await (const msg of result) {
|
|
27138
|
+
if (msg.type === "assistant") {
|
|
27139
|
+
const textBlock = msg.message.content.find(
|
|
27140
|
+
(b) => b.type === "text"
|
|
27141
|
+
);
|
|
27142
|
+
if (textBlock) {
|
|
27143
|
+
return textBlock.text.trim();
|
|
27144
|
+
}
|
|
27145
|
+
}
|
|
27146
|
+
}
|
|
27147
|
+
return null;
|
|
27148
|
+
}
|
|
27149
|
+
function emptyArtifactReport(artifactId, errors) {
|
|
27150
|
+
return {
|
|
27151
|
+
artifactId,
|
|
27152
|
+
title: "Not found",
|
|
27153
|
+
type: "unknown",
|
|
27154
|
+
marvinStatus: "unknown",
|
|
27155
|
+
marvinProgress: 0,
|
|
27156
|
+
sprint: null,
|
|
27157
|
+
parent: null,
|
|
27158
|
+
jiraKey: null,
|
|
27159
|
+
jiraStatus: null,
|
|
27160
|
+
jiraAssignee: null,
|
|
27161
|
+
jiraSubtaskProgress: null,
|
|
27162
|
+
proposedMarvinStatus: null,
|
|
27163
|
+
statusDrift: false,
|
|
27164
|
+
progressDrift: false,
|
|
27165
|
+
commentSignals: [],
|
|
27166
|
+
commentSummary: null,
|
|
27167
|
+
linkedIssues: [],
|
|
27168
|
+
linkedIssueSignals: [],
|
|
27169
|
+
children: [],
|
|
27170
|
+
proposedUpdates: [],
|
|
27171
|
+
appliedUpdates: [],
|
|
27172
|
+
signals: [],
|
|
27173
|
+
errors
|
|
27174
|
+
};
|
|
27175
|
+
}
|
|
27176
|
+
function formatArtifactReport(report) {
|
|
27177
|
+
const parts = [];
|
|
27178
|
+
parts.push(`# Artifact Assessment \u2014 ${report.artifactId}`);
|
|
27179
|
+
parts.push(report.title);
|
|
27180
|
+
parts.push("");
|
|
27181
|
+
parts.push(`## Marvin State`);
|
|
27182
|
+
const marvinParts = [`Status: ${report.marvinStatus}`, `Progress: ${report.marvinProgress}%`];
|
|
27183
|
+
if (report.sprint) marvinParts.push(`Sprint: ${report.sprint}`);
|
|
27184
|
+
if (report.parent) marvinParts.push(`Parent: ${report.parent}`);
|
|
27185
|
+
parts.push(marvinParts.join(" | "));
|
|
27186
|
+
parts.push("");
|
|
27187
|
+
if (report.jiraKey) {
|
|
27188
|
+
parts.push(`## Jira State (${report.jiraKey})`);
|
|
27189
|
+
const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
|
|
27190
|
+
if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
|
|
27191
|
+
if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
|
|
27192
|
+
parts.push(jiraParts.join(" | "));
|
|
27193
|
+
if (report.statusDrift) {
|
|
27194
|
+
parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
|
|
27195
|
+
}
|
|
27196
|
+
if (report.progressDrift && report.jiraSubtaskProgress !== null) {
|
|
27197
|
+
parts.push(`\u26A0 Progress drift: ${report.marvinProgress}% \u2192 ${report.jiraSubtaskProgress}%`);
|
|
27198
|
+
}
|
|
27199
|
+
parts.push("");
|
|
27200
|
+
}
|
|
27201
|
+
if (report.commentSummary) {
|
|
27202
|
+
parts.push(`## Comments`);
|
|
27203
|
+
parts.push(report.commentSummary);
|
|
27204
|
+
parts.push("");
|
|
27205
|
+
}
|
|
27206
|
+
if (report.children.length > 0) {
|
|
27207
|
+
const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
|
|
27208
|
+
const childWeights = report.children.map((c) => {
|
|
27209
|
+
const { weight } = resolveWeight(void 0);
|
|
27210
|
+
return { weight, progress: c.marvinProgress };
|
|
27211
|
+
});
|
|
27212
|
+
const childProgress = childWeights.length > 0 ? Math.round(childWeights.reduce((s, c) => s + c.weight * c.progress, 0) / childWeights.reduce((s, c) => s + c.weight, 0)) : 0;
|
|
27213
|
+
const bar = progressBar6(childProgress);
|
|
27214
|
+
parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
|
|
27215
|
+
for (const child of report.children) {
|
|
27216
|
+
formatArtifactChild(parts, child, 1);
|
|
27217
|
+
}
|
|
27218
|
+
parts.push("");
|
|
27219
|
+
}
|
|
27220
|
+
if (report.linkedIssues.length > 0) {
|
|
27221
|
+
parts.push(`## Linked Issues (${report.linkedIssues.length})`);
|
|
27222
|
+
for (const link of report.linkedIssues) {
|
|
27223
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
27224
|
+
parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
|
|
27225
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
27226
|
+
if (signal?.commentSummary) {
|
|
27227
|
+
parts.push(` \u{1F4AC} ${signal.commentSummary}`);
|
|
27228
|
+
}
|
|
27229
|
+
}
|
|
27230
|
+
parts.push("");
|
|
27231
|
+
}
|
|
27232
|
+
if (report.signals.length > 0) {
|
|
27233
|
+
parts.push(`## Signals`);
|
|
27234
|
+
for (const s of report.signals) {
|
|
27235
|
+
parts.push(` ${s}`);
|
|
27236
|
+
}
|
|
27237
|
+
parts.push("");
|
|
27238
|
+
}
|
|
27239
|
+
if (report.proposedUpdates.length > 0) {
|
|
27240
|
+
parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
|
|
27241
|
+
for (const update of report.proposedUpdates) {
|
|
27242
|
+
parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
27243
|
+
parts.push(` Reason: ${update.reason}`);
|
|
27244
|
+
}
|
|
27245
|
+
parts.push("");
|
|
27246
|
+
parts.push("Run with applyUpdates=true to apply these changes.");
|
|
27247
|
+
parts.push("");
|
|
27248
|
+
}
|
|
27249
|
+
if (report.appliedUpdates.length > 0) {
|
|
27250
|
+
parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
|
|
27251
|
+
for (const update of report.appliedUpdates) {
|
|
27252
|
+
parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
27253
|
+
}
|
|
27254
|
+
parts.push("");
|
|
27255
|
+
}
|
|
27256
|
+
if (report.errors.length > 0) {
|
|
27257
|
+
parts.push(`## Errors`);
|
|
27258
|
+
for (const err of report.errors) {
|
|
27259
|
+
parts.push(` ${err}`);
|
|
27260
|
+
}
|
|
27261
|
+
parts.push("");
|
|
27262
|
+
}
|
|
27263
|
+
return parts.join("\n");
|
|
27264
|
+
}
|
|
27265
|
+
function formatArtifactChild(parts, child, depth) {
|
|
27266
|
+
const indent = " ".repeat(depth);
|
|
27267
|
+
const icon = DONE_STATUSES15.has(child.marvinStatus) ? "\u2713" : child.marvinStatus === "blocked" ? "\u{1F6AB}" : child.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
27268
|
+
const jiraLabel = child.jiraKey ? ` [${child.jiraKey}: ${child.jiraStatus ?? "?"}]` : "";
|
|
27269
|
+
const driftLabel = child.statusDrift ? ` \u26A0drift \u2192 ${child.proposedMarvinStatus}` : "";
|
|
27270
|
+
const signalHints = [];
|
|
27271
|
+
for (const s of child.signals) {
|
|
27272
|
+
if (s.startsWith("\u2705 No active")) continue;
|
|
27273
|
+
signalHints.push(s);
|
|
27274
|
+
}
|
|
27275
|
+
parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
|
|
27276
|
+
if (child.commentSummary) {
|
|
27277
|
+
parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
|
|
27278
|
+
}
|
|
27279
|
+
for (const hint of signalHints) {
|
|
27280
|
+
parts.push(`${indent} ${hint}`);
|
|
27281
|
+
}
|
|
27282
|
+
for (const grandchild of child.children) {
|
|
27283
|
+
formatArtifactChild(parts, grandchild, depth + 1);
|
|
27284
|
+
}
|
|
27285
|
+
}
|
|
26508
27286
|
|
|
26509
27287
|
// src/skills/builtin/jira/tools.ts
|
|
26510
27288
|
var JIRA_TYPE = "jira-issue";
|
|
@@ -26547,7 +27325,7 @@ function findByJiraKey(store, jiraKey) {
|
|
|
26547
27325
|
function createJiraTools(store, projectConfig) {
|
|
26548
27326
|
const jiraUserConfig = loadUserConfig().jira;
|
|
26549
27327
|
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
26550
|
-
const statusMap = projectConfig?.jira?.statusMap;
|
|
27328
|
+
const statusMap = normalizeStatusMap(projectConfig?.jira?.statusMap);
|
|
26551
27329
|
return [
|
|
26552
27330
|
// --- Local read tools ---
|
|
26553
27331
|
tool20(
|
|
@@ -27174,22 +27952,31 @@ function createJiraTools(store, projectConfig) {
|
|
|
27174
27952
|
const s = issue2.fields.status.name;
|
|
27175
27953
|
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
27176
27954
|
}
|
|
27177
|
-
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
27178
|
-
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
27179
27955
|
const actionLookup = /* @__PURE__ */ new Map();
|
|
27180
|
-
for (const [marvin, value] of Object.entries(actionMap)) {
|
|
27181
|
-
const jiraStatuses = Array.isArray(value) ? value : value.default;
|
|
27182
|
-
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
27183
|
-
if (!Array.isArray(value) && value.inSprint) {
|
|
27184
|
-
for (const js of value.inSprint) actionLookup.set(js.toLowerCase(), `${marvin} (inSprint)`);
|
|
27185
|
-
}
|
|
27186
|
-
}
|
|
27187
27956
|
const taskLookup = /* @__PURE__ */ new Map();
|
|
27188
|
-
|
|
27189
|
-
const
|
|
27190
|
-
|
|
27191
|
-
|
|
27192
|
-
|
|
27957
|
+
if (statusMap.flat) {
|
|
27958
|
+
for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
|
|
27959
|
+
const lower = jiraStatus.toLowerCase();
|
|
27960
|
+
if (typeof value === "string") {
|
|
27961
|
+
actionLookup.set(lower, value);
|
|
27962
|
+
taskLookup.set(lower, value);
|
|
27963
|
+
} else {
|
|
27964
|
+
actionLookup.set(lower, value.default);
|
|
27965
|
+
taskLookup.set(lower, value.default);
|
|
27966
|
+
if (value.inSprint) {
|
|
27967
|
+
actionLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
|
|
27968
|
+
taskLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
|
|
27969
|
+
}
|
|
27970
|
+
}
|
|
27971
|
+
}
|
|
27972
|
+
} else {
|
|
27973
|
+
const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
27974
|
+
const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
27975
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
27976
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
27977
|
+
}
|
|
27978
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
27979
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
27193
27980
|
}
|
|
27194
27981
|
}
|
|
27195
27982
|
const parts = [
|
|
@@ -27211,25 +27998,20 @@ function createJiraTools(store, projectConfig) {
|
|
|
27211
27998
|
if (!taskTarget) unmappedTask.push(status);
|
|
27212
27999
|
}
|
|
27213
28000
|
if (unmappedAction.length > 0 || unmappedTask.length > 0) {
|
|
28001
|
+
const allUnmapped = [.../* @__PURE__ */ new Set([...unmappedAction, ...unmappedTask])];
|
|
27214
28002
|
parts.push("");
|
|
27215
28003
|
parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
|
|
27216
28004
|
parts.push(" jira:");
|
|
27217
28005
|
parts.push(" statusMap:");
|
|
27218
|
-
|
|
27219
|
-
parts.push("
|
|
27220
|
-
parts.push(` # Map these: ${unmappedAction.join(", ")}`);
|
|
27221
|
-
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
27222
|
-
}
|
|
27223
|
-
if (unmappedTask.length > 0) {
|
|
27224
|
-
parts.push(" task:");
|
|
27225
|
-
parts.push(` # Map these: ${unmappedTask.join(", ")}`);
|
|
27226
|
-
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
28006
|
+
for (const s of allUnmapped) {
|
|
28007
|
+
parts.push(` "${s}": <marvin-status>`);
|
|
27227
28008
|
}
|
|
28009
|
+
parts.push(" # Supported marvin statuses: done, in-progress, review, ready, blocked, backlog, open");
|
|
27228
28010
|
} else {
|
|
27229
28011
|
parts.push("");
|
|
27230
28012
|
parts.push("All statuses are mapped.");
|
|
27231
28013
|
}
|
|
27232
|
-
const usingConfig = statusMap
|
|
28014
|
+
const usingConfig = statusMap.flat || statusMap.legacy;
|
|
27233
28015
|
parts.push("");
|
|
27234
28016
|
parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
|
|
27235
28017
|
return {
|
|
@@ -27287,7 +28069,8 @@ function createJiraTools(store, projectConfig) {
|
|
|
27287
28069
|
{
|
|
27288
28070
|
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
27289
28071
|
analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
|
|
27290
|
-
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
|
|
28072
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)"),
|
|
28073
|
+
traverseLinks: external_exports.boolean().optional().describe("Traverse Jira issue links (1 hop) to surface context from connected issues \u2014 blockers, related work, cancelled items (default false)")
|
|
27291
28074
|
},
|
|
27292
28075
|
async (args) => {
|
|
27293
28076
|
const jira = createJiraClient(jiraUserConfig);
|
|
@@ -27300,6 +28083,7 @@ function createJiraTools(store, projectConfig) {
|
|
|
27300
28083
|
sprintId: args.sprintId,
|
|
27301
28084
|
analyzeComments: args.analyzeComments ?? false,
|
|
27302
28085
|
applyUpdates: args.applyUpdates ?? false,
|
|
28086
|
+
traverseLinks: args.traverseLinks ?? false,
|
|
27303
28087
|
statusMap
|
|
27304
28088
|
}
|
|
27305
28089
|
);
|
|
@@ -27309,6 +28093,34 @@ function createJiraTools(store, projectConfig) {
|
|
|
27309
28093
|
};
|
|
27310
28094
|
},
|
|
27311
28095
|
{ annotations: { readOnlyHint: false } }
|
|
28096
|
+
),
|
|
28097
|
+
// --- Single-artifact assessment ---
|
|
28098
|
+
tool20(
|
|
28099
|
+
"assess_artifact",
|
|
28100
|
+
"Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
|
|
28101
|
+
{
|
|
28102
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
|
|
28103
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
|
|
28104
|
+
},
|
|
28105
|
+
async (args) => {
|
|
28106
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
28107
|
+
if (!jira) return jiraNotConfiguredError();
|
|
28108
|
+
const report = await assessArtifact(
|
|
28109
|
+
store,
|
|
28110
|
+
jira.client,
|
|
28111
|
+
jira.host,
|
|
28112
|
+
{
|
|
28113
|
+
artifactId: args.artifactId,
|
|
28114
|
+
applyUpdates: args.applyUpdates ?? false,
|
|
28115
|
+
statusMap
|
|
28116
|
+
}
|
|
28117
|
+
);
|
|
28118
|
+
return {
|
|
28119
|
+
content: [{ type: "text", text: formatArtifactReport(report) }],
|
|
28120
|
+
isError: report.errors.length > 0 && report.type === "unknown"
|
|
28121
|
+
};
|
|
28122
|
+
},
|
|
28123
|
+
{ annotations: { readOnlyHint: false } }
|
|
27312
28124
|
)
|
|
27313
28125
|
];
|
|
27314
28126
|
}
|
|
@@ -27410,7 +28222,8 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
27410
28222
|
- \`read_confluence_page\` \u2014 **read-only**: fetch and return the content of a Confluence page by URL or page ID. Use this to review Confluence content for updating tasks, generating contributions, or answering questions.
|
|
27411
28223
|
- \`fetch_jira_status\` \u2014 **read-only**: fetch current Jira status, subtask progress, and linked issues for Jira-linked actions/tasks. Returns proposed changes without applying them.
|
|
27412
28224
|
- \`fetch_jira_daily\` \u2014 **read-only**: fetch a daily/range summary of all Jira changes \u2014 status transitions, comments, linked Confluence pages, and cross-references with Marvin artifacts. Returns proposed actions (status updates, unlinked issues, question candidates, Confluence pages to review).
|
|
27413
|
-
- \`assess_sprint_progress\` \u2014 fetch live Jira statuses for all sprint-scoped items, detect drift, group by focus area with rollup progress, and extract comment signals. Use \`analyzeComments=true\` for LLM summaries, \`applyUpdates=true\` to apply changes.
|
|
28225
|
+
- \`assess_sprint_progress\` \u2014 fetch live Jira statuses for all sprint-scoped items, detect drift, group by focus area with rollup progress, and extract comment signals. Use \`analyzeComments=true\` for LLM summaries, \`applyUpdates=true\` to apply changes, \`traverseLinks=true\` to walk Jira issue links recursively.
|
|
28226
|
+
- \`assess_artifact\` \u2014 deep assessment of a single artifact (task, action, or epic). Fetches Jira status, LLM-summarizes comments, recursively traverses linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work). Use \`applyUpdates=true\` to apply proposed changes.
|
|
27414
28227
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
27415
28228
|
- \`search_jira\` \u2014 **read-only**: search Jira via JQL and return results with Marvin cross-references. No documents created \u2014 use to preview before importing or find issues for linking.
|
|
27416
28229
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import Jira issues as local JI-xxx documents (for Jira-originated items with no existing Marvin artifact).
|
|
@@ -27427,6 +28240,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
27427
28240
|
2. Review focus area rollups, status drift, and blockers
|
|
27428
28241
|
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
27429
28242
|
|
|
28243
|
+
**Single-artifact deep dive:**
|
|
28244
|
+
1. Call \`assess_artifact\` with an artifact ID to get a focused assessment with Jira sync, comment analysis, link traversal, and child rollup
|
|
28245
|
+
2. Review signals (blockers, unblocks, handoffs) and proposed updates
|
|
28246
|
+
3. Use \`applyUpdates=true\` to apply changes
|
|
28247
|
+
|
|
27430
28248
|
**Daily review workflow:**
|
|
27431
28249
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
27432
28250
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -27451,6 +28269,7 @@ ${COMMON_WORKFLOW}
|
|
|
27451
28269
|
**As Product Owner, use Jira integration to:**
|
|
27452
28270
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
27453
28271
|
- Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
|
|
28272
|
+
- Use \`assess_artifact\` for deep dives into specific features or epics \u2014 full Jira context, linked issues, and child rollup
|
|
27454
28273
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
27455
28274
|
- Push approved features as Stories for development tracking
|
|
27456
28275
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -27464,6 +28283,7 @@ ${COMMON_WORKFLOW}
|
|
|
27464
28283
|
**As Tech Lead, use Jira integration to:**
|
|
27465
28284
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
27466
28285
|
- Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
|
|
28286
|
+
- Use \`assess_artifact\` to investigate a specific task \u2014 see full dependency chain, comment context, and blocker status
|
|
27467
28287
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
27468
28288
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
27469
28289
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -27479,6 +28299,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
27479
28299
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
27480
28300
|
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
27481
28301
|
- Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
|
|
28302
|
+
- Use \`assess_artifact\` for focused status checks on critical items \u2014 full Jira context without running the whole sprint assessment
|
|
27482
28303
|
- Pull sprint issues for tracking progress and blockers
|
|
27483
28304
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
27484
28305
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -32364,7 +33185,7 @@ async function jiraSyncCommand(artifactId, options = {}) {
|
|
|
32364
33185
|
);
|
|
32365
33186
|
return;
|
|
32366
33187
|
}
|
|
32367
|
-
const statusMap = project.config.jira?.statusMap;
|
|
33188
|
+
const statusMap = normalizeStatusMap(project.config.jira?.statusMap);
|
|
32368
33189
|
const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
|
|
32369
33190
|
console.log(chalk20.dim(label));
|
|
32370
33191
|
if (options.dryRun) {
|
|
@@ -32481,9 +33302,7 @@ async function jiraStatusesCommand(projectKey) {
|
|
|
32481
33302
|
return;
|
|
32482
33303
|
}
|
|
32483
33304
|
console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
|
|
32484
|
-
const statusMap = project.config.jira?.statusMap;
|
|
32485
|
-
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
32486
|
-
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
33305
|
+
const statusMap = normalizeStatusMap(project.config.jira?.statusMap);
|
|
32487
33306
|
const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
|
|
32488
33307
|
const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
|
|
32489
33308
|
const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
|
|
@@ -32507,14 +33326,28 @@ async function jiraStatusesCommand(projectKey) {
|
|
|
32507
33326
|
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
32508
33327
|
}
|
|
32509
33328
|
const actionLookup = /* @__PURE__ */ new Map();
|
|
32510
|
-
for (const [marvin, value] of Object.entries(actionMap)) {
|
|
32511
|
-
const jiraStatuses = Array.isArray(value) ? value : value.default;
|
|
32512
|
-
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
32513
|
-
}
|
|
32514
33329
|
const taskLookup = /* @__PURE__ */ new Map();
|
|
32515
|
-
|
|
32516
|
-
const
|
|
32517
|
-
|
|
33330
|
+
if (statusMap.flat) {
|
|
33331
|
+
for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
|
|
33332
|
+
const lower = jiraStatus.toLowerCase();
|
|
33333
|
+
if (typeof value === "string") {
|
|
33334
|
+
actionLookup.set(lower, value);
|
|
33335
|
+
taskLookup.set(lower, value);
|
|
33336
|
+
} else {
|
|
33337
|
+
const label = value.inSprint ? `${value.default} / ${value.inSprint} (inSprint)` : value.default;
|
|
33338
|
+
actionLookup.set(lower, label);
|
|
33339
|
+
taskLookup.set(lower, label);
|
|
33340
|
+
}
|
|
33341
|
+
}
|
|
33342
|
+
} else {
|
|
33343
|
+
const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
33344
|
+
const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
33345
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
33346
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
33347
|
+
}
|
|
33348
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
33349
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
33350
|
+
}
|
|
32518
33351
|
}
|
|
32519
33352
|
console.log(
|
|
32520
33353
|
`
|
|
@@ -32537,14 +33370,11 @@ Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.
|
|
|
32537
33370
|
console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
|
|
32538
33371
|
console.log(chalk20.dim(" jira:"));
|
|
32539
33372
|
console.log(chalk20.dim(" statusMap:"));
|
|
32540
|
-
console.log(chalk20.dim("
|
|
32541
|
-
console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
|
|
32542
|
-
console.log(chalk20.dim(" task:"));
|
|
32543
|
-
console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
|
|
33373
|
+
console.log(chalk20.dim(' "<Jira Status>": <marvin-status>'));
|
|
32544
33374
|
} else {
|
|
32545
33375
|
console.log(chalk20.green("\nAll statuses are mapped."));
|
|
32546
33376
|
}
|
|
32547
|
-
const usingConfig = statusMap
|
|
33377
|
+
const usingConfig = statusMap.flat || statusMap.legacy;
|
|
32548
33378
|
console.log(
|
|
32549
33379
|
chalk20.dim(
|
|
32550
33380
|
usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
|
|
@@ -32579,7 +33409,7 @@ async function jiraDailyCommand(options) {
|
|
|
32579
33409
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
32580
33410
|
const fromDate = options.from ?? today;
|
|
32581
33411
|
const toDate = options.to ?? fromDate;
|
|
32582
|
-
const statusMap = proj.config.jira?.statusMap;
|
|
33412
|
+
const statusMap = normalizeStatusMap(proj.config.jira?.statusMap);
|
|
32583
33413
|
const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
|
|
32584
33414
|
console.log(
|
|
32585
33415
|
chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
|
|
@@ -32696,7 +33526,7 @@ function createProgram() {
|
|
|
32696
33526
|
const program2 = new Command();
|
|
32697
33527
|
program2.name("marvin").description(
|
|
32698
33528
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
32699
|
-
).version("0.5.
|
|
33529
|
+
).version("0.5.18");
|
|
32700
33530
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
32701
33531
|
await initCommand();
|
|
32702
33532
|
});
|