mrvn-cli 0.5.7 → 0.5.9

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.
@@ -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
- this.baseUrl = `https://${config2.host}/rest/api/2`;
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,18 +19053,26 @@ 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} ${path11}: ${text}`
19056
+ `Jira API error ${response.status} ${method} ${url2}: ${text}`
19047
19057
  );
19048
19058
  }
19049
19059
  if (response.status === 204) return void 0;
19050
19060
  return response.json();
19051
19061
  }
19052
19062
  async searchIssues(jql, maxResults = 50) {
19063
+ return this.searchIssuesV3(
19064
+ jql,
19065
+ ["summary", "description", "status", "issuetype", "priority", "assignee", "labels", "created", "updated"],
19066
+ maxResults
19067
+ );
19068
+ }
19069
+ async searchIssuesV3(jql, fields = ["summary", "status", "issuetype", "priority", "assignee", "labels"], maxResults = 50) {
19053
19070
  const params = new URLSearchParams({
19054
19071
  jql,
19055
- maxResults: String(maxResults)
19072
+ maxResults: String(maxResults),
19073
+ fields: fields.join(",")
19056
19074
  });
19057
- return this.request(`/search?${params}`);
19075
+ return this.requestV3(`/search/jql?${params}`);
19058
19076
  }
19059
19077
  async getIssue(key) {
19060
19078
  return this.request(`/issue/${encodeURIComponent(key)}`);
@@ -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
- return { client: new JiraClient({ host, email: email3, apiToken }), host };
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 any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
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', 'F-003', 'E-002')"),
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. You can pull issues from Jira and push Marvin artifacts to Jira.
20503
+ "product-owner": `You have the **Jira Integration** skill.
19471
20504
 
19472
- **Available tools:**
19473
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19474
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
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 JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
19484
- "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
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
- **Available tools:**
19487
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19488
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
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
- - Bidirectional sync to keep local governance and Jira in alignment
19497
- - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
19498
- "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
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
- **Available tools:**
19501
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19502
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
19503
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
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, decisions, and tasks to Jira for stakeholder visibility
19510
- - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
19511
- - Sync status between Marvin governance items and Jira issues`
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 DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => DONE_STATUSES5.has(d.frontmatter.status)).length;
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 (DONE_STATUSES5.has(f.frontmatter.status)) continue;
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 && !DONE_STATUSES5.has(epic.frontmatter.status)) {
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 DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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 (DONE_STATUSES14.has(t.frontmatter.status)) done++;
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 DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
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 ?? (DONE_STATUSES6.has(w.status) ? 100 : 0);
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 DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES7.has(f.frontmatter.status)).sort((a, b) => {
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 DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES8.has(d.frontmatter.status));
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 DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES9.has(d.frontmatter.status));
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 DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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 (DONE_STATUSES10.has(d.frontmatter.status)) return false;
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 DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES11.has(a.frontmatter.status)).length;
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 (!DONE_STATUSES11.has(act.frontmatter.status)) {
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 DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => !DONE_STATUSES12.has(d.frontmatter.status));
24733
- const openTasks = tasks.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
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 DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
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) => DONE_STATUSES13.has(t.frontmatter.status)).length;
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) && !DONE_STATUSES13.has(t.frontmatter.status)
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",