@open-product-primer/cli 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -0
- package/dist/commands/doctor.js +34 -4
- package/dist/commands/init.js +6 -5
- 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 +40 -29
- 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,6 +8,7 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const init_1 = require("./commands/init");
|
|
9
9
|
const update_1 = require("./commands/update");
|
|
10
10
|
const doctor_1 = require("./commands/doctor");
|
|
11
|
+
const migrate_1 = require("./commands/migrate");
|
|
11
12
|
const measure_1 = require("./commands/measure");
|
|
12
13
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
13
14
|
const program = new commander_1.Command();
|
|
@@ -18,5 +19,6 @@ program
|
|
|
18
19
|
program.addCommand((0, init_1.initCommand)());
|
|
19
20
|
program.addCommand((0, update_1.updateCommand)());
|
|
20
21
|
program.addCommand((0, doctor_1.doctorCommand)());
|
|
22
|
+
program.addCommand((0, migrate_1.migrateCommand)());
|
|
21
23
|
program.addCommand((0, measure_1.measureCommand)());
|
|
22
24
|
program.parse();
|
package/dist/commands/doctor.js
CHANGED
|
@@ -53,8 +53,18 @@ function doctorCommand() {
|
|
|
53
53
|
.action(() => {
|
|
54
54
|
const projectRoot = process.cwd();
|
|
55
55
|
const checks = [];
|
|
56
|
-
const primerDir = path.join(projectRoot, '
|
|
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) {
|
package/dist/commands/init.js
CHANGED
|
@@ -59,7 +59,7 @@ function initCommand() {
|
|
|
59
59
|
console.log(chalk_1.default.green('✓') + ' OpenSpec detected');
|
|
60
60
|
if (graphify.detected)
|
|
61
61
|
console.log(chalk_1.default.green('✓') + ' Graphify detected');
|
|
62
|
-
const primerDir = path.join(projectRoot, '
|
|
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');
|
|
@@ -74,6 +74,16 @@ function installAgentSkills(agent, projectRoot) {
|
|
|
74
74
|
(0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
|
|
75
75
|
console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
|
|
76
76
|
}
|
|
77
|
+
// Tombstone cleanup: remove command wrappers deleted in v0.2.0 (bet/criteria/pdr/review
|
|
78
|
+
// became skills-only). Safe to remove this block once the user base has migrated past v0.2.0.
|
|
79
|
+
const tombstones = ['bet.md', 'criteria.md', 'pdr.md', 'review.md'];
|
|
80
|
+
for (const filename of tombstones) {
|
|
81
|
+
const filepath = path.join(cmdsDir, filename);
|
|
82
|
+
if (fs.existsSync(filepath)) {
|
|
83
|
+
fs.unlinkSync(filepath);
|
|
84
|
+
console.log(chalk_1.default.dim(` removed legacy command .claude/commands/oprim/${filename}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
77
87
|
if (dirCreated) {
|
|
78
88
|
console.log(chalk_1.default.dim(' .claude/ created — Claude Code will discover these files automatically.'));
|
|
79
89
|
}
|
|
@@ -107,10 +117,6 @@ exports.CLAUDE_SKILLS = {
|
|
|
107
117
|
exports.CLAUDE_COMMANDS = {
|
|
108
118
|
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
|
|
109
119
|
'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
|
|
110
|
-
'pdr.md': claudeWrapper('OPRIM: PDR', 'Create a new Product Decision Record with auto-assigned ID', 'Use the Skill tool to invoke the `oprim-pdr` skill.'),
|
|
111
|
-
'bet.md': claudeWrapper('OPRIM: Bet', 'Create a new bet decision and register it on the sequencing board', 'Use the Skill tool to invoke the `oprim-bet` skill.'),
|
|
112
|
-
'criteria.md': claudeWrapper('OPRIM: Criteria', 'Create or append to a criteria.yaml contract for a bet', 'Use the Skill tool to invoke the `oprim-criteria` skill.'),
|
|
113
|
-
'review.md': claudeWrapper('OPRIM: Review', "Create a KPI review artifact pre-filled from a bet's criteria contract", 'Use the Skill tool to invoke the `oprim-review` skill.'),
|
|
114
120
|
};
|
|
115
121
|
// ─── Cursor skill playbooks ───────────────────────────────────────────────────
|
|
116
122
|
exports.CURSOR_SKILLS = {
|
|
@@ -155,10 +161,10 @@ ${body}`;
|
|
|
155
161
|
function pdrSkill() {
|
|
156
162
|
return `---
|
|
157
163
|
name: oprim-pdr
|
|
158
|
-
description: Create a new Product Decision Record in
|
|
164
|
+
description: Create a new Product Decision Record in oprim/decisions/ with auto-assigned ID and guided prompting
|
|
159
165
|
---
|
|
160
166
|
|
|
161
|
-
Create a new Product Decision Record (PDR) in \`
|
|
167
|
+
Create a new Product Decision Record (PDR) in \`oprim/decisions/\`.
|
|
162
168
|
|
|
163
169
|
## Steps
|
|
164
170
|
|
|
@@ -166,9 +172,9 @@ Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
|
|
|
166
172
|
If not provided, ask: "What is the title of this product decision?"
|
|
167
173
|
|
|
168
174
|
### 2. Assign the next PDR ID
|
|
169
|
-
Scan \`
|
|
175
|
+
Scan \`oprim/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
|
|
170
176
|
Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
|
|
171
|
-
Output path: \`
|
|
177
|
+
Output path: \`oprim/decisions/PDR-NNN-<slug>.md\`
|
|
172
178
|
|
|
173
179
|
### 3. Gather content
|
|
174
180
|
Ask: Context (what forced this decision), Decision (clear statement), Alternatives considered (why rejected), Consequences (positives / trade-offs / follow-ups), Evidence links (optional), Related bets (optional), Related OpenSpec changes (optional).
|
|
@@ -215,10 +221,10 @@ Read the superseded file → replace Status value with \`Superseded by PDR-NNN\`
|
|
|
215
221
|
function betSkill() {
|
|
216
222
|
return `---
|
|
217
223
|
name: oprim-bet
|
|
218
|
-
description: Create a new bet directory and bet-decision artifact in
|
|
224
|
+
description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
|
|
219
225
|
---
|
|
220
226
|
|
|
221
|
-
Create a new bet in \`
|
|
227
|
+
Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
|
|
222
228
|
|
|
223
229
|
## Steps
|
|
224
230
|
|
|
@@ -226,15 +232,15 @@ Create a new bet in \`primer/bets/\` and register it on the sequencing board.
|
|
|
226
232
|
If not provided, ask: "What is the title of this bet?"
|
|
227
233
|
|
|
228
234
|
### 2. Assign the next BET ID
|
|
229
|
-
Scan \`
|
|
235
|
+
Scan \`oprim/bets/\` for directories matching \`BET-(\\d+)$\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
|
|
230
236
|
|
|
231
237
|
### 3. Check sequence.yaml exists
|
|
232
|
-
If \`
|
|
238
|
+
If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
|
|
233
239
|
|
|
234
240
|
### 4. Gather content
|
|
235
241
|
Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date (YYYY-MM-DD), Why now, Alternatives considered, Expected outcomes (metric: baseline → target in timeframe), Kill criteria / rollback trigger, PDR links (optional).
|
|
236
242
|
|
|
237
|
-
### 5. Write
|
|
243
|
+
### 5. Write oprim/bets/BET-NNN/bet-decision.md
|
|
238
244
|
\`\`\`
|
|
239
245
|
# Decision: BET-NNN <title>
|
|
240
246
|
|
|
@@ -261,7 +267,7 @@ Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date
|
|
|
261
267
|
- OpenSpec change: <to be filled when promoted>
|
|
262
268
|
\`\`\`
|
|
263
269
|
|
|
264
|
-
### 6. Append to
|
|
270
|
+
### 6. Append to oprim/sequence.yaml backlog
|
|
265
271
|
Read → parse YAML → append → write back (2-space indentation):
|
|
266
272
|
\`\`\`yaml
|
|
267
273
|
- id: BET-NNN
|
|
@@ -271,7 +277,12 @@ Read → parse YAML → append → write back (2-space indentation):
|
|
|
271
277
|
requires_pdrs: []
|
|
272
278
|
\`\`\`
|
|
273
279
|
|
|
274
|
-
### 7.
|
|
280
|
+
### 7. Prompt for optional discovery scaffolding
|
|
281
|
+
Ask: "Do you want to scaffold a discovery.md now? (y/N)"
|
|
282
|
+
- If "y": write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (same structure as \`oprim/templates/discovery.md\`).
|
|
283
|
+
- If "n" or Enter: skip silently.
|
|
284
|
+
|
|
285
|
+
### 8. Report what was created
|
|
275
286
|
`;
|
|
276
287
|
}
|
|
277
288
|
function criteriaSkill() {
|
|
@@ -280,7 +291,7 @@ name: oprim-criteria
|
|
|
280
291
|
description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
|
|
281
292
|
---
|
|
282
293
|
|
|
283
|
-
Create or append to \`
|
|
294
|
+
Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
|
|
284
295
|
|
|
285
296
|
## Steps
|
|
286
297
|
|
|
@@ -288,7 +299,7 @@ Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
|
|
|
288
299
|
If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
|
|
289
300
|
|
|
290
301
|
### 2. Verify bet exists
|
|
291
|
-
If \`
|
|
302
|
+
If \`oprim/bets/BET-NNN/\` not found: report and stop — advise using the \`oprim-bet\` skill first.
|
|
292
303
|
|
|
293
304
|
### 3. Gather metric details
|
|
294
305
|
Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
|
|
@@ -333,7 +344,7 @@ name: oprim-review
|
|
|
333
344
|
description: Create a KPI review artifact for a completed bet, pre-filled from criteria.yaml with actuals gathered from the user
|
|
334
345
|
---
|
|
335
346
|
|
|
336
|
-
Create a KPI review in \`
|
|
347
|
+
Create a KPI review in \`oprim/reviews/\`.
|
|
337
348
|
|
|
338
349
|
## Steps
|
|
339
350
|
|
|
@@ -342,10 +353,10 @@ If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
|
|
|
342
353
|
|
|
343
354
|
### 2. Load criteria and check for a run result
|
|
344
355
|
|
|
345
|
-
Read \`
|
|
356
|
+
Read \`oprim/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
|
|
346
357
|
If not found: inform user and continue with empty metrics list.
|
|
347
358
|
|
|
348
|
-
**Check for measurement run result:** Scan \`
|
|
359
|
+
**Check for measurement run result:** Scan \`oprim/bets/BET-NNN/measurements/\` for files matching \`run-*.yaml\`. If any exist, sort by filename (date-based) and read the most recent.
|
|
349
360
|
|
|
350
361
|
**If a run result exists:** use it to pre-populate actuals and status for every metric. Skip step 3 for those metrics. Note the run date — include "Actuals from run: YYYY-MM-DD" in the review artifact.
|
|
351
362
|
|
|
@@ -363,7 +374,7 @@ Status logic:
|
|
|
363
374
|
Ask: reviewer name, decision quality notes.
|
|
364
375
|
|
|
365
376
|
### 5. Output path
|
|
366
|
-
\`
|
|
377
|
+
\`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
|
|
367
378
|
|
|
368
379
|
### 6. Write the review file
|
|
369
380
|
\`\`\`markdown
|
|
@@ -391,16 +402,16 @@ Ask: reviewer name, decision quality notes.
|
|
|
391
402
|
}
|
|
392
403
|
// ─── Cursor inline content (condensed versions for command files) ─────────────
|
|
393
404
|
function pdrInlineContent() {
|
|
394
|
-
return `Create a new PDR in \`
|
|
405
|
+
return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
|
|
395
406
|
}
|
|
396
407
|
function betInlineContent() {
|
|
397
|
-
return `Create a new bet in \`
|
|
408
|
+
return `Create a new bet in \`oprim/bets/\`. Scan \`BET-(\\d+)$\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). Gather: title, decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\`. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.`;
|
|
398
409
|
}
|
|
399
410
|
function criteriaInlineContent() {
|
|
400
|
-
return `Add metrics to \`
|
|
411
|
+
return `Add metrics to \`oprim/bets/BET-NNN/criteria.yaml\`. Verify bet dir exists. Gather: metric ID, name, baseline, target, timeframe, launch date, segment. Ask source type (amplitude or bigquery). Amplitude: event, aggregation, denominator_event. BigQuery: table, metric_column, filter, aggregation, denominator_query. If file exists: append to metrics list (never overwrite). If not: create. Ask if adding more metrics. Report what was created.`;
|
|
401
412
|
}
|
|
402
413
|
function reviewInlineContent() {
|
|
403
|
-
return `Create KPI review in \`
|
|
414
|
+
return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
|
|
404
415
|
}
|
|
405
416
|
// ─── Legacy content (promote / sequence remain inline) ───────────────────────
|
|
406
417
|
function promoteContent() {
|
|
@@ -411,7 +422,7 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
|
|
|
411
422
|
|
|
412
423
|
**Steps**
|
|
413
424
|
|
|
414
|
-
1. **Locate the bet** — read \`
|
|
425
|
+
1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
415
426
|
2. **Validate status** — decision must be "Build now"
|
|
416
427
|
3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
|
|
417
428
|
4. **Create OpenSpec change** — derive the change name as \`bet-NNN-<slug>\` where \`NNN\` is the zero-padded bet number (e.g. BET-004 → \`bet-004\`) and \`<slug>\` is a short kebab-case summary of the change. Then invoke the \`/openspec-propose\` skill (or \`/opsx:propose\`) with that name to create the change directory with **all required artifacts**: \`proposal.md\`, \`design.md\`, \`tasks.md\`, and \`specs/<capability>/spec.md\` for every capability listed under \`## Capabilities\`.
|
|
@@ -421,7 +432,7 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
|
|
|
421
432
|
5. **Link artifacts**:
|
|
422
433
|
- Add OpenSpec change path to bet-decision \`## Links\` section
|
|
423
434
|
- Add bet ID to OpenSpec proposal \`## Context\` section
|
|
424
|
-
6. **Copy criteria** — if \`
|
|
435
|
+
6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
|
|
425
436
|
7. **Verify completeness** — confirm the change directory contains:
|
|
426
437
|
- \`proposal.md\`
|
|
427
438
|
- \`design.md\`
|
|
@@ -437,10 +448,10 @@ Validate the primer sequencing board and suggest rebalancing if needed.
|
|
|
437
448
|
|
|
438
449
|
**Steps**
|
|
439
450
|
|
|
440
|
-
1. **Read board** — load \`
|
|
451
|
+
1. **Read board** — load \`oprim/sequence.yaml\`
|
|
441
452
|
2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
|
|
442
453
|
3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
|
|
443
|
-
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`
|
|
454
|
+
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
|
|
444
455
|
5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
|
|
445
456
|
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
446
457
|
`;
|
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
|