@open-product-primer/cli 2.1.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 (48) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/commands/validate.d.ts +2 -0
  3. package/dist/commands/validate.js +131 -0
  4. package/dist/lib/install-agent.js +78 -977
  5. package/dist/lib/spec-delta.d.ts +30 -0
  6. package/dist/lib/spec-delta.js +216 -0
  7. package/dist/lib/validate-checks.d.ts +13 -0
  8. package/dist/lib/validate-checks.js +145 -0
  9. package/dist/lib/workflow-renderer.d.ts +19 -0
  10. package/dist/lib/workflow-renderer.js +103 -0
  11. package/dist/lib/workflow-schema.d.ts +37 -0
  12. package/dist/lib/workflow-schema.js +126 -0
  13. package/dist/workflows/archive.inline.md +10 -0
  14. package/dist/workflows/archive.schema.yaml +13 -0
  15. package/dist/workflows/archive.template.md +106 -0
  16. package/dist/workflows/bet.cursor-command.md +1 -0
  17. package/dist/workflows/bet.inline.md +13 -0
  18. package/dist/workflows/bet.schema.yaml +14 -0
  19. package/dist/workflows/bet.template.md +114 -0
  20. package/dist/workflows/context.schema.yaml +13 -0
  21. package/dist/workflows/context.template.md +34 -0
  22. package/dist/workflows/criteria.cursor-command.md +1 -0
  23. package/dist/workflows/criteria.inline.md +11 -0
  24. package/dist/workflows/criteria.schema.yaml +14 -0
  25. package/dist/workflows/criteria.template.md +52 -0
  26. package/dist/workflows/note.cursor-command.md +1 -0
  27. package/dist/workflows/note.inline.md +11 -0
  28. package/dist/workflows/note.schema.yaml +14 -0
  29. package/dist/workflows/note.template.md +66 -0
  30. package/dist/workflows/pdr.cursor-command.md +1 -0
  31. package/dist/workflows/pdr.inline.md +10 -0
  32. package/dist/workflows/pdr.schema.yaml +14 -0
  33. package/dist/workflows/pdr.template.md +67 -0
  34. package/dist/workflows/promote.native.template.md +29 -0
  35. package/dist/workflows/promote.none.template.md +25 -0
  36. package/dist/workflows/promote.openspec.template.md +40 -0
  37. package/dist/workflows/promote.schema.yaml +14 -0
  38. package/dist/workflows/review.cursor-command.md +1 -0
  39. package/dist/workflows/review.inline.md +11 -0
  40. package/dist/workflows/review.schema.yaml +14 -0
  41. package/dist/workflows/review.template.md +69 -0
  42. package/dist/workflows/sequence.cursor-command.md +12 -0
  43. package/dist/workflows/sequence.inline.md +10 -0
  44. package/dist/workflows/sequence.schema.yaml +13 -0
  45. package/dist/workflows/sequence.template.md +88 -0
  46. package/dist/workflows/spec-authoring.schema.yaml +13 -0
  47. package/dist/workflows/spec-authoring.template.md +68 -0
  48. package/package.json +2 -2
@@ -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
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: oprim-bet
3
+ description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
4
+ ---
5
+
6
+ Create a new bet in `oprim/bets/` and register it on the sequencing board.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
9
+
10
+ ## What you're creating
11
+
12
+ 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 so you know when to stop.
13
+
14
+ ## Steps
15
+
16
+ ### 1. Get the bet title
17
+ Display the naming convention before asking:
18
+
19
+ > **Naming tip:** Use "verb + object [for context]"
20
+ > - Good: "Improve bet naming for scannability"
21
+ > - Bad: "Naming"
22
+
23
+ If not provided, ask: "What is the title of this bet?"
24
+
25
+ After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters:
26
+ - Show: "Warning: this title may be too vague to scan at a glance."
27
+ - Suggest a reformulation, e.g. "Consider: 'Improve <what> for <why>'"
28
+ - Ask: "Proceed with this title anyway? (y/N)"
29
+ - If "n" or Enter: ask for a revised title and re-validate
30
+ - If "y": proceed with the original title
31
+
32
+ ### 2. Assign the next BET ID
33
+ Scan both `oprim/bets/` and `oprim/bets/archived/` for directories whose names match `BET-(\d+)(-[^/]*)?` (handles both `BET-NNN/` and `BET-NNN-<slug>/`). Extract the numeric part from each match. Assign max+1, zero-padded to 3 digits. Default `001` if none found in either location.
34
+
35
+ ### 2b. Derive the slug
36
+ From the bet title: 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, truncate to 40 characters at the last hyphen boundary. This becomes `<slug>`. Example: "Add title slugs to bet directories for scannability" → `add-title-slugs-to-bet-dirs-for`.
37
+
38
+ ### 3. Check sequence.yaml exists
39
+ If `oprim/sequence.yaml` not found: report and stop — advise `oprim init`.
40
+
41
+ ### 3b. Check for custom rules
42
+ 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.
43
+
44
+ ### 4. Gather content
45
+ 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).
46
+
47
+ Then ask about reversibility:
48
+ - "Is this a **2-way door** (reversible — easy to undo, safe to try) or a **1-way door** (hard to reverse — requires high confidence)?"
49
+
50
+ Then ask about each of the four risk dimensions (Low / Medium / High + short rationale):
51
+ - "**Value risk**: Will users/customers actually use or buy this? (Low / Medium / High — and why?)"
52
+ - "**Usability risk**: Can users figure out how to use it without help? (Low / Medium / High — and why?)"
53
+ - "**Feasibility risk**: Can we build this with our current skills, time, and technology? (Low / Medium / High — and why?)"
54
+ - "**Business viability risk**: Does this solution work for the business (revenue, legal, ops)? (Low / Medium / High — and why?)"
55
+
56
+ ### 4b. Check for OKF frontmatter
57
+ Read `oprim/templates/bet-decision.md`. If it begins with a YAML frontmatter block (`---` ... `---`), this workspace has OKF frontmatter enabled. Ask for a one-line description and comma-separated tags (subject-area keywords). Prepare a frontmatter block with `type: bet-decision`, `title: <title>`, `description: <description>`, `tags: [<tags>]`, `timestamp: <today's date, ISO 8601>`, to prepend in step 5.
58
+ If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
59
+
60
+ ### 5. Write oprim/bets/BET-NNN-<slug>/bet-decision.md
61
+ Prepend the frontmatter block from step 4b, if one was prepared.
62
+ ```
63
+ # Decision: BET-NNN <title>
64
+ <!-- Naming tip: verb + object [for context] — e.g. "Improve bet naming for scannability" not "Naming" -->
65
+
66
+ ## Status
67
+ - Decision: <decision>
68
+ - Date: <today YYYY-MM-DD>
69
+ - Owner: <owner>
70
+ - Review date: <review date>
71
+
72
+ ## Door type
73
+ - [<x if 2-way>] 2-way door (reversible — safe to try, easy to undo)
74
+ - [<x if 1-way>] 1-way door (hard to reverse — requires higher confidence before committing)
75
+
76
+ ## Risk profile
77
+ - **Value risk**: <Low / Medium / High> — <rationale>
78
+ - **Usability risk**: <Low / Medium / High> — <rationale>
79
+ - **Feasibility risk**: <Low / Medium / High> — <rationale>
80
+ - **Business viability risk**: <Low / Medium / High> — <rationale>
81
+
82
+ ## Why now
83
+ <why-now as bullet list>
84
+
85
+ ## Alternatives considered
86
+ <alternatives as bullet list>
87
+
88
+ ## Expected outcomes
89
+ <outcomes as bullet list>
90
+
91
+ ## Kill criteria / rollback trigger
92
+ <kill criteria as bullet list>
93
+
94
+ ## Links
95
+ - PDRs: <PDR-IDs or "None">
96
+ - OpenSpec change: <to be filled when promoted>
97
+ ```
98
+
99
+ ### 6. Append to oprim/sequence.yaml backlog
100
+ Read → parse YAML → append → write back (2-space indentation):
101
+ ```yaml
102
+ - id: BET-NNN
103
+ title: "<title>"
104
+ blocked_by: []
105
+ unlocks: []
106
+ requires_pdrs: []
107
+ ```
108
+
109
+ ### 7. Prompt for optional discovery scaffolding
110
+ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
111
+ - If "y": write `oprim/bets/BET-NNN-<slug>/discovery.md` from the discovery template (same structure as `oprim/templates/discovery.md`).
112
+ - If "n" or Enter: skip silently.
113
+
114
+ ### 8. Report what was created
@@ -0,0 +1,13 @@
1
+ id: context
2
+ skillName: oprim-context-init
3
+ title: "OPRIM: Context Init"
4
+ description: Declare the current project a citable remote context, guided by a short Q&A to draft its description
5
+ claude:
6
+ skill: true
7
+ command: context-init.md
8
+ cursor:
9
+ skill: false
10
+ command: null
11
+ poolside:
12
+ skill: false
13
+ inline: false
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: oprim-context-init
3
+ description: Guide the user through drafting a description before declaring the current project a citable remote context
4
+ ---
5
+
6
+ Declare the current project a citable remote context, with a clear description other projects and agents can use to decide whether to pull it.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
9
+
10
+ ## What this does
11
+
12
+ A remote context is an oprim workspace (decisions, bets, specs) that other projects can reference read-only via `oprim context register`. Without a description, other projects have no cheap way to know what a remote context covers short of fully pulling it — so this skill exists to make sure one gets written.
13
+
14
+ ## Steps
15
+
16
+ ### 1. Check for an existing identity
17
+ Check whether `.oprim-context/context.yaml` already exists in the current project. If it does, report that a remote context identity already exists (do not re-run the drafting flow below) and stop.
18
+
19
+ ### 2. Ask what this workspace covers
20
+ Ask the user, one at a time:
21
+ - "What does this project's oprim workspace cover? (e.g. product decisions, a specific domain, a team's specs)"
22
+ - "Who is this meant for — which teams or projects would reference it?"
23
+
24
+ If the user declines to answer either question, treat that as opting out of guided drafting — skip to step 4 with no description.
25
+
26
+ ### 3. Draft and confirm the description
27
+ From the answers, draft a single-sentence description (aim for under 120 characters — this is what `oprim context list` will show other projects). Show the draft to the user and ask: "Use this description? (Enter to accept, or type a replacement)"
28
+
29
+ ### 4. Call oprim context init
30
+ - If a description was drafted or accepted: use the Bash tool to run `oprim context init --description "<final text>"`.
31
+ - If the user opted out in step 2: warn clearly that the resulting remote context will show as description-less in `oprim context list`, then use the Bash tool to run `oprim context init` with no `--description` flag.
32
+
33
+ ### 5. Report what was created
34
+ Report the path (`.oprim-context/context.yaml`) and the description that was set (or the description-less warning, if opted out).
@@ -0,0 +1 @@
1
+ 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.
@@ -0,0 +1,11 @@
1
+ ### Criteria authoring (oprim-criteria)
2
+ Create or append to `oprim/bets/BET-NNN/criteria.yaml`.
3
+
4
+ 1. Ask which bet (e.g. BET-042). Verify dir exists.
5
+ 2. Gather: metric ID (snake_case), name, baseline, target, timeframe, launch date, segment.
6
+ 3. Ask source type (amplitude / bigquery).
7
+ - Amplitude: event, aggregation (unique_users/event_count/property_sum), denominator_event.
8
+ - BigQuery: table, metric_column, filter, aggregation, denominator_query.
9
+ 4. If file exists: append to `metrics` list (never overwrite). If not: create.
10
+ 5. Ask if adding more metrics.
11
+ 6. Report what was created.
@@ -0,0 +1,14 @@
1
+ id: criteria
2
+ skillName: oprim-criteria
3
+ title: null
4
+ description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
5
+ cursorDescription: Create or append to a criteria.yaml contract for a bet
6
+ claude:
7
+ skill: true
8
+ command: null
9
+ cursor:
10
+ skill: true
11
+ command: oprim-criteria.md
12
+ poolside:
13
+ skill: true
14
+ inline: true
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: oprim-criteria
3
+ description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
4
+ ---
5
+
6
+ Create or append to `oprim/bets/BET-NNN/criteria.yaml`.
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. Identify the bet
13
+ If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
14
+
15
+ ### 2. Verify bet exists
16
+ If `oprim/bets/BET-NNN/` not found: report and stop — advise using the `oprim-bet` skill first.
17
+
18
+ ### 3. Gather metric details
19
+ Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
20
+
21
+ ### 4. Gather source mapping
22
+ Ask: source type (amplitude / bigquery)
23
+
24
+ If amplitude: event name, aggregation (unique_users / event_count / property_sum), denominator event (optional).
25
+ ```yaml
26
+ source:
27
+ type: amplitude
28
+ definition:
29
+ event: <event_name>
30
+ aggregation: <aggregation>
31
+ denominator_event: <event_name | null>
32
+ ```
33
+
34
+ If bigquery: table, metric column, SQL filter, aggregation (sum / count / count_distinct / avg), denominator query (optional).
35
+ ```yaml
36
+ source:
37
+ type: bigquery
38
+ definition:
39
+ table: "<project.dataset.table>"
40
+ metric_column: "<column>"
41
+ filter: "<sql_filter>"
42
+ aggregation: <aggregation>
43
+ denominator_query: <sql | null>
44
+ ```
45
+
46
+ ### 5. Build metric entry and write
47
+ If file exists: read → parse → append to `metrics` → write back (never overwrite).
48
+ If not: create with `metrics:` list.
49
+
50
+ ### 6. Ask if more metrics needed. If yes, return to step 3.
51
+
52
+ ### 7. Report what was created
@@ -0,0 +1 @@
1
+ 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.
@@ -0,0 +1,11 @@
1
+ ### Note authoring (oprim-note)
2
+ Create a new note in `oprim/notes/` for lightweight thinking capture — not a bet, no owner or kill criterion.
3
+
4
+ 1. Ask for a short title.
5
+ 2. Assign next NOTE ID: scan `oprim/notes/NOTE-(\d+)-`, max+1 zero-padded to 3 digits (default 001).
6
+ 3. Ask for the note body (free-form), tags, and optional related BET-IDs.
7
+ 4. Tags: check against `oprim/config.yaml`'s `notes.tags` — accept and append any new tag rather than rejecting it (the vocabulary grows from usage).
8
+ 5. Check `oprim/templates/note.md`: a `description:` field in its frontmatter means the OKF tier (gather a one-line description); no field means the minimal tier; if the file is missing, fall back to `okf.enabled` in `oprim/config.yaml`.
9
+ 6. Write `oprim/notes/NOTE-NNN-<slug>.md` with the correct frontmatter tier and a `## Bets` section.
10
+ 7. For each related bet, append `- Notes: NOTE-NNN` to that bet's `## Links` section.
11
+ 8. Report what was created.
@@ -0,0 +1,14 @@
1
+ id: note
2
+ skillName: oprim-note
3
+ title: null
4
+ description: Create a new atomic note in oprim/notes/ for lightweight thinking capture, with tiered frontmatter and optional bet links
5
+ cursorDescription: Create a new atomic note for lightweight thinking capture
6
+ claude:
7
+ skill: true
8
+ command: null
9
+ cursor:
10
+ skill: true
11
+ command: oprim-note.md
12
+ poolside:
13
+ skill: true
14
+ inline: true
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: oprim-note
3
+ description: Create a new atomic note in oprim/notes/ for lightweight thinking capture, with tiered frontmatter and optional bet links
4
+ ---
5
+
6
+ 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.
7
+
8
+ **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
9
+
10
+ ## What you're creating
11
+
12
+ A note is a small, disposable unit of thinking: an observation, a stray idea, or a connection between bets, captured before it's proven enough to belong in a discovery hypothesis or bet-decision. Notes carry no owner and no kill criterion — they're not commitments. Promote a note into a bet later with `/oprim:promote NOTE-NNN` once it's worth committing to.
13
+
14
+ ## Steps
15
+
16
+ ### 1. Get the note title
17
+ If not provided, ask: "What is this note about? (a short title)"
18
+
19
+ ### 2. Assign the next NOTE ID
20
+ Scan `oprim/notes/` for files matching `NOTE-(\d+)-`. Extract the numeric part from each match. Assign max+1, zero-padded to 3 digits. Default `001` if none found.
21
+
22
+ ### 2b. Derive the slug
23
+ From the note title: 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, truncate to 40 characters at the last hyphen boundary. This becomes `<slug>`.
24
+ Output path: `oprim/notes/NOTE-NNN-<slug>.md`
25
+
26
+ ### 3. Gather the note body
27
+ Ask: "What's the observation, idea, or connection?" (free-form prose — this becomes the note body).
28
+
29
+ ### 4. Gather tags
30
+ Read `oprim/config.yaml`. If it has a `notes:` section with a `tags:` list, show it and ask the user to pick from it or add new ones. If `notes.tags` is absent or empty, ask for tags directly (comma-separated) — there's no vocabulary yet to constrain against.
31
+ A tag not already in `notes.tags` SHALL be accepted, never rejected, and appended to `oprim/config.yaml`'s `notes.tags` list (creating the `notes:` section if absent) — the vocabulary grows from usage rather than requiring upfront authoring.
32
+
33
+ ### 5. Gather optional bet links
34
+ Ask: "Does this relate to any existing bets? (comma-separated BET-IDs, or Enter to skip)"
35
+
36
+ ### 6. Check the frontmatter tier
37
+ Read `oprim/templates/note.md`.
38
+ - If it exists and its frontmatter block contains a `description:` field, this workspace is on the **OKF tier** — ask for a one-line description.
39
+ - If it exists with no `description:` field, use the **minimal tier** — skip the description.
40
+ - If the file doesn't exist (project initialized before notes were introduced), read `oprim/config.yaml` directly: `okf.enabled: true` → OKF tier (ask for a description); otherwise → minimal tier.
41
+
42
+ ### 7. Write oprim/notes/NOTE-NNN-<slug>.md
43
+
44
+ Minimal tier:
45
+ ```
46
+ ---
47
+ type: note
48
+ title: "<title>"
49
+ tags: [<tags>]
50
+ timestamp: <today, ISO 8601>
51
+ ---
52
+
53
+ # Note: <title>
54
+
55
+ <body>
56
+
57
+ ## Bets
58
+ - <BET-IDs from step 5, or "None">
59
+ ```
60
+
61
+ OKF tier: same as above, with `description: "<description>"` inserted immediately after `title`.
62
+
63
+ ### 8. Link back from referenced bets
64
+ For each BET-ID gathered in step 5: read `oprim/bets/BET-NNN/bet-decision.md`, and add `- Notes: NOTE-NNN` under its `## Links` section (append to an existing `Notes:` line, or add a new one).
65
+
66
+ ### 9. Report what was created
@@ -0,0 +1 @@
1
+ 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.