@open-product-primer/cli 2.7.0 → 2.9.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.
package/dist/cli.js CHANGED
@@ -13,6 +13,9 @@ const migrate_1 = require("./commands/migrate");
13
13
  const measure_1 = require("./commands/measure");
14
14
  const ovw_1 = require("./commands/ovw");
15
15
  const context_1 = require("./commands/context");
16
+ const list_1 = require("./commands/list");
17
+ const show_1 = require("./commands/show");
18
+ const status_1 = require("./commands/status");
16
19
  const package_json_1 = __importDefault(require("../package.json"));
17
20
  const program = new commander_1.Command();
18
21
  program
@@ -27,4 +30,7 @@ program.addCommand((0, migrate_1.migrateCommand)());
27
30
  program.addCommand((0, measure_1.measureCommand)());
28
31
  program.addCommand((0, ovw_1.ovwCommand)());
29
32
  program.addCommand((0, context_1.contextCommand)());
33
+ program.addCommand((0, list_1.listCommand)());
34
+ program.addCommand((0, show_1.showCommand)());
35
+ program.addCommand((0, status_1.statusCommand)());
30
36
  program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function listCommand(): Command;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listCommand = listCommand;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const artifacts_1 = require("../lib/artifacts");
10
+ function printTable(label, rows, columns) {
11
+ console.log(chalk_1.default.bold.cyan(`\n── ${label} ──`));
12
+ if (rows.length === 0) {
13
+ console.log(' (none)');
14
+ return;
15
+ }
16
+ for (const row of rows) {
17
+ console.log(` ${columns(row).join(' ')}`);
18
+ }
19
+ }
20
+ function listCommand() {
21
+ return new commander_1.Command('list')
22
+ .description('Enumerate oprim artifacts (bets, decisions, notes) by type')
23
+ .option('-b, --bets', 'list bets')
24
+ .option('-d, --decisions', 'list decisions (PDRs)')
25
+ .option('-n, --notes', 'list notes')
26
+ .option('--json', 'print machine-readable JSON instead of the human-readable table')
27
+ .action((opts) => {
28
+ const projectRoot = process.cwd();
29
+ const anyTypeRequested = !!(opts.bets || opts.decisions || opts.notes);
30
+ const showBets = anyTypeRequested ? !!opts.bets : true;
31
+ const showDecisions = !!opts.decisions;
32
+ const showNotes = !!opts.notes;
33
+ const bets = showBets ? (0, artifacts_1.listBets)(projectRoot) : undefined;
34
+ const decisions = showDecisions ? (0, artifacts_1.listDecisions)(projectRoot) : undefined;
35
+ const notes = showNotes ? (0, artifacts_1.listNotes)(projectRoot) : undefined;
36
+ if (opts.json) {
37
+ const result = {};
38
+ if (bets)
39
+ result.bets = bets;
40
+ if (decisions)
41
+ result.decisions = decisions;
42
+ if (notes)
43
+ result.notes = notes;
44
+ console.log(JSON.stringify(result));
45
+ return;
46
+ }
47
+ if (bets)
48
+ printTable('BETS', bets, (b) => [b.id, b.status ?? '-', b.title]);
49
+ if (decisions)
50
+ printTable('DECISIONS', decisions, (d) => [d.id, d.status ?? '-', d.title]);
51
+ if (notes)
52
+ printTable('NOTES', notes, (n) => [n.id, n.tags.join(',') || '-', n.title]);
53
+ });
54
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function showCommand(): Command;
@@ -0,0 +1,138 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.showCommand = showCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const spec_delta_1 = require("../lib/spec-delta");
45
+ const artifacts_1 = require("../lib/artifacts");
46
+ function extractLinks(content) {
47
+ const match = content.match(/^##\s*Links\s*\n([\s\S]*?)(?=\n##\s|$)/m);
48
+ return match ? match[1].trim() : null;
49
+ }
50
+ function resolveBet(projectRoot, idInput) {
51
+ for (const sub of ['pending', 'archived']) {
52
+ const betsDir = path.join(projectRoot, 'oprim', 'bets', sub);
53
+ const resolved = (0, spec_delta_1.resolveBetDirectory)(betsDir, idInput);
54
+ if (!resolved)
55
+ continue;
56
+ const decisionPath = path.join(betsDir, resolved, 'bet-decision.md');
57
+ if (!fs.existsSync(decisionPath))
58
+ continue;
59
+ const content = fs.readFileSync(decisionPath, 'utf-8');
60
+ const { title, status } = (0, artifacts_1.parseBetDecision)(content);
61
+ return {
62
+ type: 'bet',
63
+ id: resolved.match(/^BET-\d+/)?.[0] ?? resolved,
64
+ path: path.relative(projectRoot, decisionPath),
65
+ fields: { title, status, links: extractLinks(content) },
66
+ content,
67
+ };
68
+ }
69
+ return null;
70
+ }
71
+ function resolveDecision(projectRoot, idInput) {
72
+ const filePath = (0, artifacts_1.resolvePdrPath)(projectRoot, idInput);
73
+ if (!filePath)
74
+ return null;
75
+ const content = fs.readFileSync(filePath, 'utf-8');
76
+ const { id, title, status } = (0, artifacts_1.parsePdrFile)(content);
77
+ if (!id)
78
+ return null;
79
+ return {
80
+ type: 'decision',
81
+ id,
82
+ path: path.relative(projectRoot, filePath),
83
+ fields: { title, status },
84
+ content,
85
+ };
86
+ }
87
+ function resolveNote(projectRoot, idInput) {
88
+ const filePath = (0, artifacts_1.resolveNotePath)(projectRoot, idInput);
89
+ if (!filePath)
90
+ return null;
91
+ const content = fs.readFileSync(filePath, 'utf-8');
92
+ const { title, tags } = (0, artifacts_1.parseNoteFrontmatter)(content);
93
+ const id = path.basename(filePath).match(/^(NOTE-\d+)/)?.[1] ?? idInput;
94
+ return {
95
+ type: 'note',
96
+ id,
97
+ path: path.relative(projectRoot, filePath),
98
+ fields: { title, tags },
99
+ content,
100
+ };
101
+ }
102
+ function resolveArtifact(projectRoot, idInput) {
103
+ const normalized = idInput.trim().toUpperCase();
104
+ if (normalized.startsWith('BET'))
105
+ return resolveBet(projectRoot, idInput);
106
+ if (normalized.startsWith('PDR'))
107
+ return resolveDecision(projectRoot, idInput);
108
+ if (normalized.startsWith('NOTE'))
109
+ return resolveNote(projectRoot, idInput);
110
+ return null;
111
+ }
112
+ function showCommand() {
113
+ return new commander_1.Command('show')
114
+ .description('Show a single oprim artifact (bet, decision, or note) resolved by ID')
115
+ .argument('<id>', 'artifact ID, e.g. BET-030, PDR-005, or NOTE-012')
116
+ .option('--json', 'print machine-readable JSON instead of the human-readable view')
117
+ .action((idInput, opts) => {
118
+ const projectRoot = process.cwd();
119
+ const result = resolveArtifact(projectRoot, idInput);
120
+ if (!result) {
121
+ console.error(`${idInput} was not found — checked bets, decisions, and notes.`);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ if (opts.json) {
126
+ console.log(JSON.stringify({ type: result.type, id: result.id, path: result.path, ...result.fields, content: result.content }));
127
+ return;
128
+ }
129
+ console.log(chalk_1.default.bold(`${result.id}`) + chalk_1.default.dim(` (${result.type}) ${result.path}`));
130
+ for (const [key, value] of Object.entries(result.fields)) {
131
+ if (value === null || value === undefined || value === '')
132
+ continue;
133
+ console.log(chalk_1.default.dim(`${key}:`) + ` ${Array.isArray(value) ? value.join(', ') : value}`);
134
+ }
135
+ console.log('');
136
+ console.log(result.content);
137
+ });
138
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function statusCommand(): Command;
@@ -0,0 +1,95 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.statusCommand = statusCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const yaml = __importStar(require("js-yaml"));
45
+ function statusCommand() {
46
+ return new commander_1.Command('status')
47
+ .description('Show sequencing board state (now/next/later/backlog) and WIP limit usage')
48
+ .option('--json', 'print machine-readable JSON instead of the human-readable report')
49
+ .action((opts) => {
50
+ const projectRoot = process.cwd();
51
+ const sequencePath = path.join(projectRoot, 'oprim', 'sequence.yaml');
52
+ if (!fs.existsSync(sequencePath)) {
53
+ console.error("No oprim/sequence.yaml found — run 'oprim init' first");
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ let board;
58
+ try {
59
+ board = yaml.load(fs.readFileSync(sequencePath, 'utf-8')) ?? {};
60
+ }
61
+ catch {
62
+ console.error('Failed to parse oprim/sequence.yaml');
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const now = board.now ?? [];
67
+ const next = board.next ?? [];
68
+ const later = board.later ?? [];
69
+ const backlog = board.backlog ?? [];
70
+ if (opts.json) {
71
+ console.log(JSON.stringify({ wip_limits: board.wip_limits ?? {}, now, next, later, backlog }));
72
+ return;
73
+ }
74
+ console.log(chalk_1.default.bold('oprim status') + '\n');
75
+ const wipLimit = board.wip_limits?.now;
76
+ const wipLine = wipLimit !== undefined ? `${now.length}/${wipLimit}` : `${now.length}`;
77
+ console.log(chalk_1.default.dim(`WIP (now): ${wipLine}`));
78
+ const lanes = [
79
+ ['now', now],
80
+ ['next', next],
81
+ ['later', later],
82
+ ['backlog', backlog],
83
+ ];
84
+ for (const [label, bets] of lanes) {
85
+ console.log(chalk_1.default.bold.cyan(`\n── ${label.toUpperCase()} ──`));
86
+ if (bets.length === 0) {
87
+ console.log(' (empty)');
88
+ continue;
89
+ }
90
+ for (const bet of bets) {
91
+ console.log(` ${bet.id} ${bet.title}`);
92
+ }
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,44 @@
1
+ export interface BetSummary {
2
+ id: string;
3
+ title: string;
4
+ status: string | null;
5
+ path: string;
6
+ }
7
+ export interface DecisionSummary {
8
+ id: string;
9
+ title: string;
10
+ status: string | null;
11
+ path: string;
12
+ }
13
+ export interface NoteSummary {
14
+ id: string;
15
+ title: string;
16
+ tags: string[];
17
+ path: string;
18
+ }
19
+ /** Parses a bet-decision.md body for its title and `- Decision: <status>` line. */
20
+ export declare function parseBetDecision(content: string): {
21
+ title: string;
22
+ status: string | null;
23
+ };
24
+ /** Enumerates bets under oprim/bets/pending/ and oprim/bets/archived/. */
25
+ export declare function listBets(projectRoot: string): BetSummary[];
26
+ /** Parses a PDR file for its id, title, and status (ported from decisionsViewScriptTemplate). */
27
+ export declare function parsePdrFile(content: string): {
28
+ id: string | null;
29
+ title: string;
30
+ status: string | null;
31
+ };
32
+ /** Enumerates decisions (PDRs) under oprim/decisions/. */
33
+ export declare function listDecisions(projectRoot: string): DecisionSummary[];
34
+ /** Parses a note's frontmatter (title, tags) — falls back to empty values if frontmatter is absent/unparsable. */
35
+ export declare function parseNoteFrontmatter(content: string): {
36
+ title: string;
37
+ tags: string[];
38
+ };
39
+ /** Enumerates notes under oprim/notes/. */
40
+ export declare function listNotes(projectRoot: string): NoteSummary[];
41
+ /** Resolves a PDR ID (accepting `pdr-005`, `005`, `5`, or `PDR-005`) to its file path under oprim/decisions/. */
42
+ export declare function resolvePdrPath(projectRoot: string, idInput: string): string | null;
43
+ /** Resolves a note ID (accepting `note-005`, `005`, `5`, or `NOTE-005`) to its file path under oprim/notes/. */
44
+ export declare function resolveNotePath(projectRoot: string, idInput: string): string | null;
@@ -0,0 +1,163 @@
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.parseBetDecision = parseBetDecision;
37
+ exports.listBets = listBets;
38
+ exports.parsePdrFile = parsePdrFile;
39
+ exports.listDecisions = listDecisions;
40
+ exports.parseNoteFrontmatter = parseNoteFrontmatter;
41
+ exports.listNotes = listNotes;
42
+ exports.resolvePdrPath = resolvePdrPath;
43
+ exports.resolveNotePath = resolveNotePath;
44
+ const path = __importStar(require("path"));
45
+ const fs = __importStar(require("fs"));
46
+ const yaml = __importStar(require("js-yaml"));
47
+ function extractBetId(dirName) {
48
+ return dirName.match(/^BET-\d+/)?.[0] ?? dirName;
49
+ }
50
+ /** Parses a bet-decision.md body for its title and `- Decision: <status>` line. */
51
+ function parseBetDecision(content) {
52
+ const titleMatch = content.match(/^#\s*Decision:\s*BET-\d+\s+(.*)$/m);
53
+ const statusMatch = content.match(/^-\s*Decision:\s*(.+)$/m);
54
+ return {
55
+ title: titleMatch ? titleMatch[1].trim() : '',
56
+ status: statusMatch ? statusMatch[1].trim() : null,
57
+ };
58
+ }
59
+ /** Enumerates bets under oprim/bets/pending/ and oprim/bets/archived/. */
60
+ function listBets(projectRoot) {
61
+ const results = [];
62
+ for (const sub of ['pending', 'archived']) {
63
+ const dir = path.join(projectRoot, 'oprim', 'bets', sub);
64
+ if (!fs.existsSync(dir))
65
+ continue;
66
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory());
67
+ for (const entry of entries) {
68
+ const decisionPath = path.join(dir, entry.name, 'bet-decision.md');
69
+ if (!fs.existsSync(decisionPath))
70
+ continue;
71
+ const content = fs.readFileSync(decisionPath, 'utf-8');
72
+ const { title, status } = parseBetDecision(content);
73
+ results.push({
74
+ id: extractBetId(entry.name),
75
+ title,
76
+ status,
77
+ path: path.relative(projectRoot, decisionPath),
78
+ });
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+ /** Parses a PDR file for its id, title, and status (ported from decisionsViewScriptTemplate). */
84
+ function parsePdrFile(content) {
85
+ const idMatch = content.match(/^#\s*(PDR-\d+)[:\s]*(.*)$/m);
86
+ const id = idMatch ? idMatch[1] : null;
87
+ const title = idMatch ? idMatch[2].trim() : '';
88
+ const statusMatch = content.match(/^##\s*Status\s*\n(.+)$/m) ?? content.match(/^Status:\s*(.+)$/m);
89
+ const status = statusMatch ? statusMatch[1].trim() : null;
90
+ return { id, title, status };
91
+ }
92
+ /** Enumerates decisions (PDRs) under oprim/decisions/. */
93
+ function listDecisions(projectRoot) {
94
+ const dir = path.join(projectRoot, 'oprim', 'decisions');
95
+ if (!fs.existsSync(dir))
96
+ return [];
97
+ const results = [];
98
+ const files = fs.readdirSync(dir).filter((f) => /^PDR-\d+.*\.md$/.test(f));
99
+ for (const file of files) {
100
+ const filePath = path.join(dir, file);
101
+ const content = fs.readFileSync(filePath, 'utf-8');
102
+ const { id, title, status } = parsePdrFile(content);
103
+ if (!id)
104
+ continue;
105
+ results.push({ id, title, status, path: path.relative(projectRoot, filePath) });
106
+ }
107
+ return results;
108
+ }
109
+ /** Parses a note's frontmatter (title, tags) — falls back to empty values if frontmatter is absent/unparsable. */
110
+ function parseNoteFrontmatter(content) {
111
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
112
+ if (!fmMatch)
113
+ return { title: '', tags: [] };
114
+ try {
115
+ const fm = (yaml.load(fmMatch[1]) ?? {});
116
+ return { title: fm.title ?? '', tags: fm.tags ?? [] };
117
+ }
118
+ catch {
119
+ return { title: '', tags: [] };
120
+ }
121
+ }
122
+ /** Enumerates notes under oprim/notes/. */
123
+ function listNotes(projectRoot) {
124
+ const dir = path.join(projectRoot, 'oprim', 'notes');
125
+ if (!fs.existsSync(dir))
126
+ return [];
127
+ const results = [];
128
+ const files = fs.readdirSync(dir).filter((f) => /^NOTE-\d+.*\.md$/.test(f));
129
+ for (const file of files) {
130
+ const idMatch = file.match(/^(NOTE-\d+)/);
131
+ if (!idMatch)
132
+ continue;
133
+ const filePath = path.join(dir, file);
134
+ const content = fs.readFileSync(filePath, 'utf-8');
135
+ const { title, tags } = parseNoteFrontmatter(content);
136
+ results.push({ id: idMatch[1], title, tags, path: path.relative(projectRoot, filePath) });
137
+ }
138
+ return results;
139
+ }
140
+ function normalizeArtifactId(prefix, input) {
141
+ const digits = input.replace(/[^0-9]/g, '');
142
+ return `${prefix}-${digits.padStart(3, '0')}`;
143
+ }
144
+ function resolveByPrefix(dir, id) {
145
+ if (!fs.existsSync(dir))
146
+ return null;
147
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
148
+ const exact = files.find((f) => f === `${id}.md`);
149
+ if (exact)
150
+ return path.join(dir, exact);
151
+ const slugMatches = files.filter((f) => f.startsWith(`${id}-`));
152
+ if (slugMatches.length === 1)
153
+ return path.join(dir, slugMatches[0]);
154
+ return null;
155
+ }
156
+ /** Resolves a PDR ID (accepting `pdr-005`, `005`, `5`, or `PDR-005`) to its file path under oprim/decisions/. */
157
+ function resolvePdrPath(projectRoot, idInput) {
158
+ return resolveByPrefix(path.join(projectRoot, 'oprim', 'decisions'), normalizeArtifactId('PDR', idInput));
159
+ }
160
+ /** Resolves a note ID (accepting `note-005`, `005`, `5`, or `NOTE-005`) to its file path under oprim/notes/. */
161
+ function resolveNotePath(projectRoot, idInput) {
162
+ return resolveByPrefix(path.join(projectRoot, 'oprim', 'notes'), normalizeArtifactId('NOTE', idInput));
163
+ }
@@ -155,8 +155,8 @@ async function promptOkfFrontmatter() {
155
155
  }
156
156
  // Workflow ids installed as Claude/Poolside/Cursor skill files — see packages/cli/src/workflows/.
157
157
  // Order matches the pre-refactor CLAUDE_SKILLS/POOLSIDE_SKILLS/CURSOR_SKILLS declaration order.
158
- const CLAUDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'context'];
159
- 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'];
160
160
  const CURSOR_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review'];
161
161
  // Claude command wrappers (thin, invoke skill) — filename -> workflow id. Order matches the
162
162
  // pre-refactor CLAUDE_COMMANDS declaration order.
@@ -165,6 +165,8 @@ const CLAUDE_COMMAND_WORKFLOWS = [
165
165
  { filename: 'sequence.md', id: 'sequence' },
166
166
  { filename: 'archive.md', id: 'archive' },
167
167
  { filename: 'context-init.md', id: 'context' },
168
+ { filename: 'explore.md', id: 'explore' },
169
+ { filename: 'reconcile.md', id: 'reconcile' },
168
170
  ];
169
171
  // Cursor command files (full inline). Order matches the pre-refactor CURSOR_COMMANDS declaration order.
170
172
  const CURSOR_COMMAND_WORKFLOW_IDS = ['promote', 'sequence', 'pdr', 'bet', 'note', 'criteria', 'review'];
@@ -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}"
@@ -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.
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",