@bubblebrain-ai/bubble 0.0.4 → 0.0.6

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 (91) hide show
  1. package/dist/agent/budget-ledger.d.ts +20 -0
  2. package/dist/agent/budget-ledger.js +51 -0
  3. package/dist/agent/execution-governor.js +1 -1
  4. package/dist/agent/profiles.d.ts +59 -0
  5. package/dist/agent/profiles.js +460 -0
  6. package/dist/agent/subagent-control.d.ts +52 -0
  7. package/dist/agent/subagent-control.js +38 -0
  8. package/dist/agent/task-size.d.ts +9 -0
  9. package/dist/agent/task-size.js +33 -0
  10. package/dist/agent/tool-intent.d.ts +1 -0
  11. package/dist/agent/tool-intent.js +1 -1
  12. package/dist/agent.d.ts +60 -1
  13. package/dist/agent.js +648 -55
  14. package/dist/context/budget.js +1 -0
  15. package/dist/context/compact-llm.js +7 -6
  16. package/dist/context/compact.js +6 -6
  17. package/dist/context/projector.d.ts +3 -3
  18. package/dist/context/projector.js +32 -18
  19. package/dist/context/prune.d.ts +2 -2
  20. package/dist/context/prune.js +1 -4
  21. package/dist/main.js +12 -5
  22. package/dist/mcp/manager.js +1 -0
  23. package/dist/orchestrator/default-hooks.js +85 -35
  24. package/dist/orchestrator/hooks.d.ts +5 -3
  25. package/dist/prompt/compose.d.ts +1 -0
  26. package/dist/prompt/compose.js +11 -1
  27. package/dist/prompt/environment.js +23 -2
  28. package/dist/prompt/provider-prompts/deepseek.js +1 -2
  29. package/dist/prompt/provider-prompts/kimi.js +1 -2
  30. package/dist/prompt/reminders.d.ts +21 -2
  31. package/dist/prompt/reminders.js +53 -8
  32. package/dist/prompt/runtime.d.ts +1 -1
  33. package/dist/prompt/runtime.js +17 -23
  34. package/dist/provider-artifacts.d.ts +7 -0
  35. package/dist/provider-artifacts.js +60 -0
  36. package/dist/provider.d.ts +16 -8
  37. package/dist/provider.js +149 -34
  38. package/dist/session-log.js +3 -1
  39. package/dist/system-prompt.d.ts +2 -0
  40. package/dist/tools/agent-lifecycle.d.ts +6 -0
  41. package/dist/tools/agent-lifecycle.js +355 -0
  42. package/dist/tools/bash.d.ts +2 -1
  43. package/dist/tools/bash.js +3 -1
  44. package/dist/tools/edit-apply.d.ts +25 -0
  45. package/dist/tools/edit-apply.js +228 -0
  46. package/dist/tools/edit.d.ts +2 -1
  47. package/dist/tools/edit.js +75 -56
  48. package/dist/tools/exit-plan-mode.js +3 -1
  49. package/dist/tools/file-mutation-queue.d.ts +1 -0
  50. package/dist/tools/file-mutation-queue.js +32 -0
  51. package/dist/tools/file-state.d.ts +25 -0
  52. package/dist/tools/file-state.js +52 -0
  53. package/dist/tools/glob.js +1 -0
  54. package/dist/tools/grep.js +1 -0
  55. package/dist/tools/index.d.ts +3 -1
  56. package/dist/tools/index.js +9 -7
  57. package/dist/tools/lsp.js +2 -0
  58. package/dist/tools/memory.js +2 -0
  59. package/dist/tools/question.js +2 -0
  60. package/dist/tools/read.d.ts +2 -1
  61. package/dist/tools/read.js +6 -1
  62. package/dist/tools/skill.js +1 -0
  63. package/dist/tools/task.js +1 -0
  64. package/dist/tools/todo.js +1 -0
  65. package/dist/tools/tool-search.js +2 -1
  66. package/dist/tools/web-fetch.js +1 -0
  67. package/dist/tools/web-search.js +1 -0
  68. package/dist/tools/write.d.ts +4 -3
  69. package/dist/tools/write.js +135 -54
  70. package/dist/tui/display-history.d.ts +10 -1
  71. package/dist/tui/markdown-inline.d.ts +22 -0
  72. package/dist/tui/markdown-inline.js +68 -0
  73. package/dist/tui/render-signature.d.ts +1 -0
  74. package/dist/tui/render-signature.js +7 -0
  75. package/dist/tui/run.js +811 -274
  76. package/dist/tui/streaming-tool-args.d.ts +15 -0
  77. package/dist/tui/streaming-tool-args.js +30 -0
  78. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  79. package/dist/tui/tool-renderers/fallback.js +75 -0
  80. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  81. package/dist/tui/tool-renderers/registry.js +11 -0
  82. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  83. package/dist/tui/tool-renderers/subagent.js +114 -0
  84. package/dist/tui/tool-renderers/types.d.ts +36 -0
  85. package/dist/tui/tool-renderers/types.js +1 -0
  86. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  87. package/dist/tui/tool-renderers/write-preview.js +30 -0
  88. package/dist/tui/tool-renderers/write.d.ts +6 -0
  89. package/dist/tui/tool-renderers/write.js +88 -0
  90. package/dist/types.d.ts +105 -10
  91. package/package.json +1 -1
@@ -9,10 +9,15 @@ import { resolve } from "node:path";
9
9
  import { createTwoFilesPatch } from "diff";
10
10
  import { gateToolAction } from "../approval/tool-helper.js";
11
11
  import { formatDiagnosticBlocks } from "../lsp/index.js";
12
- export function createEditTool(cwd, approval, lsp) {
12
+ import { applyEditsToContent, EditApplyError, formatEditMatchNotes } from "./edit-apply.js";
13
+ import { withFileMutationQueue } from "./file-mutation-queue.js";
14
+ import { isWithinWorkspace } from "./file-state.js";
15
+ export function createEditTool(cwd, approval, lsp, fileState) {
13
16
  return {
14
17
  name: "edit",
15
- description: "Apply targeted string replacements to a file. Each oldText must match exactly once. All edits apply to the original file contents simultaneously.",
18
+ effect: "write_direct",
19
+ requiresApproval: true,
20
+ description: "Apply targeted string replacements to a file. Prefer exact oldText. The tool can tolerate line ending, trailing whitespace, Unicode punctuation/space, and blank-line differences only when the target is unique.",
16
21
  parameters: {
17
22
  type: "object",
18
23
  properties: {
@@ -34,67 +39,81 @@ export function createEditTool(cwd, approval, lsp) {
34
39
  },
35
40
  async execute(args) {
36
41
  const filePath = resolve(cwd, args.path);
37
- try {
38
- await access(filePath, constants.R_OK | constants.W_OK);
39
- }
40
- catch {
41
- return { content: `Error: Cannot read/write file: ${filePath}`, isError: true };
42
- }
43
- const original = await readFile(filePath, "utf-8");
44
- let content = original;
45
- const edits = args.edits;
46
- if (!Array.isArray(edits) || edits.length === 0) {
47
- return { content: "Error: No edits provided", isError: true };
42
+ if (!isWithinWorkspace(cwd, filePath)) {
43
+ return {
44
+ content: `Error: Edit path is outside the workspace: ${filePath}`,
45
+ isError: true,
46
+ status: "blocked",
47
+ metadata: {
48
+ kind: "security",
49
+ path: filePath,
50
+ reason: "Edit path is outside the workspace.",
51
+ },
52
+ };
48
53
  }
49
- // Validate each oldText exists exactly once
50
- for (const edit of edits) {
51
- const count = content.split(edit.oldText).length - 1;
52
- if (count === 0) {
53
- return {
54
- content: `Error: oldText not found in file: "${edit.oldText.slice(0, 50)}..."`,
55
- isError: true,
56
- };
54
+ return withFileMutationQueue(filePath, async () => {
55
+ try {
56
+ await access(filePath, constants.R_OK | constants.W_OK);
57
57
  }
58
- if (count > 1) {
59
- return {
60
- content: `Error: oldText appears ${count} times in file. Must be unique: "${edit.oldText.slice(0, 50)}..."`,
61
- isError: true,
62
- };
58
+ catch {
59
+ return { content: `Error: Cannot read/write file: ${filePath}`, isError: true };
63
60
  }
64
- }
65
- // Apply all edits in-memory to compute the proposed next content + diff.
66
- for (const edit of edits) {
67
- content = content.replace(edit.oldText, edit.newText);
68
- }
69
- const diff = createTwoFilesPatch(filePath, filePath, original, content, "original", "modified", { context: 3 });
70
- // Gate on the approval controller BEFORE persisting the change.
71
- const gate = await gateToolAction(approval, {
72
- type: "edit",
73
- path: filePath,
74
- diff,
75
- fileExists: true,
76
- });
77
- if (!gate.approved)
78
- return gate.result;
79
- await writeFile(filePath, content, "utf-8");
80
- let output = `Edited ${filePath}\n\nDiff:\n${diff}`;
81
- if (lsp) {
61
+ const original = await readFile(filePath, "utf-8");
62
+ let applied;
82
63
  try {
83
- await lsp.touchFile(filePath, "document");
84
- output += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
64
+ applied = applyEditsToContent(original, args.edits);
85
65
  }
86
- catch {
87
- // LSP diagnostics should not turn a successful edit into a failed tool call.
66
+ catch (err) {
67
+ if (err instanceof EditApplyError) {
68
+ return { content: err.message, isError: true, status: err.status };
69
+ }
70
+ throw err;
88
71
  }
89
- }
90
- return {
91
- content: output,
92
- status: "success",
93
- metadata: {
94
- kind: "edit",
72
+ const diff = createTwoFilesPatch(filePath, filePath, original, applied.content, "original", "modified", { context: 3 });
73
+ // Gate on the approval controller BEFORE persisting the change.
74
+ const gate = await gateToolAction(approval, {
75
+ type: "edit",
95
76
  path: filePath,
96
- },
97
- };
77
+ diff,
78
+ fileExists: true,
79
+ });
80
+ if (!gate.approved)
81
+ return gate.result;
82
+ const latest = await readFile(filePath, "utf-8");
83
+ if (latest !== original) {
84
+ return {
85
+ content: `Error: Cannot safely edit ${filePath} because it changed while approval was pending.\n\n`
86
+ + "Re-read the file and retry the edit against the latest content.",
87
+ isError: true,
88
+ status: "blocked",
89
+ metadata: {
90
+ kind: "security",
91
+ path: filePath,
92
+ reason: "changed",
93
+ },
94
+ };
95
+ }
96
+ await writeFile(filePath, applied.content, "utf-8");
97
+ await fileState?.observe(filePath, "edit", applied.content).catch(() => undefined);
98
+ let output = `Edited ${filePath}${formatEditMatchNotes(applied.matches)}\n\nDiff:\n${diff}`;
99
+ if (lsp) {
100
+ try {
101
+ await lsp.touchFile(filePath, "document");
102
+ output += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
103
+ }
104
+ catch {
105
+ // LSP diagnostics should not turn a successful edit into a failed tool call.
106
+ }
107
+ }
108
+ return {
109
+ content: output,
110
+ status: "success",
111
+ metadata: {
112
+ kind: "edit",
113
+ path: filePath,
114
+ },
115
+ };
116
+ });
98
117
  },
99
118
  };
100
119
  }
@@ -9,7 +9,9 @@ export function createExitPlanModeTool(controller) {
9
9
  return {
10
10
  name: "exit_plan_mode",
11
11
  readOnly: true,
12
- description: "ONLY call this tool when the harness has told you (via a <system-reminder>) that plan mode is ACTIVE. " +
12
+ effect: "read",
13
+ requiresApproval: true,
14
+ description: "ONLY call this tool when the harness has told you via a runtime reminder that plan mode is ACTIVE. " +
13
15
  "Do NOT call it during ordinary work — in default mode you should just use the regular tools directly. " +
14
16
  "In plan mode: after investigating, call this with a concrete step-by-step plan so the user can approve, edit, or reject. " +
15
17
  "Approval automatically switches the agent out of plan mode.",
@@ -0,0 +1 @@
1
+ export declare function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,32 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ const queues = new Map();
4
+ function queueKey(filePath) {
5
+ const resolved = resolve(filePath);
6
+ try {
7
+ return realpathSync.native(resolved);
8
+ }
9
+ catch {
10
+ return resolved;
11
+ }
12
+ }
13
+ export async function withFileMutationQueue(filePath, fn) {
14
+ const key = queueKey(filePath);
15
+ const current = queues.get(key) ?? Promise.resolve();
16
+ let release;
17
+ const next = new Promise((resolveNext) => {
18
+ release = resolveNext;
19
+ });
20
+ const chained = current.then(() => next);
21
+ queues.set(key, chained);
22
+ await current;
23
+ try {
24
+ return await fn();
25
+ }
26
+ finally {
27
+ release();
28
+ if (queues.get(key) === chained) {
29
+ queues.delete(key);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,25 @@
1
+ export type FileObservationSource = "read" | "write" | "edit";
2
+ export interface FileVersion {
3
+ hash: string;
4
+ mtimeMs: number;
5
+ size: number;
6
+ }
7
+ export type FileFreshnessResult = {
8
+ ok: true;
9
+ version: FileVersion;
10
+ } | {
11
+ ok: false;
12
+ reason: "unobserved" | "missing" | "changed";
13
+ observed?: FileVersion;
14
+ current?: FileVersion;
15
+ };
16
+ export declare class FileStateTracker {
17
+ private readonly cwd;
18
+ private readonly observed;
19
+ constructor(cwd: string);
20
+ observe(filePath: string, source: FileObservationSource, content?: string): Promise<FileVersion>;
21
+ checkFresh(filePath: string): Promise<FileFreshnessResult>;
22
+ private resolvePath;
23
+ private computeVersion;
24
+ }
25
+ export declare function isWithinWorkspace(cwd: string, filePath: string): boolean;
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ import { stat, readFile } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ export class FileStateTracker {
5
+ cwd;
6
+ observed = new Map();
7
+ constructor(cwd) {
8
+ this.cwd = cwd;
9
+ }
10
+ async observe(filePath, source, content) {
11
+ const absolute = this.resolvePath(filePath);
12
+ const version = await this.computeVersion(absolute, content);
13
+ this.observed.set(absolute, { ...version, source, observedAt: Date.now() });
14
+ return version;
15
+ }
16
+ async checkFresh(filePath) {
17
+ const absolute = this.resolvePath(filePath);
18
+ const observed = this.observed.get(absolute);
19
+ if (!observed) {
20
+ return { ok: false, reason: "unobserved" };
21
+ }
22
+ let current;
23
+ try {
24
+ current = await this.computeVersion(absolute);
25
+ }
26
+ catch {
27
+ return { ok: false, reason: "missing", observed };
28
+ }
29
+ if (current.hash === observed.hash && current.size === observed.size) {
30
+ return { ok: true, version: current };
31
+ }
32
+ return { ok: false, reason: "changed", observed, current };
33
+ }
34
+ resolvePath(filePath) {
35
+ return resolve(this.cwd, filePath);
36
+ }
37
+ async computeVersion(filePath, content) {
38
+ const [stats, bytes] = await Promise.all([
39
+ stat(filePath),
40
+ content === undefined ? readFile(filePath) : Promise.resolve(Buffer.from(content, "utf-8")),
41
+ ]);
42
+ return {
43
+ hash: createHash("sha256").update(bytes).digest("hex"),
44
+ mtimeMs: stats.mtimeMs,
45
+ size: stats.size,
46
+ };
47
+ }
48
+ }
49
+ export function isWithinWorkspace(cwd, filePath) {
50
+ const rel = relative(resolve(cwd), filePath);
51
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
52
+ }
@@ -20,6 +20,7 @@ export function createGlobTool(cwd) {
20
20
  return {
21
21
  name: "glob",
22
22
  readOnly: true,
23
+ effect: "read",
23
24
  description: `Find files by glob pattern without using the shell. Use this for project structure discovery and filename searches. Returns up to ${MAX_RESULTS} files sorted by recent modification time.`,
24
25
  parameters: {
25
26
  type: "object",
@@ -10,6 +10,7 @@ export function createGrepTool(cwd) {
10
10
  return {
11
11
  name: "grep",
12
12
  readOnly: true,
13
+ effect: "read",
13
14
  description: `Search file contents using regex (via ripgrep). Use this instead of running grep, rg, or ripgrep through bash. Returns up to ${MAX_MATCHES} matches.`,
14
15
  parameters: {
15
16
  type: "object",
@@ -11,7 +11,7 @@ export { createLspTool } from "./lsp.js";
11
11
  export { createWebFetchTool } from "./web-fetch.js";
12
12
  export { createWebSearchTool } from "./web-search.js";
13
13
  export { createSkillTool } from "./skill.js";
14
- export { createTaskTool } from "./task.js";
14
+ export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
15
15
  export { createTodoTool, type TodoStore } from "./todo.js";
16
16
  export { createExitPlanModeTool, type PlanController } from "./exit-plan-mode.js";
17
17
  export { createToolSearchTool, type ToolSearchController } from "./tool-search.js";
@@ -25,6 +25,7 @@ import { type LspService } from "../lsp/index.js";
25
25
  import { type TodoStore } from "./todo.js";
26
26
  import { type ToolSearchController } from "./tool-search.js";
27
27
  import type { QuestionController } from "../question/index.js";
28
+ import { FileStateTracker } from "./file-state.js";
28
29
  export interface CreateAllToolsOptions {
29
30
  todoStore?: TodoStore;
30
31
  planController?: PlanController;
@@ -32,5 +33,6 @@ export interface CreateAllToolsOptions {
32
33
  questionController?: QuestionController;
33
34
  toolSearchController?: ToolSearchController;
34
35
  lspService?: LspService;
36
+ fileStateTracker?: FileStateTracker;
35
37
  }
36
38
  export declare function createAllTools(cwd: string, skillRegistry?: SkillRegistry, options?: CreateAllToolsOptions): ToolRegistryEntry[];
@@ -11,7 +11,7 @@ export { createLspTool } from "./lsp.js";
11
11
  export { createWebFetchTool } from "./web-fetch.js";
12
12
  export { createWebSearchTool } from "./web-search.js";
13
13
  export { createSkillTool } from "./skill.js";
14
- export { createTaskTool } from "./task.js";
14
+ export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
15
15
  export { createTodoTool } from "./todo.js";
16
16
  export { createExitPlanModeTool } from "./exit-plan-mode.js";
17
17
  export { createToolSearchTool } from "./tool-search.js";
@@ -26,7 +26,7 @@ import { getLspService } from "../lsp/index.js";
26
26
  import { createLspTool } from "./lsp.js";
27
27
  import { createReadTool } from "./read.js";
28
28
  import { createSkillTool } from "./skill.js";
29
- import { createTaskTool } from "./task.js";
29
+ import { createAgentLifecycleTools } from "./agent-lifecycle.js";
30
30
  import { createTodoTool } from "./todo.js";
31
31
  import { createToolSearchTool } from "./tool-search.js";
32
32
  import { createWebFetchTool } from "./web-fetch.js";
@@ -34,14 +34,16 @@ import { createWebSearchTool } from "./web-search.js";
34
34
  import { createWriteTool } from "./write.js";
35
35
  import { createQuestionTool } from "./question.js";
36
36
  import { createMemoryReadSummaryTool, createMemorySearchTool } from "./memory.js";
37
+ import { FileStateTracker } from "./file-state.js";
37
38
  export function createAllTools(cwd, skillRegistry, options = {}) {
38
39
  const approval = options.approvalController;
39
40
  const lsp = options.lspService ?? getLspService(cwd);
41
+ const fileState = options.fileStateTracker ?? new FileStateTracker(cwd);
40
42
  return [
41
- createReadTool(cwd, approval, lsp),
42
- createBashTool(cwd, approval),
43
- createWriteTool(cwd, { refuseOverwrite: true }, approval, lsp),
44
- createEditTool(cwd, approval, lsp),
43
+ createReadTool(cwd, approval, lsp, fileState),
44
+ createBashTool(cwd, approval, fileState),
45
+ createWriteTool(cwd, { refuseOverwrite: true }, approval, lsp, fileState),
46
+ createEditTool(cwd, approval, lsp, fileState),
45
47
  createGlobTool(cwd),
46
48
  createGrepTool(cwd),
47
49
  createLspTool(cwd, lsp, approval),
@@ -49,7 +51,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
49
51
  createWebFetchTool(approval),
50
52
  createMemorySearchTool(cwd),
51
53
  createMemoryReadSummaryTool(cwd),
52
- createTaskTool(),
54
+ ...createAgentLifecycleTools(),
53
55
  ...(options.questionController ? [createQuestionTool(options.questionController)] : []),
54
56
  ...(skillRegistry ? [createSkillTool(skillRegistry)] : []),
55
57
  ...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
package/dist/tools/lsp.js CHANGED
@@ -18,6 +18,8 @@ export function createLspTool(cwd, lsp = getLspService(cwd), approval) {
18
18
  return {
19
19
  name: "lsp",
20
20
  readOnly: true,
21
+ effect: "read",
22
+ requiresApproval: true,
21
23
  description: "Use the language server for code navigation. Supports goToDefinition, findReferences, hover, documentSymbol, workspaceSymbol, goToImplementation, prepareCallHierarchy, incomingCalls, and outgoingCalls.",
22
24
  parameters: {
23
25
  type: "object",
@@ -5,6 +5,7 @@ export function createMemorySearchTool(cwd) {
5
5
  name: "memory_search",
6
6
  description: "Search persistent Bubble memory for prior project facts, user preferences, workflows, decisions, and gotchas.",
7
7
  readOnly: true,
8
+ effect: "read",
8
9
  parameters: {
9
10
  type: "object",
10
11
  properties: {
@@ -56,6 +57,7 @@ export function createMemoryReadSummaryTool(cwd) {
56
57
  name: "memory_read_summary",
57
58
  description: "Read the concise persistent memory summary for the current project, global scope, or both.",
58
59
  readOnly: true,
60
+ effect: "read",
59
61
  parameters: {
60
62
  type: "object",
61
63
  properties: {
@@ -3,6 +3,8 @@ export function createQuestionTool(controller) {
3
3
  return {
4
4
  name: "question",
5
5
  readOnly: true,
6
+ effect: "read",
7
+ requiresApproval: true,
6
8
  description: `Ask the user one or more structured questions during execution.
7
9
 
8
10
  Use this when you need to:
@@ -4,4 +4,5 @@
4
4
  import type { ApprovalController } from "../approval/types.js";
5
5
  import type { ToolRegistryEntry } from "../types.js";
6
6
  import type { LspService } from "../lsp/index.js";
7
- export declare function createReadTool(cwd: string, approval?: ApprovalController, lsp?: LspService): ToolRegistryEntry;
7
+ import type { FileStateTracker } from "./file-state.js";
8
+ export declare function createReadTool(cwd: string, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
@@ -7,10 +7,11 @@ import { resolve } from "node:path";
7
7
  import { isSensitivePath } from "./sensitive-paths.js";
8
8
  const MAX_LINES = 250;
9
9
  const MAX_BYTES = 100 * 1024;
10
- export function createReadTool(cwd, approval, lsp) {
10
+ export function createReadTool(cwd, approval, lsp, fileState) {
11
11
  return {
12
12
  name: "read",
13
13
  readOnly: true,
14
+ effect: "read",
14
15
  description: `Read the contents of a file. Output is truncated to ${MAX_LINES} lines or ${MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
15
16
  parameters: {
16
17
  type: "object",
@@ -69,6 +70,10 @@ export function createReadTool(cwd, approval, lsp) {
69
70
  if (truncated) {
70
71
  result += `\n[Output truncated: exceeded ${MAX_LINES} lines or ${MAX_BYTES / 1024}KB limit]`;
71
72
  }
73
+ const isFullRead = offset === 0 && !truncated && offset + limit >= lines.length;
74
+ if (isFullRead) {
75
+ await fileState?.observe(filePath, "read", content).catch(() => undefined);
76
+ }
72
77
  void lsp?.touchFile(filePath).catch(() => undefined);
73
78
  return {
74
79
  content: result,
@@ -21,6 +21,7 @@ export function createSkillTool(registry) {
21
21
  return {
22
22
  name: "skill",
23
23
  readOnly: true,
24
+ effect: "read",
24
25
  description: "Load a named skill on demand. Use this when a task clearly matches one of the available skills.",
25
26
  parameters: {
26
27
  type: "object",
@@ -2,6 +2,7 @@ export function createTaskTool() {
2
2
  return {
3
3
  name: "task",
4
4
  readOnly: true,
5
+ effect: "read",
5
6
  description: `Delegate a bounded investigative subtask to a read-only sub-agent and return a concise summary.
6
7
 
7
8
  Use this when:
@@ -8,6 +8,7 @@ export function createTodoTool(store) {
8
8
  return {
9
9
  name: "todo_write",
10
10
  readOnly: true,
11
+ effect: "read",
11
12
  description: `Create or update the task list for the current work. Send the COMPLETE list each call; this overwrites the prior list entirely.
12
13
 
13
14
  ## When to use
@@ -17,8 +17,9 @@ export function createToolSearchTool(controller) {
17
17
  return {
18
18
  name: "tool_search",
19
19
  readOnly: true,
20
+ effect: "read",
20
21
  description: 'Fetches full schema definitions for deferred tools so they can be called. ' +
21
- 'Deferred tools appear by name in <system-reminder> messages; their parameters are unknown ' +
22
+ 'Deferred tools appear by name in hidden runtime reminders; their parameters are unknown ' +
22
23
  'until loaded. Use this tool with query "select:<name>[,<name>...]" to load specific tools, ' +
23
24
  'or with free-text keywords to search for relevant tools.',
24
25
  parameters: {
@@ -6,6 +6,7 @@ export function createWebFetchTool(approval) {
6
6
  return {
7
7
  name: "web_fetch",
8
8
  readOnly: true,
9
+ effect: "read",
9
10
  description: "Fetch and extract the contents of a specific URL using a remote web crawling service.",
10
11
  parameters: {
11
12
  type: "object",
@@ -7,6 +7,7 @@ export function createWebSearchTool() {
7
7
  return {
8
8
  name: "web_search",
9
9
  readOnly: true,
10
+ effect: "read",
10
11
  description: "Search the web using a remote search service and return current, structured results.",
11
12
  parameters: {
12
13
  type: "object",
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Write tool - create or overwrite files.
2
+ * Write tool - create files or safely replace full file contents.
3
3
  */
4
4
  import type { ApprovalController } from "../approval/types.js";
5
5
  import type { ToolRegistryEntry } from "../types.js";
6
6
  import { type LspService } from "../lsp/index.js";
7
+ import { type FileStateTracker } from "./file-state.js";
7
8
  export interface WriteToolOptions {
8
- /** If true, refuse to overwrite existing files */
9
+ /** If true, existing files require overwrite=true plus a fresh agent-observed version. */
9
10
  refuseOverwrite?: boolean;
10
11
  }
11
- export declare function createWriteTool(cwd: string, options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService): ToolRegistryEntry;
12
+ export declare function createWriteTool(cwd: string, options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;