mrvn-cli 0.5.7 → 0.5.8
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 +7 -0
- package/dist/index.js +1457 -36
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1086 -58
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1457 -36
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -19023,13 +19023,23 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
|
19023
19023
|
// src/skills/builtin/jira/client.ts
|
|
19024
19024
|
var JiraClient = class {
|
|
19025
19025
|
baseUrl;
|
|
19026
|
+
baseUrlV3;
|
|
19026
19027
|
authHeader;
|
|
19027
19028
|
constructor(config2) {
|
|
19028
|
-
|
|
19029
|
+
const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
19030
|
+
this.baseUrl = `https://${host}/rest/api/2`;
|
|
19031
|
+
this.baseUrlV3 = `https://${host}/rest/api/3`;
|
|
19029
19032
|
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
19030
19033
|
}
|
|
19031
19034
|
async request(path11, method = "GET", body) {
|
|
19032
19035
|
const url2 = `${this.baseUrl}${path11}`;
|
|
19036
|
+
return this.doRequest(url2, method, body);
|
|
19037
|
+
}
|
|
19038
|
+
async requestV3(path11, method = "GET", body) {
|
|
19039
|
+
const url2 = `${this.baseUrlV3}${path11}`;
|
|
19040
|
+
return this.doRequest(url2, method, body);
|
|
19041
|
+
}
|
|
19042
|
+
async doRequest(url2, method, body) {
|
|
19033
19043
|
const headers = {
|
|
19034
19044
|
Authorization: this.authHeader,
|
|
19035
19045
|
"Content-Type": "application/json",
|
|
@@ -19043,7 +19053,7 @@ var JiraClient = class {
|
|
|
19043
19053
|
if (!response.ok) {
|
|
19044
19054
|
const text = await response.text().catch(() => "");
|
|
19045
19055
|
throw new Error(
|
|
19046
|
-
`Jira API error ${response.status} ${method} ${
|
|
19056
|
+
`Jira API error ${response.status} ${method} ${url2}: ${text}`
|
|
19047
19057
|
);
|
|
19048
19058
|
}
|
|
19049
19059
|
if (response.status === 204) return void 0;
|
|
@@ -19056,6 +19066,14 @@ var JiraClient = class {
|
|
|
19056
19066
|
});
|
|
19057
19067
|
return this.request(`/search?${params}`);
|
|
19058
19068
|
}
|
|
19069
|
+
async searchIssuesV3(jql, fields = ["summary", "status", "issuetype", "priority", "assignee", "labels"], maxResults = 50) {
|
|
19070
|
+
const params = new URLSearchParams({
|
|
19071
|
+
jql,
|
|
19072
|
+
maxResults: String(maxResults),
|
|
19073
|
+
fields: fields.join(",")
|
|
19074
|
+
});
|
|
19075
|
+
return this.requestV3(`/search/jql?${params}`);
|
|
19076
|
+
}
|
|
19059
19077
|
async getIssue(key) {
|
|
19060
19078
|
return this.request(`/issue/${encodeURIComponent(key)}`);
|
|
19061
19079
|
}
|
|
@@ -19069,6 +19087,28 @@ var JiraClient = class {
|
|
|
19069
19087
|
{ fields }
|
|
19070
19088
|
);
|
|
19071
19089
|
}
|
|
19090
|
+
async getIssueWithLinks(key) {
|
|
19091
|
+
return this.request(
|
|
19092
|
+
`/issue/${encodeURIComponent(key)}?fields=summary,status,issuetype,priority,assignee,labels,subtasks,issuelinks`
|
|
19093
|
+
);
|
|
19094
|
+
}
|
|
19095
|
+
async getChangelog(key) {
|
|
19096
|
+
const result = await this.request(
|
|
19097
|
+
`/issue/${encodeURIComponent(key)}/changelog?maxResults=100`
|
|
19098
|
+
);
|
|
19099
|
+
return result.values;
|
|
19100
|
+
}
|
|
19101
|
+
async getComments(key) {
|
|
19102
|
+
const result = await this.request(
|
|
19103
|
+
`/issue/${encodeURIComponent(key)}/comment?maxResults=100`
|
|
19104
|
+
);
|
|
19105
|
+
return result.comments;
|
|
19106
|
+
}
|
|
19107
|
+
async getRemoteLinks(key) {
|
|
19108
|
+
return this.request(
|
|
19109
|
+
`/issue/${encodeURIComponent(key)}/remotelink`
|
|
19110
|
+
);
|
|
19111
|
+
}
|
|
19072
19112
|
async addComment(key, body) {
|
|
19073
19113
|
await this.request(
|
|
19074
19114
|
`/issue/${encodeURIComponent(key)}/comment`,
|
|
@@ -19082,7 +19122,611 @@ function createJiraClient(jiraUserConfig) {
|
|
|
19082
19122
|
const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
|
|
19083
19123
|
const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
|
|
19084
19124
|
if (!host || !email3 || !apiToken) return null;
|
|
19085
|
-
|
|
19125
|
+
const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
19126
|
+
return { client: new JiraClient({ host, email: email3, apiToken }), host: normalizedHost };
|
|
19127
|
+
}
|
|
19128
|
+
|
|
19129
|
+
// src/skills/builtin/jira/sync.ts
|
|
19130
|
+
var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
19131
|
+
var DEFAULT_ACTION_STATUS_MAP = {
|
|
19132
|
+
done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
|
|
19133
|
+
"in-progress": ["In Progress", "In Review", "Reviewing", "Testing"],
|
|
19134
|
+
blocked: ["Blocked"],
|
|
19135
|
+
open: ["To Do", "Open", "Backlog", "New"]
|
|
19136
|
+
};
|
|
19137
|
+
var DEFAULT_TASK_STATUS_MAP = {
|
|
19138
|
+
done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
|
|
19139
|
+
review: ["In Review", "Code Review", "Reviewing", "Testing"],
|
|
19140
|
+
"in-progress": ["In Progress"],
|
|
19141
|
+
ready: ["Ready", "Selected for Development"],
|
|
19142
|
+
blocked: ["Blocked"],
|
|
19143
|
+
backlog: ["To Do", "Open", "Backlog", "New"]
|
|
19144
|
+
};
|
|
19145
|
+
function buildStatusLookup(configMap, defaults) {
|
|
19146
|
+
const map2 = configMap ?? defaults;
|
|
19147
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
19148
|
+
for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
|
|
19149
|
+
for (const js of jiraStatuses) {
|
|
19150
|
+
lookup.set(js.toLowerCase(), marvinStatus);
|
|
19151
|
+
}
|
|
19152
|
+
}
|
|
19153
|
+
return lookup;
|
|
19154
|
+
}
|
|
19155
|
+
function mapJiraStatusForAction(status, configMap) {
|
|
19156
|
+
const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
|
|
19157
|
+
return lookup.get(status.toLowerCase()) ?? "open";
|
|
19158
|
+
}
|
|
19159
|
+
function mapJiraStatusForTask(status, configMap) {
|
|
19160
|
+
const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
|
|
19161
|
+
return lookup.get(status.toLowerCase()) ?? "backlog";
|
|
19162
|
+
}
|
|
19163
|
+
function computeSubtaskProgress(subtasks) {
|
|
19164
|
+
if (subtasks.length === 0) return 0;
|
|
19165
|
+
const done = subtasks.filter(
|
|
19166
|
+
(s) => DONE_STATUSES5.has(s.fields.status.name.toLowerCase())
|
|
19167
|
+
).length;
|
|
19168
|
+
return Math.round(done / subtasks.length * 100);
|
|
19169
|
+
}
|
|
19170
|
+
async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
19171
|
+
const result = { artifacts: [], errors: [] };
|
|
19172
|
+
const actions = store.list({ type: "action" });
|
|
19173
|
+
const tasks = store.list({ type: "task" });
|
|
19174
|
+
let candidates = [...actions, ...tasks].filter(
|
|
19175
|
+
(d) => d.frontmatter.jiraKey
|
|
19176
|
+
);
|
|
19177
|
+
if (artifactId) {
|
|
19178
|
+
candidates = candidates.filter((d) => d.frontmatter.id === artifactId);
|
|
19179
|
+
if (candidates.length === 0) {
|
|
19180
|
+
const doc = store.get(artifactId);
|
|
19181
|
+
if (doc) {
|
|
19182
|
+
result.errors.push(
|
|
19183
|
+
`${artifactId} has no jiraKey \u2014 use push_artifact_to_jira or link_to_jira first`
|
|
19184
|
+
);
|
|
19185
|
+
} else {
|
|
19186
|
+
result.errors.push(`Artifact ${artifactId} not found`);
|
|
19187
|
+
}
|
|
19188
|
+
return result;
|
|
19189
|
+
}
|
|
19190
|
+
}
|
|
19191
|
+
candidates = candidates.filter(
|
|
19192
|
+
(d) => !DONE_STATUSES5.has(d.frontmatter.status)
|
|
19193
|
+
);
|
|
19194
|
+
for (const doc of candidates) {
|
|
19195
|
+
const jiraKey = doc.frontmatter.jiraKey;
|
|
19196
|
+
const artifactType = doc.frontmatter.type;
|
|
19197
|
+
try {
|
|
19198
|
+
const issue2 = await client.getIssueWithLinks(jiraKey);
|
|
19199
|
+
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
|
|
19200
|
+
const currentStatus = doc.frontmatter.status;
|
|
19201
|
+
const linkedIssues = [];
|
|
19202
|
+
if (issue2.fields.subtasks) {
|
|
19203
|
+
for (const sub of issue2.fields.subtasks) {
|
|
19204
|
+
linkedIssues.push({
|
|
19205
|
+
key: sub.key,
|
|
19206
|
+
summary: sub.fields.summary,
|
|
19207
|
+
status: sub.fields.status.name,
|
|
19208
|
+
relationship: "subtask",
|
|
19209
|
+
isDone: DONE_STATUSES5.has(sub.fields.status.name.toLowerCase())
|
|
19210
|
+
});
|
|
19211
|
+
}
|
|
19212
|
+
}
|
|
19213
|
+
if (issue2.fields.issuelinks) {
|
|
19214
|
+
for (const link of issue2.fields.issuelinks) {
|
|
19215
|
+
if (link.outwardIssue) {
|
|
19216
|
+
linkedIssues.push({
|
|
19217
|
+
key: link.outwardIssue.key,
|
|
19218
|
+
summary: link.outwardIssue.fields.summary,
|
|
19219
|
+
status: link.outwardIssue.fields.status.name,
|
|
19220
|
+
relationship: link.type.outward,
|
|
19221
|
+
isDone: DONE_STATUSES5.has(
|
|
19222
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
19223
|
+
)
|
|
19224
|
+
});
|
|
19225
|
+
}
|
|
19226
|
+
if (link.inwardIssue) {
|
|
19227
|
+
linkedIssues.push({
|
|
19228
|
+
key: link.inwardIssue.key,
|
|
19229
|
+
summary: link.inwardIssue.fields.summary,
|
|
19230
|
+
status: link.inwardIssue.fields.status.name,
|
|
19231
|
+
relationship: link.type.inward,
|
|
19232
|
+
isDone: DONE_STATUSES5.has(
|
|
19233
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
19234
|
+
)
|
|
19235
|
+
});
|
|
19236
|
+
}
|
|
19237
|
+
}
|
|
19238
|
+
}
|
|
19239
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
19240
|
+
let proposedProgress;
|
|
19241
|
+
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
19242
|
+
proposedProgress = computeSubtaskProgress(subtasks);
|
|
19243
|
+
}
|
|
19244
|
+
const currentProgress = doc.frontmatter.progress;
|
|
19245
|
+
result.artifacts.push({
|
|
19246
|
+
id: doc.frontmatter.id,
|
|
19247
|
+
type: artifactType,
|
|
19248
|
+
jiraKey,
|
|
19249
|
+
jiraUrl: `https://${host}/browse/${jiraKey}`,
|
|
19250
|
+
jiraSummary: issue2.fields.summary,
|
|
19251
|
+
jiraStatus: issue2.fields.status.name,
|
|
19252
|
+
currentMarvinStatus: currentStatus,
|
|
19253
|
+
proposedMarvinStatus: proposedStatus,
|
|
19254
|
+
statusChanged: currentStatus !== proposedStatus,
|
|
19255
|
+
currentProgress,
|
|
19256
|
+
proposedProgress,
|
|
19257
|
+
progressChanged: proposedProgress !== void 0 && proposedProgress !== currentProgress,
|
|
19258
|
+
linkedIssues
|
|
19259
|
+
});
|
|
19260
|
+
} catch (err) {
|
|
19261
|
+
result.errors.push(
|
|
19262
|
+
`${doc.frontmatter.id} (${jiraKey}): ${err instanceof Error ? err.message : String(err)}`
|
|
19263
|
+
);
|
|
19264
|
+
}
|
|
19265
|
+
}
|
|
19266
|
+
return result;
|
|
19267
|
+
}
|
|
19268
|
+
|
|
19269
|
+
// src/skills/builtin/jira/daily.ts
|
|
19270
|
+
var BLOCKER_PATTERNS = [
|
|
19271
|
+
/\bblocked\b/i,
|
|
19272
|
+
/\bblocking\b/i,
|
|
19273
|
+
/\bwaiting\s+for\b/i,
|
|
19274
|
+
/\bon\s+hold\b/i,
|
|
19275
|
+
/\bcan'?t\s+proceed\b/i,
|
|
19276
|
+
/\bdepends?\s+on\b/i,
|
|
19277
|
+
/\bstuck\b/i,
|
|
19278
|
+
/\bneed[s]?\s+(to\s+wait|approval|input|clarification)\b/i
|
|
19279
|
+
];
|
|
19280
|
+
var DECISION_PATTERNS = [
|
|
19281
|
+
/\bdecided\b/i,
|
|
19282
|
+
/\bagreed\b/i,
|
|
19283
|
+
/\bapproved?\b/i,
|
|
19284
|
+
/\blet'?s?\s+go\s+with\b/i,
|
|
19285
|
+
/\bwe('ll|\s+will)\s+(use|go|proceed|adopt)\b/i,
|
|
19286
|
+
/\bsigned\s+off\b/i,
|
|
19287
|
+
/\bconfirmed\b/i
|
|
19288
|
+
];
|
|
19289
|
+
var QUESTION_PATTERNS = [
|
|
19290
|
+
/\?/,
|
|
19291
|
+
/\bdoes\s+anyone\s+know\b/i,
|
|
19292
|
+
/\bhow\s+should\s+we\b/i,
|
|
19293
|
+
/\bneed\s+clarification\b/i,
|
|
19294
|
+
/\bwhat('s|\s+is)\s+the\s+(plan|approach|status)\b/i,
|
|
19295
|
+
/\bshould\s+we\b/i,
|
|
19296
|
+
/\bany\s+(idea|thought|suggestion)s?\b/i,
|
|
19297
|
+
/\bopen\s+question\b/i
|
|
19298
|
+
];
|
|
19299
|
+
var RESOLUTION_PATTERNS = [
|
|
19300
|
+
/\bfixed\b/i,
|
|
19301
|
+
/\bresolved\b/i,
|
|
19302
|
+
/\bmerged\b/i,
|
|
19303
|
+
/\bdeployed\b/i,
|
|
19304
|
+
/\bcompleted?\b/i,
|
|
19305
|
+
/\bshipped\b/i,
|
|
19306
|
+
/\bimplemented\b/i,
|
|
19307
|
+
/\bclosed\b/i
|
|
19308
|
+
];
|
|
19309
|
+
function detectCommentSignals(text) {
|
|
19310
|
+
const signals = [];
|
|
19311
|
+
const lines = text.split("\n");
|
|
19312
|
+
for (const line of lines) {
|
|
19313
|
+
const trimmed = line.trim();
|
|
19314
|
+
if (!trimmed) continue;
|
|
19315
|
+
for (const pattern of BLOCKER_PATTERNS) {
|
|
19316
|
+
if (pattern.test(trimmed)) {
|
|
19317
|
+
signals.push({ type: "blocker", snippet: truncate(trimmed, 120) });
|
|
19318
|
+
break;
|
|
19319
|
+
}
|
|
19320
|
+
}
|
|
19321
|
+
for (const pattern of DECISION_PATTERNS) {
|
|
19322
|
+
if (pattern.test(trimmed)) {
|
|
19323
|
+
signals.push({ type: "decision", snippet: truncate(trimmed, 120) });
|
|
19324
|
+
break;
|
|
19325
|
+
}
|
|
19326
|
+
}
|
|
19327
|
+
for (const pattern of QUESTION_PATTERNS) {
|
|
19328
|
+
if (pattern.test(trimmed)) {
|
|
19329
|
+
signals.push({ type: "question", snippet: truncate(trimmed, 120) });
|
|
19330
|
+
break;
|
|
19331
|
+
}
|
|
19332
|
+
}
|
|
19333
|
+
for (const pattern of RESOLUTION_PATTERNS) {
|
|
19334
|
+
if (pattern.test(trimmed)) {
|
|
19335
|
+
signals.push({ type: "resolution", snippet: truncate(trimmed, 120) });
|
|
19336
|
+
break;
|
|
19337
|
+
}
|
|
19338
|
+
}
|
|
19339
|
+
}
|
|
19340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19341
|
+
return signals.filter((s) => {
|
|
19342
|
+
if (seen.has(s.type)) return false;
|
|
19343
|
+
seen.add(s.type);
|
|
19344
|
+
return true;
|
|
19345
|
+
});
|
|
19346
|
+
}
|
|
19347
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
19348
|
+
"a",
|
|
19349
|
+
"an",
|
|
19350
|
+
"the",
|
|
19351
|
+
"and",
|
|
19352
|
+
"or",
|
|
19353
|
+
"but",
|
|
19354
|
+
"in",
|
|
19355
|
+
"on",
|
|
19356
|
+
"at",
|
|
19357
|
+
"to",
|
|
19358
|
+
"for",
|
|
19359
|
+
"of",
|
|
19360
|
+
"with",
|
|
19361
|
+
"by",
|
|
19362
|
+
"from",
|
|
19363
|
+
"is",
|
|
19364
|
+
"are",
|
|
19365
|
+
"was",
|
|
19366
|
+
"were",
|
|
19367
|
+
"be",
|
|
19368
|
+
"been",
|
|
19369
|
+
"this",
|
|
19370
|
+
"that",
|
|
19371
|
+
"it",
|
|
19372
|
+
"its",
|
|
19373
|
+
"as",
|
|
19374
|
+
"not",
|
|
19375
|
+
"no",
|
|
19376
|
+
"if",
|
|
19377
|
+
"do",
|
|
19378
|
+
"does",
|
|
19379
|
+
"new",
|
|
19380
|
+
"via",
|
|
19381
|
+
"use",
|
|
19382
|
+
"using",
|
|
19383
|
+
"based",
|
|
19384
|
+
"into",
|
|
19385
|
+
"e.g",
|
|
19386
|
+
"etc"
|
|
19387
|
+
]);
|
|
19388
|
+
function tokenize(text) {
|
|
19389
|
+
return new Set(
|
|
19390
|
+
text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
|
|
19391
|
+
);
|
|
19392
|
+
}
|
|
19393
|
+
function computeTitleSimilarity(jiraSummary, artifactTitle) {
|
|
19394
|
+
const jiraTokens = tokenize(jiraSummary);
|
|
19395
|
+
const artifactTokens = tokenize(artifactTitle);
|
|
19396
|
+
if (jiraTokens.size === 0 || artifactTokens.size === 0) {
|
|
19397
|
+
return { score: 0, sharedTerms: [] };
|
|
19398
|
+
}
|
|
19399
|
+
const shared = [];
|
|
19400
|
+
for (const token of jiraTokens) {
|
|
19401
|
+
if (artifactTokens.has(token)) {
|
|
19402
|
+
shared.push(token);
|
|
19403
|
+
}
|
|
19404
|
+
}
|
|
19405
|
+
const union2 = /* @__PURE__ */ new Set([...jiraTokens, ...artifactTokens]);
|
|
19406
|
+
const score = shared.length / union2.size;
|
|
19407
|
+
return { score, sharedTerms: shared };
|
|
19408
|
+
}
|
|
19409
|
+
var LINK_SUGGESTION_THRESHOLD = 0.15;
|
|
19410
|
+
var MAX_LINK_SUGGESTIONS = 3;
|
|
19411
|
+
function findLinkSuggestions(jiraSummary, allDocs) {
|
|
19412
|
+
const suggestions = [];
|
|
19413
|
+
for (const doc of allDocs) {
|
|
19414
|
+
const fm = doc.frontmatter;
|
|
19415
|
+
if (fm.jiraKey) continue;
|
|
19416
|
+
const { score, sharedTerms } = computeTitleSimilarity(
|
|
19417
|
+
jiraSummary,
|
|
19418
|
+
fm.title
|
|
19419
|
+
);
|
|
19420
|
+
if (score >= LINK_SUGGESTION_THRESHOLD && sharedTerms.length >= 2) {
|
|
19421
|
+
suggestions.push({
|
|
19422
|
+
artifactId: fm.id,
|
|
19423
|
+
artifactType: fm.type,
|
|
19424
|
+
artifactTitle: fm.title,
|
|
19425
|
+
score,
|
|
19426
|
+
sharedTerms
|
|
19427
|
+
});
|
|
19428
|
+
}
|
|
19429
|
+
}
|
|
19430
|
+
return suggestions.sort((a, b) => b.score - a.score).slice(0, MAX_LINK_SUGGESTIONS);
|
|
19431
|
+
}
|
|
19432
|
+
function extractCommentText(body) {
|
|
19433
|
+
if (typeof body === "string") return body;
|
|
19434
|
+
if (!body || typeof body !== "object") return "";
|
|
19435
|
+
const parts = [];
|
|
19436
|
+
function walk(node) {
|
|
19437
|
+
if (!node || typeof node !== "object") return;
|
|
19438
|
+
const n = node;
|
|
19439
|
+
if (n.type === "text" && typeof n.text === "string") {
|
|
19440
|
+
parts.push(n.text);
|
|
19441
|
+
}
|
|
19442
|
+
if (Array.isArray(n.content)) {
|
|
19443
|
+
for (const child of n.content) walk(child);
|
|
19444
|
+
}
|
|
19445
|
+
}
|
|
19446
|
+
walk(body);
|
|
19447
|
+
return parts.join(" ");
|
|
19448
|
+
}
|
|
19449
|
+
function truncate(text, maxLen = 200) {
|
|
19450
|
+
if (text.length <= maxLen) return text;
|
|
19451
|
+
return text.slice(0, maxLen) + "\u2026";
|
|
19452
|
+
}
|
|
19453
|
+
function isWithinRange(timestamp, range) {
|
|
19454
|
+
const date5 = timestamp.slice(0, 10);
|
|
19455
|
+
return date5 >= range.from && date5 <= range.to;
|
|
19456
|
+
}
|
|
19457
|
+
function isConfluenceUrl(url2) {
|
|
19458
|
+
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
19459
|
+
}
|
|
19460
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
19461
|
+
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
19462
|
+
const summary = {
|
|
19463
|
+
dateRange,
|
|
19464
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19465
|
+
projectKey,
|
|
19466
|
+
issues: [],
|
|
19467
|
+
proposedActions: [],
|
|
19468
|
+
errors: []
|
|
19469
|
+
};
|
|
19470
|
+
const jql = `project = ${projectKey} AND updated >= "${dateRange.from}" AND updated <= "${dateRange.to} 23:59" ORDER BY updated DESC`;
|
|
19471
|
+
let searchResult;
|
|
19472
|
+
try {
|
|
19473
|
+
searchResult = await client.searchIssuesV3(
|
|
19474
|
+
jql,
|
|
19475
|
+
["summary", "status", "issuetype", "priority", "assignee", "labels"],
|
|
19476
|
+
100
|
|
19477
|
+
);
|
|
19478
|
+
} catch (err) {
|
|
19479
|
+
summary.errors.push(
|
|
19480
|
+
`Search failed: ${err instanceof Error ? err.message : String(err)}`
|
|
19481
|
+
);
|
|
19482
|
+
return summary;
|
|
19483
|
+
}
|
|
19484
|
+
const allDocs = [
|
|
19485
|
+
...store.list({ type: "action" }),
|
|
19486
|
+
...store.list({ type: "task" }),
|
|
19487
|
+
...store.list({ type: "decision" }),
|
|
19488
|
+
...store.list({ type: "question" })
|
|
19489
|
+
];
|
|
19490
|
+
const otherTypes = store.registeredTypes.filter(
|
|
19491
|
+
(t) => !["action", "task", "decision", "question"].includes(t)
|
|
19492
|
+
);
|
|
19493
|
+
for (const t of otherTypes) {
|
|
19494
|
+
allDocs.push(...store.list({ type: t }));
|
|
19495
|
+
}
|
|
19496
|
+
const jiraKeyToArtifacts = /* @__PURE__ */ new Map();
|
|
19497
|
+
for (const doc of allDocs) {
|
|
19498
|
+
const jk = doc.frontmatter.jiraKey;
|
|
19499
|
+
if (jk) {
|
|
19500
|
+
const list = jiraKeyToArtifacts.get(jk) ?? [];
|
|
19501
|
+
list.push(doc);
|
|
19502
|
+
jiraKeyToArtifacts.set(jk, list);
|
|
19503
|
+
}
|
|
19504
|
+
}
|
|
19505
|
+
const BATCH_SIZE = 5;
|
|
19506
|
+
const issues = searchResult.issues;
|
|
19507
|
+
for (let i = 0; i < issues.length; i += BATCH_SIZE) {
|
|
19508
|
+
const batch = issues.slice(i, i + BATCH_SIZE);
|
|
19509
|
+
const results = await Promise.allSettled(
|
|
19510
|
+
batch.map(
|
|
19511
|
+
(issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
|
|
19512
|
+
)
|
|
19513
|
+
);
|
|
19514
|
+
for (let j = 0; j < results.length; j++) {
|
|
19515
|
+
const r = results[j];
|
|
19516
|
+
if (r.status === "fulfilled") {
|
|
19517
|
+
summary.issues.push(r.value);
|
|
19518
|
+
} else {
|
|
19519
|
+
summary.errors.push(
|
|
19520
|
+
`${batch[j].key}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
|
|
19521
|
+
);
|
|
19522
|
+
}
|
|
19523
|
+
}
|
|
19524
|
+
}
|
|
19525
|
+
summary.proposedActions = generateProposedActions(summary.issues);
|
|
19526
|
+
return summary;
|
|
19527
|
+
}
|
|
19528
|
+
async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
|
|
19529
|
+
const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
|
|
19530
|
+
client.getChangelog(issue2.key).catch(() => []),
|
|
19531
|
+
client.getComments(issue2.key).catch(() => []),
|
|
19532
|
+
client.getRemoteLinks(issue2.key).catch(() => []),
|
|
19533
|
+
client.getIssueWithLinks(issue2.key).catch(() => null)
|
|
19534
|
+
]);
|
|
19535
|
+
const changes = [];
|
|
19536
|
+
for (const entry of changelogResult) {
|
|
19537
|
+
if (!isWithinRange(entry.created, dateRange)) continue;
|
|
19538
|
+
for (const item of entry.items) {
|
|
19539
|
+
changes.push({
|
|
19540
|
+
field: item.field,
|
|
19541
|
+
from: item.fromString,
|
|
19542
|
+
to: item.toString,
|
|
19543
|
+
author: entry.author.displayName,
|
|
19544
|
+
timestamp: entry.created
|
|
19545
|
+
});
|
|
19546
|
+
}
|
|
19547
|
+
}
|
|
19548
|
+
const comments = [];
|
|
19549
|
+
for (const comment of commentsResult) {
|
|
19550
|
+
if (!isWithinRange(comment.created, dateRange) && !isWithinRange(comment.updated, dateRange)) {
|
|
19551
|
+
continue;
|
|
19552
|
+
}
|
|
19553
|
+
const fullText = extractCommentText(comment.body);
|
|
19554
|
+
const signals = detectCommentSignals(fullText);
|
|
19555
|
+
comments.push({
|
|
19556
|
+
author: comment.author.displayName,
|
|
19557
|
+
created: comment.created,
|
|
19558
|
+
bodyPreview: truncate(fullText),
|
|
19559
|
+
signals
|
|
19560
|
+
});
|
|
19561
|
+
}
|
|
19562
|
+
const confluenceLinks = [];
|
|
19563
|
+
for (const rl of remoteLinksResult) {
|
|
19564
|
+
if (isConfluenceUrl(rl.object.url)) {
|
|
19565
|
+
confluenceLinks.push({
|
|
19566
|
+
url: rl.object.url,
|
|
19567
|
+
title: rl.object.title
|
|
19568
|
+
});
|
|
19569
|
+
}
|
|
19570
|
+
}
|
|
19571
|
+
const linkedIssues = [];
|
|
19572
|
+
if (issueWithLinks) {
|
|
19573
|
+
if (issueWithLinks.fields.subtasks) {
|
|
19574
|
+
for (const sub of issueWithLinks.fields.subtasks) {
|
|
19575
|
+
linkedIssues.push({
|
|
19576
|
+
key: sub.key,
|
|
19577
|
+
summary: sub.fields.summary,
|
|
19578
|
+
status: sub.fields.status.name,
|
|
19579
|
+
relationship: "subtask",
|
|
19580
|
+
isDone: DONE_STATUSES6.has(sub.fields.status.name.toLowerCase())
|
|
19581
|
+
});
|
|
19582
|
+
}
|
|
19583
|
+
}
|
|
19584
|
+
if (issueWithLinks.fields.issuelinks) {
|
|
19585
|
+
for (const link of issueWithLinks.fields.issuelinks) {
|
|
19586
|
+
if (link.outwardIssue) {
|
|
19587
|
+
linkedIssues.push({
|
|
19588
|
+
key: link.outwardIssue.key,
|
|
19589
|
+
summary: link.outwardIssue.fields.summary,
|
|
19590
|
+
status: link.outwardIssue.fields.status.name,
|
|
19591
|
+
relationship: link.type.outward,
|
|
19592
|
+
isDone: DONE_STATUSES6.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
19593
|
+
});
|
|
19594
|
+
}
|
|
19595
|
+
if (link.inwardIssue) {
|
|
19596
|
+
linkedIssues.push({
|
|
19597
|
+
key: link.inwardIssue.key,
|
|
19598
|
+
summary: link.inwardIssue.fields.summary,
|
|
19599
|
+
status: link.inwardIssue.fields.status.name,
|
|
19600
|
+
relationship: link.type.inward,
|
|
19601
|
+
isDone: DONE_STATUSES6.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
19602
|
+
});
|
|
19603
|
+
}
|
|
19604
|
+
}
|
|
19605
|
+
}
|
|
19606
|
+
}
|
|
19607
|
+
const marvinArtifacts = [];
|
|
19608
|
+
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
19609
|
+
for (const doc of artifacts) {
|
|
19610
|
+
const fm = doc.frontmatter;
|
|
19611
|
+
const artifactType = fm.type;
|
|
19612
|
+
let proposedStatus = null;
|
|
19613
|
+
if (artifactType === "action" || artifactType === "task") {
|
|
19614
|
+
const jiraStatus = issue2.fields.status?.name;
|
|
19615
|
+
if (jiraStatus) {
|
|
19616
|
+
proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
|
|
19617
|
+
}
|
|
19618
|
+
}
|
|
19619
|
+
marvinArtifacts.push({
|
|
19620
|
+
id: fm.id,
|
|
19621
|
+
type: artifactType,
|
|
19622
|
+
title: fm.title,
|
|
19623
|
+
currentStatus: fm.status,
|
|
19624
|
+
proposedStatus,
|
|
19625
|
+
statusDrift: proposedStatus !== null && proposedStatus !== fm.status
|
|
19626
|
+
});
|
|
19627
|
+
}
|
|
19628
|
+
const linkSuggestions = marvinArtifacts.length === 0 ? findLinkSuggestions(issue2.fields.summary, allDocs) : [];
|
|
19629
|
+
return {
|
|
19630
|
+
key: issue2.key,
|
|
19631
|
+
summary: issue2.fields.summary,
|
|
19632
|
+
currentStatus: issue2.fields.status?.name ?? "Unknown",
|
|
19633
|
+
issueType: issue2.fields.issuetype?.name ?? "Unknown",
|
|
19634
|
+
assignee: issue2.fields.assignee?.displayName ?? null,
|
|
19635
|
+
changes,
|
|
19636
|
+
comments,
|
|
19637
|
+
linkedIssues,
|
|
19638
|
+
confluenceLinks,
|
|
19639
|
+
marvinArtifacts,
|
|
19640
|
+
linkSuggestions
|
|
19641
|
+
};
|
|
19642
|
+
}
|
|
19643
|
+
function generateProposedActions(issues) {
|
|
19644
|
+
const actions = [];
|
|
19645
|
+
for (const issue2 of issues) {
|
|
19646
|
+
for (const artifact of issue2.marvinArtifacts) {
|
|
19647
|
+
if (artifact.statusDrift && artifact.proposedStatus) {
|
|
19648
|
+
actions.push({
|
|
19649
|
+
type: "status-update",
|
|
19650
|
+
description: `Update ${artifact.id} (${artifact.type}) status: ${artifact.currentStatus} \u2192 ${artifact.proposedStatus} (Jira ${issue2.key} is "${issue2.currentStatus}")`,
|
|
19651
|
+
artifactId: artifact.id,
|
|
19652
|
+
jiraKey: issue2.key
|
|
19653
|
+
});
|
|
19654
|
+
}
|
|
19655
|
+
}
|
|
19656
|
+
if (issue2.marvinArtifacts.length === 0 && (issue2.changes.length > 0 || issue2.comments.length > 0)) {
|
|
19657
|
+
actions.push({
|
|
19658
|
+
type: "unlinked-issue",
|
|
19659
|
+
description: `${issue2.key} ("${issue2.summary}") has activity but no Marvin artifact \u2014 consider linking or creating one`,
|
|
19660
|
+
jiraKey: issue2.key
|
|
19661
|
+
});
|
|
19662
|
+
}
|
|
19663
|
+
for (const suggestion of issue2.linkSuggestions) {
|
|
19664
|
+
actions.push({
|
|
19665
|
+
type: "link-suggestion",
|
|
19666
|
+
description: `${issue2.key} ("${issue2.summary}") may match ${suggestion.artifactId} ("${suggestion.artifactTitle}") \u2014 shared terms: ${suggestion.sharedTerms.join(", ")} (${Math.round(suggestion.score * 100)}% similarity)`,
|
|
19667
|
+
artifactId: suggestion.artifactId,
|
|
19668
|
+
jiraKey: issue2.key
|
|
19669
|
+
});
|
|
19670
|
+
}
|
|
19671
|
+
for (const comment of issue2.comments) {
|
|
19672
|
+
for (const signal of comment.signals) {
|
|
19673
|
+
if (signal.type === "blocker") {
|
|
19674
|
+
actions.push({
|
|
19675
|
+
type: "blocker-detected",
|
|
19676
|
+
description: `Blocker in ${issue2.key} comment by ${comment.author}: "${signal.snippet}"`,
|
|
19677
|
+
jiraKey: issue2.key
|
|
19678
|
+
});
|
|
19679
|
+
}
|
|
19680
|
+
if (signal.type === "decision") {
|
|
19681
|
+
actions.push({
|
|
19682
|
+
type: "decision-candidate",
|
|
19683
|
+
description: `Possible decision in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a decision artifact`,
|
|
19684
|
+
jiraKey: issue2.key
|
|
19685
|
+
});
|
|
19686
|
+
}
|
|
19687
|
+
if (signal.type === "question") {
|
|
19688
|
+
const linkedQuestion = issue2.marvinArtifacts.find(
|
|
19689
|
+
(a) => a.type === "question" && a.currentStatus !== "answered"
|
|
19690
|
+
);
|
|
19691
|
+
if (linkedQuestion) {
|
|
19692
|
+
actions.push({
|
|
19693
|
+
type: "question-candidate",
|
|
19694
|
+
description: `Question in ${issue2.key} comment by ${comment.author} \u2014 may relate to ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
|
|
19695
|
+
artifactId: linkedQuestion.id,
|
|
19696
|
+
jiraKey: issue2.key
|
|
19697
|
+
});
|
|
19698
|
+
} else {
|
|
19699
|
+
actions.push({
|
|
19700
|
+
type: "question-candidate",
|
|
19701
|
+
description: `Question in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a question artifact`,
|
|
19702
|
+
jiraKey: issue2.key
|
|
19703
|
+
});
|
|
19704
|
+
}
|
|
19705
|
+
}
|
|
19706
|
+
if (signal.type === "resolution") {
|
|
19707
|
+
const linkedQuestion = issue2.marvinArtifacts.find(
|
|
19708
|
+
(a) => a.type === "question" && a.currentStatus !== "answered"
|
|
19709
|
+
);
|
|
19710
|
+
if (linkedQuestion) {
|
|
19711
|
+
actions.push({
|
|
19712
|
+
type: "resolution-detected",
|
|
19713
|
+
description: `Resolution in ${issue2.key} by ${comment.author} may answer ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
|
|
19714
|
+
artifactId: linkedQuestion.id,
|
|
19715
|
+
jiraKey: issue2.key
|
|
19716
|
+
});
|
|
19717
|
+
}
|
|
19718
|
+
}
|
|
19719
|
+
}
|
|
19720
|
+
}
|
|
19721
|
+
for (const cl of issue2.confluenceLinks) {
|
|
19722
|
+
actions.push({
|
|
19723
|
+
type: "confluence-review",
|
|
19724
|
+
description: `Confluence page "${cl.title}" linked from ${issue2.key} \u2014 review for relevant updates`,
|
|
19725
|
+
jiraKey: issue2.key
|
|
19726
|
+
});
|
|
19727
|
+
}
|
|
19728
|
+
}
|
|
19729
|
+
return actions;
|
|
19086
19730
|
}
|
|
19087
19731
|
|
|
19088
19732
|
// src/skills/builtin/jira/tools.ts
|
|
@@ -19126,6 +19770,7 @@ function findByJiraKey(store, jiraKey) {
|
|
|
19126
19770
|
function createJiraTools(store, projectConfig) {
|
|
19127
19771
|
const jiraUserConfig = loadUserConfig().jira;
|
|
19128
19772
|
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
19773
|
+
const statusMap = projectConfig?.jira?.statusMap;
|
|
19129
19774
|
return [
|
|
19130
19775
|
// --- Local read tools ---
|
|
19131
19776
|
tool20(
|
|
@@ -19286,9 +19931,9 @@ function createJiraTools(store, projectConfig) {
|
|
|
19286
19931
|
// --- Local → Jira tools ---
|
|
19287
19932
|
tool20(
|
|
19288
19933
|
"push_artifact_to_jira",
|
|
19289
|
-
"Create a Jira issue from
|
|
19934
|
+
"Create a Jira issue from a Marvin artifact. For actions/tasks, links directly via jiraKey on the artifact. For other types, creates a JI-xxx tracking document.",
|
|
19290
19935
|
{
|
|
19291
|
-
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', '
|
|
19936
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
|
|
19292
19937
|
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
|
|
19293
19938
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
19294
19939
|
},
|
|
@@ -19329,6 +19974,24 @@ function createJiraTools(store, projectConfig) {
|
|
|
19329
19974
|
description,
|
|
19330
19975
|
issuetype: { name: args.issueType ?? "Task" }
|
|
19331
19976
|
});
|
|
19977
|
+
const isDirectLink = artifact.frontmatter.type === "action" || artifact.frontmatter.type === "task";
|
|
19978
|
+
if (isDirectLink) {
|
|
19979
|
+
const existingTags = artifact.frontmatter.tags ?? [];
|
|
19980
|
+
store.update(args.artifactId, {
|
|
19981
|
+
jiraKey: jiraResult.key,
|
|
19982
|
+
jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
|
|
19983
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19984
|
+
tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${jiraResult.key}`]
|
|
19985
|
+
});
|
|
19986
|
+
return {
|
|
19987
|
+
content: [
|
|
19988
|
+
{
|
|
19989
|
+
type: "text",
|
|
19990
|
+
text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Linked directly on the artifact.`
|
|
19991
|
+
}
|
|
19992
|
+
]
|
|
19993
|
+
};
|
|
19994
|
+
}
|
|
19332
19995
|
const jiDoc = store.create(
|
|
19333
19996
|
JIRA_TYPE,
|
|
19334
19997
|
{
|
|
@@ -19450,65 +20113,430 @@ function createJiraTools(store, projectConfig) {
|
|
|
19450
20113
|
]
|
|
19451
20114
|
};
|
|
19452
20115
|
}
|
|
20116
|
+
),
|
|
20117
|
+
// --- Direct Jira linking for actions/tasks ---
|
|
20118
|
+
tool20(
|
|
20119
|
+
"link_to_jira",
|
|
20120
|
+
"Link an existing Jira issue to a Marvin action or task (sets jiraKey directly on the artifact)",
|
|
20121
|
+
{
|
|
20122
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'A-001', 'T-003')"),
|
|
20123
|
+
jiraKey: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
|
|
20124
|
+
},
|
|
20125
|
+
async (args) => {
|
|
20126
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
20127
|
+
if (!jira) return jiraNotConfiguredError();
|
|
20128
|
+
const artifact = store.get(args.artifactId);
|
|
20129
|
+
if (!artifact) {
|
|
20130
|
+
return {
|
|
20131
|
+
content: [
|
|
20132
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
20133
|
+
],
|
|
20134
|
+
isError: true
|
|
20135
|
+
};
|
|
20136
|
+
}
|
|
20137
|
+
if (artifact.frontmatter.type !== "action" && artifact.frontmatter.type !== "task") {
|
|
20138
|
+
return {
|
|
20139
|
+
content: [
|
|
20140
|
+
{
|
|
20141
|
+
type: "text",
|
|
20142
|
+
text: `link_to_jira only supports action and task artifacts. ${args.artifactId} is type "${artifact.frontmatter.type}". Use link_artifact_to_jira for JI-xxx documents instead.`
|
|
20143
|
+
}
|
|
20144
|
+
],
|
|
20145
|
+
isError: true
|
|
20146
|
+
};
|
|
20147
|
+
}
|
|
20148
|
+
const issue2 = await jira.client.getIssue(args.jiraKey);
|
|
20149
|
+
const existingTags = artifact.frontmatter.tags ?? [];
|
|
20150
|
+
store.update(args.artifactId, {
|
|
20151
|
+
jiraKey: args.jiraKey,
|
|
20152
|
+
jiraUrl: `https://${jira.host}/browse/${args.jiraKey}`,
|
|
20153
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20154
|
+
tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${args.jiraKey}`]
|
|
20155
|
+
});
|
|
20156
|
+
return {
|
|
20157
|
+
content: [
|
|
20158
|
+
{
|
|
20159
|
+
type: "text",
|
|
20160
|
+
text: `Linked ${args.artifactId} to Jira ${args.jiraKey} ("${issue2.fields.summary}").`
|
|
20161
|
+
}
|
|
20162
|
+
]
|
|
20163
|
+
};
|
|
20164
|
+
}
|
|
20165
|
+
),
|
|
20166
|
+
// --- Jira status fetch (read-only) ---
|
|
20167
|
+
tool20(
|
|
20168
|
+
"fetch_jira_status",
|
|
20169
|
+
"Fetch current Jira status for actions/tasks with jiraKey. Read-only \u2014 returns proposed changes for review. Use update_action/update_task to apply changes.",
|
|
20170
|
+
{
|
|
20171
|
+
artifactId: external_exports.string().optional().describe("Specific artifact ID to check, or omit to check all Jira-linked actions/tasks")
|
|
20172
|
+
},
|
|
20173
|
+
async (args) => {
|
|
20174
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
20175
|
+
if (!jira) return jiraNotConfiguredError();
|
|
20176
|
+
const fetchResult = await fetchJiraStatus(
|
|
20177
|
+
store,
|
|
20178
|
+
jira.client,
|
|
20179
|
+
jira.host,
|
|
20180
|
+
args.artifactId,
|
|
20181
|
+
statusMap
|
|
20182
|
+
);
|
|
20183
|
+
const parts = [];
|
|
20184
|
+
if (fetchResult.artifacts.length > 0) {
|
|
20185
|
+
for (const a of fetchResult.artifacts) {
|
|
20186
|
+
const changes = [];
|
|
20187
|
+
if (a.statusChanged) {
|
|
20188
|
+
changes.push(`status: ${a.currentMarvinStatus} \u2192 ${a.proposedMarvinStatus}`);
|
|
20189
|
+
}
|
|
20190
|
+
if (a.progressChanged) {
|
|
20191
|
+
changes.push(`progress: ${a.currentProgress ?? 0}% \u2192 ${a.proposedProgress}%`);
|
|
20192
|
+
}
|
|
20193
|
+
const header = `${a.id} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}" [${a.jiraStatus}]`;
|
|
20194
|
+
if (changes.length > 0) {
|
|
20195
|
+
parts.push(`${header}
|
|
20196
|
+
Proposed changes: ${changes.join(", ")}`);
|
|
20197
|
+
} else {
|
|
20198
|
+
parts.push(`${header}
|
|
20199
|
+
No status/progress changes.`);
|
|
20200
|
+
}
|
|
20201
|
+
if (a.linkedIssues.length > 0) {
|
|
20202
|
+
const done = a.linkedIssues.filter((l) => l.isDone).length;
|
|
20203
|
+
parts.push(` Linked issues (${done}/${a.linkedIssues.length} done):`);
|
|
20204
|
+
for (const li of a.linkedIssues) {
|
|
20205
|
+
const icon = li.isDone ? "\u2713" : "\u25CB";
|
|
20206
|
+
parts.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
|
|
20207
|
+
}
|
|
20208
|
+
}
|
|
20209
|
+
}
|
|
20210
|
+
parts.push("");
|
|
20211
|
+
parts.push("This is a read-only preview. Use update_action or update_task to apply the proposed status/progress changes.");
|
|
20212
|
+
}
|
|
20213
|
+
if (fetchResult.errors.length > 0) {
|
|
20214
|
+
parts.push("Errors:");
|
|
20215
|
+
for (const err of fetchResult.errors) {
|
|
20216
|
+
parts.push(` ${err}`);
|
|
20217
|
+
}
|
|
20218
|
+
}
|
|
20219
|
+
if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
|
|
20220
|
+
parts.push("No Jira-linked actions/tasks found.");
|
|
20221
|
+
}
|
|
20222
|
+
return {
|
|
20223
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
20224
|
+
isError: fetchResult.errors.length > 0 && fetchResult.artifacts.length === 0
|
|
20225
|
+
};
|
|
20226
|
+
},
|
|
20227
|
+
{ annotations: { readOnlyHint: true } }
|
|
20228
|
+
),
|
|
20229
|
+
// --- Jira status discovery ---
|
|
20230
|
+
tool20(
|
|
20231
|
+
"fetch_jira_statuses",
|
|
20232
|
+
"Fetch all distinct issue statuses from a Jira project and show which are mapped vs unmapped to Marvin statuses. Helps configure jira.statusMap in .marvin/config.yaml.",
|
|
20233
|
+
{
|
|
20234
|
+
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'MCB1'). Falls back to jira.projectKey from config."),
|
|
20235
|
+
maxResults: external_exports.number().optional().describe("Max issues to scan (default 100)")
|
|
20236
|
+
},
|
|
20237
|
+
async (args) => {
|
|
20238
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
20239
|
+
if (!resolvedProjectKey) {
|
|
20240
|
+
return {
|
|
20241
|
+
content: [
|
|
20242
|
+
{
|
|
20243
|
+
type: "text",
|
|
20244
|
+
text: "No projectKey provided and no default configured."
|
|
20245
|
+
}
|
|
20246
|
+
],
|
|
20247
|
+
isError: true
|
|
20248
|
+
};
|
|
20249
|
+
}
|
|
20250
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
20251
|
+
if (!jira) return jiraNotConfiguredError();
|
|
20252
|
+
const host = jira.host;
|
|
20253
|
+
const auth = "Basic " + Buffer.from(
|
|
20254
|
+
`${jiraUserConfig?.email ?? process.env.JIRA_EMAIL}:${jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN}`
|
|
20255
|
+
).toString("base64");
|
|
20256
|
+
const params = new URLSearchParams({
|
|
20257
|
+
jql: `project = ${resolvedProjectKey}`,
|
|
20258
|
+
maxResults: String(args.maxResults ?? 100),
|
|
20259
|
+
fields: "status"
|
|
20260
|
+
});
|
|
20261
|
+
const resp = await fetch(`https://${host}/rest/api/3/search/jql?${params}`, {
|
|
20262
|
+
headers: { Authorization: auth, Accept: "application/json" }
|
|
20263
|
+
});
|
|
20264
|
+
if (!resp.ok) {
|
|
20265
|
+
const text = await resp.text().catch(() => "");
|
|
20266
|
+
return {
|
|
20267
|
+
content: [
|
|
20268
|
+
{
|
|
20269
|
+
type: "text",
|
|
20270
|
+
text: `Jira API error ${resp.status}: ${text}`
|
|
20271
|
+
}
|
|
20272
|
+
],
|
|
20273
|
+
isError: true
|
|
20274
|
+
};
|
|
20275
|
+
}
|
|
20276
|
+
const data = await resp.json();
|
|
20277
|
+
const statusCounts = /* @__PURE__ */ new Map();
|
|
20278
|
+
for (const issue2 of data.issues) {
|
|
20279
|
+
const s = issue2.fields.status.name;
|
|
20280
|
+
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
20281
|
+
}
|
|
20282
|
+
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
20283
|
+
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
20284
|
+
const actionLookup = /* @__PURE__ */ new Map();
|
|
20285
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
20286
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
20287
|
+
}
|
|
20288
|
+
const taskLookup = /* @__PURE__ */ new Map();
|
|
20289
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
20290
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
20291
|
+
}
|
|
20292
|
+
const parts = [
|
|
20293
|
+
`Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
|
|
20294
|
+
""
|
|
20295
|
+
];
|
|
20296
|
+
const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
20297
|
+
const unmappedAction = [];
|
|
20298
|
+
const unmappedTask = [];
|
|
20299
|
+
for (const [status, count] of sorted) {
|
|
20300
|
+
const actionTarget = actionLookup.get(status.toLowerCase());
|
|
20301
|
+
const taskTarget = taskLookup.get(status.toLowerCase());
|
|
20302
|
+
const actionLabel = actionTarget ? `\u2192 ${actionTarget}` : "UNMAPPED (\u2192 open)";
|
|
20303
|
+
const taskLabel = taskTarget ? `\u2192 ${taskTarget}` : "UNMAPPED (\u2192 backlog)";
|
|
20304
|
+
parts.push(` ${status} (${count} issues)`);
|
|
20305
|
+
parts.push(` action: ${actionLabel}`);
|
|
20306
|
+
parts.push(` task: ${taskLabel}`);
|
|
20307
|
+
if (!actionTarget) unmappedAction.push(status);
|
|
20308
|
+
if (!taskTarget) unmappedTask.push(status);
|
|
20309
|
+
}
|
|
20310
|
+
if (unmappedAction.length > 0 || unmappedTask.length > 0) {
|
|
20311
|
+
parts.push("");
|
|
20312
|
+
parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
|
|
20313
|
+
parts.push(" jira:");
|
|
20314
|
+
parts.push(" statusMap:");
|
|
20315
|
+
if (unmappedAction.length > 0) {
|
|
20316
|
+
parts.push(" action:");
|
|
20317
|
+
parts.push(` # Map these: ${unmappedAction.join(", ")}`);
|
|
20318
|
+
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
20319
|
+
}
|
|
20320
|
+
if (unmappedTask.length > 0) {
|
|
20321
|
+
parts.push(" task:");
|
|
20322
|
+
parts.push(` # Map these: ${unmappedTask.join(", ")}`);
|
|
20323
|
+
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
20324
|
+
}
|
|
20325
|
+
} else {
|
|
20326
|
+
parts.push("");
|
|
20327
|
+
parts.push("All statuses are mapped.");
|
|
20328
|
+
}
|
|
20329
|
+
const usingConfig = statusMap?.action || statusMap?.task;
|
|
20330
|
+
parts.push("");
|
|
20331
|
+
parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
|
|
20332
|
+
return {
|
|
20333
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
20334
|
+
};
|
|
20335
|
+
},
|
|
20336
|
+
{ annotations: { readOnlyHint: true } }
|
|
20337
|
+
),
|
|
20338
|
+
// --- Jira daily summary ---
|
|
20339
|
+
tool20(
|
|
20340
|
+
"fetch_jira_daily",
|
|
20341
|
+
"Fetch a daily summary of Jira changes: status transitions, comments, linked Confluence pages, and cross-referenced Marvin artifacts. Read-only \u2014 returns proposed actions for review.",
|
|
20342
|
+
{
|
|
20343
|
+
from: external_exports.string().optional().describe("Start date (YYYY-MM-DD). Defaults to today."),
|
|
20344
|
+
to: external_exports.string().optional().describe("End date (YYYY-MM-DD). Defaults to same as 'from'."),
|
|
20345
|
+
projectKey: external_exports.string().optional().describe("Jira project key. Falls back to jira.projectKey from config.")
|
|
20346
|
+
},
|
|
20347
|
+
async (args) => {
|
|
20348
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
20349
|
+
if (!resolvedProjectKey) {
|
|
20350
|
+
return {
|
|
20351
|
+
content: [
|
|
20352
|
+
{
|
|
20353
|
+
type: "text",
|
|
20354
|
+
text: "No projectKey provided and no default configured."
|
|
20355
|
+
}
|
|
20356
|
+
],
|
|
20357
|
+
isError: true
|
|
20358
|
+
};
|
|
20359
|
+
}
|
|
20360
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
20361
|
+
if (!jira) return jiraNotConfiguredError();
|
|
20362
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
20363
|
+
const fromDate = args.from ?? today;
|
|
20364
|
+
const toDate = args.to ?? fromDate;
|
|
20365
|
+
const daily = await fetchJiraDaily(
|
|
20366
|
+
store,
|
|
20367
|
+
jira.client,
|
|
20368
|
+
jira.host,
|
|
20369
|
+
resolvedProjectKey,
|
|
20370
|
+
{ from: fromDate, to: toDate },
|
|
20371
|
+
statusMap
|
|
20372
|
+
);
|
|
20373
|
+
return {
|
|
20374
|
+
content: [{ type: "text", text: formatDailySummary(daily) }],
|
|
20375
|
+
isError: daily.errors.length > 0 && daily.issues.length === 0
|
|
20376
|
+
};
|
|
20377
|
+
},
|
|
20378
|
+
{ annotations: { readOnlyHint: true } }
|
|
19453
20379
|
)
|
|
19454
20380
|
];
|
|
19455
20381
|
}
|
|
20382
|
+
function formatDailySummary(daily) {
|
|
20383
|
+
const parts = [];
|
|
20384
|
+
const rangeLabel = daily.dateRange.from === daily.dateRange.to ? daily.dateRange.from : `${daily.dateRange.from} to ${daily.dateRange.to}`;
|
|
20385
|
+
parts.push(`Jira Daily Summary \u2014 ${daily.projectKey} \u2014 ${rangeLabel}`);
|
|
20386
|
+
parts.push(`${daily.issues.length} issue(s) updated.
|
|
20387
|
+
`);
|
|
20388
|
+
const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
|
|
20389
|
+
const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
|
|
20390
|
+
if (linked.length > 0) {
|
|
20391
|
+
parts.push("## Linked Issues (with Marvin artifacts)\n");
|
|
20392
|
+
for (const issue2 of linked) {
|
|
20393
|
+
parts.push(formatIssueEntry(issue2));
|
|
20394
|
+
}
|
|
20395
|
+
}
|
|
20396
|
+
if (unlinked.length > 0) {
|
|
20397
|
+
parts.push("## Unlinked Issues (no Marvin artifact)\n");
|
|
20398
|
+
for (const issue2 of unlinked) {
|
|
20399
|
+
parts.push(formatIssueEntry(issue2));
|
|
20400
|
+
}
|
|
20401
|
+
}
|
|
20402
|
+
if (daily.proposedActions.length > 0) {
|
|
20403
|
+
parts.push("## Proposed Actions\n");
|
|
20404
|
+
for (const action of daily.proposedActions) {
|
|
20405
|
+
const icon = action.type === "status-update" ? "\u21BB" : action.type === "unlinked-issue" ? "+" : action.type === "link-suggestion" ? "\u{1F517}" : action.type === "question-candidate" ? "?" : action.type === "decision-candidate" ? "\u2696" : action.type === "blocker-detected" ? "\u{1F6AB}" : action.type === "resolution-detected" ? "\u2713" : "\u{1F4C4}";
|
|
20406
|
+
parts.push(` ${icon} ${action.description}`);
|
|
20407
|
+
}
|
|
20408
|
+
parts.push("");
|
|
20409
|
+
parts.push("These are suggestions. Use update_action, update_task, or other tools to apply changes.");
|
|
20410
|
+
}
|
|
20411
|
+
if (daily.errors.length > 0) {
|
|
20412
|
+
parts.push("\n## Errors\n");
|
|
20413
|
+
for (const err of daily.errors) {
|
|
20414
|
+
parts.push(` ${err}`);
|
|
20415
|
+
}
|
|
20416
|
+
}
|
|
20417
|
+
return parts.join("\n");
|
|
20418
|
+
}
|
|
20419
|
+
function formatIssueEntry(issue2) {
|
|
20420
|
+
const lines = [];
|
|
20421
|
+
const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
|
|
20422
|
+
const artifactLabel = artifacts ? ` \u2192 ${artifacts}` : "";
|
|
20423
|
+
lines.push(`### ${issue2.key} \u2014 ${issue2.summary} [${issue2.currentStatus}]${artifactLabel}`);
|
|
20424
|
+
lines.push(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`);
|
|
20425
|
+
for (const a of issue2.marvinArtifacts) {
|
|
20426
|
+
if (a.statusDrift) {
|
|
20427
|
+
lines.push(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`);
|
|
20428
|
+
}
|
|
20429
|
+
}
|
|
20430
|
+
if (issue2.changes.length > 0) {
|
|
20431
|
+
lines.push(" Changes:");
|
|
20432
|
+
for (const c of issue2.changes) {
|
|
20433
|
+
lines.push(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`);
|
|
20434
|
+
}
|
|
20435
|
+
}
|
|
20436
|
+
if (issue2.comments.length > 0) {
|
|
20437
|
+
lines.push(` Comments (${issue2.comments.length}):`);
|
|
20438
|
+
for (const c of issue2.comments) {
|
|
20439
|
+
let signalIcons = "";
|
|
20440
|
+
if (c.signals.length > 0) {
|
|
20441
|
+
const icons = c.signals.map(
|
|
20442
|
+
(s) => s.type === "blocker" ? "\u{1F6AB}" : s.type === "decision" ? "\u2696" : s.type === "question" ? "?" : "\u2713"
|
|
20443
|
+
);
|
|
20444
|
+
signalIcons = ` [${icons.join("")}]`;
|
|
20445
|
+
}
|
|
20446
|
+
lines.push(` ${c.author} (${c.created.slice(0, 16)})${signalIcons}: ${c.bodyPreview}`);
|
|
20447
|
+
}
|
|
20448
|
+
}
|
|
20449
|
+
if (issue2.linkSuggestions.length > 0) {
|
|
20450
|
+
lines.push(" Possible Marvin matches:");
|
|
20451
|
+
for (const s of issue2.linkSuggestions) {
|
|
20452
|
+
lines.push(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`);
|
|
20453
|
+
}
|
|
20454
|
+
}
|
|
20455
|
+
if (issue2.linkedIssues.length > 0) {
|
|
20456
|
+
lines.push(" Linked issues:");
|
|
20457
|
+
for (const li of issue2.linkedIssues) {
|
|
20458
|
+
const icon = li.isDone ? "\u2713" : "\u25CB";
|
|
20459
|
+
lines.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
|
|
20460
|
+
}
|
|
20461
|
+
}
|
|
20462
|
+
if (issue2.confluenceLinks.length > 0) {
|
|
20463
|
+
lines.push(" Confluence pages:");
|
|
20464
|
+
for (const cl of issue2.confluenceLinks) {
|
|
20465
|
+
lines.push(` \u{1F4C4} ${cl.title}: ${cl.url}`);
|
|
20466
|
+
}
|
|
20467
|
+
}
|
|
20468
|
+
lines.push("");
|
|
20469
|
+
return lines.join("\n");
|
|
20470
|
+
}
|
|
19456
20471
|
|
|
19457
20472
|
// src/skills/builtin/jira/index.ts
|
|
20473
|
+
var COMMON_TOOLS = `**Available tools:**
|
|
20474
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues (JI-xxx documents)
|
|
20475
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20476
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact. For **actions and tasks**, links directly via \`jiraKey\` on the artifact (no JI-xxx intermediary). For other types, creates a JI-xxx tracking document.
|
|
20477
|
+
- \`link_to_jira\` \u2014 link an existing Jira issue to a Marvin action or task (sets \`jiraKey\` directly on the artifact)
|
|
20478
|
+
- \`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.
|
|
20479
|
+
- \`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).
|
|
20480
|
+
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
20481
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20482
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx`;
|
|
20483
|
+
var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
20484
|
+
1. Call \`fetch_jira_status\` to see what Jira reports for linked artifacts
|
|
20485
|
+
2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
|
|
20486
|
+
3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
|
|
20487
|
+
|
|
20488
|
+
**Daily review workflow:**
|
|
20489
|
+
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
20490
|
+
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
20491
|
+
3. Use existing tools to apply changes, create new artifacts, or link untracked issues`;
|
|
19458
20492
|
var jiraSkill = {
|
|
19459
20493
|
id: "jira",
|
|
19460
20494
|
name: "Jira Integration",
|
|
19461
20495
|
description: "Bidirectional sync between Marvin artifacts and Jira issues",
|
|
19462
20496
|
version: "1.0.0",
|
|
19463
20497
|
format: "builtin-ts",
|
|
19464
|
-
// No default persona affinity — opt-in via config.yaml skills section
|
|
19465
20498
|
documentTypeRegistrations: [
|
|
19466
20499
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
19467
20500
|
],
|
|
19468
20501
|
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
19469
20502
|
promptFragments: {
|
|
19470
|
-
"product-owner": `You have the **Jira Integration** skill.
|
|
20503
|
+
"product-owner": `You have the **Jira Integration** skill.
|
|
19471
20504
|
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19475
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
19476
|
-
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
19477
|
-
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20505
|
+
${COMMON_TOOLS}
|
|
20506
|
+
|
|
20507
|
+
${COMMON_WORKFLOW}
|
|
19478
20508
|
|
|
19479
20509
|
**As Product Owner, use Jira integration to:**
|
|
20510
|
+
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
19480
20511
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
19481
20512
|
- Push approved features as Stories for development tracking
|
|
19482
20513
|
- Link decisions to Jira issues for audit trail and traceability
|
|
19483
|
-
- Use
|
|
19484
|
-
"tech-lead": `You have the **Jira Integration** skill.
|
|
20514
|
+
- Use \`fetch_jira_statuses\` when setting up a new project to configure status mappings`,
|
|
20515
|
+
"tech-lead": `You have the **Jira Integration** skill.
|
|
19485
20516
|
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
19490
|
-
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
19491
|
-
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20517
|
+
${COMMON_TOOLS}
|
|
20518
|
+
|
|
20519
|
+
${COMMON_WORKFLOW}
|
|
19492
20520
|
|
|
19493
20521
|
**As Tech Lead, use Jira integration to:**
|
|
20522
|
+
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
19494
20523
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
19495
20524
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
19496
|
-
-
|
|
19497
|
-
- Use
|
|
19498
|
-
"delivery-manager": `You have the **Jira Integration** skill.
|
|
20525
|
+
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
20526
|
+
- Use \`fetch_jira_statuses\` to verify status mappings match the team's Jira workflow`,
|
|
20527
|
+
"delivery-manager": `You have the **Jira Integration** skill.
|
|
19499
20528
|
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
19505
|
-
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20529
|
+
${COMMON_TOOLS}
|
|
20530
|
+
|
|
20531
|
+
${COMMON_WORKFLOW}
|
|
20532
|
+
This is a third path for progress tracking alongside Contributions and Meetings.
|
|
19506
20533
|
|
|
19507
20534
|
**As Delivery Manager, use Jira integration to:**
|
|
20535
|
+
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
19508
20536
|
- Pull sprint issues for tracking progress and blockers
|
|
19509
|
-
- Push actions
|
|
19510
|
-
- Use
|
|
19511
|
-
-
|
|
20537
|
+
- Push actions and tasks to Jira for stakeholder visibility
|
|
20538
|
+
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
20539
|
+
- Use \`fetch_jira_statuses\` to ensure Jira workflow statuses are properly mapped`
|
|
19512
20540
|
}
|
|
19513
20541
|
};
|
|
19514
20542
|
|
|
@@ -22857,7 +23885,7 @@ function buildHealthGauge(categories) {
|
|
|
22857
23885
|
}
|
|
22858
23886
|
|
|
22859
23887
|
// src/web/templates/pages/po/dashboard.ts
|
|
22860
|
-
var
|
|
23888
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
22861
23889
|
var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
22862
23890
|
function poDashboardPage(ctx) {
|
|
22863
23891
|
const overview = getOverviewData(ctx.store);
|
|
@@ -22902,7 +23930,7 @@ function poDashboardPage(ctx) {
|
|
|
22902
23930
|
sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
|
|
22903
23931
|
}
|
|
22904
23932
|
}
|
|
22905
|
-
const featuresDone = features.filter((d) =>
|
|
23933
|
+
const featuresDone = features.filter((d) => DONE_STATUSES7.has(d.frontmatter.status)).length;
|
|
22906
23934
|
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
22907
23935
|
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
22908
23936
|
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
|
|
@@ -22971,7 +23999,7 @@ function poDashboardPage(ctx) {
|
|
|
22971
23999
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22972
24000
|
const atRiskItems = [];
|
|
22973
24001
|
for (const f of features) {
|
|
22974
|
-
if (
|
|
24002
|
+
if (DONE_STATUSES7.has(f.frontmatter.status)) continue;
|
|
22975
24003
|
const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
|
|
22976
24004
|
const reasons = [];
|
|
22977
24005
|
let blocked = 0;
|
|
@@ -22983,7 +24011,7 @@ function poDashboardPage(ctx) {
|
|
|
22983
24011
|
if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
|
|
22984
24012
|
for (const epic of fEpics) {
|
|
22985
24013
|
const td = epic.frontmatter.targetDate;
|
|
22986
|
-
if (td && td < today && !
|
|
24014
|
+
if (td && td < today && !DONE_STATUSES7.has(epic.frontmatter.status)) {
|
|
22987
24015
|
reasons.push(`${epic.frontmatter.id} overdue`);
|
|
22988
24016
|
}
|
|
22989
24017
|
}
|
|
@@ -23261,7 +24289,7 @@ function poBacklogPage(ctx) {
|
|
|
23261
24289
|
}
|
|
23262
24290
|
}
|
|
23263
24291
|
}
|
|
23264
|
-
const
|
|
24292
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23265
24293
|
function featureTaskStats(featureId) {
|
|
23266
24294
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
23267
24295
|
let total = 0;
|
|
@@ -23270,7 +24298,7 @@ function poBacklogPage(ctx) {
|
|
|
23270
24298
|
for (const epic of fEpics) {
|
|
23271
24299
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
23272
24300
|
total++;
|
|
23273
|
-
if (
|
|
24301
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
23274
24302
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
23275
24303
|
}
|
|
23276
24304
|
}
|
|
@@ -23624,7 +24652,7 @@ function renderWorkItemsTable(items, options) {
|
|
|
23624
24652
|
{ titleTag: "h3", defaultCollapsed }
|
|
23625
24653
|
);
|
|
23626
24654
|
}
|
|
23627
|
-
var
|
|
24655
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
|
|
23628
24656
|
function computeOwnerCompletionPct(items, owner) {
|
|
23629
24657
|
let total = 0;
|
|
23630
24658
|
let progressSum = 0;
|
|
@@ -23632,7 +24660,7 @@ function computeOwnerCompletionPct(items, owner) {
|
|
|
23632
24660
|
for (const w of list) {
|
|
23633
24661
|
if (w.type !== "contribution" && w.owner === owner) {
|
|
23634
24662
|
total++;
|
|
23635
|
-
progressSum += w.progress ?? (
|
|
24663
|
+
progressSum += w.progress ?? (DONE_STATUSES8.has(w.status) ? 100 : 0);
|
|
23636
24664
|
}
|
|
23637
24665
|
if (w.children) walk(w.children);
|
|
23638
24666
|
}
|
|
@@ -23653,7 +24681,7 @@ function filterItemsByOwner(items, owner) {
|
|
|
23653
24681
|
}
|
|
23654
24682
|
|
|
23655
24683
|
// src/web/templates/pages/po/delivery.ts
|
|
23656
|
-
var
|
|
24684
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23657
24685
|
var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
23658
24686
|
var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
23659
24687
|
function priorityClass2(p) {
|
|
@@ -23794,7 +24822,7 @@ function poDeliveryPage(ctx) {
|
|
|
23794
24822
|
}
|
|
23795
24823
|
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
23796
24824
|
}
|
|
23797
|
-
const nonDoneFeatures = features.filter((f) => !
|
|
24825
|
+
const nonDoneFeatures = features.filter((f) => !DONE_STATUSES9.has(f.frontmatter.status)).sort((a, b) => {
|
|
23798
24826
|
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
23799
24827
|
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
23800
24828
|
if (pa !== pb) return pa - pb;
|
|
@@ -24003,7 +25031,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
|
24003
25031
|
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
24004
25032
|
|
|
24005
25033
|
// src/web/templates/pages/dm/dashboard.ts
|
|
24006
|
-
var
|
|
25034
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24007
25035
|
function progressBar2(pct) {
|
|
24008
25036
|
return `<div class="sprint-progress-bar">
|
|
24009
25037
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -24014,7 +25042,7 @@ function dmDashboardPage(ctx) {
|
|
|
24014
25042
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
24015
25043
|
const upcoming = getUpcomingData(ctx.store);
|
|
24016
25044
|
const actions = ctx.store.list({ type: "action" });
|
|
24017
|
-
const openActions = actions.filter((d) => !
|
|
25045
|
+
const openActions = actions.filter((d) => !DONE_STATUSES10.has(d.frontmatter.status));
|
|
24018
25046
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
24019
25047
|
const statsCards = `
|
|
24020
25048
|
<div class="cards">
|
|
@@ -24235,7 +25263,7 @@ function dmSprintPage(ctx) {
|
|
|
24235
25263
|
}
|
|
24236
25264
|
|
|
24237
25265
|
// src/web/templates/pages/dm/actions.ts
|
|
24238
|
-
var
|
|
25266
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24239
25267
|
function urgencyBadge(tier) {
|
|
24240
25268
|
const labels = {
|
|
24241
25269
|
overdue: "Overdue",
|
|
@@ -24255,7 +25283,7 @@ function urgencyRowClass(tier) {
|
|
|
24255
25283
|
function dmActionsPage(ctx) {
|
|
24256
25284
|
const upcoming = getUpcomingData(ctx.store);
|
|
24257
25285
|
const allActions = ctx.store.list({ type: "action" });
|
|
24258
|
-
const openActions = allActions.filter((d) => !
|
|
25286
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
|
|
24259
25287
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
24260
25288
|
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
24261
25289
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
@@ -24340,7 +25368,7 @@ function dmActionsPage(ctx) {
|
|
|
24340
25368
|
}
|
|
24341
25369
|
|
|
24342
25370
|
// src/web/templates/pages/dm/risks.ts
|
|
24343
|
-
var
|
|
25371
|
+
var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24344
25372
|
function dmRisksPage(ctx) {
|
|
24345
25373
|
const allDocs = ctx.store.list();
|
|
24346
25374
|
const upcoming = getUpcomingData(ctx.store);
|
|
@@ -24351,7 +25379,7 @@ function dmRisksPage(ctx) {
|
|
|
24351
25379
|
const todayMs = new Date(today).getTime();
|
|
24352
25380
|
const fourteenDaysMs = 14 * 864e5;
|
|
24353
25381
|
const agingItems = allDocs.filter((d) => {
|
|
24354
|
-
if (
|
|
25382
|
+
if (DONE_STATUSES12.has(d.frontmatter.status)) return false;
|
|
24355
25383
|
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
24356
25384
|
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
24357
25385
|
return todayMs - createdMs > fourteenDaysMs;
|
|
@@ -24465,7 +25493,7 @@ function dmRisksPage(ctx) {
|
|
|
24465
25493
|
}
|
|
24466
25494
|
|
|
24467
25495
|
// src/web/templates/pages/dm/meetings.ts
|
|
24468
|
-
var
|
|
25496
|
+
var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24469
25497
|
function dmMeetingsPage(ctx) {
|
|
24470
25498
|
const meetings = ctx.store.list({ type: "meeting" });
|
|
24471
25499
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -24511,7 +25539,7 @@ function dmMeetingsPage(ctx) {
|
|
|
24511
25539
|
${sortedMeetings.map((m) => {
|
|
24512
25540
|
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
24513
25541
|
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
24514
|
-
const openCount = relatedActions.filter((a) => !
|
|
25542
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES13.has(a.frontmatter.status)).length;
|
|
24515
25543
|
return `
|
|
24516
25544
|
<tr>
|
|
24517
25545
|
<td>${formatDate(date5)}</td>
|
|
@@ -24526,7 +25554,7 @@ function dmMeetingsPage(ctx) {
|
|
|
24526
25554
|
const recentMeetingActions = [];
|
|
24527
25555
|
for (const [mid, acts] of meetingActionMap) {
|
|
24528
25556
|
for (const act of acts) {
|
|
24529
|
-
if (!
|
|
25557
|
+
if (!DONE_STATUSES13.has(act.frontmatter.status)) {
|
|
24530
25558
|
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
24531
25559
|
}
|
|
24532
25560
|
}
|
|
@@ -24721,7 +25749,7 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
|
24721
25749
|
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
24722
25750
|
|
|
24723
25751
|
// src/web/templates/pages/tl/dashboard.ts
|
|
24724
|
-
var
|
|
25752
|
+
var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24725
25753
|
var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
24726
25754
|
function tlDashboardPage(ctx) {
|
|
24727
25755
|
const epics = ctx.store.list({ type: "epic" });
|
|
@@ -24729,8 +25757,8 @@ function tlDashboardPage(ctx) {
|
|
|
24729
25757
|
const decisions = ctx.store.list({ type: "decision" });
|
|
24730
25758
|
const questions = ctx.store.list({ type: "question" });
|
|
24731
25759
|
const diagrams = getDiagramData(ctx.store);
|
|
24732
|
-
const openEpics = epics.filter((d) => !
|
|
24733
|
-
const openTasks = tasks.filter((d) => !
|
|
25760
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
|
|
25761
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES14.has(d.frontmatter.status));
|
|
24734
25762
|
const technicalDecisions = decisions.filter((d) => {
|
|
24735
25763
|
const tags = d.frontmatter.tags ?? [];
|
|
24736
25764
|
return tags.some((t) => {
|
|
@@ -24788,7 +25816,7 @@ function tlDashboardPage(ctx) {
|
|
|
24788
25816
|
}
|
|
24789
25817
|
|
|
24790
25818
|
// src/web/templates/pages/tl/backlog.ts
|
|
24791
|
-
var
|
|
25819
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24792
25820
|
function tlBacklogPage(ctx) {
|
|
24793
25821
|
const epics = ctx.store.list({ type: "epic" });
|
|
24794
25822
|
const tasks = ctx.store.list({ type: "task" });
|
|
@@ -24825,7 +25853,7 @@ function tlBacklogPage(ctx) {
|
|
|
24825
25853
|
<tbody>
|
|
24826
25854
|
${sortedEpics.map((e) => {
|
|
24827
25855
|
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
24828
|
-
const done = eTasks.filter((t) =>
|
|
25856
|
+
const done = eTasks.filter((t) => DONE_STATUSES15.has(t.frontmatter.status)).length;
|
|
24829
25857
|
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
24830
25858
|
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
24831
25859
|
return `
|
|
@@ -24845,7 +25873,7 @@ function tlBacklogPage(ctx) {
|
|
|
24845
25873
|
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
24846
25874
|
}
|
|
24847
25875
|
const unassignedTasks = tasks.filter(
|
|
24848
|
-
(t) => !assignedTaskIds.has(t.frontmatter.id) && !
|
|
25876
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES15.has(t.frontmatter.status)
|
|
24849
25877
|
);
|
|
24850
25878
|
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
24851
25879
|
"tl-backlog-unassigned",
|