@open-product-primer/cli 1.1.0 → 2.0.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.
@@ -61,13 +61,15 @@ function initCommand() {
61
61
  console.log(chalk_1.default.green('✓') + ' Graphify detected');
62
62
  console.log('');
63
63
  const okfEnabled = await (0, install_agent_1.promptOkfFrontmatter)();
64
+ const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
64
65
  const primerDir = path.join(projectRoot, 'oprim');
65
66
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
66
67
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
67
68
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
69
+ (0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
68
70
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
69
71
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
70
- const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
72
+ const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled, specFramework));
71
73
  const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
72
74
  const pdrContent = okfEnabled ? (0, templates_1.okfFrontmatter)('pdr', '<Decision title>') + templates_1.pdrTemplate : templates_1.pdrTemplate;
73
75
  const betContent = okfEnabled
@@ -76,11 +78,17 @@ function initCommand() {
76
78
  const kpiContent = okfEnabled
77
79
  ? (0, templates_1.okfFrontmatter)('kpi-review', 'KPI Review: BET-XXX') + templates_1.kpiReviewTemplate
78
80
  : templates_1.kpiReviewTemplate;
81
+ // Notes always carry frontmatter — minimal tier by default, OKF tier (adds `description`)
82
+ // when opted in — unlike the other three templates, which have no frontmatter when disabled.
83
+ const noteContent = okfEnabled
84
+ ? (0, templates_1.okfFrontmatter)('note', '<Note title>') + templates_1.noteTemplate
85
+ : (0, templates_1.noteMinimalFrontmatter)('<Note title>') + templates_1.noteTemplate;
79
86
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), pdrContent);
80
87
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'bet-decision.md'), betContent);
81
88
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
82
89
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), kpiContent);
83
90
  (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
91
+ (0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'note.md'), noteContent);
84
92
  (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
85
93
  if (okfEnabled) {
86
94
  (0, scaffold_1.writeFile)(path.join(primerDir, 'index.md'), (0, templates_1.indexTemplate)(projectName));
@@ -88,6 +96,7 @@ function initCommand() {
88
96
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
89
97
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
90
98
  (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
99
+ (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'notes', '.gitkeep'), '');
91
100
  console.log('\n' + chalk_1.default.green('✓') + ' oprim/ workspace created');
92
101
  const configStatus = configWritten ? 'written' : 'preserved (already exists)';
93
102
  const sequenceStatus = sequenceWritten ? 'written' : 'preserved (already exists)';
@@ -128,10 +137,8 @@ function initCommand() {
128
137
  ' after configuring an AI tool to install /oprim:* skills.');
129
138
  }
130
139
  else {
131
- let specFramework = 'openspec';
132
140
  let pdrSurfacing = false;
133
141
  if (selectedAgents.includes('claude')) {
134
- specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
135
142
  pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
136
143
  }
137
144
  console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
@@ -45,6 +45,19 @@ const install_agent_1 = require("../lib/install-agent");
45
45
  const detect_1 = require("../lib/detect");
46
46
  const scaffold_1 = require("../lib/scaffold");
47
47
  const templates_1 = require("../lib/templates");
48
+ const config_merge_1 = require("../lib/config-merge");
49
+ // bet-023 — persist the resolved spec_framework into oprim/config.yaml, inserting it only
50
+ // when the key is missing (never overwrites an already-persisted choice).
51
+ function persistSpecFramework(configPath, framework) {
52
+ if (!fs.existsSync(configPath))
53
+ return;
54
+ const existing = fs.readFileSync(configPath, 'utf-8');
55
+ const { content, changed } = (0, config_merge_1.mergeSpecFramework)(existing, framework);
56
+ if (changed) {
57
+ fs.writeFileSync(configPath, content, 'utf-8');
58
+ console.log(chalk_1.default.green('✓') + ` oprim/config.yaml — integrations.spec_framework set to ${framework}`);
59
+ }
60
+ }
48
61
  function updateCommand() {
49
62
  return new commander_1.Command('update')
50
63
  .description('Refresh /oprim:* assistant commands and skills from package templates')
@@ -58,13 +71,23 @@ function updateCommand() {
58
71
  const primerDir = path.join(projectRoot, 'oprim');
59
72
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
60
73
  (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
74
+ const configPath = path.join(primerDir, 'config.yaml');
75
+ if (fs.existsSync(configPath)) {
76
+ const existingConfig = fs.readFileSync(configPath, 'utf-8');
77
+ const { content: mergedConfig, changed } = (0, config_merge_1.mergeConfigSchema)(existingConfig);
78
+ if (changed) {
79
+ fs.writeFileSync(configPath, mergedConfig, 'utf-8');
80
+ console.log(chalk_1.default.green('✓') + ' oprim/config.yaml — schema updated with new keys (existing values preserved)');
81
+ }
82
+ }
61
83
  if (configAgents !== null && configAgents.length > 0) {
62
- let specFramework = 'openspec';
84
+ let specFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
63
85
  let pdrSurfacing = false;
64
86
  if (configAgents.includes('claude')) {
65
87
  specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
66
88
  pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
67
89
  }
90
+ persistSpecFramework(configPath, specFramework);
68
91
  for (const agent of configAgents) {
69
92
  (0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
70
93
  }
@@ -75,6 +98,7 @@ function updateCommand() {
75
98
  const legacyAgents = [];
76
99
  if (fs.existsSync(path.join(projectRoot, '.claude'))) {
77
100
  const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
101
+ persistSpecFramework(configPath, specFramework);
78
102
  const pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
79
103
  (0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework, pdrSurfacing);
80
104
  legacyAgents.push('claude');
@@ -99,22 +123,25 @@ function updateCommand() {
99
123
  default: false,
100
124
  });
101
125
  if (!addMore) {
126
+ persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
102
127
  console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
103
128
  return;
104
129
  }
105
130
  console.log('');
106
131
  const selected = await (0, install_agent_1.promptAgentSelection)(projectRoot);
107
132
  if (selected.length === 0) {
133
+ persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
108
134
  console.log('\n' + chalk_1.default.yellow('No agents selected.'));
109
135
  console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
110
136
  return;
111
137
  }
112
- let addSpecFramework = 'openspec';
138
+ let addSpecFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
113
139
  let addPdrSurfacing = false;
114
140
  if (selected.includes('claude')) {
115
141
  addSpecFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
116
142
  addPdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
117
143
  }
144
+ persistSpecFramework(configPath, addSpecFramework);
118
145
  console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
119
146
  for (const agent of selected) {
120
147
  (0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework, addPdrSurfacing);
@@ -0,0 +1,10 @@
1
+ export declare function mergeConfigSchema(existingContent: string): {
2
+ content: string;
3
+ changed: boolean;
4
+ };
5
+ export declare function readSpecFramework(content: string): string | null;
6
+ export declare function deriveDefaultSpecFramework(content: string): string;
7
+ export declare function mergeSpecFramework(existingContent: string, framework: string): {
8
+ content: string;
9
+ changed: boolean;
10
+ };
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeConfigSchema = mergeConfigSchema;
4
+ exports.readSpecFramework = readSpecFramework;
5
+ exports.deriveDefaultSpecFramework = deriveDefaultSpecFramework;
6
+ exports.mergeSpecFramework = mergeSpecFramework;
7
+ const CONFIG_SCHEMA_FIELDS = [
8
+ { key: 'context', block: 'context: ""\n' },
9
+ { key: 'rules', block: 'rules: {}\n' },
10
+ { key: 'store', block: 'store:\n enabled: false\n' },
11
+ ];
12
+ function existingTopLevelKeys(content) {
13
+ const keys = new Set();
14
+ for (const line of content.split('\n')) {
15
+ const match = line.match(/^([A-Za-z_][\w-]*):/);
16
+ if (match)
17
+ keys.add(match[1]);
18
+ }
19
+ return keys;
20
+ }
21
+ function mergeConfigSchema(existingContent) {
22
+ const present = existingTopLevelKeys(existingContent);
23
+ const missing = CONFIG_SCHEMA_FIELDS.filter((field) => !present.has(field.key));
24
+ if (missing.length === 0)
25
+ return { content: existingContent, changed: false };
26
+ const separator = existingContent.endsWith('\n') ? '' : '\n';
27
+ const additions = missing.map((field) => field.block).join('');
28
+ return { content: existingContent + separator + additions, changed: true };
29
+ }
30
+ // bet-023 — integrations.spec_framework is nested under `integrations:` rather than a
31
+ // top-level key, and its default depends on the project's existing openspec state, so it
32
+ // can't reuse the static CONFIG_SCHEMA_FIELDS table above. Handled as its own one-off per
33
+ // the bet's design (falls back to a minimal merge rather than depending on the generic
34
+ // top-level mechanism, which doesn't support nested keys or computed defaults).
35
+ function readSpecFramework(content) {
36
+ const match = content.match(/^\s*spec_framework:\s*(\S+)/m);
37
+ return match ? match[1] : null;
38
+ }
39
+ function deriveDefaultSpecFramework(content) {
40
+ return /^\s*openspec:\n\s*enabled:\s*true/m.test(content) ? 'openspec' : 'none';
41
+ }
42
+ function mergeSpecFramework(existingContent, framework) {
43
+ if (readSpecFramework(existingContent))
44
+ return { content: existingContent, changed: false };
45
+ const lines = existingContent.split('\n');
46
+ const integrationsIndex = lines.findIndex((line) => line === 'integrations:');
47
+ if (integrationsIndex === -1) {
48
+ const separator = existingContent.endsWith('\n') ? '' : '\n';
49
+ return { content: existingContent + separator + `integrations:\n spec_framework: ${framework}\n`, changed: true };
50
+ }
51
+ lines.splice(integrationsIndex + 1, 0, ` spec_framework: ${framework}`);
52
+ return { content: lines.join('\n'), changed: true };
53
+ }
@@ -1,6 +1,7 @@
1
1
  export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside';
2
2
  export declare const SUPPORTED_AGENTS: readonly Agent[];
3
3
  export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
4
+ export declare function resolveSpecFramework(projectRoot: string): string;
4
5
  export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
5
6
  export declare function promptPdrSurfacing(): Promise<boolean>;
6
7
  export declare function promptOkfFrontmatter(): Promise<boolean>;
@@ -11,6 +12,7 @@ export declare const CLAUDE_COMMANDS: Record<string, string>;
11
12
  export declare const POOLSIDE_SKILLS: Record<string, string>;
12
13
  export declare const CURSOR_SKILLS: Record<string, string>;
13
14
  export declare const CURSOR_COMMANDS: Record<string, string>;
15
+ export declare function specAuthoringSkill(): string;
14
16
  export declare function writeAgentInstructionFile(filePath: string, section: string): void;
15
17
  export declare function codexInstructions(): string;
16
18
  export declare function geminiInstructions(): string;
@@ -38,10 +38,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
40
40
  exports.promptFrameworkSelection = promptFrameworkSelection;
41
+ exports.resolveSpecFramework = resolveSpecFramework;
41
42
  exports.promptAgentSelection = promptAgentSelection;
42
43
  exports.promptPdrSurfacing = promptPdrSurfacing;
43
44
  exports.promptOkfFrontmatter = promptOkfFrontmatter;
44
45
  exports.installAgentSkills = installAgentSkills;
46
+ exports.specAuthoringSkill = specAuthoringSkill;
45
47
  exports.writeAgentInstructionFile = writeAgentInstructionFile;
46
48
  exports.codexInstructions = codexInstructions;
47
49
  exports.geminiInstructions = geminiInstructions;
@@ -51,30 +53,60 @@ const fs = __importStar(require("fs"));
51
53
  const chalk_1 = __importDefault(require("chalk"));
52
54
  const scaffold_1 = require("./scaffold");
53
55
  const detect_1 = require("./detect");
56
+ const config_merge_1 = require("./config-merge");
54
57
  exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'poolside'];
55
- async function promptFrameworkSelection(projectRoot) {
56
- const configPath = path.join(projectRoot, '.claude', 'hooks', 'config.json');
57
- if (fs.existsSync(configPath)) {
58
+ // oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
59
+ // selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
60
+ // projects that installed before that key existed.
61
+ function readPersistedFramework(projectRoot) {
62
+ const configYamlPath = path.join(projectRoot, 'oprim', 'config.yaml');
63
+ if (fs.existsSync(configYamlPath)) {
64
+ const persisted = (0, config_merge_1.readSpecFramework)(fs.readFileSync(configYamlPath, 'utf-8'));
65
+ if (persisted)
66
+ return persisted;
67
+ }
68
+ const hooksConfigPath = path.join(projectRoot, '.claude', 'hooks', 'config.json');
69
+ if (fs.existsSync(hooksConfigPath)) {
58
70
  try {
59
- const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
60
- if (typeof existing.framework === 'string') {
61
- console.log(chalk_1.default.dim(` Speccing framework: ${existing.framework} (from config)`));
71
+ const existing = JSON.parse(fs.readFileSync(hooksConfigPath, 'utf-8'));
72
+ if (typeof existing.framework === 'string')
62
73
  return existing.framework;
63
- }
64
74
  }
65
75
  catch {
66
- // fallthrough to prompt
76
+ // fallthrough
67
77
  }
68
78
  }
79
+ return null;
80
+ }
81
+ async function promptFrameworkSelection(projectRoot) {
82
+ const persisted = readPersistedFramework(projectRoot);
83
+ if (persisted) {
84
+ console.log(chalk_1.default.dim(` Speccing framework: ${persisted} (from config)`));
85
+ return persisted;
86
+ }
69
87
  const { select } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
70
88
  return select({
71
89
  message: 'Which speccing framework does this project use?',
72
90
  choices: [
73
91
  { name: 'OpenSpec (recommended)', value: 'openspec' },
92
+ { name: 'Native (oprim-authored specs, no OpenSpec required)', value: 'native' },
74
93
  { name: 'None', value: 'none' },
75
94
  ],
76
95
  });
77
96
  }
97
+ // No-prompt resolution used where an interactive choice isn't appropriate (e.g. non-Claude
98
+ // agent branches): the persisted value if one exists, else a default derived from the
99
+ // project's existing openspec state.
100
+ function resolveSpecFramework(projectRoot) {
101
+ const persisted = readPersistedFramework(projectRoot);
102
+ if (persisted)
103
+ return persisted;
104
+ const configYamlPath = path.join(projectRoot, 'oprim', 'config.yaml');
105
+ if (fs.existsSync(configYamlPath)) {
106
+ return (0, config_merge_1.deriveDefaultSpecFramework)(fs.readFileSync(configYamlPath, 'utf-8'));
107
+ }
108
+ return 'none';
109
+ }
78
110
  async function promptAgentSelection(projectRoot) {
79
111
  const detected = (0, detect_1.detectAvailableAgents)(projectRoot);
80
112
  if (detected.length > 0) {
@@ -129,6 +161,21 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
129
161
  (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), skillContent);
130
162
  console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
131
163
  }
164
+ // oprim-spec (native spec authoring) — install only when spec_framework is native, remove otherwise
165
+ const specSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
166
+ if (framework === 'native') {
167
+ const specSkillContent = pdrSurfacing ? withContextStep(specAuthoringSkill()) : specAuthoringSkill();
168
+ (0, scaffold_1.writeFile)(specSkillPath, specSkillContent);
169
+ console.log(chalk_1.default.green('✓') + ' .claude/skills/oprim-spec/SKILL.md');
170
+ }
171
+ else if (fs.existsSync(specSkillPath)) {
172
+ fs.unlinkSync(specSkillPath);
173
+ try {
174
+ fs.rmdirSync(path.dirname(specSkillPath));
175
+ }
176
+ catch { /* not empty or already gone */ }
177
+ console.log(chalk_1.default.dim(' removed .claude/skills/oprim-spec/SKILL.md'));
178
+ }
132
179
  // openspec skills — add/remove Step 0 in-place when they exist
133
180
  for (const name of OPENSPEC_SKILL_NAMES) {
134
181
  const skillFilePath = path.join(skillsBase, name, 'SKILL.md');
@@ -144,7 +191,12 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
144
191
  }
145
192
  const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
146
193
  for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
147
- (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
194
+ // promote.md is regenerated per-project since its content branches on the selected
195
+ // speccing framework — the static CLAUDE_COMMANDS entry only reflects the default.
196
+ const finalContent = filename === 'promote.md'
197
+ ? claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent(framework))
198
+ : content;
199
+ (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), finalContent);
148
200
  console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
149
201
  }
150
202
  // Tombstone cleanup: remove command wrappers deleted in v0.2.0 (bet/criteria/pdr/review
@@ -188,6 +240,19 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
188
240
  (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
189
241
  console.log(chalk_1.default.green('✓') + ` .poolside/skills/${name}/SKILL.md`);
190
242
  }
243
+ const poolsideSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
244
+ if (framework === 'native') {
245
+ (0, scaffold_1.writeFile)(poolsideSpecSkillPath, specAuthoringSkill());
246
+ console.log(chalk_1.default.green('✓') + ' .poolside/skills/oprim-spec/SKILL.md');
247
+ }
248
+ else if (fs.existsSync(poolsideSpecSkillPath)) {
249
+ fs.unlinkSync(poolsideSpecSkillPath);
250
+ try {
251
+ fs.rmdirSync(path.dirname(poolsideSpecSkillPath));
252
+ }
253
+ catch { /* not empty or already gone */ }
254
+ console.log(chalk_1.default.dim(' removed .poolside/skills/oprim-spec/SKILL.md'));
255
+ }
191
256
  const agentsFile = path.join(projectRoot, 'AGENTS.md');
192
257
  writeAgentInstructionFile(agentsFile, poolsideInstructions());
193
258
  console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
@@ -213,9 +278,27 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
213
278
  (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
214
279
  console.log(chalk_1.default.green('✓') + ` .cursor/skills/${name}/SKILL.md`);
215
280
  }
281
+ const cursorSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
282
+ if (framework === 'native') {
283
+ (0, scaffold_1.writeFile)(cursorSpecSkillPath, specAuthoringSkill());
284
+ console.log(chalk_1.default.green('✓') + ' .cursor/skills/oprim-spec/SKILL.md');
285
+ }
286
+ else if (fs.existsSync(cursorSpecSkillPath)) {
287
+ fs.unlinkSync(cursorSpecSkillPath);
288
+ try {
289
+ fs.rmdirSync(path.dirname(cursorSpecSkillPath));
290
+ }
291
+ catch { /* not empty or already gone */ }
292
+ console.log(chalk_1.default.dim(' removed .cursor/skills/oprim-spec/SKILL.md'));
293
+ }
216
294
  const cmdsDir = path.join(cursorDir, 'commands');
217
295
  for (const [filename, content] of Object.entries(exports.CURSOR_COMMANDS)) {
218
- (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
296
+ // oprim-promote.md is regenerated per-project since its content branches on the
297
+ // selected speccing framework — the static CURSOR_COMMANDS entry only reflects the default.
298
+ const finalContent = filename === 'oprim-promote.md'
299
+ ? cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent(framework))
300
+ : content;
301
+ (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), finalContent);
219
302
  console.log(chalk_1.default.green('✓') + ` .cursor/commands/${filename}`);
220
303
  }
221
304
  if (dirCreated) {
@@ -294,6 +377,7 @@ function removeContextStepFromFile(content) {
294
377
  exports.CLAUDE_SKILLS = {
295
378
  'oprim-pdr': pdrSkill(),
296
379
  'oprim-bet': betSkill(),
380
+ 'oprim-note': noteSkill(),
297
381
  'oprim-criteria': criteriaSkill(),
298
382
  'oprim-review': reviewSkill(),
299
383
  'oprim-archive': archiveSkill(),
@@ -301,7 +385,7 @@ exports.CLAUDE_SKILLS = {
301
385
  };
302
386
  // ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
303
387
  exports.CLAUDE_COMMANDS = {
304
- 'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
388
+ 'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent()),
305
389
  'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
306
390
  'archive.md': claudeWrapper('OPRIM: Archive', 'Archive a completed bet — move it out of the active board', archiveCommandContent()),
307
391
  };
@@ -309,6 +393,7 @@ exports.CLAUDE_COMMANDS = {
309
393
  exports.POOLSIDE_SKILLS = {
310
394
  'oprim-pdr': pdrSkill(),
311
395
  'oprim-bet': betSkill(),
396
+ 'oprim-note': noteSkill(),
312
397
  'oprim-criteria': criteriaSkill(),
313
398
  'oprim-review': reviewSkill(),
314
399
  'oprim-archive': archiveSkill(),
@@ -318,15 +403,17 @@ exports.POOLSIDE_SKILLS = {
318
403
  exports.CURSOR_SKILLS = {
319
404
  'oprim-pdr': pdrSkill(),
320
405
  'oprim-bet': betSkill(),
406
+ 'oprim-note': noteSkill(),
321
407
  'oprim-criteria': criteriaSkill(),
322
408
  'oprim-review': reviewSkill(),
323
409
  };
324
410
  // ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
325
411
  exports.CURSOR_COMMANDS = {
326
- 'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
412
+ 'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent()),
327
413
  'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
328
414
  'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
329
415
  'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
416
+ 'oprim-note.md': cursorWrapper('oprim-note', 'Create a new atomic note for lightweight thinking capture', noteInlineContent()),
330
417
  'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
331
418
  'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
332
419
  };
@@ -374,6 +461,9 @@ Scan \`oprim/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all intege
374
461
  Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
375
462
  Output path: \`oprim/decisions/PDR-NNN-<slug>.md\`
376
463
 
464
+ ### 2b. Check for custom rules
465
+ Read \`oprim/config.yaml\`. If it has a non-empty \`rules.pdr\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 3 and reflect it in the generated content. If \`rules.pdr\` is absent or empty, skip this step; behavior is unchanged.
466
+
377
467
  ### 3. Gather content
378
468
  Ask: Context (what forced this decision), Decision (clear statement), Alternatives considered (why rejected), Consequences (positives / trade-offs / follow-ups), Evidence links (optional), Related bets (optional), Related OpenSpec changes (optional).
379
469
 
@@ -462,6 +552,9 @@ From the bet title: lowercase all characters, replace any character that is not
462
552
  ### 3. Check sequence.yaml exists
463
553
  If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
464
554
 
555
+ ### 3b. Check for custom rules
556
+ Read \`oprim/config.yaml\`. If it has a non-empty \`rules.bet\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 4 and reflect it in the generated \`bet-decision.md\` content. If \`rules.bet\` is absent or empty, skip this step; behavior is unchanged.
557
+
465
558
  ### 4. Gather content
466
559
  Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date (YYYY-MM-DD), Why now, Alternatives considered, Expected outcomes (metric: baseline → target in timeframe), Kill criteria / rollback trigger, PDR links (optional).
467
560
 
@@ -535,6 +628,75 @@ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
535
628
  ### 8. Report what was created
536
629
  `;
537
630
  }
631
+ function noteSkill() {
632
+ return `---
633
+ name: oprim-note
634
+ description: Create a new atomic note in oprim/notes/ for lightweight thinking capture, with tiered frontmatter and optional bet links
635
+ ---
636
+
637
+ 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.
638
+
639
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
640
+
641
+ ## What you're creating
642
+
643
+ 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.
644
+
645
+ ## Steps
646
+
647
+ ### 1. Get the note title
648
+ If not provided, ask: "What is this note about? (a short title)"
649
+
650
+ ### 2. Assign the next NOTE ID
651
+ 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.
652
+
653
+ ### 2b. Derive the slug
654
+ 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>\`.
655
+ Output path: \`oprim/notes/NOTE-NNN-<slug>.md\`
656
+
657
+ ### 3. Gather the note body
658
+ Ask: "What's the observation, idea, or connection?" (free-form prose — this becomes the note body).
659
+
660
+ ### 4. Gather tags
661
+ 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.
662
+ 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.
663
+
664
+ ### 5. Gather optional bet links
665
+ Ask: "Does this relate to any existing bets? (comma-separated BET-IDs, or Enter to skip)"
666
+
667
+ ### 6. Check the frontmatter tier
668
+ Read \`oprim/templates/note.md\`.
669
+ - If it exists and its frontmatter block contains a \`description:\` field, this workspace is on the **OKF tier** — ask for a one-line description.
670
+ - If it exists with no \`description:\` field, use the **minimal tier** — skip the description.
671
+ - 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.
672
+
673
+ ### 7. Write oprim/notes/NOTE-NNN-<slug>.md
674
+
675
+ Minimal tier:
676
+ \`\`\`
677
+ ---
678
+ type: note
679
+ title: "<title>"
680
+ tags: [<tags>]
681
+ timestamp: <today, ISO 8601>
682
+ ---
683
+
684
+ # Note: <title>
685
+
686
+ <body>
687
+
688
+ ## Bets
689
+ - <BET-IDs from step 5, or "None">
690
+ \`\`\`
691
+
692
+ OKF tier: same as above, with \`description: "<description>"\` inserted immediately after \`title\`.
693
+
694
+ ### 8. Link back from referenced bets
695
+ 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).
696
+
697
+ ### 9. Report what was created
698
+ `;
699
+ }
538
700
  function criteriaSkill() {
539
701
  return `---
540
702
  name: oprim-criteria
@@ -593,10 +755,10 @@ If not: create with \`metrics:\` list.
593
755
  function archiveSkill() {
594
756
  return `---
595
757
  name: oprim-archive
596
- description: Archive a completed bet — moves it to oprim/bets/archived/ and removes its sequence.yaml entry
758
+ description: Archive a completed bet — moves it to oprim/bets/archived/, removes its sequence.yaml entry, and folds any spec deltas under its specs/ directory into oprim/specs/ current truth
597
759
  ---
598
760
 
599
- Archive a completed bet by moving it to \`oprim/bets/archived/\` and removing it from \`sequence.yaml\`.
761
+ Archive a completed bet by moving it to \`oprim/bets/archived/\`, removing it from \`sequence.yaml\`, and (if present) merging its spec deltas into current truth.
600
762
 
601
763
  **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
602
764
 
@@ -626,26 +788,50 @@ If neither pattern matches:
626
788
  - Report: "Bet BET-NNN was not found in oprim/bets/. Nothing was changed."
627
789
  - Stop.
628
790
 
629
- ### 3. Check for active dependencies in sequence.yaml
791
+ ### 3. Check for active dependencies and concurrent spec-delta conflicts
630
792
 
631
793
  Read \`oprim/sequence.yaml\`. Scan every entry across all buckets (now, next, later, backlog) for any entry whose \`blocked_by\` or \`unlocks\` list contains the target bet ID.
632
794
 
633
- If dependents are found:
634
- - Show a warning listing each dependent entry and which field references the target bet.
795
+ Separately, if \`oprim/bets/<resolved-dir>/specs/\` exists: for each \`<capability>/spec.md\` delta file under it, extract every \`### Requirement:\` header from its \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` sections. Then scan every other bet directory directly under \`oprim/bets/\` (excluding \`archived/\` and the bet being archived) for a \`specs/<capability>/spec.md\` file for the same capability; if one exists, extract its \`### Requirement:\` headers too. Flag any header that matches (whitespace-insensitive) between the archiving bet's delta and another still-active bet's delta as an **overlap**.
796
+
797
+ If either sequence.yaml dependents or delta overlaps are found:
798
+ - Show a combined warning listing each dependent entry and each overlapping requirement.
635
799
 
636
800
  Example:
637
801
  \`\`\`
638
802
  ⚠ Warning: BET-005 is referenced by active bets:
639
803
  - BET-007 (blocked_by: [BET-005])
640
804
  - BET-008 (unlocks: [BET-005])
805
+ ⚠ Warning: BET-005's delta for requirement "The system SHALL ..." in capability foo overlaps with active bet BET-009's delta for the same requirement. Archiving BET-005 now applies its version to oprim/specs/foo/spec.md; if BET-009 archives later, its version will overwrite this requirement again (last-write-wins — no 3-way merge is attempted).
641
806
  \`\`\`
642
- - Ask: "Archive BET-NNN anyway? These references will become stale. (y/N)"
807
+ - Ask: "Archive BET-NNN anyway? (y/N)"
643
808
  - If "n" or Enter: stop, no changes made.
644
809
  - If "y": proceed.
645
810
 
646
- If no dependents found: proceed without warning.
811
+ If neither is found: proceed without warning.
647
812
 
648
- ### 4. Move the bet directory to archive
813
+ ### 4. Fold spec deltas into current truth
814
+
815
+ If \`oprim/bets/<resolved-dir>/specs/\` does not exist: skip this step entirely and go to Step 5 — archive behavior is unchanged from before spec deltas existed.
816
+
817
+ Otherwise, for each capability subdirectory under \`oprim/bets/<resolved-dir>/specs/\` containing a \`spec.md\`:
818
+
819
+ 1. Read the delta file's \`## ADDED Requirements\` / \`## MODIFIED Requirements\` / \`## REMOVED Requirements\` sections. Each \`### Requirement:\` block runs from its header through its body and any \`#### Scenario:\` sub-entries, up to the next \`### Requirement:\` or \`## \` header.
820
+ 2. Read \`oprim/specs/<capability>/spec.md\` if it exists (current truth uses a single flat \`## Requirements\` section).
821
+ - **If it does not exist:**
822
+ - If the delta is entirely \`## ADDED Requirements\` (no MODIFIED/REMOVED sections): create \`oprim/specs/<capability>/spec.md\` with a \`## Requirements\` header and append each ADDED requirement block beneath it.
823
+ - If the delta contains any MODIFIED or REMOVED requirements: stop before moving anything and report an error — "cannot modify/remove requirement '<header>' in capability <capability> — no current-truth spec exists yet for this capability."
824
+ - **If it does exist:**
825
+ - **ADDED**: append the requirement block to the end of the \`## Requirements\` section.
826
+ - **MODIFIED**: find the existing \`### Requirement:\` block whose header text matches the delta's (whitespace-insensitive); replace that entire block (header, body, and scenarios) with the delta's version. If no match is found, treat it as ADDED instead (append) and note this in the final report.
827
+ - **REMOVED**: find and delete the matching block entirely. If no match is found, note this in the final report and continue — nothing to remove.
828
+ 3. Write the updated \`oprim/specs/<capability>/spec.md\`.
829
+
830
+ This fold always overwrites the matched requirement wholesale — it never reconciles two bets' overlapping changes. If a later bet's archive touches the same requirement again, its version simply replaces this one (last-write-wins, confirmed by construction — no 3-way merge).
831
+
832
+ Track which capabilities were merged (and any no-match notes) for the final report.
833
+
834
+ ### 5. Move the bet directory to archive
649
835
 
650
836
  Create the archive subfolder if it doesn't exist:
651
837
  \`\`\`bash
@@ -657,11 +843,11 @@ Move the resolved directory:
657
843
  mv oprim/bets/<resolved-dir> oprim/bets/archived/<resolved-dir>
658
844
  \`\`\`
659
845
 
660
- ### 5. Remove the bet entry from sequence.yaml
846
+ ### 6. Remove the bet entry from sequence.yaml
661
847
 
662
848
  Read \`oprim/sequence.yaml\`, parse it, and remove the entry with \`id: BET-NNN\` from whichever bucket it appears in (now, next, later, or backlog). Write the updated YAML back using 2-space indentation. Do not modify any other entries.
663
849
 
664
- ### 6. Report what was done
850
+ ### 7. Report what was done
665
851
 
666
852
  \`\`\`
667
853
  ## Bet Archived
@@ -669,6 +855,7 @@ Read \`oprim/sequence.yaml\`, parse it, and remove the entry with \`id: BET-NNN\
669
855
  **Bet:** BET-NNN
670
856
  **Archived to:** oprim/bets/archived/<resolved-dir>/
671
857
  **Removed from sequence.yaml:** ✓
858
+ **Spec deltas merged:** <capability-1>, <capability-2> (omit this line if no specs/ directory was present)
672
859
 
673
860
  The bet is preserved in full at the archive location.
674
861
  \`\`\`
@@ -813,6 +1000,9 @@ Create a KPI review in \`oprim/reviews/\`.
813
1000
  ### 1. Identify the bet
814
1001
  If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
815
1002
 
1003
+ ### 1b. Check for custom rules
1004
+ Read \`oprim/config.yaml\`. If it has a non-empty \`rules.review\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 4 and reflect it in the generated review content. If \`rules.review\` is absent or empty, skip this step; behavior is unchanged.
1005
+
816
1006
  ### 2. Load criteria and check for a run result
817
1007
 
818
1008
  Read \`oprim/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
@@ -867,18 +1057,92 @@ Prepend the frontmatter block from step 5b, if one was prepared.
867
1057
  ### 7. Report what was created
868
1058
  `;
869
1059
  }
1060
+ function specAuthoringSkill() {
1061
+ return `---
1062
+ name: oprim-spec
1063
+ description: Generate a native oprim capability spec delta at oprim/bets/BET-NNN-<slug>/specs/<capability>/spec.md while a bet is active, in RFC 2119 (SHALL/SHOULD/MAY) requirements and Gherkin scenarios — folded into oprim/specs/<capability>/spec.md (current truth) when the bet is archived
1064
+ ---
1065
+
1066
+ Generate a capability spec delta for an active bet — RFC 2119 requirements plus Gherkin scenarios, no OpenSpec required. This skill never writes to \`oprim/specs/\` directly; \`oprim-archive\` folds the delta into current truth when the bet is archived.
1067
+
1068
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
1069
+
1070
+ ## Steps
1071
+
1072
+ ### 1. Get the active bet
1073
+ If a bet ID was provided as context (e.g. invoked from \`/oprim:promote\`), use it directly. Otherwise ask: "Which bet is this spec change for? (e.g. BET-005)"
1074
+
1075
+ Resolve it to a directory in \`oprim/bets/\` using the same two patterns \`oprim-archive\` uses: exact \`BET-NNN/\` (legacy, no slug) or the slug variant \`BET-NNN-<slug>/\`. If neither matches, report "Bet BET-NNN was not found in oprim/bets/ — spec deltas can only be authored against an active bet" and stop.
1076
+
1077
+ ### 2. Get the capability name and description
1078
+ If not provided, ask: "What capability are you specifying? (a short name, e.g. 'spec-authoring')" and "What does it do? (one or two sentences)"
1079
+
1080
+ ### 2b. Derive the slug
1081
+ From the capability name: 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. This becomes \`<capability>\`.
1082
+ Output path: \`oprim/bets/<resolved-bet-dir>/specs/<capability>/spec.md\` (a delta, not \`oprim/specs/<capability>/spec.md\` — that file is current truth and is only ever written by \`oprim-archive\`'s merge step).
1083
+
1084
+ ### 2c. Check for custom rules
1085
+ Read \`oprim/config.yaml\`. If it has a non-empty \`rules.spec\` value, treat it as additional guidance from the team — factor it into the requirements and scenarios you draft. If \`rules.spec\` is absent or empty, skip this step; behavior is unchanged.
1086
+
1087
+ ### 3. Determine the delta type for each requirement
1088
+ For each requirement, ask whether it is new (**ADDED**), a change to an existing current-truth requirement (**MODIFIED**), or a removal of one (**REMOVED**).
1089
+
1090
+ - **ADDED**: gather the requirement statement fresh.
1091
+ - **MODIFIED / REMOVED**: read \`oprim/specs/<capability>/spec.md\` if it exists and list its \`### Requirement:\` headers so the user can pick the one being changed. The header text must match exactly (whitespace-insensitive) for \`oprim-archive\`'s merge step to find it later. If the file doesn't exist yet, MODIFIED/REMOVED aren't possible for this capability — fall back to ADDED.
1092
+
1093
+ ### 4. Gather requirements and scenarios
1094
+ For ADDED and MODIFIED requirements, phrase each as an RFC 2119 statement using SHALL (mandatory), SHOULD (recommended), or MAY (optional), then ask for at least one scenario: a WHEN (trigger) and a THEN (expected outcome), with an optional GIVEN (context) and additional AND steps. REMOVED requirements only need the matching header — no new scenarios.
1095
+
1096
+ ### 5. Write the delta file
1097
+ Append to (or create) \`oprim/bets/<resolved-bet-dir>/specs/<capability>/spec.md\`, grouping requirements under the matching section header — only include a section if it has at least one requirement under it:
1098
+
1099
+ \`\`\`markdown
1100
+ ## ADDED Requirements
1101
+
1102
+ ### Requirement: <capability> SHALL/SHOULD/MAY <requirement statement>
1103
+ <one-sentence elaboration>
1104
+
1105
+ #### Scenario: <scenario title>
1106
+ - **GIVEN** <context> (optional)
1107
+ - **WHEN** <trigger>
1108
+ - **THEN** <outcome>
1109
+ - **AND** <additional outcome> (optional)
1110
+
1111
+ ## MODIFIED Requirements
1112
+
1113
+ ### Requirement: <exact header text matched from oprim/specs/<capability>/spec.md>
1114
+ <revised elaboration>
1115
+
1116
+ #### Scenario: <scenario title>
1117
+ - **WHEN** <trigger>
1118
+ - **THEN** <outcome>
1119
+
1120
+ ## REMOVED Requirements
1121
+
1122
+ ### Requirement: <exact header text matched from oprim/specs/<capability>/spec.md>
1123
+ \`\`\`
1124
+
1125
+ If the delta file already exists (a prior spec-authoring pass for this bet/capability), append new requirements to the matching section, creating that section if it's not yet present.
1126
+
1127
+ ### 6. Report what was created
1128
+ Show the delta file path, which bet it's scoped to, and a summary of the ADDED/MODIFIED/REMOVED requirements captured. Note that it merges into \`oprim/specs/<capability>/spec.md\` when \`BET-NNN\` is archived — nothing is current truth yet.
1129
+ `;
1130
+ }
870
1131
  // ─── Cursor inline content (condensed versions for command files) ─────────────
871
1132
  function pdrInlineContent() {
872
- return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
1133
+ return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Read \`oprim/config.yaml\`'s \`rules.pdr\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
873
1134
  }
874
1135
  function betInlineContent() {
875
- 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.`;
1136
+ 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). Read \`oprim/config.yaml\`'s \`rules.bet\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. 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.`;
1137
+ }
1138
+ function noteInlineContent() {
1139
+ 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.`;
876
1140
  }
877
1141
  function criteriaInlineContent() {
878
1142
  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
1143
  }
880
1144
  function reviewInlineContent() {
881
- return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
1145
+ return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`oprim/config.yaml\`'s \`rules.review\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
882
1146
  }
883
1147
  // ─── Hook scripts: co-archival coordination ──────────────────────────────────
884
1148
  function hooksConfig(framework) {
@@ -1030,15 +1294,14 @@ function mergeClaudeSettingsHooks(claudeDir) {
1030
1294
  console.log(chalk_1.default.green('✓') + ' .claude/settings.json (UserPromptSubmit + Stop hooks registered)');
1031
1295
  }
1032
1296
  // ─── Legacy content (promote remains inline; sequence now delegates to skill) ─
1033
- function promoteContent() {
1034
- return `
1035
- Promote a prioritized bet to an OpenSpec change and link criteria contracts.
1036
-
1037
- **Input**: Specify a bet ID (e.g., \`/oprim:promote BET-042\`) or omit to be prompted.
1038
-
1039
- **Steps**
1040
-
1041
- 1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1297
+ function promoteContent(framework = 'openspec') {
1298
+ const sectionATitle = framework === 'openspec'
1299
+ ? 'A. Bet OpenSpec change'
1300
+ : framework === 'native'
1301
+ ? 'A. Bet native oprim spec'
1302
+ : 'A. Bet → spec (no framework configured)';
1303
+ const sectionABody = framework === 'openspec'
1304
+ ? `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1042
1305
  2. **Validate status** — decision must be "Build now"
1043
1306
  3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
1044
1307
  4. **Create OpenSpec change** — derive the change name as \`bet-NNN-<slug>\` where \`NNN\` is the zero-padded bet number (e.g. BET-004 → \`bet-004\`) and \`<slug>\` is a short kebab-case summary of the change. Then invoke the \`/openspec-propose\` skill (or \`/opsx:propose\`) with that name to create the change directory with **all required artifacts**: \`proposal.md\`, \`design.md\`, \`tasks.md\`, and \`specs/<capability>/spec.md\` for every capability listed under \`## Capabilities\`.
@@ -1055,7 +1318,41 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
1055
1318
  - \`tasks.md\`
1056
1319
  - \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
1057
1320
  If any artifact is missing, create it before reporting done.
1058
- 8. **Report** — show what was linked and what remains for engineering
1321
+ 8. **Report** — show what was linked and what remains for engineering`
1322
+ : framework === 'native'
1323
+ ? `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1324
+ 2. **Validate status** — decision must be "Build now"
1325
+ 3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
1326
+ 4. **Generate the native spec delta(s)** — for each capability listed under the bet's \`## Capabilities\` section (or a single capability derived from the bet title if none is listed), invoke the \`oprim-spec\` skill with this bet as context to write \`oprim/bets/BET-XXX/specs/<capability>/spec.md\` — a delta using \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` headers reflecting the bet's why/outcome. No OpenSpec change directory is created and OpenSpec need not be installed. Nothing is written to \`oprim/specs/<capability>/spec.md\` (current truth) at promote time — that only happens when this bet is archived.
1327
+ 5. **Link artifacts** — add \`- Spec (delta): oprim/bets/BET-XXX/specs/<capability>/spec.md\` (one line per capability) to the bet-decision \`## Links\` section
1328
+ 6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, note it alongside the spec link
1329
+ 7. **Report** — show what was created and linked, and note that merge-on-archive will fold the delta into \`oprim/specs/\` when the bet archives`
1330
+ : `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1331
+ 2. **Validate status** — decision must be "Build now"
1332
+ 3. **Report and stop** — no speccing framework is configured (\`integrations.spec_framework: none\`). Add \`- Spec: none (no speccing framework configured)\` to the bet-decision \`## Links\` section. No spec artifact is created.`;
1333
+ return `
1334
+ Promote an atomic note into a bet, or a prioritized bet into a capability spec. The promotion path is determined solely by the prefix of the ID argument — there is no separate command for each.
1335
+
1336
+ **Input**: Specify an ID (e.g., \`/oprim:promote BET-042\` or \`/oprim:promote NOTE-005\`) or omit to be prompted.
1337
+
1338
+ ### 0. Determine the promotion path from the ID prefix
1339
+ - \`BET-\` → **${sectionATitle}**
1340
+ - \`NOTE-\` → **B. Note → Bet**
1341
+ - Anything else → report "Unrecognized ID prefix — expected BET- or NOTE-" and stop. Do not silently do nothing.
1342
+
1343
+ ## ${sectionATitle}
1344
+
1345
+ ${sectionABody}
1346
+
1347
+ ## B. Note → Bet
1348
+
1349
+ 1. **Locate the note** — read \`oprim/notes/NOTE-XXX-<slug>.md\`
1350
+ 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
1351
+ 3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
1352
+ 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.
1353
+ 5. **Register the new bet** — append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`
1354
+ 6. **Link back** — add \`Bets: BET-NNN\` to the note (creating or extending its \`## Bets\` section)
1355
+ 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
1356
  `;
1060
1357
  }
1061
1358
  function sequenceContent() {
@@ -1109,12 +1406,25 @@ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
1109
1406
  2. Ask for the bet title. Validate: fewer than 4 words OR fewer than 25 chars → warn, suggest reformulation, ask "Proceed anyway? (y/N)".
1110
1407
  3. Assign next BET ID: scan \`oprim/bets/BET-(\\d+)\` dirs, max+1 zero-padded to 3 digits (default 001).
1111
1408
  4. Check \`oprim/sequence.yaml\` exists — stop if not, advise \`oprim init\`.
1409
+ 4b. Read \`oprim/config.yaml\`'s \`rules.bet\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
1112
1410
  5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
1113
1411
  6. Write \`oprim/bets/BET-NNN/bet-decision.md\` with all fields.
1114
1412
  7. Append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`.
1115
1413
  8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\`.
1116
1414
  9. Report what was created.
1117
1415
 
1416
+ ### Note authoring (oprim-note)
1417
+ Create a new note in \`oprim/notes/\` for lightweight thinking capture — not a bet, no owner or kill criterion.
1418
+
1419
+ 1. Ask for a short title.
1420
+ 2. Assign next NOTE ID: scan \`oprim/notes/NOTE-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
1421
+ 3. Ask for the note body (free-form), tags, and optional related BET-IDs.
1422
+ 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).
1423
+ 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\`.
1424
+ 6. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section.
1425
+ 7. For each related bet, append \`- Notes: NOTE-NNN\` to that bet's \`## Links\` section.
1426
+ 8. Report what was created.
1427
+
1118
1428
  ### Criteria authoring (oprim-criteria)
1119
1429
  Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
1120
1430
 
@@ -1132,6 +1442,7 @@ Create a new Product Decision Record in \`oprim/decisions/\`.
1132
1442
 
1133
1443
  1. Ask for decision title.
1134
1444
  2. Assign next PDR ID: scan \`oprim/decisions/PDR-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
1445
+ 2b. Read \`oprim/config.yaml\`'s \`rules.pdr\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
1135
1446
  3. Gather: context, decision, alternatives, consequences, evidence, related bets/specs.
1136
1447
  4. Ask if superseding an existing PDR.
1137
1448
  5. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding, update old PDR Status.
@@ -1141,6 +1452,7 @@ Create a new Product Decision Record in \`oprim/decisions/\`.
1141
1452
  Create a KPI review artifact in \`oprim/reviews/\`.
1142
1453
 
1143
1454
  1. Ask which bet (e.g. BET-042).
1455
+ 1b. Read \`oprim/config.yaml\`'s \`rules.review\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
1144
1456
  2. Read \`oprim/bets/BET-NNN/criteria.yaml\` for pre-fill. Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` — use most recent if present.
1145
1457
  3. If no run result, ask for each metric's actual value.
1146
1458
  4. Status: actual >= target → hit; actual < target → missed; not provided → pending.
@@ -1153,10 +1465,11 @@ Archive a completed bet.
1153
1465
 
1154
1466
  1. Ask for bet ID (accept bet-005, 005, 5, BET-005 — normalize to BET-NNN).
1155
1467
  2. Verify \`oprim/bets/BET-NNN/\` exists.
1156
- 3. Check \`oprim/sequence.yaml\` for entries where \`blocked_by\` or \`unlocks\` reference the target bet — warn if found, ask "Archive anyway? (y/N)".
1157
- 4. Move directory: \`oprim/bets/BET-NNN oprim/bets/archived/BET-NNN\`.
1158
- 5. Remove the bet entry from \`oprim/sequence.yaml\`.
1159
- 6. Report what was done.
1468
+ 3. Check \`oprim/sequence.yaml\` for entries where \`blocked_by\` or \`unlocks\` reference the target bet — warn if found. Also check other active bet dirs for delta specs against the same requirement (matching \`### Requirement:\` headers, whitespace-insensitive) — warn if an overlap is found. Ask "Archive anyway? (y/N)" if either warning fires.
1469
+ 4. If \`oprim/bets/BET-NNN/specs/\` exists, fold each capability's \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` delta into \`oprim/specs/<capability>/spec.md\` (matching by \`### Requirement:\` header; create the current-truth file if the delta is entirely ADDED) — last-write-wins on overlaps, no 3-way merge. Skip this step entirely if no \`specs/\` dir is present.
1470
+ 5. Move directory: \`oprim/bets/BET-NNN oprim/bets/archived/BET-NNN\`.
1471
+ 6. Remove the bet entry from \`oprim/sequence.yaml\`.
1472
+ 7. Report what was done.
1160
1473
 
1161
1474
  ### Sequencing board (oprim-sequence)
1162
1475
  Validate the primer sequencing board and regenerate the visual view.
@@ -97,7 +97,8 @@ function checkSkillVersionDrift(projectRoot, checks) {
97
97
  const skillsDir = path.join(projectRoot, '.claude', 'skills');
98
98
  if (!fs.existsSync(skillsDir))
99
99
  return;
100
- for (const [name, bundledContent] of Object.entries(install_agent_1.CLAUDE_SKILLS)) {
100
+ const bundledSkills = { ...install_agent_1.CLAUDE_SKILLS, 'oprim-spec': (0, install_agent_1.specAuthoringSkill)() };
101
+ for (const [name, bundledContent] of Object.entries(bundledSkills)) {
101
102
  const skillPath = path.join(skillsDir, name, 'SKILL.md');
102
103
  if (!fs.existsSync(skillPath))
103
104
  continue;
@@ -1,9 +1,11 @@
1
- export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean): string;
1
+ export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean, specFramework?: string): 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,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
3
+ exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
4
4
  exports.configTemplate = configTemplate;
5
5
  exports.okfFrontmatter = okfFrontmatter;
6
+ exports.noteMinimalFrontmatter = noteMinimalFrontmatter;
6
7
  exports.indexTemplate = indexTemplate;
7
- function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled) {
8
+ function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled, specFramework = openspecEnabled ? 'openspec' : 'none') {
8
9
  return `version: 1
9
10
  project:
10
11
  name: "${projectName}"
@@ -16,6 +17,7 @@ integrations:
16
17
  graphify:
17
18
  enabled: ${graphifyEnabled}
18
19
  graph_dir: graphify-out
20
+ spec_framework: ${specFramework}
19
21
  okf:
20
22
  enabled: ${okfEnabled}
21
23
  measurement:
@@ -27,6 +29,10 @@ measurement:
27
29
  sequencing:
28
30
  wip_limits:
29
31
  now: 2
32
+ context: ""
33
+ rules: {}
34
+ store:
35
+ enabled: false
30
36
  `;
31
37
  }
32
38
  // OKF (Open Knowledge Format) — https://github.com/GoogleCloudPlatform/okf
@@ -39,6 +45,18 @@ tags: []
39
45
  timestamp: YYYY-MM-DDTHH:MM:SSZ
40
46
  ---
41
47
 
48
+ `;
49
+ }
50
+ // Minimal frontmatter tier for notes — always on, independent of OKF opt-in.
51
+ // Deliberately smaller than okfFrontmatter(): no `description` field.
52
+ function noteMinimalFrontmatter(titleHint) {
53
+ return `---
54
+ type: note
55
+ title: "${titleHint}"
56
+ tags: []
57
+ timestamp: YYYY-MM-DDTHH:MM:SSZ
58
+ ---
59
+
42
60
  `;
43
61
  }
44
62
  function indexTemplate(projectName) {
@@ -114,6 +132,13 @@ exports.betDecisionTemplate = `# Decision: BET-XXX <Bet title>
114
132
  - PDRs: <PDR-IDs>
115
133
  - OpenSpec change: <path once promoted>
116
134
  `;
135
+ exports.noteTemplate = `# Note: <Note title>
136
+
137
+ <Capture the observation, idea, or connection while it's fresh.>
138
+
139
+ ## Bets
140
+ - <BET-IDs this note relates to, or "None">
141
+ `;
117
142
  exports.criteriaTemplate = `metrics:
118
143
  - id: metric_id
119
144
  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": "2.0.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",