mrvn-cli 0.3.5 → 0.3.7

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
@@ -920,10 +920,10 @@ function mergeDefs(...defs) {
920
920
  function cloneDef(schema) {
921
921
  return mergeDefs(schema._zod.def);
922
922
  }
923
- function getElementAtPath(obj, path18) {
924
- if (!path18)
923
+ function getElementAtPath(obj, path20) {
924
+ if (!path20)
925
925
  return obj;
926
- return path18.reduce((acc, key) => acc?.[key], obj);
926
+ return path20.reduce((acc, key) => acc?.[key], obj);
927
927
  }
928
928
  function promiseAllObject(promisesObj) {
929
929
  const keys = Object.keys(promisesObj);
@@ -1306,11 +1306,11 @@ function aborted(x, startIndex = 0) {
1306
1306
  }
1307
1307
  return false;
1308
1308
  }
1309
- function prefixIssues(path18, issues) {
1309
+ function prefixIssues(path20, issues) {
1310
1310
  return issues.map((iss) => {
1311
1311
  var _a2;
1312
1312
  (_a2 = iss).path ?? (_a2.path = []);
1313
- iss.path.unshift(path18);
1313
+ iss.path.unshift(path20);
1314
1314
  return iss;
1315
1315
  });
1316
1316
  }
@@ -1493,7 +1493,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1493
1493
  }
1494
1494
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1495
1495
  const result = { errors: [] };
1496
- const processError = (error49, path18 = []) => {
1496
+ const processError = (error49, path20 = []) => {
1497
1497
  var _a2, _b;
1498
1498
  for (const issue2 of error49.issues) {
1499
1499
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1503,7 +1503,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1503
1503
  } else if (issue2.code === "invalid_element") {
1504
1504
  processError({ issues: issue2.issues }, issue2.path);
1505
1505
  } else {
1506
- const fullpath = [...path18, ...issue2.path];
1506
+ const fullpath = [...path20, ...issue2.path];
1507
1507
  if (fullpath.length === 0) {
1508
1508
  result.errors.push(mapper(issue2));
1509
1509
  continue;
@@ -1535,8 +1535,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1535
1535
  }
1536
1536
  function toDotPath(_path) {
1537
1537
  const segs = [];
1538
- const path18 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1539
- for (const seg of path18) {
1538
+ const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1539
+ for (const seg of path20) {
1540
1540
  if (typeof seg === "number")
1541
1541
  segs.push(`[${seg}]`);
1542
1542
  else if (typeof seg === "symbol")
@@ -13513,13 +13513,13 @@ function resolveRef(ref, ctx) {
13513
13513
  if (!ref.startsWith("#")) {
13514
13514
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13515
13515
  }
13516
- const path18 = ref.slice(1).split("/").filter(Boolean);
13517
- if (path18.length === 0) {
13516
+ const path20 = ref.slice(1).split("/").filter(Boolean);
13517
+ if (path20.length === 0) {
13518
13518
  return ctx.rootSchema;
13519
13519
  }
13520
13520
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13521
- if (path18[0] === defsKey) {
13522
- const key = path18[1];
13521
+ if (path20[0] === defsKey) {
13522
+ const key = path20[1];
13523
13523
  if (!key || !ctx.defs[key]) {
13524
13524
  throw new Error(`Reference not found: ${ref}`);
13525
13525
  }
@@ -14124,7 +14124,7 @@ function collectGarMetrics(store) {
14124
14124
  );
14125
14125
  const openQuestions = store.list({ type: "question", status: "open" });
14126
14126
  const riskItems = allDocs.filter(
14127
- (d) => d.frontmatter.tags?.includes("risk")
14127
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
14128
14128
  );
14129
14129
  const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14130
14130
  const total = allActions.length;
@@ -14525,7 +14525,7 @@ function createReportTools(store) {
14525
14525
  async () => {
14526
14526
  const allDocs = store.list();
14527
14527
  const taggedRisks = allDocs.filter(
14528
- (d) => d.frontmatter.tags?.includes("risk")
14528
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
14529
14529
  );
14530
14530
  const highPriorityActions = store.list({ type: "action", status: "open" }).filter((d) => d.frontmatter.priority === "high");
14531
14531
  const unresolvedQuestions = store.list({ type: "question", status: "open" });
@@ -14896,7 +14896,8 @@ function createFeatureTools(store) {
14896
14896
  status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
14897
14897
  content: external_exports.string().optional().describe("New content"),
14898
14898
  owner: external_exports.string().optional().describe("New owner"),
14899
- priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority")
14899
+ priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
14900
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
14900
14901
  },
14901
14902
  async (args) => {
14902
14903
  const { id, content, ...updates } = args;
@@ -15053,7 +15054,8 @@ function createEpicTools(store) {
15053
15054
  content: external_exports.string().optional().describe("New content"),
15054
15055
  owner: external_exports.string().optional().describe("New owner"),
15055
15056
  targetDate: external_exports.string().optional().describe("New target date"),
15056
- estimatedEffort: external_exports.string().optional().describe("New estimated effort")
15057
+ estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
15058
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
15057
15059
  },
15058
15060
  async (args) => {
15059
15061
  const { id, content, ...updates } = args;
@@ -16625,6 +16627,56 @@ function getPluginPromptFragment(plugin, personaId) {
16625
16627
  return plugin.promptFragments[personaId] ?? plugin.promptFragments["*"];
16626
16628
  }
16627
16629
 
16630
+ // src/templates/claude-md.ts
16631
+ function getDefaultClaudeMdContent(projectName) {
16632
+ return `# Marvin \u2014 Project Instructions for "${projectName}"
16633
+
16634
+ You are **Marvin**, an AI-powered product development assistant.
16635
+ You operate as one of three personas \u2014 stay in role and suggest switching when a question falls outside your scope.
16636
+
16637
+ ## Personas
16638
+
16639
+ | Persona | Short | Focus |
16640
+ |---------|-------|-------|
16641
+ | Product Owner | po | Vision, backlog, requirements, features, acceptance criteria |
16642
+ | Delivery Manager | dm | Planning, risks, actions, timelines, sprints, status |
16643
+ | Tech Lead | tl | Architecture, trade-offs, technical decisions, code quality |
16644
+
16645
+ ## Proactive Governance
16646
+
16647
+ When conversation implies a commitment, risk, or open question, **suggest creating the matching artifact**:
16648
+ - A decision was made \u2192 offer to create a **Decision (D-xxx)**
16649
+ - Someone committed to a task \u2192 offer an **Action (A-xxx)** with owner and due date
16650
+ - An unanswered question surfaced \u2192 offer a **Question (Q-xxx)**
16651
+ - A new capability is discussed \u2192 offer a **Feature (F-xxx)**
16652
+ - Implementation scope is agreed \u2192 offer an **Epic (E-xxx)** linked to a feature
16653
+ - Work is being time-boxed \u2192 offer a **Sprint (SP-xxx)**
16654
+
16655
+ ## Insights
16656
+
16657
+ Proactively flag:
16658
+ - Overdue actions or unresolved questions
16659
+ - Decisions without rationale or linked features
16660
+ - Features without linked epics
16661
+ - Risks mentioned but not tracked
16662
+ - When a risk is resolved \u2192 remove the "risk" tag and add "risk-mitigated"
16663
+
16664
+ ## Tool Usage
16665
+
16666
+ - **Search before creating** \u2014 avoid duplicate artifacts
16667
+ - **Reference IDs** (e.g. D-001, A-003) when discussing existing items
16668
+ - **Link artifacts** \u2014 epics to features, actions to decisions, etc.
16669
+ - Use \`search_documents\` to find related context before answering
16670
+
16671
+ ## Communication Style
16672
+
16673
+ - Be concise and structured
16674
+ - State assumptions explicitly
16675
+ - Use bullet points and tables where they aid clarity
16676
+ - When uncertain, ask a clarifying question rather than guessing
16677
+ `;
16678
+ }
16679
+
16628
16680
  // src/cli/commands/init.ts
16629
16681
  async function initCommand() {
16630
16682
  const cwd = process.cwd();
@@ -16681,11 +16733,17 @@ async function initCommand() {
16681
16733
  YAML3.stringify(config2),
16682
16734
  "utf-8"
16683
16735
  );
16736
+ fs4.writeFileSync(
16737
+ path4.join(marvinDir, "CLAUDE.md"),
16738
+ getDefaultClaudeMdContent(projectName),
16739
+ "utf-8"
16740
+ );
16684
16741
  console.log(chalk.green(`
16685
16742
  Initialized Marvin project "${projectName}" in ${cwd}`));
16686
16743
  console.log(chalk.dim(`Methodology: ${plugin?.name ?? methodology}`));
16687
16744
  console.log(chalk.dim("\nCreated:"));
16688
16745
  console.log(chalk.dim(" .marvin/config.yaml"));
16746
+ console.log(chalk.dim(" .marvin/CLAUDE.md"));
16689
16747
  console.log(chalk.dim(" .marvin/docs/decisions/"));
16690
16748
  console.log(chalk.dim(" .marvin/docs/actions/"));
16691
16749
  console.log(chalk.dim(" .marvin/docs/questions/"));
@@ -16888,8 +16946,8 @@ function resolvePersonaId(input4) {
16888
16946
  }
16889
16947
 
16890
16948
  // src/agent/session.ts
16891
- import * as fs9 from "fs";
16892
- import * as path9 from "path";
16949
+ import * as fs10 from "fs";
16950
+ import * as path10 from "path";
16893
16951
  import * as readline from "readline";
16894
16952
  import chalk2 from "chalk";
16895
16953
  import ora from "ora";
@@ -16898,9 +16956,24 @@ import {
16898
16956
  } from "@anthropic-ai/claude-agent-sdk";
16899
16957
 
16900
16958
  // src/personas/prompt-builder.ts
16901
- function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment) {
16959
+ import * as fs5 from "fs";
16960
+ import * as path5 from "path";
16961
+ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment, marvinDir) {
16902
16962
  const parts = [];
16903
16963
  parts.push(persona.systemPrompt);
16964
+ if (marvinDir) {
16965
+ const claudeMdPath = path5.join(marvinDir, "CLAUDE.md");
16966
+ try {
16967
+ const content = fs5.readFileSync(claudeMdPath, "utf-8").trim();
16968
+ if (content) {
16969
+ parts.push(`
16970
+ ## Project Instructions
16971
+ ${content}
16972
+ `);
16973
+ }
16974
+ } catch {
16975
+ }
16976
+ }
16904
16977
  parts.push(`
16905
16978
  ## Project Context
16906
16979
  - **Project Name:** ${projectConfig.name}
@@ -16947,8 +17020,8 @@ ${projectConfig.personas[persona.id].extraInstructions}
16947
17020
  }
16948
17021
 
16949
17022
  // src/storage/store.ts
16950
- import * as fs5 from "fs";
16951
- import * as path5 from "path";
17023
+ import * as fs6 from "fs";
17024
+ import * as path6 from "path";
16952
17025
 
16953
17026
  // src/storage/document.ts
16954
17027
  import matter from "gray-matter";
@@ -16986,7 +17059,7 @@ var DocumentStore = class {
16986
17059
  typeDirs;
16987
17060
  idPrefixes;
16988
17061
  constructor(marvinDir, registrations) {
16989
- this.docsDir = path5.join(marvinDir, "docs");
17062
+ this.docsDir = path6.join(marvinDir, "docs");
16990
17063
  this.typeDirs = { ...CORE_TYPE_DIRS };
16991
17064
  this.idPrefixes = { ...CORE_ID_PREFIXES };
16992
17065
  for (const reg of registrations ?? []) {
@@ -17001,12 +17074,12 @@ var DocumentStore = class {
17001
17074
  buildIndex() {
17002
17075
  this.index.clear();
17003
17076
  for (const type of Object.keys(this.typeDirs)) {
17004
- const dir = path5.join(this.docsDir, this.typeDirs[type]);
17005
- if (!fs5.existsSync(dir)) continue;
17006
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17077
+ const dir = path6.join(this.docsDir, this.typeDirs[type]);
17078
+ if (!fs6.existsSync(dir)) continue;
17079
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
17007
17080
  for (const file2 of files) {
17008
- const filePath = path5.join(dir, file2);
17009
- const raw = fs5.readFileSync(filePath, "utf-8");
17081
+ const filePath = path6.join(dir, file2);
17082
+ const raw = fs6.readFileSync(filePath, "utf-8");
17010
17083
  const doc = parseDocument(raw, filePath);
17011
17084
  if (doc.frontmatter.id) {
17012
17085
  if (this.index.has(doc.frontmatter.id)) {
@@ -17025,12 +17098,12 @@ var DocumentStore = class {
17025
17098
  for (const type of types) {
17026
17099
  const dirName = this.typeDirs[type];
17027
17100
  if (!dirName) continue;
17028
- const dir = path5.join(this.docsDir, dirName);
17029
- if (!fs5.existsSync(dir)) continue;
17030
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17101
+ const dir = path6.join(this.docsDir, dirName);
17102
+ if (!fs6.existsSync(dir)) continue;
17103
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
17031
17104
  for (const file2 of files) {
17032
- const filePath = path5.join(dir, file2);
17033
- const raw = fs5.readFileSync(filePath, "utf-8");
17105
+ const filePath = path6.join(dir, file2);
17106
+ const raw = fs6.readFileSync(filePath, "utf-8");
17034
17107
  const doc = parseDocument(raw, filePath);
17035
17108
  if (query7?.status && doc.frontmatter.status !== query7.status) continue;
17036
17109
  if (query7?.owner && doc.frontmatter.owner !== query7.owner) continue;
@@ -17043,12 +17116,12 @@ var DocumentStore = class {
17043
17116
  }
17044
17117
  get(id) {
17045
17118
  for (const type of Object.keys(this.typeDirs)) {
17046
- const dir = path5.join(this.docsDir, this.typeDirs[type]);
17047
- if (!fs5.existsSync(dir)) continue;
17048
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17119
+ const dir = path6.join(this.docsDir, this.typeDirs[type]);
17120
+ if (!fs6.existsSync(dir)) continue;
17121
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
17049
17122
  for (const file2 of files) {
17050
- const filePath = path5.join(dir, file2);
17051
- const raw = fs5.readFileSync(filePath, "utf-8");
17123
+ const filePath = path6.join(dir, file2);
17124
+ const raw = fs6.readFileSync(filePath, "utf-8");
17052
17125
  const doc = parseDocument(raw, filePath);
17053
17126
  if (doc.frontmatter.id === id) return doc;
17054
17127
  }
@@ -17062,8 +17135,8 @@ var DocumentStore = class {
17062
17135
  if (!dirName) {
17063
17136
  throw new Error(`Unknown document type: ${type}`);
17064
17137
  }
17065
- const dir = path5.join(this.docsDir, dirName);
17066
- fs5.mkdirSync(dir, { recursive: true });
17138
+ const dir = path6.join(this.docsDir, dirName);
17139
+ fs6.mkdirSync(dir, { recursive: true });
17067
17140
  const cleaned = Object.fromEntries(
17068
17141
  Object.entries(frontmatter).filter(([, v]) => v !== void 0)
17069
17142
  );
@@ -17077,13 +17150,13 @@ var DocumentStore = class {
17077
17150
  ...cleaned
17078
17151
  };
17079
17152
  const fileName = type === "meeting" ? `${cleaned.date?.slice(0, 10) ?? now.slice(0, 10)}-${slugify2(fullFrontmatter.title)}.md` : `${id}.md`;
17080
- const filePath = path5.join(dir, fileName);
17153
+ const filePath = path6.join(dir, fileName);
17081
17154
  const doc = {
17082
17155
  frontmatter: fullFrontmatter,
17083
17156
  content,
17084
17157
  filePath
17085
17158
  };
17086
- fs5.writeFileSync(filePath, serializeDocument(doc), "utf-8");
17159
+ fs6.writeFileSync(filePath, serializeDocument(doc), "utf-8");
17087
17160
  this.index.set(id, fullFrontmatter);
17088
17161
  return doc;
17089
17162
  }
@@ -17098,12 +17171,12 @@ var DocumentStore = class {
17098
17171
  `Document ${frontmatter.id} already exists. Resolve conflicts before importing.`
17099
17172
  );
17100
17173
  }
17101
- const dir = path5.join(this.docsDir, dirName);
17102
- fs5.mkdirSync(dir, { recursive: true });
17174
+ const dir = path6.join(this.docsDir, dirName);
17175
+ fs6.mkdirSync(dir, { recursive: true });
17103
17176
  const fileName = type === "meeting" ? `${frontmatter.date?.slice(0, 10) ?? frontmatter.created.slice(0, 10)}-${slugify2(frontmatter.title)}.md` : `${frontmatter.id}.md`;
17104
- const filePath = path5.join(dir, fileName);
17177
+ const filePath = path6.join(dir, fileName);
17105
17178
  const doc = { frontmatter, content, filePath };
17106
- fs5.writeFileSync(filePath, serializeDocument(doc), "utf-8");
17179
+ fs6.writeFileSync(filePath, serializeDocument(doc), "utf-8");
17107
17180
  this.index.set(frontmatter.id, frontmatter);
17108
17181
  return doc;
17109
17182
  }
@@ -17125,7 +17198,7 @@ var DocumentStore = class {
17125
17198
  content: content ?? existing.content,
17126
17199
  filePath: existing.filePath
17127
17200
  };
17128
- fs5.writeFileSync(existing.filePath, serializeDocument(doc), "utf-8");
17201
+ fs6.writeFileSync(existing.filePath, serializeDocument(doc), "utf-8");
17129
17202
  this.index.set(id, updatedFrontmatter);
17130
17203
  return doc;
17131
17204
  }
@@ -17135,14 +17208,14 @@ var DocumentStore = class {
17135
17208
  throw new Error(`Unknown document type: ${type}`);
17136
17209
  }
17137
17210
  const dirName = this.typeDirs[type];
17138
- const dir = path5.join(this.docsDir, dirName);
17139
- if (!fs5.existsSync(dir)) return `${prefix}-001`;
17211
+ const dir = path6.join(this.docsDir, dirName);
17212
+ if (!fs6.existsSync(dir)) return `${prefix}-001`;
17140
17213
  const idPattern = new RegExp(`^${prefix}-(\\d+)$`);
17141
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
17214
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md"));
17142
17215
  let maxNum = 0;
17143
17216
  for (const file2 of files) {
17144
- const filePath = path5.join(dir, file2);
17145
- const raw = fs5.readFileSync(filePath, "utf-8");
17217
+ const filePath = path6.join(dir, file2);
17218
+ const raw = fs6.readFileSync(filePath, "utf-8");
17146
17219
  const doc = parseDocument(raw, filePath);
17147
17220
  const match = doc.frontmatter.id?.match(idPattern);
17148
17221
  if (match) {
@@ -17155,12 +17228,12 @@ var DocumentStore = class {
17155
17228
  counts() {
17156
17229
  const result = {};
17157
17230
  for (const type of Object.keys(this.typeDirs)) {
17158
- const dir = path5.join(this.docsDir, this.typeDirs[type]);
17159
- if (!fs5.existsSync(dir)) {
17231
+ const dir = path6.join(this.docsDir, this.typeDirs[type]);
17232
+ if (!fs6.existsSync(dir)) {
17160
17233
  result[type] = 0;
17161
17234
  continue;
17162
17235
  }
17163
- result[type] = fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
17236
+ result[type] = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
17164
17237
  }
17165
17238
  return result;
17166
17239
  }
@@ -17170,13 +17243,13 @@ function slugify2(text) {
17170
17243
  }
17171
17244
 
17172
17245
  // src/storage/session-store.ts
17173
- import * as fs6 from "fs";
17174
- import * as path6 from "path";
17246
+ import * as fs7 from "fs";
17247
+ import * as path7 from "path";
17175
17248
  import * as YAML4 from "yaml";
17176
17249
  var SessionStore = class {
17177
17250
  filePath;
17178
17251
  constructor(marvinDir) {
17179
- this.filePath = path6.join(marvinDir, "sessions.yaml");
17252
+ this.filePath = path7.join(marvinDir, "sessions.yaml");
17180
17253
  }
17181
17254
  list() {
17182
17255
  const entries = this.load();
@@ -17217,9 +17290,9 @@ var SessionStore = class {
17217
17290
  this.write(entries);
17218
17291
  }
17219
17292
  load() {
17220
- if (!fs6.existsSync(this.filePath)) return [];
17293
+ if (!fs7.existsSync(this.filePath)) return [];
17221
17294
  try {
17222
- const raw = fs6.readFileSync(this.filePath, "utf-8");
17295
+ const raw = fs7.readFileSync(this.filePath, "utf-8");
17223
17296
  const parsed = YAML4.parse(raw);
17224
17297
  if (!Array.isArray(parsed)) return [];
17225
17298
  return parsed;
@@ -17228,11 +17301,11 @@ var SessionStore = class {
17228
17301
  }
17229
17302
  }
17230
17303
  write(entries) {
17231
- const dir = path6.dirname(this.filePath);
17232
- if (!fs6.existsSync(dir)) {
17233
- fs6.mkdirSync(dir, { recursive: true });
17304
+ const dir = path7.dirname(this.filePath);
17305
+ if (!fs7.existsSync(dir)) {
17306
+ fs7.mkdirSync(dir, { recursive: true });
17234
17307
  }
17235
- fs6.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
17308
+ fs7.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
17236
17309
  }
17237
17310
  };
17238
17311
 
@@ -17329,7 +17402,8 @@ function createDecisionTools(store) {
17329
17402
  title: external_exports.string().optional().describe("New title"),
17330
17403
  status: external_exports.string().optional().describe("New status"),
17331
17404
  content: external_exports.string().optional().describe("New content"),
17332
- owner: external_exports.string().optional().describe("New owner")
17405
+ owner: external_exports.string().optional().describe("New owner"),
17406
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
17333
17407
  },
17334
17408
  async (args) => {
17335
17409
  const { id, content, ...updates } = args;
@@ -17480,11 +17554,21 @@ function createActionTools(store) {
17480
17554
  owner: external_exports.string().optional().describe("New owner"),
17481
17555
  priority: external_exports.string().optional().describe("New priority"),
17482
17556
  dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
17557
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
17483
17558
  sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
17484
17559
  },
17485
17560
  async (args) => {
17486
- const { id, content, sprints, ...updates } = args;
17487
- if (sprints !== void 0) {
17561
+ const { id, content, sprints, tags, ...updates } = args;
17562
+ if (tags !== void 0) {
17563
+ const merged = [...tags];
17564
+ if (sprints) {
17565
+ for (const s of sprints) {
17566
+ const tag = `sprint:${s}`;
17567
+ if (!merged.includes(tag)) merged.push(tag);
17568
+ }
17569
+ }
17570
+ updates.tags = merged;
17571
+ } else if (sprints !== void 0) {
17488
17572
  const existing = store.get(id);
17489
17573
  if (!existing) {
17490
17574
  return {
@@ -17631,7 +17715,8 @@ function createQuestionTools(store) {
17631
17715
  title: external_exports.string().optional().describe("New title"),
17632
17716
  status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
17633
17717
  content: external_exports.string().optional().describe("Updated content / answer"),
17634
- owner: external_exports.string().optional().describe("New owner")
17718
+ owner: external_exports.string().optional().describe("New owner"),
17719
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
17635
17720
  },
17636
17721
  async (args) => {
17637
17722
  const { id, content, ...updates } = args;
@@ -19271,8 +19356,8 @@ import * as http from "http";
19271
19356
  import { exec } from "child_process";
19272
19357
 
19273
19358
  // src/skills/registry.ts
19274
- import * as fs7 from "fs";
19275
- import * as path7 from "path";
19359
+ import * as fs8 from "fs";
19360
+ import * as path8 from "path";
19276
19361
  import { fileURLToPath } from "url";
19277
19362
  import * as YAML5 from "yaml";
19278
19363
  import matter2 from "gray-matter";
@@ -19325,8 +19410,8 @@ var JiraClient = class {
19325
19410
  this.baseUrl = `https://${config2.host}/rest/api/2`;
19326
19411
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
19327
19412
  }
19328
- async request(path18, method = "GET", body) {
19329
- const url2 = `${this.baseUrl}${path18}`;
19413
+ async request(path20, method = "GET", body) {
19414
+ const url2 = `${this.baseUrl}${path20}`;
19330
19415
  const headers = {
19331
19416
  Authorization: this.authHeader,
19332
19417
  "Content-Type": "application/json",
@@ -19340,7 +19425,7 @@ var JiraClient = class {
19340
19425
  if (!response.ok) {
19341
19426
  const text = await response.text().catch(() => "");
19342
19427
  throw new Error(
19343
- `Jira API error ${response.status} ${method} ${path18}: ${text}`
19428
+ `Jira API error ${response.status} ${method} ${path20}: ${text}`
19344
19429
  );
19345
19430
  }
19346
19431
  if (response.status === 204) return void 0;
@@ -19820,13 +19905,13 @@ var GOVERNANCE_TOOL_NAMES = [
19820
19905
  ];
19821
19906
  function getBuiltinSkillsDir() {
19822
19907
  const thisFile = fileURLToPath(import.meta.url);
19823
- return path7.join(path7.dirname(thisFile), "builtin");
19908
+ return path8.join(path8.dirname(thisFile), "builtin");
19824
19909
  }
19825
19910
  function loadSkillFromDirectory(dirPath) {
19826
- const skillMdPath = path7.join(dirPath, "SKILL.md");
19827
- if (!fs7.existsSync(skillMdPath)) return void 0;
19911
+ const skillMdPath = path8.join(dirPath, "SKILL.md");
19912
+ if (!fs8.existsSync(skillMdPath)) return void 0;
19828
19913
  try {
19829
- const raw = fs7.readFileSync(skillMdPath, "utf-8");
19914
+ const raw = fs8.readFileSync(skillMdPath, "utf-8");
19830
19915
  const { data, content } = matter2(raw);
19831
19916
  if (!data.name || !data.description) return void 0;
19832
19917
  const metadata = data.metadata ?? {};
@@ -19837,13 +19922,13 @@ function loadSkillFromDirectory(dirPath) {
19837
19922
  if (wildcardPrompt) {
19838
19923
  promptFragments["*"] = wildcardPrompt;
19839
19924
  }
19840
- const personasDir = path7.join(dirPath, "personas");
19841
- if (fs7.existsSync(personasDir)) {
19925
+ const personasDir = path8.join(dirPath, "personas");
19926
+ if (fs8.existsSync(personasDir)) {
19842
19927
  try {
19843
- for (const file2 of fs7.readdirSync(personasDir)) {
19928
+ for (const file2 of fs8.readdirSync(personasDir)) {
19844
19929
  if (!file2.endsWith(".md")) continue;
19845
19930
  const personaId = file2.replace(/\.md$/, "");
19846
- const personaPrompt = fs7.readFileSync(path7.join(personasDir, file2), "utf-8").trim();
19931
+ const personaPrompt = fs8.readFileSync(path8.join(personasDir, file2), "utf-8").trim();
19847
19932
  if (personaPrompt) {
19848
19933
  promptFragments[personaId] = personaPrompt;
19849
19934
  }
@@ -19852,10 +19937,10 @@ function loadSkillFromDirectory(dirPath) {
19852
19937
  }
19853
19938
  }
19854
19939
  let actions;
19855
- const actionsPath = path7.join(dirPath, "actions.yaml");
19856
- if (fs7.existsSync(actionsPath)) {
19940
+ const actionsPath = path8.join(dirPath, "actions.yaml");
19941
+ if (fs8.existsSync(actionsPath)) {
19857
19942
  try {
19858
- const actionsRaw = fs7.readFileSync(actionsPath, "utf-8");
19943
+ const actionsRaw = fs8.readFileSync(actionsPath, "utf-8");
19859
19944
  actions = YAML5.parse(actionsRaw);
19860
19945
  } catch {
19861
19946
  }
@@ -19882,10 +19967,10 @@ function loadAllSkills(marvinDir) {
19882
19967
  }
19883
19968
  try {
19884
19969
  const builtinDir = getBuiltinSkillsDir();
19885
- if (fs7.existsSync(builtinDir)) {
19886
- for (const entry of fs7.readdirSync(builtinDir)) {
19887
- const entryPath = path7.join(builtinDir, entry);
19888
- if (!fs7.statSync(entryPath).isDirectory()) continue;
19970
+ if (fs8.existsSync(builtinDir)) {
19971
+ for (const entry of fs8.readdirSync(builtinDir)) {
19972
+ const entryPath = path8.join(builtinDir, entry);
19973
+ if (!fs8.statSync(entryPath).isDirectory()) continue;
19889
19974
  if (skills.has(entry)) continue;
19890
19975
  const skill = loadSkillFromDirectory(entryPath);
19891
19976
  if (skill) skills.set(skill.id, skill);
@@ -19894,18 +19979,18 @@ function loadAllSkills(marvinDir) {
19894
19979
  } catch {
19895
19980
  }
19896
19981
  if (marvinDir) {
19897
- const skillsDir = path7.join(marvinDir, "skills");
19898
- if (fs7.existsSync(skillsDir)) {
19982
+ const skillsDir = path8.join(marvinDir, "skills");
19983
+ if (fs8.existsSync(skillsDir)) {
19899
19984
  let entries;
19900
19985
  try {
19901
- entries = fs7.readdirSync(skillsDir);
19986
+ entries = fs8.readdirSync(skillsDir);
19902
19987
  } catch {
19903
19988
  entries = [];
19904
19989
  }
19905
19990
  for (const entry of entries) {
19906
- const entryPath = path7.join(skillsDir, entry);
19991
+ const entryPath = path8.join(skillsDir, entry);
19907
19992
  try {
19908
- if (fs7.statSync(entryPath).isDirectory()) {
19993
+ if (fs8.statSync(entryPath).isDirectory()) {
19909
19994
  const skill = loadSkillFromDirectory(entryPath);
19910
19995
  if (skill) skills.set(skill.id, skill);
19911
19996
  continue;
@@ -19915,7 +20000,7 @@ function loadAllSkills(marvinDir) {
19915
20000
  }
19916
20001
  if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
19917
20002
  try {
19918
- const raw = fs7.readFileSync(entryPath, "utf-8");
20003
+ const raw = fs8.readFileSync(entryPath, "utf-8");
19919
20004
  const parsed = YAML5.parse(raw);
19920
20005
  if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
19921
20006
  const skill = {
@@ -20020,12 +20105,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
20020
20105
  return agents;
20021
20106
  }
20022
20107
  function migrateYamlToSkillMd(yamlPath, outputDir) {
20023
- const raw = fs7.readFileSync(yamlPath, "utf-8");
20108
+ const raw = fs8.readFileSync(yamlPath, "utf-8");
20024
20109
  const parsed = YAML5.parse(raw);
20025
20110
  if (!parsed?.id || !parsed?.name) {
20026
20111
  throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
20027
20112
  }
20028
- fs7.mkdirSync(outputDir, { recursive: true });
20113
+ fs8.mkdirSync(outputDir, { recursive: true });
20029
20114
  const frontmatter = {
20030
20115
  name: parsed.id,
20031
20116
  description: parsed.description ?? ""
@@ -20039,15 +20124,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
20039
20124
  const skillMd = matter2.stringify(wildcardPrompt ? `
20040
20125
  ${wildcardPrompt}
20041
20126
  ` : "\n", frontmatter);
20042
- fs7.writeFileSync(path7.join(outputDir, "SKILL.md"), skillMd, "utf-8");
20127
+ fs8.writeFileSync(path8.join(outputDir, "SKILL.md"), skillMd, "utf-8");
20043
20128
  if (promptFragments) {
20044
20129
  const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
20045
20130
  if (personaKeys.length > 0) {
20046
- const personasDir = path7.join(outputDir, "personas");
20047
- fs7.mkdirSync(personasDir, { recursive: true });
20131
+ const personasDir = path8.join(outputDir, "personas");
20132
+ fs8.mkdirSync(personasDir, { recursive: true });
20048
20133
  for (const personaId of personaKeys) {
20049
- fs7.writeFileSync(
20050
- path7.join(personasDir, `${personaId}.md`),
20134
+ fs8.writeFileSync(
20135
+ path8.join(personasDir, `${personaId}.md`),
20051
20136
  `${promptFragments[personaId]}
20052
20137
  `,
20053
20138
  "utf-8"
@@ -20057,8 +20142,8 @@ ${wildcardPrompt}
20057
20142
  }
20058
20143
  const actions = parsed.actions;
20059
20144
  if (actions && actions.length > 0) {
20060
- fs7.writeFileSync(
20061
- path7.join(outputDir, "actions.yaml"),
20145
+ fs8.writeFileSync(
20146
+ path8.join(outputDir, "actions.yaml"),
20062
20147
  YAML5.stringify(actions),
20063
20148
  "utf-8"
20064
20149
  );
@@ -20332,8 +20417,8 @@ function slugify3(text) {
20332
20417
  }
20333
20418
 
20334
20419
  // src/sources/manifest.ts
20335
- import * as fs8 from "fs";
20336
- import * as path8 from "path";
20420
+ import * as fs9 from "fs";
20421
+ import * as path9 from "path";
20337
20422
  import * as crypto from "crypto";
20338
20423
  import * as YAML6 from "yaml";
20339
20424
  var MANIFEST_FILE = ".manifest.yaml";
@@ -20346,37 +20431,37 @@ var SourceManifestManager = class {
20346
20431
  manifestPath;
20347
20432
  sourcesDir;
20348
20433
  constructor(marvinDir) {
20349
- this.sourcesDir = path8.join(marvinDir, "sources");
20350
- this.manifestPath = path8.join(this.sourcesDir, MANIFEST_FILE);
20434
+ this.sourcesDir = path9.join(marvinDir, "sources");
20435
+ this.manifestPath = path9.join(this.sourcesDir, MANIFEST_FILE);
20351
20436
  this.manifest = this.load();
20352
20437
  }
20353
20438
  load() {
20354
- if (!fs8.existsSync(this.manifestPath)) {
20439
+ if (!fs9.existsSync(this.manifestPath)) {
20355
20440
  return emptyManifest();
20356
20441
  }
20357
- const raw = fs8.readFileSync(this.manifestPath, "utf-8");
20442
+ const raw = fs9.readFileSync(this.manifestPath, "utf-8");
20358
20443
  const parsed = YAML6.parse(raw);
20359
20444
  return parsed ?? emptyManifest();
20360
20445
  }
20361
20446
  save() {
20362
- fs8.mkdirSync(this.sourcesDir, { recursive: true });
20363
- fs8.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
20447
+ fs9.mkdirSync(this.sourcesDir, { recursive: true });
20448
+ fs9.writeFileSync(this.manifestPath, YAML6.stringify(this.manifest), "utf-8");
20364
20449
  }
20365
20450
  scan() {
20366
20451
  const added = [];
20367
20452
  const changed = [];
20368
20453
  const removed = [];
20369
- if (!fs8.existsSync(this.sourcesDir)) {
20454
+ if (!fs9.existsSync(this.sourcesDir)) {
20370
20455
  return { added, changed, removed };
20371
20456
  }
20372
20457
  const onDisk = new Set(
20373
- fs8.readdirSync(this.sourcesDir).filter((f) => {
20374
- const ext = path8.extname(f).toLowerCase();
20458
+ fs9.readdirSync(this.sourcesDir).filter((f) => {
20459
+ const ext = path9.extname(f).toLowerCase();
20375
20460
  return SOURCE_EXTENSIONS.includes(ext);
20376
20461
  })
20377
20462
  );
20378
20463
  for (const fileName of onDisk) {
20379
- const filePath = path8.join(this.sourcesDir, fileName);
20464
+ const filePath = path9.join(this.sourcesDir, fileName);
20380
20465
  const hash2 = this.hashFile(filePath);
20381
20466
  const existing = this.manifest.files[fileName];
20382
20467
  if (!existing) {
@@ -20439,7 +20524,7 @@ var SourceManifestManager = class {
20439
20524
  this.save();
20440
20525
  }
20441
20526
  hashFile(filePath) {
20442
- const content = fs8.readFileSync(filePath);
20527
+ const content = fs9.readFileSync(filePath);
20443
20528
  return crypto.createHash("sha256").update(content).digest("hex");
20444
20529
  }
20445
20530
  };
@@ -20454,8 +20539,8 @@ async function startSession(options) {
20454
20539
  const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
20455
20540
  const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
20456
20541
  const sessionStore = new SessionStore(marvinDir);
20457
- const sourcesDir = path9.join(marvinDir, "sources");
20458
- const hasSourcesDir = fs9.existsSync(sourcesDir);
20542
+ const sourcesDir = path10.join(marvinDir, "sources");
20543
+ const hasSourcesDir = fs10.existsSync(sourcesDir);
20459
20544
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
20460
20545
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
20461
20546
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
@@ -20478,7 +20563,7 @@ async function startSession(options) {
20478
20563
  projectName: config2.project.name,
20479
20564
  navGroups
20480
20565
  });
20481
- const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment);
20566
+ const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment, marvinDir);
20482
20567
  let existingSession;
20483
20568
  if (options.sessionName) {
20484
20569
  existingSession = sessionStore.get(options.sessionName);
@@ -20931,13 +21016,13 @@ async function setApiKey() {
20931
21016
  }
20932
21017
 
20933
21018
  // src/cli/commands/ingest.ts
20934
- import * as fs11 from "fs";
20935
- import * as path11 from "path";
21019
+ import * as fs12 from "fs";
21020
+ import * as path12 from "path";
20936
21021
  import chalk8 from "chalk";
20937
21022
 
20938
21023
  // src/sources/ingest.ts
20939
- import * as fs10 from "fs";
20940
- import * as path10 from "path";
21024
+ import * as fs11 from "fs";
21025
+ import * as path11 from "path";
20941
21026
  import chalk7 from "chalk";
20942
21027
  import ora2 from "ora";
20943
21028
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
@@ -21040,15 +21125,15 @@ async function ingestFile(options) {
21040
21125
  const persona = getPersona(personaId);
21041
21126
  const manifest = new SourceManifestManager(marvinDir);
21042
21127
  const sourcesDir = manifest.sourcesDir;
21043
- const filePath = path10.join(sourcesDir, fileName);
21044
- if (!fs10.existsSync(filePath)) {
21128
+ const filePath = path11.join(sourcesDir, fileName);
21129
+ if (!fs11.existsSync(filePath)) {
21045
21130
  throw new Error(`Source file not found: ${filePath}`);
21046
21131
  }
21047
- const ext = path10.extname(fileName).toLowerCase();
21132
+ const ext = path11.extname(fileName).toLowerCase();
21048
21133
  const isPdf = ext === ".pdf";
21049
21134
  let fileContent = null;
21050
21135
  if (!isPdf) {
21051
- fileContent = fs10.readFileSync(filePath, "utf-8");
21136
+ fileContent = fs11.readFileSync(filePath, "utf-8");
21052
21137
  }
21053
21138
  const store = new DocumentStore(marvinDir);
21054
21139
  const createdArtifacts = [];
@@ -21151,9 +21236,9 @@ Ingest ended with error: ${message.subtype}`)
21151
21236
  async function ingestCommand(file2, options) {
21152
21237
  const project = loadProject();
21153
21238
  const marvinDir = project.marvinDir;
21154
- const sourcesDir = path11.join(marvinDir, "sources");
21155
- if (!fs11.existsSync(sourcesDir)) {
21156
- fs11.mkdirSync(sourcesDir, { recursive: true });
21239
+ const sourcesDir = path12.join(marvinDir, "sources");
21240
+ if (!fs12.existsSync(sourcesDir)) {
21241
+ fs12.mkdirSync(sourcesDir, { recursive: true });
21157
21242
  }
21158
21243
  const manifest = new SourceManifestManager(marvinDir);
21159
21244
  manifest.scan();
@@ -21164,8 +21249,8 @@ async function ingestCommand(file2, options) {
21164
21249
  return;
21165
21250
  }
21166
21251
  if (file2) {
21167
- const filePath = path11.join(sourcesDir, file2);
21168
- if (!fs11.existsSync(filePath)) {
21252
+ const filePath = path12.join(sourcesDir, file2);
21253
+ if (!fs12.existsSync(filePath)) {
21169
21254
  console.log(chalk8.red(`Source file not found: ${file2}`));
21170
21255
  console.log(chalk8.dim(`Expected at: ${filePath}`));
21171
21256
  console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
@@ -21232,7 +21317,7 @@ import ora3 from "ora";
21232
21317
  import { input as input3 } from "@inquirer/prompts";
21233
21318
 
21234
21319
  // src/git/repository.ts
21235
- import * as path12 from "path";
21320
+ import * as path13 from "path";
21236
21321
  import simpleGit from "simple-git";
21237
21322
  var MARVIN_GITIGNORE = `node_modules/
21238
21323
  .DS_Store
@@ -21252,7 +21337,7 @@ var DIR_TYPE_LABELS = {
21252
21337
  function buildCommitMessage(files) {
21253
21338
  const counts = /* @__PURE__ */ new Map();
21254
21339
  for (const f of files) {
21255
- const parts2 = f.split(path12.sep).join("/").split("/");
21340
+ const parts2 = f.split(path13.sep).join("/").split("/");
21256
21341
  const docsIdx = parts2.indexOf("docs");
21257
21342
  if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
21258
21343
  const dirName = parts2[docsIdx + 1];
@@ -21292,9 +21377,9 @@ var MarvinGit = class {
21292
21377
  );
21293
21378
  }
21294
21379
  await this.git.init();
21295
- const { writeFileSync: writeFileSync9 } = await import("fs");
21296
- writeFileSync9(
21297
- path12.join(this.marvinDir, ".gitignore"),
21380
+ const { writeFileSync: writeFileSync10 } = await import("fs");
21381
+ writeFileSync10(
21382
+ path13.join(this.marvinDir, ".gitignore"),
21298
21383
  MARVIN_GITIGNORE,
21299
21384
  "utf-8"
21300
21385
  );
@@ -21414,9 +21499,9 @@ var MarvinGit = class {
21414
21499
  }
21415
21500
  }
21416
21501
  static async clone(url2, targetDir) {
21417
- const marvinDir = path12.join(targetDir, ".marvin");
21418
- const { existsSync: existsSync16 } = await import("fs");
21419
- if (existsSync16(marvinDir)) {
21502
+ const marvinDir = path13.join(targetDir, ".marvin");
21503
+ const { existsSync: existsSync17 } = await import("fs");
21504
+ if (existsSync17(marvinDir)) {
21420
21505
  throw new GitSyncError(
21421
21506
  `.marvin/ already exists at ${targetDir}. Remove it first or choose a different directory.`
21422
21507
  );
@@ -21594,8 +21679,8 @@ async function cloneCommand(url2, directory) {
21594
21679
  }
21595
21680
 
21596
21681
  // src/mcp/stdio-server.ts
21597
- import * as fs12 from "fs";
21598
- import * as path13 from "path";
21682
+ import * as fs13 from "fs";
21683
+ import * as path14 from "path";
21599
21684
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21600
21685
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21601
21686
 
@@ -21893,8 +21978,8 @@ function collectTools(marvinDir) {
21893
21978
  const plugin = resolvePlugin(config2.methodology);
21894
21979
  const registrations = plugin?.documentTypeRegistrations ?? [];
21895
21980
  const store = new DocumentStore(marvinDir, registrations);
21896
- const sourcesDir = path13.join(marvinDir, "sources");
21897
- const hasSourcesDir = fs12.existsSync(sourcesDir);
21981
+ const sourcesDir = path14.join(marvinDir, "sources");
21982
+ const hasSourcesDir = fs13.existsSync(sourcesDir);
21898
21983
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
21899
21984
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
21900
21985
  const sessionStore = new SessionStore(marvinDir);
@@ -21902,8 +21987,14 @@ function collectTools(marvinDir) {
21902
21987
  const allSkillIds = [...allSkills.keys()];
21903
21988
  const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
21904
21989
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
21905
- const projectRoot = path13.dirname(marvinDir);
21990
+ const projectRoot = path14.dirname(marvinDir);
21906
21991
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
21992
+ const allSkillRegs = collectSkillRegistrations(allSkillIds, allSkills);
21993
+ const navGroups = buildNavGroups({
21994
+ pluginRegs: registrations,
21995
+ skillRegs: allSkillRegs,
21996
+ pluginName: plugin?.name
21997
+ });
21907
21998
  return [
21908
21999
  ...createDecisionTools(store),
21909
22000
  ...createActionTools(store),
@@ -21911,6 +22002,7 @@ function collectTools(marvinDir) {
21911
22002
  ...createDocumentTools(store),
21912
22003
  ...manifest ? createSourceTools(manifest) : [],
21913
22004
  ...createSessionTools(sessionStore),
22005
+ ...createWebTools(store, config2.name, navGroups),
21914
22006
  ...pluginTools,
21915
22007
  ...codeSkillTools,
21916
22008
  ...actionTools
@@ -21965,8 +22057,8 @@ async function serveCommand() {
21965
22057
  }
21966
22058
 
21967
22059
  // src/cli/commands/skills.ts
21968
- import * as fs13 from "fs";
21969
- import * as path14 from "path";
22060
+ import * as fs14 from "fs";
22061
+ import * as path15 from "path";
21970
22062
  import * as YAML7 from "yaml";
21971
22063
  import matter3 from "gray-matter";
21972
22064
  import chalk10 from "chalk";
@@ -22072,14 +22164,14 @@ async function skillsRemoveCommand(skillId, options) {
22072
22164
  }
22073
22165
  async function skillsCreateCommand(name) {
22074
22166
  const project = loadProject();
22075
- const skillsDir = path14.join(project.marvinDir, "skills");
22076
- fs13.mkdirSync(skillsDir, { recursive: true });
22077
- const skillDir = path14.join(skillsDir, name);
22078
- if (fs13.existsSync(skillDir)) {
22167
+ const skillsDir = path15.join(project.marvinDir, "skills");
22168
+ fs14.mkdirSync(skillsDir, { recursive: true });
22169
+ const skillDir = path15.join(skillsDir, name);
22170
+ if (fs14.existsSync(skillDir)) {
22079
22171
  console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
22080
22172
  return;
22081
22173
  }
22082
- fs13.mkdirSync(skillDir, { recursive: true });
22174
+ fs14.mkdirSync(skillDir, { recursive: true });
22083
22175
  const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
22084
22176
  const frontmatter = {
22085
22177
  name,
@@ -22093,7 +22185,7 @@ async function skillsCreateCommand(name) {
22093
22185
  You have the **${displayName}** skill.
22094
22186
  `;
22095
22187
  const skillMd = matter3.stringify(body, frontmatter);
22096
- fs13.writeFileSync(path14.join(skillDir, "SKILL.md"), skillMd, "utf-8");
22188
+ fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
22097
22189
  const actions = [
22098
22190
  {
22099
22191
  id: "run",
@@ -22103,7 +22195,7 @@ You have the **${displayName}** skill.
22103
22195
  maxTurns: 5
22104
22196
  }
22105
22197
  ];
22106
- fs13.writeFileSync(path14.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
22198
+ fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
22107
22199
  console.log(chalk10.green(`Created skill: ${skillDir}/`));
22108
22200
  console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
22109
22201
  console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
@@ -22111,14 +22203,14 @@ You have the **${displayName}** skill.
22111
22203
  }
22112
22204
  async function skillsMigrateCommand() {
22113
22205
  const project = loadProject();
22114
- const skillsDir = path14.join(project.marvinDir, "skills");
22115
- if (!fs13.existsSync(skillsDir)) {
22206
+ const skillsDir = path15.join(project.marvinDir, "skills");
22207
+ if (!fs14.existsSync(skillsDir)) {
22116
22208
  console.log(chalk10.dim("No skills directory found."));
22117
22209
  return;
22118
22210
  }
22119
22211
  let entries;
22120
22212
  try {
22121
- entries = fs13.readdirSync(skillsDir);
22213
+ entries = fs14.readdirSync(skillsDir);
22122
22214
  } catch {
22123
22215
  console.log(chalk10.red("Could not read skills directory."));
22124
22216
  return;
@@ -22130,16 +22222,16 @@ async function skillsMigrateCommand() {
22130
22222
  }
22131
22223
  let migrated = 0;
22132
22224
  for (const file2 of yamlFiles) {
22133
- const yamlPath = path14.join(skillsDir, file2);
22225
+ const yamlPath = path15.join(skillsDir, file2);
22134
22226
  const baseName = file2.replace(/\.(yaml|yml)$/, "");
22135
- const outputDir = path14.join(skillsDir, baseName);
22136
- if (fs13.existsSync(outputDir)) {
22227
+ const outputDir = path15.join(skillsDir, baseName);
22228
+ if (fs14.existsSync(outputDir)) {
22137
22229
  console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
22138
22230
  continue;
22139
22231
  }
22140
22232
  try {
22141
22233
  migrateYamlToSkillMd(yamlPath, outputDir);
22142
- fs13.renameSync(yamlPath, `${yamlPath}.bak`);
22234
+ fs14.renameSync(yamlPath, `${yamlPath}.bak`);
22143
22235
  console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
22144
22236
  migrated++;
22145
22237
  } catch (err) {
@@ -22153,35 +22245,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
22153
22245
  }
22154
22246
 
22155
22247
  // src/cli/commands/import.ts
22156
- import * as fs16 from "fs";
22157
- import * as path17 from "path";
22248
+ import * as fs17 from "fs";
22249
+ import * as path18 from "path";
22158
22250
  import chalk11 from "chalk";
22159
22251
 
22160
22252
  // src/import/engine.ts
22161
- import * as fs15 from "fs";
22162
- import * as path16 from "path";
22253
+ import * as fs16 from "fs";
22254
+ import * as path17 from "path";
22163
22255
  import matter5 from "gray-matter";
22164
22256
 
22165
22257
  // src/import/classifier.ts
22166
- import * as fs14 from "fs";
22167
- import * as path15 from "path";
22258
+ import * as fs15 from "fs";
22259
+ import * as path16 from "path";
22168
22260
  import matter4 from "gray-matter";
22169
22261
  var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
22170
22262
  var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
22171
22263
  var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
22172
22264
  function classifyPath(inputPath, knownTypes, knownDirNames) {
22173
- const resolved = path15.resolve(inputPath);
22174
- const stat = fs14.statSync(resolved);
22265
+ const resolved = path16.resolve(inputPath);
22266
+ const stat = fs15.statSync(resolved);
22175
22267
  if (!stat.isDirectory()) {
22176
22268
  return classifyFile(resolved, knownTypes);
22177
22269
  }
22178
- if (path15.basename(resolved) === ".marvin" || fs14.existsSync(path15.join(resolved, "config.yaml"))) {
22270
+ if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
22179
22271
  return { type: "marvin-project", inputPath: resolved };
22180
22272
  }
22181
22273
  const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
22182
- const entries = fs14.readdirSync(resolved);
22274
+ const entries = fs15.readdirSync(resolved);
22183
22275
  const hasDocSubdirs = entries.some(
22184
- (e) => allDirNames.has(e) && fs14.statSync(path15.join(resolved, e)).isDirectory()
22276
+ (e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
22185
22277
  );
22186
22278
  if (hasDocSubdirs) {
22187
22279
  return { type: "docs-directory", inputPath: resolved };
@@ -22190,7 +22282,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
22190
22282
  if (mdFiles.length > 0) {
22191
22283
  const hasMarvinDocs = mdFiles.some((f) => {
22192
22284
  try {
22193
- const raw = fs14.readFileSync(path15.join(resolved, f), "utf-8");
22285
+ const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
22194
22286
  const { data } = matter4(raw);
22195
22287
  return isValidMarvinDocument(data, knownTypes);
22196
22288
  } catch {
@@ -22204,14 +22296,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
22204
22296
  return { type: "raw-source-dir", inputPath: resolved };
22205
22297
  }
22206
22298
  function classifyFile(filePath, knownTypes) {
22207
- const resolved = path15.resolve(filePath);
22208
- const ext = path15.extname(resolved).toLowerCase();
22299
+ const resolved = path16.resolve(filePath);
22300
+ const ext = path16.extname(resolved).toLowerCase();
22209
22301
  if (RAW_SOURCE_EXTENSIONS.has(ext)) {
22210
22302
  return { type: "raw-source-file", inputPath: resolved };
22211
22303
  }
22212
22304
  if (ext === ".md") {
22213
22305
  try {
22214
- const raw = fs14.readFileSync(resolved, "utf-8");
22306
+ const raw = fs15.readFileSync(resolved, "utf-8");
22215
22307
  const { data } = matter4(raw);
22216
22308
  if (isValidMarvinDocument(data, knownTypes)) {
22217
22309
  return { type: "marvin-document", inputPath: resolved };
@@ -22336,9 +22428,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
22336
22428
  continue;
22337
22429
  }
22338
22430
  if (item.action === "copy") {
22339
- const targetDir = path16.dirname(item.targetPath);
22340
- fs15.mkdirSync(targetDir, { recursive: true });
22341
- fs15.copyFileSync(item.sourcePath, item.targetPath);
22431
+ const targetDir = path17.dirname(item.targetPath);
22432
+ fs16.mkdirSync(targetDir, { recursive: true });
22433
+ fs16.copyFileSync(item.sourcePath, item.targetPath);
22342
22434
  copied++;
22343
22435
  continue;
22344
22436
  }
@@ -22374,19 +22466,19 @@ function formatPlanSummary(plan) {
22374
22466
  lines.push(`Documents to import: ${imports.length}`);
22375
22467
  for (const item of imports) {
22376
22468
  const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
22377
- lines.push(` ${idInfo} ${path16.basename(item.sourcePath)}`);
22469
+ lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
22378
22470
  }
22379
22471
  }
22380
22472
  if (copies.length > 0) {
22381
22473
  lines.push(`Files to copy to sources/: ${copies.length}`);
22382
22474
  for (const item of copies) {
22383
- lines.push(` ${path16.basename(item.sourcePath)} \u2192 ${path16.basename(item.targetPath)}`);
22475
+ lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
22384
22476
  }
22385
22477
  }
22386
22478
  if (skips.length > 0) {
22387
22479
  lines.push(`Skipped (conflict): ${skips.length}`);
22388
22480
  for (const item of skips) {
22389
- lines.push(` ${item.originalId ?? path16.basename(item.sourcePath)} ${item.reason ?? ""}`);
22481
+ lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
22390
22482
  }
22391
22483
  }
22392
22484
  if (plan.items.length === 0) {
@@ -22419,11 +22511,11 @@ function getDirNameForType(store, type) {
22419
22511
  }
22420
22512
  function collectMarvinDocs(dir, knownTypes) {
22421
22513
  const docs = [];
22422
- const files = fs15.readdirSync(dir).filter((f) => f.endsWith(".md"));
22514
+ const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
22423
22515
  for (const file2 of files) {
22424
- const filePath = path16.join(dir, file2);
22516
+ const filePath = path17.join(dir, file2);
22425
22517
  try {
22426
- const raw = fs15.readFileSync(filePath, "utf-8");
22518
+ const raw = fs16.readFileSync(filePath, "utf-8");
22427
22519
  const { data, content } = matter5(raw);
22428
22520
  if (isValidMarvinDocument(data, knownTypes)) {
22429
22521
  docs.push({
@@ -22479,23 +22571,23 @@ function planDocImports(docs, store, options) {
22479
22571
  }
22480
22572
  function planFromMarvinProject(classification, store, _marvinDir, options) {
22481
22573
  let projectDir = classification.inputPath;
22482
- if (path16.basename(projectDir) !== ".marvin") {
22483
- const inner = path16.join(projectDir, ".marvin");
22484
- if (fs15.existsSync(inner)) {
22574
+ if (path17.basename(projectDir) !== ".marvin") {
22575
+ const inner = path17.join(projectDir, ".marvin");
22576
+ if (fs16.existsSync(inner)) {
22485
22577
  projectDir = inner;
22486
22578
  }
22487
22579
  }
22488
- const docsDir = path16.join(projectDir, "docs");
22489
- if (!fs15.existsSync(docsDir)) {
22580
+ const docsDir = path17.join(projectDir, "docs");
22581
+ if (!fs16.existsSync(docsDir)) {
22490
22582
  return [];
22491
22583
  }
22492
22584
  const knownTypes = store.registeredTypes;
22493
22585
  const allDocs = [];
22494
- const subdirs = fs15.readdirSync(docsDir).filter(
22495
- (d) => fs15.statSync(path16.join(docsDir, d)).isDirectory()
22586
+ const subdirs = fs16.readdirSync(docsDir).filter(
22587
+ (d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
22496
22588
  );
22497
22589
  for (const subdir of subdirs) {
22498
- const docs = collectMarvinDocs(path16.join(docsDir, subdir), knownTypes);
22590
+ const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
22499
22591
  allDocs.push(...docs);
22500
22592
  }
22501
22593
  return planDocImports(allDocs, store, options);
@@ -22505,10 +22597,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
22505
22597
  const knownTypes = store.registeredTypes;
22506
22598
  const allDocs = [];
22507
22599
  allDocs.push(...collectMarvinDocs(dir, knownTypes));
22508
- const entries = fs15.readdirSync(dir);
22600
+ const entries = fs16.readdirSync(dir);
22509
22601
  for (const entry of entries) {
22510
- const entryPath = path16.join(dir, entry);
22511
- if (fs15.statSync(entryPath).isDirectory()) {
22602
+ const entryPath = path17.join(dir, entry);
22603
+ if (fs16.statSync(entryPath).isDirectory()) {
22512
22604
  allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
22513
22605
  }
22514
22606
  }
@@ -22517,7 +22609,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
22517
22609
  function planFromSingleDocument(classification, store, _marvinDir, options) {
22518
22610
  const filePath = classification.inputPath;
22519
22611
  const knownTypes = store.registeredTypes;
22520
- const raw = fs15.readFileSync(filePath, "utf-8");
22612
+ const raw = fs16.readFileSync(filePath, "utf-8");
22521
22613
  const { data, content } = matter5(raw);
22522
22614
  if (!isValidMarvinDocument(data, knownTypes)) {
22523
22615
  return [];
@@ -22533,14 +22625,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
22533
22625
  }
22534
22626
  function planFromRawSourceDir(classification, marvinDir) {
22535
22627
  const dir = classification.inputPath;
22536
- const sourcesDir = path16.join(marvinDir, "sources");
22628
+ const sourcesDir = path17.join(marvinDir, "sources");
22537
22629
  const items = [];
22538
- const files = fs15.readdirSync(dir).filter((f) => {
22539
- const stat = fs15.statSync(path16.join(dir, f));
22630
+ const files = fs16.readdirSync(dir).filter((f) => {
22631
+ const stat = fs16.statSync(path17.join(dir, f));
22540
22632
  return stat.isFile();
22541
22633
  });
22542
22634
  for (const file2 of files) {
22543
- const sourcePath = path16.join(dir, file2);
22635
+ const sourcePath = path17.join(dir, file2);
22544
22636
  const targetPath = resolveSourceFileName(sourcesDir, file2);
22545
22637
  items.push({
22546
22638
  action: "copy",
@@ -22551,8 +22643,8 @@ function planFromRawSourceDir(classification, marvinDir) {
22551
22643
  return items;
22552
22644
  }
22553
22645
  function planFromRawSourceFile(classification, marvinDir) {
22554
- const sourcesDir = path16.join(marvinDir, "sources");
22555
- const fileName = path16.basename(classification.inputPath);
22646
+ const sourcesDir = path17.join(marvinDir, "sources");
22647
+ const fileName = path17.basename(classification.inputPath);
22556
22648
  const targetPath = resolveSourceFileName(sourcesDir, fileName);
22557
22649
  return [
22558
22650
  {
@@ -22563,25 +22655,25 @@ function planFromRawSourceFile(classification, marvinDir) {
22563
22655
  ];
22564
22656
  }
22565
22657
  function resolveSourceFileName(sourcesDir, fileName) {
22566
- const targetPath = path16.join(sourcesDir, fileName);
22567
- if (!fs15.existsSync(targetPath)) {
22658
+ const targetPath = path17.join(sourcesDir, fileName);
22659
+ if (!fs16.existsSync(targetPath)) {
22568
22660
  return targetPath;
22569
22661
  }
22570
- const ext = path16.extname(fileName);
22571
- const base = path16.basename(fileName, ext);
22662
+ const ext = path17.extname(fileName);
22663
+ const base = path17.basename(fileName, ext);
22572
22664
  let counter = 1;
22573
22665
  let candidate;
22574
22666
  do {
22575
- candidate = path16.join(sourcesDir, `${base}-${counter}${ext}`);
22667
+ candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
22576
22668
  counter++;
22577
- } while (fs15.existsSync(candidate));
22669
+ } while (fs16.existsSync(candidate));
22578
22670
  return candidate;
22579
22671
  }
22580
22672
 
22581
22673
  // src/cli/commands/import.ts
22582
22674
  async function importCommand(inputPath, options) {
22583
- const resolved = path17.resolve(inputPath);
22584
- if (!fs16.existsSync(resolved)) {
22675
+ const resolved = path18.resolve(inputPath);
22676
+ if (!fs17.existsSync(resolved)) {
22585
22677
  throw new ImportError(`Path not found: ${resolved}`);
22586
22678
  }
22587
22679
  const project = loadProject();
@@ -22633,7 +22725,7 @@ async function importCommand(inputPath, options) {
22633
22725
  console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
22634
22726
  const manifest = new SourceManifestManager(marvinDir);
22635
22727
  manifest.scan();
22636
- const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path17.basename(i.targetPath));
22728
+ const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
22637
22729
  for (const fileName of copiedFileNames) {
22638
22730
  try {
22639
22731
  await ingestFile({
@@ -23036,7 +23128,8 @@ The contributor is identifying a project risk.
23036
23128
  - Create actions for risk mitigation tasks
23037
23129
  - Create decisions for risk response strategies
23038
23130
  - Create questions for risks needing further assessment
23039
- - Tag all related artifacts with "risk" for tracking`,
23131
+ - Tag all related artifacts with "risk" for tracking
23132
+ - When a risk is resolved, use the update tool to remove the "risk" tag and add "risk-mitigated" so it no longer inflates the GAR quality metric`,
23040
23133
  "blocker-report": `
23041
23134
  ### Type-Specific Guidance: Blocker Report
23042
23135
  The contributor is reporting a blocker.
@@ -23520,12 +23613,38 @@ async function webCommand(options) {
23520
23613
  await startWebServer({ port, open: options.open });
23521
23614
  }
23522
23615
 
23616
+ // src/cli/commands/generate.ts
23617
+ import * as fs18 from "fs";
23618
+ import * as path19 from "path";
23619
+ import chalk18 from "chalk";
23620
+ import { confirm as confirm2 } from "@inquirer/prompts";
23621
+ async function generateClaudeMdCommand(options) {
23622
+ const project = loadProject();
23623
+ const filePath = path19.join(project.marvinDir, "CLAUDE.md");
23624
+ if (fs18.existsSync(filePath) && !options.force) {
23625
+ const overwrite = await confirm2({
23626
+ message: ".marvin/CLAUDE.md already exists. Overwrite?",
23627
+ default: false
23628
+ });
23629
+ if (!overwrite) {
23630
+ console.log(chalk18.dim("Aborted."));
23631
+ return;
23632
+ }
23633
+ }
23634
+ fs18.writeFileSync(
23635
+ filePath,
23636
+ getDefaultClaudeMdContent(project.config.name),
23637
+ "utf-8"
23638
+ );
23639
+ console.log(chalk18.green("Created .marvin/CLAUDE.md"));
23640
+ }
23641
+
23523
23642
  // src/cli/program.ts
23524
23643
  function createProgram() {
23525
23644
  const program2 = new Command();
23526
23645
  program2.name("marvin").description(
23527
23646
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
23528
- ).version("0.3.5");
23647
+ ).version("0.3.7");
23529
23648
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
23530
23649
  await initCommand();
23531
23650
  });
@@ -23611,6 +23730,10 @@ function createProgram() {
23611
23730
  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) => {
23612
23731
  await webCommand(options);
23613
23732
  });
23733
+ const generateCmd = program2.command("generate").description("Generate project files");
23734
+ generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
23735
+ await generateClaudeMdCommand(options);
23736
+ });
23614
23737
  return program2;
23615
23738
  }
23616
23739