@open-product-primer/cli 0.1.1 → 0.2.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,15 +8,17 @@ 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();
14
15
  program
15
- .name('open-product-primer')
16
- .description('Open Product Primer — product decisions, sequencing, and KPI tracking')
16
+ .name('oprim')
17
+ .description('oprim — product decisions, sequencing, and KPI tracking')
17
18
  .version(package_json_1.default.version);
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();
@@ -49,12 +49,22 @@ const AGENT_DIRS = {
49
49
  };
50
50
  function doctorCommand() {
51
51
  return new commander_1.Command('doctor')
52
- .description('Check Open Product Primer install health and integration readiness')
52
+ .description('Check oprim install health and integration readiness')
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) {
@@ -154,7 +184,7 @@ function doctorCommand() {
154
184
  required: false,
155
185
  });
156
186
  }
157
- console.log(chalk_1.default.bold('Open Product Primer') + ' — health check\n');
187
+ console.log(chalk_1.default.bold('oprim') + ' — health check\n');
158
188
  for (const check of checks) {
159
189
  const icon = check.pass ? chalk_1.default.green('✓') : check.required ? chalk_1.default.red('✗') : chalk_1.default.yellow('○');
160
190
  const label = check.pass ? chalk_1.default.white(check.name) : chalk_1.default.gray(check.name);
@@ -46,20 +46,20 @@ const install_agent_1 = require("../lib/install-agent");
46
46
  const templates_1 = require("../lib/templates");
47
47
  function initCommand() {
48
48
  return new commander_1.Command('init')
49
- .description('Initialize Open Product Primer in the current repository')
49
+ .description('Initialize oprim in the current repository')
50
50
  .option('--name <name>', 'project name (defaults to directory name)')
51
51
  .option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor)', (val, prev) => [...prev, val], [])
52
52
  .action(async (opts) => {
53
53
  const projectRoot = process.cwd();
54
54
  const projectName = opts.name ?? path.basename(projectRoot);
55
- console.log(chalk_1.default.bold('Open Product Primer') + ' — initializing project workspace...\n');
55
+ console.log(chalk_1.default.bold('oprim') + ' — initializing project workspace...\n');
56
56
  const openspec = (0, detect_1.detectOpenSpec)(projectRoot);
57
57
  const graphify = (0, detect_1.detectGraphify)(projectRoot);
58
58
  if (openspec.detected)
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');
@@ -155,10 +155,10 @@ ${body}`;
155
155
  function pdrSkill() {
156
156
  return `---
157
157
  name: oprim-pdr
158
- description: Create a new Product Decision Record in primer/decisions/ with auto-assigned ID and guided prompting
158
+ description: Create a new Product Decision Record in oprim/decisions/ with auto-assigned ID and guided prompting
159
159
  ---
160
160
 
161
- Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
161
+ Create a new Product Decision Record (PDR) in \`oprim/decisions/\`.
162
162
 
163
163
  ## Steps
164
164
 
@@ -166,9 +166,9 @@ Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
166
166
  If not provided, ask: "What is the title of this product decision?"
167
167
 
168
168
  ### 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.
169
+ 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
170
  Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
171
- Output path: \`primer/decisions/PDR-NNN-<slug>.md\`
171
+ Output path: \`oprim/decisions/PDR-NNN-<slug>.md\`
172
172
 
173
173
  ### 3. Gather content
174
174
  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 +215,10 @@ Read the superseded file → replace Status value with \`Superseded by PDR-NNN\`
215
215
  function betSkill() {
216
216
  return `---
217
217
  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
218
+ description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
219
219
  ---
220
220
 
221
- Create a new bet in \`primer/bets/\` and register it on the sequencing board.
221
+ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
222
222
 
223
223
  ## Steps
224
224
 
@@ -226,15 +226,15 @@ Create a new bet in \`primer/bets/\` and register it on the sequencing board.
226
226
  If not provided, ask: "What is the title of this bet?"
227
227
 
228
228
  ### 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.
229
+ Scan \`oprim/bets/\` for directories matching \`BET-(\\d+)$\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
230
230
 
231
231
  ### 3. Check sequence.yaml exists
232
- If \`primer/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
232
+ If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
233
233
 
234
234
  ### 4. Gather content
235
235
  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
236
 
237
- ### 5. Write primer/bets/BET-NNN/bet-decision.md
237
+ ### 5. Write oprim/bets/BET-NNN/bet-decision.md
238
238
  \`\`\`
239
239
  # Decision: BET-NNN <title>
240
240
 
@@ -261,7 +261,7 @@ Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date
261
261
  - OpenSpec change: <to be filled when promoted>
262
262
  \`\`\`
263
263
 
264
- ### 6. Append to primer/sequence.yaml backlog
264
+ ### 6. Append to oprim/sequence.yaml backlog
265
265
  Read → parse YAML → append → write back (2-space indentation):
266
266
  \`\`\`yaml
267
267
  - id: BET-NNN
@@ -271,7 +271,12 @@ Read → parse YAML → append → write back (2-space indentation):
271
271
  requires_pdrs: []
272
272
  \`\`\`
273
273
 
274
- ### 7. Report what was created
274
+ ### 7. Prompt for optional discovery scaffolding
275
+ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
276
+ - If "y": write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (same structure as \`oprim/templates/discovery.md\`).
277
+ - If "n" or Enter: skip silently.
278
+
279
+ ### 8. Report what was created
275
280
  `;
276
281
  }
277
282
  function criteriaSkill() {
@@ -280,7 +285,7 @@ name: oprim-criteria
280
285
  description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
281
286
  ---
282
287
 
283
- Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
288
+ Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
284
289
 
285
290
  ## Steps
286
291
 
@@ -288,7 +293,7 @@ Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
288
293
  If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
289
294
 
290
295
  ### 2. Verify bet exists
291
- If \`primer/bets/BET-NNN/\` not found: report and stop — advise \`/oprim:bet\` first.
296
+ If \`oprim/bets/BET-NNN/\` not found: report and stop — advise \`/oprim:bet\` first.
292
297
 
293
298
  ### 3. Gather metric details
294
299
  Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
@@ -333,7 +338,7 @@ name: oprim-review
333
338
  description: Create a KPI review artifact for a completed bet, pre-filled from criteria.yaml with actuals gathered from the user
334
339
  ---
335
340
 
336
- Create a KPI review in \`primer/reviews/\`.
341
+ Create a KPI review in \`oprim/reviews/\`.
337
342
 
338
343
  ## Steps
339
344
 
@@ -342,10 +347,10 @@ If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
342
347
 
343
348
  ### 2. Load criteria and check for a run result
344
349
 
345
- Read \`primer/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
350
+ Read \`oprim/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
346
351
  If not found: inform user and continue with empty metrics list.
347
352
 
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.
353
+ **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
354
 
350
355
  **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
356
 
@@ -363,7 +368,7 @@ Status logic:
363
368
  Ask: reviewer name, decision quality notes.
364
369
 
365
370
  ### 5. Output path
366
- \`primer/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
371
+ \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
367
372
 
368
373
  ### 6. Write the review file
369
374
  \`\`\`markdown
@@ -391,16 +396,16 @@ Ask: reviewer name, decision quality notes.
391
396
  }
392
397
  // ─── Cursor inline content (condensed versions for command files) ─────────────
393
398
  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.`;
399
+ 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
400
  }
396
401
  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.`;
402
+ 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
403
  }
399
404
  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.`;
405
+ 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
406
  }
402
407
  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.`;
408
+ 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
409
  }
405
410
  // ─── Legacy content (promote / sequence remain inline) ───────────────────────
406
411
  function promoteContent() {
@@ -411,15 +416,24 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
411
416
 
412
417
  **Steps**
413
418
 
414
- 1. **Locate the bet** — read \`primer/bets/BET-XXX/bet-decision.md\`
419
+ 1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
415
420
  2. **Validate status** — decision must be "Build now"
416
421
  3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
417
- 4. **Create OpenSpec change** — run \`openspec propose <change-name>\` or create change directory
422
+ 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\`.
423
+ - Pass the bet decision content as context so the proposal reflects the bet's why/outcome
424
+ - **Do not manually create a partial change directory** — the propose skill ensures no artifact is omitted
425
+ - The spec file(s) are mandatory: each capability modified or added must have WHEN/THEN scenarios under \`## ADDED Requirements\` or \`## MODIFIED Requirements\`
418
426
  5. **Link artifacts**:
419
427
  - Add OpenSpec change path to bet-decision \`## Links\` section
420
- - Add bet ID to OpenSpec proposal context
421
- 6. **Copy criteria** — if \`primer/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
422
- 7. **Report** — show what was linked and what remains for engineering
428
+ - Add bet ID to OpenSpec proposal \`## Context\` section
429
+ 6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
430
+ 7. **Verify completeness** — confirm the change directory contains:
431
+ - \`proposal.md\`
432
+ - \`design.md\`
433
+ - \`tasks.md\`
434
+ - \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
435
+ If any artifact is missing, create it before reporting done.
436
+ 8. **Report** — show what was linked and what remains for engineering
423
437
  `;
424
438
  }
425
439
  function sequenceContent() {
@@ -428,10 +442,10 @@ Validate the primer sequencing board and suggest rebalancing if needed.
428
442
 
429
443
  **Steps**
430
444
 
431
- 1. **Read board** — load \`primer/sequence.yaml\`
445
+ 1. **Read board** — load \`oprim/sequence.yaml\`
432
446
  2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
433
447
  3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
434
- 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`primer/decisions/\`
448
+ 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
435
449
  5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
436
450
  6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
437
451
  `;
@@ -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.1",
3
+ "version": "0.2.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",