@open-product-primer/cli 2.6.0 → 2.8.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 +1 -1
- package/dist/commands/ovw.js +4 -2
- package/dist/lib/detect.js +4 -0
- package/dist/lib/hyperlink.d.ts +7 -0
- package/dist/lib/hyperlink.js +57 -0
- package/dist/lib/install-agent.d.ts +5 -1
- package/dist/lib/install-agent.js +105 -4
- package/dist/lib/workflow-renderer.js +1 -1
- package/dist/lib/workflow-schema.d.ts +6 -0
- package/dist/lib/workflow-schema.js +7 -2
- package/dist/workflows/bet.cursor-command.md +1 -1
- package/dist/workflows/bet.inline.md +1 -1
- package/dist/workflows/bet.template.md +2 -2
- package/dist/workflows/explore.inline.md +9 -0
- package/dist/workflows/explore.schema.yaml +13 -0
- package/dist/workflows/explore.template.md +54 -0
- package/dist/workflows/promote.native.template.md +1 -1
- package/dist/workflows/promote.none.template.md +1 -1
- package/dist/workflows/promote.openspec.template.md +1 -1
- package/dist/workflows/reconcile.inline.md +8 -0
- package/dist/workflows/reconcile.schema.yaml +13 -0
- package/dist/workflows/reconcile.template.md +51 -0
- package/dist/workflows/sequence.template.md +1 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -48,7 +48,7 @@ function initCommand() {
|
|
|
48
48
|
return new commander_1.Command('init')
|
|
49
49
|
.description('Initialize oprim in the current repository')
|
|
50
50
|
.option('--name <name>', 'project name (defaults to directory name)')
|
|
51
|
-
.option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe)', (val, prev) => [...prev, val], [])
|
|
51
|
+
.option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe, qwen, kimi)', (val, prev) => [...prev, val], [])
|
|
52
52
|
.action(async (opts) => {
|
|
53
53
|
const projectRoot = process.cwd();
|
|
54
54
|
const projectName = opts.name ?? path.basename(projectRoot);
|
package/dist/commands/ovw.js
CHANGED
|
@@ -42,6 +42,7 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const chalk_1 = __importDefault(require("chalk"));
|
|
44
44
|
const yaml = __importStar(require("js-yaml"));
|
|
45
|
+
const hyperlink_1 = require("../lib/hyperlink");
|
|
45
46
|
function resolveBetDecisionPath(betsDir, betId) {
|
|
46
47
|
if (!fs.existsSync(betsDir))
|
|
47
48
|
return null;
|
|
@@ -128,15 +129,16 @@ function renderLane(label, bets, betsDir, showMeta) {
|
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
130
131
|
for (const bet of bets) {
|
|
132
|
+
const entry = (0, hyperlink_1.hyperlinkBetEntry)(betsDir, bet.id, `${bet.id} ${bet.title}`);
|
|
131
133
|
if (showMeta) {
|
|
132
134
|
const meta = loadBetMeta(betsDir, bet.id);
|
|
133
135
|
const inline = renderInlineMeta(meta);
|
|
134
136
|
const blockers = renderBlockers(bet);
|
|
135
|
-
console.log(` ${
|
|
137
|
+
console.log(` ${entry} ${inline}${blockers}`);
|
|
136
138
|
}
|
|
137
139
|
else {
|
|
138
140
|
const blockers = renderBlockers(bet);
|
|
139
|
-
console.log(` ${
|
|
141
|
+
console.log(` ${entry}${blockers}`);
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
}
|
package/dist/lib/detect.js
CHANGED
|
@@ -86,6 +86,10 @@ function detectAvailableAgents(projectRoot) {
|
|
|
86
86
|
detected.push('poolside');
|
|
87
87
|
if (fs.existsSync(path.join(projectRoot, '.vibe')))
|
|
88
88
|
detected.push('vibe');
|
|
89
|
+
if (fs.existsSync(path.join(projectRoot, '.qwen')))
|
|
90
|
+
detected.push('qwen');
|
|
91
|
+
if (fs.existsSync(path.join(projectRoot, '.kimi')))
|
|
92
|
+
detected.push('kimi');
|
|
89
93
|
return detected;
|
|
90
94
|
}
|
|
91
95
|
function writeAgentsToConfig(agents, projectRoot) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Wraps `text` in an OSC 8 terminal hyperlink escape sequence targeting `uri`. */
|
|
2
|
+
export declare function osc8(text: string, uri: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolves `betIdInput` to its `bet-decision.md` path under `betsDir` and returns an OSC 8
|
|
5
|
+
* hyperlink wrapping `text`. Falls back to plain `text` if the bet can't be resolved.
|
|
6
|
+
*/
|
|
7
|
+
export declare function hyperlinkBetEntry(betsDir: string, betIdInput: string, text: string): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
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.osc8 = osc8;
|
|
37
|
+
exports.hyperlinkBetEntry = hyperlinkBetEntry;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const spec_delta_1 = require("./spec-delta");
|
|
40
|
+
/** Wraps `text` in an OSC 8 terminal hyperlink escape sequence targeting `uri`. */
|
|
41
|
+
function osc8(text, uri) {
|
|
42
|
+
return `\x1b]8;;${uri}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
43
|
+
}
|
|
44
|
+
function fileUri(absPath) {
|
|
45
|
+
return 'file://' + encodeURI(absPath.split(path.sep).join('/'));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolves `betIdInput` to its `bet-decision.md` path under `betsDir` and returns an OSC 8
|
|
49
|
+
* hyperlink wrapping `text`. Falls back to plain `text` if the bet can't be resolved.
|
|
50
|
+
*/
|
|
51
|
+
function hyperlinkBetEntry(betsDir, betIdInput, text) {
|
|
52
|
+
const dirName = (0, spec_delta_1.resolveBetDirectory)(betsDir, betIdInput);
|
|
53
|
+
if (!dirName)
|
|
54
|
+
return text;
|
|
55
|
+
const decisionPath = path.join(betsDir, dirName, 'bet-decision.md');
|
|
56
|
+
return osc8(text, fileUri(decisionPath));
|
|
57
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe';
|
|
1
|
+
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe' | 'qwen' | 'kimi';
|
|
2
2
|
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
4
|
export declare function resolveSpecFramework(projectRoot: string): string;
|
|
@@ -11,6 +11,8 @@ export declare const CLAUDE_SKILLS: Record<string, string>;
|
|
|
11
11
|
export declare const CLAUDE_COMMANDS: Record<string, string>;
|
|
12
12
|
export declare const POOLSIDE_SKILLS: Record<string, string>;
|
|
13
13
|
export declare const VIBE_SKILLS: Record<string, string>;
|
|
14
|
+
export declare const QWEN_SKILLS: Record<string, string>;
|
|
15
|
+
export declare const KIMI_SKILLS: Record<string, string>;
|
|
14
16
|
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
15
17
|
export declare const CURSOR_COMMANDS: Record<string, string>;
|
|
16
18
|
export declare function specAuthoringSkill(): string;
|
|
@@ -19,3 +21,5 @@ export declare function codexInstructions(): string;
|
|
|
19
21
|
export declare function geminiInstructions(): string;
|
|
20
22
|
export declare function poolsideInstructions(): string;
|
|
21
23
|
export declare function vibeInstructions(): string;
|
|
24
|
+
export declare function qwenInstructions(): string;
|
|
25
|
+
export declare function kimiInstructions(): string;
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.VIBE_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
|
|
39
|
+
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.KIMI_SKILLS = exports.QWEN_SKILLS = exports.VIBE_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
41
|
exports.resolveSpecFramework = resolveSpecFramework;
|
|
42
42
|
exports.promptAgentSelection = promptAgentSelection;
|
|
@@ -49,6 +49,8 @@ exports.codexInstructions = codexInstructions;
|
|
|
49
49
|
exports.geminiInstructions = geminiInstructions;
|
|
50
50
|
exports.poolsideInstructions = poolsideInstructions;
|
|
51
51
|
exports.vibeInstructions = vibeInstructions;
|
|
52
|
+
exports.qwenInstructions = qwenInstructions;
|
|
53
|
+
exports.kimiInstructions = kimiInstructions;
|
|
52
54
|
const path = __importStar(require("path"));
|
|
53
55
|
const fs = __importStar(require("fs"));
|
|
54
56
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -57,7 +59,16 @@ const detect_1 = require("./detect");
|
|
|
57
59
|
const config_merge_1 = require("./config-merge");
|
|
58
60
|
const workflow_schema_1 = require("./workflow-schema");
|
|
59
61
|
const workflow_renderer_1 = require("./workflow-renderer");
|
|
60
|
-
exports.SUPPORTED_AGENTS = [
|
|
62
|
+
exports.SUPPORTED_AGENTS = [
|
|
63
|
+
'claude',
|
|
64
|
+
'cursor',
|
|
65
|
+
'codex',
|
|
66
|
+
'gemini',
|
|
67
|
+
'poolside',
|
|
68
|
+
'vibe',
|
|
69
|
+
'qwen',
|
|
70
|
+
'kimi',
|
|
71
|
+
];
|
|
61
72
|
// oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
|
|
62
73
|
// selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
|
|
63
74
|
// projects that installed before that key existed.
|
|
@@ -126,6 +137,8 @@ async function promptAgentSelection(projectRoot) {
|
|
|
126
137
|
{ name: 'Gemini CLI', value: 'gemini', checked: detected.includes('gemini') },
|
|
127
138
|
{ name: 'Poolside', value: 'poolside', checked: detected.includes('poolside') },
|
|
128
139
|
{ name: 'Mistral Vibe', value: 'vibe', checked: detected.includes('vibe') },
|
|
140
|
+
{ name: 'Qwen Code', value: 'qwen', checked: detected.includes('qwen') },
|
|
141
|
+
{ name: 'Kimi CLI', value: 'kimi', checked: detected.includes('kimi') },
|
|
129
142
|
],
|
|
130
143
|
});
|
|
131
144
|
}
|
|
@@ -142,8 +155,8 @@ async function promptOkfFrontmatter() {
|
|
|
142
155
|
}
|
|
143
156
|
// Workflow ids installed as Claude/Poolside/Cursor skill files — see packages/cli/src/workflows/.
|
|
144
157
|
// Order matches the pre-refactor CLAUDE_SKILLS/POOLSIDE_SKILLS/CURSOR_SKILLS declaration order.
|
|
145
|
-
const CLAUDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'context'];
|
|
146
|
-
const POOLSIDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence'];
|
|
158
|
+
const CLAUDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'context', 'explore', 'reconcile'];
|
|
159
|
+
const POOLSIDE_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review', 'archive', 'sequence', 'explore', 'reconcile'];
|
|
147
160
|
const CURSOR_SKILL_WORKFLOW_IDS = ['pdr', 'bet', 'note', 'criteria', 'review'];
|
|
148
161
|
// Claude command wrappers (thin, invoke skill) — filename -> workflow id. Order matches the
|
|
149
162
|
// pre-refactor CLAUDE_COMMANDS declaration order.
|
|
@@ -152,6 +165,8 @@ const CLAUDE_COMMAND_WORKFLOWS = [
|
|
|
152
165
|
{ filename: 'sequence.md', id: 'sequence' },
|
|
153
166
|
{ filename: 'archive.md', id: 'archive' },
|
|
154
167
|
{ filename: 'context-init.md', id: 'context' },
|
|
168
|
+
{ filename: 'explore.md', id: 'explore' },
|
|
169
|
+
{ filename: 'reconcile.md', id: 'reconcile' },
|
|
155
170
|
];
|
|
156
171
|
// Cursor command files (full inline). Order matches the pre-refactor CURSOR_COMMANDS declaration order.
|
|
157
172
|
const CURSOR_COMMAND_WORKFLOW_IDS = ['promote', 'sequence', 'pdr', 'bet', 'note', 'criteria', 'review'];
|
|
@@ -314,6 +329,78 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
|
|
|
314
329
|
console.log(chalk_1.default.dim(' .vibe/ created — Mistral Vibe will discover these files automatically.'));
|
|
315
330
|
}
|
|
316
331
|
}
|
|
332
|
+
else if (agent === 'qwen') {
|
|
333
|
+
const qwenDir = path.join(projectRoot, '.qwen');
|
|
334
|
+
const dirCreated = !fs.existsSync(qwenDir);
|
|
335
|
+
const skillsBase = path.join(qwenDir, 'skills');
|
|
336
|
+
for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
|
|
337
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
|
|
338
|
+
if (!schema.qwen.skill || !schema.skillName)
|
|
339
|
+
continue;
|
|
340
|
+
(0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
|
|
341
|
+
console.log(chalk_1.default.green('✓') + ` .qwen/skills/${schema.skillName}/SKILL.md`);
|
|
342
|
+
}
|
|
343
|
+
const qwenSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
|
|
344
|
+
if (framework === 'native') {
|
|
345
|
+
(0, scaffold_1.writeFile)(qwenSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
|
|
346
|
+
console.log(chalk_1.default.green('✓') + ' .qwen/skills/oprim-spec/SKILL.md');
|
|
347
|
+
}
|
|
348
|
+
else if (fs.existsSync(qwenSpecSkillPath)) {
|
|
349
|
+
fs.unlinkSync(qwenSpecSkillPath);
|
|
350
|
+
try {
|
|
351
|
+
fs.rmdirSync(path.dirname(qwenSpecSkillPath));
|
|
352
|
+
}
|
|
353
|
+
catch { /* not empty or already gone */ }
|
|
354
|
+
console.log(chalk_1.default.dim(' removed .qwen/skills/oprim-spec/SKILL.md'));
|
|
355
|
+
}
|
|
356
|
+
const agentsFile = path.join(projectRoot, 'AGENTS.md');
|
|
357
|
+
writeAgentInstructionFile(agentsFile, qwenInstructions());
|
|
358
|
+
console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
|
|
359
|
+
if (dirCreated) {
|
|
360
|
+
console.log(chalk_1.default.dim(' .qwen/ created — Qwen Code will discover these files automatically.'));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else if (agent === 'kimi') {
|
|
364
|
+
// Kimi CLI is a split-path install: .kimi/ is the detection signal, but skills are
|
|
365
|
+
// discovered from a project-root .skills/ directory, not .kimi/skills/ — see
|
|
366
|
+
// kimi-cli-agent-support spec.
|
|
367
|
+
const kimiDir = path.join(projectRoot, '.kimi');
|
|
368
|
+
const kimiDirCreated = !fs.existsSync(kimiDir);
|
|
369
|
+
if (kimiDirCreated) {
|
|
370
|
+
fs.mkdirSync(kimiDir, { recursive: true });
|
|
371
|
+
}
|
|
372
|
+
const skillsBase = path.join(projectRoot, '.skills');
|
|
373
|
+
const skillsDirCreated = !fs.existsSync(skillsBase);
|
|
374
|
+
for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
|
|
375
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
|
|
376
|
+
if (!schema.kimi.skill || !schema.skillName)
|
|
377
|
+
continue;
|
|
378
|
+
(0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
|
|
379
|
+
console.log(chalk_1.default.green('✓') + ` .skills/${schema.skillName}/SKILL.md`);
|
|
380
|
+
}
|
|
381
|
+
const kimiSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
|
|
382
|
+
if (framework === 'native') {
|
|
383
|
+
(0, scaffold_1.writeFile)(kimiSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
|
|
384
|
+
console.log(chalk_1.default.green('✓') + ' .skills/oprim-spec/SKILL.md');
|
|
385
|
+
}
|
|
386
|
+
else if (fs.existsSync(kimiSpecSkillPath)) {
|
|
387
|
+
fs.unlinkSync(kimiSpecSkillPath);
|
|
388
|
+
try {
|
|
389
|
+
fs.rmdirSync(path.dirname(kimiSpecSkillPath));
|
|
390
|
+
}
|
|
391
|
+
catch { /* not empty or already gone */ }
|
|
392
|
+
console.log(chalk_1.default.dim(' removed .skills/oprim-spec/SKILL.md'));
|
|
393
|
+
}
|
|
394
|
+
const agentsFile = path.join(projectRoot, 'AGENTS.md');
|
|
395
|
+
writeAgentInstructionFile(agentsFile, kimiInstructions());
|
|
396
|
+
console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
|
|
397
|
+
if (kimiDirCreated) {
|
|
398
|
+
console.log(chalk_1.default.dim(' .kimi/ created — Kimi CLI will discover this directory automatically.'));
|
|
399
|
+
}
|
|
400
|
+
if (skillsDirCreated) {
|
|
401
|
+
console.log(chalk_1.default.dim(' .skills/ created — Kimi CLI will discover these files automatically.'));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
317
404
|
else if (agent === 'codex') {
|
|
318
405
|
const agentsFile = path.join(projectRoot, 'AGENTS.md');
|
|
319
406
|
writeAgentInstructionFile(agentsFile, codexInstructions());
|
|
@@ -478,6 +565,14 @@ exports.VIBE_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) =>
|
|
|
478
565
|
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
479
566
|
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
480
567
|
}));
|
|
568
|
+
exports.QWEN_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
|
|
569
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
570
|
+
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
571
|
+
}));
|
|
572
|
+
exports.KIMI_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
|
|
573
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
574
|
+
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
575
|
+
}));
|
|
481
576
|
exports.CURSOR_SKILLS = Object.fromEntries(CURSOR_SKILL_WORKFLOW_IDS.map((id) => {
|
|
482
577
|
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
483
578
|
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
@@ -672,3 +767,9 @@ function poolsideInstructions() {
|
|
|
672
767
|
function vibeInstructions() {
|
|
673
768
|
return (0, workflow_renderer_1.renderAgentInstructions)();
|
|
674
769
|
}
|
|
770
|
+
function qwenInstructions() {
|
|
771
|
+
return (0, workflow_renderer_1.renderAgentInstructions)();
|
|
772
|
+
}
|
|
773
|
+
function kimiInstructions() {
|
|
774
|
+
return (0, workflow_renderer_1.renderAgentInstructions)();
|
|
775
|
+
}
|
|
@@ -8,7 +8,7 @@ exports.renderAgentInstructions = renderAgentInstructions;
|
|
|
8
8
|
const workflow_schema_1 = require("./workflow-schema");
|
|
9
9
|
// Fixed order the pre-refactor oprimWorkflowsInline() concatenated its per-workflow sections in —
|
|
10
10
|
// preserved here so Codex/Gemini/Poolside output is unchanged.
|
|
11
|
-
const INLINE_SECTION_ORDER = ['bet', 'note', 'criteria', 'pdr', 'review', 'archive', 'sequence'];
|
|
11
|
+
const INLINE_SECTION_ORDER = ['bet', 'note', 'criteria', 'pdr', 'review', 'archive', 'sequence', 'explore', 'reconcile'];
|
|
12
12
|
function claudeWrapper(name, description, body) {
|
|
13
13
|
return `---
|
|
14
14
|
name: "${name}"
|
|
@@ -68,9 +68,12 @@ function parseSchema(raw, filePath) {
|
|
|
68
68
|
const claude = requireField(obj, 'claude', filePath);
|
|
69
69
|
const cursor = requireField(obj, 'cursor', filePath);
|
|
70
70
|
const poolside = requireField(obj, 'poolside', filePath);
|
|
71
|
-
// vibe
|
|
72
|
-
// This keeps existing bundled/forked
|
|
71
|
+
// vibe/qwen/kimi are optional and mirror poolside by default — see mistral-vibe-agent-support,
|
|
72
|
+
// qwen-code-agent-support, and kimi-cli-agent-support specs. This keeps existing bundled/forked
|
|
73
|
+
// schemas (none declare a `vibe:`/`qwen:`/`kimi:` key) working unchanged.
|
|
73
74
|
const vibe = obj.vibe ?? poolside;
|
|
75
|
+
const qwen = obj.qwen ?? poolside;
|
|
76
|
+
const kimi = obj.kimi ?? poolside;
|
|
74
77
|
requireField(obj, 'inline', filePath);
|
|
75
78
|
return {
|
|
76
79
|
id: obj.id,
|
|
@@ -82,6 +85,8 @@ function parseSchema(raw, filePath) {
|
|
|
82
85
|
cursor: { skill: Boolean(cursor.skill), command: cursor.command ?? null },
|
|
83
86
|
poolside: { skill: Boolean(poolside.skill) },
|
|
84
87
|
vibe: { skill: Boolean(vibe.skill) },
|
|
88
|
+
qwen: { skill: Boolean(qwen.skill) },
|
|
89
|
+
kimi: { skill: Boolean(kimi.skill) },
|
|
85
90
|
inline: Boolean(obj.inline),
|
|
86
91
|
variants: obj.variants ?? null,
|
|
87
92
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Create a new bet in `oprim/bets/pending/`. 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 `oprim/bets/pending/BET-(\d+)` and `oprim/bets/archived/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/pending/BET-NNN/bet-decision.md` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: `{
|
|
1
|
+
Create a new bet in `oprim/bets/pending/`. 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 `oprim/bets/pending/BET-(\d+)` and `oprim/bets/archived/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/pending/BET-NNN/bet-decision.md` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/pending/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.
|
|
@@ -8,6 +8,6 @@ Create a new bet in `oprim/bets/pending/` and register it on the sequencing boar
|
|
|
8
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
9
|
5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
|
|
10
10
|
6. Write `oprim/bets/pending/BET-NNN/bet-decision.md` with all fields.
|
|
11
|
-
7. Append to `oprim/sequence.yaml` backlog: `{
|
|
11
|
+
7. Append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`.
|
|
12
12
|
8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write `oprim/bets/pending/BET-NNN/discovery.md`.
|
|
13
13
|
9. Report what was created.
|
|
@@ -99,8 +99,8 @@ Prepend the frontmatter block from step 4b, if one was prepared.
|
|
|
99
99
|
### 6. Append to oprim/sequence.yaml backlog
|
|
100
100
|
Read → parse YAML → append → write back (2-space indentation):
|
|
101
101
|
```yaml
|
|
102
|
-
-
|
|
103
|
-
|
|
102
|
+
- title: "<title>"
|
|
103
|
+
id: BET-NNN
|
|
104
104
|
blocked_by: []
|
|
105
105
|
unlocks: []
|
|
106
106
|
requires_pdrs: []
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
### Problem exploration (oprim-explore)
|
|
2
|
+
Investigate a problem or opportunity and compare candidate framings, before any decision artifact is written. Read-only and non-committal — never writes a `bet-decision.md`.
|
|
3
|
+
|
|
4
|
+
1. Ask for the problem or opportunity (a sentence or two).
|
|
5
|
+
2. Surface related PDRs: scan `oprim/decisions/PDR-*.md`, extract keywords from the problem statement, list matches by filename/title/body relevance.
|
|
6
|
+
3. Surface related notes: scan `oprim/notes/NOTE-*.md` the same way.
|
|
7
|
+
4. Surface related bets: scan `oprim/bets/pending/` and `oprim/bets/archived/` for bet-decisions with similar titles or "Why now" content — flag archived matches explicitly, since they may mean this ground has been covered before.
|
|
8
|
+
5. Ask for candidate approaches/framings (description + main tradeoff each) and present them side by side without recommending one, unless asked.
|
|
9
|
+
6. Report what was surfaced and, if the user has converged on a candidate, tell them to run `/oprim:bet` to draft the decision — explore does not create the bet itself.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
id: explore
|
|
2
|
+
skillName: oprim-explore
|
|
3
|
+
title: "OPRIM: Explore"
|
|
4
|
+
description: Investigate a problem and compare candidate framings before a bet is drafted
|
|
5
|
+
claude:
|
|
6
|
+
skill: true
|
|
7
|
+
command: explore.md
|
|
8
|
+
cursor:
|
|
9
|
+
skill: false
|
|
10
|
+
command: null
|
|
11
|
+
poolside:
|
|
12
|
+
skill: true
|
|
13
|
+
inline: true
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oprim-explore
|
|
3
|
+
description: Investigate a problem and compare candidate framings before a bet is drafted
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Investigate a problem or opportunity and compare candidate framings, before any decision artifact is written. This is oprim's think-first phase — it sits upstream of `/oprim:bet`, not in place of it.
|
|
7
|
+
|
|
8
|
+
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
9
|
+
|
|
10
|
+
## What you're doing
|
|
11
|
+
|
|
12
|
+
Explore is read-only and non-committal: it never writes a `bet-decision.md`. Use it when you've noticed a problem or opportunity but haven't yet converged on what to build, or when there are multiple plausible approaches worth weighing before committing to one. Once a candidate framing is worth committing to, hand off to `/oprim:bet` to draft the decision artifact — that boundary is deliberate, so explore doesn't duplicate `oprim-bet`'s job or drift into writing decisions itself.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Get the problem or opportunity
|
|
17
|
+
If not provided, ask: "What problem or opportunity do you want to explore? (a sentence or two)"
|
|
18
|
+
|
|
19
|
+
### 2. Surface related PDRs
|
|
20
|
+
Scan `oprim/decisions/` for files matching `PDR-*.md`. If the directory is empty or missing, skip this step. Otherwise, extract 3–10 keywords from the problem statement (subject-area nouns, capability names, bet IDs). For each PDR file, read the filename and first 25 lines; a PDR is relevant if any keyword appears in its filename, title, or body (case-insensitive). List matches:
|
|
21
|
+
|
|
22
|
+
**Related product decisions:**
|
|
23
|
+
- PDR-NNN: <title> — <Status> (`oprim/decisions/PDR-NNN-<slug>.md`)
|
|
24
|
+
|
|
25
|
+
If none match, state that plainly and continue.
|
|
26
|
+
|
|
27
|
+
### 3. Surface related notes
|
|
28
|
+
Scan `oprim/notes/` for files matching `NOTE-*.md`. Using the same keywords from step 2, list notes whose title or body mention them:
|
|
29
|
+
|
|
30
|
+
**Related notes:**
|
|
31
|
+
- NOTE-NNN: <title> (`oprim/notes/NOTE-NNN-<slug>.md`)
|
|
32
|
+
|
|
33
|
+
If `oprim/notes/` is empty or missing, or nothing matches, state that plainly and continue.
|
|
34
|
+
|
|
35
|
+
### 4. Surface related bets
|
|
36
|
+
Scan `oprim/bets/pending/` and `oprim/bets/archived/` for `bet-decision.md` files whose title or `## Why now` section mentions the keywords from step 2. List matches, noting whether each is pending or archived:
|
|
37
|
+
|
|
38
|
+
**Related bets:**
|
|
39
|
+
- BET-NNN: <title> — <pending/archived> (`oprim/bets/<pending|archived>/BET-NNN.../bet-decision.md`)
|
|
40
|
+
|
|
41
|
+
An archived match with a similar problem statement is worth flagging explicitly — it may mean this ground has been covered before.
|
|
42
|
+
|
|
43
|
+
### 5. Gather candidate framings
|
|
44
|
+
Ask: "What are the candidate approaches or framings worth comparing? (list as many as you'd like — one is fine if you already have a clear direction)"
|
|
45
|
+
|
|
46
|
+
For each candidate, ask for:
|
|
47
|
+
- A one-line description
|
|
48
|
+
- The main tradeoff or risk (what makes this candidate weaker than the alternatives, or what's uncertain about it)
|
|
49
|
+
|
|
50
|
+
### 6. Compare candidates
|
|
51
|
+
Present the candidates side by side (name, description, main tradeoff). Do not recommend one over another unless asked — the point of explore is to lay out the comparison clearly, not to decide for the user.
|
|
52
|
+
|
|
53
|
+
### 7. Report and hand off
|
|
54
|
+
Summarize what was surfaced (PDRs, notes, bets, candidate comparison). If the user has converged on a candidate worth committing to, tell them to run `/oprim:bet` to draft the decision artifact — explore does not create one itself. If no candidate has converged yet, note that explicitly and suggest gathering more information before drafting a bet.
|
|
@@ -24,6 +24,6 @@ Promote an atomic note into a bet, or a prioritized bet into a capability spec.
|
|
|
24
24
|
2. **Assign the next BET ID** — scan both `oprim/bets/pending/` and `oprim/bets/archived/` for directories matching `BET-(\d+)(-[^/]*)?`, max+1 zero-padded to 3 digits (default 001) — same convention `oprim-bet` uses
|
|
25
25
|
3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
|
|
26
26
|
4. **Draft the bet** — write `oprim/bets/pending/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.
|
|
27
|
-
5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{
|
|
27
|
+
5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`
|
|
28
28
|
6. **Link back** — add `Bets: BET-NNN` to the note (creating or extending its `## Bets` section)
|
|
29
29
|
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
|
|
@@ -20,6 +20,6 @@ Promote an atomic note into a bet, or a prioritized bet into a capability spec.
|
|
|
20
20
|
2. **Assign the next BET ID** — scan both `oprim/bets/pending/` and `oprim/bets/archived/` for directories matching `BET-(\d+)(-[^/]*)?`, max+1 zero-padded to 3 digits (default 001) — same convention `oprim-bet` uses
|
|
21
21
|
3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
|
|
22
22
|
4. **Draft the bet** — write `oprim/bets/pending/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.
|
|
23
|
-
5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{
|
|
23
|
+
5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`
|
|
24
24
|
6. **Link back** — add `Bets: BET-NNN` to the note (creating or extending its `## Bets` section)
|
|
25
25
|
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
|
|
@@ -35,6 +35,6 @@ Promote an atomic note into a bet, or a prioritized bet into a capability spec.
|
|
|
35
35
|
2. **Assign the next BET ID** — scan both `oprim/bets/pending/` and `oprim/bets/archived/` for directories matching `BET-(\d+)(-[^/]*)?`, max+1 zero-padded to 3 digits (default 001) — same convention `oprim-bet` uses
|
|
36
36
|
3. **Derive the slug** from the note's title (lowercase, non-alphanumeric → hyphen, collapse/trim hyphens, truncate to 40 chars at a hyphen boundary)
|
|
37
37
|
4. **Draft the bet** — write `oprim/bets/pending/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.
|
|
38
|
-
5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{
|
|
38
|
+
5. **Register the new bet** — append to `oprim/sequence.yaml` backlog: `{title, id, blocked_by: [], unlocks: [], requires_pdrs: []}`
|
|
39
39
|
6. **Link back** — add `Bets: BET-NNN` to the note (creating or extending its `## Bets` section)
|
|
40
40
|
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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
### Artifact reconciliation (oprim-reconcile)
|
|
2
|
+
Detect and fix drift across linked oprim artifacts (PDR ↔ bet, bet ↔ review). Never batch-applies a fix — every proposed change is confirmed individually before being written.
|
|
3
|
+
|
|
4
|
+
1. Scan every `bet-decision.md` under `oprim/bets/pending/` and `oprim/bets/archived/`: check each `## Links` reference (PDRs, Notes, Spec (delta), OpenSpec change) actually resolves to an existing file/path.
|
|
5
|
+
2. Scan every `oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md` filename: check the referenced BET-NNN still exists in pending or archived bets.
|
|
6
|
+
3. If nothing is found, report "No drift detected" and stop.
|
|
7
|
+
4. Otherwise, list every drift entry, propose a specific fix per entry (e.g. remove a dangling ID from a `## Links` line), and ask "Apply this fix? (y/N)" one item at a time — apply only confirmed fixes.
|
|
8
|
+
5. Report how many drift entries were found, fixed, and left unresolved.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
id: reconcile
|
|
2
|
+
skillName: oprim-reconcile
|
|
3
|
+
title: "OPRIM: Reconcile"
|
|
4
|
+
description: Detect and fix drift across linked oprim artifacts (PDR, bet, criteria, review)
|
|
5
|
+
claude:
|
|
6
|
+
skill: true
|
|
7
|
+
command: reconcile.md
|
|
8
|
+
cursor:
|
|
9
|
+
skill: false
|
|
10
|
+
command: null
|
|
11
|
+
poolside:
|
|
12
|
+
skill: true
|
|
13
|
+
inline: true
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oprim-reconcile
|
|
3
|
+
description: Detect and fix drift across linked oprim artifacts (PDR, bet, criteria, review)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Detect drift across linked oprim artifacts — PDR ↔ bet, bet ↔ criteria, bet ↔ review — and fix it with the user's per-item confirmation. Reconcile never batch-applies a fix; every proposed change is surfaced individually before it's written.
|
|
7
|
+
|
|
8
|
+
**Interactive prompts:** Use the **AskUserQuestion tool** for every question in this skill — do not write questions as plain text.
|
|
9
|
+
|
|
10
|
+
## What you're doing
|
|
11
|
+
|
|
12
|
+
oprim artifacts reference each other (a bet's `## Links` section names PDRs, notes, and specs; a review's filename names the bet it reviews) but nothing currently checks those references stay valid as artifacts are renamed, moved, or archived. Reconcile is a read-then-confirm-then-write pass over that link graph — it detects drift, proposes a specific fix per item, and only writes a fix the user confirms. This is distinct from `oprim doctor`, which reports sequencing-board and skill-drift issues but never writes a fix itself.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Scan bet-decision `## Links` sections
|
|
17
|
+
For every `bet-decision.md` under `oprim/bets/pending/` and `oprim/bets/archived/`, read its `## Links` section and check each reference:
|
|
18
|
+
- `PDRs: PDR-NNN, ...` — for each ID (skip "None"), verify a matching `oprim/decisions/PDR-NNN-*.md` exists
|
|
19
|
+
- `Notes: NOTE-NNN, ...` — for each ID, verify a matching `oprim/notes/NOTE-NNN-*.md` exists
|
|
20
|
+
- `Spec (delta): <path>` — verify the path exists
|
|
21
|
+
- `OpenSpec change: <path>` — skip if the value is still a placeholder (e.g. "to be filled when promoted" or "None"); otherwise verify the path exists
|
|
22
|
+
|
|
23
|
+
Record a drift entry for every reference that fails to resolve: which bet, which link line, which ID/path is missing.
|
|
24
|
+
|
|
25
|
+
### 2. Scan review filenames against bets
|
|
26
|
+
For every `oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md` file, extract `BET-NNN` and verify a matching bet directory exists in either `oprim/bets/pending/` or `oprim/bets/archived/`. Record a drift entry for any review whose bet can't be found.
|
|
27
|
+
|
|
28
|
+
### 3. Scan criteria.yaml placement
|
|
29
|
+
For every `oprim/bets/pending/BET-NNN.../criteria.yaml`, this is only valid while the bet is pending — it's expected to travel with the bet directory on archive, so no separate reference check is needed here. Skip this step; it exists to document why criteria.yaml isn't independently checked.
|
|
30
|
+
|
|
31
|
+
### 4. Report detected drift
|
|
32
|
+
If no drift was found in steps 1–2, report "No drift detected across PDR/bet/criteria/review links" and stop.
|
|
33
|
+
|
|
34
|
+
Otherwise, list every drift entry found:
|
|
35
|
+
|
|
36
|
+
**Detected drift:**
|
|
37
|
+
- BET-NNN `## Links`: `PDRs` references PDR-XXX, which does not exist at `oprim/decisions/PDR-XXX-*.md`
|
|
38
|
+
- `oprim/reviews/<file>` references BET-NNN, which does not exist in `oprim/bets/pending/` or `oprim/bets/archived/`
|
|
39
|
+
|
|
40
|
+
### 5. Propose and confirm a fix, one item at a time
|
|
41
|
+
For each drift entry, propose a specific fix:
|
|
42
|
+
- A dangling PDR/Notes ID in a `## Links` line → propose removing that ID from the comma-separated list (or replacing the line with "None" if it was the only entry)
|
|
43
|
+
- A `Spec (delta)` or `OpenSpec change` path that no longer resolves → propose removing that link line entirely
|
|
44
|
+
- A review referencing a bet that no longer exists → propose no automatic fix (the bet may have been renamed rather than deleted); ask the user to identify the correct BET-NNN or confirm the review is orphaned and should be left as-is
|
|
45
|
+
|
|
46
|
+
Ask: "Apply this fix? (y/N)" for each entry individually.
|
|
47
|
+
- If confirmed: apply only that fix to the relevant file, then move to the next drift entry.
|
|
48
|
+
- If declined: leave the artifact unchanged and move to the next drift entry.
|
|
49
|
+
|
|
50
|
+
### 6. Report what was fixed
|
|
51
|
+
Summarize: how many drift entries were found, how many fixes were applied, how many were declined or left for manual follow-up.
|
|
@@ -79,7 +79,7 @@ Ask: "Apply this change? (y/N)"
|
|
|
79
79
|
|
|
80
80
|
### 5. Write sequence.yaml
|
|
81
81
|
|
|
82
|
-
Read `oprim/sequence.yaml`. Remove the bet entry from its current lane. Insert
|
|
82
|
+
Read `oprim/sequence.yaml`. Remove the bet entry from its current lane. If the entry's fields are ordered `id:` before `title:`, reorder them to `title:` before `id:` (leave `blocked_by`, `unlocks`, `requires_pdrs` order unchanged). Insert the entry into the target lane. Write back with 2-space indentation. Do not modify any other entries.
|
|
83
83
|
|
|
84
84
|
### 6. Regenerate view
|
|
85
85
|
|