@open-product-primer/cli 2.0.0 → 2.2.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.
Files changed (56) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/context.d.ts +2 -0
  4. package/dist/commands/context.js +188 -0
  5. package/dist/commands/doctor.js +49 -0
  6. package/dist/commands/validate.d.ts +2 -0
  7. package/dist/commands/validate.js +131 -0
  8. package/dist/lib/config-merge.js +5 -1
  9. package/dist/lib/install-agent.js +78 -938
  10. package/dist/lib/remote-context.d.ts +50 -0
  11. package/dist/lib/remote-context.js +299 -0
  12. package/dist/lib/spec-delta.d.ts +30 -0
  13. package/dist/lib/spec-delta.js +216 -0
  14. package/dist/lib/templates.js +2 -1
  15. package/dist/lib/validate-checks.d.ts +13 -0
  16. package/dist/lib/validate-checks.js +145 -0
  17. package/dist/lib/workflow-renderer.d.ts +19 -0
  18. package/dist/lib/workflow-renderer.js +103 -0
  19. package/dist/lib/workflow-schema.d.ts +37 -0
  20. package/dist/lib/workflow-schema.js +126 -0
  21. package/dist/workflows/archive.inline.md +10 -0
  22. package/dist/workflows/archive.schema.yaml +13 -0
  23. package/dist/workflows/archive.template.md +106 -0
  24. package/dist/workflows/bet.cursor-command.md +1 -0
  25. package/dist/workflows/bet.inline.md +13 -0
  26. package/dist/workflows/bet.schema.yaml +14 -0
  27. package/dist/workflows/bet.template.md +114 -0
  28. package/dist/workflows/context.schema.yaml +13 -0
  29. package/dist/workflows/context.template.md +34 -0
  30. package/dist/workflows/criteria.cursor-command.md +1 -0
  31. package/dist/workflows/criteria.inline.md +11 -0
  32. package/dist/workflows/criteria.schema.yaml +14 -0
  33. package/dist/workflows/criteria.template.md +52 -0
  34. package/dist/workflows/note.cursor-command.md +1 -0
  35. package/dist/workflows/note.inline.md +11 -0
  36. package/dist/workflows/note.schema.yaml +14 -0
  37. package/dist/workflows/note.template.md +66 -0
  38. package/dist/workflows/pdr.cursor-command.md +1 -0
  39. package/dist/workflows/pdr.inline.md +10 -0
  40. package/dist/workflows/pdr.schema.yaml +14 -0
  41. package/dist/workflows/pdr.template.md +67 -0
  42. package/dist/workflows/promote.native.template.md +29 -0
  43. package/dist/workflows/promote.none.template.md +25 -0
  44. package/dist/workflows/promote.openspec.template.md +40 -0
  45. package/dist/workflows/promote.schema.yaml +14 -0
  46. package/dist/workflows/review.cursor-command.md +1 -0
  47. package/dist/workflows/review.inline.md +11 -0
  48. package/dist/workflows/review.schema.yaml +14 -0
  49. package/dist/workflows/review.template.md +69 -0
  50. package/dist/workflows/sequence.cursor-command.md +12 -0
  51. package/dist/workflows/sequence.inline.md +10 -0
  52. package/dist/workflows/sequence.schema.yaml +13 -0
  53. package/dist/workflows/sequence.template.md +88 -0
  54. package/dist/workflows/spec-authoring.schema.yaml +13 -0
  55. package/dist/workflows/spec-authoring.template.md +68 -0
  56. package/package.json +2 -2
@@ -0,0 +1,145 @@
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.checkBetDefinitionOfDone = checkBetDefinitionOfDone;
37
+ exports.checkSpecDeltaDrift = checkSpecDeltaDrift;
38
+ exports.checkCrossBetConflicts = checkCrossBetConflicts;
39
+ const path = __importStar(require("path"));
40
+ const fs = __importStar(require("fs"));
41
+ const spec_delta_1 = require("./spec-delta");
42
+ function normalizeHeader(header) {
43
+ return header.trim().replace(/\s+/g, ' ').toLowerCase();
44
+ }
45
+ function extractBetId(dirName) {
46
+ const match = dirName.match(/^(BET-\d+)/);
47
+ return match ? match[1] : dirName;
48
+ }
49
+ function activeBetDirs(betsDir) {
50
+ if (!fs.existsSync(betsDir))
51
+ return [];
52
+ return fs
53
+ .readdirSync(betsDir, { withFileTypes: true })
54
+ .filter((e) => e.isDirectory() && e.name !== 'archived');
55
+ }
56
+ function isPromoted(betDecisionContent) {
57
+ const linksMatch = betDecisionContent.match(/## Links\n([\s\S]*?)(?=\n## |$)/);
58
+ const linksSection = linksMatch ? linksMatch[1] : betDecisionContent;
59
+ const lineMatch = linksSection.match(/^- *OpenSpec change:\s*(.*)$/m);
60
+ if (!lineMatch)
61
+ return false;
62
+ const value = lineMatch[1].trim().replace(/^<|>$/g, '').trim();
63
+ if (value === '')
64
+ return false;
65
+ if (/to be filled when promoted/i.test(value))
66
+ return false;
67
+ if (/^path once promoted$/i.test(value))
68
+ return false;
69
+ return true;
70
+ }
71
+ /**
72
+ * Flags a promoted bet (one whose bet-decision.md links a non-placeholder OpenSpec
73
+ * change) that has no criteria.yaml alongside it. Un-promoted bets are not checked.
74
+ */
75
+ function checkBetDefinitionOfDone(projectRoot, checks) {
76
+ const betsDir = path.join(projectRoot, 'oprim', 'bets');
77
+ for (const entry of activeBetDirs(betsDir)) {
78
+ const betDir = path.join(betsDir, entry.name);
79
+ const decisionPath = path.join(betDir, 'bet-decision.md');
80
+ if (!fs.existsSync(decisionPath))
81
+ continue;
82
+ const content = fs.readFileSync(decisionPath, 'utf-8');
83
+ if (!isPromoted(content))
84
+ continue;
85
+ const hasCriteria = fs.existsSync(path.join(betDir, 'criteria.yaml'));
86
+ if (!hasCriteria) {
87
+ const betId = extractBetId(entry.name);
88
+ checks.push({
89
+ name: `bet: ${betId} promoted without criteria.yaml`,
90
+ pass: false,
91
+ note: `Add criteria.yaml to oprim/bets/${entry.name}/, or run the oprim-criteria skill`,
92
+ required: false,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ /**
98
+ * Flags a MODIFIED/REMOVED requirement in an active bet's spec delta whose header no
99
+ * longer text-matches (whitespace-insensitive) current truth.
100
+ */
101
+ function checkSpecDeltaDrift(projectRoot, checks) {
102
+ const betsDir = path.join(projectRoot, 'oprim', 'bets');
103
+ for (const entry of activeBetDirs(betsDir)) {
104
+ const betId = extractBetId(entry.name);
105
+ const specsDir = path.join(betsDir, entry.name, 'specs');
106
+ if (!fs.existsSync(specsDir))
107
+ continue;
108
+ const capEntries = fs.readdirSync(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
109
+ for (const capEntry of capEntries) {
110
+ const deltaPath = path.join(specsDir, capEntry.name, 'spec.md');
111
+ if (!fs.existsSync(deltaPath))
112
+ continue;
113
+ const deltaContent = fs.readFileSync(deltaPath, 'utf-8');
114
+ const currentTruthPath = path.join(projectRoot, 'oprim', 'specs', capEntry.name, 'spec.md');
115
+ const currentHeaders = fs.existsSync(currentTruthPath)
116
+ ? (0, spec_delta_1.parseCurrentTruthHeaders)(fs.readFileSync(currentTruthPath, 'utf-8'))
117
+ : [];
118
+ for (const section of ['MODIFIED', 'REMOVED']) {
119
+ for (const header of (0, spec_delta_1.parseRequirementHeaders)(deltaContent, section)) {
120
+ const matches = currentHeaders.some((h) => normalizeHeader(h) === normalizeHeader(header));
121
+ if (!matches) {
122
+ checks.push({
123
+ name: `spec-delta: ${betId}'s ${section} requirement "${header}" in ${capEntry.name} no longer matches current truth`,
124
+ pass: false,
125
+ note: `No matching requirement header found in oprim/specs/${capEntry.name}/spec.md`,
126
+ required: true,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ /** Surfaces overlapping requirement headers across active bets' spec deltas. */
135
+ function checkCrossBetConflicts(projectRoot, checks) {
136
+ const betsDir = path.join(projectRoot, 'oprim', 'bets');
137
+ for (const conflict of (0, spec_delta_1.findCrossBetConflicts)(betsDir)) {
138
+ checks.push({
139
+ name: `spec-delta: ${extractBetId(conflict.betA)} and ${extractBetId(conflict.betB)} both touch "${conflict.header}" in ${conflict.capability}`,
140
+ pass: false,
141
+ note: 'Overlapping requirement header between active bets — archiving one first applies last-write-wins to the other (no 3-way merge)',
142
+ required: true,
143
+ });
144
+ }
145
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The full workflow body — a workflow's own frontmatter (`name:`/`description:`) plus its
3
+ * instructional steps — used verbatim as the Claude/Cursor/Poolside SKILL.md content.
4
+ */
5
+ export declare function renderSkillBody(id: string, projectRoot?: string): string;
6
+ /**
7
+ * A thin Claude command wrapper that delegates to the workflow's skill via the Skill tool
8
+ * (archive/sequence/context), or promote's dynamic per-framework content.
9
+ */
10
+ export declare function renderClaudeCommand(id: string, projectRoot?: string, framework?: string): string;
11
+ /**
12
+ * A full Cursor command file: promote's dynamic per-framework content, or a condensed
13
+ * `<id>.cursor-command.md` body (Cursor has no Skill tool, so content is always inlined).
14
+ */
15
+ export declare function renderCursorCommand(id: string, projectRoot?: string, framework?: string): string;
16
+ /** A workflow's condensed section for the shared Codex/Gemini/Poolside inline instruction block. */
17
+ export declare function renderInlineSection(id: string): string | null;
18
+ /** The full `<!-- oprim:start -->`-delimited inline instruction block written into AGENTS.md/GEMINI.md. */
19
+ export declare function renderAgentInstructions(): string;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderSkillBody = renderSkillBody;
4
+ exports.renderClaudeCommand = renderClaudeCommand;
5
+ exports.renderCursorCommand = renderCursorCommand;
6
+ exports.renderInlineSection = renderInlineSection;
7
+ exports.renderAgentInstructions = renderAgentInstructions;
8
+ const workflow_schema_1 = require("./workflow-schema");
9
+ // Fixed order the pre-refactor oprimWorkflowsInline() concatenated its per-workflow sections in —
10
+ // preserved here so Codex/Gemini/Poolside output is unchanged.
11
+ const INLINE_SECTION_ORDER = ['bet', 'note', 'criteria', 'pdr', 'review', 'archive', 'sequence'];
12
+ function claudeWrapper(name, description, body) {
13
+ return `---
14
+ name: "${name}"
15
+ description: ${description}
16
+ category: Workflow
17
+ tags: [workflow, primer]
18
+ ---
19
+
20
+ ${description}.
21
+
22
+ ${body}`;
23
+ }
24
+ function cursorWrapper(id, description, body) {
25
+ return `---
26
+ name: /${id}
27
+ id: ${id}
28
+ category: Workflow
29
+ description: ${description}
30
+ ---
31
+
32
+ ${body}`;
33
+ }
34
+ function promoteVariantBody(id, schema, framework) {
35
+ const variants = schema.variants;
36
+ if (!variants)
37
+ throw new Error(`workflow "${id}" has no variants configured`);
38
+ const variantId = variants.includes(framework) ? framework : variants[0];
39
+ const content = (0, workflow_schema_1.loadWorkflowVariant)(`${id}.${variantId}.template.md`);
40
+ if (content === null) {
41
+ throw new Error(`Missing variant template ${id}.${variantId}.template.md for workflow "${id}"`);
42
+ }
43
+ return content;
44
+ }
45
+ /**
46
+ * The full workflow body — a workflow's own frontmatter (`name:`/`description:`) plus its
47
+ * instructional steps — used verbatim as the Claude/Cursor/Poolside SKILL.md content.
48
+ */
49
+ function renderSkillBody(id, projectRoot) {
50
+ return (0, workflow_schema_1.loadWorkflowTemplate)(id, projectRoot);
51
+ }
52
+ /**
53
+ * A thin Claude command wrapper that delegates to the workflow's skill via the Skill tool
54
+ * (archive/sequence/context), or promote's dynamic per-framework content.
55
+ */
56
+ function renderClaudeCommand(id, projectRoot, framework = 'openspec') {
57
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
58
+ if (!schema.claude.command) {
59
+ throw new Error(`workflow "${id}" has no Claude command target`);
60
+ }
61
+ const body = schema.variants
62
+ ? promoteVariantBody(id, schema, framework)
63
+ : schema.skillName
64
+ ? `Use the Skill tool to invoke the \`${schema.skillName}\` skill.`
65
+ : (() => {
66
+ throw new Error(`workflow "${id}" Claude command has neither a skillName nor variants`);
67
+ })();
68
+ return claudeWrapper(schema.title ?? schema.id, schema.description, body);
69
+ }
70
+ /**
71
+ * A full Cursor command file: promote's dynamic per-framework content, or a condensed
72
+ * `<id>.cursor-command.md` body (Cursor has no Skill tool, so content is always inlined).
73
+ */
74
+ function renderCursorCommand(id, projectRoot, framework = 'openspec') {
75
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
76
+ if (!schema.cursor.command) {
77
+ throw new Error(`workflow "${id}" has no Cursor command target`);
78
+ }
79
+ const cursorId = schema.cursor.command.replace(/\.md$/, '');
80
+ const body = schema.variants
81
+ ? promoteVariantBody(id, schema, framework)
82
+ : (() => {
83
+ const content = (0, workflow_schema_1.loadWorkflowVariant)(`${id}.cursor-command.md`);
84
+ if (content === null)
85
+ throw new Error(`Missing ${id}.cursor-command.md for workflow "${id}"`);
86
+ return content;
87
+ })();
88
+ return cursorWrapper(cursorId, schema.cursorDescription ?? schema.description, body);
89
+ }
90
+ /** A workflow's condensed section for the shared Codex/Gemini/Poolside inline instruction block. */
91
+ function renderInlineSection(id) {
92
+ return (0, workflow_schema_1.loadWorkflowVariant)(`${id}.inline.md`);
93
+ }
94
+ /** The full `<!-- oprim:start -->`-delimited inline instruction block written into AGENTS.md/GEMINI.md. */
95
+ function renderAgentInstructions() {
96
+ const sections = INLINE_SECTION_ORDER.map((id) => {
97
+ const content = renderInlineSection(id);
98
+ if (content === null)
99
+ throw new Error(`Missing inline section for workflow "${id}"`);
100
+ return content;
101
+ });
102
+ return '\n## oprim workflows\n\n' + sections.join('\n\n') + '\n';
103
+ }
@@ -0,0 +1,37 @@
1
+ export interface WorkflowAgentTarget {
2
+ skill: boolean;
3
+ command: string | null;
4
+ }
5
+ export interface WorkflowSchema {
6
+ id: string;
7
+ skillName: string | null;
8
+ title: string | null;
9
+ description: string;
10
+ cursorDescription: string | null;
11
+ claude: WorkflowAgentTarget;
12
+ cursor: WorkflowAgentTarget;
13
+ poolside: {
14
+ skill: boolean;
15
+ };
16
+ inline: boolean;
17
+ variants: string[] | null;
18
+ }
19
+ /**
20
+ * Loads a workflow's schema.yaml, resolving a project-level
21
+ * `oprim/workflows/<id>.schema.yaml` override over the CLI-bundled default.
22
+ * Throws an actionable, file-naming error if an override exists but fails to parse
23
+ * or is missing required fields — never silently falls back to the bundled default.
24
+ */
25
+ export declare function loadWorkflowSchema(id: string, projectRoot?: string): WorkflowSchema;
26
+ /**
27
+ * Loads a workflow's template.md, resolving a project-level
28
+ * `oprim/workflows/<id>.template.md` override over the CLI-bundled default.
29
+ */
30
+ export declare function loadWorkflowTemplate(id: string, projectRoot?: string): string;
31
+ /**
32
+ * Loads a bundled, non-overridable variant file (Cursor's condensed command body, the
33
+ * Codex/Gemini/Poolside inline section, promote's per-framework bodies). Only
34
+ * `<id>.schema.yaml` and `<id>.template.md` are project-forkable — see
35
+ * workflow-schema-authoring spec. Returns null if the workflow has no such variant.
36
+ */
37
+ export declare function loadWorkflowVariant(filename: string): string | null;
@@ -0,0 +1,126 @@
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.loadWorkflowSchema = loadWorkflowSchema;
37
+ exports.loadWorkflowTemplate = loadWorkflowTemplate;
38
+ exports.loadWorkflowVariant = loadWorkflowVariant;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const yaml = __importStar(require("js-yaml"));
42
+ // Bundled workflow definitions ship inside the compiled package at dist/workflows/ (copied
43
+ // there at build time by scripts/copy-workflow-assets.js — see package.json's "build" script).
44
+ const BUNDLED_DIR = path.join(__dirname, '..', 'workflows');
45
+ function overridesDir(projectRoot) {
46
+ return path.join(projectRoot, 'oprim', 'workflows');
47
+ }
48
+ function requireField(obj, field, filePath) {
49
+ if (!(field in obj) || obj[field] === undefined || obj[field] === null) {
50
+ throw new Error(`Invalid workflow schema at ${filePath}: missing required field "${field}"`);
51
+ }
52
+ return obj[field];
53
+ }
54
+ function parseSchema(raw, filePath) {
55
+ let doc;
56
+ try {
57
+ doc = yaml.load(raw);
58
+ }
59
+ catch (err) {
60
+ throw new Error(`Invalid workflow schema at ${filePath}: ${err.message}`);
61
+ }
62
+ if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
63
+ throw new Error(`Invalid workflow schema at ${filePath}: expected a YAML mapping`);
64
+ }
65
+ const obj = doc;
66
+ requireField(obj, 'id', filePath);
67
+ requireField(obj, 'description', filePath);
68
+ const claude = requireField(obj, 'claude', filePath);
69
+ const cursor = requireField(obj, 'cursor', filePath);
70
+ const poolside = requireField(obj, 'poolside', filePath);
71
+ requireField(obj, 'inline', filePath);
72
+ return {
73
+ id: obj.id,
74
+ skillName: obj.skillName ?? null,
75
+ title: obj.title ?? null,
76
+ description: obj.description,
77
+ cursorDescription: obj.cursorDescription ?? null,
78
+ claude: { skill: Boolean(claude.skill), command: claude.command ?? null },
79
+ cursor: { skill: Boolean(cursor.skill), command: cursor.command ?? null },
80
+ poolside: { skill: Boolean(poolside.skill) },
81
+ inline: Boolean(obj.inline),
82
+ variants: obj.variants ?? null,
83
+ };
84
+ }
85
+ /**
86
+ * Loads a workflow's schema.yaml, resolving a project-level
87
+ * `oprim/workflows/<id>.schema.yaml` override over the CLI-bundled default.
88
+ * Throws an actionable, file-naming error if an override exists but fails to parse
89
+ * or is missing required fields — never silently falls back to the bundled default.
90
+ */
91
+ function loadWorkflowSchema(id, projectRoot) {
92
+ if (projectRoot) {
93
+ const overridePath = path.join(overridesDir(projectRoot), `${id}.schema.yaml`);
94
+ if (fs.existsSync(overridePath)) {
95
+ return parseSchema(fs.readFileSync(overridePath, 'utf-8'), overridePath);
96
+ }
97
+ }
98
+ const bundledPath = path.join(BUNDLED_DIR, `${id}.schema.yaml`);
99
+ return parseSchema(fs.readFileSync(bundledPath, 'utf-8'), bundledPath);
100
+ }
101
+ /**
102
+ * Loads a workflow's template.md, resolving a project-level
103
+ * `oprim/workflows/<id>.template.md` override over the CLI-bundled default.
104
+ */
105
+ function loadWorkflowTemplate(id, projectRoot) {
106
+ if (projectRoot) {
107
+ const overridePath = path.join(overridesDir(projectRoot), `${id}.template.md`);
108
+ if (fs.existsSync(overridePath)) {
109
+ return fs.readFileSync(overridePath, 'utf-8');
110
+ }
111
+ }
112
+ const bundledPath = path.join(BUNDLED_DIR, `${id}.template.md`);
113
+ return fs.readFileSync(bundledPath, 'utf-8');
114
+ }
115
+ /**
116
+ * Loads a bundled, non-overridable variant file (Cursor's condensed command body, the
117
+ * Codex/Gemini/Poolside inline section, promote's per-framework bodies). Only
118
+ * `<id>.schema.yaml` and `<id>.template.md` are project-forkable — see
119
+ * workflow-schema-authoring spec. Returns null if the workflow has no such variant.
120
+ */
121
+ function loadWorkflowVariant(filename) {
122
+ const p = path.join(BUNDLED_DIR, filename);
123
+ if (!fs.existsSync(p))
124
+ return null;
125
+ return fs.readFileSync(p, 'utf-8');
126
+ }
@@ -0,0 +1,10 @@
1
+ ### Bet archiving (oprim-archive)
2
+ Archive a completed bet.
3
+
4
+ 1. Ask for bet ID (accept bet-005, 005, 5, BET-005 — normalize to BET-NNN).
5
+ 2. Verify `oprim/bets/BET-NNN/` exists.
6
+ 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.
7
+ 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.
8
+ 5. Move directory: `oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN`.
9
+ 6. Remove the bet entry from `oprim/sequence.yaml`.
10
+ 7. Report what was done.
@@ -0,0 +1,13 @@
1
+ id: archive
2
+ skillName: oprim-archive
3
+ title: "OPRIM: Archive"
4
+ description: Archive a completed bet — move it out of the active board
5
+ claude:
6
+ skill: true
7
+ command: archive.md
8
+ cursor:
9
+ skill: false
10
+ command: null
11
+ poolside:
12
+ skill: true
13
+ inline: true
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: oprim-archive
3
+ 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
4
+ ---
5
+
6
+ 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.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
9
+
10
+ ## Steps
11
+
12
+ ### 1. Get the bet ID
13
+
14
+ If provided as an argument (e.g., `/oprim:archive BET-005`), use it directly.
15
+
16
+ If not provided, ask: "Which bet ID would you like to archive? (e.g., BET-005)"
17
+
18
+ Normalize the input: accept `bet-005`, `005`, `5`, or `BET-005` — always treat as `BET-NNN` zero-padded to 3 digits.
19
+
20
+ ### 2. Resolve the bet directory
21
+
22
+ Look for the bet directory in `oprim/bets/` using two patterns:
23
+ 1. Exact match: `oprim/bets/BET-NNN/` (legacy non-slug format)
24
+ 2. Slug variant: any directory starting with `BET-NNN-` (e.g., `BET-NNN-<slug>/`)
25
+
26
+ Use whichever pattern matches. Call this the **resolved directory name**.
27
+
28
+ If multiple directories match (e.g., both `BET-NNN/` and `BET-NNN-slug/` exist):
29
+ - Report: "Ambiguous: found multiple directories for BET-NNN: [list them]. Please archive manually."
30
+ - Stop.
31
+
32
+ If neither pattern matches:
33
+ - Report: "Bet BET-NNN was not found in oprim/bets/. Nothing was changed."
34
+ - Stop.
35
+
36
+ ### 3. Check for active dependencies and concurrent spec-delta conflicts
37
+
38
+ 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.
39
+
40
+ 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**.
41
+
42
+ If either sequence.yaml dependents or delta overlaps are found:
43
+ - Show a combined warning listing each dependent entry and each overlapping requirement.
44
+
45
+ Example:
46
+ ```
47
+ ⚠ Warning: BET-005 is referenced by active bets:
48
+ - BET-007 (blocked_by: [BET-005])
49
+ - BET-008 (unlocks: [BET-005])
50
+ ⚠ 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).
51
+ ```
52
+ - Ask: "Archive BET-NNN anyway? (y/N)"
53
+ - If "n" or Enter: stop, no changes made.
54
+ - If "y": proceed.
55
+
56
+ If neither is found: proceed without warning.
57
+
58
+ ### 4. Fold spec deltas into current truth
59
+
60
+ 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.
61
+
62
+ Otherwise, for each capability subdirectory under `oprim/bets/<resolved-dir>/specs/` containing a `spec.md`:
63
+
64
+ 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.
65
+ 2. Read `oprim/specs/<capability>/spec.md` if it exists (current truth uses a single flat `## Requirements` section).
66
+ - **If it does not exist:**
67
+ - 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.
68
+ - 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."
69
+ - **If it does exist:**
70
+ - **ADDED**: append the requirement block to the end of the `## Requirements` section.
71
+ - **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.
72
+ - **REMOVED**: find and delete the matching block entirely. If no match is found, note this in the final report and continue — nothing to remove.
73
+ 3. Write the updated `oprim/specs/<capability>/spec.md`.
74
+
75
+ 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).
76
+
77
+ Track which capabilities were merged (and any no-match notes) for the final report.
78
+
79
+ ### 5. Move the bet directory to archive
80
+
81
+ Create the archive subfolder if it doesn't exist:
82
+ ```bash
83
+ mkdir -p oprim/bets/archived
84
+ ```
85
+
86
+ Move the resolved directory:
87
+ ```bash
88
+ mv oprim/bets/<resolved-dir> oprim/bets/archived/<resolved-dir>
89
+ ```
90
+
91
+ ### 6. Remove the bet entry from sequence.yaml
92
+
93
+ 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.
94
+
95
+ ### 7. Report what was done
96
+
97
+ ```
98
+ ## Bet Archived
99
+
100
+ **Bet:** BET-NNN
101
+ **Archived to:** oprim/bets/archived/<resolved-dir>/
102
+ **Removed from sequence.yaml:** ✓
103
+ **Spec deltas merged:** <capability-1>, <capability-2> (omit this line if no specs/ directory was present)
104
+
105
+ The bet is preserved in full at the archive location.
106
+ ```
@@ -0,0 +1 @@
1
+ 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.
@@ -0,0 +1,13 @@
1
+ ### Bet authoring (oprim-bet)
2
+ Create a new bet in `oprim/bets/` and register it on the sequencing board.
3
+
4
+ 1. Show naming tip: "verb + object [for context] — e.g. 'Improve bet naming for scannability'"
5
+ 2. Ask for the bet title. Validate: fewer than 4 words OR fewer than 25 chars → warn, suggest reformulation, ask "Proceed anyway? (y/N)".
6
+ 3. Assign next BET ID: scan `oprim/bets/BET-(\d+)` dirs, max+1 zero-padded to 3 digits (default 001).
7
+ 4. Check `oprim/sequence.yaml` exists — stop if not, advise `oprim init`.
8
+ 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.
9
+ 5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
10
+ 6. Write `oprim/bets/BET-NNN/bet-decision.md` with all fields.
11
+ 7. Append to `oprim/sequence.yaml` backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`.
12
+ 8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/BET-NNN/discovery.md`.
13
+ 9. Report what was created.
@@ -0,0 +1,14 @@
1
+ id: bet
2
+ skillName: oprim-bet
3
+ title: null
4
+ description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
5
+ cursorDescription: Create a new bet decision and register it on the sequencing board
6
+ claude:
7
+ skill: true
8
+ command: null
9
+ cursor:
10
+ skill: true
11
+ command: oprim-bet.md
12
+ poolside:
13
+ skill: true
14
+ inline: true