@open-product-primer/cli 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +3 -0
- package/dist/commands/update.js +5 -0
- package/dist/lib/install-agent.js +12 -0
- package/dist/lib/templates.d.ts +1 -0
- package/dist/lib/templates.js +109 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -64,6 +64,7 @@ function initCommand() {
|
|
|
64
64
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
|
|
65
65
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
|
|
66
66
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
|
|
67
|
+
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
67
68
|
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected));
|
|
68
69
|
const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
|
|
69
70
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), templates_1.pdrTemplate);
|
|
@@ -71,6 +72,7 @@ function initCommand() {
|
|
|
71
72
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
|
|
72
73
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), templates_1.kpiReviewTemplate);
|
|
73
74
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
|
|
75
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
74
76
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
|
|
75
77
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
|
|
76
78
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
|
|
@@ -80,6 +82,7 @@ function initCommand() {
|
|
|
80
82
|
console.log(' ' + chalk_1.default.gray('oprim/config.yaml') + ' — ' + configStatus);
|
|
81
83
|
console.log(' ' + chalk_1.default.gray('oprim/sequence.yaml') + ' — ' + sequenceStatus);
|
|
82
84
|
console.log(' ' + chalk_1.default.gray('oprim/templates/') + ' — refreshed');
|
|
85
|
+
console.log(' ' + chalk_1.default.gray('oprim/scripts/') + ' — refreshed');
|
|
83
86
|
// ── Agent selection ───────────────────────────────────────────────────────
|
|
84
87
|
let selectedAgents;
|
|
85
88
|
const flaggedAgents = opts.agent;
|
package/dist/commands/update.js
CHANGED
|
@@ -43,12 +43,17 @@ const fs = __importStar(require("fs"));
|
|
|
43
43
|
const chalk_1 = __importDefault(require("chalk"));
|
|
44
44
|
const install_agent_1 = require("../lib/install-agent");
|
|
45
45
|
const detect_1 = require("../lib/detect");
|
|
46
|
+
const scaffold_1 = require("../lib/scaffold");
|
|
47
|
+
const templates_1 = require("../lib/templates");
|
|
46
48
|
function updateCommand() {
|
|
47
49
|
return new commander_1.Command('update')
|
|
48
50
|
.description('Refresh /oprim:* assistant commands and skills from package templates')
|
|
49
51
|
.action(async () => {
|
|
50
52
|
const projectRoot = process.cwd();
|
|
51
53
|
const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
54
|
+
const primerDir = path.join(projectRoot, 'oprim');
|
|
55
|
+
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
56
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
52
57
|
if (configAgents !== null && configAgents.length > 0) {
|
|
53
58
|
let specFramework = 'openspec';
|
|
54
59
|
let pdrSurfacing = false;
|
|
@@ -851,6 +851,7 @@ Validate the primer sequencing board and suggest rebalancing if needed.
|
|
|
851
851
|
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
|
|
852
852
|
5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
|
|
853
853
|
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
854
|
+
7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
|
|
854
855
|
`;
|
|
855
856
|
}
|
|
856
857
|
// ─── Instruction-file helpers (Codex / Gemini CLI) ────────────────────────────
|
|
@@ -934,6 +935,17 @@ Archive a completed bet.
|
|
|
934
935
|
4. Move directory: \`oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN\`.
|
|
935
936
|
5. Remove the bet entry from \`oprim/sequence.yaml\`.
|
|
936
937
|
6. Report what was done.
|
|
938
|
+
|
|
939
|
+
### Sequencing board (oprim-sequence)
|
|
940
|
+
Validate the primer sequencing board and regenerate the visual view.
|
|
941
|
+
|
|
942
|
+
1. **Read board** — load \`oprim/sequence.yaml\`
|
|
943
|
+
2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
|
|
944
|
+
3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
|
|
945
|
+
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
|
|
946
|
+
5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
|
|
947
|
+
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
948
|
+
7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
|
|
937
949
|
`;
|
|
938
950
|
}
|
|
939
951
|
function codexInstructions() {
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare const pdrTemplate = "# PDR-XXX: <Decision title>\n\n## Status\nPr
|
|
|
4
4
|
export declare const betDecisionTemplate = "# Decision: BET-XXX <Bet title>\n<!-- Naming tip: verb + object [for context] \u2014 e.g. \"Improve bet naming for scannability\" not \"Naming\" -->\n\n## Status\n- Decision: Build now | Defer | Kill\n- Date: YYYY-MM-DD\n- Owner: <name>\n- Review date: YYYY-MM-DD\n\n## Why now\n- <prioritization rationale>\n\n## Alternatives considered\n- <alternative + reason>\n\n## Expected outcomes\n- <metric: baseline -> target in timeframe>\n\n## Kill criteria / rollback trigger\n- <condition and action>\n\n## Links\n- PDRs: <PDR-IDs>\n- OpenSpec change: <path once promoted>\n";
|
|
5
5
|
export declare const criteriaTemplate = "metrics:\n - id: metric_id\n name: \"Metric name\"\n baseline: 0.00\n target: 0.00\n timeframe: \"30 days post-launch\"\n launch_date: \"YYYY-MM-DD\"\n source:\n type: amplitude\n definition:\n event: event_name\n aggregation: unique_users\n denominator_event: null\n segment: null\n";
|
|
6
6
|
export declare const discoveryTemplate = "# Discovery: BET-XXX <Bet title>\n\n## Problem Framing\n- **Problem statement**: <What problem are we solving and for whom?>\n- **Evidence this is real**: <Data, support tickets, user quotes, etc.>\n- **Why it matters**: <Business or user impact if left unsolved>\n\n## User Research Signals\n- **Research conducted**: <Interviews, surveys, usability tests, etc.>\n- **Key findings**: <What did we learn?>\n- **Assumptions to validate**: <What are we still unsure about?>\n\n## Competitive Context\n- **How others solve this**: <Competitor or adjacent solutions>\n- **Our differentiation**: <Why our approach is better or different>\n- **Gaps / opportunities**: <What's underserved in the market?>\n\n## Open Questions\n- [ ] <Question 1 \u2014 what needs to be answered before committing?>\n- [ ] <Question 2>\n- [ ] <Question 3>\n";
|
|
7
|
+
export declare const sequenceViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');\nconst MAX_LABEL_LEN = 40;\n\nfunction parseSequenceYaml(text) {\n const BUCKETS = ['now', 'next', 'later', 'backlog'];\n const result = Object.fromEntries(BUCKETS.map(b => [b, []]));\n let section = null, item = null, listField = null;\n\n for (const raw of text.split('\\n')) {\n const trimmed = raw.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const indent = raw.length - raw.trimStart().length;\n\n if (indent === 0) {\n const m = trimmed.match(/^(\\w+):/);\n if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }\n continue;\n }\n\n if (!section) continue;\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n item = {}; result[section].push(item); listField = null;\n const rest = trimmed.slice(2);\n const m = rest.match(/^(\\w+):\\s*(.*)/);\n if (m) item[m[1]] = m[2].replace(/^['\"]|['\"]$/g, '').trim();\n continue;\n }\n\n if (indent === 4 && item) {\n const m = trimmed.match(/^(\\w+):\\s*(.*)/);\n if (!m) continue;\n const [, key, val] = m;\n const v = val.trim();\n if (v === '[]') { item[key] = []; listField = null; }\n else if (v === '') { item[key] = []; listField = key; }\n else { item[key] = v.replace(/^['\"]|['\"]$/g, ''); listField = null; }\n continue;\n }\n\n if (indent === 6 && item && listField && trimmed.startsWith('- ')) {\n item[listField].push(trimmed.slice(2).trim());\n }\n }\n return result;\n}\n\nfunction nodeId(betId) { return betId.replace(/-/g, ''); }\n\nfunction truncate(title) {\n return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;\n}\n\nfunction generateMermaidBlock(seq) {\n const lines = ['```mermaid', 'graph TD'];\n const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];\n\n for (const [key, label] of ACTIVE) {\n const bets = seq[key] || [];\n if (!bets.length) continue;\n lines.push(' subgraph ' + label);\n for (const bet of bets) {\n lines.push(' ' + nodeId(bet.id) + '[\"' + bet.id + ': ' + truncate(bet.title) + '\"]');\n }\n lines.push(' end');\n }\n\n for (const [key] of ACTIVE) {\n for (const bet of (seq[key] || [])) {\n for (const blocker of (bet.blocked_by || [])) {\n lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));\n }\n }\n }\n\n lines.push('```');\n return lines.join('\\n');\n}\n\nfunction generateBacklogSection(seq) {\n const backlog = seq.backlog || [];\n if (!backlog.length) return '';\n return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');\n}\n\nfunction main() {\n const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));\n const header = [\n '<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',\n '',\n '# Sequencing Board',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');\n console.log('Written: oprim/sequence-view.md');\n}\n\nmain();\n";
|
|
7
8
|
export declare const kpiReviewTemplate = "# KPI Review: BET-XXX\n\n**Review date:** YYYY-MM-DD\n**Reviewed by:** <name>\n\n| Metric | Baseline | Target | Actual | Status |\n|--------|----------|--------|--------|--------|\n| <metric name> | - | - | - | pending |\n\n## Decision quality\n<Did outcomes validate the decision? What would you do differently?>\n\n## Actions\n- [ ] Update bet-decision outcome section\n- [ ] Update affected PDRs\n- [ ] Re-sequence impacted bets\n";
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.kpiReviewTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
3
|
+
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
4
4
|
exports.configTemplate = configTemplate;
|
|
5
5
|
function configTemplate(projectName, openspecEnabled, graphifyEnabled) {
|
|
6
6
|
return `version: 1
|
|
@@ -124,6 +124,114 @@ exports.discoveryTemplate = `# Discovery: BET-XXX <Bet title>
|
|
|
124
124
|
- [ ] <Question 2>
|
|
125
125
|
- [ ] <Question 3>
|
|
126
126
|
`;
|
|
127
|
+
exports.sequenceViewScriptTemplate = `#!/usr/bin/env node
|
|
128
|
+
'use strict';
|
|
129
|
+
|
|
130
|
+
const fs = require('fs');
|
|
131
|
+
const path = require('path');
|
|
132
|
+
|
|
133
|
+
const SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');
|
|
134
|
+
const OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');
|
|
135
|
+
const MAX_LABEL_LEN = 40;
|
|
136
|
+
|
|
137
|
+
function parseSequenceYaml(text) {
|
|
138
|
+
const BUCKETS = ['now', 'next', 'later', 'backlog'];
|
|
139
|
+
const result = Object.fromEntries(BUCKETS.map(b => [b, []]));
|
|
140
|
+
let section = null, item = null, listField = null;
|
|
141
|
+
|
|
142
|
+
for (const raw of text.split('\\n')) {
|
|
143
|
+
const trimmed = raw.trim();
|
|
144
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
145
|
+
const indent = raw.length - raw.trimStart().length;
|
|
146
|
+
|
|
147
|
+
if (indent === 0) {
|
|
148
|
+
const m = trimmed.match(/^(\\w+):/);
|
|
149
|
+
if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!section) continue;
|
|
154
|
+
|
|
155
|
+
if (indent === 2 && trimmed.startsWith('- ')) {
|
|
156
|
+
item = {}; result[section].push(item); listField = null;
|
|
157
|
+
const rest = trimmed.slice(2);
|
|
158
|
+
const m = rest.match(/^(\\w+):\\s*(.*)/);
|
|
159
|
+
if (m) item[m[1]] = m[2].replace(/^['"]|['"]$/g, '').trim();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (indent === 4 && item) {
|
|
164
|
+
const m = trimmed.match(/^(\\w+):\\s*(.*)/);
|
|
165
|
+
if (!m) continue;
|
|
166
|
+
const [, key, val] = m;
|
|
167
|
+
const v = val.trim();
|
|
168
|
+
if (v === '[]') { item[key] = []; listField = null; }
|
|
169
|
+
else if (v === '') { item[key] = []; listField = key; }
|
|
170
|
+
else { item[key] = v.replace(/^['"]|['"]$/g, ''); listField = null; }
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (indent === 6 && item && listField && trimmed.startsWith('- ')) {
|
|
175
|
+
item[listField].push(trimmed.slice(2).trim());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function nodeId(betId) { return betId.replace(/-/g, ''); }
|
|
182
|
+
|
|
183
|
+
function truncate(title) {
|
|
184
|
+
return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function generateMermaidBlock(seq) {
|
|
188
|
+
const lines = ['\`\`\`mermaid', 'graph TD'];
|
|
189
|
+
const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];
|
|
190
|
+
|
|
191
|
+
for (const [key, label] of ACTIVE) {
|
|
192
|
+
const bets = seq[key] || [];
|
|
193
|
+
if (!bets.length) continue;
|
|
194
|
+
lines.push(' subgraph ' + label);
|
|
195
|
+
for (const bet of bets) {
|
|
196
|
+
lines.push(' ' + nodeId(bet.id) + '["' + bet.id + ': ' + truncate(bet.title) + '"]');
|
|
197
|
+
}
|
|
198
|
+
lines.push(' end');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const [key] of ACTIVE) {
|
|
202
|
+
for (const bet of (seq[key] || [])) {
|
|
203
|
+
for (const blocker of (bet.blocked_by || [])) {
|
|
204
|
+
lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push('\`\`\`');
|
|
210
|
+
return lines.join('\\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function generateBacklogSection(seq) {
|
|
214
|
+
const backlog = seq.backlog || [];
|
|
215
|
+
if (!backlog.length) return '';
|
|
216
|
+
return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function main() {
|
|
220
|
+
const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));
|
|
221
|
+
const header = [
|
|
222
|
+
'<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',
|
|
223
|
+
'<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',
|
|
224
|
+
'',
|
|
225
|
+
'# Sequencing Board',
|
|
226
|
+
'',
|
|
227
|
+
'',
|
|
228
|
+
].join('\\n');
|
|
229
|
+
fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');
|
|
230
|
+
console.log('Written: oprim/sequence-view.md');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
main();
|
|
234
|
+
`;
|
|
127
235
|
exports.kpiReviewTemplate = `# KPI Review: BET-XXX
|
|
128
236
|
|
|
129
237
|
**Review date:** YYYY-MM-DD
|