@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 +4 -2
- package/dist/commands/doctor.js +36 -6
- package/dist/commands/init.js +8 -7
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.js +63 -0
- package/dist/lib/detect.js +2 -2
- package/dist/lib/install-agent.js +42 -28
- package/dist/lib/templates.d.ts +1 -0
- package/dist/lib/templates.js +23 -1
- package/package.json +1 -1
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('
|
|
16
|
-
.description('
|
|
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();
|
package/dist/commands/doctor.js
CHANGED
|
@@ -49,12 +49,22 @@ const AGENT_DIRS = {
|
|
|
49
49
|
};
|
|
50
50
|
function doctorCommand() {
|
|
51
51
|
return new commander_1.Command('doctor')
|
|
52
|
-
.description('Check
|
|
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, '
|
|
57
|
-
|
|
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:
|
|
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:
|
|
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('
|
|
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);
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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('
|
|
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, '
|
|
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('✓') + '
|
|
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('
|
|
80
|
-
console.log(' ' + chalk_1.default.gray('
|
|
81
|
-
console.log(' ' + chalk_1.default.gray('
|
|
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,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
|
+
}
|
package/dist/lib/detect.js
CHANGED
|
@@ -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, '
|
|
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, '
|
|
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
|
|
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 \`
|
|
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 \`
|
|
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: \`
|
|
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
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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
|
|
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
|
|
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.
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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
|
-
\`
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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 \`
|
|
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** —
|
|
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
|
|
421
|
-
6. **Copy criteria** — if \`
|
|
422
|
-
7. **
|
|
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 \`
|
|
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 \`
|
|
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
|
`;
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -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";
|
package/dist/lib/templates.js
CHANGED
|
@@ -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
|