mrvn-cli 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/marvin.js CHANGED
@@ -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
  }
@@ -15627,6 +15627,205 @@ function createSprintPlanningTools(store) {
15627
15627
  ];
15628
15628
  }
15629
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
+
15630
15829
  // src/plugins/common.ts
15631
15830
  var COMMON_REGISTRATIONS = [
15632
15831
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -15634,7 +15833,8 @@ var COMMON_REGISTRATIONS = [
15634
15833
  { type: "feature", dirName: "features", idPrefix: "F" },
15635
15834
  { type: "epic", dirName: "epics", idPrefix: "E" },
15636
15835
  { type: "contribution", dirName: "contributions", idPrefix: "C" },
15637
- { type: "sprint", dirName: "sprints", idPrefix: "SP" }
15836
+ { type: "sprint", dirName: "sprints", idPrefix: "SP" },
15837
+ { type: "task", dirName: "tasks", idPrefix: "T" }
15638
15838
  ];
15639
15839
  function createCommonTools(store) {
15640
15840
  return [
@@ -15644,7 +15844,8 @@ function createCommonTools(store) {
15644
15844
  ...createEpicTools(store),
15645
15845
  ...createContributionTools(store),
15646
15846
  ...createSprintTools(store),
15647
- ...createSprintPlanningTools(store)
15847
+ ...createSprintPlanningTools(store),
15848
+ ...createTaskTools(store)
15648
15849
  ];
15649
15850
  }
15650
15851
 
@@ -15654,7 +15855,7 @@ var genericAgilePlugin = {
15654
15855
  name: "Generic Agile",
15655
15856
  description: "Default methodology plugin providing standard agile governance patterns for decisions, actions, and questions.",
15656
15857
  version: "0.1.0",
15657
- documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint"],
15858
+ documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint", "task"],
15658
15859
  documentTypeRegistrations: [...COMMON_REGISTRATIONS],
15659
15860
  tools: (store) => [...createCommonTools(store)],
15660
15861
  promptFragments: {
@@ -15693,6 +15894,11 @@ var genericAgilePlugin = {
15693
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.
15694
15895
  - **update_epic**: Update epic status (planned \u2192 in-progress \u2192 done), owner, and other fields.
15695
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
+
15696
15902
  **Feature Tools (read-only for awareness):**
15697
15903
  - **list_features** / **get_feature**: View features to understand what needs to be broken into epics.
15698
15904
 
@@ -15704,6 +15910,7 @@ var genericAgilePlugin = {
15704
15910
 
15705
15911
  **Key Workflow Rules:**
15706
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.
15707
15914
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
15708
15915
  - Collaborate with the Delivery Manager on target dates and effort estimates.
15709
15916
  - Each epic should have a clear scope and definition of done.
@@ -15739,6 +15946,9 @@ var genericAgilePlugin = {
15739
15946
  - **list_epics** / **get_epic**: View epics and their current status.
15740
15947
  - **update_epic**: Set targetDate and estimatedEffort on epics. Flag epics linked to deferred features.
15741
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
+
15742
15952
  **Feature Tools (tracking focus):**
15743
15953
  - **list_features** / **get_feature**: View features and their priorities.
15744
15954
 
@@ -15784,14 +15994,15 @@ var genericAgilePlugin = {
15784
15994
  - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
15785
15995
  - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
15786
15996
  - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
15787
- "*": `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:
15788
15998
 
15789
15999
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
15790
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.
15791
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).
15792
16003
  **Meetings**: Meeting records with attendees, agendas, and notes.
15793
16004
 
15794
- **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.
15795
16006
 
15796
16007
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
15797
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.
@@ -15806,10 +16017,10 @@ var genericAgilePlugin = {
15806
16017
  };
15807
16018
 
15808
16019
  // src/plugins/builtin/tools/use-cases.ts
15809
- import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
16020
+ import { tool as tool9 } from "@anthropic-ai/claude-agent-sdk";
15810
16021
  function createUseCaseTools(store) {
15811
16022
  return [
15812
- tool8(
16023
+ tool9(
15813
16024
  "list_use_cases",
15814
16025
  "List all extension use cases, optionally filtered by status or extension type",
15815
16026
  {
@@ -15839,7 +16050,7 @@ function createUseCaseTools(store) {
15839
16050
  },
15840
16051
  { annotations: { readOnlyHint: true } }
15841
16052
  ),
15842
- tool8(
16053
+ tool9(
15843
16054
  "get_use_case",
15844
16055
  "Get the full content of a specific use case by ID",
15845
16056
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -15866,7 +16077,7 @@ function createUseCaseTools(store) {
15866
16077
  },
15867
16078
  { annotations: { readOnlyHint: true } }
15868
16079
  ),
15869
- tool8(
16080
+ tool9(
15870
16081
  "create_use_case",
15871
16082
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
15872
16083
  {
@@ -15900,7 +16111,7 @@ function createUseCaseTools(store) {
15900
16111
  };
15901
16112
  }
15902
16113
  ),
15903
- tool8(
16114
+ tool9(
15904
16115
  "update_use_case",
15905
16116
  "Update an existing extension use case",
15906
16117
  {
@@ -15930,10 +16141,10 @@ function createUseCaseTools(store) {
15930
16141
  }
15931
16142
 
15932
16143
  // src/plugins/builtin/tools/tech-assessments.ts
15933
- import { tool as tool9 } from "@anthropic-ai/claude-agent-sdk";
16144
+ import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
15934
16145
  function createTechAssessmentTools(store) {
15935
16146
  return [
15936
- tool9(
16147
+ tool10(
15937
16148
  "list_tech_assessments",
15938
16149
  "List all technology assessments, optionally filtered by status",
15939
16150
  {
@@ -15964,7 +16175,7 @@ function createTechAssessmentTools(store) {
15964
16175
  },
15965
16176
  { annotations: { readOnlyHint: true } }
15966
16177
  ),
15967
- tool9(
16178
+ tool10(
15968
16179
  "get_tech_assessment",
15969
16180
  "Get the full content of a specific technology assessment by ID",
15970
16181
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -15991,7 +16202,7 @@ function createTechAssessmentTools(store) {
15991
16202
  },
15992
16203
  { annotations: { readOnlyHint: true } }
15993
16204
  ),
15994
- tool9(
16205
+ tool10(
15995
16206
  "create_tech_assessment",
15996
16207
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
15997
16208
  {
@@ -16062,7 +16273,7 @@ function createTechAssessmentTools(store) {
16062
16273
  };
16063
16274
  }
16064
16275
  ),
16065
- tool9(
16276
+ tool10(
16066
16277
  "update_tech_assessment",
16067
16278
  "Update an existing technology assessment. The linked use case cannot be changed.",
16068
16279
  {
@@ -16092,10 +16303,10 @@ function createTechAssessmentTools(store) {
16092
16303
  }
16093
16304
 
16094
16305
  // src/plugins/builtin/tools/extension-designs.ts
16095
- import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
16306
+ import { tool as tool11 } from "@anthropic-ai/claude-agent-sdk";
16096
16307
  function createExtensionDesignTools(store) {
16097
16308
  return [
16098
- tool10(
16309
+ tool11(
16099
16310
  "list_extension_designs",
16100
16311
  "List all extension designs, optionally filtered by status",
16101
16312
  {
@@ -16125,7 +16336,7 @@ function createExtensionDesignTools(store) {
16125
16336
  },
16126
16337
  { annotations: { readOnlyHint: true } }
16127
16338
  ),
16128
- tool10(
16339
+ tool11(
16129
16340
  "get_extension_design",
16130
16341
  "Get the full content of a specific extension design by ID",
16131
16342
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -16152,7 +16363,7 @@ function createExtensionDesignTools(store) {
16152
16363
  },
16153
16364
  { annotations: { readOnlyHint: true } }
16154
16365
  ),
16155
- tool10(
16366
+ tool11(
16156
16367
  "create_extension_design",
16157
16368
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
16158
16369
  {
@@ -16220,7 +16431,7 @@ function createExtensionDesignTools(store) {
16220
16431
  };
16221
16432
  }
16222
16433
  ),
16223
- tool10(
16434
+ tool11(
16224
16435
  "update_extension_design",
16225
16436
  "Update an existing extension design. The linked tech assessment cannot be changed.",
16226
16437
  {
@@ -16249,10 +16460,10 @@ function createExtensionDesignTools(store) {
16249
16460
  }
16250
16461
 
16251
16462
  // src/plugins/builtin/tools/aem-reports.ts
16252
- import { tool as tool11 } from "@anthropic-ai/claude-agent-sdk";
16463
+ import { tool as tool12 } from "@anthropic-ai/claude-agent-sdk";
16253
16464
  function createAemReportTools(store) {
16254
16465
  return [
16255
- tool11(
16466
+ tool12(
16256
16467
  "generate_extension_portfolio",
16257
16468
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
16258
16469
  {},
@@ -16304,7 +16515,7 @@ function createAemReportTools(store) {
16304
16515
  },
16305
16516
  { annotations: { readOnlyHint: true } }
16306
16517
  ),
16307
- tool11(
16518
+ tool12(
16308
16519
  "generate_tech_readiness",
16309
16520
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
16310
16521
  {},
@@ -16356,7 +16567,7 @@ function createAemReportTools(store) {
16356
16567
  },
16357
16568
  { annotations: { readOnlyHint: true } }
16358
16569
  ),
16359
- tool11(
16570
+ tool12(
16360
16571
  "generate_phase_status",
16361
16572
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
16362
16573
  {},
@@ -16418,11 +16629,11 @@ function createAemReportTools(store) {
16418
16629
  import * as fs3 from "fs";
16419
16630
  import * as path3 from "path";
16420
16631
  import * as YAML2 from "yaml";
16421
- import { tool as tool12 } from "@anthropic-ai/claude-agent-sdk";
16632
+ import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
16422
16633
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
16423
16634
  function createAemPhaseTools(store, marvinDir) {
16424
16635
  return [
16425
- tool12(
16636
+ tool13(
16426
16637
  "get_current_phase",
16427
16638
  "Get the current AEM phase from project configuration",
16428
16639
  {},
@@ -16443,7 +16654,7 @@ function createAemPhaseTools(store, marvinDir) {
16443
16654
  },
16444
16655
  { annotations: { readOnlyHint: true } }
16445
16656
  ),
16446
- tool12(
16657
+ tool13(
16447
16658
  "advance_phase",
16448
16659
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
16449
16660
  {
@@ -16952,7 +17163,7 @@ var deliveryManager = {
16952
17163
  "Epic scheduling and tracking",
16953
17164
  "Sprint planning and tracking"
16954
17165
  ],
16955
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "sprint"],
17166
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
16956
17167
  contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
16957
17168
  };
16958
17169
 
@@ -16990,9 +17201,10 @@ var techLead = {
16990
17201
  "Implementation guidance",
16991
17202
  "Non-functional requirements",
16992
17203
  "Epic creation and scoping",
17204
+ "Task creation and breakdown",
16993
17205
  "Sprint scoping and technical execution"
16994
17206
  ],
16995
- documentTypes: ["decision", "action", "question", "epic", "sprint"],
17207
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
16996
17208
  contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
16997
17209
  };
16998
17210
 
@@ -17025,8 +17237,8 @@ function resolvePersonaId(input4) {
17025
17237
  }
17026
17238
 
17027
17239
  // src/agent/session.ts
17028
- import * as fs10 from "fs";
17029
- import * as path10 from "path";
17240
+ import * as fs11 from "fs";
17241
+ import * as path11 from "path";
17030
17242
  import * as readline from "readline";
17031
17243
  import chalk2 from "chalk";
17032
17244
  import ora from "ora";
@@ -17394,10 +17606,10 @@ import {
17394
17606
  } from "@anthropic-ai/claude-agent-sdk";
17395
17607
 
17396
17608
  // src/agent/tools/decisions.ts
17397
- import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
17609
+ import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17398
17610
  function createDecisionTools(store) {
17399
17611
  return [
17400
- tool13(
17612
+ tool14(
17401
17613
  "list_decisions",
17402
17614
  "List all decisions in the project, optionally filtered by status",
17403
17615
  { status: external_exports.string().optional().describe("Filter by status (e.g. 'open', 'decided', 'superseded')") },
@@ -17415,7 +17627,7 @@ function createDecisionTools(store) {
17415
17627
  },
17416
17628
  { annotations: { readOnlyHint: true } }
17417
17629
  ),
17418
- tool13(
17630
+ tool14(
17419
17631
  "get_decision",
17420
17632
  "Get the full content of a specific decision by ID",
17421
17633
  { id: external_exports.string().describe("Decision ID (e.g. 'D-001')") },
@@ -17442,7 +17654,7 @@ function createDecisionTools(store) {
17442
17654
  },
17443
17655
  { annotations: { readOnlyHint: true } }
17444
17656
  ),
17445
- tool13(
17657
+ tool14(
17446
17658
  "create_decision",
17447
17659
  "Create a new decision record",
17448
17660
  {
@@ -17473,7 +17685,7 @@ function createDecisionTools(store) {
17473
17685
  };
17474
17686
  }
17475
17687
  ),
17476
- tool13(
17688
+ tool14(
17477
17689
  "update_decision",
17478
17690
  "Update an existing decision",
17479
17691
  {
@@ -17501,7 +17713,7 @@ function createDecisionTools(store) {
17501
17713
  }
17502
17714
 
17503
17715
  // src/agent/tools/actions.ts
17504
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17716
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17505
17717
  function findMatchingSprints(store, dueDate) {
17506
17718
  const sprints = store.list({ type: "sprint" });
17507
17719
  return sprints.filter((s) => {
@@ -17517,7 +17729,7 @@ function findMatchingSprints(store, dueDate) {
17517
17729
  }
17518
17730
  function createActionTools(store) {
17519
17731
  return [
17520
- tool14(
17732
+ tool15(
17521
17733
  "list_actions",
17522
17734
  "List all action items in the project, optionally filtered by status or owner",
17523
17735
  {
@@ -17549,7 +17761,7 @@ function createActionTools(store) {
17549
17761
  },
17550
17762
  { annotations: { readOnlyHint: true } }
17551
17763
  ),
17552
- tool14(
17764
+ tool15(
17553
17765
  "get_action",
17554
17766
  "Get the full content of a specific action item by ID",
17555
17767
  { id: external_exports.string().describe("Action ID (e.g. 'A-001')") },
@@ -17576,7 +17788,7 @@ function createActionTools(store) {
17576
17788
  },
17577
17789
  { annotations: { readOnlyHint: true } }
17578
17790
  ),
17579
- tool14(
17791
+ tool15(
17580
17792
  "create_action",
17581
17793
  "Create a new action item",
17582
17794
  {
@@ -17622,7 +17834,7 @@ function createActionTools(store) {
17622
17834
  };
17623
17835
  }
17624
17836
  ),
17625
- tool14(
17837
+ tool15(
17626
17838
  "update_action",
17627
17839
  "Update an existing action item",
17628
17840
  {
@@ -17671,7 +17883,7 @@ function createActionTools(store) {
17671
17883
  };
17672
17884
  }
17673
17885
  ),
17674
- tool14(
17886
+ tool15(
17675
17887
  "suggest_sprints_for_action",
17676
17888
  "Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
17677
17889
  {
@@ -17704,10 +17916,10 @@ function createActionTools(store) {
17704
17916
  }
17705
17917
 
17706
17918
  // src/agent/tools/questions.ts
17707
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17919
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17708
17920
  function createQuestionTools(store) {
17709
17921
  return [
17710
- tool15(
17922
+ tool16(
17711
17923
  "list_questions",
17712
17924
  "List all questions in the project, optionally filtered by status",
17713
17925
  {
@@ -17728,7 +17940,7 @@ function createQuestionTools(store) {
17728
17940
  },
17729
17941
  { annotations: { readOnlyHint: true } }
17730
17942
  ),
17731
- tool15(
17943
+ tool16(
17732
17944
  "get_question",
17733
17945
  "Get the full content of a specific question by ID",
17734
17946
  { id: external_exports.string().describe("Question ID (e.g. 'Q-001')") },
@@ -17755,7 +17967,7 @@ function createQuestionTools(store) {
17755
17967
  },
17756
17968
  { annotations: { readOnlyHint: true } }
17757
17969
  ),
17758
- tool15(
17970
+ tool16(
17759
17971
  "create_question",
17760
17972
  "Create a new question that needs to be answered",
17761
17973
  {
@@ -17786,7 +17998,7 @@ function createQuestionTools(store) {
17786
17998
  };
17787
17999
  }
17788
18000
  ),
17789
- tool15(
18001
+ tool16(
17790
18002
  "update_question",
17791
18003
  "Update an existing question",
17792
18004
  {
@@ -17814,10 +18026,10 @@ function createQuestionTools(store) {
17814
18026
  }
17815
18027
 
17816
18028
  // src/agent/tools/documents.ts
17817
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
18029
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17818
18030
  function createDocumentTools(store) {
17819
18031
  return [
17820
- tool16(
18032
+ tool17(
17821
18033
  "search_documents",
17822
18034
  "Search all project documents, optionally filtered by type, status, or tag",
17823
18035
  {
@@ -17849,7 +18061,7 @@ function createDocumentTools(store) {
17849
18061
  },
17850
18062
  { annotations: { readOnlyHint: true } }
17851
18063
  ),
17852
- tool16(
18064
+ tool17(
17853
18065
  "read_document",
17854
18066
  "Read the full content of any project document by ID",
17855
18067
  { id: external_exports.string().describe("Document ID (e.g. 'D-001', 'A-003', 'Q-002')") },
@@ -17876,7 +18088,7 @@ function createDocumentTools(store) {
17876
18088
  },
17877
18089
  { annotations: { readOnlyHint: true } }
17878
18090
  ),
17879
- tool16(
18091
+ tool17(
17880
18092
  "project_summary",
17881
18093
  "Get a summary of all project documents and their counts",
17882
18094
  {},
@@ -17908,10 +18120,10 @@ function createDocumentTools(store) {
17908
18120
  }
17909
18121
 
17910
18122
  // src/agent/tools/sources.ts
17911
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
18123
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17912
18124
  function createSourceTools(manifest) {
17913
18125
  return [
17914
- tool17(
18126
+ tool18(
17915
18127
  "list_sources",
17916
18128
  "List all source documents and their processing status",
17917
18129
  {
@@ -17941,7 +18153,7 @@ function createSourceTools(manifest) {
17941
18153
  },
17942
18154
  { annotations: { readOnlyHint: true } }
17943
18155
  ),
17944
- tool17(
18156
+ tool18(
17945
18157
  "get_source_info",
17946
18158
  "Get detailed information about a specific source document",
17947
18159
  {
@@ -17979,10 +18191,10 @@ function createSourceTools(manifest) {
17979
18191
  }
17980
18192
 
17981
18193
  // src/agent/tools/sessions.ts
17982
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
18194
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17983
18195
  function createSessionTools(store) {
17984
18196
  return [
17985
- tool18(
18197
+ tool19(
17986
18198
  "list_sessions",
17987
18199
  "List all saved chat sessions, sorted by most recently used",
17988
18200
  {
@@ -18006,7 +18218,7 @@ function createSessionTools(store) {
18006
18218
  },
18007
18219
  { annotations: { readOnlyHint: true } }
18008
18220
  ),
18009
- tool18(
18221
+ tool19(
18010
18222
  "get_session",
18011
18223
  "Get details of a specific saved session by name",
18012
18224
  { name: external_exports.string().describe("Session name (e.g. 'jwt-auth-decision')") },
@@ -18029,7 +18241,7 @@ function createSessionTools(store) {
18029
18241
 
18030
18242
  // src/agent/tools/web.ts
18031
18243
  import * as http2 from "http";
18032
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18244
+ import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
18033
18245
 
18034
18246
  // src/web/data.ts
18035
18247
  function getOverviewData(store) {
@@ -18142,35 +18354,233 @@ function getDiagramData(store) {
18142
18354
  }
18143
18355
  return { sprints, epics, features, statusCounts };
18144
18356
  }
18145
-
18146
- // src/web/templates/layout.ts
18147
- function escapeHtml(str) {
18148
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18149
- }
18150
- function statusBadge(status) {
18151
- const cls = {
18152
- open: "badge-open",
18153
- done: "badge-done",
18154
- closed: "badge-done",
18155
- resolved: "badge-resolved",
18156
- "in-progress": "badge-in-progress",
18157
- "in progress": "badge-in-progress",
18158
- draft: "badge-draft",
18159
- blocked: "badge-blocked"
18160
- }[status.toLowerCase()] ?? "badge-default";
18161
- return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
18162
- }
18163
- function formatDate(iso) {
18164
- if (!iso) return "";
18165
- return iso.slice(0, 10);
18166
- }
18167
- function typeLabel(type) {
18168
- return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
18169
- }
18170
- function renderMarkdown(md) {
18171
- const lines = md.split("\n");
18172
- const out = [];
18173
- let inList = false;
18357
+ function computeUrgency(dueDateStr, todayStr) {
18358
+ const due = new Date(dueDateStr).getTime();
18359
+ const today = new Date(todayStr).getTime();
18360
+ const diffDays = Math.floor((due - today) / 864e5);
18361
+ if (diffDays < 0) return "overdue";
18362
+ if (diffDays <= 3) return "due-3d";
18363
+ if (diffDays <= 7) return "due-7d";
18364
+ if (diffDays <= 14) return "upcoming";
18365
+ return "later";
18366
+ }
18367
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
18368
+ function getUpcomingData(store) {
18369
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
18370
+ const allDocs = store.list();
18371
+ const docById = /* @__PURE__ */ new Map();
18372
+ for (const doc of allDocs) {
18373
+ docById.set(doc.frontmatter.id, doc);
18374
+ }
18375
+ const actions = allDocs.filter(
18376
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
18377
+ );
18378
+ const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
18379
+ const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
18380
+ const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
18381
+ const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
18382
+ const epicToTasks = /* @__PURE__ */ new Map();
18383
+ for (const task of tasks) {
18384
+ const tags = task.frontmatter.tags ?? [];
18385
+ for (const tag of tags) {
18386
+ if (tag.startsWith("epic:")) {
18387
+ const epicId = tag.slice(5);
18388
+ if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
18389
+ epicToTasks.get(epicId).push(task);
18390
+ }
18391
+ }
18392
+ }
18393
+ function getSprintTasks(sprintDoc) {
18394
+ const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
18395
+ const result = [];
18396
+ for (const epicId of linkedEpics) {
18397
+ const epicTasks = epicToTasks.get(epicId) ?? [];
18398
+ result.push(...epicTasks);
18399
+ }
18400
+ return result;
18401
+ }
18402
+ function countRelatedTasks(actionDoc) {
18403
+ const actionTags = actionDoc.frontmatter.tags ?? [];
18404
+ const relatedTaskIds = /* @__PURE__ */ new Set();
18405
+ for (const tag of actionTags) {
18406
+ if (tag.startsWith("sprint:")) {
18407
+ const sprintId = tag.slice(7);
18408
+ const sprint = docById.get(sprintId);
18409
+ if (sprint) {
18410
+ const sprintTaskDocs = getSprintTasks(sprint);
18411
+ for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
18412
+ }
18413
+ }
18414
+ }
18415
+ return relatedTaskIds.size;
18416
+ }
18417
+ const dueSoonActions = actionsWithDue.map((d) => ({
18418
+ id: d.frontmatter.id,
18419
+ title: d.frontmatter.title,
18420
+ status: d.frontmatter.status,
18421
+ owner: d.frontmatter.owner,
18422
+ dueDate: d.frontmatter.dueDate,
18423
+ urgency: computeUrgency(d.frontmatter.dueDate, today),
18424
+ relatedTaskCount: countRelatedTasks(d)
18425
+ })).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
18426
+ const todayMs = new Date(today).getTime();
18427
+ const fourteenDaysMs = 14 * 864e5;
18428
+ const nearSprints = sprints.filter((s) => {
18429
+ const endDate = s.frontmatter.endDate;
18430
+ if (!endDate) return false;
18431
+ const endMs = new Date(endDate).getTime();
18432
+ const diff = endMs - todayMs;
18433
+ return diff >= 0 && diff <= fourteenDaysMs;
18434
+ });
18435
+ const taskSprintMap = /* @__PURE__ */ new Map();
18436
+ for (const sprint of nearSprints) {
18437
+ const sprintEnd = sprint.frontmatter.endDate;
18438
+ const sprintTaskDocs = getSprintTasks(sprint);
18439
+ for (const task of sprintTaskDocs) {
18440
+ if (DONE_STATUSES.has(task.frontmatter.status)) continue;
18441
+ const existing = taskSprintMap.get(task.frontmatter.id);
18442
+ if (!existing || sprintEnd < existing.sprintEnd) {
18443
+ taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
18444
+ }
18445
+ }
18446
+ }
18447
+ const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
18448
+ id: task.frontmatter.id,
18449
+ title: task.frontmatter.title,
18450
+ status: task.frontmatter.status,
18451
+ sprintId: sprint.frontmatter.id,
18452
+ sprintTitle: sprint.frontmatter.title,
18453
+ sprintEndDate: sprintEnd,
18454
+ urgency: computeUrgency(sprintEnd, today)
18455
+ })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
18456
+ const openItems = allDocs.filter(
18457
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
18458
+ );
18459
+ const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
18460
+ const recentMeetings = allDocs.filter(
18461
+ (d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
18462
+ );
18463
+ const crossRefCounts = /* @__PURE__ */ new Map();
18464
+ for (const doc of allDocs) {
18465
+ const content = doc.content ?? "";
18466
+ for (const item of openItems) {
18467
+ if (doc.frontmatter.id === item.frontmatter.id) continue;
18468
+ if (content.includes(item.frontmatter.id)) {
18469
+ crossRefCounts.set(
18470
+ item.frontmatter.id,
18471
+ (crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
18472
+ );
18473
+ }
18474
+ }
18475
+ }
18476
+ const activeSprints = sprints.filter((s) => {
18477
+ const status = s.frontmatter.status;
18478
+ if (status === "active") return true;
18479
+ const startDate = s.frontmatter.startDate;
18480
+ if (!startDate) return false;
18481
+ const startMs = new Date(startDate).getTime();
18482
+ const diff = startMs - todayMs;
18483
+ return diff >= 0 && diff <= fourteenDaysMs;
18484
+ });
18485
+ const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
18486
+ const activeEpicIds = /* @__PURE__ */ new Set();
18487
+ for (const s of activeSprints) {
18488
+ for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
18489
+ activeEpicIds.add(epicId);
18490
+ }
18491
+ }
18492
+ const trending = openItems.map((doc) => {
18493
+ const signals = [];
18494
+ let score = 0;
18495
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
18496
+ const ageDays = daysBetween(updated, today);
18497
+ const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
18498
+ if (recencyPts > 0) {
18499
+ signals.push({ factor: "recency", points: recencyPts });
18500
+ score += recencyPts;
18501
+ }
18502
+ const tags = doc.frontmatter.tags ?? [];
18503
+ const linkedToActiveSprint = tags.some(
18504
+ (t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
18505
+ );
18506
+ const linkedToActiveEpic = tags.some(
18507
+ (t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
18508
+ );
18509
+ if (linkedToActiveSprint) {
18510
+ signals.push({ factor: "sprint proximity", points: 25 });
18511
+ score += 25;
18512
+ } else if (linkedToActiveEpic) {
18513
+ signals.push({ factor: "sprint proximity", points: 15 });
18514
+ score += 15;
18515
+ }
18516
+ const mentionCount = recentMeetings.filter(
18517
+ (m) => (m.content ?? "").includes(doc.frontmatter.id)
18518
+ ).length;
18519
+ if (mentionCount > 0) {
18520
+ const meetingPts = Math.min(15, mentionCount * 5);
18521
+ signals.push({ factor: "meeting mentions", points: meetingPts });
18522
+ score += meetingPts;
18523
+ }
18524
+ const priority = doc.frontmatter.priority?.toLowerCase();
18525
+ const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
18526
+ if (priorityPts > 0) {
18527
+ signals.push({ factor: "priority", points: priorityPts });
18528
+ score += priorityPts;
18529
+ }
18530
+ if (["action", "question"].includes(doc.frontmatter.type)) {
18531
+ const createdDays = daysBetween(doc.frontmatter.created, today);
18532
+ if (createdDays >= 14) {
18533
+ const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
18534
+ signals.push({ factor: "aging", points: agingPts });
18535
+ score += agingPts;
18536
+ }
18537
+ }
18538
+ const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
18539
+ if (refs > 0) {
18540
+ const crossRefPts = Math.min(15, refs * 5);
18541
+ signals.push({ factor: "cross-references", points: crossRefPts });
18542
+ score += crossRefPts;
18543
+ }
18544
+ return {
18545
+ id: doc.frontmatter.id,
18546
+ title: doc.frontmatter.title,
18547
+ type: doc.frontmatter.type,
18548
+ status: doc.frontmatter.status,
18549
+ score,
18550
+ signals
18551
+ };
18552
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
18553
+ return { dueSoonActions, dueSoonSprintTasks, trending };
18554
+ }
18555
+
18556
+ // src/web/templates/layout.ts
18557
+ function escapeHtml(str) {
18558
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18559
+ }
18560
+ function statusBadge(status) {
18561
+ const cls = {
18562
+ open: "badge-open",
18563
+ done: "badge-done",
18564
+ closed: "badge-done",
18565
+ resolved: "badge-resolved",
18566
+ "in-progress": "badge-in-progress",
18567
+ "in progress": "badge-in-progress",
18568
+ draft: "badge-draft",
18569
+ blocked: "badge-blocked"
18570
+ }[status.toLowerCase()] ?? "badge-default";
18571
+ return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
18572
+ }
18573
+ function formatDate(iso) {
18574
+ if (!iso) return "";
18575
+ return iso.slice(0, 10);
18576
+ }
18577
+ function typeLabel(type) {
18578
+ return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
18579
+ }
18580
+ function renderMarkdown(md) {
18581
+ const lines = md.split("\n");
18582
+ const out = [];
18583
+ let inList = false;
18174
18584
  let listTag = "ul";
18175
18585
  let inTable = false;
18176
18586
  let i = 0;
@@ -18265,6 +18675,8 @@ function inline(text) {
18265
18675
  function layout(opts, body) {
18266
18676
  const topItems = [
18267
18677
  { href: "/", label: "Overview" },
18678
+ { href: "/upcoming", label: "Upcoming" },
18679
+ { href: "/timeline", label: "Timeline" },
18268
18680
  { href: "/board", label: "Board" },
18269
18681
  { href: "/gar", label: "GAR Report" },
18270
18682
  { href: "/health", label: "Health" }
@@ -18301,7 +18713,7 @@ function layout(opts, body) {
18301
18713
  ${groupsHtml}
18302
18714
  </nav>
18303
18715
  </aside>
18304
- <main class="main">
18716
+ <main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
18305
18717
  <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18306
18718
  <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>
18307
18719
  <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>
@@ -18310,7 +18722,36 @@ function layout(opts, body) {
18310
18722
  </main>
18311
18723
  </div>
18312
18724
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18313
- <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
18725
+ <script>mermaid.initialize({
18726
+ startOnLoad: true,
18727
+ theme: 'dark',
18728
+ themeVariables: {
18729
+ background: '#1a1d27',
18730
+ primaryColor: '#2a2e3a',
18731
+ sectionBkgColor: '#1a1d27',
18732
+ sectionBkgColor2: '#222632',
18733
+ altSectionBkgColor: '#222632',
18734
+ gridColor: '#2a2e3a',
18735
+ taskBorderColor: '#475569',
18736
+ doneTaskBkgColor: '#065f46',
18737
+ doneTaskBorderColor: '#34d399',
18738
+ activeTaskBkgColor: '#78350f',
18739
+ activeTaskBorderColor: '#fbbf24',
18740
+ taskTextColor: '#e1e4ea',
18741
+ sectionBkgColor: '#1a1d27',
18742
+ pie1: '#34d399',
18743
+ pie2: '#475569',
18744
+ pie3: '#fbbf24',
18745
+ pie4: '#f87171',
18746
+ pie5: '#6c8cff',
18747
+ pie6: '#a78bfa',
18748
+ pie7: '#f472b6',
18749
+ pieTitleTextColor: '#e1e4ea',
18750
+ pieSectionTextColor: '#e1e4ea',
18751
+ pieLegendTextColor: '#e1e4ea',
18752
+ pieStrokeColor: '#1a1d27'
18753
+ }
18754
+ });</script>
18314
18755
  </body>
18315
18756
  </html>`;
18316
18757
  }
@@ -18557,6 +18998,10 @@ a:hover { text-decoration: underline; }
18557
18998
  /* Table */
18558
18999
  .table-wrap {
18559
19000
  overflow-x: auto;
19001
+ overflow-y: auto;
19002
+ max-height: calc(100vh - 280px);
19003
+ border: 1px solid var(--border);
19004
+ border-radius: var(--radius);
18560
19005
  }
18561
19006
 
18562
19007
  table {
@@ -18572,6 +19017,10 @@ th {
18572
19017
  letter-spacing: 0.05em;
18573
19018
  color: var(--text-dim);
18574
19019
  border-bottom: 1px solid var(--border);
19020
+ position: sticky;
19021
+ top: 0;
19022
+ background: var(--bg-card);
19023
+ z-index: 1;
18575
19024
  }
18576
19025
 
18577
19026
  td {
@@ -18619,6 +19068,8 @@ tr:hover td {
18619
19068
  border: 1px solid var(--border);
18620
19069
  border-radius: var(--radius);
18621
19070
  padding: 1.25rem;
19071
+ display: flex;
19072
+ flex-direction: column;
18622
19073
  }
18623
19074
 
18624
19075
  .gar-area .area-header {
@@ -18649,6 +19100,9 @@ tr:hover td {
18649
19100
  .gar-area ul {
18650
19101
  list-style: none;
18651
19102
  font-size: 0.8rem;
19103
+ max-height: 200px;
19104
+ overflow-y: auto;
19105
+ scrollbar-width: thin;
18652
19106
  }
18653
19107
 
18654
19108
  .gar-area li {
@@ -18671,13 +19125,14 @@ tr:hover td {
18671
19125
  display: flex;
18672
19126
  gap: 1rem;
18673
19127
  overflow-x: auto;
19128
+ scrollbar-width: thin;
18674
19129
  padding-bottom: 1rem;
18675
19130
  }
18676
19131
 
18677
19132
  .board-column {
18678
19133
  min-width: 240px;
18679
19134
  max-width: 300px;
18680
- flex: 1;
19135
+ flex: 0 0 auto;
18681
19136
  }
18682
19137
 
18683
19138
  .board-column-header {
@@ -18690,6 +19145,7 @@ tr:hover td {
18690
19145
  margin-bottom: 0.5rem;
18691
19146
  display: flex;
18692
19147
  justify-content: space-between;
19148
+ flex-shrink: 0;
18693
19149
  }
18694
19150
 
18695
19151
  .board-column-header .count {
@@ -18871,6 +19327,291 @@ tr:hover td {
18871
19327
  .mermaid-row .mermaid-container {
18872
19328
  margin: 0;
18873
19329
  }
19330
+
19331
+ /* Three-column artifact flow */
19332
+ .flow-diagram {
19333
+ background: var(--bg-card);
19334
+ border: 1px solid var(--border);
19335
+ border-radius: var(--radius);
19336
+ padding: 1.25rem;
19337
+ position: relative;
19338
+ overflow-x: auto;
19339
+ }
19340
+
19341
+ .flow-lines {
19342
+ position: absolute;
19343
+ top: 0;
19344
+ left: 0;
19345
+ pointer-events: none;
19346
+ }
19347
+
19348
+ .flow-columns {
19349
+ display: flex;
19350
+ gap: 3rem;
19351
+ position: relative;
19352
+ min-width: 600px;
19353
+ }
19354
+
19355
+ .flow-column {
19356
+ flex: 1;
19357
+ min-width: 0;
19358
+ display: flex;
19359
+ flex-direction: column;
19360
+ gap: 0.5rem;
19361
+ }
19362
+
19363
+ .flow-column-header {
19364
+ font-size: 0.7rem;
19365
+ text-transform: uppercase;
19366
+ letter-spacing: 0.06em;
19367
+ color: var(--text-dim);
19368
+ font-weight: 600;
19369
+ padding-bottom: 0.4rem;
19370
+ border-bottom: 1px solid var(--border);
19371
+ margin-bottom: 0.25rem;
19372
+ }
19373
+
19374
+ .flow-node {
19375
+ padding: 0.5rem 0.65rem;
19376
+ border-radius: 6px;
19377
+ border-left: 3px solid var(--border);
19378
+ background: var(--bg);
19379
+ transition: border-color 0.15s, background 0.15s;
19380
+ }
19381
+
19382
+ .flow-node:hover {
19383
+ background: var(--bg-hover);
19384
+ }
19385
+
19386
+ .flow-node-id {
19387
+ display: inline-block;
19388
+ font-family: var(--mono);
19389
+ font-size: 0.65rem;
19390
+ color: var(--accent);
19391
+ margin-bottom: 0.15rem;
19392
+ text-decoration: none;
19393
+ }
19394
+
19395
+ .flow-node-id:hover {
19396
+ text-decoration: underline;
19397
+ }
19398
+
19399
+ .flow-node-title {
19400
+ display: block;
19401
+ font-size: 0.8rem;
19402
+ }
19403
+
19404
+ .flow-done { border-left-color: var(--green); }
19405
+ .flow-active { border-left-color: var(--amber); }
19406
+ .flow-blocked { border-left-color: var(--red); }
19407
+ .flow-default { border-left-color: var(--accent-dim); }
19408
+
19409
+ .flow-node { cursor: pointer; transition: opacity 0.2s, border-color 0.15s, background 0.15s; }
19410
+ .flow-dim { opacity: 0.2; }
19411
+ .flow-lit { background: var(--bg-hover); }
19412
+ .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
19413
+ .flow-line-dim { opacity: 0.08; }
19414
+
19415
+ /* Gantt truncation note */
19416
+ .mermaid-note {
19417
+ font-size: 0.75rem;
19418
+ color: var(--text-dim);
19419
+ text-align: right;
19420
+ margin-bottom: 0.5rem;
19421
+ }
19422
+
19423
+ /* HTML Gantt chart */
19424
+ .gantt {
19425
+ background: var(--bg-card);
19426
+ border: 1px solid var(--border);
19427
+ border-radius: var(--radius);
19428
+ padding: 1.25rem 1.25rem 1.25rem 0;
19429
+ position: relative;
19430
+ overflow-x: auto;
19431
+ }
19432
+
19433
+ .gantt-chart {
19434
+ min-width: 600px;
19435
+ }
19436
+
19437
+ .gantt-overlay {
19438
+ position: absolute;
19439
+ top: 0;
19440
+ left: 0;
19441
+ right: 0;
19442
+ bottom: 0;
19443
+ pointer-events: none;
19444
+ display: flex;
19445
+ }
19446
+
19447
+ .gantt-header,
19448
+ .gantt-section-row,
19449
+ .gantt-row,
19450
+ .gantt-overlay {
19451
+ display: flex;
19452
+ align-items: center;
19453
+ }
19454
+
19455
+ .gantt-label {
19456
+ width: 200px;
19457
+ min-width: 200px;
19458
+ padding: 0.3rem 0.75rem;
19459
+ font-size: 0.8rem;
19460
+ color: var(--text-dim);
19461
+ text-align: right;
19462
+ white-space: nowrap;
19463
+ overflow: hidden;
19464
+ text-overflow: ellipsis;
19465
+ }
19466
+
19467
+ .gantt-section-label {
19468
+ font-weight: 600;
19469
+ color: var(--text);
19470
+ font-size: 0.75rem;
19471
+ text-transform: uppercase;
19472
+ letter-spacing: 0.03em;
19473
+ padding-top: 0.6rem;
19474
+ }
19475
+
19476
+ .gantt-track {
19477
+ flex: 1;
19478
+ position: relative;
19479
+ height: 28px;
19480
+ min-width: 0;
19481
+ }
19482
+
19483
+ .gantt-section-row .gantt-track {
19484
+ height: 20px;
19485
+ }
19486
+
19487
+ .gantt-section-bg {
19488
+ position: absolute;
19489
+ top: 0;
19490
+ bottom: 0;
19491
+ background: var(--bg-hover);
19492
+ border-radius: 3px;
19493
+ opacity: 0.4;
19494
+ }
19495
+
19496
+ .gantt-bar {
19497
+ position: absolute;
19498
+ top: 4px;
19499
+ bottom: 4px;
19500
+ border-radius: 4px;
19501
+ min-width: 6px;
19502
+ transition: opacity 0.15s;
19503
+ }
19504
+
19505
+ .gantt-bar:hover {
19506
+ opacity: 0.85;
19507
+ }
19508
+
19509
+ .gantt-bar-done {
19510
+ background: var(--green);
19511
+ }
19512
+
19513
+ .gantt-bar-active {
19514
+ background: var(--amber);
19515
+ }
19516
+
19517
+ .gantt-bar-blocked {
19518
+ background: var(--red);
19519
+ }
19520
+
19521
+ .gantt-bar-default {
19522
+ background: var(--accent-dim);
19523
+ }
19524
+
19525
+ .gantt-dates {
19526
+ height: 24px;
19527
+ border-bottom: 1px solid var(--border);
19528
+ margin-bottom: 0.25rem;
19529
+ }
19530
+
19531
+ .gantt-marker {
19532
+ position: absolute;
19533
+ top: 0;
19534
+ bottom: 0;
19535
+ border-left: 1px solid var(--border);
19536
+ }
19537
+
19538
+ .gantt-marker span {
19539
+ position: absolute;
19540
+ top: 2px;
19541
+ left: 6px;
19542
+ font-size: 0.65rem;
19543
+ color: var(--text-dim);
19544
+ white-space: nowrap;
19545
+ }
19546
+
19547
+ .gantt-today {
19548
+ position: absolute;
19549
+ top: 0;
19550
+ bottom: 0;
19551
+ width: 2px;
19552
+ background: var(--red);
19553
+ opacity: 0.7;
19554
+ }
19555
+
19556
+ /* Pie chart color overrides */
19557
+ .mermaid-container .pieCircle {
19558
+ stroke: var(--bg-card);
19559
+ }
19560
+
19561
+ .mermaid-container text.slice {
19562
+ fill: var(--bg) !important;
19563
+ font-weight: 600;
19564
+ }
19565
+
19566
+ /* Urgency row indicators */
19567
+ .urgency-row-overdue { border-left: 3px solid var(--red); }
19568
+ .urgency-row-due-3d { border-left: 3px solid var(--amber); }
19569
+ .urgency-row-due-7d { border-left: 3px solid #e2a308; }
19570
+
19571
+ /* Urgency badge pills */
19572
+ .urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
19573
+ .urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
19574
+ .urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
19575
+ .urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
19576
+ .urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
19577
+
19578
+ /* Trending */
19579
+ .trending-rank {
19580
+ display: inline-flex;
19581
+ align-items: center;
19582
+ justify-content: center;
19583
+ width: 24px;
19584
+ height: 24px;
19585
+ border-radius: 50%;
19586
+ background: var(--bg-hover);
19587
+ font-size: 0.75rem;
19588
+ font-weight: 600;
19589
+ color: var(--text-dim);
19590
+ }
19591
+
19592
+ .trending-score {
19593
+ display: inline-block;
19594
+ padding: 0.15rem 0.6rem;
19595
+ border-radius: 999px;
19596
+ font-size: 0.7rem;
19597
+ font-weight: 700;
19598
+ background: rgba(108, 140, 255, 0.15);
19599
+ color: var(--accent);
19600
+ }
19601
+
19602
+ .signal-tag {
19603
+ display: inline-block;
19604
+ padding: 0.1rem 0.45rem;
19605
+ border-radius: 4px;
19606
+ font-size: 0.65rem;
19607
+ background: var(--bg-hover);
19608
+ color: var(--text-dim);
19609
+ margin-right: 0.25rem;
19610
+ margin-bottom: 0.15rem;
19611
+ white-space: nowrap;
19612
+ }
19613
+
19614
+ .text-dim { color: var(--text-dim); }
18874
19615
  `;
18875
19616
  }
18876
19617
 
@@ -18879,98 +19620,290 @@ function sanitize(text, maxLen = 40) {
18879
19620
  const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
18880
19621
  return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
18881
19622
  }
18882
- function mermaidBlock(definition) {
18883
- return `<div class="mermaid-container"><pre class="mermaid">
19623
+ function mermaidBlock(definition, extraClass) {
19624
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
19625
+ return `<div class="${cls}"><pre class="mermaid">
18884
19626
  ${definition}
18885
19627
  </pre></div>`;
18886
19628
  }
18887
19629
  function placeholder(message) {
18888
19630
  return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
18889
19631
  }
18890
- function buildTimelineGantt(data) {
18891
- const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
19632
+ function toMs(date5) {
19633
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
19634
+ }
19635
+ function fmtDate(ms) {
19636
+ const d = new Date(ms);
19637
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
19638
+ return `${months[d.getMonth()]} ${d.getDate()}`;
19639
+ }
19640
+ function buildTimelineGantt(data, maxSprints = 6) {
19641
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
18892
19642
  if (sprintsWithDates.length === 0) {
18893
19643
  return placeholder("No timeline data available \u2014 sprints need start and end dates.");
18894
19644
  }
19645
+ const truncated = sprintsWithDates.length > maxSprints;
19646
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
19647
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
18895
19648
  const epicMap = new Map(data.epics.map((e) => [e.id, e]));
18896
- const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
18897
- for (const sprint of sprintsWithDates) {
18898
- lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
19649
+ const allStarts = visibleSprints.map((s) => toMs(s.startDate));
19650
+ const allEnds = visibleSprints.map((s) => toMs(s.endDate));
19651
+ const timelineStart = Math.min(...allStarts);
19652
+ const timelineEnd = Math.max(...allEnds);
19653
+ const span = timelineEnd - timelineStart || 1;
19654
+ const pct = (ms) => (ms - timelineStart) / span * 100;
19655
+ const DAY = 864e5;
19656
+ const markers = [];
19657
+ let tick = timelineStart;
19658
+ const startDay = new Date(tick).getDay();
19659
+ tick += (8 - startDay) % 7 * DAY;
19660
+ while (tick <= timelineEnd) {
19661
+ const left = pct(tick);
19662
+ markers.push(
19663
+ `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
19664
+ );
19665
+ tick += 7 * DAY;
19666
+ }
19667
+ const now = Date.now();
19668
+ let todayMarker = "";
19669
+ if (now >= timelineStart && now <= timelineEnd) {
19670
+ todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
19671
+ }
19672
+ const rows = [];
19673
+ for (const sprint of visibleSprints) {
19674
+ const sStart = toMs(sprint.startDate);
19675
+ const sEnd = toMs(sprint.endDate);
19676
+ rows.push(`<div class="gantt-section-row">
19677
+ <div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
19678
+ <div class="gantt-track">
19679
+ <div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
19680
+ </div>
19681
+ </div>`);
18899
19682
  const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
18900
- if (linked.length === 0) {
18901
- lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
18902
- } else {
18903
- for (const epic of linked) {
18904
- const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
18905
- lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
18906
- }
19683
+ const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
19684
+ for (const item of items) {
19685
+ 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";
19686
+ const left = pct(sStart).toFixed(2);
19687
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
19688
+ rows.push(`<div class="gantt-row">
19689
+ <div class="gantt-label">${item.label}</div>
19690
+ <div class="gantt-track">
19691
+ <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
19692
+ </div>
19693
+ </div>`);
18907
19694
  }
18908
19695
  }
18909
- return mermaidBlock(lines.join("\n"));
19696
+ const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
19697
+ return `${note}
19698
+ <div class="gantt">
19699
+ <div class="gantt-chart">
19700
+ <div class="gantt-header">
19701
+ <div class="gantt-label"></div>
19702
+ <div class="gantt-track gantt-dates">${markers.join("")}</div>
19703
+ </div>
19704
+ ${rows.join("\n")}
19705
+ </div>
19706
+ <div class="gantt-overlay">
19707
+ <div class="gantt-label"></div>
19708
+ <div class="gantt-track">${todayMarker}</div>
19709
+ </div>
19710
+ </div>`;
19711
+ }
19712
+ function statusClass(status) {
19713
+ const s = status.toLowerCase();
19714
+ if (s === "done" || s === "completed") return "flow-done";
19715
+ if (s === "in-progress" || s === "active") return "flow-active";
19716
+ if (s === "blocked") return "flow-blocked";
19717
+ return "flow-default";
18910
19718
  }
18911
19719
  function buildArtifactFlowchart(data) {
18912
19720
  if (data.features.length === 0 && data.epics.length === 0) {
18913
19721
  return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
18914
19722
  }
18915
- const lines = ["graph TD"];
18916
- lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
18917
- lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
18918
- lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
18919
- lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
18920
- const nodeIds = /* @__PURE__ */ new Set();
19723
+ const edges = [];
19724
+ const epicsByFeature = /* @__PURE__ */ new Map();
18921
19725
  for (const epic of data.epics) {
18922
- for (const featureId of epic.linkedFeature) {
18923
- const feature = data.features.find((f) => f.id === featureId);
18924
- if (feature) {
18925
- const fNode = feature.id.replace(/-/g, "_");
18926
- const eNode = epic.id.replace(/-/g, "_");
18927
- if (!nodeIds.has(fNode)) {
18928
- lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
18929
- nodeIds.add(fNode);
18930
- }
18931
- if (!nodeIds.has(eNode)) {
18932
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18933
- nodeIds.add(eNode);
18934
- }
18935
- lines.push(` ${fNode} --> ${eNode}`);
18936
- }
19726
+ for (const fid of epic.linkedFeature) {
19727
+ if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
19728
+ epicsByFeature.get(fid).push(epic.id);
19729
+ edges.push({ from: fid, to: epic.id });
18937
19730
  }
18938
19731
  }
19732
+ const sprintsByEpic = /* @__PURE__ */ new Map();
18939
19733
  for (const sprint of data.sprints) {
18940
- const sNode = sprint.id.replace(/-/g, "_");
18941
- for (const epicId of sprint.linkedEpics) {
18942
- const epic = data.epics.find((e) => e.id === epicId);
18943
- if (epic) {
18944
- const eNode = epic.id.replace(/-/g, "_");
18945
- if (!nodeIds.has(eNode)) {
18946
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
18947
- nodeIds.add(eNode);
19734
+ for (const eid of sprint.linkedEpics) {
19735
+ if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
19736
+ sprintsByEpic.get(eid).push(sprint.id);
19737
+ edges.push({ from: eid, to: sprint.id });
19738
+ }
19739
+ }
19740
+ const connectedFeatureIds = new Set(epicsByFeature.keys());
19741
+ const connectedEpicIds = /* @__PURE__ */ new Set();
19742
+ for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
19743
+ for (const ids of sprintsByEpic.values()) ids.forEach(() => {
19744
+ });
19745
+ for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
19746
+ const connectedSprintIds = /* @__PURE__ */ new Set();
19747
+ for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
19748
+ const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
19749
+ const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
19750
+ const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
19751
+ if (features.length === 0 && epics.length === 0) {
19752
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
19753
+ }
19754
+ const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
19755
+ <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
19756
+ <span class="flow-node-title">${sanitize(title, 35)}</span>
19757
+ </div>`;
19758
+ const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
19759
+ const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
19760
+ const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
19761
+ const edgesJson = JSON.stringify(edges);
19762
+ return `
19763
+ <div class="flow-diagram" id="flow-diagram">
19764
+ <svg class="flow-lines" id="flow-lines"></svg>
19765
+ <div class="flow-columns">
19766
+ <div class="flow-column">
19767
+ <div class="flow-column-header">Features</div>
19768
+ ${featuresHtml}
19769
+ </div>
19770
+ <div class="flow-column">
19771
+ <div class="flow-column-header">Epics</div>
19772
+ ${epicsHtml}
19773
+ </div>
19774
+ <div class="flow-column">
19775
+ <div class="flow-column-header">Sprints</div>
19776
+ ${sprintsHtml}
19777
+ </div>
19778
+ </div>
19779
+ </div>
19780
+ <script>
19781
+ (function() {
19782
+ var edges = ${edgesJson};
19783
+ var container = document.getElementById('flow-diagram');
19784
+ var svg = document.getElementById('flow-lines');
19785
+ if (!container || !svg) return;
19786
+
19787
+ // Build directed adjacency maps for traversal
19788
+ var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
19789
+ var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
19790
+ edges.forEach(function(e) {
19791
+ if (!fwd[e.from]) fwd[e.from] = [];
19792
+ if (!bwd[e.to]) bwd[e.to] = [];
19793
+ fwd[e.from].push(e.to);
19794
+ bwd[e.to].push(e.from);
19795
+ });
19796
+
19797
+ function drawLines() {
19798
+ var rect = container.getBoundingClientRect();
19799
+ svg.setAttribute('width', rect.width);
19800
+ svg.setAttribute('height', rect.height);
19801
+ svg.innerHTML = '';
19802
+
19803
+ edges.forEach(function(edge) {
19804
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
19805
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
19806
+ if (!fromEl || !toEl) return;
19807
+
19808
+ var fr = fromEl.getBoundingClientRect();
19809
+ var tr = toEl.getBoundingClientRect();
19810
+ var x1 = fr.right - rect.left;
19811
+ var y1 = fr.top + fr.height / 2 - rect.top;
19812
+ var x2 = tr.left - rect.left;
19813
+ var y2 = tr.top + tr.height / 2 - rect.top;
19814
+ var mx = (x1 + x2) / 2;
19815
+
19816
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
19817
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
19818
+ path.setAttribute('fill', 'none');
19819
+ path.setAttribute('stroke', '#2a2e3a');
19820
+ path.setAttribute('stroke-width', '1.5');
19821
+ path.dataset.from = edge.from;
19822
+ path.dataset.to = edge.to;
19823
+ svg.appendChild(path);
19824
+ });
19825
+ }
19826
+
19827
+ // Find directly related nodes via directed traversal
19828
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
19829
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
19830
+ function findConnected(startId) {
19831
+ var visited = {};
19832
+ visited[startId] = true;
19833
+ // Traverse forward (from\u2192to direction)
19834
+ var queue = [startId];
19835
+ while (queue.length) {
19836
+ var id = queue.shift();
19837
+ (fwd[id] || []).forEach(function(neighbor) {
19838
+ if (!visited[neighbor]) {
19839
+ visited[neighbor] = true;
19840
+ queue.push(neighbor);
19841
+ }
19842
+ });
18948
19843
  }
18949
- if (!nodeIds.has(sNode)) {
18950
- lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
18951
- nodeIds.add(sNode);
19844
+ // Traverse backward (to\u2192from direction)
19845
+ queue = [startId];
19846
+ while (queue.length) {
19847
+ var id = queue.shift();
19848
+ (bwd[id] || []).forEach(function(neighbor) {
19849
+ if (!visited[neighbor]) {
19850
+ visited[neighbor] = true;
19851
+ queue.push(neighbor);
19852
+ }
19853
+ });
18952
19854
  }
18953
- lines.push(` ${eNode} --> ${sNode}`);
19855
+ return visited;
18954
19856
  }
18955
- }
18956
- }
18957
- if (nodeIds.size === 0) {
18958
- return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
18959
- }
18960
- const allItems = [
18961
- ...data.features.map((f) => ({ id: f.id, status: f.status })),
18962
- ...data.epics.map((e) => ({ id: e.id, status: e.status })),
18963
- ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
18964
- ];
18965
- for (const item of allItems) {
18966
- const node = item.id.replace(/-/g, "_");
18967
- if (!nodeIds.has(node)) continue;
18968
- const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
18969
- if (cls) {
18970
- lines.push(` class ${node} ${cls}`);
18971
- }
18972
- }
18973
- return mermaidBlock(lines.join("\n"));
19857
+
19858
+ function highlight(hoveredId) {
19859
+ var connected = findConnected(hoveredId);
19860
+ container.querySelectorAll('.flow-node').forEach(function(n) {
19861
+ if (connected[n.dataset.flowId]) {
19862
+ n.classList.add('flow-lit');
19863
+ n.classList.remove('flow-dim');
19864
+ } else {
19865
+ n.classList.add('flow-dim');
19866
+ n.classList.remove('flow-lit');
19867
+ }
19868
+ });
19869
+ svg.querySelectorAll('path').forEach(function(p) {
19870
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
19871
+ p.classList.add('flow-line-lit');
19872
+ p.classList.remove('flow-line-dim');
19873
+ } else {
19874
+ p.classList.add('flow-line-dim');
19875
+ p.classList.remove('flow-line-lit');
19876
+ }
19877
+ });
19878
+ }
19879
+
19880
+ function clearHighlight() {
19881
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
19882
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
19883
+ }
19884
+
19885
+ var activeId = null;
19886
+ container.addEventListener('click', function(e) {
19887
+ // Let the ID link navigate normally
19888
+ if (e.target.closest('a')) return;
19889
+
19890
+ var node = e.target.closest('.flow-node');
19891
+ var clickedId = node ? node.dataset.flowId : null;
19892
+
19893
+ if (!clickedId || clickedId === activeId) {
19894
+ activeId = null;
19895
+ clearHighlight();
19896
+ return;
19897
+ }
19898
+
19899
+ activeId = clickedId;
19900
+ highlight(clickedId);
19901
+ });
19902
+
19903
+ requestAnimationFrame(function() { setTimeout(drawLines, 100); });
19904
+ window.addEventListener('resize', drawLines);
19905
+ })();
19906
+ </script>`;
18974
19907
  }
18975
19908
  function buildStatusPie(title, counts) {
18976
19909
  const entries = Object.entries(counts).filter(([, v]) => v > 0);
@@ -19050,8 +19983,7 @@ function overviewPage(data, diagrams, navGroups) {
19050
19983
  ${groupSections}
19051
19984
  ${ungroupedSection}
19052
19985
 
19053
- <div class="section-title">Project Timeline</div>
19054
- ${buildTimelineGantt(diagrams)}
19986
+ <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
19055
19987
 
19056
19988
  <div class="section-title">Artifact Relationships</div>
19057
19989
  ${buildArtifactFlowchart(diagrams)}
@@ -19306,6 +20238,7 @@ function boardPage(data) {
19306
20238
  <span>${escapeHtml(col.status)}</span>
19307
20239
  <span class="count">${col.docs.length}</span>
19308
20240
  </div>
20241
+ <div class="board-column-cards">
19309
20242
  ${col.docs.map(
19310
20243
  (doc) => `
19311
20244
  <div class="board-card">
@@ -19316,28 +20249,166 @@ function boardPage(data) {
19316
20249
  </a>
19317
20250
  </div>`
19318
20251
  ).join("\n")}
20252
+ </div>
19319
20253
  </div>`
19320
20254
  ).join("\n");
19321
20255
  return `
19322
20256
  <div class="page-header">
19323
- <h2>Status Board</h2>
19324
- </div>
19325
-
19326
- <div class="filters">
19327
- <select onchange="filterByType(this.value)">
19328
- <option value="">All types</option>
19329
- ${typeOptions}
19330
- </select>
20257
+ <h2>Status Board</h2>
20258
+ </div>
20259
+
20260
+ <div class="filters">
20261
+ <select onchange="filterByType(this.value)">
20262
+ <option value="">All types</option>
20263
+ ${typeOptions}
20264
+ </select>
20265
+ </div>
20266
+
20267
+ ${data.columns.length > 0 ? `<div class="board">${columns}</div>` : `<div class="empty"><p>No documents to display.</p></div>`}
20268
+
20269
+ <script>
20270
+ function filterByType(type) {
20271
+ if (type) window.location = '/board/' + type;
20272
+ else window.location = '/board';
20273
+ }
20274
+ </script>
20275
+ `;
20276
+ }
20277
+
20278
+ // src/web/templates/pages/timeline.ts
20279
+ function timelinePage(diagrams) {
20280
+ return `
20281
+ <div class="page-header">
20282
+ <h2>Project Timeline</h2>
20283
+ <div class="subtitle">Sprint schedule with linked epics</div>
20284
+ </div>
20285
+
20286
+ ${buildTimelineGantt(diagrams)}
20287
+ `;
20288
+ }
20289
+
20290
+ // src/web/templates/pages/upcoming.ts
20291
+ function urgencyBadge(tier) {
20292
+ const labels = {
20293
+ overdue: "Overdue",
20294
+ "due-3d": "Due in 3d",
20295
+ "due-7d": "Due in 7d",
20296
+ upcoming: "Upcoming",
20297
+ later: "Later"
20298
+ };
20299
+ return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
20300
+ }
20301
+ function urgencyRowClass(tier) {
20302
+ if (tier === "overdue") return " urgency-row-overdue";
20303
+ if (tier === "due-3d") return " urgency-row-due-3d";
20304
+ if (tier === "due-7d") return " urgency-row-due-7d";
20305
+ return "";
20306
+ }
20307
+ function upcomingPage(data) {
20308
+ const hasActions = data.dueSoonActions.length > 0;
20309
+ const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
20310
+ const hasTrending = data.trending.length > 0;
20311
+ const actionsTable = hasActions ? `
20312
+ <h3 class="section-title">Due Soon \u2014 Actions</h3>
20313
+ <div class="table-wrap">
20314
+ <table>
20315
+ <thead>
20316
+ <tr>
20317
+ <th>ID</th>
20318
+ <th>Title</th>
20319
+ <th>Status</th>
20320
+ <th>Owner</th>
20321
+ <th>Due Date</th>
20322
+ <th>Urgency</th>
20323
+ <th>Tasks</th>
20324
+ </tr>
20325
+ </thead>
20326
+ <tbody>
20327
+ ${data.dueSoonActions.map(
20328
+ (a) => `
20329
+ <tr class="${urgencyRowClass(a.urgency)}">
20330
+ <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
20331
+ <td>${escapeHtml(a.title)}</td>
20332
+ <td>${statusBadge(a.status)}</td>
20333
+ <td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
20334
+ <td>${formatDate(a.dueDate)}</td>
20335
+ <td>${urgencyBadge(a.urgency)}</td>
20336
+ <td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
20337
+ </tr>`
20338
+ ).join("")}
20339
+ </tbody>
20340
+ </table>
20341
+ </div>` : "";
20342
+ const sprintTasksTable = hasSprintTasks ? `
20343
+ <h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
20344
+ <div class="table-wrap">
20345
+ <table>
20346
+ <thead>
20347
+ <tr>
20348
+ <th>ID</th>
20349
+ <th>Title</th>
20350
+ <th>Status</th>
20351
+ <th>Sprint</th>
20352
+ <th>Sprint Ends</th>
20353
+ <th>Urgency</th>
20354
+ </tr>
20355
+ </thead>
20356
+ <tbody>
20357
+ ${data.dueSoonSprintTasks.map(
20358
+ (t) => `
20359
+ <tr class="${urgencyRowClass(t.urgency)}">
20360
+ <td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
20361
+ <td>${escapeHtml(t.title)}</td>
20362
+ <td>${statusBadge(t.status)}</td>
20363
+ <td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
20364
+ <td>${formatDate(t.sprintEndDate)}</td>
20365
+ <td>${urgencyBadge(t.urgency)}</td>
20366
+ </tr>`
20367
+ ).join("")}
20368
+ </tbody>
20369
+ </table>
20370
+ </div>` : "";
20371
+ const trendingTable = hasTrending ? `
20372
+ <h3 class="section-title">Trending</h3>
20373
+ <div class="table-wrap">
20374
+ <table>
20375
+ <thead>
20376
+ <tr>
20377
+ <th>#</th>
20378
+ <th>ID</th>
20379
+ <th>Title</th>
20380
+ <th>Type</th>
20381
+ <th>Status</th>
20382
+ <th>Score</th>
20383
+ <th>Signals</th>
20384
+ </tr>
20385
+ </thead>
20386
+ <tbody>
20387
+ ${data.trending.map(
20388
+ (t, i) => `
20389
+ <tr>
20390
+ <td><span class="trending-rank">${i + 1}</span></td>
20391
+ <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
20392
+ <td>${escapeHtml(t.title)}</td>
20393
+ <td>${escapeHtml(typeLabel(t.type))}</td>
20394
+ <td>${statusBadge(t.status)}</td>
20395
+ <td><span class="trending-score">${t.score}</span></td>
20396
+ <td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
20397
+ </tr>`
20398
+ ).join("")}
20399
+ </tbody>
20400
+ </table>
20401
+ </div>` : "";
20402
+ const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
20403
+ return `
20404
+ <div class="page-header">
20405
+ <h2>Upcoming</h2>
20406
+ <div class="subtitle">Time-sensitive items and trending activity</div>
19331
20407
  </div>
19332
-
19333
- ${data.columns.length > 0 ? `<div class="board">${columns}</div>` : `<div class="empty"><p>No documents to display.</p></div>`}
19334
-
19335
- <script>
19336
- function filterByType(type) {
19337
- if (type) window.location = '/board/' + type;
19338
- else window.location = '/board';
19339
- }
19340
- </script>
20408
+ ${actionsTable}
20409
+ ${sprintTasksTable}
20410
+ ${trendingTable}
20411
+ ${emptyState}
19341
20412
  `;
19342
20413
  }
19343
20414
 
@@ -19362,6 +20433,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19362
20433
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
19363
20434
  return;
19364
20435
  }
20436
+ if (pathname === "/timeline") {
20437
+ const diagrams = getDiagramData(store);
20438
+ const body = timelinePage(diagrams);
20439
+ respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, mainClass: "expanded" }, body));
20440
+ return;
20441
+ }
19365
20442
  if (pathname === "/gar") {
19366
20443
  const report = getGarData(store, projectName);
19367
20444
  const body = garPage(report);
@@ -19375,6 +20452,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19375
20452
  respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
19376
20453
  return;
19377
20454
  }
20455
+ if (pathname === "/upcoming") {
20456
+ const data = getUpcomingData(store);
20457
+ const body = upcomingPage(data);
20458
+ respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
20459
+ return;
20460
+ }
19378
20461
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
19379
20462
  if (boardMatch) {
19380
20463
  const type = boardMatch[1];
@@ -19435,8 +20518,8 @@ import * as http from "http";
19435
20518
  import { exec } from "child_process";
19436
20519
 
19437
20520
  // src/skills/registry.ts
19438
- import * as fs8 from "fs";
19439
- import * as path8 from "path";
20521
+ import * as fs9 from "fs";
20522
+ import * as path9 from "path";
19440
20523
  import { fileURLToPath } from "url";
19441
20524
  import * as YAML5 from "yaml";
19442
20525
  import matter2 from "gray-matter";
@@ -19479,7 +20562,7 @@ Be thorough but concise. Focus on actionable insights.`,
19479
20562
  };
19480
20563
 
19481
20564
  // src/skills/builtin/jira/tools.ts
19482
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
20565
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19483
20566
 
19484
20567
  // src/skills/builtin/jira/client.ts
19485
20568
  var JiraClient = class {
@@ -19489,8 +20572,8 @@ var JiraClient = class {
19489
20572
  this.baseUrl = `https://${config2.host}/rest/api/2`;
19490
20573
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
19491
20574
  }
19492
- async request(path20, method = "GET", body) {
19493
- const url2 = `${this.baseUrl}${path20}`;
20575
+ async request(path21, method = "GET", body) {
20576
+ const url2 = `${this.baseUrl}${path21}`;
19494
20577
  const headers = {
19495
20578
  Authorization: this.authHeader,
19496
20579
  "Content-Type": "application/json",
@@ -19504,7 +20587,7 @@ var JiraClient = class {
19504
20587
  if (!response.ok) {
19505
20588
  const text = await response.text().catch(() => "");
19506
20589
  throw new Error(
19507
- `Jira API error ${response.status} ${method} ${path20}: ${text}`
20590
+ `Jira API error ${response.status} ${method} ${path21}: ${text}`
19508
20591
  );
19509
20592
  }
19510
20593
  if (response.status === 204) return void 0;
@@ -19584,11 +20667,12 @@ function findByJiraKey(store, jiraKey) {
19584
20667
  const docs = store.list({ type: JIRA_TYPE });
19585
20668
  return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
19586
20669
  }
19587
- function createJiraTools(store) {
20670
+ function createJiraTools(store, projectConfig) {
19588
20671
  const jiraUserConfig = loadUserConfig().jira;
20672
+ const defaultProjectKey = projectConfig?.jira?.projectKey;
19589
20673
  return [
19590
20674
  // --- Local read tools ---
19591
- tool19(
20675
+ tool20(
19592
20676
  "list_jira_issues",
19593
20677
  "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
19594
20678
  {
@@ -19616,7 +20700,7 @@ function createJiraTools(store) {
19616
20700
  },
19617
20701
  { annotations: { readOnlyHint: true } }
19618
20702
  ),
19619
- tool19(
20703
+ tool20(
19620
20704
  "get_jira_issue",
19621
20705
  "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
19622
20706
  {
@@ -19649,7 +20733,7 @@ function createJiraTools(store) {
19649
20733
  { annotations: { readOnlyHint: true } }
19650
20734
  ),
19651
20735
  // --- Jira → Local tools ---
19652
- tool19(
20736
+ tool20(
19653
20737
  "pull_jira_issue",
19654
20738
  "Fetch a single Jira issue by key and create/update a local JI-xxx document",
19655
20739
  {
@@ -19696,7 +20780,7 @@ function createJiraTools(store) {
19696
20780
  };
19697
20781
  }
19698
20782
  ),
19699
- tool19(
20783
+ tool20(
19700
20784
  "pull_jira_issues_jql",
19701
20785
  "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
19702
20786
  {
@@ -19744,15 +20828,27 @@ function createJiraTools(store) {
19744
20828
  }
19745
20829
  ),
19746
20830
  // --- Local → Jira tools ---
19747
- tool19(
20831
+ tool20(
19748
20832
  "push_artifact_to_jira",
19749
20833
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
19750
20834
  {
19751
20835
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
19752
- projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
20836
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
19753
20837
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
19754
20838
  },
19755
20839
  async (args) => {
20840
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
20841
+ if (!resolvedProjectKey) {
20842
+ return {
20843
+ content: [
20844
+ {
20845
+ type: "text",
20846
+ text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
20847
+ }
20848
+ ],
20849
+ isError: true
20850
+ };
20851
+ }
19756
20852
  const jira = createJiraClient(jiraUserConfig);
19757
20853
  if (!jira) return jiraNotConfiguredError();
19758
20854
  const artifact = store.get(args.artifactId);
@@ -19772,7 +20868,7 @@ function createJiraTools(store) {
19772
20868
  `Status: ${artifact.frontmatter.status}`
19773
20869
  ].join("\n");
19774
20870
  const jiraResult = await jira.client.createIssue({
19775
- project: { key: args.projectKey },
20871
+ project: { key: resolvedProjectKey },
19776
20872
  summary: artifact.frontmatter.title,
19777
20873
  description,
19778
20874
  issuetype: { name: args.issueType ?? "Task" }
@@ -19805,7 +20901,7 @@ function createJiraTools(store) {
19805
20901
  }
19806
20902
  ),
19807
20903
  // --- Bidirectional sync ---
19808
- tool19(
20904
+ tool20(
19809
20905
  "sync_jira_issue",
19810
20906
  "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
19811
20907
  {
@@ -19846,7 +20942,7 @@ function createJiraTools(store) {
19846
20942
  }
19847
20943
  ),
19848
20944
  // --- Local link tool ---
19849
- tool19(
20945
+ tool20(
19850
20946
  "link_artifact_to_jira",
19851
20947
  "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
19852
20948
  {
@@ -19913,14 +21009,14 @@ var jiraSkill = {
19913
21009
  documentTypeRegistrations: [
19914
21010
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
19915
21011
  ],
19916
- tools: (store) => createJiraTools(store),
21012
+ tools: (store, projectConfig) => createJiraTools(store, projectConfig),
19917
21013
  promptFragments: {
19918
21014
  "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
19919
21015
 
19920
21016
  **Available tools:**
19921
21017
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19922
21018
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
19923
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
21019
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
19924
21020
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
19925
21021
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
19926
21022
 
@@ -19934,13 +21030,13 @@ var jiraSkill = {
19934
21030
  **Available tools:**
19935
21031
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19936
21032
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
19937
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
21033
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
19938
21034
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
19939
21035
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
19940
21036
 
19941
21037
  **As Tech Lead, use Jira integration to:**
19942
21038
  - Pull technical issues and bugs for sprint planning and estimation
19943
- - Push epics and technical decisions to Jira for cross-team visibility
21039
+ - Push epics, tasks, and technical decisions to Jira for cross-team visibility
19944
21040
  - Bidirectional sync to keep local governance and Jira in alignment
19945
21041
  - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
19946
21042
  "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
@@ -19948,22 +21044,426 @@ var jiraSkill = {
19948
21044
  **Available tools:**
19949
21045
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
19950
21046
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
19951
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
21047
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
19952
21048
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
19953
21049
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
19954
21050
 
19955
21051
  **As Delivery Manager, use Jira integration to:**
19956
21052
  - Pull sprint issues for tracking progress and blockers
19957
- - Push actions and decisions to Jira for stakeholder visibility
21053
+ - Push actions, decisions, and tasks to Jira for stakeholder visibility
19958
21054
  - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
19959
21055
  - Sync status between Marvin governance items and Jira issues`
19960
21056
  }
19961
21057
  };
19962
21058
 
21059
+ // src/skills/builtin/prd-generator/tools.ts
21060
+ import * as fs8 from "fs";
21061
+ import * as path8 from "path";
21062
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
21063
+ var PRIORITY_ORDER2 = {
21064
+ critical: 0,
21065
+ high: 1,
21066
+ medium: 2,
21067
+ low: 3
21068
+ };
21069
+ function priorityRank2(p) {
21070
+ return PRIORITY_ORDER2[p ?? ""] ?? 99;
21071
+ }
21072
+ function gatherContext(store, focusFeature, includeDecisions = true, includeQuestions = true) {
21073
+ const allFeatures = store.list({ type: "feature" });
21074
+ const allEpics = store.list({ type: "epic" });
21075
+ const allTasks = store.list({ type: "task" });
21076
+ const allDecisions = includeDecisions ? store.list({ type: "decision" }) : [];
21077
+ const allQuestions = includeQuestions ? store.list({ type: "question" }) : [];
21078
+ const allActions = store.list({ type: "action" });
21079
+ let features = allFeatures;
21080
+ let epics = allEpics;
21081
+ let tasks = allTasks;
21082
+ if (focusFeature) {
21083
+ features = features.filter((f) => f.frontmatter.id === focusFeature);
21084
+ const featureIds = new Set(features.map((f) => f.frontmatter.id));
21085
+ epics = epics.filter(
21086
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).some((id) => featureIds.has(id))
21087
+ );
21088
+ const epicIds2 = new Set(epics.map((e) => e.frontmatter.id));
21089
+ tasks = tasks.filter(
21090
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).some((id) => epicIds2.has(id))
21091
+ );
21092
+ }
21093
+ const featuresByStatus = {};
21094
+ for (const f of features) {
21095
+ featuresByStatus[f.frontmatter.status] = (featuresByStatus[f.frontmatter.status] ?? 0) + 1;
21096
+ }
21097
+ const epicsByStatus = {};
21098
+ for (const e of epics) {
21099
+ epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
21100
+ }
21101
+ const epicIds = new Set(epics.map((e) => e.frontmatter.id));
21102
+ return {
21103
+ features: features.sort((a, b) => priorityRank2(a.frontmatter.priority) - priorityRank2(b.frontmatter.priority)).map((f) => ({
21104
+ id: f.frontmatter.id,
21105
+ title: f.frontmatter.title,
21106
+ status: f.frontmatter.status,
21107
+ priority: f.frontmatter.priority ?? "medium",
21108
+ content: f.content,
21109
+ linkedEpicCount: epics.filter(
21110
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
21111
+ ).length
21112
+ })),
21113
+ epics: epics.map((e) => ({
21114
+ id: e.frontmatter.id,
21115
+ title: e.frontmatter.title,
21116
+ status: e.frontmatter.status,
21117
+ linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
21118
+ targetDate: e.frontmatter.targetDate ?? null,
21119
+ estimatedEffort: e.frontmatter.estimatedEffort ?? null,
21120
+ content: e.content,
21121
+ linkedTaskCount: tasks.filter(
21122
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
21123
+ ).length
21124
+ })),
21125
+ tasks: tasks.map((t) => ({
21126
+ id: t.frontmatter.id,
21127
+ title: t.frontmatter.title,
21128
+ status: t.frontmatter.status,
21129
+ linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
21130
+ acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
21131
+ technicalNotes: t.frontmatter.technicalNotes ?? null,
21132
+ complexity: t.frontmatter.complexity ?? null,
21133
+ estimatedPoints: t.frontmatter.estimatedPoints ?? null,
21134
+ priority: t.frontmatter.priority ?? null
21135
+ })),
21136
+ decisions: allDecisions.map((d) => ({
21137
+ id: d.frontmatter.id,
21138
+ title: d.frontmatter.title,
21139
+ status: d.frontmatter.status,
21140
+ content: d.content
21141
+ })),
21142
+ questions: allQuestions.map((q) => ({
21143
+ id: q.frontmatter.id,
21144
+ title: q.frontmatter.title,
21145
+ status: q.frontmatter.status,
21146
+ content: q.content
21147
+ })),
21148
+ actions: allActions.filter((a) => {
21149
+ if (!focusFeature) return true;
21150
+ const tags = a.frontmatter.tags ?? [];
21151
+ return tags.some((t) => t.startsWith("epic:") && epicIds.has(t.replace("epic:", "")));
21152
+ }).map((a) => ({
21153
+ id: a.frontmatter.id,
21154
+ title: a.frontmatter.title,
21155
+ status: a.frontmatter.status,
21156
+ owner: a.frontmatter.owner ?? null,
21157
+ priority: a.frontmatter.priority ?? null,
21158
+ dueDate: a.frontmatter.dueDate ?? null
21159
+ })),
21160
+ summary: {
21161
+ totalFeatures: features.length,
21162
+ totalEpics: epics.length,
21163
+ totalTasks: tasks.length,
21164
+ featuresByStatus,
21165
+ epicsByStatus
21166
+ }
21167
+ };
21168
+ }
21169
+ function generateTaskMasterPrd(title, ctx, projectOverview) {
21170
+ const lines = [];
21171
+ lines.push(`# ${title}`);
21172
+ lines.push("");
21173
+ lines.push("## Project Overview");
21174
+ if (projectOverview) {
21175
+ lines.push(projectOverview);
21176
+ } else if (ctx.features.length > 0) {
21177
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
21178
+ }
21179
+ lines.push("");
21180
+ lines.push("## Goals");
21181
+ for (const f of ctx.features) {
21182
+ lines.push(`- **${f.title}** (${f.id}, Priority: ${f.priority}) \u2014 ${f.status}`);
21183
+ }
21184
+ lines.push("");
21185
+ lines.push("## Features and Requirements");
21186
+ lines.push("");
21187
+ for (const feature of ctx.features) {
21188
+ lines.push(`### ${feature.title} (${feature.id}) \u2014 Priority: ${feature.priority}`);
21189
+ lines.push("");
21190
+ if (feature.content) {
21191
+ lines.push(feature.content);
21192
+ lines.push("");
21193
+ }
21194
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
21195
+ if (featureEpics.length > 0) {
21196
+ lines.push("#### User Stories / Epics");
21197
+ lines.push("");
21198
+ for (const epic of featureEpics) {
21199
+ const effort = epic.estimatedEffort ? `, Effort: ${epic.estimatedEffort}` : "";
21200
+ lines.push(`- **${epic.id}: ${epic.title}** \u2014 Status: ${epic.status}${effort}`);
21201
+ if (epic.content) {
21202
+ lines.push(` ${epic.content.split("\n")[0]}`);
21203
+ }
21204
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
21205
+ if (epicTasks.length > 0) {
21206
+ lines.push("");
21207
+ lines.push("#### Implementation Tasks");
21208
+ lines.push("");
21209
+ for (const task of epicTasks) {
21210
+ const complexity = task.complexity ? `, Complexity: ${task.complexity}` : "";
21211
+ const points = task.estimatedPoints != null ? `, Points: ${task.estimatedPoints}` : "";
21212
+ lines.push(`- **${task.id}: ${task.title}**${complexity}${points}`);
21213
+ if (task.acceptanceCriteria) {
21214
+ lines.push(` Acceptance Criteria: ${task.acceptanceCriteria}`);
21215
+ }
21216
+ }
21217
+ }
21218
+ }
21219
+ lines.push("");
21220
+ }
21221
+ }
21222
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
21223
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
21224
+ const technicalNotes = ctx.tasks.filter((t) => t.technicalNotes).map((t) => `- **${t.id}**: ${t.technicalNotes}`);
21225
+ if (approvedDecisions.length > 0 || openQuestions.length > 0 || technicalNotes.length > 0) {
21226
+ lines.push("## Technical Considerations");
21227
+ lines.push("");
21228
+ if (approvedDecisions.length > 0) {
21229
+ lines.push("### Key Decisions");
21230
+ for (const d of approvedDecisions) {
21231
+ lines.push(`- **${d.id}: ${d.title}** \u2014 ${d.content.split("\n")[0]}`);
21232
+ }
21233
+ lines.push("");
21234
+ }
21235
+ if (technicalNotes.length > 0) {
21236
+ lines.push("### Technical Notes");
21237
+ for (const note of technicalNotes) {
21238
+ lines.push(note);
21239
+ }
21240
+ lines.push("");
21241
+ }
21242
+ if (openQuestions.length > 0) {
21243
+ lines.push("### Open Questions");
21244
+ for (const q of openQuestions) {
21245
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
21246
+ }
21247
+ lines.push("");
21248
+ }
21249
+ }
21250
+ lines.push("## Implementation Priorities");
21251
+ lines.push("");
21252
+ let priorityIdx = 1;
21253
+ for (const feature of ctx.features) {
21254
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
21255
+ const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
21256
+ return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
21257
+ });
21258
+ if (featureEpics.length === 0) continue;
21259
+ lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
21260
+ for (const epic of featureEpics) {
21261
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
21262
+ lines.push(` - ${epic.id}: ${epic.title} (${epic.status}) \u2014 ${epicTasks.length} task(s)`);
21263
+ }
21264
+ priorityIdx++;
21265
+ }
21266
+ lines.push("");
21267
+ return lines.join("\n");
21268
+ }
21269
+ function generateClaudeCodePrd(title, ctx, projectOverview) {
21270
+ const lines = [];
21271
+ lines.push(`# ${title}`);
21272
+ lines.push("");
21273
+ lines.push("## Overview");
21274
+ if (projectOverview) {
21275
+ lines.push(projectOverview);
21276
+ } else if (ctx.features.length > 0) {
21277
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
21278
+ }
21279
+ lines.push("");
21280
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
21281
+ if (approvedDecisions.length > 0) {
21282
+ lines.push("## Architecture & Technical Decisions");
21283
+ lines.push("");
21284
+ for (const d of approvedDecisions) {
21285
+ lines.push(`### ${d.id}: ${d.title}`);
21286
+ lines.push(d.content);
21287
+ lines.push("");
21288
+ }
21289
+ }
21290
+ lines.push("## Implementation Plan");
21291
+ lines.push("");
21292
+ const priorityGroups = {};
21293
+ for (const f of ctx.features) {
21294
+ const group = f.priority === "critical" || f.priority === "high" ? "Phase 1: High Priority" : "Phase 2: Medium & Low Priority";
21295
+ if (!priorityGroups[group]) priorityGroups[group] = [];
21296
+ priorityGroups[group].push(f);
21297
+ }
21298
+ for (const [phase, features] of Object.entries(priorityGroups)) {
21299
+ lines.push(`### ${phase}`);
21300
+ lines.push("");
21301
+ for (const feature of features) {
21302
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
21303
+ for (const epic of featureEpics) {
21304
+ lines.push(`- [ ] ${epic.id}: ${epic.title}`);
21305
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
21306
+ for (const task of epicTasks) {
21307
+ const complexity = task.complexity ? `complexity: ${task.complexity}` : "";
21308
+ const points = task.estimatedPoints != null ? `points: ${task.estimatedPoints}` : "";
21309
+ const meta3 = [complexity, points].filter(Boolean).join(", ");
21310
+ lines.push(` - [ ] ${task.id}: ${task.title}${meta3 ? ` (${meta3})` : ""}`);
21311
+ if (task.acceptanceCriteria) {
21312
+ lines.push(` - Acceptance: ${task.acceptanceCriteria}`);
21313
+ }
21314
+ if (task.technicalNotes) {
21315
+ lines.push(` - Notes: ${task.technicalNotes}`);
21316
+ }
21317
+ }
21318
+ }
21319
+ }
21320
+ lines.push("");
21321
+ }
21322
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
21323
+ if (openQuestions.length > 0) {
21324
+ lines.push("## Open Questions");
21325
+ lines.push("");
21326
+ for (const q of openQuestions) {
21327
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
21328
+ }
21329
+ lines.push("");
21330
+ }
21331
+ return lines.join("\n");
21332
+ }
21333
+ function createPrdTools(store) {
21334
+ return [
21335
+ tool21(
21336
+ "gather_prd_context",
21337
+ "Aggregate all governance artifacts (features, epics, tasks, decisions, questions, actions) into structured JSON for PRD generation",
21338
+ {
21339
+ focusFeature: external_exports.string().optional().describe("Filter context to a specific feature ID (e.g. 'F-001')"),
21340
+ includeDecisions: external_exports.boolean().optional().describe("Include decisions in context (default: true)"),
21341
+ includeQuestions: external_exports.boolean().optional().describe("Include questions in context (default: true)")
21342
+ },
21343
+ async (args) => {
21344
+ const ctx = gatherContext(store, args.focusFeature, args.includeDecisions ?? true, args.includeQuestions ?? true);
21345
+ return {
21346
+ content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }]
21347
+ };
21348
+ },
21349
+ { annotations: { readOnlyHint: true } }
21350
+ ),
21351
+ tool21(
21352
+ "generate_prd",
21353
+ "Generate a PRD document from governance artifacts and save it as a PRD-xxx document",
21354
+ {
21355
+ title: external_exports.string().describe("PRD title"),
21356
+ format: external_exports.enum(["taskmaster", "claude-code"]).describe("Output format: 'taskmaster' for Claude TaskMaster parse_prd, 'claude-code' for Claude Code consumption"),
21357
+ projectOverview: external_exports.string().optional().describe("Project overview text (synthesized from features if not provided)"),
21358
+ focusFeature: external_exports.string().optional().describe("Focus on a specific feature ID (e.g. 'F-001')"),
21359
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for the PRD document")
21360
+ },
21361
+ async (args) => {
21362
+ const ctx = gatherContext(store, args.focusFeature);
21363
+ const prdContent = args.format === "taskmaster" ? generateTaskMasterPrd(args.title, ctx, args.projectOverview) : generateClaudeCodePrd(args.title, ctx, args.projectOverview);
21364
+ const frontmatter = {
21365
+ title: args.title,
21366
+ status: "draft",
21367
+ format: args.format
21368
+ };
21369
+ if (args.focusFeature) frontmatter.focusFeature = args.focusFeature;
21370
+ if (args.tags) frontmatter.tags = args.tags;
21371
+ const doc = store.create("prd", frontmatter, prdContent);
21372
+ return {
21373
+ content: [
21374
+ {
21375
+ type: "text",
21376
+ text: `Generated PRD ${doc.frontmatter.id}: "${args.title}" (format: ${args.format}, ${ctx.summary.totalFeatures} features, ${ctx.summary.totalEpics} epics, ${ctx.summary.totalTasks} tasks)`
21377
+ }
21378
+ ]
21379
+ };
21380
+ }
21381
+ ),
21382
+ tool21(
21383
+ "export_prd",
21384
+ "Export a PRD document to a file path for external consumption (e.g. by Claude TaskMaster or Claude Code)",
21385
+ {
21386
+ prdId: external_exports.string().describe("PRD document ID (e.g. 'PRD-001')"),
21387
+ outputPath: external_exports.string().describe("File path to write the PRD content to")
21388
+ },
21389
+ async (args) => {
21390
+ const doc = store.get(args.prdId);
21391
+ if (!doc) {
21392
+ return {
21393
+ content: [{ type: "text", text: `PRD ${args.prdId} not found` }],
21394
+ isError: true
21395
+ };
21396
+ }
21397
+ const outputDir = path8.dirname(args.outputPath);
21398
+ fs8.mkdirSync(outputDir, { recursive: true });
21399
+ fs8.writeFileSync(args.outputPath, doc.content, "utf-8");
21400
+ return {
21401
+ content: [
21402
+ {
21403
+ type: "text",
21404
+ text: `Exported PRD ${args.prdId} to ${args.outputPath}`
21405
+ }
21406
+ ]
21407
+ };
21408
+ }
21409
+ )
21410
+ ];
21411
+ }
21412
+
21413
+ // src/skills/builtin/prd-generator/index.ts
21414
+ var prdGeneratorSkill = {
21415
+ id: "prd-generator",
21416
+ name: "PRD Generator",
21417
+ description: "Generate PRDs from governance artifacts for TaskMaster or Claude Code",
21418
+ version: "1.0.0",
21419
+ format: "builtin-ts",
21420
+ documentTypeRegistrations: [
21421
+ { type: "prd", dirName: "prds", idPrefix: "PRD" }
21422
+ ],
21423
+ tools: (store) => createPrdTools(store),
21424
+ promptFragments: {
21425
+ "tech-lead": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
21426
+
21427
+ **Available tools:**
21428
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, decisions, questions, and actions into structured JSON for analysis
21429
+ - \`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)
21430
+ - \`export_prd\` \u2014 export a PRD document to a file path for external use
21431
+
21432
+ **As Tech Lead, use PRD generation to:**
21433
+ - Create comprehensive PRDs that capture the full governance context
21434
+ - Export TaskMaster-format PRDs for automated task breakdown via \`parse_prd\`
21435
+ - Export Claude Code-format PRDs as implementation plans with checklists
21436
+ - Focus PRDs on specific features using the focusFeature parameter`,
21437
+ "delivery-manager": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
21438
+
21439
+ **Available tools:**
21440
+ - \`gather_prd_context\` \u2014 aggregate all governance artifacts into structured JSON for review
21441
+ - \`generate_prd\` \u2014 generate a formatted PRD document (taskmaster or claude-code format)
21442
+ - \`export_prd\` \u2014 export a PRD to a file path
21443
+
21444
+ **As Delivery Manager, use PRD generation to:**
21445
+ - Generate PRDs for stakeholder communication and project documentation
21446
+ - Review aggregated project context before sprint planning
21447
+ - Export PRDs to share with external teams or tools`,
21448
+ "product-owner": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
21449
+
21450
+ **Available tools:**
21451
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, and decisions into structured JSON
21452
+ - \`generate_prd\` \u2014 generate a formatted PRD document
21453
+ - \`export_prd\` \u2014 export a PRD to a file path
21454
+
21455
+ **As Product Owner, use PRD generation to:**
21456
+ - Generate PRDs that capture feature requirements and priorities
21457
+ - Review the complete governance context for product planning
21458
+ - Export PRDs for stakeholder review and sign-off`
21459
+ }
21460
+ };
21461
+
19963
21462
  // src/skills/registry.ts
19964
21463
  var BUILTIN_SKILLS = {
19965
21464
  "governance-review": governanceReviewSkill,
19966
- "jira": jiraSkill
21465
+ "jira": jiraSkill,
21466
+ "prd-generator": prdGeneratorSkill
19967
21467
  };
19968
21468
  var GOVERNANCE_TOOL_NAMES = [
19969
21469
  "mcp__marvin-governance__list_decisions",
@@ -19984,13 +21484,13 @@ var GOVERNANCE_TOOL_NAMES = [
19984
21484
  ];
19985
21485
  function getBuiltinSkillsDir() {
19986
21486
  const thisFile = fileURLToPath(import.meta.url);
19987
- return path8.join(path8.dirname(thisFile), "builtin");
21487
+ return path9.join(path9.dirname(thisFile), "builtin");
19988
21488
  }
19989
21489
  function loadSkillFromDirectory(dirPath) {
19990
- const skillMdPath = path8.join(dirPath, "SKILL.md");
19991
- if (!fs8.existsSync(skillMdPath)) return void 0;
21490
+ const skillMdPath = path9.join(dirPath, "SKILL.md");
21491
+ if (!fs9.existsSync(skillMdPath)) return void 0;
19992
21492
  try {
19993
- const raw = fs8.readFileSync(skillMdPath, "utf-8");
21493
+ const raw = fs9.readFileSync(skillMdPath, "utf-8");
19994
21494
  const { data, content } = matter2(raw);
19995
21495
  if (!data.name || !data.description) return void 0;
19996
21496
  const metadata = data.metadata ?? {};
@@ -20001,13 +21501,13 @@ function loadSkillFromDirectory(dirPath) {
20001
21501
  if (wildcardPrompt) {
20002
21502
  promptFragments["*"] = wildcardPrompt;
20003
21503
  }
20004
- const personasDir = path8.join(dirPath, "personas");
20005
- if (fs8.existsSync(personasDir)) {
21504
+ const personasDir = path9.join(dirPath, "personas");
21505
+ if (fs9.existsSync(personasDir)) {
20006
21506
  try {
20007
- for (const file2 of fs8.readdirSync(personasDir)) {
21507
+ for (const file2 of fs9.readdirSync(personasDir)) {
20008
21508
  if (!file2.endsWith(".md")) continue;
20009
21509
  const personaId = file2.replace(/\.md$/, "");
20010
- const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
21510
+ const personaPrompt = fs9.readFileSync(path9.join(personasDir, file2), "utf-8").trim();
20011
21511
  if (personaPrompt) {
20012
21512
  promptFragments[personaId] = personaPrompt;
20013
21513
  }
@@ -20016,10 +21516,10 @@ function loadSkillFromDirectory(dirPath) {
20016
21516
  }
20017
21517
  }
20018
21518
  let actions;
20019
- const actionsPath = path8.join(dirPath, "actions.yaml");
20020
- if (fs8.existsSync(actionsPath)) {
21519
+ const actionsPath = path9.join(dirPath, "actions.yaml");
21520
+ if (fs9.existsSync(actionsPath)) {
20021
21521
  try {
20022
- const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
21522
+ const actionsRaw = fs9.readFileSync(actionsPath, "utf-8");
20023
21523
  actions = YAML5.parse(actionsRaw);
20024
21524
  } catch {
20025
21525
  }
@@ -20046,10 +21546,10 @@ function loadAllSkills(marvinDir) {
20046
21546
  }
20047
21547
  try {
20048
21548
  const builtinDir = getBuiltinSkillsDir();
20049
- if (fs8.existsSync(builtinDir)) {
20050
- for (const entry of fs8.readdirSync(builtinDir)) {
20051
- const entryPath = path8.join(builtinDir, entry);
20052
- if (!fs8.statSync(entryPath).isDirectory()) continue;
21549
+ if (fs9.existsSync(builtinDir)) {
21550
+ for (const entry of fs9.readdirSync(builtinDir)) {
21551
+ const entryPath = path9.join(builtinDir, entry);
21552
+ if (!fs9.statSync(entryPath).isDirectory()) continue;
20053
21553
  if (skills.has(entry)) continue;
20054
21554
  const skill = loadSkillFromDirectory(entryPath);
20055
21555
  if (skill) skills.set(skill.id, skill);
@@ -20058,18 +21558,18 @@ function loadAllSkills(marvinDir) {
20058
21558
  } catch {
20059
21559
  }
20060
21560
  if (marvinDir) {
20061
- const skillsDir = path8.join(marvinDir, "skills");
20062
- if (fs8.existsSync(skillsDir)) {
21561
+ const skillsDir = path9.join(marvinDir, "skills");
21562
+ if (fs9.existsSync(skillsDir)) {
20063
21563
  let entries;
20064
21564
  try {
20065
- entries = fs8.readdirSync(skillsDir);
21565
+ entries = fs9.readdirSync(skillsDir);
20066
21566
  } catch {
20067
21567
  entries = [];
20068
21568
  }
20069
21569
  for (const entry of entries) {
20070
- const entryPath = path8.join(skillsDir, entry);
21570
+ const entryPath = path9.join(skillsDir, entry);
20071
21571
  try {
20072
- if (fs8.statSync(entryPath).isDirectory()) {
21572
+ if (fs9.statSync(entryPath).isDirectory()) {
20073
21573
  const skill = loadSkillFromDirectory(entryPath);
20074
21574
  if (skill) skills.set(skill.id, skill);
20075
21575
  continue;
@@ -20079,7 +21579,7 @@ function loadAllSkills(marvinDir) {
20079
21579
  }
20080
21580
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
20081
21581
  try {
20082
- const raw = fs8.readFileSync(entryPath, "utf-8");
21582
+ const raw = fs9.readFileSync(entryPath, "utf-8");
20083
21583
  const parsed = YAML5.parse(raw);
20084
21584
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
20085
21585
  const skill = {
@@ -20122,12 +21622,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
20122
21622
  }
20123
21623
  return registrations;
20124
21624
  }
20125
- function getSkillTools(skillIds, allSkills, store) {
21625
+ function getSkillTools(skillIds, allSkills, store, projectConfig) {
20126
21626
  const tools = [];
20127
21627
  for (const id of skillIds) {
20128
21628
  const skill = allSkills.get(id);
20129
21629
  if (skill?.tools) {
20130
- tools.push(...skill.tools(store));
21630
+ tools.push(...skill.tools(store, projectConfig));
20131
21631
  }
20132
21632
  }
20133
21633
  return tools;
@@ -20184,12 +21684,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
20184
21684
  return agents;
20185
21685
  }
20186
21686
  function migrateYamlToSkillMd(yamlPath, outputDir) {
20187
- const raw = fs8.readFileSync(yamlPath, "utf-8");
21687
+ const raw = fs9.readFileSync(yamlPath, "utf-8");
20188
21688
  const parsed = YAML5.parse(raw);
20189
21689
  if (!parsed?.id || !parsed?.name) {
20190
21690
  throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
20191
21691
  }
20192
- fs8.mkdirSync(outputDir, { recursive: true });
21692
+ fs9.mkdirSync(outputDir, { recursive: true });
20193
21693
  const frontmatter = {
20194
21694
  name: parsed.id,
20195
21695
  description: parsed.description ?? ""
@@ -20203,15 +21703,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
20203
21703
  const skillMd = matter2.stringify(wildcardPrompt ? `
20204
21704
  ${wildcardPrompt}
20205
21705
  ` : "\n", frontmatter);
20206
- fs8.writeFileSync(path8.join(outputDir, "SKILL.md"), skillMd, "utf-8");
21706
+ fs9.writeFileSync(path9.join(outputDir, "SKILL.md"), skillMd, "utf-8");
20207
21707
  if (promptFragments) {
20208
21708
  const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
20209
21709
  if (personaKeys.length > 0) {
20210
- const personasDir = path8.join(outputDir, "personas");
20211
- fs8.mkdirSync(personasDir, { recursive: true });
21710
+ const personasDir = path9.join(outputDir, "personas");
21711
+ fs9.mkdirSync(personasDir, { recursive: true });
20212
21712
  for (const personaId of personaKeys) {
20213
- fs8.writeFileSync(
20214
- path8.join(personasDir, `${personaId}.md`),
21713
+ fs9.writeFileSync(
21714
+ path9.join(personasDir, `${personaId}.md`),
20215
21715
  `${promptFragments[personaId]}
20216
21716
  `,
20217
21717
  "utf-8"
@@ -20221,8 +21721,8 @@ ${wildcardPrompt}
20221
21721
  }
20222
21722
  const actions = parsed.actions;
20223
21723
  if (actions && actions.length > 0) {
20224
- fs8.writeFileSync(
20225
- path8.join(outputDir, "actions.yaml"),
21724
+ fs9.writeFileSync(
21725
+ path9.join(outputDir, "actions.yaml"),
20226
21726
  YAML5.stringify(actions),
20227
21727
  "utf-8"
20228
21728
  );
@@ -20301,7 +21801,7 @@ function openBrowser(url2) {
20301
21801
  var runningServer = null;
20302
21802
  function createWebTools(store, projectName, navGroups) {
20303
21803
  return [
20304
- tool20(
21804
+ tool22(
20305
21805
  "start_web_dashboard",
20306
21806
  "Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
20307
21807
  {
@@ -20333,7 +21833,7 @@ function createWebTools(store, projectName, navGroups) {
20333
21833
  };
20334
21834
  }
20335
21835
  ),
20336
- tool20(
21836
+ tool22(
20337
21837
  "stop_web_dashboard",
20338
21838
  "Stop the running Marvin web dashboard.",
20339
21839
  {},
@@ -20353,7 +21853,7 @@ function createWebTools(store, projectName, navGroups) {
20353
21853
  };
20354
21854
  }
20355
21855
  ),
20356
- tool20(
21856
+ tool22(
20357
21857
  "get_web_dashboard_urls",
20358
21858
  "Get all available dashboard page URLs. The dashboard must be running.",
20359
21859
  {},
@@ -20367,6 +21867,7 @@ function createWebTools(store, projectName, navGroups) {
20367
21867
  const base = `http://localhost:${runningServer.port}`;
20368
21868
  const urls = {
20369
21869
  overview: base,
21870
+ upcoming: `${base}/upcoming`,
20370
21871
  gar: `${base}/gar`,
20371
21872
  board: `${base}/board`
20372
21873
  };
@@ -20379,7 +21880,7 @@ function createWebTools(store, projectName, navGroups) {
20379
21880
  },
20380
21881
  { annotations: { readOnlyHint: true } }
20381
21882
  ),
20382
- tool20(
21883
+ tool22(
20383
21884
  "get_dashboard_overview",
20384
21885
  "Get the project overview data: document type counts and recent activity. Works without the web server running.",
20385
21886
  {},
@@ -20401,7 +21902,7 @@ function createWebTools(store, projectName, navGroups) {
20401
21902
  },
20402
21903
  { annotations: { readOnlyHint: true } }
20403
21904
  ),
20404
- tool20(
21905
+ tool22(
20405
21906
  "get_dashboard_gar",
20406
21907
  "Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
20407
21908
  {},
@@ -20413,7 +21914,7 @@ function createWebTools(store, projectName, navGroups) {
20413
21914
  },
20414
21915
  { annotations: { readOnlyHint: true } }
20415
21916
  ),
20416
- tool20(
21917
+ tool22(
20417
21918
  "get_dashboard_board",
20418
21919
  "Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
20419
21920
  {
@@ -20440,6 +21941,18 @@ function createWebTools(store, projectName, navGroups) {
20440
21941
  };
20441
21942
  },
20442
21943
  { annotations: { readOnlyHint: true } }
21944
+ ),
21945
+ tool22(
21946
+ "get_dashboard_upcoming",
21947
+ "Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
21948
+ {},
21949
+ async () => {
21950
+ const data = getUpcomingData(store);
21951
+ return {
21952
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
21953
+ };
21954
+ },
21955
+ { annotations: { readOnlyHint: true } }
20443
21956
  )
20444
21957
  ];
20445
21958
  }
@@ -20496,8 +22009,8 @@ function slugify3(text) {
20496
22009
  }
20497
22010
 
20498
22011
  // src/sources/manifest.ts
20499
- import * as fs9 from "fs";
20500
- import * as path9 from "path";
22012
+ import * as fs10 from "fs";
22013
+ import * as path10 from "path";
20501
22014
  import * as crypto from "crypto";
20502
22015
  import * as YAML6 from "yaml";
20503
22016
  var MANIFEST_FILE = ".manifest.yaml";
@@ -20510,37 +22023,37 @@ var SourceManifestManager = class {
20510
22023
  manifestPath;
20511
22024
  sourcesDir;
20512
22025
  constructor(marvinDir) {
20513
- this.sourcesDir = path9.join(marvinDir, "sources");
20514
- this.manifestPath = path9.join(this.sourcesDir, MANIFEST_FILE);
22026
+ this.sourcesDir = path10.join(marvinDir, "sources");
22027
+ this.manifestPath = path10.join(this.sourcesDir, MANIFEST_FILE);
20515
22028
  this.manifest = this.load();
20516
22029
  }
20517
22030
  load() {
20518
- if (!fs9.existsSync(this.manifestPath)) {
22031
+ if (!fs10.existsSync(this.manifestPath)) {
20519
22032
  return emptyManifest();
20520
22033
  }
20521
- const raw = fs9.readFileSync(this.manifestPath, "utf-8");
22034
+ const raw = fs10.readFileSync(this.manifestPath, "utf-8");
20522
22035
  const parsed = YAML6.parse(raw);
20523
22036
  return parsed ?? emptyManifest();
20524
22037
  }
20525
22038
  save() {
20526
- fs9.mkdirSync(this.sourcesDir, { recursive: true });
20527
- fs9.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
22039
+ fs10.mkdirSync(this.sourcesDir, { recursive: true });
22040
+ fs10.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
20528
22041
  }
20529
22042
  scan() {
20530
22043
  const added = [];
20531
22044
  const changed = [];
20532
22045
  const removed = [];
20533
- if (!fs9.existsSync(this.sourcesDir)) {
22046
+ if (!fs10.existsSync(this.sourcesDir)) {
20534
22047
  return { added, changed, removed };
20535
22048
  }
20536
22049
  const onDisk = new Set(
20537
- fs9.readdirSync(this.sourcesDir).filter((f) => {
20538
- const ext = path9.extname(f).toLowerCase();
22050
+ fs10.readdirSync(this.sourcesDir).filter((f) => {
22051
+ const ext = path10.extname(f).toLowerCase();
20539
22052
  return SOURCE_EXTENSIONS.includes(ext);
20540
22053
  })
20541
22054
  );
20542
22055
  for (const fileName of onDisk) {
20543
- const filePath = path9.join(this.sourcesDir, fileName);
22056
+ const filePath = path10.join(this.sourcesDir, fileName);
20544
22057
  const hash2 = this.hashFile(filePath);
20545
22058
  const existing = this.manifest.files[fileName];
20546
22059
  if (!existing) {
@@ -20603,7 +22116,7 @@ var SourceManifestManager = class {
20603
22116
  this.save();
20604
22117
  }
20605
22118
  hashFile(filePath) {
20606
- const content = fs9.readFileSync(filePath);
22119
+ const content = fs10.readFileSync(filePath);
20607
22120
  return crypto.createHash("sha256").update(content).digest("hex");
20608
22121
  }
20609
22122
  };
@@ -20618,12 +22131,12 @@ async function startSession(options) {
20618
22131
  const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
20619
22132
  const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
20620
22133
  const sessionStore = new SessionStore(marvinDir);
20621
- const sourcesDir = path10.join(marvinDir, "sources");
20622
- const hasSourcesDir = fs10.existsSync(sourcesDir);
22134
+ const sourcesDir = path11.join(marvinDir, "sources");
22135
+ const hasSourcesDir = fs11.existsSync(sourcesDir);
20623
22136
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20624
22137
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20625
22138
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
20626
- const codeSkillTools = getSkillTools(skillIds, allSkills, store);
22139
+ const codeSkillTools = getSkillTools(skillIds, allSkills, store, config2.project);
20627
22140
  const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
20628
22141
  const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
20629
22142
  const allSkillIds = [...allSkills.keys()];
@@ -20735,6 +22248,7 @@ Marvin \u2014 ${persona.name}
20735
22248
  "mcp__marvin-governance__get_dashboard_overview",
20736
22249
  "mcp__marvin-governance__get_dashboard_gar",
20737
22250
  "mcp__marvin-governance__get_dashboard_board",
22251
+ "mcp__marvin-governance__get_dashboard_upcoming",
20738
22252
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
20739
22253
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
20740
22254
  ]
@@ -21095,13 +22609,13 @@ async function setApiKey() {
21095
22609
  }
21096
22610
 
21097
22611
  // src/cli/commands/ingest.ts
21098
- import * as fs12 from "fs";
21099
- import * as path12 from "path";
22612
+ import * as fs13 from "fs";
22613
+ import * as path13 from "path";
21100
22614
  import chalk8 from "chalk";
21101
22615
 
21102
22616
  // src/sources/ingest.ts
21103
- import * as fs11 from "fs";
21104
- import * as path11 from "path";
22617
+ import * as fs12 from "fs";
22618
+ import * as path12 from "path";
21105
22619
  import chalk7 from "chalk";
21106
22620
  import ora2 from "ora";
21107
22621
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
@@ -21204,15 +22718,15 @@ async function ingestFile(options) {
21204
22718
  const persona = getPersona(personaId);
21205
22719
  const manifest = new SourceManifestManager(marvinDir);
21206
22720
  const sourcesDir = manifest.sourcesDir;
21207
- const filePath = path11.join(sourcesDir, fileName);
21208
- if (!fs11.existsSync(filePath)) {
22721
+ const filePath = path12.join(sourcesDir, fileName);
22722
+ if (!fs12.existsSync(filePath)) {
21209
22723
  throw new Error(`Source file not found: ${filePath}`);
21210
22724
  }
21211
- const ext = path11.extname(fileName).toLowerCase();
22725
+ const ext = path12.extname(fileName).toLowerCase();
21212
22726
  const isPdf = ext === ".pdf";
21213
22727
  let fileContent = null;
21214
22728
  if (!isPdf) {
21215
- fileContent = fs11.readFileSync(filePath, "utf-8");
22729
+ fileContent = fs12.readFileSync(filePath, "utf-8");
21216
22730
  }
21217
22731
  const store = new DocumentStore(marvinDir);
21218
22732
  const createdArtifacts = [];
@@ -21315,9 +22829,9 @@ Ingest ended with error: ${message.subtype}`)
21315
22829
  async function ingestCommand(file2, options) {
21316
22830
  const project = loadProject();
21317
22831
  const marvinDir = project.marvinDir;
21318
- const sourcesDir = path12.join(marvinDir, "sources");
21319
- if (!fs12.existsSync(sourcesDir)) {
21320
- fs12.mkdirSync(sourcesDir, { recursive: true });
22832
+ const sourcesDir = path13.join(marvinDir, "sources");
22833
+ if (!fs13.existsSync(sourcesDir)) {
22834
+ fs13.mkdirSync(sourcesDir, { recursive: true });
21321
22835
  }
21322
22836
  const manifest = new SourceManifestManager(marvinDir);
21323
22837
  manifest.scan();
@@ -21328,8 +22842,8 @@ async function ingestCommand(file2, options) {
21328
22842
  return;
21329
22843
  }
21330
22844
  if (file2) {
21331
- const filePath = path12.join(sourcesDir, file2);
21332
- if (!fs12.existsSync(filePath)) {
22845
+ const filePath = path13.join(sourcesDir, file2);
22846
+ if (!fs13.existsSync(filePath)) {
21333
22847
  console.log(chalk8.red(`Source file not found: ${file2}`));
21334
22848
  console.log(chalk8.dim(`Expected at: ${filePath}`));
21335
22849
  console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
@@ -21396,7 +22910,7 @@ import ora3 from "ora";
21396
22910
  import { input as input3 } from "@inquirer/prompts";
21397
22911
 
21398
22912
  // src/git/repository.ts
21399
- import * as path13 from "path";
22913
+ import * as path14 from "path";
21400
22914
  import simpleGit from "simple-git";
21401
22915
  var MARVIN_GITIGNORE = `node_modules/
21402
22916
  .DS_Store
@@ -21416,7 +22930,7 @@ var DIR_TYPE_LABELS = {
21416
22930
  function buildCommitMessage(files) {
21417
22931
  const counts = /* @__PURE__ */ new Map();
21418
22932
  for (const f of files) {
21419
- const parts2 = f.split(path13.sep).join("/").split("/");
22933
+ const parts2 = f.split(path14.sep).join("/").split("/");
21420
22934
  const docsIdx = parts2.indexOf("docs");
21421
22935
  if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
21422
22936
  const dirName = parts2[docsIdx + 1];
@@ -21456,9 +22970,9 @@ var MarvinGit = class {
21456
22970
  );
21457
22971
  }
21458
22972
  await this.git.init();
21459
- const { writeFileSync: writeFileSync10 } = await import("fs");
21460
- writeFileSync10(
21461
- path13.join(this.marvinDir, ".gitignore"),
22973
+ const { writeFileSync: writeFileSync11 } = await import("fs");
22974
+ writeFileSync11(
22975
+ path14.join(this.marvinDir, ".gitignore"),
21462
22976
  MARVIN_GITIGNORE,
21463
22977
  "utf-8"
21464
22978
  );
@@ -21578,7 +23092,7 @@ var MarvinGit = class {
21578
23092
  }
21579
23093
  }
21580
23094
  static async clone(url2, targetDir) {
21581
- const marvinDir = path13.join(targetDir, ".marvin");
23095
+ const marvinDir = path14.join(targetDir, ".marvin");
21582
23096
  const { existsSync: existsSync17 } = await import("fs");
21583
23097
  if (existsSync17(marvinDir)) {
21584
23098
  throw new GitSyncError(
@@ -21758,13 +23272,13 @@ async function cloneCommand(url2, directory) {
21758
23272
  }
21759
23273
 
21760
23274
  // src/mcp/stdio-server.ts
21761
- import * as fs13 from "fs";
21762
- import * as path14 from "path";
23275
+ import * as fs14 from "fs";
23276
+ import * as path15 from "path";
21763
23277
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21764
23278
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21765
23279
 
21766
23280
  // src/skills/action-tools.ts
21767
- import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
23281
+ import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
21768
23282
 
21769
23283
  // src/skills/action-runner.ts
21770
23284
  import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
@@ -21830,7 +23344,7 @@ function createSkillActionTools(skills, context) {
21830
23344
  if (!skill.actions) continue;
21831
23345
  for (const action of skill.actions) {
21832
23346
  tools.push(
21833
- tool21(
23347
+ tool23(
21834
23348
  `${skill.id}__${action.id}`,
21835
23349
  action.description,
21836
23350
  {
@@ -21922,10 +23436,10 @@ ${lines.join("\n\n")}`;
21922
23436
  }
21923
23437
 
21924
23438
  // src/mcp/persona-tools.ts
21925
- import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
23439
+ import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
21926
23440
  function createPersonaTools(ctx, marvinDir) {
21927
23441
  return [
21928
- tool22(
23442
+ tool24(
21929
23443
  "set_persona",
21930
23444
  "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.",
21931
23445
  {
@@ -21955,7 +23469,7 @@ ${summaries}`
21955
23469
  };
21956
23470
  }
21957
23471
  ),
21958
- tool22(
23472
+ tool24(
21959
23473
  "get_persona_guidance",
21960
23474
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
21961
23475
  {
@@ -22057,16 +23571,16 @@ function collectTools(marvinDir) {
22057
23571
  const plugin = resolvePlugin(config2.methodology);
22058
23572
  const registrations = plugin?.documentTypeRegistrations ?? [];
22059
23573
  const store = new DocumentStore(marvinDir, registrations);
22060
- const sourcesDir = path14.join(marvinDir, "sources");
22061
- const hasSourcesDir = fs13.existsSync(sourcesDir);
23574
+ const sourcesDir = path15.join(marvinDir, "sources");
23575
+ const hasSourcesDir = fs14.existsSync(sourcesDir);
22062
23576
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
22063
23577
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
22064
23578
  const sessionStore = new SessionStore(marvinDir);
22065
23579
  const allSkills = loadAllSkills(marvinDir);
22066
23580
  const allSkillIds = [...allSkills.keys()];
22067
- const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
23581
+ const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
22068
23582
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
22069
- const projectRoot = path14.dirname(marvinDir);
23583
+ const projectRoot = path15.dirname(marvinDir);
22070
23584
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
22071
23585
  const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
22072
23586
  const navGroups = buildNavGroups({
@@ -22136,8 +23650,8 @@ async function serveCommand() {
22136
23650
  }
22137
23651
 
22138
23652
  // src/cli/commands/skills.ts
22139
- import * as fs14 from "fs";
22140
- import * as path15 from "path";
23653
+ import * as fs15 from "fs";
23654
+ import * as path16 from "path";
22141
23655
  import * as YAML7 from "yaml";
22142
23656
  import matter3 from "gray-matter";
22143
23657
  import chalk10 from "chalk";
@@ -22243,14 +23757,14 @@ async function skillsRemoveCommand(skillId, options) {
22243
23757
  }
22244
23758
  async function skillsCreateCommand(name) {
22245
23759
  const project = loadProject();
22246
- const skillsDir = path15.join(project.marvinDir, "skills");
22247
- fs14.mkdirSync(skillsDir, { recursive: true });
22248
- const skillDir = path15.join(skillsDir, name);
22249
- if (fs14.existsSync(skillDir)) {
23760
+ const skillsDir = path16.join(project.marvinDir, "skills");
23761
+ fs15.mkdirSync(skillsDir, { recursive: true });
23762
+ const skillDir = path16.join(skillsDir, name);
23763
+ if (fs15.existsSync(skillDir)) {
22250
23764
  console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
22251
23765
  return;
22252
23766
  }
22253
- fs14.mkdirSync(skillDir, { recursive: true });
23767
+ fs15.mkdirSync(skillDir, { recursive: true });
22254
23768
  const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
22255
23769
  const frontmatter = {
22256
23770
  name,
@@ -22264,7 +23778,7 @@ async function skillsCreateCommand(name) {
22264
23778
  You have the **${displayName}** skill.
22265
23779
  `;
22266
23780
  const skillMd = matter3.stringify(body, frontmatter);
22267
- fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
23781
+ fs15.writeFileSync(path16.join(skillDir, "SKILL.md"), skillMd, "utf-8");
22268
23782
  const actions = [
22269
23783
  {
22270
23784
  id: "run",
@@ -22274,7 +23788,7 @@ You have the **${displayName}** skill.
22274
23788
  maxTurns: 5
22275
23789
  }
22276
23790
  ];
22277
- fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
23791
+ fs15.writeFileSync(path16.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
22278
23792
  console.log(chalk10.green(`Created skill: ${skillDir}/`));
22279
23793
  console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
22280
23794
  console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
@@ -22282,14 +23796,14 @@ You have the **${displayName}** skill.
22282
23796
  }
22283
23797
  async function skillsMigrateCommand() {
22284
23798
  const project = loadProject();
22285
- const skillsDir = path15.join(project.marvinDir, "skills");
22286
- if (!fs14.existsSync(skillsDir)) {
23799
+ const skillsDir = path16.join(project.marvinDir, "skills");
23800
+ if (!fs15.existsSync(skillsDir)) {
22287
23801
  console.log(chalk10.dim("No skills directory found."));
22288
23802
  return;
22289
23803
  }
22290
23804
  let entries;
22291
23805
  try {
22292
- entries = fs14.readdirSync(skillsDir);
23806
+ entries = fs15.readdirSync(skillsDir);
22293
23807
  } catch {
22294
23808
  console.log(chalk10.red("Could not read skills directory."));
22295
23809
  return;
@@ -22301,16 +23815,16 @@ async function skillsMigrateCommand() {
22301
23815
  }
22302
23816
  let migrated = 0;
22303
23817
  for (const file2 of yamlFiles) {
22304
- const yamlPath = path15.join(skillsDir, file2);
23818
+ const yamlPath = path16.join(skillsDir, file2);
22305
23819
  const baseName = file2.replace(/\.(yaml|yml)$/, "");
22306
- const outputDir = path15.join(skillsDir, baseName);
22307
- if (fs14.existsSync(outputDir)) {
23820
+ const outputDir = path16.join(skillsDir, baseName);
23821
+ if (fs15.existsSync(outputDir)) {
22308
23822
  console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
22309
23823
  continue;
22310
23824
  }
22311
23825
  try {
22312
23826
  migrateYamlToSkillMd(yamlPath, outputDir);
22313
- fs14.renameSync(yamlPath, `${yamlPath}.bak`);
23827
+ fs15.renameSync(yamlPath, `${yamlPath}.bak`);
22314
23828
  console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
22315
23829
  migrated++;
22316
23830
  } catch (err) {
@@ -22324,35 +23838,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
22324
23838
  }
22325
23839
 
22326
23840
  // src/cli/commands/import.ts
22327
- import * as fs17 from "fs";
22328
- import * as path18 from "path";
23841
+ import * as fs18 from "fs";
23842
+ import * as path19 from "path";
22329
23843
  import chalk11 from "chalk";
22330
23844
 
22331
23845
  // src/import/engine.ts
22332
- import * as fs16 from "fs";
22333
- import * as path17 from "path";
23846
+ import * as fs17 from "fs";
23847
+ import * as path18 from "path";
22334
23848
  import matter5 from "gray-matter";
22335
23849
 
22336
23850
  // src/import/classifier.ts
22337
- import * as fs15 from "fs";
22338
- import * as path16 from "path";
23851
+ import * as fs16 from "fs";
23852
+ import * as path17 from "path";
22339
23853
  import matter4 from "gray-matter";
22340
23854
  var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
22341
23855
  var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
22342
23856
  var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
22343
23857
  function classifyPath(inputPath, knownTypes, knownDirNames) {
22344
- const resolved = path16.resolve(inputPath);
22345
- const stat = fs15.statSync(resolved);
23858
+ const resolved = path17.resolve(inputPath);
23859
+ const stat = fs16.statSync(resolved);
22346
23860
  if (!stat.isDirectory()) {
22347
23861
  return classifyFile(resolved, knownTypes);
22348
23862
  }
22349
- if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
23863
+ if (path17.basename(resolved) === ".marvin" || fs16.existsSync(path17.join(resolved, "config.yaml"))) {
22350
23864
  return { type: "marvin-project", inputPath: resolved };
22351
23865
  }
22352
23866
  const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
22353
- const entries = fs15.readdirSync(resolved);
23867
+ const entries = fs16.readdirSync(resolved);
22354
23868
  const hasDocSubdirs = entries.some(
22355
- (e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
23869
+ (e) => allDirNames.has(e) && fs16.statSync(path17.join(resolved, e)).isDirectory()
22356
23870
  );
22357
23871
  if (hasDocSubdirs) {
22358
23872
  return { type: "docs-directory", inputPath: resolved };
@@ -22361,7 +23875,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
22361
23875
  if (mdFiles.length > 0) {
22362
23876
  const hasMarvinDocs = mdFiles.some((f) => {
22363
23877
  try {
22364
- const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
23878
+ const raw = fs16.readFileSync(path17.join(resolved, f), "utf-8");
22365
23879
  const { data } = matter4(raw);
22366
23880
  return isValidMarvinDocument(data, knownTypes);
22367
23881
  } catch {
@@ -22375,14 +23889,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
22375
23889
  return { type: "raw-source-dir", inputPath: resolved };
22376
23890
  }
22377
23891
  function classifyFile(filePath, knownTypes) {
22378
- const resolved = path16.resolve(filePath);
22379
- const ext = path16.extname(resolved).toLowerCase();
23892
+ const resolved = path17.resolve(filePath);
23893
+ const ext = path17.extname(resolved).toLowerCase();
22380
23894
  if (RAW_SOURCE_EXTENSIONS.has(ext)) {
22381
23895
  return { type: "raw-source-file", inputPath: resolved };
22382
23896
  }
22383
23897
  if (ext === ".md") {
22384
23898
  try {
22385
- const raw = fs15.readFileSync(resolved, "utf-8");
23899
+ const raw = fs16.readFileSync(resolved, "utf-8");
22386
23900
  const { data } = matter4(raw);
22387
23901
  if (isValidMarvinDocument(data, knownTypes)) {
22388
23902
  return { type: "marvin-document", inputPath: resolved };
@@ -22507,9 +24021,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
22507
24021
  continue;
22508
24022
  }
22509
24023
  if (item.action === "copy") {
22510
- const targetDir = path17.dirname(item.targetPath);
22511
- fs16.mkdirSync(targetDir, { recursive: true });
22512
- fs16.copyFileSync(item.sourcePath, item.targetPath);
24024
+ const targetDir = path18.dirname(item.targetPath);
24025
+ fs17.mkdirSync(targetDir, { recursive: true });
24026
+ fs17.copyFileSync(item.sourcePath, item.targetPath);
22513
24027
  copied++;
22514
24028
  continue;
22515
24029
  }
@@ -22545,19 +24059,19 @@ function formatPlanSummary(plan) {
22545
24059
  lines.push(`Documents to import: ${imports.length}`);
22546
24060
  for (const item of imports) {
22547
24061
  const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
22548
- lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
24062
+ lines.push(` ${idInfo} ${path18.basename(item.sourcePath)}`);
22549
24063
  }
22550
24064
  }
22551
24065
  if (copies.length > 0) {
22552
24066
  lines.push(`Files to copy to sources/: ${copies.length}`);
22553
24067
  for (const item of copies) {
22554
- lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
24068
+ lines.push(` ${path18.basename(item.sourcePath)} \u2192 ${path18.basename(item.targetPath)}`);
22555
24069
  }
22556
24070
  }
22557
24071
  if (skips.length > 0) {
22558
24072
  lines.push(`Skipped (conflict): ${skips.length}`);
22559
24073
  for (const item of skips) {
22560
- lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
24074
+ lines.push(` ${item.originalId ?? path18.basename(item.sourcePath)} ${item.reason ?? ""}`);
22561
24075
  }
22562
24076
  }
22563
24077
  if (plan.items.length === 0) {
@@ -22590,11 +24104,11 @@ function getDirNameForType(store, type) {
22590
24104
  }
22591
24105
  function collectMarvinDocs(dir, knownTypes) {
22592
24106
  const docs = [];
22593
- const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
24107
+ const files = fs17.readdirSync(dir).filter((f) => f.endsWith(".md"));
22594
24108
  for (const file2 of files) {
22595
- const filePath = path17.join(dir, file2);
24109
+ const filePath = path18.join(dir, file2);
22596
24110
  try {
22597
- const raw = fs16.readFileSync(filePath, "utf-8");
24111
+ const raw = fs17.readFileSync(filePath, "utf-8");
22598
24112
  const { data, content } = matter5(raw);
22599
24113
  if (isValidMarvinDocument(data, knownTypes)) {
22600
24114
  docs.push({
@@ -22650,23 +24164,23 @@ function planDocImports(docs, store, options) {
22650
24164
  }
22651
24165
  function planFromMarvinProject(classification, store, _marvinDir, options) {
22652
24166
  let projectDir = classification.inputPath;
22653
- if (path17.basename(projectDir) !== ".marvin") {
22654
- const inner = path17.join(projectDir, ".marvin");
22655
- if (fs16.existsSync(inner)) {
24167
+ if (path18.basename(projectDir) !== ".marvin") {
24168
+ const inner = path18.join(projectDir, ".marvin");
24169
+ if (fs17.existsSync(inner)) {
22656
24170
  projectDir = inner;
22657
24171
  }
22658
24172
  }
22659
- const docsDir = path17.join(projectDir, "docs");
22660
- if (!fs16.existsSync(docsDir)) {
24173
+ const docsDir = path18.join(projectDir, "docs");
24174
+ if (!fs17.existsSync(docsDir)) {
22661
24175
  return [];
22662
24176
  }
22663
24177
  const knownTypes = store.registeredTypes;
22664
24178
  const allDocs = [];
22665
- const subdirs = fs16.readdirSync(docsDir).filter(
22666
- (d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
24179
+ const subdirs = fs17.readdirSync(docsDir).filter(
24180
+ (d) => fs17.statSync(path18.join(docsDir, d)).isDirectory()
22667
24181
  );
22668
24182
  for (const subdir of subdirs) {
22669
- const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
24183
+ const docs = collectMarvinDocs(path18.join(docsDir, subdir), knownTypes);
22670
24184
  allDocs.push(...docs);
22671
24185
  }
22672
24186
  return planDocImports(allDocs, store, options);
@@ -22676,10 +24190,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
22676
24190
  const knownTypes = store.registeredTypes;
22677
24191
  const allDocs = [];
22678
24192
  allDocs.push(...collectMarvinDocs(dir, knownTypes));
22679
- const entries = fs16.readdirSync(dir);
24193
+ const entries = fs17.readdirSync(dir);
22680
24194
  for (const entry of entries) {
22681
- const entryPath = path17.join(dir, entry);
22682
- if (fs16.statSync(entryPath).isDirectory()) {
24195
+ const entryPath = path18.join(dir, entry);
24196
+ if (fs17.statSync(entryPath).isDirectory()) {
22683
24197
  allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
22684
24198
  }
22685
24199
  }
@@ -22688,7 +24202,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
22688
24202
  function planFromSingleDocument(classification, store, _marvinDir, options) {
22689
24203
  const filePath = classification.inputPath;
22690
24204
  const knownTypes = store.registeredTypes;
22691
- const raw = fs16.readFileSync(filePath, "utf-8");
24205
+ const raw = fs17.readFileSync(filePath, "utf-8");
22692
24206
  const { data, content } = matter5(raw);
22693
24207
  if (!isValidMarvinDocument(data, knownTypes)) {
22694
24208
  return [];
@@ -22704,14 +24218,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
22704
24218
  }
22705
24219
  function planFromRawSourceDir(classification, marvinDir) {
22706
24220
  const dir = classification.inputPath;
22707
- const sourcesDir = path17.join(marvinDir, "sources");
24221
+ const sourcesDir = path18.join(marvinDir, "sources");
22708
24222
  const items = [];
22709
- const files = fs16.readdirSync(dir).filter((f) => {
22710
- const stat = fs16.statSync(path17.join(dir, f));
24223
+ const files = fs17.readdirSync(dir).filter((f) => {
24224
+ const stat = fs17.statSync(path18.join(dir, f));
22711
24225
  return stat.isFile();
22712
24226
  });
22713
24227
  for (const file2 of files) {
22714
- const sourcePath = path17.join(dir, file2);
24228
+ const sourcePath = path18.join(dir, file2);
22715
24229
  const targetPath = resolveSourceFileName(sourcesDir, file2);
22716
24230
  items.push({
22717
24231
  action: "copy",
@@ -22722,8 +24236,8 @@ function planFromRawSourceDir(classification, marvinDir) {
22722
24236
  return items;
22723
24237
  }
22724
24238
  function planFromRawSourceFile(classification, marvinDir) {
22725
- const sourcesDir = path17.join(marvinDir, "sources");
22726
- const fileName = path17.basename(classification.inputPath);
24239
+ const sourcesDir = path18.join(marvinDir, "sources");
24240
+ const fileName = path18.basename(classification.inputPath);
22727
24241
  const targetPath = resolveSourceFileName(sourcesDir, fileName);
22728
24242
  return [
22729
24243
  {
@@ -22734,25 +24248,25 @@ function planFromRawSourceFile(classification, marvinDir) {
22734
24248
  ];
22735
24249
  }
22736
24250
  function resolveSourceFileName(sourcesDir, fileName) {
22737
- const targetPath = path17.join(sourcesDir, fileName);
22738
- if (!fs16.existsSync(targetPath)) {
24251
+ const targetPath = path18.join(sourcesDir, fileName);
24252
+ if (!fs17.existsSync(targetPath)) {
22739
24253
  return targetPath;
22740
24254
  }
22741
- const ext = path17.extname(fileName);
22742
- const base = path17.basename(fileName, ext);
24255
+ const ext = path18.extname(fileName);
24256
+ const base = path18.basename(fileName, ext);
22743
24257
  let counter = 1;
22744
24258
  let candidate;
22745
24259
  do {
22746
- candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
24260
+ candidate = path18.join(sourcesDir, `${base}-${counter}${ext}`);
22747
24261
  counter++;
22748
- } while (fs16.existsSync(candidate));
24262
+ } while (fs17.existsSync(candidate));
22749
24263
  return candidate;
22750
24264
  }
22751
24265
 
22752
24266
  // src/cli/commands/import.ts
22753
24267
  async function importCommand(inputPath, options) {
22754
- const resolved = path18.resolve(inputPath);
22755
- if (!fs17.existsSync(resolved)) {
24268
+ const resolved = path19.resolve(inputPath);
24269
+ if (!fs18.existsSync(resolved)) {
22756
24270
  throw new ImportError(`Path not found: ${resolved}`);
22757
24271
  }
22758
24272
  const project = loadProject();
@@ -22804,7 +24318,7 @@ async function importCommand(inputPath, options) {
22804
24318
  console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
22805
24319
  const manifest = new SourceManifestManager(marvinDir);
22806
24320
  manifest.scan();
22807
- const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
24321
+ const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path19.basename(i.targetPath));
22808
24322
  for (const fileName of copiedFileNames) {
22809
24323
  try {
22810
24324
  await ingestFile({
@@ -23693,14 +25207,14 @@ async function webCommand(options) {
23693
25207
  }
23694
25208
 
23695
25209
  // src/cli/commands/generate.ts
23696
- import * as fs18 from "fs";
23697
- import * as path19 from "path";
25210
+ import * as fs19 from "fs";
25211
+ import * as path20 from "path";
23698
25212
  import chalk18 from "chalk";
23699
25213
  import { confirm as confirm2 } from "@inquirer/prompts";
23700
25214
  async function generateClaudeMdCommand(options) {
23701
25215
  const project = loadProject();
23702
- const filePath = path19.join(project.marvinDir, "CLAUDE.md");
23703
- if (fs18.existsSync(filePath) && !options.force) {
25216
+ const filePath = path20.join(project.marvinDir, "CLAUDE.md");
25217
+ if (fs19.existsSync(filePath) && !options.force) {
23704
25218
  const overwrite = await confirm2({
23705
25219
  message: ".marvin/CLAUDE.md already exists. Overwrite?",
23706
25220
  default: false
@@ -23710,7 +25224,7 @@ async function generateClaudeMdCommand(options) {
23710
25224
  return;
23711
25225
  }
23712
25226
  }
23713
- fs18.writeFileSync(
25227
+ fs19.writeFileSync(
23714
25228
  filePath,
23715
25229
  getDefaultClaudeMdContent(project.config.name),
23716
25230
  "utf-8"
@@ -23723,7 +25237,7 @@ function createProgram() {
23723
25237
  const program2 = new Command();
23724
25238
  program2.name("marvin").description(
23725
25239
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
23726
- ).version("0.4.2");
25240
+ ).version("0.4.5");
23727
25241
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
23728
25242
  await initCommand();
23729
25243
  });