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.
package/dist/index.js CHANGED
@@ -16282,6 +16282,221 @@ function getUpcomingData(store) {
16282
16282
  function getSprintSummaryData(store, sprintId) {
16283
16283
  return collectSprintSummaryData(store, sprintId);
16284
16284
  }
16285
+ var SIBLING_CAP = 8;
16286
+ var ARTIFACT_ID_PATTERN = /\b([A-Z]{1,3}-\d{3,})\b/g;
16287
+ function getArtifactRelationships(store, docId) {
16288
+ const doc = store.get(docId);
16289
+ if (!doc) return null;
16290
+ const fm = doc.frontmatter;
16291
+ const allDocs = store.list();
16292
+ const docIndex = new Map(allDocs.map((d) => [d.frontmatter.id, d]));
16293
+ const origins = [];
16294
+ const parents = [];
16295
+ const children = [];
16296
+ const external = [];
16297
+ const edges = [];
16298
+ const seen = /* @__PURE__ */ new Set([docId]);
16299
+ const addIfExists = (id, relationship, bucket) => {
16300
+ if (seen.has(id)) return false;
16301
+ const target = docIndex.get(id);
16302
+ if (!target) return false;
16303
+ seen.add(id);
16304
+ bucket.push({
16305
+ id: target.frontmatter.id,
16306
+ title: target.frontmatter.title,
16307
+ type: target.frontmatter.type,
16308
+ status: target.frontmatter.status,
16309
+ relationship
16310
+ });
16311
+ return true;
16312
+ };
16313
+ const parentId = fm.aboutArtifact;
16314
+ if (parentId && addIfExists(parentId, "parent", parents)) {
16315
+ edges.push({ from: parentId, to: docId });
16316
+ }
16317
+ const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
16318
+ for (const epicId of linkedEpics) {
16319
+ if (addIfExists(epicId, "epic", parents)) {
16320
+ edges.push({ from: epicId, to: docId });
16321
+ }
16322
+ const epicDoc = docIndex.get(epicId);
16323
+ if (epicDoc) {
16324
+ const features = normalizeLinkedFeatures(epicDoc.frontmatter.linkedFeature);
16325
+ for (const fid of features) {
16326
+ if (addIfExists(fid, "feature", parents)) {
16327
+ edges.push({ from: fid, to: epicId });
16328
+ }
16329
+ }
16330
+ }
16331
+ }
16332
+ const tags = fm.tags ?? [];
16333
+ for (const tag of tags) {
16334
+ if (tag.startsWith("sprint:")) {
16335
+ const sprintId = tag.slice(7);
16336
+ if (addIfExists(sprintId, "sprint", parents)) {
16337
+ edges.push({ from: sprintId, to: docId });
16338
+ }
16339
+ }
16340
+ }
16341
+ for (const tag of tags) {
16342
+ if (tag.startsWith("source:")) {
16343
+ const sourceId = tag.slice(7);
16344
+ if (addIfExists(sourceId, "source", origins)) {
16345
+ edges.push({ from: sourceId, to: docId });
16346
+ }
16347
+ }
16348
+ }
16349
+ const sourceField = fm.source;
16350
+ if (sourceField && /^[A-Z]{1,3}-\d{3,}$/.test(sourceField)) {
16351
+ if (addIfExists(sourceField, "source", origins)) {
16352
+ edges.push({ from: sourceField, to: docId });
16353
+ }
16354
+ }
16355
+ for (const d of allDocs) {
16356
+ if (d.frontmatter.aboutArtifact === docId) {
16357
+ if (addIfExists(d.frontmatter.id, "child", children)) {
16358
+ edges.push({ from: docId, to: d.frontmatter.id });
16359
+ }
16360
+ }
16361
+ }
16362
+ if (fm.type === "epic") {
16363
+ const epicTag = `epic:${docId}`;
16364
+ for (const d of allDocs) {
16365
+ const dfm = d.frontmatter;
16366
+ const dLinkedEpics = normalizeLinkedEpics(dfm.linkedEpic);
16367
+ const dTags = dfm.tags ?? [];
16368
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
16369
+ if (addIfExists(dfm.id, "child", children)) {
16370
+ edges.push({ from: docId, to: dfm.id });
16371
+ }
16372
+ }
16373
+ }
16374
+ }
16375
+ if (parentId) {
16376
+ let siblingCount = 0;
16377
+ for (const d of allDocs) {
16378
+ if (siblingCount >= SIBLING_CAP) break;
16379
+ if (d.frontmatter.aboutArtifact === parentId && d.frontmatter.id !== docId) {
16380
+ if (addIfExists(d.frontmatter.id, "sibling", children)) {
16381
+ edges.push({ from: parentId, to: d.frontmatter.id });
16382
+ siblingCount++;
16383
+ }
16384
+ }
16385
+ }
16386
+ }
16387
+ const jiraKey = fm.jiraKey;
16388
+ const jiraUrl = fm.jiraUrl;
16389
+ if (jiraKey) {
16390
+ external.push({
16391
+ id: jiraKey,
16392
+ title: jiraUrl ?? `Jira: ${jiraKey}`,
16393
+ type: "jira",
16394
+ status: "",
16395
+ relationship: "jira"
16396
+ });
16397
+ edges.push({ from: docId, to: jiraKey });
16398
+ }
16399
+ if (doc.content) {
16400
+ const matches = doc.content.matchAll(ARTIFACT_ID_PATTERN);
16401
+ for (const m of matches) {
16402
+ const refId = m[1];
16403
+ if (refId !== docId && docIndex.has(refId)) {
16404
+ if (addIfExists(refId, "mentioned", external)) {
16405
+ edges.push({ from: docId, to: refId });
16406
+ }
16407
+ }
16408
+ }
16409
+ }
16410
+ return {
16411
+ origins,
16412
+ parents,
16413
+ self: {
16414
+ id: fm.id,
16415
+ title: fm.title,
16416
+ type: fm.type,
16417
+ status: fm.status,
16418
+ relationship: "self"
16419
+ },
16420
+ children,
16421
+ external,
16422
+ edges
16423
+ };
16424
+ }
16425
+ function getArtifactLineageEvents(store, docId) {
16426
+ const doc = store.get(docId);
16427
+ if (!doc) return [];
16428
+ const fm = doc.frontmatter;
16429
+ const events = [];
16430
+ if (fm.created) {
16431
+ events.push({
16432
+ date: fm.created,
16433
+ type: "created",
16434
+ label: `${fm.id} created`
16435
+ });
16436
+ }
16437
+ const tags = fm.tags ?? [];
16438
+ for (const tag of tags) {
16439
+ if (tag.startsWith("source:")) {
16440
+ const sourceId = tag.slice(7);
16441
+ const sourceDoc = store.get(sourceId);
16442
+ if (sourceDoc) {
16443
+ events.push({
16444
+ date: sourceDoc.frontmatter.created,
16445
+ type: "source-linked",
16446
+ label: `Originated from ${sourceId} \u2014 ${sourceDoc.frontmatter.title}`,
16447
+ relatedId: sourceId
16448
+ });
16449
+ }
16450
+ }
16451
+ }
16452
+ const allDocs = store.list();
16453
+ for (const d of allDocs) {
16454
+ if (d.frontmatter.aboutArtifact === docId) {
16455
+ events.push({
16456
+ date: d.frontmatter.created,
16457
+ type: "child-spawned",
16458
+ label: `Spawned ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
16459
+ relatedId: d.frontmatter.id
16460
+ });
16461
+ }
16462
+ }
16463
+ if (fm.type === "epic") {
16464
+ const epicTag = `epic:${docId}`;
16465
+ for (const d of allDocs) {
16466
+ if (d.frontmatter.aboutArtifact === docId) continue;
16467
+ const dLinkedEpics = normalizeLinkedEpics(d.frontmatter.linkedEpic);
16468
+ const dTags = d.frontmatter.tags ?? [];
16469
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
16470
+ events.push({
16471
+ date: d.frontmatter.created,
16472
+ type: "child-spawned",
16473
+ label: `Linked ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
16474
+ relatedId: d.frontmatter.id
16475
+ });
16476
+ }
16477
+ }
16478
+ }
16479
+ const history = fm.assessmentHistory ?? [];
16480
+ for (const entry of history) {
16481
+ if (entry.generatedAt) {
16482
+ events.push({
16483
+ date: entry.generatedAt,
16484
+ type: "assessment",
16485
+ label: "Assessment performed"
16486
+ });
16487
+ }
16488
+ }
16489
+ const lastSync = fm.lastJiraSyncAt;
16490
+ if (lastSync) {
16491
+ events.push({
16492
+ date: lastSync,
16493
+ type: "jira-sync",
16494
+ label: `Synced with Jira ${fm.jiraKey ?? ""}`
16495
+ });
16496
+ }
16497
+ events.sort((a, b) => (b.date ?? "").localeCompare(a.date ?? ""));
16498
+ return events;
16499
+ }
16285
16500
 
16286
16501
  // src/plugins/builtin/tools/meetings.ts
16287
16502
  import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
@@ -19248,6 +19463,65 @@ a.artifact-link:hover {
19248
19463
  .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
19249
19464
  .flow-line-dim { opacity: 0.08; }
19250
19465
 
19466
+ /* Relationship graph: self-node emphasis */
19467
+ .flow-self {
19468
+ border-left-width: 4px;
19469
+ background: var(--bg-hover);
19470
+ box-shadow: 0 0 0 1px var(--accent-dim);
19471
+ }
19472
+ .flow-self .flow-node-id {
19473
+ color: var(--accent);
19474
+ font-weight: 600;
19475
+ }
19476
+
19477
+ /* Relationship graph: external nodes */
19478
+ .flow-external {
19479
+ border-left-color: var(--text-dim);
19480
+ border-left-style: dashed;
19481
+ }
19482
+
19483
+ /* Relationship graph: empty state */
19484
+ .flow-empty {
19485
+ padding: 2rem;
19486
+ text-align: center;
19487
+ color: var(--text-dim);
19488
+ font-size: 0.85rem;
19489
+ }
19490
+
19491
+ /* Lineage timeline */
19492
+ .lineage-timeline {
19493
+ margin-top: 1.5rem;
19494
+ }
19495
+ .lineage-timeline h3 {
19496
+ font-size: 1rem;
19497
+ font-weight: 600;
19498
+ margin-bottom: 0.75rem;
19499
+ }
19500
+ .lineage-entry {
19501
+ display: flex;
19502
+ gap: 0.5rem;
19503
+ padding: 0.4rem 0;
19504
+ padding-left: 0.25rem;
19505
+ }
19506
+ .lineage-marker {
19507
+ flex-shrink: 0;
19508
+ font-size: 0.7rem;
19509
+ line-height: 1.4rem;
19510
+ }
19511
+ .lineage-content {
19512
+ display: flex;
19513
+ flex-direction: column;
19514
+ gap: 0.1rem;
19515
+ }
19516
+ .lineage-date {
19517
+ font-size: 0.7rem;
19518
+ color: var(--text-dim);
19519
+ font-family: var(--mono);
19520
+ }
19521
+ .lineage-label {
19522
+ font-size: 0.85rem;
19523
+ }
19524
+
19251
19525
  /* Gantt truncation note */
19252
19526
  .mermaid-note {
19253
19527
  font-size: 0.75rem;
@@ -20174,357 +20448,27 @@ function documentsPage(data) {
20174
20448
  `;
20175
20449
  }
20176
20450
 
20177
- // src/web/templates/pages/document-detail.ts
20178
- function documentDetailPage(doc) {
20179
- const fm = doc.frontmatter;
20180
- const label = typeLabel(fm.type);
20181
- const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
20182
- const entries = Object.entries(fm).filter(
20183
- ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
20184
- );
20185
- const arrayEntries = Object.entries(fm).filter(
20186
- ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
20187
- );
20188
- const allEntries = [
20189
- ...entries.filter(([, v]) => !Array.isArray(v)),
20190
- ...arrayEntries
20191
- ];
20192
- const dtDd = allEntries.map(([key, value]) => {
20193
- let rendered;
20194
- if (key === "status") {
20195
- rendered = statusBadge(value);
20196
- } else if (key === "tags" && Array.isArray(value)) {
20197
- rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
20198
- } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
20199
- rendered = formatDate(value);
20200
- } else {
20201
- rendered = linkArtifactIds(escapeHtml(String(value)));
20202
- }
20203
- return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
20204
- }).join("\n ");
20205
- const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
20206
- const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
20207
- const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
20208
- return `
20209
- <div class="breadcrumb">
20210
- <a href="/">Overview</a><span class="sep">/</span>
20211
- <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
20212
- ${escapeHtml(fm.id)}
20213
- </div>
20214
-
20215
- <div class="page-header">
20216
- <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
20217
- <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
20218
- </div>
20219
-
20220
- <div class="detail-meta">
20221
- <dl>
20222
- ${dtDd}
20223
- </dl>
20224
- </div>
20225
-
20226
- ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
20227
-
20228
- ${timelineHtml}
20229
- `;
20230
- }
20231
- function isValidAssessmentEntry(value) {
20232
- if (typeof value !== "object" || value === null) return false;
20233
- const obj = value;
20234
- if (typeof obj.generatedAt !== "string") return false;
20235
- if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
20236
- return true;
20237
- }
20238
- function normalizeEntry(entry) {
20239
- return {
20240
- generatedAt: entry.generatedAt ?? "",
20241
- commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
20242
- commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
20243
- signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
20244
- childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
20245
- childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
20246
- childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
20247
- linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
20248
- };
20249
- }
20250
- function renderAssessmentTimeline(history) {
20251
- const entries = history.map((raw, i) => {
20252
- const entry = normalizeEntry(raw);
20253
- const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
20254
- const time3 = entry.generatedAt?.slice(11, 16) ?? "";
20255
- const isLatest = i === 0;
20256
- const parts = [];
20257
- if (entry.commentSummary) {
20258
- parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
20259
- }
20260
- if (entry.commentAnalysisProgress !== null) {
20261
- parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
20262
- }
20263
- if (entry.childCount > 0) {
20264
- const bar = progressBarHtml(entry.childRollupProgress ?? 0);
20265
- parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
20266
- }
20267
- if (entry.linkedIssueCount > 0) {
20268
- parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
20269
- }
20270
- if (entry.signals.length > 0) {
20271
- const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
20272
- parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
20273
- }
20274
- return `
20275
- <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
20276
- <div class="assessment-header">
20277
- <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
20278
- ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
20279
- </div>
20280
- ${parts.join("\n")}
20281
- </div>`;
20282
- });
20283
- return `
20284
- <div class="assessment-timeline">
20285
- <h3>Assessment History</h3>
20286
- ${entries.join("\n")}
20287
- </div>`;
20288
- }
20289
- function progressBarHtml(pct) {
20290
- const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
20291
- const empty = 10 - filled;
20292
- return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
20451
+ // src/web/templates/mermaid.ts
20452
+ function sanitize(text, maxLen = 40) {
20453
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
20454
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
20293
20455
  }
20294
-
20295
- // src/web/persona-views.ts
20296
- var VIEWS = /* @__PURE__ */ new Map();
20297
- var PAGE_RENDERERS = /* @__PURE__ */ new Map();
20298
- function registerPersonaView(config2) {
20299
- VIEWS.set(config2.shortName, config2);
20456
+ function mermaidBlock(definition, extraClass) {
20457
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
20458
+ return `<div class="${cls}"><pre class="mermaid">
20459
+ ${definition}
20460
+ </pre></div>`;
20300
20461
  }
20301
- function registerPersonaPage(persona, pageId, renderer) {
20302
- PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
20462
+ function placeholder(message) {
20463
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
20303
20464
  }
20304
- function getPersonaView(mode) {
20305
- if (!mode) return void 0;
20306
- return VIEWS.get(mode);
20465
+ function toMs(date5) {
20466
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
20307
20467
  }
20308
- function getPersonaPageRenderer(persona, pageId) {
20309
- return PAGE_RENDERERS.get(`${persona}/${pageId}`);
20310
- }
20311
- function getAllPersonaViews() {
20312
- return [...VIEWS.values()];
20313
- }
20314
- var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
20315
- function parsePersonaFromUrl(params) {
20316
- const value = params.get("persona")?.toLowerCase();
20317
- if (value && VALID_PERSONAS.has(value)) return value;
20318
- return null;
20319
- }
20320
- function parsePersonaFromPath(pathname) {
20321
- const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
20322
- return match ? match[1] : null;
20323
- }
20324
- function resolvePersona(pathname, params) {
20325
- return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
20326
- }
20327
- var SHARED_NAV_ITEMS = [
20328
- { pageId: "timeline", label: "Timeline" },
20329
- { pageId: "board", label: "Board" },
20330
- { pageId: "upcoming", label: "Upcoming" },
20331
- { pageId: "sprint-summary", label: "Sprint Summary" },
20332
- { pageId: "gar", label: "GAR Report" },
20333
- { pageId: "health", label: "Health" }
20334
- ];
20335
-
20336
- // src/web/templates/pages/persona-picker.ts
20337
- function personaPickerPage() {
20338
- const views = getAllPersonaViews();
20339
- const cards = views.map(
20340
- (v) => `
20341
- <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
20342
- <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
20343
- <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
20344
- </a>`
20345
- ).join("\n");
20346
- return `
20347
- <div class="persona-picker">
20348
- <h2>Choose Your View</h2>
20349
- <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
20350
- <div class="persona-picker-grid">
20351
- ${cards}
20352
- </div>
20353
- </div>`;
20354
- }
20355
-
20356
- // src/reports/sprint-summary/risk-assessment.ts
20357
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
20358
- var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
20359
-
20360
- 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.
20361
-
20362
- Produce a concise markdown assessment with these sections:
20363
-
20364
- ## Status Assessment
20365
- One-line verdict: is this risk actively being mitigated, stalled, or escalating?
20366
-
20367
- ## Related Activity
20368
- What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
20369
-
20370
- ## Trajectory
20371
- 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.
20372
-
20373
- ## Recommendation
20374
- One concrete next step to move this risk toward resolution.
20375
-
20376
- Rules:
20377
- - Reference artifact IDs, dates, owners, and statuses from the provided data
20378
- - Keep the tone professional and direct
20379
- - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
20380
- - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
20381
- - Produce the full assessment text directly`;
20382
- async function generateRiskAssessment(data, riskId, store) {
20383
- const risk = data.risks.find((r) => r.id === riskId);
20384
- if (!risk) return "Risk not found in sprint data.";
20385
- const prompt = buildSingleRiskPrompt(data, risk, store);
20386
- const result = query2({
20387
- prompt,
20388
- options: {
20389
- systemPrompt: SYSTEM_PROMPT2,
20390
- maxTurns: 1,
20391
- tools: [],
20392
- allowedTools: []
20393
- }
20394
- });
20395
- for await (const msg of result) {
20396
- if (msg.type === "assistant") {
20397
- const text = msg.message.content.find(
20398
- (b) => b.type === "text"
20399
- );
20400
- if (text) return text.text;
20401
- }
20402
- }
20403
- return "Unable to generate risk assessment.";
20404
- }
20405
- function buildSingleRiskPrompt(data, risk, store) {
20406
- const sections = [];
20407
- sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
20408
- if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
20409
- sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
20410
- sections.push(`Completion: ${data.workItems.completionPct}%`);
20411
- sections.push("");
20412
- const doc = store.get(risk.id);
20413
- sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
20414
- sections.push(`Type: ${risk.type}`);
20415
- if (doc) {
20416
- sections.push(`Status: ${doc.frontmatter.status}`);
20417
- if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
20418
- if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
20419
- if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
20420
- if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
20421
- if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
20422
- const tags = doc.frontmatter.tags ?? [];
20423
- if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
20424
- if (doc.content.trim()) {
20425
- sections.push(`
20426
- Description:
20427
- ${doc.content.trim()}`);
20428
- }
20429
- const allDocs = store.list();
20430
- const relatedIds = /* @__PURE__ */ new Set();
20431
- for (const d of allDocs) {
20432
- if (d.frontmatter.aboutArtifact === risk.id) {
20433
- relatedIds.add(d.frontmatter.id);
20434
- }
20435
- }
20436
- const idPattern = /\b([A-Z]-\d{3,})\b/g;
20437
- let match;
20438
- while ((match = idPattern.exec(doc.content)) !== null) {
20439
- relatedIds.add(match[1]);
20440
- }
20441
- const significantTags = tags.filter(
20442
- (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
20443
- );
20444
- if (significantTags.length > 0) {
20445
- for (const d of allDocs) {
20446
- if (d.frontmatter.id === risk.id) continue;
20447
- const dTags = d.frontmatter.tags ?? [];
20448
- if (significantTags.some((t) => dTags.includes(t))) {
20449
- relatedIds.add(d.frontmatter.id);
20450
- }
20451
- }
20452
- }
20453
- const about = doc.frontmatter.aboutArtifact;
20454
- if (about) {
20455
- relatedIds.add(about);
20456
- for (const d of allDocs) {
20457
- if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
20458
- relatedIds.add(d.frontmatter.id);
20459
- }
20460
- }
20461
- }
20462
- const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
20463
- if (relatedDocs.length > 0) {
20464
- sections.push(`
20465
- ## Related Documents (${relatedDocs.length})`);
20466
- for (const rd of relatedDocs) {
20467
- const owner = rd.frontmatter.owner ?? "unowned";
20468
- const summary = rd.content.trim().slice(0, 300);
20469
- sections.push(
20470
- `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
20471
- );
20472
- sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
20473
- if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
20474
- }
20475
- }
20476
- }
20477
- sections.push("");
20478
- sections.push(`---`);
20479
- sections.push(`
20480
- Generate the risk assessment for ${risk.id} based on the data above.`);
20481
- return sections.join("\n");
20482
- }
20483
-
20484
- // src/web/templates/persona-switcher.ts
20485
- function renderPersonaSwitcher(current, _currentPath) {
20486
- const views = getAllPersonaViews();
20487
- if (views.length === 0) return "";
20488
- const options = views.map(
20489
- (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
20490
- ).join("\n ");
20491
- return `
20492
- <div class="persona-switcher">
20493
- <label class="persona-label" for="persona-select">View</label>
20494
- <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
20495
- ${options}
20496
- </select>
20497
- </div>
20498
- <script>
20499
- function switchPersona(value) {
20500
- if (value) {
20501
- window.location.href = '/' + value + '/dashboard';
20502
- }
20503
- }
20504
- </script>`;
20505
- }
20506
-
20507
- // src/web/templates/mermaid.ts
20508
- function sanitize(text, maxLen = 40) {
20509
- const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
20510
- return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
20511
- }
20512
- function mermaidBlock(definition, extraClass) {
20513
- const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
20514
- return `<div class="${cls}"><pre class="mermaid">
20515
- ${definition}
20516
- </pre></div>`;
20517
- }
20518
- function placeholder(message) {
20519
- return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
20520
- }
20521
- function toMs(date5) {
20522
- return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
20523
- }
20524
- function fmtDate(ms) {
20525
- const d = new Date(ms);
20526
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20527
- return `${months[d.getMonth()]} ${d.getDate()}`;
20468
+ function fmtDate(ms) {
20469
+ const d = new Date(ms);
20470
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20471
+ return `${months[d.getMonth()]} ${d.getDate()}`;
20528
20472
  }
20529
20473
  function buildTimelineGantt(data, maxSprints = 6) {
20530
20474
  const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
@@ -20789,89 +20733,656 @@ function buildArtifactFlowchart(data) {
20789
20733
  return visited;
20790
20734
  }
20791
20735
 
20792
- function highlight(hoveredId) {
20793
- var connected = findConnected(hoveredId);
20794
- container.querySelectorAll('.flow-node').forEach(function(n) {
20795
- if (connected[n.dataset.flowId]) {
20796
- n.classList.add('flow-lit');
20797
- n.classList.remove('flow-dim');
20798
- } else {
20799
- n.classList.add('flow-dim');
20800
- n.classList.remove('flow-lit');
20801
- }
20802
- });
20803
- svg.querySelectorAll('path').forEach(function(p) {
20804
- if (connected[p.dataset.from] && connected[p.dataset.to]) {
20805
- p.classList.add('flow-line-lit');
20806
- p.classList.remove('flow-line-dim');
20807
- } else {
20808
- p.classList.add('flow-line-dim');
20809
- p.classList.remove('flow-line-lit');
20810
- }
20811
- });
20812
- }
20736
+ function highlight(hoveredId) {
20737
+ var connected = findConnected(hoveredId);
20738
+ container.querySelectorAll('.flow-node').forEach(function(n) {
20739
+ if (connected[n.dataset.flowId]) {
20740
+ n.classList.add('flow-lit');
20741
+ n.classList.remove('flow-dim');
20742
+ } else {
20743
+ n.classList.add('flow-dim');
20744
+ n.classList.remove('flow-lit');
20745
+ }
20746
+ });
20747
+ svg.querySelectorAll('path').forEach(function(p) {
20748
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
20749
+ p.classList.add('flow-line-lit');
20750
+ p.classList.remove('flow-line-dim');
20751
+ } else {
20752
+ p.classList.add('flow-line-dim');
20753
+ p.classList.remove('flow-line-lit');
20754
+ }
20755
+ });
20756
+ }
20757
+
20758
+ function clearHighlight() {
20759
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
20760
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
20761
+ }
20762
+
20763
+ var activeId = null;
20764
+ container.addEventListener('click', function(e) {
20765
+ // Let the ID link navigate normally
20766
+ if (e.target.closest('a')) return;
20767
+
20768
+ var node = e.target.closest('.flow-node');
20769
+ var clickedId = node ? node.dataset.flowId : null;
20770
+
20771
+ if (!clickedId || clickedId === activeId) {
20772
+ activeId = null;
20773
+ clearHighlight();
20774
+ return;
20775
+ }
20776
+
20777
+ activeId = clickedId;
20778
+ highlight(clickedId);
20779
+ });
20780
+
20781
+ function drawAndHighlight() {
20782
+ drawLines();
20783
+ if (activeId) highlight(activeId);
20784
+ }
20785
+
20786
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
20787
+ window.addEventListener('resize', drawAndHighlight);
20788
+ container.addEventListener('scroll', drawAndHighlight);
20789
+ new ResizeObserver(drawAndHighlight).observe(container);
20790
+ })();
20791
+ </script>`;
20792
+ }
20793
+ function buildStatusPie(title, counts) {
20794
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
20795
+ if (entries.length === 0) {
20796
+ return placeholder(`No data for ${title}.`);
20797
+ }
20798
+ const lines = [`pie title ${sanitize(title, 60)}`];
20799
+ for (const [label, count] of entries) {
20800
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
20801
+ }
20802
+ return mermaidBlock(lines.join("\n"));
20803
+ }
20804
+ function buildHealthGauge(categories) {
20805
+ const valid = categories.filter((c) => c.total > 0);
20806
+ if (valid.length === 0) {
20807
+ return placeholder("No completeness data available.");
20808
+ }
20809
+ const pies = valid.map((cat) => {
20810
+ const incomplete = cat.total - cat.complete;
20811
+ const lines = [
20812
+ `pie title ${sanitize(cat.name, 30)}`,
20813
+ ` "Complete" : ${cat.complete}`,
20814
+ ` "Incomplete" : ${incomplete}`
20815
+ ];
20816
+ return mermaidBlock(lines.join("\n"));
20817
+ });
20818
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
20819
+ }
20820
+
20821
+ // src/web/templates/artifact-graph.ts
20822
+ function buildArtifactRelationGraph(data) {
20823
+ const hasContent = data.origins.length > 0 || data.parents.length > 0 || data.children.length > 0 || data.external.length > 0;
20824
+ if (!hasContent) {
20825
+ return `<div class="flow-diagram flow-empty"><p>No relationships found for this artifact.</p></div>`;
20826
+ }
20827
+ const edges = data.edges;
20828
+ const renderNode = (id, title, status, type) => {
20829
+ const href = type === "jira" ? title.startsWith("http") ? title : "#" : `/docs/${type}/${id}`;
20830
+ const target = type === "jira" ? ' target="_blank" rel="noopener"' : "";
20831
+ const cls = type === "jira" ? "flow-node flow-external" : `flow-node ${statusClass(status)}`;
20832
+ const displayTitle = type === "jira" ? "Jira Issue" : sanitize(title, 35);
20833
+ const displayId = type === "jira" ? `${id} \u2197` : id;
20834
+ return `<div class="${cls}" data-flow-id="${escapeHtml(id)}">
20835
+ <a class="flow-node-id" href="${escapeHtml(href)}"${target}>${escapeHtml(displayId)}</a>
20836
+ <span class="flow-node-title">${escapeHtml(displayTitle)}</span>
20837
+ </div>`;
20838
+ };
20839
+ const selfNode = `<div class="flow-node flow-self ${statusClass(data.self.status)}" data-flow-id="${escapeHtml(data.self.id)}">
20840
+ <span class="flow-node-id">${escapeHtml(data.self.id)}</span>
20841
+ <span class="flow-node-title">${escapeHtml(sanitize(data.self.title, 35))}</span>
20842
+ </div>`;
20843
+ const columns = [];
20844
+ if (data.origins.length > 0) {
20845
+ columns.push({
20846
+ header: "Origins",
20847
+ nodes: data.origins.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
20848
+ });
20849
+ }
20850
+ if (data.parents.length > 0) {
20851
+ columns.push({
20852
+ header: "Parents",
20853
+ nodes: data.parents.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
20854
+ });
20855
+ }
20856
+ columns.push({
20857
+ header: data.self.type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
20858
+ nodes: selfNode
20859
+ });
20860
+ if (data.children.length > 0) {
20861
+ columns.push({
20862
+ header: "Children",
20863
+ nodes: data.children.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
20864
+ });
20865
+ }
20866
+ if (data.external.length > 0) {
20867
+ columns.push({
20868
+ header: "External",
20869
+ nodes: data.external.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
20870
+ });
20871
+ }
20872
+ const columnsHtml = columns.map((col) => `
20873
+ <div class="flow-column">
20874
+ <div class="flow-column-header">${escapeHtml(col.header)}</div>
20875
+ ${col.nodes}
20876
+ </div>`).join("\n");
20877
+ const edgesJson = JSON.stringify(edges);
20878
+ return `
20879
+ <div class="flow-diagram" id="rel-graph">
20880
+ <svg class="flow-lines" id="rel-lines"></svg>
20881
+ <div class="flow-columns">
20882
+ ${columnsHtml}
20883
+ </div>
20884
+ </div>
20885
+ <script>
20886
+ (function() {
20887
+ var edges = ${edgesJson};
20888
+ var container = document.getElementById('rel-graph');
20889
+ var svg = document.getElementById('rel-lines');
20890
+ if (!container || !svg) return;
20891
+
20892
+ var fwd = {};
20893
+ var bwd = {};
20894
+ edges.forEach(function(e) {
20895
+ if (!fwd[e.from]) fwd[e.from] = [];
20896
+ if (!bwd[e.to]) bwd[e.to] = [];
20897
+ fwd[e.from].push(e.to);
20898
+ bwd[e.to].push(e.from);
20899
+ });
20900
+
20901
+ function drawLines() {
20902
+ var rect = container.getBoundingClientRect();
20903
+ var scrollW = container.scrollWidth;
20904
+ var scrollH = container.scrollHeight;
20905
+ svg.setAttribute('width', scrollW);
20906
+ svg.setAttribute('height', scrollH);
20907
+ svg.innerHTML = '';
20908
+
20909
+ var scrollLeft = container.scrollLeft;
20910
+ var scrollTop = container.scrollTop;
20911
+
20912
+ edges.forEach(function(edge) {
20913
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
20914
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
20915
+ if (!fromEl || !toEl) return;
20916
+
20917
+ var fr = fromEl.getBoundingClientRect();
20918
+ var tr = toEl.getBoundingClientRect();
20919
+ var x1 = fr.right - rect.left + scrollLeft;
20920
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
20921
+ var x2 = tr.left - rect.left + scrollLeft;
20922
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
20923
+ var mx = (x1 + x2) / 2;
20924
+
20925
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
20926
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
20927
+ path.setAttribute('fill', 'none');
20928
+ path.setAttribute('stroke', '#2a2e3a');
20929
+ path.setAttribute('stroke-width', '1.5');
20930
+ path.dataset.from = edge.from;
20931
+ path.dataset.to = edge.to;
20932
+ svg.appendChild(path);
20933
+ });
20934
+ }
20935
+
20936
+ function findConnected(startId) {
20937
+ var visited = {};
20938
+ visited[startId] = true;
20939
+ var queue = [startId];
20940
+ while (queue.length) {
20941
+ var id = queue.shift();
20942
+ (fwd[id] || []).forEach(function(n) {
20943
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
20944
+ });
20945
+ }
20946
+ queue = [startId];
20947
+ while (queue.length) {
20948
+ var id = queue.shift();
20949
+ (bwd[id] || []).forEach(function(n) {
20950
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
20951
+ });
20952
+ }
20953
+ return visited;
20954
+ }
20955
+
20956
+ function highlight(hoveredId) {
20957
+ var connected = findConnected(hoveredId);
20958
+ container.querySelectorAll('.flow-node').forEach(function(n) {
20959
+ if (connected[n.dataset.flowId]) {
20960
+ n.classList.add('flow-lit'); n.classList.remove('flow-dim');
20961
+ } else {
20962
+ n.classList.add('flow-dim'); n.classList.remove('flow-lit');
20963
+ }
20964
+ });
20965
+ svg.querySelectorAll('path').forEach(function(p) {
20966
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
20967
+ p.classList.add('flow-line-lit'); p.classList.remove('flow-line-dim');
20968
+ } else {
20969
+ p.classList.add('flow-line-dim'); p.classList.remove('flow-line-lit');
20970
+ }
20971
+ });
20972
+ }
20973
+
20974
+ function clearHighlight() {
20975
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
20976
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
20977
+ }
20978
+
20979
+ var activeId = null;
20980
+ container.addEventListener('click', function(e) {
20981
+ if (e.target.closest('a')) return;
20982
+ var node = e.target.closest('.flow-node');
20983
+ var clickedId = node ? node.dataset.flowId : null;
20984
+ if (!clickedId || clickedId === activeId) {
20985
+ activeId = null; clearHighlight(); return;
20986
+ }
20987
+ activeId = clickedId;
20988
+ highlight(clickedId);
20989
+ });
20990
+
20991
+ function drawAndHighlight() {
20992
+ drawLines();
20993
+ if (activeId) highlight(activeId);
20994
+ }
20995
+
20996
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
20997
+ window.addEventListener('resize', drawAndHighlight);
20998
+ container.addEventListener('scroll', drawAndHighlight);
20999
+ new ResizeObserver(drawAndHighlight).observe(container);
21000
+ })();
21001
+ </script>`;
21002
+ }
21003
+ var EVENT_ICONS = {
21004
+ "created": "\u{1F7E2}",
21005
+ "source-linked": "\u{1F535}",
21006
+ "child-spawned": "\u{1F7E1}",
21007
+ "assessment": "\u{1F7E3}",
21008
+ "jira-sync": "\u{1F537}"
21009
+ };
21010
+ function buildLineageTimeline(events) {
21011
+ if (events.length === 0) {
21012
+ return "";
21013
+ }
21014
+ const entries = events.map((event) => {
21015
+ const icon = EVENT_ICONS[event.type] ?? "\u26AA";
21016
+ const date5 = event.date ? formatDate(event.date) : "";
21017
+ const time3 = event.date?.slice(11, 16) ?? "";
21018
+ const label = linkArtifactIds(escapeHtml(event.label));
21019
+ return `
21020
+ <div class="lineage-entry lineage-${escapeHtml(event.type)}">
21021
+ <div class="lineage-marker">${icon}</div>
21022
+ <div class="lineage-content">
21023
+ <span class="lineage-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
21024
+ <span class="lineage-label">${label}</span>
21025
+ </div>
21026
+ </div>`;
21027
+ });
21028
+ return `
21029
+ <div class="lineage-timeline">
21030
+ <h3>Lineage</h3>
21031
+ ${entries.join("\n")}
21032
+ </div>`;
21033
+ }
21034
+
21035
+ // src/web/templates/pages/document-detail.ts
21036
+ function documentDetailPage(doc, store) {
21037
+ const fm = doc.frontmatter;
21038
+ const label = typeLabel(fm.type);
21039
+ const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
21040
+ const entries = Object.entries(fm).filter(
21041
+ ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
21042
+ );
21043
+ const arrayEntries = Object.entries(fm).filter(
21044
+ ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
21045
+ );
21046
+ const allEntries = [
21047
+ ...entries.filter(([, v]) => !Array.isArray(v)),
21048
+ ...arrayEntries
21049
+ ];
21050
+ const dtDd = allEntries.map(([key, value]) => {
21051
+ let rendered;
21052
+ if (key === "status") {
21053
+ rendered = statusBadge(value);
21054
+ } else if (key === "tags" && Array.isArray(value)) {
21055
+ rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
21056
+ } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
21057
+ rendered = formatDate(value);
21058
+ } else {
21059
+ rendered = linkArtifactIds(escapeHtml(String(value)));
21060
+ }
21061
+ return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
21062
+ }).join("\n ");
21063
+ const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
21064
+ const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
21065
+ const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
21066
+ return `
21067
+ <div class="breadcrumb">
21068
+ <a href="/">Overview</a><span class="sep">/</span>
21069
+ <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
21070
+ ${escapeHtml(fm.id)}
21071
+ </div>
21072
+
21073
+ <div class="page-header">
21074
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
21075
+ <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
21076
+ </div>
21077
+
21078
+ <div class="detail-meta">
21079
+ <dl>
21080
+ ${dtDd}
21081
+ </dl>
21082
+ </div>
21083
+
21084
+ ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
21085
+
21086
+ ${timelineHtml}
21087
+
21088
+ ${store ? renderRelationshipsAndLineage(store, fm.id) : ""}
21089
+ `;
21090
+ }
21091
+ function renderRelationshipsAndLineage(store, docId) {
21092
+ const parts = [];
21093
+ const relationships = getArtifactRelationships(store, docId);
21094
+ if (relationships) {
21095
+ const graphHtml = buildArtifactRelationGraph(relationships);
21096
+ parts.push(collapsibleSection("rel-graph-" + docId, "Relationships", graphHtml));
21097
+ }
21098
+ const events = getArtifactLineageEvents(store, docId);
21099
+ if (events.length > 0) {
21100
+ const lineageHtml = buildLineageTimeline(events);
21101
+ parts.push(collapsibleSection("lineage-" + docId, "Lineage", lineageHtml, { defaultCollapsed: true }));
21102
+ }
21103
+ return parts.join("\n");
21104
+ }
21105
+ function isValidAssessmentEntry(value) {
21106
+ if (typeof value !== "object" || value === null) return false;
21107
+ const obj = value;
21108
+ if (typeof obj.generatedAt !== "string") return false;
21109
+ if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
21110
+ return true;
21111
+ }
21112
+ function normalizeEntry(entry) {
21113
+ return {
21114
+ generatedAt: entry.generatedAt ?? "",
21115
+ commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
21116
+ commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
21117
+ signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
21118
+ childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
21119
+ childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
21120
+ childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
21121
+ linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0,
21122
+ blockerProgress: typeof entry.blockerProgress === "number" ? entry.blockerProgress : null,
21123
+ totalBlockers: typeof entry.totalBlockers === "number" ? entry.totalBlockers : 0,
21124
+ resolvedBlockers: typeof entry.resolvedBlockers === "number" ? entry.resolvedBlockers : 0
21125
+ };
21126
+ }
21127
+ function renderAssessmentTimeline(history) {
21128
+ const entries = history.map((raw, i) => {
21129
+ const entry = normalizeEntry(raw);
21130
+ const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
21131
+ const time3 = entry.generatedAt?.slice(11, 16) ?? "";
21132
+ const isLatest = i === 0;
21133
+ const parts = [];
21134
+ if (entry.commentSummary) {
21135
+ parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
21136
+ }
21137
+ if (entry.commentAnalysisProgress !== null) {
21138
+ parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
21139
+ }
21140
+ if (entry.childCount > 0) {
21141
+ const bar = progressBarHtml(entry.childRollupProgress ?? 0);
21142
+ parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
21143
+ }
21144
+ if (entry.totalBlockers > 0) {
21145
+ const bar = progressBarHtml(entry.blockerProgress ?? 0);
21146
+ parts.push(`<div class="assessment-stat">\u{1F6A7} Blockers: ${entry.resolvedBlockers}/${entry.totalBlockers} resolved ${bar} ${entry.blockerProgress ?? 0}%</div>`);
21147
+ }
21148
+ if (entry.linkedIssueCount > 0) {
21149
+ parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
21150
+ }
21151
+ if (entry.signals.length > 0) {
21152
+ const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
21153
+ parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
21154
+ }
21155
+ return `
21156
+ <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
21157
+ <div class="assessment-header">
21158
+ <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
21159
+ ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
21160
+ </div>
21161
+ ${parts.join("\n")}
21162
+ </div>`;
21163
+ });
21164
+ return `
21165
+ <div class="assessment-timeline">
21166
+ <h3>Assessment History</h3>
21167
+ ${entries.join("\n")}
21168
+ </div>`;
21169
+ }
21170
+ function progressBarHtml(pct) {
21171
+ const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
21172
+ const empty = 10 - filled;
21173
+ return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
21174
+ }
21175
+
21176
+ // src/web/persona-views.ts
21177
+ var VIEWS = /* @__PURE__ */ new Map();
21178
+ var PAGE_RENDERERS = /* @__PURE__ */ new Map();
21179
+ function registerPersonaView(config2) {
21180
+ VIEWS.set(config2.shortName, config2);
21181
+ }
21182
+ function registerPersonaPage(persona, pageId, renderer) {
21183
+ PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
21184
+ }
21185
+ function getPersonaView(mode) {
21186
+ if (!mode) return void 0;
21187
+ return VIEWS.get(mode);
21188
+ }
21189
+ function getPersonaPageRenderer(persona, pageId) {
21190
+ return PAGE_RENDERERS.get(`${persona}/${pageId}`);
21191
+ }
21192
+ function getAllPersonaViews() {
21193
+ return [...VIEWS.values()];
21194
+ }
21195
+ var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
21196
+ function parsePersonaFromUrl(params) {
21197
+ const value = params.get("persona")?.toLowerCase();
21198
+ if (value && VALID_PERSONAS.has(value)) return value;
21199
+ return null;
21200
+ }
21201
+ function parsePersonaFromPath(pathname) {
21202
+ const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
21203
+ return match ? match[1] : null;
21204
+ }
21205
+ function resolvePersona(pathname, params) {
21206
+ return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
21207
+ }
21208
+ var SHARED_NAV_ITEMS = [
21209
+ { pageId: "timeline", label: "Timeline" },
21210
+ { pageId: "board", label: "Board" },
21211
+ { pageId: "upcoming", label: "Upcoming" },
21212
+ { pageId: "sprint-summary", label: "Sprint Summary" },
21213
+ { pageId: "gar", label: "GAR Report" },
21214
+ { pageId: "health", label: "Health" }
21215
+ ];
21216
+
21217
+ // src/web/templates/pages/persona-picker.ts
21218
+ function personaPickerPage() {
21219
+ const views = getAllPersonaViews();
21220
+ const cards = views.map(
21221
+ (v) => `
21222
+ <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
21223
+ <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
21224
+ <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
21225
+ </a>`
21226
+ ).join("\n");
21227
+ return `
21228
+ <div class="persona-picker">
21229
+ <h2>Choose Your View</h2>
21230
+ <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
21231
+ <div class="persona-picker-grid">
21232
+ ${cards}
21233
+ </div>
21234
+ </div>`;
21235
+ }
20813
21236
 
20814
- function clearHighlight() {
20815
- container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
20816
- svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
20817
- }
21237
+ // src/reports/sprint-summary/risk-assessment.ts
21238
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21239
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
20818
21240
 
20819
- var activeId = null;
20820
- container.addEventListener('click', function(e) {
20821
- // Let the ID link navigate normally
20822
- if (e.target.closest('a')) return;
21241
+ 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.
20823
21242
 
20824
- var node = e.target.closest('.flow-node');
20825
- var clickedId = node ? node.dataset.flowId : null;
21243
+ Produce a concise markdown assessment with these sections:
20826
21244
 
20827
- if (!clickedId || clickedId === activeId) {
20828
- activeId = null;
20829
- clearHighlight();
20830
- return;
20831
- }
21245
+ ## Status Assessment
21246
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
20832
21247
 
20833
- activeId = clickedId;
20834
- highlight(clickedId);
20835
- });
21248
+ ## Related Activity
21249
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
20836
21250
 
20837
- function drawAndHighlight() {
20838
- drawLines();
20839
- if (activeId) highlight(activeId);
20840
- }
21251
+ ## Trajectory
21252
+ 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.
20841
21253
 
20842
- requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
20843
- window.addEventListener('resize', drawAndHighlight);
20844
- container.addEventListener('scroll', drawAndHighlight);
20845
- new ResizeObserver(drawAndHighlight).observe(container);
20846
- })();
20847
- </script>`;
20848
- }
20849
- function buildStatusPie(title, counts) {
20850
- const entries = Object.entries(counts).filter(([, v]) => v > 0);
20851
- if (entries.length === 0) {
20852
- return placeholder(`No data for ${title}.`);
20853
- }
20854
- const lines = [`pie title ${sanitize(title, 60)}`];
20855
- for (const [label, count] of entries) {
20856
- lines.push(` "${sanitize(label, 30)}" : ${count}`);
21254
+ ## Recommendation
21255
+ One concrete next step to move this risk toward resolution.
21256
+
21257
+ Rules:
21258
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
21259
+ - Keep the tone professional and direct
21260
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
21261
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
21262
+ - Produce the full assessment text directly`;
21263
+ async function generateRiskAssessment(data, riskId, store) {
21264
+ const risk = data.risks.find((r) => r.id === riskId);
21265
+ if (!risk) return "Risk not found in sprint data.";
21266
+ const prompt = buildSingleRiskPrompt(data, risk, store);
21267
+ const result = query2({
21268
+ prompt,
21269
+ options: {
21270
+ systemPrompt: SYSTEM_PROMPT2,
21271
+ maxTurns: 1,
21272
+ tools: [],
21273
+ allowedTools: []
21274
+ }
21275
+ });
21276
+ for await (const msg of result) {
21277
+ if (msg.type === "assistant") {
21278
+ const text = msg.message.content.find(
21279
+ (b) => b.type === "text"
21280
+ );
21281
+ if (text) return text.text;
21282
+ }
20857
21283
  }
20858
- return mermaidBlock(lines.join("\n"));
21284
+ return "Unable to generate risk assessment.";
20859
21285
  }
20860
- function buildHealthGauge(categories) {
20861
- const valid = categories.filter((c) => c.total > 0);
20862
- if (valid.length === 0) {
20863
- return placeholder("No completeness data available.");
21286
+ function buildSingleRiskPrompt(data, risk, store) {
21287
+ const sections = [];
21288
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
21289
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
21290
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
21291
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
21292
+ sections.push("");
21293
+ const doc = store.get(risk.id);
21294
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
21295
+ sections.push(`Type: ${risk.type}`);
21296
+ if (doc) {
21297
+ sections.push(`Status: ${doc.frontmatter.status}`);
21298
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
21299
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
21300
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
21301
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
21302
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
21303
+ const tags = doc.frontmatter.tags ?? [];
21304
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
21305
+ if (doc.content.trim()) {
21306
+ sections.push(`
21307
+ Description:
21308
+ ${doc.content.trim()}`);
21309
+ }
21310
+ const allDocs = store.list();
21311
+ const relatedIds = /* @__PURE__ */ new Set();
21312
+ for (const d of allDocs) {
21313
+ if (d.frontmatter.aboutArtifact === risk.id) {
21314
+ relatedIds.add(d.frontmatter.id);
21315
+ }
21316
+ }
21317
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
21318
+ let match;
21319
+ while ((match = idPattern.exec(doc.content)) !== null) {
21320
+ relatedIds.add(match[1]);
21321
+ }
21322
+ const significantTags = tags.filter(
21323
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
21324
+ );
21325
+ if (significantTags.length > 0) {
21326
+ for (const d of allDocs) {
21327
+ if (d.frontmatter.id === risk.id) continue;
21328
+ const dTags = d.frontmatter.tags ?? [];
21329
+ if (significantTags.some((t) => dTags.includes(t))) {
21330
+ relatedIds.add(d.frontmatter.id);
21331
+ }
21332
+ }
21333
+ }
21334
+ const about = doc.frontmatter.aboutArtifact;
21335
+ if (about) {
21336
+ relatedIds.add(about);
21337
+ for (const d of allDocs) {
21338
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
21339
+ relatedIds.add(d.frontmatter.id);
21340
+ }
21341
+ }
21342
+ }
21343
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
21344
+ if (relatedDocs.length > 0) {
21345
+ sections.push(`
21346
+ ## Related Documents (${relatedDocs.length})`);
21347
+ for (const rd of relatedDocs) {
21348
+ const owner = rd.frontmatter.owner ?? "unowned";
21349
+ const summary = rd.content.trim().slice(0, 300);
21350
+ sections.push(
21351
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
21352
+ );
21353
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
21354
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
21355
+ }
21356
+ }
20864
21357
  }
20865
- const pies = valid.map((cat) => {
20866
- const incomplete = cat.total - cat.complete;
20867
- const lines = [
20868
- `pie title ${sanitize(cat.name, 30)}`,
20869
- ` "Complete" : ${cat.complete}`,
20870
- ` "Incomplete" : ${incomplete}`
20871
- ];
20872
- return mermaidBlock(lines.join("\n"));
20873
- });
20874
- return `<div class="mermaid-row">${pies.join("\n")}</div>`;
21358
+ sections.push("");
21359
+ sections.push(`---`);
21360
+ sections.push(`
21361
+ Generate the risk assessment for ${risk.id} based on the data above.`);
21362
+ return sections.join("\n");
21363
+ }
21364
+
21365
+ // src/web/templates/persona-switcher.ts
21366
+ function renderPersonaSwitcher(current, _currentPath) {
21367
+ const views = getAllPersonaViews();
21368
+ if (views.length === 0) return "";
21369
+ const options = views.map(
21370
+ (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
21371
+ ).join("\n ");
21372
+ return `
21373
+ <div class="persona-switcher">
21374
+ <label class="persona-label" for="persona-select">View</label>
21375
+ <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
21376
+ ${options}
21377
+ </select>
21378
+ </div>
21379
+ <script>
21380
+ function switchPersona(value) {
21381
+ if (value) {
21382
+ window.location.href = '/' + value + '/dashboard';
21383
+ }
21384
+ }
21385
+ </script>`;
20875
21386
  }
20876
21387
 
20877
21388
  // src/web/templates/pages/po/dashboard.ts
@@ -24006,7 +24517,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
24006
24517
  notFound(res, projectName, navGroups, pathname, persona, pOpts);
24007
24518
  return;
24008
24519
  }
24009
- const body = documentDetailPage(doc);
24520
+ const body = documentDetailPage(doc, store);
24010
24521
  respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, persona, ...pOpts }, body));
24011
24522
  return;
24012
24523
  }
@@ -26524,6 +27035,15 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
26524
27035
  });
26525
27036
  }
26526
27037
  }
27038
+ function computeBlockerProgress(linkedIssues, prerequisiteWeight) {
27039
+ const blockerLinks = linkedIssues.filter(
27040
+ (l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
27041
+ );
27042
+ if (blockerLinks.length === 0) return null;
27043
+ const resolved = blockerLinks.filter((l) => l.isDone).length;
27044
+ const blockerProgress = Math.round(resolved / blockerLinks.length * prerequisiteWeight * 100);
27045
+ return { blockerProgress, totalBlockers: blockerLinks.length, resolvedBlockers: resolved };
27046
+ }
26527
27047
  var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
26528
27048
 
26529
27049
  For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
@@ -26937,6 +27457,39 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
26937
27457
  });
26938
27458
  }
26939
27459
  }
27460
+ const prerequisiteWeight = options.prerequisiteWeight ?? 0.3;
27461
+ const blockerResult = computeBlockerProgress(linkedIssues, prerequisiteWeight);
27462
+ let blockerProgressValue = null;
27463
+ let totalBlockersCount = 0;
27464
+ let resolvedBlockersCount = 0;
27465
+ if (blockerResult && !fm.progressOverride && !DONE_STATUSES15.has(fm.status)) {
27466
+ blockerProgressValue = blockerResult.blockerProgress;
27467
+ totalBlockersCount = blockerResult.totalBlockers;
27468
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27469
+ const lastProgressUpdate = findLast(proposedUpdates, (u) => u.artifactId === fm.id && u.field === "progress");
27470
+ const implementationProgress = lastProgressUpdate ? lastProgressUpdate.proposedValue : currentProgress;
27471
+ const combinedProgress = Math.round(
27472
+ blockerResult.blockerProgress + implementationProgress * (1 - prerequisiteWeight)
27473
+ );
27474
+ const estimatedProgress = Math.max(currentProgress, combinedProgress);
27475
+ if (estimatedProgress !== currentProgress && estimatedProgress !== implementationProgress) {
27476
+ for (let i = proposedUpdates.length - 1; i >= 0; i--) {
27477
+ if (proposedUpdates[i].artifactId === fm.id && proposedUpdates[i].field === "progress") {
27478
+ proposedUpdates.splice(i, 1);
27479
+ }
27480
+ }
27481
+ proposedUpdates.push({
27482
+ artifactId: fm.id,
27483
+ field: "progress",
27484
+ currentValue: currentProgress,
27485
+ proposedValue: estimatedProgress,
27486
+ reason: `Blocker resolution (${resolvedBlockersCount}/${totalBlockersCount}) + implementation \u2192 dependency-weighted progress ${estimatedProgress}%`
27487
+ });
27488
+ }
27489
+ } else if (blockerResult) {
27490
+ totalBlockersCount = blockerResult.totalBlockers;
27491
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27492
+ }
26940
27493
  const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
26941
27494
  const appliedUpdates = [];
26942
27495
  if (options.applyUpdates && proposedUpdates.length > 0) {
@@ -26979,7 +27532,10 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
26979
27532
  commentAnalysisProgress,
26980
27533
  signals,
26981
27534
  children,
26982
- linkedIssues
27535
+ linkedIssues,
27536
+ blockerProgressValue,
27537
+ totalBlockersCount,
27538
+ resolvedBlockersCount
26983
27539
  );
26984
27540
  const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
26985
27541
  const legacySummary = fm.assessmentSummary;
@@ -27028,6 +27584,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27028
27584
  commentAnalysisProgress,
27029
27585
  linkedIssues,
27030
27586
  linkedIssueSignals,
27587
+ blockerProgress: blockerProgressValue,
27588
+ totalBlockers: totalBlockersCount,
27589
+ resolvedBlockers: resolvedBlockersCount,
27031
27590
  children,
27032
27591
  proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
27033
27592
  appliedUpdates,
@@ -27207,6 +27766,9 @@ function emptyArtifactReport(artifactId, errors) {
27207
27766
  commentAnalysisProgress: null,
27208
27767
  linkedIssues: [],
27209
27768
  linkedIssueSignals: [],
27769
+ blockerProgress: null,
27770
+ totalBlockers: 0,
27771
+ resolvedBlockers: 0,
27210
27772
  children: [],
27211
27773
  proposedUpdates: [],
27212
27774
  appliedUpdates: [],
@@ -27214,7 +27776,7 @@ function emptyArtifactReport(artifactId, errors) {
27214
27776
  errors
27215
27777
  };
27216
27778
  }
27217
- function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues) {
27779
+ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues, blockerProgress = null, totalBlockers = 0, resolvedBlockers = 0) {
27218
27780
  const childProgressValues = children.map((c) => {
27219
27781
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
27220
27782
  const lastStatus = findLast(updates, (u) => u.field === "status");
@@ -27238,7 +27800,10 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
27238
27800
  childCount: children.length,
27239
27801
  childDoneCount,
27240
27802
  childRollupProgress,
27241
- linkedIssueCount: linkedIssues.length
27803
+ linkedIssueCount: linkedIssues.length,
27804
+ blockerProgress,
27805
+ totalBlockers,
27806
+ resolvedBlockers
27242
27807
  };
27243
27808
  }
27244
27809
  function formatArtifactReport(report) {
@@ -27274,6 +27839,12 @@ function formatArtifactReport(report) {
27274
27839
  }
27275
27840
  parts.push("");
27276
27841
  }
27842
+ if (report.totalBlockers > 0) {
27843
+ parts.push(`## Blocker Resolution`);
27844
+ const bpLabel = report.blockerProgress !== null ? `${report.blockerProgress}%` : "n/a (skipped)";
27845
+ parts.push(` ${report.resolvedBlockers}/${report.totalBlockers} blockers resolved \u2192 ${bpLabel} prerequisite progress`);
27846
+ parts.push("");
27847
+ }
27277
27848
  if (report.children.length > 0) {
27278
27849
  const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
27279
27850
  const childProgress = Math.round(
@@ -28172,10 +28743,11 @@ function createJiraTools(store, projectConfig) {
28172
28743
  // --- Single-artifact assessment ---
28173
28744
  tool20(
28174
28745
  "assess_artifact",
28175
- "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).",
28746
+ "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).",
28176
28747
  {
28177
28748
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
28178
- applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
28749
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)"),
28750
+ 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.")
28179
28751
  },
28180
28752
  async (args) => {
28181
28753
  const jira = createJiraClient(jiraUserConfig);
@@ -28187,6 +28759,7 @@ function createJiraTools(store, projectConfig) {
28187
28759
  {
28188
28760
  artifactId: args.artifactId,
28189
28761
  applyUpdates: args.applyUpdates ?? false,
28762
+ prerequisiteWeight: args.prerequisiteWeight,
28190
28763
  statusMap
28191
28764
  }
28192
28765
  );
@@ -33855,7 +34428,7 @@ function createProgram() {
33855
34428
  const program = new Command();
33856
34429
  program.name("marvin").description(
33857
34430
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
33858
- ).version("0.5.25");
34431
+ ).version("0.5.27");
33859
34432
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
33860
34433
  await initCommand();
33861
34434
  });