mrvn-cli 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +303 -147
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8006 -6962
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1994 -75
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +5576 -4530
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -6474,13 +6474,13 @@ var error16 = () => {
|
|
|
6474
6474
|
// no unit
|
|
6475
6475
|
};
|
|
6476
6476
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6477
|
-
const
|
|
6477
|
+
const typeLabel2 = (t) => {
|
|
6478
6478
|
const e = typeEntry(t);
|
|
6479
6479
|
if (e)
|
|
6480
6480
|
return e.label;
|
|
6481
6481
|
return t ?? TypeNames.unknown.label;
|
|
6482
6482
|
};
|
|
6483
|
-
const withDefinite = (t) => `\u05D4${
|
|
6483
|
+
const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
|
|
6484
6484
|
const verbFor = (t) => {
|
|
6485
6485
|
const e = typeEntry(t);
|
|
6486
6486
|
const gender = e?.gender ?? "m";
|
|
@@ -6530,7 +6530,7 @@ var error16 = () => {
|
|
|
6530
6530
|
switch (issue2.code) {
|
|
6531
6531
|
case "invalid_type": {
|
|
6532
6532
|
const expectedKey = issue2.expected;
|
|
6533
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6533
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
|
|
6534
6534
|
const receivedType = parsedType(issue2.input);
|
|
6535
6535
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6536
6536
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -14227,7 +14227,7 @@ function createDecisionTools(store) {
|
|
|
14227
14227
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14228
14228
|
};
|
|
14229
14229
|
},
|
|
14230
|
-
{ annotations: {
|
|
14230
|
+
{ annotations: { readOnlyHint: true } }
|
|
14231
14231
|
),
|
|
14232
14232
|
tool(
|
|
14233
14233
|
"get_decision",
|
|
@@ -14254,7 +14254,7 @@ function createDecisionTools(store) {
|
|
|
14254
14254
|
]
|
|
14255
14255
|
};
|
|
14256
14256
|
},
|
|
14257
|
-
{ annotations: {
|
|
14257
|
+
{ annotations: { readOnlyHint: true } }
|
|
14258
14258
|
),
|
|
14259
14259
|
tool(
|
|
14260
14260
|
"create_decision",
|
|
@@ -14315,6 +14315,19 @@ function createDecisionTools(store) {
|
|
|
14315
14315
|
|
|
14316
14316
|
// src/agent/tools/actions.ts
|
|
14317
14317
|
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
14318
|
+
function findMatchingSprints(store, dueDate) {
|
|
14319
|
+
const sprints = store.list({ type: "sprint" });
|
|
14320
|
+
return sprints.filter((s) => {
|
|
14321
|
+
const start = s.frontmatter.startDate;
|
|
14322
|
+
const end = s.frontmatter.endDate;
|
|
14323
|
+
return start && end && dueDate >= start && dueDate <= end;
|
|
14324
|
+
}).map((s) => ({
|
|
14325
|
+
id: s.frontmatter.id,
|
|
14326
|
+
title: s.frontmatter.title,
|
|
14327
|
+
startDate: s.frontmatter.startDate,
|
|
14328
|
+
endDate: s.frontmatter.endDate
|
|
14329
|
+
}));
|
|
14330
|
+
}
|
|
14318
14331
|
function createActionTools(store) {
|
|
14319
14332
|
return [
|
|
14320
14333
|
tool2(
|
|
@@ -14330,19 +14343,24 @@ function createActionTools(store) {
|
|
|
14330
14343
|
status: args.status,
|
|
14331
14344
|
owner: args.owner
|
|
14332
14345
|
});
|
|
14333
|
-
const summary = docs.map((d) =>
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14346
|
+
const summary = docs.map((d) => {
|
|
14347
|
+
const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
|
|
14348
|
+
return {
|
|
14349
|
+
id: d.frontmatter.id,
|
|
14350
|
+
title: d.frontmatter.title,
|
|
14351
|
+
status: d.frontmatter.status,
|
|
14352
|
+
owner: d.frontmatter.owner,
|
|
14353
|
+
priority: d.frontmatter.priority,
|
|
14354
|
+
dueDate: d.frontmatter.dueDate,
|
|
14355
|
+
sprints: sprintIds.length > 0 ? sprintIds : void 0,
|
|
14356
|
+
created: d.frontmatter.created
|
|
14357
|
+
};
|
|
14358
|
+
});
|
|
14341
14359
|
return {
|
|
14342
14360
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14343
14361
|
};
|
|
14344
14362
|
},
|
|
14345
|
-
{ annotations: {
|
|
14363
|
+
{ annotations: { readOnlyHint: true } }
|
|
14346
14364
|
),
|
|
14347
14365
|
tool2(
|
|
14348
14366
|
"get_action",
|
|
@@ -14369,7 +14387,7 @@ function createActionTools(store) {
|
|
|
14369
14387
|
]
|
|
14370
14388
|
};
|
|
14371
14389
|
},
|
|
14372
|
-
{ annotations: {
|
|
14390
|
+
{ annotations: { readOnlyHint: true } }
|
|
14373
14391
|
),
|
|
14374
14392
|
tool2(
|
|
14375
14393
|
"create_action",
|
|
@@ -14380,9 +14398,18 @@ function createActionTools(store) {
|
|
|
14380
14398
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14381
14399
|
owner: external_exports.string().optional().describe("Person responsible"),
|
|
14382
14400
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
14383
|
-
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14401
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
14402
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14403
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
|
|
14384
14404
|
},
|
|
14385
14405
|
async (args) => {
|
|
14406
|
+
const tags = [...args.tags ?? []];
|
|
14407
|
+
if (args.sprints) {
|
|
14408
|
+
for (const sprintId of args.sprints) {
|
|
14409
|
+
const tag = `sprint:${sprintId}`;
|
|
14410
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
14411
|
+
}
|
|
14412
|
+
}
|
|
14386
14413
|
const doc = store.create(
|
|
14387
14414
|
"action",
|
|
14388
14415
|
{
|
|
@@ -14390,17 +14417,21 @@ function createActionTools(store) {
|
|
|
14390
14417
|
status: args.status,
|
|
14391
14418
|
owner: args.owner,
|
|
14392
14419
|
priority: args.priority,
|
|
14393
|
-
tags:
|
|
14420
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
14421
|
+
dueDate: args.dueDate
|
|
14394
14422
|
},
|
|
14395
14423
|
args.content
|
|
14396
14424
|
);
|
|
14425
|
+
const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
|
|
14426
|
+
if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
|
|
14427
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
14428
|
+
if (matching.length > 0) {
|
|
14429
|
+
const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
|
|
14430
|
+
parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
|
|
14431
|
+
}
|
|
14432
|
+
}
|
|
14397
14433
|
return {
|
|
14398
|
-
content: [
|
|
14399
|
-
{
|
|
14400
|
-
type: "text",
|
|
14401
|
-
text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
|
|
14402
|
-
}
|
|
14403
|
-
]
|
|
14434
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
14404
14435
|
};
|
|
14405
14436
|
}
|
|
14406
14437
|
),
|
|
@@ -14413,10 +14444,25 @@ function createActionTools(store) {
|
|
|
14413
14444
|
status: external_exports.string().optional().describe("New status"),
|
|
14414
14445
|
content: external_exports.string().optional().describe("New content"),
|
|
14415
14446
|
owner: external_exports.string().optional().describe("New owner"),
|
|
14416
|
-
priority: external_exports.string().optional().describe("New priority")
|
|
14447
|
+
priority: external_exports.string().optional().describe("New priority"),
|
|
14448
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14449
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
|
|
14417
14450
|
},
|
|
14418
14451
|
async (args) => {
|
|
14419
|
-
const { id, content, ...updates } = args;
|
|
14452
|
+
const { id, content, sprints, ...updates } = args;
|
|
14453
|
+
if (sprints !== void 0) {
|
|
14454
|
+
const existing = store.get(id);
|
|
14455
|
+
if (!existing) {
|
|
14456
|
+
return {
|
|
14457
|
+
content: [{ type: "text", text: `Action ${id} not found` }],
|
|
14458
|
+
isError: true
|
|
14459
|
+
};
|
|
14460
|
+
}
|
|
14461
|
+
const existingTags = existing.frontmatter.tags ?? [];
|
|
14462
|
+
const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
14463
|
+
const newSprintTags = sprints.map((s) => `sprint:${s}`);
|
|
14464
|
+
updates.tags = [...nonSprintTags, ...newSprintTags];
|
|
14465
|
+
}
|
|
14420
14466
|
const doc = store.update(id, updates, content);
|
|
14421
14467
|
return {
|
|
14422
14468
|
content: [
|
|
@@ -14427,6 +14473,35 @@ function createActionTools(store) {
|
|
|
14427
14473
|
]
|
|
14428
14474
|
};
|
|
14429
14475
|
}
|
|
14476
|
+
),
|
|
14477
|
+
tool2(
|
|
14478
|
+
"suggest_sprints_for_action",
|
|
14479
|
+
"Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
|
|
14480
|
+
{
|
|
14481
|
+
dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
|
|
14482
|
+
},
|
|
14483
|
+
async (args) => {
|
|
14484
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
14485
|
+
if (matching.length === 0) {
|
|
14486
|
+
return {
|
|
14487
|
+
content: [
|
|
14488
|
+
{
|
|
14489
|
+
type: "text",
|
|
14490
|
+
text: `No sprints found containing dueDate ${args.dueDate}.`
|
|
14491
|
+
}
|
|
14492
|
+
]
|
|
14493
|
+
};
|
|
14494
|
+
}
|
|
14495
|
+
return {
|
|
14496
|
+
content: [
|
|
14497
|
+
{
|
|
14498
|
+
type: "text",
|
|
14499
|
+
text: JSON.stringify(matching, null, 2)
|
|
14500
|
+
}
|
|
14501
|
+
]
|
|
14502
|
+
};
|
|
14503
|
+
},
|
|
14504
|
+
{ annotations: { readOnlyHint: true } }
|
|
14430
14505
|
)
|
|
14431
14506
|
];
|
|
14432
14507
|
}
|
|
@@ -14454,7 +14529,7 @@ function createQuestionTools(store) {
|
|
|
14454
14529
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14455
14530
|
};
|
|
14456
14531
|
},
|
|
14457
|
-
{ annotations: {
|
|
14532
|
+
{ annotations: { readOnlyHint: true } }
|
|
14458
14533
|
),
|
|
14459
14534
|
tool3(
|
|
14460
14535
|
"get_question",
|
|
@@ -14481,7 +14556,7 @@ function createQuestionTools(store) {
|
|
|
14481
14556
|
]
|
|
14482
14557
|
};
|
|
14483
14558
|
},
|
|
14484
|
-
{ annotations: {
|
|
14559
|
+
{ annotations: { readOnlyHint: true } }
|
|
14485
14560
|
),
|
|
14486
14561
|
tool3(
|
|
14487
14562
|
"create_question",
|
|
@@ -14574,7 +14649,7 @@ function createDocumentTools(store) {
|
|
|
14574
14649
|
]
|
|
14575
14650
|
};
|
|
14576
14651
|
},
|
|
14577
|
-
{ annotations: {
|
|
14652
|
+
{ annotations: { readOnlyHint: true } }
|
|
14578
14653
|
),
|
|
14579
14654
|
tool4(
|
|
14580
14655
|
"read_document",
|
|
@@ -14601,7 +14676,7 @@ function createDocumentTools(store) {
|
|
|
14601
14676
|
]
|
|
14602
14677
|
};
|
|
14603
14678
|
},
|
|
14604
|
-
{ annotations: {
|
|
14679
|
+
{ annotations: { readOnlyHint: true } }
|
|
14605
14680
|
),
|
|
14606
14681
|
tool4(
|
|
14607
14682
|
"project_summary",
|
|
@@ -14629,7 +14704,7 @@ function createDocumentTools(store) {
|
|
|
14629
14704
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14630
14705
|
};
|
|
14631
14706
|
},
|
|
14632
|
-
{ annotations: {
|
|
14707
|
+
{ annotations: { readOnlyHint: true } }
|
|
14633
14708
|
)
|
|
14634
14709
|
];
|
|
14635
14710
|
}
|
|
@@ -14666,7 +14741,7 @@ function createSourceTools(manifest) {
|
|
|
14666
14741
|
]
|
|
14667
14742
|
};
|
|
14668
14743
|
},
|
|
14669
|
-
{ annotations: {
|
|
14744
|
+
{ annotations: { readOnlyHint: true } }
|
|
14670
14745
|
),
|
|
14671
14746
|
tool5(
|
|
14672
14747
|
"get_source_info",
|
|
@@ -14700,7 +14775,7 @@ function createSourceTools(manifest) {
|
|
|
14700
14775
|
]
|
|
14701
14776
|
};
|
|
14702
14777
|
},
|
|
14703
|
-
{ annotations: {
|
|
14778
|
+
{ annotations: { readOnlyHint: true } }
|
|
14704
14779
|
)
|
|
14705
14780
|
];
|
|
14706
14781
|
}
|
|
@@ -14731,7 +14806,7 @@ function createSessionTools(store) {
|
|
|
14731
14806
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14732
14807
|
};
|
|
14733
14808
|
},
|
|
14734
|
-
{ annotations: {
|
|
14809
|
+
{ annotations: { readOnlyHint: true } }
|
|
14735
14810
|
),
|
|
14736
14811
|
tool6(
|
|
14737
14812
|
"get_session",
|
|
@@ -14749,7 +14824,7 @@ function createSessionTools(store) {
|
|
|
14749
14824
|
content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
|
|
14750
14825
|
};
|
|
14751
14826
|
},
|
|
14752
|
-
{ annotations: {
|
|
14827
|
+
{ annotations: { readOnlyHint: true } }
|
|
14753
14828
|
)
|
|
14754
14829
|
];
|
|
14755
14830
|
}
|
|
@@ -14842,7 +14917,7 @@ function createMeetingTools(store) {
|
|
|
14842
14917
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14843
14918
|
};
|
|
14844
14919
|
},
|
|
14845
|
-
{ annotations: {
|
|
14920
|
+
{ annotations: { readOnlyHint: true } }
|
|
14846
14921
|
),
|
|
14847
14922
|
tool7(
|
|
14848
14923
|
"get_meeting",
|
|
@@ -14869,7 +14944,7 @@ function createMeetingTools(store) {
|
|
|
14869
14944
|
]
|
|
14870
14945
|
};
|
|
14871
14946
|
},
|
|
14872
|
-
{ annotations: {
|
|
14947
|
+
{ annotations: { readOnlyHint: true } }
|
|
14873
14948
|
),
|
|
14874
14949
|
tool7(
|
|
14875
14950
|
"create_meeting",
|
|
@@ -14994,7 +15069,7 @@ function createMeetingTools(store) {
|
|
|
14994
15069
|
content: [{ type: "text", text: sections.join("\n") }]
|
|
14995
15070
|
};
|
|
14996
15071
|
},
|
|
14997
|
-
{ annotations: {
|
|
15072
|
+
{ annotations: { readOnlyHint: true } }
|
|
14998
15073
|
)
|
|
14999
15074
|
];
|
|
15000
15075
|
}
|
|
@@ -15011,9 +15086,17 @@ function collectGarMetrics(store) {
|
|
|
15011
15086
|
const blockedItems = allDocs.filter(
|
|
15012
15087
|
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15013
15088
|
);
|
|
15014
|
-
const
|
|
15089
|
+
const tagOverdueItems = allDocs.filter(
|
|
15015
15090
|
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15016
15091
|
);
|
|
15092
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15093
|
+
const dateOverdueActions = openActions.filter((d) => {
|
|
15094
|
+
const dueDate = d.frontmatter.dueDate;
|
|
15095
|
+
return typeof dueDate === "string" && dueDate < today;
|
|
15096
|
+
});
|
|
15097
|
+
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
15098
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15099
|
+
);
|
|
15017
15100
|
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15018
15101
|
const riskItems = allDocs.filter(
|
|
15019
15102
|
(d) => d.frontmatter.tags?.includes("risk")
|
|
@@ -15122,6 +15205,253 @@ function evaluateGar(projectName, metrics) {
|
|
|
15122
15205
|
};
|
|
15123
15206
|
}
|
|
15124
15207
|
|
|
15208
|
+
// src/reports/health/collector.ts
|
|
15209
|
+
var FIELD_CHECKS = [
|
|
15210
|
+
{
|
|
15211
|
+
type: "action",
|
|
15212
|
+
openStatuses: ["open", "in-progress"],
|
|
15213
|
+
requiredFields: ["owner", "priority", "dueDate", "content"]
|
|
15214
|
+
},
|
|
15215
|
+
{
|
|
15216
|
+
type: "decision",
|
|
15217
|
+
openStatuses: ["open", "proposed"],
|
|
15218
|
+
requiredFields: ["owner", "content"]
|
|
15219
|
+
},
|
|
15220
|
+
{
|
|
15221
|
+
type: "question",
|
|
15222
|
+
openStatuses: ["open"],
|
|
15223
|
+
requiredFields: ["owner", "content"]
|
|
15224
|
+
},
|
|
15225
|
+
{
|
|
15226
|
+
type: "feature",
|
|
15227
|
+
openStatuses: ["draft", "approved"],
|
|
15228
|
+
requiredFields: ["owner", "priority", "content"]
|
|
15229
|
+
},
|
|
15230
|
+
{
|
|
15231
|
+
type: "epic",
|
|
15232
|
+
openStatuses: ["planned", "in-progress"],
|
|
15233
|
+
requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
|
|
15234
|
+
},
|
|
15235
|
+
{
|
|
15236
|
+
type: "sprint",
|
|
15237
|
+
openStatuses: ["planned", "active"],
|
|
15238
|
+
requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
|
|
15239
|
+
}
|
|
15240
|
+
];
|
|
15241
|
+
var STALE_THRESHOLD_DAYS = 14;
|
|
15242
|
+
var AGING_THRESHOLD_DAYS = 30;
|
|
15243
|
+
function daysBetween(a, b) {
|
|
15244
|
+
const msPerDay = 864e5;
|
|
15245
|
+
const dateA = new Date(a);
|
|
15246
|
+
const dateB = new Date(b);
|
|
15247
|
+
return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
|
|
15248
|
+
}
|
|
15249
|
+
function checkMissingFields(doc, requiredFields) {
|
|
15250
|
+
const missing = [];
|
|
15251
|
+
for (const field of requiredFields) {
|
|
15252
|
+
if (field === "content") {
|
|
15253
|
+
if (!doc.content || doc.content.trim().length === 0) {
|
|
15254
|
+
missing.push("content");
|
|
15255
|
+
}
|
|
15256
|
+
} else if (field === "linkedEpics") {
|
|
15257
|
+
const val = doc.frontmatter[field];
|
|
15258
|
+
if (!Array.isArray(val) || val.length === 0) {
|
|
15259
|
+
missing.push(field);
|
|
15260
|
+
}
|
|
15261
|
+
} else {
|
|
15262
|
+
const val = doc.frontmatter[field];
|
|
15263
|
+
if (val === void 0 || val === null || val === "") {
|
|
15264
|
+
missing.push(field);
|
|
15265
|
+
}
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
return missing;
|
|
15269
|
+
}
|
|
15270
|
+
function collectCompleteness(store) {
|
|
15271
|
+
const result = {};
|
|
15272
|
+
for (const check2 of FIELD_CHECKS) {
|
|
15273
|
+
const allOfType = store.list({ type: check2.type });
|
|
15274
|
+
const openDocs = allOfType.filter(
|
|
15275
|
+
(d) => check2.openStatuses.includes(d.frontmatter.status)
|
|
15276
|
+
);
|
|
15277
|
+
const gaps = [];
|
|
15278
|
+
let complete = 0;
|
|
15279
|
+
for (const doc of openDocs) {
|
|
15280
|
+
const missingFields = checkMissingFields(doc, check2.requiredFields);
|
|
15281
|
+
if (missingFields.length === 0) {
|
|
15282
|
+
complete++;
|
|
15283
|
+
} else {
|
|
15284
|
+
gaps.push({
|
|
15285
|
+
id: doc.frontmatter.id,
|
|
15286
|
+
title: doc.frontmatter.title,
|
|
15287
|
+
missingFields
|
|
15288
|
+
});
|
|
15289
|
+
}
|
|
15290
|
+
}
|
|
15291
|
+
result[check2.type] = {
|
|
15292
|
+
total: openDocs.length,
|
|
15293
|
+
complete,
|
|
15294
|
+
gaps
|
|
15295
|
+
};
|
|
15296
|
+
}
|
|
15297
|
+
return result;
|
|
15298
|
+
}
|
|
15299
|
+
function collectProcess(store) {
|
|
15300
|
+
const today = (/* @__PURE__ */ new Date()).toISOString();
|
|
15301
|
+
const allDocs = store.list();
|
|
15302
|
+
const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
|
|
15303
|
+
const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
|
|
15304
|
+
const stale = [];
|
|
15305
|
+
for (const doc of openDocs) {
|
|
15306
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
15307
|
+
const days = daysBetween(updated, today);
|
|
15308
|
+
if (days >= STALE_THRESHOLD_DAYS) {
|
|
15309
|
+
stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
15310
|
+
}
|
|
15311
|
+
}
|
|
15312
|
+
const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
|
|
15313
|
+
const agingActions = [];
|
|
15314
|
+
for (const doc of openActions) {
|
|
15315
|
+
const days = daysBetween(doc.frontmatter.created, today);
|
|
15316
|
+
if (days >= AGING_THRESHOLD_DAYS) {
|
|
15317
|
+
agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
15318
|
+
}
|
|
15319
|
+
}
|
|
15320
|
+
const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
|
|
15321
|
+
let decisionTotal = 0;
|
|
15322
|
+
for (const doc of resolvedDecisions) {
|
|
15323
|
+
decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
15324
|
+
}
|
|
15325
|
+
const decisionVelocity = {
|
|
15326
|
+
avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
|
|
15327
|
+
count: resolvedDecisions.length
|
|
15328
|
+
};
|
|
15329
|
+
const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
|
|
15330
|
+
let questionTotal = 0;
|
|
15331
|
+
for (const doc of answeredQuestions) {
|
|
15332
|
+
questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
15333
|
+
}
|
|
15334
|
+
const questionResolution = {
|
|
15335
|
+
avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
|
|
15336
|
+
count: answeredQuestions.length
|
|
15337
|
+
};
|
|
15338
|
+
return { stale, agingActions, decisionVelocity, questionResolution };
|
|
15339
|
+
}
|
|
15340
|
+
function collectHealthMetrics(store) {
|
|
15341
|
+
return {
|
|
15342
|
+
completeness: collectCompleteness(store),
|
|
15343
|
+
process: collectProcess(store)
|
|
15344
|
+
};
|
|
15345
|
+
}
|
|
15346
|
+
|
|
15347
|
+
// src/reports/health/evaluator.ts
|
|
15348
|
+
function worstStatus2(statuses) {
|
|
15349
|
+
if (statuses.includes("red")) return "red";
|
|
15350
|
+
if (statuses.includes("amber")) return "amber";
|
|
15351
|
+
return "green";
|
|
15352
|
+
}
|
|
15353
|
+
function completenessStatus(total, complete) {
|
|
15354
|
+
if (total === 0) return "green";
|
|
15355
|
+
const pct = Math.round(complete / total * 100);
|
|
15356
|
+
if (pct >= 100) return "green";
|
|
15357
|
+
if (pct >= 75) return "amber";
|
|
15358
|
+
return "red";
|
|
15359
|
+
}
|
|
15360
|
+
var TYPE_LABELS = {
|
|
15361
|
+
action: "Actions",
|
|
15362
|
+
decision: "Decisions",
|
|
15363
|
+
question: "Questions",
|
|
15364
|
+
feature: "Features",
|
|
15365
|
+
epic: "Epics",
|
|
15366
|
+
sprint: "Sprints"
|
|
15367
|
+
};
|
|
15368
|
+
function evaluateHealth(projectName, metrics) {
|
|
15369
|
+
const completeness = [];
|
|
15370
|
+
for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
|
|
15371
|
+
const { total, complete, gaps } = catMetrics;
|
|
15372
|
+
const status = completenessStatus(total, complete);
|
|
15373
|
+
const pct = total > 0 ? Math.round(complete / total * 100) : 100;
|
|
15374
|
+
completeness.push({
|
|
15375
|
+
name: TYPE_LABELS[type] ?? type,
|
|
15376
|
+
status,
|
|
15377
|
+
summary: `${pct}% complete (${complete}/${total})`,
|
|
15378
|
+
items: gaps.map((g) => ({
|
|
15379
|
+
id: g.id,
|
|
15380
|
+
detail: `missing: ${g.missingFields.join(", ")}`
|
|
15381
|
+
}))
|
|
15382
|
+
});
|
|
15383
|
+
}
|
|
15384
|
+
const process3 = [];
|
|
15385
|
+
const staleCount = metrics.process.stale.length;
|
|
15386
|
+
const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
|
|
15387
|
+
process3.push({
|
|
15388
|
+
name: "Stale Items",
|
|
15389
|
+
status: staleStatus,
|
|
15390
|
+
summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
|
|
15391
|
+
items: metrics.process.stale.map((s) => ({
|
|
15392
|
+
id: s.id,
|
|
15393
|
+
detail: `${s.days} days since last update`
|
|
15394
|
+
}))
|
|
15395
|
+
});
|
|
15396
|
+
const agingCount = metrics.process.agingActions.length;
|
|
15397
|
+
const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
|
|
15398
|
+
process3.push({
|
|
15399
|
+
name: "Aging Actions",
|
|
15400
|
+
status: agingStatus,
|
|
15401
|
+
summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
|
|
15402
|
+
items: metrics.process.agingActions.map((a) => ({
|
|
15403
|
+
id: a.id,
|
|
15404
|
+
detail: `open for ${a.days} days`
|
|
15405
|
+
}))
|
|
15406
|
+
});
|
|
15407
|
+
const dv = metrics.process.decisionVelocity;
|
|
15408
|
+
let dvStatus;
|
|
15409
|
+
if (dv.count === 0) {
|
|
15410
|
+
dvStatus = "green";
|
|
15411
|
+
} else if (dv.avgDays <= 7) {
|
|
15412
|
+
dvStatus = "green";
|
|
15413
|
+
} else if (dv.avgDays <= 21) {
|
|
15414
|
+
dvStatus = "amber";
|
|
15415
|
+
} else {
|
|
15416
|
+
dvStatus = "red";
|
|
15417
|
+
}
|
|
15418
|
+
process3.push({
|
|
15419
|
+
name: "Decision Velocity",
|
|
15420
|
+
status: dvStatus,
|
|
15421
|
+
summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
|
|
15422
|
+
items: []
|
|
15423
|
+
});
|
|
15424
|
+
const qr = metrics.process.questionResolution;
|
|
15425
|
+
let qrStatus;
|
|
15426
|
+
if (qr.count === 0) {
|
|
15427
|
+
qrStatus = "green";
|
|
15428
|
+
} else if (qr.avgDays <= 7) {
|
|
15429
|
+
qrStatus = "green";
|
|
15430
|
+
} else if (qr.avgDays <= 14) {
|
|
15431
|
+
qrStatus = "amber";
|
|
15432
|
+
} else {
|
|
15433
|
+
qrStatus = "red";
|
|
15434
|
+
}
|
|
15435
|
+
process3.push({
|
|
15436
|
+
name: "Question Resolution",
|
|
15437
|
+
status: qrStatus,
|
|
15438
|
+
summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
|
|
15439
|
+
items: []
|
|
15440
|
+
});
|
|
15441
|
+
const allStatuses = [
|
|
15442
|
+
...completeness.map((c) => c.status),
|
|
15443
|
+
...process3.map((p) => p.status)
|
|
15444
|
+
];
|
|
15445
|
+
const overall = worstStatus2(allStatuses);
|
|
15446
|
+
return {
|
|
15447
|
+
projectName,
|
|
15448
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15449
|
+
overall,
|
|
15450
|
+
completeness,
|
|
15451
|
+
process: process3
|
|
15452
|
+
};
|
|
15453
|
+
}
|
|
15454
|
+
|
|
15125
15455
|
// src/plugins/builtin/tools/reports.ts
|
|
15126
15456
|
function createReportTools(store) {
|
|
15127
15457
|
return [
|
|
@@ -15141,7 +15471,8 @@ function createReportTools(store) {
|
|
|
15141
15471
|
id: d.frontmatter.id,
|
|
15142
15472
|
title: d.frontmatter.title,
|
|
15143
15473
|
owner: d.frontmatter.owner,
|
|
15144
|
-
priority: d.frontmatter.priority
|
|
15474
|
+
priority: d.frontmatter.priority,
|
|
15475
|
+
dueDate: d.frontmatter.dueDate
|
|
15145
15476
|
})),
|
|
15146
15477
|
completedActions: completedActions.map((d) => ({
|
|
15147
15478
|
id: d.frontmatter.id,
|
|
@@ -15160,7 +15491,7 @@ function createReportTools(store) {
|
|
|
15160
15491
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
15161
15492
|
};
|
|
15162
15493
|
},
|
|
15163
|
-
{ annotations: {
|
|
15494
|
+
{ annotations: { readOnlyHint: true } }
|
|
15164
15495
|
),
|
|
15165
15496
|
tool8(
|
|
15166
15497
|
"generate_risk_register",
|
|
@@ -15204,7 +15535,7 @@ function createReportTools(store) {
|
|
|
15204
15535
|
content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
|
|
15205
15536
|
};
|
|
15206
15537
|
},
|
|
15207
|
-
{ annotations: {
|
|
15538
|
+
{ annotations: { readOnlyHint: true } }
|
|
15208
15539
|
),
|
|
15209
15540
|
tool8(
|
|
15210
15541
|
"generate_gar_report",
|
|
@@ -15217,7 +15548,7 @@ function createReportTools(store) {
|
|
|
15217
15548
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
15218
15549
|
};
|
|
15219
15550
|
},
|
|
15220
|
-
{ annotations: {
|
|
15551
|
+
{ annotations: { readOnlyHint: true } }
|
|
15221
15552
|
),
|
|
15222
15553
|
tool8(
|
|
15223
15554
|
"generate_epic_progress",
|
|
@@ -15302,7 +15633,7 @@ function createReportTools(store) {
|
|
|
15302
15633
|
]
|
|
15303
15634
|
};
|
|
15304
15635
|
},
|
|
15305
|
-
{ annotations: {
|
|
15636
|
+
{ annotations: { readOnlyHint: true } }
|
|
15306
15637
|
),
|
|
15307
15638
|
tool8(
|
|
15308
15639
|
"generate_sprint_progress",
|
|
@@ -15347,7 +15678,8 @@ function createReportTools(store) {
|
|
|
15347
15678
|
id: d.frontmatter.id,
|
|
15348
15679
|
title: d.frontmatter.title,
|
|
15349
15680
|
type: d.frontmatter.type,
|
|
15350
|
-
status: d.frontmatter.status
|
|
15681
|
+
status: d.frontmatter.status,
|
|
15682
|
+
dueDate: d.frontmatter.dueDate
|
|
15351
15683
|
}))
|
|
15352
15684
|
}
|
|
15353
15685
|
};
|
|
@@ -15356,7 +15688,7 @@ function createReportTools(store) {
|
|
|
15356
15688
|
content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
|
|
15357
15689
|
};
|
|
15358
15690
|
},
|
|
15359
|
-
{ annotations: {
|
|
15691
|
+
{ annotations: { readOnlyHint: true } }
|
|
15360
15692
|
),
|
|
15361
15693
|
tool8(
|
|
15362
15694
|
"generate_feature_progress",
|
|
@@ -15396,7 +15728,20 @@ function createReportTools(store) {
|
|
|
15396
15728
|
content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
|
|
15397
15729
|
};
|
|
15398
15730
|
},
|
|
15399
|
-
{ annotations: {
|
|
15731
|
+
{ annotations: { readOnlyHint: true } }
|
|
15732
|
+
),
|
|
15733
|
+
tool8(
|
|
15734
|
+
"generate_health_report",
|
|
15735
|
+
"Generate a governance health check report covering artifact completeness and process health metrics",
|
|
15736
|
+
{},
|
|
15737
|
+
async () => {
|
|
15738
|
+
const metrics = collectHealthMetrics(store);
|
|
15739
|
+
const report = evaluateHealth("project", metrics);
|
|
15740
|
+
return {
|
|
15741
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
15742
|
+
};
|
|
15743
|
+
},
|
|
15744
|
+
{ annotations: { readOnlyHint: true } }
|
|
15400
15745
|
),
|
|
15401
15746
|
tool8(
|
|
15402
15747
|
"save_report",
|
|
@@ -15458,7 +15803,7 @@ function createFeatureTools(store) {
|
|
|
15458
15803
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15459
15804
|
};
|
|
15460
15805
|
},
|
|
15461
|
-
{ annotations: {
|
|
15806
|
+
{ annotations: { readOnlyHint: true } }
|
|
15462
15807
|
),
|
|
15463
15808
|
tool9(
|
|
15464
15809
|
"get_feature",
|
|
@@ -15485,7 +15830,7 @@ function createFeatureTools(store) {
|
|
|
15485
15830
|
]
|
|
15486
15831
|
};
|
|
15487
15832
|
},
|
|
15488
|
-
{ annotations: {
|
|
15833
|
+
{ annotations: { readOnlyHint: true } }
|
|
15489
15834
|
),
|
|
15490
15835
|
tool9(
|
|
15491
15836
|
"create_feature",
|
|
@@ -15576,7 +15921,7 @@ function createEpicTools(store) {
|
|
|
15576
15921
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15577
15922
|
};
|
|
15578
15923
|
},
|
|
15579
|
-
{ annotations: {
|
|
15924
|
+
{ annotations: { readOnlyHint: true } }
|
|
15580
15925
|
),
|
|
15581
15926
|
tool10(
|
|
15582
15927
|
"get_epic",
|
|
@@ -15603,7 +15948,7 @@ function createEpicTools(store) {
|
|
|
15603
15948
|
]
|
|
15604
15949
|
};
|
|
15605
15950
|
},
|
|
15606
|
-
{ annotations: {
|
|
15951
|
+
{ annotations: { readOnlyHint: true } }
|
|
15607
15952
|
),
|
|
15608
15953
|
tool10(
|
|
15609
15954
|
"create_epic",
|
|
@@ -15735,7 +16080,7 @@ function createContributionTools(store) {
|
|
|
15735
16080
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15736
16081
|
};
|
|
15737
16082
|
},
|
|
15738
|
-
{ annotations: {
|
|
16083
|
+
{ annotations: { readOnlyHint: true } }
|
|
15739
16084
|
),
|
|
15740
16085
|
tool11(
|
|
15741
16086
|
"get_contribution",
|
|
@@ -15762,7 +16107,7 @@ function createContributionTools(store) {
|
|
|
15762
16107
|
]
|
|
15763
16108
|
};
|
|
15764
16109
|
},
|
|
15765
|
-
{ annotations: {
|
|
16110
|
+
{ annotations: { readOnlyHint: true } }
|
|
15766
16111
|
),
|
|
15767
16112
|
tool11(
|
|
15768
16113
|
"create_contribution",
|
|
@@ -15847,7 +16192,7 @@ function createSprintTools(store) {
|
|
|
15847
16192
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15848
16193
|
};
|
|
15849
16194
|
},
|
|
15850
|
-
{ annotations: {
|
|
16195
|
+
{ annotations: { readOnlyHint: true } }
|
|
15851
16196
|
),
|
|
15852
16197
|
tool12(
|
|
15853
16198
|
"get_sprint",
|
|
@@ -15874,7 +16219,7 @@ function createSprintTools(store) {
|
|
|
15874
16219
|
]
|
|
15875
16220
|
};
|
|
15876
16221
|
},
|
|
15877
|
-
{ annotations: {
|
|
16222
|
+
{ annotations: { readOnlyHint: true } }
|
|
15878
16223
|
),
|
|
15879
16224
|
tool12(
|
|
15880
16225
|
"create_sprint",
|
|
@@ -16171,7 +16516,7 @@ function createSprintPlanningTools(store) {
|
|
|
16171
16516
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
16172
16517
|
};
|
|
16173
16518
|
},
|
|
16174
|
-
{ annotations: {
|
|
16519
|
+
{ annotations: { readOnlyHint: true } }
|
|
16175
16520
|
)
|
|
16176
16521
|
];
|
|
16177
16522
|
}
|
|
@@ -16225,6 +16570,7 @@ var genericAgilePlugin = {
|
|
|
16225
16570
|
- Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
|
|
16226
16571
|
- Use priority levels (critical, high, medium, low) to communicate business value.
|
|
16227
16572
|
- Tag features for categorization and cross-referencing.
|
|
16573
|
+
- Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
|
|
16228
16574
|
|
|
16229
16575
|
**Contribution Tools:**
|
|
16230
16576
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -16255,6 +16601,7 @@ var genericAgilePlugin = {
|
|
|
16255
16601
|
- Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
|
|
16256
16602
|
- Collaborate with the Delivery Manager on target dates and effort estimates.
|
|
16257
16603
|
- Each epic should have a clear scope and definition of done.
|
|
16604
|
+
- Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
|
|
16258
16605
|
|
|
16259
16606
|
**Contribution Tools:**
|
|
16260
16607
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -16314,6 +16661,11 @@ var genericAgilePlugin = {
|
|
|
16314
16661
|
- **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 %.
|
|
16315
16662
|
- Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
|
|
16316
16663
|
|
|
16664
|
+
**Date Enforcement:**
|
|
16665
|
+
- 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.
|
|
16666
|
+
- When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
|
|
16667
|
+
- Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
|
|
16668
|
+
|
|
16317
16669
|
**Sprint Workflow:**
|
|
16318
16670
|
- Create sprints with clear goals and date boundaries.
|
|
16319
16671
|
- Assign epics to sprints via linkedEpics.
|
|
@@ -16333,7 +16685,7 @@ var genericAgilePlugin = {
|
|
|
16333
16685
|
**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).
|
|
16334
16686
|
**Meetings**: Meeting records with attendees, agendas, and notes.
|
|
16335
16687
|
|
|
16336
|
-
**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.
|
|
16688
|
+
**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.
|
|
16337
16689
|
|
|
16338
16690
|
- **list_meetings** / **get_meeting**: Browse and read meeting records.
|
|
16339
16691
|
- **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.
|
|
@@ -16379,7 +16731,7 @@ function createUseCaseTools(store) {
|
|
|
16379
16731
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16380
16732
|
};
|
|
16381
16733
|
},
|
|
16382
|
-
{ annotations: {
|
|
16734
|
+
{ annotations: { readOnlyHint: true } }
|
|
16383
16735
|
),
|
|
16384
16736
|
tool14(
|
|
16385
16737
|
"get_use_case",
|
|
@@ -16406,7 +16758,7 @@ function createUseCaseTools(store) {
|
|
|
16406
16758
|
]
|
|
16407
16759
|
};
|
|
16408
16760
|
},
|
|
16409
|
-
{ annotations: {
|
|
16761
|
+
{ annotations: { readOnlyHint: true } }
|
|
16410
16762
|
),
|
|
16411
16763
|
tool14(
|
|
16412
16764
|
"create_use_case",
|
|
@@ -16504,7 +16856,7 @@ function createTechAssessmentTools(store) {
|
|
|
16504
16856
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16505
16857
|
};
|
|
16506
16858
|
},
|
|
16507
|
-
{ annotations: {
|
|
16859
|
+
{ annotations: { readOnlyHint: true } }
|
|
16508
16860
|
),
|
|
16509
16861
|
tool15(
|
|
16510
16862
|
"get_tech_assessment",
|
|
@@ -16531,7 +16883,7 @@ function createTechAssessmentTools(store) {
|
|
|
16531
16883
|
]
|
|
16532
16884
|
};
|
|
16533
16885
|
},
|
|
16534
|
-
{ annotations: {
|
|
16886
|
+
{ annotations: { readOnlyHint: true } }
|
|
16535
16887
|
),
|
|
16536
16888
|
tool15(
|
|
16537
16889
|
"create_tech_assessment",
|
|
@@ -16665,7 +17017,7 @@ function createExtensionDesignTools(store) {
|
|
|
16665
17017
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16666
17018
|
};
|
|
16667
17019
|
},
|
|
16668
|
-
{ annotations: {
|
|
17020
|
+
{ annotations: { readOnlyHint: true } }
|
|
16669
17021
|
),
|
|
16670
17022
|
tool16(
|
|
16671
17023
|
"get_extension_design",
|
|
@@ -16692,7 +17044,7 @@ function createExtensionDesignTools(store) {
|
|
|
16692
17044
|
]
|
|
16693
17045
|
};
|
|
16694
17046
|
},
|
|
16695
|
-
{ annotations: {
|
|
17047
|
+
{ annotations: { readOnlyHint: true } }
|
|
16696
17048
|
),
|
|
16697
17049
|
tool16(
|
|
16698
17050
|
"create_extension_design",
|
|
@@ -16844,7 +17196,7 @@ function createAemReportTools(store) {
|
|
|
16844
17196
|
]
|
|
16845
17197
|
};
|
|
16846
17198
|
},
|
|
16847
|
-
{ annotations: {
|
|
17199
|
+
{ annotations: { readOnlyHint: true } }
|
|
16848
17200
|
),
|
|
16849
17201
|
tool17(
|
|
16850
17202
|
"generate_tech_readiness",
|
|
@@ -16896,7 +17248,7 @@ function createAemReportTools(store) {
|
|
|
16896
17248
|
]
|
|
16897
17249
|
};
|
|
16898
17250
|
},
|
|
16899
|
-
{ annotations: {
|
|
17251
|
+
{ annotations: { readOnlyHint: true } }
|
|
16900
17252
|
),
|
|
16901
17253
|
tool17(
|
|
16902
17254
|
"generate_phase_status",
|
|
@@ -16951,7 +17303,7 @@ function createAemReportTools(store) {
|
|
|
16951
17303
|
]
|
|
16952
17304
|
};
|
|
16953
17305
|
},
|
|
16954
|
-
{ annotations: {
|
|
17306
|
+
{ annotations: { readOnlyHint: true } }
|
|
16955
17307
|
)
|
|
16956
17308
|
];
|
|
16957
17309
|
}
|
|
@@ -16983,7 +17335,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
16983
17335
|
]
|
|
16984
17336
|
};
|
|
16985
17337
|
},
|
|
16986
|
-
{ annotations: {
|
|
17338
|
+
{ annotations: { readOnlyHint: true } }
|
|
16987
17339
|
),
|
|
16988
17340
|
tool18(
|
|
16989
17341
|
"advance_phase",
|
|
@@ -17428,7 +17780,7 @@ function createJiraTools(store) {
|
|
|
17428
17780
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17429
17781
|
};
|
|
17430
17782
|
},
|
|
17431
|
-
{ annotations: {
|
|
17783
|
+
{ annotations: { readOnlyHint: true } }
|
|
17432
17784
|
),
|
|
17433
17785
|
tool19(
|
|
17434
17786
|
"get_jira_issue",
|
|
@@ -17460,7 +17812,7 @@ function createJiraTools(store) {
|
|
|
17460
17812
|
]
|
|
17461
17813
|
};
|
|
17462
17814
|
},
|
|
17463
|
-
{ annotations: {
|
|
17815
|
+
{ annotations: { readOnlyHint: true } }
|
|
17464
17816
|
),
|
|
17465
17817
|
// --- Jira → Local tools ---
|
|
17466
17818
|
tool19(
|
|
@@ -17934,7 +18286,7 @@ ${fragment}`);
|
|
|
17934
18286
|
}
|
|
17935
18287
|
|
|
17936
18288
|
// src/skills/action-tools.ts
|
|
17937
|
-
import { tool as
|
|
18289
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
17938
18290
|
|
|
17939
18291
|
// src/skills/action-runner.ts
|
|
17940
18292
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -17943,6 +18295,1570 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
17943
18295
|
import {
|
|
17944
18296
|
createSdkMcpServer
|
|
17945
18297
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
18298
|
+
|
|
18299
|
+
// src/agent/tools/web.ts
|
|
18300
|
+
import * as http2 from "http";
|
|
18301
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
18302
|
+
|
|
18303
|
+
// src/web/data.ts
|
|
18304
|
+
function getOverviewData(store) {
|
|
18305
|
+
const types = [];
|
|
18306
|
+
const counts = store.counts();
|
|
18307
|
+
for (const type of store.registeredTypes) {
|
|
18308
|
+
const total = counts[type] ?? 0;
|
|
18309
|
+
const open = store.list({ type, status: "open" }).length;
|
|
18310
|
+
types.push({ type, total, open });
|
|
18311
|
+
}
|
|
18312
|
+
const allDocs = store.list();
|
|
18313
|
+
const sorted = allDocs.sort(
|
|
18314
|
+
(a, b) => (b.frontmatter.updated ?? b.frontmatter.created).localeCompare(
|
|
18315
|
+
a.frontmatter.updated ?? a.frontmatter.created
|
|
18316
|
+
)
|
|
18317
|
+
);
|
|
18318
|
+
return { types, recent: sorted.slice(0, 20) };
|
|
18319
|
+
}
|
|
18320
|
+
function getDocumentListData(store, type, filterStatus, filterOwner) {
|
|
18321
|
+
if (!store.registeredTypes.includes(type)) return void 0;
|
|
18322
|
+
const allOfType = store.list({ type });
|
|
18323
|
+
const statuses = [...new Set(allOfType.map((d) => d.frontmatter.status))].sort();
|
|
18324
|
+
const owners = [
|
|
18325
|
+
...new Set(allOfType.map((d) => d.frontmatter.owner).filter(Boolean))
|
|
18326
|
+
].sort();
|
|
18327
|
+
let docs = allOfType;
|
|
18328
|
+
if (filterStatus) {
|
|
18329
|
+
docs = docs.filter((d) => d.frontmatter.status === filterStatus);
|
|
18330
|
+
}
|
|
18331
|
+
if (filterOwner) {
|
|
18332
|
+
docs = docs.filter((d) => d.frontmatter.owner === filterOwner);
|
|
18333
|
+
}
|
|
18334
|
+
docs.sort((a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id));
|
|
18335
|
+
return { type, docs, statuses, owners, filterStatus, filterOwner };
|
|
18336
|
+
}
|
|
18337
|
+
function getDocumentDetail(store, type, id) {
|
|
18338
|
+
if (!store.registeredTypes.includes(type)) return void 0;
|
|
18339
|
+
return store.get(id);
|
|
18340
|
+
}
|
|
18341
|
+
function getGarData(store, projectName) {
|
|
18342
|
+
const metrics = collectGarMetrics(store);
|
|
18343
|
+
return evaluateGar(projectName, metrics);
|
|
18344
|
+
}
|
|
18345
|
+
function getBoardData(store, type) {
|
|
18346
|
+
const docs = type ? store.list({ type }) : store.list();
|
|
18347
|
+
const types = store.registeredTypes;
|
|
18348
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
18349
|
+
for (const doc of docs) {
|
|
18350
|
+
const status = doc.frontmatter.status;
|
|
18351
|
+
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
18352
|
+
byStatus.get(status).push(doc);
|
|
18353
|
+
}
|
|
18354
|
+
const statusOrder = ["open", "draft", "in-progress", "blocked"];
|
|
18355
|
+
const allStatuses = [...byStatus.keys()];
|
|
18356
|
+
const ordered = [];
|
|
18357
|
+
for (const s of statusOrder) {
|
|
18358
|
+
if (allStatuses.includes(s)) ordered.push(s);
|
|
18359
|
+
}
|
|
18360
|
+
for (const s of allStatuses.sort()) {
|
|
18361
|
+
if (!ordered.includes(s) && s !== "done" && s !== "closed" && s !== "resolved") {
|
|
18362
|
+
ordered.push(s);
|
|
18363
|
+
}
|
|
18364
|
+
}
|
|
18365
|
+
for (const s of ["done", "closed", "resolved"]) {
|
|
18366
|
+
if (allStatuses.includes(s)) ordered.push(s);
|
|
18367
|
+
}
|
|
18368
|
+
const columns = ordered.map((status) => ({
|
|
18369
|
+
status,
|
|
18370
|
+
docs: byStatus.get(status) ?? []
|
|
18371
|
+
}));
|
|
18372
|
+
return { columns, type, types };
|
|
18373
|
+
}
|
|
18374
|
+
function getDiagramData(store) {
|
|
18375
|
+
const allDocs = store.list();
|
|
18376
|
+
const sprints = [];
|
|
18377
|
+
const epics = [];
|
|
18378
|
+
const features = [];
|
|
18379
|
+
const statusCounts = {};
|
|
18380
|
+
for (const doc of allDocs) {
|
|
18381
|
+
const fm = doc.frontmatter;
|
|
18382
|
+
const status = fm.status.toLowerCase();
|
|
18383
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
18384
|
+
switch (fm.type) {
|
|
18385
|
+
case "sprint":
|
|
18386
|
+
sprints.push({
|
|
18387
|
+
id: fm.id,
|
|
18388
|
+
title: fm.title,
|
|
18389
|
+
status: fm.status,
|
|
18390
|
+
startDate: fm.startDate,
|
|
18391
|
+
endDate: fm.endDate,
|
|
18392
|
+
linkedEpics: fm.linkedEpics ?? []
|
|
18393
|
+
});
|
|
18394
|
+
break;
|
|
18395
|
+
case "epic":
|
|
18396
|
+
epics.push({
|
|
18397
|
+
id: fm.id,
|
|
18398
|
+
title: fm.title,
|
|
18399
|
+
status: fm.status,
|
|
18400
|
+
linkedFeature: fm.linkedFeature
|
|
18401
|
+
});
|
|
18402
|
+
break;
|
|
18403
|
+
case "feature":
|
|
18404
|
+
features.push({
|
|
18405
|
+
id: fm.id,
|
|
18406
|
+
title: fm.title,
|
|
18407
|
+
status: fm.status
|
|
18408
|
+
});
|
|
18409
|
+
break;
|
|
18410
|
+
}
|
|
18411
|
+
}
|
|
18412
|
+
return { sprints, epics, features, statusCounts };
|
|
18413
|
+
}
|
|
18414
|
+
|
|
18415
|
+
// src/web/templates/layout.ts
|
|
18416
|
+
function escapeHtml(str) {
|
|
18417
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18418
|
+
}
|
|
18419
|
+
function statusBadge(status) {
|
|
18420
|
+
const cls = {
|
|
18421
|
+
open: "badge-open",
|
|
18422
|
+
done: "badge-done",
|
|
18423
|
+
closed: "badge-done",
|
|
18424
|
+
resolved: "badge-resolved",
|
|
18425
|
+
"in-progress": "badge-in-progress",
|
|
18426
|
+
"in progress": "badge-in-progress",
|
|
18427
|
+
draft: "badge-draft",
|
|
18428
|
+
blocked: "badge-blocked"
|
|
18429
|
+
}[status.toLowerCase()] ?? "badge-default";
|
|
18430
|
+
return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
|
|
18431
|
+
}
|
|
18432
|
+
function formatDate(iso) {
|
|
18433
|
+
if (!iso) return "";
|
|
18434
|
+
return iso.slice(0, 10);
|
|
18435
|
+
}
|
|
18436
|
+
function typeLabel(type) {
|
|
18437
|
+
return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
18438
|
+
}
|
|
18439
|
+
function renderMarkdown(md) {
|
|
18440
|
+
const lines = md.split("\n");
|
|
18441
|
+
const out = [];
|
|
18442
|
+
let inList = false;
|
|
18443
|
+
let listTag = "ul";
|
|
18444
|
+
let inTable = false;
|
|
18445
|
+
let i = 0;
|
|
18446
|
+
while (i < lines.length) {
|
|
18447
|
+
const line = lines[i];
|
|
18448
|
+
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
|
|
18449
|
+
out.push(`</${listTag}>`);
|
|
18450
|
+
inList = false;
|
|
18451
|
+
}
|
|
18452
|
+
if (inTable && !/^\s*\|/.test(line)) {
|
|
18453
|
+
out.push("</tbody></table></div>");
|
|
18454
|
+
inTable = false;
|
|
18455
|
+
}
|
|
18456
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
18457
|
+
i++;
|
|
18458
|
+
out.push("<hr>");
|
|
18459
|
+
continue;
|
|
18460
|
+
}
|
|
18461
|
+
if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
18462
|
+
const headers = parseTableRow(line);
|
|
18463
|
+
out.push('<div class="table-wrap"><table><thead><tr>');
|
|
18464
|
+
out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
|
|
18465
|
+
out.push("</tr></thead><tbody>");
|
|
18466
|
+
inTable = true;
|
|
18467
|
+
i += 2;
|
|
18468
|
+
continue;
|
|
18469
|
+
}
|
|
18470
|
+
if (inTable && /^\s*\|/.test(line)) {
|
|
18471
|
+
const cells = parseTableRow(line);
|
|
18472
|
+
out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
|
|
18473
|
+
i++;
|
|
18474
|
+
continue;
|
|
18475
|
+
}
|
|
18476
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
18477
|
+
if (headingMatch) {
|
|
18478
|
+
const level = headingMatch[1].length;
|
|
18479
|
+
out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
|
|
18480
|
+
i++;
|
|
18481
|
+
continue;
|
|
18482
|
+
}
|
|
18483
|
+
const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
18484
|
+
if (ulMatch) {
|
|
18485
|
+
if (!inList || listTag !== "ul") {
|
|
18486
|
+
if (inList) out.push(`</${listTag}>`);
|
|
18487
|
+
out.push("<ul>");
|
|
18488
|
+
inList = true;
|
|
18489
|
+
listTag = "ul";
|
|
18490
|
+
}
|
|
18491
|
+
out.push(`<li>${inline(ulMatch[1])}</li>`);
|
|
18492
|
+
i++;
|
|
18493
|
+
continue;
|
|
18494
|
+
}
|
|
18495
|
+
const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
18496
|
+
if (olMatch) {
|
|
18497
|
+
if (!inList || listTag !== "ol") {
|
|
18498
|
+
if (inList) out.push(`</${listTag}>`);
|
|
18499
|
+
out.push("<ol>");
|
|
18500
|
+
inList = true;
|
|
18501
|
+
listTag = "ol";
|
|
18502
|
+
}
|
|
18503
|
+
out.push(`<li>${inline(olMatch[1])}</li>`);
|
|
18504
|
+
i++;
|
|
18505
|
+
continue;
|
|
18506
|
+
}
|
|
18507
|
+
if (line.trim() === "") {
|
|
18508
|
+
if (inList) {
|
|
18509
|
+
out.push(`</${listTag}>`);
|
|
18510
|
+
inList = false;
|
|
18511
|
+
}
|
|
18512
|
+
i++;
|
|
18513
|
+
continue;
|
|
18514
|
+
}
|
|
18515
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
18516
|
+
i++;
|
|
18517
|
+
}
|
|
18518
|
+
if (inList) out.push(`</${listTag}>`);
|
|
18519
|
+
if (inTable) out.push("</tbody></table></div>");
|
|
18520
|
+
return out.join("\n");
|
|
18521
|
+
}
|
|
18522
|
+
function parseTableRow(line) {
|
|
18523
|
+
return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
18524
|
+
}
|
|
18525
|
+
function inline(text) {
|
|
18526
|
+
let s = escapeHtml(text);
|
|
18527
|
+
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
18528
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
18529
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
18530
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
18531
|
+
s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
|
|
18532
|
+
return s;
|
|
18533
|
+
}
|
|
18534
|
+
function layout(opts, body) {
|
|
18535
|
+
const topItems = [
|
|
18536
|
+
{ href: "/", label: "Overview" },
|
|
18537
|
+
{ href: "/board", label: "Board" },
|
|
18538
|
+
{ href: "/gar", label: "GAR Report" },
|
|
18539
|
+
{ href: "/health", label: "Health" }
|
|
18540
|
+
];
|
|
18541
|
+
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
18542
|
+
const groupsHtml = opts.navGroups.map((group) => {
|
|
18543
|
+
const links = group.types.map((type) => {
|
|
18544
|
+
const href = `/docs/${type}`;
|
|
18545
|
+
return `<a href="${href}" class="${isActive(href)}">${typeLabel(type)}s</a>`;
|
|
18546
|
+
}).join("\n ");
|
|
18547
|
+
return `
|
|
18548
|
+
<div class="nav-group">
|
|
18549
|
+
<div class="nav-group-label">${escapeHtml(group.label)}</div>
|
|
18550
|
+
${links}
|
|
18551
|
+
</div>`;
|
|
18552
|
+
}).join("\n");
|
|
18553
|
+
return `<!DOCTYPE html>
|
|
18554
|
+
<html lang="en">
|
|
18555
|
+
<head>
|
|
18556
|
+
<meta charset="UTF-8">
|
|
18557
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18558
|
+
<title>${escapeHtml(opts.title)} \u2014 Marvin</title>
|
|
18559
|
+
<link rel="stylesheet" href="/styles.css">
|
|
18560
|
+
</head>
|
|
18561
|
+
<body>
|
|
18562
|
+
<div class="shell">
|
|
18563
|
+
<aside class="sidebar">
|
|
18564
|
+
<div class="sidebar-brand">
|
|
18565
|
+
<h1>Marvin</h1>
|
|
18566
|
+
<div class="project-name">${escapeHtml(opts.projectName)}</div>
|
|
18567
|
+
</div>
|
|
18568
|
+
<nav>
|
|
18569
|
+
${topItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
|
|
18570
|
+
${groupsHtml}
|
|
18571
|
+
</nav>
|
|
18572
|
+
</aside>
|
|
18573
|
+
<main class="main">
|
|
18574
|
+
<button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
|
|
18575
|
+
<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>
|
|
18576
|
+
<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>
|
|
18577
|
+
</button>
|
|
18578
|
+
${body}
|
|
18579
|
+
</main>
|
|
18580
|
+
</div>
|
|
18581
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
18582
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
|
|
18583
|
+
</body>
|
|
18584
|
+
</html>`;
|
|
18585
|
+
}
|
|
18586
|
+
|
|
18587
|
+
// src/web/templates/styles.ts
|
|
18588
|
+
function renderStyles() {
|
|
18589
|
+
return `
|
|
18590
|
+
:root {
|
|
18591
|
+
--bg: #0f1117;
|
|
18592
|
+
--bg-card: #1a1d27;
|
|
18593
|
+
--bg-hover: #222632;
|
|
18594
|
+
--border: #2a2e3a;
|
|
18595
|
+
--text: #e1e4ea;
|
|
18596
|
+
--text-dim: #8b8fa4;
|
|
18597
|
+
--accent: #6c8cff;
|
|
18598
|
+
--accent-dim: #4a6ad4;
|
|
18599
|
+
--green: #34d399;
|
|
18600
|
+
--amber: #fbbf24;
|
|
18601
|
+
--red: #f87171;
|
|
18602
|
+
--radius: 8px;
|
|
18603
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
18604
|
+
--mono: "SF Mono", "Fira Code", monospace;
|
|
18605
|
+
}
|
|
18606
|
+
|
|
18607
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
18608
|
+
|
|
18609
|
+
body {
|
|
18610
|
+
font-family: var(--font);
|
|
18611
|
+
background: var(--bg);
|
|
18612
|
+
color: var(--text);
|
|
18613
|
+
line-height: 1.6;
|
|
18614
|
+
min-height: 100vh;
|
|
18615
|
+
}
|
|
18616
|
+
|
|
18617
|
+
a { color: var(--accent); text-decoration: none; }
|
|
18618
|
+
a:hover { text-decoration: underline; }
|
|
18619
|
+
|
|
18620
|
+
/* Layout */
|
|
18621
|
+
.shell {
|
|
18622
|
+
display: flex;
|
|
18623
|
+
min-height: 100vh;
|
|
18624
|
+
}
|
|
18625
|
+
|
|
18626
|
+
.sidebar {
|
|
18627
|
+
width: 220px;
|
|
18628
|
+
background: var(--bg-card);
|
|
18629
|
+
border-right: 1px solid var(--border);
|
|
18630
|
+
padding: 1.5rem 0;
|
|
18631
|
+
position: fixed;
|
|
18632
|
+
top: 0;
|
|
18633
|
+
left: 0;
|
|
18634
|
+
bottom: 0;
|
|
18635
|
+
overflow-y: auto;
|
|
18636
|
+
}
|
|
18637
|
+
|
|
18638
|
+
.sidebar-brand {
|
|
18639
|
+
padding: 0 1.25rem 1.25rem;
|
|
18640
|
+
border-bottom: 1px solid var(--border);
|
|
18641
|
+
margin-bottom: 1rem;
|
|
18642
|
+
}
|
|
18643
|
+
|
|
18644
|
+
.sidebar-brand h1 {
|
|
18645
|
+
font-size: 1.1rem;
|
|
18646
|
+
font-weight: 700;
|
|
18647
|
+
color: var(--accent);
|
|
18648
|
+
letter-spacing: -0.02em;
|
|
18649
|
+
}
|
|
18650
|
+
|
|
18651
|
+
.sidebar-brand .project-name {
|
|
18652
|
+
font-size: 0.75rem;
|
|
18653
|
+
color: var(--text-dim);
|
|
18654
|
+
margin-top: 0.25rem;
|
|
18655
|
+
}
|
|
18656
|
+
|
|
18657
|
+
.sidebar nav a {
|
|
18658
|
+
display: block;
|
|
18659
|
+
padding: 0.5rem 1.25rem;
|
|
18660
|
+
color: var(--text-dim);
|
|
18661
|
+
font-size: 0.875rem;
|
|
18662
|
+
transition: background 0.15s, color 0.15s;
|
|
18663
|
+
}
|
|
18664
|
+
|
|
18665
|
+
.sidebar nav a:hover {
|
|
18666
|
+
background: var(--bg-hover);
|
|
18667
|
+
color: var(--text);
|
|
18668
|
+
text-decoration: none;
|
|
18669
|
+
}
|
|
18670
|
+
|
|
18671
|
+
.sidebar nav a.active {
|
|
18672
|
+
color: var(--accent);
|
|
18673
|
+
background: rgba(108, 140, 255, 0.08);
|
|
18674
|
+
border-right: 2px solid var(--accent);
|
|
18675
|
+
}
|
|
18676
|
+
|
|
18677
|
+
.nav-group {
|
|
18678
|
+
margin-top: 0.75rem;
|
|
18679
|
+
padding-top: 0.75rem;
|
|
18680
|
+
border-top: 1px solid var(--border);
|
|
18681
|
+
}
|
|
18682
|
+
|
|
18683
|
+
.nav-group-label {
|
|
18684
|
+
padding: 0.25rem 1.25rem 0.25rem;
|
|
18685
|
+
font-size: 0.65rem;
|
|
18686
|
+
text-transform: uppercase;
|
|
18687
|
+
letter-spacing: 0.08em;
|
|
18688
|
+
color: var(--text-dim);
|
|
18689
|
+
font-weight: 600;
|
|
18690
|
+
}
|
|
18691
|
+
|
|
18692
|
+
.main {
|
|
18693
|
+
margin-left: 220px;
|
|
18694
|
+
flex: 1;
|
|
18695
|
+
padding: 2rem 2.5rem;
|
|
18696
|
+
max-width: 1200px;
|
|
18697
|
+
position: relative;
|
|
18698
|
+
transition: max-width 0.2s ease;
|
|
18699
|
+
}
|
|
18700
|
+
.main.expanded {
|
|
18701
|
+
max-width: none;
|
|
18702
|
+
}
|
|
18703
|
+
.expand-toggle {
|
|
18704
|
+
position: absolute;
|
|
18705
|
+
top: 1rem;
|
|
18706
|
+
right: 1rem;
|
|
18707
|
+
background: var(--bg-card);
|
|
18708
|
+
border: 1px solid var(--border);
|
|
18709
|
+
border-radius: var(--radius);
|
|
18710
|
+
color: var(--text-dim);
|
|
18711
|
+
cursor: pointer;
|
|
18712
|
+
padding: 0.4rem;
|
|
18713
|
+
display: flex;
|
|
18714
|
+
align-items: center;
|
|
18715
|
+
justify-content: center;
|
|
18716
|
+
transition: color 0.15s, border-color 0.15s;
|
|
18717
|
+
}
|
|
18718
|
+
.expand-toggle:hover {
|
|
18719
|
+
color: var(--text);
|
|
18720
|
+
border-color: var(--text-dim);
|
|
18721
|
+
}
|
|
18722
|
+
.main.expanded .icon-expand { display: none; }
|
|
18723
|
+
.main:not(.expanded) .icon-collapse { display: none; }
|
|
18724
|
+
|
|
18725
|
+
/* Page header */
|
|
18726
|
+
.page-header {
|
|
18727
|
+
margin-bottom: 2rem;
|
|
18728
|
+
}
|
|
18729
|
+
|
|
18730
|
+
.page-header h2 {
|
|
18731
|
+
font-size: 1.5rem;
|
|
18732
|
+
font-weight: 600;
|
|
18733
|
+
}
|
|
18734
|
+
|
|
18735
|
+
.page-header .subtitle {
|
|
18736
|
+
color: var(--text-dim);
|
|
18737
|
+
font-size: 0.875rem;
|
|
18738
|
+
margin-top: 0.25rem;
|
|
18739
|
+
}
|
|
18740
|
+
|
|
18741
|
+
/* Breadcrumb */
|
|
18742
|
+
.breadcrumb {
|
|
18743
|
+
font-size: 0.8rem;
|
|
18744
|
+
color: var(--text-dim);
|
|
18745
|
+
margin-bottom: 1rem;
|
|
18746
|
+
}
|
|
18747
|
+
|
|
18748
|
+
.breadcrumb a { color: var(--text-dim); }
|
|
18749
|
+
.breadcrumb a:hover { color: var(--accent); }
|
|
18750
|
+
.breadcrumb .sep { margin: 0 0.4rem; }
|
|
18751
|
+
|
|
18752
|
+
/* Card groups */
|
|
18753
|
+
.card-group {
|
|
18754
|
+
margin-bottom: 1.5rem;
|
|
18755
|
+
}
|
|
18756
|
+
|
|
18757
|
+
.card-group-label {
|
|
18758
|
+
font-size: 0.7rem;
|
|
18759
|
+
text-transform: uppercase;
|
|
18760
|
+
letter-spacing: 0.08em;
|
|
18761
|
+
color: var(--text-dim);
|
|
18762
|
+
font-weight: 600;
|
|
18763
|
+
margin-bottom: 0.5rem;
|
|
18764
|
+
}
|
|
18765
|
+
|
|
18766
|
+
/* Cards grid */
|
|
18767
|
+
.cards {
|
|
18768
|
+
display: grid;
|
|
18769
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
18770
|
+
gap: 1rem;
|
|
18771
|
+
margin-bottom: 0.5rem;
|
|
18772
|
+
}
|
|
18773
|
+
|
|
18774
|
+
.card {
|
|
18775
|
+
background: var(--bg-card);
|
|
18776
|
+
border: 1px solid var(--border);
|
|
18777
|
+
border-radius: var(--radius);
|
|
18778
|
+
padding: 1.25rem;
|
|
18779
|
+
transition: border-color 0.15s;
|
|
18780
|
+
}
|
|
18781
|
+
|
|
18782
|
+
.card:hover {
|
|
18783
|
+
border-color: var(--accent-dim);
|
|
18784
|
+
}
|
|
18785
|
+
|
|
18786
|
+
.card a { color: inherit; text-decoration: none; display: block; }
|
|
18787
|
+
|
|
18788
|
+
.card .card-label {
|
|
18789
|
+
font-size: 0.75rem;
|
|
18790
|
+
text-transform: uppercase;
|
|
18791
|
+
letter-spacing: 0.05em;
|
|
18792
|
+
color: var(--text-dim);
|
|
18793
|
+
margin-bottom: 0.5rem;
|
|
18794
|
+
}
|
|
18795
|
+
|
|
18796
|
+
.card .card-value {
|
|
18797
|
+
font-size: 1.75rem;
|
|
18798
|
+
font-weight: 700;
|
|
18799
|
+
}
|
|
18800
|
+
|
|
18801
|
+
.card .card-sub {
|
|
18802
|
+
font-size: 0.8rem;
|
|
18803
|
+
color: var(--text-dim);
|
|
18804
|
+
margin-top: 0.25rem;
|
|
18805
|
+
}
|
|
18806
|
+
|
|
18807
|
+
/* Status badge */
|
|
18808
|
+
.badge {
|
|
18809
|
+
display: inline-block;
|
|
18810
|
+
padding: 0.15rem 0.6rem;
|
|
18811
|
+
border-radius: 999px;
|
|
18812
|
+
font-size: 0.7rem;
|
|
18813
|
+
font-weight: 600;
|
|
18814
|
+
text-transform: uppercase;
|
|
18815
|
+
letter-spacing: 0.03em;
|
|
18816
|
+
}
|
|
18817
|
+
|
|
18818
|
+
.badge-open { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
18819
|
+
.badge-done { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
18820
|
+
.badge-in-progress { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
18821
|
+
.badge-draft { background: rgba(139, 143, 164, 0.15); color: var(--text-dim); }
|
|
18822
|
+
.badge-closed, .badge-resolved { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
18823
|
+
.badge-blocked { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
18824
|
+
.badge-default { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
18825
|
+
|
|
18826
|
+
/* Table */
|
|
18827
|
+
.table-wrap {
|
|
18828
|
+
overflow-x: auto;
|
|
18829
|
+
}
|
|
18830
|
+
|
|
18831
|
+
table {
|
|
18832
|
+
width: 100%;
|
|
18833
|
+
border-collapse: collapse;
|
|
18834
|
+
}
|
|
18835
|
+
|
|
18836
|
+
th {
|
|
18837
|
+
text-align: left;
|
|
18838
|
+
padding: 0.6rem 0.75rem;
|
|
18839
|
+
font-size: 0.7rem;
|
|
18840
|
+
text-transform: uppercase;
|
|
18841
|
+
letter-spacing: 0.05em;
|
|
18842
|
+
color: var(--text-dim);
|
|
18843
|
+
border-bottom: 1px solid var(--border);
|
|
18844
|
+
}
|
|
18845
|
+
|
|
18846
|
+
td {
|
|
18847
|
+
padding: 0.6rem 0.75rem;
|
|
18848
|
+
font-size: 0.875rem;
|
|
18849
|
+
border-bottom: 1px solid var(--border);
|
|
18850
|
+
}
|
|
18851
|
+
|
|
18852
|
+
tr:hover td {
|
|
18853
|
+
background: var(--bg-hover);
|
|
18854
|
+
}
|
|
18855
|
+
|
|
18856
|
+
/* GAR */
|
|
18857
|
+
.gar-overall {
|
|
18858
|
+
text-align: center;
|
|
18859
|
+
padding: 2rem;
|
|
18860
|
+
margin-bottom: 2rem;
|
|
18861
|
+
border-radius: var(--radius);
|
|
18862
|
+
border: 1px solid var(--border);
|
|
18863
|
+
background: var(--bg-card);
|
|
18864
|
+
}
|
|
18865
|
+
|
|
18866
|
+
.gar-overall .dot {
|
|
18867
|
+
width: 60px;
|
|
18868
|
+
height: 60px;
|
|
18869
|
+
border-radius: 50%;
|
|
18870
|
+
display: inline-block;
|
|
18871
|
+
margin-bottom: 0.75rem;
|
|
18872
|
+
}
|
|
18873
|
+
|
|
18874
|
+
.gar-overall .label {
|
|
18875
|
+
font-size: 1.1rem;
|
|
18876
|
+
font-weight: 600;
|
|
18877
|
+
text-transform: uppercase;
|
|
18878
|
+
}
|
|
18879
|
+
|
|
18880
|
+
.gar-areas {
|
|
18881
|
+
display: grid;
|
|
18882
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
18883
|
+
gap: 1rem;
|
|
18884
|
+
}
|
|
18885
|
+
|
|
18886
|
+
.gar-area {
|
|
18887
|
+
background: var(--bg-card);
|
|
18888
|
+
border: 1px solid var(--border);
|
|
18889
|
+
border-radius: var(--radius);
|
|
18890
|
+
padding: 1.25rem;
|
|
18891
|
+
}
|
|
18892
|
+
|
|
18893
|
+
.gar-area .area-header {
|
|
18894
|
+
display: flex;
|
|
18895
|
+
align-items: center;
|
|
18896
|
+
gap: 0.6rem;
|
|
18897
|
+
margin-bottom: 0.75rem;
|
|
18898
|
+
}
|
|
18899
|
+
|
|
18900
|
+
.gar-area .area-dot {
|
|
18901
|
+
width: 14px;
|
|
18902
|
+
height: 14px;
|
|
18903
|
+
border-radius: 50%;
|
|
18904
|
+
flex-shrink: 0;
|
|
18905
|
+
}
|
|
18906
|
+
|
|
18907
|
+
.gar-area .area-name {
|
|
18908
|
+
font-weight: 600;
|
|
18909
|
+
font-size: 1rem;
|
|
18910
|
+
}
|
|
18911
|
+
|
|
18912
|
+
.gar-area .area-summary {
|
|
18913
|
+
font-size: 0.85rem;
|
|
18914
|
+
color: var(--text-dim);
|
|
18915
|
+
margin-bottom: 0.75rem;
|
|
18916
|
+
}
|
|
18917
|
+
|
|
18918
|
+
.gar-area ul {
|
|
18919
|
+
list-style: none;
|
|
18920
|
+
font-size: 0.8rem;
|
|
18921
|
+
}
|
|
18922
|
+
|
|
18923
|
+
.gar-area li {
|
|
18924
|
+
padding: 0.2rem 0;
|
|
18925
|
+
color: var(--text-dim);
|
|
18926
|
+
}
|
|
18927
|
+
|
|
18928
|
+
.gar-area li .ref-id {
|
|
18929
|
+
color: var(--accent);
|
|
18930
|
+
font-family: var(--mono);
|
|
18931
|
+
margin-right: 0.4rem;
|
|
18932
|
+
}
|
|
18933
|
+
|
|
18934
|
+
.dot-green { background: var(--green); }
|
|
18935
|
+
.dot-amber { background: var(--amber); }
|
|
18936
|
+
.dot-red { background: var(--red); }
|
|
18937
|
+
|
|
18938
|
+
/* Board / Kanban */
|
|
18939
|
+
.board {
|
|
18940
|
+
display: flex;
|
|
18941
|
+
gap: 1rem;
|
|
18942
|
+
overflow-x: auto;
|
|
18943
|
+
padding-bottom: 1rem;
|
|
18944
|
+
}
|
|
18945
|
+
|
|
18946
|
+
.board-column {
|
|
18947
|
+
min-width: 240px;
|
|
18948
|
+
max-width: 300px;
|
|
18949
|
+
flex: 1;
|
|
18950
|
+
}
|
|
18951
|
+
|
|
18952
|
+
.board-column-header {
|
|
18953
|
+
font-size: 0.75rem;
|
|
18954
|
+
text-transform: uppercase;
|
|
18955
|
+
letter-spacing: 0.05em;
|
|
18956
|
+
color: var(--text-dim);
|
|
18957
|
+
padding: 0.5rem 0.75rem;
|
|
18958
|
+
border-bottom: 2px solid var(--border);
|
|
18959
|
+
margin-bottom: 0.5rem;
|
|
18960
|
+
display: flex;
|
|
18961
|
+
justify-content: space-between;
|
|
18962
|
+
}
|
|
18963
|
+
|
|
18964
|
+
.board-column-header .count {
|
|
18965
|
+
background: var(--bg-hover);
|
|
18966
|
+
padding: 0 0.5rem;
|
|
18967
|
+
border-radius: 999px;
|
|
18968
|
+
font-size: 0.7rem;
|
|
18969
|
+
}
|
|
18970
|
+
|
|
18971
|
+
.board-card {
|
|
18972
|
+
background: var(--bg-card);
|
|
18973
|
+
border: 1px solid var(--border);
|
|
18974
|
+
border-radius: var(--radius);
|
|
18975
|
+
padding: 0.75rem;
|
|
18976
|
+
margin-bottom: 0.5rem;
|
|
18977
|
+
transition: border-color 0.15s;
|
|
18978
|
+
}
|
|
18979
|
+
|
|
18980
|
+
.board-card:hover {
|
|
18981
|
+
border-color: var(--accent-dim);
|
|
18982
|
+
}
|
|
18983
|
+
|
|
18984
|
+
.board-card .bc-id {
|
|
18985
|
+
font-family: var(--mono);
|
|
18986
|
+
font-size: 0.7rem;
|
|
18987
|
+
color: var(--accent);
|
|
18988
|
+
}
|
|
18989
|
+
|
|
18990
|
+
.board-card .bc-title {
|
|
18991
|
+
font-size: 0.85rem;
|
|
18992
|
+
margin: 0.25rem 0;
|
|
18993
|
+
}
|
|
18994
|
+
|
|
18995
|
+
.board-card .bc-owner {
|
|
18996
|
+
font-size: 0.7rem;
|
|
18997
|
+
color: var(--text-dim);
|
|
18998
|
+
}
|
|
18999
|
+
|
|
19000
|
+
/* Detail page */
|
|
19001
|
+
.detail-meta {
|
|
19002
|
+
background: var(--bg-card);
|
|
19003
|
+
border: 1px solid var(--border);
|
|
19004
|
+
border-radius: var(--radius);
|
|
19005
|
+
padding: 1.25rem;
|
|
19006
|
+
margin-bottom: 1.5rem;
|
|
19007
|
+
}
|
|
19008
|
+
|
|
19009
|
+
.detail-meta dl {
|
|
19010
|
+
display: grid;
|
|
19011
|
+
grid-template-columns: 120px 1fr;
|
|
19012
|
+
gap: 0.4rem 1rem;
|
|
19013
|
+
}
|
|
19014
|
+
|
|
19015
|
+
.detail-meta dt {
|
|
19016
|
+
font-size: 0.75rem;
|
|
19017
|
+
text-transform: uppercase;
|
|
19018
|
+
letter-spacing: 0.05em;
|
|
19019
|
+
color: var(--text-dim);
|
|
19020
|
+
}
|
|
19021
|
+
|
|
19022
|
+
.detail-meta dd {
|
|
19023
|
+
font-size: 0.875rem;
|
|
19024
|
+
}
|
|
19025
|
+
|
|
19026
|
+
.detail-content {
|
|
19027
|
+
background: var(--bg-card);
|
|
19028
|
+
border: 1px solid var(--border);
|
|
19029
|
+
border-radius: var(--radius);
|
|
19030
|
+
padding: 1.5rem;
|
|
19031
|
+
line-height: 1.7;
|
|
19032
|
+
}
|
|
19033
|
+
|
|
19034
|
+
.detail-content h1, .detail-content h2, .detail-content h3 {
|
|
19035
|
+
margin: 1.25rem 0 0.5rem;
|
|
19036
|
+
font-weight: 600;
|
|
19037
|
+
}
|
|
19038
|
+
|
|
19039
|
+
.detail-content h1 { font-size: 1.3rem; }
|
|
19040
|
+
.detail-content h2 { font-size: 1.15rem; }
|
|
19041
|
+
.detail-content h3 { font-size: 1rem; }
|
|
19042
|
+
.detail-content p { margin-bottom: 0.75rem; }
|
|
19043
|
+
.detail-content ul, .detail-content ol { margin: 0.5rem 0 0.75rem 1.5rem; }
|
|
19044
|
+
.detail-content li { margin-bottom: 0.25rem; }
|
|
19045
|
+
.detail-content code {
|
|
19046
|
+
background: var(--bg-hover);
|
|
19047
|
+
padding: 0.1rem 0.35rem;
|
|
19048
|
+
border-radius: 3px;
|
|
19049
|
+
font-family: var(--mono);
|
|
19050
|
+
font-size: 0.85em;
|
|
19051
|
+
}
|
|
19052
|
+
.detail-content hr {
|
|
19053
|
+
border: none;
|
|
19054
|
+
border-top: 1px solid var(--border);
|
|
19055
|
+
margin: 1.25rem 0;
|
|
19056
|
+
}
|
|
19057
|
+
.detail-content .table-wrap {
|
|
19058
|
+
margin: 0.75rem 0;
|
|
19059
|
+
}
|
|
19060
|
+
|
|
19061
|
+
/* Filters */
|
|
19062
|
+
.filters {
|
|
19063
|
+
display: flex;
|
|
19064
|
+
gap: 0.75rem;
|
|
19065
|
+
margin-bottom: 1.5rem;
|
|
19066
|
+
flex-wrap: wrap;
|
|
19067
|
+
}
|
|
19068
|
+
|
|
19069
|
+
.filters select {
|
|
19070
|
+
background: var(--bg-card);
|
|
19071
|
+
border: 1px solid var(--border);
|
|
19072
|
+
color: var(--text);
|
|
19073
|
+
padding: 0.4rem 0.75rem;
|
|
19074
|
+
border-radius: var(--radius);
|
|
19075
|
+
font-size: 0.8rem;
|
|
19076
|
+
cursor: pointer;
|
|
19077
|
+
}
|
|
19078
|
+
|
|
19079
|
+
.filters select:focus {
|
|
19080
|
+
outline: none;
|
|
19081
|
+
border-color: var(--accent);
|
|
19082
|
+
}
|
|
19083
|
+
|
|
19084
|
+
/* Empty state */
|
|
19085
|
+
.empty {
|
|
19086
|
+
text-align: center;
|
|
19087
|
+
padding: 3rem;
|
|
19088
|
+
color: var(--text-dim);
|
|
19089
|
+
}
|
|
19090
|
+
|
|
19091
|
+
.empty p { font-size: 0.9rem; }
|
|
19092
|
+
|
|
19093
|
+
/* Section heading */
|
|
19094
|
+
.section-title {
|
|
19095
|
+
font-size: 0.9rem;
|
|
19096
|
+
font-weight: 600;
|
|
19097
|
+
margin: 1.5rem 0 0.75rem;
|
|
19098
|
+
}
|
|
19099
|
+
|
|
19100
|
+
/* Priority */
|
|
19101
|
+
.priority-high { color: var(--red); }
|
|
19102
|
+
.priority-medium { color: var(--amber); }
|
|
19103
|
+
.priority-low { color: var(--green); }
|
|
19104
|
+
|
|
19105
|
+
/* Health */
|
|
19106
|
+
.health-section-title {
|
|
19107
|
+
font-size: 1.1rem;
|
|
19108
|
+
font-weight: 600;
|
|
19109
|
+
margin: 2rem 0 1rem;
|
|
19110
|
+
color: var(--text);
|
|
19111
|
+
}
|
|
19112
|
+
|
|
19113
|
+
/* Mermaid diagrams */
|
|
19114
|
+
.mermaid-container {
|
|
19115
|
+
background: var(--bg-card);
|
|
19116
|
+
border: 1px solid var(--border);
|
|
19117
|
+
border-radius: var(--radius);
|
|
19118
|
+
padding: 1.5rem;
|
|
19119
|
+
margin: 1rem 0;
|
|
19120
|
+
overflow-x: auto;
|
|
19121
|
+
}
|
|
19122
|
+
|
|
19123
|
+
.mermaid-container .mermaid {
|
|
19124
|
+
display: flex;
|
|
19125
|
+
justify-content: center;
|
|
19126
|
+
}
|
|
19127
|
+
|
|
19128
|
+
.mermaid-empty {
|
|
19129
|
+
text-align: center;
|
|
19130
|
+
color: var(--text-dim);
|
|
19131
|
+
font-size: 0.875rem;
|
|
19132
|
+
}
|
|
19133
|
+
|
|
19134
|
+
.mermaid-row {
|
|
19135
|
+
display: grid;
|
|
19136
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
19137
|
+
gap: 1rem;
|
|
19138
|
+
}
|
|
19139
|
+
|
|
19140
|
+
.mermaid-row .mermaid-container {
|
|
19141
|
+
margin: 0;
|
|
19142
|
+
}
|
|
19143
|
+
`;
|
|
19144
|
+
}
|
|
19145
|
+
|
|
19146
|
+
// src/web/templates/mermaid.ts
|
|
19147
|
+
function sanitize(text, maxLen = 40) {
|
|
19148
|
+
const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
|
|
19149
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
|
|
19150
|
+
}
|
|
19151
|
+
function mermaidBlock(definition) {
|
|
19152
|
+
return `<div class="mermaid-container"><pre class="mermaid">
|
|
19153
|
+
${definition}
|
|
19154
|
+
</pre></div>`;
|
|
19155
|
+
}
|
|
19156
|
+
function placeholder(message) {
|
|
19157
|
+
return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
|
|
19158
|
+
}
|
|
19159
|
+
function buildTimelineGantt(data) {
|
|
19160
|
+
const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
|
|
19161
|
+
if (sprintsWithDates.length === 0) {
|
|
19162
|
+
return placeholder("No timeline data available \u2014 sprints need start and end dates.");
|
|
19163
|
+
}
|
|
19164
|
+
const epicMap = new Map(data.epics.map((e) => [e.id, e]));
|
|
19165
|
+
const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
|
|
19166
|
+
for (const sprint of sprintsWithDates) {
|
|
19167
|
+
lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
|
|
19168
|
+
const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
|
|
19169
|
+
if (linked.length === 0) {
|
|
19170
|
+
lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
|
|
19171
|
+
} else {
|
|
19172
|
+
for (const epic of linked) {
|
|
19173
|
+
const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
|
|
19174
|
+
lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
|
|
19175
|
+
}
|
|
19176
|
+
}
|
|
19177
|
+
}
|
|
19178
|
+
return mermaidBlock(lines.join("\n"));
|
|
19179
|
+
}
|
|
19180
|
+
function buildArtifactFlowchart(data) {
|
|
19181
|
+
if (data.features.length === 0 && data.epics.length === 0) {
|
|
19182
|
+
return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
|
|
19183
|
+
}
|
|
19184
|
+
const lines = ["graph TD"];
|
|
19185
|
+
lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
|
|
19186
|
+
lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
|
|
19187
|
+
lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
|
|
19188
|
+
lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
|
|
19189
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
19190
|
+
for (const epic of data.epics) {
|
|
19191
|
+
if (epic.linkedFeature) {
|
|
19192
|
+
const feature = data.features.find((f) => f.id === epic.linkedFeature);
|
|
19193
|
+
if (feature) {
|
|
19194
|
+
const fNode = feature.id.replace(/-/g, "_");
|
|
19195
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
19196
|
+
if (!nodeIds.has(fNode)) {
|
|
19197
|
+
lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
|
|
19198
|
+
nodeIds.add(fNode);
|
|
19199
|
+
}
|
|
19200
|
+
if (!nodeIds.has(eNode)) {
|
|
19201
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
19202
|
+
nodeIds.add(eNode);
|
|
19203
|
+
}
|
|
19204
|
+
lines.push(` ${fNode} --> ${eNode}`);
|
|
19205
|
+
}
|
|
19206
|
+
}
|
|
19207
|
+
}
|
|
19208
|
+
for (const sprint of data.sprints) {
|
|
19209
|
+
const sNode = sprint.id.replace(/-/g, "_");
|
|
19210
|
+
for (const epicId of sprint.linkedEpics) {
|
|
19211
|
+
const epic = data.epics.find((e) => e.id === epicId);
|
|
19212
|
+
if (epic) {
|
|
19213
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
19214
|
+
if (!nodeIds.has(eNode)) {
|
|
19215
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
19216
|
+
nodeIds.add(eNode);
|
|
19217
|
+
}
|
|
19218
|
+
if (!nodeIds.has(sNode)) {
|
|
19219
|
+
lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
|
|
19220
|
+
nodeIds.add(sNode);
|
|
19221
|
+
}
|
|
19222
|
+
lines.push(` ${eNode} --> ${sNode}`);
|
|
19223
|
+
}
|
|
19224
|
+
}
|
|
19225
|
+
}
|
|
19226
|
+
if (nodeIds.size === 0) {
|
|
19227
|
+
return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
|
|
19228
|
+
}
|
|
19229
|
+
const allItems = [
|
|
19230
|
+
...data.features.map((f) => ({ id: f.id, status: f.status })),
|
|
19231
|
+
...data.epics.map((e) => ({ id: e.id, status: e.status })),
|
|
19232
|
+
...data.sprints.map((s) => ({ id: s.id, status: s.status }))
|
|
19233
|
+
];
|
|
19234
|
+
for (const item of allItems) {
|
|
19235
|
+
const node = item.id.replace(/-/g, "_");
|
|
19236
|
+
if (!nodeIds.has(node)) continue;
|
|
19237
|
+
const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
|
|
19238
|
+
if (cls) {
|
|
19239
|
+
lines.push(` class ${node} ${cls}`);
|
|
19240
|
+
}
|
|
19241
|
+
}
|
|
19242
|
+
return mermaidBlock(lines.join("\n"));
|
|
19243
|
+
}
|
|
19244
|
+
function buildStatusPie(title, counts) {
|
|
19245
|
+
const entries = Object.entries(counts).filter(([, v]) => v > 0);
|
|
19246
|
+
if (entries.length === 0) {
|
|
19247
|
+
return placeholder(`No data for ${title}.`);
|
|
19248
|
+
}
|
|
19249
|
+
const lines = [`pie title ${sanitize(title, 60)}`];
|
|
19250
|
+
for (const [label, count] of entries) {
|
|
19251
|
+
lines.push(` "${sanitize(label, 30)}" : ${count}`);
|
|
19252
|
+
}
|
|
19253
|
+
return mermaidBlock(lines.join("\n"));
|
|
19254
|
+
}
|
|
19255
|
+
function buildHealthGauge(categories) {
|
|
19256
|
+
const valid = categories.filter((c) => c.total > 0);
|
|
19257
|
+
if (valid.length === 0) {
|
|
19258
|
+
return placeholder("No completeness data available.");
|
|
19259
|
+
}
|
|
19260
|
+
const pies = valid.map((cat) => {
|
|
19261
|
+
const incomplete = cat.total - cat.complete;
|
|
19262
|
+
const lines = [
|
|
19263
|
+
`pie title ${sanitize(cat.name, 30)}`,
|
|
19264
|
+
` "Complete" : ${cat.complete}`,
|
|
19265
|
+
` "Incomplete" : ${incomplete}`
|
|
19266
|
+
];
|
|
19267
|
+
return mermaidBlock(lines.join("\n"));
|
|
19268
|
+
});
|
|
19269
|
+
return `<div class="mermaid-row">${pies.join("\n")}</div>`;
|
|
19270
|
+
}
|
|
19271
|
+
|
|
19272
|
+
// src/web/templates/pages/overview.ts
|
|
19273
|
+
function renderCard(t) {
|
|
19274
|
+
return `
|
|
19275
|
+
<div class="card">
|
|
19276
|
+
<a href="/docs/${t.type}">
|
|
19277
|
+
<div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
|
|
19278
|
+
<div class="card-value">${t.total}</div>
|
|
19279
|
+
${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
|
|
19280
|
+
</a>
|
|
19281
|
+
</div>`;
|
|
19282
|
+
}
|
|
19283
|
+
function overviewPage(data, diagrams, navGroups) {
|
|
19284
|
+
const typeMap = new Map(data.types.map((t) => [t.type, t]));
|
|
19285
|
+
const placed = /* @__PURE__ */ new Set();
|
|
19286
|
+
const groupSections = navGroups.map((group) => {
|
|
19287
|
+
const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
|
|
19288
|
+
placed.add(type);
|
|
19289
|
+
return renderCard(typeMap.get(type));
|
|
19290
|
+
});
|
|
19291
|
+
if (groupCards.length === 0) return "";
|
|
19292
|
+
return `
|
|
19293
|
+
<div class="card-group">
|
|
19294
|
+
<div class="card-group-label">${escapeHtml(group.label)}</div>
|
|
19295
|
+
<div class="cards">${groupCards.join("\n")}</div>
|
|
19296
|
+
</div>`;
|
|
19297
|
+
}).filter(Boolean).join("\n");
|
|
19298
|
+
const ungrouped = data.types.filter((t) => !placed.has(t.type));
|
|
19299
|
+
const ungroupedSection = ungrouped.length > 0 ? `
|
|
19300
|
+
<div class="card-group">
|
|
19301
|
+
<div class="card-group-label">Other</div>
|
|
19302
|
+
<div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
|
|
19303
|
+
</div>` : "";
|
|
19304
|
+
const rows = data.recent.map(
|
|
19305
|
+
(doc) => `
|
|
19306
|
+
<tr>
|
|
19307
|
+
<td><a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
|
|
19308
|
+
<td>${escapeHtml(doc.frontmatter.title)}</td>
|
|
19309
|
+
<td>${escapeHtml(typeLabel(doc.frontmatter.type))}</td>
|
|
19310
|
+
<td>${statusBadge(doc.frontmatter.status)}</td>
|
|
19311
|
+
<td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
|
|
19312
|
+
</tr>`
|
|
19313
|
+
).join("\n");
|
|
19314
|
+
return `
|
|
19315
|
+
<div class="page-header">
|
|
19316
|
+
<h2>Project Overview</h2>
|
|
19317
|
+
</div>
|
|
19318
|
+
|
|
19319
|
+
${groupSections}
|
|
19320
|
+
${ungroupedSection}
|
|
19321
|
+
|
|
19322
|
+
<div class="section-title">Project Timeline</div>
|
|
19323
|
+
${buildTimelineGantt(diagrams)}
|
|
19324
|
+
|
|
19325
|
+
<div class="section-title">Artifact Relationships</div>
|
|
19326
|
+
${buildArtifactFlowchart(diagrams)}
|
|
19327
|
+
|
|
19328
|
+
<div class="section-title">Recent Activity</div>
|
|
19329
|
+
${data.recent.length > 0 ? `
|
|
19330
|
+
<div class="table-wrap">
|
|
19331
|
+
<table>
|
|
19332
|
+
<thead>
|
|
19333
|
+
<tr>
|
|
19334
|
+
<th>ID</th>
|
|
19335
|
+
<th>Title</th>
|
|
19336
|
+
<th>Type</th>
|
|
19337
|
+
<th>Status</th>
|
|
19338
|
+
<th>Updated</th>
|
|
19339
|
+
</tr>
|
|
19340
|
+
</thead>
|
|
19341
|
+
<tbody>
|
|
19342
|
+
${rows}
|
|
19343
|
+
</tbody>
|
|
19344
|
+
</table>
|
|
19345
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`}
|
|
19346
|
+
`;
|
|
19347
|
+
}
|
|
19348
|
+
|
|
19349
|
+
// src/web/templates/pages/documents.ts
|
|
19350
|
+
function documentsPage(data) {
|
|
19351
|
+
const label = typeLabel(data.type);
|
|
19352
|
+
const statusOptions = data.statuses.map(
|
|
19353
|
+
(s) => `<option value="${escapeHtml(s)}"${data.filterStatus === s ? " selected" : ""}>${escapeHtml(s)}</option>`
|
|
19354
|
+
).join("");
|
|
19355
|
+
const ownerOptions = data.owners.map(
|
|
19356
|
+
(o) => `<option value="${escapeHtml(o)}"${data.filterOwner === o ? " selected" : ""}>${escapeHtml(o)}</option>`
|
|
19357
|
+
).join("");
|
|
19358
|
+
const rows = data.docs.map(
|
|
19359
|
+
(doc) => `
|
|
19360
|
+
<tr>
|
|
19361
|
+
<td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
|
|
19362
|
+
<td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a></td>
|
|
19363
|
+
<td>${statusBadge(doc.frontmatter.status)}</td>
|
|
19364
|
+
<td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
|
|
19365
|
+
<td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
|
|
19366
|
+
<td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
|
|
19367
|
+
</tr>`
|
|
19368
|
+
).join("\n");
|
|
19369
|
+
return `
|
|
19370
|
+
<div class="page-header">
|
|
19371
|
+
<h2>${escapeHtml(label)}s</h2>
|
|
19372
|
+
<div class="subtitle">${data.docs.length} document${data.docs.length !== 1 ? "s" : ""}</div>
|
|
19373
|
+
</div>
|
|
19374
|
+
|
|
19375
|
+
<div class="filters">
|
|
19376
|
+
<select onchange="filterByStatus(this.value)">
|
|
19377
|
+
<option value="">All statuses</option>
|
|
19378
|
+
${statusOptions}
|
|
19379
|
+
</select>
|
|
19380
|
+
<select onchange="filterByOwner(this.value)">
|
|
19381
|
+
<option value="">All owners</option>
|
|
19382
|
+
${ownerOptions}
|
|
19383
|
+
</select>
|
|
19384
|
+
</div>
|
|
19385
|
+
|
|
19386
|
+
${data.docs.length > 0 ? `
|
|
19387
|
+
<div class="table-wrap">
|
|
19388
|
+
<table>
|
|
19389
|
+
<thead>
|
|
19390
|
+
<tr>
|
|
19391
|
+
<th>ID</th>
|
|
19392
|
+
<th>Title</th>
|
|
19393
|
+
<th>Status</th>
|
|
19394
|
+
<th>Owner</th>
|
|
19395
|
+
<th>Priority</th>
|
|
19396
|
+
<th>Updated</th>
|
|
19397
|
+
</tr>
|
|
19398
|
+
</thead>
|
|
19399
|
+
<tbody>
|
|
19400
|
+
${rows}
|
|
19401
|
+
</tbody>
|
|
19402
|
+
</table>
|
|
19403
|
+
</div>` : `<div class="empty"><p>No ${label.toLowerCase()}s found.</p></div>`}
|
|
19404
|
+
|
|
19405
|
+
<script>
|
|
19406
|
+
function filterByStatus(status) {
|
|
19407
|
+
const url = new URL(window.location);
|
|
19408
|
+
if (status) url.searchParams.set('status', status);
|
|
19409
|
+
else url.searchParams.delete('status');
|
|
19410
|
+
window.location = url;
|
|
19411
|
+
}
|
|
19412
|
+
function filterByOwner(owner) {
|
|
19413
|
+
const url = new URL(window.location);
|
|
19414
|
+
if (owner) url.searchParams.set('owner', owner);
|
|
19415
|
+
else url.searchParams.delete('owner');
|
|
19416
|
+
window.location = url;
|
|
19417
|
+
}
|
|
19418
|
+
</script>
|
|
19419
|
+
`;
|
|
19420
|
+
}
|
|
19421
|
+
|
|
19422
|
+
// src/web/templates/pages/document-detail.ts
|
|
19423
|
+
function documentDetailPage(doc) {
|
|
19424
|
+
const fm = doc.frontmatter;
|
|
19425
|
+
const label = typeLabel(fm.type);
|
|
19426
|
+
const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
|
|
19427
|
+
const entries = Object.entries(fm).filter(
|
|
19428
|
+
([key]) => !skipKeys.has(key) && fm[key] != null
|
|
19429
|
+
);
|
|
19430
|
+
const dtDd = entries.map(([key, value]) => {
|
|
19431
|
+
let rendered;
|
|
19432
|
+
if (key === "status") {
|
|
19433
|
+
rendered = statusBadge(value);
|
|
19434
|
+
} else if (key === "tags" && Array.isArray(value)) {
|
|
19435
|
+
rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
|
|
19436
|
+
} else if (key === "created" || key === "updated") {
|
|
19437
|
+
rendered = formatDate(value);
|
|
19438
|
+
} else {
|
|
19439
|
+
rendered = escapeHtml(String(value));
|
|
19440
|
+
}
|
|
19441
|
+
return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
|
|
19442
|
+
}).join("\n ");
|
|
19443
|
+
return `
|
|
19444
|
+
<div class="breadcrumb">
|
|
19445
|
+
<a href="/">Overview</a><span class="sep">/</span>
|
|
19446
|
+
<a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
|
|
19447
|
+
${escapeHtml(fm.id)}
|
|
19448
|
+
</div>
|
|
19449
|
+
|
|
19450
|
+
<div class="page-header">
|
|
19451
|
+
<h2>${escapeHtml(fm.title)}</h2>
|
|
19452
|
+
<div class="subtitle">${escapeHtml(fm.id)} · ${escapeHtml(label)}</div>
|
|
19453
|
+
</div>
|
|
19454
|
+
|
|
19455
|
+
<div class="detail-meta">
|
|
19456
|
+
<dl>
|
|
19457
|
+
${dtDd}
|
|
19458
|
+
</dl>
|
|
19459
|
+
</div>
|
|
19460
|
+
|
|
19461
|
+
${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
|
|
19462
|
+
`;
|
|
19463
|
+
}
|
|
19464
|
+
|
|
19465
|
+
// src/web/templates/pages/gar.ts
|
|
19466
|
+
function garPage(report) {
|
|
19467
|
+
const dotClass = `dot-${report.overall}`;
|
|
19468
|
+
const areaCards = report.areas.map(
|
|
19469
|
+
(area) => `
|
|
19470
|
+
<div class="gar-area">
|
|
19471
|
+
<div class="area-header">
|
|
19472
|
+
<div class="area-dot dot-${area.status}"></div>
|
|
19473
|
+
<div class="area-name">${escapeHtml(area.name)}</div>
|
|
19474
|
+
</div>
|
|
19475
|
+
<div class="area-summary">${escapeHtml(area.summary)}</div>
|
|
19476
|
+
${area.items.length > 0 ? `<ul>${area.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.title)}</li>`).join("")}</ul>` : ""}
|
|
19477
|
+
</div>`
|
|
19478
|
+
).join("\n");
|
|
19479
|
+
return `
|
|
19480
|
+
<div class="page-header">
|
|
19481
|
+
<h2>GAR Report</h2>
|
|
19482
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
19483
|
+
</div>
|
|
19484
|
+
|
|
19485
|
+
<div class="gar-overall">
|
|
19486
|
+
<div class="dot ${dotClass}"></div>
|
|
19487
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
19488
|
+
</div>
|
|
19489
|
+
|
|
19490
|
+
<div class="gar-areas">
|
|
19491
|
+
${areaCards}
|
|
19492
|
+
</div>
|
|
19493
|
+
|
|
19494
|
+
<div class="section-title">Status Distribution</div>
|
|
19495
|
+
${buildStatusPie("Action Status", {
|
|
19496
|
+
Open: report.metrics.scope.open,
|
|
19497
|
+
Done: report.metrics.scope.done,
|
|
19498
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
19499
|
+
})}
|
|
19500
|
+
`;
|
|
19501
|
+
}
|
|
19502
|
+
|
|
19503
|
+
// src/web/templates/pages/health.ts
|
|
19504
|
+
function healthPage(report, metrics) {
|
|
19505
|
+
const dotClass = `dot-${report.overall}`;
|
|
19506
|
+
function renderSection(title, categories) {
|
|
19507
|
+
const cards = categories.map(
|
|
19508
|
+
(cat) => `
|
|
19509
|
+
<div class="gar-area">
|
|
19510
|
+
<div class="area-header">
|
|
19511
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
19512
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
19513
|
+
</div>
|
|
19514
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
19515
|
+
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
19516
|
+
</div>`
|
|
19517
|
+
).join("\n");
|
|
19518
|
+
return `
|
|
19519
|
+
<div class="health-section-title">${escapeHtml(title)}</div>
|
|
19520
|
+
<div class="gar-areas">${cards}</div>
|
|
19521
|
+
`;
|
|
19522
|
+
}
|
|
19523
|
+
return `
|
|
19524
|
+
<div class="page-header">
|
|
19525
|
+
<h2>Governance Health Check</h2>
|
|
19526
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
19527
|
+
</div>
|
|
19528
|
+
|
|
19529
|
+
<div class="gar-overall">
|
|
19530
|
+
<div class="dot ${dotClass}"></div>
|
|
19531
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
19532
|
+
</div>
|
|
19533
|
+
|
|
19534
|
+
${renderSection("Completeness", report.completeness)}
|
|
19535
|
+
|
|
19536
|
+
<div class="health-section-title">Completeness Overview</div>
|
|
19537
|
+
${buildHealthGauge(
|
|
19538
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
19539
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
19540
|
+
complete: cat.complete,
|
|
19541
|
+
total: cat.total
|
|
19542
|
+
})) : report.completeness.map((c) => {
|
|
19543
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
19544
|
+
return {
|
|
19545
|
+
name: c.name,
|
|
19546
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
19547
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
19548
|
+
};
|
|
19549
|
+
})
|
|
19550
|
+
)}
|
|
19551
|
+
|
|
19552
|
+
${renderSection("Process", report.process)}
|
|
19553
|
+
|
|
19554
|
+
<div class="health-section-title">Process Summary</div>
|
|
19555
|
+
${metrics ? buildStatusPie("Process Health", {
|
|
19556
|
+
Stale: metrics.process.stale.length,
|
|
19557
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
19558
|
+
Healthy: Math.max(
|
|
19559
|
+
0,
|
|
19560
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
19561
|
+
)
|
|
19562
|
+
}) : ""}
|
|
19563
|
+
`;
|
|
19564
|
+
}
|
|
19565
|
+
|
|
19566
|
+
// src/web/templates/pages/board.ts
|
|
19567
|
+
function boardPage(data) {
|
|
19568
|
+
const typeOptions = data.types.map(
|
|
19569
|
+
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(typeLabel(t))}s</option>`
|
|
19570
|
+
).join("");
|
|
19571
|
+
const columns = data.columns.map(
|
|
19572
|
+
(col) => `
|
|
19573
|
+
<div class="board-column">
|
|
19574
|
+
<div class="board-column-header">
|
|
19575
|
+
<span>${escapeHtml(col.status)}</span>
|
|
19576
|
+
<span class="count">${col.docs.length}</span>
|
|
19577
|
+
</div>
|
|
19578
|
+
${col.docs.map(
|
|
19579
|
+
(doc) => `
|
|
19580
|
+
<div class="board-card">
|
|
19581
|
+
<a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
|
|
19582
|
+
<div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
|
|
19583
|
+
<div class="bc-title">${escapeHtml(doc.frontmatter.title)}</div>
|
|
19584
|
+
${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
|
|
19585
|
+
</a>
|
|
19586
|
+
</div>`
|
|
19587
|
+
).join("\n")}
|
|
19588
|
+
</div>`
|
|
19589
|
+
).join("\n");
|
|
19590
|
+
return `
|
|
19591
|
+
<div class="page-header">
|
|
19592
|
+
<h2>Status Board</h2>
|
|
19593
|
+
</div>
|
|
19594
|
+
|
|
19595
|
+
<div class="filters">
|
|
19596
|
+
<select onchange="filterByType(this.value)">
|
|
19597
|
+
<option value="">All types</option>
|
|
19598
|
+
${typeOptions}
|
|
19599
|
+
</select>
|
|
19600
|
+
</div>
|
|
19601
|
+
|
|
19602
|
+
${data.columns.length > 0 ? `<div class="board">${columns}</div>` : `<div class="empty"><p>No documents to display.</p></div>`}
|
|
19603
|
+
|
|
19604
|
+
<script>
|
|
19605
|
+
function filterByType(type) {
|
|
19606
|
+
if (type) window.location = '/board/' + type;
|
|
19607
|
+
else window.location = '/board';
|
|
19608
|
+
}
|
|
19609
|
+
</script>
|
|
19610
|
+
`;
|
|
19611
|
+
}
|
|
19612
|
+
|
|
19613
|
+
// src/web/router.ts
|
|
19614
|
+
function handleRequest(req, res, store, projectName, navGroups) {
|
|
19615
|
+
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
19616
|
+
const pathname = parsed.pathname;
|
|
19617
|
+
const navTypes = store.registeredTypes;
|
|
19618
|
+
try {
|
|
19619
|
+
if (pathname === "/styles.css") {
|
|
19620
|
+
res.writeHead(200, {
|
|
19621
|
+
"Content-Type": "text/css",
|
|
19622
|
+
"Cache-Control": "public, max-age=300"
|
|
19623
|
+
});
|
|
19624
|
+
res.end(renderStyles());
|
|
19625
|
+
return;
|
|
19626
|
+
}
|
|
19627
|
+
if (pathname === "/") {
|
|
19628
|
+
const data = getOverviewData(store);
|
|
19629
|
+
const diagrams = getDiagramData(store);
|
|
19630
|
+
const body = overviewPage(data, diagrams, navGroups);
|
|
19631
|
+
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
|
|
19632
|
+
return;
|
|
19633
|
+
}
|
|
19634
|
+
if (pathname === "/gar") {
|
|
19635
|
+
const report = getGarData(store, projectName);
|
|
19636
|
+
const body = garPage(report);
|
|
19637
|
+
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
|
|
19638
|
+
return;
|
|
19639
|
+
}
|
|
19640
|
+
if (pathname === "/health") {
|
|
19641
|
+
const healthMetrics = collectHealthMetrics(store);
|
|
19642
|
+
const report = evaluateHealth(projectName, healthMetrics);
|
|
19643
|
+
const body = healthPage(report, healthMetrics);
|
|
19644
|
+
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
19645
|
+
return;
|
|
19646
|
+
}
|
|
19647
|
+
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
19648
|
+
if (boardMatch) {
|
|
19649
|
+
const type = boardMatch[1];
|
|
19650
|
+
if (type && !navTypes.includes(type)) {
|
|
19651
|
+
notFound(res, projectName, navGroups, pathname);
|
|
19652
|
+
return;
|
|
19653
|
+
}
|
|
19654
|
+
const data = getBoardData(store, type);
|
|
19655
|
+
const body = boardPage(data);
|
|
19656
|
+
respond(res, layout({ title: "Board", activePath: "/board", projectName, navGroups }, body));
|
|
19657
|
+
return;
|
|
19658
|
+
}
|
|
19659
|
+
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
19660
|
+
if (detailMatch) {
|
|
19661
|
+
const [, type, id] = detailMatch;
|
|
19662
|
+
const doc = getDocumentDetail(store, type, id);
|
|
19663
|
+
if (!doc) {
|
|
19664
|
+
notFound(res, projectName, navGroups, pathname);
|
|
19665
|
+
return;
|
|
19666
|
+
}
|
|
19667
|
+
const body = documentDetailPage(doc);
|
|
19668
|
+
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
|
|
19669
|
+
return;
|
|
19670
|
+
}
|
|
19671
|
+
const listMatch = pathname.match(/^\/docs\/([^/]+)$/);
|
|
19672
|
+
if (listMatch) {
|
|
19673
|
+
const type = listMatch[1];
|
|
19674
|
+
const filterStatus = parsed.searchParams.get("status") ?? void 0;
|
|
19675
|
+
const filterOwner = parsed.searchParams.get("owner") ?? void 0;
|
|
19676
|
+
const data = getDocumentListData(store, type, filterStatus, filterOwner);
|
|
19677
|
+
if (!data) {
|
|
19678
|
+
notFound(res, projectName, navGroups, pathname);
|
|
19679
|
+
return;
|
|
19680
|
+
}
|
|
19681
|
+
const body = documentsPage(data);
|
|
19682
|
+
respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
|
|
19683
|
+
return;
|
|
19684
|
+
}
|
|
19685
|
+
notFound(res, projectName, navGroups, pathname);
|
|
19686
|
+
} catch (err) {
|
|
19687
|
+
console.error("[marvin web] Error handling request:", err);
|
|
19688
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
19689
|
+
res.end("<h1>500 \u2014 Internal Server Error</h1>");
|
|
19690
|
+
}
|
|
19691
|
+
}
|
|
19692
|
+
function respond(res, html) {
|
|
19693
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
19694
|
+
res.end(html);
|
|
19695
|
+
}
|
|
19696
|
+
function notFound(res, projectName, navGroups, activePath) {
|
|
19697
|
+
const body = `<div class="empty"><h2>404</h2><p>Page not found.</p><p><a href="/">Go to overview</a></p></div>`;
|
|
19698
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
19699
|
+
res.end(layout({ title: "Not Found", activePath, projectName, navGroups }, body));
|
|
19700
|
+
}
|
|
19701
|
+
|
|
19702
|
+
// src/web/server.ts
|
|
19703
|
+
import * as http from "http";
|
|
19704
|
+
import { exec } from "child_process";
|
|
19705
|
+
function openBrowser(url2) {
|
|
19706
|
+
const platform = process.platform;
|
|
19707
|
+
const cmd = platform === "darwin" ? `open "${url2}"` : platform === "win32" ? `start "${url2}"` : `xdg-open "${url2}"`;
|
|
19708
|
+
exec(cmd, (err) => {
|
|
19709
|
+
if (err) {
|
|
19710
|
+
}
|
|
19711
|
+
});
|
|
19712
|
+
}
|
|
19713
|
+
|
|
19714
|
+
// src/agent/tools/web.ts
|
|
19715
|
+
var runningServer = null;
|
|
19716
|
+
function createWebTools(store, projectName, navGroups) {
|
|
19717
|
+
return [
|
|
19718
|
+
tool20(
|
|
19719
|
+
"start_web_dashboard",
|
|
19720
|
+
"Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
|
|
19721
|
+
{
|
|
19722
|
+
port: external_exports.number().optional().describe("Port to listen on (default: 3000)"),
|
|
19723
|
+
open: external_exports.boolean().optional().describe("Open the dashboard in the default browser (default: true)")
|
|
19724
|
+
},
|
|
19725
|
+
async (args) => {
|
|
19726
|
+
const port = args.port ?? 3e3;
|
|
19727
|
+
if (runningServer) {
|
|
19728
|
+
const url3 = `http://localhost:${runningServer.port}`;
|
|
19729
|
+
return {
|
|
19730
|
+
content: [{ type: "text", text: `Dashboard already running at ${url3}` }]
|
|
19731
|
+
};
|
|
19732
|
+
}
|
|
19733
|
+
const server = http2.createServer((req, res) => {
|
|
19734
|
+
handleRequest(req, res, store, projectName, navGroups);
|
|
19735
|
+
});
|
|
19736
|
+
await new Promise((resolve3, reject) => {
|
|
19737
|
+
server.on("error", reject);
|
|
19738
|
+
server.listen(port, () => resolve3());
|
|
19739
|
+
});
|
|
19740
|
+
runningServer = { server, port };
|
|
19741
|
+
const url2 = `http://localhost:${port}`;
|
|
19742
|
+
if (args.open !== false) {
|
|
19743
|
+
openBrowser(url2);
|
|
19744
|
+
}
|
|
19745
|
+
return {
|
|
19746
|
+
content: [{ type: "text", text: `Dashboard started at ${url2}` }]
|
|
19747
|
+
};
|
|
19748
|
+
}
|
|
19749
|
+
),
|
|
19750
|
+
tool20(
|
|
19751
|
+
"stop_web_dashboard",
|
|
19752
|
+
"Stop the running Marvin web dashboard.",
|
|
19753
|
+
{},
|
|
19754
|
+
async () => {
|
|
19755
|
+
if (!runningServer) {
|
|
19756
|
+
return {
|
|
19757
|
+
content: [{ type: "text", text: "No dashboard is currently running." }],
|
|
19758
|
+
isError: true
|
|
19759
|
+
};
|
|
19760
|
+
}
|
|
19761
|
+
await new Promise((resolve3) => {
|
|
19762
|
+
runningServer.server.close(() => resolve3());
|
|
19763
|
+
});
|
|
19764
|
+
runningServer = null;
|
|
19765
|
+
return {
|
|
19766
|
+
content: [{ type: "text", text: "Dashboard stopped." }]
|
|
19767
|
+
};
|
|
19768
|
+
}
|
|
19769
|
+
),
|
|
19770
|
+
tool20(
|
|
19771
|
+
"get_web_dashboard_urls",
|
|
19772
|
+
"Get all available dashboard page URLs. The dashboard must be running.",
|
|
19773
|
+
{},
|
|
19774
|
+
async () => {
|
|
19775
|
+
if (!runningServer) {
|
|
19776
|
+
return {
|
|
19777
|
+
content: [{ type: "text", text: "Dashboard is not running. Use start_web_dashboard first." }],
|
|
19778
|
+
isError: true
|
|
19779
|
+
};
|
|
19780
|
+
}
|
|
19781
|
+
const base = `http://localhost:${runningServer.port}`;
|
|
19782
|
+
const urls = {
|
|
19783
|
+
overview: base,
|
|
19784
|
+
gar: `${base}/gar`,
|
|
19785
|
+
board: `${base}/board`
|
|
19786
|
+
};
|
|
19787
|
+
for (const type of store.registeredTypes) {
|
|
19788
|
+
urls[type] = `${base}/docs/${type}`;
|
|
19789
|
+
}
|
|
19790
|
+
return {
|
|
19791
|
+
content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
|
|
19792
|
+
};
|
|
19793
|
+
},
|
|
19794
|
+
{ annotations: { readOnlyHint: true } }
|
|
19795
|
+
),
|
|
19796
|
+
tool20(
|
|
19797
|
+
"get_dashboard_overview",
|
|
19798
|
+
"Get the project overview data: document type counts and recent activity. Works without the web server running.",
|
|
19799
|
+
{},
|
|
19800
|
+
async () => {
|
|
19801
|
+
const data = getOverviewData(store);
|
|
19802
|
+
const result = {
|
|
19803
|
+
types: data.types,
|
|
19804
|
+
recent: data.recent.map((d) => ({
|
|
19805
|
+
id: d.frontmatter.id,
|
|
19806
|
+
type: d.frontmatter.type,
|
|
19807
|
+
title: d.frontmatter.title,
|
|
19808
|
+
status: d.frontmatter.status,
|
|
19809
|
+
updated: d.frontmatter.updated ?? d.frontmatter.created
|
|
19810
|
+
}))
|
|
19811
|
+
};
|
|
19812
|
+
return {
|
|
19813
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19814
|
+
};
|
|
19815
|
+
},
|
|
19816
|
+
{ annotations: { readOnlyHint: true } }
|
|
19817
|
+
),
|
|
19818
|
+
tool20(
|
|
19819
|
+
"get_dashboard_gar",
|
|
19820
|
+
"Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
|
|
19821
|
+
{},
|
|
19822
|
+
async () => {
|
|
19823
|
+
const report = getGarData(store, projectName);
|
|
19824
|
+
return {
|
|
19825
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
19826
|
+
};
|
|
19827
|
+
},
|
|
19828
|
+
{ annotations: { readOnlyHint: true } }
|
|
19829
|
+
),
|
|
19830
|
+
tool20(
|
|
19831
|
+
"get_dashboard_board",
|
|
19832
|
+
"Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
|
|
19833
|
+
{
|
|
19834
|
+
type: external_exports.string().optional().describe("Document type to filter by (e.g. 'decision', 'action')")
|
|
19835
|
+
},
|
|
19836
|
+
async (args) => {
|
|
19837
|
+
const data = getBoardData(store, args.type);
|
|
19838
|
+
const result = {
|
|
19839
|
+
type: data.type ?? "all",
|
|
19840
|
+
types: data.types,
|
|
19841
|
+
columns: data.columns.map((col) => ({
|
|
19842
|
+
status: col.status,
|
|
19843
|
+
count: col.docs.length,
|
|
19844
|
+
docs: col.docs.map((d) => ({
|
|
19845
|
+
id: d.frontmatter.id,
|
|
19846
|
+
type: d.frontmatter.type,
|
|
19847
|
+
title: d.frontmatter.title,
|
|
19848
|
+
owner: d.frontmatter.owner
|
|
19849
|
+
}))
|
|
19850
|
+
}))
|
|
19851
|
+
};
|
|
19852
|
+
return {
|
|
19853
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19854
|
+
};
|
|
19855
|
+
},
|
|
19856
|
+
{ annotations: { readOnlyHint: true } }
|
|
19857
|
+
)
|
|
19858
|
+
];
|
|
19859
|
+
}
|
|
19860
|
+
|
|
19861
|
+
// src/agent/mcp-server.ts
|
|
17946
19862
|
function createMarvinMcpServer(store, options) {
|
|
17947
19863
|
const tools = [
|
|
17948
19864
|
...createDecisionTools(store),
|
|
@@ -17952,7 +19868,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
17952
19868
|
...options?.manifest ? createSourceTools(options.manifest) : [],
|
|
17953
19869
|
...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
|
|
17954
19870
|
...options?.pluginTools ?? [],
|
|
17955
|
-
...options?.skillTools ?? []
|
|
19871
|
+
...options?.skillTools ?? [],
|
|
19872
|
+
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
|
|
17956
19873
|
];
|
|
17957
19874
|
return createSdkMcpServer({
|
|
17958
19875
|
name: "marvin-governance",
|
|
@@ -18024,7 +19941,7 @@ function createSkillActionTools(skills, context) {
|
|
|
18024
19941
|
if (!skill.actions) continue;
|
|
18025
19942
|
for (const action of skill.actions) {
|
|
18026
19943
|
tools.push(
|
|
18027
|
-
|
|
19944
|
+
tool21(
|
|
18028
19945
|
`${skill.id}__${action.id}`,
|
|
18029
19946
|
action.description,
|
|
18030
19947
|
{
|
|
@@ -18094,6 +20011,8 @@ var deliveryManager = {
|
|
|
18094
20011
|
|
|
18095
20012
|
## How You Work
|
|
18096
20013
|
- Review open actions (A-xxx) and follow up on overdue items
|
|
20014
|
+
- Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
|
|
20015
|
+
- Assign actions to sprints when sprint planning is active, using the sprints parameter
|
|
18097
20016
|
- Ensure decisions (D-xxx) are properly documented with rationale
|
|
18098
20017
|
- Track questions (Q-xxx) and ensure they get answered
|
|
18099
20018
|
- Monitor project health and flag risks early
|
|
@@ -18251,10 +20170,10 @@ ${lines.join("\n\n")}`;
|
|
|
18251
20170
|
}
|
|
18252
20171
|
|
|
18253
20172
|
// src/mcp/persona-tools.ts
|
|
18254
|
-
import { tool as
|
|
20173
|
+
import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
|
|
18255
20174
|
function createPersonaTools(ctx, marvinDir) {
|
|
18256
20175
|
return [
|
|
18257
|
-
|
|
20176
|
+
tool22(
|
|
18258
20177
|
"set_persona",
|
|
18259
20178
|
"Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
|
|
18260
20179
|
{
|
|
@@ -18284,7 +20203,7 @@ ${summaries}`
|
|
|
18284
20203
|
};
|
|
18285
20204
|
}
|
|
18286
20205
|
),
|
|
18287
|
-
|
|
20206
|
+
tool22(
|
|
18288
20207
|
"get_persona_guidance",
|
|
18289
20208
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
18290
20209
|
{
|
|
@@ -18319,7 +20238,7 @@ ${summaries}`
|
|
|
18319
20238
|
content: [{ type: "text", text: guidance }]
|
|
18320
20239
|
};
|
|
18321
20240
|
},
|
|
18322
|
-
{ annotations: {
|
|
20241
|
+
{ annotations: { readOnlyHint: true } }
|
|
18323
20242
|
)
|
|
18324
20243
|
];
|
|
18325
20244
|
}
|