@open-product-primer/cli 1.0.0 → 1.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/commands/init.js +25 -4
- package/dist/commands/update.js +4 -0
- package/dist/lib/detect.d.ts +1 -0
- package/dist/lib/detect.js +10 -0
- package/dist/lib/install-agent.d.ts +1 -0
- package/dist/lib/install-agent.js +131 -5
- package/dist/lib/templates.d.ts +5 -1
- package/dist/lib/templates.js +49 -2
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -59,23 +59,43 @@ 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
|
+
console.log('');
|
|
63
|
+
const okfEnabled = await (0, install_agent_1.promptOkfFrontmatter)();
|
|
62
64
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
63
65
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
|
|
64
66
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
|
|
65
67
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
|
|
68
|
+
(0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
|
|
66
69
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
|
|
67
70
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
68
|
-
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected));
|
|
71
|
+
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
|
|
69
72
|
const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
|
|
70
|
-
(0,
|
|
71
|
-
|
|
73
|
+
const pdrContent = okfEnabled ? (0, templates_1.okfFrontmatter)('pdr', '<Decision title>') + templates_1.pdrTemplate : templates_1.pdrTemplate;
|
|
74
|
+
const betContent = okfEnabled
|
|
75
|
+
? (0, templates_1.okfFrontmatter)('bet-decision', '<Bet title>') + templates_1.betDecisionTemplate
|
|
76
|
+
: templates_1.betDecisionTemplate;
|
|
77
|
+
const kpiContent = okfEnabled
|
|
78
|
+
? (0, templates_1.okfFrontmatter)('kpi-review', 'KPI Review: BET-XXX') + templates_1.kpiReviewTemplate
|
|
79
|
+
: templates_1.kpiReviewTemplate;
|
|
80
|
+
// Notes always carry frontmatter — minimal tier by default, OKF tier (adds `description`)
|
|
81
|
+
// when opted in — unlike the other three templates, which have no frontmatter when disabled.
|
|
82
|
+
const noteContent = okfEnabled
|
|
83
|
+
? (0, templates_1.okfFrontmatter)('note', '<Note title>') + templates_1.noteTemplate
|
|
84
|
+
: (0, templates_1.noteMinimalFrontmatter)('<Note title>') + templates_1.noteTemplate;
|
|
85
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), pdrContent);
|
|
86
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'bet-decision.md'), betContent);
|
|
72
87
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
|
|
73
|
-
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'),
|
|
88
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), kpiContent);
|
|
74
89
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
|
|
90
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'note.md'), noteContent);
|
|
75
91
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
92
|
+
if (okfEnabled) {
|
|
93
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'index.md'), (0, templates_1.indexTemplate)(projectName));
|
|
94
|
+
}
|
|
76
95
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
|
|
77
96
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
|
|
78
97
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
|
|
98
|
+
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'notes', '.gitkeep'), '');
|
|
79
99
|
console.log('\n' + chalk_1.default.green('✓') + ' oprim/ workspace created');
|
|
80
100
|
const configStatus = configWritten ? 'written' : 'preserved (already exists)';
|
|
81
101
|
const sequenceStatus = sequenceWritten ? 'written' : 'preserved (already exists)';
|
|
@@ -83,6 +103,7 @@ function initCommand() {
|
|
|
83
103
|
console.log(' ' + chalk_1.default.gray('oprim/sequence.yaml') + ' — ' + sequenceStatus);
|
|
84
104
|
console.log(' ' + chalk_1.default.gray('oprim/templates/') + ' — refreshed');
|
|
85
105
|
console.log(' ' + chalk_1.default.gray('oprim/scripts/') + ' — refreshed');
|
|
106
|
+
console.log(' ' + chalk_1.default.gray('OKF frontmatter') + ' — ' + (okfEnabled ? 'enabled' : 'disabled'));
|
|
86
107
|
// ── Agent selection ───────────────────────────────────────────────────────
|
|
87
108
|
let selectedAgents;
|
|
88
109
|
const flaggedAgents = opts.agent;
|
package/dist/commands/update.js
CHANGED
|
@@ -51,6 +51,10 @@ function updateCommand() {
|
|
|
51
51
|
.action(async () => {
|
|
52
52
|
const projectRoot = process.cwd();
|
|
53
53
|
const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
54
|
+
// okf.enabled is persisted at init time; update reads it (no re-prompt) since
|
|
55
|
+
// already-scaffolded oprim/templates/*.md files are never rewritten here.
|
|
56
|
+
const okfEnabled = (0, detect_1.readOkfEnabledFromConfig)(projectRoot);
|
|
57
|
+
console.log(chalk_1.default.dim(`OKF frontmatter: ${okfEnabled ? 'enabled' : 'disabled'} (persisted from init)`));
|
|
54
58
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
55
59
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
56
60
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
package/dist/lib/detect.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export declare function detectGraphify(projectRoot: string): {
|
|
|
7
7
|
graphDir: string | null;
|
|
8
8
|
};
|
|
9
9
|
export declare function readAgentsFromConfig(projectRoot: string): string[] | null;
|
|
10
|
+
export declare function readOkfEnabledFromConfig(projectRoot: string): boolean;
|
|
10
11
|
export declare function detectAvailableAgents(projectRoot: string): string[];
|
|
11
12
|
export declare function writeAgentsToConfig(agents: string[], projectRoot: string): void;
|
package/dist/lib/detect.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.detectOpenSpec = detectOpenSpec;
|
|
37
37
|
exports.detectGraphify = detectGraphify;
|
|
38
38
|
exports.readAgentsFromConfig = readAgentsFromConfig;
|
|
39
|
+
exports.readOkfEnabledFromConfig = readOkfEnabledFromConfig;
|
|
39
40
|
exports.detectAvailableAgents = detectAvailableAgents;
|
|
40
41
|
exports.writeAgentsToConfig = writeAgentsToConfig;
|
|
41
42
|
const fs = __importStar(require("fs"));
|
|
@@ -62,6 +63,15 @@ function readAgentsFromConfig(projectRoot) {
|
|
|
62
63
|
return null;
|
|
63
64
|
return agents;
|
|
64
65
|
}
|
|
66
|
+
function readOkfEnabledFromConfig(projectRoot) {
|
|
67
|
+
const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
68
|
+
if (!fs.existsSync(configPath))
|
|
69
|
+
return false;
|
|
70
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
71
|
+
const config = yaml.load(content);
|
|
72
|
+
const okf = config?.['okf'];
|
|
73
|
+
return Boolean(okf?.['enabled']);
|
|
74
|
+
}
|
|
65
75
|
function detectAvailableAgents(projectRoot) {
|
|
66
76
|
const detected = [];
|
|
67
77
|
if (fs.existsSync(path.join(projectRoot, '.claude')))
|
|
@@ -3,6 +3,7 @@ export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
4
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
5
5
|
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
6
|
+
export declare function promptOkfFrontmatter(): Promise<boolean>;
|
|
6
7
|
export declare function installAgentSkills(agent: Agent, projectRoot: string, framework?: string, pdrSurfacing?: boolean): void;
|
|
7
8
|
export declare const OPRIM_CONTEXT_SKILL_STEP = "## Step 0: Check relevant product decisions\nInvoke the `oprim:context` skill using the Skill tool. If matching PDRs are surfaced, review them before proceeding. If no PDRs match or `oprim/decisions/` is empty, the skill exits silently \u2014 continue to Step 1 immediately.";
|
|
8
9
|
export declare const CLAUDE_SKILLS: Record<string, string>;
|
|
@@ -40,6 +40,7 @@ exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.POOLSIDE_SKILLS = expo
|
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
41
|
exports.promptAgentSelection = promptAgentSelection;
|
|
42
42
|
exports.promptPdrSurfacing = promptPdrSurfacing;
|
|
43
|
+
exports.promptOkfFrontmatter = promptOkfFrontmatter;
|
|
43
44
|
exports.installAgentSkills = installAgentSkills;
|
|
44
45
|
exports.writeAgentInstructionFile = writeAgentInstructionFile;
|
|
45
46
|
exports.codexInstructions = codexInstructions;
|
|
@@ -96,6 +97,13 @@ async function promptPdrSurfacing() {
|
|
|
96
97
|
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
97
98
|
return confirm({ message: 'Enable proactive PDR surfacing in skills? (y/N)', default: false });
|
|
98
99
|
}
|
|
100
|
+
async function promptOkfFrontmatter() {
|
|
101
|
+
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
102
|
+
return confirm({
|
|
103
|
+
message: 'Enable OKF (Open Knowledge Format) frontmatter on scaffolded artifacts? (y/N)',
|
|
104
|
+
default: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
99
107
|
function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfacing = false) {
|
|
100
108
|
if (agent === 'claude') {
|
|
101
109
|
const claudeDir = path.join(projectRoot, '.claude');
|
|
@@ -286,6 +294,7 @@ function removeContextStepFromFile(content) {
|
|
|
286
294
|
exports.CLAUDE_SKILLS = {
|
|
287
295
|
'oprim-pdr': pdrSkill(),
|
|
288
296
|
'oprim-bet': betSkill(),
|
|
297
|
+
'oprim-note': noteSkill(),
|
|
289
298
|
'oprim-criteria': criteriaSkill(),
|
|
290
299
|
'oprim-review': reviewSkill(),
|
|
291
300
|
'oprim-archive': archiveSkill(),
|
|
@@ -293,7 +302,7 @@ exports.CLAUDE_SKILLS = {
|
|
|
293
302
|
};
|
|
294
303
|
// ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
|
|
295
304
|
exports.CLAUDE_COMMANDS = {
|
|
296
|
-
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet
|
|
305
|
+
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into an OpenSpec change', promoteContent()),
|
|
297
306
|
'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
|
|
298
307
|
'archive.md': claudeWrapper('OPRIM: Archive', 'Archive a completed bet — move it out of the active board', archiveCommandContent()),
|
|
299
308
|
};
|
|
@@ -301,6 +310,7 @@ exports.CLAUDE_COMMANDS = {
|
|
|
301
310
|
exports.POOLSIDE_SKILLS = {
|
|
302
311
|
'oprim-pdr': pdrSkill(),
|
|
303
312
|
'oprim-bet': betSkill(),
|
|
313
|
+
'oprim-note': noteSkill(),
|
|
304
314
|
'oprim-criteria': criteriaSkill(),
|
|
305
315
|
'oprim-review': reviewSkill(),
|
|
306
316
|
'oprim-archive': archiveSkill(),
|
|
@@ -310,15 +320,17 @@ exports.POOLSIDE_SKILLS = {
|
|
|
310
320
|
exports.CURSOR_SKILLS = {
|
|
311
321
|
'oprim-pdr': pdrSkill(),
|
|
312
322
|
'oprim-bet': betSkill(),
|
|
323
|
+
'oprim-note': noteSkill(),
|
|
313
324
|
'oprim-criteria': criteriaSkill(),
|
|
314
325
|
'oprim-review': reviewSkill(),
|
|
315
326
|
};
|
|
316
327
|
// ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
|
|
317
328
|
exports.CURSOR_COMMANDS = {
|
|
318
|
-
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet
|
|
329
|
+
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into an OpenSpec change', promoteContent()),
|
|
319
330
|
'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
|
|
320
331
|
'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
|
|
321
332
|
'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
|
|
333
|
+
'oprim-note.md': cursorWrapper('oprim-note', 'Create a new atomic note for lightweight thinking capture', noteInlineContent()),
|
|
322
334
|
'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
|
|
323
335
|
'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
|
|
324
336
|
};
|
|
@@ -372,7 +384,12 @@ Ask: Context (what forced this decision), Decision (clear statement), Alternativ
|
|
|
372
384
|
### 4. Check for supersession
|
|
373
385
|
Ask: "Does this supersede an existing PDR? If so, which ID? (Enter to skip)"
|
|
374
386
|
|
|
387
|
+
### 4b. Check for OKF frontmatter
|
|
388
|
+
Read \`oprim/templates/pdr.md\`. If it begins with a YAML frontmatter block (\`---\` ... \`---\`), this workspace has OKF frontmatter enabled. Ask for a one-line description and comma-separated tags (subject-area keywords). Prepare a frontmatter block with \`type: pdr\`, \`title: <title>\`, \`description: <description>\`, \`tags: [<tags>]\`, \`timestamp: <today's date, ISO 8601>\`, to prepend in step 5.
|
|
389
|
+
If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
|
|
390
|
+
|
|
375
391
|
### 5. Write the PDR file
|
|
392
|
+
Prepend the frontmatter block from step 4b, if one was prepared.
|
|
376
393
|
\`\`\`
|
|
377
394
|
# PDR-NNN: <title>
|
|
378
395
|
|
|
@@ -461,7 +478,12 @@ Then ask about each of the four risk dimensions (Low / Medium / High + short rat
|
|
|
461
478
|
- "**Feasibility risk**: Can we build this with our current skills, time, and technology? (Low / Medium / High — and why?)"
|
|
462
479
|
- "**Business viability risk**: Does this solution work for the business (revenue, legal, ops)? (Low / Medium / High — and why?)"
|
|
463
480
|
|
|
481
|
+
### 4b. Check for OKF frontmatter
|
|
482
|
+
Read \`oprim/templates/bet-decision.md\`. If it begins with a YAML frontmatter block (\`---\` ... \`---\`), this workspace has OKF frontmatter enabled. Ask for a one-line description and comma-separated tags (subject-area keywords). Prepare a frontmatter block with \`type: bet-decision\`, \`title: <title>\`, \`description: <description>\`, \`tags: [<tags>]\`, \`timestamp: <today's date, ISO 8601>\`, to prepend in step 5.
|
|
483
|
+
If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
|
|
484
|
+
|
|
464
485
|
### 5. Write oprim/bets/BET-NNN-<slug>/bet-decision.md
|
|
486
|
+
Prepend the frontmatter block from step 4b, if one was prepared.
|
|
465
487
|
\`\`\`
|
|
466
488
|
# Decision: BET-NNN <title>
|
|
467
489
|
<!-- Naming tip: verb + object [for context] — e.g. "Improve bet naming for scannability" not "Naming" -->
|
|
@@ -517,6 +539,75 @@ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
|
|
|
517
539
|
### 8. Report what was created
|
|
518
540
|
`;
|
|
519
541
|
}
|
|
542
|
+
function noteSkill() {
|
|
543
|
+
return `---
|
|
544
|
+
name: oprim-note
|
|
545
|
+
description: Create a new atomic note in oprim/notes/ for lightweight thinking capture, with tiered frontmatter and optional bet links
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
Create a new note in \`oprim/notes/\` for lightweight thinking capture — an observation, idea, or connection that hasn't yet earned a place in a bet or PDR.
|
|
549
|
+
|
|
550
|
+
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
551
|
+
|
|
552
|
+
## What you're creating
|
|
553
|
+
|
|
554
|
+
A note is a small, disposable unit of thinking: an observation, a stray idea, or a connection between bets, captured before it's proven enough to belong in a discovery hypothesis or bet-decision. Notes carry no owner and no kill criterion — they're not commitments. Promote a note into a bet later with \`/oprim:promote NOTE-NNN\` once it's worth committing to.
|
|
555
|
+
|
|
556
|
+
## Steps
|
|
557
|
+
|
|
558
|
+
### 1. Get the note title
|
|
559
|
+
If not provided, ask: "What is this note about? (a short title)"
|
|
560
|
+
|
|
561
|
+
### 2. Assign the next NOTE ID
|
|
562
|
+
Scan \`oprim/notes/\` for files matching \`NOTE-(\\d+)-\`. Extract the numeric part from each match. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
|
|
563
|
+
|
|
564
|
+
### 2b. Derive the slug
|
|
565
|
+
From the note title: lowercase all characters, replace any character that is not a letter or digit with a hyphen, collapse consecutive hyphens to one, strip leading/trailing hyphens, truncate to 40 characters at the last hyphen boundary. This becomes \`<slug>\`.
|
|
566
|
+
Output path: \`oprim/notes/NOTE-NNN-<slug>.md\`
|
|
567
|
+
|
|
568
|
+
### 3. Gather the note body
|
|
569
|
+
Ask: "What's the observation, idea, or connection?" (free-form prose — this becomes the note body).
|
|
570
|
+
|
|
571
|
+
### 4. Gather tags
|
|
572
|
+
Read \`oprim/config.yaml\`. If it has a \`notes:\` section with a \`tags:\` list, show it and ask the user to pick from it or add new ones. If \`notes.tags\` is absent or empty, ask for tags directly (comma-separated) — there's no vocabulary yet to constrain against.
|
|
573
|
+
A tag not already in \`notes.tags\` SHALL be accepted, never rejected, and appended to \`oprim/config.yaml\`'s \`notes.tags\` list (creating the \`notes:\` section if absent) — the vocabulary grows from usage rather than requiring upfront authoring.
|
|
574
|
+
|
|
575
|
+
### 5. Gather optional bet links
|
|
576
|
+
Ask: "Does this relate to any existing bets? (comma-separated BET-IDs, or Enter to skip)"
|
|
577
|
+
|
|
578
|
+
### 6. Check the frontmatter tier
|
|
579
|
+
Read \`oprim/templates/note.md\`.
|
|
580
|
+
- If it exists and its frontmatter block contains a \`description:\` field, this workspace is on the **OKF tier** — ask for a one-line description.
|
|
581
|
+
- If it exists with no \`description:\` field, use the **minimal tier** — skip the description.
|
|
582
|
+
- If the file doesn't exist (project initialized before notes were introduced), read \`oprim/config.yaml\` directly: \`okf.enabled: true\` → OKF tier (ask for a description); otherwise → minimal tier.
|
|
583
|
+
|
|
584
|
+
### 7. Write oprim/notes/NOTE-NNN-<slug>.md
|
|
585
|
+
|
|
586
|
+
Minimal tier:
|
|
587
|
+
\`\`\`
|
|
588
|
+
---
|
|
589
|
+
type: note
|
|
590
|
+
title: "<title>"
|
|
591
|
+
tags: [<tags>]
|
|
592
|
+
timestamp: <today, ISO 8601>
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
# Note: <title>
|
|
596
|
+
|
|
597
|
+
<body>
|
|
598
|
+
|
|
599
|
+
## Bets
|
|
600
|
+
- <BET-IDs from step 5, or "None">
|
|
601
|
+
\`\`\`
|
|
602
|
+
|
|
603
|
+
OKF tier: same as above, with \`description: "<description>"\` inserted immediately after \`title\`.
|
|
604
|
+
|
|
605
|
+
### 8. Link back from referenced bets
|
|
606
|
+
For each BET-ID gathered in step 5: read \`oprim/bets/BET-NNN/bet-decision.md\`, and add \`- Notes: NOTE-NNN\` under its \`## Links\` section (append to an existing \`Notes:\` line, or add a new one).
|
|
607
|
+
|
|
608
|
+
### 9. Report what was created
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
520
611
|
function criteriaSkill() {
|
|
521
612
|
return `---
|
|
522
613
|
name: oprim-criteria
|
|
@@ -820,7 +911,12 @@ Ask: reviewer name, decision quality notes.
|
|
|
820
911
|
### 5. Output path
|
|
821
912
|
\`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
|
|
822
913
|
|
|
914
|
+
### 5b. Check for OKF frontmatter
|
|
915
|
+
Read \`oprim/templates/kpi-review.md\`. If it begins with a YAML frontmatter block (\`---\` ... \`---\`), this workspace has OKF frontmatter enabled. Ask for a one-line description and comma-separated tags (derived from the reviewed bet's subject area). Prepare a frontmatter block with \`type: kpi-review\`, \`title: <bet ID and title>\`, \`description: <description>\`, \`tags: [<tags>]\`, \`timestamp: <review date, ISO 8601>\`, to prepend in step 6.
|
|
916
|
+
If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
|
|
917
|
+
|
|
823
918
|
### 6. Write the review file
|
|
919
|
+
Prepend the frontmatter block from step 5b, if one was prepared.
|
|
824
920
|
\`\`\`markdown
|
|
825
921
|
# KPI Review: BET-NNN
|
|
826
922
|
|
|
@@ -851,6 +947,9 @@ function pdrInlineContent() {
|
|
|
851
947
|
function betInlineContent() {
|
|
852
948
|
return `Create a new bet in \`oprim/bets/\`. First explain: "A bet is a product decision you're committing to explore — a problem worth solving, a hypothesis worth testing, or a direction worth taking. You'll name it, explain why now, and set a kill criterion." Then show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan \`BET-(\\d+)\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters, warn "this title may be too vague", suggest a reformulation, and ask "Proceed anyway? (y/N)" — if "n", prompt for a revised title. Gather: decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\` with an inline naming tip comment in the header. 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.`;
|
|
853
949
|
}
|
|
950
|
+
function noteInlineContent() {
|
|
951
|
+
return `Create a new note in \`oprim/notes/\` for lightweight thinking capture — an observation, idea, or connection that hasn't yet earned a place in a bet or PDR. Notes carry no owner or kill criterion; promote one into a bet later with \`/oprim:promote NOTE-NNN\`. Ask for a short title. Scan \`oprim/notes/NOTE-(\\d+)-\` for the next id (zero-padded, default 001). Ask for the note body (free-form), tags, and optional related BET-IDs. Tags are checked against \`oprim/config.yaml\`'s \`notes.tags\`; any new tag is accepted and appended to that list rather than rejected — the vocabulary grows from usage. Read \`oprim/templates/note.md\` — if its frontmatter has a \`description:\` field, this workspace is on the OKF tier and needs a one-line description; if it has no \`description:\` field, use the minimal tier; if the file doesn't exist, fall back to reading \`okf.enabled\` directly from \`oprim/config.yaml\`. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section listing any related BET-IDs. For each related bet, append \`- Notes: NOTE-NNN\` to that bet-decision's \`## Links\` section. Report what was created.`;
|
|
952
|
+
}
|
|
854
953
|
function criteriaInlineContent() {
|
|
855
954
|
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.`;
|
|
856
955
|
}
|
|
@@ -1009,11 +1108,16 @@ function mergeClaudeSettingsHooks(claudeDir) {
|
|
|
1009
1108
|
// ─── Legacy content (promote remains inline; sequence now delegates to skill) ─
|
|
1010
1109
|
function promoteContent() {
|
|
1011
1110
|
return `
|
|
1012
|
-
Promote a prioritized bet
|
|
1111
|
+
Promote an atomic note into a bet, or a prioritized bet into an OpenSpec change. The promotion path is determined solely by the prefix of the ID argument — there is no separate command for each.
|
|
1013
1112
|
|
|
1014
|
-
**Input**: Specify
|
|
1113
|
+
**Input**: Specify an ID (e.g., \`/oprim:promote BET-042\` or \`/oprim:promote NOTE-005\`) or omit to be prompted.
|
|
1015
1114
|
|
|
1016
|
-
|
|
1115
|
+
### 0. Determine the promotion path from the ID prefix
|
|
1116
|
+
- \`BET-\` → **A. Bet → OpenSpec change**
|
|
1117
|
+
- \`NOTE-\` → **B. Note → Bet**
|
|
1118
|
+
- Anything else → report "Unrecognized ID prefix — expected BET- or NOTE-" and stop. Do not silently do nothing.
|
|
1119
|
+
|
|
1120
|
+
## A. Bet → OpenSpec change
|
|
1017
1121
|
|
|
1018
1122
|
1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
1019
1123
|
2. **Validate status** — decision must be "Build now"
|
|
@@ -1033,6 +1137,16 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
|
|
|
1033
1137
|
- \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
|
|
1034
1138
|
If any artifact is missing, create it before reporting done.
|
|
1035
1139
|
8. **Report** — show what was linked and what remains for engineering
|
|
1140
|
+
|
|
1141
|
+
## B. Note → Bet
|
|
1142
|
+
|
|
1143
|
+
1. **Locate the note** — read \`oprim/notes/NOTE-XXX-<slug>.md\`
|
|
1144
|
+
2. **Assign the next BET ID** — scan both \`oprim/bets/\` and \`oprim/bets/archived/\` for directories matching \`BET-(\\d+)(-[^/]*)?\`, max+1 zero-padded to 3 digits (default 001) — same convention \`oprim-bet\` uses
|
|
1145
|
+
3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
|
|
1146
|
+
4. **Draft the bet** — write \`oprim/bets/BET-NNN-<slug>/bet-decision.md\` from the standard bet-decision structure, pre-filling only \`## Why now\` from the note's body. Leave \`Alternatives considered\`, \`Expected outcomes\`, and \`Kill criteria / rollback trigger\` as template placeholders — draft from the note, don't fabricate content it doesn't support. Ask for \`Owner\` and \`Review date\`; default \`Decision: Build now\` and \`Date\` to today.
|
|
1147
|
+
5. **Register the new bet** — append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`
|
|
1148
|
+
6. **Link back** — add \`Bets: BET-NNN\` to the note (creating or extending its \`## Bets\` section)
|
|
1149
|
+
7. **Report** — show the new bet's path and flag that \`Alternatives considered\`, \`Expected outcomes\`, and \`Kill criteria\` still need authoring before this bet can itself be promoted
|
|
1036
1150
|
`;
|
|
1037
1151
|
}
|
|
1038
1152
|
function sequenceContent() {
|
|
@@ -1092,6 +1206,18 @@ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
|
|
|
1092
1206
|
8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\`.
|
|
1093
1207
|
9. Report what was created.
|
|
1094
1208
|
|
|
1209
|
+
### Note authoring (oprim-note)
|
|
1210
|
+
Create a new note in \`oprim/notes/\` for lightweight thinking capture — not a bet, no owner or kill criterion.
|
|
1211
|
+
|
|
1212
|
+
1. Ask for a short title.
|
|
1213
|
+
2. Assign next NOTE ID: scan \`oprim/notes/NOTE-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
|
|
1214
|
+
3. Ask for the note body (free-form), tags, and optional related BET-IDs.
|
|
1215
|
+
4. Tags: check against \`oprim/config.yaml\`'s \`notes.tags\` — accept and append any new tag rather than rejecting it (the vocabulary grows from usage).
|
|
1216
|
+
5. Check \`oprim/templates/note.md\`: a \`description:\` field in its frontmatter means the OKF tier (gather a one-line description); no field means the minimal tier; if the file is missing, fall back to \`okf.enabled\` in \`oprim/config.yaml\`.
|
|
1217
|
+
6. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section.
|
|
1218
|
+
7. For each related bet, append \`- Notes: NOTE-NNN\` to that bet's \`## Links\` section.
|
|
1219
|
+
8. Report what was created.
|
|
1220
|
+
|
|
1095
1221
|
### Criteria authoring (oprim-criteria)
|
|
1096
1222
|
Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
|
|
1097
1223
|
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean): string;
|
|
1
|
+
export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean): string;
|
|
2
|
+
export declare function okfFrontmatter(type: string, titleHint: string): string;
|
|
3
|
+
export declare function noteMinimalFrontmatter(titleHint: string): string;
|
|
4
|
+
export declare function indexTemplate(projectName: string): string;
|
|
2
5
|
export declare const sequenceTemplate = "wip_limits:\n now: 2\n\nnow: []\nnext: []\nlater: []\nbacklog: []\n";
|
|
3
6
|
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
7
|
export declare const betDecisionTemplate = "# Decision: BET-XXX <Bet title>\n<!-- Naming tip: verb + object [for context] \u2014 e.g. \"Improve bet naming for scannability\" not \"Naming\" -->\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";
|
|
8
|
+
export declare const noteTemplate = "# Note: <Note title>\n\n<Capture the observation, idea, or connection while it's fresh.>\n\n## Bets\n- <BET-IDs this note relates to, or \"None\">\n";
|
|
5
9
|
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
10
|
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";
|
|
7
11
|
export declare const sequenceViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');\nconst MAX_LABEL_LEN = 40;\n\nfunction parseSequenceYaml(text) {\n const BUCKETS = ['now', 'next', 'later', 'backlog'];\n const result = Object.fromEntries(BUCKETS.map(b => [b, []]));\n let section = null, item = null, listField = null;\n\n for (const raw of text.split('\\n')) {\n const trimmed = raw.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const indent = raw.length - raw.trimStart().length;\n\n if (indent === 0) {\n const m = trimmed.match(/^(\\w+):/);\n if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }\n continue;\n }\n\n if (!section) continue;\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n item = {}; result[section].push(item); listField = null;\n const rest = trimmed.slice(2);\n const m = rest.match(/^(\\w+):\\s*(.*)/);\n if (m) item[m[1]] = m[2].replace(/^['\"]|['\"]$/g, '').trim();\n continue;\n }\n\n if (indent === 4 && item) {\n const m = trimmed.match(/^(\\w+):\\s*(.*)/);\n if (!m) continue;\n const [, key, val] = m;\n const v = val.trim();\n if (v === '[]') { item[key] = []; listField = null; }\n else if (v === '') { item[key] = []; listField = key; }\n else { item[key] = v.replace(/^['\"]|['\"]$/g, ''); listField = null; }\n continue;\n }\n\n if (indent === 6 && item && listField && trimmed.startsWith('- ')) {\n item[listField].push(trimmed.slice(2).trim());\n }\n }\n return result;\n}\n\nfunction nodeId(betId) { return betId.replace(/-/g, ''); }\n\nfunction truncate(title) {\n return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;\n}\n\nfunction generateMermaidBlock(seq) {\n const lines = ['```mermaid', 'graph TD'];\n const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];\n\n for (const [key, label] of ACTIVE) {\n const bets = seq[key] || [];\n if (!bets.length) continue;\n lines.push(' subgraph ' + label);\n for (const bet of bets) {\n lines.push(' ' + nodeId(bet.id) + '[\"' + bet.id + ': ' + truncate(bet.title) + '\"]');\n }\n lines.push(' end');\n }\n\n for (const [key] of ACTIVE) {\n for (const bet of (seq[key] || [])) {\n for (const blocker of (bet.blocked_by || [])) {\n lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));\n }\n }\n }\n\n lines.push('```');\n return lines.join('\\n');\n}\n\nfunction generateBacklogSection(seq) {\n const backlog = seq.backlog || [];\n if (!backlog.length) return '';\n return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');\n}\n\nfunction main() {\n const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));\n const header = [\n '<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',\n '',\n '# Sequencing Board',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');\n console.log('Written: oprim/sequence-view.md');\n}\n\nmain();\n";
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
3
|
+
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
4
4
|
exports.configTemplate = configTemplate;
|
|
5
|
-
|
|
5
|
+
exports.okfFrontmatter = okfFrontmatter;
|
|
6
|
+
exports.noteMinimalFrontmatter = noteMinimalFrontmatter;
|
|
7
|
+
exports.indexTemplate = indexTemplate;
|
|
8
|
+
function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled) {
|
|
6
9
|
return `version: 1
|
|
7
10
|
project:
|
|
8
11
|
name: "${projectName}"
|
|
@@ -14,6 +17,8 @@ integrations:
|
|
|
14
17
|
graphify:
|
|
15
18
|
enabled: ${graphifyEnabled}
|
|
16
19
|
graph_dir: graphify-out
|
|
20
|
+
okf:
|
|
21
|
+
enabled: ${okfEnabled}
|
|
17
22
|
measurement:
|
|
18
23
|
amplitude:
|
|
19
24
|
enabled: false
|
|
@@ -25,6 +30,41 @@ sequencing:
|
|
|
25
30
|
now: 2
|
|
26
31
|
`;
|
|
27
32
|
}
|
|
33
|
+
// OKF (Open Knowledge Format) — https://github.com/GoogleCloudPlatform/okf
|
|
34
|
+
function okfFrontmatter(type, titleHint) {
|
|
35
|
+
return `---
|
|
36
|
+
type: ${type}
|
|
37
|
+
title: "${titleHint}"
|
|
38
|
+
description: "<one-line summary>"
|
|
39
|
+
tags: []
|
|
40
|
+
timestamp: YYYY-MM-DDTHH:MM:SSZ
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
// Minimal frontmatter tier for notes — always on, independent of OKF opt-in.
|
|
46
|
+
// Deliberately smaller than okfFrontmatter(): no `description` field.
|
|
47
|
+
function noteMinimalFrontmatter(titleHint) {
|
|
48
|
+
return `---
|
|
49
|
+
type: note
|
|
50
|
+
title: "${titleHint}"
|
|
51
|
+
tags: []
|
|
52
|
+
timestamp: YYYY-MM-DDTHH:MM:SSZ
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
function indexTemplate(projectName) {
|
|
58
|
+
return `${okfFrontmatter('index', `${projectName} — oprim workspace`)}# ${projectName} — oprim workspace
|
|
59
|
+
|
|
60
|
+
OKF (Open Knowledge Format) bundle entrypoint for this oprim workspace.
|
|
61
|
+
|
|
62
|
+
## Contents
|
|
63
|
+
- [Bets](./bets/) — product bet decisions
|
|
64
|
+
- [Decisions](./decisions/) — Product Decision Records (PDRs)
|
|
65
|
+
- [Reviews](./reviews/) — KPI reviews
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
28
68
|
exports.sequenceTemplate = `wip_limits:
|
|
29
69
|
now: 2
|
|
30
70
|
|
|
@@ -87,6 +127,13 @@ exports.betDecisionTemplate = `# Decision: BET-XXX <Bet title>
|
|
|
87
127
|
- PDRs: <PDR-IDs>
|
|
88
128
|
- OpenSpec change: <path once promoted>
|
|
89
129
|
`;
|
|
130
|
+
exports.noteTemplate = `# Note: <Note title>
|
|
131
|
+
|
|
132
|
+
<Capture the observation, idea, or connection while it's fresh.>
|
|
133
|
+
|
|
134
|
+
## Bets
|
|
135
|
+
- <BET-IDs this note relates to, or "None">
|
|
136
|
+
`;
|
|
90
137
|
exports.criteriaTemplate = `metrics:
|
|
91
138
|
- id: metric_id
|
|
92
139
|
name: "Metric name"
|