@open-product-primer/cli 0.7.0 → 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.
- package/dist/commands/init.js +3 -0
- package/dist/commands/update.js +5 -0
- package/dist/lib/install-agent.js +174 -35
- package/dist/lib/templates.d.ts +1 -0
- package/dist/lib/templates.js +109 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -64,6 +64,7 @@ function initCommand() {
|
|
|
64
64
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
|
|
65
65
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
|
|
66
66
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
|
|
67
|
+
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
67
68
|
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected));
|
|
68
69
|
const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
|
|
69
70
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), templates_1.pdrTemplate);
|
|
@@ -71,6 +72,7 @@ function initCommand() {
|
|
|
71
72
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
|
|
72
73
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), templates_1.kpiReviewTemplate);
|
|
73
74
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
|
|
75
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
74
76
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
|
|
75
77
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
|
|
76
78
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
|
|
@@ -80,6 +82,7 @@ function initCommand() {
|
|
|
80
82
|
console.log(' ' + chalk_1.default.gray('oprim/config.yaml') + ' — ' + configStatus);
|
|
81
83
|
console.log(' ' + chalk_1.default.gray('oprim/sequence.yaml') + ' — ' + sequenceStatus);
|
|
82
84
|
console.log(' ' + chalk_1.default.gray('oprim/templates/') + ' — refreshed');
|
|
85
|
+
console.log(' ' + chalk_1.default.gray('oprim/scripts/') + ' — refreshed');
|
|
83
86
|
// ── Agent selection ───────────────────────────────────────────────────────
|
|
84
87
|
let selectedAgents;
|
|
85
88
|
const flaggedAgents = opts.agent;
|
package/dist/commands/update.js
CHANGED
|
@@ -43,12 +43,17 @@ const fs = __importStar(require("fs"));
|
|
|
43
43
|
const chalk_1 = __importDefault(require("chalk"));
|
|
44
44
|
const install_agent_1 = require("../lib/install-agent");
|
|
45
45
|
const detect_1 = require("../lib/detect");
|
|
46
|
+
const scaffold_1 = require("../lib/scaffold");
|
|
47
|
+
const templates_1 = require("../lib/templates");
|
|
46
48
|
function updateCommand() {
|
|
47
49
|
return new commander_1.Command('update')
|
|
48
50
|
.description('Refresh /oprim:* assistant commands and skills from package templates')
|
|
49
51
|
.action(async () => {
|
|
50
52
|
const projectRoot = process.cwd();
|
|
51
53
|
const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
54
|
+
const primerDir = path.join(projectRoot, 'oprim');
|
|
55
|
+
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
56
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
52
57
|
if (configAgents !== null && configAgents.length > 0) {
|
|
53
58
|
let specFramework = 'openspec';
|
|
54
59
|
let pdrSurfacing = false;
|
|
@@ -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',
|
|
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
|
|
796
|
+
# UserPromptSubmit hook: detects lifecycle slash commands and sets pending flags.
|
|
707
797
|
|
|
708
|
-
|
|
709
|
-
|
|
798
|
+
archive_flag=".claude/hooks/.archive-pending"
|
|
799
|
+
nudge_flag=".claude/hooks/.sequence-nudge"
|
|
710
800
|
|
|
711
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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:
|
|
729
|
-
|
|
730
|
-
flag_file=".claude/hooks/.archive-pending"
|
|
731
|
-
[ -f "$flag_file" ] || exit 0
|
|
821
|
+
# Stop hook: archive co-archival coordination and sequencing nudges.
|
|
732
822
|
|
|
733
|
-
|
|
734
|
-
|
|
823
|
+
archive_flag=".claude/hooks/.archive-pending"
|
|
824
|
+
nudge_flag=".claude/hooks/.sequence-nudge"
|
|
735
825
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
fi
|
|
826
|
+
# --- Archive co-archival block ---
|
|
827
|
+
if [ -f "$archive_flag" ]; then
|
|
828
|
+
change=$(tr -d '[:space:]' < "$archive_flag")
|
|
829
|
+
rm -f "$archive_flag"
|
|
741
830
|
|
|
742
|
-
[ -z "$change" ]
|
|
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
|
-
|
|
745
|
-
|
|
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
|
-
|
|
748
|
-
[ -f "$
|
|
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
|
-
|
|
751
|
-
[ -
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -851,6 +978,7 @@ Validate the primer sequencing board and suggest rebalancing if needed.
|
|
|
851
978
|
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
|
|
852
979
|
5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
|
|
853
980
|
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
981
|
+
7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
|
|
854
982
|
`;
|
|
855
983
|
}
|
|
856
984
|
// ─── Instruction-file helpers (Codex / Gemini CLI) ────────────────────────────
|
|
@@ -934,6 +1062,17 @@ Archive a completed bet.
|
|
|
934
1062
|
4. Move directory: \`oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN\`.
|
|
935
1063
|
5. Remove the bet entry from \`oprim/sequence.yaml\`.
|
|
936
1064
|
6. Report what was done.
|
|
1065
|
+
|
|
1066
|
+
### Sequencing board (oprim-sequence)
|
|
1067
|
+
Validate the primer sequencing board and regenerate the visual view.
|
|
1068
|
+
|
|
1069
|
+
1. **Read board** — load \`oprim/sequence.yaml\`
|
|
1070
|
+
2. **Check WIP limits** — compare \`now\` count against \`wip_limits.now\`
|
|
1071
|
+
3. **Validate blockers** — for each bet in \`now\`, confirm all \`blocked_by\` entries are complete or absent
|
|
1072
|
+
4. **Validate PDR preconditions** — confirm all \`requires_pdrs\` entries exist in \`oprim/decisions/\`
|
|
1073
|
+
5. **Report violations** — list any WIP excess, unresolved blockers, or missing PDRs
|
|
1074
|
+
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
1075
|
+
7. **Regenerate view** — run \`node oprim/scripts/generate-sequence-view.js\` from the project root to update \`oprim/sequence-view.md\`
|
|
937
1076
|
`;
|
|
938
1077
|
}
|
|
939
1078
|
function codexInstructions() {
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare const pdrTemplate = "# PDR-XXX: <Decision title>\n\n## Status\nPr
|
|
|
4
4
|
export declare const betDecisionTemplate = "# Decision: BET-XXX <Bet title>\n<!-- Naming tip: verb + object [for context] \u2014 e.g. \"Improve bet naming for scannability\" not \"Naming\" -->\n\n## Status\n- Decision: Build now | Defer | Kill\n- Date: YYYY-MM-DD\n- Owner: <name>\n- Review date: YYYY-MM-DD\n\n## Why now\n- <prioritization rationale>\n\n## Alternatives considered\n- <alternative + reason>\n\n## Expected outcomes\n- <metric: baseline -> target in timeframe>\n\n## Kill criteria / rollback trigger\n- <condition and action>\n\n## Links\n- PDRs: <PDR-IDs>\n- OpenSpec change: <path once promoted>\n";
|
|
5
5
|
export declare const criteriaTemplate = "metrics:\n - id: metric_id\n name: \"Metric name\"\n baseline: 0.00\n target: 0.00\n timeframe: \"30 days post-launch\"\n launch_date: \"YYYY-MM-DD\"\n source:\n type: amplitude\n definition:\n event: event_name\n aggregation: unique_users\n denominator_event: null\n segment: null\n";
|
|
6
6
|
export declare const discoveryTemplate = "# Discovery: BET-XXX <Bet title>\n\n## Problem Framing\n- **Problem statement**: <What problem are we solving and for whom?>\n- **Evidence this is real**: <Data, support tickets, user quotes, etc.>\n- **Why it matters**: <Business or user impact if left unsolved>\n\n## User Research Signals\n- **Research conducted**: <Interviews, surveys, usability tests, etc.>\n- **Key findings**: <What did we learn?>\n- **Assumptions to validate**: <What are we still unsure about?>\n\n## Competitive Context\n- **How others solve this**: <Competitor or adjacent solutions>\n- **Our differentiation**: <Why our approach is better or different>\n- **Gaps / opportunities**: <What's underserved in the market?>\n\n## Open Questions\n- [ ] <Question 1 \u2014 what needs to be answered before committing?>\n- [ ] <Question 2>\n- [ ] <Question 3>\n";
|
|
7
|
+
export declare const sequenceViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');\nconst MAX_LABEL_LEN = 40;\n\nfunction parseSequenceYaml(text) {\n const BUCKETS = ['now', 'next', 'later', 'backlog'];\n const result = Object.fromEntries(BUCKETS.map(b => [b, []]));\n let section = null, item = null, listField = null;\n\n for (const raw of text.split('\\n')) {\n const trimmed = raw.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const indent = raw.length - raw.trimStart().length;\n\n if (indent === 0) {\n const m = trimmed.match(/^(\\w+):/);\n if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }\n continue;\n }\n\n if (!section) continue;\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n item = {}; result[section].push(item); listField = null;\n const rest = trimmed.slice(2);\n const m = rest.match(/^(\\w+):\\s*(.*)/);\n if (m) item[m[1]] = m[2].replace(/^['\"]|['\"]$/g, '').trim();\n continue;\n }\n\n if (indent === 4 && item) {\n const m = trimmed.match(/^(\\w+):\\s*(.*)/);\n if (!m) continue;\n const [, key, val] = m;\n const v = val.trim();\n if (v === '[]') { item[key] = []; listField = null; }\n else if (v === '') { item[key] = []; listField = key; }\n else { item[key] = v.replace(/^['\"]|['\"]$/g, ''); listField = null; }\n continue;\n }\n\n if (indent === 6 && item && listField && trimmed.startsWith('- ')) {\n item[listField].push(trimmed.slice(2).trim());\n }\n }\n return result;\n}\n\nfunction nodeId(betId) { return betId.replace(/-/g, ''); }\n\nfunction truncate(title) {\n return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;\n}\n\nfunction generateMermaidBlock(seq) {\n const lines = ['```mermaid', 'graph TD'];\n const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];\n\n for (const [key, label] of ACTIVE) {\n const bets = seq[key] || [];\n if (!bets.length) continue;\n lines.push(' subgraph ' + label);\n for (const bet of bets) {\n lines.push(' ' + nodeId(bet.id) + '[\"' + bet.id + ': ' + truncate(bet.title) + '\"]');\n }\n lines.push(' end');\n }\n\n for (const [key] of ACTIVE) {\n for (const bet of (seq[key] || [])) {\n for (const blocker of (bet.blocked_by || [])) {\n lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));\n }\n }\n }\n\n lines.push('```');\n return lines.join('\\n');\n}\n\nfunction generateBacklogSection(seq) {\n const backlog = seq.backlog || [];\n if (!backlog.length) return '';\n return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');\n}\n\nfunction main() {\n const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));\n const header = [\n '<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',\n '',\n '# Sequencing Board',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');\n console.log('Written: oprim/sequence-view.md');\n}\n\nmain();\n";
|
|
7
8
|
export declare const kpiReviewTemplate = "# KPI Review: BET-XXX\n\n**Review date:** YYYY-MM-DD\n**Reviewed by:** <name>\n\n| Metric | Baseline | Target | Actual | Status |\n|--------|----------|--------|--------|--------|\n| <metric name> | - | - | - | pending |\n\n## Decision quality\n<Did outcomes validate the decision? What would you do differently?>\n\n## Actions\n- [ ] Update bet-decision outcome section\n- [ ] Update affected PDRs\n- [ ] Re-sequence impacted bets\n";
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.kpiReviewTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
3
|
+
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
4
4
|
exports.configTemplate = configTemplate;
|
|
5
5
|
function configTemplate(projectName, openspecEnabled, graphifyEnabled) {
|
|
6
6
|
return `version: 1
|
|
@@ -124,6 +124,114 @@ exports.discoveryTemplate = `# Discovery: BET-XXX <Bet title>
|
|
|
124
124
|
- [ ] <Question 2>
|
|
125
125
|
- [ ] <Question 3>
|
|
126
126
|
`;
|
|
127
|
+
exports.sequenceViewScriptTemplate = `#!/usr/bin/env node
|
|
128
|
+
'use strict';
|
|
129
|
+
|
|
130
|
+
const fs = require('fs');
|
|
131
|
+
const path = require('path');
|
|
132
|
+
|
|
133
|
+
const SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');
|
|
134
|
+
const OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');
|
|
135
|
+
const MAX_LABEL_LEN = 40;
|
|
136
|
+
|
|
137
|
+
function parseSequenceYaml(text) {
|
|
138
|
+
const BUCKETS = ['now', 'next', 'later', 'backlog'];
|
|
139
|
+
const result = Object.fromEntries(BUCKETS.map(b => [b, []]));
|
|
140
|
+
let section = null, item = null, listField = null;
|
|
141
|
+
|
|
142
|
+
for (const raw of text.split('\\n')) {
|
|
143
|
+
const trimmed = raw.trim();
|
|
144
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
145
|
+
const indent = raw.length - raw.trimStart().length;
|
|
146
|
+
|
|
147
|
+
if (indent === 0) {
|
|
148
|
+
const m = trimmed.match(/^(\\w+):/);
|
|
149
|
+
if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!section) continue;
|
|
154
|
+
|
|
155
|
+
if (indent === 2 && trimmed.startsWith('- ')) {
|
|
156
|
+
item = {}; result[section].push(item); listField = null;
|
|
157
|
+
const rest = trimmed.slice(2);
|
|
158
|
+
const m = rest.match(/^(\\w+):\\s*(.*)/);
|
|
159
|
+
if (m) item[m[1]] = m[2].replace(/^['"]|['"]$/g, '').trim();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (indent === 4 && item) {
|
|
164
|
+
const m = trimmed.match(/^(\\w+):\\s*(.*)/);
|
|
165
|
+
if (!m) continue;
|
|
166
|
+
const [, key, val] = m;
|
|
167
|
+
const v = val.trim();
|
|
168
|
+
if (v === '[]') { item[key] = []; listField = null; }
|
|
169
|
+
else if (v === '') { item[key] = []; listField = key; }
|
|
170
|
+
else { item[key] = v.replace(/^['"]|['"]$/g, ''); listField = null; }
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (indent === 6 && item && listField && trimmed.startsWith('- ')) {
|
|
175
|
+
item[listField].push(trimmed.slice(2).trim());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function nodeId(betId) { return betId.replace(/-/g, ''); }
|
|
182
|
+
|
|
183
|
+
function truncate(title) {
|
|
184
|
+
return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function generateMermaidBlock(seq) {
|
|
188
|
+
const lines = ['\`\`\`mermaid', 'graph TD'];
|
|
189
|
+
const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];
|
|
190
|
+
|
|
191
|
+
for (const [key, label] of ACTIVE) {
|
|
192
|
+
const bets = seq[key] || [];
|
|
193
|
+
if (!bets.length) continue;
|
|
194
|
+
lines.push(' subgraph ' + label);
|
|
195
|
+
for (const bet of bets) {
|
|
196
|
+
lines.push(' ' + nodeId(bet.id) + '["' + bet.id + ': ' + truncate(bet.title) + '"]');
|
|
197
|
+
}
|
|
198
|
+
lines.push(' end');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const [key] of ACTIVE) {
|
|
202
|
+
for (const bet of (seq[key] || [])) {
|
|
203
|
+
for (const blocker of (bet.blocked_by || [])) {
|
|
204
|
+
lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push('\`\`\`');
|
|
210
|
+
return lines.join('\\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function generateBacklogSection(seq) {
|
|
214
|
+
const backlog = seq.backlog || [];
|
|
215
|
+
if (!backlog.length) return '';
|
|
216
|
+
return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function main() {
|
|
220
|
+
const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));
|
|
221
|
+
const header = [
|
|
222
|
+
'<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',
|
|
223
|
+
'<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',
|
|
224
|
+
'',
|
|
225
|
+
'# Sequencing Board',
|
|
226
|
+
'',
|
|
227
|
+
'',
|
|
228
|
+
].join('\\n');
|
|
229
|
+
fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');
|
|
230
|
+
console.log('Written: oprim/sequence-view.md');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
main();
|
|
234
|
+
`;
|
|
127
235
|
exports.kpiReviewTemplate = `# KPI Review: BET-XXX
|
|
128
236
|
|
|
129
237
|
**Review date:** YYYY-MM-DD
|