mrvn-cli 0.3.3 → 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.
package/dist/index.js CHANGED
@@ -432,6 +432,8 @@ var deliveryManager = {
432
432
 
433
433
  ## How You Work
434
434
  - Review open actions (A-xxx) and follow up on overdue items
435
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
436
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
435
437
  - Ensure decisions (D-xxx) are properly documented with rationale
436
438
  - Track questions (Q-xxx) and ensure they get answered
437
439
  - Monitor project health and flag risks early
@@ -536,7 +538,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
536
538
  ## Available Tools
537
539
  You have access to governance tools for managing project artifacts:
538
540
  - **Decisions** (D-xxx): List, get, create, and update decisions
539
- - **Actions** (A-xxx): List, get, create, and update action items
541
+ - **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
540
542
  - **Questions** (Q-xxx): List, get, create, and update questions
541
543
  - **Features** (F-xxx): List, get, create, and update feature definitions
542
544
  - **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
@@ -14365,7 +14367,7 @@ function createDecisionTools(store) {
14365
14367
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14366
14368
  };
14367
14369
  },
14368
- { annotations: { readOnly: true } }
14370
+ { annotations: { readOnlyHint: true } }
14369
14371
  ),
14370
14372
  tool(
14371
14373
  "get_decision",
@@ -14392,7 +14394,7 @@ function createDecisionTools(store) {
14392
14394
  ]
14393
14395
  };
14394
14396
  },
14395
- { annotations: { readOnly: true } }
14397
+ { annotations: { readOnlyHint: true } }
14396
14398
  ),
14397
14399
  tool(
14398
14400
  "create_decision",
@@ -14453,6 +14455,19 @@ function createDecisionTools(store) {
14453
14455
 
14454
14456
  // src/agent/tools/actions.ts
14455
14457
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14458
+ function findMatchingSprints(store, dueDate) {
14459
+ const sprints = store.list({ type: "sprint" });
14460
+ return sprints.filter((s) => {
14461
+ const start = s.frontmatter.startDate;
14462
+ const end = s.frontmatter.endDate;
14463
+ return start && end && dueDate >= start && dueDate <= end;
14464
+ }).map((s) => ({
14465
+ id: s.frontmatter.id,
14466
+ title: s.frontmatter.title,
14467
+ startDate: s.frontmatter.startDate,
14468
+ endDate: s.frontmatter.endDate
14469
+ }));
14470
+ }
14456
14471
  function createActionTools(store) {
14457
14472
  return [
14458
14473
  tool2(
@@ -14468,19 +14483,24 @@ function createActionTools(store) {
14468
14483
  status: args.status,
14469
14484
  owner: args.owner
14470
14485
  });
14471
- const summary = docs.map((d) => ({
14472
- id: d.frontmatter.id,
14473
- title: d.frontmatter.title,
14474
- status: d.frontmatter.status,
14475
- owner: d.frontmatter.owner,
14476
- priority: d.frontmatter.priority,
14477
- created: d.frontmatter.created
14478
- }));
14486
+ const summary = docs.map((d) => {
14487
+ const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
14488
+ return {
14489
+ id: d.frontmatter.id,
14490
+ title: d.frontmatter.title,
14491
+ status: d.frontmatter.status,
14492
+ owner: d.frontmatter.owner,
14493
+ priority: d.frontmatter.priority,
14494
+ dueDate: d.frontmatter.dueDate,
14495
+ sprints: sprintIds.length > 0 ? sprintIds : void 0,
14496
+ created: d.frontmatter.created
14497
+ };
14498
+ });
14479
14499
  return {
14480
14500
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14481
14501
  };
14482
14502
  },
14483
- { annotations: { readOnly: true } }
14503
+ { annotations: { readOnlyHint: true } }
14484
14504
  ),
14485
14505
  tool2(
14486
14506
  "get_action",
@@ -14507,7 +14527,7 @@ function createActionTools(store) {
14507
14527
  ]
14508
14528
  };
14509
14529
  },
14510
- { annotations: { readOnly: true } }
14530
+ { annotations: { readOnlyHint: true } }
14511
14531
  ),
14512
14532
  tool2(
14513
14533
  "create_action",
@@ -14518,9 +14538,18 @@ function createActionTools(store) {
14518
14538
  status: external_exports.string().optional().describe("Status (default: 'open')"),
14519
14539
  owner: external_exports.string().optional().describe("Person responsible"),
14520
14540
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
14521
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
14541
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
14542
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14543
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
14522
14544
  },
14523
14545
  async (args) => {
14546
+ const tags = [...args.tags ?? []];
14547
+ if (args.sprints) {
14548
+ for (const sprintId of args.sprints) {
14549
+ const tag = `sprint:${sprintId}`;
14550
+ if (!tags.includes(tag)) tags.push(tag);
14551
+ }
14552
+ }
14524
14553
  const doc = store.create(
14525
14554
  "action",
14526
14555
  {
@@ -14528,17 +14557,21 @@ function createActionTools(store) {
14528
14557
  status: args.status,
14529
14558
  owner: args.owner,
14530
14559
  priority: args.priority,
14531
- tags: args.tags
14560
+ tags: tags.length > 0 ? tags : void 0,
14561
+ dueDate: args.dueDate
14532
14562
  },
14533
14563
  args.content
14534
14564
  );
14565
+ const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
14566
+ if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
14567
+ const matching = findMatchingSprints(store, args.dueDate);
14568
+ if (matching.length > 0) {
14569
+ const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
14570
+ parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
14571
+ }
14572
+ }
14535
14573
  return {
14536
- content: [
14537
- {
14538
- type: "text",
14539
- text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
14540
- }
14541
- ]
14574
+ content: [{ type: "text", text: parts.join("\n") }]
14542
14575
  };
14543
14576
  }
14544
14577
  ),
@@ -14551,10 +14584,25 @@ function createActionTools(store) {
14551
14584
  status: external_exports.string().optional().describe("New status"),
14552
14585
  content: external_exports.string().optional().describe("New content"),
14553
14586
  owner: external_exports.string().optional().describe("New owner"),
14554
- priority: external_exports.string().optional().describe("New priority")
14587
+ priority: external_exports.string().optional().describe("New priority"),
14588
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14589
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
14555
14590
  },
14556
14591
  async (args) => {
14557
- const { id, content, ...updates } = args;
14592
+ const { id, content, sprints, ...updates } = args;
14593
+ if (sprints !== void 0) {
14594
+ const existing = store.get(id);
14595
+ if (!existing) {
14596
+ return {
14597
+ content: [{ type: "text", text: `Action ${id} not found` }],
14598
+ isError: true
14599
+ };
14600
+ }
14601
+ const existingTags = existing.frontmatter.tags ?? [];
14602
+ const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
14603
+ const newSprintTags = sprints.map((s) => `sprint:${s}`);
14604
+ updates.tags = [...nonSprintTags, ...newSprintTags];
14605
+ }
14558
14606
  const doc = store.update(id, updates, content);
14559
14607
  return {
14560
14608
  content: [
@@ -14565,6 +14613,35 @@ function createActionTools(store) {
14565
14613
  ]
14566
14614
  };
14567
14615
  }
14616
+ ),
14617
+ tool2(
14618
+ "suggest_sprints_for_action",
14619
+ "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
14620
+ {
14621
+ dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
14622
+ },
14623
+ async (args) => {
14624
+ const matching = findMatchingSprints(store, args.dueDate);
14625
+ if (matching.length === 0) {
14626
+ return {
14627
+ content: [
14628
+ {
14629
+ type: "text",
14630
+ text: `No sprints found containing dueDate ${args.dueDate}.`
14631
+ }
14632
+ ]
14633
+ };
14634
+ }
14635
+ return {
14636
+ content: [
14637
+ {
14638
+ type: "text",
14639
+ text: JSON.stringify(matching, null, 2)
14640
+ }
14641
+ ]
14642
+ };
14643
+ },
14644
+ { annotations: { readOnlyHint: true } }
14568
14645
  )
14569
14646
  ];
14570
14647
  }
@@ -14592,7 +14669,7 @@ function createQuestionTools(store) {
14592
14669
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14593
14670
  };
14594
14671
  },
14595
- { annotations: { readOnly: true } }
14672
+ { annotations: { readOnlyHint: true } }
14596
14673
  ),
14597
14674
  tool3(
14598
14675
  "get_question",
@@ -14619,7 +14696,7 @@ function createQuestionTools(store) {
14619
14696
  ]
14620
14697
  };
14621
14698
  },
14622
- { annotations: { readOnly: true } }
14699
+ { annotations: { readOnlyHint: true } }
14623
14700
  ),
14624
14701
  tool3(
14625
14702
  "create_question",
@@ -14712,7 +14789,7 @@ function createDocumentTools(store) {
14712
14789
  ]
14713
14790
  };
14714
14791
  },
14715
- { annotations: { readOnly: true } }
14792
+ { annotations: { readOnlyHint: true } }
14716
14793
  ),
14717
14794
  tool4(
14718
14795
  "read_document",
@@ -14739,7 +14816,7 @@ function createDocumentTools(store) {
14739
14816
  ]
14740
14817
  };
14741
14818
  },
14742
- { annotations: { readOnly: true } }
14819
+ { annotations: { readOnlyHint: true } }
14743
14820
  ),
14744
14821
  tool4(
14745
14822
  "project_summary",
@@ -14767,7 +14844,7 @@ function createDocumentTools(store) {
14767
14844
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14768
14845
  };
14769
14846
  },
14770
- { annotations: { readOnly: true } }
14847
+ { annotations: { readOnlyHint: true } }
14771
14848
  )
14772
14849
  ];
14773
14850
  }
@@ -14804,7 +14881,7 @@ function createSourceTools(manifest) {
14804
14881
  ]
14805
14882
  };
14806
14883
  },
14807
- { annotations: { readOnly: true } }
14884
+ { annotations: { readOnlyHint: true } }
14808
14885
  ),
14809
14886
  tool5(
14810
14887
  "get_source_info",
@@ -14838,7 +14915,7 @@ function createSourceTools(manifest) {
14838
14915
  ]
14839
14916
  };
14840
14917
  },
14841
- { annotations: { readOnly: true } }
14918
+ { annotations: { readOnlyHint: true } }
14842
14919
  )
14843
14920
  ];
14844
14921
  }
@@ -14869,7 +14946,7 @@ function createSessionTools(store) {
14869
14946
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14870
14947
  };
14871
14948
  },
14872
- { annotations: { readOnly: true } }
14949
+ { annotations: { readOnlyHint: true } }
14873
14950
  ),
14874
14951
  tool6(
14875
14952
  "get_session",
@@ -14887,7 +14964,7 @@ function createSessionTools(store) {
14887
14964
  content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
14888
14965
  };
14889
14966
  },
14890
- { annotations: { readOnly: true } }
14967
+ { annotations: { readOnlyHint: true } }
14891
14968
  )
14892
14969
  ];
14893
14970
  }
@@ -14905,9 +14982,17 @@ function collectGarMetrics(store) {
14905
14982
  const blockedItems = allDocs.filter(
14906
14983
  (d) => d.frontmatter.tags?.includes("blocked")
14907
14984
  );
14908
- const overdueItems = allDocs.filter(
14985
+ const tagOverdueItems = allDocs.filter(
14909
14986
  (d) => d.frontmatter.tags?.includes("overdue")
14910
14987
  );
14988
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14989
+ const dateOverdueActions = openActions.filter((d) => {
14990
+ const dueDate = d.frontmatter.dueDate;
14991
+ return typeof dueDate === "string" && dueDate < today;
14992
+ });
14993
+ const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
14994
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
14995
+ );
14911
14996
  const openQuestions = store.list({ type: "question", status: "open" });
14912
14997
  const riskItems = allDocs.filter(
14913
14998
  (d) => d.frontmatter.tags?.includes("risk")
@@ -15016,6 +15101,253 @@ function evaluateGar(projectName, metrics) {
15016
15101
  };
15017
15102
  }
15018
15103
 
15104
+ // src/reports/health/collector.ts
15105
+ var FIELD_CHECKS = [
15106
+ {
15107
+ type: "action",
15108
+ openStatuses: ["open", "in-progress"],
15109
+ requiredFields: ["owner", "priority", "dueDate", "content"]
15110
+ },
15111
+ {
15112
+ type: "decision",
15113
+ openStatuses: ["open", "proposed"],
15114
+ requiredFields: ["owner", "content"]
15115
+ },
15116
+ {
15117
+ type: "question",
15118
+ openStatuses: ["open"],
15119
+ requiredFields: ["owner", "content"]
15120
+ },
15121
+ {
15122
+ type: "feature",
15123
+ openStatuses: ["draft", "approved"],
15124
+ requiredFields: ["owner", "priority", "content"]
15125
+ },
15126
+ {
15127
+ type: "epic",
15128
+ openStatuses: ["planned", "in-progress"],
15129
+ requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
15130
+ },
15131
+ {
15132
+ type: "sprint",
15133
+ openStatuses: ["planned", "active"],
15134
+ requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
15135
+ }
15136
+ ];
15137
+ var STALE_THRESHOLD_DAYS = 14;
15138
+ var AGING_THRESHOLD_DAYS = 30;
15139
+ function daysBetween(a, b) {
15140
+ const msPerDay = 864e5;
15141
+ const dateA = new Date(a);
15142
+ const dateB = new Date(b);
15143
+ return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
15144
+ }
15145
+ function checkMissingFields(doc, requiredFields) {
15146
+ const missing = [];
15147
+ for (const field of requiredFields) {
15148
+ if (field === "content") {
15149
+ if (!doc.content || doc.content.trim().length === 0) {
15150
+ missing.push("content");
15151
+ }
15152
+ } else if (field === "linkedEpics") {
15153
+ const val = doc.frontmatter[field];
15154
+ if (!Array.isArray(val) || val.length === 0) {
15155
+ missing.push(field);
15156
+ }
15157
+ } else {
15158
+ const val = doc.frontmatter[field];
15159
+ if (val === void 0 || val === null || val === "") {
15160
+ missing.push(field);
15161
+ }
15162
+ }
15163
+ }
15164
+ return missing;
15165
+ }
15166
+ function collectCompleteness(store) {
15167
+ const result = {};
15168
+ for (const check2 of FIELD_CHECKS) {
15169
+ const allOfType = store.list({ type: check2.type });
15170
+ const openDocs = allOfType.filter(
15171
+ (d) => check2.openStatuses.includes(d.frontmatter.status)
15172
+ );
15173
+ const gaps = [];
15174
+ let complete = 0;
15175
+ for (const doc of openDocs) {
15176
+ const missingFields = checkMissingFields(doc, check2.requiredFields);
15177
+ if (missingFields.length === 0) {
15178
+ complete++;
15179
+ } else {
15180
+ gaps.push({
15181
+ id: doc.frontmatter.id,
15182
+ title: doc.frontmatter.title,
15183
+ missingFields
15184
+ });
15185
+ }
15186
+ }
15187
+ result[check2.type] = {
15188
+ total: openDocs.length,
15189
+ complete,
15190
+ gaps
15191
+ };
15192
+ }
15193
+ return result;
15194
+ }
15195
+ function collectProcess(store) {
15196
+ const today = (/* @__PURE__ */ new Date()).toISOString();
15197
+ const allDocs = store.list();
15198
+ const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
15199
+ const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
15200
+ const stale = [];
15201
+ for (const doc of openDocs) {
15202
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
15203
+ const days = daysBetween(updated, today);
15204
+ if (days >= STALE_THRESHOLD_DAYS) {
15205
+ stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15206
+ }
15207
+ }
15208
+ const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
15209
+ const agingActions = [];
15210
+ for (const doc of openActions) {
15211
+ const days = daysBetween(doc.frontmatter.created, today);
15212
+ if (days >= AGING_THRESHOLD_DAYS) {
15213
+ agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15214
+ }
15215
+ }
15216
+ const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
15217
+ let decisionTotal = 0;
15218
+ for (const doc of resolvedDecisions) {
15219
+ decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15220
+ }
15221
+ const decisionVelocity = {
15222
+ avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
15223
+ count: resolvedDecisions.length
15224
+ };
15225
+ const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
15226
+ let questionTotal = 0;
15227
+ for (const doc of answeredQuestions) {
15228
+ questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15229
+ }
15230
+ const questionResolution = {
15231
+ avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
15232
+ count: answeredQuestions.length
15233
+ };
15234
+ return { stale, agingActions, decisionVelocity, questionResolution };
15235
+ }
15236
+ function collectHealthMetrics(store) {
15237
+ return {
15238
+ completeness: collectCompleteness(store),
15239
+ process: collectProcess(store)
15240
+ };
15241
+ }
15242
+
15243
+ // src/reports/health/evaluator.ts
15244
+ function worstStatus2(statuses) {
15245
+ if (statuses.includes("red")) return "red";
15246
+ if (statuses.includes("amber")) return "amber";
15247
+ return "green";
15248
+ }
15249
+ function completenessStatus(total, complete) {
15250
+ if (total === 0) return "green";
15251
+ const pct = Math.round(complete / total * 100);
15252
+ if (pct >= 100) return "green";
15253
+ if (pct >= 75) return "amber";
15254
+ return "red";
15255
+ }
15256
+ var TYPE_LABELS = {
15257
+ action: "Actions",
15258
+ decision: "Decisions",
15259
+ question: "Questions",
15260
+ feature: "Features",
15261
+ epic: "Epics",
15262
+ sprint: "Sprints"
15263
+ };
15264
+ function evaluateHealth(projectName, metrics) {
15265
+ const completeness = [];
15266
+ for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
15267
+ const { total, complete, gaps } = catMetrics;
15268
+ const status = completenessStatus(total, complete);
15269
+ const pct = total > 0 ? Math.round(complete / total * 100) : 100;
15270
+ completeness.push({
15271
+ name: TYPE_LABELS[type] ?? type,
15272
+ status,
15273
+ summary: `${pct}% complete (${complete}/${total})`,
15274
+ items: gaps.map((g) => ({
15275
+ id: g.id,
15276
+ detail: `missing: ${g.missingFields.join(", ")}`
15277
+ }))
15278
+ });
15279
+ }
15280
+ const process3 = [];
15281
+ const staleCount = metrics.process.stale.length;
15282
+ const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
15283
+ process3.push({
15284
+ name: "Stale Items",
15285
+ status: staleStatus,
15286
+ summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
15287
+ items: metrics.process.stale.map((s) => ({
15288
+ id: s.id,
15289
+ detail: `${s.days} days since last update`
15290
+ }))
15291
+ });
15292
+ const agingCount = metrics.process.agingActions.length;
15293
+ const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
15294
+ process3.push({
15295
+ name: "Aging Actions",
15296
+ status: agingStatus,
15297
+ summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
15298
+ items: metrics.process.agingActions.map((a) => ({
15299
+ id: a.id,
15300
+ detail: `open for ${a.days} days`
15301
+ }))
15302
+ });
15303
+ const dv = metrics.process.decisionVelocity;
15304
+ let dvStatus;
15305
+ if (dv.count === 0) {
15306
+ dvStatus = "green";
15307
+ } else if (dv.avgDays <= 7) {
15308
+ dvStatus = "green";
15309
+ } else if (dv.avgDays <= 21) {
15310
+ dvStatus = "amber";
15311
+ } else {
15312
+ dvStatus = "red";
15313
+ }
15314
+ process3.push({
15315
+ name: "Decision Velocity",
15316
+ status: dvStatus,
15317
+ summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
15318
+ items: []
15319
+ });
15320
+ const qr = metrics.process.questionResolution;
15321
+ let qrStatus;
15322
+ if (qr.count === 0) {
15323
+ qrStatus = "green";
15324
+ } else if (qr.avgDays <= 7) {
15325
+ qrStatus = "green";
15326
+ } else if (qr.avgDays <= 14) {
15327
+ qrStatus = "amber";
15328
+ } else {
15329
+ qrStatus = "red";
15330
+ }
15331
+ process3.push({
15332
+ name: "Question Resolution",
15333
+ status: qrStatus,
15334
+ summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
15335
+ items: []
15336
+ });
15337
+ const allStatuses = [
15338
+ ...completeness.map((c) => c.status),
15339
+ ...process3.map((p) => p.status)
15340
+ ];
15341
+ const overall = worstStatus2(allStatuses);
15342
+ return {
15343
+ projectName,
15344
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15345
+ overall,
15346
+ completeness,
15347
+ process: process3
15348
+ };
15349
+ }
15350
+
15019
15351
  // src/web/data.ts
15020
15352
  function getOverviewData(store) {
15021
15353
  const types = [];
@@ -15087,6 +15419,46 @@ function getBoardData(store, type) {
15087
15419
  }));
15088
15420
  return { columns, type, types };
15089
15421
  }
15422
+ function getDiagramData(store) {
15423
+ const allDocs = store.list();
15424
+ const sprints = [];
15425
+ const epics = [];
15426
+ const features = [];
15427
+ const statusCounts = {};
15428
+ for (const doc of allDocs) {
15429
+ const fm = doc.frontmatter;
15430
+ const status = fm.status.toLowerCase();
15431
+ statusCounts[status] = (statusCounts[status] ?? 0) + 1;
15432
+ switch (fm.type) {
15433
+ case "sprint":
15434
+ sprints.push({
15435
+ id: fm.id,
15436
+ title: fm.title,
15437
+ status: fm.status,
15438
+ startDate: fm.startDate,
15439
+ endDate: fm.endDate,
15440
+ linkedEpics: fm.linkedEpics ?? []
15441
+ });
15442
+ break;
15443
+ case "epic":
15444
+ epics.push({
15445
+ id: fm.id,
15446
+ title: fm.title,
15447
+ status: fm.status,
15448
+ linkedFeature: fm.linkedFeature
15449
+ });
15450
+ break;
15451
+ case "feature":
15452
+ features.push({
15453
+ id: fm.id,
15454
+ title: fm.title,
15455
+ status: fm.status
15456
+ });
15457
+ break;
15458
+ }
15459
+ }
15460
+ return { sprints, epics, features, statusCounts };
15461
+ }
15090
15462
 
15091
15463
  // src/web/templates/layout.ts
15092
15464
  function escapeHtml(str) {
@@ -15117,16 +15489,43 @@ function renderMarkdown(md) {
15117
15489
  const out = [];
15118
15490
  let inList = false;
15119
15491
  let listTag = "ul";
15120
- for (const raw of lines) {
15121
- const line = raw;
15492
+ let inTable = false;
15493
+ let i = 0;
15494
+ while (i < lines.length) {
15495
+ const line = lines[i];
15122
15496
  if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
15123
15497
  out.push(`</${listTag}>`);
15124
15498
  inList = false;
15125
15499
  }
15500
+ if (inTable && !/^\s*\|/.test(line)) {
15501
+ out.push("</tbody></table></div>");
15502
+ inTable = false;
15503
+ }
15504
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
15505
+ i++;
15506
+ out.push("<hr>");
15507
+ continue;
15508
+ }
15509
+ if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
15510
+ const headers = parseTableRow(line);
15511
+ out.push('<div class="table-wrap"><table><thead><tr>');
15512
+ out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
15513
+ out.push("</tr></thead><tbody>");
15514
+ inTable = true;
15515
+ i += 2;
15516
+ continue;
15517
+ }
15518
+ if (inTable && /^\s*\|/.test(line)) {
15519
+ const cells = parseTableRow(line);
15520
+ out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
15521
+ i++;
15522
+ continue;
15523
+ }
15126
15524
  const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
15127
15525
  if (headingMatch) {
15128
15526
  const level = headingMatch[1].length;
15129
15527
  out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
15528
+ i++;
15130
15529
  continue;
15131
15530
  }
15132
15531
  const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
@@ -15138,6 +15537,7 @@ function renderMarkdown(md) {
15138
15537
  listTag = "ul";
15139
15538
  }
15140
15539
  out.push(`<li>${inline(ulMatch[1])}</li>`);
15540
+ i++;
15141
15541
  continue;
15142
15542
  }
15143
15543
  const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
@@ -15149,6 +15549,7 @@ function renderMarkdown(md) {
15149
15549
  listTag = "ol";
15150
15550
  }
15151
15551
  out.push(`<li>${inline(olMatch[1])}</li>`);
15552
+ i++;
15152
15553
  continue;
15153
15554
  }
15154
15555
  if (line.trim() === "") {
@@ -15156,13 +15557,19 @@ function renderMarkdown(md) {
15156
15557
  out.push(`</${listTag}>`);
15157
15558
  inList = false;
15158
15559
  }
15560
+ i++;
15159
15561
  continue;
15160
15562
  }
15161
15563
  out.push(`<p>${inline(line)}</p>`);
15564
+ i++;
15162
15565
  }
15163
15566
  if (inList) out.push(`</${listTag}>`);
15567
+ if (inTable) out.push("</tbody></table></div>");
15164
15568
  return out.join("\n");
15165
15569
  }
15570
+ function parseTableRow(line) {
15571
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
15572
+ }
15166
15573
  function inline(text) {
15167
15574
  let s = escapeHtml(text);
15168
15575
  s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
@@ -15176,7 +15583,8 @@ function layout(opts, body) {
15176
15583
  const topItems = [
15177
15584
  { href: "/", label: "Overview" },
15178
15585
  { href: "/board", label: "Board" },
15179
- { href: "/gar", label: "GAR Report" }
15586
+ { href: "/gar", label: "GAR Report" },
15587
+ { href: "/health", label: "Health" }
15180
15588
  ];
15181
15589
  const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
15182
15590
  const groupsHtml = opts.navGroups.map((group) => {
@@ -15211,9 +15619,15 @@ function layout(opts, body) {
15211
15619
  </nav>
15212
15620
  </aside>
15213
15621
  <main class="main">
15622
+ <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
15623
+ <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>
15624
+ <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>
15625
+ </button>
15214
15626
  ${body}
15215
15627
  </main>
15216
15628
  </div>
15629
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
15630
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
15217
15631
  </body>
15218
15632
  </html>`;
15219
15633
  }
@@ -15328,7 +15742,33 @@ a:hover { text-decoration: underline; }
15328
15742
  flex: 1;
15329
15743
  padding: 2rem 2.5rem;
15330
15744
  max-width: 1200px;
15745
+ position: relative;
15746
+ transition: max-width 0.2s ease;
15747
+ }
15748
+ .main.expanded {
15749
+ max-width: none;
15750
+ }
15751
+ .expand-toggle {
15752
+ position: absolute;
15753
+ top: 1rem;
15754
+ right: 1rem;
15755
+ background: var(--bg-card);
15756
+ border: 1px solid var(--border);
15757
+ border-radius: var(--radius);
15758
+ color: var(--text-dim);
15759
+ cursor: pointer;
15760
+ padding: 0.4rem;
15761
+ display: flex;
15762
+ align-items: center;
15763
+ justify-content: center;
15764
+ transition: color 0.15s, border-color 0.15s;
15331
15765
  }
15766
+ .expand-toggle:hover {
15767
+ color: var(--text);
15768
+ border-color: var(--text-dim);
15769
+ }
15770
+ .main.expanded .icon-expand { display: none; }
15771
+ .main:not(.expanded) .icon-collapse { display: none; }
15332
15772
 
15333
15773
  /* Page header */
15334
15774
  .page-header {
@@ -15357,12 +15797,26 @@ a:hover { text-decoration: underline; }
15357
15797
  .breadcrumb a:hover { color: var(--accent); }
15358
15798
  .breadcrumb .sep { margin: 0 0.4rem; }
15359
15799
 
15800
+ /* Card groups */
15801
+ .card-group {
15802
+ margin-bottom: 1.5rem;
15803
+ }
15804
+
15805
+ .card-group-label {
15806
+ font-size: 0.7rem;
15807
+ text-transform: uppercase;
15808
+ letter-spacing: 0.08em;
15809
+ color: var(--text-dim);
15810
+ font-weight: 600;
15811
+ margin-bottom: 0.5rem;
15812
+ }
15813
+
15360
15814
  /* Cards grid */
15361
15815
  .cards {
15362
15816
  display: grid;
15363
15817
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
15364
15818
  gap: 1rem;
15365
- margin-bottom: 2rem;
15819
+ margin-bottom: 0.5rem;
15366
15820
  }
15367
15821
 
15368
15822
  .card {
@@ -15643,6 +16097,14 @@ tr:hover td {
15643
16097
  font-family: var(--mono);
15644
16098
  font-size: 0.85em;
15645
16099
  }
16100
+ .detail-content hr {
16101
+ border: none;
16102
+ border-top: 1px solid var(--border);
16103
+ margin: 1.25rem 0;
16104
+ }
16105
+ .detail-content .table-wrap {
16106
+ margin: 0.75rem 0;
16107
+ }
15646
16108
 
15647
16109
  /* Filters */
15648
16110
  .filters {
@@ -15687,21 +16149,206 @@ tr:hover td {
15687
16149
  .priority-high { color: var(--red); }
15688
16150
  .priority-medium { color: var(--amber); }
15689
16151
  .priority-low { color: var(--green); }
16152
+
16153
+ /* Health */
16154
+ .health-section-title {
16155
+ font-size: 1.1rem;
16156
+ font-weight: 600;
16157
+ margin: 2rem 0 1rem;
16158
+ color: var(--text);
16159
+ }
16160
+
16161
+ /* Mermaid diagrams */
16162
+ .mermaid-container {
16163
+ background: var(--bg-card);
16164
+ border: 1px solid var(--border);
16165
+ border-radius: var(--radius);
16166
+ padding: 1.5rem;
16167
+ margin: 1rem 0;
16168
+ overflow-x: auto;
16169
+ }
16170
+
16171
+ .mermaid-container .mermaid {
16172
+ display: flex;
16173
+ justify-content: center;
16174
+ }
16175
+
16176
+ .mermaid-empty {
16177
+ text-align: center;
16178
+ color: var(--text-dim);
16179
+ font-size: 0.875rem;
16180
+ }
16181
+
16182
+ .mermaid-row {
16183
+ display: grid;
16184
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
16185
+ gap: 1rem;
16186
+ }
16187
+
16188
+ .mermaid-row .mermaid-container {
16189
+ margin: 0;
16190
+ }
15690
16191
  `;
15691
16192
  }
15692
16193
 
16194
+ // src/web/templates/mermaid.ts
16195
+ function sanitize(text, maxLen = 40) {
16196
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
16197
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
16198
+ }
16199
+ function mermaidBlock(definition) {
16200
+ return `<div class="mermaid-container"><pre class="mermaid">
16201
+ ${definition}
16202
+ </pre></div>`;
16203
+ }
16204
+ function placeholder(message) {
16205
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
16206
+ }
16207
+ function buildTimelineGantt(data) {
16208
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
16209
+ if (sprintsWithDates.length === 0) {
16210
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
16211
+ }
16212
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
16213
+ const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
16214
+ for (const sprint of sprintsWithDates) {
16215
+ lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
16216
+ const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
16217
+ if (linked.length === 0) {
16218
+ lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
16219
+ } else {
16220
+ for (const epic of linked) {
16221
+ const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
16222
+ lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
16223
+ }
16224
+ }
16225
+ }
16226
+ return mermaidBlock(lines.join("\n"));
16227
+ }
16228
+ function buildArtifactFlowchart(data) {
16229
+ if (data.features.length === 0 && data.epics.length === 0) {
16230
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
16231
+ }
16232
+ const lines = ["graph TD"];
16233
+ lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
16234
+ lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
16235
+ lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
16236
+ lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
16237
+ const nodeIds = /* @__PURE__ */ new Set();
16238
+ for (const epic of data.epics) {
16239
+ if (epic.linkedFeature) {
16240
+ const feature = data.features.find((f) => f.id === epic.linkedFeature);
16241
+ if (feature) {
16242
+ const fNode = feature.id.replace(/-/g, "_");
16243
+ const eNode = epic.id.replace(/-/g, "_");
16244
+ if (!nodeIds.has(fNode)) {
16245
+ lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
16246
+ nodeIds.add(fNode);
16247
+ }
16248
+ if (!nodeIds.has(eNode)) {
16249
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
16250
+ nodeIds.add(eNode);
16251
+ }
16252
+ lines.push(` ${fNode} --> ${eNode}`);
16253
+ }
16254
+ }
16255
+ }
16256
+ for (const sprint of data.sprints) {
16257
+ const sNode = sprint.id.replace(/-/g, "_");
16258
+ for (const epicId of sprint.linkedEpics) {
16259
+ const epic = data.epics.find((e) => e.id === epicId);
16260
+ if (epic) {
16261
+ const eNode = epic.id.replace(/-/g, "_");
16262
+ if (!nodeIds.has(eNode)) {
16263
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
16264
+ nodeIds.add(eNode);
16265
+ }
16266
+ if (!nodeIds.has(sNode)) {
16267
+ lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
16268
+ nodeIds.add(sNode);
16269
+ }
16270
+ lines.push(` ${eNode} --> ${sNode}`);
16271
+ }
16272
+ }
16273
+ }
16274
+ if (nodeIds.size === 0) {
16275
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
16276
+ }
16277
+ const allItems = [
16278
+ ...data.features.map((f) => ({ id: f.id, status: f.status })),
16279
+ ...data.epics.map((e) => ({ id: e.id, status: e.status })),
16280
+ ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
16281
+ ];
16282
+ for (const item of allItems) {
16283
+ const node = item.id.replace(/-/g, "_");
16284
+ if (!nodeIds.has(node)) continue;
16285
+ const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
16286
+ if (cls) {
16287
+ lines.push(` class ${node} ${cls}`);
16288
+ }
16289
+ }
16290
+ return mermaidBlock(lines.join("\n"));
16291
+ }
16292
+ function buildStatusPie(title, counts) {
16293
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
16294
+ if (entries.length === 0) {
16295
+ return placeholder(`No data for ${title}.`);
16296
+ }
16297
+ const lines = [`pie title ${sanitize(title, 60)}`];
16298
+ for (const [label, count] of entries) {
16299
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
16300
+ }
16301
+ return mermaidBlock(lines.join("\n"));
16302
+ }
16303
+ function buildHealthGauge(categories) {
16304
+ const valid = categories.filter((c) => c.total > 0);
16305
+ if (valid.length === 0) {
16306
+ return placeholder("No completeness data available.");
16307
+ }
16308
+ const pies = valid.map((cat) => {
16309
+ const incomplete = cat.total - cat.complete;
16310
+ const lines = [
16311
+ `pie title ${sanitize(cat.name, 30)}`,
16312
+ ` "Complete" : ${cat.complete}`,
16313
+ ` "Incomplete" : ${incomplete}`
16314
+ ];
16315
+ return mermaidBlock(lines.join("\n"));
16316
+ });
16317
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
16318
+ }
16319
+
15693
16320
  // src/web/templates/pages/overview.ts
15694
- function overviewPage(data) {
15695
- const cards = data.types.map(
15696
- (t) => `
16321
+ function renderCard(t) {
16322
+ return `
15697
16323
  <div class="card">
15698
16324
  <a href="/docs/${t.type}">
15699
16325
  <div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
15700
16326
  <div class="card-value">${t.total}</div>
15701
16327
  ${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
15702
16328
  </a>
15703
- </div>`
15704
- ).join("\n");
16329
+ </div>`;
16330
+ }
16331
+ function overviewPage(data, diagrams, navGroups) {
16332
+ const typeMap = new Map(data.types.map((t) => [t.type, t]));
16333
+ const placed = /* @__PURE__ */ new Set();
16334
+ const groupSections = navGroups.map((group) => {
16335
+ const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
16336
+ placed.add(type);
16337
+ return renderCard(typeMap.get(type));
16338
+ });
16339
+ if (groupCards.length === 0) return "";
16340
+ return `
16341
+ <div class="card-group">
16342
+ <div class="card-group-label">${escapeHtml(group.label)}</div>
16343
+ <div class="cards">${groupCards.join("\n")}</div>
16344
+ </div>`;
16345
+ }).filter(Boolean).join("\n");
16346
+ const ungrouped = data.types.filter((t) => !placed.has(t.type));
16347
+ const ungroupedSection = ungrouped.length > 0 ? `
16348
+ <div class="card-group">
16349
+ <div class="card-group-label">Other</div>
16350
+ <div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
16351
+ </div>` : "";
15705
16352
  const rows = data.recent.map(
15706
16353
  (doc) => `
15707
16354
  <tr>
@@ -15717,9 +16364,14 @@ function overviewPage(data) {
15717
16364
  <h2>Project Overview</h2>
15718
16365
  </div>
15719
16366
 
15720
- <div class="cards">
15721
- ${cards}
15722
- </div>
16367
+ ${groupSections}
16368
+ ${ungroupedSection}
16369
+
16370
+ <div class="section-title">Project Timeline</div>
16371
+ ${buildTimelineGantt(diagrams)}
16372
+
16373
+ <div class="section-title">Artifact Relationships</div>
16374
+ ${buildArtifactFlowchart(diagrams)}
15723
16375
 
15724
16376
  <div class="section-title">Recent Activity</div>
15725
16377
  ${data.recent.length > 0 ? `
@@ -15886,6 +16538,76 @@ function garPage(report) {
15886
16538
  <div class="gar-areas">
15887
16539
  ${areaCards}
15888
16540
  </div>
16541
+
16542
+ <div class="section-title">Status Distribution</div>
16543
+ ${buildStatusPie("Action Status", {
16544
+ Open: report.metrics.scope.open,
16545
+ Done: report.metrics.scope.done,
16546
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
16547
+ })}
16548
+ `;
16549
+ }
16550
+
16551
+ // src/web/templates/pages/health.ts
16552
+ function healthPage(report, metrics) {
16553
+ const dotClass = `dot-${report.overall}`;
16554
+ function renderSection(title, categories) {
16555
+ const cards = categories.map(
16556
+ (cat) => `
16557
+ <div class="gar-area">
16558
+ <div class="area-header">
16559
+ <div class="area-dot dot-${cat.status}"></div>
16560
+ <div class="area-name">${escapeHtml(cat.name)}</div>
16561
+ </div>
16562
+ <div class="area-summary">${escapeHtml(cat.summary)}</div>
16563
+ ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
16564
+ </div>`
16565
+ ).join("\n");
16566
+ return `
16567
+ <div class="health-section-title">${escapeHtml(title)}</div>
16568
+ <div class="gar-areas">${cards}</div>
16569
+ `;
16570
+ }
16571
+ return `
16572
+ <div class="page-header">
16573
+ <h2>Governance Health Check</h2>
16574
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
16575
+ </div>
16576
+
16577
+ <div class="gar-overall">
16578
+ <div class="dot ${dotClass}"></div>
16579
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
16580
+ </div>
16581
+
16582
+ ${renderSection("Completeness", report.completeness)}
16583
+
16584
+ <div class="health-section-title">Completeness Overview</div>
16585
+ ${buildHealthGauge(
16586
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
16587
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
16588
+ complete: cat.complete,
16589
+ total: cat.total
16590
+ })) : report.completeness.map((c) => {
16591
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
16592
+ return {
16593
+ name: c.name,
16594
+ complete: match ? parseInt(match[1], 10) : 0,
16595
+ total: match ? parseInt(match[2], 10) : 0
16596
+ };
16597
+ })
16598
+ )}
16599
+
16600
+ ${renderSection("Process", report.process)}
16601
+
16602
+ <div class="health-section-title">Process Summary</div>
16603
+ ${metrics ? buildStatusPie("Process Health", {
16604
+ Stale: metrics.process.stale.length,
16605
+ "Aging Actions": metrics.process.agingActions.length,
16606
+ Healthy: Math.max(
16607
+ 0,
16608
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
16609
+ )
16610
+ }) : ""}
15889
16611
  `;
15890
16612
  }
15891
16613
 
@@ -15952,7 +16674,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
15952
16674
  }
15953
16675
  if (pathname === "/") {
15954
16676
  const data = getOverviewData(store);
15955
- const body = overviewPage(data);
16677
+ const diagrams = getDiagramData(store);
16678
+ const body = overviewPage(data, diagrams, navGroups);
15956
16679
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
15957
16680
  return;
15958
16681
  }
@@ -15962,6 +16685,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
15962
16685
  respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
15963
16686
  return;
15964
16687
  }
16688
+ if (pathname === "/health") {
16689
+ const healthMetrics = collectHealthMetrics(store);
16690
+ const report = evaluateHealth(projectName, healthMetrics);
16691
+ const body = healthPage(report, healthMetrics);
16692
+ respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
16693
+ return;
16694
+ }
15965
16695
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
15966
16696
  if (boardMatch) {
15967
16697
  const type = boardMatch[1];
@@ -16042,7 +16772,7 @@ function createMeetingTools(store) {
16042
16772
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16043
16773
  };
16044
16774
  },
16045
- { annotations: { readOnly: true } }
16775
+ { annotations: { readOnlyHint: true } }
16046
16776
  ),
16047
16777
  tool7(
16048
16778
  "get_meeting",
@@ -16069,7 +16799,7 @@ function createMeetingTools(store) {
16069
16799
  ]
16070
16800
  };
16071
16801
  },
16072
- { annotations: { readOnly: true } }
16802
+ { annotations: { readOnlyHint: true } }
16073
16803
  ),
16074
16804
  tool7(
16075
16805
  "create_meeting",
@@ -16194,7 +16924,7 @@ function createMeetingTools(store) {
16194
16924
  content: [{ type: "text", text: sections.join("\n") }]
16195
16925
  };
16196
16926
  },
16197
- { annotations: { readOnly: true } }
16927
+ { annotations: { readOnlyHint: true } }
16198
16928
  )
16199
16929
  ];
16200
16930
  }
@@ -16219,7 +16949,8 @@ function createReportTools(store) {
16219
16949
  id: d.frontmatter.id,
16220
16950
  title: d.frontmatter.title,
16221
16951
  owner: d.frontmatter.owner,
16222
- priority: d.frontmatter.priority
16952
+ priority: d.frontmatter.priority,
16953
+ dueDate: d.frontmatter.dueDate
16223
16954
  })),
16224
16955
  completedActions: completedActions.map((d) => ({
16225
16956
  id: d.frontmatter.id,
@@ -16238,7 +16969,7 @@ function createReportTools(store) {
16238
16969
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
16239
16970
  };
16240
16971
  },
16241
- { annotations: { readOnly: true } }
16972
+ { annotations: { readOnlyHint: true } }
16242
16973
  ),
16243
16974
  tool8(
16244
16975
  "generate_risk_register",
@@ -16282,7 +17013,7 @@ function createReportTools(store) {
16282
17013
  content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
16283
17014
  };
16284
17015
  },
16285
- { annotations: { readOnly: true } }
17016
+ { annotations: { readOnlyHint: true } }
16286
17017
  ),
16287
17018
  tool8(
16288
17019
  "generate_gar_report",
@@ -16295,7 +17026,7 @@ function createReportTools(store) {
16295
17026
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
16296
17027
  };
16297
17028
  },
16298
- { annotations: { readOnly: true } }
17029
+ { annotations: { readOnlyHint: true } }
16299
17030
  ),
16300
17031
  tool8(
16301
17032
  "generate_epic_progress",
@@ -16380,7 +17111,7 @@ function createReportTools(store) {
16380
17111
  ]
16381
17112
  };
16382
17113
  },
16383
- { annotations: { readOnly: true } }
17114
+ { annotations: { readOnlyHint: true } }
16384
17115
  ),
16385
17116
  tool8(
16386
17117
  "generate_sprint_progress",
@@ -16425,7 +17156,8 @@ function createReportTools(store) {
16425
17156
  id: d.frontmatter.id,
16426
17157
  title: d.frontmatter.title,
16427
17158
  type: d.frontmatter.type,
16428
- status: d.frontmatter.status
17159
+ status: d.frontmatter.status,
17160
+ dueDate: d.frontmatter.dueDate
16429
17161
  }))
16430
17162
  }
16431
17163
  };
@@ -16434,7 +17166,7 @@ function createReportTools(store) {
16434
17166
  content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
16435
17167
  };
16436
17168
  },
16437
- { annotations: { readOnly: true } }
17169
+ { annotations: { readOnlyHint: true } }
16438
17170
  ),
16439
17171
  tool8(
16440
17172
  "generate_feature_progress",
@@ -16474,7 +17206,20 @@ function createReportTools(store) {
16474
17206
  content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
16475
17207
  };
16476
17208
  },
16477
- { annotations: { readOnly: true } }
17209
+ { annotations: { readOnlyHint: true } }
17210
+ ),
17211
+ tool8(
17212
+ "generate_health_report",
17213
+ "Generate a governance health check report covering artifact completeness and process health metrics",
17214
+ {},
17215
+ async () => {
17216
+ const metrics = collectHealthMetrics(store);
17217
+ const report = evaluateHealth("project", metrics);
17218
+ return {
17219
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
17220
+ };
17221
+ },
17222
+ { annotations: { readOnlyHint: true } }
16478
17223
  ),
16479
17224
  tool8(
16480
17225
  "save_report",
@@ -16536,7 +17281,7 @@ function createFeatureTools(store) {
16536
17281
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16537
17282
  };
16538
17283
  },
16539
- { annotations: { readOnly: true } }
17284
+ { annotations: { readOnlyHint: true } }
16540
17285
  ),
16541
17286
  tool9(
16542
17287
  "get_feature",
@@ -16563,7 +17308,7 @@ function createFeatureTools(store) {
16563
17308
  ]
16564
17309
  };
16565
17310
  },
16566
- { annotations: { readOnly: true } }
17311
+ { annotations: { readOnlyHint: true } }
16567
17312
  ),
16568
17313
  tool9(
16569
17314
  "create_feature",
@@ -16654,7 +17399,7 @@ function createEpicTools(store) {
16654
17399
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16655
17400
  };
16656
17401
  },
16657
- { annotations: { readOnly: true } }
17402
+ { annotations: { readOnlyHint: true } }
16658
17403
  ),
16659
17404
  tool10(
16660
17405
  "get_epic",
@@ -16681,7 +17426,7 @@ function createEpicTools(store) {
16681
17426
  ]
16682
17427
  };
16683
17428
  },
16684
- { annotations: { readOnly: true } }
17429
+ { annotations: { readOnlyHint: true } }
16685
17430
  ),
16686
17431
  tool10(
16687
17432
  "create_epic",
@@ -16813,7 +17558,7 @@ function createContributionTools(store) {
16813
17558
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16814
17559
  };
16815
17560
  },
16816
- { annotations: { readOnly: true } }
17561
+ { annotations: { readOnlyHint: true } }
16817
17562
  ),
16818
17563
  tool11(
16819
17564
  "get_contribution",
@@ -16840,7 +17585,7 @@ function createContributionTools(store) {
16840
17585
  ]
16841
17586
  };
16842
17587
  },
16843
- { annotations: { readOnly: true } }
17588
+ { annotations: { readOnlyHint: true } }
16844
17589
  ),
16845
17590
  tool11(
16846
17591
  "create_contribution",
@@ -16925,7 +17670,7 @@ function createSprintTools(store) {
16925
17670
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16926
17671
  };
16927
17672
  },
16928
- { annotations: { readOnly: true } }
17673
+ { annotations: { readOnlyHint: true } }
16929
17674
  ),
16930
17675
  tool12(
16931
17676
  "get_sprint",
@@ -16952,7 +17697,7 @@ function createSprintTools(store) {
16952
17697
  ]
16953
17698
  };
16954
17699
  },
16955
- { annotations: { readOnly: true } }
17700
+ { annotations: { readOnlyHint: true } }
16956
17701
  ),
16957
17702
  tool12(
16958
17703
  "create_sprint",
@@ -17249,7 +17994,7 @@ function createSprintPlanningTools(store) {
17249
17994
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
17250
17995
  };
17251
17996
  },
17252
- { annotations: { readOnly: true } }
17997
+ { annotations: { readOnlyHint: true } }
17253
17998
  )
17254
17999
  ];
17255
18000
  }
@@ -17303,6 +18048,7 @@ var genericAgilePlugin = {
17303
18048
  - Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
17304
18049
  - Use priority levels (critical, high, medium, low) to communicate business value.
17305
18050
  - Tag features for categorization and cross-referencing.
18051
+ - Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
17306
18052
 
17307
18053
  **Contribution Tools:**
17308
18054
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -17333,6 +18079,7 @@ var genericAgilePlugin = {
17333
18079
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
17334
18080
  - Collaborate with the Delivery Manager on target dates and effort estimates.
17335
18081
  - Each epic should have a clear scope and definition of done.
18082
+ - Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
17336
18083
 
17337
18084
  **Contribution Tools:**
17338
18085
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -17392,6 +18139,11 @@ var genericAgilePlugin = {
17392
18139
  - **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 %.
17393
18140
  - Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
17394
18141
 
18142
+ **Date Enforcement:**
18143
+ - 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.
18144
+ - When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
18145
+ - Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
18146
+
17395
18147
  **Sprint Workflow:**
17396
18148
  - Create sprints with clear goals and date boundaries.
17397
18149
  - Assign epics to sprints via linkedEpics.
@@ -17411,7 +18163,7 @@ var genericAgilePlugin = {
17411
18163
  **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).
17412
18164
  **Meetings**: Meeting records with attendees, agendas, and notes.
17413
18165
 
17414
- **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.
18166
+ **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.
17415
18167
 
17416
18168
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
17417
18169
  - **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.
@@ -17457,7 +18209,7 @@ function createUseCaseTools(store) {
17457
18209
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17458
18210
  };
17459
18211
  },
17460
- { annotations: { readOnly: true } }
18212
+ { annotations: { readOnlyHint: true } }
17461
18213
  ),
17462
18214
  tool14(
17463
18215
  "get_use_case",
@@ -17484,7 +18236,7 @@ function createUseCaseTools(store) {
17484
18236
  ]
17485
18237
  };
17486
18238
  },
17487
- { annotations: { readOnly: true } }
18239
+ { annotations: { readOnlyHint: true } }
17488
18240
  ),
17489
18241
  tool14(
17490
18242
  "create_use_case",
@@ -17582,7 +18334,7 @@ function createTechAssessmentTools(store) {
17582
18334
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17583
18335
  };
17584
18336
  },
17585
- { annotations: { readOnly: true } }
18337
+ { annotations: { readOnlyHint: true } }
17586
18338
  ),
17587
18339
  tool15(
17588
18340
  "get_tech_assessment",
@@ -17609,7 +18361,7 @@ function createTechAssessmentTools(store) {
17609
18361
  ]
17610
18362
  };
17611
18363
  },
17612
- { annotations: { readOnly: true } }
18364
+ { annotations: { readOnlyHint: true } }
17613
18365
  ),
17614
18366
  tool15(
17615
18367
  "create_tech_assessment",
@@ -17743,7 +18495,7 @@ function createExtensionDesignTools(store) {
17743
18495
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17744
18496
  };
17745
18497
  },
17746
- { annotations: { readOnly: true } }
18498
+ { annotations: { readOnlyHint: true } }
17747
18499
  ),
17748
18500
  tool16(
17749
18501
  "get_extension_design",
@@ -17770,7 +18522,7 @@ function createExtensionDesignTools(store) {
17770
18522
  ]
17771
18523
  };
17772
18524
  },
17773
- { annotations: { readOnly: true } }
18525
+ { annotations: { readOnlyHint: true } }
17774
18526
  ),
17775
18527
  tool16(
17776
18528
  "create_extension_design",
@@ -17922,7 +18674,7 @@ function createAemReportTools(store) {
17922
18674
  ]
17923
18675
  };
17924
18676
  },
17925
- { annotations: { readOnly: true } }
18677
+ { annotations: { readOnlyHint: true } }
17926
18678
  ),
17927
18679
  tool17(
17928
18680
  "generate_tech_readiness",
@@ -17974,7 +18726,7 @@ function createAemReportTools(store) {
17974
18726
  ]
17975
18727
  };
17976
18728
  },
17977
- { annotations: { readOnly: true } }
18729
+ { annotations: { readOnlyHint: true } }
17978
18730
  ),
17979
18731
  tool17(
17980
18732
  "generate_phase_status",
@@ -18029,7 +18781,7 @@ function createAemReportTools(store) {
18029
18781
  ]
18030
18782
  };
18031
18783
  },
18032
- { annotations: { readOnly: true } }
18784
+ { annotations: { readOnlyHint: true } }
18033
18785
  )
18034
18786
  ];
18035
18787
  }
@@ -18061,7 +18813,7 @@ function createAemPhaseTools(store, marvinDir) {
18061
18813
  ]
18062
18814
  };
18063
18815
  },
18064
- { annotations: { readOnly: true } }
18816
+ { annotations: { readOnlyHint: true } }
18065
18817
  ),
18066
18818
  tool18(
18067
18819
  "advance_phase",
@@ -18506,7 +19258,7 @@ function createJiraTools(store) {
18506
19258
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
18507
19259
  };
18508
19260
  },
18509
- { annotations: { readOnly: true } }
19261
+ { annotations: { readOnlyHint: true } }
18510
19262
  ),
18511
19263
  tool19(
18512
19264
  "get_jira_issue",
@@ -18538,7 +19290,7 @@ function createJiraTools(store) {
18538
19290
  ]
18539
19291
  };
18540
19292
  },
18541
- { annotations: { readOnly: true } }
19293
+ { annotations: { readOnlyHint: true } }
18542
19294
  ),
18543
19295
  // --- Jira → Local tools ---
18544
19296
  tool19(
@@ -19269,7 +20021,7 @@ function createWebTools(store, projectName, navGroups) {
19269
20021
  content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
19270
20022
  };
19271
20023
  },
19272
- { annotations: { readOnly: true } }
20024
+ { annotations: { readOnlyHint: true } }
19273
20025
  ),
19274
20026
  tool20(
19275
20027
  "get_dashboard_overview",
@@ -19291,7 +20043,7 @@ function createWebTools(store, projectName, navGroups) {
19291
20043
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19292
20044
  };
19293
20045
  },
19294
- { annotations: { readOnly: true } }
20046
+ { annotations: { readOnlyHint: true } }
19295
20047
  ),
19296
20048
  tool20(
19297
20049
  "get_dashboard_gar",
@@ -19303,7 +20055,7 @@ function createWebTools(store, projectName, navGroups) {
19303
20055
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
19304
20056
  };
19305
20057
  },
19306
- { annotations: { readOnly: true } }
20058
+ { annotations: { readOnlyHint: true } }
19307
20059
  ),
19308
20060
  tool20(
19309
20061
  "get_dashboard_board",
@@ -19331,7 +20083,7 @@ function createWebTools(store, projectName, navGroups) {
19331
20083
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19332
20084
  };
19333
20085
  },
19334
- { annotations: { readOnly: true } }
20086
+ { annotations: { readOnlyHint: true } }
19335
20087
  )
19336
20088
  ];
19337
20089
  }
@@ -20029,7 +20781,7 @@ ${summaries}`
20029
20781
  content: [{ type: "text", text: guidance }]
20030
20782
  };
20031
20783
  },
20032
- { annotations: { readOnly: true } }
20784
+ { annotations: { readOnlyHint: true } }
20033
20785
  )
20034
20786
  ];
20035
20787
  }
@@ -22633,6 +23385,105 @@ function renderConfluence(report) {
22633
23385
  return lines.join("\n");
22634
23386
  }
22635
23387
 
23388
+ // src/reports/health/render-ascii.ts
23389
+ import chalk17 from "chalk";
23390
+ var STATUS_DOT2 = {
23391
+ green: chalk17.green("\u25CF"),
23392
+ amber: chalk17.yellow("\u25CF"),
23393
+ red: chalk17.red("\u25CF")
23394
+ };
23395
+ var STATUS_LABEL2 = {
23396
+ green: chalk17.green.bold("GREEN"),
23397
+ amber: chalk17.yellow.bold("AMBER"),
23398
+ red: chalk17.red.bold("RED")
23399
+ };
23400
+ var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
23401
+ function renderAscii2(report) {
23402
+ const lines = [];
23403
+ lines.push("");
23404
+ lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
23405
+ lines.push(chalk17.dim(` ${report.generatedAt}`));
23406
+ lines.push("");
23407
+ lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
23408
+ lines.push("");
23409
+ lines.push(` ${SEPARATOR2}`);
23410
+ lines.push(chalk17.bold(" Completeness"));
23411
+ lines.push(` ${SEPARATOR2}`);
23412
+ for (const cat of report.completeness) {
23413
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
23414
+ for (const item of cat.items) {
23415
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23416
+ }
23417
+ }
23418
+ lines.push("");
23419
+ lines.push(` ${SEPARATOR2}`);
23420
+ lines.push(chalk17.bold(" Process"));
23421
+ lines.push(` ${SEPARATOR2}`);
23422
+ for (const cat of report.process) {
23423
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
23424
+ for (const item of cat.items) {
23425
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23426
+ }
23427
+ }
23428
+ lines.push(` ${SEPARATOR2}`);
23429
+ lines.push("");
23430
+ return lines.join("\n");
23431
+ }
23432
+
23433
+ // src/reports/health/render-confluence.ts
23434
+ var EMOJI2 = {
23435
+ green: ":green_circle:",
23436
+ amber: ":yellow_circle:",
23437
+ red: ":red_circle:"
23438
+ };
23439
+ function renderConfluence2(report) {
23440
+ const lines = [];
23441
+ lines.push(`# Health Check \u2014 ${report.projectName}`);
23442
+ lines.push("");
23443
+ lines.push(`**Date:** ${report.generatedAt}`);
23444
+ lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
23445
+ lines.push("");
23446
+ lines.push("## Completeness");
23447
+ lines.push("");
23448
+ lines.push("| Category | Status | Summary |");
23449
+ lines.push("|----------|--------|---------|");
23450
+ for (const cat of report.completeness) {
23451
+ lines.push(
23452
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23453
+ );
23454
+ }
23455
+ lines.push("");
23456
+ for (const cat of report.completeness) {
23457
+ if (cat.items.length === 0) continue;
23458
+ lines.push(`### ${cat.name}`);
23459
+ lines.push("");
23460
+ for (const item of cat.items) {
23461
+ lines.push(`- **${item.id}** ${item.detail}`);
23462
+ }
23463
+ lines.push("");
23464
+ }
23465
+ lines.push("## Process");
23466
+ lines.push("");
23467
+ lines.push("| Metric | Status | Summary |");
23468
+ lines.push("|--------|--------|---------|");
23469
+ for (const cat of report.process) {
23470
+ lines.push(
23471
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23472
+ );
23473
+ }
23474
+ lines.push("");
23475
+ for (const cat of report.process) {
23476
+ if (cat.items.length === 0) continue;
23477
+ lines.push(`### ${cat.name}`);
23478
+ lines.push("");
23479
+ for (const item of cat.items) {
23480
+ lines.push(`- **${item.id}** ${item.detail}`);
23481
+ }
23482
+ lines.push("");
23483
+ }
23484
+ return lines.join("\n");
23485
+ }
23486
+
22636
23487
  // src/cli/commands/report.ts
22637
23488
  async function garReportCommand(options) {
22638
23489
  const project = loadProject();
@@ -22648,6 +23499,20 @@ async function garReportCommand(options) {
22648
23499
  console.log(renderAscii(report));
22649
23500
  }
22650
23501
  }
23502
+ async function healthReportCommand(options) {
23503
+ const project = loadProject();
23504
+ const plugin = resolvePlugin(project.config.methodology);
23505
+ const registrations = plugin?.documentTypeRegistrations ?? [];
23506
+ const store = new DocumentStore(project.marvinDir, registrations);
23507
+ const metrics = collectHealthMetrics(store);
23508
+ const report = evaluateHealth(project.config.name, metrics);
23509
+ const format = options.format ?? "ascii";
23510
+ if (format === "confluence") {
23511
+ console.log(renderConfluence2(report));
23512
+ } else {
23513
+ console.log(renderAscii2(report));
23514
+ }
23515
+ }
22651
23516
 
22652
23517
  // src/cli/commands/web.ts
22653
23518
  async function webCommand(options) {
@@ -22664,7 +23529,7 @@ function createProgram() {
22664
23529
  const program = new Command();
22665
23530
  program.name("marvin").description(
22666
23531
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
22667
- ).version("0.3.3");
23532
+ ).version("0.3.5");
22668
23533
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
22669
23534
  await initCommand();
22670
23535
  });
@@ -22741,6 +23606,12 @@ function createProgram() {
22741
23606
  ).action(async (options) => {
22742
23607
  await garReportCommand(options);
22743
23608
  });
23609
+ reportCmd.command("health").description("Generate a governance health check report").option(
23610
+ "--format <format>",
23611
+ "Output format: ascii or confluence (default: ascii)"
23612
+ ).action(async (options) => {
23613
+ await healthReportCommand(options);
23614
+ });
22744
23615
  program.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
22745
23616
  await webCommand(options);
22746
23617
  });