@open-product-primer/cli 0.5.0 → 0.7.1

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.
@@ -189,6 +189,8 @@ function doctorCommand() {
189
189
  for (const entry of betEntries) {
190
190
  if (!entry.isDirectory())
191
191
  continue;
192
+ if (entry.name === 'archived')
193
+ continue;
192
194
  const betDir = path.join(betsDir, entry.name);
193
195
  const hasDecision = fs.existsSync(path.join(betDir, 'bet-decision.md'));
194
196
  if (!hasDecision)
@@ -64,6 +64,7 @@ function initCommand() {
64
64
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
65
65
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
66
66
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
67
+ (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
67
68
  const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected));
68
69
  const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
69
70
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), templates_1.pdrTemplate);
@@ -71,6 +72,7 @@ function initCommand() {
71
72
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
72
73
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), templates_1.kpiReviewTemplate);
73
74
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
75
+ (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
74
76
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
75
77
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
76
78
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
@@ -80,6 +82,7 @@ function initCommand() {
80
82
  console.log(' ' + chalk_1.default.gray('oprim/config.yaml') + ' — ' + configStatus);
81
83
  console.log(' ' + chalk_1.default.gray('oprim/sequence.yaml') + ' — ' + sequenceStatus);
82
84
  console.log(' ' + chalk_1.default.gray('oprim/templates/') + ' — refreshed');
85
+ console.log(' ' + chalk_1.default.gray('oprim/scripts/') + ' — refreshed');
83
86
  // ── Agent selection ───────────────────────────────────────────────────────
84
87
  let selectedAgents;
85
88
  const flaggedAgents = opts.agent;
@@ -113,12 +116,14 @@ function initCommand() {
113
116
  }
114
117
  else {
115
118
  let specFramework = 'openspec';
119
+ let pdrSurfacing = false;
116
120
  if (selectedAgents.includes('claude')) {
117
121
  specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
122
+ pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
118
123
  }
119
124
  console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
120
125
  for (const agent of selectedAgents) {
121
- (0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework);
126
+ (0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
122
127
  }
123
128
  console.log('\n' + chalk_1.default.green('✓') + ` Agent skills installed: ${selectedAgents.join(', ')}`);
124
129
  }
@@ -43,19 +43,26 @@ const fs = __importStar(require("fs"));
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
44
  const install_agent_1 = require("../lib/install-agent");
45
45
  const detect_1 = require("../lib/detect");
46
+ const scaffold_1 = require("../lib/scaffold");
47
+ const templates_1 = require("../lib/templates");
46
48
  function updateCommand() {
47
49
  return new commander_1.Command('update')
48
50
  .description('Refresh /oprim:* assistant commands and skills from package templates')
49
51
  .action(async () => {
50
52
  const projectRoot = process.cwd();
51
53
  const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
54
+ const primerDir = path.join(projectRoot, 'oprim');
55
+ (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
56
+ (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
52
57
  if (configAgents !== null && configAgents.length > 0) {
53
58
  let specFramework = 'openspec';
59
+ let pdrSurfacing = false;
54
60
  if (configAgents.includes('claude')) {
55
61
  specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
62
+ pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
56
63
  }
57
64
  for (const agent of configAgents) {
58
- (0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework);
65
+ (0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
59
66
  }
60
67
  console.log(`\nAgent skills updated: ${configAgents.join(', ')}`);
61
68
  }
@@ -64,7 +71,8 @@ function updateCommand() {
64
71
  const legacyAgents = [];
65
72
  if (fs.existsSync(path.join(projectRoot, '.claude'))) {
66
73
  const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
67
- (0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework);
74
+ const pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
75
+ (0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework, pdrSurfacing);
68
76
  legacyAgents.push('claude');
69
77
  }
70
78
  if (fs.existsSync(path.join(projectRoot, '.cursor'))) {
@@ -98,12 +106,14 @@ function updateCommand() {
98
106
  return;
99
107
  }
100
108
  let addSpecFramework = 'openspec';
109
+ let addPdrSurfacing = false;
101
110
  if (selected.includes('claude')) {
102
111
  addSpecFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
112
+ addPdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
103
113
  }
104
114
  console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
105
115
  for (const agent of selected) {
106
- (0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework);
116
+ (0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework, addPdrSurfacing);
107
117
  }
108
118
  const merged = Array.from(new Set([...currentAgents, ...selected]));
109
119
  (0, detect_1.writeAgentsToConfig)(merged, projectRoot);
@@ -2,7 +2,9 @@ export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini';
2
2
  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
- export declare function installAgentSkills(agent: Agent, projectRoot: string, framework?: string): void;
5
+ export declare function promptPdrSurfacing(): Promise<boolean>;
6
+ export declare function installAgentSkills(agent: Agent, projectRoot: string, framework?: string, pdrSurfacing?: boolean): void;
7
+ 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.";
6
8
  export declare const CLAUDE_SKILLS: Record<string, string>;
7
9
  export declare const CLAUDE_COMMANDS: Record<string, string>;
8
10
  export declare const CURSOR_SKILLS: Record<string, string>;
@@ -36,9 +36,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.SUPPORTED_AGENTS = void 0;
39
+ exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
40
40
  exports.promptFrameworkSelection = promptFrameworkSelection;
41
41
  exports.promptAgentSelection = promptAgentSelection;
42
+ exports.promptPdrSurfacing = promptPdrSurfacing;
42
43
  exports.installAgentSkills = installAgentSkills;
43
44
  exports.writeAgentInstructionFile = writeAgentInstructionFile;
44
45
  exports.codexInstructions = codexInstructions;
@@ -89,15 +90,48 @@ async function promptAgentSelection(projectRoot) {
89
90
  ],
90
91
  });
91
92
  }
92
- function installAgentSkills(agent, projectRoot, framework = 'openspec') {
93
+ async function promptPdrSurfacing() {
94
+ const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
95
+ return confirm({ message: 'Enable proactive PDR surfacing in skills? (y/N)', default: false });
96
+ }
97
+ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfacing = false) {
93
98
  if (agent === 'claude') {
94
99
  const claudeDir = path.join(projectRoot, '.claude');
95
100
  const dirCreated = !fs.existsSync(claudeDir);
96
101
  const skillsBase = path.join(claudeDir, 'skills');
102
+ // oprim:context skill — install when opted in, remove when opted out
103
+ const contextSkillPath = path.join(skillsBase, 'oprim:context', 'SKILL.md');
104
+ if (pdrSurfacing) {
105
+ (0, scaffold_1.writeFile)(contextSkillPath, oprimContextSkill());
106
+ console.log(chalk_1.default.green('✓') + ' .claude/skills/oprim:context/SKILL.md');
107
+ }
108
+ else if (fs.existsSync(contextSkillPath)) {
109
+ fs.unlinkSync(contextSkillPath);
110
+ try {
111
+ fs.rmdirSync(path.dirname(contextSkillPath));
112
+ }
113
+ catch { /* not empty or already gone */ }
114
+ console.log(chalk_1.default.dim(' removed .claude/skills/oprim:context/SKILL.md'));
115
+ }
116
+ // oprim skills — prepend Step 0 when opted in
97
117
  for (const [name, content] of Object.entries(exports.CLAUDE_SKILLS)) {
98
- (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
118
+ const skillContent = pdrSurfacing ? withContextStep(content) : content;
119
+ (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), skillContent);
99
120
  console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
100
121
  }
122
+ // openspec skills — add/remove Step 0 in-place when they exist
123
+ for (const name of OPENSPEC_SKILL_NAMES) {
124
+ const skillFilePath = path.join(skillsBase, name, 'SKILL.md');
125
+ if (fs.existsSync(skillFilePath)) {
126
+ const current = fs.readFileSync(skillFilePath, 'utf-8');
127
+ const updated = pdrSurfacing ? addContextStepToFile(current) : removeContextStepFromFile(current);
128
+ if (updated !== current) {
129
+ fs.writeFileSync(skillFilePath, updated, 'utf-8');
130
+ console.log(chalk_1.default.green('✓') +
131
+ ` .claude/skills/${name}/SKILL.md (PDR surfacing ${pdrSurfacing ? 'enabled' : 'disabled'})`);
132
+ }
133
+ }
134
+ }
101
135
  const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
102
136
  for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
103
137
  (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
@@ -164,6 +198,73 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec') {
164
198
  }
165
199
  }
166
200
  }
201
+ // ─── PDR context surfacing ────────────────────────────────────────────────────
202
+ const CTX_STEP_START = '<!-- oprim:context:start -->';
203
+ const CTX_STEP_END = '<!-- oprim:context:end -->';
204
+ exports.OPRIM_CONTEXT_SKILL_STEP = `## Step 0: Check relevant product decisions
205
+ Invoke 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 — continue to Step 1 immediately.`;
206
+ // Openspec skill names whose files are managed by the openspec CLI and modified in-place by oprim
207
+ const OPENSPEC_SKILL_NAMES = [
208
+ 'openspec-propose',
209
+ 'openspec-apply-change',
210
+ 'openspec-explore',
211
+ 'openspec-archive-change',
212
+ ];
213
+ // Insert Step 0 into an oprim skill string (written fresh each time — no markers needed)
214
+ function withContextStep(content) {
215
+ const lines = content.split('\n');
216
+ let closingDash = -1;
217
+ let dashCount = 0;
218
+ for (let i = 0; i < lines.length; i++) {
219
+ if (lines[i].trim() === '---') {
220
+ dashCount++;
221
+ if (dashCount === 2) {
222
+ closingDash = i;
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ if (closingDash === -1)
228
+ return exports.OPRIM_CONTEXT_SKILL_STEP + '\n\n' + content;
229
+ const front = lines.slice(0, closingDash + 1).join('\n');
230
+ const body = lines.slice(closingDash + 1).join('\n').trimStart();
231
+ return `${front}\n\n${exports.OPRIM_CONTEXT_SKILL_STEP}\n\n${body}`;
232
+ }
233
+ // Idempotently add Step 0 to an openspec skill file (read-modify-write)
234
+ function addContextStepToFile(content) {
235
+ if (content.includes(CTX_STEP_START))
236
+ return content; // already present
237
+ const lines = content.split('\n');
238
+ let closingDash = -1;
239
+ let dashCount = 0;
240
+ for (let i = 0; i < lines.length; i++) {
241
+ if (lines[i].trim() === '---') {
242
+ dashCount++;
243
+ if (dashCount === 2) {
244
+ closingDash = i;
245
+ break;
246
+ }
247
+ }
248
+ }
249
+ const block = `${CTX_STEP_START}\n${exports.OPRIM_CONTEXT_SKILL_STEP}\n${CTX_STEP_END}`;
250
+ if (closingDash === -1)
251
+ return block + '\n\n' + content;
252
+ const front = lines.slice(0, closingDash + 1).join('\n');
253
+ const body = lines.slice(closingDash + 1).join('\n').trimStart();
254
+ return `${front}\n\n${block}\n\n${body}`;
255
+ }
256
+ // Remove Step 0 block from an openspec skill file (read-modify-write)
257
+ function removeContextStepFromFile(content) {
258
+ const s = content.indexOf(CTX_STEP_START);
259
+ if (s === -1)
260
+ return content;
261
+ const e = content.indexOf(CTX_STEP_END, s);
262
+ if (e === -1)
263
+ return content;
264
+ const before = content.slice(0, s).trimEnd();
265
+ const after = content.slice(e + CTX_STEP_END.length).replace(/^\n+/, '\n');
266
+ return before + after;
267
+ }
167
268
  // ─── Claude skill playbooks ───────────────────────────────────────────────────
168
269
  exports.CLAUDE_SKILLS = {
169
270
  'oprim-pdr': pdrSkill(),
@@ -486,6 +587,36 @@ The bet is preserved in full at the archive location.
486
587
  \`\`\`
487
588
  `;
488
589
  }
590
+ function oprimContextSkill() {
591
+ return `---
592
+ name: oprim:context
593
+ description: Surface relevant product decisions from oprim/decisions/ by keyword-matching against the current conversation — invoke at the start of any oprim or openspec workflow when PDR surfacing is enabled
594
+ ---
595
+
596
+ Scan \`oprim/decisions/\` and surface PDRs that match keywords from the current conversation.
597
+
598
+ ## Steps
599
+
600
+ ### 1. Check for decisions
601
+ Scan \`oprim/decisions/\` for files matching \`PDR-*.md\`. If the directory is empty or contains no PDR files, exit silently — produce no output and return immediately.
602
+
603
+ ### 2. Extract keywords
604
+ From the current conversation context, extract 3–10 topic keywords: bet IDs referenced (e.g. \`BET-007\`), capability names, filenames mentioned, subject-area nouns. Focus on the most specific and distinctive terms.
605
+
606
+ ### 3. Match PDRs
607
+ For each PDR file: read the filename and the first 25 lines (to capture title, status, and context). A PDR is relevant if any keyword appears in the filename, title (\`# PDR-NNN: ...\`), or body text (case-insensitive).
608
+
609
+ ### 4. Report or exit silently
610
+ If one or more PDRs match:
611
+
612
+ **Relevant product decisions:**
613
+ - PDR-NNN: <title> — <Status> (\`oprim/decisions/PDR-NNN-<slug>.md\`)
614
+
615
+ List each match on its own line, then return — the invoking skill continues to its next step.
616
+
617
+ If no PDRs match: exit silently — produce no output.
618
+ `;
619
+ }
489
620
  function archiveCommandContent() {
490
621
  return `Use the Skill tool to invoke the \`oprim-archive\` skill.`;
491
622
  }
@@ -720,6 +851,7 @@ Validate the primer sequencing board and suggest rebalancing if needed.
720
851
  4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
721
852
  5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
722
853
  6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
854
+ 7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
723
855
  `;
724
856
  }
725
857
  // ─── Instruction-file helpers (Codex / Gemini CLI) ────────────────────────────
@@ -803,6 +935,17 @@ Archive a completed bet.
803
935
  4. Move directory: \`oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN\`.
804
936
  5. Remove the bet entry from \`oprim/sequence.yaml\`.
805
937
  6. Report what was done.
938
+
939
+ ### Sequencing board (oprim-sequence)
940
+ Validate the primer sequencing board and regenerate the visual view.
941
+
942
+ 1. **Read board** — load \`oprim/sequence.yaml\`
943
+ 2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
944
+ 3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
945
+ 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
946
+ 5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
947
+ 6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
948
+ 7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
806
949
  `;
807
950
  }
808
951
  function codexInstructions() {
@@ -230,10 +230,12 @@ async function runBigQueryMetric(sqlPath) {
230
230
  }
231
231
  // ─── Criteria scanner (used by doctor) ───────────────────────────────────────
232
232
  function scanCriteriaForSourceType(projectRoot, sourceType) {
233
- const betsDir = path.join(projectRoot, 'primer', 'bets');
233
+ const betsDir = path.join(projectRoot, 'oprim', 'bets');
234
234
  if (!fs.existsSync(betsDir))
235
235
  return false;
236
236
  for (const entry of fs.readdirSync(betsDir)) {
237
+ if (entry === 'archived')
238
+ continue;
237
239
  const criteriaPath = path.join(betsDir, entry, 'criteria.yaml');
238
240
  if (!fs.existsSync(criteriaPath))
239
241
  continue;
@@ -4,4 +4,5 @@ export declare const pdrTemplate = "# PDR-XXX: <Decision title>\n\n## Status\nPr
4
4
  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";
5
5
  export declare const criteriaTemplate = "metrics:\n - id: metric_id\n name: \"Metric name\"\n baseline: 0.00\n target: 0.00\n timeframe: \"30 days post-launch\"\n launch_date: \"YYYY-MM-DD\"\n source:\n type: amplitude\n definition:\n event: event_name\n aggregation: unique_users\n denominator_event: null\n segment: null\n";
6
6
  export declare const discoveryTemplate = "# Discovery: BET-XXX <Bet title>\n\n## Problem Framing\n- **Problem statement**: <What problem are we solving and for whom?>\n- **Evidence this is real**: <Data, support tickets, user quotes, etc.>\n- **Why it matters**: <Business or user impact if left unsolved>\n\n## User Research Signals\n- **Research conducted**: <Interviews, surveys, usability tests, etc.>\n- **Key findings**: <What did we learn?>\n- **Assumptions to validate**: <What are we still unsure about?>\n\n## Competitive Context\n- **How others solve this**: <Competitor or adjacent solutions>\n- **Our differentiation**: <Why our approach is better or different>\n- **Gaps / opportunities**: <What's underserved in the market?>\n\n## Open Questions\n- [ ] <Question 1 \u2014 what needs to be answered before committing?>\n- [ ] <Question 2>\n- [ ] <Question 3>\n";
7
+ 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";
7
8
  export declare const kpiReviewTemplate = "# KPI Review: BET-XXX\n\n**Review date:** YYYY-MM-DD\n**Reviewed by:** <name>\n\n| Metric | Baseline | Target | Actual | Status |\n|--------|----------|--------|--------|--------|\n| <metric name> | - | - | - | pending |\n\n## Decision quality\n<Did outcomes validate the decision? What would you do differently?>\n\n## Actions\n- [ ] Update bet-decision outcome section\n- [ ] Update affected PDRs\n- [ ] Re-sequence impacted bets\n";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.kpiReviewTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
3
+ exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
4
4
  exports.configTemplate = configTemplate;
5
5
  function configTemplate(projectName, openspecEnabled, graphifyEnabled) {
6
6
  return `version: 1
@@ -124,6 +124,114 @@ exports.discoveryTemplate = `# Discovery: BET-XXX <Bet title>
124
124
  - [ ] <Question 2>
125
125
  - [ ] <Question 3>
126
126
  `;
127
+ exports.sequenceViewScriptTemplate = `#!/usr/bin/env node
128
+ 'use strict';
129
+
130
+ const fs = require('fs');
131
+ const path = require('path');
132
+
133
+ const SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');
134
+ const OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');
135
+ const MAX_LABEL_LEN = 40;
136
+
137
+ function parseSequenceYaml(text) {
138
+ const BUCKETS = ['now', 'next', 'later', 'backlog'];
139
+ const result = Object.fromEntries(BUCKETS.map(b => [b, []]));
140
+ let section = null, item = null, listField = null;
141
+
142
+ for (const raw of text.split('\\n')) {
143
+ const trimmed = raw.trim();
144
+ if (!trimmed || trimmed.startsWith('#')) continue;
145
+ const indent = raw.length - raw.trimStart().length;
146
+
147
+ if (indent === 0) {
148
+ const m = trimmed.match(/^(\\w+):/);
149
+ if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }
150
+ continue;
151
+ }
152
+
153
+ if (!section) continue;
154
+
155
+ if (indent === 2 && trimmed.startsWith('- ')) {
156
+ item = {}; result[section].push(item); listField = null;
157
+ const rest = trimmed.slice(2);
158
+ const m = rest.match(/^(\\w+):\\s*(.*)/);
159
+ if (m) item[m[1]] = m[2].replace(/^['"]|['"]$/g, '').trim();
160
+ continue;
161
+ }
162
+
163
+ if (indent === 4 && item) {
164
+ const m = trimmed.match(/^(\\w+):\\s*(.*)/);
165
+ if (!m) continue;
166
+ const [, key, val] = m;
167
+ const v = val.trim();
168
+ if (v === '[]') { item[key] = []; listField = null; }
169
+ else if (v === '') { item[key] = []; listField = key; }
170
+ else { item[key] = v.replace(/^['"]|['"]$/g, ''); listField = null; }
171
+ continue;
172
+ }
173
+
174
+ if (indent === 6 && item && listField && trimmed.startsWith('- ')) {
175
+ item[listField].push(trimmed.slice(2).trim());
176
+ }
177
+ }
178
+ return result;
179
+ }
180
+
181
+ function nodeId(betId) { return betId.replace(/-/g, ''); }
182
+
183
+ function truncate(title) {
184
+ return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;
185
+ }
186
+
187
+ function generateMermaidBlock(seq) {
188
+ const lines = ['\`\`\`mermaid', 'graph TD'];
189
+ const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];
190
+
191
+ for (const [key, label] of ACTIVE) {
192
+ const bets = seq[key] || [];
193
+ if (!bets.length) continue;
194
+ lines.push(' subgraph ' + label);
195
+ for (const bet of bets) {
196
+ lines.push(' ' + nodeId(bet.id) + '["' + bet.id + ': ' + truncate(bet.title) + '"]');
197
+ }
198
+ lines.push(' end');
199
+ }
200
+
201
+ for (const [key] of ACTIVE) {
202
+ for (const bet of (seq[key] || [])) {
203
+ for (const blocker of (bet.blocked_by || [])) {
204
+ lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));
205
+ }
206
+ }
207
+ }
208
+
209
+ lines.push('\`\`\`');
210
+ return lines.join('\\n');
211
+ }
212
+
213
+ function generateBacklogSection(seq) {
214
+ const backlog = seq.backlog || [];
215
+ if (!backlog.length) return '';
216
+ return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');
217
+ }
218
+
219
+ function main() {
220
+ const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));
221
+ const header = [
222
+ '<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',
223
+ '<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',
224
+ '',
225
+ '# Sequencing Board',
226
+ '',
227
+ '',
228
+ ].join('\\n');
229
+ fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');
230
+ console.log('Written: oprim/sequence-view.md');
231
+ }
232
+
233
+ main();
234
+ `;
127
235
  exports.kpiReviewTemplate = `# KPI Review: BET-XXX
128
236
 
129
237
  **Review date:** YYYY-MM-DD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.1",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",