@open-product-primer/cli 2.1.0 → 2.3.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 (52) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/commands/doctor.js +1 -1
  3. package/dist/commands/init.js +2 -2
  4. package/dist/commands/ovw.js +1 -1
  5. package/dist/commands/validate.d.ts +2 -0
  6. package/dist/commands/validate.js +131 -0
  7. package/dist/lib/install-agent.js +78 -977
  8. package/dist/lib/measure.js +1 -3
  9. package/dist/lib/spec-delta.d.ts +30 -0
  10. package/dist/lib/spec-delta.js +216 -0
  11. package/dist/lib/validate-checks.d.ts +13 -0
  12. package/dist/lib/validate-checks.js +145 -0
  13. package/dist/lib/workflow-renderer.d.ts +19 -0
  14. package/dist/lib/workflow-renderer.js +103 -0
  15. package/dist/lib/workflow-schema.d.ts +37 -0
  16. package/dist/lib/workflow-schema.js +126 -0
  17. package/dist/workflows/archive.inline.md +10 -0
  18. package/dist/workflows/archive.schema.yaml +13 -0
  19. package/dist/workflows/archive.template.md +109 -0
  20. package/dist/workflows/bet.cursor-command.md +1 -0
  21. package/dist/workflows/bet.inline.md +13 -0
  22. package/dist/workflows/bet.schema.yaml +14 -0
  23. package/dist/workflows/bet.template.md +114 -0
  24. package/dist/workflows/context.schema.yaml +13 -0
  25. package/dist/workflows/context.template.md +34 -0
  26. package/dist/workflows/criteria.cursor-command.md +1 -0
  27. package/dist/workflows/criteria.inline.md +11 -0
  28. package/dist/workflows/criteria.schema.yaml +14 -0
  29. package/dist/workflows/criteria.template.md +52 -0
  30. package/dist/workflows/note.cursor-command.md +1 -0
  31. package/dist/workflows/note.inline.md +11 -0
  32. package/dist/workflows/note.schema.yaml +14 -0
  33. package/dist/workflows/note.template.md +66 -0
  34. package/dist/workflows/pdr.cursor-command.md +1 -0
  35. package/dist/workflows/pdr.inline.md +10 -0
  36. package/dist/workflows/pdr.schema.yaml +14 -0
  37. package/dist/workflows/pdr.template.md +67 -0
  38. package/dist/workflows/promote.native.template.md +29 -0
  39. package/dist/workflows/promote.none.template.md +25 -0
  40. package/dist/workflows/promote.openspec.template.md +40 -0
  41. package/dist/workflows/promote.schema.yaml +14 -0
  42. package/dist/workflows/review.cursor-command.md +1 -0
  43. package/dist/workflows/review.inline.md +11 -0
  44. package/dist/workflows/review.schema.yaml +14 -0
  45. package/dist/workflows/review.template.md +69 -0
  46. package/dist/workflows/sequence.cursor-command.md +12 -0
  47. package/dist/workflows/sequence.inline.md +10 -0
  48. package/dist/workflows/sequence.schema.yaml +13 -0
  49. package/dist/workflows/sequence.template.md +88 -0
  50. package/dist/workflows/spec-authoring.schema.yaml +13 -0
  51. package/dist/workflows/spec-authoring.template.md +82 -0
  52. package/package.json +2 -2
@@ -54,6 +54,8 @@ const chalk_1 = __importDefault(require("chalk"));
54
54
  const scaffold_1 = require("./scaffold");
55
55
  const detect_1 = require("./detect");
56
56
  const config_merge_1 = require("./config-merge");
57
+ const workflow_schema_1 = require("./workflow-schema");
58
+ const workflow_renderer_1 = require("./workflow-renderer");
57
59
  exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'poolside'];
58
60
  // oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
59
61
  // selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
@@ -136,6 +138,21 @@ async function promptOkfFrontmatter() {
136
138
  default: false,
137
139
  });
138
140
  }
141
+ // Workflow ids installed as Claude/Poolside/Cursor skill files — see packages/cli/src/workflows/.
142
+ // Order matches the pre-refactor CLAUDE_SKILLS/POOLSIDE_SKILLS/CURSOR_SKILLS declaration order.
143
+ const CLAUDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'context'];
144
+ const POOLSIDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence'];
145
+ const CURSOR_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review'];
146
+ // Claude command wrappers (thin, invoke skill) — filename -> workflow id. Order matches the
147
+ // pre-refactor CLAUDE_COMMANDS declaration order.
148
+ const CLAUDE_COMMAND_WORKFLOWS = [
149
+ { filename: 'promote.md', id: 'promote' },
150
+ { filename: 'sequence.md', id: 'sequence' },
151
+ { filename: 'archive.md', id: 'archive' },
152
+ { filename: 'context-init.md', id: 'context' },
153
+ ];
154
+ // Cursor command files (full inline). Order matches the pre-refactor CURSOR_COMMANDS declaration order.
155
+ const CURSOR_COMMAND_WORKFLOW_IDS = ['promote', 'sequence', 'pdr', 'bet', 'note', 'criteria', 'review'];
139
156
  function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfacing = false) {
140
157
  if (agent === 'claude') {
141
158
  const claudeDir = path.join(projectRoot, '.claude');
@@ -156,15 +173,20 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
156
173
  console.log(chalk_1.default.dim(' removed .claude/skills/oprim:context/SKILL.md'));
157
174
  }
158
175
  // oprim skills — prepend Step 0 when opted in
159
- for (const [name, content] of Object.entries(exports.CLAUDE_SKILLS)) {
160
- const skillContent = pdrSurfacing ? withContextStep(content) : content;
161
- (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), skillContent);
162
- console.log(chalk_1.default.green('✓') + ` .claude/skills/${name}/SKILL.md`);
176
+ for (const id of CLAUDE_SKILL_WORKFLOW_IDS) {
177
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
178
+ if (!schema.claude.skill || !schema.skillName)
179
+ continue;
180
+ const body = (0, workflow_renderer_1.renderSkillBody)(id, projectRoot);
181
+ const skillContent = pdrSurfacing ? withContextStep(body) : body;
182
+ (0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), skillContent);
183
+ console.log(chalk_1.default.green('✓') + ` .claude/skills/${schema.skillName}/SKILL.md`);
163
184
  }
164
185
  // oprim-spec (native spec authoring) — install only when spec_framework is native, remove otherwise
165
186
  const specSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
166
187
  if (framework === 'native') {
167
- const specSkillContent = pdrSurfacing ? withContextStep(specAuthoringSkill()) : specAuthoringSkill();
188
+ const specBody = (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot);
189
+ const specSkillContent = pdrSurfacing ? withContextStep(specBody) : specBody;
168
190
  (0, scaffold_1.writeFile)(specSkillPath, specSkillContent);
169
191
  console.log(chalk_1.default.green('✓') + ' .claude/skills/oprim-spec/SKILL.md');
170
192
  }
@@ -190,13 +212,9 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
190
212
  }
191
213
  }
192
214
  const cmdsDir = path.join(claudeDir, 'commands', 'oprim');
193
- for (const [filename, content] of Object.entries(exports.CLAUDE_COMMANDS)) {
194
- // promote.md is regenerated per-project since its content branches on the selected
195
- // speccing framework — the static CLAUDE_COMMANDS entry only reflects the default.
196
- const finalContent = filename === 'promote.md'
197
- ? claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent(framework))
198
- : content;
199
- (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), finalContent);
215
+ for (const { filename, id } of CLAUDE_COMMAND_WORKFLOWS) {
216
+ const content = (0, workflow_renderer_1.renderClaudeCommand)(id, projectRoot, framework);
217
+ (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), content);
200
218
  console.log(chalk_1.default.green('✓') + ` .claude/commands/oprim/${filename}`);
201
219
  }
202
220
  // Tombstone cleanup: remove command wrappers deleted in v0.2.0 (bet/criteria/pdr/review
@@ -236,13 +254,16 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
236
254
  const poolsideDir = path.join(projectRoot, '.poolside');
237
255
  const dirCreated = !fs.existsSync(poolsideDir);
238
256
  const skillsBase = path.join(poolsideDir, 'skills');
239
- for (const [name, content] of Object.entries(exports.POOLSIDE_SKILLS)) {
240
- (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
241
- console.log(chalk_1.default.green('✓') + ` .poolside/skills/${name}/SKILL.md`);
257
+ for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
258
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
259
+ if (!schema.poolside.skill || !schema.skillName)
260
+ continue;
261
+ (0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
262
+ console.log(chalk_1.default.green('✓') + ` .poolside/skills/${schema.skillName}/SKILL.md`);
242
263
  }
243
264
  const poolsideSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
244
265
  if (framework === 'native') {
245
- (0, scaffold_1.writeFile)(poolsideSpecSkillPath, specAuthoringSkill());
266
+ (0, scaffold_1.writeFile)(poolsideSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
246
267
  console.log(chalk_1.default.green('✓') + ' .poolside/skills/oprim-spec/SKILL.md');
247
268
  }
248
269
  else if (fs.existsSync(poolsideSpecSkillPath)) {
@@ -274,13 +295,16 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
274
295
  const cursorDir = path.join(projectRoot, '.cursor');
275
296
  const dirCreated = !fs.existsSync(cursorDir);
276
297
  const skillsBase = path.join(cursorDir, 'skills');
277
- for (const [name, content] of Object.entries(exports.CURSOR_SKILLS)) {
278
- (0, scaffold_1.writeFile)(path.join(skillsBase, name, 'SKILL.md'), content);
279
- console.log(chalk_1.default.green('✓') + ` .cursor/skills/${name}/SKILL.md`);
298
+ for (const id of CURSOR_SKILL_WORKFLOW_IDS) {
299
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
300
+ if (!schema.cursor.skill || !schema.skillName)
301
+ continue;
302
+ (0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
303
+ console.log(chalk_1.default.green('✓') + ` .cursor/skills/${schema.skillName}/SKILL.md`);
280
304
  }
281
305
  const cursorSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
282
306
  if (framework === 'native') {
283
- (0, scaffold_1.writeFile)(cursorSpecSkillPath, specAuthoringSkill());
307
+ (0, scaffold_1.writeFile)(cursorSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
284
308
  console.log(chalk_1.default.green('✓') + ' .cursor/skills/oprim-spec/SKILL.md');
285
309
  }
286
310
  else if (fs.existsSync(cursorSpecSkillPath)) {
@@ -292,14 +316,13 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
292
316
  console.log(chalk_1.default.dim(' removed .cursor/skills/oprim-spec/SKILL.md'));
293
317
  }
294
318
  const cmdsDir = path.join(cursorDir, 'commands');
295
- for (const [filename, content] of Object.entries(exports.CURSOR_COMMANDS)) {
296
- // oprim-promote.md is regenerated per-project since its content branches on the
297
- // selected speccing framework — the static CURSOR_COMMANDS entry only reflects the default.
298
- const finalContent = filename === 'oprim-promote.md'
299
- ? cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent(framework))
300
- : content;
301
- (0, scaffold_1.writeFile)(path.join(cmdsDir, filename), finalContent);
302
- console.log(chalk_1.default.green('✓') + ` .cursor/commands/${filename}`);
319
+ for (const id of CURSOR_COMMAND_WORKFLOW_IDS) {
320
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
321
+ if (!schema.cursor.command)
322
+ continue;
323
+ const content = (0, workflow_renderer_1.renderCursorCommand)(id, projectRoot, framework);
324
+ (0, scaffold_1.writeFile)(path.join(cmdsDir, schema.cursor.command), content);
325
+ console.log(chalk_1.default.green('✓') + ` .cursor/commands/${schema.cursor.command}`);
303
326
  }
304
327
  if (dirCreated) {
305
328
  console.log(chalk_1.default.dim(' .cursor/ created — Cursor will discover these files automatically.'));
@@ -373,496 +396,9 @@ function removeContextStepFromFile(content) {
373
396
  const after = content.slice(e + CTX_STEP_END.length).replace(/^\n+/, '\n');
374
397
  return before + after;
375
398
  }
376
- // ─── Claude skill playbooks ───────────────────────────────────────────────────
377
- exports.CLAUDE_SKILLS = {
378
- 'oprim-pdr': pdrSkill(),
379
- 'oprim-bet': betSkill(),
380
- 'oprim-note': noteSkill(),
381
- 'oprim-criteria': criteriaSkill(),
382
- 'oprim-review': reviewSkill(),
383
- 'oprim-archive': archiveSkill(),
384
- 'oprim-sequence': oprimSequenceSkill(),
385
- 'oprim-context-init': contextInitSkill(),
386
- };
387
- // ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
388
- exports.CLAUDE_COMMANDS = {
389
- 'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent()),
390
- 'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
391
- 'archive.md': claudeWrapper('OPRIM: Archive', 'Archive a completed bet — move it out of the active board', archiveCommandContent()),
392
- 'context-init.md': claudeWrapper('OPRIM: Context Init', 'Declare the current project a citable remote context, guided by a short Q&A to draft its description', 'Use the Skill tool to invoke the `oprim-context-init` skill.'),
393
- };
394
- // ─── Poolside skill playbooks ─────────────────────────────────────────────────
395
- exports.POOLSIDE_SKILLS = {
396
- 'oprim-pdr': pdrSkill(),
397
- 'oprim-bet': betSkill(),
398
- 'oprim-note': noteSkill(),
399
- 'oprim-criteria': criteriaSkill(),
400
- 'oprim-review': reviewSkill(),
401
- 'oprim-archive': archiveSkill(),
402
- 'oprim-sequence': oprimSequenceSkill(),
403
- };
404
- // ─── Cursor skill playbooks ───────────────────────────────────────────────────
405
- exports.CURSOR_SKILLS = {
406
- 'oprim-pdr': pdrSkill(),
407
- 'oprim-bet': betSkill(),
408
- 'oprim-note': noteSkill(),
409
- 'oprim-criteria': criteriaSkill(),
410
- 'oprim-review': reviewSkill(),
411
- };
412
- // ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
413
- exports.CURSOR_COMMANDS = {
414
- 'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into a capability spec', promoteContent()),
415
- 'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
416
- 'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
417
- 'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
418
- 'oprim-note.md': cursorWrapper('oprim-note', 'Create a new atomic note for lightweight thinking capture', noteInlineContent()),
419
- 'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
420
- 'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
421
- };
422
- // ─── Helpers ─────────────────────────────────────────────────────────────────
423
- function claudeWrapper(name, description, body) {
424
- return `---
425
- name: "${name}"
426
- description: ${description}
427
- category: Workflow
428
- tags: [workflow, primer]
429
- ---
430
-
431
- ${description}.
432
-
433
- ${body}`;
434
- }
435
- function cursorWrapper(id, description, body) {
436
- return `---
437
- name: /${id}
438
- id: ${id}
439
- category: Workflow
440
- description: ${description}
441
- ---
442
-
443
- ${body}`;
444
- }
445
- // ─── Skill content ────────────────────────────────────────────────────────────
446
- function pdrSkill() {
447
- return `---
448
- name: oprim-pdr
449
- description: Create a new Product Decision Record in oprim/decisions/ with auto-assigned ID and guided prompting
450
- ---
451
-
452
- Create a new Product Decision Record (PDR) in \`oprim/decisions/\`.
453
-
454
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
455
-
456
- ## Steps
457
-
458
- ### 1. Get the decision title
459
- If not provided, ask: "What is the title of this product decision?"
460
-
461
- ### 2. Assign the next PDR ID
462
- Scan \`oprim/decisions/\` for files matching \`PDR-(\\d+)-\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none found.
463
- Slug: title → lowercase → spaces to hyphens → remove non-alphanumeric (except hyphens).
464
- Output path: \`oprim/decisions/PDR-NNN-<slug>.md\`
465
-
466
- ### 2b. Check for custom rules
467
- Read \`oprim/config.yaml\`. If it has a non-empty \`rules.pdr\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 3 and reflect it in the generated content. If \`rules.pdr\` is absent or empty, skip this step; behavior is unchanged.
468
-
469
- ### 3. Gather content
470
- 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).
471
-
472
- ### 4. Check for supersession
473
- Ask: "Does this supersede an existing PDR? If so, which ID? (Enter to skip)"
474
-
475
- ### 4b. Check for OKF frontmatter
476
- Read \`oprim/templates/pdr.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: pdr\`, \`title: <title>\`, \`description: <description>\`, \`tags: [<tags>]\`, \`timestamp: <today's date, ISO 8601>\`, to prepend in step 5.
477
- If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
478
-
479
- ### 5. Write the PDR file
480
- Prepend the frontmatter block from step 4b, if one was prepared.
481
- \`\`\`
482
- # PDR-NNN: <title>
483
-
484
- ## Status
485
- Proposed
486
-
487
- ## Context
488
- <context>
489
-
490
- ## Decision
491
- <decision>
492
-
493
- ## Alternatives considered
494
- <alternatives as bullet list>
495
-
496
- ## Consequences
497
- - Positive: <...>
498
- - Trade-offs: <...>
499
- - Follow-ups: <...>
500
-
501
- ## Evidence
502
- <evidence or "None">
503
-
504
- ## Related
505
- - Bets: <BET-IDs or "None">
506
- - OpenSpec: <change paths or "None">
507
- - Supersedes: <PDR-ID or "None">
508
- \`\`\`
509
-
510
- ### 6. Update superseded PDR (if applicable)
511
- Read the superseded file → replace Status value with \`Superseded by PDR-NNN\` → write back.
512
-
513
- ### 7. Report what was created
514
- `;
515
- }
516
- function betSkill() {
517
- return `---
518
- name: oprim-bet
519
- description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
520
- ---
521
-
522
- Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
523
-
524
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
525
-
526
- ## What you're creating
527
-
528
- 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.
529
-
530
- ## Steps
531
-
532
- ### 1. Get the bet title
533
- Display the naming convention before asking:
534
-
535
- > **Naming tip:** Use "verb + object [for context]"
536
- > - Good: "Improve bet naming for scannability"
537
- > - Bad: "Naming"
538
-
539
- If not provided, ask: "What is the title of this bet?"
540
-
541
- After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters:
542
- - Show: "Warning: this title may be too vague to scan at a glance."
543
- - Suggest a reformulation, e.g. "Consider: 'Improve <what> for <why>'"
544
- - Ask: "Proceed with this title anyway? (y/N)"
545
- - If "n" or Enter: ask for a revised title and re-validate
546
- - If "y": proceed with the original title
547
-
548
- ### 2. Assign the next BET ID
549
- 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.
550
-
551
- ### 2b. Derive the slug
552
- 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\`.
553
-
554
- ### 3. Check sequence.yaml exists
555
- If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
556
-
557
- ### 3b. Check for custom rules
558
- 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.
559
-
560
- ### 4. Gather content
561
- 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).
562
-
563
- Then ask about reversibility:
564
- - "Is this a **2-way door** (reversible — easy to undo, safe to try) or a **1-way door** (hard to reverse — requires high confidence)?"
565
-
566
- Then ask about each of the four risk dimensions (Low / Medium / High + short rationale):
567
- - "**Value risk**: Will users/customers actually use or buy this? (Low / Medium / High — and why?)"
568
- - "**Usability risk**: Can users figure out how to use it without help? (Low / Medium / High — and why?)"
569
- - "**Feasibility risk**: Can we build this with our current skills, time, and technology? (Low / Medium / High — and why?)"
570
- - "**Business viability risk**: Does this solution work for the business (revenue, legal, ops)? (Low / Medium / High — and why?)"
571
-
572
- ### 4b. Check for OKF frontmatter
573
- 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.
574
- If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
575
-
576
- ### 5. Write oprim/bets/BET-NNN-<slug>/bet-decision.md
577
- Prepend the frontmatter block from step 4b, if one was prepared.
578
- \`\`\`
579
- # Decision: BET-NNN <title>
580
- <!-- Naming tip: verb + object [for context] — e.g. "Improve bet naming for scannability" not "Naming" -->
581
-
582
- ## Status
583
- - Decision: <decision>
584
- - Date: <today YYYY-MM-DD>
585
- - Owner: <owner>
586
- - Review date: <review date>
587
-
588
- ## Door type
589
- - [<x if 2-way>] 2-way door (reversible — safe to try, easy to undo)
590
- - [<x if 1-way>] 1-way door (hard to reverse — requires higher confidence before committing)
591
-
592
- ## Risk profile
593
- - **Value risk**: <Low / Medium / High> — <rationale>
594
- - **Usability risk**: <Low / Medium / High> — <rationale>
595
- - **Feasibility risk**: <Low / Medium / High> — <rationale>
596
- - **Business viability risk**: <Low / Medium / High> — <rationale>
597
-
598
- ## Why now
599
- <why-now as bullet list>
600
-
601
- ## Alternatives considered
602
- <alternatives as bullet list>
603
-
604
- ## Expected outcomes
605
- <outcomes as bullet list>
606
-
607
- ## Kill criteria / rollback trigger
608
- <kill criteria as bullet list>
609
-
610
- ## Links
611
- - PDRs: <PDR-IDs or "None">
612
- - OpenSpec change: <to be filled when promoted>
613
- \`\`\`
614
-
615
- ### 6. Append to oprim/sequence.yaml backlog
616
- Read → parse YAML → append → write back (2-space indentation):
617
- \`\`\`yaml
618
- - id: BET-NNN
619
- title: "<title>"
620
- blocked_by: []
621
- unlocks: []
622
- requires_pdrs: []
623
- \`\`\`
624
-
625
- ### 7. Prompt for optional discovery scaffolding
626
- Ask: "Do you want to scaffold a discovery.md now? (y/N)"
627
- - If "y": write \`oprim/bets/BET-NNN-<slug>/discovery.md\` from the discovery template (same structure as \`oprim/templates/discovery.md\`).
628
- - If "n" or Enter: skip silently.
629
-
630
- ### 8. Report what was created
631
- `;
632
- }
633
- function noteSkill() {
634
- return `---
635
- name: oprim-note
636
- description: Create a new atomic note in oprim/notes/ for lightweight thinking capture, with tiered frontmatter and optional bet links
637
- ---
638
-
639
- 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.
640
-
641
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
642
-
643
- ## What you're creating
644
-
645
- 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.
646
-
647
- ## Steps
648
-
649
- ### 1. Get the note title
650
- If not provided, ask: "What is this note about? (a short title)"
651
-
652
- ### 2. Assign the next NOTE ID
653
- 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.
654
-
655
- ### 2b. Derive the slug
656
- 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>\`.
657
- Output path: \`oprim/notes/NOTE-NNN-<slug>.md\`
658
-
659
- ### 3. Gather the note body
660
- Ask: "What's the observation, idea, or connection?" (free-form prose — this becomes the note body).
661
-
662
- ### 4. Gather tags
663
- 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.
664
- 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.
665
-
666
- ### 5. Gather optional bet links
667
- Ask: "Does this relate to any existing bets? (comma-separated BET-IDs, or Enter to skip)"
668
-
669
- ### 6. Check the frontmatter tier
670
- Read \`oprim/templates/note.md\`.
671
- - If it exists and its frontmatter block contains a \`description:\` field, this workspace is on the **OKF tier** — ask for a one-line description.
672
- - If it exists with no \`description:\` field, use the **minimal tier** — skip the description.
673
- - 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.
674
-
675
- ### 7. Write oprim/notes/NOTE-NNN-<slug>.md
676
-
677
- Minimal tier:
678
- \`\`\`
679
- ---
680
- type: note
681
- title: "<title>"
682
- tags: [<tags>]
683
- timestamp: <today, ISO 8601>
684
- ---
685
-
686
- # Note: <title>
687
-
688
- <body>
689
-
690
- ## Bets
691
- - <BET-IDs from step 5, or "None">
692
- \`\`\`
693
-
694
- OKF tier: same as above, with \`description: "<description>"\` inserted immediately after \`title\`.
695
-
696
- ### 8. Link back from referenced bets
697
- 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).
698
-
699
- ### 9. Report what was created
700
- `;
701
- }
702
- function criteriaSkill() {
703
- return `---
704
- name: oprim-criteria
705
- description: Create or append to a criteria.yaml contract for a bet, with structured Amplitude and BigQuery source mapping
706
- ---
707
-
708
- Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
709
-
710
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
711
-
712
- ## Steps
713
-
714
- ### 1. Identify the bet
715
- If not provided, ask: "Which bet are you adding criteria for? (e.g. BET-042)"
716
-
717
- ### 2. Verify bet exists
718
- If \`oprim/bets/BET-NNN/\` not found: report and stop — advise using the \`oprim-bet\` skill first.
719
-
720
- ### 3. Gather metric details
721
- Ask: metric ID (snake_case), metric name, baseline (numeric), target (numeric), timeframe, launch date (YYYY-MM-DD or TBD), segment (optional).
722
-
723
- ### 4. Gather source mapping
724
- Ask: source type (amplitude / bigquery)
725
-
726
- If amplitude: event name, aggregation (unique_users / event_count / property_sum), denominator event (optional).
727
- \`\`\`yaml
728
- source:
729
- type: amplitude
730
- definition:
731
- event: <event_name>
732
- aggregation: <aggregation>
733
- denominator_event: <event_name | null>
734
- \`\`\`
735
-
736
- If bigquery: table, metric column, SQL filter, aggregation (sum / count / count_distinct / avg), denominator query (optional).
737
- \`\`\`yaml
738
- source:
739
- type: bigquery
740
- definition:
741
- table: "<project.dataset.table>"
742
- metric_column: "<column>"
743
- filter: "<sql_filter>"
744
- aggregation: <aggregation>
745
- denominator_query: <sql | null>
746
- \`\`\`
747
-
748
- ### 5. Build metric entry and write
749
- If file exists: read → parse → append to \`metrics\` → write back (never overwrite).
750
- If not: create with \`metrics:\` list.
751
-
752
- ### 6. Ask if more metrics needed. If yes, return to step 3.
753
-
754
- ### 7. Report what was created
755
- `;
756
- }
757
- function archiveSkill() {
758
- return `---
759
- name: oprim-archive
760
- 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
761
- ---
762
-
763
- 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.
764
-
765
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
766
-
767
- ## Steps
768
-
769
- ### 1. Get the bet ID
770
-
771
- If provided as an argument (e.g., \`/oprim:archive BET-005\`), use it directly.
772
-
773
- If not provided, ask: "Which bet ID would you like to archive? (e.g., BET-005)"
774
-
775
- Normalize the input: accept \`bet-005\`, \`005\`, \`5\`, or \`BET-005\` — always treat as \`BET-NNN\` zero-padded to 3 digits.
776
-
777
- ### 2. Resolve the bet directory
778
-
779
- Look for the bet directory in \`oprim/bets/\` using two patterns:
780
- 1. Exact match: \`oprim/bets/BET-NNN/\` (legacy non-slug format)
781
- 2. Slug variant: any directory starting with \`BET-NNN-\` (e.g., \`BET-NNN-<slug>/\`)
782
-
783
- Use whichever pattern matches. Call this the **resolved directory name**.
784
-
785
- If multiple directories match (e.g., both \`BET-NNN/\` and \`BET-NNN-slug/\` exist):
786
- - Report: "Ambiguous: found multiple directories for BET-NNN: [list them]. Please archive manually."
787
- - Stop.
788
-
789
- If neither pattern matches:
790
- - Report: "Bet BET-NNN was not found in oprim/bets/. Nothing was changed."
791
- - Stop.
792
-
793
- ### 3. Check for active dependencies and concurrent spec-delta conflicts
794
-
795
- 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.
796
-
797
- 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**.
798
-
799
- If either sequence.yaml dependents or delta overlaps are found:
800
- - Show a combined warning listing each dependent entry and each overlapping requirement.
801
-
802
- Example:
803
- \`\`\`
804
- ⚠ Warning: BET-005 is referenced by active bets:
805
- - BET-007 (blocked_by: [BET-005])
806
- - BET-008 (unlocks: [BET-005])
807
- ⚠ 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).
808
- \`\`\`
809
- - Ask: "Archive BET-NNN anyway? (y/N)"
810
- - If "n" or Enter: stop, no changes made.
811
- - If "y": proceed.
812
-
813
- If neither is found: proceed without warning.
814
-
815
- ### 4. Fold spec deltas into current truth
816
-
817
- 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.
818
-
819
- Otherwise, for each capability subdirectory under \`oprim/bets/<resolved-dir>/specs/\` containing a \`spec.md\`:
820
-
821
- 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.
822
- 2. Read \`oprim/specs/<capability>/spec.md\` if it exists (current truth uses a single flat \`## Requirements\` section).
823
- - **If it does not exist:**
824
- - 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.
825
- - 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."
826
- - **If it does exist:**
827
- - **ADDED**: append the requirement block to the end of the \`## Requirements\` section.
828
- - **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.
829
- - **REMOVED**: find and delete the matching block entirely. If no match is found, note this in the final report and continue — nothing to remove.
830
- 3. Write the updated \`oprim/specs/<capability>/spec.md\`.
831
-
832
- 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).
833
-
834
- Track which capabilities were merged (and any no-match notes) for the final report.
835
-
836
- ### 5. Move the bet directory to archive
837
-
838
- Create the archive subfolder if it doesn't exist:
839
- \`\`\`bash
840
- mkdir -p oprim/bets/archived
841
- \`\`\`
842
-
843
- Move the resolved directory:
844
- \`\`\`bash
845
- mv oprim/bets/<resolved-dir> oprim/bets/archived/<resolved-dir>
846
- \`\`\`
847
-
848
- ### 6. Remove the bet entry from sequence.yaml
849
-
850
- 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.
851
-
852
- ### 7. Report what was done
853
-
854
- \`\`\`
855
- ## Bet Archived
856
-
857
- **Bet:** BET-NNN
858
- **Archived to:** oprim/bets/archived/<resolved-dir>/
859
- **Removed from sequence.yaml:** ✓
860
- **Spec deltas merged:** <capability-1>, <capability-2> (omit this line if no specs/ directory was present)
861
-
862
- The bet is preserved in full at the archive location.
863
- \`\`\`
864
- `;
865
- }
399
+ // The oprim:context skill (PDR-surfacing lookup) is install-time plumbing tied to the
400
+ // pdrSurfacing toggle, not a forkable workflow artifact — it stays a small literal here
401
+ // rather than moving into the declarative workflow-schema system.
866
402
  function oprimContextSkill() {
867
403
  return `---
868
404
  name: oprim:context
@@ -893,295 +429,28 @@ List each match on its own line, then return — the invoking skill continues to
893
429
  If no PDRs match: exit silently — produce no output.
894
430
  `;
895
431
  }
896
- function oprimSequenceSkill() {
897
- return `---
898
- name: oprim-sequence
899
- description: Validate and update the primer sequencing board — triage mode computes board health and surfaces specific suggestions; seeded mode targets a specific context
900
- ---
901
-
902
- Manage the primer sequencing board in \`oprim/sequence.yaml\`.
903
-
904
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
905
-
906
- ## Entry modes
907
-
908
- **Triage mode** no intention provided: reads board state, computes health, surfaces specific actionable suggestions.
909
- **Seeded mode** context pre-provided (e.g. from a lifecycle hook): skips full triage and targets the most relevant move for that context.
910
-
911
- ## Steps
912
-
913
- ### 1. Determine mode
914
-
915
- If lifecycle context was provided as an argument or pre-seeded in the conversation (e.g. \`bet-created\`, \`bet-promoted\`, or a specific bet ID was just archived), enter **Seeded mode** — go to Step 2B.
916
-
917
- Otherwise, enter **Triage mode** — go to Step 2A.
918
-
919
- ### 2A. Triage mode — compute board health
920
-
921
- Read \`oprim/sequence.yaml\`. Compute:
922
-
923
- - **WIP utilization**: count entries in \`now\` vs \`wip_limits.now\`
924
- - **WIP violations**: entries in \`now\` that exceed the WIP limit
925
- - **Blocked Now bets**: entries in \`now\` whose \`blocked_by\` list contains a bet ID still present in any active lane (now/next/later/backlog)
926
- - **Ready-to-pull bets**: entries in \`next\` whose \`blocked_by\` list is empty or all resolved (each blocked_by ID is absent from all active lanes)
927
- - **PDR gaps**: entries in any lane whose \`requires_pdrs\` list contains a PDR ID not found in \`oprim/decisions/\`
928
-
929
- Surface **ranked suggestions**, most urgent first:
930
- 1. WIP violations → name each excess bet, suggest deferring to \`next\` or \`later\`
931
- 2. Blocked Now bets → name the bet and its unresolved blocker, suggest deferring until blocker resolves
932
- 3. Open Now slot (count < \`wip_limits.now\`) with a ready Next bet → name a specific bet to pull into \`now\`
933
- 4. PDR gaps → name the bet and missing PDR, suggest creating it first
934
-
935
- Each suggestion must name the exact bet ID, current lane, target lane, and reason.
936
-
937
- If the board is healthy (no violations, no ready moves): report current WIP utilization and state the board is healthy. Offer to move something anyway if the user wants.
938
-
939
- After surfacing suggestions, ask: "Which move would you like to make?"
940
-
941
- ### 2B. Seeded mode — target specific context
942
-
943
- Use the provided context to jump to the most relevant suggestion:
944
-
945
- - \`bet-created\`: A new bet just landed in backlog. Check if \`now\` has an open slot and \`next\` has a ready bet to pull. If so, suggest the specific bet to pull. Otherwise, confirm the new bet is in backlog and the board looks healthy.
946
- - \`bet-promoted\`: A bet was just promoted to an OpenSpec change. Verify the bet is still correctly sequenced and surface any resequencing action the promotion warrants.
947
- - Bet archived (e.g. "BET-011 was archived"): Check if \`now\` dropped below \`wip_limits.now\`. If so, find the most ready bet in \`next\` and suggest pulling it.
948
-
949
- If no relevant move is found for the provided context, fall back to Triage mode (Step 2A).
950
-
951
- ### 3. Validate the requested move
952
-
953
- Before executing any move, verify all three constraints:
954
-
955
- **WIP limit**: Moving a bet to \`now\` must not push the count above \`wip_limits.now\`. If it would, explain the violation and suggest deferring an existing \`now\` bet first.
956
-
957
- **Blocker resolution**: Moving a bet to \`now\` requires its \`blocked_by\` list to be empty or all resolved (IDs absent from all active lanes). If unresolved, name each unresolved blocker.
958
-
959
- **PDR preconditions**: Moving a bet to \`now\` requires all \`requires_pdrs\` entries to exist as files in \`oprim/decisions/\`. If any are missing, name them and suggest creating them first.
960
-
961
- If the move is invalid: explain which constraint failed and suggest the nearest valid alternative. Do not proceed to Step 4.
962
-
963
- ### 4. Preview and confirm
964
-
965
- Show the exact YAML change before writing. Name the entry that will move: its \`id\`, \`title\`, source lane, and target lane.
966
-
967
- \`\`\`
968
- Before: BET-005 is in next (now: 1/2 slots filled)
969
- After: BET-005 moves to now (now: 2/2 slots filled)
970
- \`\`\`
971
-
972
- Ask: "Apply this change? (y/N)"
973
- - If "n" or Enter: stop, no changes made.
974
- - If "y": proceed to Step 5.
975
-
976
- ### 5. Write sequence.yaml
977
-
978
- Read \`oprim/sequence.yaml\`. Remove the bet entry from its current lane. Insert it into the target lane. Write back with 2-space indentation. Do not modify any other entries.
979
-
980
- ### 6. Regenerate view
981
-
982
- Run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`.
983
-
984
- ### 7. Report what was done
985
- `;
986
- }
987
- function archiveCommandContent() {
988
- return `Use the Skill tool to invoke the \`oprim-archive\` skill.`;
989
- }
990
- function reviewSkill() {
991
- return `---
992
- name: oprim-review
993
- description: Create a KPI review artifact for a completed bet, pre-filled from criteria.yaml with actuals gathered from the user
994
- ---
995
-
996
- Create a KPI review in \`oprim/reviews/\`.
997
-
998
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
999
-
1000
- ## Steps
1001
-
1002
- ### 1. Identify the bet
1003
- If not provided, ask: "Which bet are you reviewing? (e.g. BET-042)"
1004
-
1005
- ### 1b. Check for custom rules
1006
- Read \`oprim/config.yaml\`. If it has a non-empty \`rules.review\` value, treat it as additional guidance from the team — factor it into the questions you ask in step 4 and reflect it in the generated review content. If \`rules.review\` is absent or empty, skip this step; behavior is unchanged.
1007
-
1008
- ### 2. Load criteria and check for a run result
1009
-
1010
- Read \`oprim/bets/BET-NNN/criteria.yaml\` if it exists (pre-fills baseline and target).
1011
- If not found: inform user and continue with empty metrics list.
1012
-
1013
- **Check for measurement run result:** Scan \`oprim/bets/BET-NNN/measurements/\` for files matching \`run-*.yaml\`. If any exist, sort by filename (date-based) and read the most recent.
1014
-
1015
- **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.
1016
-
1017
- **If no run result exists:** proceed to step 3 to gather actuals manually.
1018
-
1019
- ### 3. Gather actuals per metric (only when no run result)
1020
- For each metric show name/baseline/target and ask: "What was the actual result? (number or 'pending')"
1021
-
1022
- Status logic:
1023
- - actual >= target → \`hit\`
1024
- - actual < target → \`missed\`
1025
- - 'pending' or not provided → \`pending\`
1026
-
1027
- ### 4. Get review metadata
1028
- Ask: reviewer name, decision quality notes.
1029
-
1030
- ### 5. Output path
1031
- \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\` (today's date)
1032
-
1033
- ### 5b. Check for OKF frontmatter
1034
- Read \`oprim/templates/kpi-review.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 (derived from the reviewed bet's subject area). Prepare a frontmatter block with \`type: kpi-review\`, \`title: <bet ID and title>\`, \`description: <description>\`, \`tags: [<tags>]\`, \`timestamp: <review date, ISO 8601>\`, to prepend in step 6.
1035
- If no frontmatter block is found in the template, skip this step — write the file with no frontmatter, matching current behavior.
1036
-
1037
- ### 6. Write the review file
1038
- Prepend the frontmatter block from step 5b, if one was prepared.
1039
- \`\`\`markdown
1040
- # KPI Review: BET-NNN
1041
-
1042
- **Review date:** YYYY-MM-DD
1043
- **Reviewed by:** <reviewer>
1044
- **Actuals from run:** YYYY-MM-DD ← include only when a run result was ingested
1045
-
1046
- | Metric | Baseline | Target | Actual | Status |
1047
- |--------|----------|--------|--------|--------|
1048
- | <name> | <baseline> | <target> | <actual> | <status> |
1049
-
1050
- ## Decision quality
1051
- <notes>
1052
-
1053
- ## Actions
1054
- - [ ] Update bet-decision outcome section
1055
- - [ ] Update affected PDRs
1056
- - [ ] Re-sequence impacted bets
1057
- \`\`\`
1058
-
1059
- ### 7. Report what was created
1060
- `;
1061
- }
1062
- function contextInitSkill() {
1063
- return `---
1064
- name: oprim-context-init
1065
- description: Guide the user through drafting a description before declaring the current project a citable remote context
1066
- ---
1067
-
1068
- Declare the current project a citable remote context, with a clear description other projects and agents can use to decide whether to pull it.
1069
-
1070
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
1071
-
1072
- ## What this does
1073
-
1074
- 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.
1075
-
1076
- ## Steps
1077
-
1078
- ### 1. Check for an existing identity
1079
- 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.
1080
-
1081
- ### 2. Ask what this workspace covers
1082
- Ask the user, one at a time:
1083
- - "What does this project's oprim workspace cover? (e.g. product decisions, a specific domain, a team's specs)"
1084
- - "Who is this meant for — which teams or projects would reference it?"
1085
-
1086
- If the user declines to answer either question, treat that as opting out of guided drafting — skip to step 4 with no description.
1087
-
1088
- ### 3. Draft and confirm the description
1089
- 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)"
1090
-
1091
- ### 4. Call oprim context init
1092
- - If a description was drafted or accepted: use the Bash tool to run \`oprim context init --description "<final text>"\`.
1093
- - 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.
1094
-
1095
- ### 5. Report what was created
1096
- Report the path (\`.oprim-context/context.yaml\`) and the description that was set (or the description-less warning, if opted out).
1097
- `;
1098
- }
432
+ // ─── Bundled skill/command exports (bundled defaults, no project overrides — used by tests
433
+ // and oprim doctor's skill-drift check; installAgentSkills() itself resolves overrides
434
+ // per-project via workflow-schema.ts/workflow-renderer.ts) ────────────────────────────────
435
+ exports.CLAUDE_SKILLS = Object.fromEntries(CLAUDE_SKILL_WORKFLOW_IDS.map((id) => {
436
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
437
+ return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
438
+ }));
439
+ exports.CLAUDE_COMMANDS = Object.fromEntries(CLAUDE_COMMAND_WORKFLOWS.map(({ filename, id }) => [filename, (0, workflow_renderer_1.renderClaudeCommand)(id)]));
440
+ exports.POOLSIDE_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
441
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
442
+ return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
443
+ }));
444
+ exports.CURSOR_SKILLS = Object.fromEntries(CURSOR_SKILL_WORKFLOW_IDS.map((id) => {
445
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
446
+ return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
447
+ }));
448
+ exports.CURSOR_COMMANDS = Object.fromEntries(CURSOR_COMMAND_WORKFLOW_IDS.map((id) => {
449
+ const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
450
+ return [schema.cursor.command, (0, workflow_renderer_1.renderCursorCommand)(id)];
451
+ }));
1099
452
  function specAuthoringSkill() {
1100
- return `---
1101
- name: oprim-spec
1102
- description: Generate a native oprim capability spec delta at oprim/bets/BET-NNN-<slug>/specs/<capability>/spec.md while a bet is active, in RFC 2119 (SHALL/SHOULD/MAY) requirements and Gherkin scenarios — folded into oprim/specs/<capability>/spec.md (current truth) when the bet is archived
1103
- ---
1104
-
1105
- Generate a capability spec delta for an active bet — RFC 2119 requirements plus Gherkin scenarios, no OpenSpec required. This skill never writes to \`oprim/specs/\` directly; \`oprim-archive\` folds the delta into current truth when the bet is archived.
1106
-
1107
- **Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
1108
-
1109
- ## Steps
1110
-
1111
- ### 1. Get the active bet
1112
- If a bet ID was provided as context (e.g. invoked from \`/oprim:promote\`), use it directly. Otherwise ask: "Which bet is this spec change for? (e.g. BET-005)"
1113
-
1114
- Resolve it to a directory in \`oprim/bets/\` using the same two patterns \`oprim-archive\` uses: exact \`BET-NNN/\` (legacy, no slug) or the slug variant \`BET-NNN-<slug>/\`. If neither matches, report "Bet BET-NNN was not found in oprim/bets/ — spec deltas can only be authored against an active bet" and stop.
1115
-
1116
- ### 2. Get the capability name and description
1117
- If not provided, ask: "What capability are you specifying? (a short name, e.g. 'spec-authoring')" and "What does it do? (one or two sentences)"
1118
-
1119
- ### 2b. Derive the slug
1120
- From the capability name: lowercase all characters, replace any character that is not a letter or digit with a hyphen, collapse consecutive hyphens to one, strip leading/trailing hyphens. This becomes \`<capability>\`.
1121
- Output path: \`oprim/bets/<resolved-bet-dir>/specs/<capability>/spec.md\` (a delta, not \`oprim/specs/<capability>/spec.md\` — that file is current truth and is only ever written by \`oprim-archive\`'s merge step).
1122
-
1123
- ### 2c. Check for custom rules
1124
- Read \`oprim/config.yaml\`. If it has a non-empty \`rules.spec\` value, treat it as additional guidance from the team — factor it into the requirements and scenarios you draft. If \`rules.spec\` is absent or empty, skip this step; behavior is unchanged.
1125
-
1126
- ### 3. Determine the delta type for each requirement
1127
- For each requirement, ask whether it is new (**ADDED**), a change to an existing current-truth requirement (**MODIFIED**), or a removal of one (**REMOVED**).
1128
-
1129
- - **ADDED**: gather the requirement statement fresh.
1130
- - **MODIFIED / REMOVED**: read \`oprim/specs/<capability>/spec.md\` if it exists and list its \`### Requirement:\` headers so the user can pick the one being changed. The header text must match exactly (whitespace-insensitive) for \`oprim-archive\`'s merge step to find it later. If the file doesn't exist yet, MODIFIED/REMOVED aren't possible for this capability — fall back to ADDED.
1131
-
1132
- ### 4. Gather requirements and scenarios
1133
- For ADDED and MODIFIED requirements, phrase each as an RFC 2119 statement using SHALL (mandatory), SHOULD (recommended), or MAY (optional), then ask for at least one scenario: a WHEN (trigger) and a THEN (expected outcome), with an optional GIVEN (context) and additional AND steps. REMOVED requirements only need the matching header — no new scenarios.
1134
-
1135
- ### 5. Write the delta file
1136
- Append to (or create) \`oprim/bets/<resolved-bet-dir>/specs/<capability>/spec.md\`, grouping requirements under the matching section header — only include a section if it has at least one requirement under it:
1137
-
1138
- \`\`\`markdown
1139
- ## ADDED Requirements
1140
-
1141
- ### Requirement: <capability> SHALL/SHOULD/MAY <requirement statement>
1142
- <one-sentence elaboration>
1143
-
1144
- #### Scenario: <scenario title>
1145
- - **GIVEN** <context> (optional)
1146
- - **WHEN** <trigger>
1147
- - **THEN** <outcome>
1148
- - **AND** <additional outcome> (optional)
1149
-
1150
- ## MODIFIED Requirements
1151
-
1152
- ### Requirement: <exact header text matched from oprim/specs/<capability>/spec.md>
1153
- <revised elaboration>
1154
-
1155
- #### Scenario: <scenario title>
1156
- - **WHEN** <trigger>
1157
- - **THEN** <outcome>
1158
-
1159
- ## REMOVED Requirements
1160
-
1161
- ### Requirement: <exact header text matched from oprim/specs/<capability>/spec.md>
1162
- \`\`\`
1163
-
1164
- If the delta file already exists (a prior spec-authoring pass for this bet/capability), append new requirements to the matching section, creating that section if it's not yet present.
1165
-
1166
- ### 6. Report what was created
1167
- Show the delta file path, which bet it's scoped to, and a summary of the ADDED/MODIFIED/REMOVED requirements captured. Note that it merges into \`oprim/specs/<capability>/spec.md\` when \`BET-NNN\` is archived — nothing is current truth yet.
1168
- `;
1169
- }
1170
- // ─── Cursor inline content (condensed versions for command files) ─────────────
1171
- function pdrInlineContent() {
1172
- return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). Read \`oprim/config.yaml\`'s \`rules.pdr\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.`;
1173
- }
1174
- function betInlineContent() {
1175
- return `Create a new bet in \`oprim/bets/\`. First explain: "A bet is a product decision you're committing to explore — a problem worth solving, a hypothesis worth testing, or a direction worth taking. You'll name it, explain why now, and set a kill criterion." Then show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan \`BET-(\\d+)\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). Read \`oprim/config.yaml\`'s \`rules.bet\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters, warn "this title may be too vague", suggest a reformulation, and ask "Proceed anyway? (y/N)" — if "n", prompt for a revised title. Gather: decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.`;
1176
- }
1177
- function noteInlineContent() {
1178
- return `Create a new note in \`oprim/notes/\` for lightweight thinking capture — an observation, idea, or connection that hasn't yet earned a place in a bet or PDR. Notes carry no owner or kill criterion; promote one into a bet later with \`/oprim:promote NOTE-NNN\`. Ask for a short title. Scan \`oprim/notes/NOTE-(\\d+)-\` for the next id (zero-padded, default 001). Ask for the note body (free-form), tags, and optional related BET-IDs. Tags are checked against \`oprim/config.yaml\`'s \`notes.tags\`; any new tag is accepted and appended to that list rather than rejected — the vocabulary grows from usage. Read \`oprim/templates/note.md\` — if its frontmatter has a \`description:\` field, this workspace is on the OKF tier and needs a one-line description; if it has no \`description:\` field, use the minimal tier; if the file doesn't exist, fall back to reading \`okf.enabled\` directly from \`oprim/config.yaml\`. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section listing any related BET-IDs. For each related bet, append \`- Notes: NOTE-NNN\` to that bet-decision's \`## Links\` section. Report what was created.`;
1179
- }
1180
- function criteriaInlineContent() {
1181
- return `Add metrics to \`oprim/bets/BET-NNN/criteria.yaml\`. Verify bet dir exists. Gather: metric ID, name, baseline, target, timeframe, launch date, segment. Ask source type (amplitude or bigquery). Amplitude: event, aggregation, denominator_event. BigQuery: table, metric_column, filter, aggregation, denominator_query. If file exists: append to metrics list (never overwrite). If not: create. Ask if adding more metrics. Report what was created.`;
1182
- }
1183
- function reviewInlineContent() {
1184
- return `Create KPI review in \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`. Read \`oprim/config.yaml\`'s \`rules.review\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Read \`criteria.yaml\` for pre-fill (baseline/target). Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` files — if found, use the most recent to pre-populate actuals and status (include "Actuals from run: YYYY-MM-DD" note). If no run result, ask for each metric's actual value. Status: actual >= target → hit, actual < target → missed, not provided → pending. Ask reviewer name and decision quality notes. Write review with metric table and Actions checklist. Report what was created.`;
453
+ return (0, workflow_renderer_1.renderSkillBody)('spec-authoring');
1185
454
  }
1186
455
  // ─── Hook scripts: co-archival coordination ──────────────────────────────────
1187
456
  function hooksConfig(framework) {
@@ -1332,86 +601,6 @@ function mergeClaudeSettingsHooks(claudeDir) {
1332
601
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
1333
602
  console.log(chalk_1.default.green('✓') + ' .claude/settings.json (UserPromptSubmit + Stop hooks registered)');
1334
603
  }
1335
- // ─── Legacy content (promote remains inline; sequence now delegates to skill) ─
1336
- function promoteContent(framework = 'openspec') {
1337
- const sectionATitle = framework === 'openspec'
1338
- ? 'A. Bet → OpenSpec change'
1339
- : framework === 'native'
1340
- ? 'A. Bet → native oprim spec'
1341
- : 'A. Bet → spec (no framework configured)';
1342
- const sectionABody = framework === 'openspec'
1343
- ? `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1344
- 2. **Validate status** — decision must be "Build now"
1345
- 3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
1346
- 4. **Create OpenSpec change** — derive the change name as \`bet-NNN-<slug>\` where \`NNN\` is the zero-padded bet number (e.g. BET-004 → \`bet-004\`) and \`<slug>\` is a short kebab-case summary of the change. Then invoke the \`/openspec-propose\` skill (or \`/opsx:propose\`) with that name to create the change directory with **all required artifacts**: \`proposal.md\`, \`design.md\`, \`tasks.md\`, and \`specs/<capability>/spec.md\` for every capability listed under \`## Capabilities\`.
1347
- - Pass the bet decision content as context so the proposal reflects the bet's why/outcome
1348
- - **Do not manually create a partial change directory** — the propose skill ensures no artifact is omitted
1349
- - The spec file(s) are mandatory: each capability modified or added must have WHEN/THEN scenarios under \`## ADDED Requirements\` or \`## MODIFIED Requirements\`
1350
- 5. **Link artifacts**:
1351
- - Add OpenSpec change path to bet-decision \`## Links\` section
1352
- - Add bet ID to OpenSpec proposal \`## Context\` section
1353
- 6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, link it from OpenSpec proposal
1354
- 7. **Verify completeness** — confirm the change directory contains:
1355
- - \`proposal.md\`
1356
- - \`design.md\`
1357
- - \`tasks.md\`
1358
- - \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
1359
- If any artifact is missing, create it before reporting done.
1360
- 8. **Report** — show what was linked and what remains for engineering`
1361
- : framework === 'native'
1362
- ? `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1363
- 2. **Validate status** — decision must be "Build now"
1364
- 3. **Check authority boundary** — confirm primer artifact owns why/order/outcome only
1365
- 4. **Generate the native spec delta(s)** — for each capability listed under the bet's \`## Capabilities\` section (or a single capability derived from the bet title if none is listed), invoke the \`oprim-spec\` skill with this bet as context to write \`oprim/bets/BET-XXX/specs/<capability>/spec.md\` — a delta using \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` headers reflecting the bet's why/outcome. No OpenSpec change directory is created and OpenSpec need not be installed. Nothing is written to \`oprim/specs/<capability>/spec.md\` (current truth) at promote time — that only happens when this bet is archived.
1366
- 5. **Link artifacts** — add \`- Spec (delta): oprim/bets/BET-XXX/specs/<capability>/spec.md\` (one line per capability) to the bet-decision \`## Links\` section
1367
- 6. **Copy criteria** — if \`oprim/bets/BET-XXX/criteria.yaml\` exists, note it alongside the spec link
1368
- 7. **Report** — show what was created and linked, and note that merge-on-archive will fold the delta into \`oprim/specs/\` when the bet archives`
1369
- : `1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
1370
- 2. **Validate status** — decision must be "Build now"
1371
- 3. **Report and stop** — no speccing framework is configured (\`integrations.spec_framework: none\`). Add \`- Spec: none (no speccing framework configured)\` to the bet-decision \`## Links\` section. No spec artifact is created.`;
1372
- return `
1373
- Promote an atomic note into a bet, or a prioritized bet into a capability spec. The promotion path is determined solely by the prefix of the ID argument — there is no separate command for each.
1374
-
1375
- **Input**: Specify an ID (e.g., \`/oprim:promote BET-042\` or \`/oprim:promote NOTE-005\`) or omit to be prompted.
1376
-
1377
- ### 0. Determine the promotion path from the ID prefix
1378
- - \`BET-\` → **${sectionATitle}**
1379
- - \`NOTE-\` → **B. Note → Bet**
1380
- - Anything else → report "Unrecognized ID prefix — expected BET- or NOTE-" and stop. Do not silently do nothing.
1381
-
1382
- ## ${sectionATitle}
1383
-
1384
- ${sectionABody}
1385
-
1386
- ## B. Note → Bet
1387
-
1388
- 1. **Locate the note** — read \`oprim/notes/NOTE-XXX-<slug>.md\`
1389
- 2. **Assign the next BET ID** — scan both \`oprim/bets/\` and \`oprim/bets/archived/\` for directories matching \`BET-(\\d+)(-[^/]*)?\`, max+1 zero-padded to 3 digits (default 001) — same convention \`oprim-bet\` uses
1390
- 3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
1391
- 4. **Draft the bet** — write \`oprim/bets/BET-NNN-<slug>/bet-decision.md\` from the standard bet-decision structure, pre-filling only \`## Why now\` from the note's body. Leave \`Alternatives considered\`, \`Expected outcomes\`, and \`Kill criteria / rollback trigger\` as template placeholders — draft from the note, don't fabricate content it doesn't support. Ask for \`Owner\` and \`Review date\`; default \`Decision: Build now\` and \`Date\` to today.
1392
- 5. **Register the new bet** — append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`
1393
- 6. **Link back** — add \`Bets: BET-NNN\` to the note (creating or extending its \`## Bets\` section)
1394
- 7. **Report** — show the new bet's path and flag that \`Alternatives considered\`, \`Expected outcomes\`, and \`Kill criteria\` still need authoring before this bet can itself be promoted
1395
- `;
1396
- }
1397
- function sequenceContent() {
1398
- return `Use the Skill tool to invoke the \`oprim-sequence\` skill.`;
1399
- }
1400
- function sequenceInlineContent() {
1401
- return `
1402
- Validate the primer sequencing board and suggest rebalancing if needed.
1403
-
1404
- **Steps**
1405
-
1406
- 1. **Read board** — load \`oprim/sequence.yaml\`
1407
- 2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
1408
- 3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
1409
- 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
1410
- 5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
1411
- 6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
1412
- 7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
1413
- `;
1414
- }
1415
604
  // ─── Instruction-file helpers (Codex / Gemini CLI) ────────────────────────────
1416
605
  const OPRIM_START = '<!-- oprim:start -->';
1417
606
  const OPRIM_END = '<!-- oprim:end -->';
@@ -1434,100 +623,12 @@ function writeAgentInstructionFile(filePath, section) {
1434
623
  fs.writeFileSync(filePath, existing + separator + delimited + '\n', 'utf-8');
1435
624
  }
1436
625
  }
1437
- function oprimWorkflowsInline() {
1438
- return `
1439
- ## oprim workflows
1440
-
1441
- ### Bet authoring (oprim-bet)
1442
- Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
1443
-
1444
- 1. Show naming tip: "verb + object [for context] — e.g. 'Improve bet naming for scannability'"
1445
- 2. Ask for the bet title. Validate: fewer than 4 words OR fewer than 25 chars → warn, suggest reformulation, ask "Proceed anyway? (y/N)".
1446
- 3. Assign next BET ID: scan \`oprim/bets/BET-(\\d+)\` dirs, max+1 zero-padded to 3 digits (default 001).
1447
- 4. Check \`oprim/sequence.yaml\` exists — stop if not, advise \`oprim init\`.
1448
- 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.
1449
- 5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
1450
- 6. Write \`oprim/bets/BET-NNN/bet-decision.md\` with all fields.
1451
- 7. Append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`.
1452
- 8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\`.
1453
- 9. Report what was created.
1454
-
1455
- ### Note authoring (oprim-note)
1456
- Create a new note in \`oprim/notes/\` for lightweight thinking capture — not a bet, no owner or kill criterion.
1457
-
1458
- 1. Ask for a short title.
1459
- 2. Assign next NOTE ID: scan \`oprim/notes/NOTE-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
1460
- 3. Ask for the note body (free-form), tags, and optional related BET-IDs.
1461
- 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).
1462
- 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\`.
1463
- 6. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section.
1464
- 7. For each related bet, append \`- Notes: NOTE-NNN\` to that bet's \`## Links\` section.
1465
- 8. Report what was created.
1466
-
1467
- ### Criteria authoring (oprim-criteria)
1468
- Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
1469
-
1470
- 1. Ask which bet (e.g. BET-042). Verify dir exists.
1471
- 2. Gather: metric ID (snake_case), name, baseline, target, timeframe, launch date, segment.
1472
- 3. Ask source type (amplitude / bigquery).
1473
- - Amplitude: event, aggregation (unique_users/event_count/property_sum), denominator_event.
1474
- - BigQuery: table, metric_column, filter, aggregation, denominator_query.
1475
- 4. If file exists: append to \`metrics\` list (never overwrite). If not: create.
1476
- 5. Ask if adding more metrics.
1477
- 6. Report what was created.
1478
-
1479
- ### PDR authoring (oprim-pdr)
1480
- Create a new Product Decision Record in \`oprim/decisions/\`.
1481
-
1482
- 1. Ask for decision title.
1483
- 2. Assign next PDR ID: scan \`oprim/decisions/PDR-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
1484
- 2b. Read \`oprim/config.yaml\`'s \`rules.pdr\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
1485
- 3. Gather: context, decision, alternatives, consequences, evidence, related bets/specs.
1486
- 4. Ask if superseding an existing PDR.
1487
- 5. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding, update old PDR Status.
1488
- 6. Report what was created.
1489
-
1490
- ### KPI review (oprim-review)
1491
- Create a KPI review artifact in \`oprim/reviews/\`.
1492
-
1493
- 1. Ask which bet (e.g. BET-042).
1494
- 1b. Read \`oprim/config.yaml\`'s \`rules.review\` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
1495
- 2. Read \`oprim/bets/BET-NNN/criteria.yaml\` for pre-fill. Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` — use most recent if present.
1496
- 3. If no run result, ask for each metric's actual value.
1497
- 4. Status: actual >= target → hit; actual < target → missed; not provided → pending.
1498
- 5. Ask reviewer name and decision quality notes.
1499
- 6. Write \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`.
1500
- 7. Report what was created.
1501
-
1502
- ### Bet archiving (oprim-archive)
1503
- Archive a completed bet.
1504
-
1505
- 1. Ask for bet ID (accept bet-005, 005, 5, BET-005 — normalize to BET-NNN).
1506
- 2. Verify \`oprim/bets/BET-NNN/\` exists.
1507
- 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.
1508
- 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.
1509
- 5. Move directory: \`oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN\`.
1510
- 6. Remove the bet entry from \`oprim/sequence.yaml\`.
1511
- 7. Report what was done.
1512
-
1513
- ### Sequencing board (oprim-sequence)
1514
- Validate the primer sequencing board and regenerate the visual view.
1515
-
1516
- 1. **Read board** — load \`oprim/sequence.yaml\`
1517
- 2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
1518
- 3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
1519
- 4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
1520
- 5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
1521
- 6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
1522
- 7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
1523
- `;
1524
- }
1525
626
  function codexInstructions() {
1526
- return oprimWorkflowsInline();
627
+ return (0, workflow_renderer_1.renderAgentInstructions)();
1527
628
  }
1528
629
  function geminiInstructions() {
1529
- return oprimWorkflowsInline();
630
+ return (0, workflow_renderer_1.renderAgentInstructions)();
1530
631
  }
1531
632
  function poolsideInstructions() {
1532
- return oprimWorkflowsInline();
633
+ return (0, workflow_renderer_1.renderAgentInstructions)();
1533
634
  }