[GH-ISSUE #7688] [Bug] Environment bundle CLI classifies a regular-file parent as a host write failure #4610

Open
opened 2026-10-31 00:51:18 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

CLI

Observed behavior

Selecting an absent bundle destination whose immediate parent is a regular file makes both environment bundle CLI commands return unreadable_output / execution_failure, with no destination-field issue or input-contract recovery:

node scripts/rea.mjs environment-export "$case/manifest.json" --evidence "$case/capture.json" "$case/parentfile/bundle" --json
node scripts/rea.mjs environment-build "$case/capture.json" "$case/parentfile/bundle" --json

Both exit 1. Their reason identifies EEXIST from creating parentfile, but the remediation suggests shortening the destination or freeing disk space and inspecting partial bundle output. The parent contains occupied plus a newline; no bundle was created, and its contents and filesystem identity remain unchanged.

The same destination supplied to the production stdio MCP tool returns invalid_request / invalid_input, an invalid_value issue on output_directory, and input-contract recovery:

{"name":"export_environment_bundle","arguments":{"source":{"kind":"evidence-file","path":"<case>/capture.json"},"output_directory":"<case>/parentfile/bundle","dry_run":false}}

The MCP message is Extraction output parent must be a directory. This report concerns the CLI's error family and field recovery; accepting this destination is not expected.

Reproduction

  1. Start in the stated source checkout with dependencies and the CLI already built, and put the repository-pinned Node.js v24.22.1 on PATH.
  2. Run this Python block from the checkout root. It generates valid synthetic capture/manifest inputs in-process, isolates HOME/XDG/temporary paths, and places inert npx/npm/bun fixtures first on PATH. It uses the production CLI and MCP stdio server; no external provider or captured URL is contacted.
  3. The block checks two calls per CLI command and two MCP calls, fresh-destination success controls, and a file directly at the destination. It removes its temporary files afterward.
import json, os, pathlib, shutil, subprocess, tempfile

repo = pathlib.Path.cwd()
node = shutil.which('node')
assert node
with tempfile.TemporaryDirectory(prefix='rea-parent-file-') as temporary:
    case = pathlib.Path(temporary).resolve()
    env = {'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', 'XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP'):
        folder = case / key.lower()
        folder.mkdir()
        env[key] = str(folder)
    fixture_bin = case / 'bin'
    fixture_bin.mkdir()
    for name in ('npx', 'npm', 'bun'):
        executable = fixture_bin / name
        executable.write_text('#!/bin/sh\nexit 127\n')
        executable.chmod(0o700)
    env['PATH'] = str(fixture_bin) + os.pathsep + os.environ['PATH']
    def cli(*arguments):
        result = subprocess.run([node, 'scripts/rea.mjs', *map(str, arguments), '--json'],
                                cwd=repo, env=env, capture_output=True, text=True, timeout=90)
        assert not result.stderr, result.stderr
        return result.returncode, json.loads(result.stdout)
    subprocess.run([node, '--import', 'tsx', '--input-type=module', '-e',
        'import {writeEnvironmentServeFiles} from "./tests/fixtures/environmentServe.ts";'
        'await writeEnvironmentServeFiles(process.env.CASE_DIR);'],
        cwd=repo, env={**env, 'CASE_DIR': str(case)}, check=True, timeout=45)
    parent = case / 'parentfile'
    parent.write_bytes(b'occupied\n')
    before = parent.stat()
    for command in ('environment-export', 'environment-build'):
        source = ([case / 'manifest.json', '--evidence', case / 'capture.json']
                  if command == 'environment-export' else [case / 'capture.json'])
        for attempt in (1, 2):
            code, data = cli(command, *source, parent / 'bundle')
            assert code == 1 and data['code'] == 'unreadable_output'
            assert data['category'] == 'execution_failure'
            assert 'EEXIST' in data['details']['reason']
            assert 'issues' not in data['details']
            print(command, attempt, code, data['code'], data['category'],
                  data['details']['reason'].replace(str(case), '<case>'))
        code, data = cli(command, *source, case / (command + '-fresh'))
        assert code == 0 and data['status'] in ('exported', 'built')
        for entry in data['files']:
            assert (case / (command + '-fresh') / entry['path']).stat().st_size == entry['bytes']
        print(command, 'fresh control', code, data['status'])
    code, data = cli('environment-export', case / 'manifest.json', '--evidence',
                     case / 'capture.json', parent)
    assert code == 1 and data['code'] == 'invalid_request'
    assert data['details']['issues'][0]['path'] == ['directory']
    print('file at destination control', code, data['code'], data['category'])
    probe = case / 'mcp.mjs'
    probe.write_text('''const repo = process.cwd();
const {Client} = await import(repo + "/node_modules/@modelcontextprotocol/client/dist/index.mjs");
const {StdioClientTransport} = await import(repo + "/node_modules/@modelcontextprotocol/client/dist/stdio.mjs");
const client = new Client({name:"parent-file-verifier",version:"1.0.0"});
const transport = new StdioClientTransport({command:process.execPath,
  args:["scripts/rea.mjs","mcp"],cwd:repo,env:process.env,stderr:"pipe"});
try {
  await client.connect(transport);
  for (let i = 0; i < 2; i++) {
    const response = await client.callTool({name:"export_environment_bundle",arguments:{
      source:{kind:"evidence-file",path:process.argv[2]},
      output_directory:process.argv[3],dry_run:false}});
    console.log(response.content[0].text);
  }
} finally { await client.close(); }
''')
    result = subprocess.run([node, str(probe), str(case / 'capture.json'), str(parent / 'bundle')],
                            cwd=repo, env=env, capture_output=True, text=True, timeout=90, check=True)
    for line in result.stdout.splitlines():
        error = json.loads(line)['error']
        assert error['code'] == 'invalid_request' and error['category'] == 'invalid_input'
        assert error['message'] == 'Extraction output parent must be a directory'
        assert error['details']['issues'][0]['path'] == ['output_directory']
        print('MCP', error['code'], error['category'], error['message'])
    after = parent.stat()
    assert parent.read_bytes() == b'occupied\n'
    assert (before.st_dev, before.st_ino) == (after.st_dev, after.st_ino)
    assert not (parent / 'bundle').exists()
    print('Parent contents and identity preserved; bundle absent')
print('Fixture, isolated configuration and output directories removed')

The final recipe reproduced the CLI classification in 2/2 export and 2/2 build calls, and the MCP input refusal in 2/2 calls. An earlier independent replay returned the same results. Both fresh-destination controls succeeded, and the final recipe verified every reported output file's byte count. The file-at-destination control returned an input refusal.

Expected behavior

Keep refusing a destination whose parent is a regular file, preserve that file, and classify the failed destination precondition as invalid_request / invalid_input, with an invalid_value issue on the CLI's directory argument and input-contract recovery. Explain that a directory is required or advise selecting a usable destination. No exact replacement sentence is required.

The command guide distinguishes non-writing destination refusals from later host-write failures. Its file-occupied example explicitly covers the destination itself, so that sentence alone does not enumerate every parent-chain case. The general tool-design contract requires expected input failures to retain caller field/index paths, and the shared destination admission code explicitly treats an unavailable parent as a destination-selection refusal. The executed MCP comparison confirms that this exact parent-file condition already has an input-refusal outcome. Contract and implementation locators appear in the evidence section.

CLI and MCP have different documented destination policies: CLI accepts absent or empty output directories and can create missing parent directories; MCP requires an absent absolute destination with an existing parent. This report does not require eliminating those differences or changing valid parent creation.

Impact and scope

The CLI exposes the wrong public error code/category and omits machine-readable destination correction for a deterministic caller input mistake. Its recovery describes host capacity and partial output even though no bundle was created. The underlying EEXIST reason remains available to a caller who inspects it.

This is a bounded P2 recovery-contract defect. A safe workaround was demonstrated: the same input files successfully exported/built to fresh usable destinations. No file overwrite, data loss, permission failure, actual downstream automated misrouting, or security-boundary crossing was observed. Tested scope is an immediate regular-file parent on macOS, without package-tarball or dry-run options. Relative paths, symlink parents, deeper parent chains, Windows, Linux and launching the resulting bundle were not tested.

REA version

Exact output of node scripts/rea.mjs --version:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a. This verification used the prepared source launcher, not a separate installation of the published package.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 arm64; Python 3 driver and the checkout's pinned MCP client library. No external analysis provider was used.

Target and artifact context

Synthetic Grafana-shaped capture Evidence and its authenticated environment manifest, produced with the repository fixture builder. The rejected output is an absent child of a regular file containing occupied plus a newline.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Observed output from the exact reproduction block above:

environment-export 1 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile'
environment-export 2 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile'
environment-export fresh control 0 exported
environment-build 1 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile'
environment-build 2 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile'
environment-build fresh control 0 built
file at destination control 1 invalid_request invalid_input
MCP invalid_request invalid_input Extraction output parent must be a directory
MCP invalid_request invalid_input Extraction output parent must be a directory
Parent contents and identity preserved; bundle absent
Fixture, isolated configuration and output directories removed

Selected fields from the export CLI failure, with only the generated case path abbreviated:

{
  "code": "unreadable_output",
  "category": "execution_failure",
  "retryable": false,
  "remediation": {
    "action": "Resolve the reported host failure (for example, choose a shorter destination path or free disk space). Inspect <case>/parentfile/bundle and remove any partial output this export wrote before retrying, since a retry refuses a non-empty directory."
  },
  "details": {
    "operation": "environment-export",
    "reason": "EEXIST: file already exists, mkdir '<case>/parentfile'",
    "diagnostics": {"output_directory": "<case>/parentfile/bundle"}
  }
}

details.issues and recovery are absent. Build exposes the same bundle-stage operation. The MCP error instead supplies recovery: {"kind":"inspect_input_contract","operation":"export_environment_bundle"} and details.issues: [{"path":["output_directory"],"reason":"invalid_value","message":"Extraction output parent must be a directory"}].

Contract locators at the baseline: docs/cli.md:1027-1037 (destination refusals versus host-write failures), docs/tool-design.md:348-351 (caller field/index paths for expected input failures), and docs/agent-environments.md:100-108,218-233 (bundle and adapter destination policies).

Source-derived explanation: src/application/environment/environmentBundleWrite.ts:153-158 attempts recursive parent mkdir before output-tree admission. src/artifacts/SafeOutputTree.ts:200-218 contains the regular-file-parent guard, and src/artifacts/SafeOutputTreeAdmission.ts:4-43 classifies destination refusals and translates them into input issues. src/application/environment/environmentExport.ts:484-497 maps the earlier plain system error into the observed output failure. The MCP path checks destination admission first at src/application/environment/environmentBundleTool.ts:57-65.

Analysis or suspected cause

The observed results and inspected control flow agree: CLI's recursive parent creation fails before the shared parent admission guard can provide its typed refusal. The resulting host exception reaches generic write-failure projection. MCP runs the guard first and retains the caller field. This explains the classification difference; no fix was implemented or tested.

Additional context

Open and closed issues, open/closed/merged PRs, issue comments and relevant PR review comments were searched and read. Related reports have different closure criteria:

  • #5457 and closed PR #5495 concern the identity-change explanation for a file at the destination itself. The direct-file control now returns the corrected input refusal; this parent-file case does not establish a regression of that fix.
  • #5458 concerns read-versus-write permission diagnostics. This trigger is not a permission denial.
  • #5522 and merged PR #5581 concern failures after admission or bundle writes, where retaining partial output and host-failure recovery is appropriate. Here the directory precondition fails before admission.
  • #7396 concerns extraction terminology in MCP's otherwise correct input refusal. Changing that wording would not fix the CLI classification.
  • #7571 concerns directories left by a refused trailing dot-dot path. Its open PR #7588 adds syntax validation before parent creation; the inspected diff leaves the regular-file-parent mkdir classification path unchanged.
  • #7444, #7523 and #7550 concern Evidence overwrite advice, MCP source-field attribution and ADB output admission respectively.

No matching report was found in the completed searches. No known-good version or regression interval was established. Prepared with AI assistance and independently reproduced; no checkout changes or GitHub publication occurred.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Both bundle CLI commands return the wrong public error category and omit caller-field/input-contract recovery for the reproduced deterministic destination precondition; the effect goes beyond message terminology.
  • Workaround status: available
  • Workaround basis: Both commands succeeded with the same authenticated inputs when given fresh usable destinations, with all reported file byte counts checked; the occupied parent file remained intact.
Originally created by @coygeek on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7688 ### 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 CLI ### Observed behavior Selecting an absent bundle destination whose immediate parent is a regular file makes both environment bundle CLI commands return `unreadable_output` / `execution_failure`, with no destination-field issue or input-contract recovery: ```sh node scripts/rea.mjs environment-export "$case/manifest.json" --evidence "$case/capture.json" "$case/parentfile/bundle" --json node scripts/rea.mjs environment-build "$case/capture.json" "$case/parentfile/bundle" --json ``` Both exit 1. Their reason identifies `EEXIST` from creating `parentfile`, but the remediation suggests shortening the destination or freeing disk space and inspecting partial bundle output. The parent contains `occupied` plus a newline; no bundle was created, and its contents and filesystem identity remain unchanged. The same destination supplied to the production stdio MCP tool returns `invalid_request` / `invalid_input`, an `invalid_value` issue on `output_directory`, and input-contract recovery: ```json {"name":"export_environment_bundle","arguments":{"source":{"kind":"evidence-file","path":"<case>/capture.json"},"output_directory":"<case>/parentfile/bundle","dry_run":false}} ``` The MCP message is `Extraction output parent must be a directory`. This report concerns the CLI's error family and field recovery; accepting this destination is not expected. ### Reproduction 1. Start in the stated source checkout with dependencies and the CLI already built, and put the repository-pinned Node.js v24.22.1 on PATH. 2. Run this Python block from the checkout root. It generates valid synthetic capture/manifest inputs in-process, isolates HOME/XDG/temporary paths, and places inert npx/npm/bun fixtures first on PATH. It uses the production CLI and MCP stdio server; no external provider or captured URL is contacted. 3. The block checks two calls per CLI command and two MCP calls, fresh-destination success controls, and a file directly at the destination. It removes its temporary files afterward. ```python import json, os, pathlib, shutil, subprocess, tempfile repo = pathlib.Path.cwd() node = shutil.which('node') assert node with tempfile.TemporaryDirectory(prefix='rea-parent-file-') as temporary: case = pathlib.Path(temporary).resolve() env = {'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', 'XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP'): folder = case / key.lower() folder.mkdir() env[key] = str(folder) fixture_bin = case / 'bin' fixture_bin.mkdir() for name in ('npx', 'npm', 'bun'): executable = fixture_bin / name executable.write_text('#!/bin/sh\nexit 127\n') executable.chmod(0o700) env['PATH'] = str(fixture_bin) + os.pathsep + os.environ['PATH'] def cli(*arguments): result = subprocess.run([node, 'scripts/rea.mjs', *map(str, arguments), '--json'], cwd=repo, env=env, capture_output=True, text=True, timeout=90) assert not result.stderr, result.stderr return result.returncode, json.loads(result.stdout) subprocess.run([node, '--import', 'tsx', '--input-type=module', '-e', 'import {writeEnvironmentServeFiles} from "./tests/fixtures/environmentServe.ts";' 'await writeEnvironmentServeFiles(process.env.CASE_DIR);'], cwd=repo, env={**env, 'CASE_DIR': str(case)}, check=True, timeout=45) parent = case / 'parentfile' parent.write_bytes(b'occupied\n') before = parent.stat() for command in ('environment-export', 'environment-build'): source = ([case / 'manifest.json', '--evidence', case / 'capture.json'] if command == 'environment-export' else [case / 'capture.json']) for attempt in (1, 2): code, data = cli(command, *source, parent / 'bundle') assert code == 1 and data['code'] == 'unreadable_output' assert data['category'] == 'execution_failure' assert 'EEXIST' in data['details']['reason'] assert 'issues' not in data['details'] print(command, attempt, code, data['code'], data['category'], data['details']['reason'].replace(str(case), '<case>')) code, data = cli(command, *source, case / (command + '-fresh')) assert code == 0 and data['status'] in ('exported', 'built') for entry in data['files']: assert (case / (command + '-fresh') / entry['path']).stat().st_size == entry['bytes'] print(command, 'fresh control', code, data['status']) code, data = cli('environment-export', case / 'manifest.json', '--evidence', case / 'capture.json', parent) assert code == 1 and data['code'] == 'invalid_request' assert data['details']['issues'][0]['path'] == ['directory'] print('file at destination control', code, data['code'], data['category']) probe = case / 'mcp.mjs' probe.write_text('''const repo = process.cwd(); const {Client} = await import(repo + "/node_modules/@modelcontextprotocol/client/dist/index.mjs"); const {StdioClientTransport} = await import(repo + "/node_modules/@modelcontextprotocol/client/dist/stdio.mjs"); const client = new Client({name:"parent-file-verifier",version:"1.0.0"}); const transport = new StdioClientTransport({command:process.execPath, args:["scripts/rea.mjs","mcp"],cwd:repo,env:process.env,stderr:"pipe"}); try { await client.connect(transport); for (let i = 0; i < 2; i++) { const response = await client.callTool({name:"export_environment_bundle",arguments:{ source:{kind:"evidence-file",path:process.argv[2]}, output_directory:process.argv[3],dry_run:false}}); console.log(response.content[0].text); } } finally { await client.close(); } ''') result = subprocess.run([node, str(probe), str(case / 'capture.json'), str(parent / 'bundle')], cwd=repo, env=env, capture_output=True, text=True, timeout=90, check=True) for line in result.stdout.splitlines(): error = json.loads(line)['error'] assert error['code'] == 'invalid_request' and error['category'] == 'invalid_input' assert error['message'] == 'Extraction output parent must be a directory' assert error['details']['issues'][0]['path'] == ['output_directory'] print('MCP', error['code'], error['category'], error['message']) after = parent.stat() assert parent.read_bytes() == b'occupied\n' assert (before.st_dev, before.st_ino) == (after.st_dev, after.st_ino) assert not (parent / 'bundle').exists() print('Parent contents and identity preserved; bundle absent') print('Fixture, isolated configuration and output directories removed') ``` The final recipe reproduced the CLI classification in 2/2 export and 2/2 build calls, and the MCP input refusal in 2/2 calls. An earlier independent replay returned the same results. Both fresh-destination controls succeeded, and the final recipe verified every reported output file's byte count. The file-at-destination control returned an input refusal. ### Expected behavior Keep refusing a destination whose parent is a regular file, preserve that file, and classify the failed destination precondition as `invalid_request` / `invalid_input`, with an `invalid_value` issue on the CLI's `directory` argument and input-contract recovery. Explain that a directory is required or advise selecting a usable destination. No exact replacement sentence is required. The command guide distinguishes non-writing destination refusals from later host-write failures. Its file-occupied example explicitly covers the destination itself, so that sentence alone does not enumerate every parent-chain case. The general tool-design contract requires expected input failures to retain caller field/index paths, and the shared destination admission code explicitly treats an unavailable parent as a destination-selection refusal. The executed MCP comparison confirms that this exact parent-file condition already has an input-refusal outcome. Contract and implementation locators appear in the evidence section. CLI and MCP have different documented destination policies: CLI accepts absent or empty output directories and can create missing parent directories; MCP requires an absent absolute destination with an existing parent. This report does not require eliminating those differences or changing valid parent creation. ### Impact and scope The CLI exposes the wrong public error code/category and omits machine-readable destination correction for a deterministic caller input mistake. Its recovery describes host capacity and partial output even though no bundle was created. The underlying EEXIST reason remains available to a caller who inspects it. This is a bounded P2 recovery-contract defect. A safe workaround was demonstrated: the same input files successfully exported/built to fresh usable destinations. No file overwrite, data loss, permission failure, actual downstream automated misrouting, or security-boundary crossing was observed. Tested scope is an immediate regular-file parent on macOS, without package-tarball or dry-run options. Relative paths, symlink parents, deeper parent chains, Windows, Linux and launching the resulting bundle were not tested. ### REA version Exact output of `node scripts/rea.mjs --version`: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. This verification used the prepared source launcher, not a separate installation of the published package. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1 arm64; Python 3 driver and the checkout's pinned MCP client library. No external analysis provider was used. ### Target and artifact context Synthetic Grafana-shaped capture Evidence and its authenticated environment manifest, produced with the repository fixture builder. The rejected output is an absent child of a regular file containing `occupied` plus a newline. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Observed output from the exact reproduction block above: ```text environment-export 1 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile' environment-export 2 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile' environment-export fresh control 0 exported environment-build 1 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile' environment-build 2 1 unreadable_output execution_failure EEXIST: file already exists, mkdir '<case>/parentfile' environment-build fresh control 0 built file at destination control 1 invalid_request invalid_input MCP invalid_request invalid_input Extraction output parent must be a directory MCP invalid_request invalid_input Extraction output parent must be a directory Parent contents and identity preserved; bundle absent Fixture, isolated configuration and output directories removed ``` Selected fields from the export CLI failure, with only the generated case path abbreviated: ```json { "code": "unreadable_output", "category": "execution_failure", "retryable": false, "remediation": { "action": "Resolve the reported host failure (for example, choose a shorter destination path or free disk space). Inspect <case>/parentfile/bundle and remove any partial output this export wrote before retrying, since a retry refuses a non-empty directory." }, "details": { "operation": "environment-export", "reason": "EEXIST: file already exists, mkdir '<case>/parentfile'", "diagnostics": {"output_directory": "<case>/parentfile/bundle"} } } ``` `details.issues` and `recovery` are absent. Build exposes the same bundle-stage operation. The MCP error instead supplies `recovery: {"kind":"inspect_input_contract","operation":"export_environment_bundle"}` and `details.issues: [{"path":["output_directory"],"reason":"invalid_value","message":"Extraction output parent must be a directory"}]`. Contract locators at the baseline: `docs/cli.md:1027-1037` (destination refusals versus host-write failures), `docs/tool-design.md:348-351` (caller field/index paths for expected input failures), and `docs/agent-environments.md:100-108,218-233` (bundle and adapter destination policies). Source-derived explanation: `src/application/environment/environmentBundleWrite.ts:153-158` attempts recursive parent mkdir before output-tree admission. `src/artifacts/SafeOutputTree.ts:200-218` contains the regular-file-parent guard, and `src/artifacts/SafeOutputTreeAdmission.ts:4-43` classifies destination refusals and translates them into input issues. `src/application/environment/environmentExport.ts:484-497` maps the earlier plain system error into the observed output failure. The MCP path checks destination admission first at `src/application/environment/environmentBundleTool.ts:57-65`. ### Analysis or suspected cause The observed results and inspected control flow agree: CLI's recursive parent creation fails before the shared parent admission guard can provide its typed refusal. The resulting host exception reaches generic write-failure projection. MCP runs the guard first and retains the caller field. This explains the classification difference; no fix was implemented or tested. ### Additional context Open and closed issues, open/closed/merged PRs, issue comments and relevant PR review comments were searched and read. Related reports have different closure criteria: - [#5457](https://github.com/morluto/rea/issues/5457) and closed [PR #5495](https://github.com/morluto/rea/pull/5495) concern the identity-change explanation for a file at the destination itself. The direct-file control now returns the corrected input refusal; this parent-file case does not establish a regression of that fix. - [#5458](https://github.com/morluto/rea/issues/5458) concerns read-versus-write permission diagnostics. This trigger is not a permission denial. - [#5522](https://github.com/morluto/rea/issues/5522) and merged [PR #5581](https://github.com/morluto/rea/pull/5581) concern failures after admission or bundle writes, where retaining partial output and host-failure recovery is appropriate. Here the directory precondition fails before admission. - [#7396](https://github.com/morluto/rea/issues/7396) concerns extraction terminology in MCP's otherwise correct input refusal. Changing that wording would not fix the CLI classification. - [#7571](https://github.com/morluto/rea/issues/7571) concerns directories left by a refused trailing dot-dot path. Its open [PR #7588](https://github.com/morluto/rea/pull/7588) adds syntax validation before parent creation; the inspected diff leaves the regular-file-parent mkdir classification path unchanged. - [#7444](https://github.com/morluto/rea/issues/7444), [#7523](https://github.com/morluto/rea/issues/7523) and [#7550](https://github.com/morluto/rea/issues/7550) concern Evidence overwrite advice, MCP source-field attribution and ADB output admission respectively. No matching report was found in the completed searches. No known-good version or regression interval was established. Prepared with AI assistance and independently reproduced; no checkout changes or GitHub publication occurred. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Both bundle CLI commands return the wrong public error category and omit caller-field/input-contract recovery for the reproduced deterministic destination precondition; the effect goes beyond message terminology. - Workaround status: available - Workaround basis: Both commands succeeded with the same authenticated inputs when given fresh usable destinations, with all reported file byte counts checked; the occupied parent file remained intact.
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#4610
No description provided.