@open-product-primer/cli 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +4 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +188 -0
- package/dist/commands/doctor.js +49 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +131 -0
- package/dist/lib/config-merge.js +5 -1
- package/dist/lib/install-agent.js +78 -938
- package/dist/lib/remote-context.d.ts +50 -0
- package/dist/lib/remote-context.js +299 -0
- package/dist/lib/spec-delta.d.ts +30 -0
- package/dist/lib/spec-delta.js +216 -0
- package/dist/lib/templates.js +2 -1
- package/dist/lib/validate-checks.d.ts +13 -0
- package/dist/lib/validate-checks.js +145 -0
- package/dist/lib/workflow-renderer.d.ts +19 -0
- package/dist/lib/workflow-renderer.js +103 -0
- package/dist/lib/workflow-schema.d.ts +37 -0
- package/dist/lib/workflow-schema.js +126 -0
- package/dist/workflows/archive.inline.md +10 -0
- package/dist/workflows/archive.schema.yaml +13 -0
- package/dist/workflows/archive.template.md +106 -0
- package/dist/workflows/bet.cursor-command.md +1 -0
- package/dist/workflows/bet.inline.md +13 -0
- package/dist/workflows/bet.schema.yaml +14 -0
- package/dist/workflows/bet.template.md +114 -0
- package/dist/workflows/context.schema.yaml +13 -0
- package/dist/workflows/context.template.md +34 -0
- package/dist/workflows/criteria.cursor-command.md +1 -0
- package/dist/workflows/criteria.inline.md +11 -0
- package/dist/workflows/criteria.schema.yaml +14 -0
- package/dist/workflows/criteria.template.md +52 -0
- package/dist/workflows/note.cursor-command.md +1 -0
- package/dist/workflows/note.inline.md +11 -0
- package/dist/workflows/note.schema.yaml +14 -0
- package/dist/workflows/note.template.md +66 -0
- package/dist/workflows/pdr.cursor-command.md +1 -0
- package/dist/workflows/pdr.inline.md +10 -0
- package/dist/workflows/pdr.schema.yaml +14 -0
- package/dist/workflows/pdr.template.md +67 -0
- package/dist/workflows/promote.native.template.md +29 -0
- package/dist/workflows/promote.none.template.md +25 -0
- package/dist/workflows/promote.openspec.template.md +40 -0
- package/dist/workflows/promote.schema.yaml +14 -0
- package/dist/workflows/review.cursor-command.md +1 -0
- package/dist/workflows/review.inline.md +11 -0
- package/dist/workflows/review.schema.yaml +14 -0
- package/dist/workflows/review.template.md +69 -0
- package/dist/workflows/sequence.cursor-command.md +12 -0
- package/dist/workflows/sequence.inline.md +10 -0
- package/dist/workflows/sequence.schema.yaml +13 -0
- package/dist/workflows/sequence.template.md +88 -0
- package/dist/workflows/spec-authoring.schema.yaml +13 -0
- package/dist/workflows/spec-authoring.template.md +68 -0
- package/package.json +2 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.checkBetDefinitionOfDone = checkBetDefinitionOfDone;
|
|
37
|
+
exports.checkSpecDeltaDrift = checkSpecDeltaDrift;
|
|
38
|
+
exports.checkCrossBetConflicts = checkCrossBetConflicts;
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const spec_delta_1 = require("./spec-delta");
|
|
42
|
+
function normalizeHeader(header) {
|
|
43
|
+
return header.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
function extractBetId(dirName) {
|
|
46
|
+
const match = dirName.match(/^(BET-\d+)/);
|
|
47
|
+
return match ? match[1] : dirName;
|
|
48
|
+
}
|
|
49
|
+
function activeBetDirs(betsDir) {
|
|
50
|
+
if (!fs.existsSync(betsDir))
|
|
51
|
+
return [];
|
|
52
|
+
return fs
|
|
53
|
+
.readdirSync(betsDir, { withFileTypes: true })
|
|
54
|
+
.filter((e) => e.isDirectory() && e.name !== 'archived');
|
|
55
|
+
}
|
|
56
|
+
function isPromoted(betDecisionContent) {
|
|
57
|
+
const linksMatch = betDecisionContent.match(/## Links\n([\s\S]*?)(?=\n## |$)/);
|
|
58
|
+
const linksSection = linksMatch ? linksMatch[1] : betDecisionContent;
|
|
59
|
+
const lineMatch = linksSection.match(/^- *OpenSpec change:\s*(.*)$/m);
|
|
60
|
+
if (!lineMatch)
|
|
61
|
+
return false;
|
|
62
|
+
const value = lineMatch[1].trim().replace(/^<|>$/g, '').trim();
|
|
63
|
+
if (value === '')
|
|
64
|
+
return false;
|
|
65
|
+
if (/to be filled when promoted/i.test(value))
|
|
66
|
+
return false;
|
|
67
|
+
if (/^path once promoted$/i.test(value))
|
|
68
|
+
return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Flags a promoted bet (one whose bet-decision.md links a non-placeholder OpenSpec
|
|
73
|
+
* change) that has no criteria.yaml alongside it. Un-promoted bets are not checked.
|
|
74
|
+
*/
|
|
75
|
+
function checkBetDefinitionOfDone(projectRoot, checks) {
|
|
76
|
+
const betsDir = path.join(projectRoot, 'oprim', 'bets');
|
|
77
|
+
for (const entry of activeBetDirs(betsDir)) {
|
|
78
|
+
const betDir = path.join(betsDir, entry.name);
|
|
79
|
+
const decisionPath = path.join(betDir, 'bet-decision.md');
|
|
80
|
+
if (!fs.existsSync(decisionPath))
|
|
81
|
+
continue;
|
|
82
|
+
const content = fs.readFileSync(decisionPath, 'utf-8');
|
|
83
|
+
if (!isPromoted(content))
|
|
84
|
+
continue;
|
|
85
|
+
const hasCriteria = fs.existsSync(path.join(betDir, 'criteria.yaml'));
|
|
86
|
+
if (!hasCriteria) {
|
|
87
|
+
const betId = extractBetId(entry.name);
|
|
88
|
+
checks.push({
|
|
89
|
+
name: `bet: ${betId} promoted without criteria.yaml`,
|
|
90
|
+
pass: false,
|
|
91
|
+
note: `Add criteria.yaml to oprim/bets/${entry.name}/, or run the oprim-criteria skill`,
|
|
92
|
+
required: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Flags a MODIFIED/REMOVED requirement in an active bet's spec delta whose header no
|
|
99
|
+
* longer text-matches (whitespace-insensitive) current truth.
|
|
100
|
+
*/
|
|
101
|
+
function checkSpecDeltaDrift(projectRoot, checks) {
|
|
102
|
+
const betsDir = path.join(projectRoot, 'oprim', 'bets');
|
|
103
|
+
for (const entry of activeBetDirs(betsDir)) {
|
|
104
|
+
const betId = extractBetId(entry.name);
|
|
105
|
+
const specsDir = path.join(betsDir, entry.name, 'specs');
|
|
106
|
+
if (!fs.existsSync(specsDir))
|
|
107
|
+
continue;
|
|
108
|
+
const capEntries = fs.readdirSync(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
109
|
+
for (const capEntry of capEntries) {
|
|
110
|
+
const deltaPath = path.join(specsDir, capEntry.name, 'spec.md');
|
|
111
|
+
if (!fs.existsSync(deltaPath))
|
|
112
|
+
continue;
|
|
113
|
+
const deltaContent = fs.readFileSync(deltaPath, 'utf-8');
|
|
114
|
+
const currentTruthPath = path.join(projectRoot, 'oprim', 'specs', capEntry.name, 'spec.md');
|
|
115
|
+
const currentHeaders = fs.existsSync(currentTruthPath)
|
|
116
|
+
? (0, spec_delta_1.parseCurrentTruthHeaders)(fs.readFileSync(currentTruthPath, 'utf-8'))
|
|
117
|
+
: [];
|
|
118
|
+
for (const section of ['MODIFIED', 'REMOVED']) {
|
|
119
|
+
for (const header of (0, spec_delta_1.parseRequirementHeaders)(deltaContent, section)) {
|
|
120
|
+
const matches = currentHeaders.some((h) => normalizeHeader(h) === normalizeHeader(header));
|
|
121
|
+
if (!matches) {
|
|
122
|
+
checks.push({
|
|
123
|
+
name: `spec-delta: ${betId}'s ${section} requirement "${header}" in ${capEntry.name} no longer matches current truth`,
|
|
124
|
+
pass: false,
|
|
125
|
+
note: `No matching requirement header found in oprim/specs/${capEntry.name}/spec.md`,
|
|
126
|
+
required: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Surfaces overlapping requirement headers across active bets' spec deltas. */
|
|
135
|
+
function checkCrossBetConflicts(projectRoot, checks) {
|
|
136
|
+
const betsDir = path.join(projectRoot, 'oprim', 'bets');
|
|
137
|
+
for (const conflict of (0, spec_delta_1.findCrossBetConflicts)(betsDir)) {
|
|
138
|
+
checks.push({
|
|
139
|
+
name: `spec-delta: ${extractBetId(conflict.betA)} and ${extractBetId(conflict.betB)} both touch "${conflict.header}" in ${conflict.capability}`,
|
|
140
|
+
pass: false,
|
|
141
|
+
note: 'Overlapping requirement header between active bets — archiving one first applies last-write-wins to the other (no 3-way merge)',
|
|
142
|
+
required: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full workflow body — a workflow's own frontmatter (`name:`/`description:`) plus its
|
|
3
|
+
* instructional steps — used verbatim as the Claude/Cursor/Poolside SKILL.md content.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderSkillBody(id: string, projectRoot?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* A thin Claude command wrapper that delegates to the workflow's skill via the Skill tool
|
|
8
|
+
* (archive/sequence/context), or promote's dynamic per-framework content.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderClaudeCommand(id: string, projectRoot?: string, framework?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* A full Cursor command file: promote's dynamic per-framework content, or a condensed
|
|
13
|
+
* `<id>.cursor-command.md` body (Cursor has no Skill tool, so content is always inlined).
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderCursorCommand(id: string, projectRoot?: string, framework?: string): string;
|
|
16
|
+
/** A workflow's condensed section for the shared Codex/Gemini/Poolside inline instruction block. */
|
|
17
|
+
export declare function renderInlineSection(id: string): string | null;
|
|
18
|
+
/** The full `<!-- oprim:start -->`-delimited inline instruction block written into AGENTS.md/GEMINI.md. */
|
|
19
|
+
export declare function renderAgentInstructions(): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderSkillBody = renderSkillBody;
|
|
4
|
+
exports.renderClaudeCommand = renderClaudeCommand;
|
|
5
|
+
exports.renderCursorCommand = renderCursorCommand;
|
|
6
|
+
exports.renderInlineSection = renderInlineSection;
|
|
7
|
+
exports.renderAgentInstructions = renderAgentInstructions;
|
|
8
|
+
const workflow_schema_1 = require("./workflow-schema");
|
|
9
|
+
// Fixed order the pre-refactor oprimWorkflowsInline() concatenated its per-workflow sections in —
|
|
10
|
+
// preserved here so Codex/Gemini/Poolside output is unchanged.
|
|
11
|
+
const INLINE_SECTION_ORDER = ['bet', 'note', 'criteria', 'pdr', 'review', 'archive', 'sequence'];
|
|
12
|
+
function claudeWrapper(name, description, body) {
|
|
13
|
+
return `---
|
|
14
|
+
name: "${name}"
|
|
15
|
+
description: ${description}
|
|
16
|
+
category: Workflow
|
|
17
|
+
tags: [workflow, primer]
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
${description}.
|
|
21
|
+
|
|
22
|
+
${body}`;
|
|
23
|
+
}
|
|
24
|
+
function cursorWrapper(id, description, body) {
|
|
25
|
+
return `---
|
|
26
|
+
name: /${id}
|
|
27
|
+
id: ${id}
|
|
28
|
+
category: Workflow
|
|
29
|
+
description: ${description}
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
${body}`;
|
|
33
|
+
}
|
|
34
|
+
function promoteVariantBody(id, schema, framework) {
|
|
35
|
+
const variants = schema.variants;
|
|
36
|
+
if (!variants)
|
|
37
|
+
throw new Error(`workflow "${id}" has no variants configured`);
|
|
38
|
+
const variantId = variants.includes(framework) ? framework : variants[0];
|
|
39
|
+
const content = (0, workflow_schema_1.loadWorkflowVariant)(`${id}.${variantId}.template.md`);
|
|
40
|
+
if (content === null) {
|
|
41
|
+
throw new Error(`Missing variant template ${id}.${variantId}.template.md for workflow "${id}"`);
|
|
42
|
+
}
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The full workflow body — a workflow's own frontmatter (`name:`/`description:`) plus its
|
|
47
|
+
* instructional steps — used verbatim as the Claude/Cursor/Poolside SKILL.md content.
|
|
48
|
+
*/
|
|
49
|
+
function renderSkillBody(id, projectRoot) {
|
|
50
|
+
return (0, workflow_schema_1.loadWorkflowTemplate)(id, projectRoot);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A thin Claude command wrapper that delegates to the workflow's skill via the Skill tool
|
|
54
|
+
* (archive/sequence/context), or promote's dynamic per-framework content.
|
|
55
|
+
*/
|
|
56
|
+
function renderClaudeCommand(id, projectRoot, framework = 'openspec') {
|
|
57
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
|
|
58
|
+
if (!schema.claude.command) {
|
|
59
|
+
throw new Error(`workflow "${id}" has no Claude command target`);
|
|
60
|
+
}
|
|
61
|
+
const body = schema.variants
|
|
62
|
+
? promoteVariantBody(id, schema, framework)
|
|
63
|
+
: schema.skillName
|
|
64
|
+
? `Use the Skill tool to invoke the \`${schema.skillName}\` skill.`
|
|
65
|
+
: (() => {
|
|
66
|
+
throw new Error(`workflow "${id}" Claude command has neither a skillName nor variants`);
|
|
67
|
+
})();
|
|
68
|
+
return claudeWrapper(schema.title ?? schema.id, schema.description, body);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A full Cursor command file: promote's dynamic per-framework content, or a condensed
|
|
72
|
+
* `<id>.cursor-command.md` body (Cursor has no Skill tool, so content is always inlined).
|
|
73
|
+
*/
|
|
74
|
+
function renderCursorCommand(id, projectRoot, framework = 'openspec') {
|
|
75
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
|
|
76
|
+
if (!schema.cursor.command) {
|
|
77
|
+
throw new Error(`workflow "${id}" has no Cursor command target`);
|
|
78
|
+
}
|
|
79
|
+
const cursorId = schema.cursor.command.replace(/\.md$/, '');
|
|
80
|
+
const body = schema.variants
|
|
81
|
+
? promoteVariantBody(id, schema, framework)
|
|
82
|
+
: (() => {
|
|
83
|
+
const content = (0, workflow_schema_1.loadWorkflowVariant)(`${id}.cursor-command.md`);
|
|
84
|
+
if (content === null)
|
|
85
|
+
throw new Error(`Missing ${id}.cursor-command.md for workflow "${id}"`);
|
|
86
|
+
return content;
|
|
87
|
+
})();
|
|
88
|
+
return cursorWrapper(cursorId, schema.cursorDescription ?? schema.description, body);
|
|
89
|
+
}
|
|
90
|
+
/** A workflow's condensed section for the shared Codex/Gemini/Poolside inline instruction block. */
|
|
91
|
+
function renderInlineSection(id) {
|
|
92
|
+
return (0, workflow_schema_1.loadWorkflowVariant)(`${id}.inline.md`);
|
|
93
|
+
}
|
|
94
|
+
/** The full `<!-- oprim:start -->`-delimited inline instruction block written into AGENTS.md/GEMINI.md. */
|
|
95
|
+
function renderAgentInstructions() {
|
|
96
|
+
const sections = INLINE_SECTION_ORDER.map((id) => {
|
|
97
|
+
const content = renderInlineSection(id);
|
|
98
|
+
if (content === null)
|
|
99
|
+
throw new Error(`Missing inline section for workflow "${id}"`);
|
|
100
|
+
return content;
|
|
101
|
+
});
|
|
102
|
+
return '\n## oprim workflows\n\n' + sections.join('\n\n') + '\n';
|
|
103
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface WorkflowAgentTarget {
|
|
2
|
+
skill: boolean;
|
|
3
|
+
command: string | null;
|
|
4
|
+
}
|
|
5
|
+
export interface WorkflowSchema {
|
|
6
|
+
id: string;
|
|
7
|
+
skillName: string | null;
|
|
8
|
+
title: string | null;
|
|
9
|
+
description: string;
|
|
10
|
+
cursorDescription: string | null;
|
|
11
|
+
claude: WorkflowAgentTarget;
|
|
12
|
+
cursor: WorkflowAgentTarget;
|
|
13
|
+
poolside: {
|
|
14
|
+
skill: boolean;
|
|
15
|
+
};
|
|
16
|
+
inline: boolean;
|
|
17
|
+
variants: string[] | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Loads a workflow's schema.yaml, resolving a project-level
|
|
21
|
+
* `oprim/workflows/<id>.schema.yaml` override over the CLI-bundled default.
|
|
22
|
+
* Throws an actionable, file-naming error if an override exists but fails to parse
|
|
23
|
+
* or is missing required fields — never silently falls back to the bundled default.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadWorkflowSchema(id: string, projectRoot?: string): WorkflowSchema;
|
|
26
|
+
/**
|
|
27
|
+
* Loads a workflow's template.md, resolving a project-level
|
|
28
|
+
* `oprim/workflows/<id>.template.md` override over the CLI-bundled default.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadWorkflowTemplate(id: string, projectRoot?: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Loads a bundled, non-overridable variant file (Cursor's condensed command body, the
|
|
33
|
+
* Codex/Gemini/Poolside inline section, promote's per-framework bodies). Only
|
|
34
|
+
* `<id>.schema.yaml` and `<id>.template.md` are project-forkable — see
|
|
35
|
+
* workflow-schema-authoring spec. Returns null if the workflow has no such variant.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadWorkflowVariant(filename: string): string | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadWorkflowSchema = loadWorkflowSchema;
|
|
37
|
+
exports.loadWorkflowTemplate = loadWorkflowTemplate;
|
|
38
|
+
exports.loadWorkflowVariant = loadWorkflowVariant;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const yaml = __importStar(require("js-yaml"));
|
|
42
|
+
// Bundled workflow definitions ship inside the compiled package at dist/workflows/ (copied
|
|
43
|
+
// there at build time by scripts/copy-workflow-assets.js — see package.json's "build" script).
|
|
44
|
+
const BUNDLED_DIR = path.join(__dirname, '..', 'workflows');
|
|
45
|
+
function overridesDir(projectRoot) {
|
|
46
|
+
return path.join(projectRoot, 'oprim', 'workflows');
|
|
47
|
+
}
|
|
48
|
+
function requireField(obj, field, filePath) {
|
|
49
|
+
if (!(field in obj) || obj[field] === undefined || obj[field] === null) {
|
|
50
|
+
throw new Error(`Invalid workflow schema at ${filePath}: missing required field "${field}"`);
|
|
51
|
+
}
|
|
52
|
+
return obj[field];
|
|
53
|
+
}
|
|
54
|
+
function parseSchema(raw, filePath) {
|
|
55
|
+
let doc;
|
|
56
|
+
try {
|
|
57
|
+
doc = yaml.load(raw);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Invalid workflow schema at ${filePath}: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
|
|
63
|
+
throw new Error(`Invalid workflow schema at ${filePath}: expected a YAML mapping`);
|
|
64
|
+
}
|
|
65
|
+
const obj = doc;
|
|
66
|
+
requireField(obj, 'id', filePath);
|
|
67
|
+
requireField(obj, 'description', filePath);
|
|
68
|
+
const claude = requireField(obj, 'claude', filePath);
|
|
69
|
+
const cursor = requireField(obj, 'cursor', filePath);
|
|
70
|
+
const poolside = requireField(obj, 'poolside', filePath);
|
|
71
|
+
requireField(obj, 'inline', filePath);
|
|
72
|
+
return {
|
|
73
|
+
id: obj.id,
|
|
74
|
+
skillName: obj.skillName ?? null,
|
|
75
|
+
title: obj.title ?? null,
|
|
76
|
+
description: obj.description,
|
|
77
|
+
cursorDescription: obj.cursorDescription ?? null,
|
|
78
|
+
claude: { skill: Boolean(claude.skill), command: claude.command ?? null },
|
|
79
|
+
cursor: { skill: Boolean(cursor.skill), command: cursor.command ?? null },
|
|
80
|
+
poolside: { skill: Boolean(poolside.skill) },
|
|
81
|
+
inline: Boolean(obj.inline),
|
|
82
|
+
variants: obj.variants ?? null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Loads a workflow's schema.yaml, resolving a project-level
|
|
87
|
+
* `oprim/workflows/<id>.schema.yaml` override over the CLI-bundled default.
|
|
88
|
+
* Throws an actionable, file-naming error if an override exists but fails to parse
|
|
89
|
+
* or is missing required fields — never silently falls back to the bundled default.
|
|
90
|
+
*/
|
|
91
|
+
function loadWorkflowSchema(id, projectRoot) {
|
|
92
|
+
if (projectRoot) {
|
|
93
|
+
const overridePath = path.join(overridesDir(projectRoot), `${id}.schema.yaml`);
|
|
94
|
+
if (fs.existsSync(overridePath)) {
|
|
95
|
+
return parseSchema(fs.readFileSync(overridePath, 'utf-8'), overridePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const bundledPath = path.join(BUNDLED_DIR, `${id}.schema.yaml`);
|
|
99
|
+
return parseSchema(fs.readFileSync(bundledPath, 'utf-8'), bundledPath);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Loads a workflow's template.md, resolving a project-level
|
|
103
|
+
* `oprim/workflows/<id>.template.md` override over the CLI-bundled default.
|
|
104
|
+
*/
|
|
105
|
+
function loadWorkflowTemplate(id, projectRoot) {
|
|
106
|
+
if (projectRoot) {
|
|
107
|
+
const overridePath = path.join(overridesDir(projectRoot), `${id}.template.md`);
|
|
108
|
+
if (fs.existsSync(overridePath)) {
|
|
109
|
+
return fs.readFileSync(overridePath, 'utf-8');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const bundledPath = path.join(BUNDLED_DIR, `${id}.template.md`);
|
|
113
|
+
return fs.readFileSync(bundledPath, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Loads a bundled, non-overridable variant file (Cursor's condensed command body, the
|
|
117
|
+
* Codex/Gemini/Poolside inline section, promote's per-framework bodies). Only
|
|
118
|
+
* `<id>.schema.yaml` and `<id>.template.md` are project-forkable — see
|
|
119
|
+
* workflow-schema-authoring spec. Returns null if the workflow has no such variant.
|
|
120
|
+
*/
|
|
121
|
+
function loadWorkflowVariant(filename) {
|
|
122
|
+
const p = path.join(BUNDLED_DIR, filename);
|
|
123
|
+
if (!fs.existsSync(p))
|
|
124
|
+
return null;
|
|
125
|
+
return fs.readFileSync(p, 'utf-8');
|
|
126
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
### Bet archiving (oprim-archive)
|
|
2
|
+
Archive a completed bet.
|
|
3
|
+
|
|
4
|
+
1. Ask for bet ID (accept bet-005, 005, 5, BET-005 — normalize to BET-NNN).
|
|
5
|
+
2. Verify `oprim/bets/BET-NNN/` exists.
|
|
6
|
+
3. Check `oprim/sequence.yaml` for entries where `blocked_by` or `unlocks` reference the target bet — warn if found. Also check other active bet dirs for delta specs against the same requirement (matching `### Requirement:` headers, whitespace-insensitive) — warn if an overlap is found. Ask "Archive anyway? (y/N)" if either warning fires.
|
|
7
|
+
4. If `oprim/bets/BET-NNN/specs/` exists, fold each capability's `## ADDED`/`## MODIFIED`/`## REMOVED Requirements` delta into `oprim/specs/<capability>/spec.md` (matching by `### Requirement:` header; create the current-truth file if the delta is entirely ADDED) — last-write-wins on overlaps, no 3-way merge. Skip this step entirely if no `specs/` dir is present.
|
|
8
|
+
5. Move directory: `oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN`.
|
|
9
|
+
6. Remove the bet entry from `oprim/sequence.yaml`.
|
|
10
|
+
7. Report what was done.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
id: archive
|
|
2
|
+
skillName: oprim-archive
|
|
3
|
+
title: "OPRIM: Archive"
|
|
4
|
+
description: Archive a completed bet — move it out of the active board
|
|
5
|
+
claude:
|
|
6
|
+
skill: true
|
|
7
|
+
command: archive.md
|
|
8
|
+
cursor:
|
|
9
|
+
skill: false
|
|
10
|
+
command: null
|
|
11
|
+
poolside:
|
|
12
|
+
skill: true
|
|
13
|
+
inline: true
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oprim-archive
|
|
3
|
+
description: Archive a completed bet — moves it to oprim/bets/archived/, removes its sequence.yaml entry, and folds any spec deltas under its specs/ directory into oprim/specs/ current truth
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Archive a completed bet by moving it to `oprim/bets/archived/`, removing it from `sequence.yaml`, and (if present) merging its spec deltas into current truth.
|
|
7
|
+
|
|
8
|
+
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
### 1. Get the bet ID
|
|
13
|
+
|
|
14
|
+
If provided as an argument (e.g., `/oprim:archive BET-005`), use it directly.
|
|
15
|
+
|
|
16
|
+
If not provided, ask: "Which bet ID would you like to archive? (e.g., BET-005)"
|
|
17
|
+
|
|
18
|
+
Normalize the input: accept `bet-005`, `005`, `5`, or `BET-005` — always treat as `BET-NNN` zero-padded to 3 digits.
|
|
19
|
+
|
|
20
|
+
### 2. Resolve the bet directory
|
|
21
|
+
|
|
22
|
+
Look for the bet directory in `oprim/bets/` using two patterns:
|
|
23
|
+
1. Exact match: `oprim/bets/BET-NNN/` (legacy non-slug format)
|
|
24
|
+
2. Slug variant: any directory starting with `BET-NNN-` (e.g., `BET-NNN-<slug>/`)
|
|
25
|
+
|
|
26
|
+
Use whichever pattern matches. Call this the **resolved directory name**.
|
|
27
|
+
|
|
28
|
+
If multiple directories match (e.g., both `BET-NNN/` and `BET-NNN-slug/` exist):
|
|
29
|
+
- Report: "Ambiguous: found multiple directories for BET-NNN: [list them]. Please archive manually."
|
|
30
|
+
- Stop.
|
|
31
|
+
|
|
32
|
+
If neither pattern matches:
|
|
33
|
+
- Report: "Bet BET-NNN was not found in oprim/bets/. Nothing was changed."
|
|
34
|
+
- Stop.
|
|
35
|
+
|
|
36
|
+
### 3. Check for active dependencies and concurrent spec-delta conflicts
|
|
37
|
+
|
|
38
|
+
Read `oprim/sequence.yaml`. Scan every entry across all buckets (now, next, later, backlog) for any entry whose `blocked_by` or `unlocks` list contains the target bet ID.
|
|
39
|
+
|
|
40
|
+
Separately, if `oprim/bets/<resolved-dir>/specs/` exists: for each `<capability>/spec.md` delta file under it, extract every `### Requirement:` header from its `## ADDED`/`## MODIFIED`/`## REMOVED Requirements` sections. Then scan every other bet directory directly under `oprim/bets/` (excluding `archived/` and the bet being archived) for a `specs/<capability>/spec.md` file for the same capability; if one exists, extract its `### Requirement:` headers too. Flag any header that matches (whitespace-insensitive) between the archiving bet's delta and another still-active bet's delta as an **overlap**.
|
|
41
|
+
|
|
42
|
+
If either sequence.yaml dependents or delta overlaps are found:
|
|
43
|
+
- Show a combined warning listing each dependent entry and each overlapping requirement.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
```
|
|
47
|
+
⚠ Warning: BET-005 is referenced by active bets:
|
|
48
|
+
- BET-007 (blocked_by: [BET-005])
|
|
49
|
+
- BET-008 (unlocks: [BET-005])
|
|
50
|
+
⚠ Warning: BET-005's delta for requirement "The system SHALL ..." in capability foo overlaps with active bet BET-009's delta for the same requirement. Archiving BET-005 now applies its version to oprim/specs/foo/spec.md; if BET-009 archives later, its version will overwrite this requirement again (last-write-wins — no 3-way merge is attempted).
|
|
51
|
+
```
|
|
52
|
+
- Ask: "Archive BET-NNN anyway? (y/N)"
|
|
53
|
+
- If "n" or Enter: stop, no changes made.
|
|
54
|
+
- If "y": proceed.
|
|
55
|
+
|
|
56
|
+
If neither is found: proceed without warning.
|
|
57
|
+
|
|
58
|
+
### 4. Fold spec deltas into current truth
|
|
59
|
+
|
|
60
|
+
If `oprim/bets/<resolved-dir>/specs/` does not exist: skip this step entirely and go to Step 5 — archive behavior is unchanged from before spec deltas existed.
|
|
61
|
+
|
|
62
|
+
Otherwise, for each capability subdirectory under `oprim/bets/<resolved-dir>/specs/` containing a `spec.md`:
|
|
63
|
+
|
|
64
|
+
1. Read the delta file's `## ADDED Requirements` / `## MODIFIED Requirements` / `## REMOVED Requirements` sections. Each `### Requirement:` block runs from its header through its body and any `#### Scenario:` sub-entries, up to the next `### Requirement:` or `## ` header.
|
|
65
|
+
2. Read `oprim/specs/<capability>/spec.md` if it exists (current truth uses a single flat `## Requirements` section).
|
|
66
|
+
- **If it does not exist:**
|
|
67
|
+
- If the delta is entirely `## ADDED Requirements` (no MODIFIED/REMOVED sections): create `oprim/specs/<capability>/spec.md` with a `## Requirements` header and append each ADDED requirement block beneath it.
|
|
68
|
+
- If the delta contains any MODIFIED or REMOVED requirements: stop before moving anything and report an error — "cannot modify/remove requirement '<header>' in capability <capability> — no current-truth spec exists yet for this capability."
|
|
69
|
+
- **If it does exist:**
|
|
70
|
+
- **ADDED**: append the requirement block to the end of the `## Requirements` section.
|
|
71
|
+
- **MODIFIED**: find the existing `### Requirement:` block whose header text matches the delta's (whitespace-insensitive); replace that entire block (header, body, and scenarios) with the delta's version. If no match is found, treat it as ADDED instead (append) and note this in the final report.
|
|
72
|
+
- **REMOVED**: find and delete the matching block entirely. If no match is found, note this in the final report and continue — nothing to remove.
|
|
73
|
+
3. Write the updated `oprim/specs/<capability>/spec.md`.
|
|
74
|
+
|
|
75
|
+
This fold always overwrites the matched requirement wholesale — it never reconciles two bets' overlapping changes. If a later bet's archive touches the same requirement again, its version simply replaces this one (last-write-wins, confirmed by construction — no 3-way merge).
|
|
76
|
+
|
|
77
|
+
Track which capabilities were merged (and any no-match notes) for the final report.
|
|
78
|
+
|
|
79
|
+
### 5. Move the bet directory to archive
|
|
80
|
+
|
|
81
|
+
Create the archive subfolder if it doesn't exist:
|
|
82
|
+
```bash
|
|
83
|
+
mkdir -p oprim/bets/archived
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Move the resolved directory:
|
|
87
|
+
```bash
|
|
88
|
+
mv oprim/bets/<resolved-dir> oprim/bets/archived/<resolved-dir>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 6. Remove the bet entry from sequence.yaml
|
|
92
|
+
|
|
93
|
+
Read `oprim/sequence.yaml`, parse it, and remove the entry with `id: BET-NNN` from whichever bucket it appears in (now, next, later, or backlog). Write the updated YAML back using 2-space indentation. Do not modify any other entries.
|
|
94
|
+
|
|
95
|
+
### 7. Report what was done
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
## Bet Archived
|
|
99
|
+
|
|
100
|
+
**Bet:** BET-NNN
|
|
101
|
+
**Archived to:** oprim/bets/archived/<resolved-dir>/
|
|
102
|
+
**Removed from sequence.yaml:** ✓
|
|
103
|
+
**Spec deltas merged:** <capability-1>, <capability-2> (omit this line if no specs/ directory was present)
|
|
104
|
+
|
|
105
|
+
The bet is preserved in full at the archive location.
|
|
106
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Create a new bet in `oprim/bets/`. First explain: "A bet is a product decision you're committing to explore — a problem worth solving, a hypothesis worth testing, or a direction worth taking. You'll name it, explain why now, and set a kill criterion." Then show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan `BET-(\d+)` dirs for next ID (zero-padded, default 001). Check `oprim/sequence.yaml` exists (stop if not — advise oprim init). Read `oprim/config.yaml`'s `rules.bet` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters, warn "this title may be too vague", suggest a reformulation, and ask "Proceed anyway? (y/N)" — if "n", prompt for a revised title. Gather: decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write `oprim/bets/BET-NNN/bet-decision.md` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/BET-NNN/discovery.md` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
### Bet authoring (oprim-bet)
|
|
2
|
+
Create a new bet in `oprim/bets/` and register it on the sequencing board.
|
|
3
|
+
|
|
4
|
+
1. Show naming tip: "verb + object [for context] — e.g. 'Improve bet naming for scannability'"
|
|
5
|
+
2. Ask for the bet title. Validate: fewer than 4 words OR fewer than 25 chars → warn, suggest reformulation, ask "Proceed anyway? (y/N)".
|
|
6
|
+
3. Assign next BET ID: scan `oprim/bets/BET-(\d+)` dirs, max+1 zero-padded to 3 digits (default 001).
|
|
7
|
+
4. Check `oprim/sequence.yaml` exists — stop if not, advise `oprim init`.
|
|
8
|
+
4b. Read `oprim/config.yaml`'s `rules.bet` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged.
|
|
9
|
+
5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
|
|
10
|
+
6. Write `oprim/bets/BET-NNN/bet-decision.md` with all fields.
|
|
11
|
+
7. Append to `oprim/sequence.yaml` backlog: `{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}`.
|
|
12
|
+
8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/BET-NNN/discovery.md`.
|
|
13
|
+
9. Report what was created.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
id: bet
|
|
2
|
+
skillName: oprim-bet
|
|
3
|
+
title: null
|
|
4
|
+
description: Create a new bet directory and bet-decision artifact in oprim/bets/, and add the bet to oprim/sequence.yaml backlog
|
|
5
|
+
cursorDescription: Create a new bet decision and register it on the sequencing board
|
|
6
|
+
claude:
|
|
7
|
+
skill: true
|
|
8
|
+
command: null
|
|
9
|
+
cursor:
|
|
10
|
+
skill: true
|
|
11
|
+
command: oprim-bet.md
|
|
12
|
+
poolside:
|
|
13
|
+
skill: true
|
|
14
|
+
inline: true
|