mrvn-cli 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // bin/marvin-serve.ts
9
- import * as path9 from "path";
9
+ import * as path10 from "path";
10
10
 
11
11
  // src/core/project.ts
12
12
  import * as fs2 from "fs";
@@ -98,8 +98,8 @@ function findProjectRoot(from = process.cwd()) {
98
98
  }
99
99
 
100
100
  // src/mcp/stdio-server.ts
101
- import * as fs8 from "fs";
102
- import * as path8 from "path";
101
+ import * as fs9 from "fs";
102
+ import * as path9 from "path";
103
103
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
104
104
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
105
105
 
@@ -1206,10 +1206,10 @@ function mergeDefs(...defs) {
1206
1206
  function cloneDef(schema) {
1207
1207
  return mergeDefs(schema._zod.def);
1208
1208
  }
1209
- function getElementAtPath(obj, path10) {
1210
- if (!path10)
1209
+ function getElementAtPath(obj, path11) {
1210
+ if (!path11)
1211
1211
  return obj;
1212
- return path10.reduce((acc, key) => acc?.[key], obj);
1212
+ return path11.reduce((acc, key) => acc?.[key], obj);
1213
1213
  }
1214
1214
  function promiseAllObject(promisesObj) {
1215
1215
  const keys = Object.keys(promisesObj);
@@ -1592,11 +1592,11 @@ function aborted(x, startIndex = 0) {
1592
1592
  }
1593
1593
  return false;
1594
1594
  }
1595
- function prefixIssues(path10, issues) {
1595
+ function prefixIssues(path11, issues) {
1596
1596
  return issues.map((iss) => {
1597
1597
  var _a2;
1598
1598
  (_a2 = iss).path ?? (_a2.path = []);
1599
- iss.path.unshift(path10);
1599
+ iss.path.unshift(path11);
1600
1600
  return iss;
1601
1601
  });
1602
1602
  }
@@ -1779,7 +1779,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1779
1779
  }
1780
1780
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1781
1781
  const result = { errors: [] };
1782
- const processError = (error49, path10 = []) => {
1782
+ const processError = (error49, path11 = []) => {
1783
1783
  var _a2, _b;
1784
1784
  for (const issue2 of error49.issues) {
1785
1785
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1789,7 +1789,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1789
1789
  } else if (issue2.code === "invalid_element") {
1790
1790
  processError({ issues: issue2.issues }, issue2.path);
1791
1791
  } else {
1792
- const fullpath = [...path10, ...issue2.path];
1792
+ const fullpath = [...path11, ...issue2.path];
1793
1793
  if (fullpath.length === 0) {
1794
1794
  result.errors.push(mapper(issue2));
1795
1795
  continue;
@@ -1821,8 +1821,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1821
1821
  }
1822
1822
  function toDotPath(_path) {
1823
1823
  const segs = [];
1824
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1825
- for (const seg of path10) {
1824
+ const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1825
+ for (const seg of path11) {
1826
1826
  if (typeof seg === "number")
1827
1827
  segs.push(`[${seg}]`);
1828
1828
  else if (typeof seg === "symbol")
@@ -13799,13 +13799,13 @@ function resolveRef(ref, ctx) {
13799
13799
  if (!ref.startsWith("#")) {
13800
13800
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13801
13801
  }
13802
- const path10 = ref.slice(1).split("/").filter(Boolean);
13803
- if (path10.length === 0) {
13802
+ const path11 = ref.slice(1).split("/").filter(Boolean);
13803
+ if (path11.length === 0) {
13804
13804
  return ctx.rootSchema;
13805
13805
  }
13806
13806
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13807
- if (path10[0] === defsKey) {
13808
- const key = path10[1];
13807
+ if (path11[0] === defsKey) {
13808
+ const key = path11[1];
13809
13809
  if (!key || !ctx.defs[key]) {
13810
13810
  throw new Error(`Reference not found: ${ref}`);
13811
13811
  }
@@ -16614,6 +16614,205 @@ function createSprintPlanningTools(store) {
16614
16614
  ];
16615
16615
  }
16616
16616
 
16617
+ // src/plugins/builtin/tools/tasks.ts
16618
+ import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
16619
+
16620
+ // src/plugins/builtin/tools/task-utils.ts
16621
+ function normalizeLinkedEpics(value) {
16622
+ if (value === void 0 || value === null) return [];
16623
+ if (typeof value === "string") {
16624
+ try {
16625
+ const parsed = JSON.parse(value);
16626
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
16627
+ } catch {
16628
+ }
16629
+ return [value];
16630
+ }
16631
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
16632
+ return [];
16633
+ }
16634
+ function generateEpicTags(epics) {
16635
+ return epics.map((id) => `epic:${id}`);
16636
+ }
16637
+
16638
+ // src/plugins/builtin/tools/tasks.ts
16639
+ var linkedEpicArray = external_exports.preprocess(
16640
+ (val) => {
16641
+ if (typeof val === "string") {
16642
+ try {
16643
+ const parsed = JSON.parse(val);
16644
+ if (Array.isArray(parsed)) return parsed;
16645
+ } catch {
16646
+ }
16647
+ return [val];
16648
+ }
16649
+ return val;
16650
+ },
16651
+ external_exports.array(external_exports.string())
16652
+ );
16653
+ function createTaskTools(store) {
16654
+ return [
16655
+ tool14(
16656
+ "list_tasks",
16657
+ "List all tasks in the project, optionally filtered by status, linked epic, or priority",
16658
+ {
16659
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Filter by task status"),
16660
+ linkedEpic: external_exports.string().optional().describe("Filter by linked epic ID (e.g. 'E-001')"),
16661
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority")
16662
+ },
16663
+ async (args) => {
16664
+ let docs = store.list({ type: "task", status: args.status });
16665
+ if (args.linkedEpic) {
16666
+ docs = docs.filter(
16667
+ (d) => normalizeLinkedEpics(d.frontmatter.linkedEpic).includes(args.linkedEpic)
16668
+ );
16669
+ }
16670
+ if (args.priority) {
16671
+ docs = docs.filter((d) => d.frontmatter.priority === args.priority);
16672
+ }
16673
+ const summary = docs.map((d) => ({
16674
+ id: d.frontmatter.id,
16675
+ title: d.frontmatter.title,
16676
+ status: d.frontmatter.status,
16677
+ linkedEpic: normalizeLinkedEpics(d.frontmatter.linkedEpic),
16678
+ priority: d.frontmatter.priority,
16679
+ complexity: d.frontmatter.complexity,
16680
+ estimatedPoints: d.frontmatter.estimatedPoints,
16681
+ tags: d.frontmatter.tags
16682
+ }));
16683
+ return {
16684
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
16685
+ };
16686
+ },
16687
+ { annotations: { readOnlyHint: true } }
16688
+ ),
16689
+ tool14(
16690
+ "get_task",
16691
+ "Get the full content of a specific task by ID",
16692
+ { id: external_exports.string().describe("Task ID (e.g. 'T-001')") },
16693
+ async (args) => {
16694
+ const doc = store.get(args.id);
16695
+ if (!doc) {
16696
+ return {
16697
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
16698
+ isError: true
16699
+ };
16700
+ }
16701
+ return {
16702
+ content: [
16703
+ {
16704
+ type: "text",
16705
+ text: JSON.stringify(
16706
+ { ...doc.frontmatter, content: doc.content },
16707
+ null,
16708
+ 2
16709
+ )
16710
+ }
16711
+ ]
16712
+ };
16713
+ },
16714
+ { annotations: { readOnlyHint: true } }
16715
+ ),
16716
+ tool14(
16717
+ "create_task",
16718
+ "Create a new implementation task linked to one or more epics. The linked epic is soft-validated (warns if not found, but does not block creation).",
16719
+ {
16720
+ title: external_exports.string().describe("Task title"),
16721
+ content: external_exports.string().describe("Task description and implementation details"),
16722
+ linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
16723
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
16724
+ acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
16725
+ technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
16726
+ estimatedPoints: external_exports.number().optional().describe("Story point estimate"),
16727
+ complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
16728
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
16729
+ tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
16730
+ },
16731
+ async (args) => {
16732
+ const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
16733
+ const warnings = [];
16734
+ for (const epicId of linkedEpics) {
16735
+ const epic = store.get(epicId);
16736
+ if (!epic) {
16737
+ warnings.push(`Warning: Epic ${epicId} not found`);
16738
+ } else if (epic.frontmatter.type !== "epic") {
16739
+ warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
16740
+ }
16741
+ }
16742
+ const frontmatter = {
16743
+ title: args.title,
16744
+ status: args.status ?? "backlog",
16745
+ linkedEpic: linkedEpics,
16746
+ tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
16747
+ };
16748
+ if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
16749
+ if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
16750
+ if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
16751
+ if (args.complexity) frontmatter.complexity = args.complexity;
16752
+ if (args.priority) frontmatter.priority = args.priority;
16753
+ const doc = store.create("task", frontmatter, args.content);
16754
+ const parts = [
16755
+ `Created task ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${linkedEpics.join(", ")})`
16756
+ ];
16757
+ if (warnings.length > 0) {
16758
+ parts.push(warnings.join("; "));
16759
+ }
16760
+ return {
16761
+ content: [{ type: "text", text: parts.join("\n") }]
16762
+ };
16763
+ }
16764
+ ),
16765
+ tool14(
16766
+ "update_task",
16767
+ "Update an existing task, including its linked epics.",
16768
+ {
16769
+ id: external_exports.string().describe("Task ID to update"),
16770
+ title: external_exports.string().optional().describe("New title"),
16771
+ status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
16772
+ content: external_exports.string().optional().describe("New content"),
16773
+ linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
16774
+ acceptanceCriteria: external_exports.string().optional().describe("New acceptance criteria"),
16775
+ technicalNotes: external_exports.string().optional().describe("New technical notes"),
16776
+ estimatedPoints: external_exports.number().optional().describe("New story point estimate"),
16777
+ complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
16778
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
16779
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)")
16780
+ },
16781
+ async (args) => {
16782
+ const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, ...updates } = args;
16783
+ const warnings = [];
16784
+ if (rawLinkedEpic !== void 0) {
16785
+ const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
16786
+ for (const epicId of linkedEpics) {
16787
+ const epic = store.get(epicId);
16788
+ if (!epic) {
16789
+ warnings.push(`Warning: Epic ${epicId} not found`);
16790
+ } else if (epic.frontmatter.type !== "epic") {
16791
+ warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
16792
+ }
16793
+ }
16794
+ updates.linkedEpic = linkedEpics;
16795
+ const existingDoc = store.get(id);
16796
+ const existingTags = existingDoc?.frontmatter.tags ?? [];
16797
+ const nonEpicTags = existingTags.filter((t) => !t.startsWith("epic:"));
16798
+ const baseTags = userTags ?? nonEpicTags;
16799
+ updates.tags = [...generateEpicTags(linkedEpics), ...baseTags];
16800
+ } else if (userTags !== void 0) {
16801
+ updates.tags = userTags;
16802
+ }
16803
+ const doc = store.update(id, updates, content);
16804
+ const parts = [`Updated task ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
16805
+ if (warnings.length > 0) {
16806
+ parts.push(warnings.join("; "));
16807
+ }
16808
+ return {
16809
+ content: [{ type: "text", text: parts.join("\n") }]
16810
+ };
16811
+ }
16812
+ )
16813
+ ];
16814
+ }
16815
+
16617
16816
  // src/plugins/common.ts
16618
16817
  var COMMON_REGISTRATIONS = [
16619
16818
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -16621,7 +16820,8 @@ var COMMON_REGISTRATIONS = [
16621
16820
  { type: "feature", dirName: "features", idPrefix: "F" },
16622
16821
  { type: "epic", dirName: "epics", idPrefix: "E" },
16623
16822
  { type: "contribution", dirName: "contributions", idPrefix: "C" },
16624
- { type: "sprint", dirName: "sprints", idPrefix: "SP" }
16823
+ { type: "sprint", dirName: "sprints", idPrefix: "SP" },
16824
+ { type: "task", dirName: "tasks", idPrefix: "T" }
16625
16825
  ];
16626
16826
  function createCommonTools(store) {
16627
16827
  return [
@@ -16631,7 +16831,8 @@ function createCommonTools(store) {
16631
16831
  ...createEpicTools(store),
16632
16832
  ...createContributionTools(store),
16633
16833
  ...createSprintTools(store),
16634
- ...createSprintPlanningTools(store)
16834
+ ...createSprintPlanningTools(store),
16835
+ ...createTaskTools(store)
16635
16836
  ];
16636
16837
  }
16637
16838
 
@@ -16641,7 +16842,7 @@ var genericAgilePlugin = {
16641
16842
  name: "Generic Agile",
16642
16843
  description: "Default methodology plugin providing standard agile governance patterns for decisions, actions, and questions.",
16643
16844
  version: "0.1.0",
16644
- documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint"],
16845
+ documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint", "task"],
16645
16846
  documentTypeRegistrations: [...COMMON_REGISTRATIONS],
16646
16847
  tools: (store) => [...createCommonTools(store)],
16647
16848
  promptFragments: {
@@ -16680,6 +16881,11 @@ var genericAgilePlugin = {
16680
16881
  - **create_epic**: Create implementation epics linked to approved features. The system enforces that the linked feature must exist and be approved \u2014 if it's still "draft", ask the Product Owner to approve it first.
16681
16882
  - **update_epic**: Update epic status (planned \u2192 in-progress \u2192 done), owner, and other fields.
16682
16883
 
16884
+ **Task Tools:**
16885
+ - **list_tasks** / **get_task**: Browse and read implementation tasks.
16886
+ - **create_task**: Create implementation tasks linked to epics. Linked epics are soft-validated (warns if not found, does not block). Tasks auto-generate \`epic:E-xxx\` tags. Default status: "backlog".
16887
+ - **update_task**: Update task status (backlog \u2192 ready \u2192 in-progress \u2192 review \u2192 done), acceptance criteria, technical notes, complexity, priority, and estimated points.
16888
+
16683
16889
  **Feature Tools (read-only for awareness):**
16684
16890
  - **list_features** / **get_feature**: View features to understand what needs to be broken into epics.
16685
16891
 
@@ -16691,6 +16897,7 @@ var genericAgilePlugin = {
16691
16897
 
16692
16898
  **Key Workflow Rules:**
16693
16899
  - Only create epics against approved features \u2014 create_epic enforces this.
16900
+ - Break epics into tasks (T-xxx) with clear acceptance criteria and complexity estimates.
16694
16901
  - Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
16695
16902
  - Collaborate with the Delivery Manager on target dates and effort estimates.
16696
16903
  - Each epic should have a clear scope and definition of done.
@@ -16726,6 +16933,9 @@ var genericAgilePlugin = {
16726
16933
  - **list_epics** / **get_epic**: View epics and their current status.
16727
16934
  - **update_epic**: Set targetDate and estimatedEffort on epics. Flag epics linked to deferred features.
16728
16935
 
16936
+ **Task Tools (read-only for tracking):**
16937
+ - **list_tasks** / **get_task**: View tasks and their statuses. Filter by linkedEpic to see implementation breakdown.
16938
+
16729
16939
  **Feature Tools (tracking focus):**
16730
16940
  - **list_features** / **get_feature**: View features and their priorities.
16731
16941
 
@@ -16771,14 +16981,15 @@ var genericAgilePlugin = {
16771
16981
  - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
16772
16982
  - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
16773
16983
  - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
16774
- "*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
16984
+ "*": `You have access to feature, epic, task, sprint, and meeting tools for project coordination:
16775
16985
 
16776
16986
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
16777
16987
  **Epics** (E-xxx): Implementation work packages created by the Tech Lead, linked to approved features. Epics progress through planned \u2192 in-progress \u2192 done.
16988
+ **Tasks** (T-xxx): Concrete implementation items created by the Tech Lead, linked to epics. Tasks progress through backlog \u2192 ready \u2192 in-progress \u2192 review \u2192 done.
16778
16989
  **Sprints** (SP-xxx): Time-boxed iterations that group epics and work items with delivery dates. Sprints progress through planned \u2192 active \u2192 completed (or cancelled).
16779
16990
  **Meetings**: Meeting records with attendees, agendas, and notes.
16780
16991
 
16781
- **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags. Actions support a \`dueDate\` field for schedule tracking \u2014 actions with a past due date are automatically flagged as overdue in GAR reports. Use the \`sprints\` parameter on create_action/update_action to assign actions to sprints.
16992
+ **Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics and tasks, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags. Actions support a \`dueDate\` field for schedule tracking \u2014 actions with a past due date are automatically flagged as overdue in GAR reports. Use the \`sprints\` parameter on create_action/update_action to assign actions to sprints.
16782
16993
 
16783
16994
  - **list_meetings** / **get_meeting**: Browse and read meeting records.
16784
16995
  - **create_meeting**: Record meetings with attendees, date, and agenda. The meeting date is required \u2014 extract it from the meeting content or ask the user if not found.
@@ -16793,10 +17004,10 @@ var genericAgilePlugin = {
16793
17004
  };
16794
17005
 
16795
17006
  // src/plugins/builtin/tools/use-cases.ts
16796
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17007
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
16797
17008
  function createUseCaseTools(store) {
16798
17009
  return [
16799
- tool14(
17010
+ tool15(
16800
17011
  "list_use_cases",
16801
17012
  "List all extension use cases, optionally filtered by status or extension type",
16802
17013
  {
@@ -16826,7 +17037,7 @@ function createUseCaseTools(store) {
16826
17037
  },
16827
17038
  { annotations: { readOnlyHint: true } }
16828
17039
  ),
16829
- tool14(
17040
+ tool15(
16830
17041
  "get_use_case",
16831
17042
  "Get the full content of a specific use case by ID",
16832
17043
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -16853,7 +17064,7 @@ function createUseCaseTools(store) {
16853
17064
  },
16854
17065
  { annotations: { readOnlyHint: true } }
16855
17066
  ),
16856
- tool14(
17067
+ tool15(
16857
17068
  "create_use_case",
16858
17069
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
16859
17070
  {
@@ -16887,7 +17098,7 @@ function createUseCaseTools(store) {
16887
17098
  };
16888
17099
  }
16889
17100
  ),
16890
- tool14(
17101
+ tool15(
16891
17102
  "update_use_case",
16892
17103
  "Update an existing extension use case",
16893
17104
  {
@@ -16917,10 +17128,10 @@ function createUseCaseTools(store) {
16917
17128
  }
16918
17129
 
16919
17130
  // src/plugins/builtin/tools/tech-assessments.ts
16920
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17131
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
16921
17132
  function createTechAssessmentTools(store) {
16922
17133
  return [
16923
- tool15(
17134
+ tool16(
16924
17135
  "list_tech_assessments",
16925
17136
  "List all technology assessments, optionally filtered by status",
16926
17137
  {
@@ -16951,7 +17162,7 @@ function createTechAssessmentTools(store) {
16951
17162
  },
16952
17163
  { annotations: { readOnlyHint: true } }
16953
17164
  ),
16954
- tool15(
17165
+ tool16(
16955
17166
  "get_tech_assessment",
16956
17167
  "Get the full content of a specific technology assessment by ID",
16957
17168
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -16978,7 +17189,7 @@ function createTechAssessmentTools(store) {
16978
17189
  },
16979
17190
  { annotations: { readOnlyHint: true } }
16980
17191
  ),
16981
- tool15(
17192
+ tool16(
16982
17193
  "create_tech_assessment",
16983
17194
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
16984
17195
  {
@@ -17049,7 +17260,7 @@ function createTechAssessmentTools(store) {
17049
17260
  };
17050
17261
  }
17051
17262
  ),
17052
- tool15(
17263
+ tool16(
17053
17264
  "update_tech_assessment",
17054
17265
  "Update an existing technology assessment. The linked use case cannot be changed.",
17055
17266
  {
@@ -17079,10 +17290,10 @@ function createTechAssessmentTools(store) {
17079
17290
  }
17080
17291
 
17081
17292
  // src/plugins/builtin/tools/extension-designs.ts
17082
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17293
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17083
17294
  function createExtensionDesignTools(store) {
17084
17295
  return [
17085
- tool16(
17296
+ tool17(
17086
17297
  "list_extension_designs",
17087
17298
  "List all extension designs, optionally filtered by status",
17088
17299
  {
@@ -17112,7 +17323,7 @@ function createExtensionDesignTools(store) {
17112
17323
  },
17113
17324
  { annotations: { readOnlyHint: true } }
17114
17325
  ),
17115
- tool16(
17326
+ tool17(
17116
17327
  "get_extension_design",
17117
17328
  "Get the full content of a specific extension design by ID",
17118
17329
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -17139,7 +17350,7 @@ function createExtensionDesignTools(store) {
17139
17350
  },
17140
17351
  { annotations: { readOnlyHint: true } }
17141
17352
  ),
17142
- tool16(
17353
+ tool17(
17143
17354
  "create_extension_design",
17144
17355
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
17145
17356
  {
@@ -17207,7 +17418,7 @@ function createExtensionDesignTools(store) {
17207
17418
  };
17208
17419
  }
17209
17420
  ),
17210
- tool16(
17421
+ tool17(
17211
17422
  "update_extension_design",
17212
17423
  "Update an existing extension design. The linked tech assessment cannot be changed.",
17213
17424
  {
@@ -17236,10 +17447,10 @@ function createExtensionDesignTools(store) {
17236
17447
  }
17237
17448
 
17238
17449
  // src/plugins/builtin/tools/aem-reports.ts
17239
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17450
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17240
17451
  function createAemReportTools(store) {
17241
17452
  return [
17242
- tool17(
17453
+ tool18(
17243
17454
  "generate_extension_portfolio",
17244
17455
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
17245
17456
  {},
@@ -17291,7 +17502,7 @@ function createAemReportTools(store) {
17291
17502
  },
17292
17503
  { annotations: { readOnlyHint: true } }
17293
17504
  ),
17294
- tool17(
17505
+ tool18(
17295
17506
  "generate_tech_readiness",
17296
17507
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
17297
17508
  {},
@@ -17343,7 +17554,7 @@ function createAemReportTools(store) {
17343
17554
  },
17344
17555
  { annotations: { readOnlyHint: true } }
17345
17556
  ),
17346
- tool17(
17557
+ tool18(
17347
17558
  "generate_phase_status",
17348
17559
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
17349
17560
  {},
@@ -17405,11 +17616,11 @@ function createAemReportTools(store) {
17405
17616
  import * as fs6 from "fs";
17406
17617
  import * as path6 from "path";
17407
17618
  import * as YAML4 from "yaml";
17408
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17619
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17409
17620
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
17410
17621
  function createAemPhaseTools(store, marvinDir) {
17411
17622
  return [
17412
- tool18(
17623
+ tool19(
17413
17624
  "get_current_phase",
17414
17625
  "Get the current AEM phase from project configuration",
17415
17626
  {},
@@ -17430,7 +17641,7 @@ function createAemPhaseTools(store, marvinDir) {
17430
17641
  },
17431
17642
  { annotations: { readOnlyHint: true } }
17432
17643
  ),
17433
- tool18(
17644
+ tool19(
17434
17645
  "advance_phase",
17435
17646
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
17436
17647
  {
@@ -17694,8 +17905,8 @@ function getPluginPromptFragment(plugin, personaId) {
17694
17905
  }
17695
17906
 
17696
17907
  // src/skills/registry.ts
17697
- import * as fs7 from "fs";
17698
- import * as path7 from "path";
17908
+ import * as fs8 from "fs";
17909
+ import * as path8 from "path";
17699
17910
  import { fileURLToPath } from "url";
17700
17911
  import * as YAML5 from "yaml";
17701
17912
  import matter2 from "gray-matter";
@@ -17738,7 +17949,7 @@ Be thorough but concise. Focus on actionable insights.`,
17738
17949
  };
17739
17950
 
17740
17951
  // src/skills/builtin/jira/tools.ts
17741
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17952
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
17742
17953
 
17743
17954
  // src/skills/builtin/jira/client.ts
17744
17955
  var JiraClient = class {
@@ -17748,8 +17959,8 @@ var JiraClient = class {
17748
17959
  this.baseUrl = `https://${config2.host}/rest/api/2`;
17749
17960
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17750
17961
  }
17751
- async request(path10, method = "GET", body) {
17752
- const url2 = `${this.baseUrl}${path10}`;
17962
+ async request(path11, method = "GET", body) {
17963
+ const url2 = `${this.baseUrl}${path11}`;
17753
17964
  const headers = {
17754
17965
  Authorization: this.authHeader,
17755
17966
  "Content-Type": "application/json",
@@ -17763,7 +17974,7 @@ var JiraClient = class {
17763
17974
  if (!response.ok) {
17764
17975
  const text = await response.text().catch(() => "");
17765
17976
  throw new Error(
17766
- `Jira API error ${response.status} ${method} ${path10}: ${text}`
17977
+ `Jira API error ${response.status} ${method} ${path11}: ${text}`
17767
17978
  );
17768
17979
  }
17769
17980
  if (response.status === 204) return void 0;
@@ -17843,11 +18054,12 @@ function findByJiraKey(store, jiraKey) {
17843
18054
  const docs = store.list({ type: JIRA_TYPE });
17844
18055
  return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
17845
18056
  }
17846
- function createJiraTools(store) {
18057
+ function createJiraTools(store, projectConfig) {
17847
18058
  const jiraUserConfig = loadUserConfig().jira;
18059
+ const defaultProjectKey = projectConfig?.jira?.projectKey;
17848
18060
  return [
17849
18061
  // --- Local read tools ---
17850
- tool19(
18062
+ tool20(
17851
18063
  "list_jira_issues",
17852
18064
  "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17853
18065
  {
@@ -17875,7 +18087,7 @@ function createJiraTools(store) {
17875
18087
  },
17876
18088
  { annotations: { readOnlyHint: true } }
17877
18089
  ),
17878
- tool19(
18090
+ tool20(
17879
18091
  "get_jira_issue",
17880
18092
  "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17881
18093
  {
@@ -17908,7 +18120,7 @@ function createJiraTools(store) {
17908
18120
  { annotations: { readOnlyHint: true } }
17909
18121
  ),
17910
18122
  // --- Jira → Local tools ---
17911
- tool19(
18123
+ tool20(
17912
18124
  "pull_jira_issue",
17913
18125
  "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17914
18126
  {
@@ -17955,7 +18167,7 @@ function createJiraTools(store) {
17955
18167
  };
17956
18168
  }
17957
18169
  ),
17958
- tool19(
18170
+ tool20(
17959
18171
  "pull_jira_issues_jql",
17960
18172
  "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17961
18173
  {
@@ -18003,15 +18215,27 @@ function createJiraTools(store) {
18003
18215
  }
18004
18216
  ),
18005
18217
  // --- Local → Jira tools ---
18006
- tool19(
18218
+ tool20(
18007
18219
  "push_artifact_to_jira",
18008
18220
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
18009
18221
  {
18010
18222
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
18011
- projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
18223
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
18012
18224
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
18013
18225
  },
18014
18226
  async (args) => {
18227
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
18228
+ if (!resolvedProjectKey) {
18229
+ return {
18230
+ content: [
18231
+ {
18232
+ type: "text",
18233
+ text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
18234
+ }
18235
+ ],
18236
+ isError: true
18237
+ };
18238
+ }
18015
18239
  const jira = createJiraClient(jiraUserConfig);
18016
18240
  if (!jira) return jiraNotConfiguredError();
18017
18241
  const artifact = store.get(args.artifactId);
@@ -18031,7 +18255,7 @@ function createJiraTools(store) {
18031
18255
  `Status: ${artifact.frontmatter.status}`
18032
18256
  ].join("\n");
18033
18257
  const jiraResult = await jira.client.createIssue({
18034
- project: { key: args.projectKey },
18258
+ project: { key: resolvedProjectKey },
18035
18259
  summary: artifact.frontmatter.title,
18036
18260
  description,
18037
18261
  issuetype: { name: args.issueType ?? "Task" }
@@ -18064,7 +18288,7 @@ function createJiraTools(store) {
18064
18288
  }
18065
18289
  ),
18066
18290
  // --- Bidirectional sync ---
18067
- tool19(
18291
+ tool20(
18068
18292
  "sync_jira_issue",
18069
18293
  "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
18070
18294
  {
@@ -18105,7 +18329,7 @@ function createJiraTools(store) {
18105
18329
  }
18106
18330
  ),
18107
18331
  // --- Local link tool ---
18108
- tool19(
18332
+ tool20(
18109
18333
  "link_artifact_to_jira",
18110
18334
  "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
18111
18335
  {
@@ -18172,14 +18396,14 @@ var jiraSkill = {
18172
18396
  documentTypeRegistrations: [
18173
18397
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
18174
18398
  ],
18175
- tools: (store) => createJiraTools(store),
18399
+ tools: (store, projectConfig) => createJiraTools(store, projectConfig),
18176
18400
  promptFragments: {
18177
18401
  "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18178
18402
 
18179
18403
  **Available tools:**
18180
18404
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18181
18405
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18182
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
18406
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
18183
18407
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18184
18408
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18185
18409
 
@@ -18193,13 +18417,13 @@ var jiraSkill = {
18193
18417
  **Available tools:**
18194
18418
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18195
18419
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18196
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
18420
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
18197
18421
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18198
18422
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18199
18423
 
18200
18424
  **As Tech Lead, use Jira integration to:**
18201
18425
  - Pull technical issues and bugs for sprint planning and estimation
18202
- - Push epics and technical decisions to Jira for cross-team visibility
18426
+ - Push epics, tasks, and technical decisions to Jira for cross-team visibility
18203
18427
  - Bidirectional sync to keep local governance and Jira in alignment
18204
18428
  - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
18205
18429
  "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
@@ -18207,74 +18431,478 @@ var jiraSkill = {
18207
18431
  **Available tools:**
18208
18432
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18209
18433
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18210
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
18434
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
18211
18435
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18212
18436
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18213
18437
 
18214
18438
  **As Delivery Manager, use Jira integration to:**
18215
18439
  - Pull sprint issues for tracking progress and blockers
18216
- - Push actions and decisions to Jira for stakeholder visibility
18440
+ - Push actions, decisions, and tasks to Jira for stakeholder visibility
18217
18441
  - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
18218
18442
  - Sync status between Marvin governance items and Jira issues`
18219
18443
  }
18220
18444
  };
18221
18445
 
18222
- // src/skills/registry.ts
18223
- var BUILTIN_SKILLS = {
18224
- "governance-review": governanceReviewSkill,
18225
- "jira": jiraSkill
18446
+ // src/skills/builtin/prd-generator/tools.ts
18447
+ import * as fs7 from "fs";
18448
+ import * as path7 from "path";
18449
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
18450
+ var PRIORITY_ORDER2 = {
18451
+ critical: 0,
18452
+ high: 1,
18453
+ medium: 2,
18454
+ low: 3
18226
18455
  };
18227
- function getBuiltinSkillsDir() {
18228
- const thisFile = fileURLToPath(import.meta.url);
18229
- return path7.join(path7.dirname(thisFile), "builtin");
18230
- }
18231
- function loadSkillFromDirectory(dirPath) {
18232
- const skillMdPath = path7.join(dirPath, "SKILL.md");
18233
- if (!fs7.existsSync(skillMdPath)) return void 0;
18234
- try {
18235
- const raw = fs7.readFileSync(skillMdPath, "utf-8");
18236
- const { data, content } = matter2(raw);
18237
- if (!data.name || !data.description) return void 0;
18238
- const metadata = data.metadata ?? {};
18239
- const version2 = metadata.version ?? "1.0.0";
18240
- const personas = metadata.personas;
18241
- const promptFragments = {};
18242
- const wildcardPrompt = content.trim();
18243
- if (wildcardPrompt) {
18244
- promptFragments["*"] = wildcardPrompt;
18245
- }
18246
- const personasDir = path7.join(dirPath, "personas");
18247
- if (fs7.existsSync(personasDir)) {
18248
- try {
18249
- for (const file2 of fs7.readdirSync(personasDir)) {
18250
- if (!file2.endsWith(".md")) continue;
18251
- const personaId = file2.replace(/\.md$/, "");
18252
- const personaPrompt = fs7.readFileSync(path7.join(personasDir, file2), "utf-8").trim();
18253
- if (personaPrompt) {
18254
- promptFragments[personaId] = personaPrompt;
18456
+ function priorityRank2(p) {
18457
+ return PRIORITY_ORDER2[p ?? ""] ?? 99;
18458
+ }
18459
+ function gatherContext(store, focusFeature, includeDecisions = true, includeQuestions = true) {
18460
+ const allFeatures = store.list({ type: "feature" });
18461
+ const allEpics = store.list({ type: "epic" });
18462
+ const allTasks = store.list({ type: "task" });
18463
+ const allDecisions = includeDecisions ? store.list({ type: "decision" }) : [];
18464
+ const allQuestions = includeQuestions ? store.list({ type: "question" }) : [];
18465
+ const allActions = store.list({ type: "action" });
18466
+ let features = allFeatures;
18467
+ let epics = allEpics;
18468
+ let tasks = allTasks;
18469
+ if (focusFeature) {
18470
+ features = features.filter((f) => f.frontmatter.id === focusFeature);
18471
+ const featureIds = new Set(features.map((f) => f.frontmatter.id));
18472
+ epics = epics.filter(
18473
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).some((id) => featureIds.has(id))
18474
+ );
18475
+ const epicIds2 = new Set(epics.map((e) => e.frontmatter.id));
18476
+ tasks = tasks.filter(
18477
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).some((id) => epicIds2.has(id))
18478
+ );
18479
+ }
18480
+ const featuresByStatus = {};
18481
+ for (const f of features) {
18482
+ featuresByStatus[f.frontmatter.status] = (featuresByStatus[f.frontmatter.status] ?? 0) + 1;
18483
+ }
18484
+ const epicsByStatus = {};
18485
+ for (const e of epics) {
18486
+ epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
18487
+ }
18488
+ const epicIds = new Set(epics.map((e) => e.frontmatter.id));
18489
+ return {
18490
+ features: features.sort((a, b) => priorityRank2(a.frontmatter.priority) - priorityRank2(b.frontmatter.priority)).map((f) => ({
18491
+ id: f.frontmatter.id,
18492
+ title: f.frontmatter.title,
18493
+ status: f.frontmatter.status,
18494
+ priority: f.frontmatter.priority ?? "medium",
18495
+ content: f.content,
18496
+ linkedEpicCount: epics.filter(
18497
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
18498
+ ).length
18499
+ })),
18500
+ epics: epics.map((e) => ({
18501
+ id: e.frontmatter.id,
18502
+ title: e.frontmatter.title,
18503
+ status: e.frontmatter.status,
18504
+ linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
18505
+ targetDate: e.frontmatter.targetDate ?? null,
18506
+ estimatedEffort: e.frontmatter.estimatedEffort ?? null,
18507
+ content: e.content,
18508
+ linkedTaskCount: tasks.filter(
18509
+ (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
18510
+ ).length
18511
+ })),
18512
+ tasks: tasks.map((t) => ({
18513
+ id: t.frontmatter.id,
18514
+ title: t.frontmatter.title,
18515
+ status: t.frontmatter.status,
18516
+ linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
18517
+ acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
18518
+ technicalNotes: t.frontmatter.technicalNotes ?? null,
18519
+ complexity: t.frontmatter.complexity ?? null,
18520
+ estimatedPoints: t.frontmatter.estimatedPoints ?? null,
18521
+ priority: t.frontmatter.priority ?? null
18522
+ })),
18523
+ decisions: allDecisions.map((d) => ({
18524
+ id: d.frontmatter.id,
18525
+ title: d.frontmatter.title,
18526
+ status: d.frontmatter.status,
18527
+ content: d.content
18528
+ })),
18529
+ questions: allQuestions.map((q) => ({
18530
+ id: q.frontmatter.id,
18531
+ title: q.frontmatter.title,
18532
+ status: q.frontmatter.status,
18533
+ content: q.content
18534
+ })),
18535
+ actions: allActions.filter((a) => {
18536
+ if (!focusFeature) return true;
18537
+ const tags = a.frontmatter.tags ?? [];
18538
+ return tags.some((t) => t.startsWith("epic:") && epicIds.has(t.replace("epic:", "")));
18539
+ }).map((a) => ({
18540
+ id: a.frontmatter.id,
18541
+ title: a.frontmatter.title,
18542
+ status: a.frontmatter.status,
18543
+ owner: a.frontmatter.owner ?? null,
18544
+ priority: a.frontmatter.priority ?? null,
18545
+ dueDate: a.frontmatter.dueDate ?? null
18546
+ })),
18547
+ summary: {
18548
+ totalFeatures: features.length,
18549
+ totalEpics: epics.length,
18550
+ totalTasks: tasks.length,
18551
+ featuresByStatus,
18552
+ epicsByStatus
18553
+ }
18554
+ };
18555
+ }
18556
+ function generateTaskMasterPrd(title, ctx, projectOverview) {
18557
+ const lines = [];
18558
+ lines.push(`# ${title}`);
18559
+ lines.push("");
18560
+ lines.push("## Project Overview");
18561
+ if (projectOverview) {
18562
+ lines.push(projectOverview);
18563
+ } else if (ctx.features.length > 0) {
18564
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
18565
+ }
18566
+ lines.push("");
18567
+ lines.push("## Goals");
18568
+ for (const f of ctx.features) {
18569
+ lines.push(`- **${f.title}** (${f.id}, Priority: ${f.priority}) \u2014 ${f.status}`);
18570
+ }
18571
+ lines.push("");
18572
+ lines.push("## Features and Requirements");
18573
+ lines.push("");
18574
+ for (const feature of ctx.features) {
18575
+ lines.push(`### ${feature.title} (${feature.id}) \u2014 Priority: ${feature.priority}`);
18576
+ lines.push("");
18577
+ if (feature.content) {
18578
+ lines.push(feature.content);
18579
+ lines.push("");
18580
+ }
18581
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
18582
+ if (featureEpics.length > 0) {
18583
+ lines.push("#### User Stories / Epics");
18584
+ lines.push("");
18585
+ for (const epic of featureEpics) {
18586
+ const effort = epic.estimatedEffort ? `, Effort: ${epic.estimatedEffort}` : "";
18587
+ lines.push(`- **${epic.id}: ${epic.title}** \u2014 Status: ${epic.status}${effort}`);
18588
+ if (epic.content) {
18589
+ lines.push(` ${epic.content.split("\n")[0]}`);
18590
+ }
18591
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
18592
+ if (epicTasks.length > 0) {
18593
+ lines.push("");
18594
+ lines.push("#### Implementation Tasks");
18595
+ lines.push("");
18596
+ for (const task of epicTasks) {
18597
+ const complexity = task.complexity ? `, Complexity: ${task.complexity}` : "";
18598
+ const points = task.estimatedPoints != null ? `, Points: ${task.estimatedPoints}` : "";
18599
+ lines.push(`- **${task.id}: ${task.title}**${complexity}${points}`);
18600
+ if (task.acceptanceCriteria) {
18601
+ lines.push(` Acceptance Criteria: ${task.acceptanceCriteria}`);
18602
+ }
18255
18603
  }
18256
18604
  }
18257
- } catch {
18258
18605
  }
18606
+ lines.push("");
18259
18607
  }
18260
- let actions;
18261
- const actionsPath = path7.join(dirPath, "actions.yaml");
18262
- if (fs7.existsSync(actionsPath)) {
18263
- try {
18264
- const actionsRaw = fs7.readFileSync(actionsPath, "utf-8");
18265
- actions = YAML5.parse(actionsRaw);
18266
- } catch {
18608
+ }
18609
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
18610
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
18611
+ const technicalNotes = ctx.tasks.filter((t) => t.technicalNotes).map((t) => `- **${t.id}**: ${t.technicalNotes}`);
18612
+ if (approvedDecisions.length > 0 || openQuestions.length > 0 || technicalNotes.length > 0) {
18613
+ lines.push("## Technical Considerations");
18614
+ lines.push("");
18615
+ if (approvedDecisions.length > 0) {
18616
+ lines.push("### Key Decisions");
18617
+ for (const d of approvedDecisions) {
18618
+ lines.push(`- **${d.id}: ${d.title}** \u2014 ${d.content.split("\n")[0]}`);
18267
18619
  }
18620
+ lines.push("");
18268
18621
  }
18269
- return {
18270
- id: data.name,
18271
- name: data.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
18272
- description: data.description,
18273
- version: version2,
18274
- format: "skill-md",
18275
- dirPath,
18276
- personas,
18277
- promptFragments: Object.keys(promptFragments).length > 0 ? promptFragments : void 0,
18622
+ if (technicalNotes.length > 0) {
18623
+ lines.push("### Technical Notes");
18624
+ for (const note of technicalNotes) {
18625
+ lines.push(note);
18626
+ }
18627
+ lines.push("");
18628
+ }
18629
+ if (openQuestions.length > 0) {
18630
+ lines.push("### Open Questions");
18631
+ for (const q of openQuestions) {
18632
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
18633
+ }
18634
+ lines.push("");
18635
+ }
18636
+ }
18637
+ lines.push("## Implementation Priorities");
18638
+ lines.push("");
18639
+ let priorityIdx = 1;
18640
+ for (const feature of ctx.features) {
18641
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
18642
+ const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
18643
+ return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
18644
+ });
18645
+ if (featureEpics.length === 0) continue;
18646
+ lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
18647
+ for (const epic of featureEpics) {
18648
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
18649
+ lines.push(` - ${epic.id}: ${epic.title} (${epic.status}) \u2014 ${epicTasks.length} task(s)`);
18650
+ }
18651
+ priorityIdx++;
18652
+ }
18653
+ lines.push("");
18654
+ return lines.join("\n");
18655
+ }
18656
+ function generateClaudeCodePrd(title, ctx, projectOverview) {
18657
+ const lines = [];
18658
+ lines.push(`# ${title}`);
18659
+ lines.push("");
18660
+ lines.push("## Overview");
18661
+ if (projectOverview) {
18662
+ lines.push(projectOverview);
18663
+ } else if (ctx.features.length > 0) {
18664
+ lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
18665
+ }
18666
+ lines.push("");
18667
+ const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
18668
+ if (approvedDecisions.length > 0) {
18669
+ lines.push("## Architecture & Technical Decisions");
18670
+ lines.push("");
18671
+ for (const d of approvedDecisions) {
18672
+ lines.push(`### ${d.id}: ${d.title}`);
18673
+ lines.push(d.content);
18674
+ lines.push("");
18675
+ }
18676
+ }
18677
+ lines.push("## Implementation Plan");
18678
+ lines.push("");
18679
+ const priorityGroups = {};
18680
+ for (const f of ctx.features) {
18681
+ const group = f.priority === "critical" || f.priority === "high" ? "Phase 1: High Priority" : "Phase 2: Medium & Low Priority";
18682
+ if (!priorityGroups[group]) priorityGroups[group] = [];
18683
+ priorityGroups[group].push(f);
18684
+ }
18685
+ for (const [phase, features] of Object.entries(priorityGroups)) {
18686
+ lines.push(`### ${phase}`);
18687
+ lines.push("");
18688
+ for (const feature of features) {
18689
+ const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
18690
+ for (const epic of featureEpics) {
18691
+ lines.push(`- [ ] ${epic.id}: ${epic.title}`);
18692
+ const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
18693
+ for (const task of epicTasks) {
18694
+ const complexity = task.complexity ? `complexity: ${task.complexity}` : "";
18695
+ const points = task.estimatedPoints != null ? `points: ${task.estimatedPoints}` : "";
18696
+ const meta3 = [complexity, points].filter(Boolean).join(", ");
18697
+ lines.push(` - [ ] ${task.id}: ${task.title}${meta3 ? ` (${meta3})` : ""}`);
18698
+ if (task.acceptanceCriteria) {
18699
+ lines.push(` - Acceptance: ${task.acceptanceCriteria}`);
18700
+ }
18701
+ if (task.technicalNotes) {
18702
+ lines.push(` - Notes: ${task.technicalNotes}`);
18703
+ }
18704
+ }
18705
+ }
18706
+ }
18707
+ lines.push("");
18708
+ }
18709
+ const openQuestions = ctx.questions.filter((q) => q.status === "open");
18710
+ if (openQuestions.length > 0) {
18711
+ lines.push("## Open Questions");
18712
+ lines.push("");
18713
+ for (const q of openQuestions) {
18714
+ lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
18715
+ }
18716
+ lines.push("");
18717
+ }
18718
+ return lines.join("\n");
18719
+ }
18720
+ function createPrdTools(store) {
18721
+ return [
18722
+ tool21(
18723
+ "gather_prd_context",
18724
+ "Aggregate all governance artifacts (features, epics, tasks, decisions, questions, actions) into structured JSON for PRD generation",
18725
+ {
18726
+ focusFeature: external_exports.string().optional().describe("Filter context to a specific feature ID (e.g. 'F-001')"),
18727
+ includeDecisions: external_exports.boolean().optional().describe("Include decisions in context (default: true)"),
18728
+ includeQuestions: external_exports.boolean().optional().describe("Include questions in context (default: true)")
18729
+ },
18730
+ async (args) => {
18731
+ const ctx = gatherContext(store, args.focusFeature, args.includeDecisions ?? true, args.includeQuestions ?? true);
18732
+ return {
18733
+ content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }]
18734
+ };
18735
+ },
18736
+ { annotations: { readOnlyHint: true } }
18737
+ ),
18738
+ tool21(
18739
+ "generate_prd",
18740
+ "Generate a PRD document from governance artifacts and save it as a PRD-xxx document",
18741
+ {
18742
+ title: external_exports.string().describe("PRD title"),
18743
+ format: external_exports.enum(["taskmaster", "claude-code"]).describe("Output format: 'taskmaster' for Claude TaskMaster parse_prd, 'claude-code' for Claude Code consumption"),
18744
+ projectOverview: external_exports.string().optional().describe("Project overview text (synthesized from features if not provided)"),
18745
+ focusFeature: external_exports.string().optional().describe("Focus on a specific feature ID (e.g. 'F-001')"),
18746
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for the PRD document")
18747
+ },
18748
+ async (args) => {
18749
+ const ctx = gatherContext(store, args.focusFeature);
18750
+ const prdContent = args.format === "taskmaster" ? generateTaskMasterPrd(args.title, ctx, args.projectOverview) : generateClaudeCodePrd(args.title, ctx, args.projectOverview);
18751
+ const frontmatter = {
18752
+ title: args.title,
18753
+ status: "draft",
18754
+ format: args.format
18755
+ };
18756
+ if (args.focusFeature) frontmatter.focusFeature = args.focusFeature;
18757
+ if (args.tags) frontmatter.tags = args.tags;
18758
+ const doc = store.create("prd", frontmatter, prdContent);
18759
+ return {
18760
+ content: [
18761
+ {
18762
+ type: "text",
18763
+ text: `Generated PRD ${doc.frontmatter.id}: "${args.title}" (format: ${args.format}, ${ctx.summary.totalFeatures} features, ${ctx.summary.totalEpics} epics, ${ctx.summary.totalTasks} tasks)`
18764
+ }
18765
+ ]
18766
+ };
18767
+ }
18768
+ ),
18769
+ tool21(
18770
+ "export_prd",
18771
+ "Export a PRD document to a file path for external consumption (e.g. by Claude TaskMaster or Claude Code)",
18772
+ {
18773
+ prdId: external_exports.string().describe("PRD document ID (e.g. 'PRD-001')"),
18774
+ outputPath: external_exports.string().describe("File path to write the PRD content to")
18775
+ },
18776
+ async (args) => {
18777
+ const doc = store.get(args.prdId);
18778
+ if (!doc) {
18779
+ return {
18780
+ content: [{ type: "text", text: `PRD ${args.prdId} not found` }],
18781
+ isError: true
18782
+ };
18783
+ }
18784
+ const outputDir = path7.dirname(args.outputPath);
18785
+ fs7.mkdirSync(outputDir, { recursive: true });
18786
+ fs7.writeFileSync(args.outputPath, doc.content, "utf-8");
18787
+ return {
18788
+ content: [
18789
+ {
18790
+ type: "text",
18791
+ text: `Exported PRD ${args.prdId} to ${args.outputPath}`
18792
+ }
18793
+ ]
18794
+ };
18795
+ }
18796
+ )
18797
+ ];
18798
+ }
18799
+
18800
+ // src/skills/builtin/prd-generator/index.ts
18801
+ var prdGeneratorSkill = {
18802
+ id: "prd-generator",
18803
+ name: "PRD Generator",
18804
+ description: "Generate PRDs from governance artifacts for TaskMaster or Claude Code",
18805
+ version: "1.0.0",
18806
+ format: "builtin-ts",
18807
+ documentTypeRegistrations: [
18808
+ { type: "prd", dirName: "prds", idPrefix: "PRD" }
18809
+ ],
18810
+ tools: (store) => createPrdTools(store),
18811
+ promptFragments: {
18812
+ "tech-lead": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
18813
+
18814
+ **Available tools:**
18815
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, decisions, questions, and actions into structured JSON for analysis
18816
+ - \`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)
18817
+ - \`export_prd\` \u2014 export a PRD document to a file path for external use
18818
+
18819
+ **As Tech Lead, use PRD generation to:**
18820
+ - Create comprehensive PRDs that capture the full governance context
18821
+ - Export TaskMaster-format PRDs for automated task breakdown via \`parse_prd\`
18822
+ - Export Claude Code-format PRDs as implementation plans with checklists
18823
+ - Focus PRDs on specific features using the focusFeature parameter`,
18824
+ "delivery-manager": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
18825
+
18826
+ **Available tools:**
18827
+ - \`gather_prd_context\` \u2014 aggregate all governance artifacts into structured JSON for review
18828
+ - \`generate_prd\` \u2014 generate a formatted PRD document (taskmaster or claude-code format)
18829
+ - \`export_prd\` \u2014 export a PRD to a file path
18830
+
18831
+ **As Delivery Manager, use PRD generation to:**
18832
+ - Generate PRDs for stakeholder communication and project documentation
18833
+ - Review aggregated project context before sprint planning
18834
+ - Export PRDs to share with external teams or tools`,
18835
+ "product-owner": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
18836
+
18837
+ **Available tools:**
18838
+ - \`gather_prd_context\` \u2014 aggregate features, epics, tasks, and decisions into structured JSON
18839
+ - \`generate_prd\` \u2014 generate a formatted PRD document
18840
+ - \`export_prd\` \u2014 export a PRD to a file path
18841
+
18842
+ **As Product Owner, use PRD generation to:**
18843
+ - Generate PRDs that capture feature requirements and priorities
18844
+ - Review the complete governance context for product planning
18845
+ - Export PRDs for stakeholder review and sign-off`
18846
+ }
18847
+ };
18848
+
18849
+ // src/skills/registry.ts
18850
+ var BUILTIN_SKILLS = {
18851
+ "governance-review": governanceReviewSkill,
18852
+ "jira": jiraSkill,
18853
+ "prd-generator": prdGeneratorSkill
18854
+ };
18855
+ function getBuiltinSkillsDir() {
18856
+ const thisFile = fileURLToPath(import.meta.url);
18857
+ return path8.join(path8.dirname(thisFile), "builtin");
18858
+ }
18859
+ function loadSkillFromDirectory(dirPath) {
18860
+ const skillMdPath = path8.join(dirPath, "SKILL.md");
18861
+ if (!fs8.existsSync(skillMdPath)) return void 0;
18862
+ try {
18863
+ const raw = fs8.readFileSync(skillMdPath, "utf-8");
18864
+ const { data, content } = matter2(raw);
18865
+ if (!data.name || !data.description) return void 0;
18866
+ const metadata = data.metadata ?? {};
18867
+ const version2 = metadata.version ?? "1.0.0";
18868
+ const personas = metadata.personas;
18869
+ const promptFragments = {};
18870
+ const wildcardPrompt = content.trim();
18871
+ if (wildcardPrompt) {
18872
+ promptFragments["*"] = wildcardPrompt;
18873
+ }
18874
+ const personasDir = path8.join(dirPath, "personas");
18875
+ if (fs8.existsSync(personasDir)) {
18876
+ try {
18877
+ for (const file2 of fs8.readdirSync(personasDir)) {
18878
+ if (!file2.endsWith(".md")) continue;
18879
+ const personaId = file2.replace(/\.md$/, "");
18880
+ const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
18881
+ if (personaPrompt) {
18882
+ promptFragments[personaId] = personaPrompt;
18883
+ }
18884
+ }
18885
+ } catch {
18886
+ }
18887
+ }
18888
+ let actions;
18889
+ const actionsPath = path8.join(dirPath, "actions.yaml");
18890
+ if (fs8.existsSync(actionsPath)) {
18891
+ try {
18892
+ const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
18893
+ actions = YAML5.parse(actionsRaw);
18894
+ } catch {
18895
+ }
18896
+ }
18897
+ return {
18898
+ id: data.name,
18899
+ name: data.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
18900
+ description: data.description,
18901
+ version: version2,
18902
+ format: "skill-md",
18903
+ dirPath,
18904
+ personas,
18905
+ promptFragments: Object.keys(promptFragments).length > 0 ? promptFragments : void 0,
18278
18906
  actions
18279
18907
  };
18280
18908
  } catch {
@@ -18288,10 +18916,10 @@ function loadAllSkills(marvinDir) {
18288
18916
  }
18289
18917
  try {
18290
18918
  const builtinDir = getBuiltinSkillsDir();
18291
- if (fs7.existsSync(builtinDir)) {
18292
- for (const entry of fs7.readdirSync(builtinDir)) {
18293
- const entryPath = path7.join(builtinDir, entry);
18294
- if (!fs7.statSync(entryPath).isDirectory()) continue;
18919
+ if (fs8.existsSync(builtinDir)) {
18920
+ for (const entry of fs8.readdirSync(builtinDir)) {
18921
+ const entryPath = path8.join(builtinDir, entry);
18922
+ if (!fs8.statSync(entryPath).isDirectory()) continue;
18295
18923
  if (skills.has(entry)) continue;
18296
18924
  const skill = loadSkillFromDirectory(entryPath);
18297
18925
  if (skill) skills.set(skill.id, skill);
@@ -18300,18 +18928,18 @@ function loadAllSkills(marvinDir) {
18300
18928
  } catch {
18301
18929
  }
18302
18930
  if (marvinDir) {
18303
- const skillsDir = path7.join(marvinDir, "skills");
18304
- if (fs7.existsSync(skillsDir)) {
18931
+ const skillsDir = path8.join(marvinDir, "skills");
18932
+ if (fs8.existsSync(skillsDir)) {
18305
18933
  let entries;
18306
18934
  try {
18307
- entries = fs7.readdirSync(skillsDir);
18935
+ entries = fs8.readdirSync(skillsDir);
18308
18936
  } catch {
18309
18937
  entries = [];
18310
18938
  }
18311
18939
  for (const entry of entries) {
18312
- const entryPath = path7.join(skillsDir, entry);
18940
+ const entryPath = path8.join(skillsDir, entry);
18313
18941
  try {
18314
- if (fs7.statSync(entryPath).isDirectory()) {
18942
+ if (fs8.statSync(entryPath).isDirectory()) {
18315
18943
  const skill = loadSkillFromDirectory(entryPath);
18316
18944
  if (skill) skills.set(skill.id, skill);
18317
18945
  continue;
@@ -18321,7 +18949,7 @@ function loadAllSkills(marvinDir) {
18321
18949
  }
18322
18950
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
18323
18951
  try {
18324
- const raw = fs7.readFileSync(entryPath, "utf-8");
18952
+ const raw = fs8.readFileSync(entryPath, "utf-8");
18325
18953
  const parsed = YAML5.parse(raw);
18326
18954
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
18327
18955
  const skill = {
@@ -18364,12 +18992,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
18364
18992
  }
18365
18993
  return registrations;
18366
18994
  }
18367
- function getSkillTools(skillIds, allSkills, store) {
18995
+ function getSkillTools(skillIds, allSkills, store, projectConfig) {
18368
18996
  const tools = [];
18369
18997
  for (const id of skillIds) {
18370
18998
  const skill = allSkills.get(id);
18371
18999
  if (skill?.tools) {
18372
- tools.push(...skill.tools(store));
19000
+ tools.push(...skill.tools(store, projectConfig));
18373
19001
  }
18374
19002
  }
18375
19003
  return tools;
@@ -18389,7 +19017,7 @@ ${fragment}`);
18389
19017
  }
18390
19018
 
18391
19019
  // src/skills/action-tools.ts
18392
- import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
19020
+ import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
18393
19021
 
18394
19022
  // src/skills/action-runner.ts
18395
19023
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -18401,7 +19029,7 @@ import {
18401
19029
 
18402
19030
  // src/agent/tools/web.ts
18403
19031
  import * as http2 from "http";
18404
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19032
+ import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
18405
19033
 
18406
19034
  // src/web/data.ts
18407
19035
  function getOverviewData(store) {
@@ -18514,6 +19142,204 @@ function getDiagramData(store) {
18514
19142
  }
18515
19143
  return { sprints, epics, features, statusCounts };
18516
19144
  }
19145
+ function computeUrgency(dueDateStr, todayStr) {
19146
+ const due = new Date(dueDateStr).getTime();
19147
+ const today = new Date(todayStr).getTime();
19148
+ const diffDays = Math.floor((due - today) / 864e5);
19149
+ if (diffDays < 0) return "overdue";
19150
+ if (diffDays <= 3) return "due-3d";
19151
+ if (diffDays <= 7) return "due-7d";
19152
+ if (diffDays <= 14) return "upcoming";
19153
+ return "later";
19154
+ }
19155
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19156
+ function getUpcomingData(store) {
19157
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19158
+ const allDocs = store.list();
19159
+ const docById = /* @__PURE__ */ new Map();
19160
+ for (const doc of allDocs) {
19161
+ docById.set(doc.frontmatter.id, doc);
19162
+ }
19163
+ const actions = allDocs.filter(
19164
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
19165
+ );
19166
+ const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
19167
+ const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
19168
+ const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
19169
+ const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
19170
+ const epicToTasks = /* @__PURE__ */ new Map();
19171
+ for (const task of tasks) {
19172
+ const tags = task.frontmatter.tags ?? [];
19173
+ for (const tag of tags) {
19174
+ if (tag.startsWith("epic:")) {
19175
+ const epicId = tag.slice(5);
19176
+ if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
19177
+ epicToTasks.get(epicId).push(task);
19178
+ }
19179
+ }
19180
+ }
19181
+ function getSprintTasks(sprintDoc) {
19182
+ const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
19183
+ const result = [];
19184
+ for (const epicId of linkedEpics) {
19185
+ const epicTasks = epicToTasks.get(epicId) ?? [];
19186
+ result.push(...epicTasks);
19187
+ }
19188
+ return result;
19189
+ }
19190
+ function countRelatedTasks(actionDoc) {
19191
+ const actionTags = actionDoc.frontmatter.tags ?? [];
19192
+ const relatedTaskIds = /* @__PURE__ */ new Set();
19193
+ for (const tag of actionTags) {
19194
+ if (tag.startsWith("sprint:")) {
19195
+ const sprintId = tag.slice(7);
19196
+ const sprint = docById.get(sprintId);
19197
+ if (sprint) {
19198
+ const sprintTaskDocs = getSprintTasks(sprint);
19199
+ for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
19200
+ }
19201
+ }
19202
+ }
19203
+ return relatedTaskIds.size;
19204
+ }
19205
+ const dueSoonActions = actionsWithDue.map((d) => ({
19206
+ id: d.frontmatter.id,
19207
+ title: d.frontmatter.title,
19208
+ status: d.frontmatter.status,
19209
+ owner: d.frontmatter.owner,
19210
+ dueDate: d.frontmatter.dueDate,
19211
+ urgency: computeUrgency(d.frontmatter.dueDate, today),
19212
+ relatedTaskCount: countRelatedTasks(d)
19213
+ })).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
19214
+ const todayMs = new Date(today).getTime();
19215
+ const fourteenDaysMs = 14 * 864e5;
19216
+ const nearSprints = sprints.filter((s) => {
19217
+ const endDate = s.frontmatter.endDate;
19218
+ if (!endDate) return false;
19219
+ const endMs = new Date(endDate).getTime();
19220
+ const diff = endMs - todayMs;
19221
+ return diff >= 0 && diff <= fourteenDaysMs;
19222
+ });
19223
+ const taskSprintMap = /* @__PURE__ */ new Map();
19224
+ for (const sprint of nearSprints) {
19225
+ const sprintEnd = sprint.frontmatter.endDate;
19226
+ const sprintTaskDocs = getSprintTasks(sprint);
19227
+ for (const task of sprintTaskDocs) {
19228
+ if (DONE_STATUSES.has(task.frontmatter.status)) continue;
19229
+ const existing = taskSprintMap.get(task.frontmatter.id);
19230
+ if (!existing || sprintEnd < existing.sprintEnd) {
19231
+ taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
19232
+ }
19233
+ }
19234
+ }
19235
+ const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
19236
+ id: task.frontmatter.id,
19237
+ title: task.frontmatter.title,
19238
+ status: task.frontmatter.status,
19239
+ sprintId: sprint.frontmatter.id,
19240
+ sprintTitle: sprint.frontmatter.title,
19241
+ sprintEndDate: sprintEnd,
19242
+ urgency: computeUrgency(sprintEnd, today)
19243
+ })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
19244
+ const openItems = allDocs.filter(
19245
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
19246
+ );
19247
+ const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
19248
+ const recentMeetings = allDocs.filter(
19249
+ (d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
19250
+ );
19251
+ const crossRefCounts = /* @__PURE__ */ new Map();
19252
+ for (const doc of allDocs) {
19253
+ const content = doc.content ?? "";
19254
+ for (const item of openItems) {
19255
+ if (doc.frontmatter.id === item.frontmatter.id) continue;
19256
+ if (content.includes(item.frontmatter.id)) {
19257
+ crossRefCounts.set(
19258
+ item.frontmatter.id,
19259
+ (crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
19260
+ );
19261
+ }
19262
+ }
19263
+ }
19264
+ const activeSprints = sprints.filter((s) => {
19265
+ const status = s.frontmatter.status;
19266
+ if (status === "active") return true;
19267
+ const startDate = s.frontmatter.startDate;
19268
+ if (!startDate) return false;
19269
+ const startMs = new Date(startDate).getTime();
19270
+ const diff = startMs - todayMs;
19271
+ return diff >= 0 && diff <= fourteenDaysMs;
19272
+ });
19273
+ const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
19274
+ const activeEpicIds = /* @__PURE__ */ new Set();
19275
+ for (const s of activeSprints) {
19276
+ for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
19277
+ activeEpicIds.add(epicId);
19278
+ }
19279
+ }
19280
+ const trending = openItems.map((doc) => {
19281
+ const signals = [];
19282
+ let score = 0;
19283
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
19284
+ const ageDays = daysBetween(updated, today);
19285
+ const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
19286
+ if (recencyPts > 0) {
19287
+ signals.push({ factor: "recency", points: recencyPts });
19288
+ score += recencyPts;
19289
+ }
19290
+ const tags = doc.frontmatter.tags ?? [];
19291
+ const linkedToActiveSprint = tags.some(
19292
+ (t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
19293
+ );
19294
+ const linkedToActiveEpic = tags.some(
19295
+ (t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
19296
+ );
19297
+ if (linkedToActiveSprint) {
19298
+ signals.push({ factor: "sprint proximity", points: 25 });
19299
+ score += 25;
19300
+ } else if (linkedToActiveEpic) {
19301
+ signals.push({ factor: "sprint proximity", points: 15 });
19302
+ score += 15;
19303
+ }
19304
+ const mentionCount = recentMeetings.filter(
19305
+ (m) => (m.content ?? "").includes(doc.frontmatter.id)
19306
+ ).length;
19307
+ if (mentionCount > 0) {
19308
+ const meetingPts = Math.min(15, mentionCount * 5);
19309
+ signals.push({ factor: "meeting mentions", points: meetingPts });
19310
+ score += meetingPts;
19311
+ }
19312
+ const priority = doc.frontmatter.priority?.toLowerCase();
19313
+ const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
19314
+ if (priorityPts > 0) {
19315
+ signals.push({ factor: "priority", points: priorityPts });
19316
+ score += priorityPts;
19317
+ }
19318
+ if (["action", "question"].includes(doc.frontmatter.type)) {
19319
+ const createdDays = daysBetween(doc.frontmatter.created, today);
19320
+ if (createdDays >= 14) {
19321
+ const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
19322
+ signals.push({ factor: "aging", points: agingPts });
19323
+ score += agingPts;
19324
+ }
19325
+ }
19326
+ const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
19327
+ if (refs > 0) {
19328
+ const crossRefPts = Math.min(15, refs * 5);
19329
+ signals.push({ factor: "cross-references", points: crossRefPts });
19330
+ score += crossRefPts;
19331
+ }
19332
+ return {
19333
+ id: doc.frontmatter.id,
19334
+ title: doc.frontmatter.title,
19335
+ type: doc.frontmatter.type,
19336
+ status: doc.frontmatter.status,
19337
+ score,
19338
+ signals
19339
+ };
19340
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
19341
+ return { dueSoonActions, dueSoonSprintTasks, trending };
19342
+ }
18517
19343
 
18518
19344
  // src/web/templates/layout.ts
18519
19345
  function escapeHtml(str) {
@@ -18637,6 +19463,8 @@ function inline(text) {
18637
19463
  function layout(opts, body) {
18638
19464
  const topItems = [
18639
19465
  { href: "/", label: "Overview" },
19466
+ { href: "/upcoming", label: "Upcoming" },
19467
+ { href: "/timeline", label: "Timeline" },
18640
19468
  { href: "/board", label: "Board" },
18641
19469
  { href: "/gar", label: "GAR Report" },
18642
19470
  { href: "/health", label: "Health" }
@@ -18673,7 +19501,7 @@ function layout(opts, body) {
18673
19501
  ${groupsHtml}
18674
19502
  </nav>
18675
19503
  </aside>
18676
- <main class="main">
19504
+ <main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
18677
19505
  <button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
18678
19506
  <svg class="icon-expand" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M1 1h5v1.5H3.56l3.72 3.72-1.06 1.06L2.5 3.56V6H1V1zm14 14h-5v-1.5h2.44l-3.72-3.72 1.06-1.06 3.72 3.72V10H15v5z"/></svg>
18679
19507
  <svg class="icon-collapse" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M6 7H1V5.5h2.44L0.22 2.28l1.06-1.06L4.5 4.44V2H6v5zm4-1h5v1.5h-2.44l3.22 3.22-1.06 1.06L11.5 8.56V11H10V6z"/></svg>
@@ -18682,7 +19510,36 @@ function layout(opts, body) {
18682
19510
  </main>
18683
19511
  </div>
18684
19512
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18685
- <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
19513
+ <script>mermaid.initialize({
19514
+ startOnLoad: true,
19515
+ theme: 'dark',
19516
+ themeVariables: {
19517
+ background: '#1a1d27',
19518
+ primaryColor: '#2a2e3a',
19519
+ sectionBkgColor: '#1a1d27',
19520
+ sectionBkgColor2: '#222632',
19521
+ altSectionBkgColor: '#222632',
19522
+ gridColor: '#2a2e3a',
19523
+ taskBorderColor: '#475569',
19524
+ doneTaskBkgColor: '#065f46',
19525
+ doneTaskBorderColor: '#34d399',
19526
+ activeTaskBkgColor: '#78350f',
19527
+ activeTaskBorderColor: '#fbbf24',
19528
+ taskTextColor: '#e1e4ea',
19529
+ sectionBkgColor: '#1a1d27',
19530
+ pie1: '#34d399',
19531
+ pie2: '#475569',
19532
+ pie3: '#fbbf24',
19533
+ pie4: '#f87171',
19534
+ pie5: '#6c8cff',
19535
+ pie6: '#a78bfa',
19536
+ pie7: '#f472b6',
19537
+ pieTitleTextColor: '#e1e4ea',
19538
+ pieSectionTextColor: '#e1e4ea',
19539
+ pieLegendTextColor: '#e1e4ea',
19540
+ pieStrokeColor: '#1a1d27'
19541
+ }
19542
+ });</script>
18686
19543
  </body>
18687
19544
  </html>`;
18688
19545
  }
@@ -18929,6 +19786,10 @@ a:hover { text-decoration: underline; }
18929
19786
  /* Table */
18930
19787
  .table-wrap {
18931
19788
  overflow-x: auto;
19789
+ overflow-y: auto;
19790
+ max-height: calc(100vh - 280px);
19791
+ border: 1px solid var(--border);
19792
+ border-radius: var(--radius);
18932
19793
  }
18933
19794
 
18934
19795
  table {
@@ -18944,6 +19805,10 @@ th {
18944
19805
  letter-spacing: 0.05em;
18945
19806
  color: var(--text-dim);
18946
19807
  border-bottom: 1px solid var(--border);
19808
+ position: sticky;
19809
+ top: 0;
19810
+ background: var(--bg-card);
19811
+ z-index: 1;
18947
19812
  }
18948
19813
 
18949
19814
  td {
@@ -18991,6 +19856,8 @@ tr:hover td {
18991
19856
  border: 1px solid var(--border);
18992
19857
  border-radius: var(--radius);
18993
19858
  padding: 1.25rem;
19859
+ display: flex;
19860
+ flex-direction: column;
18994
19861
  }
18995
19862
 
18996
19863
  .gar-area .area-header {
@@ -19021,6 +19888,9 @@ tr:hover td {
19021
19888
  .gar-area ul {
19022
19889
  list-style: none;
19023
19890
  font-size: 0.8rem;
19891
+ max-height: 200px;
19892
+ overflow-y: auto;
19893
+ scrollbar-width: thin;
19024
19894
  }
19025
19895
 
19026
19896
  .gar-area li {
@@ -19043,13 +19913,14 @@ tr:hover td {
19043
19913
  display: flex;
19044
19914
  gap: 1rem;
19045
19915
  overflow-x: auto;
19916
+ scrollbar-width: thin;
19046
19917
  padding-bottom: 1rem;
19047
19918
  }
19048
19919
 
19049
19920
  .board-column {
19050
19921
  min-width: 240px;
19051
19922
  max-width: 300px;
19052
- flex: 1;
19923
+ flex: 0 0 auto;
19053
19924
  }
19054
19925
 
19055
19926
  .board-column-header {
@@ -19062,6 +19933,7 @@ tr:hover td {
19062
19933
  margin-bottom: 0.5rem;
19063
19934
  display: flex;
19064
19935
  justify-content: space-between;
19936
+ flex-shrink: 0;
19065
19937
  }
19066
19938
 
19067
19939
  .board-column-header .count {
@@ -19243,6 +20115,291 @@ tr:hover td {
19243
20115
  .mermaid-row .mermaid-container {
19244
20116
  margin: 0;
19245
20117
  }
20118
+
20119
+ /* Three-column artifact flow */
20120
+ .flow-diagram {
20121
+ background: var(--bg-card);
20122
+ border: 1px solid var(--border);
20123
+ border-radius: var(--radius);
20124
+ padding: 1.25rem;
20125
+ position: relative;
20126
+ overflow-x: auto;
20127
+ }
20128
+
20129
+ .flow-lines {
20130
+ position: absolute;
20131
+ top: 0;
20132
+ left: 0;
20133
+ pointer-events: none;
20134
+ }
20135
+
20136
+ .flow-columns {
20137
+ display: flex;
20138
+ gap: 3rem;
20139
+ position: relative;
20140
+ min-width: 600px;
20141
+ }
20142
+
20143
+ .flow-column {
20144
+ flex: 1;
20145
+ min-width: 0;
20146
+ display: flex;
20147
+ flex-direction: column;
20148
+ gap: 0.5rem;
20149
+ }
20150
+
20151
+ .flow-column-header {
20152
+ font-size: 0.7rem;
20153
+ text-transform: uppercase;
20154
+ letter-spacing: 0.06em;
20155
+ color: var(--text-dim);
20156
+ font-weight: 600;
20157
+ padding-bottom: 0.4rem;
20158
+ border-bottom: 1px solid var(--border);
20159
+ margin-bottom: 0.25rem;
20160
+ }
20161
+
20162
+ .flow-node {
20163
+ padding: 0.5rem 0.65rem;
20164
+ border-radius: 6px;
20165
+ border-left: 3px solid var(--border);
20166
+ background: var(--bg);
20167
+ transition: border-color 0.15s, background 0.15s;
20168
+ }
20169
+
20170
+ .flow-node:hover {
20171
+ background: var(--bg-hover);
20172
+ }
20173
+
20174
+ .flow-node-id {
20175
+ display: inline-block;
20176
+ font-family: var(--mono);
20177
+ font-size: 0.65rem;
20178
+ color: var(--accent);
20179
+ margin-bottom: 0.15rem;
20180
+ text-decoration: none;
20181
+ }
20182
+
20183
+ .flow-node-id:hover {
20184
+ text-decoration: underline;
20185
+ }
20186
+
20187
+ .flow-node-title {
20188
+ display: block;
20189
+ font-size: 0.8rem;
20190
+ }
20191
+
20192
+ .flow-done { border-left-color: var(--green); }
20193
+ .flow-active { border-left-color: var(--amber); }
20194
+ .flow-blocked { border-left-color: var(--red); }
20195
+ .flow-default { border-left-color: var(--accent-dim); }
20196
+
20197
+ .flow-node { cursor: pointer; transition: opacity 0.2s, border-color 0.15s, background 0.15s; }
20198
+ .flow-dim { opacity: 0.2; }
20199
+ .flow-lit { background: var(--bg-hover); }
20200
+ .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
20201
+ .flow-line-dim { opacity: 0.08; }
20202
+
20203
+ /* Gantt truncation note */
20204
+ .mermaid-note {
20205
+ font-size: 0.75rem;
20206
+ color: var(--text-dim);
20207
+ text-align: right;
20208
+ margin-bottom: 0.5rem;
20209
+ }
20210
+
20211
+ /* HTML Gantt chart */
20212
+ .gantt {
20213
+ background: var(--bg-card);
20214
+ border: 1px solid var(--border);
20215
+ border-radius: var(--radius);
20216
+ padding: 1.25rem 1.25rem 1.25rem 0;
20217
+ position: relative;
20218
+ overflow-x: auto;
20219
+ }
20220
+
20221
+ .gantt-chart {
20222
+ min-width: 600px;
20223
+ }
20224
+
20225
+ .gantt-overlay {
20226
+ position: absolute;
20227
+ top: 0;
20228
+ left: 0;
20229
+ right: 0;
20230
+ bottom: 0;
20231
+ pointer-events: none;
20232
+ display: flex;
20233
+ }
20234
+
20235
+ .gantt-header,
20236
+ .gantt-section-row,
20237
+ .gantt-row,
20238
+ .gantt-overlay {
20239
+ display: flex;
20240
+ align-items: center;
20241
+ }
20242
+
20243
+ .gantt-label {
20244
+ width: 200px;
20245
+ min-width: 200px;
20246
+ padding: 0.3rem 0.75rem;
20247
+ font-size: 0.8rem;
20248
+ color: var(--text-dim);
20249
+ text-align: right;
20250
+ white-space: nowrap;
20251
+ overflow: hidden;
20252
+ text-overflow: ellipsis;
20253
+ }
20254
+
20255
+ .gantt-section-label {
20256
+ font-weight: 600;
20257
+ color: var(--text);
20258
+ font-size: 0.75rem;
20259
+ text-transform: uppercase;
20260
+ letter-spacing: 0.03em;
20261
+ padding-top: 0.6rem;
20262
+ }
20263
+
20264
+ .gantt-track {
20265
+ flex: 1;
20266
+ position: relative;
20267
+ height: 28px;
20268
+ min-width: 0;
20269
+ }
20270
+
20271
+ .gantt-section-row .gantt-track {
20272
+ height: 20px;
20273
+ }
20274
+
20275
+ .gantt-section-bg {
20276
+ position: absolute;
20277
+ top: 0;
20278
+ bottom: 0;
20279
+ background: var(--bg-hover);
20280
+ border-radius: 3px;
20281
+ opacity: 0.4;
20282
+ }
20283
+
20284
+ .gantt-bar {
20285
+ position: absolute;
20286
+ top: 4px;
20287
+ bottom: 4px;
20288
+ border-radius: 4px;
20289
+ min-width: 6px;
20290
+ transition: opacity 0.15s;
20291
+ }
20292
+
20293
+ .gantt-bar:hover {
20294
+ opacity: 0.85;
20295
+ }
20296
+
20297
+ .gantt-bar-done {
20298
+ background: var(--green);
20299
+ }
20300
+
20301
+ .gantt-bar-active {
20302
+ background: var(--amber);
20303
+ }
20304
+
20305
+ .gantt-bar-blocked {
20306
+ background: var(--red);
20307
+ }
20308
+
20309
+ .gantt-bar-default {
20310
+ background: var(--accent-dim);
20311
+ }
20312
+
20313
+ .gantt-dates {
20314
+ height: 24px;
20315
+ border-bottom: 1px solid var(--border);
20316
+ margin-bottom: 0.25rem;
20317
+ }
20318
+
20319
+ .gantt-marker {
20320
+ position: absolute;
20321
+ top: 0;
20322
+ bottom: 0;
20323
+ border-left: 1px solid var(--border);
20324
+ }
20325
+
20326
+ .gantt-marker span {
20327
+ position: absolute;
20328
+ top: 2px;
20329
+ left: 6px;
20330
+ font-size: 0.65rem;
20331
+ color: var(--text-dim);
20332
+ white-space: nowrap;
20333
+ }
20334
+
20335
+ .gantt-today {
20336
+ position: absolute;
20337
+ top: 0;
20338
+ bottom: 0;
20339
+ width: 2px;
20340
+ background: var(--red);
20341
+ opacity: 0.7;
20342
+ }
20343
+
20344
+ /* Pie chart color overrides */
20345
+ .mermaid-container .pieCircle {
20346
+ stroke: var(--bg-card);
20347
+ }
20348
+
20349
+ .mermaid-container text.slice {
20350
+ fill: var(--bg) !important;
20351
+ font-weight: 600;
20352
+ }
20353
+
20354
+ /* Urgency row indicators */
20355
+ .urgency-row-overdue { border-left: 3px solid var(--red); }
20356
+ .urgency-row-due-3d { border-left: 3px solid var(--amber); }
20357
+ .urgency-row-due-7d { border-left: 3px solid #e2a308; }
20358
+
20359
+ /* Urgency badge pills */
20360
+ .urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
20361
+ .urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
20362
+ .urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
20363
+ .urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
20364
+ .urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
20365
+
20366
+ /* Trending */
20367
+ .trending-rank {
20368
+ display: inline-flex;
20369
+ align-items: center;
20370
+ justify-content: center;
20371
+ width: 24px;
20372
+ height: 24px;
20373
+ border-radius: 50%;
20374
+ background: var(--bg-hover);
20375
+ font-size: 0.75rem;
20376
+ font-weight: 600;
20377
+ color: var(--text-dim);
20378
+ }
20379
+
20380
+ .trending-score {
20381
+ display: inline-block;
20382
+ padding: 0.15rem 0.6rem;
20383
+ border-radius: 999px;
20384
+ font-size: 0.7rem;
20385
+ font-weight: 700;
20386
+ background: rgba(108, 140, 255, 0.15);
20387
+ color: var(--accent);
20388
+ }
20389
+
20390
+ .signal-tag {
20391
+ display: inline-block;
20392
+ padding: 0.1rem 0.45rem;
20393
+ border-radius: 4px;
20394
+ font-size: 0.65rem;
20395
+ background: var(--bg-hover);
20396
+ color: var(--text-dim);
20397
+ margin-right: 0.25rem;
20398
+ margin-bottom: 0.15rem;
20399
+ white-space: nowrap;
20400
+ }
20401
+
20402
+ .text-dim { color: var(--text-dim); }
19246
20403
  `;
19247
20404
  }
19248
20405
 
@@ -19251,98 +20408,290 @@ function sanitize(text, maxLen = 40) {
19251
20408
  const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
19252
20409
  return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
19253
20410
  }
19254
- function mermaidBlock(definition) {
19255
- return `<div class="mermaid-container"><pre class="mermaid">
20411
+ function mermaidBlock(definition, extraClass) {
20412
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
20413
+ return `<div class="${cls}"><pre class="mermaid">
19256
20414
  ${definition}
19257
20415
  </pre></div>`;
19258
20416
  }
19259
20417
  function placeholder(message) {
19260
20418
  return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
19261
20419
  }
19262
- function buildTimelineGantt(data) {
19263
- const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
20420
+ function toMs(date5) {
20421
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
20422
+ }
20423
+ function fmtDate(ms) {
20424
+ const d = new Date(ms);
20425
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20426
+ return `${months[d.getMonth()]} ${d.getDate()}`;
20427
+ }
20428
+ function buildTimelineGantt(data, maxSprints = 6) {
20429
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
19264
20430
  if (sprintsWithDates.length === 0) {
19265
20431
  return placeholder("No timeline data available \u2014 sprints need start and end dates.");
19266
20432
  }
20433
+ const truncated = sprintsWithDates.length > maxSprints;
20434
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
20435
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
19267
20436
  const epicMap = new Map(data.epics.map((e) => [e.id, e]));
19268
- const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
19269
- for (const sprint of sprintsWithDates) {
19270
- lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
20437
+ const allStarts = visibleSprints.map((s) => toMs(s.startDate));
20438
+ const allEnds = visibleSprints.map((s) => toMs(s.endDate));
20439
+ const timelineStart = Math.min(...allStarts);
20440
+ const timelineEnd = Math.max(...allEnds);
20441
+ const span = timelineEnd - timelineStart || 1;
20442
+ const pct = (ms) => (ms - timelineStart) / span * 100;
20443
+ const DAY = 864e5;
20444
+ const markers = [];
20445
+ let tick = timelineStart;
20446
+ const startDay = new Date(tick).getDay();
20447
+ tick += (8 - startDay) % 7 * DAY;
20448
+ while (tick <= timelineEnd) {
20449
+ const left = pct(tick);
20450
+ markers.push(
20451
+ `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
20452
+ );
20453
+ tick += 7 * DAY;
20454
+ }
20455
+ const now = Date.now();
20456
+ let todayMarker = "";
20457
+ if (now >= timelineStart && now <= timelineEnd) {
20458
+ todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
20459
+ }
20460
+ const rows = [];
20461
+ for (const sprint of visibleSprints) {
20462
+ const sStart = toMs(sprint.startDate);
20463
+ const sEnd = toMs(sprint.endDate);
20464
+ rows.push(`<div class="gantt-section-row">
20465
+ <div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
20466
+ <div class="gantt-track">
20467
+ <div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
20468
+ </div>
20469
+ </div>`);
19271
20470
  const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
19272
- if (linked.length === 0) {
19273
- lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
19274
- } else {
19275
- for (const epic of linked) {
19276
- const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
19277
- lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
19278
- }
20471
+ const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
20472
+ for (const item of items) {
20473
+ 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";
20474
+ const left = pct(sStart).toFixed(2);
20475
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
20476
+ rows.push(`<div class="gantt-row">
20477
+ <div class="gantt-label">${item.label}</div>
20478
+ <div class="gantt-track">
20479
+ <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
20480
+ </div>
20481
+ </div>`);
19279
20482
  }
19280
20483
  }
19281
- return mermaidBlock(lines.join("\n"));
20484
+ const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
20485
+ return `${note}
20486
+ <div class="gantt">
20487
+ <div class="gantt-chart">
20488
+ <div class="gantt-header">
20489
+ <div class="gantt-label"></div>
20490
+ <div class="gantt-track gantt-dates">${markers.join("")}</div>
20491
+ </div>
20492
+ ${rows.join("\n")}
20493
+ </div>
20494
+ <div class="gantt-overlay">
20495
+ <div class="gantt-label"></div>
20496
+ <div class="gantt-track">${todayMarker}</div>
20497
+ </div>
20498
+ </div>`;
20499
+ }
20500
+ function statusClass(status) {
20501
+ const s = status.toLowerCase();
20502
+ if (s === "done" || s === "completed") return "flow-done";
20503
+ if (s === "in-progress" || s === "active") return "flow-active";
20504
+ if (s === "blocked") return "flow-blocked";
20505
+ return "flow-default";
19282
20506
  }
19283
20507
  function buildArtifactFlowchart(data) {
19284
20508
  if (data.features.length === 0 && data.epics.length === 0) {
19285
20509
  return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
19286
20510
  }
19287
- const lines = ["graph TD"];
19288
- lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
19289
- lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
19290
- lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
19291
- lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
19292
- const nodeIds = /* @__PURE__ */ new Set();
20511
+ const edges = [];
20512
+ const epicsByFeature = /* @__PURE__ */ new Map();
19293
20513
  for (const epic of data.epics) {
19294
- for (const featureId of epic.linkedFeature) {
19295
- const feature = data.features.find((f) => f.id === featureId);
19296
- if (feature) {
19297
- const fNode = feature.id.replace(/-/g, "_");
19298
- const eNode = epic.id.replace(/-/g, "_");
19299
- if (!nodeIds.has(fNode)) {
19300
- lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
19301
- nodeIds.add(fNode);
19302
- }
19303
- if (!nodeIds.has(eNode)) {
19304
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19305
- nodeIds.add(eNode);
19306
- }
19307
- lines.push(` ${fNode} --> ${eNode}`);
19308
- }
20514
+ for (const fid of epic.linkedFeature) {
20515
+ if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
20516
+ epicsByFeature.get(fid).push(epic.id);
20517
+ edges.push({ from: fid, to: epic.id });
19309
20518
  }
19310
20519
  }
20520
+ const sprintsByEpic = /* @__PURE__ */ new Map();
19311
20521
  for (const sprint of data.sprints) {
19312
- const sNode = sprint.id.replace(/-/g, "_");
19313
- for (const epicId of sprint.linkedEpics) {
19314
- const epic = data.epics.find((e) => e.id === epicId);
19315
- if (epic) {
19316
- const eNode = epic.id.replace(/-/g, "_");
19317
- if (!nodeIds.has(eNode)) {
19318
- lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
19319
- nodeIds.add(eNode);
19320
- }
19321
- if (!nodeIds.has(sNode)) {
19322
- lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
19323
- nodeIds.add(sNode);
19324
- }
19325
- lines.push(` ${eNode} --> ${sNode}`);
19326
- }
20522
+ for (const eid of sprint.linkedEpics) {
20523
+ if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
20524
+ sprintsByEpic.get(eid).push(sprint.id);
20525
+ edges.push({ from: eid, to: sprint.id });
19327
20526
  }
19328
20527
  }
19329
- if (nodeIds.size === 0) {
20528
+ const connectedFeatureIds = new Set(epicsByFeature.keys());
20529
+ const connectedEpicIds = /* @__PURE__ */ new Set();
20530
+ for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
20531
+ for (const ids of sprintsByEpic.values()) ids.forEach(() => {
20532
+ });
20533
+ for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
20534
+ const connectedSprintIds = /* @__PURE__ */ new Set();
20535
+ for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
20536
+ const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
20537
+ const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
20538
+ const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
20539
+ if (features.length === 0 && epics.length === 0) {
19330
20540
  return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
19331
20541
  }
19332
- const allItems = [
19333
- ...data.features.map((f) => ({ id: f.id, status: f.status })),
19334
- ...data.epics.map((e) => ({ id: e.id, status: e.status })),
19335
- ...data.sprints.map((s) => ({ id: s.id, status: s.status }))
19336
- ];
19337
- for (const item of allItems) {
19338
- const node = item.id.replace(/-/g, "_");
19339
- if (!nodeIds.has(node)) continue;
19340
- const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
19341
- if (cls) {
19342
- lines.push(` class ${node} ${cls}`);
19343
- }
19344
- }
19345
- return mermaidBlock(lines.join("\n"));
20542
+ const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
20543
+ <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
20544
+ <span class="flow-node-title">${sanitize(title, 35)}</span>
20545
+ </div>`;
20546
+ const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
20547
+ const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
20548
+ const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
20549
+ const edgesJson = JSON.stringify(edges);
20550
+ return `
20551
+ <div class="flow-diagram" id="flow-diagram">
20552
+ <svg class="flow-lines" id="flow-lines"></svg>
20553
+ <div class="flow-columns">
20554
+ <div class="flow-column">
20555
+ <div class="flow-column-header">Features</div>
20556
+ ${featuresHtml}
20557
+ </div>
20558
+ <div class="flow-column">
20559
+ <div class="flow-column-header">Epics</div>
20560
+ ${epicsHtml}
20561
+ </div>
20562
+ <div class="flow-column">
20563
+ <div class="flow-column-header">Sprints</div>
20564
+ ${sprintsHtml}
20565
+ </div>
20566
+ </div>
20567
+ </div>
20568
+ <script>
20569
+ (function() {
20570
+ var edges = ${edgesJson};
20571
+ var container = document.getElementById('flow-diagram');
20572
+ var svg = document.getElementById('flow-lines');
20573
+ if (!container || !svg) return;
20574
+
20575
+ // Build directed adjacency maps for traversal
20576
+ var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
20577
+ var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
20578
+ edges.forEach(function(e) {
20579
+ if (!fwd[e.from]) fwd[e.from] = [];
20580
+ if (!bwd[e.to]) bwd[e.to] = [];
20581
+ fwd[e.from].push(e.to);
20582
+ bwd[e.to].push(e.from);
20583
+ });
20584
+
20585
+ function drawLines() {
20586
+ var rect = container.getBoundingClientRect();
20587
+ svg.setAttribute('width', rect.width);
20588
+ svg.setAttribute('height', rect.height);
20589
+ svg.innerHTML = '';
20590
+
20591
+ edges.forEach(function(edge) {
20592
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
20593
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
20594
+ if (!fromEl || !toEl) return;
20595
+
20596
+ var fr = fromEl.getBoundingClientRect();
20597
+ var tr = toEl.getBoundingClientRect();
20598
+ var x1 = fr.right - rect.left;
20599
+ var y1 = fr.top + fr.height / 2 - rect.top;
20600
+ var x2 = tr.left - rect.left;
20601
+ var y2 = tr.top + tr.height / 2 - rect.top;
20602
+ var mx = (x1 + x2) / 2;
20603
+
20604
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
20605
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
20606
+ path.setAttribute('fill', 'none');
20607
+ path.setAttribute('stroke', '#2a2e3a');
20608
+ path.setAttribute('stroke-width', '1.5');
20609
+ path.dataset.from = edge.from;
20610
+ path.dataset.to = edge.to;
20611
+ svg.appendChild(path);
20612
+ });
20613
+ }
20614
+
20615
+ // Find directly related nodes via directed traversal
20616
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
20617
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
20618
+ function findConnected(startId) {
20619
+ var visited = {};
20620
+ visited[startId] = true;
20621
+ // Traverse forward (from\u2192to direction)
20622
+ var queue = [startId];
20623
+ while (queue.length) {
20624
+ var id = queue.shift();
20625
+ (fwd[id] || []).forEach(function(neighbor) {
20626
+ if (!visited[neighbor]) {
20627
+ visited[neighbor] = true;
20628
+ queue.push(neighbor);
20629
+ }
20630
+ });
20631
+ }
20632
+ // Traverse backward (to\u2192from direction)
20633
+ queue = [startId];
20634
+ while (queue.length) {
20635
+ var id = queue.shift();
20636
+ (bwd[id] || []).forEach(function(neighbor) {
20637
+ if (!visited[neighbor]) {
20638
+ visited[neighbor] = true;
20639
+ queue.push(neighbor);
20640
+ }
20641
+ });
20642
+ }
20643
+ return visited;
20644
+ }
20645
+
20646
+ function highlight(hoveredId) {
20647
+ var connected = findConnected(hoveredId);
20648
+ container.querySelectorAll('.flow-node').forEach(function(n) {
20649
+ if (connected[n.dataset.flowId]) {
20650
+ n.classList.add('flow-lit');
20651
+ n.classList.remove('flow-dim');
20652
+ } else {
20653
+ n.classList.add('flow-dim');
20654
+ n.classList.remove('flow-lit');
20655
+ }
20656
+ });
20657
+ svg.querySelectorAll('path').forEach(function(p) {
20658
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
20659
+ p.classList.add('flow-line-lit');
20660
+ p.classList.remove('flow-line-dim');
20661
+ } else {
20662
+ p.classList.add('flow-line-dim');
20663
+ p.classList.remove('flow-line-lit');
20664
+ }
20665
+ });
20666
+ }
20667
+
20668
+ function clearHighlight() {
20669
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
20670
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
20671
+ }
20672
+
20673
+ var activeId = null;
20674
+ container.addEventListener('click', function(e) {
20675
+ // Let the ID link navigate normally
20676
+ if (e.target.closest('a')) return;
20677
+
20678
+ var node = e.target.closest('.flow-node');
20679
+ var clickedId = node ? node.dataset.flowId : null;
20680
+
20681
+ if (!clickedId || clickedId === activeId) {
20682
+ activeId = null;
20683
+ clearHighlight();
20684
+ return;
20685
+ }
20686
+
20687
+ activeId = clickedId;
20688
+ highlight(clickedId);
20689
+ });
20690
+
20691
+ requestAnimationFrame(function() { setTimeout(drawLines, 100); });
20692
+ window.addEventListener('resize', drawLines);
20693
+ })();
20694
+ </script>`;
19346
20695
  }
19347
20696
  function buildStatusPie(title, counts) {
19348
20697
  const entries = Object.entries(counts).filter(([, v]) => v > 0);
@@ -19422,8 +20771,7 @@ function overviewPage(data, diagrams, navGroups) {
19422
20771
  ${groupSections}
19423
20772
  ${ungroupedSection}
19424
20773
 
19425
- <div class="section-title">Project Timeline</div>
19426
- ${buildTimelineGantt(diagrams)}
20774
+ <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
19427
20775
 
19428
20776
  <div class="section-title">Artifact Relationships</div>
19429
20777
  ${buildArtifactFlowchart(diagrams)}
@@ -19678,6 +21026,7 @@ function boardPage(data) {
19678
21026
  <span>${escapeHtml(col.status)}</span>
19679
21027
  <span class="count">${col.docs.length}</span>
19680
21028
  </div>
21029
+ <div class="board-column-cards">
19681
21030
  ${col.docs.map(
19682
21031
  (doc) => `
19683
21032
  <div class="board-card">
@@ -19688,6 +21037,7 @@ function boardPage(data) {
19688
21037
  </a>
19689
21038
  </div>`
19690
21039
  ).join("\n")}
21040
+ </div>
19691
21041
  </div>`
19692
21042
  ).join("\n");
19693
21043
  return `
@@ -19713,6 +21063,143 @@ function boardPage(data) {
19713
21063
  `;
19714
21064
  }
19715
21065
 
21066
+ // src/web/templates/pages/timeline.ts
21067
+ function timelinePage(diagrams) {
21068
+ return `
21069
+ <div class="page-header">
21070
+ <h2>Project Timeline</h2>
21071
+ <div class="subtitle">Sprint schedule with linked epics</div>
21072
+ </div>
21073
+
21074
+ ${buildTimelineGantt(diagrams)}
21075
+ `;
21076
+ }
21077
+
21078
+ // src/web/templates/pages/upcoming.ts
21079
+ function urgencyBadge(tier) {
21080
+ const labels = {
21081
+ overdue: "Overdue",
21082
+ "due-3d": "Due in 3d",
21083
+ "due-7d": "Due in 7d",
21084
+ upcoming: "Upcoming",
21085
+ later: "Later"
21086
+ };
21087
+ return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
21088
+ }
21089
+ function urgencyRowClass(tier) {
21090
+ if (tier === "overdue") return " urgency-row-overdue";
21091
+ if (tier === "due-3d") return " urgency-row-due-3d";
21092
+ if (tier === "due-7d") return " urgency-row-due-7d";
21093
+ return "";
21094
+ }
21095
+ function upcomingPage(data) {
21096
+ const hasActions = data.dueSoonActions.length > 0;
21097
+ const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
21098
+ const hasTrending = data.trending.length > 0;
21099
+ const actionsTable = hasActions ? `
21100
+ <h3 class="section-title">Due Soon \u2014 Actions</h3>
21101
+ <div class="table-wrap">
21102
+ <table>
21103
+ <thead>
21104
+ <tr>
21105
+ <th>ID</th>
21106
+ <th>Title</th>
21107
+ <th>Status</th>
21108
+ <th>Owner</th>
21109
+ <th>Due Date</th>
21110
+ <th>Urgency</th>
21111
+ <th>Tasks</th>
21112
+ </tr>
21113
+ </thead>
21114
+ <tbody>
21115
+ ${data.dueSoonActions.map(
21116
+ (a) => `
21117
+ <tr class="${urgencyRowClass(a.urgency)}">
21118
+ <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
21119
+ <td>${escapeHtml(a.title)}</td>
21120
+ <td>${statusBadge(a.status)}</td>
21121
+ <td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
21122
+ <td>${formatDate(a.dueDate)}</td>
21123
+ <td>${urgencyBadge(a.urgency)}</td>
21124
+ <td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
21125
+ </tr>`
21126
+ ).join("")}
21127
+ </tbody>
21128
+ </table>
21129
+ </div>` : "";
21130
+ const sprintTasksTable = hasSprintTasks ? `
21131
+ <h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
21132
+ <div class="table-wrap">
21133
+ <table>
21134
+ <thead>
21135
+ <tr>
21136
+ <th>ID</th>
21137
+ <th>Title</th>
21138
+ <th>Status</th>
21139
+ <th>Sprint</th>
21140
+ <th>Sprint Ends</th>
21141
+ <th>Urgency</th>
21142
+ </tr>
21143
+ </thead>
21144
+ <tbody>
21145
+ ${data.dueSoonSprintTasks.map(
21146
+ (t) => `
21147
+ <tr class="${urgencyRowClass(t.urgency)}">
21148
+ <td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
21149
+ <td>${escapeHtml(t.title)}</td>
21150
+ <td>${statusBadge(t.status)}</td>
21151
+ <td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
21152
+ <td>${formatDate(t.sprintEndDate)}</td>
21153
+ <td>${urgencyBadge(t.urgency)}</td>
21154
+ </tr>`
21155
+ ).join("")}
21156
+ </tbody>
21157
+ </table>
21158
+ </div>` : "";
21159
+ const trendingTable = hasTrending ? `
21160
+ <h3 class="section-title">Trending</h3>
21161
+ <div class="table-wrap">
21162
+ <table>
21163
+ <thead>
21164
+ <tr>
21165
+ <th>#</th>
21166
+ <th>ID</th>
21167
+ <th>Title</th>
21168
+ <th>Type</th>
21169
+ <th>Status</th>
21170
+ <th>Score</th>
21171
+ <th>Signals</th>
21172
+ </tr>
21173
+ </thead>
21174
+ <tbody>
21175
+ ${data.trending.map(
21176
+ (t, i) => `
21177
+ <tr>
21178
+ <td><span class="trending-rank">${i + 1}</span></td>
21179
+ <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
21180
+ <td>${escapeHtml(t.title)}</td>
21181
+ <td>${escapeHtml(typeLabel(t.type))}</td>
21182
+ <td>${statusBadge(t.status)}</td>
21183
+ <td><span class="trending-score">${t.score}</span></td>
21184
+ <td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
21185
+ </tr>`
21186
+ ).join("")}
21187
+ </tbody>
21188
+ </table>
21189
+ </div>` : "";
21190
+ const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
21191
+ return `
21192
+ <div class="page-header">
21193
+ <h2>Upcoming</h2>
21194
+ <div class="subtitle">Time-sensitive items and trending activity</div>
21195
+ </div>
21196
+ ${actionsTable}
21197
+ ${sprintTasksTable}
21198
+ ${trendingTable}
21199
+ ${emptyState}
21200
+ `;
21201
+ }
21202
+
19716
21203
  // src/web/router.ts
19717
21204
  function handleRequest(req, res, store, projectName, navGroups) {
19718
21205
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -19734,6 +21221,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19734
21221
  respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
19735
21222
  return;
19736
21223
  }
21224
+ if (pathname === "/timeline") {
21225
+ const diagrams = getDiagramData(store);
21226
+ const body = timelinePage(diagrams);
21227
+ respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, mainClass: "expanded" }, body));
21228
+ return;
21229
+ }
19737
21230
  if (pathname === "/gar") {
19738
21231
  const report = getGarData(store, projectName);
19739
21232
  const body = garPage(report);
@@ -19747,6 +21240,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
19747
21240
  respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
19748
21241
  return;
19749
21242
  }
21243
+ if (pathname === "/upcoming") {
21244
+ const data = getUpcomingData(store);
21245
+ const body = upcomingPage(data);
21246
+ respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
21247
+ return;
21248
+ }
19750
21249
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
19751
21250
  if (boardMatch) {
19752
21251
  const type = boardMatch[1];
@@ -19838,7 +21337,7 @@ function openBrowser(url2) {
19838
21337
  var runningServer = null;
19839
21338
  function createWebTools(store, projectName, navGroups) {
19840
21339
  return [
19841
- tool20(
21340
+ tool22(
19842
21341
  "start_web_dashboard",
19843
21342
  "Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
19844
21343
  {
@@ -19870,7 +21369,7 @@ function createWebTools(store, projectName, navGroups) {
19870
21369
  };
19871
21370
  }
19872
21371
  ),
19873
- tool20(
21372
+ tool22(
19874
21373
  "stop_web_dashboard",
19875
21374
  "Stop the running Marvin web dashboard.",
19876
21375
  {},
@@ -19890,7 +21389,7 @@ function createWebTools(store, projectName, navGroups) {
19890
21389
  };
19891
21390
  }
19892
21391
  ),
19893
- tool20(
21392
+ tool22(
19894
21393
  "get_web_dashboard_urls",
19895
21394
  "Get all available dashboard page URLs. The dashboard must be running.",
19896
21395
  {},
@@ -19904,6 +21403,7 @@ function createWebTools(store, projectName, navGroups) {
19904
21403
  const base = `http://localhost:${runningServer.port}`;
19905
21404
  const urls = {
19906
21405
  overview: base,
21406
+ upcoming: `${base}/upcoming`,
19907
21407
  gar: `${base}/gar`,
19908
21408
  board: `${base}/board`
19909
21409
  };
@@ -19916,7 +21416,7 @@ function createWebTools(store, projectName, navGroups) {
19916
21416
  },
19917
21417
  { annotations: { readOnlyHint: true } }
19918
21418
  ),
19919
- tool20(
21419
+ tool22(
19920
21420
  "get_dashboard_overview",
19921
21421
  "Get the project overview data: document type counts and recent activity. Works without the web server running.",
19922
21422
  {},
@@ -19938,7 +21438,7 @@ function createWebTools(store, projectName, navGroups) {
19938
21438
  },
19939
21439
  { annotations: { readOnlyHint: true } }
19940
21440
  ),
19941
- tool20(
21441
+ tool22(
19942
21442
  "get_dashboard_gar",
19943
21443
  "Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
19944
21444
  {},
@@ -19950,7 +21450,7 @@ function createWebTools(store, projectName, navGroups) {
19950
21450
  },
19951
21451
  { annotations: { readOnlyHint: true } }
19952
21452
  ),
19953
- tool20(
21453
+ tool22(
19954
21454
  "get_dashboard_board",
19955
21455
  "Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
19956
21456
  {
@@ -19977,6 +21477,18 @@ function createWebTools(store, projectName, navGroups) {
19977
21477
  };
19978
21478
  },
19979
21479
  { annotations: { readOnlyHint: true } }
21480
+ ),
21481
+ tool22(
21482
+ "get_dashboard_upcoming",
21483
+ "Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
21484
+ {},
21485
+ async () => {
21486
+ const data = getUpcomingData(store);
21487
+ return {
21488
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
21489
+ };
21490
+ },
21491
+ { annotations: { readOnlyHint: true } }
19980
21492
  )
19981
21493
  ];
19982
21494
  }
@@ -20064,7 +21576,7 @@ function createSkillActionTools(skills, context) {
20064
21576
  if (!skill.actions) continue;
20065
21577
  for (const action of skill.actions) {
20066
21578
  tools.push(
20067
- tool21(
21579
+ tool23(
20068
21580
  `${skill.id}__${action.id}`,
20069
21581
  action.description,
20070
21582
  {
@@ -20155,7 +21667,7 @@ var deliveryManager = {
20155
21667
  "Epic scheduling and tracking",
20156
21668
  "Sprint planning and tracking"
20157
21669
  ],
20158
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "sprint"],
21670
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
20159
21671
  contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
20160
21672
  };
20161
21673
 
@@ -20193,9 +21705,10 @@ var techLead = {
20193
21705
  "Implementation guidance",
20194
21706
  "Non-functional requirements",
20195
21707
  "Epic creation and scoping",
21708
+ "Task creation and breakdown",
20196
21709
  "Sprint scoping and technical execution"
20197
21710
  ],
20198
- documentTypes: ["decision", "action", "question", "epic", "sprint"],
21711
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
20199
21712
  contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
20200
21713
  };
20201
21714
 
@@ -20293,10 +21806,10 @@ ${lines.join("\n\n")}`;
20293
21806
  }
20294
21807
 
20295
21808
  // src/mcp/persona-tools.ts
20296
- import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
21809
+ import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
20297
21810
  function createPersonaTools(ctx, marvinDir) {
20298
21811
  return [
20299
- tool22(
21812
+ tool24(
20300
21813
  "set_persona",
20301
21814
  "Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
20302
21815
  {
@@ -20326,7 +21839,7 @@ ${summaries}`
20326
21839
  };
20327
21840
  }
20328
21841
  ),
20329
- tool22(
21842
+ tool24(
20330
21843
  "get_persona_guidance",
20331
21844
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
20332
21845
  {
@@ -20428,16 +21941,16 @@ function collectTools(marvinDir) {
20428
21941
  const plugin = resolvePlugin(config2.methodology);
20429
21942
  const registrations = plugin?.documentTypeRegistrations ?? [];
20430
21943
  const store = new DocumentStore(marvinDir, registrations);
20431
- const sourcesDir = path8.join(marvinDir, "sources");
20432
- const hasSourcesDir = fs8.existsSync(sourcesDir);
21944
+ const sourcesDir = path9.join(marvinDir, "sources");
21945
+ const hasSourcesDir = fs9.existsSync(sourcesDir);
20433
21946
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20434
21947
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20435
21948
  const sessionStore = new SessionStore(marvinDir);
20436
21949
  const allSkills = loadAllSkills(marvinDir);
20437
21950
  const allSkillIds = [...allSkills.keys()];
20438
- const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
21951
+ const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
20439
21952
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
20440
- const projectRoot = path8.dirname(marvinDir);
21953
+ const projectRoot = path9.dirname(marvinDir);
20441
21954
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
20442
21955
  const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
20443
21956
  const navGroups = buildNavGroups({
@@ -20510,9 +22023,9 @@ function parseProjectDir(argv) {
20510
22023
  }
20511
22024
  async function main() {
20512
22025
  const projectDir = parseProjectDir(process.argv);
20513
- const from = projectDir ? path9.resolve(projectDir) : process.cwd();
22026
+ const from = projectDir ? path10.resolve(projectDir) : process.cwd();
20514
22027
  const root = findProjectRoot(from);
20515
- const marvinDir = path9.join(root, ".marvin");
22028
+ const marvinDir = path10.join(root, ".marvin");
20516
22029
  await startStdioServer({ marvinDir });
20517
22030
  }
20518
22031
  main().catch((err) => {