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