Remove the duplicate model-facing list tool from this branch; directory orientation remains available through bash ls. Keep the glob sampling fix, add a real ACP composition snapshot, and narrow the decision record to the shipped bug fix.
Master had advanced 392 commits. Every conflict was a derived artifact both
sides had regenerated — `docs/tool-catalog.md`, four recorded headless
session logs, and one TUI terminal expectation — so each was resolved by
taking master's version and re-running the generator and the keyless
snapshot refresh on the merged tree. No source file conflicted.
The new `examples/jsonrpc-agent` fixtures keep master's scrubbed
`{{system}}` / `{{tools}}` headers: that suite scrubs request headers, so a
new tool changes nothing there and the refresh output was discarded.
The review made every name a JSON string with `<`, `>`, and `&` escaped. The
hazards behind that are real and each is now covered: a control character
splits one entry across lines, `</` closes the envelope, and a regular file
named `x@` reads as a socket named `x` under the non-regular marker.
Quote those, and only those. `list` is the tool an agent reaches for first
and its output sits in every transcript, so `"archive"/` on every ordinary
line is a permanent cost for a case that almost never occurs. A name is now
emitted verbatim unless it matches a control character, a leading quote, a
backslash, `</`, or a trailing `@`, and is otherwise a JSON string with `</`
neutralized — the delimiter treatment `dsh-workspace-context` already applies
to instruction text, extended to an interpolated path as its
`instruction-frame-paths` TODO asks.
Agent Note for the two changes: what the session log showed, what ordering
can and cannot fix (measured, correcting the first diagnosis), why the page
is sampled only past the cap, why `list` is needed alongside it, and the
alternatives each one beat.
Asked what a workspace contained, an agent described one subfolder as the
whole project. `glob {"pattern": "*"}` matched 10030 paths across 22
top-level entries and the inline page was the first 100 of them, all under a
single unpacked archive.
Three properties compose into that page: a pattern with no `/` matches
basenames at any depth, so `*` means the whole tree rather than its top
level; `--sort=modified` orders oldest first, and unpacking an archive
restores timestamps that predate everything the user wrote; and the page was
the head of that order. Each is defensible alone, and together they make the
most ordinary request an agent receives produce a confident wrong answer.
A result within `globMaxResults` is unchanged — shown whole, in
modification-time order. Beyond it the page is filled round-robin across the
complete result's top-level entries, so one subtree cannot own every slot,
and the footer states that the page was sampled rather than taken in
modification-time order. Measured on a 24-entry, 716-file reproduction, the
head of 100 reaches 7 top-level names and the sampled page reaches 21. The
spill artifact still holds the complete sorted list.
The guidance and schema stop steering away from `ls`, state the any-depth
pattern rule, say results are files and never directories, and point at
`list` for a directory's contents.
`ctx.fs.listDir` has shipped since the filesystem seam gained it, with skill
discovery as its only consumer; the model-facing tool was deferred to a
separate decision. Nothing else could answer "what is in this directory":
`rg --files` backs glob and grep and never emits a directory entry, so an
empty directory is invisible, no output says which names are directories,
and no output gives an entry count.
`list` takes an optional `path`, defaulting to the session workspace so the
common question needs no argument, and returns the direct children of one
directory with their type. Two presentation rules carry it: directories sort
first, then files, then non-regular children, each alphabetically — so
truncation loses leaves rather than the tree — and the footer always states
the complete listing's size and composition, so a capped view can never read
as a whole directory.
It emits no `fs/observed`: seeing a filename is not reading a file, and a
listing must never satisfy the read-before-write gate.