mrvn-cli 0.5.5 → 0.5.7

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_STATUSES14 = /* @__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_STATUSES14.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,6 +21475,186 @@ function upcomingPage(data) {
21226
21475
  `;
21227
21476
  }
21228
21477
 
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) {
21486
+ if (!data) {
21487
+ return `
21488
+ <div class="page-header">
21489
+ <h2>Sprint Summary</h2>
21490
+ <div class="subtitle">AI-powered sprint narrative</div>
21491
+ </div>
21492
+ <div class="empty">
21493
+ <h3>No Active Sprint</h3>
21494
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
21495
+ </div>`;
21496
+ }
21497
+ const statsCards = `
21498
+ <div class="cards">
21499
+ <div class="card">
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>
21503
+ </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>
21508
+ </div>
21509
+ <div class="card">
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>
21513
+ </div>
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
+
21229
21658
  // src/web/templates/pages/sprint-blockers.ts
21230
21659
  function sprintBlockersPage(data, store) {
21231
21660
  if (!data) {
@@ -25151,8 +25580,8 @@ function generateTaskMasterPrd(title, ctx, projectOverview) {
25151
25580
  let priorityIdx = 1;
25152
25581
  for (const feature of ctx.features) {
25153
25582
  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);
25583
+ const statusOrder2 = { "in-progress": 0, planned: 1, done: 2 };
25584
+ return (statusOrder2[a.status] ?? 99) - (statusOrder2[b.status] ?? 99);
25156
25585
  });
25157
25586
  if (featureEpics.length === 0) continue;
25158
25587
  lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
@@ -30074,7 +30503,7 @@ function createProgram() {
30074
30503
  const program = new Command();
30075
30504
  program.name("marvin").description(
30076
30505
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
30077
- ).version("0.5.5");
30506
+ ).version("0.5.7");
30078
30507
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
30079
30508
  await initCommand();
30080
30509
  });