@miller-tech/uap 1.195.2 → 1.196.0

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.
@@ -67,6 +67,41 @@ export declare function readFileWindow(contents: string, opts?: {
67
67
  windowBytes?: number;
68
68
  path?: string;
69
69
  }): string;
70
+ /** Bytes a single `list_dir` / `run_bash` reply may occupy. */
71
+ export declare const TOOL_OUTPUT_MAX_BYTES = 4000;
72
+ /**
73
+ * One window of a directory listing, plus what it left out.
74
+ *
75
+ * Same defect as `read_file` had, in a different costume: `readdirSync(...)
76
+ * .join('\n').slice(0, 4000)` cut mid-name and said nothing, so a large
77
+ * directory was silently unlistable past its first ~200 entries and the agent
78
+ * had no way to learn the rest existed.
79
+ *
80
+ * Truncates on ENTRY boundaries — half a filename is worse than no filename,
81
+ * because the model will confidently `read_file` it — and takes the same
82
+ * 1-based `offset` as `read_file` so a big directory is reachable rather than
83
+ * merely honest about being cut.
84
+ */
85
+ export declare function listingWindow(entries: readonly string[], opts?: {
86
+ offset?: number;
87
+ maxBytes?: number;
88
+ path?: string;
89
+ }): string;
90
+ /**
91
+ * A command's output, clipped from the MIDDLE rather than the end.
92
+ *
93
+ * `${stdout}${stderr}`.slice(0, 4000) keeps the head — which is the worst half
94
+ * to keep. A failing build puts its summary at the END ("error: could not
95
+ * compile", "3 failed, 41 passed", the exit line); head-only truncation hands
96
+ * the model a wall of compiler noise with the verdict cut off, and it cannot
97
+ * ask again because command output is not re-fetchable the way a file is.
98
+ *
99
+ * So both ends: the head carries the FIRST error (the one that usually causes
100
+ * the rest) and the invocation, the tail carries the verdict. The elision is
101
+ * stated in the middle where it happened, because a gap the reader cannot see
102
+ * is a gap the reader will read straight across.
103
+ */
104
+ export declare function clipCommandOutput(output: string, maxBytes?: number): string;
70
105
  /** Tool-call rounds before a final answer is forced (knob `UAP_MAX_TOOL_ROUNDS`). */
71
106
  export declare function defaultMaxToolRounds(): number;
72
107
  export interface AgenticExecutorOptions {
@@ -193,13 +228,17 @@ declare const TOOLS: readonly [{
193
228
  readonly type: "function";
194
229
  readonly function: {
195
230
  readonly name: "list_dir";
196
- readonly description: "List files/dirs at a path relative to the project root.";
231
+ readonly description: string;
197
232
  readonly parameters: {
198
233
  readonly type: "object";
199
234
  readonly properties: {
200
235
  readonly path: {
201
236
  readonly type: "string";
202
237
  };
238
+ readonly offset: {
239
+ readonly type: "integer";
240
+ readonly description: "1-based entry to start at. Omit for the start of the listing.";
241
+ };
203
242
  };
204
243
  readonly required: readonly ["path"];
205
244
  };
@@ -319,6 +358,44 @@ declare const TOOLS: readonly [{
319
358
  };
320
359
  };
321
360
  }];
361
+ /**
362
+ * Top-level definition HEADERS in a file, counted.
363
+ *
364
+ * Keyed on the whole header line, not the name, and that distinction is the
365
+ * whole design. Overloading is legal and common — Postgres `f(int)` beside
366
+ * `f(text)`, a TypeScript overload set — and those have DIFFERENT headers. An
367
+ * appended copy has an IDENTICAL one. Counting names would refuse the first and
368
+ * miss nothing extra; counting headers refuses only the copy.
369
+ *
370
+ * Column 0 only. A method inside a class or a nested helper is indented, and
371
+ * two classes may legitimately both define `run`. Top-level definitions are
372
+ * where re-adding is unambiguous.
373
+ *
374
+ * An unrecognised extension yields nothing, so the guard is inert rather than
375
+ * guessing at a language it does not know.
376
+ */
377
+ export declare function topLevelDefinitionHeaders(content: string, path: string): Map<string, number>;
378
+ /** Escape hatch for a file that genuinely repeats a header (generated code). */
379
+ export declare function duplicateDefinitionGuardDisabled(): boolean;
380
+ /**
381
+ * Refuse a write that RE-ADDS a definition the file already has.
382
+ *
383
+ * The guards around this one all watch a file get smaller — anti-gutting
384
+ * (bytes), stub-substance (empty bodies), byte-identical NO-OP. Nothing watched
385
+ * it get BIGGER by repetition, and that is the shape the damage actually took.
386
+ *
387
+ * Live (cognition-engine, 2026-08-11): setup.sql ended the day holding 65 copies
388
+ * of `CREATE FUNCTION cognition.join_by_i_sql`, 63 of `join_by_i_time_sql`, and
389
+ * 40+ each of six more objects — 246 KB / 8,025 lines grown to 361 KB / 11,785,
390
+ * with 8,604 duplicate non-comment lines. A client relaunching the same "add X"
391
+ * mission dozens of times, against a file the agent could only see 3% of, will
392
+ * re-add X every time. Worse, every cheap check read as SUCCESS: the six lateral
393
+ * joins the mission targeted really were gone.
394
+ *
395
+ * Only the FIRST new duplicate is named. A refusal listing sixty of them is a
396
+ * wall the model will not act on.
397
+ */
398
+ export declare function duplicateDefinitionRefusal(path: string, before: string, after: string): string | null;
322
399
  /**
323
400
  * Add an accepted contracts epic's files to the live contract lock, skipping
324
401
  * files other machinery owns (manifests/lockfiles stay editable so later
@@ -1 +1 @@
1
- {"version":3,"file":"agentic-executor.d.ts","sourceRoot":"","sources":["../../src/delivery/agentic-executor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agentic-executor.d.ts","sourceRoot":"","sources":["../../src/delivery/agentic-executor.ts"],"names":[],"mappings":"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"}
@@ -118,6 +118,77 @@ export function readFileWindow(contents, opts = {}) {
118
118
  return (`${body}\n\n… showing lines ${startLine}-${endLine} of ${totalLines} ` +
119
119
  `(${contents.length} bytes total).${more}`);
120
120
  }
121
+ /** Bytes a single `list_dir` / `run_bash` reply may occupy. */
122
+ export const TOOL_OUTPUT_MAX_BYTES = 4_000;
123
+ /**
124
+ * One window of a directory listing, plus what it left out.
125
+ *
126
+ * Same defect as `read_file` had, in a different costume: `readdirSync(...)
127
+ * .join('\n').slice(0, 4000)` cut mid-name and said nothing, so a large
128
+ * directory was silently unlistable past its first ~200 entries and the agent
129
+ * had no way to learn the rest existed.
130
+ *
131
+ * Truncates on ENTRY boundaries — half a filename is worse than no filename,
132
+ * because the model will confidently `read_file` it — and takes the same
133
+ * 1-based `offset` as `read_file` so a big directory is reachable rather than
134
+ * merely honest about being cut.
135
+ */
136
+ export function listingWindow(entries, opts = {}) {
137
+ const maxBytes = Math.max(1, opts.maxBytes ?? TOOL_OUTPUT_MAX_BYTES);
138
+ const total = entries.length;
139
+ const rawOffset = Number(opts.offset);
140
+ const start = Number.isFinite(rawOffset) ? Math.max(1, Math.trunc(rawOffset)) : 1;
141
+ if (total === 0)
142
+ return '';
143
+ if (start > total) {
144
+ return `NOTE: offset ${start} is past the end of ${opts.path ?? 'this directory'} (${total} entries).`;
145
+ }
146
+ const shown = [];
147
+ let used = 0;
148
+ for (const entry of entries.slice(start - 1)) {
149
+ const cost = entry.length + 1;
150
+ if (shown.length > 0 && used + cost > maxBytes)
151
+ break;
152
+ shown.push(entry);
153
+ used += cost;
154
+ }
155
+ const end = start + shown.length - 1;
156
+ const body = shown.join('\n');
157
+ if (start === 1 && end >= total)
158
+ return body;
159
+ const next = end + 1;
160
+ const more = next <= total
161
+ ? ` Call list_dir with {"path":"${opts.path ?? '.'}","offset":${next}} for the rest.`
162
+ : '';
163
+ return `${body}\n\n… showing entries ${start}-${end} of ${total}.${more}`;
164
+ }
165
+ /**
166
+ * A command's output, clipped from the MIDDLE rather than the end.
167
+ *
168
+ * `${stdout}${stderr}`.slice(0, 4000) keeps the head — which is the worst half
169
+ * to keep. A failing build puts its summary at the END ("error: could not
170
+ * compile", "3 failed, 41 passed", the exit line); head-only truncation hands
171
+ * the model a wall of compiler noise with the verdict cut off, and it cannot
172
+ * ask again because command output is not re-fetchable the way a file is.
173
+ *
174
+ * So both ends: the head carries the FIRST error (the one that usually causes
175
+ * the rest) and the invocation, the tail carries the verdict. The elision is
176
+ * stated in the middle where it happened, because a gap the reader cannot see
177
+ * is a gap the reader will read straight across.
178
+ */
179
+ export function clipCommandOutput(output, maxBytes = TOOL_OUTPUT_MAX_BYTES) {
180
+ if (output.length <= maxBytes)
181
+ return output;
182
+ const budget = Math.max(1, maxBytes);
183
+ // Tail-weighted: the verdict is worth more than the preamble, but the first
184
+ // error still has to survive.
185
+ const headBytes = Math.floor(budget * 0.4);
186
+ const tailBytes = budget - headBytes;
187
+ const head = output.slice(0, headBytes);
188
+ const tail = output.slice(output.length - tailBytes);
189
+ const elided = output.length - head.length - tail.length;
190
+ return `${head}\n\n… [${elided} bytes of output elided — this is the START and the END of ${output.length} bytes] …\n\n${tail}`;
191
+ }
121
192
  /** Tool-call rounds before a final answer is forced (knob `UAP_MAX_TOOL_ROUNDS`). */
122
193
  export function defaultMaxToolRounds() {
123
194
  const set = (process.env.UAP_MAX_TOOL_ROUNDS ?? '').trim();
@@ -251,10 +322,17 @@ const TOOLS = [
251
322
  type: 'function',
252
323
  function: {
253
324
  name: 'list_dir',
254
- description: 'List files/dirs at a path relative to the project root.',
325
+ description: 'List files/dirs at a path relative to the project root. A large directory comes ' +
326
+ 'back one window at a time and the reply says so — pass offset (1-based entry) for the rest.',
255
327
  parameters: {
256
328
  type: 'object',
257
- properties: { path: { type: 'string' } },
329
+ properties: {
330
+ path: { type: 'string' },
331
+ offset: {
332
+ type: 'integer',
333
+ description: '1-based entry to start at. Omit for the start of the listing.',
334
+ },
335
+ },
258
336
  required: ['path'],
259
337
  },
260
338
  },
@@ -371,6 +449,149 @@ function editGuttingRefusal(path, before, after) {
371
449
  'that removes most of the file. If you meant to delete code, do it in smaller, ' +
372
450
  'reviewable edits; if you meant to replace it, send the replacement body.');
373
451
  }
452
+ /**
453
+ * Top-level definition HEADERS in a file, counted.
454
+ *
455
+ * Keyed on the whole header line, not the name, and that distinction is the
456
+ * whole design. Overloading is legal and common — Postgres `f(int)` beside
457
+ * `f(text)`, a TypeScript overload set — and those have DIFFERENT headers. An
458
+ * appended copy has an IDENTICAL one. Counting names would refuse the first and
459
+ * miss nothing extra; counting headers refuses only the copy.
460
+ *
461
+ * Column 0 only. A method inside a class or a nested helper is indented, and
462
+ * two classes may legitimately both define `run`. Top-level definitions are
463
+ * where re-adding is unambiguous.
464
+ *
465
+ * An unrecognised extension yields nothing, so the guard is inert rather than
466
+ * guessing at a language it does not know.
467
+ */
468
+ export function topLevelDefinitionHeaders(content, path) {
469
+ const ext = (path.split('.').pop() ?? '').toLowerCase();
470
+ const patterns = {
471
+ sql: [
472
+ /^CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?(?:FUNCTION|PROCEDURE|VIEW|TABLE|TYPE|INDEX)\b.*/i,
473
+ ],
474
+ ts: [TS_DEF, TS_CONST],
475
+ tsx: [TS_DEF, TS_CONST],
476
+ js: [TS_DEF, TS_CONST],
477
+ jsx: [TS_DEF, TS_CONST],
478
+ mjs: [TS_DEF, TS_CONST],
479
+ py: [/^(?:async\s+)?(?:def|class)\s+\w+.*/],
480
+ rs: [/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:fn|struct|enum|trait)\s+\w+.*/],
481
+ go: [/^func\s+\w+.*/, /^type\s+\w+\s+.*/],
482
+ }[ext];
483
+ if (!patterns)
484
+ return new Map();
485
+ const counts = new Map();
486
+ const lines = content.split('\n');
487
+ for (let i = 0; i < lines.length; i++) {
488
+ const raw = lines[i];
489
+ // Column 0: no leading whitespace.
490
+ if (raw !== raw.trimStart())
491
+ continue;
492
+ const line = raw.trim();
493
+ // No comment filter: every pattern is anchored at ^, so `-- CREATE …`,
494
+ // `// export function …` and `# def …` cannot match in the first place.
495
+ // A filter here survived mutation because it could never change an outcome.
496
+ if (!line)
497
+ continue;
498
+ for (const re of patterns) {
499
+ if (!re.test(line))
500
+ continue;
501
+ counts.set(signatureKey(lines, i), (counts.get(signatureKey(lines, i)) ?? 0) + 1);
502
+ break;
503
+ }
504
+ }
505
+ return counts;
506
+ }
507
+ /** How many lines of a wrapped signature to gather before giving up. */
508
+ const SIGNATURE_MAX_LINES = 24;
509
+ /**
510
+ * The WHOLE signature, not just the line the definition starts on.
511
+ *
512
+ * Found by running this over the repo itself: `src/benchmarks/model-integration.ts`
513
+ * declares `runModelBenchmark` three times — a legitimate TypeScript overload
514
+ * set — and two of those start with the identical line
515
+ * `export async function runModelBenchmark(`, because the parameters wrap. Keying
516
+ * on that first line alone would call a real overload a duplicate and refuse a
517
+ * legitimate write. The parameters are exactly what distinguishes overloads, so
518
+ * the key has to reach them.
519
+ *
520
+ * Gathers until the parentheses balance, bounded, so a malformed or enormous
521
+ * signature degrades to "just this line" instead of swallowing the file.
522
+ */
523
+ function signatureKey(lines, start) {
524
+ let text = lines[start].trim();
525
+ let depth = countParens(text);
526
+ for (let j = start + 1; depth > 0 && j < lines.length && j - start < SIGNATURE_MAX_LINES; j++) {
527
+ text += ` ${lines[j].trim()}`;
528
+ depth += countParens(lines[j]);
529
+ }
530
+ // Collapse whitespace so reformatting is not read as a different signature,
531
+ // and drop a trailing brace/semicolon so `fn f() {` matches `fn f()`.
532
+ return text.replace(/\s+/g, ' ').replace(/[{;]\s*$/, '').trim();
533
+ }
534
+ function countParens(s) {
535
+ let d = 0;
536
+ for (const ch of s) {
537
+ if (ch === '(')
538
+ d += 1;
539
+ else if (ch === ')')
540
+ d -= 1;
541
+ }
542
+ return d;
543
+ }
544
+ const TS_DEF = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class)\s+\w+.*/;
545
+ const TS_CONST = /^(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?(?:\(|function\b|class\b).*/;
546
+ /** Escape hatch for a file that genuinely repeats a header (generated code). */
547
+ export function duplicateDefinitionGuardDisabled() {
548
+ return process.env.UAP_DELIVER_ALLOW_DUPLICATE_DEFS === '1';
549
+ }
550
+ /**
551
+ * Refuse a write that RE-ADDS a definition the file already has.
552
+ *
553
+ * The guards around this one all watch a file get smaller — anti-gutting
554
+ * (bytes), stub-substance (empty bodies), byte-identical NO-OP. Nothing watched
555
+ * it get BIGGER by repetition, and that is the shape the damage actually took.
556
+ *
557
+ * Live (cognition-engine, 2026-08-11): setup.sql ended the day holding 65 copies
558
+ * of `CREATE FUNCTION cognition.join_by_i_sql`, 63 of `join_by_i_time_sql`, and
559
+ * 40+ each of six more objects — 246 KB / 8,025 lines grown to 361 KB / 11,785,
560
+ * with 8,604 duplicate non-comment lines. A client relaunching the same "add X"
561
+ * mission dozens of times, against a file the agent could only see 3% of, will
562
+ * re-add X every time. Worse, every cheap check read as SUCCESS: the six lateral
563
+ * joins the mission targeted really were gone.
564
+ *
565
+ * Only the FIRST new duplicate is named. A refusal listing sixty of them is a
566
+ * wall the model will not act on.
567
+ */
568
+ export function duplicateDefinitionRefusal(path, before, after) {
569
+ if (duplicateDefinitionGuardDisabled())
570
+ return null;
571
+ const now = topLevelDefinitionHeaders(after, path);
572
+ // Fast path only — the loop below is already a no-op on an empty map. It
573
+ // earns its place by skipping the second parse, not by changing an outcome,
574
+ // which is why mutating it away leaves every test passing.
575
+ if (now.size === 0)
576
+ return null;
577
+ const was = topLevelDefinitionHeaders(before, path);
578
+ for (const [header, count] of now) {
579
+ const prior = was.get(header) ?? 0;
580
+ // Adding a brand-new definition (0 -> 1) is the normal case. Editing one in
581
+ // place (1 -> 1) is fine. Going 1 -> 2 is re-adding what is already there.
582
+ if (count <= prior || count < 2)
583
+ continue;
584
+ const firstLine = before.split('\n').findIndex((l) => l === l.trimStart() && l.trim().replace(/\s+/g, ' ').replace(/[{;]\s*$/, '').trim() === header);
585
+ const where = firstLine >= 0 ? ` at line ${firstLine + 1}` : '';
586
+ return (`ERROR: refusing to write ${path} — it would add a SECOND copy of a definition the file ` +
587
+ `already contains${where}:\n\n ${header.slice(0, 160)}\n\n` +
588
+ `The file already has this (${prior} ${prior === 1 ? 'copy' : 'copies'}; this write makes ${count}). ` +
589
+ `Do not append it again — use edit_file to change the existing one in place, or read the file ` +
590
+ `first to see what is already there. If this file genuinely repeats that header, set ` +
591
+ `UAP_DELIVER_ALLOW_DUPLICATE_DEFS=1.`);
592
+ }
593
+ return null;
594
+ }
374
595
  /** Resolve a model-supplied path inside the project root, refusing escapes. */
375
596
  function safePath(projectRoot, p) {
376
597
  const abs = isAbsolute(p) ? p : resolve(projectRoot, p);
@@ -584,7 +805,9 @@ export function repeatReadNote(cache, projectRoot, name, args, round) {
584
805
  // page 1, and scolding a model for paging through a long file would push it
585
806
  // straight back into the read-loop this is meant to end. Only a genuine
586
807
  // repeat — same path, same window, unchanged on disk — earns the note.
587
- const offsetKey = name === 'read_file' && Number.isFinite(Number(args.offset))
808
+ // Both windowed tools, not just read_file: list_dir pages too now, and a
809
+ // directory's second page is no more a re-read than a file's.
810
+ const offsetKey = Number.isFinite(Number(args.offset))
588
811
  ? `@${Math.max(1, Math.trunc(Number(args.offset)))}`
589
812
  : '';
590
813
  const key = `${name}:${args.path}${offsetKey}`;
@@ -641,11 +864,9 @@ sweep = disabledSweep()) {
641
864
  // see what is in there. Serve that intent instead of failing, and name the
642
865
  // right tool for next time. A wasted turn becomes a useful one.
643
866
  if (statSync(abs).isDirectory()) {
644
- const entries = readdirSync(abs)
867
+ const entries = listingWindow(readdirSync(abs)
645
868
  .filter((e) => !AGENT_INTERNAL_DIRS.includes(e))
646
- .map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e))
647
- .join('\n')
648
- .slice(0, 4000);
869
+ .map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e)), { path: String(args.path) });
649
870
  return (`NOTE: '${String(args.path)}' is a DIRECTORY, not a file — use list_dir for it. ` +
650
871
  `Its contents:\n${entries}`);
651
872
  }
@@ -661,13 +882,11 @@ sweep = disabledSweep()) {
661
882
  return internal;
662
883
  if (!existsSync(abs))
663
884
  return `ERROR: not found: ${args.path}`;
664
- return readdirSync(abs)
885
+ return listingWindow(readdirSync(abs)
665
886
  // Also HIDE the internal dirs from any listing (e.g. the project root), so
666
887
  // the agent never even sees `.uap/` and is not tempted to spelunk it.
667
888
  .filter((e) => !AGENT_INTERNAL_DIRS.includes(e))
668
- .map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e))
669
- .join('\n')
670
- .slice(0, 4000);
889
+ .map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e)), { offset: args.offset, path: String(args.path ?? '.') });
671
890
  }
672
891
  if (name === 'write_file') {
673
892
  const abs = safePath(projectRoot, String(args.path));
@@ -774,6 +993,17 @@ sweep = disabledSweep()) {
774
993
  if (stub.isStub)
775
994
  return stubRefusal(String(args.path), stub);
776
995
  }
996
+ // Growth by repetition. Ordered LAST among the write guards: it is the
997
+ // only one that reads the whole prior file for structure, so it never
998
+ // pays that cost for a write another guard has already refused.
999
+ if (existsSync(abs)) {
1000
+ try {
1001
+ const dup = duplicateDefinitionRefusal(String(args.path), readFileSync(abs, 'utf-8'), String(args.content ?? ''));
1002
+ if (dup)
1003
+ return dup;
1004
+ }
1005
+ catch { /* unreadable — a guard that cannot read cannot judge */ }
1006
+ }
777
1007
  mkdirSync(dirname(abs), { recursive: true });
778
1008
  writeFileSync(abs, String(args.content ?? ''), 'utf-8');
779
1009
  recordAuthorisedWrite(sweep, rel, String(args.content ?? ''));
@@ -903,6 +1133,12 @@ sweep = disabledSweep()) {
903
1133
  if (stub.isStub)
904
1134
  return stubRefusal(String(args.path), stub);
905
1135
  }
1136
+ // An edit can append just as easily as a write can — an old_string anchored
1137
+ // at the end of the file with the whole definition in new_string is exactly
1138
+ // how a file grows a second copy.
1139
+ const dupEdit = duplicateDefinitionRefusal(String(args.path), current, updated);
1140
+ if (dupEdit)
1141
+ return dupEdit;
906
1142
  writeFileSync(abs, updated, 'utf-8');
907
1143
  recordAuthorisedWrite(sweep, rel, updated);
908
1144
  const rustCheckNote = maybeRustWriteCheck(projectRoot, rel);
@@ -1017,7 +1253,7 @@ sweep = disabledSweep()) {
1017
1253
  /* best effort */
1018
1254
  }
1019
1255
  }
1020
- const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.slice(0, 4000);
1256
+ const out = clipCommandOutput(`${r.stdout ?? ''}${r.stderr ?? ''}`);
1021
1257
  const note = (restored.length > 0
1022
1258
  ? `\n[blocked: restored ${restored.length} protected file(s) your command modified]`
1023
1259
  : '') +