@open-product-primer/cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +8 -0
- package/dist/lib/install-agent.js +108 -5
- package/dist/lib/templates.d.ts +2 -0
- package/dist/lib/templates.js +21 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -65,6 +65,7 @@ function initCommand() {
|
|
|
65
65
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
|
|
66
66
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
|
|
67
67
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'reviews'));
|
|
68
|
+
(0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
|
|
68
69
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
|
|
69
70
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
70
71
|
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
|
|
@@ -76,11 +77,17 @@ function initCommand() {
|
|
|
76
77
|
const kpiContent = okfEnabled
|
|
77
78
|
? (0, templates_1.okfFrontmatter)('kpi-review', 'KPI Review: BET-XXX') + templates_1.kpiReviewTemplate
|
|
78
79
|
: templates_1.kpiReviewTemplate;
|
|
80
|
+
// Notes always carry frontmatter — minimal tier by default, OKF tier (adds `description`)
|
|
81
|
+
// when opted in — unlike the other three templates, which have no frontmatter when disabled.
|
|
82
|
+
const noteContent = okfEnabled
|
|
83
|
+
? (0, templates_1.okfFrontmatter)('note', '<Note title>') + templates_1.noteTemplate
|
|
84
|
+
: (0, templates_1.noteMinimalFrontmatter)('<Note title>') + templates_1.noteTemplate;
|
|
79
85
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'pdr.md'), pdrContent);
|
|
80
86
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'bet-decision.md'), betContent);
|
|
81
87
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'criteria.yaml'), templates_1.criteriaTemplate);
|
|
82
88
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'kpi-review.md'), kpiContent);
|
|
83
89
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
|
|
90
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'note.md'), noteContent);
|
|
84
91
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
85
92
|
if (okfEnabled) {
|
|
86
93
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'index.md'), (0, templates_1.indexTemplate)(projectName));
|
|
@@ -88,6 +95,7 @@ function initCommand() {
|
|
|
88
95
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'decisions', '.gitkeep'), '');
|
|
89
96
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'bets', '.gitkeep'), '');
|
|
90
97
|
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'reviews', '.gitkeep'), '');
|
|
98
|
+
(0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'notes', '.gitkeep'), '');
|
|
91
99
|
console.log('\n' + chalk_1.default.green('✓') + ' oprim/ workspace created');
|
|
92
100
|
const configStatus = configWritten ? 'written' : 'preserved (already exists)';
|
|
93
101
|
const sequenceStatus = sequenceWritten ? 'written' : 'preserved (already exists)';
|
|
@@ -294,6 +294,7 @@ function removeContextStepFromFile(content) {
|
|
|
294
294
|
exports.CLAUDE_SKILLS = {
|
|
295
295
|
'oprim-pdr': pdrSkill(),
|
|
296
296
|
'oprim-bet': betSkill(),
|
|
297
|
+
'oprim-note': noteSkill(),
|
|
297
298
|
'oprim-criteria': criteriaSkill(),
|
|
298
299
|
'oprim-review': reviewSkill(),
|
|
299
300
|
'oprim-archive': archiveSkill(),
|
|
@@ -301,7 +302,7 @@ exports.CLAUDE_SKILLS = {
|
|
|
301
302
|
};
|
|
302
303
|
// ─── Claude command wrappers (thin, invoke skill) ────────────────────────────
|
|
303
304
|
exports.CLAUDE_COMMANDS = {
|
|
304
|
-
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a prioritized bet
|
|
305
|
+
'promote.md': claudeWrapper('OPRIM: Promote', 'Promote a note into a bet, or a prioritized bet into an OpenSpec change', promoteContent()),
|
|
305
306
|
'sequence.md': claudeWrapper('OPRIM: Sequence', 'Validate and update the primer sequencing board', sequenceContent()),
|
|
306
307
|
'archive.md': claudeWrapper('OPRIM: Archive', 'Archive a completed bet — move it out of the active board', archiveCommandContent()),
|
|
307
308
|
};
|
|
@@ -309,6 +310,7 @@ exports.CLAUDE_COMMANDS = {
|
|
|
309
310
|
exports.POOLSIDE_SKILLS = {
|
|
310
311
|
'oprim-pdr': pdrSkill(),
|
|
311
312
|
'oprim-bet': betSkill(),
|
|
313
|
+
'oprim-note': noteSkill(),
|
|
312
314
|
'oprim-criteria': criteriaSkill(),
|
|
313
315
|
'oprim-review': reviewSkill(),
|
|
314
316
|
'oprim-archive': archiveSkill(),
|
|
@@ -318,15 +320,17 @@ exports.POOLSIDE_SKILLS = {
|
|
|
318
320
|
exports.CURSOR_SKILLS = {
|
|
319
321
|
'oprim-pdr': pdrSkill(),
|
|
320
322
|
'oprim-bet': betSkill(),
|
|
323
|
+
'oprim-note': noteSkill(),
|
|
321
324
|
'oprim-criteria': criteriaSkill(),
|
|
322
325
|
'oprim-review': reviewSkill(),
|
|
323
326
|
};
|
|
324
327
|
// ─── Cursor command files (full inline — no Skill tool in Cursor) ────────────
|
|
325
328
|
exports.CURSOR_COMMANDS = {
|
|
326
|
-
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a prioritized bet
|
|
329
|
+
'oprim-promote.md': cursorWrapper('oprim-promote', 'Promote a note into a bet, or a prioritized bet into an OpenSpec change', promoteContent()),
|
|
327
330
|
'oprim-sequence.md': cursorWrapper('oprim-sequence', 'Validate and update the primer sequencing board', sequenceInlineContent()),
|
|
328
331
|
'oprim-pdr.md': cursorWrapper('oprim-pdr', 'Create a new Product Decision Record with auto-assigned ID', pdrInlineContent()),
|
|
329
332
|
'oprim-bet.md': cursorWrapper('oprim-bet', 'Create a new bet decision and register it on the sequencing board', betInlineContent()),
|
|
333
|
+
'oprim-note.md': cursorWrapper('oprim-note', 'Create a new atomic note for lightweight thinking capture', noteInlineContent()),
|
|
330
334
|
'oprim-criteria.md': cursorWrapper('oprim-criteria', 'Create or append to a criteria.yaml contract for a bet', criteriaInlineContent()),
|
|
331
335
|
'oprim-review.md': cursorWrapper('oprim-review', "Create a KPI review artifact pre-filled from a bet's criteria contract", reviewInlineContent()),
|
|
332
336
|
};
|
|
@@ -535,6 +539,75 @@ Ask: "Do you want to scaffold a discovery.md now? (y/N)"
|
|
|
535
539
|
### 8. Report what was created
|
|
536
540
|
`;
|
|
537
541
|
}
|
|
542
|
+
function noteSkill() {
|
|
543
|
+
return `---
|
|
544
|
+
name: oprim-note
|
|
545
|
+
description: Create a new atomic note in oprim/notes/ for lightweight thinking capture, with tiered frontmatter and optional bet links
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
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.
|
|
549
|
+
|
|
550
|
+
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
551
|
+
|
|
552
|
+
## What you're creating
|
|
553
|
+
|
|
554
|
+
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.
|
|
555
|
+
|
|
556
|
+
## Steps
|
|
557
|
+
|
|
558
|
+
### 1. Get the note title
|
|
559
|
+
If not provided, ask: "What is this note about? (a short title)"
|
|
560
|
+
|
|
561
|
+
### 2. Assign the next NOTE ID
|
|
562
|
+
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.
|
|
563
|
+
|
|
564
|
+
### 2b. Derive the slug
|
|
565
|
+
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>\`.
|
|
566
|
+
Output path: \`oprim/notes/NOTE-NNN-<slug>.md\`
|
|
567
|
+
|
|
568
|
+
### 3. Gather the note body
|
|
569
|
+
Ask: "What's the observation, idea, or connection?" (free-form prose — this becomes the note body).
|
|
570
|
+
|
|
571
|
+
### 4. Gather tags
|
|
572
|
+
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.
|
|
573
|
+
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.
|
|
574
|
+
|
|
575
|
+
### 5. Gather optional bet links
|
|
576
|
+
Ask: "Does this relate to any existing bets? (comma-separated BET-IDs, or Enter to skip)"
|
|
577
|
+
|
|
578
|
+
### 6. Check the frontmatter tier
|
|
579
|
+
Read \`oprim/templates/note.md\`.
|
|
580
|
+
- If it exists and its frontmatter block contains a \`description:\` field, this workspace is on the **OKF tier** — ask for a one-line description.
|
|
581
|
+
- If it exists with no \`description:\` field, use the **minimal tier** — skip the description.
|
|
582
|
+
- 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.
|
|
583
|
+
|
|
584
|
+
### 7. Write oprim/notes/NOTE-NNN-<slug>.md
|
|
585
|
+
|
|
586
|
+
Minimal tier:
|
|
587
|
+
\`\`\`
|
|
588
|
+
---
|
|
589
|
+
type: note
|
|
590
|
+
title: "<title>"
|
|
591
|
+
tags: [<tags>]
|
|
592
|
+
timestamp: <today, ISO 8601>
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
# Note: <title>
|
|
596
|
+
|
|
597
|
+
<body>
|
|
598
|
+
|
|
599
|
+
## Bets
|
|
600
|
+
- <BET-IDs from step 5, or "None">
|
|
601
|
+
\`\`\`
|
|
602
|
+
|
|
603
|
+
OKF tier: same as above, with \`description: "<description>"\` inserted immediately after \`title\`.
|
|
604
|
+
|
|
605
|
+
### 8. Link back from referenced bets
|
|
606
|
+
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).
|
|
607
|
+
|
|
608
|
+
### 9. Report what was created
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
538
611
|
function criteriaSkill() {
|
|
539
612
|
return `---
|
|
540
613
|
name: oprim-criteria
|
|
@@ -874,6 +947,9 @@ function pdrInlineContent() {
|
|
|
874
947
|
function betInlineContent() {
|
|
875
948
|
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). 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.`;
|
|
876
949
|
}
|
|
950
|
+
function noteInlineContent() {
|
|
951
|
+
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.`;
|
|
952
|
+
}
|
|
877
953
|
function criteriaInlineContent() {
|
|
878
954
|
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.`;
|
|
879
955
|
}
|
|
@@ -1032,11 +1108,16 @@ function mergeClaudeSettingsHooks(claudeDir) {
|
|
|
1032
1108
|
// ─── Legacy content (promote remains inline; sequence now delegates to skill) ─
|
|
1033
1109
|
function promoteContent() {
|
|
1034
1110
|
return `
|
|
1035
|
-
Promote a prioritized bet
|
|
1111
|
+
Promote an atomic note into a bet, or a prioritized bet into an OpenSpec change. The promotion path is determined solely by the prefix of the ID argument — there is no separate command for each.
|
|
1036
1112
|
|
|
1037
|
-
**Input**: Specify
|
|
1113
|
+
**Input**: Specify an ID (e.g., \`/oprim:promote BET-042\` or \`/oprim:promote NOTE-005\`) or omit to be prompted.
|
|
1038
1114
|
|
|
1039
|
-
|
|
1115
|
+
### 0. Determine the promotion path from the ID prefix
|
|
1116
|
+
- \`BET-\` → **A. Bet → OpenSpec change**
|
|
1117
|
+
- \`NOTE-\` → **B. Note → Bet**
|
|
1118
|
+
- Anything else → report "Unrecognized ID prefix — expected BET- or NOTE-" and stop. Do not silently do nothing.
|
|
1119
|
+
|
|
1120
|
+
## A. Bet → OpenSpec change
|
|
1040
1121
|
|
|
1041
1122
|
1. **Locate the bet** — read \`oprim/bets/BET-XXX/bet-decision.md\`
|
|
1042
1123
|
2. **Validate status** — decision must be "Build now"
|
|
@@ -1056,6 +1137,16 @@ Promote a prioritized bet to an OpenSpec change and link criteria contracts.
|
|
|
1056
1137
|
- \`specs/<capability>/spec.md\` for each capability in \`## Capabilities\`
|
|
1057
1138
|
If any artifact is missing, create it before reporting done.
|
|
1058
1139
|
8. **Report** — show what was linked and what remains for engineering
|
|
1140
|
+
|
|
1141
|
+
## B. Note → Bet
|
|
1142
|
+
|
|
1143
|
+
1. **Locate the note** — read \`oprim/notes/NOTE-XXX-<slug>.md\`
|
|
1144
|
+
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
|
|
1145
|
+
3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
|
|
1146
|
+
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.
|
|
1147
|
+
5. **Register the new bet** — append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`
|
|
1148
|
+
6. **Link back** — add \`Bets: BET-NNN\` to the note (creating or extending its \`## Bets\` section)
|
|
1149
|
+
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
|
|
1059
1150
|
`;
|
|
1060
1151
|
}
|
|
1061
1152
|
function sequenceContent() {
|
|
@@ -1115,6 +1206,18 @@ Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
|
|
|
1115
1206
|
8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\`.
|
|
1116
1207
|
9. Report what was created.
|
|
1117
1208
|
|
|
1209
|
+
### Note authoring (oprim-note)
|
|
1210
|
+
Create a new note in \`oprim/notes/\` for lightweight thinking capture — not a bet, no owner or kill criterion.
|
|
1211
|
+
|
|
1212
|
+
1. Ask for a short title.
|
|
1213
|
+
2. Assign next NOTE ID: scan \`oprim/notes/NOTE-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
|
|
1214
|
+
3. Ask for the note body (free-form), tags, and optional related BET-IDs.
|
|
1215
|
+
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).
|
|
1216
|
+
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\`.
|
|
1217
|
+
6. Write \`oprim/notes/NOTE-NNN-<slug>.md\` with the correct frontmatter tier and a \`## Bets\` section.
|
|
1218
|
+
7. For each related bet, append \`- Notes: NOTE-NNN\` to that bet's \`## Links\` section.
|
|
1219
|
+
8. Report what was created.
|
|
1220
|
+
|
|
1118
1221
|
### Criteria authoring (oprim-criteria)
|
|
1119
1222
|
Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
|
|
1120
1223
|
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare function configTemplate(projectName: string, openspecEnabled: boolean, graphifyEnabled: boolean, okfEnabled: boolean): string;
|
|
2
2
|
export declare function okfFrontmatter(type: string, titleHint: string): string;
|
|
3
|
+
export declare function noteMinimalFrontmatter(titleHint: string): string;
|
|
3
4
|
export declare function indexTemplate(projectName: string): string;
|
|
4
5
|
export declare const sequenceTemplate = "wip_limits:\n now: 2\n\nnow: []\nnext: []\nlater: []\nbacklog: []\n";
|
|
5
6
|
export declare const pdrTemplate = "# PDR-XXX: <Decision title>\n\n## Status\nProposed | Accepted | Deprecated | Superseded by PDR-YYY\n\n## Context\n<What forced this decision?>\n\n## Decision\n<Clear statement of what is decided>\n\n## Alternatives considered\n- <Alternative A and why rejected>\n- <Alternative B and why rejected>\n\n## Consequences\n- Positive: <...>\n- Trade-offs: <...>\n- Follow-ups: <...>\n\n## Evidence\n- <Research link>\n- <Data link>\n\n## Related\n- Bets: <BET-IDs>\n- OpenSpec: <change paths>\n- Supersedes: <PDR-ID or none>\n";
|
|
6
7
|
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";
|
|
8
|
+
export declare const noteTemplate = "# Note: <Note title>\n\n<Capture the observation, idea, or connection while it's fresh.>\n\n## Bets\n- <BET-IDs this note relates to, or \"None\">\n";
|
|
7
9
|
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";
|
|
8
10
|
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";
|
|
9
11
|
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";
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
3
|
+
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
4
4
|
exports.configTemplate = configTemplate;
|
|
5
5
|
exports.okfFrontmatter = okfFrontmatter;
|
|
6
|
+
exports.noteMinimalFrontmatter = noteMinimalFrontmatter;
|
|
6
7
|
exports.indexTemplate = indexTemplate;
|
|
7
8
|
function configTemplate(projectName, openspecEnabled, graphifyEnabled, okfEnabled) {
|
|
8
9
|
return `version: 1
|
|
@@ -39,6 +40,18 @@ tags: []
|
|
|
39
40
|
timestamp: YYYY-MM-DDTHH:MM:SSZ
|
|
40
41
|
---
|
|
41
42
|
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
// Minimal frontmatter tier for notes — always on, independent of OKF opt-in.
|
|
46
|
+
// Deliberately smaller than okfFrontmatter(): no `description` field.
|
|
47
|
+
function noteMinimalFrontmatter(titleHint) {
|
|
48
|
+
return `---
|
|
49
|
+
type: note
|
|
50
|
+
title: "${titleHint}"
|
|
51
|
+
tags: []
|
|
52
|
+
timestamp: YYYY-MM-DDTHH:MM:SSZ
|
|
53
|
+
---
|
|
54
|
+
|
|
42
55
|
`;
|
|
43
56
|
}
|
|
44
57
|
function indexTemplate(projectName) {
|
|
@@ -114,6 +127,13 @@ exports.betDecisionTemplate = `# Decision: BET-XXX <Bet title>
|
|
|
114
127
|
- PDRs: <PDR-IDs>
|
|
115
128
|
- OpenSpec change: <path once promoted>
|
|
116
129
|
`;
|
|
130
|
+
exports.noteTemplate = `# Note: <Note title>
|
|
131
|
+
|
|
132
|
+
<Capture the observation, idea, or connection while it's fresh.>
|
|
133
|
+
|
|
134
|
+
## Bets
|
|
135
|
+
- <BET-IDs this note relates to, or "None">
|
|
136
|
+
`;
|
|
117
137
|
exports.criteriaTemplate = `metrics:
|
|
118
138
|
- id: metric_id
|
|
119
139
|
name: "Metric name"
|