diff --git a/AGENTS.md b/AGENTS.md index a31b734..8dea936 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,11 +9,12 @@ and the user-level MCP baseline. 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 record is tracked here with status `planned` +- [`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 @@ -209,12 +210,9 @@ promote it in `collect_notices` instead. `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 6.3 GB across 95 daily JSONL files** (`du -shk`, -2026-07-27; this figure supersedes the "roughly 13 GB" previously stated here and -the "roughly 6.7 GB" previously stated in the ownership map — the same directory -was carrying two different numbers). It grows continuously; treat it as a dated -reading, not a constant. The plist itself has no chezmoi source and no drift -check. See +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 diff --git a/docs/ai-tool-config-ownership.md b/docs/ai-tool-config-ownership.md index 2b71986..1d0cc34 100644 --- a/docs/ai-tool-config-ownership.md +++ b/docs/ai-tool-config-ownership.md @@ -3,8 +3,8 @@ title: AI Tool Configuration Ownership Map category: reference component: ai_tool_config_ownership status: active -version: 1.3.1 -last_updated: 2026-08-01 +version: 1.3.2 +last_updated: 2026-08-08 tags: [claude, codex, cursor, devin, copilot, gemini, aider, mcp, chezmoi, ownership, managed-settings, git-identity] priority: high --- @@ -96,11 +96,11 @@ currently overstate. A fifth mechanism exists and is undocumented elsewhere in this repo: **`scripts/mcp-usage-collector.sh`** writes `~/Library/LaunchAgents/com.jefahnierocks.mcp-usage-collector.plist`, whose -`ProgramArguments` point into this repo, and has produced **6.3 GB** of MCP -telemetry across **95 daily JSONL files** under +`ProgramArguments` point into this repo, and has produced **7.3 GB** of MCP +telemetry across **107 daily JSONL files** under `~/.local/state/system-config/mcp-usage-collector/` since 2026-04-24 — -re-measured by `du -shk` on 2026-07-27 **[V]**. It is repo-deployed by mechanism, -unowned by documentation. See §7. +re-measured as **7.3 GB** (`du -sk` = 7,603,080 KiB) on 2026-08-08 +**[V]**. It is repo-deployed by mechanism, unowned by documentation. See §7. > Earlier revisions of this document carried **two different figures for the same > directory** — "roughly 6.7 GB" here and "~13 GB" in §2.3 — and `AGENTS.md` @@ -229,7 +229,7 @@ tooling prunes **[A]**: | --- | --- | --- | | `~/.codex/` (`logs_2.sqlite` 1.0 GB + WAL 386 MB, 1741 session transcripts) | ~7.3 GB | runtime | | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` (+ backup) | ~4.1 GB | runtime, secret-bearing | -| `~/.local/state/system-config/mcp-usage-collector/` (95 daily JSONL) | 6.3 GB **[V, 2026-07-27]** | runtime, repo-produced | +| `~/.local/state/system-config/mcp-usage-collector/` (107 daily JSONL) | 7.3 GB **[V, 2026-08-08]** | runtime, repo-produced | | `~/.local/share/claude/versions/` (4 retained binaries) | ~1.0 GB | runtime | | `~/Library/Caches/claude-cli-nodejs/` | 346 MB | runtime | diff --git a/docs/restart-handoff-2026-08-08.md b/docs/restart-handoff-2026-08-08.md new file mode 100644 index 0000000..b00b211 --- /dev/null +++ b/docs/restart-handoff-2026-08-08.md @@ -0,0 +1,172 @@ +--- +title: "Restart Handoff — 2026-08-08" +category: handoff +component: workstation +status: active +version: 1.0.0 +last_updated: 2026-08-08 +tags: [restart, handoff, system-update, git, identity, mcp, vscode] +priority: high +--- + +# Restart Handoff — 2026-08-08 + +This is a dated, evidence-backed orientation snapshot for the next agent after a +workstation restart. It records direct local and GitHub observations from +2026-08-08 in America/Anchorage. It does not supersede `AGENTS.md` or any +task-specific authority linked there. Re-run the commands below before acting on +mutable state. + +## Start here + +1. Read [`../AGENTS.md`](../AGENTS.md) in full. +2. Read this handoff and establish exact Git/PR state with the commands below. +3. Follow the task-specific authority from `AGENTS.md`; do not treat this dated + snapshot as permission to modify another repository or operator-owned state. + +```bash +pwd +git status -sb +git log -3 --show-signature --oneline +git rev-list --left-right --count HEAD...@{upstream} +gh pr view 61 --json number,title,state,isDraft,url,headRefName,baseRefName,statusCheckRollup +``` + +## Durable Git and publication state + +Verified at 2026-08-08 09:31 AKDT, before this handoff document was added: + +- Work branch: `agent/system-update-restart-readiness`. +- Draft PR: [#61](https://github.com/jefahnierocks/system-config/pull/61), + targeting `main` from that branch. +- The branch was clean and matched its upstream (`0 0`). +- It was two signed commits ahead of `origin/main`: + - `1ff05e6` — `fix(system-update): report and resolve updates truthfully` + - `33852e4` — `docs: refresh verified host state` +- `origin/main` was `bdec977`; PR #61 was open and its validation jobs had + started. GitHub status is mutable, so re-read it rather than assuming the PR + was merged or its checks remained green. + +This handoff and its `AGENTS.md` index entry follow those two commits on the same +branch. The closeout gate is a clean worktree, `HEAD...@{upstream}` equal to +`0 0`, valid signatures, and a successful PR validation check. + +## Updater incident and verified repair + +Full update run `20260808-080519` completed every step except stable npm globals. +The stable lane passed exact `wrangler@4.120.0` with `--min-release-age 2`; that +release was too new, so npm returned `ETARGET` rather than resolving backward. + +The repair keeps the exact inventory target for logs and comparison, but uses +`@latest` as the install spec while the stable cooldown is active. The +install-script identity gate remains strict and owner-scoped. The AI channel +lane is unchanged. The authoritative incident and implementation record is +[`system-update-status-and-resilience-plan.md`](./system-update-status-and-resilience-plan.md) +§§23–24. + +Verified evidence: + +- Focused updater suite: 156 passed, 0 failed. +- Targeted live update `20260808-092739`: zero failures; Wrangler advanced from + `4.118.0` to eligible `4.119.0`; requested `4.120.0` remained held and was + disclosed by `npm-global-resolved-drift:`. +- Installed readback: `wrangler --version` and `npm ls -g --depth=0 wrangler` + both reported `4.119.0`. +- Full read-only check `20260808-093230`: all steps successful in 36 seconds; + Homebrew current; all three AI channels current with exact platform binaries; + Claude Code and gh extensions current. +- The check inventory still reported Wrangler `4.120.0` and mise-managed uv + `0.12.3` as newer. This is inventory, not proof of a failed update. Re-run + `system-update --check` before deciding either item still needs work. +- Full `scripts/validate-repo.sh`: PASS. +- `chezmoi apply --dry-run --force`: exit 0. + +Relevant logs: + +```text +~/Library/Logs/system-update/run-20260808-080519.log +~/Library/Logs/system-update/run-20260808-092739.log +~/Library/Logs/system-update/run-20260808-093230.log +``` + +## Verified host state + +- `ng-doctor` before commit closeout reported 71 passed, 3 failed, 20 skipped. + One failure was this repo's then-dirty worktree and should disappear when the + closeout gate is clean. The two identity failures below are external scope. +- The GitHub MCP disable marker exists and containment passes; no managed host + exposes GitHub MCP while it exists. +- The GUI/headless MCP service-account keychain item is present; all six MCP + doctor checks passed. The current secret-record status is `active`. +- The MCP usage collector LaunchAgent is registered, interval-driven, and last + exited 0. Its state directory measured 7,603,080 KiB (7.3 GB) across 107 daily + JSONL files. No retention policy or chezmoi-owned plist source exists. +- No updater process or `system-update.lock` was active during the initial + restart audit. Recheck after restart rather than relying on that dated fact. + +## Remaining work with current evidence + +### 1. External Git identity drift — do not repair from this repo + +`ng-doctor identity` reports 7 passed and 2 failed. The failures overlap across +three real clones: + +| Clone path | Verified live fact | +| --- | --- | +| `/Users/verlyn13/Organizations/the-nash-group-github-profile` | Origin uses `github.com`, while the registry expects `github.com-nash-group`; local status already had 3 entries. The doctor prints this same clone as `parent-org/.github` in the remote check and `The-Nash-Group/.github` in the config-drift check. | +| `/Users/verlyn13/Organizations/happy-patterns/apps/happy-patterns-org.github.io` | Clean worktree; origin uses `github.com`, while the registry expects `github.com-happy-patterns`; email/signing/host configuration also differs from the registered lane. | +| `/Users/verlyn13/Organizations/happy-patterns/apps/scopecam` | Clean worktree on `development`; origin uses `github.com`, while the registry expects `github.com-happy-patterns`; email/signing/host configuration also differs from the registered lane. | + +The registry gates themselves pass: schema version 1, internal visibility, and +zero unresolved repositories. These clone changes belong to their repositories +and identity owner. Preserve the Nash clone's existing local changes. + +### 2. MCP telemetry retention and ownership + +The collector grows continuously and has no retention policy. Treat deletion, +compression, LaunchAgent ownership, and migration as unapproved design work +until the operator selects a policy. The ownership authority and full leftover +register are [`ai-tool-config-ownership.md`](./ai-tool-config-ownership.md) +§§1.1, 7, and 8. + +### 3. Operator-gated editor and posture work + +`ng-doctor` skipped 20 checks rather than proving them complete. Current examples +include the staged VS Code profiles, 12 extension retirement candidates, and +the phased macOS/network posture checks. Use +[`vscode-configuration-plan.md`](./vscode-configuration-plan.md) and +[`security-hardening-implementation-plan.md`](./security-hardening-implementation-plan.md) +as planning authorities; re-verify live state and obtain the named operator +decisions before mutation. + +## Do not infer + +- A draft PR is not merged authority. +- A successful targeted npm update is not a substitute for re-reading the latest + full check before a future update. +- An `outdated` inventory entry under a release-age policy is not automatically + a defect or permission to bypass the hold. +- Doctor `skipped` means unverified or deferred, not passed. +- External clone drift is evidence, not authorization for cross-repo edits. +- Never expose secret values while validating the MCP service-account plane. + +## Fresh-agent restart commands + +These are read-only except the validation suite's temporary fixtures: + +```bash +cd /Users/verlyn13/Organizations/jefahnierocks/system-config +git status -sb +git fetch --prune origin +git rev-list --left-right --count HEAD...@{upstream} +git log -3 --show-signature --oneline +gh pr view 61 --json state,isDraft,url,statusCheckRollup +system-update --check +ng-doctor +scripts/validate-repo.sh +chezmoi apply --dry-run --force +``` + +Do not run `chezmoi apply`, merge PR #61, modify the three external clones, or +delete telemetry solely because this handoff exists. Each requires its normal +authority and a current readback. diff --git a/docs/system-update-status-and-resilience-plan.md b/docs/system-update-status-and-resilience-plan.md index 029f605..72cac71 100644 --- a/docs/system-update-status-and-resilience-plan.md +++ b/docs/system-update-status-and-resilience-plan.md @@ -3,8 +3,8 @@ title: "System-Update — Resilience & Status Plan" category: reference component: system_update status: active -version: 1.13.0 -last_updated: 2026-07-29 +version: 1.15.0 +last_updated: 2026-08-08 tags: [system-update, brand, terminal-style, jr-style, resilience, network, status, ux, npm, mise, concerns, homebrew, 1password, architecture, android-studio] priority: high --- @@ -1481,6 +1481,48 @@ CLI invoked daily, "runtime" means every invocation. It also misses long-dwell compromise — event-stream hid for ~72 days — and a 2-day cooldown delays this host's own `npm audit fix` by 2 days. +## 23. 2026-08-05 AI-channel check-summary truthfulness + +Read-only run `20260805-083114` resolved all three configured dist-tags +successfully and rendered `available 3`. That count was not an update inventory: +Gemini's installed version equalled its target, while Codex and Copilot were the +only mismatches. The summary was counting `ai-cli-channel-ok:`, whose contract is +"the registry lookup and channel health checks succeeded," not "an update is +available." + +Check mode now compares each resolved target with the package manifest under the +configured npm prefix and emits one of two additive markers: + +- `ai-cli-channel-current:` — installed and target versions are equal. +- `ai-cli-channel-outdated:` — installed is absent or differs from the target. + +The result row derives its update count only from the second marker and reports +the current count separately. Update mode retains `ai-cli-channel-ok:` as its +successful-install count, so the correction does not reinterpret update runs or +change channel installation behavior. The test harness reproduces the incident's +one-current/two-outdated shape and requires `2 outdated, current 1`. + +## 24. 2026-08-08 stable-global cooldown resolution + +Update run `20260808-080519` asked for `wrangler@4.120.0` under the stable +global lane's two-day release-age policy. That release was published on +2026-08-07 and was inside the cooldown, so npm rejected the exact version with +`ETARGET`: an exact ineligible target cannot resolve backward. Wrangler remained +at `4.118.0` and the run ended with one failure. + +A read-only reproduction separated the policy from the target shape: + +- exact `wrangler@4.120.0` plus `--min-release-age 2` reproduced `ETARGET`; +- `wrangler@latest` with the same gate resolved eligible `4.119.0`, published + 2026-08-05, and completed its dry run successfully. + +The stable update step now retains the exact `npm outdated` target for log and +post-install comparison, but passes `@latest` to npm whenever a positive +cooldown is active. npm can therefore select the newest eligible release, while +`npm-global-resolved-drift:` still discloses that the inventory requested a newer +version. With no cooldown, the exact inventory target remains the install spec. +The AI channel lane is unchanged. + ## Related - [`docs/brand/terminal-style.md`](./brand/terminal-style.md) — the four-tier brand spec (authority) diff --git a/scripts/system-update.sh b/scripts/system-update.sh index 00928f7..13ba252 100755 --- a/scripts/system-update.sh +++ b/scripts/system-update.sh @@ -77,8 +77,8 @@ declare -a SYSTEM_UPDATE_GO_TOOLS=() declare -a SYSTEM_UPDATE_NPM_GLOBAL_ALLOW_SCRIPTS=() declare -a SYSTEM_UPDATE_AI_CLI_NPM_CHANNELS=() declare -a SYSTEM_UPDATE_AI_CLI_NPM_ALLOW_SCRIPTS=() -# Tighten the allow-scripts grain: reject identity-only entries and require an -# exact version on every grant in both npm lanes. Default false — see +# Optional tightening switch: reject identity-only entries and require an exact +# version on every grant in both npm lanes. Default false — see # docs/system-update-status-and-resilience-plan.md §22. SYSTEM_UPDATE_NPM_ALLOW_SCRIPTS_REQUIRE_PIN="${SYSTEM_UPDATE_NPM_ALLOW_SCRIPTS_REQUIRE_PIN:-false}" @@ -1507,16 +1507,30 @@ summarize_step() { ;; "AI CLI channels") - local ok retryable failed label healed heal_failed platform_missing + local ok retryable failed healed heal_failed platform_missing current outdated ok="$(grep -c '^ai-cli-channel-ok:' "$tmp" 2>/dev/null || true)" retryable="$(grep -c '^ai-cli-channel-retryable:' "$tmp" 2>/dev/null || true)" failed="$(grep -c '^ai-cli-channel-failed:' "$tmp" 2>/dev/null || true)" healed="$(grep -c '^ai-cli-channel-healed:' "$tmp" 2>/dev/null || true)" heal_failed="$(grep -c '^ai-cli-channel-heal-failed:' "$tmp" 2>/dev/null || true)" platform_missing="$(grep -c '^ai-cli-channel-platform-missing:' "$tmp" 2>/dev/null || true)" - [[ "$MODE" == "check" ]] && label="available" || label="updated" local parts=() - [[ "$ok" -gt 0 ]] && parts+=("${label} ${ok}") + if [[ "$MODE" == "check" ]]; then + current="$(grep -c '^ai-cli-channel-current:' "$tmp" 2>/dev/null || true)" + outdated="$(grep -c '^ai-cli-channel-outdated:' "$tmp" 2>/dev/null || true)" + if [[ "$outdated" -gt 0 ]]; then + parts+=("${outdated} outdated") + [[ "$current" -gt 0 ]] && parts+=("current ${current}") + elif [[ "$current" -gt 0 ]]; then + parts+=("all current (${current})") + elif [[ "$ok" -gt 0 ]]; then + # Backstop for a partial/older transcript with successful registry + # probes but no comparable installed-version evidence. + parts+=("checked ${ok}") + fi + else + [[ "$ok" -gt 0 ]] && parts+=("updated ${ok}") + fi [[ "$healed" -gt 0 ]] && parts+=("healed ${healed}") [[ "$platform_missing" -gt 0 ]] && parts+=("platform-missing ${platform_missing}") [[ "$retryable" -gt 0 ]] && parts+=("retryable ${retryable}") @@ -2426,12 +2440,22 @@ step_npm_globals() { return 0 fi - local target package failures=0 + local target package install_target cooldown_days failures=0 + cooldown_days="$(npm_release_age_days "${SYSTEM_UPDATE_NPM_GLOBAL_MIN_RELEASE_AGE:-}")" echo "npm-major: $(npm_major_version)" for target in "${targets[@]}"; do package="${target%@*}" + install_target="$target" + # An exact version published inside --min-release-age is ineligible, so + # npm rejects it with ETARGET instead of searching backward. Keep the + # exact inventory target for reporting, but give npm a resolvable tag + # whenever the stable lane's cooldown is active. npm can then select the + # newest release old enough to satisfy the policy. + if [[ -n "$cooldown_days" ]]; then + install_target="${package}@latest" + fi echo "updating npm global: ${target}" - if npm_global_npm_install "$package" "$target" 2>&1; then + if npm_global_npm_install "$package" "$install_target" 2>&1; then echo "npm-global-updated: ${target}" npm_global_report_resolved "$package" "$target" else @@ -2448,11 +2472,12 @@ step_npm_globals() { # Compare what the inventory asked for against what actually landed. # -# A cooldown resolves BACKWARD when the newest version sits inside the window: -# npm installs an older version, prints no warning, and exits 0. Without this -# the row reads `updated wrangler@4.118.0` for a run that installed 4.117.0. -# The requested version comes from the `npm outdated` inventory the step already -# holds, so this costs no extra registry round-trip. +# A cooldown resolves BACKWARD when the install spec remains resolvable (the +# stable lane uses `@latest` while the cooldown is active): npm installs an older +# eligible version, prints no warning, and exits 0. Without this the row reads +# `updated wrangler@4.118.0` for a run that installed 4.117.0. The requested +# version comes from the `npm outdated` inventory the step already holds, so this +# costs no extra registry round-trip. npm_global_report_resolved() { local package="$1" target="$2" want got want="${target##*@}" @@ -2592,13 +2617,13 @@ npm_allow_scripts_identity_entries() { } # Return a comma-separated allowlist for one stable global npm package. Rules -# use "owner-package=dependency[@version],...". Duplicate or malformed ownership -# records fail closed before npm mutates the global tree. +# use "owner-package=dependency[,dependency@exact-version,...]". Duplicate or +# malformed ownership records fail closed before npm mutates the global tree. npm_global_allow_scripts_for_package() { local package="$1" rule owner entries bad match="" for rule in "${SYSTEM_UPDATE_NPM_GLOBAL_ALLOW_SCRIPTS[@]}"; do if [[ "$rule" != *=* ]]; then - echo "invalid npm global allow-scripts rule: ${rule} (expected owner=dependency@version,...)" >&2 + echo "invalid npm global allow-scripts rule: ${rule} (expected owner=dependency[,dependency@exact-version,...])" >&2 return 2 fi owner="${rule%%=*}" @@ -2623,8 +2648,9 @@ npm_global_allow_scripts_for_package() { } # Stable global installs use the same strict preflight as the dedicated AI -# channels. A new install script therefore requires an exact, owner-scoped -# review record instead of landing silently or running by default. +# channels. A new script-bearing identity therefore requires an owner-scoped +# grant (optionally pinned to an exact version) instead of landing silently or +# running by default. npm_global_npm_install() { local package="$1" target="$2" allow_scripts identity local args=(install -g --strict-allow-scripts) @@ -2873,15 +2899,15 @@ sweep_npm_staging() { [[ $swept -eq 0 ]] || echo "swept ${swept} stale npm staging artifact(s)" } -# Return a comma-separated, exact-version allowlist for one channel package. -# Rules use "owner-package=dependency@version,..." so Bash 3.2 can consume the -# managed config without associative arrays. Duplicate or malformed ownership -# records fail closed. +# Return a comma-separated allowlist for one channel package. Rules use +# "owner-package=dependency[,dependency@exact-version,...]" so Bash 3.2 can +# consume the managed config without associative arrays. Duplicate or malformed +# ownership records fail closed. ai_cli_allow_scripts_for_package() { local package="$1" rule owner entries bad match="" for rule in "${SYSTEM_UPDATE_AI_CLI_NPM_ALLOW_SCRIPTS[@]}"; do if [[ "$rule" != *=* ]]; then - echo "invalid AI CLI npm allow-scripts rule: ${rule} (expected owner=dependency@version,...)" >&2 + echo "invalid AI CLI npm allow-scripts rule: ${rule} (expected owner=dependency[,dependency@exact-version,...])" >&2 return 2 fi owner="${rule%%=*}" @@ -2905,9 +2931,9 @@ ai_cli_allow_scripts_for_package() { printf '%s' "$match" } -# All channel installs use npm's strict preflight. A newly introduced install -# script therefore stops before mutation unless its exact package version was -# reviewed and recorded for the owning channel package. +# All channel installs use npm's strict preflight. A newly introduced +# script-bearing identity therefore stops before mutation unless an identity or +# exact-version grant was recorded for the owning channel package. ai_cli_npm_install() { local package="$1" version="$2" allow_scripts identity local args=(install -g --strict-allow-scripts) @@ -2975,13 +3001,19 @@ step_ai_cli_channels() { fi if [[ "$MODE" == "check" ]]; then - local view_tmp + local view_tmp installed_version="" view_tmp="$(mktemp "${TMPDIR:-/tmp}/tmp.system-update.XXXXXX")" target="" if npm view "${package}@${tag}" version >"$view_tmp" 2>&1; then cat "$view_tmp" target="$(tail -1 "$view_tmp")" echo "${package}@${tag} target ${target}" + installed_version="$(npm_installed_global_version "$package" 2>/dev/null || true)" + if [[ -n "$installed_version" && "$installed_version" == "$target" ]]; then + echo "ai-cli-channel-current: ${spec} ${installed_version}" + else + echo "ai-cli-channel-outdated: ${spec} installed ${installed_version:-missing} target ${target}" + fi echo "ai-cli-channel-ok: ${spec}" elif ai_cli_npm_retryable_failure "$view_tmp"; then cat "$view_tmp" diff --git a/tests/system-update/run.sh b/tests/system-update/run.sh index 1191f10..9c59045 100755 --- a/tests/system-update/run.sh +++ b/tests/system-update/run.sh @@ -402,6 +402,54 @@ else not_ok "AI CLI version marker changed the ok-count or the step classification" fi +# Check mode must compare the resolved channel target with the locally installed +# parent version. The old code emitted `ai-cli-channel-ok:` for every successful +# registry lookup and the summary rendered that count as `available`, so one +# current channel plus two stale channels falsely became "available 3". +ai_check_version_log="$TMP_ROOT/ai-check-version.log" +( + MODE=check + SYSTEM_UPDATE_AI_CLI_NPM_CHANNELS=( + "@google/gemini-cli@nightly" + "@openai/codex@alpha" + "@github/copilot@prerelease" + ) + # shellcheck disable=SC2329 # invoked indirectly by step_ai_cli_channels + npm() { + if [[ "${1:-}" == "view" ]]; then + case "${2:-}" in + "@google/gemini-cli@nightly") printf '%s\n' "0.55.0-nightly.1" ;; + "@openai/codex@alpha") printf '%s\n' "0.147.0-alpha.10" ;; + "@github/copilot@prerelease") printf '%s\n' "1.0.79-2" ;; + *) return 1 ;; + esac + return 0 + fi + [[ "${1:-}" == "-g" && "${2:-}" == "ls" ]] && return 0 + return 1 + } + # shellcheck disable=SC2329 # invoked indirectly by step_ai_cli_channels + npm_installed_global_version() { + case "${1:-}" in + "@google/gemini-cli") printf '%s\n' "0.55.0-nightly.1" ;; + "@openai/codex") printf '%s\n' "0.147.0-alpha.6.4" ;; + "@github/copilot") printf '%s\n' "1.0.79-1" ;; + *) return 1 ;; + esac + } + # shellcheck disable=SC2329 # invoked indirectly by step_ai_cli_channels + ai_cli_command_healthy() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_ai_cli_channels + ai_cli_platform_dep_ok() { return 0; } + step_ai_cli_channels +) >"$ai_check_version_log" +if [[ "$(grep -c '^ai-cli-channel-current:' "$ai_check_version_log")" == "1" \ + && "$(grep -c '^ai-cli-channel-outdated:' "$ai_check_version_log")" == "2" ]]; then + ok "AI CLI check mode distinguishes current channels from actual updates" +else + not_ok "AI CLI check mode still counts successful registry probes as updates" +fi + # Absent/uninstalled packages must fail cleanly so the caller simply omits the # marker rather than emitting an empty or partial line. if ! ai_cli_installed_version "@definitely/not-installed-$$" >/dev/null 2>&1; then @@ -1029,6 +1077,11 @@ if [[ "$(summarize_step "Homebrew formulae" "$brew_mixed_check_log" 0)" == "1 ou else not_ok "Homebrew check summary counted brew-doctor findings as outdated formulae" fi +if [[ "$(summarize_step "AI CLI channels" "$ai_check_version_log" 0)" == "2 outdated, current 1" ]]; then + ok "AI CLI check summary reports only version mismatches as outdated" +else + not_ok "AI CLI check summary still labels every successful registry probe as available" +fi # shellcheck disable=SC2034 # consumed by sourced summarize_step MODE=update if [[ "$(summarize_step "Homebrew formulae" "$brew_update_log" 0)" == "upgraded 9 formulae" ]]; then @@ -1225,6 +1278,68 @@ else not_ok "the cooldown leaked into the AI lane by default, was ignored when set, or missed stable globals" fi +# The inventory names an exact target, but an exact version inside the cooldown +# is ineligible and npm rejects it with ETARGET. The stable update step must keep +# that exact target for truth-in-reporting while installing through @latest so +# npm can select the newest eligible version. With no cooldown, retain the exact +# inventory target. +cooldown_resolution_log="$TMP_ROOT/npm-cooldown-resolution.log" +( + MODE=update + SYSTEM_UPDATE_NPM_GLOBAL_MIN_RELEASE_AGE=2 + SYSTEM_UPDATE_AI_CLI_NPM_CHANNELS=() + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_outdated_json() { + printf '%s\n' '{"wrangler":{"current":"4.118.0","wanted":"4.120.0","latest":"4.120.0"}}' + } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_channel_packages() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_report_skipped_channels() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + sweep_npm_staging() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_major_version() { printf '%s\n' 11; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_npm_install() { printf 'install-call: %s %s\n' "$1" "$2"; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_report_resolved() { printf 'resolved-call: %s %s\n' "$1" "$2"; } + step_npm_globals +) >"$cooldown_resolution_log" + +exact_resolution_log="$TMP_ROOT/npm-exact-resolution.log" +( + MODE=update + # shellcheck disable=SC2034 # read by step_npm_globals from sourced code + SYSTEM_UPDATE_NPM_GLOBAL_MIN_RELEASE_AGE=0 + SYSTEM_UPDATE_AI_CLI_NPM_CHANNELS=() + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_outdated_json() { + printf '%s\n' '{"wrangler":{"current":"4.118.0","wanted":"4.120.0","latest":"4.120.0"}}' + } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_channel_packages() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_report_skipped_channels() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + sweep_npm_staging() { return 0; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_major_version() { printf '%s\n' 11; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_npm_install() { printf 'install-call: %s %s\n' "$1" "$2"; } + # shellcheck disable=SC2329 # invoked indirectly by step_npm_globals + npm_global_report_resolved() { printf 'resolved-call: %s %s\n' "$1" "$2"; } + step_npm_globals +) >"$exact_resolution_log" + +if grep -qx 'install-call: wrangler wrangler@latest' "$cooldown_resolution_log" \ + && grep -qx 'resolved-call: wrangler wrangler@4.120.0' "$cooldown_resolution_log" \ + && grep -qx 'install-call: wrangler wrangler@4.120.0' "$exact_resolution_log"; then + ok "stable cooldown installs through @latest while preserving the exact inventory target" +else + not_ok "stable cooldown still handed npm an ineligible exact version or lost reporting truth" +fi + # shellcheck disable=SC2329 # invoked indirectly by the sourced helper npm() { printf '11.16.0\n'