mrvn-cli 0.5.5 → 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.js CHANGED
@@ -15771,6 +15771,7 @@ function collectSprintSummaryData(store, sprintId) {
15771
15771
  type: doc.frontmatter.type,
15772
15772
  status: doc.frontmatter.status,
15773
15773
  progress: getEffectiveProgress(doc.frontmatter),
15774
+ owner: doc.frontmatter.owner,
15774
15775
  workFocus: focusTag ? focusTag.slice(6) : void 0,
15775
15776
  aboutArtifact: about
15776
15777
  };
@@ -15977,10 +15978,10 @@ function getBoardData(store, type) {
15977
15978
  if (!byStatus.has(status)) byStatus.set(status, []);
15978
15979
  byStatus.get(status).push(doc);
15979
15980
  }
15980
- const statusOrder = ["open", "draft", "in-progress", "blocked"];
15981
+ const statusOrder2 = ["open", "draft", "in-progress", "blocked"];
15981
15982
  const allStatuses = [...byStatus.keys()];
15982
15983
  const ordered = [];
15983
- for (const s of statusOrder) {
15984
+ for (const s of statusOrder2) {
15984
15985
  if (allStatuses.includes(s)) ordered.push(s);
15985
15986
  }
15986
15987
  for (const s of allStatuses.sort()) {
@@ -18131,6 +18132,31 @@ tr:hover td {
18131
18132
  .focus-group-progress {
18132
18133
  width: 96px;
18133
18134
  }
18135
+
18136
+ /* Owner badges for DM sprint view */
18137
+ .owner-badge {
18138
+ display: inline-block;
18139
+ padding: 0.1rem 0.5rem;
18140
+ border-radius: 999px;
18141
+ font-size: 0.65rem;
18142
+ font-weight: 700;
18143
+ text-transform: uppercase;
18144
+ letter-spacing: 0.04em;
18145
+ white-space: nowrap;
18146
+ }
18147
+ .owner-badge-po { background: rgba(108, 140, 255, 0.18); color: #6c8cff; }
18148
+ .owner-badge-tl { background: rgba(251, 191, 36, 0.18); color: #fbbf24; }
18149
+ .owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
18150
+ .owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
18151
+
18152
+ /* Group header rows (PO dashboard decisions/deps) */
18153
+ .group-header-row td {
18154
+ background: var(--bg-hover);
18155
+ padding-top: 0.5rem;
18156
+ padding-bottom: 0.5rem;
18157
+ border-bottom: 1px solid var(--border);
18158
+ font-size: 0.8rem;
18159
+ }
18134
18160
  `;
18135
18161
  }
18136
18162
 
@@ -18962,19 +18988,55 @@ function buildHealthGauge(categories) {
18962
18988
  }
18963
18989
 
18964
18990
  // src/web/templates/pages/po/dashboard.ts
18991
+ var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
18992
+ var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
18965
18993
  function poDashboardPage(ctx) {
18966
18994
  const overview = getOverviewData(ctx.store);
18967
- const upcoming = getUpcomingData(ctx.store);
18968
18995
  const sprintData = getSprintSummaryData(ctx.store);
18969
18996
  const diagrams = getDiagramData(ctx.store);
18970
18997
  const features = ctx.store.list({ type: "feature" });
18971
- const featuresDone = features.filter((d) => ["done", "closed", "resolved"].includes(d.frontmatter.status)).length;
18972
- const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
18973
- const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
18974
- const RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
18998
+ const epics = ctx.store.list({ type: "epic" });
18999
+ const allTasks = ctx.store.list({ type: "task" });
18975
19000
  const decisions = ctx.store.list({ type: "decision" });
18976
- const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status)).length;
18977
19001
  const questions = ctx.store.list({ type: "question" });
19002
+ const sprints = ctx.store.list({ type: "sprint" });
19003
+ const featureToEpics = /* @__PURE__ */ new Map();
19004
+ for (const epic of epics) {
19005
+ const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
19006
+ for (const fid of featureIds) {
19007
+ const arr = featureToEpics.get(fid) ?? [];
19008
+ arr.push(epic);
19009
+ featureToEpics.set(fid, arr);
19010
+ }
19011
+ }
19012
+ const epicToTasks = /* @__PURE__ */ new Map();
19013
+ for (const task of allTasks) {
19014
+ const tags = task.frontmatter.tags ?? [];
19015
+ for (const tag of tags) {
19016
+ if (tag.startsWith("epic:")) {
19017
+ const epicId = tag.slice(5);
19018
+ const arr = epicToTasks.get(epicId) ?? [];
19019
+ arr.push(task);
19020
+ epicToTasks.set(epicId, arr);
19021
+ }
19022
+ }
19023
+ }
19024
+ const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
19025
+ let sprintTimelinePct = 0;
19026
+ if (activeSprint) {
19027
+ const startDate = activeSprint.frontmatter.startDate;
19028
+ const endDate = activeSprint.frontmatter.endDate;
19029
+ if (startDate && endDate) {
19030
+ const startMs = new Date(startDate).getTime();
19031
+ const endMs = new Date(endDate).getTime();
19032
+ const totalDays = Math.max(1, endMs - startMs);
19033
+ sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
19034
+ }
19035
+ }
19036
+ const featuresDone = features.filter((d) => DONE_STATUSES5.has(d.frontmatter.status)).length;
19037
+ const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
19038
+ const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
19039
+ const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
18978
19040
  const questionsOpen = questions.filter((d) => d.frontmatter.status === "open").length;
18979
19041
  const statsCards = `
18980
19042
  <div class="cards">
@@ -19001,7 +19063,7 @@ function poDashboardPage(ctx) {
19001
19063
  </div>
19002
19064
  <div class="card">
19003
19065
  <a href="/po/delivery">
19004
- <div class="card-label">Sprint</div>
19066
+ <div class="card-label">Current Sprint</div>
19005
19067
  <div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
19006
19068
  <div class="card-sub">${sprintData ? `${sprintData.workItems.done}/${sprintData.workItems.total} items` : "No active sprint"}</div>
19007
19069
  </a>
@@ -19037,43 +19099,72 @@ function poDashboardPage(ctx) {
19037
19099
  </div>`,
19038
19100
  { titleTag: "h3" }
19039
19101
  ) : "";
19040
- function signalTagClass(points) {
19041
- if (points >= 15) return "signal-tag signal-tag-high";
19042
- if (points >= 8) return "signal-tag signal-tag-medium";
19043
- return "signal-tag signal-tag-positive";
19044
- }
19045
- const trendingSection = upcoming.trending.length > 0 ? collapsibleSection(
19046
- "po-trending",
19047
- "Trending Items",
19102
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19103
+ const atRiskItems = [];
19104
+ for (const f of features) {
19105
+ if (DONE_STATUSES5.has(f.frontmatter.status)) continue;
19106
+ const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
19107
+ const reasons = [];
19108
+ let blocked = 0;
19109
+ for (const epic of fEpics) {
19110
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
19111
+ if (t.frontmatter.status === "blocked") blocked++;
19112
+ }
19113
+ }
19114
+ if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
19115
+ for (const epic of fEpics) {
19116
+ const td = epic.frontmatter.targetDate;
19117
+ if (td && td < today && !DONE_STATUSES5.has(epic.frontmatter.status)) {
19118
+ reasons.push(`${epic.frontmatter.id} overdue`);
19119
+ }
19120
+ }
19121
+ let totalTasks = 0;
19122
+ let progressSum = 0;
19123
+ for (const epic of fEpics) {
19124
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
19125
+ totalTasks++;
19126
+ progressSum += getEffectiveProgress(t.frontmatter);
19127
+ }
19128
+ }
19129
+ const avgProgress = totalTasks > 0 ? Math.round(progressSum / totalTasks) : 0;
19130
+ if (avgProgress < 30 && sprintTimelinePct > 60 && totalTasks > 0) {
19131
+ reasons.push("Low progress vs sprint timeline");
19132
+ }
19133
+ if (reasons.length > 0) atRiskItems.push({ feature: f, reasons });
19134
+ }
19135
+ const atRiskSection = atRiskItems.length > 0 ? collapsibleSection(
19136
+ "po-at-risk",
19137
+ `At-Risk Delivery (${atRiskItems.length})`,
19048
19138
  `<div class="table-wrap">
19049
19139
  <table>
19050
19140
  <thead>
19051
- <tr><th>ID</th><th>Title</th><th>Score</th></tr>
19141
+ <tr><th>Feature</th><th>Risk Reasons</th></tr>
19052
19142
  </thead>
19053
19143
  <tbody>
19054
- ${upcoming.trending.slice(0, 8).map((t) => {
19055
- const tags = t.signals.map((s) => `<span class="${signalTagClass(s.points)}" title="${escapeHtml(s.factor)}: +${s.points}pts">${escapeHtml(s.factor)}</span>`).join("");
19056
- return `
19144
+ ${atRiskItems.map((r) => `
19057
19145
  <tr>
19058
- <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
19059
- <td>${escapeHtml(t.title)}<br>${tags}</td>
19060
- <td><span class="trending-score">${t.score}</span></td>
19061
- </tr>`;
19062
- }).join("")}
19146
+ <td><a href="/docs/feature/${escapeHtml(r.feature.frontmatter.id)}">${escapeHtml(r.feature.frontmatter.id)}</a> ${escapeHtml(r.feature.frontmatter.title)}</td>
19147
+ <td>${r.reasons.map((reason) => `<span class="signal-tag signal-tag-high">${escapeHtml(reason)}</span>`).join(" ")}</td>
19148
+ </tr>`).join("")}
19063
19149
  </tbody>
19064
19150
  </table>
19065
19151
  </div>`,
19066
19152
  { titleTag: "h3" }
19067
- ) : "";
19153
+ ) : collapsibleSection(
19154
+ "po-at-risk",
19155
+ "At-Risk Delivery",
19156
+ '<div class="empty"><p style="color: var(--green);">No at-risk items \u2014 all features on track.</p></div>',
19157
+ { titleTag: "h3", defaultCollapsed: true }
19158
+ );
19068
19159
  return `
19069
19160
  <div class="page-header">
19070
19161
  <h2>Product Owner Dashboard</h2>
19071
19162
  <div class="subtitle">Feature delivery, decisions, and stakeholder alignment</div>
19072
19163
  </div>
19073
19164
  ${statsCards}
19165
+ ${atRiskSection}
19074
19166
  ${diagramSection}
19075
19167
  ${recentTable}
19076
- ${trendingSection}
19077
19168
  `;
19078
19169
  }
19079
19170
 
@@ -19252,44 +19343,74 @@ function tableDateFilter(tableId, colIndex) {
19252
19343
  }
19253
19344
 
19254
19345
  // src/web/templates/pages/po/backlog.ts
19346
+ function priorityClass(p) {
19347
+ if (!p) return "";
19348
+ const lower = p.toLowerCase();
19349
+ if (lower === "critical" || lower === "high") return " priority-high";
19350
+ if (lower === "medium") return " priority-medium";
19351
+ if (lower === "low") return " priority-low";
19352
+ return "";
19353
+ }
19354
+ function miniProgressBar(pct) {
19355
+ return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
19356
+ }
19255
19357
  function poBacklogPage(ctx) {
19256
19358
  const features = ctx.store.list({ type: "feature" });
19257
19359
  const questions = ctx.store.list({ type: "question" });
19258
19360
  const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
19259
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
19260
- const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
19361
+ const priorityOrder2 = { critical: 0, high: 1, medium: 2, low: 3 };
19362
+ const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
19261
19363
  const sortedFeatures = [...features].sort((a, b) => {
19262
- const sa = statusOrder[a.frontmatter.status] ?? 3;
19263
- const sb = statusOrder[b.frontmatter.status] ?? 3;
19364
+ const sa = statusOrder2[a.frontmatter.status] ?? 3;
19365
+ const sb = statusOrder2[b.frontmatter.status] ?? 3;
19264
19366
  if (sa !== sb) return sa - sb;
19265
- const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
19266
- const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
19367
+ const pa = priorityOrder2[a.frontmatter.priority?.toLowerCase()] ?? 99;
19368
+ const pb = priorityOrder2[b.frontmatter.priority?.toLowerCase()] ?? 99;
19267
19369
  if (pa !== pb) return pa - pb;
19268
19370
  return a.frontmatter.id.localeCompare(b.frontmatter.id);
19269
19371
  });
19270
19372
  const epics = ctx.store.list({ type: "epic" });
19271
19373
  const featureToEpics = /* @__PURE__ */ new Map();
19272
19374
  for (const epic of epics) {
19273
- const linked = epic.frontmatter.linkedFeature;
19274
- const featureIds = Array.isArray(linked) ? linked : linked ? [linked] : [];
19375
+ const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
19275
19376
  for (const fid of featureIds) {
19276
- const existing = featureToEpics.get(String(fid)) ?? [];
19277
- existing.push(epic.frontmatter.id);
19278
- featureToEpics.set(String(fid), existing);
19377
+ const arr = featureToEpics.get(fid) ?? [];
19378
+ arr.push(epic);
19379
+ featureToEpics.set(fid, arr);
19279
19380
  }
19280
19381
  }
19281
- function priorityClass(p) {
19282
- if (!p) return "";
19283
- const lower = p.toLowerCase();
19284
- if (lower === "critical" || lower === "high") return " priority-high";
19285
- if (lower === "medium") return " priority-medium";
19286
- if (lower === "low") return " priority-low";
19287
- return "";
19382
+ const allTasks = ctx.store.list({ type: "task" });
19383
+ const epicToTasks = /* @__PURE__ */ new Map();
19384
+ for (const task of allTasks) {
19385
+ const tags = task.frontmatter.tags ?? [];
19386
+ for (const tag of tags) {
19387
+ if (tag.startsWith("epic:")) {
19388
+ const epicId = tag.slice(5);
19389
+ const arr = epicToTasks.get(epicId) ?? [];
19390
+ arr.push(task);
19391
+ epicToTasks.set(epicId, arr);
19392
+ }
19393
+ }
19394
+ }
19395
+ const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19396
+ function featureTaskStats(featureId) {
19397
+ const fEpics = featureToEpics.get(featureId) ?? [];
19398
+ let total = 0;
19399
+ let done = 0;
19400
+ let progressSum = 0;
19401
+ for (const epic of fEpics) {
19402
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
19403
+ total++;
19404
+ if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
19405
+ progressSum += getEffectiveProgress(t.frontmatter);
19406
+ }
19407
+ }
19408
+ return { epicCount: fEpics.length, total, done, avgProgress: total > 0 ? Math.round(progressSum / total) : 0 };
19288
19409
  }
19289
19410
  const featureStatuses = [...new Set(features.map((d) => d.frontmatter.status))].sort();
19290
19411
  const featurePriorities = [...new Set(features.map((d) => d.frontmatter.priority ?? "").filter(Boolean))].sort();
19291
19412
  const featureEpicIds = [...new Set(
19292
- features.flatMap((d) => featureToEpics.get(d.frontmatter.id) ?? [])
19413
+ features.flatMap((d) => (featureToEpics.get(d.frontmatter.id) ?? []).map((e) => e.frontmatter.id))
19293
19414
  )].sort();
19294
19415
  const featuresFilters = `<div class="filters">
19295
19416
  ${tableFilter("features-table", 2, "Status", featureStatuses)}
@@ -19300,12 +19421,13 @@ function poBacklogPage(ctx) {
19300
19421
  <div class="table-wrap table-short">
19301
19422
  <table id="features-table">
19302
19423
  <thead>
19303
- <tr>${sortableTh("ID", "features-table", 0)}${sortableTh("Title", "features-table", 1)}${sortableTh("Status", "features-table", 2)}${sortableTh("Priority", "features-table", 3)}<th>Linked Epics</th></tr>
19424
+ <tr>${sortableTh("ID", "features-table", 0)}${sortableTh("Title", "features-table", 1)}${sortableTh("Status", "features-table", 2)}${sortableTh("Priority", "features-table", 3)}<th>Epics</th><th>Tasks</th><th>Progress</th></tr>
19304
19425
  </thead>
19305
19426
  <tbody>
19306
19427
  ${sortedFeatures.map((d) => {
19307
- const linkedEpics = featureToEpics.get(d.frontmatter.id) ?? [];
19308
- const epicLinks = linkedEpics.map((eid) => `<a href="/docs/epic/${escapeHtml(eid)}">${escapeHtml(eid)}</a>`).join(", ");
19428
+ const stats = featureTaskStats(d.frontmatter.id);
19429
+ const linkedEpicDocs = featureToEpics.get(d.frontmatter.id) ?? [];
19430
+ const epicLinks = linkedEpicDocs.map((e) => `<a href="/docs/epic/${escapeHtml(e.frontmatter.id)}">${escapeHtml(e.frontmatter.id)}</a>`).join(", ");
19309
19431
  return `
19310
19432
  <tr>
19311
19433
  <td><a href="/docs/feature/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
@@ -19313,6 +19435,8 @@ function poBacklogPage(ctx) {
19313
19435
  <td>${statusBadge(d.frontmatter.status)}</td>
19314
19436
  <td><span class="${priorityClass(d.frontmatter.priority)}">${escapeHtml(d.frontmatter.priority ?? "\u2014")}</span></td>
19315
19437
  <td>${epicLinks || '<span class="text-dim">\u2014</span>'}</td>
19438
+ <td>${stats.total > 0 ? `${stats.done}/${stats.total}` : '<span class="text-dim">\u2014</span>'}</td>
19439
+ <td>${stats.total > 0 ? miniProgressBar(stats.avgProgress) : '<span class="text-dim">\u2014</span>'}</td>
19316
19440
  </tr>`;
19317
19441
  }).join("")}
19318
19442
  </tbody>
@@ -19354,14 +19478,23 @@ function poBacklogPage(ctx) {
19354
19478
 
19355
19479
  // src/web/templates/pages/po/decisions.ts
19356
19480
  var RESOLVED_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
19481
+ var KNOWN_OWNERS = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
19482
+ function ownerBadge(owner) {
19483
+ if (!owner) return '<span class="text-dim">\u2014</span>';
19484
+ const cls = KNOWN_OWNERS.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
19485
+ return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
19486
+ }
19357
19487
  function poDecisionsPage(ctx) {
19358
19488
  const decisions = ctx.store.list({ type: "decision" });
19489
+ const questions = ctx.store.list({ type: "question" });
19490
+ const features = ctx.store.list({ type: "feature" });
19359
19491
  const openDecisions = decisions.filter((d) => !RESOLVED_STATUSES.has(d.frontmatter.status));
19360
19492
  const resolvedDecisions = decisions.filter((d) => RESOLVED_STATUSES.has(d.frontmatter.status));
19493
+ const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
19361
19494
  const statsCards = `
19362
19495
  <div class="cards">
19363
19496
  <div class="card">
19364
- <div class="card-label">Open</div>
19497
+ <div class="card-label">Open Decisions</div>
19365
19498
  <div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
19366
19499
  <div class="card-sub">awaiting resolution</div>
19367
19500
  </div>
@@ -19370,12 +19503,79 @@ function poDecisionsPage(ctx) {
19370
19503
  <div class="card-value">${resolvedDecisions.length}</div>
19371
19504
  <div class="card-sub">decisions made</div>
19372
19505
  </div>
19506
+ <div class="card">
19507
+ <div class="card-label">Open Questions</div>
19508
+ <div class="card-value${openQuestions.length > 0 ? " priority-medium" : ""}">${openQuestions.length}</div>
19509
+ <div class="card-sub">needing answers</div>
19510
+ </div>
19373
19511
  <div class="card">
19374
19512
  <div class="card-label">Total</div>
19375
19513
  <div class="card-value">${decisions.length}</div>
19376
19514
  <div class="card-sub">all decisions</div>
19377
19515
  </div>
19378
19516
  </div>`;
19517
+ function daysSince(isoDate) {
19518
+ if (!isoDate) return 0;
19519
+ return Math.max(0, Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5));
19520
+ }
19521
+ const featureGroups = /* @__PURE__ */ new Map();
19522
+ const unlinked = [];
19523
+ function addToGroup(doc, docType) {
19524
+ const tags = doc.frontmatter.tags ?? [];
19525
+ const featureTags = tags.filter((t) => t.startsWith("feature:")).map((t) => t.slice(8));
19526
+ const item = { doc, docType, ageDays: daysSince(doc.frontmatter.created) };
19527
+ if (featureTags.length === 0) {
19528
+ unlinked.push(item);
19529
+ } else {
19530
+ for (const fid of featureTags) {
19531
+ const arr = featureGroups.get(fid) ?? [];
19532
+ arr.push(item);
19533
+ featureGroups.set(fid, arr);
19534
+ }
19535
+ }
19536
+ }
19537
+ for (const d of openDecisions) addToGroup(d, "decision");
19538
+ for (const q of openQuestions) addToGroup(q, "question");
19539
+ const totalDeps = openDecisions.length + openQuestions.length;
19540
+ const featureLookup = new Map(features.map((f) => [f.frontmatter.id, f]));
19541
+ function renderDepRows(items) {
19542
+ return items.map((item) => `
19543
+ <tr>
19544
+ <td><a href="/docs/${item.docType}/${escapeHtml(item.doc.frontmatter.id)}">${escapeHtml(item.doc.frontmatter.id)}</a></td>
19545
+ <td>${escapeHtml(item.doc.frontmatter.title)}</td>
19546
+ <td>${escapeHtml(typeLabel(item.docType))}</td>
19547
+ <td>${ownerBadge(item.doc.frontmatter.owner)}</td>
19548
+ <td>${item.ageDays}d</td>
19549
+ </tr>`).join("");
19550
+ }
19551
+ let depRows = "";
19552
+ for (const [fid, items] of featureGroups) {
19553
+ const feat = featureLookup.get(fid);
19554
+ const label = feat ? `${escapeHtml(fid)}: ${escapeHtml(feat.frontmatter.title)}` : escapeHtml(fid);
19555
+ depRows += `
19556
+ <tr class="group-header-row"><td colspan="5"><strong>${label}</strong></td></tr>
19557
+ ${renderDepRows(items)}`;
19558
+ }
19559
+ if (unlinked.length > 0) {
19560
+ depRows += `
19561
+ <tr class="group-header-row"><td colspan="5"><strong>Unlinked</strong></td></tr>
19562
+ ${renderDepRows(unlinked)}`;
19563
+ }
19564
+ const depsSection = totalDeps > 0 ? collapsibleSection(
19565
+ "po-decisions-deps",
19566
+ `By Feature (${totalDeps})`,
19567
+ `<div class="table-wrap">
19568
+ <table>
19569
+ <thead>
19570
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Owner</th><th>Age</th></tr>
19571
+ </thead>
19572
+ <tbody>
19573
+ ${depRows}
19574
+ </tbody>
19575
+ </table>
19576
+ </div>`,
19577
+ { titleTag: "h3" }
19578
+ ) : "";
19379
19579
  function decisionTable(docs, tableId) {
19380
19580
  if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
19381
19581
  const statuses = [...new Set(docs.map((d) => d.frontmatter.status))].sort();
@@ -19397,7 +19597,7 @@ function poDecisionsPage(ctx) {
19397
19597
  <td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
19398
19598
  <td>${escapeHtml(d.frontmatter.title)}</td>
19399
19599
  <td>${statusBadge(d.frontmatter.status)}</td>
19400
- <td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
19600
+ <td>${ownerBadge(d.frontmatter.owner)}</td>
19401
19601
  <td>${formatDate(d.frontmatter.created)}</td>
19402
19602
  </tr>`).join("")}
19403
19603
  </tbody>
@@ -19419,16 +19619,185 @@ function poDecisionsPage(ctx) {
19419
19619
  return `
19420
19620
  <div class="page-header">
19421
19621
  <h2>Decision Log</h2>
19422
- <div class="subtitle">Track and manage product decisions</div>
19622
+ <div class="subtitle">Track and manage product decisions and dependencies</div>
19423
19623
  </div>
19424
19624
  ${statsCards}
19625
+ ${depsSection}
19425
19626
  ${openSection}
19426
19627
  ${resolvedSection}
19427
19628
  ${renderTableUtilsScript()}
19428
19629
  `;
19429
19630
  }
19430
19631
 
19632
+ // src/web/templates/components/work-items-table.ts
19633
+ var FOCUS_BORDER_PALETTE = [
19634
+ "hsl(220, 60%, 55%)",
19635
+ "hsl(160, 50%, 45%)",
19636
+ "hsl(280, 45%, 55%)",
19637
+ "hsl(30, 65%, 55%)",
19638
+ "hsl(340, 50%, 55%)",
19639
+ "hsl(190, 50%, 45%)",
19640
+ "hsl(60, 50%, 50%)",
19641
+ "hsl(120, 40%, 45%)"
19642
+ ];
19643
+ function hashString(s) {
19644
+ let h = 0;
19645
+ for (let i = 0; i < s.length; i++) {
19646
+ h = (h << 5) - h + s.charCodeAt(i) | 0;
19647
+ }
19648
+ return Math.abs(h);
19649
+ }
19650
+ function countFocusStats(items) {
19651
+ let total = 0;
19652
+ let done = 0;
19653
+ let inProgress = 0;
19654
+ function walk(list) {
19655
+ for (const w of list) {
19656
+ if (w.type !== "contribution") {
19657
+ total++;
19658
+ const s = w.status.toLowerCase();
19659
+ if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
19660
+ else if (s === "in-progress" || s === "in progress") inProgress++;
19661
+ }
19662
+ if (w.children) walk(w.children);
19663
+ }
19664
+ }
19665
+ walk(items);
19666
+ return { total, done, inProgress };
19667
+ }
19668
+ var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
19669
+ function ownerBadge2(owner) {
19670
+ if (!owner) return '<span class="text-dim">\u2014</span>';
19671
+ const cls = KNOWN_OWNERS2.has(owner) ? `owner-badge-${owner}` : "owner-badge-other";
19672
+ return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
19673
+ }
19674
+ function renderItemRows(items, borderColor, showOwner, depth = 0) {
19675
+ return items.flatMap((w) => {
19676
+ const isChild = depth > 0;
19677
+ const isContribution = w.type === "contribution";
19678
+ const classes = ["focus-row"];
19679
+ if (isContribution) classes.push("contribution-row");
19680
+ else if (isChild) classes.push("child-row");
19681
+ const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
19682
+ const progressCell = !isContribution && w.progress !== void 0 ? `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${w.progress}%"></div><span class="mini-progress-label">${w.progress}%</span></div>` : "";
19683
+ const ownerCell = showOwner ? `<td>${ownerBadge2(w.owner)}</td>` : "";
19684
+ const row = `
19685
+ <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
19686
+ <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
19687
+ <td>${escapeHtml(w.title)}</td>
19688
+ ${ownerCell}
19689
+ <td>${statusBadge(w.status)}</td>
19690
+ <td>${progressCell}</td>
19691
+ </tr>`;
19692
+ const childRows = w.children ? renderItemRows(w.children, borderColor, showOwner, depth + 1) : [];
19693
+ return [row, ...childRows];
19694
+ });
19695
+ }
19696
+ function renderWorkItemsTable(items, options) {
19697
+ const sectionId = options?.sectionId ?? "work-items";
19698
+ const title = options?.title ?? "Work Items";
19699
+ const defaultCollapsed = options?.defaultCollapsed ?? false;
19700
+ const showOwner = options?.showOwner ?? false;
19701
+ const focusGroups = /* @__PURE__ */ new Map();
19702
+ for (const item of items) {
19703
+ const focus = item.workFocus ?? "Unassigned";
19704
+ if (!focusGroups.has(focus)) focusGroups.set(focus, []);
19705
+ focusGroups.get(focus).push(item);
19706
+ }
19707
+ const focusColorMap = /* @__PURE__ */ new Map();
19708
+ for (const name of focusGroups.keys()) {
19709
+ focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
19710
+ }
19711
+ const allWorkItemRows = [];
19712
+ for (const [focus, groupItems] of focusGroups) {
19713
+ const color = focusColorMap.get(focus);
19714
+ const stats = countFocusStats(groupItems);
19715
+ const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
19716
+ const summaryParts = [];
19717
+ if (stats.done > 0) summaryParts.push(`${stats.done} done`);
19718
+ if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
19719
+ const remaining = stats.total - stats.done - stats.inProgress;
19720
+ if (remaining > 0) summaryParts.push(`${remaining} open`);
19721
+ const leftColspan = showOwner ? 3 : 2;
19722
+ allWorkItemRows.push(`
19723
+ <tr class="focus-group-header" style="--focus-color: ${color}">
19724
+ <td colspan="${leftColspan}">
19725
+ <span class="focus-group-name">${escapeHtml(focus)}</span>
19726
+ <span class="focus-group-stats">${summaryParts.join(" / ")}</span>
19727
+ </td>
19728
+ <td colspan="2">
19729
+ <div class="mini-progress-bar focus-group-progress"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>
19730
+ </td>
19731
+ </tr>`);
19732
+ allWorkItemRows.push(...renderItemRows(groupItems, color, showOwner));
19733
+ }
19734
+ if (allWorkItemRows.length === 0) return "";
19735
+ const ownerHeader = showOwner ? "<th>Owner</th>" : "";
19736
+ return collapsibleSection(
19737
+ sectionId,
19738
+ title,
19739
+ `<div class="table-wrap">
19740
+ <table id="${sectionId}-table">
19741
+ <thead>
19742
+ <tr>
19743
+ <th>ID</th>
19744
+ <th>Title</th>
19745
+ ${ownerHeader}
19746
+ <th>Status</th>
19747
+ <th>Progress</th>
19748
+ </tr>
19749
+ </thead>
19750
+ <tbody>
19751
+ ${allWorkItemRows.join("")}
19752
+ </tbody>
19753
+ </table>
19754
+ </div>`,
19755
+ { titleTag: "h3", defaultCollapsed }
19756
+ );
19757
+ }
19758
+ var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
19759
+ function computeOwnerCompletionPct(items, owner) {
19760
+ let total = 0;
19761
+ let progressSum = 0;
19762
+ function walk(list) {
19763
+ for (const w of list) {
19764
+ if (w.type !== "contribution" && w.owner === owner) {
19765
+ total++;
19766
+ progressSum += w.progress ?? (DONE_STATUSES6.has(w.status) ? 100 : 0);
19767
+ }
19768
+ if (w.children) walk(w.children);
19769
+ }
19770
+ }
19771
+ walk(items);
19772
+ return total > 0 ? Math.round(progressSum / total) : 0;
19773
+ }
19774
+ function filterItemsByOwner(items, owner) {
19775
+ const result = [];
19776
+ for (const item of items) {
19777
+ if (item.owner === owner) {
19778
+ result.push(item);
19779
+ } else if (item.children) {
19780
+ result.push(...filterItemsByOwner(item.children, owner));
19781
+ }
19782
+ }
19783
+ return result;
19784
+ }
19785
+
19431
19786
  // src/web/templates/pages/po/delivery.ts
19787
+ var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19788
+ var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
19789
+ var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
19790
+ function priorityClass2(p) {
19791
+ if (!p) return "";
19792
+ const lower = p.toLowerCase();
19793
+ if (lower === "critical" || lower === "high") return " priority-high";
19794
+ if (lower === "medium") return " priority-medium";
19795
+ if (lower === "low") return " priority-low";
19796
+ return "";
19797
+ }
19798
+ function miniProgressBar2(pct) {
19799
+ return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
19800
+ }
19432
19801
  var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
19433
19802
  "stakeholder-feedback",
19434
19803
  "acceptance-result",
@@ -19454,24 +19823,8 @@ function poDeliveryPage(ctx) {
19454
19823
  <p>No active sprint found. Create a sprint and set its status to "active" to track delivery.</p>
19455
19824
  </div>`;
19456
19825
  }
19457
- const doneFeatures = data.workItems.items.filter(
19458
- (w) => w.type === "feature" && ["done", "closed", "resolved"].includes(w.status)
19459
- );
19460
- function findContributions(items, parentId) {
19461
- const result = [];
19462
- for (const item of items) {
19463
- if (item.type === "contribution" && PO_CONTRIBUTION_TYPES.has(item.id.split("-").slice(0, -1).join("-") || "")) {
19464
- result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
19465
- }
19466
- if (PO_CONTRIBUTION_TYPES.has(item.type)) {
19467
- result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
19468
- }
19469
- if (item.children) {
19470
- result.push(...findContributions(item.children, item.id));
19471
- }
19472
- }
19473
- return result;
19474
- }
19826
+ const poItems = filterItemsByOwner(data.workItems.items, "po");
19827
+ const poCompletionPct = computeOwnerCompletionPct(data.workItems.items, "po");
19475
19828
  const allDocs = ctx.store.list();
19476
19829
  const poContributions = allDocs.filter((d) => PO_CONTRIBUTION_TYPES.has(d.frontmatter.type));
19477
19830
  const statsCards = `
@@ -19481,16 +19834,16 @@ function poDeliveryPage(ctx) {
19481
19834
  <div class="card-value">${data.workItems.completionPct}%</div>
19482
19835
  <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
19483
19836
  </div>
19837
+ <div class="card">
19838
+ <div class="card-label">PO Completion</div>
19839
+ <div class="card-value">${poCompletionPct}%</div>
19840
+ <div class="card-sub">${poItems.length} owned items</div>
19841
+ </div>
19484
19842
  <div class="card">
19485
19843
  <div class="card-label">Days Remaining</div>
19486
19844
  <div class="card-value">${data.timeline.daysRemaining}</div>
19487
19845
  <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
19488
19846
  </div>
19489
- <div class="card">
19490
- <div class="card-label">Features Done</div>
19491
- <div class="card-value">${doneFeatures.length}</div>
19492
- <div class="card-sub">this sprint</div>
19493
- </div>
19494
19847
  <div class="card">
19495
19848
  <div class="card-label">PO Contributions</div>
19496
19849
  <div class="card-value">${poContributions.length}</div>
@@ -19502,7 +19855,11 @@ function poDeliveryPage(ctx) {
19502
19855
  <strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
19503
19856
  ${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
19504
19857
  </div>`;
19505
- const featuresSection = data.linkedEpics.length > 0 ? collapsibleSection(
19858
+ const workItemsSection = renderWorkItemsTable(poItems, {
19859
+ sectionId: "po-delivery-items",
19860
+ title: "PO Work Items"
19861
+ });
19862
+ const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
19506
19863
  "po-delivery-epics",
19507
19864
  "Linked Epics",
19508
19865
  `<div class="table-wrap">
@@ -19523,6 +19880,82 @@ function poDeliveryPage(ctx) {
19523
19880
  </div>`,
19524
19881
  { titleTag: "h3" }
19525
19882
  ) : "";
19883
+ const features = ctx.store.list({ type: "feature" });
19884
+ const epics = ctx.store.list({ type: "epic" });
19885
+ const allTasks = ctx.store.list({ type: "task" });
19886
+ const sprints = ctx.store.list({ type: "sprint" });
19887
+ const featureToEpics = /* @__PURE__ */ new Map();
19888
+ for (const epic of epics) {
19889
+ const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
19890
+ for (const fid of featureIds) {
19891
+ const arr = featureToEpics.get(fid) ?? [];
19892
+ arr.push(epic);
19893
+ featureToEpics.set(fid, arr);
19894
+ }
19895
+ }
19896
+ const epicToTasks = /* @__PURE__ */ new Map();
19897
+ for (const task of allTasks) {
19898
+ const tags = task.frontmatter.tags ?? [];
19899
+ for (const tag of tags) {
19900
+ if (tag.startsWith("epic:")) {
19901
+ const arr = epicToTasks.get(tag.slice(5)) ?? [];
19902
+ arr.push(task);
19903
+ epicToTasks.set(tag.slice(5), arr);
19904
+ }
19905
+ }
19906
+ }
19907
+ const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
19908
+ const activeSprintEpicIds = new Set(
19909
+ activeSprint ? normalizeLinkedEpics(activeSprint.frontmatter.linkedEpics) : []
19910
+ );
19911
+ function featureSprintLabel(featureId) {
19912
+ if (!activeSprint) return "\u2014";
19913
+ const fEpics = featureToEpics.get(featureId) ?? [];
19914
+ return fEpics.some((e) => activeSprintEpicIds.has(e.frontmatter.id)) ? escapeHtml(activeSprint.frontmatter.id) : "\u2014";
19915
+ }
19916
+ function featureProgress(featureId) {
19917
+ const fEpics = featureToEpics.get(featureId) ?? [];
19918
+ let total = 0;
19919
+ let progressSum = 0;
19920
+ for (const epic of fEpics) {
19921
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
19922
+ total++;
19923
+ progressSum += getEffectiveProgress(t.frontmatter);
19924
+ }
19925
+ }
19926
+ return total > 0 ? Math.round(progressSum / total) : 0;
19927
+ }
19928
+ const nonDoneFeatures = features.filter((f) => !DONE_STATUSES7.has(f.frontmatter.status)).sort((a, b) => {
19929
+ const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
19930
+ const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
19931
+ if (pa !== pb) return pa - pb;
19932
+ const sa = statusOrder[a.frontmatter.status] ?? 3;
19933
+ const sb = statusOrder[b.frontmatter.status] ?? 3;
19934
+ return sa - sb;
19935
+ });
19936
+ const priorityQueueSection = collapsibleSection(
19937
+ "po-priority-queue",
19938
+ `Priority Queue (${nonDoneFeatures.length})`,
19939
+ nonDoneFeatures.length > 0 ? `<div class="table-wrap">
19940
+ <table>
19941
+ <thead>
19942
+ <tr><th>Priority</th><th>ID</th><th>Title</th><th>Status</th><th>Sprint</th><th>Progress</th></tr>
19943
+ </thead>
19944
+ <tbody>
19945
+ ${nonDoneFeatures.map((f) => `
19946
+ <tr>
19947
+ <td><span class="${priorityClass2(f.frontmatter.priority)}">${escapeHtml(f.frontmatter.priority ?? "\u2014")}</span></td>
19948
+ <td><a href="/docs/feature/${escapeHtml(f.frontmatter.id)}">${escapeHtml(f.frontmatter.id)}</a></td>
19949
+ <td>${escapeHtml(f.frontmatter.title)}</td>
19950
+ <td>${statusBadge(f.frontmatter.status)}</td>
19951
+ <td>${featureSprintLabel(f.frontmatter.id)}</td>
19952
+ <td>${miniProgressBar2(featureProgress(f.frontmatter.id))}</td>
19953
+ </tr>`).join("")}
19954
+ </tbody>
19955
+ </table>
19956
+ </div>` : '<div class="empty"><p>No active features in the queue.</p></div>',
19957
+ { titleTag: "h3" }
19958
+ );
19526
19959
  const contributionsSection = poContributions.length > 0 ? collapsibleSection(
19527
19960
  "po-delivery-contributions",
19528
19961
  `PO Contributions (${poContributions.length})`,
@@ -19553,7 +19986,9 @@ function poDeliveryPage(ctx) {
19553
19986
  ${sprintHeader}
19554
19987
  ${progressBar(data.workItems.completionPct)}
19555
19988
  ${statsCards}
19556
- ${featuresSection}
19989
+ ${workItemsSection}
19990
+ ${epicsSection}
19991
+ ${priorityQueueSection}
19557
19992
  ${contributionsSection}
19558
19993
  `;
19559
19994
  }
@@ -19579,6 +20014,12 @@ function renderGarWidget(report) {
19579
20014
  }
19580
20015
 
19581
20016
  // src/web/templates/pages/po/stakeholders.ts
20017
+ var KNOWN_OWNERS3 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
20018
+ function ownerBadge3(owner) {
20019
+ if (!owner) return '<span class="text-dim">\u2014</span>';
20020
+ const cls = KNOWN_OWNERS3.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
20021
+ return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
20022
+ }
19582
20023
  function poStakeholdersPage(ctx) {
19583
20024
  const garReport = getGarData(ctx.store, ctx.projectName);
19584
20025
  const actions = ctx.store.list({ type: "action" });
@@ -19629,7 +20070,7 @@ function poStakeholdersPage(ctx) {
19629
20070
  <td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
19630
20071
  <td>${escapeHtml(d.frontmatter.title)}</td>
19631
20072
  <td>${statusBadge(d.frontmatter.status)}</td>
19632
- <td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
20073
+ <td>${ownerBadge3(d.frontmatter.owner)}</td>
19633
20074
  <td>${d.frontmatter.dueDate ? formatDate(d.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
19634
20075
  </tr>`).join("")}
19635
20076
  </tbody>
@@ -19652,7 +20093,7 @@ function poStakeholdersPage(ctx) {
19652
20093
  <tr>
19653
20094
  <td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
19654
20095
  <td>${escapeHtml(d.frontmatter.title)}</td>
19655
- <td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
20096
+ <td>${ownerBadge3(d.frontmatter.owner)}</td>
19656
20097
  <td>${formatDate(d.frontmatter.created)}</td>
19657
20098
  </tr>`).join("")}
19658
20099
  </tbody>
@@ -19693,7 +20134,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
19693
20134
  registerPersonaPage("po", "stakeholders", poStakeholdersPage);
19694
20135
 
19695
20136
  // src/web/templates/pages/dm/dashboard.ts
19696
- var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20137
+ var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19697
20138
  function progressBar2(pct) {
19698
20139
  return `<div class="sprint-progress-bar">
19699
20140
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
@@ -19704,7 +20145,7 @@ function dmDashboardPage(ctx) {
19704
20145
  const sprintData = getSprintSummaryData(ctx.store);
19705
20146
  const upcoming = getUpcomingData(ctx.store);
19706
20147
  const actions = ctx.store.list({ type: "action" });
19707
- const openActions = actions.filter((d) => !DONE_STATUSES5.has(d.frontmatter.status));
20148
+ const openActions = actions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
19708
20149
  const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
19709
20150
  const statsCards = `
19710
20151
  <div class="cards">
@@ -19791,42 +20232,46 @@ function dmDashboardPage(ctx) {
19791
20232
  `;
19792
20233
  }
19793
20234
 
19794
- // src/web/templates/pages/sprint-summary.ts
20235
+ // src/web/templates/pages/dm/sprint.ts
19795
20236
  function progressBar3(pct) {
19796
20237
  return `<div class="sprint-progress-bar">
19797
20238
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
19798
20239
  <span class="sprint-progress-label">${pct}%</span>
19799
20240
  </div>`;
19800
20241
  }
19801
- function sprintSummaryPage(data, cached2) {
20242
+ function dmSprintPage(ctx) {
20243
+ const data = getSprintSummaryData(ctx.store);
19802
20244
  if (!data) {
19803
20245
  return `
19804
20246
  <div class="page-header">
19805
- <h2>Sprint Summary</h2>
19806
- <div class="subtitle">AI-powered sprint narrative</div>
20247
+ <h2>Sprint Execution</h2>
20248
+ <div class="subtitle">Full sprint oversight and delivery tracking</div>
19807
20249
  </div>
19808
20250
  <div class="empty">
19809
20251
  <h3>No Active Sprint</h3>
19810
- <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
20252
+ <p>No active sprint found. Create a sprint and set its status to "active" to track execution.</p>
19811
20253
  </div>`;
19812
20254
  }
20255
+ const dmCompletionPct = computeOwnerCompletionPct(data.workItems.items, "dm");
20256
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
20257
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
19813
20258
  const statsCards = `
19814
20259
  <div class="cards">
19815
20260
  <div class="card">
19816
- <div class="card-label">Completion</div>
20261
+ <div class="card-label">Sprint Completion</div>
19817
20262
  <div class="card-value">${data.workItems.completionPct}%</div>
19818
20263
  <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
19819
20264
  </div>
20265
+ <div class="card">
20266
+ <div class="card-label">DM Completion</div>
20267
+ <div class="card-value">${dmCompletionPct}%</div>
20268
+ <div class="card-sub">DM-owned items</div>
20269
+ </div>
19820
20270
  <div class="card">
19821
20271
  <div class="card-label">Days Remaining</div>
19822
20272
  <div class="card-value">${data.timeline.daysRemaining}</div>
19823
20273
  <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
19824
20274
  </div>
19825
- <div class="card">
19826
- <div class="card-label">Epics</div>
19827
- <div class="card-value">${data.linkedEpics.length}</div>
19828
- <div class="card-sub">linked to sprint</div>
19829
- </div>
19830
20275
  <a class="card card-link" href="sprint-blockers">
19831
20276
  <div class="card-label">Blockers</div>
19832
20277
  <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
@@ -19838,13 +20283,18 @@ function sprintSummaryPage(data, cached2) {
19838
20283
  <div class="card-sub">open risk items</div>
19839
20284
  </a>
19840
20285
  </div>`;
19841
- const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
19842
- "ss-epics",
20286
+ const workItemsSection = renderWorkItemsTable(data.workItems.items, {
20287
+ sectionId: "dm-sprint-items",
20288
+ title: "Sprint Work Items",
20289
+ showOwner: true
20290
+ });
20291
+ const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
20292
+ "dm-sprint-epics",
19843
20293
  "Linked Epics",
19844
20294
  `<div class="table-wrap">
19845
20295
  <table>
19846
20296
  <thead>
19847
- <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
20297
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
19848
20298
  </thead>
19849
20299
  <tbody>
19850
20300
  ${data.linkedEpics.map((e) => `
@@ -19859,138 +20309,29 @@ function sprintSummaryPage(data, cached2) {
19859
20309
  </div>`,
19860
20310
  { titleTag: "h3" }
19861
20311
  ) : "";
19862
- const FOCUS_BORDER_PALETTE = [
19863
- "hsl(220, 60%, 55%)",
19864
- "hsl(160, 50%, 45%)",
19865
- "hsl(280, 45%, 55%)",
19866
- "hsl(30, 65%, 55%)",
19867
- "hsl(340, 50%, 55%)",
19868
- "hsl(190, 50%, 45%)",
19869
- "hsl(60, 50%, 50%)",
19870
- "hsl(120, 40%, 45%)"
19871
- ];
19872
- function hashString(s) {
19873
- let h = 0;
19874
- for (let i = 0; i < s.length; i++) {
19875
- h = (h << 5) - h + s.charCodeAt(i) | 0;
19876
- }
19877
- return Math.abs(h);
19878
- }
19879
- const focusGroups = /* @__PURE__ */ new Map();
19880
- for (const item of data.workItems.items) {
19881
- const focus = item.workFocus ?? "Unassigned";
19882
- if (!focusGroups.has(focus)) focusGroups.set(focus, []);
19883
- focusGroups.get(focus).push(item);
19884
- }
19885
- const focusColorMap = /* @__PURE__ */ new Map();
19886
- for (const name of focusGroups.keys()) {
19887
- focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
19888
- }
19889
- function countFocusStats(items) {
19890
- let total = 0;
19891
- let done = 0;
19892
- let inProgress = 0;
19893
- function walk(list) {
19894
- for (const w of list) {
19895
- if (w.type !== "contribution") {
19896
- total++;
19897
- const s = w.status.toLowerCase();
19898
- if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
19899
- else if (s === "in-progress" || s === "in progress") inProgress++;
19900
- }
19901
- if (w.children) walk(w.children);
19902
- }
19903
- }
19904
- walk(items);
19905
- return { total, done, inProgress };
19906
- }
19907
- function renderItemRows(items, borderColor, depth = 0) {
19908
- return items.flatMap((w) => {
19909
- const isChild = depth > 0;
19910
- const isContribution = w.type === "contribution";
19911
- const classes = ["focus-row"];
19912
- if (isContribution) classes.push("contribution-row");
19913
- else if (isChild) classes.push("child-row");
19914
- const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
19915
- const progressCell = !isContribution && w.progress !== void 0 ? `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${w.progress}%"></div><span class="mini-progress-label">${w.progress}%</span></div>` : "";
19916
- const row = `
19917
- <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
19918
- <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
19919
- <td>${escapeHtml(w.title)}</td>
19920
- <td>${statusBadge(w.status)}</td>
19921
- <td>${progressCell}</td>
19922
- </tr>`;
19923
- const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
19924
- return [row, ...childRows];
19925
- });
19926
- }
19927
- const allWorkItemRows = [];
19928
- for (const [focus, items] of focusGroups) {
19929
- const color = focusColorMap.get(focus);
19930
- const stats = countFocusStats(items);
19931
- const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
19932
- const summaryParts = [];
19933
- if (stats.done > 0) summaryParts.push(`${stats.done} done`);
19934
- if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
19935
- const remaining = stats.total - stats.done - stats.inProgress;
19936
- if (remaining > 0) summaryParts.push(`${remaining} open`);
19937
- allWorkItemRows.push(`
19938
- <tr class="focus-group-header" style="--focus-color: ${color}">
19939
- <td colspan="2">
19940
- <span class="focus-group-name">${escapeHtml(focus)}</span>
19941
- <span class="focus-group-stats">${summaryParts.join(" / ")}</span>
19942
- </td>
19943
- <td colspan="2">
19944
- <div class="mini-progress-bar focus-group-progress"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>
19945
- </td>
19946
- </tr>`);
19947
- allWorkItemRows.push(...renderItemRows(items, color));
19948
- }
19949
- const tableHeaders = `<tr>
19950
- <th>ID</th>
19951
- <th>Title</th>
19952
- <th>Status</th>
19953
- <th>Progress</th>
19954
- </tr>`;
19955
- const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
19956
- "ss-work-items",
19957
- "Work Items",
19958
- `<div class="table-wrap">
19959
- <table id="work-items-table">
19960
- <thead>
19961
- ${tableHeaders}
19962
- </thead>
19963
- <tbody>
19964
- ${allWorkItemRows.join("")}
19965
- </tbody>
19966
- </table>
19967
- </div>`,
19968
- { titleTag: "h3", defaultCollapsed: true }
19969
- ) : "";
19970
- const activitySection = data.artifacts.length > 0 ? collapsibleSection(
19971
- "ss-activity",
19972
- "Recent Activity",
20312
+ const actionsSection = data.openActions.length > 0 ? collapsibleSection(
20313
+ "dm-sprint-actions",
20314
+ `Open Actions (${data.openActions.length})`,
19973
20315
  `<div class="table-wrap">
19974
20316
  <table>
19975
20317
  <thead>
19976
- <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
20318
+ <tr><th>ID</th><th>Title</th><th>Owner</th><th>Due Date</th></tr>
19977
20319
  </thead>
19978
20320
  <tbody>
19979
- ${data.artifacts.slice(0, 15).map((a) => `
20321
+ ${data.openActions.map((a) => `
19980
20322
  <tr>
19981
- <td>${formatDate(a.date)}</td>
19982
- <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
20323
+ <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
19983
20324
  <td>${escapeHtml(a.title)}</td>
19984
- <td>${escapeHtml(typeLabel(a.type))}</td>
19985
- <td>${escapeHtml(a.action)}</td>
20325
+ <td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
20326
+ <td>${a.dueDate ? formatDate(a.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
19986
20327
  </tr>`).join("")}
19987
20328
  </tbody>
19988
20329
  </table>
19989
20330
  </div>`,
19990
- { titleTag: "h3", defaultCollapsed: true }
20331
+ { titleTag: "h3" }
19991
20332
  ) : "";
19992
20333
  const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
19993
- "ss-meetings",
20334
+ "dm-sprint-meetings",
19994
20335
  `Meetings (${data.meetings.length})`,
19995
20336
  `<div class="table-wrap">
19996
20337
  <table>
@@ -20009,79 +20350,23 @@ function sprintSummaryPage(data, cached2) {
20009
20350
  </div>`,
20010
20351
  { titleTag: "h3", defaultCollapsed: true }
20011
20352
  ) : "";
20012
- const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
20013
- const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
20014
20353
  return `
20015
20354
  <div class="page-header">
20016
20355
  <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
20017
- <div class="subtitle">Sprint Summary ${dateRange}</div>
20356
+ <div class="subtitle">Sprint Execution ${dateRange}</div>
20018
20357
  </div>
20019
20358
  ${goalHtml}
20020
20359
  ${progressBar3(data.timeline.percentComplete)}
20021
20360
  ${statsCards}
20022
- ${epicsTable}
20023
20361
  ${workItemsSection}
20024
- ${activitySection}
20362
+ ${epicsSection}
20363
+ ${actionsSection}
20025
20364
  ${meetingsSection}
20026
-
20027
- <div class="sprint-ai-section">
20028
- <h3>AI Summary</h3>
20029
- ${cached2 ? `<p class="text-dim">Generated ${formatDate(cached2.generatedAt)} at ${cached2.generatedAt.slice(11, 16)} UTC</p>` : `<p class="text-dim">Generate a narrative summary of this sprint's progress, risks, and projections.</p>`}
20030
- <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
20031
- <div id="summary-loading" class="sprint-loading" style="display:none">
20032
- <div class="sprint-spinner"></div>
20033
- <span>Generating summary...</span>
20034
- </div>
20035
- <div id="summary-error" class="sprint-error" style="display:none"></div>
20036
- <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
20037
- </div>
20038
-
20039
- <script>
20040
- async function generateSummary() {
20041
- var btn = document.getElementById('generate-btn');
20042
- var loading = document.getElementById('summary-loading');
20043
- var errorEl = document.getElementById('summary-error');
20044
- var content = document.getElementById('summary-content');
20045
-
20046
- btn.disabled = true;
20047
- btn.style.display = 'none';
20048
- loading.style.display = 'flex';
20049
- errorEl.style.display = 'none';
20050
- content.style.display = 'none';
20051
-
20052
- try {
20053
- var res = await fetch('/api/sprint-summary', {
20054
- method: 'POST',
20055
- headers: { 'Content-Type': 'application/json' },
20056
- body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
20057
- });
20058
- var json = await res.json();
20059
- if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
20060
- loading.style.display = 'none';
20061
- content.innerHTML = json.html;
20062
- content.style.display = 'block';
20063
- btn.textContent = 'Regenerate';
20064
- btn.style.display = '';
20065
- btn.disabled = false;
20066
- } catch (e) {
20067
- loading.style.display = 'none';
20068
- errorEl.textContent = e.message;
20069
- errorEl.style.display = 'block';
20070
- btn.style.display = '';
20071
- btn.disabled = false;
20072
- }
20073
- }
20074
- </script>`;
20075
- }
20076
-
20077
- // src/web/templates/pages/dm/sprint.ts
20078
- function dmSprintPage(ctx) {
20079
- const data = getSprintSummaryData(ctx.store);
20080
- return sprintSummaryPage(data);
20365
+ `;
20081
20366
  }
20082
20367
 
20083
20368
  // src/web/templates/pages/dm/actions.ts
20084
- var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20369
+ var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20085
20370
  function urgencyBadge(tier) {
20086
20371
  const labels = {
20087
20372
  overdue: "Overdue",
@@ -20101,7 +20386,7 @@ function urgencyRowClass(tier) {
20101
20386
  function dmActionsPage(ctx) {
20102
20387
  const upcoming = getUpcomingData(ctx.store);
20103
20388
  const allActions = ctx.store.list({ type: "action" });
20104
- const openActions = allActions.filter((d) => !DONE_STATUSES6.has(d.frontmatter.status));
20389
+ const openActions = allActions.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
20105
20390
  const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
20106
20391
  const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
20107
20392
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
@@ -20186,7 +20471,7 @@ function dmActionsPage(ctx) {
20186
20471
  }
20187
20472
 
20188
20473
  // src/web/templates/pages/dm/risks.ts
20189
- var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20474
+ var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20190
20475
  function dmRisksPage(ctx) {
20191
20476
  const allDocs = ctx.store.list();
20192
20477
  const upcoming = getUpcomingData(ctx.store);
@@ -20197,7 +20482,7 @@ function dmRisksPage(ctx) {
20197
20482
  const todayMs = new Date(today).getTime();
20198
20483
  const fourteenDaysMs = 14 * 864e5;
20199
20484
  const agingItems = allDocs.filter((d) => {
20200
- if (DONE_STATUSES7.has(d.frontmatter.status)) return false;
20485
+ if (DONE_STATUSES10.has(d.frontmatter.status)) return false;
20201
20486
  if (!["action", "question"].includes(d.frontmatter.type)) return false;
20202
20487
  const createdMs = new Date(d.frontmatter.created).getTime();
20203
20488
  return todayMs - createdMs > fourteenDaysMs;
@@ -20311,7 +20596,7 @@ function dmRisksPage(ctx) {
20311
20596
  }
20312
20597
 
20313
20598
  // src/web/templates/pages/dm/meetings.ts
20314
- var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20599
+ var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20315
20600
  function dmMeetingsPage(ctx) {
20316
20601
  const meetings = ctx.store.list({ type: "meeting" });
20317
20602
  const actions = ctx.store.list({ type: "action" });
@@ -20357,7 +20642,7 @@ function dmMeetingsPage(ctx) {
20357
20642
  ${sortedMeetings.map((m) => {
20358
20643
  const date5 = m.frontmatter.date ?? m.frontmatter.created;
20359
20644
  const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
20360
- const openCount = relatedActions.filter((a) => !DONE_STATUSES8.has(a.frontmatter.status)).length;
20645
+ const openCount = relatedActions.filter((a) => !DONE_STATUSES11.has(a.frontmatter.status)).length;
20361
20646
  return `
20362
20647
  <tr>
20363
20648
  <td>${formatDate(date5)}</td>
@@ -20372,7 +20657,7 @@ function dmMeetingsPage(ctx) {
20372
20657
  const recentMeetingActions = [];
20373
20658
  for (const [mid, acts] of meetingActionMap) {
20374
20659
  for (const act of acts) {
20375
- if (!DONE_STATUSES8.has(act.frontmatter.status)) {
20660
+ if (!DONE_STATUSES11.has(act.frontmatter.status)) {
20376
20661
  recentMeetingActions.push({ action: act, meetingId: mid });
20377
20662
  }
20378
20663
  }
@@ -20567,16 +20852,16 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
20567
20852
  registerPersonaPage("dm", "governance", dmGovernancePage);
20568
20853
 
20569
20854
  // src/web/templates/pages/tl/dashboard.ts
20570
- var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20571
- var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
20855
+ var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20856
+ var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
20572
20857
  function tlDashboardPage(ctx) {
20573
20858
  const epics = ctx.store.list({ type: "epic" });
20574
20859
  const tasks = ctx.store.list({ type: "task" });
20575
20860
  const decisions = ctx.store.list({ type: "decision" });
20576
20861
  const questions = ctx.store.list({ type: "question" });
20577
20862
  const diagrams = getDiagramData(ctx.store);
20578
- const openEpics = epics.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
20579
- const openTasks = tasks.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
20863
+ const openEpics = epics.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
20864
+ const openTasks = tasks.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
20580
20865
  const technicalDecisions = decisions.filter((d) => {
20581
20866
  const tags = d.frontmatter.tags ?? [];
20582
20867
  return tags.some((t) => {
@@ -20585,7 +20870,7 @@ function tlDashboardPage(ctx) {
20585
20870
  });
20586
20871
  });
20587
20872
  const displayDecisions = technicalDecisions.length > 0 ? technicalDecisions : decisions;
20588
- const pendingDecisions = displayDecisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status));
20873
+ const pendingDecisions = displayDecisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status));
20589
20874
  const statsCards = `
20590
20875
  <div class="cards">
20591
20876
  <div class="card">
@@ -20634,7 +20919,7 @@ function tlDashboardPage(ctx) {
20634
20919
  }
20635
20920
 
20636
20921
  // src/web/templates/pages/tl/backlog.ts
20637
- var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20922
+ var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20638
20923
  function tlBacklogPage(ctx) {
20639
20924
  const epics = ctx.store.list({ type: "epic" });
20640
20925
  const tasks = ctx.store.list({ type: "task" });
@@ -20656,10 +20941,10 @@ function tlBacklogPage(ctx) {
20656
20941
  const featureIds = Array.isArray(linked) ? linked.map(String) : linked ? [String(linked)] : [];
20657
20942
  epicFeatureMap.set(epic.frontmatter.id, featureIds);
20658
20943
  }
20659
- const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
20944
+ const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
20660
20945
  const sortedEpics = [...epics].sort((a, b) => {
20661
- const sa = statusOrder[a.frontmatter.status] ?? 3;
20662
- const sb = statusOrder[b.frontmatter.status] ?? 3;
20946
+ const sa = statusOrder2[a.frontmatter.status] ?? 3;
20947
+ const sb = statusOrder2[b.frontmatter.status] ?? 3;
20663
20948
  if (sa !== sb) return sa - sb;
20664
20949
  return a.frontmatter.id.localeCompare(b.frontmatter.id);
20665
20950
  });
@@ -20671,7 +20956,7 @@ function tlBacklogPage(ctx) {
20671
20956
  <tbody>
20672
20957
  ${sortedEpics.map((e) => {
20673
20958
  const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
20674
- const done = eTasks.filter((t) => DONE_STATUSES10.has(t.frontmatter.status)).length;
20959
+ const done = eTasks.filter((t) => DONE_STATUSES13.has(t.frontmatter.status)).length;
20675
20960
  const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
20676
20961
  const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
20677
20962
  return `
@@ -20691,7 +20976,7 @@ function tlBacklogPage(ctx) {
20691
20976
  for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
20692
20977
  }
20693
20978
  const unassignedTasks = tasks.filter(
20694
- (t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES10.has(t.frontmatter.status)
20979
+ (t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES13.has(t.frontmatter.status)
20695
20980
  );
20696
20981
  const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
20697
20982
  "tl-backlog-unassigned",
@@ -20752,7 +21037,6 @@ var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
20752
21037
  "technical-assessment",
20753
21038
  "architecture-review"
20754
21039
  ]);
20755
- var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
20756
21040
  function progressBar4(pct) {
20757
21041
  return `<div class="sprint-progress-bar">
20758
21042
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
@@ -20772,25 +21056,8 @@ function tlSprintPage(ctx) {
20772
21056
  <p>No active sprint found. Create a sprint and set its status to "active" to track sprint work.</p>
20773
21057
  </div>`;
20774
21058
  }
20775
- const techTypes = /* @__PURE__ */ new Set(["epic", "task"]);
20776
- const techItems = [];
20777
- for (const item of data.workItems.items) {
20778
- if (techTypes.has(item.type)) {
20779
- techItems.push(item);
20780
- } else if (item.children) {
20781
- const promoteChildren = (children) => {
20782
- for (const child of children) {
20783
- if (techTypes.has(child.type)) {
20784
- techItems.push(child);
20785
- } else if (child.children) {
20786
- promoteChildren(child.children);
20787
- }
20788
- }
20789
- };
20790
- promoteChildren(item.children);
20791
- }
20792
- }
20793
- const techDone = techItems.filter((w) => DONE_STATUSES11.has(w.status)).length;
21059
+ const tlItems = filterItemsByOwner(data.workItems.items, "tl");
21060
+ const tlCompletionPct = computeOwnerCompletionPct(data.workItems.items, "tl");
20794
21061
  const allDocs = ctx.store.list();
20795
21062
  const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
20796
21063
  const statsCards = `
@@ -20801,9 +21068,9 @@ function tlSprintPage(ctx) {
20801
21068
  <div class="card-sub">${data.timeline.daysRemaining} days remaining</div>
20802
21069
  </div>
20803
21070
  <div class="card">
20804
- <div class="card-label">Tech Items</div>
20805
- <div class="card-value">${techItems.length}</div>
20806
- <div class="card-sub">${techDone} done</div>
21071
+ <div class="card-label">TL Completion</div>
21072
+ <div class="card-value">${tlCompletionPct}%</div>
21073
+ <div class="card-sub">${tlItems.length} owned items</div>
20807
21074
  </div>
20808
21075
  <div class="card">
20809
21076
  <div class="card-label">Epics</div>
@@ -20821,28 +21088,10 @@ function tlSprintPage(ctx) {
20821
21088
  <strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
20822
21089
  ${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
20823
21090
  </div>`;
20824
- const workItemsSection = techItems.length > 0 ? collapsibleSection(
20825
- "tl-sprint-items",
20826
- `Sprint Work Items (${techItems.length})`,
20827
- `<div class="table-wrap">
20828
- <table>
20829
- <thead>
20830
- <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
20831
- </thead>
20832
- <tbody>
20833
- ${techItems.map((w) => `
20834
- <tr>
20835
- <td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
20836
- <td>${escapeHtml(w.title)}</td>
20837
- <td>${escapeHtml(typeLabel(w.type))}</td>
20838
- <td>${statusBadge(w.status)}</td>
20839
- <td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
20840
- </tr>`).join("")}
20841
- </tbody>
20842
- </table>
20843
- </div>`,
20844
- { titleTag: "h3" }
20845
- ) : "";
21091
+ const workItemsSection = renderWorkItemsTable(tlItems, {
21092
+ sectionId: "tl-sprint-items",
21093
+ title: "TL Work Items"
21094
+ });
20846
21095
  const contributionsSection = tlContributions.length > 0 ? collapsibleSection(
20847
21096
  "tl-sprint-contributions",
20848
21097
  `TL Contributions (${tlContributions.length})`,
@@ -21226,93 +21475,273 @@ function upcomingPage(data) {
21226
21475
  `;
21227
21476
  }
21228
21477
 
21229
- // src/web/templates/pages/sprint-blockers.ts
21230
- function sprintBlockersPage(data, store) {
21478
+ // src/web/templates/pages/sprint-summary.ts
21479
+ function progressBar5(pct) {
21480
+ return `<div class="sprint-progress-bar">
21481
+ <div class="sprint-progress-fill" style="width: ${pct}%"></div>
21482
+ <span class="sprint-progress-label">${pct}%</span>
21483
+ </div>`;
21484
+ }
21485
+ function sprintSummaryPage(data, cached2) {
21231
21486
  if (!data) {
21232
21487
  return `
21233
21488
  <div class="page-header">
21234
- <h2>Sprint Blockers</h2>
21235
- <div class="subtitle">Blocked items in the active sprint</div>
21489
+ <h2>Sprint Summary</h2>
21490
+ <div class="subtitle">AI-powered sprint narrative</div>
21236
21491
  </div>
21237
21492
  <div class="empty">
21238
21493
  <h3>No Active Sprint</h3>
21239
- <p>No active sprint found.</p>
21494
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
21240
21495
  </div>`;
21241
21496
  }
21242
- const blockerDocs = data.blockers.map((b) => {
21243
- const doc = store.get(b.id);
21244
- return { ...b, doc };
21245
- });
21246
21497
  const statsCards = `
21247
21498
  <div class="cards">
21248
21499
  <div class="card">
21249
- <div class="card-label">Blocked Items</div>
21250
- <div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
21251
- <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
21500
+ <div class="card-label">Completion</div>
21501
+ <div class="card-value">${data.workItems.completionPct}%</div>
21502
+ <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
21252
21503
  </div>
21253
- </div>`;
21254
- const itemCards = blockerDocs.map((b) => {
21255
- const doc = b.doc;
21256
- const owner = doc?.frontmatter.owner;
21257
- const assignee = doc?.frontmatter.assignee;
21258
- const content = doc?.content?.trim();
21259
- return `
21260
- <div class="blocker-card">
21261
- <div class="blocker-card-header">
21262
- <a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
21263
- <span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
21264
- ${statusBadge("blocked")}
21265
- </div>
21266
- <h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
21267
- <div class="blocker-card-meta">
21268
- ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
21269
- ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
21270
- ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
21271
- </div>
21272
- ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
21273
- </div>`;
21274
- }).join("");
21275
- const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
21276
- return `
21277
- <div class="page-header">
21278
- <h2>Sprint Blockers</h2>
21279
- <div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
21280
- </div>
21281
- ${statsCards}
21282
- ${emptyMessage}
21283
- ${itemCards}`;
21284
- }
21285
-
21286
- // src/web/templates/pages/sprint-risks.ts
21287
- function sprintRisksPage(data, store) {
21288
- if (!data) {
21289
- return `
21290
- <div class="page-header">
21291
- <h2>Sprint Risks</h2>
21292
- <div class="subtitle">Risk items in the active sprint</div>
21504
+ <div class="card">
21505
+ <div class="card-label">Days Remaining</div>
21506
+ <div class="card-value">${data.timeline.daysRemaining}</div>
21507
+ <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
21293
21508
  </div>
21294
- <div class="empty">
21295
- <h3>No Active Sprint</h3>
21296
- <p>No active sprint found.</p>
21297
- </div>`;
21298
- }
21299
- const riskDocs = data.risks.map((r) => {
21300
- const doc = store.get(r.id);
21301
- return { ...r, doc };
21302
- });
21303
- const statsCards = `
21304
- <div class="cards">
21305
21509
  <div class="card">
21306
- <div class="card-label">Open Risks</div>
21307
- <div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
21308
- <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
21510
+ <div class="card-label">Epics</div>
21511
+ <div class="card-value">${data.linkedEpics.length}</div>
21512
+ <div class="card-sub">linked to sprint</div>
21309
21513
  </div>
21310
- </div>`;
21311
- const itemCards = riskDocs.map((r) => {
21312
- const doc = r.doc;
21313
- const owner = doc?.frontmatter.owner;
21314
- const assignee = doc?.frontmatter.assignee;
21315
- const content = doc?.content?.trim();
21514
+ <a class="card card-link" href="sprint-blockers">
21515
+ <div class="card-label">Blockers</div>
21516
+ <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
21517
+ <div class="card-sub">${data.workItems.blocked} blocked items</div>
21518
+ </a>
21519
+ <a class="card card-link" href="sprint-risks">
21520
+ <div class="card-label">Risks</div>
21521
+ <div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
21522
+ <div class="card-sub">open risk items</div>
21523
+ </a>
21524
+ </div>`;
21525
+ const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
21526
+ "ss-epics",
21527
+ "Linked Epics",
21528
+ `<div class="table-wrap">
21529
+ <table>
21530
+ <thead>
21531
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
21532
+ </thead>
21533
+ <tbody>
21534
+ ${data.linkedEpics.map((e) => `
21535
+ <tr>
21536
+ <td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
21537
+ <td>${escapeHtml(e.title)}</td>
21538
+ <td>${statusBadge(e.status)}</td>
21539
+ <td>${e.tasksDone} / ${e.tasksTotal}</td>
21540
+ </tr>`).join("")}
21541
+ </tbody>
21542
+ </table>
21543
+ </div>`,
21544
+ { titleTag: "h3" }
21545
+ ) : "";
21546
+ const workItemsSection = renderWorkItemsTable(data.workItems.items, {
21547
+ sectionId: "ss-work-items",
21548
+ title: "Work Items",
21549
+ defaultCollapsed: true
21550
+ });
21551
+ const activitySection = data.artifacts.length > 0 ? collapsibleSection(
21552
+ "ss-activity",
21553
+ "Recent Activity",
21554
+ `<div class="table-wrap">
21555
+ <table>
21556
+ <thead>
21557
+ <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
21558
+ </thead>
21559
+ <tbody>
21560
+ ${data.artifacts.slice(0, 15).map((a) => `
21561
+ <tr>
21562
+ <td>${formatDate(a.date)}</td>
21563
+ <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
21564
+ <td>${escapeHtml(a.title)}</td>
21565
+ <td>${escapeHtml(typeLabel(a.type))}</td>
21566
+ <td>${escapeHtml(a.action)}</td>
21567
+ </tr>`).join("")}
21568
+ </tbody>
21569
+ </table>
21570
+ </div>`,
21571
+ { titleTag: "h3", defaultCollapsed: true }
21572
+ ) : "";
21573
+ const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
21574
+ "ss-meetings",
21575
+ `Meetings (${data.meetings.length})`,
21576
+ `<div class="table-wrap">
21577
+ <table>
21578
+ <thead>
21579
+ <tr><th>Date</th><th>ID</th><th>Title</th></tr>
21580
+ </thead>
21581
+ <tbody>
21582
+ ${data.meetings.map((m) => `
21583
+ <tr>
21584
+ <td>${formatDate(m.date)}</td>
21585
+ <td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
21586
+ <td>${escapeHtml(m.title)}</td>
21587
+ </tr>`).join("")}
21588
+ </tbody>
21589
+ </table>
21590
+ </div>`,
21591
+ { titleTag: "h3", defaultCollapsed: true }
21592
+ ) : "";
21593
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
21594
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
21595
+ return `
21596
+ <div class="page-header">
21597
+ <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
21598
+ <div class="subtitle">Sprint Summary ${dateRange}</div>
21599
+ </div>
21600
+ ${goalHtml}
21601
+ ${progressBar5(data.timeline.percentComplete)}
21602
+ ${statsCards}
21603
+ ${epicsTable}
21604
+ ${workItemsSection}
21605
+ ${activitySection}
21606
+ ${meetingsSection}
21607
+
21608
+ <div class="sprint-ai-section">
21609
+ <h3>AI Summary</h3>
21610
+ ${cached2 ? `<p class="text-dim">Generated ${formatDate(cached2.generatedAt)} at ${cached2.generatedAt.slice(11, 16)} UTC</p>` : `<p class="text-dim">Generate a narrative summary of this sprint's progress, risks, and projections.</p>`}
21611
+ <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
21612
+ <div id="summary-loading" class="sprint-loading" style="display:none">
21613
+ <div class="sprint-spinner"></div>
21614
+ <span>Generating summary...</span>
21615
+ </div>
21616
+ <div id="summary-error" class="sprint-error" style="display:none"></div>
21617
+ <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
21618
+ </div>
21619
+
21620
+ <script>
21621
+ async function generateSummary() {
21622
+ var btn = document.getElementById('generate-btn');
21623
+ var loading = document.getElementById('summary-loading');
21624
+ var errorEl = document.getElementById('summary-error');
21625
+ var content = document.getElementById('summary-content');
21626
+
21627
+ btn.disabled = true;
21628
+ btn.style.display = 'none';
21629
+ loading.style.display = 'flex';
21630
+ errorEl.style.display = 'none';
21631
+ content.style.display = 'none';
21632
+
21633
+ try {
21634
+ var res = await fetch('/api/sprint-summary', {
21635
+ method: 'POST',
21636
+ headers: { 'Content-Type': 'application/json' },
21637
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
21638
+ });
21639
+ var json = await res.json();
21640
+ if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
21641
+ loading.style.display = 'none';
21642
+ content.innerHTML = json.html;
21643
+ content.style.display = 'block';
21644
+ btn.textContent = 'Regenerate';
21645
+ btn.style.display = '';
21646
+ btn.disabled = false;
21647
+ } catch (e) {
21648
+ loading.style.display = 'none';
21649
+ errorEl.textContent = e.message;
21650
+ errorEl.style.display = 'block';
21651
+ btn.style.display = '';
21652
+ btn.disabled = false;
21653
+ }
21654
+ }
21655
+ </script>`;
21656
+ }
21657
+
21658
+ // src/web/templates/pages/sprint-blockers.ts
21659
+ function sprintBlockersPage(data, store) {
21660
+ if (!data) {
21661
+ return `
21662
+ <div class="page-header">
21663
+ <h2>Sprint Blockers</h2>
21664
+ <div class="subtitle">Blocked items in the active sprint</div>
21665
+ </div>
21666
+ <div class="empty">
21667
+ <h3>No Active Sprint</h3>
21668
+ <p>No active sprint found.</p>
21669
+ </div>`;
21670
+ }
21671
+ const blockerDocs = data.blockers.map((b) => {
21672
+ const doc = store.get(b.id);
21673
+ return { ...b, doc };
21674
+ });
21675
+ const statsCards = `
21676
+ <div class="cards">
21677
+ <div class="card">
21678
+ <div class="card-label">Blocked Items</div>
21679
+ <div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
21680
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
21681
+ </div>
21682
+ </div>`;
21683
+ const itemCards = blockerDocs.map((b) => {
21684
+ const doc = b.doc;
21685
+ const owner = doc?.frontmatter.owner;
21686
+ const assignee = doc?.frontmatter.assignee;
21687
+ const content = doc?.content?.trim();
21688
+ return `
21689
+ <div class="blocker-card">
21690
+ <div class="blocker-card-header">
21691
+ <a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
21692
+ <span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
21693
+ ${statusBadge("blocked")}
21694
+ </div>
21695
+ <h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
21696
+ <div class="blocker-card-meta">
21697
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
21698
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
21699
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
21700
+ </div>
21701
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
21702
+ </div>`;
21703
+ }).join("");
21704
+ const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
21705
+ return `
21706
+ <div class="page-header">
21707
+ <h2>Sprint Blockers</h2>
21708
+ <div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
21709
+ </div>
21710
+ ${statsCards}
21711
+ ${emptyMessage}
21712
+ ${itemCards}`;
21713
+ }
21714
+
21715
+ // src/web/templates/pages/sprint-risks.ts
21716
+ function sprintRisksPage(data, store) {
21717
+ if (!data) {
21718
+ return `
21719
+ <div class="page-header">
21720
+ <h2>Sprint Risks</h2>
21721
+ <div class="subtitle">Risk items in the active sprint</div>
21722
+ </div>
21723
+ <div class="empty">
21724
+ <h3>No Active Sprint</h3>
21725
+ <p>No active sprint found.</p>
21726
+ </div>`;
21727
+ }
21728
+ const riskDocs = data.risks.map((r) => {
21729
+ const doc = store.get(r.id);
21730
+ return { ...r, doc };
21731
+ });
21732
+ const statsCards = `
21733
+ <div class="cards">
21734
+ <div class="card">
21735
+ <div class="card-label">Open Risks</div>
21736
+ <div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
21737
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
21738
+ </div>
21739
+ </div>`;
21740
+ const itemCards = riskDocs.map((r) => {
21741
+ const doc = r.doc;
21742
+ const owner = doc?.frontmatter.owner;
21743
+ const assignee = doc?.frontmatter.assignee;
21744
+ const content = doc?.content?.trim();
21316
21745
  return `
21317
21746
  <div class="blocker-card" id="risk-${escapeHtml(r.id)}">
21318
21747
  <div class="blocker-card-header">
@@ -24466,13 +24895,23 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
24466
24895
  // src/skills/builtin/jira/client.ts
24467
24896
  var JiraClient = class {
24468
24897
  baseUrl;
24898
+ baseUrlV3;
24469
24899
  authHeader;
24470
24900
  constructor(config2) {
24471
- this.baseUrl = `https://${config2.host}/rest/api/2`;
24901
+ const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24902
+ this.baseUrl = `https://${host}/rest/api/2`;
24903
+ this.baseUrlV3 = `https://${host}/rest/api/3`;
24472
24904
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
24473
24905
  }
24474
24906
  async request(path21, method = "GET", body) {
24475
24907
  const url2 = `${this.baseUrl}${path21}`;
24908
+ return this.doRequest(url2, method, body);
24909
+ }
24910
+ async requestV3(path21, method = "GET", body) {
24911
+ const url2 = `${this.baseUrlV3}${path21}`;
24912
+ return this.doRequest(url2, method, body);
24913
+ }
24914
+ async doRequest(url2, method, body) {
24476
24915
  const headers = {
24477
24916
  Authorization: this.authHeader,
24478
24917
  "Content-Type": "application/json",
@@ -24486,7 +24925,7 @@ var JiraClient = class {
24486
24925
  if (!response.ok) {
24487
24926
  const text = await response.text().catch(() => "");
24488
24927
  throw new Error(
24489
- `Jira API error ${response.status} ${method} ${path21}: ${text}`
24928
+ `Jira API error ${response.status} ${method} ${url2}: ${text}`
24490
24929
  );
24491
24930
  }
24492
24931
  if (response.status === 204) return void 0;
@@ -24499,6 +24938,14 @@ var JiraClient = class {
24499
24938
  });
24500
24939
  return this.request(`/search?${params}`);
24501
24940
  }
24941
+ async searchIssuesV3(jql, fields = ["summary", "status", "issuetype", "priority", "assignee", "labels"], maxResults = 50) {
24942
+ const params = new URLSearchParams({
24943
+ jql,
24944
+ maxResults: String(maxResults),
24945
+ fields: fields.join(",")
24946
+ });
24947
+ return this.requestV3(`/search/jql?${params}`);
24948
+ }
24502
24949
  async getIssue(key) {
24503
24950
  return this.request(`/issue/${encodeURIComponent(key)}`);
24504
24951
  }
@@ -24512,6 +24959,28 @@ var JiraClient = class {
24512
24959
  { fields }
24513
24960
  );
24514
24961
  }
24962
+ async getIssueWithLinks(key) {
24963
+ return this.request(
24964
+ `/issue/${encodeURIComponent(key)}?fields=summary,status,issuetype,priority,assignee,labels,subtasks,issuelinks`
24965
+ );
24966
+ }
24967
+ async getChangelog(key) {
24968
+ const result = await this.request(
24969
+ `/issue/${encodeURIComponent(key)}/changelog?maxResults=100`
24970
+ );
24971
+ return result.values;
24972
+ }
24973
+ async getComments(key) {
24974
+ const result = await this.request(
24975
+ `/issue/${encodeURIComponent(key)}/comment?maxResults=100`
24976
+ );
24977
+ return result.comments;
24978
+ }
24979
+ async getRemoteLinks(key) {
24980
+ return this.request(
24981
+ `/issue/${encodeURIComponent(key)}/remotelink`
24982
+ );
24983
+ }
24515
24984
  async addComment(key, body) {
24516
24985
  await this.request(
24517
24986
  `/issue/${encodeURIComponent(key)}/comment`,
@@ -24525,7 +24994,651 @@ function createJiraClient(jiraUserConfig) {
24525
24994
  const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
24526
24995
  const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
24527
24996
  if (!host || !email3 || !apiToken) return null;
24528
- return { client: new JiraClient({ host, email: email3, apiToken }), host };
24997
+ const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24998
+ return { client: new JiraClient({ host, email: email3, apiToken }), host: normalizedHost };
24999
+ }
25000
+
25001
+ // src/skills/builtin/jira/sync.ts
25002
+ var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25003
+ var DEFAULT_ACTION_STATUS_MAP = {
25004
+ done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
25005
+ "in-progress": ["In Progress", "In Review", "Reviewing", "Testing"],
25006
+ blocked: ["Blocked"],
25007
+ open: ["To Do", "Open", "Backlog", "New"]
25008
+ };
25009
+ var DEFAULT_TASK_STATUS_MAP = {
25010
+ done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
25011
+ review: ["In Review", "Code Review", "Reviewing", "Testing"],
25012
+ "in-progress": ["In Progress"],
25013
+ ready: ["Ready", "Selected for Development"],
25014
+ blocked: ["Blocked"],
25015
+ backlog: ["To Do", "Open", "Backlog", "New"]
25016
+ };
25017
+ function buildStatusLookup(configMap, defaults) {
25018
+ const map2 = configMap ?? defaults;
25019
+ const lookup = /* @__PURE__ */ new Map();
25020
+ for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
25021
+ for (const js of jiraStatuses) {
25022
+ lookup.set(js.toLowerCase(), marvinStatus);
25023
+ }
25024
+ }
25025
+ return lookup;
25026
+ }
25027
+ function mapJiraStatusForAction(status, configMap) {
25028
+ const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
25029
+ return lookup.get(status.toLowerCase()) ?? "open";
25030
+ }
25031
+ function mapJiraStatusForTask(status, configMap) {
25032
+ const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
25033
+ return lookup.get(status.toLowerCase()) ?? "backlog";
25034
+ }
25035
+ function computeSubtaskProgress(subtasks) {
25036
+ if (subtasks.length === 0) return 0;
25037
+ const done = subtasks.filter(
25038
+ (s) => DONE_STATUSES14.has(s.fields.status.name.toLowerCase())
25039
+ ).length;
25040
+ return Math.round(done / subtasks.length * 100);
25041
+ }
25042
+ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
25043
+ const result = { artifacts: [], errors: [] };
25044
+ const actions = store.list({ type: "action" });
25045
+ const tasks = store.list({ type: "task" });
25046
+ let candidates = [...actions, ...tasks].filter(
25047
+ (d) => d.frontmatter.jiraKey
25048
+ );
25049
+ if (artifactId) {
25050
+ candidates = candidates.filter((d) => d.frontmatter.id === artifactId);
25051
+ if (candidates.length === 0) {
25052
+ const doc = store.get(artifactId);
25053
+ if (doc) {
25054
+ result.errors.push(
25055
+ `${artifactId} has no jiraKey \u2014 use push_artifact_to_jira or link_to_jira first`
25056
+ );
25057
+ } else {
25058
+ result.errors.push(`Artifact ${artifactId} not found`);
25059
+ }
25060
+ return result;
25061
+ }
25062
+ }
25063
+ candidates = candidates.filter(
25064
+ (d) => !DONE_STATUSES14.has(d.frontmatter.status)
25065
+ );
25066
+ for (const doc of candidates) {
25067
+ const jiraKey = doc.frontmatter.jiraKey;
25068
+ const artifactType = doc.frontmatter.type;
25069
+ try {
25070
+ const issue2 = await client.getIssueWithLinks(jiraKey);
25071
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
25072
+ const currentStatus = doc.frontmatter.status;
25073
+ const linkedIssues = [];
25074
+ if (issue2.fields.subtasks) {
25075
+ for (const sub of issue2.fields.subtasks) {
25076
+ linkedIssues.push({
25077
+ key: sub.key,
25078
+ summary: sub.fields.summary,
25079
+ status: sub.fields.status.name,
25080
+ relationship: "subtask",
25081
+ isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
25082
+ });
25083
+ }
25084
+ }
25085
+ if (issue2.fields.issuelinks) {
25086
+ for (const link of issue2.fields.issuelinks) {
25087
+ if (link.outwardIssue) {
25088
+ linkedIssues.push({
25089
+ key: link.outwardIssue.key,
25090
+ summary: link.outwardIssue.fields.summary,
25091
+ status: link.outwardIssue.fields.status.name,
25092
+ relationship: link.type.outward,
25093
+ isDone: DONE_STATUSES14.has(
25094
+ link.outwardIssue.fields.status.name.toLowerCase()
25095
+ )
25096
+ });
25097
+ }
25098
+ if (link.inwardIssue) {
25099
+ linkedIssues.push({
25100
+ key: link.inwardIssue.key,
25101
+ summary: link.inwardIssue.fields.summary,
25102
+ status: link.inwardIssue.fields.status.name,
25103
+ relationship: link.type.inward,
25104
+ isDone: DONE_STATUSES14.has(
25105
+ link.inwardIssue.fields.status.name.toLowerCase()
25106
+ )
25107
+ });
25108
+ }
25109
+ }
25110
+ }
25111
+ const subtasks = issue2.fields.subtasks ?? [];
25112
+ let proposedProgress;
25113
+ if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
25114
+ proposedProgress = computeSubtaskProgress(subtasks);
25115
+ }
25116
+ const currentProgress = doc.frontmatter.progress;
25117
+ result.artifacts.push({
25118
+ id: doc.frontmatter.id,
25119
+ type: artifactType,
25120
+ jiraKey,
25121
+ jiraUrl: `https://${host}/browse/${jiraKey}`,
25122
+ jiraSummary: issue2.fields.summary,
25123
+ jiraStatus: issue2.fields.status.name,
25124
+ currentMarvinStatus: currentStatus,
25125
+ proposedMarvinStatus: proposedStatus,
25126
+ statusChanged: currentStatus !== proposedStatus,
25127
+ currentProgress,
25128
+ proposedProgress,
25129
+ progressChanged: proposedProgress !== void 0 && proposedProgress !== currentProgress,
25130
+ linkedIssues
25131
+ });
25132
+ } catch (err) {
25133
+ result.errors.push(
25134
+ `${doc.frontmatter.id} (${jiraKey}): ${err instanceof Error ? err.message : String(err)}`
25135
+ );
25136
+ }
25137
+ }
25138
+ return result;
25139
+ }
25140
+ async function syncJiraProgress(store, client, host, artifactId, statusMap) {
25141
+ const fetchResult = await fetchJiraStatus(store, client, host, artifactId, statusMap);
25142
+ const result = {
25143
+ updated: [],
25144
+ unchanged: 0,
25145
+ errors: [...fetchResult.errors]
25146
+ };
25147
+ for (const artifact of fetchResult.artifacts) {
25148
+ const hasChanges = artifact.statusChanged || artifact.progressChanged || artifact.linkedIssues.length > 0;
25149
+ if (hasChanges) {
25150
+ const updates = {
25151
+ status: artifact.proposedMarvinStatus,
25152
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
25153
+ jiraLinkedIssues: artifact.linkedIssues
25154
+ };
25155
+ if (artifact.proposedProgress !== void 0) {
25156
+ updates.progress = artifact.proposedProgress;
25157
+ }
25158
+ store.update(artifact.id, updates);
25159
+ if (artifact.type === "task") {
25160
+ propagateProgressFromTask(store, artifact.id);
25161
+ } else if (artifact.type === "action") {
25162
+ propagateProgressToAction(store, artifact.id);
25163
+ }
25164
+ result.updated.push({
25165
+ id: artifact.id,
25166
+ jiraKey: artifact.jiraKey,
25167
+ oldStatus: artifact.currentMarvinStatus,
25168
+ newStatus: artifact.proposedMarvinStatus,
25169
+ linkedIssues: artifact.linkedIssues
25170
+ });
25171
+ } else {
25172
+ store.update(artifact.id, {
25173
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
25174
+ });
25175
+ result.unchanged++;
25176
+ }
25177
+ }
25178
+ return result;
25179
+ }
25180
+
25181
+ // src/skills/builtin/jira/daily.ts
25182
+ var BLOCKER_PATTERNS = [
25183
+ /\bblocked\b/i,
25184
+ /\bblocking\b/i,
25185
+ /\bwaiting\s+for\b/i,
25186
+ /\bon\s+hold\b/i,
25187
+ /\bcan'?t\s+proceed\b/i,
25188
+ /\bdepends?\s+on\b/i,
25189
+ /\bstuck\b/i,
25190
+ /\bneed[s]?\s+(to\s+wait|approval|input|clarification)\b/i
25191
+ ];
25192
+ var DECISION_PATTERNS = [
25193
+ /\bdecided\b/i,
25194
+ /\bagreed\b/i,
25195
+ /\bapproved?\b/i,
25196
+ /\blet'?s?\s+go\s+with\b/i,
25197
+ /\bwe('ll|\s+will)\s+(use|go|proceed|adopt)\b/i,
25198
+ /\bsigned\s+off\b/i,
25199
+ /\bconfirmed\b/i
25200
+ ];
25201
+ var QUESTION_PATTERNS = [
25202
+ /\?/,
25203
+ /\bdoes\s+anyone\s+know\b/i,
25204
+ /\bhow\s+should\s+we\b/i,
25205
+ /\bneed\s+clarification\b/i,
25206
+ /\bwhat('s|\s+is)\s+the\s+(plan|approach|status)\b/i,
25207
+ /\bshould\s+we\b/i,
25208
+ /\bany\s+(idea|thought|suggestion)s?\b/i,
25209
+ /\bopen\s+question\b/i
25210
+ ];
25211
+ var RESOLUTION_PATTERNS = [
25212
+ /\bfixed\b/i,
25213
+ /\bresolved\b/i,
25214
+ /\bmerged\b/i,
25215
+ /\bdeployed\b/i,
25216
+ /\bcompleted?\b/i,
25217
+ /\bshipped\b/i,
25218
+ /\bimplemented\b/i,
25219
+ /\bclosed\b/i
25220
+ ];
25221
+ function detectCommentSignals(text) {
25222
+ const signals = [];
25223
+ const lines = text.split("\n");
25224
+ for (const line of lines) {
25225
+ const trimmed = line.trim();
25226
+ if (!trimmed) continue;
25227
+ for (const pattern of BLOCKER_PATTERNS) {
25228
+ if (pattern.test(trimmed)) {
25229
+ signals.push({ type: "blocker", snippet: truncate(trimmed, 120) });
25230
+ break;
25231
+ }
25232
+ }
25233
+ for (const pattern of DECISION_PATTERNS) {
25234
+ if (pattern.test(trimmed)) {
25235
+ signals.push({ type: "decision", snippet: truncate(trimmed, 120) });
25236
+ break;
25237
+ }
25238
+ }
25239
+ for (const pattern of QUESTION_PATTERNS) {
25240
+ if (pattern.test(trimmed)) {
25241
+ signals.push({ type: "question", snippet: truncate(trimmed, 120) });
25242
+ break;
25243
+ }
25244
+ }
25245
+ for (const pattern of RESOLUTION_PATTERNS) {
25246
+ if (pattern.test(trimmed)) {
25247
+ signals.push({ type: "resolution", snippet: truncate(trimmed, 120) });
25248
+ break;
25249
+ }
25250
+ }
25251
+ }
25252
+ const seen = /* @__PURE__ */ new Set();
25253
+ return signals.filter((s) => {
25254
+ if (seen.has(s.type)) return false;
25255
+ seen.add(s.type);
25256
+ return true;
25257
+ });
25258
+ }
25259
+ var STOP_WORDS = /* @__PURE__ */ new Set([
25260
+ "a",
25261
+ "an",
25262
+ "the",
25263
+ "and",
25264
+ "or",
25265
+ "but",
25266
+ "in",
25267
+ "on",
25268
+ "at",
25269
+ "to",
25270
+ "for",
25271
+ "of",
25272
+ "with",
25273
+ "by",
25274
+ "from",
25275
+ "is",
25276
+ "are",
25277
+ "was",
25278
+ "were",
25279
+ "be",
25280
+ "been",
25281
+ "this",
25282
+ "that",
25283
+ "it",
25284
+ "its",
25285
+ "as",
25286
+ "not",
25287
+ "no",
25288
+ "if",
25289
+ "do",
25290
+ "does",
25291
+ "new",
25292
+ "via",
25293
+ "use",
25294
+ "using",
25295
+ "based",
25296
+ "into",
25297
+ "e.g",
25298
+ "etc"
25299
+ ]);
25300
+ function tokenize(text) {
25301
+ return new Set(
25302
+ text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
25303
+ );
25304
+ }
25305
+ function computeTitleSimilarity(jiraSummary, artifactTitle) {
25306
+ const jiraTokens = tokenize(jiraSummary);
25307
+ const artifactTokens = tokenize(artifactTitle);
25308
+ if (jiraTokens.size === 0 || artifactTokens.size === 0) {
25309
+ return { score: 0, sharedTerms: [] };
25310
+ }
25311
+ const shared = [];
25312
+ for (const token of jiraTokens) {
25313
+ if (artifactTokens.has(token)) {
25314
+ shared.push(token);
25315
+ }
25316
+ }
25317
+ const union2 = /* @__PURE__ */ new Set([...jiraTokens, ...artifactTokens]);
25318
+ const score = shared.length / union2.size;
25319
+ return { score, sharedTerms: shared };
25320
+ }
25321
+ var LINK_SUGGESTION_THRESHOLD = 0.15;
25322
+ var MAX_LINK_SUGGESTIONS = 3;
25323
+ function findLinkSuggestions(jiraSummary, allDocs) {
25324
+ const suggestions = [];
25325
+ for (const doc of allDocs) {
25326
+ const fm = doc.frontmatter;
25327
+ if (fm.jiraKey) continue;
25328
+ const { score, sharedTerms } = computeTitleSimilarity(
25329
+ jiraSummary,
25330
+ fm.title
25331
+ );
25332
+ if (score >= LINK_SUGGESTION_THRESHOLD && sharedTerms.length >= 2) {
25333
+ suggestions.push({
25334
+ artifactId: fm.id,
25335
+ artifactType: fm.type,
25336
+ artifactTitle: fm.title,
25337
+ score,
25338
+ sharedTerms
25339
+ });
25340
+ }
25341
+ }
25342
+ return suggestions.sort((a, b) => b.score - a.score).slice(0, MAX_LINK_SUGGESTIONS);
25343
+ }
25344
+ function extractCommentText(body) {
25345
+ if (typeof body === "string") return body;
25346
+ if (!body || typeof body !== "object") return "";
25347
+ const parts = [];
25348
+ function walk(node) {
25349
+ if (!node || typeof node !== "object") return;
25350
+ const n = node;
25351
+ if (n.type === "text" && typeof n.text === "string") {
25352
+ parts.push(n.text);
25353
+ }
25354
+ if (Array.isArray(n.content)) {
25355
+ for (const child of n.content) walk(child);
25356
+ }
25357
+ }
25358
+ walk(body);
25359
+ return parts.join(" ");
25360
+ }
25361
+ function truncate(text, maxLen = 200) {
25362
+ if (text.length <= maxLen) return text;
25363
+ return text.slice(0, maxLen) + "\u2026";
25364
+ }
25365
+ function isWithinRange(timestamp, range) {
25366
+ const date5 = timestamp.slice(0, 10);
25367
+ return date5 >= range.from && date5 <= range.to;
25368
+ }
25369
+ function isConfluenceUrl(url2) {
25370
+ return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
25371
+ }
25372
+ var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25373
+ async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
25374
+ const summary = {
25375
+ dateRange,
25376
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25377
+ projectKey,
25378
+ issues: [],
25379
+ proposedActions: [],
25380
+ errors: []
25381
+ };
25382
+ const jql = `project = ${projectKey} AND updated >= "${dateRange.from}" AND updated <= "${dateRange.to} 23:59" ORDER BY updated DESC`;
25383
+ let searchResult;
25384
+ try {
25385
+ searchResult = await client.searchIssuesV3(
25386
+ jql,
25387
+ ["summary", "status", "issuetype", "priority", "assignee", "labels"],
25388
+ 100
25389
+ );
25390
+ } catch (err) {
25391
+ summary.errors.push(
25392
+ `Search failed: ${err instanceof Error ? err.message : String(err)}`
25393
+ );
25394
+ return summary;
25395
+ }
25396
+ const allDocs = [
25397
+ ...store.list({ type: "action" }),
25398
+ ...store.list({ type: "task" }),
25399
+ ...store.list({ type: "decision" }),
25400
+ ...store.list({ type: "question" })
25401
+ ];
25402
+ const otherTypes = store.registeredTypes.filter(
25403
+ (t) => !["action", "task", "decision", "question"].includes(t)
25404
+ );
25405
+ for (const t of otherTypes) {
25406
+ allDocs.push(...store.list({ type: t }));
25407
+ }
25408
+ const jiraKeyToArtifacts = /* @__PURE__ */ new Map();
25409
+ for (const doc of allDocs) {
25410
+ const jk = doc.frontmatter.jiraKey;
25411
+ if (jk) {
25412
+ const list = jiraKeyToArtifacts.get(jk) ?? [];
25413
+ list.push(doc);
25414
+ jiraKeyToArtifacts.set(jk, list);
25415
+ }
25416
+ }
25417
+ const BATCH_SIZE = 5;
25418
+ const issues = searchResult.issues;
25419
+ for (let i = 0; i < issues.length; i += BATCH_SIZE) {
25420
+ const batch = issues.slice(i, i + BATCH_SIZE);
25421
+ const results = await Promise.allSettled(
25422
+ batch.map(
25423
+ (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
25424
+ )
25425
+ );
25426
+ for (let j = 0; j < results.length; j++) {
25427
+ const r = results[j];
25428
+ if (r.status === "fulfilled") {
25429
+ summary.issues.push(r.value);
25430
+ } else {
25431
+ summary.errors.push(
25432
+ `${batch[j].key}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
25433
+ );
25434
+ }
25435
+ }
25436
+ }
25437
+ summary.proposedActions = generateProposedActions(summary.issues);
25438
+ return summary;
25439
+ }
25440
+ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
25441
+ const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
25442
+ client.getChangelog(issue2.key).catch(() => []),
25443
+ client.getComments(issue2.key).catch(() => []),
25444
+ client.getRemoteLinks(issue2.key).catch(() => []),
25445
+ client.getIssueWithLinks(issue2.key).catch(() => null)
25446
+ ]);
25447
+ const changes = [];
25448
+ for (const entry of changelogResult) {
25449
+ if (!isWithinRange(entry.created, dateRange)) continue;
25450
+ for (const item of entry.items) {
25451
+ changes.push({
25452
+ field: item.field,
25453
+ from: item.fromString,
25454
+ to: item.toString,
25455
+ author: entry.author.displayName,
25456
+ timestamp: entry.created
25457
+ });
25458
+ }
25459
+ }
25460
+ const comments = [];
25461
+ for (const comment of commentsResult) {
25462
+ if (!isWithinRange(comment.created, dateRange) && !isWithinRange(comment.updated, dateRange)) {
25463
+ continue;
25464
+ }
25465
+ const fullText = extractCommentText(comment.body);
25466
+ const signals = detectCommentSignals(fullText);
25467
+ comments.push({
25468
+ author: comment.author.displayName,
25469
+ created: comment.created,
25470
+ bodyPreview: truncate(fullText),
25471
+ signals
25472
+ });
25473
+ }
25474
+ const confluenceLinks = [];
25475
+ for (const rl of remoteLinksResult) {
25476
+ if (isConfluenceUrl(rl.object.url)) {
25477
+ confluenceLinks.push({
25478
+ url: rl.object.url,
25479
+ title: rl.object.title
25480
+ });
25481
+ }
25482
+ }
25483
+ const linkedIssues = [];
25484
+ if (issueWithLinks) {
25485
+ if (issueWithLinks.fields.subtasks) {
25486
+ for (const sub of issueWithLinks.fields.subtasks) {
25487
+ linkedIssues.push({
25488
+ key: sub.key,
25489
+ summary: sub.fields.summary,
25490
+ status: sub.fields.status.name,
25491
+ relationship: "subtask",
25492
+ isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
25493
+ });
25494
+ }
25495
+ }
25496
+ if (issueWithLinks.fields.issuelinks) {
25497
+ for (const link of issueWithLinks.fields.issuelinks) {
25498
+ if (link.outwardIssue) {
25499
+ linkedIssues.push({
25500
+ key: link.outwardIssue.key,
25501
+ summary: link.outwardIssue.fields.summary,
25502
+ status: link.outwardIssue.fields.status.name,
25503
+ relationship: link.type.outward,
25504
+ isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
25505
+ });
25506
+ }
25507
+ if (link.inwardIssue) {
25508
+ linkedIssues.push({
25509
+ key: link.inwardIssue.key,
25510
+ summary: link.inwardIssue.fields.summary,
25511
+ status: link.inwardIssue.fields.status.name,
25512
+ relationship: link.type.inward,
25513
+ isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
25514
+ });
25515
+ }
25516
+ }
25517
+ }
25518
+ }
25519
+ const marvinArtifacts = [];
25520
+ const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
25521
+ for (const doc of artifacts) {
25522
+ const fm = doc.frontmatter;
25523
+ const artifactType = fm.type;
25524
+ let proposedStatus = null;
25525
+ if (artifactType === "action" || artifactType === "task") {
25526
+ const jiraStatus = issue2.fields.status?.name;
25527
+ if (jiraStatus) {
25528
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
25529
+ }
25530
+ }
25531
+ marvinArtifacts.push({
25532
+ id: fm.id,
25533
+ type: artifactType,
25534
+ title: fm.title,
25535
+ currentStatus: fm.status,
25536
+ proposedStatus,
25537
+ statusDrift: proposedStatus !== null && proposedStatus !== fm.status
25538
+ });
25539
+ }
25540
+ const linkSuggestions = marvinArtifacts.length === 0 ? findLinkSuggestions(issue2.fields.summary, allDocs) : [];
25541
+ return {
25542
+ key: issue2.key,
25543
+ summary: issue2.fields.summary,
25544
+ currentStatus: issue2.fields.status?.name ?? "Unknown",
25545
+ issueType: issue2.fields.issuetype?.name ?? "Unknown",
25546
+ assignee: issue2.fields.assignee?.displayName ?? null,
25547
+ changes,
25548
+ comments,
25549
+ linkedIssues,
25550
+ confluenceLinks,
25551
+ marvinArtifacts,
25552
+ linkSuggestions
25553
+ };
25554
+ }
25555
+ function generateProposedActions(issues) {
25556
+ const actions = [];
25557
+ for (const issue2 of issues) {
25558
+ for (const artifact of issue2.marvinArtifacts) {
25559
+ if (artifact.statusDrift && artifact.proposedStatus) {
25560
+ actions.push({
25561
+ type: "status-update",
25562
+ description: `Update ${artifact.id} (${artifact.type}) status: ${artifact.currentStatus} \u2192 ${artifact.proposedStatus} (Jira ${issue2.key} is "${issue2.currentStatus}")`,
25563
+ artifactId: artifact.id,
25564
+ jiraKey: issue2.key
25565
+ });
25566
+ }
25567
+ }
25568
+ if (issue2.marvinArtifacts.length === 0 && (issue2.changes.length > 0 || issue2.comments.length > 0)) {
25569
+ actions.push({
25570
+ type: "unlinked-issue",
25571
+ description: `${issue2.key} ("${issue2.summary}") has activity but no Marvin artifact \u2014 consider linking or creating one`,
25572
+ jiraKey: issue2.key
25573
+ });
25574
+ }
25575
+ for (const suggestion of issue2.linkSuggestions) {
25576
+ actions.push({
25577
+ type: "link-suggestion",
25578
+ description: `${issue2.key} ("${issue2.summary}") may match ${suggestion.artifactId} ("${suggestion.artifactTitle}") \u2014 shared terms: ${suggestion.sharedTerms.join(", ")} (${Math.round(suggestion.score * 100)}% similarity)`,
25579
+ artifactId: suggestion.artifactId,
25580
+ jiraKey: issue2.key
25581
+ });
25582
+ }
25583
+ for (const comment of issue2.comments) {
25584
+ for (const signal of comment.signals) {
25585
+ if (signal.type === "blocker") {
25586
+ actions.push({
25587
+ type: "blocker-detected",
25588
+ description: `Blocker in ${issue2.key} comment by ${comment.author}: "${signal.snippet}"`,
25589
+ jiraKey: issue2.key
25590
+ });
25591
+ }
25592
+ if (signal.type === "decision") {
25593
+ actions.push({
25594
+ type: "decision-candidate",
25595
+ description: `Possible decision in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a decision artifact`,
25596
+ jiraKey: issue2.key
25597
+ });
25598
+ }
25599
+ if (signal.type === "question") {
25600
+ const linkedQuestion = issue2.marvinArtifacts.find(
25601
+ (a) => a.type === "question" && a.currentStatus !== "answered"
25602
+ );
25603
+ if (linkedQuestion) {
25604
+ actions.push({
25605
+ type: "question-candidate",
25606
+ description: `Question in ${issue2.key} comment by ${comment.author} \u2014 may relate to ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
25607
+ artifactId: linkedQuestion.id,
25608
+ jiraKey: issue2.key
25609
+ });
25610
+ } else {
25611
+ actions.push({
25612
+ type: "question-candidate",
25613
+ description: `Question in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a question artifact`,
25614
+ jiraKey: issue2.key
25615
+ });
25616
+ }
25617
+ }
25618
+ if (signal.type === "resolution") {
25619
+ const linkedQuestion = issue2.marvinArtifacts.find(
25620
+ (a) => a.type === "question" && a.currentStatus !== "answered"
25621
+ );
25622
+ if (linkedQuestion) {
25623
+ actions.push({
25624
+ type: "resolution-detected",
25625
+ description: `Resolution in ${issue2.key} by ${comment.author} may answer ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
25626
+ artifactId: linkedQuestion.id,
25627
+ jiraKey: issue2.key
25628
+ });
25629
+ }
25630
+ }
25631
+ }
25632
+ }
25633
+ for (const cl of issue2.confluenceLinks) {
25634
+ actions.push({
25635
+ type: "confluence-review",
25636
+ description: `Confluence page "${cl.title}" linked from ${issue2.key} \u2014 review for relevant updates`,
25637
+ jiraKey: issue2.key
25638
+ });
25639
+ }
25640
+ }
25641
+ return actions;
24529
25642
  }
24530
25643
 
24531
25644
  // src/skills/builtin/jira/tools.ts
@@ -24569,6 +25682,7 @@ function findByJiraKey(store, jiraKey) {
24569
25682
  function createJiraTools(store, projectConfig) {
24570
25683
  const jiraUserConfig = loadUserConfig().jira;
24571
25684
  const defaultProjectKey = projectConfig?.jira?.projectKey;
25685
+ const statusMap = projectConfig?.jira?.statusMap;
24572
25686
  return [
24573
25687
  // --- Local read tools ---
24574
25688
  tool20(
@@ -24729,9 +25843,9 @@ function createJiraTools(store, projectConfig) {
24729
25843
  // --- Local → Jira tools ---
24730
25844
  tool20(
24731
25845
  "push_artifact_to_jira",
24732
- "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
25846
+ "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.",
24733
25847
  {
24734
- artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
25848
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
24735
25849
  projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
24736
25850
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
24737
25851
  },
@@ -24772,6 +25886,24 @@ function createJiraTools(store, projectConfig) {
24772
25886
  description,
24773
25887
  issuetype: { name: args.issueType ?? "Task" }
24774
25888
  });
25889
+ const isDirectLink = artifact.frontmatter.type === "action" || artifact.frontmatter.type === "task";
25890
+ if (isDirectLink) {
25891
+ const existingTags = artifact.frontmatter.tags ?? [];
25892
+ store.update(args.artifactId, {
25893
+ jiraKey: jiraResult.key,
25894
+ jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
25895
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
25896
+ tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${jiraResult.key}`]
25897
+ });
25898
+ return {
25899
+ content: [
25900
+ {
25901
+ type: "text",
25902
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Linked directly on the artifact.`
25903
+ }
25904
+ ]
25905
+ };
25906
+ }
24775
25907
  const jiDoc = store.create(
24776
25908
  JIRA_TYPE,
24777
25909
  {
@@ -24840,118 +25972,483 @@ function createJiraTools(store, projectConfig) {
24840
25972
  };
24841
25973
  }
24842
25974
  ),
24843
- // --- Local link tool ---
25975
+ // --- Local link tool ---
25976
+ tool20(
25977
+ "link_artifact_to_jira",
25978
+ "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
25979
+ {
25980
+ jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
25981
+ artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
25982
+ },
25983
+ async (args) => {
25984
+ const doc = store.get(args.jiraIssueId);
25985
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
25986
+ return {
25987
+ content: [
25988
+ {
25989
+ type: "text",
25990
+ text: `Jira issue ${args.jiraIssueId} not found locally`
25991
+ }
25992
+ ],
25993
+ isError: true
25994
+ };
25995
+ }
25996
+ const artifact = store.get(args.artifactId);
25997
+ if (!artifact) {
25998
+ return {
25999
+ content: [
26000
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26001
+ ],
26002
+ isError: true
26003
+ };
26004
+ }
26005
+ const linked = doc.frontmatter.linkedArtifacts ?? [];
26006
+ if (linked.includes(args.artifactId)) {
26007
+ return {
26008
+ content: [
26009
+ {
26010
+ type: "text",
26011
+ text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
26012
+ }
26013
+ ]
26014
+ };
26015
+ }
26016
+ store.update(args.jiraIssueId, {
26017
+ linkedArtifacts: [...linked, args.artifactId]
26018
+ });
26019
+ return {
26020
+ content: [
26021
+ {
26022
+ type: "text",
26023
+ text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
26024
+ }
26025
+ ]
26026
+ };
26027
+ }
26028
+ ),
26029
+ // --- Direct Jira linking for actions/tasks ---
26030
+ tool20(
26031
+ "link_to_jira",
26032
+ "Link an existing Jira issue to a Marvin action or task (sets jiraKey directly on the artifact)",
26033
+ {
26034
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'A-001', 'T-003')"),
26035
+ jiraKey: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
26036
+ },
26037
+ async (args) => {
26038
+ const jira = createJiraClient(jiraUserConfig);
26039
+ if (!jira) return jiraNotConfiguredError();
26040
+ const artifact = store.get(args.artifactId);
26041
+ if (!artifact) {
26042
+ return {
26043
+ content: [
26044
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26045
+ ],
26046
+ isError: true
26047
+ };
26048
+ }
26049
+ if (artifact.frontmatter.type !== "action" && artifact.frontmatter.type !== "task") {
26050
+ return {
26051
+ content: [
26052
+ {
26053
+ type: "text",
26054
+ 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.`
26055
+ }
26056
+ ],
26057
+ isError: true
26058
+ };
26059
+ }
26060
+ const issue2 = await jira.client.getIssue(args.jiraKey);
26061
+ const existingTags = artifact.frontmatter.tags ?? [];
26062
+ store.update(args.artifactId, {
26063
+ jiraKey: args.jiraKey,
26064
+ jiraUrl: `https://${jira.host}/browse/${args.jiraKey}`,
26065
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
26066
+ tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${args.jiraKey}`]
26067
+ });
26068
+ return {
26069
+ content: [
26070
+ {
26071
+ type: "text",
26072
+ text: `Linked ${args.artifactId} to Jira ${args.jiraKey} ("${issue2.fields.summary}").`
26073
+ }
26074
+ ]
26075
+ };
26076
+ }
26077
+ ),
26078
+ // --- Jira status fetch (read-only) ---
24844
26079
  tool20(
24845
- "link_artifact_to_jira",
24846
- "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
26080
+ "fetch_jira_status",
26081
+ "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.",
24847
26082
  {
24848
- jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
24849
- artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
26083
+ artifactId: external_exports.string().optional().describe("Specific artifact ID to check, or omit to check all Jira-linked actions/tasks")
24850
26084
  },
24851
26085
  async (args) => {
24852
- const doc = store.get(args.jiraIssueId);
24853
- if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
26086
+ const jira = createJiraClient(jiraUserConfig);
26087
+ if (!jira) return jiraNotConfiguredError();
26088
+ const fetchResult = await fetchJiraStatus(
26089
+ store,
26090
+ jira.client,
26091
+ jira.host,
26092
+ args.artifactId,
26093
+ statusMap
26094
+ );
26095
+ const parts = [];
26096
+ if (fetchResult.artifacts.length > 0) {
26097
+ for (const a of fetchResult.artifacts) {
26098
+ const changes = [];
26099
+ if (a.statusChanged) {
26100
+ changes.push(`status: ${a.currentMarvinStatus} \u2192 ${a.proposedMarvinStatus}`);
26101
+ }
26102
+ if (a.progressChanged) {
26103
+ changes.push(`progress: ${a.currentProgress ?? 0}% \u2192 ${a.proposedProgress}%`);
26104
+ }
26105
+ const header = `${a.id} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}" [${a.jiraStatus}]`;
26106
+ if (changes.length > 0) {
26107
+ parts.push(`${header}
26108
+ Proposed changes: ${changes.join(", ")}`);
26109
+ } else {
26110
+ parts.push(`${header}
26111
+ No status/progress changes.`);
26112
+ }
26113
+ if (a.linkedIssues.length > 0) {
26114
+ const done = a.linkedIssues.filter((l) => l.isDone).length;
26115
+ parts.push(` Linked issues (${done}/${a.linkedIssues.length} done):`);
26116
+ for (const li of a.linkedIssues) {
26117
+ const icon = li.isDone ? "\u2713" : "\u25CB";
26118
+ parts.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
26119
+ }
26120
+ }
26121
+ }
26122
+ parts.push("");
26123
+ parts.push("This is a read-only preview. Use update_action or update_task to apply the proposed status/progress changes.");
26124
+ }
26125
+ if (fetchResult.errors.length > 0) {
26126
+ parts.push("Errors:");
26127
+ for (const err of fetchResult.errors) {
26128
+ parts.push(` ${err}`);
26129
+ }
26130
+ }
26131
+ if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
26132
+ parts.push("No Jira-linked actions/tasks found.");
26133
+ }
26134
+ return {
26135
+ content: [{ type: "text", text: parts.join("\n") }],
26136
+ isError: fetchResult.errors.length > 0 && fetchResult.artifacts.length === 0
26137
+ };
26138
+ },
26139
+ { annotations: { readOnlyHint: true } }
26140
+ ),
26141
+ // --- Jira status discovery ---
26142
+ tool20(
26143
+ "fetch_jira_statuses",
26144
+ "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.",
26145
+ {
26146
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'MCB1'). Falls back to jira.projectKey from config."),
26147
+ maxResults: external_exports.number().optional().describe("Max issues to scan (default 100)")
26148
+ },
26149
+ async (args) => {
26150
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
26151
+ if (!resolvedProjectKey) {
24854
26152
  return {
24855
26153
  content: [
24856
26154
  {
24857
26155
  type: "text",
24858
- text: `Jira issue ${args.jiraIssueId} not found locally`
26156
+ text: "No projectKey provided and no default configured."
24859
26157
  }
24860
26158
  ],
24861
26159
  isError: true
24862
26160
  };
24863
26161
  }
24864
- const artifact = store.get(args.artifactId);
24865
- if (!artifact) {
26162
+ const jira = createJiraClient(jiraUserConfig);
26163
+ if (!jira) return jiraNotConfiguredError();
26164
+ const host = jira.host;
26165
+ const auth = "Basic " + Buffer.from(
26166
+ `${jiraUserConfig?.email ?? process.env.JIRA_EMAIL}:${jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN}`
26167
+ ).toString("base64");
26168
+ const params = new URLSearchParams({
26169
+ jql: `project = ${resolvedProjectKey}`,
26170
+ maxResults: String(args.maxResults ?? 100),
26171
+ fields: "status"
26172
+ });
26173
+ const resp = await fetch(`https://${host}/rest/api/3/search/jql?${params}`, {
26174
+ headers: { Authorization: auth, Accept: "application/json" }
26175
+ });
26176
+ if (!resp.ok) {
26177
+ const text = await resp.text().catch(() => "");
24866
26178
  return {
24867
26179
  content: [
24868
- { type: "text", text: `Artifact ${args.artifactId} not found` }
26180
+ {
26181
+ type: "text",
26182
+ text: `Jira API error ${resp.status}: ${text}`
26183
+ }
24869
26184
  ],
24870
26185
  isError: true
24871
26186
  };
24872
26187
  }
24873
- const linked = doc.frontmatter.linkedArtifacts ?? [];
24874
- if (linked.includes(args.artifactId)) {
26188
+ const data = await resp.json();
26189
+ const statusCounts = /* @__PURE__ */ new Map();
26190
+ for (const issue2 of data.issues) {
26191
+ const s = issue2.fields.status.name;
26192
+ statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
26193
+ }
26194
+ const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
26195
+ const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
26196
+ const actionLookup = /* @__PURE__ */ new Map();
26197
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
26198
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
26199
+ }
26200
+ const taskLookup = /* @__PURE__ */ new Map();
26201
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
26202
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
26203
+ }
26204
+ const parts = [
26205
+ `Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
26206
+ ""
26207
+ ];
26208
+ const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
26209
+ const unmappedAction = [];
26210
+ const unmappedTask = [];
26211
+ for (const [status, count] of sorted) {
26212
+ const actionTarget = actionLookup.get(status.toLowerCase());
26213
+ const taskTarget = taskLookup.get(status.toLowerCase());
26214
+ const actionLabel = actionTarget ? `\u2192 ${actionTarget}` : "UNMAPPED (\u2192 open)";
26215
+ const taskLabel = taskTarget ? `\u2192 ${taskTarget}` : "UNMAPPED (\u2192 backlog)";
26216
+ parts.push(` ${status} (${count} issues)`);
26217
+ parts.push(` action: ${actionLabel}`);
26218
+ parts.push(` task: ${taskLabel}`);
26219
+ if (!actionTarget) unmappedAction.push(status);
26220
+ if (!taskTarget) unmappedTask.push(status);
26221
+ }
26222
+ if (unmappedAction.length > 0 || unmappedTask.length > 0) {
26223
+ parts.push("");
26224
+ parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
26225
+ parts.push(" jira:");
26226
+ parts.push(" statusMap:");
26227
+ if (unmappedAction.length > 0) {
26228
+ parts.push(" action:");
26229
+ parts.push(` # Map these: ${unmappedAction.join(", ")}`);
26230
+ parts.push(" # <marvin-status>: [<jira-status>, ...]");
26231
+ }
26232
+ if (unmappedTask.length > 0) {
26233
+ parts.push(" task:");
26234
+ parts.push(` # Map these: ${unmappedTask.join(", ")}`);
26235
+ parts.push(" # <marvin-status>: [<jira-status>, ...]");
26236
+ }
26237
+ } else {
26238
+ parts.push("");
26239
+ parts.push("All statuses are mapped.");
26240
+ }
26241
+ const usingConfig = statusMap?.action || statusMap?.task;
26242
+ parts.push("");
26243
+ parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
26244
+ return {
26245
+ content: [{ type: "text", text: parts.join("\n") }]
26246
+ };
26247
+ },
26248
+ { annotations: { readOnlyHint: true } }
26249
+ ),
26250
+ // --- Jira daily summary ---
26251
+ tool20(
26252
+ "fetch_jira_daily",
26253
+ "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.",
26254
+ {
26255
+ from: external_exports.string().optional().describe("Start date (YYYY-MM-DD). Defaults to today."),
26256
+ to: external_exports.string().optional().describe("End date (YYYY-MM-DD). Defaults to same as 'from'."),
26257
+ projectKey: external_exports.string().optional().describe("Jira project key. Falls back to jira.projectKey from config.")
26258
+ },
26259
+ async (args) => {
26260
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
26261
+ if (!resolvedProjectKey) {
24875
26262
  return {
24876
26263
  content: [
24877
26264
  {
24878
26265
  type: "text",
24879
- text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
26266
+ text: "No projectKey provided and no default configured."
24880
26267
  }
24881
- ]
26268
+ ],
26269
+ isError: true
24882
26270
  };
24883
26271
  }
24884
- store.update(args.jiraIssueId, {
24885
- linkedArtifacts: [...linked, args.artifactId]
24886
- });
26272
+ const jira = createJiraClient(jiraUserConfig);
26273
+ if (!jira) return jiraNotConfiguredError();
26274
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
26275
+ const fromDate = args.from ?? today;
26276
+ const toDate = args.to ?? fromDate;
26277
+ const daily = await fetchJiraDaily(
26278
+ store,
26279
+ jira.client,
26280
+ jira.host,
26281
+ resolvedProjectKey,
26282
+ { from: fromDate, to: toDate },
26283
+ statusMap
26284
+ );
24887
26285
  return {
24888
- content: [
24889
- {
24890
- type: "text",
24891
- text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
24892
- }
24893
- ]
26286
+ content: [{ type: "text", text: formatDailySummary(daily) }],
26287
+ isError: daily.errors.length > 0 && daily.issues.length === 0
24894
26288
  };
24895
- }
26289
+ },
26290
+ { annotations: { readOnlyHint: true } }
24896
26291
  )
24897
26292
  ];
24898
26293
  }
26294
+ function formatDailySummary(daily) {
26295
+ const parts = [];
26296
+ const rangeLabel = daily.dateRange.from === daily.dateRange.to ? daily.dateRange.from : `${daily.dateRange.from} to ${daily.dateRange.to}`;
26297
+ parts.push(`Jira Daily Summary \u2014 ${daily.projectKey} \u2014 ${rangeLabel}`);
26298
+ parts.push(`${daily.issues.length} issue(s) updated.
26299
+ `);
26300
+ const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
26301
+ const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
26302
+ if (linked.length > 0) {
26303
+ parts.push("## Linked Issues (with Marvin artifacts)\n");
26304
+ for (const issue2 of linked) {
26305
+ parts.push(formatIssueEntry(issue2));
26306
+ }
26307
+ }
26308
+ if (unlinked.length > 0) {
26309
+ parts.push("## Unlinked Issues (no Marvin artifact)\n");
26310
+ for (const issue2 of unlinked) {
26311
+ parts.push(formatIssueEntry(issue2));
26312
+ }
26313
+ }
26314
+ if (daily.proposedActions.length > 0) {
26315
+ parts.push("## Proposed Actions\n");
26316
+ for (const action of daily.proposedActions) {
26317
+ 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}";
26318
+ parts.push(` ${icon} ${action.description}`);
26319
+ }
26320
+ parts.push("");
26321
+ parts.push("These are suggestions. Use update_action, update_task, or other tools to apply changes.");
26322
+ }
26323
+ if (daily.errors.length > 0) {
26324
+ parts.push("\n## Errors\n");
26325
+ for (const err of daily.errors) {
26326
+ parts.push(` ${err}`);
26327
+ }
26328
+ }
26329
+ return parts.join("\n");
26330
+ }
26331
+ function formatIssueEntry(issue2) {
26332
+ const lines = [];
26333
+ const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
26334
+ const artifactLabel = artifacts ? ` \u2192 ${artifacts}` : "";
26335
+ lines.push(`### ${issue2.key} \u2014 ${issue2.summary} [${issue2.currentStatus}]${artifactLabel}`);
26336
+ lines.push(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`);
26337
+ for (const a of issue2.marvinArtifacts) {
26338
+ if (a.statusDrift) {
26339
+ lines.push(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`);
26340
+ }
26341
+ }
26342
+ if (issue2.changes.length > 0) {
26343
+ lines.push(" Changes:");
26344
+ for (const c of issue2.changes) {
26345
+ lines.push(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`);
26346
+ }
26347
+ }
26348
+ if (issue2.comments.length > 0) {
26349
+ lines.push(` Comments (${issue2.comments.length}):`);
26350
+ for (const c of issue2.comments) {
26351
+ let signalIcons = "";
26352
+ if (c.signals.length > 0) {
26353
+ const icons = c.signals.map(
26354
+ (s) => s.type === "blocker" ? "\u{1F6AB}" : s.type === "decision" ? "\u2696" : s.type === "question" ? "?" : "\u2713"
26355
+ );
26356
+ signalIcons = ` [${icons.join("")}]`;
26357
+ }
26358
+ lines.push(` ${c.author} (${c.created.slice(0, 16)})${signalIcons}: ${c.bodyPreview}`);
26359
+ }
26360
+ }
26361
+ if (issue2.linkSuggestions.length > 0) {
26362
+ lines.push(" Possible Marvin matches:");
26363
+ for (const s of issue2.linkSuggestions) {
26364
+ lines.push(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`);
26365
+ }
26366
+ }
26367
+ if (issue2.linkedIssues.length > 0) {
26368
+ lines.push(" Linked issues:");
26369
+ for (const li of issue2.linkedIssues) {
26370
+ const icon = li.isDone ? "\u2713" : "\u25CB";
26371
+ lines.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
26372
+ }
26373
+ }
26374
+ if (issue2.confluenceLinks.length > 0) {
26375
+ lines.push(" Confluence pages:");
26376
+ for (const cl of issue2.confluenceLinks) {
26377
+ lines.push(` \u{1F4C4} ${cl.title}: ${cl.url}`);
26378
+ }
26379
+ }
26380
+ lines.push("");
26381
+ return lines.join("\n");
26382
+ }
24899
26383
 
24900
26384
  // src/skills/builtin/jira/index.ts
26385
+ var COMMON_TOOLS = `**Available tools:**
26386
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues (JI-xxx documents)
26387
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
26388
+ - \`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.
26389
+ - \`link_to_jira\` \u2014 link an existing Jira issue to a Marvin action or task (sets \`jiraKey\` directly on the artifact)
26390
+ - \`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.
26391
+ - \`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).
26392
+ - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
26393
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
26394
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx`;
26395
+ var COMMON_WORKFLOW = `**Jira sync workflow:**
26396
+ 1. Call \`fetch_jira_status\` to see what Jira reports for linked artifacts
26397
+ 2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
26398
+ 3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
26399
+
26400
+ **Daily review workflow:**
26401
+ 1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
26402
+ 2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
26403
+ 3. Use existing tools to apply changes, create new artifacts, or link untracked issues`;
24901
26404
  var jiraSkill = {
24902
26405
  id: "jira",
24903
26406
  name: "Jira Integration",
24904
26407
  description: "Bidirectional sync between Marvin artifacts and Jira issues",
24905
26408
  version: "1.0.0",
24906
26409
  format: "builtin-ts",
24907
- // No default persona affinity — opt-in via config.yaml skills section
24908
26410
  documentTypeRegistrations: [
24909
26411
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
24910
26412
  ],
24911
26413
  tools: (store, projectConfig) => createJiraTools(store, projectConfig),
24912
26414
  promptFragments: {
24913
- "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26415
+ "product-owner": `You have the **Jira Integration** skill.
24914
26416
 
24915
- **Available tools:**
24916
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
24917
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
24918
- - \`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\`.
24919
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
24920
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26417
+ ${COMMON_TOOLS}
26418
+
26419
+ ${COMMON_WORKFLOW}
24921
26420
 
24922
26421
  **As Product Owner, use Jira integration to:**
26422
+ - Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
24923
26423
  - Pull stakeholder-reported issues for triage and prioritization
24924
26424
  - Push approved features as Stories for development tracking
24925
26425
  - Link decisions to Jira issues for audit trail and traceability
24926
- - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
24927
- "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26426
+ - Use \`fetch_jira_statuses\` when setting up a new project to configure status mappings`,
26427
+ "tech-lead": `You have the **Jira Integration** skill.
24928
26428
 
24929
- **Available tools:**
24930
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
24931
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
24932
- - \`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\`.
24933
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
24934
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26429
+ ${COMMON_TOOLS}
26430
+
26431
+ ${COMMON_WORKFLOW}
24935
26432
 
24936
26433
  **As Tech Lead, use Jira integration to:**
26434
+ - Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
24937
26435
  - Pull technical issues and bugs for sprint planning and estimation
24938
26436
  - Push epics, tasks, and technical decisions to Jira for cross-team visibility
24939
- - Bidirectional sync to keep local governance and Jira in alignment
24940
- - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
24941
- "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26437
+ - Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
26438
+ - Use \`fetch_jira_statuses\` to verify status mappings match the team's Jira workflow`,
26439
+ "delivery-manager": `You have the **Jira Integration** skill.
24942
26440
 
24943
- **Available tools:**
24944
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
24945
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
24946
- - \`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\`.
24947
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
24948
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26441
+ ${COMMON_TOOLS}
26442
+
26443
+ ${COMMON_WORKFLOW}
26444
+ This is a third path for progress tracking alongside Contributions and Meetings.
24949
26445
 
24950
26446
  **As Delivery Manager, use Jira integration to:**
26447
+ - Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
24951
26448
  - Pull sprint issues for tracking progress and blockers
24952
- - Push actions, decisions, and tasks to Jira for stakeholder visibility
24953
- - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
24954
- - Sync status between Marvin governance items and Jira issues`
26449
+ - Push actions and tasks to Jira for stakeholder visibility
26450
+ - Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
26451
+ - Use \`fetch_jira_statuses\` to ensure Jira workflow statuses are properly mapped`
24955
26452
  }
24956
26453
  };
24957
26454
 
@@ -25151,8 +26648,8 @@ function generateTaskMasterPrd(title, ctx, projectOverview) {
25151
26648
  let priorityIdx = 1;
25152
26649
  for (const feature of ctx.features) {
25153
26650
  const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
25154
- const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
25155
- return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
26651
+ const statusOrder2 = { "in-progress": 0, planned: 1, done: 2 };
26652
+ return (statusOrder2[a.status] ?? 99) - (statusOrder2[b.status] ?? 99);
25156
26653
  });
25157
26654
  if (featureEpics.length === 0) continue;
25158
26655
  lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
@@ -30069,12 +31566,355 @@ Run "marvin doctor --fix" to auto-repair fixable issues.`));
30069
31566
  console.log();
30070
31567
  }
30071
31568
 
31569
+ // src/cli/commands/jira.ts
31570
+ import chalk20 from "chalk";
31571
+ async function jiraSyncCommand(artifactId, options = {}) {
31572
+ const project = loadProject();
31573
+ const plugin = resolvePlugin(project.config.methodology);
31574
+ const registrations = plugin?.documentTypeRegistrations ?? [];
31575
+ const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
31576
+ const store = new DocumentStore(project.marvinDir, [...registrations, jiReg]);
31577
+ const jiraUserConfig = loadUserConfig().jira;
31578
+ const jira = createJiraClient(jiraUserConfig);
31579
+ if (!jira) {
31580
+ console.log(
31581
+ chalk20.red(
31582
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31583
+ )
31584
+ );
31585
+ return;
31586
+ }
31587
+ const statusMap = project.config.jira?.statusMap;
31588
+ const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
31589
+ console.log(chalk20.dim(label));
31590
+ if (options.dryRun) {
31591
+ const fetchResult = await fetchJiraStatus(
31592
+ store,
31593
+ jira.client,
31594
+ jira.host,
31595
+ artifactId,
31596
+ statusMap
31597
+ );
31598
+ const withChanges = fetchResult.artifacts.filter(
31599
+ (a) => a.statusChanged || a.progressChanged
31600
+ );
31601
+ const noChanges = fetchResult.artifacts.filter(
31602
+ (a) => !a.statusChanged && !a.progressChanged
31603
+ );
31604
+ if (withChanges.length > 0) {
31605
+ console.log(chalk20.yellow(`
31606
+ Proposed changes for ${withChanges.length} artifact(s):`));
31607
+ for (const a of withChanges) {
31608
+ console.log(` ${chalk20.bold(a.id)} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}"`);
31609
+ if (a.statusChanged) {
31610
+ console.log(
31611
+ ` status: ${chalk20.yellow(a.currentMarvinStatus)} \u2192 ${chalk20.green(a.proposedMarvinStatus)}`
31612
+ );
31613
+ }
31614
+ if (a.progressChanged) {
31615
+ console.log(
31616
+ ` progress: ${chalk20.yellow(String(a.currentProgress ?? 0) + "%")} \u2192 ${chalk20.green(String(a.proposedProgress) + "%")}`
31617
+ );
31618
+ }
31619
+ if (a.linkedIssues.length > 0) {
31620
+ const done = a.linkedIssues.filter((l) => l.isDone).length;
31621
+ console.log(chalk20.dim(` ${done}/${a.linkedIssues.length} linked issues done`));
31622
+ }
31623
+ }
31624
+ console.log(chalk20.dim("\nRun without --dry-run to apply these changes."));
31625
+ }
31626
+ if (noChanges.length > 0) {
31627
+ console.log(chalk20.dim(`
31628
+ ${noChanges.length} artifact(s) already in sync.`));
31629
+ }
31630
+ if (fetchResult.errors.length > 0) {
31631
+ console.log(chalk20.red("\nErrors:"));
31632
+ for (const err of fetchResult.errors) {
31633
+ console.log(chalk20.red(` ${err}`));
31634
+ }
31635
+ }
31636
+ if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
31637
+ console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to check."));
31638
+ }
31639
+ return;
31640
+ }
31641
+ const result = await syncJiraProgress(
31642
+ store,
31643
+ jira.client,
31644
+ jira.host,
31645
+ artifactId,
31646
+ statusMap
31647
+ );
31648
+ if (result.updated.length > 0) {
31649
+ console.log(chalk20.green(`
31650
+ Updated ${result.updated.length} artifact(s):`));
31651
+ for (const entry of result.updated) {
31652
+ const statusChange = entry.oldStatus !== entry.newStatus ? `${chalk20.yellow(entry.oldStatus)} \u2192 ${chalk20.green(entry.newStatus)}` : chalk20.dim(entry.newStatus);
31653
+ console.log(` ${chalk20.bold(entry.id)} (${entry.jiraKey}): ${statusChange}`);
31654
+ if (entry.linkedIssues.length > 0) {
31655
+ const done = entry.linkedIssues.filter((l) => l.isDone).length;
31656
+ console.log(
31657
+ chalk20.dim(` ${done}/${entry.linkedIssues.length} linked issues done`)
31658
+ );
31659
+ for (const li of entry.linkedIssues) {
31660
+ const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
31661
+ console.log(
31662
+ chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}]`)
31663
+ );
31664
+ }
31665
+ }
31666
+ }
31667
+ }
31668
+ if (result.unchanged > 0) {
31669
+ console.log(chalk20.dim(`
31670
+ ${result.unchanged} artifact(s) unchanged.`));
31671
+ }
31672
+ if (result.errors.length > 0) {
31673
+ console.log(chalk20.red("\nErrors:"));
31674
+ for (const err of result.errors) {
31675
+ console.log(chalk20.red(` ${err}`));
31676
+ }
31677
+ }
31678
+ if (result.updated.length === 0 && result.unchanged === 0 && result.errors.length === 0) {
31679
+ console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to sync."));
31680
+ }
31681
+ }
31682
+ async function jiraStatusesCommand(projectKey) {
31683
+ const project = loadProject();
31684
+ const jiraUserConfig = loadUserConfig().jira;
31685
+ const jira = createJiraClient(jiraUserConfig);
31686
+ if (!jira) {
31687
+ console.log(
31688
+ chalk20.red(
31689
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31690
+ )
31691
+ );
31692
+ return;
31693
+ }
31694
+ const resolvedProjectKey = projectKey ?? project.config.jira?.projectKey;
31695
+ if (!resolvedProjectKey) {
31696
+ console.log(
31697
+ chalk20.red(
31698
+ "No project key provided. Pass it as an argument or set jira.projectKey in .marvin/config.yaml."
31699
+ )
31700
+ );
31701
+ return;
31702
+ }
31703
+ console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
31704
+ const statusMap = project.config.jira?.statusMap;
31705
+ const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
31706
+ const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
31707
+ const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
31708
+ const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
31709
+ const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
31710
+ const params = new URLSearchParams({
31711
+ jql: `project = ${resolvedProjectKey}`,
31712
+ maxResults: "100",
31713
+ fields: "status"
31714
+ });
31715
+ const resp = await fetch(`https://${jira.host}/rest/api/3/search/jql?${params}`, {
31716
+ headers: { Authorization: auth, Accept: "application/json" }
31717
+ });
31718
+ if (!resp.ok) {
31719
+ const text = await resp.text().catch(() => "");
31720
+ console.log(chalk20.red(`Jira API error ${resp.status}: ${text}`));
31721
+ return;
31722
+ }
31723
+ const data = await resp.json();
31724
+ const statusCounts = /* @__PURE__ */ new Map();
31725
+ for (const issue2 of data.issues) {
31726
+ const s = issue2.fields.status.name;
31727
+ statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
31728
+ }
31729
+ const actionLookup = /* @__PURE__ */ new Map();
31730
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
31731
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
31732
+ }
31733
+ const taskLookup = /* @__PURE__ */ new Map();
31734
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
31735
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
31736
+ }
31737
+ console.log(
31738
+ `
31739
+ Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.bold(resolvedProjectKey)} (scanned ${data.issues.length} of ${data.total} issues):
31740
+ `
31741
+ );
31742
+ const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
31743
+ let hasUnmapped = false;
31744
+ for (const [status, count] of sorted) {
31745
+ const actionTarget = actionLookup.get(status.toLowerCase());
31746
+ const taskTarget = taskLookup.get(status.toLowerCase());
31747
+ const actionLabel = actionTarget ? chalk20.green(`\u2192 ${actionTarget}`) : chalk20.yellow("UNMAPPED (\u2192 open)");
31748
+ const taskLabel = taskTarget ? chalk20.green(`\u2192 ${taskTarget}`) : chalk20.yellow("UNMAPPED (\u2192 backlog)");
31749
+ if (!actionTarget || !taskTarget) hasUnmapped = true;
31750
+ console.log(` ${chalk20.bold(status)} ${chalk20.dim(`(${count} issues)`)}`);
31751
+ console.log(` action: ${actionLabel}`);
31752
+ console.log(` task: ${taskLabel}`);
31753
+ }
31754
+ if (hasUnmapped) {
31755
+ console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
31756
+ console.log(chalk20.dim(" jira:"));
31757
+ console.log(chalk20.dim(" statusMap:"));
31758
+ console.log(chalk20.dim(" action:"));
31759
+ console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
31760
+ console.log(chalk20.dim(" task:"));
31761
+ console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
31762
+ } else {
31763
+ console.log(chalk20.green("\nAll statuses are mapped."));
31764
+ }
31765
+ const usingConfig = statusMap?.action || statusMap?.task;
31766
+ console.log(
31767
+ chalk20.dim(
31768
+ usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
31769
+ )
31770
+ );
31771
+ }
31772
+ async function jiraDailyCommand(options) {
31773
+ const proj = loadProject();
31774
+ const plugin = resolvePlugin(proj.config.methodology);
31775
+ const registrations = plugin?.documentTypeRegistrations ?? [];
31776
+ const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
31777
+ const store = new DocumentStore(proj.marvinDir, [...registrations, jiReg]);
31778
+ const jiraUserConfig = loadUserConfig().jira;
31779
+ const jira = createJiraClient(jiraUserConfig);
31780
+ if (!jira) {
31781
+ console.log(
31782
+ chalk20.red(
31783
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31784
+ )
31785
+ );
31786
+ return;
31787
+ }
31788
+ const resolvedProjectKey = options.project ?? proj.config.jira?.projectKey;
31789
+ if (!resolvedProjectKey) {
31790
+ console.log(
31791
+ chalk20.red(
31792
+ "No project key provided. Use --project or set jira.projectKey in .marvin/config.yaml."
31793
+ )
31794
+ );
31795
+ return;
31796
+ }
31797
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31798
+ const fromDate = options.from ?? today;
31799
+ const toDate = options.to ?? fromDate;
31800
+ const statusMap = proj.config.jira?.statusMap;
31801
+ const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
31802
+ console.log(
31803
+ chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
31804
+ );
31805
+ const daily = await fetchJiraDaily(
31806
+ store,
31807
+ jira.client,
31808
+ jira.host,
31809
+ resolvedProjectKey,
31810
+ { from: fromDate, to: toDate },
31811
+ statusMap
31812
+ );
31813
+ console.log(
31814
+ `
31815
+ ${chalk20.bold(`Jira Daily \u2014 ${resolvedProjectKey} \u2014 ${rangeLabel}`)}`
31816
+ );
31817
+ console.log(`${daily.issues.length} issue(s) updated.
31818
+ `);
31819
+ const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
31820
+ const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
31821
+ if (linked.length > 0) {
31822
+ console.log(chalk20.underline("Linked Issues (with Marvin artifacts):\n"));
31823
+ for (const issue2 of linked) {
31824
+ printIssueEntry(issue2);
31825
+ }
31826
+ }
31827
+ if (unlinked.length > 0) {
31828
+ console.log(chalk20.underline("Unlinked Issues (no Marvin artifact):\n"));
31829
+ for (const issue2 of unlinked) {
31830
+ printIssueEntry(issue2);
31831
+ }
31832
+ }
31833
+ if (daily.proposedActions.length > 0) {
31834
+ console.log(chalk20.underline("Proposed Actions:\n"));
31835
+ for (const action of daily.proposedActions) {
31836
+ 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}");
31837
+ console.log(` ${icon} ${action.description}`);
31838
+ }
31839
+ console.log();
31840
+ }
31841
+ if (daily.errors.length > 0) {
31842
+ console.log(chalk20.red("Errors:"));
31843
+ for (const err of daily.errors) {
31844
+ console.log(chalk20.red(` ${err}`));
31845
+ }
31846
+ }
31847
+ if (daily.issues.length === 0 && daily.errors.length === 0) {
31848
+ console.log(chalk20.dim("No Jira activity found for this period."));
31849
+ }
31850
+ }
31851
+ function printIssueEntry(issue2) {
31852
+ const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
31853
+ const artifactLabel = artifacts ? chalk20.cyan(` \u2192 ${artifacts}`) : "";
31854
+ console.log(
31855
+ ` ${chalk20.bold(issue2.key)} \u2014 ${issue2.summary} [${chalk20.yellow(issue2.currentStatus)}]${artifactLabel}`
31856
+ );
31857
+ console.log(
31858
+ chalk20.dim(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`)
31859
+ );
31860
+ for (const a of issue2.marvinArtifacts) {
31861
+ if (a.statusDrift) {
31862
+ console.log(
31863
+ chalk20.yellow(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`)
31864
+ );
31865
+ }
31866
+ }
31867
+ if (issue2.changes.length > 0) {
31868
+ console.log(chalk20.dim(" Changes:"));
31869
+ for (const c of issue2.changes) {
31870
+ console.log(
31871
+ chalk20.dim(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`)
31872
+ );
31873
+ }
31874
+ }
31875
+ if (issue2.comments.length > 0) {
31876
+ console.log(chalk20.dim(` Comments (${issue2.comments.length}):`));
31877
+ for (const c of issue2.comments) {
31878
+ let signalLabel = "";
31879
+ if (c.signals.length > 0) {
31880
+ const labels = c.signals.map(
31881
+ (s) => s.type === "blocker" ? chalk20.red("\u{1F6AB}blocker") : s.type === "decision" ? chalk20.yellow("\u2696decision") : s.type === "question" ? chalk20.magenta("?question") : chalk20.green("\u2713resolution")
31882
+ );
31883
+ signalLabel = ` ${labels.join(" ")}`;
31884
+ }
31885
+ console.log(chalk20.dim(` ${c.author} (${c.created.slice(0, 16)})${signalLabel}: ${c.bodyPreview}`));
31886
+ }
31887
+ }
31888
+ if (issue2.linkSuggestions.length > 0) {
31889
+ console.log(chalk20.cyan(" Possible Marvin matches:"));
31890
+ for (const s of issue2.linkSuggestions) {
31891
+ console.log(
31892
+ chalk20.cyan(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`)
31893
+ );
31894
+ }
31895
+ }
31896
+ if (issue2.linkedIssues.length > 0) {
31897
+ console.log(chalk20.dim(" Linked issues:"));
31898
+ for (const li of issue2.linkedIssues) {
31899
+ const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
31900
+ console.log(chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`));
31901
+ }
31902
+ }
31903
+ if (issue2.confluenceLinks.length > 0) {
31904
+ console.log(chalk20.dim(" Confluence pages:"));
31905
+ for (const cl of issue2.confluenceLinks) {
31906
+ console.log(chalk20.dim(` \u{1F4C4} ${cl.title}: ${cl.url}`));
31907
+ }
31908
+ }
31909
+ console.log();
31910
+ }
31911
+
30072
31912
  // src/cli/program.ts
30073
31913
  function createProgram() {
30074
31914
  const program = new Command();
30075
31915
  program.name("marvin").description(
30076
31916
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
30077
- ).version("0.5.5");
31917
+ ).version("0.5.8");
30078
31918
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
30079
31919
  await initCommand();
30080
31920
  });
@@ -30170,6 +32010,16 @@ function createProgram() {
30170
32010
  generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
30171
32011
  await generateClaudeMdCommand(options);
30172
32012
  });
32013
+ const jiraCmd = program.command("jira").description("Jira integration commands");
32014
+ 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) => {
32015
+ await jiraSyncCommand(artifactId, options);
32016
+ });
32017
+ jiraCmd.command("statuses [projectKey]").description("Show Jira project statuses and their Marvin status mappings").action(async (projectKey) => {
32018
+ await jiraStatusesCommand(projectKey);
32019
+ });
32020
+ 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) => {
32021
+ await jiraDailyCommand(options);
32022
+ });
30173
32023
  return program;
30174
32024
  }
30175
32025
  export {