mrvn-cli 0.5.13 → 0.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +572 -23
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +596 -47
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +572 -23
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -176,9 +176,9 @@ var DocumentStore = class {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
list(
|
|
179
|
+
list(query5) {
|
|
180
180
|
const results = [];
|
|
181
|
-
const types =
|
|
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 (
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
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
|
|
19546
|
+
const BATCH_SIZE2 = 5;
|
|
19542
19547
|
const issues = searchResult.issues;
|
|
19543
|
-
for (let i = 0; i < issues.length; i +=
|
|
19544
|
-
const batch = issues.slice(i, i +
|
|
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,510 @@ 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
|
+
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
19778
|
+
var COMPLEXITY_WEIGHTS = {
|
|
19779
|
+
trivial: 1,
|
|
19780
|
+
simple: 2,
|
|
19781
|
+
moderate: 3,
|
|
19782
|
+
complex: 5,
|
|
19783
|
+
"very-complex": 8
|
|
19784
|
+
};
|
|
19785
|
+
var DEFAULT_WEIGHT = 3;
|
|
19786
|
+
var STATUS_PROGRESS_DEFAULTS = {
|
|
19787
|
+
done: 100,
|
|
19788
|
+
closed: 100,
|
|
19789
|
+
resolved: 100,
|
|
19790
|
+
obsolete: 100,
|
|
19791
|
+
"wont do": 100,
|
|
19792
|
+
cancelled: 100,
|
|
19793
|
+
review: 80,
|
|
19794
|
+
"in-progress": 40,
|
|
19795
|
+
ready: 5,
|
|
19796
|
+
backlog: 0,
|
|
19797
|
+
open: 0
|
|
19798
|
+
};
|
|
19799
|
+
var BLOCKED_DEFAULT_PROGRESS = 10;
|
|
19800
|
+
function resolveWeight(complexity) {
|
|
19801
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS) {
|
|
19802
|
+
return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
|
|
19803
|
+
}
|
|
19804
|
+
return { weight: DEFAULT_WEIGHT, weightSource: "default" };
|
|
19805
|
+
}
|
|
19806
|
+
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
19807
|
+
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
19808
|
+
if (hasExplicitProgress) {
|
|
19809
|
+
return { progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))), progressSource: "explicit" };
|
|
19810
|
+
}
|
|
19811
|
+
if (commentAnalysisProgress !== null) {
|
|
19812
|
+
return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
|
|
19813
|
+
}
|
|
19814
|
+
const status = frontmatter.status;
|
|
19815
|
+
if (status === "blocked") {
|
|
19816
|
+
return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
|
|
19817
|
+
}
|
|
19818
|
+
const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
|
|
19819
|
+
return { progress: defaultProgress, progressSource: "status-default" };
|
|
19820
|
+
}
|
|
19821
|
+
function computeWeightedProgress(items) {
|
|
19822
|
+
if (items.length === 0) return 0;
|
|
19823
|
+
let totalWeight = 0;
|
|
19824
|
+
let weightedSum = 0;
|
|
19825
|
+
for (const item of items) {
|
|
19826
|
+
totalWeight += item.weight;
|
|
19827
|
+
weightedSum += item.weight * item.progress;
|
|
19828
|
+
}
|
|
19829
|
+
if (totalWeight === 0) return 0;
|
|
19830
|
+
return Math.round(weightedSum / totalWeight);
|
|
19831
|
+
}
|
|
19832
|
+
async function assessSprintProgress(store, client, host, options = {}) {
|
|
19833
|
+
const errors = [];
|
|
19834
|
+
const sprintData = collectSprintSummaryData(store, options.sprintId);
|
|
19835
|
+
if (!sprintData) {
|
|
19836
|
+
return {
|
|
19837
|
+
sprintId: options.sprintId ?? "unknown",
|
|
19838
|
+
sprintTitle: "Sprint not found",
|
|
19839
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19840
|
+
timeline: { startDate: null, endDate: null, daysRemaining: 0, totalDays: 0, percentComplete: 0 },
|
|
19841
|
+
overallProgress: 0,
|
|
19842
|
+
itemReports: [],
|
|
19843
|
+
focusAreas: [],
|
|
19844
|
+
driftItems: [],
|
|
19845
|
+
blockers: [],
|
|
19846
|
+
proposedUpdates: [],
|
|
19847
|
+
appliedUpdates: [],
|
|
19848
|
+
errors: [`Sprint ${options.sprintId ?? "(active)"} not found. Create a sprint artifact first.`]
|
|
19849
|
+
};
|
|
19850
|
+
}
|
|
19851
|
+
const sprintTag = `sprint:${sprintData.sprint.id}`;
|
|
19852
|
+
const actions = store.list({ type: "action", tag: sprintTag });
|
|
19853
|
+
const tasks = store.list({ type: "task", tag: sprintTag });
|
|
19854
|
+
const sprintItemIds = new Set([...actions, ...tasks].map((d) => d.frontmatter.id));
|
|
19855
|
+
const allTasks = store.list({ type: "task" });
|
|
19856
|
+
const allActions = store.list({ type: "action" });
|
|
19857
|
+
const nestedTasks = allTasks.filter(
|
|
19858
|
+
(d) => !sprintItemIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintItemIds.has(d.frontmatter.aboutArtifact)
|
|
19859
|
+
);
|
|
19860
|
+
const nestedActions = allActions.filter(
|
|
19861
|
+
(d) => !sprintItemIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintItemIds.has(d.frontmatter.aboutArtifact)
|
|
19862
|
+
);
|
|
19863
|
+
const allItems = [...actions, ...tasks, ...nestedTasks, ...nestedActions];
|
|
19864
|
+
const itemJiraKeys = /* @__PURE__ */ new Map();
|
|
19865
|
+
for (const doc of allItems) {
|
|
19866
|
+
const jiraKey = doc.frontmatter.jiraKey ?? extractJiraKeyFromTags(doc.frontmatter.tags);
|
|
19867
|
+
if (jiraKey) {
|
|
19868
|
+
itemJiraKeys.set(doc.frontmatter.id, jiraKey);
|
|
19869
|
+
}
|
|
19870
|
+
}
|
|
19871
|
+
const jiraKeys = [...new Set(itemJiraKeys.values())];
|
|
19872
|
+
const jiraIssues = /* @__PURE__ */ new Map();
|
|
19873
|
+
for (let i = 0; i < jiraKeys.length; i += BATCH_SIZE) {
|
|
19874
|
+
const batch = jiraKeys.slice(i, i + BATCH_SIZE);
|
|
19875
|
+
const results = await Promise.allSettled(
|
|
19876
|
+
batch.map(async (key) => {
|
|
19877
|
+
const [issue2, comments] = await Promise.all([
|
|
19878
|
+
client.getIssueWithLinks(key),
|
|
19879
|
+
client.getComments(key)
|
|
19880
|
+
]);
|
|
19881
|
+
return { key, issue: issue2, comments };
|
|
19882
|
+
})
|
|
19883
|
+
);
|
|
19884
|
+
for (const result of results) {
|
|
19885
|
+
if (result.status === "fulfilled") {
|
|
19886
|
+
jiraIssues.set(result.value.key, {
|
|
19887
|
+
issue: result.value.issue,
|
|
19888
|
+
comments: result.value.comments
|
|
19889
|
+
});
|
|
19890
|
+
} else {
|
|
19891
|
+
const batchKey = batch[results.indexOf(result)];
|
|
19892
|
+
errors.push(`Failed to fetch ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
19893
|
+
}
|
|
19894
|
+
}
|
|
19895
|
+
}
|
|
19896
|
+
const proposedUpdates = [];
|
|
19897
|
+
const itemReports = [];
|
|
19898
|
+
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
19899
|
+
for (const doc of allItems) {
|
|
19900
|
+
const fm = doc.frontmatter;
|
|
19901
|
+
const jiraKey = itemJiraKeys.get(fm.id) ?? null;
|
|
19902
|
+
const jiraData = jiraKey ? jiraIssues.get(jiraKey) : null;
|
|
19903
|
+
let jiraStatus = null;
|
|
19904
|
+
let proposedMarvinStatus = null;
|
|
19905
|
+
let jiraSubtaskProgress = null;
|
|
19906
|
+
const commentSignals = [];
|
|
19907
|
+
if (jiraData) {
|
|
19908
|
+
jiraStatus = jiraData.issue.fields.status.name;
|
|
19909
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action);
|
|
19910
|
+
const subtasks = jiraData.issue.fields.subtasks ?? [];
|
|
19911
|
+
if (subtasks.length > 0) {
|
|
19912
|
+
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
19913
|
+
}
|
|
19914
|
+
for (const comment of jiraData.comments) {
|
|
19915
|
+
const text = extractCommentText(comment.body);
|
|
19916
|
+
const signals = detectCommentSignals(text);
|
|
19917
|
+
commentSignals.push(...signals);
|
|
19918
|
+
}
|
|
19919
|
+
}
|
|
19920
|
+
const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
|
|
19921
|
+
const currentProgress = getEffectiveProgress(fm);
|
|
19922
|
+
const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
|
|
19923
|
+
if (statusDrift && proposedMarvinStatus) {
|
|
19924
|
+
proposedUpdates.push({
|
|
19925
|
+
artifactId: fm.id,
|
|
19926
|
+
field: "status",
|
|
19927
|
+
currentValue: fm.status,
|
|
19928
|
+
proposedValue: proposedMarvinStatus,
|
|
19929
|
+
reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
|
|
19930
|
+
});
|
|
19931
|
+
}
|
|
19932
|
+
if (progressDrift && jiraSubtaskProgress !== null) {
|
|
19933
|
+
proposedUpdates.push({
|
|
19934
|
+
artifactId: fm.id,
|
|
19935
|
+
field: "progress",
|
|
19936
|
+
currentValue: currentProgress,
|
|
19937
|
+
proposedValue: jiraSubtaskProgress,
|
|
19938
|
+
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
19939
|
+
});
|
|
19940
|
+
}
|
|
19941
|
+
const tags = fm.tags ?? [];
|
|
19942
|
+
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
19943
|
+
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
19944
|
+
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
19945
|
+
const report = {
|
|
19946
|
+
id: fm.id,
|
|
19947
|
+
title: fm.title,
|
|
19948
|
+
type: fm.type,
|
|
19949
|
+
marvinStatus: fm.status,
|
|
19950
|
+
marvinProgress: currentProgress,
|
|
19951
|
+
progress: resolvedProgress,
|
|
19952
|
+
progressSource,
|
|
19953
|
+
weight,
|
|
19954
|
+
weightSource,
|
|
19955
|
+
jiraKey,
|
|
19956
|
+
jiraStatus,
|
|
19957
|
+
jiraSubtaskProgress,
|
|
19958
|
+
proposedMarvinStatus,
|
|
19959
|
+
statusDrift,
|
|
19960
|
+
progressDrift,
|
|
19961
|
+
commentSignals,
|
|
19962
|
+
commentSummary: null,
|
|
19963
|
+
children: [],
|
|
19964
|
+
owner: fm.owner ?? null,
|
|
19965
|
+
focusArea: focusTag ? focusTag.slice(6) : null
|
|
19966
|
+
};
|
|
19967
|
+
const aboutArtifact = fm.aboutArtifact;
|
|
19968
|
+
if (aboutArtifact && sprintItemIds.has(aboutArtifact)) {
|
|
19969
|
+
if (!childReportsByParent.has(aboutArtifact)) {
|
|
19970
|
+
childReportsByParent.set(aboutArtifact, []);
|
|
19971
|
+
}
|
|
19972
|
+
childReportsByParent.get(aboutArtifact).push(report);
|
|
19973
|
+
}
|
|
19974
|
+
itemReports.push(report);
|
|
19975
|
+
}
|
|
19976
|
+
for (const report of itemReports) {
|
|
19977
|
+
const children = childReportsByParent.get(report.id);
|
|
19978
|
+
if (children) {
|
|
19979
|
+
report.children = children;
|
|
19980
|
+
}
|
|
19981
|
+
}
|
|
19982
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
19983
|
+
for (const children of childReportsByParent.values()) {
|
|
19984
|
+
for (const child of children) childIds.add(child.id);
|
|
19985
|
+
}
|
|
19986
|
+
const rootReports = itemReports.filter((r) => !childIds.has(r.id));
|
|
19987
|
+
for (const report of rootReports) {
|
|
19988
|
+
if (report.children.length > 0) {
|
|
19989
|
+
const doc = store.get(report.id);
|
|
19990
|
+
const hasExplicitOverride = doc?.frontmatter.progressOverride;
|
|
19991
|
+
if (!hasExplicitOverride) {
|
|
19992
|
+
report.progress = computeWeightedProgress(report.children);
|
|
19993
|
+
report.progressSource = "status-default";
|
|
19994
|
+
}
|
|
19995
|
+
}
|
|
19996
|
+
}
|
|
19997
|
+
const focusAreaMap = /* @__PURE__ */ new Map();
|
|
19998
|
+
for (const report of rootReports) {
|
|
19999
|
+
if (!report.focusArea) continue;
|
|
20000
|
+
if (!focusAreaMap.has(report.focusArea)) focusAreaMap.set(report.focusArea, []);
|
|
20001
|
+
focusAreaMap.get(report.focusArea).push(report);
|
|
20002
|
+
}
|
|
20003
|
+
const focusAreas = [];
|
|
20004
|
+
for (const [name, items] of focusAreaMap) {
|
|
20005
|
+
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
20006
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES7.has(i.marvinStatus)).length;
|
|
20007
|
+
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
20008
|
+
const progress = computeWeightedProgress(items);
|
|
20009
|
+
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
20010
|
+
const blockedWeight = items.filter((i) => i.marvinStatus === "blocked").reduce((s, i) => s + i.weight, 0);
|
|
20011
|
+
const blockedWeightPct = totalWeight > 0 ? Math.round(blockedWeight / totalWeight * 100) : 0;
|
|
20012
|
+
const riskWarning = blockedWeightPct > BLOCKED_WEIGHT_RISK_THRESHOLD * 100 ? `${blockedWeightPct}% of scope is blocked` : null;
|
|
20013
|
+
focusAreas.push({
|
|
20014
|
+
name,
|
|
20015
|
+
progress,
|
|
20016
|
+
taskCount: allFlatItems.length,
|
|
20017
|
+
doneCount,
|
|
20018
|
+
blockedCount,
|
|
20019
|
+
blockedWeightPct,
|
|
20020
|
+
riskWarning,
|
|
20021
|
+
items
|
|
20022
|
+
});
|
|
20023
|
+
}
|
|
20024
|
+
focusAreas.sort((a, b) => a.name.localeCompare(b.name));
|
|
20025
|
+
const driftItems = itemReports.filter((r) => r.statusDrift || r.progressDrift);
|
|
20026
|
+
const blockers = itemReports.filter(
|
|
20027
|
+
(r) => r.marvinStatus === "blocked" || r.commentSignals.some((s) => s.type === "blocker")
|
|
20028
|
+
);
|
|
20029
|
+
if (options.analyzeComments) {
|
|
20030
|
+
const itemsWithComments = itemReports.filter((r) => r.commentSignals.length > 0 && r.jiraKey);
|
|
20031
|
+
if (itemsWithComments.length > 0) {
|
|
20032
|
+
try {
|
|
20033
|
+
const summaries = await analyzeCommentsForProgress(
|
|
20034
|
+
itemsWithComments,
|
|
20035
|
+
jiraIssues,
|
|
20036
|
+
itemJiraKeys
|
|
20037
|
+
);
|
|
20038
|
+
for (const [artifactId, summary] of summaries) {
|
|
20039
|
+
const report = itemReports.find((r) => r.id === artifactId);
|
|
20040
|
+
if (report) {
|
|
20041
|
+
report.commentSummary = summary;
|
|
20042
|
+
if (report.progressSource === "status-default") {
|
|
20043
|
+
const pctMatch = summary.match(/(\d{1,3})%/);
|
|
20044
|
+
if (pctMatch) {
|
|
20045
|
+
const pct = parseInt(pctMatch[1], 10);
|
|
20046
|
+
if (pct >= 0 && pct <= 100) {
|
|
20047
|
+
report.progress = pct;
|
|
20048
|
+
report.progressSource = "comment-analysis";
|
|
20049
|
+
}
|
|
20050
|
+
}
|
|
20051
|
+
}
|
|
20052
|
+
}
|
|
20053
|
+
}
|
|
20054
|
+
} catch (err) {
|
|
20055
|
+
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20056
|
+
}
|
|
20057
|
+
}
|
|
20058
|
+
}
|
|
20059
|
+
const appliedUpdates = [];
|
|
20060
|
+
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
20061
|
+
for (const update of proposedUpdates) {
|
|
20062
|
+
try {
|
|
20063
|
+
store.update(update.artifactId, {
|
|
20064
|
+
[update.field]: update.proposedValue,
|
|
20065
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20066
|
+
});
|
|
20067
|
+
const doc = store.get(update.artifactId);
|
|
20068
|
+
if (doc) {
|
|
20069
|
+
if (doc.frontmatter.type === "task") {
|
|
20070
|
+
propagateProgressFromTask(store, update.artifactId);
|
|
20071
|
+
} else if (doc.frontmatter.type === "action") {
|
|
20072
|
+
propagateProgressToAction(store, update.artifactId);
|
|
20073
|
+
}
|
|
20074
|
+
}
|
|
20075
|
+
appliedUpdates.push(update);
|
|
20076
|
+
} catch (err) {
|
|
20077
|
+
errors.push(
|
|
20078
|
+
`Failed to apply update to ${update.artifactId}: ${err instanceof Error ? err.message : String(err)}`
|
|
20079
|
+
);
|
|
20080
|
+
}
|
|
20081
|
+
}
|
|
20082
|
+
}
|
|
20083
|
+
return {
|
|
20084
|
+
sprintId: sprintData.sprint.id,
|
|
20085
|
+
sprintTitle: sprintData.sprint.title,
|
|
20086
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20087
|
+
timeline: {
|
|
20088
|
+
startDate: sprintData.sprint.startDate ?? null,
|
|
20089
|
+
endDate: sprintData.sprint.endDate ?? null,
|
|
20090
|
+
daysRemaining: sprintData.timeline.daysRemaining,
|
|
20091
|
+
totalDays: sprintData.timeline.totalDays,
|
|
20092
|
+
percentComplete: sprintData.timeline.percentComplete
|
|
20093
|
+
},
|
|
20094
|
+
overallProgress: rootReports.length > 0 ? computeWeightedProgress(rootReports) : sprintData.workItems.completionPct,
|
|
20095
|
+
itemReports: rootReports,
|
|
20096
|
+
focusAreas,
|
|
20097
|
+
driftItems,
|
|
20098
|
+
blockers,
|
|
20099
|
+
proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
|
|
20100
|
+
appliedUpdates,
|
|
20101
|
+
errors
|
|
20102
|
+
};
|
|
20103
|
+
}
|
|
20104
|
+
var COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments for progress signals.
|
|
20105
|
+
|
|
20106
|
+
For each item below, read the Jira comments and produce a 1-2 sentence progress summary.
|
|
20107
|
+
Focus on: what work was done, what's pending, any blockers or decisions mentioned.
|
|
20108
|
+
|
|
20109
|
+
Return your response as a JSON object mapping artifact IDs to summary strings.
|
|
20110
|
+
Example: {"T-001": "Backend API completed and deployed. Frontend integration pending review.", "A-003": "Blocked on infrastructure team approval."}
|
|
20111
|
+
|
|
20112
|
+
IMPORTANT: Only return the JSON object, no other text.`;
|
|
20113
|
+
async function analyzeCommentsForProgress(items, jiraIssues, itemJiraKeys) {
|
|
20114
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
20115
|
+
const MAX_ITEMS_PER_CALL = 20;
|
|
20116
|
+
const itemsToAnalyze = items.slice(0, MAX_ITEMS_PER_CALL);
|
|
20117
|
+
const promptParts = [];
|
|
20118
|
+
for (const item of itemsToAnalyze) {
|
|
20119
|
+
const jiraKey = itemJiraKeys.get(item.id);
|
|
20120
|
+
if (!jiraKey) continue;
|
|
20121
|
+
const jiraData = jiraIssues.get(jiraKey);
|
|
20122
|
+
if (!jiraData || jiraData.comments.length === 0) continue;
|
|
20123
|
+
const commentTexts = jiraData.comments.map((c) => {
|
|
20124
|
+
const text = extractCommentText(c.body);
|
|
20125
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
|
|
20126
|
+
}).join("\n");
|
|
20127
|
+
promptParts.push(`## ${item.id} \u2014 ${item.title} (${jiraKey}, Jira status: ${item.jiraStatus})
|
|
20128
|
+
Comments:
|
|
20129
|
+
${commentTexts}`);
|
|
20130
|
+
}
|
|
20131
|
+
if (promptParts.length === 0) return summaries;
|
|
20132
|
+
const prompt = promptParts.join("\n\n");
|
|
20133
|
+
const result = query2({
|
|
20134
|
+
prompt,
|
|
20135
|
+
options: {
|
|
20136
|
+
systemPrompt: COMMENT_ANALYSIS_PROMPT,
|
|
20137
|
+
maxTurns: 1,
|
|
20138
|
+
tools: [],
|
|
20139
|
+
allowedTools: []
|
|
20140
|
+
}
|
|
20141
|
+
});
|
|
20142
|
+
for await (const msg of result) {
|
|
20143
|
+
if (msg.type === "assistant") {
|
|
20144
|
+
const textBlock = msg.message.content.find(
|
|
20145
|
+
(b) => b.type === "text"
|
|
20146
|
+
);
|
|
20147
|
+
if (textBlock) {
|
|
20148
|
+
try {
|
|
20149
|
+
const parsed = JSON.parse(textBlock.text);
|
|
20150
|
+
for (const [id, summary] of Object.entries(parsed)) {
|
|
20151
|
+
if (typeof summary === "string") {
|
|
20152
|
+
summaries.set(id, summary);
|
|
20153
|
+
}
|
|
20154
|
+
}
|
|
20155
|
+
} catch {
|
|
20156
|
+
const match = textBlock.text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
20157
|
+
if (match) {
|
|
20158
|
+
try {
|
|
20159
|
+
const parsed = JSON.parse(match[1]);
|
|
20160
|
+
for (const [id, summary] of Object.entries(parsed)) {
|
|
20161
|
+
if (typeof summary === "string") {
|
|
20162
|
+
summaries.set(id, summary);
|
|
20163
|
+
}
|
|
20164
|
+
}
|
|
20165
|
+
} catch {
|
|
20166
|
+
}
|
|
20167
|
+
}
|
|
20168
|
+
}
|
|
20169
|
+
}
|
|
20170
|
+
}
|
|
20171
|
+
}
|
|
20172
|
+
return summaries;
|
|
20173
|
+
}
|
|
20174
|
+
function formatProgressReport(report) {
|
|
20175
|
+
const parts = [];
|
|
20176
|
+
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
20177
|
+
parts.push(`${report.sprintTitle}`);
|
|
20178
|
+
parts.push(`Generated: ${report.generatedAt.slice(0, 16)}`);
|
|
20179
|
+
parts.push("");
|
|
20180
|
+
if (report.timeline.startDate && report.timeline.endDate) {
|
|
20181
|
+
parts.push(`## Timeline`);
|
|
20182
|
+
parts.push(`${report.timeline.startDate} \u2192 ${report.timeline.endDate}`);
|
|
20183
|
+
parts.push(`Days remaining: ${report.timeline.daysRemaining} / ${report.timeline.totalDays} (${report.timeline.percentComplete}% elapsed)`);
|
|
20184
|
+
parts.push(`Overall progress: ${report.overallProgress}%`);
|
|
20185
|
+
parts.push("");
|
|
20186
|
+
}
|
|
20187
|
+
if (report.focusAreas.length > 0) {
|
|
20188
|
+
parts.push(`## Focus Areas`);
|
|
20189
|
+
parts.push("");
|
|
20190
|
+
for (const area of report.focusAreas) {
|
|
20191
|
+
const bar = progressBar(area.progress);
|
|
20192
|
+
parts.push(`### ${area.name} ${bar} ${area.progress}%`);
|
|
20193
|
+
parts.push(`${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
|
|
20194
|
+
if (area.riskWarning) {
|
|
20195
|
+
parts.push(` \u26A0 ${area.riskWarning}`);
|
|
20196
|
+
}
|
|
20197
|
+
parts.push("");
|
|
20198
|
+
for (const item of area.items) {
|
|
20199
|
+
formatItemLine(parts, item, 0);
|
|
20200
|
+
}
|
|
20201
|
+
parts.push("");
|
|
20202
|
+
}
|
|
20203
|
+
}
|
|
20204
|
+
if (report.driftItems.length > 0) {
|
|
20205
|
+
parts.push(`## Status Drift (${report.driftItems.length} items)`);
|
|
20206
|
+
for (const item of report.driftItems) {
|
|
20207
|
+
const driftParts = [];
|
|
20208
|
+
if (item.statusDrift) {
|
|
20209
|
+
driftParts.push(`status: ${item.marvinStatus} \u2192 ${item.proposedMarvinStatus}`);
|
|
20210
|
+
}
|
|
20211
|
+
if (item.progressDrift && item.jiraSubtaskProgress !== null) {
|
|
20212
|
+
driftParts.push(`progress: ${item.marvinProgress}% \u2192 ${item.jiraSubtaskProgress}%`);
|
|
20213
|
+
}
|
|
20214
|
+
parts.push(` \u26A0 ${item.id} (${item.jiraKey}) \u2014 ${driftParts.join(", ")}`);
|
|
20215
|
+
}
|
|
20216
|
+
parts.push("");
|
|
20217
|
+
}
|
|
20218
|
+
if (report.blockers.length > 0) {
|
|
20219
|
+
parts.push(`## Blockers (${report.blockers.length})`);
|
|
20220
|
+
for (const item of report.blockers) {
|
|
20221
|
+
const blockerSignals = item.commentSignals.filter((s) => s.type === "blocker");
|
|
20222
|
+
parts.push(` \u{1F6AB} ${item.id} \u2014 ${item.title}${item.jiraKey ? ` (${item.jiraKey})` : ""}`);
|
|
20223
|
+
for (const signal of blockerSignals) {
|
|
20224
|
+
parts.push(` "${signal.snippet}"`);
|
|
20225
|
+
}
|
|
20226
|
+
}
|
|
20227
|
+
parts.push("");
|
|
20228
|
+
}
|
|
20229
|
+
if (report.proposedUpdates.length > 0) {
|
|
20230
|
+
parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
|
|
20231
|
+
for (const update of report.proposedUpdates) {
|
|
20232
|
+
parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
20233
|
+
parts.push(` Reason: ${update.reason}`);
|
|
20234
|
+
}
|
|
20235
|
+
parts.push("");
|
|
20236
|
+
parts.push("Run with applyUpdates=true to apply these changes.");
|
|
20237
|
+
parts.push("");
|
|
20238
|
+
}
|
|
20239
|
+
if (report.appliedUpdates.length > 0) {
|
|
20240
|
+
parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
|
|
20241
|
+
for (const update of report.appliedUpdates) {
|
|
20242
|
+
parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
20243
|
+
}
|
|
20244
|
+
parts.push("");
|
|
20245
|
+
}
|
|
20246
|
+
if (report.errors.length > 0) {
|
|
20247
|
+
parts.push(`## Errors`);
|
|
20248
|
+
for (const err of report.errors) {
|
|
20249
|
+
parts.push(` ${err}`);
|
|
20250
|
+
}
|
|
20251
|
+
parts.push("");
|
|
20252
|
+
}
|
|
20253
|
+
return parts.join("\n");
|
|
20254
|
+
}
|
|
20255
|
+
function formatItemLine(parts, item, depth) {
|
|
20256
|
+
const indent = " ".repeat(depth + 1);
|
|
20257
|
+
const statusIcon = DONE_STATUSES7.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
20258
|
+
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
20259
|
+
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
20260
|
+
const progressLabel = ` ${item.progress}%`;
|
|
20261
|
+
const weightLabel = `w${item.weight}`;
|
|
20262
|
+
const sourceLabel = item.progressSource === "explicit" ? "" : item.progressSource === "comment-analysis" ? " (llm)" : " (est)";
|
|
20263
|
+
parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`);
|
|
20264
|
+
if (item.commentSummary) {
|
|
20265
|
+
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
20266
|
+
}
|
|
20267
|
+
for (const child of item.children) {
|
|
20268
|
+
formatItemLine(parts, child, depth + 1);
|
|
20269
|
+
}
|
|
20270
|
+
}
|
|
20271
|
+
function progressBar(pct) {
|
|
20272
|
+
const filled = Math.round(pct / 10);
|
|
20273
|
+
const empty = 10 - filled;
|
|
20274
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
20275
|
+
}
|
|
20276
|
+
|
|
19768
20277
|
// src/skills/builtin/jira/tools.ts
|
|
19769
20278
|
var JIRA_TYPE = "jira-issue";
|
|
19770
20279
|
function jiraNotConfiguredError() {
|
|
@@ -20530,6 +21039,36 @@ function createJiraTools(store, projectConfig) {
|
|
|
20530
21039
|
};
|
|
20531
21040
|
},
|
|
20532
21041
|
{ annotations: { readOnlyHint: true } }
|
|
21042
|
+
),
|
|
21043
|
+
// --- Sprint progress assessment ---
|
|
21044
|
+
tool20(
|
|
21045
|
+
"assess_sprint_progress",
|
|
21046
|
+
"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.",
|
|
21047
|
+
{
|
|
21048
|
+
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
21049
|
+
analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
|
|
21050
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
|
|
21051
|
+
},
|
|
21052
|
+
async (args) => {
|
|
21053
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
21054
|
+
if (!jira) return jiraNotConfiguredError();
|
|
21055
|
+
const report = await assessSprintProgress(
|
|
21056
|
+
store,
|
|
21057
|
+
jira.client,
|
|
21058
|
+
jira.host,
|
|
21059
|
+
{
|
|
21060
|
+
sprintId: args.sprintId,
|
|
21061
|
+
analyzeComments: args.analyzeComments ?? false,
|
|
21062
|
+
applyUpdates: args.applyUpdates ?? false,
|
|
21063
|
+
statusMap
|
|
21064
|
+
}
|
|
21065
|
+
);
|
|
21066
|
+
return {
|
|
21067
|
+
content: [{ type: "text", text: formatProgressReport(report) }],
|
|
21068
|
+
isError: report.errors.length > 0 && report.itemReports.length === 0
|
|
21069
|
+
};
|
|
21070
|
+
},
|
|
21071
|
+
{ annotations: { readOnlyHint: false } }
|
|
20533
21072
|
)
|
|
20534
21073
|
];
|
|
20535
21074
|
}
|
|
@@ -20631,6 +21170,7 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
20631
21170
|
- \`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
21171
|
- \`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
21172
|
- \`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).
|
|
21173
|
+
- \`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
21174
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
20635
21175
|
- \`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
21176
|
- \`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 +21182,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
20642
21182
|
2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
|
|
20643
21183
|
3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
|
|
20644
21184
|
|
|
21185
|
+
**Sprint progress workflow:**
|
|
21186
|
+
1. Call \`assess_sprint_progress\` to get a comprehensive view of all sprint items with live Jira data
|
|
21187
|
+
2. Review focus area rollups, status drift, and blockers
|
|
21188
|
+
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
21189
|
+
|
|
20645
21190
|
**Daily review workflow:**
|
|
20646
21191
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
20647
21192
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -20665,6 +21210,7 @@ ${COMMON_WORKFLOW}
|
|
|
20665
21210
|
|
|
20666
21211
|
**As Product Owner, use Jira integration to:**
|
|
20667
21212
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
21213
|
+
- Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
|
|
20668
21214
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
20669
21215
|
- Push approved features as Stories for development tracking
|
|
20670
21216
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -20677,6 +21223,7 @@ ${COMMON_WORKFLOW}
|
|
|
20677
21223
|
|
|
20678
21224
|
**As Tech Lead, use Jira integration to:**
|
|
20679
21225
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
21226
|
+
- Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
|
|
20680
21227
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
20681
21228
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
20682
21229
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -20690,6 +21237,8 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
20690
21237
|
|
|
20691
21238
|
**As Delivery Manager, use Jira integration to:**
|
|
20692
21239
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
21240
|
+
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
21241
|
+
- Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
|
|
20693
21242
|
- Pull sprint issues for tracking progress and blockers
|
|
20694
21243
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
20695
21244
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -21274,7 +21823,7 @@ ${fragment}`);
|
|
|
21274
21823
|
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
21275
21824
|
|
|
21276
21825
|
// src/skills/action-runner.ts
|
|
21277
|
-
import { query as
|
|
21826
|
+
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
21278
21827
|
|
|
21279
21828
|
// src/agent/mcp-server.ts
|
|
21280
21829
|
import {
|
|
@@ -23425,7 +23974,7 @@ function personaPickerPage() {
|
|
|
23425
23974
|
}
|
|
23426
23975
|
|
|
23427
23976
|
// src/reports/sprint-summary/risk-assessment.ts
|
|
23428
|
-
import { query as
|
|
23977
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
23429
23978
|
var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
|
|
23430
23979
|
|
|
23431
23980
|
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 +24003,7 @@ async function generateRiskAssessment(data, riskId, store) {
|
|
|
23454
24003
|
const risk = data.risks.find((r) => r.id === riskId);
|
|
23455
24004
|
if (!risk) return "Risk not found in sprint data.";
|
|
23456
24005
|
const prompt = buildSingleRiskPrompt(data, risk, store);
|
|
23457
|
-
const result =
|
|
24006
|
+
const result = query3({
|
|
23458
24007
|
prompt,
|
|
23459
24008
|
options: {
|
|
23460
24009
|
systemPrompt: SYSTEM_PROMPT2,
|
|
@@ -24084,7 +24633,7 @@ function buildHealthGauge(categories) {
|
|
|
24084
24633
|
}
|
|
24085
24634
|
|
|
24086
24635
|
// src/web/templates/pages/po/dashboard.ts
|
|
24087
|
-
var
|
|
24636
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24088
24637
|
var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
24089
24638
|
function poDashboardPage(ctx) {
|
|
24090
24639
|
const overview = getOverviewData(ctx.store);
|
|
@@ -24129,7 +24678,7 @@ function poDashboardPage(ctx) {
|
|
|
24129
24678
|
sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
|
|
24130
24679
|
}
|
|
24131
24680
|
}
|
|
24132
|
-
const featuresDone = features.filter((d) =>
|
|
24681
|
+
const featuresDone = features.filter((d) => DONE_STATUSES8.has(d.frontmatter.status)).length;
|
|
24133
24682
|
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
24134
24683
|
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
24135
24684
|
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
|
|
@@ -24198,7 +24747,7 @@ function poDashboardPage(ctx) {
|
|
|
24198
24747
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
24199
24748
|
const atRiskItems = [];
|
|
24200
24749
|
for (const f of features) {
|
|
24201
|
-
if (
|
|
24750
|
+
if (DONE_STATUSES8.has(f.frontmatter.status)) continue;
|
|
24202
24751
|
const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
|
|
24203
24752
|
const reasons = [];
|
|
24204
24753
|
let blocked = 0;
|
|
@@ -24210,7 +24759,7 @@ function poDashboardPage(ctx) {
|
|
|
24210
24759
|
if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
|
|
24211
24760
|
for (const epic of fEpics) {
|
|
24212
24761
|
const td = epic.frontmatter.targetDate;
|
|
24213
|
-
if (td && td < today && !
|
|
24762
|
+
if (td && td < today && !DONE_STATUSES8.has(epic.frontmatter.status)) {
|
|
24214
24763
|
reasons.push(`${epic.frontmatter.id} overdue`);
|
|
24215
24764
|
}
|
|
24216
24765
|
}
|
|
@@ -24488,7 +25037,7 @@ function poBacklogPage(ctx) {
|
|
|
24488
25037
|
}
|
|
24489
25038
|
}
|
|
24490
25039
|
}
|
|
24491
|
-
const
|
|
25040
|
+
const DONE_STATUSES17 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24492
25041
|
function featureTaskStats(featureId) {
|
|
24493
25042
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
24494
25043
|
let total = 0;
|
|
@@ -24497,7 +25046,7 @@ function poBacklogPage(ctx) {
|
|
|
24497
25046
|
for (const epic of fEpics) {
|
|
24498
25047
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
24499
25048
|
total++;
|
|
24500
|
-
if (
|
|
25049
|
+
if (DONE_STATUSES17.has(t.frontmatter.status)) done++;
|
|
24501
25050
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
24502
25051
|
}
|
|
24503
25052
|
}
|
|
@@ -24851,7 +25400,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
24851
25400
|
{ titleTag: "h3", defaultCollapsed }
|
|
24852
25401
|
);
|
|
24853
25402
|
}
|
|
24854
|
-
var
|
|
25403
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
|
|
24855
25404
|
function computeOwnerCompletionPct(items, owner) {
|
|
24856
25405
|
let total = 0;
|
|
24857
25406
|
let progressSum = 0;
|
|
@@ -24859,7 +25408,7 @@ function computeOwnerCompletionPct(items, owner) {
|
|
|
24859
25408
|
for (const w of list) {
|
|
24860
25409
|
if (w.type !== "contribution" && w.owner === owner) {
|
|
24861
25410
|
total++;
|
|
24862
|
-
progressSum += w.progress ?? (
|
|
25411
|
+
progressSum += w.progress ?? (DONE_STATUSES9.has(w.status) ? 100 : 0);
|
|
24863
25412
|
}
|
|
24864
25413
|
if (w.children) walk(w.children);
|
|
24865
25414
|
}
|
|
@@ -24880,7 +25429,7 @@ function filterItemsByOwner(items, owner) {
|
|
|
24880
25429
|
}
|
|
24881
25430
|
|
|
24882
25431
|
// src/web/templates/pages/po/delivery.ts
|
|
24883
|
-
var
|
|
25432
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24884
25433
|
var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
24885
25434
|
var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
24886
25435
|
function priorityClass2(p) {
|
|
@@ -24900,7 +25449,7 @@ var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
24900
25449
|
"priority-change",
|
|
24901
25450
|
"market-insight"
|
|
24902
25451
|
]);
|
|
24903
|
-
function
|
|
25452
|
+
function progressBar2(pct) {
|
|
24904
25453
|
return `<div class="sprint-progress-bar">
|
|
24905
25454
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
24906
25455
|
<span class="sprint-progress-label">${pct}%</span>
|
|
@@ -25021,7 +25570,7 @@ function poDeliveryPage(ctx) {
|
|
|
25021
25570
|
}
|
|
25022
25571
|
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
25023
25572
|
}
|
|
25024
|
-
const nonDoneFeatures = features.filter((f) => !
|
|
25573
|
+
const nonDoneFeatures = features.filter((f) => !DONE_STATUSES10.has(f.frontmatter.status)).sort((a, b) => {
|
|
25025
25574
|
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
25026
25575
|
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
25027
25576
|
if (pa !== pb) return pa - pb;
|
|
@@ -25080,7 +25629,7 @@ function poDeliveryPage(ctx) {
|
|
|
25080
25629
|
<div class="subtitle">Sprint progress and feature delivery tracking</div>
|
|
25081
25630
|
</div>
|
|
25082
25631
|
${sprintHeader}
|
|
25083
|
-
${
|
|
25632
|
+
${progressBar2(data.workItems.completionPct)}
|
|
25084
25633
|
${statsCards}
|
|
25085
25634
|
${workItemsSection}
|
|
25086
25635
|
${epicsSection}
|
|
@@ -25230,8 +25779,8 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
|
25230
25779
|
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
25231
25780
|
|
|
25232
25781
|
// src/web/templates/pages/dm/dashboard.ts
|
|
25233
|
-
var
|
|
25234
|
-
function
|
|
25782
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25783
|
+
function progressBar3(pct) {
|
|
25235
25784
|
return `<div class="sprint-progress-bar">
|
|
25236
25785
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
25237
25786
|
<span class="sprint-progress-label">${pct}%</span>
|
|
@@ -25241,7 +25790,7 @@ function dmDashboardPage(ctx) {
|
|
|
25241
25790
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
25242
25791
|
const upcoming = getUpcomingData(ctx.store);
|
|
25243
25792
|
const actions = ctx.store.list({ type: "action" });
|
|
25244
|
-
const openActions = actions.filter((d) => !
|
|
25793
|
+
const openActions = actions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
|
|
25245
25794
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
25246
25795
|
const statsCards = `
|
|
25247
25796
|
<div class="cards">
|
|
@@ -25279,7 +25828,7 @@ function dmDashboardPage(ctx) {
|
|
|
25279
25828
|
<strong>${escapeHtml(sprintData.sprint.id)} \u2014 ${escapeHtml(sprintData.sprint.title)}</strong>
|
|
25280
25829
|
${sprintData.sprint.goal ? ` | ${escapeHtml(sprintData.sprint.goal)}` : ""}
|
|
25281
25830
|
</div>
|
|
25282
|
-
${
|
|
25831
|
+
${progressBar3(sprintData.workItems.completionPct)}` : "";
|
|
25283
25832
|
const riskItems = [];
|
|
25284
25833
|
if (overdueActions.length > 0) riskItems.push(`${overdueActions.length} overdue action(s)`);
|
|
25285
25834
|
if ((sprintData?.blockers.length ?? 0) > 0) riskItems.push(`${sprintData.blockers.length} blocker(s)`);
|
|
@@ -25329,7 +25878,7 @@ function dmDashboardPage(ctx) {
|
|
|
25329
25878
|
}
|
|
25330
25879
|
|
|
25331
25880
|
// src/web/templates/pages/dm/sprint.ts
|
|
25332
|
-
function
|
|
25881
|
+
function progressBar4(pct) {
|
|
25333
25882
|
return `<div class="sprint-progress-bar">
|
|
25334
25883
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
25335
25884
|
<span class="sprint-progress-label">${pct}%</span>
|
|
@@ -25452,7 +26001,7 @@ function dmSprintPage(ctx) {
|
|
|
25452
26001
|
<div class="subtitle">Sprint Execution ${dateRange}</div>
|
|
25453
26002
|
</div>
|
|
25454
26003
|
${goalHtml}
|
|
25455
|
-
${
|
|
26004
|
+
${progressBar4(data.timeline.percentComplete)}
|
|
25456
26005
|
${statsCards}
|
|
25457
26006
|
${workItemsSection}
|
|
25458
26007
|
${epicsSection}
|
|
@@ -25462,7 +26011,7 @@ function dmSprintPage(ctx) {
|
|
|
25462
26011
|
}
|
|
25463
26012
|
|
|
25464
26013
|
// src/web/templates/pages/dm/actions.ts
|
|
25465
|
-
var
|
|
26014
|
+
var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25466
26015
|
function urgencyBadge(tier) {
|
|
25467
26016
|
const labels = {
|
|
25468
26017
|
overdue: "Overdue",
|
|
@@ -25482,7 +26031,7 @@ function urgencyRowClass(tier) {
|
|
|
25482
26031
|
function dmActionsPage(ctx) {
|
|
25483
26032
|
const upcoming = getUpcomingData(ctx.store);
|
|
25484
26033
|
const allActions = ctx.store.list({ type: "action" });
|
|
25485
|
-
const openActions = allActions.filter((d) => !
|
|
26034
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
|
|
25486
26035
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
25487
26036
|
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
25488
26037
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
@@ -25567,7 +26116,7 @@ function dmActionsPage(ctx) {
|
|
|
25567
26116
|
}
|
|
25568
26117
|
|
|
25569
26118
|
// src/web/templates/pages/dm/risks.ts
|
|
25570
|
-
var
|
|
26119
|
+
var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25571
26120
|
function dmRisksPage(ctx) {
|
|
25572
26121
|
const allDocs = ctx.store.list();
|
|
25573
26122
|
const upcoming = getUpcomingData(ctx.store);
|
|
@@ -25578,7 +26127,7 @@ function dmRisksPage(ctx) {
|
|
|
25578
26127
|
const todayMs = new Date(today).getTime();
|
|
25579
26128
|
const fourteenDaysMs = 14 * 864e5;
|
|
25580
26129
|
const agingItems = allDocs.filter((d) => {
|
|
25581
|
-
if (
|
|
26130
|
+
if (DONE_STATUSES13.has(d.frontmatter.status)) return false;
|
|
25582
26131
|
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
25583
26132
|
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
25584
26133
|
return todayMs - createdMs > fourteenDaysMs;
|
|
@@ -25692,7 +26241,7 @@ function dmRisksPage(ctx) {
|
|
|
25692
26241
|
}
|
|
25693
26242
|
|
|
25694
26243
|
// src/web/templates/pages/dm/meetings.ts
|
|
25695
|
-
var
|
|
26244
|
+
var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25696
26245
|
function dmMeetingsPage(ctx) {
|
|
25697
26246
|
const meetings = ctx.store.list({ type: "meeting" });
|
|
25698
26247
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -25738,7 +26287,7 @@ function dmMeetingsPage(ctx) {
|
|
|
25738
26287
|
${sortedMeetings.map((m) => {
|
|
25739
26288
|
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
25740
26289
|
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
25741
|
-
const openCount = relatedActions.filter((a) => !
|
|
26290
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES14.has(a.frontmatter.status)).length;
|
|
25742
26291
|
return `
|
|
25743
26292
|
<tr>
|
|
25744
26293
|
<td>${formatDate(date5)}</td>
|
|
@@ -25753,7 +26302,7 @@ function dmMeetingsPage(ctx) {
|
|
|
25753
26302
|
const recentMeetingActions = [];
|
|
25754
26303
|
for (const [mid, acts] of meetingActionMap) {
|
|
25755
26304
|
for (const act of acts) {
|
|
25756
|
-
if (!
|
|
26305
|
+
if (!DONE_STATUSES14.has(act.frontmatter.status)) {
|
|
25757
26306
|
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
25758
26307
|
}
|
|
25759
26308
|
}
|
|
@@ -25948,7 +26497,7 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
|
25948
26497
|
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
25949
26498
|
|
|
25950
26499
|
// src/web/templates/pages/tl/dashboard.ts
|
|
25951
|
-
var
|
|
26500
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
25952
26501
|
var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
25953
26502
|
function tlDashboardPage(ctx) {
|
|
25954
26503
|
const epics = ctx.store.list({ type: "epic" });
|
|
@@ -25956,8 +26505,8 @@ function tlDashboardPage(ctx) {
|
|
|
25956
26505
|
const decisions = ctx.store.list({ type: "decision" });
|
|
25957
26506
|
const questions = ctx.store.list({ type: "question" });
|
|
25958
26507
|
const diagrams = getDiagramData(ctx.store);
|
|
25959
|
-
const openEpics = epics.filter((d) => !
|
|
25960
|
-
const openTasks = tasks.filter((d) => !
|
|
26508
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES15.has(d.frontmatter.status));
|
|
26509
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES15.has(d.frontmatter.status));
|
|
25961
26510
|
const technicalDecisions = decisions.filter((d) => {
|
|
25962
26511
|
const tags = d.frontmatter.tags ?? [];
|
|
25963
26512
|
return tags.some((t) => {
|
|
@@ -26015,7 +26564,7 @@ function tlDashboardPage(ctx) {
|
|
|
26015
26564
|
}
|
|
26016
26565
|
|
|
26017
26566
|
// src/web/templates/pages/tl/backlog.ts
|
|
26018
|
-
var
|
|
26567
|
+
var DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
26019
26568
|
function tlBacklogPage(ctx) {
|
|
26020
26569
|
const epics = ctx.store.list({ type: "epic" });
|
|
26021
26570
|
const tasks = ctx.store.list({ type: "task" });
|
|
@@ -26052,7 +26601,7 @@ function tlBacklogPage(ctx) {
|
|
|
26052
26601
|
<tbody>
|
|
26053
26602
|
${sortedEpics.map((e) => {
|
|
26054
26603
|
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
26055
|
-
const done = eTasks.filter((t) =>
|
|
26604
|
+
const done = eTasks.filter((t) => DONE_STATUSES16.has(t.frontmatter.status)).length;
|
|
26056
26605
|
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
26057
26606
|
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
26058
26607
|
return `
|
|
@@ -26072,7 +26621,7 @@ function tlBacklogPage(ctx) {
|
|
|
26072
26621
|
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
26073
26622
|
}
|
|
26074
26623
|
const unassignedTasks = tasks.filter(
|
|
26075
|
-
(t) => !assignedTaskIds.has(t.frontmatter.id) && !
|
|
26624
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES16.has(t.frontmatter.status)
|
|
26076
26625
|
);
|
|
26077
26626
|
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
26078
26627
|
"tl-backlog-unassigned",
|
|
@@ -26133,7 +26682,7 @@ var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
26133
26682
|
"technical-assessment",
|
|
26134
26683
|
"architecture-review"
|
|
26135
26684
|
]);
|
|
26136
|
-
function
|
|
26685
|
+
function progressBar5(pct) {
|
|
26137
26686
|
return `<div class="sprint-progress-bar">
|
|
26138
26687
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
26139
26688
|
<span class="sprint-progress-label">${pct}%</span>
|
|
@@ -26237,7 +26786,7 @@ function tlSprintPage(ctx) {
|
|
|
26237
26786
|
<div class="subtitle">Technical sprint items and contributions</div>
|
|
26238
26787
|
</div>
|
|
26239
26788
|
${sprintHeader}
|
|
26240
|
-
${
|
|
26789
|
+
${progressBar5(data.workItems.completionPct)}
|
|
26241
26790
|
${statsCards}
|
|
26242
26791
|
${workItemsSection}
|
|
26243
26792
|
${epicsSection}
|
|
@@ -26572,7 +27121,7 @@ function upcomingPage(data) {
|
|
|
26572
27121
|
}
|
|
26573
27122
|
|
|
26574
27123
|
// src/web/templates/pages/sprint-summary.ts
|
|
26575
|
-
function
|
|
27124
|
+
function progressBar6(pct) {
|
|
26576
27125
|
return `<div class="sprint-progress-bar">
|
|
26577
27126
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
26578
27127
|
<span class="sprint-progress-label">${pct}%</span>
|
|
@@ -26694,7 +27243,7 @@ function sprintSummaryPage(data, cached2) {
|
|
|
26694
27243
|
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
26695
27244
|
</div>
|
|
26696
27245
|
${goalHtml}
|
|
26697
|
-
${
|
|
27246
|
+
${progressBar6(data.timeline.percentComplete)}
|
|
26698
27247
|
${statsCards}
|
|
26699
27248
|
${epicsTable}
|
|
26700
27249
|
${workItemsSection}
|
|
@@ -28045,7 +28594,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
28045
28594
|
try {
|
|
28046
28595
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
28047
28596
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES : [];
|
|
28048
|
-
const conversation =
|
|
28597
|
+
const conversation = query4({
|
|
28049
28598
|
prompt: userPrompt,
|
|
28050
28599
|
options: {
|
|
28051
28600
|
systemPrompt: action.systemPrompt,
|