@arnilo/prism-coding-agent 0.0.96 → 0.1.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 (81) hide show
  1. package/CHANGELOG.md +139 -3
  2. package/README.md +48 -19
  3. package/dist/ask-user-decision.d.ts +160 -0
  4. package/dist/ask-user-decision.js +495 -0
  5. package/dist/atomic-write.d.ts +3 -0
  6. package/dist/atomic-write.js +24 -0
  7. package/dist/checks.js +5 -0
  8. package/dist/coding-checkpoint.js +6 -15
  9. package/dist/delete.d.ts +29 -0
  10. package/dist/delete.js +119 -0
  11. package/dist/edit-diff.js +1 -4
  12. package/dist/edit.d.ts +5 -1
  13. package/dist/edit.js +20 -9
  14. package/dist/effects.d.ts +33 -0
  15. package/dist/effects.js +89 -0
  16. package/dist/execution-policy.d.ts +8 -3
  17. package/dist/execution-policy.js +5 -2
  18. package/dist/file-mutation-queue.js +1 -2
  19. package/dist/forge/github.d.ts +2 -0
  20. package/dist/forge/github.js +554 -0
  21. package/dist/forge/index.d.ts +3 -0
  22. package/dist/forge/index.js +3 -0
  23. package/dist/forge/types.d.ts +150 -0
  24. package/dist/forge/types.js +19 -0
  25. package/dist/git-aware-repository.d.ts +25 -0
  26. package/dist/git-aware-repository.js +268 -0
  27. package/dist/git-exec.js +1 -1
  28. package/dist/git-tools.d.ts +4 -1
  29. package/dist/git-tools.js +15 -7
  30. package/dist/git.d.ts +3 -3
  31. package/dist/git.js +14 -14
  32. package/dist/glob-match.d.ts +6 -0
  33. package/dist/glob-match.js +81 -0
  34. package/dist/glob.d.ts +14 -0
  35. package/dist/glob.js +147 -0
  36. package/dist/goal-verify.d.ts +66 -0
  37. package/dist/goal-verify.js +280 -0
  38. package/dist/index.d.ts +63 -30
  39. package/dist/index.js +40 -16
  40. package/dist/language/client.d.ts +44 -0
  41. package/dist/language/client.js +290 -0
  42. package/dist/language/framing.d.ts +23 -0
  43. package/dist/language/framing.js +112 -0
  44. package/dist/language/index.d.ts +4 -0
  45. package/dist/language/index.js +4 -0
  46. package/dist/language/intelligence.d.ts +10 -0
  47. package/dist/language/intelligence.js +526 -0
  48. package/dist/language/types.d.ts +106 -0
  49. package/dist/language/types.js +21 -0
  50. package/dist/lifecycle.d.ts +75 -0
  51. package/dist/lifecycle.js +102 -0
  52. package/dist/limits.d.ts +41 -0
  53. package/dist/limits.js +41 -0
  54. package/dist/list.js +6 -10
  55. package/dist/move.d.ts +24 -0
  56. package/dist/move.js +150 -0
  57. package/dist/mutation-path.d.ts +7 -0
  58. package/dist/mutation-path.js +51 -0
  59. package/dist/output-accumulator.d.ts +8 -0
  60. package/dist/output-accumulator.js +45 -1
  61. package/dist/path-utils.js +1 -1
  62. package/dist/process/index.d.ts +3 -0
  63. package/dist/process/index.js +3 -0
  64. package/dist/process/sessions.d.ts +2 -0
  65. package/dist/process/sessions.js +592 -0
  66. package/dist/process/types.d.ts +146 -0
  67. package/dist/process/types.js +19 -0
  68. package/dist/read-path-set.d.ts +14 -0
  69. package/dist/read-path-set.js +26 -0
  70. package/dist/read.d.ts +3 -0
  71. package/dist/read.js +11 -17
  72. package/dist/repository.d.ts +54 -3
  73. package/dist/repository.js +144 -38
  74. package/dist/search.d.ts +1 -1
  75. package/dist/search.js +91 -27
  76. package/dist/shell.d.ts +3 -0
  77. package/dist/shell.js +23 -8
  78. package/dist/truncate.js +1 -1
  79. package/dist/write.d.ts +5 -1
  80. package/dist/write.js +19 -6
  81. package/package.json +6 -4
package/dist/search.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { CODING_OBSERVATION_EFFECT } from "./effects.js";
1
2
  import { enforceExecutionPolicy } from "./execution-policy.js";
2
- import { HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_MATCHES, validateCodingLimit, validateCodingLimitAllowZero, } from "./limits.js";
3
- import { createLocalRepositoryOperations, resolveRepositoryLimits, RepositoryError, } from "./repository.js";
3
+ import { HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_MATCHES, validateCodingLimit, validateCodingLimitAllowZero } from "./limits.js";
4
+ import { createLocalRepositoryOperations, RepositoryError, resolveRepositoryLimits, } from "./repository.js";
4
5
  import { truncateLine } from "./truncate.js";
5
6
  function errorResult(toolCallId, message) {
6
7
  return {
@@ -26,15 +27,71 @@ function formatMatch(match) {
26
27
  }
27
28
  function formatSearchText(result) {
28
29
  if (result.matches.length === 0) {
29
- return result.truncated
30
- ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]`
31
- : "(no matches)";
30
+ return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "(no matches)";
32
31
  }
33
32
  const body = result.matches.map(formatMatch).join("\n");
34
33
  if (!result.truncated)
35
34
  return body;
36
35
  return `${body}\n[truncated by ${result.truncatedBy ?? "limit"}]`;
37
36
  }
37
+ function uniqueMatchPaths(matches) {
38
+ return [...new Set(matches.map((m) => m.path))].sort();
39
+ }
40
+ function formatFilesWithMatches(result) {
41
+ if (result.matches.length === 0) {
42
+ return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "(no matches)";
43
+ }
44
+ const body = uniqueMatchPaths(result.matches).join("\n");
45
+ if (!result.truncated)
46
+ return body;
47
+ return `${body}\n[truncated by ${result.truncatedBy ?? "limit"}]`;
48
+ }
49
+ function formatCount(result) {
50
+ const matchCount = result.matches.length;
51
+ const fileCount = uniqueMatchPaths(result.matches).length;
52
+ if (matchCount === 0) {
53
+ return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "0 matches in 0 files";
54
+ }
55
+ const noun = fileCount === 1 ? "file" : "files";
56
+ const body = `${matchCount} matches in ${fileCount} ${noun}`;
57
+ if (!result.truncated)
58
+ return body;
59
+ return `${body}\n[truncated by ${result.truncatedBy ?? "limit"}]`;
60
+ }
61
+ function formatSearchResult(result, outputMode) {
62
+ switch (outputMode) {
63
+ case "files_with_matches":
64
+ return formatFilesWithMatches(result);
65
+ case "count":
66
+ return formatCount(result);
67
+ default:
68
+ return formatSearchText(result);
69
+ }
70
+ }
71
+ function parseOutputMode(value) {
72
+ if (value === undefined || value === "content")
73
+ return "content";
74
+ if (value === "files_with_matches" || value === "count")
75
+ return value;
76
+ throw new Error(`unsupported outputMode: ${String(value)}`);
77
+ }
78
+ function buildSearchMetadata(result, outputMode) {
79
+ const base = {
80
+ outputMode,
81
+ truncated: result.truncated,
82
+ truncatedBy: result.truncatedBy,
83
+ matchCount: result.matches.length,
84
+ scannedBytes: result.scannedBytes,
85
+ scannedFiles: result.scannedFiles,
86
+ scannedEntries: result.scannedEntries,
87
+ filesSkippedBinary: result.filesSkippedBinary,
88
+ filesSkippedOversize: result.filesSkippedOversize,
89
+ };
90
+ if (outputMode === "content") {
91
+ return { ...base, matches: result.matches };
92
+ }
93
+ return { ...base, fileCount: uniqueMatchPaths(result.matches).length };
94
+ }
38
95
  export function createRepoSearchTool(cwd, options) {
39
96
  const limits = resolveRepositoryLimits({
40
97
  ...options?.repository,
@@ -45,19 +102,20 @@ export function createRepoSearchTool(cwd, options) {
45
102
  const ops = options?.operations ?? createLocalRepositoryOperations(limits);
46
103
  return {
47
104
  name: "repo_search",
48
- description: `Search text files under the workspace. Default mode is literal substring match; set mode=regex for bounded regular expressions. Skips binary files, excluded basenames (default: ${limits.exclude.join(", ")}), and hidden names unless includeHidden is true. Does not follow symlinks. Caps matches/scanned bytes/time.`,
105
+ effect: CODING_OBSERVATION_EFFECT,
106
+ description: `Search text files under the workspace using literal substring match. Use outputMode "files_with_matches" for paths only or "count" for totals without line bodies. Skips binary files, excluded basenames (default: ${limits.exclude.join(", ")}), and hidden names unless includeHidden is true. Does not follow symlinks. Caps matches/scanned bytes/time.`,
49
107
  parameters: {
50
108
  type: "object",
51
109
  properties: {
52
- query: { type: "string", description: "Literal text or regular expression to search for (required)" },
110
+ query: { type: "string", description: "Literal text to search for (required)" },
53
111
  path: {
54
112
  type: "string",
55
113
  description: "Workspace-relative directory or file to search (default: workspace root)",
56
114
  },
57
115
  mode: {
58
116
  type: "string",
59
- description: 'Search mode: "literal" (default) or "regex"',
60
- enum: ["literal", "regex"],
117
+ description: "Search mode: literal substring match only",
118
+ enum: ["literal"],
61
119
  },
62
120
  caseSensitive: {
63
121
  type: "boolean",
@@ -75,6 +133,11 @@ export function createRepoSearchTool(cwd, options) {
75
133
  type: "number",
76
134
  description: `Maximum matches to retain (default ${limits.maxMatches}, hard ${HARD_MAX_SEARCH_MATCHES})`,
77
135
  },
136
+ outputMode: {
137
+ type: "string",
138
+ description: 'Result shape: "content" (default, line bodies), "files_with_matches" (unique paths), or "count" (match/file totals)',
139
+ enum: ["content", "files_with_matches", "count"],
140
+ },
78
141
  },
79
142
  required: ["query"],
80
143
  additionalProperties: false,
@@ -87,7 +150,20 @@ export function createRepoSearchTool(cwd, options) {
87
150
  if (query.length === 0)
88
151
  return errorResult(toolCallId, "query is required and must be a non-empty string.");
89
152
  const path = typeof args.path === "string" ? args.path : undefined;
90
- const mode = args.mode === "regex" ? "regex" : "literal";
153
+ if (args.mode === "regex") {
154
+ return errorResult(toolCallId, 'repo_search no longer supports mode "regex"; use literal substring search.');
155
+ }
156
+ if (args.mode !== undefined && args.mode !== "literal") {
157
+ return errorResult(toolCallId, `unsupported search mode: ${String(args.mode)}`);
158
+ }
159
+ const mode = "literal";
160
+ let outputMode;
161
+ try {
162
+ outputMode = parseOutputMode(args.outputMode);
163
+ }
164
+ catch (error) {
165
+ return errorResult(toolCallId, error instanceof Error ? error.message : String(error));
166
+ }
91
167
  const caseSensitive = args.caseSensitive === true;
92
168
  const includeHidden = args.includeHidden === true;
93
169
  let contextLines;
@@ -110,6 +186,7 @@ export function createRepoSearchTool(cwd, options) {
110
186
  risk: "low",
111
187
  metadata: {
112
188
  mode,
189
+ outputMode,
113
190
  caseSensitive,
114
191
  includeHidden,
115
192
  context: contextLines,
@@ -127,6 +204,7 @@ export function createRepoSearchTool(cwd, options) {
127
204
  query,
128
205
  path,
129
206
  mode,
207
+ outputMode,
130
208
  caseSensitive,
131
209
  includeHidden,
132
210
  exclude: limits.exclude,
@@ -138,26 +216,12 @@ export function createRepoSearchTool(cwd, options) {
138
216
  return {
139
217
  toolCallId,
140
218
  name: "repo_search",
141
- content: [{ type: "text", text: formatSearchText(result) }],
142
- metadata: {
143
- truncated: result.truncated,
144
- truncatedBy: result.truncatedBy,
145
- matchCount: result.matches.length,
146
- scannedBytes: result.scannedBytes,
147
- scannedFiles: result.scannedFiles,
148
- scannedEntries: result.scannedEntries,
149
- filesSkippedBinary: result.filesSkippedBinary,
150
- filesSkippedOversize: result.filesSkippedOversize,
151
- matches: result.matches,
152
- },
219
+ content: [{ type: "text", text: formatSearchResult(result, outputMode) }],
220
+ metadata: buildSearchMetadata(result, outputMode),
153
221
  };
154
222
  }
155
223
  catch (error) {
156
- const message = error instanceof RepositoryError
157
- ? error.message
158
- : error instanceof Error
159
- ? error.message
160
- : String(error);
224
+ const message = error instanceof RepositoryError ? error.message : error instanceof Error ? error.message : String(error);
161
225
  return errorResult(toolCallId, message);
162
226
  }
163
227
  },
package/dist/shell.d.ts CHANGED
@@ -55,6 +55,9 @@ export interface ShellToolOptions {
55
55
  shellPath?: string;
56
56
  /** Hook to adjust command, cwd, or env before execution. */
57
57
  spawnHook?: BashSpawnHook;
58
+ /** Restrict the process environment cloned for the spawn hook / child process to these
59
+ * names (e.g. scrub secrets). Unset keeps the full `process.env` clone. */
60
+ envAllowlist?: readonly string[];
58
61
  /** Max lines kept in the tail snapshot (default 2000). */
59
62
  maxLines?: number;
60
63
  /** Max bytes kept in the tail snapshot (default 50KB). */
package/dist/shell.js CHANGED
@@ -23,12 +23,25 @@
23
23
  import { spawn } from "node:child_process";
24
24
  import { constants, existsSync } from "node:fs";
25
25
  import { access as fsAccess } from "node:fs/promises";
26
+ import { CODING_UNSUPPORTED_EFFECT } from "./effects.js";
26
27
  import { enforceExecutionPolicy } from "./execution-policy.js";
27
- import { OutputAccumulator } from "./output-accumulator.js";
28
28
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, DEFAULT_SHELL_TIMEOUT_SECONDS, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TOTAL_OUTPUT_BYTES, HARD_SHELL_TIMEOUT_SECONDS, validateCodingLimit, } from "./limits.js";
29
+ import { OutputAccumulator } from "./output-accumulator.js";
29
30
  import { formatSize } from "./truncate.js";
30
31
  const EXIT_STDIO_GRACE_MS = 100;
31
32
  // --- spawn internals (re-ported from pi utils/shell.js + utils/child-process.js) ---
33
+ /** Clone the process environment, optionally restricted to an allowlist of names. */
34
+ function pickSpawnEnv(allowlist) {
35
+ if (!allowlist)
36
+ return { ...process.env };
37
+ const env = {};
38
+ for (const name of allowlist) {
39
+ const value = process.env[name];
40
+ if (value !== undefined)
41
+ env[name] = value;
42
+ }
43
+ return env;
44
+ }
32
45
  /** Resolve the shell binary + args. shellPath → SHELL env → /bin/bash → sh. */
33
46
  export function getShellConfig(customShellPath) {
34
47
  if (customShellPath) {
@@ -252,8 +265,9 @@ export function createShellTool(cwd, options) {
252
265
  const tempFilePrefix = options?.tempFilePrefix ?? "prism-shell";
253
266
  return {
254
267
  name: "shell",
268
+ effect: CODING_UNSUPPORTED_EFFECT,
255
269
  exclusive: true,
256
- description: `Execute a shell command in the current working directory. Returns combined stdout and stderr. Output is truncated to the last ${maxLines} lines or ${maxBytes / 1024}KB and capped at ${formatSize(maxTotalOutputBytes)} total. Truncated successful output is saved to a temp file. Timeout defaults to ${defaultTimeout} seconds.`,
270
+ description: `Execute a shell command in the current working directory. Returns combined stdout and stderr. Prefer repo_list, repo_search, glob, read, write, edit, delete, or move for those jobs — use shell only when no dedicated tool fits. Output is truncated to the last ${maxLines} lines or ${maxBytes / 1024}KB and capped at ${formatSize(maxTotalOutputBytes)} total. Truncated successful output is saved to a temp file. Timeout defaults to ${defaultTimeout} seconds.`,
257
271
  parameters: {
258
272
  type: "object",
259
273
  properties: {
@@ -284,9 +298,10 @@ export function createShellTool(cwd, options) {
284
298
  return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
285
299
  }
286
300
  const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
301
+ const baseEnv = pickSpawnEnv(options?.envAllowlist);
287
302
  let spawnContext = spawnHook
288
- ? spawnHook({ command: resolvedCommand, cwd, env: { ...process.env } })
289
- : { command: resolvedCommand, cwd, env: { ...process.env } };
303
+ ? spawnHook({ command: resolvedCommand, cwd, env: baseEnv })
304
+ : { command: resolvedCommand, cwd, env: baseEnv };
290
305
  const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
291
306
  kind: "shell",
292
307
  operation: "execute",
@@ -309,9 +324,7 @@ export function createShellTool(cwd, options) {
309
324
  onLimit: () => outputAbort.abort("output-limit"),
310
325
  onStorageError: () => outputAbort.abort("output-storage-error"),
311
326
  });
312
- const signal = context.signal
313
- ? AbortSignal.any([context.signal, outputAbort.signal])
314
- : outputAbort.signal;
327
+ const signal = context.signal ? AbortSignal.any([context.signal, outputAbort.signal]) : outputAbort.signal;
315
328
  let acceptingOutput = true;
316
329
  const handleData = (data) => {
317
330
  if (acceptingOutput)
@@ -399,7 +412,9 @@ export function createShellTool(cwd, options) {
399
412
  try {
400
413
  await output.cleanupTempFile();
401
414
  }
402
- catch { /* retain primary error */ }
415
+ catch {
416
+ /* retain primary error */
417
+ }
403
418
  const message = err instanceof Error ? err.message : String(err);
404
419
  return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
405
420
  }
package/dist/truncate.js CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Never returns partial lines (except the documented tail single-line edge case).
11
11
  */
12
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, HARD_MAX_BYTES, HARD_MAX_LINES, validateCodingLimit, } from "./limits.js";
12
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, HARD_MAX_BYTES, HARD_MAX_LINES, validateCodingLimit } from "./limits.js";
13
13
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "./limits.js";
14
14
  /**
15
15
  * Default char cap for {@link truncateLine}. pi names this GREP_MAX_LINE_LENGTH
package/dist/write.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
2
+ import type { CodingLifecycleEvent } from "./lifecycle.js";
3
+ import { type ReadBeforeWriteOptions } from "./read-path-set.js";
2
4
  /**
3
5
  * Pluggable operations for the write tool. Override to delegate file writing to remote systems
4
6
  * (e.g. SSH) while keeping the tool's directory-creation + per-path serialization behavior.
@@ -14,12 +16,14 @@ export interface WriteOperations {
14
16
  signal?: AbortSignal;
15
17
  }) => Promise<void>;
16
18
  }
17
- export interface WriteToolOptions {
19
+ export interface WriteToolOptions extends ReadBeforeWriteOptions {
18
20
  /** Structured pre-execution policy checked before filesystem writes. */
19
21
  executionPolicy?: ExecutionPolicy;
20
22
  /** Custom operations backend (default: local filesystem). */
21
23
  operations?: WriteOperations;
22
24
  /** Maximum UTF-8 input bytes accepted before any filesystem mutation (default 8 MiB). */
23
25
  maxInputBytes?: number;
26
+ /** Optional consumer-gated lifecycle listener (file_changed / permission_denied). */
27
+ onEvent?: (event: CodingLifecycleEvent) => void;
24
28
  }
25
29
  export declare function createWriteTool(cwd: string, options?: WriteToolOptions): ToolDefinition;
package/dist/write.js CHANGED
@@ -15,14 +15,17 @@
15
15
  * (pi would throw "Operation aborted" even after a successful write — misleading, so dropped).
16
16
  */
17
17
  import { Buffer } from "node:buffer";
18
- import { mkdir as fsMkdir, writeFile as fsWriteFile } from "node:fs/promises";
18
+ import { mkdir as fsMkdir } from "node:fs/promises";
19
19
  import { dirname } from "node:path";
20
+ import { atomicWriteUtf8File } from "./atomic-write.js";
21
+ import { CODING_LOCAL_EFFECT } from "./effects.js";
20
22
  import { enforceExecutionPolicy } from "./execution-policy.js";
21
- import { resolveToCwd } from "./path-utils.js";
22
23
  import { withFileMutationQueue } from "./file-mutation-queue.js";
23
- import { DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES, validateCodingLimit, } from "./limits.js";
24
+ import { DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES, validateCodingLimit } from "./limits.js";
25
+ import { resolveToCwd } from "./path-utils.js";
26
+ import { refuseReadBeforeWrite } from "./read-path-set.js";
24
27
  const defaultWriteOperations = {
25
- writeFile: (path, content, options) => fsWriteFile(path, content, { encoding: "utf-8", signal: options?.signal }),
28
+ writeFile: (path, content, options) => atomicWriteUtf8File(path, content, { signal: options?.signal }),
26
29
  mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => { }),
27
30
  };
28
31
  function errorResult(toolCallId, message) {
@@ -43,12 +46,17 @@ export function createWriteTool(cwd, options) {
43
46
  const maxInputBytes = validateCodingLimit("maxInputBytes", options?.maxInputBytes ?? DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES);
44
47
  return {
45
48
  name: "write",
46
- description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
49
+ effect: CODING_LOCAL_EFFECT,
50
+ description: "Create or overwrite a file (full replace). Creates parent directories. Prefer edit for targeted changes. When the host enabled requireReadBeforeWrite, read the path first or pass force=true to override.",
47
51
  parameters: {
48
52
  type: "object",
49
53
  properties: {
50
54
  path: { type: "string", description: "Path to the file to write (relative or absolute)" },
51
55
  content: { type: "string", description: "Content to write to the file" },
56
+ force: {
57
+ type: "boolean",
58
+ description: "Bypass read-before-write guard when the host enabled requireReadBeforeWrite.",
59
+ },
52
60
  },
53
61
  required: ["path", "content"],
54
62
  additionalProperties: false,
@@ -57,6 +65,7 @@ export function createWriteTool(cwd, options) {
57
65
  const toolCallId = context.toolCallId;
58
66
  const path = typeof args.path === "string" ? args.path : "";
59
67
  const content = typeof args.content === "string" ? args.content : undefined;
68
+ const force = args.force === true;
60
69
  if (path.length === 0) {
61
70
  return errorResult(toolCallId, "path is required and must be a non-empty string.");
62
71
  }
@@ -75,10 +84,13 @@ export function createWriteTool(cwd, options) {
75
84
  paths: [absolutePath],
76
85
  risk: "medium",
77
86
  metadata: { bytes, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
78
- }, toolCallId, "write");
87
+ }, toolCallId, "write", (denied) => options?.onEvent?.({ type: "permission_denied", ...denied }));
79
88
  if (!policyCheck.allowed)
80
89
  return policyCheck.result;
81
90
  const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
91
+ const rbwRefusal = refuseReadBeforeWrite("write", path, allowedPath, options, force);
92
+ if (rbwRefusal)
93
+ return errorResult(toolCallId, rbwRefusal);
82
94
  const dir = dirname(allowedPath);
83
95
  return await withFileMutationQueue(allowedPath, async () => {
84
96
  // Check abort before each fs op — do not start a new operation once aborted. We intentionally
@@ -89,6 +101,7 @@ export function createWriteTool(cwd, options) {
89
101
  if (context.signal?.aborted)
90
102
  return errorResult(toolCallId, "Operation aborted");
91
103
  await ops.writeFile(allowedPath, content, { maxBytes: maxInputBytes, signal: context.signal });
104
+ options?.onEvent?.({ type: "file_changed", path: allowedPath, op: "write", toolCallId });
92
105
  const lines = countLines(content);
93
106
  return {
94
107
  toolCallId,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arnilo/prism-coding-agent",
3
- "version": "0.0.96",
4
- "description": "Optional coding-agent tools (shell, read, write, edit, repo_list, repo_search, opt-in Git/check, and durable plan/checkpoint helpers) package for Prism.",
3
+ "version": "0.1.1",
4
+ "description": "Optional coding-agent tools (shell, read, write, edit, repo_list, repo_search, glob, delete, move, opt-in Git/check/ask-user-decision, and durable plan/checkpoint helpers) package for Prism.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -28,11 +28,13 @@
28
28
  "diff": "^9.0.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@arnilo/prism": "0.0.96"
31
+ "@arnilo/prism": "0.1.1",
32
+ "@arnilo/prism-workflows": "0.1.1"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@arnilo/prism": "file:../..",
35
- "@arnilo/prism-evals": "file:../evals"
36
+ "@arnilo/prism-evals": "file:../evals",
37
+ "@arnilo/prism-workflows": "file:../workflows"
36
38
  },
37
39
  "engines": {
38
40
  "node": ">=20"