[GH-ISSUE #7444] [Bug] evidence-export recommends overwrite for a directory that overwrite also rejects #4448

Closed
opened 2026-10-31 00:49:53 +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/7444

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

Exporting a valid empty Evidence bundle to an existing directory without --overwrite returns invalid_request with reason exists. Both the message and recovery action call the destination an existing file and suggest allowing overwrite. Following that advice with --overwrite returns invalid_request with reason not-file, explaining that the same destination is not a regular file. I observed this pair twice through the public CLI. Both refusals preserved the directory and its existing sentinel file.

Reproduction

  1. Use the source revision below with its dependencies and CLI runtime already built, and put Node.js 24.22.1 on PATH. Start in the checkout root; the following script creates its fixtures inline in a fresh empty disposable directory and redirects the child processes' HOME there.
  2. Run this block. Each child invocation uses relative fixture/output arguments, and both failing command variants run twice. The last two calls verify export to a new file and overwrite of that regular file.
python3 - <<'PYPROBE'
import json, os, pathlib, subprocess, tempfile
repo = pathlib.Path.cwd()
with tempfile.TemporaryDirectory() as directory:
    work = pathlib.Path(directory)
    (work / "home").mkdir()
    (work / "output").mkdir()
    (work / "output" / "keep.txt").write_text("keep\n")
    (work / "bundle.json").write_text('{"artifacts":[],"captures":[],"environments":[],"providers":[],"records":[],"scenarios":[],"unknowns":[]}')
    env = dict(os.environ, HOME=str(work / "home"), REA_LOG_LEVEL="silent", NODE_DISABLE_COMPILE_CACHE="1")
    def run(output, *flags):
        command = ["node", str(repo / "scripts/rea.mjs"), "evidence-export", "bundle.json", output, *flags, "--format", "json"]
        result = subprocess.run(command, cwd=work, env=env, capture_output=True, text=True, timeout=120)
        payload = json.loads(result.stdout)
        print("evidence-export bundle.json " + output + " " + " ".join(flags) + " --format json")
        print("exit=" + str(result.returncode) + "; stderr_bytes=" + str(len(result.stderr.encode())))
        if result.returncode:
            print(json.dumps({"code":payload.get("code"),"category":payload.get("category"),"message":payload.get("message"),"remediation":payload.get("remediation"),"details":{k:v for k,v in payload.get("details",{}).items() if k != "path"}}, sort_keys=True))
        else:
            print("success=true; destination_is_file=" + str((work / output).is_file()).lower())
        return result
    for attempt in range(2):
        print("attempt=" + str(attempt + 1))
        assert run("output").returncode == 1
        assert run("output", "--overwrite").returncode == 1
        assert (work / "output").is_dir()
        assert [p.name for p in (work / "output").iterdir()] == ["keep.txt"]
        assert (work / "output" / "keep.txt").read_text() == "keep\n"
    assert run("fresh.json").returncode == 0
    assert run("fresh.json", "--overwrite").returncode == 0
    print("directory_and_keep_file_unchanged=true")
PYPROBE
  1. In each round, the relevant output was exactly the following; only the first round is shown. The script's JSON projection omits the generated absolute details.path while retaining the actual code, category, messages and reason.
attempt=1
evidence-export bundle.json output  --format json
exit=1; stderr_bytes=0
{"category": "invalid_input", "code": "invalid_request", "details": {"operation": "write", "reason": "exists"}, "message": "Evidence file already exists. Choose another path or allow overwrite.", "remediation": {"action": "Evidence file already exists. Choose another path or allow overwrite."}}
evidence-export bundle.json output --overwrite --format json
exit=1; stderr_bytes=0
{"category": "invalid_input", "code": "invalid_request", "details": {"operation": "write", "reason": "not-file"}, "message": "Evidence path does not point to a regular file. Choose a file and try again.", "remediation": {"action": "Evidence path does not point to a regular file. Choose a file and try again."}}
  1. Both file controls exited 0 with empty stderr and success=true; destination_is_file=true. The final sentinel check printed directory_and_keep_file_unchanged=true. The wrong guidance reproduced in 2/2 ordinary attempts; its suggested overwrite failed in 2/2 follow-up attempts.

Expected behavior

Refuse a directory as the output file with an accurate destination-kind explanation and recovery that selects a file path. The first refusal should not offer overwrite as a remedy for a destination the overwrite mode also rejects. Keep the directory and its contents protected.

The EvidenceFileError row of docs/user-visible-errors.csv, line 16, specifies "Reason-specific plain message with safe recovery step." The CLI help describes --overwrite as "Replace output file". docs/cli.md, under "Import, export and compare Evidence", line 562, says "Exports preserve an existing destination unless --overwrite is explicit." That preservation rule does not require accepting a directory as a file; the defect is the inaccurate explanation and unusable offered recovery.

Impact and scope

The observed effect is misleading failure wording and an extra failed attempt when following the recovery action. The tested directory remained intact and its only file retained its original contents. Export to a fresh file and subsequent overwrite of that file both worked with the same source bundle. This reproduction covers a real directory named without a trailing slash on macOS; other filesystem object types, platforms and interfaces were not executed.

REA version

6.8.0 (source commit a905a71e31)

Runtime and platform

Node 24.22.1; macOS 27.0.1 arm64

Target and artifact context

Valid canonical Evidence bundle with seven empty arrays; an existing local directory containing one synthetic text file. No binary target or analysis provider is involved.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed public CLI results appear in the reproduction above. The same built CLI's help advertised --overwrite Replace output file. The successful file controls establish that the source bundle is accepted and the flag is supported; the failing pair establishes that the advice does not apply to this destination. The source bundle is read before destination admission; this report does not claim that the failure performs no reads.

Source-only (not executed as a separate module test): src/cliEvidenceCommands.ts:35 registers the export command and passes its source, output and overwrite flag to the application workflow. src/application/EvidenceBundleCommands.ts:58 reads the source and invokes persistence; src/application/EvidenceBundleFiles.ts:176 validates the bundle and reaches the text writer. src/application/JsonFiles.ts:370 checks existence and refuses without overwrite before the regular-file guard at line 375. src/domain/analysisErrorPresentation.ts:536 maps not-file and exists to the two observed messages.

Typed outcome or recovery expected

Keep the typed invalid-input refusal and existing-output protection, but identify the directory as a non-file destination before offering overwrite. Both flag variants should explain the same destination-kind restriction and advise selecting a file path. No exact replacement sentence is required.

Analysis or suspected cause

Source-derived reasoning: the guard order in src/application/JsonFiles.ts:370 returns exists for any existing destination when overwrite is false. Only an overwrite request reaches the following isFile() check. This explains why changing the flag changes the diagnosis for an unchanged directory. No fix was tested.

Additional context

The registry query npm view rea-agents version returned 6.8.0, matching the tested source version. This was a source-checkout reproduction using its prepared built CLI, not a separate installation of the published package.

Related (closed):

  • https://github.com/morluto/rea/issues/5408 addresses overwrite advice from inspection commands that never support overwrite; its delivery-specific fix is present on this baseline, while standalone export still gives the unusable advice for a directory.
  • https://github.com/morluto/rea/issues/5457 corrects a file-occupied destination diagnosis in environment export, whose output is a directory; its separate output-tree fix is present, while this Evidence file-output case still reproduces.

Open PR #5597 changes overwrite-policy handling for never-overwrite delivery but retains existence-before-file-kind admission in the shared writer. It does not address this demonstrated directory case. No open issue with the same behavior and acceptance criteria was found.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7444 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior Exporting a valid empty Evidence bundle to an existing directory without `--overwrite` returns `invalid_request` with reason `exists`. Both the message and recovery action call the destination an existing file and suggest allowing overwrite. Following that advice with `--overwrite` returns `invalid_request` with reason `not-file`, explaining that the same destination is not a regular file. I observed this pair twice through the public CLI. Both refusals preserved the directory and its existing sentinel file. ### Reproduction 1. Use the source revision below with its dependencies and CLI runtime already built, and put Node.js 24.22.1 on PATH. Start in the checkout root; the following script creates its fixtures inline in a fresh empty disposable directory and redirects the child processes' HOME there. 2. Run this block. Each child invocation uses relative fixture/output arguments, and both failing command variants run twice. The last two calls verify export to a new file and overwrite of that regular file. ```sh python3 - <<'PYPROBE' import json, os, pathlib, subprocess, tempfile repo = pathlib.Path.cwd() with tempfile.TemporaryDirectory() as directory: work = pathlib.Path(directory) (work / "home").mkdir() (work / "output").mkdir() (work / "output" / "keep.txt").write_text("keep\n") (work / "bundle.json").write_text('{"artifacts":[],"captures":[],"environments":[],"providers":[],"records":[],"scenarios":[],"unknowns":[]}') env = dict(os.environ, HOME=str(work / "home"), REA_LOG_LEVEL="silent", NODE_DISABLE_COMPILE_CACHE="1") def run(output, *flags): command = ["node", str(repo / "scripts/rea.mjs"), "evidence-export", "bundle.json", output, *flags, "--format", "json"] result = subprocess.run(command, cwd=work, env=env, capture_output=True, text=True, timeout=120) payload = json.loads(result.stdout) print("evidence-export bundle.json " + output + " " + " ".join(flags) + " --format json") print("exit=" + str(result.returncode) + "; stderr_bytes=" + str(len(result.stderr.encode()))) if result.returncode: print(json.dumps({"code":payload.get("code"),"category":payload.get("category"),"message":payload.get("message"),"remediation":payload.get("remediation"),"details":{k:v for k,v in payload.get("details",{}).items() if k != "path"}}, sort_keys=True)) else: print("success=true; destination_is_file=" + str((work / output).is_file()).lower()) return result for attempt in range(2): print("attempt=" + str(attempt + 1)) assert run("output").returncode == 1 assert run("output", "--overwrite").returncode == 1 assert (work / "output").is_dir() assert [p.name for p in (work / "output").iterdir()] == ["keep.txt"] assert (work / "output" / "keep.txt").read_text() == "keep\n" assert run("fresh.json").returncode == 0 assert run("fresh.json", "--overwrite").returncode == 0 print("directory_and_keep_file_unchanged=true") PYPROBE ``` 3. In each round, the relevant output was exactly the following; only the first round is shown. The script's JSON projection omits the generated absolute `details.path` while retaining the actual code, category, messages and reason. ```text attempt=1 evidence-export bundle.json output --format json exit=1; stderr_bytes=0 {"category": "invalid_input", "code": "invalid_request", "details": {"operation": "write", "reason": "exists"}, "message": "Evidence file already exists. Choose another path or allow overwrite.", "remediation": {"action": "Evidence file already exists. Choose another path or allow overwrite."}} evidence-export bundle.json output --overwrite --format json exit=1; stderr_bytes=0 {"category": "invalid_input", "code": "invalid_request", "details": {"operation": "write", "reason": "not-file"}, "message": "Evidence path does not point to a regular file. Choose a file and try again.", "remediation": {"action": "Evidence path does not point to a regular file. Choose a file and try again."}} ``` 4. Both file controls exited 0 with empty stderr and `success=true; destination_is_file=true`. The final sentinel check printed `directory_and_keep_file_unchanged=true`. The wrong guidance reproduced in 2/2 ordinary attempts; its suggested overwrite failed in 2/2 follow-up attempts. ### Expected behavior Refuse a directory as the output file with an accurate destination-kind explanation and recovery that selects a file path. The first refusal should not offer overwrite as a remedy for a destination the overwrite mode also rejects. Keep the directory and its contents protected. The EvidenceFileError row of `docs/user-visible-errors.csv`, line 16, specifies "Reason-specific plain message with safe recovery step." The CLI help describes `--overwrite` as "Replace output file". `docs/cli.md`, under "Import, export and compare Evidence", line 562, says "Exports preserve an existing destination unless `--overwrite` is explicit." That preservation rule does not require accepting a directory as a file; the defect is the inaccurate explanation and unusable offered recovery. ### Impact and scope The observed effect is misleading failure wording and an extra failed attempt when following the recovery action. The tested directory remained intact and its only file retained its original contents. Export to a fresh file and subsequent overwrite of that file both worked with the same source bundle. This reproduction covers a real directory named without a trailing slash on macOS; other filesystem object types, platforms and interfaces were not executed. ### REA version 6.8.0 (source commit a905a71e31f71457cb21fd056b234f872126974e) ### Runtime and platform Node 24.22.1; macOS 27.0.1 arm64 ### Target and artifact context Valid canonical Evidence bundle with seven empty arrays; an existing local directory containing one synthetic text file. No binary target or analysis provider is involved. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Observed public CLI results appear in the reproduction above. The same built CLI's help advertised `--overwrite Replace output file`. The successful file controls establish that the source bundle is accepted and the flag is supported; the failing pair establishes that the advice does not apply to this destination. The source bundle is read before destination admission; this report does not claim that the failure performs no reads. Source-only (not executed as a separate module test): `src/cliEvidenceCommands.ts:35` registers the export command and passes its source, output and overwrite flag to the application workflow. `src/application/EvidenceBundleCommands.ts:58` reads the source and invokes persistence; `src/application/EvidenceBundleFiles.ts:176` validates the bundle and reaches the text writer. `src/application/JsonFiles.ts:370` checks existence and refuses without overwrite before the regular-file guard at line 375. `src/domain/analysisErrorPresentation.ts:536` maps `not-file` and `exists` to the two observed messages. ### Typed outcome or recovery expected Keep the typed invalid-input refusal and existing-output protection, but identify the directory as a non-file destination before offering overwrite. Both flag variants should explain the same destination-kind restriction and advise selecting a file path. No exact replacement sentence is required. ### Analysis or suspected cause Source-derived reasoning: the guard order in `src/application/JsonFiles.ts:370` returns `exists` for any existing destination when overwrite is false. Only an overwrite request reaches the following `isFile()` check. This explains why changing the flag changes the diagnosis for an unchanged directory. No fix was tested. ### Additional context The registry query `npm view rea-agents version` returned 6.8.0, matching the tested source version. This was a source-checkout reproduction using its prepared built CLI, not a separate installation of the published package. Related (closed): - https://github.com/morluto/rea/issues/5408 addresses overwrite advice from inspection commands that never support overwrite; its delivery-specific fix is present on this baseline, while standalone export still gives the unusable advice for a directory. - https://github.com/morluto/rea/issues/5457 corrects a file-occupied destination diagnosis in environment export, whose output is a directory; its separate output-tree fix is present, while this Evidence file-output case still reproduces. Open PR #5597 changes overwrite-policy handling for never-overwrite delivery but retains existence-before-file-kind admission in the shared writer. It does not address this demonstrated directory case. No open issue with the same behavior and acceptance criteria was found.
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#4448
No description provided.