mrvn-cli 0.5.1 → 0.5.2
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.d.ts +1 -0
- package/dist/index.js +853 -181
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +783 -178
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +853 -181
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6630,13 +6630,13 @@ var error16 = () => {
|
|
|
6630
6630
|
// no unit
|
|
6631
6631
|
};
|
|
6632
6632
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6633
|
-
const
|
|
6633
|
+
const typeLabel5 = (t) => {
|
|
6634
6634
|
const e = typeEntry(t);
|
|
6635
6635
|
if (e)
|
|
6636
6636
|
return e.label;
|
|
6637
6637
|
return t ?? TypeNames.unknown.label;
|
|
6638
6638
|
};
|
|
6639
|
-
const withDefinite = (t) => `\u05D4${
|
|
6639
|
+
const withDefinite = (t) => `\u05D4${typeLabel5(t)}`;
|
|
6640
6640
|
const verbFor = (t) => {
|
|
6641
6641
|
const e = typeEntry(t);
|
|
6642
6642
|
const gender = e?.gender ?? "m";
|
|
@@ -6686,7 +6686,7 @@ var error16 = () => {
|
|
|
6686
6686
|
switch (issue2.code) {
|
|
6687
6687
|
case "invalid_type": {
|
|
6688
6688
|
const expectedKey = issue2.expected;
|
|
6689
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6689
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel5(expectedKey);
|
|
6690
6690
|
const receivedType = parsedType(issue2.input);
|
|
6691
6691
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6692
6692
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -14365,6 +14365,26 @@ config(en_default());
|
|
|
14365
14365
|
|
|
14366
14366
|
// src/agent/tools/decisions.ts
|
|
14367
14367
|
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
14368
|
+
|
|
14369
|
+
// src/personas/owner.ts
|
|
14370
|
+
var OWNER_SHORT = ["po", "dm", "tl"];
|
|
14371
|
+
var OWNER_LONG = ["product-owner", "delivery-manager", "tech-lead"];
|
|
14372
|
+
var VALID_OWNERS = [...OWNER_SHORT, ...OWNER_LONG];
|
|
14373
|
+
var LONG_TO_SHORT = {
|
|
14374
|
+
"product-owner": "po",
|
|
14375
|
+
"delivery-manager": "dm",
|
|
14376
|
+
"tech-lead": "tl"
|
|
14377
|
+
};
|
|
14378
|
+
var ownerSchema = external_exports.enum(VALID_OWNERS);
|
|
14379
|
+
function normalizeOwner(owner) {
|
|
14380
|
+
if (owner === void 0) return void 0;
|
|
14381
|
+
return LONG_TO_SHORT[owner] ?? owner;
|
|
14382
|
+
}
|
|
14383
|
+
function isValidOwner(value) {
|
|
14384
|
+
return VALID_OWNERS.includes(value);
|
|
14385
|
+
}
|
|
14386
|
+
|
|
14387
|
+
// src/agent/tools/decisions.ts
|
|
14368
14388
|
function createDecisionTools(store) {
|
|
14369
14389
|
return [
|
|
14370
14390
|
tool(
|
|
@@ -14419,7 +14439,8 @@ function createDecisionTools(store) {
|
|
|
14419
14439
|
title: external_exports.string().describe("Title of the decision"),
|
|
14420
14440
|
content: external_exports.string().describe("Decision description, context, and rationale"),
|
|
14421
14441
|
status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("Status (default: 'open')"),
|
|
14422
|
-
owner:
|
|
14442
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14443
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14423
14444
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14424
14445
|
},
|
|
14425
14446
|
async (args) => {
|
|
@@ -14428,7 +14449,8 @@ function createDecisionTools(store) {
|
|
|
14428
14449
|
{
|
|
14429
14450
|
title: args.title,
|
|
14430
14451
|
status: args.status,
|
|
14431
|
-
owner: args.owner,
|
|
14452
|
+
owner: normalizeOwner(args.owner),
|
|
14453
|
+
assignee: args.assignee,
|
|
14432
14454
|
tags: args.tags
|
|
14433
14455
|
},
|
|
14434
14456
|
args.content
|
|
@@ -14451,11 +14473,14 @@ function createDecisionTools(store) {
|
|
|
14451
14473
|
title: external_exports.string().optional().describe("New title"),
|
|
14452
14474
|
status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("New status"),
|
|
14453
14475
|
content: external_exports.string().optional().describe("New content"),
|
|
14454
|
-
owner:
|
|
14476
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14477
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14455
14478
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14456
14479
|
},
|
|
14457
14480
|
async (args) => {
|
|
14458
|
-
const { id, content, ...updates } = args;
|
|
14481
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
14482
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14483
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14459
14484
|
const doc = store.update(id, updates, content);
|
|
14460
14485
|
return {
|
|
14461
14486
|
content: [
|
|
@@ -14638,12 +14663,13 @@ function createActionTools(store) {
|
|
|
14638
14663
|
title: external_exports.string().describe("Title of the action item"),
|
|
14639
14664
|
content: external_exports.string().describe("Description of what needs to be done"),
|
|
14640
14665
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14641
|
-
owner:
|
|
14666
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14667
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14642
14668
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
14643
14669
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
14644
14670
|
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14645
14671
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags."),
|
|
14646
|
-
|
|
14672
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
|
|
14647
14673
|
},
|
|
14648
14674
|
async (args) => {
|
|
14649
14675
|
const tags = [...args.tags ?? []];
|
|
@@ -14653,15 +14679,16 @@ function createActionTools(store) {
|
|
|
14653
14679
|
if (!tags.includes(tag)) tags.push(tag);
|
|
14654
14680
|
}
|
|
14655
14681
|
}
|
|
14656
|
-
if (args.
|
|
14657
|
-
tags.push(`
|
|
14682
|
+
if (args.workFocus) {
|
|
14683
|
+
tags.push(`focus:${args.workFocus}`);
|
|
14658
14684
|
}
|
|
14659
14685
|
const doc = store.create(
|
|
14660
14686
|
"action",
|
|
14661
14687
|
{
|
|
14662
14688
|
title: args.title,
|
|
14663
14689
|
status: args.status,
|
|
14664
|
-
owner: args.owner,
|
|
14690
|
+
owner: normalizeOwner(args.owner),
|
|
14691
|
+
assignee: args.assignee,
|
|
14665
14692
|
priority: args.priority,
|
|
14666
14693
|
tags: tags.length > 0 ? tags : void 0,
|
|
14667
14694
|
dueDate: args.dueDate
|
|
@@ -14689,16 +14716,19 @@ function createActionTools(store) {
|
|
|
14689
14716
|
title: external_exports.string().optional().describe("New title"),
|
|
14690
14717
|
status: external_exports.string().optional().describe("New status"),
|
|
14691
14718
|
content: external_exports.string().optional().describe("New content"),
|
|
14692
|
-
owner:
|
|
14719
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14720
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14693
14721
|
priority: external_exports.string().optional().describe("New priority"),
|
|
14694
14722
|
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14695
14723
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
14696
14724
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
|
|
14697
|
-
|
|
14725
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
14698
14726
|
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100).")
|
|
14699
14727
|
},
|
|
14700
14728
|
async (args) => {
|
|
14701
|
-
const { id, content, sprints, tags,
|
|
14729
|
+
const { id, content, sprints, tags, workFocus, progress, owner, assignee, ...updates } = args;
|
|
14730
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14731
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14702
14732
|
if (tags !== void 0) {
|
|
14703
14733
|
const merged = [...tags];
|
|
14704
14734
|
if (sprints) {
|
|
@@ -14707,14 +14737,14 @@ function createActionTools(store) {
|
|
|
14707
14737
|
if (!merged.includes(tag)) merged.push(tag);
|
|
14708
14738
|
}
|
|
14709
14739
|
}
|
|
14710
|
-
if (
|
|
14711
|
-
const filtered = merged.filter((t) => !t.startsWith("
|
|
14712
|
-
filtered.push(`
|
|
14740
|
+
if (workFocus !== void 0) {
|
|
14741
|
+
const filtered = merged.filter((t) => !t.startsWith("focus:"));
|
|
14742
|
+
filtered.push(`focus:${workFocus}`);
|
|
14713
14743
|
updates.tags = filtered;
|
|
14714
14744
|
} else {
|
|
14715
14745
|
updates.tags = merged;
|
|
14716
14746
|
}
|
|
14717
|
-
} else if (sprints !== void 0 ||
|
|
14747
|
+
} else if (sprints !== void 0 || workFocus !== void 0) {
|
|
14718
14748
|
const existing = store.get(id);
|
|
14719
14749
|
if (!existing) {
|
|
14720
14750
|
return {
|
|
@@ -14727,9 +14757,9 @@ function createActionTools(store) {
|
|
|
14727
14757
|
existingTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
14728
14758
|
existingTags.push(...sprints.map((s) => `sprint:${s}`));
|
|
14729
14759
|
}
|
|
14730
|
-
if (
|
|
14731
|
-
existingTags = existingTags.filter((t) => !t.startsWith("
|
|
14732
|
-
existingTags.push(`
|
|
14760
|
+
if (workFocus !== void 0) {
|
|
14761
|
+
existingTags = existingTags.filter((t) => !t.startsWith("focus:"));
|
|
14762
|
+
existingTags.push(`focus:${workFocus}`);
|
|
14733
14763
|
}
|
|
14734
14764
|
updates.tags = existingTags;
|
|
14735
14765
|
}
|
|
@@ -14842,7 +14872,8 @@ function createQuestionTools(store) {
|
|
|
14842
14872
|
title: external_exports.string().describe("The question being asked"),
|
|
14843
14873
|
content: external_exports.string().describe("Context and details about the question"),
|
|
14844
14874
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14845
|
-
owner:
|
|
14875
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14876
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14846
14877
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14847
14878
|
},
|
|
14848
14879
|
async (args) => {
|
|
@@ -14851,7 +14882,8 @@ function createQuestionTools(store) {
|
|
|
14851
14882
|
{
|
|
14852
14883
|
title: args.title,
|
|
14853
14884
|
status: args.status,
|
|
14854
|
-
owner: args.owner,
|
|
14885
|
+
owner: normalizeOwner(args.owner),
|
|
14886
|
+
assignee: args.assignee,
|
|
14855
14887
|
tags: args.tags
|
|
14856
14888
|
},
|
|
14857
14889
|
args.content
|
|
@@ -14874,11 +14906,14 @@ function createQuestionTools(store) {
|
|
|
14874
14906
|
title: external_exports.string().optional().describe("New title"),
|
|
14875
14907
|
status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
|
|
14876
14908
|
content: external_exports.string().optional().describe("Updated content / answer"),
|
|
14877
|
-
owner:
|
|
14909
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14910
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14878
14911
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14879
14912
|
},
|
|
14880
14913
|
async (args) => {
|
|
14881
|
-
const { id, content, ...updates } = args;
|
|
14914
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
14915
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14916
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14882
14917
|
const doc = store.update(id, updates, content);
|
|
14883
14918
|
return {
|
|
14884
14919
|
content: [
|
|
@@ -14903,18 +14938,20 @@ function createDocumentTools(store) {
|
|
|
14903
14938
|
{
|
|
14904
14939
|
type: external_exports.string().optional().describe(`Filter by document type (registered types: ${store.registeredTypes.join(", ")})`),
|
|
14905
14940
|
status: external_exports.string().optional().describe("Filter by status"),
|
|
14941
|
+
owner: external_exports.string().optional().describe("Filter by persona role owner (po, dm, tl)"),
|
|
14906
14942
|
tag: external_exports.string().optional().describe("Filter by tag"),
|
|
14907
|
-
|
|
14943
|
+
workFocus: external_exports.string().optional().describe("Filter by work focus name (matches focus:<value> tag)")
|
|
14908
14944
|
},
|
|
14909
14945
|
async (args) => {
|
|
14910
14946
|
let docs = store.list({
|
|
14911
14947
|
type: args.type,
|
|
14912
14948
|
status: args.status,
|
|
14949
|
+
owner: args.owner,
|
|
14913
14950
|
tag: args.tag
|
|
14914
14951
|
});
|
|
14915
|
-
if (args.
|
|
14916
|
-
const
|
|
14917
|
-
docs = docs.filter((d) => d.frontmatter.tags?.includes(
|
|
14952
|
+
if (args.workFocus) {
|
|
14953
|
+
const focusTag = `focus:${args.workFocus}`;
|
|
14954
|
+
docs = docs.filter((d) => d.frontmatter.tags?.includes(focusTag));
|
|
14918
14955
|
}
|
|
14919
14956
|
const summary = docs.map((d) => ({
|
|
14920
14957
|
id: d.frontmatter.id,
|
|
@@ -15722,14 +15759,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15722
15759
|
const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
|
|
15723
15760
|
for (const doc of workItemDocs) {
|
|
15724
15761
|
const about = doc.frontmatter.aboutArtifact;
|
|
15725
|
-
const
|
|
15762
|
+
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
15726
15763
|
const item = {
|
|
15727
15764
|
id: doc.frontmatter.id,
|
|
15728
15765
|
title: doc.frontmatter.title,
|
|
15729
15766
|
type: doc.frontmatter.type,
|
|
15730
15767
|
status: doc.frontmatter.status,
|
|
15731
15768
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15732
|
-
|
|
15769
|
+
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15733
15770
|
aboutArtifact: about
|
|
15734
15771
|
};
|
|
15735
15772
|
allItemsById.set(item.id, item);
|
|
@@ -16240,7 +16277,7 @@ function formatDate(iso) {
|
|
|
16240
16277
|
if (!iso) return "";
|
|
16241
16278
|
return iso.slice(0, 10);
|
|
16242
16279
|
}
|
|
16243
|
-
function
|
|
16280
|
+
function typeLabel2(type) {
|
|
16244
16281
|
return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
16245
16282
|
}
|
|
16246
16283
|
function renderMarkdown(md) {
|
|
@@ -17995,12 +18032,45 @@ tr:hover td {
|
|
|
17995
18032
|
font-weight: 700;
|
|
17996
18033
|
color: var(--text);
|
|
17997
18034
|
}
|
|
18035
|
+
|
|
18036
|
+
/* Focus-grouped work items */
|
|
18037
|
+
.focus-row td:first-child {
|
|
18038
|
+
border-left: 3px solid var(--focus-color, var(--border));
|
|
18039
|
+
}
|
|
18040
|
+
|
|
18041
|
+
.focus-group-header td {
|
|
18042
|
+
background: var(--bg-hover);
|
|
18043
|
+
border-left: 3px solid var(--focus-color, var(--border));
|
|
18044
|
+
padding-top: 0.5rem;
|
|
18045
|
+
padding-bottom: 0.5rem;
|
|
18046
|
+
border-bottom: 1px solid var(--border);
|
|
18047
|
+
}
|
|
18048
|
+
|
|
18049
|
+
.focus-group-header td:first-child {
|
|
18050
|
+
border-left-width: 3px;
|
|
18051
|
+
}
|
|
18052
|
+
|
|
18053
|
+
.focus-group-name {
|
|
18054
|
+
font-weight: 600;
|
|
18055
|
+
font-size: 0.8rem;
|
|
18056
|
+
color: var(--text);
|
|
18057
|
+
margin-right: 0.75rem;
|
|
18058
|
+
}
|
|
18059
|
+
|
|
18060
|
+
.focus-group-stats {
|
|
18061
|
+
font-size: 0.75rem;
|
|
18062
|
+
color: var(--text-dim);
|
|
18063
|
+
}
|
|
18064
|
+
|
|
18065
|
+
.focus-group-progress {
|
|
18066
|
+
width: 96px;
|
|
18067
|
+
}
|
|
17998
18068
|
`;
|
|
17999
18069
|
}
|
|
18000
18070
|
|
|
18001
18071
|
// src/web/templates/pages/documents.ts
|
|
18002
18072
|
function documentsPage(data) {
|
|
18003
|
-
const label =
|
|
18073
|
+
const label = typeLabel2(data.type);
|
|
18004
18074
|
const statusOptions = data.statuses.map(
|
|
18005
18075
|
(s) => `<option value="${escapeHtml(s)}"${data.filterStatus === s ? " selected" : ""}>${escapeHtml(s)}</option>`
|
|
18006
18076
|
).join("");
|
|
@@ -18074,7 +18144,7 @@ function documentsPage(data) {
|
|
|
18074
18144
|
// src/web/templates/pages/document-detail.ts
|
|
18075
18145
|
function documentDetailPage(doc) {
|
|
18076
18146
|
const fm = doc.frontmatter;
|
|
18077
|
-
const label =
|
|
18147
|
+
const label = typeLabel2(fm.type);
|
|
18078
18148
|
const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
|
|
18079
18149
|
const entries = Object.entries(fm).filter(
|
|
18080
18150
|
([key]) => !skipKeys.has(key) && fm[key] != null
|
|
@@ -18757,7 +18827,7 @@ function poDashboardPage(ctx) {
|
|
|
18757
18827
|
<tr>
|
|
18758
18828
|
<td><a href="/docs/${d.frontmatter.type}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
18759
18829
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
18760
|
-
<td>${escapeHtml(
|
|
18830
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
18761
18831
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
18762
18832
|
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
18763
18833
|
</tr>`).join("")}
|
|
@@ -19265,7 +19335,7 @@ function poDeliveryPage(ctx) {
|
|
|
19265
19335
|
<tr>
|
|
19266
19336
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19267
19337
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19268
|
-
<td>${escapeHtml(
|
|
19338
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
19269
19339
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19270
19340
|
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
19271
19341
|
</tr>`).join("")}
|
|
@@ -19583,15 +19653,15 @@ function sprintSummaryPage(data, cached2) {
|
|
|
19583
19653
|
</div>`,
|
|
19584
19654
|
{ titleTag: "h3" }
|
|
19585
19655
|
) : "";
|
|
19586
|
-
const
|
|
19587
|
-
"
|
|
19588
|
-
"
|
|
19589
|
-
"
|
|
19590
|
-
"
|
|
19591
|
-
"
|
|
19592
|
-
"
|
|
19593
|
-
"
|
|
19594
|
-
"
|
|
19656
|
+
const FOCUS_BORDER_PALETTE = [
|
|
19657
|
+
"hsl(220, 60%, 55%)",
|
|
19658
|
+
"hsl(160, 50%, 45%)",
|
|
19659
|
+
"hsl(280, 45%, 55%)",
|
|
19660
|
+
"hsl(30, 65%, 55%)",
|
|
19661
|
+
"hsl(340, 50%, 55%)",
|
|
19662
|
+
"hsl(190, 50%, 45%)",
|
|
19663
|
+
"hsl(60, 50%, 50%)",
|
|
19664
|
+
"hsl(120, 40%, 45%)"
|
|
19595
19665
|
];
|
|
19596
19666
|
function hashString(s) {
|
|
19597
19667
|
let h = 0;
|
|
@@ -19600,68 +19670,92 @@ function sprintSummaryPage(data, cached2) {
|
|
|
19600
19670
|
}
|
|
19601
19671
|
return Math.abs(h);
|
|
19602
19672
|
}
|
|
19603
|
-
|
|
19604
|
-
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
19608
|
-
|
|
19673
|
+
const focusGroups = /* @__PURE__ */ new Map();
|
|
19674
|
+
for (const item of data.workItems.items) {
|
|
19675
|
+
const focus = item.workFocus ?? "Unassigned";
|
|
19676
|
+
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
19677
|
+
focusGroups.get(focus).push(item);
|
|
19678
|
+
}
|
|
19679
|
+
const focusColorMap = /* @__PURE__ */ new Map();
|
|
19680
|
+
for (const name of focusGroups.keys()) {
|
|
19681
|
+
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
19682
|
+
}
|
|
19683
|
+
function countFocusStats(items) {
|
|
19684
|
+
let total = 0;
|
|
19685
|
+
let done = 0;
|
|
19686
|
+
let inProgress = 0;
|
|
19687
|
+
function walk(list) {
|
|
19688
|
+
for (const w of list) {
|
|
19689
|
+
if (w.type !== "contribution") {
|
|
19690
|
+
total++;
|
|
19691
|
+
const s = w.status.toLowerCase();
|
|
19692
|
+
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
19693
|
+
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
19694
|
+
}
|
|
19695
|
+
if (w.children) walk(w.children);
|
|
19609
19696
|
}
|
|
19610
19697
|
}
|
|
19611
|
-
|
|
19698
|
+
walk(items);
|
|
19699
|
+
return { total, done, inProgress };
|
|
19612
19700
|
}
|
|
19613
|
-
|
|
19614
|
-
const streamColorMap = /* @__PURE__ */ new Map();
|
|
19615
|
-
for (const name of uniqueStreams) {
|
|
19616
|
-
streamColorMap.set(name, STREAM_PALETTE[hashString(name) % STREAM_PALETTE.length]);
|
|
19617
|
-
}
|
|
19618
|
-
const streamStyleRules = [...streamColorMap.entries()].map(([name, color]) => `tr[data-stream="${escapeHtml(name)}"] td { background: ${color}; }`).join("\n");
|
|
19619
|
-
const streamStyleBlock = streamStyleRules ? `<style>${streamStyleRules}</style>` : "";
|
|
19620
|
-
function renderItemRows(items, depth = 0) {
|
|
19701
|
+
function renderItemRows(items, borderColor, depth = 0) {
|
|
19621
19702
|
return items.flatMap((w) => {
|
|
19622
19703
|
const isChild = depth > 0;
|
|
19623
19704
|
const isContribution = w.type === "contribution";
|
|
19624
|
-
const classes = [];
|
|
19705
|
+
const classes = ["focus-row"];
|
|
19625
19706
|
if (isContribution) classes.push("contribution-row");
|
|
19626
19707
|
else if (isChild) classes.push("child-row");
|
|
19627
|
-
const dataStream = w.workStream ? ` data-stream="${escapeHtml(w.workStream)}"` : "";
|
|
19628
|
-
const rowAttrs = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
|
|
19629
19708
|
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
19630
|
-
const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
|
|
19631
19709
|
const progressCell = !isContribution && w.progress !== void 0 ? `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${w.progress}%"></div><span class="mini-progress-label">${w.progress}%</span></div>` : "";
|
|
19632
19710
|
const row = `
|
|
19633
|
-
<tr${
|
|
19711
|
+
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
19634
19712
|
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
19635
19713
|
<td>${escapeHtml(w.title)}</td>
|
|
19636
|
-
<td>${streamCell}</td>
|
|
19637
|
-
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
19638
19714
|
<td>${statusBadge(w.status)}</td>
|
|
19639
19715
|
<td>${progressCell}</td>
|
|
19640
19716
|
</tr>`;
|
|
19641
|
-
const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
|
|
19717
|
+
const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
|
|
19642
19718
|
return [row, ...childRows];
|
|
19643
19719
|
});
|
|
19644
19720
|
}
|
|
19645
|
-
const
|
|
19646
|
-
const
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
|
|
19721
|
+
const allWorkItemRows = [];
|
|
19722
|
+
for (const [focus, items] of focusGroups) {
|
|
19723
|
+
const color = focusColorMap.get(focus);
|
|
19724
|
+
const stats = countFocusStats(items);
|
|
19725
|
+
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
19726
|
+
const summaryParts = [];
|
|
19727
|
+
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
19728
|
+
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
19729
|
+
const remaining = stats.total - stats.done - stats.inProgress;
|
|
19730
|
+
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
19731
|
+
allWorkItemRows.push(`
|
|
19732
|
+
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
19733
|
+
<td colspan="2">
|
|
19734
|
+
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
19735
|
+
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
19736
|
+
</td>
|
|
19737
|
+
<td colspan="2">
|
|
19738
|
+
<div class="mini-progress-bar focus-group-progress"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>
|
|
19739
|
+
</td>
|
|
19740
|
+
</tr>`);
|
|
19741
|
+
allWorkItemRows.push(...renderItemRows(items, color));
|
|
19742
|
+
}
|
|
19743
|
+
const tableHeaders = `<tr>
|
|
19744
|
+
<th>ID</th>
|
|
19745
|
+
<th>Title</th>
|
|
19746
|
+
<th>Status</th>
|
|
19747
|
+
<th>Progress</th>
|
|
19653
19748
|
</tr>`;
|
|
19654
|
-
const workItemsSection =
|
|
19749
|
+
const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
|
|
19655
19750
|
"ss-work-items",
|
|
19656
19751
|
"Work Items",
|
|
19657
|
-
|
|
19658
|
-
<div class="table-wrap">
|
|
19752
|
+
`<div class="table-wrap">
|
|
19659
19753
|
<table id="work-items-table">
|
|
19660
19754
|
<thead>
|
|
19661
|
-
${
|
|
19755
|
+
${tableHeaders}
|
|
19662
19756
|
</thead>
|
|
19663
19757
|
<tbody>
|
|
19664
|
-
${
|
|
19758
|
+
${allWorkItemRows.join("")}
|
|
19665
19759
|
</tbody>
|
|
19666
19760
|
</table>
|
|
19667
19761
|
</div>`,
|
|
@@ -19737,61 +19831,6 @@ function sprintSummaryPage(data, cached2) {
|
|
|
19737
19831
|
</div>
|
|
19738
19832
|
|
|
19739
19833
|
<script>
|
|
19740
|
-
var _sortCol = -1;
|
|
19741
|
-
var _sortAsc = true;
|
|
19742
|
-
|
|
19743
|
-
function sortWorkItems(col) {
|
|
19744
|
-
var table = document.getElementById('work-items-table');
|
|
19745
|
-
if (!table) return;
|
|
19746
|
-
var tbody = table.querySelector('tbody');
|
|
19747
|
-
var allRows = Array.from(tbody.querySelectorAll('tr'));
|
|
19748
|
-
|
|
19749
|
-
// Toggle direction if clicking the same column
|
|
19750
|
-
if (_sortCol === col) {
|
|
19751
|
-
_sortAsc = !_sortAsc;
|
|
19752
|
-
} else {
|
|
19753
|
-
_sortCol = col;
|
|
19754
|
-
_sortAsc = true;
|
|
19755
|
-
}
|
|
19756
|
-
|
|
19757
|
-
// Update sort arrows
|
|
19758
|
-
for (var i = 0; i < 6; i++) {
|
|
19759
|
-
var arrow = document.getElementById('sort-arrow-' + i);
|
|
19760
|
-
if (arrow) arrow.textContent = i === col ? (_sortAsc ? ' \\u25B2' : ' \\u25BC') : '';
|
|
19761
|
-
}
|
|
19762
|
-
|
|
19763
|
-
// Group rows: root rows + their child/contribution rows
|
|
19764
|
-
var groups = [];
|
|
19765
|
-
var current = null;
|
|
19766
|
-
for (var r = 0; r < allRows.length; r++) {
|
|
19767
|
-
var row = allRows[r];
|
|
19768
|
-
var isChild = row.classList.contains('child-row') || row.classList.contains('contribution-row');
|
|
19769
|
-
if (!isChild) {
|
|
19770
|
-
current = { root: row, children: [] };
|
|
19771
|
-
groups.push(current);
|
|
19772
|
-
} else if (current) {
|
|
19773
|
-
current.children.push(row);
|
|
19774
|
-
}
|
|
19775
|
-
}
|
|
19776
|
-
|
|
19777
|
-
// Sort groups by root row text content of target column
|
|
19778
|
-
groups.sort(function(a, b) {
|
|
19779
|
-
var aText = (a.root.children[col] ? a.root.children[col].textContent : '').trim().toLowerCase();
|
|
19780
|
-
var bText = (b.root.children[col] ? b.root.children[col].textContent : '').trim().toLowerCase();
|
|
19781
|
-
if (aText < bText) return _sortAsc ? -1 : 1;
|
|
19782
|
-
if (aText > bText) return _sortAsc ? 1 : -1;
|
|
19783
|
-
return 0;
|
|
19784
|
-
});
|
|
19785
|
-
|
|
19786
|
-
// Re-append rows in sorted order
|
|
19787
|
-
for (var g = 0; g < groups.length; g++) {
|
|
19788
|
-
tbody.appendChild(groups[g].root);
|
|
19789
|
-
for (var c = 0; c < groups[g].children.length; c++) {
|
|
19790
|
-
tbody.appendChild(groups[g].children[c]);
|
|
19791
|
-
}
|
|
19792
|
-
}
|
|
19793
|
-
}
|
|
19794
|
-
|
|
19795
19834
|
async function generateSummary() {
|
|
19796
19835
|
var btn = document.getElementById('generate-btn');
|
|
19797
19836
|
var loading = document.getElementById('summary-loading');
|
|
@@ -19993,7 +20032,7 @@ function dmRisksPage(ctx) {
|
|
|
19993
20032
|
<tr>
|
|
19994
20033
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19995
20034
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19996
|
-
<td>${escapeHtml(
|
|
20035
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
19997
20036
|
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19998
20037
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19999
20038
|
</tr>`).join("")}
|
|
@@ -20017,7 +20056,7 @@ function dmRisksPage(ctx) {
|
|
|
20017
20056
|
<tr>
|
|
20018
20057
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
20019
20058
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
20020
|
-
<td>${escapeHtml(
|
|
20059
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
20021
20060
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
20022
20061
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
20023
20062
|
<td><span class="${ageDays > 30 ? "priority-high" : "priority-medium"}">${ageDays}d</span></td>
|
|
@@ -20582,16 +20621,16 @@ function tlSprintPage(ctx) {
|
|
|
20582
20621
|
`<div class="table-wrap">
|
|
20583
20622
|
<table>
|
|
20584
20623
|
<thead>
|
|
20585
|
-
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>
|
|
20624
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
|
|
20586
20625
|
</thead>
|
|
20587
20626
|
<tbody>
|
|
20588
20627
|
${techItems.map((w) => `
|
|
20589
20628
|
<tr>
|
|
20590
20629
|
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
20591
20630
|
<td>${escapeHtml(w.title)}</td>
|
|
20592
|
-
<td>${escapeHtml(
|
|
20631
|
+
<td>${escapeHtml(typeLabel2(w.type))}</td>
|
|
20593
20632
|
<td>${statusBadge(w.status)}</td>
|
|
20594
|
-
<td>${w.
|
|
20633
|
+
<td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
20595
20634
|
</tr>`).join("")}
|
|
20596
20635
|
</tbody>
|
|
20597
20636
|
</table>
|
|
@@ -20611,7 +20650,7 @@ function tlSprintPage(ctx) {
|
|
|
20611
20650
|
<tr>
|
|
20612
20651
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
20613
20652
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
20614
|
-
<td>${escapeHtml(
|
|
20653
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
20615
20654
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
20616
20655
|
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
20617
20656
|
</tr>`).join("")}
|
|
@@ -20800,7 +20839,7 @@ function timelinePage(diagrams) {
|
|
|
20800
20839
|
// src/web/templates/pages/board.ts
|
|
20801
20840
|
function boardPage(data, basePath = "/board") {
|
|
20802
20841
|
const typeOptions = data.types.map(
|
|
20803
|
-
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(
|
|
20842
|
+
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(typeLabel2(t))}s</option>`
|
|
20804
20843
|
).join("");
|
|
20805
20844
|
const columns = data.columns.map(
|
|
20806
20845
|
(col) => `
|
|
@@ -20957,7 +20996,7 @@ function upcomingPage(data) {
|
|
|
20957
20996
|
<td><span class="trending-rank">${i + 1}</span></td>
|
|
20958
20997
|
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
20959
20998
|
<td>${escapeHtml(t.title)}</td>
|
|
20960
|
-
<td>${escapeHtml(
|
|
20999
|
+
<td>${escapeHtml(typeLabel2(t.type))}</td>
|
|
20961
21000
|
<td>${statusBadge(t.status)}</td>
|
|
20962
21001
|
<td><span class="trending-score">${t.score}</span></td>
|
|
20963
21002
|
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
@@ -21051,7 +21090,7 @@ function buildPersonaLayoutOpts(persona, activePath, navGroups) {
|
|
|
21051
21090
|
const artifactGroupsHtml = navGroups.map((group) => {
|
|
21052
21091
|
const links = group.types.map((type) => {
|
|
21053
21092
|
const href = `/docs/${type}?persona=${persona}`;
|
|
21054
|
-
return `<a href="${href}" class="${isActive(`/docs/${type}`)}">${
|
|
21093
|
+
return `<a href="${href}" class="${isActive(`/docs/${type}`)}">${typeLabel2(type)}s</a>`;
|
|
21055
21094
|
}).join("\n ");
|
|
21056
21095
|
const groupActive = group.types.some(
|
|
21057
21096
|
(type) => isActive(`/docs/${type}`) !== ""
|
|
@@ -21328,7 +21367,8 @@ function createMeetingTools(store) {
|
|
|
21328
21367
|
title: external_exports.string().describe("Title of the meeting"),
|
|
21329
21368
|
content: external_exports.string().describe("Meeting agenda, notes, or minutes"),
|
|
21330
21369
|
status: external_exports.string().optional().describe("Status (default: 'scheduled')"),
|
|
21331
|
-
owner:
|
|
21370
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
21371
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
21332
21372
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
21333
21373
|
attendees: external_exports.array(external_exports.string()).optional().describe("List of attendees"),
|
|
21334
21374
|
date: external_exports.string().describe("Date the meeting took place (ISO format, e.g. '2025-01-15'). Extract from the meeting content. If not found, ask the user before calling this tool.")
|
|
@@ -21338,7 +21378,8 @@ function createMeetingTools(store) {
|
|
|
21338
21378
|
title: args.title,
|
|
21339
21379
|
status: args.status ?? "scheduled"
|
|
21340
21380
|
};
|
|
21341
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
21381
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
21382
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
21342
21383
|
if (args.tags) frontmatter.tags = args.tags;
|
|
21343
21384
|
if (args.attendees) frontmatter.attendees = args.attendees;
|
|
21344
21385
|
frontmatter.date = args.date;
|
|
@@ -21365,10 +21406,13 @@ function createMeetingTools(store) {
|
|
|
21365
21406
|
title: external_exports.string().optional().describe("New title"),
|
|
21366
21407
|
status: external_exports.string().optional().describe("New status"),
|
|
21367
21408
|
content: external_exports.string().optional().describe("New content"),
|
|
21368
|
-
owner:
|
|
21409
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
21410
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work")
|
|
21369
21411
|
},
|
|
21370
21412
|
async (args) => {
|
|
21371
|
-
const { id, content, ...updates } = args;
|
|
21413
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
21414
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
21415
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
21372
21416
|
const doc = store.update(id, updates, content);
|
|
21373
21417
|
return {
|
|
21374
21418
|
content: [
|
|
@@ -21856,7 +21900,8 @@ function createFeatureTools(store) {
|
|
|
21856
21900
|
title: external_exports.string().describe("Feature title"),
|
|
21857
21901
|
content: external_exports.string().describe("Feature description and requirements"),
|
|
21858
21902
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("Feature status (default: 'draft')"),
|
|
21859
|
-
owner:
|
|
21903
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
21904
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
21860
21905
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Feature priority"),
|
|
21861
21906
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
21862
21907
|
},
|
|
@@ -21865,7 +21910,8 @@ function createFeatureTools(store) {
|
|
|
21865
21910
|
title: args.title,
|
|
21866
21911
|
status: args.status ?? "draft"
|
|
21867
21912
|
};
|
|
21868
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
21913
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
21914
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
21869
21915
|
if (args.priority) frontmatter.priority = args.priority;
|
|
21870
21916
|
if (args.tags) frontmatter.tags = args.tags;
|
|
21871
21917
|
const doc = store.create("feature", frontmatter, args.content);
|
|
@@ -21887,12 +21933,15 @@ function createFeatureTools(store) {
|
|
|
21887
21933
|
title: external_exports.string().optional().describe("New title"),
|
|
21888
21934
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
|
|
21889
21935
|
content: external_exports.string().optional().describe("New content"),
|
|
21890
|
-
owner:
|
|
21936
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
21937
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
21891
21938
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
21892
21939
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
21893
21940
|
},
|
|
21894
21941
|
async (args) => {
|
|
21895
|
-
const { id, content, ...updates } = args;
|
|
21942
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
21943
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
21944
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
21896
21945
|
const doc = store.update(id, updates, content);
|
|
21897
21946
|
return {
|
|
21898
21947
|
content: [
|
|
@@ -21990,7 +22039,8 @@ function createEpicTools(store) {
|
|
|
21990
22039
|
content: external_exports.string().describe("Epic description and scope"),
|
|
21991
22040
|
linkedFeature: linkedFeatureArray.describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
|
|
21992
22041
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
|
|
21993
|
-
owner:
|
|
22042
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
22043
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
21994
22044
|
targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
|
|
21995
22045
|
estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g. '2 weeks', '5 story points')"),
|
|
21996
22046
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
|
|
@@ -22039,7 +22089,8 @@ function createEpicTools(store) {
|
|
|
22039
22089
|
linkedFeature: linkedFeatures,
|
|
22040
22090
|
tags: [...generateFeatureTags(linkedFeatures), ...args.tags ?? []]
|
|
22041
22091
|
};
|
|
22042
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
22092
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
22093
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
22043
22094
|
if (args.targetDate) frontmatter.targetDate = args.targetDate;
|
|
22044
22095
|
if (args.estimatedEffort) frontmatter.estimatedEffort = args.estimatedEffort;
|
|
22045
22096
|
const doc = store.create("epic", frontmatter, args.content);
|
|
@@ -22061,14 +22112,17 @@ function createEpicTools(store) {
|
|
|
22061
22112
|
title: external_exports.string().optional().describe("New title"),
|
|
22062
22113
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("New status"),
|
|
22063
22114
|
content: external_exports.string().optional().describe("New content"),
|
|
22064
|
-
owner:
|
|
22115
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
22116
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
22065
22117
|
targetDate: external_exports.string().optional().describe("New target date"),
|
|
22066
22118
|
estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
|
|
22067
22119
|
linkedFeature: linkedFeatureArray.optional().describe("New linked feature ID(s)"),
|
|
22068
22120
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
22069
22121
|
},
|
|
22070
22122
|
async (args) => {
|
|
22071
|
-
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, ...updates } = args;
|
|
22123
|
+
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, owner, assignee, ...updates } = args;
|
|
22124
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
22125
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
22072
22126
|
if (rawLinkedFeature !== void 0) {
|
|
22073
22127
|
const linkedFeatures = normalizeLinkedFeatures(rawLinkedFeature);
|
|
22074
22128
|
for (const featureId of linkedFeatures) {
|
|
@@ -22195,7 +22249,7 @@ function createContributionTools(store) {
|
|
|
22195
22249
|
aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
|
|
22196
22250
|
status: external_exports.string().optional().describe("Status (default: 'done')"),
|
|
22197
22251
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
22198
|
-
|
|
22252
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag."),
|
|
22199
22253
|
parentProgress: external_exports.number().optional().describe("Set progress (0-100) on the parent artifact (e.g. task or action). Propagates up the hierarchy.")
|
|
22200
22254
|
},
|
|
22201
22255
|
async (args) => {
|
|
@@ -22207,7 +22261,7 @@ function createContributionTools(store) {
|
|
|
22207
22261
|
};
|
|
22208
22262
|
frontmatter.aboutArtifact = args.aboutArtifact;
|
|
22209
22263
|
const tags = [...args.tags ?? []];
|
|
22210
|
-
if (args.
|
|
22264
|
+
if (args.workFocus) tags.push(`focus:${args.workFocus}`);
|
|
22211
22265
|
if (tags.length > 0) frontmatter.tags = tags;
|
|
22212
22266
|
const doc = store.create("contribution", frontmatter, args.content);
|
|
22213
22267
|
const progressParts = [];
|
|
@@ -22263,15 +22317,15 @@ function createContributionTools(store) {
|
|
|
22263
22317
|
title: external_exports.string().optional().describe("New title"),
|
|
22264
22318
|
status: external_exports.string().optional().describe("New status"),
|
|
22265
22319
|
content: external_exports.string().optional().describe("New content"),
|
|
22266
|
-
|
|
22320
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag.")
|
|
22267
22321
|
},
|
|
22268
22322
|
async (args) => {
|
|
22269
|
-
const { id, content,
|
|
22270
|
-
if (
|
|
22323
|
+
const { id, content, workFocus, ...updates } = args;
|
|
22324
|
+
if (workFocus !== void 0) {
|
|
22271
22325
|
const existing = store.get(id);
|
|
22272
22326
|
const existingTags = existing?.frontmatter.tags ?? [];
|
|
22273
|
-
const filtered = existingTags.filter((t) => !t.startsWith("
|
|
22274
|
-
filtered.push(`
|
|
22327
|
+
const filtered = existingTags.filter((t) => !t.startsWith("focus:"));
|
|
22328
|
+
filtered.push(`focus:${workFocus}`);
|
|
22275
22329
|
updates.tags = filtered;
|
|
22276
22330
|
}
|
|
22277
22331
|
const oldDoc = store.get(id);
|
|
@@ -22754,7 +22808,7 @@ function createTaskTools(store) {
|
|
|
22754
22808
|
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
|
|
22755
22809
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
|
|
22756
22810
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags"),
|
|
22757
|
-
|
|
22811
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
|
|
22758
22812
|
},
|
|
22759
22813
|
async (args) => {
|
|
22760
22814
|
const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
|
|
@@ -22768,7 +22822,7 @@ function createTaskTools(store) {
|
|
|
22768
22822
|
}
|
|
22769
22823
|
}
|
|
22770
22824
|
const baseTags = [...generateEpicTags(linkedEpics), ...args.tags ?? []];
|
|
22771
|
-
if (args.
|
|
22825
|
+
if (args.workFocus) baseTags.push(`focus:${args.workFocus}`);
|
|
22772
22826
|
const frontmatter = {
|
|
22773
22827
|
title: args.title,
|
|
22774
22828
|
status: args.status ?? "backlog",
|
|
@@ -22809,11 +22863,11 @@ function createTaskTools(store) {
|
|
|
22809
22863
|
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
|
|
22810
22864
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
22811
22865
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
|
|
22812
|
-
|
|
22866
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
22813
22867
|
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions.")
|
|
22814
22868
|
},
|
|
22815
22869
|
async (args) => {
|
|
22816
|
-
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags,
|
|
22870
|
+
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workFocus, progress, ...updates } = args;
|
|
22817
22871
|
const warnings = [];
|
|
22818
22872
|
if (rawLinkedEpic !== void 0) {
|
|
22819
22873
|
const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
|
|
@@ -22834,10 +22888,10 @@ function createTaskTools(store) {
|
|
|
22834
22888
|
} else if (userTags !== void 0) {
|
|
22835
22889
|
updates.tags = userTags;
|
|
22836
22890
|
}
|
|
22837
|
-
if (
|
|
22891
|
+
if (workFocus !== void 0) {
|
|
22838
22892
|
const currentTags = updates.tags ?? store.get(id)?.frontmatter.tags ?? [];
|
|
22839
|
-
const filtered = currentTags.filter((t) => !t.startsWith("
|
|
22840
|
-
filtered.push(`
|
|
22893
|
+
const filtered = currentTags.filter((t) => !t.startsWith("focus:"));
|
|
22894
|
+
filtered.push(`focus:${workFocus}`);
|
|
22841
22895
|
updates.tags = filtered;
|
|
22842
22896
|
}
|
|
22843
22897
|
if (typeof progress === "number") {
|
|
@@ -25409,6 +25463,556 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
25409
25463
|
];
|
|
25410
25464
|
}
|
|
25411
25465
|
|
|
25466
|
+
// src/agent/tools/doctor.ts
|
|
25467
|
+
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
25468
|
+
|
|
25469
|
+
// src/doctor/rules/tag-migration.ts
|
|
25470
|
+
var RULE_ID = "tag-migration";
|
|
25471
|
+
var RULE_NAME = "Tag Migration";
|
|
25472
|
+
var tagMigrationRule = {
|
|
25473
|
+
id: RULE_ID,
|
|
25474
|
+
name: RULE_NAME,
|
|
25475
|
+
description: "Detects deprecated stream:* tags and replaces them with focus:*",
|
|
25476
|
+
scan(ctx) {
|
|
25477
|
+
const issues = [];
|
|
25478
|
+
for (const doc of ctx.allDocuments) {
|
|
25479
|
+
const tags = doc.frontmatter.tags;
|
|
25480
|
+
if (!Array.isArray(tags)) continue;
|
|
25481
|
+
const streamTags = tags.filter((t) => t.startsWith("stream:"));
|
|
25482
|
+
for (const tag of streamTags) {
|
|
25483
|
+
issues.push({
|
|
25484
|
+
ruleId: RULE_ID,
|
|
25485
|
+
ruleName: RULE_NAME,
|
|
25486
|
+
documentId: doc.frontmatter.id,
|
|
25487
|
+
filePath: doc.filePath,
|
|
25488
|
+
documentType: doc.frontmatter.type,
|
|
25489
|
+
message: `Deprecated tag "${tag}" should be "${tag.replace("stream:", "focus:")}"`,
|
|
25490
|
+
severity: "warning",
|
|
25491
|
+
fixable: true
|
|
25492
|
+
});
|
|
25493
|
+
}
|
|
25494
|
+
}
|
|
25495
|
+
return issues;
|
|
25496
|
+
},
|
|
25497
|
+
fix(ctx) {
|
|
25498
|
+
const fixes = [];
|
|
25499
|
+
for (const doc of ctx.allDocuments) {
|
|
25500
|
+
const tags = doc.frontmatter.tags;
|
|
25501
|
+
if (!Array.isArray(tags)) continue;
|
|
25502
|
+
const streamTags = tags.filter((t) => t.startsWith("stream:"));
|
|
25503
|
+
if (streamTags.length === 0) continue;
|
|
25504
|
+
const newTags = tags.map(
|
|
25505
|
+
(t) => t.startsWith("stream:") ? t.replace("stream:", "focus:") : t
|
|
25506
|
+
);
|
|
25507
|
+
ctx.store.update(doc.frontmatter.id, { tags: newTags });
|
|
25508
|
+
for (const tag of streamTags) {
|
|
25509
|
+
fixes.push({
|
|
25510
|
+
issue: {
|
|
25511
|
+
ruleId: RULE_ID,
|
|
25512
|
+
ruleName: RULE_NAME,
|
|
25513
|
+
documentId: doc.frontmatter.id,
|
|
25514
|
+
filePath: doc.filePath,
|
|
25515
|
+
documentType: doc.frontmatter.type,
|
|
25516
|
+
message: `Deprecated tag "${tag}" should be "${tag.replace("stream:", "focus:")}"`,
|
|
25517
|
+
severity: "warning",
|
|
25518
|
+
fixable: true
|
|
25519
|
+
},
|
|
25520
|
+
fixDescription: `Renamed "${tag}" to "${tag.replace("stream:", "focus:")}"`
|
|
25521
|
+
});
|
|
25522
|
+
}
|
|
25523
|
+
}
|
|
25524
|
+
return fixes;
|
|
25525
|
+
}
|
|
25526
|
+
};
|
|
25527
|
+
|
|
25528
|
+
// src/doctor/rules/array-normalization.ts
|
|
25529
|
+
var RULE_ID2 = "array-normalization";
|
|
25530
|
+
var RULE_NAME2 = "Array Normalization";
|
|
25531
|
+
var FIELDS = [
|
|
25532
|
+
{
|
|
25533
|
+
field: "linkedEpic",
|
|
25534
|
+
aliases: ["linkedEpics"],
|
|
25535
|
+
normalize: normalizeLinkedEpics
|
|
25536
|
+
},
|
|
25537
|
+
{
|
|
25538
|
+
field: "linkedFeature",
|
|
25539
|
+
aliases: ["linkedFeatures"],
|
|
25540
|
+
normalize: normalizeLinkedFeatures
|
|
25541
|
+
}
|
|
25542
|
+
];
|
|
25543
|
+
var arrayNormalizationRule = {
|
|
25544
|
+
id: RULE_ID2,
|
|
25545
|
+
name: RULE_NAME2,
|
|
25546
|
+
description: "Normalizes linkedEpic/linkedFeature from strings to arrays and resolves field aliases",
|
|
25547
|
+
scan(ctx) {
|
|
25548
|
+
const issues = [];
|
|
25549
|
+
for (const doc of ctx.allDocuments) {
|
|
25550
|
+
const fm = doc.frontmatter;
|
|
25551
|
+
for (const cfg of FIELDS) {
|
|
25552
|
+
for (const alias of cfg.aliases) {
|
|
25553
|
+
if (fm[alias] !== void 0) {
|
|
25554
|
+
issues.push({
|
|
25555
|
+
ruleId: RULE_ID2,
|
|
25556
|
+
ruleName: RULE_NAME2,
|
|
25557
|
+
documentId: doc.frontmatter.id,
|
|
25558
|
+
filePath: doc.filePath,
|
|
25559
|
+
documentType: doc.frontmatter.type,
|
|
25560
|
+
message: `Field "${alias}" should be renamed to "${cfg.field}"`,
|
|
25561
|
+
severity: "warning",
|
|
25562
|
+
fixable: true
|
|
25563
|
+
});
|
|
25564
|
+
}
|
|
25565
|
+
}
|
|
25566
|
+
const value = fm[cfg.field];
|
|
25567
|
+
if (typeof value === "string") {
|
|
25568
|
+
issues.push({
|
|
25569
|
+
ruleId: RULE_ID2,
|
|
25570
|
+
ruleName: RULE_NAME2,
|
|
25571
|
+
documentId: doc.frontmatter.id,
|
|
25572
|
+
filePath: doc.filePath,
|
|
25573
|
+
documentType: doc.frontmatter.type,
|
|
25574
|
+
message: `Field "${cfg.field}" is a string ("${value}") but should be an array`,
|
|
25575
|
+
severity: "warning",
|
|
25576
|
+
fixable: true
|
|
25577
|
+
});
|
|
25578
|
+
}
|
|
25579
|
+
}
|
|
25580
|
+
}
|
|
25581
|
+
return issues;
|
|
25582
|
+
},
|
|
25583
|
+
fix(ctx) {
|
|
25584
|
+
const fixes = [];
|
|
25585
|
+
for (const doc of ctx.allDocuments) {
|
|
25586
|
+
const fm = doc.frontmatter;
|
|
25587
|
+
const updates = {};
|
|
25588
|
+
let needsUpdate = false;
|
|
25589
|
+
for (const cfg of FIELDS) {
|
|
25590
|
+
for (const alias of cfg.aliases) {
|
|
25591
|
+
if (fm[alias] !== void 0) {
|
|
25592
|
+
const aliasValues = cfg.normalize(fm[alias]);
|
|
25593
|
+
const existing = cfg.normalize(fm[cfg.field]);
|
|
25594
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...aliasValues])];
|
|
25595
|
+
updates[cfg.field] = merged;
|
|
25596
|
+
updates[alias] = void 0;
|
|
25597
|
+
needsUpdate = true;
|
|
25598
|
+
fixes.push({
|
|
25599
|
+
issue: {
|
|
25600
|
+
ruleId: RULE_ID2,
|
|
25601
|
+
ruleName: RULE_NAME2,
|
|
25602
|
+
documentId: doc.frontmatter.id,
|
|
25603
|
+
filePath: doc.filePath,
|
|
25604
|
+
documentType: doc.frontmatter.type,
|
|
25605
|
+
message: `Field "${alias}" should be renamed to "${cfg.field}"`,
|
|
25606
|
+
severity: "warning",
|
|
25607
|
+
fixable: true
|
|
25608
|
+
},
|
|
25609
|
+
fixDescription: `Merged "${alias}" into "${cfg.field}" and removed alias`
|
|
25610
|
+
});
|
|
25611
|
+
}
|
|
25612
|
+
}
|
|
25613
|
+
const value = updates[cfg.field] ?? fm[cfg.field];
|
|
25614
|
+
if (typeof value === "string") {
|
|
25615
|
+
updates[cfg.field] = cfg.normalize(value);
|
|
25616
|
+
needsUpdate = true;
|
|
25617
|
+
fixes.push({
|
|
25618
|
+
issue: {
|
|
25619
|
+
ruleId: RULE_ID2,
|
|
25620
|
+
ruleName: RULE_NAME2,
|
|
25621
|
+
documentId: doc.frontmatter.id,
|
|
25622
|
+
filePath: doc.filePath,
|
|
25623
|
+
documentType: doc.frontmatter.type,
|
|
25624
|
+
message: `Field "${cfg.field}" is a string ("${value}") but should be an array`,
|
|
25625
|
+
severity: "warning",
|
|
25626
|
+
fixable: true
|
|
25627
|
+
},
|
|
25628
|
+
fixDescription: `Normalized "${cfg.field}" from string to array`
|
|
25629
|
+
});
|
|
25630
|
+
}
|
|
25631
|
+
}
|
|
25632
|
+
if (needsUpdate) {
|
|
25633
|
+
ctx.store.update(doc.frontmatter.id, updates);
|
|
25634
|
+
}
|
|
25635
|
+
}
|
|
25636
|
+
return fixes;
|
|
25637
|
+
}
|
|
25638
|
+
};
|
|
25639
|
+
|
|
25640
|
+
// src/doctor/rules/missing-auto-tags.ts
|
|
25641
|
+
var RULE_ID3 = "missing-auto-tags";
|
|
25642
|
+
var RULE_NAME3 = "Missing Auto Tags";
|
|
25643
|
+
var missingAutoTagsRule = {
|
|
25644
|
+
id: RULE_ID3,
|
|
25645
|
+
name: RULE_NAME3,
|
|
25646
|
+
description: "Ensures tasks have epic:E-xxx tags for their linkedEpic and epics have feature:F-xxx tags",
|
|
25647
|
+
scan(ctx) {
|
|
25648
|
+
const issues = [];
|
|
25649
|
+
for (const doc of ctx.allDocuments) {
|
|
25650
|
+
const fm = doc.frontmatter;
|
|
25651
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
25652
|
+
const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
|
|
25653
|
+
if (linkedEpics.length > 0) {
|
|
25654
|
+
const expected = generateEpicTags(linkedEpics);
|
|
25655
|
+
const missing = expected.filter((t) => !tags.includes(t));
|
|
25656
|
+
for (const tag of missing) {
|
|
25657
|
+
issues.push({
|
|
25658
|
+
ruleId: RULE_ID3,
|
|
25659
|
+
ruleName: RULE_NAME3,
|
|
25660
|
+
documentId: doc.frontmatter.id,
|
|
25661
|
+
filePath: doc.filePath,
|
|
25662
|
+
documentType: doc.frontmatter.type,
|
|
25663
|
+
message: `Missing auto-tag "${tag}" for linkedEpic`,
|
|
25664
|
+
severity: "warning",
|
|
25665
|
+
fixable: true
|
|
25666
|
+
});
|
|
25667
|
+
}
|
|
25668
|
+
}
|
|
25669
|
+
const linkedFeatures = normalizeLinkedFeatures(fm.linkedFeature);
|
|
25670
|
+
if (linkedFeatures.length > 0) {
|
|
25671
|
+
const expected = generateFeatureTags(linkedFeatures);
|
|
25672
|
+
const missing = expected.filter((t) => !tags.includes(t));
|
|
25673
|
+
for (const tag of missing) {
|
|
25674
|
+
issues.push({
|
|
25675
|
+
ruleId: RULE_ID3,
|
|
25676
|
+
ruleName: RULE_NAME3,
|
|
25677
|
+
documentId: doc.frontmatter.id,
|
|
25678
|
+
filePath: doc.filePath,
|
|
25679
|
+
documentType: doc.frontmatter.type,
|
|
25680
|
+
message: `Missing auto-tag "${tag}" for linkedFeature`,
|
|
25681
|
+
severity: "warning",
|
|
25682
|
+
fixable: true
|
|
25683
|
+
});
|
|
25684
|
+
}
|
|
25685
|
+
}
|
|
25686
|
+
}
|
|
25687
|
+
return issues;
|
|
25688
|
+
},
|
|
25689
|
+
fix(ctx) {
|
|
25690
|
+
const fixes = [];
|
|
25691
|
+
for (const doc of ctx.allDocuments) {
|
|
25692
|
+
const fm = doc.frontmatter;
|
|
25693
|
+
const tags = [...doc.frontmatter.tags ?? []];
|
|
25694
|
+
let changed = false;
|
|
25695
|
+
const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
|
|
25696
|
+
if (linkedEpics.length > 0) {
|
|
25697
|
+
const expected = generateEpicTags(linkedEpics);
|
|
25698
|
+
for (const tag of expected) {
|
|
25699
|
+
if (!tags.includes(tag)) {
|
|
25700
|
+
tags.push(tag);
|
|
25701
|
+
changed = true;
|
|
25702
|
+
fixes.push({
|
|
25703
|
+
issue: {
|
|
25704
|
+
ruleId: RULE_ID3,
|
|
25705
|
+
ruleName: RULE_NAME3,
|
|
25706
|
+
documentId: doc.frontmatter.id,
|
|
25707
|
+
filePath: doc.filePath,
|
|
25708
|
+
documentType: doc.frontmatter.type,
|
|
25709
|
+
message: `Missing auto-tag "${tag}" for linkedEpic`,
|
|
25710
|
+
severity: "warning",
|
|
25711
|
+
fixable: true
|
|
25712
|
+
},
|
|
25713
|
+
fixDescription: `Added tag "${tag}"`
|
|
25714
|
+
});
|
|
25715
|
+
}
|
|
25716
|
+
}
|
|
25717
|
+
}
|
|
25718
|
+
const linkedFeatures = normalizeLinkedFeatures(fm.linkedFeature);
|
|
25719
|
+
if (linkedFeatures.length > 0) {
|
|
25720
|
+
const expected = generateFeatureTags(linkedFeatures);
|
|
25721
|
+
for (const tag of expected) {
|
|
25722
|
+
if (!tags.includes(tag)) {
|
|
25723
|
+
tags.push(tag);
|
|
25724
|
+
changed = true;
|
|
25725
|
+
fixes.push({
|
|
25726
|
+
issue: {
|
|
25727
|
+
ruleId: RULE_ID3,
|
|
25728
|
+
ruleName: RULE_NAME3,
|
|
25729
|
+
documentId: doc.frontmatter.id,
|
|
25730
|
+
filePath: doc.filePath,
|
|
25731
|
+
documentType: doc.frontmatter.type,
|
|
25732
|
+
message: `Missing auto-tag "${tag}" for linkedFeature`,
|
|
25733
|
+
severity: "warning",
|
|
25734
|
+
fixable: true
|
|
25735
|
+
},
|
|
25736
|
+
fixDescription: `Added tag "${tag}"`
|
|
25737
|
+
});
|
|
25738
|
+
}
|
|
25739
|
+
}
|
|
25740
|
+
}
|
|
25741
|
+
if (changed) {
|
|
25742
|
+
ctx.store.update(doc.frontmatter.id, { tags });
|
|
25743
|
+
}
|
|
25744
|
+
}
|
|
25745
|
+
return fixes;
|
|
25746
|
+
}
|
|
25747
|
+
};
|
|
25748
|
+
|
|
25749
|
+
// src/doctor/rules/progress-consistency.ts
|
|
25750
|
+
var RULE_ID4 = "progress-consistency";
|
|
25751
|
+
var RULE_NAME4 = "Progress Consistency";
|
|
25752
|
+
var progressConsistencyRule = {
|
|
25753
|
+
id: RULE_ID4,
|
|
25754
|
+
name: RULE_NAME4,
|
|
25755
|
+
description: "Detects done-status documents with progress != 100 and progressOverride:true without a progress value",
|
|
25756
|
+
scan(ctx) {
|
|
25757
|
+
const issues = [];
|
|
25758
|
+
for (const doc of ctx.allDocuments) {
|
|
25759
|
+
const fm = doc.frontmatter;
|
|
25760
|
+
const status = doc.frontmatter.status;
|
|
25761
|
+
const progress = fm.progress;
|
|
25762
|
+
const progressOverride = fm.progressOverride;
|
|
25763
|
+
if (status === "done" && progress !== void 0 && progress !== 100) {
|
|
25764
|
+
issues.push({
|
|
25765
|
+
ruleId: RULE_ID4,
|
|
25766
|
+
ruleName: RULE_NAME4,
|
|
25767
|
+
documentId: doc.frontmatter.id,
|
|
25768
|
+
filePath: doc.filePath,
|
|
25769
|
+
documentType: doc.frontmatter.type,
|
|
25770
|
+
message: `Status is "done" but progress is ${progress} (expected 100)`,
|
|
25771
|
+
severity: "error",
|
|
25772
|
+
fixable: true
|
|
25773
|
+
});
|
|
25774
|
+
}
|
|
25775
|
+
if (progressOverride === true && progress === void 0) {
|
|
25776
|
+
issues.push({
|
|
25777
|
+
ruleId: RULE_ID4,
|
|
25778
|
+
ruleName: RULE_NAME4,
|
|
25779
|
+
documentId: doc.frontmatter.id,
|
|
25780
|
+
filePath: doc.filePath,
|
|
25781
|
+
documentType: doc.frontmatter.type,
|
|
25782
|
+
message: `progressOverride is true but no progress value is set`,
|
|
25783
|
+
severity: "warning",
|
|
25784
|
+
fixable: true
|
|
25785
|
+
});
|
|
25786
|
+
}
|
|
25787
|
+
}
|
|
25788
|
+
return issues;
|
|
25789
|
+
},
|
|
25790
|
+
fix(ctx) {
|
|
25791
|
+
const fixes = [];
|
|
25792
|
+
for (const doc of ctx.allDocuments) {
|
|
25793
|
+
const fm = doc.frontmatter;
|
|
25794
|
+
const status = doc.frontmatter.status;
|
|
25795
|
+
const progress = fm.progress;
|
|
25796
|
+
const progressOverride = fm.progressOverride;
|
|
25797
|
+
if (status === "done" && progress !== void 0 && progress !== 100) {
|
|
25798
|
+
ctx.store.update(doc.frontmatter.id, { progress: 100 });
|
|
25799
|
+
fixes.push({
|
|
25800
|
+
issue: {
|
|
25801
|
+
ruleId: RULE_ID4,
|
|
25802
|
+
ruleName: RULE_NAME4,
|
|
25803
|
+
documentId: doc.frontmatter.id,
|
|
25804
|
+
filePath: doc.filePath,
|
|
25805
|
+
documentType: doc.frontmatter.type,
|
|
25806
|
+
message: `Status is "done" but progress is ${progress} (expected 100)`,
|
|
25807
|
+
severity: "error",
|
|
25808
|
+
fixable: true
|
|
25809
|
+
},
|
|
25810
|
+
fixDescription: `Set progress to 100`
|
|
25811
|
+
});
|
|
25812
|
+
}
|
|
25813
|
+
if (progressOverride === true && progress === void 0) {
|
|
25814
|
+
ctx.store.update(doc.frontmatter.id, { progressOverride: false });
|
|
25815
|
+
fixes.push({
|
|
25816
|
+
issue: {
|
|
25817
|
+
ruleId: RULE_ID4,
|
|
25818
|
+
ruleName: RULE_NAME4,
|
|
25819
|
+
documentId: doc.frontmatter.id,
|
|
25820
|
+
filePath: doc.filePath,
|
|
25821
|
+
documentType: doc.frontmatter.type,
|
|
25822
|
+
message: `progressOverride is true but no progress value is set`,
|
|
25823
|
+
severity: "warning",
|
|
25824
|
+
fixable: true
|
|
25825
|
+
},
|
|
25826
|
+
fixDescription: `Set progressOverride to false`
|
|
25827
|
+
});
|
|
25828
|
+
}
|
|
25829
|
+
}
|
|
25830
|
+
return fixes;
|
|
25831
|
+
}
|
|
25832
|
+
};
|
|
25833
|
+
|
|
25834
|
+
// src/doctor/rules/orphaned-references.ts
|
|
25835
|
+
var RULE_ID5 = "orphaned-references";
|
|
25836
|
+
var RULE_NAME5 = "Orphaned References";
|
|
25837
|
+
var REFERENCE_FIELDS = ["aboutArtifact", "linkedEpic", "linkedFeature"];
|
|
25838
|
+
var orphanedReferencesRule = {
|
|
25839
|
+
id: RULE_ID5,
|
|
25840
|
+
name: RULE_NAME5,
|
|
25841
|
+
description: "Detects references (aboutArtifact, linkedEpic, linkedFeature) pointing to non-existent documents",
|
|
25842
|
+
scan(ctx) {
|
|
25843
|
+
const issues = [];
|
|
25844
|
+
for (const doc of ctx.allDocuments) {
|
|
25845
|
+
const fm = doc.frontmatter;
|
|
25846
|
+
for (const field of REFERENCE_FIELDS) {
|
|
25847
|
+
const value = fm[field];
|
|
25848
|
+
if (value === void 0 || value === null) continue;
|
|
25849
|
+
const refs = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" ? [value] : [];
|
|
25850
|
+
for (const ref of refs) {
|
|
25851
|
+
if (!ctx.documentIndex.has(ref)) {
|
|
25852
|
+
issues.push({
|
|
25853
|
+
ruleId: RULE_ID5,
|
|
25854
|
+
ruleName: RULE_NAME5,
|
|
25855
|
+
documentId: doc.frontmatter.id,
|
|
25856
|
+
filePath: doc.filePath,
|
|
25857
|
+
documentType: doc.frontmatter.type,
|
|
25858
|
+
message: `Field "${field}" references "${ref}" which does not exist`,
|
|
25859
|
+
severity: "warning",
|
|
25860
|
+
fixable: false
|
|
25861
|
+
});
|
|
25862
|
+
}
|
|
25863
|
+
}
|
|
25864
|
+
}
|
|
25865
|
+
}
|
|
25866
|
+
return issues;
|
|
25867
|
+
},
|
|
25868
|
+
fix() {
|
|
25869
|
+
return [];
|
|
25870
|
+
}
|
|
25871
|
+
};
|
|
25872
|
+
|
|
25873
|
+
// src/doctor/rules/owner-role.ts
|
|
25874
|
+
var RULE_ID6 = "owner-role";
|
|
25875
|
+
var RULE_NAME6 = "Owner Role";
|
|
25876
|
+
var ownerRoleRule = {
|
|
25877
|
+
id: RULE_ID6,
|
|
25878
|
+
name: RULE_NAME6,
|
|
25879
|
+
description: `Detects owner values that are not valid persona roles (${OWNER_SHORT.join(", ")})`,
|
|
25880
|
+
scan(ctx) {
|
|
25881
|
+
const issues = [];
|
|
25882
|
+
for (const doc of ctx.allDocuments) {
|
|
25883
|
+
const owner = doc.frontmatter.owner;
|
|
25884
|
+
if (owner === void 0 || owner === null || owner === "") continue;
|
|
25885
|
+
if (!isValidOwner(owner)) {
|
|
25886
|
+
issues.push({
|
|
25887
|
+
ruleId: RULE_ID6,
|
|
25888
|
+
ruleName: RULE_NAME6,
|
|
25889
|
+
documentId: doc.frontmatter.id,
|
|
25890
|
+
filePath: doc.filePath,
|
|
25891
|
+
documentType: doc.frontmatter.type,
|
|
25892
|
+
message: `Owner "${owner}" is not a valid persona role. Expected one of: ${OWNER_SHORT.join(", ")}`,
|
|
25893
|
+
severity: "warning",
|
|
25894
|
+
fixable: false
|
|
25895
|
+
});
|
|
25896
|
+
}
|
|
25897
|
+
}
|
|
25898
|
+
return issues;
|
|
25899
|
+
},
|
|
25900
|
+
fix() {
|
|
25901
|
+
return [];
|
|
25902
|
+
}
|
|
25903
|
+
};
|
|
25904
|
+
|
|
25905
|
+
// src/doctor/rules/index.ts
|
|
25906
|
+
var allRules = [
|
|
25907
|
+
tagMigrationRule,
|
|
25908
|
+
arrayNormalizationRule,
|
|
25909
|
+
missingAutoTagsRule,
|
|
25910
|
+
progressConsistencyRule,
|
|
25911
|
+
orphanedReferencesRule,
|
|
25912
|
+
ownerRoleRule
|
|
25913
|
+
];
|
|
25914
|
+
|
|
25915
|
+
// src/doctor/engine.ts
|
|
25916
|
+
function buildDoctorContext(store) {
|
|
25917
|
+
const allDocuments = store.list();
|
|
25918
|
+
const documentIndex = new Map(
|
|
25919
|
+
allDocuments.map((doc) => [doc.frontmatter.id, doc])
|
|
25920
|
+
);
|
|
25921
|
+
return { store, allDocuments, documentIndex };
|
|
25922
|
+
}
|
|
25923
|
+
function runDoctorScan(store, ruleFilter) {
|
|
25924
|
+
const rules = resolveRules(ruleFilter);
|
|
25925
|
+
const ctx = buildDoctorContext(store);
|
|
25926
|
+
const issues = rules.flatMap((rule) => rule.scan(ctx));
|
|
25927
|
+
return buildReport(ctx, issues, []);
|
|
25928
|
+
}
|
|
25929
|
+
function runDoctorFix(store, ruleFilter) {
|
|
25930
|
+
const rules = resolveRules(ruleFilter);
|
|
25931
|
+
let ctx = buildDoctorContext(store);
|
|
25932
|
+
const allIssues = rules.flatMap((rule) => rule.scan(ctx));
|
|
25933
|
+
const allFixes = [];
|
|
25934
|
+
for (const rule of rules) {
|
|
25935
|
+
const fixes = rule.fix(ctx);
|
|
25936
|
+
allFixes.push(...fixes);
|
|
25937
|
+
if (fixes.length > 0) {
|
|
25938
|
+
ctx = buildDoctorContext(store);
|
|
25939
|
+
}
|
|
25940
|
+
}
|
|
25941
|
+
return buildReport(ctx, allIssues, allFixes);
|
|
25942
|
+
}
|
|
25943
|
+
function resolveRules(ruleFilter) {
|
|
25944
|
+
if (!ruleFilter) return allRules;
|
|
25945
|
+
const rule = allRules.find((r) => r.id === ruleFilter);
|
|
25946
|
+
if (!rule) {
|
|
25947
|
+
throw new Error(
|
|
25948
|
+
`Unknown rule: ${ruleFilter}. Available: ${allRules.map((r) => r.id).join(", ")}`
|
|
25949
|
+
);
|
|
25950
|
+
}
|
|
25951
|
+
return [rule];
|
|
25952
|
+
}
|
|
25953
|
+
function buildReport(ctx, issues, fixes) {
|
|
25954
|
+
const byRule = {};
|
|
25955
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
25956
|
+
let fixableIssues = 0;
|
|
25957
|
+
for (const issue2 of issues) {
|
|
25958
|
+
byRule[issue2.ruleId] = (byRule[issue2.ruleId] ?? 0) + 1;
|
|
25959
|
+
bySeverity[issue2.severity] = (bySeverity[issue2.severity] ?? 0) + 1;
|
|
25960
|
+
if (issue2.fixable) fixableIssues++;
|
|
25961
|
+
}
|
|
25962
|
+
return {
|
|
25963
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25964
|
+
totalDocuments: ctx.allDocuments.length,
|
|
25965
|
+
issues,
|
|
25966
|
+
fixes,
|
|
25967
|
+
summary: {
|
|
25968
|
+
totalIssues: issues.length,
|
|
25969
|
+
fixableIssues,
|
|
25970
|
+
fixedIssues: fixes.length,
|
|
25971
|
+
byRule,
|
|
25972
|
+
bySeverity
|
|
25973
|
+
}
|
|
25974
|
+
};
|
|
25975
|
+
}
|
|
25976
|
+
|
|
25977
|
+
// src/agent/tools/doctor.ts
|
|
25978
|
+
function createDoctorTools(store) {
|
|
25979
|
+
return [
|
|
25980
|
+
tool23(
|
|
25981
|
+
"run_doctor",
|
|
25982
|
+
"Scan project documents for structural issues and optionally auto-repair them. Returns a JSON report with all issues found and fixes applied.",
|
|
25983
|
+
{
|
|
25984
|
+
fix: external_exports.boolean().optional().default(false).describe("When true, auto-repair fixable issues"),
|
|
25985
|
+
rule: external_exports.string().optional().describe(
|
|
25986
|
+
"Run only a specific rule (e.g. tag-migration, array-normalization, missing-auto-tags, progress-consistency, orphaned-references)"
|
|
25987
|
+
)
|
|
25988
|
+
},
|
|
25989
|
+
async (args) => {
|
|
25990
|
+
try {
|
|
25991
|
+
const report = args.fix ? runDoctorFix(store, args.rule) : runDoctorScan(store, args.rule);
|
|
25992
|
+
return {
|
|
25993
|
+
content: [
|
|
25994
|
+
{
|
|
25995
|
+
type: "text",
|
|
25996
|
+
text: JSON.stringify(report, null, 2)
|
|
25997
|
+
}
|
|
25998
|
+
]
|
|
25999
|
+
};
|
|
26000
|
+
} catch (err) {
|
|
26001
|
+
return {
|
|
26002
|
+
content: [
|
|
26003
|
+
{
|
|
26004
|
+
type: "text",
|
|
26005
|
+
text: `Doctor error: ${err instanceof Error ? err.message : String(err)}`
|
|
26006
|
+
}
|
|
26007
|
+
],
|
|
26008
|
+
isError: true
|
|
26009
|
+
};
|
|
26010
|
+
}
|
|
26011
|
+
}
|
|
26012
|
+
)
|
|
26013
|
+
];
|
|
26014
|
+
}
|
|
26015
|
+
|
|
25412
26016
|
// src/agent/mcp-server.ts
|
|
25413
26017
|
function createMarvinMcpServer(store, options) {
|
|
25414
26018
|
const tools = [
|
|
@@ -25420,7 +26024,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
25420
26024
|
...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
|
|
25421
26025
|
...options?.pluginTools ?? [],
|
|
25422
26026
|
...options?.skillTools ?? [],
|
|
25423
|
-
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
|
|
26027
|
+
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : [],
|
|
26028
|
+
...createDoctorTools(store)
|
|
25424
26029
|
];
|
|
25425
26030
|
return createSdkMcpServer({
|
|
25426
26031
|
name: "marvin-governance",
|
|
@@ -25878,7 +26483,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
25878
26483
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25879
26484
|
|
|
25880
26485
|
// src/skills/action-tools.ts
|
|
25881
|
-
import { tool as
|
|
26486
|
+
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
25882
26487
|
|
|
25883
26488
|
// src/skills/action-runner.ts
|
|
25884
26489
|
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -25944,7 +26549,7 @@ function createSkillActionTools(skills, context) {
|
|
|
25944
26549
|
if (!skill.actions) continue;
|
|
25945
26550
|
for (const action of skill.actions) {
|
|
25946
26551
|
tools.push(
|
|
25947
|
-
|
|
26552
|
+
tool24(
|
|
25948
26553
|
`${skill.id}__${action.id}`,
|
|
25949
26554
|
action.description,
|
|
25950
26555
|
{
|
|
@@ -26036,10 +26641,10 @@ ${lines.join("\n\n")}`;
|
|
|
26036
26641
|
}
|
|
26037
26642
|
|
|
26038
26643
|
// src/mcp/persona-tools.ts
|
|
26039
|
-
import { tool as
|
|
26644
|
+
import { tool as tool25 } from "@anthropic-ai/claude-agent-sdk";
|
|
26040
26645
|
function createPersonaTools(ctx, marvinDir) {
|
|
26041
26646
|
return [
|
|
26042
|
-
|
|
26647
|
+
tool25(
|
|
26043
26648
|
"set_persona",
|
|
26044
26649
|
"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.",
|
|
26045
26650
|
{
|
|
@@ -26069,7 +26674,7 @@ ${summaries}`
|
|
|
26069
26674
|
};
|
|
26070
26675
|
}
|
|
26071
26676
|
),
|
|
26072
|
-
|
|
26677
|
+
tool25(
|
|
26073
26678
|
"get_persona_guidance",
|
|
26074
26679
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
26075
26680
|
{
|
|
@@ -27755,8 +28360,8 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
27755
28360
|
}
|
|
27756
28361
|
function formatPlanSummary(plan) {
|
|
27757
28362
|
const lines = [];
|
|
27758
|
-
const
|
|
27759
|
-
lines.push(`Detected: ${
|
|
28363
|
+
const typeLabel5 = classificationLabel(plan.classification.type);
|
|
28364
|
+
lines.push(`Detected: ${typeLabel5}`);
|
|
27760
28365
|
lines.push(`Source: ${plan.classification.inputPath}`);
|
|
27761
28366
|
lines.push("");
|
|
27762
28367
|
const imports = plan.items.filter((i) => i.action === "import");
|
|
@@ -28989,12 +29594,76 @@ async function generateClaudeMdCommand(options) {
|
|
|
28989
29594
|
console.log(chalk18.green("Created .marvin/CLAUDE.md"));
|
|
28990
29595
|
}
|
|
28991
29596
|
|
|
29597
|
+
// src/cli/commands/doctor.ts
|
|
29598
|
+
import chalk19 from "chalk";
|
|
29599
|
+
var SEVERITY_ICONS = {
|
|
29600
|
+
error: chalk19.red("x"),
|
|
29601
|
+
warning: chalk19.yellow("!"),
|
|
29602
|
+
info: chalk19.blue("i")
|
|
29603
|
+
};
|
|
29604
|
+
async function doctorCommand(options) {
|
|
29605
|
+
const project = loadProject();
|
|
29606
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
29607
|
+
const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
|
|
29608
|
+
const allSkills = loadAllSkills(project.marvinDir);
|
|
29609
|
+
const allSkillIds = [...allSkills.keys()];
|
|
29610
|
+
const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
|
|
29611
|
+
const store = new DocumentStore(project.marvinDir, [
|
|
29612
|
+
...pluginRegistrations,
|
|
29613
|
+
...skillRegistrations
|
|
29614
|
+
]);
|
|
29615
|
+
const report = options.fix ? runDoctorFix(store, options.rule) : runDoctorScan(store, options.rule);
|
|
29616
|
+
printReport(report, !!options.fix);
|
|
29617
|
+
}
|
|
29618
|
+
function printReport(report, didFix) {
|
|
29619
|
+
console.log(chalk19.bold(`
|
|
29620
|
+
Artifact Doctor
|
|
29621
|
+
`));
|
|
29622
|
+
console.log(`Scanned ${report.totalDocuments} documents
|
|
29623
|
+
`);
|
|
29624
|
+
if (report.issues.length === 0) {
|
|
29625
|
+
console.log(chalk19.green("No issues found. All documents are healthy.\n"));
|
|
29626
|
+
return;
|
|
29627
|
+
}
|
|
29628
|
+
const byDoc = /* @__PURE__ */ new Map();
|
|
29629
|
+
for (const issue2 of report.issues) {
|
|
29630
|
+
const key = issue2.documentId;
|
|
29631
|
+
if (!byDoc.has(key)) byDoc.set(key, []);
|
|
29632
|
+
byDoc.get(key).push(issue2);
|
|
29633
|
+
}
|
|
29634
|
+
for (const [docId, issues] of byDoc) {
|
|
29635
|
+
const first = issues[0];
|
|
29636
|
+
console.log(chalk19.cyan(docId) + chalk19.dim(` (${first.documentType})`));
|
|
29637
|
+
for (const issue2 of issues) {
|
|
29638
|
+
const icon = SEVERITY_ICONS[issue2.severity] ?? " ";
|
|
29639
|
+
const fixLabel = issue2.fixable ? chalk19.dim(" [fixable]") : "";
|
|
29640
|
+
console.log(` ${icon} ${issue2.message}${fixLabel}`);
|
|
29641
|
+
}
|
|
29642
|
+
console.log();
|
|
29643
|
+
}
|
|
29644
|
+
console.log(chalk19.underline("Summary"));
|
|
29645
|
+
console.log(` Total issues: ${report.summary.totalIssues}`);
|
|
29646
|
+
console.log(` Fixable: ${report.summary.fixableIssues}`);
|
|
29647
|
+
if (didFix) {
|
|
29648
|
+
console.log(chalk19.green(` Fixed: ${report.summary.fixedIssues}`));
|
|
29649
|
+
}
|
|
29650
|
+
const { bySeverity } = report.summary;
|
|
29651
|
+
if (bySeverity.error > 0) console.log(chalk19.red(` Errors: ${bySeverity.error}`));
|
|
29652
|
+
if (bySeverity.warning > 0) console.log(chalk19.yellow(` Warnings: ${bySeverity.warning}`));
|
|
29653
|
+
if (bySeverity.info > 0) console.log(chalk19.blue(` Info: ${bySeverity.info}`));
|
|
29654
|
+
if (!didFix && report.summary.fixableIssues > 0) {
|
|
29655
|
+
console.log(chalk19.dim(`
|
|
29656
|
+
Run "marvin doctor --fix" to auto-repair fixable issues.`));
|
|
29657
|
+
}
|
|
29658
|
+
console.log();
|
|
29659
|
+
}
|
|
29660
|
+
|
|
28992
29661
|
// src/cli/program.ts
|
|
28993
29662
|
function createProgram() {
|
|
28994
29663
|
const program = new Command();
|
|
28995
29664
|
program.name("marvin").description(
|
|
28996
29665
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
28997
|
-
).version("0.5.
|
|
29666
|
+
).version("0.5.2");
|
|
28998
29667
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
28999
29668
|
await initCommand();
|
|
29000
29669
|
});
|
|
@@ -29083,6 +29752,9 @@ function createProgram() {
|
|
|
29083
29752
|
program.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
|
|
29084
29753
|
await webCommand(options);
|
|
29085
29754
|
});
|
|
29755
|
+
program.command("doctor").description("Scan project documents for structural issues and optionally auto-repair them").option("--fix", "Auto-repair fixable issues").option("--rule <rule>", "Run only a specific rule").action(async (options) => {
|
|
29756
|
+
await doctorCommand(options);
|
|
29757
|
+
});
|
|
29086
29758
|
const generateCmd = program.command("generate").description("Generate project files");
|
|
29087
29759
|
generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
|
|
29088
29760
|
await generateClaudeMdCommand(options);
|