@aragon-agent/cli 0.5.8 → 0.5.10

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 (156) hide show
  1. package/CHANGELOG.md +1157 -747
  2. package/README.md +1552 -1163
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/boot/guard.d.ts +135 -0
  16. package/dist/boot/guard.js +218 -0
  17. package/dist/boot/rollback.d.ts +86 -0
  18. package/dist/boot/rollback.js +185 -0
  19. package/dist/cli.js +368 -3
  20. package/dist/commands/builtins.d.ts +1 -1
  21. package/dist/commands/builtins.js +426 -2
  22. package/dist/commands/registry.d.ts +17 -0
  23. package/dist/config/app-paths.d.ts +31 -0
  24. package/dist/config/app-paths.js +37 -0
  25. package/dist/config/cli-commands.d.ts +79 -2
  26. package/dist/config/cli-commands.js +185 -3
  27. package/dist/config/env.d.ts +15 -3
  28. package/dist/config/env.js +143 -3
  29. package/dist/config/load.d.ts +66 -0
  30. package/dist/config/load.js +107 -1
  31. package/dist/config/schema.d.ts +268 -2
  32. package/dist/config/schema.js +288 -1
  33. package/dist/config/store.js +32 -1
  34. package/dist/fast/critique.d.ts +43 -0
  35. package/dist/fast/critique.js +62 -0
  36. package/dist/fast/digest.d.ts +42 -0
  37. package/dist/fast/digest.js +85 -0
  38. package/dist/fast/frames.d.ts +99 -0
  39. package/dist/fast/frames.js +180 -0
  40. package/dist/fast/limits.d.ts +98 -0
  41. package/dist/fast/limits.js +98 -0
  42. package/dist/fast/prompt.d.ts +72 -0
  43. package/dist/fast/prompt.js +94 -0
  44. package/dist/fast/resolve.d.ts +43 -0
  45. package/dist/fast/resolve.js +105 -0
  46. package/dist/fast/review-call.d.ts +99 -0
  47. package/dist/fast/review-call.js +130 -0
  48. package/dist/fast/reviewer.d.ts +248 -0
  49. package/dist/fast/reviewer.js +664 -0
  50. package/dist/fast/types.d.ts +128 -0
  51. package/dist/fast/types.js +14 -0
  52. package/dist/fast/wiring.d.ts +170 -0
  53. package/dist/fast/wiring.js +316 -0
  54. package/dist/launcher.d.ts +14 -0
  55. package/dist/launcher.js +43 -0
  56. package/dist/logging/install.d.ts +15 -1
  57. package/dist/logging/install.js +37 -4
  58. package/dist/logging/logger.d.ts +1 -1
  59. package/dist/session/persist.js +57 -1
  60. package/dist/team/normalize.d.ts +26 -1
  61. package/dist/team/normalize.js +23 -3
  62. package/dist/team/prompt.d.ts +11 -0
  63. package/dist/team/prompt.js +10 -0
  64. package/dist/team/report.d.ts +18 -0
  65. package/dist/team/report.js +27 -1
  66. package/dist/team/runtime.d.ts +13 -2
  67. package/dist/team/runtime.js +33 -6
  68. package/dist/team/subagent.d.ts +19 -1
  69. package/dist/team/subagent.js +72 -10
  70. package/dist/team/task-tool.d.ts +26 -0
  71. package/dist/team/task-tool.js +48 -4
  72. package/dist/team/types.d.ts +80 -1
  73. package/dist/tools/bash-tool.js +19 -3
  74. package/dist/tools/diff.d.ts +25 -5
  75. package/dist/tools/diff.js +72 -41
  76. package/dist/tools/file-change-store.d.ts +34 -0
  77. package/dist/tools/file-change-store.js +57 -0
  78. package/dist/tools/fs-tools.d.ts +26 -0
  79. package/dist/tools/fs-tools.js +62 -5
  80. package/dist/tools/index.d.ts +31 -0
  81. package/dist/tools/index.js +7 -1
  82. package/dist/tools/patch.d.ts +115 -0
  83. package/dist/tools/patch.js +453 -0
  84. package/dist/tools/terminal-output.d.ts +107 -0
  85. package/dist/tools/terminal-output.js +198 -0
  86. package/dist/tools/tool-output-store.d.ts +68 -0
  87. package/dist/tools/tool-output-store.js +102 -0
  88. package/dist/ui/ActivityLine.d.ts +48 -0
  89. package/dist/ui/ActivityLine.js +22 -0
  90. package/dist/ui/App.d.ts +11 -4
  91. package/dist/ui/App.js +359 -37
  92. package/dist/ui/BottomStatusRow.d.ts +65 -0
  93. package/dist/ui/BottomStatusRow.js +17 -0
  94. package/dist/ui/Composer.d.ts +1 -6
  95. package/dist/ui/Composer.js +2 -2
  96. package/dist/ui/PromptInput.d.ts +1 -15
  97. package/dist/ui/PromptInput.js +1 -38
  98. package/dist/ui/StatusBar.d.ts +37 -0
  99. package/dist/ui/StatusBar.js +5 -3
  100. package/dist/ui/TeamPanel.d.ts +5 -4
  101. package/dist/ui/TeamPanel.js +1 -1
  102. package/dist/ui/TodoPanel.d.ts +5 -4
  103. package/dist/ui/Transcript.d.ts +26 -3
  104. package/dist/ui/Transcript.js +76 -10
  105. package/dist/ui/UpdateLine.d.ts +44 -0
  106. package/dist/ui/UpdateLine.js +129 -0
  107. package/dist/ui/activity-phrases.d.ts +33 -0
  108. package/dist/ui/activity-phrases.js +50 -0
  109. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  110. package/dist/ui/entries/AssistantEntry.js +3 -2
  111. package/dist/ui/entries/DiffView.d.ts +67 -0
  112. package/dist/ui/entries/DiffView.js +103 -0
  113. package/dist/ui/entries/FastCard.d.ts +53 -0
  114. package/dist/ui/entries/FastCard.js +87 -0
  115. package/dist/ui/entries/RetryCard.d.ts +56 -0
  116. package/dist/ui/entries/RetryCard.js +60 -0
  117. package/dist/ui/entries/TeamCard.js +6 -1
  118. package/dist/ui/entries/ToolCard.d.ts +38 -1
  119. package/dist/ui/entries/ToolCard.js +68 -5
  120. package/dist/ui/glyphs.d.ts +9 -0
  121. package/dist/ui/glyphs.js +2 -0
  122. package/dist/ui/layout/AppShell.d.ts +13 -22
  123. package/dist/ui/layout/AppShell.js +3 -38
  124. package/dist/ui/layout/virtual-window.d.ts +1 -1
  125. package/dist/ui/layout/virtual-window.js +109 -5
  126. package/dist/ui/overlays/HelpOverlay.js +16 -2
  127. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  128. package/dist/ui/overlays/SettingsScreen.js +147 -1
  129. package/dist/ui/transcript-text.js +27 -0
  130. package/dist/ui/use-wheel-routing.d.ts +16 -32
  131. package/dist/ui/use-wheel-routing.js +21 -38
  132. package/dist/update/classify-failure.d.ts +32 -0
  133. package/dist/update/classify-failure.js +60 -0
  134. package/dist/update/install-lock.d.ts +55 -0
  135. package/dist/update/install-lock.js +184 -0
  136. package/dist/update/install-source.d.ts +97 -0
  137. package/dist/update/install-source.js +218 -0
  138. package/dist/update/installer.d.ts +108 -0
  139. package/dist/update/installer.js +242 -0
  140. package/dist/update/limits.d.ts +111 -0
  141. package/dist/update/limits.js +111 -0
  142. package/dist/update/npm-view.d.ts +62 -0
  143. package/dist/update/npm-view.js +133 -0
  144. package/dist/update/registry.d.ts +52 -0
  145. package/dist/update/registry.js +161 -0
  146. package/dist/update/semver.d.ts +89 -0
  147. package/dist/update/semver.js +295 -0
  148. package/dist/update/service.d.ts +193 -0
  149. package/dist/update/service.js +636 -0
  150. package/dist/update/state.d.ts +103 -0
  151. package/dist/update/state.js +142 -0
  152. package/dist/update/types.d.ts +169 -0
  153. package/dist/update/types.js +68 -0
  154. package/package.json +3 -3
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The CLI-local side channel a `FilePatch` travels on (agent-activity-presentation
3
+ * §3.3.5 / D-8).
4
+ *
5
+ * The patch does NOT ride in the tool result: `ToolResult` has no metadata
6
+ * channel and `packages/core`'s surface is frozen by `public-api.test.ts`, and
7
+ * more importantly a richer display must not cost the model more tokens. So the
8
+ * tool records the patch here, keyed by its own tool-call id, and `reduceEvent`
9
+ * takes it out again when `tool_execution_end` arrives.
10
+ *
11
+ * Three properties, each load-bearing:
12
+ *
13
+ * - **Owner-scoped.** The key is `${owner}:${toolCallId}`. Only `'lead'` is ever
14
+ * written this round (`team/**` is out of scope, so `buildSubagentTools`
15
+ * passes no recorder), but subagents build their tools from the same factory
16
+ * — so the day one gets a recorder, a child's tool-call id must not be able
17
+ * to land on a lead's card. One string concatenation, spent now (D-13).
18
+ * - **Take-once.** `tool_execution_end` fires exactly once per call, and a patch
19
+ * not consumed there never will be. Removal on read is what keeps a long
20
+ * session's store at constant size however many files are edited.
21
+ * - **Bounded FIFO.** At `cap` entries the oldest key is evicted. The cap is a
22
+ * RING, not a leak detector: nothing is retained longer than `cap` writes, so
23
+ * a patch nobody consumes cannot accumulate.
24
+ */
25
+ /** Entries retained before the oldest is evicted. */
26
+ export const FILE_CHANGE_STORE_CAP = 64;
27
+ export function createFileChangeStore(cap = FILE_CHANGE_STORE_CAP) {
28
+ // A `Map` iterates in insertion order, so the first key IS the oldest.
29
+ const entries = new Map();
30
+ const limit = Math.max(1, Math.floor(cap));
31
+ const keyOf = (owner, toolCallId) => `${owner}:${toolCallId}`;
32
+ return {
33
+ record(owner, toolCallId, patch) {
34
+ const key = keyOf(owner, toolCallId);
35
+ // Delete first so a re-record moves the key to the END of the ring rather
36
+ // than leaving it at its original age.
37
+ entries.delete(key);
38
+ entries.set(key, patch);
39
+ while (entries.size > limit) {
40
+ const oldest = entries.keys().next();
41
+ if (oldest.done)
42
+ break;
43
+ entries.delete(oldest.value);
44
+ }
45
+ },
46
+ take(owner, toolCallId) {
47
+ const key = keyOf(owner, toolCallId);
48
+ const patch = entries.get(key);
49
+ if (patch !== undefined)
50
+ entries.delete(key);
51
+ return patch;
52
+ },
53
+ size() {
54
+ return entries.size;
55
+ },
56
+ };
57
+ }
@@ -7,8 +7,34 @@
7
7
  * tools do not re-implement them.
8
8
  */
9
9
  import { type AgentTool } from '@aragon-agent/core';
10
+ import { type FilePatch } from './patch.js';
10
11
  export interface ToolDeps {
11
12
  getCwd: () => string;
13
+ /**
14
+ * Hand a structured patch to the UI on the CLI-local side channel
15
+ * (agent-activity-presentation §3.3.5).
16
+ *
17
+ * PRE-BOUND TO ITS OWNER by whoever supplies it, so this module never learns
18
+ * what an owner is. OPTIONAL, and omitting it leaves both tools byte-identical
19
+ * to the pre-feature build — including the extra `write_file` stat/read, which
20
+ * is skipped entirely when there is nobody to record for.
21
+ */
22
+ recordChange?: (toolCallId: string, patch: FilePatch) => void;
23
+ /**
24
+ * Hand a raw output chunk to the UI on the CLI-local side channel
25
+ * (agent-activity-presentation-live §3.1).
26
+ *
27
+ * PRE-BOUND TO ITS OWNER, exactly like `recordChange` above, so this module
28
+ * never learns what an owner is. OPTIONAL: omitting it leaves `bash`
29
+ * byte-identical to the pre-feature build -- not one extra allocation, not one
30
+ * extra call (AC-31).
31
+ *
32
+ * ONLY `bash` CALLS IT (D-32). Every other builtin is bounded local I/O that
33
+ * settles in milliseconds, so a progress channel for them would be machinery
34
+ * with no observer. The channel is keyed by tool-call id rather than by tool
35
+ * name, so a future long-running tool costs one call site.
36
+ */
37
+ recordOutput?: (toolCallId: string, chunk: string) => void;
12
38
  }
13
39
  export declare function makeReadFile(deps: ToolDeps): AgentTool;
14
40
  export declare function makeWriteFile(deps: ToolDeps): AgentTool;
@@ -10,6 +10,7 @@ import { promises as fs } from 'node:fs';
10
10
  import { isAbsolute, join, resolve, dirname } from 'node:path';
11
11
  import { defineTool, errorResult, textResult, } from '@aragon-agent/core';
12
12
  import { renderUnifiedDiff } from './diff.js';
13
+ import { PATCH_LIMITS, buildPatch } from './patch.js';
13
14
  function resolvePath(cwd, p) {
14
15
  return isAbsolute(p) ? p : resolve(cwd, p);
15
16
  }
@@ -78,13 +79,20 @@ export function makeWriteFile(deps) {
78
79
  },
79
80
  required: ['path', 'content'],
80
81
  },
81
- async execute(_id, rawParams) {
82
+ async execute(id, rawParams) {
82
83
  const params = rawParams;
83
84
  const target = resolvePath(deps.getCwd(), params.path);
84
85
  try {
86
+ // BEFORE the write, and only when somebody is listening. `readOldSide`
87
+ // swallows every failure itself, so a presentation feature can never
88
+ // prevent or delay the operation it presents (D-14 / R-6).
89
+ const oldSide = deps.recordChange ? await readOldSide(target) : null;
85
90
  await fs.mkdir(dirname(target), { recursive: true });
86
91
  await fs.writeFile(target, params.content, 'utf-8');
92
+ if (oldSide)
93
+ recordFileChange(deps, id, oldSide, params.content, params.path);
87
94
  const bytes = Buffer.byteLength(params.content, 'utf-8');
95
+ // BYTE-IDENTICAL model-facing text: the diff costs zero extra tokens.
88
96
  return textResult(`Wrote ${bytes} bytes to ${params.path}`);
89
97
  }
90
98
  catch (err) {
@@ -116,7 +124,7 @@ export function makeEditFile(deps) {
116
124
  },
117
125
  required: ['path', 'old_string', 'new_string'],
118
126
  },
119
- async execute(_id, rawParams) {
127
+ async execute(id, rawParams) {
120
128
  const params = rawParams;
121
129
  const target = resolvePath(deps.getCwd(), params.path);
122
130
  try {
@@ -138,6 +146,10 @@ export function makeEditFile(deps) {
138
146
  ? original.split(params.old_string).join(params.new_string)
139
147
  : original.replace(params.old_string, () => params.new_string);
140
148
  await fs.writeFile(target, updated, 'utf-8');
149
+ // The same sentence, from the same data: the card's structured patch and
150
+ // the model's text are built from `original` / `updated` by one algorithm
151
+ // at two context widths (D-10 / D-20).
152
+ recordFileChange(deps, id, { text: original }, updated, params.path);
141
153
  const diff = renderUnifiedDiff(original, updated, { path: params.path });
142
154
  return textResult(`Applied edit to ${params.path}:\n${diff}`);
143
155
  }
@@ -189,9 +201,54 @@ export function makeListDir(deps) {
189
201
  },
190
202
  });
191
203
  }
192
- // ---------------------------------------------------------------------------
193
- // Helpers
194
- // ---------------------------------------------------------------------------
204
+ /**
205
+ * Best-effort read of the file `write_file` is about to overwrite.
206
+ *
207
+ * NEVER THROWS AND NEVER REPORTS AN ERROR TO THE MODEL. Every outcome is a
208
+ * `FilePatch` the card can render honestly:
209
+ *
210
+ * ENOENT -> null, no reason -> a `create` diff
211
+ * size > readMaxBytes -> null, `too-large` -> an `update` we declined to read
212
+ * binary sniff fails -> null, `binary` -> ditto
213
+ * any other throw -> null, `unreadable` -> ditto
214
+ */
215
+ async function readOldSide(target) {
216
+ try {
217
+ const stat = await fs.stat(target);
218
+ if (stat.size > PATCH_LIMITS.readMaxBytes)
219
+ return { text: null, degraded: 'too-large' };
220
+ const buf = await fs.readFile(target);
221
+ if (looksBinary(buf))
222
+ return { text: null, degraded: 'binary' };
223
+ return { text: buf.toString('utf-8') };
224
+ }
225
+ catch (err) {
226
+ if (err.code === 'ENOENT')
227
+ return { text: null };
228
+ return { text: null, degraded: 'unreadable' };
229
+ }
230
+ }
231
+ /**
232
+ * Build and hand over the patch, if anyone asked for one.
233
+ *
234
+ * WRAPPED, because `recordChange` is a caller-supplied closure and a display
235
+ * callback must not be able to fail a write that has already happened.
236
+ * `buildPatch` itself is total.
237
+ */
238
+ function recordFileChange(deps, toolCallId, oldSide, newText, path) {
239
+ if (!deps.recordChange)
240
+ return;
241
+ try {
242
+ deps.recordChange(toolCallId, buildPatch(oldSide.text, newText, {
243
+ path,
244
+ context: PATCH_LIMITS.context,
245
+ ...(oldSide.degraded ? { degraded: oldSide.degraded } : {}),
246
+ }));
247
+ }
248
+ catch {
249
+ // Presentation only. The file is already on disk.
250
+ }
251
+ }
195
252
  function countOccurrences(haystack, needle) {
196
253
  if (needle.length === 0)
197
254
  return 0;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { type AgentTool, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
12
12
  import type { AgentMode } from '../agent/agent-mode.js';
13
+ import type { FilePatch } from './patch.js';
13
14
  export interface ConfirmRequest {
14
15
  tool: string;
15
16
  summary: string;
@@ -29,6 +30,36 @@ export interface ConfirmRequest {
29
30
  }
30
31
  export interface BuiltinToolsOptions {
31
32
  getCwd: () => string;
33
+ /**
34
+ * Hand `write_file` / `edit_file` a place to leave a structured `FilePatch`
35
+ * (agent-activity-presentation §3.3.5), already bound to its owner.
36
+ *
37
+ * THE FIELD IS NEEDED IN TWO INTERFACES, NOT ONE. This bag is the factory's;
38
+ * `ToolDeps` is what the `make*` functions receive, and it is built below from
39
+ * exactly the fields named there. Adding it to `ToolDeps` alone type-checks and
40
+ * forwards nothing — the tools simply never record, and every card falls back
41
+ * to `ToolPreview` with no error anywhere (P1-3).
42
+ *
43
+ * Omitting it — every existing test, every headless run, and every subagent
44
+ * this round — leaves the tools byte-identical to today, which is the same
45
+ * opt-in shape `skillTools` / `planTools` / `todoTools` already use.
46
+ */
47
+ recordChange?: (toolCallId: string, patch: FilePatch) => void;
48
+ /**
49
+ * Hand `bash` a place to leave its raw output chunks
50
+ * (agent-activity-presentation-live §3.1.2), already bound to its owner.
51
+ *
52
+ * THE TWO-INTERFACE RULE ABOVE APPLIES VERBATIM (P1-1), and it applies because
53
+ * round 1 made this exact mistake once already with `recordChange`: adding the
54
+ * field to `ToolDeps` alone type-checks and forwards nothing, so `bash` never
55
+ * records, every running card stays the single `running` row it is today, and
56
+ * NOTHING ANYWHERE ERRORS.
57
+ *
58
+ * The conditional spread below is also what makes AC-31 true at this layer:
59
+ * with `liveToolOutput` off the controller passes no key, so no property is
60
+ * added and `deps` is the object it is today.
61
+ */
62
+ recordOutput?: (toolCallId: string, chunk: string) => void;
32
63
  /** When true (and `confirm` is provided), mutating tools are gated. */
33
64
  confirmTools?: boolean;
34
65
  /** Awaited before a mutating tool runs; resolve `false` to cancel. */
@@ -215,7 +215,13 @@ function withPlanModeGate(tool, getMode) {
215
215
  };
216
216
  }
217
217
  export function createBuiltinTools(options) {
218
- const deps = { getCwd: options.getCwd };
218
+ const deps = {
219
+ getCwd: options.getCwd,
220
+ // The forward half of the two-interface note on `recordChange` above.
221
+ ...(options.recordChange ? { recordChange: options.recordChange } : {}),
222
+ // Added in the same shape and for the same reason (P1-1).
223
+ ...(options.recordOutput ? { recordOutput: options.recordOutput } : {}),
224
+ };
219
225
  let tools = [
220
226
  makeReadFile(deps),
221
227
  makeWriteFile(deps),
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Structured file patches (agent-activity-presentation §3.3).
3
+ *
4
+ * PURE, NO I/O, NO REACT, ASCII-ONLY MARKERS. `glyphs.test.ts`'s static scan
5
+ * walks ALL of `src/` (skipping only `__tests__/` and two exempt files), so this
6
+ * module sits inside that guard rail directly rather than by analogy. The
7
+ * substantive reason is the one `reducer.ts` gives for `PREVIEW_TRUNCATION_MARK`:
8
+ * a string produced outside `src/ui/**` has no `TermCapabilities` in scope and
9
+ * must be safe on a legacy `cmd.exe`.
10
+ *
11
+ * The ladder is ANCHORED SPLIT -> BOUNDED LCS -> BLOCK REPLACE, and every
12
+ * recursion path terminates in one of three leaf returns. `blockReplace` is
13
+ * exactly what `renderUnifiedDiff` produced before this round — every old row as
14
+ * `del`, then every new row as `add` — kept as the explicit, bounded fallback so
15
+ * a pathological input degrades to today's behaviour rather than hanging.
16
+ *
17
+ * `buildPatch` is TOTAL: it never throws, for any pair of strings.
18
+ */
19
+ export type PatchLineKind = 'ctx' | 'add' | 'del';
20
+ export interface PatchLine {
21
+ kind: PatchLineKind;
22
+ /** 1-based line number in the OLD file; absent on an `add`. */
23
+ oldLine?: number;
24
+ /** 1-based line number in the NEW file; absent on a `del`. */
25
+ newLine?: number;
26
+ /** Rendered text. A trailing CR is stripped; long lines are truncated. */
27
+ text: string;
28
+ }
29
+ export interface PatchHunk {
30
+ oldStart: number;
31
+ oldCount: number;
32
+ newStart: number;
33
+ newCount: number;
34
+ lines: PatchLine[];
35
+ }
36
+ export interface FilePatch {
37
+ /** As the model wrote it — relative when the call was relative. */
38
+ path: string;
39
+ kind: 'create' | 'update';
40
+ added: number;
41
+ removed: number;
42
+ hunks: PatchHunk[];
43
+ /** Budget was hit; the tail of the change is not in `hunks`. */
44
+ truncated: boolean;
45
+ /**
46
+ * Total rendered rows across every hunk (summary and separators excluded).
47
+ *
48
+ * PRECOMPUTED so `entryRevision` stays O(1) (I-L3-1 forbids hashing, and
49
+ * summing `hunks[].lines.length` per entry per frame is exactly the cost the
50
+ * height cache exists to remove).
51
+ */
52
+ lineCount: number;
53
+ /** Set when the old side could not be read: too large, binary, or unreadable. */
54
+ degraded?: 'binary' | 'too-large' | 'unreadable';
55
+ }
56
+ export declare const PATCH_LIMITS: {
57
+ /**
58
+ * Context rows kept either side of a change, FOR THE UI.
59
+ *
60
+ * The text renderer does NOT use this (D-20): `renderUnifiedDiff` keeps its
61
+ * own `CONTEXT_LINES = 2` default so the model-facing diff does not silently
62
+ * widen by two rows on every single-region edit. One algorithm, two widths,
63
+ * chosen by the caller.
64
+ */
65
+ readonly context: 3;
66
+ /**
67
+ * Hunks separated by this many context rows or fewer are merged into one.
68
+ *
69
+ * MUST STAY `>= 2 * context` FOR EVERY CALLER'S CONTEXT WIDTH, or two adjacent
70
+ * hunks expanded by `context` would overlap and the same row would be rendered
71
+ * twice with two different line numbers. 6 covers both widths in use (3 and 2).
72
+ */
73
+ readonly mergeGap: 6;
74
+ /** Rendered rows a patch may carry, across all hunks. */
75
+ readonly maxLines: 200;
76
+ /** Total characters across all `PatchLine.text`. Mirrors STORED_PREVIEW_CHARS. */
77
+ readonly maxChars: 8000;
78
+ /** A single row is truncated past this, with `TRUNCATION_MARK`. */
79
+ readonly maxLineChars: 500;
80
+ /** Above this cell count the LCS leaf is refused; the segment becomes a block replace. */
81
+ readonly lcsCellBudget: 160000;
82
+ /** Recursion depth ceiling for the anchor split. */
83
+ readonly maxDepth: 32;
84
+ /** Old-side bytes above which `write_file` does not read for a diff. */
85
+ readonly readMaxBytes: number;
86
+ /** Model-facing diff text ceiling — a bound `edit_file` does not have today. */
87
+ readonly modelDiffMaxChars: 4000;
88
+ };
89
+ /**
90
+ * Appended to a row clipped at `maxLineChars`.
91
+ *
92
+ * SPELLED HERE RATHER THAN IMPORTED from `agent/reducer.ts`, so that `tools/`
93
+ * does not take a runtime dependency on the view model for one three-character
94
+ * string. `patch.test.ts` asserts the two spellings are equal, so the pair
95
+ * cannot drift silently — which is the property the import would have bought.
96
+ */
97
+ export declare const TRUNCATION_MARK = "...";
98
+ /** The note emitted when exactly one side ends with a newline. */
99
+ export declare const NO_NEWLINE_NOTE = "(no newline at end of file)";
100
+ export interface BuildPatchOptions {
101
+ /** As the model wrote it; carried onto the patch verbatim. */
102
+ path: string;
103
+ /** Context rows either side of a change. Defaults to `PATCH_LIMITS.context`. */
104
+ context?: number;
105
+ /**
106
+ * Why the old side is `null`.
107
+ *
108
+ * It also decides `kind`: a missing old side with no reason is a CREATE, and a
109
+ * missing old side WITH one is an UPDATE whose old half could not be read. The
110
+ * distinction matters because "created a file" and "overwrote a 5 MB file we
111
+ * declined to read" are different events and the card names them differently.
112
+ */
113
+ degraded?: FilePatch['degraded'];
114
+ }
115
+ export declare function buildPatch(oldText: string | null, newText: string, options: BuildPatchOptions): FilePatch;