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/marvin.js CHANGED
@@ -13942,7 +13942,7 @@ function createMeetingTools(store) {
13942
13942
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
13943
13943
  };
13944
13944
  },
13945
- { annotations: { readOnly: true } }
13945
+ { annotations: { readOnlyHint: true } }
13946
13946
  ),
13947
13947
  tool(
13948
13948
  "get_meeting",
@@ -13969,7 +13969,7 @@ function createMeetingTools(store) {
13969
13969
  ]
13970
13970
  };
13971
13971
  },
13972
- { annotations: { readOnly: true } }
13972
+ { annotations: { readOnlyHint: true } }
13973
13973
  ),
13974
13974
  tool(
13975
13975
  "create_meeting",
@@ -14094,7 +14094,7 @@ function createMeetingTools(store) {
14094
14094
  content: [{ type: "text", text: sections.join("\n") }]
14095
14095
  };
14096
14096
  },
14097
- { annotations: { readOnly: true } }
14097
+ { annotations: { readOnlyHint: true } }
14098
14098
  )
14099
14099
  ];
14100
14100
  }
@@ -14111,9 +14111,17 @@ function collectGarMetrics(store) {
14111
14111
  const blockedItems = allDocs.filter(
14112
14112
  (d) => d.frontmatter.tags?.includes("blocked")
14113
14113
  );
14114
- const overdueItems = allDocs.filter(
14114
+ const tagOverdueItems = allDocs.filter(
14115
14115
  (d) => d.frontmatter.tags?.includes("overdue")
14116
14116
  );
14117
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14118
+ const dateOverdueActions = openActions.filter((d) => {
14119
+ const dueDate = d.frontmatter.dueDate;
14120
+ return typeof dueDate === "string" && dueDate < today;
14121
+ });
14122
+ const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
14123
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
14124
+ );
14117
14125
  const openQuestions = store.list({ type: "question", status: "open" });
14118
14126
  const riskItems = allDocs.filter(
14119
14127
  (d) => d.frontmatter.tags?.includes("risk")
@@ -14222,6 +14230,253 @@ function evaluateGar(projectName, metrics) {
14222
14230
  };
14223
14231
  }
14224
14232
 
14233
+ // src/reports/health/collector.ts
14234
+ var FIELD_CHECKS = [
14235
+ {
14236
+ type: "action",
14237
+ openStatuses: ["open", "in-progress"],
14238
+ requiredFields: ["owner", "priority", "dueDate", "content"]
14239
+ },
14240
+ {
14241
+ type: "decision",
14242
+ openStatuses: ["open", "proposed"],
14243
+ requiredFields: ["owner", "content"]
14244
+ },
14245
+ {
14246
+ type: "question",
14247
+ openStatuses: ["open"],
14248
+ requiredFields: ["owner", "content"]
14249
+ },
14250
+ {
14251
+ type: "feature",
14252
+ openStatuses: ["draft", "approved"],
14253
+ requiredFields: ["owner", "priority", "content"]
14254
+ },
14255
+ {
14256
+ type: "epic",
14257
+ openStatuses: ["planned", "in-progress"],
14258
+ requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
14259
+ },
14260
+ {
14261
+ type: "sprint",
14262
+ openStatuses: ["planned", "active"],
14263
+ requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
14264
+ }
14265
+ ];
14266
+ var STALE_THRESHOLD_DAYS = 14;
14267
+ var AGING_THRESHOLD_DAYS = 30;
14268
+ function daysBetween(a, b) {
14269
+ const msPerDay = 864e5;
14270
+ const dateA = new Date(a);
14271
+ const dateB = new Date(b);
14272
+ return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
14273
+ }
14274
+ function checkMissingFields(doc, requiredFields) {
14275
+ const missing = [];
14276
+ for (const field of requiredFields) {
14277
+ if (field === "content") {
14278
+ if (!doc.content || doc.content.trim().length === 0) {
14279
+ missing.push("content");
14280
+ }
14281
+ } else if (field === "linkedEpics") {
14282
+ const val = doc.frontmatter[field];
14283
+ if (!Array.isArray(val) || val.length === 0) {
14284
+ missing.push(field);
14285
+ }
14286
+ } else {
14287
+ const val = doc.frontmatter[field];
14288
+ if (val === void 0 || val === null || val === "") {
14289
+ missing.push(field);
14290
+ }
14291
+ }
14292
+ }
14293
+ return missing;
14294
+ }
14295
+ function collectCompleteness(store) {
14296
+ const result = {};
14297
+ for (const check2 of FIELD_CHECKS) {
14298
+ const allOfType = store.list({ type: check2.type });
14299
+ const openDocs = allOfType.filter(
14300
+ (d) => check2.openStatuses.includes(d.frontmatter.status)
14301
+ );
14302
+ const gaps = [];
14303
+ let complete = 0;
14304
+ for (const doc of openDocs) {
14305
+ const missingFields = checkMissingFields(doc, check2.requiredFields);
14306
+ if (missingFields.length === 0) {
14307
+ complete++;
14308
+ } else {
14309
+ gaps.push({
14310
+ id: doc.frontmatter.id,
14311
+ title: doc.frontmatter.title,
14312
+ missingFields
14313
+ });
14314
+ }
14315
+ }
14316
+ result[check2.type] = {
14317
+ total: openDocs.length,
14318
+ complete,
14319
+ gaps
14320
+ };
14321
+ }
14322
+ return result;
14323
+ }
14324
+ function collectProcess(store) {
14325
+ const today = (/* @__PURE__ */ new Date()).toISOString();
14326
+ const allDocs = store.list();
14327
+ const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
14328
+ const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
14329
+ const stale = [];
14330
+ for (const doc of openDocs) {
14331
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
14332
+ const days = daysBetween(updated, today);
14333
+ if (days >= STALE_THRESHOLD_DAYS) {
14334
+ stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
14335
+ }
14336
+ }
14337
+ const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
14338
+ const agingActions = [];
14339
+ for (const doc of openActions) {
14340
+ const days = daysBetween(doc.frontmatter.created, today);
14341
+ if (days >= AGING_THRESHOLD_DAYS) {
14342
+ agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
14343
+ }
14344
+ }
14345
+ const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
14346
+ let decisionTotal = 0;
14347
+ for (const doc of resolvedDecisions) {
14348
+ decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
14349
+ }
14350
+ const decisionVelocity = {
14351
+ avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
14352
+ count: resolvedDecisions.length
14353
+ };
14354
+ const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
14355
+ let questionTotal = 0;
14356
+ for (const doc of answeredQuestions) {
14357
+ questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
14358
+ }
14359
+ const questionResolution = {
14360
+ avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
14361
+ count: answeredQuestions.length
14362
+ };
14363
+ return { stale, agingActions, decisionVelocity, questionResolution };
14364
+ }
14365
+ function collectHealthMetrics(store) {
14366
+ return {
14367
+ completeness: collectCompleteness(store),
14368
+ process: collectProcess(store)
14369
+ };
14370
+ }
14371
+
14372
+ // src/reports/health/evaluator.ts
14373
+ function worstStatus2(statuses) {
14374
+ if (statuses.includes("red")) return "red";
14375
+ if (statuses.includes("amber")) return "amber";
14376
+ return "green";
14377
+ }
14378
+ function completenessStatus(total, complete) {
14379
+ if (total === 0) return "green";
14380
+ const pct = Math.round(complete / total * 100);
14381
+ if (pct >= 100) return "green";
14382
+ if (pct >= 75) return "amber";
14383
+ return "red";
14384
+ }
14385
+ var TYPE_LABELS = {
14386
+ action: "Actions",
14387
+ decision: "Decisions",
14388
+ question: "Questions",
14389
+ feature: "Features",
14390
+ epic: "Epics",
14391
+ sprint: "Sprints"
14392
+ };
14393
+ function evaluateHealth(projectName, metrics) {
14394
+ const completeness = [];
14395
+ for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
14396
+ const { total, complete, gaps } = catMetrics;
14397
+ const status = completenessStatus(total, complete);
14398
+ const pct = total > 0 ? Math.round(complete / total * 100) : 100;
14399
+ completeness.push({
14400
+ name: TYPE_LABELS[type] ?? type,
14401
+ status,
14402
+ summary: `${pct}% complete (${complete}/${total})`,
14403
+ items: gaps.map((g) => ({
14404
+ id: g.id,
14405
+ detail: `missing: ${g.missingFields.join(", ")}`
14406
+ }))
14407
+ });
14408
+ }
14409
+ const process3 = [];
14410
+ const staleCount = metrics.process.stale.length;
14411
+ const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
14412
+ process3.push({
14413
+ name: "Stale Items",
14414
+ status: staleStatus,
14415
+ summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
14416
+ items: metrics.process.stale.map((s) => ({
14417
+ id: s.id,
14418
+ detail: `${s.days} days since last update`
14419
+ }))
14420
+ });
14421
+ const agingCount = metrics.process.agingActions.length;
14422
+ const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
14423
+ process3.push({
14424
+ name: "Aging Actions",
14425
+ status: agingStatus,
14426
+ summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
14427
+ items: metrics.process.agingActions.map((a) => ({
14428
+ id: a.id,
14429
+ detail: `open for ${a.days} days`
14430
+ }))
14431
+ });
14432
+ const dv = metrics.process.decisionVelocity;
14433
+ let dvStatus;
14434
+ if (dv.count === 0) {
14435
+ dvStatus = "green";
14436
+ } else if (dv.avgDays <= 7) {
14437
+ dvStatus = "green";
14438
+ } else if (dv.avgDays <= 21) {
14439
+ dvStatus = "amber";
14440
+ } else {
14441
+ dvStatus = "red";
14442
+ }
14443
+ process3.push({
14444
+ name: "Decision Velocity",
14445
+ status: dvStatus,
14446
+ summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
14447
+ items: []
14448
+ });
14449
+ const qr = metrics.process.questionResolution;
14450
+ let qrStatus;
14451
+ if (qr.count === 0) {
14452
+ qrStatus = "green";
14453
+ } else if (qr.avgDays <= 7) {
14454
+ qrStatus = "green";
14455
+ } else if (qr.avgDays <= 14) {
14456
+ qrStatus = "amber";
14457
+ } else {
14458
+ qrStatus = "red";
14459
+ }
14460
+ process3.push({
14461
+ name: "Question Resolution",
14462
+ status: qrStatus,
14463
+ summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
14464
+ items: []
14465
+ });
14466
+ const allStatuses = [
14467
+ ...completeness.map((c) => c.status),
14468
+ ...process3.map((p) => p.status)
14469
+ ];
14470
+ const overall = worstStatus2(allStatuses);
14471
+ return {
14472
+ projectName,
14473
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
14474
+ overall,
14475
+ completeness,
14476
+ process: process3
14477
+ };
14478
+ }
14479
+
14225
14480
  // src/plugins/builtin/tools/reports.ts
14226
14481
  function createReportTools(store) {
14227
14482
  return [
@@ -14241,7 +14496,8 @@ function createReportTools(store) {
14241
14496
  id: d.frontmatter.id,
14242
14497
  title: d.frontmatter.title,
14243
14498
  owner: d.frontmatter.owner,
14244
- priority: d.frontmatter.priority
14499
+ priority: d.frontmatter.priority,
14500
+ dueDate: d.frontmatter.dueDate
14245
14501
  })),
14246
14502
  completedActions: completedActions.map((d) => ({
14247
14503
  id: d.frontmatter.id,
@@ -14260,7 +14516,7 @@ function createReportTools(store) {
14260
14516
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
14261
14517
  };
14262
14518
  },
14263
- { annotations: { readOnly: true } }
14519
+ { annotations: { readOnlyHint: true } }
14264
14520
  ),
14265
14521
  tool2(
14266
14522
  "generate_risk_register",
@@ -14304,7 +14560,7 @@ function createReportTools(store) {
14304
14560
  content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
14305
14561
  };
14306
14562
  },
14307
- { annotations: { readOnly: true } }
14563
+ { annotations: { readOnlyHint: true } }
14308
14564
  ),
14309
14565
  tool2(
14310
14566
  "generate_gar_report",
@@ -14317,7 +14573,7 @@ function createReportTools(store) {
14317
14573
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
14318
14574
  };
14319
14575
  },
14320
- { annotations: { readOnly: true } }
14576
+ { annotations: { readOnlyHint: true } }
14321
14577
  ),
14322
14578
  tool2(
14323
14579
  "generate_epic_progress",
@@ -14402,7 +14658,7 @@ function createReportTools(store) {
14402
14658
  ]
14403
14659
  };
14404
14660
  },
14405
- { annotations: { readOnly: true } }
14661
+ { annotations: { readOnlyHint: true } }
14406
14662
  ),
14407
14663
  tool2(
14408
14664
  "generate_sprint_progress",
@@ -14447,7 +14703,8 @@ function createReportTools(store) {
14447
14703
  id: d.frontmatter.id,
14448
14704
  title: d.frontmatter.title,
14449
14705
  type: d.frontmatter.type,
14450
- status: d.frontmatter.status
14706
+ status: d.frontmatter.status,
14707
+ dueDate: d.frontmatter.dueDate
14451
14708
  }))
14452
14709
  }
14453
14710
  };
@@ -14456,7 +14713,7 @@ function createReportTools(store) {
14456
14713
  content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
14457
14714
  };
14458
14715
  },
14459
- { annotations: { readOnly: true } }
14716
+ { annotations: { readOnlyHint: true } }
14460
14717
  ),
14461
14718
  tool2(
14462
14719
  "generate_feature_progress",
@@ -14496,7 +14753,20 @@ function createReportTools(store) {
14496
14753
  content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
14497
14754
  };
14498
14755
  },
14499
- { annotations: { readOnly: true } }
14756
+ { annotations: { readOnlyHint: true } }
14757
+ ),
14758
+ tool2(
14759
+ "generate_health_report",
14760
+ "Generate a governance health check report covering artifact completeness and process health metrics",
14761
+ {},
14762
+ async () => {
14763
+ const metrics = collectHealthMetrics(store);
14764
+ const report = evaluateHealth("project", metrics);
14765
+ return {
14766
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
14767
+ };
14768
+ },
14769
+ { annotations: { readOnlyHint: true } }
14500
14770
  ),
14501
14771
  tool2(
14502
14772
  "save_report",
@@ -14558,7 +14828,7 @@ function createFeatureTools(store) {
14558
14828
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14559
14829
  };
14560
14830
  },
14561
- { annotations: { readOnly: true } }
14831
+ { annotations: { readOnlyHint: true } }
14562
14832
  ),
14563
14833
  tool3(
14564
14834
  "get_feature",
@@ -14585,7 +14855,7 @@ function createFeatureTools(store) {
14585
14855
  ]
14586
14856
  };
14587
14857
  },
14588
- { annotations: { readOnly: true } }
14858
+ { annotations: { readOnlyHint: true } }
14589
14859
  ),
14590
14860
  tool3(
14591
14861
  "create_feature",
@@ -14676,7 +14946,7 @@ function createEpicTools(store) {
14676
14946
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14677
14947
  };
14678
14948
  },
14679
- { annotations: { readOnly: true } }
14949
+ { annotations: { readOnlyHint: true } }
14680
14950
  ),
14681
14951
  tool4(
14682
14952
  "get_epic",
@@ -14703,7 +14973,7 @@ function createEpicTools(store) {
14703
14973
  ]
14704
14974
  };
14705
14975
  },
14706
- { annotations: { readOnly: true } }
14976
+ { annotations: { readOnlyHint: true } }
14707
14977
  ),
14708
14978
  tool4(
14709
14979
  "create_epic",
@@ -14835,7 +15105,7 @@ function createContributionTools(store) {
14835
15105
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14836
15106
  };
14837
15107
  },
14838
- { annotations: { readOnly: true } }
15108
+ { annotations: { readOnlyHint: true } }
14839
15109
  ),
14840
15110
  tool5(
14841
15111
  "get_contribution",
@@ -14862,7 +15132,7 @@ function createContributionTools(store) {
14862
15132
  ]
14863
15133
  };
14864
15134
  },
14865
- { annotations: { readOnly: true } }
15135
+ { annotations: { readOnlyHint: true } }
14866
15136
  ),
14867
15137
  tool5(
14868
15138
  "create_contribution",
@@ -14947,7 +15217,7 @@ function createSprintTools(store) {
14947
15217
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14948
15218
  };
14949
15219
  },
14950
- { annotations: { readOnly: true } }
15220
+ { annotations: { readOnlyHint: true } }
14951
15221
  ),
14952
15222
  tool6(
14953
15223
  "get_sprint",
@@ -14974,7 +15244,7 @@ function createSprintTools(store) {
14974
15244
  ]
14975
15245
  };
14976
15246
  },
14977
- { annotations: { readOnly: true } }
15247
+ { annotations: { readOnlyHint: true } }
14978
15248
  ),
14979
15249
  tool6(
14980
15250
  "create_sprint",
@@ -15271,7 +15541,7 @@ function createSprintPlanningTools(store) {
15271
15541
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15272
15542
  };
15273
15543
  },
15274
- { annotations: { readOnly: true } }
15544
+ { annotations: { readOnlyHint: true } }
15275
15545
  )
15276
15546
  ];
15277
15547
  }
@@ -15325,6 +15595,7 @@ var genericAgilePlugin = {
15325
15595
  - Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
15326
15596
  - Use priority levels (critical, high, medium, low) to communicate business value.
15327
15597
  - Tag features for categorization and cross-referencing.
15598
+ - Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
15328
15599
 
15329
15600
  **Contribution Tools:**
15330
15601
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -15355,6 +15626,7 @@ var genericAgilePlugin = {
15355
15626
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
15356
15627
  - Collaborate with the Delivery Manager on target dates and effort estimates.
15357
15628
  - Each epic should have a clear scope and definition of done.
15629
+ - Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
15358
15630
 
15359
15631
  **Contribution Tools:**
15360
15632
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -15414,6 +15686,11 @@ var genericAgilePlugin = {
15414
15686
  - **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 %.
15415
15687
  - Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
15416
15688
 
15689
+ **Date Enforcement:**
15690
+ - 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.
15691
+ - When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
15692
+ - Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
15693
+
15417
15694
  **Sprint Workflow:**
15418
15695
  - Create sprints with clear goals and date boundaries.
15419
15696
  - Assign epics to sprints via linkedEpics.
@@ -15433,7 +15710,7 @@ var genericAgilePlugin = {
15433
15710
  **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).
15434
15711
  **Meetings**: Meeting records with attendees, agendas, and notes.
15435
15712
 
15436
- **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.
15713
+ **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.
15437
15714
 
15438
15715
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
15439
15716
  - **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.
@@ -15479,7 +15756,7 @@ function createUseCaseTools(store) {
15479
15756
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15480
15757
  };
15481
15758
  },
15482
- { annotations: { readOnly: true } }
15759
+ { annotations: { readOnlyHint: true } }
15483
15760
  ),
15484
15761
  tool8(
15485
15762
  "get_use_case",
@@ -15506,7 +15783,7 @@ function createUseCaseTools(store) {
15506
15783
  ]
15507
15784
  };
15508
15785
  },
15509
- { annotations: { readOnly: true } }
15786
+ { annotations: { readOnlyHint: true } }
15510
15787
  ),
15511
15788
  tool8(
15512
15789
  "create_use_case",
@@ -15604,7 +15881,7 @@ function createTechAssessmentTools(store) {
15604
15881
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15605
15882
  };
15606
15883
  },
15607
- { annotations: { readOnly: true } }
15884
+ { annotations: { readOnlyHint: true } }
15608
15885
  ),
15609
15886
  tool9(
15610
15887
  "get_tech_assessment",
@@ -15631,7 +15908,7 @@ function createTechAssessmentTools(store) {
15631
15908
  ]
15632
15909
  };
15633
15910
  },
15634
- { annotations: { readOnly: true } }
15911
+ { annotations: { readOnlyHint: true } }
15635
15912
  ),
15636
15913
  tool9(
15637
15914
  "create_tech_assessment",
@@ -15765,7 +16042,7 @@ function createExtensionDesignTools(store) {
15765
16042
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15766
16043
  };
15767
16044
  },
15768
- { annotations: { readOnly: true } }
16045
+ { annotations: { readOnlyHint: true } }
15769
16046
  ),
15770
16047
  tool10(
15771
16048
  "get_extension_design",
@@ -15792,7 +16069,7 @@ function createExtensionDesignTools(store) {
15792
16069
  ]
15793
16070
  };
15794
16071
  },
15795
- { annotations: { readOnly: true } }
16072
+ { annotations: { readOnlyHint: true } }
15796
16073
  ),
15797
16074
  tool10(
15798
16075
  "create_extension_design",
@@ -15944,7 +16221,7 @@ function createAemReportTools(store) {
15944
16221
  ]
15945
16222
  };
15946
16223
  },
15947
- { annotations: { readOnly: true } }
16224
+ { annotations: { readOnlyHint: true } }
15948
16225
  ),
15949
16226
  tool11(
15950
16227
  "generate_tech_readiness",
@@ -15996,7 +16273,7 @@ function createAemReportTools(store) {
15996
16273
  ]
15997
16274
  };
15998
16275
  },
15999
- { annotations: { readOnly: true } }
16276
+ { annotations: { readOnlyHint: true } }
16000
16277
  ),
16001
16278
  tool11(
16002
16279
  "generate_phase_status",
@@ -16051,7 +16328,7 @@ function createAemReportTools(store) {
16051
16328
  ]
16052
16329
  };
16053
16330
  },
16054
- { annotations: { readOnly: true } }
16331
+ { annotations: { readOnlyHint: true } }
16055
16332
  )
16056
16333
  ];
16057
16334
  }
@@ -16083,7 +16360,7 @@ function createAemPhaseTools(store, marvinDir) {
16083
16360
  ]
16084
16361
  };
16085
16362
  },
16086
- { annotations: { readOnly: true } }
16363
+ { annotations: { readOnlyHint: true } }
16087
16364
  ),
16088
16365
  tool12(
16089
16366
  "advance_phase",
@@ -16517,6 +16794,8 @@ var deliveryManager = {
16517
16794
 
16518
16795
  ## How You Work
16519
16796
  - Review open actions (A-xxx) and follow up on overdue items
16797
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
16798
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
16520
16799
  - Ensure decisions (D-xxx) are properly documented with rationale
16521
16800
  - Track questions (Q-xxx) and ensure they get answered
16522
16801
  - Monitor project health and flag risks early
@@ -16631,7 +16910,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
16631
16910
  ## Available Tools
16632
16911
  You have access to governance tools for managing project artifacts:
16633
16912
  - **Decisions** (D-xxx): List, get, create, and update decisions
16634
- - **Actions** (A-xxx): List, get, create, and update action items
16913
+ - **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
16635
16914
  - **Questions** (Q-xxx): List, get, create, and update questions
16636
16915
  - **Features** (F-xxx): List, get, create, and update feature definitions
16637
16916
  - **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
@@ -16982,7 +17261,7 @@ function createDecisionTools(store) {
16982
17261
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16983
17262
  };
16984
17263
  },
16985
- { annotations: { readOnly: true } }
17264
+ { annotations: { readOnlyHint: true } }
16986
17265
  ),
16987
17266
  tool13(
16988
17267
  "get_decision",
@@ -17009,7 +17288,7 @@ function createDecisionTools(store) {
17009
17288
  ]
17010
17289
  };
17011
17290
  },
17012
- { annotations: { readOnly: true } }
17291
+ { annotations: { readOnlyHint: true } }
17013
17292
  ),
17014
17293
  tool13(
17015
17294
  "create_decision",
@@ -17070,6 +17349,19 @@ function createDecisionTools(store) {
17070
17349
 
17071
17350
  // src/agent/tools/actions.ts
17072
17351
  import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17352
+ function findMatchingSprints(store, dueDate) {
17353
+ const sprints = store.list({ type: "sprint" });
17354
+ return sprints.filter((s) => {
17355
+ const start = s.frontmatter.startDate;
17356
+ const end = s.frontmatter.endDate;
17357
+ return start && end && dueDate >= start && dueDate <= end;
17358
+ }).map((s) => ({
17359
+ id: s.frontmatter.id,
17360
+ title: s.frontmatter.title,
17361
+ startDate: s.frontmatter.startDate,
17362
+ endDate: s.frontmatter.endDate
17363
+ }));
17364
+ }
17073
17365
  function createActionTools(store) {
17074
17366
  return [
17075
17367
  tool14(
@@ -17085,19 +17377,24 @@ function createActionTools(store) {
17085
17377
  status: args.status,
17086
17378
  owner: args.owner
17087
17379
  });
17088
- const summary = docs.map((d) => ({
17089
- id: d.frontmatter.id,
17090
- title: d.frontmatter.title,
17091
- status: d.frontmatter.status,
17092
- owner: d.frontmatter.owner,
17093
- priority: d.frontmatter.priority,
17094
- created: d.frontmatter.created
17095
- }));
17380
+ const summary = docs.map((d) => {
17381
+ const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
17382
+ return {
17383
+ id: d.frontmatter.id,
17384
+ title: d.frontmatter.title,
17385
+ status: d.frontmatter.status,
17386
+ owner: d.frontmatter.owner,
17387
+ priority: d.frontmatter.priority,
17388
+ dueDate: d.frontmatter.dueDate,
17389
+ sprints: sprintIds.length > 0 ? sprintIds : void 0,
17390
+ created: d.frontmatter.created
17391
+ };
17392
+ });
17096
17393
  return {
17097
17394
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17098
17395
  };
17099
17396
  },
17100
- { annotations: { readOnly: true } }
17397
+ { annotations: { readOnlyHint: true } }
17101
17398
  ),
17102
17399
  tool14(
17103
17400
  "get_action",
@@ -17124,7 +17421,7 @@ function createActionTools(store) {
17124
17421
  ]
17125
17422
  };
17126
17423
  },
17127
- { annotations: { readOnly: true } }
17424
+ { annotations: { readOnlyHint: true } }
17128
17425
  ),
17129
17426
  tool14(
17130
17427
  "create_action",
@@ -17135,9 +17432,18 @@ function createActionTools(store) {
17135
17432
  status: external_exports.string().optional().describe("Status (default: 'open')"),
17136
17433
  owner: external_exports.string().optional().describe("Person responsible"),
17137
17434
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
17138
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
17435
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
17436
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
17437
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
17139
17438
  },
17140
17439
  async (args) => {
17440
+ const tags = [...args.tags ?? []];
17441
+ if (args.sprints) {
17442
+ for (const sprintId of args.sprints) {
17443
+ const tag = `sprint:${sprintId}`;
17444
+ if (!tags.includes(tag)) tags.push(tag);
17445
+ }
17446
+ }
17141
17447
  const doc = store.create(
17142
17448
  "action",
17143
17449
  {
@@ -17145,17 +17451,21 @@ function createActionTools(store) {
17145
17451
  status: args.status,
17146
17452
  owner: args.owner,
17147
17453
  priority: args.priority,
17148
- tags: args.tags
17454
+ tags: tags.length > 0 ? tags : void 0,
17455
+ dueDate: args.dueDate
17149
17456
  },
17150
17457
  args.content
17151
17458
  );
17459
+ const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
17460
+ if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
17461
+ const matching = findMatchingSprints(store, args.dueDate);
17462
+ if (matching.length > 0) {
17463
+ const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
17464
+ parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
17465
+ }
17466
+ }
17152
17467
  return {
17153
- content: [
17154
- {
17155
- type: "text",
17156
- text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
17157
- }
17158
- ]
17468
+ content: [{ type: "text", text: parts.join("\n") }]
17159
17469
  };
17160
17470
  }
17161
17471
  ),
@@ -17168,10 +17478,25 @@ function createActionTools(store) {
17168
17478
  status: external_exports.string().optional().describe("New status"),
17169
17479
  content: external_exports.string().optional().describe("New content"),
17170
17480
  owner: external_exports.string().optional().describe("New owner"),
17171
- priority: external_exports.string().optional().describe("New priority")
17481
+ priority: external_exports.string().optional().describe("New priority"),
17482
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
17483
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
17172
17484
  },
17173
17485
  async (args) => {
17174
- const { id, content, ...updates } = args;
17486
+ const { id, content, sprints, ...updates } = args;
17487
+ if (sprints !== void 0) {
17488
+ const existing = store.get(id);
17489
+ if (!existing) {
17490
+ return {
17491
+ content: [{ type: "text", text: `Action ${id} not found` }],
17492
+ isError: true
17493
+ };
17494
+ }
17495
+ const existingTags = existing.frontmatter.tags ?? [];
17496
+ const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
17497
+ const newSprintTags = sprints.map((s) => `sprint:${s}`);
17498
+ updates.tags = [...nonSprintTags, ...newSprintTags];
17499
+ }
17175
17500
  const doc = store.update(id, updates, content);
17176
17501
  return {
17177
17502
  content: [
@@ -17182,6 +17507,35 @@ function createActionTools(store) {
17182
17507
  ]
17183
17508
  };
17184
17509
  }
17510
+ ),
17511
+ tool14(
17512
+ "suggest_sprints_for_action",
17513
+ "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
17514
+ {
17515
+ dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
17516
+ },
17517
+ async (args) => {
17518
+ const matching = findMatchingSprints(store, args.dueDate);
17519
+ if (matching.length === 0) {
17520
+ return {
17521
+ content: [
17522
+ {
17523
+ type: "text",
17524
+ text: `No sprints found containing dueDate ${args.dueDate}.`
17525
+ }
17526
+ ]
17527
+ };
17528
+ }
17529
+ return {
17530
+ content: [
17531
+ {
17532
+ type: "text",
17533
+ text: JSON.stringify(matching, null, 2)
17534
+ }
17535
+ ]
17536
+ };
17537
+ },
17538
+ { annotations: { readOnlyHint: true } }
17185
17539
  )
17186
17540
  ];
17187
17541
  }
@@ -17209,7 +17563,7 @@ function createQuestionTools(store) {
17209
17563
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17210
17564
  };
17211
17565
  },
17212
- { annotations: { readOnly: true } }
17566
+ { annotations: { readOnlyHint: true } }
17213
17567
  ),
17214
17568
  tool15(
17215
17569
  "get_question",
@@ -17236,7 +17590,7 @@ function createQuestionTools(store) {
17236
17590
  ]
17237
17591
  };
17238
17592
  },
17239
- { annotations: { readOnly: true } }
17593
+ { annotations: { readOnlyHint: true } }
17240
17594
  ),
17241
17595
  tool15(
17242
17596
  "create_question",
@@ -17329,7 +17683,7 @@ function createDocumentTools(store) {
17329
17683
  ]
17330
17684
  };
17331
17685
  },
17332
- { annotations: { readOnly: true } }
17686
+ { annotations: { readOnlyHint: true } }
17333
17687
  ),
17334
17688
  tool16(
17335
17689
  "read_document",
@@ -17356,7 +17710,7 @@ function createDocumentTools(store) {
17356
17710
  ]
17357
17711
  };
17358
17712
  },
17359
- { annotations: { readOnly: true } }
17713
+ { annotations: { readOnlyHint: true } }
17360
17714
  ),
17361
17715
  tool16(
17362
17716
  "project_summary",
@@ -17384,7 +17738,7 @@ function createDocumentTools(store) {
17384
17738
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17385
17739
  };
17386
17740
  },
17387
- { annotations: { readOnly: true } }
17741
+ { annotations: { readOnlyHint: true } }
17388
17742
  )
17389
17743
  ];
17390
17744
  }
@@ -17421,7 +17775,7 @@ function createSourceTools(manifest) {
17421
17775
  ]
17422
17776
  };
17423
17777
  },
17424
- { annotations: { readOnly: true } }
17778
+ { annotations: { readOnlyHint: true } }
17425
17779
  ),
17426
17780
  tool17(
17427
17781
  "get_source_info",
@@ -17455,7 +17809,7 @@ function createSourceTools(manifest) {
17455
17809
  ]
17456
17810
  };
17457
17811
  },
17458
- { annotations: { readOnly: true } }
17812
+ { annotations: { readOnlyHint: true } }
17459
17813
  )
17460
17814
  ];
17461
17815
  }
@@ -17486,7 +17840,7 @@ function createSessionTools(store) {
17486
17840
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17487
17841
  };
17488
17842
  },
17489
- { annotations: { readOnly: true } }
17843
+ { annotations: { readOnlyHint: true } }
17490
17844
  ),
17491
17845
  tool18(
17492
17846
  "get_session",
@@ -17504,7 +17858,7 @@ function createSessionTools(store) {
17504
17858
  content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
17505
17859
  };
17506
17860
  },
17507
- { annotations: { readOnly: true } }
17861
+ { annotations: { readOnlyHint: true } }
17508
17862
  )
17509
17863
  ];
17510
17864
  }
@@ -17584,6 +17938,46 @@ function getBoardData(store, type) {
17584
17938
  }));
17585
17939
  return { columns, type, types };
17586
17940
  }
17941
+ function getDiagramData(store) {
17942
+ const allDocs = store.list();
17943
+ const sprints = [];
17944
+ const epics = [];
17945
+ const features = [];
17946
+ const statusCounts = {};
17947
+ for (const doc of allDocs) {
17948
+ const fm = doc.frontmatter;
17949
+ const status = fm.status.toLowerCase();
17950
+ statusCounts[status] = (statusCounts[status] ?? 0) + 1;
17951
+ switch (fm.type) {
17952
+ case "sprint":
17953
+ sprints.push({
17954
+ id: fm.id,
17955
+ title: fm.title,
17956
+ status: fm.status,
17957
+ startDate: fm.startDate,
17958
+ endDate: fm.endDate,
17959
+ linkedEpics: fm.linkedEpics ?? []
17960
+ });
17961
+ break;
17962
+ case "epic":
17963
+ epics.push({
17964
+ id: fm.id,
17965
+ title: fm.title,
17966
+ status: fm.status,
17967
+ linkedFeature: fm.linkedFeature
17968
+ });
17969
+ break;
17970
+ case "feature":
17971
+ features.push({
17972
+ id: fm.id,
17973
+ title: fm.title,
17974
+ status: fm.status
17975
+ });
17976
+ break;
17977
+ }
17978
+ }
17979
+ return { sprints, epics, features, statusCounts };
17980
+ }
17587
17981
 
17588
17982
  // src/web/templates/layout.ts
17589
17983
  function escapeHtml(str) {
@@ -17614,16 +18008,43 @@ function renderMarkdown(md) {
17614
18008
  const out = [];
17615
18009
  let inList = false;
17616
18010
  let listTag = "ul";
17617
- for (const raw of lines) {
17618
- const line = raw;
18011
+ let inTable = false;
18012
+ let i = 0;
18013
+ while (i < lines.length) {
18014
+ const line = lines[i];
17619
18015
  if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
17620
18016
  out.push(`</${listTag}>`);
17621
18017
  inList = false;
17622
18018
  }
18019
+ if (inTable && !/^\s*\|/.test(line)) {
18020
+ out.push("</tbody></table></div>");
18021
+ inTable = false;
18022
+ }
18023
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
18024
+ i++;
18025
+ out.push("<hr>");
18026
+ continue;
18027
+ }
18028
+ if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
18029
+ const headers = parseTableRow(line);
18030
+ out.push('<div class="table-wrap"><table><thead><tr>');
18031
+ out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
18032
+ out.push("</tr></thead><tbody>");
18033
+ inTable = true;
18034
+ i += 2;
18035
+ continue;
18036
+ }
18037
+ if (inTable && /^\s*\|/.test(line)) {
18038
+ const cells = parseTableRow(line);
18039
+ out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
18040
+ i++;
18041
+ continue;
18042
+ }
17623
18043
  const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
17624
18044
  if (headingMatch) {
17625
18045
  const level = headingMatch[1].length;
17626
18046
  out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
18047
+ i++;
17627
18048
  continue;
17628
18049
  }
17629
18050
  const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
@@ -17635,6 +18056,7 @@ function renderMarkdown(md) {
17635
18056
  listTag = "ul";
17636
18057
  }
17637
18058
  out.push(`<li>${inline(ulMatch[1])}</li>`);
18059
+ i++;
17638
18060
  continue;
17639
18061
  }
17640
18062
  const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
@@ -17646,6 +18068,7 @@ function renderMarkdown(md) {
17646
18068
  listTag = "ol";
17647
18069
  }
17648
18070
  out.push(`<li>${inline(olMatch[1])}</li>`);
18071
+ i++;
17649
18072
  continue;
17650
18073
  }
17651
18074
  if (line.trim() === "") {
@@ -17653,13 +18076,19 @@ function renderMarkdown(md) {
17653
18076
  out.push(`</${listTag}>`);
17654
18077
  inList = false;
17655
18078
  }
18079
+ i++;
17656
18080
  continue;
17657
18081
  }
17658
18082
  out.push(`<p>${inline(line)}</p>`);
18083
+ i++;
17659
18084
  }
17660
18085
  if (inList) out.push(`</${listTag}>`);
18086
+ if (inTable) out.push("</tbody></table></div>");
17661
18087
  return out.join("\n");
17662
18088
  }
18089
+ function parseTableRow(line) {
18090
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
18091
+ }
17663
18092
  function inline(text) {
17664
18093
  let s = escapeHtml(text);
17665
18094
  s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
@@ -17673,7 +18102,8 @@ function layout(opts, body) {
17673
18102
  const topItems = [
17674
18103
  { href: "/", label: "Overview" },
17675
18104
  { href: "/board", label: "Board" },
17676
- { href: "/gar", label: "GAR Report" }
18105
+ { href: "/gar", label: "GAR Report" },
18106
+ { href: "/health", label: "Health" }
17677
18107
  ];
17678
18108
  const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
17679
18109
  const groupsHtml = opts.navGroups.map((group) => {
@@ -17708,9 +18138,15 @@ function layout(opts, body) {
17708
18138
  </nav>
17709
18139
  </aside>
17710
18140
  <main class="main">
18141
+ <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18142
+ <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>
18143
+ <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>
18144
+ </button>
17711
18145
  ${body}
17712
18146
  </main>
17713
18147
  </div>
18148
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18149
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
17714
18150
  </body>
17715
18151
  </html>`;
17716
18152
  }
@@ -17825,7 +18261,33 @@ a:hover { text-decoration: underline; }
17825
18261
  flex: 1;
17826
18262
  padding: 2rem 2.5rem;
17827
18263
  max-width: 1200px;
18264
+ position: relative;
18265
+ transition: max-width 0.2s ease;
18266
+ }
18267
+ .main.expanded {
18268
+ max-width: none;
18269
+ }
18270
+ .expand-toggle {
18271
+ position: absolute;
18272
+ top: 1rem;
18273
+ right: 1rem;
18274
+ background: var(--bg-card);
18275
+ border: 1px solid var(--border);
18276
+ border-radius: var(--radius);
18277
+ color: var(--text-dim);
18278
+ cursor: pointer;
18279
+ padding: 0.4rem;
18280
+ display: flex;
18281
+ align-items: center;
18282
+ justify-content: center;
18283
+ transition: color 0.15s, border-color 0.15s;
17828
18284
  }
18285
+ .expand-toggle:hover {
18286
+ color: var(--text);
18287
+ border-color: var(--text-dim);
18288
+ }
18289
+ .main.expanded .icon-expand { display: none; }
18290
+ .main:not(.expanded) .icon-collapse { display: none; }
17829
18291
 
17830
18292
  /* Page header */
17831
18293
  .page-header {
@@ -17854,12 +18316,26 @@ a:hover { text-decoration: underline; }
17854
18316
  .breadcrumb a:hover { color: var(--accent); }
17855
18317
  .breadcrumb .sep { margin: 0 0.4rem; }
17856
18318
 
18319
+ /* Card groups */
18320
+ .card-group {
18321
+ margin-bottom: 1.5rem;
18322
+ }
18323
+
18324
+ .card-group-label {
18325
+ font-size: 0.7rem;
18326
+ text-transform: uppercase;
18327
+ letter-spacing: 0.08em;
18328
+ color: var(--text-dim);
18329
+ font-weight: 600;
18330
+ margin-bottom: 0.5rem;
18331
+ }
18332
+
17857
18333
  /* Cards grid */
17858
18334
  .cards {
17859
18335
  display: grid;
17860
18336
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
17861
18337
  gap: 1rem;
17862
- margin-bottom: 2rem;
18338
+ margin-bottom: 0.5rem;
17863
18339
  }
17864
18340
 
17865
18341
  .card {
@@ -18140,6 +18616,14 @@ tr:hover td {
18140
18616
  font-family: var(--mono);
18141
18617
  font-size: 0.85em;
18142
18618
  }
18619
+ .detail-content hr {
18620
+ border: none;
18621
+ border-top: 1px solid var(--border);
18622
+ margin: 1.25rem 0;
18623
+ }
18624
+ .detail-content .table-wrap {
18625
+ margin: 0.75rem 0;
18626
+ }
18143
18627
 
18144
18628
  /* Filters */
18145
18629
  .filters {
@@ -18184,21 +18668,206 @@ tr:hover td {
18184
18668
  .priority-high { color: var(--red); }
18185
18669
  .priority-medium { color: var(--amber); }
18186
18670
  .priority-low { color: var(--green); }
18671
+
18672
+ /* Health */
18673
+ .health-section-title {
18674
+ font-size: 1.1rem;
18675
+ font-weight: 600;
18676
+ margin: 2rem 0 1rem;
18677
+ color: var(--text);
18678
+ }
18679
+
18680
+ /* Mermaid diagrams */
18681
+ .mermaid-container {
18682
+ background: var(--bg-card);
18683
+ border: 1px solid var(--border);
18684
+ border-radius: var(--radius);
18685
+ padding: 1.5rem;
18686
+ margin: 1rem 0;
18687
+ overflow-x: auto;
18688
+ }
18689
+
18690
+ .mermaid-container .mermaid {
18691
+ display: flex;
18692
+ justify-content: center;
18693
+ }
18694
+
18695
+ .mermaid-empty {
18696
+ text-align: center;
18697
+ color: var(--text-dim);
18698
+ font-size: 0.875rem;
18699
+ }
18700
+
18701
+ .mermaid-row {
18702
+ display: grid;
18703
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
18704
+ gap: 1rem;
18705
+ }
18706
+
18707
+ .mermaid-row .mermaid-container {
18708
+ margin: 0;
18709
+ }
18187
18710
  `;
18188
18711
  }
18189
18712
 
18713
+ // src/web/templates/mermaid.ts
18714
+ function sanitize(text, maxLen = 40) {
18715
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
18716
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
18717
+ }
18718
+ function mermaidBlock(definition) {
18719
+ return `<div class="mermaid-container"><pre class="mermaid">
18720
+ ${definition}
18721
+ </pre></div>`;
18722
+ }
18723
+ function placeholder(message) {
18724
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
18725
+ }
18726
+ function buildTimelineGantt(data) {
18727
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
18728
+ if (sprintsWithDates.length === 0) {
18729
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
18730
+ }
18731
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
18732
+ const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
18733
+ for (const sprint of sprintsWithDates) {
18734
+ lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
18735
+ const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
18736
+ if (linked.length === 0) {
18737
+ lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
18738
+ } else {
18739
+ for (const epic of linked) {
18740
+ const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
18741
+ lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
18742
+ }
18743
+ }
18744
+ }
18745
+ return mermaidBlock(lines.join("\n"));
18746
+ }
18747
+ function buildArtifactFlowchart(data) {
18748
+ if (data.features.length === 0 && data.epics.length === 0) {
18749
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
18750
+ }
18751
+ const lines = ["graph TD"];
18752
+ lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
18753
+ lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
18754
+ lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
18755
+ lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
18756
+ const nodeIds = /* @__PURE__ */ new Set();
18757
+ for (const epic of data.epics) {
18758
+ if (epic.linkedFeature) {
18759
+ const feature = data.features.find((f) => f.id === epic.linkedFeature);
18760
+ if (feature) {
18761
+ const fNode = feature.id.replace(/-/g, "_");
18762
+ const eNode = epic.id.replace(/-/g, "_");
18763
+ if (!nodeIds.has(fNode)) {
18764
+ lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
18765
+ nodeIds.add(fNode);
18766
+ }
18767
+ if (!nodeIds.has(eNode)) {
18768
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18769
+ nodeIds.add(eNode);
18770
+ }
18771
+ lines.push(` ${fNode} --> ${eNode}`);
18772
+ }
18773
+ }
18774
+ }
18775
+ for (const sprint of data.sprints) {
18776
+ const sNode = sprint.id.replace(/-/g, "_");
18777
+ for (const epicId of sprint.linkedEpics) {
18778
+ const epic = data.epics.find((e) => e.id === epicId);
18779
+ if (epic) {
18780
+ const eNode = epic.id.replace(/-/g, "_");
18781
+ if (!nodeIds.has(eNode)) {
18782
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18783
+ nodeIds.add(eNode);
18784
+ }
18785
+ if (!nodeIds.has(sNode)) {
18786
+ lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
18787
+ nodeIds.add(sNode);
18788
+ }
18789
+ lines.push(` ${eNode} --> ${sNode}`);
18790
+ }
18791
+ }
18792
+ }
18793
+ if (nodeIds.size === 0) {
18794
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
18795
+ }
18796
+ const allItems = [
18797
+ ...data.features.map((f) => ({ id: f.id, status: f.status })),
18798
+ ...data.epics.map((e) => ({ id: e.id, status: e.status })),
18799
+ ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
18800
+ ];
18801
+ for (const item of allItems) {
18802
+ const node = item.id.replace(/-/g, "_");
18803
+ if (!nodeIds.has(node)) continue;
18804
+ const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
18805
+ if (cls) {
18806
+ lines.push(` class ${node} ${cls}`);
18807
+ }
18808
+ }
18809
+ return mermaidBlock(lines.join("\n"));
18810
+ }
18811
+ function buildStatusPie(title, counts) {
18812
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
18813
+ if (entries.length === 0) {
18814
+ return placeholder(`No data for ${title}.`);
18815
+ }
18816
+ const lines = [`pie title ${sanitize(title, 60)}`];
18817
+ for (const [label, count] of entries) {
18818
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
18819
+ }
18820
+ return mermaidBlock(lines.join("\n"));
18821
+ }
18822
+ function buildHealthGauge(categories) {
18823
+ const valid = categories.filter((c) => c.total > 0);
18824
+ if (valid.length === 0) {
18825
+ return placeholder("No completeness data available.");
18826
+ }
18827
+ const pies = valid.map((cat) => {
18828
+ const incomplete = cat.total - cat.complete;
18829
+ const lines = [
18830
+ `pie title ${sanitize(cat.name, 30)}`,
18831
+ ` "Complete" : ${cat.complete}`,
18832
+ ` "Incomplete" : ${incomplete}`
18833
+ ];
18834
+ return mermaidBlock(lines.join("\n"));
18835
+ });
18836
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
18837
+ }
18838
+
18190
18839
  // src/web/templates/pages/overview.ts
18191
- function overviewPage(data) {
18192
- const cards = data.types.map(
18193
- (t) => `
18840
+ function renderCard(t) {
18841
+ return `
18194
18842
  <div class="card">
18195
18843
  <a href="/docs/${t.type}">
18196
18844
  <div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
18197
18845
  <div class="card-value">${t.total}</div>
18198
18846
  ${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
18199
18847
  </a>
18200
- </div>`
18201
- ).join("\n");
18848
+ </div>`;
18849
+ }
18850
+ function overviewPage(data, diagrams, navGroups) {
18851
+ const typeMap = new Map(data.types.map((t) => [t.type, t]));
18852
+ const placed = /* @__PURE__ */ new Set();
18853
+ const groupSections = navGroups.map((group) => {
18854
+ const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
18855
+ placed.add(type);
18856
+ return renderCard(typeMap.get(type));
18857
+ });
18858
+ if (groupCards.length === 0) return "";
18859
+ return `
18860
+ <div class="card-group">
18861
+ <div class="card-group-label">${escapeHtml(group.label)}</div>
18862
+ <div class="cards">${groupCards.join("\n")}</div>
18863
+ </div>`;
18864
+ }).filter(Boolean).join("\n");
18865
+ const ungrouped = data.types.filter((t) => !placed.has(t.type));
18866
+ const ungroupedSection = ungrouped.length > 0 ? `
18867
+ <div class="card-group">
18868
+ <div class="card-group-label">Other</div>
18869
+ <div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
18870
+ </div>` : "";
18202
18871
  const rows = data.recent.map(
18203
18872
  (doc) => `
18204
18873
  <tr>
@@ -18214,9 +18883,14 @@ function overviewPage(data) {
18214
18883
  <h2>Project Overview</h2>
18215
18884
  </div>
18216
18885
 
18217
- <div class="cards">
18218
- ${cards}
18219
- </div>
18886
+ ${groupSections}
18887
+ ${ungroupedSection}
18888
+
18889
+ <div class="section-title">Project Timeline</div>
18890
+ ${buildTimelineGantt(diagrams)}
18891
+
18892
+ <div class="section-title">Artifact Relationships</div>
18893
+ ${buildArtifactFlowchart(diagrams)}
18220
18894
 
18221
18895
  <div class="section-title">Recent Activity</div>
18222
18896
  ${data.recent.length > 0 ? `
@@ -18383,6 +19057,76 @@ function garPage(report) {
18383
19057
  <div class="gar-areas">
18384
19058
  ${areaCards}
18385
19059
  </div>
19060
+
19061
+ <div class="section-title">Status Distribution</div>
19062
+ ${buildStatusPie("Action Status", {
19063
+ Open: report.metrics.scope.open,
19064
+ Done: report.metrics.scope.done,
19065
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
19066
+ })}
19067
+ `;
19068
+ }
19069
+
19070
+ // src/web/templates/pages/health.ts
19071
+ function healthPage(report, metrics) {
19072
+ const dotClass = `dot-${report.overall}`;
19073
+ function renderSection(title, categories) {
19074
+ const cards = categories.map(
19075
+ (cat) => `
19076
+ <div class="gar-area">
19077
+ <div class="area-header">
19078
+ <div class="area-dot dot-${cat.status}"></div>
19079
+ <div class="area-name">${escapeHtml(cat.name)}</div>
19080
+ </div>
19081
+ <div class="area-summary">${escapeHtml(cat.summary)}</div>
19082
+ ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
19083
+ </div>`
19084
+ ).join("\n");
19085
+ return `
19086
+ <div class="health-section-title">${escapeHtml(title)}</div>
19087
+ <div class="gar-areas">${cards}</div>
19088
+ `;
19089
+ }
19090
+ return `
19091
+ <div class="page-header">
19092
+ <h2>Governance Health Check</h2>
19093
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
19094
+ </div>
19095
+
19096
+ <div class="gar-overall">
19097
+ <div class="dot ${dotClass}"></div>
19098
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
19099
+ </div>
19100
+
19101
+ ${renderSection("Completeness", report.completeness)}
19102
+
19103
+ <div class="health-section-title">Completeness Overview</div>
19104
+ ${buildHealthGauge(
19105
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
19106
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
19107
+ complete: cat.complete,
19108
+ total: cat.total
19109
+ })) : report.completeness.map((c) => {
19110
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
19111
+ return {
19112
+ name: c.name,
19113
+ complete: match ? parseInt(match[1], 10) : 0,
19114
+ total: match ? parseInt(match[2], 10) : 0
19115
+ };
19116
+ })
19117
+ )}
19118
+
19119
+ ${renderSection("Process", report.process)}
19120
+
19121
+ <div class="health-section-title">Process Summary</div>
19122
+ ${metrics ? buildStatusPie("Process Health", {
19123
+ Stale: metrics.process.stale.length,
19124
+ "Aging Actions": metrics.process.agingActions.length,
19125
+ Healthy: Math.max(
19126
+ 0,
19127
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
19128
+ )
19129
+ }) : ""}
18386
19130
  `;
18387
19131
  }
18388
19132
 
@@ -18449,7 +19193,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
18449
19193
  }
18450
19194
  if (pathname === "/") {
18451
19195
  const data = getOverviewData(store);
18452
- const body = overviewPage(data);
19196
+ const diagrams = getDiagramData(store);
19197
+ const body = overviewPage(data, diagrams, navGroups);
18453
19198
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
18454
19199
  return;
18455
19200
  }
@@ -18459,6 +19204,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
18459
19204
  respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
18460
19205
  return;
18461
19206
  }
19207
+ if (pathname === "/health") {
19208
+ const healthMetrics = collectHealthMetrics(store);
19209
+ const report = evaluateHealth(projectName, healthMetrics);
19210
+ const body = healthPage(report, healthMetrics);
19211
+ respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
19212
+ return;
19213
+ }
18462
19214
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
18463
19215
  if (boardMatch) {
18464
19216
  const type = boardMatch[1];
@@ -18698,7 +19450,7 @@ function createJiraTools(store) {
18698
19450
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
18699
19451
  };
18700
19452
  },
18701
- { annotations: { readOnly: true } }
19453
+ { annotations: { readOnlyHint: true } }
18702
19454
  ),
18703
19455
  tool19(
18704
19456
  "get_jira_issue",
@@ -18730,7 +19482,7 @@ function createJiraTools(store) {
18730
19482
  ]
18731
19483
  };
18732
19484
  },
18733
- { annotations: { readOnly: true } }
19485
+ { annotations: { readOnlyHint: true } }
18734
19486
  ),
18735
19487
  // --- Jira → Local tools ---
18736
19488
  tool19(
@@ -19461,7 +20213,7 @@ function createWebTools(store, projectName, navGroups) {
19461
20213
  content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
19462
20214
  };
19463
20215
  },
19464
- { annotations: { readOnly: true } }
20216
+ { annotations: { readOnlyHint: true } }
19465
20217
  ),
19466
20218
  tool20(
19467
20219
  "get_dashboard_overview",
@@ -19483,7 +20235,7 @@ function createWebTools(store, projectName, navGroups) {
19483
20235
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19484
20236
  };
19485
20237
  },
19486
- { annotations: { readOnly: true } }
20238
+ { annotations: { readOnlyHint: true } }
19487
20239
  ),
19488
20240
  tool20(
19489
20241
  "get_dashboard_gar",
@@ -19495,7 +20247,7 @@ function createWebTools(store, projectName, navGroups) {
19495
20247
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
19496
20248
  };
19497
20249
  },
19498
- { annotations: { readOnly: true } }
20250
+ { annotations: { readOnlyHint: true } }
19499
20251
  ),
19500
20252
  tool20(
19501
20253
  "get_dashboard_board",
@@ -19523,7 +20275,7 @@ function createWebTools(store, projectName, navGroups) {
19523
20275
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19524
20276
  };
19525
20277
  },
19526
- { annotations: { readOnly: true } }
20278
+ { annotations: { readOnlyHint: true } }
19527
20279
  )
19528
20280
  ];
19529
20281
  }
@@ -21074,7 +21826,7 @@ ${summaries}`
21074
21826
  content: [{ type: "text", text: guidance }]
21075
21827
  };
21076
21828
  },
21077
- { annotations: { readOnly: true } }
21829
+ { annotations: { readOnlyHint: true } }
21078
21830
  )
21079
21831
  ];
21080
21832
  }
@@ -22629,6 +23381,105 @@ function renderConfluence(report) {
22629
23381
  return lines.join("\n");
22630
23382
  }
22631
23383
 
23384
+ // src/reports/health/render-ascii.ts
23385
+ import chalk17 from "chalk";
23386
+ var STATUS_DOT2 = {
23387
+ green: chalk17.green("\u25CF"),
23388
+ amber: chalk17.yellow("\u25CF"),
23389
+ red: chalk17.red("\u25CF")
23390
+ };
23391
+ var STATUS_LABEL2 = {
23392
+ green: chalk17.green.bold("GREEN"),
23393
+ amber: chalk17.yellow.bold("AMBER"),
23394
+ red: chalk17.red.bold("RED")
23395
+ };
23396
+ var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
23397
+ function renderAscii2(report) {
23398
+ const lines = [];
23399
+ lines.push("");
23400
+ lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
23401
+ lines.push(chalk17.dim(` ${report.generatedAt}`));
23402
+ lines.push("");
23403
+ lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
23404
+ lines.push("");
23405
+ lines.push(` ${SEPARATOR2}`);
23406
+ lines.push(chalk17.bold(" Completeness"));
23407
+ lines.push(` ${SEPARATOR2}`);
23408
+ for (const cat of report.completeness) {
23409
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
23410
+ for (const item of cat.items) {
23411
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23412
+ }
23413
+ }
23414
+ lines.push("");
23415
+ lines.push(` ${SEPARATOR2}`);
23416
+ lines.push(chalk17.bold(" Process"));
23417
+ lines.push(` ${SEPARATOR2}`);
23418
+ for (const cat of report.process) {
23419
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
23420
+ for (const item of cat.items) {
23421
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23422
+ }
23423
+ }
23424
+ lines.push(` ${SEPARATOR2}`);
23425
+ lines.push("");
23426
+ return lines.join("\n");
23427
+ }
23428
+
23429
+ // src/reports/health/render-confluence.ts
23430
+ var EMOJI2 = {
23431
+ green: ":green_circle:",
23432
+ amber: ":yellow_circle:",
23433
+ red: ":red_circle:"
23434
+ };
23435
+ function renderConfluence2(report) {
23436
+ const lines = [];
23437
+ lines.push(`# Health Check \u2014 ${report.projectName}`);
23438
+ lines.push("");
23439
+ lines.push(`**Date:** ${report.generatedAt}`);
23440
+ lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
23441
+ lines.push("");
23442
+ lines.push("## Completeness");
23443
+ lines.push("");
23444
+ lines.push("| Category | Status | Summary |");
23445
+ lines.push("|----------|--------|---------|");
23446
+ for (const cat of report.completeness) {
23447
+ lines.push(
23448
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23449
+ );
23450
+ }
23451
+ lines.push("");
23452
+ for (const cat of report.completeness) {
23453
+ if (cat.items.length === 0) continue;
23454
+ lines.push(`### ${cat.name}`);
23455
+ lines.push("");
23456
+ for (const item of cat.items) {
23457
+ lines.push(`- **${item.id}** ${item.detail}`);
23458
+ }
23459
+ lines.push("");
23460
+ }
23461
+ lines.push("## Process");
23462
+ lines.push("");
23463
+ lines.push("| Metric | Status | Summary |");
23464
+ lines.push("|--------|--------|---------|");
23465
+ for (const cat of report.process) {
23466
+ lines.push(
23467
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23468
+ );
23469
+ }
23470
+ lines.push("");
23471
+ for (const cat of report.process) {
23472
+ if (cat.items.length === 0) continue;
23473
+ lines.push(`### ${cat.name}`);
23474
+ lines.push("");
23475
+ for (const item of cat.items) {
23476
+ lines.push(`- **${item.id}** ${item.detail}`);
23477
+ }
23478
+ lines.push("");
23479
+ }
23480
+ return lines.join("\n");
23481
+ }
23482
+
22632
23483
  // src/cli/commands/report.ts
22633
23484
  async function garReportCommand(options) {
22634
23485
  const project = loadProject();
@@ -22644,6 +23495,20 @@ async function garReportCommand(options) {
22644
23495
  console.log(renderAscii(report));
22645
23496
  }
22646
23497
  }
23498
+ async function healthReportCommand(options) {
23499
+ const project = loadProject();
23500
+ const plugin = resolvePlugin(project.config.methodology);
23501
+ const registrations = plugin?.documentTypeRegistrations ?? [];
23502
+ const store = new DocumentStore(project.marvinDir, registrations);
23503
+ const metrics = collectHealthMetrics(store);
23504
+ const report = evaluateHealth(project.config.name, metrics);
23505
+ const format = options.format ?? "ascii";
23506
+ if (format === "confluence") {
23507
+ console.log(renderConfluence2(report));
23508
+ } else {
23509
+ console.log(renderAscii2(report));
23510
+ }
23511
+ }
22647
23512
 
22648
23513
  // src/cli/commands/web.ts
22649
23514
  async function webCommand(options) {
@@ -22660,7 +23525,7 @@ function createProgram() {
22660
23525
  const program2 = new Command();
22661
23526
  program2.name("marvin").description(
22662
23527
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
22663
- ).version("0.3.3");
23528
+ ).version("0.3.5");
22664
23529
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
22665
23530
  await initCommand();
22666
23531
  });
@@ -22737,6 +23602,12 @@ function createProgram() {
22737
23602
  ).action(async (options) => {
22738
23603
  await garReportCommand(options);
22739
23604
  });
23605
+ reportCmd.command("health").description("Generate a governance health check report").option(
23606
+ "--format <format>",
23607
+ "Output format: ascii or confluence (default: ascii)"
23608
+ ).action(async (options) => {
23609
+ await healthReportCommand(options);
23610
+ });
22740
23611
  program2.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) => {
22741
23612
  await webCommand(options);
22742
23613
  });