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/index.js
CHANGED
|
@@ -453,7 +453,7 @@ var deliveryManager = {
|
|
|
453
453
|
"Epic scheduling and tracking",
|
|
454
454
|
"Sprint planning and tracking"
|
|
455
455
|
],
|
|
456
|
-
documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "sprint"],
|
|
456
|
+
documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
|
|
457
457
|
contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
|
|
458
458
|
};
|
|
459
459
|
|
|
@@ -491,9 +491,10 @@ var techLead = {
|
|
|
491
491
|
"Implementation guidance",
|
|
492
492
|
"Non-functional requirements",
|
|
493
493
|
"Epic creation and scoping",
|
|
494
|
+
"Task creation and breakdown",
|
|
494
495
|
"Sprint scoping and technical execution"
|
|
495
496
|
],
|
|
496
|
-
documentTypes: ["decision", "action", "question", "epic", "sprint"],
|
|
497
|
+
documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
|
|
497
498
|
contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
|
|
498
499
|
};
|
|
499
500
|
|
|
@@ -1361,10 +1362,10 @@ function mergeDefs(...defs) {
|
|
|
1361
1362
|
function cloneDef(schema) {
|
|
1362
1363
|
return mergeDefs(schema._zod.def);
|
|
1363
1364
|
}
|
|
1364
|
-
function getElementAtPath(obj,
|
|
1365
|
-
if (!
|
|
1365
|
+
function getElementAtPath(obj, path21) {
|
|
1366
|
+
if (!path21)
|
|
1366
1367
|
return obj;
|
|
1367
|
-
return
|
|
1368
|
+
return path21.reduce((acc, key) => acc?.[key], obj);
|
|
1368
1369
|
}
|
|
1369
1370
|
function promiseAllObject(promisesObj) {
|
|
1370
1371
|
const keys = Object.keys(promisesObj);
|
|
@@ -1747,11 +1748,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1747
1748
|
}
|
|
1748
1749
|
return false;
|
|
1749
1750
|
}
|
|
1750
|
-
function prefixIssues(
|
|
1751
|
+
function prefixIssues(path21, issues) {
|
|
1751
1752
|
return issues.map((iss) => {
|
|
1752
1753
|
var _a2;
|
|
1753
1754
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
1754
|
-
iss.path.unshift(
|
|
1755
|
+
iss.path.unshift(path21);
|
|
1755
1756
|
return iss;
|
|
1756
1757
|
});
|
|
1757
1758
|
}
|
|
@@ -1934,7 +1935,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1934
1935
|
}
|
|
1935
1936
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
1936
1937
|
const result = { errors: [] };
|
|
1937
|
-
const processError = (error49,
|
|
1938
|
+
const processError = (error49, path21 = []) => {
|
|
1938
1939
|
var _a2, _b;
|
|
1939
1940
|
for (const issue2 of error49.issues) {
|
|
1940
1941
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -1944,7 +1945,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1944
1945
|
} else if (issue2.code === "invalid_element") {
|
|
1945
1946
|
processError({ issues: issue2.issues }, issue2.path);
|
|
1946
1947
|
} else {
|
|
1947
|
-
const fullpath = [...
|
|
1948
|
+
const fullpath = [...path21, ...issue2.path];
|
|
1948
1949
|
if (fullpath.length === 0) {
|
|
1949
1950
|
result.errors.push(mapper(issue2));
|
|
1950
1951
|
continue;
|
|
@@ -1976,8 +1977,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1976
1977
|
}
|
|
1977
1978
|
function toDotPath(_path) {
|
|
1978
1979
|
const segs = [];
|
|
1979
|
-
const
|
|
1980
|
-
for (const seg of
|
|
1980
|
+
const path21 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
1981
|
+
for (const seg of path21) {
|
|
1981
1982
|
if (typeof seg === "number")
|
|
1982
1983
|
segs.push(`[${seg}]`);
|
|
1983
1984
|
else if (typeof seg === "symbol")
|
|
@@ -13954,13 +13955,13 @@ function resolveRef(ref, ctx) {
|
|
|
13954
13955
|
if (!ref.startsWith("#")) {
|
|
13955
13956
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
13956
13957
|
}
|
|
13957
|
-
const
|
|
13958
|
-
if (
|
|
13958
|
+
const path21 = ref.slice(1).split("/").filter(Boolean);
|
|
13959
|
+
if (path21.length === 0) {
|
|
13959
13960
|
return ctx.rootSchema;
|
|
13960
13961
|
}
|
|
13961
13962
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
13962
|
-
if (
|
|
13963
|
-
const key =
|
|
13963
|
+
if (path21[0] === defsKey) {
|
|
13964
|
+
const key = path21[1];
|
|
13964
13965
|
if (!key || !ctx.defs[key]) {
|
|
13965
13966
|
throw new Error(`Reference not found: ${ref}`);
|
|
13966
13967
|
}
|
|
@@ -14998,7 +14999,7 @@ function createSessionTools(store) {
|
|
|
14998
14999
|
|
|
14999
15000
|
// src/agent/tools/web.ts
|
|
15000
15001
|
import * as http2 from "http";
|
|
15001
|
-
import { tool as
|
|
15002
|
+
import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
|
|
15002
15003
|
|
|
15003
15004
|
// src/plugins/builtin/tools/epic-utils.ts
|
|
15004
15005
|
function normalizeLinkedFeatures(value) {
|
|
@@ -15018,132 +15019,22 @@ function generateFeatureTags(features) {
|
|
|
15018
15019
|
return features.map((id) => `feature:${id}`);
|
|
15019
15020
|
}
|
|
15020
15021
|
|
|
15021
|
-
// src/
|
|
15022
|
-
function
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
);
|
|
15030
|
-
const tagOverdueItems = allDocs.filter(
|
|
15031
|
-
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15032
|
-
);
|
|
15033
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15034
|
-
const dateOverdueActions = openActions.filter((d) => {
|
|
15035
|
-
const dueDate = d.frontmatter.dueDate;
|
|
15036
|
-
return typeof dueDate === "string" && dueDate < today;
|
|
15037
|
-
});
|
|
15038
|
-
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
15039
|
-
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15040
|
-
);
|
|
15041
|
-
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15042
|
-
const riskItems = allDocs.filter(
|
|
15043
|
-
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
15044
|
-
);
|
|
15045
|
-
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15046
|
-
const total = allActions.length;
|
|
15047
|
-
const done = doneActions.length;
|
|
15048
|
-
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
15049
|
-
const scheduleItems = [
|
|
15050
|
-
...blockedItems,
|
|
15051
|
-
...overdueItems
|
|
15052
|
-
].filter(
|
|
15053
|
-
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15054
|
-
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15055
|
-
const qualityItems = [
|
|
15056
|
-
...riskItems,
|
|
15057
|
-
...openQuestions
|
|
15058
|
-
].filter(
|
|
15059
|
-
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15060
|
-
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15061
|
-
const resourceItems = unownedActions.map((d) => ({
|
|
15062
|
-
id: d.frontmatter.id,
|
|
15063
|
-
title: d.frontmatter.title
|
|
15064
|
-
}));
|
|
15065
|
-
return {
|
|
15066
|
-
scope: {
|
|
15067
|
-
total,
|
|
15068
|
-
open: openActions.length,
|
|
15069
|
-
done,
|
|
15070
|
-
completionPct
|
|
15071
|
-
},
|
|
15072
|
-
schedule: {
|
|
15073
|
-
blocked: blockedItems.length,
|
|
15074
|
-
overdue: overdueItems.length,
|
|
15075
|
-
items: scheduleItems
|
|
15076
|
-
},
|
|
15077
|
-
quality: {
|
|
15078
|
-
risks: riskItems.length,
|
|
15079
|
-
openQuestions: openQuestions.length,
|
|
15080
|
-
items: qualityItems
|
|
15081
|
-
},
|
|
15082
|
-
resources: {
|
|
15083
|
-
unowned: unownedActions.length,
|
|
15084
|
-
items: resourceItems
|
|
15022
|
+
// src/plugins/builtin/tools/task-utils.ts
|
|
15023
|
+
function normalizeLinkedEpics(value) {
|
|
15024
|
+
if (value === void 0 || value === null) return [];
|
|
15025
|
+
if (typeof value === "string") {
|
|
15026
|
+
try {
|
|
15027
|
+
const parsed = JSON.parse(value);
|
|
15028
|
+
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
15029
|
+
} catch {
|
|
15085
15030
|
}
|
|
15086
|
-
|
|
15031
|
+
return [value];
|
|
15032
|
+
}
|
|
15033
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
15034
|
+
return [];
|
|
15087
15035
|
}
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
function worstStatus(statuses) {
|
|
15091
|
-
if (statuses.includes("red")) return "red";
|
|
15092
|
-
if (statuses.includes("amber")) return "amber";
|
|
15093
|
-
return "green";
|
|
15094
|
-
}
|
|
15095
|
-
function evaluateGar(projectName, metrics) {
|
|
15096
|
-
const areas = [];
|
|
15097
|
-
const scopePct = metrics.scope.completionPct;
|
|
15098
|
-
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15099
|
-
areas.push({
|
|
15100
|
-
name: "Scope",
|
|
15101
|
-
status: scopeStatus,
|
|
15102
|
-
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15103
|
-
items: []
|
|
15104
|
-
});
|
|
15105
|
-
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15106
|
-
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15107
|
-
const scheduleParts = [];
|
|
15108
|
-
if (metrics.schedule.blocked > 0)
|
|
15109
|
-
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15110
|
-
if (metrics.schedule.overdue > 0)
|
|
15111
|
-
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15112
|
-
areas.push({
|
|
15113
|
-
name: "Schedule",
|
|
15114
|
-
status: scheduleStatus,
|
|
15115
|
-
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15116
|
-
items: metrics.schedule.items
|
|
15117
|
-
});
|
|
15118
|
-
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15119
|
-
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15120
|
-
const qualityParts = [];
|
|
15121
|
-
if (metrics.quality.risks > 0)
|
|
15122
|
-
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15123
|
-
if (metrics.quality.openQuestions > 0)
|
|
15124
|
-
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15125
|
-
areas.push({
|
|
15126
|
-
name: "Quality",
|
|
15127
|
-
status: qualityStatus,
|
|
15128
|
-
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15129
|
-
items: metrics.quality.items
|
|
15130
|
-
});
|
|
15131
|
-
const resourceCount = metrics.resources.unowned;
|
|
15132
|
-
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15133
|
-
areas.push({
|
|
15134
|
-
name: "Resources",
|
|
15135
|
-
status: resourceStatus,
|
|
15136
|
-
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15137
|
-
items: metrics.resources.items
|
|
15138
|
-
});
|
|
15139
|
-
const overall = worstStatus(areas.map((a) => a.status));
|
|
15140
|
-
return {
|
|
15141
|
-
projectName,
|
|
15142
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15143
|
-
overall,
|
|
15144
|
-
areas,
|
|
15145
|
-
metrics
|
|
15146
|
-
};
|
|
15036
|
+
function generateEpicTags(epics) {
|
|
15037
|
+
return epics.map((id) => `epic:${id}`);
|
|
15147
15038
|
}
|
|
15148
15039
|
|
|
15149
15040
|
// src/reports/health/collector.ts
|
|
@@ -15285,6 +15176,134 @@ function collectHealthMetrics(store) {
|
|
|
15285
15176
|
};
|
|
15286
15177
|
}
|
|
15287
15178
|
|
|
15179
|
+
// src/reports/gar/collector.ts
|
|
15180
|
+
function collectGarMetrics(store) {
|
|
15181
|
+
const allActions = store.list({ type: "action" });
|
|
15182
|
+
const openActions = allActions.filter((d) => d.frontmatter.status === "open");
|
|
15183
|
+
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
15184
|
+
const allDocs = store.list();
|
|
15185
|
+
const blockedItems = allDocs.filter(
|
|
15186
|
+
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15187
|
+
);
|
|
15188
|
+
const tagOverdueItems = allDocs.filter(
|
|
15189
|
+
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15190
|
+
);
|
|
15191
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15192
|
+
const dateOverdueActions = openActions.filter((d) => {
|
|
15193
|
+
const dueDate = d.frontmatter.dueDate;
|
|
15194
|
+
return typeof dueDate === "string" && dueDate < today;
|
|
15195
|
+
});
|
|
15196
|
+
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
15197
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15198
|
+
);
|
|
15199
|
+
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15200
|
+
const riskItems = allDocs.filter(
|
|
15201
|
+
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
15202
|
+
);
|
|
15203
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15204
|
+
const total = allActions.length;
|
|
15205
|
+
const done = doneActions.length;
|
|
15206
|
+
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
15207
|
+
const scheduleItems = [
|
|
15208
|
+
...blockedItems,
|
|
15209
|
+
...overdueItems
|
|
15210
|
+
].filter(
|
|
15211
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15212
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15213
|
+
const qualityItems = [
|
|
15214
|
+
...riskItems,
|
|
15215
|
+
...openQuestions
|
|
15216
|
+
].filter(
|
|
15217
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15218
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15219
|
+
const resourceItems = unownedActions.map((d) => ({
|
|
15220
|
+
id: d.frontmatter.id,
|
|
15221
|
+
title: d.frontmatter.title
|
|
15222
|
+
}));
|
|
15223
|
+
return {
|
|
15224
|
+
scope: {
|
|
15225
|
+
total,
|
|
15226
|
+
open: openActions.length,
|
|
15227
|
+
done,
|
|
15228
|
+
completionPct
|
|
15229
|
+
},
|
|
15230
|
+
schedule: {
|
|
15231
|
+
blocked: blockedItems.length,
|
|
15232
|
+
overdue: overdueItems.length,
|
|
15233
|
+
items: scheduleItems
|
|
15234
|
+
},
|
|
15235
|
+
quality: {
|
|
15236
|
+
risks: riskItems.length,
|
|
15237
|
+
openQuestions: openQuestions.length,
|
|
15238
|
+
items: qualityItems
|
|
15239
|
+
},
|
|
15240
|
+
resources: {
|
|
15241
|
+
unowned: unownedActions.length,
|
|
15242
|
+
items: resourceItems
|
|
15243
|
+
}
|
|
15244
|
+
};
|
|
15245
|
+
}
|
|
15246
|
+
|
|
15247
|
+
// src/reports/gar/evaluator.ts
|
|
15248
|
+
function worstStatus(statuses) {
|
|
15249
|
+
if (statuses.includes("red")) return "red";
|
|
15250
|
+
if (statuses.includes("amber")) return "amber";
|
|
15251
|
+
return "green";
|
|
15252
|
+
}
|
|
15253
|
+
function evaluateGar(projectName, metrics) {
|
|
15254
|
+
const areas = [];
|
|
15255
|
+
const scopePct = metrics.scope.completionPct;
|
|
15256
|
+
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15257
|
+
areas.push({
|
|
15258
|
+
name: "Scope",
|
|
15259
|
+
status: scopeStatus,
|
|
15260
|
+
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15261
|
+
items: []
|
|
15262
|
+
});
|
|
15263
|
+
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15264
|
+
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15265
|
+
const scheduleParts = [];
|
|
15266
|
+
if (metrics.schedule.blocked > 0)
|
|
15267
|
+
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15268
|
+
if (metrics.schedule.overdue > 0)
|
|
15269
|
+
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15270
|
+
areas.push({
|
|
15271
|
+
name: "Schedule",
|
|
15272
|
+
status: scheduleStatus,
|
|
15273
|
+
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15274
|
+
items: metrics.schedule.items
|
|
15275
|
+
});
|
|
15276
|
+
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15277
|
+
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15278
|
+
const qualityParts = [];
|
|
15279
|
+
if (metrics.quality.risks > 0)
|
|
15280
|
+
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15281
|
+
if (metrics.quality.openQuestions > 0)
|
|
15282
|
+
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15283
|
+
areas.push({
|
|
15284
|
+
name: "Quality",
|
|
15285
|
+
status: qualityStatus,
|
|
15286
|
+
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15287
|
+
items: metrics.quality.items
|
|
15288
|
+
});
|
|
15289
|
+
const resourceCount = metrics.resources.unowned;
|
|
15290
|
+
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15291
|
+
areas.push({
|
|
15292
|
+
name: "Resources",
|
|
15293
|
+
status: resourceStatus,
|
|
15294
|
+
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15295
|
+
items: metrics.resources.items
|
|
15296
|
+
});
|
|
15297
|
+
const overall = worstStatus(areas.map((a) => a.status));
|
|
15298
|
+
return {
|
|
15299
|
+
projectName,
|
|
15300
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15301
|
+
overall,
|
|
15302
|
+
areas,
|
|
15303
|
+
metrics
|
|
15304
|
+
};
|
|
15305
|
+
}
|
|
15306
|
+
|
|
15288
15307
|
// src/reports/health/evaluator.ts
|
|
15289
15308
|
function worstStatus2(statuses) {
|
|
15290
15309
|
if (statuses.includes("red")) return "red";
|
|
@@ -15504,6 +15523,204 @@ function getDiagramData(store) {
|
|
|
15504
15523
|
}
|
|
15505
15524
|
return { sprints, epics, features, statusCounts };
|
|
15506
15525
|
}
|
|
15526
|
+
function computeUrgency(dueDateStr, todayStr) {
|
|
15527
|
+
const due = new Date(dueDateStr).getTime();
|
|
15528
|
+
const today = new Date(todayStr).getTime();
|
|
15529
|
+
const diffDays = Math.floor((due - today) / 864e5);
|
|
15530
|
+
if (diffDays < 0) return "overdue";
|
|
15531
|
+
if (diffDays <= 3) return "due-3d";
|
|
15532
|
+
if (diffDays <= 7) return "due-7d";
|
|
15533
|
+
if (diffDays <= 14) return "upcoming";
|
|
15534
|
+
return "later";
|
|
15535
|
+
}
|
|
15536
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15537
|
+
function getUpcomingData(store) {
|
|
15538
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15539
|
+
const allDocs = store.list();
|
|
15540
|
+
const docById = /* @__PURE__ */ new Map();
|
|
15541
|
+
for (const doc of allDocs) {
|
|
15542
|
+
docById.set(doc.frontmatter.id, doc);
|
|
15543
|
+
}
|
|
15544
|
+
const actions = allDocs.filter(
|
|
15545
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
|
|
15546
|
+
);
|
|
15547
|
+
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
15548
|
+
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
15549
|
+
const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
|
|
15550
|
+
const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
|
|
15551
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
15552
|
+
for (const task of tasks) {
|
|
15553
|
+
const tags = task.frontmatter.tags ?? [];
|
|
15554
|
+
for (const tag of tags) {
|
|
15555
|
+
if (tag.startsWith("epic:")) {
|
|
15556
|
+
const epicId = tag.slice(5);
|
|
15557
|
+
if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
|
|
15558
|
+
epicToTasks.get(epicId).push(task);
|
|
15559
|
+
}
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
function getSprintTasks(sprintDoc) {
|
|
15563
|
+
const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
|
|
15564
|
+
const result = [];
|
|
15565
|
+
for (const epicId of linkedEpics) {
|
|
15566
|
+
const epicTasks = epicToTasks.get(epicId) ?? [];
|
|
15567
|
+
result.push(...epicTasks);
|
|
15568
|
+
}
|
|
15569
|
+
return result;
|
|
15570
|
+
}
|
|
15571
|
+
function countRelatedTasks(actionDoc) {
|
|
15572
|
+
const actionTags = actionDoc.frontmatter.tags ?? [];
|
|
15573
|
+
const relatedTaskIds = /* @__PURE__ */ new Set();
|
|
15574
|
+
for (const tag of actionTags) {
|
|
15575
|
+
if (tag.startsWith("sprint:")) {
|
|
15576
|
+
const sprintId = tag.slice(7);
|
|
15577
|
+
const sprint = docById.get(sprintId);
|
|
15578
|
+
if (sprint) {
|
|
15579
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
15580
|
+
for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
}
|
|
15584
|
+
return relatedTaskIds.size;
|
|
15585
|
+
}
|
|
15586
|
+
const dueSoonActions = actionsWithDue.map((d) => ({
|
|
15587
|
+
id: d.frontmatter.id,
|
|
15588
|
+
title: d.frontmatter.title,
|
|
15589
|
+
status: d.frontmatter.status,
|
|
15590
|
+
owner: d.frontmatter.owner,
|
|
15591
|
+
dueDate: d.frontmatter.dueDate,
|
|
15592
|
+
urgency: computeUrgency(d.frontmatter.dueDate, today),
|
|
15593
|
+
relatedTaskCount: countRelatedTasks(d)
|
|
15594
|
+
})).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
|
|
15595
|
+
const todayMs = new Date(today).getTime();
|
|
15596
|
+
const fourteenDaysMs = 14 * 864e5;
|
|
15597
|
+
const nearSprints = sprints.filter((s) => {
|
|
15598
|
+
const endDate = s.frontmatter.endDate;
|
|
15599
|
+
if (!endDate) return false;
|
|
15600
|
+
const endMs = new Date(endDate).getTime();
|
|
15601
|
+
const diff = endMs - todayMs;
|
|
15602
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
15603
|
+
});
|
|
15604
|
+
const taskSprintMap = /* @__PURE__ */ new Map();
|
|
15605
|
+
for (const sprint of nearSprints) {
|
|
15606
|
+
const sprintEnd = sprint.frontmatter.endDate;
|
|
15607
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
15608
|
+
for (const task of sprintTaskDocs) {
|
|
15609
|
+
if (DONE_STATUSES.has(task.frontmatter.status)) continue;
|
|
15610
|
+
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
15611
|
+
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
15612
|
+
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
15613
|
+
}
|
|
15614
|
+
}
|
|
15615
|
+
}
|
|
15616
|
+
const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
|
|
15617
|
+
id: task.frontmatter.id,
|
|
15618
|
+
title: task.frontmatter.title,
|
|
15619
|
+
status: task.frontmatter.status,
|
|
15620
|
+
sprintId: sprint.frontmatter.id,
|
|
15621
|
+
sprintTitle: sprint.frontmatter.title,
|
|
15622
|
+
sprintEndDate: sprintEnd,
|
|
15623
|
+
urgency: computeUrgency(sprintEnd, today)
|
|
15624
|
+
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
15625
|
+
const openItems = allDocs.filter(
|
|
15626
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
|
|
15627
|
+
);
|
|
15628
|
+
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
15629
|
+
const recentMeetings = allDocs.filter(
|
|
15630
|
+
(d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
|
|
15631
|
+
);
|
|
15632
|
+
const crossRefCounts = /* @__PURE__ */ new Map();
|
|
15633
|
+
for (const doc of allDocs) {
|
|
15634
|
+
const content = doc.content ?? "";
|
|
15635
|
+
for (const item of openItems) {
|
|
15636
|
+
if (doc.frontmatter.id === item.frontmatter.id) continue;
|
|
15637
|
+
if (content.includes(item.frontmatter.id)) {
|
|
15638
|
+
crossRefCounts.set(
|
|
15639
|
+
item.frontmatter.id,
|
|
15640
|
+
(crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
|
|
15641
|
+
);
|
|
15642
|
+
}
|
|
15643
|
+
}
|
|
15644
|
+
}
|
|
15645
|
+
const activeSprints = sprints.filter((s) => {
|
|
15646
|
+
const status = s.frontmatter.status;
|
|
15647
|
+
if (status === "active") return true;
|
|
15648
|
+
const startDate = s.frontmatter.startDate;
|
|
15649
|
+
if (!startDate) return false;
|
|
15650
|
+
const startMs = new Date(startDate).getTime();
|
|
15651
|
+
const diff = startMs - todayMs;
|
|
15652
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
15653
|
+
});
|
|
15654
|
+
const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
|
|
15655
|
+
const activeEpicIds = /* @__PURE__ */ new Set();
|
|
15656
|
+
for (const s of activeSprints) {
|
|
15657
|
+
for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
|
|
15658
|
+
activeEpicIds.add(epicId);
|
|
15659
|
+
}
|
|
15660
|
+
}
|
|
15661
|
+
const trending = openItems.map((doc) => {
|
|
15662
|
+
const signals = [];
|
|
15663
|
+
let score = 0;
|
|
15664
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
15665
|
+
const ageDays = daysBetween(updated, today);
|
|
15666
|
+
const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
|
|
15667
|
+
if (recencyPts > 0) {
|
|
15668
|
+
signals.push({ factor: "recency", points: recencyPts });
|
|
15669
|
+
score += recencyPts;
|
|
15670
|
+
}
|
|
15671
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
15672
|
+
const linkedToActiveSprint = tags.some(
|
|
15673
|
+
(t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
|
|
15674
|
+
);
|
|
15675
|
+
const linkedToActiveEpic = tags.some(
|
|
15676
|
+
(t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
|
|
15677
|
+
);
|
|
15678
|
+
if (linkedToActiveSprint) {
|
|
15679
|
+
signals.push({ factor: "sprint proximity", points: 25 });
|
|
15680
|
+
score += 25;
|
|
15681
|
+
} else if (linkedToActiveEpic) {
|
|
15682
|
+
signals.push({ factor: "sprint proximity", points: 15 });
|
|
15683
|
+
score += 15;
|
|
15684
|
+
}
|
|
15685
|
+
const mentionCount = recentMeetings.filter(
|
|
15686
|
+
(m) => (m.content ?? "").includes(doc.frontmatter.id)
|
|
15687
|
+
).length;
|
|
15688
|
+
if (mentionCount > 0) {
|
|
15689
|
+
const meetingPts = Math.min(15, mentionCount * 5);
|
|
15690
|
+
signals.push({ factor: "meeting mentions", points: meetingPts });
|
|
15691
|
+
score += meetingPts;
|
|
15692
|
+
}
|
|
15693
|
+
const priority = doc.frontmatter.priority?.toLowerCase();
|
|
15694
|
+
const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
|
|
15695
|
+
if (priorityPts > 0) {
|
|
15696
|
+
signals.push({ factor: "priority", points: priorityPts });
|
|
15697
|
+
score += priorityPts;
|
|
15698
|
+
}
|
|
15699
|
+
if (["action", "question"].includes(doc.frontmatter.type)) {
|
|
15700
|
+
const createdDays = daysBetween(doc.frontmatter.created, today);
|
|
15701
|
+
if (createdDays >= 14) {
|
|
15702
|
+
const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
|
|
15703
|
+
signals.push({ factor: "aging", points: agingPts });
|
|
15704
|
+
score += agingPts;
|
|
15705
|
+
}
|
|
15706
|
+
}
|
|
15707
|
+
const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
|
|
15708
|
+
if (refs > 0) {
|
|
15709
|
+
const crossRefPts = Math.min(15, refs * 5);
|
|
15710
|
+
signals.push({ factor: "cross-references", points: crossRefPts });
|
|
15711
|
+
score += crossRefPts;
|
|
15712
|
+
}
|
|
15713
|
+
return {
|
|
15714
|
+
id: doc.frontmatter.id,
|
|
15715
|
+
title: doc.frontmatter.title,
|
|
15716
|
+
type: doc.frontmatter.type,
|
|
15717
|
+
status: doc.frontmatter.status,
|
|
15718
|
+
score,
|
|
15719
|
+
signals
|
|
15720
|
+
};
|
|
15721
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
15722
|
+
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
15723
|
+
}
|
|
15507
15724
|
|
|
15508
15725
|
// src/web/templates/layout.ts
|
|
15509
15726
|
function escapeHtml(str) {
|
|
@@ -15627,6 +15844,8 @@ function inline(text) {
|
|
|
15627
15844
|
function layout(opts, body) {
|
|
15628
15845
|
const topItems = [
|
|
15629
15846
|
{ href: "/", label: "Overview" },
|
|
15847
|
+
{ href: "/upcoming", label: "Upcoming" },
|
|
15848
|
+
{ href: "/timeline", label: "Timeline" },
|
|
15630
15849
|
{ href: "/board", label: "Board" },
|
|
15631
15850
|
{ href: "/gar", label: "GAR Report" },
|
|
15632
15851
|
{ href: "/health", label: "Health" }
|
|
@@ -15663,7 +15882,7 @@ function layout(opts, body) {
|
|
|
15663
15882
|
${groupsHtml}
|
|
15664
15883
|
</nav>
|
|
15665
15884
|
</aside>
|
|
15666
|
-
<main class="main">
|
|
15885
|
+
<main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
|
|
15667
15886
|
<button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
|
|
15668
15887
|
<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>
|
|
15669
15888
|
<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>
|
|
@@ -15672,7 +15891,36 @@ function layout(opts, body) {
|
|
|
15672
15891
|
</main>
|
|
15673
15892
|
</div>
|
|
15674
15893
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
15675
|
-
<script>mermaid.initialize({
|
|
15894
|
+
<script>mermaid.initialize({
|
|
15895
|
+
startOnLoad: true,
|
|
15896
|
+
theme: 'dark',
|
|
15897
|
+
themeVariables: {
|
|
15898
|
+
background: '#1a1d27',
|
|
15899
|
+
primaryColor: '#2a2e3a',
|
|
15900
|
+
sectionBkgColor: '#1a1d27',
|
|
15901
|
+
sectionBkgColor2: '#222632',
|
|
15902
|
+
altSectionBkgColor: '#222632',
|
|
15903
|
+
gridColor: '#2a2e3a',
|
|
15904
|
+
taskBorderColor: '#475569',
|
|
15905
|
+
doneTaskBkgColor: '#065f46',
|
|
15906
|
+
doneTaskBorderColor: '#34d399',
|
|
15907
|
+
activeTaskBkgColor: '#78350f',
|
|
15908
|
+
activeTaskBorderColor: '#fbbf24',
|
|
15909
|
+
taskTextColor: '#e1e4ea',
|
|
15910
|
+
sectionBkgColor: '#1a1d27',
|
|
15911
|
+
pie1: '#34d399',
|
|
15912
|
+
pie2: '#475569',
|
|
15913
|
+
pie3: '#fbbf24',
|
|
15914
|
+
pie4: '#f87171',
|
|
15915
|
+
pie5: '#6c8cff',
|
|
15916
|
+
pie6: '#a78bfa',
|
|
15917
|
+
pie7: '#f472b6',
|
|
15918
|
+
pieTitleTextColor: '#e1e4ea',
|
|
15919
|
+
pieSectionTextColor: '#e1e4ea',
|
|
15920
|
+
pieLegendTextColor: '#e1e4ea',
|
|
15921
|
+
pieStrokeColor: '#1a1d27'
|
|
15922
|
+
}
|
|
15923
|
+
});</script>
|
|
15676
15924
|
</body>
|
|
15677
15925
|
</html>`;
|
|
15678
15926
|
}
|
|
@@ -15919,6 +16167,10 @@ a:hover { text-decoration: underline; }
|
|
|
15919
16167
|
/* Table */
|
|
15920
16168
|
.table-wrap {
|
|
15921
16169
|
overflow-x: auto;
|
|
16170
|
+
overflow-y: auto;
|
|
16171
|
+
max-height: calc(100vh - 280px);
|
|
16172
|
+
border: 1px solid var(--border);
|
|
16173
|
+
border-radius: var(--radius);
|
|
15922
16174
|
}
|
|
15923
16175
|
|
|
15924
16176
|
table {
|
|
@@ -15934,6 +16186,10 @@ th {
|
|
|
15934
16186
|
letter-spacing: 0.05em;
|
|
15935
16187
|
color: var(--text-dim);
|
|
15936
16188
|
border-bottom: 1px solid var(--border);
|
|
16189
|
+
position: sticky;
|
|
16190
|
+
top: 0;
|
|
16191
|
+
background: var(--bg-card);
|
|
16192
|
+
z-index: 1;
|
|
15937
16193
|
}
|
|
15938
16194
|
|
|
15939
16195
|
td {
|
|
@@ -15981,6 +16237,8 @@ tr:hover td {
|
|
|
15981
16237
|
border: 1px solid var(--border);
|
|
15982
16238
|
border-radius: var(--radius);
|
|
15983
16239
|
padding: 1.25rem;
|
|
16240
|
+
display: flex;
|
|
16241
|
+
flex-direction: column;
|
|
15984
16242
|
}
|
|
15985
16243
|
|
|
15986
16244
|
.gar-area .area-header {
|
|
@@ -16011,6 +16269,9 @@ tr:hover td {
|
|
|
16011
16269
|
.gar-area ul {
|
|
16012
16270
|
list-style: none;
|
|
16013
16271
|
font-size: 0.8rem;
|
|
16272
|
+
max-height: 200px;
|
|
16273
|
+
overflow-y: auto;
|
|
16274
|
+
scrollbar-width: thin;
|
|
16014
16275
|
}
|
|
16015
16276
|
|
|
16016
16277
|
.gar-area li {
|
|
@@ -16033,13 +16294,14 @@ tr:hover td {
|
|
|
16033
16294
|
display: flex;
|
|
16034
16295
|
gap: 1rem;
|
|
16035
16296
|
overflow-x: auto;
|
|
16297
|
+
scrollbar-width: thin;
|
|
16036
16298
|
padding-bottom: 1rem;
|
|
16037
16299
|
}
|
|
16038
16300
|
|
|
16039
16301
|
.board-column {
|
|
16040
16302
|
min-width: 240px;
|
|
16041
16303
|
max-width: 300px;
|
|
16042
|
-
flex:
|
|
16304
|
+
flex: 0 0 auto;
|
|
16043
16305
|
}
|
|
16044
16306
|
|
|
16045
16307
|
.board-column-header {
|
|
@@ -16052,6 +16314,7 @@ tr:hover td {
|
|
|
16052
16314
|
margin-bottom: 0.5rem;
|
|
16053
16315
|
display: flex;
|
|
16054
16316
|
justify-content: space-between;
|
|
16317
|
+
flex-shrink: 0;
|
|
16055
16318
|
}
|
|
16056
16319
|
|
|
16057
16320
|
.board-column-header .count {
|
|
@@ -16230,9 +16493,294 @@ tr:hover td {
|
|
|
16230
16493
|
gap: 1rem;
|
|
16231
16494
|
}
|
|
16232
16495
|
|
|
16233
|
-
.mermaid-row .mermaid-container {
|
|
16234
|
-
margin: 0;
|
|
16235
|
-
}
|
|
16496
|
+
.mermaid-row .mermaid-container {
|
|
16497
|
+
margin: 0;
|
|
16498
|
+
}
|
|
16499
|
+
|
|
16500
|
+
/* Three-column artifact flow */
|
|
16501
|
+
.flow-diagram {
|
|
16502
|
+
background: var(--bg-card);
|
|
16503
|
+
border: 1px solid var(--border);
|
|
16504
|
+
border-radius: var(--radius);
|
|
16505
|
+
padding: 1.25rem;
|
|
16506
|
+
position: relative;
|
|
16507
|
+
overflow-x: auto;
|
|
16508
|
+
}
|
|
16509
|
+
|
|
16510
|
+
.flow-lines {
|
|
16511
|
+
position: absolute;
|
|
16512
|
+
top: 0;
|
|
16513
|
+
left: 0;
|
|
16514
|
+
pointer-events: none;
|
|
16515
|
+
}
|
|
16516
|
+
|
|
16517
|
+
.flow-columns {
|
|
16518
|
+
display: flex;
|
|
16519
|
+
gap: 3rem;
|
|
16520
|
+
position: relative;
|
|
16521
|
+
min-width: 600px;
|
|
16522
|
+
}
|
|
16523
|
+
|
|
16524
|
+
.flow-column {
|
|
16525
|
+
flex: 1;
|
|
16526
|
+
min-width: 0;
|
|
16527
|
+
display: flex;
|
|
16528
|
+
flex-direction: column;
|
|
16529
|
+
gap: 0.5rem;
|
|
16530
|
+
}
|
|
16531
|
+
|
|
16532
|
+
.flow-column-header {
|
|
16533
|
+
font-size: 0.7rem;
|
|
16534
|
+
text-transform: uppercase;
|
|
16535
|
+
letter-spacing: 0.06em;
|
|
16536
|
+
color: var(--text-dim);
|
|
16537
|
+
font-weight: 600;
|
|
16538
|
+
padding-bottom: 0.4rem;
|
|
16539
|
+
border-bottom: 1px solid var(--border);
|
|
16540
|
+
margin-bottom: 0.25rem;
|
|
16541
|
+
}
|
|
16542
|
+
|
|
16543
|
+
.flow-node {
|
|
16544
|
+
padding: 0.5rem 0.65rem;
|
|
16545
|
+
border-radius: 6px;
|
|
16546
|
+
border-left: 3px solid var(--border);
|
|
16547
|
+
background: var(--bg);
|
|
16548
|
+
transition: border-color 0.15s, background 0.15s;
|
|
16549
|
+
}
|
|
16550
|
+
|
|
16551
|
+
.flow-node:hover {
|
|
16552
|
+
background: var(--bg-hover);
|
|
16553
|
+
}
|
|
16554
|
+
|
|
16555
|
+
.flow-node-id {
|
|
16556
|
+
display: inline-block;
|
|
16557
|
+
font-family: var(--mono);
|
|
16558
|
+
font-size: 0.65rem;
|
|
16559
|
+
color: var(--accent);
|
|
16560
|
+
margin-bottom: 0.15rem;
|
|
16561
|
+
text-decoration: none;
|
|
16562
|
+
}
|
|
16563
|
+
|
|
16564
|
+
.flow-node-id:hover {
|
|
16565
|
+
text-decoration: underline;
|
|
16566
|
+
}
|
|
16567
|
+
|
|
16568
|
+
.flow-node-title {
|
|
16569
|
+
display: block;
|
|
16570
|
+
font-size: 0.8rem;
|
|
16571
|
+
}
|
|
16572
|
+
|
|
16573
|
+
.flow-done { border-left-color: var(--green); }
|
|
16574
|
+
.flow-active { border-left-color: var(--amber); }
|
|
16575
|
+
.flow-blocked { border-left-color: var(--red); }
|
|
16576
|
+
.flow-default { border-left-color: var(--accent-dim); }
|
|
16577
|
+
|
|
16578
|
+
.flow-node { cursor: pointer; transition: opacity 0.2s, border-color 0.15s, background 0.15s; }
|
|
16579
|
+
.flow-dim { opacity: 0.2; }
|
|
16580
|
+
.flow-lit { background: var(--bg-hover); }
|
|
16581
|
+
.flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
|
|
16582
|
+
.flow-line-dim { opacity: 0.08; }
|
|
16583
|
+
|
|
16584
|
+
/* Gantt truncation note */
|
|
16585
|
+
.mermaid-note {
|
|
16586
|
+
font-size: 0.75rem;
|
|
16587
|
+
color: var(--text-dim);
|
|
16588
|
+
text-align: right;
|
|
16589
|
+
margin-bottom: 0.5rem;
|
|
16590
|
+
}
|
|
16591
|
+
|
|
16592
|
+
/* HTML Gantt chart */
|
|
16593
|
+
.gantt {
|
|
16594
|
+
background: var(--bg-card);
|
|
16595
|
+
border: 1px solid var(--border);
|
|
16596
|
+
border-radius: var(--radius);
|
|
16597
|
+
padding: 1.25rem 1.25rem 1.25rem 0;
|
|
16598
|
+
position: relative;
|
|
16599
|
+
overflow-x: auto;
|
|
16600
|
+
}
|
|
16601
|
+
|
|
16602
|
+
.gantt-chart {
|
|
16603
|
+
min-width: 600px;
|
|
16604
|
+
}
|
|
16605
|
+
|
|
16606
|
+
.gantt-overlay {
|
|
16607
|
+
position: absolute;
|
|
16608
|
+
top: 0;
|
|
16609
|
+
left: 0;
|
|
16610
|
+
right: 0;
|
|
16611
|
+
bottom: 0;
|
|
16612
|
+
pointer-events: none;
|
|
16613
|
+
display: flex;
|
|
16614
|
+
}
|
|
16615
|
+
|
|
16616
|
+
.gantt-header,
|
|
16617
|
+
.gantt-section-row,
|
|
16618
|
+
.gantt-row,
|
|
16619
|
+
.gantt-overlay {
|
|
16620
|
+
display: flex;
|
|
16621
|
+
align-items: center;
|
|
16622
|
+
}
|
|
16623
|
+
|
|
16624
|
+
.gantt-label {
|
|
16625
|
+
width: 200px;
|
|
16626
|
+
min-width: 200px;
|
|
16627
|
+
padding: 0.3rem 0.75rem;
|
|
16628
|
+
font-size: 0.8rem;
|
|
16629
|
+
color: var(--text-dim);
|
|
16630
|
+
text-align: right;
|
|
16631
|
+
white-space: nowrap;
|
|
16632
|
+
overflow: hidden;
|
|
16633
|
+
text-overflow: ellipsis;
|
|
16634
|
+
}
|
|
16635
|
+
|
|
16636
|
+
.gantt-section-label {
|
|
16637
|
+
font-weight: 600;
|
|
16638
|
+
color: var(--text);
|
|
16639
|
+
font-size: 0.75rem;
|
|
16640
|
+
text-transform: uppercase;
|
|
16641
|
+
letter-spacing: 0.03em;
|
|
16642
|
+
padding-top: 0.6rem;
|
|
16643
|
+
}
|
|
16644
|
+
|
|
16645
|
+
.gantt-track {
|
|
16646
|
+
flex: 1;
|
|
16647
|
+
position: relative;
|
|
16648
|
+
height: 28px;
|
|
16649
|
+
min-width: 0;
|
|
16650
|
+
}
|
|
16651
|
+
|
|
16652
|
+
.gantt-section-row .gantt-track {
|
|
16653
|
+
height: 20px;
|
|
16654
|
+
}
|
|
16655
|
+
|
|
16656
|
+
.gantt-section-bg {
|
|
16657
|
+
position: absolute;
|
|
16658
|
+
top: 0;
|
|
16659
|
+
bottom: 0;
|
|
16660
|
+
background: var(--bg-hover);
|
|
16661
|
+
border-radius: 3px;
|
|
16662
|
+
opacity: 0.4;
|
|
16663
|
+
}
|
|
16664
|
+
|
|
16665
|
+
.gantt-bar {
|
|
16666
|
+
position: absolute;
|
|
16667
|
+
top: 4px;
|
|
16668
|
+
bottom: 4px;
|
|
16669
|
+
border-radius: 4px;
|
|
16670
|
+
min-width: 6px;
|
|
16671
|
+
transition: opacity 0.15s;
|
|
16672
|
+
}
|
|
16673
|
+
|
|
16674
|
+
.gantt-bar:hover {
|
|
16675
|
+
opacity: 0.85;
|
|
16676
|
+
}
|
|
16677
|
+
|
|
16678
|
+
.gantt-bar-done {
|
|
16679
|
+
background: var(--green);
|
|
16680
|
+
}
|
|
16681
|
+
|
|
16682
|
+
.gantt-bar-active {
|
|
16683
|
+
background: var(--amber);
|
|
16684
|
+
}
|
|
16685
|
+
|
|
16686
|
+
.gantt-bar-blocked {
|
|
16687
|
+
background: var(--red);
|
|
16688
|
+
}
|
|
16689
|
+
|
|
16690
|
+
.gantt-bar-default {
|
|
16691
|
+
background: var(--accent-dim);
|
|
16692
|
+
}
|
|
16693
|
+
|
|
16694
|
+
.gantt-dates {
|
|
16695
|
+
height: 24px;
|
|
16696
|
+
border-bottom: 1px solid var(--border);
|
|
16697
|
+
margin-bottom: 0.25rem;
|
|
16698
|
+
}
|
|
16699
|
+
|
|
16700
|
+
.gantt-marker {
|
|
16701
|
+
position: absolute;
|
|
16702
|
+
top: 0;
|
|
16703
|
+
bottom: 0;
|
|
16704
|
+
border-left: 1px solid var(--border);
|
|
16705
|
+
}
|
|
16706
|
+
|
|
16707
|
+
.gantt-marker span {
|
|
16708
|
+
position: absolute;
|
|
16709
|
+
top: 2px;
|
|
16710
|
+
left: 6px;
|
|
16711
|
+
font-size: 0.65rem;
|
|
16712
|
+
color: var(--text-dim);
|
|
16713
|
+
white-space: nowrap;
|
|
16714
|
+
}
|
|
16715
|
+
|
|
16716
|
+
.gantt-today {
|
|
16717
|
+
position: absolute;
|
|
16718
|
+
top: 0;
|
|
16719
|
+
bottom: 0;
|
|
16720
|
+
width: 2px;
|
|
16721
|
+
background: var(--red);
|
|
16722
|
+
opacity: 0.7;
|
|
16723
|
+
}
|
|
16724
|
+
|
|
16725
|
+
/* Pie chart color overrides */
|
|
16726
|
+
.mermaid-container .pieCircle {
|
|
16727
|
+
stroke: var(--bg-card);
|
|
16728
|
+
}
|
|
16729
|
+
|
|
16730
|
+
.mermaid-container text.slice {
|
|
16731
|
+
fill: var(--bg) !important;
|
|
16732
|
+
font-weight: 600;
|
|
16733
|
+
}
|
|
16734
|
+
|
|
16735
|
+
/* Urgency row indicators */
|
|
16736
|
+
.urgency-row-overdue { border-left: 3px solid var(--red); }
|
|
16737
|
+
.urgency-row-due-3d { border-left: 3px solid var(--amber); }
|
|
16738
|
+
.urgency-row-due-7d { border-left: 3px solid #e2a308; }
|
|
16739
|
+
|
|
16740
|
+
/* Urgency badge pills */
|
|
16741
|
+
.urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
16742
|
+
.urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
16743
|
+
.urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
|
|
16744
|
+
.urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
16745
|
+
.urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
16746
|
+
|
|
16747
|
+
/* Trending */
|
|
16748
|
+
.trending-rank {
|
|
16749
|
+
display: inline-flex;
|
|
16750
|
+
align-items: center;
|
|
16751
|
+
justify-content: center;
|
|
16752
|
+
width: 24px;
|
|
16753
|
+
height: 24px;
|
|
16754
|
+
border-radius: 50%;
|
|
16755
|
+
background: var(--bg-hover);
|
|
16756
|
+
font-size: 0.75rem;
|
|
16757
|
+
font-weight: 600;
|
|
16758
|
+
color: var(--text-dim);
|
|
16759
|
+
}
|
|
16760
|
+
|
|
16761
|
+
.trending-score {
|
|
16762
|
+
display: inline-block;
|
|
16763
|
+
padding: 0.15rem 0.6rem;
|
|
16764
|
+
border-radius: 999px;
|
|
16765
|
+
font-size: 0.7rem;
|
|
16766
|
+
font-weight: 700;
|
|
16767
|
+
background: rgba(108, 140, 255, 0.15);
|
|
16768
|
+
color: var(--accent);
|
|
16769
|
+
}
|
|
16770
|
+
|
|
16771
|
+
.signal-tag {
|
|
16772
|
+
display: inline-block;
|
|
16773
|
+
padding: 0.1rem 0.45rem;
|
|
16774
|
+
border-radius: 4px;
|
|
16775
|
+
font-size: 0.65rem;
|
|
16776
|
+
background: var(--bg-hover);
|
|
16777
|
+
color: var(--text-dim);
|
|
16778
|
+
margin-right: 0.25rem;
|
|
16779
|
+
margin-bottom: 0.15rem;
|
|
16780
|
+
white-space: nowrap;
|
|
16781
|
+
}
|
|
16782
|
+
|
|
16783
|
+
.text-dim { color: var(--text-dim); }
|
|
16236
16784
|
`;
|
|
16237
16785
|
}
|
|
16238
16786
|
|
|
@@ -16241,98 +16789,290 @@ function sanitize(text, maxLen = 40) {
|
|
|
16241
16789
|
const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
|
|
16242
16790
|
return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
|
|
16243
16791
|
}
|
|
16244
|
-
function mermaidBlock(definition) {
|
|
16245
|
-
|
|
16792
|
+
function mermaidBlock(definition, extraClass) {
|
|
16793
|
+
const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
|
|
16794
|
+
return `<div class="${cls}"><pre class="mermaid">
|
|
16246
16795
|
${definition}
|
|
16247
16796
|
</pre></div>`;
|
|
16248
16797
|
}
|
|
16249
16798
|
function placeholder(message) {
|
|
16250
16799
|
return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
|
|
16251
16800
|
}
|
|
16252
|
-
function
|
|
16253
|
-
|
|
16801
|
+
function toMs(date5) {
|
|
16802
|
+
return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
|
|
16803
|
+
}
|
|
16804
|
+
function fmtDate(ms) {
|
|
16805
|
+
const d = new Date(ms);
|
|
16806
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
16807
|
+
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
16808
|
+
}
|
|
16809
|
+
function buildTimelineGantt(data, maxSprints = 6) {
|
|
16810
|
+
const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
|
|
16254
16811
|
if (sprintsWithDates.length === 0) {
|
|
16255
16812
|
return placeholder("No timeline data available \u2014 sprints need start and end dates.");
|
|
16256
16813
|
}
|
|
16814
|
+
const truncated = sprintsWithDates.length > maxSprints;
|
|
16815
|
+
const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
|
|
16816
|
+
const hiddenCount = sprintsWithDates.length - visibleSprints.length;
|
|
16257
16817
|
const epicMap = new Map(data.epics.map((e) => [e.id, e]));
|
|
16258
|
-
const
|
|
16259
|
-
|
|
16260
|
-
|
|
16818
|
+
const allStarts = visibleSprints.map((s) => toMs(s.startDate));
|
|
16819
|
+
const allEnds = visibleSprints.map((s) => toMs(s.endDate));
|
|
16820
|
+
const timelineStart = Math.min(...allStarts);
|
|
16821
|
+
const timelineEnd = Math.max(...allEnds);
|
|
16822
|
+
const span = timelineEnd - timelineStart || 1;
|
|
16823
|
+
const pct = (ms) => (ms - timelineStart) / span * 100;
|
|
16824
|
+
const DAY = 864e5;
|
|
16825
|
+
const markers = [];
|
|
16826
|
+
let tick = timelineStart;
|
|
16827
|
+
const startDay = new Date(tick).getDay();
|
|
16828
|
+
tick += (8 - startDay) % 7 * DAY;
|
|
16829
|
+
while (tick <= timelineEnd) {
|
|
16830
|
+
const left = pct(tick);
|
|
16831
|
+
markers.push(
|
|
16832
|
+
`<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
|
|
16833
|
+
);
|
|
16834
|
+
tick += 7 * DAY;
|
|
16835
|
+
}
|
|
16836
|
+
const now = Date.now();
|
|
16837
|
+
let todayMarker = "";
|
|
16838
|
+
if (now >= timelineStart && now <= timelineEnd) {
|
|
16839
|
+
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
16840
|
+
}
|
|
16841
|
+
const rows = [];
|
|
16842
|
+
for (const sprint of visibleSprints) {
|
|
16843
|
+
const sStart = toMs(sprint.startDate);
|
|
16844
|
+
const sEnd = toMs(sprint.endDate);
|
|
16845
|
+
rows.push(`<div class="gantt-section-row">
|
|
16846
|
+
<div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
|
|
16847
|
+
<div class="gantt-track">
|
|
16848
|
+
<div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
|
|
16849
|
+
</div>
|
|
16850
|
+
</div>`);
|
|
16261
16851
|
const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16852
|
+
const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
|
|
16853
|
+
for (const item of items) {
|
|
16854
|
+
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";
|
|
16855
|
+
const left = pct(sStart).toFixed(2);
|
|
16856
|
+
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
16857
|
+
rows.push(`<div class="gantt-row">
|
|
16858
|
+
<div class="gantt-label">${item.label}</div>
|
|
16859
|
+
<div class="gantt-track">
|
|
16860
|
+
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
16861
|
+
</div>
|
|
16862
|
+
</div>`);
|
|
16269
16863
|
}
|
|
16270
16864
|
}
|
|
16271
|
-
|
|
16865
|
+
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
16866
|
+
return `${note}
|
|
16867
|
+
<div class="gantt">
|
|
16868
|
+
<div class="gantt-chart">
|
|
16869
|
+
<div class="gantt-header">
|
|
16870
|
+
<div class="gantt-label"></div>
|
|
16871
|
+
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
16872
|
+
</div>
|
|
16873
|
+
${rows.join("\n")}
|
|
16874
|
+
</div>
|
|
16875
|
+
<div class="gantt-overlay">
|
|
16876
|
+
<div class="gantt-label"></div>
|
|
16877
|
+
<div class="gantt-track">${todayMarker}</div>
|
|
16878
|
+
</div>
|
|
16879
|
+
</div>`;
|
|
16880
|
+
}
|
|
16881
|
+
function statusClass(status) {
|
|
16882
|
+
const s = status.toLowerCase();
|
|
16883
|
+
if (s === "done" || s === "completed") return "flow-done";
|
|
16884
|
+
if (s === "in-progress" || s === "active") return "flow-active";
|
|
16885
|
+
if (s === "blocked") return "flow-blocked";
|
|
16886
|
+
return "flow-default";
|
|
16272
16887
|
}
|
|
16273
16888
|
function buildArtifactFlowchart(data) {
|
|
16274
16889
|
if (data.features.length === 0 && data.epics.length === 0) {
|
|
16275
16890
|
return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
|
|
16276
16891
|
}
|
|
16277
|
-
const
|
|
16278
|
-
|
|
16279
|
-
lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
|
|
16280
|
-
lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
|
|
16281
|
-
lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
|
|
16282
|
-
const nodeIds = /* @__PURE__ */ new Set();
|
|
16892
|
+
const edges = [];
|
|
16893
|
+
const epicsByFeature = /* @__PURE__ */ new Map();
|
|
16283
16894
|
for (const epic of data.epics) {
|
|
16284
|
-
for (const
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16288
|
-
const eNode = epic.id.replace(/-/g, "_");
|
|
16289
|
-
if (!nodeIds.has(fNode)) {
|
|
16290
|
-
lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
|
|
16291
|
-
nodeIds.add(fNode);
|
|
16292
|
-
}
|
|
16293
|
-
if (!nodeIds.has(eNode)) {
|
|
16294
|
-
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
16295
|
-
nodeIds.add(eNode);
|
|
16296
|
-
}
|
|
16297
|
-
lines.push(` ${fNode} --> ${eNode}`);
|
|
16298
|
-
}
|
|
16895
|
+
for (const fid of epic.linkedFeature) {
|
|
16896
|
+
if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
|
|
16897
|
+
epicsByFeature.get(fid).push(epic.id);
|
|
16898
|
+
edges.push({ from: fid, to: epic.id });
|
|
16299
16899
|
}
|
|
16300
16900
|
}
|
|
16901
|
+
const sprintsByEpic = /* @__PURE__ */ new Map();
|
|
16301
16902
|
for (const sprint of data.sprints) {
|
|
16302
|
-
const
|
|
16303
|
-
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16308
|
-
|
|
16309
|
-
|
|
16903
|
+
for (const eid of sprint.linkedEpics) {
|
|
16904
|
+
if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
|
|
16905
|
+
sprintsByEpic.get(eid).push(sprint.id);
|
|
16906
|
+
edges.push({ from: eid, to: sprint.id });
|
|
16907
|
+
}
|
|
16908
|
+
}
|
|
16909
|
+
const connectedFeatureIds = new Set(epicsByFeature.keys());
|
|
16910
|
+
const connectedEpicIds = /* @__PURE__ */ new Set();
|
|
16911
|
+
for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
|
|
16912
|
+
for (const ids of sprintsByEpic.values()) ids.forEach(() => {
|
|
16913
|
+
});
|
|
16914
|
+
for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
|
|
16915
|
+
const connectedSprintIds = /* @__PURE__ */ new Set();
|
|
16916
|
+
for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
|
|
16917
|
+
const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
|
|
16918
|
+
const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
|
|
16919
|
+
const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
|
|
16920
|
+
if (features.length === 0 && epics.length === 0) {
|
|
16921
|
+
return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
|
|
16922
|
+
}
|
|
16923
|
+
const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
|
|
16924
|
+
<a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
|
|
16925
|
+
<span class="flow-node-title">${sanitize(title, 35)}</span>
|
|
16926
|
+
</div>`;
|
|
16927
|
+
const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
|
|
16928
|
+
const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
|
|
16929
|
+
const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
|
|
16930
|
+
const edgesJson = JSON.stringify(edges);
|
|
16931
|
+
return `
|
|
16932
|
+
<div class="flow-diagram" id="flow-diagram">
|
|
16933
|
+
<svg class="flow-lines" id="flow-lines"></svg>
|
|
16934
|
+
<div class="flow-columns">
|
|
16935
|
+
<div class="flow-column">
|
|
16936
|
+
<div class="flow-column-header">Features</div>
|
|
16937
|
+
${featuresHtml}
|
|
16938
|
+
</div>
|
|
16939
|
+
<div class="flow-column">
|
|
16940
|
+
<div class="flow-column-header">Epics</div>
|
|
16941
|
+
${epicsHtml}
|
|
16942
|
+
</div>
|
|
16943
|
+
<div class="flow-column">
|
|
16944
|
+
<div class="flow-column-header">Sprints</div>
|
|
16945
|
+
${sprintsHtml}
|
|
16946
|
+
</div>
|
|
16947
|
+
</div>
|
|
16948
|
+
</div>
|
|
16949
|
+
<script>
|
|
16950
|
+
(function() {
|
|
16951
|
+
var edges = ${edgesJson};
|
|
16952
|
+
var container = document.getElementById('flow-diagram');
|
|
16953
|
+
var svg = document.getElementById('flow-lines');
|
|
16954
|
+
if (!container || !svg) return;
|
|
16955
|
+
|
|
16956
|
+
// Build directed adjacency maps for traversal
|
|
16957
|
+
var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
|
|
16958
|
+
var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
|
|
16959
|
+
edges.forEach(function(e) {
|
|
16960
|
+
if (!fwd[e.from]) fwd[e.from] = [];
|
|
16961
|
+
if (!bwd[e.to]) bwd[e.to] = [];
|
|
16962
|
+
fwd[e.from].push(e.to);
|
|
16963
|
+
bwd[e.to].push(e.from);
|
|
16964
|
+
});
|
|
16965
|
+
|
|
16966
|
+
function drawLines() {
|
|
16967
|
+
var rect = container.getBoundingClientRect();
|
|
16968
|
+
svg.setAttribute('width', rect.width);
|
|
16969
|
+
svg.setAttribute('height', rect.height);
|
|
16970
|
+
svg.innerHTML = '';
|
|
16971
|
+
|
|
16972
|
+
edges.forEach(function(edge) {
|
|
16973
|
+
var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
|
|
16974
|
+
var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
|
|
16975
|
+
if (!fromEl || !toEl) return;
|
|
16976
|
+
|
|
16977
|
+
var fr = fromEl.getBoundingClientRect();
|
|
16978
|
+
var tr = toEl.getBoundingClientRect();
|
|
16979
|
+
var x1 = fr.right - rect.left;
|
|
16980
|
+
var y1 = fr.top + fr.height / 2 - rect.top;
|
|
16981
|
+
var x2 = tr.left - rect.left;
|
|
16982
|
+
var y2 = tr.top + tr.height / 2 - rect.top;
|
|
16983
|
+
var mx = (x1 + x2) / 2;
|
|
16984
|
+
|
|
16985
|
+
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
16986
|
+
path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
|
|
16987
|
+
path.setAttribute('fill', 'none');
|
|
16988
|
+
path.setAttribute('stroke', '#2a2e3a');
|
|
16989
|
+
path.setAttribute('stroke-width', '1.5');
|
|
16990
|
+
path.dataset.from = edge.from;
|
|
16991
|
+
path.dataset.to = edge.to;
|
|
16992
|
+
svg.appendChild(path);
|
|
16993
|
+
});
|
|
16994
|
+
}
|
|
16995
|
+
|
|
16996
|
+
// Find directly related nodes via directed traversal
|
|
16997
|
+
// Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
|
|
16998
|
+
// (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
|
|
16999
|
+
function findConnected(startId) {
|
|
17000
|
+
var visited = {};
|
|
17001
|
+
visited[startId] = true;
|
|
17002
|
+
// Traverse forward (from\u2192to direction)
|
|
17003
|
+
var queue = [startId];
|
|
17004
|
+
while (queue.length) {
|
|
17005
|
+
var id = queue.shift();
|
|
17006
|
+
(fwd[id] || []).forEach(function(neighbor) {
|
|
17007
|
+
if (!visited[neighbor]) {
|
|
17008
|
+
visited[neighbor] = true;
|
|
17009
|
+
queue.push(neighbor);
|
|
17010
|
+
}
|
|
17011
|
+
});
|
|
16310
17012
|
}
|
|
16311
|
-
|
|
16312
|
-
|
|
16313
|
-
|
|
17013
|
+
// Traverse backward (to\u2192from direction)
|
|
17014
|
+
queue = [startId];
|
|
17015
|
+
while (queue.length) {
|
|
17016
|
+
var id = queue.shift();
|
|
17017
|
+
(bwd[id] || []).forEach(function(neighbor) {
|
|
17018
|
+
if (!visited[neighbor]) {
|
|
17019
|
+
visited[neighbor] = true;
|
|
17020
|
+
queue.push(neighbor);
|
|
17021
|
+
}
|
|
17022
|
+
});
|
|
16314
17023
|
}
|
|
16315
|
-
|
|
17024
|
+
return visited;
|
|
16316
17025
|
}
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
|
|
16321
|
-
|
|
16322
|
-
|
|
16323
|
-
|
|
16324
|
-
|
|
16325
|
-
|
|
16326
|
-
|
|
16327
|
-
|
|
16328
|
-
|
|
16329
|
-
|
|
16330
|
-
|
|
16331
|
-
|
|
16332
|
-
|
|
16333
|
-
|
|
16334
|
-
|
|
16335
|
-
|
|
17026
|
+
|
|
17027
|
+
function highlight(hoveredId) {
|
|
17028
|
+
var connected = findConnected(hoveredId);
|
|
17029
|
+
container.querySelectorAll('.flow-node').forEach(function(n) {
|
|
17030
|
+
if (connected[n.dataset.flowId]) {
|
|
17031
|
+
n.classList.add('flow-lit');
|
|
17032
|
+
n.classList.remove('flow-dim');
|
|
17033
|
+
} else {
|
|
17034
|
+
n.classList.add('flow-dim');
|
|
17035
|
+
n.classList.remove('flow-lit');
|
|
17036
|
+
}
|
|
17037
|
+
});
|
|
17038
|
+
svg.querySelectorAll('path').forEach(function(p) {
|
|
17039
|
+
if (connected[p.dataset.from] && connected[p.dataset.to]) {
|
|
17040
|
+
p.classList.add('flow-line-lit');
|
|
17041
|
+
p.classList.remove('flow-line-dim');
|
|
17042
|
+
} else {
|
|
17043
|
+
p.classList.add('flow-line-dim');
|
|
17044
|
+
p.classList.remove('flow-line-lit');
|
|
17045
|
+
}
|
|
17046
|
+
});
|
|
17047
|
+
}
|
|
17048
|
+
|
|
17049
|
+
function clearHighlight() {
|
|
17050
|
+
container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
|
|
17051
|
+
svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
|
|
17052
|
+
}
|
|
17053
|
+
|
|
17054
|
+
var activeId = null;
|
|
17055
|
+
container.addEventListener('click', function(e) {
|
|
17056
|
+
// Let the ID link navigate normally
|
|
17057
|
+
if (e.target.closest('a')) return;
|
|
17058
|
+
|
|
17059
|
+
var node = e.target.closest('.flow-node');
|
|
17060
|
+
var clickedId = node ? node.dataset.flowId : null;
|
|
17061
|
+
|
|
17062
|
+
if (!clickedId || clickedId === activeId) {
|
|
17063
|
+
activeId = null;
|
|
17064
|
+
clearHighlight();
|
|
17065
|
+
return;
|
|
17066
|
+
}
|
|
17067
|
+
|
|
17068
|
+
activeId = clickedId;
|
|
17069
|
+
highlight(clickedId);
|
|
17070
|
+
});
|
|
17071
|
+
|
|
17072
|
+
requestAnimationFrame(function() { setTimeout(drawLines, 100); });
|
|
17073
|
+
window.addEventListener('resize', drawLines);
|
|
17074
|
+
})();
|
|
17075
|
+
</script>`;
|
|
16336
17076
|
}
|
|
16337
17077
|
function buildStatusPie(title, counts) {
|
|
16338
17078
|
const entries = Object.entries(counts).filter(([, v]) => v > 0);
|
|
@@ -16412,8 +17152,7 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
16412
17152
|
${groupSections}
|
|
16413
17153
|
${ungroupedSection}
|
|
16414
17154
|
|
|
16415
|
-
<div class="section-title">Project Timeline
|
|
16416
|
-
${buildTimelineGantt(diagrams)}
|
|
17155
|
+
<div class="section-title"><a href="/timeline">Project Timeline →</a></div>
|
|
16417
17156
|
|
|
16418
17157
|
<div class="section-title">Artifact Relationships</div>
|
|
16419
17158
|
${buildArtifactFlowchart(diagrams)}
|
|
@@ -16668,6 +17407,7 @@ function boardPage(data) {
|
|
|
16668
17407
|
<span>${escapeHtml(col.status)}</span>
|
|
16669
17408
|
<span class="count">${col.docs.length}</span>
|
|
16670
17409
|
</div>
|
|
17410
|
+
<div class="board-column-cards">
|
|
16671
17411
|
${col.docs.map(
|
|
16672
17412
|
(doc) => `
|
|
16673
17413
|
<div class="board-card">
|
|
@@ -16678,6 +17418,7 @@ function boardPage(data) {
|
|
|
16678
17418
|
</a>
|
|
16679
17419
|
</div>`
|
|
16680
17420
|
).join("\n")}
|
|
17421
|
+
</div>
|
|
16681
17422
|
</div>`
|
|
16682
17423
|
).join("\n");
|
|
16683
17424
|
return `
|
|
@@ -16703,6 +17444,143 @@ function boardPage(data) {
|
|
|
16703
17444
|
`;
|
|
16704
17445
|
}
|
|
16705
17446
|
|
|
17447
|
+
// src/web/templates/pages/timeline.ts
|
|
17448
|
+
function timelinePage(diagrams) {
|
|
17449
|
+
return `
|
|
17450
|
+
<div class="page-header">
|
|
17451
|
+
<h2>Project Timeline</h2>
|
|
17452
|
+
<div class="subtitle">Sprint schedule with linked epics</div>
|
|
17453
|
+
</div>
|
|
17454
|
+
|
|
17455
|
+
${buildTimelineGantt(diagrams)}
|
|
17456
|
+
`;
|
|
17457
|
+
}
|
|
17458
|
+
|
|
17459
|
+
// src/web/templates/pages/upcoming.ts
|
|
17460
|
+
function urgencyBadge(tier) {
|
|
17461
|
+
const labels = {
|
|
17462
|
+
overdue: "Overdue",
|
|
17463
|
+
"due-3d": "Due in 3d",
|
|
17464
|
+
"due-7d": "Due in 7d",
|
|
17465
|
+
upcoming: "Upcoming",
|
|
17466
|
+
later: "Later"
|
|
17467
|
+
};
|
|
17468
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
17469
|
+
}
|
|
17470
|
+
function urgencyRowClass(tier) {
|
|
17471
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
17472
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
17473
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
17474
|
+
return "";
|
|
17475
|
+
}
|
|
17476
|
+
function upcomingPage(data) {
|
|
17477
|
+
const hasActions = data.dueSoonActions.length > 0;
|
|
17478
|
+
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
17479
|
+
const hasTrending = data.trending.length > 0;
|
|
17480
|
+
const actionsTable = hasActions ? `
|
|
17481
|
+
<h3 class="section-title">Due Soon \u2014 Actions</h3>
|
|
17482
|
+
<div class="table-wrap">
|
|
17483
|
+
<table>
|
|
17484
|
+
<thead>
|
|
17485
|
+
<tr>
|
|
17486
|
+
<th>ID</th>
|
|
17487
|
+
<th>Title</th>
|
|
17488
|
+
<th>Status</th>
|
|
17489
|
+
<th>Owner</th>
|
|
17490
|
+
<th>Due Date</th>
|
|
17491
|
+
<th>Urgency</th>
|
|
17492
|
+
<th>Tasks</th>
|
|
17493
|
+
</tr>
|
|
17494
|
+
</thead>
|
|
17495
|
+
<tbody>
|
|
17496
|
+
${data.dueSoonActions.map(
|
|
17497
|
+
(a) => `
|
|
17498
|
+
<tr class="${urgencyRowClass(a.urgency)}">
|
|
17499
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
17500
|
+
<td>${escapeHtml(a.title)}</td>
|
|
17501
|
+
<td>${statusBadge(a.status)}</td>
|
|
17502
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
17503
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
17504
|
+
<td>${urgencyBadge(a.urgency)}</td>
|
|
17505
|
+
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
17506
|
+
</tr>`
|
|
17507
|
+
).join("")}
|
|
17508
|
+
</tbody>
|
|
17509
|
+
</table>
|
|
17510
|
+
</div>` : "";
|
|
17511
|
+
const sprintTasksTable = hasSprintTasks ? `
|
|
17512
|
+
<h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
|
|
17513
|
+
<div class="table-wrap">
|
|
17514
|
+
<table>
|
|
17515
|
+
<thead>
|
|
17516
|
+
<tr>
|
|
17517
|
+
<th>ID</th>
|
|
17518
|
+
<th>Title</th>
|
|
17519
|
+
<th>Status</th>
|
|
17520
|
+
<th>Sprint</th>
|
|
17521
|
+
<th>Sprint Ends</th>
|
|
17522
|
+
<th>Urgency</th>
|
|
17523
|
+
</tr>
|
|
17524
|
+
</thead>
|
|
17525
|
+
<tbody>
|
|
17526
|
+
${data.dueSoonSprintTasks.map(
|
|
17527
|
+
(t) => `
|
|
17528
|
+
<tr class="${urgencyRowClass(t.urgency)}">
|
|
17529
|
+
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17530
|
+
<td>${escapeHtml(t.title)}</td>
|
|
17531
|
+
<td>${statusBadge(t.status)}</td>
|
|
17532
|
+
<td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
|
|
17533
|
+
<td>${formatDate(t.sprintEndDate)}</td>
|
|
17534
|
+
<td>${urgencyBadge(t.urgency)}</td>
|
|
17535
|
+
</tr>`
|
|
17536
|
+
).join("")}
|
|
17537
|
+
</tbody>
|
|
17538
|
+
</table>
|
|
17539
|
+
</div>` : "";
|
|
17540
|
+
const trendingTable = hasTrending ? `
|
|
17541
|
+
<h3 class="section-title">Trending</h3>
|
|
17542
|
+
<div class="table-wrap">
|
|
17543
|
+
<table>
|
|
17544
|
+
<thead>
|
|
17545
|
+
<tr>
|
|
17546
|
+
<th>#</th>
|
|
17547
|
+
<th>ID</th>
|
|
17548
|
+
<th>Title</th>
|
|
17549
|
+
<th>Type</th>
|
|
17550
|
+
<th>Status</th>
|
|
17551
|
+
<th>Score</th>
|
|
17552
|
+
<th>Signals</th>
|
|
17553
|
+
</tr>
|
|
17554
|
+
</thead>
|
|
17555
|
+
<tbody>
|
|
17556
|
+
${data.trending.map(
|
|
17557
|
+
(t, i) => `
|
|
17558
|
+
<tr>
|
|
17559
|
+
<td><span class="trending-rank">${i + 1}</span></td>
|
|
17560
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17561
|
+
<td>${escapeHtml(t.title)}</td>
|
|
17562
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
17563
|
+
<td>${statusBadge(t.status)}</td>
|
|
17564
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
17565
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
17566
|
+
</tr>`
|
|
17567
|
+
).join("")}
|
|
17568
|
+
</tbody>
|
|
17569
|
+
</table>
|
|
17570
|
+
</div>` : "";
|
|
17571
|
+
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
17572
|
+
return `
|
|
17573
|
+
<div class="page-header">
|
|
17574
|
+
<h2>Upcoming</h2>
|
|
17575
|
+
<div class="subtitle">Time-sensitive items and trending activity</div>
|
|
17576
|
+
</div>
|
|
17577
|
+
${actionsTable}
|
|
17578
|
+
${sprintTasksTable}
|
|
17579
|
+
${trendingTable}
|
|
17580
|
+
${emptyState}
|
|
17581
|
+
`;
|
|
17582
|
+
}
|
|
17583
|
+
|
|
16706
17584
|
// src/web/router.ts
|
|
16707
17585
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
16708
17586
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -16724,6 +17602,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
16724
17602
|
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
|
|
16725
17603
|
return;
|
|
16726
17604
|
}
|
|
17605
|
+
if (pathname === "/timeline") {
|
|
17606
|
+
const diagrams = getDiagramData(store);
|
|
17607
|
+
const body = timelinePage(diagrams);
|
|
17608
|
+
respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, mainClass: "expanded" }, body));
|
|
17609
|
+
return;
|
|
17610
|
+
}
|
|
16727
17611
|
if (pathname === "/gar") {
|
|
16728
17612
|
const report = getGarData(store, projectName);
|
|
16729
17613
|
const body = garPage(report);
|
|
@@ -16737,6 +17621,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
16737
17621
|
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
16738
17622
|
return;
|
|
16739
17623
|
}
|
|
17624
|
+
if (pathname === "/upcoming") {
|
|
17625
|
+
const data = getUpcomingData(store);
|
|
17626
|
+
const body = upcomingPage(data);
|
|
17627
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
17628
|
+
return;
|
|
17629
|
+
}
|
|
16740
17630
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
16741
17631
|
if (boardMatch) {
|
|
16742
17632
|
const type = boardMatch[1];
|
|
@@ -18107,6 +18997,185 @@ function createSprintPlanningTools(store) {
|
|
|
18107
18997
|
];
|
|
18108
18998
|
}
|
|
18109
18999
|
|
|
19000
|
+
// src/plugins/builtin/tools/tasks.ts
|
|
19001
|
+
import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
|
|
19002
|
+
var linkedEpicArray = external_exports.preprocess(
|
|
19003
|
+
(val) => {
|
|
19004
|
+
if (typeof val === "string") {
|
|
19005
|
+
try {
|
|
19006
|
+
const parsed = JSON.parse(val);
|
|
19007
|
+
if (Array.isArray(parsed)) return parsed;
|
|
19008
|
+
} catch {
|
|
19009
|
+
}
|
|
19010
|
+
return [val];
|
|
19011
|
+
}
|
|
19012
|
+
return val;
|
|
19013
|
+
},
|
|
19014
|
+
external_exports.array(external_exports.string())
|
|
19015
|
+
);
|
|
19016
|
+
function createTaskTools(store) {
|
|
19017
|
+
return [
|
|
19018
|
+
tool14(
|
|
19019
|
+
"list_tasks",
|
|
19020
|
+
"List all tasks in the project, optionally filtered by status, linked epic, or priority",
|
|
19021
|
+
{
|
|
19022
|
+
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Filter by task status"),
|
|
19023
|
+
linkedEpic: external_exports.string().optional().describe("Filter by linked epic ID (e.g. 'E-001')"),
|
|
19024
|
+
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority")
|
|
19025
|
+
},
|
|
19026
|
+
async (args) => {
|
|
19027
|
+
let docs = store.list({ type: "task", status: args.status });
|
|
19028
|
+
if (args.linkedEpic) {
|
|
19029
|
+
docs = docs.filter(
|
|
19030
|
+
(d) => normalizeLinkedEpics(d.frontmatter.linkedEpic).includes(args.linkedEpic)
|
|
19031
|
+
);
|
|
19032
|
+
}
|
|
19033
|
+
if (args.priority) {
|
|
19034
|
+
docs = docs.filter((d) => d.frontmatter.priority === args.priority);
|
|
19035
|
+
}
|
|
19036
|
+
const summary = docs.map((d) => ({
|
|
19037
|
+
id: d.frontmatter.id,
|
|
19038
|
+
title: d.frontmatter.title,
|
|
19039
|
+
status: d.frontmatter.status,
|
|
19040
|
+
linkedEpic: normalizeLinkedEpics(d.frontmatter.linkedEpic),
|
|
19041
|
+
priority: d.frontmatter.priority,
|
|
19042
|
+
complexity: d.frontmatter.complexity,
|
|
19043
|
+
estimatedPoints: d.frontmatter.estimatedPoints,
|
|
19044
|
+
tags: d.frontmatter.tags
|
|
19045
|
+
}));
|
|
19046
|
+
return {
|
|
19047
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
19048
|
+
};
|
|
19049
|
+
},
|
|
19050
|
+
{ annotations: { readOnlyHint: true } }
|
|
19051
|
+
),
|
|
19052
|
+
tool14(
|
|
19053
|
+
"get_task",
|
|
19054
|
+
"Get the full content of a specific task by ID",
|
|
19055
|
+
{ id: external_exports.string().describe("Task ID (e.g. 'T-001')") },
|
|
19056
|
+
async (args) => {
|
|
19057
|
+
const doc = store.get(args.id);
|
|
19058
|
+
if (!doc) {
|
|
19059
|
+
return {
|
|
19060
|
+
content: [{ type: "text", text: `Task ${args.id} not found` }],
|
|
19061
|
+
isError: true
|
|
19062
|
+
};
|
|
19063
|
+
}
|
|
19064
|
+
return {
|
|
19065
|
+
content: [
|
|
19066
|
+
{
|
|
19067
|
+
type: "text",
|
|
19068
|
+
text: JSON.stringify(
|
|
19069
|
+
{ ...doc.frontmatter, content: doc.content },
|
|
19070
|
+
null,
|
|
19071
|
+
2
|
|
19072
|
+
)
|
|
19073
|
+
}
|
|
19074
|
+
]
|
|
19075
|
+
};
|
|
19076
|
+
},
|
|
19077
|
+
{ annotations: { readOnlyHint: true } }
|
|
19078
|
+
),
|
|
19079
|
+
tool14(
|
|
19080
|
+
"create_task",
|
|
19081
|
+
"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).",
|
|
19082
|
+
{
|
|
19083
|
+
title: external_exports.string().describe("Task title"),
|
|
19084
|
+
content: external_exports.string().describe("Task description and implementation details"),
|
|
19085
|
+
linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
|
|
19086
|
+
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
|
|
19087
|
+
acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
|
|
19088
|
+
technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
|
|
19089
|
+
estimatedPoints: external_exports.number().optional().describe("Story point estimate"),
|
|
19090
|
+
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
|
|
19091
|
+
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
|
|
19092
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
|
|
19093
|
+
},
|
|
19094
|
+
async (args) => {
|
|
19095
|
+
const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
|
|
19096
|
+
const warnings = [];
|
|
19097
|
+
for (const epicId of linkedEpics) {
|
|
19098
|
+
const epic = store.get(epicId);
|
|
19099
|
+
if (!epic) {
|
|
19100
|
+
warnings.push(`Warning: Epic ${epicId} not found`);
|
|
19101
|
+
} else if (epic.frontmatter.type !== "epic") {
|
|
19102
|
+
warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
|
|
19103
|
+
}
|
|
19104
|
+
}
|
|
19105
|
+
const frontmatter = {
|
|
19106
|
+
title: args.title,
|
|
19107
|
+
status: args.status ?? "backlog",
|
|
19108
|
+
linkedEpic: linkedEpics,
|
|
19109
|
+
tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
|
|
19110
|
+
};
|
|
19111
|
+
if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
|
|
19112
|
+
if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
|
|
19113
|
+
if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
|
|
19114
|
+
if (args.complexity) frontmatter.complexity = args.complexity;
|
|
19115
|
+
if (args.priority) frontmatter.priority = args.priority;
|
|
19116
|
+
const doc = store.create("task", frontmatter, args.content);
|
|
19117
|
+
const parts = [
|
|
19118
|
+
`Created task ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${linkedEpics.join(", ")})`
|
|
19119
|
+
];
|
|
19120
|
+
if (warnings.length > 0) {
|
|
19121
|
+
parts.push(warnings.join("; "));
|
|
19122
|
+
}
|
|
19123
|
+
return {
|
|
19124
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
19125
|
+
};
|
|
19126
|
+
}
|
|
19127
|
+
),
|
|
19128
|
+
tool14(
|
|
19129
|
+
"update_task",
|
|
19130
|
+
"Update an existing task, including its linked epics.",
|
|
19131
|
+
{
|
|
19132
|
+
id: external_exports.string().describe("Task ID to update"),
|
|
19133
|
+
title: external_exports.string().optional().describe("New title"),
|
|
19134
|
+
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
|
|
19135
|
+
content: external_exports.string().optional().describe("New content"),
|
|
19136
|
+
linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
|
|
19137
|
+
acceptanceCriteria: external_exports.string().optional().describe("New acceptance criteria"),
|
|
19138
|
+
technicalNotes: external_exports.string().optional().describe("New technical notes"),
|
|
19139
|
+
estimatedPoints: external_exports.number().optional().describe("New story point estimate"),
|
|
19140
|
+
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
|
|
19141
|
+
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
19142
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)")
|
|
19143
|
+
},
|
|
19144
|
+
async (args) => {
|
|
19145
|
+
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, ...updates } = args;
|
|
19146
|
+
const warnings = [];
|
|
19147
|
+
if (rawLinkedEpic !== void 0) {
|
|
19148
|
+
const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
|
|
19149
|
+
for (const epicId of linkedEpics) {
|
|
19150
|
+
const epic = store.get(epicId);
|
|
19151
|
+
if (!epic) {
|
|
19152
|
+
warnings.push(`Warning: Epic ${epicId} not found`);
|
|
19153
|
+
} else if (epic.frontmatter.type !== "epic") {
|
|
19154
|
+
warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
|
|
19155
|
+
}
|
|
19156
|
+
}
|
|
19157
|
+
updates.linkedEpic = linkedEpics;
|
|
19158
|
+
const existingDoc = store.get(id);
|
|
19159
|
+
const existingTags = existingDoc?.frontmatter.tags ?? [];
|
|
19160
|
+
const nonEpicTags = existingTags.filter((t) => !t.startsWith("epic:"));
|
|
19161
|
+
const baseTags = userTags ?? nonEpicTags;
|
|
19162
|
+
updates.tags = [...generateEpicTags(linkedEpics), ...baseTags];
|
|
19163
|
+
} else if (userTags !== void 0) {
|
|
19164
|
+
updates.tags = userTags;
|
|
19165
|
+
}
|
|
19166
|
+
const doc = store.update(id, updates, content);
|
|
19167
|
+
const parts = [`Updated task ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
|
|
19168
|
+
if (warnings.length > 0) {
|
|
19169
|
+
parts.push(warnings.join("; "));
|
|
19170
|
+
}
|
|
19171
|
+
return {
|
|
19172
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
19173
|
+
};
|
|
19174
|
+
}
|
|
19175
|
+
)
|
|
19176
|
+
];
|
|
19177
|
+
}
|
|
19178
|
+
|
|
18110
19179
|
// src/plugins/common.ts
|
|
18111
19180
|
var COMMON_REGISTRATIONS = [
|
|
18112
19181
|
{ type: "meeting", dirName: "meetings", idPrefix: "M" },
|
|
@@ -18114,7 +19183,8 @@ var COMMON_REGISTRATIONS = [
|
|
|
18114
19183
|
{ type: "feature", dirName: "features", idPrefix: "F" },
|
|
18115
19184
|
{ type: "epic", dirName: "epics", idPrefix: "E" },
|
|
18116
19185
|
{ type: "contribution", dirName: "contributions", idPrefix: "C" },
|
|
18117
|
-
{ type: "sprint", dirName: "sprints", idPrefix: "SP" }
|
|
19186
|
+
{ type: "sprint", dirName: "sprints", idPrefix: "SP" },
|
|
19187
|
+
{ type: "task", dirName: "tasks", idPrefix: "T" }
|
|
18118
19188
|
];
|
|
18119
19189
|
function createCommonTools(store) {
|
|
18120
19190
|
return [
|
|
@@ -18124,7 +19194,8 @@ function createCommonTools(store) {
|
|
|
18124
19194
|
...createEpicTools(store),
|
|
18125
19195
|
...createContributionTools(store),
|
|
18126
19196
|
...createSprintTools(store),
|
|
18127
|
-
...createSprintPlanningTools(store)
|
|
19197
|
+
...createSprintPlanningTools(store),
|
|
19198
|
+
...createTaskTools(store)
|
|
18128
19199
|
];
|
|
18129
19200
|
}
|
|
18130
19201
|
|
|
@@ -18134,7 +19205,7 @@ var genericAgilePlugin = {
|
|
|
18134
19205
|
name: "Generic Agile",
|
|
18135
19206
|
description: "Default methodology plugin providing standard agile governance patterns for decisions, actions, and questions.",
|
|
18136
19207
|
version: "0.1.0",
|
|
18137
|
-
documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint"],
|
|
19208
|
+
documentTypes: ["decision", "action", "question", "meeting", "report", "feature", "epic", "contribution", "sprint", "task"],
|
|
18138
19209
|
documentTypeRegistrations: [...COMMON_REGISTRATIONS],
|
|
18139
19210
|
tools: (store) => [...createCommonTools(store)],
|
|
18140
19211
|
promptFragments: {
|
|
@@ -18173,6 +19244,11 @@ var genericAgilePlugin = {
|
|
|
18173
19244
|
- **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.
|
|
18174
19245
|
- **update_epic**: Update epic status (planned \u2192 in-progress \u2192 done), owner, and other fields.
|
|
18175
19246
|
|
|
19247
|
+
**Task Tools:**
|
|
19248
|
+
- **list_tasks** / **get_task**: Browse and read implementation tasks.
|
|
19249
|
+
- **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".
|
|
19250
|
+
- **update_task**: Update task status (backlog \u2192 ready \u2192 in-progress \u2192 review \u2192 done), acceptance criteria, technical notes, complexity, priority, and estimated points.
|
|
19251
|
+
|
|
18176
19252
|
**Feature Tools (read-only for awareness):**
|
|
18177
19253
|
- **list_features** / **get_feature**: View features to understand what needs to be broken into epics.
|
|
18178
19254
|
|
|
@@ -18184,6 +19260,7 @@ var genericAgilePlugin = {
|
|
|
18184
19260
|
|
|
18185
19261
|
**Key Workflow Rules:**
|
|
18186
19262
|
- Only create epics against approved features \u2014 create_epic enforces this.
|
|
19263
|
+
- Break epics into tasks (T-xxx) with clear acceptance criteria and complexity estimates.
|
|
18187
19264
|
- Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
|
|
18188
19265
|
- Collaborate with the Delivery Manager on target dates and effort estimates.
|
|
18189
19266
|
- Each epic should have a clear scope and definition of done.
|
|
@@ -18219,6 +19296,9 @@ var genericAgilePlugin = {
|
|
|
18219
19296
|
- **list_epics** / **get_epic**: View epics and their current status.
|
|
18220
19297
|
- **update_epic**: Set targetDate and estimatedEffort on epics. Flag epics linked to deferred features.
|
|
18221
19298
|
|
|
19299
|
+
**Task Tools (read-only for tracking):**
|
|
19300
|
+
- **list_tasks** / **get_task**: View tasks and their statuses. Filter by linkedEpic to see implementation breakdown.
|
|
19301
|
+
|
|
18222
19302
|
**Feature Tools (tracking focus):**
|
|
18223
19303
|
- **list_features** / **get_feature**: View features and their priorities.
|
|
18224
19304
|
|
|
@@ -18264,14 +19344,15 @@ var genericAgilePlugin = {
|
|
|
18264
19344
|
- Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
|
|
18265
19345
|
- Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
|
|
18266
19346
|
- After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
|
|
18267
|
-
"*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
|
|
19347
|
+
"*": `You have access to feature, epic, task, sprint, and meeting tools for project coordination:
|
|
18268
19348
|
|
|
18269
19349
|
**Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
|
|
18270
19350
|
**Epics** (E-xxx): Implementation work packages created by the Tech Lead, linked to approved features. Epics progress through planned \u2192 in-progress \u2192 done.
|
|
19351
|
+
**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.
|
|
18271
19352
|
**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).
|
|
18272
19353
|
**Meetings**: Meeting records with attendees, agendas, and notes.
|
|
18273
19354
|
|
|
18274
|
-
**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.
|
|
19355
|
+
**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.
|
|
18275
19356
|
|
|
18276
19357
|
- **list_meetings** / **get_meeting**: Browse and read meeting records.
|
|
18277
19358
|
- **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.
|
|
@@ -18286,10 +19367,10 @@ var genericAgilePlugin = {
|
|
|
18286
19367
|
};
|
|
18287
19368
|
|
|
18288
19369
|
// src/plugins/builtin/tools/use-cases.ts
|
|
18289
|
-
import { tool as
|
|
19370
|
+
import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
|
|
18290
19371
|
function createUseCaseTools(store) {
|
|
18291
19372
|
return [
|
|
18292
|
-
|
|
19373
|
+
tool15(
|
|
18293
19374
|
"list_use_cases",
|
|
18294
19375
|
"List all extension use cases, optionally filtered by status or extension type",
|
|
18295
19376
|
{
|
|
@@ -18319,7 +19400,7 @@ function createUseCaseTools(store) {
|
|
|
18319
19400
|
},
|
|
18320
19401
|
{ annotations: { readOnlyHint: true } }
|
|
18321
19402
|
),
|
|
18322
|
-
|
|
19403
|
+
tool15(
|
|
18323
19404
|
"get_use_case",
|
|
18324
19405
|
"Get the full content of a specific use case by ID",
|
|
18325
19406
|
{ id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
|
|
@@ -18346,7 +19427,7 @@ function createUseCaseTools(store) {
|
|
|
18346
19427
|
},
|
|
18347
19428
|
{ annotations: { readOnlyHint: true } }
|
|
18348
19429
|
),
|
|
18349
|
-
|
|
19430
|
+
tool15(
|
|
18350
19431
|
"create_use_case",
|
|
18351
19432
|
"Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
|
|
18352
19433
|
{
|
|
@@ -18380,7 +19461,7 @@ function createUseCaseTools(store) {
|
|
|
18380
19461
|
};
|
|
18381
19462
|
}
|
|
18382
19463
|
),
|
|
18383
|
-
|
|
19464
|
+
tool15(
|
|
18384
19465
|
"update_use_case",
|
|
18385
19466
|
"Update an existing extension use case",
|
|
18386
19467
|
{
|
|
@@ -18410,10 +19491,10 @@ function createUseCaseTools(store) {
|
|
|
18410
19491
|
}
|
|
18411
19492
|
|
|
18412
19493
|
// src/plugins/builtin/tools/tech-assessments.ts
|
|
18413
|
-
import { tool as
|
|
19494
|
+
import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
|
|
18414
19495
|
function createTechAssessmentTools(store) {
|
|
18415
19496
|
return [
|
|
18416
|
-
|
|
19497
|
+
tool16(
|
|
18417
19498
|
"list_tech_assessments",
|
|
18418
19499
|
"List all technology assessments, optionally filtered by status",
|
|
18419
19500
|
{
|
|
@@ -18444,7 +19525,7 @@ function createTechAssessmentTools(store) {
|
|
|
18444
19525
|
},
|
|
18445
19526
|
{ annotations: { readOnlyHint: true } }
|
|
18446
19527
|
),
|
|
18447
|
-
|
|
19528
|
+
tool16(
|
|
18448
19529
|
"get_tech_assessment",
|
|
18449
19530
|
"Get the full content of a specific technology assessment by ID",
|
|
18450
19531
|
{ id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
|
|
@@ -18471,7 +19552,7 @@ function createTechAssessmentTools(store) {
|
|
|
18471
19552
|
},
|
|
18472
19553
|
{ annotations: { readOnlyHint: true } }
|
|
18473
19554
|
),
|
|
18474
|
-
|
|
19555
|
+
tool16(
|
|
18475
19556
|
"create_tech_assessment",
|
|
18476
19557
|
"Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
|
|
18477
19558
|
{
|
|
@@ -18542,7 +19623,7 @@ function createTechAssessmentTools(store) {
|
|
|
18542
19623
|
};
|
|
18543
19624
|
}
|
|
18544
19625
|
),
|
|
18545
|
-
|
|
19626
|
+
tool16(
|
|
18546
19627
|
"update_tech_assessment",
|
|
18547
19628
|
"Update an existing technology assessment. The linked use case cannot be changed.",
|
|
18548
19629
|
{
|
|
@@ -18572,10 +19653,10 @@ function createTechAssessmentTools(store) {
|
|
|
18572
19653
|
}
|
|
18573
19654
|
|
|
18574
19655
|
// src/plugins/builtin/tools/extension-designs.ts
|
|
18575
|
-
import { tool as
|
|
19656
|
+
import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
|
|
18576
19657
|
function createExtensionDesignTools(store) {
|
|
18577
19658
|
return [
|
|
18578
|
-
|
|
19659
|
+
tool17(
|
|
18579
19660
|
"list_extension_designs",
|
|
18580
19661
|
"List all extension designs, optionally filtered by status",
|
|
18581
19662
|
{
|
|
@@ -18605,7 +19686,7 @@ function createExtensionDesignTools(store) {
|
|
|
18605
19686
|
},
|
|
18606
19687
|
{ annotations: { readOnlyHint: true } }
|
|
18607
19688
|
),
|
|
18608
|
-
|
|
19689
|
+
tool17(
|
|
18609
19690
|
"get_extension_design",
|
|
18610
19691
|
"Get the full content of a specific extension design by ID",
|
|
18611
19692
|
{ id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
|
|
@@ -18632,7 +19713,7 @@ function createExtensionDesignTools(store) {
|
|
|
18632
19713
|
},
|
|
18633
19714
|
{ annotations: { readOnlyHint: true } }
|
|
18634
19715
|
),
|
|
18635
|
-
|
|
19716
|
+
tool17(
|
|
18636
19717
|
"create_extension_design",
|
|
18637
19718
|
"Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
|
|
18638
19719
|
{
|
|
@@ -18700,7 +19781,7 @@ function createExtensionDesignTools(store) {
|
|
|
18700
19781
|
};
|
|
18701
19782
|
}
|
|
18702
19783
|
),
|
|
18703
|
-
|
|
19784
|
+
tool17(
|
|
18704
19785
|
"update_extension_design",
|
|
18705
19786
|
"Update an existing extension design. The linked tech assessment cannot be changed.",
|
|
18706
19787
|
{
|
|
@@ -18729,10 +19810,10 @@ function createExtensionDesignTools(store) {
|
|
|
18729
19810
|
}
|
|
18730
19811
|
|
|
18731
19812
|
// src/plugins/builtin/tools/aem-reports.ts
|
|
18732
|
-
import { tool as
|
|
19813
|
+
import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
|
|
18733
19814
|
function createAemReportTools(store) {
|
|
18734
19815
|
return [
|
|
18735
|
-
|
|
19816
|
+
tool18(
|
|
18736
19817
|
"generate_extension_portfolio",
|
|
18737
19818
|
"Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
|
|
18738
19819
|
{},
|
|
@@ -18784,7 +19865,7 @@ function createAemReportTools(store) {
|
|
|
18784
19865
|
},
|
|
18785
19866
|
{ annotations: { readOnlyHint: true } }
|
|
18786
19867
|
),
|
|
18787
|
-
|
|
19868
|
+
tool18(
|
|
18788
19869
|
"generate_tech_readiness",
|
|
18789
19870
|
"Generate a BTP technology readiness report showing service coverage and gaps across assessments",
|
|
18790
19871
|
{},
|
|
@@ -18836,7 +19917,7 @@ function createAemReportTools(store) {
|
|
|
18836
19917
|
},
|
|
18837
19918
|
{ annotations: { readOnlyHint: true } }
|
|
18838
19919
|
),
|
|
18839
|
-
|
|
19920
|
+
tool18(
|
|
18840
19921
|
"generate_phase_status",
|
|
18841
19922
|
"Generate a phase progress report showing artifact counts and readiness per AEM phase",
|
|
18842
19923
|
{},
|
|
@@ -18898,11 +19979,11 @@ function createAemReportTools(store) {
|
|
|
18898
19979
|
import * as fs5 from "fs";
|
|
18899
19980
|
import * as path5 from "path";
|
|
18900
19981
|
import * as YAML2 from "yaml";
|
|
18901
|
-
import { tool as
|
|
19982
|
+
import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
|
|
18902
19983
|
var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
|
|
18903
19984
|
function createAemPhaseTools(store, marvinDir) {
|
|
18904
19985
|
return [
|
|
18905
|
-
|
|
19986
|
+
tool19(
|
|
18906
19987
|
"get_current_phase",
|
|
18907
19988
|
"Get the current AEM phase from project configuration",
|
|
18908
19989
|
{},
|
|
@@ -18923,7 +20004,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
18923
20004
|
},
|
|
18924
20005
|
{ annotations: { readOnlyHint: true } }
|
|
18925
20006
|
),
|
|
18926
|
-
|
|
20007
|
+
tool19(
|
|
18927
20008
|
"advance_phase",
|
|
18928
20009
|
"Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
|
|
18929
20010
|
{
|
|
@@ -19187,8 +20268,8 @@ function getPluginPromptFragment(plugin, personaId) {
|
|
|
19187
20268
|
}
|
|
19188
20269
|
|
|
19189
20270
|
// src/skills/registry.ts
|
|
19190
|
-
import * as
|
|
19191
|
-
import * as
|
|
20271
|
+
import * as fs7 from "fs";
|
|
20272
|
+
import * as path7 from "path";
|
|
19192
20273
|
import { fileURLToPath } from "url";
|
|
19193
20274
|
import * as YAML3 from "yaml";
|
|
19194
20275
|
import matter2 from "gray-matter";
|
|
@@ -19231,7 +20312,7 @@ Be thorough but concise. Focus on actionable insights.`,
|
|
|
19231
20312
|
};
|
|
19232
20313
|
|
|
19233
20314
|
// src/skills/builtin/jira/tools.ts
|
|
19234
|
-
import { tool as
|
|
20315
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
19235
20316
|
|
|
19236
20317
|
// src/skills/builtin/jira/client.ts
|
|
19237
20318
|
var JiraClient = class {
|
|
@@ -19241,8 +20322,8 @@ var JiraClient = class {
|
|
|
19241
20322
|
this.baseUrl = `https://${config2.host}/rest/api/2`;
|
|
19242
20323
|
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
19243
20324
|
}
|
|
19244
|
-
async request(
|
|
19245
|
-
const url2 = `${this.baseUrl}${
|
|
20325
|
+
async request(path21, method = "GET", body) {
|
|
20326
|
+
const url2 = `${this.baseUrl}${path21}`;
|
|
19246
20327
|
const headers = {
|
|
19247
20328
|
Authorization: this.authHeader,
|
|
19248
20329
|
"Content-Type": "application/json",
|
|
@@ -19256,7 +20337,7 @@ var JiraClient = class {
|
|
|
19256
20337
|
if (!response.ok) {
|
|
19257
20338
|
const text = await response.text().catch(() => "");
|
|
19258
20339
|
throw new Error(
|
|
19259
|
-
`Jira API error ${response.status} ${method} ${
|
|
20340
|
+
`Jira API error ${response.status} ${method} ${path21}: ${text}`
|
|
19260
20341
|
);
|
|
19261
20342
|
}
|
|
19262
20343
|
if (response.status === 204) return void 0;
|
|
@@ -19336,11 +20417,12 @@ function findByJiraKey(store, jiraKey) {
|
|
|
19336
20417
|
const docs = store.list({ type: JIRA_TYPE });
|
|
19337
20418
|
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
19338
20419
|
}
|
|
19339
|
-
function createJiraTools(store) {
|
|
20420
|
+
function createJiraTools(store, projectConfig) {
|
|
19340
20421
|
const jiraUserConfig = loadUserConfig().jira;
|
|
20422
|
+
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
19341
20423
|
return [
|
|
19342
20424
|
// --- Local read tools ---
|
|
19343
|
-
|
|
20425
|
+
tool20(
|
|
19344
20426
|
"list_jira_issues",
|
|
19345
20427
|
"List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
|
|
19346
20428
|
{
|
|
@@ -19368,7 +20450,7 @@ function createJiraTools(store) {
|
|
|
19368
20450
|
},
|
|
19369
20451
|
{ annotations: { readOnlyHint: true } }
|
|
19370
20452
|
),
|
|
19371
|
-
|
|
20453
|
+
tool20(
|
|
19372
20454
|
"get_jira_issue",
|
|
19373
20455
|
"Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
|
|
19374
20456
|
{
|
|
@@ -19401,7 +20483,7 @@ function createJiraTools(store) {
|
|
|
19401
20483
|
{ annotations: { readOnlyHint: true } }
|
|
19402
20484
|
),
|
|
19403
20485
|
// --- Jira → Local tools ---
|
|
19404
|
-
|
|
20486
|
+
tool20(
|
|
19405
20487
|
"pull_jira_issue",
|
|
19406
20488
|
"Fetch a single Jira issue by key and create/update a local JI-xxx document",
|
|
19407
20489
|
{
|
|
@@ -19448,7 +20530,7 @@ function createJiraTools(store) {
|
|
|
19448
20530
|
};
|
|
19449
20531
|
}
|
|
19450
20532
|
),
|
|
19451
|
-
|
|
20533
|
+
tool20(
|
|
19452
20534
|
"pull_jira_issues_jql",
|
|
19453
20535
|
"Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
|
|
19454
20536
|
{
|
|
@@ -19496,15 +20578,27 @@ function createJiraTools(store) {
|
|
|
19496
20578
|
}
|
|
19497
20579
|
),
|
|
19498
20580
|
// --- Local → Jira tools ---
|
|
19499
|
-
|
|
20581
|
+
tool20(
|
|
19500
20582
|
"push_artifact_to_jira",
|
|
19501
20583
|
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
19502
20584
|
{
|
|
19503
20585
|
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
19504
|
-
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
20586
|
+
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
|
|
19505
20587
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
19506
20588
|
},
|
|
19507
20589
|
async (args) => {
|
|
20590
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
20591
|
+
if (!resolvedProjectKey) {
|
|
20592
|
+
return {
|
|
20593
|
+
content: [
|
|
20594
|
+
{
|
|
20595
|
+
type: "text",
|
|
20596
|
+
text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
|
|
20597
|
+
}
|
|
20598
|
+
],
|
|
20599
|
+
isError: true
|
|
20600
|
+
};
|
|
20601
|
+
}
|
|
19508
20602
|
const jira = createJiraClient(jiraUserConfig);
|
|
19509
20603
|
if (!jira) return jiraNotConfiguredError();
|
|
19510
20604
|
const artifact = store.get(args.artifactId);
|
|
@@ -19524,7 +20618,7 @@ function createJiraTools(store) {
|
|
|
19524
20618
|
`Status: ${artifact.frontmatter.status}`
|
|
19525
20619
|
].join("\n");
|
|
19526
20620
|
const jiraResult = await jira.client.createIssue({
|
|
19527
|
-
project: { key:
|
|
20621
|
+
project: { key: resolvedProjectKey },
|
|
19528
20622
|
summary: artifact.frontmatter.title,
|
|
19529
20623
|
description,
|
|
19530
20624
|
issuetype: { name: args.issueType ?? "Task" }
|
|
@@ -19557,7 +20651,7 @@ function createJiraTools(store) {
|
|
|
19557
20651
|
}
|
|
19558
20652
|
),
|
|
19559
20653
|
// --- Bidirectional sync ---
|
|
19560
|
-
|
|
20654
|
+
tool20(
|
|
19561
20655
|
"sync_jira_issue",
|
|
19562
20656
|
"Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
|
|
19563
20657
|
{
|
|
@@ -19598,7 +20692,7 @@ function createJiraTools(store) {
|
|
|
19598
20692
|
}
|
|
19599
20693
|
),
|
|
19600
20694
|
// --- Local link tool ---
|
|
19601
|
-
|
|
20695
|
+
tool20(
|
|
19602
20696
|
"link_artifact_to_jira",
|
|
19603
20697
|
"Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
|
|
19604
20698
|
{
|
|
@@ -19665,14 +20759,14 @@ var jiraSkill = {
|
|
|
19665
20759
|
documentTypeRegistrations: [
|
|
19666
20760
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
19667
20761
|
],
|
|
19668
|
-
tools: (store) => createJiraTools(store),
|
|
20762
|
+
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
19669
20763
|
promptFragments: {
|
|
19670
20764
|
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
19671
20765
|
|
|
19672
20766
|
**Available tools:**
|
|
19673
20767
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
19674
20768
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
19675
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
|
|
20769
|
+
- \`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\`.
|
|
19676
20770
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
19677
20771
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
19678
20772
|
|
|
@@ -19686,13 +20780,13 @@ var jiraSkill = {
|
|
|
19686
20780
|
**Available tools:**
|
|
19687
20781
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
19688
20782
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
19689
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
|
|
20783
|
+
- \`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\`.
|
|
19690
20784
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
19691
20785
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
19692
20786
|
|
|
19693
20787
|
**As Tech Lead, use Jira integration to:**
|
|
19694
20788
|
- Pull technical issues and bugs for sprint planning and estimation
|
|
19695
|
-
- Push epics and technical decisions to Jira for cross-team visibility
|
|
20789
|
+
- Push epics, tasks, and technical decisions to Jira for cross-team visibility
|
|
19696
20790
|
- Bidirectional sync to keep local governance and Jira in alignment
|
|
19697
20791
|
- Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
|
|
19698
20792
|
"delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
@@ -19700,22 +20794,426 @@ var jiraSkill = {
|
|
|
19700
20794
|
**Available tools:**
|
|
19701
20795
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
19702
20796
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
19703
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
20797
|
+
- \`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\`.
|
|
19704
20798
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
19705
20799
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
19706
20800
|
|
|
19707
20801
|
**As Delivery Manager, use Jira integration to:**
|
|
19708
20802
|
- Pull sprint issues for tracking progress and blockers
|
|
19709
|
-
- Push actions and
|
|
20803
|
+
- Push actions, decisions, and tasks to Jira for stakeholder visibility
|
|
19710
20804
|
- Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
|
|
19711
20805
|
- Sync status between Marvin governance items and Jira issues`
|
|
19712
20806
|
}
|
|
19713
20807
|
};
|
|
19714
20808
|
|
|
20809
|
+
// src/skills/builtin/prd-generator/tools.ts
|
|
20810
|
+
import * as fs6 from "fs";
|
|
20811
|
+
import * as path6 from "path";
|
|
20812
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
20813
|
+
var PRIORITY_ORDER2 = {
|
|
20814
|
+
critical: 0,
|
|
20815
|
+
high: 1,
|
|
20816
|
+
medium: 2,
|
|
20817
|
+
low: 3
|
|
20818
|
+
};
|
|
20819
|
+
function priorityRank2(p) {
|
|
20820
|
+
return PRIORITY_ORDER2[p ?? ""] ?? 99;
|
|
20821
|
+
}
|
|
20822
|
+
function gatherContext(store, focusFeature, includeDecisions = true, includeQuestions = true) {
|
|
20823
|
+
const allFeatures = store.list({ type: "feature" });
|
|
20824
|
+
const allEpics = store.list({ type: "epic" });
|
|
20825
|
+
const allTasks = store.list({ type: "task" });
|
|
20826
|
+
const allDecisions = includeDecisions ? store.list({ type: "decision" }) : [];
|
|
20827
|
+
const allQuestions = includeQuestions ? store.list({ type: "question" }) : [];
|
|
20828
|
+
const allActions = store.list({ type: "action" });
|
|
20829
|
+
let features = allFeatures;
|
|
20830
|
+
let epics = allEpics;
|
|
20831
|
+
let tasks = allTasks;
|
|
20832
|
+
if (focusFeature) {
|
|
20833
|
+
features = features.filter((f) => f.frontmatter.id === focusFeature);
|
|
20834
|
+
const featureIds = new Set(features.map((f) => f.frontmatter.id));
|
|
20835
|
+
epics = epics.filter(
|
|
20836
|
+
(e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).some((id) => featureIds.has(id))
|
|
20837
|
+
);
|
|
20838
|
+
const epicIds2 = new Set(epics.map((e) => e.frontmatter.id));
|
|
20839
|
+
tasks = tasks.filter(
|
|
20840
|
+
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).some((id) => epicIds2.has(id))
|
|
20841
|
+
);
|
|
20842
|
+
}
|
|
20843
|
+
const featuresByStatus = {};
|
|
20844
|
+
for (const f of features) {
|
|
20845
|
+
featuresByStatus[f.frontmatter.status] = (featuresByStatus[f.frontmatter.status] ?? 0) + 1;
|
|
20846
|
+
}
|
|
20847
|
+
const epicsByStatus = {};
|
|
20848
|
+
for (const e of epics) {
|
|
20849
|
+
epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
|
|
20850
|
+
}
|
|
20851
|
+
const epicIds = new Set(epics.map((e) => e.frontmatter.id));
|
|
20852
|
+
return {
|
|
20853
|
+
features: features.sort((a, b) => priorityRank2(a.frontmatter.priority) - priorityRank2(b.frontmatter.priority)).map((f) => ({
|
|
20854
|
+
id: f.frontmatter.id,
|
|
20855
|
+
title: f.frontmatter.title,
|
|
20856
|
+
status: f.frontmatter.status,
|
|
20857
|
+
priority: f.frontmatter.priority ?? "medium",
|
|
20858
|
+
content: f.content,
|
|
20859
|
+
linkedEpicCount: epics.filter(
|
|
20860
|
+
(e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
|
|
20861
|
+
).length
|
|
20862
|
+
})),
|
|
20863
|
+
epics: epics.map((e) => ({
|
|
20864
|
+
id: e.frontmatter.id,
|
|
20865
|
+
title: e.frontmatter.title,
|
|
20866
|
+
status: e.frontmatter.status,
|
|
20867
|
+
linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
|
|
20868
|
+
targetDate: e.frontmatter.targetDate ?? null,
|
|
20869
|
+
estimatedEffort: e.frontmatter.estimatedEffort ?? null,
|
|
20870
|
+
content: e.content,
|
|
20871
|
+
linkedTaskCount: tasks.filter(
|
|
20872
|
+
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
|
|
20873
|
+
).length
|
|
20874
|
+
})),
|
|
20875
|
+
tasks: tasks.map((t) => ({
|
|
20876
|
+
id: t.frontmatter.id,
|
|
20877
|
+
title: t.frontmatter.title,
|
|
20878
|
+
status: t.frontmatter.status,
|
|
20879
|
+
linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
|
|
20880
|
+
acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
|
|
20881
|
+
technicalNotes: t.frontmatter.technicalNotes ?? null,
|
|
20882
|
+
complexity: t.frontmatter.complexity ?? null,
|
|
20883
|
+
estimatedPoints: t.frontmatter.estimatedPoints ?? null,
|
|
20884
|
+
priority: t.frontmatter.priority ?? null
|
|
20885
|
+
})),
|
|
20886
|
+
decisions: allDecisions.map((d) => ({
|
|
20887
|
+
id: d.frontmatter.id,
|
|
20888
|
+
title: d.frontmatter.title,
|
|
20889
|
+
status: d.frontmatter.status,
|
|
20890
|
+
content: d.content
|
|
20891
|
+
})),
|
|
20892
|
+
questions: allQuestions.map((q) => ({
|
|
20893
|
+
id: q.frontmatter.id,
|
|
20894
|
+
title: q.frontmatter.title,
|
|
20895
|
+
status: q.frontmatter.status,
|
|
20896
|
+
content: q.content
|
|
20897
|
+
})),
|
|
20898
|
+
actions: allActions.filter((a) => {
|
|
20899
|
+
if (!focusFeature) return true;
|
|
20900
|
+
const tags = a.frontmatter.tags ?? [];
|
|
20901
|
+
return tags.some((t) => t.startsWith("epic:") && epicIds.has(t.replace("epic:", "")));
|
|
20902
|
+
}).map((a) => ({
|
|
20903
|
+
id: a.frontmatter.id,
|
|
20904
|
+
title: a.frontmatter.title,
|
|
20905
|
+
status: a.frontmatter.status,
|
|
20906
|
+
owner: a.frontmatter.owner ?? null,
|
|
20907
|
+
priority: a.frontmatter.priority ?? null,
|
|
20908
|
+
dueDate: a.frontmatter.dueDate ?? null
|
|
20909
|
+
})),
|
|
20910
|
+
summary: {
|
|
20911
|
+
totalFeatures: features.length,
|
|
20912
|
+
totalEpics: epics.length,
|
|
20913
|
+
totalTasks: tasks.length,
|
|
20914
|
+
featuresByStatus,
|
|
20915
|
+
epicsByStatus
|
|
20916
|
+
}
|
|
20917
|
+
};
|
|
20918
|
+
}
|
|
20919
|
+
function generateTaskMasterPrd(title, ctx, projectOverview) {
|
|
20920
|
+
const lines = [];
|
|
20921
|
+
lines.push(`# ${title}`);
|
|
20922
|
+
lines.push("");
|
|
20923
|
+
lines.push("## Project Overview");
|
|
20924
|
+
if (projectOverview) {
|
|
20925
|
+
lines.push(projectOverview);
|
|
20926
|
+
} else if (ctx.features.length > 0) {
|
|
20927
|
+
lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
|
|
20928
|
+
}
|
|
20929
|
+
lines.push("");
|
|
20930
|
+
lines.push("## Goals");
|
|
20931
|
+
for (const f of ctx.features) {
|
|
20932
|
+
lines.push(`- **${f.title}** (${f.id}, Priority: ${f.priority}) \u2014 ${f.status}`);
|
|
20933
|
+
}
|
|
20934
|
+
lines.push("");
|
|
20935
|
+
lines.push("## Features and Requirements");
|
|
20936
|
+
lines.push("");
|
|
20937
|
+
for (const feature of ctx.features) {
|
|
20938
|
+
lines.push(`### ${feature.title} (${feature.id}) \u2014 Priority: ${feature.priority}`);
|
|
20939
|
+
lines.push("");
|
|
20940
|
+
if (feature.content) {
|
|
20941
|
+
lines.push(feature.content);
|
|
20942
|
+
lines.push("");
|
|
20943
|
+
}
|
|
20944
|
+
const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
|
|
20945
|
+
if (featureEpics.length > 0) {
|
|
20946
|
+
lines.push("#### User Stories / Epics");
|
|
20947
|
+
lines.push("");
|
|
20948
|
+
for (const epic of featureEpics) {
|
|
20949
|
+
const effort = epic.estimatedEffort ? `, Effort: ${epic.estimatedEffort}` : "";
|
|
20950
|
+
lines.push(`- **${epic.id}: ${epic.title}** \u2014 Status: ${epic.status}${effort}`);
|
|
20951
|
+
if (epic.content) {
|
|
20952
|
+
lines.push(` ${epic.content.split("\n")[0]}`);
|
|
20953
|
+
}
|
|
20954
|
+
const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
|
|
20955
|
+
if (epicTasks.length > 0) {
|
|
20956
|
+
lines.push("");
|
|
20957
|
+
lines.push("#### Implementation Tasks");
|
|
20958
|
+
lines.push("");
|
|
20959
|
+
for (const task of epicTasks) {
|
|
20960
|
+
const complexity = task.complexity ? `, Complexity: ${task.complexity}` : "";
|
|
20961
|
+
const points = task.estimatedPoints != null ? `, Points: ${task.estimatedPoints}` : "";
|
|
20962
|
+
lines.push(`- **${task.id}: ${task.title}**${complexity}${points}`);
|
|
20963
|
+
if (task.acceptanceCriteria) {
|
|
20964
|
+
lines.push(` Acceptance Criteria: ${task.acceptanceCriteria}`);
|
|
20965
|
+
}
|
|
20966
|
+
}
|
|
20967
|
+
}
|
|
20968
|
+
}
|
|
20969
|
+
lines.push("");
|
|
20970
|
+
}
|
|
20971
|
+
}
|
|
20972
|
+
const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
|
|
20973
|
+
const openQuestions = ctx.questions.filter((q) => q.status === "open");
|
|
20974
|
+
const technicalNotes = ctx.tasks.filter((t) => t.technicalNotes).map((t) => `- **${t.id}**: ${t.technicalNotes}`);
|
|
20975
|
+
if (approvedDecisions.length > 0 || openQuestions.length > 0 || technicalNotes.length > 0) {
|
|
20976
|
+
lines.push("## Technical Considerations");
|
|
20977
|
+
lines.push("");
|
|
20978
|
+
if (approvedDecisions.length > 0) {
|
|
20979
|
+
lines.push("### Key Decisions");
|
|
20980
|
+
for (const d of approvedDecisions) {
|
|
20981
|
+
lines.push(`- **${d.id}: ${d.title}** \u2014 ${d.content.split("\n")[0]}`);
|
|
20982
|
+
}
|
|
20983
|
+
lines.push("");
|
|
20984
|
+
}
|
|
20985
|
+
if (technicalNotes.length > 0) {
|
|
20986
|
+
lines.push("### Technical Notes");
|
|
20987
|
+
for (const note of technicalNotes) {
|
|
20988
|
+
lines.push(note);
|
|
20989
|
+
}
|
|
20990
|
+
lines.push("");
|
|
20991
|
+
}
|
|
20992
|
+
if (openQuestions.length > 0) {
|
|
20993
|
+
lines.push("### Open Questions");
|
|
20994
|
+
for (const q of openQuestions) {
|
|
20995
|
+
lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
|
|
20996
|
+
}
|
|
20997
|
+
lines.push("");
|
|
20998
|
+
}
|
|
20999
|
+
}
|
|
21000
|
+
lines.push("## Implementation Priorities");
|
|
21001
|
+
lines.push("");
|
|
21002
|
+
let priorityIdx = 1;
|
|
21003
|
+
for (const feature of ctx.features) {
|
|
21004
|
+
const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
|
|
21005
|
+
const statusOrder = { "in-progress": 0, planned: 1, done: 2 };
|
|
21006
|
+
return (statusOrder[a.status] ?? 99) - (statusOrder[b.status] ?? 99);
|
|
21007
|
+
});
|
|
21008
|
+
if (featureEpics.length === 0) continue;
|
|
21009
|
+
lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
|
|
21010
|
+
for (const epic of featureEpics) {
|
|
21011
|
+
const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
|
|
21012
|
+
lines.push(` - ${epic.id}: ${epic.title} (${epic.status}) \u2014 ${epicTasks.length} task(s)`);
|
|
21013
|
+
}
|
|
21014
|
+
priorityIdx++;
|
|
21015
|
+
}
|
|
21016
|
+
lines.push("");
|
|
21017
|
+
return lines.join("\n");
|
|
21018
|
+
}
|
|
21019
|
+
function generateClaudeCodePrd(title, ctx, projectOverview) {
|
|
21020
|
+
const lines = [];
|
|
21021
|
+
lines.push(`# ${title}`);
|
|
21022
|
+
lines.push("");
|
|
21023
|
+
lines.push("## Overview");
|
|
21024
|
+
if (projectOverview) {
|
|
21025
|
+
lines.push(projectOverview);
|
|
21026
|
+
} else if (ctx.features.length > 0) {
|
|
21027
|
+
lines.push(`This project encompasses ${ctx.features.length} feature(s) spanning ${ctx.epics.length} epic(s) and ${ctx.tasks.length} implementation task(s).`);
|
|
21028
|
+
}
|
|
21029
|
+
lines.push("");
|
|
21030
|
+
const approvedDecisions = ctx.decisions.filter((d) => d.status === "approved" || d.status === "accepted");
|
|
21031
|
+
if (approvedDecisions.length > 0) {
|
|
21032
|
+
lines.push("## Architecture & Technical Decisions");
|
|
21033
|
+
lines.push("");
|
|
21034
|
+
for (const d of approvedDecisions) {
|
|
21035
|
+
lines.push(`### ${d.id}: ${d.title}`);
|
|
21036
|
+
lines.push(d.content);
|
|
21037
|
+
lines.push("");
|
|
21038
|
+
}
|
|
21039
|
+
}
|
|
21040
|
+
lines.push("## Implementation Plan");
|
|
21041
|
+
lines.push("");
|
|
21042
|
+
const priorityGroups = {};
|
|
21043
|
+
for (const f of ctx.features) {
|
|
21044
|
+
const group = f.priority === "critical" || f.priority === "high" ? "Phase 1: High Priority" : "Phase 2: Medium & Low Priority";
|
|
21045
|
+
if (!priorityGroups[group]) priorityGroups[group] = [];
|
|
21046
|
+
priorityGroups[group].push(f);
|
|
21047
|
+
}
|
|
21048
|
+
for (const [phase, features] of Object.entries(priorityGroups)) {
|
|
21049
|
+
lines.push(`### ${phase}`);
|
|
21050
|
+
lines.push("");
|
|
21051
|
+
for (const feature of features) {
|
|
21052
|
+
const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id));
|
|
21053
|
+
for (const epic of featureEpics) {
|
|
21054
|
+
lines.push(`- [ ] ${epic.id}: ${epic.title}`);
|
|
21055
|
+
const epicTasks = ctx.tasks.filter((t) => t.linkedEpic.includes(epic.id));
|
|
21056
|
+
for (const task of epicTasks) {
|
|
21057
|
+
const complexity = task.complexity ? `complexity: ${task.complexity}` : "";
|
|
21058
|
+
const points = task.estimatedPoints != null ? `points: ${task.estimatedPoints}` : "";
|
|
21059
|
+
const meta3 = [complexity, points].filter(Boolean).join(", ");
|
|
21060
|
+
lines.push(` - [ ] ${task.id}: ${task.title}${meta3 ? ` (${meta3})` : ""}`);
|
|
21061
|
+
if (task.acceptanceCriteria) {
|
|
21062
|
+
lines.push(` - Acceptance: ${task.acceptanceCriteria}`);
|
|
21063
|
+
}
|
|
21064
|
+
if (task.technicalNotes) {
|
|
21065
|
+
lines.push(` - Notes: ${task.technicalNotes}`);
|
|
21066
|
+
}
|
|
21067
|
+
}
|
|
21068
|
+
}
|
|
21069
|
+
}
|
|
21070
|
+
lines.push("");
|
|
21071
|
+
}
|
|
21072
|
+
const openQuestions = ctx.questions.filter((q) => q.status === "open");
|
|
21073
|
+
if (openQuestions.length > 0) {
|
|
21074
|
+
lines.push("## Open Questions");
|
|
21075
|
+
lines.push("");
|
|
21076
|
+
for (const q of openQuestions) {
|
|
21077
|
+
lines.push(`- **${q.id}: ${q.title}** \u2014 ${q.content.split("\n")[0]}`);
|
|
21078
|
+
}
|
|
21079
|
+
lines.push("");
|
|
21080
|
+
}
|
|
21081
|
+
return lines.join("\n");
|
|
21082
|
+
}
|
|
21083
|
+
function createPrdTools(store) {
|
|
21084
|
+
return [
|
|
21085
|
+
tool21(
|
|
21086
|
+
"gather_prd_context",
|
|
21087
|
+
"Aggregate all governance artifacts (features, epics, tasks, decisions, questions, actions) into structured JSON for PRD generation",
|
|
21088
|
+
{
|
|
21089
|
+
focusFeature: external_exports.string().optional().describe("Filter context to a specific feature ID (e.g. 'F-001')"),
|
|
21090
|
+
includeDecisions: external_exports.boolean().optional().describe("Include decisions in context (default: true)"),
|
|
21091
|
+
includeQuestions: external_exports.boolean().optional().describe("Include questions in context (default: true)")
|
|
21092
|
+
},
|
|
21093
|
+
async (args) => {
|
|
21094
|
+
const ctx = gatherContext(store, args.focusFeature, args.includeDecisions ?? true, args.includeQuestions ?? true);
|
|
21095
|
+
return {
|
|
21096
|
+
content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }]
|
|
21097
|
+
};
|
|
21098
|
+
},
|
|
21099
|
+
{ annotations: { readOnlyHint: true } }
|
|
21100
|
+
),
|
|
21101
|
+
tool21(
|
|
21102
|
+
"generate_prd",
|
|
21103
|
+
"Generate a PRD document from governance artifacts and save it as a PRD-xxx document",
|
|
21104
|
+
{
|
|
21105
|
+
title: external_exports.string().describe("PRD title"),
|
|
21106
|
+
format: external_exports.enum(["taskmaster", "claude-code"]).describe("Output format: 'taskmaster' for Claude TaskMaster parse_prd, 'claude-code' for Claude Code consumption"),
|
|
21107
|
+
projectOverview: external_exports.string().optional().describe("Project overview text (synthesized from features if not provided)"),
|
|
21108
|
+
focusFeature: external_exports.string().optional().describe("Focus on a specific feature ID (e.g. 'F-001')"),
|
|
21109
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Tags for the PRD document")
|
|
21110
|
+
},
|
|
21111
|
+
async (args) => {
|
|
21112
|
+
const ctx = gatherContext(store, args.focusFeature);
|
|
21113
|
+
const prdContent = args.format === "taskmaster" ? generateTaskMasterPrd(args.title, ctx, args.projectOverview) : generateClaudeCodePrd(args.title, ctx, args.projectOverview);
|
|
21114
|
+
const frontmatter = {
|
|
21115
|
+
title: args.title,
|
|
21116
|
+
status: "draft",
|
|
21117
|
+
format: args.format
|
|
21118
|
+
};
|
|
21119
|
+
if (args.focusFeature) frontmatter.focusFeature = args.focusFeature;
|
|
21120
|
+
if (args.tags) frontmatter.tags = args.tags;
|
|
21121
|
+
const doc = store.create("prd", frontmatter, prdContent);
|
|
21122
|
+
return {
|
|
21123
|
+
content: [
|
|
21124
|
+
{
|
|
21125
|
+
type: "text",
|
|
21126
|
+
text: `Generated PRD ${doc.frontmatter.id}: "${args.title}" (format: ${args.format}, ${ctx.summary.totalFeatures} features, ${ctx.summary.totalEpics} epics, ${ctx.summary.totalTasks} tasks)`
|
|
21127
|
+
}
|
|
21128
|
+
]
|
|
21129
|
+
};
|
|
21130
|
+
}
|
|
21131
|
+
),
|
|
21132
|
+
tool21(
|
|
21133
|
+
"export_prd",
|
|
21134
|
+
"Export a PRD document to a file path for external consumption (e.g. by Claude TaskMaster or Claude Code)",
|
|
21135
|
+
{
|
|
21136
|
+
prdId: external_exports.string().describe("PRD document ID (e.g. 'PRD-001')"),
|
|
21137
|
+
outputPath: external_exports.string().describe("File path to write the PRD content to")
|
|
21138
|
+
},
|
|
21139
|
+
async (args) => {
|
|
21140
|
+
const doc = store.get(args.prdId);
|
|
21141
|
+
if (!doc) {
|
|
21142
|
+
return {
|
|
21143
|
+
content: [{ type: "text", text: `PRD ${args.prdId} not found` }],
|
|
21144
|
+
isError: true
|
|
21145
|
+
};
|
|
21146
|
+
}
|
|
21147
|
+
const outputDir = path6.dirname(args.outputPath);
|
|
21148
|
+
fs6.mkdirSync(outputDir, { recursive: true });
|
|
21149
|
+
fs6.writeFileSync(args.outputPath, doc.content, "utf-8");
|
|
21150
|
+
return {
|
|
21151
|
+
content: [
|
|
21152
|
+
{
|
|
21153
|
+
type: "text",
|
|
21154
|
+
text: `Exported PRD ${args.prdId} to ${args.outputPath}`
|
|
21155
|
+
}
|
|
21156
|
+
]
|
|
21157
|
+
};
|
|
21158
|
+
}
|
|
21159
|
+
)
|
|
21160
|
+
];
|
|
21161
|
+
}
|
|
21162
|
+
|
|
21163
|
+
// src/skills/builtin/prd-generator/index.ts
|
|
21164
|
+
var prdGeneratorSkill = {
|
|
21165
|
+
id: "prd-generator",
|
|
21166
|
+
name: "PRD Generator",
|
|
21167
|
+
description: "Generate PRDs from governance artifacts for TaskMaster or Claude Code",
|
|
21168
|
+
version: "1.0.0",
|
|
21169
|
+
format: "builtin-ts",
|
|
21170
|
+
documentTypeRegistrations: [
|
|
21171
|
+
{ type: "prd", dirName: "prds", idPrefix: "PRD" }
|
|
21172
|
+
],
|
|
21173
|
+
tools: (store) => createPrdTools(store),
|
|
21174
|
+
promptFragments: {
|
|
21175
|
+
"tech-lead": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
|
|
21176
|
+
|
|
21177
|
+
**Available tools:**
|
|
21178
|
+
- \`gather_prd_context\` \u2014 aggregate features, epics, tasks, decisions, questions, and actions into structured JSON for analysis
|
|
21179
|
+
- \`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)
|
|
21180
|
+
- \`export_prd\` \u2014 export a PRD document to a file path for external use
|
|
21181
|
+
|
|
21182
|
+
**As Tech Lead, use PRD generation to:**
|
|
21183
|
+
- Create comprehensive PRDs that capture the full governance context
|
|
21184
|
+
- Export TaskMaster-format PRDs for automated task breakdown via \`parse_prd\`
|
|
21185
|
+
- Export Claude Code-format PRDs as implementation plans with checklists
|
|
21186
|
+
- Focus PRDs on specific features using the focusFeature parameter`,
|
|
21187
|
+
"delivery-manager": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
|
|
21188
|
+
|
|
21189
|
+
**Available tools:**
|
|
21190
|
+
- \`gather_prd_context\` \u2014 aggregate all governance artifacts into structured JSON for review
|
|
21191
|
+
- \`generate_prd\` \u2014 generate a formatted PRD document (taskmaster or claude-code format)
|
|
21192
|
+
- \`export_prd\` \u2014 export a PRD to a file path
|
|
21193
|
+
|
|
21194
|
+
**As Delivery Manager, use PRD generation to:**
|
|
21195
|
+
- Generate PRDs for stakeholder communication and project documentation
|
|
21196
|
+
- Review aggregated project context before sprint planning
|
|
21197
|
+
- Export PRDs to share with external teams or tools`,
|
|
21198
|
+
"product-owner": `You have the **PRD Generator** skill. You can generate Product Requirements Documents from governance artifacts.
|
|
21199
|
+
|
|
21200
|
+
**Available tools:**
|
|
21201
|
+
- \`gather_prd_context\` \u2014 aggregate features, epics, tasks, and decisions into structured JSON
|
|
21202
|
+
- \`generate_prd\` \u2014 generate a formatted PRD document
|
|
21203
|
+
- \`export_prd\` \u2014 export a PRD to a file path
|
|
21204
|
+
|
|
21205
|
+
**As Product Owner, use PRD generation to:**
|
|
21206
|
+
- Generate PRDs that capture feature requirements and priorities
|
|
21207
|
+
- Review the complete governance context for product planning
|
|
21208
|
+
- Export PRDs for stakeholder review and sign-off`
|
|
21209
|
+
}
|
|
21210
|
+
};
|
|
21211
|
+
|
|
19715
21212
|
// src/skills/registry.ts
|
|
19716
21213
|
var BUILTIN_SKILLS = {
|
|
19717
21214
|
"governance-review": governanceReviewSkill,
|
|
19718
|
-
"jira": jiraSkill
|
|
21215
|
+
"jira": jiraSkill,
|
|
21216
|
+
"prd-generator": prdGeneratorSkill
|
|
19719
21217
|
};
|
|
19720
21218
|
var GOVERNANCE_TOOL_NAMES = [
|
|
19721
21219
|
"mcp__marvin-governance__list_decisions",
|
|
@@ -19736,13 +21234,13 @@ var GOVERNANCE_TOOL_NAMES = [
|
|
|
19736
21234
|
];
|
|
19737
21235
|
function getBuiltinSkillsDir() {
|
|
19738
21236
|
const thisFile = fileURLToPath(import.meta.url);
|
|
19739
|
-
return
|
|
21237
|
+
return path7.join(path7.dirname(thisFile), "builtin");
|
|
19740
21238
|
}
|
|
19741
21239
|
function loadSkillFromDirectory(dirPath) {
|
|
19742
|
-
const skillMdPath =
|
|
19743
|
-
if (!
|
|
21240
|
+
const skillMdPath = path7.join(dirPath, "SKILL.md");
|
|
21241
|
+
if (!fs7.existsSync(skillMdPath)) return void 0;
|
|
19744
21242
|
try {
|
|
19745
|
-
const raw =
|
|
21243
|
+
const raw = fs7.readFileSync(skillMdPath, "utf-8");
|
|
19746
21244
|
const { data, content } = matter2(raw);
|
|
19747
21245
|
if (!data.name || !data.description) return void 0;
|
|
19748
21246
|
const metadata = data.metadata ?? {};
|
|
@@ -19753,13 +21251,13 @@ function loadSkillFromDirectory(dirPath) {
|
|
|
19753
21251
|
if (wildcardPrompt) {
|
|
19754
21252
|
promptFragments["*"] = wildcardPrompt;
|
|
19755
21253
|
}
|
|
19756
|
-
const personasDir =
|
|
19757
|
-
if (
|
|
21254
|
+
const personasDir = path7.join(dirPath, "personas");
|
|
21255
|
+
if (fs7.existsSync(personasDir)) {
|
|
19758
21256
|
try {
|
|
19759
|
-
for (const file2 of
|
|
21257
|
+
for (const file2 of fs7.readdirSync(personasDir)) {
|
|
19760
21258
|
if (!file2.endsWith(".md")) continue;
|
|
19761
21259
|
const personaId = file2.replace(/\.md$/, "");
|
|
19762
|
-
const personaPrompt =
|
|
21260
|
+
const personaPrompt = fs7.readFileSync(path7.join(personasDir, file2), "utf-8").trim();
|
|
19763
21261
|
if (personaPrompt) {
|
|
19764
21262
|
promptFragments[personaId] = personaPrompt;
|
|
19765
21263
|
}
|
|
@@ -19768,10 +21266,10 @@ function loadSkillFromDirectory(dirPath) {
|
|
|
19768
21266
|
}
|
|
19769
21267
|
}
|
|
19770
21268
|
let actions;
|
|
19771
|
-
const actionsPath =
|
|
19772
|
-
if (
|
|
21269
|
+
const actionsPath = path7.join(dirPath, "actions.yaml");
|
|
21270
|
+
if (fs7.existsSync(actionsPath)) {
|
|
19773
21271
|
try {
|
|
19774
|
-
const actionsRaw =
|
|
21272
|
+
const actionsRaw = fs7.readFileSync(actionsPath, "utf-8");
|
|
19775
21273
|
actions = YAML3.parse(actionsRaw);
|
|
19776
21274
|
} catch {
|
|
19777
21275
|
}
|
|
@@ -19798,10 +21296,10 @@ function loadAllSkills(marvinDir) {
|
|
|
19798
21296
|
}
|
|
19799
21297
|
try {
|
|
19800
21298
|
const builtinDir = getBuiltinSkillsDir();
|
|
19801
|
-
if (
|
|
19802
|
-
for (const entry of
|
|
19803
|
-
const entryPath =
|
|
19804
|
-
if (!
|
|
21299
|
+
if (fs7.existsSync(builtinDir)) {
|
|
21300
|
+
for (const entry of fs7.readdirSync(builtinDir)) {
|
|
21301
|
+
const entryPath = path7.join(builtinDir, entry);
|
|
21302
|
+
if (!fs7.statSync(entryPath).isDirectory()) continue;
|
|
19805
21303
|
if (skills.has(entry)) continue;
|
|
19806
21304
|
const skill = loadSkillFromDirectory(entryPath);
|
|
19807
21305
|
if (skill) skills.set(skill.id, skill);
|
|
@@ -19810,18 +21308,18 @@ function loadAllSkills(marvinDir) {
|
|
|
19810
21308
|
} catch {
|
|
19811
21309
|
}
|
|
19812
21310
|
if (marvinDir) {
|
|
19813
|
-
const skillsDir =
|
|
19814
|
-
if (
|
|
21311
|
+
const skillsDir = path7.join(marvinDir, "skills");
|
|
21312
|
+
if (fs7.existsSync(skillsDir)) {
|
|
19815
21313
|
let entries;
|
|
19816
21314
|
try {
|
|
19817
|
-
entries =
|
|
21315
|
+
entries = fs7.readdirSync(skillsDir);
|
|
19818
21316
|
} catch {
|
|
19819
21317
|
entries = [];
|
|
19820
21318
|
}
|
|
19821
21319
|
for (const entry of entries) {
|
|
19822
|
-
const entryPath =
|
|
21320
|
+
const entryPath = path7.join(skillsDir, entry);
|
|
19823
21321
|
try {
|
|
19824
|
-
if (
|
|
21322
|
+
if (fs7.statSync(entryPath).isDirectory()) {
|
|
19825
21323
|
const skill = loadSkillFromDirectory(entryPath);
|
|
19826
21324
|
if (skill) skills.set(skill.id, skill);
|
|
19827
21325
|
continue;
|
|
@@ -19831,7 +21329,7 @@ function loadAllSkills(marvinDir) {
|
|
|
19831
21329
|
}
|
|
19832
21330
|
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
|
|
19833
21331
|
try {
|
|
19834
|
-
const raw =
|
|
21332
|
+
const raw = fs7.readFileSync(entryPath, "utf-8");
|
|
19835
21333
|
const parsed = YAML3.parse(raw);
|
|
19836
21334
|
if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
|
|
19837
21335
|
const skill = {
|
|
@@ -19874,12 +21372,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
|
|
|
19874
21372
|
}
|
|
19875
21373
|
return registrations;
|
|
19876
21374
|
}
|
|
19877
|
-
function getSkillTools(skillIds, allSkills, store) {
|
|
21375
|
+
function getSkillTools(skillIds, allSkills, store, projectConfig) {
|
|
19878
21376
|
const tools = [];
|
|
19879
21377
|
for (const id of skillIds) {
|
|
19880
21378
|
const skill = allSkills.get(id);
|
|
19881
21379
|
if (skill?.tools) {
|
|
19882
|
-
tools.push(...skill.tools(store));
|
|
21380
|
+
tools.push(...skill.tools(store, projectConfig));
|
|
19883
21381
|
}
|
|
19884
21382
|
}
|
|
19885
21383
|
return tools;
|
|
@@ -19936,12 +21434,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
|
|
|
19936
21434
|
return agents;
|
|
19937
21435
|
}
|
|
19938
21436
|
function migrateYamlToSkillMd(yamlPath, outputDir) {
|
|
19939
|
-
const raw =
|
|
21437
|
+
const raw = fs7.readFileSync(yamlPath, "utf-8");
|
|
19940
21438
|
const parsed = YAML3.parse(raw);
|
|
19941
21439
|
if (!parsed?.id || !parsed?.name) {
|
|
19942
21440
|
throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
|
|
19943
21441
|
}
|
|
19944
|
-
|
|
21442
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
19945
21443
|
const frontmatter = {
|
|
19946
21444
|
name: parsed.id,
|
|
19947
21445
|
description: parsed.description ?? ""
|
|
@@ -19955,15 +21453,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
|
|
|
19955
21453
|
const skillMd = matter2.stringify(wildcardPrompt ? `
|
|
19956
21454
|
${wildcardPrompt}
|
|
19957
21455
|
` : "\n", frontmatter);
|
|
19958
|
-
|
|
21456
|
+
fs7.writeFileSync(path7.join(outputDir, "SKILL.md"), skillMd, "utf-8");
|
|
19959
21457
|
if (promptFragments) {
|
|
19960
21458
|
const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
|
|
19961
21459
|
if (personaKeys.length > 0) {
|
|
19962
|
-
const personasDir =
|
|
19963
|
-
|
|
21460
|
+
const personasDir = path7.join(outputDir, "personas");
|
|
21461
|
+
fs7.mkdirSync(personasDir, { recursive: true });
|
|
19964
21462
|
for (const personaId of personaKeys) {
|
|
19965
|
-
|
|
19966
|
-
|
|
21463
|
+
fs7.writeFileSync(
|
|
21464
|
+
path7.join(personasDir, `${personaId}.md`),
|
|
19967
21465
|
`${promptFragments[personaId]}
|
|
19968
21466
|
`,
|
|
19969
21467
|
"utf-8"
|
|
@@ -19973,8 +21471,8 @@ ${wildcardPrompt}
|
|
|
19973
21471
|
}
|
|
19974
21472
|
const actions = parsed.actions;
|
|
19975
21473
|
if (actions && actions.length > 0) {
|
|
19976
|
-
|
|
19977
|
-
|
|
21474
|
+
fs7.writeFileSync(
|
|
21475
|
+
path7.join(outputDir, "actions.yaml"),
|
|
19978
21476
|
YAML3.stringify(actions),
|
|
19979
21477
|
"utf-8"
|
|
19980
21478
|
);
|
|
@@ -20053,7 +21551,7 @@ function openBrowser(url2) {
|
|
|
20053
21551
|
var runningServer = null;
|
|
20054
21552
|
function createWebTools(store, projectName, navGroups) {
|
|
20055
21553
|
return [
|
|
20056
|
-
|
|
21554
|
+
tool22(
|
|
20057
21555
|
"start_web_dashboard",
|
|
20058
21556
|
"Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
|
|
20059
21557
|
{
|
|
@@ -20085,7 +21583,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20085
21583
|
};
|
|
20086
21584
|
}
|
|
20087
21585
|
),
|
|
20088
|
-
|
|
21586
|
+
tool22(
|
|
20089
21587
|
"stop_web_dashboard",
|
|
20090
21588
|
"Stop the running Marvin web dashboard.",
|
|
20091
21589
|
{},
|
|
@@ -20105,7 +21603,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20105
21603
|
};
|
|
20106
21604
|
}
|
|
20107
21605
|
),
|
|
20108
|
-
|
|
21606
|
+
tool22(
|
|
20109
21607
|
"get_web_dashboard_urls",
|
|
20110
21608
|
"Get all available dashboard page URLs. The dashboard must be running.",
|
|
20111
21609
|
{},
|
|
@@ -20119,6 +21617,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20119
21617
|
const base = `http://localhost:${runningServer.port}`;
|
|
20120
21618
|
const urls = {
|
|
20121
21619
|
overview: base,
|
|
21620
|
+
upcoming: `${base}/upcoming`,
|
|
20122
21621
|
gar: `${base}/gar`,
|
|
20123
21622
|
board: `${base}/board`
|
|
20124
21623
|
};
|
|
@@ -20131,7 +21630,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20131
21630
|
},
|
|
20132
21631
|
{ annotations: { readOnlyHint: true } }
|
|
20133
21632
|
),
|
|
20134
|
-
|
|
21633
|
+
tool22(
|
|
20135
21634
|
"get_dashboard_overview",
|
|
20136
21635
|
"Get the project overview data: document type counts and recent activity. Works without the web server running.",
|
|
20137
21636
|
{},
|
|
@@ -20153,7 +21652,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20153
21652
|
},
|
|
20154
21653
|
{ annotations: { readOnlyHint: true } }
|
|
20155
21654
|
),
|
|
20156
|
-
|
|
21655
|
+
tool22(
|
|
20157
21656
|
"get_dashboard_gar",
|
|
20158
21657
|
"Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
|
|
20159
21658
|
{},
|
|
@@ -20165,7 +21664,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20165
21664
|
},
|
|
20166
21665
|
{ annotations: { readOnlyHint: true } }
|
|
20167
21666
|
),
|
|
20168
|
-
|
|
21667
|
+
tool22(
|
|
20169
21668
|
"get_dashboard_board",
|
|
20170
21669
|
"Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
|
|
20171
21670
|
{
|
|
@@ -20192,6 +21691,18 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20192
21691
|
};
|
|
20193
21692
|
},
|
|
20194
21693
|
{ annotations: { readOnlyHint: true } }
|
|
21694
|
+
),
|
|
21695
|
+
tool22(
|
|
21696
|
+
"get_dashboard_upcoming",
|
|
21697
|
+
"Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
|
|
21698
|
+
{},
|
|
21699
|
+
async () => {
|
|
21700
|
+
const data = getUpcomingData(store);
|
|
21701
|
+
return {
|
|
21702
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
21703
|
+
};
|
|
21704
|
+
},
|
|
21705
|
+
{ annotations: { readOnlyHint: true } }
|
|
20195
21706
|
)
|
|
20196
21707
|
];
|
|
20197
21708
|
}
|
|
@@ -20217,8 +21728,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
20217
21728
|
}
|
|
20218
21729
|
|
|
20219
21730
|
// src/agent/session.ts
|
|
20220
|
-
import * as
|
|
20221
|
-
import * as
|
|
21731
|
+
import * as fs10 from "fs";
|
|
21732
|
+
import * as path10 from "path";
|
|
20222
21733
|
import * as readline from "readline";
|
|
20223
21734
|
import chalk from "chalk";
|
|
20224
21735
|
import ora from "ora";
|
|
@@ -20227,13 +21738,13 @@ import {
|
|
|
20227
21738
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
20228
21739
|
|
|
20229
21740
|
// src/storage/session-store.ts
|
|
20230
|
-
import * as
|
|
20231
|
-
import * as
|
|
21741
|
+
import * as fs8 from "fs";
|
|
21742
|
+
import * as path8 from "path";
|
|
20232
21743
|
import * as YAML4 from "yaml";
|
|
20233
21744
|
var SessionStore = class {
|
|
20234
21745
|
filePath;
|
|
20235
21746
|
constructor(marvinDir) {
|
|
20236
|
-
this.filePath =
|
|
21747
|
+
this.filePath = path8.join(marvinDir, "sessions.yaml");
|
|
20237
21748
|
}
|
|
20238
21749
|
list() {
|
|
20239
21750
|
const entries = this.load();
|
|
@@ -20274,9 +21785,9 @@ var SessionStore = class {
|
|
|
20274
21785
|
this.write(entries);
|
|
20275
21786
|
}
|
|
20276
21787
|
load() {
|
|
20277
|
-
if (!
|
|
21788
|
+
if (!fs8.existsSync(this.filePath)) return [];
|
|
20278
21789
|
try {
|
|
20279
|
-
const raw =
|
|
21790
|
+
const raw = fs8.readFileSync(this.filePath, "utf-8");
|
|
20280
21791
|
const parsed = YAML4.parse(raw);
|
|
20281
21792
|
if (!Array.isArray(parsed)) return [];
|
|
20282
21793
|
return parsed;
|
|
@@ -20285,11 +21796,11 @@ var SessionStore = class {
|
|
|
20285
21796
|
}
|
|
20286
21797
|
}
|
|
20287
21798
|
write(entries) {
|
|
20288
|
-
const dir =
|
|
20289
|
-
if (!
|
|
20290
|
-
|
|
21799
|
+
const dir = path8.dirname(this.filePath);
|
|
21800
|
+
if (!fs8.existsSync(dir)) {
|
|
21801
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
20291
21802
|
}
|
|
20292
|
-
|
|
21803
|
+
fs8.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
|
|
20293
21804
|
}
|
|
20294
21805
|
};
|
|
20295
21806
|
|
|
@@ -20325,8 +21836,8 @@ function slugify3(text) {
|
|
|
20325
21836
|
}
|
|
20326
21837
|
|
|
20327
21838
|
// src/sources/manifest.ts
|
|
20328
|
-
import * as
|
|
20329
|
-
import * as
|
|
21839
|
+
import * as fs9 from "fs";
|
|
21840
|
+
import * as path9 from "path";
|
|
20330
21841
|
import * as crypto from "crypto";
|
|
20331
21842
|
import * as YAML5 from "yaml";
|
|
20332
21843
|
var MANIFEST_FILE = ".manifest.yaml";
|
|
@@ -20339,37 +21850,37 @@ var SourceManifestManager = class {
|
|
|
20339
21850
|
manifestPath;
|
|
20340
21851
|
sourcesDir;
|
|
20341
21852
|
constructor(marvinDir) {
|
|
20342
|
-
this.sourcesDir =
|
|
20343
|
-
this.manifestPath =
|
|
21853
|
+
this.sourcesDir = path9.join(marvinDir, "sources");
|
|
21854
|
+
this.manifestPath = path9.join(this.sourcesDir, MANIFEST_FILE);
|
|
20344
21855
|
this.manifest = this.load();
|
|
20345
21856
|
}
|
|
20346
21857
|
load() {
|
|
20347
|
-
if (!
|
|
21858
|
+
if (!fs9.existsSync(this.manifestPath)) {
|
|
20348
21859
|
return emptyManifest();
|
|
20349
21860
|
}
|
|
20350
|
-
const raw =
|
|
21861
|
+
const raw = fs9.readFileSync(this.manifestPath, "utf-8");
|
|
20351
21862
|
const parsed = YAML5.parse(raw);
|
|
20352
21863
|
return parsed ?? emptyManifest();
|
|
20353
21864
|
}
|
|
20354
21865
|
save() {
|
|
20355
|
-
|
|
20356
|
-
|
|
21866
|
+
fs9.mkdirSync(this.sourcesDir, { recursive: true });
|
|
21867
|
+
fs9.writeFileSync(this.manifestPath, YAML5.stringify(this.manifest), "utf-8");
|
|
20357
21868
|
}
|
|
20358
21869
|
scan() {
|
|
20359
21870
|
const added = [];
|
|
20360
21871
|
const changed = [];
|
|
20361
21872
|
const removed = [];
|
|
20362
|
-
if (!
|
|
21873
|
+
if (!fs9.existsSync(this.sourcesDir)) {
|
|
20363
21874
|
return { added, changed, removed };
|
|
20364
21875
|
}
|
|
20365
21876
|
const onDisk = new Set(
|
|
20366
|
-
|
|
20367
|
-
const ext =
|
|
21877
|
+
fs9.readdirSync(this.sourcesDir).filter((f) => {
|
|
21878
|
+
const ext = path9.extname(f).toLowerCase();
|
|
20368
21879
|
return SOURCE_EXTENSIONS.includes(ext);
|
|
20369
21880
|
})
|
|
20370
21881
|
);
|
|
20371
21882
|
for (const fileName of onDisk) {
|
|
20372
|
-
const filePath =
|
|
21883
|
+
const filePath = path9.join(this.sourcesDir, fileName);
|
|
20373
21884
|
const hash2 = this.hashFile(filePath);
|
|
20374
21885
|
const existing = this.manifest.files[fileName];
|
|
20375
21886
|
if (!existing) {
|
|
@@ -20432,7 +21943,7 @@ var SourceManifestManager = class {
|
|
|
20432
21943
|
this.save();
|
|
20433
21944
|
}
|
|
20434
21945
|
hashFile(filePath) {
|
|
20435
|
-
const content =
|
|
21946
|
+
const content = fs9.readFileSync(filePath);
|
|
20436
21947
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
20437
21948
|
}
|
|
20438
21949
|
};
|
|
@@ -20447,12 +21958,12 @@ async function startSession(options) {
|
|
|
20447
21958
|
const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
|
|
20448
21959
|
const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
|
|
20449
21960
|
const sessionStore = new SessionStore(marvinDir);
|
|
20450
|
-
const sourcesDir =
|
|
20451
|
-
const hasSourcesDir =
|
|
21961
|
+
const sourcesDir = path10.join(marvinDir, "sources");
|
|
21962
|
+
const hasSourcesDir = fs10.existsSync(sourcesDir);
|
|
20452
21963
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
20453
21964
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
20454
21965
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
20455
|
-
const codeSkillTools = getSkillTools(skillIds, allSkills, store);
|
|
21966
|
+
const codeSkillTools = getSkillTools(skillIds, allSkills, store, config2.project);
|
|
20456
21967
|
const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
|
|
20457
21968
|
const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
|
|
20458
21969
|
const allSkillIds = [...allSkills.keys()];
|
|
@@ -20564,6 +22075,7 @@ Marvin \u2014 ${persona.name}
|
|
|
20564
22075
|
"mcp__marvin-governance__get_dashboard_overview",
|
|
20565
22076
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
20566
22077
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22078
|
+
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
20567
22079
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
20568
22080
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
20569
22081
|
]
|
|
@@ -20657,13 +22169,13 @@ Session ended with error: ${message.subtype}`));
|
|
|
20657
22169
|
}
|
|
20658
22170
|
|
|
20659
22171
|
// src/mcp/stdio-server.ts
|
|
20660
|
-
import * as
|
|
20661
|
-
import * as
|
|
22172
|
+
import * as fs11 from "fs";
|
|
22173
|
+
import * as path11 from "path";
|
|
20662
22174
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20663
22175
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20664
22176
|
|
|
20665
22177
|
// src/skills/action-tools.ts
|
|
20666
|
-
import { tool as
|
|
22178
|
+
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
20667
22179
|
|
|
20668
22180
|
// src/skills/action-runner.ts
|
|
20669
22181
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -20729,7 +22241,7 @@ function createSkillActionTools(skills, context) {
|
|
|
20729
22241
|
if (!skill.actions) continue;
|
|
20730
22242
|
for (const action of skill.actions) {
|
|
20731
22243
|
tools.push(
|
|
20732
|
-
|
|
22244
|
+
tool23(
|
|
20733
22245
|
`${skill.id}__${action.id}`,
|
|
20734
22246
|
action.description,
|
|
20735
22247
|
{
|
|
@@ -20821,10 +22333,10 @@ ${lines.join("\n\n")}`;
|
|
|
20821
22333
|
}
|
|
20822
22334
|
|
|
20823
22335
|
// src/mcp/persona-tools.ts
|
|
20824
|
-
import { tool as
|
|
22336
|
+
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
20825
22337
|
function createPersonaTools(ctx, marvinDir) {
|
|
20826
22338
|
return [
|
|
20827
|
-
|
|
22339
|
+
tool24(
|
|
20828
22340
|
"set_persona",
|
|
20829
22341
|
"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.",
|
|
20830
22342
|
{
|
|
@@ -20854,7 +22366,7 @@ ${summaries}`
|
|
|
20854
22366
|
};
|
|
20855
22367
|
}
|
|
20856
22368
|
),
|
|
20857
|
-
|
|
22369
|
+
tool24(
|
|
20858
22370
|
"get_persona_guidance",
|
|
20859
22371
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
20860
22372
|
{
|
|
@@ -20956,16 +22468,16 @@ function collectTools(marvinDir) {
|
|
|
20956
22468
|
const plugin = resolvePlugin(config2.methodology);
|
|
20957
22469
|
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
20958
22470
|
const store = new DocumentStore(marvinDir, registrations);
|
|
20959
|
-
const sourcesDir =
|
|
20960
|
-
const hasSourcesDir =
|
|
22471
|
+
const sourcesDir = path11.join(marvinDir, "sources");
|
|
22472
|
+
const hasSourcesDir = fs11.existsSync(sourcesDir);
|
|
20961
22473
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
20962
22474
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
20963
22475
|
const sessionStore = new SessionStore(marvinDir);
|
|
20964
22476
|
const allSkills = loadAllSkills(marvinDir);
|
|
20965
22477
|
const allSkillIds = [...allSkills.keys()];
|
|
20966
|
-
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
22478
|
+
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
|
|
20967
22479
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
20968
|
-
const projectRoot =
|
|
22480
|
+
const projectRoot = path11.dirname(marvinDir);
|
|
20969
22481
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|
|
20970
22482
|
const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
|
|
20971
22483
|
const navGroups = buildNavGroups({
|
|
@@ -21032,8 +22544,8 @@ async function startStdioServer(options) {
|
|
|
21032
22544
|
import { Command } from "commander";
|
|
21033
22545
|
|
|
21034
22546
|
// src/cli/commands/init.ts
|
|
21035
|
-
import * as
|
|
21036
|
-
import * as
|
|
22547
|
+
import * as fs12 from "fs";
|
|
22548
|
+
import * as path12 from "path";
|
|
21037
22549
|
import * as YAML6 from "yaml";
|
|
21038
22550
|
import chalk2 from "chalk";
|
|
21039
22551
|
import { input, confirm, select } from "@inquirer/prompts";
|
|
@@ -21099,7 +22611,7 @@ async function initCommand() {
|
|
|
21099
22611
|
}
|
|
21100
22612
|
const projectName = await input({
|
|
21101
22613
|
message: "Project name:",
|
|
21102
|
-
default:
|
|
22614
|
+
default: path12.basename(cwd)
|
|
21103
22615
|
});
|
|
21104
22616
|
const methodology = await select({
|
|
21105
22617
|
message: "Methodology:",
|
|
@@ -21111,21 +22623,21 @@ async function initCommand() {
|
|
|
21111
22623
|
});
|
|
21112
22624
|
const plugin = resolvePlugin(methodology);
|
|
21113
22625
|
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
21114
|
-
const marvinDir =
|
|
22626
|
+
const marvinDir = path12.join(cwd, ".marvin");
|
|
21115
22627
|
const dirs = [
|
|
21116
22628
|
marvinDir,
|
|
21117
|
-
|
|
21118
|
-
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
|
|
22629
|
+
path12.join(marvinDir, "templates"),
|
|
22630
|
+
path12.join(marvinDir, "docs", "decisions"),
|
|
22631
|
+
path12.join(marvinDir, "docs", "actions"),
|
|
22632
|
+
path12.join(marvinDir, "docs", "questions"),
|
|
22633
|
+
path12.join(marvinDir, "sources"),
|
|
22634
|
+
path12.join(marvinDir, "skills")
|
|
21123
22635
|
];
|
|
21124
22636
|
for (const reg of registrations) {
|
|
21125
|
-
dirs.push(
|
|
22637
|
+
dirs.push(path12.join(marvinDir, "docs", reg.dirName));
|
|
21126
22638
|
}
|
|
21127
22639
|
for (const dir of dirs) {
|
|
21128
|
-
|
|
22640
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
21129
22641
|
}
|
|
21130
22642
|
const config2 = {
|
|
21131
22643
|
name: projectName,
|
|
@@ -21139,13 +22651,13 @@ async function initCommand() {
|
|
|
21139
22651
|
if (methodology === "sap-aem") {
|
|
21140
22652
|
config2.aem = { currentPhase: "assess-use-case" };
|
|
21141
22653
|
}
|
|
21142
|
-
|
|
21143
|
-
|
|
22654
|
+
fs12.writeFileSync(
|
|
22655
|
+
path12.join(marvinDir, "config.yaml"),
|
|
21144
22656
|
YAML6.stringify(config2),
|
|
21145
22657
|
"utf-8"
|
|
21146
22658
|
);
|
|
21147
|
-
|
|
21148
|
-
|
|
22659
|
+
fs12.writeFileSync(
|
|
22660
|
+
path12.join(marvinDir, "CLAUDE.md"),
|
|
21149
22661
|
getDefaultClaudeMdContent(projectName),
|
|
21150
22662
|
"utf-8"
|
|
21151
22663
|
);
|
|
@@ -21172,18 +22684,18 @@ Initialized Marvin project "${projectName}" in ${cwd}`));
|
|
|
21172
22684
|
const sourceDir = await input({
|
|
21173
22685
|
message: "Path to directory containing source documents:"
|
|
21174
22686
|
});
|
|
21175
|
-
const resolvedDir =
|
|
21176
|
-
if (
|
|
22687
|
+
const resolvedDir = path12.resolve(sourceDir);
|
|
22688
|
+
if (fs12.existsSync(resolvedDir) && fs12.statSync(resolvedDir).isDirectory()) {
|
|
21177
22689
|
const sourceExts = [".pdf", ".md", ".txt"];
|
|
21178
|
-
const files =
|
|
21179
|
-
const ext =
|
|
22690
|
+
const files = fs12.readdirSync(resolvedDir).filter((f) => {
|
|
22691
|
+
const ext = path12.extname(f).toLowerCase();
|
|
21180
22692
|
return sourceExts.includes(ext);
|
|
21181
22693
|
});
|
|
21182
22694
|
let copied = 0;
|
|
21183
22695
|
for (const file2 of files) {
|
|
21184
|
-
const src =
|
|
21185
|
-
const dest =
|
|
21186
|
-
|
|
22696
|
+
const src = path12.join(resolvedDir, file2);
|
|
22697
|
+
const dest = path12.join(marvinDir, "sources", file2);
|
|
22698
|
+
fs12.copyFileSync(src, dest);
|
|
21187
22699
|
copied++;
|
|
21188
22700
|
}
|
|
21189
22701
|
if (copied > 0) {
|
|
@@ -21473,13 +22985,13 @@ async function setApiKey() {
|
|
|
21473
22985
|
}
|
|
21474
22986
|
|
|
21475
22987
|
// src/cli/commands/ingest.ts
|
|
21476
|
-
import * as
|
|
21477
|
-
import * as
|
|
22988
|
+
import * as fs14 from "fs";
|
|
22989
|
+
import * as path14 from "path";
|
|
21478
22990
|
import chalk8 from "chalk";
|
|
21479
22991
|
|
|
21480
22992
|
// src/sources/ingest.ts
|
|
21481
|
-
import * as
|
|
21482
|
-
import * as
|
|
22993
|
+
import * as fs13 from "fs";
|
|
22994
|
+
import * as path13 from "path";
|
|
21483
22995
|
import chalk7 from "chalk";
|
|
21484
22996
|
import ora2 from "ora";
|
|
21485
22997
|
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -21582,15 +23094,15 @@ async function ingestFile(options) {
|
|
|
21582
23094
|
const persona = getPersona(personaId);
|
|
21583
23095
|
const manifest = new SourceManifestManager(marvinDir);
|
|
21584
23096
|
const sourcesDir = manifest.sourcesDir;
|
|
21585
|
-
const filePath =
|
|
21586
|
-
if (!
|
|
23097
|
+
const filePath = path13.join(sourcesDir, fileName);
|
|
23098
|
+
if (!fs13.existsSync(filePath)) {
|
|
21587
23099
|
throw new Error(`Source file not found: ${filePath}`);
|
|
21588
23100
|
}
|
|
21589
|
-
const ext =
|
|
23101
|
+
const ext = path13.extname(fileName).toLowerCase();
|
|
21590
23102
|
const isPdf = ext === ".pdf";
|
|
21591
23103
|
let fileContent = null;
|
|
21592
23104
|
if (!isPdf) {
|
|
21593
|
-
fileContent =
|
|
23105
|
+
fileContent = fs13.readFileSync(filePath, "utf-8");
|
|
21594
23106
|
}
|
|
21595
23107
|
const store = new DocumentStore(marvinDir);
|
|
21596
23108
|
const createdArtifacts = [];
|
|
@@ -21693,9 +23205,9 @@ Ingest ended with error: ${message.subtype}`)
|
|
|
21693
23205
|
async function ingestCommand(file2, options) {
|
|
21694
23206
|
const project = loadProject();
|
|
21695
23207
|
const marvinDir = project.marvinDir;
|
|
21696
|
-
const sourcesDir =
|
|
21697
|
-
if (!
|
|
21698
|
-
|
|
23208
|
+
const sourcesDir = path14.join(marvinDir, "sources");
|
|
23209
|
+
if (!fs14.existsSync(sourcesDir)) {
|
|
23210
|
+
fs14.mkdirSync(sourcesDir, { recursive: true });
|
|
21699
23211
|
}
|
|
21700
23212
|
const manifest = new SourceManifestManager(marvinDir);
|
|
21701
23213
|
manifest.scan();
|
|
@@ -21706,8 +23218,8 @@ async function ingestCommand(file2, options) {
|
|
|
21706
23218
|
return;
|
|
21707
23219
|
}
|
|
21708
23220
|
if (file2) {
|
|
21709
|
-
const filePath =
|
|
21710
|
-
if (!
|
|
23221
|
+
const filePath = path14.join(sourcesDir, file2);
|
|
23222
|
+
if (!fs14.existsSync(filePath)) {
|
|
21711
23223
|
console.log(chalk8.red(`Source file not found: ${file2}`));
|
|
21712
23224
|
console.log(chalk8.dim(`Expected at: ${filePath}`));
|
|
21713
23225
|
console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
|
|
@@ -21774,7 +23286,7 @@ import ora3 from "ora";
|
|
|
21774
23286
|
import { input as input3 } from "@inquirer/prompts";
|
|
21775
23287
|
|
|
21776
23288
|
// src/git/repository.ts
|
|
21777
|
-
import * as
|
|
23289
|
+
import * as path15 from "path";
|
|
21778
23290
|
import simpleGit from "simple-git";
|
|
21779
23291
|
var MARVIN_GITIGNORE = `node_modules/
|
|
21780
23292
|
.DS_Store
|
|
@@ -21794,7 +23306,7 @@ var DIR_TYPE_LABELS = {
|
|
|
21794
23306
|
function buildCommitMessage(files) {
|
|
21795
23307
|
const counts = /* @__PURE__ */ new Map();
|
|
21796
23308
|
for (const f of files) {
|
|
21797
|
-
const parts2 = f.split(
|
|
23309
|
+
const parts2 = f.split(path15.sep).join("/").split("/");
|
|
21798
23310
|
const docsIdx = parts2.indexOf("docs");
|
|
21799
23311
|
if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
|
|
21800
23312
|
const dirName = parts2[docsIdx + 1];
|
|
@@ -21834,9 +23346,9 @@ var MarvinGit = class {
|
|
|
21834
23346
|
);
|
|
21835
23347
|
}
|
|
21836
23348
|
await this.git.init();
|
|
21837
|
-
const { writeFileSync:
|
|
21838
|
-
|
|
21839
|
-
|
|
23349
|
+
const { writeFileSync: writeFileSync11 } = await import("fs");
|
|
23350
|
+
writeFileSync11(
|
|
23351
|
+
path15.join(this.marvinDir, ".gitignore"),
|
|
21840
23352
|
MARVIN_GITIGNORE,
|
|
21841
23353
|
"utf-8"
|
|
21842
23354
|
);
|
|
@@ -21956,7 +23468,7 @@ var MarvinGit = class {
|
|
|
21956
23468
|
}
|
|
21957
23469
|
}
|
|
21958
23470
|
static async clone(url2, targetDir) {
|
|
21959
|
-
const marvinDir =
|
|
23471
|
+
const marvinDir = path15.join(targetDir, ".marvin");
|
|
21960
23472
|
const { existsSync: existsSync17 } = await import("fs");
|
|
21961
23473
|
if (existsSync17(marvinDir)) {
|
|
21962
23474
|
throw new GitSyncError(
|
|
@@ -22142,8 +23654,8 @@ async function serveCommand() {
|
|
|
22142
23654
|
}
|
|
22143
23655
|
|
|
22144
23656
|
// src/cli/commands/skills.ts
|
|
22145
|
-
import * as
|
|
22146
|
-
import * as
|
|
23657
|
+
import * as fs15 from "fs";
|
|
23658
|
+
import * as path16 from "path";
|
|
22147
23659
|
import * as YAML7 from "yaml";
|
|
22148
23660
|
import matter3 from "gray-matter";
|
|
22149
23661
|
import chalk10 from "chalk";
|
|
@@ -22249,14 +23761,14 @@ async function skillsRemoveCommand(skillId, options) {
|
|
|
22249
23761
|
}
|
|
22250
23762
|
async function skillsCreateCommand(name) {
|
|
22251
23763
|
const project = loadProject();
|
|
22252
|
-
const skillsDir =
|
|
22253
|
-
|
|
22254
|
-
const skillDir =
|
|
22255
|
-
if (
|
|
23764
|
+
const skillsDir = path16.join(project.marvinDir, "skills");
|
|
23765
|
+
fs15.mkdirSync(skillsDir, { recursive: true });
|
|
23766
|
+
const skillDir = path16.join(skillsDir, name);
|
|
23767
|
+
if (fs15.existsSync(skillDir)) {
|
|
22256
23768
|
console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
|
|
22257
23769
|
return;
|
|
22258
23770
|
}
|
|
22259
|
-
|
|
23771
|
+
fs15.mkdirSync(skillDir, { recursive: true });
|
|
22260
23772
|
const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
22261
23773
|
const frontmatter = {
|
|
22262
23774
|
name,
|
|
@@ -22270,7 +23782,7 @@ async function skillsCreateCommand(name) {
|
|
|
22270
23782
|
You have the **${displayName}** skill.
|
|
22271
23783
|
`;
|
|
22272
23784
|
const skillMd = matter3.stringify(body, frontmatter);
|
|
22273
|
-
|
|
23785
|
+
fs15.writeFileSync(path16.join(skillDir, "SKILL.md"), skillMd, "utf-8");
|
|
22274
23786
|
const actions = [
|
|
22275
23787
|
{
|
|
22276
23788
|
id: "run",
|
|
@@ -22280,7 +23792,7 @@ You have the **${displayName}** skill.
|
|
|
22280
23792
|
maxTurns: 5
|
|
22281
23793
|
}
|
|
22282
23794
|
];
|
|
22283
|
-
|
|
23795
|
+
fs15.writeFileSync(path16.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
|
|
22284
23796
|
console.log(chalk10.green(`Created skill: ${skillDir}/`));
|
|
22285
23797
|
console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
|
|
22286
23798
|
console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
|
|
@@ -22288,14 +23800,14 @@ You have the **${displayName}** skill.
|
|
|
22288
23800
|
}
|
|
22289
23801
|
async function skillsMigrateCommand() {
|
|
22290
23802
|
const project = loadProject();
|
|
22291
|
-
const skillsDir =
|
|
22292
|
-
if (!
|
|
23803
|
+
const skillsDir = path16.join(project.marvinDir, "skills");
|
|
23804
|
+
if (!fs15.existsSync(skillsDir)) {
|
|
22293
23805
|
console.log(chalk10.dim("No skills directory found."));
|
|
22294
23806
|
return;
|
|
22295
23807
|
}
|
|
22296
23808
|
let entries;
|
|
22297
23809
|
try {
|
|
22298
|
-
entries =
|
|
23810
|
+
entries = fs15.readdirSync(skillsDir);
|
|
22299
23811
|
} catch {
|
|
22300
23812
|
console.log(chalk10.red("Could not read skills directory."));
|
|
22301
23813
|
return;
|
|
@@ -22307,16 +23819,16 @@ async function skillsMigrateCommand() {
|
|
|
22307
23819
|
}
|
|
22308
23820
|
let migrated = 0;
|
|
22309
23821
|
for (const file2 of yamlFiles) {
|
|
22310
|
-
const yamlPath =
|
|
23822
|
+
const yamlPath = path16.join(skillsDir, file2);
|
|
22311
23823
|
const baseName = file2.replace(/\.(yaml|yml)$/, "");
|
|
22312
|
-
const outputDir =
|
|
22313
|
-
if (
|
|
23824
|
+
const outputDir = path16.join(skillsDir, baseName);
|
|
23825
|
+
if (fs15.existsSync(outputDir)) {
|
|
22314
23826
|
console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
|
|
22315
23827
|
continue;
|
|
22316
23828
|
}
|
|
22317
23829
|
try {
|
|
22318
23830
|
migrateYamlToSkillMd(yamlPath, outputDir);
|
|
22319
|
-
|
|
23831
|
+
fs15.renameSync(yamlPath, `${yamlPath}.bak`);
|
|
22320
23832
|
console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
|
|
22321
23833
|
migrated++;
|
|
22322
23834
|
} catch (err) {
|
|
@@ -22330,35 +23842,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
|
|
|
22330
23842
|
}
|
|
22331
23843
|
|
|
22332
23844
|
// src/cli/commands/import.ts
|
|
22333
|
-
import * as
|
|
22334
|
-
import * as
|
|
23845
|
+
import * as fs18 from "fs";
|
|
23846
|
+
import * as path19 from "path";
|
|
22335
23847
|
import chalk11 from "chalk";
|
|
22336
23848
|
|
|
22337
23849
|
// src/import/engine.ts
|
|
22338
|
-
import * as
|
|
22339
|
-
import * as
|
|
23850
|
+
import * as fs17 from "fs";
|
|
23851
|
+
import * as path18 from "path";
|
|
22340
23852
|
import matter5 from "gray-matter";
|
|
22341
23853
|
|
|
22342
23854
|
// src/import/classifier.ts
|
|
22343
|
-
import * as
|
|
22344
|
-
import * as
|
|
23855
|
+
import * as fs16 from "fs";
|
|
23856
|
+
import * as path17 from "path";
|
|
22345
23857
|
import matter4 from "gray-matter";
|
|
22346
23858
|
var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
|
|
22347
23859
|
var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
|
|
22348
23860
|
var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
|
|
22349
23861
|
function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
22350
|
-
const resolved =
|
|
22351
|
-
const stat =
|
|
23862
|
+
const resolved = path17.resolve(inputPath);
|
|
23863
|
+
const stat = fs16.statSync(resolved);
|
|
22352
23864
|
if (!stat.isDirectory()) {
|
|
22353
23865
|
return classifyFile(resolved, knownTypes);
|
|
22354
23866
|
}
|
|
22355
|
-
if (
|
|
23867
|
+
if (path17.basename(resolved) === ".marvin" || fs16.existsSync(path17.join(resolved, "config.yaml"))) {
|
|
22356
23868
|
return { type: "marvin-project", inputPath: resolved };
|
|
22357
23869
|
}
|
|
22358
23870
|
const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
|
|
22359
|
-
const entries =
|
|
23871
|
+
const entries = fs16.readdirSync(resolved);
|
|
22360
23872
|
const hasDocSubdirs = entries.some(
|
|
22361
|
-
(e) => allDirNames.has(e) &&
|
|
23873
|
+
(e) => allDirNames.has(e) && fs16.statSync(path17.join(resolved, e)).isDirectory()
|
|
22362
23874
|
);
|
|
22363
23875
|
if (hasDocSubdirs) {
|
|
22364
23876
|
return { type: "docs-directory", inputPath: resolved };
|
|
@@ -22367,7 +23879,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
|
22367
23879
|
if (mdFiles.length > 0) {
|
|
22368
23880
|
const hasMarvinDocs = mdFiles.some((f) => {
|
|
22369
23881
|
try {
|
|
22370
|
-
const raw =
|
|
23882
|
+
const raw = fs16.readFileSync(path17.join(resolved, f), "utf-8");
|
|
22371
23883
|
const { data } = matter4(raw);
|
|
22372
23884
|
return isValidMarvinDocument(data, knownTypes);
|
|
22373
23885
|
} catch {
|
|
@@ -22381,14 +23893,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
|
22381
23893
|
return { type: "raw-source-dir", inputPath: resolved };
|
|
22382
23894
|
}
|
|
22383
23895
|
function classifyFile(filePath, knownTypes) {
|
|
22384
|
-
const resolved =
|
|
22385
|
-
const ext =
|
|
23896
|
+
const resolved = path17.resolve(filePath);
|
|
23897
|
+
const ext = path17.extname(resolved).toLowerCase();
|
|
22386
23898
|
if (RAW_SOURCE_EXTENSIONS.has(ext)) {
|
|
22387
23899
|
return { type: "raw-source-file", inputPath: resolved };
|
|
22388
23900
|
}
|
|
22389
23901
|
if (ext === ".md") {
|
|
22390
23902
|
try {
|
|
22391
|
-
const raw =
|
|
23903
|
+
const raw = fs16.readFileSync(resolved, "utf-8");
|
|
22392
23904
|
const { data } = matter4(raw);
|
|
22393
23905
|
if (isValidMarvinDocument(data, knownTypes)) {
|
|
22394
23906
|
return { type: "marvin-document", inputPath: resolved };
|
|
@@ -22513,9 +24025,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
22513
24025
|
continue;
|
|
22514
24026
|
}
|
|
22515
24027
|
if (item.action === "copy") {
|
|
22516
|
-
const targetDir =
|
|
22517
|
-
|
|
22518
|
-
|
|
24028
|
+
const targetDir = path18.dirname(item.targetPath);
|
|
24029
|
+
fs17.mkdirSync(targetDir, { recursive: true });
|
|
24030
|
+
fs17.copyFileSync(item.sourcePath, item.targetPath);
|
|
22519
24031
|
copied++;
|
|
22520
24032
|
continue;
|
|
22521
24033
|
}
|
|
@@ -22551,19 +24063,19 @@ function formatPlanSummary(plan) {
|
|
|
22551
24063
|
lines.push(`Documents to import: ${imports.length}`);
|
|
22552
24064
|
for (const item of imports) {
|
|
22553
24065
|
const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
|
|
22554
|
-
lines.push(` ${idInfo} ${
|
|
24066
|
+
lines.push(` ${idInfo} ${path18.basename(item.sourcePath)}`);
|
|
22555
24067
|
}
|
|
22556
24068
|
}
|
|
22557
24069
|
if (copies.length > 0) {
|
|
22558
24070
|
lines.push(`Files to copy to sources/: ${copies.length}`);
|
|
22559
24071
|
for (const item of copies) {
|
|
22560
|
-
lines.push(` ${
|
|
24072
|
+
lines.push(` ${path18.basename(item.sourcePath)} \u2192 ${path18.basename(item.targetPath)}`);
|
|
22561
24073
|
}
|
|
22562
24074
|
}
|
|
22563
24075
|
if (skips.length > 0) {
|
|
22564
24076
|
lines.push(`Skipped (conflict): ${skips.length}`);
|
|
22565
24077
|
for (const item of skips) {
|
|
22566
|
-
lines.push(` ${item.originalId ??
|
|
24078
|
+
lines.push(` ${item.originalId ?? path18.basename(item.sourcePath)} ${item.reason ?? ""}`);
|
|
22567
24079
|
}
|
|
22568
24080
|
}
|
|
22569
24081
|
if (plan.items.length === 0) {
|
|
@@ -22596,11 +24108,11 @@ function getDirNameForType(store, type) {
|
|
|
22596
24108
|
}
|
|
22597
24109
|
function collectMarvinDocs(dir, knownTypes) {
|
|
22598
24110
|
const docs = [];
|
|
22599
|
-
const files =
|
|
24111
|
+
const files = fs17.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
22600
24112
|
for (const file2 of files) {
|
|
22601
|
-
const filePath =
|
|
24113
|
+
const filePath = path18.join(dir, file2);
|
|
22602
24114
|
try {
|
|
22603
|
-
const raw =
|
|
24115
|
+
const raw = fs17.readFileSync(filePath, "utf-8");
|
|
22604
24116
|
const { data, content } = matter5(raw);
|
|
22605
24117
|
if (isValidMarvinDocument(data, knownTypes)) {
|
|
22606
24118
|
docs.push({
|
|
@@ -22656,23 +24168,23 @@ function planDocImports(docs, store, options) {
|
|
|
22656
24168
|
}
|
|
22657
24169
|
function planFromMarvinProject(classification, store, _marvinDir, options) {
|
|
22658
24170
|
let projectDir = classification.inputPath;
|
|
22659
|
-
if (
|
|
22660
|
-
const inner =
|
|
22661
|
-
if (
|
|
24171
|
+
if (path18.basename(projectDir) !== ".marvin") {
|
|
24172
|
+
const inner = path18.join(projectDir, ".marvin");
|
|
24173
|
+
if (fs17.existsSync(inner)) {
|
|
22662
24174
|
projectDir = inner;
|
|
22663
24175
|
}
|
|
22664
24176
|
}
|
|
22665
|
-
const docsDir =
|
|
22666
|
-
if (!
|
|
24177
|
+
const docsDir = path18.join(projectDir, "docs");
|
|
24178
|
+
if (!fs17.existsSync(docsDir)) {
|
|
22667
24179
|
return [];
|
|
22668
24180
|
}
|
|
22669
24181
|
const knownTypes = store.registeredTypes;
|
|
22670
24182
|
const allDocs = [];
|
|
22671
|
-
const subdirs =
|
|
22672
|
-
(d) =>
|
|
24183
|
+
const subdirs = fs17.readdirSync(docsDir).filter(
|
|
24184
|
+
(d) => fs17.statSync(path18.join(docsDir, d)).isDirectory()
|
|
22673
24185
|
);
|
|
22674
24186
|
for (const subdir of subdirs) {
|
|
22675
|
-
const docs = collectMarvinDocs(
|
|
24187
|
+
const docs = collectMarvinDocs(path18.join(docsDir, subdir), knownTypes);
|
|
22676
24188
|
allDocs.push(...docs);
|
|
22677
24189
|
}
|
|
22678
24190
|
return planDocImports(allDocs, store, options);
|
|
@@ -22682,10 +24194,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
|
|
|
22682
24194
|
const knownTypes = store.registeredTypes;
|
|
22683
24195
|
const allDocs = [];
|
|
22684
24196
|
allDocs.push(...collectMarvinDocs(dir, knownTypes));
|
|
22685
|
-
const entries =
|
|
24197
|
+
const entries = fs17.readdirSync(dir);
|
|
22686
24198
|
for (const entry of entries) {
|
|
22687
|
-
const entryPath =
|
|
22688
|
-
if (
|
|
24199
|
+
const entryPath = path18.join(dir, entry);
|
|
24200
|
+
if (fs17.statSync(entryPath).isDirectory()) {
|
|
22689
24201
|
allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
|
|
22690
24202
|
}
|
|
22691
24203
|
}
|
|
@@ -22694,7 +24206,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
|
|
|
22694
24206
|
function planFromSingleDocument(classification, store, _marvinDir, options) {
|
|
22695
24207
|
const filePath = classification.inputPath;
|
|
22696
24208
|
const knownTypes = store.registeredTypes;
|
|
22697
|
-
const raw =
|
|
24209
|
+
const raw = fs17.readFileSync(filePath, "utf-8");
|
|
22698
24210
|
const { data, content } = matter5(raw);
|
|
22699
24211
|
if (!isValidMarvinDocument(data, knownTypes)) {
|
|
22700
24212
|
return [];
|
|
@@ -22710,14 +24222,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
|
|
|
22710
24222
|
}
|
|
22711
24223
|
function planFromRawSourceDir(classification, marvinDir) {
|
|
22712
24224
|
const dir = classification.inputPath;
|
|
22713
|
-
const sourcesDir =
|
|
24225
|
+
const sourcesDir = path18.join(marvinDir, "sources");
|
|
22714
24226
|
const items = [];
|
|
22715
|
-
const files =
|
|
22716
|
-
const stat =
|
|
24227
|
+
const files = fs17.readdirSync(dir).filter((f) => {
|
|
24228
|
+
const stat = fs17.statSync(path18.join(dir, f));
|
|
22717
24229
|
return stat.isFile();
|
|
22718
24230
|
});
|
|
22719
24231
|
for (const file2 of files) {
|
|
22720
|
-
const sourcePath =
|
|
24232
|
+
const sourcePath = path18.join(dir, file2);
|
|
22721
24233
|
const targetPath = resolveSourceFileName(sourcesDir, file2);
|
|
22722
24234
|
items.push({
|
|
22723
24235
|
action: "copy",
|
|
@@ -22728,8 +24240,8 @@ function planFromRawSourceDir(classification, marvinDir) {
|
|
|
22728
24240
|
return items;
|
|
22729
24241
|
}
|
|
22730
24242
|
function planFromRawSourceFile(classification, marvinDir) {
|
|
22731
|
-
const sourcesDir =
|
|
22732
|
-
const fileName =
|
|
24243
|
+
const sourcesDir = path18.join(marvinDir, "sources");
|
|
24244
|
+
const fileName = path18.basename(classification.inputPath);
|
|
22733
24245
|
const targetPath = resolveSourceFileName(sourcesDir, fileName);
|
|
22734
24246
|
return [
|
|
22735
24247
|
{
|
|
@@ -22740,25 +24252,25 @@ function planFromRawSourceFile(classification, marvinDir) {
|
|
|
22740
24252
|
];
|
|
22741
24253
|
}
|
|
22742
24254
|
function resolveSourceFileName(sourcesDir, fileName) {
|
|
22743
|
-
const targetPath =
|
|
22744
|
-
if (!
|
|
24255
|
+
const targetPath = path18.join(sourcesDir, fileName);
|
|
24256
|
+
if (!fs17.existsSync(targetPath)) {
|
|
22745
24257
|
return targetPath;
|
|
22746
24258
|
}
|
|
22747
|
-
const ext =
|
|
22748
|
-
const base =
|
|
24259
|
+
const ext = path18.extname(fileName);
|
|
24260
|
+
const base = path18.basename(fileName, ext);
|
|
22749
24261
|
let counter = 1;
|
|
22750
24262
|
let candidate;
|
|
22751
24263
|
do {
|
|
22752
|
-
candidate =
|
|
24264
|
+
candidate = path18.join(sourcesDir, `${base}-${counter}${ext}`);
|
|
22753
24265
|
counter++;
|
|
22754
|
-
} while (
|
|
24266
|
+
} while (fs17.existsSync(candidate));
|
|
22755
24267
|
return candidate;
|
|
22756
24268
|
}
|
|
22757
24269
|
|
|
22758
24270
|
// src/cli/commands/import.ts
|
|
22759
24271
|
async function importCommand(inputPath, options) {
|
|
22760
|
-
const resolved =
|
|
22761
|
-
if (!
|
|
24272
|
+
const resolved = path19.resolve(inputPath);
|
|
24273
|
+
if (!fs18.existsSync(resolved)) {
|
|
22762
24274
|
throw new ImportError(`Path not found: ${resolved}`);
|
|
22763
24275
|
}
|
|
22764
24276
|
const project = loadProject();
|
|
@@ -22810,7 +24322,7 @@ async function importCommand(inputPath, options) {
|
|
|
22810
24322
|
console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
|
|
22811
24323
|
const manifest = new SourceManifestManager(marvinDir);
|
|
22812
24324
|
manifest.scan();
|
|
22813
|
-
const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) =>
|
|
24325
|
+
const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path19.basename(i.targetPath));
|
|
22814
24326
|
for (const fileName of copiedFileNames) {
|
|
22815
24327
|
try {
|
|
22816
24328
|
await ingestFile({
|
|
@@ -23699,14 +25211,14 @@ async function webCommand(options) {
|
|
|
23699
25211
|
}
|
|
23700
25212
|
|
|
23701
25213
|
// src/cli/commands/generate.ts
|
|
23702
|
-
import * as
|
|
23703
|
-
import * as
|
|
25214
|
+
import * as fs19 from "fs";
|
|
25215
|
+
import * as path20 from "path";
|
|
23704
25216
|
import chalk18 from "chalk";
|
|
23705
25217
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
23706
25218
|
async function generateClaudeMdCommand(options) {
|
|
23707
25219
|
const project = loadProject();
|
|
23708
|
-
const filePath =
|
|
23709
|
-
if (
|
|
25220
|
+
const filePath = path20.join(project.marvinDir, "CLAUDE.md");
|
|
25221
|
+
if (fs19.existsSync(filePath) && !options.force) {
|
|
23710
25222
|
const overwrite = await confirm2({
|
|
23711
25223
|
message: ".marvin/CLAUDE.md already exists. Overwrite?",
|
|
23712
25224
|
default: false
|
|
@@ -23716,7 +25228,7 @@ async function generateClaudeMdCommand(options) {
|
|
|
23716
25228
|
return;
|
|
23717
25229
|
}
|
|
23718
25230
|
}
|
|
23719
|
-
|
|
25231
|
+
fs19.writeFileSync(
|
|
23720
25232
|
filePath,
|
|
23721
25233
|
getDefaultClaudeMdContent(project.config.name),
|
|
23722
25234
|
"utf-8"
|
|
@@ -23729,7 +25241,7 @@ function createProgram() {
|
|
|
23729
25241
|
const program = new Command();
|
|
23730
25242
|
program.name("marvin").description(
|
|
23731
25243
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
23732
|
-
).version("0.4.
|
|
25244
|
+
).version("0.4.5");
|
|
23733
25245
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
23734
25246
|
await initCommand();
|
|
23735
25247
|
});
|