mrvn-cli 0.5.13 → 0.5.14

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.
@@ -176,9 +176,9 @@ var DocumentStore = class {
176
176
  }
177
177
  }
178
178
  }
179
- list(query4) {
179
+ list(query5) {
180
180
  const results = [];
181
- const types = query4?.type ? [query4.type] : Object.keys(this.typeDirs);
181
+ const types = query5?.type ? [query5.type] : Object.keys(this.typeDirs);
182
182
  for (const type of types) {
183
183
  const dirName = this.typeDirs[type];
184
184
  if (!dirName) continue;
@@ -189,9 +189,9 @@ var DocumentStore = class {
189
189
  const filePath = path3.join(dir, file2);
190
190
  const raw = fs3.readFileSync(filePath, "utf-8");
191
191
  const doc = parseDocument(raw, filePath);
192
- if (query4?.status && doc.frontmatter.status !== query4.status) continue;
193
- if (query4?.owner && doc.frontmatter.owner !== query4.owner) continue;
194
- if (query4?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query4.tag)))
192
+ if (query5?.status && doc.frontmatter.status !== query5.status) continue;
193
+ if (query5?.owner && doc.frontmatter.owner !== query5.owner) continue;
194
+ if (query5?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query5.tag)))
195
195
  continue;
196
196
  results.push(doc);
197
197
  }
@@ -19196,6 +19196,11 @@ function mapJiraStatusForTask(status, configMap) {
19196
19196
  const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
19197
19197
  return lookup.get(status.toLowerCase()) ?? "backlog";
19198
19198
  }
19199
+ function extractJiraKeyFromTags(tags) {
19200
+ if (!tags) return void 0;
19201
+ const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
19202
+ return tag ? tag.slice(5) : void 0;
19203
+ }
19199
19204
  function computeSubtaskProgress(subtasks) {
19200
19205
  if (subtasks.length === 0) return 0;
19201
19206
  const done = subtasks.filter(
@@ -19538,10 +19543,10 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
19538
19543
  jiraKeyToArtifacts.set(jk, list);
19539
19544
  }
19540
19545
  }
19541
- const BATCH_SIZE = 5;
19546
+ const BATCH_SIZE2 = 5;
19542
19547
  const issues = searchResult.issues;
19543
- for (let i = 0; i < issues.length; i += BATCH_SIZE) {
19544
- const batch = issues.slice(i, i + BATCH_SIZE);
19548
+ for (let i = 0; i < issues.length; i += BATCH_SIZE2) {
19549
+ const batch = issues.slice(i, i + BATCH_SIZE2);
19545
19550
  const results = await Promise.allSettled(
19546
19551
  batch.map(
19547
19552
  (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
@@ -19765,6 +19770,420 @@ function generateProposedActions(issues) {
19765
19770
  return actions;
19766
19771
  }
19767
19772
 
19773
+ // src/skills/builtin/jira/sprint-progress.ts
19774
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
19775
+ var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
19776
+ var BATCH_SIZE = 5;
19777
+ async function assessSprintProgress(store, client, host, options = {}) {
19778
+ const errors = [];
19779
+ const sprintData = collectSprintSummaryData(store, options.sprintId);
19780
+ if (!sprintData) {
19781
+ return {
19782
+ sprintId: options.sprintId ?? "unknown",
19783
+ sprintTitle: "Sprint not found",
19784
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
19785
+ timeline: { startDate: null, endDate: null, daysRemaining: 0, totalDays: 0, percentComplete: 0 },
19786
+ overallProgress: 0,
19787
+ itemReports: [],
19788
+ focusAreas: [],
19789
+ driftItems: [],
19790
+ blockers: [],
19791
+ proposedUpdates: [],
19792
+ appliedUpdates: [],
19793
+ errors: [`Sprint ${options.sprintId ?? "(active)"} not found. Create a sprint artifact first.`]
19794
+ };
19795
+ }
19796
+ const sprintTag = `sprint:${sprintData.sprint.id}`;
19797
+ const actions = store.list({ type: "action", tag: sprintTag });
19798
+ const tasks = store.list({ type: "task", tag: sprintTag });
19799
+ const sprintItemIds = new Set([...actions, ...tasks].map((d) => d.frontmatter.id));
19800
+ const allTasks = store.list({ type: "task" });
19801
+ const allActions = store.list({ type: "action" });
19802
+ const nestedTasks = allTasks.filter(
19803
+ (d) => !sprintItemIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintItemIds.has(d.frontmatter.aboutArtifact)
19804
+ );
19805
+ const nestedActions = allActions.filter(
19806
+ (d) => !sprintItemIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintItemIds.has(d.frontmatter.aboutArtifact)
19807
+ );
19808
+ const allItems = [...actions, ...tasks, ...nestedTasks, ...nestedActions];
19809
+ const itemJiraKeys = /* @__PURE__ */ new Map();
19810
+ for (const doc of allItems) {
19811
+ const jiraKey = doc.frontmatter.jiraKey ?? extractJiraKeyFromTags(doc.frontmatter.tags);
19812
+ if (jiraKey) {
19813
+ itemJiraKeys.set(doc.frontmatter.id, jiraKey);
19814
+ }
19815
+ }
19816
+ const jiraKeys = [...new Set(itemJiraKeys.values())];
19817
+ const jiraIssues = /* @__PURE__ */ new Map();
19818
+ for (let i = 0; i < jiraKeys.length; i += BATCH_SIZE) {
19819
+ const batch = jiraKeys.slice(i, i + BATCH_SIZE);
19820
+ const results = await Promise.allSettled(
19821
+ batch.map(async (key) => {
19822
+ const [issue2, comments] = await Promise.all([
19823
+ client.getIssueWithLinks(key),
19824
+ client.getComments(key)
19825
+ ]);
19826
+ return { key, issue: issue2, comments };
19827
+ })
19828
+ );
19829
+ for (const result of results) {
19830
+ if (result.status === "fulfilled") {
19831
+ jiraIssues.set(result.value.key, {
19832
+ issue: result.value.issue,
19833
+ comments: result.value.comments
19834
+ });
19835
+ } else {
19836
+ const batchKey = batch[results.indexOf(result)];
19837
+ errors.push(`Failed to fetch ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
19838
+ }
19839
+ }
19840
+ }
19841
+ const proposedUpdates = [];
19842
+ const itemReports = [];
19843
+ const childReportsByParent = /* @__PURE__ */ new Map();
19844
+ for (const doc of allItems) {
19845
+ const fm = doc.frontmatter;
19846
+ const jiraKey = itemJiraKeys.get(fm.id) ?? null;
19847
+ const jiraData = jiraKey ? jiraIssues.get(jiraKey) : null;
19848
+ let jiraStatus = null;
19849
+ let proposedMarvinStatus = null;
19850
+ let jiraSubtaskProgress = null;
19851
+ const commentSignals = [];
19852
+ if (jiraData) {
19853
+ jiraStatus = jiraData.issue.fields.status.name;
19854
+ proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action);
19855
+ const subtasks = jiraData.issue.fields.subtasks ?? [];
19856
+ if (subtasks.length > 0) {
19857
+ jiraSubtaskProgress = computeSubtaskProgress(subtasks);
19858
+ }
19859
+ for (const comment of jiraData.comments) {
19860
+ const text = extractCommentText(comment.body);
19861
+ const signals = detectCommentSignals(text);
19862
+ commentSignals.push(...signals);
19863
+ }
19864
+ }
19865
+ const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
19866
+ const currentProgress = getEffectiveProgress(fm);
19867
+ const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
19868
+ if (statusDrift && proposedMarvinStatus) {
19869
+ proposedUpdates.push({
19870
+ artifactId: fm.id,
19871
+ field: "status",
19872
+ currentValue: fm.status,
19873
+ proposedValue: proposedMarvinStatus,
19874
+ reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
19875
+ });
19876
+ }
19877
+ if (progressDrift && jiraSubtaskProgress !== null) {
19878
+ proposedUpdates.push({
19879
+ artifactId: fm.id,
19880
+ field: "progress",
19881
+ currentValue: currentProgress,
19882
+ proposedValue: jiraSubtaskProgress,
19883
+ reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
19884
+ });
19885
+ }
19886
+ const tags = fm.tags ?? [];
19887
+ const focusTag = tags.find((t) => t.startsWith("focus:"));
19888
+ const report = {
19889
+ id: fm.id,
19890
+ title: fm.title,
19891
+ type: fm.type,
19892
+ marvinStatus: fm.status,
19893
+ marvinProgress: currentProgress,
19894
+ jiraKey,
19895
+ jiraStatus,
19896
+ jiraSubtaskProgress,
19897
+ proposedMarvinStatus,
19898
+ statusDrift,
19899
+ progressDrift,
19900
+ commentSignals,
19901
+ commentSummary: null,
19902
+ children: [],
19903
+ owner: fm.owner ?? null,
19904
+ focusArea: focusTag ? focusTag.slice(6) : null
19905
+ };
19906
+ const aboutArtifact = fm.aboutArtifact;
19907
+ if (aboutArtifact && sprintItemIds.has(aboutArtifact)) {
19908
+ if (!childReportsByParent.has(aboutArtifact)) {
19909
+ childReportsByParent.set(aboutArtifact, []);
19910
+ }
19911
+ childReportsByParent.get(aboutArtifact).push(report);
19912
+ }
19913
+ itemReports.push(report);
19914
+ }
19915
+ for (const report of itemReports) {
19916
+ const children = childReportsByParent.get(report.id);
19917
+ if (children) {
19918
+ report.children = children;
19919
+ }
19920
+ }
19921
+ const childIds = /* @__PURE__ */ new Set();
19922
+ for (const children of childReportsByParent.values()) {
19923
+ for (const child of children) childIds.add(child.id);
19924
+ }
19925
+ const rootReports = itemReports.filter((r) => !childIds.has(r.id));
19926
+ const focusAreaMap = /* @__PURE__ */ new Map();
19927
+ for (const report of rootReports) {
19928
+ const area = report.focusArea ?? "Uncategorized";
19929
+ if (!focusAreaMap.has(area)) focusAreaMap.set(area, []);
19930
+ focusAreaMap.get(area).push(report);
19931
+ }
19932
+ const focusAreas = [];
19933
+ for (const [name, items] of focusAreaMap) {
19934
+ const allFlatItems = items.flatMap((i) => [i, ...i.children]);
19935
+ const doneCount = allFlatItems.filter((i) => DONE_STATUSES7.has(i.marvinStatus)).length;
19936
+ const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
19937
+ const avgProgress = allFlatItems.length > 0 ? Math.round(allFlatItems.reduce((s, i) => s + i.marvinProgress, 0) / allFlatItems.length) : 0;
19938
+ focusAreas.push({
19939
+ name,
19940
+ items,
19941
+ totalCount: allFlatItems.length,
19942
+ doneCount,
19943
+ blockedCount,
19944
+ avgProgress
19945
+ });
19946
+ }
19947
+ focusAreas.sort((a, b) => {
19948
+ if (a.name === "Uncategorized") return 1;
19949
+ if (b.name === "Uncategorized") return -1;
19950
+ return a.name.localeCompare(b.name);
19951
+ });
19952
+ const driftItems = itemReports.filter((r) => r.statusDrift || r.progressDrift);
19953
+ const blockers = itemReports.filter(
19954
+ (r) => r.marvinStatus === "blocked" || r.commentSignals.some((s) => s.type === "blocker")
19955
+ );
19956
+ if (options.analyzeComments) {
19957
+ const itemsWithComments = itemReports.filter((r) => r.commentSignals.length > 0 && r.jiraKey);
19958
+ if (itemsWithComments.length > 0) {
19959
+ try {
19960
+ const summaries = await analyzeCommentsForProgress(
19961
+ itemsWithComments,
19962
+ jiraIssues,
19963
+ itemJiraKeys
19964
+ );
19965
+ for (const [artifactId, summary] of summaries) {
19966
+ const report = itemReports.find((r) => r.id === artifactId);
19967
+ if (report) report.commentSummary = summary;
19968
+ }
19969
+ } catch (err) {
19970
+ errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
19971
+ }
19972
+ }
19973
+ }
19974
+ const appliedUpdates = [];
19975
+ if (options.applyUpdates && proposedUpdates.length > 0) {
19976
+ for (const update of proposedUpdates) {
19977
+ try {
19978
+ store.update(update.artifactId, {
19979
+ [update.field]: update.proposedValue,
19980
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
19981
+ });
19982
+ const doc = store.get(update.artifactId);
19983
+ if (doc) {
19984
+ if (doc.frontmatter.type === "task") {
19985
+ propagateProgressFromTask(store, update.artifactId);
19986
+ } else if (doc.frontmatter.type === "action") {
19987
+ propagateProgressToAction(store, update.artifactId);
19988
+ }
19989
+ }
19990
+ appliedUpdates.push(update);
19991
+ } catch (err) {
19992
+ errors.push(
19993
+ `Failed to apply update to ${update.artifactId}: ${err instanceof Error ? err.message : String(err)}`
19994
+ );
19995
+ }
19996
+ }
19997
+ }
19998
+ return {
19999
+ sprintId: sprintData.sprint.id,
20000
+ sprintTitle: sprintData.sprint.title,
20001
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20002
+ timeline: {
20003
+ startDate: sprintData.sprint.startDate ?? null,
20004
+ endDate: sprintData.sprint.endDate ?? null,
20005
+ daysRemaining: sprintData.timeline.daysRemaining,
20006
+ totalDays: sprintData.timeline.totalDays,
20007
+ percentComplete: sprintData.timeline.percentComplete
20008
+ },
20009
+ overallProgress: sprintData.workItems.completionPct,
20010
+ itemReports: rootReports,
20011
+ focusAreas,
20012
+ driftItems,
20013
+ blockers,
20014
+ proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
20015
+ appliedUpdates,
20016
+ errors
20017
+ };
20018
+ }
20019
+ var COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments for progress signals.
20020
+
20021
+ For each item below, read the Jira comments and produce a 1-2 sentence progress summary.
20022
+ Focus on: what work was done, what's pending, any blockers or decisions mentioned.
20023
+
20024
+ Return your response as a JSON object mapping artifact IDs to summary strings.
20025
+ Example: {"T-001": "Backend API completed and deployed. Frontend integration pending review.", "A-003": "Blocked on infrastructure team approval."}
20026
+
20027
+ IMPORTANT: Only return the JSON object, no other text.`;
20028
+ async function analyzeCommentsForProgress(items, jiraIssues, itemJiraKeys) {
20029
+ const summaries = /* @__PURE__ */ new Map();
20030
+ const MAX_ITEMS_PER_CALL = 20;
20031
+ const itemsToAnalyze = items.slice(0, MAX_ITEMS_PER_CALL);
20032
+ const promptParts = [];
20033
+ for (const item of itemsToAnalyze) {
20034
+ const jiraKey = itemJiraKeys.get(item.id);
20035
+ if (!jiraKey) continue;
20036
+ const jiraData = jiraIssues.get(jiraKey);
20037
+ if (!jiraData || jiraData.comments.length === 0) continue;
20038
+ const commentTexts = jiraData.comments.map((c) => {
20039
+ const text = extractCommentText(c.body);
20040
+ return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
20041
+ }).join("\n");
20042
+ promptParts.push(`## ${item.id} \u2014 ${item.title} (${jiraKey}, Jira status: ${item.jiraStatus})
20043
+ Comments:
20044
+ ${commentTexts}`);
20045
+ }
20046
+ if (promptParts.length === 0) return summaries;
20047
+ const prompt = promptParts.join("\n\n");
20048
+ const result = query2({
20049
+ prompt,
20050
+ options: {
20051
+ systemPrompt: COMMENT_ANALYSIS_PROMPT,
20052
+ maxTurns: 1,
20053
+ tools: [],
20054
+ allowedTools: []
20055
+ }
20056
+ });
20057
+ for await (const msg of result) {
20058
+ if (msg.type === "assistant") {
20059
+ const textBlock = msg.message.content.find(
20060
+ (b) => b.type === "text"
20061
+ );
20062
+ if (textBlock) {
20063
+ try {
20064
+ const parsed = JSON.parse(textBlock.text);
20065
+ for (const [id, summary] of Object.entries(parsed)) {
20066
+ if (typeof summary === "string") {
20067
+ summaries.set(id, summary);
20068
+ }
20069
+ }
20070
+ } catch {
20071
+ const match = textBlock.text.match(/```(?:json)?\s*([\s\S]*?)```/);
20072
+ if (match) {
20073
+ try {
20074
+ const parsed = JSON.parse(match[1]);
20075
+ for (const [id, summary] of Object.entries(parsed)) {
20076
+ if (typeof summary === "string") {
20077
+ summaries.set(id, summary);
20078
+ }
20079
+ }
20080
+ } catch {
20081
+ }
20082
+ }
20083
+ }
20084
+ }
20085
+ }
20086
+ }
20087
+ return summaries;
20088
+ }
20089
+ function formatProgressReport(report) {
20090
+ const parts = [];
20091
+ parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
20092
+ parts.push(`${report.sprintTitle}`);
20093
+ parts.push(`Generated: ${report.generatedAt.slice(0, 16)}`);
20094
+ parts.push("");
20095
+ if (report.timeline.startDate && report.timeline.endDate) {
20096
+ parts.push(`## Timeline`);
20097
+ parts.push(`${report.timeline.startDate} \u2192 ${report.timeline.endDate}`);
20098
+ parts.push(`Days remaining: ${report.timeline.daysRemaining} / ${report.timeline.totalDays} (${report.timeline.percentComplete}% elapsed)`);
20099
+ parts.push(`Overall progress: ${report.overallProgress}%`);
20100
+ parts.push("");
20101
+ }
20102
+ if (report.focusAreas.length > 0) {
20103
+ parts.push(`## Focus Areas`);
20104
+ parts.push("");
20105
+ for (const area of report.focusAreas) {
20106
+ const bar = progressBar(area.avgProgress);
20107
+ parts.push(`### ${area.name} ${bar} ${area.avgProgress}%`);
20108
+ parts.push(`${area.doneCount}/${area.totalCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
20109
+ parts.push("");
20110
+ for (const item of area.items) {
20111
+ formatItemLine(parts, item, 0);
20112
+ }
20113
+ parts.push("");
20114
+ }
20115
+ }
20116
+ if (report.driftItems.length > 0) {
20117
+ parts.push(`## Status Drift (${report.driftItems.length} items)`);
20118
+ for (const item of report.driftItems) {
20119
+ const driftParts = [];
20120
+ if (item.statusDrift) {
20121
+ driftParts.push(`status: ${item.marvinStatus} \u2192 ${item.proposedMarvinStatus}`);
20122
+ }
20123
+ if (item.progressDrift && item.jiraSubtaskProgress !== null) {
20124
+ driftParts.push(`progress: ${item.marvinProgress}% \u2192 ${item.jiraSubtaskProgress}%`);
20125
+ }
20126
+ parts.push(` \u26A0 ${item.id} (${item.jiraKey}) \u2014 ${driftParts.join(", ")}`);
20127
+ }
20128
+ parts.push("");
20129
+ }
20130
+ if (report.blockers.length > 0) {
20131
+ parts.push(`## Blockers (${report.blockers.length})`);
20132
+ for (const item of report.blockers) {
20133
+ const blockerSignals = item.commentSignals.filter((s) => s.type === "blocker");
20134
+ parts.push(` \u{1F6AB} ${item.id} \u2014 ${item.title}${item.jiraKey ? ` (${item.jiraKey})` : ""}`);
20135
+ for (const signal of blockerSignals) {
20136
+ parts.push(` "${signal.snippet}"`);
20137
+ }
20138
+ }
20139
+ parts.push("");
20140
+ }
20141
+ if (report.proposedUpdates.length > 0) {
20142
+ parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
20143
+ for (const update of report.proposedUpdates) {
20144
+ parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
20145
+ parts.push(` Reason: ${update.reason}`);
20146
+ }
20147
+ parts.push("");
20148
+ parts.push("Run with applyUpdates=true to apply these changes.");
20149
+ parts.push("");
20150
+ }
20151
+ if (report.appliedUpdates.length > 0) {
20152
+ parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
20153
+ for (const update of report.appliedUpdates) {
20154
+ parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
20155
+ }
20156
+ parts.push("");
20157
+ }
20158
+ if (report.errors.length > 0) {
20159
+ parts.push(`## Errors`);
20160
+ for (const err of report.errors) {
20161
+ parts.push(` ${err}`);
20162
+ }
20163
+ parts.push("");
20164
+ }
20165
+ return parts.join("\n");
20166
+ }
20167
+ function formatItemLine(parts, item, depth) {
20168
+ const indent = " ".repeat(depth + 1);
20169
+ const statusIcon = DONE_STATUSES7.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
20170
+ const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
20171
+ const driftFlag = item.statusDrift ? " \u26A0drift" : "";
20172
+ const progressLabel = item.marvinProgress > 0 ? ` ${item.marvinProgress}%` : "";
20173
+ parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${jiraLabel}${driftFlag}`);
20174
+ if (item.commentSummary) {
20175
+ parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
20176
+ }
20177
+ for (const child of item.children) {
20178
+ formatItemLine(parts, child, depth + 1);
20179
+ }
20180
+ }
20181
+ function progressBar(pct) {
20182
+ const filled = Math.round(pct / 10);
20183
+ const empty = 10 - filled;
20184
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
20185
+ }
20186
+
19768
20187
  // src/skills/builtin/jira/tools.ts
19769
20188
  var JIRA_TYPE = "jira-issue";
19770
20189
  function jiraNotConfiguredError() {
@@ -20530,6 +20949,36 @@ function createJiraTools(store, projectConfig) {
20530
20949
  };
20531
20950
  },
20532
20951
  { annotations: { readOnlyHint: true } }
20952
+ ),
20953
+ // --- Sprint progress assessment ---
20954
+ tool20(
20955
+ "assess_sprint_progress",
20956
+ "Assess sprint progress by fetching live Jira statuses for all sprint-scoped items, detecting drift between Marvin and Jira, grouping by focus area with rollup progress, and extracting comment signals. Optionally applies updates and uses LLM for comment analysis.",
20957
+ {
20958
+ sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
20959
+ analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
20960
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
20961
+ },
20962
+ async (args) => {
20963
+ const jira = createJiraClient(jiraUserConfig);
20964
+ if (!jira) return jiraNotConfiguredError();
20965
+ const report = await assessSprintProgress(
20966
+ store,
20967
+ jira.client,
20968
+ jira.host,
20969
+ {
20970
+ sprintId: args.sprintId,
20971
+ analyzeComments: args.analyzeComments ?? false,
20972
+ applyUpdates: args.applyUpdates ?? false,
20973
+ statusMap
20974
+ }
20975
+ );
20976
+ return {
20977
+ content: [{ type: "text", text: formatProgressReport(report) }],
20978
+ isError: report.errors.length > 0 && report.itemReports.length === 0
20979
+ };
20980
+ },
20981
+ { annotations: { readOnlyHint: false } }
20533
20982
  )
20534
20983
  ];
20535
20984
  }
@@ -20631,6 +21080,7 @@ var COMMON_TOOLS = `**Available tools:**
20631
21080
  - \`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.
20632
21081
  - \`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.
20633
21082
  - \`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).
21083
+ - \`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.
20634
21084
  - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
20635
21085
  - \`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.
20636
21086
  - \`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).
@@ -20642,6 +21092,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
20642
21092
  2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
20643
21093
  3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
20644
21094
 
21095
+ **Sprint progress workflow:**
21096
+ 1. Call \`assess_sprint_progress\` to get a comprehensive view of all sprint items with live Jira data
21097
+ 2. Review focus area rollups, status drift, and blockers
21098
+ 3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
21099
+
20645
21100
  **Daily review workflow:**
20646
21101
  1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
20647
21102
  2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
@@ -20665,6 +21120,7 @@ ${COMMON_WORKFLOW}
20665
21120
 
20666
21121
  **As Product Owner, use Jira integration to:**
20667
21122
  - Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
21123
+ - Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
20668
21124
  - Pull stakeholder-reported issues for triage and prioritization
20669
21125
  - Push approved features as Stories for development tracking
20670
21126
  - Link decisions to Jira issues for audit trail and traceability
@@ -20677,6 +21133,7 @@ ${COMMON_WORKFLOW}
20677
21133
 
20678
21134
  **As Tech Lead, use Jira integration to:**
20679
21135
  - Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
21136
+ - Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
20680
21137
  - Pull technical issues and bugs for sprint planning and estimation
20681
21138
  - Push epics, tasks, and technical decisions to Jira for cross-team visibility
20682
21139
  - Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
@@ -20690,6 +21147,8 @@ This is a third path for progress tracking alongside Contributions and Meetings.
20690
21147
 
20691
21148
  **As Delivery Manager, use Jira integration to:**
20692
21149
  - Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
21150
+ - Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
21151
+ - Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
20693
21152
  - Pull sprint issues for tracking progress and blockers
20694
21153
  - Push actions and tasks to Jira for stakeholder visibility
20695
21154
  - Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
@@ -21274,7 +21733,7 @@ ${fragment}`);
21274
21733
  import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
21275
21734
 
21276
21735
  // src/skills/action-runner.ts
21277
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
21736
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
21278
21737
 
21279
21738
  // src/agent/mcp-server.ts
21280
21739
  import {
@@ -23425,7 +23884,7 @@ function personaPickerPage() {
23425
23884
  }
23426
23885
 
23427
23886
  // src/reports/sprint-summary/risk-assessment.ts
23428
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
23887
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
23429
23888
  var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
23430
23889
 
23431
23890
  IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
@@ -23454,7 +23913,7 @@ async function generateRiskAssessment(data, riskId, store) {
23454
23913
  const risk = data.risks.find((r) => r.id === riskId);
23455
23914
  if (!risk) return "Risk not found in sprint data.";
23456
23915
  const prompt = buildSingleRiskPrompt(data, risk, store);
23457
- const result = query2({
23916
+ const result = query3({
23458
23917
  prompt,
23459
23918
  options: {
23460
23919
  systemPrompt: SYSTEM_PROMPT2,
@@ -24084,7 +24543,7 @@ function buildHealthGauge(categories) {
24084
24543
  }
24085
24544
 
24086
24545
  // src/web/templates/pages/po/dashboard.ts
24087
- var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24546
+ var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24088
24547
  var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
24089
24548
  function poDashboardPage(ctx) {
24090
24549
  const overview = getOverviewData(ctx.store);
@@ -24129,7 +24588,7 @@ function poDashboardPage(ctx) {
24129
24588
  sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
24130
24589
  }
24131
24590
  }
24132
- const featuresDone = features.filter((d) => DONE_STATUSES7.has(d.frontmatter.status)).length;
24591
+ const featuresDone = features.filter((d) => DONE_STATUSES8.has(d.frontmatter.status)).length;
24133
24592
  const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
24134
24593
  const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
24135
24594
  const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
@@ -24198,7 +24657,7 @@ function poDashboardPage(ctx) {
24198
24657
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
24199
24658
  const atRiskItems = [];
24200
24659
  for (const f of features) {
24201
- if (DONE_STATUSES7.has(f.frontmatter.status)) continue;
24660
+ if (DONE_STATUSES8.has(f.frontmatter.status)) continue;
24202
24661
  const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
24203
24662
  const reasons = [];
24204
24663
  let blocked = 0;
@@ -24210,7 +24669,7 @@ function poDashboardPage(ctx) {
24210
24669
  if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
24211
24670
  for (const epic of fEpics) {
24212
24671
  const td = epic.frontmatter.targetDate;
24213
- if (td && td < today && !DONE_STATUSES7.has(epic.frontmatter.status)) {
24672
+ if (td && td < today && !DONE_STATUSES8.has(epic.frontmatter.status)) {
24214
24673
  reasons.push(`${epic.frontmatter.id} overdue`);
24215
24674
  }
24216
24675
  }
@@ -24488,7 +24947,7 @@ function poBacklogPage(ctx) {
24488
24947
  }
24489
24948
  }
24490
24949
  }
24491
- const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24950
+ const DONE_STATUSES17 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24492
24951
  function featureTaskStats(featureId) {
24493
24952
  const fEpics = featureToEpics.get(featureId) ?? [];
24494
24953
  let total = 0;
@@ -24497,7 +24956,7 @@ function poBacklogPage(ctx) {
24497
24956
  for (const epic of fEpics) {
24498
24957
  for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
24499
24958
  total++;
24500
- if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
24959
+ if (DONE_STATUSES17.has(t.frontmatter.status)) done++;
24501
24960
  progressSum += getEffectiveProgress(t.frontmatter);
24502
24961
  }
24503
24962
  }
@@ -24851,7 +25310,7 @@ function renderWorkItemsTable(items, options) {
24851
25310
  { titleTag: "h3", defaultCollapsed }
24852
25311
  );
24853
25312
  }
24854
- var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
25313
+ var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
24855
25314
  function computeOwnerCompletionPct(items, owner) {
24856
25315
  let total = 0;
24857
25316
  let progressSum = 0;
@@ -24859,7 +25318,7 @@ function computeOwnerCompletionPct(items, owner) {
24859
25318
  for (const w of list) {
24860
25319
  if (w.type !== "contribution" && w.owner === owner) {
24861
25320
  total++;
24862
- progressSum += w.progress ?? (DONE_STATUSES8.has(w.status) ? 100 : 0);
25321
+ progressSum += w.progress ?? (DONE_STATUSES9.has(w.status) ? 100 : 0);
24863
25322
  }
24864
25323
  if (w.children) walk(w.children);
24865
25324
  }
@@ -24880,7 +25339,7 @@ function filterItemsByOwner(items, owner) {
24880
25339
  }
24881
25340
 
24882
25341
  // src/web/templates/pages/po/delivery.ts
24883
- var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25342
+ var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24884
25343
  var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
24885
25344
  var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
24886
25345
  function priorityClass2(p) {
@@ -24900,7 +25359,7 @@ var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
24900
25359
  "priority-change",
24901
25360
  "market-insight"
24902
25361
  ]);
24903
- function progressBar(pct) {
25362
+ function progressBar2(pct) {
24904
25363
  return `<div class="sprint-progress-bar">
24905
25364
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
24906
25365
  <span class="sprint-progress-label">${pct}%</span>
@@ -25021,7 +25480,7 @@ function poDeliveryPage(ctx) {
25021
25480
  }
25022
25481
  return total > 0 ? Math.round(progressSum / total) : 0;
25023
25482
  }
25024
- const nonDoneFeatures = features.filter((f) => !DONE_STATUSES9.has(f.frontmatter.status)).sort((a, b) => {
25483
+ const nonDoneFeatures = features.filter((f) => !DONE_STATUSES10.has(f.frontmatter.status)).sort((a, b) => {
25025
25484
  const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
25026
25485
  const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
25027
25486
  if (pa !== pb) return pa - pb;
@@ -25080,7 +25539,7 @@ function poDeliveryPage(ctx) {
25080
25539
  <div class="subtitle">Sprint progress and feature delivery tracking</div>
25081
25540
  </div>
25082
25541
  ${sprintHeader}
25083
- ${progressBar(data.workItems.completionPct)}
25542
+ ${progressBar2(data.workItems.completionPct)}
25084
25543
  ${statsCards}
25085
25544
  ${workItemsSection}
25086
25545
  ${epicsSection}
@@ -25230,8 +25689,8 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
25230
25689
  registerPersonaPage("po", "stakeholders", poStakeholdersPage);
25231
25690
 
25232
25691
  // src/web/templates/pages/dm/dashboard.ts
25233
- var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25234
- function progressBar2(pct) {
25692
+ var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25693
+ function progressBar3(pct) {
25235
25694
  return `<div class="sprint-progress-bar">
25236
25695
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
25237
25696
  <span class="sprint-progress-label">${pct}%</span>
@@ -25241,7 +25700,7 @@ function dmDashboardPage(ctx) {
25241
25700
  const sprintData = getSprintSummaryData(ctx.store);
25242
25701
  const upcoming = getUpcomingData(ctx.store);
25243
25702
  const actions = ctx.store.list({ type: "action" });
25244
- const openActions = actions.filter((d) => !DONE_STATUSES10.has(d.frontmatter.status));
25703
+ const openActions = actions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
25245
25704
  const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
25246
25705
  const statsCards = `
25247
25706
  <div class="cards">
@@ -25279,7 +25738,7 @@ function dmDashboardPage(ctx) {
25279
25738
  <strong>${escapeHtml(sprintData.sprint.id)} \u2014 ${escapeHtml(sprintData.sprint.title)}</strong>
25280
25739
  ${sprintData.sprint.goal ? ` | ${escapeHtml(sprintData.sprint.goal)}` : ""}
25281
25740
  </div>
25282
- ${progressBar2(sprintData.workItems.completionPct)}` : "";
25741
+ ${progressBar3(sprintData.workItems.completionPct)}` : "";
25283
25742
  const riskItems = [];
25284
25743
  if (overdueActions.length > 0) riskItems.push(`${overdueActions.length} overdue action(s)`);
25285
25744
  if ((sprintData?.blockers.length ?? 0) > 0) riskItems.push(`${sprintData.blockers.length} blocker(s)`);
@@ -25329,7 +25788,7 @@ function dmDashboardPage(ctx) {
25329
25788
  }
25330
25789
 
25331
25790
  // src/web/templates/pages/dm/sprint.ts
25332
- function progressBar3(pct) {
25791
+ function progressBar4(pct) {
25333
25792
  return `<div class="sprint-progress-bar">
25334
25793
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
25335
25794
  <span class="sprint-progress-label">${pct}%</span>
@@ -25452,7 +25911,7 @@ function dmSprintPage(ctx) {
25452
25911
  <div class="subtitle">Sprint Execution ${dateRange}</div>
25453
25912
  </div>
25454
25913
  ${goalHtml}
25455
- ${progressBar3(data.timeline.percentComplete)}
25914
+ ${progressBar4(data.timeline.percentComplete)}
25456
25915
  ${statsCards}
25457
25916
  ${workItemsSection}
25458
25917
  ${epicsSection}
@@ -25462,7 +25921,7 @@ function dmSprintPage(ctx) {
25462
25921
  }
25463
25922
 
25464
25923
  // src/web/templates/pages/dm/actions.ts
25465
- var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25924
+ var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25466
25925
  function urgencyBadge(tier) {
25467
25926
  const labels = {
25468
25927
  overdue: "Overdue",
@@ -25482,7 +25941,7 @@ function urgencyRowClass(tier) {
25482
25941
  function dmActionsPage(ctx) {
25483
25942
  const upcoming = getUpcomingData(ctx.store);
25484
25943
  const allActions = ctx.store.list({ type: "action" });
25485
- const openActions = allActions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
25944
+ const openActions = allActions.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
25486
25945
  const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
25487
25946
  const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
25488
25947
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
@@ -25567,7 +26026,7 @@ function dmActionsPage(ctx) {
25567
26026
  }
25568
26027
 
25569
26028
  // src/web/templates/pages/dm/risks.ts
25570
- var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
26029
+ var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25571
26030
  function dmRisksPage(ctx) {
25572
26031
  const allDocs = ctx.store.list();
25573
26032
  const upcoming = getUpcomingData(ctx.store);
@@ -25578,7 +26037,7 @@ function dmRisksPage(ctx) {
25578
26037
  const todayMs = new Date(today).getTime();
25579
26038
  const fourteenDaysMs = 14 * 864e5;
25580
26039
  const agingItems = allDocs.filter((d) => {
25581
- if (DONE_STATUSES12.has(d.frontmatter.status)) return false;
26040
+ if (DONE_STATUSES13.has(d.frontmatter.status)) return false;
25582
26041
  if (!["action", "question"].includes(d.frontmatter.type)) return false;
25583
26042
  const createdMs = new Date(d.frontmatter.created).getTime();
25584
26043
  return todayMs - createdMs > fourteenDaysMs;
@@ -25692,7 +26151,7 @@ function dmRisksPage(ctx) {
25692
26151
  }
25693
26152
 
25694
26153
  // src/web/templates/pages/dm/meetings.ts
25695
- var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
26154
+ var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25696
26155
  function dmMeetingsPage(ctx) {
25697
26156
  const meetings = ctx.store.list({ type: "meeting" });
25698
26157
  const actions = ctx.store.list({ type: "action" });
@@ -25738,7 +26197,7 @@ function dmMeetingsPage(ctx) {
25738
26197
  ${sortedMeetings.map((m) => {
25739
26198
  const date5 = m.frontmatter.date ?? m.frontmatter.created;
25740
26199
  const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
25741
- const openCount = relatedActions.filter((a) => !DONE_STATUSES13.has(a.frontmatter.status)).length;
26200
+ const openCount = relatedActions.filter((a) => !DONE_STATUSES14.has(a.frontmatter.status)).length;
25742
26201
  return `
25743
26202
  <tr>
25744
26203
  <td>${formatDate(date5)}</td>
@@ -25753,7 +26212,7 @@ function dmMeetingsPage(ctx) {
25753
26212
  const recentMeetingActions = [];
25754
26213
  for (const [mid, acts] of meetingActionMap) {
25755
26214
  for (const act of acts) {
25756
- if (!DONE_STATUSES13.has(act.frontmatter.status)) {
26215
+ if (!DONE_STATUSES14.has(act.frontmatter.status)) {
25757
26216
  recentMeetingActions.push({ action: act, meetingId: mid });
25758
26217
  }
25759
26218
  }
@@ -25948,7 +26407,7 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
25948
26407
  registerPersonaPage("dm", "governance", dmGovernancePage);
25949
26408
 
25950
26409
  // src/web/templates/pages/tl/dashboard.ts
25951
- var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
26410
+ var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
25952
26411
  var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
25953
26412
  function tlDashboardPage(ctx) {
25954
26413
  const epics = ctx.store.list({ type: "epic" });
@@ -25956,8 +26415,8 @@ function tlDashboardPage(ctx) {
25956
26415
  const decisions = ctx.store.list({ type: "decision" });
25957
26416
  const questions = ctx.store.list({ type: "question" });
25958
26417
  const diagrams = getDiagramData(ctx.store);
25959
- const openEpics = epics.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
25960
- const openTasks = tasks.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
26418
+ const openEpics = epics.filter((d) => !DONE_STATUSES15.has(d.frontmatter.status));
26419
+ const openTasks = tasks.filter((d) => !DONE_STATUSES15.has(d.frontmatter.status));
25961
26420
  const technicalDecisions = decisions.filter((d) => {
25962
26421
  const tags = d.frontmatter.tags ?? [];
25963
26422
  return tags.some((t) => {
@@ -26015,7 +26474,7 @@ function tlDashboardPage(ctx) {
26015
26474
  }
26016
26475
 
26017
26476
  // src/web/templates/pages/tl/backlog.ts
26018
- var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
26477
+ var DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
26019
26478
  function tlBacklogPage(ctx) {
26020
26479
  const epics = ctx.store.list({ type: "epic" });
26021
26480
  const tasks = ctx.store.list({ type: "task" });
@@ -26052,7 +26511,7 @@ function tlBacklogPage(ctx) {
26052
26511
  <tbody>
26053
26512
  ${sortedEpics.map((e) => {
26054
26513
  const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
26055
- const done = eTasks.filter((t) => DONE_STATUSES15.has(t.frontmatter.status)).length;
26514
+ const done = eTasks.filter((t) => DONE_STATUSES16.has(t.frontmatter.status)).length;
26056
26515
  const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
26057
26516
  const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
26058
26517
  return `
@@ -26072,7 +26531,7 @@ function tlBacklogPage(ctx) {
26072
26531
  for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
26073
26532
  }
26074
26533
  const unassignedTasks = tasks.filter(
26075
- (t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES15.has(t.frontmatter.status)
26534
+ (t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES16.has(t.frontmatter.status)
26076
26535
  );
26077
26536
  const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
26078
26537
  "tl-backlog-unassigned",
@@ -26133,7 +26592,7 @@ var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
26133
26592
  "technical-assessment",
26134
26593
  "architecture-review"
26135
26594
  ]);
26136
- function progressBar4(pct) {
26595
+ function progressBar5(pct) {
26137
26596
  return `<div class="sprint-progress-bar">
26138
26597
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
26139
26598
  <span class="sprint-progress-label">${pct}%</span>
@@ -26237,7 +26696,7 @@ function tlSprintPage(ctx) {
26237
26696
  <div class="subtitle">Technical sprint items and contributions</div>
26238
26697
  </div>
26239
26698
  ${sprintHeader}
26240
- ${progressBar4(data.workItems.completionPct)}
26699
+ ${progressBar5(data.workItems.completionPct)}
26241
26700
  ${statsCards}
26242
26701
  ${workItemsSection}
26243
26702
  ${epicsSection}
@@ -26572,7 +27031,7 @@ function upcomingPage(data) {
26572
27031
  }
26573
27032
 
26574
27033
  // src/web/templates/pages/sprint-summary.ts
26575
- function progressBar5(pct) {
27034
+ function progressBar6(pct) {
26576
27035
  return `<div class="sprint-progress-bar">
26577
27036
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
26578
27037
  <span class="sprint-progress-label">${pct}%</span>
@@ -26694,7 +27153,7 @@ function sprintSummaryPage(data, cached2) {
26694
27153
  <div class="subtitle">Sprint Summary ${dateRange}</div>
26695
27154
  </div>
26696
27155
  ${goalHtml}
26697
- ${progressBar5(data.timeline.percentComplete)}
27156
+ ${progressBar6(data.timeline.percentComplete)}
26698
27157
  ${statsCards}
26699
27158
  ${epicsTable}
26700
27159
  ${workItemsSection}
@@ -28045,7 +28504,7 @@ async function runSkillAction(action, userPrompt, context) {
28045
28504
  try {
28046
28505
  const mcpServer = createMarvinMcpServer(context.store);
28047
28506
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES : [];
28048
- const conversation = query3({
28507
+ const conversation = query4({
28049
28508
  prompt: userPrompt,
28050
28509
  options: {
28051
28510
  systemPrompt: action.systemPrompt,