@open-product-primer/cli 0.1.2 → 0.3.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.
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ const commander_1 = require("commander");
8
8
  const init_1 = require("./commands/init");
9
9
  const update_1 = require("./commands/update");
10
10
  const doctor_1 = require("./commands/doctor");
11
+ const migrate_1 = require("./commands/migrate");
11
12
  const measure_1 = require("./commands/measure");
12
13
  const package_json_1 = __importDefault(require("../package.json"));
13
14
  const program = new commander_1.Command();
@@ -18,5 +19,6 @@ program
18
19
  program.addCommand((0, init_1.initCommand)());
19
20
  program.addCommand((0, update_1.updateCommand)());
20
21
  program.addCommand((0, doctor_1.doctorCommand)());
22
+ program.addCommand((0, migrate_1.migrateCommand)());
21
23
  program.addCommand((0, measure_1.measureCommand)());
22
24
  program.parse();
@@ -53,8 +53,18 @@ function doctorCommand() {
53
53
  .action(() => {
54
54
  const projectRoot = process.cwd();
55
55
  const checks = [];
56
- const primerDir = path.join(projectRoot, 'primer');
57
- for (const dir of ['primer', 'primer/decisions', 'primer/bets', 'primer/reviews', 'primer/templates']) {
56
+ const primerDir = path.join(projectRoot, 'oprim');
57
+ const legacyPrimerExists = fs.existsSync(path.join(projectRoot, 'primer'));
58
+ const oprimExists = fs.existsSync(path.join(projectRoot, 'oprim'));
59
+ if (legacyPrimerExists && !oprimExists) {
60
+ checks.push({
61
+ name: 'migration: primer/ detected',
62
+ pass: false,
63
+ note: "Run 'oprim migrate' to rename primer/ to oprim/",
64
+ required: true,
65
+ });
66
+ }
67
+ for (const dir of ['oprim', 'oprim/decisions', 'oprim/bets', 'oprim/reviews', 'oprim/templates']) {
58
68
  const exists = fs.existsSync(path.join(projectRoot, dir));
59
69
  checks.push({
60
70
  name: `scaffold: ${dir}/`,
@@ -66,14 +76,14 @@ function doctorCommand() {
66
76
  const configPath = path.join(primerDir, 'config.yaml');
67
77
  const configExists = fs.existsSync(configPath);
68
78
  checks.push({
69
- name: 'config: primer/config.yaml',
79
+ name: 'config: oprim/config.yaml',
70
80
  pass: configExists,
71
81
  note: configExists ? undefined : "Run 'oprim init' to create",
72
82
  required: true,
73
83
  });
74
84
  const sequenceExists = fs.existsSync(path.join(primerDir, 'sequence.yaml'));
75
85
  checks.push({
76
- name: 'config: primer/sequence.yaml',
86
+ name: 'config: oprim/sequence.yaml',
77
87
  pass: sequenceExists,
78
88
  note: sequenceExists ? undefined : "Run 'oprim init' to create",
79
89
  required: true,
@@ -117,6 +127,26 @@ function doctorCommand() {
117
127
  : 'No bigquery metrics in criteria.yaml — not required',
118
128
  required: false,
119
129
  });
130
+ // ── Discovery checks — warn if bet is missing discovery.md ───────────────
131
+ const betsDir = path.join(primerDir, 'bets');
132
+ if (fs.existsSync(betsDir)) {
133
+ const betEntries = fs.readdirSync(betsDir, { withFileTypes: true });
134
+ for (const entry of betEntries) {
135
+ if (!entry.isDirectory())
136
+ continue;
137
+ const betDir = path.join(betsDir, entry.name);
138
+ const hasDecision = fs.existsSync(path.join(betDir, 'bet-decision.md'));
139
+ if (!hasDecision)
140
+ continue;
141
+ const hasDiscovery = fs.existsSync(path.join(betDir, 'discovery.md'));
142
+ checks.push({
143
+ name: `discovery: ${entry.name}/discovery.md`,
144
+ pass: hasDiscovery,
145
+ note: hasDiscovery ? undefined : 'discovery.md missing — consider adding discovery context',
146
+ required: false,
147
+ });
148
+ }
149
+ }
120
150
  // ── Agent environment checks ──────────────────────────────────────────────
121
151
  const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
122
152
  if (configAgents !== null) {
@@ -59,7 +59,7 @@ function initCommand() {
59
59
  console.log(chalk_1.default.green('✓') + ' OpenSpec detected');
60
60
  if (graphify.detected)
61
61
  console.log(chalk_1.default.green('✓') + ' Graphify detected');
62
- const primerDir = path.join(projectRoot, 'primer');
62
+ const primerDir = path.join(projectRoot, 'oprim');
63
63
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
64
64
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
65
65
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
@@ -70,15 +70,16 @@ function initCommand() {
70
70
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'bet-decision.md'), templates_1.betDecisionTemplate);
71
71
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
72
72
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), templates_1.kpiReviewTemplate);
73
+ (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
73
74
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
74
75
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
75
76
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
76
- console.log('\n' + chalk_1.default.green('✓') + ' primer/ workspace created');
77
+ console.log('\n' + chalk_1.default.green('✓') + ' oprim/ workspace created');
77
78
  const configStatus = configWritten ? 'written' : 'preserved (already exists)';
78
79
  const sequenceStatus = sequenceWritten ? 'written' : 'preserved (already exists)';
79
- console.log(' ' + chalk_1.default.gray('primer/config.yaml') + ' — ' + configStatus);
80
- console.log(' ' + chalk_1.default.gray('primer/sequence.yaml') + ' — ' + sequenceStatus);
81
- console.log(' ' + chalk_1.default.gray('primer/templates/') + ' — refreshed');
80
+ console.log(' ' + chalk_1.default.gray('oprim/config.yaml') + ' — ' + configStatus);
81
+ console.log(' ' + chalk_1.default.gray('oprim/sequence.yaml') + ' — ' + sequenceStatus);
82
+ console.log(' ' + chalk_1.default.gray('oprim/templates/') + ' — refreshed');
82
83
  // ── Agent selection ───────────────────────────────────────────────────────
83
84
  let selectedAgents;
84
85
  const flaggedAgents = opts.agent;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function migrateCommand(): Command;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.migrateCommand = migrateCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ function migrateCommand() {
45
+ return new commander_1.Command('migrate')
46
+ .description('Rename primer/ to oprim/ in the current repository')
47
+ .action(() => {
48
+ const projectRoot = process.cwd();
49
+ const legacyDir = path.join(projectRoot, 'primer');
50
+ const targetDir = path.join(projectRoot, 'oprim');
51
+ if (fs.existsSync(targetDir)) {
52
+ console.log(chalk_1.default.green('✓') + ' oprim/ already exists — nothing to migrate.');
53
+ return;
54
+ }
55
+ if (!fs.existsSync(legacyDir)) {
56
+ console.error(chalk_1.default.red('✗') + ' No primer/ directory found — run ' + chalk_1.default.cyan('oprim init') + ' to create oprim/.');
57
+ process.exit(1);
58
+ }
59
+ fs.renameSync(legacyDir, targetDir);
60
+ console.log(chalk_1.default.green('✓') + ' Renamed ' + chalk_1.default.gray('primer/') + ' → ' + chalk_1.default.gray('oprim/'));
61
+ console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
62
+ });
63
+ }
@@ -50,7 +50,7 @@ function detectGraphify(projectRoot) {
50
50
  return { detected, graphDir: detected ? 'graphify-out' : null };
51
51
  }
52
52
  function readAgentsFromConfig(projectRoot) {
53
- const configPath = path.join(projectRoot, 'primer', 'config.yaml');
53
+ const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
54
54
  if (!fs.existsSync(configPath))
55
55
  return null;
56
56
  const content = fs.readFileSync(configPath, 'utf-8');
@@ -71,7 +71,7 @@ function detectAvailableAgents(projectRoot) {
71
71
  return detected;
72
72
  }
73
73
  function writeAgentsToConfig(agents, projectRoot) {
74
- const configPath = path.join(projectRoot, 'primer', 'config.yaml');
74
+ const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
75
75
  if (!fs.existsSync(configPath))
76
76
  return;
77
77
  const content = fs.readFileSync(configPath, 'utf-8');
@@ -74,6 +74,16 @@ function installAgentSkills(agent, projectRoot) {
74
74
  (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
75
75
  console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
76
76
  }
77
+ // Tombstone cleanup: remove command wrappers deleted in v0.2.0 (bet/criteria/pdr/review
78
+ // became skills-only). Safe to remove this block once the user base has migrated past v0.2.0.
79
+ const tombstones = ['bet.md', 'criteria.md', 'pdr.md', 'review.md'];
80
+ for (const filename of tombstones) {
81
+ const filepath = path.join(cmdsDir, filename);
82
+ if (fs.existsSync(filepath)) {
83
+ fs.unlinkSync(filepath);
84
+ console.log(chalk_1.default.dim(` removed legacy command .claude/commands/oprim/${filename}`));
85
+ }
86
+ }
77
87
  if (dirCreated) {
78
88
  console.log(chalk_1.default.dim(' .claude/ created — Claude Code will discover these files automatically.'));
79
89
  }
@@ -107,10 +117,6 @@ exports.CLAUDE_SKILLS = {
107
117
  exports.CLAUDE_COMMANDS = {
108
118
  'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
109
119
  'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
110
- 'pdr.md': claudeWrapper('OPRIM: PDR', 'Create a new Product Decision Record with auto-assigned ID', 'Use the Skill tool to invoke the `oprim-pdr` skill.'),
111
- 'bet.md': claudeWrapper('OPRIM: Bet', 'Create a new bet decision and register it on the sequencing board', 'Use the Skill tool to invoke the `oprim-bet` skill.'),
112
- 'criteria.md': claudeWrapper('OPRIM: Criteria', 'Create or append to a criteria.yaml contract for a bet', 'Use the Skill tool to invoke the `oprim-criteria` skill.'),
113
- 'review.md': claudeWrapper('OPRIM: Review', "Create a KPI review artifact pre-filled from a bet's criteria contract", 'Use the Skill tool to invoke the `oprim-review` skill.'),
114
120
  };
115
121
  // ─── Cursor skill playbooks ───────────────────────────────────────────────────
116
122
  exports.CURSOR_SKILLS = {
@@ -155,10 +161,10 @@ ${body}`;
155
161
  function pdrSkill() {
156
162
  return `---
157
163
  name: oprim-pdr
158
- description: Create a new Product Decision Record in primer/decisions/ with auto-assigned ID and guided prompting
164
+ description: Create a new Product Decision Record in oprim/decisions/ with auto-assigned ID and guided prompting
159
165
  ---
160
166
 
161
- Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
167
+ Create a new Product Decision Record (PDR) in \`oprim/decisions/\`.
162
168
 
163
169
  ## Steps
164
170
 
@@ -166,9 +172,9 @@ Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
166
172
  If not provided, ask: "What is the title of this product decision?"
167
173
 
168
174
  ### 2. Assign the next PDR ID
169
- Scan \`primer/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
175
+ Scan \`oprim/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
170
176
  Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
171
- Output path: \`primer/decisions/PDR-NNN-<slug>.md\`
177
+ Output path: \`oprim/decisions/PDR-NNN-<slug>.md\`
172
178
 
173
179
  ### 3. Gather content
174
180
  Ask: Context (what forced this decision), Decision (clear statement), Alternatives considered (why rejected), Consequences (positives / trade-offs / follow-ups), Evidence links (optional), Related bets (optional), Related OpenSpec changes (optional).
@@ -215,10 +221,10 @@ Read the superseded file → replace Status value with \`Superseded by PDR-NNN\`
215
221
  function betSkill() {
216
222
  return `---
217
223
  name: oprim-bet
218
- description: Create a new bet directory and bet-decision artifact in primer/bets/, and add the bet to primer/sequence.yaml backlog
224
+ description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
219
225
  ---
220
226
 
221
- Create a new bet in \`primer/bets/\` and register it on the sequencing board.
227
+ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
222
228
 
223
229
  ## Steps
224
230
 
@@ -226,15 +232,15 @@ Create a new bet in \`primer/bets/\` and register it on the sequencing board.
226
232
  If not provided, ask: "What is the title of this bet?"
227
233
 
228
234
  ### 2. Assign the next BET ID
229
- Scan \`primer/bets/\` for directories matching \`BET-(\\d+)$\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
235
+ Scan \`oprim/bets/\` for directories matching \`BET-(\\d+)$\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
230
236
 
231
237
  ### 3. Check sequence.yaml exists
232
- If \`primer/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
238
+ If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
233
239
 
234
240
  ### 4. Gather content
235
241
  Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date (YYYY-MM-DD), Why now, Alternatives considered, Expected outcomes (metric: baseline → target in timeframe), Kill criteria / rollback trigger, PDR links (optional).
236
242
 
237
- ### 5. Write primer/bets/BET-NNN/bet-decision.md
243
+ ### 5. Write oprim/bets/BET-NNN/bet-decision.md
238
244
  \`\`\`
239
245
  # Decision: BET-NNN <title>
240
246
 
@@ -261,7 +267,7 @@ Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date
261
267
  - OpenSpec change: <to be filled when promoted>
262
268
  \`\`\`
263
269
 
264
- ### 6. Append to primer/sequence.yaml backlog
270
+ ### 6. Append to oprim/sequence.yaml backlog
265
271
  Read → parse YAML → append → write back (2-space indentation):
266
272
  \`\`\`yaml
267
273
  - id: BET-NNN
@@ -271,7 +277,12 @@ Read → parse YAML → append → write back (2-space indentation):
271
277
  requires_pdrs: []
272
278
  \`\`\`
273
279
 
274
- ### 7. Report what was created
280
+ ### 7. Prompt for optional discovery scaffolding
281
+ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
282
+ - If "y": write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (same structure as \`oprim/templates/discovery.md\`).
283
+ - If "n" or Enter: skip silently.
284
+
285
+ ### 8. Report what was created
275
286
  `;
276
287
  }
277
288
  function criteriaSkill() {
@@ -280,7 +291,7 @@ name: oprim-criteria
280
291
  description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
281
292
  ---
282
293
 
283
- Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
294
+ Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
284
295
 
285
296
  ## Steps
286
297
 
@@ -288,7 +299,7 @@ Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
288
299
  If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
289
300
 
290
301
  ### 2. Verify bet exists
291
- If \`primer/bets/BET-NNN/\` not found: report and stop — advise \`/oprim:bet\` first.
302
+ If \`oprim/bets/BET-NNN/\` not found: report and stop — advise using the \`oprim-bet\` skill first.
292
303
 
293
304
  ### 3. Gather metric details
294
305
  Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
@@ -333,7 +344,7 @@ name: oprim-review
333
344
  description: Create a KPI review artifact for a completed bet, pre-filled from criteria.yaml with actuals gathered from the user
334
345
  ---
335
346
 
336
- Create a KPI review in \`primer/reviews/\`.
347
+ Create a KPI review in \`oprim/reviews/\`.
337
348
 
338
349
  ## Steps
339
350
 
@@ -342,10 +353,10 @@ If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
342
353
 
343
354
  ### 2. Load criteria and check for a run result
344
355
 
345
- Read \`primer/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
356
+ Read \`oprim/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
346
357
  If not found: inform user and continue with empty metrics list.
347
358
 
348
- **Check for measurement run result:** Scan \`primer/bets/BET-NNN/measurements/\` for files matching \`run-*.yaml\`. If any exist, sort by filename (date-based) and read the most recent.
359
+ **Check for measurement run result:** Scan \`oprim/bets/BET-NNN/measurements/\` for files matching \`run-*.yaml\`. If any exist, sort by filename (date-based) and read the most recent.
349
360
 
350
361
  **If a run result exists:** use it to pre-populate actuals and status for every metric. Skip step 3 for those metrics. Note the run date — include "Actuals from run: YYYY-MM-DD" in the review artifact.
351
362
 
@@ -363,7 +374,7 @@ Status logic:
363
374
  Ask: reviewer name, decision quality notes.
364
375
 
365
376
  ### 5. Output path
366
- \`primer/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
377
+ \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
367
378
 
368
379
  ### 6. Write the review file
369
380
  \`\`\`markdown
@@ -391,16 +402,16 @@ Ask: reviewer name, decision quality notes.
391
402
  }
392
403
  // ─── Cursor inline content (condensed versions for command files) ─────────────
393
404
  function pdrInlineContent() {
394
- return `Create a new PDR in \`primer/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`primer/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
405
+ return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). 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.`;
395
406
  }
396
407
  function betInlineContent() {
397
- return `Create a new bet in \`primer/bets/\`. Scan \`BET-(\\d+)$\` dirs for next ID (zero-padded, default 001). Check \`primer/sequence.yaml\` exists (stop if not — advise oprim init). Gather: title, decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`primer/bets/BET-NNN/bet-decision.md\`. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Report what was created.`;
408
+ return `Create a new bet in \`oprim/bets/\`. Scan \`BET-(\\d+)$\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). Gather: title, decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\`. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.`;
398
409
  }
399
410
  function criteriaInlineContent() {
400
- return `Add metrics to \`primer/bets/BET-NNN/criteria.yaml\`. Verify bet dir exists. Gather: metric ID, name, baseline, target, timeframe, launch date, segment. Ask source type (amplitude or bigquery). Amplitude: event, aggregation, denominator_event. BigQuery: table, metric_column, filter, aggregation, denominator_query. If file exists: append to metrics list (never overwrite). If not: create. Ask if adding more metrics. Report what was created.`;
411
+ return `Add metrics to \`oprim/bets/BET-NNN/criteria.yaml\`. Verify bet dir exists. Gather: metric ID, name, baseline, target, timeframe, launch date, segment. Ask source type (amplitude or bigquery). Amplitude: event, aggregation, denominator_event. BigQuery: table, metric_column, filter, aggregation, denominator_query. If file exists: append to metrics list (never overwrite). If not: create. Ask if adding more metrics. Report what was created.`;
401
412
  }
402
413
  function reviewInlineContent() {
403
- return `Create KPI review in \`primer/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`primer/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
414
+ return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
404
415
  }
405
416
  // ─── Legacy content (promote / sequence remain inline) ───────────────────────
406
417
  function promoteContent() {
@@ -411,7 +422,7 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
411
422
 
412
423
  **Steps**
413
424
 
414
- 1. **Locate the bet** — read \`primer/bets/BET-XXX/bet-decision.md\`
425
+ 1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
415
426
  2. **Validate status** — decision must be "Build now"
416
427
  3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
417
428
  4. **Create OpenSpec change** — derive the change name as \`bet-NNN-<slug>\` where \`NNN\` is the zero-padded bet number (e.g. BET-004 → \`bet-004\`) and \`<slug>\` is a short kebab-case summary of the change. Then invoke the \`/openspec-propose\` skill (or \`/opsx:propose\`) with that name to create the change directory with **all required artifacts**: \`proposal.md\`, \`design.md\`, \`tasks.md\`, and \`specs/<capability>/spec.md\` for every capability listed under \`## Capabilities\`.
@@ -421,7 +432,7 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
421
432
  5. **Link artifacts**:
422
433
  - Add OpenSpec change path to bet-decision \`## Links\` section
423
434
  - Add bet ID to OpenSpec proposal \`## Context\` section
424
- 6. **Copy criteria** — if \`primer/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
435
+ 6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
425
436
  7. **Verify completeness** — confirm the change directory contains:
426
437
  - \`proposal.md\`
427
438
  - \`design.md\`
@@ -437,10 +448,10 @@ Validate the primer sequencing board and suggest rebalancing if needed.
437
448
 
438
449
  **Steps**
439
450
 
440
- 1. **Read board** — load \`primer/sequence.yaml\`
451
+ 1. **Read board** — load \`oprim/sequence.yaml\`
441
452
  2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
442
453
  3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
443
- 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`primer/decisions/\`
454
+ 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
444
455
  5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
445
456
  6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
446
457
  `;
@@ -3,4 +3,5 @@ export declare const sequenceTemplate = "wip_limits:\n now: 2\n\nnow: []\nnext:
3
3
  export declare const pdrTemplate = "# PDR-XXX: <Decision title>\n\n## Status\nProposed | Accepted | Deprecated | Superseded by PDR-YYY\n\n## Context\n<What forced this decision?>\n\n## Decision\n<Clear statement of what is decided>\n\n## Alternatives considered\n- <Alternative A and why rejected>\n- <Alternative B and why rejected>\n\n## Consequences\n- Positive: <...>\n- Trade-offs: <...>\n- Follow-ups: <...>\n\n## Evidence\n- <Research link>\n- <Data link>\n\n## Related\n- Bets: <BET-IDs>\n- OpenSpec: <change paths>\n- Supersedes: <PDR-ID or none>\n";
4
4
  export declare const betDecisionTemplate = "# Decision: BET-XXX <Bet title>\n\n## Status\n- Decision: Build now | Defer | Kill\n- Date: YYYY-MM-DD\n- Owner: <name>\n- Review date: YYYY-MM-DD\n\n## Why now\n- <prioritization rationale>\n\n## Alternatives considered\n- <alternative + reason>\n\n## Expected outcomes\n- <metric: baseline -> target in timeframe>\n\n## Kill criteria / rollback trigger\n- <condition and action>\n\n## Links\n- PDRs: <PDR-IDs>\n- OpenSpec change: <path once promoted>\n";
5
5
  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";
6
+ 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";
6
7
  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.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
3
+ exports.kpiReviewTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
4
4
  exports.configTemplate = configTemplate;
5
5
  function configTemplate(projectName, openspecEnabled, graphifyEnabled) {
6
6
  return `version: 1
@@ -101,6 +101,28 @@ exports.criteriaTemplate = `metrics:
101
101
  denominator_event: null
102
102
  segment: null
103
103
  `;
104
+ exports.discoveryTemplate = `# Discovery: BET-XXX <Bet title>
105
+
106
+ ## Problem Framing
107
+ - **Problem statement**: <What problem are we solving and for whom?>
108
+ - **Evidence this is real**: <Data, support tickets, user quotes, etc.>
109
+ - **Why it matters**: <Business or user impact if left unsolved>
110
+
111
+ ## User Research Signals
112
+ - **Research conducted**: <Interviews, surveys, usability tests, etc.>
113
+ - **Key findings**: <What did we learn?>
114
+ - **Assumptions to validate**: <What are we still unsure about?>
115
+
116
+ ## Competitive Context
117
+ - **How others solve this**: <Competitor or adjacent solutions>
118
+ - **Our differentiation**: <Why our approach is better or different>
119
+ - **Gaps / opportunities**: <What's underserved in the market?>
120
+
121
+ ## Open Questions
122
+ - [ ] <Question 1 — what needs to be answered before committing?>
123
+ - [ ] <Question 2>
124
+ - [ ] <Question 3>
125
+ `;
104
126
  exports.kpiReviewTemplate = `# KPI Review: BET-XXX
105
127
 
106
128
  **Review date:** YYYY-MM-DD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",