oh-my-opencode-slim 0.9.15 → 1.0.1

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.
Files changed (41) hide show
  1. package/README.md +38 -18
  2. package/dist/agents/council.d.ts +7 -8
  3. package/dist/agents/orchestrator.d.ts +1 -1
  4. package/dist/background/background-manager.d.ts +11 -0
  5. package/dist/background/index.d.ts +1 -1
  6. package/dist/cli/index.js +309 -13804
  7. package/dist/cli/types.d.ts +1 -1
  8. package/dist/config/constants.d.ts +3 -3
  9. package/dist/config/council-schema.d.ts +46 -69
  10. package/dist/config/index.d.ts +1 -1
  11. package/dist/config/schema.d.ts +48 -33
  12. package/dist/config/utils.d.ts +7 -0
  13. package/dist/council/council-manager.d.ts +9 -13
  14. package/dist/hooks/auto-update-checker/types.d.ts +1 -1
  15. package/dist/hooks/foreground-fallback/index.d.ts +1 -1
  16. package/dist/hooks/phase-reminder/index.d.ts +1 -1
  17. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +0 -1
  18. package/dist/index.js +7670 -26628
  19. package/dist/interview/service.d.ts +1 -0
  20. package/dist/multiplexer/index.d.ts +1 -0
  21. package/dist/multiplexer/session-manager.d.ts +46 -0
  22. package/dist/multiplexer/types.d.ts +1 -1
  23. package/dist/tools/ast-grep/index.d.ts +1 -1
  24. package/dist/tools/background.d.ts +1 -1
  25. package/dist/tools/council.d.ts +2 -1
  26. package/dist/tools/index.d.ts +2 -2
  27. package/dist/tools/lsp/types.d.ts +1 -1
  28. package/dist/tools/preset-manager.d.ts +27 -0
  29. package/dist/utils/subagent-depth.d.ts +35 -0
  30. package/dist/utils/tmux-debug-log.d.ts +2 -0
  31. package/oh-my-opencode-slim.schema.json +35 -50
  32. package/package.json +9 -10
  33. package/src/skills/{cartography → codemap}/README.md +11 -9
  34. package/src/skills/{cartography → codemap}/SKILL.md +21 -18
  35. package/src/skills/codemap/codemap.md +36 -0
  36. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  37. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  38. package/src/skills/codemap.md +40 -0
  39. package/src/skills/simplify/codemap.md +36 -0
  40. package/src/skills/cartography/scripts/cartographer.py +0 -456
  41. package/src/skills/cartography/scripts/test_cartographer.py +0 -87
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ renameSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ export const VERSION = '1.0.0';
17
+ export const STATE_DIR = '.slim';
18
+ export const STATE_FILE = 'codemap.json';
19
+ export const LEGACY_STATE_FILE = 'cartography.json';
20
+ export const CODEMAP_FILE = 'codemap.md';
21
+
22
+ export class PatternMatcher {
23
+ regex;
24
+
25
+ constructor(patterns) {
26
+ if (!patterns.length) {
27
+ this.regex = null;
28
+ return;
29
+ }
30
+
31
+ const regexParts = patterns.map((pattern) => {
32
+ let reg = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ reg = reg.replace(/\\\*\\\*\//g, '(?:.*/)?');
34
+ reg = reg.replace(/\\\*\\\*/g, '.*');
35
+ reg = reg.replace(/\\\*/g, '[^/]*');
36
+ reg = reg.replace(/\\\?/g, '.');
37
+
38
+ if (pattern.endsWith('/')) {
39
+ reg += '.*';
40
+ }
41
+
42
+ if (pattern.startsWith('/')) {
43
+ reg = `^${reg.slice(1)}`;
44
+ } else {
45
+ reg = `(?:^|.*/)${reg}`;
46
+ }
47
+
48
+ return `(?:${reg}$)`;
49
+ });
50
+
51
+ this.regex = new RegExp(regexParts.join('|'));
52
+ }
53
+
54
+ matches(filePath) {
55
+ if (!this.regex) return false;
56
+ return this.regex.test(filePath);
57
+ }
58
+ }
59
+
60
+ export function loadGitignore(root) {
61
+ const gitignorePath = path.join(root, '.gitignore');
62
+ if (!existsSync(gitignorePath)) return [];
63
+
64
+ return readFileSync(gitignorePath, 'utf8')
65
+ .split('\n')
66
+ .map((line) => line.trim())
67
+ .filter((line) => line && !line.startsWith('#'));
68
+ }
69
+
70
+ function walkFiles(root) {
71
+ const files = [];
72
+
73
+ function visit(currentDir) {
74
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
75
+ const fullPath = path.join(currentDir, entry.name);
76
+ if (entry.isDirectory()) {
77
+ if (!entry.name.startsWith('.')) {
78
+ visit(fullPath);
79
+ }
80
+ continue;
81
+ }
82
+
83
+ if (entry.isFile()) {
84
+ files.push(fullPath);
85
+ }
86
+ }
87
+ }
88
+
89
+ visit(root);
90
+ return files.sort();
91
+ }
92
+
93
+ export function selectFiles(
94
+ root,
95
+ includePatterns,
96
+ excludePatterns,
97
+ exceptions,
98
+ gitignorePatterns,
99
+ ) {
100
+ const includeMatcher = new PatternMatcher(includePatterns);
101
+ const excludeMatcher = new PatternMatcher(excludePatterns);
102
+ const gitignoreMatcher = new PatternMatcher(gitignorePatterns);
103
+ const exceptionSet = new Set(exceptions);
104
+
105
+ return walkFiles(root).filter((fullPath) => {
106
+ let relPath = path.relative(root, fullPath).replaceAll(path.sep, '/');
107
+ if (relPath.startsWith('./')) {
108
+ relPath = relPath.slice(2);
109
+ }
110
+
111
+ if (gitignoreMatcher.matches(relPath)) return false;
112
+ if (excludeMatcher.matches(relPath) && !exceptionSet.has(relPath)) {
113
+ return false;
114
+ }
115
+
116
+ return includeMatcher.matches(relPath) || exceptionSet.has(relPath);
117
+ });
118
+ }
119
+
120
+ export function computeFileHash(filePath) {
121
+ try {
122
+ const buffer = readFileSync(filePath);
123
+ return createHash('md5').update(buffer).digest('hex');
124
+ } catch {
125
+ return '';
126
+ }
127
+ }
128
+
129
+ export function computeFolderHash(folder, fileHashes) {
130
+ const folderFiles = Object.entries(fileHashes)
131
+ .filter(
132
+ ([filePath]) =>
133
+ filePath.startsWith(`${folder}/`) ||
134
+ (folder === '.' && !filePath.includes('/')),
135
+ )
136
+ .sort(([a], [b]) => a.localeCompare(b));
137
+
138
+ if (!folderFiles.length) return '';
139
+
140
+ const hasher = createHash('md5');
141
+ for (const [filePath, hash] of folderFiles) {
142
+ hasher.update(`${filePath}:${hash}\n`);
143
+ }
144
+ return hasher.digest('hex');
145
+ }
146
+
147
+ export function getFoldersWithFiles(files, root) {
148
+ const folders = new Set(['.']);
149
+
150
+ for (const filePath of files) {
151
+ const relPath = path.relative(root, filePath).replaceAll(path.sep, '/');
152
+ const parts = relPath.split('/').slice(0, -1);
153
+ for (let i = 0; i < parts.length; i++) {
154
+ folders.add(parts.slice(0, i + 1).join('/'));
155
+ }
156
+ }
157
+
158
+ return folders;
159
+ }
160
+
161
+ export function migrateLegacyState(root) {
162
+ const stateDir = path.join(root, STATE_DIR);
163
+ const legacyPath = path.join(stateDir, LEGACY_STATE_FILE);
164
+ const statePath = path.join(stateDir, STATE_FILE);
165
+
166
+ if (existsSync(statePath) || !existsSync(legacyPath)) {
167
+ return false;
168
+ }
169
+
170
+ mkdirSync(stateDir, { recursive: true });
171
+ renameSync(legacyPath, statePath);
172
+ console.log(
173
+ `Migrated ${STATE_DIR}/${LEGACY_STATE_FILE} -> ${STATE_DIR}/${STATE_FILE}`,
174
+ );
175
+ return true;
176
+ }
177
+
178
+ export function loadState(root) {
179
+ migrateLegacyState(root);
180
+ const statePath = path.join(root, STATE_DIR, STATE_FILE);
181
+ if (!existsSync(statePath)) return null;
182
+
183
+ try {
184
+ return JSON.parse(readFileSync(statePath, 'utf8'));
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ export function saveState(root, state) {
191
+ const stateDir = path.join(root, STATE_DIR);
192
+ mkdirSync(stateDir, { recursive: true });
193
+ writeFileSync(
194
+ path.join(stateDir, STATE_FILE),
195
+ `${JSON.stringify(state, null, 2)}\n`,
196
+ );
197
+ }
198
+
199
+ export function createEmptyCodemap(folderPath, folderName) {
200
+ const codemapPath = path.join(folderPath, CODEMAP_FILE);
201
+ if (existsSync(codemapPath)) return;
202
+
203
+ const content = `# ${folderName}/
204
+
205
+ <!-- Fixer: Fill in this section with architectural understanding -->
206
+
207
+ ## Responsibility
208
+
209
+ <!-- What is this folder's job in the system? -->
210
+
211
+ ## Design
212
+
213
+ <!-- Key patterns, abstractions, architectural decisions -->
214
+
215
+ ## Flow
216
+
217
+ <!-- How does data/control flow through this module? -->
218
+
219
+ ## Integration
220
+
221
+ <!-- How does it connect to other parts of the system? -->
222
+ `;
223
+
224
+ writeFileSync(codemapPath, content);
225
+ }
226
+
227
+ function buildState(
228
+ root,
229
+ includePatterns,
230
+ excludePatterns,
231
+ exceptions,
232
+ selectedFiles,
233
+ ) {
234
+ const fileHashes = {};
235
+ for (const filePath of selectedFiles) {
236
+ const relPath = path.relative(root, filePath).replaceAll(path.sep, '/');
237
+ fileHashes[relPath] = computeFileHash(filePath);
238
+ }
239
+
240
+ const folders = getFoldersWithFiles(selectedFiles, root);
241
+ const folderHashes = {};
242
+ for (const folder of folders) {
243
+ folderHashes[folder] = computeFolderHash(folder, fileHashes);
244
+ }
245
+
246
+ const state = {
247
+ metadata: {
248
+ version: VERSION,
249
+ last_run: new Date().toISOString(),
250
+ root,
251
+ include_patterns: includePatterns,
252
+ exclude_patterns: excludePatterns,
253
+ exceptions,
254
+ },
255
+ file_hashes: fileHashes,
256
+ folder_hashes: folderHashes,
257
+ };
258
+
259
+ return { state, folders };
260
+ }
261
+
262
+ export function cmdInit({ root, include = [], exclude = [], exception = [] }) {
263
+ const resolvedRoot = path.resolve(root);
264
+ if (!existsSync(resolvedRoot) || !statSync(resolvedRoot).isDirectory()) {
265
+ console.error(`Error: ${resolvedRoot} is not a directory`);
266
+ return 1;
267
+ }
268
+
269
+ const includePatterns = include.length ? include : ['**/*'];
270
+ const excludePatterns = exclude;
271
+ const exceptions = exception;
272
+ const gitignore = loadGitignore(resolvedRoot);
273
+
274
+ console.log(`Scanning ${resolvedRoot}...`);
275
+ console.log(`Include patterns: ${JSON.stringify(includePatterns)}`);
276
+ console.log(`Exclude patterns: ${JSON.stringify(excludePatterns)}`);
277
+ console.log(`Exceptions: ${JSON.stringify(exceptions)}`);
278
+
279
+ const selectedFiles = selectFiles(
280
+ resolvedRoot,
281
+ includePatterns,
282
+ excludePatterns,
283
+ exceptions,
284
+ gitignore,
285
+ );
286
+
287
+ console.log(`Selected ${selectedFiles.length} files`);
288
+
289
+ const { state, folders } = buildState(
290
+ resolvedRoot,
291
+ includePatterns,
292
+ excludePatterns,
293
+ exceptions,
294
+ selectedFiles,
295
+ );
296
+
297
+ saveState(resolvedRoot, state);
298
+ console.log(`Created ${STATE_DIR}/${STATE_FILE}`);
299
+
300
+ for (const folder of folders) {
301
+ const folderPath =
302
+ folder === '.' ? resolvedRoot : path.join(resolvedRoot, folder);
303
+ const folderName = folder === '.' ? path.basename(resolvedRoot) : folder;
304
+ createEmptyCodemap(folderPath, folderName);
305
+ }
306
+
307
+ console.log(`Created ${folders.size} empty codemap.md files`);
308
+ return 0;
309
+ }
310
+
311
+ export function cmdChanges({ root }) {
312
+ const resolvedRoot = path.resolve(root);
313
+ const state = loadState(resolvedRoot);
314
+ if (!state) {
315
+ console.error("No codemap state found. Run 'init' first.");
316
+ return 1;
317
+ }
318
+
319
+ const metadata = state.metadata ?? {};
320
+ const includePatterns = metadata.include_patterns ?? ['**/*'];
321
+ const excludePatterns = metadata.exclude_patterns ?? [];
322
+ const exceptions = metadata.exceptions ?? [];
323
+ const gitignore = loadGitignore(resolvedRoot);
324
+
325
+ const currentFiles = selectFiles(
326
+ resolvedRoot,
327
+ includePatterns,
328
+ excludePatterns,
329
+ exceptions,
330
+ gitignore,
331
+ );
332
+
333
+ const currentHashes = Object.fromEntries(
334
+ currentFiles.map((filePath) => [
335
+ path.relative(resolvedRoot, filePath).replaceAll(path.sep, '/'),
336
+ computeFileHash(filePath),
337
+ ]),
338
+ );
339
+
340
+ const savedHashes = state.file_hashes ?? {};
341
+ const currentPaths = new Set(Object.keys(currentHashes));
342
+ const savedPaths = new Set(Object.keys(savedHashes));
343
+
344
+ const added = [...currentPaths]
345
+ .filter((filePath) => !savedPaths.has(filePath))
346
+ .sort();
347
+ const removed = [...savedPaths]
348
+ .filter((filePath) => !currentPaths.has(filePath))
349
+ .sort();
350
+ const modified = [...currentPaths]
351
+ .filter((filePath) => savedPaths.has(filePath))
352
+ .filter((filePath) => currentHashes[filePath] !== savedHashes[filePath])
353
+ .sort();
354
+
355
+ if (!added.length && !removed.length && !modified.length) {
356
+ console.log('No changes detected.');
357
+ return 0;
358
+ }
359
+
360
+ if (added.length) {
361
+ console.log(`\n${added.length} added:`);
362
+ for (const filePath of added) console.log(` + ${filePath}`);
363
+ }
364
+
365
+ if (removed.length) {
366
+ console.log(`\n${removed.length} removed:`);
367
+ for (const filePath of removed) console.log(` - ${filePath}`);
368
+ }
369
+
370
+ if (modified.length) {
371
+ console.log(`\n${modified.length} modified:`);
372
+ for (const filePath of modified) console.log(` ~ ${filePath}`);
373
+ }
374
+
375
+ const affectedFolders = new Set(['.']);
376
+ for (const filePath of [...added, ...removed, ...modified]) {
377
+ const parts = filePath.split('/').slice(0, -1);
378
+ for (let i = 0; i < parts.length; i++) {
379
+ affectedFolders.add(parts.slice(0, i + 1).join('/'));
380
+ }
381
+ }
382
+
383
+ const sortedFolders = [...affectedFolders].sort();
384
+ console.log(`\n${sortedFolders.length} folders affected:`);
385
+ for (const folder of sortedFolders) {
386
+ console.log(` ${folder}/`);
387
+ }
388
+
389
+ return 0;
390
+ }
391
+
392
+ export function cmdUpdate({ root }) {
393
+ const resolvedRoot = path.resolve(root);
394
+ const state = loadState(resolvedRoot);
395
+ if (!state) {
396
+ console.error("No codemap state found. Run 'init' first.");
397
+ return 1;
398
+ }
399
+
400
+ const metadata = state.metadata ?? {};
401
+ const includePatterns = metadata.include_patterns ?? ['**/*'];
402
+ const excludePatterns = metadata.exclude_patterns ?? [];
403
+ const exceptions = metadata.exceptions ?? [];
404
+ const gitignore = loadGitignore(resolvedRoot);
405
+
406
+ const selectedFiles = selectFiles(
407
+ resolvedRoot,
408
+ includePatterns,
409
+ excludePatterns,
410
+ exceptions,
411
+ gitignore,
412
+ );
413
+
414
+ const { state: nextState } = buildState(
415
+ resolvedRoot,
416
+ includePatterns,
417
+ excludePatterns,
418
+ exceptions,
419
+ selectedFiles,
420
+ );
421
+
422
+ saveState(resolvedRoot, nextState);
423
+ console.log(
424
+ `Updated ${STATE_DIR}/${STATE_FILE} with ${selectedFiles.length} files`,
425
+ );
426
+ return 0;
427
+ }
428
+
429
+ export function parseArgs(argv) {
430
+ const [command, ...rest] = argv;
431
+ const options = { include: [], exclude: [], exception: [] };
432
+
433
+ for (let i = 0; i < rest.length; i++) {
434
+ const arg = rest[i];
435
+ const value = rest[i + 1];
436
+
437
+ if (!arg?.startsWith('--')) continue;
438
+ if (value === undefined || value.startsWith('--')) {
439
+ throw new Error(`Missing value for ${arg}`);
440
+ }
441
+
442
+ const key = arg.slice(2);
443
+ if (key === 'include' || key === 'exclude' || key === 'exception') {
444
+ options[key].push(value);
445
+ } else if (key === 'root') {
446
+ options.root = value;
447
+ } else {
448
+ throw new Error(`Unknown option: ${arg}`);
449
+ }
450
+
451
+ i++;
452
+ }
453
+
454
+ return { command, options };
455
+ }
456
+
457
+ export function main(argv = process.argv.slice(2)) {
458
+ try {
459
+ const { command, options } = parseArgs(argv);
460
+
461
+ if (!command || !options.root) {
462
+ console.error(
463
+ 'Usage: codemap.mjs <init|changes|update> --root /path [--include glob] [--exclude glob] [--exception path]',
464
+ );
465
+ return 1;
466
+ }
467
+
468
+ if (command === 'init') return cmdInit(options);
469
+ if (command === 'changes') return cmdChanges(options);
470
+ if (command === 'update') return cmdUpdate(options);
471
+
472
+ console.error(`Unknown command: ${command}`);
473
+ return 1;
474
+ } catch (error) {
475
+ console.error(error instanceof Error ? error.message : String(error));
476
+ return 1;
477
+ }
478
+ }
479
+
480
+ const currentFilePath = fileURLToPath(import.meta.url);
481
+ if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
482
+ process.exit(main());
483
+ }
@@ -0,0 +1,129 @@
1
+ import { afterEach, describe, expect, mock, test } from 'bun:test';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+
13
+ mock.restore();
14
+
15
+ const {
16
+ computeFileHash,
17
+ computeFolderHash,
18
+ loadState,
19
+ PatternMatcher,
20
+ selectFiles,
21
+ } = await import('./codemap.mjs');
22
+
23
+ const tempDirs: string[] = [];
24
+
25
+ function createTempDir() {
26
+ const dir = mkdtempSync(path.join(os.tmpdir(), 'codemap-'));
27
+ tempDirs.push(dir);
28
+ return dir;
29
+ }
30
+
31
+ afterEach(() => {
32
+ for (const dir of tempDirs.splice(0)) {
33
+ rmSync(dir, { force: true, recursive: true });
34
+ }
35
+ });
36
+
37
+ describe('PatternMatcher', () => {
38
+ test('matches expected paths', () => {
39
+ const matcher = new PatternMatcher([
40
+ 'node_modules/',
41
+ 'dist/',
42
+ '*.log',
43
+ 'src/**/*.ts',
44
+ ]);
45
+
46
+ expect(matcher.matches('node_modules/foo.js')).toBe(true);
47
+ expect(matcher.matches('vendor/node_modules/bar.js')).toBe(true);
48
+ expect(matcher.matches('dist/main.js')).toBe(true);
49
+ expect(matcher.matches('src/dist/output.js')).toBe(true);
50
+ expect(matcher.matches('error.log')).toBe(true);
51
+ expect(matcher.matches('logs/access.log')).toBe(true);
52
+ expect(matcher.matches('src/index.ts')).toBe(true);
53
+ expect(matcher.matches('src/utils/helper.ts')).toBe(true);
54
+ expect(matcher.matches('README.md')).toBe(false);
55
+ expect(matcher.matches('tests/test.py')).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('hash helpers', () => {
60
+ test('computes file hash', () => {
61
+ const dir = createTempDir();
62
+ const filePath = path.join(dir, 'file.txt');
63
+ writeFileSync(filePath, 'test content');
64
+
65
+ expect(computeFileHash(filePath)).toBe('9473fdd0d880a43c21b7778d34872157');
66
+ });
67
+
68
+ test('computes stable folder hash', () => {
69
+ const fileHashes = {
70
+ 'src/a.ts': 'hash-a',
71
+ 'src/b.ts': 'hash-b',
72
+ 'tests/test.ts': 'hash-test',
73
+ };
74
+
75
+ const hash1 = computeFolderHash('src', fileHashes);
76
+ const hash2 = computeFolderHash('src', fileHashes);
77
+ const hash3 = computeFolderHash('src', {
78
+ 'src/a.ts': 'hash-a-modified',
79
+ 'src/b.ts': 'hash-b',
80
+ });
81
+
82
+ expect(hash1).toBe(hash2);
83
+ expect(hash1).not.toBe(hash3);
84
+ });
85
+ });
86
+
87
+ describe('selectFiles', () => {
88
+ test('respects include and exclude patterns', () => {
89
+ const root = createTempDir();
90
+ mkdirSync(path.join(root, 'src'));
91
+ mkdirSync(path.join(root, 'node_modules'));
92
+ writeFileSync(path.join(root, 'src', 'index.ts'), 'code');
93
+ writeFileSync(path.join(root, 'src', 'index.test.ts'), 'test');
94
+ writeFileSync(path.join(root, 'node_modules', 'foo.js'), 'dep');
95
+ writeFileSync(path.join(root, 'package.json'), '{}');
96
+
97
+ const selected = selectFiles(
98
+ root,
99
+ ['src/**/*.ts', 'package.json'],
100
+ ['**/*.test.ts', 'node_modules/'],
101
+ [],
102
+ [],
103
+ ).map((filePath) =>
104
+ path.relative(root, filePath).split(path.sep).join('/'),
105
+ );
106
+
107
+ expect(selected).toEqual(['package.json', 'src/index.ts']);
108
+ });
109
+ });
110
+
111
+ describe('loadState', () => {
112
+ test('migrates legacy cartography state', () => {
113
+ const root = createTempDir();
114
+ const slimDir = path.join(root, '.slim');
115
+ mkdirSync(slimDir);
116
+
117
+ const legacyState = { metadata: { version: '1.0.0' } };
118
+ writeFileSync(
119
+ path.join(slimDir, 'cartography.json'),
120
+ JSON.stringify(legacyState),
121
+ );
122
+
123
+ expect(loadState(root)).toEqual(legacyState);
124
+ expect(existsSync(path.join(slimDir, 'cartography.json'))).toBe(false);
125
+ expect(
126
+ JSON.parse(readFileSync(path.join(slimDir, 'codemap.json'), 'utf8')),
127
+ ).toEqual(legacyState);
128
+ });
129
+ });
@@ -0,0 +1,40 @@
1
+ # src/skills/
2
+
3
+ ## Responsibility
4
+
5
+ - Own metadata-driven OpenCode custom skills shipped with this package.
6
+ - Maintain the skill contract artifacts (`SKILL.md`, `README.md`, per-skill helper files) that are copied into
7
+ `${configDir}/skills` at install time.
8
+ - Preserve a canonical registry boundary: runtime code consumes skill definitions as data, not as executable
9
+ plugin dependencies.
10
+
11
+ ## Design
12
+
13
+ - `CUSTOM_SKILLS` in `src/cli/custom-skills.ts` is the authoritative skill manifest for bundled
14
+ skills; each entry maps folder name + `sourcePath` to an install-time consumer.
15
+ - `install.ts` runs `installCustomSkill()` which recursively copies `src/skills/codemap` and
16
+ `src/skills/simplify` into the OpenCode skills directory.
17
+ - This directory is partitioned by skill:
18
+ - `src/skills/codemap/` (command-style repository mapping skill)
19
+ - `src/skills/simplify/` (readability/refactor guidance skill)
20
+ - Files are considered static runtime payload. No plugin TS module in `src/` imports these files directly; they
21
+ are loaded by OpenCode via filesystem installation.
22
+
23
+ ## Flow
24
+
25
+ - `bun run install` delegates to `src/cli/install.ts`, where `installCustomSkills` gates copying of
26
+ each `CUSTOM_SKILLS` entry.
27
+ - `installCustomSkill()` computes `packageRoot`, validates `sourcePath`, then performs a recursive
28
+ directory copy via `copyDirRecursive()`.
29
+ - During plugin release, the `files` whitelist in `package.json` must include `src/skills` so
30
+ `src/skills/**` survive `npm pack`.
31
+ - OpenCode plugin startup discovers these installed folders and reads each `SKILL.md` as a prompt-level contract.
32
+
33
+ ## Integration
34
+
35
+ - `src/cli/custom-skills.ts`: source-of-truth registry consumed by installer and permission helpers.
36
+ - `src/cli/skills.ts:getSkillPermissionsForAgent()` auto-populates permission rules for
37
+ `codemap` and `simplify` when agent policy is derived from built-in recommendations.
38
+ - `verify-release-artifact.ts` enforces artifact completeness by asserting `src/skills/simplify/SKILL.md`
39
+ and `src/skills/codemap/SKILL.md` are present in the tarball.
40
+ - `package.json` scripts (`verify:release`, `build`) rely on these assets to ensure install-time skill availability.
@@ -0,0 +1,36 @@
1
+ # src/skills/simplify/
2
+
3
+ ## Responsibility
4
+
5
+ - Provide a behavior-preserving refactoring skill contract that constrains code cleanup to clarity-focused,
6
+ low-risk changes.
7
+ - Define explicit quality gates (understand-before-edit, behavior parity, incremental simplification, rollback-friendly diffs)
8
+ for any simplification task.
9
+ - Ship only metadata; no local runtime state machine is kept in this directory.
10
+
11
+ ## Design
12
+
13
+ - Contract layer: `SKILL.md` is the executable prompt specification with explicit phases:
14
+ - pre-change understanding
15
+ - simplification candidate selection
16
+ - incremental transformation and verification
17
+ - final review checklist.
18
+ - Documentation layer: `README.md` explains intent, source provenance, and plugin install behavior.
19
+ - Policy model is declarative (`description`, allowed usage, checklist) consumed by the OpenCode skill executor,
20
+ without helper scripts or plugin code dependencies.
21
+
22
+ ## Flow
23
+
24
+ - Agent discovery resolves `src/skills/simplify` as a custom skill entrypoint, then reads `SKILL.md` at runtime.
25
+ - Runtime behavior is gated by `src/cli/custom-skills.ts` (`allowedAgents: ['oracle']`) and by skill permissions
26
+ computed in `getSkillPermissionsForAgent()`.
27
+ - In practice the workflow is read-only and context-driven: simplify instructions require understanding of callers,
28
+ edge cases, and tests before mutation, then apply local, scoped refactors with validation.
29
+ - Consumers (Fixer/Oracle/Reviewer tasks) rely on this contract as operational constraints, not as executable TypeScript.
30
+
31
+ ## Integration
32
+
33
+ - Installed by plugin installer (`installCustomSkills`) using `src/cli/install.ts` via `installCustomSkill()`.
34
+ - Permission surface is enforced by hook layer in `src/hooks/filter-available-skills/index.ts` (`permissionRules`).
35
+ - Release integrity: `scripts/verify-release-artifact.ts` checks for `src/skills/simplify/SKILL.md` in package tarballs.
36
+ - Operationally paired with codemap/fixer flows in `src/index.ts` orchestrations for post-feature readability hardening.