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-serve.js
CHANGED
|
@@ -15635,6 +15635,14 @@ function evaluateHealth(projectName, metrics) {
|
|
|
15635
15635
|
|
|
15636
15636
|
// src/reports/sprint-summary/collector.ts
|
|
15637
15637
|
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15638
|
+
var COMPLEXITY_WEIGHTS = {
|
|
15639
|
+
trivial: 1,
|
|
15640
|
+
simple: 2,
|
|
15641
|
+
moderate: 3,
|
|
15642
|
+
complex: 5,
|
|
15643
|
+
"very-complex": 8
|
|
15644
|
+
};
|
|
15645
|
+
var DEFAULT_WEIGHT = 3;
|
|
15638
15646
|
function collectSprintSummaryData(store, sprintId) {
|
|
15639
15647
|
const allDocs = store.list();
|
|
15640
15648
|
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -15720,12 +15728,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15720
15728
|
for (const doc of workItemDocs) {
|
|
15721
15729
|
const about = doc.frontmatter.aboutArtifact;
|
|
15722
15730
|
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
15731
|
+
const complexity = doc.frontmatter.complexity;
|
|
15723
15732
|
const item = {
|
|
15724
15733
|
id: doc.frontmatter.id,
|
|
15725
15734
|
title: doc.frontmatter.title,
|
|
15726
15735
|
type: doc.frontmatter.type,
|
|
15727
15736
|
status: doc.frontmatter.status,
|
|
15728
15737
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15738
|
+
weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
|
|
15729
15739
|
owner: doc.frontmatter.owner,
|
|
15730
15740
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15731
15741
|
aboutArtifact: about,
|
|
@@ -19271,6 +19281,47 @@ function extractJiraKeyFromTags(tags) {
|
|
|
19271
19281
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
19272
19282
|
return tag ? tag.slice(5) : void 0;
|
|
19273
19283
|
}
|
|
19284
|
+
function collectLinkedIssues(issue2) {
|
|
19285
|
+
const linkedIssues = [];
|
|
19286
|
+
if (issue2.fields.subtasks) {
|
|
19287
|
+
for (const sub of issue2.fields.subtasks) {
|
|
19288
|
+
linkedIssues.push({
|
|
19289
|
+
key: sub.key,
|
|
19290
|
+
summary: sub.fields.summary,
|
|
19291
|
+
status: sub.fields.status.name,
|
|
19292
|
+
relationship: "subtask",
|
|
19293
|
+
isDone: DONE_STATUSES5.has(sub.fields.status.name.toLowerCase())
|
|
19294
|
+
});
|
|
19295
|
+
}
|
|
19296
|
+
}
|
|
19297
|
+
if (issue2.fields.issuelinks) {
|
|
19298
|
+
for (const link of issue2.fields.issuelinks) {
|
|
19299
|
+
if (link.outwardIssue) {
|
|
19300
|
+
linkedIssues.push({
|
|
19301
|
+
key: link.outwardIssue.key,
|
|
19302
|
+
summary: link.outwardIssue.fields.summary,
|
|
19303
|
+
status: link.outwardIssue.fields.status.name,
|
|
19304
|
+
relationship: link.type.outward,
|
|
19305
|
+
isDone: DONE_STATUSES5.has(
|
|
19306
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
19307
|
+
)
|
|
19308
|
+
});
|
|
19309
|
+
}
|
|
19310
|
+
if (link.inwardIssue) {
|
|
19311
|
+
linkedIssues.push({
|
|
19312
|
+
key: link.inwardIssue.key,
|
|
19313
|
+
summary: link.inwardIssue.fields.summary,
|
|
19314
|
+
status: link.inwardIssue.fields.status.name,
|
|
19315
|
+
relationship: link.type.inward,
|
|
19316
|
+
isDone: DONE_STATUSES5.has(
|
|
19317
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
19318
|
+
)
|
|
19319
|
+
});
|
|
19320
|
+
}
|
|
19321
|
+
}
|
|
19322
|
+
}
|
|
19323
|
+
return linkedIssues;
|
|
19324
|
+
}
|
|
19274
19325
|
function computeSubtaskProgress(subtasks) {
|
|
19275
19326
|
if (subtasks.length === 0) return 0;
|
|
19276
19327
|
const done = subtasks.filter(
|
|
@@ -19311,44 +19362,7 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
19311
19362
|
const resolved = statusMap ?? {};
|
|
19312
19363
|
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
|
|
19313
19364
|
const currentStatus = doc.frontmatter.status;
|
|
19314
|
-
const linkedIssues =
|
|
19315
|
-
if (issue2.fields.subtasks) {
|
|
19316
|
-
for (const sub of issue2.fields.subtasks) {
|
|
19317
|
-
linkedIssues.push({
|
|
19318
|
-
key: sub.key,
|
|
19319
|
-
summary: sub.fields.summary,
|
|
19320
|
-
status: sub.fields.status.name,
|
|
19321
|
-
relationship: "subtask",
|
|
19322
|
-
isDone: DONE_STATUSES5.has(sub.fields.status.name.toLowerCase())
|
|
19323
|
-
});
|
|
19324
|
-
}
|
|
19325
|
-
}
|
|
19326
|
-
if (issue2.fields.issuelinks) {
|
|
19327
|
-
for (const link of issue2.fields.issuelinks) {
|
|
19328
|
-
if (link.outwardIssue) {
|
|
19329
|
-
linkedIssues.push({
|
|
19330
|
-
key: link.outwardIssue.key,
|
|
19331
|
-
summary: link.outwardIssue.fields.summary,
|
|
19332
|
-
status: link.outwardIssue.fields.status.name,
|
|
19333
|
-
relationship: link.type.outward,
|
|
19334
|
-
isDone: DONE_STATUSES5.has(
|
|
19335
|
-
link.outwardIssue.fields.status.name.toLowerCase()
|
|
19336
|
-
)
|
|
19337
|
-
});
|
|
19338
|
-
}
|
|
19339
|
-
if (link.inwardIssue) {
|
|
19340
|
-
linkedIssues.push({
|
|
19341
|
-
key: link.inwardIssue.key,
|
|
19342
|
-
summary: link.inwardIssue.fields.summary,
|
|
19343
|
-
status: link.inwardIssue.fields.status.name,
|
|
19344
|
-
relationship: link.type.inward,
|
|
19345
|
-
isDone: DONE_STATUSES5.has(
|
|
19346
|
-
link.inwardIssue.fields.status.name.toLowerCase()
|
|
19347
|
-
)
|
|
19348
|
-
});
|
|
19349
|
-
}
|
|
19350
|
-
}
|
|
19351
|
-
}
|
|
19365
|
+
const linkedIssues = collectLinkedIssues(issue2);
|
|
19352
19366
|
const subtasks = issue2.fields.subtasks ?? [];
|
|
19353
19367
|
let proposedProgress;
|
|
19354
19368
|
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
@@ -19570,7 +19584,6 @@ function isWithinRange(timestamp, range) {
|
|
|
19570
19584
|
function isConfluenceUrl(url2) {
|
|
19571
19585
|
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
19572
19586
|
}
|
|
19573
|
-
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
19574
19587
|
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
19575
19588
|
const summary = {
|
|
19576
19589
|
dateRange,
|
|
@@ -19681,42 +19694,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
19681
19694
|
});
|
|
19682
19695
|
}
|
|
19683
19696
|
}
|
|
19684
|
-
const linkedIssues = [];
|
|
19685
|
-
if (issueWithLinks) {
|
|
19686
|
-
if (issueWithLinks.fields.subtasks) {
|
|
19687
|
-
for (const sub of issueWithLinks.fields.subtasks) {
|
|
19688
|
-
linkedIssues.push({
|
|
19689
|
-
key: sub.key,
|
|
19690
|
-
summary: sub.fields.summary,
|
|
19691
|
-
status: sub.fields.status.name,
|
|
19692
|
-
relationship: "subtask",
|
|
19693
|
-
isDone: DONE_STATUSES6.has(sub.fields.status.name.toLowerCase())
|
|
19694
|
-
});
|
|
19695
|
-
}
|
|
19696
|
-
}
|
|
19697
|
-
if (issueWithLinks.fields.issuelinks) {
|
|
19698
|
-
for (const link of issueWithLinks.fields.issuelinks) {
|
|
19699
|
-
if (link.outwardIssue) {
|
|
19700
|
-
linkedIssues.push({
|
|
19701
|
-
key: link.outwardIssue.key,
|
|
19702
|
-
summary: link.outwardIssue.fields.summary,
|
|
19703
|
-
status: link.outwardIssue.fields.status.name,
|
|
19704
|
-
relationship: link.type.outward,
|
|
19705
|
-
isDone: DONE_STATUSES6.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
19706
|
-
});
|
|
19707
|
-
}
|
|
19708
|
-
if (link.inwardIssue) {
|
|
19709
|
-
linkedIssues.push({
|
|
19710
|
-
key: link.inwardIssue.key,
|
|
19711
|
-
summary: link.inwardIssue.fields.summary,
|
|
19712
|
-
status: link.inwardIssue.fields.status.name,
|
|
19713
|
-
relationship: link.type.inward,
|
|
19714
|
-
isDone: DONE_STATUSES6.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
19715
|
-
});
|
|
19716
|
-
}
|
|
19717
|
-
}
|
|
19718
|
-
}
|
|
19719
|
-
}
|
|
19697
|
+
const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
|
|
19720
19698
|
const marvinArtifacts = [];
|
|
19721
19699
|
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
19722
19700
|
for (const doc of artifacts) {
|
|
@@ -19846,22 +19824,23 @@ function generateProposedActions(issues) {
|
|
|
19846
19824
|
|
|
19847
19825
|
// src/skills/builtin/jira/sprint-progress.ts
|
|
19848
19826
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
19849
|
-
var
|
|
19827
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
19850
19828
|
var BATCH_SIZE = 5;
|
|
19829
|
+
var MAX_LINKED_ISSUES = 50;
|
|
19851
19830
|
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
19852
|
-
var
|
|
19831
|
+
var COMPLEXITY_WEIGHTS2 = {
|
|
19853
19832
|
trivial: 1,
|
|
19854
19833
|
simple: 2,
|
|
19855
19834
|
moderate: 3,
|
|
19856
19835
|
complex: 5,
|
|
19857
19836
|
"very-complex": 8
|
|
19858
19837
|
};
|
|
19859
|
-
var
|
|
19838
|
+
var DEFAULT_WEIGHT2 = 3;
|
|
19860
19839
|
function resolveWeight(complexity) {
|
|
19861
|
-
if (complexity && complexity in
|
|
19862
|
-
return { weight:
|
|
19840
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
|
|
19841
|
+
return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
|
|
19863
19842
|
}
|
|
19864
|
-
return { weight:
|
|
19843
|
+
return { weight: DEFAULT_WEIGHT2, weightSource: "default" };
|
|
19865
19844
|
}
|
|
19866
19845
|
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
19867
19846
|
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
@@ -19950,6 +19929,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
19950
19929
|
}
|
|
19951
19930
|
}
|
|
19952
19931
|
}
|
|
19932
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
19933
|
+
if (options.traverseLinks) {
|
|
19934
|
+
const visited = new Set(jiraIssues.keys());
|
|
19935
|
+
const queue = [];
|
|
19936
|
+
for (const [, data] of jiraIssues) {
|
|
19937
|
+
const links = collectLinkedIssues(data.issue);
|
|
19938
|
+
for (const link of links) {
|
|
19939
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
19940
|
+
visited.add(link.key);
|
|
19941
|
+
queue.push(link.key);
|
|
19942
|
+
}
|
|
19943
|
+
}
|
|
19944
|
+
}
|
|
19945
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
19946
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
19947
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
19948
|
+
const results = await Promise.allSettled(
|
|
19949
|
+
batch.map(async (key) => {
|
|
19950
|
+
const [issue2, comments] = await Promise.all([
|
|
19951
|
+
client.getIssueWithLinks(key),
|
|
19952
|
+
client.getComments(key)
|
|
19953
|
+
]);
|
|
19954
|
+
return { key, issue: issue2, comments };
|
|
19955
|
+
})
|
|
19956
|
+
);
|
|
19957
|
+
for (const result of results) {
|
|
19958
|
+
if (result.status === "fulfilled") {
|
|
19959
|
+
const { key, issue: issue2, comments } = result.value;
|
|
19960
|
+
linkedJiraIssues.set(key, { issue: issue2, comments });
|
|
19961
|
+
const newLinks = collectLinkedIssues(issue2);
|
|
19962
|
+
for (const link of newLinks) {
|
|
19963
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
19964
|
+
visited.add(link.key);
|
|
19965
|
+
queue.push(link.key);
|
|
19966
|
+
}
|
|
19967
|
+
}
|
|
19968
|
+
} else {
|
|
19969
|
+
const batchKey = batch[results.indexOf(result)];
|
|
19970
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
19971
|
+
}
|
|
19972
|
+
}
|
|
19973
|
+
}
|
|
19974
|
+
if (queue.length > 0) {
|
|
19975
|
+
errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
|
|
19976
|
+
}
|
|
19977
|
+
}
|
|
19953
19978
|
const proposedUpdates = [];
|
|
19954
19979
|
const itemReports = [];
|
|
19955
19980
|
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
@@ -20015,6 +20040,23 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20015
20040
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
20016
20041
|
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
20017
20042
|
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
20043
|
+
let itemLinkedIssues = [];
|
|
20044
|
+
const itemLinkedIssueSignals = [];
|
|
20045
|
+
if (options.traverseLinks && jiraData) {
|
|
20046
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
20047
|
+
jiraData.issue,
|
|
20048
|
+
jiraIssues,
|
|
20049
|
+
linkedJiraIssues
|
|
20050
|
+
);
|
|
20051
|
+
itemLinkedIssues = allLinks;
|
|
20052
|
+
itemLinkedIssueSignals.push(...allSignals);
|
|
20053
|
+
analyzeLinkedIssueSignals(
|
|
20054
|
+
allLinks,
|
|
20055
|
+
fm,
|
|
20056
|
+
jiraKey,
|
|
20057
|
+
proposedUpdates
|
|
20058
|
+
);
|
|
20059
|
+
}
|
|
20018
20060
|
const report = {
|
|
20019
20061
|
id: fm.id,
|
|
20020
20062
|
title: fm.title,
|
|
@@ -20033,6 +20075,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20033
20075
|
progressDrift,
|
|
20034
20076
|
commentSignals,
|
|
20035
20077
|
commentSummary: null,
|
|
20078
|
+
linkedIssues: itemLinkedIssues,
|
|
20079
|
+
linkedIssueSignals: itemLinkedIssueSignals,
|
|
20036
20080
|
children: [],
|
|
20037
20081
|
owner: fm.owner ?? null,
|
|
20038
20082
|
focusArea: focusTag ? focusTag.slice(6) : null
|
|
@@ -20076,7 +20120,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20076
20120
|
const focusAreas = [];
|
|
20077
20121
|
for (const [name, items] of focusAreaMap) {
|
|
20078
20122
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
20079
|
-
const doneCount = allFlatItems.filter((i) =>
|
|
20123
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES6.has(i.marvinStatus)).length;
|
|
20080
20124
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
20081
20125
|
const progress = computeWeightedProgress(items);
|
|
20082
20126
|
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
@@ -20128,6 +20172,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20128
20172
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20129
20173
|
}
|
|
20130
20174
|
}
|
|
20175
|
+
if (options.traverseLinks) {
|
|
20176
|
+
try {
|
|
20177
|
+
const linkedSummaries = await analyzeLinkedIssueComments(
|
|
20178
|
+
itemReports,
|
|
20179
|
+
linkedJiraIssues
|
|
20180
|
+
);
|
|
20181
|
+
for (const [artifactId, signalSummaries] of linkedSummaries) {
|
|
20182
|
+
const report = itemReports.find((r) => r.id === artifactId);
|
|
20183
|
+
if (!report) continue;
|
|
20184
|
+
for (const [sourceKey, summary] of signalSummaries) {
|
|
20185
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === sourceKey);
|
|
20186
|
+
if (signal) {
|
|
20187
|
+
signal.commentSummary = summary;
|
|
20188
|
+
}
|
|
20189
|
+
}
|
|
20190
|
+
}
|
|
20191
|
+
} catch (err) {
|
|
20192
|
+
errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20193
|
+
}
|
|
20194
|
+
}
|
|
20131
20195
|
}
|
|
20132
20196
|
const appliedUpdates = [];
|
|
20133
20197
|
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
@@ -20244,6 +20308,155 @@ ${commentTexts}`);
|
|
|
20244
20308
|
}
|
|
20245
20309
|
return summaries;
|
|
20246
20310
|
}
|
|
20311
|
+
function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
|
|
20312
|
+
const allLinks = [];
|
|
20313
|
+
const allSignals = [];
|
|
20314
|
+
const visited = /* @__PURE__ */ new Set([primaryIssue.key]);
|
|
20315
|
+
const directLinks = collectLinkedIssues(primaryIssue).filter((l) => l.relationship !== "subtask");
|
|
20316
|
+
const queue = [...directLinks];
|
|
20317
|
+
for (const link of directLinks) {
|
|
20318
|
+
visited.add(link.key);
|
|
20319
|
+
}
|
|
20320
|
+
while (queue.length > 0) {
|
|
20321
|
+
const link = queue.shift();
|
|
20322
|
+
allLinks.push(link);
|
|
20323
|
+
const linkedData = linkedJiraIssues.get(link.key) ?? primaryIssues.get(link.key);
|
|
20324
|
+
if (!linkedData) continue;
|
|
20325
|
+
const linkedCommentSignals = [];
|
|
20326
|
+
for (const comment of linkedData.comments) {
|
|
20327
|
+
const text = extractCommentText(comment.body);
|
|
20328
|
+
const signals = detectCommentSignals(text);
|
|
20329
|
+
linkedCommentSignals.push(...signals);
|
|
20330
|
+
}
|
|
20331
|
+
if (linkedCommentSignals.length > 0 || linkedData.comments.length > 0) {
|
|
20332
|
+
allSignals.push({
|
|
20333
|
+
sourceKey: link.key,
|
|
20334
|
+
linkType: link.relationship,
|
|
20335
|
+
commentSignals: linkedCommentSignals,
|
|
20336
|
+
commentSummary: null
|
|
20337
|
+
});
|
|
20338
|
+
}
|
|
20339
|
+
const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
|
|
20340
|
+
for (const next of nextLinks) {
|
|
20341
|
+
visited.add(next.key);
|
|
20342
|
+
queue.push(next);
|
|
20343
|
+
}
|
|
20344
|
+
}
|
|
20345
|
+
return { allLinks, allSignals };
|
|
20346
|
+
}
|
|
20347
|
+
var BLOCKER_LINK_PATTERNS = ["blocks", "is blocked by"];
|
|
20348
|
+
var WONT_DO_STATUSES = /* @__PURE__ */ new Set(["wont do", "won't do", "cancelled"]);
|
|
20349
|
+
function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedUpdates) {
|
|
20350
|
+
if (linkedIssues.length === 0) return;
|
|
20351
|
+
const blockerLinks = linkedIssues.filter(
|
|
20352
|
+
(l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
|
|
20353
|
+
);
|
|
20354
|
+
if (blockerLinks.length > 0 && blockerLinks.every((l) => l.isDone) && frontmatter.status === "blocked") {
|
|
20355
|
+
proposedUpdates.push({
|
|
20356
|
+
artifactId: frontmatter.id,
|
|
20357
|
+
field: "status",
|
|
20358
|
+
currentValue: "blocked",
|
|
20359
|
+
proposedValue: "in-progress",
|
|
20360
|
+
reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
|
|
20361
|
+
});
|
|
20362
|
+
}
|
|
20363
|
+
const wontDoLinks = linkedIssues.filter(
|
|
20364
|
+
(l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
|
|
20365
|
+
);
|
|
20366
|
+
if (wontDoLinks.length > 0) {
|
|
20367
|
+
proposedUpdates.push({
|
|
20368
|
+
artifactId: frontmatter.id,
|
|
20369
|
+
field: "review",
|
|
20370
|
+
currentValue: null,
|
|
20371
|
+
proposedValue: "needs-review",
|
|
20372
|
+
reason: `Linked issue(s) cancelled/won't do: ${wontDoLinks.map((l) => `${l.key} "${l.summary}"`).join(", ")}`
|
|
20373
|
+
});
|
|
20374
|
+
}
|
|
20375
|
+
}
|
|
20376
|
+
var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
|
|
20377
|
+
|
|
20378
|
+
For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
|
|
20379
|
+
|
|
20380
|
+
Return your response as a JSON object mapping artifact IDs to objects mapping linked issue keys to summary strings.
|
|
20381
|
+
Example: {"T-001": {"PROJ-301": "DBA review scheduled for Thursday."}}
|
|
20382
|
+
|
|
20383
|
+
IMPORTANT: Only return the JSON object, no other text.`;
|
|
20384
|
+
async function analyzeLinkedIssueComments(items, linkedJiraIssues) {
|
|
20385
|
+
const results = /* @__PURE__ */ new Map();
|
|
20386
|
+
const promptParts = [];
|
|
20387
|
+
const itemsWithLinkedComments = [];
|
|
20388
|
+
for (const item of items) {
|
|
20389
|
+
if (item.linkedIssueSignals.length === 0) continue;
|
|
20390
|
+
const linkedParts = [];
|
|
20391
|
+
for (const signal of item.linkedIssueSignals) {
|
|
20392
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
20393
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
20394
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
20395
|
+
const text = extractCommentText(c.body);
|
|
20396
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
20397
|
+
}).join("\n");
|
|
20398
|
+
linkedParts.push(` ### ${signal.sourceKey} (${signal.linkType})
|
|
20399
|
+
${commentTexts}`);
|
|
20400
|
+
}
|
|
20401
|
+
if (linkedParts.length > 0) {
|
|
20402
|
+
itemsWithLinkedComments.push(item);
|
|
20403
|
+
promptParts.push(`## ${item.id} \u2014 ${item.title}
|
|
20404
|
+
Linked issues:
|
|
20405
|
+
${linkedParts.join("\n")}`);
|
|
20406
|
+
}
|
|
20407
|
+
}
|
|
20408
|
+
if (promptParts.length === 0) return results;
|
|
20409
|
+
const prompt = promptParts.join("\n\n");
|
|
20410
|
+
const llmResult = query2({
|
|
20411
|
+
prompt,
|
|
20412
|
+
options: {
|
|
20413
|
+
systemPrompt: LINKED_COMMENT_ANALYSIS_PROMPT,
|
|
20414
|
+
maxTurns: 1,
|
|
20415
|
+
tools: [],
|
|
20416
|
+
allowedTools: []
|
|
20417
|
+
}
|
|
20418
|
+
});
|
|
20419
|
+
for await (const msg of llmResult) {
|
|
20420
|
+
if (msg.type === "assistant") {
|
|
20421
|
+
const textBlock = msg.message.content.find(
|
|
20422
|
+
(b) => b.type === "text"
|
|
20423
|
+
);
|
|
20424
|
+
if (textBlock) {
|
|
20425
|
+
const parsed = parseLlmJson(textBlock.text);
|
|
20426
|
+
if (parsed) {
|
|
20427
|
+
for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
|
|
20428
|
+
if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
|
|
20429
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
20430
|
+
for (const [key, summary] of Object.entries(linkedSummaries)) {
|
|
20431
|
+
if (typeof summary === "string") {
|
|
20432
|
+
signalMap.set(key, summary);
|
|
20433
|
+
}
|
|
20434
|
+
}
|
|
20435
|
+
if (signalMap.size > 0) {
|
|
20436
|
+
results.set(artifactId, signalMap);
|
|
20437
|
+
}
|
|
20438
|
+
}
|
|
20439
|
+
}
|
|
20440
|
+
}
|
|
20441
|
+
}
|
|
20442
|
+
}
|
|
20443
|
+
}
|
|
20444
|
+
return results;
|
|
20445
|
+
}
|
|
20446
|
+
function parseLlmJson(text) {
|
|
20447
|
+
try {
|
|
20448
|
+
return JSON.parse(text);
|
|
20449
|
+
} catch {
|
|
20450
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
20451
|
+
if (match) {
|
|
20452
|
+
try {
|
|
20453
|
+
return JSON.parse(match[1]);
|
|
20454
|
+
} catch {
|
|
20455
|
+
}
|
|
20456
|
+
}
|
|
20457
|
+
return null;
|
|
20458
|
+
}
|
|
20459
|
+
}
|
|
20247
20460
|
function formatProgressReport(report) {
|
|
20248
20461
|
const parts = [];
|
|
20249
20462
|
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
@@ -20327,7 +20540,7 @@ function formatProgressReport(report) {
|
|
|
20327
20540
|
}
|
|
20328
20541
|
function formatItemLine(parts, item, depth) {
|
|
20329
20542
|
const indent = " ".repeat(depth + 1);
|
|
20330
|
-
const statusIcon =
|
|
20543
|
+
const statusIcon = DONE_STATUSES6.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
20331
20544
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
20332
20545
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
20333
20546
|
const progressLabel = ` ${item.progress}%`;
|
|
@@ -20337,6 +20550,19 @@ function formatItemLine(parts, item, depth) {
|
|
|
20337
20550
|
if (item.commentSummary) {
|
|
20338
20551
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
20339
20552
|
}
|
|
20553
|
+
if (item.linkedIssues.length > 0) {
|
|
20554
|
+
parts.push(`${indent} \u{1F517} Linked Issues:`);
|
|
20555
|
+
for (const link of item.linkedIssues) {
|
|
20556
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
20557
|
+
const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
|
|
20558
|
+
const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
|
|
20559
|
+
parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
|
|
20560
|
+
const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
20561
|
+
if (signal?.commentSummary) {
|
|
20562
|
+
parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
|
|
20563
|
+
}
|
|
20564
|
+
}
|
|
20565
|
+
}
|
|
20340
20566
|
for (const child of item.children) {
|
|
20341
20567
|
formatItemLine(parts, child, depth + 1);
|
|
20342
20568
|
}
|
|
@@ -20346,6 +20572,506 @@ function progressBar(pct) {
|
|
|
20346
20572
|
const empty = 10 - filled;
|
|
20347
20573
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
20348
20574
|
}
|
|
20575
|
+
var MAX_ARTIFACT_NODES = 50;
|
|
20576
|
+
var MAX_LLM_DEPTH = 3;
|
|
20577
|
+
var MAX_LLM_COMMENT_CHARS = 8e3;
|
|
20578
|
+
async function assessArtifact(store, client, host, options) {
|
|
20579
|
+
const visited = /* @__PURE__ */ new Set();
|
|
20580
|
+
return _assessArtifactRecursive(store, client, host, options, visited, 0);
|
|
20581
|
+
}
|
|
20582
|
+
async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
|
|
20583
|
+
const errors = [];
|
|
20584
|
+
if (visited.has(options.artifactId)) {
|
|
20585
|
+
return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
|
|
20586
|
+
}
|
|
20587
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
20588
|
+
return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
|
|
20589
|
+
}
|
|
20590
|
+
visited.add(options.artifactId);
|
|
20591
|
+
const doc = store.get(options.artifactId);
|
|
20592
|
+
if (!doc) {
|
|
20593
|
+
return emptyArtifactReport(options.artifactId, [`Artifact ${options.artifactId} not found`]);
|
|
20594
|
+
}
|
|
20595
|
+
const fm = doc.frontmatter;
|
|
20596
|
+
const jiraKey = fm.jiraKey ?? extractJiraKeyFromTags(fm.tags) ?? null;
|
|
20597
|
+
const tags = fm.tags ?? [];
|
|
20598
|
+
const sprintTag = tags.find((t) => t.startsWith("sprint:"));
|
|
20599
|
+
const sprint = sprintTag ? sprintTag.slice(7) : null;
|
|
20600
|
+
const parent = fm.aboutArtifact ?? null;
|
|
20601
|
+
let jiraStatus = null;
|
|
20602
|
+
let jiraAssignee = null;
|
|
20603
|
+
let proposedMarvinStatus = null;
|
|
20604
|
+
let jiraSubtaskProgress = null;
|
|
20605
|
+
const commentSignals = [];
|
|
20606
|
+
let commentSummary = null;
|
|
20607
|
+
let linkedIssues = [];
|
|
20608
|
+
let linkedIssueSignals = [];
|
|
20609
|
+
const proposedUpdates = [];
|
|
20610
|
+
const jiraIssues = /* @__PURE__ */ new Map();
|
|
20611
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
20612
|
+
if (jiraKey) {
|
|
20613
|
+
try {
|
|
20614
|
+
const [issue2, comments] = await Promise.all([
|
|
20615
|
+
client.getIssueWithLinks(jiraKey),
|
|
20616
|
+
client.getComments(jiraKey)
|
|
20617
|
+
]);
|
|
20618
|
+
jiraIssues.set(jiraKey, { issue: issue2, comments });
|
|
20619
|
+
jiraStatus = issue2.fields.status.name;
|
|
20620
|
+
jiraAssignee = issue2.fields.assignee?.displayName ?? null;
|
|
20621
|
+
const inSprint = isInActiveSprint(store, fm.tags);
|
|
20622
|
+
const resolved = options.statusMap ?? {};
|
|
20623
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
20624
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
20625
|
+
if (subtasks.length > 0) {
|
|
20626
|
+
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
20627
|
+
}
|
|
20628
|
+
for (const comment of comments) {
|
|
20629
|
+
const text = extractCommentText(comment.body);
|
|
20630
|
+
const signals2 = detectCommentSignals(text);
|
|
20631
|
+
commentSignals.push(...signals2);
|
|
20632
|
+
}
|
|
20633
|
+
const jiraVisited = /* @__PURE__ */ new Set([jiraKey]);
|
|
20634
|
+
const queue = [];
|
|
20635
|
+
const directLinks = collectLinkedIssues(issue2).filter((l) => l.relationship !== "subtask");
|
|
20636
|
+
for (const link of directLinks) {
|
|
20637
|
+
if (!jiraVisited.has(link.key)) {
|
|
20638
|
+
jiraVisited.add(link.key);
|
|
20639
|
+
queue.push(link.key);
|
|
20640
|
+
}
|
|
20641
|
+
}
|
|
20642
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
20643
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
20644
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
20645
|
+
const results = await Promise.allSettled(
|
|
20646
|
+
batch.map(async (key) => {
|
|
20647
|
+
const [li, lc] = await Promise.all([
|
|
20648
|
+
client.getIssueWithLinks(key),
|
|
20649
|
+
client.getComments(key)
|
|
20650
|
+
]);
|
|
20651
|
+
return { key, issue: li, comments: lc };
|
|
20652
|
+
})
|
|
20653
|
+
);
|
|
20654
|
+
for (const result of results) {
|
|
20655
|
+
if (result.status === "fulfilled") {
|
|
20656
|
+
const { key, issue: li, comments: lc } = result.value;
|
|
20657
|
+
linkedJiraIssues.set(key, { issue: li, comments: lc });
|
|
20658
|
+
const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
|
|
20659
|
+
for (const nl of newLinks) {
|
|
20660
|
+
jiraVisited.add(nl.key);
|
|
20661
|
+
queue.push(nl.key);
|
|
20662
|
+
}
|
|
20663
|
+
} else {
|
|
20664
|
+
const batchKey = batch[results.indexOf(result)];
|
|
20665
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
20666
|
+
}
|
|
20667
|
+
}
|
|
20668
|
+
}
|
|
20669
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
20670
|
+
issue2,
|
|
20671
|
+
jiraIssues,
|
|
20672
|
+
linkedJiraIssues
|
|
20673
|
+
);
|
|
20674
|
+
linkedIssues = allLinks;
|
|
20675
|
+
linkedIssueSignals = allSignals;
|
|
20676
|
+
analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
|
|
20677
|
+
} catch (err) {
|
|
20678
|
+
errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20679
|
+
}
|
|
20680
|
+
}
|
|
20681
|
+
const currentProgress = getEffectiveProgress(fm);
|
|
20682
|
+
const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
|
|
20683
|
+
const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
|
|
20684
|
+
if (statusDrift && proposedMarvinStatus) {
|
|
20685
|
+
proposedUpdates.push({
|
|
20686
|
+
artifactId: fm.id,
|
|
20687
|
+
field: "status",
|
|
20688
|
+
currentValue: fm.status,
|
|
20689
|
+
proposedValue: proposedMarvinStatus,
|
|
20690
|
+
reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
|
|
20691
|
+
});
|
|
20692
|
+
}
|
|
20693
|
+
if (progressDrift && jiraSubtaskProgress !== null) {
|
|
20694
|
+
proposedUpdates.push({
|
|
20695
|
+
artifactId: fm.id,
|
|
20696
|
+
field: "progress",
|
|
20697
|
+
currentValue: currentProgress,
|
|
20698
|
+
proposedValue: jiraSubtaskProgress,
|
|
20699
|
+
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
20700
|
+
});
|
|
20701
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
20702
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
20703
|
+
if (!hasExplicitProgress) {
|
|
20704
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
20705
|
+
if (proposedProgress !== currentProgress) {
|
|
20706
|
+
proposedUpdates.push({
|
|
20707
|
+
artifactId: fm.id,
|
|
20708
|
+
field: "progress",
|
|
20709
|
+
currentValue: currentProgress,
|
|
20710
|
+
proposedValue: proposedProgress,
|
|
20711
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
20712
|
+
});
|
|
20713
|
+
}
|
|
20714
|
+
}
|
|
20715
|
+
}
|
|
20716
|
+
const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
|
|
20717
|
+
if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
|
|
20718
|
+
const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
|
|
20719
|
+
if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
|
|
20720
|
+
try {
|
|
20721
|
+
const summary = await analyzeSingleArtifactComments(
|
|
20722
|
+
fm.id,
|
|
20723
|
+
fm.title,
|
|
20724
|
+
jiraKey,
|
|
20725
|
+
jiraStatus,
|
|
20726
|
+
jiraIssues,
|
|
20727
|
+
linkedJiraIssues,
|
|
20728
|
+
linkedIssueSignals
|
|
20729
|
+
);
|
|
20730
|
+
commentSummary = summary;
|
|
20731
|
+
} catch (err) {
|
|
20732
|
+
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20733
|
+
}
|
|
20734
|
+
}
|
|
20735
|
+
}
|
|
20736
|
+
const childIds = findChildIds(store, fm);
|
|
20737
|
+
const children = [];
|
|
20738
|
+
for (const childId of childIds) {
|
|
20739
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
20740
|
+
errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
|
|
20741
|
+
break;
|
|
20742
|
+
}
|
|
20743
|
+
const childReport = await _assessArtifactRecursive(
|
|
20744
|
+
store,
|
|
20745
|
+
client,
|
|
20746
|
+
host,
|
|
20747
|
+
{ ...options, artifactId: childId },
|
|
20748
|
+
visited,
|
|
20749
|
+
depth + 1
|
|
20750
|
+
);
|
|
20751
|
+
children.push(childReport);
|
|
20752
|
+
}
|
|
20753
|
+
const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
|
|
20754
|
+
const appliedUpdates = [];
|
|
20755
|
+
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
20756
|
+
for (const update of proposedUpdates) {
|
|
20757
|
+
if (update.field === "review") continue;
|
|
20758
|
+
try {
|
|
20759
|
+
store.update(update.artifactId, {
|
|
20760
|
+
[update.field]: update.proposedValue,
|
|
20761
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20762
|
+
});
|
|
20763
|
+
const updatedDoc = store.get(update.artifactId);
|
|
20764
|
+
if (updatedDoc) {
|
|
20765
|
+
if (updatedDoc.frontmatter.type === "task") {
|
|
20766
|
+
propagateProgressFromTask(store, update.artifactId);
|
|
20767
|
+
} else if (updatedDoc.frontmatter.type === "action") {
|
|
20768
|
+
propagateProgressToAction(store, update.artifactId);
|
|
20769
|
+
}
|
|
20770
|
+
}
|
|
20771
|
+
appliedUpdates.push(update);
|
|
20772
|
+
} catch (err) {
|
|
20773
|
+
errors.push(`Failed to apply update: ${err instanceof Error ? err.message : String(err)}`);
|
|
20774
|
+
}
|
|
20775
|
+
}
|
|
20776
|
+
}
|
|
20777
|
+
return {
|
|
20778
|
+
artifactId: fm.id,
|
|
20779
|
+
title: fm.title,
|
|
20780
|
+
type: fm.type,
|
|
20781
|
+
marvinStatus: fm.status,
|
|
20782
|
+
marvinProgress: currentProgress,
|
|
20783
|
+
sprint,
|
|
20784
|
+
parent,
|
|
20785
|
+
jiraKey,
|
|
20786
|
+
jiraStatus,
|
|
20787
|
+
jiraAssignee,
|
|
20788
|
+
jiraSubtaskProgress,
|
|
20789
|
+
proposedMarvinStatus,
|
|
20790
|
+
statusDrift,
|
|
20791
|
+
progressDrift,
|
|
20792
|
+
commentSignals,
|
|
20793
|
+
commentSummary,
|
|
20794
|
+
linkedIssues,
|
|
20795
|
+
linkedIssueSignals,
|
|
20796
|
+
children,
|
|
20797
|
+
proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
|
|
20798
|
+
appliedUpdates,
|
|
20799
|
+
signals,
|
|
20800
|
+
errors
|
|
20801
|
+
};
|
|
20802
|
+
}
|
|
20803
|
+
function findChildIds(store, fm) {
|
|
20804
|
+
if (fm.type === "action") {
|
|
20805
|
+
return store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === fm.id).map((d) => d.frontmatter.id);
|
|
20806
|
+
}
|
|
20807
|
+
if (fm.type === "epic") {
|
|
20808
|
+
const epicTag = `epic:${fm.id}`;
|
|
20809
|
+
const isLinked = (d) => {
|
|
20810
|
+
const le = d.frontmatter.linkedEpic;
|
|
20811
|
+
if (le?.includes(fm.id)) return true;
|
|
20812
|
+
const t = d.frontmatter.tags ?? [];
|
|
20813
|
+
return t.includes(epicTag);
|
|
20814
|
+
};
|
|
20815
|
+
return [
|
|
20816
|
+
...store.list({ type: "action" }).filter(isLinked),
|
|
20817
|
+
...store.list({ type: "task" }).filter(isLinked)
|
|
20818
|
+
].map((d) => d.frontmatter.id);
|
|
20819
|
+
}
|
|
20820
|
+
return [];
|
|
20821
|
+
}
|
|
20822
|
+
function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus) {
|
|
20823
|
+
const signals = [];
|
|
20824
|
+
const blockerSignals = commentSignals.filter((s) => s.type === "blocker");
|
|
20825
|
+
if (blockerSignals.length > 0) {
|
|
20826
|
+
for (const s of blockerSignals) {
|
|
20827
|
+
signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
|
|
20828
|
+
}
|
|
20829
|
+
}
|
|
20830
|
+
const blockingLinks = linkedIssues.filter(
|
|
20831
|
+
(l) => l.relationship.toLowerCase().includes("block")
|
|
20832
|
+
);
|
|
20833
|
+
const activeBlockers = blockingLinks.filter((l) => !l.isDone);
|
|
20834
|
+
const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
|
|
20835
|
+
if (activeBlockers.length > 0) {
|
|
20836
|
+
for (const b of activeBlockers) {
|
|
20837
|
+
signals.push(`\u{1F6AB} Blocker \u2014 ${b.relationship} ${b.key} "${b.summary}" [${b.status}]`);
|
|
20838
|
+
}
|
|
20839
|
+
}
|
|
20840
|
+
if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
|
|
20841
|
+
signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
|
|
20842
|
+
}
|
|
20843
|
+
const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
|
|
20844
|
+
for (const l of wontDoLinks) {
|
|
20845
|
+
signals.push(`\u{1F504} Superseded \u2014 ${l.key} "${l.summary}" is ${l.status}`);
|
|
20846
|
+
}
|
|
20847
|
+
const questionSignals = commentSignals.filter((s) => s.type === "question");
|
|
20848
|
+
for (const s of questionSignals) {
|
|
20849
|
+
signals.push(`\u23F3 Waiting \u2014 "${s.snippet}"`);
|
|
20850
|
+
}
|
|
20851
|
+
const relatedInProgress = linkedIssues.filter(
|
|
20852
|
+
(l) => l.relationship.toLowerCase().includes("relate") && !l.isDone
|
|
20853
|
+
);
|
|
20854
|
+
if (relatedInProgress.length > 0) {
|
|
20855
|
+
for (const l of relatedInProgress) {
|
|
20856
|
+
signals.push(`\u{1F4CB} Handoff \u2014 related work on ${l.key} "${l.summary}" [${l.status}]`);
|
|
20857
|
+
}
|
|
20858
|
+
}
|
|
20859
|
+
if (signals.length === 0) {
|
|
20860
|
+
if (statusDrift && proposedStatus) {
|
|
20861
|
+
signals.push(`\u26A0 Drift detected \u2014 Marvin and Jira statuses diverge`);
|
|
20862
|
+
} else {
|
|
20863
|
+
signals.push(`\u2705 No active blockers or concerns detected`);
|
|
20864
|
+
}
|
|
20865
|
+
}
|
|
20866
|
+
return signals;
|
|
20867
|
+
}
|
|
20868
|
+
function estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
20869
|
+
let total = 0;
|
|
20870
|
+
for (const [, data] of jiraIssues) {
|
|
20871
|
+
for (const c of data.comments) {
|
|
20872
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
20873
|
+
}
|
|
20874
|
+
}
|
|
20875
|
+
for (const signal of linkedIssueSignals) {
|
|
20876
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
20877
|
+
if (!linkedData) continue;
|
|
20878
|
+
for (const c of linkedData.comments) {
|
|
20879
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
20880
|
+
}
|
|
20881
|
+
}
|
|
20882
|
+
return total;
|
|
20883
|
+
}
|
|
20884
|
+
var SINGLE_ARTIFACT_COMMENT_PROMPT = `You are a delivery management assistant analyzing Jira comments for a single work item.
|
|
20885
|
+
|
|
20886
|
+
Produce a 2-3 sentence progress summary covering:
|
|
20887
|
+
- What work has been completed
|
|
20888
|
+
- What is pending or blocked
|
|
20889
|
+
- Any decisions, handoffs, or scheduling mentioned
|
|
20890
|
+
- Relevant context from linked issue comments (if provided)
|
|
20891
|
+
|
|
20892
|
+
Return ONLY the summary text, no JSON or formatting.`;
|
|
20893
|
+
async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraStatus, jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
20894
|
+
const promptParts = [];
|
|
20895
|
+
const primaryData = jiraIssues.get(jiraKey);
|
|
20896
|
+
if (primaryData && primaryData.comments.length > 0) {
|
|
20897
|
+
const commentTexts = primaryData.comments.map((c) => {
|
|
20898
|
+
const text = extractCommentText(c.body);
|
|
20899
|
+
return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
|
|
20900
|
+
}).join("\n");
|
|
20901
|
+
promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
|
|
20902
|
+
Comments:
|
|
20903
|
+
${commentTexts}`);
|
|
20904
|
+
}
|
|
20905
|
+
for (const signal of linkedIssueSignals) {
|
|
20906
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
20907
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
20908
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
20909
|
+
const text = extractCommentText(c.body);
|
|
20910
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
20911
|
+
}).join("\n");
|
|
20912
|
+
promptParts.push(`### Linked: ${signal.sourceKey} (${signal.linkType})
|
|
20913
|
+
${commentTexts}`);
|
|
20914
|
+
}
|
|
20915
|
+
if (promptParts.length === 0) return null;
|
|
20916
|
+
const prompt = promptParts.join("\n\n");
|
|
20917
|
+
const result = query2({
|
|
20918
|
+
prompt,
|
|
20919
|
+
options: {
|
|
20920
|
+
systemPrompt: SINGLE_ARTIFACT_COMMENT_PROMPT,
|
|
20921
|
+
maxTurns: 1,
|
|
20922
|
+
tools: [],
|
|
20923
|
+
allowedTools: []
|
|
20924
|
+
}
|
|
20925
|
+
});
|
|
20926
|
+
for await (const msg of result) {
|
|
20927
|
+
if (msg.type === "assistant") {
|
|
20928
|
+
const textBlock = msg.message.content.find(
|
|
20929
|
+
(b) => b.type === "text"
|
|
20930
|
+
);
|
|
20931
|
+
if (textBlock) {
|
|
20932
|
+
return textBlock.text.trim();
|
|
20933
|
+
}
|
|
20934
|
+
}
|
|
20935
|
+
}
|
|
20936
|
+
return null;
|
|
20937
|
+
}
|
|
20938
|
+
function emptyArtifactReport(artifactId, errors) {
|
|
20939
|
+
return {
|
|
20940
|
+
artifactId,
|
|
20941
|
+
title: "Not found",
|
|
20942
|
+
type: "unknown",
|
|
20943
|
+
marvinStatus: "unknown",
|
|
20944
|
+
marvinProgress: 0,
|
|
20945
|
+
sprint: null,
|
|
20946
|
+
parent: null,
|
|
20947
|
+
jiraKey: null,
|
|
20948
|
+
jiraStatus: null,
|
|
20949
|
+
jiraAssignee: null,
|
|
20950
|
+
jiraSubtaskProgress: null,
|
|
20951
|
+
proposedMarvinStatus: null,
|
|
20952
|
+
statusDrift: false,
|
|
20953
|
+
progressDrift: false,
|
|
20954
|
+
commentSignals: [],
|
|
20955
|
+
commentSummary: null,
|
|
20956
|
+
linkedIssues: [],
|
|
20957
|
+
linkedIssueSignals: [],
|
|
20958
|
+
children: [],
|
|
20959
|
+
proposedUpdates: [],
|
|
20960
|
+
appliedUpdates: [],
|
|
20961
|
+
signals: [],
|
|
20962
|
+
errors
|
|
20963
|
+
};
|
|
20964
|
+
}
|
|
20965
|
+
function formatArtifactReport(report) {
|
|
20966
|
+
const parts = [];
|
|
20967
|
+
parts.push(`# Artifact Assessment \u2014 ${report.artifactId}`);
|
|
20968
|
+
parts.push(report.title);
|
|
20969
|
+
parts.push("");
|
|
20970
|
+
parts.push(`## Marvin State`);
|
|
20971
|
+
const marvinParts = [`Status: ${report.marvinStatus}`, `Progress: ${report.marvinProgress}%`];
|
|
20972
|
+
if (report.sprint) marvinParts.push(`Sprint: ${report.sprint}`);
|
|
20973
|
+
if (report.parent) marvinParts.push(`Parent: ${report.parent}`);
|
|
20974
|
+
parts.push(marvinParts.join(" | "));
|
|
20975
|
+
parts.push("");
|
|
20976
|
+
if (report.jiraKey) {
|
|
20977
|
+
parts.push(`## Jira State (${report.jiraKey})`);
|
|
20978
|
+
const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
|
|
20979
|
+
if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
|
|
20980
|
+
if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
|
|
20981
|
+
parts.push(jiraParts.join(" | "));
|
|
20982
|
+
if (report.statusDrift) {
|
|
20983
|
+
parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
|
|
20984
|
+
}
|
|
20985
|
+
if (report.progressDrift && report.jiraSubtaskProgress !== null) {
|
|
20986
|
+
parts.push(`\u26A0 Progress drift: ${report.marvinProgress}% \u2192 ${report.jiraSubtaskProgress}%`);
|
|
20987
|
+
}
|
|
20988
|
+
parts.push("");
|
|
20989
|
+
}
|
|
20990
|
+
if (report.commentSummary) {
|
|
20991
|
+
parts.push(`## Comments`);
|
|
20992
|
+
parts.push(report.commentSummary);
|
|
20993
|
+
parts.push("");
|
|
20994
|
+
}
|
|
20995
|
+
if (report.children.length > 0) {
|
|
20996
|
+
const doneCount = report.children.filter((c) => DONE_STATUSES6.has(c.marvinStatus)).length;
|
|
20997
|
+
const childWeights = report.children.map((c) => {
|
|
20998
|
+
const { weight } = resolveWeight(void 0);
|
|
20999
|
+
return { weight, progress: c.marvinProgress };
|
|
21000
|
+
});
|
|
21001
|
+
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;
|
|
21002
|
+
const bar = progressBar(childProgress);
|
|
21003
|
+
parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
|
|
21004
|
+
for (const child of report.children) {
|
|
21005
|
+
formatArtifactChild(parts, child, 1);
|
|
21006
|
+
}
|
|
21007
|
+
parts.push("");
|
|
21008
|
+
}
|
|
21009
|
+
if (report.linkedIssues.length > 0) {
|
|
21010
|
+
parts.push(`## Linked Issues (${report.linkedIssues.length})`);
|
|
21011
|
+
for (const link of report.linkedIssues) {
|
|
21012
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
21013
|
+
parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
|
|
21014
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
21015
|
+
if (signal?.commentSummary) {
|
|
21016
|
+
parts.push(` \u{1F4AC} ${signal.commentSummary}`);
|
|
21017
|
+
}
|
|
21018
|
+
}
|
|
21019
|
+
parts.push("");
|
|
21020
|
+
}
|
|
21021
|
+
if (report.signals.length > 0) {
|
|
21022
|
+
parts.push(`## Signals`);
|
|
21023
|
+
for (const s of report.signals) {
|
|
21024
|
+
parts.push(` ${s}`);
|
|
21025
|
+
}
|
|
21026
|
+
parts.push("");
|
|
21027
|
+
}
|
|
21028
|
+
if (report.proposedUpdates.length > 0) {
|
|
21029
|
+
parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
|
|
21030
|
+
for (const update of report.proposedUpdates) {
|
|
21031
|
+
parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
21032
|
+
parts.push(` Reason: ${update.reason}`);
|
|
21033
|
+
}
|
|
21034
|
+
parts.push("");
|
|
21035
|
+
parts.push("Run with applyUpdates=true to apply these changes.");
|
|
21036
|
+
parts.push("");
|
|
21037
|
+
}
|
|
21038
|
+
if (report.appliedUpdates.length > 0) {
|
|
21039
|
+
parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
|
|
21040
|
+
for (const update of report.appliedUpdates) {
|
|
21041
|
+
parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
21042
|
+
}
|
|
21043
|
+
parts.push("");
|
|
21044
|
+
}
|
|
21045
|
+
if (report.errors.length > 0) {
|
|
21046
|
+
parts.push(`## Errors`);
|
|
21047
|
+
for (const err of report.errors) {
|
|
21048
|
+
parts.push(` ${err}`);
|
|
21049
|
+
}
|
|
21050
|
+
parts.push("");
|
|
21051
|
+
}
|
|
21052
|
+
return parts.join("\n");
|
|
21053
|
+
}
|
|
21054
|
+
function formatArtifactChild(parts, child, depth) {
|
|
21055
|
+
const indent = " ".repeat(depth);
|
|
21056
|
+
const icon = DONE_STATUSES6.has(child.marvinStatus) ? "\u2713" : child.marvinStatus === "blocked" ? "\u{1F6AB}" : child.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
21057
|
+
const jiraLabel = child.jiraKey ? ` [${child.jiraKey}: ${child.jiraStatus ?? "?"}]` : "";
|
|
21058
|
+
const driftLabel = child.statusDrift ? ` \u26A0drift \u2192 ${child.proposedMarvinStatus}` : "";
|
|
21059
|
+
const signalHints = [];
|
|
21060
|
+
for (const s of child.signals) {
|
|
21061
|
+
if (s.startsWith("\u2705 No active")) continue;
|
|
21062
|
+
signalHints.push(s);
|
|
21063
|
+
}
|
|
21064
|
+
parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
|
|
21065
|
+
if (child.commentSummary) {
|
|
21066
|
+
parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
|
|
21067
|
+
}
|
|
21068
|
+
for (const hint of signalHints) {
|
|
21069
|
+
parts.push(`${indent} ${hint}`);
|
|
21070
|
+
}
|
|
21071
|
+
for (const grandchild of child.children) {
|
|
21072
|
+
formatArtifactChild(parts, grandchild, depth + 1);
|
|
21073
|
+
}
|
|
21074
|
+
}
|
|
20349
21075
|
|
|
20350
21076
|
// src/skills/builtin/jira/tools.ts
|
|
20351
21077
|
var JIRA_TYPE = "jira-issue";
|
|
@@ -21132,7 +21858,8 @@ function createJiraTools(store, projectConfig) {
|
|
|
21132
21858
|
{
|
|
21133
21859
|
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
21134
21860
|
analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
|
|
21135
|
-
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
|
|
21861
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)"),
|
|
21862
|
+
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)")
|
|
21136
21863
|
},
|
|
21137
21864
|
async (args) => {
|
|
21138
21865
|
const jira = createJiraClient(jiraUserConfig);
|
|
@@ -21145,6 +21872,7 @@ function createJiraTools(store, projectConfig) {
|
|
|
21145
21872
|
sprintId: args.sprintId,
|
|
21146
21873
|
analyzeComments: args.analyzeComments ?? false,
|
|
21147
21874
|
applyUpdates: args.applyUpdates ?? false,
|
|
21875
|
+
traverseLinks: args.traverseLinks ?? false,
|
|
21148
21876
|
statusMap
|
|
21149
21877
|
}
|
|
21150
21878
|
);
|
|
@@ -21154,6 +21882,34 @@ function createJiraTools(store, projectConfig) {
|
|
|
21154
21882
|
};
|
|
21155
21883
|
},
|
|
21156
21884
|
{ annotations: { readOnlyHint: false } }
|
|
21885
|
+
),
|
|
21886
|
+
// --- Single-artifact assessment ---
|
|
21887
|
+
tool20(
|
|
21888
|
+
"assess_artifact",
|
|
21889
|
+
"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).",
|
|
21890
|
+
{
|
|
21891
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
|
|
21892
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
|
|
21893
|
+
},
|
|
21894
|
+
async (args) => {
|
|
21895
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
21896
|
+
if (!jira) return jiraNotConfiguredError();
|
|
21897
|
+
const report = await assessArtifact(
|
|
21898
|
+
store,
|
|
21899
|
+
jira.client,
|
|
21900
|
+
jira.host,
|
|
21901
|
+
{
|
|
21902
|
+
artifactId: args.artifactId,
|
|
21903
|
+
applyUpdates: args.applyUpdates ?? false,
|
|
21904
|
+
statusMap
|
|
21905
|
+
}
|
|
21906
|
+
);
|
|
21907
|
+
return {
|
|
21908
|
+
content: [{ type: "text", text: formatArtifactReport(report) }],
|
|
21909
|
+
isError: report.errors.length > 0 && report.type === "unknown"
|
|
21910
|
+
};
|
|
21911
|
+
},
|
|
21912
|
+
{ annotations: { readOnlyHint: false } }
|
|
21157
21913
|
)
|
|
21158
21914
|
];
|
|
21159
21915
|
}
|
|
@@ -21255,7 +22011,8 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
21255
22011
|
- \`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.
|
|
21256
22012
|
- \`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.
|
|
21257
22013
|
- \`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).
|
|
21258
|
-
- \`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.
|
|
22014
|
+
- \`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.
|
|
22015
|
+
- \`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.
|
|
21259
22016
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
21260
22017
|
- \`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.
|
|
21261
22018
|
- \`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).
|
|
@@ -21272,6 +22029,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
21272
22029
|
2. Review focus area rollups, status drift, and blockers
|
|
21273
22030
|
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
21274
22031
|
|
|
22032
|
+
**Single-artifact deep dive:**
|
|
22033
|
+
1. Call \`assess_artifact\` with an artifact ID to get a focused assessment with Jira sync, comment analysis, link traversal, and child rollup
|
|
22034
|
+
2. Review signals (blockers, unblocks, handoffs) and proposed updates
|
|
22035
|
+
3. Use \`applyUpdates=true\` to apply changes
|
|
22036
|
+
|
|
21275
22037
|
**Daily review workflow:**
|
|
21276
22038
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
21277
22039
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -21296,6 +22058,7 @@ ${COMMON_WORKFLOW}
|
|
|
21296
22058
|
**As Product Owner, use Jira integration to:**
|
|
21297
22059
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
21298
22060
|
- Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
|
|
22061
|
+
- Use \`assess_artifact\` for deep dives into specific features or epics \u2014 full Jira context, linked issues, and child rollup
|
|
21299
22062
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
21300
22063
|
- Push approved features as Stories for development tracking
|
|
21301
22064
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -21309,6 +22072,7 @@ ${COMMON_WORKFLOW}
|
|
|
21309
22072
|
**As Tech Lead, use Jira integration to:**
|
|
21310
22073
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
21311
22074
|
- Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
|
|
22075
|
+
- Use \`assess_artifact\` to investigate a specific task \u2014 see full dependency chain, comment context, and blocker status
|
|
21312
22076
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
21313
22077
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
21314
22078
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -21324,6 +22088,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
21324
22088
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
21325
22089
|
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
21326
22090
|
- Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
|
|
22091
|
+
- Use \`assess_artifact\` for focused status checks on critical items \u2014 full Jira context without running the whole sprint assessment
|
|
21327
22092
|
- Pull sprint issues for tracking progress and blockers
|
|
21328
22093
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
21329
22094
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -24718,7 +25483,7 @@ function buildHealthGauge(categories) {
|
|
|
24718
25483
|
}
|
|
24719
25484
|
|
|
24720
25485
|
// src/web/templates/pages/po/dashboard.ts
|
|
24721
|
-
var
|
|
25486
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24722
25487
|
var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
24723
25488
|
function poDashboardPage(ctx) {
|
|
24724
25489
|
const overview = getOverviewData(ctx.store);
|
|
@@ -24763,7 +25528,7 @@ function poDashboardPage(ctx) {
|
|
|
24763
25528
|
sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
|
|
24764
25529
|
}
|
|
24765
25530
|
}
|
|
24766
|
-
const featuresDone = features.filter((d) =>
|
|
25531
|
+
const featuresDone = features.filter((d) => DONE_STATUSES7.has(d.frontmatter.status)).length;
|
|
24767
25532
|
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
24768
25533
|
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
24769
25534
|
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
|
|
@@ -24832,7 +25597,7 @@ function poDashboardPage(ctx) {
|
|
|
24832
25597
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
24833
25598
|
const atRiskItems = [];
|
|
24834
25599
|
for (const f of features) {
|
|
24835
|
-
if (
|
|
25600
|
+
if (DONE_STATUSES7.has(f.frontmatter.status)) continue;
|
|
24836
25601
|
const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
|
|
24837
25602
|
const reasons = [];
|
|
24838
25603
|
let blocked = 0;
|
|
@@ -24844,7 +25609,7 @@ function poDashboardPage(ctx) {
|
|
|
24844
25609
|
if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
|
|
24845
25610
|
for (const epic of fEpics) {
|
|
24846
25611
|
const td = epic.frontmatter.targetDate;
|
|
24847
|
-
if (td && td < today && !
|
|
25612
|
+
if (td && td < today && !DONE_STATUSES7.has(epic.frontmatter.status)) {
|
|
24848
25613
|
reasons.push(`${epic.frontmatter.id} overdue`);
|
|
24849
25614
|
}
|
|
24850
25615
|
}
|
|
@@ -25122,7 +25887,7 @@ function poBacklogPage(ctx) {
|
|
|
25122
25887
|
}
|
|
25123
25888
|
}
|
|
25124
25889
|
}
|
|
25125
|
-
const
|
|
25890
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25126
25891
|
function featureTaskStats(featureId) {
|
|
25127
25892
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
25128
25893
|
let total = 0;
|
|
@@ -25131,7 +25896,7 @@ function poBacklogPage(ctx) {
|
|
|
25131
25896
|
for (const epic of fEpics) {
|
|
25132
25897
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
25133
25898
|
total++;
|
|
25134
|
-
if (
|
|
25899
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
25135
25900
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
25136
25901
|
}
|
|
25137
25902
|
}
|
|
@@ -25377,23 +26142,34 @@ function hashString(s) {
|
|
|
25377
26142
|
}
|
|
25378
26143
|
return Math.abs(h);
|
|
25379
26144
|
}
|
|
26145
|
+
var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
|
|
26146
|
+
var DEFAULT_WEIGHT3 = 3;
|
|
25380
26147
|
function countFocusStats(items) {
|
|
25381
26148
|
let total = 0;
|
|
25382
26149
|
let done = 0;
|
|
25383
26150
|
let inProgress = 0;
|
|
25384
|
-
|
|
26151
|
+
let totalWeight = 0;
|
|
26152
|
+
let weightedSum = 0;
|
|
26153
|
+
function walkStats(list) {
|
|
25385
26154
|
for (const w of list) {
|
|
25386
26155
|
if (w.type !== "contribution") {
|
|
25387
26156
|
total++;
|
|
25388
26157
|
const s = w.status.toLowerCase();
|
|
25389
|
-
if (s
|
|
26158
|
+
if (DONE_STATUS_SET.has(s)) done++;
|
|
25390
26159
|
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
25391
26160
|
}
|
|
25392
|
-
if (w.children)
|
|
26161
|
+
if (w.children) walkStats(w.children);
|
|
25393
26162
|
}
|
|
25394
26163
|
}
|
|
25395
|
-
|
|
25396
|
-
|
|
26164
|
+
walkStats(items);
|
|
26165
|
+
for (const w of items) {
|
|
26166
|
+
if (w.type === "contribution") continue;
|
|
26167
|
+
const weight = w.weight ?? DEFAULT_WEIGHT3;
|
|
26168
|
+
const progress = w.progress ?? (DONE_STATUS_SET.has(w.status.toLowerCase()) ? 100 : 0);
|
|
26169
|
+
totalWeight += weight;
|
|
26170
|
+
weightedSum += weight * progress;
|
|
26171
|
+
}
|
|
26172
|
+
return { total, done, inProgress, weightedProgress: totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 };
|
|
25397
26173
|
}
|
|
25398
26174
|
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
25399
26175
|
function ownerBadge2(owner) {
|
|
@@ -25442,7 +26218,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
25442
26218
|
for (const [focus, groupItems] of focusGroups) {
|
|
25443
26219
|
const color = focusColorMap.get(focus);
|
|
25444
26220
|
const stats = countFocusStats(groupItems);
|
|
25445
|
-
const pct = stats.
|
|
26221
|
+
const pct = stats.weightedProgress;
|
|
25446
26222
|
const summaryParts = [];
|
|
25447
26223
|
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
25448
26224
|
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
@@ -25485,7 +26261,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
25485
26261
|
{ titleTag: "h3", defaultCollapsed }
|
|
25486
26262
|
);
|
|
25487
26263
|
}
|
|
25488
|
-
var
|
|
26264
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
|
|
25489
26265
|
function computeOwnerCompletionPct(items, owner) {
|
|
25490
26266
|
let total = 0;
|
|
25491
26267
|
let progressSum = 0;
|
|
@@ -25493,7 +26269,7 @@ function computeOwnerCompletionPct(items, owner) {
|
|
|
25493
26269
|
for (const w of list) {
|
|
25494
26270
|
if (w.type !== "contribution" && w.owner === owner) {
|
|
25495
26271
|
total++;
|
|
25496
|
-
progressSum += w.progress ?? (
|
|
26272
|
+
progressSum += w.progress ?? (DONE_STATUSES8.has(w.status) ? 100 : 0);
|
|
25497
26273
|
}
|
|
25498
26274
|
if (w.children) walk(w.children);
|
|
25499
26275
|
}
|
|
@@ -25514,7 +26290,7 @@ function filterItemsByOwner(items, owner) {
|
|
|
25514
26290
|
}
|
|
25515
26291
|
|
|
25516
26292
|
// src/web/templates/pages/po/delivery.ts
|
|
25517
|
-
var
|
|
26293
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25518
26294
|
var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
25519
26295
|
var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
25520
26296
|
function priorityClass2(p) {
|
|
@@ -25655,7 +26431,7 @@ function poDeliveryPage(ctx) {
|
|
|
25655
26431
|
}
|
|
25656
26432
|
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
25657
26433
|
}
|
|
25658
|
-
const nonDoneFeatures = features.filter((f) => !
|
|
26434
|
+
const nonDoneFeatures = features.filter((f) => !DONE_STATUSES9.has(f.frontmatter.status)).sort((a, b) => {
|
|
25659
26435
|
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
25660
26436
|
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
25661
26437
|
if (pa !== pb) return pa - pb;
|
|
@@ -25864,7 +26640,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
|
25864
26640
|
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
25865
26641
|
|
|
25866
26642
|
// src/web/templates/pages/dm/dashboard.ts
|
|
25867
|
-
var
|
|
26643
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25868
26644
|
function progressBar3(pct) {
|
|
25869
26645
|
return `<div class="sprint-progress-bar">
|
|
25870
26646
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -25875,7 +26651,7 @@ function dmDashboardPage(ctx) {
|
|
|
25875
26651
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
25876
26652
|
const upcoming = getUpcomingData(ctx.store);
|
|
25877
26653
|
const actions = ctx.store.list({ type: "action" });
|
|
25878
|
-
const openActions = actions.filter((d) => !
|
|
26654
|
+
const openActions = actions.filter((d) => !DONE_STATUSES10.has(d.frontmatter.status));
|
|
25879
26655
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
25880
26656
|
const statsCards = `
|
|
25881
26657
|
<div class="cards">
|
|
@@ -26096,7 +26872,7 @@ function dmSprintPage(ctx) {
|
|
|
26096
26872
|
}
|
|
26097
26873
|
|
|
26098
26874
|
// src/web/templates/pages/dm/actions.ts
|
|
26099
|
-
var
|
|
26875
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26100
26876
|
function urgencyBadge(tier) {
|
|
26101
26877
|
const labels = {
|
|
26102
26878
|
overdue: "Overdue",
|
|
@@ -26116,7 +26892,7 @@ function urgencyRowClass(tier) {
|
|
|
26116
26892
|
function dmActionsPage(ctx) {
|
|
26117
26893
|
const upcoming = getUpcomingData(ctx.store);
|
|
26118
26894
|
const allActions = ctx.store.list({ type: "action" });
|
|
26119
|
-
const openActions = allActions.filter((d) => !
|
|
26895
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
|
|
26120
26896
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
26121
26897
|
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
26122
26898
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
@@ -26201,7 +26977,7 @@ function dmActionsPage(ctx) {
|
|
|
26201
26977
|
}
|
|
26202
26978
|
|
|
26203
26979
|
// src/web/templates/pages/dm/risks.ts
|
|
26204
|
-
var
|
|
26980
|
+
var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26205
26981
|
function dmRisksPage(ctx) {
|
|
26206
26982
|
const allDocs = ctx.store.list();
|
|
26207
26983
|
const upcoming = getUpcomingData(ctx.store);
|
|
@@ -26212,7 +26988,7 @@ function dmRisksPage(ctx) {
|
|
|
26212
26988
|
const todayMs = new Date(today).getTime();
|
|
26213
26989
|
const fourteenDaysMs = 14 * 864e5;
|
|
26214
26990
|
const agingItems = allDocs.filter((d) => {
|
|
26215
|
-
if (
|
|
26991
|
+
if (DONE_STATUSES12.has(d.frontmatter.status)) return false;
|
|
26216
26992
|
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
26217
26993
|
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
26218
26994
|
return todayMs - createdMs > fourteenDaysMs;
|
|
@@ -26326,7 +27102,7 @@ function dmRisksPage(ctx) {
|
|
|
26326
27102
|
}
|
|
26327
27103
|
|
|
26328
27104
|
// src/web/templates/pages/dm/meetings.ts
|
|
26329
|
-
var
|
|
27105
|
+
var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26330
27106
|
function dmMeetingsPage(ctx) {
|
|
26331
27107
|
const meetings = ctx.store.list({ type: "meeting" });
|
|
26332
27108
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -26372,7 +27148,7 @@ function dmMeetingsPage(ctx) {
|
|
|
26372
27148
|
${sortedMeetings.map((m) => {
|
|
26373
27149
|
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
26374
27150
|
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
26375
|
-
const openCount = relatedActions.filter((a) => !
|
|
27151
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES13.has(a.frontmatter.status)).length;
|
|
26376
27152
|
return `
|
|
26377
27153
|
<tr>
|
|
26378
27154
|
<td>${formatDate(date5)}</td>
|
|
@@ -26387,7 +27163,7 @@ function dmMeetingsPage(ctx) {
|
|
|
26387
27163
|
const recentMeetingActions = [];
|
|
26388
27164
|
for (const [mid, acts] of meetingActionMap) {
|
|
26389
27165
|
for (const act of acts) {
|
|
26390
|
-
if (!
|
|
27166
|
+
if (!DONE_STATUSES13.has(act.frontmatter.status)) {
|
|
26391
27167
|
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
26392
27168
|
}
|
|
26393
27169
|
}
|
|
@@ -26582,7 +27358,7 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
|
26582
27358
|
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
26583
27359
|
|
|
26584
27360
|
// src/web/templates/pages/tl/dashboard.ts
|
|
26585
|
-
var
|
|
27361
|
+
var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26586
27362
|
var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
26587
27363
|
function tlDashboardPage(ctx) {
|
|
26588
27364
|
const epics = ctx.store.list({ type: "epic" });
|
|
@@ -26590,8 +27366,8 @@ function tlDashboardPage(ctx) {
|
|
|
26590
27366
|
const decisions = ctx.store.list({ type: "decision" });
|
|
26591
27367
|
const questions = ctx.store.list({ type: "question" });
|
|
26592
27368
|
const diagrams = getDiagramData(ctx.store);
|
|
26593
|
-
const openEpics = epics.filter((d) => !
|
|
26594
|
-
const openTasks = tasks.filter((d) => !
|
|
27369
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
|
|
27370
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
|
|
26595
27371
|
const technicalDecisions = decisions.filter((d) => {
|
|
26596
27372
|
const tags = d.frontmatter.tags ?? [];
|
|
26597
27373
|
return tags.some((t) => {
|
|
@@ -26649,7 +27425,7 @@ function tlDashboardPage(ctx) {
|
|
|
26649
27425
|
}
|
|
26650
27426
|
|
|
26651
27427
|
// src/web/templates/pages/tl/backlog.ts
|
|
26652
|
-
var
|
|
27428
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26653
27429
|
function tlBacklogPage(ctx) {
|
|
26654
27430
|
const epics = ctx.store.list({ type: "epic" });
|
|
26655
27431
|
const tasks = ctx.store.list({ type: "task" });
|
|
@@ -26686,7 +27462,7 @@ function tlBacklogPage(ctx) {
|
|
|
26686
27462
|
<tbody>
|
|
26687
27463
|
${sortedEpics.map((e) => {
|
|
26688
27464
|
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
26689
|
-
const done = eTasks.filter((t) =>
|
|
27465
|
+
const done = eTasks.filter((t) => DONE_STATUSES15.has(t.frontmatter.status)).length;
|
|
26690
27466
|
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
26691
27467
|
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
26692
27468
|
return `
|
|
@@ -26706,7 +27482,7 @@ function tlBacklogPage(ctx) {
|
|
|
26706
27482
|
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
26707
27483
|
}
|
|
26708
27484
|
const unassignedTasks = tasks.filter(
|
|
26709
|
-
(t) => !assignedTaskIds.has(t.frontmatter.id) && !
|
|
27485
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES15.has(t.frontmatter.status)
|
|
26710
27486
|
);
|
|
26711
27487
|
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
26712
27488
|
"tl-backlog-unassigned",
|