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.
@@ -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 DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
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 COMPLEXITY_WEIGHTS = {
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 DEFAULT_WEIGHT = 3;
19838
+ var DEFAULT_WEIGHT2 = 3;
19860
19839
  function resolveWeight(complexity) {
19861
- if (complexity && complexity in COMPLEXITY_WEIGHTS) {
19862
- return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
19840
+ if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
19841
+ return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
19863
19842
  }
19864
- return { weight: DEFAULT_WEIGHT, weightSource: "default" };
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) => DONE_STATUSES7.has(i.marvinStatus)).length;
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 = DONE_STATUSES7.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
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 DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => DONE_STATUSES8.has(d.frontmatter.status)).length;
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 (DONE_STATUSES8.has(f.frontmatter.status)) continue;
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 && !DONE_STATUSES8.has(epic.frontmatter.status)) {
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 DONE_STATUSES17 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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 (DONE_STATUSES17.has(t.frontmatter.status)) done++;
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
- function walk(list) {
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 === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
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) walk(w.children);
26161
+ if (w.children) walkStats(w.children);
25393
26162
  }
25394
26163
  }
25395
- walk(items);
25396
- return { total, done, inProgress };
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.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
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 DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
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 ?? (DONE_STATUSES9.has(w.status) ? 100 : 0);
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 DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES10.has(f.frontmatter.status)).sort((a, b) => {
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 DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES11.has(d.frontmatter.status));
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 DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES12.has(d.frontmatter.status));
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 DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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 (DONE_STATUSES13.has(d.frontmatter.status)) return false;
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 DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES14.has(a.frontmatter.status)).length;
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 (!DONE_STATUSES14.has(act.frontmatter.status)) {
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 DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES15.has(d.frontmatter.status));
26594
- const openTasks = tasks.filter((d) => !DONE_STATUSES15.has(d.frontmatter.status));
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 DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => DONE_STATUSES16.has(t.frontmatter.status)).length;
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) && !DONE_STATUSES16.has(t.frontmatter.status)
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",