@open-product-primer/cli 0.1.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.
@@ -0,0 +1,81 @@
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.updateCommand = updateCommand;
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
+ const install_agent_1 = require("../lib/install-agent");
45
+ const detect_1 = require("../lib/detect");
46
+ function updateCommand() {
47
+ return new commander_1.Command('update')
48
+ .description('Refresh /oprim:* assistant commands and skills from package templates')
49
+ .action(() => {
50
+ const projectRoot = process.cwd();
51
+ const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
52
+ if (configAgents !== null && configAgents.length > 0) {
53
+ for (const agent of configAgents) {
54
+ (0, install_agent_1.installAgentSkills)(agent, projectRoot);
55
+ }
56
+ console.log(`\nAgent skills updated: ${configAgents.join(', ')}`);
57
+ }
58
+ else if (configAgents !== null && configAgents.length === 0) {
59
+ console.log(chalk_1.default.yellow('No agents configured') + ' — run ' + chalk_1.default.cyan('oprim init') + ' to select agents');
60
+ }
61
+ else {
62
+ // Legacy: fall back to directory detection
63
+ let updated = 0;
64
+ if (fs.existsSync(path.join(projectRoot, '.claude'))) {
65
+ (0, install_agent_1.installAgentSkills)('claude', projectRoot);
66
+ updated++;
67
+ }
68
+ if (fs.existsSync(path.join(projectRoot, '.cursor'))) {
69
+ (0, install_agent_1.installAgentSkills)('cursor', projectRoot);
70
+ updated++;
71
+ }
72
+ if (updated === 0) {
73
+ console.log(chalk_1.default.yellow('No assistant environments detected (.claude/, .cursor/).'));
74
+ console.log('Run this command from a repository where AI tools are configured.');
75
+ }
76
+ else {
77
+ console.log(`\nAgent skills updated for ${updated} environment(s).`);
78
+ }
79
+ }
80
+ });
81
+ }
@@ -0,0 +1,10 @@
1
+ export declare function detectOpenSpec(projectRoot: string): {
2
+ detected: boolean;
3
+ changesDir: string | null;
4
+ };
5
+ export declare function detectGraphify(projectRoot: string): {
6
+ detected: boolean;
7
+ graphDir: string | null;
8
+ };
9
+ export declare function readAgentsFromConfig(projectRoot: string): string[] | null;
10
+ export declare function writeAgentsToConfig(agents: string[], projectRoot: string): void;
@@ -0,0 +1,72 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.detectOpenSpec = detectOpenSpec;
37
+ exports.detectGraphify = detectGraphify;
38
+ exports.readAgentsFromConfig = readAgentsFromConfig;
39
+ exports.writeAgentsToConfig = writeAgentsToConfig;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const yaml = __importStar(require("js-yaml"));
43
+ function detectOpenSpec(projectRoot) {
44
+ const detected = fs.existsSync(path.join(projectRoot, 'openspec'));
45
+ return { detected, changesDir: detected ? 'openspec/changes' : null };
46
+ }
47
+ function detectGraphify(projectRoot) {
48
+ const detected = fs.existsSync(path.join(projectRoot, 'graphify-out'));
49
+ return { detected, graphDir: detected ? 'graphify-out' : null };
50
+ }
51
+ function readAgentsFromConfig(projectRoot) {
52
+ const configPath = path.join(projectRoot, 'primer', 'config.yaml');
53
+ if (!fs.existsSync(configPath))
54
+ return null;
55
+ const content = fs.readFileSync(configPath, 'utf-8');
56
+ const config = yaml.load(content);
57
+ if (!config || !('agents' in config))
58
+ return null;
59
+ const agents = config['agents'];
60
+ if (!Array.isArray(agents))
61
+ return null;
62
+ return agents;
63
+ }
64
+ function writeAgentsToConfig(agents, projectRoot) {
65
+ const configPath = path.join(projectRoot, 'primer', 'config.yaml');
66
+ if (!fs.existsSync(configPath))
67
+ return;
68
+ const content = fs.readFileSync(configPath, 'utf-8');
69
+ const config = yaml.load(content);
70
+ config['agents'] = agents;
71
+ fs.writeFileSync(configPath, yaml.dump(config, { indent: 2 }), 'utf-8');
72
+ }
@@ -0,0 +1,7 @@
1
+ export type Agent = 'claude' | 'cursor';
2
+ export declare const SUPPORTED_AGENTS: readonly Agent[];
3
+ export declare function installAgentSkills(agent: Agent, projectRoot: string): void;
4
+ export declare const CLAUDE_SKILLS: Record<string, string>;
5
+ export declare const CLAUDE_COMMANDS: Record<string, string>;
6
+ export declare const CURSOR_SKILLS: Record<string, string>;
7
+ export declare const CURSOR_COMMANDS: Record<string, string>;
@@ -0,0 +1,421 @@
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.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.SUPPORTED_AGENTS = void 0;
40
+ exports.installAgentSkills = installAgentSkills;
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const scaffold_1 = require("./scaffold");
45
+ exports.SUPPORTED_AGENTS = ['claude', 'cursor'];
46
+ function installAgentSkills(agent, projectRoot) {
47
+ if (agent === 'claude') {
48
+ const claudeDir = path.join(projectRoot, '.claude');
49
+ const dirCreated = !fs.existsSync(claudeDir);
50
+ const skillsBase = path.join(claudeDir, 'skills');
51
+ for (const [name, content] of Object.entries(exports.CLAUDE_SKILLS)) {
52
+ (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
53
+ console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
54
+ }
55
+ const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
56
+ for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
57
+ (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
58
+ console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
59
+ }
60
+ if (dirCreated) {
61
+ console.log(chalk_1.default.dim(' .claude/ created — Claude Code will discover these files automatically.'));
62
+ }
63
+ }
64
+ else if (agent === 'cursor') {
65
+ const cursorDir = path.join(projectRoot, '.cursor');
66
+ const dirCreated = !fs.existsSync(cursorDir);
67
+ const skillsBase = path.join(cursorDir, 'skills');
68
+ for (const [name, content] of Object.entries(exports.CURSOR_SKILLS)) {
69
+ (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
70
+ console.log(chalk_1.default.green('✓') + ` .cursor/skills/${name}/SKILL.md`);
71
+ }
72
+ const cmdsDir = path.join(cursorDir, 'commands');
73
+ for (const [filename, content] of Object.entries(exports.CURSOR_COMMANDS)) {
74
+ (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
75
+ console.log(chalk_1.default.green('✓') + ` .cursor/commands/${filename}`);
76
+ }
77
+ if (dirCreated) {
78
+ console.log(chalk_1.default.dim(' .cursor/ created — Cursor will discover these files automatically.'));
79
+ }
80
+ }
81
+ }
82
+ // ─── Claude skill playbooks ───────────────────────────────────────────────────
83
+ exports.CLAUDE_SKILLS = {
84
+ 'oprim-pdr': pdrSkill(),
85
+ 'oprim-bet': betSkill(),
86
+ 'oprim-criteria': criteriaSkill(),
87
+ 'oprim-review': reviewSkill(),
88
+ };
89
+ // ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
90
+ exports.CLAUDE_COMMANDS = {
91
+ 'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
92
+ 'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
93
+ '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.'),
94
+ '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.'),
95
+ '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.'),
96
+ '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.'),
97
+ };
98
+ // ─── Cursor skill playbooks ───────────────────────────────────────────────────
99
+ exports.CURSOR_SKILLS = {
100
+ 'oprim-pdr': pdrSkill(),
101
+ 'oprim-bet': betSkill(),
102
+ 'oprim-criteria': criteriaSkill(),
103
+ 'oprim-review': reviewSkill(),
104
+ };
105
+ // ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
106
+ exports.CURSOR_COMMANDS = {
107
+ 'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
108
+ 'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceContent()),
109
+ 'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
110
+ 'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
111
+ 'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
112
+ 'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
113
+ };
114
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
115
+ function claudeWrapper(name, description, body) {
116
+ return `---
117
+ name: "${name}"
118
+ description: ${description}
119
+ category: Workflow
120
+ tags: [workflow, primer]
121
+ ---
122
+
123
+ ${description}.
124
+
125
+ ${body}`;
126
+ }
127
+ function cursorWrapper(id, description, body) {
128
+ return `---
129
+ name: /${id}
130
+ id: ${id}
131
+ category: Workflow
132
+ description: ${description}
133
+ ---
134
+
135
+ ${body}`;
136
+ }
137
+ // ─── Skill content ────────────────────────────────────────────────────────────
138
+ function pdrSkill() {
139
+ return `---
140
+ name: oprim-pdr
141
+ description: Create a new Product Decision Record in primer/decisions/ with auto-assigned ID and guided prompting
142
+ ---
143
+
144
+ Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
145
+
146
+ ## Steps
147
+
148
+ ### 1. Get the decision title
149
+ If not provided, ask: "What is the title of this product decision?"
150
+
151
+ ### 2. Assign the next PDR ID
152
+ Scan \`primer/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
153
+ Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
154
+ Output path: \`primer/decisions/PDR-NNN-<slug>.md\`
155
+
156
+ ### 3. Gather content
157
+ 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).
158
+
159
+ ### 4. Check for supersession
160
+ Ask: "Does this supersede an existing PDR? If so, which ID? (Enter to skip)"
161
+
162
+ ### 5. Write the PDR file
163
+ \`\`\`
164
+ # PDR-NNN: <title>
165
+
166
+ ## Status
167
+ Proposed
168
+
169
+ ## Context
170
+ <context>
171
+
172
+ ## Decision
173
+ <decision>
174
+
175
+ ## Alternatives considered
176
+ <alternatives as bullet list>
177
+
178
+ ## Consequences
179
+ - Positive: <...>
180
+ - Trade-offs: <...>
181
+ - Follow-ups: <...>
182
+
183
+ ## Evidence
184
+ <evidence or "None">
185
+
186
+ ## Related
187
+ - Bets: <BET-IDs or "None">
188
+ - OpenSpec: <change paths or "None">
189
+ - Supersedes: <PDR-ID or "None">
190
+ \`\`\`
191
+
192
+ ### 6. Update superseded PDR (if applicable)
193
+ Read the superseded file → replace Status value with \`Superseded by PDR-NNN\` → write back.
194
+
195
+ ### 7. Report what was created
196
+ `;
197
+ }
198
+ function betSkill() {
199
+ return `---
200
+ name: oprim-bet
201
+ description: Create a new bet directory and bet-decision artifact in primer/bets/, and add the bet to primer/sequence.yaml backlog
202
+ ---
203
+
204
+ Create a new bet in \`primer/bets/\` and register it on the sequencing board.
205
+
206
+ ## Steps
207
+
208
+ ### 1. Get the bet title
209
+ If not provided, ask: "What is the title of this bet?"
210
+
211
+ ### 2. Assign the next BET ID
212
+ Scan \`primer/bets/\` for directories matching \`BET-(\\d+)$\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
213
+
214
+ ### 3. Check sequence.yaml exists
215
+ If \`primer/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
216
+
217
+ ### 4. Gather content
218
+ 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).
219
+
220
+ ### 5. Write primer/bets/BET-NNN/bet-decision.md
221
+ \`\`\`
222
+ # Decision: BET-NNN <title>
223
+
224
+ ## Status
225
+ - Decision: <decision>
226
+ - Date: <today YYYY-MM-DD>
227
+ - Owner: <owner>
228
+ - Review date: <review date>
229
+
230
+ ## Why now
231
+ <why-now as bullet list>
232
+
233
+ ## Alternatives considered
234
+ <alternatives as bullet list>
235
+
236
+ ## Expected outcomes
237
+ <outcomes as bullet list>
238
+
239
+ ## Kill criteria / rollback trigger
240
+ <kill criteria as bullet list>
241
+
242
+ ## Links
243
+ - PDRs: <PDR-IDs or "None">
244
+ - OpenSpec change: <to be filled when promoted>
245
+ \`\`\`
246
+
247
+ ### 6. Append to primer/sequence.yaml backlog
248
+ Read → parse YAML → append → write back (2-space indentation):
249
+ \`\`\`yaml
250
+ - id: BET-NNN
251
+ title: "<title>"
252
+ blocked_by: []
253
+ unlocks: []
254
+ requires_pdrs: []
255
+ \`\`\`
256
+
257
+ ### 7. Report what was created
258
+ `;
259
+ }
260
+ function criteriaSkill() {
261
+ return `---
262
+ name: oprim-criteria
263
+ description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
264
+ ---
265
+
266
+ Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
267
+
268
+ ## Steps
269
+
270
+ ### 1. Identify the bet
271
+ If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
272
+
273
+ ### 2. Verify bet exists
274
+ If \`primer/bets/BET-NNN/\` not found: report and stop — advise \`/oprim:bet\` first.
275
+
276
+ ### 3. Gather metric details
277
+ Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
278
+
279
+ ### 4. Gather source mapping
280
+ Ask: source type (amplitude / bigquery)
281
+
282
+ If amplitude: event name, aggregation (unique_users / event_count / property_sum), denominator event (optional).
283
+ \`\`\`yaml
284
+ source:
285
+ type: amplitude
286
+ definition:
287
+ event: <event_name>
288
+ aggregation: <aggregation>
289
+ denominator_event: <event_name | null>
290
+ \`\`\`
291
+
292
+ If bigquery: table, metric column, SQL filter, aggregation (sum / count / count_distinct / avg), denominator query (optional).
293
+ \`\`\`yaml
294
+ source:
295
+ type: bigquery
296
+ definition:
297
+ table: "<project.dataset.table>"
298
+ metric_column: "<column>"
299
+ filter: "<sql_filter>"
300
+ aggregation: <aggregation>
301
+ denominator_query: <sql | null>
302
+ \`\`\`
303
+
304
+ ### 5. Build metric entry and write
305
+ If file exists: read → parse → append to \`metrics\` → write back (never overwrite).
306
+ If not: create with \`metrics:\` list.
307
+
308
+ ### 6. Ask if more metrics needed. If yes, return to step 3.
309
+
310
+ ### 7. Report what was created
311
+ `;
312
+ }
313
+ function reviewSkill() {
314
+ return `---
315
+ name: oprim-review
316
+ description: Create a KPI review artifact for a completed bet, pre-filled from criteria.yaml with actuals gathered from the user
317
+ ---
318
+
319
+ Create a KPI review in \`primer/reviews/\`.
320
+
321
+ ## Steps
322
+
323
+ ### 1. Identify the bet
324
+ If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
325
+
326
+ ### 2. Load criteria and check for a run result
327
+
328
+ Read \`primer/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
329
+ If not found: inform user and continue with empty metrics list.
330
+
331
+ **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.
332
+
333
+ **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.
334
+
335
+ **If no run result exists:** proceed to step 3 to gather actuals manually.
336
+
337
+ ### 3. Gather actuals per metric (only when no run result)
338
+ For each metric show name/baseline/target and ask: "What was the actual result? (number or 'pending')"
339
+
340
+ Status logic:
341
+ - actual >= target → \`hit\`
342
+ - actual < target → \`missed\`
343
+ - 'pending' or not provided → \`pending\`
344
+
345
+ ### 4. Get review metadata
346
+ Ask: reviewer name, decision quality notes.
347
+
348
+ ### 5. Output path
349
+ \`primer/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
350
+
351
+ ### 6. Write the review file
352
+ \`\`\`markdown
353
+ # KPI Review: BET-NNN
354
+
355
+ **Review date:** YYYY-MM-DD
356
+ **Reviewed by:** <reviewer>
357
+ **Actuals from run:** YYYY-MM-DD ← include only when a run result was ingested
358
+
359
+ | Metric | Baseline | Target | Actual | Status |
360
+ |--------|----------|--------|--------|--------|
361
+ | <name> | <baseline> | <target> | <actual> | <status> |
362
+
363
+ ## Decision quality
364
+ <notes>
365
+
366
+ ## Actions
367
+ - [ ] Update bet-decision outcome section
368
+ - [ ] Update affected PDRs
369
+ - [ ] Re-sequence impacted bets
370
+ \`\`\`
371
+
372
+ ### 7. Report what was created
373
+ `;
374
+ }
375
+ // ─── Cursor inline content (condensed versions for command files) ─────────────
376
+ function pdrInlineContent() {
377
+ 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.`;
378
+ }
379
+ function betInlineContent() {
380
+ 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.`;
381
+ }
382
+ function criteriaInlineContent() {
383
+ 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.`;
384
+ }
385
+ function reviewInlineContent() {
386
+ 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.`;
387
+ }
388
+ // ─── Legacy content (promote / sequence remain inline) ───────────────────────
389
+ function promoteContent() {
390
+ return `
391
+ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
392
+
393
+ **Input**: Specify a bet ID (e.g., \`/oprim:promote BET-042\`) or omit to be prompted.
394
+
395
+ **Steps**
396
+
397
+ 1. **Locate the bet** — read \`primer/bets/BET-XXX/bet-decision.md\`
398
+ 2. **Validate status** — decision must be "Build now"
399
+ 3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
400
+ 4. **Create OpenSpec change** — run \`openspec propose <change-name>\` or create change directory
401
+ 5. **Link artifacts**:
402
+ - Add OpenSpec change path to bet-decision \`## Links\` section
403
+ - Add bet ID to OpenSpec proposal context
404
+ 6. **Copy criteria** — if \`primer/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
405
+ 7. **Report** — show what was linked and what remains for engineering
406
+ `;
407
+ }
408
+ function sequenceContent() {
409
+ return `
410
+ Validate the primer sequencing board and suggest rebalancing if needed.
411
+
412
+ **Steps**
413
+
414
+ 1. **Read board** — load \`primer/sequence.yaml\`
415
+ 2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
416
+ 3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
417
+ 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`primer/decisions/\`
418
+ 5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
419
+ 6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
420
+ `;
421
+ }
@@ -0,0 +1,55 @@
1
+ export interface AmplitudeDefinition {
2
+ event: string;
3
+ aggregation: string;
4
+ denominator_event?: string | null;
5
+ }
6
+ export interface BigQueryDefinition {
7
+ table: string;
8
+ metric_column: string;
9
+ filter: string;
10
+ aggregation: string;
11
+ denominator_query?: string | null;
12
+ }
13
+ export interface CriteriaMetric {
14
+ id: string;
15
+ name: string;
16
+ baseline: number;
17
+ target: number;
18
+ timeframe: string;
19
+ launch_date: string | null;
20
+ source: {
21
+ type: 'amplitude' | 'bigquery';
22
+ definition: AmplitudeDefinition | BigQueryDefinition;
23
+ };
24
+ segment?: string | null;
25
+ }
26
+ export interface CriteriaFile {
27
+ metrics: CriteriaMetric[];
28
+ }
29
+ export interface DateWindow {
30
+ start: string;
31
+ end: string;
32
+ }
33
+ export interface MetricResult {
34
+ id: string;
35
+ name: string;
36
+ source: string;
37
+ actual: number | null;
38
+ target: number;
39
+ status: 'hit' | 'missed' | 'pending';
40
+ notes: string | null;
41
+ }
42
+ export declare function computeDateWindow(launchDate: string | null, timeframe: string): DateWindow | null;
43
+ export declare function generateAmplitudeDefinition(metric: CriteriaMetric): Record<string, unknown>;
44
+ export declare function generateBigQuerySQL(metric: CriteriaMetric): string;
45
+ export declare function classifyStatus(actual: number | null, target: number): 'hit' | 'missed' | 'pending';
46
+ export declare function writeRunResult(betId: string, measurementsDir: string, results: MetricResult[], runDate: string): void;
47
+ export declare function runAmplitudeMetric(definitionPath: string, apiKey: string): Promise<{
48
+ actual: number | null;
49
+ notes: string | null;
50
+ }>;
51
+ export declare function runBigQueryMetric(sqlPath: string): Promise<{
52
+ actual: number | null;
53
+ notes: string | null;
54
+ }>;
55
+ export declare function scanCriteriaForSourceType(projectRoot: string, sourceType: string): boolean;