mrvn-cli 0.4.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -23
- package/dist/index.d.ts +6 -2
- package/dist/index.js +1968 -456
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1730 -217
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1883 -369
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// bin/marvin-serve.ts
|
|
9
|
-
import * as
|
|
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
|
|
102
|
-
import * as
|
|
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,
|
|
1210
|
-
if (!
|
|
1209
|
+
function getElementAtPath(obj, path11) {
|
|
1210
|
+
if (!path11)
|
|
1211
1211
|
return obj;
|
|
1212
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
1825
|
-
for (const seg of
|
|
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
|
|
13803
|
-
if (
|
|
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 (
|
|
13808
|
-
const key =
|
|
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
|
|
17007
|
+
import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
|
|
16797
17008
|
function createUseCaseTools(store) {
|
|
16798
17009
|
return [
|
|
16799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17131
|
+
import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
|
|
16921
17132
|
function createTechAssessmentTools(store) {
|
|
16922
17133
|
return [
|
|
16923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17293
|
+
import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
|
|
17083
17294
|
function createExtensionDesignTools(store) {
|
|
17084
17295
|
return [
|
|
17085
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17450
|
+
import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
|
|
17240
17451
|
function createAemReportTools(store) {
|
|
17241
17452
|
return [
|
|
17242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17698
|
-
import * as
|
|
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
|
|
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(
|
|
17752
|
-
const url2 = `${this.baseUrl}${
|
|
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} ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
18223
|
-
|
|
18224
|
-
|
|
18225
|
-
|
|
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
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
const
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
const
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
const
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
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
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
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
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
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 (
|
|
18292
|
-
for (const entry of
|
|
18293
|
-
const entryPath =
|
|
18294
|
-
if (!
|
|
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 =
|
|
18304
|
-
if (
|
|
18931
|
+
const skillsDir = path8.join(marvinDir, "skills");
|
|
18932
|
+
if (fs8.existsSync(skillsDir)) {
|
|
18305
18933
|
let entries;
|
|
18306
18934
|
try {
|
|
18307
|
-
entries =
|
|
18935
|
+
entries = fs8.readdirSync(skillsDir);
|
|
18308
18936
|
} catch {
|
|
18309
18937
|
entries = [];
|
|
18310
18938
|
}
|
|
18311
18939
|
for (const entry of entries) {
|
|
18312
|
-
const entryPath =
|
|
18940
|
+
const entryPath = path8.join(skillsDir, entry);
|
|
18313
18941
|
try {
|
|
18314
|
-
if (
|
|
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 =
|
|
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
|
|
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
|
|
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({
|
|
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:
|
|
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
|
-
|
|
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
|
|
19263
|
-
|
|
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
|
|
19269
|
-
|
|
19270
|
-
|
|
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
|
-
|
|
19273
|
-
|
|
19274
|
-
|
|
19275
|
-
|
|
19276
|
-
|
|
19277
|
-
|
|
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
|
-
|
|
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
|
|
19288
|
-
|
|
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
|
|
19295
|
-
|
|
19296
|
-
|
|
19297
|
-
|
|
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
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
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
|
-
|
|
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
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
lines
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
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
|
|
19426
|
-
${buildTimelineGantt(diagrams)}
|
|
20774
|
+
<div class="section-title"><a href="/timeline">Project Timeline →</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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21809
|
+
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
20297
21810
|
function createPersonaTools(ctx, marvinDir) {
|
|
20298
21811
|
return [
|
|
20299
|
-
|
|
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
|
-
|
|
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 =
|
|
20432
|
-
const hasSourcesDir =
|
|
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 =
|
|
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 ?
|
|
22026
|
+
const from = projectDir ? path10.resolve(projectDir) : process.cwd();
|
|
20514
22027
|
const root = findProjectRoot(from);
|
|
20515
|
-
const marvinDir =
|
|
22028
|
+
const marvinDir = path10.join(root, ".marvin");
|
|
20516
22029
|
await startStdioServer({ marvinDir });
|
|
20517
22030
|
}
|
|
20518
22031
|
main().catch((err) => {
|