mrvn-cli 0.5.4 → 0.5.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/dist/index.js +444 -33
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +433 -22
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +444 -33
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -223,9 +223,9 @@ var DocumentStore = class {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
-
list(
|
|
226
|
+
list(query9) {
|
|
227
227
|
const results = [];
|
|
228
|
-
const types =
|
|
228
|
+
const types = query9?.type ? [query9.type] : Object.keys(this.typeDirs);
|
|
229
229
|
for (const type of types) {
|
|
230
230
|
const dirName = this.typeDirs[type];
|
|
231
231
|
if (!dirName) continue;
|
|
@@ -236,9 +236,9 @@ var DocumentStore = class {
|
|
|
236
236
|
const filePath = path3.join(dir, file2);
|
|
237
237
|
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
238
238
|
const doc = parseDocument(raw, filePath);
|
|
239
|
-
if (
|
|
240
|
-
if (
|
|
241
|
-
if (
|
|
239
|
+
if (query9?.status && doc.frontmatter.status !== query9.status) continue;
|
|
240
|
+
if (query9?.owner && doc.frontmatter.owner !== query9.owner) continue;
|
|
241
|
+
if (query9?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query9.tag)))
|
|
242
242
|
continue;
|
|
243
243
|
results.push(doc);
|
|
244
244
|
}
|
|
@@ -316,14 +316,19 @@ var DocumentStore = class {
|
|
|
316
316
|
if (!existing) {
|
|
317
317
|
throw new Error(`Document ${id} not found`);
|
|
318
318
|
}
|
|
319
|
+
const keysToDelete = Object.entries(updates).filter(([, v]) => v === void 0).map(([k]) => k);
|
|
319
320
|
const cleanedUpdates = Object.fromEntries(
|
|
320
321
|
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
321
322
|
);
|
|
322
|
-
const
|
|
323
|
+
const merged = {
|
|
323
324
|
...existing.frontmatter,
|
|
324
325
|
...cleanedUpdates,
|
|
325
326
|
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
326
327
|
};
|
|
328
|
+
for (const key of keysToDelete) {
|
|
329
|
+
delete merged[key];
|
|
330
|
+
}
|
|
331
|
+
const updatedFrontmatter = merged;
|
|
327
332
|
const doc = {
|
|
328
333
|
frontmatter: updatedFrontmatter,
|
|
329
334
|
content: content ?? existing.content,
|
|
@@ -15874,16 +15879,13 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15874
15879
|
title: d.frontmatter.title,
|
|
15875
15880
|
type: d.frontmatter.type
|
|
15876
15881
|
}));
|
|
15877
|
-
const
|
|
15878
|
-
(d) => !DONE_STATUSES3.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15879
|
-
)
|
|
15880
|
-
|
|
15881
|
-
|
|
15882
|
-
|
|
15883
|
-
|
|
15884
|
-
type: d.frontmatter.type
|
|
15885
|
-
});
|
|
15886
|
-
}
|
|
15882
|
+
const risks = allDocs.filter(
|
|
15883
|
+
(d) => !DONE_STATUSES3.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15884
|
+
).map((d) => ({
|
|
15885
|
+
id: d.frontmatter.id,
|
|
15886
|
+
title: d.frontmatter.title,
|
|
15887
|
+
type: d.frontmatter.type
|
|
15888
|
+
}));
|
|
15887
15889
|
let velocity = null;
|
|
15888
15890
|
const currentRate = workItems.completionPct;
|
|
15889
15891
|
const completedSprints = sprintDocs.filter((s) => DONE_STATUSES3.has(s.frontmatter.status) && s.frontmatter.id !== fm.id).sort((a, b) => (b.frontmatter.endDate ?? "").localeCompare(a.frontmatter.endDate ?? ""));
|
|
@@ -15919,6 +15921,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15919
15921
|
openActions,
|
|
15920
15922
|
openQuestions,
|
|
15921
15923
|
blockers,
|
|
15924
|
+
risks,
|
|
15922
15925
|
velocity
|
|
15923
15926
|
};
|
|
15924
15927
|
}
|
|
@@ -16714,6 +16717,7 @@ a:hover { text-decoration: underline; }
|
|
|
16714
16717
|
}
|
|
16715
16718
|
|
|
16716
16719
|
.card a { color: inherit; text-decoration: none; display: block; }
|
|
16720
|
+
a.card-link { color: inherit; text-decoration: none; cursor: pointer; }
|
|
16717
16721
|
|
|
16718
16722
|
.card .card-label {
|
|
16719
16723
|
font-size: 0.75rem;
|
|
@@ -17129,6 +17133,68 @@ tr:hover td {
|
|
|
17129
17133
|
.priority-medium { color: var(--amber); }
|
|
17130
17134
|
.priority-low { color: var(--green); }
|
|
17131
17135
|
|
|
17136
|
+
/* Blocker / Risk detail cards */
|
|
17137
|
+
.blocker-card {
|
|
17138
|
+
background: var(--bg-card);
|
|
17139
|
+
border: 1px solid var(--border);
|
|
17140
|
+
border-radius: var(--radius);
|
|
17141
|
+
padding: 1.25rem;
|
|
17142
|
+
margin-bottom: 1rem;
|
|
17143
|
+
}
|
|
17144
|
+
.blocker-card-header {
|
|
17145
|
+
display: flex;
|
|
17146
|
+
align-items: center;
|
|
17147
|
+
gap: 0.5rem;
|
|
17148
|
+
font-size: 0.85rem;
|
|
17149
|
+
margin-bottom: 0.25rem;
|
|
17150
|
+
}
|
|
17151
|
+
.blocker-card-title {
|
|
17152
|
+
margin: 0.25rem 0 0.5rem;
|
|
17153
|
+
font-size: 1rem;
|
|
17154
|
+
}
|
|
17155
|
+
.blocker-card-meta {
|
|
17156
|
+
display: flex;
|
|
17157
|
+
flex-wrap: wrap;
|
|
17158
|
+
gap: 1rem;
|
|
17159
|
+
font-size: 0.85rem;
|
|
17160
|
+
color: var(--text-dim);
|
|
17161
|
+
margin-bottom: 0.75rem;
|
|
17162
|
+
}
|
|
17163
|
+
.blocker-card-content {
|
|
17164
|
+
border-top: 1px solid var(--border);
|
|
17165
|
+
padding-top: 0.75rem;
|
|
17166
|
+
font-size: 0.9rem;
|
|
17167
|
+
}
|
|
17168
|
+
.risk-assessment-content {
|
|
17169
|
+
background: var(--bg);
|
|
17170
|
+
border: 1px solid var(--border);
|
|
17171
|
+
border-left: 3px solid var(--amber);
|
|
17172
|
+
border-radius: var(--radius);
|
|
17173
|
+
padding: 1rem 1.25rem;
|
|
17174
|
+
margin-top: 0.75rem;
|
|
17175
|
+
font-size: 0.9rem;
|
|
17176
|
+
}
|
|
17177
|
+
.risk-assess-btn {
|
|
17178
|
+
font-size: 0.8rem;
|
|
17179
|
+
padding: 0.4rem 0.8rem;
|
|
17180
|
+
margin-top: 0.75rem;
|
|
17181
|
+
}
|
|
17182
|
+
.risk-assess-loading {
|
|
17183
|
+
margin-top: 0.75rem;
|
|
17184
|
+
font-size: 0.85rem;
|
|
17185
|
+
}
|
|
17186
|
+
.risk-assess-error {
|
|
17187
|
+
margin-top: 0.5rem;
|
|
17188
|
+
}
|
|
17189
|
+
.risk-assessment-label {
|
|
17190
|
+
font-size: 0.7rem;
|
|
17191
|
+
text-transform: uppercase;
|
|
17192
|
+
letter-spacing: 0.08em;
|
|
17193
|
+
color: var(--amber);
|
|
17194
|
+
font-weight: 600;
|
|
17195
|
+
margin-bottom: 0.5rem;
|
|
17196
|
+
}
|
|
17197
|
+
|
|
17132
17198
|
/* Health */
|
|
17133
17199
|
.health-section-title {
|
|
17134
17200
|
font-size: 1.1rem;
|
|
@@ -18356,6 +18422,13 @@ function buildPrompt(data) {
|
|
|
18356
18422
|
sections.push(`- ${b.id} (${b.type}): ${b.title}`);
|
|
18357
18423
|
}
|
|
18358
18424
|
}
|
|
18425
|
+
if (data.risks.length > 0) {
|
|
18426
|
+
sections.push(`
|
|
18427
|
+
## Risks`);
|
|
18428
|
+
for (const r of data.risks) {
|
|
18429
|
+
sections.push(`- ${r.id} (${r.type}): ${r.title}`);
|
|
18430
|
+
}
|
|
18431
|
+
}
|
|
18359
18432
|
if (data.velocity) {
|
|
18360
18433
|
sections.push(`
|
|
18361
18434
|
## Velocity`);
|
|
@@ -18367,6 +18440,134 @@ function buildPrompt(data) {
|
|
|
18367
18440
|
return sections.join("\n");
|
|
18368
18441
|
}
|
|
18369
18442
|
|
|
18443
|
+
// src/reports/sprint-summary/risk-assessment.ts
|
|
18444
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
18445
|
+
var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
|
|
18446
|
+
|
|
18447
|
+
IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
|
|
18448
|
+
|
|
18449
|
+
Produce a concise markdown assessment with these sections:
|
|
18450
|
+
|
|
18451
|
+
## Status Assessment
|
|
18452
|
+
One-line verdict: is this risk actively being mitigated, stalled, or escalating?
|
|
18453
|
+
|
|
18454
|
+
## Related Activity
|
|
18455
|
+
What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
|
|
18456
|
+
|
|
18457
|
+
## Trajectory
|
|
18458
|
+
Based on the data (status of related items, time remaining, ownership), is this risk trending toward resolution or toward becoming a blocker? Explain your reasoning with concrete evidence.
|
|
18459
|
+
|
|
18460
|
+
## Recommendation
|
|
18461
|
+
One concrete next step to move this risk toward resolution.
|
|
18462
|
+
|
|
18463
|
+
Rules:
|
|
18464
|
+
- Reference artifact IDs, dates, owners, and statuses from the provided data
|
|
18465
|
+
- Keep the tone professional and direct
|
|
18466
|
+
- Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
|
|
18467
|
+
- Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
|
|
18468
|
+
- Produce the full assessment text directly`;
|
|
18469
|
+
async function generateRiskAssessment(data, riskId, store) {
|
|
18470
|
+
const risk = data.risks.find((r) => r.id === riskId);
|
|
18471
|
+
if (!risk) return "Risk not found in sprint data.";
|
|
18472
|
+
const prompt = buildSingleRiskPrompt(data, risk, store);
|
|
18473
|
+
const result = query2({
|
|
18474
|
+
prompt,
|
|
18475
|
+
options: {
|
|
18476
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
18477
|
+
maxTurns: 1,
|
|
18478
|
+
tools: [],
|
|
18479
|
+
allowedTools: []
|
|
18480
|
+
}
|
|
18481
|
+
});
|
|
18482
|
+
for await (const msg of result) {
|
|
18483
|
+
if (msg.type === "assistant") {
|
|
18484
|
+
const text = msg.message.content.find(
|
|
18485
|
+
(b) => b.type === "text"
|
|
18486
|
+
);
|
|
18487
|
+
if (text) return text.text;
|
|
18488
|
+
}
|
|
18489
|
+
}
|
|
18490
|
+
return "Unable to generate risk assessment.";
|
|
18491
|
+
}
|
|
18492
|
+
function buildSingleRiskPrompt(data, risk, store) {
|
|
18493
|
+
const sections = [];
|
|
18494
|
+
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
18495
|
+
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
18496
|
+
sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
|
|
18497
|
+
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
18498
|
+
sections.push("");
|
|
18499
|
+
const doc = store.get(risk.id);
|
|
18500
|
+
sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
|
|
18501
|
+
sections.push(`Type: ${risk.type}`);
|
|
18502
|
+
if (doc) {
|
|
18503
|
+
sections.push(`Status: ${doc.frontmatter.status}`);
|
|
18504
|
+
if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
|
|
18505
|
+
if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
|
|
18506
|
+
if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
|
|
18507
|
+
if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
|
|
18508
|
+
if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
|
|
18509
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
18510
|
+
if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
|
|
18511
|
+
if (doc.content.trim()) {
|
|
18512
|
+
sections.push(`
|
|
18513
|
+
Description:
|
|
18514
|
+
${doc.content.trim()}`);
|
|
18515
|
+
}
|
|
18516
|
+
const allDocs = store.list();
|
|
18517
|
+
const relatedIds = /* @__PURE__ */ new Set();
|
|
18518
|
+
for (const d of allDocs) {
|
|
18519
|
+
if (d.frontmatter.aboutArtifact === risk.id) {
|
|
18520
|
+
relatedIds.add(d.frontmatter.id);
|
|
18521
|
+
}
|
|
18522
|
+
}
|
|
18523
|
+
const idPattern = /\b([A-Z]-\d{3,})\b/g;
|
|
18524
|
+
let match;
|
|
18525
|
+
while ((match = idPattern.exec(doc.content)) !== null) {
|
|
18526
|
+
relatedIds.add(match[1]);
|
|
18527
|
+
}
|
|
18528
|
+
const significantTags = tags.filter(
|
|
18529
|
+
(t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
|
|
18530
|
+
);
|
|
18531
|
+
if (significantTags.length > 0) {
|
|
18532
|
+
for (const d of allDocs) {
|
|
18533
|
+
if (d.frontmatter.id === risk.id) continue;
|
|
18534
|
+
const dTags = d.frontmatter.tags ?? [];
|
|
18535
|
+
if (significantTags.some((t) => dTags.includes(t))) {
|
|
18536
|
+
relatedIds.add(d.frontmatter.id);
|
|
18537
|
+
}
|
|
18538
|
+
}
|
|
18539
|
+
}
|
|
18540
|
+
const about = doc.frontmatter.aboutArtifact;
|
|
18541
|
+
if (about) {
|
|
18542
|
+
relatedIds.add(about);
|
|
18543
|
+
for (const d of allDocs) {
|
|
18544
|
+
if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
|
|
18545
|
+
relatedIds.add(d.frontmatter.id);
|
|
18546
|
+
}
|
|
18547
|
+
}
|
|
18548
|
+
}
|
|
18549
|
+
const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
|
|
18550
|
+
if (relatedDocs.length > 0) {
|
|
18551
|
+
sections.push(`
|
|
18552
|
+
## Related Documents (${relatedDocs.length})`);
|
|
18553
|
+
for (const rd of relatedDocs) {
|
|
18554
|
+
const owner = rd.frontmatter.owner ?? "unowned";
|
|
18555
|
+
const summary = rd.content.trim().slice(0, 300);
|
|
18556
|
+
sections.push(
|
|
18557
|
+
`- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
|
|
18558
|
+
);
|
|
18559
|
+
sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
|
|
18560
|
+
if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
|
|
18561
|
+
}
|
|
18562
|
+
}
|
|
18563
|
+
}
|
|
18564
|
+
sections.push("");
|
|
18565
|
+
sections.push(`---`);
|
|
18566
|
+
sections.push(`
|
|
18567
|
+
Generate the risk assessment for ${risk.id} based on the data above.`);
|
|
18568
|
+
return sections.join("\n");
|
|
18569
|
+
}
|
|
18570
|
+
|
|
18370
18571
|
// src/web/templates/persona-switcher.ts
|
|
18371
18572
|
function renderPersonaSwitcher(current, _currentPath) {
|
|
18372
18573
|
const views = getAllPersonaViews();
|
|
@@ -19626,11 +19827,16 @@ function sprintSummaryPage(data, cached2) {
|
|
|
19626
19827
|
<div class="card-value">${data.linkedEpics.length}</div>
|
|
19627
19828
|
<div class="card-sub">linked to sprint</div>
|
|
19628
19829
|
</div>
|
|
19629
|
-
<
|
|
19830
|
+
<a class="card card-link" href="sprint-blockers">
|
|
19630
19831
|
<div class="card-label">Blockers</div>
|
|
19631
19832
|
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
19632
|
-
<div class="card-sub">${data.
|
|
19633
|
-
</
|
|
19833
|
+
<div class="card-sub">${data.workItems.blocked} blocked items</div>
|
|
19834
|
+
</a>
|
|
19835
|
+
<a class="card card-link" href="sprint-risks">
|
|
19836
|
+
<div class="card-label">Risks</div>
|
|
19837
|
+
<div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
|
|
19838
|
+
<div class="card-sub">open risk items</div>
|
|
19839
|
+
</a>
|
|
19634
19840
|
</div>`;
|
|
19635
19841
|
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
19636
19842
|
"ss-epics",
|
|
@@ -21020,6 +21226,168 @@ function upcomingPage(data) {
|
|
|
21020
21226
|
`;
|
|
21021
21227
|
}
|
|
21022
21228
|
|
|
21229
|
+
// src/web/templates/pages/sprint-blockers.ts
|
|
21230
|
+
function sprintBlockersPage(data, store) {
|
|
21231
|
+
if (!data) {
|
|
21232
|
+
return `
|
|
21233
|
+
<div class="page-header">
|
|
21234
|
+
<h2>Sprint Blockers</h2>
|
|
21235
|
+
<div class="subtitle">Blocked items in the active sprint</div>
|
|
21236
|
+
</div>
|
|
21237
|
+
<div class="empty">
|
|
21238
|
+
<h3>No Active Sprint</h3>
|
|
21239
|
+
<p>No active sprint found.</p>
|
|
21240
|
+
</div>`;
|
|
21241
|
+
}
|
|
21242
|
+
const blockerDocs = data.blockers.map((b) => {
|
|
21243
|
+
const doc = store.get(b.id);
|
|
21244
|
+
return { ...b, doc };
|
|
21245
|
+
});
|
|
21246
|
+
const statsCards = `
|
|
21247
|
+
<div class="cards">
|
|
21248
|
+
<div class="card">
|
|
21249
|
+
<div class="card-label">Blocked Items</div>
|
|
21250
|
+
<div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
|
|
21251
|
+
<div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
|
|
21252
|
+
</div>
|
|
21253
|
+
</div>`;
|
|
21254
|
+
const itemCards = blockerDocs.map((b) => {
|
|
21255
|
+
const doc = b.doc;
|
|
21256
|
+
const owner = doc?.frontmatter.owner;
|
|
21257
|
+
const assignee = doc?.frontmatter.assignee;
|
|
21258
|
+
const content = doc?.content?.trim();
|
|
21259
|
+
return `
|
|
21260
|
+
<div class="blocker-card">
|
|
21261
|
+
<div class="blocker-card-header">
|
|
21262
|
+
<a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
|
|
21263
|
+
<span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
|
|
21264
|
+
${statusBadge("blocked")}
|
|
21265
|
+
</div>
|
|
21266
|
+
<h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
|
|
21267
|
+
<div class="blocker-card-meta">
|
|
21268
|
+
${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
|
|
21269
|
+
${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
|
|
21270
|
+
${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
|
|
21271
|
+
</div>
|
|
21272
|
+
${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
|
|
21273
|
+
</div>`;
|
|
21274
|
+
}).join("");
|
|
21275
|
+
const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
|
|
21276
|
+
return `
|
|
21277
|
+
<div class="page-header">
|
|
21278
|
+
<h2>Sprint Blockers</h2>
|
|
21279
|
+
<div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
|
|
21280
|
+
</div>
|
|
21281
|
+
${statsCards}
|
|
21282
|
+
${emptyMessage}
|
|
21283
|
+
${itemCards}`;
|
|
21284
|
+
}
|
|
21285
|
+
|
|
21286
|
+
// src/web/templates/pages/sprint-risks.ts
|
|
21287
|
+
function sprintRisksPage(data, store) {
|
|
21288
|
+
if (!data) {
|
|
21289
|
+
return `
|
|
21290
|
+
<div class="page-header">
|
|
21291
|
+
<h2>Sprint Risks</h2>
|
|
21292
|
+
<div class="subtitle">Risk items in the active sprint</div>
|
|
21293
|
+
</div>
|
|
21294
|
+
<div class="empty">
|
|
21295
|
+
<h3>No Active Sprint</h3>
|
|
21296
|
+
<p>No active sprint found.</p>
|
|
21297
|
+
</div>`;
|
|
21298
|
+
}
|
|
21299
|
+
const riskDocs = data.risks.map((r) => {
|
|
21300
|
+
const doc = store.get(r.id);
|
|
21301
|
+
return { ...r, doc };
|
|
21302
|
+
});
|
|
21303
|
+
const statsCards = `
|
|
21304
|
+
<div class="cards">
|
|
21305
|
+
<div class="card">
|
|
21306
|
+
<div class="card-label">Open Risks</div>
|
|
21307
|
+
<div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
|
|
21308
|
+
<div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
|
|
21309
|
+
</div>
|
|
21310
|
+
</div>`;
|
|
21311
|
+
const itemCards = riskDocs.map((r) => {
|
|
21312
|
+
const doc = r.doc;
|
|
21313
|
+
const owner = doc?.frontmatter.owner;
|
|
21314
|
+
const assignee = doc?.frontmatter.assignee;
|
|
21315
|
+
const content = doc?.content?.trim();
|
|
21316
|
+
return `
|
|
21317
|
+
<div class="blocker-card" id="risk-${escapeHtml(r.id)}">
|
|
21318
|
+
<div class="blocker-card-header">
|
|
21319
|
+
<a href="/docs/${escapeHtml(r.type)}/${escapeHtml(r.id)}">${escapeHtml(r.id)}</a>
|
|
21320
|
+
<span class="text-dim">${escapeHtml(typeLabel(r.type))}</span>
|
|
21321
|
+
${statusBadge(doc?.frontmatter.status ?? "open")}
|
|
21322
|
+
</div>
|
|
21323
|
+
<h4 class="blocker-card-title">${escapeHtml(r.title)}</h4>
|
|
21324
|
+
<div class="blocker-card-meta">
|
|
21325
|
+
${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
|
|
21326
|
+
${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
|
|
21327
|
+
${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
|
|
21328
|
+
</div>
|
|
21329
|
+
${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
|
|
21330
|
+
<div class="risk-assessment" id="assessment-${escapeHtml(r.id)}">
|
|
21331
|
+
<button class="sprint-generate-btn risk-assess-btn" onclick="generateAssessment('${escapeHtml(r.id)}', this)">Assess Risk</button>
|
|
21332
|
+
<div class="sprint-loading risk-assess-loading" style="display:none">
|
|
21333
|
+
<div class="sprint-spinner"></div>
|
|
21334
|
+
<span>Analyzing...</span>
|
|
21335
|
+
</div>
|
|
21336
|
+
<div class="sprint-error risk-assess-error" style="display:none"></div>
|
|
21337
|
+
<div class="risk-assessment-content detail-content" style="display:none"></div>
|
|
21338
|
+
</div>
|
|
21339
|
+
</div>`;
|
|
21340
|
+
}).join("");
|
|
21341
|
+
const emptyMessage = riskDocs.length === 0 ? `<div class="empty"><h3>No Risks</h3><p>No open risk items in this sprint.</p></div>` : "";
|
|
21342
|
+
const script = riskDocs.length > 0 ? `
|
|
21343
|
+
<script>
|
|
21344
|
+
async function generateAssessment(riskId, btn) {
|
|
21345
|
+
var container = document.getElementById('assessment-' + riskId);
|
|
21346
|
+
var loading = container.querySelector('.risk-assess-loading');
|
|
21347
|
+
var errorEl = container.querySelector('.risk-assess-error');
|
|
21348
|
+
var contentEl = container.querySelector('.risk-assessment-content');
|
|
21349
|
+
|
|
21350
|
+
btn.disabled = true;
|
|
21351
|
+
btn.style.display = 'none';
|
|
21352
|
+
loading.style.display = 'flex';
|
|
21353
|
+
errorEl.style.display = 'none';
|
|
21354
|
+
|
|
21355
|
+
try {
|
|
21356
|
+
var res = await fetch('/api/risk-assessment', {
|
|
21357
|
+
method: 'POST',
|
|
21358
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21359
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}', riskId: riskId })
|
|
21360
|
+
});
|
|
21361
|
+
var json = await res.json();
|
|
21362
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate assessment');
|
|
21363
|
+
|
|
21364
|
+
contentEl.innerHTML = '<div class="risk-assessment-label">AI Assessment</div>' + json.html;
|
|
21365
|
+
contentEl.style.display = 'block';
|
|
21366
|
+
|
|
21367
|
+
loading.style.display = 'none';
|
|
21368
|
+
btn.textContent = 'Regenerate';
|
|
21369
|
+
btn.style.display = '';
|
|
21370
|
+
btn.disabled = false;
|
|
21371
|
+
} catch (e) {
|
|
21372
|
+
loading.style.display = 'none';
|
|
21373
|
+
errorEl.textContent = e.message;
|
|
21374
|
+
errorEl.style.display = 'block';
|
|
21375
|
+
btn.style.display = '';
|
|
21376
|
+
btn.disabled = false;
|
|
21377
|
+
}
|
|
21378
|
+
}
|
|
21379
|
+
</script>` : "";
|
|
21380
|
+
return `
|
|
21381
|
+
<div class="page-header">
|
|
21382
|
+
<h2>Sprint Risks</h2>
|
|
21383
|
+
<div class="subtitle">Risk items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
|
|
21384
|
+
</div>
|
|
21385
|
+
${statsCards}
|
|
21386
|
+
${emptyMessage}
|
|
21387
|
+
${itemCards}
|
|
21388
|
+
${script}`;
|
|
21389
|
+
}
|
|
21390
|
+
|
|
21023
21391
|
// src/web/templates/pages/shared-wrappers.ts
|
|
21024
21392
|
function sharedTimelinePage(ctx) {
|
|
21025
21393
|
const diagrams = getDiagramData(ctx.store);
|
|
@@ -21049,6 +21417,16 @@ function sharedSprintSummaryPage(ctx) {
|
|
|
21049
21417
|
const data = getSprintSummaryData(ctx.store, sprintId);
|
|
21050
21418
|
return sprintSummaryPage(data);
|
|
21051
21419
|
}
|
|
21420
|
+
function sharedSprintBlockersPage(ctx) {
|
|
21421
|
+
const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
|
|
21422
|
+
const data = getSprintSummaryData(ctx.store, sprintId);
|
|
21423
|
+
return sprintBlockersPage(data, ctx.store);
|
|
21424
|
+
}
|
|
21425
|
+
function sharedSprintRisksPage(ctx) {
|
|
21426
|
+
const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
|
|
21427
|
+
const data = getSprintSummaryData(ctx.store, sprintId);
|
|
21428
|
+
return sprintRisksPage(data, ctx.store);
|
|
21429
|
+
}
|
|
21052
21430
|
|
|
21053
21431
|
// src/web/shared-page-registration.ts
|
|
21054
21432
|
var SHARED_PAGES = [
|
|
@@ -21057,7 +21435,9 @@ var SHARED_PAGES = [
|
|
|
21057
21435
|
{ pageId: "upcoming", renderer: sharedUpcomingPage },
|
|
21058
21436
|
{ pageId: "gar", renderer: sharedGarPage },
|
|
21059
21437
|
{ pageId: "health", renderer: sharedHealthPage },
|
|
21060
|
-
{ pageId: "sprint-summary", renderer: sharedSprintSummaryPage }
|
|
21438
|
+
{ pageId: "sprint-summary", renderer: sharedSprintSummaryPage },
|
|
21439
|
+
{ pageId: "sprint-blockers", renderer: sharedSprintBlockersPage },
|
|
21440
|
+
{ pageId: "sprint-risks", renderer: sharedSprintRisksPage }
|
|
21061
21441
|
];
|
|
21062
21442
|
for (const persona of ["po", "dm", "tl"]) {
|
|
21063
21443
|
for (const { pageId, renderer } of SHARED_PAGES) {
|
|
@@ -21258,6 +21638,37 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
21258
21638
|
});
|
|
21259
21639
|
return;
|
|
21260
21640
|
}
|
|
21641
|
+
if (pathname === "/api/risk-assessment" && req.method === "POST") {
|
|
21642
|
+
let bodyStr = "";
|
|
21643
|
+
req.on("data", (chunk) => {
|
|
21644
|
+
bodyStr += chunk;
|
|
21645
|
+
});
|
|
21646
|
+
req.on("end", async () => {
|
|
21647
|
+
try {
|
|
21648
|
+
const { sprintId, riskId } = JSON.parse(bodyStr || "{}");
|
|
21649
|
+
if (!riskId) {
|
|
21650
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
21651
|
+
res.end(JSON.stringify({ error: "riskId is required" }));
|
|
21652
|
+
return;
|
|
21653
|
+
}
|
|
21654
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
21655
|
+
if (!data) {
|
|
21656
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
21657
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
21658
|
+
return;
|
|
21659
|
+
}
|
|
21660
|
+
const markdown = await generateRiskAssessment(data, riskId, store);
|
|
21661
|
+
const html = renderMarkdown(markdown);
|
|
21662
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
21663
|
+
res.end(JSON.stringify({ riskId, html }));
|
|
21664
|
+
} catch (err) {
|
|
21665
|
+
console.error("[marvin web] Risk assessment generation error:", err);
|
|
21666
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
21667
|
+
res.end(JSON.stringify({ error: "Failed to generate risk assessment" }));
|
|
21668
|
+
}
|
|
21669
|
+
});
|
|
21670
|
+
return;
|
|
21671
|
+
}
|
|
21261
21672
|
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
21262
21673
|
if (detailMatch) {
|
|
21263
21674
|
const [, type, id] = detailMatch;
|
|
@@ -26041,7 +26452,7 @@ import * as readline from "readline";
|
|
|
26041
26452
|
import chalk from "chalk";
|
|
26042
26453
|
import ora from "ora";
|
|
26043
26454
|
import {
|
|
26044
|
-
query as
|
|
26455
|
+
query as query4
|
|
26045
26456
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
26046
26457
|
|
|
26047
26458
|
// src/storage/session-store.ts
|
|
@@ -26112,11 +26523,11 @@ var SessionStore = class {
|
|
|
26112
26523
|
};
|
|
26113
26524
|
|
|
26114
26525
|
// src/agent/session-namer.ts
|
|
26115
|
-
import { query as
|
|
26526
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
26116
26527
|
async function generateSessionName(turns) {
|
|
26117
26528
|
try {
|
|
26118
26529
|
const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
|
|
26119
|
-
const result =
|
|
26530
|
+
const result = query3({
|
|
26120
26531
|
prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
|
|
26121
26532
|
|
|
26122
26533
|
${transcript}`,
|
|
@@ -26394,7 +26805,7 @@ Marvin \u2014 ${persona.name}
|
|
|
26394
26805
|
if (existingSession) {
|
|
26395
26806
|
queryOptions.resume = existingSession.id;
|
|
26396
26807
|
}
|
|
26397
|
-
const conversation =
|
|
26808
|
+
const conversation = query4({
|
|
26398
26809
|
prompt,
|
|
26399
26810
|
options: queryOptions
|
|
26400
26811
|
});
|
|
@@ -26486,7 +26897,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
26486
26897
|
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
26487
26898
|
|
|
26488
26899
|
// src/skills/action-runner.ts
|
|
26489
|
-
import { query as
|
|
26900
|
+
import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
|
|
26490
26901
|
var GOVERNANCE_TOOL_NAMES2 = [
|
|
26491
26902
|
"mcp__marvin-governance__list_decisions",
|
|
26492
26903
|
"mcp__marvin-governance__get_decision",
|
|
@@ -26508,7 +26919,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
26508
26919
|
try {
|
|
26509
26920
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
26510
26921
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
|
|
26511
|
-
const conversation =
|
|
26922
|
+
const conversation = query5({
|
|
26512
26923
|
prompt: userPrompt,
|
|
26513
26924
|
options: {
|
|
26514
26925
|
systemPrompt: action.systemPrompt,
|
|
@@ -27302,7 +27713,7 @@ import * as fs13 from "fs";
|
|
|
27302
27713
|
import * as path13 from "path";
|
|
27303
27714
|
import chalk7 from "chalk";
|
|
27304
27715
|
import ora2 from "ora";
|
|
27305
|
-
import { query as
|
|
27716
|
+
import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
|
|
27306
27717
|
|
|
27307
27718
|
// src/sources/prompts.ts
|
|
27308
27719
|
function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -27435,7 +27846,7 @@ async function ingestFile(options) {
|
|
|
27435
27846
|
const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
|
|
27436
27847
|
spinner.start();
|
|
27437
27848
|
try {
|
|
27438
|
-
const conversation =
|
|
27849
|
+
const conversation = query6({
|
|
27439
27850
|
prompt: userPrompt,
|
|
27440
27851
|
options: {
|
|
27441
27852
|
systemPrompt,
|
|
@@ -28656,7 +29067,7 @@ import chalk13 from "chalk";
|
|
|
28656
29067
|
// src/analysis/analyze.ts
|
|
28657
29068
|
import chalk12 from "chalk";
|
|
28658
29069
|
import ora4 from "ora";
|
|
28659
|
-
import { query as
|
|
29070
|
+
import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
|
|
28660
29071
|
|
|
28661
29072
|
// src/analysis/prompts.ts
|
|
28662
29073
|
function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -28786,7 +29197,7 @@ async function analyzeMeeting(options) {
|
|
|
28786
29197
|
const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
|
|
28787
29198
|
spinner.start();
|
|
28788
29199
|
try {
|
|
28789
|
-
const conversation =
|
|
29200
|
+
const conversation = query7({
|
|
28790
29201
|
prompt: userPrompt,
|
|
28791
29202
|
options: {
|
|
28792
29203
|
systemPrompt,
|
|
@@ -28913,7 +29324,7 @@ import chalk15 from "chalk";
|
|
|
28913
29324
|
// src/contributions/contribute.ts
|
|
28914
29325
|
import chalk14 from "chalk";
|
|
28915
29326
|
import ora5 from "ora";
|
|
28916
|
-
import { query as
|
|
29327
|
+
import { query as query8 } from "@anthropic-ai/claude-agent-sdk";
|
|
28917
29328
|
|
|
28918
29329
|
// src/contributions/prompts.ts
|
|
28919
29330
|
function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
|
|
@@ -29167,7 +29578,7 @@ async function contributeFromPersona(options) {
|
|
|
29167
29578
|
"mcp__marvin-governance__get_action",
|
|
29168
29579
|
"mcp__marvin-governance__get_question"
|
|
29169
29580
|
];
|
|
29170
|
-
const conversation =
|
|
29581
|
+
const conversation = query8({
|
|
29171
29582
|
prompt: userPrompt,
|
|
29172
29583
|
options: {
|
|
29173
29584
|
systemPrompt,
|
|
@@ -29663,7 +30074,7 @@ function createProgram() {
|
|
|
29663
30074
|
const program = new Command();
|
|
29664
30075
|
program.name("marvin").description(
|
|
29665
30076
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
29666
|
-
).version("0.5.
|
|
30077
|
+
).version("0.5.5");
|
|
29667
30078
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
29668
30079
|
await initCommand();
|
|
29669
30080
|
});
|