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.
package/dist/marvin.js CHANGED
@@ -920,10 +920,10 @@ function mergeDefs(...defs) {
920
920
  function cloneDef(schema) {
921
921
  return mergeDefs(schema._zod.def);
922
922
  }
923
- function getElementAtPath(obj, path18) {
924
- if (!path18)
923
+ function getElementAtPath(obj, path20) {
924
+ if (!path20)
925
925
  return obj;
926
- return path18.reduce((acc, key) => acc?.[key], obj);
926
+ return path20.reduce((acc, key) => acc?.[key], obj);
927
927
  }
928
928
  function promiseAllObject(promisesObj) {
929
929
  const keys = Object.keys(promisesObj);
@@ -1306,11 +1306,11 @@ function aborted(x, startIndex = 0) {
1306
1306
  }
1307
1307
  return false;
1308
1308
  }
1309
- function prefixIssues(path18, issues) {
1309
+ function prefixIssues(path20, issues) {
1310
1310
  return issues.map((iss) => {
1311
1311
  var _a2;
1312
1312
  (_a2 = iss).path ?? (_a2.path = []);
1313
- iss.path.unshift(path18);
1313
+ iss.path.unshift(path20);
1314
1314
  return iss;
1315
1315
  });
1316
1316
  }
@@ -1493,7 +1493,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1493
1493
  }
1494
1494
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1495
1495
  const result = { errors: [] };
1496
- const processError = (error49, path18 = []) => {
1496
+ const processError = (error49, path20 = []) => {
1497
1497
  var _a2, _b;
1498
1498
  for (const issue2 of error49.issues) {
1499
1499
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1503,7 +1503,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1503
1503
  } else if (issue2.code === "invalid_element") {
1504
1504
  processError({ issues: issue2.issues }, issue2.path);
1505
1505
  } else {
1506
- const fullpath = [...path18, ...issue2.path];
1506
+ const fullpath = [...path20, ...issue2.path];
1507
1507
  if (fullpath.length === 0) {
1508
1508
  result.errors.push(mapper(issue2));
1509
1509
  continue;
@@ -1535,8 +1535,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1535
1535
  }
1536
1536
  function toDotPath(_path) {
1537
1537
  const segs = [];
1538
- const path18 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1539
- for (const seg of path18) {
1538
+ const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1539
+ for (const seg of path20) {
1540
1540
  if (typeof seg === "number")
1541
1541
  segs.push(`[${seg}]`);
1542
1542
  else if (typeof seg === "symbol")
@@ -13513,13 +13513,13 @@ function resolveRef(ref, ctx) {
13513
13513
  if (!ref.startsWith("#")) {
13514
13514
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13515
13515
  }
13516
- const path18 = ref.slice(1).split("/").filter(Boolean);
13517
- if (path18.length === 0) {
13516
+ const path20 = ref.slice(1).split("/").filter(Boolean);
13517
+ if (path20.length === 0) {
13518
13518
  return ctx.rootSchema;
13519
13519
  }
13520
13520
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13521
- if (path18[0] === defsKey) {
13522
- const key = path18[1];
13521
+ if (path20[0] === defsKey) {
13522
+ const key = path20[1];
13523
13523
  if (!key || !ctx.defs[key]) {
13524
13524
  throw new Error(`Reference not found: ${ref}`);
13525
13525
  }
@@ -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,12 +14111,20 @@ 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
- (d) => d.frontmatter.tags?.includes("risk")
14127
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
14120
14128
  );
14121
14129
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14122
14130
  const total = allActions.length;
@@ -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",
@@ -14269,7 +14525,7 @@ function createReportTools(store) {
14269
14525
  async () => {
14270
14526
  const allDocs = store.list();
14271
14527
  const taggedRisks = allDocs.filter(
14272
- (d) => d.frontmatter.tags?.includes("risk")
14528
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
14273
14529
  );
14274
14530
  const highPriorityActions = store.list({ type: "action", status: "open" }).filter((d) => d.frontmatter.priority === "high");
14275
14531
  const unresolvedQuestions = store.list({ type: "question", status: "open" });
@@ -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",
@@ -14626,7 +14896,8 @@ function createFeatureTools(store) {
14626
14896
  status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
14627
14897
  content: external_exports.string().optional().describe("New content"),
14628
14898
  owner: external_exports.string().optional().describe("New owner"),
14629
- priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority")
14899
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
14900
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14630
14901
  },
14631
14902
  async (args) => {
14632
14903
  const { id, content, ...updates } = args;
@@ -14676,7 +14947,7 @@ function createEpicTools(store) {
14676
14947
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14677
14948
  };
14678
14949
  },
14679
- { annotations: { readOnly: true } }
14950
+ { annotations: { readOnlyHint: true } }
14680
14951
  ),
14681
14952
  tool4(
14682
14953
  "get_epic",
@@ -14703,7 +14974,7 @@ function createEpicTools(store) {
14703
14974
  ]
14704
14975
  };
14705
14976
  },
14706
- { annotations: { readOnly: true } }
14977
+ { annotations: { readOnlyHint: true } }
14707
14978
  ),
14708
14979
  tool4(
14709
14980
  "create_epic",
@@ -14783,7 +15054,8 @@ function createEpicTools(store) {
14783
15054
  content: external_exports.string().optional().describe("New content"),
14784
15055
  owner: external_exports.string().optional().describe("New owner"),
14785
15056
  targetDate: external_exports.string().optional().describe("New target date"),
14786
- estimatedEffort: external_exports.string().optional().describe("New estimated effort")
15057
+ estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
15058
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14787
15059
  },
14788
15060
  async (args) => {
14789
15061
  const { id, content, ...updates } = args;
@@ -14835,7 +15107,7 @@ function createContributionTools(store) {
14835
15107
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14836
15108
  };
14837
15109
  },
14838
- { annotations: { readOnly: true } }
15110
+ { annotations: { readOnlyHint: true } }
14839
15111
  ),
14840
15112
  tool5(
14841
15113
  "get_contribution",
@@ -14862,7 +15134,7 @@ function createContributionTools(store) {
14862
15134
  ]
14863
15135
  };
14864
15136
  },
14865
- { annotations: { readOnly: true } }
15137
+ { annotations: { readOnlyHint: true } }
14866
15138
  ),
14867
15139
  tool5(
14868
15140
  "create_contribution",
@@ -14947,7 +15219,7 @@ function createSprintTools(store) {
14947
15219
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14948
15220
  };
14949
15221
  },
14950
- { annotations: { readOnly: true } }
15222
+ { annotations: { readOnlyHint: true } }
14951
15223
  ),
14952
15224
  tool6(
14953
15225
  "get_sprint",
@@ -14974,7 +15246,7 @@ function createSprintTools(store) {
14974
15246
  ]
14975
15247
  };
14976
15248
  },
14977
- { annotations: { readOnly: true } }
15249
+ { annotations: { readOnlyHint: true } }
14978
15250
  ),
14979
15251
  tool6(
14980
15252
  "create_sprint",
@@ -15271,7 +15543,7 @@ function createSprintPlanningTools(store) {
15271
15543
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15272
15544
  };
15273
15545
  },
15274
- { annotations: { readOnly: true } }
15546
+ { annotations: { readOnlyHint: true } }
15275
15547
  )
15276
15548
  ];
15277
15549
  }
@@ -15325,6 +15597,7 @@ var genericAgilePlugin = {
15325
15597
  - Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
15326
15598
  - Use priority levels (critical, high, medium, low) to communicate business value.
15327
15599
  - Tag features for categorization and cross-referencing.
15600
+ - Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
15328
15601
 
15329
15602
  **Contribution Tools:**
15330
15603
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -15355,6 +15628,7 @@ var genericAgilePlugin = {
15355
15628
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
15356
15629
  - Collaborate with the Delivery Manager on target dates and effort estimates.
15357
15630
  - Each epic should have a clear scope and definition of done.
15631
+ - Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
15358
15632
 
15359
15633
  **Contribution Tools:**
15360
15634
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -15414,6 +15688,11 @@ var genericAgilePlugin = {
15414
15688
  - **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
15689
  - Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
15416
15690
 
15691
+ **Date Enforcement:**
15692
+ - 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.
15693
+ - When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
15694
+ - Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
15695
+
15417
15696
  **Sprint Workflow:**
15418
15697
  - Create sprints with clear goals and date boundaries.
15419
15698
  - Assign epics to sprints via linkedEpics.
@@ -15433,7 +15712,7 @@ var genericAgilePlugin = {
15433
15712
  **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
15713
  **Meetings**: Meeting records with attendees, agendas, and notes.
15435
15714
 
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.
15715
+ **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
15716
 
15438
15717
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
15439
15718
  - **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 +15758,7 @@ function createUseCaseTools(store) {
15479
15758
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15480
15759
  };
15481
15760
  },
15482
- { annotations: { readOnly: true } }
15761
+ { annotations: { readOnlyHint: true } }
15483
15762
  ),
15484
15763
  tool8(
15485
15764
  "get_use_case",
@@ -15506,7 +15785,7 @@ function createUseCaseTools(store) {
15506
15785
  ]
15507
15786
  };
15508
15787
  },
15509
- { annotations: { readOnly: true } }
15788
+ { annotations: { readOnlyHint: true } }
15510
15789
  ),
15511
15790
  tool8(
15512
15791
  "create_use_case",
@@ -15604,7 +15883,7 @@ function createTechAssessmentTools(store) {
15604
15883
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15605
15884
  };
15606
15885
  },
15607
- { annotations: { readOnly: true } }
15886
+ { annotations: { readOnlyHint: true } }
15608
15887
  ),
15609
15888
  tool9(
15610
15889
  "get_tech_assessment",
@@ -15631,7 +15910,7 @@ function createTechAssessmentTools(store) {
15631
15910
  ]
15632
15911
  };
15633
15912
  },
15634
- { annotations: { readOnly: true } }
15913
+ { annotations: { readOnlyHint: true } }
15635
15914
  ),
15636
15915
  tool9(
15637
15916
  "create_tech_assessment",
@@ -15765,7 +16044,7 @@ function createExtensionDesignTools(store) {
15765
16044
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15766
16045
  };
15767
16046
  },
15768
- { annotations: { readOnly: true } }
16047
+ { annotations: { readOnlyHint: true } }
15769
16048
  ),
15770
16049
  tool10(
15771
16050
  "get_extension_design",
@@ -15792,7 +16071,7 @@ function createExtensionDesignTools(store) {
15792
16071
  ]
15793
16072
  };
15794
16073
  },
15795
- { annotations: { readOnly: true } }
16074
+ { annotations: { readOnlyHint: true } }
15796
16075
  ),
15797
16076
  tool10(
15798
16077
  "create_extension_design",
@@ -15944,7 +16223,7 @@ function createAemReportTools(store) {
15944
16223
  ]
15945
16224
  };
15946
16225
  },
15947
- { annotations: { readOnly: true } }
16226
+ { annotations: { readOnlyHint: true } }
15948
16227
  ),
15949
16228
  tool11(
15950
16229
  "generate_tech_readiness",
@@ -15996,7 +16275,7 @@ function createAemReportTools(store) {
15996
16275
  ]
15997
16276
  };
15998
16277
  },
15999
- { annotations: { readOnly: true } }
16278
+ { annotations: { readOnlyHint: true } }
16000
16279
  ),
16001
16280
  tool11(
16002
16281
  "generate_phase_status",
@@ -16051,7 +16330,7 @@ function createAemReportTools(store) {
16051
16330
  ]
16052
16331
  };
16053
16332
  },
16054
- { annotations: { readOnly: true } }
16333
+ { annotations: { readOnlyHint: true } }
16055
16334
  )
16056
16335
  ];
16057
16336
  }
@@ -16083,7 +16362,7 @@ function createAemPhaseTools(store, marvinDir) {
16083
16362
  ]
16084
16363
  };
16085
16364
  },
16086
- { annotations: { readOnly: true } }
16365
+ { annotations: { readOnlyHint: true } }
16087
16366
  ),
16088
16367
  tool12(
16089
16368
  "advance_phase",
@@ -16348,6 +16627,56 @@ function getPluginPromptFragment(plugin, personaId) {
16348
16627
  return plugin.promptFragments[personaId] ?? plugin.promptFragments["*"];
16349
16628
  }
16350
16629
 
16630
+ // src/templates/claude-md.ts
16631
+ function getDefaultClaudeMdContent(projectName) {
16632
+ return `# Marvin \u2014 Project Instructions for "${projectName}"
16633
+
16634
+ You are **Marvin**, an AI-powered product development assistant.
16635
+ You operate as one of three personas \u2014 stay in role and suggest switching when a question falls outside your scope.
16636
+
16637
+ ## Personas
16638
+
16639
+ | Persona | Short | Focus |
16640
+ |---------|-------|-------|
16641
+ | Product Owner | po | Vision, backlog, requirements, features, acceptance criteria |
16642
+ | Delivery Manager | dm | Planning, risks, actions, timelines, sprints, status |
16643
+ | Tech Lead | tl | Architecture, trade-offs, technical decisions, code quality |
16644
+
16645
+ ## Proactive Governance
16646
+
16647
+ When conversation implies a commitment, risk, or open question, **suggest creating the matching artifact**:
16648
+ - A decision was made \u2192 offer to create a **Decision (D-xxx)**
16649
+ - Someone committed to a task \u2192 offer an **Action (A-xxx)** with owner and due date
16650
+ - An unanswered question surfaced \u2192 offer a **Question (Q-xxx)**
16651
+ - A new capability is discussed \u2192 offer a **Feature (F-xxx)**
16652
+ - Implementation scope is agreed \u2192 offer an **Epic (E-xxx)** linked to a feature
16653
+ - Work is being time-boxed \u2192 offer a **Sprint (SP-xxx)**
16654
+
16655
+ ## Insights
16656
+
16657
+ Proactively flag:
16658
+ - Overdue actions or unresolved questions
16659
+ - Decisions without rationale or linked features
16660
+ - Features without linked epics
16661
+ - Risks mentioned but not tracked
16662
+ - When a risk is resolved \u2192 remove the "risk" tag and add "risk-mitigated"
16663
+
16664
+ ## Tool Usage
16665
+
16666
+ - **Search before creating** \u2014 avoid duplicate artifacts
16667
+ - **Reference IDs** (e.g. D-001, A-003) when discussing existing items
16668
+ - **Link artifacts** \u2014 epics to features, actions to decisions, etc.
16669
+ - Use \`search_documents\` to find related context before answering
16670
+
16671
+ ## Communication Style
16672
+
16673
+ - Be concise and structured
16674
+ - State assumptions explicitly
16675
+ - Use bullet points and tables where they aid clarity
16676
+ - When uncertain, ask a clarifying question rather than guessing
16677
+ `;
16678
+ }
16679
+
16351
16680
  // src/cli/commands/init.ts
16352
16681
  async function initCommand() {
16353
16682
  const cwd = process.cwd();
@@ -16404,11 +16733,17 @@ async function initCommand() {
16404
16733
  YAML3.stringify(config2),
16405
16734
  "utf-8"
16406
16735
  );
16736
+ fs4.writeFileSync(
16737
+ path4.join(marvinDir, "CLAUDE.md"),
16738
+ getDefaultClaudeMdContent(projectName),
16739
+ "utf-8"
16740
+ );
16407
16741
  console.log(chalk.green(`
16408
16742
  Initialized Marvin project "${projectName}" in ${cwd}`));
16409
16743
  console.log(chalk.dim(`Methodology: ${plugin?.name ?? methodology}`));
16410
16744
  console.log(chalk.dim("\nCreated:"));
16411
16745
  console.log(chalk.dim(" .marvin/config.yaml"));
16746
+ console.log(chalk.dim(" .marvin/CLAUDE.md"));
16412
16747
  console.log(chalk.dim(" .marvin/docs/decisions/"));
16413
16748
  console.log(chalk.dim(" .marvin/docs/actions/"));
16414
16749
  console.log(chalk.dim(" .marvin/docs/questions/"));
@@ -16517,6 +16852,8 @@ var deliveryManager = {
16517
16852
 
16518
16853
  ## How You Work
16519
16854
  - Review open actions (A-xxx) and follow up on overdue items
16855
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
16856
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
16520
16857
  - Ensure decisions (D-xxx) are properly documented with rationale
16521
16858
  - Track questions (Q-xxx) and ensure they get answered
16522
16859
  - Monitor project health and flag risks early
@@ -16609,8 +16946,8 @@ function resolvePersonaId(input4) {
16609
16946
  }
16610
16947
 
16611
16948
  // src/agent/session.ts
16612
- import * as fs9 from "fs";
16613
- import * as path9 from "path";
16949
+ import * as fs10 from "fs";
16950
+ import * as path10 from "path";
16614
16951
  import * as readline from "readline";
16615
16952
  import chalk2 from "chalk";
16616
16953
  import ora from "ora";
@@ -16619,9 +16956,24 @@ import {
16619
16956
  } from "@anthropic-ai/claude-agent-sdk";
16620
16957
 
16621
16958
  // src/personas/prompt-builder.ts
16622
- function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment) {
16959
+ import * as fs5 from "fs";
16960
+ import * as path5 from "path";
16961
+ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment, marvinDir) {
16623
16962
  const parts = [];
16624
16963
  parts.push(persona.systemPrompt);
16964
+ if (marvinDir) {
16965
+ const claudeMdPath = path5.join(marvinDir, "CLAUDE.md");
16966
+ try {
16967
+ const content = fs5.readFileSync(claudeMdPath, "utf-8").trim();
16968
+ if (content) {
16969
+ parts.push(`
16970
+ ## Project Instructions
16971
+ ${content}
16972
+ `);
16973
+ }
16974
+ } catch {
16975
+ }
16976
+ }
16625
16977
  parts.push(`
16626
16978
  ## Project Context
16627
16979
  - **Project Name:** ${projectConfig.name}
@@ -16631,7 +16983,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
16631
16983
  ## Available Tools
16632
16984
  You have access to governance tools for managing project artifacts:
16633
16985
  - **Decisions** (D-xxx): List, get, create, and update decisions
16634
- - **Actions** (A-xxx): List, get, create, and update action items
16986
+ - **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
16635
16987
  - **Questions** (Q-xxx): List, get, create, and update questions
16636
16988
  - **Features** (F-xxx): List, get, create, and update feature definitions
16637
16989
  - **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
@@ -16668,8 +17020,8 @@ ${projectConfig.personas[persona.id].extraInstructions}
16668
17020
  }
16669
17021
 
16670
17022
  // src/storage/store.ts
16671
- import * as fs5 from "fs";
16672
- import * as path5 from "path";
17023
+ import * as fs6 from "fs";
17024
+ import * as path6 from "path";
16673
17025
 
16674
17026
  // src/storage/document.ts
16675
17027
  import matter from "gray-matter";
@@ -16707,7 +17059,7 @@ var DocumentStore = class {
16707
17059
  typeDirs;
16708
17060
  idPrefixes;
16709
17061
  constructor(marvinDir, registrations) {
16710
- this.docsDir = path5.join(marvinDir, "docs");
17062
+ this.docsDir = path6.join(marvinDir, "docs");
16711
17063
  this.typeDirs = { ...CORE_TYPE_DIRS };
16712
17064
  this.idPrefixes = { ...CORE_ID_PREFIXES };
16713
17065
  for (const reg of registrations ?? []) {
@@ -16722,12 +17074,12 @@ var DocumentStore = class {
16722
17074
  buildIndex() {
16723
17075
  this.index.clear();
16724
17076
  for (const type of Object.keys(this.typeDirs)) {
16725
- const dir = path5.join(this.docsDir, this.typeDirs[type]);
16726
- if (!fs5.existsSync(dir)) continue;
16727
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17077
+ const dir = path6.join(this.docsDir, this.typeDirs[type]);
17078
+ if (!fs6.existsSync(dir)) continue;
17079
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
16728
17080
  for (const file2 of files) {
16729
- const filePath = path5.join(dir, file2);
16730
- const raw = fs5.readFileSync(filePath, "utf-8");
17081
+ const filePath = path6.join(dir, file2);
17082
+ const raw = fs6.readFileSync(filePath, "utf-8");
16731
17083
  const doc = parseDocument(raw, filePath);
16732
17084
  if (doc.frontmatter.id) {
16733
17085
  if (this.index.has(doc.frontmatter.id)) {
@@ -16746,12 +17098,12 @@ var DocumentStore = class {
16746
17098
  for (const type of types) {
16747
17099
  const dirName = this.typeDirs[type];
16748
17100
  if (!dirName) continue;
16749
- const dir = path5.join(this.docsDir, dirName);
16750
- if (!fs5.existsSync(dir)) continue;
16751
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17101
+ const dir = path6.join(this.docsDir, dirName);
17102
+ if (!fs6.existsSync(dir)) continue;
17103
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
16752
17104
  for (const file2 of files) {
16753
- const filePath = path5.join(dir, file2);
16754
- const raw = fs5.readFileSync(filePath, "utf-8");
17105
+ const filePath = path6.join(dir, file2);
17106
+ const raw = fs6.readFileSync(filePath, "utf-8");
16755
17107
  const doc = parseDocument(raw, filePath);
16756
17108
  if (query7?.status && doc.frontmatter.status !== query7.status) continue;
16757
17109
  if (query7?.owner && doc.frontmatter.owner !== query7.owner) continue;
@@ -16764,12 +17116,12 @@ var DocumentStore = class {
16764
17116
  }
16765
17117
  get(id) {
16766
17118
  for (const type of Object.keys(this.typeDirs)) {
16767
- const dir = path5.join(this.docsDir, this.typeDirs[type]);
16768
- if (!fs5.existsSync(dir)) continue;
16769
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17119
+ const dir = path6.join(this.docsDir, this.typeDirs[type]);
17120
+ if (!fs6.existsSync(dir)) continue;
17121
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
16770
17122
  for (const file2 of files) {
16771
- const filePath = path5.join(dir, file2);
16772
- const raw = fs5.readFileSync(filePath, "utf-8");
17123
+ const filePath = path6.join(dir, file2);
17124
+ const raw = fs6.readFileSync(filePath, "utf-8");
16773
17125
  const doc = parseDocument(raw, filePath);
16774
17126
  if (doc.frontmatter.id === id) return doc;
16775
17127
  }
@@ -16783,8 +17135,8 @@ var DocumentStore = class {
16783
17135
  if (!dirName) {
16784
17136
  throw new Error(`Unknown document type: ${type}`);
16785
17137
  }
16786
- const dir = path5.join(this.docsDir, dirName);
16787
- fs5.mkdirSync(dir, { recursive: true });
17138
+ const dir = path6.join(this.docsDir, dirName);
17139
+ fs6.mkdirSync(dir, { recursive: true });
16788
17140
  const cleaned = Object.fromEntries(
16789
17141
  Object.entries(frontmatter).filter(([, v]) => v !== void 0)
16790
17142
  );
@@ -16798,13 +17150,13 @@ var DocumentStore = class {
16798
17150
  ...cleaned
16799
17151
  };
16800
17152
  const fileName = type === "meeting" ? `${cleaned.date?.slice(0, 10) ?? now.slice(0, 10)}-${slugify2(fullFrontmatter.title)}.md` : `${id}.md`;
16801
- const filePath = path5.join(dir, fileName);
17153
+ const filePath = path6.join(dir, fileName);
16802
17154
  const doc = {
16803
17155
  frontmatter: fullFrontmatter,
16804
17156
  content,
16805
17157
  filePath
16806
17158
  };
16807
- fs5.writeFileSync(filePath, serializeDocument(doc), "utf-8");
17159
+ fs6.writeFileSync(filePath, serializeDocument(doc), "utf-8");
16808
17160
  this.index.set(id, fullFrontmatter);
16809
17161
  return doc;
16810
17162
  }
@@ -16819,12 +17171,12 @@ var DocumentStore = class {
16819
17171
  `Document ${frontmatter.id} already exists. Resolve conflicts before importing.`
16820
17172
  );
16821
17173
  }
16822
- const dir = path5.join(this.docsDir, dirName);
16823
- fs5.mkdirSync(dir, { recursive: true });
17174
+ const dir = path6.join(this.docsDir, dirName);
17175
+ fs6.mkdirSync(dir, { recursive: true });
16824
17176
  const fileName = type === "meeting" ? `${frontmatter.date?.slice(0, 10) ?? frontmatter.created.slice(0, 10)}-${slugify2(frontmatter.title)}.md` : `${frontmatter.id}.md`;
16825
- const filePath = path5.join(dir, fileName);
17177
+ const filePath = path6.join(dir, fileName);
16826
17178
  const doc = { frontmatter, content, filePath };
16827
- fs5.writeFileSync(filePath, serializeDocument(doc), "utf-8");
17179
+ fs6.writeFileSync(filePath, serializeDocument(doc), "utf-8");
16828
17180
  this.index.set(frontmatter.id, frontmatter);
16829
17181
  return doc;
16830
17182
  }
@@ -16846,7 +17198,7 @@ var DocumentStore = class {
16846
17198
  content: content ?? existing.content,
16847
17199
  filePath: existing.filePath
16848
17200
  };
16849
- fs5.writeFileSync(existing.filePath, serializeDocument(doc), "utf-8");
17201
+ fs6.writeFileSync(existing.filePath, serializeDocument(doc), "utf-8");
16850
17202
  this.index.set(id, updatedFrontmatter);
16851
17203
  return doc;
16852
17204
  }
@@ -16856,14 +17208,14 @@ var DocumentStore = class {
16856
17208
  throw new Error(`Unknown document type: ${type}`);
16857
17209
  }
16858
17210
  const dirName = this.typeDirs[type];
16859
- const dir = path5.join(this.docsDir, dirName);
16860
- if (!fs5.existsSync(dir)) return `${prefix}-001`;
17211
+ const dir = path6.join(this.docsDir, dirName);
17212
+ if (!fs6.existsSync(dir)) return `${prefix}-001`;
16861
17213
  const idPattern = new RegExp(`^${prefix}-(\\d+)$`);
16862
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17214
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
16863
17215
  let maxNum = 0;
16864
17216
  for (const file2 of files) {
16865
- const filePath = path5.join(dir, file2);
16866
- const raw = fs5.readFileSync(filePath, "utf-8");
17217
+ const filePath = path6.join(dir, file2);
17218
+ const raw = fs6.readFileSync(filePath, "utf-8");
16867
17219
  const doc = parseDocument(raw, filePath);
16868
17220
  const match = doc.frontmatter.id?.match(idPattern);
16869
17221
  if (match) {
@@ -16876,12 +17228,12 @@ var DocumentStore = class {
16876
17228
  counts() {
16877
17229
  const result = {};
16878
17230
  for (const type of Object.keys(this.typeDirs)) {
16879
- const dir = path5.join(this.docsDir, this.typeDirs[type]);
16880
- if (!fs5.existsSync(dir)) {
17231
+ const dir = path6.join(this.docsDir, this.typeDirs[type]);
17232
+ if (!fs6.existsSync(dir)) {
16881
17233
  result[type] = 0;
16882
17234
  continue;
16883
17235
  }
16884
- result[type] = fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
17236
+ result[type] = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
16885
17237
  }
16886
17238
  return result;
16887
17239
  }
@@ -16891,13 +17243,13 @@ function slugify2(text) {
16891
17243
  }
16892
17244
 
16893
17245
  // src/storage/session-store.ts
16894
- import * as fs6 from "fs";
16895
- import * as path6 from "path";
17246
+ import * as fs7 from "fs";
17247
+ import * as path7 from "path";
16896
17248
  import * as YAML4 from "yaml";
16897
17249
  var SessionStore = class {
16898
17250
  filePath;
16899
17251
  constructor(marvinDir) {
16900
- this.filePath = path6.join(marvinDir, "sessions.yaml");
17252
+ this.filePath = path7.join(marvinDir, "sessions.yaml");
16901
17253
  }
16902
17254
  list() {
16903
17255
  const entries = this.load();
@@ -16938,9 +17290,9 @@ var SessionStore = class {
16938
17290
  this.write(entries);
16939
17291
  }
16940
17292
  load() {
16941
- if (!fs6.existsSync(this.filePath)) return [];
17293
+ if (!fs7.existsSync(this.filePath)) return [];
16942
17294
  try {
16943
- const raw = fs6.readFileSync(this.filePath, "utf-8");
17295
+ const raw = fs7.readFileSync(this.filePath, "utf-8");
16944
17296
  const parsed = YAML4.parse(raw);
16945
17297
  if (!Array.isArray(parsed)) return [];
16946
17298
  return parsed;
@@ -16949,11 +17301,11 @@ var SessionStore = class {
16949
17301
  }
16950
17302
  }
16951
17303
  write(entries) {
16952
- const dir = path6.dirname(this.filePath);
16953
- if (!fs6.existsSync(dir)) {
16954
- fs6.mkdirSync(dir, { recursive: true });
17304
+ const dir = path7.dirname(this.filePath);
17305
+ if (!fs7.existsSync(dir)) {
17306
+ fs7.mkdirSync(dir, { recursive: true });
16955
17307
  }
16956
- fs6.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
17308
+ fs7.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
16957
17309
  }
16958
17310
  };
16959
17311
 
@@ -16982,7 +17334,7 @@ function createDecisionTools(store) {
16982
17334
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16983
17335
  };
16984
17336
  },
16985
- { annotations: { readOnly: true } }
17337
+ { annotations: { readOnlyHint: true } }
16986
17338
  ),
16987
17339
  tool13(
16988
17340
  "get_decision",
@@ -17009,7 +17361,7 @@ function createDecisionTools(store) {
17009
17361
  ]
17010
17362
  };
17011
17363
  },
17012
- { annotations: { readOnly: true } }
17364
+ { annotations: { readOnlyHint: true } }
17013
17365
  ),
17014
17366
  tool13(
17015
17367
  "create_decision",
@@ -17050,7 +17402,8 @@ function createDecisionTools(store) {
17050
17402
  title: external_exports.string().optional().describe("New title"),
17051
17403
  status: external_exports.string().optional().describe("New status"),
17052
17404
  content: external_exports.string().optional().describe("New content"),
17053
- owner: external_exports.string().optional().describe("New owner")
17405
+ owner: external_exports.string().optional().describe("New owner"),
17406
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
17054
17407
  },
17055
17408
  async (args) => {
17056
17409
  const { id, content, ...updates } = args;
@@ -17070,6 +17423,19 @@ function createDecisionTools(store) {
17070
17423
 
17071
17424
  // src/agent/tools/actions.ts
17072
17425
  import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17426
+ function findMatchingSprints(store, dueDate) {
17427
+ const sprints = store.list({ type: "sprint" });
17428
+ return sprints.filter((s) => {
17429
+ const start = s.frontmatter.startDate;
17430
+ const end = s.frontmatter.endDate;
17431
+ return start && end && dueDate >= start && dueDate <= end;
17432
+ }).map((s) => ({
17433
+ id: s.frontmatter.id,
17434
+ title: s.frontmatter.title,
17435
+ startDate: s.frontmatter.startDate,
17436
+ endDate: s.frontmatter.endDate
17437
+ }));
17438
+ }
17073
17439
  function createActionTools(store) {
17074
17440
  return [
17075
17441
  tool14(
@@ -17085,19 +17451,24 @@ function createActionTools(store) {
17085
17451
  status: args.status,
17086
17452
  owner: args.owner
17087
17453
  });
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
- }));
17454
+ const summary = docs.map((d) => {
17455
+ const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
17456
+ return {
17457
+ id: d.frontmatter.id,
17458
+ title: d.frontmatter.title,
17459
+ status: d.frontmatter.status,
17460
+ owner: d.frontmatter.owner,
17461
+ priority: d.frontmatter.priority,
17462
+ dueDate: d.frontmatter.dueDate,
17463
+ sprints: sprintIds.length > 0 ? sprintIds : void 0,
17464
+ created: d.frontmatter.created
17465
+ };
17466
+ });
17096
17467
  return {
17097
17468
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17098
17469
  };
17099
17470
  },
17100
- { annotations: { readOnly: true } }
17471
+ { annotations: { readOnlyHint: true } }
17101
17472
  ),
17102
17473
  tool14(
17103
17474
  "get_action",
@@ -17124,7 +17495,7 @@ function createActionTools(store) {
17124
17495
  ]
17125
17496
  };
17126
17497
  },
17127
- { annotations: { readOnly: true } }
17498
+ { annotations: { readOnlyHint: true } }
17128
17499
  ),
17129
17500
  tool14(
17130
17501
  "create_action",
@@ -17135,9 +17506,18 @@ function createActionTools(store) {
17135
17506
  status: external_exports.string().optional().describe("Status (default: 'open')"),
17136
17507
  owner: external_exports.string().optional().describe("Person responsible"),
17137
17508
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
17138
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
17509
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
17510
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
17511
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
17139
17512
  },
17140
17513
  async (args) => {
17514
+ const tags = [...args.tags ?? []];
17515
+ if (args.sprints) {
17516
+ for (const sprintId of args.sprints) {
17517
+ const tag = `sprint:${sprintId}`;
17518
+ if (!tags.includes(tag)) tags.push(tag);
17519
+ }
17520
+ }
17141
17521
  const doc = store.create(
17142
17522
  "action",
17143
17523
  {
@@ -17145,17 +17525,21 @@ function createActionTools(store) {
17145
17525
  status: args.status,
17146
17526
  owner: args.owner,
17147
17527
  priority: args.priority,
17148
- tags: args.tags
17528
+ tags: tags.length > 0 ? tags : void 0,
17529
+ dueDate: args.dueDate
17149
17530
  },
17150
17531
  args.content
17151
17532
  );
17533
+ const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
17534
+ if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
17535
+ const matching = findMatchingSprints(store, args.dueDate);
17536
+ if (matching.length > 0) {
17537
+ const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
17538
+ parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
17539
+ }
17540
+ }
17152
17541
  return {
17153
- content: [
17154
- {
17155
- type: "text",
17156
- text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
17157
- }
17158
- ]
17542
+ content: [{ type: "text", text: parts.join("\n") }]
17159
17543
  };
17160
17544
  }
17161
17545
  ),
@@ -17168,10 +17552,35 @@ function createActionTools(store) {
17168
17552
  status: external_exports.string().optional().describe("New status"),
17169
17553
  content: external_exports.string().optional().describe("New content"),
17170
17554
  owner: external_exports.string().optional().describe("New owner"),
17171
- priority: external_exports.string().optional().describe("New priority")
17555
+ priority: external_exports.string().optional().describe("New priority"),
17556
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
17557
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
17558
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
17172
17559
  },
17173
17560
  async (args) => {
17174
- const { id, content, ...updates } = args;
17561
+ const { id, content, sprints, tags, ...updates } = args;
17562
+ if (tags !== void 0) {
17563
+ const merged = [...tags];
17564
+ if (sprints) {
17565
+ for (const s of sprints) {
17566
+ const tag = `sprint:${s}`;
17567
+ if (!merged.includes(tag)) merged.push(tag);
17568
+ }
17569
+ }
17570
+ updates.tags = merged;
17571
+ } else if (sprints !== void 0) {
17572
+ const existing = store.get(id);
17573
+ if (!existing) {
17574
+ return {
17575
+ content: [{ type: "text", text: `Action ${id} not found` }],
17576
+ isError: true
17577
+ };
17578
+ }
17579
+ const existingTags = existing.frontmatter.tags ?? [];
17580
+ const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
17581
+ const newSprintTags = sprints.map((s) => `sprint:${s}`);
17582
+ updates.tags = [...nonSprintTags, ...newSprintTags];
17583
+ }
17175
17584
  const doc = store.update(id, updates, content);
17176
17585
  return {
17177
17586
  content: [
@@ -17182,6 +17591,35 @@ function createActionTools(store) {
17182
17591
  ]
17183
17592
  };
17184
17593
  }
17594
+ ),
17595
+ tool14(
17596
+ "suggest_sprints_for_action",
17597
+ "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
17598
+ {
17599
+ dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
17600
+ },
17601
+ async (args) => {
17602
+ const matching = findMatchingSprints(store, args.dueDate);
17603
+ if (matching.length === 0) {
17604
+ return {
17605
+ content: [
17606
+ {
17607
+ type: "text",
17608
+ text: `No sprints found containing dueDate ${args.dueDate}.`
17609
+ }
17610
+ ]
17611
+ };
17612
+ }
17613
+ return {
17614
+ content: [
17615
+ {
17616
+ type: "text",
17617
+ text: JSON.stringify(matching, null, 2)
17618
+ }
17619
+ ]
17620
+ };
17621
+ },
17622
+ { annotations: { readOnlyHint: true } }
17185
17623
  )
17186
17624
  ];
17187
17625
  }
@@ -17209,7 +17647,7 @@ function createQuestionTools(store) {
17209
17647
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17210
17648
  };
17211
17649
  },
17212
- { annotations: { readOnly: true } }
17650
+ { annotations: { readOnlyHint: true } }
17213
17651
  ),
17214
17652
  tool15(
17215
17653
  "get_question",
@@ -17236,7 +17674,7 @@ function createQuestionTools(store) {
17236
17674
  ]
17237
17675
  };
17238
17676
  },
17239
- { annotations: { readOnly: true } }
17677
+ { annotations: { readOnlyHint: true } }
17240
17678
  ),
17241
17679
  tool15(
17242
17680
  "create_question",
@@ -17277,7 +17715,8 @@ function createQuestionTools(store) {
17277
17715
  title: external_exports.string().optional().describe("New title"),
17278
17716
  status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
17279
17717
  content: external_exports.string().optional().describe("Updated content / answer"),
17280
- owner: external_exports.string().optional().describe("New owner")
17718
+ owner: external_exports.string().optional().describe("New owner"),
17719
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
17281
17720
  },
17282
17721
  async (args) => {
17283
17722
  const { id, content, ...updates } = args;
@@ -17329,7 +17768,7 @@ function createDocumentTools(store) {
17329
17768
  ]
17330
17769
  };
17331
17770
  },
17332
- { annotations: { readOnly: true } }
17771
+ { annotations: { readOnlyHint: true } }
17333
17772
  ),
17334
17773
  tool16(
17335
17774
  "read_document",
@@ -17356,7 +17795,7 @@ function createDocumentTools(store) {
17356
17795
  ]
17357
17796
  };
17358
17797
  },
17359
- { annotations: { readOnly: true } }
17798
+ { annotations: { readOnlyHint: true } }
17360
17799
  ),
17361
17800
  tool16(
17362
17801
  "project_summary",
@@ -17384,7 +17823,7 @@ function createDocumentTools(store) {
17384
17823
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17385
17824
  };
17386
17825
  },
17387
- { annotations: { readOnly: true } }
17826
+ { annotations: { readOnlyHint: true } }
17388
17827
  )
17389
17828
  ];
17390
17829
  }
@@ -17421,7 +17860,7 @@ function createSourceTools(manifest) {
17421
17860
  ]
17422
17861
  };
17423
17862
  },
17424
- { annotations: { readOnly: true } }
17863
+ { annotations: { readOnlyHint: true } }
17425
17864
  ),
17426
17865
  tool17(
17427
17866
  "get_source_info",
@@ -17455,7 +17894,7 @@ function createSourceTools(manifest) {
17455
17894
  ]
17456
17895
  };
17457
17896
  },
17458
- { annotations: { readOnly: true } }
17897
+ { annotations: { readOnlyHint: true } }
17459
17898
  )
17460
17899
  ];
17461
17900
  }
@@ -17486,7 +17925,7 @@ function createSessionTools(store) {
17486
17925
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17487
17926
  };
17488
17927
  },
17489
- { annotations: { readOnly: true } }
17928
+ { annotations: { readOnlyHint: true } }
17490
17929
  ),
17491
17930
  tool18(
17492
17931
  "get_session",
@@ -17504,7 +17943,7 @@ function createSessionTools(store) {
17504
17943
  content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
17505
17944
  };
17506
17945
  },
17507
- { annotations: { readOnly: true } }
17946
+ { annotations: { readOnlyHint: true } }
17508
17947
  )
17509
17948
  ];
17510
17949
  }
@@ -17584,6 +18023,46 @@ function getBoardData(store, type) {
17584
18023
  }));
17585
18024
  return { columns, type, types };
17586
18025
  }
18026
+ function getDiagramData(store) {
18027
+ const allDocs = store.list();
18028
+ const sprints = [];
18029
+ const epics = [];
18030
+ const features = [];
18031
+ const statusCounts = {};
18032
+ for (const doc of allDocs) {
18033
+ const fm = doc.frontmatter;
18034
+ const status = fm.status.toLowerCase();
18035
+ statusCounts[status] = (statusCounts[status] ?? 0) + 1;
18036
+ switch (fm.type) {
18037
+ case "sprint":
18038
+ sprints.push({
18039
+ id: fm.id,
18040
+ title: fm.title,
18041
+ status: fm.status,
18042
+ startDate: fm.startDate,
18043
+ endDate: fm.endDate,
18044
+ linkedEpics: fm.linkedEpics ?? []
18045
+ });
18046
+ break;
18047
+ case "epic":
18048
+ epics.push({
18049
+ id: fm.id,
18050
+ title: fm.title,
18051
+ status: fm.status,
18052
+ linkedFeature: fm.linkedFeature
18053
+ });
18054
+ break;
18055
+ case "feature":
18056
+ features.push({
18057
+ id: fm.id,
18058
+ title: fm.title,
18059
+ status: fm.status
18060
+ });
18061
+ break;
18062
+ }
18063
+ }
18064
+ return { sprints, epics, features, statusCounts };
18065
+ }
17587
18066
 
17588
18067
  // src/web/templates/layout.ts
17589
18068
  function escapeHtml(str) {
@@ -17614,16 +18093,43 @@ function renderMarkdown(md) {
17614
18093
  const out = [];
17615
18094
  let inList = false;
17616
18095
  let listTag = "ul";
17617
- for (const raw of lines) {
17618
- const line = raw;
18096
+ let inTable = false;
18097
+ let i = 0;
18098
+ while (i < lines.length) {
18099
+ const line = lines[i];
17619
18100
  if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
17620
18101
  out.push(`</${listTag}>`);
17621
18102
  inList = false;
17622
18103
  }
18104
+ if (inTable && !/^\s*\|/.test(line)) {
18105
+ out.push("</tbody></table></div>");
18106
+ inTable = false;
18107
+ }
18108
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
18109
+ i++;
18110
+ out.push("<hr>");
18111
+ continue;
18112
+ }
18113
+ if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
18114
+ const headers = parseTableRow(line);
18115
+ out.push('<div class="table-wrap"><table><thead><tr>');
18116
+ out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
18117
+ out.push("</tr></thead><tbody>");
18118
+ inTable = true;
18119
+ i += 2;
18120
+ continue;
18121
+ }
18122
+ if (inTable && /^\s*\|/.test(line)) {
18123
+ const cells = parseTableRow(line);
18124
+ out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
18125
+ i++;
18126
+ continue;
18127
+ }
17623
18128
  const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
17624
18129
  if (headingMatch) {
17625
18130
  const level = headingMatch[1].length;
17626
18131
  out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
18132
+ i++;
17627
18133
  continue;
17628
18134
  }
17629
18135
  const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
@@ -17635,6 +18141,7 @@ function renderMarkdown(md) {
17635
18141
  listTag = "ul";
17636
18142
  }
17637
18143
  out.push(`<li>${inline(ulMatch[1])}</li>`);
18144
+ i++;
17638
18145
  continue;
17639
18146
  }
17640
18147
  const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
@@ -17646,6 +18153,7 @@ function renderMarkdown(md) {
17646
18153
  listTag = "ol";
17647
18154
  }
17648
18155
  out.push(`<li>${inline(olMatch[1])}</li>`);
18156
+ i++;
17649
18157
  continue;
17650
18158
  }
17651
18159
  if (line.trim() === "") {
@@ -17653,13 +18161,19 @@ function renderMarkdown(md) {
17653
18161
  out.push(`</${listTag}>`);
17654
18162
  inList = false;
17655
18163
  }
18164
+ i++;
17656
18165
  continue;
17657
18166
  }
17658
18167
  out.push(`<p>${inline(line)}</p>`);
18168
+ i++;
17659
18169
  }
17660
18170
  if (inList) out.push(`</${listTag}>`);
18171
+ if (inTable) out.push("</tbody></table></div>");
17661
18172
  return out.join("\n");
17662
18173
  }
18174
+ function parseTableRow(line) {
18175
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
18176
+ }
17663
18177
  function inline(text) {
17664
18178
  let s = escapeHtml(text);
17665
18179
  s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
@@ -17673,7 +18187,8 @@ function layout(opts, body) {
17673
18187
  const topItems = [
17674
18188
  { href: "/", label: "Overview" },
17675
18189
  { href: "/board", label: "Board" },
17676
- { href: "/gar", label: "GAR Report" }
18190
+ { href: "/gar", label: "GAR Report" },
18191
+ { href: "/health", label: "Health" }
17677
18192
  ];
17678
18193
  const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
17679
18194
  const groupsHtml = opts.navGroups.map((group) => {
@@ -17708,9 +18223,15 @@ function layout(opts, body) {
17708
18223
  </nav>
17709
18224
  </aside>
17710
18225
  <main class="main">
18226
+ <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18227
+ <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>
18228
+ <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>
18229
+ </button>
17711
18230
  ${body}
17712
18231
  </main>
17713
18232
  </div>
18233
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18234
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
17714
18235
  </body>
17715
18236
  </html>`;
17716
18237
  }
@@ -17825,7 +18346,33 @@ a:hover { text-decoration: underline; }
17825
18346
  flex: 1;
17826
18347
  padding: 2rem 2.5rem;
17827
18348
  max-width: 1200px;
18349
+ position: relative;
18350
+ transition: max-width 0.2s ease;
18351
+ }
18352
+ .main.expanded {
18353
+ max-width: none;
18354
+ }
18355
+ .expand-toggle {
18356
+ position: absolute;
18357
+ top: 1rem;
18358
+ right: 1rem;
18359
+ background: var(--bg-card);
18360
+ border: 1px solid var(--border);
18361
+ border-radius: var(--radius);
18362
+ color: var(--text-dim);
18363
+ cursor: pointer;
18364
+ padding: 0.4rem;
18365
+ display: flex;
18366
+ align-items: center;
18367
+ justify-content: center;
18368
+ transition: color 0.15s, border-color 0.15s;
17828
18369
  }
18370
+ .expand-toggle:hover {
18371
+ color: var(--text);
18372
+ border-color: var(--text-dim);
18373
+ }
18374
+ .main.expanded .icon-expand { display: none; }
18375
+ .main:not(.expanded) .icon-collapse { display: none; }
17829
18376
 
17830
18377
  /* Page header */
17831
18378
  .page-header {
@@ -17854,12 +18401,26 @@ a:hover { text-decoration: underline; }
17854
18401
  .breadcrumb a:hover { color: var(--accent); }
17855
18402
  .breadcrumb .sep { margin: 0 0.4rem; }
17856
18403
 
18404
+ /* Card groups */
18405
+ .card-group {
18406
+ margin-bottom: 1.5rem;
18407
+ }
18408
+
18409
+ .card-group-label {
18410
+ font-size: 0.7rem;
18411
+ text-transform: uppercase;
18412
+ letter-spacing: 0.08em;
18413
+ color: var(--text-dim);
18414
+ font-weight: 600;
18415
+ margin-bottom: 0.5rem;
18416
+ }
18417
+
17857
18418
  /* Cards grid */
17858
18419
  .cards {
17859
18420
  display: grid;
17860
18421
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
17861
18422
  gap: 1rem;
17862
- margin-bottom: 2rem;
18423
+ margin-bottom: 0.5rem;
17863
18424
  }
17864
18425
 
17865
18426
  .card {
@@ -18140,6 +18701,14 @@ tr:hover td {
18140
18701
  font-family: var(--mono);
18141
18702
  font-size: 0.85em;
18142
18703
  }
18704
+ .detail-content hr {
18705
+ border: none;
18706
+ border-top: 1px solid var(--border);
18707
+ margin: 1.25rem 0;
18708
+ }
18709
+ .detail-content .table-wrap {
18710
+ margin: 0.75rem 0;
18711
+ }
18143
18712
 
18144
18713
  /* Filters */
18145
18714
  .filters {
@@ -18184,21 +18753,206 @@ tr:hover td {
18184
18753
  .priority-high { color: var(--red); }
18185
18754
  .priority-medium { color: var(--amber); }
18186
18755
  .priority-low { color: var(--green); }
18756
+
18757
+ /* Health */
18758
+ .health-section-title {
18759
+ font-size: 1.1rem;
18760
+ font-weight: 600;
18761
+ margin: 2rem 0 1rem;
18762
+ color: var(--text);
18763
+ }
18764
+
18765
+ /* Mermaid diagrams */
18766
+ .mermaid-container {
18767
+ background: var(--bg-card);
18768
+ border: 1px solid var(--border);
18769
+ border-radius: var(--radius);
18770
+ padding: 1.5rem;
18771
+ margin: 1rem 0;
18772
+ overflow-x: auto;
18773
+ }
18774
+
18775
+ .mermaid-container .mermaid {
18776
+ display: flex;
18777
+ justify-content: center;
18778
+ }
18779
+
18780
+ .mermaid-empty {
18781
+ text-align: center;
18782
+ color: var(--text-dim);
18783
+ font-size: 0.875rem;
18784
+ }
18785
+
18786
+ .mermaid-row {
18787
+ display: grid;
18788
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
18789
+ gap: 1rem;
18790
+ }
18791
+
18792
+ .mermaid-row .mermaid-container {
18793
+ margin: 0;
18794
+ }
18187
18795
  `;
18188
18796
  }
18189
18797
 
18798
+ // src/web/templates/mermaid.ts
18799
+ function sanitize(text, maxLen = 40) {
18800
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
18801
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
18802
+ }
18803
+ function mermaidBlock(definition) {
18804
+ return `<div class="mermaid-container"><pre class="mermaid">
18805
+ ${definition}
18806
+ </pre></div>`;
18807
+ }
18808
+ function placeholder(message) {
18809
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
18810
+ }
18811
+ function buildTimelineGantt(data) {
18812
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
18813
+ if (sprintsWithDates.length === 0) {
18814
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
18815
+ }
18816
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
18817
+ const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
18818
+ for (const sprint of sprintsWithDates) {
18819
+ lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
18820
+ const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
18821
+ if (linked.length === 0) {
18822
+ lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
18823
+ } else {
18824
+ for (const epic of linked) {
18825
+ const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
18826
+ lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
18827
+ }
18828
+ }
18829
+ }
18830
+ return mermaidBlock(lines.join("\n"));
18831
+ }
18832
+ function buildArtifactFlowchart(data) {
18833
+ if (data.features.length === 0 && data.epics.length === 0) {
18834
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
18835
+ }
18836
+ const lines = ["graph TD"];
18837
+ lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
18838
+ lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
18839
+ lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
18840
+ lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
18841
+ const nodeIds = /* @__PURE__ */ new Set();
18842
+ for (const epic of data.epics) {
18843
+ if (epic.linkedFeature) {
18844
+ const feature = data.features.find((f) => f.id === epic.linkedFeature);
18845
+ if (feature) {
18846
+ const fNode = feature.id.replace(/-/g, "_");
18847
+ const eNode = epic.id.replace(/-/g, "_");
18848
+ if (!nodeIds.has(fNode)) {
18849
+ lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
18850
+ nodeIds.add(fNode);
18851
+ }
18852
+ if (!nodeIds.has(eNode)) {
18853
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18854
+ nodeIds.add(eNode);
18855
+ }
18856
+ lines.push(` ${fNode} --> ${eNode}`);
18857
+ }
18858
+ }
18859
+ }
18860
+ for (const sprint of data.sprints) {
18861
+ const sNode = sprint.id.replace(/-/g, "_");
18862
+ for (const epicId of sprint.linkedEpics) {
18863
+ const epic = data.epics.find((e) => e.id === epicId);
18864
+ if (epic) {
18865
+ const eNode = epic.id.replace(/-/g, "_");
18866
+ if (!nodeIds.has(eNode)) {
18867
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18868
+ nodeIds.add(eNode);
18869
+ }
18870
+ if (!nodeIds.has(sNode)) {
18871
+ lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
18872
+ nodeIds.add(sNode);
18873
+ }
18874
+ lines.push(` ${eNode} --> ${sNode}`);
18875
+ }
18876
+ }
18877
+ }
18878
+ if (nodeIds.size === 0) {
18879
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
18880
+ }
18881
+ const allItems = [
18882
+ ...data.features.map((f) => ({ id: f.id, status: f.status })),
18883
+ ...data.epics.map((e) => ({ id: e.id, status: e.status })),
18884
+ ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
18885
+ ];
18886
+ for (const item of allItems) {
18887
+ const node = item.id.replace(/-/g, "_");
18888
+ if (!nodeIds.has(node)) continue;
18889
+ const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
18890
+ if (cls) {
18891
+ lines.push(` class ${node} ${cls}`);
18892
+ }
18893
+ }
18894
+ return mermaidBlock(lines.join("\n"));
18895
+ }
18896
+ function buildStatusPie(title, counts) {
18897
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
18898
+ if (entries.length === 0) {
18899
+ return placeholder(`No data for ${title}.`);
18900
+ }
18901
+ const lines = [`pie title ${sanitize(title, 60)}`];
18902
+ for (const [label, count] of entries) {
18903
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
18904
+ }
18905
+ return mermaidBlock(lines.join("\n"));
18906
+ }
18907
+ function buildHealthGauge(categories) {
18908
+ const valid = categories.filter((c) => c.total > 0);
18909
+ if (valid.length === 0) {
18910
+ return placeholder("No completeness data available.");
18911
+ }
18912
+ const pies = valid.map((cat) => {
18913
+ const incomplete = cat.total - cat.complete;
18914
+ const lines = [
18915
+ `pie title ${sanitize(cat.name, 30)}`,
18916
+ ` "Complete" : ${cat.complete}`,
18917
+ ` "Incomplete" : ${incomplete}`
18918
+ ];
18919
+ return mermaidBlock(lines.join("\n"));
18920
+ });
18921
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
18922
+ }
18923
+
18190
18924
  // src/web/templates/pages/overview.ts
18191
- function overviewPage(data) {
18192
- const cards = data.types.map(
18193
- (t) => `
18925
+ function renderCard(t) {
18926
+ return `
18194
18927
  <div class="card">
18195
18928
  <a href="/docs/${t.type}">
18196
18929
  <div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
18197
18930
  <div class="card-value">${t.total}</div>
18198
18931
  ${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
18199
18932
  </a>
18200
- </div>`
18201
- ).join("\n");
18933
+ </div>`;
18934
+ }
18935
+ function overviewPage(data, diagrams, navGroups) {
18936
+ const typeMap = new Map(data.types.map((t) => [t.type, t]));
18937
+ const placed = /* @__PURE__ */ new Set();
18938
+ const groupSections = navGroups.map((group) => {
18939
+ const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
18940
+ placed.add(type);
18941
+ return renderCard(typeMap.get(type));
18942
+ });
18943
+ if (groupCards.length === 0) return "";
18944
+ return `
18945
+ <div class="card-group">
18946
+ <div class="card-group-label">${escapeHtml(group.label)}</div>
18947
+ <div class="cards">${groupCards.join("\n")}</div>
18948
+ </div>`;
18949
+ }).filter(Boolean).join("\n");
18950
+ const ungrouped = data.types.filter((t) => !placed.has(t.type));
18951
+ const ungroupedSection = ungrouped.length > 0 ? `
18952
+ <div class="card-group">
18953
+ <div class="card-group-label">Other</div>
18954
+ <div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
18955
+ </div>` : "";
18202
18956
  const rows = data.recent.map(
18203
18957
  (doc) => `
18204
18958
  <tr>
@@ -18214,9 +18968,14 @@ function overviewPage(data) {
18214
18968
  <h2>Project Overview</h2>
18215
18969
  </div>
18216
18970
 
18217
- <div class="cards">
18218
- ${cards}
18219
- </div>
18971
+ ${groupSections}
18972
+ ${ungroupedSection}
18973
+
18974
+ <div class="section-title">Project Timeline</div>
18975
+ ${buildTimelineGantt(diagrams)}
18976
+
18977
+ <div class="section-title">Artifact Relationships</div>
18978
+ ${buildArtifactFlowchart(diagrams)}
18220
18979
 
18221
18980
  <div class="section-title">Recent Activity</div>
18222
18981
  ${data.recent.length > 0 ? `
@@ -18383,6 +19142,76 @@ function garPage(report) {
18383
19142
  <div class="gar-areas">
18384
19143
  ${areaCards}
18385
19144
  </div>
19145
+
19146
+ <div class="section-title">Status Distribution</div>
19147
+ ${buildStatusPie("Action Status", {
19148
+ Open: report.metrics.scope.open,
19149
+ Done: report.metrics.scope.done,
19150
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
19151
+ })}
19152
+ `;
19153
+ }
19154
+
19155
+ // src/web/templates/pages/health.ts
19156
+ function healthPage(report, metrics) {
19157
+ const dotClass = `dot-${report.overall}`;
19158
+ function renderSection(title, categories) {
19159
+ const cards = categories.map(
19160
+ (cat) => `
19161
+ <div class="gar-area">
19162
+ <div class="area-header">
19163
+ <div class="area-dot dot-${cat.status}"></div>
19164
+ <div class="area-name">${escapeHtml(cat.name)}</div>
19165
+ </div>
19166
+ <div class="area-summary">${escapeHtml(cat.summary)}</div>
19167
+ ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
19168
+ </div>`
19169
+ ).join("\n");
19170
+ return `
19171
+ <div class="health-section-title">${escapeHtml(title)}</div>
19172
+ <div class="gar-areas">${cards}</div>
19173
+ `;
19174
+ }
19175
+ return `
19176
+ <div class="page-header">
19177
+ <h2>Governance Health Check</h2>
19178
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
19179
+ </div>
19180
+
19181
+ <div class="gar-overall">
19182
+ <div class="dot ${dotClass}"></div>
19183
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
19184
+ </div>
19185
+
19186
+ ${renderSection("Completeness", report.completeness)}
19187
+
19188
+ <div class="health-section-title">Completeness Overview</div>
19189
+ ${buildHealthGauge(
19190
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
19191
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
19192
+ complete: cat.complete,
19193
+ total: cat.total
19194
+ })) : report.completeness.map((c) => {
19195
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
19196
+ return {
19197
+ name: c.name,
19198
+ complete: match ? parseInt(match[1], 10) : 0,
19199
+ total: match ? parseInt(match[2], 10) : 0
19200
+ };
19201
+ })
19202
+ )}
19203
+
19204
+ ${renderSection("Process", report.process)}
19205
+
19206
+ <div class="health-section-title">Process Summary</div>
19207
+ ${metrics ? buildStatusPie("Process Health", {
19208
+ Stale: metrics.process.stale.length,
19209
+ "Aging Actions": metrics.process.agingActions.length,
19210
+ Healthy: Math.max(
19211
+ 0,
19212
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
19213
+ )
19214
+ }) : ""}
18386
19215
  `;
18387
19216
  }
18388
19217
 
@@ -18449,7 +19278,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
18449
19278
  }
18450
19279
  if (pathname === "/") {
18451
19280
  const data = getOverviewData(store);
18452
- const body = overviewPage(data);
19281
+ const diagrams = getDiagramData(store);
19282
+ const body = overviewPage(data, diagrams, navGroups);
18453
19283
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
18454
19284
  return;
18455
19285
  }
@@ -18459,6 +19289,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
18459
19289
  respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
18460
19290
  return;
18461
19291
  }
19292
+ if (pathname === "/health") {
19293
+ const healthMetrics = collectHealthMetrics(store);
19294
+ const report = evaluateHealth(projectName, healthMetrics);
19295
+ const body = healthPage(report, healthMetrics);
19296
+ respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
19297
+ return;
19298
+ }
18462
19299
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
18463
19300
  if (boardMatch) {
18464
19301
  const type = boardMatch[1];
@@ -18519,8 +19356,8 @@ import * as http from "http";
18519
19356
  import { exec } from "child_process";
18520
19357
 
18521
19358
  // src/skills/registry.ts
18522
- import * as fs7 from "fs";
18523
- import * as path7 from "path";
19359
+ import * as fs8 from "fs";
19360
+ import * as path8 from "path";
18524
19361
  import { fileURLToPath } from "url";
18525
19362
  import * as YAML5 from "yaml";
18526
19363
  import matter2 from "gray-matter";
@@ -18573,8 +19410,8 @@ var JiraClient = class {
18573
19410
  this.baseUrl = `https://${config2.host}/rest/api/2`;
18574
19411
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
18575
19412
  }
18576
- async request(path18, method = "GET", body) {
18577
- const url2 = `${this.baseUrl}${path18}`;
19413
+ async request(path20, method = "GET", body) {
19414
+ const url2 = `${this.baseUrl}${path20}`;
18578
19415
  const headers = {
18579
19416
  Authorization: this.authHeader,
18580
19417
  "Content-Type": "application/json",
@@ -18588,7 +19425,7 @@ var JiraClient = class {
18588
19425
  if (!response.ok) {
18589
19426
  const text = await response.text().catch(() => "");
18590
19427
  throw new Error(
18591
- `Jira API error ${response.status} ${method} ${path18}: ${text}`
19428
+ `Jira API error ${response.status} ${method} ${path20}: ${text}`
18592
19429
  );
18593
19430
  }
18594
19431
  if (response.status === 204) return void 0;
@@ -18698,7 +19535,7 @@ function createJiraTools(store) {
18698
19535
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
18699
19536
  };
18700
19537
  },
18701
- { annotations: { readOnly: true } }
19538
+ { annotations: { readOnlyHint: true } }
18702
19539
  ),
18703
19540
  tool19(
18704
19541
  "get_jira_issue",
@@ -18730,7 +19567,7 @@ function createJiraTools(store) {
18730
19567
  ]
18731
19568
  };
18732
19569
  },
18733
- { annotations: { readOnly: true } }
19570
+ { annotations: { readOnlyHint: true } }
18734
19571
  ),
18735
19572
  // --- Jira → Local tools ---
18736
19573
  tool19(
@@ -19068,13 +19905,13 @@ var GOVERNANCE_TOOL_NAMES = [
19068
19905
  ];
19069
19906
  function getBuiltinSkillsDir() {
19070
19907
  const thisFile = fileURLToPath(import.meta.url);
19071
- return path7.join(path7.dirname(thisFile), "builtin");
19908
+ return path8.join(path8.dirname(thisFile), "builtin");
19072
19909
  }
19073
19910
  function loadSkillFromDirectory(dirPath) {
19074
- const skillMdPath = path7.join(dirPath, "SKILL.md");
19075
- if (!fs7.existsSync(skillMdPath)) return void 0;
19911
+ const skillMdPath = path8.join(dirPath, "SKILL.md");
19912
+ if (!fs8.existsSync(skillMdPath)) return void 0;
19076
19913
  try {
19077
- const raw = fs7.readFileSync(skillMdPath, "utf-8");
19914
+ const raw = fs8.readFileSync(skillMdPath, "utf-8");
19078
19915
  const { data, content } = matter2(raw);
19079
19916
  if (!data.name || !data.description) return void 0;
19080
19917
  const metadata = data.metadata ?? {};
@@ -19085,13 +19922,13 @@ function loadSkillFromDirectory(dirPath) {
19085
19922
  if (wildcardPrompt) {
19086
19923
  promptFragments["*"] = wildcardPrompt;
19087
19924
  }
19088
- const personasDir = path7.join(dirPath, "personas");
19089
- if (fs7.existsSync(personasDir)) {
19925
+ const personasDir = path8.join(dirPath, "personas");
19926
+ if (fs8.existsSync(personasDir)) {
19090
19927
  try {
19091
- for (const file2 of fs7.readdirSync(personasDir)) {
19928
+ for (const file2 of fs8.readdirSync(personasDir)) {
19092
19929
  if (!file2.endsWith(".md")) continue;
19093
19930
  const personaId = file2.replace(/\.md$/, "");
19094
- const personaPrompt = fs7.readFileSync(path7.join(personasDir, file2), "utf-8").trim();
19931
+ const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
19095
19932
  if (personaPrompt) {
19096
19933
  promptFragments[personaId] = personaPrompt;
19097
19934
  }
@@ -19100,10 +19937,10 @@ function loadSkillFromDirectory(dirPath) {
19100
19937
  }
19101
19938
  }
19102
19939
  let actions;
19103
- const actionsPath = path7.join(dirPath, "actions.yaml");
19104
- if (fs7.existsSync(actionsPath)) {
19940
+ const actionsPath = path8.join(dirPath, "actions.yaml");
19941
+ if (fs8.existsSync(actionsPath)) {
19105
19942
  try {
19106
- const actionsRaw = fs7.readFileSync(actionsPath, "utf-8");
19943
+ const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
19107
19944
  actions = YAML5.parse(actionsRaw);
19108
19945
  } catch {
19109
19946
  }
@@ -19130,10 +19967,10 @@ function loadAllSkills(marvinDir) {
19130
19967
  }
19131
19968
  try {
19132
19969
  const builtinDir = getBuiltinSkillsDir();
19133
- if (fs7.existsSync(builtinDir)) {
19134
- for (const entry of fs7.readdirSync(builtinDir)) {
19135
- const entryPath = path7.join(builtinDir, entry);
19136
- if (!fs7.statSync(entryPath).isDirectory()) continue;
19970
+ if (fs8.existsSync(builtinDir)) {
19971
+ for (const entry of fs8.readdirSync(builtinDir)) {
19972
+ const entryPath = path8.join(builtinDir, entry);
19973
+ if (!fs8.statSync(entryPath).isDirectory()) continue;
19137
19974
  if (skills.has(entry)) continue;
19138
19975
  const skill = loadSkillFromDirectory(entryPath);
19139
19976
  if (skill) skills.set(skill.id, skill);
@@ -19142,18 +19979,18 @@ function loadAllSkills(marvinDir) {
19142
19979
  } catch {
19143
19980
  }
19144
19981
  if (marvinDir) {
19145
- const skillsDir = path7.join(marvinDir, "skills");
19146
- if (fs7.existsSync(skillsDir)) {
19982
+ const skillsDir = path8.join(marvinDir, "skills");
19983
+ if (fs8.existsSync(skillsDir)) {
19147
19984
  let entries;
19148
19985
  try {
19149
- entries = fs7.readdirSync(skillsDir);
19986
+ entries = fs8.readdirSync(skillsDir);
19150
19987
  } catch {
19151
19988
  entries = [];
19152
19989
  }
19153
19990
  for (const entry of entries) {
19154
- const entryPath = path7.join(skillsDir, entry);
19991
+ const entryPath = path8.join(skillsDir, entry);
19155
19992
  try {
19156
- if (fs7.statSync(entryPath).isDirectory()) {
19993
+ if (fs8.statSync(entryPath).isDirectory()) {
19157
19994
  const skill = loadSkillFromDirectory(entryPath);
19158
19995
  if (skill) skills.set(skill.id, skill);
19159
19996
  continue;
@@ -19163,7 +20000,7 @@ function loadAllSkills(marvinDir) {
19163
20000
  }
19164
20001
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
19165
20002
  try {
19166
- const raw = fs7.readFileSync(entryPath, "utf-8");
20003
+ const raw = fs8.readFileSync(entryPath, "utf-8");
19167
20004
  const parsed = YAML5.parse(raw);
19168
20005
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
19169
20006
  const skill = {
@@ -19268,12 +20105,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
19268
20105
  return agents;
19269
20106
  }
19270
20107
  function migrateYamlToSkillMd(yamlPath, outputDir) {
19271
- const raw = fs7.readFileSync(yamlPath, "utf-8");
20108
+ const raw = fs8.readFileSync(yamlPath, "utf-8");
19272
20109
  const parsed = YAML5.parse(raw);
19273
20110
  if (!parsed?.id || !parsed?.name) {
19274
20111
  throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
19275
20112
  }
19276
- fs7.mkdirSync(outputDir, { recursive: true });
20113
+ fs8.mkdirSync(outputDir, { recursive: true });
19277
20114
  const frontmatter = {
19278
20115
  name: parsed.id,
19279
20116
  description: parsed.description ?? ""
@@ -19287,15 +20124,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
19287
20124
  const skillMd = matter2.stringify(wildcardPrompt ? `
19288
20125
  ${wildcardPrompt}
19289
20126
  ` : "\n", frontmatter);
19290
- fs7.writeFileSync(path7.join(outputDir, "SKILL.md"), skillMd, "utf-8");
20127
+ fs8.writeFileSync(path8.join(outputDir, "SKILL.md"), skillMd, "utf-8");
19291
20128
  if (promptFragments) {
19292
20129
  const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
19293
20130
  if (personaKeys.length > 0) {
19294
- const personasDir = path7.join(outputDir, "personas");
19295
- fs7.mkdirSync(personasDir, { recursive: true });
20131
+ const personasDir = path8.join(outputDir, "personas");
20132
+ fs8.mkdirSync(personasDir, { recursive: true });
19296
20133
  for (const personaId of personaKeys) {
19297
- fs7.writeFileSync(
19298
- path7.join(personasDir, `${personaId}.md`),
20134
+ fs8.writeFileSync(
20135
+ path8.join(personasDir, `${personaId}.md`),
19299
20136
  `${promptFragments[personaId]}
19300
20137
  `,
19301
20138
  "utf-8"
@@ -19305,8 +20142,8 @@ ${wildcardPrompt}
19305
20142
  }
19306
20143
  const actions = parsed.actions;
19307
20144
  if (actions && actions.length > 0) {
19308
- fs7.writeFileSync(
19309
- path7.join(outputDir, "actions.yaml"),
20145
+ fs8.writeFileSync(
20146
+ path8.join(outputDir, "actions.yaml"),
19310
20147
  YAML5.stringify(actions),
19311
20148
  "utf-8"
19312
20149
  );
@@ -19461,7 +20298,7 @@ function createWebTools(store, projectName, navGroups) {
19461
20298
  content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
19462
20299
  };
19463
20300
  },
19464
- { annotations: { readOnly: true } }
20301
+ { annotations: { readOnlyHint: true } }
19465
20302
  ),
19466
20303
  tool20(
19467
20304
  "get_dashboard_overview",
@@ -19483,7 +20320,7 @@ function createWebTools(store, projectName, navGroups) {
19483
20320
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19484
20321
  };
19485
20322
  },
19486
- { annotations: { readOnly: true } }
20323
+ { annotations: { readOnlyHint: true } }
19487
20324
  ),
19488
20325
  tool20(
19489
20326
  "get_dashboard_gar",
@@ -19495,7 +20332,7 @@ function createWebTools(store, projectName, navGroups) {
19495
20332
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
19496
20333
  };
19497
20334
  },
19498
- { annotations: { readOnly: true } }
20335
+ { annotations: { readOnlyHint: true } }
19499
20336
  ),
19500
20337
  tool20(
19501
20338
  "get_dashboard_board",
@@ -19523,7 +20360,7 @@ function createWebTools(store, projectName, navGroups) {
19523
20360
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19524
20361
  };
19525
20362
  },
19526
- { annotations: { readOnly: true } }
20363
+ { annotations: { readOnlyHint: true } }
19527
20364
  )
19528
20365
  ];
19529
20366
  }
@@ -19580,8 +20417,8 @@ function slugify3(text) {
19580
20417
  }
19581
20418
 
19582
20419
  // src/sources/manifest.ts
19583
- import * as fs8 from "fs";
19584
- import * as path8 from "path";
20420
+ import * as fs9 from "fs";
20421
+ import * as path9 from "path";
19585
20422
  import * as crypto from "crypto";
19586
20423
  import * as YAML6 from "yaml";
19587
20424
  var MANIFEST_FILE = ".manifest.yaml";
@@ -19594,37 +20431,37 @@ var SourceManifestManager = class {
19594
20431
  manifestPath;
19595
20432
  sourcesDir;
19596
20433
  constructor(marvinDir) {
19597
- this.sourcesDir = path8.join(marvinDir, "sources");
19598
- this.manifestPath = path8.join(this.sourcesDir, MANIFEST_FILE);
20434
+ this.sourcesDir = path9.join(marvinDir, "sources");
20435
+ this.manifestPath = path9.join(this.sourcesDir, MANIFEST_FILE);
19599
20436
  this.manifest = this.load();
19600
20437
  }
19601
20438
  load() {
19602
- if (!fs8.existsSync(this.manifestPath)) {
20439
+ if (!fs9.existsSync(this.manifestPath)) {
19603
20440
  return emptyManifest();
19604
20441
  }
19605
- const raw = fs8.readFileSync(this.manifestPath, "utf-8");
20442
+ const raw = fs9.readFileSync(this.manifestPath, "utf-8");
19606
20443
  const parsed = YAML6.parse(raw);
19607
20444
  return parsed ?? emptyManifest();
19608
20445
  }
19609
20446
  save() {
19610
- fs8.mkdirSync(this.sourcesDir, { recursive: true });
19611
- fs8.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
20447
+ fs9.mkdirSync(this.sourcesDir, { recursive: true });
20448
+ fs9.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
19612
20449
  }
19613
20450
  scan() {
19614
20451
  const added = [];
19615
20452
  const changed = [];
19616
20453
  const removed = [];
19617
- if (!fs8.existsSync(this.sourcesDir)) {
20454
+ if (!fs9.existsSync(this.sourcesDir)) {
19618
20455
  return { added, changed, removed };
19619
20456
  }
19620
20457
  const onDisk = new Set(
19621
- fs8.readdirSync(this.sourcesDir).filter((f) => {
19622
- const ext = path8.extname(f).toLowerCase();
20458
+ fs9.readdirSync(this.sourcesDir).filter((f) => {
20459
+ const ext = path9.extname(f).toLowerCase();
19623
20460
  return SOURCE_EXTENSIONS.includes(ext);
19624
20461
  })
19625
20462
  );
19626
20463
  for (const fileName of onDisk) {
19627
- const filePath = path8.join(this.sourcesDir, fileName);
20464
+ const filePath = path9.join(this.sourcesDir, fileName);
19628
20465
  const hash2 = this.hashFile(filePath);
19629
20466
  const existing = this.manifest.files[fileName];
19630
20467
  if (!existing) {
@@ -19687,7 +20524,7 @@ var SourceManifestManager = class {
19687
20524
  this.save();
19688
20525
  }
19689
20526
  hashFile(filePath) {
19690
- const content = fs8.readFileSync(filePath);
20527
+ const content = fs9.readFileSync(filePath);
19691
20528
  return crypto.createHash("sha256").update(content).digest("hex");
19692
20529
  }
19693
20530
  };
@@ -19702,8 +20539,8 @@ async function startSession(options) {
19702
20539
  const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
19703
20540
  const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
19704
20541
  const sessionStore = new SessionStore(marvinDir);
19705
- const sourcesDir = path9.join(marvinDir, "sources");
19706
- const hasSourcesDir = fs9.existsSync(sourcesDir);
20542
+ const sourcesDir = path10.join(marvinDir, "sources");
20543
+ const hasSourcesDir = fs10.existsSync(sourcesDir);
19707
20544
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
19708
20545
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
19709
20546
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
@@ -19726,7 +20563,7 @@ async function startSession(options) {
19726
20563
  projectName: config2.project.name,
19727
20564
  navGroups
19728
20565
  });
19729
- const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment);
20566
+ const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment, marvinDir);
19730
20567
  let existingSession;
19731
20568
  if (options.sessionName) {
19732
20569
  existingSession = sessionStore.get(options.sessionName);
@@ -20179,13 +21016,13 @@ async function setApiKey() {
20179
21016
  }
20180
21017
 
20181
21018
  // src/cli/commands/ingest.ts
20182
- import * as fs11 from "fs";
20183
- import * as path11 from "path";
21019
+ import * as fs12 from "fs";
21020
+ import * as path12 from "path";
20184
21021
  import chalk8 from "chalk";
20185
21022
 
20186
21023
  // src/sources/ingest.ts
20187
- import * as fs10 from "fs";
20188
- import * as path10 from "path";
21024
+ import * as fs11 from "fs";
21025
+ import * as path11 from "path";
20189
21026
  import chalk7 from "chalk";
20190
21027
  import ora2 from "ora";
20191
21028
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
@@ -20288,15 +21125,15 @@ async function ingestFile(options) {
20288
21125
  const persona = getPersona(personaId);
20289
21126
  const manifest = new SourceManifestManager(marvinDir);
20290
21127
  const sourcesDir = manifest.sourcesDir;
20291
- const filePath = path10.join(sourcesDir, fileName);
20292
- if (!fs10.existsSync(filePath)) {
21128
+ const filePath = path11.join(sourcesDir, fileName);
21129
+ if (!fs11.existsSync(filePath)) {
20293
21130
  throw new Error(`Source file not found: ${filePath}`);
20294
21131
  }
20295
- const ext = path10.extname(fileName).toLowerCase();
21132
+ const ext = path11.extname(fileName).toLowerCase();
20296
21133
  const isPdf = ext === ".pdf";
20297
21134
  let fileContent = null;
20298
21135
  if (!isPdf) {
20299
- fileContent = fs10.readFileSync(filePath, "utf-8");
21136
+ fileContent = fs11.readFileSync(filePath, "utf-8");
20300
21137
  }
20301
21138
  const store = new DocumentStore(marvinDir);
20302
21139
  const createdArtifacts = [];
@@ -20399,9 +21236,9 @@ Ingest ended with error: ${message.subtype}`)
20399
21236
  async function ingestCommand(file2, options) {
20400
21237
  const project = loadProject();
20401
21238
  const marvinDir = project.marvinDir;
20402
- const sourcesDir = path11.join(marvinDir, "sources");
20403
- if (!fs11.existsSync(sourcesDir)) {
20404
- fs11.mkdirSync(sourcesDir, { recursive: true });
21239
+ const sourcesDir = path12.join(marvinDir, "sources");
21240
+ if (!fs12.existsSync(sourcesDir)) {
21241
+ fs12.mkdirSync(sourcesDir, { recursive: true });
20405
21242
  }
20406
21243
  const manifest = new SourceManifestManager(marvinDir);
20407
21244
  manifest.scan();
@@ -20412,8 +21249,8 @@ async function ingestCommand(file2, options) {
20412
21249
  return;
20413
21250
  }
20414
21251
  if (file2) {
20415
- const filePath = path11.join(sourcesDir, file2);
20416
- if (!fs11.existsSync(filePath)) {
21252
+ const filePath = path12.join(sourcesDir, file2);
21253
+ if (!fs12.existsSync(filePath)) {
20417
21254
  console.log(chalk8.red(`Source file not found: ${file2}`));
20418
21255
  console.log(chalk8.dim(`Expected at: ${filePath}`));
20419
21256
  console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
@@ -20480,7 +21317,7 @@ import ora3 from "ora";
20480
21317
  import { input as input3 } from "@inquirer/prompts";
20481
21318
 
20482
21319
  // src/git/repository.ts
20483
- import * as path12 from "path";
21320
+ import * as path13 from "path";
20484
21321
  import simpleGit from "simple-git";
20485
21322
  var MARVIN_GITIGNORE = `node_modules/
20486
21323
  .DS_Store
@@ -20500,7 +21337,7 @@ var DIR_TYPE_LABELS = {
20500
21337
  function buildCommitMessage(files) {
20501
21338
  const counts = /* @__PURE__ */ new Map();
20502
21339
  for (const f of files) {
20503
- const parts2 = f.split(path12.sep).join("/").split("/");
21340
+ const parts2 = f.split(path13.sep).join("/").split("/");
20504
21341
  const docsIdx = parts2.indexOf("docs");
20505
21342
  if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
20506
21343
  const dirName = parts2[docsIdx + 1];
@@ -20540,9 +21377,9 @@ var MarvinGit = class {
20540
21377
  );
20541
21378
  }
20542
21379
  await this.git.init();
20543
- const { writeFileSync: writeFileSync9 } = await import("fs");
20544
- writeFileSync9(
20545
- path12.join(this.marvinDir, ".gitignore"),
21380
+ const { writeFileSync: writeFileSync10 } = await import("fs");
21381
+ writeFileSync10(
21382
+ path13.join(this.marvinDir, ".gitignore"),
20546
21383
  MARVIN_GITIGNORE,
20547
21384
  "utf-8"
20548
21385
  );
@@ -20662,9 +21499,9 @@ var MarvinGit = class {
20662
21499
  }
20663
21500
  }
20664
21501
  static async clone(url2, targetDir) {
20665
- const marvinDir = path12.join(targetDir, ".marvin");
20666
- const { existsSync: existsSync16 } = await import("fs");
20667
- if (existsSync16(marvinDir)) {
21502
+ const marvinDir = path13.join(targetDir, ".marvin");
21503
+ const { existsSync: existsSync17 } = await import("fs");
21504
+ if (existsSync17(marvinDir)) {
20668
21505
  throw new GitSyncError(
20669
21506
  `.marvin/ already exists at ${targetDir}. Remove it first or choose a different directory.`
20670
21507
  );
@@ -20842,8 +21679,8 @@ async function cloneCommand(url2, directory) {
20842
21679
  }
20843
21680
 
20844
21681
  // src/mcp/stdio-server.ts
20845
- import * as fs12 from "fs";
20846
- import * as path13 from "path";
21682
+ import * as fs13 from "fs";
21683
+ import * as path14 from "path";
20847
21684
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20848
21685
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20849
21686
 
@@ -21074,7 +21911,7 @@ ${summaries}`
21074
21911
  content: [{ type: "text", text: guidance }]
21075
21912
  };
21076
21913
  },
21077
- { annotations: { readOnly: true } }
21914
+ { annotations: { readOnlyHint: true } }
21078
21915
  )
21079
21916
  ];
21080
21917
  }
@@ -21141,8 +21978,8 @@ function collectTools(marvinDir) {
21141
21978
  const plugin = resolvePlugin(config2.methodology);
21142
21979
  const registrations = plugin?.documentTypeRegistrations ?? [];
21143
21980
  const store = new DocumentStore(marvinDir, registrations);
21144
- const sourcesDir = path13.join(marvinDir, "sources");
21145
- const hasSourcesDir = fs12.existsSync(sourcesDir);
21981
+ const sourcesDir = path14.join(marvinDir, "sources");
21982
+ const hasSourcesDir = fs13.existsSync(sourcesDir);
21146
21983
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
21147
21984
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
21148
21985
  const sessionStore = new SessionStore(marvinDir);
@@ -21150,7 +21987,7 @@ function collectTools(marvinDir) {
21150
21987
  const allSkillIds = [...allSkills.keys()];
21151
21988
  const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
21152
21989
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
21153
- const projectRoot = path13.dirname(marvinDir);
21990
+ const projectRoot = path14.dirname(marvinDir);
21154
21991
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
21155
21992
  return [
21156
21993
  ...createDecisionTools(store),
@@ -21213,8 +22050,8 @@ async function serveCommand() {
21213
22050
  }
21214
22051
 
21215
22052
  // src/cli/commands/skills.ts
21216
- import * as fs13 from "fs";
21217
- import * as path14 from "path";
22053
+ import * as fs14 from "fs";
22054
+ import * as path15 from "path";
21218
22055
  import * as YAML7 from "yaml";
21219
22056
  import matter3 from "gray-matter";
21220
22057
  import chalk10 from "chalk";
@@ -21320,14 +22157,14 @@ async function skillsRemoveCommand(skillId, options) {
21320
22157
  }
21321
22158
  async function skillsCreateCommand(name) {
21322
22159
  const project = loadProject();
21323
- const skillsDir = path14.join(project.marvinDir, "skills");
21324
- fs13.mkdirSync(skillsDir, { recursive: true });
21325
- const skillDir = path14.join(skillsDir, name);
21326
- if (fs13.existsSync(skillDir)) {
22160
+ const skillsDir = path15.join(project.marvinDir, "skills");
22161
+ fs14.mkdirSync(skillsDir, { recursive: true });
22162
+ const skillDir = path15.join(skillsDir, name);
22163
+ if (fs14.existsSync(skillDir)) {
21327
22164
  console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
21328
22165
  return;
21329
22166
  }
21330
- fs13.mkdirSync(skillDir, { recursive: true });
22167
+ fs14.mkdirSync(skillDir, { recursive: true });
21331
22168
  const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
21332
22169
  const frontmatter = {
21333
22170
  name,
@@ -21341,7 +22178,7 @@ async function skillsCreateCommand(name) {
21341
22178
  You have the **${displayName}** skill.
21342
22179
  `;
21343
22180
  const skillMd = matter3.stringify(body, frontmatter);
21344
- fs13.writeFileSync(path14.join(skillDir, "SKILL.md"), skillMd, "utf-8");
22181
+ fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
21345
22182
  const actions = [
21346
22183
  {
21347
22184
  id: "run",
@@ -21351,7 +22188,7 @@ You have the **${displayName}** skill.
21351
22188
  maxTurns: 5
21352
22189
  }
21353
22190
  ];
21354
- fs13.writeFileSync(path14.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
22191
+ fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
21355
22192
  console.log(chalk10.green(`Created skill: ${skillDir}/`));
21356
22193
  console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
21357
22194
  console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
@@ -21359,14 +22196,14 @@ You have the **${displayName}** skill.
21359
22196
  }
21360
22197
  async function skillsMigrateCommand() {
21361
22198
  const project = loadProject();
21362
- const skillsDir = path14.join(project.marvinDir, "skills");
21363
- if (!fs13.existsSync(skillsDir)) {
22199
+ const skillsDir = path15.join(project.marvinDir, "skills");
22200
+ if (!fs14.existsSync(skillsDir)) {
21364
22201
  console.log(chalk10.dim("No skills directory found."));
21365
22202
  return;
21366
22203
  }
21367
22204
  let entries;
21368
22205
  try {
21369
- entries = fs13.readdirSync(skillsDir);
22206
+ entries = fs14.readdirSync(skillsDir);
21370
22207
  } catch {
21371
22208
  console.log(chalk10.red("Could not read skills directory."));
21372
22209
  return;
@@ -21378,16 +22215,16 @@ async function skillsMigrateCommand() {
21378
22215
  }
21379
22216
  let migrated = 0;
21380
22217
  for (const file2 of yamlFiles) {
21381
- const yamlPath = path14.join(skillsDir, file2);
22218
+ const yamlPath = path15.join(skillsDir, file2);
21382
22219
  const baseName = file2.replace(/\.(yaml|yml)$/, "");
21383
- const outputDir = path14.join(skillsDir, baseName);
21384
- if (fs13.existsSync(outputDir)) {
22220
+ const outputDir = path15.join(skillsDir, baseName);
22221
+ if (fs14.existsSync(outputDir)) {
21385
22222
  console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
21386
22223
  continue;
21387
22224
  }
21388
22225
  try {
21389
22226
  migrateYamlToSkillMd(yamlPath, outputDir);
21390
- fs13.renameSync(yamlPath, `${yamlPath}.bak`);
22227
+ fs14.renameSync(yamlPath, `${yamlPath}.bak`);
21391
22228
  console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
21392
22229
  migrated++;
21393
22230
  } catch (err) {
@@ -21401,35 +22238,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
21401
22238
  }
21402
22239
 
21403
22240
  // src/cli/commands/import.ts
21404
- import * as fs16 from "fs";
21405
- import * as path17 from "path";
22241
+ import * as fs17 from "fs";
22242
+ import * as path18 from "path";
21406
22243
  import chalk11 from "chalk";
21407
22244
 
21408
22245
  // src/import/engine.ts
21409
- import * as fs15 from "fs";
21410
- import * as path16 from "path";
22246
+ import * as fs16 from "fs";
22247
+ import * as path17 from "path";
21411
22248
  import matter5 from "gray-matter";
21412
22249
 
21413
22250
  // src/import/classifier.ts
21414
- import * as fs14 from "fs";
21415
- import * as path15 from "path";
22251
+ import * as fs15 from "fs";
22252
+ import * as path16 from "path";
21416
22253
  import matter4 from "gray-matter";
21417
22254
  var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
21418
22255
  var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
21419
22256
  var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
21420
22257
  function classifyPath(inputPath, knownTypes, knownDirNames) {
21421
- const resolved = path15.resolve(inputPath);
21422
- const stat = fs14.statSync(resolved);
22258
+ const resolved = path16.resolve(inputPath);
22259
+ const stat = fs15.statSync(resolved);
21423
22260
  if (!stat.isDirectory()) {
21424
22261
  return classifyFile(resolved, knownTypes);
21425
22262
  }
21426
- if (path15.basename(resolved) === ".marvin" || fs14.existsSync(path15.join(resolved, "config.yaml"))) {
22263
+ if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
21427
22264
  return { type: "marvin-project", inputPath: resolved };
21428
22265
  }
21429
22266
  const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
21430
- const entries = fs14.readdirSync(resolved);
22267
+ const entries = fs15.readdirSync(resolved);
21431
22268
  const hasDocSubdirs = entries.some(
21432
- (e) => allDirNames.has(e) && fs14.statSync(path15.join(resolved, e)).isDirectory()
22269
+ (e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
21433
22270
  );
21434
22271
  if (hasDocSubdirs) {
21435
22272
  return { type: "docs-directory", inputPath: resolved };
@@ -21438,7 +22275,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
21438
22275
  if (mdFiles.length > 0) {
21439
22276
  const hasMarvinDocs = mdFiles.some((f) => {
21440
22277
  try {
21441
- const raw = fs14.readFileSync(path15.join(resolved, f), "utf-8");
22278
+ const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
21442
22279
  const { data } = matter4(raw);
21443
22280
  return isValidMarvinDocument(data, knownTypes);
21444
22281
  } catch {
@@ -21452,14 +22289,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
21452
22289
  return { type: "raw-source-dir", inputPath: resolved };
21453
22290
  }
21454
22291
  function classifyFile(filePath, knownTypes) {
21455
- const resolved = path15.resolve(filePath);
21456
- const ext = path15.extname(resolved).toLowerCase();
22292
+ const resolved = path16.resolve(filePath);
22293
+ const ext = path16.extname(resolved).toLowerCase();
21457
22294
  if (RAW_SOURCE_EXTENSIONS.has(ext)) {
21458
22295
  return { type: "raw-source-file", inputPath: resolved };
21459
22296
  }
21460
22297
  if (ext === ".md") {
21461
22298
  try {
21462
- const raw = fs14.readFileSync(resolved, "utf-8");
22299
+ const raw = fs15.readFileSync(resolved, "utf-8");
21463
22300
  const { data } = matter4(raw);
21464
22301
  if (isValidMarvinDocument(data, knownTypes)) {
21465
22302
  return { type: "marvin-document", inputPath: resolved };
@@ -21584,9 +22421,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
21584
22421
  continue;
21585
22422
  }
21586
22423
  if (item.action === "copy") {
21587
- const targetDir = path16.dirname(item.targetPath);
21588
- fs15.mkdirSync(targetDir, { recursive: true });
21589
- fs15.copyFileSync(item.sourcePath, item.targetPath);
22424
+ const targetDir = path17.dirname(item.targetPath);
22425
+ fs16.mkdirSync(targetDir, { recursive: true });
22426
+ fs16.copyFileSync(item.sourcePath, item.targetPath);
21590
22427
  copied++;
21591
22428
  continue;
21592
22429
  }
@@ -21622,19 +22459,19 @@ function formatPlanSummary(plan) {
21622
22459
  lines.push(`Documents to import: ${imports.length}`);
21623
22460
  for (const item of imports) {
21624
22461
  const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
21625
- lines.push(` ${idInfo} ${path16.basename(item.sourcePath)}`);
22462
+ lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
21626
22463
  }
21627
22464
  }
21628
22465
  if (copies.length > 0) {
21629
22466
  lines.push(`Files to copy to sources/: ${copies.length}`);
21630
22467
  for (const item of copies) {
21631
- lines.push(` ${path16.basename(item.sourcePath)} \u2192 ${path16.basename(item.targetPath)}`);
22468
+ lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
21632
22469
  }
21633
22470
  }
21634
22471
  if (skips.length > 0) {
21635
22472
  lines.push(`Skipped (conflict): ${skips.length}`);
21636
22473
  for (const item of skips) {
21637
- lines.push(` ${item.originalId ?? path16.basename(item.sourcePath)} ${item.reason ?? ""}`);
22474
+ lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
21638
22475
  }
21639
22476
  }
21640
22477
  if (plan.items.length === 0) {
@@ -21667,11 +22504,11 @@ function getDirNameForType(store, type) {
21667
22504
  }
21668
22505
  function collectMarvinDocs(dir, knownTypes) {
21669
22506
  const docs = [];
21670
- const files = fs15.readdirSync(dir).filter((f) => f.endsWith(".md"));
22507
+ const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
21671
22508
  for (const file2 of files) {
21672
- const filePath = path16.join(dir, file2);
22509
+ const filePath = path17.join(dir, file2);
21673
22510
  try {
21674
- const raw = fs15.readFileSync(filePath, "utf-8");
22511
+ const raw = fs16.readFileSync(filePath, "utf-8");
21675
22512
  const { data, content } = matter5(raw);
21676
22513
  if (isValidMarvinDocument(data, knownTypes)) {
21677
22514
  docs.push({
@@ -21727,23 +22564,23 @@ function planDocImports(docs, store, options) {
21727
22564
  }
21728
22565
  function planFromMarvinProject(classification, store, _marvinDir, options) {
21729
22566
  let projectDir = classification.inputPath;
21730
- if (path16.basename(projectDir) !== ".marvin") {
21731
- const inner = path16.join(projectDir, ".marvin");
21732
- if (fs15.existsSync(inner)) {
22567
+ if (path17.basename(projectDir) !== ".marvin") {
22568
+ const inner = path17.join(projectDir, ".marvin");
22569
+ if (fs16.existsSync(inner)) {
21733
22570
  projectDir = inner;
21734
22571
  }
21735
22572
  }
21736
- const docsDir = path16.join(projectDir, "docs");
21737
- if (!fs15.existsSync(docsDir)) {
22573
+ const docsDir = path17.join(projectDir, "docs");
22574
+ if (!fs16.existsSync(docsDir)) {
21738
22575
  return [];
21739
22576
  }
21740
22577
  const knownTypes = store.registeredTypes;
21741
22578
  const allDocs = [];
21742
- const subdirs = fs15.readdirSync(docsDir).filter(
21743
- (d) => fs15.statSync(path16.join(docsDir, d)).isDirectory()
22579
+ const subdirs = fs16.readdirSync(docsDir).filter(
22580
+ (d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
21744
22581
  );
21745
22582
  for (const subdir of subdirs) {
21746
- const docs = collectMarvinDocs(path16.join(docsDir, subdir), knownTypes);
22583
+ const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
21747
22584
  allDocs.push(...docs);
21748
22585
  }
21749
22586
  return planDocImports(allDocs, store, options);
@@ -21753,10 +22590,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
21753
22590
  const knownTypes = store.registeredTypes;
21754
22591
  const allDocs = [];
21755
22592
  allDocs.push(...collectMarvinDocs(dir, knownTypes));
21756
- const entries = fs15.readdirSync(dir);
22593
+ const entries = fs16.readdirSync(dir);
21757
22594
  for (const entry of entries) {
21758
- const entryPath = path16.join(dir, entry);
21759
- if (fs15.statSync(entryPath).isDirectory()) {
22595
+ const entryPath = path17.join(dir, entry);
22596
+ if (fs16.statSync(entryPath).isDirectory()) {
21760
22597
  allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
21761
22598
  }
21762
22599
  }
@@ -21765,7 +22602,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
21765
22602
  function planFromSingleDocument(classification, store, _marvinDir, options) {
21766
22603
  const filePath = classification.inputPath;
21767
22604
  const knownTypes = store.registeredTypes;
21768
- const raw = fs15.readFileSync(filePath, "utf-8");
22605
+ const raw = fs16.readFileSync(filePath, "utf-8");
21769
22606
  const { data, content } = matter5(raw);
21770
22607
  if (!isValidMarvinDocument(data, knownTypes)) {
21771
22608
  return [];
@@ -21781,14 +22618,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
21781
22618
  }
21782
22619
  function planFromRawSourceDir(classification, marvinDir) {
21783
22620
  const dir = classification.inputPath;
21784
- const sourcesDir = path16.join(marvinDir, "sources");
22621
+ const sourcesDir = path17.join(marvinDir, "sources");
21785
22622
  const items = [];
21786
- const files = fs15.readdirSync(dir).filter((f) => {
21787
- const stat = fs15.statSync(path16.join(dir, f));
22623
+ const files = fs16.readdirSync(dir).filter((f) => {
22624
+ const stat = fs16.statSync(path17.join(dir, f));
21788
22625
  return stat.isFile();
21789
22626
  });
21790
22627
  for (const file2 of files) {
21791
- const sourcePath = path16.join(dir, file2);
22628
+ const sourcePath = path17.join(dir, file2);
21792
22629
  const targetPath = resolveSourceFileName(sourcesDir, file2);
21793
22630
  items.push({
21794
22631
  action: "copy",
@@ -21799,8 +22636,8 @@ function planFromRawSourceDir(classification, marvinDir) {
21799
22636
  return items;
21800
22637
  }
21801
22638
  function planFromRawSourceFile(classification, marvinDir) {
21802
- const sourcesDir = path16.join(marvinDir, "sources");
21803
- const fileName = path16.basename(classification.inputPath);
22639
+ const sourcesDir = path17.join(marvinDir, "sources");
22640
+ const fileName = path17.basename(classification.inputPath);
21804
22641
  const targetPath = resolveSourceFileName(sourcesDir, fileName);
21805
22642
  return [
21806
22643
  {
@@ -21811,25 +22648,25 @@ function planFromRawSourceFile(classification, marvinDir) {
21811
22648
  ];
21812
22649
  }
21813
22650
  function resolveSourceFileName(sourcesDir, fileName) {
21814
- const targetPath = path16.join(sourcesDir, fileName);
21815
- if (!fs15.existsSync(targetPath)) {
22651
+ const targetPath = path17.join(sourcesDir, fileName);
22652
+ if (!fs16.existsSync(targetPath)) {
21816
22653
  return targetPath;
21817
22654
  }
21818
- const ext = path16.extname(fileName);
21819
- const base = path16.basename(fileName, ext);
22655
+ const ext = path17.extname(fileName);
22656
+ const base = path17.basename(fileName, ext);
21820
22657
  let counter = 1;
21821
22658
  let candidate;
21822
22659
  do {
21823
- candidate = path16.join(sourcesDir, `${base}-${counter}${ext}`);
22660
+ candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
21824
22661
  counter++;
21825
- } while (fs15.existsSync(candidate));
22662
+ } while (fs16.existsSync(candidate));
21826
22663
  return candidate;
21827
22664
  }
21828
22665
 
21829
22666
  // src/cli/commands/import.ts
21830
22667
  async function importCommand(inputPath, options) {
21831
- const resolved = path17.resolve(inputPath);
21832
- if (!fs16.existsSync(resolved)) {
22668
+ const resolved = path18.resolve(inputPath);
22669
+ if (!fs17.existsSync(resolved)) {
21833
22670
  throw new ImportError(`Path not found: ${resolved}`);
21834
22671
  }
21835
22672
  const project = loadProject();
@@ -21881,7 +22718,7 @@ async function importCommand(inputPath, options) {
21881
22718
  console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
21882
22719
  const manifest = new SourceManifestManager(marvinDir);
21883
22720
  manifest.scan();
21884
- const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path17.basename(i.targetPath));
22721
+ const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
21885
22722
  for (const fileName of copiedFileNames) {
21886
22723
  try {
21887
22724
  await ingestFile({
@@ -22284,7 +23121,8 @@ The contributor is identifying a project risk.
22284
23121
  - Create actions for risk mitigation tasks
22285
23122
  - Create decisions for risk response strategies
22286
23123
  - Create questions for risks needing further assessment
22287
- - Tag all related artifacts with "risk" for tracking`,
23124
+ - Tag all related artifacts with "risk" for tracking
23125
+ - When a risk is resolved, use the update tool to remove the "risk" tag and add "risk-mitigated" so it no longer inflates the GAR quality metric`,
22288
23126
  "blocker-report": `
22289
23127
  ### Type-Specific Guidance: Blocker Report
22290
23128
  The contributor is reporting a blocker.
@@ -22629,6 +23467,105 @@ function renderConfluence(report) {
22629
23467
  return lines.join("\n");
22630
23468
  }
22631
23469
 
23470
+ // src/reports/health/render-ascii.ts
23471
+ import chalk17 from "chalk";
23472
+ var STATUS_DOT2 = {
23473
+ green: chalk17.green("\u25CF"),
23474
+ amber: chalk17.yellow("\u25CF"),
23475
+ red: chalk17.red("\u25CF")
23476
+ };
23477
+ var STATUS_LABEL2 = {
23478
+ green: chalk17.green.bold("GREEN"),
23479
+ amber: chalk17.yellow.bold("AMBER"),
23480
+ red: chalk17.red.bold("RED")
23481
+ };
23482
+ var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
23483
+ function renderAscii2(report) {
23484
+ const lines = [];
23485
+ lines.push("");
23486
+ lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
23487
+ lines.push(chalk17.dim(` ${report.generatedAt}`));
23488
+ lines.push("");
23489
+ lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
23490
+ lines.push("");
23491
+ lines.push(` ${SEPARATOR2}`);
23492
+ lines.push(chalk17.bold(" Completeness"));
23493
+ lines.push(` ${SEPARATOR2}`);
23494
+ for (const cat of report.completeness) {
23495
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
23496
+ for (const item of cat.items) {
23497
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23498
+ }
23499
+ }
23500
+ lines.push("");
23501
+ lines.push(` ${SEPARATOR2}`);
23502
+ lines.push(chalk17.bold(" Process"));
23503
+ lines.push(` ${SEPARATOR2}`);
23504
+ for (const cat of report.process) {
23505
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
23506
+ for (const item of cat.items) {
23507
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23508
+ }
23509
+ }
23510
+ lines.push(` ${SEPARATOR2}`);
23511
+ lines.push("");
23512
+ return lines.join("\n");
23513
+ }
23514
+
23515
+ // src/reports/health/render-confluence.ts
23516
+ var EMOJI2 = {
23517
+ green: ":green_circle:",
23518
+ amber: ":yellow_circle:",
23519
+ red: ":red_circle:"
23520
+ };
23521
+ function renderConfluence2(report) {
23522
+ const lines = [];
23523
+ lines.push(`# Health Check \u2014 ${report.projectName}`);
23524
+ lines.push("");
23525
+ lines.push(`**Date:** ${report.generatedAt}`);
23526
+ lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
23527
+ lines.push("");
23528
+ lines.push("## Completeness");
23529
+ lines.push("");
23530
+ lines.push("| Category | Status | Summary |");
23531
+ lines.push("|----------|--------|---------|");
23532
+ for (const cat of report.completeness) {
23533
+ lines.push(
23534
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23535
+ );
23536
+ }
23537
+ lines.push("");
23538
+ for (const cat of report.completeness) {
23539
+ if (cat.items.length === 0) continue;
23540
+ lines.push(`### ${cat.name}`);
23541
+ lines.push("");
23542
+ for (const item of cat.items) {
23543
+ lines.push(`- **${item.id}** ${item.detail}`);
23544
+ }
23545
+ lines.push("");
23546
+ }
23547
+ lines.push("## Process");
23548
+ lines.push("");
23549
+ lines.push("| Metric | Status | Summary |");
23550
+ lines.push("|--------|--------|---------|");
23551
+ for (const cat of report.process) {
23552
+ lines.push(
23553
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23554
+ );
23555
+ }
23556
+ lines.push("");
23557
+ for (const cat of report.process) {
23558
+ if (cat.items.length === 0) continue;
23559
+ lines.push(`### ${cat.name}`);
23560
+ lines.push("");
23561
+ for (const item of cat.items) {
23562
+ lines.push(`- **${item.id}** ${item.detail}`);
23563
+ }
23564
+ lines.push("");
23565
+ }
23566
+ return lines.join("\n");
23567
+ }
23568
+
22632
23569
  // src/cli/commands/report.ts
22633
23570
  async function garReportCommand(options) {
22634
23571
  const project = loadProject();
@@ -22644,6 +23581,20 @@ async function garReportCommand(options) {
22644
23581
  console.log(renderAscii(report));
22645
23582
  }
22646
23583
  }
23584
+ async function healthReportCommand(options) {
23585
+ const project = loadProject();
23586
+ const plugin = resolvePlugin(project.config.methodology);
23587
+ const registrations = plugin?.documentTypeRegistrations ?? [];
23588
+ const store = new DocumentStore(project.marvinDir, registrations);
23589
+ const metrics = collectHealthMetrics(store);
23590
+ const report = evaluateHealth(project.config.name, metrics);
23591
+ const format = options.format ?? "ascii";
23592
+ if (format === "confluence") {
23593
+ console.log(renderConfluence2(report));
23594
+ } else {
23595
+ console.log(renderAscii2(report));
23596
+ }
23597
+ }
22647
23598
 
22648
23599
  // src/cli/commands/web.ts
22649
23600
  async function webCommand(options) {
@@ -22655,12 +23606,38 @@ async function webCommand(options) {
22655
23606
  await startWebServer({ port, open: options.open });
22656
23607
  }
22657
23608
 
23609
+ // src/cli/commands/generate.ts
23610
+ import * as fs18 from "fs";
23611
+ import * as path19 from "path";
23612
+ import chalk18 from "chalk";
23613
+ import { confirm as confirm2 } from "@inquirer/prompts";
23614
+ async function generateClaudeMdCommand(options) {
23615
+ const project = loadProject();
23616
+ const filePath = path19.join(project.marvinDir, "CLAUDE.md");
23617
+ if (fs18.existsSync(filePath) && !options.force) {
23618
+ const overwrite = await confirm2({
23619
+ message: ".marvin/CLAUDE.md already exists. Overwrite?",
23620
+ default: false
23621
+ });
23622
+ if (!overwrite) {
23623
+ console.log(chalk18.dim("Aborted."));
23624
+ return;
23625
+ }
23626
+ }
23627
+ fs18.writeFileSync(
23628
+ filePath,
23629
+ getDefaultClaudeMdContent(project.config.name),
23630
+ "utf-8"
23631
+ );
23632
+ console.log(chalk18.green("Created .marvin/CLAUDE.md"));
23633
+ }
23634
+
22658
23635
  // src/cli/program.ts
22659
23636
  function createProgram() {
22660
23637
  const program2 = new Command();
22661
23638
  program2.name("marvin").description(
22662
23639
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
22663
- ).version("0.3.3");
23640
+ ).version("0.3.6");
22664
23641
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
22665
23642
  await initCommand();
22666
23643
  });
@@ -22737,9 +23714,19 @@ function createProgram() {
22737
23714
  ).action(async (options) => {
22738
23715
  await garReportCommand(options);
22739
23716
  });
23717
+ reportCmd.command("health").description("Generate a governance health check report").option(
23718
+ "--format <format>",
23719
+ "Output format: ascii or confluence (default: ascii)"
23720
+ ).action(async (options) => {
23721
+ await healthReportCommand(options);
23722
+ });
22740
23723
  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
23724
  await webCommand(options);
22742
23725
  });
23726
+ const generateCmd = program2.command("generate").description("Generate project files");
23727
+ generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
23728
+ await generateClaudeMdCommand(options);
23729
+ });
22743
23730
  return program2;
22744
23731
  }
22745
23732