From 5ff93896b39663bc27ae1b53594c3cbd07a8adca Mon Sep 17 00:00:00 2001 From: verlyn13 Date: Wed, 12 Aug 2026 21:39:54 -0800 Subject: [PATCH 1/2] feat(android): add Android host-capability baseline Extend the existing chezmoi, mise, ng-doctor, Homebrew, and system-update mechanisms rather than adding a parallel android/ subsystem. - docs/android-development.md states the host/project ownership split: system-config owns host capabilities, the repository owns build policy. - home/dot_androidrc.tmpl deploys the canonical SDK root as the Android CLI default, gated on the `android` machine-data key via .chezmoiignore. - ng-doctor gains an `android [project]` category (7 checks) reporting host capability and, optionally, a project's declared requirements read-only. The doctor separates launcher installation from payload initialization: the Homebrew cask ships a stub that downloads the real CLI to $ANDROID_USER_HOME/bin/android-cli on first run, so a cask receipt is not evidence the CLI exists or has ever parsed ~/.androidrc. Parsers ignore commented-out declarations and search modules at any depth, so a superseded value left behind as a comment cannot shadow the live one. tests/android/run.sh adds 28 assertions and is wired into the gate; the workflow now installs a pinned chezmoi, without which the suite's deliberate hard-fail would redden CI on every push. --- .github/workflows/repo-validation.yml | 18 + README.md | 14 +- docs/android-development.md | 239 +++++++++ docs/project-conventions.md | 21 + home/.chezmoiignore | 8 + home/dot_androidrc.tmpl | 1 + home/dot_local/bin/executable_ng-doctor.tmpl | 530 ++++++++++++++++++- policies/version-policy.md | 22 +- scripts/validate-repo.sh | 1 + tests/android/run.sh | 294 ++++++++++ 10 files changed, 1133 insertions(+), 15 deletions(-) create mode 100644 docs/android-development.md create mode 100644 home/dot_androidrc.tmpl create mode 100755 tests/android/run.sh diff --git a/.github/workflows/repo-validation.yml b/.github/workflows/repo-validation.yml index 221137f..2c409cb 100644 --- a/.github/workflows/repo-validation.yml +++ b/.github/workflows/repo-validation.yml @@ -15,6 +15,11 @@ jobs: # CI disagreed with a mise-pinned workstation and the gate was not # reproducible. Bump both files together. SHELLCHECK_VERSION: "0.11.0" + # tests/android/run.sh renders the managed chezmoi templates and hard-fails + # when chezmoi is absent, which is deliberate: a suite that skips in CI + # asserts nothing. The runner has no chezmoi, so the gate must install it. + # Keep in lockstep with the workstation's Homebrew chezmoi. + CHEZMOI_VERSION: "2.72.0" steps: - uses: actions/checkout@v4 @@ -36,5 +41,18 @@ jobs: fi shellcheck --version + - name: Install pinned chezmoi + run: | + set -euo pipefail + url="https://github.com/twpayne/chezmoi/releases/download/v${CHEZMOI_VERSION}/chezmoi_${CHEZMOI_VERSION}_linux_amd64.tar.gz" + curl -fsSL "$url" | tar -xz -C /tmp chezmoi + sudo install -m 0755 /tmp/chezmoi /usr/local/bin/chezmoi + installed="$(chezmoi --version | sed -n 's/^chezmoi version v\([0-9.]*\).*/\1/p')" + if [[ "$installed" != "$CHEZMOI_VERSION" ]]; then + echo "expected chezmoi ${CHEZMOI_VERSION}, got ${installed}" >&2 + exit 1 + fi + chezmoi --version + - name: Validate repository run: scripts/validate-repo.sh diff --git a/README.md b/README.md index f075f52..9819c90 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ never committed — secrets live in 1Password and are resolved at launch. | Shell | zsh startup, PATH, 15 `zshrc.d` modules, agentic-mode gating | `home/dot_zshenv.tmpl` → `home/dot_zshrc.tmpl` | | Runtimes | global `mise` defaults | `home/dot_config/mise/` | | Env | `direnv` helpers and policy | `home/dot_config/direnv/` | +| Android | canonical SDK root, Android CLI defaults, host/project capability reporting | `home/dot_androidrc.tmpl`, `ng-doctor android` | | SSH (client) | `~/.ssh/config`, `conf.d/`, `allowed_signers` | `home/private_dot_ssh/` | | Terminal | 14 iTerm2 dynamic profiles + color presets | `iterm2/`, `scripts/install-iterm2-profiles.sh` | | Editor | reviewed subset of VS Code user settings, profiles, extensions | `vscode/`, `scripts/sync-vscode.sh` | @@ -57,7 +58,7 @@ resolve server-side (§1, class "Account-managed") — both of which carry real authorization and are invisible to every mechanism below. Enumeration there is legibility, not governance. -1. **chezmoi** — `home/` is the source tree; 77 managed targets under `$HOME`. +1. **chezmoi** — `home/` is the source tree; 78 managed targets under `$HOME`. Go templates, not Jinja2. Optional keys must use `| default`. 2. **Repo scripts** — `scripts/sync-mcp.sh`, `scripts/sync-vscode.sh`, `scripts/install-iterm2-profiles.sh`, `scripts/mcp-usage-collector.sh`. @@ -94,8 +95,8 @@ iterm2/ dynamic profile and color-preset definitions vscode/ managed user-setting keys, profile and extension manifests macos/ root-owned configuration deployed by sudo, not chezmoi policies/ canonical live policy; vendored byte-identically by a sibling repo -tests/ 8 suites (see validation caveat below) -docs/ 151 files; AGENTS.md is the authoritative index +tests/ 10 suites (see validation caveat below) +docs/ 153 files; AGENTS.md is the authoritative index ``` ## Boundaries @@ -121,13 +122,14 @@ Owned elsewhere, deliberately: - `shellcheck` (pinned to 0.11.0 via `.mise.toml`) over tracked `*.sh` plus two named templates - `jq` shape checks on the MCP and VS Code JSON manifests -- `tests/system-update`, `tests/vscode`, `tests/mcp-vscode`, `tests/mcp-codex` +- `tests/system-update`, `tests/android`, `tests/vscode`, `tests/mcp-vscode`, + `tests/mcp-codex`, `tests/mcp-claude-desktop` - `scripts/policy-lint.sh`, `scripts/check-instruction-bridge.sh` - repo-contract greps (no fish surface, no stale path references) -**Known coverage gaps, stated rather than implied:** four of the eight suites — +**Known coverage gaps, stated rather than implied:** four of the ten suites — `tests/git-identity-gate`, `tests/jr-style`, `tests/mise-config`, -`tests/policies` — are not invoked by the gate. `ng-doctor` reports 91 checks, +`tests/policies` — are not invoked by the gate. `ng-doctor` reports 101 checks, a number of which are unimplemented skip stubs rather than assertions, so a green summary is not by itself evidence of a healthy host. There is no secret or PII scanner in the gate. diff --git a/docs/android-development.md b/docs/android-development.md new file mode 100644 index 0000000..1fe486a --- /dev/null +++ b/docs/android-development.md @@ -0,0 +1,239 @@ +--- +title: Android Host Development Baseline +category: reference +component: android +status: active +version: 1.0.0 +last_updated: 2026-08-12 +tags: [android, android-studio, android-cli, gradle, sdk, ndk, mise] +priority: high +--- + +# Android host development baseline + +> **System-config owns Android host capabilities; individual repositories own +> Android build policy.** + +This is the host contract for Android Studio, terminal tools, SDK packages, and +project compatibility checks. It extends the existing chezmoi, mise, +`ng-doctor`, Homebrew, and `system-update` mechanisms. There is intentionally no +parallel `android/` subsystem and no universal Android package/version manifest. + +## Ownership + +| Concern | Owner | Authority | +|---|---|---| +| SDK root and shell exposure | system-config | chezmoi templates and `~/.androidrc` | +| Android CLI launcher installation and updates | system-config host baseline | Homebrew `android-cli` cask; cask visibility/update lanes in `system-update` | +| Android CLI downloaded payload and first launch | operator and Android CLI | initialized on an explicit CLI invocation; observed read-only by `ng-doctor` | +| Android Studio Preview health | system-config observation | `ng-doctor android`; report-only `android-studio-canary` updater plugin | +| Installed SDK platforms, Build Tools, platform-tools, emulator, NDK, CMake, JDKs | host capability | Android Studio/Android CLI plus read-only doctor evidence | +| AGP, Gradle, Kotlin, Java, `compileSdk`, `targetSdk`, NDK, CMake | project repository | Gradle wrapper, version catalog/build files, `.mise.toml` | +| Gradle execution | project repository | checked-in `./gradlew`, never a global Gradle version | + +Installation never changes project intent. Stable and preview SDKs may coexist; +the project still declares exactly what it compiles and targets. A project may, +for example, compile against API 37 while retaining `targetSdk = 36` until it is +ready for the new runtime behavior boundary. + +Global `~/.gradle` state must not inject repositories, plugins, dependency +versions, or build behavior. It may provide ordinary caches and runtime support. + +## Canonical SDK root + +Android-capable machines set `android = true` in chezmoi machine data. That +enables the existing zsh, bash, and global mise exports and now deploys: + +```text +~/.androidrc: --sdk=/Users/verlyn13/Library/Android/sdk +ANDROID_HOME=/Users/verlyn13/Library/Android/sdk +ANDROID_SDK_ROOT=/Users/verlyn13/Library/Android/sdk +``` + +The managed `~/.androidrc` contains exactly the documented flag line. It does +not add comments because comment syntax is not part of the verified file +contract. + +Project `local.properties` remains checkout-local Android Studio state. It is +not the only reason a terminal build can find the SDK. + +## Android CLI + +The canonical terminal entry point is Google's +[Android CLI](https://developer.android.com/tools/agents/android-cli). The +upstream `android/tap` repository and Homebrew's core cask both distribute a +cask. The upstream tap documents: + +```bash +brew install android/tap/android-cli +``` + +This baseline currently chooses Homebrew's +[`android-cli` cask](https://formulae.brew.sh/cask/android-cli): + +```bash +brew install --cask android-cli +``` + +That is a provenance choice, not a claim that Google's tap is absent. Three +differences matter: + +| | Homebrew core cask | `android/tap` cask | +|---|---|---| +| Launcher version | pinned, with a real SHA-256 | rolling `latest` URL, `sha256 :no_check` | +| `postflight` | none | de-quarantines and runs the launcher with `ANDROID_CLI_FRESH_INSTALL` | +| `zap` stanza | `~/.android` and `~/Library/Android` | none | + +The `postflight` difference is the one that changes this document's model: +installing from the tap performs the payload download during `brew install`, +unattended, which collapses the launcher/payload boundary the rest of this +section depends on. Any future switch to the tap must follow the item-level +review in [`docs/homebrew-tap-trust.md`](./homebrew-tap-trust.md). + +The installed `android` executable is a launcher. On first invocation it +downloads the actual CLI payload from Google's `latest` endpoint to +`$ANDROID_USER_HOME/bin/android-cli`, where `ANDROID_USER_HOME` defaults to +`~/.android` — **not** `~/.android/cli/`, which Android Studio already owns for +its IPC handshake. Therefore the cask receipt pins and verifies the launcher, +not the subsequently downloaded payload. `ng-doctor` reports launcher +installation and payload initialization separately; it does not launch the +command or treat a launcher receipt as proof that the CLI has parsed +`~/.androidrc`. + +The legacy `sdkmanager`/`avdmanager` commands may remain for compatibility, but +they are not the host-health authority. First initialization is an operator +step; run an ordinary read command such as `android info` in an interactive +terminal, review any download, and then rerun the doctor. The doctor must remain +read-only and must not depend on Google's live catalog. + +`system-update` already reports stale casks in its core Homebrew step. Actual +cask upgrades remain in the existing default-off `brew-casks` plugin/operator +lane. Do not add a second Android-specific updater. + +> **Never run `brew uninstall --zap android-cli`.** The zap list in Homebrew's +> core cask — the one installed here — includes both `~/.android` and +> `~/Library/Android`; the latter contains the canonical SDK. Ordinary +> install/upgrade does not require `--zap`. + +Google documents `android update` as its native update command. On this host, +prefer the Homebrew upgrade lane so the installed receipt and executable stay +under one package owner. + +## Read-only doctor + +```bash +ng-doctor android +ng-doctor android /path/to/project +``` + +The host-only form reports: + +- canonical SDK root across the shell, `~/.androidrc`, and filesystem; +- Android CLI launcher cask path/version and downloaded-payload initialization; +- installed Android Studio Stable/Preview bundle, build, and JBR architecture; +- platform-tools and `adb` version; +- metadata-backed SDK platforms, Build Tools, NDK, and CMake packages; +- incomplete package directories that must not count as installed; +- mise JDK inventory, with Android Studio's bundled JBR kept separate. + +The optional project form reads only repository-owned declarations. It reports +`PROJECT DECLARES`, `REQUIRED AND PRESENT`, `REQUIRED AND ABSENT`, and +`INSTALLED BUT NOT REQUIRED`. It does not edit the project, install packages, +run a Gradle build, or infer “latest installed” as the project SDK. + +The compatibility table is deliberately fail-limited. Its current +source-backed mapping covers AGP 9.3: maximum API 37, minimum Gradle 9.5.0, +default Build Tools 36.0.0, and minimum JDK 17. Future or unknown AGP lines are +reported as `NOT EVALUATED`, not guessed. Update the mapping from Google's +[AGP release notes](https://developer.android.com/build/releases/agp-9-3-0-release-notes) +when a project adopts a new AGP line. + +Android's [Android 17 setup guide](https://developer.android.com/about/versions/17/setup-sdk) +keeps `compileSdk` and `targetSdk` as separate project decisions. The doctor +preserves that distinction. + +## Deterministic shell behavior + +Scripts that require BSD/POSIX semantics use explicit system executables such +as `/usr/bin/find`, `/usr/bin/grep`, `/usr/bin/sed`, and `/usr/bin/awk`, or use +`rg`/`fd` with their native syntax. Interactive aliases are not a scripting +contract. + +Instructions intended for an interactive terminal must not place +`set -euo pipefail` directly in the caller's shell. Use a script or a bounded +subshell: + +```bash +( + set -euo pipefail + # bounded operation +) +``` + +## Worktree trust + +`direnv` and mise trust are local security state, not Android build failures. +Use `direnv status`, `mise trust --show`, and a human-approved `direnv allow` or +`mise trust`; do not add Android-specific auto-trust. + +The current global mise template separately contains +`trusted_config_paths = ["~/Organizations"]`. Removing or narrowing that broad +existing grant affects every organization repository and requires the normal +ask-first review for a `~/.config` chezmoi template. This Android revision does +not silently change that cross-project policy. + +## Dated host snapshot: 2026-08-12 + +- Android Studio Preview: Quail 4 / 2026.1.4 Canary 4, build + `AI-261.26222.65.2614.16027044`; bundled JBR 25.0.3, native arm64. +- Homebrew's `android-cli` cask receipt is `1.0.15985488` and resolves + `/opt/homebrew/bin/android` to its launcher. The downloaded CLI payload is not + initialized, so `~/.androidrc` parsing remains operator-unverified. +- Android Studio Preview is not Homebrew-managed. A Stable + `android-studio` 2025.2.2.8 cask receipt remains even though its application + bundle is absent; that orphaned receipt is host cleanup evidence, not proof + of an installed Stable IDE. +- Canonical SDK: `/Users/verlyn13/Library/Android/sdk`. +- Metadata-backed platforms: Android 34, 35, 36, 36.1, Android 17/API 37 + (`platforms/android-37.0`), and a 37.1 Canary platform. +- Build Tools: 35.0.0, 36.0.0, 36.1.0; platform-tools 37.0.1. +- NDK 27.0.12077973 and CMake 3.22.1 are present. The empty + `ndk/29.0.13599879/.installer` remnant is not an installed NDK and the doctor + fails it as incomplete state. Remediation is below. +- Mise provides Temurin 21. Android Studio's JBR is application-owned and does + not replace the project JDK pin. + +This snapshot is evidence, not a permanent package manifest. Re-run the doctor +for current truth. + +## Clearing the two standing doctor failures + +Both are operator actions. The doctor reports them and stops there, because one +downloads code from Google and the other deletes from the SDK root. + +**`android_cli_available` — payload not initialized.** Run a read-only CLI +command in an interactive terminal, review the download it offers, and rerun the +doctor: + +```bash +android info +ng-doctor android +``` + +The payload lands at `$ANDROID_USER_HOME/bin/android-cli`, defaulting to +`~/.android/bin/android-cli`. If you export `ANDROID_USER_HOME`, the doctor +follows it. + +**`android_sdk_packages_readable` — incomplete NDK directory.** The failure +names the offending path. Verify it is genuinely metadata-free before removing +it, because a directory that merely *looks* partial may be a live package: + +```bash +ls -la ~/Library/Android/sdk/ndk/29.0.13599879 +# Expect: only .installer, no source.properties, no package.xml. +rm -rf ~/Library/Android/sdk/ndk/29.0.13599879 +``` + +Do not script this. The doctor deliberately does not auto-delete: the same +condition is produced by an interrupted download that a running Android Studio +may still be resuming, and `~/Library/Android` is the canonical SDK root. diff --git a/docs/project-conventions.md b/docs/project-conventions.md index a551c04..f840df6 100644 --- a/docs/project-conventions.md +++ b/docs/project-conventions.md @@ -258,6 +258,27 @@ Prefer relocatable commands over path-bound wrappers: Projects move between paths (local → workspace host). Direct venv shebangs can go stale after a repo move. +### Android projects + +System-config supplies Android host capabilities, not a shared Android build +policy. Android repositories own and pin: + +- AGP, Kotlin, and dependency versions in the repository's Gradle model; +- Gradle through `gradle/wrapper/gradle-wrapper.properties` and `./gradlew`; +- Java through `.mise.toml`; +- `compileSdk`, `targetSdk`, Build Tools when explicitly overridden, NDK, and + CMake requirements in Gradle build files/version catalogs. + +Do not depend on a globally installed Gradle or on “latest SDK installed.” Do +not use `~/.gradle` to inject project repositories, plugins, versions, or build +behavior. Check a checkout against the host without modifying either side: + +```bash +ng-doctor android "$PWD" +``` + +Full host policy: [`docs/android-development.md`](./android-development.md). + ### `.envrc` scope Keep `.envrc` narrow. Use it for: diff --git a/home/.chezmoiignore b/home/.chezmoiignore index 2e5c8c5..9ee0b8a 100644 --- a/home/.chezmoiignore +++ b/home/.chezmoiignore @@ -34,6 +34,14 @@ # NOTE: *.local would also match ~/.local/ — use specific patterns instead .zshrc.local .bashrc.local +# Android CLI configuration exists only on Android-capable hosts. +{{ if hasKey . "android" -}} +{{ if not .android -}} +.androidrc +{{ end -}} +{{ else -}} +.androidrc +{{ end -}} # Secret Management secrets/** diff --git a/home/dot_androidrc.tmpl b/home/dot_androidrc.tmpl new file mode 100644 index 0000000..30c32a2 --- /dev/null +++ b/home/dot_androidrc.tmpl @@ -0,0 +1 @@ +--sdk={{ joinPath .chezmoi.homeDir "Library/Android/sdk" }} diff --git a/home/dot_local/bin/executable_ng-doctor.tmpl b/home/dot_local/bin/executable_ng-doctor.tmpl index 416c272..0a127fb 100755 --- a/home/dot_local/bin/executable_ng-doctor.tmpl +++ b/home/dot_local/bin/executable_ng-doctor.tmpl @@ -11,6 +11,7 @@ # Usage: # ng-doctor # run all checks # ng-doctor # run checks for one category +# ng-doctor android [project-directory] # ng-doctor --list # list all checks # ng-doctor --summary # compact pass/fail/skip counts @@ -26,6 +27,8 @@ OP_VAULT="Dev" OP_SSH_AGENT_SOCKET="$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" OP_SSH_AGENT_CONFIG="$HOME/.config/1Password/ssh/agent.toml" OP_SSH_SIGN_PROGRAM="/Applications/1Password.app/Contents/MacOS/op-ssh-sign" +ANDROID_HOST_ENABLED='{{ .android | default false }}' +ANDROID_SDK_CANONICAL='{{ joinPath .chezmoi.homeDir "Library/Android/sdk" }}' # --- Output helpers ----------------------------------------------------------- @@ -407,6 +410,513 @@ run_tools_checks() { check_op_ready } +# --- Category: android -------------------------------------------------------- + +android_source_property() { + local file="$1" key="$2" + [[ -f "$file" ]] || return 0 + /usr/bin/awk -F= -v key="$key" ' + $1 == key { + sub(/^[^=]*=/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + print + exit + } + ' "$file" +} + +android_dir_inventory() { + local base="$1" path + local -a names=() + [[ -d "$base" ]] || { printf 'none'; return; } + for path in "$base"/*; do + [[ -d "$path" ]] || continue + [[ -f "$path/source.properties" ]] || continue + names+=("${path##*/}") + done + if (( ${#names[@]} == 0 )); then + printf 'none' + else + local IFS=',' + printf '%s' "${names[*]}" + fi +} + +android_incomplete_inventory() { + local category path + local -a names=() + for category in platforms build-tools ndk cmake; do + [[ -d "$ANDROID_SDK_CANONICAL/$category" ]] || continue + for path in "$ANDROID_SDK_CANONICAL/$category"/*; do + [[ -d "$path" && ! -f "$path/source.properties" ]] || continue + names+=("${category}/${path##*/}") + done + done + if (( ${#names[@]} > 0 )); then + local IFS=',' + printf '%s' "${names[*]}" + fi +} + +android_platform_path_for_api() { + local api="$1" path level preview + [[ -d "$ANDROID_SDK_CANONICAL/platforms" ]] || return 0 + for path in "$ANDROID_SDK_CANONICAL/platforms"/*; do + [[ -f "$path/source.properties" ]] || continue + level="$(android_source_property "$path/source.properties" "AndroidVersion.ApiLevel")" + preview="$(android_source_property "$path/source.properties" "AndroidVersion.PreviewSdkInt")" + [[ "$level" == "${api}" || "$level" == "${api}.0" ]] || continue + [[ -z "$preview" || "$preview" == "0" ]] || continue + printf '%s' "$path" + return + done + return 0 +} + +android_toml_string() { + local file="$1" key="$2" + [[ -f "$file" ]] || return 0 + /usr/bin/awk -v key="$key" ' + $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { + line=$0 + sub(/#.*/, "", line) + sub(/^[^=]*=[[:space:]]*/, "", line) + if (match(line, /"[^"]+"/)) { + print substr(line, RSTART + 1, RLENGTH - 2) + } + exit + } + ' "$file" +} + +android_join_semicolon() { + local item joined="" + for item in "$@"; do + [[ -n "$joined" ]] && joined+="; " + joined+="$item" + done + printf '%s' "$joined" +} + +# Repository-owned build files, at any module depth, with the directories that +# only ever hold generated or vendored copies pruned. Emits one path per line. +android_project_files() { + local project="$1" name + shift + local -a name_args=() + for name in "$@"; do + (( ${#name_args[@]} > 0 )) && name_args+=(-o) + name_args+=(-name "$name") + done + (( ${#name_args[@]} > 0 )) || return 0 + /usr/bin/find "$project" \ + \( -type d \( -name .git -o -name .gradle -o -name build -o -name node_modules \) -prune \) -o \ + \( -type f \( "${name_args[@]}" \) -print \) | + /usr/bin/sort +} + +android_gradle_literal() { + local project="$1" key="$2" keys file value + case "$key" in + compileSdk) keys="compileSdk|compileSdkVersion" ;; + targetSdk) keys="targetSdk|targetSdkVersion" ;; + minSdk) keys="minSdk|minSdkVersion" ;; + *) keys="$key" ;; + esac + while IFS= read -r file; do + value="$(/usr/bin/awk -v keys="$keys" ' + function strip_block_comments(text, start, tail, stop, prefix) { + while (1) { + if (in_block) { + stop=index(text, "*/") + if (!stop) return "" + text=substr(text, stop + 2) + in_block=0 + } + start=index(text, "/*") + if (!start) return text + prefix=substr(text, 1, start - 1) + tail=substr(text, start + 2) + stop=index(tail, "*/") + if (!stop) { + in_block=1 + return prefix + } + text=prefix substr(tail, stop + 2) + } + } + { + line=strip_block_comments($0) + sub(/\/\/.*/, "", line) + pattern="^[[:space:]]*(" keys ")([[:space:]]*=|[[:space:]]+)" + if (line !~ pattern) next + sub("^[[:space:]]*(" keys ")[[:space:]]*", "", line) + sub("^=[[:space:]]*", "", line) + if (match(line, /^\"?[0-9]+([.][0-9]+)*\"?/)) { + value=substr(line, RSTART, RLENGTH) + gsub(/\"/, "", value) + print value + exit + } + } + ' "$file")" + [[ -n "$value" ]] && { printf '%s' "$value"; return; } + done < <(android_project_files "$project" build.gradle build.gradle.kts) + return 0 +} + +android_cmake_literal() { + local project="$1" file value + while IFS= read -r file; do + value="$(/usr/bin/awk ' + { + line=$0 + sub(/#.*/, "", line) + if (!match(line, /cmake_minimum_required[[:space:]]*\([[:space:]]*VERSION[[:space:]]+[0-9][0-9.]*/)) next + chunk=substr(line, RSTART, RLENGTH) + if (match(chunk, /[0-9][0-9.]*$/)) { + print substr(chunk, RSTART, RLENGTH) + exit + } + } + ' "$file")" + [[ -n "$value" ]] && { printf '%s' "$value"; return; } + done < <(android_project_files "$project" CMakeLists.txt) + return 0 +} + +android_version_at_least() { + local actual="$1" required="$2" + /usr/bin/awk -v actual="$actual" -v required="$required" 'BEGIN { + gsub(/[^0-9.].*$/, "", actual) + gsub(/[^0-9.].*$/, "", required) + an=split(actual, a, ".") + rn=split(required, r, ".") + n=(an > rn ? an : rn) + for (i=1; i<=n; i++) { + av=(a[i] == "" ? 0 : a[i]) + 0 + rv=(r[i] == "" ? 0 : r[i]) + 0 + if (av > rv) exit 0 + if (av < rv) exit 1 + } + exit 0 + }' +} + +android_requirement_path() { + local label="$1" value="$2" path="$3" package_metadata="${4:-false}" + local present=false + if [[ "$package_metadata" == "true" ]]; then + [[ -f "$path/source.properties" ]] && present=true + else + [[ -e "$path" ]] && present=true + fi + _android_project_requirement_count=$(( _android_project_requirement_count + 1 )) + if $present; then + printf ' REQUIRED AND PRESENT: %s %s\n' "$label" "$value" + else + printf ' REQUIRED AND ABSENT: %s %s\n' "$label" "$value" + _android_project_missing_count=$(( _android_project_missing_count + 1 )) + fi +} + +android_requirement_compatible() { + local label="$1" actual="$2" required="$3" + _android_project_requirement_count=$(( _android_project_requirement_count + 1 )) + if android_version_at_least "$actual" "$required"; then + printf ' REQUIRED AND PRESENT: %s %s (minimum %s)\n' "$label" "$actual" "$required" + else + printf ' REQUIRED AND ABSENT: %s %s (minimum %s)\n' "$label" "$actual" "$required" + _android_project_missing_count=$(( _android_project_missing_count + 1 )) + fi +} + +check_android_sdk_root_consistent() { + local rc_sdk="" got=() + if [[ -f "$HOME/.androidrc" ]]; then + rc_sdk="$(/usr/bin/sed -n 's/^--sdk=//p' "$HOME/.androidrc" | /usr/bin/head -1)" + fi + [[ "${ANDROID_HOME:-}" == "$ANDROID_SDK_CANONICAL" ]] || got+=("ANDROID_HOME=${ANDROID_HOME:-unset}") + [[ "${ANDROID_SDK_ROOT:-}" == "$ANDROID_SDK_CANONICAL" ]] || got+=("ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT:-unset}") + [[ "$rc_sdk" == "$ANDROID_SDK_CANONICAL" ]] || got+=(".androidrc=${rc_sdk:-missing}") + [[ -d "$ANDROID_SDK_CANONICAL" ]] || got+=("SDK directory=missing") + if (( ${#got[@]} == 0 )); then + pass "android_sdk_root_consistent" "shell, Android CLI, and SDK directory agree on ${ANDROID_SDK_CANONICAL}" + else + fail "android_sdk_root_consistent" "canonical Android SDK root" \ + "$ANDROID_SDK_CANONICAL in the shell, ~/.androidrc, and filesystem" \ + "$(android_join_semicolon "${got[@]}")" + fi +} + +check_android_cli_available() { + local cli="${HOMEBREW_PREFIX}/bin/android" receipt="" resolved="" + # The launcher installs the payload at $ANDROID_USER_HOME/bin/android-cli and + # defaults ANDROID_USER_HOME to ~/.android (not ~/.android/cli, which Android + # Studio already owns for its IPC handshake). + local android_user_home="${ANDROID_USER_HOME:-$HOME/.android}" + local payload="$android_user_home/bin/android-cli" + if command -v brew >/dev/null 2>&1; then + receipt="$(brew list --cask --versions android-cli 2>/dev/null || true)" + fi + resolved="$(command -v android 2>/dev/null || true)" + if [[ -n "$receipt" && -x "$cli" && "$resolved" == "$cli" && -x "$payload" ]]; then + pass "android_cli_available" \ + "Homebrew launcher ${receipt#android-cli } resolves at ${cli}; payload initialized at ${payload}" + elif [[ -n "$receipt" && -x "$cli" && "$resolved" == "$cli" ]]; then + fail "android_cli_available" "Android CLI payload initialization" \ + "operator-initialized payload at $payload" \ + "Homebrew launcher ${receipt#android-cli } is present; payload is absent" + elif [[ -n "$receipt" && -x "$cli" ]]; then + fail "android_cli_available" "Android CLI launcher command resolution" "$cli" "${resolved:-not on PATH}" + else + fail "android_cli_available" "Android CLI Homebrew launcher cask" \ + "brew install --cask android-cli" "not installed" + fi +} + +check_android_studio_available() { + local app product build selector runtime arches host_arch joined + local preview_app="${NG_DOCTOR_ANDROID_STUDIO_PREVIEW_APP:-/Applications/Android Studio Preview.app}" + local stable_app="${NG_DOCTOR_ANDROID_STUDIO_STABLE_APP:-/Applications/Android Studio.app}" + local found=0 bad_arch=0 unreadable_runtime=0 + local -a details=() + host_arch="$(uname -m 2>/dev/null || true)" + for app in "$preview_app" "$stable_app"; do + [[ -d "$app" ]] || continue + found=$(( found + 1 )) + product="$app/Contents/Resources/product-info.json" + runtime="$app/Contents/jbr/Contents/Home/bin/java" + build="unknown" + selector="unknown" + [[ -f "$product" ]] && build="$(jq -r '.buildNumber // .version // "unknown"' "$product" 2>/dev/null || true)" + [[ -f "$product" ]] && selector="$(jq -r '.dataDirectoryName // "unknown"' "$product" 2>/dev/null || true)" + if [[ ! -x "$runtime" ]]; then + unreadable_runtime=$(( unreadable_runtime + 1 )) + details+=("${app##*/}: ${selector}, ${build}, runtime missing or unreadable") + continue + fi + arches="$(/usr/bin/file -b "$runtime" 2>/dev/null || true)" + if [[ -z "$arches" ]]; then + unreadable_runtime=$(( unreadable_runtime + 1 )) + elif [[ "$host_arch" == "arm64" && "$arches" != *"arm64"* ]]; then + bad_arch=$(( bad_arch + 1 )) + elif [[ "$host_arch" == "x86_64" && "$arches" != *"x86_64"* && "$arches" != *"x86-64"* ]]; then + bad_arch=$(( bad_arch + 1 )) + fi + details+=("${app##*/}: ${selector}, ${build}, ${arches:-runtime unreadable}") + done + # Expanding an empty array under `set -u` is fatal on bash < 4.4 (macOS ships + # 3.2), so resolve the no-bundle case before touching "${details[@]}". + if (( found == 0 )); then + fail "android_studio_available" "Android Studio application" "Preview or Stable bundle" "not installed" + return + fi + joined="$(android_join_semicolon "${details[@]}")" + if (( bad_arch > 0 )); then + fail "android_studio_available" "Android Studio bundled runtime architecture" "$host_arch" "$joined" + elif (( unreadable_runtime > 0 )); then + fail "android_studio_available" "Android Studio bundled runtime readability" \ + "executable Mach-O JBR for $host_arch" "$joined" + else + pass "android_studio_available" "$joined" + fi +} + +check_android_platform_tools_available() { + local adb="$ANDROID_SDK_CANONICAL/platform-tools/adb" revision="" version="" + revision="$(android_source_property "$ANDROID_SDK_CANONICAL/platform-tools/source.properties" "Pkg.Revision")" + if [[ -x "$adb" ]]; then + version="$("$adb" version 2>/dev/null | /usr/bin/head -1 || true)" + pass "android_platform_tools_available" "platform-tools ${revision:-unknown}; ${version:-adb version unreadable}" + else + fail "android_platform_tools_available" "canonical SDK platform-tools" \ + "executable at $adb" "missing" + fi +} + +check_android_sdk_packages_readable() { + local platforms build_tools ndks cmakes incomplete + platforms="$(android_dir_inventory "$ANDROID_SDK_CANONICAL/platforms")" + build_tools="$(android_dir_inventory "$ANDROID_SDK_CANONICAL/build-tools")" + ndks="$(android_dir_inventory "$ANDROID_SDK_CANONICAL/ndk")" + cmakes="$(android_dir_inventory "$ANDROID_SDK_CANONICAL/cmake")" + incomplete="$(android_incomplete_inventory)" + if [[ "$platforms" == "none" || "$build_tools" == "none" || -n "$incomplete" ]]; then + fail "android_sdk_packages_readable" "installed SDK package inventory" \ + "metadata-backed packages and no incomplete package directories" \ + "platforms=${platforms}; build-tools=${build_tools}; ndk=${ndks}; cmake=${cmakes}; incomplete=${incomplete:-none}" + else + pass "android_sdk_packages_readable" \ + "platforms=${platforms}; build-tools=${build_tools}; ndk=${ndks}; cmake=${cmakes}" + fi +} + +check_android_jdks_available() { + local json versions + if ! command -v mise >/dev/null 2>&1; then + skip "android_jdks_available" "mise not installed" + return + fi + if ! command -v jq >/dev/null 2>&1; then + skip "android_jdks_available" "jq not installed" + return + fi + json="$(mise ls java --installed --json 2>/dev/null || true)" + if ! jq -e 'type == "array"' <<<"$json" >/dev/null 2>&1; then + fail "android_jdks_available" "mise JDK inventory readability" \ + "valid JSON array from mise" "missing or malformed output" + return + fi + versions="$(jq -r '.[].version // empty' <<<"$json" 2>/dev/null | /usr/bin/paste -sd, - || true)" + if [[ -n "$versions" ]]; then + pass "android_jdks_available" "mise-installed JDKs=${versions}; Android Studio also owns its bundled JBR" + else + fail "android_jdks_available" "mise Android JDK capability" "at least one project-selectable JDK" "none installed" + fi +} + +check_android_project_requirements() { + local project="$1" catalog wrapper mise_file + local compile_sdk target_sdk min_sdk agp kotlin gradle java ndk cmake build_tools java_major path compile_platform + if [[ -z "$project" ]]; then + skip "android_project_requirements" "no project supplied; run ng-doctor android /path/to/project" + return + fi + if ! project="$(cd "$project" 2>/dev/null && pwd -P)"; then + fail "android_project_requirements" "Android project directory" "readable directory" "$1" + return + fi + catalog="$project/gradle/libs.versions.toml" + wrapper="$project/gradle/wrapper/gradle-wrapper.properties" + mise_file="$project/.mise.toml" + if [[ ! -f "$wrapper" ]]; then + fail "android_project_requirements" "Android project contract" \ + "checked-in Gradle wrapper" "missing at $wrapper" + return + fi + + compile_sdk="$(android_toml_string "$catalog" "androidCompileSdk")" + [[ -n "$compile_sdk" ]] || compile_sdk="$(android_gradle_literal "$project" "compileSdk")" + target_sdk="$(android_toml_string "$catalog" "androidTargetSdk")" + [[ -n "$target_sdk" ]] || target_sdk="$(android_gradle_literal "$project" "targetSdk")" + min_sdk="$(android_toml_string "$catalog" "androidMinSdk")" + [[ -n "$min_sdk" ]] || min_sdk="$(android_gradle_literal "$project" "minSdk")" + agp="$(android_toml_string "$catalog" "androidGradlePlugin")" + kotlin="$(android_toml_string "$catalog" "kotlin")" + # gradle-wrapper.properties is a Java properties file, so a superseded + # distributionUrl is routinely left behind as a `#` comment after a bump. + gradle="$(/usr/bin/awk ' + { + line=$0 + sub(/#.*/, "", line) + if (line !~ /^[[:space:]]*distributionUrl[[:space:]]*[=:]/) next + if (match(line, /gradle-[0-9][0-9.]*-/)) { + print substr(line, RSTART + 7, RLENGTH - 8) + exit + } + } + ' "$wrapper")" + java="$(android_toml_string "$mise_file" "java")" + ndk="$(android_gradle_literal "$project" "ndkVersion")" + cmake="$(android_cmake_literal "$project")" + build_tools="$(android_gradle_literal "$project" "buildToolsVersion")" + + printf ' PROJECT DECLARES: compileSdk=%s targetSdk=%s minSdk=%s AGP=%s Gradle=%s Kotlin=%s JDK=%s\n' \ + "${compile_sdk:-unknown}" "${target_sdk:-unknown}" "${min_sdk:-unknown}" \ + "${agp:-unknown}" "${gradle:-unknown}" "${kotlin:-unknown}" "${java:-unknown}" + + _android_project_missing_count=0 + _android_project_requirement_count=0 + if [[ -n "$compile_sdk" ]]; then + compile_platform="$(android_platform_path_for_api "$compile_sdk")" + android_requirement_path "SDK platform" "$compile_sdk" \ + "${compile_platform:-$ANDROID_SDK_CANONICAL/platforms/android-${compile_sdk}}" true + else + printf ' REQUIRED AND ABSENT: compileSdk declaration\n' + _android_project_missing_count=$(( _android_project_missing_count + 1 )) + _android_project_requirement_count=$(( _android_project_requirement_count + 1 )) + fi + [[ -n "$ndk" ]] && android_requirement_path "NDK" "$ndk" "$ANDROID_SDK_CANONICAL/ndk/$ndk" true + [[ -n "$cmake" ]] && android_requirement_path "CMake" "$cmake" "$ANDROID_SDK_CANONICAL/cmake/$cmake" true + [[ -n "$build_tools" ]] && android_requirement_path "Build Tools" "$build_tools" "$ANDROID_SDK_CANONICAL/build-tools/$build_tools" true + + if [[ -n "$java" ]]; then + path="" + for path in "$HOME/.local/share/mise/installs/java/${java}"*; do + [[ -d "$path" ]] && break + path="" + done + android_requirement_path "mise JDK" "$java" "${path:-$HOME/.local/share/mise/installs/java/$java}" + fi + + # Source-backed compatibility floor for the AGP line currently mapped here. + # Unknown/future AGP lines are reported, never guessed. + case "$agp" in + 9.3 | 9.3.*) + [[ -n "$gradle" ]] && android_requirement_compatible "AGP 9.3 Gradle" "$gradle" "9.5.0" + if [[ -n "$compile_sdk" ]]; then + _android_project_requirement_count=$(( _android_project_requirement_count + 1 )) + if [[ "$compile_sdk" =~ ^[0-9]+([.][0-9]+)*$ ]] && \ + android_version_at_least "37" "$compile_sdk"; then + printf ' REQUIRED AND PRESENT: AGP 9.3 maximum API 37 (project uses %s)\n' "$compile_sdk" + else + printf ' REQUIRED AND ABSENT: AGP 9.3 maximum API 37 (project uses %s)\n' "$compile_sdk" + _android_project_missing_count=$(( _android_project_missing_count + 1 )) + fi + fi + if [[ -z "$build_tools" ]]; then + android_requirement_path "AGP 9.3 default Build Tools" "36.0.0" \ + "$ANDROID_SDK_CANONICAL/build-tools/36.0.0" true + fi + java_major="$(/usr/bin/sed -nE 's/^[^0-9]*([0-9]+).*/\1/p' <<<"${java#*-}")" + [[ -n "$java_major" ]] && android_requirement_compatible "AGP 9.3 JDK" "$java_major" "17" + ;; + *) + printf ' NOT EVALUATED: no source-backed AGP compatibility mapping for %s\n' "${agp:-unknown}" + ;; + esac + + if [[ -n "$compile_sdk" && -d "$ANDROID_SDK_CANONICAL/platforms" ]]; then + for path in "$ANDROID_SDK_CANONICAL/platforms"/*; do + [[ -f "$path/source.properties" ]] || continue + [[ -n "$compile_platform" && "$path" == "$compile_platform" ]] && continue + printf ' INSTALLED BUT NOT REQUIRED: SDK platform %s\n' "${path##*/}" + done + fi + + if (( _android_project_missing_count == 0 )); then + pass "android_project_requirements" \ + "${_android_project_requirement_count} host requirement(s) for ${project} are present" + else + fail "android_project_requirements" "project-declared Android host requirements" \ + "all required capabilities present" \ + "${_android_project_missing_count} of ${_android_project_requirement_count} absent for ${project}" + fi +} + +run_android_checks() { + local project="${1:-}" + section "android" + if [[ "$ANDROID_HOST_ENABLED" != "true" ]]; then + skip "android_sdk_root_consistent" "android=false in chezmoi machine data" + skip "android_cli_available" "android=false in chezmoi machine data" + skip "android_studio_available" "android=false in chezmoi machine data" + skip "android_platform_tools_available" "android=false in chezmoi machine data" + skip "android_sdk_packages_readable" "android=false in chezmoi machine data" + skip "android_jdks_available" "android=false in chezmoi machine data" + skip "android_project_requirements" "android=false in chezmoi machine data" + return + fi + check_android_sdk_root_consistent + check_android_cli_available + check_android_studio_available + check_android_platform_tools_available + check_android_sdk_packages_readable + check_android_jdks_available + check_android_project_requirements "$project" +} + # --- Category: ssh ------------------------------------------------------------ check_ssh_config_exists() { @@ -2206,10 +2716,10 @@ run_mcp_checks() { # --- Main --------------------------------------------------------------------- -ALL_CATEGORIES=(shell path tools ssh iterm2 agentic filesystem hygiene governance identity posture vscode mcp) +ALL_CATEGORIES=(shell path tools android ssh iterm2 agentic filesystem hygiene governance identity posture vscode mcp) list_checks() { - echo "ng-doctor checks (94 checks, 13 categories):" + echo "ng-doctor checks (101 checks, 14 categories):" echo "" printf "%-14s %s\n" "CATEGORY" "CHECK" printf "%-14s %s\n" "────────" "─────" @@ -2239,6 +2749,13 @@ list_checks() { printf "%-14s %s\n" "tools" "cargo_clippy_installed" printf "%-14s %s\n" "tools" "op_installed" printf "%-14s %s\n" "tools" "op_ready" + printf "%-14s %s\n" "android" "android_sdk_root_consistent" + printf "%-14s %s\n" "android" "android_cli_available" + printf "%-14s %s\n" "android" "android_studio_available" + printf "%-14s %s\n" "android" "android_platform_tools_available" + printf "%-14s %s\n" "android" "android_sdk_packages_readable" + printf "%-14s %s\n" "android" "android_jdks_available" + printf "%-14s %s\n" "android" "android_project_requirements" printf "%-14s %s\n" "ssh" "ssh_config_exists" printf "%-14s %s\n" "ssh" "ssh_config_includes_conf_d" printf "%-14s %s\n" "ssh" "ssh_1password_agent_config_exists" @@ -2314,6 +2831,7 @@ run_category() { shell) run_shell_checks ;; path) run_path_checks ;; tools) run_tools_checks ;; + android) run_android_checks "${2:-}" ;; ssh) run_ssh_checks ;; iterm2) run_iterm2_checks ;; agentic) run_agentic_checks ;; @@ -2332,6 +2850,12 @@ main() { echo "ng-doctor — system-config development machine verification" echo "" + if (( $# > 2 )) || { (( $# == 2 )) && [[ "$1" != "android" ]]; }; then + echo "Usage: ng-doctor [--list|--summary|category]" >&2 + echo " ng-doctor android [project-directory]" >&2 + exit 2 + fi + case "${1:-}" in --list) list_checks @@ -2351,7 +2875,7 @@ main() { done ;; *) - run_category "$1" + run_category "$1" "${2:-}" ;; esac diff --git a/policies/version-policy.md b/policies/version-policy.md index 56f6c31..b0e5317 100644 --- a/policies/version-policy.md +++ b/policies/version-policy.md @@ -3,8 +3,8 @@ title: Version Management Policy category: policy component: versions status: active -version: 1.1.0 -last_updated: 2026-04-15 +version: 1.2.0 +last_updated: 2026-08-12 tags: [policy, compliance, versions] priority: medium --- @@ -33,7 +33,7 @@ priority: medium - Python: 3.13 - Rust: stable channel - Go: 1.23.x - - Java: Temurin 17 (for Android) + - Java: project-pinned for Android; the host currently keeps Temurin 21 available - **Dependencies:** Exact pins via lockfiles - **Use for:** Production, teaching, publishing @@ -144,11 +144,21 @@ rust = "stable" ```toml # .mise.toml [tools] -java = "temurin-17" +java = "temurin-21" # example only; pin the version required by the project ``` +- `system-config` owns host capabilities: Android Studio/CLI, the canonical SDK + root, installed SDK/NDK/CMake packages, and project-selectable JDKs +- The repository owns build policy: AGP, Gradle, Kotlin, Java, `compileSdk`, + `targetSdk`, NDK, and CMake requirements - Version catalog: `gradle/libs.versions.toml` -- Wrapper: `gradle-wrapper.properties` -- Use ARM64 system images locally (M3 optimization) +- Wrapper: `gradle/wrapper/gradle-wrapper.properties`; invoke `./gradlew`, not a + globally installed Gradle +- Installing a newer SDK never rewrites project declarations; stable and + preview SDKs may coexist +- Keep global `~/.gradle` state limited to caches and runtime support; it must + not inject repositories, plugins, dependency versions, or build behavior +- Use ARM64 system images locally on Apple Silicon +- Host/project compatibility check: `ng-doctor android /path/to/project` --- diff --git a/scripts/validate-repo.sh b/scripts/validate-repo.sh index 444d7b9..e4252a6 100755 --- a/scripts/validate-repo.sh +++ b/scripts/validate-repo.sh @@ -22,6 +22,7 @@ shellcheck home/dot_local/bin/executable_mcp-*.tmpl shellcheck home/dot_local/bin/executable_ng-doctor.tmpl tests/system-update/run.sh +tests/android/run.sh tests/vscode/run.sh tests/mcp-vscode/run.sh tests/mcp-codex/run.sh diff --git a/tests/android/run.sh b/tests/android/run.sh new file mode 100755 index 0000000..d495b1a --- /dev/null +++ b/tests/android/run.sh @@ -0,0 +1,294 @@ +#!/usr/bin/env bash +# Android host-policy rendering and ng-doctor regression tests. + +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DOCTOR_TEMPLATE="$ROOT/home/dot_local/bin/executable_ng-doctor.tmpl" +ANDROIDRC_TEMPLATE="$ROOT/home/dot_androidrc.tmpl" +IGNORE_TEMPLATE="$ROOT/home/.chezmoiignore" +if ! TEST_ROOT="$(mktemp -d)" || [[ -z "$TEST_ROOT" || ! -d "$TEST_ROOT" ]]; then + echo "not ok - mktemp -d failed; refusing to run with an empty TEST_ROOT" >&2 + exit 1 +fi +trap 'rm -rf "$TEST_ROOT"' EXIT + +pass=0 +fail=0 +ok() { echo "ok - $1"; pass=$((pass + 1)); } +not_ok() { echo "not ok - $1"; fail=$((fail + 1)); } + +assert_eq() { + local label="$1" actual="$2" expected="$3" + if [[ "$actual" == "$expected" ]]; then ok "$label"; else not_ok "$label"; fi +} + +assert_has() { + local label="$1" needle="$2" haystack="$3" + if grep -qF -- "$needle" <<<"$haystack"; then ok "$label"; else not_ok "$label"; fi +} + +assert_not_has() { + local label="$1" needle="$2" haystack="$3" + if grep -qF -- "$needle" <<<"$haystack"; then not_ok "$label"; else ok "$label"; fi +} + +if ! command -v chezmoi >/dev/null 2>&1; then + echo "not ok - chezmoi is required for Android template tests" >&2 + exit 1 +fi + +FAKE_HOME="$TEST_ROOT/home" +BREW_ROOT="$TEST_ROOT/homebrew" +SDK="$FAKE_HOME/Library/Android/sdk" +EMPTY_CONFIG="$TEST_ROOT/empty.toml" +OVERRIDE_DATA="$(jq -cn --arg prefix "$BREW_ROOT" '{android:true,homebrew_prefix:$prefix}')" +mkdir -p "$FAKE_HOME" "$BREW_ROOT/bin" +: >"$EMPTY_CONFIG" + +render_with_data() { + local template="$1" data="$2" + HOME="$FAKE_HOME" chezmoi \ + --config "$EMPTY_CONFIG" \ + --source "$ROOT/home" \ + --working-tree "$ROOT" \ + --override-data "$data" \ + execute-template --file "$template" +} + +androidrc="$(render_with_data "$ANDROIDRC_TEMPLATE" "$OVERRIDE_DATA")" +assert_eq "androidrc is one documented flag with no untested comment syntax" \ + "$androidrc" "--sdk=$SDK" + +missing_key_ignore="$( + HOME="$FAKE_HOME" chezmoi \ + --config "$EMPTY_CONFIG" \ + --source "$ROOT/home" \ + --working-tree "$ROOT" \ + execute-template --file "$IGNORE_TEMPLATE" 2>&1 +)" +status=$? +assert_eq "chezmoiignore renders when android machine data is absent" "$status" "0" +assert_has "missing android data fails closed by ignoring androidrc" ".androidrc" "$missing_key_ignore" + +android_false_ignore="$(render_with_data "$IGNORE_TEMPLATE" '{"android":false}')" +android_true_ignore="$(render_with_data "$IGNORE_TEMPLATE" '{"android":true}')" +assert_has "android=false ignores androidrc" ".androidrc" "$android_false_ignore" +assert_not_has "android=true manages androidrc" ".androidrc" "$android_true_ignore" + +# Build a host fixture. The Preview bundle intentionally lacks its JBR so the +# test can distinguish runtime readability from architecture mismatch; the +# Stable bundle carries a real host-native runtime so the architecture +# comparison is exercised rather than dead, and so the two-bundle join runs. +mkdir -p \ + "$SDK/platforms/android-36" \ + "$SDK/platforms/android-37.0" \ + "$SDK/build-tools/36.0.0" \ + "$SDK/platform-tools" \ + "$SDK/ndk/27.0.12077973" \ + "$SDK/cmake/3.22.1" \ + "$FAKE_HOME/.android/bin" \ + "$FAKE_HOME/.local/share/mise/installs/java/temurin-21" \ + "$TEST_ROOT/Android Studio Preview.app/Contents/Resources" \ + "$TEST_ROOT/Android Studio.app/Contents/Resources" \ + "$TEST_ROOT/Android Studio.app/Contents/jbr/Contents/Home/bin" +cp /bin/sh "$TEST_ROOT/Android Studio.app/Contents/jbr/Contents/Home/bin/java" +printf '%s\n' '{"buildNumber":"AI-stable","dataDirectoryName":"AndroidStudioStable"}' \ + >"$TEST_ROOT/Android Studio.app/Contents/Resources/product-info.json" + +printf '%s\n' '--sdk='"$SDK" >"$FAKE_HOME/.androidrc" +printf '%s\n' 'AndroidVersion.ApiLevel=36' 'AndroidVersion.PreviewSdkInt=0' \ + >"$SDK/platforms/android-36/source.properties" +printf '%s\n' 'AndroidVersion.ApiLevel=37.0' 'AndroidVersion.PreviewSdkInt=0' \ + >"$SDK/platforms/android-37.0/source.properties" +printf '%s\n' 'Pkg.Revision=36.0.0' >"$SDK/build-tools/36.0.0/source.properties" +printf '%s\n' 'Pkg.Revision=37.0.1' >"$SDK/platform-tools/source.properties" +printf '%s\n' 'Pkg.Revision=27.0.12077973' >"$SDK/ndk/27.0.12077973/source.properties" +printf '%s\n' 'Pkg.Revision=3.22.1' >"$SDK/cmake/3.22.1/source.properties" +printf '%s\n' '{"buildNumber":"AI-test","dataDirectoryName":"AndroidStudioTest"}' \ + >"$TEST_ROOT/Android Studio Preview.app/Contents/Resources/product-info.json" + +cat >"$BREW_ROOT/bin/brew" <<'EOF' +#!/usr/bin/env bash +if [[ "$*" == "list --cask --versions android-cli" ]]; then + echo "android-cli 1.0.test" + exit 0 +fi +exit 1 +EOF +cat >"$BREW_ROOT/bin/android" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF +cat >"$FAKE_HOME/.android/bin/android-cli" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF +cat >"$SDK/platform-tools/adb" <<'EOF' +#!/usr/bin/env bash +echo "Android Debug Bridge version 1.0.test" +EOF +cat >"$BREW_ROOT/bin/mise" <<'EOF' +#!/usr/bin/env bash +if [[ "${ANDROID_TEST_MISE_INVALID:-0}" == "1" ]]; then + echo "not-json" +else + echo '[{"version":"temurin-21"}]' +fi +EOF +chmod +x \ + "$BREW_ROOT/bin/brew" \ + "$BREW_ROOT/bin/android" \ + "$BREW_ROOT/bin/mise" \ + "$FAKE_HOME/.android/bin/android-cli" \ + "$SDK/platform-tools/adb" + +RENDERED_DOCTOR="$TEST_ROOT/ng-doctor" +render_with_data "$DOCTOR_TEMPLATE" "$OVERRIDE_DATA" >"$RENDERED_DOCTOR" +chmod +x "$RENDERED_DOCTOR" + +PROJECT_KTS="$TEST_ROOT/project-kts" +mkdir -p "$PROJECT_KTS/gradle/wrapper" "$PROJECT_KTS/features/camera" +cat >"$PROJECT_KTS/gradle/libs.versions.toml" <<'EOF' +[versions] +androidCompileSdk = "37.0" +androidTargetSdk = "36" +androidMinSdk = "26" +androidGradlePlugin = "9.3.1" +kotlin = "2.4.10" +EOF +cat >"$PROJECT_KTS/gradle/wrapper/gradle-wrapper.properties" <<'EOF' +#distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +EOF +cat >"$PROJECT_KTS/.mise.toml" <<'EOF' +[tools] +java = "temurin-21" +EOF +cat >"$PROJECT_KTS/features/camera/build.gradle.kts" <<'EOF' +android { + // ndkVersion = "27.0.12077973" + /* ndkVersion = "28.0.13004108" */ + ndkVersion = "29.0.13599879" +} +EOF +cat >"$PROJECT_KTS/CMakeLists.txt" <<'EOF' +# cmake_minimum_required(VERSION 99.9.9) +EOF +cat >"$PROJECT_KTS/features/camera/CMakeLists.txt" <<'EOF' +cmake_minimum_required(VERSION 3.22.1) +EOF + +run_doctor() { + local project="$1" + # ANDROID_USER_HOME is emptied so the check exercises its own default rather + # than whatever the ambient environment happens to carry. + HOME="$FAKE_HOME" \ + ANDROID_USER_HOME="" \ + ANDROID_HOME="$SDK" \ + ANDROID_SDK_ROOT="$SDK" \ + PATH="$BREW_ROOT/bin:/usr/bin:/bin" \ + NG_DOCTOR_ANDROID_STUDIO_PREVIEW_APP="${NG_DOCTOR_ANDROID_STUDIO_PREVIEW_APP-$TEST_ROOT/Android Studio Preview.app}" \ + NG_DOCTOR_ANDROID_STUDIO_STABLE_APP="${NG_DOCTOR_ANDROID_STUDIO_STABLE_APP-$TEST_ROOT/Android Studio.app}" \ + "$RENDERED_DOCTOR" android "$project" 2>&1 +} + +kts_output="$(run_doctor "$PROJECT_KTS")" +assert_has "launcher and initialized payload are reported separately" \ + "android_cli_available: Homebrew launcher 1.0.test" "$kts_output" +assert_has "dotted compileSdk is parsed" "PROJECT DECLARES: compileSdk=37.0" "$kts_output" +assert_has "stable dotted API resolves to an installed platform" \ + "REQUIRED AND PRESENT: SDK platform 37.0" "$kts_output" +assert_has "dotted API is accepted by the AGP maximum check" \ + "REQUIRED AND PRESENT: AGP 9.3 maximum API 37 (project uses 37.0)" "$kts_output" +assert_has "nested live NDK declaration wins over comments" \ + "REQUIRED AND ABSENT: NDK 29.0.13599879" "$kts_output" +assert_not_has "commented NDK is not treated as the declaration" \ + "REQUIRED AND PRESENT: NDK 27.0.12077973" "$kts_output" +assert_has "missing JBR reports readability" \ + "Android Studio bundled runtime readability" "$kts_output" +assert_not_has "missing JBR is not mislabeled as architecture" \ + "Android Studio bundled runtime architecture" "$kts_output" +assert_not_has "a readable host-native JBR is not reported as unreadable" \ + "AndroidStudioStable, AI-stable, runtime missing or unreadable" "$kts_output" +assert_has "multiple Studio bundles join with a two-character separator" \ + "runtime missing or unreadable; Android Studio.app: AndroidStudioStable" "$kts_output" +assert_has "nested CMake declaration is found behind a commented decoy" \ + "REQUIRED AND PRESENT: CMake 3.22.1" "$kts_output" +assert_not_has "commented CMake version is not treated as the declaration" \ + "CMake 99.9.9" "$kts_output" +assert_has "live distributionUrl wins over a superseded commented one" \ + "Gradle=9.6.1" "$kts_output" +assert_not_has "commented distributionUrl does not shadow the live one" \ + "Gradle=9.1.0" "$kts_output" + +no_studio_output="$( + export NG_DOCTOR_ANDROID_STUDIO_PREVIEW_APP="$TEST_ROOT/absent-preview.app" + export NG_DOCTOR_ANDROID_STUDIO_STABLE_APP="$TEST_ROOT/absent-stable.app" + run_doctor "$PROJECT_KTS" +)" +assert_has "no Studio bundle reports the application as absent" \ + "Android Studio application" "$no_studio_output" +assert_has "an empty Studio detail array does not abort the run under bash 3.2" \ + "Summary:" "$no_studio_output" + +mv "$FAKE_HOME/.android/bin/android-cli" "$FAKE_HOME/.android/bin/android-cli.hold" +launcher_only_output="$(run_doctor "$PROJECT_KTS")" +mv "$FAKE_HOME/.android/bin/android-cli.hold" "$FAKE_HOME/.android/bin/android-cli" +assert_has "launcher-only installation fails payload initialization" \ + "android_cli_available: Android CLI payload initialization" "$launcher_only_output" + +PROJECT_GROOVY="$TEST_ROOT/project-groovy" +mkdir -p "$PROJECT_GROOVY/gradle/wrapper" "$PROJECT_GROOVY/modules/deep/camera" +cat >"$PROJECT_GROOVY/gradle/libs.versions.toml" <<'EOF' +[versions] +androidGradlePlugin = "9.2.0" +kotlin = "2.3.0" +EOF +cat >"$PROJECT_GROOVY/gradle/wrapper/gradle-wrapper.properties" <<'EOF' +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +EOF +cat >"$PROJECT_GROOVY/.mise.toml" <<'EOF' +[tools] +java = "temurin-21" +EOF +cat >"$PROJECT_GROOVY/modules/deep/camera/build.gradle" <<'EOF' +android { + // compileSdk = 99 + compileSdk 36 + targetSdkVersion = 35 + minSdkVersion 26 +} +EOF + +groovy_output="$(run_doctor "$PROJECT_GROOVY")" +assert_has "recursive Groovy compileSdk syntax is parsed" \ + "PROJECT DECLARES: compileSdk=36 targetSdk=35 minSdk=26" "$groovy_output" +assert_not_has "commented Groovy compileSdk is ignored" \ + "PROJECT DECLARES: compileSdk=99" "$groovy_output" + +PROJECT_OVERMAX="$TEST_ROOT/project-overmax" +mkdir -p "$PROJECT_OVERMAX/gradle/wrapper" +cat >"$PROJECT_OVERMAX/gradle/libs.versions.toml" <<'EOF' +[versions] +androidCompileSdk = "37.1" +androidGradlePlugin = "9.3.1" +EOF +cat >"$PROJECT_OVERMAX/gradle/wrapper/gradle-wrapper.properties" <<'EOF' +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +EOF + +overmax_output="$(run_doctor "$PROJECT_OVERMAX")" +assert_has "an API level above the AGP 9.3 maximum is rejected" \ + "REQUIRED AND ABSENT: AGP 9.3 maximum API 37 (project uses 37.1)" "$overmax_output" + +invalid_mise_output="$(ANDROID_TEST_MISE_INVALID=1 run_doctor "$PROJECT_GROOVY")" +assert_has "malformed mise JSON produces a bounded JDK failure" \ + "android_jdks_available: mise JDK inventory readability" "$invalid_mise_output" +assert_has "project check still runs after malformed mise JSON" \ + "android_project_requirements:" "$invalid_mise_output" +assert_has "doctor still prints a summary after malformed mise JSON" \ + "Summary:" "$invalid_mise_output" + +printf '%s\n' "android tests: $pass passed, $fail failed" +[[ "$fail" -eq 0 ]] From f8bc4585489c61ed00c315977739433fbcd508a1 Mon Sep 17 00:00:00 2001 From: verlyn13 Date: Wed, 12 Aug 2026 21:41:13 -0800 Subject: [PATCH 2/2] docs(agents): shrink AGENTS.md to a contract and move routing to docs/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md had grown to 300 lines / 19.9 KB, 53% of the always-on instruction context a Claude session loads before reading a line of code. Roughly half of it was derivable (a directory tree, a restatement of validate-repo.sh's steps, an MCP inventory `jq` prints) or duplicated from an ancestor file. The annotated doc index alone was 8 KB — 132% of a 6 KB budget for the whole file — so it moves to docs/README.md, which has no budget, can carry the long annotations verbatim, and is now complete: 50 top-level docs plus every subtree entry point, against 42 before. Eleven docs were previously unindexed, including the incident that justifies the GitHub-MCP containment. Hazards embedded in index annotations are lifted into an explicit `## Hazards` block and stated as imperatives. An annotation only reaches an agent that has already decided to open that doc; the agent about to delete the MCP disable marker did not arrive via the index. Resolves a live self-contradiction: the file required `hasKey` in one section, mandated `| default` in Definition of done, and prescribed `| default` as the fix for `map has no entry for key` — the error `| default` produces. Stated once now: chezmoi runs missingkey=error, `android` is a required key so reading it bare is correct, and `hasKey` is for genuinely optional keys. Also corrected: the gate description claimed shellcheck covers every `.sh` (it is tracked-only, deliberately) and called the fish check a grep (it is a directory test); `chezmoi apply --dry-run` needs `--force` to complete non-interactively; README's docs count and index pointer. AGENTS.md: 300 -> 106 lines, 19,944 -> 5,674 bytes. --- AGENTS.md | 375 +++++++++++++------------------------------------ CLAUDE.md | 32 +++-- README.md | 5 +- docs/README.md | 243 ++++++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+), 294 deletions(-) create mode 100644 docs/README.md diff --git a/AGENTS.md b/AGENTS.md index 8dea936..a509bd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,285 +1,106 @@ # AGENTS.md -Reproducible macOS development environment. `system-config` is the active -chezmoi source for the zsh-first shell surface, global mise defaults, -direnv helpers, iTerm2 dynamic profiles, nonsecret SSH client policy, -and the user-level MCP baseline. +`system-config` is the active chezmoi source for this workstation's shell, +mise, direnv, iTerm2, SSH client policy, and user-level MCP baseline. Machine +data lives outside the repo in `~/.config/chezmoi/chezmoi.toml`. -## Authoritative docs - -Read these for current state: - -- [`docs/restart-handoff-2026-08-08.md`](./docs/restart-handoff-2026-08-08.md) — dated restart snapshot for a fresh agent: verified Git and host state, updater evidence, remaining work, scope boundaries, and first commands. It routes to the authorities below and does not replace them -- [`docs/ai-tool-config-ownership.md`](./docs/ai-tool-config-ownership.md) — **the ownership boundary across every AI tool**: which of the four mechanisms deploys each path, the global-user config tree, the project-scope model, the established managed/root tier, the git-identity registration gap, and the leftover-workflow register. Read before editing any agent config file -- [`docs/project-conventions.md`](./docs/project-conventions.md) — compatibility guide for downstream projects (link this from a project's own `AGENTS.md`) -- [`docs/secrets.md`](./docs/secrets.md) — secret-handling policy (1Password) -- [`docs/1password-service-account.md`](./docs/1password-service-account.md) — **live** operator runbook for the `mcp-gui-readonly` service account: the non-interactive `op` auth plane that GUI/headless MCP wrappers (brave-search, firecrawl, runpod) are coded to use. **Provisioning state 2026-07-27: LIVE** — service account created (read-only on `Dev`), keychain item present and intact, `ng-doctor mcp` 6/6. Read the token-storage box before rotating: `security add-generic-password -w` truncates at 128 bytes and cannot store an SA token; use `scripts/store-op-sa-token.sh`. Indexed here directly because it was previously reachable only through the two draft platform docs below, which this file labels "not live provisioning policy" — the runbook that would have caught the failure was two hops behind a "not live" sign -- [`docs/secret-records.md`](./docs/secret-records.md) — credential-record register (identifier, owner, consumer, scope, rotation, stop rules; no secret values). The SA token's current record status is `active`; the dated 2026-05-28 transition entry remains historical evidence of its earlier `planned` state -- [`docs/infisical-cli.md`](./docs/infisical-cli.md) — Infisical CLI baseline for project runtime secret workflows; Homebrew core install/update path and syntax guardrails -- [`docs/homebrew-tap-trust.md`](./docs/homebrew-tap-trust.md) — Homebrew 6 third-party tap trust boundary; item-level trust policy, operator review workflow, and fail-closed updater behavior -- [`docs/1password-platform-architecture.md`](./docs/1password-platform-architecture.md) — draft target architecture for 1Password developer-platform adoption; not live provisioning policy -- [`docs/1password-platform-implementation-plan.md`](./docs/1password-platform-implementation-plan.md) — draft phased execution plan for the platform architecture (6 phases, per-step operator/agent + validation gates, Business/Teams tier-gating matrix); not live provisioning policy -- [`docs/ssh.md`](./docs/ssh.md) — SSH client policy -- [`docs/git-identity.md`](./docs/git-identity.md) — GitHub identity (authorship/transport/API): system-config is the read-only **consumer/enforcer** of meta-inventory's entity registry; the matrix is never copied into this public repo (Phase 2 gate mechanism + Phase 4 read-only `ng-doctor identity` checks; local hook activation complete — all four installed runtimes (Claude/Codex/Cursor/Copilot) wired + firing-verified) -- [`docs/agentic-hook-enforcement.md`](./docs/agentic-hook-enforcement.md) — per-runtime hook surfaces (Claude/Codex/Copilot/Cursor/Devin) for the git-identity gate; verified local state + adapter model + unresolved items -- [`docs/mercurial-cli.md`](./docs/mercurial-cli.md) — Mercurial (`hg`) baseline for agent repo operations; Homebrew install/update path and identity boundary -- [`docs/huggingface-cli.md`](./docs/huggingface-cli.md) — Hugging Face Hub (`hf`) baseline for agent Hub operations; Homebrew install/update path and auth boundary -- [`docs/huggingface-project-agent-skill-manual.md`](./docs/huggingface-project-agent-skill-manual.md) — project-local Hugging Face Agent Skill authoring manual -- [`docs/workstation-dns-resolution.md`](./docs/workstation-dns-resolution.md) — portable workstation DNS rule (no static `/etc/resolver` pin; resolve `home.arpa` via the active site resolver) -- [`docs/security-hardening-implementation-plan.md`](./docs/security-hardening-implementation-plan.md) — audited hardening backlog and implementation plan -- [`docs/device-admin/README.md`](./docs/device-admin/README.md) — Windows/Linux fleet administration (78 files): lifecycle phases, per-device classification in `current-status.yaml`, handoff/handback packets, and the DESKTOP-2JJ3187 SSH-lane RCA. Previously unindexed here despite being the largest subtree in `docs/` -- [`docs/host-capability-substrate/project-substrate-adoption.md`](./docs/host-capability-substrate/project-substrate-adoption.md) — transitional host-local project substrate admission policy -- [`docs/mcp-config.md`](./docs/mcp-config.md) — MCP framework (scope model, launch patterns, sync behavior) -- [`docs/github-mcp.md`](./docs/github-mcp.md) — GitHub MCP integration -- [`docs/cloudflare-mcp.md`](./docs/cloudflare-mcp.md) — Cloudflare MCP integration (OAuth, Codemode usage, conventions) -- [`docs/cloudflare-one-terminology.md`](./docs/cloudflare-one-terminology.md) — Cloudflare One terminology and dashboard ontology for current docs -- [`docs/google-admin-tooling.md`](./docs/google-admin-tooling.md) — local Google Cloud CLI + GAM state, Guardian auth shape, and reauth stop rules -- [`docs/hetzner-cli.md`](./docs/hetzner-cli.md) — Hetzner Cloud CLI install/update/auth boundary -- [`docs/orbstack-setup.md`](./docs/orbstack-setup.md) — OrbStack container-tool install/usage baseline and repo boundary (system-config does not manage OrbStack; the app self-manages its DynamicProfiles entry) -- [`docs/agentic-tooling.md`](./docs/agentic-tooling.md) — shell + tool contract -- [`docs/brand/terminal-style.md`](./docs/brand/terminal-style.md) — terminal styling brand slice (ANSI-semantic palette, glyph + ascii fallback, four output tiers); ships the `jr-style.sh` shell library (`home/dot_local/lib/`, deploys to `~/.local/lib/`), the `jefahnierocks-design` skill, and the `jefahnierocks-phosphor`/`-paper` iTerm2 presets -- [`docs/shell-config.md`](./docs/shell-config.md) — shell startup model, agentic mode, and 1Password/chezmoi verification notes -- [`docs/tailscale-cli.md`](./docs/tailscale-cli.md) — Tailscale CLI install shape, command surface, and agentic usage boundary -- [`docs/workspace-management.md`](./docs/workspace-management.md) — workspace POC -- [`docs/github-org-setup.md`](./docs/github-org-setup.md) — org-level GitHub config (teams, ruleset, CODEOWNERS) and reproduce recipe -- [`docs/claude-cli-setup.md`](./docs/claude-cli-setup.md), [`docs/codex-cli-setup.md`](./docs/codex-cli-setup.md), [`docs/copilot-cli-setup.md`](./docs/copilot-cli-setup.md), [`docs/claude-desktop-setup.md`](./docs/claude-desktop-setup.md) — per-tool setup -- [`docs/vscode-setup.md`](./docs/vscode-setup.md) — live VS Code user baseline, profiles/extensions, project contract, native MCP, validation, and HCS boundary -- [`docs/vscode-configuration-plan.md`](./docs/vscode-configuration-plan.md) — evidence-backed implementation roadmap and remaining rollout decisions -- [`docs/claude-macos-app.md`](./docs/claude-macos-app.md) — Claude macOS app configuration **ownership map + hazards** (decisions, not tutorial): the three config domains, bypass × deny-precedence (hook=conditional, deny-rule=unconditional floor, both survive bypass), the Cowork config-overwrite hazard, verified `PreToolUse` hook placement, the GitHub-MCP containment, and the managed-settings "managed-only" trap - -## Directory layout - -```text -system-config/ -├── home/ # Active chezmoi source -│ ├── .chezmoidata.yaml # Shared template data -│ ├── .chezmoiignore -│ ├── dot_zshenv.tmpl # XDG exports -│ ├── dot_zprofile.tmpl # PATH bootstrap -│ ├── dot_zshrc.tmpl # Thin loader → zshrc.d/ -│ ├── dot_bash_profile.tmpl -│ ├── dot_bashrc.tmpl -│ ├── dot_config/ -│ │ ├── zshrc.d/ # Modular zsh config (NG_MODE gated) -│ │ ├── direnv/ # direnvrc.tmpl + direnv.toml.tmpl -│ │ ├── mise/ # Global mise config -│ │ ├── mcp/ # op-backed MCP secrets manifest (common.env) -│ │ ├── 1Password/ # 1P SSH agent config -│ │ └── starship.toml.tmpl -│ ├── private_dot_ssh/ # SSH client policy (config, private_conf.d/, allowed_signers); private_ prefix preserves 0700 -│ ├── dot_local/bin/ # ng-doctor, system-update, agentic, workspace, -│ │ # workspace-doctor, MCP wrappers, git-identity-* adapters -│ └── dot_local/lib/ # jr-style.sh terminal styling library, git_identity_hooklib.py -│ # (→ ~/.local/lib/) -├── iterm2/ -│ ├── profiles/ # Dynamic profile JSONs -│ └── color-presets/ # Color-only presets (incl. jefahnierocks-phosphor/-paper) -├── scripts/ # sync-mcp.sh, system-update.sh, etc. -│ └── system-update.d/ # Drop-in update plugins -├── vscode/ # Managed user-setting keys + profile/extension manifests -├── .vscode/ # This repo's project settings, recommendations, tasks -├── docs/ # Authoritative reference docs -├── policies/ # Version policy -├── macos/ # Root-owned macOS config (sudo-deployed, not chezmoi) -│ └── claude-code/ # Claude Code managed-settings deny floor -├── AGENTS.md # This file -└── CLAUDE.md # Claude Code shim -``` - -## Shell policy - -- zsh is the only managed interactive shell. -- bash is a script/runtime shell only. -- fish is not part of the managed config surface. Do not add fish - templates, fish aliases, or fish-only agent workflows. -- Current startup and agentic-mode details live in - [`docs/shell-config.md`](./docs/shell-config.md). - -## Chezmoi - -- Source: `system-config/home/` -- Machine data: `~/.config/chezmoi/chezmoi.toml` -- Shared data: `home/.chezmoidata.yaml` -- Template rule: always use `| default` for optional keys - ```go - // CORRECT - {{ if not (.android | default false) -}} - // INCORRECT - {{ if not .android -}} - ``` -- Required machine data key: `android` (bool) — consumed by `dot_zshenv.tmpl`, - `dot_zprofile.tmpl`, `dot_bashrc.tmpl`, and `dot_config/mise/config.toml.tmpl`. - `headless` is still set in `~/.config/chezmoi/chezmoi.toml` but no template - reads it; treat it as vestigial, not required. - -## Common commands +## Commands ```bash -chezmoi apply --dry-run +scripts/validate-repo.sh # the only gate CI runs; run before every commit +chezmoi apply --dry-run --force # bare --dry-run PROMPTS, then dies: "could not open a new TTY" chezmoi apply - -ng-doctor -ng-doctor --summary - -system-update -system-update --check -system-update --list - -scripts/install-iterm2-profiles.sh - -scripts/jr-style-demo.sh # preview the terminal styling brand -tests/jr-style/run.sh # test the jr-style.sh library - -scripts/sync-mcp.sh -scripts/sync-mcp.sh --dry-run -scripts/codex-mcp-readiness.sh - -scripts/sync-vscode.sh --check -scripts/sync-vscode.sh --apply -ng-doctor vscode -scripts/validate-repo.sh +ng-doctor [--summary | android | vscode | mcp | identity] +system-update [--check | --list] +scripts/sync-mcp.sh --dry-run # then without --dry-run +scripts/sync-vscode.sh --check # then --apply ``` -## Secrets - -- 1Password account: `my.1password.com`; primary vault: `Dev` -- Retrieval: `op read "op://Dev//"` -- Readiness check: `op vault get Dev --account my.1password.com` -- Live policy: [`docs/secrets.md`](./docs/secrets.md) -- Never commit tokens, passphrases, or API keys. -- Never write secret values into persistent user config files. - -## MCP - -Global MCP servers synced by `scripts/sync-mcp.sh` (see -[`docs/mcp-config.md`](./docs/mcp-config.md) for the full inventory): - -- `context7`, `memory`, `sequential-thinking`, `brave-search`, `firecrawl` -- `runpod`, `runpod-docs`, `cloudflare-api`, `cloudflare-docs` -- `github` (host-aware rendering; see [`docs/github-mcp.md`](./docs/github-mcp.md)) — - **currently omitted from every rendered config** while the hard-disable marker - `~/.local/state/system-config/mcp-github.disabled` exists. It stays in the - managed-key set so stale entries are swept. Read this inventory as the sync - set, not as a drift report - -Sync targets: Claude Code CLI (`~/.claude.json`), Claude Desktop -(`~/Library/Application Support/Claude/claude_desktop_config.json`), -Cursor (`~/.cursor/mcp.json`), Devin Desktop -(`~/.codeium/windsurf/mcp_config.json`), Copilot CLI -(`~/.copilot/mcp-config.json`), Codex CLI (`~/.codex/config.toml`), VS Code -(`~/Library/Application Support/Code/User/mcp.json`). - -Policy: - -- Project-specific MCP servers belong in each host's project-native file; - VS Code uses `.vscode/mcp.json` with a top-level `servers` object. -- User configs must not contain expanded API keys or tokens. -- Bearer-token stdio servers use runtime wrappers in `home/dot_local/bin/` - that read from 1Password at launch. OAuth remotes such as Cloudflare use - per-client OAuth sessions instead. -- Claude Desktop's `claude_desktop_config.json` is synced too, but only - the `mcpServers` block — `globalShortcut`, `preferences`, and any - user-added servers outside the managed set are preserved. Its file - format is stdio-only, so HTTP remotes are wrapped via `mcp-remote`. -- Gemini CLI is currently unmanaged for MCP sync. - -Full framework: [`docs/mcp-config.md`](./docs/mcp-config.md). - -## System update - -Core steps: Homebrew index → formulae → npm globals → AI-CLI channels → pip → -Claude Code → gh extensions → mise runtimes → cleanup. Plugins in -`scripts/system-update.d/*.sh`. Logs in `~/Library/Logs/system-update/`. - -See [`docs/system-update-status-and-resilience-plan.md`](./docs/system-update-status-and-resilience-plan.md) -for the network-vs-real failure classification, AI-CLI platform-binary heal, and -live brand-conformant status model (all landed). §19 covers architecture -verification and the keg-only false positive. - -Read [`scripts/system-update.d/README.md`](./scripts/system-update.d/README.md) -before writing or editing a plugin — it is the plugin contract, and it was -previously unreachable from this index. The trap it now documents: **step -functions run in a subshell, so `add_notice`/`add_action_once` called from a -plugin are silently discarded.** Print a `^-: ` marker and -promote it in `collect_notices` instead. - -## MCP usage telemetry - -`scripts/mcp-usage-collector.sh` installs a LaunchAgent at -`~/Library/LaunchAgents/com.jefahnierocks.mcp-usage-collector.plist` whose -`ProgramArguments` point into this repo. It has run every 60s since 2026-04-24, -writing daily JSONL to `~/.local/state/system-config/mcp-usage-collector/` and -logs to `~/Library/Logs/system-config/`. **There is no retention policy and the -state directory has grown to 7.3 GB across 107 daily JSONL files** (`du -sk` = -7,603,080 KiB, 2026-08-08). It grows continuously; treat this as a dated reading, -not a constant. The plist itself has no chezmoi source and no drift check. See -[`docs/ai-tool-config-ownership.md`](./docs/ai-tool-config-ownership.md) §1.1. - -## Source of truth - -| Surface | Owner | Notes | -|---------|-------|-------| -| `system-config/home/` | system-config | chezmoi source for shell, direnv, mise, starship, SSH policy, MCP wrappers, MCP secrets manifest | -| `scripts/` | system-config | Operational tooling | -| `iterm2/profiles/` | system-config | Dynamic profile definitions | -| `vscode/` | system-config | Reviewed VS Code user-setting keys and extension/profile manifests | -| Project `.mise.toml`, `.envrc`, `.vscode/`, MCP files | project repo | Version pins, env vars, editor correctness, tasks, project MCP servers | - -## Definition of done - -- `scripts/validate-repo.sh` passes. This is the single gate CI runs - (`.github/workflows/repo-validation.yml`), so satisfying anything narrower - means a green workstation and a red PR. It covers: - - `shellcheck` — pinned to 0.11.0 via `.mise.toml`, over every `.sh` file plus - `home/dot_local/bin/executable_mcp-*.tmpl` and `executable_ng-doctor.tmpl` - - `jq` shape checks on the MCP and VS Code JSON manifests - - `tests/system-update/run.sh`, `tests/vscode/run.sh`, - `tests/mcp-vscode/run.sh`, and `scripts/policy-lint.sh` - - the repo-contract greps (no fish surface, no stale path references) -- Chezmoi templates use `| default` for optional keys -- `chezmoi apply --dry-run` completes without template errors -- Global MCP sync writes only structure, not secret material -- `scripts/sync-vscode.sh --check` is value-blind and apply is atomic/backed up - -## Boundaries - -### Always -- Use conventional commits: `type(scope): description` -- Sign commits using the approved Git signing configuration -- Run `scripts/validate-repo.sh` before committing — bare `shellcheck` is a - narrower check than CI, and an unpinned shellcheck reports different findings - than the pinned 0.11.0 the gate uses - -### Ask first -- Deleting or renaming files outside `scripts/` and `docs/` -- Modifying chezmoi templates that affect `~/.config/` -- Changes to `scripts/system-update.sh` core step logic - -### Never -- Commit secrets, tokens, API keys, or passphrases -- Modify `~/.config/chezmoi/chezmoi.toml` directly -- Run `chezmoi apply` without `--dry-run` first in unfamiliar contexts -- Create a global `~/.envrc` -- Reintroduce a fish-managed shell surface - -## Known issues - -- **Template error `map has no entry for key`** — template missing - `| default` guard or machine data missing the expected key. -- **macOS plist cache** — run `killall cfprefsd` before modifying - plist-backed preferences on disk. -- **Agentic zsh startup time** — profile with `zsh -xlic exit 2>&1` - before adding more shell init. - -## Ecosystem awareness — agentic-coding-lab - -`agentic-coding-lab` (`~/ai/agentic-coding-lab`) is a downstream `~/ai` project that depends on the -environment this repo provisions and follows `docs/project-conventions.md`. Two practical seams: - -- **Toolchain.** The lab pins quality gates against `uv` + Python 3.13 and drives `aider` against - the local `llm-gateway` (`:8811`). Changing the global mise defaults for these can break the - lab's pinned, gate-identical (local == pre-commit == CI) checks — coordinate version bumps. -- **State path.** The lab writes redacted runtime traces under - `~/.local/state/agentic-coding-lab/runs/` (XDG state). It keeps no durable store of its own; this - path is runtime evidence only. If host-local project-substrate admission applies (see - `docs/host-capability-substrate/project-substrate-adoption.md`), the lab is one such project. +`ng-doctor`, `system-update`, `agentic`, `workspace` are chezmoi templates in +`home/dot_local/bin/` deployed to `~/.local/bin`; they are not in `scripts/`. +For anything else: `ls scripts/`, then `--help`. + +## Hazards + +- **1Password service-account token.** `security add-generic-password -w` + silently truncates at 128 bytes and cannot store an SA token. Use + `scripts/store-op-sa-token.sh`. +- **`github` MCP is disabled deliberately.** It is omitted from every rendered + config while `~/.local/state/system-config/mcp-github.disabled` exists — that + is argv-exposure containment. "Server disconnected" is the intended state. + Never delete the marker. +- **system-update plugins run in a subshell.** `add_notice` / + `add_action_once` called from `scripts/system-update.d/*.sh` are silently + discarded. Print a `^-: ` marker and promote it in + `collect_notices`. Contract: `scripts/system-update.d/README.md`. +- **macOS plist cache.** Run `killall cfprefsd` before editing a plist-backed + preference on disk, or the daemon overwrites it with no error. +- **`home/private_dot_ssh/`.** The `private_` prefix is what preserves mode + 0700; renaming it silently widens permissions on the deployed `~/.ssh` tree. +- **`macos/` is root-owned** and sudo-deployed, not chezmoi-deployed. +- **`policies/host-capability-substrate/tiers.yaml` is byte-pinned** by a + sibling repo's vendored snapshot. Coordinated re-vendor only, never a + unilateral edit. +- **This repo is public.** Never copy identity values out of the entity + registry into it. +- **`~/.local/state/system-config/mcp-usage-collector/`** is append-only, + multi-GB, and has no retention policy; its LaunchAgent executes code from + this repo path with no chezmoi source and no drift check. Measure with + `du -sh`; do not trust a recorded figure. + +## Conventions specific to this repo + +- zsh is the only managed interactive shell; bash is script/runtime only. Do + not add fish templates, fish aliases, or fish-only workflows. +- Chezmoi runs with `missingkey=error`, so `{{ .k | default x }}` does NOT + rescue an absent key — the lookup fails before `default` is reached. + `android` is a REQUIRED machine-data key, so reading it bare or through + `| default` is correct. Use `{{ if hasKey . "k" }}` only for keys that may + genuinely be absent. +- 1Password: account `my.1password.com`, vault `Dev`; read with + `op read "op://Dev//"`. +- Secret values never reach a rendered config. Bearer-token stdio MCP servers + launch through wrappers in `home/dot_local/bin/` that read 1Password at + runtime; OAuth remotes use per-client sessions. +- Project-scoped MCP servers go in each host's project-native file, never in + the user-level baseline `scripts/sync-mcp.sh` manages. VS Code's project file + uses a top-level `servers` object. +- `.mise.toml`, `.envrc`, `.vscode/` and MCP files belonging to another project + are owned by that project. Do not edit them from here. + +## Ask first, and never + +Ask before editing chezmoi templates under `home/dot_config/`, changing +`scripts/system-update.sh` core step logic, or deleting/renaming outside +`scripts/` and `docs/`. + +Never run `chezmoi apply` before a dry run: it rewrites the live home directory +and is not transactional. Never edit `~/.config/chezmoi/chezmoi.toml` — its +`sourceDir` line is the only thing pointing chezmoi at this repo, and losing it +silently reactivates the retired `~/.local/share/chezmoi/` source. Never create +a global `~/.envrc`; direnv helpers belong in `~/.config/direnv/direnvrc`. + +## Done + +`scripts/validate-repo.sh` passes. Read that script for its step list rather +than any prose copy. Two things it will not tell you at the call site: it +shellchecks only git-**tracked** `*.sh` (untracked scratch files used to redden +CI), and shellcheck is pinned to 0.11.0 in `.mise.toml` — an unpinned local +shellcheck reports different findings than the gate, so a green workstation can +still ship a red PR. + +Not covered by the gate, still required: `chezmoi apply --dry-run --force` +renders cleanly, and MCP/VS Code sync writes structure only, never values. + +## Docs + +[`docs/README.md`](./docs/README.md) is the annotated index of every doc in +`docs/`. Read these before acting: + +- [`docs/ai-tool-config-ownership.md`](./docs/ai-tool-config-ownership.md) — read before editing any agent config path, in this repo or under `~` +- [`docs/secrets.md`](./docs/secrets.md) — live secret-handling policy +- [`docs/mcp-config.md`](./docs/mcp-config.md) — MCP scope model, launch patterns, sync behavior +- [`docs/git-identity.md`](./docs/git-identity.md) — authorship, signing, transport identity, and the commit gate +- [`docs/agentic-tooling.md`](./docs/agentic-tooling.md) — shell and tool contract +- [`docs/shell-config.md`](./docs/shell-config.md) — startup model and `NG_MODE` agentic gating +- [`docs/project-conventions.md`](./docs/project-conventions.md) — what downstream projects may rely on; link it from their `AGENTS.md` diff --git a/CLAUDE.md b/CLAUDE.md index 1200e80..8ca578e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,24 +3,26 @@ title: Claude Code Project Context category: reference component: ai-context status: active -version: 3.1.0 -last_updated: 2026-07-24 -tags: [ai-cli, configuration, zsh, mcp] +version: 4.0.0 +last_updated: 2026-08-12 +tags: [ai-cli, configuration, mcp] priority: medium --- @AGENTS.md -## Claude-Specific Notes +## Claude-specific notes -- Prefer specialized tools: `Read` over `cat`, `Grep` over `rg`, `Glob` over `find`, `Edit` over `sed` -- zsh is the only managed interactive shell in this repo. Do not add fish config, fish syntax, or fish-specific aliases here. -- Run `scripts/validate-repo.sh` before committing (the gate CI runs; it uses - the shellcheck pinned in `.mise.toml`) -- Global agents and commands are available at `~/.claude/` and `~/.codex/` where tool-native config expects them -- Chezmoi templates use Go syntax, not Jinja2 -- Project runtime and secret decisions belong in `.mise.toml` and `.envrc` -- Live secret-handling policy for this system is in `docs/secrets.md` -- Project MCP servers go in the host's project-native file (`.mcp.json` for - Claude Code; `.vscode/mcp.json` with a top-level `servers` object for VS - Code); `scripts/sync-mcp.sh` manages only the user-level global baseline +- The deny floor at `/Library/Application Support/ClaudeCode/managed-settings.json` + is root-owned and unconditional: it survives `bypassPermissions`, whereas a + `PreToolUse` hook is conditional. Its source of truth is `macos/claude-code/` + in this repo — edit there, then sudo-deploy; never edit the live file in + place. Read `docs/claude-macos-app.md` before changing any Claude config. +- Cowork rewrites `~/Library/Application Support/Claude/claude_desktop_config.json` + wholesale. Re-run `scripts/sync-mcp.sh` after using it. +- Claude Code's project-native MCP file is `.mcp.json` at a project root. A + project server written into `~/.claude.json` instead is swept by + `scripts/sync-mcp.sh`'s managed-key pass. +- Global agents, skills and commands live at `~/.claude/` (and `~/.codex/`) + where each tool natively looks for them. Do not re-create them under this + repo's `.claude/`. diff --git a/README.md b/README.md index 9819c90..581e53e 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ authorization and are invisible to every mechanism below. Enumeration there is legibility, not governance. 1. **chezmoi** — `home/` is the source tree; 78 managed targets under `$HOME`. - Go templates, not Jinja2. Optional keys must use `| default`. + Go templates, not Jinja2. Guard possibly absent map keys with `hasKey` before + reading them; `| default` cannot recover from `missingkey=error`. 2. **Repo scripts** — `scripts/sync-mcp.sh`, `scripts/sync-vscode.sh`, `scripts/install-iterm2-profiles.sh`, `scripts/mcp-usage-collector.sh`. These write files chezmoi does not own, usually because the host application @@ -96,7 +97,7 @@ vscode/ managed user-setting keys, profile and extension manifests macos/ root-owned configuration deployed by sudo, not chezmoi policies/ canonical live policy; vendored byte-identically by a sibling repo tests/ 10 suites (see validation caveat below) -docs/ 153 files; AGENTS.md is the authoritative index +docs/ 154 files; docs/README.md is the authoritative annotated index ``` ## Boundaries diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b529adb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,243 @@ +--- +title: system-config Documentation Index +category: reference +component: docs +status: active +version: 1.0.0 +last_updated: 2026-08-12 +tags: [docs, index, routing] +priority: high +--- + +# Documentation index + +Annotated router for everything in `docs/`. Each entry says what the document +is authoritative for and, where one exists, the trap it prevents. + +`AGENTS.md` in the repo root carries the commands, hazards and boundaries an +agent needs before its first write, and points here. This file is the complete +map; it does not restate the root contract. + +**Draft and dated material is grouped at the bottom and is not live policy.** + +--- + +## Ownership: who deploys which config path + +- [`ai-tool-config-ownership.md`](ai-tool-config-ownership.md) — **read before + editing any agent config path**, in this repo or under `~`. The four deploy + mechanisms and which one owns each path, the global-user config tree, the + project-scope model, the managed/root tier, the git-identity registration + gap, and the leftover-workflow register. §1.1 owns the MCP-usage-collector + LaunchAgent record; §1.2 owns the `chezmoi.toml` single-point-of-failure + analysis. Its own open finding: `~/.claude/CLAUDE.md` is unowned by any of + the four mechanisms, loads ahead of this repo's files, and cannot be checked + by this repo's CI. +- [`agentic-tooling.md`](agentic-tooling.md) — the shell and tool contract + agents operate under on this host. +- [`project-conventions.md`](project-conventions.md) — the compatibility + surface downstream projects may rely on (global mise defaults, direnv, + secret-read patterns). Link this from a downstream project's own `AGENTS.md` + instead of copying it. Changing the global mise baseline can break a pinned + downstream consumer — coordinate version bumps here. +- [`evidence/README.md`](evidence/README.md) — evidence records and their + retirement gates, including the measurement showing Claude Code does not + natively discover `AGENTS.md` (which is why the root `CLAUDE.md` bridge + exists and is CI-checked by `scripts/check-instruction-bridge.sh`). + +## Secrets and credentials + +- [`secrets.md`](secrets.md) — **live** secret-handling policy: 1Password + account, vault, retrieval shape, and what may never be written to disk. +- [`1password-service-account.md`](1password-service-account.md) — **live** + operator runbook for the `mcp-gui-readonly` service account, the + non-interactive `op` auth plane the GUI/headless MCP wrappers (brave-search, + firecrawl, runpod) are coded against. **Read the token-storage box before + rotating:** `security add-generic-password -w` silently truncates at 128 + bytes and cannot store a service-account token — the write appears to + succeed and the credential fails later. Use `scripts/store-op-sa-token.sh`. +- [`secret-records.md`](secret-records.md) — credential-record register: + identifier, owner, consumer, scope, rotation cadence, stop rules. Record + status lives here; no secret values. +- [`infisical-cli.md`](infisical-cli.md) — Infisical CLI baseline for project + runtime secret workflows: Homebrew install/update path and the flag + guardrails (validate flags against installed behavior, not documentation). + +## Identity, signing, and the commit gate + +- [`git-identity.md`](git-identity.md) — GitHub identity across authorship, + transport and API. system-config is the read-only **consumer/enforcer** of a + registry produced elsewhere. **The identity matrix is never copied into this + repo — it is public.** Covers the gate mechanism and the read-only + `ng-doctor identity` checks. +- [`agentic-hook-enforcement.md`](agentic-hook-enforcement.md) — per-runtime + hook surfaces (Claude, Codex, Copilot, Cursor, Devin) that carry the + git-identity gate, the adapter model, verified local state, and unresolved + items. The adapters ship dormant in this repo; activation is user-scope + wiring on a workstation, so a clone inherits none of it. + +## MCP + +- [`mcp-config.md`](mcp-config.md) — the MCP framework: scope model, launch + patterns, the managed server inventory, the seven sync targets, and sync + semantics. Three things that are not visible from a call site: Claude + Desktop's config is merged, not overwritten (only the `mcpServers` block is + managed, and `globalShortcut`/`preferences`/user-added servers survive); its + format is stdio-only, so HTTP remotes are wrapped via `mcp-remote`; and + Gemini CLI is deliberately not a sync target. +- [`github-mcp.md`](github-mcp.md) — GitHub MCP integration and host-aware + rendering. The server is omitted from every rendered config while + `~/.local/state/system-config/mcp-github.disabled` exists, yet stays in the + managed-key set so stale entries are still swept. +- [`incident-2026-05-08-mcp-bearer-argv-exposure.md`](incident-2026-05-08-mcp-bearer-argv-exposure.md) + — the incident that produced that containment. Read it before concluding the + disabled GitHub server is a bug. +- [`cloudflare-mcp.md`](cloudflare-mcp.md) — Cloudflare MCP integration: OAuth + session model, Codemode usage, conventions. Zone-scoped tokens fail the MCP + gateway; account- or user-level scope is required. +- [`claude-desktop-setup.md`](claude-desktop-setup.md) — Claude Desktop + configuration. + +Note for editors of the MCP manifests: `MCP_REMOTE_VERSION` is pinned in two +files and `scripts/validate-repo.sh` compares the literals — bump both +together. Do **not** extend that check to assert `~/.mcp-auth/mcp-remote-/` +exists; 0.1.38 ships an inlined `"0.1.37"` and correctly uses the 0.1.37 state +directory, so such a check would fail permanently. + +## Shell, terminal, and editors + +- [`shell-config.md`](shell-config.md) — startup model, module load order, and + `NG_MODE` agentic gating (which decides what an agent's shell actually + loads). Also the 1Password and chezmoi verification notes. +- [`ssh.md`](ssh.md) — SSH client policy (nonsecret): config layout, + `conf.d` drop-ins, allowed signers. +- [`brand/terminal-style.md`](brand/terminal-style.md) — terminal styling brand + slice: ANSI-semantic palette, glyph plus ASCII fallback, four output tiers. + Ships the `jr-style.sh` library, the `jefahnierocks-design` skill, and the + `jefahnierocks-phosphor` / `-paper` iTerm2 presets. +- [`iterm2-profile-redesign.md`](iterm2-profile-redesign.md) — the + authoritative iTerm2 profile architecture (Option 3 hybrid) and why iTerm2 is + an adapter layer, not a system boundary. +- [`iterm2-jefahnierocks-profiles.md`](iterm2-jefahnierocks-profiles.md), + [`iterm2-happy-patterns-profiles.md`](iterm2-happy-patterns-profiles.md), + [`iterm2-guardian-profiles.md`](iterm2-guardian-profiles.md) — per-scope + profile definitions. +- [`vscode-setup.md`](vscode-setup.md) — live VS Code user baseline, profiles + and extensions, the project contract, native MCP, validation, and the HCS + boundary. +- [`claude-cli-setup.md`](claude-cli-setup.md), + [`codex-cli-setup.md`](codex-cli-setup.md), + [`copilot-cli-setup.md`](copilot-cli-setup.md) — per-tool CLI setup. +- [`claude-code-cli-settings-official.md`](claude-code-cli-settings-official.md) + — snapshot of the official Claude Code settings reference, for comparison + against what this host actually sets. +- [`claude-macos-app.md`](claude-macos-app.md) — Claude macOS app ownership map + and hazards (decisions, not tutorial). The three config domains; the + bypass × deny precedence — **a `permissions.deny` rule is an unconditional + floor and a `PreToolUse` hook is conditional, and both survive + `bypassPermissions`**; the Cowork hazard, where Cowork rewrites + `claude_desktop_config.json` wholesale and discards managed MCP entries; + verified `PreToolUse` hook placement; the GitHub-MCP containment; and the + managed-settings "managed-only" trap. +- [`workspace-management.md`](workspace-management.md) — the `workspace` / + `workspace-doctor` proof of concept. Both binaries are installed; the design + is still experimental. + +## Host capability and per-tool baselines + +- [`android-development.md`](android-development.md) — Android host-capability + baseline: the official Android CLI, canonical SDK root, Studio/SDK inventory, + project-owned Gradle policy, and `ng-doctor android [project]`. +- [`homebrew-tap-trust.md`](homebrew-tap-trust.md) — Homebrew third-party tap + trust boundary: item-level trust policy, operator review workflow, and + **fail-closed updater behavior** (an update refuses rather than proceeding on + an untrusted tap). +- [`orbstack-setup.md`](orbstack-setup.md) — OrbStack install and usage + baseline, and the repo boundary: **system-config does not manage OrbStack**, + and the app self-manages its own iTerm2 DynamicProfiles entry. Do not "fix" + that unmanaged entry sitting beside the managed symlinks. +- [`google-admin-tooling.md`](google-admin-tooling.md) — local Google Cloud CLI + and GAM state, the Guardian auth shape, and the **reauth stop rules**. +- [`hetzner-cli.md`](hetzner-cli.md) — Hetzner Cloud CLI install, update, and + auth boundary. +- [`mercurial-cli.md`](mercurial-cli.md) — Mercurial (`hg`) baseline for agent + repo operations, plus the identity boundary. +- [`huggingface-cli.md`](huggingface-cli.md) — Hugging Face Hub (`hf`) baseline + and auth boundary. +- [`huggingface-project-agent-skill-manual.md`](huggingface-project-agent-skill-manual.md) + — authoring manual for project-local Hugging Face Agent Skills. +- [`terraform-cli-setup.md`](terraform-cli-setup.md), + [`vercel-cli-setup.md`](vercel-cli-setup.md), + [`sentry-cli-setup.md`](sentry-cli-setup.md) — per-CLI install and auth + baselines. +- [`github-org-setup.md`](github-org-setup.md) — org-level GitHub + configuration (teams, ruleset, CODEOWNERS) and the reproduce recipe. + +## Updates and maintenance + +- [`../scripts/system-update.d/README.md`](../scripts/system-update.d/README.md) + — **the plugin contract. Read before writing or editing a plugin.** Its + central trap: step functions run in a subshell, so `add_notice` / + `add_action_once` called from a plugin are *silently* discarded — no notice, + no error. Print a `^-: ` marker and promote it in + `collect_notices`. +- [`system-update-status-and-resilience-plan.md`](system-update-status-and-resilience-plan.md) + — the append-only design record for the updater: network-vs-real failure + classification, AI-CLI platform-binary heal, the status model, architecture + verification and the keg-only false positive, and the npm install-script + gate. Numbered sections are appended over time; search by topic rather than + citing a section number. + +## Networking and fleet + +- [`workstation-dns-resolution.md`](workstation-dns-resolution.md) — the + portable workstation DNS rule: **no static `/etc/resolver` pin**; resolve + `home.arpa` through the active site resolver. +- [`tailscale-cli.md`](tailscale-cli.md) — Tailscale CLI install shape, command + surface, and agentic usage boundary. See the open question in + `docs/device-admin/` about the Cloudflare-mesh migration's effect on this. +- [`cloudflare-one-terminology.md`](cloudflare-one-terminology.md) — + Cloudflare One terminology and dashboard ontology, for writing current docs. +- [`device-admin/README.md`](device-admin/README.md) — Windows and Linux fleet + administration, the largest subtree here: lifecycle phases, per-device + classification in `current-status.yaml`, handoff/handback packets, and the + DESKTOP-2JJ3187 SSH-lane RCA (drop-in ACL inheritance, not sshd config). + +## Host capability substrate (HCS) + +- [`host-capability-substrate/implementation-charter.md`](host-capability-substrate/implementation-charter.md) + — the charter and scope split with the sibling HCS repo. +- [`host-capability-substrate/0001-repo-boundary-decision.md`](host-capability-substrate/0001-repo-boundary-decision.md) + — the repo boundary and scaffolding decision. +- [`host-capability-substrate/project-substrate-adoption.md`](host-capability-substrate/project-substrate-adoption.md) + — host-local project substrate admission policy. +- [`host-capability-substrate/tooling-surface-matrix.md`](host-capability-substrate/tooling-surface-matrix.md) + — the tooling surface matrix. +- Dated operational records in the same directory + (`2026-04-*`, `2026-05-*`, `2026-07-*`) are evidence, not policy. + +`policies/host-capability-substrate/tiers.yaml` is the live policy artifact +these documents describe. It is vendored byte-identically by the sibling repo +and is sha-pinned there, so a byte change de-synchronizes that snapshot until a +coordinated re-vendor. Never edit it unilaterally. + +## Drafts, plans, and dated snapshots — not live policy + +- [`1password-platform-architecture.md`](1password-platform-architecture.md) + and + [`1password-platform-implementation-plan.md`](1password-platform-implementation-plan.md) + — draft target architecture and phased execution plan for 1Password + developer-platform adoption. Design only; do not implement as policy. The + live operational surface is `1password-service-account.md` above. +- [`security-hardening-implementation-plan.md`](security-hardening-implementation-plan.md) + — audited hardening backlog. Work not yet done. +- [`vscode-configuration-plan.md`](vscode-configuration-plan.md) — VS Code + rollout roadmap and undecided items. Live state is `vscode-setup.md`. +- [`host-capability-substrate-research-plan.md`](host-capability-substrate-research-plan.md) + — HCS research plan. +- [`restart-handoff-2026-08-08.md`](restart-handoff-2026-08-08.md) — dated + restart snapshot: verified Git and host state, updater evidence, remaining + work. Accurate as of its date only; the documents above supersede it wherever + they disagree. +- [`secure-control-plane-host-evidence-2026-05-28.md`](secure-control-plane-host-evidence-2026-05-28.md) + — dated host-side current-state evidence for the secure control plane.