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/index.js CHANGED
@@ -432,6 +432,8 @@ var deliveryManager = {
432
432
 
433
433
  ## How You Work
434
434
  - Review open actions (A-xxx) and follow up on overdue items
435
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
436
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
435
437
  - Ensure decisions (D-xxx) are properly documented with rationale
436
438
  - Track questions (Q-xxx) and ensure they get answered
437
439
  - Monitor project health and flag risks early
@@ -524,9 +526,24 @@ function resolvePersonaId(input4) {
524
526
  }
525
527
 
526
528
  // src/personas/prompt-builder.ts
527
- function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment) {
529
+ import * as fs4 from "fs";
530
+ import * as path4 from "path";
531
+ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment, marvinDir) {
528
532
  const parts = [];
529
533
  parts.push(persona.systemPrompt);
534
+ if (marvinDir) {
535
+ const claudeMdPath = path4.join(marvinDir, "CLAUDE.md");
536
+ try {
537
+ const content = fs4.readFileSync(claudeMdPath, "utf-8").trim();
538
+ if (content) {
539
+ parts.push(`
540
+ ## Project Instructions
541
+ ${content}
542
+ `);
543
+ }
544
+ } catch {
545
+ }
546
+ }
530
547
  parts.push(`
531
548
  ## Project Context
532
549
  - **Project Name:** ${projectConfig.name}
@@ -536,7 +553,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
536
553
  ## Available Tools
537
554
  You have access to governance tools for managing project artifacts:
538
555
  - **Decisions** (D-xxx): List, get, create, and update decisions
539
- - **Actions** (A-xxx): List, get, create, and update action items
556
+ - **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
540
557
  - **Questions** (Q-xxx): List, get, create, and update questions
541
558
  - **Features** (F-xxx): List, get, create, and update feature definitions
542
559
  - **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
@@ -1344,10 +1361,10 @@ function mergeDefs(...defs) {
1344
1361
  function cloneDef(schema) {
1345
1362
  return mergeDefs(schema._zod.def);
1346
1363
  }
1347
- function getElementAtPath(obj, path18) {
1348
- if (!path18)
1364
+ function getElementAtPath(obj, path20) {
1365
+ if (!path20)
1349
1366
  return obj;
1350
- return path18.reduce((acc, key) => acc?.[key], obj);
1367
+ return path20.reduce((acc, key) => acc?.[key], obj);
1351
1368
  }
1352
1369
  function promiseAllObject(promisesObj) {
1353
1370
  const keys = Object.keys(promisesObj);
@@ -1730,11 +1747,11 @@ function aborted(x, startIndex = 0) {
1730
1747
  }
1731
1748
  return false;
1732
1749
  }
1733
- function prefixIssues(path18, issues) {
1750
+ function prefixIssues(path20, issues) {
1734
1751
  return issues.map((iss) => {
1735
1752
  var _a2;
1736
1753
  (_a2 = iss).path ?? (_a2.path = []);
1737
- iss.path.unshift(path18);
1754
+ iss.path.unshift(path20);
1738
1755
  return iss;
1739
1756
  });
1740
1757
  }
@@ -1917,7 +1934,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1917
1934
  }
1918
1935
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1919
1936
  const result = { errors: [] };
1920
- const processError = (error49, path18 = []) => {
1937
+ const processError = (error49, path20 = []) => {
1921
1938
  var _a2, _b;
1922
1939
  for (const issue2 of error49.issues) {
1923
1940
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1927,7 +1944,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1927
1944
  } else if (issue2.code === "invalid_element") {
1928
1945
  processError({ issues: issue2.issues }, issue2.path);
1929
1946
  } else {
1930
- const fullpath = [...path18, ...issue2.path];
1947
+ const fullpath = [...path20, ...issue2.path];
1931
1948
  if (fullpath.length === 0) {
1932
1949
  result.errors.push(mapper(issue2));
1933
1950
  continue;
@@ -1959,8 +1976,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1959
1976
  }
1960
1977
  function toDotPath(_path) {
1961
1978
  const segs = [];
1962
- const path18 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1963
- for (const seg of path18) {
1979
+ const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1980
+ for (const seg of path20) {
1964
1981
  if (typeof seg === "number")
1965
1982
  segs.push(`[${seg}]`);
1966
1983
  else if (typeof seg === "symbol")
@@ -13937,13 +13954,13 @@ function resolveRef(ref, ctx) {
13937
13954
  if (!ref.startsWith("#")) {
13938
13955
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13939
13956
  }
13940
- const path18 = ref.slice(1).split("/").filter(Boolean);
13941
- if (path18.length === 0) {
13957
+ const path20 = ref.slice(1).split("/").filter(Boolean);
13958
+ if (path20.length === 0) {
13942
13959
  return ctx.rootSchema;
13943
13960
  }
13944
13961
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13945
- if (path18[0] === defsKey) {
13946
- const key = path18[1];
13962
+ if (path20[0] === defsKey) {
13963
+ const key = path20[1];
13947
13964
  if (!key || !ctx.defs[key]) {
13948
13965
  throw new Error(`Reference not found: ${ref}`);
13949
13966
  }
@@ -14365,7 +14382,7 @@ function createDecisionTools(store) {
14365
14382
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14366
14383
  };
14367
14384
  },
14368
- { annotations: { readOnly: true } }
14385
+ { annotations: { readOnlyHint: true } }
14369
14386
  ),
14370
14387
  tool(
14371
14388
  "get_decision",
@@ -14392,7 +14409,7 @@ function createDecisionTools(store) {
14392
14409
  ]
14393
14410
  };
14394
14411
  },
14395
- { annotations: { readOnly: true } }
14412
+ { annotations: { readOnlyHint: true } }
14396
14413
  ),
14397
14414
  tool(
14398
14415
  "create_decision",
@@ -14433,7 +14450,8 @@ function createDecisionTools(store) {
14433
14450
  title: external_exports.string().optional().describe("New title"),
14434
14451
  status: external_exports.string().optional().describe("New status"),
14435
14452
  content: external_exports.string().optional().describe("New content"),
14436
- owner: external_exports.string().optional().describe("New owner")
14453
+ owner: external_exports.string().optional().describe("New owner"),
14454
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14437
14455
  },
14438
14456
  async (args) => {
14439
14457
  const { id, content, ...updates } = args;
@@ -14453,6 +14471,19 @@ function createDecisionTools(store) {
14453
14471
 
14454
14472
  // src/agent/tools/actions.ts
14455
14473
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14474
+ function findMatchingSprints(store, dueDate) {
14475
+ const sprints = store.list({ type: "sprint" });
14476
+ return sprints.filter((s) => {
14477
+ const start = s.frontmatter.startDate;
14478
+ const end = s.frontmatter.endDate;
14479
+ return start && end && dueDate >= start && dueDate <= end;
14480
+ }).map((s) => ({
14481
+ id: s.frontmatter.id,
14482
+ title: s.frontmatter.title,
14483
+ startDate: s.frontmatter.startDate,
14484
+ endDate: s.frontmatter.endDate
14485
+ }));
14486
+ }
14456
14487
  function createActionTools(store) {
14457
14488
  return [
14458
14489
  tool2(
@@ -14468,19 +14499,24 @@ function createActionTools(store) {
14468
14499
  status: args.status,
14469
14500
  owner: args.owner
14470
14501
  });
14471
- const summary = docs.map((d) => ({
14472
- id: d.frontmatter.id,
14473
- title: d.frontmatter.title,
14474
- status: d.frontmatter.status,
14475
- owner: d.frontmatter.owner,
14476
- priority: d.frontmatter.priority,
14477
- created: d.frontmatter.created
14478
- }));
14502
+ const summary = docs.map((d) => {
14503
+ const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
14504
+ return {
14505
+ id: d.frontmatter.id,
14506
+ title: d.frontmatter.title,
14507
+ status: d.frontmatter.status,
14508
+ owner: d.frontmatter.owner,
14509
+ priority: d.frontmatter.priority,
14510
+ dueDate: d.frontmatter.dueDate,
14511
+ sprints: sprintIds.length > 0 ? sprintIds : void 0,
14512
+ created: d.frontmatter.created
14513
+ };
14514
+ });
14479
14515
  return {
14480
14516
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14481
14517
  };
14482
14518
  },
14483
- { annotations: { readOnly: true } }
14519
+ { annotations: { readOnlyHint: true } }
14484
14520
  ),
14485
14521
  tool2(
14486
14522
  "get_action",
@@ -14507,7 +14543,7 @@ function createActionTools(store) {
14507
14543
  ]
14508
14544
  };
14509
14545
  },
14510
- { annotations: { readOnly: true } }
14546
+ { annotations: { readOnlyHint: true } }
14511
14547
  ),
14512
14548
  tool2(
14513
14549
  "create_action",
@@ -14518,9 +14554,18 @@ function createActionTools(store) {
14518
14554
  status: external_exports.string().optional().describe("Status (default: 'open')"),
14519
14555
  owner: external_exports.string().optional().describe("Person responsible"),
14520
14556
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
14521
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
14557
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
14558
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14559
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
14522
14560
  },
14523
14561
  async (args) => {
14562
+ const tags = [...args.tags ?? []];
14563
+ if (args.sprints) {
14564
+ for (const sprintId of args.sprints) {
14565
+ const tag = `sprint:${sprintId}`;
14566
+ if (!tags.includes(tag)) tags.push(tag);
14567
+ }
14568
+ }
14524
14569
  const doc = store.create(
14525
14570
  "action",
14526
14571
  {
@@ -14528,17 +14573,21 @@ function createActionTools(store) {
14528
14573
  status: args.status,
14529
14574
  owner: args.owner,
14530
14575
  priority: args.priority,
14531
- tags: args.tags
14576
+ tags: tags.length > 0 ? tags : void 0,
14577
+ dueDate: args.dueDate
14532
14578
  },
14533
14579
  args.content
14534
14580
  );
14581
+ const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
14582
+ if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
14583
+ const matching = findMatchingSprints(store, args.dueDate);
14584
+ if (matching.length > 0) {
14585
+ const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
14586
+ parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
14587
+ }
14588
+ }
14535
14589
  return {
14536
- content: [
14537
- {
14538
- type: "text",
14539
- text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
14540
- }
14541
- ]
14590
+ content: [{ type: "text", text: parts.join("\n") }]
14542
14591
  };
14543
14592
  }
14544
14593
  ),
@@ -14551,10 +14600,35 @@ function createActionTools(store) {
14551
14600
  status: external_exports.string().optional().describe("New status"),
14552
14601
  content: external_exports.string().optional().describe("New content"),
14553
14602
  owner: external_exports.string().optional().describe("New owner"),
14554
- priority: external_exports.string().optional().describe("New priority")
14603
+ priority: external_exports.string().optional().describe("New priority"),
14604
+ dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14605
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
14606
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
14555
14607
  },
14556
14608
  async (args) => {
14557
- const { id, content, ...updates } = args;
14609
+ const { id, content, sprints, tags, ...updates } = args;
14610
+ if (tags !== void 0) {
14611
+ const merged = [...tags];
14612
+ if (sprints) {
14613
+ for (const s of sprints) {
14614
+ const tag = `sprint:${s}`;
14615
+ if (!merged.includes(tag)) merged.push(tag);
14616
+ }
14617
+ }
14618
+ updates.tags = merged;
14619
+ } else if (sprints !== void 0) {
14620
+ const existing = store.get(id);
14621
+ if (!existing) {
14622
+ return {
14623
+ content: [{ type: "text", text: `Action ${id} not found` }],
14624
+ isError: true
14625
+ };
14626
+ }
14627
+ const existingTags = existing.frontmatter.tags ?? [];
14628
+ const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
14629
+ const newSprintTags = sprints.map((s) => `sprint:${s}`);
14630
+ updates.tags = [...nonSprintTags, ...newSprintTags];
14631
+ }
14558
14632
  const doc = store.update(id, updates, content);
14559
14633
  return {
14560
14634
  content: [
@@ -14565,6 +14639,35 @@ function createActionTools(store) {
14565
14639
  ]
14566
14640
  };
14567
14641
  }
14642
+ ),
14643
+ tool2(
14644
+ "suggest_sprints_for_action",
14645
+ "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
14646
+ {
14647
+ dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
14648
+ },
14649
+ async (args) => {
14650
+ const matching = findMatchingSprints(store, args.dueDate);
14651
+ if (matching.length === 0) {
14652
+ return {
14653
+ content: [
14654
+ {
14655
+ type: "text",
14656
+ text: `No sprints found containing dueDate ${args.dueDate}.`
14657
+ }
14658
+ ]
14659
+ };
14660
+ }
14661
+ return {
14662
+ content: [
14663
+ {
14664
+ type: "text",
14665
+ text: JSON.stringify(matching, null, 2)
14666
+ }
14667
+ ]
14668
+ };
14669
+ },
14670
+ { annotations: { readOnlyHint: true } }
14568
14671
  )
14569
14672
  ];
14570
14673
  }
@@ -14592,7 +14695,7 @@ function createQuestionTools(store) {
14592
14695
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14593
14696
  };
14594
14697
  },
14595
- { annotations: { readOnly: true } }
14698
+ { annotations: { readOnlyHint: true } }
14596
14699
  ),
14597
14700
  tool3(
14598
14701
  "get_question",
@@ -14619,7 +14722,7 @@ function createQuestionTools(store) {
14619
14722
  ]
14620
14723
  };
14621
14724
  },
14622
- { annotations: { readOnly: true } }
14725
+ { annotations: { readOnlyHint: true } }
14623
14726
  ),
14624
14727
  tool3(
14625
14728
  "create_question",
@@ -14660,7 +14763,8 @@ function createQuestionTools(store) {
14660
14763
  title: external_exports.string().optional().describe("New title"),
14661
14764
  status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
14662
14765
  content: external_exports.string().optional().describe("Updated content / answer"),
14663
- owner: external_exports.string().optional().describe("New owner")
14766
+ owner: external_exports.string().optional().describe("New owner"),
14767
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14664
14768
  },
14665
14769
  async (args) => {
14666
14770
  const { id, content, ...updates } = args;
@@ -14712,7 +14816,7 @@ function createDocumentTools(store) {
14712
14816
  ]
14713
14817
  };
14714
14818
  },
14715
- { annotations: { readOnly: true } }
14819
+ { annotations: { readOnlyHint: true } }
14716
14820
  ),
14717
14821
  tool4(
14718
14822
  "read_document",
@@ -14739,7 +14843,7 @@ function createDocumentTools(store) {
14739
14843
  ]
14740
14844
  };
14741
14845
  },
14742
- { annotations: { readOnly: true } }
14846
+ { annotations: { readOnlyHint: true } }
14743
14847
  ),
14744
14848
  tool4(
14745
14849
  "project_summary",
@@ -14767,7 +14871,7 @@ function createDocumentTools(store) {
14767
14871
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14768
14872
  };
14769
14873
  },
14770
- { annotations: { readOnly: true } }
14874
+ { annotations: { readOnlyHint: true } }
14771
14875
  )
14772
14876
  ];
14773
14877
  }
@@ -14804,7 +14908,7 @@ function createSourceTools(manifest) {
14804
14908
  ]
14805
14909
  };
14806
14910
  },
14807
- { annotations: { readOnly: true } }
14911
+ { annotations: { readOnlyHint: true } }
14808
14912
  ),
14809
14913
  tool5(
14810
14914
  "get_source_info",
@@ -14838,7 +14942,7 @@ function createSourceTools(manifest) {
14838
14942
  ]
14839
14943
  };
14840
14944
  },
14841
- { annotations: { readOnly: true } }
14945
+ { annotations: { readOnlyHint: true } }
14842
14946
  )
14843
14947
  ];
14844
14948
  }
@@ -14869,7 +14973,7 @@ function createSessionTools(store) {
14869
14973
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
14870
14974
  };
14871
14975
  },
14872
- { annotations: { readOnly: true } }
14976
+ { annotations: { readOnlyHint: true } }
14873
14977
  ),
14874
14978
  tool6(
14875
14979
  "get_session",
@@ -14887,7 +14991,7 @@ function createSessionTools(store) {
14887
14991
  content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
14888
14992
  };
14889
14993
  },
14890
- { annotations: { readOnly: true } }
14994
+ { annotations: { readOnlyHint: true } }
14891
14995
  )
14892
14996
  ];
14893
14997
  }
@@ -14905,12 +15009,20 @@ function collectGarMetrics(store) {
14905
15009
  const blockedItems = allDocs.filter(
14906
15010
  (d) => d.frontmatter.tags?.includes("blocked")
14907
15011
  );
14908
- const overdueItems = allDocs.filter(
15012
+ const tagOverdueItems = allDocs.filter(
14909
15013
  (d) => d.frontmatter.tags?.includes("overdue")
14910
15014
  );
15015
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15016
+ const dateOverdueActions = openActions.filter((d) => {
15017
+ const dueDate = d.frontmatter.dueDate;
15018
+ return typeof dueDate === "string" && dueDate < today;
15019
+ });
15020
+ const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
15021
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15022
+ );
14911
15023
  const openQuestions = store.list({ type: "question", status: "open" });
14912
15024
  const riskItems = allDocs.filter(
14913
- (d) => d.frontmatter.tags?.includes("risk")
15025
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
14914
15026
  );
14915
15027
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14916
15028
  const total = allActions.length;
@@ -15016,6 +15128,253 @@ function evaluateGar(projectName, metrics) {
15016
15128
  };
15017
15129
  }
15018
15130
 
15131
+ // src/reports/health/collector.ts
15132
+ var FIELD_CHECKS = [
15133
+ {
15134
+ type: "action",
15135
+ openStatuses: ["open", "in-progress"],
15136
+ requiredFields: ["owner", "priority", "dueDate", "content"]
15137
+ },
15138
+ {
15139
+ type: "decision",
15140
+ openStatuses: ["open", "proposed"],
15141
+ requiredFields: ["owner", "content"]
15142
+ },
15143
+ {
15144
+ type: "question",
15145
+ openStatuses: ["open"],
15146
+ requiredFields: ["owner", "content"]
15147
+ },
15148
+ {
15149
+ type: "feature",
15150
+ openStatuses: ["draft", "approved"],
15151
+ requiredFields: ["owner", "priority", "content"]
15152
+ },
15153
+ {
15154
+ type: "epic",
15155
+ openStatuses: ["planned", "in-progress"],
15156
+ requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
15157
+ },
15158
+ {
15159
+ type: "sprint",
15160
+ openStatuses: ["planned", "active"],
15161
+ requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
15162
+ }
15163
+ ];
15164
+ var STALE_THRESHOLD_DAYS = 14;
15165
+ var AGING_THRESHOLD_DAYS = 30;
15166
+ function daysBetween(a, b) {
15167
+ const msPerDay = 864e5;
15168
+ const dateA = new Date(a);
15169
+ const dateB = new Date(b);
15170
+ return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
15171
+ }
15172
+ function checkMissingFields(doc, requiredFields) {
15173
+ const missing = [];
15174
+ for (const field of requiredFields) {
15175
+ if (field === "content") {
15176
+ if (!doc.content || doc.content.trim().length === 0) {
15177
+ missing.push("content");
15178
+ }
15179
+ } else if (field === "linkedEpics") {
15180
+ const val = doc.frontmatter[field];
15181
+ if (!Array.isArray(val) || val.length === 0) {
15182
+ missing.push(field);
15183
+ }
15184
+ } else {
15185
+ const val = doc.frontmatter[field];
15186
+ if (val === void 0 || val === null || val === "") {
15187
+ missing.push(field);
15188
+ }
15189
+ }
15190
+ }
15191
+ return missing;
15192
+ }
15193
+ function collectCompleteness(store) {
15194
+ const result = {};
15195
+ for (const check2 of FIELD_CHECKS) {
15196
+ const allOfType = store.list({ type: check2.type });
15197
+ const openDocs = allOfType.filter(
15198
+ (d) => check2.openStatuses.includes(d.frontmatter.status)
15199
+ );
15200
+ const gaps = [];
15201
+ let complete = 0;
15202
+ for (const doc of openDocs) {
15203
+ const missingFields = checkMissingFields(doc, check2.requiredFields);
15204
+ if (missingFields.length === 0) {
15205
+ complete++;
15206
+ } else {
15207
+ gaps.push({
15208
+ id: doc.frontmatter.id,
15209
+ title: doc.frontmatter.title,
15210
+ missingFields
15211
+ });
15212
+ }
15213
+ }
15214
+ result[check2.type] = {
15215
+ total: openDocs.length,
15216
+ complete,
15217
+ gaps
15218
+ };
15219
+ }
15220
+ return result;
15221
+ }
15222
+ function collectProcess(store) {
15223
+ const today = (/* @__PURE__ */ new Date()).toISOString();
15224
+ const allDocs = store.list();
15225
+ const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
15226
+ const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
15227
+ const stale = [];
15228
+ for (const doc of openDocs) {
15229
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
15230
+ const days = daysBetween(updated, today);
15231
+ if (days >= STALE_THRESHOLD_DAYS) {
15232
+ stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15233
+ }
15234
+ }
15235
+ const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
15236
+ const agingActions = [];
15237
+ for (const doc of openActions) {
15238
+ const days = daysBetween(doc.frontmatter.created, today);
15239
+ if (days >= AGING_THRESHOLD_DAYS) {
15240
+ agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
15241
+ }
15242
+ }
15243
+ const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
15244
+ let decisionTotal = 0;
15245
+ for (const doc of resolvedDecisions) {
15246
+ decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15247
+ }
15248
+ const decisionVelocity = {
15249
+ avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
15250
+ count: resolvedDecisions.length
15251
+ };
15252
+ const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
15253
+ let questionTotal = 0;
15254
+ for (const doc of answeredQuestions) {
15255
+ questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
15256
+ }
15257
+ const questionResolution = {
15258
+ avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
15259
+ count: answeredQuestions.length
15260
+ };
15261
+ return { stale, agingActions, decisionVelocity, questionResolution };
15262
+ }
15263
+ function collectHealthMetrics(store) {
15264
+ return {
15265
+ completeness: collectCompleteness(store),
15266
+ process: collectProcess(store)
15267
+ };
15268
+ }
15269
+
15270
+ // src/reports/health/evaluator.ts
15271
+ function worstStatus2(statuses) {
15272
+ if (statuses.includes("red")) return "red";
15273
+ if (statuses.includes("amber")) return "amber";
15274
+ return "green";
15275
+ }
15276
+ function completenessStatus(total, complete) {
15277
+ if (total === 0) return "green";
15278
+ const pct = Math.round(complete / total * 100);
15279
+ if (pct >= 100) return "green";
15280
+ if (pct >= 75) return "amber";
15281
+ return "red";
15282
+ }
15283
+ var TYPE_LABELS = {
15284
+ action: "Actions",
15285
+ decision: "Decisions",
15286
+ question: "Questions",
15287
+ feature: "Features",
15288
+ epic: "Epics",
15289
+ sprint: "Sprints"
15290
+ };
15291
+ function evaluateHealth(projectName, metrics) {
15292
+ const completeness = [];
15293
+ for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
15294
+ const { total, complete, gaps } = catMetrics;
15295
+ const status = completenessStatus(total, complete);
15296
+ const pct = total > 0 ? Math.round(complete / total * 100) : 100;
15297
+ completeness.push({
15298
+ name: TYPE_LABELS[type] ?? type,
15299
+ status,
15300
+ summary: `${pct}% complete (${complete}/${total})`,
15301
+ items: gaps.map((g) => ({
15302
+ id: g.id,
15303
+ detail: `missing: ${g.missingFields.join(", ")}`
15304
+ }))
15305
+ });
15306
+ }
15307
+ const process3 = [];
15308
+ const staleCount = metrics.process.stale.length;
15309
+ const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
15310
+ process3.push({
15311
+ name: "Stale Items",
15312
+ status: staleStatus,
15313
+ summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
15314
+ items: metrics.process.stale.map((s) => ({
15315
+ id: s.id,
15316
+ detail: `${s.days} days since last update`
15317
+ }))
15318
+ });
15319
+ const agingCount = metrics.process.agingActions.length;
15320
+ const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
15321
+ process3.push({
15322
+ name: "Aging Actions",
15323
+ status: agingStatus,
15324
+ summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
15325
+ items: metrics.process.agingActions.map((a) => ({
15326
+ id: a.id,
15327
+ detail: `open for ${a.days} days`
15328
+ }))
15329
+ });
15330
+ const dv = metrics.process.decisionVelocity;
15331
+ let dvStatus;
15332
+ if (dv.count === 0) {
15333
+ dvStatus = "green";
15334
+ } else if (dv.avgDays <= 7) {
15335
+ dvStatus = "green";
15336
+ } else if (dv.avgDays <= 21) {
15337
+ dvStatus = "amber";
15338
+ } else {
15339
+ dvStatus = "red";
15340
+ }
15341
+ process3.push({
15342
+ name: "Decision Velocity",
15343
+ status: dvStatus,
15344
+ summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
15345
+ items: []
15346
+ });
15347
+ const qr = metrics.process.questionResolution;
15348
+ let qrStatus;
15349
+ if (qr.count === 0) {
15350
+ qrStatus = "green";
15351
+ } else if (qr.avgDays <= 7) {
15352
+ qrStatus = "green";
15353
+ } else if (qr.avgDays <= 14) {
15354
+ qrStatus = "amber";
15355
+ } else {
15356
+ qrStatus = "red";
15357
+ }
15358
+ process3.push({
15359
+ name: "Question Resolution",
15360
+ status: qrStatus,
15361
+ summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
15362
+ items: []
15363
+ });
15364
+ const allStatuses = [
15365
+ ...completeness.map((c) => c.status),
15366
+ ...process3.map((p) => p.status)
15367
+ ];
15368
+ const overall = worstStatus2(allStatuses);
15369
+ return {
15370
+ projectName,
15371
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15372
+ overall,
15373
+ completeness,
15374
+ process: process3
15375
+ };
15376
+ }
15377
+
15019
15378
  // src/web/data.ts
15020
15379
  function getOverviewData(store) {
15021
15380
  const types = [];
@@ -15087,6 +15446,46 @@ function getBoardData(store, type) {
15087
15446
  }));
15088
15447
  return { columns, type, types };
15089
15448
  }
15449
+ function getDiagramData(store) {
15450
+ const allDocs = store.list();
15451
+ const sprints = [];
15452
+ const epics = [];
15453
+ const features = [];
15454
+ const statusCounts = {};
15455
+ for (const doc of allDocs) {
15456
+ const fm = doc.frontmatter;
15457
+ const status = fm.status.toLowerCase();
15458
+ statusCounts[status] = (statusCounts[status] ?? 0) + 1;
15459
+ switch (fm.type) {
15460
+ case "sprint":
15461
+ sprints.push({
15462
+ id: fm.id,
15463
+ title: fm.title,
15464
+ status: fm.status,
15465
+ startDate: fm.startDate,
15466
+ endDate: fm.endDate,
15467
+ linkedEpics: fm.linkedEpics ?? []
15468
+ });
15469
+ break;
15470
+ case "epic":
15471
+ epics.push({
15472
+ id: fm.id,
15473
+ title: fm.title,
15474
+ status: fm.status,
15475
+ linkedFeature: fm.linkedFeature
15476
+ });
15477
+ break;
15478
+ case "feature":
15479
+ features.push({
15480
+ id: fm.id,
15481
+ title: fm.title,
15482
+ status: fm.status
15483
+ });
15484
+ break;
15485
+ }
15486
+ }
15487
+ return { sprints, epics, features, statusCounts };
15488
+ }
15090
15489
 
15091
15490
  // src/web/templates/layout.ts
15092
15491
  function escapeHtml(str) {
@@ -15117,16 +15516,43 @@ function renderMarkdown(md) {
15117
15516
  const out = [];
15118
15517
  let inList = false;
15119
15518
  let listTag = "ul";
15120
- for (const raw of lines) {
15121
- const line = raw;
15519
+ let inTable = false;
15520
+ let i = 0;
15521
+ while (i < lines.length) {
15522
+ const line = lines[i];
15122
15523
  if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
15123
15524
  out.push(`</${listTag}>`);
15124
15525
  inList = false;
15125
15526
  }
15527
+ if (inTable && !/^\s*\|/.test(line)) {
15528
+ out.push("</tbody></table></div>");
15529
+ inTable = false;
15530
+ }
15531
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
15532
+ i++;
15533
+ out.push("<hr>");
15534
+ continue;
15535
+ }
15536
+ if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
15537
+ const headers = parseTableRow(line);
15538
+ out.push('<div class="table-wrap"><table><thead><tr>');
15539
+ out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
15540
+ out.push("</tr></thead><tbody>");
15541
+ inTable = true;
15542
+ i += 2;
15543
+ continue;
15544
+ }
15545
+ if (inTable && /^\s*\|/.test(line)) {
15546
+ const cells = parseTableRow(line);
15547
+ out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
15548
+ i++;
15549
+ continue;
15550
+ }
15126
15551
  const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
15127
15552
  if (headingMatch) {
15128
15553
  const level = headingMatch[1].length;
15129
15554
  out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
15555
+ i++;
15130
15556
  continue;
15131
15557
  }
15132
15558
  const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
@@ -15138,6 +15564,7 @@ function renderMarkdown(md) {
15138
15564
  listTag = "ul";
15139
15565
  }
15140
15566
  out.push(`<li>${inline(ulMatch[1])}</li>`);
15567
+ i++;
15141
15568
  continue;
15142
15569
  }
15143
15570
  const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
@@ -15149,6 +15576,7 @@ function renderMarkdown(md) {
15149
15576
  listTag = "ol";
15150
15577
  }
15151
15578
  out.push(`<li>${inline(olMatch[1])}</li>`);
15579
+ i++;
15152
15580
  continue;
15153
15581
  }
15154
15582
  if (line.trim() === "") {
@@ -15156,13 +15584,19 @@ function renderMarkdown(md) {
15156
15584
  out.push(`</${listTag}>`);
15157
15585
  inList = false;
15158
15586
  }
15587
+ i++;
15159
15588
  continue;
15160
15589
  }
15161
15590
  out.push(`<p>${inline(line)}</p>`);
15591
+ i++;
15162
15592
  }
15163
15593
  if (inList) out.push(`</${listTag}>`);
15594
+ if (inTable) out.push("</tbody></table></div>");
15164
15595
  return out.join("\n");
15165
15596
  }
15597
+ function parseTableRow(line) {
15598
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
15599
+ }
15166
15600
  function inline(text) {
15167
15601
  let s = escapeHtml(text);
15168
15602
  s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
@@ -15176,7 +15610,8 @@ function layout(opts, body) {
15176
15610
  const topItems = [
15177
15611
  { href: "/", label: "Overview" },
15178
15612
  { href: "/board", label: "Board" },
15179
- { href: "/gar", label: "GAR Report" }
15613
+ { href: "/gar", label: "GAR Report" },
15614
+ { href: "/health", label: "Health" }
15180
15615
  ];
15181
15616
  const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
15182
15617
  const groupsHtml = opts.navGroups.map((group) => {
@@ -15211,9 +15646,15 @@ function layout(opts, body) {
15211
15646
  </nav>
15212
15647
  </aside>
15213
15648
  <main class="main">
15649
+ <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
15650
+ <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>
15651
+ <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>
15652
+ </button>
15214
15653
  ${body}
15215
15654
  </main>
15216
15655
  </div>
15656
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
15657
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
15217
15658
  </body>
15218
15659
  </html>`;
15219
15660
  }
@@ -15328,7 +15769,33 @@ a:hover { text-decoration: underline; }
15328
15769
  flex: 1;
15329
15770
  padding: 2rem 2.5rem;
15330
15771
  max-width: 1200px;
15772
+ position: relative;
15773
+ transition: max-width 0.2s ease;
15774
+ }
15775
+ .main.expanded {
15776
+ max-width: none;
15777
+ }
15778
+ .expand-toggle {
15779
+ position: absolute;
15780
+ top: 1rem;
15781
+ right: 1rem;
15782
+ background: var(--bg-card);
15783
+ border: 1px solid var(--border);
15784
+ border-radius: var(--radius);
15785
+ color: var(--text-dim);
15786
+ cursor: pointer;
15787
+ padding: 0.4rem;
15788
+ display: flex;
15789
+ align-items: center;
15790
+ justify-content: center;
15791
+ transition: color 0.15s, border-color 0.15s;
15331
15792
  }
15793
+ .expand-toggle:hover {
15794
+ color: var(--text);
15795
+ border-color: var(--text-dim);
15796
+ }
15797
+ .main.expanded .icon-expand { display: none; }
15798
+ .main:not(.expanded) .icon-collapse { display: none; }
15332
15799
 
15333
15800
  /* Page header */
15334
15801
  .page-header {
@@ -15357,12 +15824,26 @@ a:hover { text-decoration: underline; }
15357
15824
  .breadcrumb a:hover { color: var(--accent); }
15358
15825
  .breadcrumb .sep { margin: 0 0.4rem; }
15359
15826
 
15827
+ /* Card groups */
15828
+ .card-group {
15829
+ margin-bottom: 1.5rem;
15830
+ }
15831
+
15832
+ .card-group-label {
15833
+ font-size: 0.7rem;
15834
+ text-transform: uppercase;
15835
+ letter-spacing: 0.08em;
15836
+ color: var(--text-dim);
15837
+ font-weight: 600;
15838
+ margin-bottom: 0.5rem;
15839
+ }
15840
+
15360
15841
  /* Cards grid */
15361
15842
  .cards {
15362
15843
  display: grid;
15363
15844
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
15364
15845
  gap: 1rem;
15365
- margin-bottom: 2rem;
15846
+ margin-bottom: 0.5rem;
15366
15847
  }
15367
15848
 
15368
15849
  .card {
@@ -15643,6 +16124,14 @@ tr:hover td {
15643
16124
  font-family: var(--mono);
15644
16125
  font-size: 0.85em;
15645
16126
  }
16127
+ .detail-content hr {
16128
+ border: none;
16129
+ border-top: 1px solid var(--border);
16130
+ margin: 1.25rem 0;
16131
+ }
16132
+ .detail-content .table-wrap {
16133
+ margin: 0.75rem 0;
16134
+ }
15646
16135
 
15647
16136
  /* Filters */
15648
16137
  .filters {
@@ -15687,21 +16176,206 @@ tr:hover td {
15687
16176
  .priority-high { color: var(--red); }
15688
16177
  .priority-medium { color: var(--amber); }
15689
16178
  .priority-low { color: var(--green); }
16179
+
16180
+ /* Health */
16181
+ .health-section-title {
16182
+ font-size: 1.1rem;
16183
+ font-weight: 600;
16184
+ margin: 2rem 0 1rem;
16185
+ color: var(--text);
16186
+ }
16187
+
16188
+ /* Mermaid diagrams */
16189
+ .mermaid-container {
16190
+ background: var(--bg-card);
16191
+ border: 1px solid var(--border);
16192
+ border-radius: var(--radius);
16193
+ padding: 1.5rem;
16194
+ margin: 1rem 0;
16195
+ overflow-x: auto;
16196
+ }
16197
+
16198
+ .mermaid-container .mermaid {
16199
+ display: flex;
16200
+ justify-content: center;
16201
+ }
16202
+
16203
+ .mermaid-empty {
16204
+ text-align: center;
16205
+ color: var(--text-dim);
16206
+ font-size: 0.875rem;
16207
+ }
16208
+
16209
+ .mermaid-row {
16210
+ display: grid;
16211
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
16212
+ gap: 1rem;
16213
+ }
16214
+
16215
+ .mermaid-row .mermaid-container {
16216
+ margin: 0;
16217
+ }
15690
16218
  `;
15691
16219
  }
15692
16220
 
16221
+ // src/web/templates/mermaid.ts
16222
+ function sanitize(text, maxLen = 40) {
16223
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
16224
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
16225
+ }
16226
+ function mermaidBlock(definition) {
16227
+ return `<div class="mermaid-container"><pre class="mermaid">
16228
+ ${definition}
16229
+ </pre></div>`;
16230
+ }
16231
+ function placeholder(message) {
16232
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
16233
+ }
16234
+ function buildTimelineGantt(data) {
16235
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
16236
+ if (sprintsWithDates.length === 0) {
16237
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
16238
+ }
16239
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
16240
+ const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
16241
+ for (const sprint of sprintsWithDates) {
16242
+ lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
16243
+ const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
16244
+ if (linked.length === 0) {
16245
+ lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
16246
+ } else {
16247
+ for (const epic of linked) {
16248
+ const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
16249
+ lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
16250
+ }
16251
+ }
16252
+ }
16253
+ return mermaidBlock(lines.join("\n"));
16254
+ }
16255
+ function buildArtifactFlowchart(data) {
16256
+ if (data.features.length === 0 && data.epics.length === 0) {
16257
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
16258
+ }
16259
+ const lines = ["graph TD"];
16260
+ lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
16261
+ lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
16262
+ lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
16263
+ lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
16264
+ const nodeIds = /* @__PURE__ */ new Set();
16265
+ for (const epic of data.epics) {
16266
+ if (epic.linkedFeature) {
16267
+ const feature = data.features.find((f) => f.id === epic.linkedFeature);
16268
+ if (feature) {
16269
+ const fNode = feature.id.replace(/-/g, "_");
16270
+ const eNode = epic.id.replace(/-/g, "_");
16271
+ if (!nodeIds.has(fNode)) {
16272
+ lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
16273
+ nodeIds.add(fNode);
16274
+ }
16275
+ if (!nodeIds.has(eNode)) {
16276
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
16277
+ nodeIds.add(eNode);
16278
+ }
16279
+ lines.push(` ${fNode} --> ${eNode}`);
16280
+ }
16281
+ }
16282
+ }
16283
+ for (const sprint of data.sprints) {
16284
+ const sNode = sprint.id.replace(/-/g, "_");
16285
+ for (const epicId of sprint.linkedEpics) {
16286
+ const epic = data.epics.find((e) => e.id === epicId);
16287
+ if (epic) {
16288
+ const eNode = epic.id.replace(/-/g, "_");
16289
+ if (!nodeIds.has(eNode)) {
16290
+ lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
16291
+ nodeIds.add(eNode);
16292
+ }
16293
+ if (!nodeIds.has(sNode)) {
16294
+ lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
16295
+ nodeIds.add(sNode);
16296
+ }
16297
+ lines.push(` ${eNode} --> ${sNode}`);
16298
+ }
16299
+ }
16300
+ }
16301
+ if (nodeIds.size === 0) {
16302
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
16303
+ }
16304
+ const allItems = [
16305
+ ...data.features.map((f) => ({ id: f.id, status: f.status })),
16306
+ ...data.epics.map((e) => ({ id: e.id, status: e.status })),
16307
+ ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
16308
+ ];
16309
+ for (const item of allItems) {
16310
+ const node = item.id.replace(/-/g, "_");
16311
+ if (!nodeIds.has(node)) continue;
16312
+ const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
16313
+ if (cls) {
16314
+ lines.push(` class ${node} ${cls}`);
16315
+ }
16316
+ }
16317
+ return mermaidBlock(lines.join("\n"));
16318
+ }
16319
+ function buildStatusPie(title, counts) {
16320
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
16321
+ if (entries.length === 0) {
16322
+ return placeholder(`No data for ${title}.`);
16323
+ }
16324
+ const lines = [`pie title ${sanitize(title, 60)}`];
16325
+ for (const [label, count] of entries) {
16326
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
16327
+ }
16328
+ return mermaidBlock(lines.join("\n"));
16329
+ }
16330
+ function buildHealthGauge(categories) {
16331
+ const valid = categories.filter((c) => c.total > 0);
16332
+ if (valid.length === 0) {
16333
+ return placeholder("No completeness data available.");
16334
+ }
16335
+ const pies = valid.map((cat) => {
16336
+ const incomplete = cat.total - cat.complete;
16337
+ const lines = [
16338
+ `pie title ${sanitize(cat.name, 30)}`,
16339
+ ` "Complete" : ${cat.complete}`,
16340
+ ` "Incomplete" : ${incomplete}`
16341
+ ];
16342
+ return mermaidBlock(lines.join("\n"));
16343
+ });
16344
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
16345
+ }
16346
+
15693
16347
  // src/web/templates/pages/overview.ts
15694
- function overviewPage(data) {
15695
- const cards = data.types.map(
15696
- (t) => `
16348
+ function renderCard(t) {
16349
+ return `
15697
16350
  <div class="card">
15698
16351
  <a href="/docs/${t.type}">
15699
16352
  <div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
15700
16353
  <div class="card-value">${t.total}</div>
15701
16354
  ${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
15702
16355
  </a>
15703
- </div>`
15704
- ).join("\n");
16356
+ </div>`;
16357
+ }
16358
+ function overviewPage(data, diagrams, navGroups) {
16359
+ const typeMap = new Map(data.types.map((t) => [t.type, t]));
16360
+ const placed = /* @__PURE__ */ new Set();
16361
+ const groupSections = navGroups.map((group) => {
16362
+ const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
16363
+ placed.add(type);
16364
+ return renderCard(typeMap.get(type));
16365
+ });
16366
+ if (groupCards.length === 0) return "";
16367
+ return `
16368
+ <div class="card-group">
16369
+ <div class="card-group-label">${escapeHtml(group.label)}</div>
16370
+ <div class="cards">${groupCards.join("\n")}</div>
16371
+ </div>`;
16372
+ }).filter(Boolean).join("\n");
16373
+ const ungrouped = data.types.filter((t) => !placed.has(t.type));
16374
+ const ungroupedSection = ungrouped.length > 0 ? `
16375
+ <div class="card-group">
16376
+ <div class="card-group-label">Other</div>
16377
+ <div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
16378
+ </div>` : "";
15705
16379
  const rows = data.recent.map(
15706
16380
  (doc) => `
15707
16381
  <tr>
@@ -15717,9 +16391,14 @@ function overviewPage(data) {
15717
16391
  <h2>Project Overview</h2>
15718
16392
  </div>
15719
16393
 
15720
- <div class="cards">
15721
- ${cards}
15722
- </div>
16394
+ ${groupSections}
16395
+ ${ungroupedSection}
16396
+
16397
+ <div class="section-title">Project Timeline</div>
16398
+ ${buildTimelineGantt(diagrams)}
16399
+
16400
+ <div class="section-title">Artifact Relationships</div>
16401
+ ${buildArtifactFlowchart(diagrams)}
15723
16402
 
15724
16403
  <div class="section-title">Recent Activity</div>
15725
16404
  ${data.recent.length > 0 ? `
@@ -15886,6 +16565,76 @@ function garPage(report) {
15886
16565
  <div class="gar-areas">
15887
16566
  ${areaCards}
15888
16567
  </div>
16568
+
16569
+ <div class="section-title">Status Distribution</div>
16570
+ ${buildStatusPie("Action Status", {
16571
+ Open: report.metrics.scope.open,
16572
+ Done: report.metrics.scope.done,
16573
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
16574
+ })}
16575
+ `;
16576
+ }
16577
+
16578
+ // src/web/templates/pages/health.ts
16579
+ function healthPage(report, metrics) {
16580
+ const dotClass = `dot-${report.overall}`;
16581
+ function renderSection(title, categories) {
16582
+ const cards = categories.map(
16583
+ (cat) => `
16584
+ <div class="gar-area">
16585
+ <div class="area-header">
16586
+ <div class="area-dot dot-${cat.status}"></div>
16587
+ <div class="area-name">${escapeHtml(cat.name)}</div>
16588
+ </div>
16589
+ <div class="area-summary">${escapeHtml(cat.summary)}</div>
16590
+ ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
16591
+ </div>`
16592
+ ).join("\n");
16593
+ return `
16594
+ <div class="health-section-title">${escapeHtml(title)}</div>
16595
+ <div class="gar-areas">${cards}</div>
16596
+ `;
16597
+ }
16598
+ return `
16599
+ <div class="page-header">
16600
+ <h2>Governance Health Check</h2>
16601
+ <div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
16602
+ </div>
16603
+
16604
+ <div class="gar-overall">
16605
+ <div class="dot ${dotClass}"></div>
16606
+ <div class="label">Overall: ${escapeHtml(report.overall)}</div>
16607
+ </div>
16608
+
16609
+ ${renderSection("Completeness", report.completeness)}
16610
+
16611
+ <div class="health-section-title">Completeness Overview</div>
16612
+ ${buildHealthGauge(
16613
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
16614
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
16615
+ complete: cat.complete,
16616
+ total: cat.total
16617
+ })) : report.completeness.map((c) => {
16618
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
16619
+ return {
16620
+ name: c.name,
16621
+ complete: match ? parseInt(match[1], 10) : 0,
16622
+ total: match ? parseInt(match[2], 10) : 0
16623
+ };
16624
+ })
16625
+ )}
16626
+
16627
+ ${renderSection("Process", report.process)}
16628
+
16629
+ <div class="health-section-title">Process Summary</div>
16630
+ ${metrics ? buildStatusPie("Process Health", {
16631
+ Stale: metrics.process.stale.length,
16632
+ "Aging Actions": metrics.process.agingActions.length,
16633
+ Healthy: Math.max(
16634
+ 0,
16635
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
16636
+ )
16637
+ }) : ""}
15889
16638
  `;
15890
16639
  }
15891
16640
 
@@ -15952,7 +16701,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
15952
16701
  }
15953
16702
  if (pathname === "/") {
15954
16703
  const data = getOverviewData(store);
15955
- const body = overviewPage(data);
16704
+ const diagrams = getDiagramData(store);
16705
+ const body = overviewPage(data, diagrams, navGroups);
15956
16706
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
15957
16707
  return;
15958
16708
  }
@@ -15962,6 +16712,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
15962
16712
  respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
15963
16713
  return;
15964
16714
  }
16715
+ if (pathname === "/health") {
16716
+ const healthMetrics = collectHealthMetrics(store);
16717
+ const report = evaluateHealth(projectName, healthMetrics);
16718
+ const body = healthPage(report, healthMetrics);
16719
+ respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
16720
+ return;
16721
+ }
15965
16722
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
15966
16723
  if (boardMatch) {
15967
16724
  const type = boardMatch[1];
@@ -16042,7 +16799,7 @@ function createMeetingTools(store) {
16042
16799
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16043
16800
  };
16044
16801
  },
16045
- { annotations: { readOnly: true } }
16802
+ { annotations: { readOnlyHint: true } }
16046
16803
  ),
16047
16804
  tool7(
16048
16805
  "get_meeting",
@@ -16069,7 +16826,7 @@ function createMeetingTools(store) {
16069
16826
  ]
16070
16827
  };
16071
16828
  },
16072
- { annotations: { readOnly: true } }
16829
+ { annotations: { readOnlyHint: true } }
16073
16830
  ),
16074
16831
  tool7(
16075
16832
  "create_meeting",
@@ -16194,7 +16951,7 @@ function createMeetingTools(store) {
16194
16951
  content: [{ type: "text", text: sections.join("\n") }]
16195
16952
  };
16196
16953
  },
16197
- { annotations: { readOnly: true } }
16954
+ { annotations: { readOnlyHint: true } }
16198
16955
  )
16199
16956
  ];
16200
16957
  }
@@ -16219,7 +16976,8 @@ function createReportTools(store) {
16219
16976
  id: d.frontmatter.id,
16220
16977
  title: d.frontmatter.title,
16221
16978
  owner: d.frontmatter.owner,
16222
- priority: d.frontmatter.priority
16979
+ priority: d.frontmatter.priority,
16980
+ dueDate: d.frontmatter.dueDate
16223
16981
  })),
16224
16982
  completedActions: completedActions.map((d) => ({
16225
16983
  id: d.frontmatter.id,
@@ -16238,7 +16996,7 @@ function createReportTools(store) {
16238
16996
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
16239
16997
  };
16240
16998
  },
16241
- { annotations: { readOnly: true } }
16999
+ { annotations: { readOnlyHint: true } }
16242
17000
  ),
16243
17001
  tool8(
16244
17002
  "generate_risk_register",
@@ -16247,7 +17005,7 @@ function createReportTools(store) {
16247
17005
  async () => {
16248
17006
  const allDocs = store.list();
16249
17007
  const taggedRisks = allDocs.filter(
16250
- (d) => d.frontmatter.tags?.includes("risk")
17008
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
16251
17009
  );
16252
17010
  const highPriorityActions = store.list({ type: "action", status: "open" }).filter((d) => d.frontmatter.priority === "high");
16253
17011
  const unresolvedQuestions = store.list({ type: "question", status: "open" });
@@ -16282,7 +17040,7 @@ function createReportTools(store) {
16282
17040
  content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
16283
17041
  };
16284
17042
  },
16285
- { annotations: { readOnly: true } }
17043
+ { annotations: { readOnlyHint: true } }
16286
17044
  ),
16287
17045
  tool8(
16288
17046
  "generate_gar_report",
@@ -16295,7 +17053,7 @@ function createReportTools(store) {
16295
17053
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
16296
17054
  };
16297
17055
  },
16298
- { annotations: { readOnly: true } }
17056
+ { annotations: { readOnlyHint: true } }
16299
17057
  ),
16300
17058
  tool8(
16301
17059
  "generate_epic_progress",
@@ -16380,7 +17138,7 @@ function createReportTools(store) {
16380
17138
  ]
16381
17139
  };
16382
17140
  },
16383
- { annotations: { readOnly: true } }
17141
+ { annotations: { readOnlyHint: true } }
16384
17142
  ),
16385
17143
  tool8(
16386
17144
  "generate_sprint_progress",
@@ -16425,7 +17183,8 @@ function createReportTools(store) {
16425
17183
  id: d.frontmatter.id,
16426
17184
  title: d.frontmatter.title,
16427
17185
  type: d.frontmatter.type,
16428
- status: d.frontmatter.status
17186
+ status: d.frontmatter.status,
17187
+ dueDate: d.frontmatter.dueDate
16429
17188
  }))
16430
17189
  }
16431
17190
  };
@@ -16434,7 +17193,7 @@ function createReportTools(store) {
16434
17193
  content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
16435
17194
  };
16436
17195
  },
16437
- { annotations: { readOnly: true } }
17196
+ { annotations: { readOnlyHint: true } }
16438
17197
  ),
16439
17198
  tool8(
16440
17199
  "generate_feature_progress",
@@ -16474,7 +17233,20 @@ function createReportTools(store) {
16474
17233
  content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
16475
17234
  };
16476
17235
  },
16477
- { annotations: { readOnly: true } }
17236
+ { annotations: { readOnlyHint: true } }
17237
+ ),
17238
+ tool8(
17239
+ "generate_health_report",
17240
+ "Generate a governance health check report covering artifact completeness and process health metrics",
17241
+ {},
17242
+ async () => {
17243
+ const metrics = collectHealthMetrics(store);
17244
+ const report = evaluateHealth("project", metrics);
17245
+ return {
17246
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
17247
+ };
17248
+ },
17249
+ { annotations: { readOnlyHint: true } }
16478
17250
  ),
16479
17251
  tool8(
16480
17252
  "save_report",
@@ -16536,7 +17308,7 @@ function createFeatureTools(store) {
16536
17308
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16537
17309
  };
16538
17310
  },
16539
- { annotations: { readOnly: true } }
17311
+ { annotations: { readOnlyHint: true } }
16540
17312
  ),
16541
17313
  tool9(
16542
17314
  "get_feature",
@@ -16563,7 +17335,7 @@ function createFeatureTools(store) {
16563
17335
  ]
16564
17336
  };
16565
17337
  },
16566
- { annotations: { readOnly: true } }
17338
+ { annotations: { readOnlyHint: true } }
16567
17339
  ),
16568
17340
  tool9(
16569
17341
  "create_feature",
@@ -16604,7 +17376,8 @@ function createFeatureTools(store) {
16604
17376
  status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
16605
17377
  content: external_exports.string().optional().describe("New content"),
16606
17378
  owner: external_exports.string().optional().describe("New owner"),
16607
- priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority")
17379
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
17380
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
16608
17381
  },
16609
17382
  async (args) => {
16610
17383
  const { id, content, ...updates } = args;
@@ -16654,7 +17427,7 @@ function createEpicTools(store) {
16654
17427
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16655
17428
  };
16656
17429
  },
16657
- { annotations: { readOnly: true } }
17430
+ { annotations: { readOnlyHint: true } }
16658
17431
  ),
16659
17432
  tool10(
16660
17433
  "get_epic",
@@ -16681,7 +17454,7 @@ function createEpicTools(store) {
16681
17454
  ]
16682
17455
  };
16683
17456
  },
16684
- { annotations: { readOnly: true } }
17457
+ { annotations: { readOnlyHint: true } }
16685
17458
  ),
16686
17459
  tool10(
16687
17460
  "create_epic",
@@ -16761,7 +17534,8 @@ function createEpicTools(store) {
16761
17534
  content: external_exports.string().optional().describe("New content"),
16762
17535
  owner: external_exports.string().optional().describe("New owner"),
16763
17536
  targetDate: external_exports.string().optional().describe("New target date"),
16764
- estimatedEffort: external_exports.string().optional().describe("New estimated effort")
17537
+ estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
17538
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
16765
17539
  },
16766
17540
  async (args) => {
16767
17541
  const { id, content, ...updates } = args;
@@ -16813,7 +17587,7 @@ function createContributionTools(store) {
16813
17587
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16814
17588
  };
16815
17589
  },
16816
- { annotations: { readOnly: true } }
17590
+ { annotations: { readOnlyHint: true } }
16817
17591
  ),
16818
17592
  tool11(
16819
17593
  "get_contribution",
@@ -16840,7 +17614,7 @@ function createContributionTools(store) {
16840
17614
  ]
16841
17615
  };
16842
17616
  },
16843
- { annotations: { readOnly: true } }
17617
+ { annotations: { readOnlyHint: true } }
16844
17618
  ),
16845
17619
  tool11(
16846
17620
  "create_contribution",
@@ -16925,7 +17699,7 @@ function createSprintTools(store) {
16925
17699
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16926
17700
  };
16927
17701
  },
16928
- { annotations: { readOnly: true } }
17702
+ { annotations: { readOnlyHint: true } }
16929
17703
  ),
16930
17704
  tool12(
16931
17705
  "get_sprint",
@@ -16952,7 +17726,7 @@ function createSprintTools(store) {
16952
17726
  ]
16953
17727
  };
16954
17728
  },
16955
- { annotations: { readOnly: true } }
17729
+ { annotations: { readOnlyHint: true } }
16956
17730
  ),
16957
17731
  tool12(
16958
17732
  "create_sprint",
@@ -17249,7 +18023,7 @@ function createSprintPlanningTools(store) {
17249
18023
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
17250
18024
  };
17251
18025
  },
17252
- { annotations: { readOnly: true } }
18026
+ { annotations: { readOnlyHint: true } }
17253
18027
  )
17254
18028
  ];
17255
18029
  }
@@ -17303,6 +18077,7 @@ var genericAgilePlugin = {
17303
18077
  - Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
17304
18078
  - Use priority levels (critical, high, medium, low) to communicate business value.
17305
18079
  - Tag features for categorization and cross-referencing.
18080
+ - Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
17306
18081
 
17307
18082
  **Contribution Tools:**
17308
18083
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -17333,6 +18108,7 @@ var genericAgilePlugin = {
17333
18108
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
17334
18109
  - Collaborate with the Delivery Manager on target dates and effort estimates.
17335
18110
  - Each epic should have a clear scope and definition of done.
18111
+ - Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
17336
18112
 
17337
18113
  **Contribution Tools:**
17338
18114
  - **list_contributions** / **get_contribution**: Browse and read contribution records.
@@ -17392,6 +18168,11 @@ var genericAgilePlugin = {
17392
18168
  - **generate_sprint_progress**: Progress report for a specific sprint or all sprints \u2014 shows linked epics with statuses, work items tagged \`sprint:SP-xxx\` grouped by status, and done/total completion %.
17393
18169
  - Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
17394
18170
 
18171
+ **Date Enforcement:**
18172
+ - 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.
18173
+ - When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
18174
+ - Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
18175
+
17395
18176
  **Sprint Workflow:**
17396
18177
  - Create sprints with clear goals and date boundaries.
17397
18178
  - Assign epics to sprints via linkedEpics.
@@ -17411,7 +18192,7 @@ var genericAgilePlugin = {
17411
18192
  **Sprints** (SP-xxx): Time-boxed iterations that group epics and work items with delivery dates. Sprints progress through planned \u2192 active \u2192 completed (or cancelled).
17412
18193
  **Meetings**: Meeting records with attendees, agendas, and notes.
17413
18194
 
17414
- **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags.
18195
+ **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags. Actions support a \`dueDate\` field for schedule tracking \u2014 actions with a past due date are automatically flagged as overdue in GAR reports. Use the \`sprints\` parameter on create_action/update_action to assign actions to sprints.
17415
18196
 
17416
18197
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
17417
18198
  - **create_meeting**: Record meetings with attendees, date, and agenda. The meeting date is required \u2014 extract it from the meeting content or ask the user if not found.
@@ -17457,7 +18238,7 @@ function createUseCaseTools(store) {
17457
18238
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17458
18239
  };
17459
18240
  },
17460
- { annotations: { readOnly: true } }
18241
+ { annotations: { readOnlyHint: true } }
17461
18242
  ),
17462
18243
  tool14(
17463
18244
  "get_use_case",
@@ -17484,7 +18265,7 @@ function createUseCaseTools(store) {
17484
18265
  ]
17485
18266
  };
17486
18267
  },
17487
- { annotations: { readOnly: true } }
18268
+ { annotations: { readOnlyHint: true } }
17488
18269
  ),
17489
18270
  tool14(
17490
18271
  "create_use_case",
@@ -17582,7 +18363,7 @@ function createTechAssessmentTools(store) {
17582
18363
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17583
18364
  };
17584
18365
  },
17585
- { annotations: { readOnly: true } }
18366
+ { annotations: { readOnlyHint: true } }
17586
18367
  ),
17587
18368
  tool15(
17588
18369
  "get_tech_assessment",
@@ -17609,7 +18390,7 @@ function createTechAssessmentTools(store) {
17609
18390
  ]
17610
18391
  };
17611
18392
  },
17612
- { annotations: { readOnly: true } }
18393
+ { annotations: { readOnlyHint: true } }
17613
18394
  ),
17614
18395
  tool15(
17615
18396
  "create_tech_assessment",
@@ -17743,7 +18524,7 @@ function createExtensionDesignTools(store) {
17743
18524
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17744
18525
  };
17745
18526
  },
17746
- { annotations: { readOnly: true } }
18527
+ { annotations: { readOnlyHint: true } }
17747
18528
  ),
17748
18529
  tool16(
17749
18530
  "get_extension_design",
@@ -17770,7 +18551,7 @@ function createExtensionDesignTools(store) {
17770
18551
  ]
17771
18552
  };
17772
18553
  },
17773
- { annotations: { readOnly: true } }
18554
+ { annotations: { readOnlyHint: true } }
17774
18555
  ),
17775
18556
  tool16(
17776
18557
  "create_extension_design",
@@ -17922,7 +18703,7 @@ function createAemReportTools(store) {
17922
18703
  ]
17923
18704
  };
17924
18705
  },
17925
- { annotations: { readOnly: true } }
18706
+ { annotations: { readOnlyHint: true } }
17926
18707
  ),
17927
18708
  tool17(
17928
18709
  "generate_tech_readiness",
@@ -17974,7 +18755,7 @@ function createAemReportTools(store) {
17974
18755
  ]
17975
18756
  };
17976
18757
  },
17977
- { annotations: { readOnly: true } }
18758
+ { annotations: { readOnlyHint: true } }
17978
18759
  ),
17979
18760
  tool17(
17980
18761
  "generate_phase_status",
@@ -18029,14 +18810,14 @@ function createAemReportTools(store) {
18029
18810
  ]
18030
18811
  };
18031
18812
  },
18032
- { annotations: { readOnly: true } }
18813
+ { annotations: { readOnlyHint: true } }
18033
18814
  )
18034
18815
  ];
18035
18816
  }
18036
18817
 
18037
18818
  // src/plugins/builtin/tools/aem-phase.ts
18038
- import * as fs4 from "fs";
18039
- import * as path4 from "path";
18819
+ import * as fs5 from "fs";
18820
+ import * as path5 from "path";
18040
18821
  import * as YAML2 from "yaml";
18041
18822
  import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
18042
18823
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
@@ -18061,7 +18842,7 @@ function createAemPhaseTools(store, marvinDir) {
18061
18842
  ]
18062
18843
  };
18063
18844
  },
18064
- { annotations: { readOnly: true } }
18845
+ { annotations: { readOnlyHint: true } }
18065
18846
  ),
18066
18847
  tool18(
18067
18848
  "advance_phase",
@@ -18141,8 +18922,8 @@ function createAemPhaseTools(store, marvinDir) {
18141
18922
  function readPhase(marvinDir) {
18142
18923
  if (!marvinDir) return void 0;
18143
18924
  try {
18144
- const configPath = path4.join(marvinDir, "config.yaml");
18145
- const raw = fs4.readFileSync(configPath, "utf-8");
18925
+ const configPath = path5.join(marvinDir, "config.yaml");
18926
+ const raw = fs5.readFileSync(configPath, "utf-8");
18146
18927
  const config2 = YAML2.parse(raw);
18147
18928
  const aem = config2.aem;
18148
18929
  return aem?.currentPhase;
@@ -18152,14 +18933,14 @@ function readPhase(marvinDir) {
18152
18933
  }
18153
18934
  function writePhase(marvinDir, phase) {
18154
18935
  if (!marvinDir) return;
18155
- const configPath = path4.join(marvinDir, "config.yaml");
18156
- const raw = fs4.readFileSync(configPath, "utf-8");
18936
+ const configPath = path5.join(marvinDir, "config.yaml");
18937
+ const raw = fs5.readFileSync(configPath, "utf-8");
18157
18938
  const config2 = YAML2.parse(raw);
18158
18939
  if (!config2.aem) {
18159
18940
  config2.aem = {};
18160
18941
  }
18161
18942
  config2.aem.currentPhase = phase;
18162
- fs4.writeFileSync(configPath, YAML2.stringify(config2), "utf-8");
18943
+ fs5.writeFileSync(configPath, YAML2.stringify(config2), "utf-8");
18163
18944
  }
18164
18945
  function getPhaseDescription(phase) {
18165
18946
  switch (phase) {
@@ -18327,8 +19108,8 @@ function getPluginPromptFragment(plugin, personaId) {
18327
19108
  }
18328
19109
 
18329
19110
  // src/skills/registry.ts
18330
- import * as fs5 from "fs";
18331
- import * as path5 from "path";
19111
+ import * as fs6 from "fs";
19112
+ import * as path6 from "path";
18332
19113
  import { fileURLToPath } from "url";
18333
19114
  import * as YAML3 from "yaml";
18334
19115
  import matter2 from "gray-matter";
@@ -18381,8 +19162,8 @@ var JiraClient = class {
18381
19162
  this.baseUrl = `https://${config2.host}/rest/api/2`;
18382
19163
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
18383
19164
  }
18384
- async request(path18, method = "GET", body) {
18385
- const url2 = `${this.baseUrl}${path18}`;
19165
+ async request(path20, method = "GET", body) {
19166
+ const url2 = `${this.baseUrl}${path20}`;
18386
19167
  const headers = {
18387
19168
  Authorization: this.authHeader,
18388
19169
  "Content-Type": "application/json",
@@ -18396,7 +19177,7 @@ var JiraClient = class {
18396
19177
  if (!response.ok) {
18397
19178
  const text = await response.text().catch(() => "");
18398
19179
  throw new Error(
18399
- `Jira API error ${response.status} ${method} ${path18}: ${text}`
19180
+ `Jira API error ${response.status} ${method} ${path20}: ${text}`
18400
19181
  );
18401
19182
  }
18402
19183
  if (response.status === 204) return void 0;
@@ -18506,7 +19287,7 @@ function createJiraTools(store) {
18506
19287
  content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
18507
19288
  };
18508
19289
  },
18509
- { annotations: { readOnly: true } }
19290
+ { annotations: { readOnlyHint: true } }
18510
19291
  ),
18511
19292
  tool19(
18512
19293
  "get_jira_issue",
@@ -18538,7 +19319,7 @@ function createJiraTools(store) {
18538
19319
  ]
18539
19320
  };
18540
19321
  },
18541
- { annotations: { readOnly: true } }
19322
+ { annotations: { readOnlyHint: true } }
18542
19323
  ),
18543
19324
  // --- Jira → Local tools ---
18544
19325
  tool19(
@@ -18876,13 +19657,13 @@ var GOVERNANCE_TOOL_NAMES = [
18876
19657
  ];
18877
19658
  function getBuiltinSkillsDir() {
18878
19659
  const thisFile = fileURLToPath(import.meta.url);
18879
- return path5.join(path5.dirname(thisFile), "builtin");
19660
+ return path6.join(path6.dirname(thisFile), "builtin");
18880
19661
  }
18881
19662
  function loadSkillFromDirectory(dirPath) {
18882
- const skillMdPath = path5.join(dirPath, "SKILL.md");
18883
- if (!fs5.existsSync(skillMdPath)) return void 0;
19663
+ const skillMdPath = path6.join(dirPath, "SKILL.md");
19664
+ if (!fs6.existsSync(skillMdPath)) return void 0;
18884
19665
  try {
18885
- const raw = fs5.readFileSync(skillMdPath, "utf-8");
19666
+ const raw = fs6.readFileSync(skillMdPath, "utf-8");
18886
19667
  const { data, content } = matter2(raw);
18887
19668
  if (!data.name || !data.description) return void 0;
18888
19669
  const metadata = data.metadata ?? {};
@@ -18893,13 +19674,13 @@ function loadSkillFromDirectory(dirPath) {
18893
19674
  if (wildcardPrompt) {
18894
19675
  promptFragments["*"] = wildcardPrompt;
18895
19676
  }
18896
- const personasDir = path5.join(dirPath, "personas");
18897
- if (fs5.existsSync(personasDir)) {
19677
+ const personasDir = path6.join(dirPath, "personas");
19678
+ if (fs6.existsSync(personasDir)) {
18898
19679
  try {
18899
- for (const file2 of fs5.readdirSync(personasDir)) {
19680
+ for (const file2 of fs6.readdirSync(personasDir)) {
18900
19681
  if (!file2.endsWith(".md")) continue;
18901
19682
  const personaId = file2.replace(/\.md$/, "");
18902
- const personaPrompt = fs5.readFileSync(path5.join(personasDir, file2), "utf-8").trim();
19683
+ const personaPrompt = fs6.readFileSync(path6.join(personasDir, file2), "utf-8").trim();
18903
19684
  if (personaPrompt) {
18904
19685
  promptFragments[personaId] = personaPrompt;
18905
19686
  }
@@ -18908,10 +19689,10 @@ function loadSkillFromDirectory(dirPath) {
18908
19689
  }
18909
19690
  }
18910
19691
  let actions;
18911
- const actionsPath = path5.join(dirPath, "actions.yaml");
18912
- if (fs5.existsSync(actionsPath)) {
19692
+ const actionsPath = path6.join(dirPath, "actions.yaml");
19693
+ if (fs6.existsSync(actionsPath)) {
18913
19694
  try {
18914
- const actionsRaw = fs5.readFileSync(actionsPath, "utf-8");
19695
+ const actionsRaw = fs6.readFileSync(actionsPath, "utf-8");
18915
19696
  actions = YAML3.parse(actionsRaw);
18916
19697
  } catch {
18917
19698
  }
@@ -18938,10 +19719,10 @@ function loadAllSkills(marvinDir) {
18938
19719
  }
18939
19720
  try {
18940
19721
  const builtinDir = getBuiltinSkillsDir();
18941
- if (fs5.existsSync(builtinDir)) {
18942
- for (const entry of fs5.readdirSync(builtinDir)) {
18943
- const entryPath = path5.join(builtinDir, entry);
18944
- if (!fs5.statSync(entryPath).isDirectory()) continue;
19722
+ if (fs6.existsSync(builtinDir)) {
19723
+ for (const entry of fs6.readdirSync(builtinDir)) {
19724
+ const entryPath = path6.join(builtinDir, entry);
19725
+ if (!fs6.statSync(entryPath).isDirectory()) continue;
18945
19726
  if (skills.has(entry)) continue;
18946
19727
  const skill = loadSkillFromDirectory(entryPath);
18947
19728
  if (skill) skills.set(skill.id, skill);
@@ -18950,18 +19731,18 @@ function loadAllSkills(marvinDir) {
18950
19731
  } catch {
18951
19732
  }
18952
19733
  if (marvinDir) {
18953
- const skillsDir = path5.join(marvinDir, "skills");
18954
- if (fs5.existsSync(skillsDir)) {
19734
+ const skillsDir = path6.join(marvinDir, "skills");
19735
+ if (fs6.existsSync(skillsDir)) {
18955
19736
  let entries;
18956
19737
  try {
18957
- entries = fs5.readdirSync(skillsDir);
19738
+ entries = fs6.readdirSync(skillsDir);
18958
19739
  } catch {
18959
19740
  entries = [];
18960
19741
  }
18961
19742
  for (const entry of entries) {
18962
- const entryPath = path5.join(skillsDir, entry);
19743
+ const entryPath = path6.join(skillsDir, entry);
18963
19744
  try {
18964
- if (fs5.statSync(entryPath).isDirectory()) {
19745
+ if (fs6.statSync(entryPath).isDirectory()) {
18965
19746
  const skill = loadSkillFromDirectory(entryPath);
18966
19747
  if (skill) skills.set(skill.id, skill);
18967
19748
  continue;
@@ -18971,7 +19752,7 @@ function loadAllSkills(marvinDir) {
18971
19752
  }
18972
19753
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
18973
19754
  try {
18974
- const raw = fs5.readFileSync(entryPath, "utf-8");
19755
+ const raw = fs6.readFileSync(entryPath, "utf-8");
18975
19756
  const parsed = YAML3.parse(raw);
18976
19757
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
18977
19758
  const skill = {
@@ -19076,12 +19857,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
19076
19857
  return agents;
19077
19858
  }
19078
19859
  function migrateYamlToSkillMd(yamlPath, outputDir) {
19079
- const raw = fs5.readFileSync(yamlPath, "utf-8");
19860
+ const raw = fs6.readFileSync(yamlPath, "utf-8");
19080
19861
  const parsed = YAML3.parse(raw);
19081
19862
  if (!parsed?.id || !parsed?.name) {
19082
19863
  throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
19083
19864
  }
19084
- fs5.mkdirSync(outputDir, { recursive: true });
19865
+ fs6.mkdirSync(outputDir, { recursive: true });
19085
19866
  const frontmatter = {
19086
19867
  name: parsed.id,
19087
19868
  description: parsed.description ?? ""
@@ -19095,15 +19876,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
19095
19876
  const skillMd = matter2.stringify(wildcardPrompt ? `
19096
19877
  ${wildcardPrompt}
19097
19878
  ` : "\n", frontmatter);
19098
- fs5.writeFileSync(path5.join(outputDir, "SKILL.md"), skillMd, "utf-8");
19879
+ fs6.writeFileSync(path6.join(outputDir, "SKILL.md"), skillMd, "utf-8");
19099
19880
  if (promptFragments) {
19100
19881
  const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
19101
19882
  if (personaKeys.length > 0) {
19102
- const personasDir = path5.join(outputDir, "personas");
19103
- fs5.mkdirSync(personasDir, { recursive: true });
19883
+ const personasDir = path6.join(outputDir, "personas");
19884
+ fs6.mkdirSync(personasDir, { recursive: true });
19104
19885
  for (const personaId of personaKeys) {
19105
- fs5.writeFileSync(
19106
- path5.join(personasDir, `${personaId}.md`),
19886
+ fs6.writeFileSync(
19887
+ path6.join(personasDir, `${personaId}.md`),
19107
19888
  `${promptFragments[personaId]}
19108
19889
  `,
19109
19890
  "utf-8"
@@ -19113,8 +19894,8 @@ ${wildcardPrompt}
19113
19894
  }
19114
19895
  const actions = parsed.actions;
19115
19896
  if (actions && actions.length > 0) {
19116
- fs5.writeFileSync(
19117
- path5.join(outputDir, "actions.yaml"),
19897
+ fs6.writeFileSync(
19898
+ path6.join(outputDir, "actions.yaml"),
19118
19899
  YAML3.stringify(actions),
19119
19900
  "utf-8"
19120
19901
  );
@@ -19269,7 +20050,7 @@ function createWebTools(store, projectName, navGroups) {
19269
20050
  content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
19270
20051
  };
19271
20052
  },
19272
- { annotations: { readOnly: true } }
20053
+ { annotations: { readOnlyHint: true } }
19273
20054
  ),
19274
20055
  tool20(
19275
20056
  "get_dashboard_overview",
@@ -19291,7 +20072,7 @@ function createWebTools(store, projectName, navGroups) {
19291
20072
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19292
20073
  };
19293
20074
  },
19294
- { annotations: { readOnly: true } }
20075
+ { annotations: { readOnlyHint: true } }
19295
20076
  ),
19296
20077
  tool20(
19297
20078
  "get_dashboard_gar",
@@ -19303,7 +20084,7 @@ function createWebTools(store, projectName, navGroups) {
19303
20084
  content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
19304
20085
  };
19305
20086
  },
19306
- { annotations: { readOnly: true } }
20087
+ { annotations: { readOnlyHint: true } }
19307
20088
  ),
19308
20089
  tool20(
19309
20090
  "get_dashboard_board",
@@ -19331,7 +20112,7 @@ function createWebTools(store, projectName, navGroups) {
19331
20112
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19332
20113
  };
19333
20114
  },
19334
- { annotations: { readOnly: true } }
20115
+ { annotations: { readOnlyHint: true } }
19335
20116
  )
19336
20117
  ];
19337
20118
  }
@@ -19357,8 +20138,8 @@ function createMarvinMcpServer(store, options) {
19357
20138
  }
19358
20139
 
19359
20140
  // src/agent/session.ts
19360
- import * as fs8 from "fs";
19361
- import * as path8 from "path";
20141
+ import * as fs9 from "fs";
20142
+ import * as path9 from "path";
19362
20143
  import * as readline from "readline";
19363
20144
  import chalk from "chalk";
19364
20145
  import ora from "ora";
@@ -19367,13 +20148,13 @@ import {
19367
20148
  } from "@anthropic-ai/claude-agent-sdk";
19368
20149
 
19369
20150
  // src/storage/session-store.ts
19370
- import * as fs6 from "fs";
19371
- import * as path6 from "path";
20151
+ import * as fs7 from "fs";
20152
+ import * as path7 from "path";
19372
20153
  import * as YAML4 from "yaml";
19373
20154
  var SessionStore = class {
19374
20155
  filePath;
19375
20156
  constructor(marvinDir) {
19376
- this.filePath = path6.join(marvinDir, "sessions.yaml");
20157
+ this.filePath = path7.join(marvinDir, "sessions.yaml");
19377
20158
  }
19378
20159
  list() {
19379
20160
  const entries = this.load();
@@ -19414,9 +20195,9 @@ var SessionStore = class {
19414
20195
  this.write(entries);
19415
20196
  }
19416
20197
  load() {
19417
- if (!fs6.existsSync(this.filePath)) return [];
20198
+ if (!fs7.existsSync(this.filePath)) return [];
19418
20199
  try {
19419
- const raw = fs6.readFileSync(this.filePath, "utf-8");
20200
+ const raw = fs7.readFileSync(this.filePath, "utf-8");
19420
20201
  const parsed = YAML4.parse(raw);
19421
20202
  if (!Array.isArray(parsed)) return [];
19422
20203
  return parsed;
@@ -19425,11 +20206,11 @@ var SessionStore = class {
19425
20206
  }
19426
20207
  }
19427
20208
  write(entries) {
19428
- const dir = path6.dirname(this.filePath);
19429
- if (!fs6.existsSync(dir)) {
19430
- fs6.mkdirSync(dir, { recursive: true });
20209
+ const dir = path7.dirname(this.filePath);
20210
+ if (!fs7.existsSync(dir)) {
20211
+ fs7.mkdirSync(dir, { recursive: true });
19431
20212
  }
19432
- fs6.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
20213
+ fs7.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
19433
20214
  }
19434
20215
  };
19435
20216
 
@@ -19465,8 +20246,8 @@ function slugify3(text) {
19465
20246
  }
19466
20247
 
19467
20248
  // src/sources/manifest.ts
19468
- import * as fs7 from "fs";
19469
- import * as path7 from "path";
20249
+ import * as fs8 from "fs";
20250
+ import * as path8 from "path";
19470
20251
  import * as crypto from "crypto";
19471
20252
  import * as YAML5 from "yaml";
19472
20253
  var MANIFEST_FILE = ".manifest.yaml";
@@ -19479,37 +20260,37 @@ var SourceManifestManager = class {
19479
20260
  manifestPath;
19480
20261
  sourcesDir;
19481
20262
  constructor(marvinDir) {
19482
- this.sourcesDir = path7.join(marvinDir, "sources");
19483
- this.manifestPath = path7.join(this.sourcesDir, MANIFEST_FILE);
20263
+ this.sourcesDir = path8.join(marvinDir, "sources");
20264
+ this.manifestPath = path8.join(this.sourcesDir, MANIFEST_FILE);
19484
20265
  this.manifest = this.load();
19485
20266
  }
19486
20267
  load() {
19487
- if (!fs7.existsSync(this.manifestPath)) {
20268
+ if (!fs8.existsSync(this.manifestPath)) {
19488
20269
  return emptyManifest();
19489
20270
  }
19490
- const raw = fs7.readFileSync(this.manifestPath, "utf-8");
20271
+ const raw = fs8.readFileSync(this.manifestPath, "utf-8");
19491
20272
  const parsed = YAML5.parse(raw);
19492
20273
  return parsed ?? emptyManifest();
19493
20274
  }
19494
20275
  save() {
19495
- fs7.mkdirSync(this.sourcesDir, { recursive: true });
19496
- fs7.writeFileSync(this.manifestPath, YAML5.stringify(this.manifest), "utf-8");
20276
+ fs8.mkdirSync(this.sourcesDir, { recursive: true });
20277
+ fs8.writeFileSync(this.manifestPath, YAML5.stringify(this.manifest), "utf-8");
19497
20278
  }
19498
20279
  scan() {
19499
20280
  const added = [];
19500
20281
  const changed = [];
19501
20282
  const removed = [];
19502
- if (!fs7.existsSync(this.sourcesDir)) {
20283
+ if (!fs8.existsSync(this.sourcesDir)) {
19503
20284
  return { added, changed, removed };
19504
20285
  }
19505
20286
  const onDisk = new Set(
19506
- fs7.readdirSync(this.sourcesDir).filter((f) => {
19507
- const ext = path7.extname(f).toLowerCase();
20287
+ fs8.readdirSync(this.sourcesDir).filter((f) => {
20288
+ const ext = path8.extname(f).toLowerCase();
19508
20289
  return SOURCE_EXTENSIONS.includes(ext);
19509
20290
  })
19510
20291
  );
19511
20292
  for (const fileName of onDisk) {
19512
- const filePath = path7.join(this.sourcesDir, fileName);
20293
+ const filePath = path8.join(this.sourcesDir, fileName);
19513
20294
  const hash2 = this.hashFile(filePath);
19514
20295
  const existing = this.manifest.files[fileName];
19515
20296
  if (!existing) {
@@ -19572,7 +20353,7 @@ var SourceManifestManager = class {
19572
20353
  this.save();
19573
20354
  }
19574
20355
  hashFile(filePath) {
19575
- const content = fs7.readFileSync(filePath);
20356
+ const content = fs8.readFileSync(filePath);
19576
20357
  return crypto.createHash("sha256").update(content).digest("hex");
19577
20358
  }
19578
20359
  };
@@ -19587,8 +20368,8 @@ async function startSession(options) {
19587
20368
  const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
19588
20369
  const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
19589
20370
  const sessionStore = new SessionStore(marvinDir);
19590
- const sourcesDir = path8.join(marvinDir, "sources");
19591
- const hasSourcesDir = fs8.existsSync(sourcesDir);
20371
+ const sourcesDir = path9.join(marvinDir, "sources");
20372
+ const hasSourcesDir = fs9.existsSync(sourcesDir);
19592
20373
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
19593
20374
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
19594
20375
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
@@ -19611,7 +20392,7 @@ async function startSession(options) {
19611
20392
  projectName: config2.project.name,
19612
20393
  navGroups
19613
20394
  });
19614
- const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment);
20395
+ const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment, marvinDir);
19615
20396
  let existingSession;
19616
20397
  if (options.sessionName) {
19617
20398
  existingSession = sessionStore.get(options.sessionName);
@@ -19797,8 +20578,8 @@ Session ended with error: ${message.subtype}`));
19797
20578
  }
19798
20579
 
19799
20580
  // src/mcp/stdio-server.ts
19800
- import * as fs9 from "fs";
19801
- import * as path9 from "path";
20581
+ import * as fs10 from "fs";
20582
+ import * as path10 from "path";
19802
20583
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19803
20584
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19804
20585
 
@@ -20029,7 +20810,7 @@ ${summaries}`
20029
20810
  content: [{ type: "text", text: guidance }]
20030
20811
  };
20031
20812
  },
20032
- { annotations: { readOnly: true } }
20813
+ { annotations: { readOnlyHint: true } }
20033
20814
  )
20034
20815
  ];
20035
20816
  }
@@ -20096,8 +20877,8 @@ function collectTools(marvinDir) {
20096
20877
  const plugin = resolvePlugin(config2.methodology);
20097
20878
  const registrations = plugin?.documentTypeRegistrations ?? [];
20098
20879
  const store = new DocumentStore(marvinDir, registrations);
20099
- const sourcesDir = path9.join(marvinDir, "sources");
20100
- const hasSourcesDir = fs9.existsSync(sourcesDir);
20880
+ const sourcesDir = path10.join(marvinDir, "sources");
20881
+ const hasSourcesDir = fs10.existsSync(sourcesDir);
20101
20882
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20102
20883
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20103
20884
  const sessionStore = new SessionStore(marvinDir);
@@ -20105,7 +20886,7 @@ function collectTools(marvinDir) {
20105
20886
  const allSkillIds = [...allSkills.keys()];
20106
20887
  const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
20107
20888
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
20108
- const projectRoot = path9.dirname(marvinDir);
20889
+ const projectRoot = path10.dirname(marvinDir);
20109
20890
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
20110
20891
  return [
20111
20892
  ...createDecisionTools(store),
@@ -20165,11 +20946,63 @@ async function startStdioServer(options) {
20165
20946
  import { Command } from "commander";
20166
20947
 
20167
20948
  // src/cli/commands/init.ts
20168
- import * as fs10 from "fs";
20169
- import * as path10 from "path";
20949
+ import * as fs11 from "fs";
20950
+ import * as path11 from "path";
20170
20951
  import * as YAML6 from "yaml";
20171
20952
  import chalk2 from "chalk";
20172
20953
  import { input, confirm, select } from "@inquirer/prompts";
20954
+
20955
+ // src/templates/claude-md.ts
20956
+ function getDefaultClaudeMdContent(projectName) {
20957
+ return `# Marvin \u2014 Project Instructions for "${projectName}"
20958
+
20959
+ You are **Marvin**, an AI-powered product development assistant.
20960
+ You operate as one of three personas \u2014 stay in role and suggest switching when a question falls outside your scope.
20961
+
20962
+ ## Personas
20963
+
20964
+ | Persona | Short | Focus |
20965
+ |---------|-------|-------|
20966
+ | Product Owner | po | Vision, backlog, requirements, features, acceptance criteria |
20967
+ | Delivery Manager | dm | Planning, risks, actions, timelines, sprints, status |
20968
+ | Tech Lead | tl | Architecture, trade-offs, technical decisions, code quality |
20969
+
20970
+ ## Proactive Governance
20971
+
20972
+ When conversation implies a commitment, risk, or open question, **suggest creating the matching artifact**:
20973
+ - A decision was made \u2192 offer to create a **Decision (D-xxx)**
20974
+ - Someone committed to a task \u2192 offer an **Action (A-xxx)** with owner and due date
20975
+ - An unanswered question surfaced \u2192 offer a **Question (Q-xxx)**
20976
+ - A new capability is discussed \u2192 offer a **Feature (F-xxx)**
20977
+ - Implementation scope is agreed \u2192 offer an **Epic (E-xxx)** linked to a feature
20978
+ - Work is being time-boxed \u2192 offer a **Sprint (SP-xxx)**
20979
+
20980
+ ## Insights
20981
+
20982
+ Proactively flag:
20983
+ - Overdue actions or unresolved questions
20984
+ - Decisions without rationale or linked features
20985
+ - Features without linked epics
20986
+ - Risks mentioned but not tracked
20987
+ - When a risk is resolved \u2192 remove the "risk" tag and add "risk-mitigated"
20988
+
20989
+ ## Tool Usage
20990
+
20991
+ - **Search before creating** \u2014 avoid duplicate artifacts
20992
+ - **Reference IDs** (e.g. D-001, A-003) when discussing existing items
20993
+ - **Link artifacts** \u2014 epics to features, actions to decisions, etc.
20994
+ - Use \`search_documents\` to find related context before answering
20995
+
20996
+ ## Communication Style
20997
+
20998
+ - Be concise and structured
20999
+ - State assumptions explicitly
21000
+ - Use bullet points and tables where they aid clarity
21001
+ - When uncertain, ask a clarifying question rather than guessing
21002
+ `;
21003
+ }
21004
+
21005
+ // src/cli/commands/init.ts
20173
21006
  async function initCommand() {
20174
21007
  const cwd = process.cwd();
20175
21008
  if (isMarvinProject(cwd)) {
@@ -20180,7 +21013,7 @@ async function initCommand() {
20180
21013
  }
20181
21014
  const projectName = await input({
20182
21015
  message: "Project name:",
20183
- default: path10.basename(cwd)
21016
+ default: path11.basename(cwd)
20184
21017
  });
20185
21018
  const methodology = await select({
20186
21019
  message: "Methodology:",
@@ -20192,21 +21025,21 @@ async function initCommand() {
20192
21025
  });
20193
21026
  const plugin = resolvePlugin(methodology);
20194
21027
  const registrations = plugin?.documentTypeRegistrations ?? [];
20195
- const marvinDir = path10.join(cwd, ".marvin");
21028
+ const marvinDir = path11.join(cwd, ".marvin");
20196
21029
  const dirs = [
20197
21030
  marvinDir,
20198
- path10.join(marvinDir, "templates"),
20199
- path10.join(marvinDir, "docs", "decisions"),
20200
- path10.join(marvinDir, "docs", "actions"),
20201
- path10.join(marvinDir, "docs", "questions"),
20202
- path10.join(marvinDir, "sources"),
20203
- path10.join(marvinDir, "skills")
21031
+ path11.join(marvinDir, "templates"),
21032
+ path11.join(marvinDir, "docs", "decisions"),
21033
+ path11.join(marvinDir, "docs", "actions"),
21034
+ path11.join(marvinDir, "docs", "questions"),
21035
+ path11.join(marvinDir, "sources"),
21036
+ path11.join(marvinDir, "skills")
20204
21037
  ];
20205
21038
  for (const reg of registrations) {
20206
- dirs.push(path10.join(marvinDir, "docs", reg.dirName));
21039
+ dirs.push(path11.join(marvinDir, "docs", reg.dirName));
20207
21040
  }
20208
21041
  for (const dir of dirs) {
20209
- fs10.mkdirSync(dir, { recursive: true });
21042
+ fs11.mkdirSync(dir, { recursive: true });
20210
21043
  }
20211
21044
  const config2 = {
20212
21045
  name: projectName,
@@ -20220,16 +21053,22 @@ async function initCommand() {
20220
21053
  if (methodology === "sap-aem") {
20221
21054
  config2.aem = { currentPhase: "assess-use-case" };
20222
21055
  }
20223
- fs10.writeFileSync(
20224
- path10.join(marvinDir, "config.yaml"),
21056
+ fs11.writeFileSync(
21057
+ path11.join(marvinDir, "config.yaml"),
20225
21058
  YAML6.stringify(config2),
20226
21059
  "utf-8"
20227
21060
  );
21061
+ fs11.writeFileSync(
21062
+ path11.join(marvinDir, "CLAUDE.md"),
21063
+ getDefaultClaudeMdContent(projectName),
21064
+ "utf-8"
21065
+ );
20228
21066
  console.log(chalk2.green(`
20229
21067
  Initialized Marvin project "${projectName}" in ${cwd}`));
20230
21068
  console.log(chalk2.dim(`Methodology: ${plugin?.name ?? methodology}`));
20231
21069
  console.log(chalk2.dim("\nCreated:"));
20232
21070
  console.log(chalk2.dim(" .marvin/config.yaml"));
21071
+ console.log(chalk2.dim(" .marvin/CLAUDE.md"));
20233
21072
  console.log(chalk2.dim(" .marvin/docs/decisions/"));
20234
21073
  console.log(chalk2.dim(" .marvin/docs/actions/"));
20235
21074
  console.log(chalk2.dim(" .marvin/docs/questions/"));
@@ -20247,18 +21086,18 @@ Initialized Marvin project "${projectName}" in ${cwd}`));
20247
21086
  const sourceDir = await input({
20248
21087
  message: "Path to directory containing source documents:"
20249
21088
  });
20250
- const resolvedDir = path10.resolve(sourceDir);
20251
- if (fs10.existsSync(resolvedDir) && fs10.statSync(resolvedDir).isDirectory()) {
21089
+ const resolvedDir = path11.resolve(sourceDir);
21090
+ if (fs11.existsSync(resolvedDir) && fs11.statSync(resolvedDir).isDirectory()) {
20252
21091
  const sourceExts = [".pdf", ".md", ".txt"];
20253
- const files = fs10.readdirSync(resolvedDir).filter((f) => {
20254
- const ext = path10.extname(f).toLowerCase();
21092
+ const files = fs11.readdirSync(resolvedDir).filter((f) => {
21093
+ const ext = path11.extname(f).toLowerCase();
20255
21094
  return sourceExts.includes(ext);
20256
21095
  });
20257
21096
  let copied = 0;
20258
21097
  for (const file2 of files) {
20259
- const src = path10.join(resolvedDir, file2);
20260
- const dest = path10.join(marvinDir, "sources", file2);
20261
- fs10.copyFileSync(src, dest);
21098
+ const src = path11.join(resolvedDir, file2);
21099
+ const dest = path11.join(marvinDir, "sources", file2);
21100
+ fs11.copyFileSync(src, dest);
20262
21101
  copied++;
20263
21102
  }
20264
21103
  if (copied > 0) {
@@ -20548,13 +21387,13 @@ async function setApiKey() {
20548
21387
  }
20549
21388
 
20550
21389
  // src/cli/commands/ingest.ts
20551
- import * as fs12 from "fs";
20552
- import * as path12 from "path";
21390
+ import * as fs13 from "fs";
21391
+ import * as path13 from "path";
20553
21392
  import chalk8 from "chalk";
20554
21393
 
20555
21394
  // src/sources/ingest.ts
20556
- import * as fs11 from "fs";
20557
- import * as path11 from "path";
21395
+ import * as fs12 from "fs";
21396
+ import * as path12 from "path";
20558
21397
  import chalk7 from "chalk";
20559
21398
  import ora2 from "ora";
20560
21399
  import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
@@ -20657,15 +21496,15 @@ async function ingestFile(options) {
20657
21496
  const persona = getPersona(personaId);
20658
21497
  const manifest = new SourceManifestManager(marvinDir);
20659
21498
  const sourcesDir = manifest.sourcesDir;
20660
- const filePath = path11.join(sourcesDir, fileName);
20661
- if (!fs11.existsSync(filePath)) {
21499
+ const filePath = path12.join(sourcesDir, fileName);
21500
+ if (!fs12.existsSync(filePath)) {
20662
21501
  throw new Error(`Source file not found: ${filePath}`);
20663
21502
  }
20664
- const ext = path11.extname(fileName).toLowerCase();
21503
+ const ext = path12.extname(fileName).toLowerCase();
20665
21504
  const isPdf = ext === ".pdf";
20666
21505
  let fileContent = null;
20667
21506
  if (!isPdf) {
20668
- fileContent = fs11.readFileSync(filePath, "utf-8");
21507
+ fileContent = fs12.readFileSync(filePath, "utf-8");
20669
21508
  }
20670
21509
  const store = new DocumentStore(marvinDir);
20671
21510
  const createdArtifacts = [];
@@ -20768,9 +21607,9 @@ Ingest ended with error: ${message.subtype}`)
20768
21607
  async function ingestCommand(file2, options) {
20769
21608
  const project = loadProject();
20770
21609
  const marvinDir = project.marvinDir;
20771
- const sourcesDir = path12.join(marvinDir, "sources");
20772
- if (!fs12.existsSync(sourcesDir)) {
20773
- fs12.mkdirSync(sourcesDir, { recursive: true });
21610
+ const sourcesDir = path13.join(marvinDir, "sources");
21611
+ if (!fs13.existsSync(sourcesDir)) {
21612
+ fs13.mkdirSync(sourcesDir, { recursive: true });
20774
21613
  }
20775
21614
  const manifest = new SourceManifestManager(marvinDir);
20776
21615
  manifest.scan();
@@ -20781,8 +21620,8 @@ async function ingestCommand(file2, options) {
20781
21620
  return;
20782
21621
  }
20783
21622
  if (file2) {
20784
- const filePath = path12.join(sourcesDir, file2);
20785
- if (!fs12.existsSync(filePath)) {
21623
+ const filePath = path13.join(sourcesDir, file2);
21624
+ if (!fs13.existsSync(filePath)) {
20786
21625
  console.log(chalk8.red(`Source file not found: ${file2}`));
20787
21626
  console.log(chalk8.dim(`Expected at: ${filePath}`));
20788
21627
  console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
@@ -20849,7 +21688,7 @@ import ora3 from "ora";
20849
21688
  import { input as input3 } from "@inquirer/prompts";
20850
21689
 
20851
21690
  // src/git/repository.ts
20852
- import * as path13 from "path";
21691
+ import * as path14 from "path";
20853
21692
  import simpleGit from "simple-git";
20854
21693
  var MARVIN_GITIGNORE = `node_modules/
20855
21694
  .DS_Store
@@ -20869,7 +21708,7 @@ var DIR_TYPE_LABELS = {
20869
21708
  function buildCommitMessage(files) {
20870
21709
  const counts = /* @__PURE__ */ new Map();
20871
21710
  for (const f of files) {
20872
- const parts2 = f.split(path13.sep).join("/").split("/");
21711
+ const parts2 = f.split(path14.sep).join("/").split("/");
20873
21712
  const docsIdx = parts2.indexOf("docs");
20874
21713
  if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
20875
21714
  const dirName = parts2[docsIdx + 1];
@@ -20909,9 +21748,9 @@ var MarvinGit = class {
20909
21748
  );
20910
21749
  }
20911
21750
  await this.git.init();
20912
- const { writeFileSync: writeFileSync9 } = await import("fs");
20913
- writeFileSync9(
20914
- path13.join(this.marvinDir, ".gitignore"),
21751
+ const { writeFileSync: writeFileSync10 } = await import("fs");
21752
+ writeFileSync10(
21753
+ path14.join(this.marvinDir, ".gitignore"),
20915
21754
  MARVIN_GITIGNORE,
20916
21755
  "utf-8"
20917
21756
  );
@@ -21031,9 +21870,9 @@ var MarvinGit = class {
21031
21870
  }
21032
21871
  }
21033
21872
  static async clone(url2, targetDir) {
21034
- const marvinDir = path13.join(targetDir, ".marvin");
21035
- const { existsSync: existsSync16 } = await import("fs");
21036
- if (existsSync16(marvinDir)) {
21873
+ const marvinDir = path14.join(targetDir, ".marvin");
21874
+ const { existsSync: existsSync17 } = await import("fs");
21875
+ if (existsSync17(marvinDir)) {
21037
21876
  throw new GitSyncError(
21038
21877
  `.marvin/ already exists at ${targetDir}. Remove it first or choose a different directory.`
21039
21878
  );
@@ -21217,8 +22056,8 @@ async function serveCommand() {
21217
22056
  }
21218
22057
 
21219
22058
  // src/cli/commands/skills.ts
21220
- import * as fs13 from "fs";
21221
- import * as path14 from "path";
22059
+ import * as fs14 from "fs";
22060
+ import * as path15 from "path";
21222
22061
  import * as YAML7 from "yaml";
21223
22062
  import matter3 from "gray-matter";
21224
22063
  import chalk10 from "chalk";
@@ -21324,14 +22163,14 @@ async function skillsRemoveCommand(skillId, options) {
21324
22163
  }
21325
22164
  async function skillsCreateCommand(name) {
21326
22165
  const project = loadProject();
21327
- const skillsDir = path14.join(project.marvinDir, "skills");
21328
- fs13.mkdirSync(skillsDir, { recursive: true });
21329
- const skillDir = path14.join(skillsDir, name);
21330
- if (fs13.existsSync(skillDir)) {
22166
+ const skillsDir = path15.join(project.marvinDir, "skills");
22167
+ fs14.mkdirSync(skillsDir, { recursive: true });
22168
+ const skillDir = path15.join(skillsDir, name);
22169
+ if (fs14.existsSync(skillDir)) {
21331
22170
  console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
21332
22171
  return;
21333
22172
  }
21334
- fs13.mkdirSync(skillDir, { recursive: true });
22173
+ fs14.mkdirSync(skillDir, { recursive: true });
21335
22174
  const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
21336
22175
  const frontmatter = {
21337
22176
  name,
@@ -21345,7 +22184,7 @@ async function skillsCreateCommand(name) {
21345
22184
  You have the **${displayName}** skill.
21346
22185
  `;
21347
22186
  const skillMd = matter3.stringify(body, frontmatter);
21348
- fs13.writeFileSync(path14.join(skillDir, "SKILL.md"), skillMd, "utf-8");
22187
+ fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
21349
22188
  const actions = [
21350
22189
  {
21351
22190
  id: "run",
@@ -21355,7 +22194,7 @@ You have the **${displayName}** skill.
21355
22194
  maxTurns: 5
21356
22195
  }
21357
22196
  ];
21358
- fs13.writeFileSync(path14.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
22197
+ fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
21359
22198
  console.log(chalk10.green(`Created skill: ${skillDir}/`));
21360
22199
  console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
21361
22200
  console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
@@ -21363,14 +22202,14 @@ You have the **${displayName}** skill.
21363
22202
  }
21364
22203
  async function skillsMigrateCommand() {
21365
22204
  const project = loadProject();
21366
- const skillsDir = path14.join(project.marvinDir, "skills");
21367
- if (!fs13.existsSync(skillsDir)) {
22205
+ const skillsDir = path15.join(project.marvinDir, "skills");
22206
+ if (!fs14.existsSync(skillsDir)) {
21368
22207
  console.log(chalk10.dim("No skills directory found."));
21369
22208
  return;
21370
22209
  }
21371
22210
  let entries;
21372
22211
  try {
21373
- entries = fs13.readdirSync(skillsDir);
22212
+ entries = fs14.readdirSync(skillsDir);
21374
22213
  } catch {
21375
22214
  console.log(chalk10.red("Could not read skills directory."));
21376
22215
  return;
@@ -21382,16 +22221,16 @@ async function skillsMigrateCommand() {
21382
22221
  }
21383
22222
  let migrated = 0;
21384
22223
  for (const file2 of yamlFiles) {
21385
- const yamlPath = path14.join(skillsDir, file2);
22224
+ const yamlPath = path15.join(skillsDir, file2);
21386
22225
  const baseName = file2.replace(/\.(yaml|yml)$/, "");
21387
- const outputDir = path14.join(skillsDir, baseName);
21388
- if (fs13.existsSync(outputDir)) {
22226
+ const outputDir = path15.join(skillsDir, baseName);
22227
+ if (fs14.existsSync(outputDir)) {
21389
22228
  console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
21390
22229
  continue;
21391
22230
  }
21392
22231
  try {
21393
22232
  migrateYamlToSkillMd(yamlPath, outputDir);
21394
- fs13.renameSync(yamlPath, `${yamlPath}.bak`);
22233
+ fs14.renameSync(yamlPath, `${yamlPath}.bak`);
21395
22234
  console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
21396
22235
  migrated++;
21397
22236
  } catch (err) {
@@ -21405,35 +22244,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
21405
22244
  }
21406
22245
 
21407
22246
  // src/cli/commands/import.ts
21408
- import * as fs16 from "fs";
21409
- import * as path17 from "path";
22247
+ import * as fs17 from "fs";
22248
+ import * as path18 from "path";
21410
22249
  import chalk11 from "chalk";
21411
22250
 
21412
22251
  // src/import/engine.ts
21413
- import * as fs15 from "fs";
21414
- import * as path16 from "path";
22252
+ import * as fs16 from "fs";
22253
+ import * as path17 from "path";
21415
22254
  import matter5 from "gray-matter";
21416
22255
 
21417
22256
  // src/import/classifier.ts
21418
- import * as fs14 from "fs";
21419
- import * as path15 from "path";
22257
+ import * as fs15 from "fs";
22258
+ import * as path16 from "path";
21420
22259
  import matter4 from "gray-matter";
21421
22260
  var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
21422
22261
  var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
21423
22262
  var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
21424
22263
  function classifyPath(inputPath, knownTypes, knownDirNames) {
21425
- const resolved = path15.resolve(inputPath);
21426
- const stat = fs14.statSync(resolved);
22264
+ const resolved = path16.resolve(inputPath);
22265
+ const stat = fs15.statSync(resolved);
21427
22266
  if (!stat.isDirectory()) {
21428
22267
  return classifyFile(resolved, knownTypes);
21429
22268
  }
21430
- if (path15.basename(resolved) === ".marvin" || fs14.existsSync(path15.join(resolved, "config.yaml"))) {
22269
+ if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
21431
22270
  return { type: "marvin-project", inputPath: resolved };
21432
22271
  }
21433
22272
  const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
21434
- const entries = fs14.readdirSync(resolved);
22273
+ const entries = fs15.readdirSync(resolved);
21435
22274
  const hasDocSubdirs = entries.some(
21436
- (e) => allDirNames.has(e) && fs14.statSync(path15.join(resolved, e)).isDirectory()
22275
+ (e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
21437
22276
  );
21438
22277
  if (hasDocSubdirs) {
21439
22278
  return { type: "docs-directory", inputPath: resolved };
@@ -21442,7 +22281,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
21442
22281
  if (mdFiles.length > 0) {
21443
22282
  const hasMarvinDocs = mdFiles.some((f) => {
21444
22283
  try {
21445
- const raw = fs14.readFileSync(path15.join(resolved, f), "utf-8");
22284
+ const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
21446
22285
  const { data } = matter4(raw);
21447
22286
  return isValidMarvinDocument(data, knownTypes);
21448
22287
  } catch {
@@ -21456,14 +22295,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
21456
22295
  return { type: "raw-source-dir", inputPath: resolved };
21457
22296
  }
21458
22297
  function classifyFile(filePath, knownTypes) {
21459
- const resolved = path15.resolve(filePath);
21460
- const ext = path15.extname(resolved).toLowerCase();
22298
+ const resolved = path16.resolve(filePath);
22299
+ const ext = path16.extname(resolved).toLowerCase();
21461
22300
  if (RAW_SOURCE_EXTENSIONS.has(ext)) {
21462
22301
  return { type: "raw-source-file", inputPath: resolved };
21463
22302
  }
21464
22303
  if (ext === ".md") {
21465
22304
  try {
21466
- const raw = fs14.readFileSync(resolved, "utf-8");
22305
+ const raw = fs15.readFileSync(resolved, "utf-8");
21467
22306
  const { data } = matter4(raw);
21468
22307
  if (isValidMarvinDocument(data, knownTypes)) {
21469
22308
  return { type: "marvin-document", inputPath: resolved };
@@ -21588,9 +22427,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
21588
22427
  continue;
21589
22428
  }
21590
22429
  if (item.action === "copy") {
21591
- const targetDir = path16.dirname(item.targetPath);
21592
- fs15.mkdirSync(targetDir, { recursive: true });
21593
- fs15.copyFileSync(item.sourcePath, item.targetPath);
22430
+ const targetDir = path17.dirname(item.targetPath);
22431
+ fs16.mkdirSync(targetDir, { recursive: true });
22432
+ fs16.copyFileSync(item.sourcePath, item.targetPath);
21594
22433
  copied++;
21595
22434
  continue;
21596
22435
  }
@@ -21626,19 +22465,19 @@ function formatPlanSummary(plan) {
21626
22465
  lines.push(`Documents to import: ${imports.length}`);
21627
22466
  for (const item of imports) {
21628
22467
  const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
21629
- lines.push(` ${idInfo} ${path16.basename(item.sourcePath)}`);
22468
+ lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
21630
22469
  }
21631
22470
  }
21632
22471
  if (copies.length > 0) {
21633
22472
  lines.push(`Files to copy to sources/: ${copies.length}`);
21634
22473
  for (const item of copies) {
21635
- lines.push(` ${path16.basename(item.sourcePath)} \u2192 ${path16.basename(item.targetPath)}`);
22474
+ lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
21636
22475
  }
21637
22476
  }
21638
22477
  if (skips.length > 0) {
21639
22478
  lines.push(`Skipped (conflict): ${skips.length}`);
21640
22479
  for (const item of skips) {
21641
- lines.push(` ${item.originalId ?? path16.basename(item.sourcePath)} ${item.reason ?? ""}`);
22480
+ lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
21642
22481
  }
21643
22482
  }
21644
22483
  if (plan.items.length === 0) {
@@ -21671,11 +22510,11 @@ function getDirNameForType(store, type) {
21671
22510
  }
21672
22511
  function collectMarvinDocs(dir, knownTypes) {
21673
22512
  const docs = [];
21674
- const files = fs15.readdirSync(dir).filter((f) => f.endsWith(".md"));
22513
+ const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
21675
22514
  for (const file2 of files) {
21676
- const filePath = path16.join(dir, file2);
22515
+ const filePath = path17.join(dir, file2);
21677
22516
  try {
21678
- const raw = fs15.readFileSync(filePath, "utf-8");
22517
+ const raw = fs16.readFileSync(filePath, "utf-8");
21679
22518
  const { data, content } = matter5(raw);
21680
22519
  if (isValidMarvinDocument(data, knownTypes)) {
21681
22520
  docs.push({
@@ -21731,23 +22570,23 @@ function planDocImports(docs, store, options) {
21731
22570
  }
21732
22571
  function planFromMarvinProject(classification, store, _marvinDir, options) {
21733
22572
  let projectDir = classification.inputPath;
21734
- if (path16.basename(projectDir) !== ".marvin") {
21735
- const inner = path16.join(projectDir, ".marvin");
21736
- if (fs15.existsSync(inner)) {
22573
+ if (path17.basename(projectDir) !== ".marvin") {
22574
+ const inner = path17.join(projectDir, ".marvin");
22575
+ if (fs16.existsSync(inner)) {
21737
22576
  projectDir = inner;
21738
22577
  }
21739
22578
  }
21740
- const docsDir = path16.join(projectDir, "docs");
21741
- if (!fs15.existsSync(docsDir)) {
22579
+ const docsDir = path17.join(projectDir, "docs");
22580
+ if (!fs16.existsSync(docsDir)) {
21742
22581
  return [];
21743
22582
  }
21744
22583
  const knownTypes = store.registeredTypes;
21745
22584
  const allDocs = [];
21746
- const subdirs = fs15.readdirSync(docsDir).filter(
21747
- (d) => fs15.statSync(path16.join(docsDir, d)).isDirectory()
22585
+ const subdirs = fs16.readdirSync(docsDir).filter(
22586
+ (d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
21748
22587
  );
21749
22588
  for (const subdir of subdirs) {
21750
- const docs = collectMarvinDocs(path16.join(docsDir, subdir), knownTypes);
22589
+ const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
21751
22590
  allDocs.push(...docs);
21752
22591
  }
21753
22592
  return planDocImports(allDocs, store, options);
@@ -21757,10 +22596,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
21757
22596
  const knownTypes = store.registeredTypes;
21758
22597
  const allDocs = [];
21759
22598
  allDocs.push(...collectMarvinDocs(dir, knownTypes));
21760
- const entries = fs15.readdirSync(dir);
22599
+ const entries = fs16.readdirSync(dir);
21761
22600
  for (const entry of entries) {
21762
- const entryPath = path16.join(dir, entry);
21763
- if (fs15.statSync(entryPath).isDirectory()) {
22601
+ const entryPath = path17.join(dir, entry);
22602
+ if (fs16.statSync(entryPath).isDirectory()) {
21764
22603
  allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
21765
22604
  }
21766
22605
  }
@@ -21769,7 +22608,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
21769
22608
  function planFromSingleDocument(classification, store, _marvinDir, options) {
21770
22609
  const filePath = classification.inputPath;
21771
22610
  const knownTypes = store.registeredTypes;
21772
- const raw = fs15.readFileSync(filePath, "utf-8");
22611
+ const raw = fs16.readFileSync(filePath, "utf-8");
21773
22612
  const { data, content } = matter5(raw);
21774
22613
  if (!isValidMarvinDocument(data, knownTypes)) {
21775
22614
  return [];
@@ -21785,14 +22624,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
21785
22624
  }
21786
22625
  function planFromRawSourceDir(classification, marvinDir) {
21787
22626
  const dir = classification.inputPath;
21788
- const sourcesDir = path16.join(marvinDir, "sources");
22627
+ const sourcesDir = path17.join(marvinDir, "sources");
21789
22628
  const items = [];
21790
- const files = fs15.readdirSync(dir).filter((f) => {
21791
- const stat = fs15.statSync(path16.join(dir, f));
22629
+ const files = fs16.readdirSync(dir).filter((f) => {
22630
+ const stat = fs16.statSync(path17.join(dir, f));
21792
22631
  return stat.isFile();
21793
22632
  });
21794
22633
  for (const file2 of files) {
21795
- const sourcePath = path16.join(dir, file2);
22634
+ const sourcePath = path17.join(dir, file2);
21796
22635
  const targetPath = resolveSourceFileName(sourcesDir, file2);
21797
22636
  items.push({
21798
22637
  action: "copy",
@@ -21803,8 +22642,8 @@ function planFromRawSourceDir(classification, marvinDir) {
21803
22642
  return items;
21804
22643
  }
21805
22644
  function planFromRawSourceFile(classification, marvinDir) {
21806
- const sourcesDir = path16.join(marvinDir, "sources");
21807
- const fileName = path16.basename(classification.inputPath);
22645
+ const sourcesDir = path17.join(marvinDir, "sources");
22646
+ const fileName = path17.basename(classification.inputPath);
21808
22647
  const targetPath = resolveSourceFileName(sourcesDir, fileName);
21809
22648
  return [
21810
22649
  {
@@ -21815,25 +22654,25 @@ function planFromRawSourceFile(classification, marvinDir) {
21815
22654
  ];
21816
22655
  }
21817
22656
  function resolveSourceFileName(sourcesDir, fileName) {
21818
- const targetPath = path16.join(sourcesDir, fileName);
21819
- if (!fs15.existsSync(targetPath)) {
22657
+ const targetPath = path17.join(sourcesDir, fileName);
22658
+ if (!fs16.existsSync(targetPath)) {
21820
22659
  return targetPath;
21821
22660
  }
21822
- const ext = path16.extname(fileName);
21823
- const base = path16.basename(fileName, ext);
22661
+ const ext = path17.extname(fileName);
22662
+ const base = path17.basename(fileName, ext);
21824
22663
  let counter = 1;
21825
22664
  let candidate;
21826
22665
  do {
21827
- candidate = path16.join(sourcesDir, `${base}-${counter}${ext}`);
22666
+ candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
21828
22667
  counter++;
21829
- } while (fs15.existsSync(candidate));
22668
+ } while (fs16.existsSync(candidate));
21830
22669
  return candidate;
21831
22670
  }
21832
22671
 
21833
22672
  // src/cli/commands/import.ts
21834
22673
  async function importCommand(inputPath, options) {
21835
- const resolved = path17.resolve(inputPath);
21836
- if (!fs16.existsSync(resolved)) {
22674
+ const resolved = path18.resolve(inputPath);
22675
+ if (!fs17.existsSync(resolved)) {
21837
22676
  throw new ImportError(`Path not found: ${resolved}`);
21838
22677
  }
21839
22678
  const project = loadProject();
@@ -21885,7 +22724,7 @@ async function importCommand(inputPath, options) {
21885
22724
  console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
21886
22725
  const manifest = new SourceManifestManager(marvinDir);
21887
22726
  manifest.scan();
21888
- const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path17.basename(i.targetPath));
22727
+ const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
21889
22728
  for (const fileName of copiedFileNames) {
21890
22729
  try {
21891
22730
  await ingestFile({
@@ -22288,7 +23127,8 @@ The contributor is identifying a project risk.
22288
23127
  - Create actions for risk mitigation tasks
22289
23128
  - Create decisions for risk response strategies
22290
23129
  - Create questions for risks needing further assessment
22291
- - Tag all related artifacts with "risk" for tracking`,
23130
+ - Tag all related artifacts with "risk" for tracking
23131
+ - 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`,
22292
23132
  "blocker-report": `
22293
23133
  ### Type-Specific Guidance: Blocker Report
22294
23134
  The contributor is reporting a blocker.
@@ -22633,6 +23473,105 @@ function renderConfluence(report) {
22633
23473
  return lines.join("\n");
22634
23474
  }
22635
23475
 
23476
+ // src/reports/health/render-ascii.ts
23477
+ import chalk17 from "chalk";
23478
+ var STATUS_DOT2 = {
23479
+ green: chalk17.green("\u25CF"),
23480
+ amber: chalk17.yellow("\u25CF"),
23481
+ red: chalk17.red("\u25CF")
23482
+ };
23483
+ var STATUS_LABEL2 = {
23484
+ green: chalk17.green.bold("GREEN"),
23485
+ amber: chalk17.yellow.bold("AMBER"),
23486
+ red: chalk17.red.bold("RED")
23487
+ };
23488
+ var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
23489
+ function renderAscii2(report) {
23490
+ const lines = [];
23491
+ lines.push("");
23492
+ lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
23493
+ lines.push(chalk17.dim(` ${report.generatedAt}`));
23494
+ lines.push("");
23495
+ lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
23496
+ lines.push("");
23497
+ lines.push(` ${SEPARATOR2}`);
23498
+ lines.push(chalk17.bold(" Completeness"));
23499
+ lines.push(` ${SEPARATOR2}`);
23500
+ for (const cat of report.completeness) {
23501
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
23502
+ for (const item of cat.items) {
23503
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23504
+ }
23505
+ }
23506
+ lines.push("");
23507
+ lines.push(` ${SEPARATOR2}`);
23508
+ lines.push(chalk17.bold(" Process"));
23509
+ lines.push(` ${SEPARATOR2}`);
23510
+ for (const cat of report.process) {
23511
+ lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
23512
+ for (const item of cat.items) {
23513
+ lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
23514
+ }
23515
+ }
23516
+ lines.push(` ${SEPARATOR2}`);
23517
+ lines.push("");
23518
+ return lines.join("\n");
23519
+ }
23520
+
23521
+ // src/reports/health/render-confluence.ts
23522
+ var EMOJI2 = {
23523
+ green: ":green_circle:",
23524
+ amber: ":yellow_circle:",
23525
+ red: ":red_circle:"
23526
+ };
23527
+ function renderConfluence2(report) {
23528
+ const lines = [];
23529
+ lines.push(`# Health Check \u2014 ${report.projectName}`);
23530
+ lines.push("");
23531
+ lines.push(`**Date:** ${report.generatedAt}`);
23532
+ lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
23533
+ lines.push("");
23534
+ lines.push("## Completeness");
23535
+ lines.push("");
23536
+ lines.push("| Category | Status | Summary |");
23537
+ lines.push("|----------|--------|---------|");
23538
+ for (const cat of report.completeness) {
23539
+ lines.push(
23540
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23541
+ );
23542
+ }
23543
+ lines.push("");
23544
+ for (const cat of report.completeness) {
23545
+ if (cat.items.length === 0) continue;
23546
+ lines.push(`### ${cat.name}`);
23547
+ lines.push("");
23548
+ for (const item of cat.items) {
23549
+ lines.push(`- **${item.id}** ${item.detail}`);
23550
+ }
23551
+ lines.push("");
23552
+ }
23553
+ lines.push("## Process");
23554
+ lines.push("");
23555
+ lines.push("| Metric | Status | Summary |");
23556
+ lines.push("|--------|--------|---------|");
23557
+ for (const cat of report.process) {
23558
+ lines.push(
23559
+ `| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
23560
+ );
23561
+ }
23562
+ lines.push("");
23563
+ for (const cat of report.process) {
23564
+ if (cat.items.length === 0) continue;
23565
+ lines.push(`### ${cat.name}`);
23566
+ lines.push("");
23567
+ for (const item of cat.items) {
23568
+ lines.push(`- **${item.id}** ${item.detail}`);
23569
+ }
23570
+ lines.push("");
23571
+ }
23572
+ return lines.join("\n");
23573
+ }
23574
+
22636
23575
  // src/cli/commands/report.ts
22637
23576
  async function garReportCommand(options) {
22638
23577
  const project = loadProject();
@@ -22648,6 +23587,20 @@ async function garReportCommand(options) {
22648
23587
  console.log(renderAscii(report));
22649
23588
  }
22650
23589
  }
23590
+ async function healthReportCommand(options) {
23591
+ const project = loadProject();
23592
+ const plugin = resolvePlugin(project.config.methodology);
23593
+ const registrations = plugin?.documentTypeRegistrations ?? [];
23594
+ const store = new DocumentStore(project.marvinDir, registrations);
23595
+ const metrics = collectHealthMetrics(store);
23596
+ const report = evaluateHealth(project.config.name, metrics);
23597
+ const format = options.format ?? "ascii";
23598
+ if (format === "confluence") {
23599
+ console.log(renderConfluence2(report));
23600
+ } else {
23601
+ console.log(renderAscii2(report));
23602
+ }
23603
+ }
22651
23604
 
22652
23605
  // src/cli/commands/web.ts
22653
23606
  async function webCommand(options) {
@@ -22659,12 +23612,38 @@ async function webCommand(options) {
22659
23612
  await startWebServer({ port, open: options.open });
22660
23613
  }
22661
23614
 
23615
+ // src/cli/commands/generate.ts
23616
+ import * as fs18 from "fs";
23617
+ import * as path19 from "path";
23618
+ import chalk18 from "chalk";
23619
+ import { confirm as confirm2 } from "@inquirer/prompts";
23620
+ async function generateClaudeMdCommand(options) {
23621
+ const project = loadProject();
23622
+ const filePath = path19.join(project.marvinDir, "CLAUDE.md");
23623
+ if (fs18.existsSync(filePath) && !options.force) {
23624
+ const overwrite = await confirm2({
23625
+ message: ".marvin/CLAUDE.md already exists. Overwrite?",
23626
+ default: false
23627
+ });
23628
+ if (!overwrite) {
23629
+ console.log(chalk18.dim("Aborted."));
23630
+ return;
23631
+ }
23632
+ }
23633
+ fs18.writeFileSync(
23634
+ filePath,
23635
+ getDefaultClaudeMdContent(project.config.name),
23636
+ "utf-8"
23637
+ );
23638
+ console.log(chalk18.green("Created .marvin/CLAUDE.md"));
23639
+ }
23640
+
22662
23641
  // src/cli/program.ts
22663
23642
  function createProgram() {
22664
23643
  const program = new Command();
22665
23644
  program.name("marvin").description(
22666
23645
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
22667
- ).version("0.3.3");
23646
+ ).version("0.3.6");
22668
23647
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
22669
23648
  await initCommand();
22670
23649
  });
@@ -22741,9 +23720,19 @@ function createProgram() {
22741
23720
  ).action(async (options) => {
22742
23721
  await garReportCommand(options);
22743
23722
  });
23723
+ reportCmd.command("health").description("Generate a governance health check report").option(
23724
+ "--format <format>",
23725
+ "Output format: ascii or confluence (default: ascii)"
23726
+ ).action(async (options) => {
23727
+ await healthReportCommand(options);
23728
+ });
22744
23729
  program.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
22745
23730
  await webCommand(options);
22746
23731
  });
23732
+ const generateCmd = program.command("generate").description("Generate project files");
23733
+ generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
23734
+ await generateClaudeMdCommand(options);
23735
+ });
22747
23736
  return program;
22748
23737
  }
22749
23738
  export {