mrvn-cli 0.3.2 → 0.3.5

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.
@@ -6474,13 +6474,13 @@ var error16 = () => {
6474
6474
  // no unit
6475
6475
  };
6476
6476
  const typeEntry = (t) => t ? TypeNames[t] : void 0;
6477
- const typeLabel = (t) => {
6477
+ const typeLabel2 = (t) => {
6478
6478
  const e = typeEntry(t);
6479
6479
  if (e)
6480
6480
  return e.label;
6481
6481
  return t ?? TypeNames.unknown.label;
6482
6482
  };
6483
- const withDefinite = (t) => `\u05D4${typeLabel(t)}`;
6483
+ const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
6484
6484
  const verbFor = (t) => {
6485
6485
  const e = typeEntry(t);
6486
6486
  const gender = e?.gender ?? "m";
@@ -6530,7 +6530,7 @@ var error16 = () => {
6530
6530
  switch (issue2.code) {
6531
6531
  case "invalid_type": {
6532
6532
  const expectedKey = issue2.expected;
6533
- const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel(expectedKey);
6533
+ const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
6534
6534
  const receivedType = parsedType(issue2.input);
6535
6535
  const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
6536
6536
  if (/^[A-Z]/.test(issue2.expected)) {
@@ -14227,7 +14227,7 @@ function createDecisionTools(store) {
14227
14227
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14228
14228
  };
14229
14229
  },
14230
- { annotations: { readOnly: true } }
14230
+ { annotations: { readOnlyHint: true } }
14231
14231
  ),
14232
14232
  tool(
14233
14233
  "get_decision",
@@ -14254,7 +14254,7 @@ function createDecisionTools(store) {
14254
14254
  ]
14255
14255
  };
14256
14256
  },
14257
- { annotations: { readOnly: true } }
14257
+ { annotations: { readOnlyHint: true } }
14258
14258
  ),
14259
14259
  tool(
14260
14260
  "create_decision",
@@ -14315,6 +14315,19 @@ function createDecisionTools(store) {
14315
14315
 
14316
14316
  // src/agent/tools/actions.ts
14317
14317
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14318
+ function findMatchingSprints(store, dueDate) {
14319
+ const sprints = store.list({ type: "sprint" });
14320
+ return sprints.filter((s) => {
14321
+ const start = s.frontmatter.startDate;
14322
+ const end = s.frontmatter.endDate;
14323
+ return start && end && dueDate >= start && dueDate <= end;
14324
+ }).map((s) => ({
14325
+ id: s.frontmatter.id,
14326
+ title: s.frontmatter.title,
14327
+ startDate: s.frontmatter.startDate,
14328
+ endDate: s.frontmatter.endDate
14329
+ }));
14330
+ }
14318
14331
  function createActionTools(store) {
14319
14332
  return [
14320
14333
  tool2(
@@ -14330,19 +14343,24 @@ function createActionTools(store) {
14330
14343
  status: args.status,
14331
14344
  owner: args.owner
14332
14345
  });
14333
- const summary = docs.map((d) => ({
14334
- id: d.frontmatter.id,
14335
- title: d.frontmatter.title,
14336
- status: d.frontmatter.status,
14337
- owner: d.frontmatter.owner,
14338
- priority: d.frontmatter.priority,
14339
- created: d.frontmatter.created
14340
- }));
14346
+ const summary = docs.map((d) => {
14347
+ const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
14348
+ return {
14349
+ id: d.frontmatter.id,
14350
+ title: d.frontmatter.title,
14351
+ status: d.frontmatter.status,
14352
+ owner: d.frontmatter.owner,
14353
+ priority: d.frontmatter.priority,
14354
+ dueDate: d.frontmatter.dueDate,
14355
+ sprints: sprintIds.length > 0 ? sprintIds : void 0,
14356
+ created: d.frontmatter.created
14357
+ };
14358
+ });
14341
14359
  return {
14342
14360
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14343
14361
  };
14344
14362
  },
14345
- { annotations: { readOnly: true } }
14363
+ { annotations: { readOnlyHint: true } }
14346
14364
  ),
14347
14365
  tool2(
14348
14366
  "get_action",
@@ -14369,7 +14387,7 @@ function createActionTools(store) {
14369
14387
  ]
14370
14388
  };
14371
14389
  },
14372
- { annotations: { readOnly: true } }
14390
+ { annotations: { readOnlyHint: true } }
14373
14391
  ),
14374
14392
  tool2(
14375
14393
  "create_action",
@@ -14380,9 +14398,18 @@ function createActionTools(store) {
14380
14398
  status: external_exports.string().optional().describe("Status (default: 'open')"),
14381
14399
  owner: external_exports.string().optional().describe("Person responsible"),
14382
14400
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
14383
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
14401
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
14402
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14403
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
14384
14404
  },
14385
14405
  async (args) => {
14406
+ const tags = [...args.tags ?? []];
14407
+ if (args.sprints) {
14408
+ for (const sprintId of args.sprints) {
14409
+ const tag = `sprint:${sprintId}`;
14410
+ if (!tags.includes(tag)) tags.push(tag);
14411
+ }
14412
+ }
14386
14413
  const doc = store.create(
14387
14414
  "action",
14388
14415
  {
@@ -14390,17 +14417,21 @@ function createActionTools(store) {
14390
14417
  status: args.status,
14391
14418
  owner: args.owner,
14392
14419
  priority: args.priority,
14393
- tags: args.tags
14420
+ tags: tags.length > 0 ? tags : void 0,
14421
+ dueDate: args.dueDate
14394
14422
  },
14395
14423
  args.content
14396
14424
  );
14425
+ const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
14426
+ if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
14427
+ const matching = findMatchingSprints(store, args.dueDate);
14428
+ if (matching.length > 0) {
14429
+ const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
14430
+ parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
14431
+ }
14432
+ }
14397
14433
  return {
14398
- content: [
14399
- {
14400
- type: "text",
14401
- text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
14402
- }
14403
- ]
14434
+ content: [{ type: "text", text: parts.join("\n") }]
14404
14435
  };
14405
14436
  }
14406
14437
  ),
@@ -14413,10 +14444,25 @@ function createActionTools(store) {
14413
14444
  status: external_exports.string().optional().describe("New status"),
14414
14445
  content: external_exports.string().optional().describe("New content"),
14415
14446
  owner: external_exports.string().optional().describe("New owner"),
14416
- priority: external_exports.string().optional().describe("New priority")
14447
+ priority: external_exports.string().optional().describe("New priority"),
14448
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14449
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
14417
14450
  },
14418
14451
  async (args) => {
14419
- const { id, content, ...updates } = args;
14452
+ const { id, content, sprints, ...updates } = args;
14453
+ if (sprints !== void 0) {
14454
+ const existing = store.get(id);
14455
+ if (!existing) {
14456
+ return {
14457
+ content: [{ type: "text", text: `Action ${id} not found` }],
14458
+ isError: true
14459
+ };
14460
+ }
14461
+ const existingTags = existing.frontmatter.tags ?? [];
14462
+ const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
14463
+ const newSprintTags = sprints.map((s) => `sprint:${s}`);
14464
+ updates.tags = [...nonSprintTags, ...newSprintTags];
14465
+ }
14420
14466
  const doc = store.update(id, updates, content);
14421
14467
  return {
14422
14468
  content: [
@@ -14427,6 +14473,35 @@ function createActionTools(store) {
14427
14473
  ]
14428
14474
  };
14429
14475
  }
14476
+ ),
14477
+ tool2(
14478
+ "suggest_sprints_for_action",
14479
+ "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
14480
+ {
14481
+ dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
14482
+ },
14483
+ async (args) => {
14484
+ const matching = findMatchingSprints(store, args.dueDate);
14485
+ if (matching.length === 0) {
14486
+ return {
14487
+ content: [
14488
+ {
14489
+ type: "text",
14490
+ text: `No sprints found containing dueDate ${args.dueDate}.`
14491
+ }
14492
+ ]
14493
+ };
14494
+ }
14495
+ return {
14496
+ content: [
14497
+ {
14498
+ type: "text",
14499
+ text: JSON.stringify(matching, null, 2)
14500
+ }
14501
+ ]
14502
+ };
14503
+ },
14504
+ { annotations: { readOnlyHint: true } }
14430
14505
  )
14431
14506
  ];
14432
14507
  }
@@ -14454,7 +14529,7 @@ function createQuestionTools(store) {
14454
14529
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14455
14530
  };
14456
14531
  },
14457
- { annotations: { readOnly: true } }
14532
+ { annotations: { readOnlyHint: true } }
14458
14533
  ),
14459
14534
  tool3(
14460
14535
  "get_question",
@@ -14481,7 +14556,7 @@ function createQuestionTools(store) {
14481
14556
  ]
14482
14557
  };
14483
14558
  },
14484
- { annotations: { readOnly: true } }
14559
+ { annotations: { readOnlyHint: true } }
14485
14560
  ),
14486
14561
  tool3(
14487
14562
  "create_question",
@@ -14574,7 +14649,7 @@ function createDocumentTools(store) {
14574
14649
  ]
14575
14650
  };
14576
14651
  },
14577
- { annotations: { readOnly: true } }
14652
+ { annotations: { readOnlyHint: true } }
14578
14653
  ),
14579
14654
  tool4(
14580
14655
  "read_document",
@@ -14601,7 +14676,7 @@ function createDocumentTools(store) {
14601
14676
  ]
14602
14677
  };
14603
14678
  },
14604
- { annotations: { readOnly: true } }
14679
+ { annotations: { readOnlyHint: true } }
14605
14680
  ),
14606
14681
  tool4(
14607
14682
  "project_summary",
@@ -14629,7 +14704,7 @@ function createDocumentTools(store) {
14629
14704
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14630
14705
  };
14631
14706
  },
14632
- { annotations: { readOnly: true } }
14707
+ { annotations: { readOnlyHint: true } }
14633
14708
  )
14634
14709
  ];
14635
14710
  }
@@ -14666,7 +14741,7 @@ function createSourceTools(manifest) {
14666
14741
  ]
14667
14742
  };
14668
14743
  },
14669
- { annotations: { readOnly: true } }
14744
+ { annotations: { readOnlyHint: true } }
14670
14745
  ),
14671
14746
  tool5(
14672
14747
  "get_source_info",
@@ -14700,7 +14775,7 @@ function createSourceTools(manifest) {
14700
14775
  ]
14701
14776
  };
14702
14777
  },
14703
- { annotations: { readOnly: true } }
14778
+ { annotations: { readOnlyHint: true } }
14704
14779
  )
14705
14780
  ];
14706
14781
  }
@@ -14731,7 +14806,7 @@ function createSessionTools(store) {
14731
14806
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14732
14807
  };
14733
14808
  },
14734
- { annotations: { readOnly: true } }
14809
+ { annotations: { readOnlyHint: true } }
14735
14810
  ),
14736
14811
  tool6(
14737
14812
  "get_session",
@@ -14749,7 +14824,7 @@ function createSessionTools(store) {
14749
14824
  content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
14750
14825
  };
14751
14826
  },
14752
- { annotations: { readOnly: true } }
14827
+ { annotations: { readOnlyHint: true } }
14753
14828
  )
14754
14829
  ];
14755
14830
  }
@@ -14842,7 +14917,7 @@ function createMeetingTools(store) {
14842
14917
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14843
14918
  };
14844
14919
  },
14845
- { annotations: { readOnly: true } }
14920
+ { annotations: { readOnlyHint: true } }
14846
14921
  ),
14847
14922
  tool7(
14848
14923
  "get_meeting",
@@ -14869,7 +14944,7 @@ function createMeetingTools(store) {
14869
14944
  ]
14870
14945
  };
14871
14946
  },
14872
- { annotations: { readOnly: true } }
14947
+ { annotations: { readOnlyHint: true } }
14873
14948
  ),
14874
14949
  tool7(
14875
14950
  "create_meeting",
@@ -14994,7 +15069,7 @@ function createMeetingTools(store) {
14994
15069
  content: [{ type: "text", text: sections.join("\n") }]
14995
15070
  };
14996
15071
  },
14997
- { annotations: { readOnly: true } }
15072
+ { annotations: { readOnlyHint: true } }
14998
15073
  )
14999
15074
  ];
15000
15075
  }
@@ -15011,9 +15086,17 @@ function collectGarMetrics(store) {
15011
15086
  const blockedItems = allDocs.filter(
15012
15087
  (d) => d.frontmatter.tags?.includes("blocked")
15013
15088
  );
15014
- const overdueItems = allDocs.filter(
15089
+ const tagOverdueItems = allDocs.filter(
15015
15090
  (d) => d.frontmatter.tags?.includes("overdue")
15016
15091
  );
15092
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15093
+ const dateOverdueActions = openActions.filter((d) => {
15094
+ const dueDate = d.frontmatter.dueDate;
15095
+ return typeof dueDate === "string" && dueDate < today;
15096
+ });
15097
+ const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
15098
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15099
+ );
15017
15100
  const openQuestions = store.list({ type: "question", status: "open" });
15018
15101
  const riskItems = allDocs.filter(
15019
15102
  (d) => d.frontmatter.tags?.includes("risk")
@@ -15122,6 +15205,253 @@ function evaluateGar(projectName, metrics) {
15122
15205
  };
15123
15206
  }
15124
15207
 
15208
+ // src/reports/health/collector.ts
15209
+ var FIELD_CHECKS = [
15210
+ {
15211
+ type: "action",
15212
+ openStatuses: ["open", "in-progress"],
15213
+ requiredFields: ["owner", "priority", "dueDate", "content"]
15214
+ },
15215
+ {
15216
+ type: "decision",
15217
+ openStatuses: ["open", "proposed"],
15218
+ requiredFields: ["owner", "content"]
15219
+ },
15220
+ {
15221
+ type: "question",
15222
+ openStatuses: ["open"],
15223
+ requiredFields: ["owner", "content"]
15224
+ },
15225
+ {
15226
+ type: "feature",
15227
+ openStatuses: ["draft", "approved"],
15228
+ requiredFields: ["owner", "priority", "content"]
15229
+ },
15230
+ {
15231
+ type: "epic",
15232
+ openStatuses: ["planned", "in-progress"],
15233
+ requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
15234
+ },
15235
+ {
15236
+ type: "sprint",
15237
+ openStatuses: ["planned", "active"],
15238
+ requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
15239
+ }
15240
+ ];
15241
+ var STALE_THRESHOLD_DAYS = 14;
15242
+ var AGING_THRESHOLD_DAYS = 30;
15243
+ function daysBetween(a, b) {
15244
+ const msPerDay = 864e5;
15245
+ const dateA = new Date(a);
15246
+ const dateB = new Date(b);
15247
+ return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
15248
+ }
15249
+ function checkMissingFields(doc, requiredFields) {
15250
+ const missing = [];
15251
+ for (const field of requiredFields) {
15252
+ if (field === "content") {
15253
+ if (!doc.content || doc.content.trim().length === 0) {
15254
+ missing.push("content");
15255
+ }
15256
+ } else if (field === "linkedEpics") {
15257
+ const val = doc.frontmatter[field];
15258
+ if (!Array.isArray(val) || val.length === 0) {
15259
+ missing.push(field);
15260
+ }
15261
+ } else {
15262
+ const val = doc.frontmatter[field];
15263
+ if (val === void 0 || val === null || val === "") {
15264
+ missing.push(field);
15265
+ }
15266
+ }
15267
+ }
15268
+ return missing;
15269
+ }
15270
+ function collectCompleteness(store) {
15271
+ const result = {};
15272
+ for (const check2 of FIELD_CHECKS) {
15273
+ const allOfType = store.list({ type: check2.type });
15274
+ const openDocs = allOfType.filter(
15275
+ (d) => check2.openStatuses.includes(d.frontmatter.status)
15276
+ );
15277
+ const gaps = [];
15278
+ let complete = 0;
15279
+ for (const doc of openDocs) {
15280
+ const missingFields = checkMissingFields(doc, check2.requiredFields);
15281
+ if (missingFields.length === 0) {
15282
+ complete++;
15283
+ } else {
15284
+ gaps.push({
15285
+ id: doc.frontmatter.id,
15286
+ title: doc.frontmatter.title,
15287
+ missingFields
15288
+ });
15289
+ }
15290
+ }
15291
+ result[check2.type] = {
15292
+ total: openDocs.length,
15293
+ complete,
15294
+ gaps
15295
+ };
15296
+ }
15297
+ return result;
15298
+ }
15299
+ function collectProcess(store) {
15300
+ const today = (/* @__PURE__ */ new Date()).toISOString();
15301
+ const allDocs = store.list();
15302
+ const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
15303
+ const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
15304
+ const stale = [];
15305
+ for (const doc of openDocs) {
15306
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
15307
+ const days = daysBetween(updated, today);
15308
+ if (days >= STALE_THRESHOLD_DAYS) {
15309
+ stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15310
+ }
15311
+ }
15312
+ const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
15313
+ const agingActions = [];
15314
+ for (const doc of openActions) {
15315
+ const days = daysBetween(doc.frontmatter.created, today);
15316
+ if (days >= AGING_THRESHOLD_DAYS) {
15317
+ agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15318
+ }
15319
+ }
15320
+ const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
15321
+ let decisionTotal = 0;
15322
+ for (const doc of resolvedDecisions) {
15323
+ decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15324
+ }
15325
+ const decisionVelocity = {
15326
+ avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
15327
+ count: resolvedDecisions.length
15328
+ };
15329
+ const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
15330
+ let questionTotal = 0;
15331
+ for (const doc of answeredQuestions) {
15332
+ questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15333
+ }
15334
+ const questionResolution = {
15335
+ avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
15336
+ count: answeredQuestions.length
15337
+ };
15338
+ return { stale, agingActions, decisionVelocity, questionResolution };
15339
+ }
15340
+ function collectHealthMetrics(store) {
15341
+ return {
15342
+ completeness: collectCompleteness(store),
15343
+ process: collectProcess(store)
15344
+ };
15345
+ }
15346
+
15347
+ // src/reports/health/evaluator.ts
15348
+ function worstStatus2(statuses) {
15349
+ if (statuses.includes("red")) return "red";
15350
+ if (statuses.includes("amber")) return "amber";
15351
+ return "green";
15352
+ }
15353
+ function completenessStatus(total, complete) {
15354
+ if (total === 0) return "green";
15355
+ const pct = Math.round(complete / total * 100);
15356
+ if (pct >= 100) return "green";
15357
+ if (pct >= 75) return "amber";
15358
+ return "red";
15359
+ }
15360
+ var TYPE_LABELS = {
15361
+ action: "Actions",
15362
+ decision: "Decisions",
15363
+ question: "Questions",
15364
+ feature: "Features",
15365
+ epic: "Epics",
15366
+ sprint: "Sprints"
15367
+ };
15368
+ function evaluateHealth(projectName, metrics) {
15369
+ const completeness = [];
15370
+ for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
15371
+ const { total, complete, gaps } = catMetrics;
15372
+ const status = completenessStatus(total, complete);
15373
+ const pct = total > 0 ? Math.round(complete / total * 100) : 100;
15374
+ completeness.push({
15375
+ name: TYPE_LABELS[type] ?? type,
15376
+ status,
15377
+ summary: `${pct}% complete (${complete}/${total})`,
15378
+ items: gaps.map((g) => ({
15379
+ id: g.id,
15380
+ detail: `missing: ${g.missingFields.join(", ")}`
15381
+ }))
15382
+ });
15383
+ }
15384
+ const process3 = [];
15385
+ const staleCount = metrics.process.stale.length;
15386
+ const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
15387
+ process3.push({
15388
+ name: "Stale Items",
15389
+ status: staleStatus,
15390
+ summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
15391
+ items: metrics.process.stale.map((s) => ({
15392
+ id: s.id,
15393
+ detail: `${s.days} days since last update`
15394
+ }))
15395
+ });
15396
+ const agingCount = metrics.process.agingActions.length;
15397
+ const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
15398
+ process3.push({
15399
+ name: "Aging Actions",
15400
+ status: agingStatus,
15401
+ summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
15402
+ items: metrics.process.agingActions.map((a) => ({
15403
+ id: a.id,
15404
+ detail: `open for ${a.days} days`
15405
+ }))
15406
+ });
15407
+ const dv = metrics.process.decisionVelocity;
15408
+ let dvStatus;
15409
+ if (dv.count === 0) {
15410
+ dvStatus = "green";
15411
+ } else if (dv.avgDays <= 7) {
15412
+ dvStatus = "green";
15413
+ } else if (dv.avgDays <= 21) {
15414
+ dvStatus = "amber";
15415
+ } else {
15416
+ dvStatus = "red";
15417
+ }
15418
+ process3.push({
15419
+ name: "Decision Velocity",
15420
+ status: dvStatus,
15421
+ summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
15422
+ items: []
15423
+ });
15424
+ const qr = metrics.process.questionResolution;
15425
+ let qrStatus;
15426
+ if (qr.count === 0) {
15427
+ qrStatus = "green";
15428
+ } else if (qr.avgDays <= 7) {
15429
+ qrStatus = "green";
15430
+ } else if (qr.avgDays <= 14) {
15431
+ qrStatus = "amber";
15432
+ } else {
15433
+ qrStatus = "red";
15434
+ }
15435
+ process3.push({
15436
+ name: "Question Resolution",
15437
+ status: qrStatus,
15438
+ summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
15439
+ items: []
15440
+ });
15441
+ const allStatuses = [
15442
+ ...completeness.map((c) => c.status),
15443
+ ...process3.map((p) => p.status)
15444
+ ];
15445
+ const overall = worstStatus2(allStatuses);
15446
+ return {
15447
+ projectName,
15448
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15449
+ overall,
15450
+ completeness,
15451
+ process: process3
15452
+ };
15453
+ }
15454
+
15125
15455
  // src/plugins/builtin/tools/reports.ts
15126
15456
  function createReportTools(store) {
15127
15457
  return [
@@ -15141,7 +15471,8 @@ function createReportTools(store) {
15141
15471
  id: d.frontmatter.id,
15142
15472
  title: d.frontmatter.title,
15143
15473
  owner: d.frontmatter.owner,
15144
- priority: d.frontmatter.priority
15474
+ priority: d.frontmatter.priority,
15475
+ dueDate: d.frontmatter.dueDate
15145
15476
  })),
15146
15477
  completedActions: completedActions.map((d) => ({
15147
15478
  id: d.frontmatter.id,
@@ -15160,7 +15491,7 @@ function createReportTools(store) {
15160
15491
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15161
15492
  };
15162
15493
  },
15163
- { annotations: { readOnly: true } }
15494
+ { annotations: { readOnlyHint: true } }
15164
15495
  ),
15165
15496
  tool8(
15166
15497
  "generate_risk_register",
@@ -15204,7 +15535,7 @@ function createReportTools(store) {
15204
15535
  content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
15205
15536
  };
15206
15537
  },
15207
- { annotations: { readOnly: true } }
15538
+ { annotations: { readOnlyHint: true } }
15208
15539
  ),
15209
15540
  tool8(
15210
15541
  "generate_gar_report",
@@ -15217,7 +15548,7 @@ function createReportTools(store) {
15217
15548
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15218
15549
  };
15219
15550
  },
15220
- { annotations: { readOnly: true } }
15551
+ { annotations: { readOnlyHint: true } }
15221
15552
  ),
15222
15553
  tool8(
15223
15554
  "generate_epic_progress",
@@ -15302,7 +15633,7 @@ function createReportTools(store) {
15302
15633
  ]
15303
15634
  };
15304
15635
  },
15305
- { annotations: { readOnly: true } }
15636
+ { annotations: { readOnlyHint: true } }
15306
15637
  ),
15307
15638
  tool8(
15308
15639
  "generate_sprint_progress",
@@ -15347,7 +15678,8 @@ function createReportTools(store) {
15347
15678
  id: d.frontmatter.id,
15348
15679
  title: d.frontmatter.title,
15349
15680
  type: d.frontmatter.type,
15350
- status: d.frontmatter.status
15681
+ status: d.frontmatter.status,
15682
+ dueDate: d.frontmatter.dueDate
15351
15683
  }))
15352
15684
  }
15353
15685
  };
@@ -15356,7 +15688,7 @@ function createReportTools(store) {
15356
15688
  content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
15357
15689
  };
15358
15690
  },
15359
- { annotations: { readOnly: true } }
15691
+ { annotations: { readOnlyHint: true } }
15360
15692
  ),
15361
15693
  tool8(
15362
15694
  "generate_feature_progress",
@@ -15396,7 +15728,20 @@ function createReportTools(store) {
15396
15728
  content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
15397
15729
  };
15398
15730
  },
15399
- { annotations: { readOnly: true } }
15731
+ { annotations: { readOnlyHint: true } }
15732
+ ),
15733
+ tool8(
15734
+ "generate_health_report",
15735
+ "Generate a governance health check report covering artifact completeness and process health metrics",
15736
+ {},
15737
+ async () => {
15738
+ const metrics = collectHealthMetrics(store);
15739
+ const report = evaluateHealth("project", metrics);
15740
+ return {
15741
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15742
+ };
15743
+ },
15744
+ { annotations: { readOnlyHint: true } }
15400
15745
  ),
15401
15746
  tool8(
15402
15747
  "save_report",
@@ -15458,7 +15803,7 @@ function createFeatureTools(store) {
15458
15803
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15459
15804
  };
15460
15805
  },
15461
- { annotations: { readOnly: true } }
15806
+ { annotations: { readOnlyHint: true } }
15462
15807
  ),
15463
15808
  tool9(
15464
15809
  "get_feature",
@@ -15485,7 +15830,7 @@ function createFeatureTools(store) {
15485
15830
  ]
15486
15831
  };
15487
15832
  },
15488
- { annotations: { readOnly: true } }
15833
+ { annotations: { readOnlyHint: true } }
15489
15834
  ),
15490
15835
  tool9(
15491
15836
  "create_feature",
@@ -15576,7 +15921,7 @@ function createEpicTools(store) {
15576
15921
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15577
15922
  };
15578
15923
  },
15579
- { annotations: { readOnly: true } }
15924
+ { annotations: { readOnlyHint: true } }
15580
15925
  ),
15581
15926
  tool10(
15582
15927
  "get_epic",
@@ -15603,7 +15948,7 @@ function createEpicTools(store) {
15603
15948
  ]
15604
15949
  };
15605
15950
  },
15606
- { annotations: { readOnly: true } }
15951
+ { annotations: { readOnlyHint: true } }
15607
15952
  ),
15608
15953
  tool10(
15609
15954
  "create_epic",
@@ -15735,7 +16080,7 @@ function createContributionTools(store) {
15735
16080
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15736
16081
  };
15737
16082
  },
15738
- { annotations: { readOnly: true } }
16083
+ { annotations: { readOnlyHint: true } }
15739
16084
  ),
15740
16085
  tool11(
15741
16086
  "get_contribution",
@@ -15762,7 +16107,7 @@ function createContributionTools(store) {
15762
16107
  ]
15763
16108
  };
15764
16109
  },
15765
- { annotations: { readOnly: true } }
16110
+ { annotations: { readOnlyHint: true } }
15766
16111
  ),
15767
16112
  tool11(
15768
16113
  "create_contribution",
@@ -15847,7 +16192,7 @@ function createSprintTools(store) {
15847
16192
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15848
16193
  };
15849
16194
  },
15850
- { annotations: { readOnly: true } }
16195
+ { annotations: { readOnlyHint: true } }
15851
16196
  ),
15852
16197
  tool12(
15853
16198
  "get_sprint",
@@ -15874,7 +16219,7 @@ function createSprintTools(store) {
15874
16219
  ]
15875
16220
  };
15876
16221
  },
15877
- { annotations: { readOnly: true } }
16222
+ { annotations: { readOnlyHint: true } }
15878
16223
  ),
15879
16224
  tool12(
15880
16225
  "create_sprint",
@@ -16171,7 +16516,7 @@ function createSprintPlanningTools(store) {
16171
16516
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16172
16517
  };
16173
16518
  },
16174
- { annotations: { readOnly: true } }
16519
+ { annotations: { readOnlyHint: true } }
16175
16520
  )
16176
16521
  ];
16177
16522
  }
@@ -16225,6 +16570,7 @@ var genericAgilePlugin = {
16225
16570
  - Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
16226
16571
  - Use priority levels (critical, high, medium, low) to communicate business value.
16227
16572
  - Tag features for categorization and cross-referencing.
16573
+ - Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
16228
16574
 
16229
16575
  **Contribution Tools:**
16230
16576
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -16255,6 +16601,7 @@ var genericAgilePlugin = {
16255
16601
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
16256
16602
  - Collaborate with the Delivery Manager on target dates and effort estimates.
16257
16603
  - Each epic should have a clear scope and definition of done.
16604
+ - Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
16258
16605
 
16259
16606
  **Contribution Tools:**
16260
16607
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -16314,6 +16661,11 @@ var genericAgilePlugin = {
16314
16661
  - **generate_sprint_progress**: Progress report for a specific sprint or all sprints \u2014 shows linked epics with statuses, work items tagged \`sprint:SP-xxx\` grouped by status, and done/total completion %.
16315
16662
  - Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
16316
16663
 
16664
+ **Date Enforcement:**
16665
+ - Always set \`dueDate\` when creating or updating actions. Use the \`sprints\` parameter to assign actions to sprints \u2014 the tool translates this into \`sprint:SP-xxx\` tags automatically.
16666
+ - When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
16667
+ - Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
16668
+
16317
16669
  **Sprint Workflow:**
16318
16670
  - Create sprints with clear goals and date boundaries.
16319
16671
  - Assign epics to sprints via linkedEpics.
@@ -16333,7 +16685,7 @@ var genericAgilePlugin = {
16333
16685
  **Sprints** (SP-xxx): Time-boxed iterations that group epics and work items with delivery dates. Sprints progress through planned \u2192 active \u2192 completed (or cancelled).
16334
16686
  **Meetings**: Meeting records with attendees, agendas, and notes.
16335
16687
 
16336
- **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags.
16688
+ **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags. Actions support a \`dueDate\` field for schedule tracking \u2014 actions with a past due date are automatically flagged as overdue in GAR reports. Use the \`sprints\` parameter on create_action/update_action to assign actions to sprints.
16337
16689
 
16338
16690
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
16339
16691
  - **create_meeting**: Record meetings with attendees, date, and agenda. The meeting date is required \u2014 extract it from the meeting content or ask the user if not found.
@@ -16379,7 +16731,7 @@ function createUseCaseTools(store) {
16379
16731
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16380
16732
  };
16381
16733
  },
16382
- { annotations: { readOnly: true } }
16734
+ { annotations: { readOnlyHint: true } }
16383
16735
  ),
16384
16736
  tool14(
16385
16737
  "get_use_case",
@@ -16406,7 +16758,7 @@ function createUseCaseTools(store) {
16406
16758
  ]
16407
16759
  };
16408
16760
  },
16409
- { annotations: { readOnly: true } }
16761
+ { annotations: { readOnlyHint: true } }
16410
16762
  ),
16411
16763
  tool14(
16412
16764
  "create_use_case",
@@ -16504,7 +16856,7 @@ function createTechAssessmentTools(store) {
16504
16856
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16505
16857
  };
16506
16858
  },
16507
- { annotations: { readOnly: true } }
16859
+ { annotations: { readOnlyHint: true } }
16508
16860
  ),
16509
16861
  tool15(
16510
16862
  "get_tech_assessment",
@@ -16531,7 +16883,7 @@ function createTechAssessmentTools(store) {
16531
16883
  ]
16532
16884
  };
16533
16885
  },
16534
- { annotations: { readOnly: true } }
16886
+ { annotations: { readOnlyHint: true } }
16535
16887
  ),
16536
16888
  tool15(
16537
16889
  "create_tech_assessment",
@@ -16665,7 +17017,7 @@ function createExtensionDesignTools(store) {
16665
17017
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16666
17018
  };
16667
17019
  },
16668
- { annotations: { readOnly: true } }
17020
+ { annotations: { readOnlyHint: true } }
16669
17021
  ),
16670
17022
  tool16(
16671
17023
  "get_extension_design",
@@ -16692,7 +17044,7 @@ function createExtensionDesignTools(store) {
16692
17044
  ]
16693
17045
  };
16694
17046
  },
16695
- { annotations: { readOnly: true } }
17047
+ { annotations: { readOnlyHint: true } }
16696
17048
  ),
16697
17049
  tool16(
16698
17050
  "create_extension_design",
@@ -16844,7 +17196,7 @@ function createAemReportTools(store) {
16844
17196
  ]
16845
17197
  };
16846
17198
  },
16847
- { annotations: { readOnly: true } }
17199
+ { annotations: { readOnlyHint: true } }
16848
17200
  ),
16849
17201
  tool17(
16850
17202
  "generate_tech_readiness",
@@ -16896,7 +17248,7 @@ function createAemReportTools(store) {
16896
17248
  ]
16897
17249
  };
16898
17250
  },
16899
- { annotations: { readOnly: true } }
17251
+ { annotations: { readOnlyHint: true } }
16900
17252
  ),
16901
17253
  tool17(
16902
17254
  "generate_phase_status",
@@ -16951,7 +17303,7 @@ function createAemReportTools(store) {
16951
17303
  ]
16952
17304
  };
16953
17305
  },
16954
- { annotations: { readOnly: true } }
17306
+ { annotations: { readOnlyHint: true } }
16955
17307
  )
16956
17308
  ];
16957
17309
  }
@@ -16983,7 +17335,7 @@ function createAemPhaseTools(store, marvinDir) {
16983
17335
  ]
16984
17336
  };
16985
17337
  },
16986
- { annotations: { readOnly: true } }
17338
+ { annotations: { readOnlyHint: true } }
16987
17339
  ),
16988
17340
  tool18(
16989
17341
  "advance_phase",
@@ -17428,7 +17780,7 @@ function createJiraTools(store) {
17428
17780
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17429
17781
  };
17430
17782
  },
17431
- { annotations: { readOnly: true } }
17783
+ { annotations: { readOnlyHint: true } }
17432
17784
  ),
17433
17785
  tool19(
17434
17786
  "get_jira_issue",
@@ -17460,7 +17812,7 @@ function createJiraTools(store) {
17460
17812
  ]
17461
17813
  };
17462
17814
  },
17463
- { annotations: { readOnly: true } }
17815
+ { annotations: { readOnlyHint: true } }
17464
17816
  ),
17465
17817
  // --- Jira → Local tools ---
17466
17818
  tool19(
@@ -17934,7 +18286,7 @@ ${fragment}`);
17934
18286
  }
17935
18287
 
17936
18288
  // src/skills/action-tools.ts
17937
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18289
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
17938
18290
 
17939
18291
  // src/skills/action-runner.ts
17940
18292
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -17943,6 +18295,1570 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
17943
18295
  import {
17944
18296
  createSdkMcpServer
17945
18297
  } from "@anthropic-ai/claude-agent-sdk";
18298
+
18299
+ // src/agent/tools/web.ts
18300
+ import * as http2 from "http";
18301
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18302
+
18303
+ // src/web/data.ts
18304
+ function getOverviewData(store) {
18305
+ const types = [];
18306
+ const counts = store.counts();
18307
+ for (const type of store.registeredTypes) {
18308
+ const total = counts[type] ?? 0;
18309
+ const open = store.list({ type, status: "open" }).length;
18310
+ types.push({ type, total, open });
18311
+ }
18312
+ const allDocs = store.list();
18313
+ const sorted = allDocs.sort(
18314
+ (a, b) => (b.frontmatter.updated ?? b.frontmatter.created).localeCompare(
18315
+ a.frontmatter.updated ?? a.frontmatter.created
18316
+ )
18317
+ );
18318
+ return { types, recent: sorted.slice(0, 20) };
18319
+ }
18320
+ function getDocumentListData(store, type, filterStatus, filterOwner) {
18321
+ if (!store.registeredTypes.includes(type)) return void 0;
18322
+ const allOfType = store.list({ type });
18323
+ const statuses = [...new Set(allOfType.map((d) => d.frontmatter.status))].sort();
18324
+ const owners = [
18325
+ ...new Set(allOfType.map((d) => d.frontmatter.owner).filter(Boolean))
18326
+ ].sort();
18327
+ let docs = allOfType;
18328
+ if (filterStatus) {
18329
+ docs = docs.filter((d) => d.frontmatter.status === filterStatus);
18330
+ }
18331
+ if (filterOwner) {
18332
+ docs = docs.filter((d) => d.frontmatter.owner === filterOwner);
18333
+ }
18334
+ docs.sort((a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id));
18335
+ return { type, docs, statuses, owners, filterStatus, filterOwner };
18336
+ }
18337
+ function getDocumentDetail(store, type, id) {
18338
+ if (!store.registeredTypes.includes(type)) return void 0;
18339
+ return store.get(id);
18340
+ }
18341
+ function getGarData(store, projectName) {
18342
+ const metrics = collectGarMetrics(store);
18343
+ return evaluateGar(projectName, metrics);
18344
+ }
18345
+ function getBoardData(store, type) {
18346
+ const docs = type ? store.list({ type }) : store.list();
18347
+ const types = store.registeredTypes;
18348
+ const byStatus = /* @__PURE__ */ new Map();
18349
+ for (const doc of docs) {
18350
+ const status = doc.frontmatter.status;
18351
+ if (!byStatus.has(status)) byStatus.set(status, []);
18352
+ byStatus.get(status).push(doc);
18353
+ }
18354
+ const statusOrder = ["open", "draft", "in-progress", "blocked"];
18355
+ const allStatuses = [...byStatus.keys()];
18356
+ const ordered = [];
18357
+ for (const s of statusOrder) {
18358
+ if (allStatuses.includes(s)) ordered.push(s);
18359
+ }
18360
+ for (const s of allStatuses.sort()) {
18361
+ if (!ordered.includes(s) && s !== "done" && s !== "closed" && s !== "resolved") {
18362
+ ordered.push(s);
18363
+ }
18364
+ }
18365
+ for (const s of ["done", "closed", "resolved"]) {
18366
+ if (allStatuses.includes(s)) ordered.push(s);
18367
+ }
18368
+ const columns = ordered.map((status) => ({
18369
+ status,
18370
+ docs: byStatus.get(status) ?? []
18371
+ }));
18372
+ return { columns, type, types };
18373
+ }
18374
+ function getDiagramData(store) {
18375
+ const allDocs = store.list();
18376
+ const sprints = [];
18377
+ const epics = [];
18378
+ const features = [];
18379
+ const statusCounts = {};
18380
+ for (const doc of allDocs) {
18381
+ const fm = doc.frontmatter;
18382
+ const status = fm.status.toLowerCase();
18383
+ statusCounts[status] = (statusCounts[status] ?? 0) + 1;
18384
+ switch (fm.type) {
18385
+ case "sprint":
18386
+ sprints.push({
18387
+ id: fm.id,
18388
+ title: fm.title,
18389
+ status: fm.status,
18390
+ startDate: fm.startDate,
18391
+ endDate: fm.endDate,
18392
+ linkedEpics: fm.linkedEpics ?? []
18393
+ });
18394
+ break;
18395
+ case "epic":
18396
+ epics.push({
18397
+ id: fm.id,
18398
+ title: fm.title,
18399
+ status: fm.status,
18400
+ linkedFeature: fm.linkedFeature
18401
+ });
18402
+ break;
18403
+ case "feature":
18404
+ features.push({
18405
+ id: fm.id,
18406
+ title: fm.title,
18407
+ status: fm.status
18408
+ });
18409
+ break;
18410
+ }
18411
+ }
18412
+ return { sprints, epics, features, statusCounts };
18413
+ }
18414
+
18415
+ // src/web/templates/layout.ts
18416
+ function escapeHtml(str) {
18417
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18418
+ }
18419
+ function statusBadge(status) {
18420
+ const cls = {
18421
+ open: "badge-open",
18422
+ done: "badge-done",
18423
+ closed: "badge-done",
18424
+ resolved: "badge-resolved",
18425
+ "in-progress": "badge-in-progress",
18426
+ "in progress": "badge-in-progress",
18427
+ draft: "badge-draft",
18428
+ blocked: "badge-blocked"
18429
+ }[status.toLowerCase()] ?? "badge-default";
18430
+ return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
18431
+ }
18432
+ function formatDate(iso) {
18433
+ if (!iso) return "";
18434
+ return iso.slice(0, 10);
18435
+ }
18436
+ function typeLabel(type) {
18437
+ return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
18438
+ }
18439
+ function renderMarkdown(md) {
18440
+ const lines = md.split("\n");
18441
+ const out = [];
18442
+ let inList = false;
18443
+ let listTag = "ul";
18444
+ let inTable = false;
18445
+ let i = 0;
18446
+ while (i < lines.length) {
18447
+ const line = lines[i];
18448
+ if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
18449
+ out.push(`</${listTag}>`);
18450
+ inList = false;
18451
+ }
18452
+ if (inTable && !/^\s*\|/.test(line)) {
18453
+ out.push("</tbody></table></div>");
18454
+ inTable = false;
18455
+ }
18456
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
18457
+ i++;
18458
+ out.push("<hr>");
18459
+ continue;
18460
+ }
18461
+ if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
18462
+ const headers = parseTableRow(line);
18463
+ out.push('<div class="table-wrap"><table><thead><tr>');
18464
+ out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
18465
+ out.push("</tr></thead><tbody>");
18466
+ inTable = true;
18467
+ i += 2;
18468
+ continue;
18469
+ }
18470
+ if (inTable && /^\s*\|/.test(line)) {
18471
+ const cells = parseTableRow(line);
18472
+ out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
18473
+ i++;
18474
+ continue;
18475
+ }
18476
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
18477
+ if (headingMatch) {
18478
+ const level = headingMatch[1].length;
18479
+ out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
18480
+ i++;
18481
+ continue;
18482
+ }
18483
+ const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
18484
+ if (ulMatch) {
18485
+ if (!inList || listTag !== "ul") {
18486
+ if (inList) out.push(`</${listTag}>`);
18487
+ out.push("<ul>");
18488
+ inList = true;
18489
+ listTag = "ul";
18490
+ }
18491
+ out.push(`<li>${inline(ulMatch[1])}</li>`);
18492
+ i++;
18493
+ continue;
18494
+ }
18495
+ const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
18496
+ if (olMatch) {
18497
+ if (!inList || listTag !== "ol") {
18498
+ if (inList) out.push(`</${listTag}>`);
18499
+ out.push("<ol>");
18500
+ inList = true;
18501
+ listTag = "ol";
18502
+ }
18503
+ out.push(`<li>${inline(olMatch[1])}</li>`);
18504
+ i++;
18505
+ continue;
18506
+ }
18507
+ if (line.trim() === "") {
18508
+ if (inList) {
18509
+ out.push(`</${listTag}>`);
18510
+ inList = false;
18511
+ }
18512
+ i++;
18513
+ continue;
18514
+ }
18515
+ out.push(`<p>${inline(line)}</p>`);
18516
+ i++;
18517
+ }
18518
+ if (inList) out.push(`</${listTag}>`);
18519
+ if (inTable) out.push("</tbody></table></div>");
18520
+ return out.join("\n");
18521
+ }
18522
+ function parseTableRow(line) {
18523
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
18524
+ }
18525
+ function inline(text) {
18526
+ let s = escapeHtml(text);
18527
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
18528
+ s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
18529
+ s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
18530
+ s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
18531
+ s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
18532
+ return s;
18533
+ }
18534
+ function layout(opts, body) {
18535
+ const topItems = [
18536
+ { href: "/", label: "Overview" },
18537
+ { href: "/board", label: "Board" },
18538
+ { href: "/gar", label: "GAR Report" },
18539
+ { href: "/health", label: "Health" }
18540
+ ];
18541
+ const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
18542
+ const groupsHtml = opts.navGroups.map((group) => {
18543
+ const links = group.types.map((type) => {
18544
+ const href = `/docs/${type}`;
18545
+ return `<a href="${href}" class="${isActive(href)}">${typeLabel(type)}s</a>`;
18546
+ }).join("\n ");
18547
+ return `
18548
+ <div class="nav-group">
18549
+ <div class="nav-group-label">${escapeHtml(group.label)}</div>
18550
+ ${links}
18551
+ </div>`;
18552
+ }).join("\n");
18553
+ return `<!DOCTYPE html>
18554
+ <html lang="en">
18555
+ <head>
18556
+ <meta charset="UTF-8">
18557
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
18558
+ <title>${escapeHtml(opts.title)} \u2014 Marvin</title>
18559
+ <link rel="stylesheet" href="/styles.css">
18560
+ </head>
18561
+ <body>
18562
+ <div class="shell">
18563
+ <aside class="sidebar">
18564
+ <div class="sidebar-brand">
18565
+ <h1>Marvin</h1>
18566
+ <div class="project-name">${escapeHtml(opts.projectName)}</div>
18567
+ </div>
18568
+ <nav>
18569
+ ${topItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
18570
+ ${groupsHtml}
18571
+ </nav>
18572
+ </aside>
18573
+ <main class="main">
18574
+ <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18575
+ <svg class="icon-expand" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M1 1h5v1.5H3.56l3.72 3.72-1.06 1.06L2.5 3.56V6H1V1zm14 14h-5v-1.5h2.44l-3.72-3.72 1.06-1.06 3.72 3.72V10H15v5z"/></svg>
18576
+ <svg class="icon-collapse" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M6 7H1V5.5h2.44L0.22 2.28l1.06-1.06L4.5 4.44V2H6v5zm4-1h5v1.5h-2.44l3.22 3.22-1.06 1.06L11.5 8.56V11H10V6z"/></svg>
18577
+ </button>
18578
+ ${body}
18579
+ </main>
18580
+ </div>
18581
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18582
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
18583
+ </body>
18584
+ </html>`;
18585
+ }
18586
+
18587
+ // src/web/templates/styles.ts
18588
+ function renderStyles() {
18589
+ return `
18590
+ :root {
18591
+ --bg: #0f1117;
18592
+ --bg-card: #1a1d27;
18593
+ --bg-hover: #222632;
18594
+ --border: #2a2e3a;
18595
+ --text: #e1e4ea;
18596
+ --text-dim: #8b8fa4;
18597
+ --accent: #6c8cff;
18598
+ --accent-dim: #4a6ad4;
18599
+ --green: #34d399;
18600
+ --amber: #fbbf24;
18601
+ --red: #f87171;
18602
+ --radius: 8px;
18603
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
18604
+ --mono: "SF Mono", "Fira Code", monospace;
18605
+ }
18606
+
18607
+ * { margin: 0; padding: 0; box-sizing: border-box; }
18608
+
18609
+ body {
18610
+ font-family: var(--font);
18611
+ background: var(--bg);
18612
+ color: var(--text);
18613
+ line-height: 1.6;
18614
+ min-height: 100vh;
18615
+ }
18616
+
18617
+ a { color: var(--accent); text-decoration: none; }
18618
+ a:hover { text-decoration: underline; }
18619
+
18620
+ /* Layout */
18621
+ .shell {
18622
+ display: flex;
18623
+ min-height: 100vh;
18624
+ }
18625
+
18626
+ .sidebar {
18627
+ width: 220px;
18628
+ background: var(--bg-card);
18629
+ border-right: 1px solid var(--border);
18630
+ padding: 1.5rem 0;
18631
+ position: fixed;
18632
+ top: 0;
18633
+ left: 0;
18634
+ bottom: 0;
18635
+ overflow-y: auto;
18636
+ }
18637
+
18638
+ .sidebar-brand {
18639
+ padding: 0 1.25rem 1.25rem;
18640
+ border-bottom: 1px solid var(--border);
18641
+ margin-bottom: 1rem;
18642
+ }
18643
+
18644
+ .sidebar-brand h1 {
18645
+ font-size: 1.1rem;
18646
+ font-weight: 700;
18647
+ color: var(--accent);
18648
+ letter-spacing: -0.02em;
18649
+ }
18650
+
18651
+ .sidebar-brand .project-name {
18652
+ font-size: 0.75rem;
18653
+ color: var(--text-dim);
18654
+ margin-top: 0.25rem;
18655
+ }
18656
+
18657
+ .sidebar nav a {
18658
+ display: block;
18659
+ padding: 0.5rem 1.25rem;
18660
+ color: var(--text-dim);
18661
+ font-size: 0.875rem;
18662
+ transition: background 0.15s, color 0.15s;
18663
+ }
18664
+
18665
+ .sidebar nav a:hover {
18666
+ background: var(--bg-hover);
18667
+ color: var(--text);
18668
+ text-decoration: none;
18669
+ }
18670
+
18671
+ .sidebar nav a.active {
18672
+ color: var(--accent);
18673
+ background: rgba(108, 140, 255, 0.08);
18674
+ border-right: 2px solid var(--accent);
18675
+ }
18676
+
18677
+ .nav-group {
18678
+ margin-top: 0.75rem;
18679
+ padding-top: 0.75rem;
18680
+ border-top: 1px solid var(--border);
18681
+ }
18682
+
18683
+ .nav-group-label {
18684
+ padding: 0.25rem 1.25rem 0.25rem;
18685
+ font-size: 0.65rem;
18686
+ text-transform: uppercase;
18687
+ letter-spacing: 0.08em;
18688
+ color: var(--text-dim);
18689
+ font-weight: 600;
18690
+ }
18691
+
18692
+ .main {
18693
+ margin-left: 220px;
18694
+ flex: 1;
18695
+ padding: 2rem 2.5rem;
18696
+ max-width: 1200px;
18697
+ position: relative;
18698
+ transition: max-width 0.2s ease;
18699
+ }
18700
+ .main.expanded {
18701
+ max-width: none;
18702
+ }
18703
+ .expand-toggle {
18704
+ position: absolute;
18705
+ top: 1rem;
18706
+ right: 1rem;
18707
+ background: var(--bg-card);
18708
+ border: 1px solid var(--border);
18709
+ border-radius: var(--radius);
18710
+ color: var(--text-dim);
18711
+ cursor: pointer;
18712
+ padding: 0.4rem;
18713
+ display: flex;
18714
+ align-items: center;
18715
+ justify-content: center;
18716
+ transition: color 0.15s, border-color 0.15s;
18717
+ }
18718
+ .expand-toggle:hover {
18719
+ color: var(--text);
18720
+ border-color: var(--text-dim);
18721
+ }
18722
+ .main.expanded .icon-expand { display: none; }
18723
+ .main:not(.expanded) .icon-collapse { display: none; }
18724
+
18725
+ /* Page header */
18726
+ .page-header {
18727
+ margin-bottom: 2rem;
18728
+ }
18729
+
18730
+ .page-header h2 {
18731
+ font-size: 1.5rem;
18732
+ font-weight: 600;
18733
+ }
18734
+
18735
+ .page-header .subtitle {
18736
+ color: var(--text-dim);
18737
+ font-size: 0.875rem;
18738
+ margin-top: 0.25rem;
18739
+ }
18740
+
18741
+ /* Breadcrumb */
18742
+ .breadcrumb {
18743
+ font-size: 0.8rem;
18744
+ color: var(--text-dim);
18745
+ margin-bottom: 1rem;
18746
+ }
18747
+
18748
+ .breadcrumb a { color: var(--text-dim); }
18749
+ .breadcrumb a:hover { color: var(--accent); }
18750
+ .breadcrumb .sep { margin: 0 0.4rem; }
18751
+
18752
+ /* Card groups */
18753
+ .card-group {
18754
+ margin-bottom: 1.5rem;
18755
+ }
18756
+
18757
+ .card-group-label {
18758
+ font-size: 0.7rem;
18759
+ text-transform: uppercase;
18760
+ letter-spacing: 0.08em;
18761
+ color: var(--text-dim);
18762
+ font-weight: 600;
18763
+ margin-bottom: 0.5rem;
18764
+ }
18765
+
18766
+ /* Cards grid */
18767
+ .cards {
18768
+ display: grid;
18769
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
18770
+ gap: 1rem;
18771
+ margin-bottom: 0.5rem;
18772
+ }
18773
+
18774
+ .card {
18775
+ background: var(--bg-card);
18776
+ border: 1px solid var(--border);
18777
+ border-radius: var(--radius);
18778
+ padding: 1.25rem;
18779
+ transition: border-color 0.15s;
18780
+ }
18781
+
18782
+ .card:hover {
18783
+ border-color: var(--accent-dim);
18784
+ }
18785
+
18786
+ .card a { color: inherit; text-decoration: none; display: block; }
18787
+
18788
+ .card .card-label {
18789
+ font-size: 0.75rem;
18790
+ text-transform: uppercase;
18791
+ letter-spacing: 0.05em;
18792
+ color: var(--text-dim);
18793
+ margin-bottom: 0.5rem;
18794
+ }
18795
+
18796
+ .card .card-value {
18797
+ font-size: 1.75rem;
18798
+ font-weight: 700;
18799
+ }
18800
+
18801
+ .card .card-sub {
18802
+ font-size: 0.8rem;
18803
+ color: var(--text-dim);
18804
+ margin-top: 0.25rem;
18805
+ }
18806
+
18807
+ /* Status badge */
18808
+ .badge {
18809
+ display: inline-block;
18810
+ padding: 0.15rem 0.6rem;
18811
+ border-radius: 999px;
18812
+ font-size: 0.7rem;
18813
+ font-weight: 600;
18814
+ text-transform: uppercase;
18815
+ letter-spacing: 0.03em;
18816
+ }
18817
+
18818
+ .badge-open { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
18819
+ .badge-done { background: rgba(52, 211, 153, 0.15); color: var(--green); }
18820
+ .badge-in-progress { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
18821
+ .badge-draft { background: rgba(139, 143, 164, 0.15); color: var(--text-dim); }
18822
+ .badge-closed, .badge-resolved { background: rgba(52, 211, 153, 0.15); color: var(--green); }
18823
+ .badge-blocked { background: rgba(248, 113, 113, 0.15); color: var(--red); }
18824
+ .badge-default { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
18825
+
18826
+ /* Table */
18827
+ .table-wrap {
18828
+ overflow-x: auto;
18829
+ }
18830
+
18831
+ table {
18832
+ width: 100%;
18833
+ border-collapse: collapse;
18834
+ }
18835
+
18836
+ th {
18837
+ text-align: left;
18838
+ padding: 0.6rem 0.75rem;
18839
+ font-size: 0.7rem;
18840
+ text-transform: uppercase;
18841
+ letter-spacing: 0.05em;
18842
+ color: var(--text-dim);
18843
+ border-bottom: 1px solid var(--border);
18844
+ }
18845
+
18846
+ td {
18847
+ padding: 0.6rem 0.75rem;
18848
+ font-size: 0.875rem;
18849
+ border-bottom: 1px solid var(--border);
18850
+ }
18851
+
18852
+ tr:hover td {
18853
+ background: var(--bg-hover);
18854
+ }
18855
+
18856
+ /* GAR */
18857
+ .gar-overall {
18858
+ text-align: center;
18859
+ padding: 2rem;
18860
+ margin-bottom: 2rem;
18861
+ border-radius: var(--radius);
18862
+ border: 1px solid var(--border);
18863
+ background: var(--bg-card);
18864
+ }
18865
+
18866
+ .gar-overall .dot {
18867
+ width: 60px;
18868
+ height: 60px;
18869
+ border-radius: 50%;
18870
+ display: inline-block;
18871
+ margin-bottom: 0.75rem;
18872
+ }
18873
+
18874
+ .gar-overall .label {
18875
+ font-size: 1.1rem;
18876
+ font-weight: 600;
18877
+ text-transform: uppercase;
18878
+ }
18879
+
18880
+ .gar-areas {
18881
+ display: grid;
18882
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
18883
+ gap: 1rem;
18884
+ }
18885
+
18886
+ .gar-area {
18887
+ background: var(--bg-card);
18888
+ border: 1px solid var(--border);
18889
+ border-radius: var(--radius);
18890
+ padding: 1.25rem;
18891
+ }
18892
+
18893
+ .gar-area .area-header {
18894
+ display: flex;
18895
+ align-items: center;
18896
+ gap: 0.6rem;
18897
+ margin-bottom: 0.75rem;
18898
+ }
18899
+
18900
+ .gar-area .area-dot {
18901
+ width: 14px;
18902
+ height: 14px;
18903
+ border-radius: 50%;
18904
+ flex-shrink: 0;
18905
+ }
18906
+
18907
+ .gar-area .area-name {
18908
+ font-weight: 600;
18909
+ font-size: 1rem;
18910
+ }
18911
+
18912
+ .gar-area .area-summary {
18913
+ font-size: 0.85rem;
18914
+ color: var(--text-dim);
18915
+ margin-bottom: 0.75rem;
18916
+ }
18917
+
18918
+ .gar-area ul {
18919
+ list-style: none;
18920
+ font-size: 0.8rem;
18921
+ }
18922
+
18923
+ .gar-area li {
18924
+ padding: 0.2rem 0;
18925
+ color: var(--text-dim);
18926
+ }
18927
+
18928
+ .gar-area li .ref-id {
18929
+ color: var(--accent);
18930
+ font-family: var(--mono);
18931
+ margin-right: 0.4rem;
18932
+ }
18933
+
18934
+ .dot-green { background: var(--green); }
18935
+ .dot-amber { background: var(--amber); }
18936
+ .dot-red { background: var(--red); }
18937
+
18938
+ /* Board / Kanban */
18939
+ .board {
18940
+ display: flex;
18941
+ gap: 1rem;
18942
+ overflow-x: auto;
18943
+ padding-bottom: 1rem;
18944
+ }
18945
+
18946
+ .board-column {
18947
+ min-width: 240px;
18948
+ max-width: 300px;
18949
+ flex: 1;
18950
+ }
18951
+
18952
+ .board-column-header {
18953
+ font-size: 0.75rem;
18954
+ text-transform: uppercase;
18955
+ letter-spacing: 0.05em;
18956
+ color: var(--text-dim);
18957
+ padding: 0.5rem 0.75rem;
18958
+ border-bottom: 2px solid var(--border);
18959
+ margin-bottom: 0.5rem;
18960
+ display: flex;
18961
+ justify-content: space-between;
18962
+ }
18963
+
18964
+ .board-column-header .count {
18965
+ background: var(--bg-hover);
18966
+ padding: 0 0.5rem;
18967
+ border-radius: 999px;
18968
+ font-size: 0.7rem;
18969
+ }
18970
+
18971
+ .board-card {
18972
+ background: var(--bg-card);
18973
+ border: 1px solid var(--border);
18974
+ border-radius: var(--radius);
18975
+ padding: 0.75rem;
18976
+ margin-bottom: 0.5rem;
18977
+ transition: border-color 0.15s;
18978
+ }
18979
+
18980
+ .board-card:hover {
18981
+ border-color: var(--accent-dim);
18982
+ }
18983
+
18984
+ .board-card .bc-id {
18985
+ font-family: var(--mono);
18986
+ font-size: 0.7rem;
18987
+ color: var(--accent);
18988
+ }
18989
+
18990
+ .board-card .bc-title {
18991
+ font-size: 0.85rem;
18992
+ margin: 0.25rem 0;
18993
+ }
18994
+
18995
+ .board-card .bc-owner {
18996
+ font-size: 0.7rem;
18997
+ color: var(--text-dim);
18998
+ }
18999
+
19000
+ /* Detail page */
19001
+ .detail-meta {
19002
+ background: var(--bg-card);
19003
+ border: 1px solid var(--border);
19004
+ border-radius: var(--radius);
19005
+ padding: 1.25rem;
19006
+ margin-bottom: 1.5rem;
19007
+ }
19008
+
19009
+ .detail-meta dl {
19010
+ display: grid;
19011
+ grid-template-columns: 120px 1fr;
19012
+ gap: 0.4rem 1rem;
19013
+ }
19014
+
19015
+ .detail-meta dt {
19016
+ font-size: 0.75rem;
19017
+ text-transform: uppercase;
19018
+ letter-spacing: 0.05em;
19019
+ color: var(--text-dim);
19020
+ }
19021
+
19022
+ .detail-meta dd {
19023
+ font-size: 0.875rem;
19024
+ }
19025
+
19026
+ .detail-content {
19027
+ background: var(--bg-card);
19028
+ border: 1px solid var(--border);
19029
+ border-radius: var(--radius);
19030
+ padding: 1.5rem;
19031
+ line-height: 1.7;
19032
+ }
19033
+
19034
+ .detail-content h1, .detail-content h2, .detail-content h3 {
19035
+ margin: 1.25rem 0 0.5rem;
19036
+ font-weight: 600;
19037
+ }
19038
+
19039
+ .detail-content h1 { font-size: 1.3rem; }
19040
+ .detail-content h2 { font-size: 1.15rem; }
19041
+ .detail-content h3 { font-size: 1rem; }
19042
+ .detail-content p { margin-bottom: 0.75rem; }
19043
+ .detail-content ul, .detail-content ol { margin: 0.5rem 0 0.75rem 1.5rem; }
19044
+ .detail-content li { margin-bottom: 0.25rem; }
19045
+ .detail-content code {
19046
+ background: var(--bg-hover);
19047
+ padding: 0.1rem 0.35rem;
19048
+ border-radius: 3px;
19049
+ font-family: var(--mono);
19050
+ font-size: 0.85em;
19051
+ }
19052
+ .detail-content hr {
19053
+ border: none;
19054
+ border-top: 1px solid var(--border);
19055
+ margin: 1.25rem 0;
19056
+ }
19057
+ .detail-content .table-wrap {
19058
+ margin: 0.75rem 0;
19059
+ }
19060
+
19061
+ /* Filters */
19062
+ .filters {
19063
+ display: flex;
19064
+ gap: 0.75rem;
19065
+ margin-bottom: 1.5rem;
19066
+ flex-wrap: wrap;
19067
+ }
19068
+
19069
+ .filters select {
19070
+ background: var(--bg-card);
19071
+ border: 1px solid var(--border);
19072
+ color: var(--text);
19073
+ padding: 0.4rem 0.75rem;
19074
+ border-radius: var(--radius);
19075
+ font-size: 0.8rem;
19076
+ cursor: pointer;
19077
+ }
19078
+
19079
+ .filters select:focus {
19080
+ outline: none;
19081
+ border-color: var(--accent);
19082
+ }
19083
+
19084
+ /* Empty state */
19085
+ .empty {
19086
+ text-align: center;
19087
+ padding: 3rem;
19088
+ color: var(--text-dim);
19089
+ }
19090
+
19091
+ .empty p { font-size: 0.9rem; }
19092
+
19093
+ /* Section heading */
19094
+ .section-title {
19095
+ font-size: 0.9rem;
19096
+ font-weight: 600;
19097
+ margin: 1.5rem 0 0.75rem;
19098
+ }
19099
+
19100
+ /* Priority */
19101
+ .priority-high { color: var(--red); }
19102
+ .priority-medium { color: var(--amber); }
19103
+ .priority-low { color: var(--green); }
19104
+
19105
+ /* Health */
19106
+ .health-section-title {
19107
+ font-size: 1.1rem;
19108
+ font-weight: 600;
19109
+ margin: 2rem 0 1rem;
19110
+ color: var(--text);
19111
+ }
19112
+
19113
+ /* Mermaid diagrams */
19114
+ .mermaid-container {
19115
+ background: var(--bg-card);
19116
+ border: 1px solid var(--border);
19117
+ border-radius: var(--radius);
19118
+ padding: 1.5rem;
19119
+ margin: 1rem 0;
19120
+ overflow-x: auto;
19121
+ }
19122
+
19123
+ .mermaid-container .mermaid {
19124
+ display: flex;
19125
+ justify-content: center;
19126
+ }
19127
+
19128
+ .mermaid-empty {
19129
+ text-align: center;
19130
+ color: var(--text-dim);
19131
+ font-size: 0.875rem;
19132
+ }
19133
+
19134
+ .mermaid-row {
19135
+ display: grid;
19136
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
19137
+ gap: 1rem;
19138
+ }
19139
+
19140
+ .mermaid-row .mermaid-container {
19141
+ margin: 0;
19142
+ }
19143
+ `;
19144
+ }
19145
+
19146
+ // src/web/templates/mermaid.ts
19147
+ function sanitize(text, maxLen = 40) {
19148
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
19149
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19150
+ }
19151
+ function mermaidBlock(definition) {
19152
+ return `<div class="mermaid-container"><pre class="mermaid">
19153
+ ${definition}
19154
+ </pre></div>`;
19155
+ }
19156
+ function placeholder(message) {
19157
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19158
+ }
19159
+ function buildTimelineGantt(data) {
19160
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
19161
+ if (sprintsWithDates.length === 0) {
19162
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
19163
+ }
19164
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
19165
+ const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
19166
+ for (const sprint of sprintsWithDates) {
19167
+ lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
19168
+ const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
19169
+ if (linked.length === 0) {
19170
+ lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
19171
+ } else {
19172
+ for (const epic of linked) {
19173
+ const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
19174
+ lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
19175
+ }
19176
+ }
19177
+ }
19178
+ return mermaidBlock(lines.join("\n"));
19179
+ }
19180
+ function buildArtifactFlowchart(data) {
19181
+ if (data.features.length === 0 && data.epics.length === 0) {
19182
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
19183
+ }
19184
+ const lines = ["graph TD"];
19185
+ lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
19186
+ lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
19187
+ lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
19188
+ lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
19189
+ const nodeIds = /* @__PURE__ */ new Set();
19190
+ for (const epic of data.epics) {
19191
+ if (epic.linkedFeature) {
19192
+ const feature = data.features.find((f) => f.id === epic.linkedFeature);
19193
+ if (feature) {
19194
+ const fNode = feature.id.replace(/-/g, "_");
19195
+ const eNode = epic.id.replace(/-/g, "_");
19196
+ if (!nodeIds.has(fNode)) {
19197
+ lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
19198
+ nodeIds.add(fNode);
19199
+ }
19200
+ if (!nodeIds.has(eNode)) {
19201
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19202
+ nodeIds.add(eNode);
19203
+ }
19204
+ lines.push(` ${fNode} --> ${eNode}`);
19205
+ }
19206
+ }
19207
+ }
19208
+ for (const sprint of data.sprints) {
19209
+ const sNode = sprint.id.replace(/-/g, "_");
19210
+ for (const epicId of sprint.linkedEpics) {
19211
+ const epic = data.epics.find((e) => e.id === epicId);
19212
+ if (epic) {
19213
+ const eNode = epic.id.replace(/-/g, "_");
19214
+ if (!nodeIds.has(eNode)) {
19215
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19216
+ nodeIds.add(eNode);
19217
+ }
19218
+ if (!nodeIds.has(sNode)) {
19219
+ lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
19220
+ nodeIds.add(sNode);
19221
+ }
19222
+ lines.push(` ${eNode} --> ${sNode}`);
19223
+ }
19224
+ }
19225
+ }
19226
+ if (nodeIds.size === 0) {
19227
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
19228
+ }
19229
+ const allItems = [
19230
+ ...data.features.map((f) => ({ id: f.id, status: f.status })),
19231
+ ...data.epics.map((e) => ({ id: e.id, status: e.status })),
19232
+ ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
19233
+ ];
19234
+ for (const item of allItems) {
19235
+ const node = item.id.replace(/-/g, "_");
19236
+ if (!nodeIds.has(node)) continue;
19237
+ const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
19238
+ if (cls) {
19239
+ lines.push(` class ${node} ${cls}`);
19240
+ }
19241
+ }
19242
+ return mermaidBlock(lines.join("\n"));
19243
+ }
19244
+ function buildStatusPie(title, counts) {
19245
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
19246
+ if (entries.length === 0) {
19247
+ return placeholder(`No data for ${title}.`);
19248
+ }
19249
+ const lines = [`pie title ${sanitize(title, 60)}`];
19250
+ for (const [label, count] of entries) {
19251
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
19252
+ }
19253
+ return mermaidBlock(lines.join("\n"));
19254
+ }
19255
+ function buildHealthGauge(categories) {
19256
+ const valid = categories.filter((c) => c.total > 0);
19257
+ if (valid.length === 0) {
19258
+ return placeholder("No completeness data available.");
19259
+ }
19260
+ const pies = valid.map((cat) => {
19261
+ const incomplete = cat.total - cat.complete;
19262
+ const lines = [
19263
+ `pie title ${sanitize(cat.name, 30)}`,
19264
+ ` "Complete" : ${cat.complete}`,
19265
+ ` "Incomplete" : ${incomplete}`
19266
+ ];
19267
+ return mermaidBlock(lines.join("\n"));
19268
+ });
19269
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
19270
+ }
19271
+
19272
+ // src/web/templates/pages/overview.ts
19273
+ function renderCard(t) {
19274
+ return `
19275
+ <div class="card">
19276
+ <a href="/docs/${t.type}">
19277
+ <div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
19278
+ <div class="card-value">${t.total}</div>
19279
+ ${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
19280
+ </a>
19281
+ </div>`;
19282
+ }
19283
+ function overviewPage(data, diagrams, navGroups) {
19284
+ const typeMap = new Map(data.types.map((t) => [t.type, t]));
19285
+ const placed = /* @__PURE__ */ new Set();
19286
+ const groupSections = navGroups.map((group) => {
19287
+ const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
19288
+ placed.add(type);
19289
+ return renderCard(typeMap.get(type));
19290
+ });
19291
+ if (groupCards.length === 0) return "";
19292
+ return `
19293
+ <div class="card-group">
19294
+ <div class="card-group-label">${escapeHtml(group.label)}</div>
19295
+ <div class="cards">${groupCards.join("\n")}</div>
19296
+ </div>`;
19297
+ }).filter(Boolean).join("\n");
19298
+ const ungrouped = data.types.filter((t) => !placed.has(t.type));
19299
+ const ungroupedSection = ungrouped.length > 0 ? `
19300
+ <div class="card-group">
19301
+ <div class="card-group-label">Other</div>
19302
+ <div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
19303
+ </div>` : "";
19304
+ const rows = data.recent.map(
19305
+ (doc) => `
19306
+ <tr>
19307
+ <td><a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
19308
+ <td>${escapeHtml(doc.frontmatter.title)}</td>
19309
+ <td>${escapeHtml(typeLabel(doc.frontmatter.type))}</td>
19310
+ <td>${statusBadge(doc.frontmatter.status)}</td>
19311
+ <td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
19312
+ </tr>`
19313
+ ).join("\n");
19314
+ return `
19315
+ <div class="page-header">
19316
+ <h2>Project Overview</h2>
19317
+ </div>
19318
+
19319
+ ${groupSections}
19320
+ ${ungroupedSection}
19321
+
19322
+ <div class="section-title">Project Timeline</div>
19323
+ ${buildTimelineGantt(diagrams)}
19324
+
19325
+ <div class="section-title">Artifact Relationships</div>
19326
+ ${buildArtifactFlowchart(diagrams)}
19327
+
19328
+ <div class="section-title">Recent Activity</div>
19329
+ ${data.recent.length > 0 ? `
19330
+ <div class="table-wrap">
19331
+ <table>
19332
+ <thead>
19333
+ <tr>
19334
+ <th>ID</th>
19335
+ <th>Title</th>
19336
+ <th>Type</th>
19337
+ <th>Status</th>
19338
+ <th>Updated</th>
19339
+ </tr>
19340
+ </thead>
19341
+ <tbody>
19342
+ ${rows}
19343
+ </tbody>
19344
+ </table>
19345
+ </div>` : `<div class="empty"><p>No documents yet.</p></div>`}
19346
+ `;
19347
+ }
19348
+
19349
+ // src/web/templates/pages/documents.ts
19350
+ function documentsPage(data) {
19351
+ const label = typeLabel(data.type);
19352
+ const statusOptions = data.statuses.map(
19353
+ (s) => `<option value="${escapeHtml(s)}"${data.filterStatus === s ? " selected" : ""}>${escapeHtml(s)}</option>`
19354
+ ).join("");
19355
+ const ownerOptions = data.owners.map(
19356
+ (o) => `<option value="${escapeHtml(o)}"${data.filterOwner === o ? " selected" : ""}>${escapeHtml(o)}</option>`
19357
+ ).join("");
19358
+ const rows = data.docs.map(
19359
+ (doc) => `
19360
+ <tr>
19361
+ <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
19362
+ <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a></td>
19363
+ <td>${statusBadge(doc.frontmatter.status)}</td>
19364
+ <td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
19365
+ <td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
19366
+ <td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
19367
+ </tr>`
19368
+ ).join("\n");
19369
+ return `
19370
+ <div class="page-header">
19371
+ <h2>${escapeHtml(label)}s</h2>
19372
+ <div class="subtitle">${data.docs.length} document${data.docs.length !== 1 ? "s" : ""}</div>
19373
+ </div>
19374
+
19375
+ <div class="filters">
19376
+ <select onchange="filterByStatus(this.value)">
19377
+ <option value="">All statuses</option>
19378
+ ${statusOptions}
19379
+ </select>
19380
+ <select onchange="filterByOwner(this.value)">
19381
+ <option value="">All owners</option>
19382
+ ${ownerOptions}
19383
+ </select>
19384
+ </div>
19385
+
19386
+ ${data.docs.length > 0 ? `
19387
+ <div class="table-wrap">
19388
+ <table>
19389
+ <thead>
19390
+ <tr>
19391
+ <th>ID</th>
19392
+ <th>Title</th>
19393
+ <th>Status</th>
19394
+ <th>Owner</th>
19395
+ <th>Priority</th>
19396
+ <th>Updated</th>
19397
+ </tr>
19398
+ </thead>
19399
+ <tbody>
19400
+ ${rows}
19401
+ </tbody>
19402
+ </table>
19403
+ </div>` : `<div class="empty"><p>No ${label.toLowerCase()}s found.</p></div>`}
19404
+
19405
+ <script>
19406
+ function filterByStatus(status) {
19407
+ const url = new URL(window.location);
19408
+ if (status) url.searchParams.set('status', status);
19409
+ else url.searchParams.delete('status');
19410
+ window.location = url;
19411
+ }
19412
+ function filterByOwner(owner) {
19413
+ const url = new URL(window.location);
19414
+ if (owner) url.searchParams.set('owner', owner);
19415
+ else url.searchParams.delete('owner');
19416
+ window.location = url;
19417
+ }
19418
+ </script>
19419
+ `;
19420
+ }
19421
+
19422
+ // src/web/templates/pages/document-detail.ts
19423
+ function documentDetailPage(doc) {
19424
+ const fm = doc.frontmatter;
19425
+ const label = typeLabel(fm.type);
19426
+ const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
19427
+ const entries = Object.entries(fm).filter(
19428
+ ([key]) => !skipKeys.has(key) && fm[key] != null
19429
+ );
19430
+ const dtDd = entries.map(([key, value]) => {
19431
+ let rendered;
19432
+ if (key === "status") {
19433
+ rendered = statusBadge(value);
19434
+ } else if (key === "tags" && Array.isArray(value)) {
19435
+ rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
19436
+ } else if (key === "created" || key === "updated") {
19437
+ rendered = formatDate(value);
19438
+ } else {
19439
+ rendered = escapeHtml(String(value));
19440
+ }
19441
+ return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
19442
+ }).join("\n ");
19443
+ return `
19444
+ <div class="breadcrumb">
19445
+ <a href="/">Overview</a><span class="sep">/</span>
19446
+ <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
19447
+ ${escapeHtml(fm.id)}
19448
+ </div>
19449
+
19450
+ <div class="page-header">
19451
+ <h2>${escapeHtml(fm.title)}</h2>
19452
+ <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
19453
+ </div>
19454
+
19455
+ <div class="detail-meta">
19456
+ <dl>
19457
+ ${dtDd}
19458
+ </dl>
19459
+ </div>
19460
+
19461
+ ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
19462
+ `;
19463
+ }
19464
+
19465
+ // src/web/templates/pages/gar.ts
19466
+ function garPage(report) {
19467
+ const dotClass = `dot-${report.overall}`;
19468
+ const areaCards = report.areas.map(
19469
+ (area) => `
19470
+ <div class="gar-area">
19471
+ <div class="area-header">
19472
+ <div class="area-dot dot-${area.status}"></div>
19473
+ <div class="area-name">${escapeHtml(area.name)}</div>
19474
+ </div>
19475
+ <div class="area-summary">${escapeHtml(area.summary)}</div>
19476
+ ${area.items.length > 0 ? `<ul>${area.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.title)}</li>`).join("")}</ul>` : ""}
19477
+ </div>`
19478
+ ).join("\n");
19479
+ return `
19480
+ <div class="page-header">
19481
+ <h2>GAR Report</h2>
19482
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
19483
+ </div>
19484
+
19485
+ <div class="gar-overall">
19486
+ <div class="dot ${dotClass}"></div>
19487
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
19488
+ </div>
19489
+
19490
+ <div class="gar-areas">
19491
+ ${areaCards}
19492
+ </div>
19493
+
19494
+ <div class="section-title">Status Distribution</div>
19495
+ ${buildStatusPie("Action Status", {
19496
+ Open: report.metrics.scope.open,
19497
+ Done: report.metrics.scope.done,
19498
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
19499
+ })}
19500
+ `;
19501
+ }
19502
+
19503
+ // src/web/templates/pages/health.ts
19504
+ function healthPage(report, metrics) {
19505
+ const dotClass = `dot-${report.overall}`;
19506
+ function renderSection(title, categories) {
19507
+ const cards = categories.map(
19508
+ (cat) => `
19509
+ <div class="gar-area">
19510
+ <div class="area-header">
19511
+ <div class="area-dot dot-${cat.status}"></div>
19512
+ <div class="area-name">${escapeHtml(cat.name)}</div>
19513
+ </div>
19514
+ <div class="area-summary">${escapeHtml(cat.summary)}</div>
19515
+ ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
19516
+ </div>`
19517
+ ).join("\n");
19518
+ return `
19519
+ <div class="health-section-title">${escapeHtml(title)}</div>
19520
+ <div class="gar-areas">${cards}</div>
19521
+ `;
19522
+ }
19523
+ return `
19524
+ <div class="page-header">
19525
+ <h2>Governance Health Check</h2>
19526
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
19527
+ </div>
19528
+
19529
+ <div class="gar-overall">
19530
+ <div class="dot ${dotClass}"></div>
19531
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
19532
+ </div>
19533
+
19534
+ ${renderSection("Completeness", report.completeness)}
19535
+
19536
+ <div class="health-section-title">Completeness Overview</div>
19537
+ ${buildHealthGauge(
19538
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
19539
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
19540
+ complete: cat.complete,
19541
+ total: cat.total
19542
+ })) : report.completeness.map((c) => {
19543
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
19544
+ return {
19545
+ name: c.name,
19546
+ complete: match ? parseInt(match[1], 10) : 0,
19547
+ total: match ? parseInt(match[2], 10) : 0
19548
+ };
19549
+ })
19550
+ )}
19551
+
19552
+ ${renderSection("Process", report.process)}
19553
+
19554
+ <div class="health-section-title">Process Summary</div>
19555
+ ${metrics ? buildStatusPie("Process Health", {
19556
+ Stale: metrics.process.stale.length,
19557
+ "Aging Actions": metrics.process.agingActions.length,
19558
+ Healthy: Math.max(
19559
+ 0,
19560
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
19561
+ )
19562
+ }) : ""}
19563
+ `;
19564
+ }
19565
+
19566
+ // src/web/templates/pages/board.ts
19567
+ function boardPage(data) {
19568
+ const typeOptions = data.types.map(
19569
+ (t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(typeLabel(t))}s</option>`
19570
+ ).join("");
19571
+ const columns = data.columns.map(
19572
+ (col) => `
19573
+ <div class="board-column">
19574
+ <div class="board-column-header">
19575
+ <span>${escapeHtml(col.status)}</span>
19576
+ <span class="count">${col.docs.length}</span>
19577
+ </div>
19578
+ ${col.docs.map(
19579
+ (doc) => `
19580
+ <div class="board-card">
19581
+ <a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
19582
+ <div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
19583
+ <div class="bc-title">${escapeHtml(doc.frontmatter.title)}</div>
19584
+ ${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
19585
+ </a>
19586
+ </div>`
19587
+ ).join("\n")}
19588
+ </div>`
19589
+ ).join("\n");
19590
+ return `
19591
+ <div class="page-header">
19592
+ <h2>Status Board</h2>
19593
+ </div>
19594
+
19595
+ <div class="filters">
19596
+ <select onchange="filterByType(this.value)">
19597
+ <option value="">All types</option>
19598
+ ${typeOptions}
19599
+ </select>
19600
+ </div>
19601
+
19602
+ ${data.columns.length > 0 ? `<div class="board">${columns}</div>` : `<div class="empty"><p>No documents to display.</p></div>`}
19603
+
19604
+ <script>
19605
+ function filterByType(type) {
19606
+ if (type) window.location = '/board/' + type;
19607
+ else window.location = '/board';
19608
+ }
19609
+ </script>
19610
+ `;
19611
+ }
19612
+
19613
+ // src/web/router.ts
19614
+ function handleRequest(req, res, store, projectName, navGroups) {
19615
+ const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
19616
+ const pathname = parsed.pathname;
19617
+ const navTypes = store.registeredTypes;
19618
+ try {
19619
+ if (pathname === "/styles.css") {
19620
+ res.writeHead(200, {
19621
+ "Content-Type": "text/css",
19622
+ "Cache-Control": "public, max-age=300"
19623
+ });
19624
+ res.end(renderStyles());
19625
+ return;
19626
+ }
19627
+ if (pathname === "/") {
19628
+ const data = getOverviewData(store);
19629
+ const diagrams = getDiagramData(store);
19630
+ const body = overviewPage(data, diagrams, navGroups);
19631
+ respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
19632
+ return;
19633
+ }
19634
+ if (pathname === "/gar") {
19635
+ const report = getGarData(store, projectName);
19636
+ const body = garPage(report);
19637
+ respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
19638
+ return;
19639
+ }
19640
+ if (pathname === "/health") {
19641
+ const healthMetrics = collectHealthMetrics(store);
19642
+ const report = evaluateHealth(projectName, healthMetrics);
19643
+ const body = healthPage(report, healthMetrics);
19644
+ respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
19645
+ return;
19646
+ }
19647
+ const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
19648
+ if (boardMatch) {
19649
+ const type = boardMatch[1];
19650
+ if (type && !navTypes.includes(type)) {
19651
+ notFound(res, projectName, navGroups, pathname);
19652
+ return;
19653
+ }
19654
+ const data = getBoardData(store, type);
19655
+ const body = boardPage(data);
19656
+ respond(res, layout({ title: "Board", activePath: "/board", projectName, navGroups }, body));
19657
+ return;
19658
+ }
19659
+ const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
19660
+ if (detailMatch) {
19661
+ const [, type, id] = detailMatch;
19662
+ const doc = getDocumentDetail(store, type, id);
19663
+ if (!doc) {
19664
+ notFound(res, projectName, navGroups, pathname);
19665
+ return;
19666
+ }
19667
+ const body = documentDetailPage(doc);
19668
+ respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
19669
+ return;
19670
+ }
19671
+ const listMatch = pathname.match(/^\/docs\/([^/]+)$/);
19672
+ if (listMatch) {
19673
+ const type = listMatch[1];
19674
+ const filterStatus = parsed.searchParams.get("status") ?? void 0;
19675
+ const filterOwner = parsed.searchParams.get("owner") ?? void 0;
19676
+ const data = getDocumentListData(store, type, filterStatus, filterOwner);
19677
+ if (!data) {
19678
+ notFound(res, projectName, navGroups, pathname);
19679
+ return;
19680
+ }
19681
+ const body = documentsPage(data);
19682
+ respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
19683
+ return;
19684
+ }
19685
+ notFound(res, projectName, navGroups, pathname);
19686
+ } catch (err) {
19687
+ console.error("[marvin web] Error handling request:", err);
19688
+ res.writeHead(500, { "Content-Type": "text/html" });
19689
+ res.end("<h1>500 \u2014 Internal Server Error</h1>");
19690
+ }
19691
+ }
19692
+ function respond(res, html) {
19693
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19694
+ res.end(html);
19695
+ }
19696
+ function notFound(res, projectName, navGroups, activePath) {
19697
+ const body = `<div class="empty"><h2>404</h2><p>Page not found.</p><p><a href="/">Go to overview</a></p></div>`;
19698
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
19699
+ res.end(layout({ title: "Not Found", activePath, projectName, navGroups }, body));
19700
+ }
19701
+
19702
+ // src/web/server.ts
19703
+ import * as http from "http";
19704
+ import { exec } from "child_process";
19705
+ function openBrowser(url2) {
19706
+ const platform = process.platform;
19707
+ const cmd = platform === "darwin" ? `open "${url2}"` : platform === "win32" ? `start "${url2}"` : `xdg-open "${url2}"`;
19708
+ exec(cmd, (err) => {
19709
+ if (err) {
19710
+ }
19711
+ });
19712
+ }
19713
+
19714
+ // src/agent/tools/web.ts
19715
+ var runningServer = null;
19716
+ function createWebTools(store, projectName, navGroups) {
19717
+ return [
19718
+ tool20(
19719
+ "start_web_dashboard",
19720
+ "Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
19721
+ {
19722
+ port: external_exports.number().optional().describe("Port to listen on (default: 3000)"),
19723
+ open: external_exports.boolean().optional().describe("Open the dashboard in the default browser (default: true)")
19724
+ },
19725
+ async (args) => {
19726
+ const port = args.port ?? 3e3;
19727
+ if (runningServer) {
19728
+ const url3 = `http://localhost:${runningServer.port}`;
19729
+ return {
19730
+ content: [{ type: "text", text: `Dashboard already running at ${url3}` }]
19731
+ };
19732
+ }
19733
+ const server = http2.createServer((req, res) => {
19734
+ handleRequest(req, res, store, projectName, navGroups);
19735
+ });
19736
+ await new Promise((resolve3, reject) => {
19737
+ server.on("error", reject);
19738
+ server.listen(port, () => resolve3());
19739
+ });
19740
+ runningServer = { server, port };
19741
+ const url2 = `http://localhost:${port}`;
19742
+ if (args.open !== false) {
19743
+ openBrowser(url2);
19744
+ }
19745
+ return {
19746
+ content: [{ type: "text", text: `Dashboard started at ${url2}` }]
19747
+ };
19748
+ }
19749
+ ),
19750
+ tool20(
19751
+ "stop_web_dashboard",
19752
+ "Stop the running Marvin web dashboard.",
19753
+ {},
19754
+ async () => {
19755
+ if (!runningServer) {
19756
+ return {
19757
+ content: [{ type: "text", text: "No dashboard is currently running." }],
19758
+ isError: true
19759
+ };
19760
+ }
19761
+ await new Promise((resolve3) => {
19762
+ runningServer.server.close(() => resolve3());
19763
+ });
19764
+ runningServer = null;
19765
+ return {
19766
+ content: [{ type: "text", text: "Dashboard stopped." }]
19767
+ };
19768
+ }
19769
+ ),
19770
+ tool20(
19771
+ "get_web_dashboard_urls",
19772
+ "Get all available dashboard page URLs. The dashboard must be running.",
19773
+ {},
19774
+ async () => {
19775
+ if (!runningServer) {
19776
+ return {
19777
+ content: [{ type: "text", text: "Dashboard is not running. Use start_web_dashboard first." }],
19778
+ isError: true
19779
+ };
19780
+ }
19781
+ const base = `http://localhost:${runningServer.port}`;
19782
+ const urls = {
19783
+ overview: base,
19784
+ gar: `${base}/gar`,
19785
+ board: `${base}/board`
19786
+ };
19787
+ for (const type of store.registeredTypes) {
19788
+ urls[type] = `${base}/docs/${type}`;
19789
+ }
19790
+ return {
19791
+ content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
19792
+ };
19793
+ },
19794
+ { annotations: { readOnlyHint: true } }
19795
+ ),
19796
+ tool20(
19797
+ "get_dashboard_overview",
19798
+ "Get the project overview data: document type counts and recent activity. Works without the web server running.",
19799
+ {},
19800
+ async () => {
19801
+ const data = getOverviewData(store);
19802
+ const result = {
19803
+ types: data.types,
19804
+ recent: data.recent.map((d) => ({
19805
+ id: d.frontmatter.id,
19806
+ type: d.frontmatter.type,
19807
+ title: d.frontmatter.title,
19808
+ status: d.frontmatter.status,
19809
+ updated: d.frontmatter.updated ?? d.frontmatter.created
19810
+ }))
19811
+ };
19812
+ return {
19813
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19814
+ };
19815
+ },
19816
+ { annotations: { readOnlyHint: true } }
19817
+ ),
19818
+ tool20(
19819
+ "get_dashboard_gar",
19820
+ "Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
19821
+ {},
19822
+ async () => {
19823
+ const report = getGarData(store, projectName);
19824
+ return {
19825
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
19826
+ };
19827
+ },
19828
+ { annotations: { readOnlyHint: true } }
19829
+ ),
19830
+ tool20(
19831
+ "get_dashboard_board",
19832
+ "Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
19833
+ {
19834
+ type: external_exports.string().optional().describe("Document type to filter by (e.g. 'decision', 'action')")
19835
+ },
19836
+ async (args) => {
19837
+ const data = getBoardData(store, args.type);
19838
+ const result = {
19839
+ type: data.type ?? "all",
19840
+ types: data.types,
19841
+ columns: data.columns.map((col) => ({
19842
+ status: col.status,
19843
+ count: col.docs.length,
19844
+ docs: col.docs.map((d) => ({
19845
+ id: d.frontmatter.id,
19846
+ type: d.frontmatter.type,
19847
+ title: d.frontmatter.title,
19848
+ owner: d.frontmatter.owner
19849
+ }))
19850
+ }))
19851
+ };
19852
+ return {
19853
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19854
+ };
19855
+ },
19856
+ { annotations: { readOnlyHint: true } }
19857
+ )
19858
+ ];
19859
+ }
19860
+
19861
+ // src/agent/mcp-server.ts
17946
19862
  function createMarvinMcpServer(store, options) {
17947
19863
  const tools = [
17948
19864
  ...createDecisionTools(store),
@@ -17952,7 +19868,8 @@ function createMarvinMcpServer(store, options) {
17952
19868
  ...options?.manifest ? createSourceTools(options.manifest) : [],
17953
19869
  ...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
17954
19870
  ...options?.pluginTools ?? [],
17955
- ...options?.skillTools ?? []
19871
+ ...options?.skillTools ?? [],
19872
+ ...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
17956
19873
  ];
17957
19874
  return createSdkMcpServer({
17958
19875
  name: "marvin-governance",
@@ -18024,7 +19941,7 @@ function createSkillActionTools(skills, context) {
18024
19941
  if (!skill.actions) continue;
18025
19942
  for (const action of skill.actions) {
18026
19943
  tools.push(
18027
- tool20(
19944
+ tool21(
18028
19945
  `${skill.id}__${action.id}`,
18029
19946
  action.description,
18030
19947
  {
@@ -18094,6 +20011,8 @@ var deliveryManager = {
18094
20011
 
18095
20012
  ## How You Work
18096
20013
  - Review open actions (A-xxx) and follow up on overdue items
20014
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
20015
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
18097
20016
  - Ensure decisions (D-xxx) are properly documented with rationale
18098
20017
  - Track questions (Q-xxx) and ensure they get answered
18099
20018
  - Monitor project health and flag risks early
@@ -18251,10 +20170,10 @@ ${lines.join("\n\n")}`;
18251
20170
  }
18252
20171
 
18253
20172
  // src/mcp/persona-tools.ts
18254
- import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
20173
+ import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
18255
20174
  function createPersonaTools(ctx, marvinDir) {
18256
20175
  return [
18257
- tool21(
20176
+ tool22(
18258
20177
  "set_persona",
18259
20178
  "Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
18260
20179
  {
@@ -18284,7 +20203,7 @@ ${summaries}`
18284
20203
  };
18285
20204
  }
18286
20205
  ),
18287
- tool21(
20206
+ tool22(
18288
20207
  "get_persona_guidance",
18289
20208
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
18290
20209
  {
@@ -18319,7 +20238,7 @@ ${summaries}`
18319
20238
  content: [{ type: "text", text: guidance }]
18320
20239
  };
18321
20240
  },
18322
- { annotations: { readOnly: true } }
20241
+ { annotations: { readOnlyHint: true } }
18323
20242
  )
18324
20243
  ];
18325
20244
  }