{
  "shell.native_background": {
    "score": 0,
    "evidence": "No background execution path: packages/opencode/src/tool/shell.ts:428-590 spawns the command and returns only on exit/timeout/abort; parameters are command/timeout/workdir only (packages/opencode/src/tool/shell/prompt.ts:16-21). The builtin tool list has no job/background tool (packages/opencode/src/tool/registry.ts:231-249)."
  },
  "shell.timeout_cap": {
    "score": 0.5,
    "evidence": "Default 120000 ms at packages/opencode/src/tool/shell.ts:347 and model-settable (prompt.ts:18; shell.ts:618), but the only validation rejects negatives (shell.ts:615-617) and the schema is a bare PositiveInt (prompt.ts:18), so there is no hard maximum."
  },
  "shell.persistent": {
    "score": 0,
    "evidence": "Each call spawns a fresh child (packages/opencode/src/tool/shell.ts:303-309); only cwd is settable per call (shell.ts:611-614) and env is process.env + plugin hook (shell.ts:424-433). The prompt's 'persistent shell session' claim (prompt.ts:259) has no implementation and nothing survives calls."
  },
  "shell.powershell": {
    "score": 1,
    "evidence": "PowerShell is first class: PS/CMD shell sets and full pwsh/powershell/cmd prompt profiles (packages/opencode/src/tool/shell/prompt.ts:8-9, 200-262), win32 PowerShell spawn (shell.ts:270-279), and a tree-sitter-powershell parser loaded alongside bash (shell.ts:311-330)."
  },
  "shell.middle_out": {
    "score": 0,
    "evidence": "Oversized output is tail-only: packages/opencode/src/tool/shell.ts:569 calls tail(raw, maxLines, maxBytes), then shell.ts:577-580 emits a truncation banner followed by that tail. The shared formatter also supports only head|tail (packages/opencode/src/tool/truncate.ts:100-140); no head+tail middle-out."
  },
  "shell.full_log": {
    "score": 1,
    "evidence": "Overflow switches to a file mid-stream (packages/opencode/src/tool/shell.ts:509-521) and the whole raw text is written via trunc.write (shell.ts:572-574); the path is printed to the model as 'Full output saved to: <path>' (shell.ts:577-580) and returned as metadata.outputPath (shell.ts:589-592)."
  },
  "shell.omitted_counts": {
    "score": 0.5,
    "evidence": "Recovery is given ('Full output saved to: <path>', packages/opencode/src/tool/shell.ts:579) but the banner is literally '...output truncated...' with no omitted count. The count-bearing formatter ('...N bytes/lines truncated...', truncate.ts:124,134-139) is bypassed for shell because shell sets metadata.truncated (tool.ts:139-141)."
  },
  "shell.signal_honesty": {
    "score": 0.5,
    "evidence": "A signal death makes exitCode fail with 'Process interrupted due to receipt of signal' (packages/core/src/cross-spawn-spawner.ts:417-424), and shell only maps kind==='exit' to a code, otherwise reporting null plus a timeout/abort note (packages/opencode/src/tool/shell.ts:542-566). It never reports a fake success, but there is no 128+N (137/143) mapping."
  },
  "shell.benign_exit": {
    "score": 0,
    "evidence": "No benign-exit annotation anywhere: exit is passed through raw (packages/opencode/src/tool/shell.ts:590) and no benign/expected-failure handling exists in packages/opencode/src/tool (grep found nothing); grep/diff/test exit 1 is surfaced as an ordinary non-zero exit."
  },
  "shell.untrusted_fencing": {
    "score": 0,
    "evidence": "Process output is concatenated verbatim into the result (packages/opencode/src/tool/shell.ts:573-580); the only wrapper is <shell_metadata> around timeout/abort notes (shell.ts:582-584). There are no inert-data delimiters around command output."
  },
  "shell.bg_output_reader": {
    "score": 0,
    "evidence": "No model-facing job-output reader: the builtin list (packages/opencode/src/tool/registry.ts:231-249) has none. BackgroundJob is consumed only by task.ts:257-303 and an experimental HTTP route (packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts:158-171)."
  },
  "shell.wait_modes": {
    "score": 0,
    "evidence": "No job wait tool. BackgroundService.wait({timeout}) exists internally (packages/core/src/background-job.ts:78-81, 292-302) but is not exposed to the model; the task tool only auto-notifies (packages/opencode/src/tool/task.ts:256-266)."
  },
  "shell.cursor_reads": {
    "score": 0,
    "evidence": "No cursor/delta reads: BackgroundJob stores a single output blob per job (packages/core/src/background-job.ts:16, 28, 126-156) and no tool exposes that blob at all, let alone by cursor."
  },
  "shell.list_tasks": {
    "score": 0,
    "evidence": "BackgroundJob.list exists (packages/core/src/background-job.ts:190-195) but is reachable only through the experimental HTTP API (packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts:158-171), not a model tool; no list tool in registry.ts:231-249."
  },
  "shell.kill_id": {
    "score": 0,
    "evidence": "No kill/cancel tool: the builtin list has none (packages/opencode/src/tool/registry.ts:231-249). BackgroundJob.cancel (packages/core/src/background-job.ts:337) is only called internally on interrupt (packages/opencode/src/tool/task.ts:349-352); the model cannot cancel a job by id."
  },
  "shell.kill_pid_port": {
    "score": 0,
    "evidence": "No pid or port kill path: shell only kills its own child on timeout/abort (packages/opencode/src/tool/shell.ts:550,554), and no tool accepts a pid or port."
  },
  "shell.sigterm_escalation": {
    "score": 0.5,
    "evidence": "The only kill path does escalate SIGTERM then SIGKILL after a 3 s grace: kill({forceKillAfter:'3 seconds'}) at packages/opencode/src/tool/shell.ts:550,554, implemented as timeoutOrElse(SIGTERM, SIGKILL) at packages/core/src/cross-spawn-spawner.ts:427-437. It fires only on timeout/user abort; there is no cancel-by-id command."
  },
  "shell.group_kill": {
    "score": 1,
    "evidence": "Children are spawned detached into their own group (packages/core/src/cross-spawn-spawner.ts:378) and kills signal -proc.pid of that spawned child with a killOne fallback (same file:292-321, 386-395). It never signals a process group it did not create."
  },
  "shell.monitor": {
    "score": 0,
    "evidence": "No schedule or output-pattern monitor: the only timer is the timeout race (packages/opencode/src/tool/shell.ts:540). Subagent completion injects a synthetic message (task.ts:227-266) but that is completion notification behind an off-by-default flag, not a monitor, and does not cover shell jobs."
  },
  "shell.sleep": {
    "score": 0,
    "evidence": "No sleep tool: grep of packages/opencode/src/tool finds only Effect.sleep for the shell timeout (shell.ts:540) and prompt text telling the model not to sleep (task.ts:33,39)."
  },
  "shell.cron": {
    "score": 0,
    "evidence": "No scheduler or durable scheduled prompts; the job registry is explicitly process-local and non-durable (packages/core/src/background-job.ts:120-134)."
  },
  "shell.bg_subagents": {
    "score": 0.5,
    "evidence": "The task tool has background=true (packages/opencode/src/tool/task.ts:58-61), but it is gated on OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS (task.ts:97-101) and the parameter is hidden from the model unless the flag is on (task.ts:362-366). Results are auto-injected (task.ts:227-266); there is no explicit wait/collect tool."
  },
  "shell.safe_classification": {
    "score": 1,
    "evidence": "Commands are parsed to a tree-sitter bash/powershell AST (packages/opencode/src/tool/shell.ts:311-330) and permission decisions are derived from the parsed command tree and argv (collect() at shell.ts:378-412, ask() at shell.ts:263-283), not a token allowlist."
  },
  "shell.workspace_boundary": {
    "score": 1,
    "evidence": "Shell resolves each path argument and asks external_directory for anything outside the instance (packages/opencode/src/tool/shell.ts:392-412, 626; ask at shell.ts:263-283); read/write use assertExternalDirectoryEffect (packages/opencode/src/tool/external-directory.ts:15-44)."
  },
  "read.line_window": {
    "score": 1,
    "evidence": "DEFAULT_READ_LIMIT = 2000 (packages/opencode/src/tool/read.ts:13), applied at read.ts:331 with a 'Showing lines a-b of N' note at read.ts:343-347."
  },
  "read.second_ceiling": {
    "score": 1,
    "evidence": "MAX_BYTES = 50 * 1024 (packages/opencode/src/tool/read.ts:16) is checked per line at read.ts:167-176, with '(Output capped at 50 KB...)' at read.ts:344."
  },
  "read.per_line_clamp": {
    "score": 1,
    "evidence": "Lines longer than MAX_LINE_LENGTH = 2000 are cut and annotated with '... (line truncated to 2000 chars)' (packages/opencode/src/tool/read.ts:14-15, 162)."
  },
  "read.line_prefixes": {
    "score": 1,
    "evidence": "Output uses `${i + file.offset}: ${line}` (packages/opencode/src/tool/read.ts:339), so numbering is 1-indexed and matches cat -n, including when an offset is supplied."
  },
  "read.resume_offset": {
    "score": 1,
    "evidence": "Truncation notes name the exact next offset: '(Showing lines a-b of N. Use offset=<next> to continue.)' and '(Output capped at 50 KB. Showing lines a-b. Use offset=<next> to continue.)' (packages/opencode/src/tool/read.ts:343-347)."
  },
  "read.empty_file_note": {
    "score": 1,
    "evidence": "An empty file returns '<content>' plus '(End of file - total 0 lines)' (packages/opencode/src/tool/read.ts:349); the zero-lines/offset-1 case is explicitly exempted from the out-of-range error (read.ts:332-334)."
  },
  "read.eof_note": {
    "score": 0.5,
    "evidence": "Offset past EOF is detected and the total line count is included, but it is raised as an Error: packages/opencode/src/tool/read.ts:332-335 'Offset N is out of range for this file (M lines)'. It is not a recovery note."
  },
  "read.streaming": {
    "score": 1,
    "evidence": "Reads go through fs.stream piped to Stream.splitLines (packages/opencode/src/tool/read.ts:147-149) and the stream is stopped at the byte cap via ReadStop (read.ts:170-176), so memory is bounded rather than loading the file."
  },
  "read.deferred_chunk": {
    "score": 1,
    "evidence": "'more' is set only when an extra line is actually observed (packages/opencode/src/tool/read.ts:157-164) and EOF prints '(End of file - total N lines)' (read.ts:349); a byte-cap cut reports 'capped' with a resume offset instead of claiming a known remainder (read.ts:344)."
  },
  "read.dedup": {
    "score": 0,
    "evidence": "No unchanged-read cache: packages/opencode/src/tool/read.ts always re-streams the file (read.ts:331) and grep for unchanged/dedup across packages/opencode/src/tool found no read-result cache."
  },
  "read.ledger": {
    "score": 0,
    "evidence": "Writes never consult reads: packages/opencode/src/tool/write.ts:38-73 stats the file, asks permission with a diff, then writes; edit.ts:69-145 does the same from oldString. There is no read ledger."
  },
  "read.partial_ledger": {
    "score": 0,
    "evidence": "No record of what the model has seen: read.ts returns its window (packages/opencode/src/tool/read.ts:331-360) without recording it, so write.ts/edit.ts cannot refuse a destructive overwrite of a partially-read file."
  },
  "read.did_you_mean": {
    "score": 1,
    "evidence": "On NotFound it suggests up to 3 same-directory names matching by case-insensitive substring and returns 'Did you mean one of these?' (packages/opencode/src/tool/read.ts:82-99). Note this is substring-based, not edit distance."
  },
  "read.unicode_retry": {
    "score": 0,
    "evidence": "No NFC/NFD/unicode-normalization retry: grep for NFC/NFD/normalize in packages/opencode/src/tool found no filename normalization, and the miss path fails at packages/opencode/src/tool/read.ts:82-99."
  },
  "read.confusables": {
    "score": 0,
    "evidence": "No zero-width/bidi detection or disclosure: grep for zero-width/bidi/u200b across packages/opencode/src and packages/core/src found nothing in the read path."
  },
  "read.image_vision": {
    "score": 1,
    "evidence": "Files whose sniffed/extension mime is jpeg/png/gif/webp are returned as base64 data-URL attachments (packages/opencode/src/tool/read.ts:303-325), and the tool description advertises image/PDF attachment (packages/opencode/src/tool/read.txt, last bullet)."
  },
  "read.coord_scale": {
    "score": 0.5,
    "evidence": "Downscaling exists (packages/core/src/image/photon.ts:55-80, invoked from packages/opencode/src/session/prompt.ts:1011-1019), but the attachment carries only type/mime/url (read.ts:317-324): no scale factor or original dimensions are disclosed."
  },
  "read.magic_bytes": {
    "score": 0.5,
    "evidence": "sniffAttachmentMime checks PNG/JPEG/GIF/BMP/PDF/WebP magic bytes first (packages/opencode/src/util/media.ts:14-24, used at read.ts:303), but it falls back to the extension mime (read.ts:303 -> packages/core/src/fs-util.ts:224-226), so a text file named .png is still classified as an image."
  },
  "read.notebooks": {
    "score": 0,
    "evidence": "No .ipynb handling: grep for ipynb in packages/opencode/src and packages/core/src found nothing; notebooks fall through to plain-text reading."
  },
  "read.docx": {
    "score": 0,
    "evidence": ".docx is only listed as a binary extension (packages/opencode/src/tool/read.ts:193-202) and rejected with 'Cannot read binary file' (read.ts:327-329); no docx parser exists."
  },
  "read.pptx": {
    "score": 0,
    "evidence": ".pptx is binary-blocked (packages/opencode/src/tool/read.ts:199-200, 327-329); there is no slide or speaker-note extraction."
  },
  "read.xlsx": {
    "score": 0,
    "evidence": ".xlsx is binary-blocked (packages/opencode/src/tool/read.ts:197-198, 327-329); no sheet/table rendering exists."
  },
  "read.odf": {
    "score": 0,
    "evidence": ".odt/.ods/.odp are binary-blocked (packages/opencode/src/tool/read.ts:201-203, 327-329); no ODF reader."
  },
  "read.rtf_epub": {
    "score": 0,
    "evidence": "No RTF or EPUB handling: .epub is not special-cased and grep for epub/rtf found only an HTTP compression regex (packages/opencode/src/server/routes/instance/httpapi/middleware/compression.ts:7)."
  },
  "read.pdf_text": {
    "score": 0,
    "evidence": "PDFs are only attached as raw bytes: isPdfAttachment (packages/opencode/src/util/media.ts:3-5) routes to the attachment branch that base64-encodes the file (packages/opencode/src/tool/read.ts:303-325). No text extraction."
  },
  "read.scanned_pdf": {
    "score": 0,
    "evidence": "No scanned-PDF detection or rendering hint: PDFs are only base64-attached (packages/opencode/src/tool/read.ts:309-325); nothing inspects page text or names pages."
  },
  "read.docs_windowed": {
    "score": 0,
    "evidence": "Documents never become text, so offset/limit does not apply to them; document extensions are rejected before the line reader (packages/opencode/src/tool/read.ts:303-329)."
  },
  "read.docs_bulk": {
    "score": 0,
    "evidence": "Read takes a single filePath (packages/opencode/src/tool/read.ts:27-35); there is no bulk read, so documents cannot participate in one."
  },
  "read.converter_on_demand": {
    "score": 0,
    "evidence": "No document converter exists to fetch: grep for mammoth/pdfjs/pandoc in packages/opencode/src found nothing, and document extensions are hard-rejected as binary (packages/opencode/src/tool/read.ts:193-203). Nothing is lazily installed because conversion is absent."
  },
  "read.many_files": {
    "score": 0,
    "evidence": "Parameters has one filePath (packages/opencode/src/tool/read.ts:27-35); multiple files require multiple calls."
  },
  "read.globs": {
    "score": 0,
    "evidence": "No glob expansion: the path is stat'ed literally (packages/opencode/src/tool/read.ts:243-252). Globbing is a separate tool (packages/opencode/src/tool/glob.ts)."
  },
  "read.per_file_range": {
    "score": 0,
    "evidence": "Single filePath with one offset/limit pair (packages/opencode/src/tool/read.ts:27-35); no per-entry ranges."
  },
  "read.exclusions_gitignore": {
    "score": 0,
    "evidence": "No exclusion or gitignore parameters/filtering in the read tool (parameters at packages/opencode/src/tool/read.ts:27-35); directory listing filters nothing (read.ts:262-283)."
  },
  "read.aggregate_cap": {
    "score": 0,
    "evidence": "No multi-file budget: the 50 KB cap applies to the single file being read (packages/opencode/src/tool/read.ts:16, 167-176)."
  },
  "read.aliased_input": {
    "score": 0,
    "evidence": "Parameters are decoded with a strict Schema.Struct (packages/opencode/src/tool/read.ts:27-35); failures raise InvalidArgumentsError telling the model to rewrite the input, with no alias repair (packages/opencode/src/tool/tool.ts:25-38, 120-127)."
  },
  "read.negative_offset": {
    "score": 0,
    "evidence": "offset is NonNegativeInt (packages/opencode/src/tool/read.ts:30), so negative/tail offsets are rejected rather than interpreted as reading the last N lines."
  },
  "read.device_blocklist": {
    "score": 0,
    "evidence": "No /dev blocklist: read opens the path and reads a 4 KB sample (packages/opencode/src/tool/read.ts:122-136, 301) before isBinaryFile rejects NUL data (read.ts:182-215, 327). There is no pre-I/O refusal."
  }
}
