opencode-rules-md 0.6.5

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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +650 -0
  3. package/dist/cli.mjs +374 -0
  4. package/dist/src/active-rules-state.d.ts +12 -0
  5. package/dist/src/active-rules-state.d.ts.map +1 -0
  6. package/dist/src/active-rules-state.js +118 -0
  7. package/dist/src/active-rules-state.js.map +1 -0
  8. package/dist/src/cli/config.d.ts +62 -0
  9. package/dist/src/cli/config.d.ts.map +1 -0
  10. package/dist/src/cli/config.js +246 -0
  11. package/dist/src/cli/config.js.map +1 -0
  12. package/dist/src/cli/install.d.ts +21 -0
  13. package/dist/src/cli/install.d.ts.map +1 -0
  14. package/dist/src/cli/install.js +63 -0
  15. package/dist/src/cli/install.js.map +1 -0
  16. package/dist/src/cli/main.d.ts +19 -0
  17. package/dist/src/cli/main.d.ts.map +1 -0
  18. package/dist/src/cli/main.js +135 -0
  19. package/dist/src/cli/main.js.map +1 -0
  20. package/dist/src/cli/real-fs.d.ts +26 -0
  21. package/dist/src/cli/real-fs.d.ts.map +1 -0
  22. package/dist/src/cli/real-fs.js +42 -0
  23. package/dist/src/cli/real-fs.js.map +1 -0
  24. package/dist/src/cli/status.d.ts +19 -0
  25. package/dist/src/cli/status.d.ts.map +1 -0
  26. package/dist/src/cli/status.js +58 -0
  27. package/dist/src/cli/status.js.map +1 -0
  28. package/dist/src/debug.d.ts +3 -0
  29. package/dist/src/debug.d.ts.map +1 -0
  30. package/dist/src/debug.js +8 -0
  31. package/dist/src/debug.js.map +1 -0
  32. package/dist/src/git-branch.d.ts +2 -0
  33. package/dist/src/git-branch.d.ts.map +1 -0
  34. package/dist/src/git-branch.js +30 -0
  35. package/dist/src/git-branch.js.map +1 -0
  36. package/dist/src/index.d.ts +26 -0
  37. package/dist/src/index.d.ts.map +1 -0
  38. package/dist/src/index.js +55 -0
  39. package/dist/src/index.js.map +1 -0
  40. package/dist/src/mcp-tools.d.ts +8 -0
  41. package/dist/src/mcp-tools.d.ts.map +1 -0
  42. package/dist/src/mcp-tools.js +18 -0
  43. package/dist/src/mcp-tools.js.map +1 -0
  44. package/dist/src/message-context.d.ts +55 -0
  45. package/dist/src/message-context.d.ts.map +1 -0
  46. package/dist/src/message-context.js +110 -0
  47. package/dist/src/message-context.js.map +1 -0
  48. package/dist/src/message-paths.d.ts +34 -0
  49. package/dist/src/message-paths.d.ts.map +1 -0
  50. package/dist/src/message-paths.js +112 -0
  51. package/dist/src/message-paths.js.map +1 -0
  52. package/dist/src/project-fingerprint.d.ts +2 -0
  53. package/dist/src/project-fingerprint.d.ts.map +1 -0
  54. package/dist/src/project-fingerprint.js +61 -0
  55. package/dist/src/project-fingerprint.js.map +1 -0
  56. package/dist/src/rule-discovery.d.ts +49 -0
  57. package/dist/src/rule-discovery.d.ts.map +1 -0
  58. package/dist/src/rule-discovery.js +143 -0
  59. package/dist/src/rule-discovery.js.map +1 -0
  60. package/dist/src/rule-filter.d.ts +61 -0
  61. package/dist/src/rule-filter.d.ts.map +1 -0
  62. package/dist/src/rule-filter.js +180 -0
  63. package/dist/src/rule-filter.js.map +1 -0
  64. package/dist/src/rule-metadata.d.ts +29 -0
  65. package/dist/src/rule-metadata.d.ts.map +1 -0
  66. package/dist/src/rule-metadata.js +100 -0
  67. package/dist/src/rule-metadata.js.map +1 -0
  68. package/dist/src/runtime-chat.d.ts +34 -0
  69. package/dist/src/runtime-chat.d.ts.map +1 -0
  70. package/dist/src/runtime-chat.js +39 -0
  71. package/dist/src/runtime-chat.js.map +1 -0
  72. package/dist/src/runtime-context.d.ts +25 -0
  73. package/dist/src/runtime-context.d.ts.map +1 -0
  74. package/dist/src/runtime-context.js +109 -0
  75. package/dist/src/runtime-context.js.map +1 -0
  76. package/dist/src/runtime.d.ts +31 -0
  77. package/dist/src/runtime.d.ts.map +1 -0
  78. package/dist/src/runtime.js +238 -0
  79. package/dist/src/runtime.js.map +1 -0
  80. package/dist/src/session-store.d.ts +34 -0
  81. package/dist/src/session-store.d.ts.map +1 -0
  82. package/dist/src/session-store.js +90 -0
  83. package/dist/src/session-store.js.map +1 -0
  84. package/dist/src/utils.d.ts +15 -0
  85. package/dist/src/utils.d.ts.map +1 -0
  86. package/dist/src/utils.js +19 -0
  87. package/dist/src/utils.js.map +1 -0
  88. package/dist/tui/data/rules.d.ts +63 -0
  89. package/dist/tui/data/rules.d.ts.map +1 -0
  90. package/dist/tui/data/rules.js +173 -0
  91. package/dist/tui/data/rules.js.map +1 -0
  92. package/dist/tui/index.d.ts +8 -0
  93. package/dist/tui/index.d.ts.map +1 -0
  94. package/dist/tui/index.js +13 -0
  95. package/dist/tui/index.js.map +1 -0
  96. package/dist/tui/slots/sidebar-content.d.ts +11 -0
  97. package/dist/tui/slots/sidebar-content.d.ts.map +1 -0
  98. package/dist/tui/slots/sidebar-content.js +150 -0
  99. package/dist/tui/slots/sidebar-content.js.map +1 -0
  100. package/package.json +101 -0
  101. package/src/active-rules-state.ts +166 -0
  102. package/src/cli/config.ts +312 -0
  103. package/src/cli/install.ts +94 -0
  104. package/src/cli/main.ts +167 -0
  105. package/src/cli/real-fs.ts +65 -0
  106. package/src/cli/status.ts +80 -0
  107. package/src/debug.ts +9 -0
  108. package/src/git-branch.ts +37 -0
  109. package/src/index.ts +68 -0
  110. package/src/mcp-tools.ts +24 -0
  111. package/src/message-context.ts +139 -0
  112. package/src/message-paths.ts +153 -0
  113. package/src/project-fingerprint.ts +72 -0
  114. package/src/rule-discovery.ts +205 -0
  115. package/src/rule-filter.ts +269 -0
  116. package/src/rule-metadata.ts +161 -0
  117. package/src/runtime-chat.ts +68 -0
  118. package/src/runtime-context.ts +136 -0
  119. package/src/runtime.ts +369 -0
  120. package/src/session-store.ts +129 -0
  121. package/src/utils.ts +36 -0
  122. package/tui/.atl/.skill-registry.cache.json +3 -0
  123. package/tui/.atl/skill-registry.md +57 -0
  124. package/tui/data/rules.ts +230 -0
  125. package/tui/index.tsx +23 -0
  126. package/tui/slots/sidebar-content.tsx +364 -0
  127. package/tui/types/opencode-plugin-tui.d.ts +78 -0
package/src/runtime.ts ADDED
@@ -0,0 +1,369 @@
1
+ import { readAndFormatRules, type RuleFilterContext } from './rule-filter.js';
2
+ import { extractFilePathsFromMessages } from './message-paths.js';
3
+ import { type DiscoveredRule } from './rule-discovery.js';
4
+ import {
5
+ extractLatestUserPrompt,
6
+ extractSessionID,
7
+ normalizeContextPath,
8
+ sanitizePathForContext,
9
+ toExtractableMessages,
10
+ type MessageWithInfo,
11
+ } from './message-context.js';
12
+ import { extractConnectedMcpCapabilityIDs } from './mcp-tools.js';
13
+ import { createDebugLog, type DebugLog } from './debug.js';
14
+ import type { SessionStore } from './session-store.js';
15
+ import {
16
+ buildFilterContext,
17
+ type BuildFilterContextOptions,
18
+ } from './runtime-context.js';
19
+ import {
20
+ handleChatMessage,
21
+ type ChatMessageInput,
22
+ type ChatMessageOutput,
23
+ } from './runtime-chat.js';
24
+ import { writeActiveRulesState } from './active-rules-state.js';
25
+
26
+ interface MessagesTransformOutput {
27
+ messages: MessageWithInfo[];
28
+ }
29
+
30
+ interface SystemTransformInput {
31
+ sessionID?: string;
32
+ }
33
+
34
+ interface SystemTransformOutput {
35
+ system?: string | string[];
36
+ }
37
+
38
+ interface OpenCodeRulesRuntimeOptions {
39
+ client: unknown;
40
+ directory: string;
41
+ projectDirectory: string;
42
+ ruleFiles: DiscoveredRule[];
43
+ sessionStore: SessionStore;
44
+ debugLog?: DebugLog;
45
+ now?: () => number;
46
+ }
47
+
48
+ export class OpenCodeRulesRuntime {
49
+ private client: unknown;
50
+ private directory: string;
51
+ private projectDirectory: string;
52
+ private ruleFiles: DiscoveredRule[];
53
+ private sessionStore: SessionStore;
54
+ private debugLog: DebugLog;
55
+ private now: () => number;
56
+
57
+ constructor(opts: OpenCodeRulesRuntimeOptions) {
58
+ this.client = opts.client;
59
+ this.directory = opts.directory;
60
+ this.projectDirectory = opts.projectDirectory;
61
+ this.ruleFiles = opts.ruleFiles;
62
+ this.sessionStore = opts.sessionStore;
63
+ this.debugLog = opts.debugLog ?? createDebugLog();
64
+ this.now = opts.now ?? (() => Date.now());
65
+ }
66
+
67
+ createHooks(): Record<string, unknown> {
68
+ return {
69
+ 'tool.execute.before': this.onToolExecuteBefore.bind(this),
70
+ 'experimental.chat.messages.transform':
71
+ this.onMessagesTransform.bind(this),
72
+ 'chat.message': this.onChatMessage.bind(this),
73
+ 'experimental.chat.system.transform': this.onSystemTransform.bind(this),
74
+ 'experimental.session.compacting': this.onSessionCompacting.bind(this),
75
+ };
76
+ }
77
+
78
+ private async onToolExecuteBefore(
79
+ input: { tool?: string; sessionID?: string; callID?: string },
80
+ output: { args?: Record<string, unknown> }
81
+ ): Promise<void> {
82
+ const sessionID = input?.sessionID;
83
+ const toolName = input?.tool;
84
+ const args = output?.args;
85
+
86
+ if (!sessionID || !toolName || !args) {
87
+ return;
88
+ }
89
+
90
+ let filePath: string | undefined;
91
+
92
+ if (['read', 'edit', 'write'].includes(toolName)) {
93
+ const arg = args.filePath;
94
+ if (typeof arg === 'string' && arg.length > 0) {
95
+ filePath = arg;
96
+ }
97
+ } else if (['glob', 'grep'].includes(toolName)) {
98
+ const arg = args.path;
99
+ if (typeof arg === 'string' && arg.length > 0) {
100
+ filePath = arg;
101
+ }
102
+ } else if (toolName === 'bash') {
103
+ const arg = args.workdir;
104
+ if (typeof arg === 'string' && arg.length > 0) {
105
+ filePath = arg;
106
+ }
107
+ }
108
+
109
+ if (filePath) {
110
+ const normalized = normalizeContextPath(filePath, this.projectDirectory);
111
+ this.sessionStore.upsert(sessionID, state => {
112
+ state.contextPaths.add(normalized);
113
+ });
114
+
115
+ this.debugLog(
116
+ `Recorded context path from tool ${toolName}: ${normalized}`
117
+ );
118
+ }
119
+ }
120
+
121
+ private async onMessagesTransform(
122
+ _input: Record<string, never>,
123
+ output: MessagesTransformOutput
124
+ ): Promise<MessagesTransformOutput> {
125
+ const sessionID = extractSessionID(output.messages);
126
+ if (!sessionID) {
127
+ this.debugLog('No sessionID found in messages');
128
+ return output;
129
+ }
130
+
131
+ const existingState = this.sessionStore.get(sessionID);
132
+ if (existingState && existingState.seededFromHistory) {
133
+ this.debugLog(`Session ${sessionID} already seeded, skipping rescan`);
134
+ return output;
135
+ }
136
+
137
+ const contextPaths = extractFilePathsFromMessages(
138
+ toExtractableMessages(output.messages)
139
+ );
140
+ const userPrompt = extractLatestUserPrompt(output.messages);
141
+
142
+ this.sessionStore.upsert(sessionID, state => {
143
+ for (const p of contextPaths) {
144
+ state.contextPaths.add(normalizeContextPath(p, this.projectDirectory));
145
+ }
146
+ if (userPrompt && !state.lastUserPrompt) {
147
+ state.lastUserPrompt = userPrompt;
148
+ }
149
+ state.seededFromHistory = true;
150
+ state.seedCount = (state.seedCount ?? 0) + 1;
151
+ });
152
+
153
+ if (contextPaths.length > 0) {
154
+ this.debugLog(
155
+ `Seeded ${contextPaths.length} context path(s) for session ${sessionID}: ${contextPaths
156
+ .slice(0, 5)
157
+ .join(', ')}${contextPaths.length > 5 ? '...' : ''}`
158
+ );
159
+ }
160
+
161
+ if (userPrompt) {
162
+ this.debugLog(
163
+ `Seeded user prompt for session ${sessionID} (len=${userPrompt.length})`
164
+ );
165
+ }
166
+
167
+ return output;
168
+ }
169
+
170
+ private async onChatMessage(
171
+ input: ChatMessageInput,
172
+ output: ChatMessageOutput
173
+ ): Promise<void> {
174
+ handleChatMessage(input, output, this.sessionStore, this.debugLog);
175
+ }
176
+
177
+ private async onSystemTransform(
178
+ hookInput: SystemTransformInput,
179
+ output: SystemTransformOutput | null
180
+ ): Promise<SystemTransformOutput> {
181
+ const sessionID = hookInput?.sessionID;
182
+ const sessionState = sessionID
183
+ ? this.sessionStore.get(sessionID)
184
+ : undefined;
185
+
186
+ if (sessionID) {
187
+ const skip = this.sessionStore.shouldSkipInjection(
188
+ sessionID,
189
+ this.now(),
190
+ 30_000
191
+ );
192
+ if (skip) {
193
+ this.debugLog(
194
+ `Session ${sessionID} is compacting - skipping rule injection`
195
+ );
196
+ return output ?? {};
197
+ }
198
+ }
199
+
200
+ if (sessionState?.rulesInjected) {
201
+ this.debugLog(
202
+ `Session ${sessionID} already has rules injected - skipping to prevent loop`
203
+ );
204
+ return output ?? {};
205
+ }
206
+
207
+ const contextPaths = sessionState
208
+ ? Array.from(sessionState.contextPaths).sort((a, b) => a.localeCompare(b))
209
+ : [];
210
+ const userPrompt = sessionState?.lastUserPrompt;
211
+
212
+ const availableToolIDs = await this.queryAvailableToolIDs();
213
+
214
+ const filterContextOpts: BuildFilterContextOptions = {
215
+ contextFilePaths: contextPaths,
216
+ userPrompt,
217
+ availableToolIDs,
218
+ modelID: sessionState?.lastModelID,
219
+ agentType: sessionState?.lastAgentType,
220
+ };
221
+
222
+ const filterContext: RuleFilterContext = await buildFilterContext(
223
+ filterContextOpts,
224
+ this.projectDirectory,
225
+ this.debugLog
226
+ );
227
+
228
+ const { formattedRules, matchedPaths } = await readAndFormatRules(
229
+ this.ruleFiles,
230
+ filterContext
231
+ );
232
+
233
+ if (sessionID) {
234
+ writeActiveRulesState(sessionID, matchedPaths);
235
+ }
236
+
237
+ if (!formattedRules) {
238
+ this.debugLog('No applicable rules for current context');
239
+ return output ?? {};
240
+ }
241
+
242
+ this.debugLog('Injecting rules into system prompt');
243
+
244
+ if (!output) {
245
+ if (sessionID) {
246
+ this.sessionStore.upsert(sessionID, state => {
247
+ state.rulesInjected = true;
248
+ state.lastInjectedAt = this.now();
249
+ });
250
+ }
251
+ return { system: formattedRules };
252
+ }
253
+
254
+ if (Array.isArray(output.system)) {
255
+ output.system.push(formattedRules);
256
+ } else {
257
+ output.system = output.system
258
+ ? `${output.system}\n\n${formattedRules}`
259
+ : formattedRules;
260
+ }
261
+
262
+ if (sessionID) {
263
+ this.sessionStore.upsert(sessionID, state => {
264
+ state.rulesInjected = true;
265
+ state.lastInjectedAt = this.now();
266
+ });
267
+ }
268
+
269
+ return output;
270
+ }
271
+
272
+ private async queryAvailableToolIDs(): Promise<string[]> {
273
+ const ids = new Set<string>();
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ const client = this.client as any;
276
+ const query = { directory: this.directory };
277
+
278
+ const [toolResult, mcpResult] = await Promise.allSettled([
279
+ client.tool?.ids?.({ query }),
280
+ client.mcp?.status?.({ query }),
281
+ ]);
282
+
283
+ if (
284
+ toolResult.status === 'fulfilled' &&
285
+ Array.isArray(toolResult.value?.data)
286
+ ) {
287
+ for (const id of toolResult.value.data) {
288
+ ids.add(id);
289
+ }
290
+ this.debugLog(
291
+ `Built-in tools: ${toolResult.value.data.slice(0, 10).join(', ')}${toolResult.value.data.length > 10 ? '...' : ''} (${toolResult.value.data.length} total)`
292
+ );
293
+ } else if (toolResult.status === 'rejected') {
294
+ const message =
295
+ toolResult.reason instanceof Error
296
+ ? toolResult.reason.message
297
+ : String(toolResult.reason);
298
+ console.warn(
299
+ `[opencode-rules-md] Warning: Failed to query tool IDs: ${message}`
300
+ );
301
+ }
302
+
303
+ if (mcpResult.status === 'fulfilled' && mcpResult.value?.data) {
304
+ const mcpIds = extractConnectedMcpCapabilityIDs(mcpResult.value.data);
305
+ for (const id of mcpIds) {
306
+ ids.add(id);
307
+ }
308
+ if (mcpIds.length > 0) {
309
+ this.debugLog(`MCP capability IDs: ${mcpIds.join(', ')}`);
310
+ }
311
+ } else if (mcpResult.status === 'rejected') {
312
+ const message =
313
+ mcpResult.reason instanceof Error
314
+ ? mcpResult.reason.message
315
+ : String(mcpResult.reason);
316
+ console.warn(
317
+ `[opencode-rules-md] Warning: Failed to query MCP status: ${message}`
318
+ );
319
+ }
320
+
321
+ return Array.from(ids);
322
+ }
323
+
324
+ private async onSessionCompacting(
325
+ input: { sessionID?: string },
326
+ output: { context?: string[] }
327
+ ): Promise<void> {
328
+ const sessionID = input?.sessionID;
329
+ if (!sessionID) {
330
+ this.debugLog('No sessionID in compacting hook input');
331
+ return;
332
+ }
333
+
334
+ const sessionState = this.sessionStore.get(sessionID);
335
+ if (!sessionState || sessionState.contextPaths.size === 0) {
336
+ this.debugLog(
337
+ `No context paths for session ${sessionID} during compaction`
338
+ );
339
+ return;
340
+ }
341
+
342
+ this.sessionStore.markCompacting(sessionID, this.now());
343
+
344
+ const sortedPaths = Array.from(sessionState.contextPaths).sort((a, b) =>
345
+ a.localeCompare(b)
346
+ );
347
+ const maxPaths = 20;
348
+ const pathsToInclude = sortedPaths.slice(0, maxPaths);
349
+
350
+ const contextString = [
351
+ 'OpenCode Rules: Working context',
352
+ 'Current file paths in context:',
353
+ ...pathsToInclude.map(p => ` - ${sanitizePathForContext(p)}`),
354
+ ...(sortedPaths.length > maxPaths
355
+ ? [` ... and ${sortedPaths.length - maxPaths} more paths`]
356
+ : []),
357
+ ].join('\n');
358
+
359
+ if (!output.context) {
360
+ output.context = [];
361
+ }
362
+
363
+ output.context.push(contextString);
364
+
365
+ this.debugLog(
366
+ `Added ${pathsToInclude.length} context path(s) to compaction for session ${sessionID}`
367
+ );
368
+ }
369
+ }
@@ -0,0 +1,129 @@
1
+ export interface SessionState {
2
+ contextPaths: Set<string>;
3
+ lastUserPrompt?: string;
4
+ lastUpdated: number;
5
+ isCompacting?: boolean;
6
+ compactingSince?: number;
7
+ seededFromHistory: boolean;
8
+ seedCount?: number;
9
+ lastModelID?: string;
10
+ lastAgentType?: string;
11
+ rulesInjected?: boolean;
12
+ lastInjectedAt?: number;
13
+ }
14
+
15
+ interface SessionStoreOptions {
16
+ max?: number;
17
+ }
18
+
19
+ export class SessionStore {
20
+ private stateMap = new Map<string, SessionState>();
21
+ private max: number;
22
+ private tick = 0;
23
+
24
+ constructor(opts: SessionStoreOptions = {}) {
25
+ this.max = opts.max ?? 100;
26
+ }
27
+
28
+ setMax(limit: number): void {
29
+ this.max = limit;
30
+ }
31
+
32
+ ids(): string[] {
33
+ return Array.from(this.stateMap.keys());
34
+ }
35
+
36
+ get(sessionID: string): SessionState | undefined {
37
+ return this.stateMap.get(sessionID);
38
+ }
39
+
40
+ snapshot(sessionID: string): SessionState | undefined {
41
+ const s = this.stateMap.get(sessionID);
42
+ if (!s) return undefined;
43
+ return {
44
+ ...s,
45
+ contextPaths: new Set(s.contextPaths),
46
+ };
47
+ }
48
+
49
+ reset(): void {
50
+ this.stateMap.clear();
51
+ this.max = 100;
52
+ this.tick = 0;
53
+ }
54
+
55
+ upsert(sessionID: string, mutator: (state: SessionState) => void): void {
56
+ let state = this.stateMap.get(sessionID);
57
+ if (!state) {
58
+ state = this.createDefaultState();
59
+ this.stateMap.set(sessionID, state);
60
+ }
61
+
62
+ mutator(state);
63
+
64
+ // Match existing semantics: overwrite lastUpdated after mutation.
65
+ state.lastUpdated = ++this.tick;
66
+
67
+ while (this.stateMap.size > this.max) {
68
+ let oldestID: string | null = null;
69
+ let oldestTime = Infinity;
70
+
71
+ for (const [id, st] of this.stateMap.entries()) {
72
+ if (st.lastUpdated < oldestTime) {
73
+ oldestTime = st.lastUpdated;
74
+ oldestID = id;
75
+ }
76
+ }
77
+
78
+ if (oldestID) {
79
+ this.stateMap.delete(oldestID);
80
+ }
81
+ }
82
+ }
83
+
84
+ markCompacting(sessionID: string, nowMs: number): void {
85
+ this.upsert(sessionID, state => {
86
+ state.isCompacting = true;
87
+ state.compactingSince = nowMs;
88
+ });
89
+ }
90
+
91
+ shouldSkipInjection(
92
+ sessionID: string,
93
+ nowMs: number,
94
+ ttlMs = 30_000
95
+ ): boolean {
96
+ const state = this.stateMap.get(sessionID);
97
+ if (!state?.isCompacting) return false;
98
+
99
+ // Preserve existing behavior: missing timestamp means "still compacting".
100
+ if (!state.compactingSince) {
101
+ return true;
102
+ }
103
+
104
+ const expired = nowMs - state.compactingSince > ttlMs;
105
+ if (!expired) {
106
+ return true;
107
+ }
108
+
109
+ this.upsert(sessionID, s => {
110
+ s.isCompacting = false;
111
+ });
112
+
113
+ return false;
114
+ }
115
+
116
+ private createDefaultState(): SessionState {
117
+ // Match existing semantics: tick increments on creation, then again on upsert.
118
+ return {
119
+ contextPaths: new Set<string>(),
120
+ lastUpdated: ++this.tick,
121
+ seededFromHistory: false,
122
+ seedCount: 0,
123
+ };
124
+ }
125
+ }
126
+
127
+ export function createSessionStore(opts?: SessionStoreOptions): SessionStore {
128
+ return new SessionStore(opts ?? { max: 100 });
129
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Utility functions for OpenCode Rules Plugin
3
+ *
4
+ * This module serves as a compatibility facade that re-exports
5
+ * from focused modules:
6
+ * - rule-discovery.ts: File discovery and caching
7
+ * - rule-metadata.ts: Frontmatter parsing
8
+ * - rule-filter.ts: Rule filtering and formatting
9
+ * - message-paths.ts: Message path extraction
10
+ */
11
+
12
+ // Re-export from rule-discovery
13
+ export {
14
+ discoverRuleFiles,
15
+ clearRuleCache,
16
+ type DiscoveredRule,
17
+ } from './rule-discovery.js';
18
+
19
+ // Re-export from rule-metadata (RuleMetadata is internal, not re-exported)
20
+ export { parseRuleMetadata } from './rule-metadata.js';
21
+
22
+ // Re-export from rule-filter
23
+ export {
24
+ promptMatchesKeywords,
25
+ toolsMatchAvailable,
26
+ readAndFormatRules,
27
+ type RuleFilterContext,
28
+ type FilterResult,
29
+ } from './rule-filter.js';
30
+
31
+ // Re-export from message-paths
32
+ export {
33
+ extractFilePathsFromMessages,
34
+ type Message,
35
+ type MessagePart,
36
+ } from './message-paths.js';
@@ -0,0 +1,3 @@
1
+ {
2
+ "fingerprint": "18d17ed2835d3b3e0683b8421e3f9f306bc8764b"
3
+ }
@@ -0,0 +1,57 @@
1
+ # Skill Registry — tui
2
+
3
+ <!-- Auto-generated by gentle-ai skill-registry refresh. Run `gentle-ai skill-registry refresh --force` to regenerate. -->
4
+
5
+ Last updated: 2026-07-08
6
+
7
+ ## Sources scanned
8
+
9
+ - /home/metalbolicx/.config/opencode/skills
10
+ - /home/metalbolicx/.claude/skills
11
+
12
+ ## Contract
13
+
14
+ **Delegator use only.** This registry is an index, not a summary. Any agent that launches subagents reads it to select relevant skills, then passes exact `SKILL.md` paths for the subagent to read before work.
15
+
16
+ `SKILL.md` remains the source of truth. Do not inject generated summaries or compact rules by default; pass paths so subagents load the full runtime contract and preserve author intent.
17
+
18
+ ## Skills
19
+
20
+ | Skill | Trigger / description | Scope | Path |
21
+ | --- | --- | --- | --- |
22
+ | `branch-pr` | Create Gentle AI pull requests with issue-first checks. Trigger: creating, opening, or preparing PRs for review. | user | `/home/metalbolicx/.config/opencode/skills/branch-pr/SKILL.md` |
23
+ | `chained-pr` | Trigger: PRs over 400 lines, stacked PRs, review slices. Split oversized changes into chained PRs that protect review focus. | user | `/home/metalbolicx/.config/opencode/skills/chained-pr/SKILL.md` |
24
+ | `cognitive-doc-design` | Design docs that reduce cognitive load. Trigger: writing guides, READMEs, RFCs, onboarding, architecture, or review-facing docs. | user | `/home/metalbolicx/.config/opencode/skills/cognitive-doc-design/SKILL.md` |
25
+ | `comment-writer` | Write warm, direct collaboration comments. Trigger: PR feedback, issue replies, reviews, Slack messages, or GitHub comments. | user | `/home/metalbolicx/.config/opencode/skills/comment-writer/SKILL.md` |
26
+ | `commit-process` | Walks from git diff to a well-crafted commit message and executes the commit. | user | `/home/metalbolicx/.claude/skills/commit-process/SKILL.md` |
27
+ | `conceptual-grill` | Interview the user relentlessly about a theoretical plan or design to forge a shared mental model from scratch without a codebase. | user | `/home/metalbolicx/.claude/skills/conceptual-grill/SKILL.md` |
28
+ | `diagnose` | Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce -> minimise -> hypothesise -> instrument -> fix -> regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression. | user | `/home/metalbolicx/.claude/skills/diagnose/SKILL.md` |
29
+ | `doc-comments` | Context-aware documentation and type-hinting tool. Analyzes code to inject language-specific documentation and strict type hints while maintaining original logic. | user | `/home/metalbolicx/.claude/skills/doc-comments/SKILL.md` |
30
+ | `docsify-docs-editor` | Architects and mutates a modular Docsify documentation suite. Enforces strict technical clarity, accurate component mapping, and deterministic diagram generation. | user | `/home/metalbolicx/.claude/skills/docsify-docs-editor/SKILL.md` |
31
+ | `go-testing` | Trigger: Go tests, go test coverage, Bubbletea teatest, golden files. Apply focused Go testing patterns. | user | `/home/metalbolicx/.config/opencode/skills/go-testing/SKILL.md` |
32
+ | `good-comments` | Refactors or adds code comments to emphasize the 'why' (intent, rationale) and 'how' (complex mechanics, edge cases) over the 'what' (syntax). | user | `/home/metalbolicx/.claude/skills/good-comments/SKILL.md` |
33
+ | `grill-me` | Relentlessly interrogates the user to stress-test architectural plans or designs by systematically navigating decision trees. | user | `/home/metalbolicx/.claude/skills/grill-me/SKILL.md` |
34
+ | `handoff` | Compacts the current conversation into a modular handoff document for a seamless transition to another agent. Avoids redundancy by linking to existing artifacts. | user | `/home/metalbolicx/.claude/skills/handoff/SKILL.md` |
35
+ | `improve` | Survey any codebase as a senior advisor and produce prioritized, self-contained plans for other agents to execute. Read-only on source code; never implements, fixes, or refactors directly. | user | `/home/metalbolicx/.claude/skills/improve/SKILL.md` |
36
+ | `issue-creation` | Create Gentle AI issues with issue-first checks. Trigger: creating GitHub issues, bug reports, or feature requests. | user | `/home/metalbolicx/.config/opencode/skills/issue-creation/SKILL.md` |
37
+ | `judgment-day` | Trigger: judgment day, dual review, adversarial review, juzgar. Run blind dual review, fix confirmed issues, then re-judge. | user | `/home/metalbolicx/.config/opencode/skills/judgment-day/SKILL.md` |
38
+ | `mermaid-diagram-generator` | Generates highly accurate, syntactically correct Mermaid diagrams based on user requirements. Supports over 23 diagram types including flowcharts, architecture diagrams, and timelines. | user | `/home/metalbolicx/.claude/skills/mermaid-diagram-generator/SKILL.md` |
39
+ | `opencode-choice` | Decide between Commands, Skills, and Agents in OpenCode. Provides a decision table and flowchart to pick the right extension type. | user | `/home/metalbolicx/.claude/skills/opencode-choice/SKILL.md` |
40
+ | `readme-refactor` | Interactively refactors a README.md into a professional, codebase-accurate document. Analyzes code behavior and applies standardized formatting rules. | user | `/home/metalbolicx/.claude/skills/readme-refactor/SKILL.md` |
41
+ | `refactor` | Surgical code refactoring to improve maintainability without changing behavior. Covers extracting functions, renaming variables, breaking down god functions, improving type safety, eliminating code smells, and applying design patterns. Less drastic than repo-rebuilder; use for gradual improvements. | user | `/home/metalbolicx/.claude/skills/refactor/SKILL.md` |
42
+ | `refactor-skill` | Refactors and modularizes raw instructions into a standardized Agent Skill. Optimizes formatting for 3-Level tiered context loading and enforces Separation of Concerns to minimize token overhead. | user | `/home/metalbolicx/.claude/skills/refactor-skill/SKILL.md` |
43
+ | `rename-refactoring` | Refactors identifiers within code blocks to improve clarity, maintainability, and domain alignment. It operates strictly on naming conventions without altering underlying logic or structural flow. | user | `/home/metalbolicx/.claude/skills/rename-refactoring/SKILL.md` |
44
+ | `review` | Review the changes since a fixed point (commit, branch, tag) along two axes: Standards and Spec. Runs both reviews in parallel sub-agents and reports them side by side. | user | `/home/metalbolicx/.claude/skills/review/SKILL.md` |
45
+ | `skill-creator` | Trigger: new skills, agent instructions, documenting AI usage patterns. Create LLM-first skills with valid frontmatter. | user | `/home/metalbolicx/.config/opencode/skills/skill-creator/SKILL.md` |
46
+ | `skill-improver` | Trigger: improve skills, audit skills, refactor skills, skill quality. Audit and upgrade existing LLM-first skills. | user | `/home/metalbolicx/.config/opencode/skills/skill-improver/SKILL.md` |
47
+ | `ubiquitous-language` | Extracts a Domain-Driven Design ubiquitous language glossary from conversations. Helps resolve ambiguities and establishes canonical domain terminology. | user | `/home/metalbolicx/.claude/skills/ubiquitous-language/SKILL.md` |
48
+ | `web-coder` | Expert 10x engineer with comprehensive knowledge of web development, internet protocols, and web standards. Use when working with HTML, CSS, JavaScript, web APIs, HTTP/HTTPS, web security, performance optimization, accessibility, or any web/internet concepts. Specializes in translating web terminology accurately and implementing modern web standards across frontend and backend development. | user | `/home/metalbolicx/.claude/skills/web-coder/SKILL.md` |
49
+ | `work-unit-commits` | Plan commits as reviewable work units. Trigger: implementation, commit splitting, chained PRs, or keeping tests and docs with code. | user | `/home/metalbolicx/.config/opencode/skills/work-unit-commits/SKILL.md` |
50
+ | `write-a-skill` | Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new agent skill. | user | `/home/metalbolicx/.claude/skills/write-a-skill/SKILL.md` |
51
+
52
+ ## Loading protocol
53
+
54
+ 1. Match task context and target files against the `Trigger / description` column.
55
+ 2. Pass only the matching `Path` values to the subagent under `## Skills to load before work`.
56
+ 3. Instruct the subagent to read those exact `SKILL.md` files before reading, writing, reviewing, testing, or creating artifacts.
57
+ 4. If no matching skill exists, proceed without project skill injection and report `skill_resolution: none`.