@open-product-primer/cli 0.7.1 → 0.9.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.
@@ -272,6 +272,7 @@ exports.CLAUDE_SKILLS = {
272
272
  'oprim-criteria': criteriaSkill(),
273
273
  'oprim-review': reviewSkill(),
274
274
  'oprim-archive': archiveSkill(),
275
+ 'oprim-sequence': oprimSequenceSkill(),
275
276
  };
276
277
  // ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
277
278
  exports.CLAUDE_COMMANDS = {
@@ -289,7 +290,7 @@ exports.CURSOR_SKILLS = {
289
290
  // ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
290
291
  exports.CURSOR_COMMANDS = {
291
292
  'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet to an OpenSpec change', promoteContent()),
292
- 'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceContent()),
293
+ 'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
293
294
  'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
294
295
  'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
295
296
  'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
@@ -617,6 +618,95 @@ List each match on its own line, then return — the invoking skill continues to
617
618
  If no PDRs match: exit silently — produce no output.
618
619
  `;
619
620
  }
621
+ function oprimSequenceSkill() {
622
+ return `---
623
+ name: oprim-sequence
624
+ description: Validate and update the primer sequencing board — triage mode computes board health and surfaces specific suggestions; seeded mode targets a specific context
625
+ ---
626
+
627
+ Manage the primer sequencing board in \`oprim/sequence.yaml\`.
628
+
629
+ ## Entry modes
630
+
631
+ **Triage mode** — no intention provided: reads board state, computes health, surfaces specific actionable suggestions.
632
+ **Seeded mode** — context pre-provided (e.g. from a lifecycle hook): skips full triage and targets the most relevant move for that context.
633
+
634
+ ## Steps
635
+
636
+ ### 1. Determine mode
637
+
638
+ 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.
639
+
640
+ Otherwise, enter **Triage mode** — go to Step 2A.
641
+
642
+ ### 2A. Triage mode — compute board health
643
+
644
+ Read \`oprim/sequence.yaml\`. Compute:
645
+
646
+ - **WIP utilization**: count entries in \`now\` vs \`wip_limits.now\`
647
+ - **WIP violations**: entries in \`now\` that exceed the WIP limit
648
+ - **Blocked Now bets**: entries in \`now\` whose \`blocked_by\` list contains a bet ID still present in any active lane (now/next/later/backlog)
649
+ - **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)
650
+ - **PDR gaps**: entries in any lane whose \`requires_pdrs\` list contains a PDR ID not found in \`oprim/decisions/\`
651
+
652
+ Surface **ranked suggestions**, most urgent first:
653
+ 1. WIP violations → name each excess bet, suggest deferring to \`next\` or \`later\`
654
+ 2. Blocked Now bets → name the bet and its unresolved blocker, suggest deferring until blocker resolves
655
+ 3. Open Now slot (count < \`wip_limits.now\`) with a ready Next bet → name a specific bet to pull into \`now\`
656
+ 4. PDR gaps → name the bet and missing PDR, suggest creating it first
657
+
658
+ Each suggestion must name the exact bet ID, current lane, target lane, and reason.
659
+
660
+ 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.
661
+
662
+ After surfacing suggestions, ask: "Which move would you like to make?"
663
+
664
+ ### 2B. Seeded mode — target specific context
665
+
666
+ Use the provided context to jump to the most relevant suggestion:
667
+
668
+ - \`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.
669
+ - \`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.
670
+ - 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.
671
+
672
+ If no relevant move is found for the provided context, fall back to Triage mode (Step 2A).
673
+
674
+ ### 3. Validate the requested move
675
+
676
+ Before executing any move, verify all three constraints:
677
+
678
+ **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.
679
+
680
+ **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.
681
+
682
+ **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.
683
+
684
+ If the move is invalid: explain which constraint failed and suggest the nearest valid alternative. Do not proceed to Step 4.
685
+
686
+ ### 4. Preview and confirm
687
+
688
+ Show the exact YAML change before writing. Name the entry that will move: its \`id\`, \`title\`, source lane, and target lane.
689
+
690
+ \`\`\`
691
+ Before: BET-005 is in next (now: 1/2 slots filled)
692
+ After: BET-005 moves to now (now: 2/2 slots filled)
693
+ \`\`\`
694
+
695
+ Ask: "Apply this change? (y/N)"
696
+ - If "n" or Enter: stop, no changes made.
697
+ - If "y": proceed to Step 5.
698
+
699
+ ### 5. Write sequence.yaml
700
+
701
+ 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.
702
+
703
+ ### 6. Regenerate view
704
+
705
+ Run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`.
706
+
707
+ ### 7. Report what was done
708
+ `;
709
+ }
620
710
  function archiveCommandContent() {
621
711
  return `Use the Skill tool to invoke the \`oprim-archive\` skill.`;
622
712
  }
@@ -703,54 +793,88 @@ function hooksConfig(framework) {
703
793
  }, null, 2) + '\n');
704
794
  }
705
795
  const ON_PROMPT_SUBMIT_HOOK = `#!/usr/bin/env bash
706
- # UserPromptSubmit hook: detects archive slash commands and sets a pending flag.
796
+ # UserPromptSubmit hook: detects lifecycle slash commands and sets pending flags.
707
797
 
708
- config_file=".claude/hooks/config.json"
709
- flag_file=".claude/hooks/.archive-pending"
798
+ archive_flag=".claude/hooks/.archive-pending"
799
+ nudge_flag=".claude/hooks/.sequence-nudge"
710
800
 
711
- framework="openspec"
712
- if [ -f "$config_file" ]; then
713
- framework=$(python3 -c "import sys,json; print(json.load(open('$config_file')).get('framework','openspec'))" 2>/dev/null || echo "openspec")
714
- fi
801
+ input=$(cat)
715
802
 
716
- prompt=$(cat | python3 -c "import sys,json; print(json.load(sys.stdin).get('prompt',''))" 2>/dev/null || true)
803
+ # Archive co-archival detection
804
+ if printf '%s' "$input" | grep -qE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*(opsx:archive|openspec-archive-change)'; then
805
+ prompt=$(printf '%s' "$input" | grep -oE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"//;s/"$//')
806
+ arg=$(printf '%s' "$prompt" | sed 's|^[[:space:]]*/[^[:space:]]* *||' | awk '{print $1}' | sed 's|^@||' | sed 's|.*/changes/||' | sed 's|/$||' | xargs 2>/dev/null || true)
807
+ printf '%s' "$arg" > "$archive_flag"
808
+ fi
717
809
 
718
- [ -z "$prompt" ] && exit 0
810
+ # Bet creation detection
811
+ if printf '%s' "$input" | grep -qE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*oprim:bet'; then
812
+ printf 'bet-created' > "$nudge_flag"
813
+ fi
719
814
 
720
- if [ "$framework" = "openspec" ]; then
721
- if echo "$prompt" | grep -qE '^[[:space:]]*/(opsx:archive|openspec-archive-change)([[:space:]]|$)'; then
722
- arg=$(echo "$prompt" | sed 's|^[[:space:]]*/[^[:space:]]* *||' | sed 's|^@||' | sed 's|.*/changes/||' | sed 's|/$||' | xargs 2>/dev/null || true)
723
- echo "$arg" > "$flag_file"
724
- fi
815
+ # Bet promotion detection
816
+ if printf '%s' "$input" | grep -qE '"prompt"[[:space:]]*:[[:space:]]*"[^"]*oprim:promote'; then
817
+ printf 'bet-promoted' > "$nudge_flag"
725
818
  fi
726
819
  `;
727
820
  const ON_STOP_HOOK = `#!/usr/bin/env bash
728
- # Stop hook: if an archive command was detected, find the linked bet and prompt co-archival.
821
+ # Stop hook: archive co-archival coordination and sequencing nudges.
729
822
 
730
- flag_file=".claude/hooks/.archive-pending"
731
- [ -f "$flag_file" ] || exit 0
823
+ archive_flag=".claude/hooks/.archive-pending"
824
+ nudge_flag=".claude/hooks/.sequence-nudge"
732
825
 
733
- change=$(tr -d '[:space:]' < "$flag_file")
734
- rm -f "$flag_file"
826
+ # --- Archive co-archival block ---
827
+ if [ -f "$archive_flag" ]; then
828
+ change=$(tr -d '[:space:]' < "$archive_flag")
829
+ rm -f "$archive_flag"
735
830
 
736
- if [ -z "$change" ]; then
737
- latest=$(ls openspec/changes/archive/ 2>/dev/null | sort -r | head -1)
738
- [ -z "$latest" ] && exit 0
739
- change=$(echo "$latest" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-//')
740
- fi
741
-
742
- [ -z "$change" ] && exit 0
831
+ if [ -z "$change" ]; then
832
+ latest=$(ls openspec/changes/archive/ 2>/dev/null | sort -r | head -1)
833
+ [ -n "$latest" ] && change=$(echo "$latest" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-//')
834
+ fi
743
835
 
744
- archive_dir=$(ls openspec/changes/archive/ 2>/dev/null | grep -F "$change" | sort -r | head -1)
745
- [ -z "$archive_dir" ] && exit 0
836
+ if [ -n "$change" ]; then
837
+ archive_dir=$(ls openspec/changes/archive/ 2>/dev/null | grep -F "$change" | sort -r | head -1)
838
+ if [ -n "$archive_dir" ]; then
839
+ proposal="openspec/changes/archive/$archive_dir/proposal.md"
840
+ if [ -f "$proposal" ]; then
841
+ bet_id=$(grep -oE 'BET-[0-9]+' "$proposal" | head -1)
842
+ if [ -n "$bet_id" ]; then
843
+ printf '{"decision":"block","reason":"The openspec change '''%s''' was just archived. Its proposal.md references %s. Please invoke \`/oprim:archive %s\` to co-archive the linked bet."}\\n' "$change" "$bet_id" "$bet_id"
844
+ exit 0
845
+ fi
846
+ fi
847
+ fi
848
+ fi
849
+ fi
746
850
 
747
- proposal="openspec/changes/archive/$archive_dir/proposal.md"
748
- [ -f "$proposal" ] || exit 0
851
+ # --- Sequencing nudge from lifecycle event ---
852
+ if [ -f "$nudge_flag" ]; then
853
+ context=$(cat "$nudge_flag")
854
+ rm -f "$nudge_flag"
855
+
856
+ case "$context" in
857
+ bet-created)
858
+ printf '\\n💡 A new bet was added to your backlog. Run \`/oprim:sequence\` to check if it should be pulled into Now or Next.\\n'
859
+ ;;
860
+ bet-promoted)
861
+ printf '\\n💡 A bet was promoted to an OpenSpec change. Run \`/oprim:sequence\` to verify the board reflects this.\\n'
862
+ ;;
863
+ esac
864
+ fi
749
865
 
750
- bet_id=$(grep -oE 'BET-[0-9]+' "$proposal" | head -1)
751
- [ -z "$bet_id" ] && exit 0
866
+ # --- Open Now slot check ---
867
+ if [ -f "oprim/sequence.yaml" ]; then
868
+ wip_limit=$(awk '/^wip_limits:/{in_wip=1} in_wip && /^ now:/{print $2; exit} /^[^ ]/{in_wip=0}' oprim/sequence.yaml 2>/dev/null)
869
+ now_count=$(awk '/^now:/{in_now=1; next} in_now && /^[^ ]/{in_now=0} in_now && /^ - id:/{count++} END{print count+0}' oprim/sequence.yaml 2>/dev/null)
870
+ next_count=$(awk '/^next:/{in_next=1; next} in_next && /^[^ ]/{in_next=0} in_next && /^ - id:/{count++} END{print count+0}' oprim/sequence.yaml 2>/dev/null)
752
871
 
753
- printf '{"decision":"block","reason":"The openspec change '\''%s'\'' was just archived. Its proposal.md references %s. Please invoke \`/oprim:archive %s\` to co-archive the linked bet."}\\n' "$change" "$bet_id" "$bet_id"
872
+ if [ -n "$wip_limit" ] && [ -n "$now_count" ] && [ -n "$next_count" ]; then
873
+ if [ "$now_count" -lt "$wip_limit" ] 2>/dev/null && [ "$next_count" -gt 0 ] 2>/dev/null; then
874
+ printf '\\n💡 Now lane has capacity (%s/%s). Run \`/oprim:sequence\` to pull something from Next.\\n' "$now_count" "$wip_limit"
875
+ fi
876
+ fi
877
+ fi
754
878
  `;
755
879
  // Merge UserPromptSubmit + Stop hooks into .claude/settings.json without clobbering existing entries.
756
880
  // Also removes the legacy PostToolUse/Skill hook from on-skill-archive.sh if present.
@@ -810,7 +934,7 @@ function mergeClaudeSettingsHooks(claudeDir) {
810
934
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
811
935
  console.log(chalk_1.default.green('✓') + ' .claude/settings.json (UserPromptSubmit + Stop hooks registered)');
812
936
  }
813
- // ─── Legacy content (promote / sequence remain inline) ───────────────────────
937
+ // ─── Legacy content (promote remains inline; sequence now delegates to skill)
814
938
  function promoteContent() {
815
939
  return `
816
940
  Promote a prioritized bet to an OpenSpec change and link criteria contracts.
@@ -840,6 +964,9 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
840
964
  `;
841
965
  }
842
966
  function sequenceContent() {
967
+ return `Use the Skill tool to invoke the \`oprim-sequence\` skill.`;
968
+ }
969
+ function sequenceInlineContent() {
843
970
  return `
844
971
  Validate the primer sequencing board and suggest rebalancing if needed.
845
972
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",