@open-product-primer/cli 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/bin/oprim.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +22 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +176 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/measure.d.ts +2 -0
- package/dist/commands/measure.js +207 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +81 -0
- package/dist/lib/detect.d.ts +10 -0
- package/dist/lib/detect.js +72 -0
- package/dist/lib/install-agent.d.ts +7 -0
- package/dist/lib/install-agent.js +421 -0
- package/dist/lib/measure.d.ts +55 -0
- package/dist/lib/measure.js +250 -0
- package/dist/lib/scaffold.d.ts +4 -0
- package/dist/lib/scaffold.js +60 -0
- package/dist/lib/templates.d.ts +6 -0
- package/dist/lib/templates.js +120 -0
- package/package.json +60 -0
|
@@ -0,0 +1,81 @@
|
|
|
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.updateCommand = updateCommand;
|
|
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 install_agent_1 = require("../lib/install-agent");
|
|
45
|
+
const detect_1 = require("../lib/detect");
|
|
46
|
+
function updateCommand() {
|
|
47
|
+
return new commander_1.Command('update')
|
|
48
|
+
.description('Refresh /oprim:* assistant commands and skills from package templates')
|
|
49
|
+
.action(() => {
|
|
50
|
+
const projectRoot = process.cwd();
|
|
51
|
+
const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
52
|
+
if (configAgents !== null && configAgents.length > 0) {
|
|
53
|
+
for (const agent of configAgents) {
|
|
54
|
+
(0, install_agent_1.installAgentSkills)(agent, projectRoot);
|
|
55
|
+
}
|
|
56
|
+
console.log(`\nAgent skills updated: ${configAgents.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
else if (configAgents !== null && configAgents.length === 0) {
|
|
59
|
+
console.log(chalk_1.default.yellow('No agents configured') + ' — run ' + chalk_1.default.cyan('oprim init') + ' to select agents');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Legacy: fall back to directory detection
|
|
63
|
+
let updated = 0;
|
|
64
|
+
if (fs.existsSync(path.join(projectRoot, '.claude'))) {
|
|
65
|
+
(0, install_agent_1.installAgentSkills)('claude', projectRoot);
|
|
66
|
+
updated++;
|
|
67
|
+
}
|
|
68
|
+
if (fs.existsSync(path.join(projectRoot, '.cursor'))) {
|
|
69
|
+
(0, install_agent_1.installAgentSkills)('cursor', projectRoot);
|
|
70
|
+
updated++;
|
|
71
|
+
}
|
|
72
|
+
if (updated === 0) {
|
|
73
|
+
console.log(chalk_1.default.yellow('No assistant environments detected (.claude/, .cursor/).'));
|
|
74
|
+
console.log('Run this command from a repository where AI tools are configured.');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(`\nAgent skills updated for ${updated} environment(s).`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function detectOpenSpec(projectRoot: string): {
|
|
2
|
+
detected: boolean;
|
|
3
|
+
changesDir: string | null;
|
|
4
|
+
};
|
|
5
|
+
export declare function detectGraphify(projectRoot: string): {
|
|
6
|
+
detected: boolean;
|
|
7
|
+
graphDir: string | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function readAgentsFromConfig(projectRoot: string): string[] | null;
|
|
10
|
+
export declare function writeAgentsToConfig(agents: string[], projectRoot: string): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
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.detectOpenSpec = detectOpenSpec;
|
|
37
|
+
exports.detectGraphify = detectGraphify;
|
|
38
|
+
exports.readAgentsFromConfig = readAgentsFromConfig;
|
|
39
|
+
exports.writeAgentsToConfig = writeAgentsToConfig;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const yaml = __importStar(require("js-yaml"));
|
|
43
|
+
function detectOpenSpec(projectRoot) {
|
|
44
|
+
const detected = fs.existsSync(path.join(projectRoot, 'openspec'));
|
|
45
|
+
return { detected, changesDir: detected ? 'openspec/changes' : null };
|
|
46
|
+
}
|
|
47
|
+
function detectGraphify(projectRoot) {
|
|
48
|
+
const detected = fs.existsSync(path.join(projectRoot, 'graphify-out'));
|
|
49
|
+
return { detected, graphDir: detected ? 'graphify-out' : null };
|
|
50
|
+
}
|
|
51
|
+
function readAgentsFromConfig(projectRoot) {
|
|
52
|
+
const configPath = path.join(projectRoot, 'primer', 'config.yaml');
|
|
53
|
+
if (!fs.existsSync(configPath))
|
|
54
|
+
return null;
|
|
55
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
56
|
+
const config = yaml.load(content);
|
|
57
|
+
if (!config || !('agents' in config))
|
|
58
|
+
return null;
|
|
59
|
+
const agents = config['agents'];
|
|
60
|
+
if (!Array.isArray(agents))
|
|
61
|
+
return null;
|
|
62
|
+
return agents;
|
|
63
|
+
}
|
|
64
|
+
function writeAgentsToConfig(agents, projectRoot) {
|
|
65
|
+
const configPath = path.join(projectRoot, 'primer', 'config.yaml');
|
|
66
|
+
if (!fs.existsSync(configPath))
|
|
67
|
+
return;
|
|
68
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
69
|
+
const config = yaml.load(content);
|
|
70
|
+
config['agents'] = agents;
|
|
71
|
+
fs.writeFileSync(configPath, yaml.dump(config, { indent: 2 }), 'utf-8');
|
|
72
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Agent = 'claude' | 'cursor';
|
|
2
|
+
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
|
+
export declare function installAgentSkills(agent: Agent, projectRoot: string): void;
|
|
4
|
+
export declare const CLAUDE_SKILLS: Record<string, string>;
|
|
5
|
+
export declare const CLAUDE_COMMANDS: Record<string, string>;
|
|
6
|
+
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
7
|
+
export declare const CURSOR_COMMANDS: Record<string, string>;
|
|
@@ -0,0 +1,421 @@
|
|
|
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.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.SUPPORTED_AGENTS = void 0;
|
|
40
|
+
exports.installAgentSkills = installAgentSkills;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const scaffold_1 = require("./scaffold");
|
|
45
|
+
exports.SUPPORTED_AGENTS = ['claude', 'cursor'];
|
|
46
|
+
function installAgentSkills(agent, projectRoot) {
|
|
47
|
+
if (agent === 'claude') {
|
|
48
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
49
|
+
const dirCreated = !fs.existsSync(claudeDir);
|
|
50
|
+
const skillsBase = path.join(claudeDir, 'skills');
|
|
51
|
+
for (const [name, content] of Object.entries(exports.CLAUDE_SKILLS)) {
|
|
52
|
+
(0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
|
|
53
|
+
console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
|
|
54
|
+
}
|
|
55
|
+
const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
|
|
56
|
+
for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
|
|
57
|
+
(0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
|
|
58
|
+
console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
|
|
59
|
+
}
|
|
60
|
+
if (dirCreated) {
|
|
61
|
+
console.log(chalk_1.default.dim(' .claude/ created — Claude Code will discover these files automatically.'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (agent === 'cursor') {
|
|
65
|
+
const cursorDir = path.join(projectRoot, '.cursor');
|
|
66
|
+
const dirCreated = !fs.existsSync(cursorDir);
|
|
67
|
+
const skillsBase = path.join(cursorDir, 'skills');
|
|
68
|
+
for (const [name, content] of Object.entries(exports.CURSOR_SKILLS)) {
|
|
69
|
+
(0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
|
|
70
|
+
console.log(chalk_1.default.green('✓') + ` .cursor/skills/${name}/SKILL.md`);
|
|
71
|
+
}
|
|
72
|
+
const cmdsDir = path.join(cursorDir, 'commands');
|
|
73
|
+
for (const [filename, content] of Object.entries(exports.CURSOR_COMMANDS)) {
|
|
74
|
+
(0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
|
|
75
|
+
console.log(chalk_1.default.green('✓') + ` .cursor/commands/${filename}`);
|
|
76
|
+
}
|
|
77
|
+
if (dirCreated) {
|
|
78
|
+
console.log(chalk_1.default.dim(' .cursor/ created — Cursor will discover these files automatically.'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ─── Claude skill playbooks ───────────────────────────────────────────────────
|
|
83
|
+
exports.CLAUDE_SKILLS = {
|
|
84
|
+
'oprim-pdr': pdrSkill(),
|
|
85
|
+
'oprim-bet': betSkill(),
|
|
86
|
+
'oprim-criteria': criteriaSkill(),
|
|
87
|
+
'oprim-review': reviewSkill(),
|
|
88
|
+
};
|
|
89
|
+
// ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
|
|
90
|
+
exports.CLAUDE_COMMANDS = {
|
|
91
|
+
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
|
|
92
|
+
'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
|
|
93
|
+
'pdr.md': claudeWrapper('OPRIM: PDR', 'Create a new Product Decision Record with auto-assigned ID', 'Use the Skill tool to invoke the `oprim-pdr` skill.'),
|
|
94
|
+
'bet.md': claudeWrapper('OPRIM: Bet', 'Create a new bet decision and register it on the sequencing board', 'Use the Skill tool to invoke the `oprim-bet` skill.'),
|
|
95
|
+
'criteria.md': claudeWrapper('OPRIM: Criteria', 'Create or append to a criteria.yaml contract for a bet', 'Use the Skill tool to invoke the `oprim-criteria` skill.'),
|
|
96
|
+
'review.md': claudeWrapper('OPRIM: Review', "Create a KPI review artifact pre-filled from a bet's criteria contract", 'Use the Skill tool to invoke the `oprim-review` skill.'),
|
|
97
|
+
};
|
|
98
|
+
// ─── Cursor skill playbooks ───────────────────────────────────────────────────
|
|
99
|
+
exports.CURSOR_SKILLS = {
|
|
100
|
+
'oprim-pdr': pdrSkill(),
|
|
101
|
+
'oprim-bet': betSkill(),
|
|
102
|
+
'oprim-criteria': criteriaSkill(),
|
|
103
|
+
'oprim-review': reviewSkill(),
|
|
104
|
+
};
|
|
105
|
+
// ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
|
|
106
|
+
exports.CURSOR_COMMANDS = {
|
|
107
|
+
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
|
|
108
|
+
'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceContent()),
|
|
109
|
+
'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
|
|
110
|
+
'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
|
|
111
|
+
'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
|
|
112
|
+
'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
|
|
113
|
+
};
|
|
114
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
115
|
+
function claudeWrapper(name, description, body) {
|
|
116
|
+
return `---
|
|
117
|
+
name: "${name}"
|
|
118
|
+
description: ${description}
|
|
119
|
+
category: Workflow
|
|
120
|
+
tags: [workflow, primer]
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
${description}.
|
|
124
|
+
|
|
125
|
+
${body}`;
|
|
126
|
+
}
|
|
127
|
+
function cursorWrapper(id, description, body) {
|
|
128
|
+
return `---
|
|
129
|
+
name: /${id}
|
|
130
|
+
id: ${id}
|
|
131
|
+
category: Workflow
|
|
132
|
+
description: ${description}
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
${body}`;
|
|
136
|
+
}
|
|
137
|
+
// ─── Skill content ────────────────────────────────────────────────────────────
|
|
138
|
+
function pdrSkill() {
|
|
139
|
+
return `---
|
|
140
|
+
name: oprim-pdr
|
|
141
|
+
description: Create a new Product Decision Record in primer/decisions/ with auto-assigned ID and guided prompting
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Create a new Product Decision Record (PDR) in \`primer/decisions/\`.
|
|
145
|
+
|
|
146
|
+
## Steps
|
|
147
|
+
|
|
148
|
+
### 1. Get the decision title
|
|
149
|
+
If not provided, ask: "What is the title of this product decision?"
|
|
150
|
+
|
|
151
|
+
### 2. Assign the next PDR ID
|
|
152
|
+
Scan \`primer/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
|
|
153
|
+
Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
|
|
154
|
+
Output path: \`primer/decisions/PDR-NNN-<slug>.md\`
|
|
155
|
+
|
|
156
|
+
### 3. Gather content
|
|
157
|
+
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).
|
|
158
|
+
|
|
159
|
+
### 4. Check for supersession
|
|
160
|
+
Ask: "Does this supersede an existing PDR? If so, which ID? (Enter to skip)"
|
|
161
|
+
|
|
162
|
+
### 5. Write the PDR file
|
|
163
|
+
\`\`\`
|
|
164
|
+
# PDR-NNN: <title>
|
|
165
|
+
|
|
166
|
+
## Status
|
|
167
|
+
Proposed
|
|
168
|
+
|
|
169
|
+
## Context
|
|
170
|
+
<context>
|
|
171
|
+
|
|
172
|
+
## Decision
|
|
173
|
+
<decision>
|
|
174
|
+
|
|
175
|
+
## Alternatives considered
|
|
176
|
+
<alternatives as bullet list>
|
|
177
|
+
|
|
178
|
+
## Consequences
|
|
179
|
+
- Positive: <...>
|
|
180
|
+
- Trade-offs: <...>
|
|
181
|
+
- Follow-ups: <...>
|
|
182
|
+
|
|
183
|
+
## Evidence
|
|
184
|
+
<evidence or "None">
|
|
185
|
+
|
|
186
|
+
## Related
|
|
187
|
+
- Bets: <BET-IDs or "None">
|
|
188
|
+
- OpenSpec: <change paths or "None">
|
|
189
|
+
- Supersedes: <PDR-ID or "None">
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
### 6. Update superseded PDR (if applicable)
|
|
193
|
+
Read the superseded file → replace Status value with \`Superseded by PDR-NNN\` → write back.
|
|
194
|
+
|
|
195
|
+
### 7. Report what was created
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
function betSkill() {
|
|
199
|
+
return `---
|
|
200
|
+
name: oprim-bet
|
|
201
|
+
description: Create a new bet directory and bet-decision artifact in primer/bets/, and add the bet to primer/sequence.yaml backlog
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
Create a new bet in \`primer/bets/\` and register it on the sequencing board.
|
|
205
|
+
|
|
206
|
+
## Steps
|
|
207
|
+
|
|
208
|
+
### 1. Get the bet title
|
|
209
|
+
If not provided, ask: "What is the title of this bet?"
|
|
210
|
+
|
|
211
|
+
### 2. Assign the next BET ID
|
|
212
|
+
Scan \`primer/bets/\` for directories matching \`BET-(\\d+)$\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
|
|
213
|
+
|
|
214
|
+
### 3. Check sequence.yaml exists
|
|
215
|
+
If \`primer/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
|
|
216
|
+
|
|
217
|
+
### 4. Gather content
|
|
218
|
+
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).
|
|
219
|
+
|
|
220
|
+
### 5. Write primer/bets/BET-NNN/bet-decision.md
|
|
221
|
+
\`\`\`
|
|
222
|
+
# Decision: BET-NNN <title>
|
|
223
|
+
|
|
224
|
+
## Status
|
|
225
|
+
- Decision: <decision>
|
|
226
|
+
- Date: <today YYYY-MM-DD>
|
|
227
|
+
- Owner: <owner>
|
|
228
|
+
- Review date: <review date>
|
|
229
|
+
|
|
230
|
+
## Why now
|
|
231
|
+
<why-now as bullet list>
|
|
232
|
+
|
|
233
|
+
## Alternatives considered
|
|
234
|
+
<alternatives as bullet list>
|
|
235
|
+
|
|
236
|
+
## Expected outcomes
|
|
237
|
+
<outcomes as bullet list>
|
|
238
|
+
|
|
239
|
+
## Kill criteria / rollback trigger
|
|
240
|
+
<kill criteria as bullet list>
|
|
241
|
+
|
|
242
|
+
## Links
|
|
243
|
+
- PDRs: <PDR-IDs or "None">
|
|
244
|
+
- OpenSpec change: <to be filled when promoted>
|
|
245
|
+
\`\`\`
|
|
246
|
+
|
|
247
|
+
### 6. Append to primer/sequence.yaml backlog
|
|
248
|
+
Read → parse YAML → append → write back (2-space indentation):
|
|
249
|
+
\`\`\`yaml
|
|
250
|
+
- id: BET-NNN
|
|
251
|
+
title: "<title>"
|
|
252
|
+
blocked_by: []
|
|
253
|
+
unlocks: []
|
|
254
|
+
requires_pdrs: []
|
|
255
|
+
\`\`\`
|
|
256
|
+
|
|
257
|
+
### 7. Report what was created
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
function criteriaSkill() {
|
|
261
|
+
return `---
|
|
262
|
+
name: oprim-criteria
|
|
263
|
+
description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
Create or append to \`primer/bets/BET-NNN/criteria.yaml\`.
|
|
267
|
+
|
|
268
|
+
## Steps
|
|
269
|
+
|
|
270
|
+
### 1. Identify the bet
|
|
271
|
+
If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
|
|
272
|
+
|
|
273
|
+
### 2. Verify bet exists
|
|
274
|
+
If \`primer/bets/BET-NNN/\` not found: report and stop — advise \`/oprim:bet\` first.
|
|
275
|
+
|
|
276
|
+
### 3. Gather metric details
|
|
277
|
+
Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
|
|
278
|
+
|
|
279
|
+
### 4. Gather source mapping
|
|
280
|
+
Ask: source type (amplitude / bigquery)
|
|
281
|
+
|
|
282
|
+
If amplitude: event name, aggregation (unique_users / event_count / property_sum), denominator event (optional).
|
|
283
|
+
\`\`\`yaml
|
|
284
|
+
source:
|
|
285
|
+
type: amplitude
|
|
286
|
+
definition:
|
|
287
|
+
event: <event_name>
|
|
288
|
+
aggregation: <aggregation>
|
|
289
|
+
denominator_event: <event_name | null>
|
|
290
|
+
\`\`\`
|
|
291
|
+
|
|
292
|
+
If bigquery: table, metric column, SQL filter, aggregation (sum / count / count_distinct / avg), denominator query (optional).
|
|
293
|
+
\`\`\`yaml
|
|
294
|
+
source:
|
|
295
|
+
type: bigquery
|
|
296
|
+
definition:
|
|
297
|
+
table: "<project.dataset.table>"
|
|
298
|
+
metric_column: "<column>"
|
|
299
|
+
filter: "<sql_filter>"
|
|
300
|
+
aggregation: <aggregation>
|
|
301
|
+
denominator_query: <sql | null>
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
### 5. Build metric entry and write
|
|
305
|
+
If file exists: read → parse → append to \`metrics\` → write back (never overwrite).
|
|
306
|
+
If not: create with \`metrics:\` list.
|
|
307
|
+
|
|
308
|
+
### 6. Ask if more metrics needed. If yes, return to step 3.
|
|
309
|
+
|
|
310
|
+
### 7. Report what was created
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
function reviewSkill() {
|
|
314
|
+
return `---
|
|
315
|
+
name: oprim-review
|
|
316
|
+
description: Create a KPI review artifact for a completed bet, pre-filled from criteria.yaml with actuals gathered from the user
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
Create a KPI review in \`primer/reviews/\`.
|
|
320
|
+
|
|
321
|
+
## Steps
|
|
322
|
+
|
|
323
|
+
### 1. Identify the bet
|
|
324
|
+
If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
|
|
325
|
+
|
|
326
|
+
### 2. Load criteria and check for a run result
|
|
327
|
+
|
|
328
|
+
Read \`primer/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
|
|
329
|
+
If not found: inform user and continue with empty metrics list.
|
|
330
|
+
|
|
331
|
+
**Check for measurement run result:** Scan \`primer/bets/BET-NNN/measurements/\` for files matching \`run-*.yaml\`. If any exist, sort by filename (date-based) and read the most recent.
|
|
332
|
+
|
|
333
|
+
**If a run result exists:** use it to pre-populate actuals and status for every metric. Skip step 3 for those metrics. Note the run date — include "Actuals from run: YYYY-MM-DD" in the review artifact.
|
|
334
|
+
|
|
335
|
+
**If no run result exists:** proceed to step 3 to gather actuals manually.
|
|
336
|
+
|
|
337
|
+
### 3. Gather actuals per metric (only when no run result)
|
|
338
|
+
For each metric show name/baseline/target and ask: "What was the actual result? (number or 'pending')"
|
|
339
|
+
|
|
340
|
+
Status logic:
|
|
341
|
+
- actual >= target → \`hit\`
|
|
342
|
+
- actual < target → \`missed\`
|
|
343
|
+
- 'pending' or not provided → \`pending\`
|
|
344
|
+
|
|
345
|
+
### 4. Get review metadata
|
|
346
|
+
Ask: reviewer name, decision quality notes.
|
|
347
|
+
|
|
348
|
+
### 5. Output path
|
|
349
|
+
\`primer/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
|
|
350
|
+
|
|
351
|
+
### 6. Write the review file
|
|
352
|
+
\`\`\`markdown
|
|
353
|
+
# KPI Review: BET-NNN
|
|
354
|
+
|
|
355
|
+
**Review date:** YYYY-MM-DD
|
|
356
|
+
**Reviewed by:** <reviewer>
|
|
357
|
+
**Actuals from run:** YYYY-MM-DD ← include only when a run result was ingested
|
|
358
|
+
|
|
359
|
+
| Metric | Baseline | Target | Actual | Status |
|
|
360
|
+
|--------|----------|--------|--------|--------|
|
|
361
|
+
| <name> | <baseline> | <target> | <actual> | <status> |
|
|
362
|
+
|
|
363
|
+
## Decision quality
|
|
364
|
+
<notes>
|
|
365
|
+
|
|
366
|
+
## Actions
|
|
367
|
+
- [ ] Update bet-decision outcome section
|
|
368
|
+
- [ ] Update affected PDRs
|
|
369
|
+
- [ ] Re-sequence impacted bets
|
|
370
|
+
\`\`\`
|
|
371
|
+
|
|
372
|
+
### 7. Report what was created
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
// ─── Cursor inline content (condensed versions for command files) ─────────────
|
|
376
|
+
function pdrInlineContent() {
|
|
377
|
+
return `Create a new PDR in \`primer/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 \`primer/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
|
|
378
|
+
}
|
|
379
|
+
function betInlineContent() {
|
|
380
|
+
return `Create a new bet in \`primer/bets/\`. Scan \`BET-(\\d+)$\` dirs for next ID (zero-padded, default 001). Check \`primer/sequence.yaml\` exists (stop if not — advise oprim init). Gather: title, decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`primer/bets/BET-NNN/bet-decision.md\`. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Report what was created.`;
|
|
381
|
+
}
|
|
382
|
+
function criteriaInlineContent() {
|
|
383
|
+
return `Add metrics to \`primer/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.`;
|
|
384
|
+
}
|
|
385
|
+
function reviewInlineContent() {
|
|
386
|
+
return `Create KPI review in \`primer/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`primer/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.`;
|
|
387
|
+
}
|
|
388
|
+
// ─── Legacy content (promote / sequence remain inline) ───────────────────────
|
|
389
|
+
function promoteContent() {
|
|
390
|
+
return `
|
|
391
|
+
Promote a prioritized bet to an OpenSpec change and link criteria contracts.
|
|
392
|
+
|
|
393
|
+
**Input**: Specify a bet ID (e.g., \`/oprim:promote BET-042\`) or omit to be prompted.
|
|
394
|
+
|
|
395
|
+
**Steps**
|
|
396
|
+
|
|
397
|
+
1. **Locate the bet** — read \`primer/bets/BET-XXX/bet-decision.md\`
|
|
398
|
+
2. **Validate status** — decision must be "Build now"
|
|
399
|
+
3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
|
|
400
|
+
4. **Create OpenSpec change** — run \`openspec propose <change-name>\` or create change directory
|
|
401
|
+
5. **Link artifacts**:
|
|
402
|
+
- Add OpenSpec change path to bet-decision \`## Links\` section
|
|
403
|
+
- Add bet ID to OpenSpec proposal context
|
|
404
|
+
6. **Copy criteria** — if \`primer/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
|
|
405
|
+
7. **Report** — show what was linked and what remains for engineering
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
function sequenceContent() {
|
|
409
|
+
return `
|
|
410
|
+
Validate the primer sequencing board and suggest rebalancing if needed.
|
|
411
|
+
|
|
412
|
+
**Steps**
|
|
413
|
+
|
|
414
|
+
1. **Read board** — load \`primer/sequence.yaml\`
|
|
415
|
+
2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
|
|
416
|
+
3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
|
|
417
|
+
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`primer/decisions/\`
|
|
418
|
+
5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
|
|
419
|
+
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface AmplitudeDefinition {
|
|
2
|
+
event: string;
|
|
3
|
+
aggregation: string;
|
|
4
|
+
denominator_event?: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface BigQueryDefinition {
|
|
7
|
+
table: string;
|
|
8
|
+
metric_column: string;
|
|
9
|
+
filter: string;
|
|
10
|
+
aggregation: string;
|
|
11
|
+
denominator_query?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface CriteriaMetric {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
baseline: number;
|
|
17
|
+
target: number;
|
|
18
|
+
timeframe: string;
|
|
19
|
+
launch_date: string | null;
|
|
20
|
+
source: {
|
|
21
|
+
type: 'amplitude' | 'bigquery';
|
|
22
|
+
definition: AmplitudeDefinition | BigQueryDefinition;
|
|
23
|
+
};
|
|
24
|
+
segment?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface CriteriaFile {
|
|
27
|
+
metrics: CriteriaMetric[];
|
|
28
|
+
}
|
|
29
|
+
export interface DateWindow {
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MetricResult {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
source: string;
|
|
37
|
+
actual: number | null;
|
|
38
|
+
target: number;
|
|
39
|
+
status: 'hit' | 'missed' | 'pending';
|
|
40
|
+
notes: string | null;
|
|
41
|
+
}
|
|
42
|
+
export declare function computeDateWindow(launchDate: string | null, timeframe: string): DateWindow | null;
|
|
43
|
+
export declare function generateAmplitudeDefinition(metric: CriteriaMetric): Record<string, unknown>;
|
|
44
|
+
export declare function generateBigQuerySQL(metric: CriteriaMetric): string;
|
|
45
|
+
export declare function classifyStatus(actual: number | null, target: number): 'hit' | 'missed' | 'pending';
|
|
46
|
+
export declare function writeRunResult(betId: string, measurementsDir: string, results: MetricResult[], runDate: string): void;
|
|
47
|
+
export declare function runAmplitudeMetric(definitionPath: string, apiKey: string): Promise<{
|
|
48
|
+
actual: number | null;
|
|
49
|
+
notes: string | null;
|
|
50
|
+
}>;
|
|
51
|
+
export declare function runBigQueryMetric(sqlPath: string): Promise<{
|
|
52
|
+
actual: number | null;
|
|
53
|
+
notes: string | null;
|
|
54
|
+
}>;
|
|
55
|
+
export declare function scanCriteriaForSourceType(projectRoot: string, sourceType: string): boolean;
|