{
  "shell.native_background": {
    "score": 0,
    "evidence": "bash is a single blocking spawn that resolves only on exit/timeout/abort (packages/coding-agent/src/core/tools/bash.ts:99-142), and its schema has only command/timeout (packages/coding-agent/src/core/tools/bash.ts:42-45). No task id or background flag. packages/coding-agent/README.md:509 states 'No background bash'; grep 'background' across packages/coding-agent/src hits only TUI theme detection and the Ctrl-Z suspend binding (modes/interactive/interactive-mode.ts:4076-4086)."
  },
  "shell.timeout_cap": {
    "score": 0.5,
    "evidence": "Model-settable timeout with a hard cap but no default: packages/coding-agent/src/core/tools/bash.ts:26-40 caps at 2_147_483_647 ms and rejects non-positive values, and the schema says 'no default timeout' (packages/coding-agent/src/core/tools/bash.ts:44); resolveTimeoutMs returns undefined when omitted (packages/coding-agent/src/core/tools/bash.ts:29-31). No bash timeout default exists in packages/coding-agent/src/core/settings-manager.ts (only httpIdleTimeoutMs and provider retry timeouts)."
  },
  "shell.persistent": {
    "score": 0,
    "evidence": "Each call spawns a fresh shell with -c and a fixed cwd: createLocalShellOperations spawns shellConfig.shell (packages/coding-agent/src/core/tools/bash.ts:91-105); the cwd is captured when the tool is created (packages/coding-agent/src/core/tools/bash.ts:338-343) and no state survives the call. grep 'persistent|interactive shell' across src finds no shell session; examples/extensions/interactive-shell.ts is a non-default example."
  },
  "shell.powershell": {
    "score": 0.5,
    "evidence": "A real PowerShell tool exists (packages/coding-agent/src/core/tools/powershell.ts:39-61; packages/coding-agent/src/utils/shell.ts:122-136 resolves pwsh/powershell) but it is off by default: defaultActiveToolNames is ['read','bash','edit','write'] (packages/coding-agent/src/core/sdk.ts:256), so powershell is registered (packages/coding-agent/src/core/tools/index.ts:95-105,182-193) yet inactive unless --tools/defaultTools opts in. It is also Windows-only (packages/coding-agent/src/utils/shell.ts:125-128)."
  },
  "shell.middle_out": {
    "score": 0,
    "evidence": "Tail-only: OutputAccumulator.snapshot calls truncateTail (packages/coding-agent/src/core/tools/output-accumulator.ts:91-95); truncate.ts only has truncateHead and truncateTail (packages/coding-agent/src/core/tools/truncate.ts:78-241); the bash banner keeps the last lines and says 'Showing lines X-Y of N' (packages/coding-agent/src/core/tools/bash.ts:432-441). No head+tail formatter."
  },
  "shell.full_log": {
    "score": 1,
    "evidence": "Truncated bash output is streamed to a temp file (packages/coding-agent/src/core/tools/output-accumulator.ts:205-221) and the path is returned both in the model-visible text and details.fullOutputPath (packages/coding-agent/src/core/tools/bash.ts:54-57,431-441). Test asserts the file exists and contains the first and last lines (packages/coding-agent/test/tools.test.ts:729-750)."
  },
  "shell.omitted_counts": {
    "score": 1,
    "evidence": "The banner gives the exact shown/total range plus the recovery path: '[Showing lines ${startLine}-${endLine} of ${totalLines}. Full output: ${path}]' (packages/coding-agent/src/core/tools/bash.ts:432-441); the byte variant also names the 50KB limit (packages/coding-agent/src/core/tools/bash.ts:439-440). Test pins the format (packages/coding-agent/test/tools.test.ts:737-747)."
  },
  "shell.signal_honesty": {
    "score": 0,
    "evidence": "Signal death is reported as success: waitForChildProcess resolves Node's raw exit code, which is null on signal death (packages/coding-agent/src/utils/child-process.ts:72-79,116-127), and the bash tool only throws when exitCode !== 0 && exitCode !== null (packages/coding-agent/src/core/tools/bash.ts:473-475), so a SIGKILLed/SIGTERMed command returns its partial output as a normal result. No 128+N mapping exists anywhere."
  },
  "shell.benign_exit": {
    "score": 0,
    "evidence": "Every non-zero exit is thrown as an error 'Command exited with code N' (packages/coding-agent/src/core/tools/bash.ts:473-475). grep across packages/coding-agent/src/core/tools finds no grep/diff/test expected-failure annotation."
  },
  "shell.untrusted_fencing": {
    "score": 0,
    "evidence": "Output is placed verbatim into the result text (packages/coding-agent/src/core/tools/bash.ts:426-443,471-476); the only bracketed text is the truncation footer. No inert-data delimiters. docs/security.md:33 explicitly treats prompt injection from build output as expected local-agent risk."
  },
  "shell.bg_output_reader": {
    "score": 0,
    "evidence": "No model-facing job object: bash blocks on a single spawn (packages/coding-agent/src/core/tools/bash.ts:99-142), the builtin tool list has no task/job tool (packages/coding-agent/src/core/tools/index.ts:95-105,182-193), and packages/coding-agent/README.md:509 states 'No background bash'. No job-output reader exists in packages/coding-agent/src."
  },
  "shell.wait_modes": {
    "score": 0,
    "evidence": "No job wait API: bash returns only when the single spawned process exits, times out, or aborts (packages/coding-agent/src/core/tools/bash.ts:117-142); no wait-now / wait-for-output / wait-for-exit tool exists in the registry (packages/coding-agent/src/core/tools/index.ts:95-105,182-193)."
  },
  "shell.cursor_reads": {
    "score": 0,
    "evidence": "No background stream to read: OutputAccumulator is a per-call rolling buffer that exists only for the duration of the call (packages/coding-agent/src/core/tools/output-accumulator.ts:35-62) and is never addressable by the model, let alone by byte cursor."
  },
  "shell.list_tasks": {
    "score": 0,
    "evidence": "No live-job registry and no list tool: the only builtin tools are read/bash/powershell/edit/write/grep/find/ls (packages/coding-agent/src/core/tools/index.ts:95-105,182-193), and packages/coding-agent/README.md:509 confirms there is no background bash."
  },
  "shell.kill_id": {
    "score": 0,
    "evidence": "No model-invokable cancel: there is no job/task tool in the builtin registry (packages/coding-agent/src/core/tools/index.ts:95-105,182-193); killProcessTree is internal abort/timeout cleanup only (packages/coding-agent/src/core/tools/bash.ts:113-124)."
  },
  "shell.kill_pid_port": {
    "score": 0,
    "evidence": "No kill tool at all, so neither raw pid nor listening-port lookup exists: the builtin registry is read/bash/powershell/edit/write/grep/find/ls (packages/coding-agent/src/core/tools/index.ts:95-105,182-193), and killProcessTree is internal cleanup (packages/coding-agent/src/utils/shell.ts:216-246)."
  },
  "shell.sigterm_escalation": {
    "score": 0,
    "evidence": "killProcessTree sends SIGKILL immediately on Unix (packages/coding-agent/src/utils/shell.ts:234-245) and taskkill /F /T on Windows (packages/coding-agent/src/utils/shell.ts:216-233). There is no SIGTERM, grace poll, then SIGKILL escalation."
  },
  "shell.group_kill": {
    "score": 1,
    "evidence": "Children are spawned detached into their own process group (packages/coding-agent/src/core/tools/bash.ts:99-105, detached: process.platform !== 'win32'), and killProcessTree signals -pid of that spawned child, falling back to pid (packages/coding-agent/src/utils/shell.ts:216-246). The shutdown sweep iterates only pids pi itself spawned and tracked (packages/coding-agent/src/utils/shell.ts:196-211; packages/coding-agent/src/core/tools/bash.ts:110,144), so it never signals a foreign group."
  },
  "shell.monitor": {
    "score": 0,
    "evidence": "No scheduler or output-pattern watcher: the only bash timers are the timeout (packages/coding-agent/src/core/tools/bash.ts:117-124) and the 100ms TUI update throttle (packages/coding-agent/src/core/tools/bash.ts:211-212,391-404). No runtime wake-up path exists."
  },
  "shell.sleep": {
    "score": 0,
    "evidence": "No sleep tool in the builtin registry (packages/coding-agent/src/core/tools/index.ts:95-105,182-193); grep for sleep/schedule finds no model-facing parking primitive, only internal setTimeout throttles."
  },
  "shell.cron": {
    "score": 0,
    "evidence": "No durable scheduling: grep 'cron' across packages/coding-agent/src and packages/agent/src returns nothing, and sessions persist messages only (packages/coding-agent/src/core/session-manager.ts)."
  },
  "shell.bg_subagents": {
    "score": 0,
    "evidence": "No sub-agent support at all: packages/coding-agent/README.md:501 states 'No sub-agents', and no task/subagent tool exists in the builtin registry (packages/coding-agent/src/core/tools/index.ts:95-105,182-193)."
  },
  "shell.safe_classification": {
    "score": 0,
    "evidence": "No AST-based command classification or permission layer in the default tools: packages/coding-agent/README.md:503 states 'No permission popups', packages/coding-agent/docs/usage.md:309 says permission popups are intentionally absent, and the only allowlist is an opt-in plan-mode example (packages/coding-agent/examples/extensions/plan-mode/index.ts:9,171,212), not the default configuration."
  },
  "shell.workspace_boundary": {
    "score": 0,
    "evidence": "No outside-workspace detection or gate: docs/security.md:33 states 'Pi does not include a built-in sandbox', and project trust only gates resource loading, not tool paths (docs/security.md:5-9). Bash runs with the session cwd and no boundary check (packages/coding-agent/src/core/tools/bash.ts:91-105)."
  },
  "read.line_window": {
    "score": 1,
    "evidence": "Default 2000-line window: DEFAULT_MAX_LINES = 2000 (packages/coding-agent/src/core/tools/truncate.ts:11-12), applied by truncateHead in packages/coding-agent/src/core/tools/read.ts:295; the test pins 2000 lines plus the next-offset note (packages/coding-agent/test/tools.test.ts:88-100)."
  },
  "read.second_ceiling": {
    "score": 1,
    "evidence": "50KB byte ceiling in addition to the line window: DEFAULT_MAX_BYTES = 50*1024 (packages/coding-agent/src/core/tools/truncate.ts:12), enforced inside truncateHead (packages/coding-agent/src/core/tools/truncate.ts:126-137) and reported by read as the byte-limit variant (packages/coding-agent/src/core/tools/read.ts:307-311). Test covers a 500-line file over 50KB (packages/coding-agent/test/tools.test.ts:102-114)."
  },
  "read.per_line_clamp": {
    "score": 0,
    "evidence": "No per-line clamp: read emits whole lines (packages/coding-agent/src/core/tools/read.ts:284-295). truncateHead either drops a first line that alone exceeds the byte budget (packages/coding-agent/src/core/tools/truncate.ts:103-119; surfaced as a sed fallback at packages/coding-agent/src/core/tools/read.ts:297-301) or stops before it. The only per-line truncator is truncateLine, used by grep (packages/coding-agent/src/core/tools/truncate.ts:268-276; grep.ts:331), not read."
  },
  "read.line_prefixes": {
    "score": 0,
    "evidence": "No line numbers are added: outputText is truncation.content verbatim (packages/coding-agent/src/core/tools/read.ts:306,320), and the test asserts the output equals the file content exactly (packages/coding-agent/test/tools.test.ts:77-80). Numbering exists only in grep results (grep.ts:331)."
  },
  "read.resume_offset": {
    "score": 1,
    "evidence": "The truncation note names the next offset directly: 'Use offset=${nextOffset} to continue.' for both line and byte truncation (packages/coding-agent/src/core/tools/read.ts:304-311) and for a user-limited window (packages/coding-agent/src/core/tools/read.ts:313-317). Test pins 'Use offset=2001' (packages/coding-agent/test/tools.test.ts:99)."
  },
  "read.empty_file_note": {
    "score": 0,
    "evidence": "An empty file returns an empty text block, not a note: buffer.split('\\n') yields [''], startLine 0 is in range, truncateHead('') is not truncated, so outputText is '' (packages/coding-agent/src/core/tools/read.ts:273-276,295,318-321). No empty-file or EOF marker exists in read.ts."
  },
  "read.eof_note": {
    "score": 0.5,
    "evidence": "Past-EOF is detected and the total is given, but as a thrown Error: 'Offset ${offset} is beyond end of file (${allLines.length} lines total)' (packages/coding-agent/src/core/tools/read.ts:281-283; packages/coding-agent/test/tools.test.ts:164-171). The definition wants a recovery note, so partial only."
  },
  "read.streaming": {
    "score": 0,
    "evidence": "Whole-file buffering: readFile returns the entire Buffer, which is decoded and split in memory (packages/coding-agent/src/core/tools/read.ts:273-276); there is no stream or memory cap. The alternate harness is the same (packages/agent/src/harness/tools/read.ts:97-98)."
  },
  "read.deferred_chunk": {
    "score": 0.5,
    "evidence": "Totals are exact because the whole file is buffered (packages/coding-agent/src/core/tools/read.ts:275-276,295), so the 'more remains' notes are never speculative (packages/coding-agent/src/core/tools/read.ts:304-317); but there is no chunked/streaming truncation to defer at, since it pays for that honesty by loading the file first (packages/coding-agent/src/core/tools/read.ts:273-275). Partial."
  },
  "read.dedup": {
    "score": 0,
    "evidence": "No unchanged-read cache: read always re-reads and returns the content (packages/coding-agent/src/core/tools/read.ts:273-322). grep for unchanged/dedup in packages/coding-agent/src/core/tools finds no read-result cache; dedupe hits are diagnostics/compaction/package dedupe only."
  },
  "read.ledger": {
    "score": 0,
    "evidence": "Writes never consult reads: write overwrites unconditionally (write.ts:206-232), and edit matches oldText against a fresh readFile (edit.ts:357-372). No read ledger exists."
  },
  "read.partial_ledger": {
    "score": 0,
    "evidence": "No record of partial views: read.ts does not persist the window it returned (packages/coding-agent/src/core/tools/read.ts:223-335), so write/edit cannot refuse a destructive overwrite of a partially seen file (write.ts:218-230)."
  },
  "read.did_you_mean": {
    "score": 0,
    "evidence": "A miss fails with the raw ENOENT from ops.access (packages/coding-agent/src/core/tools/read.ts:247-249); resolveReadPathAsync tries unicode-equivalent spellings only, never candidate filenames (path-utils.ts:86-118). grep 'did you mean' in src hits only package-manager.ts:1397-1401."
  },
  "read.unicode_retry": {
    "score": 1,
    "evidence": "Known-equivalent filename spellings are retried before failing: narrow no-break-space AM/PM, NFD, curly apostrophe, and NFD+curly (path-utils.ts:52-118), used by read at packages/coding-agent/src/core/tools/read.ts:245,250."
  },
  "read.confusables": {
    "score": 0,
    "evidence": "No zero-width/bidi disclosure in model-facing content: read returns the decoded text unchanged (packages/coding-agent/src/core/tools/read.ts:273-322). sanitizeBinaryOutput is applied only to bash capture and TUI rendering (packages/coding-agent/src/utils/shell.ts:160-190; packages/coding-agent/src/core/tools/render-utils.ts:48), not the read result."
  },
  "read.image_vision": {
    "score": 1,
    "evidence": "Images are sniffed, optionally resized, and returned as a text note plus {type:'image', data, mimeType} (packages/coding-agent/src/core/tools/read.ts:250-270; packages/coding-agent/src/utils/image-process.ts:86-119). Default autoResizeImages=true (packages/coding-agent/src/core/tools/read.ts:65-66,213)."
  },
  "read.coord_scale": {
    "score": 1,
    "evidence": "Resized images disclose the mapping: '[Image: original WxH, displayed at wxh. Multiply coordinates by <scale> to map to original image.]' (packages/coding-agent/src/utils/image-resize.ts:116-122), appended as a hint to the read result (packages/coding-agent/src/utils/image-process.ts:104-108; packages/coding-agent/src/core/tools/read.ts:263-265)."
  },
  "read.magic_bytes": {
    "score": 1,
    "evidence": "Media type comes from bytes only: detectSupportedImageMimeType checks JPEG/PNG/GIF/WEBP/BMP magic (packages/coding-agent/src/utils/mime.ts:6-23) against the first 4100 bytes (mime.ts:25-34), with no extension fallback. A text file named .png is read as text (packages/coding-agent/test/tools.test.ts:228-238)."
  },
  "read.notebooks": {
    "score": 0,
    "evidence": "No .ipynb handling: grep for ipynb/notebook across packages finds nothing in source; .ipynb falls through to the plain-text path at packages/coding-agent/src/core/tools/read.ts:273-275."
  },
  "read.docx": {
    "score": 0,
    "evidence": "No .docx extraction: grep for docx/mammoth across packages/coding-agent/src and packages/agent/src finds nothing, and read decodes the raw bytes as UTF-8 (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.pptx": {
    "score": 0,
    "evidence": "No .pptx or speaker-note handling: grep for pptx across packages/coding-agent/src and packages/agent/src finds nothing; the file is decoded as plain text (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.xlsx": {
    "score": 0,
    "evidence": "No .xlsx or sheet/table rendering: grep for xlsx/spreadsheet across packages/coding-agent/src and packages/agent/src finds nothing; the file is decoded as plain text (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.odf": {
    "score": 0,
    "evidence": "No ODF (.odt/.ods/.odp) reader: grep for odt/odf/ods/odp across packages/coding-agent/src and packages/agent/src finds nothing; any such file is decoded as plain text (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.rtf_epub": {
    "score": 0,
    "evidence": "No RTF or EPUB handling: grep for rtf/epub across packages/coding-agent/src and packages/agent/src finds nothing; the bytes are decoded as UTF-8 text (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.pdf_text": {
    "score": 0,
    "evidence": "No PDF text extraction: grep for pdf across packages/coding-agent/src and packages/agent/src finds nothing; a PDF matches no image magic (packages/coding-agent/src/utils/mime.ts:6-23), so it is decoded as UTF-8 (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.scanned_pdf": {
    "score": 0,
    "evidence": "No scan detection, page naming, or rendering hint: there is no PDF path at all (grep pdf finds nothing in source); read treats the bytes as plain text (packages/coding-agent/src/core/tools/read.ts:273-274)."
  },
  "read.docs_windowed": {
    "score": 0,
    "evidence": "offset/limit apply only to the plain-text line split (packages/coding-agent/src/core/tools/read.ts:278-293); documents never become text, so they cannot honour the line window."
  },
  "read.docs_bulk": {
    "score": 0,
    "evidence": "read accepts a single path (packages/coding-agent/src/core/tools/read.ts:21-25) and executes on it once (packages/coding-agent/src/core/tools/read.ts:245); there is no multi-file read for documents to participate in."
  },
  "read.converter_on_demand": {
    "score": 0,
    "evidence": "No document converter exists to fetch: grep for mammoth/pdfjs/pandoc/libreoffice across packages finds nothing, and read has no conversion branch (packages/coding-agent/src/core/tools/read.ts:250-274). Nothing is lazily installed because conversion is absent."
  },
  "read.many_files": {
    "score": 0,
    "evidence": "read takes one path string (packages/coding-agent/src/core/tools/read.ts:21-25) and executes on it once (packages/coding-agent/src/core/tools/read.ts:245); multiple files require multiple calls."
  },
  "read.globs": {
    "score": 0,
    "evidence": "No glob expansion: the path is resolved/stat'ed literally (packages/coding-agent/src/core/tools/read.ts:245,248; path-utils.ts:86-118). Glob and .gitignore filtering live in separate find/grep tools, not read."
  },
  "read.per_file_range": {
    "score": 0,
    "evidence": "One offset/limit pair for the single path (packages/coding-agent/src/core/tools/read.ts:21-25,278-293); there is no per-entry range structure."
  },
  "read.exclusions_gitignore": {
    "score": 0,
    "evidence": "read has no exclude or gitignore parameters and no filtering step (packages/coding-agent/src/core/tools/read.ts:21-25,245-322)."
  },
  "read.aggregate_cap": {
    "score": 0,
    "evidence": "The 50KB/2000-line budget is per single file (packages/coding-agent/src/core/tools/truncate.ts:11-12; packages/coding-agent/src/core/tools/read.ts:295); there is no multi-file budget because read is single-file (packages/coding-agent/src/core/tools/read.ts:21-25)."
  },
  "read.aliased_input": {
    "score": 0,
    "evidence": "No argument repair for read: the schema accepts only path/offset/limit (packages/coding-agent/src/core/tools/read.ts:21-25) and execute destructures path (packages/coding-agent/src/core/tools/read.ts:225). Unlike edit, which defines prepareArguments for legacy oldText/newText and stringified edits (edit.ts:331 and edit.ts:116-146), read defines none (packages/coding-agent/src/core/tools/read.ts:215-222)."
  },
  "read.negative_offset": {
    "score": 0,
    "evidence": "A negative offset is clamped to the start rather than reading the tail: 'const startLine = offset ? Math.max(0, offset - 1) : 0' (packages/coding-agent/src/core/tools/read.ts:278)."
  },
  "read.device_blocklist": {
    "score": 0,
    "evidence": "No /dev or device-path refusal: resolveReadPathAsync only resolves/retries the path (path-utils.ts:86-118), then read opens and buffers it (packages/coding-agent/src/core/tools/read.ts:248-256). grep for /dev/|blocklist|device in src finds no guard."
  }
}
