@open-product-primer/cli 1.1.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.
@@ -65,6 +65,7 @@ function initCommand() {
65
65
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
66
66
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
67
67
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
68
+ (0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
68
69
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
69
70
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
70
71
  const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
@@ -76,11 +77,17 @@ function initCommand() {
76
77
  const kpiContent = okfEnabled
77
78
  ? (0, templates_1.okfFrontmatter)('kpi-review', 'KPI Review: BET-XXX') + templates_1.kpiReviewTemplate
78
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;
79
85
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), pdrContent);
80
86
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'bet-decision.md'), betContent);
81
87
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
82
88
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), kpiContent);
83
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);
84
91
  (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
85
92
  if (okfEnabled) {
86
93
  (0, scaffold_1.writeFile)(path.join(primerDir, 'index.md'), (0, templates_1.indexTemplate)(projectName));
@@ -88,6 +95,7 @@ function initCommand() {
88
95
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
89
96
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
90
97
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
98
+ (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'notes', '.gitkeep'), '');
91
99
  console.log('\n' + chalk_1.default.green('✓') + ' oprim/ workspace created');
92
100
  const configStatus = configWritten ? 'written' : 'preserved (already exists)';
93
101
  const sequenceStatus = sequenceWritten ? 'written' : 'preserved (already exists)';
@@ -294,6 +294,7 @@ function removeContextStepFromFile(content) {
294
294
  exports.CLAUDE_SKILLS = {
295
295
  'oprim-pdr': pdrSkill(),
296
296
  'oprim-bet': betSkill(),
297
+ 'oprim-note': noteSkill(),
297
298
  'oprim-criteria': criteriaSkill(),
298
299
  'oprim-review': reviewSkill(),
299
300
  'oprim-archive': archiveSkill(),
@@ -301,7 +302,7 @@ exports.CLAUDE_SKILLS = {
301
302
  };
302
303
  // ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
303
304
  exports.CLAUDE_COMMANDS = {
304
- '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()),
305
306
  'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
306
307
  'archive.md': claudeWrapper('OPRIM: Archive', 'Archive a completed bet — move it out of the active board', archiveCommandContent()),
307
308
  };
@@ -309,6 +310,7 @@ exports.CLAUDE_COMMANDS = {
309
310
  exports.POOLSIDE_SKILLS = {
310
311
  'oprim-pdr': pdrSkill(),
311
312
  'oprim-bet': betSkill(),
313
+ 'oprim-note': noteSkill(),
312
314
  'oprim-criteria': criteriaSkill(),
313
315
  'oprim-review': reviewSkill(),
314
316
  'oprim-archive': archiveSkill(),
@@ -318,15 +320,17 @@ exports.POOLSIDE_SKILLS = {
318
320
  exports.CURSOR_SKILLS = {
319
321
  'oprim-pdr': pdrSkill(),
320
322
  'oprim-bet': betSkill(),
323
+ 'oprim-note': noteSkill(),
321
324
  'oprim-criteria': criteriaSkill(),
322
325
  'oprim-review': reviewSkill(),
323
326
  };
324
327
  // ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
325
328
  exports.CURSOR_COMMANDS = {
326
- '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()),
327
330
  'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
328
331
  'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
329
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()),
330
334
  'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
331
335
  'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
332
336
  };
@@ -535,6 +539,75 @@ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
535
539
  ### 8. Report what was created
536
540
  `;
537
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
+ }
538
611
  function criteriaSkill() {
539
612
  return `---
540
613
  name: oprim-criteria
@@ -874,6 +947,9 @@ function pdrInlineContent() {
874
947
  function betInlineContent() {
875
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.`;
876
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
+ }
877
953
  function criteriaInlineContent() {
878
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.`;
879
955
  }
@@ -1032,11 +1108,16 @@ function mergeClaudeSettingsHooks(claudeDir) {
1032
1108
  // ─── Legacy content (promote remains inline; sequence now delegates to skill) ─
1033
1109
  function promoteContent() {
1034
1110
  return `
1035
- 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.
1036
1112
 
1037
- **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.
1038
1114
 
1039
- **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
1040
1121
 
1041
1122
  1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1042
1123
  2. **Validate status** — decision must be "Build now"
@@ -1056,6 +1137,16 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
1056
1137
  - \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
1057
1138
  If any artifact is missing, create it before reporting done.
1058
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
1059
1150
  `;
1060
1151
  }
1061
1152
  function sequenceContent() {
@@ -1115,6 +1206,18 @@ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
1115
1206
  8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\`.
1116
1207
  9. Report what was created.
1117
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
+
1118
1221
  ### Criteria authoring (oprim-criteria)
1119
1222
  Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
1120
1223
 
@@ -1,9 +1,11 @@
1
1
  export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean): string;
2
2
  export declare function okfFrontmatter(type: string, titleHint: string): string;
3
+ export declare function noteMinimalFrontmatter(titleHint: string): string;
3
4
  export declare function indexTemplate(projectName: string): string;
4
5
  export declare const sequenceTemplate = "wip_limits:\n now: 2\n\nnow: []\nnext: []\nlater: []\nbacklog: []\n";
5
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";
6
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";
7
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";
8
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";
9
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,9 @@
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;
6
7
  exports.indexTemplate = indexTemplate;
7
8
  function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled) {
8
9
  return `version: 1
@@ -39,6 +40,18 @@ tags: []
39
40
  timestamp: YYYY-MM-DDTHH:MM:SSZ
40
41
  ---
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
+
42
55
  `;
43
56
  }
44
57
  function indexTemplate(projectName) {
@@ -114,6 +127,13 @@ exports.betDecisionTemplate = `# Decision: BET-XXX <Bet title>
114
127
  - PDRs: <PDR-IDs>
115
128
  - OpenSpec change: <path once promoted>
116
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
+ `;
117
137
  exports.criteriaTemplate = `metrics:
118
138
  - id: metric_id
119
139
  name: "Metric name"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "1.1.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",