mrvn-cli 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // bin/marvin-serve.ts
9
- import * as path9 from "path";
9
+ import * as path10 from "path";
10
10
 
11
11
  // src/core/project.ts
12
12
  import * as fs2 from "fs";
@@ -98,8 +98,8 @@ function findProjectRoot(from = process.cwd()) {
98
98
  }
99
99
 
100
100
  // src/mcp/stdio-server.ts
101
- import * as fs8 from "fs";
102
- import * as path8 from "path";
101
+ import * as fs9 from "fs";
102
+ import * as path9 from "path";
103
103
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
104
104
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
105
105
 
@@ -1206,10 +1206,10 @@ function mergeDefs(...defs) {
1206
1206
  function cloneDef(schema) {
1207
1207
  return mergeDefs(schema._zod.def);
1208
1208
  }
1209
- function getElementAtPath(obj, path10) {
1210
- if (!path10)
1209
+ function getElementAtPath(obj, path11) {
1210
+ if (!path11)
1211
1211
  return obj;
1212
- return path10.reduce((acc, key) => acc?.[key], obj);
1212
+ return path11.reduce((acc, key) => acc?.[key], obj);
1213
1213
  }
1214
1214
  function promiseAllObject(promisesObj) {
1215
1215
  const keys = Object.keys(promisesObj);
@@ -1592,11 +1592,11 @@ function aborted(x, startIndex = 0) {
1592
1592
  }
1593
1593
  return false;
1594
1594
  }
1595
- function prefixIssues(path10, issues) {
1595
+ function prefixIssues(path11, issues) {
1596
1596
  return issues.map((iss) => {
1597
1597
  var _a2;
1598
1598
  (_a2 = iss).path ?? (_a2.path = []);
1599
- iss.path.unshift(path10);
1599
+ iss.path.unshift(path11);
1600
1600
  return iss;
1601
1601
  });
1602
1602
  }
@@ -1779,7 +1779,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1779
1779
  }
1780
1780
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1781
1781
  const result = { errors: [] };
1782
- const processError = (error49, path10 = []) => {
1782
+ const processError = (error49, path11 = []) => {
1783
1783
  var _a2, _b;
1784
1784
  for (const issue2 of error49.issues) {
1785
1785
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1789,7 +1789,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1789
1789
  } else if (issue2.code === "invalid_element") {
1790
1790
  processError({ issues: issue2.issues }, issue2.path);
1791
1791
  } else {
1792
- const fullpath = [...path10, ...issue2.path];
1792
+ const fullpath = [...path11, ...issue2.path];
1793
1793
  if (fullpath.length === 0) {
1794
1794
  result.errors.push(mapper(issue2));
1795
1795
  continue;
@@ -1821,8 +1821,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1821
1821
  }
1822
1822
  function toDotPath(_path) {
1823
1823
  const segs = [];
1824
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1825
- for (const seg of path10) {
1824
+ const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1825
+ for (const seg of path11) {
1826
1826
  if (typeof seg === "number")
1827
1827
  segs.push(`[${seg}]`);
1828
1828
  else if (typeof seg === "symbol")
@@ -13799,13 +13799,13 @@ function resolveRef(ref, ctx) {
13799
13799
  if (!ref.startsWith("#")) {
13800
13800
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13801
13801
  }
13802
- const path10 = ref.slice(1).split("/").filter(Boolean);
13803
- if (path10.length === 0) {
13802
+ const path11 = ref.slice(1).split("/").filter(Boolean);
13803
+ if (path11.length === 0) {
13804
13804
  return ctx.rootSchema;
13805
13805
  }
13806
13806
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13807
- if (path10[0] === defsKey) {
13808
- const key = path10[1];
13807
+ if (path11[0] === defsKey) {
13808
+ const key = path11[1];
13809
13809
  if (!key || !ctx.defs[key]) {
13810
13810
  throw new Error(`Reference not found: ${ref}`);
13811
13811
  }
@@ -16614,6 +16614,205 @@ function createSprintPlanningTools(store) {
16614
16614
  ];
16615
16615
  }
16616
16616
 
16617
+ // src/plugins/builtin/tools/tasks.ts
16618
+ import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
16619
+
16620
+ // src/plugins/builtin/tools/task-utils.ts
16621
+ function normalizeLinkedEpics(value) {
16622
+ if (value === void 0 || value === null) return [];
16623
+ if (typeof value === "string") {
16624
+ try {
16625
+ const parsed = JSON.parse(value);
16626
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
16627
+ } catch {
16628
+ }
16629
+ return [value];
16630
+ }
16631
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
16632
+ return [];
16633
+ }
16634
+ function generateEpicTags(epics) {
16635
+ return epics.map((id) => `epic:${id}`);
16636
+ }
16637
+
16638
+ // src/plugins/builtin/tools/tasks.ts
16639
+ var linkedEpicArray = external_exports.preprocess(
16640
+ (val) => {
16641
+ if (typeof val === "string") {
16642
+ try {
16643
+ const parsed = JSON.parse(val);
16644
+ if (Array.isArray(parsed)) return parsed;
16645
+ } catch {
16646
+ }
16647
+ return [val];
16648
+ }
16649
+ return val;
16650
+ },
16651
+ external_exports.array(external_exports.string())
16652
+ );
16653
+ function createTaskTools(store) {
16654
+ return [
16655
+ tool14(
16656
+ "list_tasks",
16657
+ "List all tasks in the project, optionally filtered by status, linked epic, or priority",
16658
+ {
16659
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Filter by task status"),
16660
+ linkedEpic: external_exports.string().optional().describe("Filter by linked epic ID (e.g. 'E-001')"),
16661
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority")
16662
+ },
16663
+ async (args) => {
16664
+ let docs = store.list({ type: "task", status: args.status });
16665
+ if (args.linkedEpic) {
16666
+ docs = docs.filter(
16667
+ (d) => normalizeLinkedEpics(d.frontmatter.linkedEpic).includes(args.linkedEpic)
16668
+ );
16669
+ }
16670
+ if (args.priority) {
16671
+ docs = docs.filter((d) => d.frontmatter.priority === args.priority);
16672
+ }
16673
+ const summary = docs.map((d) => ({
16674
+ id: d.frontmatter.id,
16675
+ title: d.frontmatter.title,
16676
+ status: d.frontmatter.status,
16677
+ linkedEpic: normalizeLinkedEpics(d.frontmatter.linkedEpic),
16678
+ priority: d.frontmatter.priority,
16679
+ complexity: d.frontmatter.complexity,
16680
+ estimatedPoints: d.frontmatter.estimatedPoints,
16681
+ tags: d.frontmatter.tags
16682
+ }));
16683
+ return {
16684
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16685
+ };
16686
+ },
16687
+ { annotations: { readOnlyHint: true } }
16688
+ ),
16689
+ tool14(
16690
+ "get_task",
16691
+ "Get the full content of a specific task by ID",
16692
+ { id: external_exports.string().describe("Task ID (e.g. 'T-001')") },
16693
+ async (args) => {
16694
+ const doc = store.get(args.id);
16695
+ if (!doc) {
16696
+ return {
16697
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
16698
+ isError: true
16699
+ };
16700
+ }
16701
+ return {
16702
+ content: [
16703
+ {
16704
+ type: "text",
16705
+ text: JSON.stringify(
16706
+ { ...doc.frontmatter, content: doc.content },
16707
+ null,
16708
+ 2
16709
+ )
16710
+ }
16711
+ ]
16712
+ };
16713
+ },
16714
+ { annotations: { readOnlyHint: true } }
16715
+ ),
16716
+ tool14(
16717
+ "create_task",
16718
+ "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).",
16719
+ {
16720
+ title: external_exports.string().describe("Task title"),
16721
+ content: external_exports.string().describe("Task description and implementation details"),
16722
+ linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
16723
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
16724
+ acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
16725
+ technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
16726
+ estimatedPoints: external_exports.number().optional().describe("Story point estimate"),
16727
+ complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
16728
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
16729
+ tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
16730
+ },
16731
+ async (args) => {
16732
+ const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
16733
+ const warnings = [];
16734
+ for (const epicId of linkedEpics) {
16735
+ const epic = store.get(epicId);
16736
+ if (!epic) {
16737
+ warnings.push(`Warning: Epic ${epicId} not found`);
16738
+ } else if (epic.frontmatter.type !== "epic") {
16739
+ warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
16740
+ }
16741
+ }
16742
+ const frontmatter = {
16743
+ title: args.title,
16744
+ status: args.status ?? "backlog",
16745
+ linkedEpic: linkedEpics,
16746
+ tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
16747
+ };
16748
+ if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
16749
+ if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
16750
+ if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
16751
+ if (args.complexity) frontmatter.complexity = args.complexity;
16752
+ if (args.priority) frontmatter.priority = args.priority;
16753
+ const doc = store.create("task", frontmatter, args.content);
16754
+ const parts = [
16755
+ `Created task ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${linkedEpics.join(", ")})`
16756
+ ];
16757
+ if (warnings.length > 0) {
16758
+ parts.push(warnings.join("; "));
16759
+ }
16760
+ return {
16761
+ content: [{ type: "text", text: parts.join("\n") }]
16762
+ };
16763
+ }
16764
+ ),
16765
+ tool14(
16766
+ "update_task",
16767
+ "Update an existing task, including its linked epics.",
16768
+ {
16769
+ id: external_exports.string().describe("Task ID to update"),
16770
+ title: external_exports.string().optional().describe("New title"),
16771
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
16772
+ content: external_exports.string().optional().describe("New content"),
16773
+ linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
16774
+ acceptanceCriteria: external_exports.string().optional().describe("New acceptance criteria"),
16775
+ technicalNotes: external_exports.string().optional().describe("New technical notes"),
16776
+ estimatedPoints: external_exports.number().optional().describe("New story point estimate"),
16777
+ complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
16778
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
16779
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)")
16780
+ },
16781
+ async (args) => {
16782
+ const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, ...updates } = args;
16783
+ const warnings = [];
16784
+ if (rawLinkedEpic !== void 0) {
16785
+ const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
16786
+ for (const epicId of linkedEpics) {
16787
+ const epic = store.get(epicId);
16788
+ if (!epic) {
16789
+ warnings.push(`Warning: Epic ${epicId} not found`);
16790
+ } else if (epic.frontmatter.type !== "epic") {
16791
+ warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
16792
+ }
16793
+ }
16794
+ updates.linkedEpic = linkedEpics;
16795
+ const existingDoc = store.get(id);
16796
+ const existingTags = existingDoc?.frontmatter.tags ?? [];
16797
+ const nonEpicTags = existingTags.filter((t) => !t.startsWith("epic:"));
16798
+ const baseTags = userTags ?? nonEpicTags;
16799
+ updates.tags = [...generateEpicTags(linkedEpics), ...baseTags];
16800
+ } else if (userTags !== void 0) {
16801
+ updates.tags = userTags;
16802
+ }
16803
+ const doc = store.update(id, updates, content);
16804
+ const parts = [`Updated task ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
16805
+ if (warnings.length > 0) {
16806
+ parts.push(warnings.join("; "));
16807
+ }
16808
+ return {
16809
+ content: [{ type: "text", text: parts.join("\n") }]
16810
+ };
16811
+ }
16812
+ )
16813
+ ];
16814
+ }
16815
+
16617
16816
  // src/plugins/common.ts
16618
16817
  var COMMON_REGISTRATIONS = [
16619
16818
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -16621,7 +16820,8 @@ var COMMON_REGISTRATIONS = [
16621
16820
  { type: "feature", dirName: "features", idPrefix: "F" },
16622
16821
  { type: "epic", dirName: "epics", idPrefix: "E" },
16623
16822
  { type: "contribution", dirName: "contributions", idPrefix: "C" },
16624
- { type: "sprint", dirName: "sprints", idPrefix: "SP" }
16823
+ { type: "sprint", dirName: "sprints", idPrefix: "SP" },
16824
+ { type: "task", dirName: "tasks", idPrefix: "T" }
16625
16825
  ];
16626
16826
  function createCommonTools(store) {
16627
16827
  return [
@@ -16631,7 +16831,8 @@ function createCommonTools(store) {
16631
16831
  ...createEpicTools(store),
16632
16832
  ...createContributionTools(store),
16633
16833
  ...createSprintTools(store),
16634
- ...createSprintPlanningTools(store)
16834
+ ...createSprintPlanningTools(store),
16835
+ ...createTaskTools(store)
16635
16836
  ];
16636
16837
  }
16637
16838
 
@@ -16641,7 +16842,7 @@ var genericAgilePlugin = {
16641
16842
  name: "Generic Agile",
16642
16843
  description: "Default methodology plugin providing standard agile governance patterns for decisions, actions, and questions.",
16643
16844
  version: "0.1.0",
16644
- documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint"],
16845
+ documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint", "task"],
16645
16846
  documentTypeRegistrations: [...COMMON_REGISTRATIONS],
16646
16847
  tools: (store) => [...createCommonTools(store)],
16647
16848
  promptFragments: {
@@ -16680,6 +16881,11 @@ var genericAgilePlugin = {
16680
16881
  - **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.
16681
16882
  - **update_epic**: Update epic status (planned \u2192 in-progress \u2192 done), owner, and other fields.
16682
16883
 
16884
+ **Task Tools:**
16885
+ - **list_tasks** / **get_task**: Browse and read implementation tasks.
16886
+ - **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".
16887
+ - **update_task**: Update task status (backlog \u2192 ready \u2192 in-progress \u2192 review \u2192 done), acceptance criteria, technical notes, complexity, priority, and estimated points.
16888
+
16683
16889
  **Feature Tools (read-only for awareness):**
16684
16890
  - **list_features** / **get_feature**: View features to understand what needs to be broken into epics.
16685
16891
 
@@ -16691,6 +16897,7 @@ var genericAgilePlugin = {
16691
16897
 
16692
16898
  **Key Workflow Rules:**
16693
16899
  - Only create epics against approved features \u2014 create_epic enforces this.
16900
+ - Break epics into tasks (T-xxx) with clear acceptance criteria and complexity estimates.
16694
16901
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
16695
16902
  - Collaborate with the Delivery Manager on target dates and effort estimates.
16696
16903
  - Each epic should have a clear scope and definition of done.
@@ -16726,6 +16933,9 @@ var genericAgilePlugin = {
16726
16933
  - **list_epics** / **get_epic**: View epics and their current status.
16727
16934
  - **update_epic**: Set targetDate and estimatedEffort on epics. Flag epics linked to deferred features.
16728
16935
 
16936
+ **Task Tools (read-only for tracking):**
16937
+ - **list_tasks** / **get_task**: View tasks and their statuses. Filter by linkedEpic to see implementation breakdown.
16938
+
16729
16939
  **Feature Tools (tracking focus):**
16730
16940
  - **list_features** / **get_feature**: View features and their priorities.
16731
16941
 
@@ -16771,14 +16981,15 @@ var genericAgilePlugin = {
16771
16981
  - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
16772
16982
  - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
16773
16983
  - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
16774
- "*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
16984
+ "*": `You have access to feature, epic, task, sprint, and meeting tools for project coordination:
16775
16985
 
16776
16986
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
16777
16987
  **Epics** (E-xxx): Implementation work packages created by the Tech Lead, linked to approved features. Epics progress through planned \u2192 in-progress \u2192 done.
16988
+ **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.
16778
16989
  **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).
16779
16990
  **Meetings**: Meeting records with attendees, agendas, and notes.
16780
16991
 
16781
- **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.
16992
+ **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.
16782
16993
 
16783
16994
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
16784
16995
  - **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.
@@ -16793,10 +17004,10 @@ var genericAgilePlugin = {
16793
17004
  };
16794
17005
 
16795
17006
  // src/plugins/builtin/tools/use-cases.ts
16796
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17007
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
16797
17008
  function createUseCaseTools(store) {
16798
17009
  return [
16799
- tool14(
17010
+ tool15(
16800
17011
  "list_use_cases",
16801
17012
  "List all extension use cases, optionally filtered by status or extension type",
16802
17013
  {
@@ -16826,7 +17037,7 @@ function createUseCaseTools(store) {
16826
17037
  },
16827
17038
  { annotations: { readOnlyHint: true } }
16828
17039
  ),
16829
- tool14(
17040
+ tool15(
16830
17041
  "get_use_case",
16831
17042
  "Get the full content of a specific use case by ID",
16832
17043
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -16853,7 +17064,7 @@ function createUseCaseTools(store) {
16853
17064
  },
16854
17065
  { annotations: { readOnlyHint: true } }
16855
17066
  ),
16856
- tool14(
17067
+ tool15(
16857
17068
  "create_use_case",
16858
17069
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
16859
17070
  {
@@ -16887,7 +17098,7 @@ function createUseCaseTools(store) {
16887
17098
  };
16888
17099
  }
16889
17100
  ),
16890
- tool14(
17101
+ tool15(
16891
17102
  "update_use_case",
16892
17103
  "Update an existing extension use case",
16893
17104
  {
@@ -16917,10 +17128,10 @@ function createUseCaseTools(store) {
16917
17128
  }
16918
17129
 
16919
17130
  // src/plugins/builtin/tools/tech-assessments.ts
16920
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17131
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
16921
17132
  function createTechAssessmentTools(store) {
16922
17133
  return [
16923
- tool15(
17134
+ tool16(
16924
17135
  "list_tech_assessments",
16925
17136
  "List all technology assessments, optionally filtered by status",
16926
17137
  {
@@ -16951,7 +17162,7 @@ function createTechAssessmentTools(store) {
16951
17162
  },
16952
17163
  { annotations: { readOnlyHint: true } }
16953
17164
  ),
16954
- tool15(
17165
+ tool16(
16955
17166
  "get_tech_assessment",
16956
17167
  "Get the full content of a specific technology assessment by ID",
16957
17168
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -16978,7 +17189,7 @@ function createTechAssessmentTools(store) {
16978
17189
  },
16979
17190
  { annotations: { readOnlyHint: true } }
16980
17191
  ),
16981
- tool15(
17192
+ tool16(
16982
17193
  "create_tech_assessment",
16983
17194
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
16984
17195
  {
@@ -17049,7 +17260,7 @@ function createTechAssessmentTools(store) {
17049
17260
  };
17050
17261
  }
17051
17262
  ),
17052
- tool15(
17263
+ tool16(
17053
17264
  "update_tech_assessment",
17054
17265
  "Update an existing technology assessment. The linked use case cannot be changed.",
17055
17266
  {
@@ -17079,10 +17290,10 @@ function createTechAssessmentTools(store) {
17079
17290
  }
17080
17291
 
17081
17292
  // src/plugins/builtin/tools/extension-designs.ts
17082
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17293
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17083
17294
  function createExtensionDesignTools(store) {
17084
17295
  return [
17085
- tool16(
17296
+ tool17(
17086
17297
  "list_extension_designs",
17087
17298
  "List all extension designs, optionally filtered by status",
17088
17299
  {
@@ -17112,7 +17323,7 @@ function createExtensionDesignTools(store) {
17112
17323
  },
17113
17324
  { annotations: { readOnlyHint: true } }
17114
17325
  ),
17115
- tool16(
17326
+ tool17(
17116
17327
  "get_extension_design",
17117
17328
  "Get the full content of a specific extension design by ID",
17118
17329
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -17139,7 +17350,7 @@ function createExtensionDesignTools(store) {
17139
17350
  },
17140
17351
  { annotations: { readOnlyHint: true } }
17141
17352
  ),
17142
- tool16(
17353
+ tool17(
17143
17354
  "create_extension_design",
17144
17355
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
17145
17356
  {
@@ -17207,7 +17418,7 @@ function createExtensionDesignTools(store) {
17207
17418
  };
17208
17419
  }
17209
17420
  ),
17210
- tool16(
17421
+ tool17(
17211
17422
  "update_extension_design",
17212
17423
  "Update an existing extension design. The linked tech assessment cannot be changed.",
17213
17424
  {
@@ -17236,10 +17447,10 @@ function createExtensionDesignTools(store) {
17236
17447
  }
17237
17448
 
17238
17449
  // src/plugins/builtin/tools/aem-reports.ts
17239
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17450
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17240
17451
  function createAemReportTools(store) {
17241
17452
  return [
17242
- tool17(
17453
+ tool18(
17243
17454
  "generate_extension_portfolio",
17244
17455
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
17245
17456
  {},
@@ -17291,7 +17502,7 @@ function createAemReportTools(store) {
17291
17502
  },
17292
17503
  { annotations: { readOnlyHint: true } }
17293
17504
  ),
17294
- tool17(
17505
+ tool18(
17295
17506
  "generate_tech_readiness",
17296
17507
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
17297
17508
  {},
@@ -17343,7 +17554,7 @@ function createAemReportTools(store) {
17343
17554
  },
17344
17555
  { annotations: { readOnlyHint: true } }
17345
17556
  ),
17346
- tool17(
17557
+ tool18(
17347
17558
  "generate_phase_status",
17348
17559
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
17349
17560
  {},
@@ -17405,11 +17616,11 @@ function createAemReportTools(store) {
17405
17616
  import * as fs6 from "fs";
17406
17617
  import * as path6 from "path";
17407
17618
  import * as YAML4 from "yaml";
17408
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17619
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17409
17620
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
17410
17621
  function createAemPhaseTools(store, marvinDir) {
17411
17622
  return [
17412
- tool18(
17623
+ tool19(
17413
17624
  "get_current_phase",
17414
17625
  "Get the current AEM phase from project configuration",
17415
17626
  {},
@@ -17430,7 +17641,7 @@ function createAemPhaseTools(store, marvinDir) {
17430
17641
  },
17431
17642
  { annotations: { readOnlyHint: true } }
17432
17643
  ),
17433
- tool18(
17644
+ tool19(
17434
17645
  "advance_phase",
17435
17646
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
17436
17647
  {
@@ -17694,8 +17905,8 @@ function getPluginPromptFragment(plugin, personaId) {
17694
17905
  }
17695
17906
 
17696
17907
  // src/skills/registry.ts
17697
- import * as fs7 from "fs";
17698
- import * as path7 from "path";
17908
+ import * as fs8 from "fs";
17909
+ import * as path8 from "path";
17699
17910
  import { fileURLToPath } from "url";
17700
17911
  import * as YAML5 from "yaml";
17701
17912
  import matter2 from "gray-matter";
@@ -17738,7 +17949,7 @@ Be thorough but concise. Focus on actionable insights.`,
17738
17949
  };
17739
17950
 
17740
17951
  // src/skills/builtin/jira/tools.ts
17741
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17952
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
17742
17953
 
17743
17954
  // src/skills/builtin/jira/client.ts
17744
17955
  var JiraClient = class {
@@ -17748,8 +17959,8 @@ var JiraClient = class {
17748
17959
  this.baseUrl = `https://${config2.host}/rest/api/2`;
17749
17960
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17750
17961
  }
17751
- async request(path10, method = "GET", body) {
17752
- const url2 = `${this.baseUrl}${path10}`;
17962
+ async request(path11, method = "GET", body) {
17963
+ const url2 = `${this.baseUrl}${path11}`;
17753
17964
  const headers = {
17754
17965
  Authorization: this.authHeader,
17755
17966
  "Content-Type": "application/json",
@@ -17763,7 +17974,7 @@ var JiraClient = class {
17763
17974
  if (!response.ok) {
17764
17975
  const text = await response.text().catch(() => "");
17765
17976
  throw new Error(
17766
- `Jira API error ${response.status} ${method} ${path10}: ${text}`
17977
+ `Jira API error ${response.status} ${method} ${path11}: ${text}`
17767
17978
  );
17768
17979
  }
17769
17980
  if (response.status === 204) return void 0;
@@ -17847,7 +18058,7 @@ function createJiraTools(store) {
17847
18058
  const jiraUserConfig = loadUserConfig().jira;
17848
18059
  return [
17849
18060
  // --- Local read tools ---
17850
- tool19(
18061
+ tool20(
17851
18062
  "list_jira_issues",
17852
18063
  "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17853
18064
  {
@@ -17875,7 +18086,7 @@ function createJiraTools(store) {
17875
18086
  },
17876
18087
  { annotations: { readOnlyHint: true } }
17877
18088
  ),
17878
- tool19(
18089
+ tool20(
17879
18090
  "get_jira_issue",
17880
18091
  "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17881
18092
  {
@@ -17908,7 +18119,7 @@ function createJiraTools(store) {
17908
18119
  { annotations: { readOnlyHint: true } }
17909
18120
  ),
17910
18121
  // --- Jira → Local tools ---
17911
- tool19(
18122
+ tool20(
17912
18123
  "pull_jira_issue",
17913
18124
  "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17914
18125
  {
@@ -17955,7 +18166,7 @@ function createJiraTools(store) {
17955
18166
  };
17956
18167
  }
17957
18168
  ),
17958
- tool19(
18169
+ tool20(
17959
18170
  "pull_jira_issues_jql",
17960
18171
  "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17961
18172
  {
@@ -18003,7 +18214,7 @@ function createJiraTools(store) {
18003
18214
  }
18004
18215
  ),
18005
18216
  // --- Local → Jira tools ---
18006
- tool19(
18217
+ tool20(
18007
18218
  "push_artifact_to_jira",
18008
18219
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
18009
18220
  {
@@ -18064,7 +18275,7 @@ function createJiraTools(store) {
18064
18275
  }
18065
18276
  ),
18066
18277
  // --- Bidirectional sync ---
18067
- tool19(
18278
+ tool20(
18068
18279
  "sync_jira_issue",
18069
18280
  "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
18070
18281
  {
@@ -18105,7 +18316,7 @@ function createJiraTools(store) {
18105
18316
  }
18106
18317
  ),
18107
18318
  // --- Local link tool ---
18108
- tool19(
18319
+ tool20(
18109
18320
  "link_artifact_to_jira",
18110
18321
  "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
18111
18322
  {
@@ -18193,13 +18404,13 @@ var jiraSkill = {
18193
18404
  **Available tools:**
18194
18405
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18195
18406
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18196
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
18407
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
18197
18408
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18198
18409
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18199
18410
 
18200
18411
  **As Tech Lead, use Jira integration to:**
18201
18412
  - Pull technical issues and bugs for sprint planning and estimation
18202
- - Push epics and technical decisions to Jira for cross-team visibility
18413
+ - Push epics, tasks, and technical decisions to Jira for cross-team visibility
18203
18414
  - Bidirectional sync to keep local governance and Jira in alignment
18204
18415
  - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
18205
18416
  "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
@@ -18213,74 +18424,478 @@ var jiraSkill = {
18213
18424
 
18214
18425
  **As Delivery Manager, use Jira integration to:**
18215
18426
  - Pull sprint issues for tracking progress and blockers
18216
- - Push actions and decisions to Jira for stakeholder visibility
18427
+ - Push actions, decisions, and tasks to Jira for stakeholder visibility
18217
18428
  - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
18218
18429
  - Sync status between Marvin governance items and Jira issues`
18219
18430
  }
18220
18431
  };
18221
18432
 
18222
- // src/skills/registry.ts
18223
- var BUILTIN_SKILLS = {
18224
- "governance-review": governanceReviewSkill,
18225
- "jira": jiraSkill
18433
+ // src/skills/builtin/prd-generator/tools.ts
18434
+ import * as fs7 from "fs";
18435
+ import * as path7 from "path";
18436
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
18437
+ var PRIORITY_ORDER2 = {
18438
+ critical: 0,
18439
+ high: 1,
18440
+ medium: 2,
18441
+ low: 3
18226
18442
  };
18227
- function getBuiltinSkillsDir() {
18228
- const thisFile = fileURLToPath(import.meta.url);
18229
- return path7.join(path7.dirname(thisFile), "builtin");
18230
- }
18231
- function loadSkillFromDirectory(dirPath) {
18232
- const skillMdPath = path7.join(dirPath, "SKILL.md");
18233
- if (!fs7.existsSync(skillMdPath)) return void 0;
18234
- try {
18235
- const raw = fs7.readFileSync(skillMdPath, "utf-8");
18236
- const { data, content } = matter2(raw);
18237
- if (!data.name || !data.description) return void 0;
18238
- const metadata = data.metadata ?? {};
18239
- const version2 = metadata.version ?? "1.0.0";
18240
- const personas = metadata.personas;
18241
- const promptFragments = {};
18242
- const wildcardPrompt = content.trim();
18243
- if (wildcardPrompt) {
18244
- promptFragments["*"] = wildcardPrompt;
18245
- }
18246
- const personasDir = path7.join(dirPath, "personas");
18247
- if (fs7.existsSync(personasDir)) {
18248
- try {
18249
- for (const file2 of fs7.readdirSync(personasDir)) {
18250
- if (!file2.endsWith(".md")) continue;
18251
- const personaId = file2.replace(/\.md$/, "");
18252
- const personaPrompt = fs7.readFileSync(path7.join(personasDir, file2), "utf-8").trim();
18253
- if (personaPrompt) {
18254
- promptFragments[personaId] = personaPrompt;
18443
+ function priorityRank2(p) {
18444
+ return PRIORITY_ORDER2[p ?? ""] ?? 99;
18445
+ }
18446
+ function gatherContext(store, focusFeature, includeDecisions = true, includeQuestions = true) {
18447
+ const allFeatures = store.list({ type: "feature" });
18448
+ const allEpics = store.list({ type: "epic" });
18449
+ const allTasks = store.list({ type: "task" });
18450
+ const allDecisions = includeDecisions ? store.list({ type: "decision" }) : [];
18451
+ const allQuestions = includeQuestions ? store.list({ type: "question" }) : [];
18452
+ const allActions = store.list({ type: "action" });
18453
+ let features = allFeatures;
18454
+ let epics = allEpics;
18455
+ let tasks = allTasks;
18456
+ if (focusFeature) {
18457
+ features = features.filter((f) => f.frontmatter.id === focusFeature);
18458
+ const featureIds = new Set(features.map((f) => f.frontmatter.id));
18459
+ epics = epics.filter(
18460
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).some((id) => featureIds.has(id))
18461
+ );
18462
+ const epicIds2 = new Set(epics.map((e) => e.frontmatter.id));
18463
+ tasks = tasks.filter(
18464
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).some((id) => epicIds2.has(id))
18465
+ );
18466
+ }
18467
+ const featuresByStatus = {};
18468
+ for (const f of features) {
18469
+ featuresByStatus[f.frontmatter.status] = (featuresByStatus[f.frontmatter.status] ?? 0) + 1;
18470
+ }
18471
+ const epicsByStatus = {};
18472
+ for (const e of epics) {
18473
+ epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
18474
+ }
18475
+ const epicIds = new Set(epics.map((e) => e.frontmatter.id));
18476
+ return {
18477
+ features: features.sort((a, b) => priorityRank2(a.frontmatter.priority) - priorityRank2(b.frontmatter.priority)).map((f) => ({
18478
+ id: f.frontmatter.id,
18479
+ title: f.frontmatter.title,
18480
+ status: f.frontmatter.status,
18481
+ priority: f.frontmatter.priority ?? "medium",
18482
+ content: f.content,
18483
+ linkedEpicCount: epics.filter(
18484
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
18485
+ ).length
18486
+ })),
18487
+ epics: epics.map((e) => ({
18488
+ id: e.frontmatter.id,
18489
+ title: e.frontmatter.title,
18490
+ status: e.frontmatter.status,
18491
+ linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
18492
+ targetDate: e.frontmatter.targetDate ?? null,
18493
+ estimatedEffort: e.frontmatter.estimatedEffort ?? null,
18494
+ content: e.content,
18495
+ linkedTaskCount: tasks.filter(
18496
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
18497
+ ).length
18498
+ })),
18499
+ tasks: tasks.map((t) => ({
18500
+ id: t.frontmatter.id,
18501
+ title: t.frontmatter.title,
18502
+ status: t.frontmatter.status,
18503
+ linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
18504
+ acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
18505
+ technicalNotes: t.frontmatter.technicalNotes ?? null,
18506
+ complexity: t.frontmatter.complexity ?? null,
18507
+ estimatedPoints: t.frontmatter.estimatedPoints ?? null,
18508
+ priority: t.frontmatter.priority ?? null
18509
+ })),
18510
+ decisions: allDecisions.map((d) => ({
18511
+ id: d.frontmatter.id,
18512
+ title: d.frontmatter.title,
18513
+ status: d.frontmatter.status,
18514
+ content: d.content
18515
+ })),
18516
+ questions: allQuestions.map((q) => ({
18517
+ id: q.frontmatter.id,
18518
+ title: q.frontmatter.title,
18519
+ status: q.frontmatter.status,
18520
+ content: q.content
18521
+ })),
18522
+ actions: allActions.filter((a) => {
18523
+ if (!focusFeature) return true;
18524
+ const tags = a.frontmatter.tags ?? [];
18525
+ return tags.some((t) => t.startsWith("epic:") && epicIds.has(t.replace("epic:", "")));
18526
+ }).map((a) => ({
18527
+ id: a.frontmatter.id,
18528
+ title: a.frontmatter.title,
18529
+ status: a.frontmatter.status,
18530
+ owner: a.frontmatter.owner ?? null,
18531
+ priority: a.frontmatter.priority ?? null,
18532
+ dueDate: a.frontmatter.dueDate ?? null
18533
+ })),
18534
+ summary: {
18535
+ totalFeatures: features.length,
18536
+ totalEpics: epics.length,
18537
+ totalTasks: tasks.length,
18538
+ featuresByStatus,
18539
+ epicsByStatus
18540
+ }
18541
+ };
18542
+ }
18543
+ function generateTaskMasterPrd(title, ctx, projectOverview) {
18544
+ const lines = [];
18545
+ lines.push(`# ${title}`);
18546
+ lines.push("");
18547
+ lines.push("## Project Overview");
18548
+ if (projectOverview) {
18549
+ lines.push(projectOverview);
18550
+ } else if (ctx.features.length > 0) {
18551
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
18552
+ }
18553
+ lines.push("");
18554
+ lines.push("## Goals");
18555
+ for (const f of ctx.features) {
18556
+ lines.push(`- **${f.title}** (${f.id}, Priority: ${f.priority}) \u2014 ${f.status}`);
18557
+ }
18558
+ lines.push("");
18559
+ lines.push("## Features and Requirements");
18560
+ lines.push("");
18561
+ for (const feature of ctx.features) {
18562
+ lines.push(`### ${feature.title} (${feature.id}) \u2014 Priority: ${feature.priority}`);
18563
+ lines.push("");
18564
+ if (feature.content) {
18565
+ lines.push(feature.content);
18566
+ lines.push("");
18567
+ }
18568
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
18569
+ if (featureEpics.length > 0) {
18570
+ lines.push("#### User Stories / Epics");
18571
+ lines.push("");
18572
+ for (const epic of featureEpics) {
18573
+ const effort = epic.estimatedEffort ? `, Effort: ${epic.estimatedEffort}` : "";
18574
+ lines.push(`- **${epic.id}: ${epic.title}** \u2014 Status: ${epic.status}${effort}`);
18575
+ if (epic.content) {
18576
+ lines.push(` ${epic.content.split("\n")[0]}`);
18577
+ }
18578
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
18579
+ if (epicTasks.length > 0) {
18580
+ lines.push("");
18581
+ lines.push("#### Implementation Tasks");
18582
+ lines.push("");
18583
+ for (const task of epicTasks) {
18584
+ const complexity = task.complexity ? `, Complexity: ${task.complexity}` : "";
18585
+ const points = task.estimatedPoints != null ? `, Points: ${task.estimatedPoints}` : "";
18586
+ lines.push(`- **${task.id}: ${task.title}**${complexity}${points}`);
18587
+ if (task.acceptanceCriteria) {
18588
+ lines.push(` Acceptance Criteria: ${task.acceptanceCriteria}`);
18589
+ }
18255
18590
  }
18256
18591
  }
18257
- } catch {
18258
18592
  }
18593
+ lines.push("");
18259
18594
  }
18260
- let actions;
18261
- const actionsPath = path7.join(dirPath, "actions.yaml");
18262
- if (fs7.existsSync(actionsPath)) {
18263
- try {
18264
- const actionsRaw = fs7.readFileSync(actionsPath, "utf-8");
18265
- actions = YAML5.parse(actionsRaw);
18266
- } catch {
18595
+ }
18596
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
18597
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
18598
+ const technicalNotes = ctx.tasks.filter((t) => t.technicalNotes).map((t) => `- **${t.id}**: ${t.technicalNotes}`);
18599
+ if (approvedDecisions.length > 0 || openQuestions.length > 0 || technicalNotes.length > 0) {
18600
+ lines.push("## Technical Considerations");
18601
+ lines.push("");
18602
+ if (approvedDecisions.length > 0) {
18603
+ lines.push("### Key Decisions");
18604
+ for (const d of approvedDecisions) {
18605
+ lines.push(`- **${d.id}: ${d.title}** \u2014 ${d.content.split("\n")[0]}`);
18267
18606
  }
18607
+ lines.push("");
18608
+ }
18609
+ if (technicalNotes.length > 0) {
18610
+ lines.push("### Technical Notes");
18611
+ for (const note of technicalNotes) {
18612
+ lines.push(note);
18613
+ }
18614
+ lines.push("");
18615
+ }
18616
+ if (openQuestions.length > 0) {
18617
+ lines.push("### Open Questions");
18618
+ for (const q of openQuestions) {
18619
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
18620
+ }
18621
+ lines.push("");
18268
18622
  }
18269
- return {
18270
- id: data.name,
18271
- name: data.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
18272
- description: data.description,
18273
- version: version2,
18274
- format: "skill-md",
18275
- dirPath,
18276
- personas,
18277
- promptFragments: Object.keys(promptFragments).length > 0 ? promptFragments : void 0,
18278
- actions
18279
- };
18280
- } catch {
18281
- return void 0;
18282
18623
  }
18283
- }
18624
+ lines.push("## Implementation Priorities");
18625
+ lines.push("");
18626
+ let priorityIdx = 1;
18627
+ for (const feature of ctx.features) {
18628
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
18629
+ const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
18630
+ return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
18631
+ });
18632
+ if (featureEpics.length === 0) continue;
18633
+ lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
18634
+ for (const epic of featureEpics) {
18635
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
18636
+ lines.push(` - ${epic.id}: ${epic.title} (${epic.status}) \u2014 ${epicTasks.length} task(s)`);
18637
+ }
18638
+ priorityIdx++;
18639
+ }
18640
+ lines.push("");
18641
+ return lines.join("\n");
18642
+ }
18643
+ function generateClaudeCodePrd(title, ctx, projectOverview) {
18644
+ const lines = [];
18645
+ lines.push(`# ${title}`);
18646
+ lines.push("");
18647
+ lines.push("## Overview");
18648
+ if (projectOverview) {
18649
+ lines.push(projectOverview);
18650
+ } else if (ctx.features.length > 0) {
18651
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
18652
+ }
18653
+ lines.push("");
18654
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
18655
+ if (approvedDecisions.length > 0) {
18656
+ lines.push("## Architecture & Technical Decisions");
18657
+ lines.push("");
18658
+ for (const d of approvedDecisions) {
18659
+ lines.push(`### ${d.id}: ${d.title}`);
18660
+ lines.push(d.content);
18661
+ lines.push("");
18662
+ }
18663
+ }
18664
+ lines.push("## Implementation Plan");
18665
+ lines.push("");
18666
+ const priorityGroups = {};
18667
+ for (const f of ctx.features) {
18668
+ const group = f.priority === "critical" || f.priority === "high" ? "Phase 1: High Priority" : "Phase 2: Medium & Low Priority";
18669
+ if (!priorityGroups[group]) priorityGroups[group] = [];
18670
+ priorityGroups[group].push(f);
18671
+ }
18672
+ for (const [phase, features] of Object.entries(priorityGroups)) {
18673
+ lines.push(`### ${phase}`);
18674
+ lines.push("");
18675
+ for (const feature of features) {
18676
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
18677
+ for (const epic of featureEpics) {
18678
+ lines.push(`- [ ] ${epic.id}: ${epic.title}`);
18679
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
18680
+ for (const task of epicTasks) {
18681
+ const complexity = task.complexity ? `complexity: ${task.complexity}` : "";
18682
+ const points = task.estimatedPoints != null ? `points: ${task.estimatedPoints}` : "";
18683
+ const meta3 = [complexity, points].filter(Boolean).join(", ");
18684
+ lines.push(` - [ ] ${task.id}: ${task.title}${meta3 ? ` (${meta3})` : ""}`);
18685
+ if (task.acceptanceCriteria) {
18686
+ lines.push(` - Acceptance: ${task.acceptanceCriteria}`);
18687
+ }
18688
+ if (task.technicalNotes) {
18689
+ lines.push(` - Notes: ${task.technicalNotes}`);
18690
+ }
18691
+ }
18692
+ }
18693
+ }
18694
+ lines.push("");
18695
+ }
18696
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
18697
+ if (openQuestions.length > 0) {
18698
+ lines.push("## Open Questions");
18699
+ lines.push("");
18700
+ for (const q of openQuestions) {
18701
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
18702
+ }
18703
+ lines.push("");
18704
+ }
18705
+ return lines.join("\n");
18706
+ }
18707
+ function createPrdTools(store) {
18708
+ return [
18709
+ tool21(
18710
+ "gather_prd_context",
18711
+ "Aggregate all governance artifacts (features, epics, tasks, decisions, questions, actions) into structured JSON for PRD generation",
18712
+ {
18713
+ focusFeature: external_exports.string().optional().describe("Filter context to a specific feature ID (e.g. 'F-001')"),
18714
+ includeDecisions: external_exports.boolean().optional().describe("Include decisions in context (default: true)"),
18715
+ includeQuestions: external_exports.boolean().optional().describe("Include questions in context (default: true)")
18716
+ },
18717
+ async (args) => {
18718
+ const ctx = gatherContext(store, args.focusFeature, args.includeDecisions ?? true, args.includeQuestions ?? true);
18719
+ return {
18720
+ content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }]
18721
+ };
18722
+ },
18723
+ { annotations: { readOnlyHint: true } }
18724
+ ),
18725
+ tool21(
18726
+ "generate_prd",
18727
+ "Generate a PRD document from governance artifacts and save it as a PRD-xxx document",
18728
+ {
18729
+ title: external_exports.string().describe("PRD title"),
18730
+ format: external_exports.enum(["taskmaster", "claude-code"]).describe("Output format: 'taskmaster' for Claude TaskMaster parse_prd, 'claude-code' for Claude Code consumption"),
18731
+ projectOverview: external_exports.string().optional().describe("Project overview text (synthesized from features if not provided)"),
18732
+ focusFeature: external_exports.string().optional().describe("Focus on a specific feature ID (e.g. 'F-001')"),
18733
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for the PRD document")
18734
+ },
18735
+ async (args) => {
18736
+ const ctx = gatherContext(store, args.focusFeature);
18737
+ const prdContent = args.format === "taskmaster" ? generateTaskMasterPrd(args.title, ctx, args.projectOverview) : generateClaudeCodePrd(args.title, ctx, args.projectOverview);
18738
+ const frontmatter = {
18739
+ title: args.title,
18740
+ status: "draft",
18741
+ format: args.format
18742
+ };
18743
+ if (args.focusFeature) frontmatter.focusFeature = args.focusFeature;
18744
+ if (args.tags) frontmatter.tags = args.tags;
18745
+ const doc = store.create("prd", frontmatter, prdContent);
18746
+ return {
18747
+ content: [
18748
+ {
18749
+ type: "text",
18750
+ text: `Generated PRD ${doc.frontmatter.id}: "${args.title}" (format: ${args.format}, ${ctx.summary.totalFeatures} features, ${ctx.summary.totalEpics} epics, ${ctx.summary.totalTasks} tasks)`
18751
+ }
18752
+ ]
18753
+ };
18754
+ }
18755
+ ),
18756
+ tool21(
18757
+ "export_prd",
18758
+ "Export a PRD document to a file path for external consumption (e.g. by Claude TaskMaster or Claude Code)",
18759
+ {
18760
+ prdId: external_exports.string().describe("PRD document ID (e.g. 'PRD-001')"),
18761
+ outputPath: external_exports.string().describe("File path to write the PRD content to")
18762
+ },
18763
+ async (args) => {
18764
+ const doc = store.get(args.prdId);
18765
+ if (!doc) {
18766
+ return {
18767
+ content: [{ type: "text", text: `PRD ${args.prdId} not found` }],
18768
+ isError: true
18769
+ };
18770
+ }
18771
+ const outputDir = path7.dirname(args.outputPath);
18772
+ fs7.mkdirSync(outputDir, { recursive: true });
18773
+ fs7.writeFileSync(args.outputPath, doc.content, "utf-8");
18774
+ return {
18775
+ content: [
18776
+ {
18777
+ type: "text",
18778
+ text: `Exported PRD ${args.prdId} to ${args.outputPath}`
18779
+ }
18780
+ ]
18781
+ };
18782
+ }
18783
+ )
18784
+ ];
18785
+ }
18786
+
18787
+ // src/skills/builtin/prd-generator/index.ts
18788
+ var prdGeneratorSkill = {
18789
+ id: "prd-generator",
18790
+ name: "PRD Generator",
18791
+ description: "Generate PRDs from governance artifacts for TaskMaster or Claude Code",
18792
+ version: "1.0.0",
18793
+ format: "builtin-ts",
18794
+ documentTypeRegistrations: [
18795
+ { type: "prd", dirName: "prds", idPrefix: "PRD" }
18796
+ ],
18797
+ tools: (store) => createPrdTools(store),
18798
+ promptFragments: {
18799
+ "tech-lead": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
18800
+
18801
+ **Available tools:**
18802
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, decisions, questions, and actions into structured JSON for analysis
18803
+ - \`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)
18804
+ - \`export_prd\` \u2014 export a PRD document to a file path for external use
18805
+
18806
+ **As Tech Lead, use PRD generation to:**
18807
+ - Create comprehensive PRDs that capture the full governance context
18808
+ - Export TaskMaster-format PRDs for automated task breakdown via \`parse_prd\`
18809
+ - Export Claude Code-format PRDs as implementation plans with checklists
18810
+ - Focus PRDs on specific features using the focusFeature parameter`,
18811
+ "delivery-manager": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
18812
+
18813
+ **Available tools:**
18814
+ - \`gather_prd_context\` \u2014 aggregate all governance artifacts into structured JSON for review
18815
+ - \`generate_prd\` \u2014 generate a formatted PRD document (taskmaster or claude-code format)
18816
+ - \`export_prd\` \u2014 export a PRD to a file path
18817
+
18818
+ **As Delivery Manager, use PRD generation to:**
18819
+ - Generate PRDs for stakeholder communication and project documentation
18820
+ - Review aggregated project context before sprint planning
18821
+ - Export PRDs to share with external teams or tools`,
18822
+ "product-owner": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
18823
+
18824
+ **Available tools:**
18825
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, and decisions into structured JSON
18826
+ - \`generate_prd\` \u2014 generate a formatted PRD document
18827
+ - \`export_prd\` \u2014 export a PRD to a file path
18828
+
18829
+ **As Product Owner, use PRD generation to:**
18830
+ - Generate PRDs that capture feature requirements and priorities
18831
+ - Review the complete governance context for product planning
18832
+ - Export PRDs for stakeholder review and sign-off`
18833
+ }
18834
+ };
18835
+
18836
+ // src/skills/registry.ts
18837
+ var BUILTIN_SKILLS = {
18838
+ "governance-review": governanceReviewSkill,
18839
+ "jira": jiraSkill,
18840
+ "prd-generator": prdGeneratorSkill
18841
+ };
18842
+ function getBuiltinSkillsDir() {
18843
+ const thisFile = fileURLToPath(import.meta.url);
18844
+ return path8.join(path8.dirname(thisFile), "builtin");
18845
+ }
18846
+ function loadSkillFromDirectory(dirPath) {
18847
+ const skillMdPath = path8.join(dirPath, "SKILL.md");
18848
+ if (!fs8.existsSync(skillMdPath)) return void 0;
18849
+ try {
18850
+ const raw = fs8.readFileSync(skillMdPath, "utf-8");
18851
+ const { data, content } = matter2(raw);
18852
+ if (!data.name || !data.description) return void 0;
18853
+ const metadata = data.metadata ?? {};
18854
+ const version2 = metadata.version ?? "1.0.0";
18855
+ const personas = metadata.personas;
18856
+ const promptFragments = {};
18857
+ const wildcardPrompt = content.trim();
18858
+ if (wildcardPrompt) {
18859
+ promptFragments["*"] = wildcardPrompt;
18860
+ }
18861
+ const personasDir = path8.join(dirPath, "personas");
18862
+ if (fs8.existsSync(personasDir)) {
18863
+ try {
18864
+ for (const file2 of fs8.readdirSync(personasDir)) {
18865
+ if (!file2.endsWith(".md")) continue;
18866
+ const personaId = file2.replace(/\.md$/, "");
18867
+ const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
18868
+ if (personaPrompt) {
18869
+ promptFragments[personaId] = personaPrompt;
18870
+ }
18871
+ }
18872
+ } catch {
18873
+ }
18874
+ }
18875
+ let actions;
18876
+ const actionsPath = path8.join(dirPath, "actions.yaml");
18877
+ if (fs8.existsSync(actionsPath)) {
18878
+ try {
18879
+ const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
18880
+ actions = YAML5.parse(actionsRaw);
18881
+ } catch {
18882
+ }
18883
+ }
18884
+ return {
18885
+ id: data.name,
18886
+ name: data.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
18887
+ description: data.description,
18888
+ version: version2,
18889
+ format: "skill-md",
18890
+ dirPath,
18891
+ personas,
18892
+ promptFragments: Object.keys(promptFragments).length > 0 ? promptFragments : void 0,
18893
+ actions
18894
+ };
18895
+ } catch {
18896
+ return void 0;
18897
+ }
18898
+ }
18284
18899
  function loadAllSkills(marvinDir) {
18285
18900
  const skills = /* @__PURE__ */ new Map();
18286
18901
  for (const [id, skill] of Object.entries(BUILTIN_SKILLS)) {
@@ -18288,10 +18903,10 @@ function loadAllSkills(marvinDir) {
18288
18903
  }
18289
18904
  try {
18290
18905
  const builtinDir = getBuiltinSkillsDir();
18291
- if (fs7.existsSync(builtinDir)) {
18292
- for (const entry of fs7.readdirSync(builtinDir)) {
18293
- const entryPath = path7.join(builtinDir, entry);
18294
- if (!fs7.statSync(entryPath).isDirectory()) continue;
18906
+ if (fs8.existsSync(builtinDir)) {
18907
+ for (const entry of fs8.readdirSync(builtinDir)) {
18908
+ const entryPath = path8.join(builtinDir, entry);
18909
+ if (!fs8.statSync(entryPath).isDirectory()) continue;
18295
18910
  if (skills.has(entry)) continue;
18296
18911
  const skill = loadSkillFromDirectory(entryPath);
18297
18912
  if (skill) skills.set(skill.id, skill);
@@ -18300,18 +18915,18 @@ function loadAllSkills(marvinDir) {
18300
18915
  } catch {
18301
18916
  }
18302
18917
  if (marvinDir) {
18303
- const skillsDir = path7.join(marvinDir, "skills");
18304
- if (fs7.existsSync(skillsDir)) {
18918
+ const skillsDir = path8.join(marvinDir, "skills");
18919
+ if (fs8.existsSync(skillsDir)) {
18305
18920
  let entries;
18306
18921
  try {
18307
- entries = fs7.readdirSync(skillsDir);
18922
+ entries = fs8.readdirSync(skillsDir);
18308
18923
  } catch {
18309
18924
  entries = [];
18310
18925
  }
18311
18926
  for (const entry of entries) {
18312
- const entryPath = path7.join(skillsDir, entry);
18927
+ const entryPath = path8.join(skillsDir, entry);
18313
18928
  try {
18314
- if (fs7.statSync(entryPath).isDirectory()) {
18929
+ if (fs8.statSync(entryPath).isDirectory()) {
18315
18930
  const skill = loadSkillFromDirectory(entryPath);
18316
18931
  if (skill) skills.set(skill.id, skill);
18317
18932
  continue;
@@ -18321,7 +18936,7 @@ function loadAllSkills(marvinDir) {
18321
18936
  }
18322
18937
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
18323
18938
  try {
18324
- const raw = fs7.readFileSync(entryPath, "utf-8");
18939
+ const raw = fs8.readFileSync(entryPath, "utf-8");
18325
18940
  const parsed = YAML5.parse(raw);
18326
18941
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
18327
18942
  const skill = {
@@ -18389,7 +19004,7 @@ ${fragment}`);
18389
19004
  }
18390
19005
 
18391
19006
  // src/skills/action-tools.ts
18392
- import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
19007
+ import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
18393
19008
 
18394
19009
  // src/skills/action-runner.ts
18395
19010
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -18401,7 +19016,7 @@ import {
18401
19016
 
18402
19017
  // src/agent/tools/web.ts
18403
19018
  import * as http2 from "http";
18404
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19019
+ import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
18405
19020
 
18406
19021
  // src/web/data.ts
18407
19022
  function getOverviewData(store) {
@@ -18637,6 +19252,7 @@ function inline(text) {
18637
19252
  function layout(opts, body) {
18638
19253
  const topItems = [
18639
19254
  { href: "/", label: "Overview" },
19255
+ { href: "/timeline", label: "Timeline" },
18640
19256
  { href: "/board", label: "Board" },
18641
19257
  { href: "/gar", label: "GAR Report" },
18642
19258
  { href: "/health", label: "Health" }
@@ -18673,7 +19289,7 @@ function layout(opts, body) {
18673
19289
  ${groupsHtml}
18674
19290
  </nav>
18675
19291
  </aside>
18676
- <main class="main">
19292
+ <main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
18677
19293
  <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18678
19294
  <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>
18679
19295
  <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>
@@ -18682,7 +19298,36 @@ function layout(opts, body) {
18682
19298
  </main>
18683
19299
  </div>
18684
19300
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18685
- <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
19301
+ <script>mermaid.initialize({
19302
+ startOnLoad: true,
19303
+ theme: 'dark',
19304
+ themeVariables: {
19305
+ background: '#1a1d27',
19306
+ primaryColor: '#2a2e3a',
19307
+ sectionBkgColor: '#1a1d27',
19308
+ sectionBkgColor2: '#222632',
19309
+ altSectionBkgColor: '#222632',
19310
+ gridColor: '#2a2e3a',
19311
+ taskBorderColor: '#475569',
19312
+ doneTaskBkgColor: '#065f46',
19313
+ doneTaskBorderColor: '#34d399',
19314
+ activeTaskBkgColor: '#78350f',
19315
+ activeTaskBorderColor: '#fbbf24',
19316
+ taskTextColor: '#e1e4ea',
19317
+ sectionBkgColor: '#1a1d27',
19318
+ pie1: '#34d399',
19319
+ pie2: '#475569',
19320
+ pie3: '#fbbf24',
19321
+ pie4: '#f87171',
19322
+ pie5: '#6c8cff',
19323
+ pie6: '#a78bfa',
19324
+ pie7: '#f472b6',
19325
+ pieTitleTextColor: '#e1e4ea',
19326
+ pieSectionTextColor: '#e1e4ea',
19327
+ pieLegendTextColor: '#e1e4ea',
19328
+ pieStrokeColor: '#1a1d27'
19329
+ }
19330
+ });</script>
18686
19331
  </body>
18687
19332
  </html>`;
18688
19333
  }
@@ -18929,6 +19574,10 @@ a:hover { text-decoration: underline; }
18929
19574
  /* Table */
18930
19575
  .table-wrap {
18931
19576
  overflow-x: auto;
19577
+ overflow-y: auto;
19578
+ max-height: calc(100vh - 280px);
19579
+ border: 1px solid var(--border);
19580
+ border-radius: var(--radius);
18932
19581
  }
18933
19582
 
18934
19583
  table {
@@ -18944,6 +19593,10 @@ th {
18944
19593
  letter-spacing: 0.05em;
18945
19594
  color: var(--text-dim);
18946
19595
  border-bottom: 1px solid var(--border);
19596
+ position: sticky;
19597
+ top: 0;
19598
+ background: var(--bg-card);
19599
+ z-index: 1;
18947
19600
  }
18948
19601
 
18949
19602
  td {
@@ -18991,6 +19644,8 @@ tr:hover td {
18991
19644
  border: 1px solid var(--border);
18992
19645
  border-radius: var(--radius);
18993
19646
  padding: 1.25rem;
19647
+ display: flex;
19648
+ flex-direction: column;
18994
19649
  }
18995
19650
 
18996
19651
  .gar-area .area-header {
@@ -19021,6 +19676,9 @@ tr:hover td {
19021
19676
  .gar-area ul {
19022
19677
  list-style: none;
19023
19678
  font-size: 0.8rem;
19679
+ max-height: 200px;
19680
+ overflow-y: auto;
19681
+ scrollbar-width: thin;
19024
19682
  }
19025
19683
 
19026
19684
  .gar-area li {
@@ -19043,13 +19701,14 @@ tr:hover td {
19043
19701
  display: flex;
19044
19702
  gap: 1rem;
19045
19703
  overflow-x: auto;
19704
+ scrollbar-width: thin;
19046
19705
  padding-bottom: 1rem;
19047
19706
  }
19048
19707
 
19049
19708
  .board-column {
19050
19709
  min-width: 240px;
19051
19710
  max-width: 300px;
19052
- flex: 1;
19711
+ flex: 0 0 auto;
19053
19712
  }
19054
19713
 
19055
19714
  .board-column-header {
@@ -19062,6 +19721,7 @@ tr:hover td {
19062
19721
  margin-bottom: 0.5rem;
19063
19722
  display: flex;
19064
19723
  justify-content: space-between;
19724
+ flex-shrink: 0;
19065
19725
  }
19066
19726
 
19067
19727
  .board-column-header .count {
@@ -19243,6 +19903,241 @@ tr:hover td {
19243
19903
  .mermaid-row .mermaid-container {
19244
19904
  margin: 0;
19245
19905
  }
19906
+
19907
+ /* Three-column artifact flow */
19908
+ .flow-diagram {
19909
+ background: var(--bg-card);
19910
+ border: 1px solid var(--border);
19911
+ border-radius: var(--radius);
19912
+ padding: 1.25rem;
19913
+ position: relative;
19914
+ overflow-x: auto;
19915
+ }
19916
+
19917
+ .flow-lines {
19918
+ position: absolute;
19919
+ top: 0;
19920
+ left: 0;
19921
+ pointer-events: none;
19922
+ }
19923
+
19924
+ .flow-columns {
19925
+ display: flex;
19926
+ gap: 3rem;
19927
+ position: relative;
19928
+ min-width: 600px;
19929
+ }
19930
+
19931
+ .flow-column {
19932
+ flex: 1;
19933
+ min-width: 0;
19934
+ display: flex;
19935
+ flex-direction: column;
19936
+ gap: 0.5rem;
19937
+ }
19938
+
19939
+ .flow-column-header {
19940
+ font-size: 0.7rem;
19941
+ text-transform: uppercase;
19942
+ letter-spacing: 0.06em;
19943
+ color: var(--text-dim);
19944
+ font-weight: 600;
19945
+ padding-bottom: 0.4rem;
19946
+ border-bottom: 1px solid var(--border);
19947
+ margin-bottom: 0.25rem;
19948
+ }
19949
+
19950
+ .flow-node {
19951
+ padding: 0.5rem 0.65rem;
19952
+ border-radius: 6px;
19953
+ border-left: 3px solid var(--border);
19954
+ background: var(--bg);
19955
+ transition: border-color 0.15s, background 0.15s;
19956
+ }
19957
+
19958
+ .flow-node:hover {
19959
+ background: var(--bg-hover);
19960
+ }
19961
+
19962
+ .flow-node-id {
19963
+ display: inline-block;
19964
+ font-family: var(--mono);
19965
+ font-size: 0.65rem;
19966
+ color: var(--accent);
19967
+ margin-bottom: 0.15rem;
19968
+ text-decoration: none;
19969
+ }
19970
+
19971
+ .flow-node-id:hover {
19972
+ text-decoration: underline;
19973
+ }
19974
+
19975
+ .flow-node-title {
19976
+ display: block;
19977
+ font-size: 0.8rem;
19978
+ }
19979
+
19980
+ .flow-done { border-left-color: var(--green); }
19981
+ .flow-active { border-left-color: var(--amber); }
19982
+ .flow-blocked { border-left-color: var(--red); }
19983
+ .flow-default { border-left-color: var(--accent-dim); }
19984
+
19985
+ .flow-node { cursor: pointer; transition: opacity 0.2s, border-color 0.15s, background 0.15s; }
19986
+ .flow-dim { opacity: 0.2; }
19987
+ .flow-lit { background: var(--bg-hover); }
19988
+ .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
19989
+ .flow-line-dim { opacity: 0.08; }
19990
+
19991
+ /* Gantt truncation note */
19992
+ .mermaid-note {
19993
+ font-size: 0.75rem;
19994
+ color: var(--text-dim);
19995
+ text-align: right;
19996
+ margin-bottom: 0.5rem;
19997
+ }
19998
+
19999
+ /* HTML Gantt chart */
20000
+ .gantt {
20001
+ background: var(--bg-card);
20002
+ border: 1px solid var(--border);
20003
+ border-radius: var(--radius);
20004
+ padding: 1.25rem 1.25rem 1.25rem 0;
20005
+ position: relative;
20006
+ overflow-x: auto;
20007
+ }
20008
+
20009
+ .gantt-chart {
20010
+ min-width: 600px;
20011
+ }
20012
+
20013
+ .gantt-overlay {
20014
+ position: absolute;
20015
+ top: 0;
20016
+ left: 0;
20017
+ right: 0;
20018
+ bottom: 0;
20019
+ pointer-events: none;
20020
+ display: flex;
20021
+ }
20022
+
20023
+ .gantt-header,
20024
+ .gantt-section-row,
20025
+ .gantt-row,
20026
+ .gantt-overlay {
20027
+ display: flex;
20028
+ align-items: center;
20029
+ }
20030
+
20031
+ .gantt-label {
20032
+ width: 200px;
20033
+ min-width: 200px;
20034
+ padding: 0.3rem 0.75rem;
20035
+ font-size: 0.8rem;
20036
+ color: var(--text-dim);
20037
+ text-align: right;
20038
+ white-space: nowrap;
20039
+ overflow: hidden;
20040
+ text-overflow: ellipsis;
20041
+ }
20042
+
20043
+ .gantt-section-label {
20044
+ font-weight: 600;
20045
+ color: var(--text);
20046
+ font-size: 0.75rem;
20047
+ text-transform: uppercase;
20048
+ letter-spacing: 0.03em;
20049
+ padding-top: 0.6rem;
20050
+ }
20051
+
20052
+ .gantt-track {
20053
+ flex: 1;
20054
+ position: relative;
20055
+ height: 28px;
20056
+ min-width: 0;
20057
+ }
20058
+
20059
+ .gantt-section-row .gantt-track {
20060
+ height: 20px;
20061
+ }
20062
+
20063
+ .gantt-section-bg {
20064
+ position: absolute;
20065
+ top: 0;
20066
+ bottom: 0;
20067
+ background: var(--bg-hover);
20068
+ border-radius: 3px;
20069
+ opacity: 0.4;
20070
+ }
20071
+
20072
+ .gantt-bar {
20073
+ position: absolute;
20074
+ top: 4px;
20075
+ bottom: 4px;
20076
+ border-radius: 4px;
20077
+ min-width: 6px;
20078
+ transition: opacity 0.15s;
20079
+ }
20080
+
20081
+ .gantt-bar:hover {
20082
+ opacity: 0.85;
20083
+ }
20084
+
20085
+ .gantt-bar-done {
20086
+ background: var(--green);
20087
+ }
20088
+
20089
+ .gantt-bar-active {
20090
+ background: var(--amber);
20091
+ }
20092
+
20093
+ .gantt-bar-blocked {
20094
+ background: var(--red);
20095
+ }
20096
+
20097
+ .gantt-bar-default {
20098
+ background: var(--accent-dim);
20099
+ }
20100
+
20101
+ .gantt-dates {
20102
+ height: 24px;
20103
+ border-bottom: 1px solid var(--border);
20104
+ margin-bottom: 0.25rem;
20105
+ }
20106
+
20107
+ .gantt-marker {
20108
+ position: absolute;
20109
+ top: 0;
20110
+ bottom: 0;
20111
+ border-left: 1px solid var(--border);
20112
+ }
20113
+
20114
+ .gantt-marker span {
20115
+ position: absolute;
20116
+ top: 2px;
20117
+ left: 6px;
20118
+ font-size: 0.65rem;
20119
+ color: var(--text-dim);
20120
+ white-space: nowrap;
20121
+ }
20122
+
20123
+ .gantt-today {
20124
+ position: absolute;
20125
+ top: 0;
20126
+ bottom: 0;
20127
+ width: 2px;
20128
+ background: var(--red);
20129
+ opacity: 0.7;
20130
+ }
20131
+
20132
+ /* Pie chart color overrides */
20133
+ .mermaid-container .pieCircle {
20134
+ stroke: var(--bg-card);
20135
+ }
20136
+
20137
+ .mermaid-container text.slice {
20138
+ fill: var(--bg) !important;
20139
+ font-weight: 600;
20140
+ }
19246
20141
  `;
19247
20142
  }
19248
20143
 
@@ -19251,98 +20146,275 @@ function sanitize(text, maxLen = 40) {
19251
20146
  const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
19252
20147
  return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19253
20148
  }
19254
- function mermaidBlock(definition) {
19255
- return `<div class="mermaid-container"><pre class="mermaid">
20149
+ function mermaidBlock(definition, extraClass) {
20150
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
20151
+ return `<div class="${cls}"><pre class="mermaid">
19256
20152
  ${definition}
19257
20153
  </pre></div>`;
19258
20154
  }
19259
20155
  function placeholder(message) {
19260
20156
  return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19261
20157
  }
19262
- function buildTimelineGantt(data) {
19263
- const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
20158
+ function toMs(date5) {
20159
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
20160
+ }
20161
+ function fmtDate(ms) {
20162
+ const d = new Date(ms);
20163
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20164
+ return `${months[d.getMonth()]} ${d.getDate()}`;
20165
+ }
20166
+ function buildTimelineGantt(data, maxSprints = 6) {
20167
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
19264
20168
  if (sprintsWithDates.length === 0) {
19265
20169
  return placeholder("No timeline data available \u2014 sprints need start and end dates.");
19266
20170
  }
20171
+ const truncated = sprintsWithDates.length > maxSprints;
20172
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
20173
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
19267
20174
  const epicMap = new Map(data.epics.map((e) => [e.id, e]));
19268
- const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
19269
- for (const sprint of sprintsWithDates) {
19270
- lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
20175
+ const allStarts = visibleSprints.map((s) => toMs(s.startDate));
20176
+ const allEnds = visibleSprints.map((s) => toMs(s.endDate));
20177
+ const timelineStart = Math.min(...allStarts);
20178
+ const timelineEnd = Math.max(...allEnds);
20179
+ const span = timelineEnd - timelineStart || 1;
20180
+ const pct = (ms) => (ms - timelineStart) / span * 100;
20181
+ const DAY = 864e5;
20182
+ const markers = [];
20183
+ let tick = timelineStart;
20184
+ const startDay = new Date(tick).getDay();
20185
+ tick += (8 - startDay) % 7 * DAY;
20186
+ while (tick <= timelineEnd) {
20187
+ const left = pct(tick);
20188
+ markers.push(
20189
+ `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
20190
+ );
20191
+ tick += 7 * DAY;
20192
+ }
20193
+ const now = Date.now();
20194
+ let todayMarker = "";
20195
+ if (now >= timelineStart && now <= timelineEnd) {
20196
+ todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
20197
+ }
20198
+ const rows = [];
20199
+ for (const sprint of visibleSprints) {
20200
+ const sStart = toMs(sprint.startDate);
20201
+ const sEnd = toMs(sprint.endDate);
20202
+ rows.push(`<div class="gantt-section-row">
20203
+ <div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
20204
+ <div class="gantt-track">
20205
+ <div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
20206
+ </div>
20207
+ </div>`);
19271
20208
  const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
19272
- if (linked.length === 0) {
19273
- lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
19274
- } else {
19275
- for (const epic of linked) {
19276
- const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
19277
- lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
19278
- }
20209
+ const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
20210
+ for (const item of items) {
20211
+ 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";
20212
+ const left = pct(sStart).toFixed(2);
20213
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
20214
+ rows.push(`<div class="gantt-row">
20215
+ <div class="gantt-label">${item.label}</div>
20216
+ <div class="gantt-track">
20217
+ <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
20218
+ </div>
20219
+ </div>`);
19279
20220
  }
19280
20221
  }
19281
- return mermaidBlock(lines.join("\n"));
20222
+ const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
20223
+ return `${note}
20224
+ <div class="gantt">
20225
+ <div class="gantt-chart">
20226
+ <div class="gantt-header">
20227
+ <div class="gantt-label"></div>
20228
+ <div class="gantt-track gantt-dates">${markers.join("")}</div>
20229
+ </div>
20230
+ ${rows.join("\n")}
20231
+ </div>
20232
+ <div class="gantt-overlay">
20233
+ <div class="gantt-label"></div>
20234
+ <div class="gantt-track">${todayMarker}</div>
20235
+ </div>
20236
+ </div>`;
20237
+ }
20238
+ function statusClass(status) {
20239
+ const s = status.toLowerCase();
20240
+ if (s === "done" || s === "completed") return "flow-done";
20241
+ if (s === "in-progress" || s === "active") return "flow-active";
20242
+ if (s === "blocked") return "flow-blocked";
20243
+ return "flow-default";
19282
20244
  }
19283
20245
  function buildArtifactFlowchart(data) {
19284
20246
  if (data.features.length === 0 && data.epics.length === 0) {
19285
20247
  return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
19286
20248
  }
19287
- const lines = ["graph TD"];
19288
- lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
19289
- lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
19290
- lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
19291
- lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
19292
- const nodeIds = /* @__PURE__ */ new Set();
20249
+ const edges = [];
20250
+ const epicsByFeature = /* @__PURE__ */ new Map();
19293
20251
  for (const epic of data.epics) {
19294
- for (const featureId of epic.linkedFeature) {
19295
- const feature = data.features.find((f) => f.id === featureId);
19296
- if (feature) {
19297
- const fNode = feature.id.replace(/-/g, "_");
19298
- const eNode = epic.id.replace(/-/g, "_");
19299
- if (!nodeIds.has(fNode)) {
19300
- lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
19301
- nodeIds.add(fNode);
19302
- }
19303
- if (!nodeIds.has(eNode)) {
19304
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19305
- nodeIds.add(eNode);
19306
- }
19307
- lines.push(` ${fNode} --> ${eNode}`);
19308
- }
20252
+ for (const fid of epic.linkedFeature) {
20253
+ if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
20254
+ epicsByFeature.get(fid).push(epic.id);
20255
+ edges.push({ from: fid, to: epic.id });
19309
20256
  }
19310
20257
  }
20258
+ const sprintsByEpic = /* @__PURE__ */ new Map();
19311
20259
  for (const sprint of data.sprints) {
19312
- const sNode = sprint.id.replace(/-/g, "_");
19313
- for (const epicId of sprint.linkedEpics) {
19314
- const epic = data.epics.find((e) => e.id === epicId);
19315
- if (epic) {
19316
- const eNode = epic.id.replace(/-/g, "_");
19317
- if (!nodeIds.has(eNode)) {
19318
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19319
- nodeIds.add(eNode);
19320
- }
19321
- if (!nodeIds.has(sNode)) {
19322
- lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
19323
- nodeIds.add(sNode);
19324
- }
19325
- lines.push(` ${eNode} --> ${sNode}`);
19326
- }
20260
+ for (const eid of sprint.linkedEpics) {
20261
+ if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
20262
+ sprintsByEpic.get(eid).push(sprint.id);
20263
+ edges.push({ from: eid, to: sprint.id });
19327
20264
  }
19328
20265
  }
19329
- if (nodeIds.size === 0) {
20266
+ const connectedFeatureIds = new Set(epicsByFeature.keys());
20267
+ const connectedEpicIds = /* @__PURE__ */ new Set();
20268
+ for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
20269
+ for (const ids of sprintsByEpic.values()) ids.forEach(() => {
20270
+ });
20271
+ for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
20272
+ const connectedSprintIds = /* @__PURE__ */ new Set();
20273
+ for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
20274
+ const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
20275
+ const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
20276
+ const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
20277
+ if (features.length === 0 && epics.length === 0) {
19330
20278
  return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
19331
20279
  }
19332
- const allItems = [
19333
- ...data.features.map((f) => ({ id: f.id, status: f.status })),
19334
- ...data.epics.map((e) => ({ id: e.id, status: e.status })),
19335
- ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
19336
- ];
19337
- for (const item of allItems) {
19338
- const node = item.id.replace(/-/g, "_");
19339
- if (!nodeIds.has(node)) continue;
19340
- const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
19341
- if (cls) {
19342
- lines.push(` class ${node} ${cls}`);
19343
- }
19344
- }
19345
- return mermaidBlock(lines.join("\n"));
20280
+ const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
20281
+ <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
20282
+ <span class="flow-node-title">${sanitize(title, 35)}</span>
20283
+ </div>`;
20284
+ const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
20285
+ const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
20286
+ const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
20287
+ const edgesJson = JSON.stringify(edges);
20288
+ return `
20289
+ <div class="flow-diagram" id="flow-diagram">
20290
+ <svg class="flow-lines" id="flow-lines"></svg>
20291
+ <div class="flow-columns">
20292
+ <div class="flow-column">
20293
+ <div class="flow-column-header">Features</div>
20294
+ ${featuresHtml}
20295
+ </div>
20296
+ <div class="flow-column">
20297
+ <div class="flow-column-header">Epics</div>
20298
+ ${epicsHtml}
20299
+ </div>
20300
+ <div class="flow-column">
20301
+ <div class="flow-column-header">Sprints</div>
20302
+ ${sprintsHtml}
20303
+ </div>
20304
+ </div>
20305
+ </div>
20306
+ <script>
20307
+ (function() {
20308
+ var edges = ${edgesJson};
20309
+ var container = document.getElementById('flow-diagram');
20310
+ var svg = document.getElementById('flow-lines');
20311
+ if (!container || !svg) return;
20312
+
20313
+ // Build adjacency map (bidirectional) for traversal
20314
+ var adj = {};
20315
+ edges.forEach(function(e) {
20316
+ if (!adj[e.from]) adj[e.from] = [];
20317
+ if (!adj[e.to]) adj[e.to] = [];
20318
+ adj[e.from].push(e.to);
20319
+ adj[e.to].push(e.from);
20320
+ });
20321
+
20322
+ function drawLines() {
20323
+ var rect = container.getBoundingClientRect();
20324
+ svg.setAttribute('width', rect.width);
20325
+ svg.setAttribute('height', rect.height);
20326
+ svg.innerHTML = '';
20327
+
20328
+ edges.forEach(function(edge) {
20329
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
20330
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
20331
+ if (!fromEl || !toEl) return;
20332
+
20333
+ var fr = fromEl.getBoundingClientRect();
20334
+ var tr = toEl.getBoundingClientRect();
20335
+ var x1 = fr.right - rect.left;
20336
+ var y1 = fr.top + fr.height / 2 - rect.top;
20337
+ var x2 = tr.left - rect.left;
20338
+ var y2 = tr.top + tr.height / 2 - rect.top;
20339
+ var mx = (x1 + x2) / 2;
20340
+
20341
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
20342
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
20343
+ path.setAttribute('fill', 'none');
20344
+ path.setAttribute('stroke', '#2a2e3a');
20345
+ path.setAttribute('stroke-width', '1.5');
20346
+ path.dataset.from = edge.from;
20347
+ path.dataset.to = edge.to;
20348
+ svg.appendChild(path);
20349
+ });
20350
+ }
20351
+
20352
+ // Find all nodes reachable from a starting node
20353
+ function findConnected(startId) {
20354
+ var visited = {};
20355
+ var queue = [startId];
20356
+ visited[startId] = true;
20357
+ while (queue.length) {
20358
+ var id = queue.shift();
20359
+ (adj[id] || []).forEach(function(neighbor) {
20360
+ if (!visited[neighbor]) {
20361
+ visited[neighbor] = true;
20362
+ queue.push(neighbor);
20363
+ }
20364
+ });
20365
+ }
20366
+ return visited;
20367
+ }
20368
+
20369
+ function highlight(hoveredId) {
20370
+ var connected = findConnected(hoveredId);
20371
+ container.querySelectorAll('.flow-node').forEach(function(n) {
20372
+ if (connected[n.dataset.flowId]) {
20373
+ n.classList.add('flow-lit');
20374
+ n.classList.remove('flow-dim');
20375
+ } else {
20376
+ n.classList.add('flow-dim');
20377
+ n.classList.remove('flow-lit');
20378
+ }
20379
+ });
20380
+ svg.querySelectorAll('path').forEach(function(p) {
20381
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
20382
+ p.classList.add('flow-line-lit');
20383
+ p.classList.remove('flow-line-dim');
20384
+ } else {
20385
+ p.classList.add('flow-line-dim');
20386
+ p.classList.remove('flow-line-lit');
20387
+ }
20388
+ });
20389
+ }
20390
+
20391
+ function clearHighlight() {
20392
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
20393
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
20394
+ }
20395
+
20396
+ var activeId = null;
20397
+ container.addEventListener('click', function(e) {
20398
+ // Let the ID link navigate normally
20399
+ if (e.target.closest('a')) return;
20400
+
20401
+ var node = e.target.closest('.flow-node');
20402
+ var clickedId = node ? node.dataset.flowId : null;
20403
+
20404
+ if (!clickedId || clickedId === activeId) {
20405
+ activeId = null;
20406
+ clearHighlight();
20407
+ return;
20408
+ }
20409
+
20410
+ activeId = clickedId;
20411
+ highlight(clickedId);
20412
+ });
20413
+
20414
+ requestAnimationFrame(function() { setTimeout(drawLines, 100); });
20415
+ window.addEventListener('resize', drawLines);
20416
+ })();
20417
+ </script>`;
19346
20418
  }
19347
20419
  function buildStatusPie(title, counts) {
19348
20420
  const entries = Object.entries(counts).filter(([, v]) => v > 0);
@@ -19422,8 +20494,7 @@ function overviewPage(data, diagrams, navGroups) {
19422
20494
  ${groupSections}
19423
20495
  ${ungroupedSection}
19424
20496
 
19425
- <div class="section-title">Project Timeline</div>
19426
- ${buildTimelineGantt(diagrams)}
20497
+ <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
19427
20498
 
19428
20499
  <div class="section-title">Artifact Relationships</div>
19429
20500
  ${buildArtifactFlowchart(diagrams)}
@@ -19678,6 +20749,7 @@ function boardPage(data) {
19678
20749
  <span>${escapeHtml(col.status)}</span>
19679
20750
  <span class="count">${col.docs.length}</span>
19680
20751
  </div>
20752
+ <div class="board-column-cards">
19681
20753
  ${col.docs.map(
19682
20754
  (doc) => `
19683
20755
  <div class="board-card">
@@ -19688,6 +20760,7 @@ function boardPage(data) {
19688
20760
  </a>
19689
20761
  </div>`
19690
20762
  ).join("\n")}
20763
+ </div>
19691
20764
  </div>`
19692
20765
  ).join("\n");
19693
20766
  return `
@@ -19713,6 +20786,18 @@ function boardPage(data) {
19713
20786
  `;
19714
20787
  }
19715
20788
 
20789
+ // src/web/templates/pages/timeline.ts
20790
+ function timelinePage(diagrams) {
20791
+ return `
20792
+ <div class="page-header">
20793
+ <h2>Project Timeline</h2>
20794
+ <div class="subtitle">Sprint schedule with linked epics</div>
20795
+ </div>
20796
+
20797
+ ${buildTimelineGantt(diagrams)}
20798
+ `;
20799
+ }
20800
+
19716
20801
  // src/web/router.ts
19717
20802
  function handleRequest(req, res, store, projectName, navGroups) {
19718
20803
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -19734,6 +20819,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19734
20819
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
19735
20820
  return;
19736
20821
  }
20822
+ if (pathname === "/timeline") {
20823
+ const diagrams = getDiagramData(store);
20824
+ const body = timelinePage(diagrams);
20825
+ respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, mainClass: "expanded" }, body));
20826
+ return;
20827
+ }
19737
20828
  if (pathname === "/gar") {
19738
20829
  const report = getGarData(store, projectName);
19739
20830
  const body = garPage(report);
@@ -19838,7 +20929,7 @@ function openBrowser(url2) {
19838
20929
  var runningServer = null;
19839
20930
  function createWebTools(store, projectName, navGroups) {
19840
20931
  return [
19841
- tool20(
20932
+ tool22(
19842
20933
  "start_web_dashboard",
19843
20934
  "Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
19844
20935
  {
@@ -19870,7 +20961,7 @@ function createWebTools(store, projectName, navGroups) {
19870
20961
  };
19871
20962
  }
19872
20963
  ),
19873
- tool20(
20964
+ tool22(
19874
20965
  "stop_web_dashboard",
19875
20966
  "Stop the running Marvin web dashboard.",
19876
20967
  {},
@@ -19890,7 +20981,7 @@ function createWebTools(store, projectName, navGroups) {
19890
20981
  };
19891
20982
  }
19892
20983
  ),
19893
- tool20(
20984
+ tool22(
19894
20985
  "get_web_dashboard_urls",
19895
20986
  "Get all available dashboard page URLs. The dashboard must be running.",
19896
20987
  {},
@@ -19916,7 +21007,7 @@ function createWebTools(store, projectName, navGroups) {
19916
21007
  },
19917
21008
  { annotations: { readOnlyHint: true } }
19918
21009
  ),
19919
- tool20(
21010
+ tool22(
19920
21011
  "get_dashboard_overview",
19921
21012
  "Get the project overview data: document type counts and recent activity. Works without the web server running.",
19922
21013
  {},
@@ -19938,7 +21029,7 @@ function createWebTools(store, projectName, navGroups) {
19938
21029
  },
19939
21030
  { annotations: { readOnlyHint: true } }
19940
21031
  ),
19941
- tool20(
21032
+ tool22(
19942
21033
  "get_dashboard_gar",
19943
21034
  "Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
19944
21035
  {},
@@ -19950,7 +21041,7 @@ function createWebTools(store, projectName, navGroups) {
19950
21041
  },
19951
21042
  { annotations: { readOnlyHint: true } }
19952
21043
  ),
19953
- tool20(
21044
+ tool22(
19954
21045
  "get_dashboard_board",
19955
21046
  "Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
19956
21047
  {
@@ -20064,7 +21155,7 @@ function createSkillActionTools(skills, context) {
20064
21155
  if (!skill.actions) continue;
20065
21156
  for (const action of skill.actions) {
20066
21157
  tools.push(
20067
- tool21(
21158
+ tool23(
20068
21159
  `${skill.id}__${action.id}`,
20069
21160
  action.description,
20070
21161
  {
@@ -20155,7 +21246,7 @@ var deliveryManager = {
20155
21246
  "Epic scheduling and tracking",
20156
21247
  "Sprint planning and tracking"
20157
21248
  ],
20158
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "sprint"],
21249
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
20159
21250
  contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
20160
21251
  };
20161
21252
 
@@ -20193,9 +21284,10 @@ var techLead = {
20193
21284
  "Implementation guidance",
20194
21285
  "Non-functional requirements",
20195
21286
  "Epic creation and scoping",
21287
+ "Task creation and breakdown",
20196
21288
  "Sprint scoping and technical execution"
20197
21289
  ],
20198
- documentTypes: ["decision", "action", "question", "epic", "sprint"],
21290
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
20199
21291
  contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
20200
21292
  };
20201
21293
 
@@ -20293,10 +21385,10 @@ ${lines.join("\n\n")}`;
20293
21385
  }
20294
21386
 
20295
21387
  // src/mcp/persona-tools.ts
20296
- import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
21388
+ import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
20297
21389
  function createPersonaTools(ctx, marvinDir) {
20298
21390
  return [
20299
- tool22(
21391
+ tool24(
20300
21392
  "set_persona",
20301
21393
  "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.",
20302
21394
  {
@@ -20326,7 +21418,7 @@ ${summaries}`
20326
21418
  };
20327
21419
  }
20328
21420
  ),
20329
- tool22(
21421
+ tool24(
20330
21422
  "get_persona_guidance",
20331
21423
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
20332
21424
  {
@@ -20428,8 +21520,8 @@ function collectTools(marvinDir) {
20428
21520
  const plugin = resolvePlugin(config2.methodology);
20429
21521
  const registrations = plugin?.documentTypeRegistrations ?? [];
20430
21522
  const store = new DocumentStore(marvinDir, registrations);
20431
- const sourcesDir = path8.join(marvinDir, "sources");
20432
- const hasSourcesDir = fs8.existsSync(sourcesDir);
21523
+ const sourcesDir = path9.join(marvinDir, "sources");
21524
+ const hasSourcesDir = fs9.existsSync(sourcesDir);
20433
21525
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20434
21526
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20435
21527
  const sessionStore = new SessionStore(marvinDir);
@@ -20437,7 +21529,7 @@ function collectTools(marvinDir) {
20437
21529
  const allSkillIds = [...allSkills.keys()];
20438
21530
  const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
20439
21531
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
20440
- const projectRoot = path8.dirname(marvinDir);
21532
+ const projectRoot = path9.dirname(marvinDir);
20441
21533
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
20442
21534
  const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
20443
21535
  const navGroups = buildNavGroups({
@@ -20510,9 +21602,9 @@ function parseProjectDir(argv) {
20510
21602
  }
20511
21603
  async function main() {
20512
21604
  const projectDir = parseProjectDir(process.argv);
20513
- const from = projectDir ? path9.resolve(projectDir) : process.cwd();
21605
+ const from = projectDir ? path10.resolve(projectDir) : process.cwd();
20514
21606
  const root = findProjectRoot(from);
20515
- const marvinDir = path9.join(root, ".marvin");
21607
+ const marvinDir = path10.join(root, ".marvin");
20516
21608
  await startStdioServer({ marvinDir });
20517
21609
  }
20518
21610
  main().catch((err) => {