@open-product-primer/cli 2.6.0 → 2.8.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.
@@ -48,7 +48,7 @@ function initCommand() {
48
48
  return new commander_1.Command('init')
49
49
  .description('Initialize oprim in the current repository')
50
50
  .option('--name <name>', 'project name (defaults to directory name)')
51
- .option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe)', (val, prev) => [...prev, val], [])
51
+ .option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe, qwen, kimi)', (val, prev) => [...prev, val], [])
52
52
  .action(async (opts) => {
53
53
  const projectRoot = process.cwd();
54
54
  const projectName = opts.name ?? path.basename(projectRoot);
@@ -42,6 +42,7 @@ const path = __importStar(require("path"));
42
42
  const fs = __importStar(require("fs"));
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
44
  const yaml = __importStar(require("js-yaml"));
45
+ const hyperlink_1 = require("../lib/hyperlink");
45
46
  function resolveBetDecisionPath(betsDir, betId) {
46
47
  if (!fs.existsSync(betsDir))
47
48
  return null;
@@ -128,15 +129,16 @@ function renderLane(label, bets, betsDir, showMeta) {
128
129
  return;
129
130
  }
130
131
  for (const bet of bets) {
132
+ const entry = (0, hyperlink_1.hyperlinkBetEntry)(betsDir, bet.id, `${bet.id} ${bet.title}`);
131
133
  if (showMeta) {
132
134
  const meta = loadBetMeta(betsDir, bet.id);
133
135
  const inline = renderInlineMeta(meta);
134
136
  const blockers = renderBlockers(bet);
135
- console.log(` ${bet.id} ${bet.title} ${inline}${blockers}`);
137
+ console.log(` ${entry} ${inline}${blockers}`);
136
138
  }
137
139
  else {
138
140
  const blockers = renderBlockers(bet);
139
- console.log(` ${bet.id} ${bet.title}${blockers}`);
141
+ console.log(` ${entry}${blockers}`);
140
142
  }
141
143
  }
142
144
  }
@@ -86,6 +86,10 @@ function detectAvailableAgents(projectRoot) {
86
86
  detected.push('poolside');
87
87
  if (fs.existsSync(path.join(projectRoot, '.vibe')))
88
88
  detected.push('vibe');
89
+ if (fs.existsSync(path.join(projectRoot, '.qwen')))
90
+ detected.push('qwen');
91
+ if (fs.existsSync(path.join(projectRoot, '.kimi')))
92
+ detected.push('kimi');
89
93
  return detected;
90
94
  }
91
95
  function writeAgentsToConfig(agents, projectRoot) {
@@ -0,0 +1,7 @@
1
+ /** Wraps `text` in an OSC 8 terminal hyperlink escape sequence targeting `uri`. */
2
+ export declare function osc8(text: string, uri: string): string;
3
+ /**
4
+ * Resolves `betIdInput` to its `bet-decision.md` path under `betsDir` and returns an OSC 8
5
+ * hyperlink wrapping `text`. Falls back to plain `text` if the bet can't be resolved.
6
+ */
7
+ export declare function hyperlinkBetEntry(betsDir: string, betIdInput: string, text: string): string;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.osc8 = osc8;
37
+ exports.hyperlinkBetEntry = hyperlinkBetEntry;
38
+ const path = __importStar(require("path"));
39
+ const spec_delta_1 = require("./spec-delta");
40
+ /** Wraps `text` in an OSC 8 terminal hyperlink escape sequence targeting `uri`. */
41
+ function osc8(text, uri) {
42
+ return `\x1b]8;;${uri}\x1b\\${text}\x1b]8;;\x1b\\`;
43
+ }
44
+ function fileUri(absPath) {
45
+ return 'file://' + encodeURI(absPath.split(path.sep).join('/'));
46
+ }
47
+ /**
48
+ * Resolves `betIdInput` to its `bet-decision.md` path under `betsDir` and returns an OSC 8
49
+ * hyperlink wrapping `text`. Falls back to plain `text` if the bet can't be resolved.
50
+ */
51
+ function hyperlinkBetEntry(betsDir, betIdInput, text) {
52
+ const dirName = (0, spec_delta_1.resolveBetDirectory)(betsDir, betIdInput);
53
+ if (!dirName)
54
+ return text;
55
+ const decisionPath = path.join(betsDir, dirName, 'bet-decision.md');
56
+ return osc8(text, fileUri(decisionPath));
57
+ }
@@ -1,4 +1,4 @@
1
- export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe';
1
+ export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe' | 'qwen' | 'kimi';
2
2
  export declare const SUPPORTED_AGENTS: readonly Agent[];
3
3
  export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
4
4
  export declare function resolveSpecFramework(projectRoot: string): string;
@@ -11,6 +11,8 @@ export declare const CLAUDE_SKILLS: Record<string, string>;
11
11
  export declare const CLAUDE_COMMANDS: Record<string, string>;
12
12
  export declare const POOLSIDE_SKILLS: Record<string, string>;
13
13
  export declare const VIBE_SKILLS: Record<string, string>;
14
+ export declare const QWEN_SKILLS: Record<string, string>;
15
+ export declare const KIMI_SKILLS: Record<string, string>;
14
16
  export declare const CURSOR_SKILLS: Record<string, string>;
15
17
  export declare const CURSOR_COMMANDS: Record<string, string>;
16
18
  export declare function specAuthoringSkill(): string;
@@ -19,3 +21,5 @@ export declare function codexInstructions(): string;
19
21
  export declare function geminiInstructions(): string;
20
22
  export declare function poolsideInstructions(): string;
21
23
  export declare function vibeInstructions(): string;
24
+ export declare function qwenInstructions(): string;
25
+ export declare function kimiInstructions(): string;
@@ -36,7 +36,7 @@ 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.VIBE_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
39
+ exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.KIMI_SKILLS = exports.QWEN_SKILLS = exports.VIBE_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
41
  exports.resolveSpecFramework = resolveSpecFramework;
42
42
  exports.promptAgentSelection = promptAgentSelection;
@@ -49,6 +49,8 @@ exports.codexInstructions = codexInstructions;
49
49
  exports.geminiInstructions = geminiInstructions;
50
50
  exports.poolsideInstructions = poolsideInstructions;
51
51
  exports.vibeInstructions = vibeInstructions;
52
+ exports.qwenInstructions = qwenInstructions;
53
+ exports.kimiInstructions = kimiInstructions;
52
54
  const path = __importStar(require("path"));
53
55
  const fs = __importStar(require("fs"));
54
56
  const chalk_1 = __importDefault(require("chalk"));
@@ -57,7 +59,16 @@ const detect_1 = require("./detect");
57
59
  const config_merge_1 = require("./config-merge");
58
60
  const workflow_schema_1 = require("./workflow-schema");
59
61
  const workflow_renderer_1 = require("./workflow-renderer");
60
- exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'poolside', 'vibe'];
62
+ exports.SUPPORTED_AGENTS = [
63
+ 'claude',
64
+ 'cursor',
65
+ 'codex',
66
+ 'gemini',
67
+ 'poolside',
68
+ 'vibe',
69
+ 'qwen',
70
+ 'kimi',
71
+ ];
61
72
  // oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
62
73
  // selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
63
74
  // projects that installed before that key existed.
@@ -126,6 +137,8 @@ async function promptAgentSelection(projectRoot) {
126
137
  { name: 'Gemini CLI', value: 'gemini', checked: detected.includes('gemini') },
127
138
  { name: 'Poolside', value: 'poolside', checked: detected.includes('poolside') },
128
139
  { name: 'Mistral Vibe', value: 'vibe', checked: detected.includes('vibe') },
140
+ { name: 'Qwen Code', value: 'qwen', checked: detected.includes('qwen') },
141
+ { name: 'Kimi CLI', value: 'kimi', checked: detected.includes('kimi') },
129
142
  ],
130
143
  });
131
144
  }
@@ -142,8 +155,8 @@ async function promptOkfFrontmatter() {
142
155
  }
143
156
  // Workflow ids installed as Claude/Poolside/Cursor skill files — see packages/cli/src/workflows/.
144
157
  // Order matches the pre-refactor CLAUDE_SKILLS/POOLSIDE_SKILLS/CURSOR_SKILLS declaration order.
145
- const CLAUDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'context'];
146
- const POOLSIDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence'];
158
+ const CLAUDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'context', 'explore', 'reconcile'];
159
+ const POOLSIDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'explore', 'reconcile'];
147
160
  const CURSOR_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review'];
148
161
  // Claude command wrappers (thin, invoke skill) — filename -> workflow id. Order matches the
149
162
  // pre-refactor CLAUDE_COMMANDS declaration order.
@@ -152,6 +165,8 @@ const CLAUDE_COMMAND_WORKFLOWS = [
152
165
  { filename: 'sequence.md', id: 'sequence' },
153
166
  { filename: 'archive.md', id: 'archive' },
154
167
  { filename: 'context-init.md', id: 'context' },
168
+ { filename: 'explore.md', id: 'explore' },
169
+ { filename: 'reconcile.md', id: 'reconcile' },
155
170
  ];
156
171
  // Cursor command files (full inline). Order matches the pre-refactor CURSOR_COMMANDS declaration order.
157
172
  const CURSOR_COMMAND_WORKFLOW_IDS = ['promote', 'sequence', 'pdr', 'bet', 'note', 'criteria', 'review'];
@@ -314,6 +329,78 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
314
329
  console.log(chalk_1.default.dim(' .vibe/ created — Mistral Vibe will discover these files automatically.'));
315
330
  }
316
331
  }
332
+ else if (agent === 'qwen') {
333
+ const qwenDir = path.join(projectRoot, '.qwen');
334
+ const dirCreated = !fs.existsSync(qwenDir);
335
+ const skillsBase = path.join(qwenDir, 'skills');
336
+ for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
337
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
338
+ if (!schema.qwen.skill || !schema.skillName)
339
+ continue;
340
+ (0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
341
+ console.log(chalk_1.default.green('✓') + ` .qwen/skills/${schema.skillName}/SKILL.md`);
342
+ }
343
+ const qwenSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
344
+ if (framework === 'native') {
345
+ (0, scaffold_1.writeFile)(qwenSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
346
+ console.log(chalk_1.default.green('✓') + ' .qwen/skills/oprim-spec/SKILL.md');
347
+ }
348
+ else if (fs.existsSync(qwenSpecSkillPath)) {
349
+ fs.unlinkSync(qwenSpecSkillPath);
350
+ try {
351
+ fs.rmdirSync(path.dirname(qwenSpecSkillPath));
352
+ }
353
+ catch { /* not empty or already gone */ }
354
+ console.log(chalk_1.default.dim(' removed .qwen/skills/oprim-spec/SKILL.md'));
355
+ }
356
+ const agentsFile = path.join(projectRoot, 'AGENTS.md');
357
+ writeAgentInstructionFile(agentsFile, qwenInstructions());
358
+ console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
359
+ if (dirCreated) {
360
+ console.log(chalk_1.default.dim(' .qwen/ created — Qwen Code will discover these files automatically.'));
361
+ }
362
+ }
363
+ else if (agent === 'kimi') {
364
+ // Kimi CLI is a split-path install: .kimi/ is the detection signal, but skills are
365
+ // discovered from a project-root .skills/ directory, not .kimi/skills/ — see
366
+ // kimi-cli-agent-support spec.
367
+ const kimiDir = path.join(projectRoot, '.kimi');
368
+ const kimiDirCreated = !fs.existsSync(kimiDir);
369
+ if (kimiDirCreated) {
370
+ fs.mkdirSync(kimiDir, { recursive: true });
371
+ }
372
+ const skillsBase = path.join(projectRoot, '.skills');
373
+ const skillsDirCreated = !fs.existsSync(skillsBase);
374
+ for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
375
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
376
+ if (!schema.kimi.skill || !schema.skillName)
377
+ continue;
378
+ (0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
379
+ console.log(chalk_1.default.green('✓') + ` .skills/${schema.skillName}/SKILL.md`);
380
+ }
381
+ const kimiSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
382
+ if (framework === 'native') {
383
+ (0, scaffold_1.writeFile)(kimiSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
384
+ console.log(chalk_1.default.green('✓') + ' .skills/oprim-spec/SKILL.md');
385
+ }
386
+ else if (fs.existsSync(kimiSpecSkillPath)) {
387
+ fs.unlinkSync(kimiSpecSkillPath);
388
+ try {
389
+ fs.rmdirSync(path.dirname(kimiSpecSkillPath));
390
+ }
391
+ catch { /* not empty or already gone */ }
392
+ console.log(chalk_1.default.dim(' removed .skills/oprim-spec/SKILL.md'));
393
+ }
394
+ const agentsFile = path.join(projectRoot, 'AGENTS.md');
395
+ writeAgentInstructionFile(agentsFile, kimiInstructions());
396
+ console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
397
+ if (kimiDirCreated) {
398
+ console.log(chalk_1.default.dim(' .kimi/ created — Kimi CLI will discover this directory automatically.'));
399
+ }
400
+ if (skillsDirCreated) {
401
+ console.log(chalk_1.default.dim(' .skills/ created — Kimi CLI will discover these files automatically.'));
402
+ }
403
+ }
317
404
  else if (agent === 'codex') {
318
405
  const agentsFile = path.join(projectRoot, 'AGENTS.md');
319
406
  writeAgentInstructionFile(agentsFile, codexInstructions());
@@ -478,6 +565,14 @@ exports.VIBE_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) =>
478
565
  const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
479
566
  return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
480
567
  }));
568
+ exports.QWEN_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
569
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
570
+ return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
571
+ }));
572
+ exports.KIMI_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
573
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
574
+ return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
575
+ }));
481
576
  exports.CURSOR_SKILLS = Object.fromEntries(CURSOR_SKILL_WORKFLOW_IDS.map((id) => {
482
577
  const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
483
578
  return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
@@ -672,3 +767,9 @@ function poolsideInstructions() {
672
767
  function vibeInstructions() {
673
768
  return (0, workflow_renderer_1.renderAgentInstructions)();
674
769
  }
770
+ function qwenInstructions() {
771
+ return (0, workflow_renderer_1.renderAgentInstructions)();
772
+ }
773
+ function kimiInstructions() {
774
+ return (0, workflow_renderer_1.renderAgentInstructions)();
775
+ }
@@ -8,7 +8,7 @@ exports.renderAgentInstructions = renderAgentInstructions;
8
8
  const workflow_schema_1 = require("./workflow-schema");
9
9
  // Fixed order the pre-refactor oprimWorkflowsInline() concatenated its per-workflow sections in —
10
10
  // preserved here so Codex/Gemini/Poolside output is unchanged.
11
- const INLINE_SECTION_ORDER = ['bet', 'note', 'criteria', 'pdr', 'review', 'archive', 'sequence'];
11
+ const INLINE_SECTION_ORDER = ['bet', 'note', 'criteria', 'pdr', 'review', 'archive', 'sequence', 'explore', 'reconcile'];
12
12
  function claudeWrapper(name, description, body) {
13
13
  return `---
14
14
  name: "${name}"
@@ -16,6 +16,12 @@ export interface WorkflowSchema {
16
16
  vibe: {
17
17
  skill: boolean;
18
18
  };
19
+ qwen: {
20
+ skill: boolean;
21
+ };
22
+ kimi: {
23
+ skill: boolean;
24
+ };
19
25
  inline: boolean;
20
26
  variants: string[] | null;
21
27
  }
@@ -68,9 +68,12 @@ function parseSchema(raw, filePath) {
68
68
  const claude = requireField(obj, 'claude', filePath);
69
69
  const cursor = requireField(obj, 'cursor', filePath);
70
70
  const poolside = requireField(obj, 'poolside', filePath);
71
- // vibe is optional and mirrors poolside by default — see mistral-vibe-agent-support spec.
72
- // This keeps existing bundled/forked schemas (none declare a `vibe:` key) working unchanged.
71
+ // vibe/qwen/kimi are optional and mirror poolside by default — see mistral-vibe-agent-support,
72
+ // qwen-code-agent-support, and kimi-cli-agent-support specs. This keeps existing bundled/forked
73
+ // schemas (none declare a `vibe:`/`qwen:`/`kimi:` key) working unchanged.
73
74
  const vibe = obj.vibe ?? poolside;
75
+ const qwen = obj.qwen ?? poolside;
76
+ const kimi = obj.kimi ?? poolside;
74
77
  requireField(obj, 'inline', filePath);
75
78
  return {
76
79
  id: obj.id,
@@ -82,6 +85,8 @@ function parseSchema(raw, filePath) {
82
85
  cursor: { skill: Boolean(cursor.skill), command: cursor.command ?? null },
83
86
  poolside: { skill: Boolean(poolside.skill) },
84
87
  vibe: { skill: Boolean(vibe.skill) },
88
+ qwen: { skill: Boolean(qwen.skill) },
89
+ kimi: { skill: Boolean(kimi.skill) },
85
90
  inline: Boolean(obj.inline),
86
91
  variants: obj.variants ?? null,
87
92
  };
@@ -1 +1 @@
1
- Create a new bet in `oprim/bets/pending/`. 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 `oprim/bets/pending/BET-(\d+)` and `oprim/bets/archived/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/pending/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/pending/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.
1
+ Create a new bet in `oprim/bets/pending/`. 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 `oprim/bets/pending/BET-(\d+)` and `oprim/bets/archived/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/pending/BET-NNN/bet-decision.md` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/pending/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.
@@ -8,6 +8,6 @@ Create a new bet in `oprim/bets/pending/` and register it on the sequencing boar
8
8
  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.
9
9
  5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
10
10
  6. Write `oprim/bets/pending/BET-NNN/bet-decision.md` with all fields.
11
- 7. Append to `oprim/sequence.yaml` backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`.
11
+ 7. Append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`.
12
12
  8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/pending/BET-NNN/discovery.md`.
13
13
  9. Report what was created.
@@ -99,8 +99,8 @@ Prepend the frontmatter block from step 4b, if one was prepared.
99
99
  ### 6. Append to oprim/sequence.yaml backlog
100
100
  Read → parse YAML → append → write back (2-space indentation):
101
101
  ```yaml
102
- - id: BET-NNN
103
- title: "<title>"
102
+ - title: "<title>"
103
+ id: BET-NNN
104
104
  blocked_by: []
105
105
  unlocks: []
106
106
  requires_pdrs: []
@@ -0,0 +1,9 @@
1
+ ### Problem exploration (oprim-explore)
2
+ Investigate a problem or opportunity and compare candidate framings, before any decision artifact is written. Read-only and non-committal — never writes a `bet-decision.md`.
3
+
4
+ 1. Ask for the problem or opportunity (a sentence or two).
5
+ 2. Surface related PDRs: scan `oprim/decisions/PDR-*.md`, extract keywords from the problem statement, list matches by filename/title/body relevance.
6
+ 3. Surface related notes: scan `oprim/notes/NOTE-*.md` the same way.
7
+ 4. Surface related bets: scan `oprim/bets/pending/` and `oprim/bets/archived/` for bet-decisions with similar titles or "Why now" content — flag archived matches explicitly, since they may mean this ground has been covered before.
8
+ 5. Ask for candidate approaches/framings (description + main tradeoff each) and present them side by side without recommending one, unless asked.
9
+ 6. Report what was surfaced and, if the user has converged on a candidate, tell them to run `/oprim:bet` to draft the decision — explore does not create the bet itself.
@@ -0,0 +1,13 @@
1
+ id: explore
2
+ skillName: oprim-explore
3
+ title: "OPRIM: Explore"
4
+ description: Investigate a problem and compare candidate framings before a bet is drafted
5
+ claude:
6
+ skill: true
7
+ command: explore.md
8
+ cursor:
9
+ skill: false
10
+ command: null
11
+ poolside:
12
+ skill: true
13
+ inline: true
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: oprim-explore
3
+ description: Investigate a problem and compare candidate framings before a bet is drafted
4
+ ---
5
+
6
+ Investigate a problem or opportunity and compare candidate framings, before any decision artifact is written. This is oprim's think-first phase — it sits upstream of `/oprim:bet`, not in place of it.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
9
+
10
+ ## What you're doing
11
+
12
+ Explore is read-only and non-committal: it never writes a `bet-decision.md`. Use it when you've noticed a problem or opportunity but haven't yet converged on what to build, or when there are multiple plausible approaches worth weighing before committing to one. Once a candidate framing is worth committing to, hand off to `/oprim:bet` to draft the decision artifact — that boundary is deliberate, so explore doesn't duplicate `oprim-bet`'s job or drift into writing decisions itself.
13
+
14
+ ## Steps
15
+
16
+ ### 1. Get the problem or opportunity
17
+ If not provided, ask: "What problem or opportunity do you want to explore? (a sentence or two)"
18
+
19
+ ### 2. Surface related PDRs
20
+ Scan `oprim/decisions/` for files matching `PDR-*.md`. If the directory is empty or missing, skip this step. Otherwise, extract 3–10 keywords from the problem statement (subject-area nouns, capability names, bet IDs). For each PDR file, read the filename and first 25 lines; a PDR is relevant if any keyword appears in its filename, title, or body (case-insensitive). List matches:
21
+
22
+ **Related product decisions:**
23
+ - PDR-NNN: <title> — <Status> (`oprim/decisions/PDR-NNN-<slug>.md`)
24
+
25
+ If none match, state that plainly and continue.
26
+
27
+ ### 3. Surface related notes
28
+ Scan `oprim/notes/` for files matching `NOTE-*.md`. Using the same keywords from step 2, list notes whose title or body mention them:
29
+
30
+ **Related notes:**
31
+ - NOTE-NNN: <title> (`oprim/notes/NOTE-NNN-<slug>.md`)
32
+
33
+ If `oprim/notes/` is empty or missing, or nothing matches, state that plainly and continue.
34
+
35
+ ### 4. Surface related bets
36
+ Scan `oprim/bets/pending/` and `oprim/bets/archived/` for `bet-decision.md` files whose title or `## Why now` section mentions the keywords from step 2. List matches, noting whether each is pending or archived:
37
+
38
+ **Related bets:**
39
+ - BET-NNN: <title> — <pending/archived> (`oprim/bets/<pending|archived>/BET-NNN.../bet-decision.md`)
40
+
41
+ An archived match with a similar problem statement is worth flagging explicitly — it may mean this ground has been covered before.
42
+
43
+ ### 5. Gather candidate framings
44
+ Ask: "What are the candidate approaches or framings worth comparing? (list as many as you'd like — one is fine if you already have a clear direction)"
45
+
46
+ For each candidate, ask for:
47
+ - A one-line description
48
+ - The main tradeoff or risk (what makes this candidate weaker than the alternatives, or what's uncertain about it)
49
+
50
+ ### 6. Compare candidates
51
+ Present the candidates side by side (name, description, main tradeoff). Do not recommend one over another unless asked — the point of explore is to lay out the comparison clearly, not to decide for the user.
52
+
53
+ ### 7. Report and hand off
54
+ Summarize what was surfaced (PDRs, notes, bets, candidate comparison). If the user has converged on a candidate worth committing to, tell them to run `/oprim:bet` to draft the decision artifact — explore does not create one itself. If no candidate has converged yet, note that explicitly and suggest gathering more information before drafting a bet.
@@ -24,6 +24,6 @@ Promote an atomic note into a bet, or a prioritized bet into a capability spec.
24
24
  2. **Assign the next BET ID** — scan both `oprim/bets/pending/` and `oprim/bets/archived/` for directories matching `BET-(\d+)(-[^/]*)?`, max+1 zero-padded to 3 digits (default 001) — same convention `oprim-bet` uses
25
25
  3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
26
26
  4. **Draft the bet** — write `oprim/bets/pending/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.
27
- 5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`
27
+ 5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`
28
28
  6. **Link back** — add `Bets: BET-NNN` to the note (creating or extending its `## Bets` section)
29
29
  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
@@ -20,6 +20,6 @@ Promote an atomic note into a bet, or a prioritized bet into a capability spec.
20
20
  2. **Assign the next BET ID** — scan both `oprim/bets/pending/` and `oprim/bets/archived/` for directories matching `BET-(\d+)(-[^/]*)?`, max+1 zero-padded to 3 digits (default 001) — same convention `oprim-bet` uses
21
21
  3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
22
22
  4. **Draft the bet** — write `oprim/bets/pending/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.
23
- 5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`
23
+ 5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`
24
24
  6. **Link back** — add `Bets: BET-NNN` to the note (creating or extending its `## Bets` section)
25
25
  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
@@ -35,6 +35,6 @@ Promote an atomic note into a bet, or a prioritized bet into a capability spec.
35
35
  2. **Assign the next BET ID** — scan both `oprim/bets/pending/` and `oprim/bets/archived/` for directories matching `BET-(\d+)(-[^/]*)?`, max+1 zero-padded to 3 digits (default 001) — same convention `oprim-bet` uses
36
36
  3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
37
37
  4. **Draft the bet** — write `oprim/bets/pending/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.
38
- 5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`
38
+ 5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`
39
39
  6. **Link back** — add `Bets: BET-NNN` to the note (creating or extending its `## Bets` section)
40
40
  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
@@ -0,0 +1,8 @@
1
+ ### Artifact reconciliation (oprim-reconcile)
2
+ Detect and fix drift across linked oprim artifacts (PDR ↔ bet, bet ↔ review). Never batch-applies a fix — every proposed change is confirmed individually before being written.
3
+
4
+ 1. Scan every `bet-decision.md` under `oprim/bets/pending/` and `oprim/bets/archived/`: check each `## Links` reference (PDRs, Notes, Spec (delta), OpenSpec change) actually resolves to an existing file/path.
5
+ 2. Scan every `oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md` filename: check the referenced BET-NNN still exists in pending or archived bets.
6
+ 3. If nothing is found, report "No drift detected" and stop.
7
+ 4. Otherwise, list every drift entry, propose a specific fix per entry (e.g. remove a dangling ID from a `## Links` line), and ask "Apply this fix? (y/N)" one item at a time — apply only confirmed fixes.
8
+ 5. Report how many drift entries were found, fixed, and left unresolved.
@@ -0,0 +1,13 @@
1
+ id: reconcile
2
+ skillName: oprim-reconcile
3
+ title: "OPRIM: Reconcile"
4
+ description: Detect and fix drift across linked oprim artifacts (PDR, bet, criteria, review)
5
+ claude:
6
+ skill: true
7
+ command: reconcile.md
8
+ cursor:
9
+ skill: false
10
+ command: null
11
+ poolside:
12
+ skill: true
13
+ inline: true
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: oprim-reconcile
3
+ description: Detect and fix drift across linked oprim artifacts (PDR, bet, criteria, review)
4
+ ---
5
+
6
+ Detect drift across linked oprim artifacts — PDR ↔ bet, bet ↔ criteria, bet ↔ review — and fix it with the user's per-item confirmation. Reconcile never batch-applies a fix; every proposed change is surfaced individually before it's written.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
9
+
10
+ ## What you're doing
11
+
12
+ oprim artifacts reference each other (a bet's `## Links` section names PDRs, notes, and specs; a review's filename names the bet it reviews) but nothing currently checks those references stay valid as artifacts are renamed, moved, or archived. Reconcile is a read-then-confirm-then-write pass over that link graph — it detects drift, proposes a specific fix per item, and only writes a fix the user confirms. This is distinct from `oprim doctor`, which reports sequencing-board and skill-drift issues but never writes a fix itself.
13
+
14
+ ## Steps
15
+
16
+ ### 1. Scan bet-decision `## Links` sections
17
+ For every `bet-decision.md` under `oprim/bets/pending/` and `oprim/bets/archived/`, read its `## Links` section and check each reference:
18
+ - `PDRs: PDR-NNN, ...` — for each ID (skip "None"), verify a matching `oprim/decisions/PDR-NNN-*.md` exists
19
+ - `Notes: NOTE-NNN, ...` — for each ID, verify a matching `oprim/notes/NOTE-NNN-*.md` exists
20
+ - `Spec (delta): <path>` — verify the path exists
21
+ - `OpenSpec change: <path>` — skip if the value is still a placeholder (e.g. "to be filled when promoted" or "None"); otherwise verify the path exists
22
+
23
+ Record a drift entry for every reference that fails to resolve: which bet, which link line, which ID/path is missing.
24
+
25
+ ### 2. Scan review filenames against bets
26
+ For every `oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md` file, extract `BET-NNN` and verify a matching bet directory exists in either `oprim/bets/pending/` or `oprim/bets/archived/`. Record a drift entry for any review whose bet can't be found.
27
+
28
+ ### 3. Scan criteria.yaml placement
29
+ For every `oprim/bets/pending/BET-NNN.../criteria.yaml`, this is only valid while the bet is pending — it's expected to travel with the bet directory on archive, so no separate reference check is needed here. Skip this step; it exists to document why criteria.yaml isn't independently checked.
30
+
31
+ ### 4. Report detected drift
32
+ If no drift was found in steps 1–2, report "No drift detected across PDR/bet/criteria/review links" and stop.
33
+
34
+ Otherwise, list every drift entry found:
35
+
36
+ **Detected drift:**
37
+ - BET-NNN `## Links`: `PDRs` references PDR-XXX, which does not exist at `oprim/decisions/PDR-XXX-*.md`
38
+ - `oprim/reviews/<file>` references BET-NNN, which does not exist in `oprim/bets/pending/` or `oprim/bets/archived/`
39
+
40
+ ### 5. Propose and confirm a fix, one item at a time
41
+ For each drift entry, propose a specific fix:
42
+ - A dangling PDR/Notes ID in a `## Links` line → propose removing that ID from the comma-separated list (or replacing the line with "None" if it was the only entry)
43
+ - A `Spec (delta)` or `OpenSpec change` path that no longer resolves → propose removing that link line entirely
44
+ - A review referencing a bet that no longer exists → propose no automatic fix (the bet may have been renamed rather than deleted); ask the user to identify the correct BET-NNN or confirm the review is orphaned and should be left as-is
45
+
46
+ Ask: "Apply this fix? (y/N)" for each entry individually.
47
+ - If confirmed: apply only that fix to the relevant file, then move to the next drift entry.
48
+ - If declined: leave the artifact unchanged and move to the next drift entry.
49
+
50
+ ### 6. Report what was fixed
51
+ Summarize: how many drift entries were found, how many fixes were applied, how many were declined or left for manual follow-up.
@@ -79,7 +79,7 @@ Ask: "Apply this change? (y/N)"
79
79
 
80
80
  ### 5. Write sequence.yaml
81
81
 
82
- Read `oprim/sequence.yaml`. Remove the bet entry from its current lane. Insert it into the target lane. Write back with 2-space indentation. Do not modify any other entries.
82
+ Read `oprim/sequence.yaml`. Remove the bet entry from its current lane. If the entry's fields are ordered `id:` before `title:`, reorder them to `title:` before `id:` (leave `blocked_by`, `unlocks`, `requires_pdrs` order unchanged). Insert the entry into the target lane. Write back with 2-space indentation. Do not modify any other entries.
83
83
 
84
84
  ### 6. Regenerate view
85
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",