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/marvin-serve.js
CHANGED
|
@@ -14586,7 +14586,7 @@ function createActionTools(store) {
|
|
|
14586
14586
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
14587
14587
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
|
|
14588
14588
|
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
14589
|
-
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100).")
|
|
14589
|
+
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.")
|
|
14590
14590
|
},
|
|
14591
14591
|
async (args) => {
|
|
14592
14592
|
const { id, content, sprints, tags, workFocus, progress, owner, assignee, ...updates } = args;
|
|
@@ -14629,6 +14629,8 @@ function createActionTools(store) {
|
|
|
14629
14629
|
if (typeof progress === "number") {
|
|
14630
14630
|
updates.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
14631
14631
|
updates.progressOverride = true;
|
|
14632
|
+
} else if (progress === null) {
|
|
14633
|
+
updates.progressOverride = false;
|
|
14632
14634
|
}
|
|
14633
14635
|
const doc = store.update(id, updates, content);
|
|
14634
14636
|
if (args.status !== void 0 || typeof progress === "number") {
|
|
@@ -15635,6 +15637,14 @@ function evaluateHealth(projectName, metrics) {
|
|
|
15635
15637
|
|
|
15636
15638
|
// src/reports/sprint-summary/collector.ts
|
|
15637
15639
|
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15640
|
+
var COMPLEXITY_WEIGHTS = {
|
|
15641
|
+
trivial: 1,
|
|
15642
|
+
simple: 2,
|
|
15643
|
+
moderate: 3,
|
|
15644
|
+
complex: 5,
|
|
15645
|
+
"very-complex": 8
|
|
15646
|
+
};
|
|
15647
|
+
var DEFAULT_WEIGHT = 3;
|
|
15638
15648
|
function collectSprintSummaryData(store, sprintId) {
|
|
15639
15649
|
const allDocs = store.list();
|
|
15640
15650
|
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -15720,12 +15730,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15720
15730
|
for (const doc of workItemDocs) {
|
|
15721
15731
|
const about = doc.frontmatter.aboutArtifact;
|
|
15722
15732
|
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
15733
|
+
const complexity = doc.frontmatter.complexity;
|
|
15723
15734
|
const item = {
|
|
15724
15735
|
id: doc.frontmatter.id,
|
|
15725
15736
|
title: doc.frontmatter.title,
|
|
15726
15737
|
type: doc.frontmatter.type,
|
|
15727
15738
|
status: doc.frontmatter.status,
|
|
15728
15739
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15740
|
+
weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
|
|
15729
15741
|
owner: doc.frontmatter.owner,
|
|
15730
15742
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15731
15743
|
aboutArtifact: about,
|
|
@@ -17855,7 +17867,7 @@ function createTaskTools(store) {
|
|
|
17855
17867
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
17856
17868
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
|
|
17857
17869
|
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
17858
|
-
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions.")
|
|
17870
|
+
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.")
|
|
17859
17871
|
},
|
|
17860
17872
|
async (args) => {
|
|
17861
17873
|
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workFocus, progress, ...updates } = args;
|
|
@@ -17888,6 +17900,8 @@ function createTaskTools(store) {
|
|
|
17888
17900
|
if (typeof progress === "number") {
|
|
17889
17901
|
updates.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
17890
17902
|
updates.progressOverride = true;
|
|
17903
|
+
} else if (progress === null) {
|
|
17904
|
+
updates.progressOverride = false;
|
|
17891
17905
|
}
|
|
17892
17906
|
const doc = store.update(id, updates, content);
|
|
17893
17907
|
if (args.status !== void 0 || typeof progress === "number") {
|
|
@@ -19271,6 +19285,47 @@ function extractJiraKeyFromTags(tags) {
|
|
|
19271
19285
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
19272
19286
|
return tag ? tag.slice(5) : void 0;
|
|
19273
19287
|
}
|
|
19288
|
+
function collectLinkedIssues(issue2) {
|
|
19289
|
+
const linkedIssues = [];
|
|
19290
|
+
if (issue2.fields.subtasks) {
|
|
19291
|
+
for (const sub of issue2.fields.subtasks) {
|
|
19292
|
+
linkedIssues.push({
|
|
19293
|
+
key: sub.key,
|
|
19294
|
+
summary: sub.fields.summary,
|
|
19295
|
+
status: sub.fields.status.name,
|
|
19296
|
+
relationship: "subtask",
|
|
19297
|
+
isDone: DONE_STATUSES5.has(sub.fields.status.name.toLowerCase())
|
|
19298
|
+
});
|
|
19299
|
+
}
|
|
19300
|
+
}
|
|
19301
|
+
if (issue2.fields.issuelinks) {
|
|
19302
|
+
for (const link of issue2.fields.issuelinks) {
|
|
19303
|
+
if (link.outwardIssue) {
|
|
19304
|
+
linkedIssues.push({
|
|
19305
|
+
key: link.outwardIssue.key,
|
|
19306
|
+
summary: link.outwardIssue.fields.summary,
|
|
19307
|
+
status: link.outwardIssue.fields.status.name,
|
|
19308
|
+
relationship: link.type.outward,
|
|
19309
|
+
isDone: DONE_STATUSES5.has(
|
|
19310
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
19311
|
+
)
|
|
19312
|
+
});
|
|
19313
|
+
}
|
|
19314
|
+
if (link.inwardIssue) {
|
|
19315
|
+
linkedIssues.push({
|
|
19316
|
+
key: link.inwardIssue.key,
|
|
19317
|
+
summary: link.inwardIssue.fields.summary,
|
|
19318
|
+
status: link.inwardIssue.fields.status.name,
|
|
19319
|
+
relationship: link.type.inward,
|
|
19320
|
+
isDone: DONE_STATUSES5.has(
|
|
19321
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
19322
|
+
)
|
|
19323
|
+
});
|
|
19324
|
+
}
|
|
19325
|
+
}
|
|
19326
|
+
}
|
|
19327
|
+
return linkedIssues;
|
|
19328
|
+
}
|
|
19274
19329
|
function computeSubtaskProgress(subtasks) {
|
|
19275
19330
|
if (subtasks.length === 0) return 0;
|
|
19276
19331
|
const done = subtasks.filter(
|
|
@@ -19311,44 +19366,7 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
19311
19366
|
const resolved = statusMap ?? {};
|
|
19312
19367
|
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
|
|
19313
19368
|
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
|
-
}
|
|
19369
|
+
const linkedIssues = collectLinkedIssues(issue2);
|
|
19352
19370
|
const subtasks = issue2.fields.subtasks ?? [];
|
|
19353
19371
|
let proposedProgress;
|
|
19354
19372
|
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
@@ -19570,7 +19588,6 @@ function isWithinRange(timestamp, range) {
|
|
|
19570
19588
|
function isConfluenceUrl(url2) {
|
|
19571
19589
|
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
19572
19590
|
}
|
|
19573
|
-
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
19574
19591
|
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
19575
19592
|
const summary = {
|
|
19576
19593
|
dateRange,
|
|
@@ -19681,42 +19698,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
19681
19698
|
});
|
|
19682
19699
|
}
|
|
19683
19700
|
}
|
|
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
|
-
}
|
|
19701
|
+
const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
|
|
19720
19702
|
const marvinArtifacts = [];
|
|
19721
19703
|
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
19722
19704
|
for (const doc of artifacts) {
|
|
@@ -19846,22 +19828,23 @@ function generateProposedActions(issues) {
|
|
|
19846
19828
|
|
|
19847
19829
|
// src/skills/builtin/jira/sprint-progress.ts
|
|
19848
19830
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
19849
|
-
var
|
|
19831
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
19850
19832
|
var BATCH_SIZE = 5;
|
|
19833
|
+
var MAX_LINKED_ISSUES = 50;
|
|
19851
19834
|
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
19852
|
-
var
|
|
19835
|
+
var COMPLEXITY_WEIGHTS2 = {
|
|
19853
19836
|
trivial: 1,
|
|
19854
19837
|
simple: 2,
|
|
19855
19838
|
moderate: 3,
|
|
19856
19839
|
complex: 5,
|
|
19857
19840
|
"very-complex": 8
|
|
19858
19841
|
};
|
|
19859
|
-
var
|
|
19842
|
+
var DEFAULT_WEIGHT2 = 3;
|
|
19860
19843
|
function resolveWeight(complexity) {
|
|
19861
|
-
if (complexity && complexity in
|
|
19862
|
-
return { weight:
|
|
19844
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
|
|
19845
|
+
return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
|
|
19863
19846
|
}
|
|
19864
|
-
return { weight:
|
|
19847
|
+
return { weight: DEFAULT_WEIGHT2, weightSource: "default" };
|
|
19865
19848
|
}
|
|
19866
19849
|
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
19867
19850
|
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
@@ -19950,6 +19933,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
19950
19933
|
}
|
|
19951
19934
|
}
|
|
19952
19935
|
}
|
|
19936
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
19937
|
+
if (options.traverseLinks) {
|
|
19938
|
+
const visited = new Set(jiraIssues.keys());
|
|
19939
|
+
const queue = [];
|
|
19940
|
+
for (const [, data] of jiraIssues) {
|
|
19941
|
+
const links = collectLinkedIssues(data.issue);
|
|
19942
|
+
for (const link of links) {
|
|
19943
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
19944
|
+
visited.add(link.key);
|
|
19945
|
+
queue.push(link.key);
|
|
19946
|
+
}
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
19950
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
19951
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
19952
|
+
const results = await Promise.allSettled(
|
|
19953
|
+
batch.map(async (key) => {
|
|
19954
|
+
const [issue2, comments] = await Promise.all([
|
|
19955
|
+
client.getIssueWithLinks(key),
|
|
19956
|
+
client.getComments(key)
|
|
19957
|
+
]);
|
|
19958
|
+
return { key, issue: issue2, comments };
|
|
19959
|
+
})
|
|
19960
|
+
);
|
|
19961
|
+
for (const result of results) {
|
|
19962
|
+
if (result.status === "fulfilled") {
|
|
19963
|
+
const { key, issue: issue2, comments } = result.value;
|
|
19964
|
+
linkedJiraIssues.set(key, { issue: issue2, comments });
|
|
19965
|
+
const newLinks = collectLinkedIssues(issue2);
|
|
19966
|
+
for (const link of newLinks) {
|
|
19967
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
19968
|
+
visited.add(link.key);
|
|
19969
|
+
queue.push(link.key);
|
|
19970
|
+
}
|
|
19971
|
+
}
|
|
19972
|
+
} else {
|
|
19973
|
+
const batchKey = batch[results.indexOf(result)];
|
|
19974
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
19975
|
+
}
|
|
19976
|
+
}
|
|
19977
|
+
}
|
|
19978
|
+
if (queue.length > 0) {
|
|
19979
|
+
errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
|
|
19980
|
+
}
|
|
19981
|
+
}
|
|
19953
19982
|
const proposedUpdates = [];
|
|
19954
19983
|
const itemReports = [];
|
|
19955
19984
|
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
@@ -20015,6 +20044,23 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20015
20044
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
20016
20045
|
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
20017
20046
|
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
20047
|
+
let itemLinkedIssues = [];
|
|
20048
|
+
const itemLinkedIssueSignals = [];
|
|
20049
|
+
if (options.traverseLinks && jiraData) {
|
|
20050
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
20051
|
+
jiraData.issue,
|
|
20052
|
+
jiraIssues,
|
|
20053
|
+
linkedJiraIssues
|
|
20054
|
+
);
|
|
20055
|
+
itemLinkedIssues = allLinks;
|
|
20056
|
+
itemLinkedIssueSignals.push(...allSignals);
|
|
20057
|
+
analyzeLinkedIssueSignals(
|
|
20058
|
+
allLinks,
|
|
20059
|
+
fm,
|
|
20060
|
+
jiraKey,
|
|
20061
|
+
proposedUpdates
|
|
20062
|
+
);
|
|
20063
|
+
}
|
|
20018
20064
|
const report = {
|
|
20019
20065
|
id: fm.id,
|
|
20020
20066
|
title: fm.title,
|
|
@@ -20033,6 +20079,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20033
20079
|
progressDrift,
|
|
20034
20080
|
commentSignals,
|
|
20035
20081
|
commentSummary: null,
|
|
20082
|
+
linkedIssues: itemLinkedIssues,
|
|
20083
|
+
linkedIssueSignals: itemLinkedIssueSignals,
|
|
20036
20084
|
children: [],
|
|
20037
20085
|
owner: fm.owner ?? null,
|
|
20038
20086
|
focusArea: focusTag ? focusTag.slice(6) : null
|
|
@@ -20076,7 +20124,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20076
20124
|
const focusAreas = [];
|
|
20077
20125
|
for (const [name, items] of focusAreaMap) {
|
|
20078
20126
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
20079
|
-
const doneCount = allFlatItems.filter((i) =>
|
|
20127
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES6.has(i.marvinStatus)).length;
|
|
20080
20128
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
20081
20129
|
const progress = computeWeightedProgress(items);
|
|
20082
20130
|
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
@@ -20128,6 +20176,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20128
20176
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20129
20177
|
}
|
|
20130
20178
|
}
|
|
20179
|
+
if (options.traverseLinks) {
|
|
20180
|
+
try {
|
|
20181
|
+
const linkedSummaries = await analyzeLinkedIssueComments(
|
|
20182
|
+
itemReports,
|
|
20183
|
+
linkedJiraIssues
|
|
20184
|
+
);
|
|
20185
|
+
for (const [artifactId, signalSummaries] of linkedSummaries) {
|
|
20186
|
+
const report = itemReports.find((r) => r.id === artifactId);
|
|
20187
|
+
if (!report) continue;
|
|
20188
|
+
for (const [sourceKey, summary] of signalSummaries) {
|
|
20189
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === sourceKey);
|
|
20190
|
+
if (signal) {
|
|
20191
|
+
signal.commentSummary = summary;
|
|
20192
|
+
}
|
|
20193
|
+
}
|
|
20194
|
+
}
|
|
20195
|
+
} catch (err) {
|
|
20196
|
+
errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20197
|
+
}
|
|
20198
|
+
}
|
|
20131
20199
|
}
|
|
20132
20200
|
const appliedUpdates = [];
|
|
20133
20201
|
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
@@ -20244,6 +20312,155 @@ ${commentTexts}`);
|
|
|
20244
20312
|
}
|
|
20245
20313
|
return summaries;
|
|
20246
20314
|
}
|
|
20315
|
+
function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
|
|
20316
|
+
const allLinks = [];
|
|
20317
|
+
const allSignals = [];
|
|
20318
|
+
const visited = /* @__PURE__ */ new Set([primaryIssue.key]);
|
|
20319
|
+
const directLinks = collectLinkedIssues(primaryIssue).filter((l) => l.relationship !== "subtask");
|
|
20320
|
+
const queue = [...directLinks];
|
|
20321
|
+
for (const link of directLinks) {
|
|
20322
|
+
visited.add(link.key);
|
|
20323
|
+
}
|
|
20324
|
+
while (queue.length > 0) {
|
|
20325
|
+
const link = queue.shift();
|
|
20326
|
+
allLinks.push(link);
|
|
20327
|
+
const linkedData = linkedJiraIssues.get(link.key) ?? primaryIssues.get(link.key);
|
|
20328
|
+
if (!linkedData) continue;
|
|
20329
|
+
const linkedCommentSignals = [];
|
|
20330
|
+
for (const comment of linkedData.comments) {
|
|
20331
|
+
const text = extractCommentText(comment.body);
|
|
20332
|
+
const signals = detectCommentSignals(text);
|
|
20333
|
+
linkedCommentSignals.push(...signals);
|
|
20334
|
+
}
|
|
20335
|
+
if (linkedCommentSignals.length > 0 || linkedData.comments.length > 0) {
|
|
20336
|
+
allSignals.push({
|
|
20337
|
+
sourceKey: link.key,
|
|
20338
|
+
linkType: link.relationship,
|
|
20339
|
+
commentSignals: linkedCommentSignals,
|
|
20340
|
+
commentSummary: null
|
|
20341
|
+
});
|
|
20342
|
+
}
|
|
20343
|
+
const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
|
|
20344
|
+
for (const next of nextLinks) {
|
|
20345
|
+
visited.add(next.key);
|
|
20346
|
+
queue.push(next);
|
|
20347
|
+
}
|
|
20348
|
+
}
|
|
20349
|
+
return { allLinks, allSignals };
|
|
20350
|
+
}
|
|
20351
|
+
var BLOCKER_LINK_PATTERNS = ["blocks", "is blocked by"];
|
|
20352
|
+
var WONT_DO_STATUSES = /* @__PURE__ */ new Set(["wont do", "won't do", "cancelled"]);
|
|
20353
|
+
function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedUpdates) {
|
|
20354
|
+
if (linkedIssues.length === 0) return;
|
|
20355
|
+
const blockerLinks = linkedIssues.filter(
|
|
20356
|
+
(l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
|
|
20357
|
+
);
|
|
20358
|
+
if (blockerLinks.length > 0 && blockerLinks.every((l) => l.isDone) && frontmatter.status === "blocked") {
|
|
20359
|
+
proposedUpdates.push({
|
|
20360
|
+
artifactId: frontmatter.id,
|
|
20361
|
+
field: "status",
|
|
20362
|
+
currentValue: "blocked",
|
|
20363
|
+
proposedValue: "in-progress",
|
|
20364
|
+
reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
|
|
20365
|
+
});
|
|
20366
|
+
}
|
|
20367
|
+
const wontDoLinks = linkedIssues.filter(
|
|
20368
|
+
(l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
|
|
20369
|
+
);
|
|
20370
|
+
if (wontDoLinks.length > 0) {
|
|
20371
|
+
proposedUpdates.push({
|
|
20372
|
+
artifactId: frontmatter.id,
|
|
20373
|
+
field: "review",
|
|
20374
|
+
currentValue: null,
|
|
20375
|
+
proposedValue: "needs-review",
|
|
20376
|
+
reason: `Linked issue(s) cancelled/won't do: ${wontDoLinks.map((l) => `${l.key} "${l.summary}"`).join(", ")}`
|
|
20377
|
+
});
|
|
20378
|
+
}
|
|
20379
|
+
}
|
|
20380
|
+
var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
|
|
20381
|
+
|
|
20382
|
+
For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
|
|
20383
|
+
|
|
20384
|
+
Return your response as a JSON object mapping artifact IDs to objects mapping linked issue keys to summary strings.
|
|
20385
|
+
Example: {"T-001": {"PROJ-301": "DBA review scheduled for Thursday."}}
|
|
20386
|
+
|
|
20387
|
+
IMPORTANT: Only return the JSON object, no other text.`;
|
|
20388
|
+
async function analyzeLinkedIssueComments(items, linkedJiraIssues) {
|
|
20389
|
+
const results = /* @__PURE__ */ new Map();
|
|
20390
|
+
const promptParts = [];
|
|
20391
|
+
const itemsWithLinkedComments = [];
|
|
20392
|
+
for (const item of items) {
|
|
20393
|
+
if (item.linkedIssueSignals.length === 0) continue;
|
|
20394
|
+
const linkedParts = [];
|
|
20395
|
+
for (const signal of item.linkedIssueSignals) {
|
|
20396
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
20397
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
20398
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
20399
|
+
const text = extractCommentText(c.body);
|
|
20400
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
20401
|
+
}).join("\n");
|
|
20402
|
+
linkedParts.push(` ### ${signal.sourceKey} (${signal.linkType})
|
|
20403
|
+
${commentTexts}`);
|
|
20404
|
+
}
|
|
20405
|
+
if (linkedParts.length > 0) {
|
|
20406
|
+
itemsWithLinkedComments.push(item);
|
|
20407
|
+
promptParts.push(`## ${item.id} \u2014 ${item.title}
|
|
20408
|
+
Linked issues:
|
|
20409
|
+
${linkedParts.join("\n")}`);
|
|
20410
|
+
}
|
|
20411
|
+
}
|
|
20412
|
+
if (promptParts.length === 0) return results;
|
|
20413
|
+
const prompt = promptParts.join("\n\n");
|
|
20414
|
+
const llmResult = query2({
|
|
20415
|
+
prompt,
|
|
20416
|
+
options: {
|
|
20417
|
+
systemPrompt: LINKED_COMMENT_ANALYSIS_PROMPT,
|
|
20418
|
+
maxTurns: 1,
|
|
20419
|
+
tools: [],
|
|
20420
|
+
allowedTools: []
|
|
20421
|
+
}
|
|
20422
|
+
});
|
|
20423
|
+
for await (const msg of llmResult) {
|
|
20424
|
+
if (msg.type === "assistant") {
|
|
20425
|
+
const textBlock = msg.message.content.find(
|
|
20426
|
+
(b) => b.type === "text"
|
|
20427
|
+
);
|
|
20428
|
+
if (textBlock) {
|
|
20429
|
+
const parsed = parseLlmJson(textBlock.text);
|
|
20430
|
+
if (parsed) {
|
|
20431
|
+
for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
|
|
20432
|
+
if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
|
|
20433
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
20434
|
+
for (const [key, summary] of Object.entries(linkedSummaries)) {
|
|
20435
|
+
if (typeof summary === "string") {
|
|
20436
|
+
signalMap.set(key, summary);
|
|
20437
|
+
}
|
|
20438
|
+
}
|
|
20439
|
+
if (signalMap.size > 0) {
|
|
20440
|
+
results.set(artifactId, signalMap);
|
|
20441
|
+
}
|
|
20442
|
+
}
|
|
20443
|
+
}
|
|
20444
|
+
}
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
}
|
|
20448
|
+
return results;
|
|
20449
|
+
}
|
|
20450
|
+
function parseLlmJson(text) {
|
|
20451
|
+
try {
|
|
20452
|
+
return JSON.parse(text);
|
|
20453
|
+
} catch {
|
|
20454
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
20455
|
+
if (match) {
|
|
20456
|
+
try {
|
|
20457
|
+
return JSON.parse(match[1]);
|
|
20458
|
+
} catch {
|
|
20459
|
+
}
|
|
20460
|
+
}
|
|
20461
|
+
return null;
|
|
20462
|
+
}
|
|
20463
|
+
}
|
|
20247
20464
|
function formatProgressReport(report) {
|
|
20248
20465
|
const parts = [];
|
|
20249
20466
|
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
@@ -20327,7 +20544,7 @@ function formatProgressReport(report) {
|
|
|
20327
20544
|
}
|
|
20328
20545
|
function formatItemLine(parts, item, depth) {
|
|
20329
20546
|
const indent = " ".repeat(depth + 1);
|
|
20330
|
-
const statusIcon =
|
|
20547
|
+
const statusIcon = DONE_STATUSES6.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
20331
20548
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
20332
20549
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
20333
20550
|
const progressLabel = ` ${item.progress}%`;
|
|
@@ -20337,6 +20554,19 @@ function formatItemLine(parts, item, depth) {
|
|
|
20337
20554
|
if (item.commentSummary) {
|
|
20338
20555
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
20339
20556
|
}
|
|
20557
|
+
if (item.linkedIssues.length > 0) {
|
|
20558
|
+
parts.push(`${indent} \u{1F517} Linked Issues:`);
|
|
20559
|
+
for (const link of item.linkedIssues) {
|
|
20560
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
20561
|
+
const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
|
|
20562
|
+
const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
|
|
20563
|
+
parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
|
|
20564
|
+
const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
20565
|
+
if (signal?.commentSummary) {
|
|
20566
|
+
parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
|
|
20567
|
+
}
|
|
20568
|
+
}
|
|
20569
|
+
}
|
|
20340
20570
|
for (const child of item.children) {
|
|
20341
20571
|
formatItemLine(parts, child, depth + 1);
|
|
20342
20572
|
}
|
|
@@ -20346,6 +20576,527 @@ function progressBar(pct) {
|
|
|
20346
20576
|
const empty = 10 - filled;
|
|
20347
20577
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
20348
20578
|
}
|
|
20579
|
+
var MAX_ARTIFACT_NODES = 50;
|
|
20580
|
+
var MAX_LLM_DEPTH = 3;
|
|
20581
|
+
var MAX_LLM_COMMENT_CHARS = 8e3;
|
|
20582
|
+
async function assessArtifact(store, client, host, options) {
|
|
20583
|
+
const visited = /* @__PURE__ */ new Set();
|
|
20584
|
+
return _assessArtifactRecursive(store, client, host, options, visited, 0);
|
|
20585
|
+
}
|
|
20586
|
+
async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
|
|
20587
|
+
const errors = [];
|
|
20588
|
+
if (visited.has(options.artifactId)) {
|
|
20589
|
+
return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
|
|
20590
|
+
}
|
|
20591
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
20592
|
+
return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
|
|
20593
|
+
}
|
|
20594
|
+
visited.add(options.artifactId);
|
|
20595
|
+
const doc = store.get(options.artifactId);
|
|
20596
|
+
if (!doc) {
|
|
20597
|
+
return emptyArtifactReport(options.artifactId, [`Artifact ${options.artifactId} not found`]);
|
|
20598
|
+
}
|
|
20599
|
+
const fm = doc.frontmatter;
|
|
20600
|
+
const jiraKey = fm.jiraKey ?? extractJiraKeyFromTags(fm.tags) ?? null;
|
|
20601
|
+
const tags = fm.tags ?? [];
|
|
20602
|
+
const sprintTag = tags.find((t) => t.startsWith("sprint:"));
|
|
20603
|
+
const sprint = sprintTag ? sprintTag.slice(7) : null;
|
|
20604
|
+
const parent = fm.aboutArtifact ?? null;
|
|
20605
|
+
let jiraStatus = null;
|
|
20606
|
+
let jiraAssignee = null;
|
|
20607
|
+
let proposedMarvinStatus = null;
|
|
20608
|
+
let jiraSubtaskProgress = null;
|
|
20609
|
+
const commentSignals = [];
|
|
20610
|
+
let commentSummary = null;
|
|
20611
|
+
let linkedIssues = [];
|
|
20612
|
+
let linkedIssueSignals = [];
|
|
20613
|
+
const proposedUpdates = [];
|
|
20614
|
+
const jiraIssues = /* @__PURE__ */ new Map();
|
|
20615
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
20616
|
+
if (jiraKey) {
|
|
20617
|
+
try {
|
|
20618
|
+
const [issue2, comments] = await Promise.all([
|
|
20619
|
+
client.getIssueWithLinks(jiraKey),
|
|
20620
|
+
client.getComments(jiraKey)
|
|
20621
|
+
]);
|
|
20622
|
+
jiraIssues.set(jiraKey, { issue: issue2, comments });
|
|
20623
|
+
jiraStatus = issue2.fields.status.name;
|
|
20624
|
+
jiraAssignee = issue2.fields.assignee?.displayName ?? null;
|
|
20625
|
+
const inSprint = isInActiveSprint(store, fm.tags);
|
|
20626
|
+
const resolved = options.statusMap ?? {};
|
|
20627
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
20628
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
20629
|
+
if (subtasks.length > 0) {
|
|
20630
|
+
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
20631
|
+
}
|
|
20632
|
+
for (const comment of comments) {
|
|
20633
|
+
const text = extractCommentText(comment.body);
|
|
20634
|
+
const signals2 = detectCommentSignals(text);
|
|
20635
|
+
commentSignals.push(...signals2);
|
|
20636
|
+
}
|
|
20637
|
+
const jiraVisited = /* @__PURE__ */ new Set([jiraKey]);
|
|
20638
|
+
const queue = [];
|
|
20639
|
+
const directLinks = collectLinkedIssues(issue2).filter((l) => l.relationship !== "subtask");
|
|
20640
|
+
for (const link of directLinks) {
|
|
20641
|
+
if (!jiraVisited.has(link.key)) {
|
|
20642
|
+
jiraVisited.add(link.key);
|
|
20643
|
+
queue.push(link.key);
|
|
20644
|
+
}
|
|
20645
|
+
}
|
|
20646
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
20647
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
20648
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
20649
|
+
const results = await Promise.allSettled(
|
|
20650
|
+
batch.map(async (key) => {
|
|
20651
|
+
const [li, lc] = await Promise.all([
|
|
20652
|
+
client.getIssueWithLinks(key),
|
|
20653
|
+
client.getComments(key)
|
|
20654
|
+
]);
|
|
20655
|
+
return { key, issue: li, comments: lc };
|
|
20656
|
+
})
|
|
20657
|
+
);
|
|
20658
|
+
for (const result of results) {
|
|
20659
|
+
if (result.status === "fulfilled") {
|
|
20660
|
+
const { key, issue: li, comments: lc } = result.value;
|
|
20661
|
+
linkedJiraIssues.set(key, { issue: li, comments: lc });
|
|
20662
|
+
const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
|
|
20663
|
+
for (const nl of newLinks) {
|
|
20664
|
+
jiraVisited.add(nl.key);
|
|
20665
|
+
queue.push(nl.key);
|
|
20666
|
+
}
|
|
20667
|
+
} else {
|
|
20668
|
+
const batchKey = batch[results.indexOf(result)];
|
|
20669
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
20670
|
+
}
|
|
20671
|
+
}
|
|
20672
|
+
}
|
|
20673
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
20674
|
+
issue2,
|
|
20675
|
+
jiraIssues,
|
|
20676
|
+
linkedJiraIssues
|
|
20677
|
+
);
|
|
20678
|
+
linkedIssues = allLinks;
|
|
20679
|
+
linkedIssueSignals = allSignals;
|
|
20680
|
+
analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
|
|
20681
|
+
} catch (err) {
|
|
20682
|
+
errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20683
|
+
}
|
|
20684
|
+
}
|
|
20685
|
+
const currentProgress = getEffectiveProgress(fm);
|
|
20686
|
+
const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
|
|
20687
|
+
const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
|
|
20688
|
+
if (statusDrift && proposedMarvinStatus) {
|
|
20689
|
+
proposedUpdates.push({
|
|
20690
|
+
artifactId: fm.id,
|
|
20691
|
+
field: "status",
|
|
20692
|
+
currentValue: fm.status,
|
|
20693
|
+
proposedValue: proposedMarvinStatus,
|
|
20694
|
+
reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
|
|
20695
|
+
});
|
|
20696
|
+
}
|
|
20697
|
+
if (progressDrift && jiraSubtaskProgress !== null) {
|
|
20698
|
+
proposedUpdates.push({
|
|
20699
|
+
artifactId: fm.id,
|
|
20700
|
+
field: "progress",
|
|
20701
|
+
currentValue: currentProgress,
|
|
20702
|
+
proposedValue: jiraSubtaskProgress,
|
|
20703
|
+
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
20704
|
+
});
|
|
20705
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
20706
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
20707
|
+
if (!hasExplicitProgress) {
|
|
20708
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
20709
|
+
if (proposedProgress !== currentProgress) {
|
|
20710
|
+
proposedUpdates.push({
|
|
20711
|
+
artifactId: fm.id,
|
|
20712
|
+
field: "progress",
|
|
20713
|
+
currentValue: currentProgress,
|
|
20714
|
+
proposedValue: proposedProgress,
|
|
20715
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
20716
|
+
});
|
|
20717
|
+
}
|
|
20718
|
+
}
|
|
20719
|
+
}
|
|
20720
|
+
const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
|
|
20721
|
+
if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
|
|
20722
|
+
const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
|
|
20723
|
+
if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
|
|
20724
|
+
try {
|
|
20725
|
+
const summary = await analyzeSingleArtifactComments(
|
|
20726
|
+
fm.id,
|
|
20727
|
+
fm.title,
|
|
20728
|
+
jiraKey,
|
|
20729
|
+
jiraStatus,
|
|
20730
|
+
jiraIssues,
|
|
20731
|
+
linkedJiraIssues,
|
|
20732
|
+
linkedIssueSignals
|
|
20733
|
+
);
|
|
20734
|
+
commentSummary = summary;
|
|
20735
|
+
} catch (err) {
|
|
20736
|
+
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20737
|
+
}
|
|
20738
|
+
}
|
|
20739
|
+
}
|
|
20740
|
+
const childIds = findChildIds(store, fm);
|
|
20741
|
+
const children = [];
|
|
20742
|
+
for (const childId of childIds) {
|
|
20743
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
20744
|
+
errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
|
|
20745
|
+
break;
|
|
20746
|
+
}
|
|
20747
|
+
const childReport = await _assessArtifactRecursive(
|
|
20748
|
+
store,
|
|
20749
|
+
client,
|
|
20750
|
+
host,
|
|
20751
|
+
{ ...options, artifactId: childId },
|
|
20752
|
+
visited,
|
|
20753
|
+
depth + 1
|
|
20754
|
+
);
|
|
20755
|
+
children.push(childReport);
|
|
20756
|
+
}
|
|
20757
|
+
if (children.length > 0) {
|
|
20758
|
+
const rolledUpProgress = computeWeightedProgress(
|
|
20759
|
+
children.map((c) => ({
|
|
20760
|
+
weight: resolveWeight(void 0).weight,
|
|
20761
|
+
progress: c.marvinProgress
|
|
20762
|
+
}))
|
|
20763
|
+
);
|
|
20764
|
+
if (rolledUpProgress !== currentProgress) {
|
|
20765
|
+
proposedUpdates.push({
|
|
20766
|
+
artifactId: fm.id,
|
|
20767
|
+
field: "progress",
|
|
20768
|
+
currentValue: currentProgress,
|
|
20769
|
+
proposedValue: rolledUpProgress,
|
|
20770
|
+
reason: `Rolled up from ${children.length} children (weighted average ${rolledUpProgress}%)`
|
|
20771
|
+
});
|
|
20772
|
+
}
|
|
20773
|
+
}
|
|
20774
|
+
const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
|
|
20775
|
+
const appliedUpdates = [];
|
|
20776
|
+
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
20777
|
+
for (const update of proposedUpdates) {
|
|
20778
|
+
if (update.field === "review") continue;
|
|
20779
|
+
try {
|
|
20780
|
+
const updatePayload = {
|
|
20781
|
+
[update.field]: update.proposedValue,
|
|
20782
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20783
|
+
};
|
|
20784
|
+
if (update.field === "progress") {
|
|
20785
|
+
updatePayload.progressOverride = false;
|
|
20786
|
+
}
|
|
20787
|
+
store.update(update.artifactId, updatePayload);
|
|
20788
|
+
const updatedDoc = store.get(update.artifactId);
|
|
20789
|
+
if (updatedDoc) {
|
|
20790
|
+
if (updatedDoc.frontmatter.type === "task") {
|
|
20791
|
+
propagateProgressFromTask(store, update.artifactId);
|
|
20792
|
+
} else if (updatedDoc.frontmatter.type === "action") {
|
|
20793
|
+
propagateProgressToAction(store, update.artifactId);
|
|
20794
|
+
}
|
|
20795
|
+
}
|
|
20796
|
+
appliedUpdates.push(update);
|
|
20797
|
+
} catch (err) {
|
|
20798
|
+
errors.push(`Failed to apply update: ${err instanceof Error ? err.message : String(err)}`);
|
|
20799
|
+
}
|
|
20800
|
+
}
|
|
20801
|
+
}
|
|
20802
|
+
return {
|
|
20803
|
+
artifactId: fm.id,
|
|
20804
|
+
title: fm.title,
|
|
20805
|
+
type: fm.type,
|
|
20806
|
+
marvinStatus: fm.status,
|
|
20807
|
+
marvinProgress: currentProgress,
|
|
20808
|
+
sprint,
|
|
20809
|
+
parent,
|
|
20810
|
+
jiraKey,
|
|
20811
|
+
jiraStatus,
|
|
20812
|
+
jiraAssignee,
|
|
20813
|
+
jiraSubtaskProgress,
|
|
20814
|
+
proposedMarvinStatus,
|
|
20815
|
+
statusDrift,
|
|
20816
|
+
progressDrift,
|
|
20817
|
+
commentSignals,
|
|
20818
|
+
commentSummary,
|
|
20819
|
+
linkedIssues,
|
|
20820
|
+
linkedIssueSignals,
|
|
20821
|
+
children,
|
|
20822
|
+
proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
|
|
20823
|
+
appliedUpdates,
|
|
20824
|
+
signals,
|
|
20825
|
+
errors
|
|
20826
|
+
};
|
|
20827
|
+
}
|
|
20828
|
+
function findChildIds(store, fm) {
|
|
20829
|
+
if (fm.type === "action") {
|
|
20830
|
+
return store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === fm.id).map((d) => d.frontmatter.id);
|
|
20831
|
+
}
|
|
20832
|
+
if (fm.type === "epic") {
|
|
20833
|
+
const epicTag = `epic:${fm.id}`;
|
|
20834
|
+
const isLinked = (d) => {
|
|
20835
|
+
const le = d.frontmatter.linkedEpic;
|
|
20836
|
+
if (le?.includes(fm.id)) return true;
|
|
20837
|
+
const t = d.frontmatter.tags ?? [];
|
|
20838
|
+
return t.includes(epicTag);
|
|
20839
|
+
};
|
|
20840
|
+
return [
|
|
20841
|
+
...store.list({ type: "action" }).filter(isLinked),
|
|
20842
|
+
...store.list({ type: "task" }).filter(isLinked)
|
|
20843
|
+
].map((d) => d.frontmatter.id);
|
|
20844
|
+
}
|
|
20845
|
+
return [];
|
|
20846
|
+
}
|
|
20847
|
+
function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus) {
|
|
20848
|
+
const signals = [];
|
|
20849
|
+
const blockerSignals = commentSignals.filter((s) => s.type === "blocker");
|
|
20850
|
+
if (blockerSignals.length > 0) {
|
|
20851
|
+
for (const s of blockerSignals) {
|
|
20852
|
+
signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
|
|
20853
|
+
}
|
|
20854
|
+
}
|
|
20855
|
+
const blockingLinks = linkedIssues.filter(
|
|
20856
|
+
(l) => l.relationship.toLowerCase().includes("block")
|
|
20857
|
+
);
|
|
20858
|
+
const activeBlockers = blockingLinks.filter((l) => !l.isDone);
|
|
20859
|
+
const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
|
|
20860
|
+
if (activeBlockers.length > 0) {
|
|
20861
|
+
for (const b of activeBlockers) {
|
|
20862
|
+
signals.push(`\u{1F6AB} Blocker \u2014 ${b.relationship} ${b.key} "${b.summary}" [${b.status}]`);
|
|
20863
|
+
}
|
|
20864
|
+
}
|
|
20865
|
+
if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
|
|
20866
|
+
signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
|
|
20867
|
+
}
|
|
20868
|
+
const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
|
|
20869
|
+
for (const l of wontDoLinks) {
|
|
20870
|
+
signals.push(`\u{1F504} Superseded \u2014 ${l.key} "${l.summary}" is ${l.status}`);
|
|
20871
|
+
}
|
|
20872
|
+
const questionSignals = commentSignals.filter((s) => s.type === "question");
|
|
20873
|
+
for (const s of questionSignals) {
|
|
20874
|
+
signals.push(`\u23F3 Waiting \u2014 "${s.snippet}"`);
|
|
20875
|
+
}
|
|
20876
|
+
const relatedInProgress = linkedIssues.filter(
|
|
20877
|
+
(l) => l.relationship.toLowerCase().includes("relate") && !l.isDone
|
|
20878
|
+
);
|
|
20879
|
+
if (relatedInProgress.length > 0) {
|
|
20880
|
+
for (const l of relatedInProgress) {
|
|
20881
|
+
signals.push(`\u{1F4CB} Handoff \u2014 related work on ${l.key} "${l.summary}" [${l.status}]`);
|
|
20882
|
+
}
|
|
20883
|
+
}
|
|
20884
|
+
if (signals.length === 0) {
|
|
20885
|
+
if (statusDrift && proposedStatus) {
|
|
20886
|
+
signals.push(`\u26A0 Drift detected \u2014 Marvin and Jira statuses diverge`);
|
|
20887
|
+
} else {
|
|
20888
|
+
signals.push(`\u2705 No active blockers or concerns detected`);
|
|
20889
|
+
}
|
|
20890
|
+
}
|
|
20891
|
+
return signals;
|
|
20892
|
+
}
|
|
20893
|
+
function estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
20894
|
+
let total = 0;
|
|
20895
|
+
for (const [, data] of jiraIssues) {
|
|
20896
|
+
for (const c of data.comments) {
|
|
20897
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
20898
|
+
}
|
|
20899
|
+
}
|
|
20900
|
+
for (const signal of linkedIssueSignals) {
|
|
20901
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
20902
|
+
if (!linkedData) continue;
|
|
20903
|
+
for (const c of linkedData.comments) {
|
|
20904
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
20905
|
+
}
|
|
20906
|
+
}
|
|
20907
|
+
return total;
|
|
20908
|
+
}
|
|
20909
|
+
var SINGLE_ARTIFACT_COMMENT_PROMPT = `You are a delivery management assistant analyzing Jira comments for a single work item.
|
|
20910
|
+
|
|
20911
|
+
Produce a 2-3 sentence progress summary covering:
|
|
20912
|
+
- What work has been completed
|
|
20913
|
+
- What is pending or blocked
|
|
20914
|
+
- Any decisions, handoffs, or scheduling mentioned
|
|
20915
|
+
- Relevant context from linked issue comments (if provided)
|
|
20916
|
+
|
|
20917
|
+
Return ONLY the summary text, no JSON or formatting.`;
|
|
20918
|
+
async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraStatus, jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
20919
|
+
const promptParts = [];
|
|
20920
|
+
const primaryData = jiraIssues.get(jiraKey);
|
|
20921
|
+
if (primaryData && primaryData.comments.length > 0) {
|
|
20922
|
+
const commentTexts = primaryData.comments.map((c) => {
|
|
20923
|
+
const text = extractCommentText(c.body);
|
|
20924
|
+
return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
|
|
20925
|
+
}).join("\n");
|
|
20926
|
+
promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
|
|
20927
|
+
Comments:
|
|
20928
|
+
${commentTexts}`);
|
|
20929
|
+
}
|
|
20930
|
+
for (const signal of linkedIssueSignals) {
|
|
20931
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
20932
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
20933
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
20934
|
+
const text = extractCommentText(c.body);
|
|
20935
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
20936
|
+
}).join("\n");
|
|
20937
|
+
promptParts.push(`### Linked: ${signal.sourceKey} (${signal.linkType})
|
|
20938
|
+
${commentTexts}`);
|
|
20939
|
+
}
|
|
20940
|
+
if (promptParts.length === 0) return null;
|
|
20941
|
+
const prompt = promptParts.join("\n\n");
|
|
20942
|
+
const result = query2({
|
|
20943
|
+
prompt,
|
|
20944
|
+
options: {
|
|
20945
|
+
systemPrompt: SINGLE_ARTIFACT_COMMENT_PROMPT,
|
|
20946
|
+
maxTurns: 1,
|
|
20947
|
+
tools: [],
|
|
20948
|
+
allowedTools: []
|
|
20949
|
+
}
|
|
20950
|
+
});
|
|
20951
|
+
for await (const msg of result) {
|
|
20952
|
+
if (msg.type === "assistant") {
|
|
20953
|
+
const textBlock = msg.message.content.find(
|
|
20954
|
+
(b) => b.type === "text"
|
|
20955
|
+
);
|
|
20956
|
+
if (textBlock) {
|
|
20957
|
+
return textBlock.text.trim();
|
|
20958
|
+
}
|
|
20959
|
+
}
|
|
20960
|
+
}
|
|
20961
|
+
return null;
|
|
20962
|
+
}
|
|
20963
|
+
function emptyArtifactReport(artifactId, errors) {
|
|
20964
|
+
return {
|
|
20965
|
+
artifactId,
|
|
20966
|
+
title: "Not found",
|
|
20967
|
+
type: "unknown",
|
|
20968
|
+
marvinStatus: "unknown",
|
|
20969
|
+
marvinProgress: 0,
|
|
20970
|
+
sprint: null,
|
|
20971
|
+
parent: null,
|
|
20972
|
+
jiraKey: null,
|
|
20973
|
+
jiraStatus: null,
|
|
20974
|
+
jiraAssignee: null,
|
|
20975
|
+
jiraSubtaskProgress: null,
|
|
20976
|
+
proposedMarvinStatus: null,
|
|
20977
|
+
statusDrift: false,
|
|
20978
|
+
progressDrift: false,
|
|
20979
|
+
commentSignals: [],
|
|
20980
|
+
commentSummary: null,
|
|
20981
|
+
linkedIssues: [],
|
|
20982
|
+
linkedIssueSignals: [],
|
|
20983
|
+
children: [],
|
|
20984
|
+
proposedUpdates: [],
|
|
20985
|
+
appliedUpdates: [],
|
|
20986
|
+
signals: [],
|
|
20987
|
+
errors
|
|
20988
|
+
};
|
|
20989
|
+
}
|
|
20990
|
+
function formatArtifactReport(report) {
|
|
20991
|
+
const parts = [];
|
|
20992
|
+
parts.push(`# Artifact Assessment \u2014 ${report.artifactId}`);
|
|
20993
|
+
parts.push(report.title);
|
|
20994
|
+
parts.push("");
|
|
20995
|
+
parts.push(`## Marvin State`);
|
|
20996
|
+
const marvinParts = [`Status: ${report.marvinStatus}`, `Progress: ${report.marvinProgress}%`];
|
|
20997
|
+
if (report.sprint) marvinParts.push(`Sprint: ${report.sprint}`);
|
|
20998
|
+
if (report.parent) marvinParts.push(`Parent: ${report.parent}`);
|
|
20999
|
+
parts.push(marvinParts.join(" | "));
|
|
21000
|
+
parts.push("");
|
|
21001
|
+
if (report.jiraKey) {
|
|
21002
|
+
parts.push(`## Jira State (${report.jiraKey})`);
|
|
21003
|
+
const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
|
|
21004
|
+
if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
|
|
21005
|
+
if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
|
|
21006
|
+
parts.push(jiraParts.join(" | "));
|
|
21007
|
+
if (report.statusDrift) {
|
|
21008
|
+
parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
|
|
21009
|
+
}
|
|
21010
|
+
if (report.progressDrift && report.jiraSubtaskProgress !== null) {
|
|
21011
|
+
parts.push(`\u26A0 Progress drift: ${report.marvinProgress}% \u2192 ${report.jiraSubtaskProgress}%`);
|
|
21012
|
+
}
|
|
21013
|
+
parts.push("");
|
|
21014
|
+
}
|
|
21015
|
+
if (report.commentSummary) {
|
|
21016
|
+
parts.push(`## Comments`);
|
|
21017
|
+
parts.push(report.commentSummary);
|
|
21018
|
+
parts.push("");
|
|
21019
|
+
}
|
|
21020
|
+
if (report.children.length > 0) {
|
|
21021
|
+
const doneCount = report.children.filter((c) => DONE_STATUSES6.has(c.marvinStatus)).length;
|
|
21022
|
+
const childWeights = report.children.map((c) => {
|
|
21023
|
+
const { weight } = resolveWeight(void 0);
|
|
21024
|
+
return { weight, progress: c.marvinProgress };
|
|
21025
|
+
});
|
|
21026
|
+
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;
|
|
21027
|
+
const bar = progressBar(childProgress);
|
|
21028
|
+
parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
|
|
21029
|
+
for (const child of report.children) {
|
|
21030
|
+
formatArtifactChild(parts, child, 1);
|
|
21031
|
+
}
|
|
21032
|
+
parts.push("");
|
|
21033
|
+
}
|
|
21034
|
+
if (report.linkedIssues.length > 0) {
|
|
21035
|
+
parts.push(`## Linked Issues (${report.linkedIssues.length})`);
|
|
21036
|
+
for (const link of report.linkedIssues) {
|
|
21037
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
21038
|
+
parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
|
|
21039
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
21040
|
+
if (signal?.commentSummary) {
|
|
21041
|
+
parts.push(` \u{1F4AC} ${signal.commentSummary}`);
|
|
21042
|
+
}
|
|
21043
|
+
}
|
|
21044
|
+
parts.push("");
|
|
21045
|
+
}
|
|
21046
|
+
if (report.signals.length > 0) {
|
|
21047
|
+
parts.push(`## Signals`);
|
|
21048
|
+
for (const s of report.signals) {
|
|
21049
|
+
parts.push(` ${s}`);
|
|
21050
|
+
}
|
|
21051
|
+
parts.push("");
|
|
21052
|
+
}
|
|
21053
|
+
if (report.proposedUpdates.length > 0) {
|
|
21054
|
+
parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
|
|
21055
|
+
for (const update of report.proposedUpdates) {
|
|
21056
|
+
parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
21057
|
+
parts.push(` Reason: ${update.reason}`);
|
|
21058
|
+
}
|
|
21059
|
+
parts.push("");
|
|
21060
|
+
parts.push("Run with applyUpdates=true to apply these changes.");
|
|
21061
|
+
parts.push("");
|
|
21062
|
+
}
|
|
21063
|
+
if (report.appliedUpdates.length > 0) {
|
|
21064
|
+
parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
|
|
21065
|
+
for (const update of report.appliedUpdates) {
|
|
21066
|
+
parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
21067
|
+
}
|
|
21068
|
+
parts.push("");
|
|
21069
|
+
}
|
|
21070
|
+
if (report.errors.length > 0) {
|
|
21071
|
+
parts.push(`## Errors`);
|
|
21072
|
+
for (const err of report.errors) {
|
|
21073
|
+
parts.push(` ${err}`);
|
|
21074
|
+
}
|
|
21075
|
+
parts.push("");
|
|
21076
|
+
}
|
|
21077
|
+
return parts.join("\n");
|
|
21078
|
+
}
|
|
21079
|
+
function formatArtifactChild(parts, child, depth) {
|
|
21080
|
+
const indent = " ".repeat(depth);
|
|
21081
|
+
const icon = DONE_STATUSES6.has(child.marvinStatus) ? "\u2713" : child.marvinStatus === "blocked" ? "\u{1F6AB}" : child.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
21082
|
+
const jiraLabel = child.jiraKey ? ` [${child.jiraKey}: ${child.jiraStatus ?? "?"}]` : "";
|
|
21083
|
+
const driftLabel = child.statusDrift ? ` \u26A0drift \u2192 ${child.proposedMarvinStatus}` : "";
|
|
21084
|
+
const signalHints = [];
|
|
21085
|
+
for (const s of child.signals) {
|
|
21086
|
+
if (s.startsWith("\u2705 No active")) continue;
|
|
21087
|
+
signalHints.push(s);
|
|
21088
|
+
}
|
|
21089
|
+
parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
|
|
21090
|
+
if (child.commentSummary) {
|
|
21091
|
+
parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
|
|
21092
|
+
}
|
|
21093
|
+
for (const hint of signalHints) {
|
|
21094
|
+
parts.push(`${indent} ${hint}`);
|
|
21095
|
+
}
|
|
21096
|
+
for (const grandchild of child.children) {
|
|
21097
|
+
formatArtifactChild(parts, grandchild, depth + 1);
|
|
21098
|
+
}
|
|
21099
|
+
}
|
|
20349
21100
|
|
|
20350
21101
|
// src/skills/builtin/jira/tools.ts
|
|
20351
21102
|
var JIRA_TYPE = "jira-issue";
|
|
@@ -21132,7 +21883,8 @@ function createJiraTools(store, projectConfig) {
|
|
|
21132
21883
|
{
|
|
21133
21884
|
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
21134
21885
|
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)")
|
|
21886
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)"),
|
|
21887
|
+
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
21888
|
},
|
|
21137
21889
|
async (args) => {
|
|
21138
21890
|
const jira = createJiraClient(jiraUserConfig);
|
|
@@ -21145,6 +21897,7 @@ function createJiraTools(store, projectConfig) {
|
|
|
21145
21897
|
sprintId: args.sprintId,
|
|
21146
21898
|
analyzeComments: args.analyzeComments ?? false,
|
|
21147
21899
|
applyUpdates: args.applyUpdates ?? false,
|
|
21900
|
+
traverseLinks: args.traverseLinks ?? false,
|
|
21148
21901
|
statusMap
|
|
21149
21902
|
}
|
|
21150
21903
|
);
|
|
@@ -21154,6 +21907,34 @@ function createJiraTools(store, projectConfig) {
|
|
|
21154
21907
|
};
|
|
21155
21908
|
},
|
|
21156
21909
|
{ annotations: { readOnlyHint: false } }
|
|
21910
|
+
),
|
|
21911
|
+
// --- Single-artifact assessment ---
|
|
21912
|
+
tool20(
|
|
21913
|
+
"assess_artifact",
|
|
21914
|
+
"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).",
|
|
21915
|
+
{
|
|
21916
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
|
|
21917
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
|
|
21918
|
+
},
|
|
21919
|
+
async (args) => {
|
|
21920
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
21921
|
+
if (!jira) return jiraNotConfiguredError();
|
|
21922
|
+
const report = await assessArtifact(
|
|
21923
|
+
store,
|
|
21924
|
+
jira.client,
|
|
21925
|
+
jira.host,
|
|
21926
|
+
{
|
|
21927
|
+
artifactId: args.artifactId,
|
|
21928
|
+
applyUpdates: args.applyUpdates ?? false,
|
|
21929
|
+
statusMap
|
|
21930
|
+
}
|
|
21931
|
+
);
|
|
21932
|
+
return {
|
|
21933
|
+
content: [{ type: "text", text: formatArtifactReport(report) }],
|
|
21934
|
+
isError: report.errors.length > 0 && report.type === "unknown"
|
|
21935
|
+
};
|
|
21936
|
+
},
|
|
21937
|
+
{ annotations: { readOnlyHint: false } }
|
|
21157
21938
|
)
|
|
21158
21939
|
];
|
|
21159
21940
|
}
|
|
@@ -21255,7 +22036,8 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
21255
22036
|
- \`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
22037
|
- \`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
22038
|
- \`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.
|
|
22039
|
+
- \`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.
|
|
22040
|
+
- \`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
22041
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
21260
22042
|
- \`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
22043
|
- \`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 +22054,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
21272
22054
|
2. Review focus area rollups, status drift, and blockers
|
|
21273
22055
|
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
21274
22056
|
|
|
22057
|
+
**Single-artifact deep dive:**
|
|
22058
|
+
1. Call \`assess_artifact\` with an artifact ID to get a focused assessment with Jira sync, comment analysis, link traversal, and child rollup
|
|
22059
|
+
2. Review signals (blockers, unblocks, handoffs) and proposed updates
|
|
22060
|
+
3. Use \`applyUpdates=true\` to apply changes
|
|
22061
|
+
|
|
21275
22062
|
**Daily review workflow:**
|
|
21276
22063
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
21277
22064
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -21296,6 +22083,7 @@ ${COMMON_WORKFLOW}
|
|
|
21296
22083
|
**As Product Owner, use Jira integration to:**
|
|
21297
22084
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
21298
22085
|
- Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
|
|
22086
|
+
- Use \`assess_artifact\` for deep dives into specific features or epics \u2014 full Jira context, linked issues, and child rollup
|
|
21299
22087
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
21300
22088
|
- Push approved features as Stories for development tracking
|
|
21301
22089
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -21309,6 +22097,7 @@ ${COMMON_WORKFLOW}
|
|
|
21309
22097
|
**As Tech Lead, use Jira integration to:**
|
|
21310
22098
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
21311
22099
|
- Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
|
|
22100
|
+
- Use \`assess_artifact\` to investigate a specific task \u2014 see full dependency chain, comment context, and blocker status
|
|
21312
22101
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
21313
22102
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
21314
22103
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -21324,6 +22113,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
21324
22113
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
21325
22114
|
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
21326
22115
|
- Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
|
|
22116
|
+
- Use \`assess_artifact\` for focused status checks on critical items \u2014 full Jira context without running the whole sprint assessment
|
|
21327
22117
|
- Pull sprint issues for tracking progress and blockers
|
|
21328
22118
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
21329
22119
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -24718,7 +25508,7 @@ function buildHealthGauge(categories) {
|
|
|
24718
25508
|
}
|
|
24719
25509
|
|
|
24720
25510
|
// src/web/templates/pages/po/dashboard.ts
|
|
24721
|
-
var
|
|
25511
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24722
25512
|
var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
24723
25513
|
function poDashboardPage(ctx) {
|
|
24724
25514
|
const overview = getOverviewData(ctx.store);
|
|
@@ -24763,7 +25553,7 @@ function poDashboardPage(ctx) {
|
|
|
24763
25553
|
sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
|
|
24764
25554
|
}
|
|
24765
25555
|
}
|
|
24766
|
-
const featuresDone = features.filter((d) =>
|
|
25556
|
+
const featuresDone = features.filter((d) => DONE_STATUSES7.has(d.frontmatter.status)).length;
|
|
24767
25557
|
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
24768
25558
|
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
24769
25559
|
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
|
|
@@ -24832,7 +25622,7 @@ function poDashboardPage(ctx) {
|
|
|
24832
25622
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
24833
25623
|
const atRiskItems = [];
|
|
24834
25624
|
for (const f of features) {
|
|
24835
|
-
if (
|
|
25625
|
+
if (DONE_STATUSES7.has(f.frontmatter.status)) continue;
|
|
24836
25626
|
const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
|
|
24837
25627
|
const reasons = [];
|
|
24838
25628
|
let blocked = 0;
|
|
@@ -24844,7 +25634,7 @@ function poDashboardPage(ctx) {
|
|
|
24844
25634
|
if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
|
|
24845
25635
|
for (const epic of fEpics) {
|
|
24846
25636
|
const td = epic.frontmatter.targetDate;
|
|
24847
|
-
if (td && td < today && !
|
|
25637
|
+
if (td && td < today && !DONE_STATUSES7.has(epic.frontmatter.status)) {
|
|
24848
25638
|
reasons.push(`${epic.frontmatter.id} overdue`);
|
|
24849
25639
|
}
|
|
24850
25640
|
}
|
|
@@ -25122,7 +25912,7 @@ function poBacklogPage(ctx) {
|
|
|
25122
25912
|
}
|
|
25123
25913
|
}
|
|
25124
25914
|
}
|
|
25125
|
-
const
|
|
25915
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25126
25916
|
function featureTaskStats(featureId) {
|
|
25127
25917
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
25128
25918
|
let total = 0;
|
|
@@ -25131,7 +25921,7 @@ function poBacklogPage(ctx) {
|
|
|
25131
25921
|
for (const epic of fEpics) {
|
|
25132
25922
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
25133
25923
|
total++;
|
|
25134
|
-
if (
|
|
25924
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
25135
25925
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
25136
25926
|
}
|
|
25137
25927
|
}
|
|
@@ -25377,23 +26167,34 @@ function hashString(s) {
|
|
|
25377
26167
|
}
|
|
25378
26168
|
return Math.abs(h);
|
|
25379
26169
|
}
|
|
26170
|
+
var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
|
|
26171
|
+
var DEFAULT_WEIGHT3 = 3;
|
|
25380
26172
|
function countFocusStats(items) {
|
|
25381
26173
|
let total = 0;
|
|
25382
26174
|
let done = 0;
|
|
25383
26175
|
let inProgress = 0;
|
|
25384
|
-
|
|
26176
|
+
let totalWeight = 0;
|
|
26177
|
+
let weightedSum = 0;
|
|
26178
|
+
function walkStats(list) {
|
|
25385
26179
|
for (const w of list) {
|
|
25386
26180
|
if (w.type !== "contribution") {
|
|
25387
26181
|
total++;
|
|
25388
26182
|
const s = w.status.toLowerCase();
|
|
25389
|
-
if (s
|
|
26183
|
+
if (DONE_STATUS_SET.has(s)) done++;
|
|
25390
26184
|
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
25391
26185
|
}
|
|
25392
|
-
if (w.children)
|
|
26186
|
+
if (w.children) walkStats(w.children);
|
|
25393
26187
|
}
|
|
25394
26188
|
}
|
|
25395
|
-
|
|
25396
|
-
|
|
26189
|
+
walkStats(items);
|
|
26190
|
+
for (const w of items) {
|
|
26191
|
+
if (w.type === "contribution") continue;
|
|
26192
|
+
const weight = w.weight ?? DEFAULT_WEIGHT3;
|
|
26193
|
+
const progress = w.progress ?? (DONE_STATUS_SET.has(w.status.toLowerCase()) ? 100 : 0);
|
|
26194
|
+
totalWeight += weight;
|
|
26195
|
+
weightedSum += weight * progress;
|
|
26196
|
+
}
|
|
26197
|
+
return { total, done, inProgress, weightedProgress: totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 };
|
|
25397
26198
|
}
|
|
25398
26199
|
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
25399
26200
|
function ownerBadge2(owner) {
|
|
@@ -25442,7 +26243,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
25442
26243
|
for (const [focus, groupItems] of focusGroups) {
|
|
25443
26244
|
const color = focusColorMap.get(focus);
|
|
25444
26245
|
const stats = countFocusStats(groupItems);
|
|
25445
|
-
const pct = stats.
|
|
26246
|
+
const pct = stats.weightedProgress;
|
|
25446
26247
|
const summaryParts = [];
|
|
25447
26248
|
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
25448
26249
|
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
@@ -25485,7 +26286,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
25485
26286
|
{ titleTag: "h3", defaultCollapsed }
|
|
25486
26287
|
);
|
|
25487
26288
|
}
|
|
25488
|
-
var
|
|
26289
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
|
|
25489
26290
|
function computeOwnerCompletionPct(items, owner) {
|
|
25490
26291
|
let total = 0;
|
|
25491
26292
|
let progressSum = 0;
|
|
@@ -25493,7 +26294,7 @@ function computeOwnerCompletionPct(items, owner) {
|
|
|
25493
26294
|
for (const w of list) {
|
|
25494
26295
|
if (w.type !== "contribution" && w.owner === owner) {
|
|
25495
26296
|
total++;
|
|
25496
|
-
progressSum += w.progress ?? (
|
|
26297
|
+
progressSum += w.progress ?? (DONE_STATUSES8.has(w.status) ? 100 : 0);
|
|
25497
26298
|
}
|
|
25498
26299
|
if (w.children) walk(w.children);
|
|
25499
26300
|
}
|
|
@@ -25514,7 +26315,7 @@ function filterItemsByOwner(items, owner) {
|
|
|
25514
26315
|
}
|
|
25515
26316
|
|
|
25516
26317
|
// src/web/templates/pages/po/delivery.ts
|
|
25517
|
-
var
|
|
26318
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25518
26319
|
var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
25519
26320
|
var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
25520
26321
|
function priorityClass2(p) {
|
|
@@ -25655,7 +26456,7 @@ function poDeliveryPage(ctx) {
|
|
|
25655
26456
|
}
|
|
25656
26457
|
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
25657
26458
|
}
|
|
25658
|
-
const nonDoneFeatures = features.filter((f) => !
|
|
26459
|
+
const nonDoneFeatures = features.filter((f) => !DONE_STATUSES9.has(f.frontmatter.status)).sort((a, b) => {
|
|
25659
26460
|
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
25660
26461
|
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
25661
26462
|
if (pa !== pb) return pa - pb;
|
|
@@ -25864,7 +26665,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
|
25864
26665
|
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
25865
26666
|
|
|
25866
26667
|
// src/web/templates/pages/dm/dashboard.ts
|
|
25867
|
-
var
|
|
26668
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25868
26669
|
function progressBar3(pct) {
|
|
25869
26670
|
return `<div class="sprint-progress-bar">
|
|
25870
26671
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -25875,7 +26676,7 @@ function dmDashboardPage(ctx) {
|
|
|
25875
26676
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
25876
26677
|
const upcoming = getUpcomingData(ctx.store);
|
|
25877
26678
|
const actions = ctx.store.list({ type: "action" });
|
|
25878
|
-
const openActions = actions.filter((d) => !
|
|
26679
|
+
const openActions = actions.filter((d) => !DONE_STATUSES10.has(d.frontmatter.status));
|
|
25879
26680
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
25880
26681
|
const statsCards = `
|
|
25881
26682
|
<div class="cards">
|
|
@@ -26096,7 +26897,7 @@ function dmSprintPage(ctx) {
|
|
|
26096
26897
|
}
|
|
26097
26898
|
|
|
26098
26899
|
// src/web/templates/pages/dm/actions.ts
|
|
26099
|
-
var
|
|
26900
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26100
26901
|
function urgencyBadge(tier) {
|
|
26101
26902
|
const labels = {
|
|
26102
26903
|
overdue: "Overdue",
|
|
@@ -26116,7 +26917,7 @@ function urgencyRowClass(tier) {
|
|
|
26116
26917
|
function dmActionsPage(ctx) {
|
|
26117
26918
|
const upcoming = getUpcomingData(ctx.store);
|
|
26118
26919
|
const allActions = ctx.store.list({ type: "action" });
|
|
26119
|
-
const openActions = allActions.filter((d) => !
|
|
26920
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
|
|
26120
26921
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
26121
26922
|
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
26122
26923
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
@@ -26201,7 +27002,7 @@ function dmActionsPage(ctx) {
|
|
|
26201
27002
|
}
|
|
26202
27003
|
|
|
26203
27004
|
// src/web/templates/pages/dm/risks.ts
|
|
26204
|
-
var
|
|
27005
|
+
var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26205
27006
|
function dmRisksPage(ctx) {
|
|
26206
27007
|
const allDocs = ctx.store.list();
|
|
26207
27008
|
const upcoming = getUpcomingData(ctx.store);
|
|
@@ -26212,7 +27013,7 @@ function dmRisksPage(ctx) {
|
|
|
26212
27013
|
const todayMs = new Date(today).getTime();
|
|
26213
27014
|
const fourteenDaysMs = 14 * 864e5;
|
|
26214
27015
|
const agingItems = allDocs.filter((d) => {
|
|
26215
|
-
if (
|
|
27016
|
+
if (DONE_STATUSES12.has(d.frontmatter.status)) return false;
|
|
26216
27017
|
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
26217
27018
|
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
26218
27019
|
return todayMs - createdMs > fourteenDaysMs;
|
|
@@ -26326,7 +27127,7 @@ function dmRisksPage(ctx) {
|
|
|
26326
27127
|
}
|
|
26327
27128
|
|
|
26328
27129
|
// src/web/templates/pages/dm/meetings.ts
|
|
26329
|
-
var
|
|
27130
|
+
var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26330
27131
|
function dmMeetingsPage(ctx) {
|
|
26331
27132
|
const meetings = ctx.store.list({ type: "meeting" });
|
|
26332
27133
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -26372,7 +27173,7 @@ function dmMeetingsPage(ctx) {
|
|
|
26372
27173
|
${sortedMeetings.map((m) => {
|
|
26373
27174
|
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
26374
27175
|
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
26375
|
-
const openCount = relatedActions.filter((a) => !
|
|
27176
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES13.has(a.frontmatter.status)).length;
|
|
26376
27177
|
return `
|
|
26377
27178
|
<tr>
|
|
26378
27179
|
<td>${formatDate(date5)}</td>
|
|
@@ -26387,7 +27188,7 @@ function dmMeetingsPage(ctx) {
|
|
|
26387
27188
|
const recentMeetingActions = [];
|
|
26388
27189
|
for (const [mid, acts] of meetingActionMap) {
|
|
26389
27190
|
for (const act of acts) {
|
|
26390
|
-
if (!
|
|
27191
|
+
if (!DONE_STATUSES13.has(act.frontmatter.status)) {
|
|
26391
27192
|
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
26392
27193
|
}
|
|
26393
27194
|
}
|
|
@@ -26582,7 +27383,7 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
|
26582
27383
|
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
26583
27384
|
|
|
26584
27385
|
// src/web/templates/pages/tl/dashboard.ts
|
|
26585
|
-
var
|
|
27386
|
+
var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26586
27387
|
var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
26587
27388
|
function tlDashboardPage(ctx) {
|
|
26588
27389
|
const epics = ctx.store.list({ type: "epic" });
|
|
@@ -26590,8 +27391,8 @@ function tlDashboardPage(ctx) {
|
|
|
26590
27391
|
const decisions = ctx.store.list({ type: "decision" });
|
|
26591
27392
|
const questions = ctx.store.list({ type: "question" });
|
|
26592
27393
|
const diagrams = getDiagramData(ctx.store);
|
|
26593
|
-
const openEpics = epics.filter((d) => !
|
|
26594
|
-
const openTasks = tasks.filter((d) => !
|
|
27394
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
|
|
27395
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
|
|
26595
27396
|
const technicalDecisions = decisions.filter((d) => {
|
|
26596
27397
|
const tags = d.frontmatter.tags ?? [];
|
|
26597
27398
|
return tags.some((t) => {
|
|
@@ -26649,7 +27450,7 @@ function tlDashboardPage(ctx) {
|
|
|
26649
27450
|
}
|
|
26650
27451
|
|
|
26651
27452
|
// src/web/templates/pages/tl/backlog.ts
|
|
26652
|
-
var
|
|
27453
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26653
27454
|
function tlBacklogPage(ctx) {
|
|
26654
27455
|
const epics = ctx.store.list({ type: "epic" });
|
|
26655
27456
|
const tasks = ctx.store.list({ type: "task" });
|
|
@@ -26686,7 +27487,7 @@ function tlBacklogPage(ctx) {
|
|
|
26686
27487
|
<tbody>
|
|
26687
27488
|
${sortedEpics.map((e) => {
|
|
26688
27489
|
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
26689
|
-
const done = eTasks.filter((t) =>
|
|
27490
|
+
const done = eTasks.filter((t) => DONE_STATUSES15.has(t.frontmatter.status)).length;
|
|
26690
27491
|
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
26691
27492
|
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
26692
27493
|
return `
|
|
@@ -26706,7 +27507,7 @@ function tlBacklogPage(ctx) {
|
|
|
26706
27507
|
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
26707
27508
|
}
|
|
26708
27509
|
const unassignedTasks = tasks.filter(
|
|
26709
|
-
(t) => !assignedTaskIds.has(t.frontmatter.id) && !
|
|
27510
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES15.has(t.frontmatter.status)
|
|
26710
27511
|
);
|
|
26711
27512
|
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
26712
27513
|
"tl-backlog-unassigned",
|