[GH-ISSUE #7550] [Bug] ADB output-directory refusals return unreadable-output errors without field correction #4491

Closed
opened 2026-10-31 00:50:14 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7550

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

Selecting an existing regular file as --output-directory makes pull-adb-file, capture-adb-screen and collect-adb-bugreport exit 1 with unreadable_output / execution_failure. Their message and remediation say that analysis returned an unreadable result and recommend rea doctor. The error retains the output path and a directory-creation/access explanation in details.reason, but supplies no output_directory field issue or input-contract recovery.

The reproduction below ran through the built public CLI with a fixture adb selected by REA_ADB_PATH. Every refused request invoked only the fixture's version probe. No transfer, capture or bugreport command ran. All three corresponding production stdio MCP tools also returned isError: true with the same canonical error, once each. No real device or adb server was contacted.

Reproduction

  1. Start in the built checkout at the revision below, with its pinned Node.js 24.22.1 on PATH. Run this Python 3 block. It creates all fixtures inline, isolates REA's HOME/XDG/TMPDIR, records fixture adb calls, and removes its temporary fixture directory afterward.
import json, os, pathlib, subprocess, tempfile
repo = pathlib.Path.cwd()
with tempfile.TemporaryDirectory() as directory:
    work = pathlib.Path(directory)
    env = dict(os.environ, REA_LOG_LEVEL="silent", NODE_DISABLE_COMPILE_CACHE="1")
    for key in ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "TMPDIR"]:
        path = work / key.lower()
        path.mkdir()
        env[key] = str(path)
    log = work / "calls.log"
    log.write_text("")
    env["REA_FIXTURE_LOG"] = str(log)
    adb = work / "adb"
    adb.write_text('''#!/bin/sh
printf '%s\\n' "$*" >> "$REA_FIXTURE_LOG"
if [ "$1" = version ]; then
  printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-fixture\\n'
  exit 0
fi
if [ "$1" = -s ] && [ "$3" = pull ]; then
  printf 'fixture bytes\\n' > "$5"
  exit 0
fi
exit 97
''')
    adb.chmod(0o700)
    env["REA_ADB_PATH"] = str(adb)
    occupied = work / "out"
    occupied.write_text("preserve this file\n")
    def run(arguments):
        result = subprocess.run(["node", str(repo / "scripts/rea.mjs"), *arguments, "--format", "json"], env=env, capture_output=True, text=True, timeout=60)
        payload = json.loads(result.stdout)
        print(json.dumps({"command": arguments, "exit": result.returncode, "result": payload}).replace(str(work), "<work>"))
        assert not result.stderr
        return result, payload
    for attempt in range(2):
        for command in ["pull-adb-file", "capture-adb-screen", "collect-adb-bugreport"]:
            arguments = [command, "fixture-device"]
            if command == "pull-adb-file":
                arguments.append("/sdcard/notes.txt")
            result, payload = run(arguments + ["--output-directory", str(occupied)])
            assert result.returncode == 1 and payload["code"] == "unreadable_output"
            assert occupied.read_text() == "preserve this file\n"
    assert log.read_text().splitlines() == ["version"] * 6
    print("refusals: only six version probes; existing file preserved")
    result, payload = run(["pull-adb-file", "fixture-device", "/sdcard/notes.txt", "--output-directory", str(work / "fresh")])
    assert result.returncode == 0
    assert (work / "fresh/notes.txt").read_bytes() == b"fixture bytes\n"
    print("fresh-directory control: success with the fixture payload")
  1. Each refused command prints exit 1 and the generic error. The block runs each command twice and asserts that those six refusals executed only adb version, preserving the existing file. I observed the same defect in six CLI invocations per command across the independent probes and replays. The retained block itself reproduces twice per command.
  2. The last command changes only the output destination to a fresh directory. It exits 0 and produces the expected 14-byte fixture file. This is a simulated-provider success control, not a real-device transfer.

Expected behavior

Preserve the existing-file refusal, but identify the caller-selected output directory as the failed precondition and provide actionable directory-selection recovery. Retain the caller field path for this expected input failure so an MCP or CLI consumer can correct output_directory instead of diagnosing unreadable analysis output.

The ADB guide says pull/capture outputs are additive and an existing destination is refused. The tool-design guide separately requires expected input failures to retain caller field/index paths and safe constraints. The MCP error contract instructs callers to follow returned remediation and recommends changing a field only when remediation explicitly says to do so. These requirements apply before capture or transfer; no device-generated result failed parsing here. No exact replacement sentence is required.

On this same baseline, extract-firmware given the identical existing file returns invalid_request / invalid_input, an output_directory issue and input-contract recovery. environment-export returns that classification with a directory issue and explains that a directory is required. These are executed comparisons showing the established admission-error pattern; they do not imply that the commands share every output policy.

Impact and scope

The tested caller mistake is classified as an execution/output failure rather than an input refusal, and consumers receive no machine-readable destination-field correction. The recovery directs the caller toward installation diagnostics even though selecting a fresh output directory allowed the fixture pull to succeed. The true destination reason remains available in details.reason, and all occupied-file contents remained intact. No data loss, installation fault, actual automated-consumer misroute or security-boundary crossing was observed.

Observed scope: three CLI commands with existing regular-file output-directory selections and their three production stdio MCP tools. Untested: unwritable directories, missing parents, package pulls, existing child-file collisions, live Android devices, Windows and Linux.

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Incorrect error code/category and absent caller-field correction impair recovery from a bounded destination-input failure; this extends beyond terminology alone.
  • Workaround status: available
  • Workaround basis: The reason preserves the selected path, and the fresh-directory fixture pull completed with the expected bytes using the same serial and source path.

REA version

Exact node scripts/rea.mjs --version stdout:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. The local shell fixture reports Android Debug Bridge 1.0.41 and 34.0.5-fixture; these are fixture strings, not measurements of installed Android platform tools.

Target and artifact context

Synthetic serial fixture-device, ordinary remote basename notes.txt, and a local regular file containing a preservation marker. The control fixture implements a pull by writing fixture bytes followed by a newline to the selected host file. No binary target or Android device is involved.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

For the first pull-adb-file call above, the observed JSON was the following, with only the generated fixture root replaced by <work>:

{"code":"unreadable_output","category":"execution_failure","message":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.","retryable":false,"remediation":{"action":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed."},"details":{"operation":"pull_adb_file","reason":"The output directory is not writable or could not be created: <work>/out"}}

The other two CLI commands return the same members with their operation names. Separate MCP calls used serial: "fixture-device", the same existing-file output_directory, and the ordinary notes.txt source path for pull_adb_file; each returned the error above within content text as {"error": ...} and isError: true. The fixture's first CLI/MCP matrix logged exactly nine version calls, with no other adb arguments. The inline CLI replay independently checks version-only admission failures and unchanged file bytes.

Executed comparisons with the same occupied file:

  • extract-firmware <fixture-firmware> <occupied-file> --format json: invalid_request, invalid_input, details.issues[0].path: ["output_directory"], message Extraction output root already exists, recovery kind inspect_input_contract. This exercised admission only, without Unblob execution.
  • environment-export <fixture-manifest> <occupied-file> --evidence <fixture-capture> --format json: invalid_request, invalid_input, issue path ["directory"], message Output destination must be a directory: <occupied-file>, input-contract recovery. The manifest/capture pair came from writeEnvironmentServeFiles in tests/fixtures/environmentServe.ts.
  • evidence-export <valid-empty-bundle> <occupied-file> --format json: invalid_request, invalid_input, reason exists. Unlike ADB, its destination is a file; it correctly treats the fixture as an occupied output file rather than a directory.

Expected-source locators at this baseline: docs/adb-device-analysis.md:225-227 (additive refusal), docs/tool-design.md:348-351 (caller-field correction), docs/mcp-contracts.md:141-150 (canonical errors and remediation), and AGENTS.md:33 (meaningful failure reasons). The AnalysisOutputError row at docs/user-visible-errors.csv:9 describes the generic copy's trigger as analysis-output parsing failure.

Failure-source locators: src/adb/AdbProvider.ts:446-465 owns output-directory preparation. The public pull, screen and bugreport paths reach it at lines 2380, 3082 and 3207, respectively, before their capture/transfer command. src/domain/analysisErrorPresentation.ts:426-428 supplies the observed generic wording. These source observations explain the executed failures; no source change or fix was tested.

Typed outcome or recovery expected

Use destination-input classification with an output_directory issue and corrective recovery for this existing-file selection, consistent with the expected-input contract and baseline comparisons. Preserve non-overwrite behavior and the existing file. Do not generalize this fixture to later publication failures, permissions or resource constraints; those may need distinct error classes.

Analysis or suspected cause

Source-derived: the shared directory guard converts the mkdir/access exception to AnalysisOutputError with a free-text reason and cause. It provides neither an input-field issue nor specialized user-message/remediation options. The canonical projector therefore emits its standard unreadable-analysis-result copy and execution-failure category. The source path and public results agree. No implementation approach was tested.

Additional context

Duplicate searches covered open and closed issues, open/closed/merged PRs, and issue/PR comments using adb/output, unreadable, writable, destination, output_directory/doctor, and prepareOutputDirectory queries. No report with this ADB destination-admission contract and closure condition was found.

Related, distinct reports:

  • Open #7444 concerns Evidence export offering overwrite for a directory; open #7396 concerns environment-bundle extraction terminology while retaining invalid-input field attribution.
  • Closed #6079 concerns a missing receipt after a successful pull followed by digest-read failure. Closed #4643 concerns remote-file attribution after an adb command. This fixture reaches neither stage.
  • Closed #5746 and #6029 concern analogous firmware and browser-export admission classification. Their different workflows are already corrected on the tested baseline; this is not a claimed regression of either.
  • Open PR #1903 specializes package pm path refusals. It does not address the top-level host output-directory guard reproduced here.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7550 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior Selecting an existing regular file as `--output-directory` makes `pull-adb-file`, `capture-adb-screen` and `collect-adb-bugreport` exit 1 with `unreadable_output` / `execution_failure`. Their message and remediation say that analysis returned an unreadable result and recommend `rea doctor`. The error retains the output path and a directory-creation/access explanation in `details.reason`, but supplies no `output_directory` field issue or input-contract recovery. The reproduction below ran through the built public CLI with a fixture adb selected by `REA_ADB_PATH`. Every refused request invoked only the fixture's version probe. No transfer, capture or bugreport command ran. All three corresponding production stdio MCP tools also returned `isError: true` with the same canonical error, once each. No real device or adb server was contacted. ### Reproduction 1. Start in the built checkout at the revision below, with its pinned Node.js 24.22.1 on PATH. Run this Python 3 block. It creates all fixtures inline, isolates REA's HOME/XDG/TMPDIR, records fixture adb calls, and removes its temporary fixture directory afterward. ```python import json, os, pathlib, subprocess, tempfile repo = pathlib.Path.cwd() with tempfile.TemporaryDirectory() as directory: work = pathlib.Path(directory) env = dict(os.environ, REA_LOG_LEVEL="silent", NODE_DISABLE_COMPILE_CACHE="1") for key in ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "TMPDIR"]: path = work / key.lower() path.mkdir() env[key] = str(path) log = work / "calls.log" log.write_text("") env["REA_FIXTURE_LOG"] = str(log) adb = work / "adb" adb.write_text('''#!/bin/sh printf '%s\\n' "$*" >> "$REA_FIXTURE_LOG" if [ "$1" = version ]; then printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-fixture\\n' exit 0 fi if [ "$1" = -s ] && [ "$3" = pull ]; then printf 'fixture bytes\\n' > "$5" exit 0 fi exit 97 ''') adb.chmod(0o700) env["REA_ADB_PATH"] = str(adb) occupied = work / "out" occupied.write_text("preserve this file\n") def run(arguments): result = subprocess.run(["node", str(repo / "scripts/rea.mjs"), *arguments, "--format", "json"], env=env, capture_output=True, text=True, timeout=60) payload = json.loads(result.stdout) print(json.dumps({"command": arguments, "exit": result.returncode, "result": payload}).replace(str(work), "<work>")) assert not result.stderr return result, payload for attempt in range(2): for command in ["pull-adb-file", "capture-adb-screen", "collect-adb-bugreport"]: arguments = [command, "fixture-device"] if command == "pull-adb-file": arguments.append("/sdcard/notes.txt") result, payload = run(arguments + ["--output-directory", str(occupied)]) assert result.returncode == 1 and payload["code"] == "unreadable_output" assert occupied.read_text() == "preserve this file\n" assert log.read_text().splitlines() == ["version"] * 6 print("refusals: only six version probes; existing file preserved") result, payload = run(["pull-adb-file", "fixture-device", "/sdcard/notes.txt", "--output-directory", str(work / "fresh")]) assert result.returncode == 0 assert (work / "fresh/notes.txt").read_bytes() == b"fixture bytes\n" print("fresh-directory control: success with the fixture payload") ``` 2. Each refused command prints exit 1 and the generic error. The block runs each command twice and asserts that those six refusals executed only `adb version`, preserving the existing file. I observed the same defect in six CLI invocations per command across the independent probes and replays. The retained block itself reproduces twice per command. 3. The last command changes only the output destination to a fresh directory. It exits 0 and produces the expected 14-byte fixture file. This is a simulated-provider success control, not a real-device transfer. ### Expected behavior Preserve the existing-file refusal, but identify the caller-selected output directory as the failed precondition and provide actionable directory-selection recovery. Retain the caller field path for this expected input failure so an MCP or CLI consumer can correct `output_directory` instead of diagnosing unreadable analysis output. The ADB guide says pull/capture outputs are additive and an existing destination is refused. The tool-design guide separately requires expected input failures to retain caller field/index paths and safe constraints. The MCP error contract instructs callers to follow returned remediation and recommends changing a field only when remediation explicitly says to do so. These requirements apply before capture or transfer; no device-generated result failed parsing here. No exact replacement sentence is required. On this same baseline, `extract-firmware` given the identical existing file returns `invalid_request` / `invalid_input`, an `output_directory` issue and input-contract recovery. `environment-export` returns that classification with a `directory` issue and explains that a directory is required. These are executed comparisons showing the established admission-error pattern; they do not imply that the commands share every output policy. ### Impact and scope The tested caller mistake is classified as an execution/output failure rather than an input refusal, and consumers receive no machine-readable destination-field correction. The recovery directs the caller toward installation diagnostics even though selecting a fresh output directory allowed the fixture pull to succeed. The true destination reason remains available in `details.reason`, and all occupied-file contents remained intact. No data loss, installation fault, actual automated-consumer misroute or security-boundary crossing was observed. Observed scope: three CLI commands with existing regular-file output-directory selections and their three production stdio MCP tools. Untested: unwritable directories, missing parents, package pulls, existing child-file collisions, live Android devices, Windows and Linux. - Impact level: P2 - Assessment status: supported - Impact basis: Incorrect error code/category and absent caller-field correction impair recovery from a bounded destination-input failure; this extends beyond terminology alone. - Workaround status: available - Workaround basis: The reason preserves the selected path, and the fresh-directory fixture pull completed with the expected bytes using the same serial and source path. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. The local shell fixture reports Android Debug Bridge 1.0.41 and `34.0.5-fixture`; these are fixture strings, not measurements of installed Android platform tools. ### Target and artifact context Synthetic serial `fixture-device`, ordinary remote basename `notes.txt`, and a local regular file containing a preservation marker. The control fixture implements a pull by writing `fixture bytes` followed by a newline to the selected host file. No binary target or Android device is involved. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence For the first `pull-adb-file` call above, the observed JSON was the following, with only the generated fixture root replaced by `<work>`: ```json {"code":"unreadable_output","category":"execution_failure","message":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.","retryable":false,"remediation":{"action":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed."},"details":{"operation":"pull_adb_file","reason":"The output directory is not writable or could not be created: <work>/out"}} ``` The other two CLI commands return the same members with their operation names. Separate MCP calls used `serial: "fixture-device"`, the same existing-file `output_directory`, and the ordinary `notes.txt` source path for `pull_adb_file`; each returned the error above within `content` text as `{"error": ...}` and `isError: true`. The fixture's first CLI/MCP matrix logged exactly nine `version` calls, with no other adb arguments. The inline CLI replay independently checks version-only admission failures and unchanged file bytes. Executed comparisons with the same occupied file: - `extract-firmware <fixture-firmware> <occupied-file> --format json`: `invalid_request`, `invalid_input`, `details.issues[0].path: ["output_directory"]`, message `Extraction output root already exists`, recovery kind `inspect_input_contract`. This exercised admission only, without Unblob execution. - `environment-export <fixture-manifest> <occupied-file> --evidence <fixture-capture> --format json`: `invalid_request`, `invalid_input`, issue path `["directory"]`, message `Output destination must be a directory: <occupied-file>`, input-contract recovery. The manifest/capture pair came from `writeEnvironmentServeFiles` in `tests/fixtures/environmentServe.ts`. - `evidence-export <valid-empty-bundle> <occupied-file> --format json`: `invalid_request`, `invalid_input`, reason `exists`. Unlike ADB, its destination is a file; it correctly treats the fixture as an occupied output file rather than a directory. Expected-source locators at this baseline: `docs/adb-device-analysis.md:225-227` (additive refusal), `docs/tool-design.md:348-351` (caller-field correction), `docs/mcp-contracts.md:141-150` (canonical errors and remediation), and `AGENTS.md:33` (meaningful failure reasons). The `AnalysisOutputError` row at `docs/user-visible-errors.csv:9` describes the generic copy's trigger as analysis-output parsing failure. Failure-source locators: `src/adb/AdbProvider.ts:446-465` owns output-directory preparation. The public pull, screen and bugreport paths reach it at lines 2380, 3082 and 3207, respectively, before their capture/transfer command. `src/domain/analysisErrorPresentation.ts:426-428` supplies the observed generic wording. These source observations explain the executed failures; no source change or fix was tested. ### Typed outcome or recovery expected Use destination-input classification with an `output_directory` issue and corrective recovery for this existing-file selection, consistent with the expected-input contract and baseline comparisons. Preserve non-overwrite behavior and the existing file. Do not generalize this fixture to later publication failures, permissions or resource constraints; those may need distinct error classes. ### Analysis or suspected cause Source-derived: the shared directory guard converts the mkdir/access exception to `AnalysisOutputError` with a free-text reason and cause. It provides neither an input-field issue nor specialized user-message/remediation options. The canonical projector therefore emits its standard unreadable-analysis-result copy and execution-failure category. The source path and public results agree. No implementation approach was tested. ### Additional context Duplicate searches covered open and closed issues, open/closed/merged PRs, and issue/PR comments using adb/output, unreadable, writable, destination, output_directory/doctor, and prepareOutputDirectory queries. No report with this ADB destination-admission contract and closure condition was found. Related, distinct reports: - Open [#7444](https://github.com/morluto/rea/issues/7444) concerns Evidence export offering overwrite for a directory; open [#7396](https://github.com/morluto/rea/issues/7396) concerns environment-bundle extraction terminology while retaining invalid-input field attribution. - Closed [#6079](https://github.com/morluto/rea/issues/6079) concerns a missing receipt after a successful pull followed by digest-read failure. Closed [#4643](https://github.com/morluto/rea/issues/4643) concerns remote-file attribution after an adb command. This fixture reaches neither stage. - Closed [#5746](https://github.com/morluto/rea/issues/5746) and [#6029](https://github.com/morluto/rea/issues/6029) concern analogous firmware and browser-export admission classification. Their different workflows are already corrected on the tested baseline; this is not a claimed regression of either. - Open [PR #1903](https://github.com/morluto/rea/pull/1903) specializes package `pm path` refusals. It does not address the top-level host output-directory guard reproduced here.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4491
No description provided.