mrvn-cli 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/marvin.js CHANGED
@@ -920,10 +920,10 @@ function mergeDefs(...defs) {
920
920
  function cloneDef(schema) {
921
921
  return mergeDefs(schema._zod.def);
922
922
  }
923
- function getElementAtPath(obj, path20) {
924
- if (!path20)
923
+ function getElementAtPath(obj, path21) {
924
+ if (!path21)
925
925
  return obj;
926
- return path20.reduce((acc, key) => acc?.[key], obj);
926
+ return path21.reduce((acc, key) => acc?.[key], obj);
927
927
  }
928
928
  function promiseAllObject(promisesObj) {
929
929
  const keys = Object.keys(promisesObj);
@@ -1306,11 +1306,11 @@ function aborted(x, startIndex = 0) {
1306
1306
  }
1307
1307
  return false;
1308
1308
  }
1309
- function prefixIssues(path20, issues) {
1309
+ function prefixIssues(path21, issues) {
1310
1310
  return issues.map((iss) => {
1311
1311
  var _a2;
1312
1312
  (_a2 = iss).path ?? (_a2.path = []);
1313
- iss.path.unshift(path20);
1313
+ iss.path.unshift(path21);
1314
1314
  return iss;
1315
1315
  });
1316
1316
  }
@@ -1493,7 +1493,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1493
1493
  }
1494
1494
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1495
1495
  const result = { errors: [] };
1496
- const processError = (error49, path20 = []) => {
1496
+ const processError = (error49, path21 = []) => {
1497
1497
  var _a2, _b;
1498
1498
  for (const issue2 of error49.issues) {
1499
1499
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1503,7 +1503,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1503
1503
  } else if (issue2.code === "invalid_element") {
1504
1504
  processError({ issues: issue2.issues }, issue2.path);
1505
1505
  } else {
1506
- const fullpath = [...path20, ...issue2.path];
1506
+ const fullpath = [...path21, ...issue2.path];
1507
1507
  if (fullpath.length === 0) {
1508
1508
  result.errors.push(mapper(issue2));
1509
1509
  continue;
@@ -1535,8 +1535,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1535
1535
  }
1536
1536
  function toDotPath(_path) {
1537
1537
  const segs = [];
1538
- const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1539
- for (const seg of path20) {
1538
+ const path21 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1539
+ for (const seg of path21) {
1540
1540
  if (typeof seg === "number")
1541
1541
  segs.push(`[${seg}]`);
1542
1542
  else if (typeof seg === "symbol")
@@ -13513,13 +13513,13 @@ function resolveRef(ref, ctx) {
13513
13513
  if (!ref.startsWith("#")) {
13514
13514
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13515
13515
  }
13516
- const path20 = ref.slice(1).split("/").filter(Boolean);
13517
- if (path20.length === 0) {
13516
+ const path21 = ref.slice(1).split("/").filter(Boolean);
13517
+ if (path21.length === 0) {
13518
13518
  return ctx.rootSchema;
13519
13519
  }
13520
13520
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13521
- if (path20[0] === defsKey) {
13522
- const key = path20[1];
13521
+ if (path21[0] === defsKey) {
13522
+ const key = path21[1];
13523
13523
  if (!key || !ctx.defs[key]) {
13524
13524
  throw new Error(`Reference not found: ${ref}`);
13525
13525
  }
@@ -14105,7 +14105,14 @@ import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14105
14105
  // src/plugins/builtin/tools/epic-utils.ts
14106
14106
  function normalizeLinkedFeatures(value) {
14107
14107
  if (value === void 0 || value === null) return [];
14108
- if (typeof value === "string") return [value];
14108
+ if (typeof value === "string") {
14109
+ try {
14110
+ const parsed = JSON.parse(value);
14111
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
14112
+ } catch {
14113
+ }
14114
+ return [value];
14115
+ }
14109
14116
  if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
14110
14117
  return [];
14111
14118
  }
@@ -14928,6 +14935,20 @@ function createFeatureTools(store) {
14928
14935
 
14929
14936
  // src/plugins/builtin/tools/epics.ts
14930
14937
  import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
14938
+ var linkedFeatureArray = external_exports.preprocess(
14939
+ (val) => {
14940
+ if (typeof val === "string") {
14941
+ try {
14942
+ const parsed = JSON.parse(val);
14943
+ if (Array.isArray(parsed)) return parsed;
14944
+ } catch {
14945
+ }
14946
+ return [val];
14947
+ }
14948
+ return val;
14949
+ },
14950
+ external_exports.array(external_exports.string())
14951
+ );
14931
14952
  function createEpicTools(store) {
14932
14953
  return [
14933
14954
  tool4(
@@ -14993,7 +15014,7 @@ function createEpicTools(store) {
14993
15014
  {
14994
15015
  title: external_exports.string().describe("Epic title"),
14995
15016
  content: external_exports.string().describe("Epic description and scope"),
14996
- linkedFeature: external_exports.array(external_exports.string()).describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
15017
+ linkedFeature: linkedFeatureArray.describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
14997
15018
  status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
14998
15019
  owner: external_exports.string().optional().describe("Epic owner"),
14999
15020
  targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
@@ -15069,7 +15090,7 @@ function createEpicTools(store) {
15069
15090
  owner: external_exports.string().optional().describe("New owner"),
15070
15091
  targetDate: external_exports.string().optional().describe("New target date"),
15071
15092
  estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
15072
- linkedFeature: external_exports.array(external_exports.string()).optional().describe("New linked feature ID(s)"),
15093
+ linkedFeature: linkedFeatureArray.optional().describe("New linked feature ID(s)"),
15073
15094
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
15074
15095
  },
15075
15096
  async (args) => {
@@ -15606,6 +15627,205 @@ function createSprintPlanningTools(store) {
15606
15627
  ];
15607
15628
  }
15608
15629
 
15630
+ // src/plugins/builtin/tools/tasks.ts
15631
+ import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
15632
+
15633
+ // src/plugins/builtin/tools/task-utils.ts
15634
+ function normalizeLinkedEpics(value) {
15635
+ if (value === void 0 || value === null) return [];
15636
+ if (typeof value === "string") {
15637
+ try {
15638
+ const parsed = JSON.parse(value);
15639
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
15640
+ } catch {
15641
+ }
15642
+ return [value];
15643
+ }
15644
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
15645
+ return [];
15646
+ }
15647
+ function generateEpicTags(epics) {
15648
+ return epics.map((id) => `epic:${id}`);
15649
+ }
15650
+
15651
+ // src/plugins/builtin/tools/tasks.ts
15652
+ var linkedEpicArray = external_exports.preprocess(
15653
+ (val) => {
15654
+ if (typeof val === "string") {
15655
+ try {
15656
+ const parsed = JSON.parse(val);
15657
+ if (Array.isArray(parsed)) return parsed;
15658
+ } catch {
15659
+ }
15660
+ return [val];
15661
+ }
15662
+ return val;
15663
+ },
15664
+ external_exports.array(external_exports.string())
15665
+ );
15666
+ function createTaskTools(store) {
15667
+ return [
15668
+ tool8(
15669
+ "list_tasks",
15670
+ "List all tasks in the project, optionally filtered by status, linked epic, or priority",
15671
+ {
15672
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Filter by task status"),
15673
+ linkedEpic: external_exports.string().optional().describe("Filter by linked epic ID (e.g. 'E-001')"),
15674
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority")
15675
+ },
15676
+ async (args) => {
15677
+ let docs = store.list({ type: "task", status: args.status });
15678
+ if (args.linkedEpic) {
15679
+ docs = docs.filter(
15680
+ (d) => normalizeLinkedEpics(d.frontmatter.linkedEpic).includes(args.linkedEpic)
15681
+ );
15682
+ }
15683
+ if (args.priority) {
15684
+ docs = docs.filter((d) => d.frontmatter.priority === args.priority);
15685
+ }
15686
+ const summary = docs.map((d) => ({
15687
+ id: d.frontmatter.id,
15688
+ title: d.frontmatter.title,
15689
+ status: d.frontmatter.status,
15690
+ linkedEpic: normalizeLinkedEpics(d.frontmatter.linkedEpic),
15691
+ priority: d.frontmatter.priority,
15692
+ complexity: d.frontmatter.complexity,
15693
+ estimatedPoints: d.frontmatter.estimatedPoints,
15694
+ tags: d.frontmatter.tags
15695
+ }));
15696
+ return {
15697
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
15698
+ };
15699
+ },
15700
+ { annotations: { readOnlyHint: true } }
15701
+ ),
15702
+ tool8(
15703
+ "get_task",
15704
+ "Get the full content of a specific task by ID",
15705
+ { id: external_exports.string().describe("Task ID (e.g. 'T-001')") },
15706
+ async (args) => {
15707
+ const doc = store.get(args.id);
15708
+ if (!doc) {
15709
+ return {
15710
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
15711
+ isError: true
15712
+ };
15713
+ }
15714
+ return {
15715
+ content: [
15716
+ {
15717
+ type: "text",
15718
+ text: JSON.stringify(
15719
+ { ...doc.frontmatter, content: doc.content },
15720
+ null,
15721
+ 2
15722
+ )
15723
+ }
15724
+ ]
15725
+ };
15726
+ },
15727
+ { annotations: { readOnlyHint: true } }
15728
+ ),
15729
+ tool8(
15730
+ "create_task",
15731
+ "Create a new implementation task linked to one or more epics. The linked epic is soft-validated (warns if not found, but does not block creation).",
15732
+ {
15733
+ title: external_exports.string().describe("Task title"),
15734
+ content: external_exports.string().describe("Task description and implementation details"),
15735
+ linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
15736
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
15737
+ acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
15738
+ technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
15739
+ estimatedPoints: external_exports.number().optional().describe("Story point estimate"),
15740
+ complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
15741
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
15742
+ tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
15743
+ },
15744
+ async (args) => {
15745
+ const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
15746
+ const warnings = [];
15747
+ for (const epicId of linkedEpics) {
15748
+ const epic = store.get(epicId);
15749
+ if (!epic) {
15750
+ warnings.push(`Warning: Epic ${epicId} not found`);
15751
+ } else if (epic.frontmatter.type !== "epic") {
15752
+ warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
15753
+ }
15754
+ }
15755
+ const frontmatter = {
15756
+ title: args.title,
15757
+ status: args.status ?? "backlog",
15758
+ linkedEpic: linkedEpics,
15759
+ tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
15760
+ };
15761
+ if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
15762
+ if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
15763
+ if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
15764
+ if (args.complexity) frontmatter.complexity = args.complexity;
15765
+ if (args.priority) frontmatter.priority = args.priority;
15766
+ const doc = store.create("task", frontmatter, args.content);
15767
+ const parts = [
15768
+ `Created task ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${linkedEpics.join(", ")})`
15769
+ ];
15770
+ if (warnings.length > 0) {
15771
+ parts.push(warnings.join("; "));
15772
+ }
15773
+ return {
15774
+ content: [{ type: "text", text: parts.join("\n") }]
15775
+ };
15776
+ }
15777
+ ),
15778
+ tool8(
15779
+ "update_task",
15780
+ "Update an existing task, including its linked epics.",
15781
+ {
15782
+ id: external_exports.string().describe("Task ID to update"),
15783
+ title: external_exports.string().optional().describe("New title"),
15784
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
15785
+ content: external_exports.string().optional().describe("New content"),
15786
+ linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
15787
+ acceptanceCriteria: external_exports.string().optional().describe("New acceptance criteria"),
15788
+ technicalNotes: external_exports.string().optional().describe("New technical notes"),
15789
+ estimatedPoints: external_exports.number().optional().describe("New story point estimate"),
15790
+ complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
15791
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
15792
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)")
15793
+ },
15794
+ async (args) => {
15795
+ const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, ...updates } = args;
15796
+ const warnings = [];
15797
+ if (rawLinkedEpic !== void 0) {
15798
+ const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
15799
+ for (const epicId of linkedEpics) {
15800
+ const epic = store.get(epicId);
15801
+ if (!epic) {
15802
+ warnings.push(`Warning: Epic ${epicId} not found`);
15803
+ } else if (epic.frontmatter.type !== "epic") {
15804
+ warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
15805
+ }
15806
+ }
15807
+ updates.linkedEpic = linkedEpics;
15808
+ const existingDoc = store.get(id);
15809
+ const existingTags = existingDoc?.frontmatter.tags ?? [];
15810
+ const nonEpicTags = existingTags.filter((t) => !t.startsWith("epic:"));
15811
+ const baseTags = userTags ?? nonEpicTags;
15812
+ updates.tags = [...generateEpicTags(linkedEpics), ...baseTags];
15813
+ } else if (userTags !== void 0) {
15814
+ updates.tags = userTags;
15815
+ }
15816
+ const doc = store.update(id, updates, content);
15817
+ const parts = [`Updated task ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
15818
+ if (warnings.length > 0) {
15819
+ parts.push(warnings.join("; "));
15820
+ }
15821
+ return {
15822
+ content: [{ type: "text", text: parts.join("\n") }]
15823
+ };
15824
+ }
15825
+ )
15826
+ ];
15827
+ }
15828
+
15609
15829
  // src/plugins/common.ts
15610
15830
  var COMMON_REGISTRATIONS = [
15611
15831
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -15613,7 +15833,8 @@ var COMMON_REGISTRATIONS = [
15613
15833
  { type: "feature", dirName: "features", idPrefix: "F" },
15614
15834
  { type: "epic", dirName: "epics", idPrefix: "E" },
15615
15835
  { type: "contribution", dirName: "contributions", idPrefix: "C" },
15616
- { type: "sprint", dirName: "sprints", idPrefix: "SP" }
15836
+ { type: "sprint", dirName: "sprints", idPrefix: "SP" },
15837
+ { type: "task", dirName: "tasks", idPrefix: "T" }
15617
15838
  ];
15618
15839
  function createCommonTools(store) {
15619
15840
  return [
@@ -15623,7 +15844,8 @@ function createCommonTools(store) {
15623
15844
  ...createEpicTools(store),
15624
15845
  ...createContributionTools(store),
15625
15846
  ...createSprintTools(store),
15626
- ...createSprintPlanningTools(store)
15847
+ ...createSprintPlanningTools(store),
15848
+ ...createTaskTools(store)
15627
15849
  ];
15628
15850
  }
15629
15851
 
@@ -15633,7 +15855,7 @@ var genericAgilePlugin = {
15633
15855
  name: "Generic Agile",
15634
15856
  description: "Default methodology plugin providing standard agile governance patterns for decisions, actions, and questions.",
15635
15857
  version: "0.1.0",
15636
- documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint"],
15858
+ documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint", "task"],
15637
15859
  documentTypeRegistrations: [...COMMON_REGISTRATIONS],
15638
15860
  tools: (store) => [...createCommonTools(store)],
15639
15861
  promptFragments: {
@@ -15672,6 +15894,11 @@ var genericAgilePlugin = {
15672
15894
  - **create_epic**: Create implementation epics linked to approved features. The system enforces that the linked feature must exist and be approved \u2014 if it's still "draft", ask the Product Owner to approve it first.
15673
15895
  - **update_epic**: Update epic status (planned \u2192 in-progress \u2192 done), owner, and other fields.
15674
15896
 
15897
+ **Task Tools:**
15898
+ - **list_tasks** / **get_task**: Browse and read implementation tasks.
15899
+ - **create_task**: Create implementation tasks linked to epics. Linked epics are soft-validated (warns if not found, does not block). Tasks auto-generate \`epic:E-xxx\` tags. Default status: "backlog".
15900
+ - **update_task**: Update task status (backlog \u2192 ready \u2192 in-progress \u2192 review \u2192 done), acceptance criteria, technical notes, complexity, priority, and estimated points.
15901
+
15675
15902
  **Feature Tools (read-only for awareness):**
15676
15903
  - **list_features** / **get_feature**: View features to understand what needs to be broken into epics.
15677
15904
 
@@ -15683,6 +15910,7 @@ var genericAgilePlugin = {
15683
15910
 
15684
15911
  **Key Workflow Rules:**
15685
15912
  - Only create epics against approved features \u2014 create_epic enforces this.
15913
+ - Break epics into tasks (T-xxx) with clear acceptance criteria and complexity estimates.
15686
15914
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
15687
15915
  - Collaborate with the Delivery Manager on target dates and effort estimates.
15688
15916
  - Each epic should have a clear scope and definition of done.
@@ -15718,6 +15946,9 @@ var genericAgilePlugin = {
15718
15946
  - **list_epics** / **get_epic**: View epics and their current status.
15719
15947
  - **update_epic**: Set targetDate and estimatedEffort on epics. Flag epics linked to deferred features.
15720
15948
 
15949
+ **Task Tools (read-only for tracking):**
15950
+ - **list_tasks** / **get_task**: View tasks and their statuses. Filter by linkedEpic to see implementation breakdown.
15951
+
15721
15952
  **Feature Tools (tracking focus):**
15722
15953
  - **list_features** / **get_feature**: View features and their priorities.
15723
15954
 
@@ -15763,14 +15994,15 @@ var genericAgilePlugin = {
15763
15994
  - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
15764
15995
  - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
15765
15996
  - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
15766
- "*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
15997
+ "*": `You have access to feature, epic, task, sprint, and meeting tools for project coordination:
15767
15998
 
15768
15999
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
15769
16000
  **Epics** (E-xxx): Implementation work packages created by the Tech Lead, linked to approved features. Epics progress through planned \u2192 in-progress \u2192 done.
16001
+ **Tasks** (T-xxx): Concrete implementation items created by the Tech Lead, linked to epics. Tasks progress through backlog \u2192 ready \u2192 in-progress \u2192 review \u2192 done.
15770
16002
  **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).
15771
16003
  **Meetings**: Meeting records with attendees, agendas, and notes.
15772
16004
 
15773
- **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.
16005
+ **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 and tasks, 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.
15774
16006
 
15775
16007
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
15776
16008
  - **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.
@@ -15785,10 +16017,10 @@ var genericAgilePlugin = {
15785
16017
  };
15786
16018
 
15787
16019
  // src/plugins/builtin/tools/use-cases.ts
15788
- import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
16020
+ import { tool as tool9 } from "@anthropic-ai/claude-agent-sdk";
15789
16021
  function createUseCaseTools(store) {
15790
16022
  return [
15791
- tool8(
16023
+ tool9(
15792
16024
  "list_use_cases",
15793
16025
  "List all extension use cases, optionally filtered by status or extension type",
15794
16026
  {
@@ -15818,7 +16050,7 @@ function createUseCaseTools(store) {
15818
16050
  },
15819
16051
  { annotations: { readOnlyHint: true } }
15820
16052
  ),
15821
- tool8(
16053
+ tool9(
15822
16054
  "get_use_case",
15823
16055
  "Get the full content of a specific use case by ID",
15824
16056
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -15845,7 +16077,7 @@ function createUseCaseTools(store) {
15845
16077
  },
15846
16078
  { annotations: { readOnlyHint: true } }
15847
16079
  ),
15848
- tool8(
16080
+ tool9(
15849
16081
  "create_use_case",
15850
16082
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
15851
16083
  {
@@ -15879,7 +16111,7 @@ function createUseCaseTools(store) {
15879
16111
  };
15880
16112
  }
15881
16113
  ),
15882
- tool8(
16114
+ tool9(
15883
16115
  "update_use_case",
15884
16116
  "Update an existing extension use case",
15885
16117
  {
@@ -15909,10 +16141,10 @@ function createUseCaseTools(store) {
15909
16141
  }
15910
16142
 
15911
16143
  // src/plugins/builtin/tools/tech-assessments.ts
15912
- import { tool as tool9 } from "@anthropic-ai/claude-agent-sdk";
16144
+ import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
15913
16145
  function createTechAssessmentTools(store) {
15914
16146
  return [
15915
- tool9(
16147
+ tool10(
15916
16148
  "list_tech_assessments",
15917
16149
  "List all technology assessments, optionally filtered by status",
15918
16150
  {
@@ -15943,7 +16175,7 @@ function createTechAssessmentTools(store) {
15943
16175
  },
15944
16176
  { annotations: { readOnlyHint: true } }
15945
16177
  ),
15946
- tool9(
16178
+ tool10(
15947
16179
  "get_tech_assessment",
15948
16180
  "Get the full content of a specific technology assessment by ID",
15949
16181
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -15970,7 +16202,7 @@ function createTechAssessmentTools(store) {
15970
16202
  },
15971
16203
  { annotations: { readOnlyHint: true } }
15972
16204
  ),
15973
- tool9(
16205
+ tool10(
15974
16206
  "create_tech_assessment",
15975
16207
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
15976
16208
  {
@@ -16041,7 +16273,7 @@ function createTechAssessmentTools(store) {
16041
16273
  };
16042
16274
  }
16043
16275
  ),
16044
- tool9(
16276
+ tool10(
16045
16277
  "update_tech_assessment",
16046
16278
  "Update an existing technology assessment. The linked use case cannot be changed.",
16047
16279
  {
@@ -16071,10 +16303,10 @@ function createTechAssessmentTools(store) {
16071
16303
  }
16072
16304
 
16073
16305
  // src/plugins/builtin/tools/extension-designs.ts
16074
- import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
16306
+ import { tool as tool11 } from "@anthropic-ai/claude-agent-sdk";
16075
16307
  function createExtensionDesignTools(store) {
16076
16308
  return [
16077
- tool10(
16309
+ tool11(
16078
16310
  "list_extension_designs",
16079
16311
  "List all extension designs, optionally filtered by status",
16080
16312
  {
@@ -16104,7 +16336,7 @@ function createExtensionDesignTools(store) {
16104
16336
  },
16105
16337
  { annotations: { readOnlyHint: true } }
16106
16338
  ),
16107
- tool10(
16339
+ tool11(
16108
16340
  "get_extension_design",
16109
16341
  "Get the full content of a specific extension design by ID",
16110
16342
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -16131,7 +16363,7 @@ function createExtensionDesignTools(store) {
16131
16363
  },
16132
16364
  { annotations: { readOnlyHint: true } }
16133
16365
  ),
16134
- tool10(
16366
+ tool11(
16135
16367
  "create_extension_design",
16136
16368
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
16137
16369
  {
@@ -16199,7 +16431,7 @@ function createExtensionDesignTools(store) {
16199
16431
  };
16200
16432
  }
16201
16433
  ),
16202
- tool10(
16434
+ tool11(
16203
16435
  "update_extension_design",
16204
16436
  "Update an existing extension design. The linked tech assessment cannot be changed.",
16205
16437
  {
@@ -16228,10 +16460,10 @@ function createExtensionDesignTools(store) {
16228
16460
  }
16229
16461
 
16230
16462
  // src/plugins/builtin/tools/aem-reports.ts
16231
- import { tool as tool11 } from "@anthropic-ai/claude-agent-sdk";
16463
+ import { tool as tool12 } from "@anthropic-ai/claude-agent-sdk";
16232
16464
  function createAemReportTools(store) {
16233
16465
  return [
16234
- tool11(
16466
+ tool12(
16235
16467
  "generate_extension_portfolio",
16236
16468
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
16237
16469
  {},
@@ -16283,7 +16515,7 @@ function createAemReportTools(store) {
16283
16515
  },
16284
16516
  { annotations: { readOnlyHint: true } }
16285
16517
  ),
16286
- tool11(
16518
+ tool12(
16287
16519
  "generate_tech_readiness",
16288
16520
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
16289
16521
  {},
@@ -16335,7 +16567,7 @@ function createAemReportTools(store) {
16335
16567
  },
16336
16568
  { annotations: { readOnlyHint: true } }
16337
16569
  ),
16338
- tool11(
16570
+ tool12(
16339
16571
  "generate_phase_status",
16340
16572
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
16341
16573
  {},
@@ -16397,11 +16629,11 @@ function createAemReportTools(store) {
16397
16629
  import * as fs3 from "fs";
16398
16630
  import * as path3 from "path";
16399
16631
  import * as YAML2 from "yaml";
16400
- import { tool as tool12 } from "@anthropic-ai/claude-agent-sdk";
16632
+ import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
16401
16633
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
16402
16634
  function createAemPhaseTools(store, marvinDir) {
16403
16635
  return [
16404
- tool12(
16636
+ tool13(
16405
16637
  "get_current_phase",
16406
16638
  "Get the current AEM phase from project configuration",
16407
16639
  {},
@@ -16422,7 +16654,7 @@ function createAemPhaseTools(store, marvinDir) {
16422
16654
  },
16423
16655
  { annotations: { readOnlyHint: true } }
16424
16656
  ),
16425
- tool12(
16657
+ tool13(
16426
16658
  "advance_phase",
16427
16659
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
16428
16660
  {
@@ -16931,7 +17163,7 @@ var deliveryManager = {
16931
17163
  "Epic scheduling and tracking",
16932
17164
  "Sprint planning and tracking"
16933
17165
  ],
16934
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "sprint"],
17166
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
16935
17167
  contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
16936
17168
  };
16937
17169
 
@@ -16969,9 +17201,10 @@ var techLead = {
16969
17201
  "Implementation guidance",
16970
17202
  "Non-functional requirements",
16971
17203
  "Epic creation and scoping",
17204
+ "Task creation and breakdown",
16972
17205
  "Sprint scoping and technical execution"
16973
17206
  ],
16974
- documentTypes: ["decision", "action", "question", "epic", "sprint"],
17207
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
16975
17208
  contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
16976
17209
  };
16977
17210
 
@@ -17004,8 +17237,8 @@ function resolvePersonaId(input4) {
17004
17237
  }
17005
17238
 
17006
17239
  // src/agent/session.ts
17007
- import * as fs10 from "fs";
17008
- import * as path10 from "path";
17240
+ import * as fs11 from "fs";
17241
+ import * as path11 from "path";
17009
17242
  import * as readline from "readline";
17010
17243
  import chalk2 from "chalk";
17011
17244
  import ora from "ora";
@@ -17373,10 +17606,10 @@ import {
17373
17606
  } from "@anthropic-ai/claude-agent-sdk";
17374
17607
 
17375
17608
  // src/agent/tools/decisions.ts
17376
- import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
17609
+ import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17377
17610
  function createDecisionTools(store) {
17378
17611
  return [
17379
- tool13(
17612
+ tool14(
17380
17613
  "list_decisions",
17381
17614
  "List all decisions in the project, optionally filtered by status",
17382
17615
  { status: external_exports.string().optional().describe("Filter by status (e.g. 'open', 'decided', 'superseded')") },
@@ -17394,7 +17627,7 @@ function createDecisionTools(store) {
17394
17627
  },
17395
17628
  { annotations: { readOnlyHint: true } }
17396
17629
  ),
17397
- tool13(
17630
+ tool14(
17398
17631
  "get_decision",
17399
17632
  "Get the full content of a specific decision by ID",
17400
17633
  { id: external_exports.string().describe("Decision ID (e.g. 'D-001')") },
@@ -17421,7 +17654,7 @@ function createDecisionTools(store) {
17421
17654
  },
17422
17655
  { annotations: { readOnlyHint: true } }
17423
17656
  ),
17424
- tool13(
17657
+ tool14(
17425
17658
  "create_decision",
17426
17659
  "Create a new decision record",
17427
17660
  {
@@ -17452,7 +17685,7 @@ function createDecisionTools(store) {
17452
17685
  };
17453
17686
  }
17454
17687
  ),
17455
- tool13(
17688
+ tool14(
17456
17689
  "update_decision",
17457
17690
  "Update an existing decision",
17458
17691
  {
@@ -17480,7 +17713,7 @@ function createDecisionTools(store) {
17480
17713
  }
17481
17714
 
17482
17715
  // src/agent/tools/actions.ts
17483
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17716
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17484
17717
  function findMatchingSprints(store, dueDate) {
17485
17718
  const sprints = store.list({ type: "sprint" });
17486
17719
  return sprints.filter((s) => {
@@ -17496,7 +17729,7 @@ function findMatchingSprints(store, dueDate) {
17496
17729
  }
17497
17730
  function createActionTools(store) {
17498
17731
  return [
17499
- tool14(
17732
+ tool15(
17500
17733
  "list_actions",
17501
17734
  "List all action items in the project, optionally filtered by status or owner",
17502
17735
  {
@@ -17528,7 +17761,7 @@ function createActionTools(store) {
17528
17761
  },
17529
17762
  { annotations: { readOnlyHint: true } }
17530
17763
  ),
17531
- tool14(
17764
+ tool15(
17532
17765
  "get_action",
17533
17766
  "Get the full content of a specific action item by ID",
17534
17767
  { id: external_exports.string().describe("Action ID (e.g. 'A-001')") },
@@ -17555,7 +17788,7 @@ function createActionTools(store) {
17555
17788
  },
17556
17789
  { annotations: { readOnlyHint: true } }
17557
17790
  ),
17558
- tool14(
17791
+ tool15(
17559
17792
  "create_action",
17560
17793
  "Create a new action item",
17561
17794
  {
@@ -17601,7 +17834,7 @@ function createActionTools(store) {
17601
17834
  };
17602
17835
  }
17603
17836
  ),
17604
- tool14(
17837
+ tool15(
17605
17838
  "update_action",
17606
17839
  "Update an existing action item",
17607
17840
  {
@@ -17650,7 +17883,7 @@ function createActionTools(store) {
17650
17883
  };
17651
17884
  }
17652
17885
  ),
17653
- tool14(
17886
+ tool15(
17654
17887
  "suggest_sprints_for_action",
17655
17888
  "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
17656
17889
  {
@@ -17683,10 +17916,10 @@ function createActionTools(store) {
17683
17916
  }
17684
17917
 
17685
17918
  // src/agent/tools/questions.ts
17686
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17919
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17687
17920
  function createQuestionTools(store) {
17688
17921
  return [
17689
- tool15(
17922
+ tool16(
17690
17923
  "list_questions",
17691
17924
  "List all questions in the project, optionally filtered by status",
17692
17925
  {
@@ -17707,7 +17940,7 @@ function createQuestionTools(store) {
17707
17940
  },
17708
17941
  { annotations: { readOnlyHint: true } }
17709
17942
  ),
17710
- tool15(
17943
+ tool16(
17711
17944
  "get_question",
17712
17945
  "Get the full content of a specific question by ID",
17713
17946
  { id: external_exports.string().describe("Question ID (e.g. 'Q-001')") },
@@ -17734,7 +17967,7 @@ function createQuestionTools(store) {
17734
17967
  },
17735
17968
  { annotations: { readOnlyHint: true } }
17736
17969
  ),
17737
- tool15(
17970
+ tool16(
17738
17971
  "create_question",
17739
17972
  "Create a new question that needs to be answered",
17740
17973
  {
@@ -17765,7 +17998,7 @@ function createQuestionTools(store) {
17765
17998
  };
17766
17999
  }
17767
18000
  ),
17768
- tool15(
18001
+ tool16(
17769
18002
  "update_question",
17770
18003
  "Update an existing question",
17771
18004
  {
@@ -17793,10 +18026,10 @@ function createQuestionTools(store) {
17793
18026
  }
17794
18027
 
17795
18028
  // src/agent/tools/documents.ts
17796
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
18029
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17797
18030
  function createDocumentTools(store) {
17798
18031
  return [
17799
- tool16(
18032
+ tool17(
17800
18033
  "search_documents",
17801
18034
  "Search all project documents, optionally filtered by type, status, or tag",
17802
18035
  {
@@ -17828,7 +18061,7 @@ function createDocumentTools(store) {
17828
18061
  },
17829
18062
  { annotations: { readOnlyHint: true } }
17830
18063
  ),
17831
- tool16(
18064
+ tool17(
17832
18065
  "read_document",
17833
18066
  "Read the full content of any project document by ID",
17834
18067
  { id: external_exports.string().describe("Document ID (e.g. 'D-001', 'A-003', 'Q-002')") },
@@ -17855,7 +18088,7 @@ function createDocumentTools(store) {
17855
18088
  },
17856
18089
  { annotations: { readOnlyHint: true } }
17857
18090
  ),
17858
- tool16(
18091
+ tool17(
17859
18092
  "project_summary",
17860
18093
  "Get a summary of all project documents and their counts",
17861
18094
  {},
@@ -17887,10 +18120,10 @@ function createDocumentTools(store) {
17887
18120
  }
17888
18121
 
17889
18122
  // src/agent/tools/sources.ts
17890
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
18123
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17891
18124
  function createSourceTools(manifest) {
17892
18125
  return [
17893
- tool17(
18126
+ tool18(
17894
18127
  "list_sources",
17895
18128
  "List all source documents and their processing status",
17896
18129
  {
@@ -17920,7 +18153,7 @@ function createSourceTools(manifest) {
17920
18153
  },
17921
18154
  { annotations: { readOnlyHint: true } }
17922
18155
  ),
17923
- tool17(
18156
+ tool18(
17924
18157
  "get_source_info",
17925
18158
  "Get detailed information about a specific source document",
17926
18159
  {
@@ -17958,10 +18191,10 @@ function createSourceTools(manifest) {
17958
18191
  }
17959
18192
 
17960
18193
  // src/agent/tools/sessions.ts
17961
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
18194
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17962
18195
  function createSessionTools(store) {
17963
18196
  return [
17964
- tool18(
18197
+ tool19(
17965
18198
  "list_sessions",
17966
18199
  "List all saved chat sessions, sorted by most recently used",
17967
18200
  {
@@ -17985,7 +18218,7 @@ function createSessionTools(store) {
17985
18218
  },
17986
18219
  { annotations: { readOnlyHint: true } }
17987
18220
  ),
17988
- tool18(
18221
+ tool19(
17989
18222
  "get_session",
17990
18223
  "Get details of a specific saved session by name",
17991
18224
  { name: external_exports.string().describe("Session name (e.g. 'jwt-auth-decision')") },
@@ -18008,7 +18241,7 @@ function createSessionTools(store) {
18008
18241
 
18009
18242
  // src/agent/tools/web.ts
18010
18243
  import * as http2 from "http";
18011
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18244
+ import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
18012
18245
 
18013
18246
  // src/web/data.ts
18014
18247
  function getOverviewData(store) {
@@ -18244,6 +18477,7 @@ function inline(text) {
18244
18477
  function layout(opts, body) {
18245
18478
  const topItems = [
18246
18479
  { href: "/", label: "Overview" },
18480
+ { href: "/timeline", label: "Timeline" },
18247
18481
  { href: "/board", label: "Board" },
18248
18482
  { href: "/gar", label: "GAR Report" },
18249
18483
  { href: "/health", label: "Health" }
@@ -18280,7 +18514,7 @@ function layout(opts, body) {
18280
18514
  ${groupsHtml}
18281
18515
  </nav>
18282
18516
  </aside>
18283
- <main class="main">
18517
+ <main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
18284
18518
  <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18285
18519
  <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>
18286
18520
  <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>
@@ -18289,7 +18523,36 @@ function layout(opts, body) {
18289
18523
  </main>
18290
18524
  </div>
18291
18525
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18292
- <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
18526
+ <script>mermaid.initialize({
18527
+ startOnLoad: true,
18528
+ theme: 'dark',
18529
+ themeVariables: {
18530
+ background: '#1a1d27',
18531
+ primaryColor: '#2a2e3a',
18532
+ sectionBkgColor: '#1a1d27',
18533
+ sectionBkgColor2: '#222632',
18534
+ altSectionBkgColor: '#222632',
18535
+ gridColor: '#2a2e3a',
18536
+ taskBorderColor: '#475569',
18537
+ doneTaskBkgColor: '#065f46',
18538
+ doneTaskBorderColor: '#34d399',
18539
+ activeTaskBkgColor: '#78350f',
18540
+ activeTaskBorderColor: '#fbbf24',
18541
+ taskTextColor: '#e1e4ea',
18542
+ sectionBkgColor: '#1a1d27',
18543
+ pie1: '#34d399',
18544
+ pie2: '#475569',
18545
+ pie3: '#fbbf24',
18546
+ pie4: '#f87171',
18547
+ pie5: '#6c8cff',
18548
+ pie6: '#a78bfa',
18549
+ pie7: '#f472b6',
18550
+ pieTitleTextColor: '#e1e4ea',
18551
+ pieSectionTextColor: '#e1e4ea',
18552
+ pieLegendTextColor: '#e1e4ea',
18553
+ pieStrokeColor: '#1a1d27'
18554
+ }
18555
+ });</script>
18293
18556
  </body>
18294
18557
  </html>`;
18295
18558
  }
@@ -18536,6 +18799,10 @@ a:hover { text-decoration: underline; }
18536
18799
  /* Table */
18537
18800
  .table-wrap {
18538
18801
  overflow-x: auto;
18802
+ overflow-y: auto;
18803
+ max-height: calc(100vh - 280px);
18804
+ border: 1px solid var(--border);
18805
+ border-radius: var(--radius);
18539
18806
  }
18540
18807
 
18541
18808
  table {
@@ -18551,6 +18818,10 @@ th {
18551
18818
  letter-spacing: 0.05em;
18552
18819
  color: var(--text-dim);
18553
18820
  border-bottom: 1px solid var(--border);
18821
+ position: sticky;
18822
+ top: 0;
18823
+ background: var(--bg-card);
18824
+ z-index: 1;
18554
18825
  }
18555
18826
 
18556
18827
  td {
@@ -18598,6 +18869,8 @@ tr:hover td {
18598
18869
  border: 1px solid var(--border);
18599
18870
  border-radius: var(--radius);
18600
18871
  padding: 1.25rem;
18872
+ display: flex;
18873
+ flex-direction: column;
18601
18874
  }
18602
18875
 
18603
18876
  .gar-area .area-header {
@@ -18628,6 +18901,9 @@ tr:hover td {
18628
18901
  .gar-area ul {
18629
18902
  list-style: none;
18630
18903
  font-size: 0.8rem;
18904
+ max-height: 200px;
18905
+ overflow-y: auto;
18906
+ scrollbar-width: thin;
18631
18907
  }
18632
18908
 
18633
18909
  .gar-area li {
@@ -18650,13 +18926,14 @@ tr:hover td {
18650
18926
  display: flex;
18651
18927
  gap: 1rem;
18652
18928
  overflow-x: auto;
18929
+ scrollbar-width: thin;
18653
18930
  padding-bottom: 1rem;
18654
18931
  }
18655
18932
 
18656
18933
  .board-column {
18657
18934
  min-width: 240px;
18658
18935
  max-width: 300px;
18659
- flex: 1;
18936
+ flex: 0 0 auto;
18660
18937
  }
18661
18938
 
18662
18939
  .board-column-header {
@@ -18669,6 +18946,7 @@ tr:hover td {
18669
18946
  margin-bottom: 0.5rem;
18670
18947
  display: flex;
18671
18948
  justify-content: space-between;
18949
+ flex-shrink: 0;
18672
18950
  }
18673
18951
 
18674
18952
  .board-column-header .count {
@@ -18850,106 +19128,518 @@ tr:hover td {
18850
19128
  .mermaid-row .mermaid-container {
18851
19129
  margin: 0;
18852
19130
  }
18853
- `;
18854
- }
18855
19131
 
18856
- // src/web/templates/mermaid.ts
18857
- function sanitize(text, maxLen = 40) {
18858
- const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
18859
- return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19132
+ /* Three-column artifact flow */
19133
+ .flow-diagram {
19134
+ background: var(--bg-card);
19135
+ border: 1px solid var(--border);
19136
+ border-radius: var(--radius);
19137
+ padding: 1.25rem;
19138
+ position: relative;
19139
+ overflow-x: auto;
18860
19140
  }
18861
- function mermaidBlock(definition) {
18862
- return `<div class="mermaid-container"><pre class="mermaid">
18863
- ${definition}
18864
- </pre></div>`;
19141
+
19142
+ .flow-lines {
19143
+ position: absolute;
19144
+ top: 0;
19145
+ left: 0;
19146
+ pointer-events: none;
18865
19147
  }
18866
- function placeholder(message) {
18867
- return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19148
+
19149
+ .flow-columns {
19150
+ display: flex;
19151
+ gap: 3rem;
19152
+ position: relative;
19153
+ min-width: 600px;
18868
19154
  }
18869
- function buildTimelineGantt(data) {
18870
- const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
19155
+
19156
+ .flow-column {
19157
+ flex: 1;
19158
+ min-width: 0;
19159
+ display: flex;
19160
+ flex-direction: column;
19161
+ gap: 0.5rem;
19162
+ }
19163
+
19164
+ .flow-column-header {
19165
+ font-size: 0.7rem;
19166
+ text-transform: uppercase;
19167
+ letter-spacing: 0.06em;
19168
+ color: var(--text-dim);
19169
+ font-weight: 600;
19170
+ padding-bottom: 0.4rem;
19171
+ border-bottom: 1px solid var(--border);
19172
+ margin-bottom: 0.25rem;
19173
+ }
19174
+
19175
+ .flow-node {
19176
+ padding: 0.5rem 0.65rem;
19177
+ border-radius: 6px;
19178
+ border-left: 3px solid var(--border);
19179
+ background: var(--bg);
19180
+ transition: border-color 0.15s, background 0.15s;
19181
+ }
19182
+
19183
+ .flow-node:hover {
19184
+ background: var(--bg-hover);
19185
+ }
19186
+
19187
+ .flow-node-id {
19188
+ display: inline-block;
19189
+ font-family: var(--mono);
19190
+ font-size: 0.65rem;
19191
+ color: var(--accent);
19192
+ margin-bottom: 0.15rem;
19193
+ text-decoration: none;
19194
+ }
19195
+
19196
+ .flow-node-id:hover {
19197
+ text-decoration: underline;
19198
+ }
19199
+
19200
+ .flow-node-title {
19201
+ display: block;
19202
+ font-size: 0.8rem;
19203
+ }
19204
+
19205
+ .flow-done { border-left-color: var(--green); }
19206
+ .flow-active { border-left-color: var(--amber); }
19207
+ .flow-blocked { border-left-color: var(--red); }
19208
+ .flow-default { border-left-color: var(--accent-dim); }
19209
+
19210
+ .flow-node { cursor: pointer; transition: opacity 0.2s, border-color 0.15s, background 0.15s; }
19211
+ .flow-dim { opacity: 0.2; }
19212
+ .flow-lit { background: var(--bg-hover); }
19213
+ .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
19214
+ .flow-line-dim { opacity: 0.08; }
19215
+
19216
+ /* Gantt truncation note */
19217
+ .mermaid-note {
19218
+ font-size: 0.75rem;
19219
+ color: var(--text-dim);
19220
+ text-align: right;
19221
+ margin-bottom: 0.5rem;
19222
+ }
19223
+
19224
+ /* HTML Gantt chart */
19225
+ .gantt {
19226
+ background: var(--bg-card);
19227
+ border: 1px solid var(--border);
19228
+ border-radius: var(--radius);
19229
+ padding: 1.25rem 1.25rem 1.25rem 0;
19230
+ position: relative;
19231
+ overflow-x: auto;
19232
+ }
19233
+
19234
+ .gantt-chart {
19235
+ min-width: 600px;
19236
+ }
19237
+
19238
+ .gantt-overlay {
19239
+ position: absolute;
19240
+ top: 0;
19241
+ left: 0;
19242
+ right: 0;
19243
+ bottom: 0;
19244
+ pointer-events: none;
19245
+ display: flex;
19246
+ }
19247
+
19248
+ .gantt-header,
19249
+ .gantt-section-row,
19250
+ .gantt-row,
19251
+ .gantt-overlay {
19252
+ display: flex;
19253
+ align-items: center;
19254
+ }
19255
+
19256
+ .gantt-label {
19257
+ width: 200px;
19258
+ min-width: 200px;
19259
+ padding: 0.3rem 0.75rem;
19260
+ font-size: 0.8rem;
19261
+ color: var(--text-dim);
19262
+ text-align: right;
19263
+ white-space: nowrap;
19264
+ overflow: hidden;
19265
+ text-overflow: ellipsis;
19266
+ }
19267
+
19268
+ .gantt-section-label {
19269
+ font-weight: 600;
19270
+ color: var(--text);
19271
+ font-size: 0.75rem;
19272
+ text-transform: uppercase;
19273
+ letter-spacing: 0.03em;
19274
+ padding-top: 0.6rem;
19275
+ }
19276
+
19277
+ .gantt-track {
19278
+ flex: 1;
19279
+ position: relative;
19280
+ height: 28px;
19281
+ min-width: 0;
19282
+ }
19283
+
19284
+ .gantt-section-row .gantt-track {
19285
+ height: 20px;
19286
+ }
19287
+
19288
+ .gantt-section-bg {
19289
+ position: absolute;
19290
+ top: 0;
19291
+ bottom: 0;
19292
+ background: var(--bg-hover);
19293
+ border-radius: 3px;
19294
+ opacity: 0.4;
19295
+ }
19296
+
19297
+ .gantt-bar {
19298
+ position: absolute;
19299
+ top: 4px;
19300
+ bottom: 4px;
19301
+ border-radius: 4px;
19302
+ min-width: 6px;
19303
+ transition: opacity 0.15s;
19304
+ }
19305
+
19306
+ .gantt-bar:hover {
19307
+ opacity: 0.85;
19308
+ }
19309
+
19310
+ .gantt-bar-done {
19311
+ background: var(--green);
19312
+ }
19313
+
19314
+ .gantt-bar-active {
19315
+ background: var(--amber);
19316
+ }
19317
+
19318
+ .gantt-bar-blocked {
19319
+ background: var(--red);
19320
+ }
19321
+
19322
+ .gantt-bar-default {
19323
+ background: var(--accent-dim);
19324
+ }
19325
+
19326
+ .gantt-dates {
19327
+ height: 24px;
19328
+ border-bottom: 1px solid var(--border);
19329
+ margin-bottom: 0.25rem;
19330
+ }
19331
+
19332
+ .gantt-marker {
19333
+ position: absolute;
19334
+ top: 0;
19335
+ bottom: 0;
19336
+ border-left: 1px solid var(--border);
19337
+ }
19338
+
19339
+ .gantt-marker span {
19340
+ position: absolute;
19341
+ top: 2px;
19342
+ left: 6px;
19343
+ font-size: 0.65rem;
19344
+ color: var(--text-dim);
19345
+ white-space: nowrap;
19346
+ }
19347
+
19348
+ .gantt-today {
19349
+ position: absolute;
19350
+ top: 0;
19351
+ bottom: 0;
19352
+ width: 2px;
19353
+ background: var(--red);
19354
+ opacity: 0.7;
19355
+ }
19356
+
19357
+ /* Pie chart color overrides */
19358
+ .mermaid-container .pieCircle {
19359
+ stroke: var(--bg-card);
19360
+ }
19361
+
19362
+ .mermaid-container text.slice {
19363
+ fill: var(--bg) !important;
19364
+ font-weight: 600;
19365
+ }
19366
+ `;
19367
+ }
19368
+
19369
+ // src/web/templates/mermaid.ts
19370
+ function sanitize(text, maxLen = 40) {
19371
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
19372
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19373
+ }
19374
+ function mermaidBlock(definition, extraClass) {
19375
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
19376
+ return `<div class="${cls}"><pre class="mermaid">
19377
+ ${definition}
19378
+ </pre></div>`;
19379
+ }
19380
+ function placeholder(message) {
19381
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19382
+ }
19383
+ function toMs(date5) {
19384
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
19385
+ }
19386
+ function fmtDate(ms) {
19387
+ const d = new Date(ms);
19388
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
19389
+ return `${months[d.getMonth()]} ${d.getDate()}`;
19390
+ }
19391
+ function buildTimelineGantt(data, maxSprints = 6) {
19392
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
18871
19393
  if (sprintsWithDates.length === 0) {
18872
19394
  return placeholder("No timeline data available \u2014 sprints need start and end dates.");
18873
19395
  }
19396
+ const truncated = sprintsWithDates.length > maxSprints;
19397
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
19398
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
18874
19399
  const epicMap = new Map(data.epics.map((e) => [e.id, e]));
18875
- const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
18876
- for (const sprint of sprintsWithDates) {
18877
- lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
19400
+ const allStarts = visibleSprints.map((s) => toMs(s.startDate));
19401
+ const allEnds = visibleSprints.map((s) => toMs(s.endDate));
19402
+ const timelineStart = Math.min(...allStarts);
19403
+ const timelineEnd = Math.max(...allEnds);
19404
+ const span = timelineEnd - timelineStart || 1;
19405
+ const pct = (ms) => (ms - timelineStart) / span * 100;
19406
+ const DAY = 864e5;
19407
+ const markers = [];
19408
+ let tick = timelineStart;
19409
+ const startDay = new Date(tick).getDay();
19410
+ tick += (8 - startDay) % 7 * DAY;
19411
+ while (tick <= timelineEnd) {
19412
+ const left = pct(tick);
19413
+ markers.push(
19414
+ `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
19415
+ );
19416
+ tick += 7 * DAY;
19417
+ }
19418
+ const now = Date.now();
19419
+ let todayMarker = "";
19420
+ if (now >= timelineStart && now <= timelineEnd) {
19421
+ todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
19422
+ }
19423
+ const rows = [];
19424
+ for (const sprint of visibleSprints) {
19425
+ const sStart = toMs(sprint.startDate);
19426
+ const sEnd = toMs(sprint.endDate);
19427
+ rows.push(`<div class="gantt-section-row">
19428
+ <div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
19429
+ <div class="gantt-track">
19430
+ <div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
19431
+ </div>
19432
+ </div>`);
18878
19433
  const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
18879
- if (linked.length === 0) {
18880
- lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
18881
- } else {
18882
- for (const epic of linked) {
18883
- const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
18884
- lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
18885
- }
19434
+ const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
19435
+ for (const item of items) {
19436
+ const cls = item.status === "done" || item.status === "completed" ? "gantt-bar-done" : item.status === "in-progress" || item.status === "active" ? "gantt-bar-active" : item.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
19437
+ const left = pct(sStart).toFixed(2);
19438
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
19439
+ rows.push(`<div class="gantt-row">
19440
+ <div class="gantt-label">${item.label}</div>
19441
+ <div class="gantt-track">
19442
+ <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
19443
+ </div>
19444
+ </div>`);
18886
19445
  }
18887
19446
  }
18888
- return mermaidBlock(lines.join("\n"));
19447
+ const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
19448
+ return `${note}
19449
+ <div class="gantt">
19450
+ <div class="gantt-chart">
19451
+ <div class="gantt-header">
19452
+ <div class="gantt-label"></div>
19453
+ <div class="gantt-track gantt-dates">${markers.join("")}</div>
19454
+ </div>
19455
+ ${rows.join("\n")}
19456
+ </div>
19457
+ <div class="gantt-overlay">
19458
+ <div class="gantt-label"></div>
19459
+ <div class="gantt-track">${todayMarker}</div>
19460
+ </div>
19461
+ </div>`;
19462
+ }
19463
+ function statusClass(status) {
19464
+ const s = status.toLowerCase();
19465
+ if (s === "done" || s === "completed") return "flow-done";
19466
+ if (s === "in-progress" || s === "active") return "flow-active";
19467
+ if (s === "blocked") return "flow-blocked";
19468
+ return "flow-default";
18889
19469
  }
18890
19470
  function buildArtifactFlowchart(data) {
18891
19471
  if (data.features.length === 0 && data.epics.length === 0) {
18892
19472
  return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
18893
19473
  }
18894
- const lines = ["graph TD"];
18895
- lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
18896
- lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
18897
- lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
18898
- lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
18899
- const nodeIds = /* @__PURE__ */ new Set();
19474
+ const edges = [];
19475
+ const epicsByFeature = /* @__PURE__ */ new Map();
18900
19476
  for (const epic of data.epics) {
18901
- for (const featureId of epic.linkedFeature) {
18902
- const feature = data.features.find((f) => f.id === featureId);
18903
- if (feature) {
18904
- const fNode = feature.id.replace(/-/g, "_");
18905
- const eNode = epic.id.replace(/-/g, "_");
18906
- if (!nodeIds.has(fNode)) {
18907
- lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
18908
- nodeIds.add(fNode);
18909
- }
18910
- if (!nodeIds.has(eNode)) {
18911
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18912
- nodeIds.add(eNode);
18913
- }
18914
- lines.push(` ${fNode} --> ${eNode}`);
18915
- }
19477
+ for (const fid of epic.linkedFeature) {
19478
+ if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
19479
+ epicsByFeature.get(fid).push(epic.id);
19480
+ edges.push({ from: fid, to: epic.id });
18916
19481
  }
18917
19482
  }
19483
+ const sprintsByEpic = /* @__PURE__ */ new Map();
18918
19484
  for (const sprint of data.sprints) {
18919
- const sNode = sprint.id.replace(/-/g, "_");
18920
- for (const epicId of sprint.linkedEpics) {
18921
- const epic = data.epics.find((e) => e.id === epicId);
18922
- if (epic) {
18923
- const eNode = epic.id.replace(/-/g, "_");
18924
- if (!nodeIds.has(eNode)) {
18925
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18926
- nodeIds.add(eNode);
18927
- }
18928
- if (!nodeIds.has(sNode)) {
18929
- lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
18930
- nodeIds.add(sNode);
18931
- }
18932
- lines.push(` ${eNode} --> ${sNode}`);
18933
- }
18934
- }
18935
- }
18936
- if (nodeIds.size === 0) {
19485
+ for (const eid of sprint.linkedEpics) {
19486
+ if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
19487
+ sprintsByEpic.get(eid).push(sprint.id);
19488
+ edges.push({ from: eid, to: sprint.id });
19489
+ }
19490
+ }
19491
+ const connectedFeatureIds = new Set(epicsByFeature.keys());
19492
+ const connectedEpicIds = /* @__PURE__ */ new Set();
19493
+ for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
19494
+ for (const ids of sprintsByEpic.values()) ids.forEach(() => {
19495
+ });
19496
+ for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
19497
+ const connectedSprintIds = /* @__PURE__ */ new Set();
19498
+ for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
19499
+ const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
19500
+ const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
19501
+ const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
19502
+ if (features.length === 0 && epics.length === 0) {
18937
19503
  return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
18938
19504
  }
18939
- const allItems = [
18940
- ...data.features.map((f) => ({ id: f.id, status: f.status })),
18941
- ...data.epics.map((e) => ({ id: e.id, status: e.status })),
18942
- ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
18943
- ];
18944
- for (const item of allItems) {
18945
- const node = item.id.replace(/-/g, "_");
18946
- if (!nodeIds.has(node)) continue;
18947
- const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
18948
- if (cls) {
18949
- lines.push(` class ${node} ${cls}`);
18950
- }
18951
- }
18952
- return mermaidBlock(lines.join("\n"));
19505
+ const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
19506
+ <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
19507
+ <span class="flow-node-title">${sanitize(title, 35)}</span>
19508
+ </div>`;
19509
+ const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
19510
+ const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
19511
+ const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
19512
+ const edgesJson = JSON.stringify(edges);
19513
+ return `
19514
+ <div class="flow-diagram" id="flow-diagram">
19515
+ <svg class="flow-lines" id="flow-lines"></svg>
19516
+ <div class="flow-columns">
19517
+ <div class="flow-column">
19518
+ <div class="flow-column-header">Features</div>
19519
+ ${featuresHtml}
19520
+ </div>
19521
+ <div class="flow-column">
19522
+ <div class="flow-column-header">Epics</div>
19523
+ ${epicsHtml}
19524
+ </div>
19525
+ <div class="flow-column">
19526
+ <div class="flow-column-header">Sprints</div>
19527
+ ${sprintsHtml}
19528
+ </div>
19529
+ </div>
19530
+ </div>
19531
+ <script>
19532
+ (function() {
19533
+ var edges = ${edgesJson};
19534
+ var container = document.getElementById('flow-diagram');
19535
+ var svg = document.getElementById('flow-lines');
19536
+ if (!container || !svg) return;
19537
+
19538
+ // Build adjacency map (bidirectional) for traversal
19539
+ var adj = {};
19540
+ edges.forEach(function(e) {
19541
+ if (!adj[e.from]) adj[e.from] = [];
19542
+ if (!adj[e.to]) adj[e.to] = [];
19543
+ adj[e.from].push(e.to);
19544
+ adj[e.to].push(e.from);
19545
+ });
19546
+
19547
+ function drawLines() {
19548
+ var rect = container.getBoundingClientRect();
19549
+ svg.setAttribute('width', rect.width);
19550
+ svg.setAttribute('height', rect.height);
19551
+ svg.innerHTML = '';
19552
+
19553
+ edges.forEach(function(edge) {
19554
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
19555
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
19556
+ if (!fromEl || !toEl) return;
19557
+
19558
+ var fr = fromEl.getBoundingClientRect();
19559
+ var tr = toEl.getBoundingClientRect();
19560
+ var x1 = fr.right - rect.left;
19561
+ var y1 = fr.top + fr.height / 2 - rect.top;
19562
+ var x2 = tr.left - rect.left;
19563
+ var y2 = tr.top + tr.height / 2 - rect.top;
19564
+ var mx = (x1 + x2) / 2;
19565
+
19566
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
19567
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
19568
+ path.setAttribute('fill', 'none');
19569
+ path.setAttribute('stroke', '#2a2e3a');
19570
+ path.setAttribute('stroke-width', '1.5');
19571
+ path.dataset.from = edge.from;
19572
+ path.dataset.to = edge.to;
19573
+ svg.appendChild(path);
19574
+ });
19575
+ }
19576
+
19577
+ // Find all nodes reachable from a starting node
19578
+ function findConnected(startId) {
19579
+ var visited = {};
19580
+ var queue = [startId];
19581
+ visited[startId] = true;
19582
+ while (queue.length) {
19583
+ var id = queue.shift();
19584
+ (adj[id] || []).forEach(function(neighbor) {
19585
+ if (!visited[neighbor]) {
19586
+ visited[neighbor] = true;
19587
+ queue.push(neighbor);
19588
+ }
19589
+ });
19590
+ }
19591
+ return visited;
19592
+ }
19593
+
19594
+ function highlight(hoveredId) {
19595
+ var connected = findConnected(hoveredId);
19596
+ container.querySelectorAll('.flow-node').forEach(function(n) {
19597
+ if (connected[n.dataset.flowId]) {
19598
+ n.classList.add('flow-lit');
19599
+ n.classList.remove('flow-dim');
19600
+ } else {
19601
+ n.classList.add('flow-dim');
19602
+ n.classList.remove('flow-lit');
19603
+ }
19604
+ });
19605
+ svg.querySelectorAll('path').forEach(function(p) {
19606
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
19607
+ p.classList.add('flow-line-lit');
19608
+ p.classList.remove('flow-line-dim');
19609
+ } else {
19610
+ p.classList.add('flow-line-dim');
19611
+ p.classList.remove('flow-line-lit');
19612
+ }
19613
+ });
19614
+ }
19615
+
19616
+ function clearHighlight() {
19617
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
19618
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
19619
+ }
19620
+
19621
+ var activeId = null;
19622
+ container.addEventListener('click', function(e) {
19623
+ // Let the ID link navigate normally
19624
+ if (e.target.closest('a')) return;
19625
+
19626
+ var node = e.target.closest('.flow-node');
19627
+ var clickedId = node ? node.dataset.flowId : null;
19628
+
19629
+ if (!clickedId || clickedId === activeId) {
19630
+ activeId = null;
19631
+ clearHighlight();
19632
+ return;
19633
+ }
19634
+
19635
+ activeId = clickedId;
19636
+ highlight(clickedId);
19637
+ });
19638
+
19639
+ requestAnimationFrame(function() { setTimeout(drawLines, 100); });
19640
+ window.addEventListener('resize', drawLines);
19641
+ })();
19642
+ </script>`;
18953
19643
  }
18954
19644
  function buildStatusPie(title, counts) {
18955
19645
  const entries = Object.entries(counts).filter(([, v]) => v > 0);
@@ -19029,8 +19719,7 @@ function overviewPage(data, diagrams, navGroups) {
19029
19719
  ${groupSections}
19030
19720
  ${ungroupedSection}
19031
19721
 
19032
- <div class="section-title">Project Timeline</div>
19033
- ${buildTimelineGantt(diagrams)}
19722
+ <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
19034
19723
 
19035
19724
  <div class="section-title">Artifact Relationships</div>
19036
19725
  ${buildArtifactFlowchart(diagrams)}
@@ -19285,6 +19974,7 @@ function boardPage(data) {
19285
19974
  <span>${escapeHtml(col.status)}</span>
19286
19975
  <span class="count">${col.docs.length}</span>
19287
19976
  </div>
19977
+ <div class="board-column-cards">
19288
19978
  ${col.docs.map(
19289
19979
  (doc) => `
19290
19980
  <div class="board-card">
@@ -19295,6 +19985,7 @@ function boardPage(data) {
19295
19985
  </a>
19296
19986
  </div>`
19297
19987
  ).join("\n")}
19988
+ </div>
19298
19989
  </div>`
19299
19990
  ).join("\n");
19300
19991
  return `
@@ -19320,6 +20011,18 @@ function boardPage(data) {
19320
20011
  `;
19321
20012
  }
19322
20013
 
20014
+ // src/web/templates/pages/timeline.ts
20015
+ function timelinePage(diagrams) {
20016
+ return `
20017
+ <div class="page-header">
20018
+ <h2>Project Timeline</h2>
20019
+ <div class="subtitle">Sprint schedule with linked epics</div>
20020
+ </div>
20021
+
20022
+ ${buildTimelineGantt(diagrams)}
20023
+ `;
20024
+ }
20025
+
19323
20026
  // src/web/router.ts
19324
20027
  function handleRequest(req, res, store, projectName, navGroups) {
19325
20028
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -19341,6 +20044,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19341
20044
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
19342
20045
  return;
19343
20046
  }
20047
+ if (pathname === "/timeline") {
20048
+ const diagrams = getDiagramData(store);
20049
+ const body = timelinePage(diagrams);
20050
+ respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, mainClass: "expanded" }, body));
20051
+ return;
20052
+ }
19344
20053
  if (pathname === "/gar") {
19345
20054
  const report = getGarData(store, projectName);
19346
20055
  const body = garPage(report);
@@ -19414,8 +20123,8 @@ import * as http from "http";
19414
20123
  import { exec } from "child_process";
19415
20124
 
19416
20125
  // src/skills/registry.ts
19417
- import * as fs8 from "fs";
19418
- import * as path8 from "path";
20126
+ import * as fs9 from "fs";
20127
+ import * as path9 from "path";
19419
20128
  import { fileURLToPath } from "url";
19420
20129
  import * as YAML5 from "yaml";
19421
20130
  import matter2 from "gray-matter";
@@ -19458,7 +20167,7 @@ Be thorough but concise. Focus on actionable insights.`,
19458
20167
  };
19459
20168
 
19460
20169
  // src/skills/builtin/jira/tools.ts
19461
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
20170
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19462
20171
 
19463
20172
  // src/skills/builtin/jira/client.ts
19464
20173
  var JiraClient = class {
@@ -19468,8 +20177,8 @@ var JiraClient = class {
19468
20177
  this.baseUrl = `https://${config2.host}/rest/api/2`;
19469
20178
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
19470
20179
  }
19471
- async request(path20, method = "GET", body) {
19472
- const url2 = `${this.baseUrl}${path20}`;
20180
+ async request(path21, method = "GET", body) {
20181
+ const url2 = `${this.baseUrl}${path21}`;
19473
20182
  const headers = {
19474
20183
  Authorization: this.authHeader,
19475
20184
  "Content-Type": "application/json",
@@ -19483,7 +20192,7 @@ var JiraClient = class {
19483
20192
  if (!response.ok) {
19484
20193
  const text = await response.text().catch(() => "");
19485
20194
  throw new Error(
19486
- `Jira API error ${response.status} ${method} ${path20}: ${text}`
20195
+ `Jira API error ${response.status} ${method} ${path21}: ${text}`
19487
20196
  );
19488
20197
  }
19489
20198
  if (response.status === 204) return void 0;
@@ -19567,7 +20276,7 @@ function createJiraTools(store) {
19567
20276
  const jiraUserConfig = loadUserConfig().jira;
19568
20277
  return [
19569
20278
  // --- Local read tools ---
19570
- tool19(
20279
+ tool20(
19571
20280
  "list_jira_issues",
19572
20281
  "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
19573
20282
  {
@@ -19595,7 +20304,7 @@ function createJiraTools(store) {
19595
20304
  },
19596
20305
  { annotations: { readOnlyHint: true } }
19597
20306
  ),
19598
- tool19(
20307
+ tool20(
19599
20308
  "get_jira_issue",
19600
20309
  "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
19601
20310
  {
@@ -19628,7 +20337,7 @@ function createJiraTools(store) {
19628
20337
  { annotations: { readOnlyHint: true } }
19629
20338
  ),
19630
20339
  // --- Jira → Local tools ---
19631
- tool19(
20340
+ tool20(
19632
20341
  "pull_jira_issue",
19633
20342
  "Fetch a single Jira issue by key and create/update a local JI-xxx document",
19634
20343
  {
@@ -19675,7 +20384,7 @@ function createJiraTools(store) {
19675
20384
  };
19676
20385
  }
19677
20386
  ),
19678
- tool19(
20387
+ tool20(
19679
20388
  "pull_jira_issues_jql",
19680
20389
  "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
19681
20390
  {
@@ -19723,7 +20432,7 @@ function createJiraTools(store) {
19723
20432
  }
19724
20433
  ),
19725
20434
  // --- Local → Jira tools ---
19726
- tool19(
20435
+ tool20(
19727
20436
  "push_artifact_to_jira",
19728
20437
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
19729
20438
  {
@@ -19784,7 +20493,7 @@ function createJiraTools(store) {
19784
20493
  }
19785
20494
  ),
19786
20495
  // --- Bidirectional sync ---
19787
- tool19(
20496
+ tool20(
19788
20497
  "sync_jira_issue",
19789
20498
  "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
19790
20499
  {
@@ -19825,7 +20534,7 @@ function createJiraTools(store) {
19825
20534
  }
19826
20535
  ),
19827
20536
  // --- Local link tool ---
19828
- tool19(
20537
+ tool20(
19829
20538
  "link_artifact_to_jira",
19830
20539
  "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
19831
20540
  {
@@ -19913,13 +20622,13 @@ var jiraSkill = {
19913
20622
  **Available tools:**
19914
20623
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19915
20624
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
19916
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
20625
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
19917
20626
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
19918
20627
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
19919
20628
 
19920
20629
  **As Tech Lead, use Jira integration to:**
19921
20630
  - Pull technical issues and bugs for sprint planning and estimation
19922
- - Push epics and technical decisions to Jira for cross-team visibility
20631
+ - Push epics, tasks, and technical decisions to Jira for cross-team visibility
19923
20632
  - Bidirectional sync to keep local governance and Jira in alignment
19924
20633
  - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
19925
20634
  "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
@@ -19933,16 +20642,420 @@ var jiraSkill = {
19933
20642
 
19934
20643
  **As Delivery Manager, use Jira integration to:**
19935
20644
  - Pull sprint issues for tracking progress and blockers
19936
- - Push actions and decisions to Jira for stakeholder visibility
20645
+ - Push actions, decisions, and tasks to Jira for stakeholder visibility
19937
20646
  - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
19938
20647
  - Sync status between Marvin governance items and Jira issues`
19939
20648
  }
19940
20649
  };
19941
20650
 
20651
+ // src/skills/builtin/prd-generator/tools.ts
20652
+ import * as fs8 from "fs";
20653
+ import * as path8 from "path";
20654
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
20655
+ var PRIORITY_ORDER2 = {
20656
+ critical: 0,
20657
+ high: 1,
20658
+ medium: 2,
20659
+ low: 3
20660
+ };
20661
+ function priorityRank2(p) {
20662
+ return PRIORITY_ORDER2[p ?? ""] ?? 99;
20663
+ }
20664
+ function gatherContext(store, focusFeature, includeDecisions = true, includeQuestions = true) {
20665
+ const allFeatures = store.list({ type: "feature" });
20666
+ const allEpics = store.list({ type: "epic" });
20667
+ const allTasks = store.list({ type: "task" });
20668
+ const allDecisions = includeDecisions ? store.list({ type: "decision" }) : [];
20669
+ const allQuestions = includeQuestions ? store.list({ type: "question" }) : [];
20670
+ const allActions = store.list({ type: "action" });
20671
+ let features = allFeatures;
20672
+ let epics = allEpics;
20673
+ let tasks = allTasks;
20674
+ if (focusFeature) {
20675
+ features = features.filter((f) => f.frontmatter.id === focusFeature);
20676
+ const featureIds = new Set(features.map((f) => f.frontmatter.id));
20677
+ epics = epics.filter(
20678
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).some((id) => featureIds.has(id))
20679
+ );
20680
+ const epicIds2 = new Set(epics.map((e) => e.frontmatter.id));
20681
+ tasks = tasks.filter(
20682
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).some((id) => epicIds2.has(id))
20683
+ );
20684
+ }
20685
+ const featuresByStatus = {};
20686
+ for (const f of features) {
20687
+ featuresByStatus[f.frontmatter.status] = (featuresByStatus[f.frontmatter.status] ?? 0) + 1;
20688
+ }
20689
+ const epicsByStatus = {};
20690
+ for (const e of epics) {
20691
+ epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
20692
+ }
20693
+ const epicIds = new Set(epics.map((e) => e.frontmatter.id));
20694
+ return {
20695
+ features: features.sort((a, b) => priorityRank2(a.frontmatter.priority) - priorityRank2(b.frontmatter.priority)).map((f) => ({
20696
+ id: f.frontmatter.id,
20697
+ title: f.frontmatter.title,
20698
+ status: f.frontmatter.status,
20699
+ priority: f.frontmatter.priority ?? "medium",
20700
+ content: f.content,
20701
+ linkedEpicCount: epics.filter(
20702
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
20703
+ ).length
20704
+ })),
20705
+ epics: epics.map((e) => ({
20706
+ id: e.frontmatter.id,
20707
+ title: e.frontmatter.title,
20708
+ status: e.frontmatter.status,
20709
+ linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
20710
+ targetDate: e.frontmatter.targetDate ?? null,
20711
+ estimatedEffort: e.frontmatter.estimatedEffort ?? null,
20712
+ content: e.content,
20713
+ linkedTaskCount: tasks.filter(
20714
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
20715
+ ).length
20716
+ })),
20717
+ tasks: tasks.map((t) => ({
20718
+ id: t.frontmatter.id,
20719
+ title: t.frontmatter.title,
20720
+ status: t.frontmatter.status,
20721
+ linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
20722
+ acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
20723
+ technicalNotes: t.frontmatter.technicalNotes ?? null,
20724
+ complexity: t.frontmatter.complexity ?? null,
20725
+ estimatedPoints: t.frontmatter.estimatedPoints ?? null,
20726
+ priority: t.frontmatter.priority ?? null
20727
+ })),
20728
+ decisions: allDecisions.map((d) => ({
20729
+ id: d.frontmatter.id,
20730
+ title: d.frontmatter.title,
20731
+ status: d.frontmatter.status,
20732
+ content: d.content
20733
+ })),
20734
+ questions: allQuestions.map((q) => ({
20735
+ id: q.frontmatter.id,
20736
+ title: q.frontmatter.title,
20737
+ status: q.frontmatter.status,
20738
+ content: q.content
20739
+ })),
20740
+ actions: allActions.filter((a) => {
20741
+ if (!focusFeature) return true;
20742
+ const tags = a.frontmatter.tags ?? [];
20743
+ return tags.some((t) => t.startsWith("epic:") && epicIds.has(t.replace("epic:", "")));
20744
+ }).map((a) => ({
20745
+ id: a.frontmatter.id,
20746
+ title: a.frontmatter.title,
20747
+ status: a.frontmatter.status,
20748
+ owner: a.frontmatter.owner ?? null,
20749
+ priority: a.frontmatter.priority ?? null,
20750
+ dueDate: a.frontmatter.dueDate ?? null
20751
+ })),
20752
+ summary: {
20753
+ totalFeatures: features.length,
20754
+ totalEpics: epics.length,
20755
+ totalTasks: tasks.length,
20756
+ featuresByStatus,
20757
+ epicsByStatus
20758
+ }
20759
+ };
20760
+ }
20761
+ function generateTaskMasterPrd(title, ctx, projectOverview) {
20762
+ const lines = [];
20763
+ lines.push(`# ${title}`);
20764
+ lines.push("");
20765
+ lines.push("## Project Overview");
20766
+ if (projectOverview) {
20767
+ lines.push(projectOverview);
20768
+ } else if (ctx.features.length > 0) {
20769
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
20770
+ }
20771
+ lines.push("");
20772
+ lines.push("## Goals");
20773
+ for (const f of ctx.features) {
20774
+ lines.push(`- **${f.title}** (${f.id}, Priority: ${f.priority}) \u2014 ${f.status}`);
20775
+ }
20776
+ lines.push("");
20777
+ lines.push("## Features and Requirements");
20778
+ lines.push("");
20779
+ for (const feature of ctx.features) {
20780
+ lines.push(`### ${feature.title} (${feature.id}) \u2014 Priority: ${feature.priority}`);
20781
+ lines.push("");
20782
+ if (feature.content) {
20783
+ lines.push(feature.content);
20784
+ lines.push("");
20785
+ }
20786
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
20787
+ if (featureEpics.length > 0) {
20788
+ lines.push("#### User Stories / Epics");
20789
+ lines.push("");
20790
+ for (const epic of featureEpics) {
20791
+ const effort = epic.estimatedEffort ? `, Effort: ${epic.estimatedEffort}` : "";
20792
+ lines.push(`- **${epic.id}: ${epic.title}** \u2014 Status: ${epic.status}${effort}`);
20793
+ if (epic.content) {
20794
+ lines.push(` ${epic.content.split("\n")[0]}`);
20795
+ }
20796
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
20797
+ if (epicTasks.length > 0) {
20798
+ lines.push("");
20799
+ lines.push("#### Implementation Tasks");
20800
+ lines.push("");
20801
+ for (const task of epicTasks) {
20802
+ const complexity = task.complexity ? `, Complexity: ${task.complexity}` : "";
20803
+ const points = task.estimatedPoints != null ? `, Points: ${task.estimatedPoints}` : "";
20804
+ lines.push(`- **${task.id}: ${task.title}**${complexity}${points}`);
20805
+ if (task.acceptanceCriteria) {
20806
+ lines.push(` Acceptance Criteria: ${task.acceptanceCriteria}`);
20807
+ }
20808
+ }
20809
+ }
20810
+ }
20811
+ lines.push("");
20812
+ }
20813
+ }
20814
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
20815
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
20816
+ const technicalNotes = ctx.tasks.filter((t) => t.technicalNotes).map((t) => `- **${t.id}**: ${t.technicalNotes}`);
20817
+ if (approvedDecisions.length > 0 || openQuestions.length > 0 || technicalNotes.length > 0) {
20818
+ lines.push("## Technical Considerations");
20819
+ lines.push("");
20820
+ if (approvedDecisions.length > 0) {
20821
+ lines.push("### Key Decisions");
20822
+ for (const d of approvedDecisions) {
20823
+ lines.push(`- **${d.id}: ${d.title}** \u2014 ${d.content.split("\n")[0]}`);
20824
+ }
20825
+ lines.push("");
20826
+ }
20827
+ if (technicalNotes.length > 0) {
20828
+ lines.push("### Technical Notes");
20829
+ for (const note of technicalNotes) {
20830
+ lines.push(note);
20831
+ }
20832
+ lines.push("");
20833
+ }
20834
+ if (openQuestions.length > 0) {
20835
+ lines.push("### Open Questions");
20836
+ for (const q of openQuestions) {
20837
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
20838
+ }
20839
+ lines.push("");
20840
+ }
20841
+ }
20842
+ lines.push("## Implementation Priorities");
20843
+ lines.push("");
20844
+ let priorityIdx = 1;
20845
+ for (const feature of ctx.features) {
20846
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
20847
+ const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
20848
+ return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
20849
+ });
20850
+ if (featureEpics.length === 0) continue;
20851
+ lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
20852
+ for (const epic of featureEpics) {
20853
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
20854
+ lines.push(` - ${epic.id}: ${epic.title} (${epic.status}) \u2014 ${epicTasks.length} task(s)`);
20855
+ }
20856
+ priorityIdx++;
20857
+ }
20858
+ lines.push("");
20859
+ return lines.join("\n");
20860
+ }
20861
+ function generateClaudeCodePrd(title, ctx, projectOverview) {
20862
+ const lines = [];
20863
+ lines.push(`# ${title}`);
20864
+ lines.push("");
20865
+ lines.push("## Overview");
20866
+ if (projectOverview) {
20867
+ lines.push(projectOverview);
20868
+ } else if (ctx.features.length > 0) {
20869
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
20870
+ }
20871
+ lines.push("");
20872
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
20873
+ if (approvedDecisions.length > 0) {
20874
+ lines.push("## Architecture & Technical Decisions");
20875
+ lines.push("");
20876
+ for (const d of approvedDecisions) {
20877
+ lines.push(`### ${d.id}: ${d.title}`);
20878
+ lines.push(d.content);
20879
+ lines.push("");
20880
+ }
20881
+ }
20882
+ lines.push("## Implementation Plan");
20883
+ lines.push("");
20884
+ const priorityGroups = {};
20885
+ for (const f of ctx.features) {
20886
+ const group = f.priority === "critical" || f.priority === "high" ? "Phase 1: High Priority" : "Phase 2: Medium & Low Priority";
20887
+ if (!priorityGroups[group]) priorityGroups[group] = [];
20888
+ priorityGroups[group].push(f);
20889
+ }
20890
+ for (const [phase, features] of Object.entries(priorityGroups)) {
20891
+ lines.push(`### ${phase}`);
20892
+ lines.push("");
20893
+ for (const feature of features) {
20894
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
20895
+ for (const epic of featureEpics) {
20896
+ lines.push(`- [ ] ${epic.id}: ${epic.title}`);
20897
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
20898
+ for (const task of epicTasks) {
20899
+ const complexity = task.complexity ? `complexity: ${task.complexity}` : "";
20900
+ const points = task.estimatedPoints != null ? `points: ${task.estimatedPoints}` : "";
20901
+ const meta3 = [complexity, points].filter(Boolean).join(", ");
20902
+ lines.push(` - [ ] ${task.id}: ${task.title}${meta3 ? ` (${meta3})` : ""}`);
20903
+ if (task.acceptanceCriteria) {
20904
+ lines.push(` - Acceptance: ${task.acceptanceCriteria}`);
20905
+ }
20906
+ if (task.technicalNotes) {
20907
+ lines.push(` - Notes: ${task.technicalNotes}`);
20908
+ }
20909
+ }
20910
+ }
20911
+ }
20912
+ lines.push("");
20913
+ }
20914
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
20915
+ if (openQuestions.length > 0) {
20916
+ lines.push("## Open Questions");
20917
+ lines.push("");
20918
+ for (const q of openQuestions) {
20919
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
20920
+ }
20921
+ lines.push("");
20922
+ }
20923
+ return lines.join("\n");
20924
+ }
20925
+ function createPrdTools(store) {
20926
+ return [
20927
+ tool21(
20928
+ "gather_prd_context",
20929
+ "Aggregate all governance artifacts (features, epics, tasks, decisions, questions, actions) into structured JSON for PRD generation",
20930
+ {
20931
+ focusFeature: external_exports.string().optional().describe("Filter context to a specific feature ID (e.g. 'F-001')"),
20932
+ includeDecisions: external_exports.boolean().optional().describe("Include decisions in context (default: true)"),
20933
+ includeQuestions: external_exports.boolean().optional().describe("Include questions in context (default: true)")
20934
+ },
20935
+ async (args) => {
20936
+ const ctx = gatherContext(store, args.focusFeature, args.includeDecisions ?? true, args.includeQuestions ?? true);
20937
+ return {
20938
+ content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }]
20939
+ };
20940
+ },
20941
+ { annotations: { readOnlyHint: true } }
20942
+ ),
20943
+ tool21(
20944
+ "generate_prd",
20945
+ "Generate a PRD document from governance artifacts and save it as a PRD-xxx document",
20946
+ {
20947
+ title: external_exports.string().describe("PRD title"),
20948
+ format: external_exports.enum(["taskmaster", "claude-code"]).describe("Output format: 'taskmaster' for Claude TaskMaster parse_prd, 'claude-code' for Claude Code consumption"),
20949
+ projectOverview: external_exports.string().optional().describe("Project overview text (synthesized from features if not provided)"),
20950
+ focusFeature: external_exports.string().optional().describe("Focus on a specific feature ID (e.g. 'F-001')"),
20951
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for the PRD document")
20952
+ },
20953
+ async (args) => {
20954
+ const ctx = gatherContext(store, args.focusFeature);
20955
+ const prdContent = args.format === "taskmaster" ? generateTaskMasterPrd(args.title, ctx, args.projectOverview) : generateClaudeCodePrd(args.title, ctx, args.projectOverview);
20956
+ const frontmatter = {
20957
+ title: args.title,
20958
+ status: "draft",
20959
+ format: args.format
20960
+ };
20961
+ if (args.focusFeature) frontmatter.focusFeature = args.focusFeature;
20962
+ if (args.tags) frontmatter.tags = args.tags;
20963
+ const doc = store.create("prd", frontmatter, prdContent);
20964
+ return {
20965
+ content: [
20966
+ {
20967
+ type: "text",
20968
+ text: `Generated PRD ${doc.frontmatter.id}: "${args.title}" (format: ${args.format}, ${ctx.summary.totalFeatures} features, ${ctx.summary.totalEpics} epics, ${ctx.summary.totalTasks} tasks)`
20969
+ }
20970
+ ]
20971
+ };
20972
+ }
20973
+ ),
20974
+ tool21(
20975
+ "export_prd",
20976
+ "Export a PRD document to a file path for external consumption (e.g. by Claude TaskMaster or Claude Code)",
20977
+ {
20978
+ prdId: external_exports.string().describe("PRD document ID (e.g. 'PRD-001')"),
20979
+ outputPath: external_exports.string().describe("File path to write the PRD content to")
20980
+ },
20981
+ async (args) => {
20982
+ const doc = store.get(args.prdId);
20983
+ if (!doc) {
20984
+ return {
20985
+ content: [{ type: "text", text: `PRD ${args.prdId} not found` }],
20986
+ isError: true
20987
+ };
20988
+ }
20989
+ const outputDir = path8.dirname(args.outputPath);
20990
+ fs8.mkdirSync(outputDir, { recursive: true });
20991
+ fs8.writeFileSync(args.outputPath, doc.content, "utf-8");
20992
+ return {
20993
+ content: [
20994
+ {
20995
+ type: "text",
20996
+ text: `Exported PRD ${args.prdId} to ${args.outputPath}`
20997
+ }
20998
+ ]
20999
+ };
21000
+ }
21001
+ )
21002
+ ];
21003
+ }
21004
+
21005
+ // src/skills/builtin/prd-generator/index.ts
21006
+ var prdGeneratorSkill = {
21007
+ id: "prd-generator",
21008
+ name: "PRD Generator",
21009
+ description: "Generate PRDs from governance artifacts for TaskMaster or Claude Code",
21010
+ version: "1.0.0",
21011
+ format: "builtin-ts",
21012
+ documentTypeRegistrations: [
21013
+ { type: "prd", dirName: "prds", idPrefix: "PRD" }
21014
+ ],
21015
+ tools: (store) => createPrdTools(store),
21016
+ promptFragments: {
21017
+ "tech-lead": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
21018
+
21019
+ **Available tools:**
21020
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, decisions, questions, and actions into structured JSON for analysis
21021
+ - \`generate_prd\` \u2014 generate a formatted PRD document and save it as PRD-xxx. Supports "taskmaster" format (for Claude TaskMaster parse_prd) and "claude-code" format (for Claude Code consumption)
21022
+ - \`export_prd\` \u2014 export a PRD document to a file path for external use
21023
+
21024
+ **As Tech Lead, use PRD generation to:**
21025
+ - Create comprehensive PRDs that capture the full governance context
21026
+ - Export TaskMaster-format PRDs for automated task breakdown via \`parse_prd\`
21027
+ - Export Claude Code-format PRDs as implementation plans with checklists
21028
+ - Focus PRDs on specific features using the focusFeature parameter`,
21029
+ "delivery-manager": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
21030
+
21031
+ **Available tools:**
21032
+ - \`gather_prd_context\` \u2014 aggregate all governance artifacts into structured JSON for review
21033
+ - \`generate_prd\` \u2014 generate a formatted PRD document (taskmaster or claude-code format)
21034
+ - \`export_prd\` \u2014 export a PRD to a file path
21035
+
21036
+ **As Delivery Manager, use PRD generation to:**
21037
+ - Generate PRDs for stakeholder communication and project documentation
21038
+ - Review aggregated project context before sprint planning
21039
+ - Export PRDs to share with external teams or tools`,
21040
+ "product-owner": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
21041
+
21042
+ **Available tools:**
21043
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, and decisions into structured JSON
21044
+ - \`generate_prd\` \u2014 generate a formatted PRD document
21045
+ - \`export_prd\` \u2014 export a PRD to a file path
21046
+
21047
+ **As Product Owner, use PRD generation to:**
21048
+ - Generate PRDs that capture feature requirements and priorities
21049
+ - Review the complete governance context for product planning
21050
+ - Export PRDs for stakeholder review and sign-off`
21051
+ }
21052
+ };
21053
+
19942
21054
  // src/skills/registry.ts
19943
21055
  var BUILTIN_SKILLS = {
19944
21056
  "governance-review": governanceReviewSkill,
19945
- "jira": jiraSkill
21057
+ "jira": jiraSkill,
21058
+ "prd-generator": prdGeneratorSkill
19946
21059
  };
19947
21060
  var GOVERNANCE_TOOL_NAMES = [
19948
21061
  "mcp__marvin-governance__list_decisions",
@@ -19963,13 +21076,13 @@ var GOVERNANCE_TOOL_NAMES = [
19963
21076
  ];
19964
21077
  function getBuiltinSkillsDir() {
19965
21078
  const thisFile = fileURLToPath(import.meta.url);
19966
- return path8.join(path8.dirname(thisFile), "builtin");
21079
+ return path9.join(path9.dirname(thisFile), "builtin");
19967
21080
  }
19968
21081
  function loadSkillFromDirectory(dirPath) {
19969
- const skillMdPath = path8.join(dirPath, "SKILL.md");
19970
- if (!fs8.existsSync(skillMdPath)) return void 0;
21082
+ const skillMdPath = path9.join(dirPath, "SKILL.md");
21083
+ if (!fs9.existsSync(skillMdPath)) return void 0;
19971
21084
  try {
19972
- const raw = fs8.readFileSync(skillMdPath, "utf-8");
21085
+ const raw = fs9.readFileSync(skillMdPath, "utf-8");
19973
21086
  const { data, content } = matter2(raw);
19974
21087
  if (!data.name || !data.description) return void 0;
19975
21088
  const metadata = data.metadata ?? {};
@@ -19980,13 +21093,13 @@ function loadSkillFromDirectory(dirPath) {
19980
21093
  if (wildcardPrompt) {
19981
21094
  promptFragments["*"] = wildcardPrompt;
19982
21095
  }
19983
- const personasDir = path8.join(dirPath, "personas");
19984
- if (fs8.existsSync(personasDir)) {
21096
+ const personasDir = path9.join(dirPath, "personas");
21097
+ if (fs9.existsSync(personasDir)) {
19985
21098
  try {
19986
- for (const file2 of fs8.readdirSync(personasDir)) {
21099
+ for (const file2 of fs9.readdirSync(personasDir)) {
19987
21100
  if (!file2.endsWith(".md")) continue;
19988
21101
  const personaId = file2.replace(/\.md$/, "");
19989
- const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
21102
+ const personaPrompt = fs9.readFileSync(path9.join(personasDir, file2), "utf-8").trim();
19990
21103
  if (personaPrompt) {
19991
21104
  promptFragments[personaId] = personaPrompt;
19992
21105
  }
@@ -19995,10 +21108,10 @@ function loadSkillFromDirectory(dirPath) {
19995
21108
  }
19996
21109
  }
19997
21110
  let actions;
19998
- const actionsPath = path8.join(dirPath, "actions.yaml");
19999
- if (fs8.existsSync(actionsPath)) {
21111
+ const actionsPath = path9.join(dirPath, "actions.yaml");
21112
+ if (fs9.existsSync(actionsPath)) {
20000
21113
  try {
20001
- const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
21114
+ const actionsRaw = fs9.readFileSync(actionsPath, "utf-8");
20002
21115
  actions = YAML5.parse(actionsRaw);
20003
21116
  } catch {
20004
21117
  }
@@ -20025,10 +21138,10 @@ function loadAllSkills(marvinDir) {
20025
21138
  }
20026
21139
  try {
20027
21140
  const builtinDir = getBuiltinSkillsDir();
20028
- if (fs8.existsSync(builtinDir)) {
20029
- for (const entry of fs8.readdirSync(builtinDir)) {
20030
- const entryPath = path8.join(builtinDir, entry);
20031
- if (!fs8.statSync(entryPath).isDirectory()) continue;
21141
+ if (fs9.existsSync(builtinDir)) {
21142
+ for (const entry of fs9.readdirSync(builtinDir)) {
21143
+ const entryPath = path9.join(builtinDir, entry);
21144
+ if (!fs9.statSync(entryPath).isDirectory()) continue;
20032
21145
  if (skills.has(entry)) continue;
20033
21146
  const skill = loadSkillFromDirectory(entryPath);
20034
21147
  if (skill) skills.set(skill.id, skill);
@@ -20037,18 +21150,18 @@ function loadAllSkills(marvinDir) {
20037
21150
  } catch {
20038
21151
  }
20039
21152
  if (marvinDir) {
20040
- const skillsDir = path8.join(marvinDir, "skills");
20041
- if (fs8.existsSync(skillsDir)) {
21153
+ const skillsDir = path9.join(marvinDir, "skills");
21154
+ if (fs9.existsSync(skillsDir)) {
20042
21155
  let entries;
20043
21156
  try {
20044
- entries = fs8.readdirSync(skillsDir);
21157
+ entries = fs9.readdirSync(skillsDir);
20045
21158
  } catch {
20046
21159
  entries = [];
20047
21160
  }
20048
21161
  for (const entry of entries) {
20049
- const entryPath = path8.join(skillsDir, entry);
21162
+ const entryPath = path9.join(skillsDir, entry);
20050
21163
  try {
20051
- if (fs8.statSync(entryPath).isDirectory()) {
21164
+ if (fs9.statSync(entryPath).isDirectory()) {
20052
21165
  const skill = loadSkillFromDirectory(entryPath);
20053
21166
  if (skill) skills.set(skill.id, skill);
20054
21167
  continue;
@@ -20058,7 +21171,7 @@ function loadAllSkills(marvinDir) {
20058
21171
  }
20059
21172
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
20060
21173
  try {
20061
- const raw = fs8.readFileSync(entryPath, "utf-8");
21174
+ const raw = fs9.readFileSync(entryPath, "utf-8");
20062
21175
  const parsed = YAML5.parse(raw);
20063
21176
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
20064
21177
  const skill = {
@@ -20163,12 +21276,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
20163
21276
  return agents;
20164
21277
  }
20165
21278
  function migrateYamlToSkillMd(yamlPath, outputDir) {
20166
- const raw = fs8.readFileSync(yamlPath, "utf-8");
21279
+ const raw = fs9.readFileSync(yamlPath, "utf-8");
20167
21280
  const parsed = YAML5.parse(raw);
20168
21281
  if (!parsed?.id || !parsed?.name) {
20169
21282
  throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
20170
21283
  }
20171
- fs8.mkdirSync(outputDir, { recursive: true });
21284
+ fs9.mkdirSync(outputDir, { recursive: true });
20172
21285
  const frontmatter = {
20173
21286
  name: parsed.id,
20174
21287
  description: parsed.description ?? ""
@@ -20182,15 +21295,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
20182
21295
  const skillMd = matter2.stringify(wildcardPrompt ? `
20183
21296
  ${wildcardPrompt}
20184
21297
  ` : "\n", frontmatter);
20185
- fs8.writeFileSync(path8.join(outputDir, "SKILL.md"), skillMd, "utf-8");
21298
+ fs9.writeFileSync(path9.join(outputDir, "SKILL.md"), skillMd, "utf-8");
20186
21299
  if (promptFragments) {
20187
21300
  const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
20188
21301
  if (personaKeys.length > 0) {
20189
- const personasDir = path8.join(outputDir, "personas");
20190
- fs8.mkdirSync(personasDir, { recursive: true });
21302
+ const personasDir = path9.join(outputDir, "personas");
21303
+ fs9.mkdirSync(personasDir, { recursive: true });
20191
21304
  for (const personaId of personaKeys) {
20192
- fs8.writeFileSync(
20193
- path8.join(personasDir, `${personaId}.md`),
21305
+ fs9.writeFileSync(
21306
+ path9.join(personasDir, `${personaId}.md`),
20194
21307
  `${promptFragments[personaId]}
20195
21308
  `,
20196
21309
  "utf-8"
@@ -20200,8 +21313,8 @@ ${wildcardPrompt}
20200
21313
  }
20201
21314
  const actions = parsed.actions;
20202
21315
  if (actions && actions.length > 0) {
20203
- fs8.writeFileSync(
20204
- path8.join(outputDir, "actions.yaml"),
21316
+ fs9.writeFileSync(
21317
+ path9.join(outputDir, "actions.yaml"),
20205
21318
  YAML5.stringify(actions),
20206
21319
  "utf-8"
20207
21320
  );
@@ -20280,7 +21393,7 @@ function openBrowser(url2) {
20280
21393
  var runningServer = null;
20281
21394
  function createWebTools(store, projectName, navGroups) {
20282
21395
  return [
20283
- tool20(
21396
+ tool22(
20284
21397
  "start_web_dashboard",
20285
21398
  "Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
20286
21399
  {
@@ -20312,7 +21425,7 @@ function createWebTools(store, projectName, navGroups) {
20312
21425
  };
20313
21426
  }
20314
21427
  ),
20315
- tool20(
21428
+ tool22(
20316
21429
  "stop_web_dashboard",
20317
21430
  "Stop the running Marvin web dashboard.",
20318
21431
  {},
@@ -20332,7 +21445,7 @@ function createWebTools(store, projectName, navGroups) {
20332
21445
  };
20333
21446
  }
20334
21447
  ),
20335
- tool20(
21448
+ tool22(
20336
21449
  "get_web_dashboard_urls",
20337
21450
  "Get all available dashboard page URLs. The dashboard must be running.",
20338
21451
  {},
@@ -20358,7 +21471,7 @@ function createWebTools(store, projectName, navGroups) {
20358
21471
  },
20359
21472
  { annotations: { readOnlyHint: true } }
20360
21473
  ),
20361
- tool20(
21474
+ tool22(
20362
21475
  "get_dashboard_overview",
20363
21476
  "Get the project overview data: document type counts and recent activity. Works without the web server running.",
20364
21477
  {},
@@ -20380,7 +21493,7 @@ function createWebTools(store, projectName, navGroups) {
20380
21493
  },
20381
21494
  { annotations: { readOnlyHint: true } }
20382
21495
  ),
20383
- tool20(
21496
+ tool22(
20384
21497
  "get_dashboard_gar",
20385
21498
  "Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
20386
21499
  {},
@@ -20392,7 +21505,7 @@ function createWebTools(store, projectName, navGroups) {
20392
21505
  },
20393
21506
  { annotations: { readOnlyHint: true } }
20394
21507
  ),
20395
- tool20(
21508
+ tool22(
20396
21509
  "get_dashboard_board",
20397
21510
  "Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
20398
21511
  {
@@ -20475,8 +21588,8 @@ function slugify3(text) {
20475
21588
  }
20476
21589
 
20477
21590
  // src/sources/manifest.ts
20478
- import * as fs9 from "fs";
20479
- import * as path9 from "path";
21591
+ import * as fs10 from "fs";
21592
+ import * as path10 from "path";
20480
21593
  import * as crypto from "crypto";
20481
21594
  import * as YAML6 from "yaml";
20482
21595
  var MANIFEST_FILE = ".manifest.yaml";
@@ -20489,37 +21602,37 @@ var SourceManifestManager = class {
20489
21602
  manifestPath;
20490
21603
  sourcesDir;
20491
21604
  constructor(marvinDir) {
20492
- this.sourcesDir = path9.join(marvinDir, "sources");
20493
- this.manifestPath = path9.join(this.sourcesDir, MANIFEST_FILE);
21605
+ this.sourcesDir = path10.join(marvinDir, "sources");
21606
+ this.manifestPath = path10.join(this.sourcesDir, MANIFEST_FILE);
20494
21607
  this.manifest = this.load();
20495
21608
  }
20496
21609
  load() {
20497
- if (!fs9.existsSync(this.manifestPath)) {
21610
+ if (!fs10.existsSync(this.manifestPath)) {
20498
21611
  return emptyManifest();
20499
21612
  }
20500
- const raw = fs9.readFileSync(this.manifestPath, "utf-8");
21613
+ const raw = fs10.readFileSync(this.manifestPath, "utf-8");
20501
21614
  const parsed = YAML6.parse(raw);
20502
21615
  return parsed ?? emptyManifest();
20503
21616
  }
20504
21617
  save() {
20505
- fs9.mkdirSync(this.sourcesDir, { recursive: true });
20506
- fs9.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
21618
+ fs10.mkdirSync(this.sourcesDir, { recursive: true });
21619
+ fs10.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
20507
21620
  }
20508
21621
  scan() {
20509
21622
  const added = [];
20510
21623
  const changed = [];
20511
21624
  const removed = [];
20512
- if (!fs9.existsSync(this.sourcesDir)) {
21625
+ if (!fs10.existsSync(this.sourcesDir)) {
20513
21626
  return { added, changed, removed };
20514
21627
  }
20515
21628
  const onDisk = new Set(
20516
- fs9.readdirSync(this.sourcesDir).filter((f) => {
20517
- const ext = path9.extname(f).toLowerCase();
21629
+ fs10.readdirSync(this.sourcesDir).filter((f) => {
21630
+ const ext = path10.extname(f).toLowerCase();
20518
21631
  return SOURCE_EXTENSIONS.includes(ext);
20519
21632
  })
20520
21633
  );
20521
21634
  for (const fileName of onDisk) {
20522
- const filePath = path9.join(this.sourcesDir, fileName);
21635
+ const filePath = path10.join(this.sourcesDir, fileName);
20523
21636
  const hash2 = this.hashFile(filePath);
20524
21637
  const existing = this.manifest.files[fileName];
20525
21638
  if (!existing) {
@@ -20582,7 +21695,7 @@ var SourceManifestManager = class {
20582
21695
  this.save();
20583
21696
  }
20584
21697
  hashFile(filePath) {
20585
- const content = fs9.readFileSync(filePath);
21698
+ const content = fs10.readFileSync(filePath);
20586
21699
  return crypto.createHash("sha256").update(content).digest("hex");
20587
21700
  }
20588
21701
  };
@@ -20597,8 +21710,8 @@ async function startSession(options) {
20597
21710
  const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
20598
21711
  const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
20599
21712
  const sessionStore = new SessionStore(marvinDir);
20600
- const sourcesDir = path10.join(marvinDir, "sources");
20601
- const hasSourcesDir = fs10.existsSync(sourcesDir);
21713
+ const sourcesDir = path11.join(marvinDir, "sources");
21714
+ const hasSourcesDir = fs11.existsSync(sourcesDir);
20602
21715
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20603
21716
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20604
21717
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
@@ -21074,13 +22187,13 @@ async function setApiKey() {
21074
22187
  }
21075
22188
 
21076
22189
  // src/cli/commands/ingest.ts
21077
- import * as fs12 from "fs";
21078
- import * as path12 from "path";
22190
+ import * as fs13 from "fs";
22191
+ import * as path13 from "path";
21079
22192
  import chalk8 from "chalk";
21080
22193
 
21081
22194
  // src/sources/ingest.ts
21082
- import * as fs11 from "fs";
21083
- import * as path11 from "path";
22195
+ import * as fs12 from "fs";
22196
+ import * as path12 from "path";
21084
22197
  import chalk7 from "chalk";
21085
22198
  import ora2 from "ora";
21086
22199
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
@@ -21183,15 +22296,15 @@ async function ingestFile(options) {
21183
22296
  const persona = getPersona(personaId);
21184
22297
  const manifest = new SourceManifestManager(marvinDir);
21185
22298
  const sourcesDir = manifest.sourcesDir;
21186
- const filePath = path11.join(sourcesDir, fileName);
21187
- if (!fs11.existsSync(filePath)) {
22299
+ const filePath = path12.join(sourcesDir, fileName);
22300
+ if (!fs12.existsSync(filePath)) {
21188
22301
  throw new Error(`Source file not found: ${filePath}`);
21189
22302
  }
21190
- const ext = path11.extname(fileName).toLowerCase();
22303
+ const ext = path12.extname(fileName).toLowerCase();
21191
22304
  const isPdf = ext === ".pdf";
21192
22305
  let fileContent = null;
21193
22306
  if (!isPdf) {
21194
- fileContent = fs11.readFileSync(filePath, "utf-8");
22307
+ fileContent = fs12.readFileSync(filePath, "utf-8");
21195
22308
  }
21196
22309
  const store = new DocumentStore(marvinDir);
21197
22310
  const createdArtifacts = [];
@@ -21294,9 +22407,9 @@ Ingest ended with error: ${message.subtype}`)
21294
22407
  async function ingestCommand(file2, options) {
21295
22408
  const project = loadProject();
21296
22409
  const marvinDir = project.marvinDir;
21297
- const sourcesDir = path12.join(marvinDir, "sources");
21298
- if (!fs12.existsSync(sourcesDir)) {
21299
- fs12.mkdirSync(sourcesDir, { recursive: true });
22410
+ const sourcesDir = path13.join(marvinDir, "sources");
22411
+ if (!fs13.existsSync(sourcesDir)) {
22412
+ fs13.mkdirSync(sourcesDir, { recursive: true });
21300
22413
  }
21301
22414
  const manifest = new SourceManifestManager(marvinDir);
21302
22415
  manifest.scan();
@@ -21307,8 +22420,8 @@ async function ingestCommand(file2, options) {
21307
22420
  return;
21308
22421
  }
21309
22422
  if (file2) {
21310
- const filePath = path12.join(sourcesDir, file2);
21311
- if (!fs12.existsSync(filePath)) {
22423
+ const filePath = path13.join(sourcesDir, file2);
22424
+ if (!fs13.existsSync(filePath)) {
21312
22425
  console.log(chalk8.red(`Source file not found: ${file2}`));
21313
22426
  console.log(chalk8.dim(`Expected at: ${filePath}`));
21314
22427
  console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
@@ -21375,7 +22488,7 @@ import ora3 from "ora";
21375
22488
  import { input as input3 } from "@inquirer/prompts";
21376
22489
 
21377
22490
  // src/git/repository.ts
21378
- import * as path13 from "path";
22491
+ import * as path14 from "path";
21379
22492
  import simpleGit from "simple-git";
21380
22493
  var MARVIN_GITIGNORE = `node_modules/
21381
22494
  .DS_Store
@@ -21395,7 +22508,7 @@ var DIR_TYPE_LABELS = {
21395
22508
  function buildCommitMessage(files) {
21396
22509
  const counts = /* @__PURE__ */ new Map();
21397
22510
  for (const f of files) {
21398
- const parts2 = f.split(path13.sep).join("/").split("/");
22511
+ const parts2 = f.split(path14.sep).join("/").split("/");
21399
22512
  const docsIdx = parts2.indexOf("docs");
21400
22513
  if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
21401
22514
  const dirName = parts2[docsIdx + 1];
@@ -21435,9 +22548,9 @@ var MarvinGit = class {
21435
22548
  );
21436
22549
  }
21437
22550
  await this.git.init();
21438
- const { writeFileSync: writeFileSync10 } = await import("fs");
21439
- writeFileSync10(
21440
- path13.join(this.marvinDir, ".gitignore"),
22551
+ const { writeFileSync: writeFileSync11 } = await import("fs");
22552
+ writeFileSync11(
22553
+ path14.join(this.marvinDir, ".gitignore"),
21441
22554
  MARVIN_GITIGNORE,
21442
22555
  "utf-8"
21443
22556
  );
@@ -21557,7 +22670,7 @@ var MarvinGit = class {
21557
22670
  }
21558
22671
  }
21559
22672
  static async clone(url2, targetDir) {
21560
- const marvinDir = path13.join(targetDir, ".marvin");
22673
+ const marvinDir = path14.join(targetDir, ".marvin");
21561
22674
  const { existsSync: existsSync17 } = await import("fs");
21562
22675
  if (existsSync17(marvinDir)) {
21563
22676
  throw new GitSyncError(
@@ -21737,13 +22850,13 @@ async function cloneCommand(url2, directory) {
21737
22850
  }
21738
22851
 
21739
22852
  // src/mcp/stdio-server.ts
21740
- import * as fs13 from "fs";
21741
- import * as path14 from "path";
22853
+ import * as fs14 from "fs";
22854
+ import * as path15 from "path";
21742
22855
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21743
22856
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21744
22857
 
21745
22858
  // src/skills/action-tools.ts
21746
- import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
22859
+ import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
21747
22860
 
21748
22861
  // src/skills/action-runner.ts
21749
22862
  import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
@@ -21809,7 +22922,7 @@ function createSkillActionTools(skills, context) {
21809
22922
  if (!skill.actions) continue;
21810
22923
  for (const action of skill.actions) {
21811
22924
  tools.push(
21812
- tool21(
22925
+ tool23(
21813
22926
  `${skill.id}__${action.id}`,
21814
22927
  action.description,
21815
22928
  {
@@ -21901,10 +23014,10 @@ ${lines.join("\n\n")}`;
21901
23014
  }
21902
23015
 
21903
23016
  // src/mcp/persona-tools.ts
21904
- import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
23017
+ import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
21905
23018
  function createPersonaTools(ctx, marvinDir) {
21906
23019
  return [
21907
- tool22(
23020
+ tool24(
21908
23021
  "set_persona",
21909
23022
  "Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
21910
23023
  {
@@ -21934,7 +23047,7 @@ ${summaries}`
21934
23047
  };
21935
23048
  }
21936
23049
  ),
21937
- tool22(
23050
+ tool24(
21938
23051
  "get_persona_guidance",
21939
23052
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
21940
23053
  {
@@ -22036,8 +23149,8 @@ function collectTools(marvinDir) {
22036
23149
  const plugin = resolvePlugin(config2.methodology);
22037
23150
  const registrations = plugin?.documentTypeRegistrations ?? [];
22038
23151
  const store = new DocumentStore(marvinDir, registrations);
22039
- const sourcesDir = path14.join(marvinDir, "sources");
22040
- const hasSourcesDir = fs13.existsSync(sourcesDir);
23152
+ const sourcesDir = path15.join(marvinDir, "sources");
23153
+ const hasSourcesDir = fs14.existsSync(sourcesDir);
22041
23154
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
22042
23155
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
22043
23156
  const sessionStore = new SessionStore(marvinDir);
@@ -22045,7 +23158,7 @@ function collectTools(marvinDir) {
22045
23158
  const allSkillIds = [...allSkills.keys()];
22046
23159
  const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
22047
23160
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
22048
- const projectRoot = path14.dirname(marvinDir);
23161
+ const projectRoot = path15.dirname(marvinDir);
22049
23162
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
22050
23163
  const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
22051
23164
  const navGroups = buildNavGroups({
@@ -22115,8 +23228,8 @@ async function serveCommand() {
22115
23228
  }
22116
23229
 
22117
23230
  // src/cli/commands/skills.ts
22118
- import * as fs14 from "fs";
22119
- import * as path15 from "path";
23231
+ import * as fs15 from "fs";
23232
+ import * as path16 from "path";
22120
23233
  import * as YAML7 from "yaml";
22121
23234
  import matter3 from "gray-matter";
22122
23235
  import chalk10 from "chalk";
@@ -22222,14 +23335,14 @@ async function skillsRemoveCommand(skillId, options) {
22222
23335
  }
22223
23336
  async function skillsCreateCommand(name) {
22224
23337
  const project = loadProject();
22225
- const skillsDir = path15.join(project.marvinDir, "skills");
22226
- fs14.mkdirSync(skillsDir, { recursive: true });
22227
- const skillDir = path15.join(skillsDir, name);
22228
- if (fs14.existsSync(skillDir)) {
23338
+ const skillsDir = path16.join(project.marvinDir, "skills");
23339
+ fs15.mkdirSync(skillsDir, { recursive: true });
23340
+ const skillDir = path16.join(skillsDir, name);
23341
+ if (fs15.existsSync(skillDir)) {
22229
23342
  console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
22230
23343
  return;
22231
23344
  }
22232
- fs14.mkdirSync(skillDir, { recursive: true });
23345
+ fs15.mkdirSync(skillDir, { recursive: true });
22233
23346
  const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
22234
23347
  const frontmatter = {
22235
23348
  name,
@@ -22243,7 +23356,7 @@ async function skillsCreateCommand(name) {
22243
23356
  You have the **${displayName}** skill.
22244
23357
  `;
22245
23358
  const skillMd = matter3.stringify(body, frontmatter);
22246
- fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
23359
+ fs15.writeFileSync(path16.join(skillDir, "SKILL.md"), skillMd, "utf-8");
22247
23360
  const actions = [
22248
23361
  {
22249
23362
  id: "run",
@@ -22253,7 +23366,7 @@ You have the **${displayName}** skill.
22253
23366
  maxTurns: 5
22254
23367
  }
22255
23368
  ];
22256
- fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
23369
+ fs15.writeFileSync(path16.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
22257
23370
  console.log(chalk10.green(`Created skill: ${skillDir}/`));
22258
23371
  console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
22259
23372
  console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
@@ -22261,14 +23374,14 @@ You have the **${displayName}** skill.
22261
23374
  }
22262
23375
  async function skillsMigrateCommand() {
22263
23376
  const project = loadProject();
22264
- const skillsDir = path15.join(project.marvinDir, "skills");
22265
- if (!fs14.existsSync(skillsDir)) {
23377
+ const skillsDir = path16.join(project.marvinDir, "skills");
23378
+ if (!fs15.existsSync(skillsDir)) {
22266
23379
  console.log(chalk10.dim("No skills directory found."));
22267
23380
  return;
22268
23381
  }
22269
23382
  let entries;
22270
23383
  try {
22271
- entries = fs14.readdirSync(skillsDir);
23384
+ entries = fs15.readdirSync(skillsDir);
22272
23385
  } catch {
22273
23386
  console.log(chalk10.red("Could not read skills directory."));
22274
23387
  return;
@@ -22280,16 +23393,16 @@ async function skillsMigrateCommand() {
22280
23393
  }
22281
23394
  let migrated = 0;
22282
23395
  for (const file2 of yamlFiles) {
22283
- const yamlPath = path15.join(skillsDir, file2);
23396
+ const yamlPath = path16.join(skillsDir, file2);
22284
23397
  const baseName = file2.replace(/\.(yaml|yml)$/, "");
22285
- const outputDir = path15.join(skillsDir, baseName);
22286
- if (fs14.existsSync(outputDir)) {
23398
+ const outputDir = path16.join(skillsDir, baseName);
23399
+ if (fs15.existsSync(outputDir)) {
22287
23400
  console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
22288
23401
  continue;
22289
23402
  }
22290
23403
  try {
22291
23404
  migrateYamlToSkillMd(yamlPath, outputDir);
22292
- fs14.renameSync(yamlPath, `${yamlPath}.bak`);
23405
+ fs15.renameSync(yamlPath, `${yamlPath}.bak`);
22293
23406
  console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
22294
23407
  migrated++;
22295
23408
  } catch (err) {
@@ -22303,35 +23416,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
22303
23416
  }
22304
23417
 
22305
23418
  // src/cli/commands/import.ts
22306
- import * as fs17 from "fs";
22307
- import * as path18 from "path";
23419
+ import * as fs18 from "fs";
23420
+ import * as path19 from "path";
22308
23421
  import chalk11 from "chalk";
22309
23422
 
22310
23423
  // src/import/engine.ts
22311
- import * as fs16 from "fs";
22312
- import * as path17 from "path";
23424
+ import * as fs17 from "fs";
23425
+ import * as path18 from "path";
22313
23426
  import matter5 from "gray-matter";
22314
23427
 
22315
23428
  // src/import/classifier.ts
22316
- import * as fs15 from "fs";
22317
- import * as path16 from "path";
23429
+ import * as fs16 from "fs";
23430
+ import * as path17 from "path";
22318
23431
  import matter4 from "gray-matter";
22319
23432
  var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
22320
23433
  var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
22321
23434
  var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
22322
23435
  function classifyPath(inputPath, knownTypes, knownDirNames) {
22323
- const resolved = path16.resolve(inputPath);
22324
- const stat = fs15.statSync(resolved);
23436
+ const resolved = path17.resolve(inputPath);
23437
+ const stat = fs16.statSync(resolved);
22325
23438
  if (!stat.isDirectory()) {
22326
23439
  return classifyFile(resolved, knownTypes);
22327
23440
  }
22328
- if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
23441
+ if (path17.basename(resolved) === ".marvin" || fs16.existsSync(path17.join(resolved, "config.yaml"))) {
22329
23442
  return { type: "marvin-project", inputPath: resolved };
22330
23443
  }
22331
23444
  const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
22332
- const entries = fs15.readdirSync(resolved);
23445
+ const entries = fs16.readdirSync(resolved);
22333
23446
  const hasDocSubdirs = entries.some(
22334
- (e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
23447
+ (e) => allDirNames.has(e) && fs16.statSync(path17.join(resolved, e)).isDirectory()
22335
23448
  );
22336
23449
  if (hasDocSubdirs) {
22337
23450
  return { type: "docs-directory", inputPath: resolved };
@@ -22340,7 +23453,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
22340
23453
  if (mdFiles.length > 0) {
22341
23454
  const hasMarvinDocs = mdFiles.some((f) => {
22342
23455
  try {
22343
- const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
23456
+ const raw = fs16.readFileSync(path17.join(resolved, f), "utf-8");
22344
23457
  const { data } = matter4(raw);
22345
23458
  return isValidMarvinDocument(data, knownTypes);
22346
23459
  } catch {
@@ -22354,14 +23467,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
22354
23467
  return { type: "raw-source-dir", inputPath: resolved };
22355
23468
  }
22356
23469
  function classifyFile(filePath, knownTypes) {
22357
- const resolved = path16.resolve(filePath);
22358
- const ext = path16.extname(resolved).toLowerCase();
23470
+ const resolved = path17.resolve(filePath);
23471
+ const ext = path17.extname(resolved).toLowerCase();
22359
23472
  if (RAW_SOURCE_EXTENSIONS.has(ext)) {
22360
23473
  return { type: "raw-source-file", inputPath: resolved };
22361
23474
  }
22362
23475
  if (ext === ".md") {
22363
23476
  try {
22364
- const raw = fs15.readFileSync(resolved, "utf-8");
23477
+ const raw = fs16.readFileSync(resolved, "utf-8");
22365
23478
  const { data } = matter4(raw);
22366
23479
  if (isValidMarvinDocument(data, knownTypes)) {
22367
23480
  return { type: "marvin-document", inputPath: resolved };
@@ -22486,9 +23599,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
22486
23599
  continue;
22487
23600
  }
22488
23601
  if (item.action === "copy") {
22489
- const targetDir = path17.dirname(item.targetPath);
22490
- fs16.mkdirSync(targetDir, { recursive: true });
22491
- fs16.copyFileSync(item.sourcePath, item.targetPath);
23602
+ const targetDir = path18.dirname(item.targetPath);
23603
+ fs17.mkdirSync(targetDir, { recursive: true });
23604
+ fs17.copyFileSync(item.sourcePath, item.targetPath);
22492
23605
  copied++;
22493
23606
  continue;
22494
23607
  }
@@ -22524,19 +23637,19 @@ function formatPlanSummary(plan) {
22524
23637
  lines.push(`Documents to import: ${imports.length}`);
22525
23638
  for (const item of imports) {
22526
23639
  const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
22527
- lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
23640
+ lines.push(` ${idInfo} ${path18.basename(item.sourcePath)}`);
22528
23641
  }
22529
23642
  }
22530
23643
  if (copies.length > 0) {
22531
23644
  lines.push(`Files to copy to sources/: ${copies.length}`);
22532
23645
  for (const item of copies) {
22533
- lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
23646
+ lines.push(` ${path18.basename(item.sourcePath)} \u2192 ${path18.basename(item.targetPath)}`);
22534
23647
  }
22535
23648
  }
22536
23649
  if (skips.length > 0) {
22537
23650
  lines.push(`Skipped (conflict): ${skips.length}`);
22538
23651
  for (const item of skips) {
22539
- lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
23652
+ lines.push(` ${item.originalId ?? path18.basename(item.sourcePath)} ${item.reason ?? ""}`);
22540
23653
  }
22541
23654
  }
22542
23655
  if (plan.items.length === 0) {
@@ -22569,11 +23682,11 @@ function getDirNameForType(store, type) {
22569
23682
  }
22570
23683
  function collectMarvinDocs(dir, knownTypes) {
22571
23684
  const docs = [];
22572
- const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
23685
+ const files = fs17.readdirSync(dir).filter((f) => f.endsWith(".md"));
22573
23686
  for (const file2 of files) {
22574
- const filePath = path17.join(dir, file2);
23687
+ const filePath = path18.join(dir, file2);
22575
23688
  try {
22576
- const raw = fs16.readFileSync(filePath, "utf-8");
23689
+ const raw = fs17.readFileSync(filePath, "utf-8");
22577
23690
  const { data, content } = matter5(raw);
22578
23691
  if (isValidMarvinDocument(data, knownTypes)) {
22579
23692
  docs.push({
@@ -22629,23 +23742,23 @@ function planDocImports(docs, store, options) {
22629
23742
  }
22630
23743
  function planFromMarvinProject(classification, store, _marvinDir, options) {
22631
23744
  let projectDir = classification.inputPath;
22632
- if (path17.basename(projectDir) !== ".marvin") {
22633
- const inner = path17.join(projectDir, ".marvin");
22634
- if (fs16.existsSync(inner)) {
23745
+ if (path18.basename(projectDir) !== ".marvin") {
23746
+ const inner = path18.join(projectDir, ".marvin");
23747
+ if (fs17.existsSync(inner)) {
22635
23748
  projectDir = inner;
22636
23749
  }
22637
23750
  }
22638
- const docsDir = path17.join(projectDir, "docs");
22639
- if (!fs16.existsSync(docsDir)) {
23751
+ const docsDir = path18.join(projectDir, "docs");
23752
+ if (!fs17.existsSync(docsDir)) {
22640
23753
  return [];
22641
23754
  }
22642
23755
  const knownTypes = store.registeredTypes;
22643
23756
  const allDocs = [];
22644
- const subdirs = fs16.readdirSync(docsDir).filter(
22645
- (d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
23757
+ const subdirs = fs17.readdirSync(docsDir).filter(
23758
+ (d) => fs17.statSync(path18.join(docsDir, d)).isDirectory()
22646
23759
  );
22647
23760
  for (const subdir of subdirs) {
22648
- const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
23761
+ const docs = collectMarvinDocs(path18.join(docsDir, subdir), knownTypes);
22649
23762
  allDocs.push(...docs);
22650
23763
  }
22651
23764
  return planDocImports(allDocs, store, options);
@@ -22655,10 +23768,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
22655
23768
  const knownTypes = store.registeredTypes;
22656
23769
  const allDocs = [];
22657
23770
  allDocs.push(...collectMarvinDocs(dir, knownTypes));
22658
- const entries = fs16.readdirSync(dir);
23771
+ const entries = fs17.readdirSync(dir);
22659
23772
  for (const entry of entries) {
22660
- const entryPath = path17.join(dir, entry);
22661
- if (fs16.statSync(entryPath).isDirectory()) {
23773
+ const entryPath = path18.join(dir, entry);
23774
+ if (fs17.statSync(entryPath).isDirectory()) {
22662
23775
  allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
22663
23776
  }
22664
23777
  }
@@ -22667,7 +23780,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
22667
23780
  function planFromSingleDocument(classification, store, _marvinDir, options) {
22668
23781
  const filePath = classification.inputPath;
22669
23782
  const knownTypes = store.registeredTypes;
22670
- const raw = fs16.readFileSync(filePath, "utf-8");
23783
+ const raw = fs17.readFileSync(filePath, "utf-8");
22671
23784
  const { data, content } = matter5(raw);
22672
23785
  if (!isValidMarvinDocument(data, knownTypes)) {
22673
23786
  return [];
@@ -22683,14 +23796,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
22683
23796
  }
22684
23797
  function planFromRawSourceDir(classification, marvinDir) {
22685
23798
  const dir = classification.inputPath;
22686
- const sourcesDir = path17.join(marvinDir, "sources");
23799
+ const sourcesDir = path18.join(marvinDir, "sources");
22687
23800
  const items = [];
22688
- const files = fs16.readdirSync(dir).filter((f) => {
22689
- const stat = fs16.statSync(path17.join(dir, f));
23801
+ const files = fs17.readdirSync(dir).filter((f) => {
23802
+ const stat = fs17.statSync(path18.join(dir, f));
22690
23803
  return stat.isFile();
22691
23804
  });
22692
23805
  for (const file2 of files) {
22693
- const sourcePath = path17.join(dir, file2);
23806
+ const sourcePath = path18.join(dir, file2);
22694
23807
  const targetPath = resolveSourceFileName(sourcesDir, file2);
22695
23808
  items.push({
22696
23809
  action: "copy",
@@ -22701,8 +23814,8 @@ function planFromRawSourceDir(classification, marvinDir) {
22701
23814
  return items;
22702
23815
  }
22703
23816
  function planFromRawSourceFile(classification, marvinDir) {
22704
- const sourcesDir = path17.join(marvinDir, "sources");
22705
- const fileName = path17.basename(classification.inputPath);
23817
+ const sourcesDir = path18.join(marvinDir, "sources");
23818
+ const fileName = path18.basename(classification.inputPath);
22706
23819
  const targetPath = resolveSourceFileName(sourcesDir, fileName);
22707
23820
  return [
22708
23821
  {
@@ -22713,25 +23826,25 @@ function planFromRawSourceFile(classification, marvinDir) {
22713
23826
  ];
22714
23827
  }
22715
23828
  function resolveSourceFileName(sourcesDir, fileName) {
22716
- const targetPath = path17.join(sourcesDir, fileName);
22717
- if (!fs16.existsSync(targetPath)) {
23829
+ const targetPath = path18.join(sourcesDir, fileName);
23830
+ if (!fs17.existsSync(targetPath)) {
22718
23831
  return targetPath;
22719
23832
  }
22720
- const ext = path17.extname(fileName);
22721
- const base = path17.basename(fileName, ext);
23833
+ const ext = path18.extname(fileName);
23834
+ const base = path18.basename(fileName, ext);
22722
23835
  let counter = 1;
22723
23836
  let candidate;
22724
23837
  do {
22725
- candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
23838
+ candidate = path18.join(sourcesDir, `${base}-${counter}${ext}`);
22726
23839
  counter++;
22727
- } while (fs16.existsSync(candidate));
23840
+ } while (fs17.existsSync(candidate));
22728
23841
  return candidate;
22729
23842
  }
22730
23843
 
22731
23844
  // src/cli/commands/import.ts
22732
23845
  async function importCommand(inputPath, options) {
22733
- const resolved = path18.resolve(inputPath);
22734
- if (!fs17.existsSync(resolved)) {
23846
+ const resolved = path19.resolve(inputPath);
23847
+ if (!fs18.existsSync(resolved)) {
22735
23848
  throw new ImportError(`Path not found: ${resolved}`);
22736
23849
  }
22737
23850
  const project = loadProject();
@@ -22783,7 +23896,7 @@ async function importCommand(inputPath, options) {
22783
23896
  console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
22784
23897
  const manifest = new SourceManifestManager(marvinDir);
22785
23898
  manifest.scan();
22786
- const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
23899
+ const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path19.basename(i.targetPath));
22787
23900
  for (const fileName of copiedFileNames) {
22788
23901
  try {
22789
23902
  await ingestFile({
@@ -23672,14 +24785,14 @@ async function webCommand(options) {
23672
24785
  }
23673
24786
 
23674
24787
  // src/cli/commands/generate.ts
23675
- import * as fs18 from "fs";
23676
- import * as path19 from "path";
24788
+ import * as fs19 from "fs";
24789
+ import * as path20 from "path";
23677
24790
  import chalk18 from "chalk";
23678
24791
  import { confirm as confirm2 } from "@inquirer/prompts";
23679
24792
  async function generateClaudeMdCommand(options) {
23680
24793
  const project = loadProject();
23681
- const filePath = path19.join(project.marvinDir, "CLAUDE.md");
23682
- if (fs18.existsSync(filePath) && !options.force) {
24794
+ const filePath = path20.join(project.marvinDir, "CLAUDE.md");
24795
+ if (fs19.existsSync(filePath) && !options.force) {
23683
24796
  const overwrite = await confirm2({
23684
24797
  message: ".marvin/CLAUDE.md already exists. Overwrite?",
23685
24798
  default: false
@@ -23689,7 +24802,7 @@ async function generateClaudeMdCommand(options) {
23689
24802
  return;
23690
24803
  }
23691
24804
  }
23692
- fs18.writeFileSync(
24805
+ fs19.writeFileSync(
23693
24806
  filePath,
23694
24807
  getDefaultClaudeMdContent(project.config.name),
23695
24808
  "utf-8"
@@ -23702,7 +24815,7 @@ function createProgram() {
23702
24815
  const program2 = new Command();
23703
24816
  program2.name("marvin").description(
23704
24817
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
23705
- ).version("0.4.1");
24818
+ ).version("0.4.4");
23706
24819
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
23707
24820
  await initCommand();
23708
24821
  });