mrvn-cli 0.3.3 → 0.3.6
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 +303 -147
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1278 -289
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +855 -89
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1283 -296
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -920,10 +920,10 @@ function mergeDefs(...defs) {
|
|
|
920
920
|
function cloneDef(schema) {
|
|
921
921
|
return mergeDefs(schema._zod.def);
|
|
922
922
|
}
|
|
923
|
-
function getElementAtPath(obj,
|
|
924
|
-
if (!
|
|
923
|
+
function getElementAtPath(obj, path20) {
|
|
924
|
+
if (!path20)
|
|
925
925
|
return obj;
|
|
926
|
-
return
|
|
926
|
+
return path20.reduce((acc, key) => acc?.[key], obj);
|
|
927
927
|
}
|
|
928
928
|
function promiseAllObject(promisesObj) {
|
|
929
929
|
const keys = Object.keys(promisesObj);
|
|
@@ -1306,11 +1306,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1306
1306
|
}
|
|
1307
1307
|
return false;
|
|
1308
1308
|
}
|
|
1309
|
-
function prefixIssues(
|
|
1309
|
+
function prefixIssues(path20, issues) {
|
|
1310
1310
|
return issues.map((iss) => {
|
|
1311
1311
|
var _a2;
|
|
1312
1312
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
1313
|
-
iss.path.unshift(
|
|
1313
|
+
iss.path.unshift(path20);
|
|
1314
1314
|
return iss;
|
|
1315
1315
|
});
|
|
1316
1316
|
}
|
|
@@ -1493,7 +1493,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1493
1493
|
}
|
|
1494
1494
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
1495
1495
|
const result = { errors: [] };
|
|
1496
|
-
const processError = (error49,
|
|
1496
|
+
const processError = (error49, path20 = []) => {
|
|
1497
1497
|
var _a2, _b;
|
|
1498
1498
|
for (const issue2 of error49.issues) {
|
|
1499
1499
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -1503,7 +1503,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1503
1503
|
} else if (issue2.code === "invalid_element") {
|
|
1504
1504
|
processError({ issues: issue2.issues }, issue2.path);
|
|
1505
1505
|
} else {
|
|
1506
|
-
const fullpath = [...
|
|
1506
|
+
const fullpath = [...path20, ...issue2.path];
|
|
1507
1507
|
if (fullpath.length === 0) {
|
|
1508
1508
|
result.errors.push(mapper(issue2));
|
|
1509
1509
|
continue;
|
|
@@ -1535,8 +1535,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1535
1535
|
}
|
|
1536
1536
|
function toDotPath(_path) {
|
|
1537
1537
|
const segs = [];
|
|
1538
|
-
const
|
|
1539
|
-
for (const seg of
|
|
1538
|
+
const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
1539
|
+
for (const seg of path20) {
|
|
1540
1540
|
if (typeof seg === "number")
|
|
1541
1541
|
segs.push(`[${seg}]`);
|
|
1542
1542
|
else if (typeof seg === "symbol")
|
|
@@ -13513,13 +13513,13 @@ function resolveRef(ref, ctx) {
|
|
|
13513
13513
|
if (!ref.startsWith("#")) {
|
|
13514
13514
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
13515
13515
|
}
|
|
13516
|
-
const
|
|
13517
|
-
if (
|
|
13516
|
+
const path20 = ref.slice(1).split("/").filter(Boolean);
|
|
13517
|
+
if (path20.length === 0) {
|
|
13518
13518
|
return ctx.rootSchema;
|
|
13519
13519
|
}
|
|
13520
13520
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
13521
|
-
if (
|
|
13522
|
-
const key =
|
|
13521
|
+
if (path20[0] === defsKey) {
|
|
13522
|
+
const key = path20[1];
|
|
13523
13523
|
if (!key || !ctx.defs[key]) {
|
|
13524
13524
|
throw new Error(`Reference not found: ${ref}`);
|
|
13525
13525
|
}
|
|
@@ -13942,7 +13942,7 @@ function createMeetingTools(store) {
|
|
|
13942
13942
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
13943
13943
|
};
|
|
13944
13944
|
},
|
|
13945
|
-
{ annotations: {
|
|
13945
|
+
{ annotations: { readOnlyHint: true } }
|
|
13946
13946
|
),
|
|
13947
13947
|
tool(
|
|
13948
13948
|
"get_meeting",
|
|
@@ -13969,7 +13969,7 @@ function createMeetingTools(store) {
|
|
|
13969
13969
|
]
|
|
13970
13970
|
};
|
|
13971
13971
|
},
|
|
13972
|
-
{ annotations: {
|
|
13972
|
+
{ annotations: { readOnlyHint: true } }
|
|
13973
13973
|
),
|
|
13974
13974
|
tool(
|
|
13975
13975
|
"create_meeting",
|
|
@@ -14094,7 +14094,7 @@ function createMeetingTools(store) {
|
|
|
14094
14094
|
content: [{ type: "text", text: sections.join("\n") }]
|
|
14095
14095
|
};
|
|
14096
14096
|
},
|
|
14097
|
-
{ annotations: {
|
|
14097
|
+
{ annotations: { readOnlyHint: true } }
|
|
14098
14098
|
)
|
|
14099
14099
|
];
|
|
14100
14100
|
}
|
|
@@ -14111,12 +14111,20 @@ function collectGarMetrics(store) {
|
|
|
14111
14111
|
const blockedItems = allDocs.filter(
|
|
14112
14112
|
(d) => d.frontmatter.tags?.includes("blocked")
|
|
14113
14113
|
);
|
|
14114
|
-
const
|
|
14114
|
+
const tagOverdueItems = allDocs.filter(
|
|
14115
14115
|
(d) => d.frontmatter.tags?.includes("overdue")
|
|
14116
14116
|
);
|
|
14117
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
14118
|
+
const dateOverdueActions = openActions.filter((d) => {
|
|
14119
|
+
const dueDate = d.frontmatter.dueDate;
|
|
14120
|
+
return typeof dueDate === "string" && dueDate < today;
|
|
14121
|
+
});
|
|
14122
|
+
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
14123
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
14124
|
+
);
|
|
14117
14125
|
const openQuestions = store.list({ type: "question", status: "open" });
|
|
14118
14126
|
const riskItems = allDocs.filter(
|
|
14119
|
-
(d) => d.frontmatter.tags?.includes("risk")
|
|
14127
|
+
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
14120
14128
|
);
|
|
14121
14129
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
14122
14130
|
const total = allActions.length;
|
|
@@ -14222,6 +14230,253 @@ function evaluateGar(projectName, metrics) {
|
|
|
14222
14230
|
};
|
|
14223
14231
|
}
|
|
14224
14232
|
|
|
14233
|
+
// src/reports/health/collector.ts
|
|
14234
|
+
var FIELD_CHECKS = [
|
|
14235
|
+
{
|
|
14236
|
+
type: "action",
|
|
14237
|
+
openStatuses: ["open", "in-progress"],
|
|
14238
|
+
requiredFields: ["owner", "priority", "dueDate", "content"]
|
|
14239
|
+
},
|
|
14240
|
+
{
|
|
14241
|
+
type: "decision",
|
|
14242
|
+
openStatuses: ["open", "proposed"],
|
|
14243
|
+
requiredFields: ["owner", "content"]
|
|
14244
|
+
},
|
|
14245
|
+
{
|
|
14246
|
+
type: "question",
|
|
14247
|
+
openStatuses: ["open"],
|
|
14248
|
+
requiredFields: ["owner", "content"]
|
|
14249
|
+
},
|
|
14250
|
+
{
|
|
14251
|
+
type: "feature",
|
|
14252
|
+
openStatuses: ["draft", "approved"],
|
|
14253
|
+
requiredFields: ["owner", "priority", "content"]
|
|
14254
|
+
},
|
|
14255
|
+
{
|
|
14256
|
+
type: "epic",
|
|
14257
|
+
openStatuses: ["planned", "in-progress"],
|
|
14258
|
+
requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
|
|
14259
|
+
},
|
|
14260
|
+
{
|
|
14261
|
+
type: "sprint",
|
|
14262
|
+
openStatuses: ["planned", "active"],
|
|
14263
|
+
requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
|
|
14264
|
+
}
|
|
14265
|
+
];
|
|
14266
|
+
var STALE_THRESHOLD_DAYS = 14;
|
|
14267
|
+
var AGING_THRESHOLD_DAYS = 30;
|
|
14268
|
+
function daysBetween(a, b) {
|
|
14269
|
+
const msPerDay = 864e5;
|
|
14270
|
+
const dateA = new Date(a);
|
|
14271
|
+
const dateB = new Date(b);
|
|
14272
|
+
return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
|
|
14273
|
+
}
|
|
14274
|
+
function checkMissingFields(doc, requiredFields) {
|
|
14275
|
+
const missing = [];
|
|
14276
|
+
for (const field of requiredFields) {
|
|
14277
|
+
if (field === "content") {
|
|
14278
|
+
if (!doc.content || doc.content.trim().length === 0) {
|
|
14279
|
+
missing.push("content");
|
|
14280
|
+
}
|
|
14281
|
+
} else if (field === "linkedEpics") {
|
|
14282
|
+
const val = doc.frontmatter[field];
|
|
14283
|
+
if (!Array.isArray(val) || val.length === 0) {
|
|
14284
|
+
missing.push(field);
|
|
14285
|
+
}
|
|
14286
|
+
} else {
|
|
14287
|
+
const val = doc.frontmatter[field];
|
|
14288
|
+
if (val === void 0 || val === null || val === "") {
|
|
14289
|
+
missing.push(field);
|
|
14290
|
+
}
|
|
14291
|
+
}
|
|
14292
|
+
}
|
|
14293
|
+
return missing;
|
|
14294
|
+
}
|
|
14295
|
+
function collectCompleteness(store) {
|
|
14296
|
+
const result = {};
|
|
14297
|
+
for (const check2 of FIELD_CHECKS) {
|
|
14298
|
+
const allOfType = store.list({ type: check2.type });
|
|
14299
|
+
const openDocs = allOfType.filter(
|
|
14300
|
+
(d) => check2.openStatuses.includes(d.frontmatter.status)
|
|
14301
|
+
);
|
|
14302
|
+
const gaps = [];
|
|
14303
|
+
let complete = 0;
|
|
14304
|
+
for (const doc of openDocs) {
|
|
14305
|
+
const missingFields = checkMissingFields(doc, check2.requiredFields);
|
|
14306
|
+
if (missingFields.length === 0) {
|
|
14307
|
+
complete++;
|
|
14308
|
+
} else {
|
|
14309
|
+
gaps.push({
|
|
14310
|
+
id: doc.frontmatter.id,
|
|
14311
|
+
title: doc.frontmatter.title,
|
|
14312
|
+
missingFields
|
|
14313
|
+
});
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
result[check2.type] = {
|
|
14317
|
+
total: openDocs.length,
|
|
14318
|
+
complete,
|
|
14319
|
+
gaps
|
|
14320
|
+
};
|
|
14321
|
+
}
|
|
14322
|
+
return result;
|
|
14323
|
+
}
|
|
14324
|
+
function collectProcess(store) {
|
|
14325
|
+
const today = (/* @__PURE__ */ new Date()).toISOString();
|
|
14326
|
+
const allDocs = store.list();
|
|
14327
|
+
const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
|
|
14328
|
+
const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
|
|
14329
|
+
const stale = [];
|
|
14330
|
+
for (const doc of openDocs) {
|
|
14331
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
14332
|
+
const days = daysBetween(updated, today);
|
|
14333
|
+
if (days >= STALE_THRESHOLD_DAYS) {
|
|
14334
|
+
stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
14335
|
+
}
|
|
14336
|
+
}
|
|
14337
|
+
const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
|
|
14338
|
+
const agingActions = [];
|
|
14339
|
+
for (const doc of openActions) {
|
|
14340
|
+
const days = daysBetween(doc.frontmatter.created, today);
|
|
14341
|
+
if (days >= AGING_THRESHOLD_DAYS) {
|
|
14342
|
+
agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
14343
|
+
}
|
|
14344
|
+
}
|
|
14345
|
+
const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
|
|
14346
|
+
let decisionTotal = 0;
|
|
14347
|
+
for (const doc of resolvedDecisions) {
|
|
14348
|
+
decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
14349
|
+
}
|
|
14350
|
+
const decisionVelocity = {
|
|
14351
|
+
avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
|
|
14352
|
+
count: resolvedDecisions.length
|
|
14353
|
+
};
|
|
14354
|
+
const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
|
|
14355
|
+
let questionTotal = 0;
|
|
14356
|
+
for (const doc of answeredQuestions) {
|
|
14357
|
+
questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
14358
|
+
}
|
|
14359
|
+
const questionResolution = {
|
|
14360
|
+
avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
|
|
14361
|
+
count: answeredQuestions.length
|
|
14362
|
+
};
|
|
14363
|
+
return { stale, agingActions, decisionVelocity, questionResolution };
|
|
14364
|
+
}
|
|
14365
|
+
function collectHealthMetrics(store) {
|
|
14366
|
+
return {
|
|
14367
|
+
completeness: collectCompleteness(store),
|
|
14368
|
+
process: collectProcess(store)
|
|
14369
|
+
};
|
|
14370
|
+
}
|
|
14371
|
+
|
|
14372
|
+
// src/reports/health/evaluator.ts
|
|
14373
|
+
function worstStatus2(statuses) {
|
|
14374
|
+
if (statuses.includes("red")) return "red";
|
|
14375
|
+
if (statuses.includes("amber")) return "amber";
|
|
14376
|
+
return "green";
|
|
14377
|
+
}
|
|
14378
|
+
function completenessStatus(total, complete) {
|
|
14379
|
+
if (total === 0) return "green";
|
|
14380
|
+
const pct = Math.round(complete / total * 100);
|
|
14381
|
+
if (pct >= 100) return "green";
|
|
14382
|
+
if (pct >= 75) return "amber";
|
|
14383
|
+
return "red";
|
|
14384
|
+
}
|
|
14385
|
+
var TYPE_LABELS = {
|
|
14386
|
+
action: "Actions",
|
|
14387
|
+
decision: "Decisions",
|
|
14388
|
+
question: "Questions",
|
|
14389
|
+
feature: "Features",
|
|
14390
|
+
epic: "Epics",
|
|
14391
|
+
sprint: "Sprints"
|
|
14392
|
+
};
|
|
14393
|
+
function evaluateHealth(projectName, metrics) {
|
|
14394
|
+
const completeness = [];
|
|
14395
|
+
for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
|
|
14396
|
+
const { total, complete, gaps } = catMetrics;
|
|
14397
|
+
const status = completenessStatus(total, complete);
|
|
14398
|
+
const pct = total > 0 ? Math.round(complete / total * 100) : 100;
|
|
14399
|
+
completeness.push({
|
|
14400
|
+
name: TYPE_LABELS[type] ?? type,
|
|
14401
|
+
status,
|
|
14402
|
+
summary: `${pct}% complete (${complete}/${total})`,
|
|
14403
|
+
items: gaps.map((g) => ({
|
|
14404
|
+
id: g.id,
|
|
14405
|
+
detail: `missing: ${g.missingFields.join(", ")}`
|
|
14406
|
+
}))
|
|
14407
|
+
});
|
|
14408
|
+
}
|
|
14409
|
+
const process3 = [];
|
|
14410
|
+
const staleCount = metrics.process.stale.length;
|
|
14411
|
+
const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
|
|
14412
|
+
process3.push({
|
|
14413
|
+
name: "Stale Items",
|
|
14414
|
+
status: staleStatus,
|
|
14415
|
+
summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
|
|
14416
|
+
items: metrics.process.stale.map((s) => ({
|
|
14417
|
+
id: s.id,
|
|
14418
|
+
detail: `${s.days} days since last update`
|
|
14419
|
+
}))
|
|
14420
|
+
});
|
|
14421
|
+
const agingCount = metrics.process.agingActions.length;
|
|
14422
|
+
const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
|
|
14423
|
+
process3.push({
|
|
14424
|
+
name: "Aging Actions",
|
|
14425
|
+
status: agingStatus,
|
|
14426
|
+
summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
|
|
14427
|
+
items: metrics.process.agingActions.map((a) => ({
|
|
14428
|
+
id: a.id,
|
|
14429
|
+
detail: `open for ${a.days} days`
|
|
14430
|
+
}))
|
|
14431
|
+
});
|
|
14432
|
+
const dv = metrics.process.decisionVelocity;
|
|
14433
|
+
let dvStatus;
|
|
14434
|
+
if (dv.count === 0) {
|
|
14435
|
+
dvStatus = "green";
|
|
14436
|
+
} else if (dv.avgDays <= 7) {
|
|
14437
|
+
dvStatus = "green";
|
|
14438
|
+
} else if (dv.avgDays <= 21) {
|
|
14439
|
+
dvStatus = "amber";
|
|
14440
|
+
} else {
|
|
14441
|
+
dvStatus = "red";
|
|
14442
|
+
}
|
|
14443
|
+
process3.push({
|
|
14444
|
+
name: "Decision Velocity",
|
|
14445
|
+
status: dvStatus,
|
|
14446
|
+
summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
|
|
14447
|
+
items: []
|
|
14448
|
+
});
|
|
14449
|
+
const qr = metrics.process.questionResolution;
|
|
14450
|
+
let qrStatus;
|
|
14451
|
+
if (qr.count === 0) {
|
|
14452
|
+
qrStatus = "green";
|
|
14453
|
+
} else if (qr.avgDays <= 7) {
|
|
14454
|
+
qrStatus = "green";
|
|
14455
|
+
} else if (qr.avgDays <= 14) {
|
|
14456
|
+
qrStatus = "amber";
|
|
14457
|
+
} else {
|
|
14458
|
+
qrStatus = "red";
|
|
14459
|
+
}
|
|
14460
|
+
process3.push({
|
|
14461
|
+
name: "Question Resolution",
|
|
14462
|
+
status: qrStatus,
|
|
14463
|
+
summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
|
|
14464
|
+
items: []
|
|
14465
|
+
});
|
|
14466
|
+
const allStatuses = [
|
|
14467
|
+
...completeness.map((c) => c.status),
|
|
14468
|
+
...process3.map((p) => p.status)
|
|
14469
|
+
];
|
|
14470
|
+
const overall = worstStatus2(allStatuses);
|
|
14471
|
+
return {
|
|
14472
|
+
projectName,
|
|
14473
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
14474
|
+
overall,
|
|
14475
|
+
completeness,
|
|
14476
|
+
process: process3
|
|
14477
|
+
};
|
|
14478
|
+
}
|
|
14479
|
+
|
|
14225
14480
|
// src/plugins/builtin/tools/reports.ts
|
|
14226
14481
|
function createReportTools(store) {
|
|
14227
14482
|
return [
|
|
@@ -14241,7 +14496,8 @@ function createReportTools(store) {
|
|
|
14241
14496
|
id: d.frontmatter.id,
|
|
14242
14497
|
title: d.frontmatter.title,
|
|
14243
14498
|
owner: d.frontmatter.owner,
|
|
14244
|
-
priority: d.frontmatter.priority
|
|
14499
|
+
priority: d.frontmatter.priority,
|
|
14500
|
+
dueDate: d.frontmatter.dueDate
|
|
14245
14501
|
})),
|
|
14246
14502
|
completedActions: completedActions.map((d) => ({
|
|
14247
14503
|
id: d.frontmatter.id,
|
|
@@ -14260,7 +14516,7 @@ function createReportTools(store) {
|
|
|
14260
14516
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
14261
14517
|
};
|
|
14262
14518
|
},
|
|
14263
|
-
{ annotations: {
|
|
14519
|
+
{ annotations: { readOnlyHint: true } }
|
|
14264
14520
|
),
|
|
14265
14521
|
tool2(
|
|
14266
14522
|
"generate_risk_register",
|
|
@@ -14269,7 +14525,7 @@ function createReportTools(store) {
|
|
|
14269
14525
|
async () => {
|
|
14270
14526
|
const allDocs = store.list();
|
|
14271
14527
|
const taggedRisks = allDocs.filter(
|
|
14272
|
-
(d) => d.frontmatter.tags?.includes("risk")
|
|
14528
|
+
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
14273
14529
|
);
|
|
14274
14530
|
const highPriorityActions = store.list({ type: "action", status: "open" }).filter((d) => d.frontmatter.priority === "high");
|
|
14275
14531
|
const unresolvedQuestions = store.list({ type: "question", status: "open" });
|
|
@@ -14304,7 +14560,7 @@ function createReportTools(store) {
|
|
|
14304
14560
|
content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
|
|
14305
14561
|
};
|
|
14306
14562
|
},
|
|
14307
|
-
{ annotations: {
|
|
14563
|
+
{ annotations: { readOnlyHint: true } }
|
|
14308
14564
|
),
|
|
14309
14565
|
tool2(
|
|
14310
14566
|
"generate_gar_report",
|
|
@@ -14317,7 +14573,7 @@ function createReportTools(store) {
|
|
|
14317
14573
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
14318
14574
|
};
|
|
14319
14575
|
},
|
|
14320
|
-
{ annotations: {
|
|
14576
|
+
{ annotations: { readOnlyHint: true } }
|
|
14321
14577
|
),
|
|
14322
14578
|
tool2(
|
|
14323
14579
|
"generate_epic_progress",
|
|
@@ -14402,7 +14658,7 @@ function createReportTools(store) {
|
|
|
14402
14658
|
]
|
|
14403
14659
|
};
|
|
14404
14660
|
},
|
|
14405
|
-
{ annotations: {
|
|
14661
|
+
{ annotations: { readOnlyHint: true } }
|
|
14406
14662
|
),
|
|
14407
14663
|
tool2(
|
|
14408
14664
|
"generate_sprint_progress",
|
|
@@ -14447,7 +14703,8 @@ function createReportTools(store) {
|
|
|
14447
14703
|
id: d.frontmatter.id,
|
|
14448
14704
|
title: d.frontmatter.title,
|
|
14449
14705
|
type: d.frontmatter.type,
|
|
14450
|
-
status: d.frontmatter.status
|
|
14706
|
+
status: d.frontmatter.status,
|
|
14707
|
+
dueDate: d.frontmatter.dueDate
|
|
14451
14708
|
}))
|
|
14452
14709
|
}
|
|
14453
14710
|
};
|
|
@@ -14456,7 +14713,7 @@ function createReportTools(store) {
|
|
|
14456
14713
|
content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
|
|
14457
14714
|
};
|
|
14458
14715
|
},
|
|
14459
|
-
{ annotations: {
|
|
14716
|
+
{ annotations: { readOnlyHint: true } }
|
|
14460
14717
|
),
|
|
14461
14718
|
tool2(
|
|
14462
14719
|
"generate_feature_progress",
|
|
@@ -14496,7 +14753,20 @@ function createReportTools(store) {
|
|
|
14496
14753
|
content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
|
|
14497
14754
|
};
|
|
14498
14755
|
},
|
|
14499
|
-
{ annotations: {
|
|
14756
|
+
{ annotations: { readOnlyHint: true } }
|
|
14757
|
+
),
|
|
14758
|
+
tool2(
|
|
14759
|
+
"generate_health_report",
|
|
14760
|
+
"Generate a governance health check report covering artifact completeness and process health metrics",
|
|
14761
|
+
{},
|
|
14762
|
+
async () => {
|
|
14763
|
+
const metrics = collectHealthMetrics(store);
|
|
14764
|
+
const report = evaluateHealth("project", metrics);
|
|
14765
|
+
return {
|
|
14766
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
14767
|
+
};
|
|
14768
|
+
},
|
|
14769
|
+
{ annotations: { readOnlyHint: true } }
|
|
14500
14770
|
),
|
|
14501
14771
|
tool2(
|
|
14502
14772
|
"save_report",
|
|
@@ -14558,7 +14828,7 @@ function createFeatureTools(store) {
|
|
|
14558
14828
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14559
14829
|
};
|
|
14560
14830
|
},
|
|
14561
|
-
{ annotations: {
|
|
14831
|
+
{ annotations: { readOnlyHint: true } }
|
|
14562
14832
|
),
|
|
14563
14833
|
tool3(
|
|
14564
14834
|
"get_feature",
|
|
@@ -14585,7 +14855,7 @@ function createFeatureTools(store) {
|
|
|
14585
14855
|
]
|
|
14586
14856
|
};
|
|
14587
14857
|
},
|
|
14588
|
-
{ annotations: {
|
|
14858
|
+
{ annotations: { readOnlyHint: true } }
|
|
14589
14859
|
),
|
|
14590
14860
|
tool3(
|
|
14591
14861
|
"create_feature",
|
|
@@ -14626,7 +14896,8 @@ function createFeatureTools(store) {
|
|
|
14626
14896
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
|
|
14627
14897
|
content: external_exports.string().optional().describe("New content"),
|
|
14628
14898
|
owner: external_exports.string().optional().describe("New owner"),
|
|
14629
|
-
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority")
|
|
14899
|
+
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
14900
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14630
14901
|
},
|
|
14631
14902
|
async (args) => {
|
|
14632
14903
|
const { id, content, ...updates } = args;
|
|
@@ -14676,7 +14947,7 @@ function createEpicTools(store) {
|
|
|
14676
14947
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14677
14948
|
};
|
|
14678
14949
|
},
|
|
14679
|
-
{ annotations: {
|
|
14950
|
+
{ annotations: { readOnlyHint: true } }
|
|
14680
14951
|
),
|
|
14681
14952
|
tool4(
|
|
14682
14953
|
"get_epic",
|
|
@@ -14703,7 +14974,7 @@ function createEpicTools(store) {
|
|
|
14703
14974
|
]
|
|
14704
14975
|
};
|
|
14705
14976
|
},
|
|
14706
|
-
{ annotations: {
|
|
14977
|
+
{ annotations: { readOnlyHint: true } }
|
|
14707
14978
|
),
|
|
14708
14979
|
tool4(
|
|
14709
14980
|
"create_epic",
|
|
@@ -14783,7 +15054,8 @@ function createEpicTools(store) {
|
|
|
14783
15054
|
content: external_exports.string().optional().describe("New content"),
|
|
14784
15055
|
owner: external_exports.string().optional().describe("New owner"),
|
|
14785
15056
|
targetDate: external_exports.string().optional().describe("New target date"),
|
|
14786
|
-
estimatedEffort: external_exports.string().optional().describe("New estimated effort")
|
|
15057
|
+
estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
|
|
15058
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14787
15059
|
},
|
|
14788
15060
|
async (args) => {
|
|
14789
15061
|
const { id, content, ...updates } = args;
|
|
@@ -14835,7 +15107,7 @@ function createContributionTools(store) {
|
|
|
14835
15107
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14836
15108
|
};
|
|
14837
15109
|
},
|
|
14838
|
-
{ annotations: {
|
|
15110
|
+
{ annotations: { readOnlyHint: true } }
|
|
14839
15111
|
),
|
|
14840
15112
|
tool5(
|
|
14841
15113
|
"get_contribution",
|
|
@@ -14862,7 +15134,7 @@ function createContributionTools(store) {
|
|
|
14862
15134
|
]
|
|
14863
15135
|
};
|
|
14864
15136
|
},
|
|
14865
|
-
{ annotations: {
|
|
15137
|
+
{ annotations: { readOnlyHint: true } }
|
|
14866
15138
|
),
|
|
14867
15139
|
tool5(
|
|
14868
15140
|
"create_contribution",
|
|
@@ -14947,7 +15219,7 @@ function createSprintTools(store) {
|
|
|
14947
15219
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14948
15220
|
};
|
|
14949
15221
|
},
|
|
14950
|
-
{ annotations: {
|
|
15222
|
+
{ annotations: { readOnlyHint: true } }
|
|
14951
15223
|
),
|
|
14952
15224
|
tool6(
|
|
14953
15225
|
"get_sprint",
|
|
@@ -14974,7 +15246,7 @@ function createSprintTools(store) {
|
|
|
14974
15246
|
]
|
|
14975
15247
|
};
|
|
14976
15248
|
},
|
|
14977
|
-
{ annotations: {
|
|
15249
|
+
{ annotations: { readOnlyHint: true } }
|
|
14978
15250
|
),
|
|
14979
15251
|
tool6(
|
|
14980
15252
|
"create_sprint",
|
|
@@ -15271,7 +15543,7 @@ function createSprintPlanningTools(store) {
|
|
|
15271
15543
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
15272
15544
|
};
|
|
15273
15545
|
},
|
|
15274
|
-
{ annotations: {
|
|
15546
|
+
{ annotations: { readOnlyHint: true } }
|
|
15275
15547
|
)
|
|
15276
15548
|
];
|
|
15277
15549
|
}
|
|
@@ -15325,6 +15597,7 @@ var genericAgilePlugin = {
|
|
|
15325
15597
|
- Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
|
|
15326
15598
|
- Use priority levels (critical, high, medium, low) to communicate business value.
|
|
15327
15599
|
- Tag features for categorization and cross-referencing.
|
|
15600
|
+
- Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
|
|
15328
15601
|
|
|
15329
15602
|
**Contribution Tools:**
|
|
15330
15603
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -15355,6 +15628,7 @@ var genericAgilePlugin = {
|
|
|
15355
15628
|
- Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
|
|
15356
15629
|
- Collaborate with the Delivery Manager on target dates and effort estimates.
|
|
15357
15630
|
- Each epic should have a clear scope and definition of done.
|
|
15631
|
+
- Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
|
|
15358
15632
|
|
|
15359
15633
|
**Contribution Tools:**
|
|
15360
15634
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -15414,6 +15688,11 @@ var genericAgilePlugin = {
|
|
|
15414
15688
|
- **generate_sprint_progress**: Progress report for a specific sprint or all sprints \u2014 shows linked epics with statuses, work items tagged \`sprint:SP-xxx\` grouped by status, and done/total completion %.
|
|
15415
15689
|
- Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
|
|
15416
15690
|
|
|
15691
|
+
**Date Enforcement:**
|
|
15692
|
+
- Always set \`dueDate\` when creating or updating actions. Use the \`sprints\` parameter to assign actions to sprints \u2014 the tool translates this into \`sprint:SP-xxx\` tags automatically.
|
|
15693
|
+
- When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
|
|
15694
|
+
- Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
|
|
15695
|
+
|
|
15417
15696
|
**Sprint Workflow:**
|
|
15418
15697
|
- Create sprints with clear goals and date boundaries.
|
|
15419
15698
|
- Assign epics to sprints via linkedEpics.
|
|
@@ -15433,7 +15712,7 @@ var genericAgilePlugin = {
|
|
|
15433
15712
|
**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).
|
|
15434
15713
|
**Meetings**: Meeting records with attendees, agendas, and notes.
|
|
15435
15714
|
|
|
15436
|
-
**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.
|
|
15715
|
+
**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.
|
|
15437
15716
|
|
|
15438
15717
|
- **list_meetings** / **get_meeting**: Browse and read meeting records.
|
|
15439
15718
|
- **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.
|
|
@@ -15479,7 +15758,7 @@ function createUseCaseTools(store) {
|
|
|
15479
15758
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15480
15759
|
};
|
|
15481
15760
|
},
|
|
15482
|
-
{ annotations: {
|
|
15761
|
+
{ annotations: { readOnlyHint: true } }
|
|
15483
15762
|
),
|
|
15484
15763
|
tool8(
|
|
15485
15764
|
"get_use_case",
|
|
@@ -15506,7 +15785,7 @@ function createUseCaseTools(store) {
|
|
|
15506
15785
|
]
|
|
15507
15786
|
};
|
|
15508
15787
|
},
|
|
15509
|
-
{ annotations: {
|
|
15788
|
+
{ annotations: { readOnlyHint: true } }
|
|
15510
15789
|
),
|
|
15511
15790
|
tool8(
|
|
15512
15791
|
"create_use_case",
|
|
@@ -15604,7 +15883,7 @@ function createTechAssessmentTools(store) {
|
|
|
15604
15883
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15605
15884
|
};
|
|
15606
15885
|
},
|
|
15607
|
-
{ annotations: {
|
|
15886
|
+
{ annotations: { readOnlyHint: true } }
|
|
15608
15887
|
),
|
|
15609
15888
|
tool9(
|
|
15610
15889
|
"get_tech_assessment",
|
|
@@ -15631,7 +15910,7 @@ function createTechAssessmentTools(store) {
|
|
|
15631
15910
|
]
|
|
15632
15911
|
};
|
|
15633
15912
|
},
|
|
15634
|
-
{ annotations: {
|
|
15913
|
+
{ annotations: { readOnlyHint: true } }
|
|
15635
15914
|
),
|
|
15636
15915
|
tool9(
|
|
15637
15916
|
"create_tech_assessment",
|
|
@@ -15765,7 +16044,7 @@ function createExtensionDesignTools(store) {
|
|
|
15765
16044
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15766
16045
|
};
|
|
15767
16046
|
},
|
|
15768
|
-
{ annotations: {
|
|
16047
|
+
{ annotations: { readOnlyHint: true } }
|
|
15769
16048
|
),
|
|
15770
16049
|
tool10(
|
|
15771
16050
|
"get_extension_design",
|
|
@@ -15792,7 +16071,7 @@ function createExtensionDesignTools(store) {
|
|
|
15792
16071
|
]
|
|
15793
16072
|
};
|
|
15794
16073
|
},
|
|
15795
|
-
{ annotations: {
|
|
16074
|
+
{ annotations: { readOnlyHint: true } }
|
|
15796
16075
|
),
|
|
15797
16076
|
tool10(
|
|
15798
16077
|
"create_extension_design",
|
|
@@ -15944,7 +16223,7 @@ function createAemReportTools(store) {
|
|
|
15944
16223
|
]
|
|
15945
16224
|
};
|
|
15946
16225
|
},
|
|
15947
|
-
{ annotations: {
|
|
16226
|
+
{ annotations: { readOnlyHint: true } }
|
|
15948
16227
|
),
|
|
15949
16228
|
tool11(
|
|
15950
16229
|
"generate_tech_readiness",
|
|
@@ -15996,7 +16275,7 @@ function createAemReportTools(store) {
|
|
|
15996
16275
|
]
|
|
15997
16276
|
};
|
|
15998
16277
|
},
|
|
15999
|
-
{ annotations: {
|
|
16278
|
+
{ annotations: { readOnlyHint: true } }
|
|
16000
16279
|
),
|
|
16001
16280
|
tool11(
|
|
16002
16281
|
"generate_phase_status",
|
|
@@ -16051,7 +16330,7 @@ function createAemReportTools(store) {
|
|
|
16051
16330
|
]
|
|
16052
16331
|
};
|
|
16053
16332
|
},
|
|
16054
|
-
{ annotations: {
|
|
16333
|
+
{ annotations: { readOnlyHint: true } }
|
|
16055
16334
|
)
|
|
16056
16335
|
];
|
|
16057
16336
|
}
|
|
@@ -16083,7 +16362,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
16083
16362
|
]
|
|
16084
16363
|
};
|
|
16085
16364
|
},
|
|
16086
|
-
{ annotations: {
|
|
16365
|
+
{ annotations: { readOnlyHint: true } }
|
|
16087
16366
|
),
|
|
16088
16367
|
tool12(
|
|
16089
16368
|
"advance_phase",
|
|
@@ -16348,6 +16627,56 @@ function getPluginPromptFragment(plugin, personaId) {
|
|
|
16348
16627
|
return plugin.promptFragments[personaId] ?? plugin.promptFragments["*"];
|
|
16349
16628
|
}
|
|
16350
16629
|
|
|
16630
|
+
// src/templates/claude-md.ts
|
|
16631
|
+
function getDefaultClaudeMdContent(projectName) {
|
|
16632
|
+
return `# Marvin \u2014 Project Instructions for "${projectName}"
|
|
16633
|
+
|
|
16634
|
+
You are **Marvin**, an AI-powered product development assistant.
|
|
16635
|
+
You operate as one of three personas \u2014 stay in role and suggest switching when a question falls outside your scope.
|
|
16636
|
+
|
|
16637
|
+
## Personas
|
|
16638
|
+
|
|
16639
|
+
| Persona | Short | Focus |
|
|
16640
|
+
|---------|-------|-------|
|
|
16641
|
+
| Product Owner | po | Vision, backlog, requirements, features, acceptance criteria |
|
|
16642
|
+
| Delivery Manager | dm | Planning, risks, actions, timelines, sprints, status |
|
|
16643
|
+
| Tech Lead | tl | Architecture, trade-offs, technical decisions, code quality |
|
|
16644
|
+
|
|
16645
|
+
## Proactive Governance
|
|
16646
|
+
|
|
16647
|
+
When conversation implies a commitment, risk, or open question, **suggest creating the matching artifact**:
|
|
16648
|
+
- A decision was made \u2192 offer to create a **Decision (D-xxx)**
|
|
16649
|
+
- Someone committed to a task \u2192 offer an **Action (A-xxx)** with owner and due date
|
|
16650
|
+
- An unanswered question surfaced \u2192 offer a **Question (Q-xxx)**
|
|
16651
|
+
- A new capability is discussed \u2192 offer a **Feature (F-xxx)**
|
|
16652
|
+
- Implementation scope is agreed \u2192 offer an **Epic (E-xxx)** linked to a feature
|
|
16653
|
+
- Work is being time-boxed \u2192 offer a **Sprint (SP-xxx)**
|
|
16654
|
+
|
|
16655
|
+
## Insights
|
|
16656
|
+
|
|
16657
|
+
Proactively flag:
|
|
16658
|
+
- Overdue actions or unresolved questions
|
|
16659
|
+
- Decisions without rationale or linked features
|
|
16660
|
+
- Features without linked epics
|
|
16661
|
+
- Risks mentioned but not tracked
|
|
16662
|
+
- When a risk is resolved \u2192 remove the "risk" tag and add "risk-mitigated"
|
|
16663
|
+
|
|
16664
|
+
## Tool Usage
|
|
16665
|
+
|
|
16666
|
+
- **Search before creating** \u2014 avoid duplicate artifacts
|
|
16667
|
+
- **Reference IDs** (e.g. D-001, A-003) when discussing existing items
|
|
16668
|
+
- **Link artifacts** \u2014 epics to features, actions to decisions, etc.
|
|
16669
|
+
- Use \`search_documents\` to find related context before answering
|
|
16670
|
+
|
|
16671
|
+
## Communication Style
|
|
16672
|
+
|
|
16673
|
+
- Be concise and structured
|
|
16674
|
+
- State assumptions explicitly
|
|
16675
|
+
- Use bullet points and tables where they aid clarity
|
|
16676
|
+
- When uncertain, ask a clarifying question rather than guessing
|
|
16677
|
+
`;
|
|
16678
|
+
}
|
|
16679
|
+
|
|
16351
16680
|
// src/cli/commands/init.ts
|
|
16352
16681
|
async function initCommand() {
|
|
16353
16682
|
const cwd = process.cwd();
|
|
@@ -16404,11 +16733,17 @@ async function initCommand() {
|
|
|
16404
16733
|
YAML3.stringify(config2),
|
|
16405
16734
|
"utf-8"
|
|
16406
16735
|
);
|
|
16736
|
+
fs4.writeFileSync(
|
|
16737
|
+
path4.join(marvinDir, "CLAUDE.md"),
|
|
16738
|
+
getDefaultClaudeMdContent(projectName),
|
|
16739
|
+
"utf-8"
|
|
16740
|
+
);
|
|
16407
16741
|
console.log(chalk.green(`
|
|
16408
16742
|
Initialized Marvin project "${projectName}" in ${cwd}`));
|
|
16409
16743
|
console.log(chalk.dim(`Methodology: ${plugin?.name ?? methodology}`));
|
|
16410
16744
|
console.log(chalk.dim("\nCreated:"));
|
|
16411
16745
|
console.log(chalk.dim(" .marvin/config.yaml"));
|
|
16746
|
+
console.log(chalk.dim(" .marvin/CLAUDE.md"));
|
|
16412
16747
|
console.log(chalk.dim(" .marvin/docs/decisions/"));
|
|
16413
16748
|
console.log(chalk.dim(" .marvin/docs/actions/"));
|
|
16414
16749
|
console.log(chalk.dim(" .marvin/docs/questions/"));
|
|
@@ -16517,6 +16852,8 @@ var deliveryManager = {
|
|
|
16517
16852
|
|
|
16518
16853
|
## How You Work
|
|
16519
16854
|
- Review open actions (A-xxx) and follow up on overdue items
|
|
16855
|
+
- Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
|
|
16856
|
+
- Assign actions to sprints when sprint planning is active, using the sprints parameter
|
|
16520
16857
|
- Ensure decisions (D-xxx) are properly documented with rationale
|
|
16521
16858
|
- Track questions (Q-xxx) and ensure they get answered
|
|
16522
16859
|
- Monitor project health and flag risks early
|
|
@@ -16609,8 +16946,8 @@ function resolvePersonaId(input4) {
|
|
|
16609
16946
|
}
|
|
16610
16947
|
|
|
16611
16948
|
// src/agent/session.ts
|
|
16612
|
-
import * as
|
|
16613
|
-
import * as
|
|
16949
|
+
import * as fs10 from "fs";
|
|
16950
|
+
import * as path10 from "path";
|
|
16614
16951
|
import * as readline from "readline";
|
|
16615
16952
|
import chalk2 from "chalk";
|
|
16616
16953
|
import ora from "ora";
|
|
@@ -16619,9 +16956,24 @@ import {
|
|
|
16619
16956
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
16620
16957
|
|
|
16621
16958
|
// src/personas/prompt-builder.ts
|
|
16622
|
-
|
|
16959
|
+
import * as fs5 from "fs";
|
|
16960
|
+
import * as path5 from "path";
|
|
16961
|
+
function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment, marvinDir) {
|
|
16623
16962
|
const parts = [];
|
|
16624
16963
|
parts.push(persona.systemPrompt);
|
|
16964
|
+
if (marvinDir) {
|
|
16965
|
+
const claudeMdPath = path5.join(marvinDir, "CLAUDE.md");
|
|
16966
|
+
try {
|
|
16967
|
+
const content = fs5.readFileSync(claudeMdPath, "utf-8").trim();
|
|
16968
|
+
if (content) {
|
|
16969
|
+
parts.push(`
|
|
16970
|
+
## Project Instructions
|
|
16971
|
+
${content}
|
|
16972
|
+
`);
|
|
16973
|
+
}
|
|
16974
|
+
} catch {
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16625
16977
|
parts.push(`
|
|
16626
16978
|
## Project Context
|
|
16627
16979
|
- **Project Name:** ${projectConfig.name}
|
|
@@ -16631,7 +16983,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
|
|
|
16631
16983
|
## Available Tools
|
|
16632
16984
|
You have access to governance tools for managing project artifacts:
|
|
16633
16985
|
- **Decisions** (D-xxx): List, get, create, and update decisions
|
|
16634
|
-
- **Actions** (A-xxx): List, get, create, and update action items
|
|
16986
|
+
- **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
|
|
16635
16987
|
- **Questions** (Q-xxx): List, get, create, and update questions
|
|
16636
16988
|
- **Features** (F-xxx): List, get, create, and update feature definitions
|
|
16637
16989
|
- **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
|
|
@@ -16668,8 +17020,8 @@ ${projectConfig.personas[persona.id].extraInstructions}
|
|
|
16668
17020
|
}
|
|
16669
17021
|
|
|
16670
17022
|
// src/storage/store.ts
|
|
16671
|
-
import * as
|
|
16672
|
-
import * as
|
|
17023
|
+
import * as fs6 from "fs";
|
|
17024
|
+
import * as path6 from "path";
|
|
16673
17025
|
|
|
16674
17026
|
// src/storage/document.ts
|
|
16675
17027
|
import matter from "gray-matter";
|
|
@@ -16707,7 +17059,7 @@ var DocumentStore = class {
|
|
|
16707
17059
|
typeDirs;
|
|
16708
17060
|
idPrefixes;
|
|
16709
17061
|
constructor(marvinDir, registrations) {
|
|
16710
|
-
this.docsDir =
|
|
17062
|
+
this.docsDir = path6.join(marvinDir, "docs");
|
|
16711
17063
|
this.typeDirs = { ...CORE_TYPE_DIRS };
|
|
16712
17064
|
this.idPrefixes = { ...CORE_ID_PREFIXES };
|
|
16713
17065
|
for (const reg of registrations ?? []) {
|
|
@@ -16722,12 +17074,12 @@ var DocumentStore = class {
|
|
|
16722
17074
|
buildIndex() {
|
|
16723
17075
|
this.index.clear();
|
|
16724
17076
|
for (const type of Object.keys(this.typeDirs)) {
|
|
16725
|
-
const dir =
|
|
16726
|
-
if (!
|
|
16727
|
-
const files =
|
|
17077
|
+
const dir = path6.join(this.docsDir, this.typeDirs[type]);
|
|
17078
|
+
if (!fs6.existsSync(dir)) continue;
|
|
17079
|
+
const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
16728
17080
|
for (const file2 of files) {
|
|
16729
|
-
const filePath =
|
|
16730
|
-
const raw =
|
|
17081
|
+
const filePath = path6.join(dir, file2);
|
|
17082
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
16731
17083
|
const doc = parseDocument(raw, filePath);
|
|
16732
17084
|
if (doc.frontmatter.id) {
|
|
16733
17085
|
if (this.index.has(doc.frontmatter.id)) {
|
|
@@ -16746,12 +17098,12 @@ var DocumentStore = class {
|
|
|
16746
17098
|
for (const type of types) {
|
|
16747
17099
|
const dirName = this.typeDirs[type];
|
|
16748
17100
|
if (!dirName) continue;
|
|
16749
|
-
const dir =
|
|
16750
|
-
if (!
|
|
16751
|
-
const files =
|
|
17101
|
+
const dir = path6.join(this.docsDir, dirName);
|
|
17102
|
+
if (!fs6.existsSync(dir)) continue;
|
|
17103
|
+
const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
16752
17104
|
for (const file2 of files) {
|
|
16753
|
-
const filePath =
|
|
16754
|
-
const raw =
|
|
17105
|
+
const filePath = path6.join(dir, file2);
|
|
17106
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
16755
17107
|
const doc = parseDocument(raw, filePath);
|
|
16756
17108
|
if (query7?.status && doc.frontmatter.status !== query7.status) continue;
|
|
16757
17109
|
if (query7?.owner && doc.frontmatter.owner !== query7.owner) continue;
|
|
@@ -16764,12 +17116,12 @@ var DocumentStore = class {
|
|
|
16764
17116
|
}
|
|
16765
17117
|
get(id) {
|
|
16766
17118
|
for (const type of Object.keys(this.typeDirs)) {
|
|
16767
|
-
const dir =
|
|
16768
|
-
if (!
|
|
16769
|
-
const files =
|
|
17119
|
+
const dir = path6.join(this.docsDir, this.typeDirs[type]);
|
|
17120
|
+
if (!fs6.existsSync(dir)) continue;
|
|
17121
|
+
const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
16770
17122
|
for (const file2 of files) {
|
|
16771
|
-
const filePath =
|
|
16772
|
-
const raw =
|
|
17123
|
+
const filePath = path6.join(dir, file2);
|
|
17124
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
16773
17125
|
const doc = parseDocument(raw, filePath);
|
|
16774
17126
|
if (doc.frontmatter.id === id) return doc;
|
|
16775
17127
|
}
|
|
@@ -16783,8 +17135,8 @@ var DocumentStore = class {
|
|
|
16783
17135
|
if (!dirName) {
|
|
16784
17136
|
throw new Error(`Unknown document type: ${type}`);
|
|
16785
17137
|
}
|
|
16786
|
-
const dir =
|
|
16787
|
-
|
|
17138
|
+
const dir = path6.join(this.docsDir, dirName);
|
|
17139
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
16788
17140
|
const cleaned = Object.fromEntries(
|
|
16789
17141
|
Object.entries(frontmatter).filter(([, v]) => v !== void 0)
|
|
16790
17142
|
);
|
|
@@ -16798,13 +17150,13 @@ var DocumentStore = class {
|
|
|
16798
17150
|
...cleaned
|
|
16799
17151
|
};
|
|
16800
17152
|
const fileName = type === "meeting" ? `${cleaned.date?.slice(0, 10) ?? now.slice(0, 10)}-${slugify2(fullFrontmatter.title)}.md` : `${id}.md`;
|
|
16801
|
-
const filePath =
|
|
17153
|
+
const filePath = path6.join(dir, fileName);
|
|
16802
17154
|
const doc = {
|
|
16803
17155
|
frontmatter: fullFrontmatter,
|
|
16804
17156
|
content,
|
|
16805
17157
|
filePath
|
|
16806
17158
|
};
|
|
16807
|
-
|
|
17159
|
+
fs6.writeFileSync(filePath, serializeDocument(doc), "utf-8");
|
|
16808
17160
|
this.index.set(id, fullFrontmatter);
|
|
16809
17161
|
return doc;
|
|
16810
17162
|
}
|
|
@@ -16819,12 +17171,12 @@ var DocumentStore = class {
|
|
|
16819
17171
|
`Document ${frontmatter.id} already exists. Resolve conflicts before importing.`
|
|
16820
17172
|
);
|
|
16821
17173
|
}
|
|
16822
|
-
const dir =
|
|
16823
|
-
|
|
17174
|
+
const dir = path6.join(this.docsDir, dirName);
|
|
17175
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
16824
17176
|
const fileName = type === "meeting" ? `${frontmatter.date?.slice(0, 10) ?? frontmatter.created.slice(0, 10)}-${slugify2(frontmatter.title)}.md` : `${frontmatter.id}.md`;
|
|
16825
|
-
const filePath =
|
|
17177
|
+
const filePath = path6.join(dir, fileName);
|
|
16826
17178
|
const doc = { frontmatter, content, filePath };
|
|
16827
|
-
|
|
17179
|
+
fs6.writeFileSync(filePath, serializeDocument(doc), "utf-8");
|
|
16828
17180
|
this.index.set(frontmatter.id, frontmatter);
|
|
16829
17181
|
return doc;
|
|
16830
17182
|
}
|
|
@@ -16846,7 +17198,7 @@ var DocumentStore = class {
|
|
|
16846
17198
|
content: content ?? existing.content,
|
|
16847
17199
|
filePath: existing.filePath
|
|
16848
17200
|
};
|
|
16849
|
-
|
|
17201
|
+
fs6.writeFileSync(existing.filePath, serializeDocument(doc), "utf-8");
|
|
16850
17202
|
this.index.set(id, updatedFrontmatter);
|
|
16851
17203
|
return doc;
|
|
16852
17204
|
}
|
|
@@ -16856,14 +17208,14 @@ var DocumentStore = class {
|
|
|
16856
17208
|
throw new Error(`Unknown document type: ${type}`);
|
|
16857
17209
|
}
|
|
16858
17210
|
const dirName = this.typeDirs[type];
|
|
16859
|
-
const dir =
|
|
16860
|
-
if (!
|
|
17211
|
+
const dir = path6.join(this.docsDir, dirName);
|
|
17212
|
+
if (!fs6.existsSync(dir)) return `${prefix}-001`;
|
|
16861
17213
|
const idPattern = new RegExp(`^${prefix}-(\\d+)$`);
|
|
16862
|
-
const files =
|
|
17214
|
+
const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
16863
17215
|
let maxNum = 0;
|
|
16864
17216
|
for (const file2 of files) {
|
|
16865
|
-
const filePath =
|
|
16866
|
-
const raw =
|
|
17217
|
+
const filePath = path6.join(dir, file2);
|
|
17218
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
16867
17219
|
const doc = parseDocument(raw, filePath);
|
|
16868
17220
|
const match = doc.frontmatter.id?.match(idPattern);
|
|
16869
17221
|
if (match) {
|
|
@@ -16876,12 +17228,12 @@ var DocumentStore = class {
|
|
|
16876
17228
|
counts() {
|
|
16877
17229
|
const result = {};
|
|
16878
17230
|
for (const type of Object.keys(this.typeDirs)) {
|
|
16879
|
-
const dir =
|
|
16880
|
-
if (!
|
|
17231
|
+
const dir = path6.join(this.docsDir, this.typeDirs[type]);
|
|
17232
|
+
if (!fs6.existsSync(dir)) {
|
|
16881
17233
|
result[type] = 0;
|
|
16882
17234
|
continue;
|
|
16883
17235
|
}
|
|
16884
|
-
result[type] =
|
|
17236
|
+
result[type] = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
|
|
16885
17237
|
}
|
|
16886
17238
|
return result;
|
|
16887
17239
|
}
|
|
@@ -16891,13 +17243,13 @@ function slugify2(text) {
|
|
|
16891
17243
|
}
|
|
16892
17244
|
|
|
16893
17245
|
// src/storage/session-store.ts
|
|
16894
|
-
import * as
|
|
16895
|
-
import * as
|
|
17246
|
+
import * as fs7 from "fs";
|
|
17247
|
+
import * as path7 from "path";
|
|
16896
17248
|
import * as YAML4 from "yaml";
|
|
16897
17249
|
var SessionStore = class {
|
|
16898
17250
|
filePath;
|
|
16899
17251
|
constructor(marvinDir) {
|
|
16900
|
-
this.filePath =
|
|
17252
|
+
this.filePath = path7.join(marvinDir, "sessions.yaml");
|
|
16901
17253
|
}
|
|
16902
17254
|
list() {
|
|
16903
17255
|
const entries = this.load();
|
|
@@ -16938,9 +17290,9 @@ var SessionStore = class {
|
|
|
16938
17290
|
this.write(entries);
|
|
16939
17291
|
}
|
|
16940
17292
|
load() {
|
|
16941
|
-
if (!
|
|
17293
|
+
if (!fs7.existsSync(this.filePath)) return [];
|
|
16942
17294
|
try {
|
|
16943
|
-
const raw =
|
|
17295
|
+
const raw = fs7.readFileSync(this.filePath, "utf-8");
|
|
16944
17296
|
const parsed = YAML4.parse(raw);
|
|
16945
17297
|
if (!Array.isArray(parsed)) return [];
|
|
16946
17298
|
return parsed;
|
|
@@ -16949,11 +17301,11 @@ var SessionStore = class {
|
|
|
16949
17301
|
}
|
|
16950
17302
|
}
|
|
16951
17303
|
write(entries) {
|
|
16952
|
-
const dir =
|
|
16953
|
-
if (!
|
|
16954
|
-
|
|
17304
|
+
const dir = path7.dirname(this.filePath);
|
|
17305
|
+
if (!fs7.existsSync(dir)) {
|
|
17306
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
16955
17307
|
}
|
|
16956
|
-
|
|
17308
|
+
fs7.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
|
|
16957
17309
|
}
|
|
16958
17310
|
};
|
|
16959
17311
|
|
|
@@ -16982,7 +17334,7 @@ function createDecisionTools(store) {
|
|
|
16982
17334
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16983
17335
|
};
|
|
16984
17336
|
},
|
|
16985
|
-
{ annotations: {
|
|
17337
|
+
{ annotations: { readOnlyHint: true } }
|
|
16986
17338
|
),
|
|
16987
17339
|
tool13(
|
|
16988
17340
|
"get_decision",
|
|
@@ -17009,7 +17361,7 @@ function createDecisionTools(store) {
|
|
|
17009
17361
|
]
|
|
17010
17362
|
};
|
|
17011
17363
|
},
|
|
17012
|
-
{ annotations: {
|
|
17364
|
+
{ annotations: { readOnlyHint: true } }
|
|
17013
17365
|
),
|
|
17014
17366
|
tool13(
|
|
17015
17367
|
"create_decision",
|
|
@@ -17050,7 +17402,8 @@ function createDecisionTools(store) {
|
|
|
17050
17402
|
title: external_exports.string().optional().describe("New title"),
|
|
17051
17403
|
status: external_exports.string().optional().describe("New status"),
|
|
17052
17404
|
content: external_exports.string().optional().describe("New content"),
|
|
17053
|
-
owner: external_exports.string().optional().describe("New owner")
|
|
17405
|
+
owner: external_exports.string().optional().describe("New owner"),
|
|
17406
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
17054
17407
|
},
|
|
17055
17408
|
async (args) => {
|
|
17056
17409
|
const { id, content, ...updates } = args;
|
|
@@ -17070,6 +17423,19 @@ function createDecisionTools(store) {
|
|
|
17070
17423
|
|
|
17071
17424
|
// src/agent/tools/actions.ts
|
|
17072
17425
|
import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
|
|
17426
|
+
function findMatchingSprints(store, dueDate) {
|
|
17427
|
+
const sprints = store.list({ type: "sprint" });
|
|
17428
|
+
return sprints.filter((s) => {
|
|
17429
|
+
const start = s.frontmatter.startDate;
|
|
17430
|
+
const end = s.frontmatter.endDate;
|
|
17431
|
+
return start && end && dueDate >= start && dueDate <= end;
|
|
17432
|
+
}).map((s) => ({
|
|
17433
|
+
id: s.frontmatter.id,
|
|
17434
|
+
title: s.frontmatter.title,
|
|
17435
|
+
startDate: s.frontmatter.startDate,
|
|
17436
|
+
endDate: s.frontmatter.endDate
|
|
17437
|
+
}));
|
|
17438
|
+
}
|
|
17073
17439
|
function createActionTools(store) {
|
|
17074
17440
|
return [
|
|
17075
17441
|
tool14(
|
|
@@ -17085,19 +17451,24 @@ function createActionTools(store) {
|
|
|
17085
17451
|
status: args.status,
|
|
17086
17452
|
owner: args.owner
|
|
17087
17453
|
});
|
|
17088
|
-
const summary = docs.map((d) =>
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17454
|
+
const summary = docs.map((d) => {
|
|
17455
|
+
const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
|
|
17456
|
+
return {
|
|
17457
|
+
id: d.frontmatter.id,
|
|
17458
|
+
title: d.frontmatter.title,
|
|
17459
|
+
status: d.frontmatter.status,
|
|
17460
|
+
owner: d.frontmatter.owner,
|
|
17461
|
+
priority: d.frontmatter.priority,
|
|
17462
|
+
dueDate: d.frontmatter.dueDate,
|
|
17463
|
+
sprints: sprintIds.length > 0 ? sprintIds : void 0,
|
|
17464
|
+
created: d.frontmatter.created
|
|
17465
|
+
};
|
|
17466
|
+
});
|
|
17096
17467
|
return {
|
|
17097
17468
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17098
17469
|
};
|
|
17099
17470
|
},
|
|
17100
|
-
{ annotations: {
|
|
17471
|
+
{ annotations: { readOnlyHint: true } }
|
|
17101
17472
|
),
|
|
17102
17473
|
tool14(
|
|
17103
17474
|
"get_action",
|
|
@@ -17124,7 +17495,7 @@ function createActionTools(store) {
|
|
|
17124
17495
|
]
|
|
17125
17496
|
};
|
|
17126
17497
|
},
|
|
17127
|
-
{ annotations: {
|
|
17498
|
+
{ annotations: { readOnlyHint: true } }
|
|
17128
17499
|
),
|
|
17129
17500
|
tool14(
|
|
17130
17501
|
"create_action",
|
|
@@ -17135,9 +17506,18 @@ function createActionTools(store) {
|
|
|
17135
17506
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
17136
17507
|
owner: external_exports.string().optional().describe("Person responsible"),
|
|
17137
17508
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
17138
|
-
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
17509
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
17510
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
17511
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
|
|
17139
17512
|
},
|
|
17140
17513
|
async (args) => {
|
|
17514
|
+
const tags = [...args.tags ?? []];
|
|
17515
|
+
if (args.sprints) {
|
|
17516
|
+
for (const sprintId of args.sprints) {
|
|
17517
|
+
const tag = `sprint:${sprintId}`;
|
|
17518
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
17519
|
+
}
|
|
17520
|
+
}
|
|
17141
17521
|
const doc = store.create(
|
|
17142
17522
|
"action",
|
|
17143
17523
|
{
|
|
@@ -17145,17 +17525,21 @@ function createActionTools(store) {
|
|
|
17145
17525
|
status: args.status,
|
|
17146
17526
|
owner: args.owner,
|
|
17147
17527
|
priority: args.priority,
|
|
17148
|
-
tags:
|
|
17528
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
17529
|
+
dueDate: args.dueDate
|
|
17149
17530
|
},
|
|
17150
17531
|
args.content
|
|
17151
17532
|
);
|
|
17533
|
+
const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
|
|
17534
|
+
if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
|
|
17535
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
17536
|
+
if (matching.length > 0) {
|
|
17537
|
+
const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
|
|
17538
|
+
parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
|
|
17539
|
+
}
|
|
17540
|
+
}
|
|
17152
17541
|
return {
|
|
17153
|
-
content: [
|
|
17154
|
-
{
|
|
17155
|
-
type: "text",
|
|
17156
|
-
text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
|
|
17157
|
-
}
|
|
17158
|
-
]
|
|
17542
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
17159
17543
|
};
|
|
17160
17544
|
}
|
|
17161
17545
|
),
|
|
@@ -17168,10 +17552,35 @@ function createActionTools(store) {
|
|
|
17168
17552
|
status: external_exports.string().optional().describe("New status"),
|
|
17169
17553
|
content: external_exports.string().optional().describe("New content"),
|
|
17170
17554
|
owner: external_exports.string().optional().describe("New owner"),
|
|
17171
|
-
priority: external_exports.string().optional().describe("New priority")
|
|
17555
|
+
priority: external_exports.string().optional().describe("New priority"),
|
|
17556
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
17557
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
17558
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
|
|
17172
17559
|
},
|
|
17173
17560
|
async (args) => {
|
|
17174
|
-
const { id, content, ...updates } = args;
|
|
17561
|
+
const { id, content, sprints, tags, ...updates } = args;
|
|
17562
|
+
if (tags !== void 0) {
|
|
17563
|
+
const merged = [...tags];
|
|
17564
|
+
if (sprints) {
|
|
17565
|
+
for (const s of sprints) {
|
|
17566
|
+
const tag = `sprint:${s}`;
|
|
17567
|
+
if (!merged.includes(tag)) merged.push(tag);
|
|
17568
|
+
}
|
|
17569
|
+
}
|
|
17570
|
+
updates.tags = merged;
|
|
17571
|
+
} else if (sprints !== void 0) {
|
|
17572
|
+
const existing = store.get(id);
|
|
17573
|
+
if (!existing) {
|
|
17574
|
+
return {
|
|
17575
|
+
content: [{ type: "text", text: `Action ${id} not found` }],
|
|
17576
|
+
isError: true
|
|
17577
|
+
};
|
|
17578
|
+
}
|
|
17579
|
+
const existingTags = existing.frontmatter.tags ?? [];
|
|
17580
|
+
const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
17581
|
+
const newSprintTags = sprints.map((s) => `sprint:${s}`);
|
|
17582
|
+
updates.tags = [...nonSprintTags, ...newSprintTags];
|
|
17583
|
+
}
|
|
17175
17584
|
const doc = store.update(id, updates, content);
|
|
17176
17585
|
return {
|
|
17177
17586
|
content: [
|
|
@@ -17182,6 +17591,35 @@ function createActionTools(store) {
|
|
|
17182
17591
|
]
|
|
17183
17592
|
};
|
|
17184
17593
|
}
|
|
17594
|
+
),
|
|
17595
|
+
tool14(
|
|
17596
|
+
"suggest_sprints_for_action",
|
|
17597
|
+
"Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
|
|
17598
|
+
{
|
|
17599
|
+
dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
|
|
17600
|
+
},
|
|
17601
|
+
async (args) => {
|
|
17602
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
17603
|
+
if (matching.length === 0) {
|
|
17604
|
+
return {
|
|
17605
|
+
content: [
|
|
17606
|
+
{
|
|
17607
|
+
type: "text",
|
|
17608
|
+
text: `No sprints found containing dueDate ${args.dueDate}.`
|
|
17609
|
+
}
|
|
17610
|
+
]
|
|
17611
|
+
};
|
|
17612
|
+
}
|
|
17613
|
+
return {
|
|
17614
|
+
content: [
|
|
17615
|
+
{
|
|
17616
|
+
type: "text",
|
|
17617
|
+
text: JSON.stringify(matching, null, 2)
|
|
17618
|
+
}
|
|
17619
|
+
]
|
|
17620
|
+
};
|
|
17621
|
+
},
|
|
17622
|
+
{ annotations: { readOnlyHint: true } }
|
|
17185
17623
|
)
|
|
17186
17624
|
];
|
|
17187
17625
|
}
|
|
@@ -17209,7 +17647,7 @@ function createQuestionTools(store) {
|
|
|
17209
17647
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17210
17648
|
};
|
|
17211
17649
|
},
|
|
17212
|
-
{ annotations: {
|
|
17650
|
+
{ annotations: { readOnlyHint: true } }
|
|
17213
17651
|
),
|
|
17214
17652
|
tool15(
|
|
17215
17653
|
"get_question",
|
|
@@ -17236,7 +17674,7 @@ function createQuestionTools(store) {
|
|
|
17236
17674
|
]
|
|
17237
17675
|
};
|
|
17238
17676
|
},
|
|
17239
|
-
{ annotations: {
|
|
17677
|
+
{ annotations: { readOnlyHint: true } }
|
|
17240
17678
|
),
|
|
17241
17679
|
tool15(
|
|
17242
17680
|
"create_question",
|
|
@@ -17277,7 +17715,8 @@ function createQuestionTools(store) {
|
|
|
17277
17715
|
title: external_exports.string().optional().describe("New title"),
|
|
17278
17716
|
status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
|
|
17279
17717
|
content: external_exports.string().optional().describe("Updated content / answer"),
|
|
17280
|
-
owner: external_exports.string().optional().describe("New owner")
|
|
17718
|
+
owner: external_exports.string().optional().describe("New owner"),
|
|
17719
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
17281
17720
|
},
|
|
17282
17721
|
async (args) => {
|
|
17283
17722
|
const { id, content, ...updates } = args;
|
|
@@ -17329,7 +17768,7 @@ function createDocumentTools(store) {
|
|
|
17329
17768
|
]
|
|
17330
17769
|
};
|
|
17331
17770
|
},
|
|
17332
|
-
{ annotations: {
|
|
17771
|
+
{ annotations: { readOnlyHint: true } }
|
|
17333
17772
|
),
|
|
17334
17773
|
tool16(
|
|
17335
17774
|
"read_document",
|
|
@@ -17356,7 +17795,7 @@ function createDocumentTools(store) {
|
|
|
17356
17795
|
]
|
|
17357
17796
|
};
|
|
17358
17797
|
},
|
|
17359
|
-
{ annotations: {
|
|
17798
|
+
{ annotations: { readOnlyHint: true } }
|
|
17360
17799
|
),
|
|
17361
17800
|
tool16(
|
|
17362
17801
|
"project_summary",
|
|
@@ -17384,7 +17823,7 @@ function createDocumentTools(store) {
|
|
|
17384
17823
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17385
17824
|
};
|
|
17386
17825
|
},
|
|
17387
|
-
{ annotations: {
|
|
17826
|
+
{ annotations: { readOnlyHint: true } }
|
|
17388
17827
|
)
|
|
17389
17828
|
];
|
|
17390
17829
|
}
|
|
@@ -17421,7 +17860,7 @@ function createSourceTools(manifest) {
|
|
|
17421
17860
|
]
|
|
17422
17861
|
};
|
|
17423
17862
|
},
|
|
17424
|
-
{ annotations: {
|
|
17863
|
+
{ annotations: { readOnlyHint: true } }
|
|
17425
17864
|
),
|
|
17426
17865
|
tool17(
|
|
17427
17866
|
"get_source_info",
|
|
@@ -17455,7 +17894,7 @@ function createSourceTools(manifest) {
|
|
|
17455
17894
|
]
|
|
17456
17895
|
};
|
|
17457
17896
|
},
|
|
17458
|
-
{ annotations: {
|
|
17897
|
+
{ annotations: { readOnlyHint: true } }
|
|
17459
17898
|
)
|
|
17460
17899
|
];
|
|
17461
17900
|
}
|
|
@@ -17486,7 +17925,7 @@ function createSessionTools(store) {
|
|
|
17486
17925
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17487
17926
|
};
|
|
17488
17927
|
},
|
|
17489
|
-
{ annotations: {
|
|
17928
|
+
{ annotations: { readOnlyHint: true } }
|
|
17490
17929
|
),
|
|
17491
17930
|
tool18(
|
|
17492
17931
|
"get_session",
|
|
@@ -17504,7 +17943,7 @@ function createSessionTools(store) {
|
|
|
17504
17943
|
content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
|
|
17505
17944
|
};
|
|
17506
17945
|
},
|
|
17507
|
-
{ annotations: {
|
|
17946
|
+
{ annotations: { readOnlyHint: true } }
|
|
17508
17947
|
)
|
|
17509
17948
|
];
|
|
17510
17949
|
}
|
|
@@ -17584,6 +18023,46 @@ function getBoardData(store, type) {
|
|
|
17584
18023
|
}));
|
|
17585
18024
|
return { columns, type, types };
|
|
17586
18025
|
}
|
|
18026
|
+
function getDiagramData(store) {
|
|
18027
|
+
const allDocs = store.list();
|
|
18028
|
+
const sprints = [];
|
|
18029
|
+
const epics = [];
|
|
18030
|
+
const features = [];
|
|
18031
|
+
const statusCounts = {};
|
|
18032
|
+
for (const doc of allDocs) {
|
|
18033
|
+
const fm = doc.frontmatter;
|
|
18034
|
+
const status = fm.status.toLowerCase();
|
|
18035
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
18036
|
+
switch (fm.type) {
|
|
18037
|
+
case "sprint":
|
|
18038
|
+
sprints.push({
|
|
18039
|
+
id: fm.id,
|
|
18040
|
+
title: fm.title,
|
|
18041
|
+
status: fm.status,
|
|
18042
|
+
startDate: fm.startDate,
|
|
18043
|
+
endDate: fm.endDate,
|
|
18044
|
+
linkedEpics: fm.linkedEpics ?? []
|
|
18045
|
+
});
|
|
18046
|
+
break;
|
|
18047
|
+
case "epic":
|
|
18048
|
+
epics.push({
|
|
18049
|
+
id: fm.id,
|
|
18050
|
+
title: fm.title,
|
|
18051
|
+
status: fm.status,
|
|
18052
|
+
linkedFeature: fm.linkedFeature
|
|
18053
|
+
});
|
|
18054
|
+
break;
|
|
18055
|
+
case "feature":
|
|
18056
|
+
features.push({
|
|
18057
|
+
id: fm.id,
|
|
18058
|
+
title: fm.title,
|
|
18059
|
+
status: fm.status
|
|
18060
|
+
});
|
|
18061
|
+
break;
|
|
18062
|
+
}
|
|
18063
|
+
}
|
|
18064
|
+
return { sprints, epics, features, statusCounts };
|
|
18065
|
+
}
|
|
17587
18066
|
|
|
17588
18067
|
// src/web/templates/layout.ts
|
|
17589
18068
|
function escapeHtml(str) {
|
|
@@ -17614,16 +18093,43 @@ function renderMarkdown(md) {
|
|
|
17614
18093
|
const out = [];
|
|
17615
18094
|
let inList = false;
|
|
17616
18095
|
let listTag = "ul";
|
|
17617
|
-
|
|
17618
|
-
|
|
18096
|
+
let inTable = false;
|
|
18097
|
+
let i = 0;
|
|
18098
|
+
while (i < lines.length) {
|
|
18099
|
+
const line = lines[i];
|
|
17619
18100
|
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
|
|
17620
18101
|
out.push(`</${listTag}>`);
|
|
17621
18102
|
inList = false;
|
|
17622
18103
|
}
|
|
18104
|
+
if (inTable && !/^\s*\|/.test(line)) {
|
|
18105
|
+
out.push("</tbody></table></div>");
|
|
18106
|
+
inTable = false;
|
|
18107
|
+
}
|
|
18108
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
18109
|
+
i++;
|
|
18110
|
+
out.push("<hr>");
|
|
18111
|
+
continue;
|
|
18112
|
+
}
|
|
18113
|
+
if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
18114
|
+
const headers = parseTableRow(line);
|
|
18115
|
+
out.push('<div class="table-wrap"><table><thead><tr>');
|
|
18116
|
+
out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
|
|
18117
|
+
out.push("</tr></thead><tbody>");
|
|
18118
|
+
inTable = true;
|
|
18119
|
+
i += 2;
|
|
18120
|
+
continue;
|
|
18121
|
+
}
|
|
18122
|
+
if (inTable && /^\s*\|/.test(line)) {
|
|
18123
|
+
const cells = parseTableRow(line);
|
|
18124
|
+
out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
|
|
18125
|
+
i++;
|
|
18126
|
+
continue;
|
|
18127
|
+
}
|
|
17623
18128
|
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
17624
18129
|
if (headingMatch) {
|
|
17625
18130
|
const level = headingMatch[1].length;
|
|
17626
18131
|
out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
|
|
18132
|
+
i++;
|
|
17627
18133
|
continue;
|
|
17628
18134
|
}
|
|
17629
18135
|
const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
@@ -17635,6 +18141,7 @@ function renderMarkdown(md) {
|
|
|
17635
18141
|
listTag = "ul";
|
|
17636
18142
|
}
|
|
17637
18143
|
out.push(`<li>${inline(ulMatch[1])}</li>`);
|
|
18144
|
+
i++;
|
|
17638
18145
|
continue;
|
|
17639
18146
|
}
|
|
17640
18147
|
const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
@@ -17646,6 +18153,7 @@ function renderMarkdown(md) {
|
|
|
17646
18153
|
listTag = "ol";
|
|
17647
18154
|
}
|
|
17648
18155
|
out.push(`<li>${inline(olMatch[1])}</li>`);
|
|
18156
|
+
i++;
|
|
17649
18157
|
continue;
|
|
17650
18158
|
}
|
|
17651
18159
|
if (line.trim() === "") {
|
|
@@ -17653,13 +18161,19 @@ function renderMarkdown(md) {
|
|
|
17653
18161
|
out.push(`</${listTag}>`);
|
|
17654
18162
|
inList = false;
|
|
17655
18163
|
}
|
|
18164
|
+
i++;
|
|
17656
18165
|
continue;
|
|
17657
18166
|
}
|
|
17658
18167
|
out.push(`<p>${inline(line)}</p>`);
|
|
18168
|
+
i++;
|
|
17659
18169
|
}
|
|
17660
18170
|
if (inList) out.push(`</${listTag}>`);
|
|
18171
|
+
if (inTable) out.push("</tbody></table></div>");
|
|
17661
18172
|
return out.join("\n");
|
|
17662
18173
|
}
|
|
18174
|
+
function parseTableRow(line) {
|
|
18175
|
+
return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
18176
|
+
}
|
|
17663
18177
|
function inline(text) {
|
|
17664
18178
|
let s = escapeHtml(text);
|
|
17665
18179
|
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
@@ -17673,7 +18187,8 @@ function layout(opts, body) {
|
|
|
17673
18187
|
const topItems = [
|
|
17674
18188
|
{ href: "/", label: "Overview" },
|
|
17675
18189
|
{ href: "/board", label: "Board" },
|
|
17676
|
-
{ href: "/gar", label: "GAR Report" }
|
|
18190
|
+
{ href: "/gar", label: "GAR Report" },
|
|
18191
|
+
{ href: "/health", label: "Health" }
|
|
17677
18192
|
];
|
|
17678
18193
|
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
17679
18194
|
const groupsHtml = opts.navGroups.map((group) => {
|
|
@@ -17708,9 +18223,15 @@ function layout(opts, body) {
|
|
|
17708
18223
|
</nav>
|
|
17709
18224
|
</aside>
|
|
17710
18225
|
<main class="main">
|
|
18226
|
+
<button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
|
|
18227
|
+
<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>
|
|
18228
|
+
<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>
|
|
18229
|
+
</button>
|
|
17711
18230
|
${body}
|
|
17712
18231
|
</main>
|
|
17713
18232
|
</div>
|
|
18233
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
18234
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
|
|
17714
18235
|
</body>
|
|
17715
18236
|
</html>`;
|
|
17716
18237
|
}
|
|
@@ -17825,7 +18346,33 @@ a:hover { text-decoration: underline; }
|
|
|
17825
18346
|
flex: 1;
|
|
17826
18347
|
padding: 2rem 2.5rem;
|
|
17827
18348
|
max-width: 1200px;
|
|
18349
|
+
position: relative;
|
|
18350
|
+
transition: max-width 0.2s ease;
|
|
18351
|
+
}
|
|
18352
|
+
.main.expanded {
|
|
18353
|
+
max-width: none;
|
|
18354
|
+
}
|
|
18355
|
+
.expand-toggle {
|
|
18356
|
+
position: absolute;
|
|
18357
|
+
top: 1rem;
|
|
18358
|
+
right: 1rem;
|
|
18359
|
+
background: var(--bg-card);
|
|
18360
|
+
border: 1px solid var(--border);
|
|
18361
|
+
border-radius: var(--radius);
|
|
18362
|
+
color: var(--text-dim);
|
|
18363
|
+
cursor: pointer;
|
|
18364
|
+
padding: 0.4rem;
|
|
18365
|
+
display: flex;
|
|
18366
|
+
align-items: center;
|
|
18367
|
+
justify-content: center;
|
|
18368
|
+
transition: color 0.15s, border-color 0.15s;
|
|
17828
18369
|
}
|
|
18370
|
+
.expand-toggle:hover {
|
|
18371
|
+
color: var(--text);
|
|
18372
|
+
border-color: var(--text-dim);
|
|
18373
|
+
}
|
|
18374
|
+
.main.expanded .icon-expand { display: none; }
|
|
18375
|
+
.main:not(.expanded) .icon-collapse { display: none; }
|
|
17829
18376
|
|
|
17830
18377
|
/* Page header */
|
|
17831
18378
|
.page-header {
|
|
@@ -17854,12 +18401,26 @@ a:hover { text-decoration: underline; }
|
|
|
17854
18401
|
.breadcrumb a:hover { color: var(--accent); }
|
|
17855
18402
|
.breadcrumb .sep { margin: 0 0.4rem; }
|
|
17856
18403
|
|
|
18404
|
+
/* Card groups */
|
|
18405
|
+
.card-group {
|
|
18406
|
+
margin-bottom: 1.5rem;
|
|
18407
|
+
}
|
|
18408
|
+
|
|
18409
|
+
.card-group-label {
|
|
18410
|
+
font-size: 0.7rem;
|
|
18411
|
+
text-transform: uppercase;
|
|
18412
|
+
letter-spacing: 0.08em;
|
|
18413
|
+
color: var(--text-dim);
|
|
18414
|
+
font-weight: 600;
|
|
18415
|
+
margin-bottom: 0.5rem;
|
|
18416
|
+
}
|
|
18417
|
+
|
|
17857
18418
|
/* Cards grid */
|
|
17858
18419
|
.cards {
|
|
17859
18420
|
display: grid;
|
|
17860
18421
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
17861
18422
|
gap: 1rem;
|
|
17862
|
-
margin-bottom:
|
|
18423
|
+
margin-bottom: 0.5rem;
|
|
17863
18424
|
}
|
|
17864
18425
|
|
|
17865
18426
|
.card {
|
|
@@ -18140,6 +18701,14 @@ tr:hover td {
|
|
|
18140
18701
|
font-family: var(--mono);
|
|
18141
18702
|
font-size: 0.85em;
|
|
18142
18703
|
}
|
|
18704
|
+
.detail-content hr {
|
|
18705
|
+
border: none;
|
|
18706
|
+
border-top: 1px solid var(--border);
|
|
18707
|
+
margin: 1.25rem 0;
|
|
18708
|
+
}
|
|
18709
|
+
.detail-content .table-wrap {
|
|
18710
|
+
margin: 0.75rem 0;
|
|
18711
|
+
}
|
|
18143
18712
|
|
|
18144
18713
|
/* Filters */
|
|
18145
18714
|
.filters {
|
|
@@ -18184,21 +18753,206 @@ tr:hover td {
|
|
|
18184
18753
|
.priority-high { color: var(--red); }
|
|
18185
18754
|
.priority-medium { color: var(--amber); }
|
|
18186
18755
|
.priority-low { color: var(--green); }
|
|
18756
|
+
|
|
18757
|
+
/* Health */
|
|
18758
|
+
.health-section-title {
|
|
18759
|
+
font-size: 1.1rem;
|
|
18760
|
+
font-weight: 600;
|
|
18761
|
+
margin: 2rem 0 1rem;
|
|
18762
|
+
color: var(--text);
|
|
18763
|
+
}
|
|
18764
|
+
|
|
18765
|
+
/* Mermaid diagrams */
|
|
18766
|
+
.mermaid-container {
|
|
18767
|
+
background: var(--bg-card);
|
|
18768
|
+
border: 1px solid var(--border);
|
|
18769
|
+
border-radius: var(--radius);
|
|
18770
|
+
padding: 1.5rem;
|
|
18771
|
+
margin: 1rem 0;
|
|
18772
|
+
overflow-x: auto;
|
|
18773
|
+
}
|
|
18774
|
+
|
|
18775
|
+
.mermaid-container .mermaid {
|
|
18776
|
+
display: flex;
|
|
18777
|
+
justify-content: center;
|
|
18778
|
+
}
|
|
18779
|
+
|
|
18780
|
+
.mermaid-empty {
|
|
18781
|
+
text-align: center;
|
|
18782
|
+
color: var(--text-dim);
|
|
18783
|
+
font-size: 0.875rem;
|
|
18784
|
+
}
|
|
18785
|
+
|
|
18786
|
+
.mermaid-row {
|
|
18787
|
+
display: grid;
|
|
18788
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
18789
|
+
gap: 1rem;
|
|
18790
|
+
}
|
|
18791
|
+
|
|
18792
|
+
.mermaid-row .mermaid-container {
|
|
18793
|
+
margin: 0;
|
|
18794
|
+
}
|
|
18187
18795
|
`;
|
|
18188
18796
|
}
|
|
18189
18797
|
|
|
18798
|
+
// src/web/templates/mermaid.ts
|
|
18799
|
+
function sanitize(text, maxLen = 40) {
|
|
18800
|
+
const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
|
|
18801
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
|
|
18802
|
+
}
|
|
18803
|
+
function mermaidBlock(definition) {
|
|
18804
|
+
return `<div class="mermaid-container"><pre class="mermaid">
|
|
18805
|
+
${definition}
|
|
18806
|
+
</pre></div>`;
|
|
18807
|
+
}
|
|
18808
|
+
function placeholder(message) {
|
|
18809
|
+
return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
|
|
18810
|
+
}
|
|
18811
|
+
function buildTimelineGantt(data) {
|
|
18812
|
+
const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
|
|
18813
|
+
if (sprintsWithDates.length === 0) {
|
|
18814
|
+
return placeholder("No timeline data available \u2014 sprints need start and end dates.");
|
|
18815
|
+
}
|
|
18816
|
+
const epicMap = new Map(data.epics.map((e) => [e.id, e]));
|
|
18817
|
+
const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
|
|
18818
|
+
for (const sprint of sprintsWithDates) {
|
|
18819
|
+
lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
|
|
18820
|
+
const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
|
|
18821
|
+
if (linked.length === 0) {
|
|
18822
|
+
lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
|
|
18823
|
+
} else {
|
|
18824
|
+
for (const epic of linked) {
|
|
18825
|
+
const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
|
|
18826
|
+
lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
|
|
18827
|
+
}
|
|
18828
|
+
}
|
|
18829
|
+
}
|
|
18830
|
+
return mermaidBlock(lines.join("\n"));
|
|
18831
|
+
}
|
|
18832
|
+
function buildArtifactFlowchart(data) {
|
|
18833
|
+
if (data.features.length === 0 && data.epics.length === 0) {
|
|
18834
|
+
return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
|
|
18835
|
+
}
|
|
18836
|
+
const lines = ["graph TD"];
|
|
18837
|
+
lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
|
|
18838
|
+
lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
|
|
18839
|
+
lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
|
|
18840
|
+
lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
|
|
18841
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
18842
|
+
for (const epic of data.epics) {
|
|
18843
|
+
if (epic.linkedFeature) {
|
|
18844
|
+
const feature = data.features.find((f) => f.id === epic.linkedFeature);
|
|
18845
|
+
if (feature) {
|
|
18846
|
+
const fNode = feature.id.replace(/-/g, "_");
|
|
18847
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
18848
|
+
if (!nodeIds.has(fNode)) {
|
|
18849
|
+
lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
|
|
18850
|
+
nodeIds.add(fNode);
|
|
18851
|
+
}
|
|
18852
|
+
if (!nodeIds.has(eNode)) {
|
|
18853
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
18854
|
+
nodeIds.add(eNode);
|
|
18855
|
+
}
|
|
18856
|
+
lines.push(` ${fNode} --> ${eNode}`);
|
|
18857
|
+
}
|
|
18858
|
+
}
|
|
18859
|
+
}
|
|
18860
|
+
for (const sprint of data.sprints) {
|
|
18861
|
+
const sNode = sprint.id.replace(/-/g, "_");
|
|
18862
|
+
for (const epicId of sprint.linkedEpics) {
|
|
18863
|
+
const epic = data.epics.find((e) => e.id === epicId);
|
|
18864
|
+
if (epic) {
|
|
18865
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
18866
|
+
if (!nodeIds.has(eNode)) {
|
|
18867
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
18868
|
+
nodeIds.add(eNode);
|
|
18869
|
+
}
|
|
18870
|
+
if (!nodeIds.has(sNode)) {
|
|
18871
|
+
lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
|
|
18872
|
+
nodeIds.add(sNode);
|
|
18873
|
+
}
|
|
18874
|
+
lines.push(` ${eNode} --> ${sNode}`);
|
|
18875
|
+
}
|
|
18876
|
+
}
|
|
18877
|
+
}
|
|
18878
|
+
if (nodeIds.size === 0) {
|
|
18879
|
+
return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
|
|
18880
|
+
}
|
|
18881
|
+
const allItems = [
|
|
18882
|
+
...data.features.map((f) => ({ id: f.id, status: f.status })),
|
|
18883
|
+
...data.epics.map((e) => ({ id: e.id, status: e.status })),
|
|
18884
|
+
...data.sprints.map((s) => ({ id: s.id, status: s.status }))
|
|
18885
|
+
];
|
|
18886
|
+
for (const item of allItems) {
|
|
18887
|
+
const node = item.id.replace(/-/g, "_");
|
|
18888
|
+
if (!nodeIds.has(node)) continue;
|
|
18889
|
+
const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
|
|
18890
|
+
if (cls) {
|
|
18891
|
+
lines.push(` class ${node} ${cls}`);
|
|
18892
|
+
}
|
|
18893
|
+
}
|
|
18894
|
+
return mermaidBlock(lines.join("\n"));
|
|
18895
|
+
}
|
|
18896
|
+
function buildStatusPie(title, counts) {
|
|
18897
|
+
const entries = Object.entries(counts).filter(([, v]) => v > 0);
|
|
18898
|
+
if (entries.length === 0) {
|
|
18899
|
+
return placeholder(`No data for ${title}.`);
|
|
18900
|
+
}
|
|
18901
|
+
const lines = [`pie title ${sanitize(title, 60)}`];
|
|
18902
|
+
for (const [label, count] of entries) {
|
|
18903
|
+
lines.push(` "${sanitize(label, 30)}" : ${count}`);
|
|
18904
|
+
}
|
|
18905
|
+
return mermaidBlock(lines.join("\n"));
|
|
18906
|
+
}
|
|
18907
|
+
function buildHealthGauge(categories) {
|
|
18908
|
+
const valid = categories.filter((c) => c.total > 0);
|
|
18909
|
+
if (valid.length === 0) {
|
|
18910
|
+
return placeholder("No completeness data available.");
|
|
18911
|
+
}
|
|
18912
|
+
const pies = valid.map((cat) => {
|
|
18913
|
+
const incomplete = cat.total - cat.complete;
|
|
18914
|
+
const lines = [
|
|
18915
|
+
`pie title ${sanitize(cat.name, 30)}`,
|
|
18916
|
+
` "Complete" : ${cat.complete}`,
|
|
18917
|
+
` "Incomplete" : ${incomplete}`
|
|
18918
|
+
];
|
|
18919
|
+
return mermaidBlock(lines.join("\n"));
|
|
18920
|
+
});
|
|
18921
|
+
return `<div class="mermaid-row">${pies.join("\n")}</div>`;
|
|
18922
|
+
}
|
|
18923
|
+
|
|
18190
18924
|
// src/web/templates/pages/overview.ts
|
|
18191
|
-
function
|
|
18192
|
-
|
|
18193
|
-
(t) => `
|
|
18925
|
+
function renderCard(t) {
|
|
18926
|
+
return `
|
|
18194
18927
|
<div class="card">
|
|
18195
18928
|
<a href="/docs/${t.type}">
|
|
18196
18929
|
<div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
|
|
18197
18930
|
<div class="card-value">${t.total}</div>
|
|
18198
18931
|
${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
|
|
18199
18932
|
</a>
|
|
18200
|
-
</div
|
|
18201
|
-
|
|
18933
|
+
</div>`;
|
|
18934
|
+
}
|
|
18935
|
+
function overviewPage(data, diagrams, navGroups) {
|
|
18936
|
+
const typeMap = new Map(data.types.map((t) => [t.type, t]));
|
|
18937
|
+
const placed = /* @__PURE__ */ new Set();
|
|
18938
|
+
const groupSections = navGroups.map((group) => {
|
|
18939
|
+
const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
|
|
18940
|
+
placed.add(type);
|
|
18941
|
+
return renderCard(typeMap.get(type));
|
|
18942
|
+
});
|
|
18943
|
+
if (groupCards.length === 0) return "";
|
|
18944
|
+
return `
|
|
18945
|
+
<div class="card-group">
|
|
18946
|
+
<div class="card-group-label">${escapeHtml(group.label)}</div>
|
|
18947
|
+
<div class="cards">${groupCards.join("\n")}</div>
|
|
18948
|
+
</div>`;
|
|
18949
|
+
}).filter(Boolean).join("\n");
|
|
18950
|
+
const ungrouped = data.types.filter((t) => !placed.has(t.type));
|
|
18951
|
+
const ungroupedSection = ungrouped.length > 0 ? `
|
|
18952
|
+
<div class="card-group">
|
|
18953
|
+
<div class="card-group-label">Other</div>
|
|
18954
|
+
<div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
|
|
18955
|
+
</div>` : "";
|
|
18202
18956
|
const rows = data.recent.map(
|
|
18203
18957
|
(doc) => `
|
|
18204
18958
|
<tr>
|
|
@@ -18214,9 +18968,14 @@ function overviewPage(data) {
|
|
|
18214
18968
|
<h2>Project Overview</h2>
|
|
18215
18969
|
</div>
|
|
18216
18970
|
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18971
|
+
${groupSections}
|
|
18972
|
+
${ungroupedSection}
|
|
18973
|
+
|
|
18974
|
+
<div class="section-title">Project Timeline</div>
|
|
18975
|
+
${buildTimelineGantt(diagrams)}
|
|
18976
|
+
|
|
18977
|
+
<div class="section-title">Artifact Relationships</div>
|
|
18978
|
+
${buildArtifactFlowchart(diagrams)}
|
|
18220
18979
|
|
|
18221
18980
|
<div class="section-title">Recent Activity</div>
|
|
18222
18981
|
${data.recent.length > 0 ? `
|
|
@@ -18383,6 +19142,76 @@ function garPage(report) {
|
|
|
18383
19142
|
<div class="gar-areas">
|
|
18384
19143
|
${areaCards}
|
|
18385
19144
|
</div>
|
|
19145
|
+
|
|
19146
|
+
<div class="section-title">Status Distribution</div>
|
|
19147
|
+
${buildStatusPie("Action Status", {
|
|
19148
|
+
Open: report.metrics.scope.open,
|
|
19149
|
+
Done: report.metrics.scope.done,
|
|
19150
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
19151
|
+
})}
|
|
19152
|
+
`;
|
|
19153
|
+
}
|
|
19154
|
+
|
|
19155
|
+
// src/web/templates/pages/health.ts
|
|
19156
|
+
function healthPage(report, metrics) {
|
|
19157
|
+
const dotClass = `dot-${report.overall}`;
|
|
19158
|
+
function renderSection(title, categories) {
|
|
19159
|
+
const cards = categories.map(
|
|
19160
|
+
(cat) => `
|
|
19161
|
+
<div class="gar-area">
|
|
19162
|
+
<div class="area-header">
|
|
19163
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
19164
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
19165
|
+
</div>
|
|
19166
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
19167
|
+
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
19168
|
+
</div>`
|
|
19169
|
+
).join("\n");
|
|
19170
|
+
return `
|
|
19171
|
+
<div class="health-section-title">${escapeHtml(title)}</div>
|
|
19172
|
+
<div class="gar-areas">${cards}</div>
|
|
19173
|
+
`;
|
|
19174
|
+
}
|
|
19175
|
+
return `
|
|
19176
|
+
<div class="page-header">
|
|
19177
|
+
<h2>Governance Health Check</h2>
|
|
19178
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
19179
|
+
</div>
|
|
19180
|
+
|
|
19181
|
+
<div class="gar-overall">
|
|
19182
|
+
<div class="dot ${dotClass}"></div>
|
|
19183
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
19184
|
+
</div>
|
|
19185
|
+
|
|
19186
|
+
${renderSection("Completeness", report.completeness)}
|
|
19187
|
+
|
|
19188
|
+
<div class="health-section-title">Completeness Overview</div>
|
|
19189
|
+
${buildHealthGauge(
|
|
19190
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
19191
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
19192
|
+
complete: cat.complete,
|
|
19193
|
+
total: cat.total
|
|
19194
|
+
})) : report.completeness.map((c) => {
|
|
19195
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
19196
|
+
return {
|
|
19197
|
+
name: c.name,
|
|
19198
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
19199
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
19200
|
+
};
|
|
19201
|
+
})
|
|
19202
|
+
)}
|
|
19203
|
+
|
|
19204
|
+
${renderSection("Process", report.process)}
|
|
19205
|
+
|
|
19206
|
+
<div class="health-section-title">Process Summary</div>
|
|
19207
|
+
${metrics ? buildStatusPie("Process Health", {
|
|
19208
|
+
Stale: metrics.process.stale.length,
|
|
19209
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
19210
|
+
Healthy: Math.max(
|
|
19211
|
+
0,
|
|
19212
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
19213
|
+
)
|
|
19214
|
+
}) : ""}
|
|
18386
19215
|
`;
|
|
18387
19216
|
}
|
|
18388
19217
|
|
|
@@ -18449,7 +19278,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18449
19278
|
}
|
|
18450
19279
|
if (pathname === "/") {
|
|
18451
19280
|
const data = getOverviewData(store);
|
|
18452
|
-
const
|
|
19281
|
+
const diagrams = getDiagramData(store);
|
|
19282
|
+
const body = overviewPage(data, diagrams, navGroups);
|
|
18453
19283
|
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
|
|
18454
19284
|
return;
|
|
18455
19285
|
}
|
|
@@ -18459,6 +19289,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18459
19289
|
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
|
|
18460
19290
|
return;
|
|
18461
19291
|
}
|
|
19292
|
+
if (pathname === "/health") {
|
|
19293
|
+
const healthMetrics = collectHealthMetrics(store);
|
|
19294
|
+
const report = evaluateHealth(projectName, healthMetrics);
|
|
19295
|
+
const body = healthPage(report, healthMetrics);
|
|
19296
|
+
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
19297
|
+
return;
|
|
19298
|
+
}
|
|
18462
19299
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
18463
19300
|
if (boardMatch) {
|
|
18464
19301
|
const type = boardMatch[1];
|
|
@@ -18519,8 +19356,8 @@ import * as http from "http";
|
|
|
18519
19356
|
import { exec } from "child_process";
|
|
18520
19357
|
|
|
18521
19358
|
// src/skills/registry.ts
|
|
18522
|
-
import * as
|
|
18523
|
-
import * as
|
|
19359
|
+
import * as fs8 from "fs";
|
|
19360
|
+
import * as path8 from "path";
|
|
18524
19361
|
import { fileURLToPath } from "url";
|
|
18525
19362
|
import * as YAML5 from "yaml";
|
|
18526
19363
|
import matter2 from "gray-matter";
|
|
@@ -18573,8 +19410,8 @@ var JiraClient = class {
|
|
|
18573
19410
|
this.baseUrl = `https://${config2.host}/rest/api/2`;
|
|
18574
19411
|
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
18575
19412
|
}
|
|
18576
|
-
async request(
|
|
18577
|
-
const url2 = `${this.baseUrl}${
|
|
19413
|
+
async request(path20, method = "GET", body) {
|
|
19414
|
+
const url2 = `${this.baseUrl}${path20}`;
|
|
18578
19415
|
const headers = {
|
|
18579
19416
|
Authorization: this.authHeader,
|
|
18580
19417
|
"Content-Type": "application/json",
|
|
@@ -18588,7 +19425,7 @@ var JiraClient = class {
|
|
|
18588
19425
|
if (!response.ok) {
|
|
18589
19426
|
const text = await response.text().catch(() => "");
|
|
18590
19427
|
throw new Error(
|
|
18591
|
-
`Jira API error ${response.status} ${method} ${
|
|
19428
|
+
`Jira API error ${response.status} ${method} ${path20}: ${text}`
|
|
18592
19429
|
);
|
|
18593
19430
|
}
|
|
18594
19431
|
if (response.status === 204) return void 0;
|
|
@@ -18698,7 +19535,7 @@ function createJiraTools(store) {
|
|
|
18698
19535
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
18699
19536
|
};
|
|
18700
19537
|
},
|
|
18701
|
-
{ annotations: {
|
|
19538
|
+
{ annotations: { readOnlyHint: true } }
|
|
18702
19539
|
),
|
|
18703
19540
|
tool19(
|
|
18704
19541
|
"get_jira_issue",
|
|
@@ -18730,7 +19567,7 @@ function createJiraTools(store) {
|
|
|
18730
19567
|
]
|
|
18731
19568
|
};
|
|
18732
19569
|
},
|
|
18733
|
-
{ annotations: {
|
|
19570
|
+
{ annotations: { readOnlyHint: true } }
|
|
18734
19571
|
),
|
|
18735
19572
|
// --- Jira → Local tools ---
|
|
18736
19573
|
tool19(
|
|
@@ -19068,13 +19905,13 @@ var GOVERNANCE_TOOL_NAMES = [
|
|
|
19068
19905
|
];
|
|
19069
19906
|
function getBuiltinSkillsDir() {
|
|
19070
19907
|
const thisFile = fileURLToPath(import.meta.url);
|
|
19071
|
-
return
|
|
19908
|
+
return path8.join(path8.dirname(thisFile), "builtin");
|
|
19072
19909
|
}
|
|
19073
19910
|
function loadSkillFromDirectory(dirPath) {
|
|
19074
|
-
const skillMdPath =
|
|
19075
|
-
if (!
|
|
19911
|
+
const skillMdPath = path8.join(dirPath, "SKILL.md");
|
|
19912
|
+
if (!fs8.existsSync(skillMdPath)) return void 0;
|
|
19076
19913
|
try {
|
|
19077
|
-
const raw =
|
|
19914
|
+
const raw = fs8.readFileSync(skillMdPath, "utf-8");
|
|
19078
19915
|
const { data, content } = matter2(raw);
|
|
19079
19916
|
if (!data.name || !data.description) return void 0;
|
|
19080
19917
|
const metadata = data.metadata ?? {};
|
|
@@ -19085,13 +19922,13 @@ function loadSkillFromDirectory(dirPath) {
|
|
|
19085
19922
|
if (wildcardPrompt) {
|
|
19086
19923
|
promptFragments["*"] = wildcardPrompt;
|
|
19087
19924
|
}
|
|
19088
|
-
const personasDir =
|
|
19089
|
-
if (
|
|
19925
|
+
const personasDir = path8.join(dirPath, "personas");
|
|
19926
|
+
if (fs8.existsSync(personasDir)) {
|
|
19090
19927
|
try {
|
|
19091
|
-
for (const file2 of
|
|
19928
|
+
for (const file2 of fs8.readdirSync(personasDir)) {
|
|
19092
19929
|
if (!file2.endsWith(".md")) continue;
|
|
19093
19930
|
const personaId = file2.replace(/\.md$/, "");
|
|
19094
|
-
const personaPrompt =
|
|
19931
|
+
const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
|
|
19095
19932
|
if (personaPrompt) {
|
|
19096
19933
|
promptFragments[personaId] = personaPrompt;
|
|
19097
19934
|
}
|
|
@@ -19100,10 +19937,10 @@ function loadSkillFromDirectory(dirPath) {
|
|
|
19100
19937
|
}
|
|
19101
19938
|
}
|
|
19102
19939
|
let actions;
|
|
19103
|
-
const actionsPath =
|
|
19104
|
-
if (
|
|
19940
|
+
const actionsPath = path8.join(dirPath, "actions.yaml");
|
|
19941
|
+
if (fs8.existsSync(actionsPath)) {
|
|
19105
19942
|
try {
|
|
19106
|
-
const actionsRaw =
|
|
19943
|
+
const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
|
|
19107
19944
|
actions = YAML5.parse(actionsRaw);
|
|
19108
19945
|
} catch {
|
|
19109
19946
|
}
|
|
@@ -19130,10 +19967,10 @@ function loadAllSkills(marvinDir) {
|
|
|
19130
19967
|
}
|
|
19131
19968
|
try {
|
|
19132
19969
|
const builtinDir = getBuiltinSkillsDir();
|
|
19133
|
-
if (
|
|
19134
|
-
for (const entry of
|
|
19135
|
-
const entryPath =
|
|
19136
|
-
if (!
|
|
19970
|
+
if (fs8.existsSync(builtinDir)) {
|
|
19971
|
+
for (const entry of fs8.readdirSync(builtinDir)) {
|
|
19972
|
+
const entryPath = path8.join(builtinDir, entry);
|
|
19973
|
+
if (!fs8.statSync(entryPath).isDirectory()) continue;
|
|
19137
19974
|
if (skills.has(entry)) continue;
|
|
19138
19975
|
const skill = loadSkillFromDirectory(entryPath);
|
|
19139
19976
|
if (skill) skills.set(skill.id, skill);
|
|
@@ -19142,18 +19979,18 @@ function loadAllSkills(marvinDir) {
|
|
|
19142
19979
|
} catch {
|
|
19143
19980
|
}
|
|
19144
19981
|
if (marvinDir) {
|
|
19145
|
-
const skillsDir =
|
|
19146
|
-
if (
|
|
19982
|
+
const skillsDir = path8.join(marvinDir, "skills");
|
|
19983
|
+
if (fs8.existsSync(skillsDir)) {
|
|
19147
19984
|
let entries;
|
|
19148
19985
|
try {
|
|
19149
|
-
entries =
|
|
19986
|
+
entries = fs8.readdirSync(skillsDir);
|
|
19150
19987
|
} catch {
|
|
19151
19988
|
entries = [];
|
|
19152
19989
|
}
|
|
19153
19990
|
for (const entry of entries) {
|
|
19154
|
-
const entryPath =
|
|
19991
|
+
const entryPath = path8.join(skillsDir, entry);
|
|
19155
19992
|
try {
|
|
19156
|
-
if (
|
|
19993
|
+
if (fs8.statSync(entryPath).isDirectory()) {
|
|
19157
19994
|
const skill = loadSkillFromDirectory(entryPath);
|
|
19158
19995
|
if (skill) skills.set(skill.id, skill);
|
|
19159
19996
|
continue;
|
|
@@ -19163,7 +20000,7 @@ function loadAllSkills(marvinDir) {
|
|
|
19163
20000
|
}
|
|
19164
20001
|
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
|
|
19165
20002
|
try {
|
|
19166
|
-
const raw =
|
|
20003
|
+
const raw = fs8.readFileSync(entryPath, "utf-8");
|
|
19167
20004
|
const parsed = YAML5.parse(raw);
|
|
19168
20005
|
if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
|
|
19169
20006
|
const skill = {
|
|
@@ -19268,12 +20105,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
|
|
|
19268
20105
|
return agents;
|
|
19269
20106
|
}
|
|
19270
20107
|
function migrateYamlToSkillMd(yamlPath, outputDir) {
|
|
19271
|
-
const raw =
|
|
20108
|
+
const raw = fs8.readFileSync(yamlPath, "utf-8");
|
|
19272
20109
|
const parsed = YAML5.parse(raw);
|
|
19273
20110
|
if (!parsed?.id || !parsed?.name) {
|
|
19274
20111
|
throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
|
|
19275
20112
|
}
|
|
19276
|
-
|
|
20113
|
+
fs8.mkdirSync(outputDir, { recursive: true });
|
|
19277
20114
|
const frontmatter = {
|
|
19278
20115
|
name: parsed.id,
|
|
19279
20116
|
description: parsed.description ?? ""
|
|
@@ -19287,15 +20124,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
|
|
|
19287
20124
|
const skillMd = matter2.stringify(wildcardPrompt ? `
|
|
19288
20125
|
${wildcardPrompt}
|
|
19289
20126
|
` : "\n", frontmatter);
|
|
19290
|
-
|
|
20127
|
+
fs8.writeFileSync(path8.join(outputDir, "SKILL.md"), skillMd, "utf-8");
|
|
19291
20128
|
if (promptFragments) {
|
|
19292
20129
|
const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
|
|
19293
20130
|
if (personaKeys.length > 0) {
|
|
19294
|
-
const personasDir =
|
|
19295
|
-
|
|
20131
|
+
const personasDir = path8.join(outputDir, "personas");
|
|
20132
|
+
fs8.mkdirSync(personasDir, { recursive: true });
|
|
19296
20133
|
for (const personaId of personaKeys) {
|
|
19297
|
-
|
|
19298
|
-
|
|
20134
|
+
fs8.writeFileSync(
|
|
20135
|
+
path8.join(personasDir, `${personaId}.md`),
|
|
19299
20136
|
`${promptFragments[personaId]}
|
|
19300
20137
|
`,
|
|
19301
20138
|
"utf-8"
|
|
@@ -19305,8 +20142,8 @@ ${wildcardPrompt}
|
|
|
19305
20142
|
}
|
|
19306
20143
|
const actions = parsed.actions;
|
|
19307
20144
|
if (actions && actions.length > 0) {
|
|
19308
|
-
|
|
19309
|
-
|
|
20145
|
+
fs8.writeFileSync(
|
|
20146
|
+
path8.join(outputDir, "actions.yaml"),
|
|
19310
20147
|
YAML5.stringify(actions),
|
|
19311
20148
|
"utf-8"
|
|
19312
20149
|
);
|
|
@@ -19461,7 +20298,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19461
20298
|
content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
|
|
19462
20299
|
};
|
|
19463
20300
|
},
|
|
19464
|
-
{ annotations: {
|
|
20301
|
+
{ annotations: { readOnlyHint: true } }
|
|
19465
20302
|
),
|
|
19466
20303
|
tool20(
|
|
19467
20304
|
"get_dashboard_overview",
|
|
@@ -19483,7 +20320,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19483
20320
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19484
20321
|
};
|
|
19485
20322
|
},
|
|
19486
|
-
{ annotations: {
|
|
20323
|
+
{ annotations: { readOnlyHint: true } }
|
|
19487
20324
|
),
|
|
19488
20325
|
tool20(
|
|
19489
20326
|
"get_dashboard_gar",
|
|
@@ -19495,7 +20332,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19495
20332
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
19496
20333
|
};
|
|
19497
20334
|
},
|
|
19498
|
-
{ annotations: {
|
|
20335
|
+
{ annotations: { readOnlyHint: true } }
|
|
19499
20336
|
),
|
|
19500
20337
|
tool20(
|
|
19501
20338
|
"get_dashboard_board",
|
|
@@ -19523,7 +20360,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19523
20360
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19524
20361
|
};
|
|
19525
20362
|
},
|
|
19526
|
-
{ annotations: {
|
|
20363
|
+
{ annotations: { readOnlyHint: true } }
|
|
19527
20364
|
)
|
|
19528
20365
|
];
|
|
19529
20366
|
}
|
|
@@ -19580,8 +20417,8 @@ function slugify3(text) {
|
|
|
19580
20417
|
}
|
|
19581
20418
|
|
|
19582
20419
|
// src/sources/manifest.ts
|
|
19583
|
-
import * as
|
|
19584
|
-
import * as
|
|
20420
|
+
import * as fs9 from "fs";
|
|
20421
|
+
import * as path9 from "path";
|
|
19585
20422
|
import * as crypto from "crypto";
|
|
19586
20423
|
import * as YAML6 from "yaml";
|
|
19587
20424
|
var MANIFEST_FILE = ".manifest.yaml";
|
|
@@ -19594,37 +20431,37 @@ var SourceManifestManager = class {
|
|
|
19594
20431
|
manifestPath;
|
|
19595
20432
|
sourcesDir;
|
|
19596
20433
|
constructor(marvinDir) {
|
|
19597
|
-
this.sourcesDir =
|
|
19598
|
-
this.manifestPath =
|
|
20434
|
+
this.sourcesDir = path9.join(marvinDir, "sources");
|
|
20435
|
+
this.manifestPath = path9.join(this.sourcesDir, MANIFEST_FILE);
|
|
19599
20436
|
this.manifest = this.load();
|
|
19600
20437
|
}
|
|
19601
20438
|
load() {
|
|
19602
|
-
if (!
|
|
20439
|
+
if (!fs9.existsSync(this.manifestPath)) {
|
|
19603
20440
|
return emptyManifest();
|
|
19604
20441
|
}
|
|
19605
|
-
const raw =
|
|
20442
|
+
const raw = fs9.readFileSync(this.manifestPath, "utf-8");
|
|
19606
20443
|
const parsed = YAML6.parse(raw);
|
|
19607
20444
|
return parsed ?? emptyManifest();
|
|
19608
20445
|
}
|
|
19609
20446
|
save() {
|
|
19610
|
-
|
|
19611
|
-
|
|
20447
|
+
fs9.mkdirSync(this.sourcesDir, { recursive: true });
|
|
20448
|
+
fs9.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
|
|
19612
20449
|
}
|
|
19613
20450
|
scan() {
|
|
19614
20451
|
const added = [];
|
|
19615
20452
|
const changed = [];
|
|
19616
20453
|
const removed = [];
|
|
19617
|
-
if (!
|
|
20454
|
+
if (!fs9.existsSync(this.sourcesDir)) {
|
|
19618
20455
|
return { added, changed, removed };
|
|
19619
20456
|
}
|
|
19620
20457
|
const onDisk = new Set(
|
|
19621
|
-
|
|
19622
|
-
const ext =
|
|
20458
|
+
fs9.readdirSync(this.sourcesDir).filter((f) => {
|
|
20459
|
+
const ext = path9.extname(f).toLowerCase();
|
|
19623
20460
|
return SOURCE_EXTENSIONS.includes(ext);
|
|
19624
20461
|
})
|
|
19625
20462
|
);
|
|
19626
20463
|
for (const fileName of onDisk) {
|
|
19627
|
-
const filePath =
|
|
20464
|
+
const filePath = path9.join(this.sourcesDir, fileName);
|
|
19628
20465
|
const hash2 = this.hashFile(filePath);
|
|
19629
20466
|
const existing = this.manifest.files[fileName];
|
|
19630
20467
|
if (!existing) {
|
|
@@ -19687,7 +20524,7 @@ var SourceManifestManager = class {
|
|
|
19687
20524
|
this.save();
|
|
19688
20525
|
}
|
|
19689
20526
|
hashFile(filePath) {
|
|
19690
|
-
const content =
|
|
20527
|
+
const content = fs9.readFileSync(filePath);
|
|
19691
20528
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
19692
20529
|
}
|
|
19693
20530
|
};
|
|
@@ -19702,8 +20539,8 @@ async function startSession(options) {
|
|
|
19702
20539
|
const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
|
|
19703
20540
|
const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
|
|
19704
20541
|
const sessionStore = new SessionStore(marvinDir);
|
|
19705
|
-
const sourcesDir =
|
|
19706
|
-
const hasSourcesDir =
|
|
20542
|
+
const sourcesDir = path10.join(marvinDir, "sources");
|
|
20543
|
+
const hasSourcesDir = fs10.existsSync(sourcesDir);
|
|
19707
20544
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
19708
20545
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
19709
20546
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
@@ -19726,7 +20563,7 @@ async function startSession(options) {
|
|
|
19726
20563
|
projectName: config2.project.name,
|
|
19727
20564
|
navGroups
|
|
19728
20565
|
});
|
|
19729
|
-
const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment);
|
|
20566
|
+
const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment, marvinDir);
|
|
19730
20567
|
let existingSession;
|
|
19731
20568
|
if (options.sessionName) {
|
|
19732
20569
|
existingSession = sessionStore.get(options.sessionName);
|
|
@@ -20179,13 +21016,13 @@ async function setApiKey() {
|
|
|
20179
21016
|
}
|
|
20180
21017
|
|
|
20181
21018
|
// src/cli/commands/ingest.ts
|
|
20182
|
-
import * as
|
|
20183
|
-
import * as
|
|
21019
|
+
import * as fs12 from "fs";
|
|
21020
|
+
import * as path12 from "path";
|
|
20184
21021
|
import chalk8 from "chalk";
|
|
20185
21022
|
|
|
20186
21023
|
// src/sources/ingest.ts
|
|
20187
|
-
import * as
|
|
20188
|
-
import * as
|
|
21024
|
+
import * as fs11 from "fs";
|
|
21025
|
+
import * as path11 from "path";
|
|
20189
21026
|
import chalk7 from "chalk";
|
|
20190
21027
|
import ora2 from "ora";
|
|
20191
21028
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -20288,15 +21125,15 @@ async function ingestFile(options) {
|
|
|
20288
21125
|
const persona = getPersona(personaId);
|
|
20289
21126
|
const manifest = new SourceManifestManager(marvinDir);
|
|
20290
21127
|
const sourcesDir = manifest.sourcesDir;
|
|
20291
|
-
const filePath =
|
|
20292
|
-
if (!
|
|
21128
|
+
const filePath = path11.join(sourcesDir, fileName);
|
|
21129
|
+
if (!fs11.existsSync(filePath)) {
|
|
20293
21130
|
throw new Error(`Source file not found: ${filePath}`);
|
|
20294
21131
|
}
|
|
20295
|
-
const ext =
|
|
21132
|
+
const ext = path11.extname(fileName).toLowerCase();
|
|
20296
21133
|
const isPdf = ext === ".pdf";
|
|
20297
21134
|
let fileContent = null;
|
|
20298
21135
|
if (!isPdf) {
|
|
20299
|
-
fileContent =
|
|
21136
|
+
fileContent = fs11.readFileSync(filePath, "utf-8");
|
|
20300
21137
|
}
|
|
20301
21138
|
const store = new DocumentStore(marvinDir);
|
|
20302
21139
|
const createdArtifacts = [];
|
|
@@ -20399,9 +21236,9 @@ Ingest ended with error: ${message.subtype}`)
|
|
|
20399
21236
|
async function ingestCommand(file2, options) {
|
|
20400
21237
|
const project = loadProject();
|
|
20401
21238
|
const marvinDir = project.marvinDir;
|
|
20402
|
-
const sourcesDir =
|
|
20403
|
-
if (!
|
|
20404
|
-
|
|
21239
|
+
const sourcesDir = path12.join(marvinDir, "sources");
|
|
21240
|
+
if (!fs12.existsSync(sourcesDir)) {
|
|
21241
|
+
fs12.mkdirSync(sourcesDir, { recursive: true });
|
|
20405
21242
|
}
|
|
20406
21243
|
const manifest = new SourceManifestManager(marvinDir);
|
|
20407
21244
|
manifest.scan();
|
|
@@ -20412,8 +21249,8 @@ async function ingestCommand(file2, options) {
|
|
|
20412
21249
|
return;
|
|
20413
21250
|
}
|
|
20414
21251
|
if (file2) {
|
|
20415
|
-
const filePath =
|
|
20416
|
-
if (!
|
|
21252
|
+
const filePath = path12.join(sourcesDir, file2);
|
|
21253
|
+
if (!fs12.existsSync(filePath)) {
|
|
20417
21254
|
console.log(chalk8.red(`Source file not found: ${file2}`));
|
|
20418
21255
|
console.log(chalk8.dim(`Expected at: ${filePath}`));
|
|
20419
21256
|
console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
|
|
@@ -20480,7 +21317,7 @@ import ora3 from "ora";
|
|
|
20480
21317
|
import { input as input3 } from "@inquirer/prompts";
|
|
20481
21318
|
|
|
20482
21319
|
// src/git/repository.ts
|
|
20483
|
-
import * as
|
|
21320
|
+
import * as path13 from "path";
|
|
20484
21321
|
import simpleGit from "simple-git";
|
|
20485
21322
|
var MARVIN_GITIGNORE = `node_modules/
|
|
20486
21323
|
.DS_Store
|
|
@@ -20500,7 +21337,7 @@ var DIR_TYPE_LABELS = {
|
|
|
20500
21337
|
function buildCommitMessage(files) {
|
|
20501
21338
|
const counts = /* @__PURE__ */ new Map();
|
|
20502
21339
|
for (const f of files) {
|
|
20503
|
-
const parts2 = f.split(
|
|
21340
|
+
const parts2 = f.split(path13.sep).join("/").split("/");
|
|
20504
21341
|
const docsIdx = parts2.indexOf("docs");
|
|
20505
21342
|
if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
|
|
20506
21343
|
const dirName = parts2[docsIdx + 1];
|
|
@@ -20540,9 +21377,9 @@ var MarvinGit = class {
|
|
|
20540
21377
|
);
|
|
20541
21378
|
}
|
|
20542
21379
|
await this.git.init();
|
|
20543
|
-
const { writeFileSync:
|
|
20544
|
-
|
|
20545
|
-
|
|
21380
|
+
const { writeFileSync: writeFileSync10 } = await import("fs");
|
|
21381
|
+
writeFileSync10(
|
|
21382
|
+
path13.join(this.marvinDir, ".gitignore"),
|
|
20546
21383
|
MARVIN_GITIGNORE,
|
|
20547
21384
|
"utf-8"
|
|
20548
21385
|
);
|
|
@@ -20662,9 +21499,9 @@ var MarvinGit = class {
|
|
|
20662
21499
|
}
|
|
20663
21500
|
}
|
|
20664
21501
|
static async clone(url2, targetDir) {
|
|
20665
|
-
const marvinDir =
|
|
20666
|
-
const { existsSync:
|
|
20667
|
-
if (
|
|
21502
|
+
const marvinDir = path13.join(targetDir, ".marvin");
|
|
21503
|
+
const { existsSync: existsSync17 } = await import("fs");
|
|
21504
|
+
if (existsSync17(marvinDir)) {
|
|
20668
21505
|
throw new GitSyncError(
|
|
20669
21506
|
`.marvin/ already exists at ${targetDir}. Remove it first or choose a different directory.`
|
|
20670
21507
|
);
|
|
@@ -20842,8 +21679,8 @@ async function cloneCommand(url2, directory) {
|
|
|
20842
21679
|
}
|
|
20843
21680
|
|
|
20844
21681
|
// src/mcp/stdio-server.ts
|
|
20845
|
-
import * as
|
|
20846
|
-
import * as
|
|
21682
|
+
import * as fs13 from "fs";
|
|
21683
|
+
import * as path14 from "path";
|
|
20847
21684
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20848
21685
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20849
21686
|
|
|
@@ -21074,7 +21911,7 @@ ${summaries}`
|
|
|
21074
21911
|
content: [{ type: "text", text: guidance }]
|
|
21075
21912
|
};
|
|
21076
21913
|
},
|
|
21077
|
-
{ annotations: {
|
|
21914
|
+
{ annotations: { readOnlyHint: true } }
|
|
21078
21915
|
)
|
|
21079
21916
|
];
|
|
21080
21917
|
}
|
|
@@ -21141,8 +21978,8 @@ function collectTools(marvinDir) {
|
|
|
21141
21978
|
const plugin = resolvePlugin(config2.methodology);
|
|
21142
21979
|
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
21143
21980
|
const store = new DocumentStore(marvinDir, registrations);
|
|
21144
|
-
const sourcesDir =
|
|
21145
|
-
const hasSourcesDir =
|
|
21981
|
+
const sourcesDir = path14.join(marvinDir, "sources");
|
|
21982
|
+
const hasSourcesDir = fs13.existsSync(sourcesDir);
|
|
21146
21983
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
21147
21984
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
21148
21985
|
const sessionStore = new SessionStore(marvinDir);
|
|
@@ -21150,7 +21987,7 @@ function collectTools(marvinDir) {
|
|
|
21150
21987
|
const allSkillIds = [...allSkills.keys()];
|
|
21151
21988
|
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
21152
21989
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
21153
|
-
const projectRoot =
|
|
21990
|
+
const projectRoot = path14.dirname(marvinDir);
|
|
21154
21991
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|
|
21155
21992
|
return [
|
|
21156
21993
|
...createDecisionTools(store),
|
|
@@ -21213,8 +22050,8 @@ async function serveCommand() {
|
|
|
21213
22050
|
}
|
|
21214
22051
|
|
|
21215
22052
|
// src/cli/commands/skills.ts
|
|
21216
|
-
import * as
|
|
21217
|
-
import * as
|
|
22053
|
+
import * as fs14 from "fs";
|
|
22054
|
+
import * as path15 from "path";
|
|
21218
22055
|
import * as YAML7 from "yaml";
|
|
21219
22056
|
import matter3 from "gray-matter";
|
|
21220
22057
|
import chalk10 from "chalk";
|
|
@@ -21320,14 +22157,14 @@ async function skillsRemoveCommand(skillId, options) {
|
|
|
21320
22157
|
}
|
|
21321
22158
|
async function skillsCreateCommand(name) {
|
|
21322
22159
|
const project = loadProject();
|
|
21323
|
-
const skillsDir =
|
|
21324
|
-
|
|
21325
|
-
const skillDir =
|
|
21326
|
-
if (
|
|
22160
|
+
const skillsDir = path15.join(project.marvinDir, "skills");
|
|
22161
|
+
fs14.mkdirSync(skillsDir, { recursive: true });
|
|
22162
|
+
const skillDir = path15.join(skillsDir, name);
|
|
22163
|
+
if (fs14.existsSync(skillDir)) {
|
|
21327
22164
|
console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
|
|
21328
22165
|
return;
|
|
21329
22166
|
}
|
|
21330
|
-
|
|
22167
|
+
fs14.mkdirSync(skillDir, { recursive: true });
|
|
21331
22168
|
const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
21332
22169
|
const frontmatter = {
|
|
21333
22170
|
name,
|
|
@@ -21341,7 +22178,7 @@ async function skillsCreateCommand(name) {
|
|
|
21341
22178
|
You have the **${displayName}** skill.
|
|
21342
22179
|
`;
|
|
21343
22180
|
const skillMd = matter3.stringify(body, frontmatter);
|
|
21344
|
-
|
|
22181
|
+
fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
|
|
21345
22182
|
const actions = [
|
|
21346
22183
|
{
|
|
21347
22184
|
id: "run",
|
|
@@ -21351,7 +22188,7 @@ You have the **${displayName}** skill.
|
|
|
21351
22188
|
maxTurns: 5
|
|
21352
22189
|
}
|
|
21353
22190
|
];
|
|
21354
|
-
|
|
22191
|
+
fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
|
|
21355
22192
|
console.log(chalk10.green(`Created skill: ${skillDir}/`));
|
|
21356
22193
|
console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
|
|
21357
22194
|
console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
|
|
@@ -21359,14 +22196,14 @@ You have the **${displayName}** skill.
|
|
|
21359
22196
|
}
|
|
21360
22197
|
async function skillsMigrateCommand() {
|
|
21361
22198
|
const project = loadProject();
|
|
21362
|
-
const skillsDir =
|
|
21363
|
-
if (!
|
|
22199
|
+
const skillsDir = path15.join(project.marvinDir, "skills");
|
|
22200
|
+
if (!fs14.existsSync(skillsDir)) {
|
|
21364
22201
|
console.log(chalk10.dim("No skills directory found."));
|
|
21365
22202
|
return;
|
|
21366
22203
|
}
|
|
21367
22204
|
let entries;
|
|
21368
22205
|
try {
|
|
21369
|
-
entries =
|
|
22206
|
+
entries = fs14.readdirSync(skillsDir);
|
|
21370
22207
|
} catch {
|
|
21371
22208
|
console.log(chalk10.red("Could not read skills directory."));
|
|
21372
22209
|
return;
|
|
@@ -21378,16 +22215,16 @@ async function skillsMigrateCommand() {
|
|
|
21378
22215
|
}
|
|
21379
22216
|
let migrated = 0;
|
|
21380
22217
|
for (const file2 of yamlFiles) {
|
|
21381
|
-
const yamlPath =
|
|
22218
|
+
const yamlPath = path15.join(skillsDir, file2);
|
|
21382
22219
|
const baseName = file2.replace(/\.(yaml|yml)$/, "");
|
|
21383
|
-
const outputDir =
|
|
21384
|
-
if (
|
|
22220
|
+
const outputDir = path15.join(skillsDir, baseName);
|
|
22221
|
+
if (fs14.existsSync(outputDir)) {
|
|
21385
22222
|
console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
|
|
21386
22223
|
continue;
|
|
21387
22224
|
}
|
|
21388
22225
|
try {
|
|
21389
22226
|
migrateYamlToSkillMd(yamlPath, outputDir);
|
|
21390
|
-
|
|
22227
|
+
fs14.renameSync(yamlPath, `${yamlPath}.bak`);
|
|
21391
22228
|
console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
|
|
21392
22229
|
migrated++;
|
|
21393
22230
|
} catch (err) {
|
|
@@ -21401,35 +22238,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
|
|
|
21401
22238
|
}
|
|
21402
22239
|
|
|
21403
22240
|
// src/cli/commands/import.ts
|
|
21404
|
-
import * as
|
|
21405
|
-
import * as
|
|
22241
|
+
import * as fs17 from "fs";
|
|
22242
|
+
import * as path18 from "path";
|
|
21406
22243
|
import chalk11 from "chalk";
|
|
21407
22244
|
|
|
21408
22245
|
// src/import/engine.ts
|
|
21409
|
-
import * as
|
|
21410
|
-
import * as
|
|
22246
|
+
import * as fs16 from "fs";
|
|
22247
|
+
import * as path17 from "path";
|
|
21411
22248
|
import matter5 from "gray-matter";
|
|
21412
22249
|
|
|
21413
22250
|
// src/import/classifier.ts
|
|
21414
|
-
import * as
|
|
21415
|
-
import * as
|
|
22251
|
+
import * as fs15 from "fs";
|
|
22252
|
+
import * as path16 from "path";
|
|
21416
22253
|
import matter4 from "gray-matter";
|
|
21417
22254
|
var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
|
|
21418
22255
|
var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
|
|
21419
22256
|
var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
|
|
21420
22257
|
function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
21421
|
-
const resolved =
|
|
21422
|
-
const stat =
|
|
22258
|
+
const resolved = path16.resolve(inputPath);
|
|
22259
|
+
const stat = fs15.statSync(resolved);
|
|
21423
22260
|
if (!stat.isDirectory()) {
|
|
21424
22261
|
return classifyFile(resolved, knownTypes);
|
|
21425
22262
|
}
|
|
21426
|
-
if (
|
|
22263
|
+
if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
|
|
21427
22264
|
return { type: "marvin-project", inputPath: resolved };
|
|
21428
22265
|
}
|
|
21429
22266
|
const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
|
|
21430
|
-
const entries =
|
|
22267
|
+
const entries = fs15.readdirSync(resolved);
|
|
21431
22268
|
const hasDocSubdirs = entries.some(
|
|
21432
|
-
(e) => allDirNames.has(e) &&
|
|
22269
|
+
(e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
|
|
21433
22270
|
);
|
|
21434
22271
|
if (hasDocSubdirs) {
|
|
21435
22272
|
return { type: "docs-directory", inputPath: resolved };
|
|
@@ -21438,7 +22275,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
|
21438
22275
|
if (mdFiles.length > 0) {
|
|
21439
22276
|
const hasMarvinDocs = mdFiles.some((f) => {
|
|
21440
22277
|
try {
|
|
21441
|
-
const raw =
|
|
22278
|
+
const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
|
|
21442
22279
|
const { data } = matter4(raw);
|
|
21443
22280
|
return isValidMarvinDocument(data, knownTypes);
|
|
21444
22281
|
} catch {
|
|
@@ -21452,14 +22289,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
|
21452
22289
|
return { type: "raw-source-dir", inputPath: resolved };
|
|
21453
22290
|
}
|
|
21454
22291
|
function classifyFile(filePath, knownTypes) {
|
|
21455
|
-
const resolved =
|
|
21456
|
-
const ext =
|
|
22292
|
+
const resolved = path16.resolve(filePath);
|
|
22293
|
+
const ext = path16.extname(resolved).toLowerCase();
|
|
21457
22294
|
if (RAW_SOURCE_EXTENSIONS.has(ext)) {
|
|
21458
22295
|
return { type: "raw-source-file", inputPath: resolved };
|
|
21459
22296
|
}
|
|
21460
22297
|
if (ext === ".md") {
|
|
21461
22298
|
try {
|
|
21462
|
-
const raw =
|
|
22299
|
+
const raw = fs15.readFileSync(resolved, "utf-8");
|
|
21463
22300
|
const { data } = matter4(raw);
|
|
21464
22301
|
if (isValidMarvinDocument(data, knownTypes)) {
|
|
21465
22302
|
return { type: "marvin-document", inputPath: resolved };
|
|
@@ -21584,9 +22421,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
21584
22421
|
continue;
|
|
21585
22422
|
}
|
|
21586
22423
|
if (item.action === "copy") {
|
|
21587
|
-
const targetDir =
|
|
21588
|
-
|
|
21589
|
-
|
|
22424
|
+
const targetDir = path17.dirname(item.targetPath);
|
|
22425
|
+
fs16.mkdirSync(targetDir, { recursive: true });
|
|
22426
|
+
fs16.copyFileSync(item.sourcePath, item.targetPath);
|
|
21590
22427
|
copied++;
|
|
21591
22428
|
continue;
|
|
21592
22429
|
}
|
|
@@ -21622,19 +22459,19 @@ function formatPlanSummary(plan) {
|
|
|
21622
22459
|
lines.push(`Documents to import: ${imports.length}`);
|
|
21623
22460
|
for (const item of imports) {
|
|
21624
22461
|
const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
|
|
21625
|
-
lines.push(` ${idInfo} ${
|
|
22462
|
+
lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
|
|
21626
22463
|
}
|
|
21627
22464
|
}
|
|
21628
22465
|
if (copies.length > 0) {
|
|
21629
22466
|
lines.push(`Files to copy to sources/: ${copies.length}`);
|
|
21630
22467
|
for (const item of copies) {
|
|
21631
|
-
lines.push(` ${
|
|
22468
|
+
lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
|
|
21632
22469
|
}
|
|
21633
22470
|
}
|
|
21634
22471
|
if (skips.length > 0) {
|
|
21635
22472
|
lines.push(`Skipped (conflict): ${skips.length}`);
|
|
21636
22473
|
for (const item of skips) {
|
|
21637
|
-
lines.push(` ${item.originalId ??
|
|
22474
|
+
lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
|
|
21638
22475
|
}
|
|
21639
22476
|
}
|
|
21640
22477
|
if (plan.items.length === 0) {
|
|
@@ -21667,11 +22504,11 @@ function getDirNameForType(store, type) {
|
|
|
21667
22504
|
}
|
|
21668
22505
|
function collectMarvinDocs(dir, knownTypes) {
|
|
21669
22506
|
const docs = [];
|
|
21670
|
-
const files =
|
|
22507
|
+
const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
21671
22508
|
for (const file2 of files) {
|
|
21672
|
-
const filePath =
|
|
22509
|
+
const filePath = path17.join(dir, file2);
|
|
21673
22510
|
try {
|
|
21674
|
-
const raw =
|
|
22511
|
+
const raw = fs16.readFileSync(filePath, "utf-8");
|
|
21675
22512
|
const { data, content } = matter5(raw);
|
|
21676
22513
|
if (isValidMarvinDocument(data, knownTypes)) {
|
|
21677
22514
|
docs.push({
|
|
@@ -21727,23 +22564,23 @@ function planDocImports(docs, store, options) {
|
|
|
21727
22564
|
}
|
|
21728
22565
|
function planFromMarvinProject(classification, store, _marvinDir, options) {
|
|
21729
22566
|
let projectDir = classification.inputPath;
|
|
21730
|
-
if (
|
|
21731
|
-
const inner =
|
|
21732
|
-
if (
|
|
22567
|
+
if (path17.basename(projectDir) !== ".marvin") {
|
|
22568
|
+
const inner = path17.join(projectDir, ".marvin");
|
|
22569
|
+
if (fs16.existsSync(inner)) {
|
|
21733
22570
|
projectDir = inner;
|
|
21734
22571
|
}
|
|
21735
22572
|
}
|
|
21736
|
-
const docsDir =
|
|
21737
|
-
if (!
|
|
22573
|
+
const docsDir = path17.join(projectDir, "docs");
|
|
22574
|
+
if (!fs16.existsSync(docsDir)) {
|
|
21738
22575
|
return [];
|
|
21739
22576
|
}
|
|
21740
22577
|
const knownTypes = store.registeredTypes;
|
|
21741
22578
|
const allDocs = [];
|
|
21742
|
-
const subdirs =
|
|
21743
|
-
(d) =>
|
|
22579
|
+
const subdirs = fs16.readdirSync(docsDir).filter(
|
|
22580
|
+
(d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
|
|
21744
22581
|
);
|
|
21745
22582
|
for (const subdir of subdirs) {
|
|
21746
|
-
const docs = collectMarvinDocs(
|
|
22583
|
+
const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
|
|
21747
22584
|
allDocs.push(...docs);
|
|
21748
22585
|
}
|
|
21749
22586
|
return planDocImports(allDocs, store, options);
|
|
@@ -21753,10 +22590,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
|
|
|
21753
22590
|
const knownTypes = store.registeredTypes;
|
|
21754
22591
|
const allDocs = [];
|
|
21755
22592
|
allDocs.push(...collectMarvinDocs(dir, knownTypes));
|
|
21756
|
-
const entries =
|
|
22593
|
+
const entries = fs16.readdirSync(dir);
|
|
21757
22594
|
for (const entry of entries) {
|
|
21758
|
-
const entryPath =
|
|
21759
|
-
if (
|
|
22595
|
+
const entryPath = path17.join(dir, entry);
|
|
22596
|
+
if (fs16.statSync(entryPath).isDirectory()) {
|
|
21760
22597
|
allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
|
|
21761
22598
|
}
|
|
21762
22599
|
}
|
|
@@ -21765,7 +22602,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
|
|
|
21765
22602
|
function planFromSingleDocument(classification, store, _marvinDir, options) {
|
|
21766
22603
|
const filePath = classification.inputPath;
|
|
21767
22604
|
const knownTypes = store.registeredTypes;
|
|
21768
|
-
const raw =
|
|
22605
|
+
const raw = fs16.readFileSync(filePath, "utf-8");
|
|
21769
22606
|
const { data, content } = matter5(raw);
|
|
21770
22607
|
if (!isValidMarvinDocument(data, knownTypes)) {
|
|
21771
22608
|
return [];
|
|
@@ -21781,14 +22618,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
|
|
|
21781
22618
|
}
|
|
21782
22619
|
function planFromRawSourceDir(classification, marvinDir) {
|
|
21783
22620
|
const dir = classification.inputPath;
|
|
21784
|
-
const sourcesDir =
|
|
22621
|
+
const sourcesDir = path17.join(marvinDir, "sources");
|
|
21785
22622
|
const items = [];
|
|
21786
|
-
const files =
|
|
21787
|
-
const stat =
|
|
22623
|
+
const files = fs16.readdirSync(dir).filter((f) => {
|
|
22624
|
+
const stat = fs16.statSync(path17.join(dir, f));
|
|
21788
22625
|
return stat.isFile();
|
|
21789
22626
|
});
|
|
21790
22627
|
for (const file2 of files) {
|
|
21791
|
-
const sourcePath =
|
|
22628
|
+
const sourcePath = path17.join(dir, file2);
|
|
21792
22629
|
const targetPath = resolveSourceFileName(sourcesDir, file2);
|
|
21793
22630
|
items.push({
|
|
21794
22631
|
action: "copy",
|
|
@@ -21799,8 +22636,8 @@ function planFromRawSourceDir(classification, marvinDir) {
|
|
|
21799
22636
|
return items;
|
|
21800
22637
|
}
|
|
21801
22638
|
function planFromRawSourceFile(classification, marvinDir) {
|
|
21802
|
-
const sourcesDir =
|
|
21803
|
-
const fileName =
|
|
22639
|
+
const sourcesDir = path17.join(marvinDir, "sources");
|
|
22640
|
+
const fileName = path17.basename(classification.inputPath);
|
|
21804
22641
|
const targetPath = resolveSourceFileName(sourcesDir, fileName);
|
|
21805
22642
|
return [
|
|
21806
22643
|
{
|
|
@@ -21811,25 +22648,25 @@ function planFromRawSourceFile(classification, marvinDir) {
|
|
|
21811
22648
|
];
|
|
21812
22649
|
}
|
|
21813
22650
|
function resolveSourceFileName(sourcesDir, fileName) {
|
|
21814
|
-
const targetPath =
|
|
21815
|
-
if (!
|
|
22651
|
+
const targetPath = path17.join(sourcesDir, fileName);
|
|
22652
|
+
if (!fs16.existsSync(targetPath)) {
|
|
21816
22653
|
return targetPath;
|
|
21817
22654
|
}
|
|
21818
|
-
const ext =
|
|
21819
|
-
const base =
|
|
22655
|
+
const ext = path17.extname(fileName);
|
|
22656
|
+
const base = path17.basename(fileName, ext);
|
|
21820
22657
|
let counter = 1;
|
|
21821
22658
|
let candidate;
|
|
21822
22659
|
do {
|
|
21823
|
-
candidate =
|
|
22660
|
+
candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
|
|
21824
22661
|
counter++;
|
|
21825
|
-
} while (
|
|
22662
|
+
} while (fs16.existsSync(candidate));
|
|
21826
22663
|
return candidate;
|
|
21827
22664
|
}
|
|
21828
22665
|
|
|
21829
22666
|
// src/cli/commands/import.ts
|
|
21830
22667
|
async function importCommand(inputPath, options) {
|
|
21831
|
-
const resolved =
|
|
21832
|
-
if (!
|
|
22668
|
+
const resolved = path18.resolve(inputPath);
|
|
22669
|
+
if (!fs17.existsSync(resolved)) {
|
|
21833
22670
|
throw new ImportError(`Path not found: ${resolved}`);
|
|
21834
22671
|
}
|
|
21835
22672
|
const project = loadProject();
|
|
@@ -21881,7 +22718,7 @@ async function importCommand(inputPath, options) {
|
|
|
21881
22718
|
console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
|
|
21882
22719
|
const manifest = new SourceManifestManager(marvinDir);
|
|
21883
22720
|
manifest.scan();
|
|
21884
|
-
const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) =>
|
|
22721
|
+
const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
|
|
21885
22722
|
for (const fileName of copiedFileNames) {
|
|
21886
22723
|
try {
|
|
21887
22724
|
await ingestFile({
|
|
@@ -22284,7 +23121,8 @@ The contributor is identifying a project risk.
|
|
|
22284
23121
|
- Create actions for risk mitigation tasks
|
|
22285
23122
|
- Create decisions for risk response strategies
|
|
22286
23123
|
- Create questions for risks needing further assessment
|
|
22287
|
-
- Tag all related artifacts with "risk" for tracking
|
|
23124
|
+
- Tag all related artifacts with "risk" for tracking
|
|
23125
|
+
- When a risk is resolved, use the update tool to remove the "risk" tag and add "risk-mitigated" so it no longer inflates the GAR quality metric`,
|
|
22288
23126
|
"blocker-report": `
|
|
22289
23127
|
### Type-Specific Guidance: Blocker Report
|
|
22290
23128
|
The contributor is reporting a blocker.
|
|
@@ -22629,6 +23467,105 @@ function renderConfluence(report) {
|
|
|
22629
23467
|
return lines.join("\n");
|
|
22630
23468
|
}
|
|
22631
23469
|
|
|
23470
|
+
// src/reports/health/render-ascii.ts
|
|
23471
|
+
import chalk17 from "chalk";
|
|
23472
|
+
var STATUS_DOT2 = {
|
|
23473
|
+
green: chalk17.green("\u25CF"),
|
|
23474
|
+
amber: chalk17.yellow("\u25CF"),
|
|
23475
|
+
red: chalk17.red("\u25CF")
|
|
23476
|
+
};
|
|
23477
|
+
var STATUS_LABEL2 = {
|
|
23478
|
+
green: chalk17.green.bold("GREEN"),
|
|
23479
|
+
amber: chalk17.yellow.bold("AMBER"),
|
|
23480
|
+
red: chalk17.red.bold("RED")
|
|
23481
|
+
};
|
|
23482
|
+
var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
|
|
23483
|
+
function renderAscii2(report) {
|
|
23484
|
+
const lines = [];
|
|
23485
|
+
lines.push("");
|
|
23486
|
+
lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
|
|
23487
|
+
lines.push(chalk17.dim(` ${report.generatedAt}`));
|
|
23488
|
+
lines.push("");
|
|
23489
|
+
lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
|
|
23490
|
+
lines.push("");
|
|
23491
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23492
|
+
lines.push(chalk17.bold(" Completeness"));
|
|
23493
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23494
|
+
for (const cat of report.completeness) {
|
|
23495
|
+
lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
|
|
23496
|
+
for (const item of cat.items) {
|
|
23497
|
+
lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
|
|
23498
|
+
}
|
|
23499
|
+
}
|
|
23500
|
+
lines.push("");
|
|
23501
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23502
|
+
lines.push(chalk17.bold(" Process"));
|
|
23503
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23504
|
+
for (const cat of report.process) {
|
|
23505
|
+
lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
|
|
23506
|
+
for (const item of cat.items) {
|
|
23507
|
+
lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
|
|
23508
|
+
}
|
|
23509
|
+
}
|
|
23510
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23511
|
+
lines.push("");
|
|
23512
|
+
return lines.join("\n");
|
|
23513
|
+
}
|
|
23514
|
+
|
|
23515
|
+
// src/reports/health/render-confluence.ts
|
|
23516
|
+
var EMOJI2 = {
|
|
23517
|
+
green: ":green_circle:",
|
|
23518
|
+
amber: ":yellow_circle:",
|
|
23519
|
+
red: ":red_circle:"
|
|
23520
|
+
};
|
|
23521
|
+
function renderConfluence2(report) {
|
|
23522
|
+
const lines = [];
|
|
23523
|
+
lines.push(`# Health Check \u2014 ${report.projectName}`);
|
|
23524
|
+
lines.push("");
|
|
23525
|
+
lines.push(`**Date:** ${report.generatedAt}`);
|
|
23526
|
+
lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
|
|
23527
|
+
lines.push("");
|
|
23528
|
+
lines.push("## Completeness");
|
|
23529
|
+
lines.push("");
|
|
23530
|
+
lines.push("| Category | Status | Summary |");
|
|
23531
|
+
lines.push("|----------|--------|---------|");
|
|
23532
|
+
for (const cat of report.completeness) {
|
|
23533
|
+
lines.push(
|
|
23534
|
+
`| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
|
|
23535
|
+
);
|
|
23536
|
+
}
|
|
23537
|
+
lines.push("");
|
|
23538
|
+
for (const cat of report.completeness) {
|
|
23539
|
+
if (cat.items.length === 0) continue;
|
|
23540
|
+
lines.push(`### ${cat.name}`);
|
|
23541
|
+
lines.push("");
|
|
23542
|
+
for (const item of cat.items) {
|
|
23543
|
+
lines.push(`- **${item.id}** ${item.detail}`);
|
|
23544
|
+
}
|
|
23545
|
+
lines.push("");
|
|
23546
|
+
}
|
|
23547
|
+
lines.push("## Process");
|
|
23548
|
+
lines.push("");
|
|
23549
|
+
lines.push("| Metric | Status | Summary |");
|
|
23550
|
+
lines.push("|--------|--------|---------|");
|
|
23551
|
+
for (const cat of report.process) {
|
|
23552
|
+
lines.push(
|
|
23553
|
+
`| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
|
|
23554
|
+
);
|
|
23555
|
+
}
|
|
23556
|
+
lines.push("");
|
|
23557
|
+
for (const cat of report.process) {
|
|
23558
|
+
if (cat.items.length === 0) continue;
|
|
23559
|
+
lines.push(`### ${cat.name}`);
|
|
23560
|
+
lines.push("");
|
|
23561
|
+
for (const item of cat.items) {
|
|
23562
|
+
lines.push(`- **${item.id}** ${item.detail}`);
|
|
23563
|
+
}
|
|
23564
|
+
lines.push("");
|
|
23565
|
+
}
|
|
23566
|
+
return lines.join("\n");
|
|
23567
|
+
}
|
|
23568
|
+
|
|
22632
23569
|
// src/cli/commands/report.ts
|
|
22633
23570
|
async function garReportCommand(options) {
|
|
22634
23571
|
const project = loadProject();
|
|
@@ -22644,6 +23581,20 @@ async function garReportCommand(options) {
|
|
|
22644
23581
|
console.log(renderAscii(report));
|
|
22645
23582
|
}
|
|
22646
23583
|
}
|
|
23584
|
+
async function healthReportCommand(options) {
|
|
23585
|
+
const project = loadProject();
|
|
23586
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
23587
|
+
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
23588
|
+
const store = new DocumentStore(project.marvinDir, registrations);
|
|
23589
|
+
const metrics = collectHealthMetrics(store);
|
|
23590
|
+
const report = evaluateHealth(project.config.name, metrics);
|
|
23591
|
+
const format = options.format ?? "ascii";
|
|
23592
|
+
if (format === "confluence") {
|
|
23593
|
+
console.log(renderConfluence2(report));
|
|
23594
|
+
} else {
|
|
23595
|
+
console.log(renderAscii2(report));
|
|
23596
|
+
}
|
|
23597
|
+
}
|
|
22647
23598
|
|
|
22648
23599
|
// src/cli/commands/web.ts
|
|
22649
23600
|
async function webCommand(options) {
|
|
@@ -22655,12 +23606,38 @@ async function webCommand(options) {
|
|
|
22655
23606
|
await startWebServer({ port, open: options.open });
|
|
22656
23607
|
}
|
|
22657
23608
|
|
|
23609
|
+
// src/cli/commands/generate.ts
|
|
23610
|
+
import * as fs18 from "fs";
|
|
23611
|
+
import * as path19 from "path";
|
|
23612
|
+
import chalk18 from "chalk";
|
|
23613
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
23614
|
+
async function generateClaudeMdCommand(options) {
|
|
23615
|
+
const project = loadProject();
|
|
23616
|
+
const filePath = path19.join(project.marvinDir, "CLAUDE.md");
|
|
23617
|
+
if (fs18.existsSync(filePath) && !options.force) {
|
|
23618
|
+
const overwrite = await confirm2({
|
|
23619
|
+
message: ".marvin/CLAUDE.md already exists. Overwrite?",
|
|
23620
|
+
default: false
|
|
23621
|
+
});
|
|
23622
|
+
if (!overwrite) {
|
|
23623
|
+
console.log(chalk18.dim("Aborted."));
|
|
23624
|
+
return;
|
|
23625
|
+
}
|
|
23626
|
+
}
|
|
23627
|
+
fs18.writeFileSync(
|
|
23628
|
+
filePath,
|
|
23629
|
+
getDefaultClaudeMdContent(project.config.name),
|
|
23630
|
+
"utf-8"
|
|
23631
|
+
);
|
|
23632
|
+
console.log(chalk18.green("Created .marvin/CLAUDE.md"));
|
|
23633
|
+
}
|
|
23634
|
+
|
|
22658
23635
|
// src/cli/program.ts
|
|
22659
23636
|
function createProgram() {
|
|
22660
23637
|
const program2 = new Command();
|
|
22661
23638
|
program2.name("marvin").description(
|
|
22662
23639
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
22663
|
-
).version("0.3.
|
|
23640
|
+
).version("0.3.6");
|
|
22664
23641
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
22665
23642
|
await initCommand();
|
|
22666
23643
|
});
|
|
@@ -22737,9 +23714,19 @@ function createProgram() {
|
|
|
22737
23714
|
).action(async (options) => {
|
|
22738
23715
|
await garReportCommand(options);
|
|
22739
23716
|
});
|
|
23717
|
+
reportCmd.command("health").description("Generate a governance health check report").option(
|
|
23718
|
+
"--format <format>",
|
|
23719
|
+
"Output format: ascii or confluence (default: ascii)"
|
|
23720
|
+
).action(async (options) => {
|
|
23721
|
+
await healthReportCommand(options);
|
|
23722
|
+
});
|
|
22740
23723
|
program2.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
|
|
22741
23724
|
await webCommand(options);
|
|
22742
23725
|
});
|
|
23726
|
+
const generateCmd = program2.command("generate").description("Generate project files");
|
|
23727
|
+
generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
|
|
23728
|
+
await generateClaudeMdCommand(options);
|
|
23729
|
+
});
|
|
22743
23730
|
return program2;
|
|
22744
23731
|
}
|
|
22745
23732
|
|