@open-product-primer/cli 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +2 -3
- package/dist/commands/update.js +29 -2
- package/dist/lib/config-merge.d.ts +10 -0
- package/dist/lib/config-merge.js +53 -0
- package/dist/lib/install-agent.d.ts +2 -0
- package/dist/lib/install-agent.js +254 -44
- package/dist/lib/integrity.js +2 -1
- package/dist/lib/templates.d.ts +1 -1
- package/dist/lib/templates.js +6 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -61,6 +61,7 @@ function initCommand() {
|
|
|
61
61
|
console.log(chalk_1.default.green('✓') + ' Graphify detected');
|
|
62
62
|
console.log('');
|
|
63
63
|
const okfEnabled = await (0, install_agent_1.promptOkfFrontmatter)();
|
|
64
|
+
const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
64
65
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
65
66
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
|
|
66
67
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
|
|
@@ -68,7 +69,7 @@ function initCommand() {
|
|
|
68
69
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
|
|
69
70
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
|
|
70
71
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
71
|
-
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
|
|
72
|
+
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled, specFramework));
|
|
72
73
|
const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
|
|
73
74
|
const pdrContent = okfEnabled ? (0, templates_1.okfFrontmatter)('pdr', '<Decision title>') + templates_1.pdrTemplate : templates_1.pdrTemplate;
|
|
74
75
|
const betContent = okfEnabled
|
|
@@ -136,10 +137,8 @@ function initCommand() {
|
|
|
136
137
|
' after configuring an AI tool to install /oprim:* skills.');
|
|
137
138
|
}
|
|
138
139
|
else {
|
|
139
|
-
let specFramework = 'openspec';
|
|
140
140
|
let pdrSurfacing = false;
|
|
141
141
|
if (selectedAgents.includes('claude')) {
|
|
142
|
-
specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
143
142
|
pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
144
143
|
}
|
|
145
144
|
console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
|
package/dist/commands/update.js
CHANGED
|
@@ -45,6 +45,19 @@ const install_agent_1 = require("../lib/install-agent");
|
|
|
45
45
|
const detect_1 = require("../lib/detect");
|
|
46
46
|
const scaffold_1 = require("../lib/scaffold");
|
|
47
47
|
const templates_1 = require("../lib/templates");
|
|
48
|
+
const config_merge_1 = require("../lib/config-merge");
|
|
49
|
+
// bet-023 — persist the resolved spec_framework into oprim/config.yaml, inserting it only
|
|
50
|
+
// when the key is missing (never overwrites an already-persisted choice).
|
|
51
|
+
function persistSpecFramework(configPath, framework) {
|
|
52
|
+
if (!fs.existsSync(configPath))
|
|
53
|
+
return;
|
|
54
|
+
const existing = fs.readFileSync(configPath, 'utf-8');
|
|
55
|
+
const { content, changed } = (0, config_merge_1.mergeSpecFramework)(existing, framework);
|
|
56
|
+
if (changed) {
|
|
57
|
+
fs.writeFileSync(configPath, content, 'utf-8');
|
|
58
|
+
console.log(chalk_1.default.green('✓') + ` oprim/config.yaml — integrations.spec_framework set to ${framework}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
48
61
|
function updateCommand() {
|
|
49
62
|
return new commander_1.Command('update')
|
|
50
63
|
.description('Refresh /oprim:* assistant commands and skills from package templates')
|
|
@@ -58,13 +71,23 @@ function updateCommand() {
|
|
|
58
71
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
59
72
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
60
73
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
74
|
+
const configPath = path.join(primerDir, 'config.yaml');
|
|
75
|
+
if (fs.existsSync(configPath)) {
|
|
76
|
+
const existingConfig = fs.readFileSync(configPath, 'utf-8');
|
|
77
|
+
const { content: mergedConfig, changed } = (0, config_merge_1.mergeConfigSchema)(existingConfig);
|
|
78
|
+
if (changed) {
|
|
79
|
+
fs.writeFileSync(configPath, mergedConfig, 'utf-8');
|
|
80
|
+
console.log(chalk_1.default.green('✓') + ' oprim/config.yaml — schema updated with new keys (existing values preserved)');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
61
83
|
if (configAgents !== null && configAgents.length > 0) {
|
|
62
|
-
let specFramework =
|
|
84
|
+
let specFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
|
|
63
85
|
let pdrSurfacing = false;
|
|
64
86
|
if (configAgents.includes('claude')) {
|
|
65
87
|
specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
66
88
|
pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
67
89
|
}
|
|
90
|
+
persistSpecFramework(configPath, specFramework);
|
|
68
91
|
for (const agent of configAgents) {
|
|
69
92
|
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
|
|
70
93
|
}
|
|
@@ -75,6 +98,7 @@ function updateCommand() {
|
|
|
75
98
|
const legacyAgents = [];
|
|
76
99
|
if (fs.existsSync(path.join(projectRoot, '.claude'))) {
|
|
77
100
|
const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
101
|
+
persistSpecFramework(configPath, specFramework);
|
|
78
102
|
const pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
79
103
|
(0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework, pdrSurfacing);
|
|
80
104
|
legacyAgents.push('claude');
|
|
@@ -99,22 +123,25 @@ function updateCommand() {
|
|
|
99
123
|
default: false,
|
|
100
124
|
});
|
|
101
125
|
if (!addMore) {
|
|
126
|
+
persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
|
|
102
127
|
console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
|
|
103
128
|
return;
|
|
104
129
|
}
|
|
105
130
|
console.log('');
|
|
106
131
|
const selected = await (0, install_agent_1.promptAgentSelection)(projectRoot);
|
|
107
132
|
if (selected.length === 0) {
|
|
133
|
+
persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
|
|
108
134
|
console.log('\n' + chalk_1.default.yellow('No agents selected.'));
|
|
109
135
|
console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
|
|
110
136
|
return;
|
|
111
137
|
}
|
|
112
|
-
let addSpecFramework =
|
|
138
|
+
let addSpecFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
|
|
113
139
|
let addPdrSurfacing = false;
|
|
114
140
|
if (selected.includes('claude')) {
|
|
115
141
|
addSpecFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
116
142
|
addPdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
117
143
|
}
|
|
144
|
+
persistSpecFramework(configPath, addSpecFramework);
|
|
118
145
|
console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
|
|
119
146
|
for (const agent of selected) {
|
|
120
147
|
(0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework, addPdrSurfacing);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function mergeConfigSchema(existingContent: string): {
|
|
2
|
+
content: string;
|
|
3
|
+
changed: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function readSpecFramework(content: string): string | null;
|
|
6
|
+
export declare function deriveDefaultSpecFramework(content: string): string;
|
|
7
|
+
export declare function mergeSpecFramework(existingContent: string, framework: string): {
|
|
8
|
+
content: string;
|
|
9
|
+
changed: boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeConfigSchema = mergeConfigSchema;
|
|
4
|
+
exports.readSpecFramework = readSpecFramework;
|
|
5
|
+
exports.deriveDefaultSpecFramework = deriveDefaultSpecFramework;
|
|
6
|
+
exports.mergeSpecFramework = mergeSpecFramework;
|
|
7
|
+
const CONFIG_SCHEMA_FIELDS = [
|
|
8
|
+
{ key: 'context', block: 'context: ""\n' },
|
|
9
|
+
{ key: 'rules', block: 'rules: {}\n' },
|
|
10
|
+
{ key: 'store', block: 'store:\n enabled: false\n' },
|
|
11
|
+
];
|
|
12
|
+
function existingTopLevelKeys(content) {
|
|
13
|
+
const keys = new Set();
|
|
14
|
+
for (const line of content.split('\n')) {
|
|
15
|
+
const match = line.match(/^([A-Za-z_][\w-]*):/);
|
|
16
|
+
if (match)
|
|
17
|
+
keys.add(match[1]);
|
|
18
|
+
}
|
|
19
|
+
return keys;
|
|
20
|
+
}
|
|
21
|
+
function mergeConfigSchema(existingContent) {
|
|
22
|
+
const present = existingTopLevelKeys(existingContent);
|
|
23
|
+
const missing = CONFIG_SCHEMA_FIELDS.filter((field) => !present.has(field.key));
|
|
24
|
+
if (missing.length === 0)
|
|
25
|
+
return { content: existingContent, changed: false };
|
|
26
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
27
|
+
const additions = missing.map((field) => field.block).join('');
|
|
28
|
+
return { content: existingContent + separator + additions, changed: true };
|
|
29
|
+
}
|
|
30
|
+
// bet-023 — integrations.spec_framework is nested under `integrations:` rather than a
|
|
31
|
+
// top-level key, and its default depends on the project's existing openspec state, so it
|
|
32
|
+
// can't reuse the static CONFIG_SCHEMA_FIELDS table above. Handled as its own one-off per
|
|
33
|
+
// the bet's design (falls back to a minimal merge rather than depending on the generic
|
|
34
|
+
// top-level mechanism, which doesn't support nested keys or computed defaults).
|
|
35
|
+
function readSpecFramework(content) {
|
|
36
|
+
const match = content.match(/^\s*spec_framework:\s*(\S+)/m);
|
|
37
|
+
return match ? match[1] : null;
|
|
38
|
+
}
|
|
39
|
+
function deriveDefaultSpecFramework(content) {
|
|
40
|
+
return /^\s*openspec:\n\s*enabled:\s*true/m.test(content) ? 'openspec' : 'none';
|
|
41
|
+
}
|
|
42
|
+
function mergeSpecFramework(existingContent, framework) {
|
|
43
|
+
if (readSpecFramework(existingContent))
|
|
44
|
+
return { content: existingContent, changed: false };
|
|
45
|
+
const lines = existingContent.split('\n');
|
|
46
|
+
const integrationsIndex = lines.findIndex((line) => line === 'integrations:');
|
|
47
|
+
if (integrationsIndex === -1) {
|
|
48
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
49
|
+
return { content: existingContent + separator + `integrations:\n spec_framework: ${framework}\n`, changed: true };
|
|
50
|
+
}
|
|
51
|
+
lines.splice(integrationsIndex + 1, 0, ` spec_framework: ${framework}`);
|
|
52
|
+
return { content: lines.join('\n'), changed: true };
|
|
53
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside';
|
|
2
2
|
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
|
+
export declare function resolveSpecFramework(projectRoot: string): string;
|
|
4
5
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
5
6
|
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
6
7
|
export declare function promptOkfFrontmatter(): Promise<boolean>;
|
|
@@ -11,6 +12,7 @@ export declare const CLAUDE_COMMANDS: Record<string, string>;
|
|
|
11
12
|
export declare const POOLSIDE_SKILLS: Record<string, string>;
|
|
12
13
|
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
13
14
|
export declare const CURSOR_COMMANDS: Record<string, string>;
|
|
15
|
+
export declare function specAuthoringSkill(): string;
|
|
14
16
|
export declare function writeAgentInstructionFile(filePath: string, section: string): void;
|
|
15
17
|
export declare function codexInstructions(): string;
|
|
16
18
|
export declare function geminiInstructions(): string;
|
|
@@ -38,10 +38,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
|
+
exports.resolveSpecFramework = resolveSpecFramework;
|
|
41
42
|
exports.promptAgentSelection = promptAgentSelection;
|
|
42
43
|
exports.promptPdrSurfacing = promptPdrSurfacing;
|
|
43
44
|
exports.promptOkfFrontmatter = promptOkfFrontmatter;
|
|
44
45
|
exports.installAgentSkills = installAgentSkills;
|
|
46
|
+
exports.specAuthoringSkill = specAuthoringSkill;
|
|
45
47
|
exports.writeAgentInstructionFile = writeAgentInstructionFile;
|
|
46
48
|
exports.codexInstructions = codexInstructions;
|
|
47
49
|
exports.geminiInstructions = geminiInstructions;
|
|
@@ -51,30 +53,60 @@ const fs = __importStar(require("fs"));
|
|
|
51
53
|
const chalk_1 = __importDefault(require("chalk"));
|
|
52
54
|
const scaffold_1 = require("./scaffold");
|
|
53
55
|
const detect_1 = require("./detect");
|
|
56
|
+
const config_merge_1 = require("./config-merge");
|
|
54
57
|
exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'poolside'];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
// oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
|
|
59
|
+
// selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
|
|
60
|
+
// projects that installed before that key existed.
|
|
61
|
+
function readPersistedFramework(projectRoot) {
|
|
62
|
+
const configYamlPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
63
|
+
if (fs.existsSync(configYamlPath)) {
|
|
64
|
+
const persisted = (0, config_merge_1.readSpecFramework)(fs.readFileSync(configYamlPath, 'utf-8'));
|
|
65
|
+
if (persisted)
|
|
66
|
+
return persisted;
|
|
67
|
+
}
|
|
68
|
+
const hooksConfigPath = path.join(projectRoot, '.claude', 'hooks', 'config.json');
|
|
69
|
+
if (fs.existsSync(hooksConfigPath)) {
|
|
58
70
|
try {
|
|
59
|
-
const existing = JSON.parse(fs.readFileSync(
|
|
60
|
-
if (typeof existing.framework === 'string')
|
|
61
|
-
console.log(chalk_1.default.dim(` Speccing framework: ${existing.framework} (from config)`));
|
|
71
|
+
const existing = JSON.parse(fs.readFileSync(hooksConfigPath, 'utf-8'));
|
|
72
|
+
if (typeof existing.framework === 'string')
|
|
62
73
|
return existing.framework;
|
|
63
|
-
}
|
|
64
74
|
}
|
|
65
75
|
catch {
|
|
66
|
-
// fallthrough
|
|
76
|
+
// fallthrough
|
|
67
77
|
}
|
|
68
78
|
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function promptFrameworkSelection(projectRoot) {
|
|
82
|
+
const persisted = readPersistedFramework(projectRoot);
|
|
83
|
+
if (persisted) {
|
|
84
|
+
console.log(chalk_1.default.dim(` Speccing framework: ${persisted} (from config)`));
|
|
85
|
+
return persisted;
|
|
86
|
+
}
|
|
69
87
|
const { select } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
70
88
|
return select({
|
|
71
89
|
message: 'Which speccing framework does this project use?',
|
|
72
90
|
choices: [
|
|
73
91
|
{ name: 'OpenSpec (recommended)', value: 'openspec' },
|
|
92
|
+
{ name: 'Native (oprim-authored specs, no OpenSpec required)', value: 'native' },
|
|
74
93
|
{ name: 'None', value: 'none' },
|
|
75
94
|
],
|
|
76
95
|
});
|
|
77
96
|
}
|
|
97
|
+
// No-prompt resolution used where an interactive choice isn't appropriate (e.g. non-Claude
|
|
98
|
+
// agent branches): the persisted value if one exists, else a default derived from the
|
|
99
|
+
// project's existing openspec state.
|
|
100
|
+
function resolveSpecFramework(projectRoot) {
|
|
101
|
+
const persisted = readPersistedFramework(projectRoot);
|
|
102
|
+
if (persisted)
|
|
103
|
+
return persisted;
|
|
104
|
+
const configYamlPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
105
|
+
if (fs.existsSync(configYamlPath)) {
|
|
106
|
+
return (0, config_merge_1.deriveDefaultSpecFramework)(fs.readFileSync(configYamlPath, 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
return 'none';
|
|
109
|
+
}
|
|
78
110
|
async function promptAgentSelection(projectRoot) {
|
|
79
111
|
const detected = (0, detect_1.detectAvailableAgents)(projectRoot);
|
|
80
112
|
if (detected.length > 0) {
|
|
@@ -129,6 +161,21 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
|
|
|
129
161
|
(0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), skillContent);
|
|
130
162
|
console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
|
|
131
163
|
}
|
|
164
|
+
// oprim-spec (native spec authoring) — install only when spec_framework is native, remove otherwise
|
|
165
|
+
const specSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
|
|
166
|
+
if (framework === 'native') {
|
|
167
|
+
const specSkillContent = pdrSurfacing ? withContextStep(specAuthoringSkill()) : specAuthoringSkill();
|
|
168
|
+
(0, scaffold_1.writeFile)(specSkillPath, specSkillContent);
|
|
169
|
+
console.log(chalk_1.default.green('✓') + ' .claude/skills/oprim-spec/SKILL.md');
|
|
170
|
+
}
|
|
171
|
+
else if (fs.existsSync(specSkillPath)) {
|
|
172
|
+
fs.unlinkSync(specSkillPath);
|
|
173
|
+
try {
|
|
174
|
+
fs.rmdirSync(path.dirname(specSkillPath));
|
|
175
|
+
}
|
|
176
|
+
catch { /* not empty or already gone */ }
|
|
177
|
+
console.log(chalk_1.default.dim(' removed .claude/skills/oprim-spec/SKILL.md'));
|
|
178
|
+
}
|
|
132
179
|
// openspec skills — add/remove Step 0 in-place when they exist
|
|
133
180
|
for (const name of OPENSPEC_SKILL_NAMES) {
|
|
134
181
|
const skillFilePath = path.join(skillsBase, name, 'SKILL.md');
|
|
@@ -144,7 +191,12 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
|
|
|
144
191
|
}
|
|
145
192
|
const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
|
|
146
193
|
for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
|
|
147
|
-
|
|
194
|
+
// promote.md is regenerated per-project since its content branches on the selected
|
|
195
|
+
// speccing framework — the static CLAUDE_COMMANDS entry only reflects the default.
|
|
196
|
+
const finalContent = filename === 'promote.md'
|
|
197
|
+
? claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent(framework))
|
|
198
|
+
: content;
|
|
199
|
+
(0, scaffold_1.writeFile)(path.join(cmdsDir, filename), finalContent);
|
|
148
200
|
console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
|
|
149
201
|
}
|
|
150
202
|
// Tombstone cleanup: remove command wrappers deleted in v0.2.0 (bet/criteria/pdr/review
|
|
@@ -188,6 +240,19 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
|
|
|
188
240
|
(0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
|
|
189
241
|
console.log(chalk_1.default.green('✓') + ` .poolside/skills/${name}/SKILL.md`);
|
|
190
242
|
}
|
|
243
|
+
const poolsideSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
|
|
244
|
+
if (framework === 'native') {
|
|
245
|
+
(0, scaffold_1.writeFile)(poolsideSpecSkillPath, specAuthoringSkill());
|
|
246
|
+
console.log(chalk_1.default.green('✓') + ' .poolside/skills/oprim-spec/SKILL.md');
|
|
247
|
+
}
|
|
248
|
+
else if (fs.existsSync(poolsideSpecSkillPath)) {
|
|
249
|
+
fs.unlinkSync(poolsideSpecSkillPath);
|
|
250
|
+
try {
|
|
251
|
+
fs.rmdirSync(path.dirname(poolsideSpecSkillPath));
|
|
252
|
+
}
|
|
253
|
+
catch { /* not empty or already gone */ }
|
|
254
|
+
console.log(chalk_1.default.dim(' removed .poolside/skills/oprim-spec/SKILL.md'));
|
|
255
|
+
}
|
|
191
256
|
const agentsFile = path.join(projectRoot, 'AGENTS.md');
|
|
192
257
|
writeAgentInstructionFile(agentsFile, poolsideInstructions());
|
|
193
258
|
console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
|
|
@@ -213,9 +278,27 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
|
|
|
213
278
|
(0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
|
|
214
279
|
console.log(chalk_1.default.green('✓') + ` .cursor/skills/${name}/SKILL.md`);
|
|
215
280
|
}
|
|
281
|
+
const cursorSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
|
|
282
|
+
if (framework === 'native') {
|
|
283
|
+
(0, scaffold_1.writeFile)(cursorSpecSkillPath, specAuthoringSkill());
|
|
284
|
+
console.log(chalk_1.default.green('✓') + ' .cursor/skills/oprim-spec/SKILL.md');
|
|
285
|
+
}
|
|
286
|
+
else if (fs.existsSync(cursorSpecSkillPath)) {
|
|
287
|
+
fs.unlinkSync(cursorSpecSkillPath);
|
|
288
|
+
try {
|
|
289
|
+
fs.rmdirSync(path.dirname(cursorSpecSkillPath));
|
|
290
|
+
}
|
|
291
|
+
catch { /* not empty or already gone */ }
|
|
292
|
+
console.log(chalk_1.default.dim(' removed .cursor/skills/oprim-spec/SKILL.md'));
|
|
293
|
+
}
|
|
216
294
|
const cmdsDir = path.join(cursorDir, 'commands');
|
|
217
295
|
for (const [filename, content] of Object.entries(exports.CURSOR_COMMANDS)) {
|
|
218
|
-
|
|
296
|
+
// oprim-promote.md is regenerated per-project since its content branches on the
|
|
297
|
+
// selected speccing framework — the static CURSOR_COMMANDS entry only reflects the default.
|
|
298
|
+
const finalContent = filename === 'oprim-promote.md'
|
|
299
|
+
? cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent(framework))
|
|
300
|
+
: content;
|
|
301
|
+
(0, scaffold_1.writeFile)(path.join(cmdsDir, filename), finalContent);
|
|
219
302
|
console.log(chalk_1.default.green('✓') + ` .cursor/commands/${filename}`);
|
|
220
303
|
}
|
|
221
304
|
if (dirCreated) {
|
|
@@ -302,7 +385,7 @@ exports.CLAUDE_SKILLS = {
|
|
|
302
385
|
};
|
|
303
386
|
// ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
|
|
304
387
|
exports.CLAUDE_COMMANDS = {
|
|
305
|
-
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into
|
|
388
|
+
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent()),
|
|
306
389
|
'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
|
|
307
390
|
'archive.md': claudeWrapper('OPRIM: Archive', 'Archive a completed bet — move it out of the active board', archiveCommandContent()),
|
|
308
391
|
};
|
|
@@ -326,7 +409,7 @@ exports.CURSOR_SKILLS = {
|
|
|
326
409
|
};
|
|
327
410
|
// ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
|
|
328
411
|
exports.CURSOR_COMMANDS = {
|
|
329
|
-
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into
|
|
412
|
+
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent()),
|
|
330
413
|
'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
|
|
331
414
|
'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
|
|
332
415
|
'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
|
|
@@ -378,6 +461,9 @@ Scan \`oprim/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all intege
|
|
|
378
461
|
Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
|
|
379
462
|
Output path: \`oprim/decisions/PDR-NNN-<slug>.md\`
|
|
380
463
|
|
|
464
|
+
### 2b. Check for custom rules
|
|
465
|
+
Read \`oprim/config.yaml\`. If it has a non-empty \`rules.pdr\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 3 and reflect it in the generated content. If \`rules.pdr\` is absent or empty, skip this step; behavior is unchanged.
|
|
466
|
+
|
|
381
467
|
### 3. Gather content
|
|
382
468
|
Ask: Context (what forced this decision), Decision (clear statement), Alternatives considered (why rejected), Consequences (positives / trade-offs / follow-ups), Evidence links (optional), Related bets (optional), Related OpenSpec changes (optional).
|
|
383
469
|
|
|
@@ -466,6 +552,9 @@ From the bet title: lowercase all characters, replace any character that is not
|
|
|
466
552
|
### 3. Check sequence.yaml exists
|
|
467
553
|
If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
|
|
468
554
|
|
|
555
|
+
### 3b. Check for custom rules
|
|
556
|
+
Read \`oprim/config.yaml\`. If it has a non-empty \`rules.bet\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 4 and reflect it in the generated \`bet-decision.md\` content. If \`rules.bet\` is absent or empty, skip this step; behavior is unchanged.
|
|
557
|
+
|
|
469
558
|
### 4. Gather content
|
|
470
559
|
Ask: Decision (Build now / Defer / Kill, default Build now), Owner, Review date (YYYY-MM-DD), Why now, Alternatives considered, Expected outcomes (metric: baseline → target in timeframe), Kill criteria / rollback trigger, PDR links (optional).
|
|
471
560
|
|
|
@@ -666,10 +755,10 @@ If not: create with \`metrics:\` list.
|
|
|
666
755
|
function archiveSkill() {
|
|
667
756
|
return `---
|
|
668
757
|
name: oprim-archive
|
|
669
|
-
description: Archive a completed bet — moves it to oprim/bets/archived
|
|
758
|
+
description: Archive a completed bet — moves it to oprim/bets/archived/, removes its sequence.yaml entry, and folds any spec deltas under its specs/ directory into oprim/specs/ current truth
|
|
670
759
|
---
|
|
671
760
|
|
|
672
|
-
Archive a completed bet by moving it to \`oprim/bets/archived
|
|
761
|
+
Archive a completed bet by moving it to \`oprim/bets/archived/\`, removing it from \`sequence.yaml\`, and (if present) merging its spec deltas into current truth.
|
|
673
762
|
|
|
674
763
|
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
675
764
|
|
|
@@ -699,26 +788,50 @@ If neither pattern matches:
|
|
|
699
788
|
- Report: "Bet BET-NNN was not found in oprim/bets/. Nothing was changed."
|
|
700
789
|
- Stop.
|
|
701
790
|
|
|
702
|
-
### 3. Check for active dependencies
|
|
791
|
+
### 3. Check for active dependencies and concurrent spec-delta conflicts
|
|
703
792
|
|
|
704
793
|
Read \`oprim/sequence.yaml\`. Scan every entry across all buckets (now, next, later, backlog) for any entry whose \`blocked_by\` or \`unlocks\` list contains the target bet ID.
|
|
705
794
|
|
|
706
|
-
|
|
707
|
-
|
|
795
|
+
Separately, if \`oprim/bets/<resolved-dir>/specs/\` exists: for each \`<capability>/spec.md\` delta file under it, extract every \`### Requirement:\` header from its \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` sections. Then scan every other bet directory directly under \`oprim/bets/\` (excluding \`archived/\` and the bet being archived) for a \`specs/<capability>/spec.md\` file for the same capability; if one exists, extract its \`### Requirement:\` headers too. Flag any header that matches (whitespace-insensitive) between the archiving bet's delta and another still-active bet's delta as an **overlap**.
|
|
796
|
+
|
|
797
|
+
If either sequence.yaml dependents or delta overlaps are found:
|
|
798
|
+
- Show a combined warning listing each dependent entry and each overlapping requirement.
|
|
708
799
|
|
|
709
800
|
Example:
|
|
710
801
|
\`\`\`
|
|
711
802
|
⚠ Warning: BET-005 is referenced by active bets:
|
|
712
803
|
- BET-007 (blocked_by: [BET-005])
|
|
713
804
|
- BET-008 (unlocks: [BET-005])
|
|
805
|
+
⚠ Warning: BET-005's delta for requirement "The system SHALL ..." in capability foo overlaps with active bet BET-009's delta for the same requirement. Archiving BET-005 now applies its version to oprim/specs/foo/spec.md; if BET-009 archives later, its version will overwrite this requirement again (last-write-wins — no 3-way merge is attempted).
|
|
714
806
|
\`\`\`
|
|
715
|
-
- Ask: "Archive BET-NNN anyway?
|
|
807
|
+
- Ask: "Archive BET-NNN anyway? (y/N)"
|
|
716
808
|
- If "n" or Enter: stop, no changes made.
|
|
717
809
|
- If "y": proceed.
|
|
718
810
|
|
|
719
|
-
If
|
|
811
|
+
If neither is found: proceed without warning.
|
|
812
|
+
|
|
813
|
+
### 4. Fold spec deltas into current truth
|
|
814
|
+
|
|
815
|
+
If \`oprim/bets/<resolved-dir>/specs/\` does not exist: skip this step entirely and go to Step 5 — archive behavior is unchanged from before spec deltas existed.
|
|
720
816
|
|
|
721
|
-
|
|
817
|
+
Otherwise, for each capability subdirectory under \`oprim/bets/<resolved-dir>/specs/\` containing a \`spec.md\`:
|
|
818
|
+
|
|
819
|
+
1. Read the delta file's \`## ADDED Requirements\` / \`## MODIFIED Requirements\` / \`## REMOVED Requirements\` sections. Each \`### Requirement:\` block runs from its header through its body and any \`#### Scenario:\` sub-entries, up to the next \`### Requirement:\` or \`## \` header.
|
|
820
|
+
2. Read \`oprim/specs/<capability>/spec.md\` if it exists (current truth uses a single flat \`## Requirements\` section).
|
|
821
|
+
- **If it does not exist:**
|
|
822
|
+
- If the delta is entirely \`## ADDED Requirements\` (no MODIFIED/REMOVED sections): create \`oprim/specs/<capability>/spec.md\` with a \`## Requirements\` header and append each ADDED requirement block beneath it.
|
|
823
|
+
- If the delta contains any MODIFIED or REMOVED requirements: stop before moving anything and report an error — "cannot modify/remove requirement '<header>' in capability <capability> — no current-truth spec exists yet for this capability."
|
|
824
|
+
- **If it does exist:**
|
|
825
|
+
- **ADDED**: append the requirement block to the end of the \`## Requirements\` section.
|
|
826
|
+
- **MODIFIED**: find the existing \`### Requirement:\` block whose header text matches the delta's (whitespace-insensitive); replace that entire block (header, body, and scenarios) with the delta's version. If no match is found, treat it as ADDED instead (append) and note this in the final report.
|
|
827
|
+
- **REMOVED**: find and delete the matching block entirely. If no match is found, note this in the final report and continue — nothing to remove.
|
|
828
|
+
3. Write the updated \`oprim/specs/<capability>/spec.md\`.
|
|
829
|
+
|
|
830
|
+
This fold always overwrites the matched requirement wholesale — it never reconciles two bets' overlapping changes. If a later bet's archive touches the same requirement again, its version simply replaces this one (last-write-wins, confirmed by construction — no 3-way merge).
|
|
831
|
+
|
|
832
|
+
Track which capabilities were merged (and any no-match notes) for the final report.
|
|
833
|
+
|
|
834
|
+
### 5. Move the bet directory to archive
|
|
722
835
|
|
|
723
836
|
Create the archive subfolder if it doesn't exist:
|
|
724
837
|
\`\`\`bash
|
|
@@ -730,11 +843,11 @@ Move the resolved directory:
|
|
|
730
843
|
mv oprim/bets/<resolved-dir> oprim/bets/archived/<resolved-dir>
|
|
731
844
|
\`\`\`
|
|
732
845
|
|
|
733
|
-
###
|
|
846
|
+
### 6. Remove the bet entry from sequence.yaml
|
|
734
847
|
|
|
735
848
|
Read \`oprim/sequence.yaml\`, parse it, and remove the entry with \`id: BET-NNN\` from whichever bucket it appears in (now, next, later, or backlog). Write the updated YAML back using 2-space indentation. Do not modify any other entries.
|
|
736
849
|
|
|
737
|
-
###
|
|
850
|
+
### 7. Report what was done
|
|
738
851
|
|
|
739
852
|
\`\`\`
|
|
740
853
|
## Bet Archived
|
|
@@ -742,6 +855,7 @@ Read \`oprim/sequence.yaml\`, parse it, and remove the entry with \`id: BET-NNN\
|
|
|
742
855
|
**Bet:** BET-NNN
|
|
743
856
|
**Archived to:** oprim/bets/archived/<resolved-dir>/
|
|
744
857
|
**Removed from sequence.yaml:** ✓
|
|
858
|
+
**Spec deltas merged:** <capability-1>, <capability-2> (omit this line if no specs/ directory was present)
|
|
745
859
|
|
|
746
860
|
The bet is preserved in full at the archive location.
|
|
747
861
|
\`\`\`
|
|
@@ -886,6 +1000,9 @@ Create a KPI review in \`oprim/reviews/\`.
|
|
|
886
1000
|
### 1. Identify the bet
|
|
887
1001
|
If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
|
|
888
1002
|
|
|
1003
|
+
### 1b. Check for custom rules
|
|
1004
|
+
Read \`oprim/config.yaml\`. If it has a non-empty \`rules.review\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 4 and reflect it in the generated review content. If \`rules.review\` is absent or empty, skip this step; behavior is unchanged.
|
|
1005
|
+
|
|
889
1006
|
### 2. Load criteria and check for a run result
|
|
890
1007
|
|
|
891
1008
|
Read \`oprim/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
|
|
@@ -940,12 +1057,83 @@ Prepend the frontmatter block from step 5b, if one was prepared.
|
|
|
940
1057
|
### 7. Report what was created
|
|
941
1058
|
`;
|
|
942
1059
|
}
|
|
1060
|
+
function specAuthoringSkill() {
|
|
1061
|
+
return `---
|
|
1062
|
+
name: oprim-spec
|
|
1063
|
+
description: Generate a native oprim capability spec delta at oprim/bets/BET-NNN-<slug>/specs/<capability>/spec.md while a bet is active, in RFC 2119 (SHALL/SHOULD/MAY) requirements and Gherkin scenarios — folded into oprim/specs/<capability>/spec.md (current truth) when the bet is archived
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
Generate a capability spec delta for an active bet — RFC 2119 requirements plus Gherkin scenarios, no OpenSpec required. This skill never writes to \`oprim/specs/\` directly; \`oprim-archive\` folds the delta into current truth when the bet is archived.
|
|
1067
|
+
|
|
1068
|
+
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
1069
|
+
|
|
1070
|
+
## Steps
|
|
1071
|
+
|
|
1072
|
+
### 1. Get the active bet
|
|
1073
|
+
If a bet ID was provided as context (e.g. invoked from \`/oprim:promote\`), use it directly. Otherwise ask: "Which bet is this spec change for? (e.g. BET-005)"
|
|
1074
|
+
|
|
1075
|
+
Resolve it to a directory in \`oprim/bets/\` using the same two patterns \`oprim-archive\` uses: exact \`BET-NNN/\` (legacy, no slug) or the slug variant \`BET-NNN-<slug>/\`. If neither matches, report "Bet BET-NNN was not found in oprim/bets/ — spec deltas can only be authored against an active bet" and stop.
|
|
1076
|
+
|
|
1077
|
+
### 2. Get the capability name and description
|
|
1078
|
+
If not provided, ask: "What capability are you specifying? (a short name, e.g. 'spec-authoring')" and "What does it do? (one or two sentences)"
|
|
1079
|
+
|
|
1080
|
+
### 2b. Derive the slug
|
|
1081
|
+
From the capability name: lowercase all characters, replace any character that is not a letter or digit with a hyphen, collapse consecutive hyphens to one, strip leading/trailing hyphens. This becomes \`<capability>\`.
|
|
1082
|
+
Output path: \`oprim/bets/<resolved-bet-dir>/specs/<capability>/spec.md\` (a delta, not \`oprim/specs/<capability>/spec.md\` — that file is current truth and is only ever written by \`oprim-archive\`'s merge step).
|
|
1083
|
+
|
|
1084
|
+
### 2c. Check for custom rules
|
|
1085
|
+
Read \`oprim/config.yaml\`. If it has a non-empty \`rules.spec\` value, treat it as additional guidance from the team — factor it into the requirements and scenarios you draft. If \`rules.spec\` is absent or empty, skip this step; behavior is unchanged.
|
|
1086
|
+
|
|
1087
|
+
### 3. Determine the delta type for each requirement
|
|
1088
|
+
For each requirement, ask whether it is new (**ADDED**), a change to an existing current-truth requirement (**MODIFIED**), or a removal of one (**REMOVED**).
|
|
1089
|
+
|
|
1090
|
+
- **ADDED**: gather the requirement statement fresh.
|
|
1091
|
+
- **MODIFIED / REMOVED**: read \`oprim/specs/<capability>/spec.md\` if it exists and list its \`### Requirement:\` headers so the user can pick the one being changed. The header text must match exactly (whitespace-insensitive) for \`oprim-archive\`'s merge step to find it later. If the file doesn't exist yet, MODIFIED/REMOVED aren't possible for this capability — fall back to ADDED.
|
|
1092
|
+
|
|
1093
|
+
### 4. Gather requirements and scenarios
|
|
1094
|
+
For ADDED and MODIFIED requirements, phrase each as an RFC 2119 statement using SHALL (mandatory), SHOULD (recommended), or MAY (optional), then ask for at least one scenario: a WHEN (trigger) and a THEN (expected outcome), with an optional GIVEN (context) and additional AND steps. REMOVED requirements only need the matching header — no new scenarios.
|
|
1095
|
+
|
|
1096
|
+
### 5. Write the delta file
|
|
1097
|
+
Append to (or create) \`oprim/bets/<resolved-bet-dir>/specs/<capability>/spec.md\`, grouping requirements under the matching section header — only include a section if it has at least one requirement under it:
|
|
1098
|
+
|
|
1099
|
+
\`\`\`markdown
|
|
1100
|
+
## ADDED Requirements
|
|
1101
|
+
|
|
1102
|
+
### Requirement: <capability> SHALL/SHOULD/MAY <requirement statement>
|
|
1103
|
+
<one-sentence elaboration>
|
|
1104
|
+
|
|
1105
|
+
#### Scenario: <scenario title>
|
|
1106
|
+
- **GIVEN** <context> (optional)
|
|
1107
|
+
- **WHEN** <trigger>
|
|
1108
|
+
- **THEN** <outcome>
|
|
1109
|
+
- **AND** <additional outcome> (optional)
|
|
1110
|
+
|
|
1111
|
+
## MODIFIED Requirements
|
|
1112
|
+
|
|
1113
|
+
### Requirement: <exact header text matched from oprim/specs/<capability>/spec.md>
|
|
1114
|
+
<revised elaboration>
|
|
1115
|
+
|
|
1116
|
+
#### Scenario: <scenario title>
|
|
1117
|
+
- **WHEN** <trigger>
|
|
1118
|
+
- **THEN** <outcome>
|
|
1119
|
+
|
|
1120
|
+
## REMOVED Requirements
|
|
1121
|
+
|
|
1122
|
+
### Requirement: <exact header text matched from oprim/specs/<capability>/spec.md>
|
|
1123
|
+
\`\`\`
|
|
1124
|
+
|
|
1125
|
+
If the delta file already exists (a prior spec-authoring pass for this bet/capability), append new requirements to the matching section, creating that section if it's not yet present.
|
|
1126
|
+
|
|
1127
|
+
### 6. Report what was created
|
|
1128
|
+
Show the delta file path, which bet it's scoped to, and a summary of the ADDED/MODIFIED/REMOVED requirements captured. Note that it merges into \`oprim/specs/<capability>/spec.md\` when \`BET-NNN\` is archived — nothing is current truth yet.
|
|
1129
|
+
`;
|
|
1130
|
+
}
|
|
943
1131
|
// ─── Cursor inline content (condensed versions for command files) ─────────────
|
|
944
1132
|
function pdrInlineContent() {
|
|
945
|
-
return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
|
|
1133
|
+
return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Read \`oprim/config.yaml\`'s \`rules.pdr\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
|
|
946
1134
|
}
|
|
947
1135
|
function betInlineContent() {
|
|
948
|
-
return `Create a new bet in \`oprim/bets/\`. First explain: "A bet is a product decision you're committing to explore — a problem worth solving, a hypothesis worth testing, or a direction worth taking. You'll name it, explain why now, and set a kill criterion." Then show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan \`BET-(\\d+)\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters, warn "this title may be too vague", suggest a reformulation, and ask "Proceed anyway? (y/N)" — if "n", prompt for a revised title. Gather: decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.`;
|
|
1136
|
+
return `Create a new bet in \`oprim/bets/\`. First explain: "A bet is a product decision you're committing to explore — a problem worth solving, a hypothesis worth testing, or a direction worth taking. You'll name it, explain why now, and set a kill criterion." Then show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan \`BET-(\\d+)\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). Read \`oprim/config.yaml\`'s \`rules.bet\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters, warn "this title may be too vague", suggest a reformulation, and ask "Proceed anyway? (y/N)" — if "n", prompt for a revised title. Gather: decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.`;
|
|
949
1137
|
}
|
|
950
1138
|
function noteInlineContent() {
|
|
951
1139
|
return `Create a new note in \`oprim/notes/\` for lightweight thinking capture — an observation, idea, or connection that hasn't yet earned a place in a bet or PDR. Notes carry no owner or kill criterion; promote one into a bet later with \`/oprim:promote NOTE-NNN\`. Ask for a short title. Scan \`oprim/notes/NOTE-(\\d+)-\` for the next id (zero-padded, default 001). Ask for the note body (free-form), tags, and optional related BET-IDs. Tags are checked against \`oprim/config.yaml\`'s \`notes.tags\`; any new tag is accepted and appended to that list rather than rejected — the vocabulary grows from usage. Read \`oprim/templates/note.md\` — if its frontmatter has a \`description:\` field, this workspace is on the OKF tier and needs a one-line description; if it has no \`description:\` field, use the minimal tier; if the file doesn't exist, fall back to reading \`okf.enabled\` directly from \`oprim/config.yaml\`. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section listing any related BET-IDs. For each related bet, append \`- Notes: NOTE-NNN\` to that bet-decision's \`## Links\` section. Report what was created.`;
|
|
@@ -954,7 +1142,7 @@ function criteriaInlineContent() {
|
|
|
954
1142
|
return `Add metrics to \`oprim/bets/BET-NNN/criteria.yaml\`. Verify bet dir exists. Gather: metric ID, name, baseline, target, timeframe, launch date, segment. Ask source type (amplitude or bigquery). Amplitude: event, aggregation, denominator_event. BigQuery: table, metric_column, filter, aggregation, denominator_query. If file exists: append to metrics list (never overwrite). If not: create. Ask if adding more metrics. Report what was created.`;
|
|
955
1143
|
}
|
|
956
1144
|
function reviewInlineContent() {
|
|
957
|
-
return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
|
|
1145
|
+
return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`oprim/config.yaml\`'s \`rules.review\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
|
|
958
1146
|
}
|
|
959
1147
|
// ─── Hook scripts: co-archival coordination ──────────────────────────────────
|
|
960
1148
|
function hooksConfig(framework) {
|
|
@@ -1106,20 +1294,14 @@ function mergeClaudeSettingsHooks(claudeDir) {
|
|
|
1106
1294
|
console.log(chalk_1.default.green('✓') + ' .claude/settings.json (UserPromptSubmit + Stop hooks registered)');
|
|
1107
1295
|
}
|
|
1108
1296
|
// ─── Legacy content (promote remains inline; sequence now delegates to skill) ─
|
|
1109
|
-
function promoteContent() {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
- \`NOTE-\` → **B. Note → Bet**
|
|
1118
|
-
- Anything else → report "Unrecognized ID prefix — expected BET- or NOTE-" and stop. Do not silently do nothing.
|
|
1119
|
-
|
|
1120
|
-
## A. Bet → OpenSpec change
|
|
1121
|
-
|
|
1122
|
-
1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
1297
|
+
function promoteContent(framework = 'openspec') {
|
|
1298
|
+
const sectionATitle = framework === 'openspec'
|
|
1299
|
+
? 'A. Bet → OpenSpec change'
|
|
1300
|
+
: framework === 'native'
|
|
1301
|
+
? 'A. Bet → native oprim spec'
|
|
1302
|
+
: 'A. Bet → spec (no framework configured)';
|
|
1303
|
+
const sectionABody = framework === 'openspec'
|
|
1304
|
+
? `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
1123
1305
|
2. **Validate status** — decision must be "Build now"
|
|
1124
1306
|
3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
|
|
1125
1307
|
4. **Create OpenSpec change** — derive the change name as \`bet-NNN-<slug>\` where \`NNN\` is the zero-padded bet number (e.g. BET-004 → \`bet-004\`) and \`<slug>\` is a short kebab-case summary of the change. Then invoke the \`/openspec-propose\` skill (or \`/opsx:propose\`) with that name to create the change directory with **all required artifacts**: \`proposal.md\`, \`design.md\`, \`tasks.md\`, and \`specs/<capability>/spec.md\` for every capability listed under \`## Capabilities\`.
|
|
@@ -1136,7 +1318,31 @@ Promote an atomic note into a bet, or a prioritized bet into an OpenSpec change.
|
|
|
1136
1318
|
- \`tasks.md\`
|
|
1137
1319
|
- \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
|
|
1138
1320
|
If any artifact is missing, create it before reporting done.
|
|
1139
|
-
8. **Report** — show what was linked and what remains for engineering
|
|
1321
|
+
8. **Report** — show what was linked and what remains for engineering`
|
|
1322
|
+
: framework === 'native'
|
|
1323
|
+
? `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
1324
|
+
2. **Validate status** — decision must be "Build now"
|
|
1325
|
+
3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
|
|
1326
|
+
4. **Generate the native spec delta(s)** — for each capability listed under the bet's \`## Capabilities\` section (or a single capability derived from the bet title if none is listed), invoke the \`oprim-spec\` skill with this bet as context to write \`oprim/bets/BET-XXX/specs/<capability>/spec.md\` — a delta using \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` headers reflecting the bet's why/outcome. No OpenSpec change directory is created and OpenSpec need not be installed. Nothing is written to \`oprim/specs/<capability>/spec.md\` (current truth) at promote time — that only happens when this bet is archived.
|
|
1327
|
+
5. **Link artifacts** — add \`- Spec (delta): oprim/bets/BET-XXX/specs/<capability>/spec.md\` (one line per capability) to the bet-decision \`## Links\` section
|
|
1328
|
+
6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, note it alongside the spec link
|
|
1329
|
+
7. **Report** — show what was created and linked, and note that merge-on-archive will fold the delta into \`oprim/specs/\` when the bet archives`
|
|
1330
|
+
: `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
1331
|
+
2. **Validate status** — decision must be "Build now"
|
|
1332
|
+
3. **Report and stop** — no speccing framework is configured (\`integrations.spec_framework: none\`). Add \`- Spec: none (no speccing framework configured)\` to the bet-decision \`## Links\` section. No spec artifact is created.`;
|
|
1333
|
+
return `
|
|
1334
|
+
Promote an atomic note into a bet, or a prioritized bet into a capability spec. The promotion path is determined solely by the prefix of the ID argument — there is no separate command for each.
|
|
1335
|
+
|
|
1336
|
+
**Input**: Specify an ID (e.g., \`/oprim:promote BET-042\` or \`/oprim:promote NOTE-005\`) or omit to be prompted.
|
|
1337
|
+
|
|
1338
|
+
### 0. Determine the promotion path from the ID prefix
|
|
1339
|
+
- \`BET-\` → **${sectionATitle}**
|
|
1340
|
+
- \`NOTE-\` → **B. Note → Bet**
|
|
1341
|
+
- Anything else → report "Unrecognized ID prefix — expected BET- or NOTE-" and stop. Do not silently do nothing.
|
|
1342
|
+
|
|
1343
|
+
## ${sectionATitle}
|
|
1344
|
+
|
|
1345
|
+
${sectionABody}
|
|
1140
1346
|
|
|
1141
1347
|
## B. Note → Bet
|
|
1142
1348
|
|
|
@@ -1200,6 +1406,7 @@ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
|
|
|
1200
1406
|
2. Ask for the bet title. Validate: fewer than 4 words OR fewer than 25 chars → warn, suggest reformulation, ask "Proceed anyway? (y/N)".
|
|
1201
1407
|
3. Assign next BET ID: scan \`oprim/bets/BET-(\\d+)\` dirs, max+1 zero-padded to 3 digits (default 001).
|
|
1202
1408
|
4. Check \`oprim/sequence.yaml\` exists — stop if not, advise \`oprim init\`.
|
|
1409
|
+
4b. Read \`oprim/config.yaml\`'s \`rules.bet\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
|
|
1203
1410
|
5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
|
|
1204
1411
|
6. Write \`oprim/bets/BET-NNN/bet-decision.md\` with all fields.
|
|
1205
1412
|
7. Append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`.
|
|
@@ -1235,6 +1442,7 @@ Create a new Product Decision Record in \`oprim/decisions/\`.
|
|
|
1235
1442
|
|
|
1236
1443
|
1. Ask for decision title.
|
|
1237
1444
|
2. Assign next PDR ID: scan \`oprim/decisions/PDR-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
|
|
1445
|
+
2b. Read \`oprim/config.yaml\`'s \`rules.pdr\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
|
|
1238
1446
|
3. Gather: context, decision, alternatives, consequences, evidence, related bets/specs.
|
|
1239
1447
|
4. Ask if superseding an existing PDR.
|
|
1240
1448
|
5. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding, update old PDR Status.
|
|
@@ -1244,6 +1452,7 @@ Create a new Product Decision Record in \`oprim/decisions/\`.
|
|
|
1244
1452
|
Create a KPI review artifact in \`oprim/reviews/\`.
|
|
1245
1453
|
|
|
1246
1454
|
1. Ask which bet (e.g. BET-042).
|
|
1455
|
+
1b. Read \`oprim/config.yaml\`'s \`rules.review\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
|
|
1247
1456
|
2. Read \`oprim/bets/BET-NNN/criteria.yaml\` for pre-fill. Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` — use most recent if present.
|
|
1248
1457
|
3. If no run result, ask for each metric's actual value.
|
|
1249
1458
|
4. Status: actual >= target → hit; actual < target → missed; not provided → pending.
|
|
@@ -1256,10 +1465,11 @@ Archive a completed bet.
|
|
|
1256
1465
|
|
|
1257
1466
|
1. Ask for bet ID (accept bet-005, 005, 5, BET-005 — normalize to BET-NNN).
|
|
1258
1467
|
2. Verify \`oprim/bets/BET-NNN/\` exists.
|
|
1259
|
-
3. Check \`oprim/sequence.yaml\` for entries where \`blocked_by\` or \`unlocks\` reference the target bet — warn if found,
|
|
1260
|
-
4.
|
|
1261
|
-
5.
|
|
1262
|
-
6.
|
|
1468
|
+
3. Check \`oprim/sequence.yaml\` for entries where \`blocked_by\` or \`unlocks\` reference the target bet — warn if found. Also check other active bet dirs for delta specs against the same requirement (matching \`### Requirement:\` headers, whitespace-insensitive) — warn if an overlap is found. Ask "Archive anyway? (y/N)" if either warning fires.
|
|
1469
|
+
4. If \`oprim/bets/BET-NNN/specs/\` exists, fold each capability's \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` delta into \`oprim/specs/<capability>/spec.md\` (matching by \`### Requirement:\` header; create the current-truth file if the delta is entirely ADDED) — last-write-wins on overlaps, no 3-way merge. Skip this step entirely if no \`specs/\` dir is present.
|
|
1470
|
+
5. Move directory: \`oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN\`.
|
|
1471
|
+
6. Remove the bet entry from \`oprim/sequence.yaml\`.
|
|
1472
|
+
7. Report what was done.
|
|
1263
1473
|
|
|
1264
1474
|
### Sequencing board (oprim-sequence)
|
|
1265
1475
|
Validate the primer sequencing board and regenerate the visual view.
|
package/dist/lib/integrity.js
CHANGED
|
@@ -97,7 +97,8 @@ function checkSkillVersionDrift(projectRoot, checks) {
|
|
|
97
97
|
const skillsDir = path.join(projectRoot, '.claude', 'skills');
|
|
98
98
|
if (!fs.existsSync(skillsDir))
|
|
99
99
|
return;
|
|
100
|
-
|
|
100
|
+
const bundledSkills = { ...install_agent_1.CLAUDE_SKILLS, 'oprim-spec': (0, install_agent_1.specAuthoringSkill)() };
|
|
101
|
+
for (const [name, bundledContent] of Object.entries(bundledSkills)) {
|
|
101
102
|
const skillPath = path.join(skillsDir, name, 'SKILL.md');
|
|
102
103
|
if (!fs.existsSync(skillPath))
|
|
103
104
|
continue;
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean): string;
|
|
1
|
+
export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean, specFramework?: string): string;
|
|
2
2
|
export declare function okfFrontmatter(type: string, titleHint: string): string;
|
|
3
3
|
export declare function noteMinimalFrontmatter(titleHint: string): string;
|
|
4
4
|
export declare function indexTemplate(projectName: string): string;
|
package/dist/lib/templates.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.configTemplate = configTemplate;
|
|
|
5
5
|
exports.okfFrontmatter = okfFrontmatter;
|
|
6
6
|
exports.noteMinimalFrontmatter = noteMinimalFrontmatter;
|
|
7
7
|
exports.indexTemplate = indexTemplate;
|
|
8
|
-
function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled) {
|
|
8
|
+
function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled, specFramework = openspecEnabled ? 'openspec' : 'none') {
|
|
9
9
|
return `version: 1
|
|
10
10
|
project:
|
|
11
11
|
name: "${projectName}"
|
|
@@ -17,6 +17,7 @@ integrations:
|
|
|
17
17
|
graphify:
|
|
18
18
|
enabled: ${graphifyEnabled}
|
|
19
19
|
graph_dir: graphify-out
|
|
20
|
+
spec_framework: ${specFramework}
|
|
20
21
|
okf:
|
|
21
22
|
enabled: ${okfEnabled}
|
|
22
23
|
measurement:
|
|
@@ -28,6 +29,10 @@ measurement:
|
|
|
28
29
|
sequencing:
|
|
29
30
|
wip_limits:
|
|
30
31
|
now: 2
|
|
32
|
+
context: ""
|
|
33
|
+
rules: {}
|
|
34
|
+
store:
|
|
35
|
+
enabled: false
|
|
31
36
|
`;
|
|
32
37
|
}
|
|
33
38
|
// OKF (Open Knowledge Format) — https://github.com/GoogleCloudPlatform/okf
|