@open-product-primer/cli 1.2.0 → 2.1.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 CHANGED
@@ -27,5 +27,11 @@ Bin aliases: `open-product-primer`, `oprim`.
27
27
  | `oprim update` | Refresh `/oprim:*` assistant commands and skills |
28
28
  | `oprim doctor` | Verify scaffold, integrations, and measurement env |
29
29
  | `oprim measure` | Run KPI measurement pipeline for a bet |
30
+ | `oprim context` | Print resolved remote context content (`--source <name>` to scope to one) |
31
+ | `oprim context init` | Declare the current project a citable remote context (`--description`); prefer the guided `/oprim:context-init` skill in Claude Code |
32
+ | `oprim context register` | Register a remote context source (`--git <url>` or `--local <path>`, `--name`, optional `--description`) |
33
+ | `oprim context list` | List every registered source's name, kind, and description without fully resolving any of them |
34
+
35
+ A remote context is an oprim workspace bundled somewhere outside the current project's directory — a remote git repo or a local sibling directory — that this project can reference read-only. Content resolves fresh on demand (never a persistent clone you have to `git pull` yourself); `oprim context list` is the cheap way to see what's registered before pulling full content.
30
36
 
31
37
  Full documentation: [github.com/eshraw/open-product-primer](https://github.com/eshraw/open-product-primer).
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ const doctor_1 = require("./commands/doctor");
11
11
  const migrate_1 = require("./commands/migrate");
12
12
  const measure_1 = require("./commands/measure");
13
13
  const ovw_1 = require("./commands/ovw");
14
+ const context_1 = require("./commands/context");
14
15
  const package_json_1 = __importDefault(require("../package.json"));
15
16
  const program = new commander_1.Command();
16
17
  program
@@ -23,4 +24,5 @@ program.addCommand((0, doctor_1.doctorCommand)());
23
24
  program.addCommand((0, migrate_1.migrateCommand)());
24
25
  program.addCommand((0, measure_1.measureCommand)());
25
26
  program.addCommand((0, ovw_1.ovwCommand)());
27
+ program.addCommand((0, context_1.contextCommand)());
26
28
  program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function contextCommand(): Command;
@@ -0,0 +1,188 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.contextCommand = contextCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const remote_context_1 = require("../lib/remote-context");
44
+ const scaffold_1 = require("../lib/scaffold");
45
+ function sourceKind(source) {
46
+ return (0, remote_context_1.isGitSource)(source) ? 'git' : 'path';
47
+ }
48
+ function initSubcommand() {
49
+ return new commander_1.Command('init')
50
+ .description('Declare the current project as a citable remote context')
51
+ .option('--description <text>', 'canonical description of what this context contains')
52
+ .action((opts) => {
53
+ const projectRoot = process.cwd();
54
+ if ((0, scaffold_1.fileExists)((0, remote_context_1.identityFilePath)(projectRoot))) {
55
+ console.log(chalk_1.default.yellow('A remote context identity already exists at ') + chalk_1.default.cyan('.oprim-context/context.yaml'));
56
+ return;
57
+ }
58
+ const name = path.basename(projectRoot);
59
+ (0, remote_context_1.writeIdentity)(projectRoot, { name, version: '1', description: opts.description });
60
+ console.log(chalk_1.default.green('✓') + ' .oprim-context/context.yaml created');
61
+ console.log(` name: ${name}`);
62
+ if (opts.description) {
63
+ console.log(` description: ${opts.description}`);
64
+ }
65
+ else {
66
+ console.log(chalk_1.default.yellow(' no description set') +
67
+ ' — other projects referencing this context via ' +
68
+ chalk_1.default.cyan('oprim context list') +
69
+ ' will see it as description-less.');
70
+ }
71
+ });
72
+ }
73
+ function registerSubcommand() {
74
+ return new commander_1.Command('register')
75
+ .description('Register a remote context source in the current project')
76
+ .option('--git <url>', 'git remote URL of the remote context')
77
+ .option('--local <path>', 'local filesystem path of the remote context')
78
+ .requiredOption('--name <name>', 'name to register this source under')
79
+ .option('--description <text>', 'local note about why this source was registered')
80
+ .action((opts) => {
81
+ const projectRoot = process.cwd();
82
+ if ((opts.git && opts.local) || (!opts.git && !opts.local)) {
83
+ console.error(chalk_1.default.red('Exactly one of --git or --local is required.'));
84
+ process.exit(1);
85
+ }
86
+ const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
87
+ if ((0, remote_context_1.findSourceByName)(config, opts.name)) {
88
+ console.error(chalk_1.default.red(`A source named "${opts.name}" is already registered.`));
89
+ process.exit(1);
90
+ }
91
+ const source = opts.git
92
+ ? { name: opts.name, git: opts.git, description: opts.description }
93
+ : { name: opts.name, path: opts.local, description: opts.description };
94
+ config.enabled = true;
95
+ config.sources.push(source);
96
+ (0, remote_context_1.writeRemoteContextConfig)(projectRoot, config);
97
+ console.log(chalk_1.default.green('✓') + ` Registered "${opts.name}" (${sourceKind(source)})`);
98
+ const result = (0, remote_context_1.resolveIdentityOnly)(source);
99
+ if (result.error) {
100
+ console.log(chalk_1.default.yellow(' Could not confirm this source yet: ') +
101
+ result.error +
102
+ chalk_1.default.dim(' — try `oprim context list` or `oprim doctor` to retry later.'));
103
+ return;
104
+ }
105
+ if (result.nameMismatch) {
106
+ console.log(chalk_1.default.yellow(` Warning: declared name "${result.nameMismatch.declared}" does not match resolved identity name "${result.nameMismatch.resolved}".`));
107
+ }
108
+ if (result.identity?.description) {
109
+ console.log(` ${chalk_1.default.dim('description:')} ${result.identity.description}`);
110
+ }
111
+ else {
112
+ console.log(chalk_1.default.dim(' (no canonical description set on this source)'));
113
+ }
114
+ });
115
+ }
116
+ function listSubcommand() {
117
+ return new commander_1.Command('list')
118
+ .description('List every registered remote context source, without fully resolving any of them')
119
+ .action(() => {
120
+ const projectRoot = process.cwd();
121
+ const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
122
+ if (!config.enabled || config.sources.length === 0) {
123
+ console.log('No remote contexts configured.');
124
+ return;
125
+ }
126
+ for (const source of config.sources) {
127
+ const kind = sourceKind(source);
128
+ console.log(`${chalk_1.default.bold(source.name)} ${chalk_1.default.dim(`(${kind})`)}`);
129
+ const result = (0, remote_context_1.resolveIdentityOnly)(source);
130
+ if (result.error) {
131
+ console.log(' ' + chalk_1.default.red(`unresolved: ${result.error}`));
132
+ }
133
+ else {
134
+ const desc = result.identity?.description;
135
+ console.log(' ' + (desc ? desc : chalk_1.default.dim('no description set')));
136
+ if (result.nameMismatch) {
137
+ console.log(chalk_1.default.yellow(` name mismatch: declared "${result.nameMismatch.declared}", resolved "${result.nameMismatch.resolved}"`));
138
+ }
139
+ }
140
+ if (source.description) {
141
+ console.log(' ' + chalk_1.default.dim(`local note: ${source.description}`));
142
+ }
143
+ console.log('');
144
+ }
145
+ });
146
+ }
147
+ function contextCommand() {
148
+ const cmd = new commander_1.Command('context')
149
+ .description('Print resolved remote context content')
150
+ .option('--source <name>', 'limit output to a single named source')
151
+ .action((opts) => {
152
+ const projectRoot = process.cwd();
153
+ const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
154
+ if (!config.enabled || config.sources.length === 0) {
155
+ console.log('No remote contexts configured.');
156
+ return;
157
+ }
158
+ let sources = config.sources;
159
+ if (opts.source) {
160
+ const match = (0, remote_context_1.findSourceByName)(config, opts.source);
161
+ if (!match) {
162
+ console.error(chalk_1.default.red(`No source named "${opts.source}" is registered.`));
163
+ process.exit(1);
164
+ }
165
+ sources = [match];
166
+ }
167
+ for (const source of sources) {
168
+ console.log(chalk_1.default.bold(`═══ ${source.name} ═══`));
169
+ const result = (0, remote_context_1.resolveFull)(source);
170
+ if (result.error) {
171
+ console.log(chalk_1.default.red(` unresolved: ${result.error}`));
172
+ continue;
173
+ }
174
+ if (result.stale) {
175
+ console.log(chalk_1.default.yellow(' (stale — last successful fetch could not be refreshed)'));
176
+ }
177
+ if (result.nameMismatch) {
178
+ console.log(chalk_1.default.yellow(` name mismatch: declared "${result.nameMismatch.declared}", resolved "${result.nameMismatch.resolved}"`));
179
+ }
180
+ console.log((0, remote_context_1.assembleOprimWorkspaceContent)(result.workspaceRoot));
181
+ console.log('');
182
+ }
183
+ });
184
+ cmd.addCommand(initSubcommand());
185
+ cmd.addCommand(registerSubcommand());
186
+ cmd.addCommand(listSubcommand());
187
+ return cmd;
188
+ }
@@ -44,6 +44,7 @@ const chalk_1 = __importDefault(require("chalk"));
44
44
  const detect_1 = require("../lib/detect");
45
45
  const measure_1 = require("../lib/measure");
46
46
  const integrity_1 = require("../lib/integrity");
47
+ const remote_context_1 = require("../lib/remote-context");
47
48
  const AGENT_DIRS = {
48
49
  claude: '.claude',
49
50
  cursor: '.cursor',
@@ -103,6 +104,52 @@ function checkClaudeHooks(projectRoot, checks) {
103
104
  required: false,
104
105
  });
105
106
  }
107
+ // bet-026 — validates each configured remote_context.sources entry using the identity-only
108
+ // fetch (cheap: single file, not a full workspace pull), plus a separate "ever fully
109
+ // resolved" nudge that only applies to git sources (local paths have no persistent cache).
110
+ function checkRemoteContexts(projectRoot, checks) {
111
+ const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
112
+ if (!config.enabled || config.sources.length === 0)
113
+ return;
114
+ for (const source of config.sources) {
115
+ const kind = (0, remote_context_1.isGitSource)(source) ? 'git' : 'path';
116
+ const result = (0, remote_context_1.resolveIdentityOnly)(source);
117
+ if (result.error) {
118
+ checks.push({
119
+ name: `remote_context: ${source.name} (${kind})`,
120
+ pass: false,
121
+ note: `${kind === 'git' ? 'Remote unreachable' : 'Path not found'}: ${result.error} — run 'oprim context list' to retry`,
122
+ required: false,
123
+ });
124
+ continue;
125
+ }
126
+ if (!result.identity) {
127
+ checks.push({
128
+ name: `remote_context: ${source.name} (${kind})`,
129
+ pass: false,
130
+ note: `Missing remote context identity — ask the source project to run 'oprim context init'`,
131
+ required: false,
132
+ });
133
+ continue;
134
+ }
135
+ checks.push({
136
+ name: `remote_context: ${source.name} (${kind})`,
137
+ pass: true,
138
+ note: result.nameMismatch
139
+ ? `Name mismatch: declared "${result.nameMismatch.declared}", resolved "${result.nameMismatch.resolved}"`
140
+ : undefined,
141
+ required: false,
142
+ });
143
+ if ((0, remote_context_1.isGitSource)(source) && !(0, remote_context_1.hasEverFullyResolved)(source)) {
144
+ checks.push({
145
+ name: `remote_context: ${source.name} never fully resolved`,
146
+ pass: false,
147
+ note: `Run 'oprim context --source ${source.name}' to pull its content for the first time`,
148
+ required: false,
149
+ });
150
+ }
151
+ }
152
+ }
106
153
  function doctorCommand() {
107
154
  return new commander_1.Command('doctor')
108
155
  .description('Check oprim install health and integration readiness')
@@ -209,6 +256,8 @@ function doctorCommand() {
209
256
  (0, integrity_1.checkSequenceIntegrity)(projectRoot, checks);
210
257
  // ── Skill version drift checks ────────────────────────────────────────────
211
258
  (0, integrity_1.checkSkillVersionDrift)(projectRoot, checks);
259
+ // ── Remote context checks ─────────────────────────────────────────────────
260
+ checkRemoteContexts(projectRoot, checks);
212
261
  // ── Agent environment checks ──────────────────────────────────────────────
213
262
  const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
214
263
  if (configAgents !== null) {
@@ -61,6 +61,7 @@ function initCommand() {
61
61
  console.log(chalk_1.default.green('✓') + ' Graphify detected');
62
62
  console.log('');
63
63
  const okfEnabled = await (0, install_agent_1.promptOkfFrontmatter)();
64
+ const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
64
65
  const primerDir = path.join(projectRoot, 'oprim');
65
66
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
66
67
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
@@ -68,7 +69,7 @@ function initCommand() {
68
69
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
69
70
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
70
71
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
71
- const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
72
+ const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled, specFramework));
72
73
  const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
73
74
  const pdrContent = okfEnabled ? (0, templates_1.okfFrontmatter)('pdr', '<Decision title>') + templates_1.pdrTemplate : templates_1.pdrTemplate;
74
75
  const betContent = okfEnabled
@@ -136,10 +137,8 @@ function initCommand() {
136
137
  ' after configuring an AI tool to install /oprim:* skills.');
137
138
  }
138
139
  else {
139
- let specFramework = 'openspec';
140
140
  let pdrSurfacing = false;
141
141
  if (selectedAgents.includes('claude')) {
142
- specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
143
142
  pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
144
143
  }
145
144
  console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
@@ -45,6 +45,19 @@ const install_agent_1 = require("../lib/install-agent");
45
45
  const detect_1 = require("../lib/detect");
46
46
  const scaffold_1 = require("../lib/scaffold");
47
47
  const templates_1 = require("../lib/templates");
48
+ const config_merge_1 = require("../lib/config-merge");
49
+ // bet-023 — persist the resolved spec_framework into oprim/config.yaml, inserting it only
50
+ // when the key is missing (never overwrites an already-persisted choice).
51
+ function persistSpecFramework(configPath, framework) {
52
+ if (!fs.existsSync(configPath))
53
+ return;
54
+ const existing = fs.readFileSync(configPath, 'utf-8');
55
+ const { content, changed } = (0, config_merge_1.mergeSpecFramework)(existing, framework);
56
+ if (changed) {
57
+ fs.writeFileSync(configPath, content, 'utf-8');
58
+ console.log(chalk_1.default.green('✓') + ` oprim/config.yaml — integrations.spec_framework set to ${framework}`);
59
+ }
60
+ }
48
61
  function updateCommand() {
49
62
  return new commander_1.Command('update')
50
63
  .description('Refresh /oprim:* assistant commands and skills from package templates')
@@ -58,13 +71,23 @@ function updateCommand() {
58
71
  const primerDir = path.join(projectRoot, 'oprim');
59
72
  (0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
60
73
  (0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
74
+ const configPath = path.join(primerDir, 'config.yaml');
75
+ if (fs.existsSync(configPath)) {
76
+ const existingConfig = fs.readFileSync(configPath, 'utf-8');
77
+ const { content: mergedConfig, changed } = (0, config_merge_1.mergeConfigSchema)(existingConfig);
78
+ if (changed) {
79
+ fs.writeFileSync(configPath, mergedConfig, 'utf-8');
80
+ console.log(chalk_1.default.green('✓') + ' oprim/config.yaml — schema updated with new keys (existing values preserved)');
81
+ }
82
+ }
61
83
  if (configAgents !== null && configAgents.length > 0) {
62
- let specFramework = 'openspec';
84
+ let specFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
63
85
  let pdrSurfacing = false;
64
86
  if (configAgents.includes('claude')) {
65
87
  specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
66
88
  pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
67
89
  }
90
+ persistSpecFramework(configPath, specFramework);
68
91
  for (const agent of configAgents) {
69
92
  (0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
70
93
  }
@@ -75,6 +98,7 @@ function updateCommand() {
75
98
  const legacyAgents = [];
76
99
  if (fs.existsSync(path.join(projectRoot, '.claude'))) {
77
100
  const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
101
+ persistSpecFramework(configPath, specFramework);
78
102
  const pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
79
103
  (0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework, pdrSurfacing);
80
104
  legacyAgents.push('claude');
@@ -99,22 +123,25 @@ function updateCommand() {
99
123
  default: false,
100
124
  });
101
125
  if (!addMore) {
126
+ persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
102
127
  console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
103
128
  return;
104
129
  }
105
130
  console.log('');
106
131
  const selected = await (0, install_agent_1.promptAgentSelection)(projectRoot);
107
132
  if (selected.length === 0) {
133
+ persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
108
134
  console.log('\n' + chalk_1.default.yellow('No agents selected.'));
109
135
  console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
110
136
  return;
111
137
  }
112
- let addSpecFramework = 'openspec';
138
+ let addSpecFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
113
139
  let addPdrSurfacing = false;
114
140
  if (selected.includes('claude')) {
115
141
  addSpecFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
116
142
  addPdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
117
143
  }
144
+ persistSpecFramework(configPath, addSpecFramework);
118
145
  console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
119
146
  for (const agent of selected) {
120
147
  (0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework, addPdrSurfacing);
@@ -0,0 +1,10 @@
1
+ export declare function mergeConfigSchema(existingContent: string): {
2
+ content: string;
3
+ changed: boolean;
4
+ };
5
+ export declare function readSpecFramework(content: string): string | null;
6
+ export declare function deriveDefaultSpecFramework(content: string): string;
7
+ export declare function mergeSpecFramework(existingContent: string, framework: string): {
8
+ content: string;
9
+ changed: boolean;
10
+ };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeConfigSchema = mergeConfigSchema;
4
+ exports.readSpecFramework = readSpecFramework;
5
+ exports.deriveDefaultSpecFramework = deriveDefaultSpecFramework;
6
+ exports.mergeSpecFramework = mergeSpecFramework;
7
+ const CONFIG_SCHEMA_FIELDS = [
8
+ { key: 'context', block: 'context: ""\n' },
9
+ { key: 'rules', block: 'rules: {}\n' },
10
+ // bet-026 — supersedes the old inert `store:` key (BET-025). `store` is intentionally left
11
+ // out of this table going forward: a project that already has it keeps it untouched (this
12
+ // merge only adds missing keys, never removes existing ones), but new/updated configs only
13
+ // ever gain `remote_context`.
14
+ { key: 'remote_context', block: 'remote_context:\n enabled: false\n sources: []\n' },
15
+ ];
16
+ function existingTopLevelKeys(content) {
17
+ const keys = new Set();
18
+ for (const line of content.split('\n')) {
19
+ const match = line.match(/^([A-Za-z_][\w-]*):/);
20
+ if (match)
21
+ keys.add(match[1]);
22
+ }
23
+ return keys;
24
+ }
25
+ function mergeConfigSchema(existingContent) {
26
+ const present = existingTopLevelKeys(existingContent);
27
+ const missing = CONFIG_SCHEMA_FIELDS.filter((field) => !present.has(field.key));
28
+ if (missing.length === 0)
29
+ return { content: existingContent, changed: false };
30
+ const separator = existingContent.endsWith('\n') ? '' : '\n';
31
+ const additions = missing.map((field) => field.block).join('');
32
+ return { content: existingContent + separator + additions, changed: true };
33
+ }
34
+ // bet-023 — integrations.spec_framework is nested under `integrations:` rather than a
35
+ // top-level key, and its default depends on the project's existing openspec state, so it
36
+ // can't reuse the static CONFIG_SCHEMA_FIELDS table above. Handled as its own one-off per
37
+ // the bet's design (falls back to a minimal merge rather than depending on the generic
38
+ // top-level mechanism, which doesn't support nested keys or computed defaults).
39
+ function readSpecFramework(content) {
40
+ const match = content.match(/^\s*spec_framework:\s*(\S+)/m);
41
+ return match ? match[1] : null;
42
+ }
43
+ function deriveDefaultSpecFramework(content) {
44
+ return /^\s*openspec:\n\s*enabled:\s*true/m.test(content) ? 'openspec' : 'none';
45
+ }
46
+ function mergeSpecFramework(existingContent, framework) {
47
+ if (readSpecFramework(existingContent))
48
+ return { content: existingContent, changed: false };
49
+ const lines = existingContent.split('\n');
50
+ const integrationsIndex = lines.findIndex((line) => line === 'integrations:');
51
+ if (integrationsIndex === -1) {
52
+ const separator = existingContent.endsWith('\n') ? '' : '\n';
53
+ return { content: existingContent + separator + `integrations:\n spec_framework: ${framework}\n`, changed: true };
54
+ }
55
+ lines.splice(integrationsIndex + 1, 0, ` spec_framework: ${framework}`);
56
+ return { content: lines.join('\n'), changed: true };
57
+ }
@@ -1,6 +1,7 @@
1
1
  export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside';
2
2
  export declare const SUPPORTED_AGENTS: readonly Agent[];
3
3
  export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
4
+ export declare function resolveSpecFramework(projectRoot: string): string;
4
5
  export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
5
6
  export declare function promptPdrSurfacing(): Promise<boolean>;
6
7
  export declare function promptOkfFrontmatter(): Promise<boolean>;
@@ -11,6 +12,7 @@ export declare const CLAUDE_COMMANDS: Record<string, string>;
11
12
  export declare const POOLSIDE_SKILLS: Record<string, string>;
12
13
  export declare const CURSOR_SKILLS: Record<string, string>;
13
14
  export declare const CURSOR_COMMANDS: Record<string, string>;
15
+ export declare function specAuthoringSkill(): string;
14
16
  export declare function writeAgentInstructionFile(filePath: string, section: string): void;
15
17
  export declare function codexInstructions(): string;
16
18
  export declare function geminiInstructions(): string;