{
  "shell.native_background": {
    "score": 1,
    "evidence": "packages/opencode/src/kilocode/tool/background-process.ts:14-42 defines the `background_process` tool with a tracked process id and start/list/status/logs/stop/restart actions; packages/opencode/src/kilocode/tool/registry.ts:218 registers it by default for the cli and vscode clients; processes are spawned and tracked in packages/opencode/src/kilocode/background-process/index.ts:900-970."
  },
  "shell.timeout_cap": {
    "score": 0.5,
    "evidence": "Default is 120000 ms at packages/opencode/src/tool/shell.ts:534 and the model can set `timeout` at packages/opencode/src/tool/shell.ts:747, but the only hard cap is the env-only KILO_COMMAND_TIMEOUT_MAX_MS clamp at packages/opencode/src/kilocode/command-timeout.ts:8-19, which is unset in the default configuration."
  },
  "shell.persistent": {
    "score": 0,
    "evidence": "Each call spawns a fresh shell process and discards it: packages/opencode/src/tool/shell.ts:599-602 and 478-497. No cwd/env state survives across calls. The prompt claims a 'persistent shell session' at packages/opencode/src/tool/shell/prompt.ts:276, but the implementation does not; packages/opencode/src/kilocode/tool/interactive-terminal.txt:14 states 'it is not a persistent shell'."
  },
  "shell.powershell": {
    "score": 1,
    "evidence": "First-class PowerShell support: pwsh invocation at packages/opencode/src/tool/shell.ts:479-487 and the tree-sitter-powershell parser at 512-521; packages/core/src/shell.ts:14-25 registers powershell/pwsh, 99-107 makes pwsh the first Windows default, and 167-202 builds PowerShell args; dedicated prompts at packages/opencode/src/tool/shell/prompt.ts:46-67."
  },
  "shell.middle_out": {
    "score": 0,
    "evidence": "Only a tail is retained: packages/opencode/src/tool/shell.ts:248-278 (tail()) is used at 696-697 and the result is tail + notice at 703-708. There is no head+tail splice."
  },
  "shell.full_log": {
    "score": 1,
    "evidence": "The full stream is spooled to a truncation file once it exceeds the byte budget (packages/opencode/src/tool/shell.ts:619-641, 699-701), and the saved path is included in the model-facing output at 706-707."
  },
  "shell.omitted_counts": {
    "score": 0,
    "evidence": "The truncation note is only '...output truncated...\\n\\nFull output saved to: <file>' with no omitted line or byte count (packages/opencode/src/tool/shell.ts:706-708). The read-side Truncate.output does compute counts (packages/opencode/src/tool/truncate.ts:126-139), but shell bypasses it by setting metadata.truncated (packages/opencode/src/tool/tool.ts:138-141)."
  },
  "shell.signal_honesty": {
    "score": 0,
    "evidence": "Exit is captured at packages/opencode/src/tool/shell.ts:661-665 but only stored in metadata at 716-720; the model-facing output built at 703-723 never states 128+N and never states any exit code. The model receives only part.state.output (packages/opencode/src/session/message-v2.ts:441-453), so a SIGKILLed command reads as ordinary EOF."
  },
  "shell.benign_exit": {
    "score": 0,
    "evidence": "No annotation for grep/diff/test exit 1 anywhere in packages/opencode/src/tool/shell.ts:686-723; the model does not even see the exit code (packages/opencode/src/session/message-v2.ts:441-453)."
  },
  "shell.untrusted_fencing": {
    "score": 0,
    "evidence": "Raw command output is returned verbatim (packages/opencode/src/tool/shell.ts:703-708). The only wrapper is <shell_metadata> for timeout/abort (710-711), which is harness status, not an inert-data fence around untrusted output."
  },
  "shell.bg_output_reader": {
    "score": 1,
    "evidence": "The `logs` action returns a running process's captured output: packages/opencode/src/kilocode/tool/background-process.ts:137-145, backed by packages/opencode/src/kilocode/background-process/index.ts:1202-1208."
  },
  "shell.wait_modes": {
    "score": 0.5,
    "evidence": "Only a non-blocking snapshot exists: `status` and `logs` at packages/opencode/src/kilocode/tool/background-process.ts:137-161. The tool doc explicitly says 'This tool cannot wait' at packages/opencode/src/kilocode/tool/background-process.txt:5. No wait-for-new-output and no wait-for-exit mode."
  },
  "shell.cursor_reads": {
    "score": 0,
    "evidence": "`logs` re-returns the full retained buffer on every call (packages/opencode/src/kilocode/tool/background-process.ts:143-144; packages/opencode/src/kilocode/background-process/index.ts:1202-1208). The Params struct at packages/opencode/src/kilocode/tool/background-process.ts:14-42 has no offset/cursor input."
  },
  "shell.list_tasks": {
    "score": 1,
    "evidence": "The `list` action enumerates live processes for the session: packages/opencode/src/kilocode/tool/background-process.ts:117-130; packages/opencode/src/kilocode/background-process/index.ts:1186-1199."
  },
  "shell.kill_id": {
    "score": 1,
    "evidence": "The `stop` action cancels by tracked process id: packages/opencode/src/kilocode/tool/background-process.ts:147-155; packages/opencode/src/kilocode/background-process/index.ts:1210-1216."
  },
  "shell.kill_pid_port": {
    "score": 0,
    "evidence": "Only the tracked process id is accepted; the Params schema at packages/opencode/src/kilocode/tool/background-process.ts:14-42 has command/id/actions and no pid or port. No port-to-pid lookup exists in packages/opencode/src/kilocode/background-process/index.ts."
  },
  "shell.sigterm_escalation": {
    "score": 1,
    "evidence": "Termination sends SIGTERM, polls until the process is gone, then sends SIGKILL: packages/opencode/src/kilocode/background-process/index.ts:729-778 (and 700-710 for the poll). The shell timeout path also uses forceKillAfter 3 seconds at packages/opencode/src/tool/shell.ts:669-673."
  },
  "shell.group_kill": {
    "score": 1,
    "evidence": "Background workers are spawned detached into their own group (packages/opencode/src/kilocode/background-process/index.ts:929) and are signalled only as -pid after an ownership probe (713-778, 597-604). The harness does not signal an inherited group."
  },
  "shell.monitor": {
    "score": 0,
    "evidence": "No wake-up/scheduling path for shell jobs. The tool doc tells the model it cannot wait and to use a blocking shell sleep instead (packages/opencode/src/kilocode/tool/background-process.txt:5-8). The internal watch loop (packages/opencode/src/kilocode/background-process/index.ts:838-860) only refreshes retained output and never prompts the agent."
  },
  "shell.sleep": {
    "score": 0,
    "evidence": "No sleep/park tool is registered in the builtin list at packages/opencode/src/tool/registry.ts:290-314 or in packages/opencode/src/kilocode/tool/registry.ts:196-232; searched both tool directories for a sleep tool and found none."
  },
  "shell.cron": {
    "score": 0,
    "evidence": "No cron or durable scheduled-prompt tool in packages/opencode/src/tool/registry.ts:290-314 or packages/opencode/src/kilocode/tool/registry.ts:196-232; searched both tool directories for schedule/cron and found none."
  },
  "shell.bg_subagents": {
    "score": 0.5,
    "evidence": "Background subagents are on by default (packages/opencode/src/tool/task.ts:66-69,110; packages/opencode/src/effect/runtime-flags.ts:48-50) and results are auto-injected into the parent (packages/opencode/src/tool/task.ts:320-326). There is no model-facing wait/collect tool: collection is the internal background.wait at task.ts:321, and the registry at packages/opencode/src/tool/registry.ts:290-314 exposes no wait tool."
  },
  "shell.safe_classification": {
    "score": 1,
    "evidence": "Commands are parsed with tree-sitter bash/powershell and permissions are derived from the AST, not a token allowlist: packages/opencode/src/tool/shell.ts:280-284, 368-421, 498-523, with fail-closed handling for unparsed command text at 412-418."
  },
  "shell.workspace_boundary": {
    "score": 1,
    "evidence": "External workdir and AST-detected file-argument paths produce an external_directory permission request: packages/opencode/src/tool/shell.ts:294-315, 431-435. Background process start gates an external cwd the same way at packages/opencode/src/kilocode/tool/background-process.ts:169-180."
  },
  "read.line_window": {
    "score": 1,
    "evidence": "Default 2000-line window at packages/opencode/src/tool/read.ts:24, applied at 345, and enforced by the line-limited collector at 404-433."
  },
  "read.second_ceiling": {
    "score": 1,
    "evidence": "A 50 KB byte budget in addition to the line window: packages/opencode/src/tool/read.ts:29-30, enforced mid-stream at 426-429 and reported at 360."
  },
  "read.per_line_clamp": {
    "score": 1,
    "evidence": "Lines are clamped at 2000 chars with an explicit suffix: packages/opencode/src/tool/read.ts:25-27, 421-423."
  },
  "read.line_prefixes": {
    "score": 1,
    "evidence": "1-indexed line prefixes are printed as `i + file.offset` followed by ': ' at packages/opencode/src/tool/read.ts:355, where file.offset is the 1-based window start set at 345."
  },
  "read.resume_offset": {
    "score": 1,
    "evidence": "Both truncation notes name the exact next offset: packages/opencode/src/tool/read.ts:360 and 362 (\"Use offset=<line> to continue\")."
  },
  "read.empty_file_note": {
    "score": 1,
    "evidence": "An empty file falls through to '(End of file - total 0 lines)' at packages/opencode/src/tool/read.ts:363-364, distinguishable from a tool failure."
  },
  "read.eof_note": {
    "score": 0,
    "evidence": "Offset past EOF is a hard Error: packages/opencode/src/tool/read.ts:348-351 ('Offset ... is out of range for this file'), surfaced as a tool failure via Effect.orDie at 397-398. No recovery note."
  },
  "read.streaming": {
    "score": 1,
    "evidence": "Text is read through a chunked file iterator at packages/opencode/src/kilocode/tool/read-object.ts:68-80 and a line-reader over that stream at packages/opencode/src/tool/read.ts:404-438; accumulation is capped by lines/bytes rather than loading the file."
  },
  "read.deferred_chunk": {
    "score": 1,
    "evidence": "`more` is set only after seeing a line beyond the limit (packages/opencode/src/tool/read.ts:414-420) or a real line that no longer fits the byte budget (425-430), and the line-limit path keeps reading to EOF for the total, so it does not claim unseen content."
  },
  "read.dedup": {
    "score": 0,
    "evidence": "No unchanged-read cache or stub: every call re-streams the window (packages/opencode/src/tool/read.ts:313-347). Searched packages/opencode/src/session and packages/opencode/src/tool for read fingerprints/dedup and found none."
  },
  "read.ledger": {
    "score": 0,
    "evidence": "Write reads the old file only to build a diff and permission request, never to check what the model has seen: packages/opencode/src/tool/write.ts:52-79. No read ledger exists in session state."
  },
  "read.partial_ledger": {
    "score": 0,
    "evidence": "Write overwrites regardless of whether the model saw a partial window: packages/opencode/src/tool/write.ts:52-80. No partial-view tracking exists."
  },
  "read.did_you_mean": {
    "score": 0,
    "evidence": "A miss only stats the parent directory and fails with 'File not found': packages/opencode/src/tool/read.ts:88-100. No filename suggestions."
  },
  "read.unicode_retry": {
    "score": 0,
    "evidence": "Paths are resolved once at packages/opencode/src/tool/read.ts:203-209 (FSUtil.normalizePath is Windows-only at packages/core/src/fs-util.ts:223-231); no NFC/NFD or known-equivalent retry."
  },
  "read.confusables": {
    "score": 0,
    "evidence": "Content lines are emitted verbatim at packages/opencode/src/tool/read.ts:355 and 364; no zero-width or bidi disclosure. Bidi handling exists only for skill display at packages/opencode/src/kilocode/skills/display.ts:8."
  },
  "read.image_vision": {
    "score": 1,
    "evidence": "Supported image bytes become file attachments for vision models: packages/opencode/src/tool/read.ts:323-337; media attachments are forwarded to the model at packages/opencode/src/session/message-v2.ts:430-452."
  },
  "read.coord_scale": {
    "score": 0,
    "evidence": "Images are resized at packages/core/src/image/photon.ts:52-100, but normalize returns only content bytes and read.ts:87-90 attaches them with no scale factor or dimension note."
  },
  "read.magic_bytes": {
    "score": 0.5,
    "evidence": "Bytes are sniffed first at packages/opencode/src/util/media.ts:24-35, but the fallback is the extension-based FSUtil.mimeType at packages/opencode/src/tool/read.ts:323 (packages/core/src/fs-util.ts:219-221), so a text file named .png is still attached as image/png."
  },
  "read.notebooks": {
    "score": 1,
    "evidence": "packages/opencode/src/kilocode/tool/notebook.ts:24-27 renders tagged <markdown_cell>/<code_cell> blocks and 33-48 walks the notebook cells; wired through packages/opencode/src/kilocode/tool/read-extract.ts:10-21. Cell outputs are omitted entirely, and the whole rendering is bounded by the shared 50KB/2000-line window (packages/opencode/src/tool/read.ts:404-433)."
  },
  "read.docx": {
    "score": 1,
    "evidence": "DOCX is detected by extension and extracted to text with mammoth: packages/opencode/src/kilocode/tool/read-docx.ts:1-20, wired in packages/opencode/src/kilocode/tool/read-extract.ts:10-21."
  },
  "read.pptx": {
    "score": 0,
    "evidence": "No PPTX extractor: packages/opencode/src/kilocode/tool/read-extract.ts:10-21 handles only docx, xlsx, and ipynb, and .pptx is in the binary-reject list at packages/opencode/src/tool/read.ts:163-164."
  },
  "read.xlsx": {
    "score": 1,
    "evidence": "XLSX is parsed with SheetJS into tab-separated per-sheet tables: packages/opencode/src/kilocode/tool/xlsx.ts:12-100, wired in packages/opencode/src/kilocode/tool/read-extract.ts:10-21."
  },
  "read.odf": {
    "score": 0.5,
    "evidence": "Only .ods is supported, via the spreadsheet reader at packages/opencode/src/kilocode/tool/xlsx.ts:12-38; .odt and .odp are rejected as binary at packages/opencode/src/tool/read.ts:165-167."
  },
  "read.rtf_epub": {
    "score": 0,
    "evidence": "No RTF or EPUB readers in packages/opencode/src/kilocode/tool/read-extract.ts:10-21. EPUB falls into binary detection at packages/opencode/src/tool/read.ts:146-196, and RTF is returned as raw markup rather than converted."
  },
  "read.pdf_text": {
    "score": 0,
    "evidence": "PDF is only attached as a file part, not text-extracted: packages/opencode/src/tool/read.ts:326-336; detection uses isPdfAttachment at packages/opencode/src/util/media.ts:4-6."
  },
  "read.scanned_pdf": {
    "score": 0,
    "evidence": "No PDF page analysis and no rendering hint; PDFs are passed through as attachments only at packages/opencode/src/tool/read.ts:326-336."
  },
  "read.docs_windowed": {
    "score": 1,
    "evidence": "Extracted document text is returned as a Readable and then consumed by the same offset/limit collector as plain text: packages/opencode/src/tool/read.ts:125-143 and 404-438."
  },
  "read.docs_bulk": {
    "score": 0,
    "evidence": "Read accepts a single path (packages/opencode/src/tool/read.ts:38-46), and document extraction is invoked for that one file at packages/opencode/src/tool/read.ts:130-132. No multi-file read exists, so documents cannot participate in one."
  },
  "read.converter_on_demand": {
    "score": 0,
    "evidence": "Converters are installed dependencies, not fetched on first use: packages/opencode/package.json:150 (mammoth) and 172 (xlsx), statically imported at packages/opencode/src/kilocode/tool/read-extract.ts:1-4."
  },
  "read.many_files": {
    "score": 0,
    "evidence": "Read accepts one `filePath` string (packages/opencode/src/tool/read.ts:39-47); there is no array or bulk form. The builtin registration is a single read tool at packages/opencode/src/tool/registry.ts:294-295."
  },
  "read.globs": {
    "score": 0,
    "evidence": "No glob expansion inside read: the path is used as-is and only that path is stat/opened (packages/opencode/src/tool/read.ts:203-231). Globbing lives in the separate glob tool."
  },
  "read.per_file_range": {
    "score": 0,
    "evidence": "There is no bulk-read entry list; read has one offset/limit pair for one file (packages/opencode/src/tool/read.ts:39-47)."
  },
  "read.exclusions_gitignore": {
    "score": 0,
    "evidence": "Read does no file enumeration and no .gitignore filtering (packages/opencode/src/tool/read.ts:198-231). Exclusions exist only in the separate glob/grep tools."
  },
  "read.aggregate_cap": {
    "score": 0,
    "evidence": "No multi-file read, so no aggregate budget; the 50 KB cap is per call at packages/opencode/src/tool/read.ts:29 and 426-429."
  },
  "read.aliased_input": {
    "score": 0,
    "evidence": "Parameters require the exact `filePath`/`offset`/`limit` keys at packages/opencode/src/tool/read.ts:39-47; invalid args produce a rewrite request rather than alias repair at packages/opencode/src/tool/tool.ts:25-37 and 123-136."
  },
  "read.negative_offset": {
    "score": 0,
    "evidence": "`offset` is NonNegativeInt at packages/opencode/src/tool/read.ts:41, so negative values are rejected; no tail-from-negative path exists."
  },
  "read.device_blocklist": {
    "score": 0,
    "evidence": "No device-path blocklist; only Windows named pipes are refused at packages/opencode/src/kilocode/tool/read-object.ts:32-38. /dev/* paths reach stat/open at packages/opencode/src/tool/read.ts:224-231."
  }
}
