mrvn-cli 0.5.17 → 0.5.19
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 +897 -96
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +919 -118
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +897 -96
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14742,7 +14742,7 @@ function createActionTools(store) {
|
|
|
14742
14742
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
14743
14743
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
|
|
14744
14744
|
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
14745
|
-
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100).")
|
|
14745
|
+
progress: external_exports.number().nullable().optional().describe("Explicit progress percentage (0-100). Pass null to clear the override and revert to auto-calculation from children.")
|
|
14746
14746
|
},
|
|
14747
14747
|
async (args) => {
|
|
14748
14748
|
const { id, content, sprints, tags, workFocus, progress, owner, assignee, ...updates } = args;
|
|
@@ -14785,6 +14785,8 @@ function createActionTools(store) {
|
|
|
14785
14785
|
if (typeof progress === "number") {
|
|
14786
14786
|
updates.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
14787
14787
|
updates.progressOverride = true;
|
|
14788
|
+
} else if (progress === null) {
|
|
14789
|
+
updates.progressOverride = false;
|
|
14788
14790
|
}
|
|
14789
14791
|
const doc = store.update(id, updates, content);
|
|
14790
14792
|
if (args.status !== void 0 || typeof progress === "number") {
|
|
@@ -15699,6 +15701,14 @@ function evaluateHealth(projectName, metrics) {
|
|
|
15699
15701
|
|
|
15700
15702
|
// src/reports/sprint-summary/collector.ts
|
|
15701
15703
|
var DONE_STATUSES3 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15704
|
+
var COMPLEXITY_WEIGHTS = {
|
|
15705
|
+
trivial: 1,
|
|
15706
|
+
simple: 2,
|
|
15707
|
+
moderate: 3,
|
|
15708
|
+
complex: 5,
|
|
15709
|
+
"very-complex": 8
|
|
15710
|
+
};
|
|
15711
|
+
var DEFAULT_WEIGHT = 3;
|
|
15702
15712
|
function collectSprintSummaryData(store, sprintId) {
|
|
15703
15713
|
const allDocs = store.list();
|
|
15704
15714
|
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -15784,12 +15794,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15784
15794
|
for (const doc of workItemDocs) {
|
|
15785
15795
|
const about = doc.frontmatter.aboutArtifact;
|
|
15786
15796
|
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
15797
|
+
const complexity = doc.frontmatter.complexity;
|
|
15787
15798
|
const item = {
|
|
15788
15799
|
id: doc.frontmatter.id,
|
|
15789
15800
|
title: doc.frontmatter.title,
|
|
15790
15801
|
type: doc.frontmatter.type,
|
|
15791
15802
|
status: doc.frontmatter.status,
|
|
15792
15803
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15804
|
+
weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
|
|
15793
15805
|
owner: doc.frontmatter.owner,
|
|
15794
15806
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15795
15807
|
aboutArtifact: about,
|
|
@@ -19457,7 +19469,7 @@ function poBacklogPage(ctx) {
|
|
|
19457
19469
|
}
|
|
19458
19470
|
}
|
|
19459
19471
|
}
|
|
19460
|
-
const
|
|
19472
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19461
19473
|
function featureTaskStats(featureId) {
|
|
19462
19474
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
19463
19475
|
let total = 0;
|
|
@@ -19466,7 +19478,7 @@ function poBacklogPage(ctx) {
|
|
|
19466
19478
|
for (const epic of fEpics) {
|
|
19467
19479
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
19468
19480
|
total++;
|
|
19469
|
-
if (
|
|
19481
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
19470
19482
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
19471
19483
|
}
|
|
19472
19484
|
}
|
|
@@ -19712,23 +19724,34 @@ function hashString(s) {
|
|
|
19712
19724
|
}
|
|
19713
19725
|
return Math.abs(h);
|
|
19714
19726
|
}
|
|
19727
|
+
var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
|
|
19728
|
+
var DEFAULT_WEIGHT2 = 3;
|
|
19715
19729
|
function countFocusStats(items) {
|
|
19716
19730
|
let total = 0;
|
|
19717
19731
|
let done = 0;
|
|
19718
19732
|
let inProgress = 0;
|
|
19719
|
-
|
|
19733
|
+
let totalWeight = 0;
|
|
19734
|
+
let weightedSum = 0;
|
|
19735
|
+
function walkStats(list) {
|
|
19720
19736
|
for (const w of list) {
|
|
19721
19737
|
if (w.type !== "contribution") {
|
|
19722
19738
|
total++;
|
|
19723
19739
|
const s = w.status.toLowerCase();
|
|
19724
|
-
if (s
|
|
19740
|
+
if (DONE_STATUS_SET.has(s)) done++;
|
|
19725
19741
|
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
19726
19742
|
}
|
|
19727
|
-
if (w.children)
|
|
19743
|
+
if (w.children) walkStats(w.children);
|
|
19728
19744
|
}
|
|
19729
19745
|
}
|
|
19730
|
-
|
|
19731
|
-
|
|
19746
|
+
walkStats(items);
|
|
19747
|
+
for (const w of items) {
|
|
19748
|
+
if (w.type === "contribution") continue;
|
|
19749
|
+
const weight = w.weight ?? DEFAULT_WEIGHT2;
|
|
19750
|
+
const progress = w.progress ?? (DONE_STATUS_SET.has(w.status.toLowerCase()) ? 100 : 0);
|
|
19751
|
+
totalWeight += weight;
|
|
19752
|
+
weightedSum += weight * progress;
|
|
19753
|
+
}
|
|
19754
|
+
return { total, done, inProgress, weightedProgress: totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 };
|
|
19732
19755
|
}
|
|
19733
19756
|
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
19734
19757
|
function ownerBadge2(owner) {
|
|
@@ -19777,7 +19800,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
19777
19800
|
for (const [focus, groupItems] of focusGroups) {
|
|
19778
19801
|
const color = focusColorMap.get(focus);
|
|
19779
19802
|
const stats = countFocusStats(groupItems);
|
|
19780
|
-
const pct = stats.
|
|
19803
|
+
const pct = stats.weightedProgress;
|
|
19781
19804
|
const summaryParts = [];
|
|
19782
19805
|
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
19783
19806
|
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
@@ -23769,7 +23792,7 @@ function createTaskTools(store) {
|
|
|
23769
23792
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
23770
23793
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
|
|
23771
23794
|
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
23772
|
-
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions.")
|
|
23795
|
+
progress: external_exports.number().nullable().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions. Pass null to clear the override and revert to auto-calculation.")
|
|
23773
23796
|
},
|
|
23774
23797
|
async (args) => {
|
|
23775
23798
|
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workFocus, progress, ...updates } = args;
|
|
@@ -23802,6 +23825,8 @@ function createTaskTools(store) {
|
|
|
23802
23825
|
if (typeof progress === "number") {
|
|
23803
23826
|
updates.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
23804
23827
|
updates.progressOverride = true;
|
|
23828
|
+
} else if (progress === null) {
|
|
23829
|
+
updates.progressOverride = false;
|
|
23805
23830
|
}
|
|
23806
23831
|
const doc = store.update(id, updates, content);
|
|
23807
23832
|
if (args.status !== void 0 || typeof progress === "number") {
|
|
@@ -25185,6 +25210,47 @@ function extractJiraKeyFromTags(tags) {
|
|
|
25185
25210
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
25186
25211
|
return tag ? tag.slice(5) : void 0;
|
|
25187
25212
|
}
|
|
25213
|
+
function collectLinkedIssues(issue2) {
|
|
25214
|
+
const linkedIssues = [];
|
|
25215
|
+
if (issue2.fields.subtasks) {
|
|
25216
|
+
for (const sub of issue2.fields.subtasks) {
|
|
25217
|
+
linkedIssues.push({
|
|
25218
|
+
key: sub.key,
|
|
25219
|
+
summary: sub.fields.summary,
|
|
25220
|
+
status: sub.fields.status.name,
|
|
25221
|
+
relationship: "subtask",
|
|
25222
|
+
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25223
|
+
});
|
|
25224
|
+
}
|
|
25225
|
+
}
|
|
25226
|
+
if (issue2.fields.issuelinks) {
|
|
25227
|
+
for (const link of issue2.fields.issuelinks) {
|
|
25228
|
+
if (link.outwardIssue) {
|
|
25229
|
+
linkedIssues.push({
|
|
25230
|
+
key: link.outwardIssue.key,
|
|
25231
|
+
summary: link.outwardIssue.fields.summary,
|
|
25232
|
+
status: link.outwardIssue.fields.status.name,
|
|
25233
|
+
relationship: link.type.outward,
|
|
25234
|
+
isDone: DONE_STATUSES14.has(
|
|
25235
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25236
|
+
)
|
|
25237
|
+
});
|
|
25238
|
+
}
|
|
25239
|
+
if (link.inwardIssue) {
|
|
25240
|
+
linkedIssues.push({
|
|
25241
|
+
key: link.inwardIssue.key,
|
|
25242
|
+
summary: link.inwardIssue.fields.summary,
|
|
25243
|
+
status: link.inwardIssue.fields.status.name,
|
|
25244
|
+
relationship: link.type.inward,
|
|
25245
|
+
isDone: DONE_STATUSES14.has(
|
|
25246
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25247
|
+
)
|
|
25248
|
+
});
|
|
25249
|
+
}
|
|
25250
|
+
}
|
|
25251
|
+
}
|
|
25252
|
+
return linkedIssues;
|
|
25253
|
+
}
|
|
25188
25254
|
function computeSubtaskProgress(subtasks) {
|
|
25189
25255
|
if (subtasks.length === 0) return 0;
|
|
25190
25256
|
const done = subtasks.filter(
|
|
@@ -25225,44 +25291,7 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
25225
25291
|
const resolved = statusMap ?? {};
|
|
25226
25292
|
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
|
|
25227
25293
|
const currentStatus = doc.frontmatter.status;
|
|
25228
|
-
const linkedIssues =
|
|
25229
|
-
if (issue2.fields.subtasks) {
|
|
25230
|
-
for (const sub of issue2.fields.subtasks) {
|
|
25231
|
-
linkedIssues.push({
|
|
25232
|
-
key: sub.key,
|
|
25233
|
-
summary: sub.fields.summary,
|
|
25234
|
-
status: sub.fields.status.name,
|
|
25235
|
-
relationship: "subtask",
|
|
25236
|
-
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25237
|
-
});
|
|
25238
|
-
}
|
|
25239
|
-
}
|
|
25240
|
-
if (issue2.fields.issuelinks) {
|
|
25241
|
-
for (const link of issue2.fields.issuelinks) {
|
|
25242
|
-
if (link.outwardIssue) {
|
|
25243
|
-
linkedIssues.push({
|
|
25244
|
-
key: link.outwardIssue.key,
|
|
25245
|
-
summary: link.outwardIssue.fields.summary,
|
|
25246
|
-
status: link.outwardIssue.fields.status.name,
|
|
25247
|
-
relationship: link.type.outward,
|
|
25248
|
-
isDone: DONE_STATUSES14.has(
|
|
25249
|
-
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25250
|
-
)
|
|
25251
|
-
});
|
|
25252
|
-
}
|
|
25253
|
-
if (link.inwardIssue) {
|
|
25254
|
-
linkedIssues.push({
|
|
25255
|
-
key: link.inwardIssue.key,
|
|
25256
|
-
summary: link.inwardIssue.fields.summary,
|
|
25257
|
-
status: link.inwardIssue.fields.status.name,
|
|
25258
|
-
relationship: link.type.inward,
|
|
25259
|
-
isDone: DONE_STATUSES14.has(
|
|
25260
|
-
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25261
|
-
)
|
|
25262
|
-
});
|
|
25263
|
-
}
|
|
25264
|
-
}
|
|
25265
|
-
}
|
|
25294
|
+
const linkedIssues = collectLinkedIssues(issue2);
|
|
25266
25295
|
const subtasks = issue2.fields.subtasks ?? [];
|
|
25267
25296
|
let proposedProgress;
|
|
25268
25297
|
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
@@ -25524,7 +25553,6 @@ function isWithinRange(timestamp, range) {
|
|
|
25524
25553
|
function isConfluenceUrl(url2) {
|
|
25525
25554
|
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
25526
25555
|
}
|
|
25527
|
-
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
25528
25556
|
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
25529
25557
|
const summary = {
|
|
25530
25558
|
dateRange,
|
|
@@ -25635,42 +25663,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
25635
25663
|
});
|
|
25636
25664
|
}
|
|
25637
25665
|
}
|
|
25638
|
-
const linkedIssues = [];
|
|
25639
|
-
if (issueWithLinks) {
|
|
25640
|
-
if (issueWithLinks.fields.subtasks) {
|
|
25641
|
-
for (const sub of issueWithLinks.fields.subtasks) {
|
|
25642
|
-
linkedIssues.push({
|
|
25643
|
-
key: sub.key,
|
|
25644
|
-
summary: sub.fields.summary,
|
|
25645
|
-
status: sub.fields.status.name,
|
|
25646
|
-
relationship: "subtask",
|
|
25647
|
-
isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
|
|
25648
|
-
});
|
|
25649
|
-
}
|
|
25650
|
-
}
|
|
25651
|
-
if (issueWithLinks.fields.issuelinks) {
|
|
25652
|
-
for (const link of issueWithLinks.fields.issuelinks) {
|
|
25653
|
-
if (link.outwardIssue) {
|
|
25654
|
-
linkedIssues.push({
|
|
25655
|
-
key: link.outwardIssue.key,
|
|
25656
|
-
summary: link.outwardIssue.fields.summary,
|
|
25657
|
-
status: link.outwardIssue.fields.status.name,
|
|
25658
|
-
relationship: link.type.outward,
|
|
25659
|
-
isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
25660
|
-
});
|
|
25661
|
-
}
|
|
25662
|
-
if (link.inwardIssue) {
|
|
25663
|
-
linkedIssues.push({
|
|
25664
|
-
key: link.inwardIssue.key,
|
|
25665
|
-
summary: link.inwardIssue.fields.summary,
|
|
25666
|
-
status: link.inwardIssue.fields.status.name,
|
|
25667
|
-
relationship: link.type.inward,
|
|
25668
|
-
isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
25669
|
-
});
|
|
25670
|
-
}
|
|
25671
|
-
}
|
|
25672
|
-
}
|
|
25673
|
-
}
|
|
25666
|
+
const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
|
|
25674
25667
|
const marvinArtifacts = [];
|
|
25675
25668
|
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
25676
25669
|
for (const doc of artifacts) {
|
|
@@ -25800,22 +25793,23 @@ function generateProposedActions(issues) {
|
|
|
25800
25793
|
|
|
25801
25794
|
// src/skills/builtin/jira/sprint-progress.ts
|
|
25802
25795
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
25803
|
-
var
|
|
25796
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
25804
25797
|
var BATCH_SIZE = 5;
|
|
25798
|
+
var MAX_LINKED_ISSUES = 50;
|
|
25805
25799
|
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
25806
|
-
var
|
|
25800
|
+
var COMPLEXITY_WEIGHTS2 = {
|
|
25807
25801
|
trivial: 1,
|
|
25808
25802
|
simple: 2,
|
|
25809
25803
|
moderate: 3,
|
|
25810
25804
|
complex: 5,
|
|
25811
25805
|
"very-complex": 8
|
|
25812
25806
|
};
|
|
25813
|
-
var
|
|
25807
|
+
var DEFAULT_WEIGHT3 = 3;
|
|
25814
25808
|
function resolveWeight(complexity) {
|
|
25815
|
-
if (complexity && complexity in
|
|
25816
|
-
return { weight:
|
|
25809
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
|
|
25810
|
+
return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
|
|
25817
25811
|
}
|
|
25818
|
-
return { weight:
|
|
25812
|
+
return { weight: DEFAULT_WEIGHT3, weightSource: "default" };
|
|
25819
25813
|
}
|
|
25820
25814
|
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
25821
25815
|
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
@@ -25904,6 +25898,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25904
25898
|
}
|
|
25905
25899
|
}
|
|
25906
25900
|
}
|
|
25901
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
25902
|
+
if (options.traverseLinks) {
|
|
25903
|
+
const visited = new Set(jiraIssues.keys());
|
|
25904
|
+
const queue = [];
|
|
25905
|
+
for (const [, data] of jiraIssues) {
|
|
25906
|
+
const links = collectLinkedIssues(data.issue);
|
|
25907
|
+
for (const link of links) {
|
|
25908
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
25909
|
+
visited.add(link.key);
|
|
25910
|
+
queue.push(link.key);
|
|
25911
|
+
}
|
|
25912
|
+
}
|
|
25913
|
+
}
|
|
25914
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
25915
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
25916
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
25917
|
+
const results = await Promise.allSettled(
|
|
25918
|
+
batch.map(async (key) => {
|
|
25919
|
+
const [issue2, comments] = await Promise.all([
|
|
25920
|
+
client.getIssueWithLinks(key),
|
|
25921
|
+
client.getComments(key)
|
|
25922
|
+
]);
|
|
25923
|
+
return { key, issue: issue2, comments };
|
|
25924
|
+
})
|
|
25925
|
+
);
|
|
25926
|
+
for (const result of results) {
|
|
25927
|
+
if (result.status === "fulfilled") {
|
|
25928
|
+
const { key, issue: issue2, comments } = result.value;
|
|
25929
|
+
linkedJiraIssues.set(key, { issue: issue2, comments });
|
|
25930
|
+
const newLinks = collectLinkedIssues(issue2);
|
|
25931
|
+
for (const link of newLinks) {
|
|
25932
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
25933
|
+
visited.add(link.key);
|
|
25934
|
+
queue.push(link.key);
|
|
25935
|
+
}
|
|
25936
|
+
}
|
|
25937
|
+
} else {
|
|
25938
|
+
const batchKey = batch[results.indexOf(result)];
|
|
25939
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
25940
|
+
}
|
|
25941
|
+
}
|
|
25942
|
+
}
|
|
25943
|
+
if (queue.length > 0) {
|
|
25944
|
+
errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
|
|
25945
|
+
}
|
|
25946
|
+
}
|
|
25907
25947
|
const proposedUpdates = [];
|
|
25908
25948
|
const itemReports = [];
|
|
25909
25949
|
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
@@ -25969,6 +26009,23 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25969
26009
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
25970
26010
|
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
25971
26011
|
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
26012
|
+
let itemLinkedIssues = [];
|
|
26013
|
+
const itemLinkedIssueSignals = [];
|
|
26014
|
+
if (options.traverseLinks && jiraData) {
|
|
26015
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
26016
|
+
jiraData.issue,
|
|
26017
|
+
jiraIssues,
|
|
26018
|
+
linkedJiraIssues
|
|
26019
|
+
);
|
|
26020
|
+
itemLinkedIssues = allLinks;
|
|
26021
|
+
itemLinkedIssueSignals.push(...allSignals);
|
|
26022
|
+
analyzeLinkedIssueSignals(
|
|
26023
|
+
allLinks,
|
|
26024
|
+
fm,
|
|
26025
|
+
jiraKey,
|
|
26026
|
+
proposedUpdates
|
|
26027
|
+
);
|
|
26028
|
+
}
|
|
25972
26029
|
const report = {
|
|
25973
26030
|
id: fm.id,
|
|
25974
26031
|
title: fm.title,
|
|
@@ -25987,6 +26044,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25987
26044
|
progressDrift,
|
|
25988
26045
|
commentSignals,
|
|
25989
26046
|
commentSummary: null,
|
|
26047
|
+
linkedIssues: itemLinkedIssues,
|
|
26048
|
+
linkedIssueSignals: itemLinkedIssueSignals,
|
|
25990
26049
|
children: [],
|
|
25991
26050
|
owner: fm.owner ?? null,
|
|
25992
26051
|
focusArea: focusTag ? focusTag.slice(6) : null
|
|
@@ -26030,7 +26089,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26030
26089
|
const focusAreas = [];
|
|
26031
26090
|
for (const [name, items] of focusAreaMap) {
|
|
26032
26091
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
26033
|
-
const doneCount = allFlatItems.filter((i) =>
|
|
26092
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES15.has(i.marvinStatus)).length;
|
|
26034
26093
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
26035
26094
|
const progress = computeWeightedProgress(items);
|
|
26036
26095
|
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
@@ -26082,6 +26141,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26082
26141
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26083
26142
|
}
|
|
26084
26143
|
}
|
|
26144
|
+
if (options.traverseLinks) {
|
|
26145
|
+
try {
|
|
26146
|
+
const linkedSummaries = await analyzeLinkedIssueComments(
|
|
26147
|
+
itemReports,
|
|
26148
|
+
linkedJiraIssues
|
|
26149
|
+
);
|
|
26150
|
+
for (const [artifactId, signalSummaries] of linkedSummaries) {
|
|
26151
|
+
const report = itemReports.find((r) => r.id === artifactId);
|
|
26152
|
+
if (!report) continue;
|
|
26153
|
+
for (const [sourceKey, summary] of signalSummaries) {
|
|
26154
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === sourceKey);
|
|
26155
|
+
if (signal) {
|
|
26156
|
+
signal.commentSummary = summary;
|
|
26157
|
+
}
|
|
26158
|
+
}
|
|
26159
|
+
}
|
|
26160
|
+
} catch (err) {
|
|
26161
|
+
errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26162
|
+
}
|
|
26163
|
+
}
|
|
26085
26164
|
}
|
|
26086
26165
|
const appliedUpdates = [];
|
|
26087
26166
|
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
@@ -26198,6 +26277,155 @@ ${commentTexts}`);
|
|
|
26198
26277
|
}
|
|
26199
26278
|
return summaries;
|
|
26200
26279
|
}
|
|
26280
|
+
function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
|
|
26281
|
+
const allLinks = [];
|
|
26282
|
+
const allSignals = [];
|
|
26283
|
+
const visited = /* @__PURE__ */ new Set([primaryIssue.key]);
|
|
26284
|
+
const directLinks = collectLinkedIssues(primaryIssue).filter((l) => l.relationship !== "subtask");
|
|
26285
|
+
const queue = [...directLinks];
|
|
26286
|
+
for (const link of directLinks) {
|
|
26287
|
+
visited.add(link.key);
|
|
26288
|
+
}
|
|
26289
|
+
while (queue.length > 0) {
|
|
26290
|
+
const link = queue.shift();
|
|
26291
|
+
allLinks.push(link);
|
|
26292
|
+
const linkedData = linkedJiraIssues.get(link.key) ?? primaryIssues.get(link.key);
|
|
26293
|
+
if (!linkedData) continue;
|
|
26294
|
+
const linkedCommentSignals = [];
|
|
26295
|
+
for (const comment of linkedData.comments) {
|
|
26296
|
+
const text = extractCommentText(comment.body);
|
|
26297
|
+
const signals = detectCommentSignals(text);
|
|
26298
|
+
linkedCommentSignals.push(...signals);
|
|
26299
|
+
}
|
|
26300
|
+
if (linkedCommentSignals.length > 0 || linkedData.comments.length > 0) {
|
|
26301
|
+
allSignals.push({
|
|
26302
|
+
sourceKey: link.key,
|
|
26303
|
+
linkType: link.relationship,
|
|
26304
|
+
commentSignals: linkedCommentSignals,
|
|
26305
|
+
commentSummary: null
|
|
26306
|
+
});
|
|
26307
|
+
}
|
|
26308
|
+
const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
|
|
26309
|
+
for (const next of nextLinks) {
|
|
26310
|
+
visited.add(next.key);
|
|
26311
|
+
queue.push(next);
|
|
26312
|
+
}
|
|
26313
|
+
}
|
|
26314
|
+
return { allLinks, allSignals };
|
|
26315
|
+
}
|
|
26316
|
+
var BLOCKER_LINK_PATTERNS = ["blocks", "is blocked by"];
|
|
26317
|
+
var WONT_DO_STATUSES = /* @__PURE__ */ new Set(["wont do", "won't do", "cancelled"]);
|
|
26318
|
+
function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedUpdates) {
|
|
26319
|
+
if (linkedIssues.length === 0) return;
|
|
26320
|
+
const blockerLinks = linkedIssues.filter(
|
|
26321
|
+
(l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
|
|
26322
|
+
);
|
|
26323
|
+
if (blockerLinks.length > 0 && blockerLinks.every((l) => l.isDone) && frontmatter.status === "blocked") {
|
|
26324
|
+
proposedUpdates.push({
|
|
26325
|
+
artifactId: frontmatter.id,
|
|
26326
|
+
field: "status",
|
|
26327
|
+
currentValue: "blocked",
|
|
26328
|
+
proposedValue: "in-progress",
|
|
26329
|
+
reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
|
|
26330
|
+
});
|
|
26331
|
+
}
|
|
26332
|
+
const wontDoLinks = linkedIssues.filter(
|
|
26333
|
+
(l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
|
|
26334
|
+
);
|
|
26335
|
+
if (wontDoLinks.length > 0) {
|
|
26336
|
+
proposedUpdates.push({
|
|
26337
|
+
artifactId: frontmatter.id,
|
|
26338
|
+
field: "review",
|
|
26339
|
+
currentValue: null,
|
|
26340
|
+
proposedValue: "needs-review",
|
|
26341
|
+
reason: `Linked issue(s) cancelled/won't do: ${wontDoLinks.map((l) => `${l.key} "${l.summary}"`).join(", ")}`
|
|
26342
|
+
});
|
|
26343
|
+
}
|
|
26344
|
+
}
|
|
26345
|
+
var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
|
|
26346
|
+
|
|
26347
|
+
For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
|
|
26348
|
+
|
|
26349
|
+
Return your response as a JSON object mapping artifact IDs to objects mapping linked issue keys to summary strings.
|
|
26350
|
+
Example: {"T-001": {"PROJ-301": "DBA review scheduled for Thursday."}}
|
|
26351
|
+
|
|
26352
|
+
IMPORTANT: Only return the JSON object, no other text.`;
|
|
26353
|
+
async function analyzeLinkedIssueComments(items, linkedJiraIssues) {
|
|
26354
|
+
const results = /* @__PURE__ */ new Map();
|
|
26355
|
+
const promptParts = [];
|
|
26356
|
+
const itemsWithLinkedComments = [];
|
|
26357
|
+
for (const item of items) {
|
|
26358
|
+
if (item.linkedIssueSignals.length === 0) continue;
|
|
26359
|
+
const linkedParts = [];
|
|
26360
|
+
for (const signal of item.linkedIssueSignals) {
|
|
26361
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26362
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
26363
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
26364
|
+
const text = extractCommentText(c.body);
|
|
26365
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
26366
|
+
}).join("\n");
|
|
26367
|
+
linkedParts.push(` ### ${signal.sourceKey} (${signal.linkType})
|
|
26368
|
+
${commentTexts}`);
|
|
26369
|
+
}
|
|
26370
|
+
if (linkedParts.length > 0) {
|
|
26371
|
+
itemsWithLinkedComments.push(item);
|
|
26372
|
+
promptParts.push(`## ${item.id} \u2014 ${item.title}
|
|
26373
|
+
Linked issues:
|
|
26374
|
+
${linkedParts.join("\n")}`);
|
|
26375
|
+
}
|
|
26376
|
+
}
|
|
26377
|
+
if (promptParts.length === 0) return results;
|
|
26378
|
+
const prompt = promptParts.join("\n\n");
|
|
26379
|
+
const llmResult = query3({
|
|
26380
|
+
prompt,
|
|
26381
|
+
options: {
|
|
26382
|
+
systemPrompt: LINKED_COMMENT_ANALYSIS_PROMPT,
|
|
26383
|
+
maxTurns: 1,
|
|
26384
|
+
tools: [],
|
|
26385
|
+
allowedTools: []
|
|
26386
|
+
}
|
|
26387
|
+
});
|
|
26388
|
+
for await (const msg of llmResult) {
|
|
26389
|
+
if (msg.type === "assistant") {
|
|
26390
|
+
const textBlock = msg.message.content.find(
|
|
26391
|
+
(b) => b.type === "text"
|
|
26392
|
+
);
|
|
26393
|
+
if (textBlock) {
|
|
26394
|
+
const parsed = parseLlmJson(textBlock.text);
|
|
26395
|
+
if (parsed) {
|
|
26396
|
+
for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
|
|
26397
|
+
if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
|
|
26398
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
26399
|
+
for (const [key, summary] of Object.entries(linkedSummaries)) {
|
|
26400
|
+
if (typeof summary === "string") {
|
|
26401
|
+
signalMap.set(key, summary);
|
|
26402
|
+
}
|
|
26403
|
+
}
|
|
26404
|
+
if (signalMap.size > 0) {
|
|
26405
|
+
results.set(artifactId, signalMap);
|
|
26406
|
+
}
|
|
26407
|
+
}
|
|
26408
|
+
}
|
|
26409
|
+
}
|
|
26410
|
+
}
|
|
26411
|
+
}
|
|
26412
|
+
}
|
|
26413
|
+
return results;
|
|
26414
|
+
}
|
|
26415
|
+
function parseLlmJson(text) {
|
|
26416
|
+
try {
|
|
26417
|
+
return JSON.parse(text);
|
|
26418
|
+
} catch {
|
|
26419
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
26420
|
+
if (match) {
|
|
26421
|
+
try {
|
|
26422
|
+
return JSON.parse(match[1]);
|
|
26423
|
+
} catch {
|
|
26424
|
+
}
|
|
26425
|
+
}
|
|
26426
|
+
return null;
|
|
26427
|
+
}
|
|
26428
|
+
}
|
|
26201
26429
|
function formatProgressReport(report) {
|
|
26202
26430
|
const parts = [];
|
|
26203
26431
|
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
@@ -26281,7 +26509,7 @@ function formatProgressReport(report) {
|
|
|
26281
26509
|
}
|
|
26282
26510
|
function formatItemLine(parts, item, depth) {
|
|
26283
26511
|
const indent = " ".repeat(depth + 1);
|
|
26284
|
-
const statusIcon =
|
|
26512
|
+
const statusIcon = DONE_STATUSES15.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
26285
26513
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
26286
26514
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
26287
26515
|
const progressLabel = ` ${item.progress}%`;
|
|
@@ -26291,6 +26519,19 @@ function formatItemLine(parts, item, depth) {
|
|
|
26291
26519
|
if (item.commentSummary) {
|
|
26292
26520
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
26293
26521
|
}
|
|
26522
|
+
if (item.linkedIssues.length > 0) {
|
|
26523
|
+
parts.push(`${indent} \u{1F517} Linked Issues:`);
|
|
26524
|
+
for (const link of item.linkedIssues) {
|
|
26525
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
26526
|
+
const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
|
|
26527
|
+
const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
|
|
26528
|
+
parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
|
|
26529
|
+
const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
26530
|
+
if (signal?.commentSummary) {
|
|
26531
|
+
parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
|
|
26532
|
+
}
|
|
26533
|
+
}
|
|
26534
|
+
}
|
|
26294
26535
|
for (const child of item.children) {
|
|
26295
26536
|
formatItemLine(parts, child, depth + 1);
|
|
26296
26537
|
}
|
|
@@ -26300,6 +26541,527 @@ function progressBar6(pct) {
|
|
|
26300
26541
|
const empty = 10 - filled;
|
|
26301
26542
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
26302
26543
|
}
|
|
26544
|
+
var MAX_ARTIFACT_NODES = 50;
|
|
26545
|
+
var MAX_LLM_DEPTH = 3;
|
|
26546
|
+
var MAX_LLM_COMMENT_CHARS = 8e3;
|
|
26547
|
+
async function assessArtifact(store, client, host, options) {
|
|
26548
|
+
const visited = /* @__PURE__ */ new Set();
|
|
26549
|
+
return _assessArtifactRecursive(store, client, host, options, visited, 0);
|
|
26550
|
+
}
|
|
26551
|
+
async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
|
|
26552
|
+
const errors = [];
|
|
26553
|
+
if (visited.has(options.artifactId)) {
|
|
26554
|
+
return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
|
|
26555
|
+
}
|
|
26556
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
26557
|
+
return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
|
|
26558
|
+
}
|
|
26559
|
+
visited.add(options.artifactId);
|
|
26560
|
+
const doc = store.get(options.artifactId);
|
|
26561
|
+
if (!doc) {
|
|
26562
|
+
return emptyArtifactReport(options.artifactId, [`Artifact ${options.artifactId} not found`]);
|
|
26563
|
+
}
|
|
26564
|
+
const fm = doc.frontmatter;
|
|
26565
|
+
const jiraKey = fm.jiraKey ?? extractJiraKeyFromTags(fm.tags) ?? null;
|
|
26566
|
+
const tags = fm.tags ?? [];
|
|
26567
|
+
const sprintTag = tags.find((t) => t.startsWith("sprint:"));
|
|
26568
|
+
const sprint = sprintTag ? sprintTag.slice(7) : null;
|
|
26569
|
+
const parent = fm.aboutArtifact ?? null;
|
|
26570
|
+
let jiraStatus = null;
|
|
26571
|
+
let jiraAssignee = null;
|
|
26572
|
+
let proposedMarvinStatus = null;
|
|
26573
|
+
let jiraSubtaskProgress = null;
|
|
26574
|
+
const commentSignals = [];
|
|
26575
|
+
let commentSummary = null;
|
|
26576
|
+
let linkedIssues = [];
|
|
26577
|
+
let linkedIssueSignals = [];
|
|
26578
|
+
const proposedUpdates = [];
|
|
26579
|
+
const jiraIssues = /* @__PURE__ */ new Map();
|
|
26580
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
26581
|
+
if (jiraKey) {
|
|
26582
|
+
try {
|
|
26583
|
+
const [issue2, comments] = await Promise.all([
|
|
26584
|
+
client.getIssueWithLinks(jiraKey),
|
|
26585
|
+
client.getComments(jiraKey)
|
|
26586
|
+
]);
|
|
26587
|
+
jiraIssues.set(jiraKey, { issue: issue2, comments });
|
|
26588
|
+
jiraStatus = issue2.fields.status.name;
|
|
26589
|
+
jiraAssignee = issue2.fields.assignee?.displayName ?? null;
|
|
26590
|
+
const inSprint = isInActiveSprint(store, fm.tags);
|
|
26591
|
+
const resolved = options.statusMap ?? {};
|
|
26592
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
26593
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
26594
|
+
if (subtasks.length > 0) {
|
|
26595
|
+
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
26596
|
+
}
|
|
26597
|
+
for (const comment of comments) {
|
|
26598
|
+
const text = extractCommentText(comment.body);
|
|
26599
|
+
const signals2 = detectCommentSignals(text);
|
|
26600
|
+
commentSignals.push(...signals2);
|
|
26601
|
+
}
|
|
26602
|
+
const jiraVisited = /* @__PURE__ */ new Set([jiraKey]);
|
|
26603
|
+
const queue = [];
|
|
26604
|
+
const directLinks = collectLinkedIssues(issue2).filter((l) => l.relationship !== "subtask");
|
|
26605
|
+
for (const link of directLinks) {
|
|
26606
|
+
if (!jiraVisited.has(link.key)) {
|
|
26607
|
+
jiraVisited.add(link.key);
|
|
26608
|
+
queue.push(link.key);
|
|
26609
|
+
}
|
|
26610
|
+
}
|
|
26611
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
26612
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
26613
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
26614
|
+
const results = await Promise.allSettled(
|
|
26615
|
+
batch.map(async (key) => {
|
|
26616
|
+
const [li, lc] = await Promise.all([
|
|
26617
|
+
client.getIssueWithLinks(key),
|
|
26618
|
+
client.getComments(key)
|
|
26619
|
+
]);
|
|
26620
|
+
return { key, issue: li, comments: lc };
|
|
26621
|
+
})
|
|
26622
|
+
);
|
|
26623
|
+
for (const result of results) {
|
|
26624
|
+
if (result.status === "fulfilled") {
|
|
26625
|
+
const { key, issue: li, comments: lc } = result.value;
|
|
26626
|
+
linkedJiraIssues.set(key, { issue: li, comments: lc });
|
|
26627
|
+
const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
|
|
26628
|
+
for (const nl of newLinks) {
|
|
26629
|
+
jiraVisited.add(nl.key);
|
|
26630
|
+
queue.push(nl.key);
|
|
26631
|
+
}
|
|
26632
|
+
} else {
|
|
26633
|
+
const batchKey = batch[results.indexOf(result)];
|
|
26634
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
26635
|
+
}
|
|
26636
|
+
}
|
|
26637
|
+
}
|
|
26638
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
26639
|
+
issue2,
|
|
26640
|
+
jiraIssues,
|
|
26641
|
+
linkedJiraIssues
|
|
26642
|
+
);
|
|
26643
|
+
linkedIssues = allLinks;
|
|
26644
|
+
linkedIssueSignals = allSignals;
|
|
26645
|
+
analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
|
|
26646
|
+
} catch (err) {
|
|
26647
|
+
errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26648
|
+
}
|
|
26649
|
+
}
|
|
26650
|
+
const currentProgress = getEffectiveProgress(fm);
|
|
26651
|
+
const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
|
|
26652
|
+
const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
|
|
26653
|
+
if (statusDrift && proposedMarvinStatus) {
|
|
26654
|
+
proposedUpdates.push({
|
|
26655
|
+
artifactId: fm.id,
|
|
26656
|
+
field: "status",
|
|
26657
|
+
currentValue: fm.status,
|
|
26658
|
+
proposedValue: proposedMarvinStatus,
|
|
26659
|
+
reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
|
|
26660
|
+
});
|
|
26661
|
+
}
|
|
26662
|
+
if (progressDrift && jiraSubtaskProgress !== null) {
|
|
26663
|
+
proposedUpdates.push({
|
|
26664
|
+
artifactId: fm.id,
|
|
26665
|
+
field: "progress",
|
|
26666
|
+
currentValue: currentProgress,
|
|
26667
|
+
proposedValue: jiraSubtaskProgress,
|
|
26668
|
+
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
26669
|
+
});
|
|
26670
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
26671
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
26672
|
+
if (!hasExplicitProgress) {
|
|
26673
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
26674
|
+
if (proposedProgress !== currentProgress) {
|
|
26675
|
+
proposedUpdates.push({
|
|
26676
|
+
artifactId: fm.id,
|
|
26677
|
+
field: "progress",
|
|
26678
|
+
currentValue: currentProgress,
|
|
26679
|
+
proposedValue: proposedProgress,
|
|
26680
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
26681
|
+
});
|
|
26682
|
+
}
|
|
26683
|
+
}
|
|
26684
|
+
}
|
|
26685
|
+
const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
|
|
26686
|
+
if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
|
|
26687
|
+
const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
|
|
26688
|
+
if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
|
|
26689
|
+
try {
|
|
26690
|
+
const summary = await analyzeSingleArtifactComments(
|
|
26691
|
+
fm.id,
|
|
26692
|
+
fm.title,
|
|
26693
|
+
jiraKey,
|
|
26694
|
+
jiraStatus,
|
|
26695
|
+
jiraIssues,
|
|
26696
|
+
linkedJiraIssues,
|
|
26697
|
+
linkedIssueSignals
|
|
26698
|
+
);
|
|
26699
|
+
commentSummary = summary;
|
|
26700
|
+
} catch (err) {
|
|
26701
|
+
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26702
|
+
}
|
|
26703
|
+
}
|
|
26704
|
+
}
|
|
26705
|
+
const childIds = findChildIds(store, fm);
|
|
26706
|
+
const children = [];
|
|
26707
|
+
for (const childId of childIds) {
|
|
26708
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
26709
|
+
errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
|
|
26710
|
+
break;
|
|
26711
|
+
}
|
|
26712
|
+
const childReport = await _assessArtifactRecursive(
|
|
26713
|
+
store,
|
|
26714
|
+
client,
|
|
26715
|
+
host,
|
|
26716
|
+
{ ...options, artifactId: childId },
|
|
26717
|
+
visited,
|
|
26718
|
+
depth + 1
|
|
26719
|
+
);
|
|
26720
|
+
children.push(childReport);
|
|
26721
|
+
}
|
|
26722
|
+
if (children.length > 0) {
|
|
26723
|
+
const rolledUpProgress = computeWeightedProgress(
|
|
26724
|
+
children.map((c) => ({
|
|
26725
|
+
weight: resolveWeight(void 0).weight,
|
|
26726
|
+
progress: c.marvinProgress
|
|
26727
|
+
}))
|
|
26728
|
+
);
|
|
26729
|
+
if (rolledUpProgress !== currentProgress) {
|
|
26730
|
+
proposedUpdates.push({
|
|
26731
|
+
artifactId: fm.id,
|
|
26732
|
+
field: "progress",
|
|
26733
|
+
currentValue: currentProgress,
|
|
26734
|
+
proposedValue: rolledUpProgress,
|
|
26735
|
+
reason: `Rolled up from ${children.length} children (weighted average ${rolledUpProgress}%)`
|
|
26736
|
+
});
|
|
26737
|
+
}
|
|
26738
|
+
}
|
|
26739
|
+
const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
|
|
26740
|
+
const appliedUpdates = [];
|
|
26741
|
+
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
26742
|
+
for (const update of proposedUpdates) {
|
|
26743
|
+
if (update.field === "review") continue;
|
|
26744
|
+
try {
|
|
26745
|
+
const updatePayload = {
|
|
26746
|
+
[update.field]: update.proposedValue,
|
|
26747
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26748
|
+
};
|
|
26749
|
+
if (update.field === "progress") {
|
|
26750
|
+
updatePayload.progressOverride = false;
|
|
26751
|
+
}
|
|
26752
|
+
store.update(update.artifactId, updatePayload);
|
|
26753
|
+
const updatedDoc = store.get(update.artifactId);
|
|
26754
|
+
if (updatedDoc) {
|
|
26755
|
+
if (updatedDoc.frontmatter.type === "task") {
|
|
26756
|
+
propagateProgressFromTask(store, update.artifactId);
|
|
26757
|
+
} else if (updatedDoc.frontmatter.type === "action") {
|
|
26758
|
+
propagateProgressToAction(store, update.artifactId);
|
|
26759
|
+
}
|
|
26760
|
+
}
|
|
26761
|
+
appliedUpdates.push(update);
|
|
26762
|
+
} catch (err) {
|
|
26763
|
+
errors.push(`Failed to apply update: ${err instanceof Error ? err.message : String(err)}`);
|
|
26764
|
+
}
|
|
26765
|
+
}
|
|
26766
|
+
}
|
|
26767
|
+
return {
|
|
26768
|
+
artifactId: fm.id,
|
|
26769
|
+
title: fm.title,
|
|
26770
|
+
type: fm.type,
|
|
26771
|
+
marvinStatus: fm.status,
|
|
26772
|
+
marvinProgress: currentProgress,
|
|
26773
|
+
sprint,
|
|
26774
|
+
parent,
|
|
26775
|
+
jiraKey,
|
|
26776
|
+
jiraStatus,
|
|
26777
|
+
jiraAssignee,
|
|
26778
|
+
jiraSubtaskProgress,
|
|
26779
|
+
proposedMarvinStatus,
|
|
26780
|
+
statusDrift,
|
|
26781
|
+
progressDrift,
|
|
26782
|
+
commentSignals,
|
|
26783
|
+
commentSummary,
|
|
26784
|
+
linkedIssues,
|
|
26785
|
+
linkedIssueSignals,
|
|
26786
|
+
children,
|
|
26787
|
+
proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
|
|
26788
|
+
appliedUpdates,
|
|
26789
|
+
signals,
|
|
26790
|
+
errors
|
|
26791
|
+
};
|
|
26792
|
+
}
|
|
26793
|
+
function findChildIds(store, fm) {
|
|
26794
|
+
if (fm.type === "action") {
|
|
26795
|
+
return store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === fm.id).map((d) => d.frontmatter.id);
|
|
26796
|
+
}
|
|
26797
|
+
if (fm.type === "epic") {
|
|
26798
|
+
const epicTag = `epic:${fm.id}`;
|
|
26799
|
+
const isLinked = (d) => {
|
|
26800
|
+
const le = d.frontmatter.linkedEpic;
|
|
26801
|
+
if (le?.includes(fm.id)) return true;
|
|
26802
|
+
const t = d.frontmatter.tags ?? [];
|
|
26803
|
+
return t.includes(epicTag);
|
|
26804
|
+
};
|
|
26805
|
+
return [
|
|
26806
|
+
...store.list({ type: "action" }).filter(isLinked),
|
|
26807
|
+
...store.list({ type: "task" }).filter(isLinked)
|
|
26808
|
+
].map((d) => d.frontmatter.id);
|
|
26809
|
+
}
|
|
26810
|
+
return [];
|
|
26811
|
+
}
|
|
26812
|
+
function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus) {
|
|
26813
|
+
const signals = [];
|
|
26814
|
+
const blockerSignals = commentSignals.filter((s) => s.type === "blocker");
|
|
26815
|
+
if (blockerSignals.length > 0) {
|
|
26816
|
+
for (const s of blockerSignals) {
|
|
26817
|
+
signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
|
|
26818
|
+
}
|
|
26819
|
+
}
|
|
26820
|
+
const blockingLinks = linkedIssues.filter(
|
|
26821
|
+
(l) => l.relationship.toLowerCase().includes("block")
|
|
26822
|
+
);
|
|
26823
|
+
const activeBlockers = blockingLinks.filter((l) => !l.isDone);
|
|
26824
|
+
const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
|
|
26825
|
+
if (activeBlockers.length > 0) {
|
|
26826
|
+
for (const b of activeBlockers) {
|
|
26827
|
+
signals.push(`\u{1F6AB} Blocker \u2014 ${b.relationship} ${b.key} "${b.summary}" [${b.status}]`);
|
|
26828
|
+
}
|
|
26829
|
+
}
|
|
26830
|
+
if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
|
|
26831
|
+
signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
|
|
26832
|
+
}
|
|
26833
|
+
const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
|
|
26834
|
+
for (const l of wontDoLinks) {
|
|
26835
|
+
signals.push(`\u{1F504} Superseded \u2014 ${l.key} "${l.summary}" is ${l.status}`);
|
|
26836
|
+
}
|
|
26837
|
+
const questionSignals = commentSignals.filter((s) => s.type === "question");
|
|
26838
|
+
for (const s of questionSignals) {
|
|
26839
|
+
signals.push(`\u23F3 Waiting \u2014 "${s.snippet}"`);
|
|
26840
|
+
}
|
|
26841
|
+
const relatedInProgress = linkedIssues.filter(
|
|
26842
|
+
(l) => l.relationship.toLowerCase().includes("relate") && !l.isDone
|
|
26843
|
+
);
|
|
26844
|
+
if (relatedInProgress.length > 0) {
|
|
26845
|
+
for (const l of relatedInProgress) {
|
|
26846
|
+
signals.push(`\u{1F4CB} Handoff \u2014 related work on ${l.key} "${l.summary}" [${l.status}]`);
|
|
26847
|
+
}
|
|
26848
|
+
}
|
|
26849
|
+
if (signals.length === 0) {
|
|
26850
|
+
if (statusDrift && proposedStatus) {
|
|
26851
|
+
signals.push(`\u26A0 Drift detected \u2014 Marvin and Jira statuses diverge`);
|
|
26852
|
+
} else {
|
|
26853
|
+
signals.push(`\u2705 No active blockers or concerns detected`);
|
|
26854
|
+
}
|
|
26855
|
+
}
|
|
26856
|
+
return signals;
|
|
26857
|
+
}
|
|
26858
|
+
function estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
26859
|
+
let total = 0;
|
|
26860
|
+
for (const [, data] of jiraIssues) {
|
|
26861
|
+
for (const c of data.comments) {
|
|
26862
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
26863
|
+
}
|
|
26864
|
+
}
|
|
26865
|
+
for (const signal of linkedIssueSignals) {
|
|
26866
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26867
|
+
if (!linkedData) continue;
|
|
26868
|
+
for (const c of linkedData.comments) {
|
|
26869
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
26870
|
+
}
|
|
26871
|
+
}
|
|
26872
|
+
return total;
|
|
26873
|
+
}
|
|
26874
|
+
var SINGLE_ARTIFACT_COMMENT_PROMPT = `You are a delivery management assistant analyzing Jira comments for a single work item.
|
|
26875
|
+
|
|
26876
|
+
Produce a 2-3 sentence progress summary covering:
|
|
26877
|
+
- What work has been completed
|
|
26878
|
+
- What is pending or blocked
|
|
26879
|
+
- Any decisions, handoffs, or scheduling mentioned
|
|
26880
|
+
- Relevant context from linked issue comments (if provided)
|
|
26881
|
+
|
|
26882
|
+
Return ONLY the summary text, no JSON or formatting.`;
|
|
26883
|
+
async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraStatus, jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
26884
|
+
const promptParts = [];
|
|
26885
|
+
const primaryData = jiraIssues.get(jiraKey);
|
|
26886
|
+
if (primaryData && primaryData.comments.length > 0) {
|
|
26887
|
+
const commentTexts = primaryData.comments.map((c) => {
|
|
26888
|
+
const text = extractCommentText(c.body);
|
|
26889
|
+
return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
|
|
26890
|
+
}).join("\n");
|
|
26891
|
+
promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
|
|
26892
|
+
Comments:
|
|
26893
|
+
${commentTexts}`);
|
|
26894
|
+
}
|
|
26895
|
+
for (const signal of linkedIssueSignals) {
|
|
26896
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26897
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
26898
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
26899
|
+
const text = extractCommentText(c.body);
|
|
26900
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
26901
|
+
}).join("\n");
|
|
26902
|
+
promptParts.push(`### Linked: ${signal.sourceKey} (${signal.linkType})
|
|
26903
|
+
${commentTexts}`);
|
|
26904
|
+
}
|
|
26905
|
+
if (promptParts.length === 0) return null;
|
|
26906
|
+
const prompt = promptParts.join("\n\n");
|
|
26907
|
+
const result = query3({
|
|
26908
|
+
prompt,
|
|
26909
|
+
options: {
|
|
26910
|
+
systemPrompt: SINGLE_ARTIFACT_COMMENT_PROMPT,
|
|
26911
|
+
maxTurns: 1,
|
|
26912
|
+
tools: [],
|
|
26913
|
+
allowedTools: []
|
|
26914
|
+
}
|
|
26915
|
+
});
|
|
26916
|
+
for await (const msg of result) {
|
|
26917
|
+
if (msg.type === "assistant") {
|
|
26918
|
+
const textBlock = msg.message.content.find(
|
|
26919
|
+
(b) => b.type === "text"
|
|
26920
|
+
);
|
|
26921
|
+
if (textBlock) {
|
|
26922
|
+
return textBlock.text.trim();
|
|
26923
|
+
}
|
|
26924
|
+
}
|
|
26925
|
+
}
|
|
26926
|
+
return null;
|
|
26927
|
+
}
|
|
26928
|
+
function emptyArtifactReport(artifactId, errors) {
|
|
26929
|
+
return {
|
|
26930
|
+
artifactId,
|
|
26931
|
+
title: "Not found",
|
|
26932
|
+
type: "unknown",
|
|
26933
|
+
marvinStatus: "unknown",
|
|
26934
|
+
marvinProgress: 0,
|
|
26935
|
+
sprint: null,
|
|
26936
|
+
parent: null,
|
|
26937
|
+
jiraKey: null,
|
|
26938
|
+
jiraStatus: null,
|
|
26939
|
+
jiraAssignee: null,
|
|
26940
|
+
jiraSubtaskProgress: null,
|
|
26941
|
+
proposedMarvinStatus: null,
|
|
26942
|
+
statusDrift: false,
|
|
26943
|
+
progressDrift: false,
|
|
26944
|
+
commentSignals: [],
|
|
26945
|
+
commentSummary: null,
|
|
26946
|
+
linkedIssues: [],
|
|
26947
|
+
linkedIssueSignals: [],
|
|
26948
|
+
children: [],
|
|
26949
|
+
proposedUpdates: [],
|
|
26950
|
+
appliedUpdates: [],
|
|
26951
|
+
signals: [],
|
|
26952
|
+
errors
|
|
26953
|
+
};
|
|
26954
|
+
}
|
|
26955
|
+
function formatArtifactReport(report) {
|
|
26956
|
+
const parts = [];
|
|
26957
|
+
parts.push(`# Artifact Assessment \u2014 ${report.artifactId}`);
|
|
26958
|
+
parts.push(report.title);
|
|
26959
|
+
parts.push("");
|
|
26960
|
+
parts.push(`## Marvin State`);
|
|
26961
|
+
const marvinParts = [`Status: ${report.marvinStatus}`, `Progress: ${report.marvinProgress}%`];
|
|
26962
|
+
if (report.sprint) marvinParts.push(`Sprint: ${report.sprint}`);
|
|
26963
|
+
if (report.parent) marvinParts.push(`Parent: ${report.parent}`);
|
|
26964
|
+
parts.push(marvinParts.join(" | "));
|
|
26965
|
+
parts.push("");
|
|
26966
|
+
if (report.jiraKey) {
|
|
26967
|
+
parts.push(`## Jira State (${report.jiraKey})`);
|
|
26968
|
+
const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
|
|
26969
|
+
if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
|
|
26970
|
+
if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
|
|
26971
|
+
parts.push(jiraParts.join(" | "));
|
|
26972
|
+
if (report.statusDrift) {
|
|
26973
|
+
parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
|
|
26974
|
+
}
|
|
26975
|
+
if (report.progressDrift && report.jiraSubtaskProgress !== null) {
|
|
26976
|
+
parts.push(`\u26A0 Progress drift: ${report.marvinProgress}% \u2192 ${report.jiraSubtaskProgress}%`);
|
|
26977
|
+
}
|
|
26978
|
+
parts.push("");
|
|
26979
|
+
}
|
|
26980
|
+
if (report.commentSummary) {
|
|
26981
|
+
parts.push(`## Comments`);
|
|
26982
|
+
parts.push(report.commentSummary);
|
|
26983
|
+
parts.push("");
|
|
26984
|
+
}
|
|
26985
|
+
if (report.children.length > 0) {
|
|
26986
|
+
const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
|
|
26987
|
+
const childWeights = report.children.map((c) => {
|
|
26988
|
+
const { weight } = resolveWeight(void 0);
|
|
26989
|
+
return { weight, progress: c.marvinProgress };
|
|
26990
|
+
});
|
|
26991
|
+
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;
|
|
26992
|
+
const bar = progressBar6(childProgress);
|
|
26993
|
+
parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
|
|
26994
|
+
for (const child of report.children) {
|
|
26995
|
+
formatArtifactChild(parts, child, 1);
|
|
26996
|
+
}
|
|
26997
|
+
parts.push("");
|
|
26998
|
+
}
|
|
26999
|
+
if (report.linkedIssues.length > 0) {
|
|
27000
|
+
parts.push(`## Linked Issues (${report.linkedIssues.length})`);
|
|
27001
|
+
for (const link of report.linkedIssues) {
|
|
27002
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
27003
|
+
parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
|
|
27004
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
27005
|
+
if (signal?.commentSummary) {
|
|
27006
|
+
parts.push(` \u{1F4AC} ${signal.commentSummary}`);
|
|
27007
|
+
}
|
|
27008
|
+
}
|
|
27009
|
+
parts.push("");
|
|
27010
|
+
}
|
|
27011
|
+
if (report.signals.length > 0) {
|
|
27012
|
+
parts.push(`## Signals`);
|
|
27013
|
+
for (const s of report.signals) {
|
|
27014
|
+
parts.push(` ${s}`);
|
|
27015
|
+
}
|
|
27016
|
+
parts.push("");
|
|
27017
|
+
}
|
|
27018
|
+
if (report.proposedUpdates.length > 0) {
|
|
27019
|
+
parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
|
|
27020
|
+
for (const update of report.proposedUpdates) {
|
|
27021
|
+
parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
27022
|
+
parts.push(` Reason: ${update.reason}`);
|
|
27023
|
+
}
|
|
27024
|
+
parts.push("");
|
|
27025
|
+
parts.push("Run with applyUpdates=true to apply these changes.");
|
|
27026
|
+
parts.push("");
|
|
27027
|
+
}
|
|
27028
|
+
if (report.appliedUpdates.length > 0) {
|
|
27029
|
+
parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
|
|
27030
|
+
for (const update of report.appliedUpdates) {
|
|
27031
|
+
parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
27032
|
+
}
|
|
27033
|
+
parts.push("");
|
|
27034
|
+
}
|
|
27035
|
+
if (report.errors.length > 0) {
|
|
27036
|
+
parts.push(`## Errors`);
|
|
27037
|
+
for (const err of report.errors) {
|
|
27038
|
+
parts.push(` ${err}`);
|
|
27039
|
+
}
|
|
27040
|
+
parts.push("");
|
|
27041
|
+
}
|
|
27042
|
+
return parts.join("\n");
|
|
27043
|
+
}
|
|
27044
|
+
function formatArtifactChild(parts, child, depth) {
|
|
27045
|
+
const indent = " ".repeat(depth);
|
|
27046
|
+
const icon = DONE_STATUSES15.has(child.marvinStatus) ? "\u2713" : child.marvinStatus === "blocked" ? "\u{1F6AB}" : child.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
27047
|
+
const jiraLabel = child.jiraKey ? ` [${child.jiraKey}: ${child.jiraStatus ?? "?"}]` : "";
|
|
27048
|
+
const driftLabel = child.statusDrift ? ` \u26A0drift \u2192 ${child.proposedMarvinStatus}` : "";
|
|
27049
|
+
const signalHints = [];
|
|
27050
|
+
for (const s of child.signals) {
|
|
27051
|
+
if (s.startsWith("\u2705 No active")) continue;
|
|
27052
|
+
signalHints.push(s);
|
|
27053
|
+
}
|
|
27054
|
+
parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
|
|
27055
|
+
if (child.commentSummary) {
|
|
27056
|
+
parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
|
|
27057
|
+
}
|
|
27058
|
+
for (const hint of signalHints) {
|
|
27059
|
+
parts.push(`${indent} ${hint}`);
|
|
27060
|
+
}
|
|
27061
|
+
for (const grandchild of child.children) {
|
|
27062
|
+
formatArtifactChild(parts, grandchild, depth + 1);
|
|
27063
|
+
}
|
|
27064
|
+
}
|
|
26303
27065
|
|
|
26304
27066
|
// src/skills/builtin/jira/tools.ts
|
|
26305
27067
|
var JIRA_TYPE = "jira-issue";
|
|
@@ -27086,7 +27848,8 @@ function createJiraTools(store, projectConfig) {
|
|
|
27086
27848
|
{
|
|
27087
27849
|
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
27088
27850
|
analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
|
|
27089
|
-
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
|
|
27851
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)"),
|
|
27852
|
+
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)")
|
|
27090
27853
|
},
|
|
27091
27854
|
async (args) => {
|
|
27092
27855
|
const jira = createJiraClient(jiraUserConfig);
|
|
@@ -27099,6 +27862,7 @@ function createJiraTools(store, projectConfig) {
|
|
|
27099
27862
|
sprintId: args.sprintId,
|
|
27100
27863
|
analyzeComments: args.analyzeComments ?? false,
|
|
27101
27864
|
applyUpdates: args.applyUpdates ?? false,
|
|
27865
|
+
traverseLinks: args.traverseLinks ?? false,
|
|
27102
27866
|
statusMap
|
|
27103
27867
|
}
|
|
27104
27868
|
);
|
|
@@ -27108,6 +27872,34 @@ function createJiraTools(store, projectConfig) {
|
|
|
27108
27872
|
};
|
|
27109
27873
|
},
|
|
27110
27874
|
{ annotations: { readOnlyHint: false } }
|
|
27875
|
+
),
|
|
27876
|
+
// --- Single-artifact assessment ---
|
|
27877
|
+
tool20(
|
|
27878
|
+
"assess_artifact",
|
|
27879
|
+
"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).",
|
|
27880
|
+
{
|
|
27881
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
|
|
27882
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
|
|
27883
|
+
},
|
|
27884
|
+
async (args) => {
|
|
27885
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
27886
|
+
if (!jira) return jiraNotConfiguredError();
|
|
27887
|
+
const report = await assessArtifact(
|
|
27888
|
+
store,
|
|
27889
|
+
jira.client,
|
|
27890
|
+
jira.host,
|
|
27891
|
+
{
|
|
27892
|
+
artifactId: args.artifactId,
|
|
27893
|
+
applyUpdates: args.applyUpdates ?? false,
|
|
27894
|
+
statusMap
|
|
27895
|
+
}
|
|
27896
|
+
);
|
|
27897
|
+
return {
|
|
27898
|
+
content: [{ type: "text", text: formatArtifactReport(report) }],
|
|
27899
|
+
isError: report.errors.length > 0 && report.type === "unknown"
|
|
27900
|
+
};
|
|
27901
|
+
},
|
|
27902
|
+
{ annotations: { readOnlyHint: false } }
|
|
27111
27903
|
)
|
|
27112
27904
|
];
|
|
27113
27905
|
}
|
|
@@ -27209,7 +28001,8 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
27209
28001
|
- \`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.
|
|
27210
28002
|
- \`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.
|
|
27211
28003
|
- \`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).
|
|
27212
|
-
- \`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.
|
|
28004
|
+
- \`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.
|
|
28005
|
+
- \`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.
|
|
27213
28006
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
27214
28007
|
- \`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.
|
|
27215
28008
|
- \`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).
|
|
@@ -27226,6 +28019,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
27226
28019
|
2. Review focus area rollups, status drift, and blockers
|
|
27227
28020
|
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
27228
28021
|
|
|
28022
|
+
**Single-artifact deep dive:**
|
|
28023
|
+
1. Call \`assess_artifact\` with an artifact ID to get a focused assessment with Jira sync, comment analysis, link traversal, and child rollup
|
|
28024
|
+
2. Review signals (blockers, unblocks, handoffs) and proposed updates
|
|
28025
|
+
3. Use \`applyUpdates=true\` to apply changes
|
|
28026
|
+
|
|
27229
28027
|
**Daily review workflow:**
|
|
27230
28028
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
27231
28029
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -27250,6 +28048,7 @@ ${COMMON_WORKFLOW}
|
|
|
27250
28048
|
**As Product Owner, use Jira integration to:**
|
|
27251
28049
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
27252
28050
|
- Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
|
|
28051
|
+
- Use \`assess_artifact\` for deep dives into specific features or epics \u2014 full Jira context, linked issues, and child rollup
|
|
27253
28052
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
27254
28053
|
- Push approved features as Stories for development tracking
|
|
27255
28054
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -27263,6 +28062,7 @@ ${COMMON_WORKFLOW}
|
|
|
27263
28062
|
**As Tech Lead, use Jira integration to:**
|
|
27264
28063
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
27265
28064
|
- Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
|
|
28065
|
+
- Use \`assess_artifact\` to investigate a specific task \u2014 see full dependency chain, comment context, and blocker status
|
|
27266
28066
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
27267
28067
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
27268
28068
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -27278,6 +28078,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
27278
28078
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
27279
28079
|
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
27280
28080
|
- Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
|
|
28081
|
+
- Use \`assess_artifact\` for focused status checks on critical items \u2014 full Jira context without running the whole sprint assessment
|
|
27281
28082
|
- Pull sprint issues for tracking progress and blockers
|
|
27282
28083
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
27283
28084
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -32758,7 +33559,7 @@ function createProgram() {
|
|
|
32758
33559
|
const program = new Command();
|
|
32759
33560
|
program.name("marvin").description(
|
|
32760
33561
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
32761
|
-
).version("0.5.
|
|
33562
|
+
).version("0.5.19");
|
|
32762
33563
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
32763
33564
|
await initCommand();
|
|
32764
33565
|
});
|