@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.
@@ -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, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), templates_1.pdrTemplate);
71
- (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'bet-decision.md'), templates_1.betDecisionTemplate);
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'), templates_1.kpiReviewTemplate);
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;
@@ -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);
@@ -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;
@@ -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 to an OpenSpec change', promoteContent()),
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 to an OpenSpec change', promoteContent()),
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 to an OpenSpec change and link criteria contracts.
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 a bet ID (e.g., \`/oprim:promote BET-042\`) or omit to be prompted.
1113
+ **Input**: Specify an ID (e.g., \`/oprim:promote BET-042\` or \`/oprim:promote NOTE-005\`) or omit to be prompted.
1015
1114
 
1016
- **Steps**
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
 
@@ -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";
@@ -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
- function configTemplate(projectName, openspecEnabled, graphifyEnabled) {
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",