mrvn-cli 0.5.1 → 0.5.2

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