mrvn-cli 0.5.1 → 0.5.3
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 +836 -164
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +768 -163
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +836 -164
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -14209,6 +14209,26 @@ config(en_default());
|
|
|
14209
14209
|
|
|
14210
14210
|
// src/agent/tools/decisions.ts
|
|
14211
14211
|
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
14212
|
+
|
|
14213
|
+
// src/personas/owner.ts
|
|
14214
|
+
var OWNER_SHORT = ["po", "dm", "tl"];
|
|
14215
|
+
var OWNER_LONG = ["product-owner", "delivery-manager", "tech-lead"];
|
|
14216
|
+
var VALID_OWNERS = [...OWNER_SHORT, ...OWNER_LONG];
|
|
14217
|
+
var LONG_TO_SHORT = {
|
|
14218
|
+
"product-owner": "po",
|
|
14219
|
+
"delivery-manager": "dm",
|
|
14220
|
+
"tech-lead": "tl"
|
|
14221
|
+
};
|
|
14222
|
+
var ownerSchema = external_exports.enum(VALID_OWNERS);
|
|
14223
|
+
function normalizeOwner(owner) {
|
|
14224
|
+
if (owner === void 0) return void 0;
|
|
14225
|
+
return LONG_TO_SHORT[owner] ?? owner;
|
|
14226
|
+
}
|
|
14227
|
+
function isValidOwner(value) {
|
|
14228
|
+
return VALID_OWNERS.includes(value);
|
|
14229
|
+
}
|
|
14230
|
+
|
|
14231
|
+
// src/agent/tools/decisions.ts
|
|
14212
14232
|
function createDecisionTools(store) {
|
|
14213
14233
|
return [
|
|
14214
14234
|
tool(
|
|
@@ -14263,7 +14283,8 @@ function createDecisionTools(store) {
|
|
|
14263
14283
|
title: external_exports.string().describe("Title of the decision"),
|
|
14264
14284
|
content: external_exports.string().describe("Decision description, context, and rationale"),
|
|
14265
14285
|
status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("Status (default: 'open')"),
|
|
14266
|
-
owner:
|
|
14286
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14287
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14267
14288
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14268
14289
|
},
|
|
14269
14290
|
async (args) => {
|
|
@@ -14272,7 +14293,8 @@ function createDecisionTools(store) {
|
|
|
14272
14293
|
{
|
|
14273
14294
|
title: args.title,
|
|
14274
14295
|
status: args.status,
|
|
14275
|
-
owner: args.owner,
|
|
14296
|
+
owner: normalizeOwner(args.owner),
|
|
14297
|
+
assignee: args.assignee,
|
|
14276
14298
|
tags: args.tags
|
|
14277
14299
|
},
|
|
14278
14300
|
args.content
|
|
@@ -14295,11 +14317,14 @@ function createDecisionTools(store) {
|
|
|
14295
14317
|
title: external_exports.string().optional().describe("New title"),
|
|
14296
14318
|
status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("New status"),
|
|
14297
14319
|
content: external_exports.string().optional().describe("New content"),
|
|
14298
|
-
owner:
|
|
14320
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14321
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14299
14322
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14300
14323
|
},
|
|
14301
14324
|
async (args) => {
|
|
14302
|
-
const { id, content, ...updates } = args;
|
|
14325
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
14326
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14327
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14303
14328
|
const doc = store.update(id, updates, content);
|
|
14304
14329
|
return {
|
|
14305
14330
|
content: [
|
|
@@ -14482,12 +14507,13 @@ function createActionTools(store) {
|
|
|
14482
14507
|
title: external_exports.string().describe("Title of the action item"),
|
|
14483
14508
|
content: external_exports.string().describe("Description of what needs to be done"),
|
|
14484
14509
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14485
|
-
owner:
|
|
14510
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14511
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14486
14512
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
14487
14513
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
14488
14514
|
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14489
14515
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags."),
|
|
14490
|
-
|
|
14516
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
|
|
14491
14517
|
},
|
|
14492
14518
|
async (args) => {
|
|
14493
14519
|
const tags = [...args.tags ?? []];
|
|
@@ -14497,15 +14523,16 @@ function createActionTools(store) {
|
|
|
14497
14523
|
if (!tags.includes(tag)) tags.push(tag);
|
|
14498
14524
|
}
|
|
14499
14525
|
}
|
|
14500
|
-
if (args.
|
|
14501
|
-
tags.push(`
|
|
14526
|
+
if (args.workFocus) {
|
|
14527
|
+
tags.push(`focus:${args.workFocus}`);
|
|
14502
14528
|
}
|
|
14503
14529
|
const doc = store.create(
|
|
14504
14530
|
"action",
|
|
14505
14531
|
{
|
|
14506
14532
|
title: args.title,
|
|
14507
14533
|
status: args.status,
|
|
14508
|
-
owner: args.owner,
|
|
14534
|
+
owner: normalizeOwner(args.owner),
|
|
14535
|
+
assignee: args.assignee,
|
|
14509
14536
|
priority: args.priority,
|
|
14510
14537
|
tags: tags.length > 0 ? tags : void 0,
|
|
14511
14538
|
dueDate: args.dueDate
|
|
@@ -14533,16 +14560,19 @@ function createActionTools(store) {
|
|
|
14533
14560
|
title: external_exports.string().optional().describe("New title"),
|
|
14534
14561
|
status: external_exports.string().optional().describe("New status"),
|
|
14535
14562
|
content: external_exports.string().optional().describe("New content"),
|
|
14536
|
-
owner:
|
|
14563
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14564
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14537
14565
|
priority: external_exports.string().optional().describe("New priority"),
|
|
14538
14566
|
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14539
14567
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
14540
14568
|
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
|
|
14541
|
-
|
|
14569
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
14542
14570
|
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100).")
|
|
14543
14571
|
},
|
|
14544
14572
|
async (args) => {
|
|
14545
|
-
const { id, content, sprints, tags,
|
|
14573
|
+
const { id, content, sprints, tags, workFocus, progress, owner, assignee, ...updates } = args;
|
|
14574
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14575
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14546
14576
|
if (tags !== void 0) {
|
|
14547
14577
|
const merged = [...tags];
|
|
14548
14578
|
if (sprints) {
|
|
@@ -14551,14 +14581,14 @@ function createActionTools(store) {
|
|
|
14551
14581
|
if (!merged.includes(tag)) merged.push(tag);
|
|
14552
14582
|
}
|
|
14553
14583
|
}
|
|
14554
|
-
if (
|
|
14555
|
-
const filtered = merged.filter((t) => !t.startsWith("
|
|
14556
|
-
filtered.push(`
|
|
14584
|
+
if (workFocus !== void 0) {
|
|
14585
|
+
const filtered = merged.filter((t) => !t.startsWith("focus:"));
|
|
14586
|
+
filtered.push(`focus:${workFocus}`);
|
|
14557
14587
|
updates.tags = filtered;
|
|
14558
14588
|
} else {
|
|
14559
14589
|
updates.tags = merged;
|
|
14560
14590
|
}
|
|
14561
|
-
} else if (sprints !== void 0 ||
|
|
14591
|
+
} else if (sprints !== void 0 || workFocus !== void 0) {
|
|
14562
14592
|
const existing = store.get(id);
|
|
14563
14593
|
if (!existing) {
|
|
14564
14594
|
return {
|
|
@@ -14571,9 +14601,9 @@ function createActionTools(store) {
|
|
|
14571
14601
|
existingTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
14572
14602
|
existingTags.push(...sprints.map((s) => `sprint:${s}`));
|
|
14573
14603
|
}
|
|
14574
|
-
if (
|
|
14575
|
-
existingTags = existingTags.filter((t) => !t.startsWith("
|
|
14576
|
-
existingTags.push(`
|
|
14604
|
+
if (workFocus !== void 0) {
|
|
14605
|
+
existingTags = existingTags.filter((t) => !t.startsWith("focus:"));
|
|
14606
|
+
existingTags.push(`focus:${workFocus}`);
|
|
14577
14607
|
}
|
|
14578
14608
|
updates.tags = existingTags;
|
|
14579
14609
|
}
|
|
@@ -14686,7 +14716,8 @@ function createQuestionTools(store) {
|
|
|
14686
14716
|
title: external_exports.string().describe("The question being asked"),
|
|
14687
14717
|
content: external_exports.string().describe("Context and details about the question"),
|
|
14688
14718
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14689
|
-
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"),
|
|
14690
14721
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14691
14722
|
},
|
|
14692
14723
|
async (args) => {
|
|
@@ -14695,7 +14726,8 @@ function createQuestionTools(store) {
|
|
|
14695
14726
|
{
|
|
14696
14727
|
title: args.title,
|
|
14697
14728
|
status: args.status,
|
|
14698
|
-
owner: args.owner,
|
|
14729
|
+
owner: normalizeOwner(args.owner),
|
|
14730
|
+
assignee: args.assignee,
|
|
14699
14731
|
tags: args.tags
|
|
14700
14732
|
},
|
|
14701
14733
|
args.content
|
|
@@ -14718,11 +14750,14 @@ function createQuestionTools(store) {
|
|
|
14718
14750
|
title: external_exports.string().optional().describe("New title"),
|
|
14719
14751
|
status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
|
|
14720
14752
|
content: external_exports.string().optional().describe("Updated content / answer"),
|
|
14721
|
-
owner:
|
|
14753
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
14754
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
14722
14755
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14723
14756
|
},
|
|
14724
14757
|
async (args) => {
|
|
14725
|
-
const { id, content, ...updates } = args;
|
|
14758
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
14759
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
14760
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
14726
14761
|
const doc = store.update(id, updates, content);
|
|
14727
14762
|
return {
|
|
14728
14763
|
content: [
|
|
@@ -14747,18 +14782,20 @@ function createDocumentTools(store) {
|
|
|
14747
14782
|
{
|
|
14748
14783
|
type: external_exports.string().optional().describe(`Filter by document type (registered types: ${store.registeredTypes.join(", ")})`),
|
|
14749
14784
|
status: external_exports.string().optional().describe("Filter by status"),
|
|
14785
|
+
owner: external_exports.string().optional().describe("Filter by persona role owner (po, dm, tl)"),
|
|
14750
14786
|
tag: external_exports.string().optional().describe("Filter by tag"),
|
|
14751
|
-
|
|
14787
|
+
workFocus: external_exports.string().optional().describe("Filter by work focus name (matches focus:<value> tag)")
|
|
14752
14788
|
},
|
|
14753
14789
|
async (args) => {
|
|
14754
14790
|
let docs = store.list({
|
|
14755
14791
|
type: args.type,
|
|
14756
14792
|
status: args.status,
|
|
14793
|
+
owner: args.owner,
|
|
14757
14794
|
tag: args.tag
|
|
14758
14795
|
});
|
|
14759
|
-
if (args.
|
|
14760
|
-
const
|
|
14761
|
-
docs = docs.filter((d) => d.frontmatter.tags?.includes(
|
|
14796
|
+
if (args.workFocus) {
|
|
14797
|
+
const focusTag = `focus:${args.workFocus}`;
|
|
14798
|
+
docs = docs.filter((d) => d.frontmatter.tags?.includes(focusTag));
|
|
14762
14799
|
}
|
|
14763
14800
|
const summary = docs.map((d) => ({
|
|
14764
14801
|
id: d.frontmatter.id,
|
|
@@ -15080,7 +15117,8 @@ function createMeetingTools(store) {
|
|
|
15080
15117
|
title: external_exports.string().describe("Title of the meeting"),
|
|
15081
15118
|
content: external_exports.string().describe("Meeting agenda, notes, or minutes"),
|
|
15082
15119
|
status: external_exports.string().optional().describe("Status (default: 'scheduled')"),
|
|
15083
|
-
owner:
|
|
15120
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
15121
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
15084
15122
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
15085
15123
|
attendees: external_exports.array(external_exports.string()).optional().describe("List of attendees"),
|
|
15086
15124
|
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.")
|
|
@@ -15090,7 +15128,8 @@ function createMeetingTools(store) {
|
|
|
15090
15128
|
title: args.title,
|
|
15091
15129
|
status: args.status ?? "scheduled"
|
|
15092
15130
|
};
|
|
15093
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
15131
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
15132
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
15094
15133
|
if (args.tags) frontmatter.tags = args.tags;
|
|
15095
15134
|
if (args.attendees) frontmatter.attendees = args.attendees;
|
|
15096
15135
|
frontmatter.date = args.date;
|
|
@@ -15117,10 +15156,13 @@ function createMeetingTools(store) {
|
|
|
15117
15156
|
title: external_exports.string().optional().describe("New title"),
|
|
15118
15157
|
status: external_exports.string().optional().describe("New status"),
|
|
15119
15158
|
content: external_exports.string().optional().describe("New content"),
|
|
15120
|
-
owner:
|
|
15159
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
15160
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work")
|
|
15121
15161
|
},
|
|
15122
15162
|
async (args) => {
|
|
15123
|
-
const { id, content, ...updates } = args;
|
|
15163
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
15164
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
15165
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
15124
15166
|
const doc = store.update(id, updates, content);
|
|
15125
15167
|
return {
|
|
15126
15168
|
content: [
|
|
@@ -15653,14 +15695,14 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15653
15695
|
const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
|
|
15654
15696
|
for (const doc of workItemDocs) {
|
|
15655
15697
|
const about = doc.frontmatter.aboutArtifact;
|
|
15656
|
-
const
|
|
15698
|
+
const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
|
|
15657
15699
|
const item = {
|
|
15658
15700
|
id: doc.frontmatter.id,
|
|
15659
15701
|
title: doc.frontmatter.title,
|
|
15660
15702
|
type: doc.frontmatter.type,
|
|
15661
15703
|
status: doc.frontmatter.status,
|
|
15662
15704
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15663
|
-
|
|
15705
|
+
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15664
15706
|
aboutArtifact: about
|
|
15665
15707
|
};
|
|
15666
15708
|
allItemsById.set(item.id, item);
|
|
@@ -16815,7 +16857,8 @@ function createFeatureTools(store) {
|
|
|
16815
16857
|
title: external_exports.string().describe("Feature title"),
|
|
16816
16858
|
content: external_exports.string().describe("Feature description and requirements"),
|
|
16817
16859
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("Feature status (default: 'draft')"),
|
|
16818
|
-
owner:
|
|
16860
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
16861
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
16819
16862
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Feature priority"),
|
|
16820
16863
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
16821
16864
|
},
|
|
@@ -16824,7 +16867,8 @@ function createFeatureTools(store) {
|
|
|
16824
16867
|
title: args.title,
|
|
16825
16868
|
status: args.status ?? "draft"
|
|
16826
16869
|
};
|
|
16827
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
16870
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
16871
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
16828
16872
|
if (args.priority) frontmatter.priority = args.priority;
|
|
16829
16873
|
if (args.tags) frontmatter.tags = args.tags;
|
|
16830
16874
|
const doc = store.create("feature", frontmatter, args.content);
|
|
@@ -16846,12 +16890,15 @@ function createFeatureTools(store) {
|
|
|
16846
16890
|
title: external_exports.string().optional().describe("New title"),
|
|
16847
16891
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
|
|
16848
16892
|
content: external_exports.string().optional().describe("New content"),
|
|
16849
|
-
owner:
|
|
16893
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
16894
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
16850
16895
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
16851
16896
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
16852
16897
|
},
|
|
16853
16898
|
async (args) => {
|
|
16854
|
-
const { id, content, ...updates } = args;
|
|
16899
|
+
const { id, content, owner, assignee, ...updates } = args;
|
|
16900
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
16901
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
16855
16902
|
const doc = store.update(id, updates, content);
|
|
16856
16903
|
return {
|
|
16857
16904
|
content: [
|
|
@@ -16949,7 +16996,8 @@ function createEpicTools(store) {
|
|
|
16949
16996
|
content: external_exports.string().describe("Epic description and scope"),
|
|
16950
16997
|
linkedFeature: linkedFeatureArray.describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
|
|
16951
16998
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
|
|
16952
|
-
owner:
|
|
16999
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
17000
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
16953
17001
|
targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
|
|
16954
17002
|
estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g. '2 weeks', '5 story points')"),
|
|
16955
17003
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
|
|
@@ -16998,7 +17046,8 @@ function createEpicTools(store) {
|
|
|
16998
17046
|
linkedFeature: linkedFeatures,
|
|
16999
17047
|
tags: [...generateFeatureTags(linkedFeatures), ...args.tags ?? []]
|
|
17000
17048
|
};
|
|
17001
|
-
if (args.owner) frontmatter.owner = args.owner;
|
|
17049
|
+
if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
|
|
17050
|
+
if (args.assignee) frontmatter.assignee = args.assignee;
|
|
17002
17051
|
if (args.targetDate) frontmatter.targetDate = args.targetDate;
|
|
17003
17052
|
if (args.estimatedEffort) frontmatter.estimatedEffort = args.estimatedEffort;
|
|
17004
17053
|
const doc = store.create("epic", frontmatter, args.content);
|
|
@@ -17020,14 +17069,17 @@ function createEpicTools(store) {
|
|
|
17020
17069
|
title: external_exports.string().optional().describe("New title"),
|
|
17021
17070
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("New status"),
|
|
17022
17071
|
content: external_exports.string().optional().describe("New content"),
|
|
17023
|
-
owner:
|
|
17072
|
+
owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
|
|
17073
|
+
assignee: external_exports.string().optional().describe("Person assigned to do the work"),
|
|
17024
17074
|
targetDate: external_exports.string().optional().describe("New target date"),
|
|
17025
17075
|
estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
|
|
17026
17076
|
linkedFeature: linkedFeatureArray.optional().describe("New linked feature ID(s)"),
|
|
17027
17077
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
17028
17078
|
},
|
|
17029
17079
|
async (args) => {
|
|
17030
|
-
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, ...updates } = args;
|
|
17080
|
+
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, owner, assignee, ...updates } = args;
|
|
17081
|
+
if (owner !== void 0) updates.owner = normalizeOwner(owner);
|
|
17082
|
+
if (assignee !== void 0) updates.assignee = assignee;
|
|
17031
17083
|
if (rawLinkedFeature !== void 0) {
|
|
17032
17084
|
const linkedFeatures = normalizeLinkedFeatures(rawLinkedFeature);
|
|
17033
17085
|
for (const featureId of linkedFeatures) {
|
|
@@ -17154,7 +17206,7 @@ function createContributionTools(store) {
|
|
|
17154
17206
|
aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
|
|
17155
17207
|
status: external_exports.string().optional().describe("Status (default: 'done')"),
|
|
17156
17208
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
17157
|
-
|
|
17209
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag."),
|
|
17158
17210
|
parentProgress: external_exports.number().optional().describe("Set progress (0-100) on the parent artifact (e.g. task or action). Propagates up the hierarchy.")
|
|
17159
17211
|
},
|
|
17160
17212
|
async (args) => {
|
|
@@ -17166,7 +17218,7 @@ function createContributionTools(store) {
|
|
|
17166
17218
|
};
|
|
17167
17219
|
frontmatter.aboutArtifact = args.aboutArtifact;
|
|
17168
17220
|
const tags = [...args.tags ?? []];
|
|
17169
|
-
if (args.
|
|
17221
|
+
if (args.workFocus) tags.push(`focus:${args.workFocus}`);
|
|
17170
17222
|
if (tags.length > 0) frontmatter.tags = tags;
|
|
17171
17223
|
const doc = store.create("contribution", frontmatter, args.content);
|
|
17172
17224
|
const progressParts = [];
|
|
@@ -17222,15 +17274,15 @@ function createContributionTools(store) {
|
|
|
17222
17274
|
title: external_exports.string().optional().describe("New title"),
|
|
17223
17275
|
status: external_exports.string().optional().describe("New status"),
|
|
17224
17276
|
content: external_exports.string().optional().describe("New content"),
|
|
17225
|
-
|
|
17277
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag.")
|
|
17226
17278
|
},
|
|
17227
17279
|
async (args) => {
|
|
17228
|
-
const { id, content,
|
|
17229
|
-
if (
|
|
17280
|
+
const { id, content, workFocus, ...updates } = args;
|
|
17281
|
+
if (workFocus !== void 0) {
|
|
17230
17282
|
const existing = store.get(id);
|
|
17231
17283
|
const existingTags = existing?.frontmatter.tags ?? [];
|
|
17232
|
-
const filtered = existingTags.filter((t) => !t.startsWith("
|
|
17233
|
-
filtered.push(`
|
|
17284
|
+
const filtered = existingTags.filter((t) => !t.startsWith("focus:"));
|
|
17285
|
+
filtered.push(`focus:${workFocus}`);
|
|
17234
17286
|
updates.tags = filtered;
|
|
17235
17287
|
}
|
|
17236
17288
|
const oldDoc = store.get(id);
|
|
@@ -17713,7 +17765,7 @@ function createTaskTools(store) {
|
|
|
17713
17765
|
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
|
|
17714
17766
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
|
|
17715
17767
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags"),
|
|
17716
|
-
|
|
17768
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
|
|
17717
17769
|
},
|
|
17718
17770
|
async (args) => {
|
|
17719
17771
|
const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
|
|
@@ -17727,7 +17779,7 @@ function createTaskTools(store) {
|
|
|
17727
17779
|
}
|
|
17728
17780
|
}
|
|
17729
17781
|
const baseTags = [...generateEpicTags(linkedEpics), ...args.tags ?? []];
|
|
17730
|
-
if (args.
|
|
17782
|
+
if (args.workFocus) baseTags.push(`focus:${args.workFocus}`);
|
|
17731
17783
|
const frontmatter = {
|
|
17732
17784
|
title: args.title,
|
|
17733
17785
|
status: args.status ?? "backlog",
|
|
@@ -17768,11 +17820,11 @@ function createTaskTools(store) {
|
|
|
17768
17820
|
complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
|
|
17769
17821
|
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
17770
17822
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
|
|
17771
|
-
|
|
17823
|
+
workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
|
|
17772
17824
|
progress: external_exports.number().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions.")
|
|
17773
17825
|
},
|
|
17774
17826
|
async (args) => {
|
|
17775
|
-
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags,
|
|
17827
|
+
const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workFocus, progress, ...updates } = args;
|
|
17776
17828
|
const warnings = [];
|
|
17777
17829
|
if (rawLinkedEpic !== void 0) {
|
|
17778
17830
|
const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
|
|
@@ -17793,10 +17845,10 @@ function createTaskTools(store) {
|
|
|
17793
17845
|
} else if (userTags !== void 0) {
|
|
17794
17846
|
updates.tags = userTags;
|
|
17795
17847
|
}
|
|
17796
|
-
if (
|
|
17848
|
+
if (workFocus !== void 0) {
|
|
17797
17849
|
const currentTags = updates.tags ?? store.get(id)?.frontmatter.tags ?? [];
|
|
17798
|
-
const filtered = currentTags.filter((t) => !t.startsWith("
|
|
17799
|
-
filtered.push(`
|
|
17850
|
+
const filtered = currentTags.filter((t) => !t.startsWith("focus:"));
|
|
17851
|
+
filtered.push(`focus:${workFocus}`);
|
|
17800
17852
|
updates.tags = filtered;
|
|
17801
17853
|
}
|
|
17802
17854
|
if (typeof progress === "number") {
|
|
@@ -20023,7 +20075,7 @@ ${fragment}`);
|
|
|
20023
20075
|
}
|
|
20024
20076
|
|
|
20025
20077
|
// src/skills/action-tools.ts
|
|
20026
|
-
import { tool as
|
|
20078
|
+
import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
|
|
20027
20079
|
|
|
20028
20080
|
// src/skills/action-runner.ts
|
|
20029
20081
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -21833,6 +21885,39 @@ tr:hover td {
|
|
|
21833
21885
|
font-weight: 700;
|
|
21834
21886
|
color: var(--text);
|
|
21835
21887
|
}
|
|
21888
|
+
|
|
21889
|
+
/* Focus-grouped work items */
|
|
21890
|
+
.focus-row td:first-child {
|
|
21891
|
+
border-left: 3px solid var(--focus-color, var(--border));
|
|
21892
|
+
}
|
|
21893
|
+
|
|
21894
|
+
.focus-group-header td {
|
|
21895
|
+
background: var(--bg-hover);
|
|
21896
|
+
border-left: 3px solid var(--focus-color, var(--border));
|
|
21897
|
+
padding-top: 0.5rem;
|
|
21898
|
+
padding-bottom: 0.5rem;
|
|
21899
|
+
border-bottom: 1px solid var(--border);
|
|
21900
|
+
}
|
|
21901
|
+
|
|
21902
|
+
.focus-group-header td:first-child {
|
|
21903
|
+
border-left-width: 3px;
|
|
21904
|
+
}
|
|
21905
|
+
|
|
21906
|
+
.focus-group-name {
|
|
21907
|
+
font-weight: 600;
|
|
21908
|
+
font-size: 0.8rem;
|
|
21909
|
+
color: var(--text);
|
|
21910
|
+
margin-right: 0.75rem;
|
|
21911
|
+
}
|
|
21912
|
+
|
|
21913
|
+
.focus-group-stats {
|
|
21914
|
+
font-size: 0.75rem;
|
|
21915
|
+
color: var(--text-dim);
|
|
21916
|
+
}
|
|
21917
|
+
|
|
21918
|
+
.focus-group-progress {
|
|
21919
|
+
width: 96px;
|
|
21920
|
+
}
|
|
21836
21921
|
`;
|
|
21837
21922
|
}
|
|
21838
21923
|
|
|
@@ -23437,15 +23522,15 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23437
23522
|
</div>`,
|
|
23438
23523
|
{ titleTag: "h3" }
|
|
23439
23524
|
) : "";
|
|
23440
|
-
const
|
|
23441
|
-
"
|
|
23442
|
-
"
|
|
23443
|
-
"
|
|
23444
|
-
"
|
|
23445
|
-
"
|
|
23446
|
-
"
|
|
23447
|
-
"
|
|
23448
|
-
"
|
|
23525
|
+
const FOCUS_BORDER_PALETTE = [
|
|
23526
|
+
"hsl(220, 60%, 55%)",
|
|
23527
|
+
"hsl(160, 50%, 45%)",
|
|
23528
|
+
"hsl(280, 45%, 55%)",
|
|
23529
|
+
"hsl(30, 65%, 55%)",
|
|
23530
|
+
"hsl(340, 50%, 55%)",
|
|
23531
|
+
"hsl(190, 50%, 45%)",
|
|
23532
|
+
"hsl(60, 50%, 50%)",
|
|
23533
|
+
"hsl(120, 40%, 45%)"
|
|
23449
23534
|
];
|
|
23450
23535
|
function hashString(s) {
|
|
23451
23536
|
let h = 0;
|
|
@@ -23454,68 +23539,92 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23454
23539
|
}
|
|
23455
23540
|
return Math.abs(h);
|
|
23456
23541
|
}
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
|
|
23542
|
+
const focusGroups = /* @__PURE__ */ new Map();
|
|
23543
|
+
for (const item of data.workItems.items) {
|
|
23544
|
+
const focus = item.workFocus ?? "Unassigned";
|
|
23545
|
+
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
23546
|
+
focusGroups.get(focus).push(item);
|
|
23547
|
+
}
|
|
23548
|
+
const focusColorMap = /* @__PURE__ */ new Map();
|
|
23549
|
+
for (const name of focusGroups.keys()) {
|
|
23550
|
+
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
23551
|
+
}
|
|
23552
|
+
function countFocusStats(items) {
|
|
23553
|
+
let total = 0;
|
|
23554
|
+
let done = 0;
|
|
23555
|
+
let inProgress = 0;
|
|
23556
|
+
function walk(list) {
|
|
23557
|
+
for (const w of list) {
|
|
23558
|
+
if (w.type !== "contribution") {
|
|
23559
|
+
total++;
|
|
23560
|
+
const s = w.status.toLowerCase();
|
|
23561
|
+
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
23562
|
+
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
23563
|
+
}
|
|
23564
|
+
if (w.children) walk(w.children);
|
|
23463
23565
|
}
|
|
23464
23566
|
}
|
|
23465
|
-
|
|
23567
|
+
walk(items);
|
|
23568
|
+
return { total, done, inProgress };
|
|
23466
23569
|
}
|
|
23467
|
-
|
|
23468
|
-
const streamColorMap = /* @__PURE__ */ new Map();
|
|
23469
|
-
for (const name of uniqueStreams) {
|
|
23470
|
-
streamColorMap.set(name, STREAM_PALETTE[hashString(name) % STREAM_PALETTE.length]);
|
|
23471
|
-
}
|
|
23472
|
-
const streamStyleRules = [...streamColorMap.entries()].map(([name, color]) => `tr[data-stream="${escapeHtml(name)}"] td { background: ${color}; }`).join("\n");
|
|
23473
|
-
const streamStyleBlock = streamStyleRules ? `<style>${streamStyleRules}</style>` : "";
|
|
23474
|
-
function renderItemRows(items, depth = 0) {
|
|
23570
|
+
function renderItemRows(items, borderColor, depth = 0) {
|
|
23475
23571
|
return items.flatMap((w) => {
|
|
23476
23572
|
const isChild = depth > 0;
|
|
23477
23573
|
const isContribution = w.type === "contribution";
|
|
23478
|
-
const classes = [];
|
|
23574
|
+
const classes = ["focus-row"];
|
|
23479
23575
|
if (isContribution) classes.push("contribution-row");
|
|
23480
23576
|
else if (isChild) classes.push("child-row");
|
|
23481
|
-
const dataStream = w.workStream ? ` data-stream="${escapeHtml(w.workStream)}"` : "";
|
|
23482
|
-
const rowAttrs = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
|
|
23483
23577
|
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
23484
|
-
const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
|
|
23485
23578
|
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>` : "";
|
|
23486
23579
|
const row = `
|
|
23487
|
-
<tr${
|
|
23580
|
+
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
23488
23581
|
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
23489
23582
|
<td>${escapeHtml(w.title)}</td>
|
|
23490
|
-
<td>${streamCell}</td>
|
|
23491
|
-
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
23492
23583
|
<td>${statusBadge(w.status)}</td>
|
|
23493
23584
|
<td>${progressCell}</td>
|
|
23494
23585
|
</tr>`;
|
|
23495
|
-
const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
|
|
23586
|
+
const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
|
|
23496
23587
|
return [row, ...childRows];
|
|
23497
23588
|
});
|
|
23498
23589
|
}
|
|
23499
|
-
const
|
|
23500
|
-
const
|
|
23501
|
-
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23590
|
+
const allWorkItemRows = [];
|
|
23591
|
+
for (const [focus, items] of focusGroups) {
|
|
23592
|
+
const color = focusColorMap.get(focus);
|
|
23593
|
+
const stats = countFocusStats(items);
|
|
23594
|
+
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
23595
|
+
const summaryParts = [];
|
|
23596
|
+
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
23597
|
+
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
23598
|
+
const remaining = stats.total - stats.done - stats.inProgress;
|
|
23599
|
+
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
23600
|
+
allWorkItemRows.push(`
|
|
23601
|
+
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
23602
|
+
<td colspan="2">
|
|
23603
|
+
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
23604
|
+
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
23605
|
+
</td>
|
|
23606
|
+
<td colspan="2">
|
|
23607
|
+
<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>
|
|
23608
|
+
</td>
|
|
23609
|
+
</tr>`);
|
|
23610
|
+
allWorkItemRows.push(...renderItemRows(items, color));
|
|
23611
|
+
}
|
|
23612
|
+
const tableHeaders = `<tr>
|
|
23613
|
+
<th>ID</th>
|
|
23614
|
+
<th>Title</th>
|
|
23615
|
+
<th>Status</th>
|
|
23616
|
+
<th>Progress</th>
|
|
23507
23617
|
</tr>`;
|
|
23508
|
-
const workItemsSection =
|
|
23618
|
+
const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
|
|
23509
23619
|
"ss-work-items",
|
|
23510
23620
|
"Work Items",
|
|
23511
|
-
|
|
23512
|
-
<div class="table-wrap">
|
|
23621
|
+
`<div class="table-wrap">
|
|
23513
23622
|
<table id="work-items-table">
|
|
23514
23623
|
<thead>
|
|
23515
|
-
${
|
|
23624
|
+
${tableHeaders}
|
|
23516
23625
|
</thead>
|
|
23517
23626
|
<tbody>
|
|
23518
|
-
${
|
|
23627
|
+
${allWorkItemRows.join("")}
|
|
23519
23628
|
</tbody>
|
|
23520
23629
|
</table>
|
|
23521
23630
|
</div>`,
|
|
@@ -23591,61 +23700,6 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23591
23700
|
</div>
|
|
23592
23701
|
|
|
23593
23702
|
<script>
|
|
23594
|
-
var _sortCol = -1;
|
|
23595
|
-
var _sortAsc = true;
|
|
23596
|
-
|
|
23597
|
-
function sortWorkItems(col) {
|
|
23598
|
-
var table = document.getElementById('work-items-table');
|
|
23599
|
-
if (!table) return;
|
|
23600
|
-
var tbody = table.querySelector('tbody');
|
|
23601
|
-
var allRows = Array.from(tbody.querySelectorAll('tr'));
|
|
23602
|
-
|
|
23603
|
-
// Toggle direction if clicking the same column
|
|
23604
|
-
if (_sortCol === col) {
|
|
23605
|
-
_sortAsc = !_sortAsc;
|
|
23606
|
-
} else {
|
|
23607
|
-
_sortCol = col;
|
|
23608
|
-
_sortAsc = true;
|
|
23609
|
-
}
|
|
23610
|
-
|
|
23611
|
-
// Update sort arrows
|
|
23612
|
-
for (var i = 0; i < 6; i++) {
|
|
23613
|
-
var arrow = document.getElementById('sort-arrow-' + i);
|
|
23614
|
-
if (arrow) arrow.textContent = i === col ? (_sortAsc ? ' \\u25B2' : ' \\u25BC') : '';
|
|
23615
|
-
}
|
|
23616
|
-
|
|
23617
|
-
// Group rows: root rows + their child/contribution rows
|
|
23618
|
-
var groups = [];
|
|
23619
|
-
var current = null;
|
|
23620
|
-
for (var r = 0; r < allRows.length; r++) {
|
|
23621
|
-
var row = allRows[r];
|
|
23622
|
-
var isChild = row.classList.contains('child-row') || row.classList.contains('contribution-row');
|
|
23623
|
-
if (!isChild) {
|
|
23624
|
-
current = { root: row, children: [] };
|
|
23625
|
-
groups.push(current);
|
|
23626
|
-
} else if (current) {
|
|
23627
|
-
current.children.push(row);
|
|
23628
|
-
}
|
|
23629
|
-
}
|
|
23630
|
-
|
|
23631
|
-
// Sort groups by root row text content of target column
|
|
23632
|
-
groups.sort(function(a, b) {
|
|
23633
|
-
var aText = (a.root.children[col] ? a.root.children[col].textContent : '').trim().toLowerCase();
|
|
23634
|
-
var bText = (b.root.children[col] ? b.root.children[col].textContent : '').trim().toLowerCase();
|
|
23635
|
-
if (aText < bText) return _sortAsc ? -1 : 1;
|
|
23636
|
-
if (aText > bText) return _sortAsc ? 1 : -1;
|
|
23637
|
-
return 0;
|
|
23638
|
-
});
|
|
23639
|
-
|
|
23640
|
-
// Re-append rows in sorted order
|
|
23641
|
-
for (var g = 0; g < groups.length; g++) {
|
|
23642
|
-
tbody.appendChild(groups[g].root);
|
|
23643
|
-
for (var c = 0; c < groups[g].children.length; c++) {
|
|
23644
|
-
tbody.appendChild(groups[g].children[c]);
|
|
23645
|
-
}
|
|
23646
|
-
}
|
|
23647
|
-
}
|
|
23648
|
-
|
|
23649
23703
|
async function generateSummary() {
|
|
23650
23704
|
var btn = document.getElementById('generate-btn');
|
|
23651
23705
|
var loading = document.getElementById('summary-loading');
|
|
@@ -24436,7 +24490,7 @@ function tlSprintPage(ctx) {
|
|
|
24436
24490
|
`<div class="table-wrap">
|
|
24437
24491
|
<table>
|
|
24438
24492
|
<thead>
|
|
24439
|
-
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>
|
|
24493
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
|
|
24440
24494
|
</thead>
|
|
24441
24495
|
<tbody>
|
|
24442
24496
|
${techItems.map((w) => `
|
|
@@ -24445,7 +24499,7 @@ function tlSprintPage(ctx) {
|
|
|
24445
24499
|
<td>${escapeHtml(w.title)}</td>
|
|
24446
24500
|
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
24447
24501
|
<td>${statusBadge(w.status)}</td>
|
|
24448
|
-
<td>${w.
|
|
24502
|
+
<td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
24449
24503
|
</tr>`).join("")}
|
|
24450
24504
|
</tbody>
|
|
24451
24505
|
</table>
|
|
@@ -25331,6 +25385,556 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
25331
25385
|
];
|
|
25332
25386
|
}
|
|
25333
25387
|
|
|
25388
|
+
// src/agent/tools/doctor.ts
|
|
25389
|
+
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
25390
|
+
|
|
25391
|
+
// src/doctor/rules/tag-migration.ts
|
|
25392
|
+
var RULE_ID = "tag-migration";
|
|
25393
|
+
var RULE_NAME = "Tag Migration";
|
|
25394
|
+
var tagMigrationRule = {
|
|
25395
|
+
id: RULE_ID,
|
|
25396
|
+
name: RULE_NAME,
|
|
25397
|
+
description: "Detects deprecated stream:* tags and replaces them with focus:*",
|
|
25398
|
+
scan(ctx) {
|
|
25399
|
+
const issues = [];
|
|
25400
|
+
for (const doc of ctx.allDocuments) {
|
|
25401
|
+
const tags = doc.frontmatter.tags;
|
|
25402
|
+
if (!Array.isArray(tags)) continue;
|
|
25403
|
+
const streamTags = tags.filter((t) => t.startsWith("stream:"));
|
|
25404
|
+
for (const tag of streamTags) {
|
|
25405
|
+
issues.push({
|
|
25406
|
+
ruleId: RULE_ID,
|
|
25407
|
+
ruleName: RULE_NAME,
|
|
25408
|
+
documentId: doc.frontmatter.id,
|
|
25409
|
+
filePath: doc.filePath,
|
|
25410
|
+
documentType: doc.frontmatter.type,
|
|
25411
|
+
message: `Deprecated tag "${tag}" should be "${tag.replace("stream:", "focus:")}"`,
|
|
25412
|
+
severity: "warning",
|
|
25413
|
+
fixable: true
|
|
25414
|
+
});
|
|
25415
|
+
}
|
|
25416
|
+
}
|
|
25417
|
+
return issues;
|
|
25418
|
+
},
|
|
25419
|
+
fix(ctx) {
|
|
25420
|
+
const fixes = [];
|
|
25421
|
+
for (const doc of ctx.allDocuments) {
|
|
25422
|
+
const tags = doc.frontmatter.tags;
|
|
25423
|
+
if (!Array.isArray(tags)) continue;
|
|
25424
|
+
const streamTags = tags.filter((t) => t.startsWith("stream:"));
|
|
25425
|
+
if (streamTags.length === 0) continue;
|
|
25426
|
+
const newTags = tags.map(
|
|
25427
|
+
(t) => t.startsWith("stream:") ? t.replace("stream:", "focus:") : t
|
|
25428
|
+
);
|
|
25429
|
+
ctx.store.update(doc.frontmatter.id, { tags: newTags });
|
|
25430
|
+
for (const tag of streamTags) {
|
|
25431
|
+
fixes.push({
|
|
25432
|
+
issue: {
|
|
25433
|
+
ruleId: RULE_ID,
|
|
25434
|
+
ruleName: RULE_NAME,
|
|
25435
|
+
documentId: doc.frontmatter.id,
|
|
25436
|
+
filePath: doc.filePath,
|
|
25437
|
+
documentType: doc.frontmatter.type,
|
|
25438
|
+
message: `Deprecated tag "${tag}" should be "${tag.replace("stream:", "focus:")}"`,
|
|
25439
|
+
severity: "warning",
|
|
25440
|
+
fixable: true
|
|
25441
|
+
},
|
|
25442
|
+
fixDescription: `Renamed "${tag}" to "${tag.replace("stream:", "focus:")}"`
|
|
25443
|
+
});
|
|
25444
|
+
}
|
|
25445
|
+
}
|
|
25446
|
+
return fixes;
|
|
25447
|
+
}
|
|
25448
|
+
};
|
|
25449
|
+
|
|
25450
|
+
// src/doctor/rules/array-normalization.ts
|
|
25451
|
+
var RULE_ID2 = "array-normalization";
|
|
25452
|
+
var RULE_NAME2 = "Array Normalization";
|
|
25453
|
+
var FIELDS = [
|
|
25454
|
+
{
|
|
25455
|
+
field: "linkedEpic",
|
|
25456
|
+
aliases: ["linkedEpics"],
|
|
25457
|
+
normalize: normalizeLinkedEpics
|
|
25458
|
+
},
|
|
25459
|
+
{
|
|
25460
|
+
field: "linkedFeature",
|
|
25461
|
+
aliases: ["linkedFeatures"],
|
|
25462
|
+
normalize: normalizeLinkedFeatures
|
|
25463
|
+
}
|
|
25464
|
+
];
|
|
25465
|
+
var arrayNormalizationRule = {
|
|
25466
|
+
id: RULE_ID2,
|
|
25467
|
+
name: RULE_NAME2,
|
|
25468
|
+
description: "Normalizes linkedEpic/linkedFeature from strings to arrays and resolves field aliases",
|
|
25469
|
+
scan(ctx) {
|
|
25470
|
+
const issues = [];
|
|
25471
|
+
for (const doc of ctx.allDocuments) {
|
|
25472
|
+
const fm = doc.frontmatter;
|
|
25473
|
+
for (const cfg of FIELDS) {
|
|
25474
|
+
for (const alias of cfg.aliases) {
|
|
25475
|
+
if (fm[alias] !== void 0) {
|
|
25476
|
+
issues.push({
|
|
25477
|
+
ruleId: RULE_ID2,
|
|
25478
|
+
ruleName: RULE_NAME2,
|
|
25479
|
+
documentId: doc.frontmatter.id,
|
|
25480
|
+
filePath: doc.filePath,
|
|
25481
|
+
documentType: doc.frontmatter.type,
|
|
25482
|
+
message: `Field "${alias}" should be renamed to "${cfg.field}"`,
|
|
25483
|
+
severity: "warning",
|
|
25484
|
+
fixable: true
|
|
25485
|
+
});
|
|
25486
|
+
}
|
|
25487
|
+
}
|
|
25488
|
+
const value = fm[cfg.field];
|
|
25489
|
+
if (typeof value === "string") {
|
|
25490
|
+
issues.push({
|
|
25491
|
+
ruleId: RULE_ID2,
|
|
25492
|
+
ruleName: RULE_NAME2,
|
|
25493
|
+
documentId: doc.frontmatter.id,
|
|
25494
|
+
filePath: doc.filePath,
|
|
25495
|
+
documentType: doc.frontmatter.type,
|
|
25496
|
+
message: `Field "${cfg.field}" is a string ("${value}") but should be an array`,
|
|
25497
|
+
severity: "warning",
|
|
25498
|
+
fixable: true
|
|
25499
|
+
});
|
|
25500
|
+
}
|
|
25501
|
+
}
|
|
25502
|
+
}
|
|
25503
|
+
return issues;
|
|
25504
|
+
},
|
|
25505
|
+
fix(ctx) {
|
|
25506
|
+
const fixes = [];
|
|
25507
|
+
for (const doc of ctx.allDocuments) {
|
|
25508
|
+
const fm = doc.frontmatter;
|
|
25509
|
+
const updates = {};
|
|
25510
|
+
let needsUpdate = false;
|
|
25511
|
+
for (const cfg of FIELDS) {
|
|
25512
|
+
for (const alias of cfg.aliases) {
|
|
25513
|
+
if (fm[alias] !== void 0) {
|
|
25514
|
+
const aliasValues = cfg.normalize(fm[alias]);
|
|
25515
|
+
const existing = cfg.normalize(fm[cfg.field]);
|
|
25516
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...aliasValues])];
|
|
25517
|
+
updates[cfg.field] = merged;
|
|
25518
|
+
updates[alias] = void 0;
|
|
25519
|
+
needsUpdate = true;
|
|
25520
|
+
fixes.push({
|
|
25521
|
+
issue: {
|
|
25522
|
+
ruleId: RULE_ID2,
|
|
25523
|
+
ruleName: RULE_NAME2,
|
|
25524
|
+
documentId: doc.frontmatter.id,
|
|
25525
|
+
filePath: doc.filePath,
|
|
25526
|
+
documentType: doc.frontmatter.type,
|
|
25527
|
+
message: `Field "${alias}" should be renamed to "${cfg.field}"`,
|
|
25528
|
+
severity: "warning",
|
|
25529
|
+
fixable: true
|
|
25530
|
+
},
|
|
25531
|
+
fixDescription: `Merged "${alias}" into "${cfg.field}" and removed alias`
|
|
25532
|
+
});
|
|
25533
|
+
}
|
|
25534
|
+
}
|
|
25535
|
+
const value = updates[cfg.field] ?? fm[cfg.field];
|
|
25536
|
+
if (typeof value === "string") {
|
|
25537
|
+
updates[cfg.field] = cfg.normalize(value);
|
|
25538
|
+
needsUpdate = true;
|
|
25539
|
+
fixes.push({
|
|
25540
|
+
issue: {
|
|
25541
|
+
ruleId: RULE_ID2,
|
|
25542
|
+
ruleName: RULE_NAME2,
|
|
25543
|
+
documentId: doc.frontmatter.id,
|
|
25544
|
+
filePath: doc.filePath,
|
|
25545
|
+
documentType: doc.frontmatter.type,
|
|
25546
|
+
message: `Field "${cfg.field}" is a string ("${value}") but should be an array`,
|
|
25547
|
+
severity: "warning",
|
|
25548
|
+
fixable: true
|
|
25549
|
+
},
|
|
25550
|
+
fixDescription: `Normalized "${cfg.field}" from string to array`
|
|
25551
|
+
});
|
|
25552
|
+
}
|
|
25553
|
+
}
|
|
25554
|
+
if (needsUpdate) {
|
|
25555
|
+
ctx.store.update(doc.frontmatter.id, updates);
|
|
25556
|
+
}
|
|
25557
|
+
}
|
|
25558
|
+
return fixes;
|
|
25559
|
+
}
|
|
25560
|
+
};
|
|
25561
|
+
|
|
25562
|
+
// src/doctor/rules/missing-auto-tags.ts
|
|
25563
|
+
var RULE_ID3 = "missing-auto-tags";
|
|
25564
|
+
var RULE_NAME3 = "Missing Auto Tags";
|
|
25565
|
+
var missingAutoTagsRule = {
|
|
25566
|
+
id: RULE_ID3,
|
|
25567
|
+
name: RULE_NAME3,
|
|
25568
|
+
description: "Ensures tasks have epic:E-xxx tags for their linkedEpic and epics have feature:F-xxx tags",
|
|
25569
|
+
scan(ctx) {
|
|
25570
|
+
const issues = [];
|
|
25571
|
+
for (const doc of ctx.allDocuments) {
|
|
25572
|
+
const fm = doc.frontmatter;
|
|
25573
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
25574
|
+
const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
|
|
25575
|
+
if (linkedEpics.length > 0) {
|
|
25576
|
+
const expected = generateEpicTags(linkedEpics);
|
|
25577
|
+
const missing = expected.filter((t) => !tags.includes(t));
|
|
25578
|
+
for (const tag of missing) {
|
|
25579
|
+
issues.push({
|
|
25580
|
+
ruleId: RULE_ID3,
|
|
25581
|
+
ruleName: RULE_NAME3,
|
|
25582
|
+
documentId: doc.frontmatter.id,
|
|
25583
|
+
filePath: doc.filePath,
|
|
25584
|
+
documentType: doc.frontmatter.type,
|
|
25585
|
+
message: `Missing auto-tag "${tag}" for linkedEpic`,
|
|
25586
|
+
severity: "warning",
|
|
25587
|
+
fixable: true
|
|
25588
|
+
});
|
|
25589
|
+
}
|
|
25590
|
+
}
|
|
25591
|
+
const linkedFeatures = normalizeLinkedFeatures(fm.linkedFeature);
|
|
25592
|
+
if (linkedFeatures.length > 0) {
|
|
25593
|
+
const expected = generateFeatureTags(linkedFeatures);
|
|
25594
|
+
const missing = expected.filter((t) => !tags.includes(t));
|
|
25595
|
+
for (const tag of missing) {
|
|
25596
|
+
issues.push({
|
|
25597
|
+
ruleId: RULE_ID3,
|
|
25598
|
+
ruleName: RULE_NAME3,
|
|
25599
|
+
documentId: doc.frontmatter.id,
|
|
25600
|
+
filePath: doc.filePath,
|
|
25601
|
+
documentType: doc.frontmatter.type,
|
|
25602
|
+
message: `Missing auto-tag "${tag}" for linkedFeature`,
|
|
25603
|
+
severity: "warning",
|
|
25604
|
+
fixable: true
|
|
25605
|
+
});
|
|
25606
|
+
}
|
|
25607
|
+
}
|
|
25608
|
+
}
|
|
25609
|
+
return issues;
|
|
25610
|
+
},
|
|
25611
|
+
fix(ctx) {
|
|
25612
|
+
const fixes = [];
|
|
25613
|
+
for (const doc of ctx.allDocuments) {
|
|
25614
|
+
const fm = doc.frontmatter;
|
|
25615
|
+
const tags = [...doc.frontmatter.tags ?? []];
|
|
25616
|
+
let changed = false;
|
|
25617
|
+
const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
|
|
25618
|
+
if (linkedEpics.length > 0) {
|
|
25619
|
+
const expected = generateEpicTags(linkedEpics);
|
|
25620
|
+
for (const tag of expected) {
|
|
25621
|
+
if (!tags.includes(tag)) {
|
|
25622
|
+
tags.push(tag);
|
|
25623
|
+
changed = true;
|
|
25624
|
+
fixes.push({
|
|
25625
|
+
issue: {
|
|
25626
|
+
ruleId: RULE_ID3,
|
|
25627
|
+
ruleName: RULE_NAME3,
|
|
25628
|
+
documentId: doc.frontmatter.id,
|
|
25629
|
+
filePath: doc.filePath,
|
|
25630
|
+
documentType: doc.frontmatter.type,
|
|
25631
|
+
message: `Missing auto-tag "${tag}" for linkedEpic`,
|
|
25632
|
+
severity: "warning",
|
|
25633
|
+
fixable: true
|
|
25634
|
+
},
|
|
25635
|
+
fixDescription: `Added tag "${tag}"`
|
|
25636
|
+
});
|
|
25637
|
+
}
|
|
25638
|
+
}
|
|
25639
|
+
}
|
|
25640
|
+
const linkedFeatures = normalizeLinkedFeatures(fm.linkedFeature);
|
|
25641
|
+
if (linkedFeatures.length > 0) {
|
|
25642
|
+
const expected = generateFeatureTags(linkedFeatures);
|
|
25643
|
+
for (const tag of expected) {
|
|
25644
|
+
if (!tags.includes(tag)) {
|
|
25645
|
+
tags.push(tag);
|
|
25646
|
+
changed = true;
|
|
25647
|
+
fixes.push({
|
|
25648
|
+
issue: {
|
|
25649
|
+
ruleId: RULE_ID3,
|
|
25650
|
+
ruleName: RULE_NAME3,
|
|
25651
|
+
documentId: doc.frontmatter.id,
|
|
25652
|
+
filePath: doc.filePath,
|
|
25653
|
+
documentType: doc.frontmatter.type,
|
|
25654
|
+
message: `Missing auto-tag "${tag}" for linkedFeature`,
|
|
25655
|
+
severity: "warning",
|
|
25656
|
+
fixable: true
|
|
25657
|
+
},
|
|
25658
|
+
fixDescription: `Added tag "${tag}"`
|
|
25659
|
+
});
|
|
25660
|
+
}
|
|
25661
|
+
}
|
|
25662
|
+
}
|
|
25663
|
+
if (changed) {
|
|
25664
|
+
ctx.store.update(doc.frontmatter.id, { tags });
|
|
25665
|
+
}
|
|
25666
|
+
}
|
|
25667
|
+
return fixes;
|
|
25668
|
+
}
|
|
25669
|
+
};
|
|
25670
|
+
|
|
25671
|
+
// src/doctor/rules/progress-consistency.ts
|
|
25672
|
+
var RULE_ID4 = "progress-consistency";
|
|
25673
|
+
var RULE_NAME4 = "Progress Consistency";
|
|
25674
|
+
var progressConsistencyRule = {
|
|
25675
|
+
id: RULE_ID4,
|
|
25676
|
+
name: RULE_NAME4,
|
|
25677
|
+
description: "Detects done-status documents with progress != 100 and progressOverride:true without a progress value",
|
|
25678
|
+
scan(ctx) {
|
|
25679
|
+
const issues = [];
|
|
25680
|
+
for (const doc of ctx.allDocuments) {
|
|
25681
|
+
const fm = doc.frontmatter;
|
|
25682
|
+
const status = doc.frontmatter.status;
|
|
25683
|
+
const progress = fm.progress;
|
|
25684
|
+
const progressOverride = fm.progressOverride;
|
|
25685
|
+
if (status === "done" && progress !== void 0 && progress !== 100) {
|
|
25686
|
+
issues.push({
|
|
25687
|
+
ruleId: RULE_ID4,
|
|
25688
|
+
ruleName: RULE_NAME4,
|
|
25689
|
+
documentId: doc.frontmatter.id,
|
|
25690
|
+
filePath: doc.filePath,
|
|
25691
|
+
documentType: doc.frontmatter.type,
|
|
25692
|
+
message: `Status is "done" but progress is ${progress} (expected 100)`,
|
|
25693
|
+
severity: "error",
|
|
25694
|
+
fixable: true
|
|
25695
|
+
});
|
|
25696
|
+
}
|
|
25697
|
+
if (progressOverride === true && progress === void 0) {
|
|
25698
|
+
issues.push({
|
|
25699
|
+
ruleId: RULE_ID4,
|
|
25700
|
+
ruleName: RULE_NAME4,
|
|
25701
|
+
documentId: doc.frontmatter.id,
|
|
25702
|
+
filePath: doc.filePath,
|
|
25703
|
+
documentType: doc.frontmatter.type,
|
|
25704
|
+
message: `progressOverride is true but no progress value is set`,
|
|
25705
|
+
severity: "warning",
|
|
25706
|
+
fixable: true
|
|
25707
|
+
});
|
|
25708
|
+
}
|
|
25709
|
+
}
|
|
25710
|
+
return issues;
|
|
25711
|
+
},
|
|
25712
|
+
fix(ctx) {
|
|
25713
|
+
const fixes = [];
|
|
25714
|
+
for (const doc of ctx.allDocuments) {
|
|
25715
|
+
const fm = doc.frontmatter;
|
|
25716
|
+
const status = doc.frontmatter.status;
|
|
25717
|
+
const progress = fm.progress;
|
|
25718
|
+
const progressOverride = fm.progressOverride;
|
|
25719
|
+
if (status === "done" && progress !== void 0 && progress !== 100) {
|
|
25720
|
+
ctx.store.update(doc.frontmatter.id, { progress: 100 });
|
|
25721
|
+
fixes.push({
|
|
25722
|
+
issue: {
|
|
25723
|
+
ruleId: RULE_ID4,
|
|
25724
|
+
ruleName: RULE_NAME4,
|
|
25725
|
+
documentId: doc.frontmatter.id,
|
|
25726
|
+
filePath: doc.filePath,
|
|
25727
|
+
documentType: doc.frontmatter.type,
|
|
25728
|
+
message: `Status is "done" but progress is ${progress} (expected 100)`,
|
|
25729
|
+
severity: "error",
|
|
25730
|
+
fixable: true
|
|
25731
|
+
},
|
|
25732
|
+
fixDescription: `Set progress to 100`
|
|
25733
|
+
});
|
|
25734
|
+
}
|
|
25735
|
+
if (progressOverride === true && progress === void 0) {
|
|
25736
|
+
ctx.store.update(doc.frontmatter.id, { progressOverride: false });
|
|
25737
|
+
fixes.push({
|
|
25738
|
+
issue: {
|
|
25739
|
+
ruleId: RULE_ID4,
|
|
25740
|
+
ruleName: RULE_NAME4,
|
|
25741
|
+
documentId: doc.frontmatter.id,
|
|
25742
|
+
filePath: doc.filePath,
|
|
25743
|
+
documentType: doc.frontmatter.type,
|
|
25744
|
+
message: `progressOverride is true but no progress value is set`,
|
|
25745
|
+
severity: "warning",
|
|
25746
|
+
fixable: true
|
|
25747
|
+
},
|
|
25748
|
+
fixDescription: `Set progressOverride to false`
|
|
25749
|
+
});
|
|
25750
|
+
}
|
|
25751
|
+
}
|
|
25752
|
+
return fixes;
|
|
25753
|
+
}
|
|
25754
|
+
};
|
|
25755
|
+
|
|
25756
|
+
// src/doctor/rules/orphaned-references.ts
|
|
25757
|
+
var RULE_ID5 = "orphaned-references";
|
|
25758
|
+
var RULE_NAME5 = "Orphaned References";
|
|
25759
|
+
var REFERENCE_FIELDS = ["aboutArtifact", "linkedEpic", "linkedFeature"];
|
|
25760
|
+
var orphanedReferencesRule = {
|
|
25761
|
+
id: RULE_ID5,
|
|
25762
|
+
name: RULE_NAME5,
|
|
25763
|
+
description: "Detects references (aboutArtifact, linkedEpic, linkedFeature) pointing to non-existent documents",
|
|
25764
|
+
scan(ctx) {
|
|
25765
|
+
const issues = [];
|
|
25766
|
+
for (const doc of ctx.allDocuments) {
|
|
25767
|
+
const fm = doc.frontmatter;
|
|
25768
|
+
for (const field of REFERENCE_FIELDS) {
|
|
25769
|
+
const value = fm[field];
|
|
25770
|
+
if (value === void 0 || value === null) continue;
|
|
25771
|
+
const refs = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" ? [value] : [];
|
|
25772
|
+
for (const ref of refs) {
|
|
25773
|
+
if (!ctx.documentIndex.has(ref)) {
|
|
25774
|
+
issues.push({
|
|
25775
|
+
ruleId: RULE_ID5,
|
|
25776
|
+
ruleName: RULE_NAME5,
|
|
25777
|
+
documentId: doc.frontmatter.id,
|
|
25778
|
+
filePath: doc.filePath,
|
|
25779
|
+
documentType: doc.frontmatter.type,
|
|
25780
|
+
message: `Field "${field}" references "${ref}" which does not exist`,
|
|
25781
|
+
severity: "warning",
|
|
25782
|
+
fixable: false
|
|
25783
|
+
});
|
|
25784
|
+
}
|
|
25785
|
+
}
|
|
25786
|
+
}
|
|
25787
|
+
}
|
|
25788
|
+
return issues;
|
|
25789
|
+
},
|
|
25790
|
+
fix() {
|
|
25791
|
+
return [];
|
|
25792
|
+
}
|
|
25793
|
+
};
|
|
25794
|
+
|
|
25795
|
+
// src/doctor/rules/owner-role.ts
|
|
25796
|
+
var RULE_ID6 = "owner-role";
|
|
25797
|
+
var RULE_NAME6 = "Owner Role";
|
|
25798
|
+
var ownerRoleRule = {
|
|
25799
|
+
id: RULE_ID6,
|
|
25800
|
+
name: RULE_NAME6,
|
|
25801
|
+
description: `Detects owner values that are not valid persona roles (${OWNER_SHORT.join(", ")})`,
|
|
25802
|
+
scan(ctx) {
|
|
25803
|
+
const issues = [];
|
|
25804
|
+
for (const doc of ctx.allDocuments) {
|
|
25805
|
+
const owner = doc.frontmatter.owner;
|
|
25806
|
+
if (owner === void 0 || owner === null || owner === "") continue;
|
|
25807
|
+
if (!isValidOwner(owner)) {
|
|
25808
|
+
issues.push({
|
|
25809
|
+
ruleId: RULE_ID6,
|
|
25810
|
+
ruleName: RULE_NAME6,
|
|
25811
|
+
documentId: doc.frontmatter.id,
|
|
25812
|
+
filePath: doc.filePath,
|
|
25813
|
+
documentType: doc.frontmatter.type,
|
|
25814
|
+
message: `Owner "${owner}" is not a valid persona role. Expected one of: ${OWNER_SHORT.join(", ")}`,
|
|
25815
|
+
severity: "warning",
|
|
25816
|
+
fixable: false
|
|
25817
|
+
});
|
|
25818
|
+
}
|
|
25819
|
+
}
|
|
25820
|
+
return issues;
|
|
25821
|
+
},
|
|
25822
|
+
fix() {
|
|
25823
|
+
return [];
|
|
25824
|
+
}
|
|
25825
|
+
};
|
|
25826
|
+
|
|
25827
|
+
// src/doctor/rules/index.ts
|
|
25828
|
+
var allRules = [
|
|
25829
|
+
tagMigrationRule,
|
|
25830
|
+
arrayNormalizationRule,
|
|
25831
|
+
missingAutoTagsRule,
|
|
25832
|
+
progressConsistencyRule,
|
|
25833
|
+
orphanedReferencesRule,
|
|
25834
|
+
ownerRoleRule
|
|
25835
|
+
];
|
|
25836
|
+
|
|
25837
|
+
// src/doctor/engine.ts
|
|
25838
|
+
function buildDoctorContext(store) {
|
|
25839
|
+
const allDocuments = store.list();
|
|
25840
|
+
const documentIndex = new Map(
|
|
25841
|
+
allDocuments.map((doc) => [doc.frontmatter.id, doc])
|
|
25842
|
+
);
|
|
25843
|
+
return { store, allDocuments, documentIndex };
|
|
25844
|
+
}
|
|
25845
|
+
function runDoctorScan(store, ruleFilter) {
|
|
25846
|
+
const rules = resolveRules(ruleFilter);
|
|
25847
|
+
const ctx = buildDoctorContext(store);
|
|
25848
|
+
const issues = rules.flatMap((rule) => rule.scan(ctx));
|
|
25849
|
+
return buildReport(ctx, issues, []);
|
|
25850
|
+
}
|
|
25851
|
+
function runDoctorFix(store, ruleFilter) {
|
|
25852
|
+
const rules = resolveRules(ruleFilter);
|
|
25853
|
+
let ctx = buildDoctorContext(store);
|
|
25854
|
+
const allIssues = rules.flatMap((rule) => rule.scan(ctx));
|
|
25855
|
+
const allFixes = [];
|
|
25856
|
+
for (const rule of rules) {
|
|
25857
|
+
const fixes = rule.fix(ctx);
|
|
25858
|
+
allFixes.push(...fixes);
|
|
25859
|
+
if (fixes.length > 0) {
|
|
25860
|
+
ctx = buildDoctorContext(store);
|
|
25861
|
+
}
|
|
25862
|
+
}
|
|
25863
|
+
return buildReport(ctx, allIssues, allFixes);
|
|
25864
|
+
}
|
|
25865
|
+
function resolveRules(ruleFilter) {
|
|
25866
|
+
if (!ruleFilter) return allRules;
|
|
25867
|
+
const rule = allRules.find((r) => r.id === ruleFilter);
|
|
25868
|
+
if (!rule) {
|
|
25869
|
+
throw new Error(
|
|
25870
|
+
`Unknown rule: ${ruleFilter}. Available: ${allRules.map((r) => r.id).join(", ")}`
|
|
25871
|
+
);
|
|
25872
|
+
}
|
|
25873
|
+
return [rule];
|
|
25874
|
+
}
|
|
25875
|
+
function buildReport(ctx, issues, fixes) {
|
|
25876
|
+
const byRule = {};
|
|
25877
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
25878
|
+
let fixableIssues = 0;
|
|
25879
|
+
for (const issue2 of issues) {
|
|
25880
|
+
byRule[issue2.ruleId] = (byRule[issue2.ruleId] ?? 0) + 1;
|
|
25881
|
+
bySeverity[issue2.severity] = (bySeverity[issue2.severity] ?? 0) + 1;
|
|
25882
|
+
if (issue2.fixable) fixableIssues++;
|
|
25883
|
+
}
|
|
25884
|
+
return {
|
|
25885
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25886
|
+
totalDocuments: ctx.allDocuments.length,
|
|
25887
|
+
issues,
|
|
25888
|
+
fixes,
|
|
25889
|
+
summary: {
|
|
25890
|
+
totalIssues: issues.length,
|
|
25891
|
+
fixableIssues,
|
|
25892
|
+
fixedIssues: fixes.length,
|
|
25893
|
+
byRule,
|
|
25894
|
+
bySeverity
|
|
25895
|
+
}
|
|
25896
|
+
};
|
|
25897
|
+
}
|
|
25898
|
+
|
|
25899
|
+
// src/agent/tools/doctor.ts
|
|
25900
|
+
function createDoctorTools(store) {
|
|
25901
|
+
return [
|
|
25902
|
+
tool23(
|
|
25903
|
+
"run_doctor",
|
|
25904
|
+
"Scan project documents for structural issues and optionally auto-repair them. Returns a JSON report with all issues found and fixes applied.",
|
|
25905
|
+
{
|
|
25906
|
+
fix: external_exports.boolean().optional().default(false).describe("When true, auto-repair fixable issues"),
|
|
25907
|
+
rule: external_exports.string().optional().describe(
|
|
25908
|
+
"Run only a specific rule (e.g. tag-migration, array-normalization, missing-auto-tags, progress-consistency, orphaned-references)"
|
|
25909
|
+
)
|
|
25910
|
+
},
|
|
25911
|
+
async (args) => {
|
|
25912
|
+
try {
|
|
25913
|
+
const report = args.fix ? runDoctorFix(store, args.rule) : runDoctorScan(store, args.rule);
|
|
25914
|
+
return {
|
|
25915
|
+
content: [
|
|
25916
|
+
{
|
|
25917
|
+
type: "text",
|
|
25918
|
+
text: JSON.stringify(report, null, 2)
|
|
25919
|
+
}
|
|
25920
|
+
]
|
|
25921
|
+
};
|
|
25922
|
+
} catch (err) {
|
|
25923
|
+
return {
|
|
25924
|
+
content: [
|
|
25925
|
+
{
|
|
25926
|
+
type: "text",
|
|
25927
|
+
text: `Doctor error: ${err instanceof Error ? err.message : String(err)}`
|
|
25928
|
+
}
|
|
25929
|
+
],
|
|
25930
|
+
isError: true
|
|
25931
|
+
};
|
|
25932
|
+
}
|
|
25933
|
+
}
|
|
25934
|
+
)
|
|
25935
|
+
];
|
|
25936
|
+
}
|
|
25937
|
+
|
|
25334
25938
|
// src/agent/mcp-server.ts
|
|
25335
25939
|
function createMarvinMcpServer(store, options) {
|
|
25336
25940
|
const tools = [
|
|
@@ -25342,7 +25946,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
25342
25946
|
...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
|
|
25343
25947
|
...options?.pluginTools ?? [],
|
|
25344
25948
|
...options?.skillTools ?? [],
|
|
25345
|
-
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
|
|
25949
|
+
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : [],
|
|
25950
|
+
...createDoctorTools(store)
|
|
25346
25951
|
];
|
|
25347
25952
|
return createSdkMcpServer({
|
|
25348
25953
|
name: "marvin-governance",
|
|
@@ -25414,7 +26019,7 @@ function createSkillActionTools(skills, context) {
|
|
|
25414
26019
|
if (!skill.actions) continue;
|
|
25415
26020
|
for (const action of skill.actions) {
|
|
25416
26021
|
tools.push(
|
|
25417
|
-
|
|
26022
|
+
tool24(
|
|
25418
26023
|
`${skill.id}__${action.id}`,
|
|
25419
26024
|
action.description,
|
|
25420
26025
|
{
|
|
@@ -25506,10 +26111,10 @@ ${lines.join("\n\n")}`;
|
|
|
25506
26111
|
}
|
|
25507
26112
|
|
|
25508
26113
|
// src/mcp/persona-tools.ts
|
|
25509
|
-
import { tool as
|
|
26114
|
+
import { tool as tool25 } from "@anthropic-ai/claude-agent-sdk";
|
|
25510
26115
|
function createPersonaTools(ctx, marvinDir) {
|
|
25511
26116
|
return [
|
|
25512
|
-
|
|
26117
|
+
tool25(
|
|
25513
26118
|
"set_persona",
|
|
25514
26119
|
"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.",
|
|
25515
26120
|
{
|
|
@@ -25539,7 +26144,7 @@ ${summaries}`
|
|
|
25539
26144
|
};
|
|
25540
26145
|
}
|
|
25541
26146
|
),
|
|
25542
|
-
|
|
26147
|
+
tool25(
|
|
25543
26148
|
"get_persona_guidance",
|
|
25544
26149
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
25545
26150
|
{
|