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.js
CHANGED
|
@@ -22333,7 +22333,7 @@ function poBacklogPage(ctx) {
|
|
|
22333
22333
|
}
|
|
22334
22334
|
}
|
|
22335
22335
|
}
|
|
22336
|
-
const
|
|
22336
|
+
const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
22337
22337
|
function featureTaskStats(featureId) {
|
|
22338
22338
|
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
22339
22339
|
let total = 0;
|
|
@@ -22342,7 +22342,7 @@ function poBacklogPage(ctx) {
|
|
|
22342
22342
|
for (const epic of fEpics) {
|
|
22343
22343
|
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
22344
22344
|
total++;
|
|
22345
|
-
if (
|
|
22345
|
+
if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
|
|
22346
22346
|
progressSum += getEffectiveProgress(t.frontmatter);
|
|
22347
22347
|
}
|
|
22348
22348
|
}
|
|
@@ -25141,13 +25141,23 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
|
25141
25141
|
// src/skills/builtin/jira/client.ts
|
|
25142
25142
|
var JiraClient = class {
|
|
25143
25143
|
baseUrl;
|
|
25144
|
+
baseUrlV3;
|
|
25144
25145
|
authHeader;
|
|
25145
25146
|
constructor(config2) {
|
|
25146
|
-
|
|
25147
|
+
const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
25148
|
+
this.baseUrl = `https://${host}/rest/api/2`;
|
|
25149
|
+
this.baseUrlV3 = `https://${host}/rest/api/3`;
|
|
25147
25150
|
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
25148
25151
|
}
|
|
25149
25152
|
async request(path21, method = "GET", body) {
|
|
25150
25153
|
const url2 = `${this.baseUrl}${path21}`;
|
|
25154
|
+
return this.doRequest(url2, method, body);
|
|
25155
|
+
}
|
|
25156
|
+
async requestV3(path21, method = "GET", body) {
|
|
25157
|
+
const url2 = `${this.baseUrlV3}${path21}`;
|
|
25158
|
+
return this.doRequest(url2, method, body);
|
|
25159
|
+
}
|
|
25160
|
+
async doRequest(url2, method, body) {
|
|
25151
25161
|
const headers = {
|
|
25152
25162
|
Authorization: this.authHeader,
|
|
25153
25163
|
"Content-Type": "application/json",
|
|
@@ -25161,7 +25171,7 @@ var JiraClient = class {
|
|
|
25161
25171
|
if (!response.ok) {
|
|
25162
25172
|
const text = await response.text().catch(() => "");
|
|
25163
25173
|
throw new Error(
|
|
25164
|
-
`Jira API error ${response.status} ${method} ${
|
|
25174
|
+
`Jira API error ${response.status} ${method} ${url2}: ${text}`
|
|
25165
25175
|
);
|
|
25166
25176
|
}
|
|
25167
25177
|
if (response.status === 204) return void 0;
|
|
@@ -25174,6 +25184,14 @@ var JiraClient = class {
|
|
|
25174
25184
|
});
|
|
25175
25185
|
return this.request(`/search?${params}`);
|
|
25176
25186
|
}
|
|
25187
|
+
async searchIssuesV3(jql, fields = ["summary", "status", "issuetype", "priority", "assignee", "labels"], maxResults = 50) {
|
|
25188
|
+
const params = new URLSearchParams({
|
|
25189
|
+
jql,
|
|
25190
|
+
maxResults: String(maxResults),
|
|
25191
|
+
fields: fields.join(",")
|
|
25192
|
+
});
|
|
25193
|
+
return this.requestV3(`/search/jql?${params}`);
|
|
25194
|
+
}
|
|
25177
25195
|
async getIssue(key) {
|
|
25178
25196
|
return this.request(`/issue/${encodeURIComponent(key)}`);
|
|
25179
25197
|
}
|
|
@@ -25187,6 +25205,28 @@ var JiraClient = class {
|
|
|
25187
25205
|
{ fields }
|
|
25188
25206
|
);
|
|
25189
25207
|
}
|
|
25208
|
+
async getIssueWithLinks(key) {
|
|
25209
|
+
return this.request(
|
|
25210
|
+
`/issue/${encodeURIComponent(key)}?fields=summary,status,issuetype,priority,assignee,labels,subtasks,issuelinks`
|
|
25211
|
+
);
|
|
25212
|
+
}
|
|
25213
|
+
async getChangelog(key) {
|
|
25214
|
+
const result = await this.request(
|
|
25215
|
+
`/issue/${encodeURIComponent(key)}/changelog?maxResults=100`
|
|
25216
|
+
);
|
|
25217
|
+
return result.values;
|
|
25218
|
+
}
|
|
25219
|
+
async getComments(key) {
|
|
25220
|
+
const result = await this.request(
|
|
25221
|
+
`/issue/${encodeURIComponent(key)}/comment?maxResults=100`
|
|
25222
|
+
);
|
|
25223
|
+
return result.comments;
|
|
25224
|
+
}
|
|
25225
|
+
async getRemoteLinks(key) {
|
|
25226
|
+
return this.request(
|
|
25227
|
+
`/issue/${encodeURIComponent(key)}/remotelink`
|
|
25228
|
+
);
|
|
25229
|
+
}
|
|
25190
25230
|
async addComment(key, body) {
|
|
25191
25231
|
await this.request(
|
|
25192
25232
|
`/issue/${encodeURIComponent(key)}/comment`,
|
|
@@ -25200,7 +25240,651 @@ function createJiraClient(jiraUserConfig) {
|
|
|
25200
25240
|
const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
|
|
25201
25241
|
const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
|
|
25202
25242
|
if (!host || !email3 || !apiToken) return null;
|
|
25203
|
-
|
|
25243
|
+
const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
25244
|
+
return { client: new JiraClient({ host, email: email3, apiToken }), host: normalizedHost };
|
|
25245
|
+
}
|
|
25246
|
+
|
|
25247
|
+
// src/skills/builtin/jira/sync.ts
|
|
25248
|
+
var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
25249
|
+
var DEFAULT_ACTION_STATUS_MAP = {
|
|
25250
|
+
done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
|
|
25251
|
+
"in-progress": ["In Progress", "In Review", "Reviewing", "Testing"],
|
|
25252
|
+
blocked: ["Blocked"],
|
|
25253
|
+
open: ["To Do", "Open", "Backlog", "New"]
|
|
25254
|
+
};
|
|
25255
|
+
var DEFAULT_TASK_STATUS_MAP = {
|
|
25256
|
+
done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
|
|
25257
|
+
review: ["In Review", "Code Review", "Reviewing", "Testing"],
|
|
25258
|
+
"in-progress": ["In Progress"],
|
|
25259
|
+
ready: ["Ready", "Selected for Development"],
|
|
25260
|
+
blocked: ["Blocked"],
|
|
25261
|
+
backlog: ["To Do", "Open", "Backlog", "New"]
|
|
25262
|
+
};
|
|
25263
|
+
function buildStatusLookup(configMap, defaults) {
|
|
25264
|
+
const map2 = configMap ?? defaults;
|
|
25265
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
25266
|
+
for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
|
|
25267
|
+
for (const js of jiraStatuses) {
|
|
25268
|
+
lookup.set(js.toLowerCase(), marvinStatus);
|
|
25269
|
+
}
|
|
25270
|
+
}
|
|
25271
|
+
return lookup;
|
|
25272
|
+
}
|
|
25273
|
+
function mapJiraStatusForAction(status, configMap) {
|
|
25274
|
+
const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
|
|
25275
|
+
return lookup.get(status.toLowerCase()) ?? "open";
|
|
25276
|
+
}
|
|
25277
|
+
function mapJiraStatusForTask(status, configMap) {
|
|
25278
|
+
const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
|
|
25279
|
+
return lookup.get(status.toLowerCase()) ?? "backlog";
|
|
25280
|
+
}
|
|
25281
|
+
function computeSubtaskProgress(subtasks) {
|
|
25282
|
+
if (subtasks.length === 0) return 0;
|
|
25283
|
+
const done = subtasks.filter(
|
|
25284
|
+
(s) => DONE_STATUSES14.has(s.fields.status.name.toLowerCase())
|
|
25285
|
+
).length;
|
|
25286
|
+
return Math.round(done / subtasks.length * 100);
|
|
25287
|
+
}
|
|
25288
|
+
async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
25289
|
+
const result = { artifacts: [], errors: [] };
|
|
25290
|
+
const actions = store.list({ type: "action" });
|
|
25291
|
+
const tasks = store.list({ type: "task" });
|
|
25292
|
+
let candidates = [...actions, ...tasks].filter(
|
|
25293
|
+
(d) => d.frontmatter.jiraKey
|
|
25294
|
+
);
|
|
25295
|
+
if (artifactId) {
|
|
25296
|
+
candidates = candidates.filter((d) => d.frontmatter.id === artifactId);
|
|
25297
|
+
if (candidates.length === 0) {
|
|
25298
|
+
const doc = store.get(artifactId);
|
|
25299
|
+
if (doc) {
|
|
25300
|
+
result.errors.push(
|
|
25301
|
+
`${artifactId} has no jiraKey \u2014 use push_artifact_to_jira or link_to_jira first`
|
|
25302
|
+
);
|
|
25303
|
+
} else {
|
|
25304
|
+
result.errors.push(`Artifact ${artifactId} not found`);
|
|
25305
|
+
}
|
|
25306
|
+
return result;
|
|
25307
|
+
}
|
|
25308
|
+
}
|
|
25309
|
+
candidates = candidates.filter(
|
|
25310
|
+
(d) => !DONE_STATUSES14.has(d.frontmatter.status)
|
|
25311
|
+
);
|
|
25312
|
+
for (const doc of candidates) {
|
|
25313
|
+
const jiraKey = doc.frontmatter.jiraKey;
|
|
25314
|
+
const artifactType = doc.frontmatter.type;
|
|
25315
|
+
try {
|
|
25316
|
+
const issue2 = await client.getIssueWithLinks(jiraKey);
|
|
25317
|
+
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
|
|
25318
|
+
const currentStatus = doc.frontmatter.status;
|
|
25319
|
+
const linkedIssues = [];
|
|
25320
|
+
if (issue2.fields.subtasks) {
|
|
25321
|
+
for (const sub of issue2.fields.subtasks) {
|
|
25322
|
+
linkedIssues.push({
|
|
25323
|
+
key: sub.key,
|
|
25324
|
+
summary: sub.fields.summary,
|
|
25325
|
+
status: sub.fields.status.name,
|
|
25326
|
+
relationship: "subtask",
|
|
25327
|
+
isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
|
|
25328
|
+
});
|
|
25329
|
+
}
|
|
25330
|
+
}
|
|
25331
|
+
if (issue2.fields.issuelinks) {
|
|
25332
|
+
for (const link of issue2.fields.issuelinks) {
|
|
25333
|
+
if (link.outwardIssue) {
|
|
25334
|
+
linkedIssues.push({
|
|
25335
|
+
key: link.outwardIssue.key,
|
|
25336
|
+
summary: link.outwardIssue.fields.summary,
|
|
25337
|
+
status: link.outwardIssue.fields.status.name,
|
|
25338
|
+
relationship: link.type.outward,
|
|
25339
|
+
isDone: DONE_STATUSES14.has(
|
|
25340
|
+
link.outwardIssue.fields.status.name.toLowerCase()
|
|
25341
|
+
)
|
|
25342
|
+
});
|
|
25343
|
+
}
|
|
25344
|
+
if (link.inwardIssue) {
|
|
25345
|
+
linkedIssues.push({
|
|
25346
|
+
key: link.inwardIssue.key,
|
|
25347
|
+
summary: link.inwardIssue.fields.summary,
|
|
25348
|
+
status: link.inwardIssue.fields.status.name,
|
|
25349
|
+
relationship: link.type.inward,
|
|
25350
|
+
isDone: DONE_STATUSES14.has(
|
|
25351
|
+
link.inwardIssue.fields.status.name.toLowerCase()
|
|
25352
|
+
)
|
|
25353
|
+
});
|
|
25354
|
+
}
|
|
25355
|
+
}
|
|
25356
|
+
}
|
|
25357
|
+
const subtasks = issue2.fields.subtasks ?? [];
|
|
25358
|
+
let proposedProgress;
|
|
25359
|
+
if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
|
|
25360
|
+
proposedProgress = computeSubtaskProgress(subtasks);
|
|
25361
|
+
}
|
|
25362
|
+
const currentProgress = doc.frontmatter.progress;
|
|
25363
|
+
result.artifacts.push({
|
|
25364
|
+
id: doc.frontmatter.id,
|
|
25365
|
+
type: artifactType,
|
|
25366
|
+
jiraKey,
|
|
25367
|
+
jiraUrl: `https://${host}/browse/${jiraKey}`,
|
|
25368
|
+
jiraSummary: issue2.fields.summary,
|
|
25369
|
+
jiraStatus: issue2.fields.status.name,
|
|
25370
|
+
currentMarvinStatus: currentStatus,
|
|
25371
|
+
proposedMarvinStatus: proposedStatus,
|
|
25372
|
+
statusChanged: currentStatus !== proposedStatus,
|
|
25373
|
+
currentProgress,
|
|
25374
|
+
proposedProgress,
|
|
25375
|
+
progressChanged: proposedProgress !== void 0 && proposedProgress !== currentProgress,
|
|
25376
|
+
linkedIssues
|
|
25377
|
+
});
|
|
25378
|
+
} catch (err) {
|
|
25379
|
+
result.errors.push(
|
|
25380
|
+
`${doc.frontmatter.id} (${jiraKey}): ${err instanceof Error ? err.message : String(err)}`
|
|
25381
|
+
);
|
|
25382
|
+
}
|
|
25383
|
+
}
|
|
25384
|
+
return result;
|
|
25385
|
+
}
|
|
25386
|
+
async function syncJiraProgress(store, client, host, artifactId, statusMap) {
|
|
25387
|
+
const fetchResult = await fetchJiraStatus(store, client, host, artifactId, statusMap);
|
|
25388
|
+
const result = {
|
|
25389
|
+
updated: [],
|
|
25390
|
+
unchanged: 0,
|
|
25391
|
+
errors: [...fetchResult.errors]
|
|
25392
|
+
};
|
|
25393
|
+
for (const artifact of fetchResult.artifacts) {
|
|
25394
|
+
const hasChanges = artifact.statusChanged || artifact.progressChanged || artifact.linkedIssues.length > 0;
|
|
25395
|
+
if (hasChanges) {
|
|
25396
|
+
const updates = {
|
|
25397
|
+
status: artifact.proposedMarvinStatus,
|
|
25398
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25399
|
+
jiraLinkedIssues: artifact.linkedIssues
|
|
25400
|
+
};
|
|
25401
|
+
if (artifact.proposedProgress !== void 0) {
|
|
25402
|
+
updates.progress = artifact.proposedProgress;
|
|
25403
|
+
}
|
|
25404
|
+
store.update(artifact.id, updates);
|
|
25405
|
+
if (artifact.type === "task") {
|
|
25406
|
+
propagateProgressFromTask(store, artifact.id);
|
|
25407
|
+
} else if (artifact.type === "action") {
|
|
25408
|
+
propagateProgressToAction(store, artifact.id);
|
|
25409
|
+
}
|
|
25410
|
+
result.updated.push({
|
|
25411
|
+
id: artifact.id,
|
|
25412
|
+
jiraKey: artifact.jiraKey,
|
|
25413
|
+
oldStatus: artifact.currentMarvinStatus,
|
|
25414
|
+
newStatus: artifact.proposedMarvinStatus,
|
|
25415
|
+
linkedIssues: artifact.linkedIssues
|
|
25416
|
+
});
|
|
25417
|
+
} else {
|
|
25418
|
+
store.update(artifact.id, {
|
|
25419
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25420
|
+
});
|
|
25421
|
+
result.unchanged++;
|
|
25422
|
+
}
|
|
25423
|
+
}
|
|
25424
|
+
return result;
|
|
25425
|
+
}
|
|
25426
|
+
|
|
25427
|
+
// src/skills/builtin/jira/daily.ts
|
|
25428
|
+
var BLOCKER_PATTERNS = [
|
|
25429
|
+
/\bblocked\b/i,
|
|
25430
|
+
/\bblocking\b/i,
|
|
25431
|
+
/\bwaiting\s+for\b/i,
|
|
25432
|
+
/\bon\s+hold\b/i,
|
|
25433
|
+
/\bcan'?t\s+proceed\b/i,
|
|
25434
|
+
/\bdepends?\s+on\b/i,
|
|
25435
|
+
/\bstuck\b/i,
|
|
25436
|
+
/\bneed[s]?\s+(to\s+wait|approval|input|clarification)\b/i
|
|
25437
|
+
];
|
|
25438
|
+
var DECISION_PATTERNS = [
|
|
25439
|
+
/\bdecided\b/i,
|
|
25440
|
+
/\bagreed\b/i,
|
|
25441
|
+
/\bapproved?\b/i,
|
|
25442
|
+
/\blet'?s?\s+go\s+with\b/i,
|
|
25443
|
+
/\bwe('ll|\s+will)\s+(use|go|proceed|adopt)\b/i,
|
|
25444
|
+
/\bsigned\s+off\b/i,
|
|
25445
|
+
/\bconfirmed\b/i
|
|
25446
|
+
];
|
|
25447
|
+
var QUESTION_PATTERNS = [
|
|
25448
|
+
/\?/,
|
|
25449
|
+
/\bdoes\s+anyone\s+know\b/i,
|
|
25450
|
+
/\bhow\s+should\s+we\b/i,
|
|
25451
|
+
/\bneed\s+clarification\b/i,
|
|
25452
|
+
/\bwhat('s|\s+is)\s+the\s+(plan|approach|status)\b/i,
|
|
25453
|
+
/\bshould\s+we\b/i,
|
|
25454
|
+
/\bany\s+(idea|thought|suggestion)s?\b/i,
|
|
25455
|
+
/\bopen\s+question\b/i
|
|
25456
|
+
];
|
|
25457
|
+
var RESOLUTION_PATTERNS = [
|
|
25458
|
+
/\bfixed\b/i,
|
|
25459
|
+
/\bresolved\b/i,
|
|
25460
|
+
/\bmerged\b/i,
|
|
25461
|
+
/\bdeployed\b/i,
|
|
25462
|
+
/\bcompleted?\b/i,
|
|
25463
|
+
/\bshipped\b/i,
|
|
25464
|
+
/\bimplemented\b/i,
|
|
25465
|
+
/\bclosed\b/i
|
|
25466
|
+
];
|
|
25467
|
+
function detectCommentSignals(text) {
|
|
25468
|
+
const signals = [];
|
|
25469
|
+
const lines = text.split("\n");
|
|
25470
|
+
for (const line of lines) {
|
|
25471
|
+
const trimmed = line.trim();
|
|
25472
|
+
if (!trimmed) continue;
|
|
25473
|
+
for (const pattern of BLOCKER_PATTERNS) {
|
|
25474
|
+
if (pattern.test(trimmed)) {
|
|
25475
|
+
signals.push({ type: "blocker", snippet: truncate(trimmed, 120) });
|
|
25476
|
+
break;
|
|
25477
|
+
}
|
|
25478
|
+
}
|
|
25479
|
+
for (const pattern of DECISION_PATTERNS) {
|
|
25480
|
+
if (pattern.test(trimmed)) {
|
|
25481
|
+
signals.push({ type: "decision", snippet: truncate(trimmed, 120) });
|
|
25482
|
+
break;
|
|
25483
|
+
}
|
|
25484
|
+
}
|
|
25485
|
+
for (const pattern of QUESTION_PATTERNS) {
|
|
25486
|
+
if (pattern.test(trimmed)) {
|
|
25487
|
+
signals.push({ type: "question", snippet: truncate(trimmed, 120) });
|
|
25488
|
+
break;
|
|
25489
|
+
}
|
|
25490
|
+
}
|
|
25491
|
+
for (const pattern of RESOLUTION_PATTERNS) {
|
|
25492
|
+
if (pattern.test(trimmed)) {
|
|
25493
|
+
signals.push({ type: "resolution", snippet: truncate(trimmed, 120) });
|
|
25494
|
+
break;
|
|
25495
|
+
}
|
|
25496
|
+
}
|
|
25497
|
+
}
|
|
25498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25499
|
+
return signals.filter((s) => {
|
|
25500
|
+
if (seen.has(s.type)) return false;
|
|
25501
|
+
seen.add(s.type);
|
|
25502
|
+
return true;
|
|
25503
|
+
});
|
|
25504
|
+
}
|
|
25505
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
25506
|
+
"a",
|
|
25507
|
+
"an",
|
|
25508
|
+
"the",
|
|
25509
|
+
"and",
|
|
25510
|
+
"or",
|
|
25511
|
+
"but",
|
|
25512
|
+
"in",
|
|
25513
|
+
"on",
|
|
25514
|
+
"at",
|
|
25515
|
+
"to",
|
|
25516
|
+
"for",
|
|
25517
|
+
"of",
|
|
25518
|
+
"with",
|
|
25519
|
+
"by",
|
|
25520
|
+
"from",
|
|
25521
|
+
"is",
|
|
25522
|
+
"are",
|
|
25523
|
+
"was",
|
|
25524
|
+
"were",
|
|
25525
|
+
"be",
|
|
25526
|
+
"been",
|
|
25527
|
+
"this",
|
|
25528
|
+
"that",
|
|
25529
|
+
"it",
|
|
25530
|
+
"its",
|
|
25531
|
+
"as",
|
|
25532
|
+
"not",
|
|
25533
|
+
"no",
|
|
25534
|
+
"if",
|
|
25535
|
+
"do",
|
|
25536
|
+
"does",
|
|
25537
|
+
"new",
|
|
25538
|
+
"via",
|
|
25539
|
+
"use",
|
|
25540
|
+
"using",
|
|
25541
|
+
"based",
|
|
25542
|
+
"into",
|
|
25543
|
+
"e.g",
|
|
25544
|
+
"etc"
|
|
25545
|
+
]);
|
|
25546
|
+
function tokenize(text) {
|
|
25547
|
+
return new Set(
|
|
25548
|
+
text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
|
|
25549
|
+
);
|
|
25550
|
+
}
|
|
25551
|
+
function computeTitleSimilarity(jiraSummary, artifactTitle) {
|
|
25552
|
+
const jiraTokens = tokenize(jiraSummary);
|
|
25553
|
+
const artifactTokens = tokenize(artifactTitle);
|
|
25554
|
+
if (jiraTokens.size === 0 || artifactTokens.size === 0) {
|
|
25555
|
+
return { score: 0, sharedTerms: [] };
|
|
25556
|
+
}
|
|
25557
|
+
const shared = [];
|
|
25558
|
+
for (const token of jiraTokens) {
|
|
25559
|
+
if (artifactTokens.has(token)) {
|
|
25560
|
+
shared.push(token);
|
|
25561
|
+
}
|
|
25562
|
+
}
|
|
25563
|
+
const union2 = /* @__PURE__ */ new Set([...jiraTokens, ...artifactTokens]);
|
|
25564
|
+
const score = shared.length / union2.size;
|
|
25565
|
+
return { score, sharedTerms: shared };
|
|
25566
|
+
}
|
|
25567
|
+
var LINK_SUGGESTION_THRESHOLD = 0.15;
|
|
25568
|
+
var MAX_LINK_SUGGESTIONS = 3;
|
|
25569
|
+
function findLinkSuggestions(jiraSummary, allDocs) {
|
|
25570
|
+
const suggestions = [];
|
|
25571
|
+
for (const doc of allDocs) {
|
|
25572
|
+
const fm = doc.frontmatter;
|
|
25573
|
+
if (fm.jiraKey) continue;
|
|
25574
|
+
const { score, sharedTerms } = computeTitleSimilarity(
|
|
25575
|
+
jiraSummary,
|
|
25576
|
+
fm.title
|
|
25577
|
+
);
|
|
25578
|
+
if (score >= LINK_SUGGESTION_THRESHOLD && sharedTerms.length >= 2) {
|
|
25579
|
+
suggestions.push({
|
|
25580
|
+
artifactId: fm.id,
|
|
25581
|
+
artifactType: fm.type,
|
|
25582
|
+
artifactTitle: fm.title,
|
|
25583
|
+
score,
|
|
25584
|
+
sharedTerms
|
|
25585
|
+
});
|
|
25586
|
+
}
|
|
25587
|
+
}
|
|
25588
|
+
return suggestions.sort((a, b) => b.score - a.score).slice(0, MAX_LINK_SUGGESTIONS);
|
|
25589
|
+
}
|
|
25590
|
+
function extractCommentText(body) {
|
|
25591
|
+
if (typeof body === "string") return body;
|
|
25592
|
+
if (!body || typeof body !== "object") return "";
|
|
25593
|
+
const parts = [];
|
|
25594
|
+
function walk(node) {
|
|
25595
|
+
if (!node || typeof node !== "object") return;
|
|
25596
|
+
const n = node;
|
|
25597
|
+
if (n.type === "text" && typeof n.text === "string") {
|
|
25598
|
+
parts.push(n.text);
|
|
25599
|
+
}
|
|
25600
|
+
if (Array.isArray(n.content)) {
|
|
25601
|
+
for (const child of n.content) walk(child);
|
|
25602
|
+
}
|
|
25603
|
+
}
|
|
25604
|
+
walk(body);
|
|
25605
|
+
return parts.join(" ");
|
|
25606
|
+
}
|
|
25607
|
+
function truncate(text, maxLen = 200) {
|
|
25608
|
+
if (text.length <= maxLen) return text;
|
|
25609
|
+
return text.slice(0, maxLen) + "\u2026";
|
|
25610
|
+
}
|
|
25611
|
+
function isWithinRange(timestamp, range) {
|
|
25612
|
+
const date5 = timestamp.slice(0, 10);
|
|
25613
|
+
return date5 >= range.from && date5 <= range.to;
|
|
25614
|
+
}
|
|
25615
|
+
function isConfluenceUrl(url2) {
|
|
25616
|
+
return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
|
|
25617
|
+
}
|
|
25618
|
+
var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
|
|
25619
|
+
async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
|
|
25620
|
+
const summary = {
|
|
25621
|
+
dateRange,
|
|
25622
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25623
|
+
projectKey,
|
|
25624
|
+
issues: [],
|
|
25625
|
+
proposedActions: [],
|
|
25626
|
+
errors: []
|
|
25627
|
+
};
|
|
25628
|
+
const jql = `project = ${projectKey} AND updated >= "${dateRange.from}" AND updated <= "${dateRange.to} 23:59" ORDER BY updated DESC`;
|
|
25629
|
+
let searchResult;
|
|
25630
|
+
try {
|
|
25631
|
+
searchResult = await client.searchIssuesV3(
|
|
25632
|
+
jql,
|
|
25633
|
+
["summary", "status", "issuetype", "priority", "assignee", "labels"],
|
|
25634
|
+
100
|
|
25635
|
+
);
|
|
25636
|
+
} catch (err) {
|
|
25637
|
+
summary.errors.push(
|
|
25638
|
+
`Search failed: ${err instanceof Error ? err.message : String(err)}`
|
|
25639
|
+
);
|
|
25640
|
+
return summary;
|
|
25641
|
+
}
|
|
25642
|
+
const allDocs = [
|
|
25643
|
+
...store.list({ type: "action" }),
|
|
25644
|
+
...store.list({ type: "task" }),
|
|
25645
|
+
...store.list({ type: "decision" }),
|
|
25646
|
+
...store.list({ type: "question" })
|
|
25647
|
+
];
|
|
25648
|
+
const otherTypes = store.registeredTypes.filter(
|
|
25649
|
+
(t) => !["action", "task", "decision", "question"].includes(t)
|
|
25650
|
+
);
|
|
25651
|
+
for (const t of otherTypes) {
|
|
25652
|
+
allDocs.push(...store.list({ type: t }));
|
|
25653
|
+
}
|
|
25654
|
+
const jiraKeyToArtifacts = /* @__PURE__ */ new Map();
|
|
25655
|
+
for (const doc of allDocs) {
|
|
25656
|
+
const jk = doc.frontmatter.jiraKey;
|
|
25657
|
+
if (jk) {
|
|
25658
|
+
const list = jiraKeyToArtifacts.get(jk) ?? [];
|
|
25659
|
+
list.push(doc);
|
|
25660
|
+
jiraKeyToArtifacts.set(jk, list);
|
|
25661
|
+
}
|
|
25662
|
+
}
|
|
25663
|
+
const BATCH_SIZE = 5;
|
|
25664
|
+
const issues = searchResult.issues;
|
|
25665
|
+
for (let i = 0; i < issues.length; i += BATCH_SIZE) {
|
|
25666
|
+
const batch = issues.slice(i, i + BATCH_SIZE);
|
|
25667
|
+
const results = await Promise.allSettled(
|
|
25668
|
+
batch.map(
|
|
25669
|
+
(issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
|
|
25670
|
+
)
|
|
25671
|
+
);
|
|
25672
|
+
for (let j = 0; j < results.length; j++) {
|
|
25673
|
+
const r = results[j];
|
|
25674
|
+
if (r.status === "fulfilled") {
|
|
25675
|
+
summary.issues.push(r.value);
|
|
25676
|
+
} else {
|
|
25677
|
+
summary.errors.push(
|
|
25678
|
+
`${batch[j].key}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
|
|
25679
|
+
);
|
|
25680
|
+
}
|
|
25681
|
+
}
|
|
25682
|
+
}
|
|
25683
|
+
summary.proposedActions = generateProposedActions(summary.issues);
|
|
25684
|
+
return summary;
|
|
25685
|
+
}
|
|
25686
|
+
async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
|
|
25687
|
+
const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
|
|
25688
|
+
client.getChangelog(issue2.key).catch(() => []),
|
|
25689
|
+
client.getComments(issue2.key).catch(() => []),
|
|
25690
|
+
client.getRemoteLinks(issue2.key).catch(() => []),
|
|
25691
|
+
client.getIssueWithLinks(issue2.key).catch(() => null)
|
|
25692
|
+
]);
|
|
25693
|
+
const changes = [];
|
|
25694
|
+
for (const entry of changelogResult) {
|
|
25695
|
+
if (!isWithinRange(entry.created, dateRange)) continue;
|
|
25696
|
+
for (const item of entry.items) {
|
|
25697
|
+
changes.push({
|
|
25698
|
+
field: item.field,
|
|
25699
|
+
from: item.fromString,
|
|
25700
|
+
to: item.toString,
|
|
25701
|
+
author: entry.author.displayName,
|
|
25702
|
+
timestamp: entry.created
|
|
25703
|
+
});
|
|
25704
|
+
}
|
|
25705
|
+
}
|
|
25706
|
+
const comments = [];
|
|
25707
|
+
for (const comment of commentsResult) {
|
|
25708
|
+
if (!isWithinRange(comment.created, dateRange) && !isWithinRange(comment.updated, dateRange)) {
|
|
25709
|
+
continue;
|
|
25710
|
+
}
|
|
25711
|
+
const fullText = extractCommentText(comment.body);
|
|
25712
|
+
const signals = detectCommentSignals(fullText);
|
|
25713
|
+
comments.push({
|
|
25714
|
+
author: comment.author.displayName,
|
|
25715
|
+
created: comment.created,
|
|
25716
|
+
bodyPreview: truncate(fullText),
|
|
25717
|
+
signals
|
|
25718
|
+
});
|
|
25719
|
+
}
|
|
25720
|
+
const confluenceLinks = [];
|
|
25721
|
+
for (const rl of remoteLinksResult) {
|
|
25722
|
+
if (isConfluenceUrl(rl.object.url)) {
|
|
25723
|
+
confluenceLinks.push({
|
|
25724
|
+
url: rl.object.url,
|
|
25725
|
+
title: rl.object.title
|
|
25726
|
+
});
|
|
25727
|
+
}
|
|
25728
|
+
}
|
|
25729
|
+
const linkedIssues = [];
|
|
25730
|
+
if (issueWithLinks) {
|
|
25731
|
+
if (issueWithLinks.fields.subtasks) {
|
|
25732
|
+
for (const sub of issueWithLinks.fields.subtasks) {
|
|
25733
|
+
linkedIssues.push({
|
|
25734
|
+
key: sub.key,
|
|
25735
|
+
summary: sub.fields.summary,
|
|
25736
|
+
status: sub.fields.status.name,
|
|
25737
|
+
relationship: "subtask",
|
|
25738
|
+
isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
|
|
25739
|
+
});
|
|
25740
|
+
}
|
|
25741
|
+
}
|
|
25742
|
+
if (issueWithLinks.fields.issuelinks) {
|
|
25743
|
+
for (const link of issueWithLinks.fields.issuelinks) {
|
|
25744
|
+
if (link.outwardIssue) {
|
|
25745
|
+
linkedIssues.push({
|
|
25746
|
+
key: link.outwardIssue.key,
|
|
25747
|
+
summary: link.outwardIssue.fields.summary,
|
|
25748
|
+
status: link.outwardIssue.fields.status.name,
|
|
25749
|
+
relationship: link.type.outward,
|
|
25750
|
+
isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
|
|
25751
|
+
});
|
|
25752
|
+
}
|
|
25753
|
+
if (link.inwardIssue) {
|
|
25754
|
+
linkedIssues.push({
|
|
25755
|
+
key: link.inwardIssue.key,
|
|
25756
|
+
summary: link.inwardIssue.fields.summary,
|
|
25757
|
+
status: link.inwardIssue.fields.status.name,
|
|
25758
|
+
relationship: link.type.inward,
|
|
25759
|
+
isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
|
|
25760
|
+
});
|
|
25761
|
+
}
|
|
25762
|
+
}
|
|
25763
|
+
}
|
|
25764
|
+
}
|
|
25765
|
+
const marvinArtifacts = [];
|
|
25766
|
+
const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
|
|
25767
|
+
for (const doc of artifacts) {
|
|
25768
|
+
const fm = doc.frontmatter;
|
|
25769
|
+
const artifactType = fm.type;
|
|
25770
|
+
let proposedStatus = null;
|
|
25771
|
+
if (artifactType === "action" || artifactType === "task") {
|
|
25772
|
+
const jiraStatus = issue2.fields.status?.name;
|
|
25773
|
+
if (jiraStatus) {
|
|
25774
|
+
proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
|
|
25775
|
+
}
|
|
25776
|
+
}
|
|
25777
|
+
marvinArtifacts.push({
|
|
25778
|
+
id: fm.id,
|
|
25779
|
+
type: artifactType,
|
|
25780
|
+
title: fm.title,
|
|
25781
|
+
currentStatus: fm.status,
|
|
25782
|
+
proposedStatus,
|
|
25783
|
+
statusDrift: proposedStatus !== null && proposedStatus !== fm.status
|
|
25784
|
+
});
|
|
25785
|
+
}
|
|
25786
|
+
const linkSuggestions = marvinArtifacts.length === 0 ? findLinkSuggestions(issue2.fields.summary, allDocs) : [];
|
|
25787
|
+
return {
|
|
25788
|
+
key: issue2.key,
|
|
25789
|
+
summary: issue2.fields.summary,
|
|
25790
|
+
currentStatus: issue2.fields.status?.name ?? "Unknown",
|
|
25791
|
+
issueType: issue2.fields.issuetype?.name ?? "Unknown",
|
|
25792
|
+
assignee: issue2.fields.assignee?.displayName ?? null,
|
|
25793
|
+
changes,
|
|
25794
|
+
comments,
|
|
25795
|
+
linkedIssues,
|
|
25796
|
+
confluenceLinks,
|
|
25797
|
+
marvinArtifacts,
|
|
25798
|
+
linkSuggestions
|
|
25799
|
+
};
|
|
25800
|
+
}
|
|
25801
|
+
function generateProposedActions(issues) {
|
|
25802
|
+
const actions = [];
|
|
25803
|
+
for (const issue2 of issues) {
|
|
25804
|
+
for (const artifact of issue2.marvinArtifacts) {
|
|
25805
|
+
if (artifact.statusDrift && artifact.proposedStatus) {
|
|
25806
|
+
actions.push({
|
|
25807
|
+
type: "status-update",
|
|
25808
|
+
description: `Update ${artifact.id} (${artifact.type}) status: ${artifact.currentStatus} \u2192 ${artifact.proposedStatus} (Jira ${issue2.key} is "${issue2.currentStatus}")`,
|
|
25809
|
+
artifactId: artifact.id,
|
|
25810
|
+
jiraKey: issue2.key
|
|
25811
|
+
});
|
|
25812
|
+
}
|
|
25813
|
+
}
|
|
25814
|
+
if (issue2.marvinArtifacts.length === 0 && (issue2.changes.length > 0 || issue2.comments.length > 0)) {
|
|
25815
|
+
actions.push({
|
|
25816
|
+
type: "unlinked-issue",
|
|
25817
|
+
description: `${issue2.key} ("${issue2.summary}") has activity but no Marvin artifact \u2014 consider linking or creating one`,
|
|
25818
|
+
jiraKey: issue2.key
|
|
25819
|
+
});
|
|
25820
|
+
}
|
|
25821
|
+
for (const suggestion of issue2.linkSuggestions) {
|
|
25822
|
+
actions.push({
|
|
25823
|
+
type: "link-suggestion",
|
|
25824
|
+
description: `${issue2.key} ("${issue2.summary}") may match ${suggestion.artifactId} ("${suggestion.artifactTitle}") \u2014 shared terms: ${suggestion.sharedTerms.join(", ")} (${Math.round(suggestion.score * 100)}% similarity)`,
|
|
25825
|
+
artifactId: suggestion.artifactId,
|
|
25826
|
+
jiraKey: issue2.key
|
|
25827
|
+
});
|
|
25828
|
+
}
|
|
25829
|
+
for (const comment of issue2.comments) {
|
|
25830
|
+
for (const signal of comment.signals) {
|
|
25831
|
+
if (signal.type === "blocker") {
|
|
25832
|
+
actions.push({
|
|
25833
|
+
type: "blocker-detected",
|
|
25834
|
+
description: `Blocker in ${issue2.key} comment by ${comment.author}: "${signal.snippet}"`,
|
|
25835
|
+
jiraKey: issue2.key
|
|
25836
|
+
});
|
|
25837
|
+
}
|
|
25838
|
+
if (signal.type === "decision") {
|
|
25839
|
+
actions.push({
|
|
25840
|
+
type: "decision-candidate",
|
|
25841
|
+
description: `Possible decision in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a decision artifact`,
|
|
25842
|
+
jiraKey: issue2.key
|
|
25843
|
+
});
|
|
25844
|
+
}
|
|
25845
|
+
if (signal.type === "question") {
|
|
25846
|
+
const linkedQuestion = issue2.marvinArtifacts.find(
|
|
25847
|
+
(a) => a.type === "question" && a.currentStatus !== "answered"
|
|
25848
|
+
);
|
|
25849
|
+
if (linkedQuestion) {
|
|
25850
|
+
actions.push({
|
|
25851
|
+
type: "question-candidate",
|
|
25852
|
+
description: `Question in ${issue2.key} comment by ${comment.author} \u2014 may relate to ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
|
|
25853
|
+
artifactId: linkedQuestion.id,
|
|
25854
|
+
jiraKey: issue2.key
|
|
25855
|
+
});
|
|
25856
|
+
} else {
|
|
25857
|
+
actions.push({
|
|
25858
|
+
type: "question-candidate",
|
|
25859
|
+
description: `Question in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a question artifact`,
|
|
25860
|
+
jiraKey: issue2.key
|
|
25861
|
+
});
|
|
25862
|
+
}
|
|
25863
|
+
}
|
|
25864
|
+
if (signal.type === "resolution") {
|
|
25865
|
+
const linkedQuestion = issue2.marvinArtifacts.find(
|
|
25866
|
+
(a) => a.type === "question" && a.currentStatus !== "answered"
|
|
25867
|
+
);
|
|
25868
|
+
if (linkedQuestion) {
|
|
25869
|
+
actions.push({
|
|
25870
|
+
type: "resolution-detected",
|
|
25871
|
+
description: `Resolution in ${issue2.key} by ${comment.author} may answer ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
|
|
25872
|
+
artifactId: linkedQuestion.id,
|
|
25873
|
+
jiraKey: issue2.key
|
|
25874
|
+
});
|
|
25875
|
+
}
|
|
25876
|
+
}
|
|
25877
|
+
}
|
|
25878
|
+
}
|
|
25879
|
+
for (const cl of issue2.confluenceLinks) {
|
|
25880
|
+
actions.push({
|
|
25881
|
+
type: "confluence-review",
|
|
25882
|
+
description: `Confluence page "${cl.title}" linked from ${issue2.key} \u2014 review for relevant updates`,
|
|
25883
|
+
jiraKey: issue2.key
|
|
25884
|
+
});
|
|
25885
|
+
}
|
|
25886
|
+
}
|
|
25887
|
+
return actions;
|
|
25204
25888
|
}
|
|
25205
25889
|
|
|
25206
25890
|
// src/skills/builtin/jira/tools.ts
|
|
@@ -25244,6 +25928,7 @@ function findByJiraKey(store, jiraKey) {
|
|
|
25244
25928
|
function createJiraTools(store, projectConfig) {
|
|
25245
25929
|
const jiraUserConfig = loadUserConfig().jira;
|
|
25246
25930
|
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
25931
|
+
const statusMap = projectConfig?.jira?.statusMap;
|
|
25247
25932
|
return [
|
|
25248
25933
|
// --- Local read tools ---
|
|
25249
25934
|
tool20(
|
|
@@ -25404,9 +26089,9 @@ function createJiraTools(store, projectConfig) {
|
|
|
25404
26089
|
// --- Local → Jira tools ---
|
|
25405
26090
|
tool20(
|
|
25406
26091
|
"push_artifact_to_jira",
|
|
25407
|
-
"Create a Jira issue from
|
|
26092
|
+
"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.",
|
|
25408
26093
|
{
|
|
25409
|
-
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', '
|
|
26094
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
|
|
25410
26095
|
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
|
|
25411
26096
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
25412
26097
|
},
|
|
@@ -25447,6 +26132,24 @@ function createJiraTools(store, projectConfig) {
|
|
|
25447
26132
|
description,
|
|
25448
26133
|
issuetype: { name: args.issueType ?? "Task" }
|
|
25449
26134
|
});
|
|
26135
|
+
const isDirectLink = artifact.frontmatter.type === "action" || artifact.frontmatter.type === "task";
|
|
26136
|
+
if (isDirectLink) {
|
|
26137
|
+
const existingTags = artifact.frontmatter.tags ?? [];
|
|
26138
|
+
store.update(args.artifactId, {
|
|
26139
|
+
jiraKey: jiraResult.key,
|
|
26140
|
+
jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
|
|
26141
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26142
|
+
tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${jiraResult.key}`]
|
|
26143
|
+
});
|
|
26144
|
+
return {
|
|
26145
|
+
content: [
|
|
26146
|
+
{
|
|
26147
|
+
type: "text",
|
|
26148
|
+
text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Linked directly on the artifact.`
|
|
26149
|
+
}
|
|
26150
|
+
]
|
|
26151
|
+
};
|
|
26152
|
+
}
|
|
25450
26153
|
const jiDoc = store.create(
|
|
25451
26154
|
JIRA_TYPE,
|
|
25452
26155
|
{
|
|
@@ -25568,65 +26271,430 @@ function createJiraTools(store, projectConfig) {
|
|
|
25568
26271
|
]
|
|
25569
26272
|
};
|
|
25570
26273
|
}
|
|
26274
|
+
),
|
|
26275
|
+
// --- Direct Jira linking for actions/tasks ---
|
|
26276
|
+
tool20(
|
|
26277
|
+
"link_to_jira",
|
|
26278
|
+
"Link an existing Jira issue to a Marvin action or task (sets jiraKey directly on the artifact)",
|
|
26279
|
+
{
|
|
26280
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'A-001', 'T-003')"),
|
|
26281
|
+
jiraKey: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
|
|
26282
|
+
},
|
|
26283
|
+
async (args) => {
|
|
26284
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
26285
|
+
if (!jira) return jiraNotConfiguredError();
|
|
26286
|
+
const artifact = store.get(args.artifactId);
|
|
26287
|
+
if (!artifact) {
|
|
26288
|
+
return {
|
|
26289
|
+
content: [
|
|
26290
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
26291
|
+
],
|
|
26292
|
+
isError: true
|
|
26293
|
+
};
|
|
26294
|
+
}
|
|
26295
|
+
if (artifact.frontmatter.type !== "action" && artifact.frontmatter.type !== "task") {
|
|
26296
|
+
return {
|
|
26297
|
+
content: [
|
|
26298
|
+
{
|
|
26299
|
+
type: "text",
|
|
26300
|
+
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.`
|
|
26301
|
+
}
|
|
26302
|
+
],
|
|
26303
|
+
isError: true
|
|
26304
|
+
};
|
|
26305
|
+
}
|
|
26306
|
+
const issue2 = await jira.client.getIssue(args.jiraKey);
|
|
26307
|
+
const existingTags = artifact.frontmatter.tags ?? [];
|
|
26308
|
+
store.update(args.artifactId, {
|
|
26309
|
+
jiraKey: args.jiraKey,
|
|
26310
|
+
jiraUrl: `https://${jira.host}/browse/${args.jiraKey}`,
|
|
26311
|
+
lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26312
|
+
tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${args.jiraKey}`]
|
|
26313
|
+
});
|
|
26314
|
+
return {
|
|
26315
|
+
content: [
|
|
26316
|
+
{
|
|
26317
|
+
type: "text",
|
|
26318
|
+
text: `Linked ${args.artifactId} to Jira ${args.jiraKey} ("${issue2.fields.summary}").`
|
|
26319
|
+
}
|
|
26320
|
+
]
|
|
26321
|
+
};
|
|
26322
|
+
}
|
|
26323
|
+
),
|
|
26324
|
+
// --- Jira status fetch (read-only) ---
|
|
26325
|
+
tool20(
|
|
26326
|
+
"fetch_jira_status",
|
|
26327
|
+
"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.",
|
|
26328
|
+
{
|
|
26329
|
+
artifactId: external_exports.string().optional().describe("Specific artifact ID to check, or omit to check all Jira-linked actions/tasks")
|
|
26330
|
+
},
|
|
26331
|
+
async (args) => {
|
|
26332
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
26333
|
+
if (!jira) return jiraNotConfiguredError();
|
|
26334
|
+
const fetchResult = await fetchJiraStatus(
|
|
26335
|
+
store,
|
|
26336
|
+
jira.client,
|
|
26337
|
+
jira.host,
|
|
26338
|
+
args.artifactId,
|
|
26339
|
+
statusMap
|
|
26340
|
+
);
|
|
26341
|
+
const parts = [];
|
|
26342
|
+
if (fetchResult.artifacts.length > 0) {
|
|
26343
|
+
for (const a of fetchResult.artifacts) {
|
|
26344
|
+
const changes = [];
|
|
26345
|
+
if (a.statusChanged) {
|
|
26346
|
+
changes.push(`status: ${a.currentMarvinStatus} \u2192 ${a.proposedMarvinStatus}`);
|
|
26347
|
+
}
|
|
26348
|
+
if (a.progressChanged) {
|
|
26349
|
+
changes.push(`progress: ${a.currentProgress ?? 0}% \u2192 ${a.proposedProgress}%`);
|
|
26350
|
+
}
|
|
26351
|
+
const header = `${a.id} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}" [${a.jiraStatus}]`;
|
|
26352
|
+
if (changes.length > 0) {
|
|
26353
|
+
parts.push(`${header}
|
|
26354
|
+
Proposed changes: ${changes.join(", ")}`);
|
|
26355
|
+
} else {
|
|
26356
|
+
parts.push(`${header}
|
|
26357
|
+
No status/progress changes.`);
|
|
26358
|
+
}
|
|
26359
|
+
if (a.linkedIssues.length > 0) {
|
|
26360
|
+
const done = a.linkedIssues.filter((l) => l.isDone).length;
|
|
26361
|
+
parts.push(` Linked issues (${done}/${a.linkedIssues.length} done):`);
|
|
26362
|
+
for (const li of a.linkedIssues) {
|
|
26363
|
+
const icon = li.isDone ? "\u2713" : "\u25CB";
|
|
26364
|
+
parts.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
|
|
26365
|
+
}
|
|
26366
|
+
}
|
|
26367
|
+
}
|
|
26368
|
+
parts.push("");
|
|
26369
|
+
parts.push("This is a read-only preview. Use update_action or update_task to apply the proposed status/progress changes.");
|
|
26370
|
+
}
|
|
26371
|
+
if (fetchResult.errors.length > 0) {
|
|
26372
|
+
parts.push("Errors:");
|
|
26373
|
+
for (const err of fetchResult.errors) {
|
|
26374
|
+
parts.push(` ${err}`);
|
|
26375
|
+
}
|
|
26376
|
+
}
|
|
26377
|
+
if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
|
|
26378
|
+
parts.push("No Jira-linked actions/tasks found.");
|
|
26379
|
+
}
|
|
26380
|
+
return {
|
|
26381
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
26382
|
+
isError: fetchResult.errors.length > 0 && fetchResult.artifacts.length === 0
|
|
26383
|
+
};
|
|
26384
|
+
},
|
|
26385
|
+
{ annotations: { readOnlyHint: true } }
|
|
26386
|
+
),
|
|
26387
|
+
// --- Jira status discovery ---
|
|
26388
|
+
tool20(
|
|
26389
|
+
"fetch_jira_statuses",
|
|
26390
|
+
"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.",
|
|
26391
|
+
{
|
|
26392
|
+
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'MCB1'). Falls back to jira.projectKey from config."),
|
|
26393
|
+
maxResults: external_exports.number().optional().describe("Max issues to scan (default 100)")
|
|
26394
|
+
},
|
|
26395
|
+
async (args) => {
|
|
26396
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
26397
|
+
if (!resolvedProjectKey) {
|
|
26398
|
+
return {
|
|
26399
|
+
content: [
|
|
26400
|
+
{
|
|
26401
|
+
type: "text",
|
|
26402
|
+
text: "No projectKey provided and no default configured."
|
|
26403
|
+
}
|
|
26404
|
+
],
|
|
26405
|
+
isError: true
|
|
26406
|
+
};
|
|
26407
|
+
}
|
|
26408
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
26409
|
+
if (!jira) return jiraNotConfiguredError();
|
|
26410
|
+
const host = jira.host;
|
|
26411
|
+
const auth = "Basic " + Buffer.from(
|
|
26412
|
+
`${jiraUserConfig?.email ?? process.env.JIRA_EMAIL}:${jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN}`
|
|
26413
|
+
).toString("base64");
|
|
26414
|
+
const params = new URLSearchParams({
|
|
26415
|
+
jql: `project = ${resolvedProjectKey}`,
|
|
26416
|
+
maxResults: String(args.maxResults ?? 100),
|
|
26417
|
+
fields: "status"
|
|
26418
|
+
});
|
|
26419
|
+
const resp = await fetch(`https://${host}/rest/api/3/search/jql?${params}`, {
|
|
26420
|
+
headers: { Authorization: auth, Accept: "application/json" }
|
|
26421
|
+
});
|
|
26422
|
+
if (!resp.ok) {
|
|
26423
|
+
const text = await resp.text().catch(() => "");
|
|
26424
|
+
return {
|
|
26425
|
+
content: [
|
|
26426
|
+
{
|
|
26427
|
+
type: "text",
|
|
26428
|
+
text: `Jira API error ${resp.status}: ${text}`
|
|
26429
|
+
}
|
|
26430
|
+
],
|
|
26431
|
+
isError: true
|
|
26432
|
+
};
|
|
26433
|
+
}
|
|
26434
|
+
const data = await resp.json();
|
|
26435
|
+
const statusCounts = /* @__PURE__ */ new Map();
|
|
26436
|
+
for (const issue2 of data.issues) {
|
|
26437
|
+
const s = issue2.fields.status.name;
|
|
26438
|
+
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
26439
|
+
}
|
|
26440
|
+
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
26441
|
+
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
26442
|
+
const actionLookup = /* @__PURE__ */ new Map();
|
|
26443
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
26444
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
26445
|
+
}
|
|
26446
|
+
const taskLookup = /* @__PURE__ */ new Map();
|
|
26447
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
26448
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
26449
|
+
}
|
|
26450
|
+
const parts = [
|
|
26451
|
+
`Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
|
|
26452
|
+
""
|
|
26453
|
+
];
|
|
26454
|
+
const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
26455
|
+
const unmappedAction = [];
|
|
26456
|
+
const unmappedTask = [];
|
|
26457
|
+
for (const [status, count] of sorted) {
|
|
26458
|
+
const actionTarget = actionLookup.get(status.toLowerCase());
|
|
26459
|
+
const taskTarget = taskLookup.get(status.toLowerCase());
|
|
26460
|
+
const actionLabel = actionTarget ? `\u2192 ${actionTarget}` : "UNMAPPED (\u2192 open)";
|
|
26461
|
+
const taskLabel = taskTarget ? `\u2192 ${taskTarget}` : "UNMAPPED (\u2192 backlog)";
|
|
26462
|
+
parts.push(` ${status} (${count} issues)`);
|
|
26463
|
+
parts.push(` action: ${actionLabel}`);
|
|
26464
|
+
parts.push(` task: ${taskLabel}`);
|
|
26465
|
+
if (!actionTarget) unmappedAction.push(status);
|
|
26466
|
+
if (!taskTarget) unmappedTask.push(status);
|
|
26467
|
+
}
|
|
26468
|
+
if (unmappedAction.length > 0 || unmappedTask.length > 0) {
|
|
26469
|
+
parts.push("");
|
|
26470
|
+
parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
|
|
26471
|
+
parts.push(" jira:");
|
|
26472
|
+
parts.push(" statusMap:");
|
|
26473
|
+
if (unmappedAction.length > 0) {
|
|
26474
|
+
parts.push(" action:");
|
|
26475
|
+
parts.push(` # Map these: ${unmappedAction.join(", ")}`);
|
|
26476
|
+
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
26477
|
+
}
|
|
26478
|
+
if (unmappedTask.length > 0) {
|
|
26479
|
+
parts.push(" task:");
|
|
26480
|
+
parts.push(` # Map these: ${unmappedTask.join(", ")}`);
|
|
26481
|
+
parts.push(" # <marvin-status>: [<jira-status>, ...]");
|
|
26482
|
+
}
|
|
26483
|
+
} else {
|
|
26484
|
+
parts.push("");
|
|
26485
|
+
parts.push("All statuses are mapped.");
|
|
26486
|
+
}
|
|
26487
|
+
const usingConfig = statusMap?.action || statusMap?.task;
|
|
26488
|
+
parts.push("");
|
|
26489
|
+
parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
|
|
26490
|
+
return {
|
|
26491
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
26492
|
+
};
|
|
26493
|
+
},
|
|
26494
|
+
{ annotations: { readOnlyHint: true } }
|
|
26495
|
+
),
|
|
26496
|
+
// --- Jira daily summary ---
|
|
26497
|
+
tool20(
|
|
26498
|
+
"fetch_jira_daily",
|
|
26499
|
+
"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.",
|
|
26500
|
+
{
|
|
26501
|
+
from: external_exports.string().optional().describe("Start date (YYYY-MM-DD). Defaults to today."),
|
|
26502
|
+
to: external_exports.string().optional().describe("End date (YYYY-MM-DD). Defaults to same as 'from'."),
|
|
26503
|
+
projectKey: external_exports.string().optional().describe("Jira project key. Falls back to jira.projectKey from config.")
|
|
26504
|
+
},
|
|
26505
|
+
async (args) => {
|
|
26506
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
26507
|
+
if (!resolvedProjectKey) {
|
|
26508
|
+
return {
|
|
26509
|
+
content: [
|
|
26510
|
+
{
|
|
26511
|
+
type: "text",
|
|
26512
|
+
text: "No projectKey provided and no default configured."
|
|
26513
|
+
}
|
|
26514
|
+
],
|
|
26515
|
+
isError: true
|
|
26516
|
+
};
|
|
26517
|
+
}
|
|
26518
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
26519
|
+
if (!jira) return jiraNotConfiguredError();
|
|
26520
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
26521
|
+
const fromDate = args.from ?? today;
|
|
26522
|
+
const toDate = args.to ?? fromDate;
|
|
26523
|
+
const daily = await fetchJiraDaily(
|
|
26524
|
+
store,
|
|
26525
|
+
jira.client,
|
|
26526
|
+
jira.host,
|
|
26527
|
+
resolvedProjectKey,
|
|
26528
|
+
{ from: fromDate, to: toDate },
|
|
26529
|
+
statusMap
|
|
26530
|
+
);
|
|
26531
|
+
return {
|
|
26532
|
+
content: [{ type: "text", text: formatDailySummary(daily) }],
|
|
26533
|
+
isError: daily.errors.length > 0 && daily.issues.length === 0
|
|
26534
|
+
};
|
|
26535
|
+
},
|
|
26536
|
+
{ annotations: { readOnlyHint: true } }
|
|
25571
26537
|
)
|
|
25572
26538
|
];
|
|
25573
26539
|
}
|
|
26540
|
+
function formatDailySummary(daily) {
|
|
26541
|
+
const parts = [];
|
|
26542
|
+
const rangeLabel = daily.dateRange.from === daily.dateRange.to ? daily.dateRange.from : `${daily.dateRange.from} to ${daily.dateRange.to}`;
|
|
26543
|
+
parts.push(`Jira Daily Summary \u2014 ${daily.projectKey} \u2014 ${rangeLabel}`);
|
|
26544
|
+
parts.push(`${daily.issues.length} issue(s) updated.
|
|
26545
|
+
`);
|
|
26546
|
+
const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
|
|
26547
|
+
const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
|
|
26548
|
+
if (linked.length > 0) {
|
|
26549
|
+
parts.push("## Linked Issues (with Marvin artifacts)\n");
|
|
26550
|
+
for (const issue2 of linked) {
|
|
26551
|
+
parts.push(formatIssueEntry(issue2));
|
|
26552
|
+
}
|
|
26553
|
+
}
|
|
26554
|
+
if (unlinked.length > 0) {
|
|
26555
|
+
parts.push("## Unlinked Issues (no Marvin artifact)\n");
|
|
26556
|
+
for (const issue2 of unlinked) {
|
|
26557
|
+
parts.push(formatIssueEntry(issue2));
|
|
26558
|
+
}
|
|
26559
|
+
}
|
|
26560
|
+
if (daily.proposedActions.length > 0) {
|
|
26561
|
+
parts.push("## Proposed Actions\n");
|
|
26562
|
+
for (const action of daily.proposedActions) {
|
|
26563
|
+
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}";
|
|
26564
|
+
parts.push(` ${icon} ${action.description}`);
|
|
26565
|
+
}
|
|
26566
|
+
parts.push("");
|
|
26567
|
+
parts.push("These are suggestions. Use update_action, update_task, or other tools to apply changes.");
|
|
26568
|
+
}
|
|
26569
|
+
if (daily.errors.length > 0) {
|
|
26570
|
+
parts.push("\n## Errors\n");
|
|
26571
|
+
for (const err of daily.errors) {
|
|
26572
|
+
parts.push(` ${err}`);
|
|
26573
|
+
}
|
|
26574
|
+
}
|
|
26575
|
+
return parts.join("\n");
|
|
26576
|
+
}
|
|
26577
|
+
function formatIssueEntry(issue2) {
|
|
26578
|
+
const lines = [];
|
|
26579
|
+
const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
|
|
26580
|
+
const artifactLabel = artifacts ? ` \u2192 ${artifacts}` : "";
|
|
26581
|
+
lines.push(`### ${issue2.key} \u2014 ${issue2.summary} [${issue2.currentStatus}]${artifactLabel}`);
|
|
26582
|
+
lines.push(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`);
|
|
26583
|
+
for (const a of issue2.marvinArtifacts) {
|
|
26584
|
+
if (a.statusDrift) {
|
|
26585
|
+
lines.push(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`);
|
|
26586
|
+
}
|
|
26587
|
+
}
|
|
26588
|
+
if (issue2.changes.length > 0) {
|
|
26589
|
+
lines.push(" Changes:");
|
|
26590
|
+
for (const c of issue2.changes) {
|
|
26591
|
+
lines.push(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`);
|
|
26592
|
+
}
|
|
26593
|
+
}
|
|
26594
|
+
if (issue2.comments.length > 0) {
|
|
26595
|
+
lines.push(` Comments (${issue2.comments.length}):`);
|
|
26596
|
+
for (const c of issue2.comments) {
|
|
26597
|
+
let signalIcons = "";
|
|
26598
|
+
if (c.signals.length > 0) {
|
|
26599
|
+
const icons = c.signals.map(
|
|
26600
|
+
(s) => s.type === "blocker" ? "\u{1F6AB}" : s.type === "decision" ? "\u2696" : s.type === "question" ? "?" : "\u2713"
|
|
26601
|
+
);
|
|
26602
|
+
signalIcons = ` [${icons.join("")}]`;
|
|
26603
|
+
}
|
|
26604
|
+
lines.push(` ${c.author} (${c.created.slice(0, 16)})${signalIcons}: ${c.bodyPreview}`);
|
|
26605
|
+
}
|
|
26606
|
+
}
|
|
26607
|
+
if (issue2.linkSuggestions.length > 0) {
|
|
26608
|
+
lines.push(" Possible Marvin matches:");
|
|
26609
|
+
for (const s of issue2.linkSuggestions) {
|
|
26610
|
+
lines.push(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`);
|
|
26611
|
+
}
|
|
26612
|
+
}
|
|
26613
|
+
if (issue2.linkedIssues.length > 0) {
|
|
26614
|
+
lines.push(" Linked issues:");
|
|
26615
|
+
for (const li of issue2.linkedIssues) {
|
|
26616
|
+
const icon = li.isDone ? "\u2713" : "\u25CB";
|
|
26617
|
+
lines.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
|
|
26618
|
+
}
|
|
26619
|
+
}
|
|
26620
|
+
if (issue2.confluenceLinks.length > 0) {
|
|
26621
|
+
lines.push(" Confluence pages:");
|
|
26622
|
+
for (const cl of issue2.confluenceLinks) {
|
|
26623
|
+
lines.push(` \u{1F4C4} ${cl.title}: ${cl.url}`);
|
|
26624
|
+
}
|
|
26625
|
+
}
|
|
26626
|
+
lines.push("");
|
|
26627
|
+
return lines.join("\n");
|
|
26628
|
+
}
|
|
25574
26629
|
|
|
25575
26630
|
// src/skills/builtin/jira/index.ts
|
|
26631
|
+
var COMMON_TOOLS = `**Available tools:**
|
|
26632
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues (JI-xxx documents)
|
|
26633
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
26634
|
+
- \`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.
|
|
26635
|
+
- \`link_to_jira\` \u2014 link an existing Jira issue to a Marvin action or task (sets \`jiraKey\` directly on the artifact)
|
|
26636
|
+
- \`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.
|
|
26637
|
+
- \`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).
|
|
26638
|
+
- \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
|
|
26639
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
26640
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx`;
|
|
26641
|
+
var COMMON_WORKFLOW = `**Jira sync workflow:**
|
|
26642
|
+
1. Call \`fetch_jira_status\` to see what Jira reports for linked artifacts
|
|
26643
|
+
2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
|
|
26644
|
+
3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
|
|
26645
|
+
|
|
26646
|
+
**Daily review workflow:**
|
|
26647
|
+
1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
|
|
26648
|
+
2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
|
|
26649
|
+
3. Use existing tools to apply changes, create new artifacts, or link untracked issues`;
|
|
25576
26650
|
var jiraSkill = {
|
|
25577
26651
|
id: "jira",
|
|
25578
26652
|
name: "Jira Integration",
|
|
25579
26653
|
description: "Bidirectional sync between Marvin artifacts and Jira issues",
|
|
25580
26654
|
version: "1.0.0",
|
|
25581
26655
|
format: "builtin-ts",
|
|
25582
|
-
// No default persona affinity — opt-in via config.yaml skills section
|
|
25583
26656
|
documentTypeRegistrations: [
|
|
25584
26657
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
25585
26658
|
],
|
|
25586
26659
|
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
25587
26660
|
promptFragments: {
|
|
25588
|
-
"product-owner": `You have the **Jira Integration** skill.
|
|
26661
|
+
"product-owner": `You have the **Jira Integration** skill.
|
|
25589
26662
|
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
25593
|
-
- \`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\`.
|
|
25594
|
-
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
25595
|
-
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
26663
|
+
${COMMON_TOOLS}
|
|
26664
|
+
|
|
26665
|
+
${COMMON_WORKFLOW}
|
|
25596
26666
|
|
|
25597
26667
|
**As Product Owner, use Jira integration to:**
|
|
26668
|
+
- Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
|
|
25598
26669
|
- Pull stakeholder-reported issues for triage and prioritization
|
|
25599
26670
|
- Push approved features as Stories for development tracking
|
|
25600
26671
|
- Link decisions to Jira issues for audit trail and traceability
|
|
25601
|
-
- Use
|
|
25602
|
-
"tech-lead": `You have the **Jira Integration** skill.
|
|
26672
|
+
- Use \`fetch_jira_statuses\` when setting up a new project to configure status mappings`,
|
|
26673
|
+
"tech-lead": `You have the **Jira Integration** skill.
|
|
25603
26674
|
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
- \`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\`.
|
|
25608
|
-
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
25609
|
-
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
26675
|
+
${COMMON_TOOLS}
|
|
26676
|
+
|
|
26677
|
+
${COMMON_WORKFLOW}
|
|
25610
26678
|
|
|
25611
26679
|
**As Tech Lead, use Jira integration to:**
|
|
26680
|
+
- Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
|
|
25612
26681
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
25613
26682
|
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
25614
|
-
-
|
|
25615
|
-
- Use
|
|
25616
|
-
"delivery-manager": `You have the **Jira Integration** skill.
|
|
26683
|
+
- Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
|
|
26684
|
+
- Use \`fetch_jira_statuses\` to verify status mappings match the team's Jira workflow`,
|
|
26685
|
+
"delivery-manager": `You have the **Jira Integration** skill.
|
|
25617
26686
|
|
|
25618
|
-
|
|
25619
|
-
|
|
25620
|
-
|
|
25621
|
-
|
|
25622
|
-
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
25623
|
-
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
26687
|
+
${COMMON_TOOLS}
|
|
26688
|
+
|
|
26689
|
+
${COMMON_WORKFLOW}
|
|
26690
|
+
This is a third path for progress tracking alongside Contributions and Meetings.
|
|
25624
26691
|
|
|
25625
26692
|
**As Delivery Manager, use Jira integration to:**
|
|
26693
|
+
- Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
|
|
25626
26694
|
- Pull sprint issues for tracking progress and blockers
|
|
25627
|
-
- Push actions
|
|
25628
|
-
- Use
|
|
25629
|
-
-
|
|
26695
|
+
- Push actions and tasks to Jira for stakeholder visibility
|
|
26696
|
+
- Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
|
|
26697
|
+
- Use \`fetch_jira_statuses\` to ensure Jira workflow statuses are properly mapped`
|
|
25630
26698
|
}
|
|
25631
26699
|
};
|
|
25632
26700
|
|
|
@@ -30490,12 +31558,355 @@ Run "marvin doctor --fix" to auto-repair fixable issues.`));
|
|
|
30490
31558
|
console.log();
|
|
30491
31559
|
}
|
|
30492
31560
|
|
|
31561
|
+
// src/cli/commands/jira.ts
|
|
31562
|
+
import chalk20 from "chalk";
|
|
31563
|
+
async function jiraSyncCommand(artifactId, options = {}) {
|
|
31564
|
+
const project = loadProject();
|
|
31565
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
31566
|
+
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
31567
|
+
const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
|
|
31568
|
+
const store = new DocumentStore(project.marvinDir, [...registrations, jiReg]);
|
|
31569
|
+
const jiraUserConfig = loadUserConfig().jira;
|
|
31570
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
31571
|
+
if (!jira) {
|
|
31572
|
+
console.log(
|
|
31573
|
+
chalk20.red(
|
|
31574
|
+
'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
|
|
31575
|
+
)
|
|
31576
|
+
);
|
|
31577
|
+
return;
|
|
31578
|
+
}
|
|
31579
|
+
const statusMap = project.config.jira?.statusMap;
|
|
31580
|
+
const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
|
|
31581
|
+
console.log(chalk20.dim(label));
|
|
31582
|
+
if (options.dryRun) {
|
|
31583
|
+
const fetchResult = await fetchJiraStatus(
|
|
31584
|
+
store,
|
|
31585
|
+
jira.client,
|
|
31586
|
+
jira.host,
|
|
31587
|
+
artifactId,
|
|
31588
|
+
statusMap
|
|
31589
|
+
);
|
|
31590
|
+
const withChanges = fetchResult.artifacts.filter(
|
|
31591
|
+
(a) => a.statusChanged || a.progressChanged
|
|
31592
|
+
);
|
|
31593
|
+
const noChanges = fetchResult.artifacts.filter(
|
|
31594
|
+
(a) => !a.statusChanged && !a.progressChanged
|
|
31595
|
+
);
|
|
31596
|
+
if (withChanges.length > 0) {
|
|
31597
|
+
console.log(chalk20.yellow(`
|
|
31598
|
+
Proposed changes for ${withChanges.length} artifact(s):`));
|
|
31599
|
+
for (const a of withChanges) {
|
|
31600
|
+
console.log(` ${chalk20.bold(a.id)} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}"`);
|
|
31601
|
+
if (a.statusChanged) {
|
|
31602
|
+
console.log(
|
|
31603
|
+
` status: ${chalk20.yellow(a.currentMarvinStatus)} \u2192 ${chalk20.green(a.proposedMarvinStatus)}`
|
|
31604
|
+
);
|
|
31605
|
+
}
|
|
31606
|
+
if (a.progressChanged) {
|
|
31607
|
+
console.log(
|
|
31608
|
+
` progress: ${chalk20.yellow(String(a.currentProgress ?? 0) + "%")} \u2192 ${chalk20.green(String(a.proposedProgress) + "%")}`
|
|
31609
|
+
);
|
|
31610
|
+
}
|
|
31611
|
+
if (a.linkedIssues.length > 0) {
|
|
31612
|
+
const done = a.linkedIssues.filter((l) => l.isDone).length;
|
|
31613
|
+
console.log(chalk20.dim(` ${done}/${a.linkedIssues.length} linked issues done`));
|
|
31614
|
+
}
|
|
31615
|
+
}
|
|
31616
|
+
console.log(chalk20.dim("\nRun without --dry-run to apply these changes."));
|
|
31617
|
+
}
|
|
31618
|
+
if (noChanges.length > 0) {
|
|
31619
|
+
console.log(chalk20.dim(`
|
|
31620
|
+
${noChanges.length} artifact(s) already in sync.`));
|
|
31621
|
+
}
|
|
31622
|
+
if (fetchResult.errors.length > 0) {
|
|
31623
|
+
console.log(chalk20.red("\nErrors:"));
|
|
31624
|
+
for (const err of fetchResult.errors) {
|
|
31625
|
+
console.log(chalk20.red(` ${err}`));
|
|
31626
|
+
}
|
|
31627
|
+
}
|
|
31628
|
+
if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
|
|
31629
|
+
console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to check."));
|
|
31630
|
+
}
|
|
31631
|
+
return;
|
|
31632
|
+
}
|
|
31633
|
+
const result = await syncJiraProgress(
|
|
31634
|
+
store,
|
|
31635
|
+
jira.client,
|
|
31636
|
+
jira.host,
|
|
31637
|
+
artifactId,
|
|
31638
|
+
statusMap
|
|
31639
|
+
);
|
|
31640
|
+
if (result.updated.length > 0) {
|
|
31641
|
+
console.log(chalk20.green(`
|
|
31642
|
+
Updated ${result.updated.length} artifact(s):`));
|
|
31643
|
+
for (const entry of result.updated) {
|
|
31644
|
+
const statusChange = entry.oldStatus !== entry.newStatus ? `${chalk20.yellow(entry.oldStatus)} \u2192 ${chalk20.green(entry.newStatus)}` : chalk20.dim(entry.newStatus);
|
|
31645
|
+
console.log(` ${chalk20.bold(entry.id)} (${entry.jiraKey}): ${statusChange}`);
|
|
31646
|
+
if (entry.linkedIssues.length > 0) {
|
|
31647
|
+
const done = entry.linkedIssues.filter((l) => l.isDone).length;
|
|
31648
|
+
console.log(
|
|
31649
|
+
chalk20.dim(` ${done}/${entry.linkedIssues.length} linked issues done`)
|
|
31650
|
+
);
|
|
31651
|
+
for (const li of entry.linkedIssues) {
|
|
31652
|
+
const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
|
|
31653
|
+
console.log(
|
|
31654
|
+
chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}]`)
|
|
31655
|
+
);
|
|
31656
|
+
}
|
|
31657
|
+
}
|
|
31658
|
+
}
|
|
31659
|
+
}
|
|
31660
|
+
if (result.unchanged > 0) {
|
|
31661
|
+
console.log(chalk20.dim(`
|
|
31662
|
+
${result.unchanged} artifact(s) unchanged.`));
|
|
31663
|
+
}
|
|
31664
|
+
if (result.errors.length > 0) {
|
|
31665
|
+
console.log(chalk20.red("\nErrors:"));
|
|
31666
|
+
for (const err of result.errors) {
|
|
31667
|
+
console.log(chalk20.red(` ${err}`));
|
|
31668
|
+
}
|
|
31669
|
+
}
|
|
31670
|
+
if (result.updated.length === 0 && result.unchanged === 0 && result.errors.length === 0) {
|
|
31671
|
+
console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to sync."));
|
|
31672
|
+
}
|
|
31673
|
+
}
|
|
31674
|
+
async function jiraStatusesCommand(projectKey) {
|
|
31675
|
+
const project = loadProject();
|
|
31676
|
+
const jiraUserConfig = loadUserConfig().jira;
|
|
31677
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
31678
|
+
if (!jira) {
|
|
31679
|
+
console.log(
|
|
31680
|
+
chalk20.red(
|
|
31681
|
+
'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
|
|
31682
|
+
)
|
|
31683
|
+
);
|
|
31684
|
+
return;
|
|
31685
|
+
}
|
|
31686
|
+
const resolvedProjectKey = projectKey ?? project.config.jira?.projectKey;
|
|
31687
|
+
if (!resolvedProjectKey) {
|
|
31688
|
+
console.log(
|
|
31689
|
+
chalk20.red(
|
|
31690
|
+
"No project key provided. Pass it as an argument or set jira.projectKey in .marvin/config.yaml."
|
|
31691
|
+
)
|
|
31692
|
+
);
|
|
31693
|
+
return;
|
|
31694
|
+
}
|
|
31695
|
+
console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
|
|
31696
|
+
const statusMap = project.config.jira?.statusMap;
|
|
31697
|
+
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
31698
|
+
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
31699
|
+
const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
|
|
31700
|
+
const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
|
|
31701
|
+
const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
|
|
31702
|
+
const params = new URLSearchParams({
|
|
31703
|
+
jql: `project = ${resolvedProjectKey}`,
|
|
31704
|
+
maxResults: "100",
|
|
31705
|
+
fields: "status"
|
|
31706
|
+
});
|
|
31707
|
+
const resp = await fetch(`https://${jira.host}/rest/api/3/search/jql?${params}`, {
|
|
31708
|
+
headers: { Authorization: auth, Accept: "application/json" }
|
|
31709
|
+
});
|
|
31710
|
+
if (!resp.ok) {
|
|
31711
|
+
const text = await resp.text().catch(() => "");
|
|
31712
|
+
console.log(chalk20.red(`Jira API error ${resp.status}: ${text}`));
|
|
31713
|
+
return;
|
|
31714
|
+
}
|
|
31715
|
+
const data = await resp.json();
|
|
31716
|
+
const statusCounts = /* @__PURE__ */ new Map();
|
|
31717
|
+
for (const issue2 of data.issues) {
|
|
31718
|
+
const s = issue2.fields.status.name;
|
|
31719
|
+
statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
|
|
31720
|
+
}
|
|
31721
|
+
const actionLookup = /* @__PURE__ */ new Map();
|
|
31722
|
+
for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
|
|
31723
|
+
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
31724
|
+
}
|
|
31725
|
+
const taskLookup = /* @__PURE__ */ new Map();
|
|
31726
|
+
for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
|
|
31727
|
+
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
31728
|
+
}
|
|
31729
|
+
console.log(
|
|
31730
|
+
`
|
|
31731
|
+
Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.bold(resolvedProjectKey)} (scanned ${data.issues.length} of ${data.total} issues):
|
|
31732
|
+
`
|
|
31733
|
+
);
|
|
31734
|
+
const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
31735
|
+
let hasUnmapped = false;
|
|
31736
|
+
for (const [status, count] of sorted) {
|
|
31737
|
+
const actionTarget = actionLookup.get(status.toLowerCase());
|
|
31738
|
+
const taskTarget = taskLookup.get(status.toLowerCase());
|
|
31739
|
+
const actionLabel = actionTarget ? chalk20.green(`\u2192 ${actionTarget}`) : chalk20.yellow("UNMAPPED (\u2192 open)");
|
|
31740
|
+
const taskLabel = taskTarget ? chalk20.green(`\u2192 ${taskTarget}`) : chalk20.yellow("UNMAPPED (\u2192 backlog)");
|
|
31741
|
+
if (!actionTarget || !taskTarget) hasUnmapped = true;
|
|
31742
|
+
console.log(` ${chalk20.bold(status)} ${chalk20.dim(`(${count} issues)`)}`);
|
|
31743
|
+
console.log(` action: ${actionLabel}`);
|
|
31744
|
+
console.log(` task: ${taskLabel}`);
|
|
31745
|
+
}
|
|
31746
|
+
if (hasUnmapped) {
|
|
31747
|
+
console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
|
|
31748
|
+
console.log(chalk20.dim(" jira:"));
|
|
31749
|
+
console.log(chalk20.dim(" statusMap:"));
|
|
31750
|
+
console.log(chalk20.dim(" action:"));
|
|
31751
|
+
console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
|
|
31752
|
+
console.log(chalk20.dim(" task:"));
|
|
31753
|
+
console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
|
|
31754
|
+
} else {
|
|
31755
|
+
console.log(chalk20.green("\nAll statuses are mapped."));
|
|
31756
|
+
}
|
|
31757
|
+
const usingConfig = statusMap?.action || statusMap?.task;
|
|
31758
|
+
console.log(
|
|
31759
|
+
chalk20.dim(
|
|
31760
|
+
usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
|
|
31761
|
+
)
|
|
31762
|
+
);
|
|
31763
|
+
}
|
|
31764
|
+
async function jiraDailyCommand(options) {
|
|
31765
|
+
const proj = loadProject();
|
|
31766
|
+
const plugin = resolvePlugin(proj.config.methodology);
|
|
31767
|
+
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
31768
|
+
const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
|
|
31769
|
+
const store = new DocumentStore(proj.marvinDir, [...registrations, jiReg]);
|
|
31770
|
+
const jiraUserConfig = loadUserConfig().jira;
|
|
31771
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
31772
|
+
if (!jira) {
|
|
31773
|
+
console.log(
|
|
31774
|
+
chalk20.red(
|
|
31775
|
+
'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
|
|
31776
|
+
)
|
|
31777
|
+
);
|
|
31778
|
+
return;
|
|
31779
|
+
}
|
|
31780
|
+
const resolvedProjectKey = options.project ?? proj.config.jira?.projectKey;
|
|
31781
|
+
if (!resolvedProjectKey) {
|
|
31782
|
+
console.log(
|
|
31783
|
+
chalk20.red(
|
|
31784
|
+
"No project key provided. Use --project or set jira.projectKey in .marvin/config.yaml."
|
|
31785
|
+
)
|
|
31786
|
+
);
|
|
31787
|
+
return;
|
|
31788
|
+
}
|
|
31789
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31790
|
+
const fromDate = options.from ?? today;
|
|
31791
|
+
const toDate = options.to ?? fromDate;
|
|
31792
|
+
const statusMap = proj.config.jira?.statusMap;
|
|
31793
|
+
const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
|
|
31794
|
+
console.log(
|
|
31795
|
+
chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
|
|
31796
|
+
);
|
|
31797
|
+
const daily = await fetchJiraDaily(
|
|
31798
|
+
store,
|
|
31799
|
+
jira.client,
|
|
31800
|
+
jira.host,
|
|
31801
|
+
resolvedProjectKey,
|
|
31802
|
+
{ from: fromDate, to: toDate },
|
|
31803
|
+
statusMap
|
|
31804
|
+
);
|
|
31805
|
+
console.log(
|
|
31806
|
+
`
|
|
31807
|
+
${chalk20.bold(`Jira Daily \u2014 ${resolvedProjectKey} \u2014 ${rangeLabel}`)}`
|
|
31808
|
+
);
|
|
31809
|
+
console.log(`${daily.issues.length} issue(s) updated.
|
|
31810
|
+
`);
|
|
31811
|
+
const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
|
|
31812
|
+
const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
|
|
31813
|
+
if (linked.length > 0) {
|
|
31814
|
+
console.log(chalk20.underline("Linked Issues (with Marvin artifacts):\n"));
|
|
31815
|
+
for (const issue2 of linked) {
|
|
31816
|
+
printIssueEntry(issue2);
|
|
31817
|
+
}
|
|
31818
|
+
}
|
|
31819
|
+
if (unlinked.length > 0) {
|
|
31820
|
+
console.log(chalk20.underline("Unlinked Issues (no Marvin artifact):\n"));
|
|
31821
|
+
for (const issue2 of unlinked) {
|
|
31822
|
+
printIssueEntry(issue2);
|
|
31823
|
+
}
|
|
31824
|
+
}
|
|
31825
|
+
if (daily.proposedActions.length > 0) {
|
|
31826
|
+
console.log(chalk20.underline("Proposed Actions:\n"));
|
|
31827
|
+
for (const action of daily.proposedActions) {
|
|
31828
|
+
const icon = action.type === "status-update" ? chalk20.yellow("\u21BB") : action.type === "unlinked-issue" ? chalk20.blue("+") : action.type === "link-suggestion" ? chalk20.cyan("\u{1F517}") : action.type === "question-candidate" ? chalk20.magenta("?") : action.type === "decision-candidate" ? chalk20.yellow("\u2696") : action.type === "blocker-detected" ? chalk20.red("\u{1F6AB}") : action.type === "resolution-detected" ? chalk20.green("\u2713") : chalk20.cyan("\u{1F4C4}");
|
|
31829
|
+
console.log(` ${icon} ${action.description}`);
|
|
31830
|
+
}
|
|
31831
|
+
console.log();
|
|
31832
|
+
}
|
|
31833
|
+
if (daily.errors.length > 0) {
|
|
31834
|
+
console.log(chalk20.red("Errors:"));
|
|
31835
|
+
for (const err of daily.errors) {
|
|
31836
|
+
console.log(chalk20.red(` ${err}`));
|
|
31837
|
+
}
|
|
31838
|
+
}
|
|
31839
|
+
if (daily.issues.length === 0 && daily.errors.length === 0) {
|
|
31840
|
+
console.log(chalk20.dim("No Jira activity found for this period."));
|
|
31841
|
+
}
|
|
31842
|
+
}
|
|
31843
|
+
function printIssueEntry(issue2) {
|
|
31844
|
+
const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
|
|
31845
|
+
const artifactLabel = artifacts ? chalk20.cyan(` \u2192 ${artifacts}`) : "";
|
|
31846
|
+
console.log(
|
|
31847
|
+
` ${chalk20.bold(issue2.key)} \u2014 ${issue2.summary} [${chalk20.yellow(issue2.currentStatus)}]${artifactLabel}`
|
|
31848
|
+
);
|
|
31849
|
+
console.log(
|
|
31850
|
+
chalk20.dim(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`)
|
|
31851
|
+
);
|
|
31852
|
+
for (const a of issue2.marvinArtifacts) {
|
|
31853
|
+
if (a.statusDrift) {
|
|
31854
|
+
console.log(
|
|
31855
|
+
chalk20.yellow(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`)
|
|
31856
|
+
);
|
|
31857
|
+
}
|
|
31858
|
+
}
|
|
31859
|
+
if (issue2.changes.length > 0) {
|
|
31860
|
+
console.log(chalk20.dim(" Changes:"));
|
|
31861
|
+
for (const c of issue2.changes) {
|
|
31862
|
+
console.log(
|
|
31863
|
+
chalk20.dim(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`)
|
|
31864
|
+
);
|
|
31865
|
+
}
|
|
31866
|
+
}
|
|
31867
|
+
if (issue2.comments.length > 0) {
|
|
31868
|
+
console.log(chalk20.dim(` Comments (${issue2.comments.length}):`));
|
|
31869
|
+
for (const c of issue2.comments) {
|
|
31870
|
+
let signalLabel = "";
|
|
31871
|
+
if (c.signals.length > 0) {
|
|
31872
|
+
const labels = c.signals.map(
|
|
31873
|
+
(s) => s.type === "blocker" ? chalk20.red("\u{1F6AB}blocker") : s.type === "decision" ? chalk20.yellow("\u2696decision") : s.type === "question" ? chalk20.magenta("?question") : chalk20.green("\u2713resolution")
|
|
31874
|
+
);
|
|
31875
|
+
signalLabel = ` ${labels.join(" ")}`;
|
|
31876
|
+
}
|
|
31877
|
+
console.log(chalk20.dim(` ${c.author} (${c.created.slice(0, 16)})${signalLabel}: ${c.bodyPreview}`));
|
|
31878
|
+
}
|
|
31879
|
+
}
|
|
31880
|
+
if (issue2.linkSuggestions.length > 0) {
|
|
31881
|
+
console.log(chalk20.cyan(" Possible Marvin matches:"));
|
|
31882
|
+
for (const s of issue2.linkSuggestions) {
|
|
31883
|
+
console.log(
|
|
31884
|
+
chalk20.cyan(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`)
|
|
31885
|
+
);
|
|
31886
|
+
}
|
|
31887
|
+
}
|
|
31888
|
+
if (issue2.linkedIssues.length > 0) {
|
|
31889
|
+
console.log(chalk20.dim(" Linked issues:"));
|
|
31890
|
+
for (const li of issue2.linkedIssues) {
|
|
31891
|
+
const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
|
|
31892
|
+
console.log(chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`));
|
|
31893
|
+
}
|
|
31894
|
+
}
|
|
31895
|
+
if (issue2.confluenceLinks.length > 0) {
|
|
31896
|
+
console.log(chalk20.dim(" Confluence pages:"));
|
|
31897
|
+
for (const cl of issue2.confluenceLinks) {
|
|
31898
|
+
console.log(chalk20.dim(` \u{1F4C4} ${cl.title}: ${cl.url}`));
|
|
31899
|
+
}
|
|
31900
|
+
}
|
|
31901
|
+
console.log();
|
|
31902
|
+
}
|
|
31903
|
+
|
|
30493
31904
|
// src/cli/program.ts
|
|
30494
31905
|
function createProgram() {
|
|
30495
31906
|
const program2 = new Command();
|
|
30496
31907
|
program2.name("marvin").description(
|
|
30497
31908
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
30498
|
-
).version("0.5.
|
|
31909
|
+
).version("0.5.8");
|
|
30499
31910
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
30500
31911
|
await initCommand();
|
|
30501
31912
|
});
|
|
@@ -30591,6 +32002,16 @@ function createProgram() {
|
|
|
30591
32002
|
generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
|
|
30592
32003
|
await generateClaudeMdCommand(options);
|
|
30593
32004
|
});
|
|
32005
|
+
const jiraCmd = program2.command("jira").description("Jira integration commands");
|
|
32006
|
+
jiraCmd.command("sync [artifactId]").description("Sync Jira-linked actions/tasks with their Jira issues").option("--dry-run", "Preview proposed changes without applying them").action(async (artifactId, options) => {
|
|
32007
|
+
await jiraSyncCommand(artifactId, options);
|
|
32008
|
+
});
|
|
32009
|
+
jiraCmd.command("statuses [projectKey]").description("Show Jira project statuses and their Marvin status mappings").action(async (projectKey) => {
|
|
32010
|
+
await jiraStatusesCommand(projectKey);
|
|
32011
|
+
});
|
|
32012
|
+
jiraCmd.command("daily").description("Show daily summary of Jira changes with Marvin cross-references").option("--from <date>", "Start date (YYYY-MM-DD, default: today)").option("--to <date>", "End date (YYYY-MM-DD, default: same as --from)").option("--project <key>", "Jira project key (falls back to config)").action(async (options) => {
|
|
32013
|
+
await jiraDailyCommand(options);
|
|
32014
|
+
});
|
|
30594
32015
|
return program2;
|
|
30595
32016
|
}
|
|
30596
32017
|
|