micode 0.6.0 → 0.7.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 (84) hide show
  1. package/README.md +64 -331
  2. package/package.json +9 -14
  3. package/src/agents/artifact-searcher.ts +46 -0
  4. package/src/agents/brainstormer.ts +145 -0
  5. package/src/agents/codebase-analyzer.ts +75 -0
  6. package/src/agents/codebase-locator.ts +71 -0
  7. package/src/agents/commander.ts +138 -0
  8. package/src/agents/executor.ts +215 -0
  9. package/src/agents/implementer.ts +99 -0
  10. package/src/agents/index.ts +44 -0
  11. package/src/agents/ledger-creator.ts +113 -0
  12. package/src/agents/pattern-finder.ts +70 -0
  13. package/src/agents/planner.ts +230 -0
  14. package/src/agents/project-initializer.ts +264 -0
  15. package/src/agents/reviewer.ts +102 -0
  16. package/src/config-loader.ts +89 -0
  17. package/src/hooks/artifact-auto-index.ts +111 -0
  18. package/src/hooks/auto-clear-ledger.ts +230 -0
  19. package/src/hooks/auto-compact.ts +241 -0
  20. package/src/hooks/comment-checker.ts +120 -0
  21. package/src/hooks/context-injector.ts +163 -0
  22. package/src/hooks/context-window-monitor.ts +106 -0
  23. package/src/hooks/file-ops-tracker.ts +96 -0
  24. package/src/hooks/ledger-loader.ts +78 -0
  25. package/src/hooks/preemptive-compaction.ts +183 -0
  26. package/src/hooks/session-recovery.ts +258 -0
  27. package/src/hooks/token-aware-truncation.ts +189 -0
  28. package/src/index.ts +258 -0
  29. package/src/tools/artifact-index/index.ts +269 -0
  30. package/src/tools/artifact-index/schema.sql +44 -0
  31. package/src/tools/artifact-search.ts +49 -0
  32. package/src/tools/ast-grep/index.ts +189 -0
  33. package/src/tools/background-task/manager.ts +374 -0
  34. package/src/tools/background-task/tools.ts +145 -0
  35. package/src/tools/background-task/types.ts +68 -0
  36. package/src/tools/btca/index.ts +82 -0
  37. package/src/tools/look-at.ts +210 -0
  38. package/src/tools/pty/buffer.ts +49 -0
  39. package/src/tools/pty/index.ts +34 -0
  40. package/src/tools/pty/manager.ts +159 -0
  41. package/src/tools/pty/tools/kill.ts +68 -0
  42. package/src/tools/pty/tools/list.ts +55 -0
  43. package/src/tools/pty/tools/read.ts +152 -0
  44. package/src/tools/pty/tools/spawn.ts +78 -0
  45. package/src/tools/pty/tools/write.ts +97 -0
  46. package/src/tools/pty/types.ts +62 -0
  47. package/src/utils/model-limits.ts +36 -0
  48. package/dist/agents/artifact-searcher.d.ts +0 -2
  49. package/dist/agents/brainstormer.d.ts +0 -2
  50. package/dist/agents/codebase-analyzer.d.ts +0 -2
  51. package/dist/agents/codebase-locator.d.ts +0 -2
  52. package/dist/agents/commander.d.ts +0 -3
  53. package/dist/agents/executor.d.ts +0 -2
  54. package/dist/agents/implementer.d.ts +0 -2
  55. package/dist/agents/index.d.ts +0 -15
  56. package/dist/agents/ledger-creator.d.ts +0 -2
  57. package/dist/agents/pattern-finder.d.ts +0 -2
  58. package/dist/agents/planner.d.ts +0 -2
  59. package/dist/agents/project-initializer.d.ts +0 -2
  60. package/dist/agents/reviewer.d.ts +0 -2
  61. package/dist/config-loader.d.ts +0 -20
  62. package/dist/hooks/artifact-auto-index.d.ts +0 -19
  63. package/dist/hooks/auto-clear-ledger.d.ts +0 -11
  64. package/dist/hooks/auto-compact.d.ts +0 -9
  65. package/dist/hooks/comment-checker.d.ts +0 -9
  66. package/dist/hooks/context-injector.d.ts +0 -15
  67. package/dist/hooks/context-window-monitor.d.ts +0 -15
  68. package/dist/hooks/file-ops-tracker.d.ts +0 -26
  69. package/dist/hooks/ledger-loader.d.ts +0 -16
  70. package/dist/hooks/preemptive-compaction.d.ts +0 -9
  71. package/dist/hooks/session-recovery.d.ts +0 -9
  72. package/dist/hooks/token-aware-truncation.d.ts +0 -15
  73. package/dist/index.d.ts +0 -3
  74. package/dist/index.js +0 -16267
  75. package/dist/tools/artifact-index/index.d.ts +0 -38
  76. package/dist/tools/artifact-search.d.ts +0 -17
  77. package/dist/tools/ast-grep/index.d.ts +0 -88
  78. package/dist/tools/background-task/manager.d.ts +0 -27
  79. package/dist/tools/background-task/tools.d.ts +0 -41
  80. package/dist/tools/background-task/types.d.ts +0 -53
  81. package/dist/tools/btca/index.d.ts +0 -19
  82. package/dist/tools/look-at.d.ts +0 -11
  83. package/dist/utils/model-limits.d.ts +0 -7
  84. /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
@@ -0,0 +1,230 @@
1
+ // src/hooks/auto-clear-ledger.ts
2
+ import type { PluginInput } from "@opencode-ai/plugin";
3
+ import { findCurrentLedger, formatLedgerInjection } from "./ledger-loader";
4
+ import { getFileOps, clearFileOps, formatFileOpsForPrompt } from "./file-ops-tracker";
5
+ import { getContextLimit } from "../utils/model-limits";
6
+
7
+ export const DEFAULT_THRESHOLD = 0.6; // 60% of context window
8
+ const MIN_TOKENS_FOR_CLEAR = 50_000;
9
+ export const CLEAR_COOLDOWN_MS = 60_000;
10
+
11
+ interface ClearState {
12
+ lastClearTime: Map<string, number>;
13
+ clearInProgress: Set<string>;
14
+ }
15
+
16
+ export function createAutoClearLedgerHook(ctx: PluginInput) {
17
+ const state: ClearState = {
18
+ lastClearTime: new Map(),
19
+ clearInProgress: new Set(),
20
+ };
21
+
22
+ async function checkAndClear(sessionID: string, _providerID?: string, modelID?: string): Promise<void> {
23
+ // Skip if clear in progress
24
+ if (state.clearInProgress.has(sessionID)) return;
25
+
26
+ // Respect cooldown
27
+ const lastTime = state.lastClearTime.get(sessionID) || 0;
28
+ if (Date.now() - lastTime < CLEAR_COOLDOWN_MS) return;
29
+
30
+ try {
31
+ // Get session messages to calculate token usage
32
+ const resp = await ctx.client.session.messages({
33
+ path: { id: sessionID },
34
+ query: { directory: ctx.directory },
35
+ });
36
+
37
+ const messages = (resp as { data?: unknown[] }).data;
38
+ if (!Array.isArray(messages) || messages.length === 0) return;
39
+
40
+ // Find last assistant message with token info
41
+ const lastAssistant = [...messages].reverse().find((m) => {
42
+ const msg = m as Record<string, unknown>;
43
+ const info = msg.info as Record<string, unknown> | undefined;
44
+ return info?.role === "assistant";
45
+ }) as Record<string, unknown> | undefined;
46
+
47
+ if (!lastAssistant) return;
48
+
49
+ const info = lastAssistant.info as Record<string, unknown> | undefined;
50
+ const usage = info?.usage as Record<string, unknown> | undefined;
51
+
52
+ // Calculate token usage
53
+ const inputTokens = (usage?.inputTokens as number) || 0;
54
+ const cacheRead = (usage?.cacheReadInputTokens as number) || 0;
55
+ const totalUsed = inputTokens + cacheRead;
56
+
57
+ if (totalUsed < MIN_TOKENS_FOR_CLEAR) return;
58
+
59
+ // Get model context limit
60
+ const model = modelID || (info?.modelID as string) || "";
61
+ const contextLimit = getContextLimit(model);
62
+ const usageRatio = totalUsed / contextLimit;
63
+
64
+ if (usageRatio < DEFAULT_THRESHOLD) return;
65
+
66
+ // Start clear process
67
+ state.clearInProgress.add(sessionID);
68
+ state.lastClearTime.set(sessionID, Date.now());
69
+
70
+ await ctx.client.tui
71
+ .showToast({
72
+ body: {
73
+ title: "Context Window",
74
+ message: `${Math.round(usageRatio * 100)}% used - saving ledger and clearing...`,
75
+ variant: "warning",
76
+ duration: 3000,
77
+ },
78
+ })
79
+ .catch(() => {});
80
+
81
+ // Step 1: Get file operations and existing ledger (don't clear yet)
82
+ const fileOps = getFileOps(sessionID);
83
+ const existingLedger = await findCurrentLedger(ctx.directory);
84
+
85
+ // Step 2: Spawn ledger-creator agent to update ledger
86
+ const ledgerSessionResp = await ctx.client.session.create({
87
+ body: {},
88
+ query: { directory: ctx.directory },
89
+ });
90
+ const ledgerSessionID = (ledgerSessionResp as { data?: { id?: string } }).data?.id;
91
+
92
+ if (ledgerSessionID) {
93
+ // Build prompt with previous ledger and file ops
94
+ let promptText = "";
95
+
96
+ if (existingLedger) {
97
+ promptText += `<previous-ledger>\n${existingLedger.content}\n</previous-ledger>\n\n`;
98
+ }
99
+
100
+ promptText += formatFileOpsForPrompt(fileOps);
101
+ promptText += "\n\n<instruction>\n";
102
+ promptText += existingLedger
103
+ ? "Update the ledger with the current session state. Merge the file operations above with any existing ones in the previous ledger."
104
+ : "Create a new continuity ledger for this session.";
105
+ promptText += "\n</instruction>";
106
+
107
+ await ctx.client.session.prompt({
108
+ path: { id: ledgerSessionID },
109
+ body: {
110
+ parts: [{ type: "text", text: promptText }],
111
+ agent: "ledger-creator",
112
+ },
113
+ query: { directory: ctx.directory },
114
+ });
115
+
116
+ // Wait for ledger completion (poll for idle)
117
+ let attempts = 0;
118
+ let ledgerCompleted = false;
119
+ while (attempts < 30) {
120
+ await new Promise((resolve) => setTimeout(resolve, 2000));
121
+ const statusResp = await ctx.client.session.get({
122
+ path: { id: ledgerSessionID },
123
+ query: { directory: ctx.directory },
124
+ });
125
+ if ((statusResp as { data?: { status?: string } }).data?.status === "idle") {
126
+ ledgerCompleted = true;
127
+ break;
128
+ }
129
+ attempts++;
130
+ }
131
+
132
+ // Only clear file ops after ledger-creator successfully completed
133
+ if (ledgerCompleted) {
134
+ clearFileOps(sessionID);
135
+ }
136
+ }
137
+
138
+ // Step 3: Get first message ID for revert
139
+ const firstMessage = messages[0] as Record<string, unknown> | undefined;
140
+ const firstMessageID = (firstMessage?.info as Record<string, unknown> | undefined)?.id as string | undefined;
141
+
142
+ if (!firstMessageID) {
143
+ throw new Error("Could not find first message ID for revert");
144
+ }
145
+
146
+ // Step 4: Revert session to first message
147
+ await ctx.client.session.revert({
148
+ path: { id: sessionID },
149
+ body: { messageID: firstMessageID },
150
+ query: { directory: ctx.directory },
151
+ });
152
+
153
+ // Step 5: Inject ledger context
154
+ const ledger = await findCurrentLedger(ctx.directory);
155
+ if (ledger) {
156
+ const injection = formatLedgerInjection(ledger);
157
+ await ctx.client.session.prompt({
158
+ path: { id: sessionID },
159
+ body: {
160
+ parts: [{ type: "text", text: injection }],
161
+ noReply: true,
162
+ },
163
+ query: { directory: ctx.directory },
164
+ });
165
+ }
166
+
167
+ await ctx.client.tui
168
+ .showToast({
169
+ body: {
170
+ title: "Context Cleared",
171
+ message: "Ledger saved. Session ready to continue.",
172
+ variant: "success",
173
+ duration: 5000,
174
+ },
175
+ })
176
+ .catch(() => {});
177
+ } catch (e) {
178
+ // Log error but don't interrupt user flow
179
+ console.error("[auto-clear-ledger] Error:", e);
180
+ await ctx.client.tui
181
+ .showToast({
182
+ body: {
183
+ title: "Clear Failed",
184
+ message: "Could not complete context clear. Continuing normally.",
185
+ variant: "error",
186
+ duration: 5000,
187
+ },
188
+ })
189
+ .catch(() => {});
190
+ } finally {
191
+ state.clearInProgress.delete(sessionID);
192
+ }
193
+ }
194
+
195
+ return {
196
+ event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
197
+ const props = event.properties as Record<string, unknown> | undefined;
198
+
199
+ // Cleanup on session delete
200
+ if (event.type === "session.deleted") {
201
+ const sessionInfo = props?.info as { id?: string } | undefined;
202
+ if (sessionInfo?.id) {
203
+ state.lastClearTime.delete(sessionInfo.id);
204
+ state.clearInProgress.delete(sessionInfo.id);
205
+ }
206
+ return;
207
+ }
208
+
209
+ // Check on message update (assistant finished)
210
+ if (event.type === "message.updated") {
211
+ const info = props?.info as Record<string, unknown> | undefined;
212
+ const sessionID = info?.sessionID as string | undefined;
213
+
214
+ if (sessionID && info?.role === "assistant") {
215
+ const providerID = info.providerID as string | undefined;
216
+ const modelID = info.modelID as string | undefined;
217
+ await checkAndClear(sessionID, providerID, modelID);
218
+ }
219
+ }
220
+
221
+ // Check when session goes idle
222
+ if (event.type === "session.idle") {
223
+ const sessionID = props?.sessionID as string | undefined;
224
+ if (sessionID) {
225
+ await checkAndClear(sessionID);
226
+ }
227
+ }
228
+ },
229
+ };
230
+ }
@@ -0,0 +1,241 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ interface TokenLimitError {
4
+ currentTokens?: number;
5
+ maxTokens?: number;
6
+ providerID?: string;
7
+ modelID?: string;
8
+ }
9
+
10
+ // Parse Anthropic token limit errors
11
+ function parseTokenLimitError(error: unknown): TokenLimitError | null {
12
+ if (!error) return null;
13
+
14
+ const errorStr = typeof error === "string" ? error : JSON.stringify(error);
15
+
16
+ // Check for Anthropic-specific token limit messages
17
+ const patterns = [
18
+ /prompt is too long.*?(\d+)\s*tokens.*?maximum.*?(\d+)/i,
19
+ /context.*?(\d+).*?exceeds.*?(\d+)/i,
20
+ /token limit.*?(\d+).*?max.*?(\d+)/i,
21
+ ];
22
+
23
+ for (const pattern of patterns) {
24
+ const match = errorStr.match(pattern);
25
+ if (match) {
26
+ return {
27
+ currentTokens: parseInt(match[1], 10),
28
+ maxTokens: parseInt(match[2], 10),
29
+ };
30
+ }
31
+ }
32
+
33
+ // Check for generic rate limit / context errors
34
+ if (
35
+ errorStr.includes("context_length_exceeded") ||
36
+ errorStr.includes("token") ||
37
+ errorStr.includes("prompt is too long")
38
+ ) {
39
+ return {};
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ interface AutoCompactState {
46
+ pendingCompact: Set<string>;
47
+ errorData: Map<string, TokenLimitError>;
48
+ retryCount: Map<string, number>;
49
+ inProgress: Set<string>;
50
+ }
51
+
52
+ export function createAutoCompactHook(ctx: PluginInput) {
53
+ const state: AutoCompactState = {
54
+ pendingCompact: new Set(),
55
+ errorData: new Map(),
56
+ retryCount: new Map(),
57
+ inProgress: new Set(),
58
+ };
59
+
60
+ const MAX_RETRIES = 3;
61
+
62
+ async function attemptRecovery(sessionID: string, providerID?: string, modelID?: string): Promise<void> {
63
+ if (state.inProgress.has(sessionID)) return;
64
+ state.inProgress.add(sessionID);
65
+
66
+ const retries = state.retryCount.get(sessionID) || 0;
67
+
68
+ if (retries >= MAX_RETRIES) {
69
+ await ctx.client.tui
70
+ .showToast({
71
+ body: {
72
+ title: "Auto Compact Failed",
73
+ message: "Max retries reached. Please start a new session or manually compact.",
74
+ variant: "error",
75
+ duration: 5000,
76
+ },
77
+ })
78
+ .catch(() => {});
79
+ state.inProgress.delete(sessionID);
80
+ state.pendingCompact.delete(sessionID);
81
+ return;
82
+ }
83
+
84
+ try {
85
+ await ctx.client.tui
86
+ .showToast({
87
+ body: {
88
+ title: "Context Limit Hit",
89
+ message: `Attempting to summarize session (attempt ${retries + 1}/${MAX_RETRIES})...`,
90
+ variant: "warning",
91
+ duration: 3000,
92
+ },
93
+ })
94
+ .catch(() => {});
95
+
96
+ // Try to summarize the session
97
+ if (providerID && modelID) {
98
+ await ctx.client.session.summarize({
99
+ path: { id: sessionID },
100
+ body: { providerID, modelID },
101
+ query: { directory: ctx.directory },
102
+ });
103
+
104
+ await ctx.client.tui
105
+ .showToast({
106
+ body: {
107
+ title: "Session Compacted",
108
+ message: "Context has been summarized. Continuing...",
109
+ variant: "success",
110
+ duration: 3000,
111
+ },
112
+ })
113
+ .catch(() => {});
114
+
115
+ // Clear state on success
116
+ state.pendingCompact.delete(sessionID);
117
+ state.errorData.delete(sessionID);
118
+ state.retryCount.delete(sessionID);
119
+
120
+ // Send continue prompt
121
+ setTimeout(async () => {
122
+ try {
123
+ await ctx.client.session.prompt({
124
+ path: { id: sessionID },
125
+ body: { parts: [{ type: "text", text: "Continue" }] },
126
+ query: { directory: ctx.directory },
127
+ });
128
+ } catch {}
129
+ }, 500);
130
+ } else {
131
+ await ctx.client.tui
132
+ .showToast({
133
+ body: {
134
+ title: "Cannot Auto-Compact",
135
+ message: "Missing model info. Please compact manually with /compact.",
136
+ variant: "error",
137
+ duration: 5000,
138
+ },
139
+ })
140
+ .catch(() => {});
141
+ }
142
+ } catch (_e) {
143
+ state.retryCount.set(sessionID, retries + 1);
144
+
145
+ // Exponential backoff
146
+ const delay = Math.min(1000 * 2 ** retries, 10000);
147
+ setTimeout(() => {
148
+ state.inProgress.delete(sessionID);
149
+ attemptRecovery(sessionID, providerID, modelID);
150
+ }, delay);
151
+ return;
152
+ }
153
+
154
+ state.inProgress.delete(sessionID);
155
+ }
156
+
157
+ return {
158
+ event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
159
+ const props = event.properties as Record<string, unknown> | undefined;
160
+
161
+ // Clean up on session delete
162
+ if (event.type === "session.deleted") {
163
+ const sessionInfo = props?.info as { id?: string } | undefined;
164
+ if (sessionInfo?.id) {
165
+ state.pendingCompact.delete(sessionInfo.id);
166
+ state.errorData.delete(sessionInfo.id);
167
+ state.retryCount.delete(sessionInfo.id);
168
+ state.inProgress.delete(sessionInfo.id);
169
+ }
170
+ return;
171
+ }
172
+
173
+ // Detect token limit errors
174
+ if (event.type === "session.error") {
175
+ const sessionID = props?.sessionID as string | undefined;
176
+ const error = props?.error;
177
+
178
+ if (!sessionID) return;
179
+
180
+ const parsed = parseTokenLimitError(error);
181
+ if (parsed) {
182
+ state.pendingCompact.add(sessionID);
183
+ state.errorData.set(sessionID, parsed);
184
+
185
+ // Get last assistant message for provider/model info
186
+ const lastAssistant = await getLastAssistantInfo(sessionID);
187
+ const providerID = parsed.providerID || lastAssistant?.providerID;
188
+ const modelID = parsed.modelID || lastAssistant?.modelID;
189
+
190
+ attemptRecovery(sessionID, providerID, modelID);
191
+ }
192
+ }
193
+
194
+ // Also check message.updated for errors
195
+ if (event.type === "message.updated") {
196
+ const info = props?.info as Record<string, unknown> | undefined;
197
+ const sessionID = info?.sessionID as string | undefined;
198
+
199
+ if (sessionID && info?.role === "assistant" && info.error) {
200
+ const parsed = parseTokenLimitError(info.error);
201
+ if (parsed) {
202
+ parsed.providerID = info.providerID as string | undefined;
203
+ parsed.modelID = info.modelID as string | undefined;
204
+
205
+ state.pendingCompact.add(sessionID);
206
+ state.errorData.set(sessionID, parsed);
207
+
208
+ attemptRecovery(sessionID, parsed.providerID, parsed.modelID);
209
+ }
210
+ }
211
+ }
212
+ },
213
+ };
214
+
215
+ async function getLastAssistantInfo(sessionID: string): Promise<{ providerID?: string; modelID?: string } | null> {
216
+ try {
217
+ const resp = await ctx.client.session.messages({
218
+ path: { id: sessionID },
219
+ query: { directory: ctx.directory },
220
+ });
221
+
222
+ const data = (resp as { data?: unknown[] }).data;
223
+ if (!Array.isArray(data)) return null;
224
+
225
+ const lastAssistant = [...data].reverse().find((m) => {
226
+ const msg = m as Record<string, unknown>;
227
+ const info = msg.info as Record<string, unknown> | undefined;
228
+ return info?.role === "assistant";
229
+ });
230
+
231
+ if (!lastAssistant) return null;
232
+ const info = (lastAssistant as { info?: Record<string, unknown> }).info;
233
+ return {
234
+ providerID: info?.providerID as string | undefined,
235
+ modelID: info?.modelID as string | undefined,
236
+ };
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,120 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+
3
+ // Patterns that indicate excessive/unnecessary comments
4
+ const EXCESSIVE_COMMENT_PATTERNS = [
5
+ // Obvious comments that explain what code does (not why)
6
+ /\/\/\s*(increment|decrement|add|subtract|set|get|return|call|create|initialize|init)\s+/i,
7
+ /\/\/\s*(the|this|a|an)\s+(following|above|below|next|previous)/i,
8
+ // Section dividers
9
+ /\/\/\s*[-=]{3,}/,
10
+ /\/\/\s*#{3,}/,
11
+ // Empty or whitespace-only comments
12
+ /\/\/\s*$/,
13
+ // "End of" comments
14
+ /\/\/\s*end\s+(of|function|class|method|if|loop|for|while)/i,
15
+ ];
16
+
17
+ // Patterns that are valid and should be ignored
18
+ const VALID_COMMENT_PATTERNS = [
19
+ // TODO/FIXME/NOTE comments
20
+ /\/\/\s*(TODO|FIXME|NOTE|HACK|XXX|BUG|WARN):/i,
21
+ // JSDoc/TSDoc
22
+ /^\s*\*|\/\*\*/,
23
+ // Directive comments (eslint, prettier, ts, etc.)
24
+ /\/\/\s*@|\/\/\s*eslint|\/\/\s*prettier|\/\/\s*ts-|\/\/\s*type:/i,
25
+ // License headers
26
+ /\/\/\s*(copyright|license|spdx)/i,
27
+ // BDD-style comments (describe, it, given, when, then)
28
+ /\/\/\s*(given|when|then|and|but|describe|it|should|expect)/i,
29
+ // URL references
30
+ /\/\/\s*https?:\/\//i,
31
+ // Regex explanations (often necessary)
32
+ /\/\/\s*regex|\/\/\s*pattern/i,
33
+ ];
34
+
35
+ interface CommentIssue {
36
+ line: number;
37
+ comment: string;
38
+ reason: string;
39
+ }
40
+
41
+ function analyzeComments(content: string): CommentIssue[] {
42
+ const issues: CommentIssue[] = [];
43
+ const lines = content.split("\n");
44
+
45
+ let consecutiveComments = 0;
46
+ let lastCommentLine = -2;
47
+
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const line = lines[i];
50
+ const trimmed = line.trim();
51
+
52
+ // Check for comment lines
53
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
54
+ // Skip valid patterns
55
+ if (VALID_COMMENT_PATTERNS.some((p) => p.test(trimmed))) {
56
+ continue;
57
+ }
58
+
59
+ // Check for excessive patterns
60
+ for (const pattern of EXCESSIVE_COMMENT_PATTERNS) {
61
+ if (pattern.test(trimmed)) {
62
+ issues.push({
63
+ line: i + 1,
64
+ comment: trimmed.slice(0, 60) + (trimmed.length > 60 ? "..." : ""),
65
+ reason: "Explains what, not why",
66
+ });
67
+ break;
68
+ }
69
+ }
70
+
71
+ // Track consecutive comments (might indicate over-documentation)
72
+ if (i === lastCommentLine + 1) {
73
+ consecutiveComments++;
74
+ if (consecutiveComments > 5) {
75
+ issues.push({
76
+ line: i + 1,
77
+ comment: trimmed.slice(0, 60),
78
+ reason: "Excessive consecutive comments",
79
+ });
80
+ }
81
+ } else {
82
+ consecutiveComments = 1;
83
+ }
84
+ lastCommentLine = i;
85
+ }
86
+ }
87
+
88
+ return issues;
89
+ }
90
+
91
+ export function createCommentCheckerHook(_ctx: PluginInput) {
92
+ return {
93
+ // Check after file edits
94
+ "tool.execute.after": async (
95
+ input: { tool: string; args?: Record<string, unknown> },
96
+ output: { output?: string },
97
+ ) => {
98
+ // Only check Edit tool
99
+ if (input.tool !== "Edit" && input.tool !== "edit") return;
100
+
101
+ const newString = input.args?.new_string as string | undefined;
102
+ if (!newString) return;
103
+
104
+ const issues = analyzeComments(newString);
105
+
106
+ if (issues.length > 0) {
107
+ const warning = `\n\n⚠️ **Comment Check**: Found ${issues.length} potentially unnecessary comment(s):\n${issues
108
+ .slice(0, 3)
109
+ .map((i) => `- Line ${i.line}: "${i.comment}" (${i.reason})`)
110
+ .join(
111
+ "\n",
112
+ )}${issues.length > 3 ? `\n...and ${issues.length - 3} more` : ""}\n\nComments should explain WHY, not WHAT. Consider removing obvious comments.`;
113
+
114
+ if (output.output) {
115
+ output.output += warning;
116
+ }
117
+ }
118
+ },
119
+ };
120
+ }