mrvn-cli 0.5.16 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +29 -8
- package/dist/index.js +1005 -175
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1001 -180
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1005 -175
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14505,11 +14505,25 @@ import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
|
14505
14505
|
|
|
14506
14506
|
// src/storage/progress.ts
|
|
14507
14507
|
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
14508
|
+
var STATUS_PROGRESS_DEFAULTS = {
|
|
14509
|
+
done: 100,
|
|
14510
|
+
closed: 100,
|
|
14511
|
+
resolved: 100,
|
|
14512
|
+
obsolete: 100,
|
|
14513
|
+
"wont do": 100,
|
|
14514
|
+
cancelled: 100,
|
|
14515
|
+
review: 80,
|
|
14516
|
+
"in-progress": 40,
|
|
14517
|
+
ready: 5,
|
|
14518
|
+
blocked: 10,
|
|
14519
|
+
backlog: 0,
|
|
14520
|
+
open: 0
|
|
14521
|
+
};
|
|
14508
14522
|
function getEffectiveProgress(frontmatter) {
|
|
14509
14523
|
if (DONE_STATUSES.has(frontmatter.status)) return 100;
|
|
14510
14524
|
const raw = frontmatter.progress;
|
|
14511
14525
|
if (typeof raw === "number") return Math.max(0, Math.min(100, Math.round(raw)));
|
|
14512
|
-
return 0;
|
|
14526
|
+
return STATUS_PROGRESS_DEFAULTS[frontmatter.status] ?? 0;
|
|
14513
14527
|
}
|
|
14514
14528
|
function propagateProgressFromTask(store, taskId) {
|
|
14515
14529
|
const updated = [];
|
|
@@ -15685,6 +15699,14 @@ function evaluateHealth(projectName, metrics) {
|
|
|
15685
15699
|
|
|
15686
15700
|
// src/reports/sprint-summary/collector.ts
|
|
15687
15701
|
var DONE_STATUSES3 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15702
|
+
var COMPLEXITY_WEIGHTS = {
|
|
15703
|
+
trivial: 1,
|
|
15704
|
+
simple: 2,
|
|
15705
|
+
moderate: 3,
|
|
15706
|
+
complex: 5,
|
|
15707
|
+
"very-complex": 8
|
|
15708
|
+
};
|
|
15709
|
+
var DEFAULT_WEIGHT = 3;
|
|
15688
15710
|
function collectSprintSummaryData(store, sprintId) {
|
|
15689
15711
|
const allDocs = store.list();
|
|
15690
15712
|
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -15770,12 +15792,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15770
15792
|
for (const doc of workItemDocs) {
|
|
15771
15793
|
const about = doc.frontmatter.aboutArtifact;
|
|
15772
15794
|
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
15795
|
+
const complexity = doc.frontmatter.complexity;
|
|
15773
15796
|
const item = {
|
|
15774
15797
|
id: doc.frontmatter.id,
|
|
15775
15798
|
title: doc.frontmatter.title,
|
|
15776
15799
|
type: doc.frontmatter.type,
|
|
15777
15800
|
status: doc.frontmatter.status,
|
|
15778
15801
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15802
|
+
weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
|
|
15779
15803
|
owner: doc.frontmatter.owner,
|
|
15780
15804
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15781
15805
|
aboutArtifact: about,
|
|
@@ -19443,7 +19467,7 @@ function poBacklogPage(ctx) {
|
|
|
19443
19467
|
}
|
|
19444
19468
|
}
|
|
19445
19469
|
}
|
|
19446
|
-
const
|
|
19470
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19447
19471
|
function featureTaskStats(featureId) {
|
|
19448
19472
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
19449
19473
|
let total = 0;
|
|
@@ -19452,7 +19476,7 @@ function poBacklogPage(ctx) {
|
|
|
19452
19476
|
for (const epic of fEpics) {
|
|
19453
19477
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
19454
19478
|
total++;
|
|
19455
|
-
if (
|
|
19479
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
19456
19480
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
19457
19481
|
}
|
|
19458
19482
|
}
|
|
@@ -19698,23 +19722,34 @@ function hashString(s) {
|
|
|
19698
19722
|
}
|
|
19699
19723
|
return Math.abs(h);
|
|
19700
19724
|
}
|
|
19725
|
+
var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
|
|
19726
|
+
var DEFAULT_WEIGHT2 = 3;
|
|
19701
19727
|
function countFocusStats(items) {
|
|
19702
19728
|
let total = 0;
|
|
19703
19729
|
let done = 0;
|
|
19704
19730
|
let inProgress = 0;
|
|
19705
|
-
|
|
19731
|
+
let totalWeight = 0;
|
|
19732
|
+
let weightedSum = 0;
|
|
19733
|
+
function walkStats(list) {
|
|
19706
19734
|
for (const w of list) {
|
|
19707
19735
|
if (w.type !== "contribution") {
|
|
19708
19736
|
total++;
|
|
19709
19737
|
const s = w.status.toLowerCase();
|
|
19710
|
-
if (s
|
|
19738
|
+
if (DONE_STATUS_SET.has(s)) done++;
|
|
19711
19739
|
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
19712
19740
|
}
|
|
19713
|
-
if (w.children)
|
|
19741
|
+
if (w.children) walkStats(w.children);
|
|
19714
19742
|
}
|
|
19715
19743
|
}
|
|
19716
|
-
|
|
19717
|
-
|
|
19744
|
+
walkStats(items);
|
|
19745
|
+
for (const w of items) {
|
|
19746
|
+
if (w.type === "contribution") continue;
|
|
19747
|
+
const weight = w.weight ?? DEFAULT_WEIGHT2;
|
|
19748
|
+
const progress = w.progress ?? (DONE_STATUS_SET.has(w.status.toLowerCase()) ? 100 : 0);
|
|
19749
|
+
totalWeight += weight;
|
|
19750
|
+
weightedSum += weight * progress;
|
|
19751
|
+
}
|
|
19752
|
+
return { total, done, inProgress, weightedProgress: totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 };
|
|
19718
19753
|
}
|
|
19719
19754
|
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
19720
19755
|
function ownerBadge2(owner) {
|
|
@@ -19763,7 +19798,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
19763
19798
|
for (const [focus, groupItems] of focusGroups) {
|
|
19764
19799
|
const color = focusColorMap.get(focus);
|
|
19765
19800
|
const stats = countFocusStats(groupItems);
|
|
19766
|
-
const pct = stats.
|
|
19801
|
+
const pct = stats.weightedProgress;
|
|
19767
19802
|
const summaryParts = [];
|
|
19768
19803
|
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
19769
19804
|
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
@@ -25092,35 +25127,62 @@ var DEFAULT_TASK_STATUS_MAP = {
|
|
|
25092
25127
|
blocked: ["Blocked"],
|
|
25093
25128
|
backlog: ["To Do", "Open", "Backlog", "New"]
|
|
25094
25129
|
};
|
|
25095
|
-
function
|
|
25096
|
-
|
|
25130
|
+
function isLegacyFormat(statusMap) {
|
|
25131
|
+
if (!statusMap || typeof statusMap !== "object") return false;
|
|
25132
|
+
const keys = Object.keys(statusMap);
|
|
25133
|
+
if (!keys.every((k) => k === "action" || k === "task")) return false;
|
|
25134
|
+
for (const key of keys) {
|
|
25135
|
+
const val = statusMap[key];
|
|
25136
|
+
if (typeof val !== "object" || val === null) return false;
|
|
25137
|
+
for (const innerVal of Object.values(val)) {
|
|
25138
|
+
if (!Array.isArray(innerVal)) return false;
|
|
25139
|
+
if (!innerVal.every((v) => typeof v === "string")) return false;
|
|
25140
|
+
}
|
|
25141
|
+
}
|
|
25142
|
+
return true;
|
|
25097
25143
|
}
|
|
25098
|
-
function
|
|
25099
|
-
const map2 = configMap ?? defaults;
|
|
25144
|
+
function buildLegacyLookup(legacyMap) {
|
|
25100
25145
|
const lookup = /* @__PURE__ */ new Map();
|
|
25101
|
-
for (const [marvinStatus,
|
|
25102
|
-
const
|
|
25103
|
-
for (const js of statuses) {
|
|
25146
|
+
for (const [marvinStatus, jiraStatuses] of Object.entries(legacyMap)) {
|
|
25147
|
+
for (const js of jiraStatuses) {
|
|
25104
25148
|
lookup.set(js.toLowerCase(), marvinStatus);
|
|
25105
25149
|
}
|
|
25106
25150
|
}
|
|
25107
|
-
|
|
25108
|
-
|
|
25109
|
-
|
|
25110
|
-
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
|
|
25151
|
+
return lookup;
|
|
25152
|
+
}
|
|
25153
|
+
function buildFlatLookup(flatMap, inSprint) {
|
|
25154
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
25155
|
+
for (const [jiraStatus, value] of Object.entries(flatMap)) {
|
|
25156
|
+
if (typeof value === "string") {
|
|
25157
|
+
lookup.set(jiraStatus.toLowerCase(), value);
|
|
25158
|
+
} else {
|
|
25159
|
+
const resolved = inSprint && value.inSprint ? value.inSprint : value.default;
|
|
25160
|
+
lookup.set(jiraStatus.toLowerCase(), resolved);
|
|
25114
25161
|
}
|
|
25115
25162
|
}
|
|
25116
25163
|
return lookup;
|
|
25117
25164
|
}
|
|
25118
|
-
function
|
|
25119
|
-
|
|
25165
|
+
function normalizeStatusMap(statusMap) {
|
|
25166
|
+
if (!statusMap) return {};
|
|
25167
|
+
if (isLegacyFormat(statusMap)) {
|
|
25168
|
+
return { legacy: statusMap };
|
|
25169
|
+
}
|
|
25170
|
+
return { flat: statusMap };
|
|
25171
|
+
}
|
|
25172
|
+
function mapJiraStatusForAction(status, resolved, inSprint = false) {
|
|
25173
|
+
if (resolved.flat) {
|
|
25174
|
+
const lookup2 = buildFlatLookup(resolved.flat, inSprint);
|
|
25175
|
+
return lookup2.get(status.toLowerCase()) ?? "open";
|
|
25176
|
+
}
|
|
25177
|
+
const lookup = buildLegacyLookup(resolved.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP);
|
|
25120
25178
|
return lookup.get(status.toLowerCase()) ?? "open";
|
|
25121
25179
|
}
|
|
25122
|
-
function mapJiraStatusForTask(status,
|
|
25123
|
-
|
|
25180
|
+
function mapJiraStatusForTask(status, resolved, inSprint = false) {
|
|
25181
|
+
if (resolved.flat) {
|
|
25182
|
+
const lookup2 = buildFlatLookup(resolved.flat, inSprint);
|
|
25183
|
+
return lookup2.get(status.toLowerCase()) ?? "backlog";
|
|
25184
|
+
}
|
|
25185
|
+
const lookup = buildLegacyLookup(resolved.legacy?.task ?? DEFAULT_TASK_STATUS_MAP);
|
|
25124
25186
|
return lookup.get(status.toLowerCase()) ?? "backlog";
|
|
25125
25187
|
}
|
|
25126
25188
|
function isInActiveSprint(store, tags) {
|
|
@@ -25144,6 +25206,47 @@ function extractJiraKeyFromTags(tags) {
|
|
|
25144
25206
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
25145
25207
|
return tag ? tag.slice(5) : void 0;
|
|
25146
25208
|
}
|
|
25209
|
+
function collectLinkedIssues(issue2) {
|
|
25210
|
+
const linkedIssues = [];
|
|
25211
|
+
if (issue2.fields.subtasks) {
|
|
25212
|
+
for (const sub of issue2.fields.subtasks) {
|
|
25213
|
+
linkedIssues.push({
|
|
25214
|
+
key: sub.key,
|
|
25215
|
+
summary: sub.fields.summary,
|
|
25216
|
+
status: sub.fields.status.name,
|
|
25217
|
+
relationship: "subtask",
|
|
25218
|
+
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25219
|
+
});
|
|
25220
|
+
}
|
|
25221
|
+
}
|
|
25222
|
+
if (issue2.fields.issuelinks) {
|
|
25223
|
+
for (const link of issue2.fields.issuelinks) {
|
|
25224
|
+
if (link.outwardIssue) {
|
|
25225
|
+
linkedIssues.push({
|
|
25226
|
+
key: link.outwardIssue.key,
|
|
25227
|
+
summary: link.outwardIssue.fields.summary,
|
|
25228
|
+
status: link.outwardIssue.fields.status.name,
|
|
25229
|
+
relationship: link.type.outward,
|
|
25230
|
+
isDone: DONE_STATUSES14.has(
|
|
25231
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25232
|
+
)
|
|
25233
|
+
});
|
|
25234
|
+
}
|
|
25235
|
+
if (link.inwardIssue) {
|
|
25236
|
+
linkedIssues.push({
|
|
25237
|
+
key: link.inwardIssue.key,
|
|
25238
|
+
summary: link.inwardIssue.fields.summary,
|
|
25239
|
+
status: link.inwardIssue.fields.status.name,
|
|
25240
|
+
relationship: link.type.inward,
|
|
25241
|
+
isDone: DONE_STATUSES14.has(
|
|
25242
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25243
|
+
)
|
|
25244
|
+
});
|
|
25245
|
+
}
|
|
25246
|
+
}
|
|
25247
|
+
}
|
|
25248
|
+
return linkedIssues;
|
|
25249
|
+
}
|
|
25147
25250
|
function computeSubtaskProgress(subtasks) {
|
|
25148
25251
|
if (subtasks.length === 0) return 0;
|
|
25149
25252
|
const done = subtasks.filter(
|
|
@@ -25181,46 +25284,10 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
25181
25284
|
try {
|
|
25182
25285
|
const issue2 = await client.getIssueWithLinks(jiraKey);
|
|
25183
25286
|
const inSprint = isInActiveSprint(store, doc.frontmatter.tags);
|
|
25184
|
-
const
|
|
25287
|
+
const resolved = statusMap ?? {};
|
|
25288
|
+
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
|
|
25185
25289
|
const currentStatus = doc.frontmatter.status;
|
|
25186
|
-
const linkedIssues =
|
|
25187
|
-
if (issue2.fields.subtasks) {
|
|
25188
|
-
for (const sub of issue2.fields.subtasks) {
|
|
25189
|
-
linkedIssues.push({
|
|
25190
|
-
key: sub.key,
|
|
25191
|
-
summary: sub.fields.summary,
|
|
25192
|
-
status: sub.fields.status.name,
|
|
25193
|
-
relationship: "subtask",
|
|
25194
|
-
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25195
|
-
});
|
|
25196
|
-
}
|
|
25197
|
-
}
|
|
25198
|
-
if (issue2.fields.issuelinks) {
|
|
25199
|
-
for (const link of issue2.fields.issuelinks) {
|
|
25200
|
-
if (link.outwardIssue) {
|
|
25201
|
-
linkedIssues.push({
|
|
25202
|
-
key: link.outwardIssue.key,
|
|
25203
|
-
summary: link.outwardIssue.fields.summary,
|
|
25204
|
-
status: link.outwardIssue.fields.status.name,
|
|
25205
|
-
relationship: link.type.outward,
|
|
25206
|
-
isDone: DONE_STATUSES14.has(
|
|
25207
|
-
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25208
|
-
)
|
|
25209
|
-
});
|
|
25210
|
-
}
|
|
25211
|
-
if (link.inwardIssue) {
|
|
25212
|
-
linkedIssues.push({
|
|
25213
|
-
key: link.inwardIssue.key,
|
|
25214
|
-
summary: link.inwardIssue.fields.summary,
|
|
25215
|
-
status: link.inwardIssue.fields.status.name,
|
|
25216
|
-
relationship: link.type.inward,
|
|
25217
|
-
isDone: DONE_STATUSES14.has(
|
|
25218
|
-
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25219
|
-
)
|
|
25220
|
-
});
|
|
25221
|
-
}
|
|
25222
|
-
}
|
|
25223
|
-
}
|
|
25290
|
+
const linkedIssues = collectLinkedIssues(issue2);
|
|
25224
25291
|
const subtasks = issue2.fields.subtasks ?? [];
|
|
25225
25292
|
let proposedProgress;
|
|
25226
25293
|
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
@@ -25482,7 +25549,6 @@ function isWithinRange(timestamp, range) {
|
|
|
25482
25549
|
function isConfluenceUrl(url2) {
|
|
25483
25550
|
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
25484
25551
|
}
|
|
25485
|
-
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
25486
25552
|
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
25487
25553
|
const summary = {
|
|
25488
25554
|
dateRange,
|
|
@@ -25593,42 +25659,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
25593
25659
|
});
|
|
25594
25660
|
}
|
|
25595
25661
|
}
|
|
25596
|
-
const linkedIssues = [];
|
|
25597
|
-
if (issueWithLinks) {
|
|
25598
|
-
if (issueWithLinks.fields.subtasks) {
|
|
25599
|
-
for (const sub of issueWithLinks.fields.subtasks) {
|
|
25600
|
-
linkedIssues.push({
|
|
25601
|
-
key: sub.key,
|
|
25602
|
-
summary: sub.fields.summary,
|
|
25603
|
-
status: sub.fields.status.name,
|
|
25604
|
-
relationship: "subtask",
|
|
25605
|
-
isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
|
|
25606
|
-
});
|
|
25607
|
-
}
|
|
25608
|
-
}
|
|
25609
|
-
if (issueWithLinks.fields.issuelinks) {
|
|
25610
|
-
for (const link of issueWithLinks.fields.issuelinks) {
|
|
25611
|
-
if (link.outwardIssue) {
|
|
25612
|
-
linkedIssues.push({
|
|
25613
|
-
key: link.outwardIssue.key,
|
|
25614
|
-
summary: link.outwardIssue.fields.summary,
|
|
25615
|
-
status: link.outwardIssue.fields.status.name,
|
|
25616
|
-
relationship: link.type.outward,
|
|
25617
|
-
isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
25618
|
-
});
|
|
25619
|
-
}
|
|
25620
|
-
if (link.inwardIssue) {
|
|
25621
|
-
linkedIssues.push({
|
|
25622
|
-
key: link.inwardIssue.key,
|
|
25623
|
-
summary: link.inwardIssue.fields.summary,
|
|
25624
|
-
status: link.inwardIssue.fields.status.name,
|
|
25625
|
-
relationship: link.type.inward,
|
|
25626
|
-
isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
25627
|
-
});
|
|
25628
|
-
}
|
|
25629
|
-
}
|
|
25630
|
-
}
|
|
25631
|
-
}
|
|
25662
|
+
const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
|
|
25632
25663
|
const marvinArtifacts = [];
|
|
25633
25664
|
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
25634
25665
|
for (const doc of artifacts) {
|
|
@@ -25639,7 +25670,8 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
25639
25670
|
const jiraStatus = issue2.fields.status?.name;
|
|
25640
25671
|
if (jiraStatus) {
|
|
25641
25672
|
const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
|
|
25642
|
-
|
|
25673
|
+
const resolved = statusMap ?? {};
|
|
25674
|
+
proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
25643
25675
|
}
|
|
25644
25676
|
}
|
|
25645
25677
|
marvinArtifacts.push({
|
|
@@ -25757,36 +25789,23 @@ function generateProposedActions(issues) {
|
|
|
25757
25789
|
|
|
25758
25790
|
// src/skills/builtin/jira/sprint-progress.ts
|
|
25759
25791
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
25760
|
-
var
|
|
25792
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
25761
25793
|
var BATCH_SIZE = 5;
|
|
25794
|
+
var MAX_LINKED_ISSUES = 50;
|
|
25762
25795
|
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
25763
|
-
var
|
|
25796
|
+
var COMPLEXITY_WEIGHTS2 = {
|
|
25764
25797
|
trivial: 1,
|
|
25765
25798
|
simple: 2,
|
|
25766
25799
|
moderate: 3,
|
|
25767
25800
|
complex: 5,
|
|
25768
25801
|
"very-complex": 8
|
|
25769
25802
|
};
|
|
25770
|
-
var
|
|
25771
|
-
var STATUS_PROGRESS_DEFAULTS = {
|
|
25772
|
-
done: 100,
|
|
25773
|
-
closed: 100,
|
|
25774
|
-
resolved: 100,
|
|
25775
|
-
obsolete: 100,
|
|
25776
|
-
"wont do": 100,
|
|
25777
|
-
cancelled: 100,
|
|
25778
|
-
review: 80,
|
|
25779
|
-
"in-progress": 40,
|
|
25780
|
-
ready: 5,
|
|
25781
|
-
backlog: 0,
|
|
25782
|
-
open: 0
|
|
25783
|
-
};
|
|
25784
|
-
var BLOCKED_DEFAULT_PROGRESS = 10;
|
|
25803
|
+
var DEFAULT_WEIGHT3 = 3;
|
|
25785
25804
|
function resolveWeight(complexity) {
|
|
25786
|
-
if (complexity && complexity in
|
|
25787
|
-
return { weight:
|
|
25805
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
|
|
25806
|
+
return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
|
|
25788
25807
|
}
|
|
25789
|
-
return { weight:
|
|
25808
|
+
return { weight: DEFAULT_WEIGHT3, weightSource: "default" };
|
|
25790
25809
|
}
|
|
25791
25810
|
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
25792
25811
|
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
@@ -25797,9 +25816,6 @@ function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
|
25797
25816
|
return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
|
|
25798
25817
|
}
|
|
25799
25818
|
const status = frontmatter.status;
|
|
25800
|
-
if (status === "blocked") {
|
|
25801
|
-
return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
|
|
25802
|
-
}
|
|
25803
25819
|
const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
|
|
25804
25820
|
return { progress: defaultProgress, progressSource: "status-default" };
|
|
25805
25821
|
}
|
|
@@ -25878,6 +25894,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25878
25894
|
}
|
|
25879
25895
|
}
|
|
25880
25896
|
}
|
|
25897
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
25898
|
+
if (options.traverseLinks) {
|
|
25899
|
+
const visited = new Set(jiraIssues.keys());
|
|
25900
|
+
const queue = [];
|
|
25901
|
+
for (const [, data] of jiraIssues) {
|
|
25902
|
+
const links = collectLinkedIssues(data.issue);
|
|
25903
|
+
for (const link of links) {
|
|
25904
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
25905
|
+
visited.add(link.key);
|
|
25906
|
+
queue.push(link.key);
|
|
25907
|
+
}
|
|
25908
|
+
}
|
|
25909
|
+
}
|
|
25910
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
25911
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
25912
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
25913
|
+
const results = await Promise.allSettled(
|
|
25914
|
+
batch.map(async (key) => {
|
|
25915
|
+
const [issue2, comments] = await Promise.all([
|
|
25916
|
+
client.getIssueWithLinks(key),
|
|
25917
|
+
client.getComments(key)
|
|
25918
|
+
]);
|
|
25919
|
+
return { key, issue: issue2, comments };
|
|
25920
|
+
})
|
|
25921
|
+
);
|
|
25922
|
+
for (const result of results) {
|
|
25923
|
+
if (result.status === "fulfilled") {
|
|
25924
|
+
const { key, issue: issue2, comments } = result.value;
|
|
25925
|
+
linkedJiraIssues.set(key, { issue: issue2, comments });
|
|
25926
|
+
const newLinks = collectLinkedIssues(issue2);
|
|
25927
|
+
for (const link of newLinks) {
|
|
25928
|
+
if (link.relationship !== "subtask" && !visited.has(link.key)) {
|
|
25929
|
+
visited.add(link.key);
|
|
25930
|
+
queue.push(link.key);
|
|
25931
|
+
}
|
|
25932
|
+
}
|
|
25933
|
+
} else {
|
|
25934
|
+
const batchKey = batch[results.indexOf(result)];
|
|
25935
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
25936
|
+
}
|
|
25937
|
+
}
|
|
25938
|
+
}
|
|
25939
|
+
if (queue.length > 0) {
|
|
25940
|
+
errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
|
|
25941
|
+
}
|
|
25942
|
+
}
|
|
25881
25943
|
const proposedUpdates = [];
|
|
25882
25944
|
const itemReports = [];
|
|
25883
25945
|
const childReportsByParent = /* @__PURE__ */ new Map();
|
|
@@ -25892,7 +25954,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25892
25954
|
if (jiraData) {
|
|
25893
25955
|
jiraStatus = jiraData.issue.fields.status.name;
|
|
25894
25956
|
const inSprint = isInActiveSprint(store, fm.tags);
|
|
25895
|
-
|
|
25957
|
+
const resolved = options.statusMap ?? {};
|
|
25958
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
25896
25959
|
const subtasks = jiraData.issue.fields.subtasks ?? [];
|
|
25897
25960
|
if (subtasks.length > 0) {
|
|
25898
25961
|
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
@@ -25923,11 +25986,42 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25923
25986
|
proposedValue: jiraSubtaskProgress,
|
|
25924
25987
|
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
25925
25988
|
});
|
|
25989
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
25990
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
25991
|
+
if (!hasExplicitProgress) {
|
|
25992
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
25993
|
+
if (proposedProgress !== currentProgress) {
|
|
25994
|
+
proposedUpdates.push({
|
|
25995
|
+
artifactId: fm.id,
|
|
25996
|
+
field: "progress",
|
|
25997
|
+
currentValue: currentProgress,
|
|
25998
|
+
proposedValue: proposedProgress,
|
|
25999
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
26000
|
+
});
|
|
26001
|
+
}
|
|
26002
|
+
}
|
|
25926
26003
|
}
|
|
25927
26004
|
const tags = fm.tags ?? [];
|
|
25928
26005
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
25929
26006
|
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
25930
26007
|
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
26008
|
+
let itemLinkedIssues = [];
|
|
26009
|
+
const itemLinkedIssueSignals = [];
|
|
26010
|
+
if (options.traverseLinks && jiraData) {
|
|
26011
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
26012
|
+
jiraData.issue,
|
|
26013
|
+
jiraIssues,
|
|
26014
|
+
linkedJiraIssues
|
|
26015
|
+
);
|
|
26016
|
+
itemLinkedIssues = allLinks;
|
|
26017
|
+
itemLinkedIssueSignals.push(...allSignals);
|
|
26018
|
+
analyzeLinkedIssueSignals(
|
|
26019
|
+
allLinks,
|
|
26020
|
+
fm,
|
|
26021
|
+
jiraKey,
|
|
26022
|
+
proposedUpdates
|
|
26023
|
+
);
|
|
26024
|
+
}
|
|
25931
26025
|
const report = {
|
|
25932
26026
|
id: fm.id,
|
|
25933
26027
|
title: fm.title,
|
|
@@ -25946,6 +26040,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25946
26040
|
progressDrift,
|
|
25947
26041
|
commentSignals,
|
|
25948
26042
|
commentSummary: null,
|
|
26043
|
+
linkedIssues: itemLinkedIssues,
|
|
26044
|
+
linkedIssueSignals: itemLinkedIssueSignals,
|
|
25949
26045
|
children: [],
|
|
25950
26046
|
owner: fm.owner ?? null,
|
|
25951
26047
|
focusArea: focusTag ? focusTag.slice(6) : null
|
|
@@ -25989,7 +26085,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
25989
26085
|
const focusAreas = [];
|
|
25990
26086
|
for (const [name, items] of focusAreaMap) {
|
|
25991
26087
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
25992
|
-
const doneCount = allFlatItems.filter((i) =>
|
|
26088
|
+
const doneCount = allFlatItems.filter((i) => DONE_STATUSES15.has(i.marvinStatus)).length;
|
|
25993
26089
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
25994
26090
|
const progress = computeWeightedProgress(items);
|
|
25995
26091
|
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
@@ -26041,6 +26137,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
26041
26137
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26042
26138
|
}
|
|
26043
26139
|
}
|
|
26140
|
+
if (options.traverseLinks) {
|
|
26141
|
+
try {
|
|
26142
|
+
const linkedSummaries = await analyzeLinkedIssueComments(
|
|
26143
|
+
itemReports,
|
|
26144
|
+
linkedJiraIssues
|
|
26145
|
+
);
|
|
26146
|
+
for (const [artifactId, signalSummaries] of linkedSummaries) {
|
|
26147
|
+
const report = itemReports.find((r) => r.id === artifactId);
|
|
26148
|
+
if (!report) continue;
|
|
26149
|
+
for (const [sourceKey, summary] of signalSummaries) {
|
|
26150
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === sourceKey);
|
|
26151
|
+
if (signal) {
|
|
26152
|
+
signal.commentSummary = summary;
|
|
26153
|
+
}
|
|
26154
|
+
}
|
|
26155
|
+
}
|
|
26156
|
+
} catch (err) {
|
|
26157
|
+
errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26158
|
+
}
|
|
26159
|
+
}
|
|
26044
26160
|
}
|
|
26045
26161
|
const appliedUpdates = [];
|
|
26046
26162
|
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
@@ -26157,6 +26273,155 @@ ${commentTexts}`);
|
|
|
26157
26273
|
}
|
|
26158
26274
|
return summaries;
|
|
26159
26275
|
}
|
|
26276
|
+
function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
|
|
26277
|
+
const allLinks = [];
|
|
26278
|
+
const allSignals = [];
|
|
26279
|
+
const visited = /* @__PURE__ */ new Set([primaryIssue.key]);
|
|
26280
|
+
const directLinks = collectLinkedIssues(primaryIssue).filter((l) => l.relationship !== "subtask");
|
|
26281
|
+
const queue = [...directLinks];
|
|
26282
|
+
for (const link of directLinks) {
|
|
26283
|
+
visited.add(link.key);
|
|
26284
|
+
}
|
|
26285
|
+
while (queue.length > 0) {
|
|
26286
|
+
const link = queue.shift();
|
|
26287
|
+
allLinks.push(link);
|
|
26288
|
+
const linkedData = linkedJiraIssues.get(link.key) ?? primaryIssues.get(link.key);
|
|
26289
|
+
if (!linkedData) continue;
|
|
26290
|
+
const linkedCommentSignals = [];
|
|
26291
|
+
for (const comment of linkedData.comments) {
|
|
26292
|
+
const text = extractCommentText(comment.body);
|
|
26293
|
+
const signals = detectCommentSignals(text);
|
|
26294
|
+
linkedCommentSignals.push(...signals);
|
|
26295
|
+
}
|
|
26296
|
+
if (linkedCommentSignals.length > 0 || linkedData.comments.length > 0) {
|
|
26297
|
+
allSignals.push({
|
|
26298
|
+
sourceKey: link.key,
|
|
26299
|
+
linkType: link.relationship,
|
|
26300
|
+
commentSignals: linkedCommentSignals,
|
|
26301
|
+
commentSummary: null
|
|
26302
|
+
});
|
|
26303
|
+
}
|
|
26304
|
+
const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
|
|
26305
|
+
for (const next of nextLinks) {
|
|
26306
|
+
visited.add(next.key);
|
|
26307
|
+
queue.push(next);
|
|
26308
|
+
}
|
|
26309
|
+
}
|
|
26310
|
+
return { allLinks, allSignals };
|
|
26311
|
+
}
|
|
26312
|
+
var BLOCKER_LINK_PATTERNS = ["blocks", "is blocked by"];
|
|
26313
|
+
var WONT_DO_STATUSES = /* @__PURE__ */ new Set(["wont do", "won't do", "cancelled"]);
|
|
26314
|
+
function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedUpdates) {
|
|
26315
|
+
if (linkedIssues.length === 0) return;
|
|
26316
|
+
const blockerLinks = linkedIssues.filter(
|
|
26317
|
+
(l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
|
|
26318
|
+
);
|
|
26319
|
+
if (blockerLinks.length > 0 && blockerLinks.every((l) => l.isDone) && frontmatter.status === "blocked") {
|
|
26320
|
+
proposedUpdates.push({
|
|
26321
|
+
artifactId: frontmatter.id,
|
|
26322
|
+
field: "status",
|
|
26323
|
+
currentValue: "blocked",
|
|
26324
|
+
proposedValue: "in-progress",
|
|
26325
|
+
reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
|
|
26326
|
+
});
|
|
26327
|
+
}
|
|
26328
|
+
const wontDoLinks = linkedIssues.filter(
|
|
26329
|
+
(l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
|
|
26330
|
+
);
|
|
26331
|
+
if (wontDoLinks.length > 0) {
|
|
26332
|
+
proposedUpdates.push({
|
|
26333
|
+
artifactId: frontmatter.id,
|
|
26334
|
+
field: "review",
|
|
26335
|
+
currentValue: null,
|
|
26336
|
+
proposedValue: "needs-review",
|
|
26337
|
+
reason: `Linked issue(s) cancelled/won't do: ${wontDoLinks.map((l) => `${l.key} "${l.summary}"`).join(", ")}`
|
|
26338
|
+
});
|
|
26339
|
+
}
|
|
26340
|
+
}
|
|
26341
|
+
var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
|
|
26342
|
+
|
|
26343
|
+
For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
|
|
26344
|
+
|
|
26345
|
+
Return your response as a JSON object mapping artifact IDs to objects mapping linked issue keys to summary strings.
|
|
26346
|
+
Example: {"T-001": {"PROJ-301": "DBA review scheduled for Thursday."}}
|
|
26347
|
+
|
|
26348
|
+
IMPORTANT: Only return the JSON object, no other text.`;
|
|
26349
|
+
async function analyzeLinkedIssueComments(items, linkedJiraIssues) {
|
|
26350
|
+
const results = /* @__PURE__ */ new Map();
|
|
26351
|
+
const promptParts = [];
|
|
26352
|
+
const itemsWithLinkedComments = [];
|
|
26353
|
+
for (const item of items) {
|
|
26354
|
+
if (item.linkedIssueSignals.length === 0) continue;
|
|
26355
|
+
const linkedParts = [];
|
|
26356
|
+
for (const signal of item.linkedIssueSignals) {
|
|
26357
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26358
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
26359
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
26360
|
+
const text = extractCommentText(c.body);
|
|
26361
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
26362
|
+
}).join("\n");
|
|
26363
|
+
linkedParts.push(` ### ${signal.sourceKey} (${signal.linkType})
|
|
26364
|
+
${commentTexts}`);
|
|
26365
|
+
}
|
|
26366
|
+
if (linkedParts.length > 0) {
|
|
26367
|
+
itemsWithLinkedComments.push(item);
|
|
26368
|
+
promptParts.push(`## ${item.id} \u2014 ${item.title}
|
|
26369
|
+
Linked issues:
|
|
26370
|
+
${linkedParts.join("\n")}`);
|
|
26371
|
+
}
|
|
26372
|
+
}
|
|
26373
|
+
if (promptParts.length === 0) return results;
|
|
26374
|
+
const prompt = promptParts.join("\n\n");
|
|
26375
|
+
const llmResult = query3({
|
|
26376
|
+
prompt,
|
|
26377
|
+
options: {
|
|
26378
|
+
systemPrompt: LINKED_COMMENT_ANALYSIS_PROMPT,
|
|
26379
|
+
maxTurns: 1,
|
|
26380
|
+
tools: [],
|
|
26381
|
+
allowedTools: []
|
|
26382
|
+
}
|
|
26383
|
+
});
|
|
26384
|
+
for await (const msg of llmResult) {
|
|
26385
|
+
if (msg.type === "assistant") {
|
|
26386
|
+
const textBlock = msg.message.content.find(
|
|
26387
|
+
(b) => b.type === "text"
|
|
26388
|
+
);
|
|
26389
|
+
if (textBlock) {
|
|
26390
|
+
const parsed = parseLlmJson(textBlock.text);
|
|
26391
|
+
if (parsed) {
|
|
26392
|
+
for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
|
|
26393
|
+
if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
|
|
26394
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
26395
|
+
for (const [key, summary] of Object.entries(linkedSummaries)) {
|
|
26396
|
+
if (typeof summary === "string") {
|
|
26397
|
+
signalMap.set(key, summary);
|
|
26398
|
+
}
|
|
26399
|
+
}
|
|
26400
|
+
if (signalMap.size > 0) {
|
|
26401
|
+
results.set(artifactId, signalMap);
|
|
26402
|
+
}
|
|
26403
|
+
}
|
|
26404
|
+
}
|
|
26405
|
+
}
|
|
26406
|
+
}
|
|
26407
|
+
}
|
|
26408
|
+
}
|
|
26409
|
+
return results;
|
|
26410
|
+
}
|
|
26411
|
+
function parseLlmJson(text) {
|
|
26412
|
+
try {
|
|
26413
|
+
return JSON.parse(text);
|
|
26414
|
+
} catch {
|
|
26415
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
26416
|
+
if (match) {
|
|
26417
|
+
try {
|
|
26418
|
+
return JSON.parse(match[1]);
|
|
26419
|
+
} catch {
|
|
26420
|
+
}
|
|
26421
|
+
}
|
|
26422
|
+
return null;
|
|
26423
|
+
}
|
|
26424
|
+
}
|
|
26160
26425
|
function formatProgressReport(report) {
|
|
26161
26426
|
const parts = [];
|
|
26162
26427
|
parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
|
|
@@ -26240,7 +26505,7 @@ function formatProgressReport(report) {
|
|
|
26240
26505
|
}
|
|
26241
26506
|
function formatItemLine(parts, item, depth) {
|
|
26242
26507
|
const indent = " ".repeat(depth + 1);
|
|
26243
|
-
const statusIcon =
|
|
26508
|
+
const statusIcon = DONE_STATUSES15.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
26244
26509
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
26245
26510
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
26246
26511
|
const progressLabel = ` ${item.progress}%`;
|
|
@@ -26250,6 +26515,19 @@ function formatItemLine(parts, item, depth) {
|
|
|
26250
26515
|
if (item.commentSummary) {
|
|
26251
26516
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
26252
26517
|
}
|
|
26518
|
+
if (item.linkedIssues.length > 0) {
|
|
26519
|
+
parts.push(`${indent} \u{1F517} Linked Issues:`);
|
|
26520
|
+
for (const link of item.linkedIssues) {
|
|
26521
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
26522
|
+
const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
|
|
26523
|
+
const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
|
|
26524
|
+
parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
|
|
26525
|
+
const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
26526
|
+
if (signal?.commentSummary) {
|
|
26527
|
+
parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
|
|
26528
|
+
}
|
|
26529
|
+
}
|
|
26530
|
+
}
|
|
26253
26531
|
for (const child of item.children) {
|
|
26254
26532
|
formatItemLine(parts, child, depth + 1);
|
|
26255
26533
|
}
|
|
@@ -26259,6 +26537,506 @@ function progressBar6(pct) {
|
|
|
26259
26537
|
const empty = 10 - filled;
|
|
26260
26538
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
26261
26539
|
}
|
|
26540
|
+
var MAX_ARTIFACT_NODES = 50;
|
|
26541
|
+
var MAX_LLM_DEPTH = 3;
|
|
26542
|
+
var MAX_LLM_COMMENT_CHARS = 8e3;
|
|
26543
|
+
async function assessArtifact(store, client, host, options) {
|
|
26544
|
+
const visited = /* @__PURE__ */ new Set();
|
|
26545
|
+
return _assessArtifactRecursive(store, client, host, options, visited, 0);
|
|
26546
|
+
}
|
|
26547
|
+
async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
|
|
26548
|
+
const errors = [];
|
|
26549
|
+
if (visited.has(options.artifactId)) {
|
|
26550
|
+
return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
|
|
26551
|
+
}
|
|
26552
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
26553
|
+
return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
|
|
26554
|
+
}
|
|
26555
|
+
visited.add(options.artifactId);
|
|
26556
|
+
const doc = store.get(options.artifactId);
|
|
26557
|
+
if (!doc) {
|
|
26558
|
+
return emptyArtifactReport(options.artifactId, [`Artifact ${options.artifactId} not found`]);
|
|
26559
|
+
}
|
|
26560
|
+
const fm = doc.frontmatter;
|
|
26561
|
+
const jiraKey = fm.jiraKey ?? extractJiraKeyFromTags(fm.tags) ?? null;
|
|
26562
|
+
const tags = fm.tags ?? [];
|
|
26563
|
+
const sprintTag = tags.find((t) => t.startsWith("sprint:"));
|
|
26564
|
+
const sprint = sprintTag ? sprintTag.slice(7) : null;
|
|
26565
|
+
const parent = fm.aboutArtifact ?? null;
|
|
26566
|
+
let jiraStatus = null;
|
|
26567
|
+
let jiraAssignee = null;
|
|
26568
|
+
let proposedMarvinStatus = null;
|
|
26569
|
+
let jiraSubtaskProgress = null;
|
|
26570
|
+
const commentSignals = [];
|
|
26571
|
+
let commentSummary = null;
|
|
26572
|
+
let linkedIssues = [];
|
|
26573
|
+
let linkedIssueSignals = [];
|
|
26574
|
+
const proposedUpdates = [];
|
|
26575
|
+
const jiraIssues = /* @__PURE__ */ new Map();
|
|
26576
|
+
const linkedJiraIssues = /* @__PURE__ */ new Map();
|
|
26577
|
+
if (jiraKey) {
|
|
26578
|
+
try {
|
|
26579
|
+
const [issue2, comments] = await Promise.all([
|
|
26580
|
+
client.getIssueWithLinks(jiraKey),
|
|
26581
|
+
client.getComments(jiraKey)
|
|
26582
|
+
]);
|
|
26583
|
+
jiraIssues.set(jiraKey, { issue: issue2, comments });
|
|
26584
|
+
jiraStatus = issue2.fields.status.name;
|
|
26585
|
+
jiraAssignee = issue2.fields.assignee?.displayName ?? null;
|
|
26586
|
+
const inSprint = isInActiveSprint(store, fm.tags);
|
|
26587
|
+
const resolved = options.statusMap ?? {};
|
|
26588
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
|
|
26589
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
26590
|
+
if (subtasks.length > 0) {
|
|
26591
|
+
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
26592
|
+
}
|
|
26593
|
+
for (const comment of comments) {
|
|
26594
|
+
const text = extractCommentText(comment.body);
|
|
26595
|
+
const signals2 = detectCommentSignals(text);
|
|
26596
|
+
commentSignals.push(...signals2);
|
|
26597
|
+
}
|
|
26598
|
+
const jiraVisited = /* @__PURE__ */ new Set([jiraKey]);
|
|
26599
|
+
const queue = [];
|
|
26600
|
+
const directLinks = collectLinkedIssues(issue2).filter((l) => l.relationship !== "subtask");
|
|
26601
|
+
for (const link of directLinks) {
|
|
26602
|
+
if (!jiraVisited.has(link.key)) {
|
|
26603
|
+
jiraVisited.add(link.key);
|
|
26604
|
+
queue.push(link.key);
|
|
26605
|
+
}
|
|
26606
|
+
}
|
|
26607
|
+
while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
|
|
26608
|
+
const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
|
|
26609
|
+
const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
|
|
26610
|
+
const results = await Promise.allSettled(
|
|
26611
|
+
batch.map(async (key) => {
|
|
26612
|
+
const [li, lc] = await Promise.all([
|
|
26613
|
+
client.getIssueWithLinks(key),
|
|
26614
|
+
client.getComments(key)
|
|
26615
|
+
]);
|
|
26616
|
+
return { key, issue: li, comments: lc };
|
|
26617
|
+
})
|
|
26618
|
+
);
|
|
26619
|
+
for (const result of results) {
|
|
26620
|
+
if (result.status === "fulfilled") {
|
|
26621
|
+
const { key, issue: li, comments: lc } = result.value;
|
|
26622
|
+
linkedJiraIssues.set(key, { issue: li, comments: lc });
|
|
26623
|
+
const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
|
|
26624
|
+
for (const nl of newLinks) {
|
|
26625
|
+
jiraVisited.add(nl.key);
|
|
26626
|
+
queue.push(nl.key);
|
|
26627
|
+
}
|
|
26628
|
+
} else {
|
|
26629
|
+
const batchKey = batch[results.indexOf(result)];
|
|
26630
|
+
errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
26631
|
+
}
|
|
26632
|
+
}
|
|
26633
|
+
}
|
|
26634
|
+
const { allLinks, allSignals } = collectTransitiveLinks(
|
|
26635
|
+
issue2,
|
|
26636
|
+
jiraIssues,
|
|
26637
|
+
linkedJiraIssues
|
|
26638
|
+
);
|
|
26639
|
+
linkedIssues = allLinks;
|
|
26640
|
+
linkedIssueSignals = allSignals;
|
|
26641
|
+
analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
|
|
26642
|
+
} catch (err) {
|
|
26643
|
+
errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26644
|
+
}
|
|
26645
|
+
}
|
|
26646
|
+
const currentProgress = getEffectiveProgress(fm);
|
|
26647
|
+
const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
|
|
26648
|
+
const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
|
|
26649
|
+
if (statusDrift && proposedMarvinStatus) {
|
|
26650
|
+
proposedUpdates.push({
|
|
26651
|
+
artifactId: fm.id,
|
|
26652
|
+
field: "status",
|
|
26653
|
+
currentValue: fm.status,
|
|
26654
|
+
proposedValue: proposedMarvinStatus,
|
|
26655
|
+
reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
|
|
26656
|
+
});
|
|
26657
|
+
}
|
|
26658
|
+
if (progressDrift && jiraSubtaskProgress !== null) {
|
|
26659
|
+
proposedUpdates.push({
|
|
26660
|
+
artifactId: fm.id,
|
|
26661
|
+
field: "progress",
|
|
26662
|
+
currentValue: currentProgress,
|
|
26663
|
+
proposedValue: jiraSubtaskProgress,
|
|
26664
|
+
reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
|
|
26665
|
+
});
|
|
26666
|
+
} else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
|
|
26667
|
+
const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
|
|
26668
|
+
if (!hasExplicitProgress) {
|
|
26669
|
+
const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
|
|
26670
|
+
if (proposedProgress !== currentProgress) {
|
|
26671
|
+
proposedUpdates.push({
|
|
26672
|
+
artifactId: fm.id,
|
|
26673
|
+
field: "progress",
|
|
26674
|
+
currentValue: currentProgress,
|
|
26675
|
+
proposedValue: proposedProgress,
|
|
26676
|
+
reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
|
|
26677
|
+
});
|
|
26678
|
+
}
|
|
26679
|
+
}
|
|
26680
|
+
}
|
|
26681
|
+
const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
|
|
26682
|
+
if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
|
|
26683
|
+
const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
|
|
26684
|
+
if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
|
|
26685
|
+
try {
|
|
26686
|
+
const summary = await analyzeSingleArtifactComments(
|
|
26687
|
+
fm.id,
|
|
26688
|
+
fm.title,
|
|
26689
|
+
jiraKey,
|
|
26690
|
+
jiraStatus,
|
|
26691
|
+
jiraIssues,
|
|
26692
|
+
linkedJiraIssues,
|
|
26693
|
+
linkedIssueSignals
|
|
26694
|
+
);
|
|
26695
|
+
commentSummary = summary;
|
|
26696
|
+
} catch (err) {
|
|
26697
|
+
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26698
|
+
}
|
|
26699
|
+
}
|
|
26700
|
+
}
|
|
26701
|
+
const childIds = findChildIds(store, fm);
|
|
26702
|
+
const children = [];
|
|
26703
|
+
for (const childId of childIds) {
|
|
26704
|
+
if (visited.size >= MAX_ARTIFACT_NODES) {
|
|
26705
|
+
errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
|
|
26706
|
+
break;
|
|
26707
|
+
}
|
|
26708
|
+
const childReport = await _assessArtifactRecursive(
|
|
26709
|
+
store,
|
|
26710
|
+
client,
|
|
26711
|
+
host,
|
|
26712
|
+
{ ...options, artifactId: childId },
|
|
26713
|
+
visited,
|
|
26714
|
+
depth + 1
|
|
26715
|
+
);
|
|
26716
|
+
children.push(childReport);
|
|
26717
|
+
}
|
|
26718
|
+
const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
|
|
26719
|
+
const appliedUpdates = [];
|
|
26720
|
+
if (options.applyUpdates && proposedUpdates.length > 0) {
|
|
26721
|
+
for (const update of proposedUpdates) {
|
|
26722
|
+
if (update.field === "review") continue;
|
|
26723
|
+
try {
|
|
26724
|
+
store.update(update.artifactId, {
|
|
26725
|
+
[update.field]: update.proposedValue,
|
|
26726
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26727
|
+
});
|
|
26728
|
+
const updatedDoc = store.get(update.artifactId);
|
|
26729
|
+
if (updatedDoc) {
|
|
26730
|
+
if (updatedDoc.frontmatter.type === "task") {
|
|
26731
|
+
propagateProgressFromTask(store, update.artifactId);
|
|
26732
|
+
} else if (updatedDoc.frontmatter.type === "action") {
|
|
26733
|
+
propagateProgressToAction(store, update.artifactId);
|
|
26734
|
+
}
|
|
26735
|
+
}
|
|
26736
|
+
appliedUpdates.push(update);
|
|
26737
|
+
} catch (err) {
|
|
26738
|
+
errors.push(`Failed to apply update: ${err instanceof Error ? err.message : String(err)}`);
|
|
26739
|
+
}
|
|
26740
|
+
}
|
|
26741
|
+
}
|
|
26742
|
+
return {
|
|
26743
|
+
artifactId: fm.id,
|
|
26744
|
+
title: fm.title,
|
|
26745
|
+
type: fm.type,
|
|
26746
|
+
marvinStatus: fm.status,
|
|
26747
|
+
marvinProgress: currentProgress,
|
|
26748
|
+
sprint,
|
|
26749
|
+
parent,
|
|
26750
|
+
jiraKey,
|
|
26751
|
+
jiraStatus,
|
|
26752
|
+
jiraAssignee,
|
|
26753
|
+
jiraSubtaskProgress,
|
|
26754
|
+
proposedMarvinStatus,
|
|
26755
|
+
statusDrift,
|
|
26756
|
+
progressDrift,
|
|
26757
|
+
commentSignals,
|
|
26758
|
+
commentSummary,
|
|
26759
|
+
linkedIssues,
|
|
26760
|
+
linkedIssueSignals,
|
|
26761
|
+
children,
|
|
26762
|
+
proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
|
|
26763
|
+
appliedUpdates,
|
|
26764
|
+
signals,
|
|
26765
|
+
errors
|
|
26766
|
+
};
|
|
26767
|
+
}
|
|
26768
|
+
function findChildIds(store, fm) {
|
|
26769
|
+
if (fm.type === "action") {
|
|
26770
|
+
return store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === fm.id).map((d) => d.frontmatter.id);
|
|
26771
|
+
}
|
|
26772
|
+
if (fm.type === "epic") {
|
|
26773
|
+
const epicTag = `epic:${fm.id}`;
|
|
26774
|
+
const isLinked = (d) => {
|
|
26775
|
+
const le = d.frontmatter.linkedEpic;
|
|
26776
|
+
if (le?.includes(fm.id)) return true;
|
|
26777
|
+
const t = d.frontmatter.tags ?? [];
|
|
26778
|
+
return t.includes(epicTag);
|
|
26779
|
+
};
|
|
26780
|
+
return [
|
|
26781
|
+
...store.list({ type: "action" }).filter(isLinked),
|
|
26782
|
+
...store.list({ type: "task" }).filter(isLinked)
|
|
26783
|
+
].map((d) => d.frontmatter.id);
|
|
26784
|
+
}
|
|
26785
|
+
return [];
|
|
26786
|
+
}
|
|
26787
|
+
function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus) {
|
|
26788
|
+
const signals = [];
|
|
26789
|
+
const blockerSignals = commentSignals.filter((s) => s.type === "blocker");
|
|
26790
|
+
if (blockerSignals.length > 0) {
|
|
26791
|
+
for (const s of blockerSignals) {
|
|
26792
|
+
signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
|
|
26793
|
+
}
|
|
26794
|
+
}
|
|
26795
|
+
const blockingLinks = linkedIssues.filter(
|
|
26796
|
+
(l) => l.relationship.toLowerCase().includes("block")
|
|
26797
|
+
);
|
|
26798
|
+
const activeBlockers = blockingLinks.filter((l) => !l.isDone);
|
|
26799
|
+
const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
|
|
26800
|
+
if (activeBlockers.length > 0) {
|
|
26801
|
+
for (const b of activeBlockers) {
|
|
26802
|
+
signals.push(`\u{1F6AB} Blocker \u2014 ${b.relationship} ${b.key} "${b.summary}" [${b.status}]`);
|
|
26803
|
+
}
|
|
26804
|
+
}
|
|
26805
|
+
if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
|
|
26806
|
+
signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
|
|
26807
|
+
}
|
|
26808
|
+
const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
|
|
26809
|
+
for (const l of wontDoLinks) {
|
|
26810
|
+
signals.push(`\u{1F504} Superseded \u2014 ${l.key} "${l.summary}" is ${l.status}`);
|
|
26811
|
+
}
|
|
26812
|
+
const questionSignals = commentSignals.filter((s) => s.type === "question");
|
|
26813
|
+
for (const s of questionSignals) {
|
|
26814
|
+
signals.push(`\u23F3 Waiting \u2014 "${s.snippet}"`);
|
|
26815
|
+
}
|
|
26816
|
+
const relatedInProgress = linkedIssues.filter(
|
|
26817
|
+
(l) => l.relationship.toLowerCase().includes("relate") && !l.isDone
|
|
26818
|
+
);
|
|
26819
|
+
if (relatedInProgress.length > 0) {
|
|
26820
|
+
for (const l of relatedInProgress) {
|
|
26821
|
+
signals.push(`\u{1F4CB} Handoff \u2014 related work on ${l.key} "${l.summary}" [${l.status}]`);
|
|
26822
|
+
}
|
|
26823
|
+
}
|
|
26824
|
+
if (signals.length === 0) {
|
|
26825
|
+
if (statusDrift && proposedStatus) {
|
|
26826
|
+
signals.push(`\u26A0 Drift detected \u2014 Marvin and Jira statuses diverge`);
|
|
26827
|
+
} else {
|
|
26828
|
+
signals.push(`\u2705 No active blockers or concerns detected`);
|
|
26829
|
+
}
|
|
26830
|
+
}
|
|
26831
|
+
return signals;
|
|
26832
|
+
}
|
|
26833
|
+
function estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
26834
|
+
let total = 0;
|
|
26835
|
+
for (const [, data] of jiraIssues) {
|
|
26836
|
+
for (const c of data.comments) {
|
|
26837
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
26838
|
+
}
|
|
26839
|
+
}
|
|
26840
|
+
for (const signal of linkedIssueSignals) {
|
|
26841
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26842
|
+
if (!linkedData) continue;
|
|
26843
|
+
for (const c of linkedData.comments) {
|
|
26844
|
+
total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
|
|
26845
|
+
}
|
|
26846
|
+
}
|
|
26847
|
+
return total;
|
|
26848
|
+
}
|
|
26849
|
+
var SINGLE_ARTIFACT_COMMENT_PROMPT = `You are a delivery management assistant analyzing Jira comments for a single work item.
|
|
26850
|
+
|
|
26851
|
+
Produce a 2-3 sentence progress summary covering:
|
|
26852
|
+
- What work has been completed
|
|
26853
|
+
- What is pending or blocked
|
|
26854
|
+
- Any decisions, handoffs, or scheduling mentioned
|
|
26855
|
+
- Relevant context from linked issue comments (if provided)
|
|
26856
|
+
|
|
26857
|
+
Return ONLY the summary text, no JSON or formatting.`;
|
|
26858
|
+
async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraStatus, jiraIssues, linkedJiraIssues, linkedIssueSignals) {
|
|
26859
|
+
const promptParts = [];
|
|
26860
|
+
const primaryData = jiraIssues.get(jiraKey);
|
|
26861
|
+
if (primaryData && primaryData.comments.length > 0) {
|
|
26862
|
+
const commentTexts = primaryData.comments.map((c) => {
|
|
26863
|
+
const text = extractCommentText(c.body);
|
|
26864
|
+
return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
|
|
26865
|
+
}).join("\n");
|
|
26866
|
+
promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
|
|
26867
|
+
Comments:
|
|
26868
|
+
${commentTexts}`);
|
|
26869
|
+
}
|
|
26870
|
+
for (const signal of linkedIssueSignals) {
|
|
26871
|
+
const linkedData = linkedJiraIssues.get(signal.sourceKey);
|
|
26872
|
+
if (!linkedData || linkedData.comments.length === 0) continue;
|
|
26873
|
+
const commentTexts = linkedData.comments.map((c) => {
|
|
26874
|
+
const text = extractCommentText(c.body);
|
|
26875
|
+
return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
|
|
26876
|
+
}).join("\n");
|
|
26877
|
+
promptParts.push(`### Linked: ${signal.sourceKey} (${signal.linkType})
|
|
26878
|
+
${commentTexts}`);
|
|
26879
|
+
}
|
|
26880
|
+
if (promptParts.length === 0) return null;
|
|
26881
|
+
const prompt = promptParts.join("\n\n");
|
|
26882
|
+
const result = query3({
|
|
26883
|
+
prompt,
|
|
26884
|
+
options: {
|
|
26885
|
+
systemPrompt: SINGLE_ARTIFACT_COMMENT_PROMPT,
|
|
26886
|
+
maxTurns: 1,
|
|
26887
|
+
tools: [],
|
|
26888
|
+
allowedTools: []
|
|
26889
|
+
}
|
|
26890
|
+
});
|
|
26891
|
+
for await (const msg of result) {
|
|
26892
|
+
if (msg.type === "assistant") {
|
|
26893
|
+
const textBlock = msg.message.content.find(
|
|
26894
|
+
(b) => b.type === "text"
|
|
26895
|
+
);
|
|
26896
|
+
if (textBlock) {
|
|
26897
|
+
return textBlock.text.trim();
|
|
26898
|
+
}
|
|
26899
|
+
}
|
|
26900
|
+
}
|
|
26901
|
+
return null;
|
|
26902
|
+
}
|
|
26903
|
+
function emptyArtifactReport(artifactId, errors) {
|
|
26904
|
+
return {
|
|
26905
|
+
artifactId,
|
|
26906
|
+
title: "Not found",
|
|
26907
|
+
type: "unknown",
|
|
26908
|
+
marvinStatus: "unknown",
|
|
26909
|
+
marvinProgress: 0,
|
|
26910
|
+
sprint: null,
|
|
26911
|
+
parent: null,
|
|
26912
|
+
jiraKey: null,
|
|
26913
|
+
jiraStatus: null,
|
|
26914
|
+
jiraAssignee: null,
|
|
26915
|
+
jiraSubtaskProgress: null,
|
|
26916
|
+
proposedMarvinStatus: null,
|
|
26917
|
+
statusDrift: false,
|
|
26918
|
+
progressDrift: false,
|
|
26919
|
+
commentSignals: [],
|
|
26920
|
+
commentSummary: null,
|
|
26921
|
+
linkedIssues: [],
|
|
26922
|
+
linkedIssueSignals: [],
|
|
26923
|
+
children: [],
|
|
26924
|
+
proposedUpdates: [],
|
|
26925
|
+
appliedUpdates: [],
|
|
26926
|
+
signals: [],
|
|
26927
|
+
errors
|
|
26928
|
+
};
|
|
26929
|
+
}
|
|
26930
|
+
function formatArtifactReport(report) {
|
|
26931
|
+
const parts = [];
|
|
26932
|
+
parts.push(`# Artifact Assessment \u2014 ${report.artifactId}`);
|
|
26933
|
+
parts.push(report.title);
|
|
26934
|
+
parts.push("");
|
|
26935
|
+
parts.push(`## Marvin State`);
|
|
26936
|
+
const marvinParts = [`Status: ${report.marvinStatus}`, `Progress: ${report.marvinProgress}%`];
|
|
26937
|
+
if (report.sprint) marvinParts.push(`Sprint: ${report.sprint}`);
|
|
26938
|
+
if (report.parent) marvinParts.push(`Parent: ${report.parent}`);
|
|
26939
|
+
parts.push(marvinParts.join(" | "));
|
|
26940
|
+
parts.push("");
|
|
26941
|
+
if (report.jiraKey) {
|
|
26942
|
+
parts.push(`## Jira State (${report.jiraKey})`);
|
|
26943
|
+
const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
|
|
26944
|
+
if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
|
|
26945
|
+
if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
|
|
26946
|
+
parts.push(jiraParts.join(" | "));
|
|
26947
|
+
if (report.statusDrift) {
|
|
26948
|
+
parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
|
|
26949
|
+
}
|
|
26950
|
+
if (report.progressDrift && report.jiraSubtaskProgress !== null) {
|
|
26951
|
+
parts.push(`\u26A0 Progress drift: ${report.marvinProgress}% \u2192 ${report.jiraSubtaskProgress}%`);
|
|
26952
|
+
}
|
|
26953
|
+
parts.push("");
|
|
26954
|
+
}
|
|
26955
|
+
if (report.commentSummary) {
|
|
26956
|
+
parts.push(`## Comments`);
|
|
26957
|
+
parts.push(report.commentSummary);
|
|
26958
|
+
parts.push("");
|
|
26959
|
+
}
|
|
26960
|
+
if (report.children.length > 0) {
|
|
26961
|
+
const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
|
|
26962
|
+
const childWeights = report.children.map((c) => {
|
|
26963
|
+
const { weight } = resolveWeight(void 0);
|
|
26964
|
+
return { weight, progress: c.marvinProgress };
|
|
26965
|
+
});
|
|
26966
|
+
const childProgress = childWeights.length > 0 ? Math.round(childWeights.reduce((s, c) => s + c.weight * c.progress, 0) / childWeights.reduce((s, c) => s + c.weight, 0)) : 0;
|
|
26967
|
+
const bar = progressBar6(childProgress);
|
|
26968
|
+
parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
|
|
26969
|
+
for (const child of report.children) {
|
|
26970
|
+
formatArtifactChild(parts, child, 1);
|
|
26971
|
+
}
|
|
26972
|
+
parts.push("");
|
|
26973
|
+
}
|
|
26974
|
+
if (report.linkedIssues.length > 0) {
|
|
26975
|
+
parts.push(`## Linked Issues (${report.linkedIssues.length})`);
|
|
26976
|
+
for (const link of report.linkedIssues) {
|
|
26977
|
+
const doneMarker = link.isDone ? " \u2713" : "";
|
|
26978
|
+
parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
|
|
26979
|
+
const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
|
|
26980
|
+
if (signal?.commentSummary) {
|
|
26981
|
+
parts.push(` \u{1F4AC} ${signal.commentSummary}`);
|
|
26982
|
+
}
|
|
26983
|
+
}
|
|
26984
|
+
parts.push("");
|
|
26985
|
+
}
|
|
26986
|
+
if (report.signals.length > 0) {
|
|
26987
|
+
parts.push(`## Signals`);
|
|
26988
|
+
for (const s of report.signals) {
|
|
26989
|
+
parts.push(` ${s}`);
|
|
26990
|
+
}
|
|
26991
|
+
parts.push("");
|
|
26992
|
+
}
|
|
26993
|
+
if (report.proposedUpdates.length > 0) {
|
|
26994
|
+
parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
|
|
26995
|
+
for (const update of report.proposedUpdates) {
|
|
26996
|
+
parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
26997
|
+
parts.push(` Reason: ${update.reason}`);
|
|
26998
|
+
}
|
|
26999
|
+
parts.push("");
|
|
27000
|
+
parts.push("Run with applyUpdates=true to apply these changes.");
|
|
27001
|
+
parts.push("");
|
|
27002
|
+
}
|
|
27003
|
+
if (report.appliedUpdates.length > 0) {
|
|
27004
|
+
parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
|
|
27005
|
+
for (const update of report.appliedUpdates) {
|
|
27006
|
+
parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
|
|
27007
|
+
}
|
|
27008
|
+
parts.push("");
|
|
27009
|
+
}
|
|
27010
|
+
if (report.errors.length > 0) {
|
|
27011
|
+
parts.push(`## Errors`);
|
|
27012
|
+
for (const err of report.errors) {
|
|
27013
|
+
parts.push(` ${err}`);
|
|
27014
|
+
}
|
|
27015
|
+
parts.push("");
|
|
27016
|
+
}
|
|
27017
|
+
return parts.join("\n");
|
|
27018
|
+
}
|
|
27019
|
+
function formatArtifactChild(parts, child, depth) {
|
|
27020
|
+
const indent = " ".repeat(depth);
|
|
27021
|
+
const icon = DONE_STATUSES15.has(child.marvinStatus) ? "\u2713" : child.marvinStatus === "blocked" ? "\u{1F6AB}" : child.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
27022
|
+
const jiraLabel = child.jiraKey ? ` [${child.jiraKey}: ${child.jiraStatus ?? "?"}]` : "";
|
|
27023
|
+
const driftLabel = child.statusDrift ? ` \u26A0drift \u2192 ${child.proposedMarvinStatus}` : "";
|
|
27024
|
+
const signalHints = [];
|
|
27025
|
+
for (const s of child.signals) {
|
|
27026
|
+
if (s.startsWith("\u2705 No active")) continue;
|
|
27027
|
+
signalHints.push(s);
|
|
27028
|
+
}
|
|
27029
|
+
parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
|
|
27030
|
+
if (child.commentSummary) {
|
|
27031
|
+
parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
|
|
27032
|
+
}
|
|
27033
|
+
for (const hint of signalHints) {
|
|
27034
|
+
parts.push(`${indent} ${hint}`);
|
|
27035
|
+
}
|
|
27036
|
+
for (const grandchild of child.children) {
|
|
27037
|
+
formatArtifactChild(parts, grandchild, depth + 1);
|
|
27038
|
+
}
|
|
27039
|
+
}
|
|
26262
27040
|
|
|
26263
27041
|
// src/skills/builtin/jira/tools.ts
|
|
26264
27042
|
var JIRA_TYPE = "jira-issue";
|
|
@@ -26301,7 +27079,7 @@ function findByJiraKey(store, jiraKey) {
|
|
|
26301
27079
|
function createJiraTools(store, projectConfig) {
|
|
26302
27080
|
const jiraUserConfig = loadUserConfig().jira;
|
|
26303
27081
|
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
26304
|
-
const statusMap = projectConfig?.jira?.statusMap;
|
|
27082
|
+
const statusMap = normalizeStatusMap(projectConfig?.jira?.statusMap);
|
|
26305
27083
|
return [
|
|
26306
27084
|
// --- Local read tools ---
|
|
26307
27085
|
tool20(
|
|
@@ -26928,22 +27706,31 @@ function createJiraTools(store, projectConfig) {
|
|
|
26928
27706
|
const s = issue2.fields.status.name;
|
|
26929
27707
|
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
26930
27708
|
}
|
|
26931
|
-
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
26932
|
-
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
26933
27709
|
const actionLookup = /* @__PURE__ */ new Map();
|
|
26934
|
-
for (const [marvin, value] of Object.entries(actionMap)) {
|
|
26935
|
-
const jiraStatuses = Array.isArray(value) ? value : value.default;
|
|
26936
|
-
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
26937
|
-
if (!Array.isArray(value) && value.inSprint) {
|
|
26938
|
-
for (const js of value.inSprint) actionLookup.set(js.toLowerCase(), `${marvin} (inSprint)`);
|
|
26939
|
-
}
|
|
26940
|
-
}
|
|
26941
27710
|
const taskLookup = /* @__PURE__ */ new Map();
|
|
26942
|
-
|
|
26943
|
-
const
|
|
26944
|
-
|
|
26945
|
-
|
|
26946
|
-
|
|
27711
|
+
if (statusMap.flat) {
|
|
27712
|
+
for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
|
|
27713
|
+
const lower = jiraStatus.toLowerCase();
|
|
27714
|
+
if (typeof value === "string") {
|
|
27715
|
+
actionLookup.set(lower, value);
|
|
27716
|
+
taskLookup.set(lower, value);
|
|
27717
|
+
} else {
|
|
27718
|
+
actionLookup.set(lower, value.default);
|
|
27719
|
+
taskLookup.set(lower, value.default);
|
|
27720
|
+
if (value.inSprint) {
|
|
27721
|
+
actionLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
|
|
27722
|
+
taskLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
|
|
27723
|
+
}
|
|
27724
|
+
}
|
|
27725
|
+
}
|
|
27726
|
+
} else {
|
|
27727
|
+
const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
27728
|
+
const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
27729
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
27730
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
27731
|
+
}
|
|
27732
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
27733
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
26947
27734
|
}
|
|
26948
27735
|
}
|
|
26949
27736
|
const parts = [
|
|
@@ -26965,25 +27752,20 @@ function createJiraTools(store, projectConfig) {
|
|
|
26965
27752
|
if (!taskTarget) unmappedTask.push(status);
|
|
26966
27753
|
}
|
|
26967
27754
|
if (unmappedAction.length > 0 || unmappedTask.length > 0) {
|
|
27755
|
+
const allUnmapped = [.../* @__PURE__ */ new Set([...unmappedAction, ...unmappedTask])];
|
|
26968
27756
|
parts.push("");
|
|
26969
27757
|
parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
|
|
26970
27758
|
parts.push(" jira:");
|
|
26971
27759
|
parts.push(" statusMap:");
|
|
26972
|
-
|
|
26973
|
-
parts.push("
|
|
26974
|
-
parts.push(` # Map these: ${unmappedAction.join(", ")}`);
|
|
26975
|
-
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
26976
|
-
}
|
|
26977
|
-
if (unmappedTask.length > 0) {
|
|
26978
|
-
parts.push(" task:");
|
|
26979
|
-
parts.push(` # Map these: ${unmappedTask.join(", ")}`);
|
|
26980
|
-
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
27760
|
+
for (const s of allUnmapped) {
|
|
27761
|
+
parts.push(` "${s}": <marvin-status>`);
|
|
26981
27762
|
}
|
|
27763
|
+
parts.push(" # Supported marvin statuses: done, in-progress, review, ready, blocked, backlog, open");
|
|
26982
27764
|
} else {
|
|
26983
27765
|
parts.push("");
|
|
26984
27766
|
parts.push("All statuses are mapped.");
|
|
26985
27767
|
}
|
|
26986
|
-
const usingConfig = statusMap
|
|
27768
|
+
const usingConfig = statusMap.flat || statusMap.legacy;
|
|
26987
27769
|
parts.push("");
|
|
26988
27770
|
parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
|
|
26989
27771
|
return {
|
|
@@ -27041,7 +27823,8 @@ function createJiraTools(store, projectConfig) {
|
|
|
27041
27823
|
{
|
|
27042
27824
|
sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
|
|
27043
27825
|
analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
|
|
27044
|
-
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
|
|
27826
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)"),
|
|
27827
|
+
traverseLinks: external_exports.boolean().optional().describe("Traverse Jira issue links (1 hop) to surface context from connected issues \u2014 blockers, related work, cancelled items (default false)")
|
|
27045
27828
|
},
|
|
27046
27829
|
async (args) => {
|
|
27047
27830
|
const jira = createJiraClient(jiraUserConfig);
|
|
@@ -27054,6 +27837,7 @@ function createJiraTools(store, projectConfig) {
|
|
|
27054
27837
|
sprintId: args.sprintId,
|
|
27055
27838
|
analyzeComments: args.analyzeComments ?? false,
|
|
27056
27839
|
applyUpdates: args.applyUpdates ?? false,
|
|
27840
|
+
traverseLinks: args.traverseLinks ?? false,
|
|
27057
27841
|
statusMap
|
|
27058
27842
|
}
|
|
27059
27843
|
);
|
|
@@ -27063,6 +27847,34 @@ function createJiraTools(store, projectConfig) {
|
|
|
27063
27847
|
};
|
|
27064
27848
|
},
|
|
27065
27849
|
{ annotations: { readOnlyHint: false } }
|
|
27850
|
+
),
|
|
27851
|
+
// --- Single-artifact assessment ---
|
|
27852
|
+
tool20(
|
|
27853
|
+
"assess_artifact",
|
|
27854
|
+
"Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
|
|
27855
|
+
{
|
|
27856
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
|
|
27857
|
+
applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
|
|
27858
|
+
},
|
|
27859
|
+
async (args) => {
|
|
27860
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
27861
|
+
if (!jira) return jiraNotConfiguredError();
|
|
27862
|
+
const report = await assessArtifact(
|
|
27863
|
+
store,
|
|
27864
|
+
jira.client,
|
|
27865
|
+
jira.host,
|
|
27866
|
+
{
|
|
27867
|
+
artifactId: args.artifactId,
|
|
27868
|
+
applyUpdates: args.applyUpdates ?? false,
|
|
27869
|
+
statusMap
|
|
27870
|
+
}
|
|
27871
|
+
);
|
|
27872
|
+
return {
|
|
27873
|
+
content: [{ type: "text", text: formatArtifactReport(report) }],
|
|
27874
|
+
isError: report.errors.length > 0 && report.type === "unknown"
|
|
27875
|
+
};
|
|
27876
|
+
},
|
|
27877
|
+
{ annotations: { readOnlyHint: false } }
|
|
27066
27878
|
)
|
|
27067
27879
|
];
|
|
27068
27880
|
}
|
|
@@ -27164,7 +27976,8 @@ var COMMON_TOOLS = `**Available tools:**
|
|
|
27164
27976
|
- \`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.
|
|
27165
27977
|
- \`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.
|
|
27166
27978
|
- \`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).
|
|
27167
|
-
- \`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.
|
|
27979
|
+
- \`assess_sprint_progress\` \u2014 fetch live Jira statuses for all sprint-scoped items, detect drift, group by focus area with rollup progress, and extract comment signals. Use \`analyzeComments=true\` for LLM summaries, \`applyUpdates=true\` to apply changes, \`traverseLinks=true\` to walk Jira issue links recursively.
|
|
27980
|
+
- \`assess_artifact\` \u2014 deep assessment of a single artifact (task, action, or epic). Fetches Jira status, LLM-summarizes comments, recursively traverses linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work). Use \`applyUpdates=true\` to apply proposed changes.
|
|
27168
27981
|
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
27169
27982
|
- \`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.
|
|
27170
27983
|
- \`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).
|
|
@@ -27181,6 +27994,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
|
27181
27994
|
2. Review focus area rollups, status drift, and blockers
|
|
27182
27995
|
3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
|
|
27183
27996
|
|
|
27997
|
+
**Single-artifact deep dive:**
|
|
27998
|
+
1. Call \`assess_artifact\` with an artifact ID to get a focused assessment with Jira sync, comment analysis, link traversal, and child rollup
|
|
27999
|
+
2. Review signals (blockers, unblocks, handoffs) and proposed updates
|
|
28000
|
+
3. Use \`applyUpdates=true\` to apply changes
|
|
28001
|
+
|
|
27184
28002
|
**Daily review workflow:**
|
|
27185
28003
|
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
27186
28004
|
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
@@ -27205,6 +28023,7 @@ ${COMMON_WORKFLOW}
|
|
|
27205
28023
|
**As Product Owner, use Jira integration to:**
|
|
27206
28024
|
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
27207
28025
|
- Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
|
|
28026
|
+
- Use \`assess_artifact\` for deep dives into specific features or epics \u2014 full Jira context, linked issues, and child rollup
|
|
27208
28027
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
27209
28028
|
- Push approved features as Stories for development tracking
|
|
27210
28029
|
- Link decisions to Jira issues for audit trail and traceability
|
|
@@ -27218,6 +28037,7 @@ ${COMMON_WORKFLOW}
|
|
|
27218
28037
|
**As Tech Lead, use Jira integration to:**
|
|
27219
28038
|
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
27220
28039
|
- Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
|
|
28040
|
+
- Use \`assess_artifact\` to investigate a specific task \u2014 see full dependency chain, comment context, and blocker status
|
|
27221
28041
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
27222
28042
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
27223
28043
|
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
@@ -27233,6 +28053,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
|
|
|
27233
28053
|
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
27234
28054
|
- Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
|
|
27235
28055
|
- Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
|
|
28056
|
+
- Use \`assess_artifact\` for focused status checks on critical items \u2014 full Jira context without running the whole sprint assessment
|
|
27236
28057
|
- Pull sprint issues for tracking progress and blockers
|
|
27237
28058
|
- Push actions and tasks to Jira for stakeholder visibility
|
|
27238
28059
|
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
@@ -32372,7 +33193,7 @@ async function jiraSyncCommand(artifactId, options = {}) {
|
|
|
32372
33193
|
);
|
|
32373
33194
|
return;
|
|
32374
33195
|
}
|
|
32375
|
-
const statusMap = project.config.jira?.statusMap;
|
|
33196
|
+
const statusMap = normalizeStatusMap(project.config.jira?.statusMap);
|
|
32376
33197
|
const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
|
|
32377
33198
|
console.log(chalk20.dim(label));
|
|
32378
33199
|
if (options.dryRun) {
|
|
@@ -32489,9 +33310,7 @@ async function jiraStatusesCommand(projectKey) {
|
|
|
32489
33310
|
return;
|
|
32490
33311
|
}
|
|
32491
33312
|
console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
|
|
32492
|
-
const statusMap = project.config.jira?.statusMap;
|
|
32493
|
-
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
32494
|
-
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
33313
|
+
const statusMap = normalizeStatusMap(project.config.jira?.statusMap);
|
|
32495
33314
|
const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
|
|
32496
33315
|
const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
|
|
32497
33316
|
const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
|
|
@@ -32515,14 +33334,28 @@ async function jiraStatusesCommand(projectKey) {
|
|
|
32515
33334
|
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
32516
33335
|
}
|
|
32517
33336
|
const actionLookup = /* @__PURE__ */ new Map();
|
|
32518
|
-
for (const [marvin, value] of Object.entries(actionMap)) {
|
|
32519
|
-
const jiraStatuses = Array.isArray(value) ? value : value.default;
|
|
32520
|
-
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
32521
|
-
}
|
|
32522
33337
|
const taskLookup = /* @__PURE__ */ new Map();
|
|
32523
|
-
|
|
32524
|
-
const
|
|
32525
|
-
|
|
33338
|
+
if (statusMap.flat) {
|
|
33339
|
+
for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
|
|
33340
|
+
const lower = jiraStatus.toLowerCase();
|
|
33341
|
+
if (typeof value === "string") {
|
|
33342
|
+
actionLookup.set(lower, value);
|
|
33343
|
+
taskLookup.set(lower, value);
|
|
33344
|
+
} else {
|
|
33345
|
+
const label = value.inSprint ? `${value.default} / ${value.inSprint} (inSprint)` : value.default;
|
|
33346
|
+
actionLookup.set(lower, label);
|
|
33347
|
+
taskLookup.set(lower, label);
|
|
33348
|
+
}
|
|
33349
|
+
}
|
|
33350
|
+
} else {
|
|
33351
|
+
const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
33352
|
+
const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
33353
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
33354
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
33355
|
+
}
|
|
33356
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
33357
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
33358
|
+
}
|
|
32526
33359
|
}
|
|
32527
33360
|
console.log(
|
|
32528
33361
|
`
|
|
@@ -32545,14 +33378,11 @@ Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.
|
|
|
32545
33378
|
console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
|
|
32546
33379
|
console.log(chalk20.dim(" jira:"));
|
|
32547
33380
|
console.log(chalk20.dim(" statusMap:"));
|
|
32548
|
-
console.log(chalk20.dim("
|
|
32549
|
-
console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
|
|
32550
|
-
console.log(chalk20.dim(" task:"));
|
|
32551
|
-
console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
|
|
33381
|
+
console.log(chalk20.dim(' "<Jira Status>": <marvin-status>'));
|
|
32552
33382
|
} else {
|
|
32553
33383
|
console.log(chalk20.green("\nAll statuses are mapped."));
|
|
32554
33384
|
}
|
|
32555
|
-
const usingConfig = statusMap
|
|
33385
|
+
const usingConfig = statusMap.flat || statusMap.legacy;
|
|
32556
33386
|
console.log(
|
|
32557
33387
|
chalk20.dim(
|
|
32558
33388
|
usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
|
|
@@ -32587,7 +33417,7 @@ async function jiraDailyCommand(options) {
|
|
|
32587
33417
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
32588
33418
|
const fromDate = options.from ?? today;
|
|
32589
33419
|
const toDate = options.to ?? fromDate;
|
|
32590
|
-
const statusMap = proj.config.jira?.statusMap;
|
|
33420
|
+
const statusMap = normalizeStatusMap(proj.config.jira?.statusMap);
|
|
32591
33421
|
const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
|
|
32592
33422
|
console.log(
|
|
32593
33423
|
chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
|
|
@@ -32704,7 +33534,7 @@ function createProgram() {
|
|
|
32704
33534
|
const program = new Command();
|
|
32705
33535
|
program.name("marvin").description(
|
|
32706
33536
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
32707
|
-
).version("0.5.
|
|
33537
|
+
).version("0.5.18");
|
|
32708
33538
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
32709
33539
|
await initCommand();
|
|
32710
33540
|
});
|