mrvn-cli 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -15092,7 +15092,14 @@ import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
15092
15092
  // src/plugins/builtin/tools/epic-utils.ts
15093
15093
  function normalizeLinkedFeatures(value) {
15094
15094
  if (value === void 0 || value === null) return [];
15095
- if (typeof value === "string") return [value];
15095
+ if (typeof value === "string") {
15096
+ try {
15097
+ const parsed = JSON.parse(value);
15098
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
15099
+ } catch {
15100
+ }
15101
+ return [value];
15102
+ }
15096
15103
  if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
15097
15104
  return [];
15098
15105
  }
@@ -15915,6 +15922,20 @@ function createFeatureTools(store) {
15915
15922
 
15916
15923
  // src/plugins/builtin/tools/epics.ts
15917
15924
  import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
15925
+ var linkedFeatureArray = external_exports.preprocess(
15926
+ (val) => {
15927
+ if (typeof val === "string") {
15928
+ try {
15929
+ const parsed = JSON.parse(val);
15930
+ if (Array.isArray(parsed)) return parsed;
15931
+ } catch {
15932
+ }
15933
+ return [val];
15934
+ }
15935
+ return val;
15936
+ },
15937
+ external_exports.array(external_exports.string())
15938
+ );
15918
15939
  function createEpicTools(store) {
15919
15940
  return [
15920
15941
  tool10(
@@ -15980,7 +16001,7 @@ function createEpicTools(store) {
15980
16001
  {
15981
16002
  title: external_exports.string().describe("Epic title"),
15982
16003
  content: external_exports.string().describe("Epic description and scope"),
15983
- linkedFeature: external_exports.array(external_exports.string()).describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
16004
+ linkedFeature: linkedFeatureArray.describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
15984
16005
  status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
15985
16006
  owner: external_exports.string().optional().describe("Epic owner"),
15986
16007
  targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
@@ -16056,7 +16077,7 @@ function createEpicTools(store) {
16056
16077
  owner: external_exports.string().optional().describe("New owner"),
16057
16078
  targetDate: external_exports.string().optional().describe("New target date"),
16058
16079
  estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
16059
- linkedFeature: external_exports.array(external_exports.string()).optional().describe("New linked feature ID(s)"),
16080
+ linkedFeature: linkedFeatureArray.optional().describe("New linked feature ID(s)"),
16060
16081
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
16061
16082
  },
16062
16083
  async (args) => {
@@ -16593,6 +16614,205 @@ function createSprintPlanningTools(store) {
16593
16614
  ];
16594
16615
  }
16595
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
+
16596
16816
  // src/plugins/common.ts
16597
16817
  var COMMON_REGISTRATIONS = [
16598
16818
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -16600,7 +16820,8 @@ var COMMON_REGISTRATIONS = [
16600
16820
  { type: "feature", dirName: "features", idPrefix: "F" },
16601
16821
  { type: "epic", dirName: "epics", idPrefix: "E" },
16602
16822
  { type: "contribution", dirName: "contributions", idPrefix: "C" },
16603
- { type: "sprint", dirName: "sprints", idPrefix: "SP" }
16823
+ { type: "sprint", dirName: "sprints", idPrefix: "SP" },
16824
+ { type: "task", dirName: "tasks", idPrefix: "T" }
16604
16825
  ];
16605
16826
  function createCommonTools(store) {
16606
16827
  return [
@@ -16610,7 +16831,8 @@ function createCommonTools(store) {
16610
16831
  ...createEpicTools(store),
16611
16832
  ...createContributionTools(store),
16612
16833
  ...createSprintTools(store),
16613
- ...createSprintPlanningTools(store)
16834
+ ...createSprintPlanningTools(store),
16835
+ ...createTaskTools(store)
16614
16836
  ];
16615
16837
  }
16616
16838
 
@@ -16620,7 +16842,7 @@ var genericAgilePlugin = {
16620
16842
  name: "Generic Agile",
16621
16843
  description: "Default methodology plugin providing standard agile governance patterns for decisions, actions, and questions.",
16622
16844
  version: "0.1.0",
16623
- documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint"],
16845
+ documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint", "task"],
16624
16846
  documentTypeRegistrations: [...COMMON_REGISTRATIONS],
16625
16847
  tools: (store) => [...createCommonTools(store)],
16626
16848
  promptFragments: {
@@ -16659,6 +16881,11 @@ var genericAgilePlugin = {
16659
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.
16660
16882
  - **update_epic**: Update epic status (planned \u2192 in-progress \u2192 done), owner, and other fields.
16661
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
+
16662
16889
  **Feature Tools (read-only for awareness):**
16663
16890
  - **list_features** / **get_feature**: View features to understand what needs to be broken into epics.
16664
16891
 
@@ -16670,6 +16897,7 @@ var genericAgilePlugin = {
16670
16897
 
16671
16898
  **Key Workflow Rules:**
16672
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.
16673
16901
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
16674
16902
  - Collaborate with the Delivery Manager on target dates and effort estimates.
16675
16903
  - Each epic should have a clear scope and definition of done.
@@ -16705,6 +16933,9 @@ var genericAgilePlugin = {
16705
16933
  - **list_epics** / **get_epic**: View epics and their current status.
16706
16934
  - **update_epic**: Set targetDate and estimatedEffort on epics. Flag epics linked to deferred features.
16707
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
+
16708
16939
  **Feature Tools (tracking focus):**
16709
16940
  - **list_features** / **get_feature**: View features and their priorities.
16710
16941
 
@@ -16750,14 +16981,15 @@ var genericAgilePlugin = {
16750
16981
  - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
16751
16982
  - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
16752
16983
  - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
16753
- "*": `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:
16754
16985
 
16755
16986
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
16756
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.
16757
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).
16758
16990
  **Meetings**: Meeting records with attendees, agendas, and notes.
16759
16991
 
16760
- **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.
16761
16993
 
16762
16994
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
16763
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.
@@ -16772,10 +17004,10 @@ var genericAgilePlugin = {
16772
17004
  };
16773
17005
 
16774
17006
  // src/plugins/builtin/tools/use-cases.ts
16775
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17007
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
16776
17008
  function createUseCaseTools(store) {
16777
17009
  return [
16778
- tool14(
17010
+ tool15(
16779
17011
  "list_use_cases",
16780
17012
  "List all extension use cases, optionally filtered by status or extension type",
16781
17013
  {
@@ -16805,7 +17037,7 @@ function createUseCaseTools(store) {
16805
17037
  },
16806
17038
  { annotations: { readOnlyHint: true } }
16807
17039
  ),
16808
- tool14(
17040
+ tool15(
16809
17041
  "get_use_case",
16810
17042
  "Get the full content of a specific use case by ID",
16811
17043
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -16832,7 +17064,7 @@ function createUseCaseTools(store) {
16832
17064
  },
16833
17065
  { annotations: { readOnlyHint: true } }
16834
17066
  ),
16835
- tool14(
17067
+ tool15(
16836
17068
  "create_use_case",
16837
17069
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
16838
17070
  {
@@ -16866,7 +17098,7 @@ function createUseCaseTools(store) {
16866
17098
  };
16867
17099
  }
16868
17100
  ),
16869
- tool14(
17101
+ tool15(
16870
17102
  "update_use_case",
16871
17103
  "Update an existing extension use case",
16872
17104
  {
@@ -16896,10 +17128,10 @@ function createUseCaseTools(store) {
16896
17128
  }
16897
17129
 
16898
17130
  // src/plugins/builtin/tools/tech-assessments.ts
16899
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17131
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
16900
17132
  function createTechAssessmentTools(store) {
16901
17133
  return [
16902
- tool15(
17134
+ tool16(
16903
17135
  "list_tech_assessments",
16904
17136
  "List all technology assessments, optionally filtered by status",
16905
17137
  {
@@ -16930,7 +17162,7 @@ function createTechAssessmentTools(store) {
16930
17162
  },
16931
17163
  { annotations: { readOnlyHint: true } }
16932
17164
  ),
16933
- tool15(
17165
+ tool16(
16934
17166
  "get_tech_assessment",
16935
17167
  "Get the full content of a specific technology assessment by ID",
16936
17168
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -16957,7 +17189,7 @@ function createTechAssessmentTools(store) {
16957
17189
  },
16958
17190
  { annotations: { readOnlyHint: true } }
16959
17191
  ),
16960
- tool15(
17192
+ tool16(
16961
17193
  "create_tech_assessment",
16962
17194
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
16963
17195
  {
@@ -17028,7 +17260,7 @@ function createTechAssessmentTools(store) {
17028
17260
  };
17029
17261
  }
17030
17262
  ),
17031
- tool15(
17263
+ tool16(
17032
17264
  "update_tech_assessment",
17033
17265
  "Update an existing technology assessment. The linked use case cannot be changed.",
17034
17266
  {
@@ -17058,10 +17290,10 @@ function createTechAssessmentTools(store) {
17058
17290
  }
17059
17291
 
17060
17292
  // src/plugins/builtin/tools/extension-designs.ts
17061
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17293
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17062
17294
  function createExtensionDesignTools(store) {
17063
17295
  return [
17064
- tool16(
17296
+ tool17(
17065
17297
  "list_extension_designs",
17066
17298
  "List all extension designs, optionally filtered by status",
17067
17299
  {
@@ -17091,7 +17323,7 @@ function createExtensionDesignTools(store) {
17091
17323
  },
17092
17324
  { annotations: { readOnlyHint: true } }
17093
17325
  ),
17094
- tool16(
17326
+ tool17(
17095
17327
  "get_extension_design",
17096
17328
  "Get the full content of a specific extension design by ID",
17097
17329
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -17118,7 +17350,7 @@ function createExtensionDesignTools(store) {
17118
17350
  },
17119
17351
  { annotations: { readOnlyHint: true } }
17120
17352
  ),
17121
- tool16(
17353
+ tool17(
17122
17354
  "create_extension_design",
17123
17355
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
17124
17356
  {
@@ -17186,7 +17418,7 @@ function createExtensionDesignTools(store) {
17186
17418
  };
17187
17419
  }
17188
17420
  ),
17189
- tool16(
17421
+ tool17(
17190
17422
  "update_extension_design",
17191
17423
  "Update an existing extension design. The linked tech assessment cannot be changed.",
17192
17424
  {
@@ -17215,10 +17447,10 @@ function createExtensionDesignTools(store) {
17215
17447
  }
17216
17448
 
17217
17449
  // src/plugins/builtin/tools/aem-reports.ts
17218
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17450
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17219
17451
  function createAemReportTools(store) {
17220
17452
  return [
17221
- tool17(
17453
+ tool18(
17222
17454
  "generate_extension_portfolio",
17223
17455
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
17224
17456
  {},
@@ -17270,7 +17502,7 @@ function createAemReportTools(store) {
17270
17502
  },
17271
17503
  { annotations: { readOnlyHint: true } }
17272
17504
  ),
17273
- tool17(
17505
+ tool18(
17274
17506
  "generate_tech_readiness",
17275
17507
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
17276
17508
  {},
@@ -17322,7 +17554,7 @@ function createAemReportTools(store) {
17322
17554
  },
17323
17555
  { annotations: { readOnlyHint: true } }
17324
17556
  ),
17325
- tool17(
17557
+ tool18(
17326
17558
  "generate_phase_status",
17327
17559
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
17328
17560
  {},
@@ -17384,11 +17616,11 @@ function createAemReportTools(store) {
17384
17616
  import * as fs6 from "fs";
17385
17617
  import * as path6 from "path";
17386
17618
  import * as YAML4 from "yaml";
17387
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17619
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17388
17620
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
17389
17621
  function createAemPhaseTools(store, marvinDir) {
17390
17622
  return [
17391
- tool18(
17623
+ tool19(
17392
17624
  "get_current_phase",
17393
17625
  "Get the current AEM phase from project configuration",
17394
17626
  {},
@@ -17409,7 +17641,7 @@ function createAemPhaseTools(store, marvinDir) {
17409
17641
  },
17410
17642
  { annotations: { readOnlyHint: true } }
17411
17643
  ),
17412
- tool18(
17644
+ tool19(
17413
17645
  "advance_phase",
17414
17646
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
17415
17647
  {
@@ -17673,8 +17905,8 @@ function getPluginPromptFragment(plugin, personaId) {
17673
17905
  }
17674
17906
 
17675
17907
  // src/skills/registry.ts
17676
- import * as fs7 from "fs";
17677
- import * as path7 from "path";
17908
+ import * as fs8 from "fs";
17909
+ import * as path8 from "path";
17678
17910
  import { fileURLToPath } from "url";
17679
17911
  import * as YAML5 from "yaml";
17680
17912
  import matter2 from "gray-matter";
@@ -17717,7 +17949,7 @@ Be thorough but concise. Focus on actionable insights.`,
17717
17949
  };
17718
17950
 
17719
17951
  // src/skills/builtin/jira/tools.ts
17720
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17952
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
17721
17953
 
17722
17954
  // src/skills/builtin/jira/client.ts
17723
17955
  var JiraClient = class {
@@ -17727,8 +17959,8 @@ var JiraClient = class {
17727
17959
  this.baseUrl = `https://${config2.host}/rest/api/2`;
17728
17960
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17729
17961
  }
17730
- async request(path10, method = "GET", body) {
17731
- const url2 = `${this.baseUrl}${path10}`;
17962
+ async request(path11, method = "GET", body) {
17963
+ const url2 = `${this.baseUrl}${path11}`;
17732
17964
  const headers = {
17733
17965
  Authorization: this.authHeader,
17734
17966
  "Content-Type": "application/json",
@@ -17742,7 +17974,7 @@ var JiraClient = class {
17742
17974
  if (!response.ok) {
17743
17975
  const text = await response.text().catch(() => "");
17744
17976
  throw new Error(
17745
- `Jira API error ${response.status} ${method} ${path10}: ${text}`
17977
+ `Jira API error ${response.status} ${method} ${path11}: ${text}`
17746
17978
  );
17747
17979
  }
17748
17980
  if (response.status === 204) return void 0;
@@ -17826,7 +18058,7 @@ function createJiraTools(store) {
17826
18058
  const jiraUserConfig = loadUserConfig().jira;
17827
18059
  return [
17828
18060
  // --- Local read tools ---
17829
- tool19(
18061
+ tool20(
17830
18062
  "list_jira_issues",
17831
18063
  "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17832
18064
  {
@@ -17854,7 +18086,7 @@ function createJiraTools(store) {
17854
18086
  },
17855
18087
  { annotations: { readOnlyHint: true } }
17856
18088
  ),
17857
- tool19(
18089
+ tool20(
17858
18090
  "get_jira_issue",
17859
18091
  "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17860
18092
  {
@@ -17887,7 +18119,7 @@ function createJiraTools(store) {
17887
18119
  { annotations: { readOnlyHint: true } }
17888
18120
  ),
17889
18121
  // --- Jira → Local tools ---
17890
- tool19(
18122
+ tool20(
17891
18123
  "pull_jira_issue",
17892
18124
  "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17893
18125
  {
@@ -17934,7 +18166,7 @@ function createJiraTools(store) {
17934
18166
  };
17935
18167
  }
17936
18168
  ),
17937
- tool19(
18169
+ tool20(
17938
18170
  "pull_jira_issues_jql",
17939
18171
  "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17940
18172
  {
@@ -17982,7 +18214,7 @@ function createJiraTools(store) {
17982
18214
  }
17983
18215
  ),
17984
18216
  // --- Local → Jira tools ---
17985
- tool19(
18217
+ tool20(
17986
18218
  "push_artifact_to_jira",
17987
18219
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
17988
18220
  {
@@ -18043,7 +18275,7 @@ function createJiraTools(store) {
18043
18275
  }
18044
18276
  ),
18045
18277
  // --- Bidirectional sync ---
18046
- tool19(
18278
+ tool20(
18047
18279
  "sync_jira_issue",
18048
18280
  "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
18049
18281
  {
@@ -18084,7 +18316,7 @@ function createJiraTools(store) {
18084
18316
  }
18085
18317
  ),
18086
18318
  // --- Local link tool ---
18087
- tool19(
18319
+ tool20(
18088
18320
  "link_artifact_to_jira",
18089
18321
  "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
18090
18322
  {
@@ -18172,13 +18404,13 @@ var jiraSkill = {
18172
18404
  **Available tools:**
18173
18405
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18174
18406
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18175
- - \`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.)
18176
18408
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18177
18409
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18178
18410
 
18179
18411
  **As Tech Lead, use Jira integration to:**
18180
18412
  - Pull technical issues and bugs for sprint planning and estimation
18181
- - Push epics and technical decisions to Jira for cross-team visibility
18413
+ - Push epics, tasks, and technical decisions to Jira for cross-team visibility
18182
18414
  - Bidirectional sync to keep local governance and Jira in alignment
18183
18415
  - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
18184
18416
  "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
@@ -18192,72 +18424,476 @@ var jiraSkill = {
18192
18424
 
18193
18425
  **As Delivery Manager, use Jira integration to:**
18194
18426
  - Pull sprint issues for tracking progress and blockers
18195
- - Push actions and decisions to Jira for stakeholder visibility
18427
+ - Push actions, decisions, and tasks to Jira for stakeholder visibility
18196
18428
  - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
18197
18429
  - Sync status between Marvin governance items and Jira issues`
18198
18430
  }
18199
18431
  };
18200
18432
 
18201
- // src/skills/registry.ts
18202
- var BUILTIN_SKILLS = {
18203
- "governance-review": governanceReviewSkill,
18204
- "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
18205
18442
  };
18206
- function getBuiltinSkillsDir() {
18207
- const thisFile = fileURLToPath(import.meta.url);
18208
- return path7.join(path7.dirname(thisFile), "builtin");
18209
- }
18210
- function loadSkillFromDirectory(dirPath) {
18211
- const skillMdPath = path7.join(dirPath, "SKILL.md");
18212
- if (!fs7.existsSync(skillMdPath)) return void 0;
18213
- try {
18214
- const raw = fs7.readFileSync(skillMdPath, "utf-8");
18215
- const { data, content } = matter2(raw);
18216
- if (!data.name || !data.description) return void 0;
18217
- const metadata = data.metadata ?? {};
18218
- const version2 = metadata.version ?? "1.0.0";
18219
- const personas = metadata.personas;
18220
- const promptFragments = {};
18221
- const wildcardPrompt = content.trim();
18222
- if (wildcardPrompt) {
18223
- promptFragments["*"] = wildcardPrompt;
18224
- }
18225
- const personasDir = path7.join(dirPath, "personas");
18226
- if (fs7.existsSync(personasDir)) {
18227
- try {
18228
- for (const file2 of fs7.readdirSync(personasDir)) {
18229
- if (!file2.endsWith(".md")) continue;
18230
- const personaId = file2.replace(/\.md$/, "");
18231
- const personaPrompt = fs7.readFileSync(path7.join(personasDir, file2), "utf-8").trim();
18232
- if (personaPrompt) {
18233
- 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
+ }
18234
18590
  }
18235
18591
  }
18236
- } catch {
18237
18592
  }
18593
+ lines.push("");
18238
18594
  }
18239
- let actions;
18240
- const actionsPath = path7.join(dirPath, "actions.yaml");
18241
- if (fs7.existsSync(actionsPath)) {
18242
- try {
18243
- const actionsRaw = fs7.readFileSync(actionsPath, "utf-8");
18244
- actions = YAML5.parse(actionsRaw);
18245
- } 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]}`);
18246
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("");
18247
18622
  }
18248
- return {
18249
- id: data.name,
18250
- name: data.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
18251
- description: data.description,
18252
- version: version2,
18253
- format: "skill-md",
18254
- dirPath,
18255
- personas,
18256
- promptFragments: Object.keys(promptFragments).length > 0 ? promptFragments : void 0,
18257
- actions
18258
- };
18259
- } catch {
18260
- return void 0;
18623
+ }
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;
18261
18897
  }
18262
18898
  }
18263
18899
  function loadAllSkills(marvinDir) {
@@ -18267,10 +18903,10 @@ function loadAllSkills(marvinDir) {
18267
18903
  }
18268
18904
  try {
18269
18905
  const builtinDir = getBuiltinSkillsDir();
18270
- if (fs7.existsSync(builtinDir)) {
18271
- for (const entry of fs7.readdirSync(builtinDir)) {
18272
- const entryPath = path7.join(builtinDir, entry);
18273
- 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;
18274
18910
  if (skills.has(entry)) continue;
18275
18911
  const skill = loadSkillFromDirectory(entryPath);
18276
18912
  if (skill) skills.set(skill.id, skill);
@@ -18279,18 +18915,18 @@ function loadAllSkills(marvinDir) {
18279
18915
  } catch {
18280
18916
  }
18281
18917
  if (marvinDir) {
18282
- const skillsDir = path7.join(marvinDir, "skills");
18283
- if (fs7.existsSync(skillsDir)) {
18918
+ const skillsDir = path8.join(marvinDir, "skills");
18919
+ if (fs8.existsSync(skillsDir)) {
18284
18920
  let entries;
18285
18921
  try {
18286
- entries = fs7.readdirSync(skillsDir);
18922
+ entries = fs8.readdirSync(skillsDir);
18287
18923
  } catch {
18288
18924
  entries = [];
18289
18925
  }
18290
18926
  for (const entry of entries) {
18291
- const entryPath = path7.join(skillsDir, entry);
18927
+ const entryPath = path8.join(skillsDir, entry);
18292
18928
  try {
18293
- if (fs7.statSync(entryPath).isDirectory()) {
18929
+ if (fs8.statSync(entryPath).isDirectory()) {
18294
18930
  const skill = loadSkillFromDirectory(entryPath);
18295
18931
  if (skill) skills.set(skill.id, skill);
18296
18932
  continue;
@@ -18300,7 +18936,7 @@ function loadAllSkills(marvinDir) {
18300
18936
  }
18301
18937
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
18302
18938
  try {
18303
- const raw = fs7.readFileSync(entryPath, "utf-8");
18939
+ const raw = fs8.readFileSync(entryPath, "utf-8");
18304
18940
  const parsed = YAML5.parse(raw);
18305
18941
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
18306
18942
  const skill = {
@@ -18368,7 +19004,7 @@ ${fragment}`);
18368
19004
  }
18369
19005
 
18370
19006
  // src/skills/action-tools.ts
18371
- import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
19007
+ import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
18372
19008
 
18373
19009
  // src/skills/action-runner.ts
18374
19010
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -18380,7 +19016,7 @@ import {
18380
19016
 
18381
19017
  // src/agent/tools/web.ts
18382
19018
  import * as http2 from "http";
18383
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19019
+ import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
18384
19020
 
18385
19021
  // src/web/data.ts
18386
19022
  function getOverviewData(store) {
@@ -18616,6 +19252,7 @@ function inline(text) {
18616
19252
  function layout(opts, body) {
18617
19253
  const topItems = [
18618
19254
  { href: "/", label: "Overview" },
19255
+ { href: "/timeline", label: "Timeline" },
18619
19256
  { href: "/board", label: "Board" },
18620
19257
  { href: "/gar", label: "GAR Report" },
18621
19258
  { href: "/health", label: "Health" }
@@ -18652,7 +19289,7 @@ function layout(opts, body) {
18652
19289
  ${groupsHtml}
18653
19290
  </nav>
18654
19291
  </aside>
18655
- <main class="main">
19292
+ <main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
18656
19293
  <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18657
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>
18658
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>
@@ -18661,7 +19298,36 @@ function layout(opts, body) {
18661
19298
  </main>
18662
19299
  </div>
18663
19300
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18664
- <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>
18665
19331
  </body>
18666
19332
  </html>`;
18667
19333
  }
@@ -18908,6 +19574,10 @@ a:hover { text-decoration: underline; }
18908
19574
  /* Table */
18909
19575
  .table-wrap {
18910
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);
18911
19581
  }
18912
19582
 
18913
19583
  table {
@@ -18923,6 +19593,10 @@ th {
18923
19593
  letter-spacing: 0.05em;
18924
19594
  color: var(--text-dim);
18925
19595
  border-bottom: 1px solid var(--border);
19596
+ position: sticky;
19597
+ top: 0;
19598
+ background: var(--bg-card);
19599
+ z-index: 1;
18926
19600
  }
18927
19601
 
18928
19602
  td {
@@ -18970,6 +19644,8 @@ tr:hover td {
18970
19644
  border: 1px solid var(--border);
18971
19645
  border-radius: var(--radius);
18972
19646
  padding: 1.25rem;
19647
+ display: flex;
19648
+ flex-direction: column;
18973
19649
  }
18974
19650
 
18975
19651
  .gar-area .area-header {
@@ -19000,6 +19676,9 @@ tr:hover td {
19000
19676
  .gar-area ul {
19001
19677
  list-style: none;
19002
19678
  font-size: 0.8rem;
19679
+ max-height: 200px;
19680
+ overflow-y: auto;
19681
+ scrollbar-width: thin;
19003
19682
  }
19004
19683
 
19005
19684
  .gar-area li {
@@ -19022,13 +19701,14 @@ tr:hover td {
19022
19701
  display: flex;
19023
19702
  gap: 1rem;
19024
19703
  overflow-x: auto;
19704
+ scrollbar-width: thin;
19025
19705
  padding-bottom: 1rem;
19026
19706
  }
19027
19707
 
19028
19708
  .board-column {
19029
19709
  min-width: 240px;
19030
19710
  max-width: 300px;
19031
- flex: 1;
19711
+ flex: 0 0 auto;
19032
19712
  }
19033
19713
 
19034
19714
  .board-column-header {
@@ -19041,6 +19721,7 @@ tr:hover td {
19041
19721
  margin-bottom: 0.5rem;
19042
19722
  display: flex;
19043
19723
  justify-content: space-between;
19724
+ flex-shrink: 0;
19044
19725
  }
19045
19726
 
19046
19727
  .board-column-header .count {
@@ -19222,6 +19903,241 @@ tr:hover td {
19222
19903
  .mermaid-row .mermaid-container {
19223
19904
  margin: 0;
19224
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
+ }
19225
20141
  `;
19226
20142
  }
19227
20143
 
@@ -19230,98 +20146,275 @@ function sanitize(text, maxLen = 40) {
19230
20146
  const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
19231
20147
  return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19232
20148
  }
19233
- function mermaidBlock(definition) {
19234
- 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">
19235
20152
  ${definition}
19236
20153
  </pre></div>`;
19237
20154
  }
19238
20155
  function placeholder(message) {
19239
20156
  return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19240
20157
  }
19241
- function buildTimelineGantt(data) {
19242
- 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);
19243
20168
  if (sprintsWithDates.length === 0) {
19244
20169
  return placeholder("No timeline data available \u2014 sprints need start and end dates.");
19245
20170
  }
20171
+ const truncated = sprintsWithDates.length > maxSprints;
20172
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
20173
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
19246
20174
  const epicMap = new Map(data.epics.map((e) => [e.id, e]));
19247
- const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
19248
- for (const sprint of sprintsWithDates) {
19249
- 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>`);
19250
20208
  const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
19251
- if (linked.length === 0) {
19252
- lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
19253
- } else {
19254
- for (const epic of linked) {
19255
- const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
19256
- lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
19257
- }
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>`);
19258
20220
  }
19259
20221
  }
19260
- 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";
19261
20244
  }
19262
20245
  function buildArtifactFlowchart(data) {
19263
20246
  if (data.features.length === 0 && data.epics.length === 0) {
19264
20247
  return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
19265
20248
  }
19266
- const lines = ["graph TD"];
19267
- lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
19268
- lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
19269
- lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
19270
- lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
19271
- const nodeIds = /* @__PURE__ */ new Set();
20249
+ const edges = [];
20250
+ const epicsByFeature = /* @__PURE__ */ new Map();
19272
20251
  for (const epic of data.epics) {
19273
- for (const featureId of epic.linkedFeature) {
19274
- const feature = data.features.find((f) => f.id === featureId);
19275
- if (feature) {
19276
- const fNode = feature.id.replace(/-/g, "_");
19277
- const eNode = epic.id.replace(/-/g, "_");
19278
- if (!nodeIds.has(fNode)) {
19279
- lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
19280
- nodeIds.add(fNode);
19281
- }
19282
- if (!nodeIds.has(eNode)) {
19283
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19284
- nodeIds.add(eNode);
19285
- }
19286
- lines.push(` ${fNode} --> ${eNode}`);
19287
- }
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 });
19288
20256
  }
19289
20257
  }
20258
+ const sprintsByEpic = /* @__PURE__ */ new Map();
19290
20259
  for (const sprint of data.sprints) {
19291
- const sNode = sprint.id.replace(/-/g, "_");
19292
- for (const epicId of sprint.linkedEpics) {
19293
- const epic = data.epics.find((e) => e.id === epicId);
19294
- if (epic) {
19295
- const eNode = epic.id.replace(/-/g, "_");
19296
- if (!nodeIds.has(eNode)) {
19297
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19298
- nodeIds.add(eNode);
19299
- }
19300
- if (!nodeIds.has(sNode)) {
19301
- lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
19302
- nodeIds.add(sNode);
19303
- }
19304
- lines.push(` ${eNode} --> ${sNode}`);
19305
- }
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 });
19306
20264
  }
19307
20265
  }
19308
- 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) {
19309
20278
  return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
19310
20279
  }
19311
- const allItems = [
19312
- ...data.features.map((f) => ({ id: f.id, status: f.status })),
19313
- ...data.epics.map((e) => ({ id: e.id, status: e.status })),
19314
- ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
19315
- ];
19316
- for (const item of allItems) {
19317
- const node = item.id.replace(/-/g, "_");
19318
- if (!nodeIds.has(node)) continue;
19319
- const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
19320
- if (cls) {
19321
- lines.push(` class ${node} ${cls}`);
19322
- }
19323
- }
19324
- 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>`;
19325
20418
  }
19326
20419
  function buildStatusPie(title, counts) {
19327
20420
  const entries = Object.entries(counts).filter(([, v]) => v > 0);
@@ -19401,8 +20494,7 @@ function overviewPage(data, diagrams, navGroups) {
19401
20494
  ${groupSections}
19402
20495
  ${ungroupedSection}
19403
20496
 
19404
- <div class="section-title">Project Timeline</div>
19405
- ${buildTimelineGantt(diagrams)}
20497
+ <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
19406
20498
 
19407
20499
  <div class="section-title">Artifact Relationships</div>
19408
20500
  ${buildArtifactFlowchart(diagrams)}
@@ -19657,6 +20749,7 @@ function boardPage(data) {
19657
20749
  <span>${escapeHtml(col.status)}</span>
19658
20750
  <span class="count">${col.docs.length}</span>
19659
20751
  </div>
20752
+ <div class="board-column-cards">
19660
20753
  ${col.docs.map(
19661
20754
  (doc) => `
19662
20755
  <div class="board-card">
@@ -19667,6 +20760,7 @@ function boardPage(data) {
19667
20760
  </a>
19668
20761
  </div>`
19669
20762
  ).join("\n")}
20763
+ </div>
19670
20764
  </div>`
19671
20765
  ).join("\n");
19672
20766
  return `
@@ -19692,6 +20786,18 @@ function boardPage(data) {
19692
20786
  `;
19693
20787
  }
19694
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
+
19695
20801
  // src/web/router.ts
19696
20802
  function handleRequest(req, res, store, projectName, navGroups) {
19697
20803
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -19713,6 +20819,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19713
20819
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
19714
20820
  return;
19715
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
+ }
19716
20828
  if (pathname === "/gar") {
19717
20829
  const report = getGarData(store, projectName);
19718
20830
  const body = garPage(report);
@@ -19817,7 +20929,7 @@ function openBrowser(url2) {
19817
20929
  var runningServer = null;
19818
20930
  function createWebTools(store, projectName, navGroups) {
19819
20931
  return [
19820
- tool20(
20932
+ tool22(
19821
20933
  "start_web_dashboard",
19822
20934
  "Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
19823
20935
  {
@@ -19849,7 +20961,7 @@ function createWebTools(store, projectName, navGroups) {
19849
20961
  };
19850
20962
  }
19851
20963
  ),
19852
- tool20(
20964
+ tool22(
19853
20965
  "stop_web_dashboard",
19854
20966
  "Stop the running Marvin web dashboard.",
19855
20967
  {},
@@ -19869,7 +20981,7 @@ function createWebTools(store, projectName, navGroups) {
19869
20981
  };
19870
20982
  }
19871
20983
  ),
19872
- tool20(
20984
+ tool22(
19873
20985
  "get_web_dashboard_urls",
19874
20986
  "Get all available dashboard page URLs. The dashboard must be running.",
19875
20987
  {},
@@ -19895,7 +21007,7 @@ function createWebTools(store, projectName, navGroups) {
19895
21007
  },
19896
21008
  { annotations: { readOnlyHint: true } }
19897
21009
  ),
19898
- tool20(
21010
+ tool22(
19899
21011
  "get_dashboard_overview",
19900
21012
  "Get the project overview data: document type counts and recent activity. Works without the web server running.",
19901
21013
  {},
@@ -19917,7 +21029,7 @@ function createWebTools(store, projectName, navGroups) {
19917
21029
  },
19918
21030
  { annotations: { readOnlyHint: true } }
19919
21031
  ),
19920
- tool20(
21032
+ tool22(
19921
21033
  "get_dashboard_gar",
19922
21034
  "Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
19923
21035
  {},
@@ -19929,7 +21041,7 @@ function createWebTools(store, projectName, navGroups) {
19929
21041
  },
19930
21042
  { annotations: { readOnlyHint: true } }
19931
21043
  ),
19932
- tool20(
21044
+ tool22(
19933
21045
  "get_dashboard_board",
19934
21046
  "Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
19935
21047
  {
@@ -20043,7 +21155,7 @@ function createSkillActionTools(skills, context) {
20043
21155
  if (!skill.actions) continue;
20044
21156
  for (const action of skill.actions) {
20045
21157
  tools.push(
20046
- tool21(
21158
+ tool23(
20047
21159
  `${skill.id}__${action.id}`,
20048
21160
  action.description,
20049
21161
  {
@@ -20134,7 +21246,7 @@ var deliveryManager = {
20134
21246
  "Epic scheduling and tracking",
20135
21247
  "Sprint planning and tracking"
20136
21248
  ],
20137
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "sprint"],
21249
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
20138
21250
  contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
20139
21251
  };
20140
21252
 
@@ -20172,9 +21284,10 @@ var techLead = {
20172
21284
  "Implementation guidance",
20173
21285
  "Non-functional requirements",
20174
21286
  "Epic creation and scoping",
21287
+ "Task creation and breakdown",
20175
21288
  "Sprint scoping and technical execution"
20176
21289
  ],
20177
- documentTypes: ["decision", "action", "question", "epic", "sprint"],
21290
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
20178
21291
  contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
20179
21292
  };
20180
21293
 
@@ -20272,10 +21385,10 @@ ${lines.join("\n\n")}`;
20272
21385
  }
20273
21386
 
20274
21387
  // src/mcp/persona-tools.ts
20275
- import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
21388
+ import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
20276
21389
  function createPersonaTools(ctx, marvinDir) {
20277
21390
  return [
20278
- tool22(
21391
+ tool24(
20279
21392
  "set_persona",
20280
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.",
20281
21394
  {
@@ -20305,7 +21418,7 @@ ${summaries}`
20305
21418
  };
20306
21419
  }
20307
21420
  ),
20308
- tool22(
21421
+ tool24(
20309
21422
  "get_persona_guidance",
20310
21423
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
20311
21424
  {
@@ -20407,8 +21520,8 @@ function collectTools(marvinDir) {
20407
21520
  const plugin = resolvePlugin(config2.methodology);
20408
21521
  const registrations = plugin?.documentTypeRegistrations ?? [];
20409
21522
  const store = new DocumentStore(marvinDir, registrations);
20410
- const sourcesDir = path8.join(marvinDir, "sources");
20411
- const hasSourcesDir = fs8.existsSync(sourcesDir);
21523
+ const sourcesDir = path9.join(marvinDir, "sources");
21524
+ const hasSourcesDir = fs9.existsSync(sourcesDir);
20412
21525
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20413
21526
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20414
21527
  const sessionStore = new SessionStore(marvinDir);
@@ -20416,7 +21529,7 @@ function collectTools(marvinDir) {
20416
21529
  const allSkillIds = [...allSkills.keys()];
20417
21530
  const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
20418
21531
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
20419
- const projectRoot = path8.dirname(marvinDir);
21532
+ const projectRoot = path9.dirname(marvinDir);
20420
21533
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
20421
21534
  const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
20422
21535
  const navGroups = buildNavGroups({
@@ -20489,9 +21602,9 @@ function parseProjectDir(argv) {
20489
21602
  }
20490
21603
  async function main() {
20491
21604
  const projectDir = parseProjectDir(process.argv);
20492
- const from = projectDir ? path9.resolve(projectDir) : process.cwd();
21605
+ const from = projectDir ? path10.resolve(projectDir) : process.cwd();
20493
21606
  const root = findProjectRoot(from);
20494
- const marvinDir = path9.join(root, ".marvin");
21607
+ const marvinDir = path10.join(root, ".marvin");
20495
21608
  await startStdioServer({ marvinDir });
20496
21609
  }
20497
21610
  main().catch((err) => {