mrvn-cli 0.5.0 → 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 +897 -205
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +827 -202
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +897 -205
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -6188,13 +6188,13 @@ var error16 = () => {
|
|
|
6188
6188
|
// no unit
|
|
6189
6189
|
};
|
|
6190
6190
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6191
|
-
const
|
|
6191
|
+
const typeLabel5 = (t) => {
|
|
6192
6192
|
const e = typeEntry(t);
|
|
6193
6193
|
if (e)
|
|
6194
6194
|
return e.label;
|
|
6195
6195
|
return t ?? TypeNames.unknown.label;
|
|
6196
6196
|
};
|
|
6197
|
-
const withDefinite = (t) => `\u05D4${
|
|
6197
|
+
const withDefinite = (t) => `\u05D4${typeLabel5(t)}`;
|
|
6198
6198
|
const verbFor = (t) => {
|
|
6199
6199
|
const e = typeEntry(t);
|
|
6200
6200
|
const gender = e?.gender ?? "m";
|
|
@@ -6244,7 +6244,7 @@ var error16 = () => {
|
|
|
6244
6244
|
switch (issue2.code) {
|
|
6245
6245
|
case "invalid_type": {
|
|
6246
6246
|
const expectedKey = issue2.expected;
|
|
6247
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6247
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel5(expectedKey);
|
|
6248
6248
|
const receivedType = parsedType(issue2.input);
|
|
6249
6249
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6250
6250
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -13923,6 +13923,26 @@ config(en_default());
|
|
|
13923
13923
|
|
|
13924
13924
|
// src/plugins/builtin/tools/meetings.ts
|
|
13925
13925
|
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
13926
|
+
|
|
13927
|
+
// src/personas/owner.ts
|
|
13928
|
+
var OWNER_SHORT = ["po", "dm", "tl"];
|
|
13929
|
+
var OWNER_LONG = ["product-owner", "delivery-manager", "tech-lead"];
|
|
13930
|
+
var VALID_OWNERS = [...OWNER_SHORT, ...OWNER_LONG];
|
|
13931
|
+
var LONG_TO_SHORT = {
|
|
13932
|
+
"product-owner": "po",
|
|
13933
|
+
"delivery-manager": "dm",
|
|
13934
|
+
"tech-lead": "tl"
|
|
13935
|
+
};
|
|
13936
|
+
var ownerSchema = external_exports.enum(VALID_OWNERS);
|
|
13937
|
+
function normalizeOwner(owner) {
|
|
13938
|
+
if (owner === void 0) return void 0;
|
|
13939
|
+
return LONG_TO_SHORT[owner] ?? owner;
|
|
13940
|
+
}
|
|
13941
|
+
function isValidOwner(value) {
|
|
13942
|
+
return VALID_OWNERS.includes(value);
|
|
13943
|
+
}
|
|
13944
|
+
|
|
13945
|
+
// src/plugins/builtin/tools/meetings.ts
|
|
13926
13946
|
function createMeetingTools(store) {
|
|
13927
13947
|
return [
|
|
13928
13948
|
tool(
|
|
@@ -13978,7 +13998,8 @@ function createMeetingTools(store) {
|
|
|
13978
13998
|
title: external_exports.string().describe("Title of the meeting"),
|
|
13979
13999
|
content: external_exports.string().describe("Meeting agenda, notes, or minutes"),
|
|
13980
14000
|
status: external_exports.string().optional().describe("Status (default: 'scheduled')"),
|
|
13981
|
-
owner:
|
|
14001
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14002
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
13982
14003
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
13983
14004
|
attendees: external_exports.array(external_exports.string()).optional().describe("List of attendees"),
|
|
13984
14005
|
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.")
|
|
@@ -13988,7 +14009,8 @@ function createMeetingTools(store) {
|
|
|
13988
14009
|
title: args.title,
|
|
13989
14010
|
status: args.status ?? "scheduled"
|
|
13990
14011
|
};
|
|
13991
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
14012
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
14013
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
13992
14014
|
if (args.tags) frontmatter.tags = args.tags;
|
|
13993
14015
|
if (args.attendees) frontmatter.attendees = args.attendees;
|
|
13994
14016
|
frontmatter.date = args.date;
|
|
@@ -14015,10 +14037,13 @@ function createMeetingTools(store) {
|
|
|
14015
14037
|
title: external_exports.string().optional().describe("New title"),
|
|
14016
14038
|
status: external_exports.string().optional().describe("New status"),
|
|
14017
14039
|
content: external_exports.string().optional().describe("New content"),
|
|
14018
|
-
owner:
|
|
14040
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14041
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work")
|
|
14019
14042
|
},
|
|
14020
14043
|
async (args) => {
|
|
14021
|
-
const { id, content, ...updates } = args;
|
|
14044
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
14045
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14046
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14022
14047
|
const doc = store.update(id, updates, content);
|
|
14023
14048
|
return {
|
|
14024
14049
|
content: [
|
|
@@ -14487,7 +14512,7 @@ function propagateProgressFromTask(store, taskId) {
|
|
|
14487
14512
|
store.update(taskId, { progress: 100 });
|
|
14488
14513
|
updated.push(taskId);
|
|
14489
14514
|
}
|
|
14490
|
-
} else {
|
|
14515
|
+
} else if (!task.frontmatter.progressOverride) {
|
|
14491
14516
|
const children = store.list({ type: "contribution" }).filter((d) => d.frontmatter.aboutArtifact === taskId);
|
|
14492
14517
|
if (children.length > 0) {
|
|
14493
14518
|
const avg = children.reduce((sum, c) => sum + getEffectiveProgress(c.frontmatter), 0) / children.length;
|
|
@@ -14518,27 +14543,29 @@ function propagateProgressToAction(store, actionId) {
|
|
|
14518
14543
|
}
|
|
14519
14544
|
return updated;
|
|
14520
14545
|
}
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14546
|
+
if (!action.frontmatter.progressOverride) {
|
|
14547
|
+
const childTasks = store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === actionId);
|
|
14548
|
+
const directContribs = store.list({ type: "contribution" }).filter((d) => d.frontmatter.aboutArtifact === actionId);
|
|
14549
|
+
const hasTasks = childTasks.length > 0;
|
|
14550
|
+
const hasContribs = directContribs.length > 0;
|
|
14551
|
+
let progress;
|
|
14552
|
+
if (hasTasks && hasContribs) {
|
|
14553
|
+
const taskAvg = childTasks.reduce((s, t) => s + getEffectiveProgress(t.frontmatter), 0) / childTasks.length;
|
|
14554
|
+
const contribAvg = directContribs.reduce((s, c) => s + getEffectiveProgress(c.frontmatter), 0) / directContribs.length;
|
|
14555
|
+
progress = Math.round(taskAvg * 0.8 + contribAvg * 0.2);
|
|
14556
|
+
} else if (hasTasks) {
|
|
14557
|
+
progress = Math.round(
|
|
14558
|
+
childTasks.reduce((s, t) => s + getEffectiveProgress(t.frontmatter), 0) / childTasks.length
|
|
14559
|
+
);
|
|
14560
|
+
} else if (hasContribs) {
|
|
14561
|
+
progress = Math.round(
|
|
14562
|
+
directContribs.reduce((s, c) => s + getEffectiveProgress(c.frontmatter), 0) / directContribs.length
|
|
14563
|
+
);
|
|
14564
|
+
}
|
|
14565
|
+
if (progress !== void 0) {
|
|
14566
|
+
store.update(actionId, { progress });
|
|
14567
|
+
updated.push(actionId);
|
|
14568
|
+
}
|
|
14542
14569
|
}
|
|
14543
14570
|
return updated;
|
|
14544
14571
|
}
|
|
@@ -14632,14 +14659,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
14632
14659
|
const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
|
|
14633
14660
|
for (const doc of workItemDocs) {
|
|
14634
14661
|
const about = doc.frontmatter.aboutArtifact;
|
|
14635
|
-
const
|
|
14662
|
+
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
14636
14663
|
const item = {
|
|
14637
14664
|
id: doc.frontmatter.id,
|
|
14638
14665
|
title: doc.frontmatter.title,
|
|
14639
14666
|
type: doc.frontmatter.type,
|
|
14640
14667
|
status: doc.frontmatter.status,
|
|
14641
14668
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
14642
|
-
|
|
14669
|
+
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
14643
14670
|
aboutArtifact: about
|
|
14644
14671
|
};
|
|
14645
14672
|
allItemsById.set(item.id, item);
|
|
@@ -15794,7 +15821,8 @@ function createFeatureTools(store) {
|
|
|
15794
15821
|
title: external_exports.string().describe("Feature title"),
|
|
15795
15822
|
content: external_exports.string().describe("Feature description and requirements"),
|
|
15796
15823
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("Feature status (default: 'draft')"),
|
|
15797
|
-
owner:
|
|
15824
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
15825
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
15798
15826
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Feature priority"),
|
|
15799
15827
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
15800
15828
|
},
|
|
@@ -15803,7 +15831,8 @@ function createFeatureTools(store) {
|
|
|
15803
15831
|
title: args.title,
|
|
15804
15832
|
status: args.status ?? "draft"
|
|
15805
15833
|
};
|
|
15806
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
15834
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
15835
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
15807
15836
|
if (args.priority) frontmatter.priority = args.priority;
|
|
15808
15837
|
if (args.tags) frontmatter.tags = args.tags;
|
|
15809
15838
|
const doc = store.create("feature", frontmatter, args.content);
|
|
@@ -15825,12 +15854,15 @@ function createFeatureTools(store) {
|
|
|
15825
15854
|
title: external_exports.string().optional().describe("New title"),
|
|
15826
15855
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
|
|
15827
15856
|
content: external_exports.string().optional().describe("New content"),
|
|
15828
|
-
owner:
|
|
15857
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
15858
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
15829
15859
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
15830
15860
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
15831
15861
|
},
|
|
15832
15862
|
async (args) => {
|
|
15833
|
-
const { id, content, ...updates } = args;
|
|
15863
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
15864
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
15865
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
15834
15866
|
const doc = store.update(id, updates, content);
|
|
15835
15867
|
return {
|
|
15836
15868
|
content: [
|
|
@@ -15928,7 +15960,8 @@ function createEpicTools(store) {
|
|
|
15928
15960
|
content: external_exports.string().describe("Epic description and scope"),
|
|
15929
15961
|
linkedFeature: linkedFeatureArray.describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
|
|
15930
15962
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
|
|
15931
|
-
owner:
|
|
15963
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
15964
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
15932
15965
|
targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
|
|
15933
15966
|
estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g. '2 weeks', '5 story points')"),
|
|
15934
15967
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
|
|
@@ -15977,7 +16010,8 @@ function createEpicTools(store) {
|
|
|
15977
16010
|
linkedFeature: linkedFeatures,
|
|
15978
16011
|
tags: [...generateFeatureTags(linkedFeatures), ...args.tags ?? []]
|
|
15979
16012
|
};
|
|
15980
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
16013
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
16014
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
15981
16015
|
if (args.targetDate) frontmatter.targetDate = args.targetDate;
|
|
15982
16016
|
if (args.estimatedEffort) frontmatter.estimatedEffort = args.estimatedEffort;
|
|
15983
16017
|
const doc = store.create("epic", frontmatter, args.content);
|
|
@@ -15999,14 +16033,17 @@ function createEpicTools(store) {
|
|
|
15999
16033
|
title: external_exports.string().optional().describe("New title"),
|
|
16000
16034
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("New status"),
|
|
16001
16035
|
content: external_exports.string().optional().describe("New content"),
|
|
16002
|
-
owner:
|
|
16036
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
16037
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
16003
16038
|
targetDate: external_exports.string().optional().describe("New target date"),
|
|
16004
16039
|
estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
|
|
16005
16040
|
linkedFeature: linkedFeatureArray.optional().describe("New linked feature ID(s)"),
|
|
16006
16041
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
16007
16042
|
},
|
|
16008
16043
|
async (args) => {
|
|
16009
|
-
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, ...updates } = args;
|
|
16044
|
+
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, owner, assignee, ...updates } = args;
|
|
16045
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
16046
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
16010
16047
|
if (rawLinkedFeature !== void 0) {
|
|
16011
16048
|
const linkedFeatures = normalizeLinkedFeatures(rawLinkedFeature);
|
|
16012
16049
|
for (const featureId of linkedFeatures) {
|
|
@@ -16133,7 +16170,7 @@ function createContributionTools(store) {
|
|
|
16133
16170
|
aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
|
|
16134
16171
|
status: external_exports.string().optional().describe("Status (default: 'done')"),
|
|
16135
16172
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
16136
|
-
|
|
16173
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag."),
|
|
16137
16174
|
parentProgress: external_exports.number().optional().describe("Set progress (0-100) on the parent artifact (e.g. task or action). Propagates up the hierarchy.")
|
|
16138
16175
|
},
|
|
16139
16176
|
async (args) => {
|
|
@@ -16145,7 +16182,7 @@ function createContributionTools(store) {
|
|
|
16145
16182
|
};
|
|
16146
16183
|
frontmatter.aboutArtifact = args.aboutArtifact;
|
|
16147
16184
|
const tags = [...args.tags ?? []];
|
|
16148
|
-
if (args.
|
|
16185
|
+
if (args.workFocus) tags.push(`focus:${args.workFocus}`);
|
|
16149
16186
|
if (tags.length > 0) frontmatter.tags = tags;
|
|
16150
16187
|
const doc = store.create("contribution", frontmatter, args.content);
|
|
16151
16188
|
const progressParts = [];
|
|
@@ -16154,7 +16191,7 @@ function createContributionTools(store) {
|
|
|
16154
16191
|
if (parent) {
|
|
16155
16192
|
if (typeof args.parentProgress === "number") {
|
|
16156
16193
|
const clamped = Math.max(0, Math.min(100, Math.round(args.parentProgress)));
|
|
16157
|
-
store.update(args.aboutArtifact, { progress: clamped });
|
|
16194
|
+
store.update(args.aboutArtifact, { progress: clamped, progressOverride: true });
|
|
16158
16195
|
progressParts.push(`${args.aboutArtifact} \u2192 ${clamped}%`);
|
|
16159
16196
|
if (parent.frontmatter.type === "task") {
|
|
16160
16197
|
const grandparent = parent.frontmatter.aboutArtifact;
|
|
@@ -16201,15 +16238,15 @@ function createContributionTools(store) {
|
|
|
16201
16238
|
title: external_exports.string().optional().describe("New title"),
|
|
16202
16239
|
status: external_exports.string().optional().describe("New status"),
|
|
16203
16240
|
content: external_exports.string().optional().describe("New content"),
|
|
16204
|
-
|
|
16241
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag.")
|
|
16205
16242
|
},
|
|
16206
16243
|
async (args) => {
|
|
16207
|
-
const { id, content,
|
|
16208
|
-
if (
|
|
16244
|
+
const { id, content, workFocus, ...updates } = args;
|
|
16245
|
+
if (workFocus !== void 0) {
|
|
16209
16246
|
const existing = store.get(id);
|
|
16210
16247
|
const existingTags = existing?.frontmatter.tags ?? [];
|
|
16211
|
-
const filtered = existingTags.filter((t) => !t.startsWith("
|
|
16212
|
-
filtered.push(`
|
|
16248
|
+
const filtered = existingTags.filter((t) => !t.startsWith("focus:"));
|
|
16249
|
+
filtered.push(`focus:${workFocus}`);
|
|
16213
16250
|
updates.tags = filtered;
|
|
16214
16251
|
}
|
|
16215
16252
|
const oldDoc = store.get(id);
|
|
@@ -16692,7 +16729,7 @@ function createTaskTools(store) {
|
|
|
16692
16729
|
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
|
|
16693
16730
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
|
|
16694
16731
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags"),
|
|
16695
|
-
|
|
16732
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
|
|
16696
16733
|
},
|
|
16697
16734
|
async (args) => {
|
|
16698
16735
|
const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
|
|
@@ -16706,7 +16743,7 @@ function createTaskTools(store) {
|
|
|
16706
16743
|
}
|
|
16707
16744
|
}
|
|
16708
16745
|
const baseTags = [...generateEpicTags(linkedEpics), ...args.tags ?? []];
|
|
16709
|
-
if (args.
|
|
16746
|
+
if (args.workFocus) baseTags.push(`focus:${args.workFocus}`);
|
|
16710
16747
|
const frontmatter = {
|
|
16711
16748
|
title: args.title,
|
|
16712
16749
|
status: args.status ?? "backlog",
|
|
@@ -16747,11 +16784,11 @@ function createTaskTools(store) {
|
|
|
16747
16784
|
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
|
|
16748
16785
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
16749
16786
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
|
|
16750
|
-
|
|
16787
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
16751
16788
|
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions.")
|
|
16752
16789
|
},
|
|
16753
16790
|
async (args) => {
|
|
16754
|
-
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags,
|
|
16791
|
+
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workFocus, progress, ...updates } = args;
|
|
16755
16792
|
const warnings = [];
|
|
16756
16793
|
if (rawLinkedEpic !== void 0) {
|
|
16757
16794
|
const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
|
|
@@ -16772,14 +16809,15 @@ function createTaskTools(store) {
|
|
|
16772
16809
|
} else if (userTags !== void 0) {
|
|
16773
16810
|
updates.tags = userTags;
|
|
16774
16811
|
}
|
|
16775
|
-
if (
|
|
16812
|
+
if (workFocus !== void 0) {
|
|
16776
16813
|
const currentTags = updates.tags ?? store.get(id)?.frontmatter.tags ?? [];
|
|
16777
|
-
const filtered = currentTags.filter((t) => !t.startsWith("
|
|
16778
|
-
filtered.push(`
|
|
16814
|
+
const filtered = currentTags.filter((t) => !t.startsWith("focus:"));
|
|
16815
|
+
filtered.push(`focus:${workFocus}`);
|
|
16779
16816
|
updates.tags = filtered;
|
|
16780
16817
|
}
|
|
16781
16818
|
if (typeof progress === "number") {
|
|
16782
16819
|
updates.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
16820
|
+
updates.progressOverride = true;
|
|
16783
16821
|
}
|
|
16784
16822
|
const doc = store.update(id, updates, content);
|
|
16785
16823
|
if (args.status !== void 0 || typeof progress === "number") {
|
|
@@ -18632,7 +18670,8 @@ function createDecisionTools(store) {
|
|
|
18632
18670
|
title: external_exports.string().describe("Title of the decision"),
|
|
18633
18671
|
content: external_exports.string().describe("Decision description, context, and rationale"),
|
|
18634
18672
|
status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("Status (default: 'open')"),
|
|
18635
|
-
owner:
|
|
18673
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
18674
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
18636
18675
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
18637
18676
|
},
|
|
18638
18677
|
async (args) => {
|
|
@@ -18641,7 +18680,8 @@ function createDecisionTools(store) {
|
|
|
18641
18680
|
{
|
|
18642
18681
|
title: args.title,
|
|
18643
18682
|
status: args.status,
|
|
18644
|
-
owner: args.owner,
|
|
18683
|
+
owner: normalizeOwner(args.owner),
|
|
18684
|
+
assignee: args.assignee,
|
|
18645
18685
|
tags: args.tags
|
|
18646
18686
|
},
|
|
18647
18687
|
args.content
|
|
@@ -18664,11 +18704,14 @@ function createDecisionTools(store) {
|
|
|
18664
18704
|
title: external_exports.string().optional().describe("New title"),
|
|
18665
18705
|
status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("New status"),
|
|
18666
18706
|
content: external_exports.string().optional().describe("New content"),
|
|
18667
|
-
owner:
|
|
18707
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
18708
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
18668
18709
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
18669
18710
|
},
|
|
18670
18711
|
async (args) => {
|
|
18671
|
-
const { id, content, ...updates } = args;
|
|
18712
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
18713
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
18714
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
18672
18715
|
const doc = store.update(id, updates, content);
|
|
18673
18716
|
return {
|
|
18674
18717
|
content: [
|
|
@@ -18766,12 +18809,13 @@ function createActionTools(store) {
|
|
|
18766
18809
|
title: external_exports.string().describe("Title of the action item"),
|
|
18767
18810
|
content: external_exports.string().describe("Description of what needs to be done"),
|
|
18768
18811
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
18769
|
-
owner:
|
|
18812
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
18813
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
18770
18814
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
18771
18815
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
18772
18816
|
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
18773
18817
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags."),
|
|
18774
|
-
|
|
18818
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
|
|
18775
18819
|
},
|
|
18776
18820
|
async (args) => {
|
|
18777
18821
|
const tags = [...args.tags ?? []];
|
|
@@ -18781,15 +18825,16 @@ function createActionTools(store) {
|
|
|
18781
18825
|
if (!tags.includes(tag)) tags.push(tag);
|
|
18782
18826
|
}
|
|
18783
18827
|
}
|
|
18784
|
-
if (args.
|
|
18785
|
-
tags.push(`
|
|
18828
|
+
if (args.workFocus) {
|
|
18829
|
+
tags.push(`focus:${args.workFocus}`);
|
|
18786
18830
|
}
|
|
18787
18831
|
const doc = store.create(
|
|
18788
18832
|
"action",
|
|
18789
18833
|
{
|
|
18790
18834
|
title: args.title,
|
|
18791
18835
|
status: args.status,
|
|
18792
|
-
owner: args.owner,
|
|
18836
|
+
owner: normalizeOwner(args.owner),
|
|
18837
|
+
assignee: args.assignee,
|
|
18793
18838
|
priority: args.priority,
|
|
18794
18839
|
tags: tags.length > 0 ? tags : void 0,
|
|
18795
18840
|
dueDate: args.dueDate
|
|
@@ -18817,16 +18862,19 @@ function createActionTools(store) {
|
|
|
18817
18862
|
title: external_exports.string().optional().describe("New title"),
|
|
18818
18863
|
status: external_exports.string().optional().describe("New status"),
|
|
18819
18864
|
content: external_exports.string().optional().describe("New content"),
|
|
18820
|
-
owner:
|
|
18865
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
18866
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
18821
18867
|
priority: external_exports.string().optional().describe("New priority"),
|
|
18822
18868
|
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
18823
18869
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
18824
18870
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
|
|
18825
|
-
|
|
18871
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
18826
18872
|
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100).")
|
|
18827
18873
|
},
|
|
18828
18874
|
async (args) => {
|
|
18829
|
-
const { id, content, sprints, tags,
|
|
18875
|
+
const { id, content, sprints, tags, workFocus, progress, owner, assignee, ...updates } = args;
|
|
18876
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
18877
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
18830
18878
|
if (tags !== void 0) {
|
|
18831
18879
|
const merged = [...tags];
|
|
18832
18880
|
if (sprints) {
|
|
@@ -18835,14 +18883,14 @@ function createActionTools(store) {
|
|
|
18835
18883
|
if (!merged.includes(tag)) merged.push(tag);
|
|
18836
18884
|
}
|
|
18837
18885
|
}
|
|
18838
|
-
if (
|
|
18839
|
-
const filtered = merged.filter((t) => !t.startsWith("
|
|
18840
|
-
filtered.push(`
|
|
18886
|
+
if (workFocus !== void 0) {
|
|
18887
|
+
const filtered = merged.filter((t) => !t.startsWith("focus:"));
|
|
18888
|
+
filtered.push(`focus:${workFocus}`);
|
|
18841
18889
|
updates.tags = filtered;
|
|
18842
18890
|
} else {
|
|
18843
18891
|
updates.tags = merged;
|
|
18844
18892
|
}
|
|
18845
|
-
} else if (sprints !== void 0 ||
|
|
18893
|
+
} else if (sprints !== void 0 || workFocus !== void 0) {
|
|
18846
18894
|
const existing = store.get(id);
|
|
18847
18895
|
if (!existing) {
|
|
18848
18896
|
return {
|
|
@@ -18855,14 +18903,15 @@ function createActionTools(store) {
|
|
|
18855
18903
|
existingTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
18856
18904
|
existingTags.push(...sprints.map((s) => `sprint:${s}`));
|
|
18857
18905
|
}
|
|
18858
|
-
if (
|
|
18859
|
-
existingTags = existingTags.filter((t) => !t.startsWith("
|
|
18860
|
-
existingTags.push(`
|
|
18906
|
+
if (workFocus !== void 0) {
|
|
18907
|
+
existingTags = existingTags.filter((t) => !t.startsWith("focus:"));
|
|
18908
|
+
existingTags.push(`focus:${workFocus}`);
|
|
18861
18909
|
}
|
|
18862
18910
|
updates.tags = existingTags;
|
|
18863
18911
|
}
|
|
18864
18912
|
if (typeof progress === "number") {
|
|
18865
18913
|
updates.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
18914
|
+
updates.progressOverride = true;
|
|
18866
18915
|
}
|
|
18867
18916
|
const doc = store.update(id, updates, content);
|
|
18868
18917
|
if (args.status !== void 0 || typeof progress === "number") {
|
|
@@ -18969,7 +19018,8 @@ function createQuestionTools(store) {
|
|
|
18969
19018
|
title: external_exports.string().describe("The question being asked"),
|
|
18970
19019
|
content: external_exports.string().describe("Context and details about the question"),
|
|
18971
19020
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
18972
|
-
owner:
|
|
19021
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
19022
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
18973
19023
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
18974
19024
|
},
|
|
18975
19025
|
async (args) => {
|
|
@@ -18978,7 +19028,8 @@ function createQuestionTools(store) {
|
|
|
18978
19028
|
{
|
|
18979
19029
|
title: args.title,
|
|
18980
19030
|
status: args.status,
|
|
18981
|
-
owner: args.owner,
|
|
19031
|
+
owner: normalizeOwner(args.owner),
|
|
19032
|
+
assignee: args.assignee,
|
|
18982
19033
|
tags: args.tags
|
|
18983
19034
|
},
|
|
18984
19035
|
args.content
|
|
@@ -19001,11 +19052,14 @@ function createQuestionTools(store) {
|
|
|
19001
19052
|
title: external_exports.string().optional().describe("New title"),
|
|
19002
19053
|
status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
|
|
19003
19054
|
content: external_exports.string().optional().describe("Updated content / answer"),
|
|
19004
|
-
owner:
|
|
19055
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
19056
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
19005
19057
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
19006
19058
|
},
|
|
19007
19059
|
async (args) => {
|
|
19008
|
-
const { id, content, ...updates } = args;
|
|
19060
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
19061
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
19062
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
19009
19063
|
const doc = store.update(id, updates, content);
|
|
19010
19064
|
return {
|
|
19011
19065
|
content: [
|
|
@@ -19030,18 +19084,20 @@ function createDocumentTools(store) {
|
|
|
19030
19084
|
{
|
|
19031
19085
|
type: external_exports.string().optional().describe(`Filter by document type (registered types: ${store.registeredTypes.join(", ")})`),
|
|
19032
19086
|
status: external_exports.string().optional().describe("Filter by status"),
|
|
19087
|
+
owner: external_exports.string().optional().describe("Filter by persona role owner (po, dm, tl)"),
|
|
19033
19088
|
tag: external_exports.string().optional().describe("Filter by tag"),
|
|
19034
|
-
|
|
19089
|
+
workFocus: external_exports.string().optional().describe("Filter by work focus name (matches focus:<value> tag)")
|
|
19035
19090
|
},
|
|
19036
19091
|
async (args) => {
|
|
19037
19092
|
let docs = store.list({
|
|
19038
19093
|
type: args.type,
|
|
19039
19094
|
status: args.status,
|
|
19095
|
+
owner: args.owner,
|
|
19040
19096
|
tag: args.tag
|
|
19041
19097
|
});
|
|
19042
|
-
if (args.
|
|
19043
|
-
const
|
|
19044
|
-
docs = docs.filter((d) => d.frontmatter.tags?.includes(
|
|
19098
|
+
if (args.workFocus) {
|
|
19099
|
+
const focusTag = `focus:${args.workFocus}`;
|
|
19100
|
+
docs = docs.filter((d) => d.frontmatter.tags?.includes(focusTag));
|
|
19045
19101
|
}
|
|
19046
19102
|
const summary = docs.map((d) => ({
|
|
19047
19103
|
id: d.frontmatter.id,
|
|
@@ -19284,7 +19340,7 @@ function formatDate(iso) {
|
|
|
19284
19340
|
if (!iso) return "";
|
|
19285
19341
|
return iso.slice(0, 10);
|
|
19286
19342
|
}
|
|
19287
|
-
function
|
|
19343
|
+
function typeLabel2(type) {
|
|
19288
19344
|
return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
19289
19345
|
}
|
|
19290
19346
|
function renderMarkdown(md) {
|
|
@@ -21039,12 +21095,45 @@ tr:hover td {
|
|
|
21039
21095
|
font-weight: 700;
|
|
21040
21096
|
color: var(--text);
|
|
21041
21097
|
}
|
|
21098
|
+
|
|
21099
|
+
/* Focus-grouped work items */
|
|
21100
|
+
.focus-row td:first-child {
|
|
21101
|
+
border-left: 3px solid var(--focus-color, var(--border));
|
|
21102
|
+
}
|
|
21103
|
+
|
|
21104
|
+
.focus-group-header td {
|
|
21105
|
+
background: var(--bg-hover);
|
|
21106
|
+
border-left: 3px solid var(--focus-color, var(--border));
|
|
21107
|
+
padding-top: 0.5rem;
|
|
21108
|
+
padding-bottom: 0.5rem;
|
|
21109
|
+
border-bottom: 1px solid var(--border);
|
|
21110
|
+
}
|
|
21111
|
+
|
|
21112
|
+
.focus-group-header td:first-child {
|
|
21113
|
+
border-left-width: 3px;
|
|
21114
|
+
}
|
|
21115
|
+
|
|
21116
|
+
.focus-group-name {
|
|
21117
|
+
font-weight: 600;
|
|
21118
|
+
font-size: 0.8rem;
|
|
21119
|
+
color: var(--text);
|
|
21120
|
+
margin-right: 0.75rem;
|
|
21121
|
+
}
|
|
21122
|
+
|
|
21123
|
+
.focus-group-stats {
|
|
21124
|
+
font-size: 0.75rem;
|
|
21125
|
+
color: var(--text-dim);
|
|
21126
|
+
}
|
|
21127
|
+
|
|
21128
|
+
.focus-group-progress {
|
|
21129
|
+
width: 96px;
|
|
21130
|
+
}
|
|
21042
21131
|
`;
|
|
21043
21132
|
}
|
|
21044
21133
|
|
|
21045
21134
|
// src/web/templates/pages/documents.ts
|
|
21046
21135
|
function documentsPage(data) {
|
|
21047
|
-
const label =
|
|
21136
|
+
const label = typeLabel2(data.type);
|
|
21048
21137
|
const statusOptions = data.statuses.map(
|
|
21049
21138
|
(s) => `<option value="${escapeHtml(s)}"${data.filterStatus === s ? " selected" : ""}>${escapeHtml(s)}</option>`
|
|
21050
21139
|
).join("");
|
|
@@ -21118,7 +21207,7 @@ function documentsPage(data) {
|
|
|
21118
21207
|
// src/web/templates/pages/document-detail.ts
|
|
21119
21208
|
function documentDetailPage(doc) {
|
|
21120
21209
|
const fm = doc.frontmatter;
|
|
21121
|
-
const label =
|
|
21210
|
+
const label = typeLabel2(fm.type);
|
|
21122
21211
|
const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
|
|
21123
21212
|
const entries = Object.entries(fm).filter(
|
|
21124
21213
|
([key]) => !skipKeys.has(key) && fm[key] != null
|
|
@@ -21679,7 +21768,7 @@ function poDashboardPage(ctx) {
|
|
|
21679
21768
|
<tr>
|
|
21680
21769
|
<td><a href="/docs/${d.frontmatter.type}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
21681
21770
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
21682
|
-
<td>${escapeHtml(
|
|
21771
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
21683
21772
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
21684
21773
|
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
21685
21774
|
</tr>`).join("")}
|
|
@@ -22187,7 +22276,7 @@ function poDeliveryPage(ctx) {
|
|
|
22187
22276
|
<tr>
|
|
22188
22277
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22189
22278
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22190
|
-
<td>${escapeHtml(
|
|
22279
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
22191
22280
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
22192
22281
|
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
22193
22282
|
</tr>`).join("")}
|
|
@@ -22505,15 +22594,15 @@ function sprintSummaryPage(data, cached2) {
|
|
|
22505
22594
|
</div>`,
|
|
22506
22595
|
{ titleTag: "h3" }
|
|
22507
22596
|
) : "";
|
|
22508
|
-
const
|
|
22509
|
-
"
|
|
22510
|
-
"
|
|
22511
|
-
"
|
|
22512
|
-
"
|
|
22513
|
-
"
|
|
22514
|
-
"
|
|
22515
|
-
"
|
|
22516
|
-
"
|
|
22597
|
+
const FOCUS_BORDER_PALETTE = [
|
|
22598
|
+
"hsl(220, 60%, 55%)",
|
|
22599
|
+
"hsl(160, 50%, 45%)",
|
|
22600
|
+
"hsl(280, 45%, 55%)",
|
|
22601
|
+
"hsl(30, 65%, 55%)",
|
|
22602
|
+
"hsl(340, 50%, 55%)",
|
|
22603
|
+
"hsl(190, 50%, 45%)",
|
|
22604
|
+
"hsl(60, 50%, 50%)",
|
|
22605
|
+
"hsl(120, 40%, 45%)"
|
|
22517
22606
|
];
|
|
22518
22607
|
function hashString(s) {
|
|
22519
22608
|
let h = 0;
|
|
@@ -22522,68 +22611,92 @@ function sprintSummaryPage(data, cached2) {
|
|
|
22522
22611
|
}
|
|
22523
22612
|
return Math.abs(h);
|
|
22524
22613
|
}
|
|
22525
|
-
|
|
22526
|
-
|
|
22527
|
-
|
|
22528
|
-
|
|
22529
|
-
|
|
22530
|
-
|
|
22614
|
+
const focusGroups = /* @__PURE__ */ new Map();
|
|
22615
|
+
for (const item of data.workItems.items) {
|
|
22616
|
+
const focus = item.workFocus ?? "Unassigned";
|
|
22617
|
+
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
22618
|
+
focusGroups.get(focus).push(item);
|
|
22619
|
+
}
|
|
22620
|
+
const focusColorMap = /* @__PURE__ */ new Map();
|
|
22621
|
+
for (const name of focusGroups.keys()) {
|
|
22622
|
+
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
22623
|
+
}
|
|
22624
|
+
function countFocusStats(items) {
|
|
22625
|
+
let total = 0;
|
|
22626
|
+
let done = 0;
|
|
22627
|
+
let inProgress = 0;
|
|
22628
|
+
function walk(list) {
|
|
22629
|
+
for (const w of list) {
|
|
22630
|
+
if (w.type !== "contribution") {
|
|
22631
|
+
total++;
|
|
22632
|
+
const s = w.status.toLowerCase();
|
|
22633
|
+
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
22634
|
+
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
22635
|
+
}
|
|
22636
|
+
if (w.children) walk(w.children);
|
|
22531
22637
|
}
|
|
22532
22638
|
}
|
|
22533
|
-
|
|
22639
|
+
walk(items);
|
|
22640
|
+
return { total, done, inProgress };
|
|
22534
22641
|
}
|
|
22535
|
-
|
|
22536
|
-
const streamColorMap = /* @__PURE__ */ new Map();
|
|
22537
|
-
for (const name of uniqueStreams) {
|
|
22538
|
-
streamColorMap.set(name, STREAM_PALETTE[hashString(name) % STREAM_PALETTE.length]);
|
|
22539
|
-
}
|
|
22540
|
-
const streamStyleRules = [...streamColorMap.entries()].map(([name, color]) => `tr[data-stream="${escapeHtml(name)}"] td { background: ${color}; }`).join("\n");
|
|
22541
|
-
const streamStyleBlock = streamStyleRules ? `<style>${streamStyleRules}</style>` : "";
|
|
22542
|
-
function renderItemRows(items, depth = 0) {
|
|
22642
|
+
function renderItemRows(items, borderColor, depth = 0) {
|
|
22543
22643
|
return items.flatMap((w) => {
|
|
22544
22644
|
const isChild = depth > 0;
|
|
22545
22645
|
const isContribution = w.type === "contribution";
|
|
22546
|
-
const classes = [];
|
|
22646
|
+
const classes = ["focus-row"];
|
|
22547
22647
|
if (isContribution) classes.push("contribution-row");
|
|
22548
22648
|
else if (isChild) classes.push("child-row");
|
|
22549
|
-
const dataStream = w.workStream ? ` data-stream="${escapeHtml(w.workStream)}"` : "";
|
|
22550
|
-
const rowAttrs = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
|
|
22551
22649
|
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
22552
|
-
const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
|
|
22553
22650
|
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>` : "";
|
|
22554
22651
|
const row = `
|
|
22555
|
-
<tr${
|
|
22652
|
+
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
22556
22653
|
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
22557
22654
|
<td>${escapeHtml(w.title)}</td>
|
|
22558
|
-
<td>${streamCell}</td>
|
|
22559
|
-
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
22560
22655
|
<td>${statusBadge(w.status)}</td>
|
|
22561
22656
|
<td>${progressCell}</td>
|
|
22562
22657
|
</tr>`;
|
|
22563
|
-
const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
|
|
22658
|
+
const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
|
|
22564
22659
|
return [row, ...childRows];
|
|
22565
22660
|
});
|
|
22566
22661
|
}
|
|
22567
|
-
const
|
|
22568
|
-
const
|
|
22569
|
-
|
|
22570
|
-
|
|
22571
|
-
|
|
22572
|
-
|
|
22573
|
-
|
|
22574
|
-
|
|
22662
|
+
const allWorkItemRows = [];
|
|
22663
|
+
for (const [focus, items] of focusGroups) {
|
|
22664
|
+
const color = focusColorMap.get(focus);
|
|
22665
|
+
const stats = countFocusStats(items);
|
|
22666
|
+
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
22667
|
+
const summaryParts = [];
|
|
22668
|
+
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
22669
|
+
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
22670
|
+
const remaining = stats.total - stats.done - stats.inProgress;
|
|
22671
|
+
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
22672
|
+
allWorkItemRows.push(`
|
|
22673
|
+
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
22674
|
+
<td colspan="2">
|
|
22675
|
+
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
22676
|
+
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
22677
|
+
</td>
|
|
22678
|
+
<td colspan="2">
|
|
22679
|
+
<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>
|
|
22680
|
+
</td>
|
|
22681
|
+
</tr>`);
|
|
22682
|
+
allWorkItemRows.push(...renderItemRows(items, color));
|
|
22683
|
+
}
|
|
22684
|
+
const tableHeaders = `<tr>
|
|
22685
|
+
<th>ID</th>
|
|
22686
|
+
<th>Title</th>
|
|
22687
|
+
<th>Status</th>
|
|
22688
|
+
<th>Progress</th>
|
|
22575
22689
|
</tr>`;
|
|
22576
|
-
const workItemsSection =
|
|
22690
|
+
const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
|
|
22577
22691
|
"ss-work-items",
|
|
22578
22692
|
"Work Items",
|
|
22579
|
-
|
|
22580
|
-
<div class="table-wrap">
|
|
22693
|
+
`<div class="table-wrap">
|
|
22581
22694
|
<table id="work-items-table">
|
|
22582
22695
|
<thead>
|
|
22583
|
-
${
|
|
22696
|
+
${tableHeaders}
|
|
22584
22697
|
</thead>
|
|
22585
22698
|
<tbody>
|
|
22586
|
-
${
|
|
22699
|
+
${allWorkItemRows.join("")}
|
|
22587
22700
|
</tbody>
|
|
22588
22701
|
</table>
|
|
22589
22702
|
</div>`,
|
|
@@ -22659,61 +22772,6 @@ function sprintSummaryPage(data, cached2) {
|
|
|
22659
22772
|
</div>
|
|
22660
22773
|
|
|
22661
22774
|
<script>
|
|
22662
|
-
var _sortCol = -1;
|
|
22663
|
-
var _sortAsc = true;
|
|
22664
|
-
|
|
22665
|
-
function sortWorkItems(col) {
|
|
22666
|
-
var table = document.getElementById('work-items-table');
|
|
22667
|
-
if (!table) return;
|
|
22668
|
-
var tbody = table.querySelector('tbody');
|
|
22669
|
-
var allRows = Array.from(tbody.querySelectorAll('tr'));
|
|
22670
|
-
|
|
22671
|
-
// Toggle direction if clicking the same column
|
|
22672
|
-
if (_sortCol === col) {
|
|
22673
|
-
_sortAsc = !_sortAsc;
|
|
22674
|
-
} else {
|
|
22675
|
-
_sortCol = col;
|
|
22676
|
-
_sortAsc = true;
|
|
22677
|
-
}
|
|
22678
|
-
|
|
22679
|
-
// Update sort arrows
|
|
22680
|
-
for (var i = 0; i < 6; i++) {
|
|
22681
|
-
var arrow = document.getElementById('sort-arrow-' + i);
|
|
22682
|
-
if (arrow) arrow.textContent = i === col ? (_sortAsc ? ' \\u25B2' : ' \\u25BC') : '';
|
|
22683
|
-
}
|
|
22684
|
-
|
|
22685
|
-
// Group rows: root rows + their child/contribution rows
|
|
22686
|
-
var groups = [];
|
|
22687
|
-
var current = null;
|
|
22688
|
-
for (var r = 0; r < allRows.length; r++) {
|
|
22689
|
-
var row = allRows[r];
|
|
22690
|
-
var isChild = row.classList.contains('child-row') || row.classList.contains('contribution-row');
|
|
22691
|
-
if (!isChild) {
|
|
22692
|
-
current = { root: row, children: [] };
|
|
22693
|
-
groups.push(current);
|
|
22694
|
-
} else if (current) {
|
|
22695
|
-
current.children.push(row);
|
|
22696
|
-
}
|
|
22697
|
-
}
|
|
22698
|
-
|
|
22699
|
-
// Sort groups by root row text content of target column
|
|
22700
|
-
groups.sort(function(a, b) {
|
|
22701
|
-
var aText = (a.root.children[col] ? a.root.children[col].textContent : '').trim().toLowerCase();
|
|
22702
|
-
var bText = (b.root.children[col] ? b.root.children[col].textContent : '').trim().toLowerCase();
|
|
22703
|
-
if (aText < bText) return _sortAsc ? -1 : 1;
|
|
22704
|
-
if (aText > bText) return _sortAsc ? 1 : -1;
|
|
22705
|
-
return 0;
|
|
22706
|
-
});
|
|
22707
|
-
|
|
22708
|
-
// Re-append rows in sorted order
|
|
22709
|
-
for (var g = 0; g < groups.length; g++) {
|
|
22710
|
-
tbody.appendChild(groups[g].root);
|
|
22711
|
-
for (var c = 0; c < groups[g].children.length; c++) {
|
|
22712
|
-
tbody.appendChild(groups[g].children[c]);
|
|
22713
|
-
}
|
|
22714
|
-
}
|
|
22715
|
-
}
|
|
22716
|
-
|
|
22717
22775
|
async function generateSummary() {
|
|
22718
22776
|
var btn = document.getElementById('generate-btn');
|
|
22719
22777
|
var loading = document.getElementById('summary-loading');
|
|
@@ -22915,7 +22973,7 @@ function dmRisksPage(ctx) {
|
|
|
22915
22973
|
<tr>
|
|
22916
22974
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22917
22975
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22918
|
-
<td>${escapeHtml(
|
|
22976
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
22919
22977
|
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
22920
22978
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
22921
22979
|
</tr>`).join("")}
|
|
@@ -22939,7 +22997,7 @@ function dmRisksPage(ctx) {
|
|
|
22939
22997
|
<tr>
|
|
22940
22998
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22941
22999
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22942
|
-
<td>${escapeHtml(
|
|
23000
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
22943
23001
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
22944
23002
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
22945
23003
|
<td><span class="${ageDays > 30 ? "priority-high" : "priority-medium"}">${ageDays}d</span></td>
|
|
@@ -23450,7 +23508,23 @@ function tlSprintPage(ctx) {
|
|
|
23450
23508
|
</div>`;
|
|
23451
23509
|
}
|
|
23452
23510
|
const techTypes = /* @__PURE__ */ new Set(["epic", "task"]);
|
|
23453
|
-
const techItems =
|
|
23511
|
+
const techItems = [];
|
|
23512
|
+
for (const item of data.workItems.items) {
|
|
23513
|
+
if (techTypes.has(item.type)) {
|
|
23514
|
+
techItems.push(item);
|
|
23515
|
+
} else if (item.children) {
|
|
23516
|
+
const promoteChildren = (children) => {
|
|
23517
|
+
for (const child of children) {
|
|
23518
|
+
if (techTypes.has(child.type)) {
|
|
23519
|
+
techItems.push(child);
|
|
23520
|
+
} else if (child.children) {
|
|
23521
|
+
promoteChildren(child.children);
|
|
23522
|
+
}
|
|
23523
|
+
}
|
|
23524
|
+
};
|
|
23525
|
+
promoteChildren(item.children);
|
|
23526
|
+
}
|
|
23527
|
+
}
|
|
23454
23528
|
const techDone = techItems.filter((w) => DONE_STATUSES11.has(w.status)).length;
|
|
23455
23529
|
const allDocs = ctx.store.list();
|
|
23456
23530
|
const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
@@ -23488,16 +23562,16 @@ function tlSprintPage(ctx) {
|
|
|
23488
23562
|
`<div class="table-wrap">
|
|
23489
23563
|
<table>
|
|
23490
23564
|
<thead>
|
|
23491
|
-
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>
|
|
23565
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
|
|
23492
23566
|
</thead>
|
|
23493
23567
|
<tbody>
|
|
23494
23568
|
${techItems.map((w) => `
|
|
23495
23569
|
<tr>
|
|
23496
23570
|
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
23497
23571
|
<td>${escapeHtml(w.title)}</td>
|
|
23498
|
-
<td>${escapeHtml(
|
|
23572
|
+
<td>${escapeHtml(typeLabel2(w.type))}</td>
|
|
23499
23573
|
<td>${statusBadge(w.status)}</td>
|
|
23500
|
-
<td>${w.
|
|
23574
|
+
<td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
23501
23575
|
</tr>`).join("")}
|
|
23502
23576
|
</tbody>
|
|
23503
23577
|
</table>
|
|
@@ -23517,7 +23591,7 @@ function tlSprintPage(ctx) {
|
|
|
23517
23591
|
<tr>
|
|
23518
23592
|
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23519
23593
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23520
|
-
<td>${escapeHtml(
|
|
23594
|
+
<td>${escapeHtml(typeLabel2(d.frontmatter.type))}</td>
|
|
23521
23595
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23522
23596
|
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
23523
23597
|
</tr>`).join("")}
|
|
@@ -23706,7 +23780,7 @@ function timelinePage(diagrams) {
|
|
|
23706
23780
|
// src/web/templates/pages/board.ts
|
|
23707
23781
|
function boardPage(data, basePath = "/board") {
|
|
23708
23782
|
const typeOptions = data.types.map(
|
|
23709
|
-
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(
|
|
23783
|
+
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(typeLabel2(t))}s</option>`
|
|
23710
23784
|
).join("");
|
|
23711
23785
|
const columns = data.columns.map(
|
|
23712
23786
|
(col) => `
|
|
@@ -23863,7 +23937,7 @@ function upcomingPage(data) {
|
|
|
23863
23937
|
<td><span class="trending-rank">${i + 1}</span></td>
|
|
23864
23938
|
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
23865
23939
|
<td>${escapeHtml(t.title)}</td>
|
|
23866
|
-
<td>${escapeHtml(
|
|
23940
|
+
<td>${escapeHtml(typeLabel2(t.type))}</td>
|
|
23867
23941
|
<td>${statusBadge(t.status)}</td>
|
|
23868
23942
|
<td><span class="trending-score">${t.score}</span></td>
|
|
23869
23943
|
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
@@ -23957,7 +24031,7 @@ function buildPersonaLayoutOpts(persona, activePath, navGroups) {
|
|
|
23957
24031
|
const artifactGroupsHtml = navGroups.map((group) => {
|
|
23958
24032
|
const links = group.types.map((type) => {
|
|
23959
24033
|
const href = `/docs/${type}?persona=${persona}`;
|
|
23960
|
-
return `<a href="${href}" class="${isActive(`/docs/${type}`)}">${
|
|
24034
|
+
return `<a href="${href}" class="${isActive(`/docs/${type}`)}">${typeLabel2(type)}s</a>`;
|
|
23961
24035
|
}).join("\n ");
|
|
23962
24036
|
const groupActive = group.types.some(
|
|
23963
24037
|
(type) => isActive(`/docs/${type}`) !== ""
|
|
@@ -25635,6 +25709,556 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
25635
25709
|
];
|
|
25636
25710
|
}
|
|
25637
25711
|
|
|
25712
|
+
// src/agent/tools/doctor.ts
|
|
25713
|
+
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
25714
|
+
|
|
25715
|
+
// src/doctor/rules/tag-migration.ts
|
|
25716
|
+
var RULE_ID = "tag-migration";
|
|
25717
|
+
var RULE_NAME = "Tag Migration";
|
|
25718
|
+
var tagMigrationRule = {
|
|
25719
|
+
id: RULE_ID,
|
|
25720
|
+
name: RULE_NAME,
|
|
25721
|
+
description: "Detects deprecated stream:* tags and replaces them with focus:*",
|
|
25722
|
+
scan(ctx) {
|
|
25723
|
+
const issues = [];
|
|
25724
|
+
for (const doc of ctx.allDocuments) {
|
|
25725
|
+
const tags = doc.frontmatter.tags;
|
|
25726
|
+
if (!Array.isArray(tags)) continue;
|
|
25727
|
+
const streamTags = tags.filter((t) => t.startsWith("stream:"));
|
|
25728
|
+
for (const tag of streamTags) {
|
|
25729
|
+
issues.push({
|
|
25730
|
+
ruleId: RULE_ID,
|
|
25731
|
+
ruleName: RULE_NAME,
|
|
25732
|
+
documentId: doc.frontmatter.id,
|
|
25733
|
+
filePath: doc.filePath,
|
|
25734
|
+
documentType: doc.frontmatter.type,
|
|
25735
|
+
message: `Deprecated tag "${tag}" should be "${tag.replace("stream:", "focus:")}"`,
|
|
25736
|
+
severity: "warning",
|
|
25737
|
+
fixable: true
|
|
25738
|
+
});
|
|
25739
|
+
}
|
|
25740
|
+
}
|
|
25741
|
+
return issues;
|
|
25742
|
+
},
|
|
25743
|
+
fix(ctx) {
|
|
25744
|
+
const fixes = [];
|
|
25745
|
+
for (const doc of ctx.allDocuments) {
|
|
25746
|
+
const tags = doc.frontmatter.tags;
|
|
25747
|
+
if (!Array.isArray(tags)) continue;
|
|
25748
|
+
const streamTags = tags.filter((t) => t.startsWith("stream:"));
|
|
25749
|
+
if (streamTags.length === 0) continue;
|
|
25750
|
+
const newTags = tags.map(
|
|
25751
|
+
(t) => t.startsWith("stream:") ? t.replace("stream:", "focus:") : t
|
|
25752
|
+
);
|
|
25753
|
+
ctx.store.update(doc.frontmatter.id, { tags: newTags });
|
|
25754
|
+
for (const tag of streamTags) {
|
|
25755
|
+
fixes.push({
|
|
25756
|
+
issue: {
|
|
25757
|
+
ruleId: RULE_ID,
|
|
25758
|
+
ruleName: RULE_NAME,
|
|
25759
|
+
documentId: doc.frontmatter.id,
|
|
25760
|
+
filePath: doc.filePath,
|
|
25761
|
+
documentType: doc.frontmatter.type,
|
|
25762
|
+
message: `Deprecated tag "${tag}" should be "${tag.replace("stream:", "focus:")}"`,
|
|
25763
|
+
severity: "warning",
|
|
25764
|
+
fixable: true
|
|
25765
|
+
},
|
|
25766
|
+
fixDescription: `Renamed "${tag}" to "${tag.replace("stream:", "focus:")}"`
|
|
25767
|
+
});
|
|
25768
|
+
}
|
|
25769
|
+
}
|
|
25770
|
+
return fixes;
|
|
25771
|
+
}
|
|
25772
|
+
};
|
|
25773
|
+
|
|
25774
|
+
// src/doctor/rules/array-normalization.ts
|
|
25775
|
+
var RULE_ID2 = "array-normalization";
|
|
25776
|
+
var RULE_NAME2 = "Array Normalization";
|
|
25777
|
+
var FIELDS = [
|
|
25778
|
+
{
|
|
25779
|
+
field: "linkedEpic",
|
|
25780
|
+
aliases: ["linkedEpics"],
|
|
25781
|
+
normalize: normalizeLinkedEpics
|
|
25782
|
+
},
|
|
25783
|
+
{
|
|
25784
|
+
field: "linkedFeature",
|
|
25785
|
+
aliases: ["linkedFeatures"],
|
|
25786
|
+
normalize: normalizeLinkedFeatures
|
|
25787
|
+
}
|
|
25788
|
+
];
|
|
25789
|
+
var arrayNormalizationRule = {
|
|
25790
|
+
id: RULE_ID2,
|
|
25791
|
+
name: RULE_NAME2,
|
|
25792
|
+
description: "Normalizes linkedEpic/linkedFeature from strings to arrays and resolves field aliases",
|
|
25793
|
+
scan(ctx) {
|
|
25794
|
+
const issues = [];
|
|
25795
|
+
for (const doc of ctx.allDocuments) {
|
|
25796
|
+
const fm = doc.frontmatter;
|
|
25797
|
+
for (const cfg of FIELDS) {
|
|
25798
|
+
for (const alias of cfg.aliases) {
|
|
25799
|
+
if (fm[alias] !== void 0) {
|
|
25800
|
+
issues.push({
|
|
25801
|
+
ruleId: RULE_ID2,
|
|
25802
|
+
ruleName: RULE_NAME2,
|
|
25803
|
+
documentId: doc.frontmatter.id,
|
|
25804
|
+
filePath: doc.filePath,
|
|
25805
|
+
documentType: doc.frontmatter.type,
|
|
25806
|
+
message: `Field "${alias}" should be renamed to "${cfg.field}"`,
|
|
25807
|
+
severity: "warning",
|
|
25808
|
+
fixable: true
|
|
25809
|
+
});
|
|
25810
|
+
}
|
|
25811
|
+
}
|
|
25812
|
+
const value = fm[cfg.field];
|
|
25813
|
+
if (typeof value === "string") {
|
|
25814
|
+
issues.push({
|
|
25815
|
+
ruleId: RULE_ID2,
|
|
25816
|
+
ruleName: RULE_NAME2,
|
|
25817
|
+
documentId: doc.frontmatter.id,
|
|
25818
|
+
filePath: doc.filePath,
|
|
25819
|
+
documentType: doc.frontmatter.type,
|
|
25820
|
+
message: `Field "${cfg.field}" is a string ("${value}") but should be an array`,
|
|
25821
|
+
severity: "warning",
|
|
25822
|
+
fixable: true
|
|
25823
|
+
});
|
|
25824
|
+
}
|
|
25825
|
+
}
|
|
25826
|
+
}
|
|
25827
|
+
return issues;
|
|
25828
|
+
},
|
|
25829
|
+
fix(ctx) {
|
|
25830
|
+
const fixes = [];
|
|
25831
|
+
for (const doc of ctx.allDocuments) {
|
|
25832
|
+
const fm = doc.frontmatter;
|
|
25833
|
+
const updates = {};
|
|
25834
|
+
let needsUpdate = false;
|
|
25835
|
+
for (const cfg of FIELDS) {
|
|
25836
|
+
for (const alias of cfg.aliases) {
|
|
25837
|
+
if (fm[alias] !== void 0) {
|
|
25838
|
+
const aliasValues = cfg.normalize(fm[alias]);
|
|
25839
|
+
const existing = cfg.normalize(fm[cfg.field]);
|
|
25840
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...aliasValues])];
|
|
25841
|
+
updates[cfg.field] = merged;
|
|
25842
|
+
updates[alias] = void 0;
|
|
25843
|
+
needsUpdate = true;
|
|
25844
|
+
fixes.push({
|
|
25845
|
+
issue: {
|
|
25846
|
+
ruleId: RULE_ID2,
|
|
25847
|
+
ruleName: RULE_NAME2,
|
|
25848
|
+
documentId: doc.frontmatter.id,
|
|
25849
|
+
filePath: doc.filePath,
|
|
25850
|
+
documentType: doc.frontmatter.type,
|
|
25851
|
+
message: `Field "${alias}" should be renamed to "${cfg.field}"`,
|
|
25852
|
+
severity: "warning",
|
|
25853
|
+
fixable: true
|
|
25854
|
+
},
|
|
25855
|
+
fixDescription: `Merged "${alias}" into "${cfg.field}" and removed alias`
|
|
25856
|
+
});
|
|
25857
|
+
}
|
|
25858
|
+
}
|
|
25859
|
+
const value = updates[cfg.field] ?? fm[cfg.field];
|
|
25860
|
+
if (typeof value === "string") {
|
|
25861
|
+
updates[cfg.field] = cfg.normalize(value);
|
|
25862
|
+
needsUpdate = true;
|
|
25863
|
+
fixes.push({
|
|
25864
|
+
issue: {
|
|
25865
|
+
ruleId: RULE_ID2,
|
|
25866
|
+
ruleName: RULE_NAME2,
|
|
25867
|
+
documentId: doc.frontmatter.id,
|
|
25868
|
+
filePath: doc.filePath,
|
|
25869
|
+
documentType: doc.frontmatter.type,
|
|
25870
|
+
message: `Field "${cfg.field}" is a string ("${value}") but should be an array`,
|
|
25871
|
+
severity: "warning",
|
|
25872
|
+
fixable: true
|
|
25873
|
+
},
|
|
25874
|
+
fixDescription: `Normalized "${cfg.field}" from string to array`
|
|
25875
|
+
});
|
|
25876
|
+
}
|
|
25877
|
+
}
|
|
25878
|
+
if (needsUpdate) {
|
|
25879
|
+
ctx.store.update(doc.frontmatter.id, updates);
|
|
25880
|
+
}
|
|
25881
|
+
}
|
|
25882
|
+
return fixes;
|
|
25883
|
+
}
|
|
25884
|
+
};
|
|
25885
|
+
|
|
25886
|
+
// src/doctor/rules/missing-auto-tags.ts
|
|
25887
|
+
var RULE_ID3 = "missing-auto-tags";
|
|
25888
|
+
var RULE_NAME3 = "Missing Auto Tags";
|
|
25889
|
+
var missingAutoTagsRule = {
|
|
25890
|
+
id: RULE_ID3,
|
|
25891
|
+
name: RULE_NAME3,
|
|
25892
|
+
description: "Ensures tasks have epic:E-xxx tags for their linkedEpic and epics have feature:F-xxx tags",
|
|
25893
|
+
scan(ctx) {
|
|
25894
|
+
const issues = [];
|
|
25895
|
+
for (const doc of ctx.allDocuments) {
|
|
25896
|
+
const fm = doc.frontmatter;
|
|
25897
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
25898
|
+
const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
|
|
25899
|
+
if (linkedEpics.length > 0) {
|
|
25900
|
+
const expected = generateEpicTags(linkedEpics);
|
|
25901
|
+
const missing = expected.filter((t) => !tags.includes(t));
|
|
25902
|
+
for (const tag of missing) {
|
|
25903
|
+
issues.push({
|
|
25904
|
+
ruleId: RULE_ID3,
|
|
25905
|
+
ruleName: RULE_NAME3,
|
|
25906
|
+
documentId: doc.frontmatter.id,
|
|
25907
|
+
filePath: doc.filePath,
|
|
25908
|
+
documentType: doc.frontmatter.type,
|
|
25909
|
+
message: `Missing auto-tag "${tag}" for linkedEpic`,
|
|
25910
|
+
severity: "warning",
|
|
25911
|
+
fixable: true
|
|
25912
|
+
});
|
|
25913
|
+
}
|
|
25914
|
+
}
|
|
25915
|
+
const linkedFeatures = normalizeLinkedFeatures(fm.linkedFeature);
|
|
25916
|
+
if (linkedFeatures.length > 0) {
|
|
25917
|
+
const expected = generateFeatureTags(linkedFeatures);
|
|
25918
|
+
const missing = expected.filter((t) => !tags.includes(t));
|
|
25919
|
+
for (const tag of missing) {
|
|
25920
|
+
issues.push({
|
|
25921
|
+
ruleId: RULE_ID3,
|
|
25922
|
+
ruleName: RULE_NAME3,
|
|
25923
|
+
documentId: doc.frontmatter.id,
|
|
25924
|
+
filePath: doc.filePath,
|
|
25925
|
+
documentType: doc.frontmatter.type,
|
|
25926
|
+
message: `Missing auto-tag "${tag}" for linkedFeature`,
|
|
25927
|
+
severity: "warning",
|
|
25928
|
+
fixable: true
|
|
25929
|
+
});
|
|
25930
|
+
}
|
|
25931
|
+
}
|
|
25932
|
+
}
|
|
25933
|
+
return issues;
|
|
25934
|
+
},
|
|
25935
|
+
fix(ctx) {
|
|
25936
|
+
const fixes = [];
|
|
25937
|
+
for (const doc of ctx.allDocuments) {
|
|
25938
|
+
const fm = doc.frontmatter;
|
|
25939
|
+
const tags = [...doc.frontmatter.tags ?? []];
|
|
25940
|
+
let changed = false;
|
|
25941
|
+
const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
|
|
25942
|
+
if (linkedEpics.length > 0) {
|
|
25943
|
+
const expected = generateEpicTags(linkedEpics);
|
|
25944
|
+
for (const tag of expected) {
|
|
25945
|
+
if (!tags.includes(tag)) {
|
|
25946
|
+
tags.push(tag);
|
|
25947
|
+
changed = true;
|
|
25948
|
+
fixes.push({
|
|
25949
|
+
issue: {
|
|
25950
|
+
ruleId: RULE_ID3,
|
|
25951
|
+
ruleName: RULE_NAME3,
|
|
25952
|
+
documentId: doc.frontmatter.id,
|
|
25953
|
+
filePath: doc.filePath,
|
|
25954
|
+
documentType: doc.frontmatter.type,
|
|
25955
|
+
message: `Missing auto-tag "${tag}" for linkedEpic`,
|
|
25956
|
+
severity: "warning",
|
|
25957
|
+
fixable: true
|
|
25958
|
+
},
|
|
25959
|
+
fixDescription: `Added tag "${tag}"`
|
|
25960
|
+
});
|
|
25961
|
+
}
|
|
25962
|
+
}
|
|
25963
|
+
}
|
|
25964
|
+
const linkedFeatures = normalizeLinkedFeatures(fm.linkedFeature);
|
|
25965
|
+
if (linkedFeatures.length > 0) {
|
|
25966
|
+
const expected = generateFeatureTags(linkedFeatures);
|
|
25967
|
+
for (const tag of expected) {
|
|
25968
|
+
if (!tags.includes(tag)) {
|
|
25969
|
+
tags.push(tag);
|
|
25970
|
+
changed = true;
|
|
25971
|
+
fixes.push({
|
|
25972
|
+
issue: {
|
|
25973
|
+
ruleId: RULE_ID3,
|
|
25974
|
+
ruleName: RULE_NAME3,
|
|
25975
|
+
documentId: doc.frontmatter.id,
|
|
25976
|
+
filePath: doc.filePath,
|
|
25977
|
+
documentType: doc.frontmatter.type,
|
|
25978
|
+
message: `Missing auto-tag "${tag}" for linkedFeature`,
|
|
25979
|
+
severity: "warning",
|
|
25980
|
+
fixable: true
|
|
25981
|
+
},
|
|
25982
|
+
fixDescription: `Added tag "${tag}"`
|
|
25983
|
+
});
|
|
25984
|
+
}
|
|
25985
|
+
}
|
|
25986
|
+
}
|
|
25987
|
+
if (changed) {
|
|
25988
|
+
ctx.store.update(doc.frontmatter.id, { tags });
|
|
25989
|
+
}
|
|
25990
|
+
}
|
|
25991
|
+
return fixes;
|
|
25992
|
+
}
|
|
25993
|
+
};
|
|
25994
|
+
|
|
25995
|
+
// src/doctor/rules/progress-consistency.ts
|
|
25996
|
+
var RULE_ID4 = "progress-consistency";
|
|
25997
|
+
var RULE_NAME4 = "Progress Consistency";
|
|
25998
|
+
var progressConsistencyRule = {
|
|
25999
|
+
id: RULE_ID4,
|
|
26000
|
+
name: RULE_NAME4,
|
|
26001
|
+
description: "Detects done-status documents with progress != 100 and progressOverride:true without a progress value",
|
|
26002
|
+
scan(ctx) {
|
|
26003
|
+
const issues = [];
|
|
26004
|
+
for (const doc of ctx.allDocuments) {
|
|
26005
|
+
const fm = doc.frontmatter;
|
|
26006
|
+
const status = doc.frontmatter.status;
|
|
26007
|
+
const progress = fm.progress;
|
|
26008
|
+
const progressOverride = fm.progressOverride;
|
|
26009
|
+
if (status === "done" && progress !== void 0 && progress !== 100) {
|
|
26010
|
+
issues.push({
|
|
26011
|
+
ruleId: RULE_ID4,
|
|
26012
|
+
ruleName: RULE_NAME4,
|
|
26013
|
+
documentId: doc.frontmatter.id,
|
|
26014
|
+
filePath: doc.filePath,
|
|
26015
|
+
documentType: doc.frontmatter.type,
|
|
26016
|
+
message: `Status is "done" but progress is ${progress} (expected 100)`,
|
|
26017
|
+
severity: "error",
|
|
26018
|
+
fixable: true
|
|
26019
|
+
});
|
|
26020
|
+
}
|
|
26021
|
+
if (progressOverride === true && progress === void 0) {
|
|
26022
|
+
issues.push({
|
|
26023
|
+
ruleId: RULE_ID4,
|
|
26024
|
+
ruleName: RULE_NAME4,
|
|
26025
|
+
documentId: doc.frontmatter.id,
|
|
26026
|
+
filePath: doc.filePath,
|
|
26027
|
+
documentType: doc.frontmatter.type,
|
|
26028
|
+
message: `progressOverride is true but no progress value is set`,
|
|
26029
|
+
severity: "warning",
|
|
26030
|
+
fixable: true
|
|
26031
|
+
});
|
|
26032
|
+
}
|
|
26033
|
+
}
|
|
26034
|
+
return issues;
|
|
26035
|
+
},
|
|
26036
|
+
fix(ctx) {
|
|
26037
|
+
const fixes = [];
|
|
26038
|
+
for (const doc of ctx.allDocuments) {
|
|
26039
|
+
const fm = doc.frontmatter;
|
|
26040
|
+
const status = doc.frontmatter.status;
|
|
26041
|
+
const progress = fm.progress;
|
|
26042
|
+
const progressOverride = fm.progressOverride;
|
|
26043
|
+
if (status === "done" && progress !== void 0 && progress !== 100) {
|
|
26044
|
+
ctx.store.update(doc.frontmatter.id, { progress: 100 });
|
|
26045
|
+
fixes.push({
|
|
26046
|
+
issue: {
|
|
26047
|
+
ruleId: RULE_ID4,
|
|
26048
|
+
ruleName: RULE_NAME4,
|
|
26049
|
+
documentId: doc.frontmatter.id,
|
|
26050
|
+
filePath: doc.filePath,
|
|
26051
|
+
documentType: doc.frontmatter.type,
|
|
26052
|
+
message: `Status is "done" but progress is ${progress} (expected 100)`,
|
|
26053
|
+
severity: "error",
|
|
26054
|
+
fixable: true
|
|
26055
|
+
},
|
|
26056
|
+
fixDescription: `Set progress to 100`
|
|
26057
|
+
});
|
|
26058
|
+
}
|
|
26059
|
+
if (progressOverride === true && progress === void 0) {
|
|
26060
|
+
ctx.store.update(doc.frontmatter.id, { progressOverride: false });
|
|
26061
|
+
fixes.push({
|
|
26062
|
+
issue: {
|
|
26063
|
+
ruleId: RULE_ID4,
|
|
26064
|
+
ruleName: RULE_NAME4,
|
|
26065
|
+
documentId: doc.frontmatter.id,
|
|
26066
|
+
filePath: doc.filePath,
|
|
26067
|
+
documentType: doc.frontmatter.type,
|
|
26068
|
+
message: `progressOverride is true but no progress value is set`,
|
|
26069
|
+
severity: "warning",
|
|
26070
|
+
fixable: true
|
|
26071
|
+
},
|
|
26072
|
+
fixDescription: `Set progressOverride to false`
|
|
26073
|
+
});
|
|
26074
|
+
}
|
|
26075
|
+
}
|
|
26076
|
+
return fixes;
|
|
26077
|
+
}
|
|
26078
|
+
};
|
|
26079
|
+
|
|
26080
|
+
// src/doctor/rules/orphaned-references.ts
|
|
26081
|
+
var RULE_ID5 = "orphaned-references";
|
|
26082
|
+
var RULE_NAME5 = "Orphaned References";
|
|
26083
|
+
var REFERENCE_FIELDS = ["aboutArtifact", "linkedEpic", "linkedFeature"];
|
|
26084
|
+
var orphanedReferencesRule = {
|
|
26085
|
+
id: RULE_ID5,
|
|
26086
|
+
name: RULE_NAME5,
|
|
26087
|
+
description: "Detects references (aboutArtifact, linkedEpic, linkedFeature) pointing to non-existent documents",
|
|
26088
|
+
scan(ctx) {
|
|
26089
|
+
const issues = [];
|
|
26090
|
+
for (const doc of ctx.allDocuments) {
|
|
26091
|
+
const fm = doc.frontmatter;
|
|
26092
|
+
for (const field of REFERENCE_FIELDS) {
|
|
26093
|
+
const value = fm[field];
|
|
26094
|
+
if (value === void 0 || value === null) continue;
|
|
26095
|
+
const refs = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" ? [value] : [];
|
|
26096
|
+
for (const ref of refs) {
|
|
26097
|
+
if (!ctx.documentIndex.has(ref)) {
|
|
26098
|
+
issues.push({
|
|
26099
|
+
ruleId: RULE_ID5,
|
|
26100
|
+
ruleName: RULE_NAME5,
|
|
26101
|
+
documentId: doc.frontmatter.id,
|
|
26102
|
+
filePath: doc.filePath,
|
|
26103
|
+
documentType: doc.frontmatter.type,
|
|
26104
|
+
message: `Field "${field}" references "${ref}" which does not exist`,
|
|
26105
|
+
severity: "warning",
|
|
26106
|
+
fixable: false
|
|
26107
|
+
});
|
|
26108
|
+
}
|
|
26109
|
+
}
|
|
26110
|
+
}
|
|
26111
|
+
}
|
|
26112
|
+
return issues;
|
|
26113
|
+
},
|
|
26114
|
+
fix() {
|
|
26115
|
+
return [];
|
|
26116
|
+
}
|
|
26117
|
+
};
|
|
26118
|
+
|
|
26119
|
+
// src/doctor/rules/owner-role.ts
|
|
26120
|
+
var RULE_ID6 = "owner-role";
|
|
26121
|
+
var RULE_NAME6 = "Owner Role";
|
|
26122
|
+
var ownerRoleRule = {
|
|
26123
|
+
id: RULE_ID6,
|
|
26124
|
+
name: RULE_NAME6,
|
|
26125
|
+
description: `Detects owner values that are not valid persona roles (${OWNER_SHORT.join(", ")})`,
|
|
26126
|
+
scan(ctx) {
|
|
26127
|
+
const issues = [];
|
|
26128
|
+
for (const doc of ctx.allDocuments) {
|
|
26129
|
+
const owner = doc.frontmatter.owner;
|
|
26130
|
+
if (owner === void 0 || owner === null || owner === "") continue;
|
|
26131
|
+
if (!isValidOwner(owner)) {
|
|
26132
|
+
issues.push({
|
|
26133
|
+
ruleId: RULE_ID6,
|
|
26134
|
+
ruleName: RULE_NAME6,
|
|
26135
|
+
documentId: doc.frontmatter.id,
|
|
26136
|
+
filePath: doc.filePath,
|
|
26137
|
+
documentType: doc.frontmatter.type,
|
|
26138
|
+
message: `Owner "${owner}" is not a valid persona role. Expected one of: ${OWNER_SHORT.join(", ")}`,
|
|
26139
|
+
severity: "warning",
|
|
26140
|
+
fixable: false
|
|
26141
|
+
});
|
|
26142
|
+
}
|
|
26143
|
+
}
|
|
26144
|
+
return issues;
|
|
26145
|
+
},
|
|
26146
|
+
fix() {
|
|
26147
|
+
return [];
|
|
26148
|
+
}
|
|
26149
|
+
};
|
|
26150
|
+
|
|
26151
|
+
// src/doctor/rules/index.ts
|
|
26152
|
+
var allRules = [
|
|
26153
|
+
tagMigrationRule,
|
|
26154
|
+
arrayNormalizationRule,
|
|
26155
|
+
missingAutoTagsRule,
|
|
26156
|
+
progressConsistencyRule,
|
|
26157
|
+
orphanedReferencesRule,
|
|
26158
|
+
ownerRoleRule
|
|
26159
|
+
];
|
|
26160
|
+
|
|
26161
|
+
// src/doctor/engine.ts
|
|
26162
|
+
function buildDoctorContext(store) {
|
|
26163
|
+
const allDocuments = store.list();
|
|
26164
|
+
const documentIndex = new Map(
|
|
26165
|
+
allDocuments.map((doc) => [doc.frontmatter.id, doc])
|
|
26166
|
+
);
|
|
26167
|
+
return { store, allDocuments, documentIndex };
|
|
26168
|
+
}
|
|
26169
|
+
function runDoctorScan(store, ruleFilter) {
|
|
26170
|
+
const rules = resolveRules(ruleFilter);
|
|
26171
|
+
const ctx = buildDoctorContext(store);
|
|
26172
|
+
const issues = rules.flatMap((rule) => rule.scan(ctx));
|
|
26173
|
+
return buildReport(ctx, issues, []);
|
|
26174
|
+
}
|
|
26175
|
+
function runDoctorFix(store, ruleFilter) {
|
|
26176
|
+
const rules = resolveRules(ruleFilter);
|
|
26177
|
+
let ctx = buildDoctorContext(store);
|
|
26178
|
+
const allIssues = rules.flatMap((rule) => rule.scan(ctx));
|
|
26179
|
+
const allFixes = [];
|
|
26180
|
+
for (const rule of rules) {
|
|
26181
|
+
const fixes = rule.fix(ctx);
|
|
26182
|
+
allFixes.push(...fixes);
|
|
26183
|
+
if (fixes.length > 0) {
|
|
26184
|
+
ctx = buildDoctorContext(store);
|
|
26185
|
+
}
|
|
26186
|
+
}
|
|
26187
|
+
return buildReport(ctx, allIssues, allFixes);
|
|
26188
|
+
}
|
|
26189
|
+
function resolveRules(ruleFilter) {
|
|
26190
|
+
if (!ruleFilter) return allRules;
|
|
26191
|
+
const rule = allRules.find((r) => r.id === ruleFilter);
|
|
26192
|
+
if (!rule) {
|
|
26193
|
+
throw new Error(
|
|
26194
|
+
`Unknown rule: ${ruleFilter}. Available: ${allRules.map((r) => r.id).join(", ")}`
|
|
26195
|
+
);
|
|
26196
|
+
}
|
|
26197
|
+
return [rule];
|
|
26198
|
+
}
|
|
26199
|
+
function buildReport(ctx, issues, fixes) {
|
|
26200
|
+
const byRule = {};
|
|
26201
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
26202
|
+
let fixableIssues = 0;
|
|
26203
|
+
for (const issue2 of issues) {
|
|
26204
|
+
byRule[issue2.ruleId] = (byRule[issue2.ruleId] ?? 0) + 1;
|
|
26205
|
+
bySeverity[issue2.severity] = (bySeverity[issue2.severity] ?? 0) + 1;
|
|
26206
|
+
if (issue2.fixable) fixableIssues++;
|
|
26207
|
+
}
|
|
26208
|
+
return {
|
|
26209
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26210
|
+
totalDocuments: ctx.allDocuments.length,
|
|
26211
|
+
issues,
|
|
26212
|
+
fixes,
|
|
26213
|
+
summary: {
|
|
26214
|
+
totalIssues: issues.length,
|
|
26215
|
+
fixableIssues,
|
|
26216
|
+
fixedIssues: fixes.length,
|
|
26217
|
+
byRule,
|
|
26218
|
+
bySeverity
|
|
26219
|
+
}
|
|
26220
|
+
};
|
|
26221
|
+
}
|
|
26222
|
+
|
|
26223
|
+
// src/agent/tools/doctor.ts
|
|
26224
|
+
function createDoctorTools(store) {
|
|
26225
|
+
return [
|
|
26226
|
+
tool23(
|
|
26227
|
+
"run_doctor",
|
|
26228
|
+
"Scan project documents for structural issues and optionally auto-repair them. Returns a JSON report with all issues found and fixes applied.",
|
|
26229
|
+
{
|
|
26230
|
+
fix: external_exports.boolean().optional().default(false).describe("When true, auto-repair fixable issues"),
|
|
26231
|
+
rule: external_exports.string().optional().describe(
|
|
26232
|
+
"Run only a specific rule (e.g. tag-migration, array-normalization, missing-auto-tags, progress-consistency, orphaned-references)"
|
|
26233
|
+
)
|
|
26234
|
+
},
|
|
26235
|
+
async (args) => {
|
|
26236
|
+
try {
|
|
26237
|
+
const report = args.fix ? runDoctorFix(store, args.rule) : runDoctorScan(store, args.rule);
|
|
26238
|
+
return {
|
|
26239
|
+
content: [
|
|
26240
|
+
{
|
|
26241
|
+
type: "text",
|
|
26242
|
+
text: JSON.stringify(report, null, 2)
|
|
26243
|
+
}
|
|
26244
|
+
]
|
|
26245
|
+
};
|
|
26246
|
+
} catch (err) {
|
|
26247
|
+
return {
|
|
26248
|
+
content: [
|
|
26249
|
+
{
|
|
26250
|
+
type: "text",
|
|
26251
|
+
text: `Doctor error: ${err instanceof Error ? err.message : String(err)}`
|
|
26252
|
+
}
|
|
26253
|
+
],
|
|
26254
|
+
isError: true
|
|
26255
|
+
};
|
|
26256
|
+
}
|
|
26257
|
+
}
|
|
26258
|
+
)
|
|
26259
|
+
];
|
|
26260
|
+
}
|
|
26261
|
+
|
|
25638
26262
|
// src/agent/mcp-server.ts
|
|
25639
26263
|
function createMarvinMcpServer(store, options) {
|
|
25640
26264
|
const tools = [
|
|
@@ -25646,7 +26270,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
25646
26270
|
...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
|
|
25647
26271
|
...options?.pluginTools ?? [],
|
|
25648
26272
|
...options?.skillTools ?? [],
|
|
25649
|
-
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
|
|
26273
|
+
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : [],
|
|
26274
|
+
...createDoctorTools(store)
|
|
25650
26275
|
];
|
|
25651
26276
|
return createSdkMcpServer({
|
|
25652
26277
|
name: "marvin-governance",
|
|
@@ -26957,7 +27582,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
26957
27582
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26958
27583
|
|
|
26959
27584
|
// src/skills/action-tools.ts
|
|
26960
|
-
import { tool as
|
|
27585
|
+
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
26961
27586
|
|
|
26962
27587
|
// src/skills/action-runner.ts
|
|
26963
27588
|
import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -27023,7 +27648,7 @@ function createSkillActionTools(skills, context) {
|
|
|
27023
27648
|
if (!skill.actions) continue;
|
|
27024
27649
|
for (const action of skill.actions) {
|
|
27025
27650
|
tools.push(
|
|
27026
|
-
|
|
27651
|
+
tool24(
|
|
27027
27652
|
`${skill.id}__${action.id}`,
|
|
27028
27653
|
action.description,
|
|
27029
27654
|
{
|
|
@@ -27115,10 +27740,10 @@ ${lines.join("\n\n")}`;
|
|
|
27115
27740
|
}
|
|
27116
27741
|
|
|
27117
27742
|
// src/mcp/persona-tools.ts
|
|
27118
|
-
import { tool as
|
|
27743
|
+
import { tool as tool25 } from "@anthropic-ai/claude-agent-sdk";
|
|
27119
27744
|
function createPersonaTools(ctx, marvinDir) {
|
|
27120
27745
|
return [
|
|
27121
|
-
|
|
27746
|
+
tool25(
|
|
27122
27747
|
"set_persona",
|
|
27123
27748
|
"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.",
|
|
27124
27749
|
{
|
|
@@ -27148,7 +27773,7 @@ ${summaries}`
|
|
|
27148
27773
|
};
|
|
27149
27774
|
}
|
|
27150
27775
|
),
|
|
27151
|
-
|
|
27776
|
+
tool25(
|
|
27152
27777
|
"get_persona_guidance",
|
|
27153
27778
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
27154
27779
|
{
|
|
@@ -27727,8 +28352,8 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
27727
28352
|
}
|
|
27728
28353
|
function formatPlanSummary(plan) {
|
|
27729
28354
|
const lines = [];
|
|
27730
|
-
const
|
|
27731
|
-
lines.push(`Detected: ${
|
|
28355
|
+
const typeLabel5 = classificationLabel(plan.classification.type);
|
|
28356
|
+
lines.push(`Detected: ${typeLabel5}`);
|
|
27732
28357
|
lines.push(`Source: ${plan.classification.inputPath}`);
|
|
27733
28358
|
lines.push("");
|
|
27734
28359
|
const imports = plan.items.filter((i) => i.action === "import");
|
|
@@ -28961,12 +29586,76 @@ async function generateClaudeMdCommand(options) {
|
|
|
28961
29586
|
console.log(chalk18.green("Created .marvin/CLAUDE.md"));
|
|
28962
29587
|
}
|
|
28963
29588
|
|
|
29589
|
+
// src/cli/commands/doctor.ts
|
|
29590
|
+
import chalk19 from "chalk";
|
|
29591
|
+
var SEVERITY_ICONS = {
|
|
29592
|
+
error: chalk19.red("x"),
|
|
29593
|
+
warning: chalk19.yellow("!"),
|
|
29594
|
+
info: chalk19.blue("i")
|
|
29595
|
+
};
|
|
29596
|
+
async function doctorCommand(options) {
|
|
29597
|
+
const project = loadProject();
|
|
29598
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
29599
|
+
const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
|
|
29600
|
+
const allSkills = loadAllSkills(project.marvinDir);
|
|
29601
|
+
const allSkillIds = [...allSkills.keys()];
|
|
29602
|
+
const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
|
|
29603
|
+
const store = new DocumentStore(project.marvinDir, [
|
|
29604
|
+
...pluginRegistrations,
|
|
29605
|
+
...skillRegistrations
|
|
29606
|
+
]);
|
|
29607
|
+
const report = options.fix ? runDoctorFix(store, options.rule) : runDoctorScan(store, options.rule);
|
|
29608
|
+
printReport(report, !!options.fix);
|
|
29609
|
+
}
|
|
29610
|
+
function printReport(report, didFix) {
|
|
29611
|
+
console.log(chalk19.bold(`
|
|
29612
|
+
Artifact Doctor
|
|
29613
|
+
`));
|
|
29614
|
+
console.log(`Scanned ${report.totalDocuments} documents
|
|
29615
|
+
`);
|
|
29616
|
+
if (report.issues.length === 0) {
|
|
29617
|
+
console.log(chalk19.green("No issues found. All documents are healthy.\n"));
|
|
29618
|
+
return;
|
|
29619
|
+
}
|
|
29620
|
+
const byDoc = /* @__PURE__ */ new Map();
|
|
29621
|
+
for (const issue2 of report.issues) {
|
|
29622
|
+
const key = issue2.documentId;
|
|
29623
|
+
if (!byDoc.has(key)) byDoc.set(key, []);
|
|
29624
|
+
byDoc.get(key).push(issue2);
|
|
29625
|
+
}
|
|
29626
|
+
for (const [docId, issues] of byDoc) {
|
|
29627
|
+
const first = issues[0];
|
|
29628
|
+
console.log(chalk19.cyan(docId) + chalk19.dim(` (${first.documentType})`));
|
|
29629
|
+
for (const issue2 of issues) {
|
|
29630
|
+
const icon = SEVERITY_ICONS[issue2.severity] ?? " ";
|
|
29631
|
+
const fixLabel = issue2.fixable ? chalk19.dim(" [fixable]") : "";
|
|
29632
|
+
console.log(` ${icon} ${issue2.message}${fixLabel}`);
|
|
29633
|
+
}
|
|
29634
|
+
console.log();
|
|
29635
|
+
}
|
|
29636
|
+
console.log(chalk19.underline("Summary"));
|
|
29637
|
+
console.log(` Total issues: ${report.summary.totalIssues}`);
|
|
29638
|
+
console.log(` Fixable: ${report.summary.fixableIssues}`);
|
|
29639
|
+
if (didFix) {
|
|
29640
|
+
console.log(chalk19.green(` Fixed: ${report.summary.fixedIssues}`));
|
|
29641
|
+
}
|
|
29642
|
+
const { bySeverity } = report.summary;
|
|
29643
|
+
if (bySeverity.error > 0) console.log(chalk19.red(` Errors: ${bySeverity.error}`));
|
|
29644
|
+
if (bySeverity.warning > 0) console.log(chalk19.yellow(` Warnings: ${bySeverity.warning}`));
|
|
29645
|
+
if (bySeverity.info > 0) console.log(chalk19.blue(` Info: ${bySeverity.info}`));
|
|
29646
|
+
if (!didFix && report.summary.fixableIssues > 0) {
|
|
29647
|
+
console.log(chalk19.dim(`
|
|
29648
|
+
Run "marvin doctor --fix" to auto-repair fixable issues.`));
|
|
29649
|
+
}
|
|
29650
|
+
console.log();
|
|
29651
|
+
}
|
|
29652
|
+
|
|
28964
29653
|
// src/cli/program.ts
|
|
28965
29654
|
function createProgram() {
|
|
28966
29655
|
const program2 = new Command();
|
|
28967
29656
|
program2.name("marvin").description(
|
|
28968
29657
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
28969
|
-
).version("0.5.
|
|
29658
|
+
).version("0.5.2");
|
|
28970
29659
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
28971
29660
|
await initCommand();
|
|
28972
29661
|
});
|
|
@@ -29055,6 +29744,9 @@ function createProgram() {
|
|
|
29055
29744
|
program2.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
|
|
29056
29745
|
await webCommand(options);
|
|
29057
29746
|
});
|
|
29747
|
+
program2.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) => {
|
|
29748
|
+
await doctorCommand(options);
|
|
29749
|
+
});
|
|
29058
29750
|
const generateCmd = program2.command("generate").description("Generate project files");
|
|
29059
29751
|
generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
|
|
29060
29752
|
await generateClaudeMdCommand(options);
|