@open-product-primer/cli 2.4.0 → 2.6.0

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.
@@ -48,7 +48,7 @@ function initCommand() {
48
48
  return new commander_1.Command('init')
49
49
  .description('Initialize oprim in the current repository')
50
50
  .option('--name <name>', 'project name (defaults to directory name)')
51
- .option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside)', (val, prev) => [...prev, val], [])
51
+ .option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe)', (val, prev) => [...prev, val], [])
52
52
  .action(async (opts) => {
53
53
  const projectRoot = process.cwd();
54
54
  const projectName = opts.name ?? path.basename(projectRoot);
@@ -90,6 +90,7 @@ function initCommand() {
90
90
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
91
91
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'note.md'), noteContent);
92
92
  (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
93
+ (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-decisions-view.js'), templates_1.decisionsViewScriptTemplate);
93
94
  if (okfEnabled) {
94
95
  (0, scaffold_1.writeFile)(path.join(primerDir, 'index.md'), (0, templates_1.indexTemplate)(projectName));
95
96
  }
@@ -71,6 +71,7 @@ function updateCommand() {
71
71
  const primerDir = path.join(projectRoot, 'oprim');
72
72
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
73
73
  (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
74
+ (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-decisions-view.js'), templates_1.decisionsViewScriptTemplate);
74
75
  const configPath = path.join(primerDir, 'config.yaml');
75
76
  if (fs.existsSync(configPath)) {
76
77
  const existingConfig = fs.readFileSync(configPath, 'utf-8');
@@ -84,6 +84,8 @@ function detectAvailableAgents(projectRoot) {
84
84
  detected.push('gemini');
85
85
  if (fs.existsSync(path.join(projectRoot, '.poolside')))
86
86
  detected.push('poolside');
87
+ if (fs.existsSync(path.join(projectRoot, '.vibe')))
88
+ detected.push('vibe');
87
89
  return detected;
88
90
  }
89
91
  function writeAgentsToConfig(agents, projectRoot) {
@@ -1,4 +1,4 @@
1
- export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside';
1
+ export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe';
2
2
  export declare const SUPPORTED_AGENTS: readonly Agent[];
3
3
  export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
4
4
  export declare function resolveSpecFramework(projectRoot: string): string;
@@ -10,6 +10,7 @@ export declare const OPRIM_CONTEXT_SKILL_STEP = "## Step 0: Check relevant produ
10
10
  export declare const CLAUDE_SKILLS: Record<string, string>;
11
11
  export declare const CLAUDE_COMMANDS: Record<string, string>;
12
12
  export declare const POOLSIDE_SKILLS: Record<string, string>;
13
+ export declare const VIBE_SKILLS: Record<string, string>;
13
14
  export declare const CURSOR_SKILLS: Record<string, string>;
14
15
  export declare const CURSOR_COMMANDS: Record<string, string>;
15
16
  export declare function specAuthoringSkill(): string;
@@ -17,3 +18,4 @@ export declare function writeAgentInstructionFile(filePath: string, section: str
17
18
  export declare function codexInstructions(): string;
18
19
  export declare function geminiInstructions(): string;
19
20
  export declare function poolsideInstructions(): string;
21
+ export declare function vibeInstructions(): string;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
39
+ exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.VIBE_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
40
40
  exports.promptFrameworkSelection = promptFrameworkSelection;
41
41
  exports.resolveSpecFramework = resolveSpecFramework;
42
42
  exports.promptAgentSelection = promptAgentSelection;
@@ -48,6 +48,7 @@ exports.writeAgentInstructionFile = writeAgentInstructionFile;
48
48
  exports.codexInstructions = codexInstructions;
49
49
  exports.geminiInstructions = geminiInstructions;
50
50
  exports.poolsideInstructions = poolsideInstructions;
51
+ exports.vibeInstructions = vibeInstructions;
51
52
  const path = __importStar(require("path"));
52
53
  const fs = __importStar(require("fs"));
53
54
  const chalk_1 = __importDefault(require("chalk"));
@@ -56,7 +57,7 @@ const detect_1 = require("./detect");
56
57
  const config_merge_1 = require("./config-merge");
57
58
  const workflow_schema_1 = require("./workflow-schema");
58
59
  const workflow_renderer_1 = require("./workflow-renderer");
59
- exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'poolside'];
60
+ exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'poolside', 'vibe'];
60
61
  // oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
61
62
  // selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
62
63
  // projects that installed before that key existed.
@@ -90,8 +91,8 @@ async function promptFrameworkSelection(projectRoot) {
90
91
  return select({
91
92
  message: 'Which speccing framework does this project use?',
92
93
  choices: [
93
- { name: 'OpenSpec (recommended)', value: 'openspec' },
94
- { name: 'Native (oprim-authored specs, no OpenSpec required)', value: 'native' },
94
+ { name: 'Native (oprim-authored specs, recommended)', value: 'native' },
95
+ { name: 'OpenSpec', value: 'openspec' },
95
96
  { name: 'None', value: 'none' },
96
97
  ],
97
98
  });
@@ -124,12 +125,13 @@ async function promptAgentSelection(projectRoot) {
124
125
  { name: 'Codex', value: 'codex', checked: detected.includes('codex') },
125
126
  { name: 'Gemini CLI', value: 'gemini', checked: detected.includes('gemini') },
126
127
  { name: 'Poolside', value: 'poolside', checked: detected.includes('poolside') },
128
+ { name: 'Mistral Vibe', value: 'vibe', checked: detected.includes('vibe') },
127
129
  ],
128
130
  });
129
131
  }
130
132
  async function promptPdrSurfacing() {
131
133
  const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
132
- return confirm({ message: 'Enable proactive PDR surfacing in skills? (y/N)', default: false });
134
+ return confirm({ message: 'Enable proactive PDR surfacing in skills? (Y/n)', default: true });
133
135
  }
134
136
  async function promptOkfFrontmatter() {
135
137
  const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
@@ -281,6 +283,37 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
281
283
  console.log(chalk_1.default.dim(' .poolside/ created — Poolside will discover these files automatically.'));
282
284
  }
283
285
  }
286
+ else if (agent === 'vibe') {
287
+ const vibeDir = path.join(projectRoot, '.vibe');
288
+ const dirCreated = !fs.existsSync(vibeDir);
289
+ const skillsBase = path.join(vibeDir, 'skills');
290
+ for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
291
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
292
+ if (!schema.vibe.skill || !schema.skillName)
293
+ continue;
294
+ (0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
295
+ console.log(chalk_1.default.green('✓') + ` .vibe/skills/${schema.skillName}/SKILL.md`);
296
+ }
297
+ const vibeSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
298
+ if (framework === 'native') {
299
+ (0, scaffold_1.writeFile)(vibeSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
300
+ console.log(chalk_1.default.green('✓') + ' .vibe/skills/oprim-spec/SKILL.md');
301
+ }
302
+ else if (fs.existsSync(vibeSpecSkillPath)) {
303
+ fs.unlinkSync(vibeSpecSkillPath);
304
+ try {
305
+ fs.rmdirSync(path.dirname(vibeSpecSkillPath));
306
+ }
307
+ catch { /* not empty or already gone */ }
308
+ console.log(chalk_1.default.dim(' removed .vibe/skills/oprim-spec/SKILL.md'));
309
+ }
310
+ const agentsFile = path.join(projectRoot, 'AGENTS.md');
311
+ writeAgentInstructionFile(agentsFile, vibeInstructions());
312
+ console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
313
+ if (dirCreated) {
314
+ console.log(chalk_1.default.dim(' .vibe/ created — Mistral Vibe will discover these files automatically.'));
315
+ }
316
+ }
284
317
  else if (agent === 'codex') {
285
318
  const agentsFile = path.join(projectRoot, 'AGENTS.md');
286
319
  writeAgentInstructionFile(agentsFile, codexInstructions());
@@ -441,6 +474,10 @@ exports.POOLSIDE_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id
441
474
  const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
442
475
  return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
443
476
  }));
477
+ exports.VIBE_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
478
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
479
+ return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
480
+ }));
444
481
  exports.CURSOR_SKILLS = Object.fromEntries(CURSOR_SKILL_WORKFLOW_IDS.map((id) => {
445
482
  const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
446
483
  return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
@@ -632,3 +669,6 @@ function geminiInstructions() {
632
669
  function poolsideInstructions() {
633
670
  return (0, workflow_renderer_1.renderAgentInstructions)();
634
671
  }
672
+ function vibeInstructions() {
673
+ return (0, workflow_renderer_1.renderAgentInstructions)();
674
+ }
@@ -9,4 +9,5 @@ export declare const noteTemplate = "# Note: <Note title>\n\n<Capture the observ
9
9
  export declare const criteriaTemplate = "metrics:\n - id: metric_id\n name: \"Metric name\"\n baseline: 0.00\n target: 0.00\n timeframe: \"30 days post-launch\"\n launch_date: \"YYYY-MM-DD\"\n source:\n type: amplitude\n definition:\n event: event_name\n aggregation: unique_users\n denominator_event: null\n segment: null\n";
10
10
  export declare const discoveryTemplate = "# Discovery: BET-XXX <Bet title>\n\n## Problem Framing\n- **Problem statement**: <What problem are we solving and for whom?>\n- **Evidence this is real**: <Data, support tickets, user quotes, etc.>\n- **Why it matters**: <Business or user impact if left unsolved>\n\n## User Research Signals\n- **Research conducted**: <Interviews, surveys, usability tests, etc.>\n- **Key findings**: <What did we learn?>\n- **Assumptions to validate**: <What are we still unsure about?>\n\n## Competitive Context\n- **How others solve this**: <Competitor or adjacent solutions>\n- **Our differentiation**: <Why our approach is better or different>\n- **Gaps / opportunities**: <What's underserved in the market?>\n\n## Open Questions\n- [ ] <Question 1 \u2014 what needs to be answered before committing?>\n- [ ] <Question 2>\n- [ ] <Question 3>\n";
11
11
  export declare const sequenceViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');\nconst MAX_LABEL_LEN = 40;\n\nfunction parseSequenceYaml(text) {\n const BUCKETS = ['now', 'next', 'later', 'backlog'];\n const result = Object.fromEntries(BUCKETS.map(b => [b, []]));\n let section = null, item = null, listField = null;\n\n for (const raw of text.split('\\n')) {\n const trimmed = raw.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const indent = raw.length - raw.trimStart().length;\n\n if (indent === 0) {\n const m = trimmed.match(/^(\\w+):/);\n if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }\n continue;\n }\n\n if (!section) continue;\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n item = {}; result[section].push(item); listField = null;\n const rest = trimmed.slice(2);\n const m = rest.match(/^(\\w+):\\s*(.*)/);\n if (m) item[m[1]] = m[2].replace(/^['\"]|['\"]$/g, '').trim();\n continue;\n }\n\n if (indent === 4 && item) {\n const m = trimmed.match(/^(\\w+):\\s*(.*)/);\n if (!m) continue;\n const [, key, val] = m;\n const v = val.trim();\n if (v === '[]') { item[key] = []; listField = null; }\n else if (v === '') { item[key] = []; listField = key; }\n else { item[key] = v.replace(/^['\"]|['\"]$/g, ''); listField = null; }\n continue;\n }\n\n if (indent === 6 && item && listField && trimmed.startsWith('- ')) {\n item[listField].push(trimmed.slice(2).trim());\n }\n }\n return result;\n}\n\nfunction nodeId(betId) { return betId.replace(/-/g, ''); }\n\nfunction truncate(title) {\n return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;\n}\n\nfunction generateMermaidBlock(seq) {\n const lines = ['```mermaid', 'graph TD'];\n const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];\n\n for (const [key, label] of ACTIVE) {\n const bets = seq[key] || [];\n if (!bets.length) continue;\n lines.push(' subgraph ' + label);\n for (const bet of bets) {\n lines.push(' ' + nodeId(bet.id) + '[\"' + bet.id + ': ' + truncate(bet.title) + '\"]');\n }\n lines.push(' end');\n }\n\n for (const [key] of ACTIVE) {\n for (const bet of (seq[key] || [])) {\n for (const blocker of (bet.blocked_by || [])) {\n lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));\n }\n }\n }\n\n lines.push('```');\n return lines.join('\\n');\n}\n\nfunction generateBacklogSection(seq) {\n const backlog = seq.backlog || [];\n if (!backlog.length) return '';\n return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');\n}\n\nfunction main() {\n const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));\n const header = [\n '<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',\n '',\n '# Sequencing Board',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');\n console.log('Written: oprim/sequence-view.md');\n}\n\nmain();\n";
12
+ export declare const decisionsViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst DECISIONS_DIR = path.join(process.cwd(), 'oprim/decisions');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/decisions-view.md');\n\nfunction parsePdrFile(text) {\n const idMatch = text.match(/^#\\s*(PDR-\\d+)[:\\s]*(.*)$/m);\n const id = idMatch ? idMatch[1] : null;\n const title = idMatch ? idMatch[2].trim() : '';\n\n const statusMatch = text.match(/^##\\s*Status\\s*\\n(.+)$/m) || text.match(/^Status:\\s*(.+)$/m);\n const statusLine = statusMatch ? statusMatch[1].trim() : '';\n const supersededMatch = statusLine.match(/Superseded by (PDR-\\d+)/);\n\n return {\n id,\n title,\n status: statusLine,\n supersededBy: supersededMatch ? supersededMatch[1] : null,\n };\n}\n\nfunction readPdrs() {\n if (!fs.existsSync(DECISIONS_DIR)) return [];\n const files = fs.readdirSync(DECISIONS_DIR).filter(f => /^PDR-\\d+.*\\.md$/.test(f));\n return files\n .map(f => ({ file: f, ...parsePdrFile(fs.readFileSync(path.join(DECISIONS_DIR, f), 'utf8')) }))\n .filter(pdr => pdr.id);\n}\n\nfunction generateBody(pdrs) {\n if (pdrs.length === 0) {\n return 'No decisions yet. Run `/oprim:pdr` to record your first product decision.\\n';\n }\n\n const byId = Object.fromEntries(pdrs.map(p => [p.id, p]));\n const supersededOf = {};\n for (const pdr of pdrs) {\n if (pdr.supersededBy && byId[pdr.supersededBy]) {\n (supersededOf[pdr.supersededBy] = supersededOf[pdr.supersededBy] || []).push(pdr);\n }\n }\n\n const current = pdrs.filter(p => !p.supersededBy);\n if (current.length === 0) {\n return 'No current decisions \u2014 all recorded PDRs have been superseded.\\n';\n }\n\n const lines = ['## Current decisions', ''];\n for (const pdr of current) {\n lines.push('- **' + pdr.id + '**: ' + pdr.title);\n for (const old of (supersededOf[pdr.id] || [])) {\n lines.push(' - supersedes [' + old.id + '](decisions/' + old.file + '): ' + old.title);\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction main() {\n const pdrs = readPdrs();\n const header = [\n '<!-- Auto-generated from oprim/decisions/. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-decisions-view.js -->',\n '',\n '# Current Decisions',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateBody(pdrs));\n console.log('Written: oprim/decisions-view.md');\n}\n\nmain();\n";
12
13
  export declare const kpiReviewTemplate = "# KPI Review: BET-XXX\n\n**Review date:** YYYY-MM-DD\n**Reviewed by:** <name>\n\n| Metric | Baseline | Target | Actual | Status |\n|--------|----------|--------|--------|--------|\n| <metric name> | - | - | - | pending |\n\n## Decision quality\n<Did outcomes validate the decision? What would you do differently?>\n\n## Actions\n- [ ] Update bet-decision outcome section\n- [ ] Update affected PDRs\n- [ ] Re-sequence impacted bets\n";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
3
+ exports.kpiReviewTemplate = exports.decisionsViewScriptTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
4
4
  exports.configTemplate = configTemplate;
5
5
  exports.okfFrontmatter = okfFrontmatter;
6
6
  exports.noteMinimalFrontmatter = noteMinimalFrontmatter;
@@ -283,6 +283,85 @@ function main() {
283
283
  console.log('Written: oprim/sequence-view.md');
284
284
  }
285
285
 
286
+ main();
287
+ `;
288
+ exports.decisionsViewScriptTemplate = `#!/usr/bin/env node
289
+ 'use strict';
290
+
291
+ const fs = require('fs');
292
+ const path = require('path');
293
+
294
+ const DECISIONS_DIR = path.join(process.cwd(), 'oprim/decisions');
295
+ const OUTPUT_PATH = path.join(process.cwd(), 'oprim/decisions-view.md');
296
+
297
+ function parsePdrFile(text) {
298
+ const idMatch = text.match(/^#\\s*(PDR-\\d+)[:\\s]*(.*)$/m);
299
+ const id = idMatch ? idMatch[1] : null;
300
+ const title = idMatch ? idMatch[2].trim() : '';
301
+
302
+ const statusMatch = text.match(/^##\\s*Status\\s*\\n(.+)$/m) || text.match(/^Status:\\s*(.+)$/m);
303
+ const statusLine = statusMatch ? statusMatch[1].trim() : '';
304
+ const supersededMatch = statusLine.match(/Superseded by (PDR-\\d+)/);
305
+
306
+ return {
307
+ id,
308
+ title,
309
+ status: statusLine,
310
+ supersededBy: supersededMatch ? supersededMatch[1] : null,
311
+ };
312
+ }
313
+
314
+ function readPdrs() {
315
+ if (!fs.existsSync(DECISIONS_DIR)) return [];
316
+ const files = fs.readdirSync(DECISIONS_DIR).filter(f => /^PDR-\\d+.*\\.md$/.test(f));
317
+ return files
318
+ .map(f => ({ file: f, ...parsePdrFile(fs.readFileSync(path.join(DECISIONS_DIR, f), 'utf8')) }))
319
+ .filter(pdr => pdr.id);
320
+ }
321
+
322
+ function generateBody(pdrs) {
323
+ if (pdrs.length === 0) {
324
+ return 'No decisions yet. Run \`/oprim:pdr\` to record your first product decision.\\n';
325
+ }
326
+
327
+ const byId = Object.fromEntries(pdrs.map(p => [p.id, p]));
328
+ const supersededOf = {};
329
+ for (const pdr of pdrs) {
330
+ if (pdr.supersededBy && byId[pdr.supersededBy]) {
331
+ (supersededOf[pdr.supersededBy] = supersededOf[pdr.supersededBy] || []).push(pdr);
332
+ }
333
+ }
334
+
335
+ const current = pdrs.filter(p => !p.supersededBy);
336
+ if (current.length === 0) {
337
+ return 'No current decisions — all recorded PDRs have been superseded.\\n';
338
+ }
339
+
340
+ const lines = ['## Current decisions', ''];
341
+ for (const pdr of current) {
342
+ lines.push('- **' + pdr.id + '**: ' + pdr.title);
343
+ for (const old of (supersededOf[pdr.id] || [])) {
344
+ lines.push(' - supersedes [' + old.id + '](decisions/' + old.file + '): ' + old.title);
345
+ }
346
+ }
347
+
348
+ return lines.join('\\n') + '\\n';
349
+ }
350
+
351
+ function main() {
352
+ const pdrs = readPdrs();
353
+ const header = [
354
+ '<!-- Auto-generated from oprim/decisions/. Do not edit directly. -->',
355
+ '<!-- Regenerate by running: node oprim/scripts/generate-decisions-view.js -->',
356
+ '',
357
+ '# Current Decisions',
358
+ '',
359
+ '',
360
+ ].join('\\n');
361
+ fs.writeFileSync(OUTPUT_PATH, header + generateBody(pdrs));
362
+ console.log('Written: oprim/decisions-view.md');
363
+ }
364
+
286
365
  main();
287
366
  `;
288
367
  exports.kpiReviewTemplate = `# KPI Review: BET-XXX
@@ -13,6 +13,9 @@ export interface WorkflowSchema {
13
13
  poolside: {
14
14
  skill: boolean;
15
15
  };
16
+ vibe: {
17
+ skill: boolean;
18
+ };
16
19
  inline: boolean;
17
20
  variants: string[] | null;
18
21
  }
@@ -68,6 +68,9 @@ function parseSchema(raw, filePath) {
68
68
  const claude = requireField(obj, 'claude', filePath);
69
69
  const cursor = requireField(obj, 'cursor', filePath);
70
70
  const poolside = requireField(obj, 'poolside', filePath);
71
+ // vibe is optional and mirrors poolside by default — see mistral-vibe-agent-support spec.
72
+ // This keeps existing bundled/forked schemas (none declare a `vibe:` key) working unchanged.
73
+ const vibe = obj.vibe ?? poolside;
71
74
  requireField(obj, 'inline', filePath);
72
75
  return {
73
76
  id: obj.id,
@@ -78,6 +81,7 @@ function parseSchema(raw, filePath) {
78
81
  claude: { skill: Boolean(claude.skill), command: claude.command ?? null },
79
82
  cursor: { skill: Boolean(cursor.skill), command: cursor.command ?? null },
80
83
  poolside: { skill: Boolean(poolside.skill) },
84
+ vibe: { skill: Boolean(vibe.skill) },
81
85
  inline: Boolean(obj.inline),
82
86
  variants: obj.variants ?? null,
83
87
  };
@@ -1 +1 @@
1
- Create a new PDR in `oprim/decisions/`. Scan for `PDR-(\d+)-` to assign next ID (zero-padded, default 001). Read `oprim/config.yaml`'s `rules.pdr` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write `oprim/decisions/PDR-NNN-<slug>.md`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.
1
+ Create a new PDR in `oprim/decisions/`. Scan for `PDR-(\d+)-` to assign next ID (zero-padded, default 001). Read `oprim/config.yaml`'s `rules.pdr` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write `oprim/decisions/PDR-NNN-<slug>.md`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created. Then run `node oprim/scripts/generate-decisions-view.js` from the project root to update `oprim/decisions-view.md`.
@@ -7,4 +7,5 @@ Create a new Product Decision Record in `oprim/decisions/`.
7
7
  3. Gather: context, decision, alternatives, consequences, evidence, related bets/specs.
8
8
  4. Ask if superseding an existing PDR.
9
9
  5. Write `oprim/decisions/PDR-NNN-<slug>.md`. If superseding, update old PDR Status.
10
- 6. Report what was created.
10
+ 6. Report what was created.
11
+ 7. Run `node oprim/scripts/generate-decisions-view.js` from the project root to update `oprim/decisions-view.md`.
@@ -65,3 +65,6 @@ Proposed
65
65
  Read the superseded file → replace Status value with `Superseded by PDR-NNN` → write back.
66
66
 
67
67
  ### 7. Report what was created
68
+
69
+ ### 8. Regenerate the decisions rollup view
70
+ Run `node oprim/scripts/generate-decisions-view.js` from the project root to update `oprim/decisions-view.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",