mrvn-cli 0.3.3 → 0.3.6

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.
@@ -14227,7 +14227,7 @@ function createDecisionTools(store) {
14227
14227
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14228
14228
  };
14229
14229
  },
14230
- { annotations: { readOnly: true } }
14230
+ { annotations: { readOnlyHint: true } }
14231
14231
  ),
14232
14232
  tool(
14233
14233
  "get_decision",
@@ -14254,7 +14254,7 @@ function createDecisionTools(store) {
14254
14254
  ]
14255
14255
  };
14256
14256
  },
14257
- { annotations: { readOnly: true } }
14257
+ { annotations: { readOnlyHint: true } }
14258
14258
  ),
14259
14259
  tool(
14260
14260
  "create_decision",
@@ -14295,7 +14295,8 @@ function createDecisionTools(store) {
14295
14295
  title: external_exports.string().optional().describe("New title"),
14296
14296
  status: external_exports.string().optional().describe("New status"),
14297
14297
  content: external_exports.string().optional().describe("New content"),
14298
- owner: external_exports.string().optional().describe("New owner")
14298
+ owner: external_exports.string().optional().describe("New owner"),
14299
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14299
14300
  },
14300
14301
  async (args) => {
14301
14302
  const { id, content, ...updates } = args;
@@ -14315,6 +14316,19 @@ function createDecisionTools(store) {
14315
14316
 
14316
14317
  // src/agent/tools/actions.ts
14317
14318
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14319
+ function findMatchingSprints(store, dueDate) {
14320
+ const sprints = store.list({ type: "sprint" });
14321
+ return sprints.filter((s) => {
14322
+ const start = s.frontmatter.startDate;
14323
+ const end = s.frontmatter.endDate;
14324
+ return start && end && dueDate >= start && dueDate <= end;
14325
+ }).map((s) => ({
14326
+ id: s.frontmatter.id,
14327
+ title: s.frontmatter.title,
14328
+ startDate: s.frontmatter.startDate,
14329
+ endDate: s.frontmatter.endDate
14330
+ }));
14331
+ }
14318
14332
  function createActionTools(store) {
14319
14333
  return [
14320
14334
  tool2(
@@ -14330,19 +14344,24 @@ function createActionTools(store) {
14330
14344
  status: args.status,
14331
14345
  owner: args.owner
14332
14346
  });
14333
- const summary = docs.map((d) => ({
14334
- id: d.frontmatter.id,
14335
- title: d.frontmatter.title,
14336
- status: d.frontmatter.status,
14337
- owner: d.frontmatter.owner,
14338
- priority: d.frontmatter.priority,
14339
- created: d.frontmatter.created
14340
- }));
14347
+ const summary = docs.map((d) => {
14348
+ const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
14349
+ return {
14350
+ id: d.frontmatter.id,
14351
+ title: d.frontmatter.title,
14352
+ status: d.frontmatter.status,
14353
+ owner: d.frontmatter.owner,
14354
+ priority: d.frontmatter.priority,
14355
+ dueDate: d.frontmatter.dueDate,
14356
+ sprints: sprintIds.length > 0 ? sprintIds : void 0,
14357
+ created: d.frontmatter.created
14358
+ };
14359
+ });
14341
14360
  return {
14342
14361
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14343
14362
  };
14344
14363
  },
14345
- { annotations: { readOnly: true } }
14364
+ { annotations: { readOnlyHint: true } }
14346
14365
  ),
14347
14366
  tool2(
14348
14367
  "get_action",
@@ -14369,7 +14388,7 @@ function createActionTools(store) {
14369
14388
  ]
14370
14389
  };
14371
14390
  },
14372
- { annotations: { readOnly: true } }
14391
+ { annotations: { readOnlyHint: true } }
14373
14392
  ),
14374
14393
  tool2(
14375
14394
  "create_action",
@@ -14380,9 +14399,18 @@ function createActionTools(store) {
14380
14399
  status: external_exports.string().optional().describe("Status (default: 'open')"),
14381
14400
  owner: external_exports.string().optional().describe("Person responsible"),
14382
14401
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
14383
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
14402
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
14403
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14404
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
14384
14405
  },
14385
14406
  async (args) => {
14407
+ const tags = [...args.tags ?? []];
14408
+ if (args.sprints) {
14409
+ for (const sprintId of args.sprints) {
14410
+ const tag = `sprint:${sprintId}`;
14411
+ if (!tags.includes(tag)) tags.push(tag);
14412
+ }
14413
+ }
14386
14414
  const doc = store.create(
14387
14415
  "action",
14388
14416
  {
@@ -14390,17 +14418,21 @@ function createActionTools(store) {
14390
14418
  status: args.status,
14391
14419
  owner: args.owner,
14392
14420
  priority: args.priority,
14393
- tags: args.tags
14421
+ tags: tags.length > 0 ? tags : void 0,
14422
+ dueDate: args.dueDate
14394
14423
  },
14395
14424
  args.content
14396
14425
  );
14426
+ const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
14427
+ if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
14428
+ const matching = findMatchingSprints(store, args.dueDate);
14429
+ if (matching.length > 0) {
14430
+ const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
14431
+ parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
14432
+ }
14433
+ }
14397
14434
  return {
14398
- content: [
14399
- {
14400
- type: "text",
14401
- text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
14402
- }
14403
- ]
14435
+ content: [{ type: "text", text: parts.join("\n") }]
14404
14436
  };
14405
14437
  }
14406
14438
  ),
@@ -14413,10 +14445,35 @@ function createActionTools(store) {
14413
14445
  status: external_exports.string().optional().describe("New status"),
14414
14446
  content: external_exports.string().optional().describe("New content"),
14415
14447
  owner: external_exports.string().optional().describe("New owner"),
14416
- priority: external_exports.string().optional().describe("New priority")
14448
+ priority: external_exports.string().optional().describe("New priority"),
14449
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14450
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
14451
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
14417
14452
  },
14418
14453
  async (args) => {
14419
- const { id, content, ...updates } = args;
14454
+ const { id, content, sprints, tags, ...updates } = args;
14455
+ if (tags !== void 0) {
14456
+ const merged = [...tags];
14457
+ if (sprints) {
14458
+ for (const s of sprints) {
14459
+ const tag = `sprint:${s}`;
14460
+ if (!merged.includes(tag)) merged.push(tag);
14461
+ }
14462
+ }
14463
+ updates.tags = merged;
14464
+ } else if (sprints !== void 0) {
14465
+ const existing = store.get(id);
14466
+ if (!existing) {
14467
+ return {
14468
+ content: [{ type: "text", text: `Action ${id} not found` }],
14469
+ isError: true
14470
+ };
14471
+ }
14472
+ const existingTags = existing.frontmatter.tags ?? [];
14473
+ const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
14474
+ const newSprintTags = sprints.map((s) => `sprint:${s}`);
14475
+ updates.tags = [...nonSprintTags, ...newSprintTags];
14476
+ }
14420
14477
  const doc = store.update(id, updates, content);
14421
14478
  return {
14422
14479
  content: [
@@ -14427,6 +14484,35 @@ function createActionTools(store) {
14427
14484
  ]
14428
14485
  };
14429
14486
  }
14487
+ ),
14488
+ tool2(
14489
+ "suggest_sprints_for_action",
14490
+ "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
14491
+ {
14492
+ dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
14493
+ },
14494
+ async (args) => {
14495
+ const matching = findMatchingSprints(store, args.dueDate);
14496
+ if (matching.length === 0) {
14497
+ return {
14498
+ content: [
14499
+ {
14500
+ type: "text",
14501
+ text: `No sprints found containing dueDate ${args.dueDate}.`
14502
+ }
14503
+ ]
14504
+ };
14505
+ }
14506
+ return {
14507
+ content: [
14508
+ {
14509
+ type: "text",
14510
+ text: JSON.stringify(matching, null, 2)
14511
+ }
14512
+ ]
14513
+ };
14514
+ },
14515
+ { annotations: { readOnlyHint: true } }
14430
14516
  )
14431
14517
  ];
14432
14518
  }
@@ -14454,7 +14540,7 @@ function createQuestionTools(store) {
14454
14540
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14455
14541
  };
14456
14542
  },
14457
- { annotations: { readOnly: true } }
14543
+ { annotations: { readOnlyHint: true } }
14458
14544
  ),
14459
14545
  tool3(
14460
14546
  "get_question",
@@ -14481,7 +14567,7 @@ function createQuestionTools(store) {
14481
14567
  ]
14482
14568
  };
14483
14569
  },
14484
- { annotations: { readOnly: true } }
14570
+ { annotations: { readOnlyHint: true } }
14485
14571
  ),
14486
14572
  tool3(
14487
14573
  "create_question",
@@ -14522,7 +14608,8 @@ function createQuestionTools(store) {
14522
14608
  title: external_exports.string().optional().describe("New title"),
14523
14609
  status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
14524
14610
  content: external_exports.string().optional().describe("Updated content / answer"),
14525
- owner: external_exports.string().optional().describe("New owner")
14611
+ owner: external_exports.string().optional().describe("New owner"),
14612
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14526
14613
  },
14527
14614
  async (args) => {
14528
14615
  const { id, content, ...updates } = args;
@@ -14574,7 +14661,7 @@ function createDocumentTools(store) {
14574
14661
  ]
14575
14662
  };
14576
14663
  },
14577
- { annotations: { readOnly: true } }
14664
+ { annotations: { readOnlyHint: true } }
14578
14665
  ),
14579
14666
  tool4(
14580
14667
  "read_document",
@@ -14601,7 +14688,7 @@ function createDocumentTools(store) {
14601
14688
  ]
14602
14689
  };
14603
14690
  },
14604
- { annotations: { readOnly: true } }
14691
+ { annotations: { readOnlyHint: true } }
14605
14692
  ),
14606
14693
  tool4(
14607
14694
  "project_summary",
@@ -14629,7 +14716,7 @@ function createDocumentTools(store) {
14629
14716
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14630
14717
  };
14631
14718
  },
14632
- { annotations: { readOnly: true } }
14719
+ { annotations: { readOnlyHint: true } }
14633
14720
  )
14634
14721
  ];
14635
14722
  }
@@ -14666,7 +14753,7 @@ function createSourceTools(manifest) {
14666
14753
  ]
14667
14754
  };
14668
14755
  },
14669
- { annotations: { readOnly: true } }
14756
+ { annotations: { readOnlyHint: true } }
14670
14757
  ),
14671
14758
  tool5(
14672
14759
  "get_source_info",
@@ -14700,7 +14787,7 @@ function createSourceTools(manifest) {
14700
14787
  ]
14701
14788
  };
14702
14789
  },
14703
- { annotations: { readOnly: true } }
14790
+ { annotations: { readOnlyHint: true } }
14704
14791
  )
14705
14792
  ];
14706
14793
  }
@@ -14731,7 +14818,7 @@ function createSessionTools(store) {
14731
14818
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14732
14819
  };
14733
14820
  },
14734
- { annotations: { readOnly: true } }
14821
+ { annotations: { readOnlyHint: true } }
14735
14822
  ),
14736
14823
  tool6(
14737
14824
  "get_session",
@@ -14749,7 +14836,7 @@ function createSessionTools(store) {
14749
14836
  content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
14750
14837
  };
14751
14838
  },
14752
- { annotations: { readOnly: true } }
14839
+ { annotations: { readOnlyHint: true } }
14753
14840
  )
14754
14841
  ];
14755
14842
  }
@@ -14842,7 +14929,7 @@ function createMeetingTools(store) {
14842
14929
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14843
14930
  };
14844
14931
  },
14845
- { annotations: { readOnly: true } }
14932
+ { annotations: { readOnlyHint: true } }
14846
14933
  ),
14847
14934
  tool7(
14848
14935
  "get_meeting",
@@ -14869,7 +14956,7 @@ function createMeetingTools(store) {
14869
14956
  ]
14870
14957
  };
14871
14958
  },
14872
- { annotations: { readOnly: true } }
14959
+ { annotations: { readOnlyHint: true } }
14873
14960
  ),
14874
14961
  tool7(
14875
14962
  "create_meeting",
@@ -14994,7 +15081,7 @@ function createMeetingTools(store) {
14994
15081
  content: [{ type: "text", text: sections.join("\n") }]
14995
15082
  };
14996
15083
  },
14997
- { annotations: { readOnly: true } }
15084
+ { annotations: { readOnlyHint: true } }
14998
15085
  )
14999
15086
  ];
15000
15087
  }
@@ -15011,12 +15098,20 @@ function collectGarMetrics(store) {
15011
15098
  const blockedItems = allDocs.filter(
15012
15099
  (d) => d.frontmatter.tags?.includes("blocked")
15013
15100
  );
15014
- const overdueItems = allDocs.filter(
15101
+ const tagOverdueItems = allDocs.filter(
15015
15102
  (d) => d.frontmatter.tags?.includes("overdue")
15016
15103
  );
15104
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15105
+ const dateOverdueActions = openActions.filter((d) => {
15106
+ const dueDate = d.frontmatter.dueDate;
15107
+ return typeof dueDate === "string" && dueDate < today;
15108
+ });
15109
+ const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
15110
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15111
+ );
15017
15112
  const openQuestions = store.list({ type: "question", status: "open" });
15018
15113
  const riskItems = allDocs.filter(
15019
- (d) => d.frontmatter.tags?.includes("risk")
15114
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
15020
15115
  );
15021
15116
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
15022
15117
  const total = allActions.length;
@@ -15122,6 +15217,253 @@ function evaluateGar(projectName, metrics) {
15122
15217
  };
15123
15218
  }
15124
15219
 
15220
+ // src/reports/health/collector.ts
15221
+ var FIELD_CHECKS = [
15222
+ {
15223
+ type: "action",
15224
+ openStatuses: ["open", "in-progress"],
15225
+ requiredFields: ["owner", "priority", "dueDate", "content"]
15226
+ },
15227
+ {
15228
+ type: "decision",
15229
+ openStatuses: ["open", "proposed"],
15230
+ requiredFields: ["owner", "content"]
15231
+ },
15232
+ {
15233
+ type: "question",
15234
+ openStatuses: ["open"],
15235
+ requiredFields: ["owner", "content"]
15236
+ },
15237
+ {
15238
+ type: "feature",
15239
+ openStatuses: ["draft", "approved"],
15240
+ requiredFields: ["owner", "priority", "content"]
15241
+ },
15242
+ {
15243
+ type: "epic",
15244
+ openStatuses: ["planned", "in-progress"],
15245
+ requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
15246
+ },
15247
+ {
15248
+ type: "sprint",
15249
+ openStatuses: ["planned", "active"],
15250
+ requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
15251
+ }
15252
+ ];
15253
+ var STALE_THRESHOLD_DAYS = 14;
15254
+ var AGING_THRESHOLD_DAYS = 30;
15255
+ function daysBetween(a, b) {
15256
+ const msPerDay = 864e5;
15257
+ const dateA = new Date(a);
15258
+ const dateB = new Date(b);
15259
+ return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
15260
+ }
15261
+ function checkMissingFields(doc, requiredFields) {
15262
+ const missing = [];
15263
+ for (const field of requiredFields) {
15264
+ if (field === "content") {
15265
+ if (!doc.content || doc.content.trim().length === 0) {
15266
+ missing.push("content");
15267
+ }
15268
+ } else if (field === "linkedEpics") {
15269
+ const val = doc.frontmatter[field];
15270
+ if (!Array.isArray(val) || val.length === 0) {
15271
+ missing.push(field);
15272
+ }
15273
+ } else {
15274
+ const val = doc.frontmatter[field];
15275
+ if (val === void 0 || val === null || val === "") {
15276
+ missing.push(field);
15277
+ }
15278
+ }
15279
+ }
15280
+ return missing;
15281
+ }
15282
+ function collectCompleteness(store) {
15283
+ const result = {};
15284
+ for (const check2 of FIELD_CHECKS) {
15285
+ const allOfType = store.list({ type: check2.type });
15286
+ const openDocs = allOfType.filter(
15287
+ (d) => check2.openStatuses.includes(d.frontmatter.status)
15288
+ );
15289
+ const gaps = [];
15290
+ let complete = 0;
15291
+ for (const doc of openDocs) {
15292
+ const missingFields = checkMissingFields(doc, check2.requiredFields);
15293
+ if (missingFields.length === 0) {
15294
+ complete++;
15295
+ } else {
15296
+ gaps.push({
15297
+ id: doc.frontmatter.id,
15298
+ title: doc.frontmatter.title,
15299
+ missingFields
15300
+ });
15301
+ }
15302
+ }
15303
+ result[check2.type] = {
15304
+ total: openDocs.length,
15305
+ complete,
15306
+ gaps
15307
+ };
15308
+ }
15309
+ return result;
15310
+ }
15311
+ function collectProcess(store) {
15312
+ const today = (/* @__PURE__ */ new Date()).toISOString();
15313
+ const allDocs = store.list();
15314
+ const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
15315
+ const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
15316
+ const stale = [];
15317
+ for (const doc of openDocs) {
15318
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
15319
+ const days = daysBetween(updated, today);
15320
+ if (days >= STALE_THRESHOLD_DAYS) {
15321
+ stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15322
+ }
15323
+ }
15324
+ const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
15325
+ const agingActions = [];
15326
+ for (const doc of openActions) {
15327
+ const days = daysBetween(doc.frontmatter.created, today);
15328
+ if (days >= AGING_THRESHOLD_DAYS) {
15329
+ agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15330
+ }
15331
+ }
15332
+ const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
15333
+ let decisionTotal = 0;
15334
+ for (const doc of resolvedDecisions) {
15335
+ decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15336
+ }
15337
+ const decisionVelocity = {
15338
+ avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
15339
+ count: resolvedDecisions.length
15340
+ };
15341
+ const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
15342
+ let questionTotal = 0;
15343
+ for (const doc of answeredQuestions) {
15344
+ questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15345
+ }
15346
+ const questionResolution = {
15347
+ avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
15348
+ count: answeredQuestions.length
15349
+ };
15350
+ return { stale, agingActions, decisionVelocity, questionResolution };
15351
+ }
15352
+ function collectHealthMetrics(store) {
15353
+ return {
15354
+ completeness: collectCompleteness(store),
15355
+ process: collectProcess(store)
15356
+ };
15357
+ }
15358
+
15359
+ // src/reports/health/evaluator.ts
15360
+ function worstStatus2(statuses) {
15361
+ if (statuses.includes("red")) return "red";
15362
+ if (statuses.includes("amber")) return "amber";
15363
+ return "green";
15364
+ }
15365
+ function completenessStatus(total, complete) {
15366
+ if (total === 0) return "green";
15367
+ const pct = Math.round(complete / total * 100);
15368
+ if (pct >= 100) return "green";
15369
+ if (pct >= 75) return "amber";
15370
+ return "red";
15371
+ }
15372
+ var TYPE_LABELS = {
15373
+ action: "Actions",
15374
+ decision: "Decisions",
15375
+ question: "Questions",
15376
+ feature: "Features",
15377
+ epic: "Epics",
15378
+ sprint: "Sprints"
15379
+ };
15380
+ function evaluateHealth(projectName, metrics) {
15381
+ const completeness = [];
15382
+ for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
15383
+ const { total, complete, gaps } = catMetrics;
15384
+ const status = completenessStatus(total, complete);
15385
+ const pct = total > 0 ? Math.round(complete / total * 100) : 100;
15386
+ completeness.push({
15387
+ name: TYPE_LABELS[type] ?? type,
15388
+ status,
15389
+ summary: `${pct}% complete (${complete}/${total})`,
15390
+ items: gaps.map((g) => ({
15391
+ id: g.id,
15392
+ detail: `missing: ${g.missingFields.join(", ")}`
15393
+ }))
15394
+ });
15395
+ }
15396
+ const process3 = [];
15397
+ const staleCount = metrics.process.stale.length;
15398
+ const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
15399
+ process3.push({
15400
+ name: "Stale Items",
15401
+ status: staleStatus,
15402
+ summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
15403
+ items: metrics.process.stale.map((s) => ({
15404
+ id: s.id,
15405
+ detail: `${s.days} days since last update`
15406
+ }))
15407
+ });
15408
+ const agingCount = metrics.process.agingActions.length;
15409
+ const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
15410
+ process3.push({
15411
+ name: "Aging Actions",
15412
+ status: agingStatus,
15413
+ summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
15414
+ items: metrics.process.agingActions.map((a) => ({
15415
+ id: a.id,
15416
+ detail: `open for ${a.days} days`
15417
+ }))
15418
+ });
15419
+ const dv = metrics.process.decisionVelocity;
15420
+ let dvStatus;
15421
+ if (dv.count === 0) {
15422
+ dvStatus = "green";
15423
+ } else if (dv.avgDays <= 7) {
15424
+ dvStatus = "green";
15425
+ } else if (dv.avgDays <= 21) {
15426
+ dvStatus = "amber";
15427
+ } else {
15428
+ dvStatus = "red";
15429
+ }
15430
+ process3.push({
15431
+ name: "Decision Velocity",
15432
+ status: dvStatus,
15433
+ summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
15434
+ items: []
15435
+ });
15436
+ const qr = metrics.process.questionResolution;
15437
+ let qrStatus;
15438
+ if (qr.count === 0) {
15439
+ qrStatus = "green";
15440
+ } else if (qr.avgDays <= 7) {
15441
+ qrStatus = "green";
15442
+ } else if (qr.avgDays <= 14) {
15443
+ qrStatus = "amber";
15444
+ } else {
15445
+ qrStatus = "red";
15446
+ }
15447
+ process3.push({
15448
+ name: "Question Resolution",
15449
+ status: qrStatus,
15450
+ summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
15451
+ items: []
15452
+ });
15453
+ const allStatuses = [
15454
+ ...completeness.map((c) => c.status),
15455
+ ...process3.map((p) => p.status)
15456
+ ];
15457
+ const overall = worstStatus2(allStatuses);
15458
+ return {
15459
+ projectName,
15460
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15461
+ overall,
15462
+ completeness,
15463
+ process: process3
15464
+ };
15465
+ }
15466
+
15125
15467
  // src/plugins/builtin/tools/reports.ts
15126
15468
  function createReportTools(store) {
15127
15469
  return [
@@ -15141,7 +15483,8 @@ function createReportTools(store) {
15141
15483
  id: d.frontmatter.id,
15142
15484
  title: d.frontmatter.title,
15143
15485
  owner: d.frontmatter.owner,
15144
- priority: d.frontmatter.priority
15486
+ priority: d.frontmatter.priority,
15487
+ dueDate: d.frontmatter.dueDate
15145
15488
  })),
15146
15489
  completedActions: completedActions.map((d) => ({
15147
15490
  id: d.frontmatter.id,
@@ -15160,7 +15503,7 @@ function createReportTools(store) {
15160
15503
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15161
15504
  };
15162
15505
  },
15163
- { annotations: { readOnly: true } }
15506
+ { annotations: { readOnlyHint: true } }
15164
15507
  ),
15165
15508
  tool8(
15166
15509
  "generate_risk_register",
@@ -15169,7 +15512,7 @@ function createReportTools(store) {
15169
15512
  async () => {
15170
15513
  const allDocs = store.list();
15171
15514
  const taggedRisks = allDocs.filter(
15172
- (d) => d.frontmatter.tags?.includes("risk")
15515
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
15173
15516
  );
15174
15517
  const highPriorityActions = store.list({ type: "action", status: "open" }).filter((d) => d.frontmatter.priority === "high");
15175
15518
  const unresolvedQuestions = store.list({ type: "question", status: "open" });
@@ -15204,7 +15547,7 @@ function createReportTools(store) {
15204
15547
  content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
15205
15548
  };
15206
15549
  },
15207
- { annotations: { readOnly: true } }
15550
+ { annotations: { readOnlyHint: true } }
15208
15551
  ),
15209
15552
  tool8(
15210
15553
  "generate_gar_report",
@@ -15217,7 +15560,7 @@ function createReportTools(store) {
15217
15560
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15218
15561
  };
15219
15562
  },
15220
- { annotations: { readOnly: true } }
15563
+ { annotations: { readOnlyHint: true } }
15221
15564
  ),
15222
15565
  tool8(
15223
15566
  "generate_epic_progress",
@@ -15302,7 +15645,7 @@ function createReportTools(store) {
15302
15645
  ]
15303
15646
  };
15304
15647
  },
15305
- { annotations: { readOnly: true } }
15648
+ { annotations: { readOnlyHint: true } }
15306
15649
  ),
15307
15650
  tool8(
15308
15651
  "generate_sprint_progress",
@@ -15347,7 +15690,8 @@ function createReportTools(store) {
15347
15690
  id: d.frontmatter.id,
15348
15691
  title: d.frontmatter.title,
15349
15692
  type: d.frontmatter.type,
15350
- status: d.frontmatter.status
15693
+ status: d.frontmatter.status,
15694
+ dueDate: d.frontmatter.dueDate
15351
15695
  }))
15352
15696
  }
15353
15697
  };
@@ -15356,7 +15700,7 @@ function createReportTools(store) {
15356
15700
  content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
15357
15701
  };
15358
15702
  },
15359
- { annotations: { readOnly: true } }
15703
+ { annotations: { readOnlyHint: true } }
15360
15704
  ),
15361
15705
  tool8(
15362
15706
  "generate_feature_progress",
@@ -15396,7 +15740,20 @@ function createReportTools(store) {
15396
15740
  content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
15397
15741
  };
15398
15742
  },
15399
- { annotations: { readOnly: true } }
15743
+ { annotations: { readOnlyHint: true } }
15744
+ ),
15745
+ tool8(
15746
+ "generate_health_report",
15747
+ "Generate a governance health check report covering artifact completeness and process health metrics",
15748
+ {},
15749
+ async () => {
15750
+ const metrics = collectHealthMetrics(store);
15751
+ const report = evaluateHealth("project", metrics);
15752
+ return {
15753
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15754
+ };
15755
+ },
15756
+ { annotations: { readOnlyHint: true } }
15400
15757
  ),
15401
15758
  tool8(
15402
15759
  "save_report",
@@ -15458,7 +15815,7 @@ function createFeatureTools(store) {
15458
15815
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15459
15816
  };
15460
15817
  },
15461
- { annotations: { readOnly: true } }
15818
+ { annotations: { readOnlyHint: true } }
15462
15819
  ),
15463
15820
  tool9(
15464
15821
  "get_feature",
@@ -15485,7 +15842,7 @@ function createFeatureTools(store) {
15485
15842
  ]
15486
15843
  };
15487
15844
  },
15488
- { annotations: { readOnly: true } }
15845
+ { annotations: { readOnlyHint: true } }
15489
15846
  ),
15490
15847
  tool9(
15491
15848
  "create_feature",
@@ -15526,7 +15883,8 @@ function createFeatureTools(store) {
15526
15883
  status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
15527
15884
  content: external_exports.string().optional().describe("New content"),
15528
15885
  owner: external_exports.string().optional().describe("New owner"),
15529
- priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority")
15886
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
15887
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
15530
15888
  },
15531
15889
  async (args) => {
15532
15890
  const { id, content, ...updates } = args;
@@ -15576,7 +15934,7 @@ function createEpicTools(store) {
15576
15934
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15577
15935
  };
15578
15936
  },
15579
- { annotations: { readOnly: true } }
15937
+ { annotations: { readOnlyHint: true } }
15580
15938
  ),
15581
15939
  tool10(
15582
15940
  "get_epic",
@@ -15603,7 +15961,7 @@ function createEpicTools(store) {
15603
15961
  ]
15604
15962
  };
15605
15963
  },
15606
- { annotations: { readOnly: true } }
15964
+ { annotations: { readOnlyHint: true } }
15607
15965
  ),
15608
15966
  tool10(
15609
15967
  "create_epic",
@@ -15683,7 +16041,8 @@ function createEpicTools(store) {
15683
16041
  content: external_exports.string().optional().describe("New content"),
15684
16042
  owner: external_exports.string().optional().describe("New owner"),
15685
16043
  targetDate: external_exports.string().optional().describe("New target date"),
15686
- estimatedEffort: external_exports.string().optional().describe("New estimated effort")
16044
+ estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
16045
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
15687
16046
  },
15688
16047
  async (args) => {
15689
16048
  const { id, content, ...updates } = args;
@@ -15735,7 +16094,7 @@ function createContributionTools(store) {
15735
16094
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15736
16095
  };
15737
16096
  },
15738
- { annotations: { readOnly: true } }
16097
+ { annotations: { readOnlyHint: true } }
15739
16098
  ),
15740
16099
  tool11(
15741
16100
  "get_contribution",
@@ -15762,7 +16121,7 @@ function createContributionTools(store) {
15762
16121
  ]
15763
16122
  };
15764
16123
  },
15765
- { annotations: { readOnly: true } }
16124
+ { annotations: { readOnlyHint: true } }
15766
16125
  ),
15767
16126
  tool11(
15768
16127
  "create_contribution",
@@ -15847,7 +16206,7 @@ function createSprintTools(store) {
15847
16206
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15848
16207
  };
15849
16208
  },
15850
- { annotations: { readOnly: true } }
16209
+ { annotations: { readOnlyHint: true } }
15851
16210
  ),
15852
16211
  tool12(
15853
16212
  "get_sprint",
@@ -15874,7 +16233,7 @@ function createSprintTools(store) {
15874
16233
  ]
15875
16234
  };
15876
16235
  },
15877
- { annotations: { readOnly: true } }
16236
+ { annotations: { readOnlyHint: true } }
15878
16237
  ),
15879
16238
  tool12(
15880
16239
  "create_sprint",
@@ -16171,7 +16530,7 @@ function createSprintPlanningTools(store) {
16171
16530
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16172
16531
  };
16173
16532
  },
16174
- { annotations: { readOnly: true } }
16533
+ { annotations: { readOnlyHint: true } }
16175
16534
  )
16176
16535
  ];
16177
16536
  }
@@ -16225,6 +16584,7 @@ var genericAgilePlugin = {
16225
16584
  - Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
16226
16585
  - Use priority levels (critical, high, medium, low) to communicate business value.
16227
16586
  - Tag features for categorization and cross-referencing.
16587
+ - Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
16228
16588
 
16229
16589
  **Contribution Tools:**
16230
16590
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -16255,6 +16615,7 @@ var genericAgilePlugin = {
16255
16615
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
16256
16616
  - Collaborate with the Delivery Manager on target dates and effort estimates.
16257
16617
  - Each epic should have a clear scope and definition of done.
16618
+ - Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
16258
16619
 
16259
16620
  **Contribution Tools:**
16260
16621
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -16314,6 +16675,11 @@ var genericAgilePlugin = {
16314
16675
  - **generate_sprint_progress**: Progress report for a specific sprint or all sprints \u2014 shows linked epics with statuses, work items tagged \`sprint:SP-xxx\` grouped by status, and done/total completion %.
16315
16676
  - Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
16316
16677
 
16678
+ **Date Enforcement:**
16679
+ - 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.
16680
+ - When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
16681
+ - Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
16682
+
16317
16683
  **Sprint Workflow:**
16318
16684
  - Create sprints with clear goals and date boundaries.
16319
16685
  - Assign epics to sprints via linkedEpics.
@@ -16333,7 +16699,7 @@ var genericAgilePlugin = {
16333
16699
  **Sprints** (SP-xxx): Time-boxed iterations that group epics and work items with delivery dates. Sprints progress through planned \u2192 active \u2192 completed (or cancelled).
16334
16700
  **Meetings**: Meeting records with attendees, agendas, and notes.
16335
16701
 
16336
- **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags.
16702
+ **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags. Actions support a \`dueDate\` field for schedule tracking \u2014 actions with a past due date are automatically flagged as overdue in GAR reports. Use the \`sprints\` parameter on create_action/update_action to assign actions to sprints.
16337
16703
 
16338
16704
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
16339
16705
  - **create_meeting**: Record meetings with attendees, date, and agenda. The meeting date is required \u2014 extract it from the meeting content or ask the user if not found.
@@ -16379,7 +16745,7 @@ function createUseCaseTools(store) {
16379
16745
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16380
16746
  };
16381
16747
  },
16382
- { annotations: { readOnly: true } }
16748
+ { annotations: { readOnlyHint: true } }
16383
16749
  ),
16384
16750
  tool14(
16385
16751
  "get_use_case",
@@ -16406,7 +16772,7 @@ function createUseCaseTools(store) {
16406
16772
  ]
16407
16773
  };
16408
16774
  },
16409
- { annotations: { readOnly: true } }
16775
+ { annotations: { readOnlyHint: true } }
16410
16776
  ),
16411
16777
  tool14(
16412
16778
  "create_use_case",
@@ -16504,7 +16870,7 @@ function createTechAssessmentTools(store) {
16504
16870
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16505
16871
  };
16506
16872
  },
16507
- { annotations: { readOnly: true } }
16873
+ { annotations: { readOnlyHint: true } }
16508
16874
  ),
16509
16875
  tool15(
16510
16876
  "get_tech_assessment",
@@ -16531,7 +16897,7 @@ function createTechAssessmentTools(store) {
16531
16897
  ]
16532
16898
  };
16533
16899
  },
16534
- { annotations: { readOnly: true } }
16900
+ { annotations: { readOnlyHint: true } }
16535
16901
  ),
16536
16902
  tool15(
16537
16903
  "create_tech_assessment",
@@ -16665,7 +17031,7 @@ function createExtensionDesignTools(store) {
16665
17031
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16666
17032
  };
16667
17033
  },
16668
- { annotations: { readOnly: true } }
17034
+ { annotations: { readOnlyHint: true } }
16669
17035
  ),
16670
17036
  tool16(
16671
17037
  "get_extension_design",
@@ -16692,7 +17058,7 @@ function createExtensionDesignTools(store) {
16692
17058
  ]
16693
17059
  };
16694
17060
  },
16695
- { annotations: { readOnly: true } }
17061
+ { annotations: { readOnlyHint: true } }
16696
17062
  ),
16697
17063
  tool16(
16698
17064
  "create_extension_design",
@@ -16844,7 +17210,7 @@ function createAemReportTools(store) {
16844
17210
  ]
16845
17211
  };
16846
17212
  },
16847
- { annotations: { readOnly: true } }
17213
+ { annotations: { readOnlyHint: true } }
16848
17214
  ),
16849
17215
  tool17(
16850
17216
  "generate_tech_readiness",
@@ -16896,7 +17262,7 @@ function createAemReportTools(store) {
16896
17262
  ]
16897
17263
  };
16898
17264
  },
16899
- { annotations: { readOnly: true } }
17265
+ { annotations: { readOnlyHint: true } }
16900
17266
  ),
16901
17267
  tool17(
16902
17268
  "generate_phase_status",
@@ -16951,7 +17317,7 @@ function createAemReportTools(store) {
16951
17317
  ]
16952
17318
  };
16953
17319
  },
16954
- { annotations: { readOnly: true } }
17320
+ { annotations: { readOnlyHint: true } }
16955
17321
  )
16956
17322
  ];
16957
17323
  }
@@ -16983,7 +17349,7 @@ function createAemPhaseTools(store, marvinDir) {
16983
17349
  ]
16984
17350
  };
16985
17351
  },
16986
- { annotations: { readOnly: true } }
17352
+ { annotations: { readOnlyHint: true } }
16987
17353
  ),
16988
17354
  tool18(
16989
17355
  "advance_phase",
@@ -17428,7 +17794,7 @@ function createJiraTools(store) {
17428
17794
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17429
17795
  };
17430
17796
  },
17431
- { annotations: { readOnly: true } }
17797
+ { annotations: { readOnlyHint: true } }
17432
17798
  ),
17433
17799
  tool19(
17434
17800
  "get_jira_issue",
@@ -17460,7 +17826,7 @@ function createJiraTools(store) {
17460
17826
  ]
17461
17827
  };
17462
17828
  },
17463
- { annotations: { readOnly: true } }
17829
+ { annotations: { readOnlyHint: true } }
17464
17830
  ),
17465
17831
  // --- Jira → Local tools ---
17466
17832
  tool19(
@@ -18019,6 +18385,46 @@ function getBoardData(store, type) {
18019
18385
  }));
18020
18386
  return { columns, type, types };
18021
18387
  }
18388
+ function getDiagramData(store) {
18389
+ const allDocs = store.list();
18390
+ const sprints = [];
18391
+ const epics = [];
18392
+ const features = [];
18393
+ const statusCounts = {};
18394
+ for (const doc of allDocs) {
18395
+ const fm = doc.frontmatter;
18396
+ const status = fm.status.toLowerCase();
18397
+ statusCounts[status] = (statusCounts[status] ?? 0) + 1;
18398
+ switch (fm.type) {
18399
+ case "sprint":
18400
+ sprints.push({
18401
+ id: fm.id,
18402
+ title: fm.title,
18403
+ status: fm.status,
18404
+ startDate: fm.startDate,
18405
+ endDate: fm.endDate,
18406
+ linkedEpics: fm.linkedEpics ?? []
18407
+ });
18408
+ break;
18409
+ case "epic":
18410
+ epics.push({
18411
+ id: fm.id,
18412
+ title: fm.title,
18413
+ status: fm.status,
18414
+ linkedFeature: fm.linkedFeature
18415
+ });
18416
+ break;
18417
+ case "feature":
18418
+ features.push({
18419
+ id: fm.id,
18420
+ title: fm.title,
18421
+ status: fm.status
18422
+ });
18423
+ break;
18424
+ }
18425
+ }
18426
+ return { sprints, epics, features, statusCounts };
18427
+ }
18022
18428
 
18023
18429
  // src/web/templates/layout.ts
18024
18430
  function escapeHtml(str) {
@@ -18049,16 +18455,43 @@ function renderMarkdown(md) {
18049
18455
  const out = [];
18050
18456
  let inList = false;
18051
18457
  let listTag = "ul";
18052
- for (const raw of lines) {
18053
- const line = raw;
18458
+ let inTable = false;
18459
+ let i = 0;
18460
+ while (i < lines.length) {
18461
+ const line = lines[i];
18054
18462
  if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
18055
18463
  out.push(`</${listTag}>`);
18056
18464
  inList = false;
18057
18465
  }
18466
+ if (inTable && !/^\s*\|/.test(line)) {
18467
+ out.push("</tbody></table></div>");
18468
+ inTable = false;
18469
+ }
18470
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
18471
+ i++;
18472
+ out.push("<hr>");
18473
+ continue;
18474
+ }
18475
+ if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
18476
+ const headers = parseTableRow(line);
18477
+ out.push('<div class="table-wrap"><table><thead><tr>');
18478
+ out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
18479
+ out.push("</tr></thead><tbody>");
18480
+ inTable = true;
18481
+ i += 2;
18482
+ continue;
18483
+ }
18484
+ if (inTable && /^\s*\|/.test(line)) {
18485
+ const cells = parseTableRow(line);
18486
+ out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
18487
+ i++;
18488
+ continue;
18489
+ }
18058
18490
  const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
18059
18491
  if (headingMatch) {
18060
18492
  const level = headingMatch[1].length;
18061
18493
  out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
18494
+ i++;
18062
18495
  continue;
18063
18496
  }
18064
18497
  const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
@@ -18070,6 +18503,7 @@ function renderMarkdown(md) {
18070
18503
  listTag = "ul";
18071
18504
  }
18072
18505
  out.push(`<li>${inline(ulMatch[1])}</li>`);
18506
+ i++;
18073
18507
  continue;
18074
18508
  }
18075
18509
  const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
@@ -18081,6 +18515,7 @@ function renderMarkdown(md) {
18081
18515
  listTag = "ol";
18082
18516
  }
18083
18517
  out.push(`<li>${inline(olMatch[1])}</li>`);
18518
+ i++;
18084
18519
  continue;
18085
18520
  }
18086
18521
  if (line.trim() === "") {
@@ -18088,13 +18523,19 @@ function renderMarkdown(md) {
18088
18523
  out.push(`</${listTag}>`);
18089
18524
  inList = false;
18090
18525
  }
18526
+ i++;
18091
18527
  continue;
18092
18528
  }
18093
18529
  out.push(`<p>${inline(line)}</p>`);
18530
+ i++;
18094
18531
  }
18095
18532
  if (inList) out.push(`</${listTag}>`);
18533
+ if (inTable) out.push("</tbody></table></div>");
18096
18534
  return out.join("\n");
18097
18535
  }
18536
+ function parseTableRow(line) {
18537
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
18538
+ }
18098
18539
  function inline(text) {
18099
18540
  let s = escapeHtml(text);
18100
18541
  s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
@@ -18108,7 +18549,8 @@ function layout(opts, body) {
18108
18549
  const topItems = [
18109
18550
  { href: "/", label: "Overview" },
18110
18551
  { href: "/board", label: "Board" },
18111
- { href: "/gar", label: "GAR Report" }
18552
+ { href: "/gar", label: "GAR Report" },
18553
+ { href: "/health", label: "Health" }
18112
18554
  ];
18113
18555
  const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
18114
18556
  const groupsHtml = opts.navGroups.map((group) => {
@@ -18143,9 +18585,15 @@ function layout(opts, body) {
18143
18585
  </nav>
18144
18586
  </aside>
18145
18587
  <main class="main">
18588
+ <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18589
+ <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>
18590
+ <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>
18591
+ </button>
18146
18592
  ${body}
18147
18593
  </main>
18148
18594
  </div>
18595
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18596
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
18149
18597
  </body>
18150
18598
  </html>`;
18151
18599
  }
@@ -18260,7 +18708,33 @@ a:hover { text-decoration: underline; }
18260
18708
  flex: 1;
18261
18709
  padding: 2rem 2.5rem;
18262
18710
  max-width: 1200px;
18711
+ position: relative;
18712
+ transition: max-width 0.2s ease;
18263
18713
  }
18714
+ .main.expanded {
18715
+ max-width: none;
18716
+ }
18717
+ .expand-toggle {
18718
+ position: absolute;
18719
+ top: 1rem;
18720
+ right: 1rem;
18721
+ background: var(--bg-card);
18722
+ border: 1px solid var(--border);
18723
+ border-radius: var(--radius);
18724
+ color: var(--text-dim);
18725
+ cursor: pointer;
18726
+ padding: 0.4rem;
18727
+ display: flex;
18728
+ align-items: center;
18729
+ justify-content: center;
18730
+ transition: color 0.15s, border-color 0.15s;
18731
+ }
18732
+ .expand-toggle:hover {
18733
+ color: var(--text);
18734
+ border-color: var(--text-dim);
18735
+ }
18736
+ .main.expanded .icon-expand { display: none; }
18737
+ .main:not(.expanded) .icon-collapse { display: none; }
18264
18738
 
18265
18739
  /* Page header */
18266
18740
  .page-header {
@@ -18289,12 +18763,26 @@ a:hover { text-decoration: underline; }
18289
18763
  .breadcrumb a:hover { color: var(--accent); }
18290
18764
  .breadcrumb .sep { margin: 0 0.4rem; }
18291
18765
 
18766
+ /* Card groups */
18767
+ .card-group {
18768
+ margin-bottom: 1.5rem;
18769
+ }
18770
+
18771
+ .card-group-label {
18772
+ font-size: 0.7rem;
18773
+ text-transform: uppercase;
18774
+ letter-spacing: 0.08em;
18775
+ color: var(--text-dim);
18776
+ font-weight: 600;
18777
+ margin-bottom: 0.5rem;
18778
+ }
18779
+
18292
18780
  /* Cards grid */
18293
18781
  .cards {
18294
18782
  display: grid;
18295
18783
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
18296
18784
  gap: 1rem;
18297
- margin-bottom: 2rem;
18785
+ margin-bottom: 0.5rem;
18298
18786
  }
18299
18787
 
18300
18788
  .card {
@@ -18575,6 +19063,14 @@ tr:hover td {
18575
19063
  font-family: var(--mono);
18576
19064
  font-size: 0.85em;
18577
19065
  }
19066
+ .detail-content hr {
19067
+ border: none;
19068
+ border-top: 1px solid var(--border);
19069
+ margin: 1.25rem 0;
19070
+ }
19071
+ .detail-content .table-wrap {
19072
+ margin: 0.75rem 0;
19073
+ }
18578
19074
 
18579
19075
  /* Filters */
18580
19076
  .filters {
@@ -18619,21 +19115,206 @@ tr:hover td {
18619
19115
  .priority-high { color: var(--red); }
18620
19116
  .priority-medium { color: var(--amber); }
18621
19117
  .priority-low { color: var(--green); }
19118
+
19119
+ /* Health */
19120
+ .health-section-title {
19121
+ font-size: 1.1rem;
19122
+ font-weight: 600;
19123
+ margin: 2rem 0 1rem;
19124
+ color: var(--text);
19125
+ }
19126
+
19127
+ /* Mermaid diagrams */
19128
+ .mermaid-container {
19129
+ background: var(--bg-card);
19130
+ border: 1px solid var(--border);
19131
+ border-radius: var(--radius);
19132
+ padding: 1.5rem;
19133
+ margin: 1rem 0;
19134
+ overflow-x: auto;
19135
+ }
19136
+
19137
+ .mermaid-container .mermaid {
19138
+ display: flex;
19139
+ justify-content: center;
19140
+ }
19141
+
19142
+ .mermaid-empty {
19143
+ text-align: center;
19144
+ color: var(--text-dim);
19145
+ font-size: 0.875rem;
19146
+ }
19147
+
19148
+ .mermaid-row {
19149
+ display: grid;
19150
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
19151
+ gap: 1rem;
19152
+ }
19153
+
19154
+ .mermaid-row .mermaid-container {
19155
+ margin: 0;
19156
+ }
18622
19157
  `;
18623
19158
  }
18624
19159
 
19160
+ // src/web/templates/mermaid.ts
19161
+ function sanitize(text, maxLen = 40) {
19162
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
19163
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19164
+ }
19165
+ function mermaidBlock(definition) {
19166
+ return `<div class="mermaid-container"><pre class="mermaid">
19167
+ ${definition}
19168
+ </pre></div>`;
19169
+ }
19170
+ function placeholder(message) {
19171
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19172
+ }
19173
+ function buildTimelineGantt(data) {
19174
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
19175
+ if (sprintsWithDates.length === 0) {
19176
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
19177
+ }
19178
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
19179
+ const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
19180
+ for (const sprint of sprintsWithDates) {
19181
+ lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
19182
+ const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
19183
+ if (linked.length === 0) {
19184
+ lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
19185
+ } else {
19186
+ for (const epic of linked) {
19187
+ const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
19188
+ lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
19189
+ }
19190
+ }
19191
+ }
19192
+ return mermaidBlock(lines.join("\n"));
19193
+ }
19194
+ function buildArtifactFlowchart(data) {
19195
+ if (data.features.length === 0 && data.epics.length === 0) {
19196
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
19197
+ }
19198
+ const lines = ["graph TD"];
19199
+ lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
19200
+ lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
19201
+ lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
19202
+ lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
19203
+ const nodeIds = /* @__PURE__ */ new Set();
19204
+ for (const epic of data.epics) {
19205
+ if (epic.linkedFeature) {
19206
+ const feature = data.features.find((f) => f.id === epic.linkedFeature);
19207
+ if (feature) {
19208
+ const fNode = feature.id.replace(/-/g, "_");
19209
+ const eNode = epic.id.replace(/-/g, "_");
19210
+ if (!nodeIds.has(fNode)) {
19211
+ lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
19212
+ nodeIds.add(fNode);
19213
+ }
19214
+ if (!nodeIds.has(eNode)) {
19215
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19216
+ nodeIds.add(eNode);
19217
+ }
19218
+ lines.push(` ${fNode} --> ${eNode}`);
19219
+ }
19220
+ }
19221
+ }
19222
+ for (const sprint of data.sprints) {
19223
+ const sNode = sprint.id.replace(/-/g, "_");
19224
+ for (const epicId of sprint.linkedEpics) {
19225
+ const epic = data.epics.find((e) => e.id === epicId);
19226
+ if (epic) {
19227
+ const eNode = epic.id.replace(/-/g, "_");
19228
+ if (!nodeIds.has(eNode)) {
19229
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19230
+ nodeIds.add(eNode);
19231
+ }
19232
+ if (!nodeIds.has(sNode)) {
19233
+ lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
19234
+ nodeIds.add(sNode);
19235
+ }
19236
+ lines.push(` ${eNode} --> ${sNode}`);
19237
+ }
19238
+ }
19239
+ }
19240
+ if (nodeIds.size === 0) {
19241
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
19242
+ }
19243
+ const allItems = [
19244
+ ...data.features.map((f) => ({ id: f.id, status: f.status })),
19245
+ ...data.epics.map((e) => ({ id: e.id, status: e.status })),
19246
+ ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
19247
+ ];
19248
+ for (const item of allItems) {
19249
+ const node = item.id.replace(/-/g, "_");
19250
+ if (!nodeIds.has(node)) continue;
19251
+ const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
19252
+ if (cls) {
19253
+ lines.push(` class ${node} ${cls}`);
19254
+ }
19255
+ }
19256
+ return mermaidBlock(lines.join("\n"));
19257
+ }
19258
+ function buildStatusPie(title, counts) {
19259
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
19260
+ if (entries.length === 0) {
19261
+ return placeholder(`No data for ${title}.`);
19262
+ }
19263
+ const lines = [`pie title ${sanitize(title, 60)}`];
19264
+ for (const [label, count] of entries) {
19265
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
19266
+ }
19267
+ return mermaidBlock(lines.join("\n"));
19268
+ }
19269
+ function buildHealthGauge(categories) {
19270
+ const valid = categories.filter((c) => c.total > 0);
19271
+ if (valid.length === 0) {
19272
+ return placeholder("No completeness data available.");
19273
+ }
19274
+ const pies = valid.map((cat) => {
19275
+ const incomplete = cat.total - cat.complete;
19276
+ const lines = [
19277
+ `pie title ${sanitize(cat.name, 30)}`,
19278
+ ` "Complete" : ${cat.complete}`,
19279
+ ` "Incomplete" : ${incomplete}`
19280
+ ];
19281
+ return mermaidBlock(lines.join("\n"));
19282
+ });
19283
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
19284
+ }
19285
+
18625
19286
  // src/web/templates/pages/overview.ts
18626
- function overviewPage(data) {
18627
- const cards = data.types.map(
18628
- (t) => `
19287
+ function renderCard(t) {
19288
+ return `
18629
19289
  <div class="card">
18630
19290
  <a href="/docs/${t.type}">
18631
19291
  <div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
18632
19292
  <div class="card-value">${t.total}</div>
18633
19293
  ${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
18634
19294
  </a>
18635
- </div>`
18636
- ).join("\n");
19295
+ </div>`;
19296
+ }
19297
+ function overviewPage(data, diagrams, navGroups) {
19298
+ const typeMap = new Map(data.types.map((t) => [t.type, t]));
19299
+ const placed = /* @__PURE__ */ new Set();
19300
+ const groupSections = navGroups.map((group) => {
19301
+ const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
19302
+ placed.add(type);
19303
+ return renderCard(typeMap.get(type));
19304
+ });
19305
+ if (groupCards.length === 0) return "";
19306
+ return `
19307
+ <div class="card-group">
19308
+ <div class="card-group-label">${escapeHtml(group.label)}</div>
19309
+ <div class="cards">${groupCards.join("\n")}</div>
19310
+ </div>`;
19311
+ }).filter(Boolean).join("\n");
19312
+ const ungrouped = data.types.filter((t) => !placed.has(t.type));
19313
+ const ungroupedSection = ungrouped.length > 0 ? `
19314
+ <div class="card-group">
19315
+ <div class="card-group-label">Other</div>
19316
+ <div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
19317
+ </div>` : "";
18637
19318
  const rows = data.recent.map(
18638
19319
  (doc) => `
18639
19320
  <tr>
@@ -18649,9 +19330,14 @@ function overviewPage(data) {
18649
19330
  <h2>Project Overview</h2>
18650
19331
  </div>
18651
19332
 
18652
- <div class="cards">
18653
- ${cards}
18654
- </div>
19333
+ ${groupSections}
19334
+ ${ungroupedSection}
19335
+
19336
+ <div class="section-title">Project Timeline</div>
19337
+ ${buildTimelineGantt(diagrams)}
19338
+
19339
+ <div class="section-title">Artifact Relationships</div>
19340
+ ${buildArtifactFlowchart(diagrams)}
18655
19341
 
18656
19342
  <div class="section-title">Recent Activity</div>
18657
19343
  ${data.recent.length > 0 ? `
@@ -18818,6 +19504,76 @@ function garPage(report) {
18818
19504
  <div class="gar-areas">
18819
19505
  ${areaCards}
18820
19506
  </div>
19507
+
19508
+ <div class="section-title">Status Distribution</div>
19509
+ ${buildStatusPie("Action Status", {
19510
+ Open: report.metrics.scope.open,
19511
+ Done: report.metrics.scope.done,
19512
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
19513
+ })}
19514
+ `;
19515
+ }
19516
+
19517
+ // src/web/templates/pages/health.ts
19518
+ function healthPage(report, metrics) {
19519
+ const dotClass = `dot-${report.overall}`;
19520
+ function renderSection(title, categories) {
19521
+ const cards = categories.map(
19522
+ (cat) => `
19523
+ <div class="gar-area">
19524
+ <div class="area-header">
19525
+ <div class="area-dot dot-${cat.status}"></div>
19526
+ <div class="area-name">${escapeHtml(cat.name)}</div>
19527
+ </div>
19528
+ <div class="area-summary">${escapeHtml(cat.summary)}</div>
19529
+ ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
19530
+ </div>`
19531
+ ).join("\n");
19532
+ return `
19533
+ <div class="health-section-title">${escapeHtml(title)}</div>
19534
+ <div class="gar-areas">${cards}</div>
19535
+ `;
19536
+ }
19537
+ return `
19538
+ <div class="page-header">
19539
+ <h2>Governance Health Check</h2>
19540
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
19541
+ </div>
19542
+
19543
+ <div class="gar-overall">
19544
+ <div class="dot ${dotClass}"></div>
19545
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
19546
+ </div>
19547
+
19548
+ ${renderSection("Completeness", report.completeness)}
19549
+
19550
+ <div class="health-section-title">Completeness Overview</div>
19551
+ ${buildHealthGauge(
19552
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
19553
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
19554
+ complete: cat.complete,
19555
+ total: cat.total
19556
+ })) : report.completeness.map((c) => {
19557
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
19558
+ return {
19559
+ name: c.name,
19560
+ complete: match ? parseInt(match[1], 10) : 0,
19561
+ total: match ? parseInt(match[2], 10) : 0
19562
+ };
19563
+ })
19564
+ )}
19565
+
19566
+ ${renderSection("Process", report.process)}
19567
+
19568
+ <div class="health-section-title">Process Summary</div>
19569
+ ${metrics ? buildStatusPie("Process Health", {
19570
+ Stale: metrics.process.stale.length,
19571
+ "Aging Actions": metrics.process.agingActions.length,
19572
+ Healthy: Math.max(
19573
+ 0,
19574
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
19575
+ )
19576
+ }) : ""}
18821
19577
  `;
18822
19578
  }
18823
19579
 
@@ -18884,7 +19640,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
18884
19640
  }
18885
19641
  if (pathname === "/") {
18886
19642
  const data = getOverviewData(store);
18887
- const body = overviewPage(data);
19643
+ const diagrams = getDiagramData(store);
19644
+ const body = overviewPage(data, diagrams, navGroups);
18888
19645
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
18889
19646
  return;
18890
19647
  }
@@ -18894,6 +19651,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
18894
19651
  respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
18895
19652
  return;
18896
19653
  }
19654
+ if (pathname === "/health") {
19655
+ const healthMetrics = collectHealthMetrics(store);
19656
+ const report = evaluateHealth(projectName, healthMetrics);
19657
+ const body = healthPage(report, healthMetrics);
19658
+ respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
19659
+ return;
19660
+ }
18897
19661
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
18898
19662
  if (boardMatch) {
18899
19663
  const type = boardMatch[1];
@@ -19041,7 +19805,7 @@ function createWebTools(store, projectName, navGroups) {
19041
19805
  content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
19042
19806
  };
19043
19807
  },
19044
- { annotations: { readOnly: true } }
19808
+ { annotations: { readOnlyHint: true } }
19045
19809
  ),
19046
19810
  tool20(
19047
19811
  "get_dashboard_overview",
@@ -19063,7 +19827,7 @@ function createWebTools(store, projectName, navGroups) {
19063
19827
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19064
19828
  };
19065
19829
  },
19066
- { annotations: { readOnly: true } }
19830
+ { annotations: { readOnlyHint: true } }
19067
19831
  ),
19068
19832
  tool20(
19069
19833
  "get_dashboard_gar",
@@ -19075,7 +19839,7 @@ function createWebTools(store, projectName, navGroups) {
19075
19839
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
19076
19840
  };
19077
19841
  },
19078
- { annotations: { readOnly: true } }
19842
+ { annotations: { readOnlyHint: true } }
19079
19843
  ),
19080
19844
  tool20(
19081
19845
  "get_dashboard_board",
@@ -19103,7 +19867,7 @@ function createWebTools(store, projectName, navGroups) {
19103
19867
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19104
19868
  };
19105
19869
  },
19106
- { annotations: { readOnly: true } }
19870
+ { annotations: { readOnlyHint: true } }
19107
19871
  )
19108
19872
  ];
19109
19873
  }
@@ -19261,6 +20025,8 @@ var deliveryManager = {
19261
20025
 
19262
20026
  ## How You Work
19263
20027
  - Review open actions (A-xxx) and follow up on overdue items
20028
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
20029
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
19264
20030
  - Ensure decisions (D-xxx) are properly documented with rationale
19265
20031
  - Track questions (Q-xxx) and ensure they get answered
19266
20032
  - Monitor project health and flag risks early
@@ -19486,7 +20252,7 @@ ${summaries}`
19486
20252
  content: [{ type: "text", text: guidance }]
19487
20253
  };
19488
20254
  },
19489
- { annotations: { readOnly: true } }
20255
+ { annotations: { readOnlyHint: true } }
19490
20256
  )
19491
20257
  ];
19492
20258
  }