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/marvin.js CHANGED
@@ -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: external_exports.string().optional().describe("Meeting organizer"),
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: external_exports.string().optional().describe("New 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: [
@@ -14634,14 +14659,14 @@ function collectSprintSummaryData(store, sprintId) {
14634
14659
  const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
14635
14660
  for (const doc of workItemDocs) {
14636
14661
  const about = doc.frontmatter.aboutArtifact;
14637
- const streamTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("stream:"));
14662
+ const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
14638
14663
  const item = {
14639
14664
  id: doc.frontmatter.id,
14640
14665
  title: doc.frontmatter.title,
14641
14666
  type: doc.frontmatter.type,
14642
14667
  status: doc.frontmatter.status,
14643
14668
  progress: getEffectiveProgress(doc.frontmatter),
14644
- workStream: streamTag ? streamTag.slice(7) : void 0,
14669
+ workFocus: focusTag ? focusTag.slice(6) : void 0,
14645
14670
  aboutArtifact: about
14646
14671
  };
14647
14672
  allItemsById.set(item.id, item);
@@ -15796,7 +15821,8 @@ function createFeatureTools(store) {
15796
15821
  title: external_exports.string().describe("Feature title"),
15797
15822
  content: external_exports.string().describe("Feature description and requirements"),
15798
15823
  status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("Feature status (default: 'draft')"),
15799
- owner: external_exports.string().optional().describe("Feature 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"),
15800
15826
  priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Feature priority"),
15801
15827
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
15802
15828
  },
@@ -15805,7 +15831,8 @@ function createFeatureTools(store) {
15805
15831
  title: args.title,
15806
15832
  status: args.status ?? "draft"
15807
15833
  };
15808
- if (args.owner) frontmatter.owner = args.owner;
15834
+ if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
15835
+ if (args.assignee) frontmatter.assignee = args.assignee;
15809
15836
  if (args.priority) frontmatter.priority = args.priority;
15810
15837
  if (args.tags) frontmatter.tags = args.tags;
15811
15838
  const doc = store.create("feature", frontmatter, args.content);
@@ -15827,12 +15854,15 @@ function createFeatureTools(store) {
15827
15854
  title: external_exports.string().optional().describe("New title"),
15828
15855
  status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
15829
15856
  content: external_exports.string().optional().describe("New content"),
15830
- owner: external_exports.string().optional().describe("New 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"),
15831
15859
  priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
15832
15860
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
15833
15861
  },
15834
15862
  async (args) => {
15835
- 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;
15836
15866
  const doc = store.update(id, updates, content);
15837
15867
  return {
15838
15868
  content: [
@@ -15930,7 +15960,8 @@ function createEpicTools(store) {
15930
15960
  content: external_exports.string().describe("Epic description and scope"),
15931
15961
  linkedFeature: linkedFeatureArray.describe("Feature ID(s) to link this epic to (e.g. ['F-001'] or ['F-001', 'F-002'])"),
15932
15962
  status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
15933
- owner: external_exports.string().optional().describe("Epic 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"),
15934
15965
  targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
15935
15966
  estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g. '2 weeks', '5 story points')"),
15936
15967
  tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
@@ -15979,7 +16010,8 @@ function createEpicTools(store) {
15979
16010
  linkedFeature: linkedFeatures,
15980
16011
  tags: [...generateFeatureTags(linkedFeatures), ...args.tags ?? []]
15981
16012
  };
15982
- if (args.owner) frontmatter.owner = args.owner;
16013
+ if (args.owner) frontmatter.owner = normalizeOwner(args.owner);
16014
+ if (args.assignee) frontmatter.assignee = args.assignee;
15983
16015
  if (args.targetDate) frontmatter.targetDate = args.targetDate;
15984
16016
  if (args.estimatedEffort) frontmatter.estimatedEffort = args.estimatedEffort;
15985
16017
  const doc = store.create("epic", frontmatter, args.content);
@@ -16001,14 +16033,17 @@ function createEpicTools(store) {
16001
16033
  title: external_exports.string().optional().describe("New title"),
16002
16034
  status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("New status"),
16003
16035
  content: external_exports.string().optional().describe("New content"),
16004
- owner: external_exports.string().optional().describe("New 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"),
16005
16038
  targetDate: external_exports.string().optional().describe("New target date"),
16006
16039
  estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
16007
16040
  linkedFeature: linkedFeatureArray.optional().describe("New linked feature ID(s)"),
16008
16041
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
16009
16042
  },
16010
16043
  async (args) => {
16011
- 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;
16012
16047
  if (rawLinkedFeature !== void 0) {
16013
16048
  const linkedFeatures = normalizeLinkedFeatures(rawLinkedFeature);
16014
16049
  for (const featureId of linkedFeatures) {
@@ -16135,7 +16170,7 @@ function createContributionTools(store) {
16135
16170
  aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
16136
16171
  status: external_exports.string().optional().describe("Status (default: 'done')"),
16137
16172
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
16138
- workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Adds a stream:<value> tag."),
16173
+ workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag."),
16139
16174
  parentProgress: external_exports.number().optional().describe("Set progress (0-100) on the parent artifact (e.g. task or action). Propagates up the hierarchy.")
16140
16175
  },
16141
16176
  async (args) => {
@@ -16147,7 +16182,7 @@ function createContributionTools(store) {
16147
16182
  };
16148
16183
  frontmatter.aboutArtifact = args.aboutArtifact;
16149
16184
  const tags = [...args.tags ?? []];
16150
- if (args.workStream) tags.push(`stream:${args.workStream}`);
16185
+ if (args.workFocus) tags.push(`focus:${args.workFocus}`);
16151
16186
  if (tags.length > 0) frontmatter.tags = tags;
16152
16187
  const doc = store.create("contribution", frontmatter, args.content);
16153
16188
  const progressParts = [];
@@ -16203,15 +16238,15 @@ function createContributionTools(store) {
16203
16238
  title: external_exports.string().optional().describe("New title"),
16204
16239
  status: external_exports.string().optional().describe("New status"),
16205
16240
  content: external_exports.string().optional().describe("New content"),
16206
- workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Replaces existing stream:<value> tag.")
16241
+ workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag.")
16207
16242
  },
16208
16243
  async (args) => {
16209
- const { id, content, workStream, ...updates } = args;
16210
- if (workStream !== void 0) {
16244
+ const { id, content, workFocus, ...updates } = args;
16245
+ if (workFocus !== void 0) {
16211
16246
  const existing = store.get(id);
16212
16247
  const existingTags = existing?.frontmatter.tags ?? [];
16213
- const filtered = existingTags.filter((t) => !t.startsWith("stream:"));
16214
- filtered.push(`stream:${workStream}`);
16248
+ const filtered = existingTags.filter((t) => !t.startsWith("focus:"));
16249
+ filtered.push(`focus:${workFocus}`);
16215
16250
  updates.tags = filtered;
16216
16251
  }
16217
16252
  const oldDoc = store.get(id);
@@ -16694,7 +16729,7 @@ function createTaskTools(store) {
16694
16729
  complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
16695
16730
  priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
16696
16731
  tags: external_exports.array(external_exports.string()).optional().describe("Additional tags"),
16697
- workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Adds a stream:<value> tag.")
16732
+ workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
16698
16733
  },
16699
16734
  async (args) => {
16700
16735
  const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
@@ -16708,7 +16743,7 @@ function createTaskTools(store) {
16708
16743
  }
16709
16744
  }
16710
16745
  const baseTags = [...generateEpicTags(linkedEpics), ...args.tags ?? []];
16711
- if (args.workStream) baseTags.push(`stream:${args.workStream}`);
16746
+ if (args.workFocus) baseTags.push(`focus:${args.workFocus}`);
16712
16747
  const frontmatter = {
16713
16748
  title: args.title,
16714
16749
  status: args.status ?? "backlog",
@@ -16749,11 +16784,11 @@ function createTaskTools(store) {
16749
16784
  complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
16750
16785
  priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
16751
16786
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
16752
- workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Replaces existing stream:<value> tag."),
16787
+ workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
16753
16788
  progress: external_exports.number().optional().describe("Explicit progress percentage (0-100). Overrides auto-calculation from child contributions.")
16754
16789
  },
16755
16790
  async (args) => {
16756
- const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workStream, progress, ...updates } = args;
16791
+ const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workFocus, progress, ...updates } = args;
16757
16792
  const warnings = [];
16758
16793
  if (rawLinkedEpic !== void 0) {
16759
16794
  const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
@@ -16774,10 +16809,10 @@ function createTaskTools(store) {
16774
16809
  } else if (userTags !== void 0) {
16775
16810
  updates.tags = userTags;
16776
16811
  }
16777
- if (workStream !== void 0) {
16812
+ if (workFocus !== void 0) {
16778
16813
  const currentTags = updates.tags ?? store.get(id)?.frontmatter.tags ?? [];
16779
- const filtered = currentTags.filter((t) => !t.startsWith("stream:"));
16780
- filtered.push(`stream:${workStream}`);
16814
+ const filtered = currentTags.filter((t) => !t.startsWith("focus:"));
16815
+ filtered.push(`focus:${workFocus}`);
16781
16816
  updates.tags = filtered;
16782
16817
  }
16783
16818
  if (typeof progress === "number") {
@@ -18635,7 +18670,8 @@ function createDecisionTools(store) {
18635
18670
  title: external_exports.string().describe("Title of the decision"),
18636
18671
  content: external_exports.string().describe("Decision description, context, and rationale"),
18637
18672
  status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("Status (default: 'open')"),
18638
- owner: external_exports.string().optional().describe("Person responsible for this decision"),
18673
+ owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
18674
+ assignee: external_exports.string().optional().describe("Person assigned to do the work"),
18639
18675
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
18640
18676
  },
18641
18677
  async (args) => {
@@ -18644,7 +18680,8 @@ function createDecisionTools(store) {
18644
18680
  {
18645
18681
  title: args.title,
18646
18682
  status: args.status,
18647
- owner: args.owner,
18683
+ owner: normalizeOwner(args.owner),
18684
+ assignee: args.assignee,
18648
18685
  tags: args.tags
18649
18686
  },
18650
18687
  args.content
@@ -18667,11 +18704,14 @@ function createDecisionTools(store) {
18667
18704
  title: external_exports.string().optional().describe("New title"),
18668
18705
  status: external_exports.enum(["open", "decided", "superseded", "dismissed"]).optional().describe("New status"),
18669
18706
  content: external_exports.string().optional().describe("New content"),
18670
- owner: external_exports.string().optional().describe("New 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"),
18671
18709
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
18672
18710
  },
18673
18711
  async (args) => {
18674
- 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;
18675
18715
  const doc = store.update(id, updates, content);
18676
18716
  return {
18677
18717
  content: [
@@ -18769,12 +18809,13 @@ function createActionTools(store) {
18769
18809
  title: external_exports.string().describe("Title of the action item"),
18770
18810
  content: external_exports.string().describe("Description of what needs to be done"),
18771
18811
  status: external_exports.string().optional().describe("Status (default: 'open')"),
18772
- owner: external_exports.string().optional().describe("Person responsible"),
18812
+ owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
18813
+ assignee: external_exports.string().optional().describe("Person assigned to do the work"),
18773
18814
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
18774
18815
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
18775
18816
  dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
18776
18817
  sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags."),
18777
- workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Adds a stream:<value> tag.")
18818
+ workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Adds a focus:<value> tag.")
18778
18819
  },
18779
18820
  async (args) => {
18780
18821
  const tags = [...args.tags ?? []];
@@ -18784,15 +18825,16 @@ function createActionTools(store) {
18784
18825
  if (!tags.includes(tag)) tags.push(tag);
18785
18826
  }
18786
18827
  }
18787
- if (args.workStream) {
18788
- tags.push(`stream:${args.workStream}`);
18828
+ if (args.workFocus) {
18829
+ tags.push(`focus:${args.workFocus}`);
18789
18830
  }
18790
18831
  const doc = store.create(
18791
18832
  "action",
18792
18833
  {
18793
18834
  title: args.title,
18794
18835
  status: args.status,
18795
- owner: args.owner,
18836
+ owner: normalizeOwner(args.owner),
18837
+ assignee: args.assignee,
18796
18838
  priority: args.priority,
18797
18839
  tags: tags.length > 0 ? tags : void 0,
18798
18840
  dueDate: args.dueDate
@@ -18820,16 +18862,19 @@ function createActionTools(store) {
18820
18862
  title: external_exports.string().optional().describe("New title"),
18821
18863
  status: external_exports.string().optional().describe("New status"),
18822
18864
  content: external_exports.string().optional().describe("New content"),
18823
- owner: external_exports.string().optional().describe("New 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"),
18824
18867
  priority: external_exports.string().optional().describe("New priority"),
18825
18868
  dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
18826
18869
  tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
18827
18870
  sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
18828
- workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Replaces existing stream:<value> tag."),
18871
+ workFocus: external_exports.string().optional().describe("Work focus name (e.g. 'Budget UX'). Replaces existing focus:<value> tag."),
18829
18872
  progress: external_exports.number().optional().describe("Explicit progress percentage (0-100).")
18830
18873
  },
18831
18874
  async (args) => {
18832
- const { id, content, sprints, tags, workStream, progress, ...updates } = args;
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;
18833
18878
  if (tags !== void 0) {
18834
18879
  const merged = [...tags];
18835
18880
  if (sprints) {
@@ -18838,14 +18883,14 @@ function createActionTools(store) {
18838
18883
  if (!merged.includes(tag)) merged.push(tag);
18839
18884
  }
18840
18885
  }
18841
- if (workStream !== void 0) {
18842
- const filtered = merged.filter((t) => !t.startsWith("stream:"));
18843
- filtered.push(`stream:${workStream}`);
18886
+ if (workFocus !== void 0) {
18887
+ const filtered = merged.filter((t) => !t.startsWith("focus:"));
18888
+ filtered.push(`focus:${workFocus}`);
18844
18889
  updates.tags = filtered;
18845
18890
  } else {
18846
18891
  updates.tags = merged;
18847
18892
  }
18848
- } else if (sprints !== void 0 || workStream !== void 0) {
18893
+ } else if (sprints !== void 0 || workFocus !== void 0) {
18849
18894
  const existing = store.get(id);
18850
18895
  if (!existing) {
18851
18896
  return {
@@ -18858,9 +18903,9 @@ function createActionTools(store) {
18858
18903
  existingTags = existingTags.filter((t) => !t.startsWith("sprint:"));
18859
18904
  existingTags.push(...sprints.map((s) => `sprint:${s}`));
18860
18905
  }
18861
- if (workStream !== void 0) {
18862
- existingTags = existingTags.filter((t) => !t.startsWith("stream:"));
18863
- existingTags.push(`stream:${workStream}`);
18906
+ if (workFocus !== void 0) {
18907
+ existingTags = existingTags.filter((t) => !t.startsWith("focus:"));
18908
+ existingTags.push(`focus:${workFocus}`);
18864
18909
  }
18865
18910
  updates.tags = existingTags;
18866
18911
  }
@@ -18973,7 +19018,8 @@ function createQuestionTools(store) {
18973
19018
  title: external_exports.string().describe("The question being asked"),
18974
19019
  content: external_exports.string().describe("Context and details about the question"),
18975
19020
  status: external_exports.string().optional().describe("Status (default: 'open')"),
18976
- owner: external_exports.string().optional().describe("Person who should answer this"),
19021
+ owner: ownerSchema.optional().describe("Persona role responsible (po, dm, tl)"),
19022
+ assignee: external_exports.string().optional().describe("Person assigned to do the work"),
18977
19023
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
18978
19024
  },
18979
19025
  async (args) => {
@@ -18982,7 +19028,8 @@ function createQuestionTools(store) {
18982
19028
  {
18983
19029
  title: args.title,
18984
19030
  status: args.status,
18985
- owner: args.owner,
19031
+ owner: normalizeOwner(args.owner),
19032
+ assignee: args.assignee,
18986
19033
  tags: args.tags
18987
19034
  },
18988
19035
  args.content
@@ -19005,11 +19052,14 @@ function createQuestionTools(store) {
19005
19052
  title: external_exports.string().optional().describe("New title"),
19006
19053
  status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
19007
19054
  content: external_exports.string().optional().describe("Updated content / answer"),
19008
- owner: external_exports.string().optional().describe("New 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"),
19009
19057
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
19010
19058
  },
19011
19059
  async (args) => {
19012
- 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;
19013
19063
  const doc = store.update(id, updates, content);
19014
19064
  return {
19015
19065
  content: [
@@ -19034,18 +19084,20 @@ function createDocumentTools(store) {
19034
19084
  {
19035
19085
  type: external_exports.string().optional().describe(`Filter by document type (registered types: ${store.registeredTypes.join(", ")})`),
19036
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)"),
19037
19088
  tag: external_exports.string().optional().describe("Filter by tag"),
19038
- workStream: external_exports.string().optional().describe("Filter by work stream name (matches stream:<value> tag)")
19089
+ workFocus: external_exports.string().optional().describe("Filter by work focus name (matches focus:<value> tag)")
19039
19090
  },
19040
19091
  async (args) => {
19041
19092
  let docs = store.list({
19042
19093
  type: args.type,
19043
19094
  status: args.status,
19095
+ owner: args.owner,
19044
19096
  tag: args.tag
19045
19097
  });
19046
- if (args.workStream) {
19047
- const streamTag = `stream:${args.workStream}`;
19048
- docs = docs.filter((d) => d.frontmatter.tags?.includes(streamTag));
19098
+ if (args.workFocus) {
19099
+ const focusTag = `focus:${args.workFocus}`;
19100
+ docs = docs.filter((d) => d.frontmatter.tags?.includes(focusTag));
19049
19101
  }
19050
19102
  const summary = docs.map((d) => ({
19051
19103
  id: d.frontmatter.id,
@@ -21043,6 +21095,39 @@ tr:hover td {
21043
21095
  font-weight: 700;
21044
21096
  color: var(--text);
21045
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
+ }
21046
21131
  `;
21047
21132
  }
21048
21133
 
@@ -22509,15 +22594,15 @@ function sprintSummaryPage(data, cached2) {
22509
22594
  </div>`,
22510
22595
  { titleTag: "h3" }
22511
22596
  ) : "";
22512
- const STREAM_PALETTE = [
22513
- "hsla(220, 30%, 22%, 0.45)",
22514
- "hsla(160, 30%, 20%, 0.45)",
22515
- "hsla(280, 25%, 22%, 0.45)",
22516
- "hsla(30, 35%, 22%, 0.45)",
22517
- "hsla(340, 25%, 22%, 0.45)",
22518
- "hsla(190, 30%, 20%, 0.45)",
22519
- "hsla(60, 25%, 20%, 0.45)",
22520
- "hsla(120, 20%, 20%, 0.45)"
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%)"
22521
22606
  ];
22522
22607
  function hashString(s) {
22523
22608
  let h = 0;
@@ -22526,68 +22611,92 @@ function sprintSummaryPage(data, cached2) {
22526
22611
  }
22527
22612
  return Math.abs(h);
22528
22613
  }
22529
- function collectStreams(items) {
22530
- const streams = /* @__PURE__ */ new Set();
22531
- for (const w of items) {
22532
- if (w.workStream) streams.add(w.workStream);
22533
- if (w.children) {
22534
- for (const s of collectStreams(w.children)) streams.add(s);
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);
22535
22637
  }
22536
22638
  }
22537
- return streams;
22538
- }
22539
- const uniqueStreams = collectStreams(data.workItems.items);
22540
- const streamColorMap = /* @__PURE__ */ new Map();
22541
- for (const name of uniqueStreams) {
22542
- streamColorMap.set(name, STREAM_PALETTE[hashString(name) % STREAM_PALETTE.length]);
22639
+ walk(items);
22640
+ return { total, done, inProgress };
22543
22641
  }
22544
- const streamStyleRules = [...streamColorMap.entries()].map(([name, color]) => `tr[data-stream="${escapeHtml(name)}"] td { background: ${color}; }`).join("\n");
22545
- const streamStyleBlock = streamStyleRules ? `<style>${streamStyleRules}</style>` : "";
22546
- function renderItemRows(items, depth = 0) {
22642
+ function renderItemRows(items, borderColor, depth = 0) {
22547
22643
  return items.flatMap((w) => {
22548
22644
  const isChild = depth > 0;
22549
22645
  const isContribution = w.type === "contribution";
22550
- const classes = [];
22646
+ const classes = ["focus-row"];
22551
22647
  if (isContribution) classes.push("contribution-row");
22552
22648
  else if (isChild) classes.push("child-row");
22553
- const dataStream = w.workStream ? ` data-stream="${escapeHtml(w.workStream)}"` : "";
22554
- const rowAttrs = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
22555
22649
  const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
22556
- const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
22557
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>` : "";
22558
22651
  const row = `
22559
- <tr${rowAttrs}${dataStream}>
22652
+ <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
22560
22653
  <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
22561
22654
  <td>${escapeHtml(w.title)}</td>
22562
- <td>${streamCell}</td>
22563
- <td>${escapeHtml(typeLabel(w.type))}</td>
22564
22655
  <td>${statusBadge(w.status)}</td>
22565
22656
  <td>${progressCell}</td>
22566
22657
  </tr>`;
22567
- const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
22658
+ const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
22568
22659
  return [row, ...childRows];
22569
22660
  });
22570
22661
  }
22571
- const workItemRows = renderItemRows(data.workItems.items);
22572
- const sortableHeaders = `<tr>
22573
- <th class="sortable-th" onclick="sortWorkItems(0)">ID<span class="sort-arrow" id="sort-arrow-0"></span></th>
22574
- <th class="sortable-th" onclick="sortWorkItems(1)">Title<span class="sort-arrow" id="sort-arrow-1"></span></th>
22575
- <th class="sortable-th" onclick="sortWorkItems(2)">Stream<span class="sort-arrow" id="sort-arrow-2"></span></th>
22576
- <th class="sortable-th" onclick="sortWorkItems(3)">Type<span class="sort-arrow" id="sort-arrow-3"></span></th>
22577
- <th class="sortable-th" onclick="sortWorkItems(4)">Status<span class="sort-arrow" id="sort-arrow-4"></span></th>
22578
- <th class="sortable-th" onclick="sortWorkItems(5)">Progress<span class="sort-arrow" id="sort-arrow-5"></span></th>
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>
22579
22689
  </tr>`;
22580
- const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
22690
+ const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
22581
22691
  "ss-work-items",
22582
22692
  "Work Items",
22583
- `${streamStyleBlock}
22584
- <div class="table-wrap">
22693
+ `<div class="table-wrap">
22585
22694
  <table id="work-items-table">
22586
22695
  <thead>
22587
- ${sortableHeaders}
22696
+ ${tableHeaders}
22588
22697
  </thead>
22589
22698
  <tbody>
22590
- ${workItemRows.join("")}
22699
+ ${allWorkItemRows.join("")}
22591
22700
  </tbody>
22592
22701
  </table>
22593
22702
  </div>`,
@@ -22663,61 +22772,6 @@ function sprintSummaryPage(data, cached2) {
22663
22772
  </div>
22664
22773
 
22665
22774
  <script>
22666
- var _sortCol = -1;
22667
- var _sortAsc = true;
22668
-
22669
- function sortWorkItems(col) {
22670
- var table = document.getElementById('work-items-table');
22671
- if (!table) return;
22672
- var tbody = table.querySelector('tbody');
22673
- var allRows = Array.from(tbody.querySelectorAll('tr'));
22674
-
22675
- // Toggle direction if clicking the same column
22676
- if (_sortCol === col) {
22677
- _sortAsc = !_sortAsc;
22678
- } else {
22679
- _sortCol = col;
22680
- _sortAsc = true;
22681
- }
22682
-
22683
- // Update sort arrows
22684
- for (var i = 0; i < 6; i++) {
22685
- var arrow = document.getElementById('sort-arrow-' + i);
22686
- if (arrow) arrow.textContent = i === col ? (_sortAsc ? ' \\u25B2' : ' \\u25BC') : '';
22687
- }
22688
-
22689
- // Group rows: root rows + their child/contribution rows
22690
- var groups = [];
22691
- var current = null;
22692
- for (var r = 0; r < allRows.length; r++) {
22693
- var row = allRows[r];
22694
- var isChild = row.classList.contains('child-row') || row.classList.contains('contribution-row');
22695
- if (!isChild) {
22696
- current = { root: row, children: [] };
22697
- groups.push(current);
22698
- } else if (current) {
22699
- current.children.push(row);
22700
- }
22701
- }
22702
-
22703
- // Sort groups by root row text content of target column
22704
- groups.sort(function(a, b) {
22705
- var aText = (a.root.children[col] ? a.root.children[col].textContent : '').trim().toLowerCase();
22706
- var bText = (b.root.children[col] ? b.root.children[col].textContent : '').trim().toLowerCase();
22707
- if (aText < bText) return _sortAsc ? -1 : 1;
22708
- if (aText > bText) return _sortAsc ? 1 : -1;
22709
- return 0;
22710
- });
22711
-
22712
- // Re-append rows in sorted order
22713
- for (var g = 0; g < groups.length; g++) {
22714
- tbody.appendChild(groups[g].root);
22715
- for (var c = 0; c < groups[g].children.length; c++) {
22716
- tbody.appendChild(groups[g].children[c]);
22717
- }
22718
- }
22719
- }
22720
-
22721
22775
  async function generateSummary() {
22722
22776
  var btn = document.getElementById('generate-btn');
22723
22777
  var loading = document.getElementById('summary-loading');
@@ -23508,7 +23562,7 @@ function tlSprintPage(ctx) {
23508
23562
  `<div class="table-wrap">
23509
23563
  <table>
23510
23564
  <thead>
23511
- <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Stream</th></tr>
23565
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
23512
23566
  </thead>
23513
23567
  <tbody>
23514
23568
  ${techItems.map((w) => `
@@ -23517,7 +23571,7 @@ function tlSprintPage(ctx) {
23517
23571
  <td>${escapeHtml(w.title)}</td>
23518
23572
  <td>${escapeHtml(typeLabel(w.type))}</td>
23519
23573
  <td>${statusBadge(w.status)}</td>
23520
- <td>${w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
23574
+ <td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
23521
23575
  </tr>`).join("")}
23522
23576
  </tbody>
23523
23577
  </table>
@@ -25655,6 +25709,556 @@ function createWebTools(store, projectName, navGroups) {
25655
25709
  ];
25656
25710
  }
25657
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
+
25658
26262
  // src/agent/mcp-server.ts
25659
26263
  function createMarvinMcpServer(store, options) {
25660
26264
  const tools = [
@@ -25666,7 +26270,8 @@ function createMarvinMcpServer(store, options) {
25666
26270
  ...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
25667
26271
  ...options?.pluginTools ?? [],
25668
26272
  ...options?.skillTools ?? [],
25669
- ...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
26273
+ ...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : [],
26274
+ ...createDoctorTools(store)
25670
26275
  ];
25671
26276
  return createSdkMcpServer({
25672
26277
  name: "marvin-governance",
@@ -26977,7 +27582,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26977
27582
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26978
27583
 
26979
27584
  // src/skills/action-tools.ts
26980
- import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
27585
+ import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
26981
27586
 
26982
27587
  // src/skills/action-runner.ts
26983
27588
  import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
@@ -27043,7 +27648,7 @@ function createSkillActionTools(skills, context) {
27043
27648
  if (!skill.actions) continue;
27044
27649
  for (const action of skill.actions) {
27045
27650
  tools.push(
27046
- tool23(
27651
+ tool24(
27047
27652
  `${skill.id}__${action.id}`,
27048
27653
  action.description,
27049
27654
  {
@@ -27135,10 +27740,10 @@ ${lines.join("\n\n")}`;
27135
27740
  }
27136
27741
 
27137
27742
  // src/mcp/persona-tools.ts
27138
- import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
27743
+ import { tool as tool25 } from "@anthropic-ai/claude-agent-sdk";
27139
27744
  function createPersonaTools(ctx, marvinDir) {
27140
27745
  return [
27141
- tool24(
27746
+ tool25(
27142
27747
  "set_persona",
27143
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.",
27144
27749
  {
@@ -27168,7 +27773,7 @@ ${summaries}`
27168
27773
  };
27169
27774
  }
27170
27775
  ),
27171
- tool24(
27776
+ tool25(
27172
27777
  "get_persona_guidance",
27173
27778
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
27174
27779
  {
@@ -28981,12 +29586,76 @@ async function generateClaudeMdCommand(options) {
28981
29586
  console.log(chalk18.green("Created .marvin/CLAUDE.md"));
28982
29587
  }
28983
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
+
28984
29653
  // src/cli/program.ts
28985
29654
  function createProgram() {
28986
29655
  const program2 = new Command();
28987
29656
  program2.name("marvin").description(
28988
29657
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
28989
- ).version("0.5.1");
29658
+ ).version("0.5.3");
28990
29659
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
28991
29660
  await initCommand();
28992
29661
  });
@@ -29075,6 +29744,9 @@ function createProgram() {
29075
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) => {
29076
29745
  await webCommand(options);
29077
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
+ });
29078
29750
  const generateCmd = program2.command("generate").description("Generate project files");
29079
29751
  generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
29080
29752
  await generateClaudeMdCommand(options);