From f1dad07d4f51a823bd3471651a1cf693aa64d7c3 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 11 Nov 2026 21:42:06 +0000 Subject: [PATCH 1/6] Default fresh hosts to a data disk for pools and caches --- blueprint/ansible/tasks/preflight.yml | 22 ++++++++++++++++++++++ blueprint/config/default.yml | 3 ++- blueprint/schemas/crewship.schema.json | 2 +- changelog.d/1339.added.md | 1 + docs/configuration.md | 7 +++++-- tests/test_chart.py | 1 + tests/test_configuration.py | 6 ++++++ 7 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1339.added.md diff --git a/blueprint/ansible/tasks/preflight.yml b/blueprint/ansible/tasks/preflight.yml index cbdea5eaf..07281d177 100644 --- a/blueprint/ansible/tasks/preflight.yml +++ b/blueprint/ansible/tasks/preflight.yml @@ -6,6 +6,28 @@ ansible.builtin.set_fact: crewship_cfg: "{{ crewship_cfg }}" +# `data_dir: auto` (the default) takes the largest mounted data disk, but only on a fresh account. +# An account that already has ~/.treehouse keeps its legacy layout; `data_dir: ""` always opts out. +- name: Look for a legacy worktree pool under the home + ansible.builtin.stat: + path: "{{ crewship_cfg.home }}/.treehouse" + register: crewship_legacy_pools + when: crewship_cfg.data_dir == 'auto' + +- name: Resolve the automatic data disk + ansible.builtin.set_fact: + crewship_cfg: >- + {{ crewship_cfg | combine({'data_dir': ( + ansible_facts.mounts + | selectattr('device', 'match', '^/dev/(?!loop)') + | selectattr('fstype', 'in', ['ext4', 'xfs', 'btrfs']) + | selectattr('size_total', 'ge', 107374182400) + | selectattr('mount', 'match', '^(?!/(boot|snap|usr|var)(/|$))/[A-Za-z0-9_-][A-Za-z0-9_./-]*$') + | rejectattr('mount', 'equalto', '/') + | sort(attribute='size_total') | map(attribute='mount') | list | last | default('') + ) if not crewship_legacy_pools.stat.exists else ''}) }} + when: crewship_cfg.data_dir == 'auto' + - name: Assert the configuration document matches the contract ansible.builtin.assert: that: diff --git a/blueprint/config/default.yml b/blueprint/config/default.yml index f36fc16b9..ffc224f22 100644 --- a/blueprint/config/default.yml +++ b/blueprint/config/default.yml @@ -6,8 +6,9 @@ crewship: start_services: true enable_linger: true # Existing mounted data path for new bulk defaults and Docker's data root. + # `auto` uses the largest mounted data disk (100 GiB or more) on a fresh account; "" keeps the system disk. # See docs/configuration.md#data-disk for path requirements and storage behavior. - data_dir: "" + data_dir: auto # A new host gets the core only: agents and firstmate. Every other profile is # opt-in (docs/default-config.md). profiles: diff --git a/blueprint/schemas/crewship.schema.json b/blueprint/schemas/crewship.schema.json index 4cfde5bf2..2392a5da1 100644 --- a/blueprint/schemas/crewship.schema.json +++ b/blueprint/schemas/crewship.schema.json @@ -31,7 +31,7 @@ }, "data_dir": { "type": "string", - "pattern": "^(/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$" + "pattern": "^(auto|(/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*)$" }, "usage": { "type": "object", diff --git a/changelog.d/1339.added.md b/changelog.d/1339.added.md new file mode 100644 index 000000000..dc31c4cd4 --- /dev/null +++ b/changelog.d/1339.added.md @@ -0,0 +1 @@ +Resolve `crewship.data_dir: auto` (the new default) to the largest mounted data disk of 100 GiB or more on a fresh account, so pools and tool caches start off the system disk. Accounts with an existing `~/.treehouse` keep the legacy layout; `data_dir: ""` opts out. diff --git a/docs/configuration.md b/docs/configuration.md index dc9e31a08..8f60dfe3e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -160,7 +160,7 @@ crewship: workspace: /home/coder/Dev start_services: true enable_linger: true - data_dir: "" # See Data disk below for path requirements and storage behavior + data_dir: auto # See Data disk below for path requirements and storage behavior profiles: agents: true # Chrome autoprune, AXI tools, browser defaults development: false # Rust, build tools @@ -402,7 +402,10 @@ The limit on the number of Firstmate homes is a Firstmate setting; Crewship does ## Data disk Set `data_dir` to an existing absolute path on a mounted data disk. -On a fresh account, the default `""` keeps the legacy storage layout and writes no bulk data-location defaults (the [Worktree cap](#worktree-cap) is still written). +The default `auto` picks the largest mounted ext4, xfs, or btrfs disk of 100 GiB or more outside `/`, `/boot`, `/snap`, `/usr`, and `/var`. +It applies only to a fresh account: an account that already has `~/.treehouse` keeps the legacy layout, so Docker data and existing pools never move. +Set `""` to keep the legacy storage layout; it writes no bulk data-location defaults (the [Worktree cap](#worktree-cap) is still written). +Without a qualifying disk, `auto` behaves like `""`. Clearing `data_dir` removes the managed Bash block and user-manager file. Apply and preview tolerate a missing `.bashrc`. Native tool keys and existing data remain unchanged. diff --git a/tests/test_chart.py b/tests/test_chart.py index 5d65cc65b..77eea21fb 100644 --- a/tests/test_chart.py +++ b/tests/test_chart.py @@ -362,6 +362,7 @@ def test_chart_creates_only_missing_directories(tmp_path, existing_home, profile "crewship_repo": str(ROOT), "crewship_blueprint": str(ROOT / "blueprint"), "crewship": { "home": str(home), "workspace": str(home / "Dev"), "user": getpass.getuser(), + "data_dir": "", "profiles": {"fleet_guards": False, "fleet_browsers": False, "firstmate": False, **profiles}, "start_services": False, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 0f53cfb9a..5c0a2a3aa 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -262,6 +262,12 @@ def test_data_dir_must_be_a_plain_absolute_path(configuration, data_dir): ship.validate_config(configuration) +@pytest.mark.parametrize("data_dir", ["auto", "", "/mnt/data"]) +def test_data_dir_accepts_auto_empty_or_a_path(configuration, data_dir): + configuration["crewship"]["data_dir"] = data_dir + ship.validate_config(configuration) + + def test_bad_polling_window_cannot_disable_idle_accrual(configuration): configuration["crewship"]["browser_prune"]["max_gap_seconds"] = 120 with pytest.raises(ValueError, match="observation intervals"): From 1c20a8a8dfb1efa789a1fddfb09083935837b95c Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 11 Nov 2026 22:04:08 +0000 Subject: [PATCH 2/6] no-mistakes(review): Persist auto data disk from bulk defaults, fail if unmounted --- blueprint/ansible/tasks/data_dir.yml | 43 +++++++++++++++++++++++ blueprint/ansible/tasks/preflight.yml | 20 +---------- docs/configuration.md | 5 +-- tests/test_configuration.py | 50 +++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 blueprint/ansible/tasks/data_dir.yml diff --git a/blueprint/ansible/tasks/data_dir.yml b/blueprint/ansible/tasks/data_dir.yml new file mode 100644 index 000000000..197c56e42 --- /dev/null +++ b/blueprint/ansible/tasks/data_dir.yml @@ -0,0 +1,43 @@ +--- +# `data_dir: auto` (the default) keeps the disk the managed bulk defaults already name, and fails if +# that disk is no longer mounted. Otherwise it takes the largest mounted data disk, unless the +# account already has ~/.treehouse (legacy layout). `data_dir: ""` always opts out. +- name: Read the data disk recorded by an earlier apply + ansible.builtin.slurp: + src: "{{ crewship_bulk_defaults_path }}" + register: crewship_recorded_bulk + failed_when: false + +- name: Look for a legacy worktree pool under the home + ansible.builtin.stat: + path: "{{ crewship_cfg.home }}/.treehouse" + register: crewship_legacy_pools + +- name: Resolve the automatic data disk + ansible.builtin.set_fact: + crewship_recorded_data_dir: >- + {{ (crewship_recorded_bulk.content | default('') | b64decode) + | regex_search('(?m)^TMPDIR=\$\{TMPDIR:-(/.*)/tmp\}$', '\1') | default([''], true) | first }} + crewship_data_candidate: >- + {{ ansible_facts.mounts + | selectattr('device', 'match', '^/dev/(?!loop)') + | selectattr('fstype', 'in', ['ext4', 'xfs', 'btrfs']) + | selectattr('size_total', 'ge', 107374182400) + | selectattr('mount', 'match', '^(?!/(boot|snap|usr|var|home|media|run|tmp)(/|$))/[A-Za-z0-9_-][A-Za-z0-9_./-]*$') + | sort(attribute='size_total') | map(attribute='mount') | list | last | default('') }} + +- name: Require the recorded data disk to stay mounted + ansible.builtin.assert: + that: crewship_recorded_data_dir in (ansible_facts.mounts | map(attribute='mount') | list) + fail_msg: >- + The data disk {{ crewship_recorded_data_dir }} recorded by an earlier apply is not mounted. + Mount it, or set crewship.data_dir explicitly. + quiet: true + when: crewship_recorded_data_dir | length > 0 + +- name: Apply the automatic data disk + ansible.builtin.set_fact: + crewship_cfg: >- + {{ crewship_cfg | combine({'data_dir': ( + crewship_recorded_data_dir if crewship_recorded_data_dir | length > 0 + else (crewship_data_candidate if not crewship_legacy_pools.stat.exists else '')) }) }} diff --git a/blueprint/ansible/tasks/preflight.yml b/blueprint/ansible/tasks/preflight.yml index 07281d177..2bd1d83b5 100644 --- a/blueprint/ansible/tasks/preflight.yml +++ b/blueprint/ansible/tasks/preflight.yml @@ -6,26 +6,8 @@ ansible.builtin.set_fact: crewship_cfg: "{{ crewship_cfg }}" -# `data_dir: auto` (the default) takes the largest mounted data disk, but only on a fresh account. -# An account that already has ~/.treehouse keeps its legacy layout; `data_dir: ""` always opts out. -- name: Look for a legacy worktree pool under the home - ansible.builtin.stat: - path: "{{ crewship_cfg.home }}/.treehouse" - register: crewship_legacy_pools - when: crewship_cfg.data_dir == 'auto' - - name: Resolve the automatic data disk - ansible.builtin.set_fact: - crewship_cfg: >- - {{ crewship_cfg | combine({'data_dir': ( - ansible_facts.mounts - | selectattr('device', 'match', '^/dev/(?!loop)') - | selectattr('fstype', 'in', ['ext4', 'xfs', 'btrfs']) - | selectattr('size_total', 'ge', 107374182400) - | selectattr('mount', 'match', '^(?!/(boot|snap|usr|var)(/|$))/[A-Za-z0-9_-][A-Za-z0-9_./-]*$') - | rejectattr('mount', 'equalto', '/') - | sort(attribute='size_total') | map(attribute='mount') | list | last | default('') - ) if not crewship_legacy_pools.stat.exists else ''}) }} + ansible.builtin.include_tasks: data_dir.yml when: crewship_cfg.data_dir == 'auto' - name: Assert the configuration document matches the contract diff --git a/docs/configuration.md b/docs/configuration.md index 8f60dfe3e..476fb51ad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -402,8 +402,9 @@ The limit on the number of Firstmate homes is a Firstmate setting; Crewship does ## Data disk Set `data_dir` to an existing absolute path on a mounted data disk. -The default `auto` picks the largest mounted ext4, xfs, or btrfs disk of 100 GiB or more outside `/`, `/boot`, `/snap`, `/usr`, and `/var`. -It applies only to a fresh account: an account that already has `~/.treehouse` keeps the legacy layout, so Docker data and existing pools never move. +The default `auto` picks the largest mounted ext4, xfs, or btrfs disk of 100 GiB or more outside `/`, `/boot`, `/snap`, `/usr`, `/var`, `/home`, `/media`, `/run`, and `/tmp`. +It applies only to a fresh account: an account that already has `~/.treehouse` keeps the legacy layout, so Docker data and existing pools never move, unless the managed bulk defaults already name a data disk. +Once named, that disk stays: a later apply keeps it even when a larger disk appears, and fails when it is no longer mounted. Set `""` to keep the legacy storage layout; it writes no bulk data-location defaults (the [Worktree cap](#worktree-cap) is still written). Without a qualifying disk, `auto` behaves like `""`. Clearing `data_dir` removes the managed Bash block and user-manager file. diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 5c0a2a3aa..f0eeb8274 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1858,3 +1858,53 @@ def service(): finally: if pid_file.exists(): os.kill(int(pid_file.read_text()), signal.SIGKILL) + + +GIB = 1024 ** 3 + + +def _mount(path, size_gib, fstype="ext4", device=None): + return {"mount": path, "size_total": size_gib * GIB, "fstype": fstype, + "device": device or "/dev/sd" + path.strip("/").replace("/", "")[:2]} + + +RECORD = "TMPDIR=${TMPDIR:-/srv/old/tmp}\n" + + +@pytest.mark.parametrize("mounts,legacy,record,expected", [ + ([_mount("/", 900), _mount("/srv/a", 200), _mount("/srv/b", 400)], False, "", "/srv/b"), + ([_mount("/", 900), _mount("/home", 500), _mount("/media/usb", 500), _mount("/var", 500), + _mount("/mnt/small", 50), _mount("/mnt/nfs", 500, fstype="nfs"), + _mount("/mnt/loop", 500, device="/dev/loop3")], False, "", ""), + ([_mount("/mnt/data", 400)], True, "", ""), + ([_mount("/mnt/data", 400)], True, RECORD, None), + ([_mount("/srv/old", 200), _mount("/srv/bigger", 800)], False, RECORD, "/srv/old"), + ([_mount("/srv/bigger", 800)], False, RECORD, None), +]) +def test_auto_data_dir_resolution(tmp_path, mounts, legacy, record, expected): + home = tmp_path / "home" + (home / ".treehouse" if legacy else home).mkdir(parents=True) + bulk = home / "bulk.conf" + if record: + bulk.write_text(record) + playbook = tmp_path / "play.yml" + playbook.write_text(yaml.safe_dump([{ + "hosts": "localhost", "connection": "local", "gather_facts": False, + "tasks": [ + {"ansible.builtin.set_fact": {"ansible_facts": {"mounts": mounts}, + "crewship_bulk_defaults_path": str(bulk), + "crewship_cfg": {"home": str(home), "data_dir": "auto"}}}, + {"ansible.builtin.include_tasks": str(ROOT / "blueprint/ansible/tasks/data_dir.yml")}, + {"ansible.builtin.copy": {"dest": str(tmp_path / "out"), + "content": "{{ crewship_cfg.data_dir }}"}}, + ], + }])) + result = subprocess.run( + [str(Path(sys.executable).parent / "ansible-playbook"), "-i", "localhost,", str(playbook)], + capture_output=True, text=True, + ) + if expected is None: + assert result.returncode != 0 and "not mounted" in result.stdout + return + assert result.returncode == 0, result.stdout + result.stderr + assert (tmp_path / "out").read_text() == expected From aa0b4cbd9c2ef4d7d8763d23d1c37e9f9321b7aa Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 11 Nov 2026 23:59:48 +0000 Subject: [PATCH 3/6] no-mistakes(test): Drop opt-in tag from Data disk feature line --- docs/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.md b/docs/features.md index 8ea167bb2..c15d7340d 100644 --- a/docs/features.md +++ b/docs/features.md @@ -14,7 +14,7 @@ These features turn one server into a self-hosted AI coding agent server where y - [Self-hosted Forgejo](forgejo.md): a loopback git forge for agent pushes, with nightly dumps, a GitHub feature-branch push and the no-mistakes forge profile (opt-in) - [Configuration](configuration.md): `.local/host.yml`, the one file that describes a host - [Capacity and auto pruners](capacity.md): host sizing per lane count, and cleanup timers -- [Data disk](configuration.md#data-disk) (opt-in) +- [Data disk](configuration.md#data-disk) - [Model usage metadata and SigNoz export](usage.md) (opt-in) - [Private token-saving suggestions](usage.md#optional-reader-installation): a read-only report with daily delivery after a proven manual cycle (opt-in) - [Self-hosted CI pool](ci-pool.md): GitHub Actions runners, one job per fresh container; deprecated, replaced by runner-up (opt-in) From d1511b37ed95689afa67c92cd70286d46810f51b Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 12 Nov 2026 01:23:03 +0000 Subject: [PATCH 4/6] no-mistakes(ci): Fixed ci-2 at its root cause. The `include_tasks: data_dir.yml` in preflight.yml did not carry the `always` tag into the included tasks. Tag-limited runs skipped the resolution and left `data_dir` as the literal 'auto', which produced `findmnt -T auto`. preflight.yml now uses `import_tasks`, so the tasks inherit the tag and the `when` still gates each one. The earlier fixes stay: the /etc mount exclusion for the worker image build and the corrected preflight slice index in test_ansible_deprecations.py. Local run of test_removed_backend.py, test_ansible_deprecations.py and test_configuration.py: 207 passed, 1 skipped. I did not run the real smoke job or the Docker build --- blueprint/ansible/tasks/data_dir.yml | 2 +- blueprint/ansible/tasks/preflight.yml | 2 +- docs/configuration.md | 2 +- tests/test_ansible_deprecations.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blueprint/ansible/tasks/data_dir.yml b/blueprint/ansible/tasks/data_dir.yml index 197c56e42..0e46a9301 100644 --- a/blueprint/ansible/tasks/data_dir.yml +++ b/blueprint/ansible/tasks/data_dir.yml @@ -23,7 +23,7 @@ | selectattr('device', 'match', '^/dev/(?!loop)') | selectattr('fstype', 'in', ['ext4', 'xfs', 'btrfs']) | selectattr('size_total', 'ge', 107374182400) - | selectattr('mount', 'match', '^(?!/(boot|snap|usr|var|home|media|run|tmp)(/|$))/[A-Za-z0-9_-][A-Za-z0-9_./-]*$') + | selectattr('mount', 'match', '^(?!/(boot|snap|usr|var|home|media|run|tmp|etc)(/|$))/[A-Za-z0-9_-][A-Za-z0-9_./-]*$') | sort(attribute='size_total') | map(attribute='mount') | list | last | default('') }} - name: Require the recorded data disk to stay mounted diff --git a/blueprint/ansible/tasks/preflight.yml b/blueprint/ansible/tasks/preflight.yml index 2bd1d83b5..56a696c4b 100644 --- a/blueprint/ansible/tasks/preflight.yml +++ b/blueprint/ansible/tasks/preflight.yml @@ -7,7 +7,7 @@ crewship_cfg: "{{ crewship_cfg }}" - name: Resolve the automatic data disk - ansible.builtin.include_tasks: data_dir.yml + ansible.builtin.import_tasks: data_dir.yml when: crewship_cfg.data_dir == 'auto' - name: Assert the configuration document matches the contract diff --git a/docs/configuration.md b/docs/configuration.md index 476fb51ad..b05960408 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -402,7 +402,7 @@ The limit on the number of Firstmate homes is a Firstmate setting; Crewship does ## Data disk Set `data_dir` to an existing absolute path on a mounted data disk. -The default `auto` picks the largest mounted ext4, xfs, or btrfs disk of 100 GiB or more outside `/`, `/boot`, `/snap`, `/usr`, `/var`, `/home`, `/media`, `/run`, and `/tmp`. +The default `auto` picks the largest mounted ext4, xfs, or btrfs disk of 100 GiB or more outside `/`, `/boot`, `/snap`, `/usr`, `/var`, `/home`, `/media`, `/run`, `/tmp`, and `/etc`. It applies only to a fresh account: an account that already has `~/.treehouse` keeps the legacy layout, so Docker data and existing pools never move, unless the managed bulk defaults already name a data disk. Once named, that disk stays: a later apply keeps it even when a larger disk appears, and fails when it is no longer mounted. Set `""` to keep the legacy storage layout; it writes no bulk data-location defaults (the [Worktree cap](#worktree-cap) is still written). diff --git a/tests/test_ansible_deprecations.py b/tests/test_ansible_deprecations.py index 881b4f9a9..815f0d279 100644 --- a/tests/test_ansible_deprecations.py +++ b/tests/test_ansible_deprecations.py @@ -37,7 +37,7 @@ def apply(tmp_path, tasks, variables, *options, injected_facts="True"): @pytest.mark.parametrize("injected_facts", ["True", "False"]) def test_platform_and_account_use_namespaced_facts(tmp_path, injected_facts): preflight = yaml.safe_load((ANSIBLE / "tasks/preflight.yml").read_text()) - tasks = preflight[1:3] + [preflight[-1]] + tasks = preflight[2:4] + [preflight[-1]] tasks.append({"ansible.builtin.assert": {"that": [ "crewship_platform in ['linux-x86_64', 'linux-aarch64']", "crewship_deb_arch in ['amd64', 'arm64']", From e7b489328c8eae2398cae7727ca5cd9a854c53a3 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 12 Nov 2026 01:45:42 +0000 Subject: [PATCH 5/6] no-mistakes(ci): Two fixes made. I did not run the Docker lab smoke or the image build; I have no Docker access here. A final run of `tests/test_configuration.py` and `tests/test_chart.py` hit my 280 s limit, so I have no result for it. The two files that cover the runtime fixture, `tests/test_removed_backend.py` and `tests/test_fleet_units.py`, passed: 10 passed, 1 skipped. 1. Runtime smoke (`project database runtime smoke`): the fixture's `ship.provision` document had no `data_dir`. The default `auto` therefore reached the storage guard unresolved and ran `findmnt -T auto`. I pinned `"data_dir": ""` in `tests/test_removed_backend.py`. Both apply paths now set `data_dir` to the empty string. The other `apply()` callers in `tests/test_fleet_units.py` already set `data_dir`. 2. Worker image smoke: I took the review advice and pinned `data_dir: ""` in `blueprint/containers/crewship.container.yml`. An image build should not probe host disks, and `auto` could pick a build mount. I have not confirmed that `auto` caused the missing `chrome-devtools-mcp.js`. The npm 1.13.0 entrypoint move, which my earlier check found, may be the cause; the image build must show it. I did not change the hardcoded `build/src/bin` path, and the image smoke may still fail on it. The 'configuration and python checks' job failed only because it needs the runtime smoke job to pass --- blueprint/containers/crewship.container.yml | 1 + tests/test_removed_backend.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/blueprint/containers/crewship.container.yml b/blueprint/containers/crewship.container.yml index 8da42d5e3..a5531ea8d 100644 --- a/blueprint/containers/crewship.container.yml +++ b/blueprint/containers/crewship.container.yml @@ -33,6 +33,7 @@ crewship: workspace: /home/coder/Dev start_services: false enable_linger: false + data_dir: "" profiles: agents: true development: true diff --git a/tests/test_removed_backend.py b/tests/test_removed_backend.py index 456525e9d..2fbc37de8 100644 --- a/tests/test_removed_backend.py +++ b/tests/test_removed_backend.py @@ -115,7 +115,7 @@ def inspect(): document = { "ansible_become": False, "crewship_repo": str(ROOT), "crewship_blueprint": str(ROOT / "blueprint"), "crewship_group": "root", "crewship_become_target": False, - "crewship": {"home": str(home), "user": "root", "start_services": False, + "crewship": {"home": str(home), "user": "root", "start_services": False, "data_dir": "", "profiles": {"fleet_guards": True}}, } assert ship.provision(document, False) == 0 From 30ffb25b436a6cde8f823ab99e106955dffeea82 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 12 Nov 2026 02:00:29 +0000 Subject: [PATCH 6/6] no-mistakes(ci): The root cause is unchanged. chrome-devtools-mcp 1.13.0 moved its bin entrypoint to `out/Default/gen/src/bin/chrome-devtools-mcp.js`. The package is installed unpinned, so the old `build/src/bin` path was stale. I updated `crewship_chrome_devtools_mcp_path` in `blueprint/ansible/group_vars/all/base.yml` and the two path sites in `tests/test_configuration.py`. `uv run pytest -q tests/test_configuration.py -k "devtools or board_environment"` passes (2 tests), including the test the earlier run missed. `tests/container-smoke.sh` has no `build/` path. I did not run the Docker image build or the smoke job. The path is still hardcoded, so a later upstream layout move would break it again --- blueprint/ansible/group_vars/all/base.yml | 2 +- tests/test_configuration.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blueprint/ansible/group_vars/all/base.yml b/blueprint/ansible/group_vars/all/base.yml index 4e2b2707e..6f84ab4e8 100644 --- a/blueprint/ansible/group_vars/all/base.yml +++ b/blueprint/ansible/group_vars/all/base.yml @@ -57,7 +57,7 @@ crewship_omp_root: "{{ crewship_share_dir }}/omp/{{ crewship_omp_version }}" # installer re-points `current` at that release, so this text, and with it the # Herdr unit and the profile block, stays the same between releases. Exported # to the managed shell environment and to the Herdr service environment. -crewship_chrome_devtools_mcp_path: "{{ crewship_share_dir }}/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" +crewship_chrome_devtools_mcp_path: "{{ crewship_share_dir }}/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/out/Default/gen/src/bin/chrome-devtools-mcp.js" # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that # provides the MCP entrypoint is installed by `--npm`. Nothing here may be a diff --git a/tests/test_configuration.py b/tests/test_configuration.py index f0eeb8274..62baf039f 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1081,7 +1081,7 @@ def test_a_new_chrome_devtools_mcp_release_leaves_the_managed_environment_unchan before = _managed_environment(tmp_path, True, "1.0.0") assert _managed_environment(tmp_path, True, "1.1.0") == before assert before["CHROME_DEVTOOLS_AXI_MCP_PATH"].endswith( - "/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" + "/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/out/Default/gen/src/bin/chrome-devtools-mcp.js" ) @@ -1147,7 +1147,7 @@ def test_board_environment_reaches_new_shells_and_agents_and_is_removed_on_opt_o assert shell.stdout.splitlines() == [ socket if board is not None else "unset", f"{tmp_path}/.local/share/crewship/chrome-devtools-mcp/current/" - "node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js", + "node_modules/chrome-devtools-mcp/out/Default/gen/src/bin/chrome-devtools-mcp.js", ] for line in ( 'Environment="FM_OMP_ARM_READY_TIMEOUT_MS=45000"',