@open-product-primer/cli 2.3.0 → 2.5.1

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.
@@ -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');
@@ -90,8 +90,8 @@ async function promptFrameworkSelection(projectRoot) {
90
90
  return select({
91
91
  message: 'Which speccing framework does this project use?',
92
92
  choices: [
93
- { name: 'OpenSpec (recommended)', value: 'openspec' },
94
- { name: 'Native (oprim-authored specs, no OpenSpec required)', value: 'native' },
93
+ { name: 'Native (oprim-authored specs, recommended)', value: 'native' },
94
+ { name: 'OpenSpec', value: 'openspec' },
95
95
  { name: 'None', value: 'none' },
96
96
  ],
97
97
  });
@@ -129,7 +129,7 @@ async function promptAgentSelection(projectRoot) {
129
129
  }
130
130
  async function promptPdrSurfacing() {
131
131
  const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
132
- return confirm({ message: 'Enable proactive PDR surfacing in skills? (y/N)', default: false });
132
+ return confirm({ message: 'Enable proactive PDR surfacing in skills? (Y/n)', default: true });
133
133
  }
134
134
  async function promptOkfFrontmatter() {
135
135
  const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
@@ -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
@@ -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`.
@@ -0,0 +1,13 @@
1
+ id: tutorial
2
+ skillName: oprim-tutorial
3
+ title: null
4
+ description: Guided walkthrough of the oprim bet -> spec -> archive cycle inside the todo-app example workspace (examples/) -- creates a real "add due dates to tasks" bet, specs it, and archives it, narrating each step against the example's existing PDRs, bets, and current-truth specs
5
+ claude:
6
+ skill: true
7
+ command: null
8
+ cursor:
9
+ skill: true
10
+ command: null
11
+ poolside:
12
+ skill: true
13
+ inline: false
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: oprim-tutorial
3
+ description: Guided walkthrough of the oprim bet -> spec -> archive cycle inside the todo-app example workspace (examples/) -- creates a real "add due dates to tasks" bet, specs it, and archives it, narrating each step against the example's existing PDRs, bets, and current-truth specs
4
+ ---
5
+
6
+ Walk the user through one full oprim workflow cycle — bet → spec → archive — inside this example workspace, using a real new feature for the mock todo app: **"Add due dates to tasks."** This is not a simulation: it invokes the same `oprim-bet`, `oprim-spec`, and `oprim-archive` skills a real project uses, against this workspace's real files.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question you ask directly. When a step delegates to another skill via the Skill tool, that skill drives its own prompts.
9
+
10
+ ## Before you start
11
+
12
+ Orient the user in 2-3 sentences before Step 1:
13
+ - This workspace (`oprim/`) already has a working example: `PDR-001`/`PDR-002` record two early product decisions, `BET-001` (archived) shipped the core add/complete/delete loop, and `oprim/specs/task-management/spec.md` is that feature's current-truth spec. `BET-002` (pending) is a backlog idea for later.
14
+ - This tutorial adds a new bet on top of that foundation — due dates — and carries it all the way through the cycle: bet decision → spec delta → archive.
15
+
16
+ ## Steps
17
+
18
+ ### 1. Create the bet
19
+ Invoke the **`oprim-bet`** skill using the Skill tool. Suggest (but do not force) the title **"Add due dates to tasks"** if the user doesn't already have one in mind, and note that PDR-001 (flat lists) and PDR-002 (local-only storage) are the relevant prior decisions to link.
20
+
21
+ After the skill finishes, confirm the new bet's ID (it will be the next available `BET-NNN` — `BET-003` if this is the first tutorial run) and its directory under `oprim/bets/pending/`.
22
+
23
+ ### 2. Author the spec delta
24
+ Invoke the **`oprim-spec`** skill using the Skill tool, passing the bet ID from Step 1. When it asks for a capability name, suggest **"task-management"** — the same capability `BET-001` shipped — since due dates extend that existing surface rather than introducing a new one. When it asks whether each requirement is ADDED/MODIFIED/REMOVED, this is an ADDED requirement (due dates are new, nothing existing changes).
25
+
26
+ This also scaffolds `design.md` and `tasks.md` for the bet (first `oprim-spec` invocation for a bet always does). Point the user at both files afterward.
27
+
28
+ ### 3. Note the pre-archive state
29
+ Before archiving, briefly show the user what's about to change:
30
+ - `oprim/bets/pending/BET-NNN-.../` will move to `oprim/bets/archived/`
31
+ - The new ADDED requirement(s) will be appended to the existing `oprim/specs/task-management/spec.md` (current truth), alongside the three requirements `BET-001` already put there
32
+ - The bet's entry will be removed from `oprim/sequence.yaml`
33
+
34
+ This is the same fold `/oprim:archive` performs on any real project — nothing tutorial-specific happens here.
35
+
36
+ ### 4. Archive the bet
37
+ Invoke the **`oprim-archive`** skill using the Skill tool, passing the bet ID from Step 1. Let it run its normal checks (dependents, delta overlaps, incomplete `tasks.md`) — if `tasks.md` still has unchecked items, that's expected for a fresh tutorial run; the user can confirm through the warning or check off items first.
38
+
39
+ ### 5. Show the result
40
+ Report what changed, pointing at real file paths:
41
+ - `oprim/bets/archived/BET-NNN-.../` — the completed bet, in full
42
+ - `oprim/specs/task-management/spec.md` — now includes the due-date requirement(s) alongside `BET-001`'s original three
43
+ - `oprim/sequence.yaml` — no longer lists `BET-NNN`
44
+
45
+ Close by telling the user they just completed a full oprim cycle — the same one this tool uses on itself (see this repository's own `oprim/` workspace) — and that running it again works identically on their own project after `oprim init`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "2.3.0",
3
+ "version": "2.5.1",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",