From 146c8be2ecbbb17f9c4837f1eb88ea4955d4d310 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 12:58:38 +0000 Subject: [PATCH 01/12] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20agent=20cons?= =?UTF-8?q?traints=20about=20mount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 406f43f..b09ebeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,3 +59,32 @@ devcontainer features test . # Copyright (C) 2025 baxyz # SPDX-License-Identifier: LGPL-3.0-or-later ``` + +## Design constraints for features + +These are hard requirements, not style preferences — violating them breaks the +container for users, sometimes silently. + +- **Never rely on a direct `mounts` entry for a host path that might not exist.** + DevContainer `mounts` are resolved by Docker before any `install.sh` / + `postCreateCommand` / `postStartCommand` runs. If the host source path is + missing — file *or* directory — the mount fails and the container fails to + start. No feature script can catch or work around this after the fact. This + is why `dotfiles-sync` doesn't bind-mount straight into the final target + (`~/.gitconfig`, `~/.ssh`, …); it stages into `/mnt/h4dotfiles` and merges at + `postStartCommand`, tolerating an absent source. Any new feature that needs + host state (credentials, config dirs) must follow the same staging pattern — + don't copy `claude-dev`'s/`mistral-dev`'s direct-mount-and-symlink shape + without first confirming it can't crash on a first-time user who has no + `~/.claude` / `~/.vibe` yet. +- **Features must work out-of-the-box.** Never require the user to manually + create a folder or file on the host before first use. +- **Must work inside a VS Code multi-root `.code-workspace`** — this repo's own + devcontainer bundles 6 sibling repos this way (see the root `.dev/CLAUDE.md` + workspace layout). Treat this as a standard case, not an edge case. +- **Dedicated AI-tool features must cover three things**: CLI install, IDE + extension, and settings/configuration — see `claude-dev`, `mistral-dev`, + `copilot-dev`. `github-dev` intentionally reimplements `gh` CLI install + rather than depending on an upstream feature, because no known existing + devcontainer feature bundles the CLI *and* the IDE extension together — that + bundling is the actual reason this feature exists. From ba414d5fa3db28c358fd9cd0c54930d71b408823 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:04:15 +0000 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20version=20co?= =?UTF-8?q?nstrain=20for=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b09ebeb..3c40e8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,18 @@ devcontainer features test . 4. `.github/workflows/pr-validation.yml` + `test.yml` → add to test matrix 5. This `AGENTS.md` features table +**Modifying an existing feature — version bump:** + +Any change under `src//` (`install.sh`, `devcontainer-feature.json`, +`README.md`, …) must bump that feature's `version` field (patch by default, +minor/major when warranted) — `release.yml` only tags and publishes a feature +whose `version` changed between the base branch and HEAD, so an unbumped +feature change silently never ships. Bump it **once per branch**: if the +version on the branch already differs from `main`'s, a further commit on that +same branch/PR must *not* bump it again — check the diff against `main` +first, don't bump reflexively on every commit. This should eventually be a CI +check/auto-bump rather than a manual habit — see `TODO.md`. + **License header (all scripts):** ```bash From 1ea92f16a6e28de17c9fd36f2ae072ecbf7b7f1a Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:04:29 +0000 Subject: [PATCH 03/12] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..f29e5d9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,68 @@ +# TODO — devcontainer feature audit + +Plan for two follow-up axes from a feature-suite audit (relevance, host-sync +efficiency, cross-feature coherence). Relevance findings were resolved inline +during the audit; this file tracks the remaining two axes as actionable items. + +See `AGENTS.md` → "Design constraints for features" for the invariants this +plan builds on (no direct `mounts` on a host path that might not exist, +out-of-the-box requirement, multi-root workspace support, AI-feature pattern). + +## 1. Host sync efficiency + +- [ ] **`mistral-dev` is non-functional.** `devcontainer-feature.json` has + neither `mounts` nor `postStartCommand` — `setup-credentials.sh` is generated + at build time but never run, and even if it were, `/mnt/h4vibe` would never + exist. **Do not fix by copying `claude-dev`'s direct-mount pattern** — same + crash risk applies (see next item). Needs the `dotfiles-sync`-style staging + pattern instead, adapted for a single directory instead of many small files. +- [ ] **`claude-dev`'s direct mount of `~/.claude` may have the same + fragility.** If a missing host directory source really does fail the mount + hard (per the constraint above), a first-time user with no `~/.claude` yet + fails at container build. Confirm the actual failure mode, and if it fails, + give `claude-dev` the same staging fix as `mistral-dev`. +- [ ] **`dotfiles-sync`: undocumented risk on two mandatory file mounts.** + `~/.npmrc` and `~/.yarnrc.yml` are bind-mounted unconditionally even though + they're "frequently absent" files — the same category the v1.0.4 changelog + removed others for (`~/.gitignore_global`, `~/.cargo/config.toml`, …). Either + drop them from the mandatory `mounts` list, or document the same "create it, + even empty, to unblock startup" caveat already given for the opt-in section. +- [ ] **`dotfiles-sync` username resolution diverges from the rest of the + suite.** It doesn't use `h4_detect_user` (identical in 8 other features) — + falls back hard to `"node"` instead of auto-detecting vscode/codespace/ + uid-1000. Align it, or document why it's deliberately different. + +## 2. Cross-feature coherence + +- [ ] **Shared bootstrap duplication.** The `h4_detect_user` / + `h4_resolve_home` / `h4_ensure_packages` block is byte-identical across 8 + `install.sh` files (verified via md5). Fine as a deliberate "self-contained, + no GHCR pull" fallback, but nothing keeps the 8 copies in sync when the block + changes. Decide: document the manual-sync obligation explicitly, or add a CI + check that diffs every copy against `helpers4-common`'s canonical version. +- [ ] **License header format drift.** `angular-dev`, `git-absorb`, + `shell-history-per-project` still use the pre-AGENTS.md header format (prose + license line, no SPDX identifier). Update to the canonical 3-line header. +- [ ] **Stale reference to a removed feature.** `package-auto-install`'s + `installsAfter` still lists `ghcr.io/helpers4/devcontainer/local-mounts`, + which no longer resolves (renamed to `dotfiles-sync`). Harmless + (`installsAfter` silently ignores unresolvable IDs) but should be updated. +- [ ] **JSON key ordering.** `git-absorb` and `shell-history-per-project` put + `version` before `id`; every other feature does the opposite. Cosmetic — + align for consistency. + +## 3. Process / tooling + +- [ ] **Auto-bump feature version on change, once per branch.** `release.yml` + only tags/publishes a feature whose `version` changed between the base + branch and HEAD (see AGENTS.md "Modifying an existing feature — version + bump"), so today an unbumped change silently never ships — this is a manual + habit, not enforced. Add a `pr-validation.yml` step (or a separate workflow) + that, for each `src//` touched by the PR: + - reads `version` for that feature on `main` vs on the PR branch HEAD, + - if unchanged → bump patch and push a commit to the PR branch, + - if already different → do nothing (this *is* the "once per branch" rule: + no extra branch-local state needed, the diff-against-`main` check is + naturally idempotent across repeated pushes to the same branch). + Decide bump granularity policy (always patch vs. letting a PR label pick + minor/major) before implementing. From 2bbf36f35cd6a1f94f3aabbe9dbac8c6c035b0c7 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:37:21 +0000 Subject: [PATCH 04/12] =?UTF-8?q?feat(CI-CD):=20=F0=9F=94=96=20add=20versi?= =?UTF-8?q?on-bump-check=20job=20to=20enforce=20version=20updates=20in=20P?= =?UTF-8?q?Rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pr-validation.yml | 63 ++++++++++++++++++++++++++++- AGENTS.md | 10 ++++- TODO.md | 24 +++++------ 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7818436..fe9ef85 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -31,6 +31,66 @@ jobs: echo "status=failure" >> $GITHUB_OUTPUT fi + version-bump-check: + runs-on: ubuntu-latest + outputs: + status: ${{ steps.status.outputs.status }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Check touched features bumped their version + id: check + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + + MERGE_BASE=$(git merge-base "${BASE_SHA}" HEAD) + echo "::notice::Diff base = ${MERGE_BASE} → HEAD ($(git rev-parse HEAD))" + + mapfile -t names < <(git diff --name-only "${MERGE_BASE}..HEAD" -- src/ \ + | awk -F/ '{print $2}' | sort -u) + + FAILED=0 + for name in "${names[@]}"; do + manifest="src/${name}/devcontainer-feature.json" + [ -f "$manifest" ] || continue # feature removed on this branch + + new_version=$(jq -r '.version' "$manifest") + old_version=$(git show "${MERGE_BASE}:${manifest}" 2>/dev/null | jq -r '.version' 2>/dev/null || echo "") + + if [ -z "$old_version" ]; then + echo "✅ ${name}: new feature, no bump required" + elif [ "$old_version" = "$new_version" ]; then + echo "❌ ${name}: touched but version unchanged (${new_version})" + FAILED=1 + else + echo "✅ ${name}: ${old_version} → ${new_version}" + fi + done + + if [ "$FAILED" -eq 1 ]; then + echo "" + echo "One or more touched features didn't bump 'version' in devcontainer-feature.json." + echo "release.yml only publishes a feature whose version changed — bump it (patch by" + echo "default), once for the whole branch. See AGENTS.md 'Modifying an existing" + echo "feature — version bump'." + fi + exit "$FAILED" + + - name: Set status + id: status + if: always() + run: | + if [ "${{ job.status }}" == "success" ]; then + echo "status=success" >> $GITHUB_OUTPUT + else + echo "status=failure" >> $GITHUB_OUTPUT + fi + test-features: runs-on: ubuntu-latest strategy: @@ -175,7 +235,7 @@ jobs: pr-comment: runs-on: ubuntu-latest - needs: [conventional-commits, test-features, shellcheck] + needs: [conventional-commits, version-bump-check, test-features, shellcheck] if: always() steps: - name: Update PR comment @@ -185,6 +245,7 @@ jobs: script: | const jobs = { '🧾 Conventional Commits': "${{ needs.conventional-commits.outputs.status || 'unknown' }}", + '🔖 Version Bump': "${{ needs.version-bump-check.outputs.status || 'unknown' }}", '🧪 Feature Tests': "${{ needs.test-features.result || 'unknown' }}", '🐚 ShellCheck': "${{ needs.shellcheck.outputs.status || 'unknown' }}", }; diff --git a/AGENTS.md b/AGENTS.md index 3c40e8b..31e375a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,8 +61,14 @@ whose `version` changed between the base branch and HEAD, so an unbumped feature change silently never ships. Bump it **once per branch**: if the version on the branch already differs from `main`'s, a further commit on that same branch/PR must *not* bump it again — check the diff against `main` -first, don't bump reflexively on every commit. This should eventually be a CI -check/auto-bump rather than a manual habit — see `TODO.md`. +first, don't bump reflexively on every commit. + +Enforced by the `version-bump-check` job in `pr-validation.yml`: it fails the +PR if a touched feature's `version` is unchanged from `main`. It's a blocking +check only — it never commits a bump on your behalf (deliberately: no bot +commits, no push-permission/fork edge cases, consistent with how +`conventional-commits` already works in this repo). Bump the version yourself +and push again. **License header (all scripts):** diff --git a/TODO.md b/TODO.md index f29e5d9..e6bc587 100644 --- a/TODO.md +++ b/TODO.md @@ -53,16 +53,14 @@ out-of-the-box requirement, multi-root workspace support, AI-feature pattern). ## 3. Process / tooling -- [ ] **Auto-bump feature version on change, once per branch.** `release.yml` - only tags/publishes a feature whose `version` changed between the base - branch and HEAD (see AGENTS.md "Modifying an existing feature — version - bump"), so today an unbumped change silently never ships — this is a manual - habit, not enforced. Add a `pr-validation.yml` step (or a separate workflow) - that, for each `src//` touched by the PR: - - reads `version` for that feature on `main` vs on the PR branch HEAD, - - if unchanged → bump patch and push a commit to the PR branch, - - if already different → do nothing (this *is* the "once per branch" rule: - no extra branch-local state needed, the diff-against-`main` check is - naturally idempotent across repeated pushes to the same branch). - Decide bump granularity policy (always patch vs. letting a PR label pick - minor/major) before implementing. +- [x] **Bump feature version on change, once per branch — blocking check.** + Implemented as a `version-bump-check` job in `pr-validation.yml` (same + pattern as `conventional-commits`/`shellcheck`: sets `outputs.status`, feeds + the `pr-comment` summary table). For each feature touched by the PR, diffs + `version` between the PR's merge-base and HEAD; **fails the job** if it's + unchanged. No bot commits — a first attempt at auto-bumping-and-pushing was + dropped as unnecessarily complex (push permissions, fork PRs can't be + pushed to, bot-commit noise) in favor of a plain blocking check, consistent + with how `conventional-commits` already works here. The "once per branch" + behavior falls out of the same diff-against-base logic: once bumped, the + check passes for every subsequent push to that branch without re-bumping. From 45f7395fdff632c0430f37cfaf865dc5305bee03 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:01 +0000 Subject: [PATCH 05/12] =?UTF-8?q?fix(mistral-dev):=20=F0=9F=90=9B=20wire?= =?UTF-8?q?=20up=20mounts/postStartCommand,=20document=20required=20initia?= =?UTF-8?q?lizeCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The feature had neither mounts nor postStartCommand — setup-credentials.sh was generated at build time but never executed, so credential persistence never actually worked. Added the missing mounts + postStartCommand (same shape as claude-dev). A Feature-level initializeCommand does not work (silently ignored by the devcontainers CLI), so the required 'mkdir -p ~/.vibe' is documented as a consumer-side devcontainer.json addition in the README instead, matching claude-dev's existing pattern. Co-Authored-By: Claude Sonnet 5 --- src/mistral-dev/README.md | 31 ++++++++++++++++++++--- src/mistral-dev/devcontainer-feature.json | 12 +++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/mistral-dev/README.md b/src/mistral-dev/README.md index d4f710d..585c0bb 100644 --- a/src/mistral-dev/README.md +++ b/src/mistral-dev/README.md @@ -16,6 +16,18 @@ including `--no-cache`. } ``` +Add `initializeCommand` to your `devcontainer.json` so the host directory is +guaranteed to exist before Docker tries to bind-mount it: + +```jsonc +{ + "initializeCommand": "mkdir -p ~/.vibe", + "features": { + "ghcr.io/helpers4/devcontainer/mistral-dev:1": {} + } +} +``` + With the optional CLI: ```jsonc @@ -50,16 +62,27 @@ The feature declares the `mistralai.mistral-vibe-code` extension via the installs it automatically. ### Credential persistence -At build time, `install.sh` generates `/usr/local/share/mistral-dev/setup-credentials.sh` -with the target user's home directory baked in. At every container start -(`postStartCommand`), that script replaces `~/.vibe` with a symlink pointing to -the host bind-mounted path `/mnt/h4vibe` (sourced from `~/.vibe` on the host). +1. **Build time** (`install.sh`): generates `/usr/local/share/mistral-dev/setup-credentials.sh` + with the target user's home path baked in. +2. **Mount** (`devcontainer-feature.json → mounts`): bind-mounts `$HOME/.vibe` from + the host to `/mnt/h4vibe` inside the container. +3. **Every start** (`postStartCommand`): `setup-credentials.sh` replaces `~/.vibe` + with a symlink to `/mnt/h4vibe` — credentials and config survive rebuilds. This means: - Credentials survive container rebuilds (including `--no-cache`). - First-time auth inside the container writes back to the host automatically. - `VIBE_HOME` is not required — the symlink is transparent to Mistral Vibe. +Docker refuses to start the container if a bind-mount source has never existed +on the host (fresh machine, first Vibe login). A Feature's `devcontainer-feature.json` +can't fix this itself — `initializeCommand` set there is silently ignored by the +devcontainers CLI, only the consumer's top-level `devcontainer.json` is honored for +it — which is why `initializeCommand` above is required in your own config rather +than bundled into the feature. If `/mnt/h4vibe` is not mounted (e.g. missing +`initializeCommand`, standalone test), the script errors out — check that +`initializeCommand` is present in your `devcontainer.json`. + ### CLI (optional) When `installCli: true`, the `vibe` command is installed at build time via `uv` (preferred) or `pip`. See [Mistral Vibe CLI docs](https://docs.mistral.ai/vibe/code/cli/install-setup). diff --git a/src/mistral-dev/devcontainer-feature.json b/src/mistral-dev/devcontainer-feature.json index fbcf1e9..324da17 100644 --- a/src/mistral-dev/devcontainer-feature.json +++ b/src/mistral-dev/devcontainer-feature.json @@ -1,8 +1,8 @@ { "id": "mistral-dev", - "version": "1.0.1", + "version": "1.0.2", "name": "Mistral Vibe Development Environment", - "description": "Installs the Mistral Vibe IDE extension (mistralai.mistral-vibe-code) for VS Code and Cursor. Mounts ~/.vibe from the host via a directory symlink — credentials and config persist across all rebuilds.", + "description": "Installs the Mistral Vibe IDE extension (mistralai.mistral-vibe-code) for VS Code and Cursor. Bind-mounts ~/.vibe from the host and symlinks it at container start — credentials and config persist across all rebuilds.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/mistral-dev", "licenseURL": "https://github.com/helpers4/devcontainer/blob/main/LICENSE", "keywords": [ @@ -25,6 +25,14 @@ } }, + "mounts": [ + { + "source": "${localEnv:HOME}/.vibe", + "target": "/mnt/h4vibe", + "type": "bind" + } + ], + "postStartCommand": "/usr/local/share/mistral-dev/setup-credentials.sh", "customizations": { "vscode": { "extensions": [ From b03c296eba16f5ecd743ef44dcd968697c64eab0 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:08 +0000 Subject: [PATCH 06/12] =?UTF-8?q?fix(dotfiles-sync):=20=F0=9F=90=9B=20docu?= =?UTF-8?q?ment=20required=20initializeCommand=20for=20all=20mount=20sourc?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker file bind-mounts fail hard if the host source doesn't exist — previously only documented for the opt-in AWS/kube/Docker mounts, but the same risk applies to the mandatory ones (~/.npmrc and ~/.yarnrc.yml in particular are frequently absent on a fresh machine). Added a comprehensive initializeCommand to the README's Usage section covering every bind-mount source, mandatory and opt-in. No change to sync-files.sh behavior. Co-Authored-By: Claude Sonnet 5 --- src/dotfiles-sync/README.md | 28 +++++++++++++++++++-- src/dotfiles-sync/devcontainer-feature.json | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/dotfiles-sync/README.md b/src/dotfiles-sync/README.md index 42ea43b..4c22b01 100644 --- a/src/dotfiles-sync/README.md +++ b/src/dotfiles-sync/README.md @@ -12,7 +12,26 @@ Syncs local Git, SSH, GPG, npm, and yarn config files into the devcontainer. Opt } ``` -That's it. The feature auto-detects the environment and adapts its behavior. +The feature auto-detects the environment and adapts its behavior. **Add +`initializeCommand`** so every bind-mount source is guaranteed to exist before +Docker resolves the mounts — without it, a container can fail to start on a +machine that's simply never created one of these files (see "Operational +note" below): + +```json +{ + "initializeCommand": "mkdir -p ~/.ssh ~/.gnupg ~/.config/git ~/.aws ~/.kube ~/.docker && touch ~/.gitconfig ~/.npmrc ~/.yarnrc.yml ~/.aws/config ~/.kube/config ~/.docker/config.json", + "features": { + "ghcr.io/helpers4/devcontainer/dotfiles-sync:1": {} + } +} +``` + +This can't be baked into the feature itself — a Feature's own +`initializeCommand` field is silently ignored by the devcontainers CLI, only +the consumer's top-level `devcontainer.json` is honored for it (see AGENTS.md +"Design constraints for features"). `mkdir -p`/`touch` on paths that already +exist are no-ops, so this is always safe to add. ### With custom username @@ -56,9 +75,13 @@ That's it. The feature auto-detects the environment and adapts its behavior. **Operational note — bind-mounts are unconditional:** DevContainer Feature `mounts` cannot be gated on option values. The files below are always bind-mounted into `/mnt/h4dotfiles` at container start, regardless of the option value. The option only controls whether `sync-files.sh` copies the staged file into `$HOME`. Consequences: -- **Startup failure risk** — Docker file bind-mounts fail hard if the source path does not exist on the host. If you don't have `~/.aws/config`, `~/.kube/config`, or `~/.docker/config.json`, the container will fail to start even if the corresponding option is `false`. Create the file (it can be empty) to unblock startup. +- **Startup failure risk** — Docker file bind-mounts fail hard if the source path does not exist on the host. If you don't have `~/.aws/config`, `~/.kube/config`, or `~/.docker/config.json`, the container will fail to start even if the corresponding option is `false`. The `initializeCommand` in "Usage" above pre-creates all of them (empty file, `touch`) so this can't happen — that's the supported fix, not creating the file by hand once and hoping it survives. - **Data visible in staging** — even when the option is `false`, the host file is accessible inside the container at `/mnt/h4dotfiles/`. Nothing reads that path unless the option is enabled, but if your threat model requires full isolation, do not use the feature for that credential. +The same startup-failure risk applies to the "always synced" files above +(`~/.npmrc`, `~/.yarnrc.yml` in particular are frequently absent on a fresh +machine) — the `initializeCommand` in "Usage" covers those too. + | Local Path | Option | Notes | |------------|--------|-------| | `~/.aws/config` | `syncAwsConfig` | AWS profiles. `~/.aws/credentials` (long-lived access keys) is **not bind-mounted** and never synced. | @@ -244,6 +267,7 @@ ssh-add -l ## Version History +- **v1.0.7**: Documented the required `initializeCommand` in "Usage" (pre-creates every bind-mount source, mandatory and opt-in) so a container can't fail to start on a machine missing one of these files — a Feature's own `initializeCommand` is silently ignored by the devcontainers CLI, so this has to live in the consumer's `devcontainer.json`, not the feature. No behavior change to `sync-files.sh`. - **v1.0.4**: Removed bind-mounts for files that are frequently absent on host machines and have little value inside a devcontainer: `~/.gitignore_global` (redundant with `~/.config/git/` directory mount), `~/.config/pnpm/rc` (pnpm store-dir is counter-productive in a container), `~/.config/gh/config.yml` and `~/.config/gh/hosts.yml` (gh CLI auth managed separately), `~/.cargo/config.toml` (cargo not relevant in most containers), `~/.config/pip/pip.conf` (too environment-specific). Docker file bind-mounts fail hard if the source path doesn't exist on the host, which was causing containers to fail to start. The `syncGhAuth` option is removed. - **v1.0.3**: Fixed incompatibility with `docker-in-docker` feature — staging directory moved from `/tmp/dotfiles-sync/` to `/mnt/h4dotfiles/` to avoid being hidden by the tmpfs that `docker-in-docker` mounts on `/tmp` at container start. - **v1.0.2**: Added `syncGhAuth` opt-in to copy `~/.config/gh/hosts.yml` (GitHub OAuth token used by `gh` CLI) into `$HOME`. Default `false`, skipped on cloud environments. The file is bind-mounted into `/tmp/dotfiles-sync/` regardless (Feature `mounts` cannot be conditional) but only copied to `$HOME` when the option is enabled. For fine-grained PATs prefer the `github-dev` feature with `GH_TOKEN`. diff --git a/src/dotfiles-sync/devcontainer-feature.json b/src/dotfiles-sync/devcontainer-feature.json index e24de34..bb730ec 100644 --- a/src/dotfiles-sync/devcontainer-feature.json +++ b/src/dotfiles-sync/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "dotfiles-sync", - "version": "1.0.6", + "version": "1.0.7", "name": "Dotfiles Sync", "description": "Syncs local Git, SSH, GPG, npm, yarn config files into the devcontainer. Optionally syncs cloud credentials (AWS, kube, Docker) — opt-in only. Works on macOS, Linux, Windows (WSL), Codespaces, Gitpod, DevPod. Merges instead of overwriting.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/dotfiles-sync", From 8d6fb9b430e7dcf35ed5ed73e1d01a12522659c0 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:14 +0000 Subject: [PATCH 07/12] =?UTF-8?q?fix(angular-dev):=20=F0=9F=90=9B=20normal?= =?UTF-8?q?ize=20license=20header=20to=20AGENTS.md=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- src/angular-dev/devcontainer-feature.json | 2 +- src/angular-dev/install.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/angular-dev/devcontainer-feature.json b/src/angular-dev/devcontainer-feature.json index 84fe105..f3798e8 100644 --- a/src/angular-dev/devcontainer-feature.json +++ b/src/angular-dev/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "angular-dev", - "version": "1.0.5", + "version": "1.0.6", "name": "Angular Development Environment", "description": "Angular-specific development environment with port forwarding, VS Code extensions, and CLI autocompletion.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/angular-dev", diff --git a/src/angular-dev/install.sh b/src/angular-dev/install.sh index 75bb942..6169981 100644 --- a/src/angular-dev/install.sh +++ b/src/angular-dev/install.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Angular Development Environment DevContainer Feature +# This file is part of helpers4. # Copyright (C) 2025 baxyz -# Licensed under LGPL-3.0 - see LICENSE file for details +# SPDX-License-Identifier: LGPL-3.0-or-later # # Configures Angular development environment with CLI autocompletion From f0f2effb320bdc3639e8107cf6c23fb86984f62d Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:19 +0000 Subject: [PATCH 08/12] =?UTF-8?q?fix(git-absorb):=20=F0=9F=90=9B=20normali?= =?UTF-8?q?ze=20license=20header,=20reorder=20id=20before=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- src/git-absorb/devcontainer-feature.json | 2 +- src/git-absorb/install.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/git-absorb/devcontainer-feature.json b/src/git-absorb/devcontainer-feature.json index 95537ce..7150b7b 100644 --- a/src/git-absorb/devcontainer-feature.json +++ b/src/git-absorb/devcontainer-feature.json @@ -1,6 +1,6 @@ { - "version": "1.0.6", "id": "git-absorb", + "version": "1.0.7", "name": "git-absorb", "description": "Installs git-absorb, a tool to automatically absorb staged changes into their logical commits. Like 'git commit --fixup' but automatic.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/git-absorb", diff --git a/src/git-absorb/install.sh b/src/git-absorb/install.sh index c1781d9..8f076a1 100755 --- a/src/git-absorb/install.sh +++ b/src/git-absorb/install.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# git-absorb DevContainer Feature +# This file is part of helpers4. # Copyright (C) 2025 baxyz -# Licensed under LGPL-3.0 - see LICENSE file for details +# SPDX-License-Identifier: LGPL-3.0-or-later # # Installs git-absorb: automatically absorb staged changes into their logical commits From 65de9c23c54756a9b3b2d1acfc4da2194f21eb47 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:24 +0000 Subject: [PATCH 09/12] =?UTF-8?q?fix(shell-history-per-project):=20?= =?UTF-8?q?=F0=9F=90=9B=20normalize=20license=20header,=20reorder=20id=20b?= =?UTF-8?q?efore=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- src/shell-history-per-project/devcontainer-feature.json | 2 +- src/shell-history-per-project/install.sh | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shell-history-per-project/devcontainer-feature.json b/src/shell-history-per-project/devcontainer-feature.json index f29af3b..453f140 100644 --- a/src/shell-history-per-project/devcontainer-feature.json +++ b/src/shell-history-per-project/devcontainer-feature.json @@ -1,6 +1,6 @@ { - "version": "1.0.6", "id": "shell-history-per-project", + "version": "1.0.7", "name": "Shell History Per Project", "description": "Persist shell history per project by automatically detecting and configuring all available shells (zsh, bash, fish). Supports auto-detection or manual shell selection.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/shell-history-per-project", diff --git a/src/shell-history-per-project/install.sh b/src/shell-history-per-project/install.sh index 9b37628..1b3ebe5 100755 --- a/src/shell-history-per-project/install.sh +++ b/src/shell-history-per-project/install.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -# Shell History Per Project DevContainer Feature +# This file is part of helpers4. # Copyright (C) 2025 baxyz -# Licensed under LGPL-3.0 - see LICENSE file for details +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Shell History Per Project DevContainer Feature set -euo pipefail From def0f64d74bd26d37578898d995510a45e39e8c0 Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:30 +0000 Subject: [PATCH 10/12] =?UTF-8?q?fix(package-auto-install):=20=F0=9F=90=9B?= =?UTF-8?q?=20replace=20stale=20local-mounts=20reference=20with=20dotfiles?= =?UTF-8?q?-sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit installsAfter pointed at ghcr.io/helpers4/devcontainer/local-mounts, which no longer resolves — the feature was renamed to dotfiles-sync. Harmless (installsAfter silently ignores unresolvable ids) but misleading. Co-Authored-By: Claude Sonnet 5 --- src/package-auto-install/devcontainer-feature.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/package-auto-install/devcontainer-feature.json b/src/package-auto-install/devcontainer-feature.json index 55646da..6048555 100644 --- a/src/package-auto-install/devcontainer-feature.json +++ b/src/package-auto-install/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "package-auto-install", - "version": "1.0.6", + "version": "1.0.7", "name": "Automatic Package Installation", "description": "Automatically detects and runs npm/yarn/pnpm install in non-interactive mode after container creation.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/package-auto-install", @@ -63,6 +63,6 @@ "ghcr.io/devcontainers/features/node", "ghcr.io/helpers4/devcontainer/angular-dev", "ghcr.io/helpers4/devcontainer/vite-plus", - "ghcr.io/helpers4/devcontainer/local-mounts" + "ghcr.io/helpers4/devcontainer/dotfiles-sync" ] } From 3102fa658fa1c747105d2c7b2953d388a2bf057c Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:39 +0000 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20/add-devcontain?= =?UTF-8?q?er-feature=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scaffolds a new DevContainer Feature end to end (manifest, install.sh, README, test), mirroring helpers4/typescript's /add-helper skill. Bakes in the mounts/initializeCommand constraints and JSON-only-manifest rule from AGENTS.md so new features don't repeat the mistral-dev/dotfiles-sync mistakes just fixed. Co-Authored-By: Claude Sonnet 5 --- .../skills/add-devcontainer-feature/SKILL.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 .claude/skills/add-devcontainer-feature/SKILL.md diff --git a/.claude/skills/add-devcontainer-feature/SKILL.md b/.claude/skills/add-devcontainer-feature/SKILL.md new file mode 100644 index 0000000..4064c7f --- /dev/null +++ b/.claude/skills/add-devcontainer-feature/SKILL.md @@ -0,0 +1,171 @@ +--- +name: add-devcontainer-feature +description: Scaffold a new DevContainer Feature — manifest, install.sh, README, test — following this repo's conventions +--- + +Add a new DevContainer Feature to `helpers4/devcontainer`, end to end. Ask the user for the +feature's name and purpose first if it wasn't already specified (kebab-case id, e.g. `foo-dev`). + +## 1. Before creating anything + +- Check `src/` for an existing feature that already covers this — `ls src/` for the current + list, don't hardcode or guess it, it drifts. Read `AGENTS.md`'s features table for a + one-line summary of each. +- Check whether an official/community feature already does this (e.g. + `ghcr.io/devcontainers/features/*`, `ghcr.io/devcontainers-extra/features/*`). If one exists + and covers the whole need, prefer depending on it (`installsAfter`) over reimplementing — + see `angular-dev`, which delegates CLI install to + `ghcr.io/devcontainers-extra/features/angular-cli` and only adds extensions/settings on top. + Reimplementing something that already exists needs a real justification (e.g. `github-dev` + reimplements `gh` CLI install because no known feature bundles the CLI *and* the IDE + extension together — that bundling is the point of the feature). +- If this is a **dedicated AI-tool feature** (a new IDE assistant, following `claude-dev`/ + `mistral-dev`/`copilot-dev`), it must cover all three: CLI install (if one exists), IDE + extension, and settings/configuration — not just one or two. + +## 2. Files to create + +```text +src//devcontainer-feature.json +src//install.sh +src//README.md +test//test.sh +``` + +Use `src/git-absorb/` as a reference for a simple binary-install feature, or `src/claude-dev/` +for one that persists host state via a bind-mount + symlink. + +### `devcontainer-feature.json` + +- Key order: `id` before `version` (some older features have this backwards — don't copy them). +- Start new features at `"version": "1.0.0"`. +- **Must be plain JSON — no `//` comments.** `devcontainers/action` parses it with + `JSON.parse`, which chokes on JSONC; this broke the release workflow once already (commit + `4e6e5ee`). Put rationale in the README or `install.sh` instead. +- `installsAfter`: at minimum `["ghcr.io/devcontainers/features/common-utils"]`; add + `ghcr.io/helpers4/devcontainer/helpers4-common` or other helpers4 features here if this one + needs them ordered first. Never reference a feature id that isn't in `src/` (or the official + registry) — it's silently ignored if unresolvable, which hides real typos. +- If this feature needs to persist state from the host (credentials, config), read the + "Design constraints for features" section in `AGENTS.md` **before** adding a `mounts` entry: + - A `mounts` source that doesn't exist on the host — file *or* directory — fails the whole + container at start, unconditionally. No feature script can catch this. + - A Feature-level `initializeCommand` **does not work** — verified against the + devcontainers CLI source; it's silently ignored. The only place `initializeCommand` has + effect is the *consumer's* top-level `devcontainer.json`. So: add the `mounts` + + `postStartCommand` to the feature as usual, but the required `mkdir -p`/`touch` line goes + in the feature's own README "Example Usage" as something the consumer must add — see + `claude-dev`'s or `mistral-dev`'s README for the exact pattern to copy. + +### `install.sh` + +Pattern (copy the bootstrap block verbatim from `src/helpers4-common/install.sh` or any +existing feature — a CI check in `pr-validation.yml`'s `shellcheck` job fails the build if a +copy drifts from the canonical version): + +```bash +#!/usr/bin/env bash + +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# + +set -euo pipefail + +# Bootstrap helpers4 shared library. helpers4-common installs it; if running +# standalone (e.g. devcontainer features test), create it inline so the feature +# is self-contained without a GHCR pull. +if [ ! -f /usr/local/share/helpers4/common.sh ]; then + mkdir -p /usr/local/share/helpers4 + cat > /usr/local/share/helpers4/common.sh << 'H4_COMMON' +# shellcheck shell=bash +h4_detect_user() { ... } # copy the exact body from an existing feature +h4_resolve_home() { ... } +h4_apt_update() { ... } +h4_ensure_packages() { ... } +H4_COMMON +fi +# shellcheck source=/dev/null +. /usr/local/share/helpers4/common.sh + +if [ "$(id -u)" -ne 0 ]; then + echo 'Script must be run as root.' + exit 1 +fi + +h4_detect_user +h4_resolve_home + +trap cleanup EXIT +cleanup() { rm -rf /tmp/-* ; } + +# ... arch detection (x86_64/aarch64), h4_ensure_packages for apt deps, +# download/install to /usr/local/bin/ ... +``` + +Read the actual bootstrap block out of an existing `install.sh` rather than retyping it from +this skill — copy it byte-for-byte so the CI drift check passes. + +### `README.md` + +Follow the shape of an existing feature's README: title, one-paragraph description, "Example +Usage" (with the `initializeCommand` snippet if this feature mounts host state), an options +table, and a "How it works" section. If the feature evolves after this, add a "Version +History" section (see `dotfiles-sync`'s) documenting what changed and why per bump — future +maintainers (and future agents) rely on it to avoid re-deriving decisions from scratch. + +### `test//test.sh` + +```bash +#!/usr/bin/env bash + +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later + +set -e + +echo "Testing feature..." +# assert the thing the feature installs is present/working; exit 1 on failure +echo "✅ PASS: ..." +``` + +Keep it about **build-time artifacts** (binary present, file generated, config written) — it +can't exercise `postStartCommand`/mount-dependent behavior, since `devcontainer features test` +doesn't wire up bind mounts from a real host. If the feature declares `mounts`, add a +"Create mount sources for ``" step to `pr-validation.yml`'s `test-features` job (see the +existing steps for `claude-dev`/`mistral-dev`/`dotfiles-sync`) so the test job's own container +build doesn't fail on a missing source. + +## 3. Wire it into the repo + +In this order — missing any of these breaks CI or leaves the feature undiscoverable: + +1. `scopes.json` → add `` (commit scope validation reads this file automatically; PR CI + fails without it). +2. `.github/workflows/pr-validation.yml` → add `` to the `test-features` job's matrix + (one entry per base image worth testing against), plus a mount-source step if needed. +3. `.github/workflows/test.yml` → add the same matrix entries (this one runs on push to `main`, + not per-PR). +4. `AGENTS.md` → add a row to the features table (`| \`\` | 1.0.0 | |`). + +## 4. Verify + +```bash +devcontainer features test --features . +``` + +Then, since this is a **new** feature (not yet published), the version-bump-check in +`pr-validation.yml` won't flag it either way — but confirm the manifest still validates as +plain JSON (`jq empty src//devcontainer-feature.json`) and that `bash -n +src//install.sh` / `test//test.sh` come back clean before moving on. Run +`shellcheck -S warning` on both scripts if it's available locally. + +## 5. Commit + +Only if explicitly asked to commit *this turn* — per `.dev/AGENTS.md`, commit authorization is +per-turn, not a standing grant. When authorized: `feat(): ✨ add feature` as the +scope (matches `scopes.json` once step 3.1 above is done), following the commit format in +`AGENTS.md` (`(): `). From 244978aa908593f30bbcfef2fc80639ed4b1682f Mon Sep 17 00:00:00 2001 From: baxyz Date: Sun, 26 Jul 2026 13:56:46 +0000 Subject: [PATCH 12/12] =?UTF-8?q?docs:=20=F0=9F=93=9D=20update=20AGENTS.md?= =?UTF-8?q?=20and=20TODO.md=20after=20host-sync/coherence=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects the AGENTS.md mount guidance with the verified mechanism (Feature-level initializeCommand is silently ignored by the devcontainers CLI — only the consumer's devcontainer.json is honored) and checks off the resolved TODO items, including two findings that changed conclusion under closer inspection (claude-dev's initializeCommand removal was correct, not a regression; dotfiles-sync's username handling isn't actually worse than claude-dev/mistral-dev's). Logs one new follow-up: claude-dev/mistral-dev's username option is dead code, not wired into their install.sh. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 60 ++++++++++++++++++++----------- TODO.md | 103 ++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 106 insertions(+), 57 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 31e375a..0fc735a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,17 +32,17 @@ devcontainer features test . | `github-dev` | 1.0.3 | gh CLI, Copilot Chat, PR/Issues/Actions extensions | | `copilot-dev` | 1.0.1 | Copilot Chat + AI instructions (commits, PRs, code review) | | `claude-dev` | 1.0.4 | Claude Code extension + `~/.claude` bind-mount (credentials + memory persist) | -| `mistral-dev` | 1.0.1 | Mistral Vibe extension + `~/.vibe` bind-mount | +| `mistral-dev` | 1.0.2 | Mistral Vibe extension + `~/.vibe` bind-mount | | `typescript-dev` | 1.0.5 | TS/JS dev, import management (dependsOn essential-dev) | -| `angular-dev` | 1.0.2 | Angular dev, port 4200 | +| `angular-dev` | 1.0.6 | Angular dev, port 4200 | | `vite-plus` | 1.0.3 | vp CLI, Oxlint/Oxfmt, Vitest | -| `package-auto-install` | — | Auto-detect and install packages | +| `package-auto-install` | 1.0.7 | Auto-detect and install packages | | `pnpm-store` | 1.0.4 | Shared pnpm store via Docker named volume (dependsOn helpers4-common) | | `auto-header` | — | LGPL-3.0 license headers | -| `git-absorb` | 1.0.2 | git-absorb from GitHub releases | -| `dotfiles-sync` | 1.0.2 | Sync Git/SSH/GPG/npm/gh config from host | +| `git-absorb` | 1.0.7 | git-absorb from GitHub releases | +| `dotfiles-sync` | 1.0.7 | Sync Git/SSH/GPG/npm/gh config from host | | `peon-ping` | 1.0.3 | AI agent sound notifications | -| `shell-history-per-project` | 1.0.2 | Persistent shell history (zsh/bash/fish) | +| `shell-history-per-project` | 1.0.7 | Persistent shell history (zsh/bash/fish) | **Adding a new feature — checklist:** @@ -83,20 +83,40 @@ and push again. These are hard requirements, not style preferences — violating them breaks the container for users, sometimes silently. -- **Never rely on a direct `mounts` entry for a host path that might not exist.** - DevContainer `mounts` are resolved by Docker before any `install.sh` / - `postCreateCommand` / `postStartCommand` runs. If the host source path is - missing — file *or* directory — the mount fails and the container fails to - start. No feature script can catch or work around this after the fact. This - is why `dotfiles-sync` doesn't bind-mount straight into the final target - (`~/.gitconfig`, `~/.ssh`, …); it stages into `/mnt/h4dotfiles` and merges at - `postStartCommand`, tolerating an absent source. Any new feature that needs - host state (credentials, config dirs) must follow the same staging pattern — - don't copy `claude-dev`'s/`mistral-dev`'s direct-mount-and-symlink shape - without first confirming it can't crash on a first-time user who has no - `~/.claude` / `~/.vibe` yet. -- **Features must work out-of-the-box.** Never require the user to manually - create a folder or file on the host before first use. +- **A `mounts` entry fails the whole container if the host source doesn't + exist — file *or* directory, no exceptions.** Verified against + `@devcontainers/cli` directly (decompiled `devContainersSpecCLI.js`): + `docker run --mount type=bind,...` is used, which errors out + (`bind source path does not exist`) instead of silently creating anything. + No feature script can catch this — mounts resolve before `install.sh` / + `postCreateCommand` / `postStartCommand` ever run. +- **A Feature's own `initializeCommand` cannot fix this — it's silently + ignored.** Also verified in the CLI source: the lifecycle-command merge + step only collects `onCreateCommand`, `updateContentCommand`, + `postCreateCommand`, `postStartCommand`, `postAttachCommand` from each + Feature (`lv` in the minified bundle) plus `mounts`/`customizations`/etc. + (`xV`) — `initializeCommand` is *not* in either list. It only has effect in + the **consumer's own top-level `devcontainer.json`**. This was tried on + `claude-dev` (v1.0.3, since reverted as dead code) before the real fix + landed: document a required `initializeCommand` in the feature's README + Example Usage (see `claude-dev`'s and `mistral-dev`'s READMEs) — a Feature + cannot inject into the consumer's `devcontainer.json`, so this is the only + place it can actually run. +- **"Out-of-the-box" for a mount-dependent feature means "one documented + `initializeCommand` line away," not zero-config.** Given the constraint + above, don't chase a fully silent fix for a feature with a hard mount + dependency — document the required line prominently instead. If a feature + can tolerate a missing/empty source at the file level (many small files, + like `dotfiles-sync`), prefer staging into `/mnt/h4dotfiles`-style path and + merging at `postStartCommand` over a hard mount dependency — but note that + even staged mounts still fail hard if *their* source is missing, so staging + only helps once the source is guaranteed to exist (see `dotfiles-sync`'s + own `initializeCommand` requirement for exactly the files it can't avoid + mounting). +- **`devcontainer-feature.json` must be plain JSON — no `//` comments.** + `devcontainers/action` parses it with `JSON.parse`, which chokes on JSONC. + This broke the release workflow once already (see commit `4e6e5ee`). Put + rationale in the README or a neighboring `.sh` file instead. - **Must work inside a VS Code multi-root `.code-workspace`** — this repo's own devcontainer bundles 6 sibling repos this way (see the root `.dev/CLAUDE.md` workspace layout). Treat this as a standard case, not an edge case. diff --git a/TODO.md b/TODO.md index e6bc587..3b72ce1 100644 --- a/TODO.md +++ b/TODO.md @@ -10,46 +10,63 @@ out-of-the-box requirement, multi-root workspace support, AI-feature pattern). ## 1. Host sync efficiency -- [ ] **`mistral-dev` is non-functional.** `devcontainer-feature.json` has - neither `mounts` nor `postStartCommand` — `setup-credentials.sh` is generated - at build time but never run, and even if it were, `/mnt/h4vibe` would never - exist. **Do not fix by copying `claude-dev`'s direct-mount pattern** — same - crash risk applies (see next item). Needs the `dotfiles-sync`-style staging - pattern instead, adapted for a single directory instead of many small files. -- [ ] **`claude-dev`'s direct mount of `~/.claude` may have the same - fragility.** If a missing host directory source really does fail the mount - hard (per the constraint above), a first-time user with no `~/.claude` yet - fails at container build. Confirm the actual failure mode, and if it fails, - give `claude-dev` the same staging fix as `mistral-dev`. -- [ ] **`dotfiles-sync`: undocumented risk on two mandatory file mounts.** - `~/.npmrc` and `~/.yarnrc.yml` are bind-mounted unconditionally even though - they're "frequently absent" files — the same category the v1.0.4 changelog - removed others for (`~/.gitignore_global`, `~/.cargo/config.toml`, …). Either - drop them from the mandatory `mounts` list, or document the same "create it, - even empty, to unblock startup" caveat already given for the opt-in section. -- [ ] **`dotfiles-sync` username resolution diverges from the rest of the - suite.** It doesn't use `h4_detect_user` (identical in 8 other features) — - falls back hard to `"node"` instead of auto-detecting vscode/codespace/ - uid-1000. Align it, or document why it's deliberately different. +- [x] **`mistral-dev` was non-functional.** Fixed: added `mounts` + + `postStartCommand` to `devcontainer-feature.json` (v1.0.2). Turns out a + Feature-level `initializeCommand` (the originally-planned fix) is **silently + ignored** by the devcontainers CLI — verified by decompiling + `@devcontainers/cli`'s `devContainersSpecCLI.js`: the lifecycle-merge step + only collects `initializeCommand` from the top-level `devcontainer.json`, + never from a Feature. The real fix is a documented, required + `initializeCommand` line in the consumer's own `devcontainer.json` — added to + mistral-dev's README "Example Usage", mirroring the pattern `claude-dev` + already used correctly. See AGENTS.md "Design constraints for features". +- [x] **`claude-dev`'s direct mount of `~/.claude` — checked, no fix needed.** + Same crash risk is real (confirmed empirically with `docker --mount + type=bind`: hard failure, both for missing files and missing directories), + but `claude-dev` already documents the required consumer-side + `initializeCommand` correctly in its README (this was in fact tried as a + Feature-level field in v1.0.3 and reverted in a later commit as dead code — + the removal was correct, not a regression as originally suspected here). No + change made. +- [x] **`dotfiles-sync`: mandatory file mounts can crash startup.** Fixed by + documenting a comprehensive `initializeCommand` in the README covering every + mount source (mandatory *and* opt-in), not just the two originally flagged + (`~/.npmrc`, `~/.yarnrc.yml`) — same underlying constraint as above (v1.0.7). +- [x] **`dotfiles-sync` username resolution — investigated, not actually a + divergence.** `claude-dev`/`mistral-dev`'s `username` option turned out to be + dead code too: their `install.sh` never reads `_BUILD_ARG_USERNAME`, so + `h4_detect_user`'s auto-detection runs unconditionally and the option has no + effect either way. `dotfiles-sync`'s simpler `_BUILD_ARG_USERNAME` → getent + → `"node"` fallback is actually the *more* correct behavior of the three (it's + the only one where setting the option does anything), and is already + documented ("Match this to your `remoteUser`"). No change made here — but see + the new follow-up below, this surfaced a real bug elsewhere. +- [ ] **New: `claude-dev` and `mistral-dev`'s `username` option is dead code.** + Neither `install.sh` reads `_BUILD_ARG_USERNAME` — `h4_detect_user` runs its + own auto-detection regardless of what a consumer sets via the `username` + option. Either wire the option's value into `h4_detect_user`'s precedence + (consistent with `dotfiles-sync`), or remove the option from both schemas if + auto-detection is meant to always win. Needs its own pass — touches 2 + features + the shared bootstrap block copied in 8 files, not done in this + pass to avoid scope creep. ## 2. Cross-feature coherence -- [ ] **Shared bootstrap duplication.** The `h4_detect_user` / - `h4_resolve_home` / `h4_ensure_packages` block is byte-identical across 8 - `install.sh` files (verified via md5). Fine as a deliberate "self-contained, - no GHCR pull" fallback, but nothing keeps the 8 copies in sync when the block - changes. Decide: document the manual-sync obligation explicitly, or add a CI - check that diffs every copy against `helpers4-common`'s canonical version. -- [ ] **License header format drift.** `angular-dev`, `git-absorb`, - `shell-history-per-project` still use the pre-AGENTS.md header format (prose - license line, no SPDX identifier). Update to the canonical 3-line header. -- [ ] **Stale reference to a removed feature.** `package-auto-install`'s - `installsAfter` still lists `ghcr.io/helpers4/devcontainer/local-mounts`, - which no longer resolves (renamed to `dotfiles-sync`). Harmless - (`installsAfter` silently ignores unresolvable IDs) but should be updated. -- [ ] **JSON key ordering.** `git-absorb` and `shell-history-per-project` put - `version` before `id`; every other feature does the opposite. Cosmetic — - align for consistency. +- [x] **Shared bootstrap duplication — already covered, no new code needed.** + `pr-validation.yml`'s `shellcheck` job already has a "Verify bootstrap copies + match helpers4-common canonical" step that diffs every `install.sh` copy of + the `h4_detect_user`/`h4_resolve_home`/`h4_ensure_packages` block against + `helpers4-common`'s canonical version and fails the job on drift. This + predates this audit; re-verified it actually catches drift (tested locally). +- [x] **License header format drift.** Fixed: `angular-dev`, `git-absorb`, + `shell-history-per-project` now use the canonical 3-line header (bumped + 1.0.6, 1.0.7, 1.0.7 respectively). +- [x] **Stale reference to a removed feature.** Fixed: `package-auto-install`'s + `installsAfter` now points at `dotfiles-sync` instead of the removed + `local-mounts` (v1.0.7). +- [x] **JSON key ordering.** Fixed: `git-absorb` and `shell-history-per-project` + now put `id` before `version`, matching every other feature (same commits as + the header fix above). ## 3. Process / tooling @@ -64,3 +81,15 @@ out-of-the-box requirement, multi-root workspace support, AI-feature pattern). with how `conventional-commits` already works here. The "once per branch" behavior falls out of the same diff-against-base logic: once bumped, the check passes for every subsequent push to that branch without re-bumping. + +## 4. Tooling for contributors + +- [x] **`/add-devcontainer-feature` Claude skill.** Added + `.claude/skills/add-devcontainer-feature/SKILL.md`, mirroring + `helpers4/typescript`'s `/add-helper` skill: scaffolds a new feature end to + end following the AGENTS.md checklist (manifest, install.sh, README, test, + `scopes.json`, workflow matrices, features table) and bakes in the design + constraints above (mounts, `initializeCommand` placement, JSON-only + manifests, version bump). Wired into the workspace via + `.dev/.claude/settings.json`'s `additionalDirectories`, same mechanism + already used for the typescript repo's skills directory.