@open-product-primer/cli 0.5.0 → 0.7.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/commands/doctor.js
CHANGED
|
@@ -189,6 +189,8 @@ function doctorCommand() {
|
|
|
189
189
|
for (const entry of betEntries) {
|
|
190
190
|
if (!entry.isDirectory())
|
|
191
191
|
continue;
|
|
192
|
+
if (entry.name === 'archived')
|
|
193
|
+
continue;
|
|
192
194
|
const betDir = path.join(betsDir, entry.name);
|
|
193
195
|
const hasDecision = fs.existsSync(path.join(betDir, 'bet-decision.md'));
|
|
194
196
|
if (!hasDecision)
|
package/dist/commands/init.js
CHANGED
|
@@ -113,12 +113,14 @@ function initCommand() {
|
|
|
113
113
|
}
|
|
114
114
|
else {
|
|
115
115
|
let specFramework = 'openspec';
|
|
116
|
+
let pdrSurfacing = false;
|
|
116
117
|
if (selectedAgents.includes('claude')) {
|
|
117
118
|
specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
119
|
+
pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
118
120
|
}
|
|
119
121
|
console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
|
|
120
122
|
for (const agent of selectedAgents) {
|
|
121
|
-
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework);
|
|
123
|
+
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
|
|
122
124
|
}
|
|
123
125
|
console.log('\n' + chalk_1.default.green('✓') + ` Agent skills installed: ${selectedAgents.join(', ')}`);
|
|
124
126
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -51,11 +51,13 @@ function updateCommand() {
|
|
|
51
51
|
const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
52
52
|
if (configAgents !== null && configAgents.length > 0) {
|
|
53
53
|
let specFramework = 'openspec';
|
|
54
|
+
let pdrSurfacing = false;
|
|
54
55
|
if (configAgents.includes('claude')) {
|
|
55
56
|
specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
57
|
+
pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
56
58
|
}
|
|
57
59
|
for (const agent of configAgents) {
|
|
58
|
-
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework);
|
|
60
|
+
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
|
|
59
61
|
}
|
|
60
62
|
console.log(`\nAgent skills updated: ${configAgents.join(', ')}`);
|
|
61
63
|
}
|
|
@@ -64,7 +66,8 @@ function updateCommand() {
|
|
|
64
66
|
const legacyAgents = [];
|
|
65
67
|
if (fs.existsSync(path.join(projectRoot, '.claude'))) {
|
|
66
68
|
const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
67
|
-
(0, install_agent_1.
|
|
69
|
+
const pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
70
|
+
(0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework, pdrSurfacing);
|
|
68
71
|
legacyAgents.push('claude');
|
|
69
72
|
}
|
|
70
73
|
if (fs.existsSync(path.join(projectRoot, '.cursor'))) {
|
|
@@ -98,12 +101,14 @@ function updateCommand() {
|
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
100
103
|
let addSpecFramework = 'openspec';
|
|
104
|
+
let addPdrSurfacing = false;
|
|
101
105
|
if (selected.includes('claude')) {
|
|
102
106
|
addSpecFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
107
|
+
addPdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
103
108
|
}
|
|
104
109
|
console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
|
|
105
110
|
for (const agent of selected) {
|
|
106
|
-
(0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework);
|
|
111
|
+
(0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework, addPdrSurfacing);
|
|
107
112
|
}
|
|
108
113
|
const merged = Array.from(new Set([...currentAgents, ...selected]));
|
|
109
114
|
(0, detect_1.writeAgentsToConfig)(merged, projectRoot);
|
|
@@ -2,7 +2,9 @@ export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini';
|
|
|
2
2
|
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
4
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
6
|
+
export declare function installAgentSkills(agent: Agent, projectRoot: string, framework?: string, pdrSurfacing?: boolean): void;
|
|
7
|
+
export declare const OPRIM_CONTEXT_SKILL_STEP = "## Step 0: Check relevant product decisions\nInvoke the `oprim:context` skill using the Skill tool. If matching PDRs are surfaced, review them before proceeding. If no PDRs match or `oprim/decisions/` is empty, the skill exits silently \u2014 continue to Step 1 immediately.";
|
|
6
8
|
export declare const CLAUDE_SKILLS: Record<string, string>;
|
|
7
9
|
export declare const CLAUDE_COMMANDS: Record<string, string>;
|
|
8
10
|
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
@@ -36,9 +36,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.SUPPORTED_AGENTS = void 0;
|
|
39
|
+
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
41
|
exports.promptAgentSelection = promptAgentSelection;
|
|
42
|
+
exports.promptPdrSurfacing = promptPdrSurfacing;
|
|
42
43
|
exports.installAgentSkills = installAgentSkills;
|
|
43
44
|
exports.writeAgentInstructionFile = writeAgentInstructionFile;
|
|
44
45
|
exports.codexInstructions = codexInstructions;
|
|
@@ -89,15 +90,48 @@ async function promptAgentSelection(projectRoot) {
|
|
|
89
90
|
],
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
|
-
function
|
|
93
|
+
async function promptPdrSurfacing() {
|
|
94
|
+
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
95
|
+
return confirm({ message: 'Enable proactive PDR surfacing in skills? (y/N)', default: false });
|
|
96
|
+
}
|
|
97
|
+
function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfacing = false) {
|
|
93
98
|
if (agent === 'claude') {
|
|
94
99
|
const claudeDir = path.join(projectRoot, '.claude');
|
|
95
100
|
const dirCreated = !fs.existsSync(claudeDir);
|
|
96
101
|
const skillsBase = path.join(claudeDir, 'skills');
|
|
102
|
+
// oprim:context skill — install when opted in, remove when opted out
|
|
103
|
+
const contextSkillPath = path.join(skillsBase, 'oprim:context', 'SKILL.md');
|
|
104
|
+
if (pdrSurfacing) {
|
|
105
|
+
(0, scaffold_1.writeFile)(contextSkillPath, oprimContextSkill());
|
|
106
|
+
console.log(chalk_1.default.green('✓') + ' .claude/skills/oprim:context/SKILL.md');
|
|
107
|
+
}
|
|
108
|
+
else if (fs.existsSync(contextSkillPath)) {
|
|
109
|
+
fs.unlinkSync(contextSkillPath);
|
|
110
|
+
try {
|
|
111
|
+
fs.rmdirSync(path.dirname(contextSkillPath));
|
|
112
|
+
}
|
|
113
|
+
catch { /* not empty or already gone */ }
|
|
114
|
+
console.log(chalk_1.default.dim(' removed .claude/skills/oprim:context/SKILL.md'));
|
|
115
|
+
}
|
|
116
|
+
// oprim skills — prepend Step 0 when opted in
|
|
97
117
|
for (const [name, content] of Object.entries(exports.CLAUDE_SKILLS)) {
|
|
98
|
-
|
|
118
|
+
const skillContent = pdrSurfacing ? withContextStep(content) : content;
|
|
119
|
+
(0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), skillContent);
|
|
99
120
|
console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
|
|
100
121
|
}
|
|
122
|
+
// openspec skills — add/remove Step 0 in-place when they exist
|
|
123
|
+
for (const name of OPENSPEC_SKILL_NAMES) {
|
|
124
|
+
const skillFilePath = path.join(skillsBase, name, 'SKILL.md');
|
|
125
|
+
if (fs.existsSync(skillFilePath)) {
|
|
126
|
+
const current = fs.readFileSync(skillFilePath, 'utf-8');
|
|
127
|
+
const updated = pdrSurfacing ? addContextStepToFile(current) : removeContextStepFromFile(current);
|
|
128
|
+
if (updated !== current) {
|
|
129
|
+
fs.writeFileSync(skillFilePath, updated, 'utf-8');
|
|
130
|
+
console.log(chalk_1.default.green('✓') +
|
|
131
|
+
` .claude/skills/${name}/SKILL.md (PDR surfacing ${pdrSurfacing ? 'enabled' : 'disabled'})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
101
135
|
const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
|
|
102
136
|
for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
|
|
103
137
|
(0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
|
|
@@ -164,6 +198,73 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec') {
|
|
|
164
198
|
}
|
|
165
199
|
}
|
|
166
200
|
}
|
|
201
|
+
// ─── PDR context surfacing ────────────────────────────────────────────────────
|
|
202
|
+
const CTX_STEP_START = '<!-- oprim:context:start -->';
|
|
203
|
+
const CTX_STEP_END = '<!-- oprim:context:end -->';
|
|
204
|
+
exports.OPRIM_CONTEXT_SKILL_STEP = `## Step 0: Check relevant product decisions
|
|
205
|
+
Invoke the \`oprim:context\` skill using the Skill tool. If matching PDRs are surfaced, review them before proceeding. If no PDRs match or \`oprim/decisions/\` is empty, the skill exits silently — continue to Step 1 immediately.`;
|
|
206
|
+
// Openspec skill names whose files are managed by the openspec CLI and modified in-place by oprim
|
|
207
|
+
const OPENSPEC_SKILL_NAMES = [
|
|
208
|
+
'openspec-propose',
|
|
209
|
+
'openspec-apply-change',
|
|
210
|
+
'openspec-explore',
|
|
211
|
+
'openspec-archive-change',
|
|
212
|
+
];
|
|
213
|
+
// Insert Step 0 into an oprim skill string (written fresh each time — no markers needed)
|
|
214
|
+
function withContextStep(content) {
|
|
215
|
+
const lines = content.split('\n');
|
|
216
|
+
let closingDash = -1;
|
|
217
|
+
let dashCount = 0;
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
if (lines[i].trim() === '---') {
|
|
220
|
+
dashCount++;
|
|
221
|
+
if (dashCount === 2) {
|
|
222
|
+
closingDash = i;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (closingDash === -1)
|
|
228
|
+
return exports.OPRIM_CONTEXT_SKILL_STEP + '\n\n' + content;
|
|
229
|
+
const front = lines.slice(0, closingDash + 1).join('\n');
|
|
230
|
+
const body = lines.slice(closingDash + 1).join('\n').trimStart();
|
|
231
|
+
return `${front}\n\n${exports.OPRIM_CONTEXT_SKILL_STEP}\n\n${body}`;
|
|
232
|
+
}
|
|
233
|
+
// Idempotently add Step 0 to an openspec skill file (read-modify-write)
|
|
234
|
+
function addContextStepToFile(content) {
|
|
235
|
+
if (content.includes(CTX_STEP_START))
|
|
236
|
+
return content; // already present
|
|
237
|
+
const lines = content.split('\n');
|
|
238
|
+
let closingDash = -1;
|
|
239
|
+
let dashCount = 0;
|
|
240
|
+
for (let i = 0; i < lines.length; i++) {
|
|
241
|
+
if (lines[i].trim() === '---') {
|
|
242
|
+
dashCount++;
|
|
243
|
+
if (dashCount === 2) {
|
|
244
|
+
closingDash = i;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const block = `${CTX_STEP_START}\n${exports.OPRIM_CONTEXT_SKILL_STEP}\n${CTX_STEP_END}`;
|
|
250
|
+
if (closingDash === -1)
|
|
251
|
+
return block + '\n\n' + content;
|
|
252
|
+
const front = lines.slice(0, closingDash + 1).join('\n');
|
|
253
|
+
const body = lines.slice(closingDash + 1).join('\n').trimStart();
|
|
254
|
+
return `${front}\n\n${block}\n\n${body}`;
|
|
255
|
+
}
|
|
256
|
+
// Remove Step 0 block from an openspec skill file (read-modify-write)
|
|
257
|
+
function removeContextStepFromFile(content) {
|
|
258
|
+
const s = content.indexOf(CTX_STEP_START);
|
|
259
|
+
if (s === -1)
|
|
260
|
+
return content;
|
|
261
|
+
const e = content.indexOf(CTX_STEP_END, s);
|
|
262
|
+
if (e === -1)
|
|
263
|
+
return content;
|
|
264
|
+
const before = content.slice(0, s).trimEnd();
|
|
265
|
+
const after = content.slice(e + CTX_STEP_END.length).replace(/^\n+/, '\n');
|
|
266
|
+
return before + after;
|
|
267
|
+
}
|
|
167
268
|
// ─── Claude skill playbooks ───────────────────────────────────────────────────
|
|
168
269
|
exports.CLAUDE_SKILLS = {
|
|
169
270
|
'oprim-pdr': pdrSkill(),
|
|
@@ -486,6 +587,36 @@ The bet is preserved in full at the archive location.
|
|
|
486
587
|
\`\`\`
|
|
487
588
|
`;
|
|
488
589
|
}
|
|
590
|
+
function oprimContextSkill() {
|
|
591
|
+
return `---
|
|
592
|
+
name: oprim:context
|
|
593
|
+
description: Surface relevant product decisions from oprim/decisions/ by keyword-matching against the current conversation — invoke at the start of any oprim or openspec workflow when PDR surfacing is enabled
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
Scan \`oprim/decisions/\` and surface PDRs that match keywords from the current conversation.
|
|
597
|
+
|
|
598
|
+
## Steps
|
|
599
|
+
|
|
600
|
+
### 1. Check for decisions
|
|
601
|
+
Scan \`oprim/decisions/\` for files matching \`PDR-*.md\`. If the directory is empty or contains no PDR files, exit silently — produce no output and return immediately.
|
|
602
|
+
|
|
603
|
+
### 2. Extract keywords
|
|
604
|
+
From the current conversation context, extract 3–10 topic keywords: bet IDs referenced (e.g. \`BET-007\`), capability names, filenames mentioned, subject-area nouns. Focus on the most specific and distinctive terms.
|
|
605
|
+
|
|
606
|
+
### 3. Match PDRs
|
|
607
|
+
For each PDR file: read the filename and the first 25 lines (to capture title, status, and context). A PDR is relevant if any keyword appears in the filename, title (\`# PDR-NNN: ...\`), or body text (case-insensitive).
|
|
608
|
+
|
|
609
|
+
### 4. Report or exit silently
|
|
610
|
+
If one or more PDRs match:
|
|
611
|
+
|
|
612
|
+
**Relevant product decisions:**
|
|
613
|
+
- PDR-NNN: <title> — <Status> (\`oprim/decisions/PDR-NNN-<slug>.md\`)
|
|
614
|
+
|
|
615
|
+
List each match on its own line, then return — the invoking skill continues to its next step.
|
|
616
|
+
|
|
617
|
+
If no PDRs match: exit silently — produce no output.
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
489
620
|
function archiveCommandContent() {
|
|
490
621
|
return `Use the Skill tool to invoke the \`oprim-archive\` skill.`;
|
|
491
622
|
}
|
package/dist/lib/measure.js
CHANGED
|
@@ -230,10 +230,12 @@ async function runBigQueryMetric(sqlPath) {
|
|
|
230
230
|
}
|
|
231
231
|
// ─── Criteria scanner (used by doctor) ───────────────────────────────────────
|
|
232
232
|
function scanCriteriaForSourceType(projectRoot, sourceType) {
|
|
233
|
-
const betsDir = path.join(projectRoot, '
|
|
233
|
+
const betsDir = path.join(projectRoot, 'oprim', 'bets');
|
|
234
234
|
if (!fs.existsSync(betsDir))
|
|
235
235
|
return false;
|
|
236
236
|
for (const entry of fs.readdirSync(betsDir)) {
|
|
237
|
+
if (entry === 'archived')
|
|
238
|
+
continue;
|
|
237
239
|
const criteriaPath = path.join(betsDir, entry, 'criteria.yaml');
|
|
238
240
|
if (!fs.existsSync(criteriaPath))
|
|
239
241
|
continue;
|