mrvn-cli 0.5.17 → 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.js +870 -94
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +892 -116
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +870 -94
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -14594,6 +14594,14 @@ function calculateSprintCompletionPct(primaryDocs) {
|
|
|
14594
14594
|
|
|
14595
14595
|
// src/reports/sprint-summary/collector.ts
|
|
14596
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;
|
|
14597
14605
|
function collectSprintSummaryData(store, sprintId) {
|
|
14598
14606
|
const allDocs = store.list();
|
|
14599
14607
|
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -14679,12 +14687,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
14679
14687
|
for (const doc of workItemDocs) {
|
|
14680
14688
|
const about = doc.frontmatter.aboutArtifact;
|
|
14681
14689
|
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
14690
|
+
const complexity = doc.frontmatter.complexity;
|
|
14682
14691
|
const item = {
|
|
14683
14692
|
id: doc.frontmatter.id,
|
|
14684
14693
|
title: doc.frontmatter.title,
|
|
14685
14694
|
type: doc.frontmatter.type,
|
|
14686
14695
|
status: doc.frontmatter.status,
|
|
14687
14696
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
14697
|
+
weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
|
|
14688
14698
|
owner: doc.frontmatter.owner,
|
|
14689
14699
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
14690
14700
|
aboutArtifact: about,
|
|
@@ -22398,7 +22408,7 @@ function poBacklogPage(ctx) {
|
|
|
22398
22408
|
}
|
|
22399
22409
|
}
|
|
22400
22410
|
}
|
|
22401
|
-
const
|
|
22411
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
22402
22412
|
function featureTaskStats(featureId) {
|
|
22403
22413
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
22404
22414
|
let total = 0;
|
|
@@ -22407,7 +22417,7 @@ function poBacklogPage(ctx) {
|
|
|
22407
22417
|
for (const epic of fEpics) {
|
|
22408
22418
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
22409
22419
|
total++;
|
|
22410
|
-
if (
|
|
22420
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
22411
22421
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
22412
22422
|
}
|
|
22413
22423
|
}
|
|
@@ -22653,23 +22663,34 @@ function hashString(s) {
|
|
|
22653
22663
|
}
|
|
22654
22664
|
return Math.abs(h);
|
|
22655
22665
|
}
|
|
22666
|
+
var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
|
|
22667
|
+
var DEFAULT_WEIGHT2 = 3;
|
|
22656
22668
|
function countFocusStats(items) {
|
|
22657
22669
|
let total = 0;
|
|
22658
22670
|
let done = 0;
|
|
22659
22671
|
let inProgress = 0;
|
|
22660
|
-
|
|
22672
|
+
let totalWeight = 0;
|
|
22673
|
+
let weightedSum = 0;
|
|
22674
|
+
function walkStats(list) {
|
|
22661
22675
|
for (const w of list) {
|
|
22662
22676
|
if (w.type !== "contribution") {
|
|
22663
22677
|
total++;
|
|
22664
22678
|
const s = w.status.toLowerCase();
|
|
22665
|
-
if (s
|
|
22679
|
+
if (DONE_STATUS_SET.has(s)) done++;
|
|
22666
22680
|
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
22667
22681
|
}
|
|
22668
|
-
if (w.children)
|
|
22682
|
+
if (w.children) walkStats(w.children);
|
|
22669
22683
|
}
|
|
22670
22684
|
}
|
|
22671
|
-
|
|
22672
|
-
|
|
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 };
|
|
22673
22694
|
}
|
|
22674
22695
|
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
22675
22696
|
function ownerBadge2(owner) {
|
|
@@ -22718,7 +22739,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
22718
22739
|
for (const [focus, groupItems] of focusGroups) {
|
|
22719
22740
|
const color = focusColorMap.get(focus);
|
|
22720
22741
|
const stats = countFocusStats(groupItems);
|
|
22721
|
-
const pct = stats.
|
|
22742
|
+
const pct = stats.weightedProgress;
|
|
22722
22743
|
const summaryParts = [];
|
|
22723
22744
|
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
22724
22745
|
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
@@ -25431,6 +25452,47 @@ function extractJiraKeyFromTags(tags) {
|
|
|
25431
25452
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
25432
25453
|
return tag ? tag.slice(5) : void 0;
|
|
25433
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
|
+
}
|
|
25434
25496
|
function computeSubtaskProgress(subtasks) {
|
|
25435
25497
|
if (subtasks.length === 0) return 0;
|
|
25436
25498
|
const done = subtasks.filter(
|
|
@@ -25471,44 +25533,7 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
25471
25533
|
const resolved = statusMap ?? {};
|
|
25472
25534
|
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
|
|
25473
25535
|
const currentStatus = doc.frontmatter.status;
|
|
25474
|
-
const linkedIssues =
|
|
25475
|
-
if (issue2.fields.subtasks) {
|
|
25476
|
-
for (const sub of issue2.fields.subtasks) {
|
|
25477
|
-
linkedIssues.push({
|
|
25478
|
-
key: sub.key,
|
|
25479
|
-
summary: sub.fields.summary,
|
|
25480
|
-
status: sub.fields.status.name,
|
|
25481
|
-
relationship: "subtask",
|
|
25482
|
-
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25483
|
-
});
|
|
25484
|
-
}
|
|
25485
|
-
}
|
|
25486
|
-
if (issue2.fields.issuelinks) {
|
|
25487
|
-
for (const link of issue2.fields.issuelinks) {
|
|
25488
|
-
if (link.outwardIssue) {
|
|
25489
|
-
linkedIssues.push({
|
|
25490
|
-
key: link.outwardIssue.key,
|
|
25491
|
-
summary: link.outwardIssue.fields.summary,
|
|
25492
|
-
status: link.outwardIssue.fields.status.name,
|
|
25493
|
-
relationship: link.type.outward,
|
|
25494
|
-
isDone: DONE_STATUSES14.has(
|
|
25495
|
-
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25496
|
-
)
|
|
25497
|
-
});
|
|
25498
|
-
}
|
|
25499
|
-
if (link.inwardIssue) {
|
|
25500
|
-
linkedIssues.push({
|
|
25501
|
-
key: link.inwardIssue.key,
|
|
25502
|
-
summary: link.inwardIssue.fields.summary,
|
|
25503
|
-
status: link.inwardIssue.fields.status.name,
|
|
25504
|
-
relationship: link.type.inward,
|
|
25505
|
-
isDone: DONE_STATUSES14.has(
|
|
25506
|
-
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25507
|
-
)
|
|
25508
|
-
});
|
|
25509
|
-
}
|
|
25510
|
-
}
|
|
25511
|
-
}
|
|
25536
|
+
const linkedIssues = collectLinkedIssues(issue2);
|
|
25512
25537
|
const subtasks = issue2.fields.subtasks ?? [];
|
|
25513
25538
|
let proposedProgress;
|
|
25514
25539
|
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
@@ -25770,7 +25795,6 @@ function isWithinRange(timestamp, range) {
|
|
|
25770
25795
|
function isConfluenceUrl(url2) {
|
|
25771
25796
|
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
25772
25797
|
}
|
|
25773
|
-
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
25774
25798
|
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
25775
25799
|
const summary = {
|
|
25776
25800
|
dateRange,
|
|
@@ -25881,42 +25905,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
25881
25905
|
});
|
|
25882
25906
|
}
|
|
25883
25907
|
}
|
|
25884
|
-
const linkedIssues = [];
|
|
25885
|
-
if (issueWithLinks) {
|
|
25886
|
-
if (issueWithLinks.fields.subtasks) {
|
|
25887
|
-
for (const sub of issueWithLinks.fields.subtasks) {
|
|
25888
|
-
linkedIssues.push({
|
|
25889
|
-
key: sub.key,
|
|
25890
|
-
summary: sub.fields.summary,
|
|
25891
|
-
status: sub.fields.status.name,
|
|
25892
|
-
relationship: "subtask",
|
|
25893
|
-
isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
|
|
25894
|
-
});
|
|
25895
|
-
}
|
|
25896
|
-
}
|
|
25897
|
-
if (issueWithLinks.fields.issuelinks) {
|
|
25898
|
-
for (const link of issueWithLinks.fields.issuelinks) {
|
|
25899
|
-
if (link.outwardIssue) {
|
|
25900
|
-
linkedIssues.push({
|
|
25901
|
-
key: link.outwardIssue.key,
|
|
25902
|
-
summary: link.outwardIssue.fields.summary,
|
|
25903
|
-
status: link.outwardIssue.fields.status.name,
|
|
25904
|
-
relationship: link.type.outward,
|
|
25905
|
-
isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
25906
|
-
});
|
|
25907
|
-
}
|
|
25908
|
-
if (link.inwardIssue) {
|
|
25909
|
-
linkedIssues.push({
|
|
25910
|
-
key: link.inwardIssue.key,
|
|
25911
|
-
summary: link.inwardIssue.fields.summary,
|
|
25912
|
-
status: link.inwardIssue.fields.status.name,
|
|
25913
|
-
relationship: link.type.inward,
|
|
25914
|
-
isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
25915
|
-
});
|
|
25916
|
-
}
|
|
25917
|
-
}
|
|
25918
|
-
}
|
|
25919
|
-
}
|
|
25908
|
+
const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
|
|
25920
25909
|
const marvinArtifacts = [];
|
|
25921
25910
|
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
25922
25911
|
for (const doc of artifacts) {
|
|
@@ -26046,22 +26035,23 @@ function generateProposedActions(issues) {
|
|
|
26046
26035
|
|
|
26047
26036
|
// src/skills/builtin/jira/sprint-progress.ts
|
|
26048
26037
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
26049
|
-
var
|
|
26038
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
26050
26039
|
var BATCH_SIZE = 5;
|
|
26040
|
+
var MAX_LINKED_ISSUES = 50;
|
|
26051
26041
|
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
26052
|
-
var
|
|
26042
|
+
var COMPLEXITY_WEIGHTS2 = {
|
|
26053
26043
|
trivial: 1,
|
|
26054
26044
|
simple: 2,
|
|
26055
26045
|
moderate: 3,
|
|
26056
26046
|
complex: 5,
|
|
26057
26047
|
"very-complex": 8
|
|
26058
26048
|
};
|
|
26059
|
-
var
|
|
26049
|
+
var DEFAULT_WEIGHT3 = 3;
|
|
26060
26050
|
function resolveWeight(complexity) {
|
|
26061
|
-
if (complexity && complexity in
|
|
26062
|
-
return { weight:
|
|
26051
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
|
|
26052
|
+
return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
|
|
26063
26053
|
}
|
|
26064
|
-
return { weight:
|
|
26054
|
+
return { weight: DEFAULT_WEIGHT3, weightSource: "default" };
|
|
26065
26055
|
}
|
|
26066
26056
|
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
26067
26057
|
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
@@ -26150,6 +26140,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26150
26140
|
}
|
|
26151
26141
|
}
|
|
26152
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
|
+
}
|
|
26153
26189
|
const proposedUpdates = [];
|
|
26154
26190
|
const itemReports = [];
|
|
26155
26191
|
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
@@ -26215,6 +26251,23 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26215
26251
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
26216
26252
|
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
26217
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
|
+
}
|
|
26218
26271
|
const report = {
|
|
26219
26272
|
id: fm.id,
|
|
26220
26273
|
title: fm.title,
|
|
@@ -26233,6 +26286,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26233
26286
|
progressDrift,
|
|
26234
26287
|
commentSignals,
|
|
26235
26288
|
commentSummary: null,
|
|
26289
|
+
linkedIssues: itemLinkedIssues,
|
|
26290
|
+
linkedIssueSignals: itemLinkedIssueSignals,
|
|
26236
26291
|
children: [],
|
|
26237
26292
|
owner: fm.owner ?? null,
|
|
26238
26293
|
focusArea: focusTag ? focusTag.slice(6) : null
|
|
@@ -26276,7 +26331,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26276
26331
|
const focusAreas = [];
|
|
26277
26332
|
for (const [name, items] of focusAreaMap) {
|
|
26278
26333
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
26279
|
-
const doneCount = allFlatItems.filter((i) =>
|
|
26334
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES15.has(i.marvinStatus)).length;
|
|
26280
26335
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
26281
26336
|
const progress = computeWeightedProgress(items);
|
|
26282
26337
|
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
@@ -26328,6 +26383,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26328
26383
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26329
26384
|
}
|
|
26330
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
|
+
}
|
|
26331
26406
|
}
|
|
26332
26407
|
const appliedUpdates = [];
|
|
26333
26408
|
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
@@ -26444,6 +26519,155 @@ ${commentTexts}`);
|
|
|
26444
26519
|
}
|
|
26445
26520
|
return summaries;
|
|
26446
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
|
+
}
|
|
26447
26671
|
function formatProgressReport(report) {
|
|
26448
26672
|
const parts = [];
|
|
26449
26673
|
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
@@ -26527,7 +26751,7 @@ function formatProgressReport(report) {
|
|
|
26527
26751
|
}
|
|
26528
26752
|
function formatItemLine(parts, item, depth) {
|
|
26529
26753
|
const indent = " ".repeat(depth + 1);
|
|
26530
|
-
const statusIcon =
|
|
26754
|
+
const statusIcon = DONE_STATUSES15.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
26531
26755
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
26532
26756
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
26533
26757
|
const progressLabel = ` ${item.progress}%`;
|
|
@@ -26537,6 +26761,19 @@ function formatItemLine(parts, item, depth) {
|
|
|
26537
26761
|
if (item.commentSummary) {
|
|
26538
26762
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
26539
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
|
+
}
|
|
26540
26777
|
for (const child of item.children) {
|
|
26541
26778
|
formatItemLine(parts, child, depth + 1);
|
|
26542
26779
|
}
|
|
@@ -26546,6 +26783,506 @@ function progressBar6(pct) {
|
|
|
26546
26783
|
const empty = 10 - filled;
|
|
26547
26784
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
26548
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
|
+
}
|
|
26549
27286
|
|
|
26550
27287
|
// src/skills/builtin/jira/tools.ts
|
|
26551
27288
|
var JIRA_TYPE = "jira-issue";
|
|
@@ -27332,7 +28069,8 @@ function createJiraTools(store, projectConfig) {
|
|
|
27332
28069
|
{
|
|
27333
28070
|
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
27334
28071
|
analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
|
|
27335
|
-
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)")
|
|
27336
28074
|
},
|
|
27337
28075
|
async (args) => {
|
|
27338
28076
|
const jira = createJiraClient(jiraUserConfig);
|
|
@@ -27345,6 +28083,7 @@ function createJiraTools(store, projectConfig) {
|
|
|
27345
28083
|
sprintId: args.sprintId,
|
|
27346
28084
|
analyzeComments: args.analyzeComments ?? false,
|
|
27347
28085
|
applyUpdates: args.applyUpdates ?? false,
|
|
28086
|
+
traverseLinks: args.traverseLinks ?? false,
|
|
27348
28087
|
statusMap
|
|
27349
28088
|
}
|
|
27350
28089
|
);
|
|
@@ -27354,6 +28093,34 @@ function createJiraTools(store, projectConfig) {
|
|
|
27354
28093
|
};
|
|
27355
28094
|
},
|
|
27356
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 } }
|
|
27357
28124
|
)
|
|
27358
28125
|
];
|
|
27359
28126
|
}
|
|
@@ -27455,7 +28222,8 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
27455
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.
|
|
27456
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.
|
|
27457
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).
|
|
27458
|
-
- \`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.
|
|
27459
28227
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
27460
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.
|
|
27461
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).
|
|
@@ -27472,6 +28240,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
27472
28240
|
2. Review focus area rollups, status drift, and blockers
|
|
27473
28241
|
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
27474
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
|
+
|
|
27475
28248
|
**Daily review workflow:**
|
|
27476
28249
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
27477
28250
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -27496,6 +28269,7 @@ ${COMMON_WORKFLOW}
|
|
|
27496
28269
|
**As Product Owner, use Jira integration to:**
|
|
27497
28270
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
27498
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
|
|
27499
28273
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
27500
28274
|
- Push approved features as Stories for development tracking
|
|
27501
28275
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -27509,6 +28283,7 @@ ${COMMON_WORKFLOW}
|
|
|
27509
28283
|
**As Tech Lead, use Jira integration to:**
|
|
27510
28284
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
27511
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
|
|
27512
28287
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
27513
28288
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
27514
28289
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -27524,6 +28299,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
27524
28299
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
27525
28300
|
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
27526
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
|
|
27527
28303
|
- Pull sprint issues for tracking progress and blockers
|
|
27528
28304
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
27529
28305
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -32750,7 +33526,7 @@ function createProgram() {
|
|
|
32750
33526
|
const program2 = new Command();
|
|
32751
33527
|
program2.name("marvin").description(
|
|
32752
33528
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
32753
|
-
).version("0.5.
|
|
33529
|
+
).version("0.5.18");
|
|
32754
33530
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
32755
33531
|
await initCommand();
|
|
32756
33532
|
});
|