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.
@@ -15707,6 +15707,7 @@ function collectSprintSummaryData(store, sprintId) {
15707
15707
  type: doc.frontmatter.type,
15708
15708
  status: doc.frontmatter.status,
15709
15709
  progress: getEffectiveProgress(doc.frontmatter),
15710
+ owner: doc.frontmatter.owner,
15710
15711
  workFocus: focusTag ? focusTag.slice(6) : void 0,
15711
15712
  aboutArtifact: about
15712
15713
  };
@@ -15913,10 +15914,10 @@ function getBoardData(store, type) {
15913
15914
  if (!byStatus.has(status)) byStatus.set(status, []);
15914
15915
  byStatus.get(status).push(doc);
15915
15916
  }
15916
- const statusOrder = ["open", "draft", "in-progress", "blocked"];
15917
+ const statusOrder2 = ["open", "draft", "in-progress", "blocked"];
15917
15918
  const allStatuses = [...byStatus.keys()];
15918
15919
  const ordered = [];
15919
- for (const s of statusOrder) {
15920
+ for (const s of statusOrder2) {
15920
15921
  if (allStatuses.includes(s)) ordered.push(s);
15921
15922
  }
15922
15923
  for (const s of allStatuses.sort()) {
@@ -19707,8 +19708,8 @@ function generateTaskMasterPrd(title, ctx, projectOverview) {
19707
19708
  let priorityIdx = 1;
19708
19709
  for (const feature of ctx.features) {
19709
19710
  const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
19710
- const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
19711
- return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
19711
+ const statusOrder2 = { "in-progress": 0, planned: 1, done: 2 };
19712
+ return (statusOrder2[a.status] ?? 99) - (statusOrder2[b.status] ?? 99);
19712
19713
  });
19713
19714
  if (featureEpics.length === 0) continue;
19714
19715
  lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
@@ -21991,6 +21992,31 @@ tr:hover td {
21991
21992
  .focus-group-progress {
21992
21993
  width: 96px;
21993
21994
  }
21995
+
21996
+ /* Owner badges for DM sprint view */
21997
+ .owner-badge {
21998
+ display: inline-block;
21999
+ padding: 0.1rem 0.5rem;
22000
+ border-radius: 999px;
22001
+ font-size: 0.65rem;
22002
+ font-weight: 700;
22003
+ text-transform: uppercase;
22004
+ letter-spacing: 0.04em;
22005
+ white-space: nowrap;
22006
+ }
22007
+ .owner-badge-po { background: rgba(108, 140, 255, 0.18); color: #6c8cff; }
22008
+ .owner-badge-tl { background: rgba(251, 191, 36, 0.18); color: #fbbf24; }
22009
+ .owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
22010
+ .owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
22011
+
22012
+ /* Group header rows (PO dashboard decisions/deps) */
22013
+ .group-header-row td {
22014
+ background: var(--bg-hover);
22015
+ padding-top: 0.5rem;
22016
+ padding-bottom: 0.5rem;
22017
+ border-bottom: 1px solid var(--border);
22018
+ font-size: 0.8rem;
22019
+ }
21994
22020
  `;
21995
22021
  }
21996
22022
 
@@ -22831,19 +22857,55 @@ function buildHealthGauge(categories) {
22831
22857
  }
22832
22858
 
22833
22859
  // src/web/templates/pages/po/dashboard.ts
22860
+ var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
22861
+ var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
22834
22862
  function poDashboardPage(ctx) {
22835
22863
  const overview = getOverviewData(ctx.store);
22836
- const upcoming = getUpcomingData(ctx.store);
22837
22864
  const sprintData = getSprintSummaryData(ctx.store);
22838
22865
  const diagrams = getDiagramData(ctx.store);
22839
22866
  const features = ctx.store.list({ type: "feature" });
22840
- const featuresDone = features.filter((d) => ["done", "closed", "resolved"].includes(d.frontmatter.status)).length;
22841
- const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
22842
- const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
22843
- const RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
22867
+ const epics = ctx.store.list({ type: "epic" });
22868
+ const allTasks = ctx.store.list({ type: "task" });
22844
22869
  const decisions = ctx.store.list({ type: "decision" });
22845
- const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status)).length;
22846
22870
  const questions = ctx.store.list({ type: "question" });
22871
+ const sprints = ctx.store.list({ type: "sprint" });
22872
+ const featureToEpics = /* @__PURE__ */ new Map();
22873
+ for (const epic of epics) {
22874
+ const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
22875
+ for (const fid of featureIds) {
22876
+ const arr = featureToEpics.get(fid) ?? [];
22877
+ arr.push(epic);
22878
+ featureToEpics.set(fid, arr);
22879
+ }
22880
+ }
22881
+ const epicToTasks = /* @__PURE__ */ new Map();
22882
+ for (const task of allTasks) {
22883
+ const tags = task.frontmatter.tags ?? [];
22884
+ for (const tag of tags) {
22885
+ if (tag.startsWith("epic:")) {
22886
+ const epicId = tag.slice(5);
22887
+ const arr = epicToTasks.get(epicId) ?? [];
22888
+ arr.push(task);
22889
+ epicToTasks.set(epicId, arr);
22890
+ }
22891
+ }
22892
+ }
22893
+ const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
22894
+ let sprintTimelinePct = 0;
22895
+ if (activeSprint) {
22896
+ const startDate = activeSprint.frontmatter.startDate;
22897
+ const endDate = activeSprint.frontmatter.endDate;
22898
+ if (startDate && endDate) {
22899
+ const startMs = new Date(startDate).getTime();
22900
+ const endMs = new Date(endDate).getTime();
22901
+ const totalDays = Math.max(1, endMs - startMs);
22902
+ sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
22903
+ }
22904
+ }
22905
+ const featuresDone = features.filter((d) => DONE_STATUSES5.has(d.frontmatter.status)).length;
22906
+ const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
22907
+ const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
22908
+ const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
22847
22909
  const questionsOpen = questions.filter((d) => d.frontmatter.status === "open").length;
22848
22910
  const statsCards = `
22849
22911
  <div class="cards">
@@ -22870,7 +22932,7 @@ function poDashboardPage(ctx) {
22870
22932
  </div>
22871
22933
  <div class="card">
22872
22934
  <a href="/po/delivery">
22873
- <div class="card-label">Sprint</div>
22935
+ <div class="card-label">Current Sprint</div>
22874
22936
  <div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
22875
22937
  <div class="card-sub">${sprintData ? `${sprintData.workItems.done}/${sprintData.workItems.total} items` : "No active sprint"}</div>
22876
22938
  </a>
@@ -22906,43 +22968,72 @@ function poDashboardPage(ctx) {
22906
22968
  </div>`,
22907
22969
  { titleTag: "h3" }
22908
22970
  ) : "";
22909
- function signalTagClass(points) {
22910
- if (points >= 15) return "signal-tag signal-tag-high";
22911
- if (points >= 8) return "signal-tag signal-tag-medium";
22912
- return "signal-tag signal-tag-positive";
22913
- }
22914
- const trendingSection = upcoming.trending.length > 0 ? collapsibleSection(
22915
- "po-trending",
22916
- "Trending Items",
22971
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22972
+ const atRiskItems = [];
22973
+ for (const f of features) {
22974
+ if (DONE_STATUSES5.has(f.frontmatter.status)) continue;
22975
+ const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
22976
+ const reasons = [];
22977
+ let blocked = 0;
22978
+ for (const epic of fEpics) {
22979
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
22980
+ if (t.frontmatter.status === "blocked") blocked++;
22981
+ }
22982
+ }
22983
+ if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
22984
+ for (const epic of fEpics) {
22985
+ const td = epic.frontmatter.targetDate;
22986
+ if (td && td < today && !DONE_STATUSES5.has(epic.frontmatter.status)) {
22987
+ reasons.push(`${epic.frontmatter.id} overdue`);
22988
+ }
22989
+ }
22990
+ let totalTasks = 0;
22991
+ let progressSum = 0;
22992
+ for (const epic of fEpics) {
22993
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
22994
+ totalTasks++;
22995
+ progressSum += getEffectiveProgress(t.frontmatter);
22996
+ }
22997
+ }
22998
+ const avgProgress = totalTasks > 0 ? Math.round(progressSum / totalTasks) : 0;
22999
+ if (avgProgress < 30 && sprintTimelinePct > 60 && totalTasks > 0) {
23000
+ reasons.push("Low progress vs sprint timeline");
23001
+ }
23002
+ if (reasons.length > 0) atRiskItems.push({ feature: f, reasons });
23003
+ }
23004
+ const atRiskSection = atRiskItems.length > 0 ? collapsibleSection(
23005
+ "po-at-risk",
23006
+ `At-Risk Delivery (${atRiskItems.length})`,
22917
23007
  `<div class="table-wrap">
22918
23008
  <table>
22919
23009
  <thead>
22920
- <tr><th>ID</th><th>Title</th><th>Score</th></tr>
23010
+ <tr><th>Feature</th><th>Risk Reasons</th></tr>
22921
23011
  </thead>
22922
23012
  <tbody>
22923
- ${upcoming.trending.slice(0, 8).map((t) => {
22924
- const tags = t.signals.map((s) => `<span class="${signalTagClass(s.points)}" title="${escapeHtml(s.factor)}: +${s.points}pts">${escapeHtml(s.factor)}</span>`).join("");
22925
- return `
23013
+ ${atRiskItems.map((r) => `
22926
23014
  <tr>
22927
- <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
22928
- <td>${escapeHtml(t.title)}<br>${tags}</td>
22929
- <td><span class="trending-score">${t.score}</span></td>
22930
- </tr>`;
22931
- }).join("")}
23015
+ <td><a href="/docs/feature/${escapeHtml(r.feature.frontmatter.id)}">${escapeHtml(r.feature.frontmatter.id)}</a> ${escapeHtml(r.feature.frontmatter.title)}</td>
23016
+ <td>${r.reasons.map((reason) => `<span class="signal-tag signal-tag-high">${escapeHtml(reason)}</span>`).join(" ")}</td>
23017
+ </tr>`).join("")}
22932
23018
  </tbody>
22933
23019
  </table>
22934
23020
  </div>`,
22935
23021
  { titleTag: "h3" }
22936
- ) : "";
23022
+ ) : collapsibleSection(
23023
+ "po-at-risk",
23024
+ "At-Risk Delivery",
23025
+ '<div class="empty"><p style="color: var(--green);">No at-risk items \u2014 all features on track.</p></div>',
23026
+ { titleTag: "h3", defaultCollapsed: true }
23027
+ );
22937
23028
  return `
22938
23029
  <div class="page-header">
22939
23030
  <h2>Product Owner Dashboard</h2>
22940
23031
  <div class="subtitle">Feature delivery, decisions, and stakeholder alignment</div>
22941
23032
  </div>
22942
23033
  ${statsCards}
23034
+ ${atRiskSection}
22943
23035
  ${diagramSection}
22944
23036
  ${recentTable}
22945
- ${trendingSection}
22946
23037
  `;
22947
23038
  }
22948
23039
 
@@ -23121,44 +23212,74 @@ function tableDateFilter(tableId, colIndex) {
23121
23212
  }
23122
23213
 
23123
23214
  // src/web/templates/pages/po/backlog.ts
23215
+ function priorityClass(p) {
23216
+ if (!p) return "";
23217
+ const lower = p.toLowerCase();
23218
+ if (lower === "critical" || lower === "high") return " priority-high";
23219
+ if (lower === "medium") return " priority-medium";
23220
+ if (lower === "low") return " priority-low";
23221
+ return "";
23222
+ }
23223
+ function miniProgressBar(pct) {
23224
+ return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
23225
+ }
23124
23226
  function poBacklogPage(ctx) {
23125
23227
  const features = ctx.store.list({ type: "feature" });
23126
23228
  const questions = ctx.store.list({ type: "question" });
23127
23229
  const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
23128
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
23129
- const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
23230
+ const priorityOrder2 = { critical: 0, high: 1, medium: 2, low: 3 };
23231
+ const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
23130
23232
  const sortedFeatures = [...features].sort((a, b) => {
23131
- const sa = statusOrder[a.frontmatter.status] ?? 3;
23132
- const sb = statusOrder[b.frontmatter.status] ?? 3;
23233
+ const sa = statusOrder2[a.frontmatter.status] ?? 3;
23234
+ const sb = statusOrder2[b.frontmatter.status] ?? 3;
23133
23235
  if (sa !== sb) return sa - sb;
23134
- const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
23135
- const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
23236
+ const pa = priorityOrder2[a.frontmatter.priority?.toLowerCase()] ?? 99;
23237
+ const pb = priorityOrder2[b.frontmatter.priority?.toLowerCase()] ?? 99;
23136
23238
  if (pa !== pb) return pa - pb;
23137
23239
  return a.frontmatter.id.localeCompare(b.frontmatter.id);
23138
23240
  });
23139
23241
  const epics = ctx.store.list({ type: "epic" });
23140
23242
  const featureToEpics = /* @__PURE__ */ new Map();
23141
23243
  for (const epic of epics) {
23142
- const linked = epic.frontmatter.linkedFeature;
23143
- const featureIds = Array.isArray(linked) ? linked : linked ? [linked] : [];
23244
+ const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
23144
23245
  for (const fid of featureIds) {
23145
- const existing = featureToEpics.get(String(fid)) ?? [];
23146
- existing.push(epic.frontmatter.id);
23147
- featureToEpics.set(String(fid), existing);
23246
+ const arr = featureToEpics.get(fid) ?? [];
23247
+ arr.push(epic);
23248
+ featureToEpics.set(fid, arr);
23148
23249
  }
23149
23250
  }
23150
- function priorityClass(p) {
23151
- if (!p) return "";
23152
- const lower = p.toLowerCase();
23153
- if (lower === "critical" || lower === "high") return " priority-high";
23154
- if (lower === "medium") return " priority-medium";
23155
- if (lower === "low") return " priority-low";
23156
- return "";
23251
+ const allTasks = ctx.store.list({ type: "task" });
23252
+ const epicToTasks = /* @__PURE__ */ new Map();
23253
+ for (const task of allTasks) {
23254
+ const tags = task.frontmatter.tags ?? [];
23255
+ for (const tag of tags) {
23256
+ if (tag.startsWith("epic:")) {
23257
+ const epicId = tag.slice(5);
23258
+ const arr = epicToTasks.get(epicId) ?? [];
23259
+ arr.push(task);
23260
+ epicToTasks.set(epicId, arr);
23261
+ }
23262
+ }
23263
+ }
23264
+ const DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
23265
+ function featureTaskStats(featureId) {
23266
+ const fEpics = featureToEpics.get(featureId) ?? [];
23267
+ let total = 0;
23268
+ let done = 0;
23269
+ let progressSum = 0;
23270
+ for (const epic of fEpics) {
23271
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
23272
+ total++;
23273
+ if (DONE_STATUSES14.has(t.frontmatter.status)) done++;
23274
+ progressSum += getEffectiveProgress(t.frontmatter);
23275
+ }
23276
+ }
23277
+ return { epicCount: fEpics.length, total, done, avgProgress: total > 0 ? Math.round(progressSum / total) : 0 };
23157
23278
  }
23158
23279
  const featureStatuses = [...new Set(features.map((d) => d.frontmatter.status))].sort();
23159
23280
  const featurePriorities = [...new Set(features.map((d) => d.frontmatter.priority ?? "").filter(Boolean))].sort();
23160
23281
  const featureEpicIds = [...new Set(
23161
- features.flatMap((d) => featureToEpics.get(d.frontmatter.id) ?? [])
23282
+ features.flatMap((d) => (featureToEpics.get(d.frontmatter.id) ?? []).map((e) => e.frontmatter.id))
23162
23283
  )].sort();
23163
23284
  const featuresFilters = `<div class="filters">
23164
23285
  ${tableFilter("features-table", 2, "Status", featureStatuses)}
@@ -23169,12 +23290,13 @@ function poBacklogPage(ctx) {
23169
23290
  <div class="table-wrap table-short">
23170
23291
  <table id="features-table">
23171
23292
  <thead>
23172
- <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>
23293
+ <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>
23173
23294
  </thead>
23174
23295
  <tbody>
23175
23296
  ${sortedFeatures.map((d) => {
23176
- const linkedEpics = featureToEpics.get(d.frontmatter.id) ?? [];
23177
- const epicLinks = linkedEpics.map((eid) => `<a href="/docs/epic/${escapeHtml(eid)}">${escapeHtml(eid)}</a>`).join(", ");
23297
+ const stats = featureTaskStats(d.frontmatter.id);
23298
+ const linkedEpicDocs = featureToEpics.get(d.frontmatter.id) ?? [];
23299
+ const epicLinks = linkedEpicDocs.map((e) => `<a href="/docs/epic/${escapeHtml(e.frontmatter.id)}">${escapeHtml(e.frontmatter.id)}</a>`).join(", ");
23178
23300
  return `
23179
23301
  <tr>
23180
23302
  <td><a href="/docs/feature/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
@@ -23182,6 +23304,8 @@ function poBacklogPage(ctx) {
23182
23304
  <td>${statusBadge(d.frontmatter.status)}</td>
23183
23305
  <td><span class="${priorityClass(d.frontmatter.priority)}">${escapeHtml(d.frontmatter.priority ?? "\u2014")}</span></td>
23184
23306
  <td>${epicLinks || '<span class="text-dim">\u2014</span>'}</td>
23307
+ <td>${stats.total > 0 ? `${stats.done}/${stats.total}` : '<span class="text-dim">\u2014</span>'}</td>
23308
+ <td>${stats.total > 0 ? miniProgressBar(stats.avgProgress) : '<span class="text-dim">\u2014</span>'}</td>
23185
23309
  </tr>`;
23186
23310
  }).join("")}
23187
23311
  </tbody>
@@ -23223,14 +23347,23 @@ function poBacklogPage(ctx) {
23223
23347
 
23224
23348
  // src/web/templates/pages/po/decisions.ts
23225
23349
  var RESOLVED_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
23350
+ var KNOWN_OWNERS = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
23351
+ function ownerBadge(owner) {
23352
+ if (!owner) return '<span class="text-dim">\u2014</span>';
23353
+ const cls = KNOWN_OWNERS.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
23354
+ return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
23355
+ }
23226
23356
  function poDecisionsPage(ctx) {
23227
23357
  const decisions = ctx.store.list({ type: "decision" });
23358
+ const questions = ctx.store.list({ type: "question" });
23359
+ const features = ctx.store.list({ type: "feature" });
23228
23360
  const openDecisions = decisions.filter((d) => !RESOLVED_STATUSES.has(d.frontmatter.status));
23229
23361
  const resolvedDecisions = decisions.filter((d) => RESOLVED_STATUSES.has(d.frontmatter.status));
23362
+ const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
23230
23363
  const statsCards = `
23231
23364
  <div class="cards">
23232
23365
  <div class="card">
23233
- <div class="card-label">Open</div>
23366
+ <div class="card-label">Open Decisions</div>
23234
23367
  <div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
23235
23368
  <div class="card-sub">awaiting resolution</div>
23236
23369
  </div>
@@ -23239,12 +23372,79 @@ function poDecisionsPage(ctx) {
23239
23372
  <div class="card-value">${resolvedDecisions.length}</div>
23240
23373
  <div class="card-sub">decisions made</div>
23241
23374
  </div>
23375
+ <div class="card">
23376
+ <div class="card-label">Open Questions</div>
23377
+ <div class="card-value${openQuestions.length > 0 ? " priority-medium" : ""}">${openQuestions.length}</div>
23378
+ <div class="card-sub">needing answers</div>
23379
+ </div>
23242
23380
  <div class="card">
23243
23381
  <div class="card-label">Total</div>
23244
23382
  <div class="card-value">${decisions.length}</div>
23245
23383
  <div class="card-sub">all decisions</div>
23246
23384
  </div>
23247
23385
  </div>`;
23386
+ function daysSince(isoDate) {
23387
+ if (!isoDate) return 0;
23388
+ return Math.max(0, Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5));
23389
+ }
23390
+ const featureGroups = /* @__PURE__ */ new Map();
23391
+ const unlinked = [];
23392
+ function addToGroup(doc, docType) {
23393
+ const tags = doc.frontmatter.tags ?? [];
23394
+ const featureTags = tags.filter((t) => t.startsWith("feature:")).map((t) => t.slice(8));
23395
+ const item = { doc, docType, ageDays: daysSince(doc.frontmatter.created) };
23396
+ if (featureTags.length === 0) {
23397
+ unlinked.push(item);
23398
+ } else {
23399
+ for (const fid of featureTags) {
23400
+ const arr = featureGroups.get(fid) ?? [];
23401
+ arr.push(item);
23402
+ featureGroups.set(fid, arr);
23403
+ }
23404
+ }
23405
+ }
23406
+ for (const d of openDecisions) addToGroup(d, "decision");
23407
+ for (const q of openQuestions) addToGroup(q, "question");
23408
+ const totalDeps = openDecisions.length + openQuestions.length;
23409
+ const featureLookup = new Map(features.map((f) => [f.frontmatter.id, f]));
23410
+ function renderDepRows(items) {
23411
+ return items.map((item) => `
23412
+ <tr>
23413
+ <td><a href="/docs/${item.docType}/${escapeHtml(item.doc.frontmatter.id)}">${escapeHtml(item.doc.frontmatter.id)}</a></td>
23414
+ <td>${escapeHtml(item.doc.frontmatter.title)}</td>
23415
+ <td>${escapeHtml(typeLabel(item.docType))}</td>
23416
+ <td>${ownerBadge(item.doc.frontmatter.owner)}</td>
23417
+ <td>${item.ageDays}d</td>
23418
+ </tr>`).join("");
23419
+ }
23420
+ let depRows = "";
23421
+ for (const [fid, items] of featureGroups) {
23422
+ const feat = featureLookup.get(fid);
23423
+ const label = feat ? `${escapeHtml(fid)}: ${escapeHtml(feat.frontmatter.title)}` : escapeHtml(fid);
23424
+ depRows += `
23425
+ <tr class="group-header-row"><td colspan="5"><strong>${label}</strong></td></tr>
23426
+ ${renderDepRows(items)}`;
23427
+ }
23428
+ if (unlinked.length > 0) {
23429
+ depRows += `
23430
+ <tr class="group-header-row"><td colspan="5"><strong>Unlinked</strong></td></tr>
23431
+ ${renderDepRows(unlinked)}`;
23432
+ }
23433
+ const depsSection = totalDeps > 0 ? collapsibleSection(
23434
+ "po-decisions-deps",
23435
+ `By Feature (${totalDeps})`,
23436
+ `<div class="table-wrap">
23437
+ <table>
23438
+ <thead>
23439
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Owner</th><th>Age</th></tr>
23440
+ </thead>
23441
+ <tbody>
23442
+ ${depRows}
23443
+ </tbody>
23444
+ </table>
23445
+ </div>`,
23446
+ { titleTag: "h3" }
23447
+ ) : "";
23248
23448
  function decisionTable(docs, tableId) {
23249
23449
  if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
23250
23450
  const statuses = [...new Set(docs.map((d) => d.frontmatter.status))].sort();
@@ -23266,7 +23466,7 @@ function poDecisionsPage(ctx) {
23266
23466
  <td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
23267
23467
  <td>${escapeHtml(d.frontmatter.title)}</td>
23268
23468
  <td>${statusBadge(d.frontmatter.status)}</td>
23269
- <td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
23469
+ <td>${ownerBadge(d.frontmatter.owner)}</td>
23270
23470
  <td>${formatDate(d.frontmatter.created)}</td>
23271
23471
  </tr>`).join("")}
23272
23472
  </tbody>
@@ -23288,16 +23488,185 @@ function poDecisionsPage(ctx) {
23288
23488
  return `
23289
23489
  <div class="page-header">
23290
23490
  <h2>Decision Log</h2>
23291
- <div class="subtitle">Track and manage product decisions</div>
23491
+ <div class="subtitle">Track and manage product decisions and dependencies</div>
23292
23492
  </div>
23293
23493
  ${statsCards}
23494
+ ${depsSection}
23294
23495
  ${openSection}
23295
23496
  ${resolvedSection}
23296
23497
  ${renderTableUtilsScript()}
23297
23498
  `;
23298
23499
  }
23299
23500
 
23501
+ // src/web/templates/components/work-items-table.ts
23502
+ var FOCUS_BORDER_PALETTE = [
23503
+ "hsl(220, 60%, 55%)",
23504
+ "hsl(160, 50%, 45%)",
23505
+ "hsl(280, 45%, 55%)",
23506
+ "hsl(30, 65%, 55%)",
23507
+ "hsl(340, 50%, 55%)",
23508
+ "hsl(190, 50%, 45%)",
23509
+ "hsl(60, 50%, 50%)",
23510
+ "hsl(120, 40%, 45%)"
23511
+ ];
23512
+ function hashString(s) {
23513
+ let h = 0;
23514
+ for (let i = 0; i < s.length; i++) {
23515
+ h = (h << 5) - h + s.charCodeAt(i) | 0;
23516
+ }
23517
+ return Math.abs(h);
23518
+ }
23519
+ function countFocusStats(items) {
23520
+ let total = 0;
23521
+ let done = 0;
23522
+ let inProgress = 0;
23523
+ function walk(list) {
23524
+ for (const w of list) {
23525
+ if (w.type !== "contribution") {
23526
+ total++;
23527
+ const s = w.status.toLowerCase();
23528
+ if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
23529
+ else if (s === "in-progress" || s === "in progress") inProgress++;
23530
+ }
23531
+ if (w.children) walk(w.children);
23532
+ }
23533
+ }
23534
+ walk(items);
23535
+ return { total, done, inProgress };
23536
+ }
23537
+ var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
23538
+ function ownerBadge2(owner) {
23539
+ if (!owner) return '<span class="text-dim">\u2014</span>';
23540
+ const cls = KNOWN_OWNERS2.has(owner) ? `owner-badge-${owner}` : "owner-badge-other";
23541
+ return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
23542
+ }
23543
+ function renderItemRows(items, borderColor, showOwner, depth = 0) {
23544
+ return items.flatMap((w) => {
23545
+ const isChild = depth > 0;
23546
+ const isContribution = w.type === "contribution";
23547
+ const classes = ["focus-row"];
23548
+ if (isContribution) classes.push("contribution-row");
23549
+ else if (isChild) classes.push("child-row");
23550
+ const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
23551
+ 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>` : "";
23552
+ const ownerCell = showOwner ? `<td>${ownerBadge2(w.owner)}</td>` : "";
23553
+ const row = `
23554
+ <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
23555
+ <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
23556
+ <td>${escapeHtml(w.title)}</td>
23557
+ ${ownerCell}
23558
+ <td>${statusBadge(w.status)}</td>
23559
+ <td>${progressCell}</td>
23560
+ </tr>`;
23561
+ const childRows = w.children ? renderItemRows(w.children, borderColor, showOwner, depth + 1) : [];
23562
+ return [row, ...childRows];
23563
+ });
23564
+ }
23565
+ function renderWorkItemsTable(items, options) {
23566
+ const sectionId = options?.sectionId ?? "work-items";
23567
+ const title = options?.title ?? "Work Items";
23568
+ const defaultCollapsed = options?.defaultCollapsed ?? false;
23569
+ const showOwner = options?.showOwner ?? false;
23570
+ const focusGroups = /* @__PURE__ */ new Map();
23571
+ for (const item of items) {
23572
+ const focus = item.workFocus ?? "Unassigned";
23573
+ if (!focusGroups.has(focus)) focusGroups.set(focus, []);
23574
+ focusGroups.get(focus).push(item);
23575
+ }
23576
+ const focusColorMap = /* @__PURE__ */ new Map();
23577
+ for (const name of focusGroups.keys()) {
23578
+ focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
23579
+ }
23580
+ const allWorkItemRows = [];
23581
+ for (const [focus, groupItems] of focusGroups) {
23582
+ const color = focusColorMap.get(focus);
23583
+ const stats = countFocusStats(groupItems);
23584
+ const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
23585
+ const summaryParts = [];
23586
+ if (stats.done > 0) summaryParts.push(`${stats.done} done`);
23587
+ if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
23588
+ const remaining = stats.total - stats.done - stats.inProgress;
23589
+ if (remaining > 0) summaryParts.push(`${remaining} open`);
23590
+ const leftColspan = showOwner ? 3 : 2;
23591
+ allWorkItemRows.push(`
23592
+ <tr class="focus-group-header" style="--focus-color: ${color}">
23593
+ <td colspan="${leftColspan}">
23594
+ <span class="focus-group-name">${escapeHtml(focus)}</span>
23595
+ <span class="focus-group-stats">${summaryParts.join(" / ")}</span>
23596
+ </td>
23597
+ <td colspan="2">
23598
+ <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>
23599
+ </td>
23600
+ </tr>`);
23601
+ allWorkItemRows.push(...renderItemRows(groupItems, color, showOwner));
23602
+ }
23603
+ if (allWorkItemRows.length === 0) return "";
23604
+ const ownerHeader = showOwner ? "<th>Owner</th>" : "";
23605
+ return collapsibleSection(
23606
+ sectionId,
23607
+ title,
23608
+ `<div class="table-wrap">
23609
+ <table id="${sectionId}-table">
23610
+ <thead>
23611
+ <tr>
23612
+ <th>ID</th>
23613
+ <th>Title</th>
23614
+ ${ownerHeader}
23615
+ <th>Status</th>
23616
+ <th>Progress</th>
23617
+ </tr>
23618
+ </thead>
23619
+ <tbody>
23620
+ ${allWorkItemRows.join("")}
23621
+ </tbody>
23622
+ </table>
23623
+ </div>`,
23624
+ { titleTag: "h3", defaultCollapsed }
23625
+ );
23626
+ }
23627
+ var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
23628
+ function computeOwnerCompletionPct(items, owner) {
23629
+ let total = 0;
23630
+ let progressSum = 0;
23631
+ function walk(list) {
23632
+ for (const w of list) {
23633
+ if (w.type !== "contribution" && w.owner === owner) {
23634
+ total++;
23635
+ progressSum += w.progress ?? (DONE_STATUSES6.has(w.status) ? 100 : 0);
23636
+ }
23637
+ if (w.children) walk(w.children);
23638
+ }
23639
+ }
23640
+ walk(items);
23641
+ return total > 0 ? Math.round(progressSum / total) : 0;
23642
+ }
23643
+ function filterItemsByOwner(items, owner) {
23644
+ const result = [];
23645
+ for (const item of items) {
23646
+ if (item.owner === owner) {
23647
+ result.push(item);
23648
+ } else if (item.children) {
23649
+ result.push(...filterItemsByOwner(item.children, owner));
23650
+ }
23651
+ }
23652
+ return result;
23653
+ }
23654
+
23300
23655
  // src/web/templates/pages/po/delivery.ts
23656
+ var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
23657
+ var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
23658
+ var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
23659
+ function priorityClass2(p) {
23660
+ if (!p) return "";
23661
+ const lower = p.toLowerCase();
23662
+ if (lower === "critical" || lower === "high") return " priority-high";
23663
+ if (lower === "medium") return " priority-medium";
23664
+ if (lower === "low") return " priority-low";
23665
+ return "";
23666
+ }
23667
+ function miniProgressBar2(pct) {
23668
+ return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
23669
+ }
23301
23670
  var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
23302
23671
  "stakeholder-feedback",
23303
23672
  "acceptance-result",
@@ -23323,24 +23692,8 @@ function poDeliveryPage(ctx) {
23323
23692
  <p>No active sprint found. Create a sprint and set its status to "active" to track delivery.</p>
23324
23693
  </div>`;
23325
23694
  }
23326
- const doneFeatures = data.workItems.items.filter(
23327
- (w) => w.type === "feature" && ["done", "closed", "resolved"].includes(w.status)
23328
- );
23329
- function findContributions(items, parentId) {
23330
- const result = [];
23331
- for (const item of items) {
23332
- if (item.type === "contribution" && PO_CONTRIBUTION_TYPES.has(item.id.split("-").slice(0, -1).join("-") || "")) {
23333
- result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
23334
- }
23335
- if (PO_CONTRIBUTION_TYPES.has(item.type)) {
23336
- result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
23337
- }
23338
- if (item.children) {
23339
- result.push(...findContributions(item.children, item.id));
23340
- }
23341
- }
23342
- return result;
23343
- }
23695
+ const poItems = filterItemsByOwner(data.workItems.items, "po");
23696
+ const poCompletionPct = computeOwnerCompletionPct(data.workItems.items, "po");
23344
23697
  const allDocs = ctx.store.list();
23345
23698
  const poContributions = allDocs.filter((d) => PO_CONTRIBUTION_TYPES.has(d.frontmatter.type));
23346
23699
  const statsCards = `
@@ -23350,16 +23703,16 @@ function poDeliveryPage(ctx) {
23350
23703
  <div class="card-value">${data.workItems.completionPct}%</div>
23351
23704
  <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
23352
23705
  </div>
23706
+ <div class="card">
23707
+ <div class="card-label">PO Completion</div>
23708
+ <div class="card-value">${poCompletionPct}%</div>
23709
+ <div class="card-sub">${poItems.length} owned items</div>
23710
+ </div>
23353
23711
  <div class="card">
23354
23712
  <div class="card-label">Days Remaining</div>
23355
23713
  <div class="card-value">${data.timeline.daysRemaining}</div>
23356
23714
  <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
23357
23715
  </div>
23358
- <div class="card">
23359
- <div class="card-label">Features Done</div>
23360
- <div class="card-value">${doneFeatures.length}</div>
23361
- <div class="card-sub">this sprint</div>
23362
- </div>
23363
23716
  <div class="card">
23364
23717
  <div class="card-label">PO Contributions</div>
23365
23718
  <div class="card-value">${poContributions.length}</div>
@@ -23371,7 +23724,11 @@ function poDeliveryPage(ctx) {
23371
23724
  <strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
23372
23725
  ${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
23373
23726
  </div>`;
23374
- const featuresSection = data.linkedEpics.length > 0 ? collapsibleSection(
23727
+ const workItemsSection = renderWorkItemsTable(poItems, {
23728
+ sectionId: "po-delivery-items",
23729
+ title: "PO Work Items"
23730
+ });
23731
+ const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
23375
23732
  "po-delivery-epics",
23376
23733
  "Linked Epics",
23377
23734
  `<div class="table-wrap">
@@ -23392,6 +23749,82 @@ function poDeliveryPage(ctx) {
23392
23749
  </div>`,
23393
23750
  { titleTag: "h3" }
23394
23751
  ) : "";
23752
+ const features = ctx.store.list({ type: "feature" });
23753
+ const epics = ctx.store.list({ type: "epic" });
23754
+ const allTasks = ctx.store.list({ type: "task" });
23755
+ const sprints = ctx.store.list({ type: "sprint" });
23756
+ const featureToEpics = /* @__PURE__ */ new Map();
23757
+ for (const epic of epics) {
23758
+ const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
23759
+ for (const fid of featureIds) {
23760
+ const arr = featureToEpics.get(fid) ?? [];
23761
+ arr.push(epic);
23762
+ featureToEpics.set(fid, arr);
23763
+ }
23764
+ }
23765
+ const epicToTasks = /* @__PURE__ */ new Map();
23766
+ for (const task of allTasks) {
23767
+ const tags = task.frontmatter.tags ?? [];
23768
+ for (const tag of tags) {
23769
+ if (tag.startsWith("epic:")) {
23770
+ const arr = epicToTasks.get(tag.slice(5)) ?? [];
23771
+ arr.push(task);
23772
+ epicToTasks.set(tag.slice(5), arr);
23773
+ }
23774
+ }
23775
+ }
23776
+ const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
23777
+ const activeSprintEpicIds = new Set(
23778
+ activeSprint ? normalizeLinkedEpics(activeSprint.frontmatter.linkedEpics) : []
23779
+ );
23780
+ function featureSprintLabel(featureId) {
23781
+ if (!activeSprint) return "\u2014";
23782
+ const fEpics = featureToEpics.get(featureId) ?? [];
23783
+ return fEpics.some((e) => activeSprintEpicIds.has(e.frontmatter.id)) ? escapeHtml(activeSprint.frontmatter.id) : "\u2014";
23784
+ }
23785
+ function featureProgress(featureId) {
23786
+ const fEpics = featureToEpics.get(featureId) ?? [];
23787
+ let total = 0;
23788
+ let progressSum = 0;
23789
+ for (const epic of fEpics) {
23790
+ for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
23791
+ total++;
23792
+ progressSum += getEffectiveProgress(t.frontmatter);
23793
+ }
23794
+ }
23795
+ return total > 0 ? Math.round(progressSum / total) : 0;
23796
+ }
23797
+ const nonDoneFeatures = features.filter((f) => !DONE_STATUSES7.has(f.frontmatter.status)).sort((a, b) => {
23798
+ const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
23799
+ const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
23800
+ if (pa !== pb) return pa - pb;
23801
+ const sa = statusOrder[a.frontmatter.status] ?? 3;
23802
+ const sb = statusOrder[b.frontmatter.status] ?? 3;
23803
+ return sa - sb;
23804
+ });
23805
+ const priorityQueueSection = collapsibleSection(
23806
+ "po-priority-queue",
23807
+ `Priority Queue (${nonDoneFeatures.length})`,
23808
+ nonDoneFeatures.length > 0 ? `<div class="table-wrap">
23809
+ <table>
23810
+ <thead>
23811
+ <tr><th>Priority</th><th>ID</th><th>Title</th><th>Status</th><th>Sprint</th><th>Progress</th></tr>
23812
+ </thead>
23813
+ <tbody>
23814
+ ${nonDoneFeatures.map((f) => `
23815
+ <tr>
23816
+ <td><span class="${priorityClass2(f.frontmatter.priority)}">${escapeHtml(f.frontmatter.priority ?? "\u2014")}</span></td>
23817
+ <td><a href="/docs/feature/${escapeHtml(f.frontmatter.id)}">${escapeHtml(f.frontmatter.id)}</a></td>
23818
+ <td>${escapeHtml(f.frontmatter.title)}</td>
23819
+ <td>${statusBadge(f.frontmatter.status)}</td>
23820
+ <td>${featureSprintLabel(f.frontmatter.id)}</td>
23821
+ <td>${miniProgressBar2(featureProgress(f.frontmatter.id))}</td>
23822
+ </tr>`).join("")}
23823
+ </tbody>
23824
+ </table>
23825
+ </div>` : '<div class="empty"><p>No active features in the queue.</p></div>',
23826
+ { titleTag: "h3" }
23827
+ );
23395
23828
  const contributionsSection = poContributions.length > 0 ? collapsibleSection(
23396
23829
  "po-delivery-contributions",
23397
23830
  `PO Contributions (${poContributions.length})`,
@@ -23422,7 +23855,9 @@ function poDeliveryPage(ctx) {
23422
23855
  ${sprintHeader}
23423
23856
  ${progressBar(data.workItems.completionPct)}
23424
23857
  ${statsCards}
23425
- ${featuresSection}
23858
+ ${workItemsSection}
23859
+ ${epicsSection}
23860
+ ${priorityQueueSection}
23426
23861
  ${contributionsSection}
23427
23862
  `;
23428
23863
  }
@@ -23448,6 +23883,12 @@ function renderGarWidget(report) {
23448
23883
  }
23449
23884
 
23450
23885
  // src/web/templates/pages/po/stakeholders.ts
23886
+ var KNOWN_OWNERS3 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
23887
+ function ownerBadge3(owner) {
23888
+ if (!owner) return '<span class="text-dim">\u2014</span>';
23889
+ const cls = KNOWN_OWNERS3.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
23890
+ return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
23891
+ }
23451
23892
  function poStakeholdersPage(ctx) {
23452
23893
  const garReport = getGarData(ctx.store, ctx.projectName);
23453
23894
  const actions = ctx.store.list({ type: "action" });
@@ -23498,7 +23939,7 @@ function poStakeholdersPage(ctx) {
23498
23939
  <td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
23499
23940
  <td>${escapeHtml(d.frontmatter.title)}</td>
23500
23941
  <td>${statusBadge(d.frontmatter.status)}</td>
23501
- <td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
23942
+ <td>${ownerBadge3(d.frontmatter.owner)}</td>
23502
23943
  <td>${d.frontmatter.dueDate ? formatDate(d.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
23503
23944
  </tr>`).join("")}
23504
23945
  </tbody>
@@ -23521,7 +23962,7 @@ function poStakeholdersPage(ctx) {
23521
23962
  <tr>
23522
23963
  <td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
23523
23964
  <td>${escapeHtml(d.frontmatter.title)}</td>
23524
- <td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
23965
+ <td>${ownerBadge3(d.frontmatter.owner)}</td>
23525
23966
  <td>${formatDate(d.frontmatter.created)}</td>
23526
23967
  </tr>`).join("")}
23527
23968
  </tbody>
@@ -23562,7 +24003,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
23562
24003
  registerPersonaPage("po", "stakeholders", poStakeholdersPage);
23563
24004
 
23564
24005
  // src/web/templates/pages/dm/dashboard.ts
23565
- var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24006
+ var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
23566
24007
  function progressBar2(pct) {
23567
24008
  return `<div class="sprint-progress-bar">
23568
24009
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
@@ -23573,7 +24014,7 @@ function dmDashboardPage(ctx) {
23573
24014
  const sprintData = getSprintSummaryData(ctx.store);
23574
24015
  const upcoming = getUpcomingData(ctx.store);
23575
24016
  const actions = ctx.store.list({ type: "action" });
23576
- const openActions = actions.filter((d) => !DONE_STATUSES5.has(d.frontmatter.status));
24017
+ const openActions = actions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
23577
24018
  const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
23578
24019
  const statsCards = `
23579
24020
  <div class="cards">
@@ -23660,42 +24101,46 @@ function dmDashboardPage(ctx) {
23660
24101
  `;
23661
24102
  }
23662
24103
 
23663
- // src/web/templates/pages/sprint-summary.ts
24104
+ // src/web/templates/pages/dm/sprint.ts
23664
24105
  function progressBar3(pct) {
23665
24106
  return `<div class="sprint-progress-bar">
23666
24107
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
23667
24108
  <span class="sprint-progress-label">${pct}%</span>
23668
24109
  </div>`;
23669
24110
  }
23670
- function sprintSummaryPage(data, cached2) {
24111
+ function dmSprintPage(ctx) {
24112
+ const data = getSprintSummaryData(ctx.store);
23671
24113
  if (!data) {
23672
24114
  return `
23673
24115
  <div class="page-header">
23674
- <h2>Sprint Summary</h2>
23675
- <div class="subtitle">AI-powered sprint narrative</div>
24116
+ <h2>Sprint Execution</h2>
24117
+ <div class="subtitle">Full sprint oversight and delivery tracking</div>
23676
24118
  </div>
23677
24119
  <div class="empty">
23678
24120
  <h3>No Active Sprint</h3>
23679
- <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
24121
+ <p>No active sprint found. Create a sprint and set its status to "active" to track execution.</p>
23680
24122
  </div>`;
23681
24123
  }
24124
+ const dmCompletionPct = computeOwnerCompletionPct(data.workItems.items, "dm");
24125
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
24126
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
23682
24127
  const statsCards = `
23683
24128
  <div class="cards">
23684
24129
  <div class="card">
23685
- <div class="card-label">Completion</div>
24130
+ <div class="card-label">Sprint Completion</div>
23686
24131
  <div class="card-value">${data.workItems.completionPct}%</div>
23687
24132
  <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
23688
24133
  </div>
24134
+ <div class="card">
24135
+ <div class="card-label">DM Completion</div>
24136
+ <div class="card-value">${dmCompletionPct}%</div>
24137
+ <div class="card-sub">DM-owned items</div>
24138
+ </div>
23689
24139
  <div class="card">
23690
24140
  <div class="card-label">Days Remaining</div>
23691
24141
  <div class="card-value">${data.timeline.daysRemaining}</div>
23692
24142
  <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
23693
24143
  </div>
23694
- <div class="card">
23695
- <div class="card-label">Epics</div>
23696
- <div class="card-value">${data.linkedEpics.length}</div>
23697
- <div class="card-sub">linked to sprint</div>
23698
- </div>
23699
24144
  <a class="card card-link" href="sprint-blockers">
23700
24145
  <div class="card-label">Blockers</div>
23701
24146
  <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
@@ -23707,13 +24152,18 @@ function sprintSummaryPage(data, cached2) {
23707
24152
  <div class="card-sub">open risk items</div>
23708
24153
  </a>
23709
24154
  </div>`;
23710
- const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
23711
- "ss-epics",
24155
+ const workItemsSection = renderWorkItemsTable(data.workItems.items, {
24156
+ sectionId: "dm-sprint-items",
24157
+ title: "Sprint Work Items",
24158
+ showOwner: true
24159
+ });
24160
+ const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
24161
+ "dm-sprint-epics",
23712
24162
  "Linked Epics",
23713
24163
  `<div class="table-wrap">
23714
24164
  <table>
23715
24165
  <thead>
23716
- <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
24166
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
23717
24167
  </thead>
23718
24168
  <tbody>
23719
24169
  ${data.linkedEpics.map((e) => `
@@ -23728,138 +24178,29 @@ function sprintSummaryPage(data, cached2) {
23728
24178
  </div>`,
23729
24179
  { titleTag: "h3" }
23730
24180
  ) : "";
23731
- const FOCUS_BORDER_PALETTE = [
23732
- "hsl(220, 60%, 55%)",
23733
- "hsl(160, 50%, 45%)",
23734
- "hsl(280, 45%, 55%)",
23735
- "hsl(30, 65%, 55%)",
23736
- "hsl(340, 50%, 55%)",
23737
- "hsl(190, 50%, 45%)",
23738
- "hsl(60, 50%, 50%)",
23739
- "hsl(120, 40%, 45%)"
23740
- ];
23741
- function hashString(s) {
23742
- let h = 0;
23743
- for (let i = 0; i < s.length; i++) {
23744
- h = (h << 5) - h + s.charCodeAt(i) | 0;
23745
- }
23746
- return Math.abs(h);
23747
- }
23748
- const focusGroups = /* @__PURE__ */ new Map();
23749
- for (const item of data.workItems.items) {
23750
- const focus = item.workFocus ?? "Unassigned";
23751
- if (!focusGroups.has(focus)) focusGroups.set(focus, []);
23752
- focusGroups.get(focus).push(item);
23753
- }
23754
- const focusColorMap = /* @__PURE__ */ new Map();
23755
- for (const name of focusGroups.keys()) {
23756
- focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
23757
- }
23758
- function countFocusStats(items) {
23759
- let total = 0;
23760
- let done = 0;
23761
- let inProgress = 0;
23762
- function walk(list) {
23763
- for (const w of list) {
23764
- if (w.type !== "contribution") {
23765
- total++;
23766
- const s = w.status.toLowerCase();
23767
- if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
23768
- else if (s === "in-progress" || s === "in progress") inProgress++;
23769
- }
23770
- if (w.children) walk(w.children);
23771
- }
23772
- }
23773
- walk(items);
23774
- return { total, done, inProgress };
23775
- }
23776
- function renderItemRows(items, borderColor, depth = 0) {
23777
- return items.flatMap((w) => {
23778
- const isChild = depth > 0;
23779
- const isContribution = w.type === "contribution";
23780
- const classes = ["focus-row"];
23781
- if (isContribution) classes.push("contribution-row");
23782
- else if (isChild) classes.push("child-row");
23783
- const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
23784
- 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>` : "";
23785
- const row = `
23786
- <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
23787
- <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
23788
- <td>${escapeHtml(w.title)}</td>
23789
- <td>${statusBadge(w.status)}</td>
23790
- <td>${progressCell}</td>
23791
- </tr>`;
23792
- const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
23793
- return [row, ...childRows];
23794
- });
23795
- }
23796
- const allWorkItemRows = [];
23797
- for (const [focus, items] of focusGroups) {
23798
- const color = focusColorMap.get(focus);
23799
- const stats = countFocusStats(items);
23800
- const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
23801
- const summaryParts = [];
23802
- if (stats.done > 0) summaryParts.push(`${stats.done} done`);
23803
- if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
23804
- const remaining = stats.total - stats.done - stats.inProgress;
23805
- if (remaining > 0) summaryParts.push(`${remaining} open`);
23806
- allWorkItemRows.push(`
23807
- <tr class="focus-group-header" style="--focus-color: ${color}">
23808
- <td colspan="2">
23809
- <span class="focus-group-name">${escapeHtml(focus)}</span>
23810
- <span class="focus-group-stats">${summaryParts.join(" / ")}</span>
23811
- </td>
23812
- <td colspan="2">
23813
- <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>
23814
- </td>
23815
- </tr>`);
23816
- allWorkItemRows.push(...renderItemRows(items, color));
23817
- }
23818
- const tableHeaders = `<tr>
23819
- <th>ID</th>
23820
- <th>Title</th>
23821
- <th>Status</th>
23822
- <th>Progress</th>
23823
- </tr>`;
23824
- const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
23825
- "ss-work-items",
23826
- "Work Items",
23827
- `<div class="table-wrap">
23828
- <table id="work-items-table">
23829
- <thead>
23830
- ${tableHeaders}
23831
- </thead>
23832
- <tbody>
23833
- ${allWorkItemRows.join("")}
23834
- </tbody>
23835
- </table>
23836
- </div>`,
23837
- { titleTag: "h3", defaultCollapsed: true }
23838
- ) : "";
23839
- const activitySection = data.artifacts.length > 0 ? collapsibleSection(
23840
- "ss-activity",
23841
- "Recent Activity",
24181
+ const actionsSection = data.openActions.length > 0 ? collapsibleSection(
24182
+ "dm-sprint-actions",
24183
+ `Open Actions (${data.openActions.length})`,
23842
24184
  `<div class="table-wrap">
23843
24185
  <table>
23844
24186
  <thead>
23845
- <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
24187
+ <tr><th>ID</th><th>Title</th><th>Owner</th><th>Due Date</th></tr>
23846
24188
  </thead>
23847
24189
  <tbody>
23848
- ${data.artifacts.slice(0, 15).map((a) => `
24190
+ ${data.openActions.map((a) => `
23849
24191
  <tr>
23850
- <td>${formatDate(a.date)}</td>
23851
- <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
24192
+ <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
23852
24193
  <td>${escapeHtml(a.title)}</td>
23853
- <td>${escapeHtml(typeLabel(a.type))}</td>
23854
- <td>${escapeHtml(a.action)}</td>
24194
+ <td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
24195
+ <td>${a.dueDate ? formatDate(a.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
23855
24196
  </tr>`).join("")}
23856
24197
  </tbody>
23857
24198
  </table>
23858
24199
  </div>`,
23859
- { titleTag: "h3", defaultCollapsed: true }
24200
+ { titleTag: "h3" }
23860
24201
  ) : "";
23861
24202
  const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
23862
- "ss-meetings",
24203
+ "dm-sprint-meetings",
23863
24204
  `Meetings (${data.meetings.length})`,
23864
24205
  `<div class="table-wrap">
23865
24206
  <table>
@@ -23878,79 +24219,23 @@ function sprintSummaryPage(data, cached2) {
23878
24219
  </div>`,
23879
24220
  { titleTag: "h3", defaultCollapsed: true }
23880
24221
  ) : "";
23881
- const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
23882
- const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
23883
24222
  return `
23884
24223
  <div class="page-header">
23885
24224
  <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
23886
- <div class="subtitle">Sprint Summary ${dateRange}</div>
24225
+ <div class="subtitle">Sprint Execution ${dateRange}</div>
23887
24226
  </div>
23888
24227
  ${goalHtml}
23889
24228
  ${progressBar3(data.timeline.percentComplete)}
23890
24229
  ${statsCards}
23891
- ${epicsTable}
23892
24230
  ${workItemsSection}
23893
- ${activitySection}
24231
+ ${epicsSection}
24232
+ ${actionsSection}
23894
24233
  ${meetingsSection}
23895
-
23896
- <div class="sprint-ai-section">
23897
- <h3>AI Summary</h3>
23898
- ${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>`}
23899
- <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
23900
- <div id="summary-loading" class="sprint-loading" style="display:none">
23901
- <div class="sprint-spinner"></div>
23902
- <span>Generating summary...</span>
23903
- </div>
23904
- <div id="summary-error" class="sprint-error" style="display:none"></div>
23905
- <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
23906
- </div>
23907
-
23908
- <script>
23909
- async function generateSummary() {
23910
- var btn = document.getElementById('generate-btn');
23911
- var loading = document.getElementById('summary-loading');
23912
- var errorEl = document.getElementById('summary-error');
23913
- var content = document.getElementById('summary-content');
23914
-
23915
- btn.disabled = true;
23916
- btn.style.display = 'none';
23917
- loading.style.display = 'flex';
23918
- errorEl.style.display = 'none';
23919
- content.style.display = 'none';
23920
-
23921
- try {
23922
- var res = await fetch('/api/sprint-summary', {
23923
- method: 'POST',
23924
- headers: { 'Content-Type': 'application/json' },
23925
- body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
23926
- });
23927
- var json = await res.json();
23928
- if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
23929
- loading.style.display = 'none';
23930
- content.innerHTML = json.html;
23931
- content.style.display = 'block';
23932
- btn.textContent = 'Regenerate';
23933
- btn.style.display = '';
23934
- btn.disabled = false;
23935
- } catch (e) {
23936
- loading.style.display = 'none';
23937
- errorEl.textContent = e.message;
23938
- errorEl.style.display = 'block';
23939
- btn.style.display = '';
23940
- btn.disabled = false;
23941
- }
23942
- }
23943
- </script>`;
23944
- }
23945
-
23946
- // src/web/templates/pages/dm/sprint.ts
23947
- function dmSprintPage(ctx) {
23948
- const data = getSprintSummaryData(ctx.store);
23949
- return sprintSummaryPage(data);
24234
+ `;
23950
24235
  }
23951
24236
 
23952
24237
  // src/web/templates/pages/dm/actions.ts
23953
- var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24238
+ var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
23954
24239
  function urgencyBadge(tier) {
23955
24240
  const labels = {
23956
24241
  overdue: "Overdue",
@@ -23970,7 +24255,7 @@ function urgencyRowClass(tier) {
23970
24255
  function dmActionsPage(ctx) {
23971
24256
  const upcoming = getUpcomingData(ctx.store);
23972
24257
  const allActions = ctx.store.list({ type: "action" });
23973
- const openActions = allActions.filter((d) => !DONE_STATUSES6.has(d.frontmatter.status));
24258
+ const openActions = allActions.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
23974
24259
  const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
23975
24260
  const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
23976
24261
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
@@ -24055,7 +24340,7 @@ function dmActionsPage(ctx) {
24055
24340
  }
24056
24341
 
24057
24342
  // src/web/templates/pages/dm/risks.ts
24058
- var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24343
+ var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24059
24344
  function dmRisksPage(ctx) {
24060
24345
  const allDocs = ctx.store.list();
24061
24346
  const upcoming = getUpcomingData(ctx.store);
@@ -24066,7 +24351,7 @@ function dmRisksPage(ctx) {
24066
24351
  const todayMs = new Date(today).getTime();
24067
24352
  const fourteenDaysMs = 14 * 864e5;
24068
24353
  const agingItems = allDocs.filter((d) => {
24069
- if (DONE_STATUSES7.has(d.frontmatter.status)) return false;
24354
+ if (DONE_STATUSES10.has(d.frontmatter.status)) return false;
24070
24355
  if (!["action", "question"].includes(d.frontmatter.type)) return false;
24071
24356
  const createdMs = new Date(d.frontmatter.created).getTime();
24072
24357
  return todayMs - createdMs > fourteenDaysMs;
@@ -24180,7 +24465,7 @@ function dmRisksPage(ctx) {
24180
24465
  }
24181
24466
 
24182
24467
  // src/web/templates/pages/dm/meetings.ts
24183
- var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24468
+ var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24184
24469
  function dmMeetingsPage(ctx) {
24185
24470
  const meetings = ctx.store.list({ type: "meeting" });
24186
24471
  const actions = ctx.store.list({ type: "action" });
@@ -24226,7 +24511,7 @@ function dmMeetingsPage(ctx) {
24226
24511
  ${sortedMeetings.map((m) => {
24227
24512
  const date5 = m.frontmatter.date ?? m.frontmatter.created;
24228
24513
  const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
24229
- const openCount = relatedActions.filter((a) => !DONE_STATUSES8.has(a.frontmatter.status)).length;
24514
+ const openCount = relatedActions.filter((a) => !DONE_STATUSES11.has(a.frontmatter.status)).length;
24230
24515
  return `
24231
24516
  <tr>
24232
24517
  <td>${formatDate(date5)}</td>
@@ -24241,7 +24526,7 @@ function dmMeetingsPage(ctx) {
24241
24526
  const recentMeetingActions = [];
24242
24527
  for (const [mid, acts] of meetingActionMap) {
24243
24528
  for (const act of acts) {
24244
- if (!DONE_STATUSES8.has(act.frontmatter.status)) {
24529
+ if (!DONE_STATUSES11.has(act.frontmatter.status)) {
24245
24530
  recentMeetingActions.push({ action: act, meetingId: mid });
24246
24531
  }
24247
24532
  }
@@ -24436,16 +24721,16 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
24436
24721
  registerPersonaPage("dm", "governance", dmGovernancePage);
24437
24722
 
24438
24723
  // src/web/templates/pages/tl/dashboard.ts
24439
- var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24440
- var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
24724
+ var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24725
+ var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
24441
24726
  function tlDashboardPage(ctx) {
24442
24727
  const epics = ctx.store.list({ type: "epic" });
24443
24728
  const tasks = ctx.store.list({ type: "task" });
24444
24729
  const decisions = ctx.store.list({ type: "decision" });
24445
24730
  const questions = ctx.store.list({ type: "question" });
24446
24731
  const diagrams = getDiagramData(ctx.store);
24447
- const openEpics = epics.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
24448
- const openTasks = tasks.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
24732
+ const openEpics = epics.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
24733
+ const openTasks = tasks.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
24449
24734
  const technicalDecisions = decisions.filter((d) => {
24450
24735
  const tags = d.frontmatter.tags ?? [];
24451
24736
  return tags.some((t) => {
@@ -24454,7 +24739,7 @@ function tlDashboardPage(ctx) {
24454
24739
  });
24455
24740
  });
24456
24741
  const displayDecisions = technicalDecisions.length > 0 ? technicalDecisions : decisions;
24457
- const pendingDecisions = displayDecisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status));
24742
+ const pendingDecisions = displayDecisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status));
24458
24743
  const statsCards = `
24459
24744
  <div class="cards">
24460
24745
  <div class="card">
@@ -24503,7 +24788,7 @@ function tlDashboardPage(ctx) {
24503
24788
  }
24504
24789
 
24505
24790
  // src/web/templates/pages/tl/backlog.ts
24506
- var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24791
+ var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24507
24792
  function tlBacklogPage(ctx) {
24508
24793
  const epics = ctx.store.list({ type: "epic" });
24509
24794
  const tasks = ctx.store.list({ type: "task" });
@@ -24525,10 +24810,10 @@ function tlBacklogPage(ctx) {
24525
24810
  const featureIds = Array.isArray(linked) ? linked.map(String) : linked ? [String(linked)] : [];
24526
24811
  epicFeatureMap.set(epic.frontmatter.id, featureIds);
24527
24812
  }
24528
- const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
24813
+ const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
24529
24814
  const sortedEpics = [...epics].sort((a, b) => {
24530
- const sa = statusOrder[a.frontmatter.status] ?? 3;
24531
- const sb = statusOrder[b.frontmatter.status] ?? 3;
24815
+ const sa = statusOrder2[a.frontmatter.status] ?? 3;
24816
+ const sb = statusOrder2[b.frontmatter.status] ?? 3;
24532
24817
  if (sa !== sb) return sa - sb;
24533
24818
  return a.frontmatter.id.localeCompare(b.frontmatter.id);
24534
24819
  });
@@ -24540,7 +24825,7 @@ function tlBacklogPage(ctx) {
24540
24825
  <tbody>
24541
24826
  ${sortedEpics.map((e) => {
24542
24827
  const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
24543
- const done = eTasks.filter((t) => DONE_STATUSES10.has(t.frontmatter.status)).length;
24828
+ const done = eTasks.filter((t) => DONE_STATUSES13.has(t.frontmatter.status)).length;
24544
24829
  const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
24545
24830
  const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
24546
24831
  return `
@@ -24560,7 +24845,7 @@ function tlBacklogPage(ctx) {
24560
24845
  for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
24561
24846
  }
24562
24847
  const unassignedTasks = tasks.filter(
24563
- (t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES10.has(t.frontmatter.status)
24848
+ (t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES13.has(t.frontmatter.status)
24564
24849
  );
24565
24850
  const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
24566
24851
  "tl-backlog-unassigned",
@@ -24621,7 +24906,6 @@ var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
24621
24906
  "technical-assessment",
24622
24907
  "architecture-review"
24623
24908
  ]);
24624
- var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
24625
24909
  function progressBar4(pct) {
24626
24910
  return `<div class="sprint-progress-bar">
24627
24911
  <div class="sprint-progress-fill" style="width: ${pct}%"></div>
@@ -24641,25 +24925,8 @@ function tlSprintPage(ctx) {
24641
24925
  <p>No active sprint found. Create a sprint and set its status to "active" to track sprint work.</p>
24642
24926
  </div>`;
24643
24927
  }
24644
- const techTypes = /* @__PURE__ */ new Set(["epic", "task"]);
24645
- const techItems = [];
24646
- for (const item of data.workItems.items) {
24647
- if (techTypes.has(item.type)) {
24648
- techItems.push(item);
24649
- } else if (item.children) {
24650
- const promoteChildren = (children) => {
24651
- for (const child of children) {
24652
- if (techTypes.has(child.type)) {
24653
- techItems.push(child);
24654
- } else if (child.children) {
24655
- promoteChildren(child.children);
24656
- }
24657
- }
24658
- };
24659
- promoteChildren(item.children);
24660
- }
24661
- }
24662
- const techDone = techItems.filter((w) => DONE_STATUSES11.has(w.status)).length;
24928
+ const tlItems = filterItemsByOwner(data.workItems.items, "tl");
24929
+ const tlCompletionPct = computeOwnerCompletionPct(data.workItems.items, "tl");
24663
24930
  const allDocs = ctx.store.list();
24664
24931
  const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
24665
24932
  const statsCards = `
@@ -24670,9 +24937,9 @@ function tlSprintPage(ctx) {
24670
24937
  <div class="card-sub">${data.timeline.daysRemaining} days remaining</div>
24671
24938
  </div>
24672
24939
  <div class="card">
24673
- <div class="card-label">Tech Items</div>
24674
- <div class="card-value">${techItems.length}</div>
24675
- <div class="card-sub">${techDone} done</div>
24940
+ <div class="card-label">TL Completion</div>
24941
+ <div class="card-value">${tlCompletionPct}%</div>
24942
+ <div class="card-sub">${tlItems.length} owned items</div>
24676
24943
  </div>
24677
24944
  <div class="card">
24678
24945
  <div class="card-label">Epics</div>
@@ -24690,28 +24957,10 @@ function tlSprintPage(ctx) {
24690
24957
  <strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
24691
24958
  ${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
24692
24959
  </div>`;
24693
- const workItemsSection = techItems.length > 0 ? collapsibleSection(
24694
- "tl-sprint-items",
24695
- `Sprint Work Items (${techItems.length})`,
24696
- `<div class="table-wrap">
24697
- <table>
24698
- <thead>
24699
- <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
24700
- </thead>
24701
- <tbody>
24702
- ${techItems.map((w) => `
24703
- <tr>
24704
- <td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
24705
- <td>${escapeHtml(w.title)}</td>
24706
- <td>${escapeHtml(typeLabel(w.type))}</td>
24707
- <td>${statusBadge(w.status)}</td>
24708
- <td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
24709
- </tr>`).join("")}
24710
- </tbody>
24711
- </table>
24712
- </div>`,
24713
- { titleTag: "h3" }
24714
- ) : "";
24960
+ const workItemsSection = renderWorkItemsTable(tlItems, {
24961
+ sectionId: "tl-sprint-items",
24962
+ title: "TL Work Items"
24963
+ });
24715
24964
  const contributionsSection = tlContributions.length > 0 ? collapsibleSection(
24716
24965
  "tl-sprint-contributions",
24717
24966
  `TL Contributions (${tlContributions.length})`,
@@ -25095,6 +25344,186 @@ function upcomingPage(data) {
25095
25344
  `;
25096
25345
  }
25097
25346
 
25347
+ // src/web/templates/pages/sprint-summary.ts
25348
+ function progressBar5(pct) {
25349
+ return `<div class="sprint-progress-bar">
25350
+ <div class="sprint-progress-fill" style="width: ${pct}%"></div>
25351
+ <span class="sprint-progress-label">${pct}%</span>
25352
+ </div>`;
25353
+ }
25354
+ function sprintSummaryPage(data, cached2) {
25355
+ if (!data) {
25356
+ return `
25357
+ <div class="page-header">
25358
+ <h2>Sprint Summary</h2>
25359
+ <div class="subtitle">AI-powered sprint narrative</div>
25360
+ </div>
25361
+ <div class="empty">
25362
+ <h3>No Active Sprint</h3>
25363
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
25364
+ </div>`;
25365
+ }
25366
+ const statsCards = `
25367
+ <div class="cards">
25368
+ <div class="card">
25369
+ <div class="card-label">Completion</div>
25370
+ <div class="card-value">${data.workItems.completionPct}%</div>
25371
+ <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
25372
+ </div>
25373
+ <div class="card">
25374
+ <div class="card-label">Days Remaining</div>
25375
+ <div class="card-value">${data.timeline.daysRemaining}</div>
25376
+ <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
25377
+ </div>
25378
+ <div class="card">
25379
+ <div class="card-label">Epics</div>
25380
+ <div class="card-value">${data.linkedEpics.length}</div>
25381
+ <div class="card-sub">linked to sprint</div>
25382
+ </div>
25383
+ <a class="card card-link" href="sprint-blockers">
25384
+ <div class="card-label">Blockers</div>
25385
+ <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
25386
+ <div class="card-sub">${data.workItems.blocked} blocked items</div>
25387
+ </a>
25388
+ <a class="card card-link" href="sprint-risks">
25389
+ <div class="card-label">Risks</div>
25390
+ <div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
25391
+ <div class="card-sub">open risk items</div>
25392
+ </a>
25393
+ </div>`;
25394
+ const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
25395
+ "ss-epics",
25396
+ "Linked Epics",
25397
+ `<div class="table-wrap">
25398
+ <table>
25399
+ <thead>
25400
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
25401
+ </thead>
25402
+ <tbody>
25403
+ ${data.linkedEpics.map((e) => `
25404
+ <tr>
25405
+ <td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
25406
+ <td>${escapeHtml(e.title)}</td>
25407
+ <td>${statusBadge(e.status)}</td>
25408
+ <td>${e.tasksDone} / ${e.tasksTotal}</td>
25409
+ </tr>`).join("")}
25410
+ </tbody>
25411
+ </table>
25412
+ </div>`,
25413
+ { titleTag: "h3" }
25414
+ ) : "";
25415
+ const workItemsSection = renderWorkItemsTable(data.workItems.items, {
25416
+ sectionId: "ss-work-items",
25417
+ title: "Work Items",
25418
+ defaultCollapsed: true
25419
+ });
25420
+ const activitySection = data.artifacts.length > 0 ? collapsibleSection(
25421
+ "ss-activity",
25422
+ "Recent Activity",
25423
+ `<div class="table-wrap">
25424
+ <table>
25425
+ <thead>
25426
+ <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
25427
+ </thead>
25428
+ <tbody>
25429
+ ${data.artifacts.slice(0, 15).map((a) => `
25430
+ <tr>
25431
+ <td>${formatDate(a.date)}</td>
25432
+ <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
25433
+ <td>${escapeHtml(a.title)}</td>
25434
+ <td>${escapeHtml(typeLabel(a.type))}</td>
25435
+ <td>${escapeHtml(a.action)}</td>
25436
+ </tr>`).join("")}
25437
+ </tbody>
25438
+ </table>
25439
+ </div>`,
25440
+ { titleTag: "h3", defaultCollapsed: true }
25441
+ ) : "";
25442
+ const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
25443
+ "ss-meetings",
25444
+ `Meetings (${data.meetings.length})`,
25445
+ `<div class="table-wrap">
25446
+ <table>
25447
+ <thead>
25448
+ <tr><th>Date</th><th>ID</th><th>Title</th></tr>
25449
+ </thead>
25450
+ <tbody>
25451
+ ${data.meetings.map((m) => `
25452
+ <tr>
25453
+ <td>${formatDate(m.date)}</td>
25454
+ <td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
25455
+ <td>${escapeHtml(m.title)}</td>
25456
+ </tr>`).join("")}
25457
+ </tbody>
25458
+ </table>
25459
+ </div>`,
25460
+ { titleTag: "h3", defaultCollapsed: true }
25461
+ ) : "";
25462
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
25463
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
25464
+ return `
25465
+ <div class="page-header">
25466
+ <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
25467
+ <div class="subtitle">Sprint Summary ${dateRange}</div>
25468
+ </div>
25469
+ ${goalHtml}
25470
+ ${progressBar5(data.timeline.percentComplete)}
25471
+ ${statsCards}
25472
+ ${epicsTable}
25473
+ ${workItemsSection}
25474
+ ${activitySection}
25475
+ ${meetingsSection}
25476
+
25477
+ <div class="sprint-ai-section">
25478
+ <h3>AI Summary</h3>
25479
+ ${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>`}
25480
+ <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
25481
+ <div id="summary-loading" class="sprint-loading" style="display:none">
25482
+ <div class="sprint-spinner"></div>
25483
+ <span>Generating summary...</span>
25484
+ </div>
25485
+ <div id="summary-error" class="sprint-error" style="display:none"></div>
25486
+ <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
25487
+ </div>
25488
+
25489
+ <script>
25490
+ async function generateSummary() {
25491
+ var btn = document.getElementById('generate-btn');
25492
+ var loading = document.getElementById('summary-loading');
25493
+ var errorEl = document.getElementById('summary-error');
25494
+ var content = document.getElementById('summary-content');
25495
+
25496
+ btn.disabled = true;
25497
+ btn.style.display = 'none';
25498
+ loading.style.display = 'flex';
25499
+ errorEl.style.display = 'none';
25500
+ content.style.display = 'none';
25501
+
25502
+ try {
25503
+ var res = await fetch('/api/sprint-summary', {
25504
+ method: 'POST',
25505
+ headers: { 'Content-Type': 'application/json' },
25506
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
25507
+ });
25508
+ var json = await res.json();
25509
+ if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
25510
+ loading.style.display = 'none';
25511
+ content.innerHTML = json.html;
25512
+ content.style.display = 'block';
25513
+ btn.textContent = 'Regenerate';
25514
+ btn.style.display = '';
25515
+ btn.disabled = false;
25516
+ } catch (e) {
25517
+ loading.style.display = 'none';
25518
+ errorEl.textContent = e.message;
25519
+ errorEl.style.display = 'block';
25520
+ btn.style.display = '';
25521
+ btn.disabled = false;
25522
+ }
25523
+ }
25524
+ </script>`;
25525
+ }
25526
+
25098
25527
  // src/web/templates/pages/sprint-blockers.ts
25099
25528
  function sprintBlockersPage(data, store) {
25100
25529
  if (!data) {