mrvn-cli 0.5.3 → 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 +445 -34
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +434 -23
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +445 -34
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -176,9 +176,9 @@ var DocumentStore = class {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
list(
|
|
179
|
+
list(query4) {
|
|
180
180
|
const results = [];
|
|
181
|
-
const types =
|
|
181
|
+
const types = query4?.type ? [query4.type] : Object.keys(this.typeDirs);
|
|
182
182
|
for (const type of types) {
|
|
183
183
|
const dirName = this.typeDirs[type];
|
|
184
184
|
if (!dirName) continue;
|
|
@@ -189,9 +189,9 @@ var DocumentStore = class {
|
|
|
189
189
|
const filePath = path3.join(dir, file2);
|
|
190
190
|
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
191
191
|
const doc = parseDocument(raw, filePath);
|
|
192
|
-
if (
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
192
|
+
if (query4?.status && doc.frontmatter.status !== query4.status) continue;
|
|
193
|
+
if (query4?.owner && doc.frontmatter.owner !== query4.owner) continue;
|
|
194
|
+
if (query4?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query4.tag)))
|
|
195
195
|
continue;
|
|
196
196
|
results.push(doc);
|
|
197
197
|
}
|
|
@@ -269,14 +269,19 @@ var DocumentStore = class {
|
|
|
269
269
|
if (!existing) {
|
|
270
270
|
throw new Error(`Document ${id} not found`);
|
|
271
271
|
}
|
|
272
|
+
const keysToDelete = Object.entries(updates).filter(([, v]) => v === void 0).map(([k]) => k);
|
|
272
273
|
const cleanedUpdates = Object.fromEntries(
|
|
273
274
|
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
274
275
|
);
|
|
275
|
-
const
|
|
276
|
+
const merged = {
|
|
276
277
|
...existing.frontmatter,
|
|
277
278
|
...cleanedUpdates,
|
|
278
279
|
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
279
280
|
};
|
|
281
|
+
for (const key of keysToDelete) {
|
|
282
|
+
delete merged[key];
|
|
283
|
+
}
|
|
284
|
+
const updatedFrontmatter = merged;
|
|
280
285
|
const doc = {
|
|
281
286
|
frontmatter: updatedFrontmatter,
|
|
282
287
|
content: content ?? existing.content,
|
|
@@ -15672,7 +15677,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15672
15677
|
});
|
|
15673
15678
|
const sprintTag = `sprint:${fm.id}`;
|
|
15674
15679
|
const workItemDocs = allDocs.filter(
|
|
15675
|
-
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.tags?.includes(sprintTag)
|
|
15680
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.type !== "decision" && d.frontmatter.type !== "question" && d.frontmatter.tags?.includes(sprintTag)
|
|
15676
15681
|
);
|
|
15677
15682
|
const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
|
|
15678
15683
|
const byStatus = {};
|
|
@@ -15810,16 +15815,13 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15810
15815
|
title: d.frontmatter.title,
|
|
15811
15816
|
type: d.frontmatter.type
|
|
15812
15817
|
}));
|
|
15813
|
-
const
|
|
15814
|
-
(d) => !DONE_STATUSES2.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15815
|
-
)
|
|
15816
|
-
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
type: d.frontmatter.type
|
|
15821
|
-
});
|
|
15822
|
-
}
|
|
15818
|
+
const risks = allDocs.filter(
|
|
15819
|
+
(d) => !DONE_STATUSES2.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15820
|
+
).map((d) => ({
|
|
15821
|
+
id: d.frontmatter.id,
|
|
15822
|
+
title: d.frontmatter.title,
|
|
15823
|
+
type: d.frontmatter.type
|
|
15824
|
+
}));
|
|
15823
15825
|
let velocity = null;
|
|
15824
15826
|
const currentRate = workItems.completionPct;
|
|
15825
15827
|
const completedSprints = sprintDocs.filter((s) => DONE_STATUSES2.has(s.frontmatter.status) && s.frontmatter.id !== fm.id).sort((a, b) => (b.frontmatter.endDate ?? "").localeCompare(a.frontmatter.endDate ?? ""));
|
|
@@ -15855,6 +15857,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15855
15857
|
openActions,
|
|
15856
15858
|
openQuestions,
|
|
15857
15859
|
blockers,
|
|
15860
|
+
risks,
|
|
15858
15861
|
velocity
|
|
15859
15862
|
};
|
|
15860
15863
|
}
|
|
@@ -16440,6 +16443,13 @@ function buildPrompt(data) {
|
|
|
16440
16443
|
sections.push(`- ${b.id} (${b.type}): ${b.title}`);
|
|
16441
16444
|
}
|
|
16442
16445
|
}
|
|
16446
|
+
if (data.risks.length > 0) {
|
|
16447
|
+
sections.push(`
|
|
16448
|
+
## Risks`);
|
|
16449
|
+
for (const r of data.risks) {
|
|
16450
|
+
sections.push(`- ${r.id} (${r.type}): ${r.title}`);
|
|
16451
|
+
}
|
|
16452
|
+
}
|
|
16443
16453
|
if (data.velocity) {
|
|
16444
16454
|
sections.push(`
|
|
16445
16455
|
## Velocity`);
|
|
@@ -20078,7 +20088,7 @@ ${fragment}`);
|
|
|
20078
20088
|
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
20079
20089
|
|
|
20080
20090
|
// src/skills/action-runner.ts
|
|
20081
|
-
import { query as
|
|
20091
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
20082
20092
|
|
|
20083
20093
|
// src/agent/mcp-server.ts
|
|
20084
20094
|
import {
|
|
@@ -20567,6 +20577,7 @@ a:hover { text-decoration: underline; }
|
|
|
20567
20577
|
}
|
|
20568
20578
|
|
|
20569
20579
|
.card a { color: inherit; text-decoration: none; display: block; }
|
|
20580
|
+
a.card-link { color: inherit; text-decoration: none; cursor: pointer; }
|
|
20570
20581
|
|
|
20571
20582
|
.card .card-label {
|
|
20572
20583
|
font-size: 0.75rem;
|
|
@@ -20982,6 +20993,68 @@ tr:hover td {
|
|
|
20982
20993
|
.priority-medium { color: var(--amber); }
|
|
20983
20994
|
.priority-low { color: var(--green); }
|
|
20984
20995
|
|
|
20996
|
+
/* Blocker / Risk detail cards */
|
|
20997
|
+
.blocker-card {
|
|
20998
|
+
background: var(--bg-card);
|
|
20999
|
+
border: 1px solid var(--border);
|
|
21000
|
+
border-radius: var(--radius);
|
|
21001
|
+
padding: 1.25rem;
|
|
21002
|
+
margin-bottom: 1rem;
|
|
21003
|
+
}
|
|
21004
|
+
.blocker-card-header {
|
|
21005
|
+
display: flex;
|
|
21006
|
+
align-items: center;
|
|
21007
|
+
gap: 0.5rem;
|
|
21008
|
+
font-size: 0.85rem;
|
|
21009
|
+
margin-bottom: 0.25rem;
|
|
21010
|
+
}
|
|
21011
|
+
.blocker-card-title {
|
|
21012
|
+
margin: 0.25rem 0 0.5rem;
|
|
21013
|
+
font-size: 1rem;
|
|
21014
|
+
}
|
|
21015
|
+
.blocker-card-meta {
|
|
21016
|
+
display: flex;
|
|
21017
|
+
flex-wrap: wrap;
|
|
21018
|
+
gap: 1rem;
|
|
21019
|
+
font-size: 0.85rem;
|
|
21020
|
+
color: var(--text-dim);
|
|
21021
|
+
margin-bottom: 0.75rem;
|
|
21022
|
+
}
|
|
21023
|
+
.blocker-card-content {
|
|
21024
|
+
border-top: 1px solid var(--border);
|
|
21025
|
+
padding-top: 0.75rem;
|
|
21026
|
+
font-size: 0.9rem;
|
|
21027
|
+
}
|
|
21028
|
+
.risk-assessment-content {
|
|
21029
|
+
background: var(--bg);
|
|
21030
|
+
border: 1px solid var(--border);
|
|
21031
|
+
border-left: 3px solid var(--amber);
|
|
21032
|
+
border-radius: var(--radius);
|
|
21033
|
+
padding: 1rem 1.25rem;
|
|
21034
|
+
margin-top: 0.75rem;
|
|
21035
|
+
font-size: 0.9rem;
|
|
21036
|
+
}
|
|
21037
|
+
.risk-assess-btn {
|
|
21038
|
+
font-size: 0.8rem;
|
|
21039
|
+
padding: 0.4rem 0.8rem;
|
|
21040
|
+
margin-top: 0.75rem;
|
|
21041
|
+
}
|
|
21042
|
+
.risk-assess-loading {
|
|
21043
|
+
margin-top: 0.75rem;
|
|
21044
|
+
font-size: 0.85rem;
|
|
21045
|
+
}
|
|
21046
|
+
.risk-assess-error {
|
|
21047
|
+
margin-top: 0.5rem;
|
|
21048
|
+
}
|
|
21049
|
+
.risk-assessment-label {
|
|
21050
|
+
font-size: 0.7rem;
|
|
21051
|
+
text-transform: uppercase;
|
|
21052
|
+
letter-spacing: 0.08em;
|
|
21053
|
+
color: var(--amber);
|
|
21054
|
+
font-weight: 600;
|
|
21055
|
+
margin-bottom: 0.5rem;
|
|
21056
|
+
}
|
|
21057
|
+
|
|
20985
21058
|
/* Health */
|
|
20986
21059
|
.health-section-title {
|
|
20987
21060
|
font-size: 1.1rem;
|
|
@@ -22098,6 +22171,134 @@ function personaPickerPage() {
|
|
|
22098
22171
|
</div>`;
|
|
22099
22172
|
}
|
|
22100
22173
|
|
|
22174
|
+
// src/reports/sprint-summary/risk-assessment.ts
|
|
22175
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
22176
|
+
var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
|
|
22177
|
+
|
|
22178
|
+
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.
|
|
22179
|
+
|
|
22180
|
+
Produce a concise markdown assessment with these sections:
|
|
22181
|
+
|
|
22182
|
+
## Status Assessment
|
|
22183
|
+
One-line verdict: is this risk actively being mitigated, stalled, or escalating?
|
|
22184
|
+
|
|
22185
|
+
## Related Activity
|
|
22186
|
+
What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
|
|
22187
|
+
|
|
22188
|
+
## Trajectory
|
|
22189
|
+
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.
|
|
22190
|
+
|
|
22191
|
+
## Recommendation
|
|
22192
|
+
One concrete next step to move this risk toward resolution.
|
|
22193
|
+
|
|
22194
|
+
Rules:
|
|
22195
|
+
- Reference artifact IDs, dates, owners, and statuses from the provided data
|
|
22196
|
+
- Keep the tone professional and direct
|
|
22197
|
+
- Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
|
|
22198
|
+
- Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
|
|
22199
|
+
- Produce the full assessment text directly`;
|
|
22200
|
+
async function generateRiskAssessment(data, riskId, store) {
|
|
22201
|
+
const risk = data.risks.find((r) => r.id === riskId);
|
|
22202
|
+
if (!risk) return "Risk not found in sprint data.";
|
|
22203
|
+
const prompt = buildSingleRiskPrompt(data, risk, store);
|
|
22204
|
+
const result = query2({
|
|
22205
|
+
prompt,
|
|
22206
|
+
options: {
|
|
22207
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
22208
|
+
maxTurns: 1,
|
|
22209
|
+
tools: [],
|
|
22210
|
+
allowedTools: []
|
|
22211
|
+
}
|
|
22212
|
+
});
|
|
22213
|
+
for await (const msg of result) {
|
|
22214
|
+
if (msg.type === "assistant") {
|
|
22215
|
+
const text = msg.message.content.find(
|
|
22216
|
+
(b) => b.type === "text"
|
|
22217
|
+
);
|
|
22218
|
+
if (text) return text.text;
|
|
22219
|
+
}
|
|
22220
|
+
}
|
|
22221
|
+
return "Unable to generate risk assessment.";
|
|
22222
|
+
}
|
|
22223
|
+
function buildSingleRiskPrompt(data, risk, store) {
|
|
22224
|
+
const sections = [];
|
|
22225
|
+
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
22226
|
+
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
22227
|
+
sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
|
|
22228
|
+
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
22229
|
+
sections.push("");
|
|
22230
|
+
const doc = store.get(risk.id);
|
|
22231
|
+
sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
|
|
22232
|
+
sections.push(`Type: ${risk.type}`);
|
|
22233
|
+
if (doc) {
|
|
22234
|
+
sections.push(`Status: ${doc.frontmatter.status}`);
|
|
22235
|
+
if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
|
|
22236
|
+
if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
|
|
22237
|
+
if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
|
|
22238
|
+
if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
|
|
22239
|
+
if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
|
|
22240
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
22241
|
+
if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
|
|
22242
|
+
if (doc.content.trim()) {
|
|
22243
|
+
sections.push(`
|
|
22244
|
+
Description:
|
|
22245
|
+
${doc.content.trim()}`);
|
|
22246
|
+
}
|
|
22247
|
+
const allDocs = store.list();
|
|
22248
|
+
const relatedIds = /* @__PURE__ */ new Set();
|
|
22249
|
+
for (const d of allDocs) {
|
|
22250
|
+
if (d.frontmatter.aboutArtifact === risk.id) {
|
|
22251
|
+
relatedIds.add(d.frontmatter.id);
|
|
22252
|
+
}
|
|
22253
|
+
}
|
|
22254
|
+
const idPattern = /\b([A-Z]-\d{3,})\b/g;
|
|
22255
|
+
let match;
|
|
22256
|
+
while ((match = idPattern.exec(doc.content)) !== null) {
|
|
22257
|
+
relatedIds.add(match[1]);
|
|
22258
|
+
}
|
|
22259
|
+
const significantTags = tags.filter(
|
|
22260
|
+
(t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
|
|
22261
|
+
);
|
|
22262
|
+
if (significantTags.length > 0) {
|
|
22263
|
+
for (const d of allDocs) {
|
|
22264
|
+
if (d.frontmatter.id === risk.id) continue;
|
|
22265
|
+
const dTags = d.frontmatter.tags ?? [];
|
|
22266
|
+
if (significantTags.some((t) => dTags.includes(t))) {
|
|
22267
|
+
relatedIds.add(d.frontmatter.id);
|
|
22268
|
+
}
|
|
22269
|
+
}
|
|
22270
|
+
}
|
|
22271
|
+
const about = doc.frontmatter.aboutArtifact;
|
|
22272
|
+
if (about) {
|
|
22273
|
+
relatedIds.add(about);
|
|
22274
|
+
for (const d of allDocs) {
|
|
22275
|
+
if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
|
|
22276
|
+
relatedIds.add(d.frontmatter.id);
|
|
22277
|
+
}
|
|
22278
|
+
}
|
|
22279
|
+
}
|
|
22280
|
+
const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
|
|
22281
|
+
if (relatedDocs.length > 0) {
|
|
22282
|
+
sections.push(`
|
|
22283
|
+
## Related Documents (${relatedDocs.length})`);
|
|
22284
|
+
for (const rd of relatedDocs) {
|
|
22285
|
+
const owner = rd.frontmatter.owner ?? "unowned";
|
|
22286
|
+
const summary = rd.content.trim().slice(0, 300);
|
|
22287
|
+
sections.push(
|
|
22288
|
+
`- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
|
|
22289
|
+
);
|
|
22290
|
+
sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
|
|
22291
|
+
if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
|
|
22292
|
+
}
|
|
22293
|
+
}
|
|
22294
|
+
}
|
|
22295
|
+
sections.push("");
|
|
22296
|
+
sections.push(`---`);
|
|
22297
|
+
sections.push(`
|
|
22298
|
+
Generate the risk assessment for ${risk.id} based on the data above.`);
|
|
22299
|
+
return sections.join("\n");
|
|
22300
|
+
}
|
|
22301
|
+
|
|
22101
22302
|
// src/personas/builtin/product-owner.ts
|
|
22102
22303
|
var productOwner = {
|
|
22103
22304
|
id: "product-owner",
|
|
@@ -23495,11 +23696,16 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23495
23696
|
<div class="card-value">${data.linkedEpics.length}</div>
|
|
23496
23697
|
<div class="card-sub">linked to sprint</div>
|
|
23497
23698
|
</div>
|
|
23498
|
-
<
|
|
23699
|
+
<a class="card card-link" href="sprint-blockers">
|
|
23499
23700
|
<div class="card-label">Blockers</div>
|
|
23500
23701
|
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
23501
|
-
<div class="card-sub">${data.
|
|
23502
|
-
</
|
|
23702
|
+
<div class="card-sub">${data.workItems.blocked} blocked items</div>
|
|
23703
|
+
</a>
|
|
23704
|
+
<a class="card card-link" href="sprint-risks">
|
|
23705
|
+
<div class="card-label">Risks</div>
|
|
23706
|
+
<div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
|
|
23707
|
+
<div class="card-sub">open risk items</div>
|
|
23708
|
+
</a>
|
|
23503
23709
|
</div>`;
|
|
23504
23710
|
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
23505
23711
|
"ss-epics",
|
|
@@ -24889,6 +25095,168 @@ function upcomingPage(data) {
|
|
|
24889
25095
|
`;
|
|
24890
25096
|
}
|
|
24891
25097
|
|
|
25098
|
+
// src/web/templates/pages/sprint-blockers.ts
|
|
25099
|
+
function sprintBlockersPage(data, store) {
|
|
25100
|
+
if (!data) {
|
|
25101
|
+
return `
|
|
25102
|
+
<div class="page-header">
|
|
25103
|
+
<h2>Sprint Blockers</h2>
|
|
25104
|
+
<div class="subtitle">Blocked items in the active sprint</div>
|
|
25105
|
+
</div>
|
|
25106
|
+
<div class="empty">
|
|
25107
|
+
<h3>No Active Sprint</h3>
|
|
25108
|
+
<p>No active sprint found.</p>
|
|
25109
|
+
</div>`;
|
|
25110
|
+
}
|
|
25111
|
+
const blockerDocs = data.blockers.map((b) => {
|
|
25112
|
+
const doc = store.get(b.id);
|
|
25113
|
+
return { ...b, doc };
|
|
25114
|
+
});
|
|
25115
|
+
const statsCards = `
|
|
25116
|
+
<div class="cards">
|
|
25117
|
+
<div class="card">
|
|
25118
|
+
<div class="card-label">Blocked Items</div>
|
|
25119
|
+
<div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
|
|
25120
|
+
<div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
|
|
25121
|
+
</div>
|
|
25122
|
+
</div>`;
|
|
25123
|
+
const itemCards = blockerDocs.map((b) => {
|
|
25124
|
+
const doc = b.doc;
|
|
25125
|
+
const owner = doc?.frontmatter.owner;
|
|
25126
|
+
const assignee = doc?.frontmatter.assignee;
|
|
25127
|
+
const content = doc?.content?.trim();
|
|
25128
|
+
return `
|
|
25129
|
+
<div class="blocker-card">
|
|
25130
|
+
<div class="blocker-card-header">
|
|
25131
|
+
<a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
|
|
25132
|
+
<span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
|
|
25133
|
+
${statusBadge("blocked")}
|
|
25134
|
+
</div>
|
|
25135
|
+
<h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
|
|
25136
|
+
<div class="blocker-card-meta">
|
|
25137
|
+
${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
|
|
25138
|
+
${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
|
|
25139
|
+
${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
|
|
25140
|
+
</div>
|
|
25141
|
+
${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
|
|
25142
|
+
</div>`;
|
|
25143
|
+
}).join("");
|
|
25144
|
+
const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
|
|
25145
|
+
return `
|
|
25146
|
+
<div class="page-header">
|
|
25147
|
+
<h2>Sprint Blockers</h2>
|
|
25148
|
+
<div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
|
|
25149
|
+
</div>
|
|
25150
|
+
${statsCards}
|
|
25151
|
+
${emptyMessage}
|
|
25152
|
+
${itemCards}`;
|
|
25153
|
+
}
|
|
25154
|
+
|
|
25155
|
+
// src/web/templates/pages/sprint-risks.ts
|
|
25156
|
+
function sprintRisksPage(data, store) {
|
|
25157
|
+
if (!data) {
|
|
25158
|
+
return `
|
|
25159
|
+
<div class="page-header">
|
|
25160
|
+
<h2>Sprint Risks</h2>
|
|
25161
|
+
<div class="subtitle">Risk items in the active sprint</div>
|
|
25162
|
+
</div>
|
|
25163
|
+
<div class="empty">
|
|
25164
|
+
<h3>No Active Sprint</h3>
|
|
25165
|
+
<p>No active sprint found.</p>
|
|
25166
|
+
</div>`;
|
|
25167
|
+
}
|
|
25168
|
+
const riskDocs = data.risks.map((r) => {
|
|
25169
|
+
const doc = store.get(r.id);
|
|
25170
|
+
return { ...r, doc };
|
|
25171
|
+
});
|
|
25172
|
+
const statsCards = `
|
|
25173
|
+
<div class="cards">
|
|
25174
|
+
<div class="card">
|
|
25175
|
+
<div class="card-label">Open Risks</div>
|
|
25176
|
+
<div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
|
|
25177
|
+
<div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
|
|
25178
|
+
</div>
|
|
25179
|
+
</div>`;
|
|
25180
|
+
const itemCards = riskDocs.map((r) => {
|
|
25181
|
+
const doc = r.doc;
|
|
25182
|
+
const owner = doc?.frontmatter.owner;
|
|
25183
|
+
const assignee = doc?.frontmatter.assignee;
|
|
25184
|
+
const content = doc?.content?.trim();
|
|
25185
|
+
return `
|
|
25186
|
+
<div class="blocker-card" id="risk-${escapeHtml(r.id)}">
|
|
25187
|
+
<div class="blocker-card-header">
|
|
25188
|
+
<a href="/docs/${escapeHtml(r.type)}/${escapeHtml(r.id)}">${escapeHtml(r.id)}</a>
|
|
25189
|
+
<span class="text-dim">${escapeHtml(typeLabel(r.type))}</span>
|
|
25190
|
+
${statusBadge(doc?.frontmatter.status ?? "open")}
|
|
25191
|
+
</div>
|
|
25192
|
+
<h4 class="blocker-card-title">${escapeHtml(r.title)}</h4>
|
|
25193
|
+
<div class="blocker-card-meta">
|
|
25194
|
+
${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
|
|
25195
|
+
${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
|
|
25196
|
+
${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
|
|
25197
|
+
</div>
|
|
25198
|
+
${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
|
|
25199
|
+
<div class="risk-assessment" id="assessment-${escapeHtml(r.id)}">
|
|
25200
|
+
<button class="sprint-generate-btn risk-assess-btn" onclick="generateAssessment('${escapeHtml(r.id)}', this)">Assess Risk</button>
|
|
25201
|
+
<div class="sprint-loading risk-assess-loading" style="display:none">
|
|
25202
|
+
<div class="sprint-spinner"></div>
|
|
25203
|
+
<span>Analyzing...</span>
|
|
25204
|
+
</div>
|
|
25205
|
+
<div class="sprint-error risk-assess-error" style="display:none"></div>
|
|
25206
|
+
<div class="risk-assessment-content detail-content" style="display:none"></div>
|
|
25207
|
+
</div>
|
|
25208
|
+
</div>`;
|
|
25209
|
+
}).join("");
|
|
25210
|
+
const emptyMessage = riskDocs.length === 0 ? `<div class="empty"><h3>No Risks</h3><p>No open risk items in this sprint.</p></div>` : "";
|
|
25211
|
+
const script = riskDocs.length > 0 ? `
|
|
25212
|
+
<script>
|
|
25213
|
+
async function generateAssessment(riskId, btn) {
|
|
25214
|
+
var container = document.getElementById('assessment-' + riskId);
|
|
25215
|
+
var loading = container.querySelector('.risk-assess-loading');
|
|
25216
|
+
var errorEl = container.querySelector('.risk-assess-error');
|
|
25217
|
+
var contentEl = container.querySelector('.risk-assessment-content');
|
|
25218
|
+
|
|
25219
|
+
btn.disabled = true;
|
|
25220
|
+
btn.style.display = 'none';
|
|
25221
|
+
loading.style.display = 'flex';
|
|
25222
|
+
errorEl.style.display = 'none';
|
|
25223
|
+
|
|
25224
|
+
try {
|
|
25225
|
+
var res = await fetch('/api/risk-assessment', {
|
|
25226
|
+
method: 'POST',
|
|
25227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25228
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}', riskId: riskId })
|
|
25229
|
+
});
|
|
25230
|
+
var json = await res.json();
|
|
25231
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate assessment');
|
|
25232
|
+
|
|
25233
|
+
contentEl.innerHTML = '<div class="risk-assessment-label">AI Assessment</div>' + json.html;
|
|
25234
|
+
contentEl.style.display = 'block';
|
|
25235
|
+
|
|
25236
|
+
loading.style.display = 'none';
|
|
25237
|
+
btn.textContent = 'Regenerate';
|
|
25238
|
+
btn.style.display = '';
|
|
25239
|
+
btn.disabled = false;
|
|
25240
|
+
} catch (e) {
|
|
25241
|
+
loading.style.display = 'none';
|
|
25242
|
+
errorEl.textContent = e.message;
|
|
25243
|
+
errorEl.style.display = 'block';
|
|
25244
|
+
btn.style.display = '';
|
|
25245
|
+
btn.disabled = false;
|
|
25246
|
+
}
|
|
25247
|
+
}
|
|
25248
|
+
</script>` : "";
|
|
25249
|
+
return `
|
|
25250
|
+
<div class="page-header">
|
|
25251
|
+
<h2>Sprint Risks</h2>
|
|
25252
|
+
<div class="subtitle">Risk items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
|
|
25253
|
+
</div>
|
|
25254
|
+
${statsCards}
|
|
25255
|
+
${emptyMessage}
|
|
25256
|
+
${itemCards}
|
|
25257
|
+
${script}`;
|
|
25258
|
+
}
|
|
25259
|
+
|
|
24892
25260
|
// src/web/templates/pages/shared-wrappers.ts
|
|
24893
25261
|
function sharedTimelinePage(ctx) {
|
|
24894
25262
|
const diagrams = getDiagramData(ctx.store);
|
|
@@ -24918,6 +25286,16 @@ function sharedSprintSummaryPage(ctx) {
|
|
|
24918
25286
|
const data = getSprintSummaryData(ctx.store, sprintId);
|
|
24919
25287
|
return sprintSummaryPage(data);
|
|
24920
25288
|
}
|
|
25289
|
+
function sharedSprintBlockersPage(ctx) {
|
|
25290
|
+
const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
|
|
25291
|
+
const data = getSprintSummaryData(ctx.store, sprintId);
|
|
25292
|
+
return sprintBlockersPage(data, ctx.store);
|
|
25293
|
+
}
|
|
25294
|
+
function sharedSprintRisksPage(ctx) {
|
|
25295
|
+
const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
|
|
25296
|
+
const data = getSprintSummaryData(ctx.store, sprintId);
|
|
25297
|
+
return sprintRisksPage(data, ctx.store);
|
|
25298
|
+
}
|
|
24921
25299
|
|
|
24922
25300
|
// src/web/shared-page-registration.ts
|
|
24923
25301
|
var SHARED_PAGES = [
|
|
@@ -24926,7 +25304,9 @@ var SHARED_PAGES = [
|
|
|
24926
25304
|
{ pageId: "upcoming", renderer: sharedUpcomingPage },
|
|
24927
25305
|
{ pageId: "gar", renderer: sharedGarPage },
|
|
24928
25306
|
{ pageId: "health", renderer: sharedHealthPage },
|
|
24929
|
-
{ pageId: "sprint-summary", renderer: sharedSprintSummaryPage }
|
|
25307
|
+
{ pageId: "sprint-summary", renderer: sharedSprintSummaryPage },
|
|
25308
|
+
{ pageId: "sprint-blockers", renderer: sharedSprintBlockersPage },
|
|
25309
|
+
{ pageId: "sprint-risks", renderer: sharedSprintRisksPage }
|
|
24930
25310
|
];
|
|
24931
25311
|
for (const persona of ["po", "dm", "tl"]) {
|
|
24932
25312
|
for (const { pageId, renderer } of SHARED_PAGES) {
|
|
@@ -25127,6 +25507,37 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
25127
25507
|
});
|
|
25128
25508
|
return;
|
|
25129
25509
|
}
|
|
25510
|
+
if (pathname === "/api/risk-assessment" && req.method === "POST") {
|
|
25511
|
+
let bodyStr = "";
|
|
25512
|
+
req.on("data", (chunk) => {
|
|
25513
|
+
bodyStr += chunk;
|
|
25514
|
+
});
|
|
25515
|
+
req.on("end", async () => {
|
|
25516
|
+
try {
|
|
25517
|
+
const { sprintId, riskId } = JSON.parse(bodyStr || "{}");
|
|
25518
|
+
if (!riskId) {
|
|
25519
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
25520
|
+
res.end(JSON.stringify({ error: "riskId is required" }));
|
|
25521
|
+
return;
|
|
25522
|
+
}
|
|
25523
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
25524
|
+
if (!data) {
|
|
25525
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
25526
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
25527
|
+
return;
|
|
25528
|
+
}
|
|
25529
|
+
const markdown = await generateRiskAssessment(data, riskId, store);
|
|
25530
|
+
const html = renderMarkdown(markdown);
|
|
25531
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
25532
|
+
res.end(JSON.stringify({ riskId, html }));
|
|
25533
|
+
} catch (err) {
|
|
25534
|
+
console.error("[marvin web] Risk assessment generation error:", err);
|
|
25535
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
25536
|
+
res.end(JSON.stringify({ error: "Failed to generate risk assessment" }));
|
|
25537
|
+
}
|
|
25538
|
+
});
|
|
25539
|
+
return;
|
|
25540
|
+
}
|
|
25130
25541
|
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
25131
25542
|
if (detailMatch) {
|
|
25132
25543
|
const [, type, id] = detailMatch;
|
|
@@ -25978,7 +26389,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
25978
26389
|
try {
|
|
25979
26390
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
25980
26391
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES : [];
|
|
25981
|
-
const conversation =
|
|
26392
|
+
const conversation = query3({
|
|
25982
26393
|
prompt: userPrompt,
|
|
25983
26394
|
options: {
|
|
25984
26395
|
systemPrompt: action.systemPrompt,
|