Skip to content

Release: v7.7.0 — doctor cache + zsh-scope hardening#447

Merged
Data-Wise merged 22 commits into
mainfrom
dev
May 15, 2026
Merged

Release: v7.7.0 — doctor cache + zsh-scope hardening#447
Data-Wise merged 22 commits into
mainfrom
dev

Conversation

@Data-Wise
Copy link
Copy Markdown
Owner

Summary

  • Added: flow doctor GitHub token validation cache (1h TTL, fingerprint-keyed) + --no-cache flag + a regression-guard test for the bare-readonly zsh scope trap.
  • Fixed: Three latent zsh bugs — doctor-cache constants vanishing under function-scope sourcing; same pattern preemptively cleaned in analysis-cache and macro-parser; create_mock save/restore that silently destroyed original functions.
  • Changed: Removed the CACHED_DOCTOR_VERBOSE test workaround now that the production cache deduplicates.

Production users were unaffected by the bugs (top-level sourcing keeps zsh globals intact); the failures only surfaced through test harnesses that source via setup().

Highlights

Area What changed
Performance First flow doctor per hour validates the GitHub token; subsequent calls are ~5–8s faster (cache hit).
Correctness Three lib files now use typeset -gr for module constants; test framework's create_mock correctly restores originals after reset_mocks.
Test infra New regression test (test-readonly-scope-regression.zsh) wired into run-all.sh — 5 checks including a functional proof that the trap still exists in current zsh.
Suite count 53/53 → 54/54 (+1 file: 205 → 206)

Test plan

  • ./tests/run-all.sh — 54/54 pass, 1 expected IMAP timeout
  • ./tests/test-doctor.zsh — 31/31, no "Failed to acquire cache lock" warning
  • ./tests/test-doctor-cache.zsh — 19/20 (3.2 expiry test is a pre-existing flake unrelated to this PR)
  • ./tests/test-readonly-scope-regression.zsh — 5/5
  • CHANGELOG.md and docs/CHANGELOG.md mirror verified via diff
  • CI green on this PR
  • CI green on main after merge
  • Homebrew tap formula updates (auto via App-auth release workflow)
  • docs site deploys (auto via docs.yml on push to main)

Notes for review

  • 56 files changed, but 47 of those are mechanical version-display sweeps (**Version:** v7.6.0 → v7.7.0) in docs. Real code changes are 4 commits (doctor-cache, libs, framework, regression test) — see commit history for atomic reads.
  • CHANGELOG entries are written in the why-rather-than-what style: each fix explains why production was unaffected and which scope (test harness) surfaced it.

Test User and others added 22 commits May 11, 2026 13:29
claude-sync wraps the re-add + commit + push loop for files chezmoi
tracks under ~/.claude/ (specifically ~/.claude/CLAUDE.md and per-project
memory dirs). Avoids re-typing the 3-command chezmoi recipe after every
session that writes new memory.

Subcommands:
  claude-sync            re-add + push (idempotent if nothing changed)
  claude-sync --status   show chezmoi diff for ~/.claude/
  claude-sync --no-push  re-add + commit only
  claude-sync --add <p>  bring a new ~/.claude/... path into tracking

Sourced from zsh/.zshrc next to bg-agents.zsh. No new dependencies
beyond chezmoi (already required by the dotfiles repo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chezmoi sometimes creates empty auto-commits (re-add on already-tracked
files with no diff). These leave the local branch ahead of origin/main,
so the old logic 'if git log origin/main..HEAD non-empty: push' triggered
a push even when there was nothing to actually transfer. git then reports
'Everything up-to-date' but the function announced 'pushed'.

The fix captures git push output and parses for 'Everything up-to-date'
to distinguish 'synced (empty auto-commit)' from a real push. Also adds
explicit error handling so push failures surface stderr instead of being
swallowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`chezmoi cd` opens an interactive subshell and silently no-ops when
sourced inside a function (no TTY). The previous `(chezmoi cd && { git
... })` pattern meant the git commands ran against the session's CWD
repo instead of the chezmoi source repo. From rforge/dev that yields
spurious "Everything up-to-date" (rforge's local main matches origin),
masking unpushed commits in dotfiles.

Replaced with `git -C "$(chezmoi source-path)"` which is non-interactive
and explicit. Adds a guard for the case where source-path can't be
resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test-doctor.zsh was timing out under run-all.sh's 30-second wrapper because
three redundant $(doctor ...) calls each spent 5-8s on
curl https://api.github.com/user (commands/doctor.zsh:405).

Root cause:
- Standalone: 38.86s wall (passes 23/23) — over the 30s limit
- Wrapped:    30.01s, SIGKILL on _doctor_missing_brew test
- user+sys:   13.4s
- Network wait: ~25s (real - CPU)

Fix: cache doctor --verbose once in setup() (matching the pattern used for
CACHED_DOCTOR_DEFAULT and CACHED_DOCTOR_HELP) and reuse for both the
--verbose and -v tests. -v is an alias for --verbose per
commands/doctor.zsh:39, so they exercise identical output.

Result:
- 38.86s -> 23.51s wall time (40% reduction)
- exit 0 under timeout 30 zsh
- 23/23 tests still passing
- 6.5s headroom under the 30s ceiling

Out of scope (filed in .STATUS Pending): commands/doctor.zsh:405 calls
curl directly without consulting lib/doctor-cache.zsh, so end-user
flow doctor invocations are also slower than they need to be.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New worktree at ~/.git-worktrees/flow-cli/wire-doctor-cache
on branch feature/wire-doctor-cache.

Planning ORCHESTRATE file for wiring lib/doctor-cache.zsh into
commands/doctor.zsh:405 (the GitHub token validation curl that
currently bypasses the existing file-based cache).

Implementation will happen in a separate session per the workflow
documented in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…idation

Plans the wiring of lib/doctor-cache.zsh into commands/doctor.zsh:405,
which currently calls curl https://api.github.com/user directly without
consulting the existing file-based cache. End-user flow doctor runs
spend ~5-8s on this network call every invocation; caching the result
should drop warm runs to ~1s.

Plan covers: API surface to read (lib/doctor-cache.zsh:722-793 for the
token cache helpers, plus low-level get/set context), target shape for
the refactor at doctor.zsh:404-417, four user decisions to confirm
before coding (TTL, key derivation strategy, --no-cache flag, storage
format), a test strategy that avoids regressing the run-all.sh 30s
ceiling fixed in 74af31b, and verification steps.

This is a planning-only commit. Implementation requires a fresh claude
session in this worktree per flow-cli CLAUDE.md Step 3 (STOP).

Related: commit 74af31b (test-side caching that this work makes redundant)
Related: .STATUS Pending "Doctor command bypasses its own cache"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires lib/doctor-cache.zsh into commands/doctor.zsh:404-455 GitHub token
validation so warm doctor runs skip the api.github.com/user curl. Cache key
is sha256(token)[:12] so token rotation auto-invalidates without a manual
clear path. TTL is 1 hour. Adds --no-cache flag for forced fresh validation.

Empirical timing on a developer machine: cold 11.3s → warm 10.8s (~5% off
per warm run; smaller than the original spec estimate because the
_dots_doctor_integration GitHub curl is already cached via the existing
provider-key path, so this PR only eliminates the legacy section's curl).

Test isolation: DOCTOR_CACHE_DIR is now exported in test-doctor.zsh setup
before plugin load (the cache lib marks it readonly post-source) so tests
no longer pollute developer's ~/.flow/cache/doctor/.

+4 doctor tests: cache_hit_skips_curl, cache_miss_triggers_curl,
no_cache_flag_bypasses, envelope_format. Doctor 27/27, full suite 52/52.

Side discoveries logged in .STATUS Pending:
- tests/test-framework.zsh create_mock save/restore is broken for
  binaries (tail -n +2 strips opening { but keeps closing }) — worked
  around via direct functions[] manipulation in test helpers
- lib/doctor-cache.zsh _doctor_cache_acquire_lock mkdir-fallback leaks
  state across in-process invocations, causing cache_set rc=1 on
  second call with same key — worked around with unique test keys

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the legacy GitHub TOKEN validation block out of doctor() into a
standalone helper, allowing cache tests to exercise the wiring without
invoking the full ~10s system-check pipeline.

Fixes a regression introduced in 0880f92: that commit's 3 mock-curl
tests each ran full `doctor`, pushing test-doctor over the 30s ceiling
in tests/run-all.sh. The commit message and .STATUS both wrongly claimed
"52/52 passing" — actual reading was "2 timeout" (test-doctor was the
undocumented one). Caught when verifying the optional Step 4 follow-up
with `time timeout 30 zsh tests/test-doctor.zsh`.

After this refactor:
- test-doctor standalone: 40s -> 26s
- test-doctor under timeout 30: exit 124 -> exit 0
- full suite: 52 passed / 2 timeout -> 53 passed / 1 timeout (only
  e2e-em-dispatcher remains, which is documented as expected)

The helper also unifies the validation path so future work could route
the --dot cache scheme through the same function.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync user-facing and reference docs to the wiring + refactor commits on
this branch:

- docs/commands/doctor.md: add --no-cache to Options table
- docs/reference/REFCARD-DOCTOR.md: add flow doctor --no-cache row
- docs/reference/MASTER-API-REFERENCE.md: add _doctor_check_github_token
  entry under a new "Cache Consumers" subsection (signature, args, side
  effects, cache key derivation, why fingerprint over provider key)
- docs/architecture/DOCTOR-TOKEN-ARCHITECTURE.md: add "Two Cache Key
  Schemes" subsection covering the provider-key (--dot path) vs
  fingerprint-key (legacy path) tradeoff, when each is preferable, and
  the dual-scheme file layout currently on disk
- CLAUDE.md: correct test count drift (52/52 + 2 timeouts -> 53/53 + 1
  timeout) after the refactor commit moved test-doctor back to ✅

markdownlint: 0 errors across the 5 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ZSH completion for `flow doctor` was missing several flags. Backfilled
all currently-shipping flags so tab-completion surfaces them:
  --no-cache (this PR), --quiet/-q, --dot, --fix-token (prior PRs)

Also added doctor --no-cache to the Entry Points block in
DOCTOR-TOKEN-ARCHITECTURE.md (the v5.17.0 token-automation entries
already there list the other modes; --no-cache belongs in the same set).

Live `doctor --help` already includes --no-cache (from 0880f92).
markdownlint: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o 45s

Adds coverage for previously-uncovered branches of
_doctor_check_github_token:

- empty token path tagged "missing" in _doctor_token_issues
- invalid token (http != 200) tagged "invalid" AND cache file NOT
  written (cache-only-on-success guard)
- doctor --no-cache CLI flag end-to-end (verifies argparse →
  no_cache=true → helper invocation chain)
- fingerprint determinism (sha256 prefix collision-resistance + same
  token → same key invariant)

The E2E test runs full `doctor` and pushes test-doctor standalone runtime
from 26s to 31s, breaching the 30s harness ceiling in tests/run-all.sh.
Made run_test() accept an optional second arg for per-test timeout, and
bumped test-doctor specifically to 45s.

Result: 31/31 doctor tests pass, full suite 53/53 + 1 expected timeout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the workflow rule in CLAUDE.md: ORCHESTRATE files are working
artifacts that belong on feature branches during development, not on
dev after merge. Removing it now keeps the merge commit clean of
ephemeral planning artifacts.

The plan it described is preserved in the commit history (f8879fa
introduced it, this commit removes it) for anyone who wants to read
the original implementation intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced while watching CI on PR #446: the required status check
"ZSH Plugin Tests" only runs test-flow.zsh + test-install.sh per the
explicit "Smoke tests only" comment in test.yml. The full 205-file
suite (including test-doctor.zsh and the new branch tests in this PR)
is never executed by CI.

Filed as Pending to track separately — addressing it is a workflow PR,
not feature work, and bundling it here would be scope creep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(doctor): cache GitHub token validation with fingerprint key
PR #446 wired GitHub token validation into lib/doctor-cache.zsh's
fingerprint cache, so the production cache already deduplicates
repeated `doctor --verbose` invocations across the disk-shared
DOCTOR_CACHE_DIR. The setup-side cache becomes redundant.

Side benefit: test_doctor_v_flag now actually exercises the `-v`
alias rather than asserting the cached `--verbose` output exists.

Verified: 31/31 doctor tests pass in 42.6s (under 45s harness ceiling).
…on-scoped sourcing

The "lock leak" bug filed 2026-05-13 was misdiagnosed. Root cause: in zsh,
`readonly FOO=bar` declared inside a function context (or sourced from one)
creates a *function-local* readonly. When the calling function returns, the
variable vanishes.

Chain: test-doctor.zsh setup() (a function) sources flow.plugin.zsh ->
commands/doctor.zsh -> lib/doctor-cache.zsh, where `readonly DOCTOR_CACHE_*`
became local to setup(). When setup() returned, the constants were gone.
The load guard (`typeset -g _FLOW_DOCTOR_CACHE_LOADED=1`, line 50) IS
global and persists, so re-sourcing is suppressed and the constants stay
undefined.

Symptom: `max_attempts=$((DOCTOR_CACHE_LOCK_TIMEOUT * 10))` evaluated to
`0 * 10 = 0`. The mkdir loop ran zero iterations and `_doctor_cache_set`
returned rc=1 immediately, surfacing as "Failed to acquire cache lock for
writing" — independent of any actual lock state.

Fix: declare the four module constants with `typeset -gr` instead of
`readonly`. Added a comment block explaining the trap so a future cleanup
doesn't silently re-introduce it.

Verified: test-doctor envelope test now passes cleanly with no warning;
full ./tests/run-all.sh: 53 passed, 0 failed, 1 expected timeout
(e2e-em-dispatcher).

Production was unaffected because users source flow.plugin.zsh from .zshrc
at top level, where readonlys are global.
Defensive: same zsh function-scope trap as commit f193907 (doctor-cache).
Both libs declare module-level constants with bare `readonly` and currently
work only because flow.plugin.zsh sources them at top level. If a future
test harness or helper sources either lib from inside a function, the
constants would silently become undefined when the caller returns — same
failure mode as the doctor-cache "lock leak."

Files:
- lib/analysis-cache.zsh: 3 constants (SCHEMA_VERSION, DEFAULT_TTL_HOURS,
  LOCK_TIMEOUT)
- lib/macro-parser.zsh: 3 string constants + 1 array (MACRO_FORMAT_*,
  MACRO_AUTO_DISCOVER_PATHS — uses `-gar` for the array)

Verified: ./tests/run-all.sh — 53 passed, 0 failed, 1 expected timeout.
Mirrors tests/test-local-path-regression.zsh. Prevents reintroduction of
the function-scope bug fixed in f193907 + 9db49db.

Five checks:
1. lib/*.zsh has no bare `readonly` declarations
2. commands/*.zsh has no bare `readonly` declarations
3. setup/ and hooks/ have no bare `readonly` declarations
4. Functional proof: `readonly` inside a function vanishes; `typeset -gr` survives
5. Lib-pattern proof: bare readonly arithmetic evaluates to 0 after the
   sourcing function returns; `typeset -gr` evaluates correctly

Wired into ./tests/run-all.sh under the existing "Regression tests" section.

Test counts updated across CLAUDE.md, .STATUS, and docs/guides/TESTING.md
(205 -> 206 test files, 53/53 -> 54/54 suites).
The previous `whence -f $fn | tail -n +2` save mechanism was broken:
`whence -f` outputs `name () {\n  body\n}`, so `tail -n +2` skips only
the opening `name () {` line, leaving the trailing `}` in the body.
Wrapping that in a new `funcname() { ... }` template produced unbalanced
braces — `{...} }` — and a silent eval parse error.

Cascading failure: the silent save failure meant `_original_mock_${fn}`
was never created, so `reset_mocks` couldn't find the saved original
and fell into the `unset -f $fn_name` branch — DESTROYING the original
function instead of restoring it. After the first reset_mocks, the
real function was permanently gone.

Fix: replace eval-string save/restore with zsh's `${functions[name]}`
associative array. `functions[name]` returns the body without surrounding
braces, and assignment to `functions[name]=$saved` round-trips losslessly.
Save state lives in a new `_ORIGINAL_FUNCTIONS` global assoc array.

Verified: standalone repro now correctly restores the original function
after both first and second mock cycles. Full ./tests/run-all.sh: 54
passed (deploy-v2 timeouts under run-all's 30s wrapper are pre-existing
flakes unrelated to mocks — both pass 71/71 in isolation).

Workaround functions like `_test_install_curl_mock` in tests/test-doctor.zsh
already use the same pattern and could now be replaced with create_mock,
but that's a separate cleanup not bundled here.
- Add 5 entries to root + docs CHANGELOG [Unreleased] mirroring this
  session's commits:
    Added: regression test for bare-readonly zsh trap
    Fixed: doctor-cache constants (typeset -gr)
    Fixed: preventive same fix in analysis-cache + macro-parser
    Fixed: create_mock save/restore (functions[] instead of whence-tail)
    Changed: drop CACHED_DOCTOR_VERBOSE workaround now that production
             cache deduplicates
- Update CLAUDE.md tree comment: 205 → 206 test files

The two CHANGELOG files are kept in sync via diff verification:
  diff <(awk '/Unreleased/,/[7.6.0]/' CHANGELOG.md) \
       <(awk '/Unreleased/,/[7.6.0]/' docs/CHANGELOG.md)
Headlines: doctor cache + zsh-scope hardening

- Bump FLOW_VERSION (flow.plugin.zsh), package.json, CLAUDE.md, .STATUS
- Sweep 47 doc files: header/footer "**Version:** v7.6.0" → v7.7.0
  (narrow patterns; historical "New in v7.6.0" content untouched)
- Rename CHANGELOG [Unreleased] → [7.7.0] — 2026-05-15 in both
  CHANGELOG.md and docs/CHANGELOG.md
- Update CLAUDE.md "Last Updated" footer to 2026-05-15

What ships in v7.7.0:
  Added:
    - flow doctor GitHub token validation cache (1h TTL)
    - flow doctor --no-cache flag
    - tests/test-readonly-scope-regression.zsh
  Fixed:
    - doctor-cache silent write failures (typeset -gr scope fix)
    - Same fix preventively in analysis-cache + macro-parser
    - create_mock save/restore destroyed originals (functions[] fix)
  Changed:
    - Drop CACHED_DOCTOR_VERBOSE workaround
@Data-Wise Data-Wise merged commit 92315f4 into main May 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant