@ridit/lens 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ridit/lens",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Know Your Codebase.",
5
5
  "author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
- import { Box, Static, Text } from "ink";
2
+ import { Box, Static, Text, useStdout } from "ink";
3
3
  import Spinner from "ink-spinner";
4
- import TextInput from "ink-text-input";
4
+ import { TextArea } from "./TextArea";
5
5
  import { ACCENT, GREEN, RED } from "../../colors";
6
6
  import { DiffViewer } from "../repo/DiffViewer";
7
7
  import { StaticMessage } from "./ChatMessage";
@@ -134,25 +134,24 @@ export function InputBox({
134
134
  onSubmit: (v: string) => void;
135
135
  inputKey?: number;
136
136
  }) {
137
+ const { stdout } = useStdout();
138
+ const cols = stdout?.columns ?? 80;
139
+ const rule = "─".repeat(Math.max(1, cols));
140
+
137
141
  return (
138
- <Box
139
- marginTop={1}
140
- borderBottom
141
- borderTop
142
- borderRight={false}
143
- borderLeft={false}
144
- borderColor={"gray"}
145
- borderStyle="single"
146
- >
142
+ <Box flexDirection="column" marginTop={1}>
143
+ <Text color="gray" dimColor>{rule}</Text>
147
144
  <Box gap={1}>
148
145
  <Text color={ACCENT}>{">"}</Text>
149
- <TextInput
146
+ <TextArea
150
147
  key={inputKey}
151
148
  value={value}
152
149
  onChange={onChange}
153
150
  onSubmit={onSubmit}
151
+ placeholder="ask anything..."
154
152
  />
155
153
  </Box>
154
+ <Text color="gray" dimColor>{rule}</Text>
156
155
  </Box>
157
156
  );
158
157
  }
@@ -196,7 +195,7 @@ export function ShortcutBar({
196
195
  return (
197
196
  <Box gap={3} marginTop={0}>
198
197
  <Text color="gray" dimColor>
199
- enter send · ^v paste · ^c exit
198
+ enter send · alt+enter newline · ^w del word · ^c exit
200
199
  </Text>
201
200
  {forceApprove ? (
202
201
  <Text color={RED}>⚡⚡ force-all</Text>
@@ -4,7 +4,7 @@ import Spinner from "ink-spinner";
4
4
  import { useState } from "react";
5
5
  import path from "path";
6
6
  import os from "os";
7
- import TextInput from "ink-text-input";
7
+ import { TextArea } from "./TextArea";
8
8
  import { ACCENT } from "../../colors";
9
9
  import { ProviderPicker } from "../provider/ProviderPicker";
10
10
  import { startCloneRepo } from "../../utils/repo";
@@ -136,7 +136,7 @@ function ForceAllWarning({
136
136
  esc
137
137
  </Text>
138
138
  <Text color="gray"> to cancel: </Text>
139
- <TextInput
139
+ <TextArea
140
140
  value={input}
141
141
  onChange={setInput}
142
142
  onSubmit={(v) => onConfirm(v.trim().toLowerCase() === "yes")}
@@ -0,0 +1,176 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Text, useInput } from "ink";
3
+ import chalk from "chalk";
4
+
5
+ function isWordChar(ch: string): boolean {
6
+ return /[\w]/.test(ch);
7
+ }
8
+
9
+ function wordBoundaryLeft(text: string, pos: number): number {
10
+ if (pos <= 0) return 0;
11
+ let i = pos - 1;
12
+
13
+ while (i > 0 && !isWordChar(text[i]!)) i--;
14
+
15
+ while (i > 0 && isWordChar(text[i - 1]!)) i--;
16
+ return i;
17
+ }
18
+
19
+ function wordBoundaryRight(text: string, pos: number): number {
20
+ const len = text.length;
21
+ if (pos >= len) return len;
22
+ let i = pos;
23
+
24
+ while (i < len && isWordChar(text[i]!)) i++;
25
+
26
+ while (i < len && !isWordChar(text[i]!)) i++;
27
+ return i;
28
+ }
29
+
30
+ export interface TextAreaProps {
31
+ value: string;
32
+ onChange: (value: string) => void;
33
+ onSubmit: (value: string) => void;
34
+ focus?: boolean;
35
+ placeholder?: string;
36
+ }
37
+
38
+ export function TextArea({
39
+ value,
40
+ onChange,
41
+ onSubmit,
42
+ focus = true,
43
+ placeholder = "",
44
+ }: TextAreaProps) {
45
+ const [cursor, setCursor] = useState(value.length);
46
+ const [prevValue, setPrevValue] = useState(value);
47
+
48
+ if (value !== prevValue) {
49
+ setPrevValue(value);
50
+ const lenDiff = Math.abs(value.length - prevValue.length);
51
+ if (cursor > value.length || lenDiff > 1) {
52
+ setCursor(value.length);
53
+ }
54
+ }
55
+
56
+ useInput(
57
+ (input, key) => {
58
+ if (key.upArrow || key.downArrow) return;
59
+ if (key.tab || (key.shift && key.tab)) return;
60
+ if (key.ctrl && input === "c") return;
61
+
62
+ if (key.return && !key.meta) {
63
+ onSubmit(value);
64
+ return;
65
+ }
66
+
67
+ if ((key.return && key.meta) || (key.ctrl && input === "j")) {
68
+ const next = value.slice(0, cursor) + "\n" + value.slice(cursor);
69
+ onChange(next);
70
+ setCursor((c) => c + 1);
71
+ return;
72
+ }
73
+
74
+ if (key.leftArrow && key.ctrl) {
75
+ setCursor(wordBoundaryLeft(value, cursor));
76
+ return;
77
+ }
78
+
79
+ if (key.rightArrow && key.ctrl) {
80
+ setCursor(wordBoundaryRight(value, cursor));
81
+ return;
82
+ }
83
+
84
+ if (key.leftArrow) {
85
+ setCursor((c) => Math.max(0, c - 1));
86
+ return;
87
+ }
88
+
89
+ if (key.rightArrow) {
90
+ setCursor((c) => Math.min(value.length, c + 1));
91
+ return;
92
+ }
93
+
94
+ if (key.ctrl && input === "a") {
95
+ const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
96
+ setCursor(lineStart);
97
+ return;
98
+ }
99
+
100
+ if (key.ctrl && input === "e") {
101
+ const lineEnd = value.indexOf("\n", cursor);
102
+ setCursor(lineEnd === -1 ? value.length : lineEnd);
103
+ return;
104
+ }
105
+
106
+ if (key.ctrl && input === "u") {
107
+ const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
108
+ onChange(value.slice(0, lineStart) + value.slice(cursor));
109
+ setCursor(lineStart);
110
+ return;
111
+ }
112
+
113
+ if (key.ctrl && input === "k") {
114
+ const lineEnd = value.indexOf("\n", cursor);
115
+ onChange(
116
+ value.slice(0, cursor) + (lineEnd === -1 ? "" : value.slice(lineEnd)),
117
+ );
118
+ return;
119
+ }
120
+
121
+ if (key.ctrl && input === "w") {
122
+ const to = wordBoundaryLeft(value, cursor);
123
+ onChange(value.slice(0, to) + value.slice(cursor));
124
+ setCursor(to);
125
+ return;
126
+ }
127
+
128
+ if (key.ctrl && key.delete) {
129
+ const to = wordBoundaryRight(value, cursor);
130
+ onChange(value.slice(0, cursor) + value.slice(to));
131
+ return;
132
+ }
133
+
134
+ if (key.backspace || key.delete) {
135
+ if (cursor > 0) {
136
+ onChange(value.slice(0, cursor - 1) + value.slice(cursor));
137
+ setCursor((c) => c - 1);
138
+ }
139
+ return;
140
+ }
141
+
142
+ if (key.escape) return;
143
+
144
+ if (input) {
145
+ const next = value.slice(0, cursor) + input + value.slice(cursor);
146
+ onChange(next);
147
+ setCursor((c) => c + input.length);
148
+ }
149
+ },
150
+ { isActive: focus },
151
+ );
152
+
153
+ if (value.length === 0 && placeholder) {
154
+ return (
155
+ <Text>
156
+ {chalk.inverse(placeholder[0] ?? " ")}
157
+ {placeholder.length > 1 ? chalk.gray(placeholder.slice(1)) : ""}
158
+ </Text>
159
+ );
160
+ }
161
+
162
+ let rendered = "";
163
+ for (let i = 0; i < value.length; i++) {
164
+ const ch = value[i]!;
165
+ if (i === cursor) {
166
+ rendered += ch === "\n" ? chalk.inverse(" ") + "\n" : chalk.inverse(ch);
167
+ } else {
168
+ rendered += ch;
169
+ }
170
+ }
171
+ if (cursor === value.length) {
172
+ rendered += chalk.inverse(" ");
173
+ }
174
+
175
+ return <Text>{rendered}</Text>;
176
+ }
@@ -13,6 +13,8 @@ import {
13
13
  buildMemorySummary,
14
14
  addMemory,
15
15
  deleteMemory,
16
+ getSessionToolSummary,
17
+ logToolCall,
16
18
  } from "../../../utils/memory";
17
19
  import { fetchFileTree, readImportantFiles } from "../../../utils/files";
18
20
  import { readLensFile } from "../../../utils/lensfile";
@@ -26,6 +28,7 @@ import {
26
28
  buildSystemPrompt,
27
29
  parseResponse,
28
30
  callChat,
31
+ type ChatResult,
29
32
  } from "../../../utils/chat";
30
33
 
31
34
  export function useChat(repoPath: string) {
@@ -90,32 +93,26 @@ export function useChat(repoPath: string) {
90
93
  setStage({ type: "idle" });
91
94
  };
92
95
 
93
- const TOOL_TAG_NAMES = [
94
- "shell",
95
- "fetch",
96
- "read-file",
97
- "read-folder",
98
- "grep",
99
- "write-file",
100
- "delete-file",
101
- "delete-folder",
102
- "open-url",
103
- "generate-pdf",
104
- "search",
105
- "clone",
106
- "changes",
107
- ];
96
+ const MAX_AUTO_CONTINUES = 3;
108
97
 
109
98
  function isLikelyTruncated(text: string): boolean {
110
- return TOOL_TAG_NAMES.some(
111
- (tag) => text.includes(`<${tag}>`) && !text.includes(`</${tag}>`),
112
- );
99
+ // Check unclosed XML tool tags (dynamic — includes addon tools)
100
+ for (const tag of registry.names()) {
101
+ if (text.includes(`<${tag}>`) && !text.includes(`</${tag}>`))
102
+ return true;
103
+ }
104
+ // Check unclosed fenced code blocks (```tool\n... without closing ```)
105
+ const fences = text.match(/```/g);
106
+ if (fences && fences.length % 2 !== 0) return true;
107
+ return false;
113
108
  }
114
109
 
115
110
  const processResponse = (
116
111
  raw: string,
117
112
  currentAll: Message[],
118
113
  signal: AbortSignal,
114
+ truncated = false,
115
+ continueCount = 0,
119
116
  ) => {
120
117
  if (signal.aborted) {
121
118
  batchApprovedRef.current = false;
@@ -123,16 +120,73 @@ export function useChat(repoPath: string) {
123
120
  return;
124
121
  }
125
122
 
126
- if (isLikelyTruncated(raw)) {
127
- const truncMsg: Message = {
123
+ if (truncated || isLikelyTruncated(raw)) {
124
+ if (continueCount >= MAX_AUTO_CONTINUES) {
125
+ // Give up after max attempts — show whatever we have
126
+ batchApprovedRef.current = false;
127
+ const msg: Message = {
128
+ role: "assistant",
129
+ content:
130
+ raw.trim() ||
131
+ "(response was empty after multiple continuation attempts)",
132
+ type: "text",
133
+ };
134
+ setAllMessages([...currentAll, msg]);
135
+ setCommitted((prev) => [...prev, msg]);
136
+ setStage({ type: "idle" });
137
+ return;
138
+ }
139
+
140
+ // Include the partial response so the model knows where it left off
141
+ const partialMsg: Message = {
128
142
  role: "assistant",
143
+ content: raw,
144
+ type: "text",
145
+ };
146
+ const nudgeMsg: Message = {
147
+ role: "user",
129
148
  content:
130
- "(response cut off — the model hit its output limit mid-tool-call. Try asking it to continue, or simplify the request.)",
149
+ "Your response was cut off. Please continue exactly from where you left off.",
150
+ type: "text",
151
+ };
152
+ const withContext = [...currentAll, partialMsg, nudgeMsg];
153
+
154
+ const truncMsg: Message = {
155
+ role: "assistant",
156
+ content: `(response cut off — auto-continuing ${continueCount + 1}/${MAX_AUTO_CONTINUES}…)`,
131
157
  type: "text",
132
158
  };
133
159
  setAllMessages([...currentAll, truncMsg]);
134
160
  setCommitted((prev) => [...prev, truncMsg]);
135
- setStage({ type: "idle" });
161
+
162
+ const currentProvider = providerRef.current;
163
+ const currentSystemPrompt = systemPromptRef.current;
164
+
165
+ if (!currentProvider) {
166
+ setStage({ type: "idle" });
167
+ return;
168
+ }
169
+
170
+ const nextAbort = new AbortController();
171
+ abortControllerRef.current = nextAbort;
172
+ setStage({ type: "thinking" });
173
+ callChat(
174
+ currentProvider,
175
+ currentSystemPrompt,
176
+ withContext,
177
+ nextAbort.signal,
178
+ )
179
+ .then((result: ChatResult) => {
180
+ if (nextAbort.signal.aborted) return;
181
+ processResponse(
182
+ result.text ?? "",
183
+ withContext,
184
+ nextAbort.signal,
185
+ result.truncated,
186
+ continueCount + 1,
187
+ );
188
+ })
189
+ .catch(handleError(withContext));
136
190
  return;
137
191
  }
138
192
 
@@ -313,13 +367,14 @@ export function useChat(repoPath: string) {
313
367
  }
314
368
 
315
369
  if (approved && !result.startsWith("Error:")) {
316
- appendMemory({
317
- kind: "shell-run",
318
- detail: tool.summariseInput
370
+ logToolCall(
371
+ parsed.toolName,
372
+ tool.summariseInput
319
373
  ? String(tool.summariseInput(parsed.input))
320
374
  : parsed.rawInput,
321
- summary: result.split("\n")[0]?.slice(0, 120) ?? "",
322
- });
375
+ result,
376
+ repoPath,
377
+ );
323
378
  }
324
379
 
325
380
  const displayContent = tool.summariseInput
@@ -340,7 +395,7 @@ export function useChat(repoPath: string) {
340
395
  setCommitted((prev) => [...prev, toolMsg]);
341
396
 
342
397
  if (approved && remainder && remainder.length > 0) {
343
- processResponse(remainder, withTool, signal);
398
+ processResponse(remainder, withTool, signal, truncated, continueCount);
344
399
  return;
345
400
  }
346
401
 
@@ -350,31 +405,55 @@ export function useChat(repoPath: string) {
350
405
  abortControllerRef.current = nextAbort;
351
406
  setStage({ type: "thinking" });
352
407
 
353
- callChat(currentProvider, currentSystemPrompt, withTool, nextAbort.signal)
354
- .then((r: string) => {
355
- if (nextAbort.signal.aborted) return;
356
- if (!r.trim()) {
357
- const nudged: Message[] = [
358
- ...withTool,
359
- { role: "user", content: "Please continue.", type: "text" },
360
- ];
361
- return callChat(
362
- currentProvider,
363
- currentSystemPrompt,
364
- nudged,
365
- nextAbort.signal,
366
- );
367
- }
368
- return r;
369
- })
370
- .then((r: string | undefined) => {
408
+ const callWithAutoContinue = async (
409
+ messages: Message[],
410
+ maxRetries = 3,
411
+ ): Promise<ChatResult> => {
412
+ let currentMessages = messages;
413
+ for (let i = 0; i < maxRetries; i++) {
414
+ if (nextAbort.signal.aborted)
415
+ return { text: "", truncated: false };
416
+ const result = await callChat(
417
+ currentProvider,
418
+ currentSystemPrompt,
419
+ currentMessages,
420
+ nextAbort.signal,
421
+ );
422
+ if (result.text.trim()) return result;
423
+ const nudgeMsg: Message = {
424
+ role: "assistant",
425
+ content: `(model stalled auto-continuing, attempt ${i + 1}/${maxRetries})`,
426
+ type: "text",
427
+ };
428
+ setCommitted((prev) => [...prev, nudgeMsg]);
429
+ setAllMessages((prev) => [...prev, nudgeMsg]);
430
+ currentMessages = [
431
+ ...currentMessages,
432
+ {
433
+ role: "user",
434
+ content:
435
+ "Please continue. Provide your response to the previous tool output.",
436
+ type: "text",
437
+ },
438
+ ];
439
+ }
440
+ return { text: "", truncated: false };
441
+ };
442
+
443
+ callWithAutoContinue(withTool)
444
+ .then((result: ChatResult) => {
371
445
  if (nextAbort.signal.aborted) return;
372
- processResponse(r ?? "", withTool, nextAbort.signal);
446
+ processResponse(
447
+ result.text ?? "",
448
+ withTool,
449
+ nextAbort.signal,
450
+ result.truncated,
451
+ );
373
452
  })
374
453
  .catch(handleError(withTool));
375
454
  };
376
455
 
377
- if (forceApprove || (autoApprove && isSafe) || batchApprovedRef.current) {
456
+ if (forceApprove || isSafe || batchApprovedRef.current) {
378
457
  executeAndContinue(true);
379
458
  return;
380
459
  }
@@ -424,15 +503,25 @@ export function useChat(repoPath: string) {
424
503
 
425
504
  const intent = classifyIntent(text);
426
505
  const scopedToolsSection = registry.buildSystemPromptSection(intent);
506
+ const sessionSummary = getSessionToolSummary(repoPath);
427
507
 
428
- const scopedSystemPrompt = currentSystemPrompt.replace(
508
+ let scopedSystemPrompt = currentSystemPrompt.replace(
429
509
  /## TOOLS[\s\S]*?(?=\n## (?!TOOLS))/,
430
510
  scopedToolsSection + "\n\n",
431
511
  );
432
512
 
513
+ if (sessionSummary) {
514
+ scopedSystemPrompt = scopedSystemPrompt.replace(
515
+ /## CODEBASE/,
516
+ sessionSummary + "\n\n## CODEBASE",
517
+ );
518
+ }
519
+
433
520
  setStage({ type: "thinking" });
434
521
  callChat(currentProvider, scopedSystemPrompt, nextAll, abort.signal)
435
- .then((raw: string) => processResponse(raw, nextAll, abort.signal))
522
+ .then((result: ChatResult) =>
523
+ processResponse(result.text, nextAll, abort.signal, result.truncated),
524
+ )
436
525
  .catch(handleError(nextAll));
437
526
  };
438
527
 
@@ -477,22 +566,24 @@ export function useChat(repoPath: string) {
477
566
  const applyPatchesAndContinue = (patches: any[]) => {
478
567
  try {
479
568
  applyPatches(repoPath, patches);
480
- appendMemory({
481
- kind: "code-applied",
482
- detail: patches.map((p) => p.path).join(", "),
483
- summary: `Applied changes to ${patches.length} file(s)`,
484
- });
569
+ logToolCall(
570
+ "changes",
571
+ patches.map((p) => p.path).join(", "),
572
+ `Applied changes to ${patches.length} file(s)`,
573
+ repoPath,
574
+ );
485
575
  } catch {
486
576
  /* non-fatal */
487
577
  }
488
578
  };
489
579
 
490
580
  const skipPatches = (patches: any[]) => {
491
- appendMemory({
492
- kind: "code-skipped",
493
- detail: patches.map((p: { path: string }) => p.path).join(", "),
494
- summary: `Skipped changes to ${patches.length} file(s)`,
495
- });
581
+ logToolCall(
582
+ "changes-skipped",
583
+ patches.map((p: { path: string }) => p.path).join(", "),
584
+ `Skipped changes to ${patches.length} file(s)`,
585
+ repoPath,
586
+ );
496
587
  };
497
588
 
498
589
  return {
@@ -174,11 +174,11 @@ function CodebaseQA({
174
174
  abortRef.current = abort;
175
175
 
176
176
  callChat(provider, systemPrompt, nextAll, abort.signal)
177
- .then((answer) => {
177
+ .then((result) => {
178
178
  const assistantMsg: Message = {
179
179
  role: "assistant",
180
180
  type: "text",
181
- content: answer,
181
+ content: result.text,
182
182
  };
183
183
  setCommitted((prev) => [...prev, assistantMsg]);
184
184
  setAllMessages([...nextAll, assistantMsg]);
@@ -777,9 +777,9 @@ ${summarizeTimeline(commits)}`;
777
777
 
778
778
  const runChat = async (history: Message[], signal: AbortSignal) => {
779
779
  try {
780
- const raw = await callChat(provider, systemPrompt, history, signal);
780
+ const result = await callChat(provider, systemPrompt, history, signal);
781
781
  if (signal.aborted) return;
782
- processResponse(raw, history, signal);
782
+ processResponse(result.text, history, signal);
783
783
  } catch (e: any) {
784
784
  if (e?.name === "AbortError") return;
785
785
  setMessages((prev) => [
@@ -602,12 +602,13 @@ ${lensFile.suggestions.length > 0 ? `\nProject suggestions:\n${lensFile.suggesti
602
602
 
603
603
  let raw: string;
604
604
  try {
605
- raw = await callChat(
605
+ const result = await callChat(
606
606
  provider,
607
607
  systemPromptRef.current,
608
608
  messages,
609
609
  combinedSignal,
610
610
  );
611
+ raw = result.text;
611
612
  } finally {
612
613
  clearTimeout(timeoutId);
613
614
  }
@@ -231,4 +231,22 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
231
231
  content:
232
232
  "Done — addons/hello-world.js created using defineTool from @ridit/lens-sdk.",
233
233
  },
234
+ {
235
+ role: "user",
236
+ content: "I ran the app and got this error:\n[ERROR] slice(None, 2, None)",
237
+ },
238
+ {
239
+ role: "assistant",
240
+ content: "<read-file>webfetch/parser.py</read-file>",
241
+ },
242
+ {
243
+ role: "user",
244
+ content:
245
+ "Here is the output from read-file of webfetch/parser.py:\n\n# file content here\n\nPlease continue your response based on this output.",
246
+ },
247
+ {
248
+ role: "assistant",
249
+ content:
250
+ '<write-file>\n{"path": "webfetch/parser.py", "content": "...complete fixed content..."}\n</write-file>',
251
+ },
234
252
  ];
@@ -21,13 +21,25 @@ ${tools}
21
21
  You can save and delete memories at any time by emitting these tags alongside your normal response.
22
22
  They are stripped before display — the user will not see the raw tags.
23
23
 
24
- ### memory-add — save something important to long-term memory for this repo
24
+ ### memory-add — save something important to long-term memory
25
25
  <memory-add>User prefers TypeScript strict mode in all new files</memory-add>
26
26
 
27
+ Use [global] prefix for things that apply across ALL repos (user preferences, name, coding style):
28
+ <memory-add>[global] User prefers bun over npm for all projects</memory-add>
29
+
30
+ Omit [global] for repo-specific memories (architecture decisions, patterns, agreed conventions):
31
+ <memory-add>This repo uses path aliases defined in tsconfig.json</memory-add>
32
+
27
33
  ### memory-delete — delete a memory by its ID (shown in brackets like [abc123])
28
34
  <memory-delete>abc123</memory-delete>
29
35
 
30
- Use memory-add when the user asks you to remember something, or when you learn something project-specific that would be useful in future sessions.
36
+ Use memory-add ONLY for information that cannot be inferred by reading the codebase:
37
+ - User preferences and coding conventions
38
+ - Decisions made during the session (e.g. "user chose bun over npm")
39
+ - Things the user explicitly asked you to remember
40
+ - Cross-session context that would otherwise be lost
41
+
42
+ NEVER save memories that just describe what files exist or what the project does — that can be read directly from the codebase.
31
43
  Use memory-delete when the user asks you to forget something or a memory is outdated.
32
44
 
33
45
  ## RULES
@@ -49,6 +61,8 @@ Use memory-delete when the user asks you to forget something or a memory is outd
49
61
  15. When explaining how to use a tool in text, use [tag] bracket notation — NEVER emit a real XML tool tag as part of an explanation.
50
62
  16. NEVER use markdown formatting in plain text responses — no bold, no headings, no bullet points. Only use fenced code blocks when showing actual code.
51
63
  17. When scaffolding multiple files, emit ONE write-file tag per response and wait for the result before writing the next file.
64
+ 18. When you identify a bug or error, ALWAYS write the fix immediately using write-file or changes. Never describe the fix without writing it.
65
+ 19. NEVER use shell for filesystem inspection or searching — always use grep, read-file, or read-folder instead.
52
66
 
53
67
  ## ADDON FORMAT
54
68