mrvn-cli 0.5.0 → 0.5.2

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