mrvn-cli 0.5.25 → 0.5.27

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.
@@ -16218,6 +16218,221 @@ function getUpcomingData(store) {
16218
16218
  function getSprintSummaryData(store, sprintId) {
16219
16219
  return collectSprintSummaryData(store, sprintId);
16220
16220
  }
16221
+ var SIBLING_CAP = 8;
16222
+ var ARTIFACT_ID_PATTERN = /\b([A-Z]{1,3}-\d{3,})\b/g;
16223
+ function getArtifactRelationships(store, docId) {
16224
+ const doc = store.get(docId);
16225
+ if (!doc) return null;
16226
+ const fm = doc.frontmatter;
16227
+ const allDocs = store.list();
16228
+ const docIndex = new Map(allDocs.map((d) => [d.frontmatter.id, d]));
16229
+ const origins = [];
16230
+ const parents = [];
16231
+ const children = [];
16232
+ const external = [];
16233
+ const edges = [];
16234
+ const seen = /* @__PURE__ */ new Set([docId]);
16235
+ const addIfExists = (id, relationship, bucket) => {
16236
+ if (seen.has(id)) return false;
16237
+ const target = docIndex.get(id);
16238
+ if (!target) return false;
16239
+ seen.add(id);
16240
+ bucket.push({
16241
+ id: target.frontmatter.id,
16242
+ title: target.frontmatter.title,
16243
+ type: target.frontmatter.type,
16244
+ status: target.frontmatter.status,
16245
+ relationship
16246
+ });
16247
+ return true;
16248
+ };
16249
+ const parentId = fm.aboutArtifact;
16250
+ if (parentId && addIfExists(parentId, "parent", parents)) {
16251
+ edges.push({ from: parentId, to: docId });
16252
+ }
16253
+ const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
16254
+ for (const epicId of linkedEpics) {
16255
+ if (addIfExists(epicId, "epic", parents)) {
16256
+ edges.push({ from: epicId, to: docId });
16257
+ }
16258
+ const epicDoc = docIndex.get(epicId);
16259
+ if (epicDoc) {
16260
+ const features = normalizeLinkedFeatures(epicDoc.frontmatter.linkedFeature);
16261
+ for (const fid of features) {
16262
+ if (addIfExists(fid, "feature", parents)) {
16263
+ edges.push({ from: fid, to: epicId });
16264
+ }
16265
+ }
16266
+ }
16267
+ }
16268
+ const tags = fm.tags ?? [];
16269
+ for (const tag of tags) {
16270
+ if (tag.startsWith("sprint:")) {
16271
+ const sprintId = tag.slice(7);
16272
+ if (addIfExists(sprintId, "sprint", parents)) {
16273
+ edges.push({ from: sprintId, to: docId });
16274
+ }
16275
+ }
16276
+ }
16277
+ for (const tag of tags) {
16278
+ if (tag.startsWith("source:")) {
16279
+ const sourceId = tag.slice(7);
16280
+ if (addIfExists(sourceId, "source", origins)) {
16281
+ edges.push({ from: sourceId, to: docId });
16282
+ }
16283
+ }
16284
+ }
16285
+ const sourceField = fm.source;
16286
+ if (sourceField && /^[A-Z]{1,3}-\d{3,}$/.test(sourceField)) {
16287
+ if (addIfExists(sourceField, "source", origins)) {
16288
+ edges.push({ from: sourceField, to: docId });
16289
+ }
16290
+ }
16291
+ for (const d of allDocs) {
16292
+ if (d.frontmatter.aboutArtifact === docId) {
16293
+ if (addIfExists(d.frontmatter.id, "child", children)) {
16294
+ edges.push({ from: docId, to: d.frontmatter.id });
16295
+ }
16296
+ }
16297
+ }
16298
+ if (fm.type === "epic") {
16299
+ const epicTag = `epic:${docId}`;
16300
+ for (const d of allDocs) {
16301
+ const dfm = d.frontmatter;
16302
+ const dLinkedEpics = normalizeLinkedEpics(dfm.linkedEpic);
16303
+ const dTags = dfm.tags ?? [];
16304
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
16305
+ if (addIfExists(dfm.id, "child", children)) {
16306
+ edges.push({ from: docId, to: dfm.id });
16307
+ }
16308
+ }
16309
+ }
16310
+ }
16311
+ if (parentId) {
16312
+ let siblingCount = 0;
16313
+ for (const d of allDocs) {
16314
+ if (siblingCount >= SIBLING_CAP) break;
16315
+ if (d.frontmatter.aboutArtifact === parentId && d.frontmatter.id !== docId) {
16316
+ if (addIfExists(d.frontmatter.id, "sibling", children)) {
16317
+ edges.push({ from: parentId, to: d.frontmatter.id });
16318
+ siblingCount++;
16319
+ }
16320
+ }
16321
+ }
16322
+ }
16323
+ const jiraKey = fm.jiraKey;
16324
+ const jiraUrl = fm.jiraUrl;
16325
+ if (jiraKey) {
16326
+ external.push({
16327
+ id: jiraKey,
16328
+ title: jiraUrl ?? `Jira: ${jiraKey}`,
16329
+ type: "jira",
16330
+ status: "",
16331
+ relationship: "jira"
16332
+ });
16333
+ edges.push({ from: docId, to: jiraKey });
16334
+ }
16335
+ if (doc.content) {
16336
+ const matches = doc.content.matchAll(ARTIFACT_ID_PATTERN);
16337
+ for (const m of matches) {
16338
+ const refId = m[1];
16339
+ if (refId !== docId && docIndex.has(refId)) {
16340
+ if (addIfExists(refId, "mentioned", external)) {
16341
+ edges.push({ from: docId, to: refId });
16342
+ }
16343
+ }
16344
+ }
16345
+ }
16346
+ return {
16347
+ origins,
16348
+ parents,
16349
+ self: {
16350
+ id: fm.id,
16351
+ title: fm.title,
16352
+ type: fm.type,
16353
+ status: fm.status,
16354
+ relationship: "self"
16355
+ },
16356
+ children,
16357
+ external,
16358
+ edges
16359
+ };
16360
+ }
16361
+ function getArtifactLineageEvents(store, docId) {
16362
+ const doc = store.get(docId);
16363
+ if (!doc) return [];
16364
+ const fm = doc.frontmatter;
16365
+ const events = [];
16366
+ if (fm.created) {
16367
+ events.push({
16368
+ date: fm.created,
16369
+ type: "created",
16370
+ label: `${fm.id} created`
16371
+ });
16372
+ }
16373
+ const tags = fm.tags ?? [];
16374
+ for (const tag of tags) {
16375
+ if (tag.startsWith("source:")) {
16376
+ const sourceId = tag.slice(7);
16377
+ const sourceDoc = store.get(sourceId);
16378
+ if (sourceDoc) {
16379
+ events.push({
16380
+ date: sourceDoc.frontmatter.created,
16381
+ type: "source-linked",
16382
+ label: `Originated from ${sourceId} \u2014 ${sourceDoc.frontmatter.title}`,
16383
+ relatedId: sourceId
16384
+ });
16385
+ }
16386
+ }
16387
+ }
16388
+ const allDocs = store.list();
16389
+ for (const d of allDocs) {
16390
+ if (d.frontmatter.aboutArtifact === docId) {
16391
+ events.push({
16392
+ date: d.frontmatter.created,
16393
+ type: "child-spawned",
16394
+ label: `Spawned ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
16395
+ relatedId: d.frontmatter.id
16396
+ });
16397
+ }
16398
+ }
16399
+ if (fm.type === "epic") {
16400
+ const epicTag = `epic:${docId}`;
16401
+ for (const d of allDocs) {
16402
+ if (d.frontmatter.aboutArtifact === docId) continue;
16403
+ const dLinkedEpics = normalizeLinkedEpics(d.frontmatter.linkedEpic);
16404
+ const dTags = d.frontmatter.tags ?? [];
16405
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
16406
+ events.push({
16407
+ date: d.frontmatter.created,
16408
+ type: "child-spawned",
16409
+ label: `Linked ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
16410
+ relatedId: d.frontmatter.id
16411
+ });
16412
+ }
16413
+ }
16414
+ }
16415
+ const history = fm.assessmentHistory ?? [];
16416
+ for (const entry of history) {
16417
+ if (entry.generatedAt) {
16418
+ events.push({
16419
+ date: entry.generatedAt,
16420
+ type: "assessment",
16421
+ label: "Assessment performed"
16422
+ });
16423
+ }
16424
+ }
16425
+ const lastSync = fm.lastJiraSyncAt;
16426
+ if (lastSync) {
16427
+ events.push({
16428
+ date: lastSync,
16429
+ type: "jira-sync",
16430
+ label: `Synced with Jira ${fm.jiraKey ?? ""}`
16431
+ });
16432
+ }
16433
+ events.sort((a, b) => (b.date ?? "").localeCompare(a.date ?? ""));
16434
+ return events;
16435
+ }
16221
16436
 
16222
16437
  // src/reports/gar/collector.ts
16223
16438
  var DONE_STATUSES4 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
@@ -20391,6 +20606,15 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
20391
20606
  });
20392
20607
  }
20393
20608
  }
20609
+ function computeBlockerProgress(linkedIssues, prerequisiteWeight) {
20610
+ const blockerLinks = linkedIssues.filter(
20611
+ (l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
20612
+ );
20613
+ if (blockerLinks.length === 0) return null;
20614
+ const resolved = blockerLinks.filter((l) => l.isDone).length;
20615
+ const blockerProgress = Math.round(resolved / blockerLinks.length * prerequisiteWeight * 100);
20616
+ return { blockerProgress, totalBlockers: blockerLinks.length, resolvedBlockers: resolved };
20617
+ }
20394
20618
  var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
20395
20619
 
20396
20620
  For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
@@ -20804,6 +21028,39 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
20804
21028
  });
20805
21029
  }
20806
21030
  }
21031
+ const prerequisiteWeight = options.prerequisiteWeight ?? 0.3;
21032
+ const blockerResult = computeBlockerProgress(linkedIssues, prerequisiteWeight);
21033
+ let blockerProgressValue = null;
21034
+ let totalBlockersCount = 0;
21035
+ let resolvedBlockersCount = 0;
21036
+ if (blockerResult && !fm.progressOverride && !DONE_STATUSES6.has(fm.status)) {
21037
+ blockerProgressValue = blockerResult.blockerProgress;
21038
+ totalBlockersCount = blockerResult.totalBlockers;
21039
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
21040
+ const lastProgressUpdate = findLast(proposedUpdates, (u) => u.artifactId === fm.id && u.field === "progress");
21041
+ const implementationProgress = lastProgressUpdate ? lastProgressUpdate.proposedValue : currentProgress;
21042
+ const combinedProgress = Math.round(
21043
+ blockerResult.blockerProgress + implementationProgress * (1 - prerequisiteWeight)
21044
+ );
21045
+ const estimatedProgress = Math.max(currentProgress, combinedProgress);
21046
+ if (estimatedProgress !== currentProgress && estimatedProgress !== implementationProgress) {
21047
+ for (let i = proposedUpdates.length - 1; i >= 0; i--) {
21048
+ if (proposedUpdates[i].artifactId === fm.id && proposedUpdates[i].field === "progress") {
21049
+ proposedUpdates.splice(i, 1);
21050
+ }
21051
+ }
21052
+ proposedUpdates.push({
21053
+ artifactId: fm.id,
21054
+ field: "progress",
21055
+ currentValue: currentProgress,
21056
+ proposedValue: estimatedProgress,
21057
+ reason: `Blocker resolution (${resolvedBlockersCount}/${totalBlockersCount}) + implementation \u2192 dependency-weighted progress ${estimatedProgress}%`
21058
+ });
21059
+ }
21060
+ } else if (blockerResult) {
21061
+ totalBlockersCount = blockerResult.totalBlockers;
21062
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
21063
+ }
20807
21064
  const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
20808
21065
  const appliedUpdates = [];
20809
21066
  if (options.applyUpdates && proposedUpdates.length > 0) {
@@ -20846,7 +21103,10 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
20846
21103
  commentAnalysisProgress,
20847
21104
  signals,
20848
21105
  children,
20849
- linkedIssues
21106
+ linkedIssues,
21107
+ blockerProgressValue,
21108
+ totalBlockersCount,
21109
+ resolvedBlockersCount
20850
21110
  );
20851
21111
  const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
20852
21112
  const legacySummary = fm.assessmentSummary;
@@ -20895,6 +21155,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
20895
21155
  commentAnalysisProgress,
20896
21156
  linkedIssues,
20897
21157
  linkedIssueSignals,
21158
+ blockerProgress: blockerProgressValue,
21159
+ totalBlockers: totalBlockersCount,
21160
+ resolvedBlockers: resolvedBlockersCount,
20898
21161
  children,
20899
21162
  proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
20900
21163
  appliedUpdates,
@@ -21074,6 +21337,9 @@ function emptyArtifactReport(artifactId, errors) {
21074
21337
  commentAnalysisProgress: null,
21075
21338
  linkedIssues: [],
21076
21339
  linkedIssueSignals: [],
21340
+ blockerProgress: null,
21341
+ totalBlockers: 0,
21342
+ resolvedBlockers: 0,
21077
21343
  children: [],
21078
21344
  proposedUpdates: [],
21079
21345
  appliedUpdates: [],
@@ -21081,7 +21347,7 @@ function emptyArtifactReport(artifactId, errors) {
21081
21347
  errors
21082
21348
  };
21083
21349
  }
21084
- function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues) {
21350
+ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues, blockerProgress = null, totalBlockers = 0, resolvedBlockers = 0) {
21085
21351
  const childProgressValues = children.map((c) => {
21086
21352
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
21087
21353
  const lastStatus = findLast(updates, (u) => u.field === "status");
@@ -21105,7 +21371,10 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
21105
21371
  childCount: children.length,
21106
21372
  childDoneCount,
21107
21373
  childRollupProgress,
21108
- linkedIssueCount: linkedIssues.length
21374
+ linkedIssueCount: linkedIssues.length,
21375
+ blockerProgress,
21376
+ totalBlockers,
21377
+ resolvedBlockers
21109
21378
  };
21110
21379
  }
21111
21380
  function formatArtifactReport(report) {
@@ -21141,6 +21410,12 @@ function formatArtifactReport(report) {
21141
21410
  }
21142
21411
  parts.push("");
21143
21412
  }
21413
+ if (report.totalBlockers > 0) {
21414
+ parts.push(`## Blocker Resolution`);
21415
+ const bpLabel = report.blockerProgress !== null ? `${report.blockerProgress}%` : "n/a (skipped)";
21416
+ parts.push(` ${report.resolvedBlockers}/${report.totalBlockers} blockers resolved \u2192 ${bpLabel} prerequisite progress`);
21417
+ parts.push("");
21418
+ }
21144
21419
  if (report.children.length > 0) {
21145
21420
  const doneCount = report.children.filter((c) => DONE_STATUSES6.has(c.marvinStatus)).length;
21146
21421
  const childProgress = Math.round(
@@ -22039,10 +22314,11 @@ function createJiraTools(store, projectConfig) {
22039
22314
  // --- Single-artifact assessment ---
22040
22315
  tool20(
22041
22316
  "assess_artifact",
22042
- "Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
22317
+ "Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, computes dependency-weighted progress from blocker resolution, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
22043
22318
  {
22044
22319
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
22045
- applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
22320
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)"),
22321
+ prerequisiteWeight: external_exports.number().min(0).max(1).optional().describe("Weight for blocker-resolution progress signal (0-1, default 0.3). Portion of effort attributed to dependency readiness.")
22046
22322
  },
22047
22323
  async (args) => {
22048
22324
  const jira = createJiraClient(jiraUserConfig);
@@ -22054,6 +22330,7 @@ function createJiraTools(store, projectConfig) {
22054
22330
  {
22055
22331
  artifactId: args.artifactId,
22056
22332
  applyUpdates: args.applyUpdates ?? false,
22333
+ prerequisiteWeight: args.prerequisiteWeight,
22057
22334
  statusMap
22058
22335
  }
22059
22336
  );
@@ -24037,6 +24314,65 @@ a.artifact-link:hover {
24037
24314
  .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
24038
24315
  .flow-line-dim { opacity: 0.08; }
24039
24316
 
24317
+ /* Relationship graph: self-node emphasis */
24318
+ .flow-self {
24319
+ border-left-width: 4px;
24320
+ background: var(--bg-hover);
24321
+ box-shadow: 0 0 0 1px var(--accent-dim);
24322
+ }
24323
+ .flow-self .flow-node-id {
24324
+ color: var(--accent);
24325
+ font-weight: 600;
24326
+ }
24327
+
24328
+ /* Relationship graph: external nodes */
24329
+ .flow-external {
24330
+ border-left-color: var(--text-dim);
24331
+ border-left-style: dashed;
24332
+ }
24333
+
24334
+ /* Relationship graph: empty state */
24335
+ .flow-empty {
24336
+ padding: 2rem;
24337
+ text-align: center;
24338
+ color: var(--text-dim);
24339
+ font-size: 0.85rem;
24340
+ }
24341
+
24342
+ /* Lineage timeline */
24343
+ .lineage-timeline {
24344
+ margin-top: 1.5rem;
24345
+ }
24346
+ .lineage-timeline h3 {
24347
+ font-size: 1rem;
24348
+ font-weight: 600;
24349
+ margin-bottom: 0.75rem;
24350
+ }
24351
+ .lineage-entry {
24352
+ display: flex;
24353
+ gap: 0.5rem;
24354
+ padding: 0.4rem 0;
24355
+ padding-left: 0.25rem;
24356
+ }
24357
+ .lineage-marker {
24358
+ flex-shrink: 0;
24359
+ font-size: 0.7rem;
24360
+ line-height: 1.4rem;
24361
+ }
24362
+ .lineage-content {
24363
+ display: flex;
24364
+ flex-direction: column;
24365
+ gap: 0.1rem;
24366
+ }
24367
+ .lineage-date {
24368
+ font-size: 0.7rem;
24369
+ color: var(--text-dim);
24370
+ font-family: var(--mono);
24371
+ }
24372
+ .lineage-label {
24373
+ font-size: 0.85rem;
24374
+ }
24375
+
24040
24376
  /* Gantt truncation note */
24041
24377
  .mermaid-note {
24042
24378
  font-size: 0.75rem;
@@ -24963,842 +25299,1079 @@ function documentsPage(data) {
24963
25299
  `;
24964
25300
  }
24965
25301
 
24966
- // src/web/templates/pages/document-detail.ts
24967
- function documentDetailPage(doc) {
24968
- const fm = doc.frontmatter;
24969
- const label = typeLabel(fm.type);
24970
- const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
24971
- const entries = Object.entries(fm).filter(
24972
- ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
24973
- );
24974
- const arrayEntries = Object.entries(fm).filter(
24975
- ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
24976
- );
24977
- const allEntries = [
24978
- ...entries.filter(([, v]) => !Array.isArray(v)),
24979
- ...arrayEntries
24980
- ];
24981
- const dtDd = allEntries.map(([key, value]) => {
24982
- let rendered;
24983
- if (key === "status") {
24984
- rendered = statusBadge(value);
24985
- } else if (key === "tags" && Array.isArray(value)) {
24986
- rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
24987
- } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
24988
- rendered = formatDate(value);
24989
- } else {
24990
- rendered = linkArtifactIds(escapeHtml(String(value)));
24991
- }
24992
- return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
24993
- }).join("\n ");
24994
- const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
24995
- const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
24996
- const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
24997
- return `
24998
- <div class="breadcrumb">
24999
- <a href="/">Overview</a><span class="sep">/</span>
25000
- <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
25001
- ${escapeHtml(fm.id)}
25002
- </div>
25003
-
25004
- <div class="page-header">
25005
- <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
25006
- <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
25007
- </div>
25008
-
25009
- <div class="detail-meta">
25010
- <dl>
25011
- ${dtDd}
25012
- </dl>
25013
- </div>
25014
-
25015
- ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
25016
-
25017
- ${timelineHtml}
25018
- `;
25302
+ // src/web/templates/mermaid.ts
25303
+ function sanitize(text, maxLen = 40) {
25304
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
25305
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
25019
25306
  }
25020
- function isValidAssessmentEntry(value) {
25021
- if (typeof value !== "object" || value === null) return false;
25022
- const obj = value;
25023
- if (typeof obj.generatedAt !== "string") return false;
25024
- if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
25025
- return true;
25307
+ function mermaidBlock(definition, extraClass) {
25308
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
25309
+ return `<div class="${cls}"><pre class="mermaid">
25310
+ ${definition}
25311
+ </pre></div>`;
25026
25312
  }
25027
- function normalizeEntry(entry) {
25028
- return {
25029
- generatedAt: entry.generatedAt ?? "",
25030
- commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
25031
- commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
25032
- signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
25033
- childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
25034
- childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
25035
- childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
25036
- linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
25037
- };
25313
+ function placeholder(message) {
25314
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
25038
25315
  }
25039
- function renderAssessmentTimeline(history) {
25040
- const entries = history.map((raw, i) => {
25041
- const entry = normalizeEntry(raw);
25042
- const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
25043
- const time3 = entry.generatedAt?.slice(11, 16) ?? "";
25044
- const isLatest = i === 0;
25045
- const parts = [];
25046
- if (entry.commentSummary) {
25047
- parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
25048
- }
25049
- if (entry.commentAnalysisProgress !== null) {
25050
- parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
25051
- }
25052
- if (entry.childCount > 0) {
25053
- const bar = progressBarHtml(entry.childRollupProgress ?? 0);
25054
- parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
25055
- }
25056
- if (entry.linkedIssueCount > 0) {
25057
- parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
25058
- }
25059
- if (entry.signals.length > 0) {
25060
- const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
25061
- parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
25062
- }
25063
- return `
25064
- <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
25065
- <div class="assessment-header">
25066
- <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
25067
- ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
25068
- </div>
25069
- ${parts.join("\n")}
25070
- </div>`;
25071
- });
25072
- return `
25073
- <div class="assessment-timeline">
25074
- <h3>Assessment History</h3>
25075
- ${entries.join("\n")}
25076
- </div>`;
25316
+ function toMs(date5) {
25317
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
25077
25318
  }
25078
- function progressBarHtml(pct) {
25079
- const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
25080
- const empty = 10 - filled;
25081
- return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
25319
+ function fmtDate(ms) {
25320
+ const d = new Date(ms);
25321
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
25322
+ return `${months[d.getMonth()]} ${d.getDate()}`;
25082
25323
  }
25083
-
25084
- // src/web/persona-views.ts
25085
- var VIEWS = /* @__PURE__ */ new Map();
25086
- var PAGE_RENDERERS = /* @__PURE__ */ new Map();
25087
- function registerPersonaView(config2) {
25088
- VIEWS.set(config2.shortName, config2);
25089
- }
25090
- function registerPersonaPage(persona, pageId, renderer) {
25091
- PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
25092
- }
25093
- function getPersonaView(mode) {
25094
- if (!mode) return void 0;
25095
- return VIEWS.get(mode);
25096
- }
25097
- function getPersonaPageRenderer(persona, pageId) {
25098
- return PAGE_RENDERERS.get(`${persona}/${pageId}`);
25099
- }
25100
- function getAllPersonaViews() {
25101
- return [...VIEWS.values()];
25102
- }
25103
- var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
25104
- function parsePersonaFromUrl(params) {
25105
- const value = params.get("persona")?.toLowerCase();
25106
- if (value && VALID_PERSONAS.has(value)) return value;
25107
- return null;
25108
- }
25109
- function parsePersonaFromPath(pathname) {
25110
- const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
25111
- return match ? match[1] : null;
25324
+ function buildTimelineGantt(data, maxSprints = 6) {
25325
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
25326
+ if (sprintsWithDates.length === 0) {
25327
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
25328
+ }
25329
+ const truncated = sprintsWithDates.length > maxSprints;
25330
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
25331
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
25332
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
25333
+ const allStarts = visibleSprints.map((s) => toMs(s.startDate));
25334
+ const allEnds = visibleSprints.map((s) => toMs(s.endDate));
25335
+ const timelineStart = Math.min(...allStarts);
25336
+ const timelineEnd = Math.max(...allEnds);
25337
+ const span = timelineEnd - timelineStart || 1;
25338
+ const pct = (ms) => (ms - timelineStart) / span * 100;
25339
+ const DAY = 864e5;
25340
+ const markers = [];
25341
+ let tick = timelineStart;
25342
+ const startDay = new Date(tick).getDay();
25343
+ tick += (8 - startDay) % 7 * DAY;
25344
+ while (tick <= timelineEnd) {
25345
+ const left = pct(tick);
25346
+ markers.push(
25347
+ `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
25348
+ );
25349
+ tick += 7 * DAY;
25350
+ }
25351
+ const gridLines = [];
25352
+ let gridTick = timelineStart;
25353
+ const gridStartDay = new Date(gridTick).getDay();
25354
+ gridTick += (8 - gridStartDay) % 7 * DAY;
25355
+ while (gridTick <= timelineEnd) {
25356
+ gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
25357
+ gridTick += 7 * DAY;
25358
+ }
25359
+ const sprintBoundaries = /* @__PURE__ */ new Set();
25360
+ for (const sprint of visibleSprints) {
25361
+ sprintBoundaries.add(toMs(sprint.startDate));
25362
+ sprintBoundaries.add(toMs(sprint.endDate));
25363
+ }
25364
+ const sprintLines = [...sprintBoundaries].map(
25365
+ (ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
25366
+ );
25367
+ const now = Date.now();
25368
+ let todayMarker = "";
25369
+ if (now >= timelineStart && now <= timelineEnd) {
25370
+ todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
25371
+ }
25372
+ const sprintBlocks = visibleSprints.map((sprint) => {
25373
+ const sStart = toMs(sprint.startDate);
25374
+ const sEnd = toMs(sprint.endDate);
25375
+ const left = pct(sStart).toFixed(2);
25376
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
25377
+ return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
25378
+ }).join("");
25379
+ const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
25380
+ <div class="gantt-label gantt-section-label">Sprints</div>
25381
+ <div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
25382
+ </div>`;
25383
+ const epicSpanMap = /* @__PURE__ */ new Map();
25384
+ for (const sprint of visibleSprints) {
25385
+ const sStart = toMs(sprint.startDate);
25386
+ const sEnd = toMs(sprint.endDate);
25387
+ for (const eid of sprint.linkedEpics) {
25388
+ if (!epicMap.has(eid)) continue;
25389
+ const existing = epicSpanMap.get(eid);
25390
+ if (existing) {
25391
+ existing.startMs = Math.min(existing.startMs, sStart);
25392
+ existing.endMs = Math.max(existing.endMs, sEnd);
25393
+ } else {
25394
+ epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
25395
+ }
25396
+ }
25397
+ }
25398
+ const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
25399
+ const aSpan = epicSpanMap.get(a);
25400
+ const bSpan = epicSpanMap.get(b);
25401
+ if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
25402
+ return a.localeCompare(b);
25403
+ });
25404
+ const epicRows = sortedEpicIds.map((eid) => {
25405
+ const epic = epicMap.get(eid);
25406
+ const { startMs, endMs } = epicSpanMap.get(eid);
25407
+ const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
25408
+ const left = pct(startMs).toFixed(2);
25409
+ const width = (pct(endMs) - pct(startMs)).toFixed(2);
25410
+ const label = sanitize(epic.id + " " + epic.title);
25411
+ return `<div class="gantt-row">
25412
+ <div class="gantt-label">${label}</div>
25413
+ <div class="gantt-track">
25414
+ <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
25415
+ </div>
25416
+ </div>`;
25417
+ }).join("\n");
25418
+ const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
25419
+ return `${note}
25420
+ <div class="gantt">
25421
+ <div class="gantt-chart">
25422
+ <div class="gantt-header">
25423
+ <div class="gantt-label"></div>
25424
+ <div class="gantt-track gantt-dates">${markers.join("")}</div>
25425
+ </div>
25426
+ ${sprintBandRow}
25427
+ ${epicRows}
25428
+ </div>
25429
+ <div class="gantt-overlay">
25430
+ <div class="gantt-label"></div>
25431
+ <div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
25432
+ </div>
25433
+ </div>`;
25112
25434
  }
25113
- function resolvePersona(pathname, params) {
25114
- return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
25435
+ function statusClass(status) {
25436
+ const s = status.toLowerCase();
25437
+ if (s === "done" || s === "completed") return "flow-done";
25438
+ if (s === "in-progress" || s === "active") return "flow-active";
25439
+ if (s === "blocked") return "flow-blocked";
25440
+ return "flow-default";
25115
25441
  }
25116
- var SHARED_NAV_ITEMS = [
25117
- { pageId: "timeline", label: "Timeline" },
25118
- { pageId: "board", label: "Board" },
25119
- { pageId: "upcoming", label: "Upcoming" },
25120
- { pageId: "sprint-summary", label: "Sprint Summary" },
25121
- { pageId: "gar", label: "GAR Report" },
25122
- { pageId: "health", label: "Health" }
25123
- ];
25124
-
25125
- // src/web/templates/pages/persona-picker.ts
25126
- function personaPickerPage() {
25127
- const views = getAllPersonaViews();
25128
- const cards = views.map(
25129
- (v) => `
25130
- <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
25131
- <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
25132
- <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
25133
- </a>`
25134
- ).join("\n");
25442
+ function buildArtifactFlowchart(data) {
25443
+ if (data.features.length === 0 && data.epics.length === 0) {
25444
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
25445
+ }
25446
+ const edges = [];
25447
+ const epicsByFeature = /* @__PURE__ */ new Map();
25448
+ for (const epic of data.epics) {
25449
+ for (const fid of epic.linkedFeature) {
25450
+ if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
25451
+ epicsByFeature.get(fid).push(epic.id);
25452
+ edges.push({ from: fid, to: epic.id });
25453
+ }
25454
+ }
25455
+ const sprintsByEpic = /* @__PURE__ */ new Map();
25456
+ for (const sprint of data.sprints) {
25457
+ for (const eid of sprint.linkedEpics) {
25458
+ if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
25459
+ sprintsByEpic.get(eid).push(sprint.id);
25460
+ edges.push({ from: eid, to: sprint.id });
25461
+ }
25462
+ }
25463
+ const connectedFeatureIds = new Set(epicsByFeature.keys());
25464
+ const connectedEpicIds = /* @__PURE__ */ new Set();
25465
+ for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
25466
+ for (const ids of sprintsByEpic.values()) ids.forEach(() => {
25467
+ });
25468
+ for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
25469
+ const connectedSprintIds = /* @__PURE__ */ new Set();
25470
+ for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
25471
+ const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
25472
+ const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
25473
+ const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
25474
+ if (features.length === 0 && epics.length === 0) {
25475
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
25476
+ }
25477
+ const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
25478
+ <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
25479
+ <span class="flow-node-title">${sanitize(title, 35)}</span>
25480
+ </div>`;
25481
+ const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
25482
+ const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
25483
+ const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
25484
+ const edgesJson = JSON.stringify(edges);
25135
25485
  return `
25136
- <div class="persona-picker">
25137
- <h2>Choose Your View</h2>
25138
- <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
25139
- <div class="persona-picker-grid">
25140
- ${cards}
25486
+ <div class="flow-diagram" id="flow-diagram">
25487
+ <svg class="flow-lines" id="flow-lines"></svg>
25488
+ <div class="flow-columns">
25489
+ <div class="flow-column">
25490
+ <div class="flow-column-header">Features</div>
25491
+ ${featuresHtml}
25492
+ </div>
25493
+ <div class="flow-column">
25494
+ <div class="flow-column-header">Epics</div>
25495
+ ${epicsHtml}
25496
+ </div>
25497
+ <div class="flow-column">
25498
+ <div class="flow-column-header">Sprints</div>
25499
+ ${sprintsHtml}
25500
+ </div>
25141
25501
  </div>
25142
- </div>`;
25143
- }
25502
+ </div>
25503
+ <script>
25504
+ (function() {
25505
+ var edges = ${edgesJson};
25506
+ var container = document.getElementById('flow-diagram');
25507
+ var svg = document.getElementById('flow-lines');
25508
+ if (!container || !svg) return;
25509
+
25510
+ // Build directed adjacency maps for traversal
25511
+ var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
25512
+ var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
25513
+ edges.forEach(function(e) {
25514
+ if (!fwd[e.from]) fwd[e.from] = [];
25515
+ if (!bwd[e.to]) bwd[e.to] = [];
25516
+ fwd[e.from].push(e.to);
25517
+ bwd[e.to].push(e.from);
25518
+ });
25519
+
25520
+ function drawLines() {
25521
+ var rect = container.getBoundingClientRect();
25522
+ var scrollW = container.scrollWidth;
25523
+ var scrollH = container.scrollHeight;
25524
+ svg.setAttribute('width', scrollW);
25525
+ svg.setAttribute('height', scrollH);
25526
+ svg.innerHTML = '';
25527
+
25528
+ // Use scroll offsets so lines align with scrolled content
25529
+ var scrollLeft = container.scrollLeft;
25530
+ var scrollTop = container.scrollTop;
25531
+
25532
+ edges.forEach(function(edge) {
25533
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
25534
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
25535
+ if (!fromEl || !toEl) return;
25536
+
25537
+ var fr = fromEl.getBoundingClientRect();
25538
+ var tr = toEl.getBoundingClientRect();
25539
+ var x1 = fr.right - rect.left + scrollLeft;
25540
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
25541
+ var x2 = tr.left - rect.left + scrollLeft;
25542
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
25543
+ var mx = (x1 + x2) / 2;
25144
25544
 
25145
- // src/reports/sprint-summary/risk-assessment.ts
25146
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
25147
- var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
25545
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
25546
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
25547
+ path.setAttribute('fill', 'none');
25548
+ path.setAttribute('stroke', '#2a2e3a');
25549
+ path.setAttribute('stroke-width', '1.5');
25550
+ path.dataset.from = edge.from;
25551
+ path.dataset.to = edge.to;
25552
+ svg.appendChild(path);
25553
+ });
25554
+ }
25148
25555
 
25149
- IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
25556
+ // Find directly related nodes via directed traversal
25557
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
25558
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
25559
+ function findConnected(startId) {
25560
+ var visited = {};
25561
+ visited[startId] = true;
25562
+ // Traverse forward (from\u2192to direction)
25563
+ var queue = [startId];
25564
+ while (queue.length) {
25565
+ var id = queue.shift();
25566
+ (fwd[id] || []).forEach(function(neighbor) {
25567
+ if (!visited[neighbor]) {
25568
+ visited[neighbor] = true;
25569
+ queue.push(neighbor);
25570
+ }
25571
+ });
25572
+ }
25573
+ // Traverse backward (to\u2192from direction)
25574
+ queue = [startId];
25575
+ while (queue.length) {
25576
+ var id = queue.shift();
25577
+ (bwd[id] || []).forEach(function(neighbor) {
25578
+ if (!visited[neighbor]) {
25579
+ visited[neighbor] = true;
25580
+ queue.push(neighbor);
25581
+ }
25582
+ });
25583
+ }
25584
+ return visited;
25585
+ }
25150
25586
 
25151
- Produce a concise markdown assessment with these sections:
25587
+ function highlight(hoveredId) {
25588
+ var connected = findConnected(hoveredId);
25589
+ container.querySelectorAll('.flow-node').forEach(function(n) {
25590
+ if (connected[n.dataset.flowId]) {
25591
+ n.classList.add('flow-lit');
25592
+ n.classList.remove('flow-dim');
25593
+ } else {
25594
+ n.classList.add('flow-dim');
25595
+ n.classList.remove('flow-lit');
25596
+ }
25597
+ });
25598
+ svg.querySelectorAll('path').forEach(function(p) {
25599
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
25600
+ p.classList.add('flow-line-lit');
25601
+ p.classList.remove('flow-line-dim');
25602
+ } else {
25603
+ p.classList.add('flow-line-dim');
25604
+ p.classList.remove('flow-line-lit');
25605
+ }
25606
+ });
25607
+ }
25152
25608
 
25153
- ## Status Assessment
25154
- One-line verdict: is this risk actively being mitigated, stalled, or escalating?
25609
+ function clearHighlight() {
25610
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
25611
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
25612
+ }
25155
25613
 
25156
- ## Related Activity
25157
- What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
25614
+ var activeId = null;
25615
+ container.addEventListener('click', function(e) {
25616
+ // Let the ID link navigate normally
25617
+ if (e.target.closest('a')) return;
25158
25618
 
25159
- ## Trajectory
25160
- Based on the data (status of related items, time remaining, ownership), is this risk trending toward resolution or toward becoming a blocker? Explain your reasoning with concrete evidence.
25619
+ var node = e.target.closest('.flow-node');
25620
+ var clickedId = node ? node.dataset.flowId : null;
25161
25621
 
25162
- ## Recommendation
25163
- One concrete next step to move this risk toward resolution.
25622
+ if (!clickedId || clickedId === activeId) {
25623
+ activeId = null;
25624
+ clearHighlight();
25625
+ return;
25626
+ }
25164
25627
 
25165
- Rules:
25166
- - Reference artifact IDs, dates, owners, and statuses from the provided data
25167
- - Keep the tone professional and direct
25168
- - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
25169
- - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
25170
- - Produce the full assessment text directly`;
25171
- async function generateRiskAssessment(data, riskId, store) {
25172
- const risk = data.risks.find((r) => r.id === riskId);
25173
- if (!risk) return "Risk not found in sprint data.";
25174
- const prompt = buildSingleRiskPrompt(data, risk, store);
25175
- const result = query3({
25176
- prompt,
25177
- options: {
25178
- systemPrompt: SYSTEM_PROMPT2,
25179
- maxTurns: 1,
25180
- tools: [],
25181
- allowedTools: []
25182
- }
25183
- });
25184
- for await (const msg of result) {
25185
- if (msg.type === "assistant") {
25186
- const text = msg.message.content.find(
25187
- (b) => b.type === "text"
25188
- );
25189
- if (text) return text.text;
25190
- }
25628
+ activeId = clickedId;
25629
+ highlight(clickedId);
25630
+ });
25631
+
25632
+ function drawAndHighlight() {
25633
+ drawLines();
25634
+ if (activeId) highlight(activeId);
25635
+ }
25636
+
25637
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
25638
+ window.addEventListener('resize', drawAndHighlight);
25639
+ container.addEventListener('scroll', drawAndHighlight);
25640
+ new ResizeObserver(drawAndHighlight).observe(container);
25641
+ })();
25642
+ </script>`;
25643
+ }
25644
+ function buildStatusPie(title, counts) {
25645
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
25646
+ if (entries.length === 0) {
25647
+ return placeholder(`No data for ${title}.`);
25191
25648
  }
25192
- return "Unable to generate risk assessment.";
25649
+ const lines = [`pie title ${sanitize(title, 60)}`];
25650
+ for (const [label, count] of entries) {
25651
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
25652
+ }
25653
+ return mermaidBlock(lines.join("\n"));
25193
25654
  }
25194
- function buildSingleRiskPrompt(data, risk, store) {
25195
- const sections = [];
25196
- sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
25197
- if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
25198
- sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
25199
- sections.push(`Completion: ${data.workItems.completionPct}%`);
25200
- sections.push("");
25201
- const doc = store.get(risk.id);
25202
- sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
25203
- sections.push(`Type: ${risk.type}`);
25204
- if (doc) {
25205
- sections.push(`Status: ${doc.frontmatter.status}`);
25206
- if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
25207
- if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
25208
- if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
25209
- if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
25210
- if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
25211
- const tags = doc.frontmatter.tags ?? [];
25212
- if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
25213
- if (doc.content.trim()) {
25214
- sections.push(`
25215
- Description:
25216
- ${doc.content.trim()}`);
25217
- }
25218
- const allDocs = store.list();
25219
- const relatedIds = /* @__PURE__ */ new Set();
25220
- for (const d of allDocs) {
25221
- if (d.frontmatter.aboutArtifact === risk.id) {
25222
- relatedIds.add(d.frontmatter.id);
25223
- }
25224
- }
25225
- const idPattern = /\b([A-Z]-\d{3,})\b/g;
25226
- let match;
25227
- while ((match = idPattern.exec(doc.content)) !== null) {
25228
- relatedIds.add(match[1]);
25229
- }
25230
- const significantTags = tags.filter(
25231
- (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
25232
- );
25233
- if (significantTags.length > 0) {
25234
- for (const d of allDocs) {
25235
- if (d.frontmatter.id === risk.id) continue;
25236
- const dTags = d.frontmatter.tags ?? [];
25237
- if (significantTags.some((t) => dTags.includes(t))) {
25238
- relatedIds.add(d.frontmatter.id);
25239
- }
25240
- }
25241
- }
25242
- const about = doc.frontmatter.aboutArtifact;
25243
- if (about) {
25244
- relatedIds.add(about);
25245
- for (const d of allDocs) {
25246
- if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
25247
- relatedIds.add(d.frontmatter.id);
25248
- }
25249
- }
25250
- }
25251
- const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
25252
- if (relatedDocs.length > 0) {
25253
- sections.push(`
25254
- ## Related Documents (${relatedDocs.length})`);
25255
- for (const rd of relatedDocs) {
25256
- const owner = rd.frontmatter.owner ?? "unowned";
25257
- const summary = rd.content.trim().slice(0, 300);
25258
- sections.push(
25259
- `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
25260
- );
25261
- sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
25262
- if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
25263
- }
25264
- }
25655
+ function buildHealthGauge(categories) {
25656
+ const valid = categories.filter((c) => c.total > 0);
25657
+ if (valid.length === 0) {
25658
+ return placeholder("No completeness data available.");
25265
25659
  }
25266
- sections.push("");
25267
- sections.push(`---`);
25268
- sections.push(`
25269
- Generate the risk assessment for ${risk.id} based on the data above.`);
25270
- return sections.join("\n");
25660
+ const pies = valid.map((cat) => {
25661
+ const incomplete = cat.total - cat.complete;
25662
+ const lines = [
25663
+ `pie title ${sanitize(cat.name, 30)}`,
25664
+ ` "Complete" : ${cat.complete}`,
25665
+ ` "Incomplete" : ${incomplete}`
25666
+ ];
25667
+ return mermaidBlock(lines.join("\n"));
25668
+ });
25669
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
25271
25670
  }
25272
25671
 
25273
- // src/personas/builtin/product-owner.ts
25274
- var productOwner = {
25275
- id: "product-owner",
25276
- name: "Product Owner",
25277
- shortName: "po",
25278
- description: "Focuses on product vision, stakeholder needs, backlog prioritization, and value delivery.",
25279
- systemPrompt: `You are Marvin, acting as a **Product Owner**. Your role is to help the team maximize the value delivered by the product.
25672
+ // src/web/templates/artifact-graph.ts
25673
+ function buildArtifactRelationGraph(data) {
25674
+ const hasContent = data.origins.length > 0 || data.parents.length > 0 || data.children.length > 0 || data.external.length > 0;
25675
+ if (!hasContent) {
25676
+ return `<div class="flow-diagram flow-empty"><p>No relationships found for this artifact.</p></div>`;
25677
+ }
25678
+ const edges = data.edges;
25679
+ const renderNode = (id, title, status, type) => {
25680
+ const href = type === "jira" ? title.startsWith("http") ? title : "#" : `/docs/${type}/${id}`;
25681
+ const target = type === "jira" ? ' target="_blank" rel="noopener"' : "";
25682
+ const cls = type === "jira" ? "flow-node flow-external" : `flow-node ${statusClass(status)}`;
25683
+ const displayTitle = type === "jira" ? "Jira Issue" : sanitize(title, 35);
25684
+ const displayId = type === "jira" ? `${id} \u2197` : id;
25685
+ return `<div class="${cls}" data-flow-id="${escapeHtml(id)}">
25686
+ <a class="flow-node-id" href="${escapeHtml(href)}"${target}>${escapeHtml(displayId)}</a>
25687
+ <span class="flow-node-title">${escapeHtml(displayTitle)}</span>
25688
+ </div>`;
25689
+ };
25690
+ const selfNode = `<div class="flow-node flow-self ${statusClass(data.self.status)}" data-flow-id="${escapeHtml(data.self.id)}">
25691
+ <span class="flow-node-id">${escapeHtml(data.self.id)}</span>
25692
+ <span class="flow-node-title">${escapeHtml(sanitize(data.self.title, 35))}</span>
25693
+ </div>`;
25694
+ const columns = [];
25695
+ if (data.origins.length > 0) {
25696
+ columns.push({
25697
+ header: "Origins",
25698
+ nodes: data.origins.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25699
+ });
25700
+ }
25701
+ if (data.parents.length > 0) {
25702
+ columns.push({
25703
+ header: "Parents",
25704
+ nodes: data.parents.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25705
+ });
25706
+ }
25707
+ columns.push({
25708
+ header: data.self.type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
25709
+ nodes: selfNode
25710
+ });
25711
+ if (data.children.length > 0) {
25712
+ columns.push({
25713
+ header: "Children",
25714
+ nodes: data.children.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25715
+ });
25716
+ }
25717
+ if (data.external.length > 0) {
25718
+ columns.push({
25719
+ header: "External",
25720
+ nodes: data.external.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25721
+ });
25722
+ }
25723
+ const columnsHtml = columns.map((col) => `
25724
+ <div class="flow-column">
25725
+ <div class="flow-column-header">${escapeHtml(col.header)}</div>
25726
+ ${col.nodes}
25727
+ </div>`).join("\n");
25728
+ const edgesJson = JSON.stringify(edges);
25729
+ return `
25730
+ <div class="flow-diagram" id="rel-graph">
25731
+ <svg class="flow-lines" id="rel-lines"></svg>
25732
+ <div class="flow-columns">
25733
+ ${columnsHtml}
25734
+ </div>
25735
+ </div>
25736
+ <script>
25737
+ (function() {
25738
+ var edges = ${edgesJson};
25739
+ var container = document.getElementById('rel-graph');
25740
+ var svg = document.getElementById('rel-lines');
25741
+ if (!container || !svg) return;
25742
+
25743
+ var fwd = {};
25744
+ var bwd = {};
25745
+ edges.forEach(function(e) {
25746
+ if (!fwd[e.from]) fwd[e.from] = [];
25747
+ if (!bwd[e.to]) bwd[e.to] = [];
25748
+ fwd[e.from].push(e.to);
25749
+ bwd[e.to].push(e.from);
25750
+ });
25280
25751
 
25281
- ## Core Responsibilities
25282
- - Define and communicate the product vision and strategy
25283
- - Manage and prioritize the product backlog
25284
- - Ensure stakeholder needs are understood and addressed
25285
- - Make decisions about scope, priority, and trade-offs
25286
- - Accept or reject work results based on acceptance criteria
25752
+ function drawLines() {
25753
+ var rect = container.getBoundingClientRect();
25754
+ var scrollW = container.scrollWidth;
25755
+ var scrollH = container.scrollHeight;
25756
+ svg.setAttribute('width', scrollW);
25757
+ svg.setAttribute('height', scrollH);
25758
+ svg.innerHTML = '';
25287
25759
 
25288
- ## How You Work
25289
- - Ask clarifying questions to understand business value and user needs
25290
- - Create and refine decisions (D-xxx) for important product choices
25291
- - Track questions (Q-xxx) that need stakeholder input
25292
- - Define acceptance criteria for features and deliverables
25293
- - Prioritize actions (A-xxx) based on business value
25760
+ var scrollLeft = container.scrollLeft;
25761
+ var scrollTop = container.scrollTop;
25294
25762
 
25295
- ## Communication Style
25296
- - Business-oriented language, avoid unnecessary technical jargon
25297
- - Focus on outcomes and value, not implementation details
25298
- - Be decisive but transparent about trade-offs
25299
- - Challenge assumptions that don't align with product goals`,
25300
- focusAreas: [
25301
- "Product vision and strategy",
25302
- "Backlog management",
25303
- "Stakeholder communication",
25304
- "Value delivery",
25305
- "Acceptance criteria",
25306
- "Feature definition and prioritization"
25307
- ],
25308
- documentTypes: ["decision", "question", "action", "feature"],
25309
- contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
25310
- };
25763
+ edges.forEach(function(edge) {
25764
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
25765
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
25766
+ if (!fromEl || !toEl) return;
25311
25767
 
25312
- // src/personas/builtin/delivery-manager.ts
25313
- var deliveryManager = {
25314
- id: "delivery-manager",
25315
- name: "Delivery Manager",
25316
- shortName: "dm",
25317
- description: "Focuses on project delivery, risk management, team coordination, and process governance.",
25318
- systemPrompt: `You are Marvin, acting as a **Delivery Manager**. Your role is to ensure the project is delivered on time, within scope, and with managed risks.
25768
+ var fr = fromEl.getBoundingClientRect();
25769
+ var tr = toEl.getBoundingClientRect();
25770
+ var x1 = fr.right - rect.left + scrollLeft;
25771
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
25772
+ var x2 = tr.left - rect.left + scrollLeft;
25773
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
25774
+ var mx = (x1 + x2) / 2;
25319
25775
 
25320
- ## Core Responsibilities
25321
- - Track project progress and identify blockers
25322
- - Manage risks, issues, and dependencies
25323
- - Coordinate between team members and stakeholders
25324
- - Ensure governance processes are followed (decisions logged, actions tracked)
25325
- - Facilitate meetings and ensure outcomes are captured
25776
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
25777
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
25778
+ path.setAttribute('fill', 'none');
25779
+ path.setAttribute('stroke', '#2a2e3a');
25780
+ path.setAttribute('stroke-width', '1.5');
25781
+ path.dataset.from = edge.from;
25782
+ path.dataset.to = edge.to;
25783
+ svg.appendChild(path);
25784
+ });
25785
+ }
25326
25786
 
25327
- ## How You Work
25328
- - Review open actions (A-xxx) and follow up on overdue items
25329
- - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
25330
- - Assign actions to sprints when sprint planning is active, using the sprints parameter
25331
- - Ensure decisions (D-xxx) are properly documented with rationale
25332
- - Track questions (Q-xxx) and ensure they get answered
25333
- - Monitor project health and flag risks early
25334
- - Create meeting notes and ensure action items are assigned
25787
+ function findConnected(startId) {
25788
+ var visited = {};
25789
+ visited[startId] = true;
25790
+ var queue = [startId];
25791
+ while (queue.length) {
25792
+ var id = queue.shift();
25793
+ (fwd[id] || []).forEach(function(n) {
25794
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
25795
+ });
25796
+ }
25797
+ queue = [startId];
25798
+ while (queue.length) {
25799
+ var id = queue.shift();
25800
+ (bwd[id] || []).forEach(function(n) {
25801
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
25802
+ });
25803
+ }
25804
+ return visited;
25805
+ }
25335
25806
 
25336
- ## Communication Style
25337
- - Process-oriented but pragmatic
25338
- - Focus on status, risks, and blockers
25339
- - Be proactive about follow-ups and deadlines
25340
- - Keep stakeholders informed with concise updates`,
25341
- focusAreas: [
25342
- "Project delivery",
25343
- "Risk management",
25344
- "Team coordination",
25345
- "Process governance",
25346
- "Status tracking",
25347
- "Epic scheduling and tracking",
25348
- "Sprint planning and tracking"
25349
- ],
25350
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
25351
- contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
25352
- };
25807
+ function highlight(hoveredId) {
25808
+ var connected = findConnected(hoveredId);
25809
+ container.querySelectorAll('.flow-node').forEach(function(n) {
25810
+ if (connected[n.dataset.flowId]) {
25811
+ n.classList.add('flow-lit'); n.classList.remove('flow-dim');
25812
+ } else {
25813
+ n.classList.add('flow-dim'); n.classList.remove('flow-lit');
25814
+ }
25815
+ });
25816
+ svg.querySelectorAll('path').forEach(function(p) {
25817
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
25818
+ p.classList.add('flow-line-lit'); p.classList.remove('flow-line-dim');
25819
+ } else {
25820
+ p.classList.add('flow-line-dim'); p.classList.remove('flow-line-lit');
25821
+ }
25822
+ });
25823
+ }
25353
25824
 
25354
- // src/personas/builtin/tech-lead.ts
25355
- var techLead = {
25356
- id: "tech-lead",
25357
- name: "Technical Lead",
25358
- shortName: "tl",
25359
- description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
25360
- systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
25825
+ function clearHighlight() {
25826
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
25827
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
25828
+ }
25361
25829
 
25362
- ## Core Responsibilities
25363
- - Define and maintain technical architecture
25364
- - Make and document technical decisions with clear rationale
25365
- - Review technical approaches and identify potential issues
25366
- - Guide the team on best practices and patterns
25367
- - Evaluate technical risks and propose mitigations
25830
+ var activeId = null;
25831
+ container.addEventListener('click', function(e) {
25832
+ if (e.target.closest('a')) return;
25833
+ var node = e.target.closest('.flow-node');
25834
+ var clickedId = node ? node.dataset.flowId : null;
25835
+ if (!clickedId || clickedId === activeId) {
25836
+ activeId = null; clearHighlight(); return;
25837
+ }
25838
+ activeId = clickedId;
25839
+ highlight(clickedId);
25840
+ });
25368
25841
 
25369
- ## How You Work
25370
- - Create decisions (D-xxx) for significant technical choices (framework, architecture, patterns)
25371
- - Document technical questions (Q-xxx) that need investigation or proof-of-concept
25372
- - Define technical actions (A-xxx) for implementation tasks
25373
- - Consider non-functional requirements (performance, security, maintainability)
25374
- - Provide clear technical guidance with examples when helpful
25842
+ function drawAndHighlight() {
25843
+ drawLines();
25844
+ if (activeId) highlight(activeId);
25845
+ }
25375
25846
 
25376
- ## Communication Style
25377
- - Technical but accessible \u2014 explain complex concepts clearly
25378
- - Evidence-based decision making with documented trade-offs
25379
- - Pragmatic about technical debt vs. delivery speed
25380
- - Focus on maintainability and long-term sustainability`,
25381
- focusAreas: [
25382
- "Technical architecture",
25383
- "Code quality",
25384
- "Technical decisions",
25385
- "Implementation guidance",
25386
- "Non-functional requirements",
25387
- "Epic creation and scoping",
25388
- "Task creation and breakdown",
25389
- "Sprint scoping and technical execution"
25390
- ],
25391
- documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
25392
- contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
25847
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
25848
+ window.addEventListener('resize', drawAndHighlight);
25849
+ container.addEventListener('scroll', drawAndHighlight);
25850
+ new ResizeObserver(drawAndHighlight).observe(container);
25851
+ })();
25852
+ </script>`;
25853
+ }
25854
+ var EVENT_ICONS = {
25855
+ "created": "\u{1F7E2}",
25856
+ "source-linked": "\u{1F535}",
25857
+ "child-spawned": "\u{1F7E1}",
25858
+ "assessment": "\u{1F7E3}",
25859
+ "jira-sync": "\u{1F537}"
25393
25860
  };
25861
+ function buildLineageTimeline(events) {
25862
+ if (events.length === 0) {
25863
+ return "";
25864
+ }
25865
+ const entries = events.map((event) => {
25866
+ const icon = EVENT_ICONS[event.type] ?? "\u26AA";
25867
+ const date5 = event.date ? formatDate(event.date) : "";
25868
+ const time3 = event.date?.slice(11, 16) ?? "";
25869
+ const label = linkArtifactIds(escapeHtml(event.label));
25870
+ return `
25871
+ <div class="lineage-entry lineage-${escapeHtml(event.type)}">
25872
+ <div class="lineage-marker">${icon}</div>
25873
+ <div class="lineage-content">
25874
+ <span class="lineage-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
25875
+ <span class="lineage-label">${label}</span>
25876
+ </div>
25877
+ </div>`;
25878
+ });
25879
+ return `
25880
+ <div class="lineage-timeline">
25881
+ <h3>Lineage</h3>
25882
+ ${entries.join("\n")}
25883
+ </div>`;
25884
+ }
25885
+
25886
+ // src/web/templates/pages/document-detail.ts
25887
+ function documentDetailPage(doc, store) {
25888
+ const fm = doc.frontmatter;
25889
+ const label = typeLabel(fm.type);
25890
+ const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
25891
+ const entries = Object.entries(fm).filter(
25892
+ ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
25893
+ );
25894
+ const arrayEntries = Object.entries(fm).filter(
25895
+ ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
25896
+ );
25897
+ const allEntries = [
25898
+ ...entries.filter(([, v]) => !Array.isArray(v)),
25899
+ ...arrayEntries
25900
+ ];
25901
+ const dtDd = allEntries.map(([key, value]) => {
25902
+ let rendered;
25903
+ if (key === "status") {
25904
+ rendered = statusBadge(value);
25905
+ } else if (key === "tags" && Array.isArray(value)) {
25906
+ rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
25907
+ } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
25908
+ rendered = formatDate(value);
25909
+ } else {
25910
+ rendered = linkArtifactIds(escapeHtml(String(value)));
25911
+ }
25912
+ return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
25913
+ }).join("\n ");
25914
+ const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
25915
+ const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
25916
+ const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
25917
+ return `
25918
+ <div class="breadcrumb">
25919
+ <a href="/">Overview</a><span class="sep">/</span>
25920
+ <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
25921
+ ${escapeHtml(fm.id)}
25922
+ </div>
25923
+
25924
+ <div class="page-header">
25925
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
25926
+ <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
25927
+ </div>
25928
+
25929
+ <div class="detail-meta">
25930
+ <dl>
25931
+ ${dtDd}
25932
+ </dl>
25933
+ </div>
25394
25934
 
25395
- // src/personas/registry.ts
25396
- var BUILTIN_PERSONAS = [
25397
- productOwner,
25398
- deliveryManager,
25399
- techLead
25400
- ];
25401
- function getPersona(idOrShortName) {
25402
- const key = idOrShortName.toLowerCase();
25403
- return BUILTIN_PERSONAS.find(
25404
- (p) => p.id === key || p.shortName === key
25405
- );
25935
+ ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
25936
+
25937
+ ${timelineHtml}
25938
+
25939
+ ${store ? renderRelationshipsAndLineage(store, fm.id) : ""}
25940
+ `;
25406
25941
  }
25407
- function listPersonas() {
25408
- return [...BUILTIN_PERSONAS];
25942
+ function renderRelationshipsAndLineage(store, docId) {
25943
+ const parts = [];
25944
+ const relationships = getArtifactRelationships(store, docId);
25945
+ if (relationships) {
25946
+ const graphHtml = buildArtifactRelationGraph(relationships);
25947
+ parts.push(collapsibleSection("rel-graph-" + docId, "Relationships", graphHtml));
25948
+ }
25949
+ const events = getArtifactLineageEvents(store, docId);
25950
+ if (events.length > 0) {
25951
+ const lineageHtml = buildLineageTimeline(events);
25952
+ parts.push(collapsibleSection("lineage-" + docId, "Lineage", lineageHtml, { defaultCollapsed: true }));
25953
+ }
25954
+ return parts.join("\n");
25409
25955
  }
25410
-
25411
- // src/web/templates/persona-switcher.ts
25412
- function renderPersonaSwitcher(current, _currentPath) {
25413
- const views = getAllPersonaViews();
25414
- if (views.length === 0) return "";
25415
- const options = views.map(
25416
- (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
25417
- ).join("\n ");
25956
+ function isValidAssessmentEntry(value) {
25957
+ if (typeof value !== "object" || value === null) return false;
25958
+ const obj = value;
25959
+ if (typeof obj.generatedAt !== "string") return false;
25960
+ if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
25961
+ return true;
25962
+ }
25963
+ function normalizeEntry(entry) {
25964
+ return {
25965
+ generatedAt: entry.generatedAt ?? "",
25966
+ commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
25967
+ commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
25968
+ signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
25969
+ childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
25970
+ childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
25971
+ childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
25972
+ linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0,
25973
+ blockerProgress: typeof entry.blockerProgress === "number" ? entry.blockerProgress : null,
25974
+ totalBlockers: typeof entry.totalBlockers === "number" ? entry.totalBlockers : 0,
25975
+ resolvedBlockers: typeof entry.resolvedBlockers === "number" ? entry.resolvedBlockers : 0
25976
+ };
25977
+ }
25978
+ function renderAssessmentTimeline(history) {
25979
+ const entries = history.map((raw, i) => {
25980
+ const entry = normalizeEntry(raw);
25981
+ const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
25982
+ const time3 = entry.generatedAt?.slice(11, 16) ?? "";
25983
+ const isLatest = i === 0;
25984
+ const parts = [];
25985
+ if (entry.commentSummary) {
25986
+ parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
25987
+ }
25988
+ if (entry.commentAnalysisProgress !== null) {
25989
+ parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
25990
+ }
25991
+ if (entry.childCount > 0) {
25992
+ const bar = progressBarHtml(entry.childRollupProgress ?? 0);
25993
+ parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
25994
+ }
25995
+ if (entry.totalBlockers > 0) {
25996
+ const bar = progressBarHtml(entry.blockerProgress ?? 0);
25997
+ parts.push(`<div class="assessment-stat">\u{1F6A7} Blockers: ${entry.resolvedBlockers}/${entry.totalBlockers} resolved ${bar} ${entry.blockerProgress ?? 0}%</div>`);
25998
+ }
25999
+ if (entry.linkedIssueCount > 0) {
26000
+ parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
26001
+ }
26002
+ if (entry.signals.length > 0) {
26003
+ const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
26004
+ parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
26005
+ }
26006
+ return `
26007
+ <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
26008
+ <div class="assessment-header">
26009
+ <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
26010
+ ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
26011
+ </div>
26012
+ ${parts.join("\n")}
26013
+ </div>`;
26014
+ });
25418
26015
  return `
25419
- <div class="persona-switcher">
25420
- <label class="persona-label" for="persona-select">View</label>
25421
- <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
25422
- ${options}
25423
- </select>
25424
- </div>
25425
- <script>
25426
- function switchPersona(value) {
25427
- if (value) {
25428
- window.location.href = '/' + value + '/dashboard';
25429
- }
25430
- }
25431
- </script>`;
26016
+ <div class="assessment-timeline">
26017
+ <h3>Assessment History</h3>
26018
+ ${entries.join("\n")}
26019
+ </div>`;
26020
+ }
26021
+ function progressBarHtml(pct) {
26022
+ const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
26023
+ const empty = 10 - filled;
26024
+ return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
25432
26025
  }
25433
26026
 
25434
- // src/web/templates/mermaid.ts
25435
- function sanitize(text, maxLen = 40) {
25436
- const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
25437
- return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
26027
+ // src/web/persona-views.ts
26028
+ var VIEWS = /* @__PURE__ */ new Map();
26029
+ var PAGE_RENDERERS = /* @__PURE__ */ new Map();
26030
+ function registerPersonaView(config2) {
26031
+ VIEWS.set(config2.shortName, config2);
25438
26032
  }
25439
- function mermaidBlock(definition, extraClass) {
25440
- const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
25441
- return `<div class="${cls}"><pre class="mermaid">
25442
- ${definition}
25443
- </pre></div>`;
26033
+ function registerPersonaPage(persona, pageId, renderer) {
26034
+ PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
25444
26035
  }
25445
- function placeholder(message) {
25446
- return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
26036
+ function getPersonaView(mode) {
26037
+ if (!mode) return void 0;
26038
+ return VIEWS.get(mode);
25447
26039
  }
25448
- function toMs(date5) {
25449
- return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
26040
+ function getPersonaPageRenderer(persona, pageId) {
26041
+ return PAGE_RENDERERS.get(`${persona}/${pageId}`);
25450
26042
  }
25451
- function fmtDate(ms) {
25452
- const d = new Date(ms);
25453
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
25454
- return `${months[d.getMonth()]} ${d.getDate()}`;
26043
+ function getAllPersonaViews() {
26044
+ return [...VIEWS.values()];
25455
26045
  }
25456
- function buildTimelineGantt(data, maxSprints = 6) {
25457
- const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
25458
- if (sprintsWithDates.length === 0) {
25459
- return placeholder("No timeline data available \u2014 sprints need start and end dates.");
25460
- }
25461
- const truncated = sprintsWithDates.length > maxSprints;
25462
- const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
25463
- const hiddenCount = sprintsWithDates.length - visibleSprints.length;
25464
- const epicMap = new Map(data.epics.map((e) => [e.id, e]));
25465
- const allStarts = visibleSprints.map((s) => toMs(s.startDate));
25466
- const allEnds = visibleSprints.map((s) => toMs(s.endDate));
25467
- const timelineStart = Math.min(...allStarts);
25468
- const timelineEnd = Math.max(...allEnds);
25469
- const span = timelineEnd - timelineStart || 1;
25470
- const pct = (ms) => (ms - timelineStart) / span * 100;
25471
- const DAY = 864e5;
25472
- const markers = [];
25473
- let tick = timelineStart;
25474
- const startDay = new Date(tick).getDay();
25475
- tick += (8 - startDay) % 7 * DAY;
25476
- while (tick <= timelineEnd) {
25477
- const left = pct(tick);
25478
- markers.push(
25479
- `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
25480
- );
25481
- tick += 7 * DAY;
25482
- }
25483
- const gridLines = [];
25484
- let gridTick = timelineStart;
25485
- const gridStartDay = new Date(gridTick).getDay();
25486
- gridTick += (8 - gridStartDay) % 7 * DAY;
25487
- while (gridTick <= timelineEnd) {
25488
- gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
25489
- gridTick += 7 * DAY;
25490
- }
25491
- const sprintBoundaries = /* @__PURE__ */ new Set();
25492
- for (const sprint of visibleSprints) {
25493
- sprintBoundaries.add(toMs(sprint.startDate));
25494
- sprintBoundaries.add(toMs(sprint.endDate));
25495
- }
25496
- const sprintLines = [...sprintBoundaries].map(
25497
- (ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
25498
- );
25499
- const now = Date.now();
25500
- let todayMarker = "";
25501
- if (now >= timelineStart && now <= timelineEnd) {
25502
- todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
25503
- }
25504
- const sprintBlocks = visibleSprints.map((sprint) => {
25505
- const sStart = toMs(sprint.startDate);
25506
- const sEnd = toMs(sprint.endDate);
25507
- const left = pct(sStart).toFixed(2);
25508
- const width = (pct(sEnd) - pct(sStart)).toFixed(2);
25509
- return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
25510
- }).join("");
25511
- const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
25512
- <div class="gantt-label gantt-section-label">Sprints</div>
25513
- <div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
25514
- </div>`;
25515
- const epicSpanMap = /* @__PURE__ */ new Map();
25516
- for (const sprint of visibleSprints) {
25517
- const sStart = toMs(sprint.startDate);
25518
- const sEnd = toMs(sprint.endDate);
25519
- for (const eid of sprint.linkedEpics) {
25520
- if (!epicMap.has(eid)) continue;
25521
- const existing = epicSpanMap.get(eid);
25522
- if (existing) {
25523
- existing.startMs = Math.min(existing.startMs, sStart);
25524
- existing.endMs = Math.max(existing.endMs, sEnd);
25525
- } else {
25526
- epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
25527
- }
25528
- }
25529
- }
25530
- const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
25531
- const aSpan = epicSpanMap.get(a);
25532
- const bSpan = epicSpanMap.get(b);
25533
- if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
25534
- return a.localeCompare(b);
25535
- });
25536
- const epicRows = sortedEpicIds.map((eid) => {
25537
- const epic = epicMap.get(eid);
25538
- const { startMs, endMs } = epicSpanMap.get(eid);
25539
- const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
25540
- const left = pct(startMs).toFixed(2);
25541
- const width = (pct(endMs) - pct(startMs)).toFixed(2);
25542
- const label = sanitize(epic.id + " " + epic.title);
25543
- return `<div class="gantt-row">
25544
- <div class="gantt-label">${label}</div>
25545
- <div class="gantt-track">
25546
- <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
25547
- </div>
25548
- </div>`;
25549
- }).join("\n");
25550
- const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
25551
- return `${note}
25552
- <div class="gantt">
25553
- <div class="gantt-chart">
25554
- <div class="gantt-header">
25555
- <div class="gantt-label"></div>
25556
- <div class="gantt-track gantt-dates">${markers.join("")}</div>
25557
- </div>
25558
- ${sprintBandRow}
25559
- ${epicRows}
25560
- </div>
25561
- <div class="gantt-overlay">
25562
- <div class="gantt-label"></div>
25563
- <div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
26046
+ var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
26047
+ function parsePersonaFromUrl(params) {
26048
+ const value = params.get("persona")?.toLowerCase();
26049
+ if (value && VALID_PERSONAS.has(value)) return value;
26050
+ return null;
26051
+ }
26052
+ function parsePersonaFromPath(pathname) {
26053
+ const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
26054
+ return match ? match[1] : null;
26055
+ }
26056
+ function resolvePersona(pathname, params) {
26057
+ return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
26058
+ }
26059
+ var SHARED_NAV_ITEMS = [
26060
+ { pageId: "timeline", label: "Timeline" },
26061
+ { pageId: "board", label: "Board" },
26062
+ { pageId: "upcoming", label: "Upcoming" },
26063
+ { pageId: "sprint-summary", label: "Sprint Summary" },
26064
+ { pageId: "gar", label: "GAR Report" },
26065
+ { pageId: "health", label: "Health" }
26066
+ ];
26067
+
26068
+ // src/web/templates/pages/persona-picker.ts
26069
+ function personaPickerPage() {
26070
+ const views = getAllPersonaViews();
26071
+ const cards = views.map(
26072
+ (v) => `
26073
+ <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
26074
+ <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
26075
+ <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
26076
+ </a>`
26077
+ ).join("\n");
26078
+ return `
26079
+ <div class="persona-picker">
26080
+ <h2>Choose Your View</h2>
26081
+ <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
26082
+ <div class="persona-picker-grid">
26083
+ ${cards}
25564
26084
  </div>
25565
26085
  </div>`;
25566
26086
  }
25567
- function statusClass(status) {
25568
- const s = status.toLowerCase();
25569
- if (s === "done" || s === "completed") return "flow-done";
25570
- if (s === "in-progress" || s === "active") return "flow-active";
25571
- if (s === "blocked") return "flow-blocked";
25572
- return "flow-default";
25573
- }
25574
- function buildArtifactFlowchart(data) {
25575
- if (data.features.length === 0 && data.epics.length === 0) {
25576
- return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
25577
- }
25578
- const edges = [];
25579
- const epicsByFeature = /* @__PURE__ */ new Map();
25580
- for (const epic of data.epics) {
25581
- for (const fid of epic.linkedFeature) {
25582
- if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
25583
- epicsByFeature.get(fid).push(epic.id);
25584
- edges.push({ from: fid, to: epic.id });
26087
+
26088
+ // src/reports/sprint-summary/risk-assessment.ts
26089
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
26090
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
26091
+
26092
+ IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
26093
+
26094
+ Produce a concise markdown assessment with these sections:
26095
+
26096
+ ## Status Assessment
26097
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
26098
+
26099
+ ## Related Activity
26100
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
26101
+
26102
+ ## Trajectory
26103
+ Based on the data (status of related items, time remaining, ownership), is this risk trending toward resolution or toward becoming a blocker? Explain your reasoning with concrete evidence.
26104
+
26105
+ ## Recommendation
26106
+ One concrete next step to move this risk toward resolution.
26107
+
26108
+ Rules:
26109
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
26110
+ - Keep the tone professional and direct
26111
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
26112
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
26113
+ - Produce the full assessment text directly`;
26114
+ async function generateRiskAssessment(data, riskId, store) {
26115
+ const risk = data.risks.find((r) => r.id === riskId);
26116
+ if (!risk) return "Risk not found in sprint data.";
26117
+ const prompt = buildSingleRiskPrompt(data, risk, store);
26118
+ const result = query3({
26119
+ prompt,
26120
+ options: {
26121
+ systemPrompt: SYSTEM_PROMPT2,
26122
+ maxTurns: 1,
26123
+ tools: [],
26124
+ allowedTools: []
25585
26125
  }
25586
- }
25587
- const sprintsByEpic = /* @__PURE__ */ new Map();
25588
- for (const sprint of data.sprints) {
25589
- for (const eid of sprint.linkedEpics) {
25590
- if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
25591
- sprintsByEpic.get(eid).push(sprint.id);
25592
- edges.push({ from: eid, to: sprint.id });
26126
+ });
26127
+ for await (const msg of result) {
26128
+ if (msg.type === "assistant") {
26129
+ const text = msg.message.content.find(
26130
+ (b) => b.type === "text"
26131
+ );
26132
+ if (text) return text.text;
25593
26133
  }
25594
26134
  }
25595
- const connectedFeatureIds = new Set(epicsByFeature.keys());
25596
- const connectedEpicIds = /* @__PURE__ */ new Set();
25597
- for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
25598
- for (const ids of sprintsByEpic.values()) ids.forEach(() => {
25599
- });
25600
- for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
25601
- const connectedSprintIds = /* @__PURE__ */ new Set();
25602
- for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
25603
- const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
25604
- const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
25605
- const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
25606
- if (features.length === 0 && epics.length === 0) {
25607
- return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
26135
+ return "Unable to generate risk assessment.";
26136
+ }
26137
+ function buildSingleRiskPrompt(data, risk, store) {
26138
+ const sections = [];
26139
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
26140
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
26141
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
26142
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
26143
+ sections.push("");
26144
+ const doc = store.get(risk.id);
26145
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
26146
+ sections.push(`Type: ${risk.type}`);
26147
+ if (doc) {
26148
+ sections.push(`Status: ${doc.frontmatter.status}`);
26149
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
26150
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
26151
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
26152
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
26153
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
26154
+ const tags = doc.frontmatter.tags ?? [];
26155
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
26156
+ if (doc.content.trim()) {
26157
+ sections.push(`
26158
+ Description:
26159
+ ${doc.content.trim()}`);
26160
+ }
26161
+ const allDocs = store.list();
26162
+ const relatedIds = /* @__PURE__ */ new Set();
26163
+ for (const d of allDocs) {
26164
+ if (d.frontmatter.aboutArtifact === risk.id) {
26165
+ relatedIds.add(d.frontmatter.id);
26166
+ }
26167
+ }
26168
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
26169
+ let match;
26170
+ while ((match = idPattern.exec(doc.content)) !== null) {
26171
+ relatedIds.add(match[1]);
26172
+ }
26173
+ const significantTags = tags.filter(
26174
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
26175
+ );
26176
+ if (significantTags.length > 0) {
26177
+ for (const d of allDocs) {
26178
+ if (d.frontmatter.id === risk.id) continue;
26179
+ const dTags = d.frontmatter.tags ?? [];
26180
+ if (significantTags.some((t) => dTags.includes(t))) {
26181
+ relatedIds.add(d.frontmatter.id);
26182
+ }
26183
+ }
26184
+ }
26185
+ const about = doc.frontmatter.aboutArtifact;
26186
+ if (about) {
26187
+ relatedIds.add(about);
26188
+ for (const d of allDocs) {
26189
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
26190
+ relatedIds.add(d.frontmatter.id);
26191
+ }
26192
+ }
26193
+ }
26194
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
26195
+ if (relatedDocs.length > 0) {
26196
+ sections.push(`
26197
+ ## Related Documents (${relatedDocs.length})`);
26198
+ for (const rd of relatedDocs) {
26199
+ const owner = rd.frontmatter.owner ?? "unowned";
26200
+ const summary = rd.content.trim().slice(0, 300);
26201
+ sections.push(
26202
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
26203
+ );
26204
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
26205
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
26206
+ }
26207
+ }
25608
26208
  }
25609
- const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
25610
- <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
25611
- <span class="flow-node-title">${sanitize(title, 35)}</span>
25612
- </div>`;
25613
- const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
25614
- const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
25615
- const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
25616
- const edgesJson = JSON.stringify(edges);
25617
- return `
25618
- <div class="flow-diagram" id="flow-diagram">
25619
- <svg class="flow-lines" id="flow-lines"></svg>
25620
- <div class="flow-columns">
25621
- <div class="flow-column">
25622
- <div class="flow-column-header">Features</div>
25623
- ${featuresHtml}
25624
- </div>
25625
- <div class="flow-column">
25626
- <div class="flow-column-header">Epics</div>
25627
- ${epicsHtml}
25628
- </div>
25629
- <div class="flow-column">
25630
- <div class="flow-column-header">Sprints</div>
25631
- ${sprintsHtml}
25632
- </div>
25633
- </div>
25634
- </div>
25635
- <script>
25636
- (function() {
25637
- var edges = ${edgesJson};
25638
- var container = document.getElementById('flow-diagram');
25639
- var svg = document.getElementById('flow-lines');
25640
- if (!container || !svg) return;
25641
-
25642
- // Build directed adjacency maps for traversal
25643
- var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
25644
- var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
25645
- edges.forEach(function(e) {
25646
- if (!fwd[e.from]) fwd[e.from] = [];
25647
- if (!bwd[e.to]) bwd[e.to] = [];
25648
- fwd[e.from].push(e.to);
25649
- bwd[e.to].push(e.from);
25650
- });
25651
-
25652
- function drawLines() {
25653
- var rect = container.getBoundingClientRect();
25654
- var scrollW = container.scrollWidth;
25655
- var scrollH = container.scrollHeight;
25656
- svg.setAttribute('width', scrollW);
25657
- svg.setAttribute('height', scrollH);
25658
- svg.innerHTML = '';
26209
+ sections.push("");
26210
+ sections.push(`---`);
26211
+ sections.push(`
26212
+ Generate the risk assessment for ${risk.id} based on the data above.`);
26213
+ return sections.join("\n");
26214
+ }
25659
26215
 
25660
- // Use scroll offsets so lines align with scrolled content
25661
- var scrollLeft = container.scrollLeft;
25662
- var scrollTop = container.scrollTop;
26216
+ // src/personas/builtin/product-owner.ts
26217
+ var productOwner = {
26218
+ id: "product-owner",
26219
+ name: "Product Owner",
26220
+ shortName: "po",
26221
+ description: "Focuses on product vision, stakeholder needs, backlog prioritization, and value delivery.",
26222
+ systemPrompt: `You are Marvin, acting as a **Product Owner**. Your role is to help the team maximize the value delivered by the product.
25663
26223
 
25664
- edges.forEach(function(edge) {
25665
- var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
25666
- var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
25667
- if (!fromEl || !toEl) return;
26224
+ ## Core Responsibilities
26225
+ - Define and communicate the product vision and strategy
26226
+ - Manage and prioritize the product backlog
26227
+ - Ensure stakeholder needs are understood and addressed
26228
+ - Make decisions about scope, priority, and trade-offs
26229
+ - Accept or reject work results based on acceptance criteria
25668
26230
 
25669
- var fr = fromEl.getBoundingClientRect();
25670
- var tr = toEl.getBoundingClientRect();
25671
- var x1 = fr.right - rect.left + scrollLeft;
25672
- var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
25673
- var x2 = tr.left - rect.left + scrollLeft;
25674
- var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
25675
- var mx = (x1 + x2) / 2;
26231
+ ## How You Work
26232
+ - Ask clarifying questions to understand business value and user needs
26233
+ - Create and refine decisions (D-xxx) for important product choices
26234
+ - Track questions (Q-xxx) that need stakeholder input
26235
+ - Define acceptance criteria for features and deliverables
26236
+ - Prioritize actions (A-xxx) based on business value
25676
26237
 
25677
- var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
25678
- path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
25679
- path.setAttribute('fill', 'none');
25680
- path.setAttribute('stroke', '#2a2e3a');
25681
- path.setAttribute('stroke-width', '1.5');
25682
- path.dataset.from = edge.from;
25683
- path.dataset.to = edge.to;
25684
- svg.appendChild(path);
25685
- });
25686
- }
26238
+ ## Communication Style
26239
+ - Business-oriented language, avoid unnecessary technical jargon
26240
+ - Focus on outcomes and value, not implementation details
26241
+ - Be decisive but transparent about trade-offs
26242
+ - Challenge assumptions that don't align with product goals`,
26243
+ focusAreas: [
26244
+ "Product vision and strategy",
26245
+ "Backlog management",
26246
+ "Stakeholder communication",
26247
+ "Value delivery",
26248
+ "Acceptance criteria",
26249
+ "Feature definition and prioritization"
26250
+ ],
26251
+ documentTypes: ["decision", "question", "action", "feature"],
26252
+ contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
26253
+ };
25687
26254
 
25688
- // Find directly related nodes via directed traversal
25689
- // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
25690
- // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
25691
- function findConnected(startId) {
25692
- var visited = {};
25693
- visited[startId] = true;
25694
- // Traverse forward (from\u2192to direction)
25695
- var queue = [startId];
25696
- while (queue.length) {
25697
- var id = queue.shift();
25698
- (fwd[id] || []).forEach(function(neighbor) {
25699
- if (!visited[neighbor]) {
25700
- visited[neighbor] = true;
25701
- queue.push(neighbor);
25702
- }
25703
- });
25704
- }
25705
- // Traverse backward (to\u2192from direction)
25706
- queue = [startId];
25707
- while (queue.length) {
25708
- var id = queue.shift();
25709
- (bwd[id] || []).forEach(function(neighbor) {
25710
- if (!visited[neighbor]) {
25711
- visited[neighbor] = true;
25712
- queue.push(neighbor);
25713
- }
25714
- });
25715
- }
25716
- return visited;
25717
- }
26255
+ // src/personas/builtin/delivery-manager.ts
26256
+ var deliveryManager = {
26257
+ id: "delivery-manager",
26258
+ name: "Delivery Manager",
26259
+ shortName: "dm",
26260
+ description: "Focuses on project delivery, risk management, team coordination, and process governance.",
26261
+ systemPrompt: `You are Marvin, acting as a **Delivery Manager**. Your role is to ensure the project is delivered on time, within scope, and with managed risks.
25718
26262
 
25719
- function highlight(hoveredId) {
25720
- var connected = findConnected(hoveredId);
25721
- container.querySelectorAll('.flow-node').forEach(function(n) {
25722
- if (connected[n.dataset.flowId]) {
25723
- n.classList.add('flow-lit');
25724
- n.classList.remove('flow-dim');
25725
- } else {
25726
- n.classList.add('flow-dim');
25727
- n.classList.remove('flow-lit');
25728
- }
25729
- });
25730
- svg.querySelectorAll('path').forEach(function(p) {
25731
- if (connected[p.dataset.from] && connected[p.dataset.to]) {
25732
- p.classList.add('flow-line-lit');
25733
- p.classList.remove('flow-line-dim');
25734
- } else {
25735
- p.classList.add('flow-line-dim');
25736
- p.classList.remove('flow-line-lit');
25737
- }
25738
- });
25739
- }
26263
+ ## Core Responsibilities
26264
+ - Track project progress and identify blockers
26265
+ - Manage risks, issues, and dependencies
26266
+ - Coordinate between team members and stakeholders
26267
+ - Ensure governance processes are followed (decisions logged, actions tracked)
26268
+ - Facilitate meetings and ensure outcomes are captured
25740
26269
 
25741
- function clearHighlight() {
25742
- container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
25743
- svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
25744
- }
26270
+ ## How You Work
26271
+ - Review open actions (A-xxx) and follow up on overdue items
26272
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
26273
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
26274
+ - Ensure decisions (D-xxx) are properly documented with rationale
26275
+ - Track questions (Q-xxx) and ensure they get answered
26276
+ - Monitor project health and flag risks early
26277
+ - Create meeting notes and ensure action items are assigned
25745
26278
 
25746
- var activeId = null;
25747
- container.addEventListener('click', function(e) {
25748
- // Let the ID link navigate normally
25749
- if (e.target.closest('a')) return;
26279
+ ## Communication Style
26280
+ - Process-oriented but pragmatic
26281
+ - Focus on status, risks, and blockers
26282
+ - Be proactive about follow-ups and deadlines
26283
+ - Keep stakeholders informed with concise updates`,
26284
+ focusAreas: [
26285
+ "Project delivery",
26286
+ "Risk management",
26287
+ "Team coordination",
26288
+ "Process governance",
26289
+ "Status tracking",
26290
+ "Epic scheduling and tracking",
26291
+ "Sprint planning and tracking"
26292
+ ],
26293
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
26294
+ contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
26295
+ };
25750
26296
 
25751
- var node = e.target.closest('.flow-node');
25752
- var clickedId = node ? node.dataset.flowId : null;
26297
+ // src/personas/builtin/tech-lead.ts
26298
+ var techLead = {
26299
+ id: "tech-lead",
26300
+ name: "Technical Lead",
26301
+ shortName: "tl",
26302
+ description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
26303
+ systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
25753
26304
 
25754
- if (!clickedId || clickedId === activeId) {
25755
- activeId = null;
25756
- clearHighlight();
25757
- return;
25758
- }
26305
+ ## Core Responsibilities
26306
+ - Define and maintain technical architecture
26307
+ - Make and document technical decisions with clear rationale
26308
+ - Review technical approaches and identify potential issues
26309
+ - Guide the team on best practices and patterns
26310
+ - Evaluate technical risks and propose mitigations
25759
26311
 
25760
- activeId = clickedId;
25761
- highlight(clickedId);
25762
- });
26312
+ ## How You Work
26313
+ - Create decisions (D-xxx) for significant technical choices (framework, architecture, patterns)
26314
+ - Document technical questions (Q-xxx) that need investigation or proof-of-concept
26315
+ - Define technical actions (A-xxx) for implementation tasks
26316
+ - Consider non-functional requirements (performance, security, maintainability)
26317
+ - Provide clear technical guidance with examples when helpful
25763
26318
 
25764
- function drawAndHighlight() {
25765
- drawLines();
25766
- if (activeId) highlight(activeId);
25767
- }
26319
+ ## Communication Style
26320
+ - Technical but accessible \u2014 explain complex concepts clearly
26321
+ - Evidence-based decision making with documented trade-offs
26322
+ - Pragmatic about technical debt vs. delivery speed
26323
+ - Focus on maintainability and long-term sustainability`,
26324
+ focusAreas: [
26325
+ "Technical architecture",
26326
+ "Code quality",
26327
+ "Technical decisions",
26328
+ "Implementation guidance",
26329
+ "Non-functional requirements",
26330
+ "Epic creation and scoping",
26331
+ "Task creation and breakdown",
26332
+ "Sprint scoping and technical execution"
26333
+ ],
26334
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
26335
+ contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
26336
+ };
25768
26337
 
25769
- requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
25770
- window.addEventListener('resize', drawAndHighlight);
25771
- container.addEventListener('scroll', drawAndHighlight);
25772
- new ResizeObserver(drawAndHighlight).observe(container);
25773
- })();
25774
- </script>`;
26338
+ // src/personas/registry.ts
26339
+ var BUILTIN_PERSONAS = [
26340
+ productOwner,
26341
+ deliveryManager,
26342
+ techLead
26343
+ ];
26344
+ function getPersona(idOrShortName) {
26345
+ const key = idOrShortName.toLowerCase();
26346
+ return BUILTIN_PERSONAS.find(
26347
+ (p) => p.id === key || p.shortName === key
26348
+ );
25775
26349
  }
25776
- function buildStatusPie(title, counts) {
25777
- const entries = Object.entries(counts).filter(([, v]) => v > 0);
25778
- if (entries.length === 0) {
25779
- return placeholder(`No data for ${title}.`);
25780
- }
25781
- const lines = [`pie title ${sanitize(title, 60)}`];
25782
- for (const [label, count] of entries) {
25783
- lines.push(` "${sanitize(label, 30)}" : ${count}`);
25784
- }
25785
- return mermaidBlock(lines.join("\n"));
26350
+ function listPersonas() {
26351
+ return [...BUILTIN_PERSONAS];
25786
26352
  }
25787
- function buildHealthGauge(categories) {
25788
- const valid = categories.filter((c) => c.total > 0);
25789
- if (valid.length === 0) {
25790
- return placeholder("No completeness data available.");
25791
- }
25792
- const pies = valid.map((cat) => {
25793
- const incomplete = cat.total - cat.complete;
25794
- const lines = [
25795
- `pie title ${sanitize(cat.name, 30)}`,
25796
- ` "Complete" : ${cat.complete}`,
25797
- ` "Incomplete" : ${incomplete}`
25798
- ];
25799
- return mermaidBlock(lines.join("\n"));
25800
- });
25801
- return `<div class="mermaid-row">${pies.join("\n")}</div>`;
26353
+
26354
+ // src/web/templates/persona-switcher.ts
26355
+ function renderPersonaSwitcher(current, _currentPath) {
26356
+ const views = getAllPersonaViews();
26357
+ if (views.length === 0) return "";
26358
+ const options = views.map(
26359
+ (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
26360
+ ).join("\n ");
26361
+ return `
26362
+ <div class="persona-switcher">
26363
+ <label class="persona-label" for="persona-select">View</label>
26364
+ <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
26365
+ ${options}
26366
+ </select>
26367
+ </div>
26368
+ <script>
26369
+ function switchPersona(value) {
26370
+ if (value) {
26371
+ window.location.href = '/' + value + '/dashboard';
26372
+ }
26373
+ }
26374
+ </script>`;
25802
26375
  }
25803
26376
 
25804
26377
  // src/web/templates/pages/po/dashboard.ts
@@ -28933,7 +29506,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
28933
29506
  notFound(res, projectName, navGroups, pathname, persona, pOpts);
28934
29507
  return;
28935
29508
  }
28936
- const body = documentDetailPage(doc);
29509
+ const body = documentDetailPage(doc, store);
28937
29510
  respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, persona, ...pOpts }, body));
28938
29511
  return;
28939
29512
  }