diff --git a/.STATUS b/.STATUS index 27222c7a9..2934357ff 100644 --- a/.STATUS +++ b/.STATUS @@ -5,11 +5,29 @@ ## Type: zsh-plugin ## Status: active ## Focus: --help -## Phase: Released (v7.11.0) +## Phase: Release Pending (v7.13.0) ## Priority: 2 ## Progress: 100 -## Current Session (2026-06-19) β€” flow claude check docs + merge cleanup βœ… COMPLETE +## Current Session (2026-06-19) β€” v7.13.0 Release Pipeline πŸš€ + +**Session activity:** +- **Bumped** version to v7.13.0 (flow.plugin.zsh, package.json, CLAUDE.md, man pages) +- **Updated** CHANGELOG.md + docs/CHANGELOG.md with 7.13.0 release date (2026-06-19) +- **Inserted** 7.12.0 entry in docs/CHANGELOG.md (was missing) +- **Updated** docs/reference/MASTER-DISPATCHER-GUIDE.md version footer +- **Docs:** `docs/commands/claude.md` + `docs/tutorials/49-flow-claude-check.md` updated for C7-C11 + watch daemon (v7.13.0) +- **PR:** In progress β€” gh pr create --base main --head dev + +## Previous Session (2026-06-19) β€” v7.12.0 SHIPPED [main + Homebrew] βœ… + +**Session activity:** +- **Fixed** `_flow_claude_fix_c1`: replaced `sed -i ''` (BSD-only) with portable temp-file approach for GNU sed compat (Linux CI) +- **Merged** PR #474 (Release v7.12.0) β€” both CI checks green (ZSH Plugin Tests + Full Test Suite) +- **Tagged** v7.12.0 β†’ GitHub release β†’ Homebrew auto-updated (7.11.0 β†’ 7.12.0) βœ… +- **CI:** All green (main + Version Guard + Homebrew Release + Deploy Documentation) + +## Previous Session (2026-06-19) β€” flow claude check docs + merge cleanup βœ… COMPLETE **Session activity:** - **Merged** PR #473 (`feature/flow-claude` β†’ dev) β€” `flow claude check` C1–C6 + `--fix` + tutorial 49 + command ref @@ -403,7 +421,7 @@ | Worktree | Branch | Status | |----------|--------|--------| -| Main repo | `dev` | v7.10.0 SHIPPED + CI full-suite gate MERGED (PR #465 β†’ dev `b57c0d87`); both worktrees removed; no active worktrees. **Pending:** 3 squash-merged branches (manpage-refresh/tok-autosync/scholar-fix) await manual `git branch -D`; CI-gate **Phase 3** (promote `full-suite` to required check after dev soak). | +| `~/.git-worktrees/flow-cli/feature-flow-claude-v2` | `feature/flow-claude-v2` | ORCHESTRATE written β€” awaiting impl in new session | --- @@ -440,4 +458,4 @@ **Status:** v7.10.2 SHIPPED (main + Homebrew tap) β€” docs polish + dep maintenance | full suite REQUIRED on main β€” 64 passed / 0 failed / 1 skipped | 14 dispatchers + at bridge | 216 test files | 12000+ test functions | 0 lint errors | 0 broken links ## wins: Fixed the regression bug (2026-06-19), --category fix squashed the bug (2026-06-19), fixed the bug (2026-06-19), Fixed the regression bug (2026-06-19), --category fix squashed the bug (2026-06-19) ## streak: 1 -## last_active: 2026-06-19 08:50 +## last_active: 2026-06-19 10:30 diff --git a/CHANGELOG.md b/CHANGELOG.md index d589e8700..6b1b6c370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.13.0] β€” 2026-06-19 β€” flow claude: C7-C11 checks + watch daemon + +### Added + +- **C4 two-tier threshold** (`commands/claude.zsh`): >100 lines β†’ WARN "approaching 180-line limit"; >180 lines β†’ ERROR "exceeds 180-line hard limit" (was a single >100 check) +- **C7 per-project CLAUDE.md audit**: scans `$FLOW_CLAUDE_PROJECTS_ROOT` (default: `~/projects`) up to depth 4; warns on files >180 lines or version refs that don't match `git describe --tags`; injectable via `FLOW_CLAUDE_PROJECTS_ROOT` +- **C8 orphaned memory dirs**: decodes `~/.claude/projects/` slug names back to filesystem paths (`/a-b-c` β†’ `/a/b/c`); warns on stale dirs whose decoded path no longer exists +- **C9 rules drift**: checks that every `~/.claude/rules/*.md` stem is referenced in `~/.claude/CLAUDE.md`; warns on unreferenced rules +- **C10 missing hook files**: parses `settings.json` hook commands; errors on any absolute-path script that isn't present on disk +- **C11 plugin health**: checks `~/.claude/plugins/*/plugin.json` exists and is valid JSON (skips `cache/` subdir); warns on broken plugins +- **`flow claude watch [--interval N] [--stop] [--status]`**: background health watcher β€” runs `flow claude check` on a configurable interval (default: 30 min), writes state to `~/.flow/claude-health-state.json`, and fires a desktop notification via `terminal-notifier` when status changes between pass/warn/error; gracefully silent on Linux where `terminal-notifier` is absent + ## [7.12.0] β€” 2026-06-19 β€” flow claude check: Claude Code environment health ### Added diff --git a/CLAUDE.md b/CLAUDE.md index fa389f7cc..49af2c46a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file provides guidance to Claude Code when working with code in this reposi **flow-cli** - Pure ZSH plugin for ADHD-optimized workflow management. Zero dependencies. Standalone (works without Oh-My-Zsh or any plugin manager). - **Architecture:** Pure ZSH plugin (no Node.js runtime required) -- **Current Version:** v7.12.0 +- **Current Version:** v7.13.0 - **Install:** Homebrew (recommended), or any plugin manager - **Source:** `source /opt/homebrew/opt/flow-cli/flow.plugin.zsh` (via Homebrew) - **Optional:** Atlas integration for enhanced state management @@ -137,7 +137,7 @@ flow-cli/ β”œβ”€β”€ docs/ # Documentation (MkDocs) β”‚ └── internal/ # Internal conventions & contributor templates β”œβ”€β”€ scripts/ # Standalone validators (check-math.zsh) -β”œβ”€β”€ tests/ # 218 test files, 12000+ test functions +β”œβ”€β”€ tests/ # 219 test files, 12000+ test functions β”‚ └── fixtures/demo-course/ # STAT-101 demo course for E2E └── .archive/ # Archived Node.js CLI ``` @@ -182,7 +182,7 @@ flow-cli/ ## Testing -**218 test files, 12000+ test functions.** Run: `./tests/run-all.sh` (66/66 passing, 1 expected interactive/tmux timeout) or individual suites in `tests/`. +**219 test files, 12000+ test functions.** Run: `./tests/run-all.sh` (67/67 passing, 1 expected interactive/tmux timeout) or individual suites in `tests/`. See `docs/guides/TESTING.md` for patterns, mocks, assertions, TDD workflow. @@ -216,8 +216,8 @@ export FLOW_FORCE_DISPATCHER_OBS=1 # Force-keep one dispatcher (FLOW_F ## Current Status -**Version:** v7.12.0 | **Tests:** 12000+ (66/66 suite, 1 interactive timeout) | **Docs:** https://Data-Wise.github.io/flow-cli/ +**Version:** v7.13.0 | **Tests:** 12000+ (67/67 suite, 1 interactive timeout) | **Docs:** https://Data-Wise.github.io/flow-cli/ --- -**Last Updated:** 2026-06-19 (v7.12.0) +**Last Updated:** 2026-06-19 (v7.13.0) diff --git a/ORCHESTRATE-flow-claude-v2.md b/ORCHESTRATE-flow-claude-v2.md new file mode 100644 index 000000000..9cbb70b33 --- /dev/null +++ b/ORCHESTRATE-flow-claude-v2.md @@ -0,0 +1,149 @@ +# ORCHESTRATE: flow claude v2 β€” Extended Checks + Watch + +**Branch:** `feature/flow-claude-v2` +**Spec:** `docs/specs/SPEC-flow-claude-v2.md` +**Target:** v7.13.0 + +--- + +## Objective + +Extend `flow claude check` with C4 two-tier fix and new checks C7–C11, then +add `flow claude watch` background daemon with terminal-notifier alerts on +WARN/ERROR state change. + +--- + +## Task List + +### Wave 1 β€” Existing file changes (no new files) + +- [ ] **1a. Fix C4 two-tier threshold** (`commands/claude.zsh`) + - Change single `if (( line_count > 100 ))` to two-tier: + - `> 100` β†’ `_flow_log_warning` + `has_warn=1` + - `> 180` β†’ `_flow_log_error` + `has_error=1` + - Update help text: `C4 CLAUDE.md length warns > 100 lines, errors > 180` + - Tests: 95 lines β†’ pass, 150 β†’ warn, 200 β†’ error + +- [ ] **1b. Add C8: Orphaned memory dirs** (`commands/claude.zsh`) + - After C7 block, iterate `${FLOW_CLAUDE_HOME:-$HOME/.claude}/projects/*/` + - Decode slug to path: `/${slug//-//}` (leading `-` β†’ `/`) + - Flag if `[[ ! -d "$decoded_path" ]]` + - Injectable: `FLOW_CLAUDE_HOME` (already used by C1–C4) + - Tests: mock a slug for a nonexistent dir β†’ warn; valid dir β†’ pass + +- [ ] **1c. Add C9: Rules drift** (`commands/claude.zsh`) + - Iterate `$claude_home/rules/*.md` + - Extract stem: `${rule_file:t:r}` + - Check `grep -qF "$stem" "$claude_home/CLAUDE.md"` + - Flag unreferenced stems + - Tests: rule file not cited in CLAUDE.md β†’ warn; all cited β†’ pass + +- [ ] **1d. Add C10: Missing hook files** (`commands/claude.zsh`) + - Requires `jq` (guard same as C1) + - Extract from settings.json: `jq -r '(.hooks // {}) | to_entries[] | .value[] | .command'` + - For each command starting with `/` or `~`: check file exists + - Severity: ERROR (not WARN β€” missing hook = silent breakage) + - Tests: hook path missing β†’ error; hook path present β†’ pass; no hooks β†’ pass + +- [ ] **1e. Add C11: Plugin health** (`commands/claude.zsh`) + - Iterate `$claude_home/plugins/*/` (skip `cache/`) + - Check `plugin.json` exists + is valid JSON (`jq empty`) + - Severity: WARN + - Tests: missing plugin.json β†’ warn; invalid JSON β†’ warn; valid β†’ pass + +### Wave 2 β€” C7 (needs helper + git calls) + +- [ ] **2a. Add `_flow_find_project_claude_mds` helper** (`commands/claude.zsh` or `lib/core.zsh`) + - `find "${FLOW_CLAUDE_PROJECTS_ROOT:-$HOME/projects}" -maxdepth 4 -name "CLAUDE.md" -not -path "*/.git/*" -not -path "*/node_modules/*"` + - Returns newline-separated paths via stdout + +- [ ] **2b. Add C7: Per-project CLAUDE.md audit** (`commands/claude.zsh`) + - Call `_flow_find_project_claude_mds` β†’ iterate paths + - C7a: `wc -l < "$file"` > 180 β†’ warn + - C7b: `git -C "$proj_dir" describe --tags --abbrev=0 2>/dev/null` β†’ if succeeds, + grep file for `v[0-9]+\.[0-9]+\.[0-9]+`, compare each match against tag; + mismatch β†’ warn; no tags β†’ skip silently + - Injectable: `FLOW_CLAUDE_PROJECTS_ROOT` + - Tests: fixture dir with 200-line CLAUDE.md β†’ warn; mocked git tag mismatch β†’ warn; + no tags β†’ pass (no false positive) + +### Wave 3 β€” Watch daemon + +- [ ] **3a. Add `flow claude watch` routing** (`commands/claude.zsh`) + - Add `watch)` case to `flow_claude()` dispatch + - Parse `--stop`, `--status`, `--interval N` flags + +- [ ] **3b. Implement `_flow_claude_watch_start`** (`commands/claude.zsh`) + - Stale PID check via `kill -0` + - Background loop: `_flow_claude_watch_run_check` β†’ sleep β†’ repeat + - Log rotation: keep last 50KB of `~/.flow/claude-watch.log` + - Write PID to `~/.flow/claude-watch.pid` + - `disown $!` after backgrounding + +- [ ] **3c. Implement `_flow_claude_watch_run_check`** (`commands/claude.zsh`) + - Runs `_flow_claude_check` in a subshell, captures exit code + - Translates exit code to result string: 0=pass, 1=error, 2=warn + - Reads previous state from `~/.flow/claude-health-state.json` + - Calls `_flow_claude_watch_notify` on state change + - Writes new state JSON + +- [ ] **3d. Implement `_flow_claude_watch_notify`** (`commands/claude.zsh`) + - Only notify if new/old state is warn or error (skip info/pass↔pass) + - `command -v terminal-notifier` guard β€” silent on Linux + - Call: `terminal-notifier -title "flow claude" -subtitle "..." -message "..." -sound default` + +- [ ] **3e. Implement `--stop` and `--status`** (`commands/claude.zsh`) + - `--stop`: read PID file, `kill $pid`, remove pid file + - `--status`: read state JSON, show PID/interval/last-check/result + +- [ ] **3f. Update `_flow_claude_help`** with watch subcommand docs + +### Wave 4 β€” Tests + +- [ ] **4a. Tests for C4 two-tier, C8, C9, C10, C11** (`tests/test-flow-claude.zsh`) + - Fixture dirs injected via `FLOW_CLAUDE_HOME` / `FLOW_CLAUDE_PROJECTS_ROOT` + - See full test list in `SPEC-flow-claude-v2.md` + +- [ ] **4b. Tests for C7** (`tests/test-flow-claude.zsh`) + - Mock `git describe` via function override in test scope + +- [ ] **4c. Tests for watch** (`tests/test-flow-claude.zsh`) + - Mock `_flow_claude_check` exit code + - Assert state file transitions + - Assert `--stop` removes pid file + - Assert `--status` doesn't crash when watcher is not running + +### Wave 5 β€” Docs + completions + +- [ ] **5a. Update `completions/_flow_claude`** β€” add `watch`, `--interval`, `--stop`, `--status` +- [ ] **5b. Update `man/man1/flow.1`** β€” document watch + new checks +- [ ] **5c. Update `CLAUDE.md`** β€” test count, suite count +- [ ] **5d. Update `TESTING.md`** β€” test count (3 locations) +- [ ] **5e. Update `CHANGELOG.md`** β€” Unreleased section + +--- + +## Verification + +After each wave: +```zsh +source flow.plugin.zsh +flow claude check +./tests/run-all.sh +``` + +Final gate before PR: +```zsh +./tests/run-all.sh # expect 66/66 suites passing (+ new test-flow-claude tests) +flow claude check # run against real ~/.claude β€” all checks should execute +flow claude watch --interval 1 # start 1-min watcher, verify state file written +flow claude watch --status # confirm output +flow claude watch --stop # confirm clean shutdown +``` + +--- + +## PR target + +`gh pr create --base dev --title "feat(claude): C7-C11 checks + watch daemon (v7.13.0)"` diff --git a/commands/claude.zsh b/commands/claude.zsh index 0bbf858e9..c41a2c7de 100644 --- a/commands/claude.zsh +++ b/commands/claude.zsh @@ -7,6 +7,7 @@ flow_claude() { case "$subcmd" in check|doctor) _flow_claude_check "$@" ;; + watch) _flow_claude_watch "$@" ;; help|--help|-h) _flow_claude_help ;; *) _flow_log_error "Unknown subcommand: $subcmd" @@ -21,17 +22,26 @@ _flow_claude_help() { print "${b}flow claude${r} β€” Claude Code environment health checker" print "" print "${b}Usage:${r}" - print " flow claude check Run all environment checks" - print " flow claude check --fix Run checks + auto-repair safe mismatches (C1, C6)" - print " flow claude doctor Alias for check" + print " flow claude check Run all environment checks" + print " flow claude check --fix Run checks + auto-repair safe mismatches (C1, C6)" + print " flow claude doctor Alias for check" + print " flow claude watch Start background health watcher (30-min default)" + print " flow claude watch --stop Stop watcher" + print " flow claude watch --status Show watcher status + last result" + print " flow claude watch --interval N Set poll interval in minutes" print "" print "${b}Checks:${r}" print " C1 Settings parity settings.json env block vs zshrc exports" print " C2 Hook health post-compact-reinject.sh exists + executable + shellcheck" print " C3 Memory index drift .md file count vs MEMORY.md entry count" - print " C4 CLAUDE.md length warns when > 100 lines" + print " C4 CLAUDE.md length warns > 100 lines, errors > 180" print " C5 Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE exported" print " C6 Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS > 8192 (auto-fixable with --fix)" + print " C7 Project CLAUDE.md per-project line count + version drift" + print " C8 Orphaned memory ~/.claude/projects/ dirs for deleted projects" + print " C9 Rules drift ~/.claude/rules/*.md files not cited in CLAUDE.md" + print " C10 Hook files hooks in settings.json pointing to missing scripts" + print " C11 Plugin health ~/.claude/plugins/ dirs missing valid plugin.json" print "" print "${b}Exit codes:${r} 0=all pass 1=any ERROR 2=any WARN (no ERROR)" } @@ -156,8 +166,11 @@ _flow_claude_check() { else local line_count line_count=$(wc -l < "$claude_md" | tr -d ' ') - if (( line_count > 100 )); then - _flow_log_warning "C4 CLAUDE.md length $line_count lines β€” exceeds 100-line rule (trim before adding)" + if (( line_count > 180 )); then + _flow_log_error "C4 CLAUDE.md length $line_count lines β€” exceeds 180-line hard limit (see ~/.claude/rules/claude-md-length.md)" + has_error=1 + elif (( line_count > 100 )); then + _flow_log_warning "C4 CLAUDE.md length $line_count lines β€” approaching 180-line limit (trim before adding)" has_warn=1 else _flow_log_success "C4 CLAUDE.md length $line_count lines β€” within limit" @@ -198,6 +211,180 @@ _flow_claude_check() { _flow_log_success "C6 Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS=${token_val}" fi + # ── C7: Per-project CLAUDE.md audit ───────────────────────────────────── + local projects_root="${FLOW_CLAUDE_PROJECTS_ROOT:-$HOME/projects}" + if [[ -d "$projects_root" ]]; then + local c7_issues=() c7_scanned=0 c7_clean=0 + local proj_file + while IFS= read -r proj_file; do + [[ -z "$proj_file" ]] && continue + (( c7_scanned++ )) + local proj_dir="${proj_file:h}" + local rel_path="${proj_file#$projects_root/}" + local file_has_issue=0 + + # C7a: line count + local plines + plines=$(wc -l < "$proj_file" | tr -d ' ') + if (( plines > 180 )); then + c7_issues+=("$rel_path: $plines lines (> 180)") + file_has_issue=1 + fi + + # C7b: version drift (only if repo has tags) + local git_tag + git_tag=$(git -C "$proj_dir" describe --tags --abbrev=0 2>/dev/null) + if [[ -n "$git_tag" ]]; then + local version_refs + version_refs=$(grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' "$proj_file" 2>/dev/null) + local vref + while IFS= read -r vref; do + [[ -z "$vref" ]] && continue + if [[ "$vref" != "$git_tag" ]]; then + c7_issues+=("$rel_path: version ref $vref (current: $git_tag)") + file_has_issue=1 + fi + done <<< "$version_refs" + fi + + (( file_has_issue )) || (( c7_clean++ )) + done <<< "$(_flow_find_project_claude_mds "$projects_root")" + + if (( ${#c7_issues[@]} > 0 )); then + _flow_log_warning "C7 Project CLAUDE.md ${#c7_issues[@]} issue(s) ($c7_scanned files scanned):" + local issue + for issue in "${c7_issues[@]}"; do + print " $issue" + done + if (( c7_clean > 0 )); then + _flow_log_success "C7 Project CLAUDE.md $c7_clean clean" + fi + has_warn=1 + else + _flow_log_success "C7 Project CLAUDE.md $c7_scanned files scanned, all clean" + fi + else + _flow_log_success "C7 Project CLAUDE.md projects root not found (skipped)" + fi + + # ── C8: Orphaned memory dirs ──────────────────────────────────────────── + local claude_projects="$claude_home/projects" + if [[ -d "$claude_projects" ]]; then + local orphaned=() valid_count=0 + local proj_slug proj_dir_path decoded_path + for proj_dir_path in "$claude_projects"/*(N/); do + proj_slug="${proj_dir_path##*/}" + decoded_path="/${proj_slug//-//}" + if [[ ! -d "$decoded_path" ]]; then + orphaned+=("$proj_slug (path $decoded_path not found)") + else + (( valid_count++ )) + fi + done + if (( ${#orphaned[@]} > 0 )); then + _flow_log_warning "C8 Orphaned memory ${#orphaned[@]} stale dir(s):" + local orphan + for orphan in "${orphaned[@]}"; do + print " $orphan" + done + has_warn=1 + else + _flow_log_success "C8 Orphaned memory $valid_count dirs all valid" + fi + fi + + # ── C9: Rules drift ───────────────────────────────────────────────────── + local rules_dir="$claude_home/rules" + local claude_md_global="$claude_home/CLAUDE.md" + if [[ -d "$rules_dir" ]] && [[ -f "$claude_md_global" ]]; then + local unreferenced=() ref_count=0 + local rule_file rule_stem + for rule_file in "$rules_dir"/*.md(N); do + rule_stem="${rule_file:t:r}" + if ! grep -qF "$rule_stem" "$claude_md_global" 2>/dev/null; then + unreferenced+=("$rule_stem") + else + (( ref_count++ )) + fi + done + if (( ${#unreferenced[@]} > 0 )); then + _flow_log_warning "C9 Rules drift ${#unreferenced[@]} unreferenced rule(s):" + local rule + for rule in "${unreferenced[@]}"; do + print " $rule (not mentioned in CLAUDE.md)" + done + has_warn=1 + else + _flow_log_success "C9 Rules drift $ref_count rules all referenced" + fi + elif [[ ! -d "$rules_dir" ]]; then + _flow_log_success "C9 Rules drift no rules dir found (skipped)" + fi + + # ── C10: Missing hook files ────────────────────────────────────────────── + if [[ -f "$settings_json" ]]; then + if ! command -v jq &>/dev/null; then + _flow_log_warning "C10 Hook files jq not installed β€” cannot check hooks" + has_warn=1 + else + local missing_hooks=() hooks_present=0 + local hook_cmd hook_cmds + hook_cmds=$(jq -r '(.hooks // {}) | to_entries[] | .value[] | .command' "$settings_json" 2>/dev/null) + while IFS= read -r hook_cmd; do + [[ -z "$hook_cmd" ]] && continue + # Only check file paths (absolute or home-relative) + if [[ "$hook_cmd" == /* ]] || [[ "$hook_cmd" == ~* ]]; then + local expanded_cmd="${hook_cmd/#\~/$HOME}" + local script_path="${expanded_cmd%% *}" + if [[ ! -f "$script_path" ]]; then + missing_hooks+=("$script_path (defined in settings.json, not found)") + else + (( hooks_present++ )) + fi + fi + done <<< "$hook_cmds" + + if (( ${#missing_hooks[@]} > 0 )); then + _flow_log_error "C10 Hook files ${#missing_hooks[@]} missing:" + local mh + for mh in "${missing_hooks[@]}"; do + print " $mh" + done + has_error=1 + else + _flow_log_success "C10 Hook files $hooks_present hooks all present" + fi + fi + fi + + # ── C11: Plugin health ─────────────────────────────────────────────────── + local plugins_dir="$claude_home/plugins" + if [[ -d "$plugins_dir" ]] && command -v jq &>/dev/null; then + local broken_plugins=() healthy_plugins=0 + local plugin_dir pjson + for plugin_dir in "$plugins_dir"/*(N/); do + [[ "${plugin_dir:t}" == "cache" ]] && continue + pjson="$plugin_dir/plugin.json" + if [[ ! -f "$pjson" ]]; then + broken_plugins+=("${plugin_dir:t}: missing plugin.json") + elif ! jq empty "$pjson" 2>/dev/null; then + broken_plugins+=("${plugin_dir:t}: invalid JSON in plugin.json") + else + (( healthy_plugins++ )) + fi + done + if (( ${#broken_plugins[@]} > 0 )); then + _flow_log_warning "C11 Plugin health ${#broken_plugins[@]} broken plugin(s):" + local bp + for bp in "${broken_plugins[@]}"; do + print " $bp" + done + has_warn=1 + else + _flow_log_success "C11 Plugin health $healthy_plugins plugins healthy" + fi + fi + # ── Summary ────────────────────────────────────────────────────────────── print "" if (( has_error )); then @@ -212,6 +399,15 @@ _flow_claude_check() { fi } +# Find all project CLAUDE.md files under the given root +_flow_find_project_claude_mds() { + local projects_root="${1:-$HOME/projects}" + find "$projects_root" -maxdepth 4 -name "CLAUDE.md" \ + -not -path "*/.git/*" \ + -not -path "*/node_modules/*" \ + 2>/dev/null +} + # Repair C6: set CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000 in zshrc _flow_claude_fix_c6() { local zshrc="$1" @@ -241,3 +437,198 @@ _flow_claude_fix_c1() { _flow_log_success " --fix: added $key to zshrc" fi } + +# ── Watch daemon ────────────────────────────────────────────────────────── + +_flow_claude_watch() { + local interval=30 + local do_stop=0 + local do_status=0 + + while [[ $# -gt 0 ]]; do + case "$1" in + --stop) do_stop=1; shift ;; + --status) do_status=1; shift ;; + --interval) interval="${2:-30}"; shift 2 ;; + --interval=*) interval="${1#--interval=}"; shift ;; + *) shift ;; + esac + done + + if (( do_stop )); then + _flow_claude_watch_stop + elif (( do_status )); then + _flow_claude_watch_status + else + _flow_claude_watch_start "$interval" + fi +} + +_flow_claude_watch_start() { + local interval_min="${1:-30}" + local interval_sec=$(( interval_min * 60 )) + local flow_dir="$HOME/.flow" + local pid_file="$flow_dir/claude-watch.pid" + local state_file="$flow_dir/claude-health-state.json" + local log_file="$flow_dir/claude-watch.log" + + mkdir -p "$flow_dir" + + # Stale PID check + if [[ -f "$pid_file" ]]; then + local old_pid + old_pid=$(< "$pid_file") + if kill -0 "$old_pid" 2>/dev/null; then + _flow_log_warning "watch already running (PID $old_pid) β€” use --stop first" + return 1 + fi + rm -f "$pid_file" + fi + + # Launch background loop + ( + print $$ > "$pid_file" + while true; do + _flow_claude_watch_run_check "$state_file" >> "$log_file" 2>&1 + # Log rotation: keep last 50KB + if [[ -f "$log_file" ]] && (( $(wc -c < "$log_file") > 50000 )); then + tail -c 50000 "$log_file" > "${log_file}.tmp" && mv "${log_file}.tmp" "$log_file" + fi + sleep "$interval_sec" + done + ) & + disown $! + _flow_log_success "watch started (PID $!, interval ${interval_min}m)" +} + +_flow_claude_watch_stop() { + local pid_file="$HOME/.flow/claude-watch.pid" + if [[ ! -f "$pid_file" ]]; then + _flow_log_warning "watch not running (no pid file)" + return 0 + fi + local pid + pid=$(< "$pid_file") + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null + rm -f "$pid_file" + _flow_log_success "watch stopped (PID $pid)" + else + rm -f "$pid_file" + _flow_log_warning "watch was not running (stale pid $pid), cleaned up" + fi +} + +_flow_claude_watch_status() { + local pid_file="$HOME/.flow/claude-watch.pid" + local state_file="$HOME/.flow/claude-health-state.json" + + local is_running=0 pid="" + if [[ -f "$pid_file" ]]; then + pid=$(< "$pid_file") + kill -0 "$pid" 2>/dev/null && is_running=1 + fi + + if (( is_running )); then + print "● flow claude watch running (PID $pid)" + else + print "β—‹ flow claude watch not running" + fi + + if [[ -f "$state_file" ]] && command -v jq &>/dev/null; then + local last_check result interval_sec interval_min + last_check=$(jq -r '.last_check // "unknown"' "$state_file" 2>/dev/null) + result=$(jq -r '.result // "unknown"' "$state_file" 2>/dev/null) + interval_sec=$(jq -r '.interval // 1800' "$state_file" 2>/dev/null) + interval_min=$(( interval_sec / 60 )) + print " Last check: $last_check β€” $(print "$result" | tr '[:lower:]' '[:upper:]')" + if (( is_running )); then + print " (interval: ${interval_min}m)" + else + print " (watcher was stopped)" + fi + fi +} + +_flow_claude_watch_run_check() { + local state_file="${1:-$HOME/.flow/claude-health-state.json}" + + # Read previous result + local prev_result="pass" + if [[ -f "$state_file" ]] && command -v jq &>/dev/null; then + prev_result=$(jq -r '.result // "pass"' "$state_file" 2>/dev/null) + fi + + # Run check in subshell, capture exit code + local check_rc + _flow_claude_check > /dev/null 2>&1 + check_rc=$? + + local new_result + case "$check_rc" in + 0) new_result="pass" ;; + 1) new_result="error" ;; + 2) new_result="warn" ;; + *) new_result="error" ;; + esac + + local now + now=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date '+%Y-%m-%dT%H:%M:%SZ') + + # Preserve interval from existing state or default to 1800 + local interval=1800 + if [[ -f "$state_file" ]] && command -v jq &>/dev/null; then + interval=$(jq -r '.interval // 1800' "$state_file" 2>/dev/null) + fi + + local pid_file="$HOME/.flow/claude-watch.pid" + local pid="" + [[ -f "$pid_file" ]] && pid=$(< "$pid_file") + + # Write new state JSON + if command -v jq &>/dev/null; then + jq -n \ + --arg pid "$pid" \ + --argjson interval "$interval" \ + --arg last_check "$now" \ + --arg result "$new_result" \ + '{pid: $pid, interval: $interval, last_check: $last_check, result: $result}' \ + > "$state_file" 2>/dev/null + else + print "{\"pid\":\"$pid\",\"interval\":$interval,\"last_check\":\"$now\",\"result\":\"$new_result\"}" > "$state_file" + fi + + # Notify on state change + _flow_claude_watch_notify "$prev_result" "$new_result" "Health state changed to $new_result" +} + +_flow_claude_watch_notify() { + local prev_result="$1" + local new_result="$2" + local summary="$3" + + # No change, silent + [[ "$prev_result" == "$new_result" ]] && return + + # Only notify if new or old state is warn/error (skip pass↔pass) + if [[ "$new_result" == "pass" && "$prev_result" == "pass" ]]; then return; fi + + local title="flow claude" + local subtitle message + if [[ "$new_result" == "pass" ]]; then + subtitle="Health restored" + message="All checks passing" + elif [[ "$new_result" == "error" ]]; then + subtitle="Health degraded β€” ERROR" + message="$summary" + else + subtitle="Health warning" + message="$summary" + fi + + if command -v terminal-notifier &>/dev/null; then + terminal-notifier -title "$title" -subtitle "$subtitle" \ + -message "$message" -sound default + fi + # Silent fallback on Linux (no terminal-notifier) +} diff --git a/completions/_flow b/completions/_flow index 10fb6ae4a..bf351e84a 100644 --- a/completions/_flow +++ b/completions/_flow @@ -446,13 +446,21 @@ _flow() { claude_subcmds=( 'check:Run all environment checks' 'doctor:Alias for check' + 'watch:Background health watcher with desktop notifications' ) claude_opts=( '--fix:Auto-repair safe mismatches (C1 settings parity only)' '--help:Show help' ) + local -a watch_opts + watch_opts=( + '--interval:Check interval in minutes (default: 30)' + '--stop:Stop the background watcher' + '--status:Show watcher status and last result' + ) _describe -t commands 'claude subcommand' claude_subcmds _describe -t options 'option' claude_opts + _describe -t watch-options 'watch option' watch_opts ;; *) _files diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b52416925..c51ae9357 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,38 @@ The format follows [Keep a Changelog](https://keepachangelog.com/), and this pro ## [Unreleased] +## [7.13.0] β€” 2026-06-19 β€” flow claude: C7-C11 checks + watch daemon + +### Added + +- **C4 two-tier threshold** (`commands/claude.zsh`): >100 lines β†’ WARN "approaching 180-line limit"; >180 lines β†’ ERROR "exceeds 180-line hard limit" (was a single >100 check) +- **C7 per-project CLAUDE.md audit**: scans `$FLOW_CLAUDE_PROJECTS_ROOT` (default: `~/projects`) up to depth 4; warns on files >180 lines or version refs that don't match `git describe --tags`; injectable via `FLOW_CLAUDE_PROJECTS_ROOT` +- **C8 orphaned memory dirs**: decodes `~/.claude/projects/` slug names back to filesystem paths (`/a-b-c` β†’ `/a/b/c`); warns on stale dirs whose decoded path no longer exists +- **C9 rules drift**: checks that every `~/.claude/rules/*.md` stem is referenced in `~/.claude/CLAUDE.md`; warns on unreferenced rules +- **C10 missing hook files**: parses `settings.json` hook commands; errors on any absolute-path script that isn't present on disk +- **C11 plugin health**: checks `~/.claude/plugins/*/plugin.json` exists and is valid JSON (skips `cache/` subdir); warns on broken plugins +- **`flow claude watch [--interval N] [--stop] [--status]`**: background health watcher β€” runs `flow claude check` on a configurable interval (default: 30 min), writes state to `~/.flow/claude-health-state.json`, and fires a desktop notification via `terminal-notifier` when status changes between pass/warn/error; gracefully silent on Linux where `terminal-notifier` is absent + +## [7.12.0] β€” 2026-06-19 β€” flow claude check: Claude Code environment health + +### Added + +- **`flow claude check` (C1–C6)** (`commands/claude.zsh`): new environment health command β€” `flow claude check` (alias: `flow claude doctor`) runs six checks and reports status with exit codes `0`=all pass, `1`=any ERROR, `2`=any WARN. + - **C1 Settings parity** β€” `settings.json` `.env` keys vs zshrc exports; warns on missing or mismatched values; auto-fixable with `--fix` + - **C2 Hook health** β€” `post-compact-reinject.sh` exists, is executable, and passes `shellcheck`; ERROR-level + - **C3 Memory index drift** β€” `.md` file count vs `MEMORY.md` entry count per `~/.claude/projects/*/memory` + - **C4 CLAUDE.md length** β€” warns when `~/.claude/CLAUDE.md` exceeds 100 lines (mirrors the `claude-md-length` rule) + - **C5 Shell env parity** β€” reports `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` INFO-level (set or unset) + - **C6 Output token limit** β€” warns when `CLAUDE_CODE_MAX_OUTPUT_TOKENS` unset or ≀ 8192; reads `settings.json` first (jq), falls back to zshrc grep; auto-fixable with `--fix` +- **`flow claude check --fix`**: repairs C1 (sync zshrc to `settings.json` env block) and C6 (append/update `CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000`) without touching `settings.json` +- **Tutorial 49** (`docs/tutorials/49-flow-claude-check.md`): step-by-step guide with `--fix` workflow and all six checks documented +- **`docs/commands/claude.md`**: command reference with check table, exit codes, `--fix` behavior, and dependency notes +- **`docs/troubleshooting/CLAUDE-CODE-ENVIRONMENT.md`**: diagnostic guide using `flow claude check` to triage Claude Code environment issues + +### Fixed + +- **`_flow_claude_fix_c1` sed portability** (`commands/claude.zsh`): replaced `sed -i ''` (BSD-only) with portable temp-file approach for GNU sed compatibility on Linux CI + ## [7.11.0] β€” 2026-06-19 β€” at-dispatcher completions + atlas doctor fixes ### Added diff --git a/docs/commands/claude.md b/docs/commands/claude.md index 7a948f4ac..2f3773a7b 100644 --- a/docs/commands/claude.md +++ b/docs/commands/claude.md @@ -9,10 +9,14 @@ The `flow claude` command inspects the Claude Code environment for settings that ## Synopsis ```bash -flow claude check # Run all checks, print report, exit with status -flow claude check --fix # Run checks + auto-repair safe mismatches -flow claude doctor # Alias for check -flow claude doctor --fix # Alias for check --fix +flow claude check # Run all checks, print report, exit with status +flow claude check --fix # Run checks + auto-repair safe mismatches +flow claude doctor # Alias for check +flow claude doctor --fix # Alias for check --fix +flow claude watch # Start background health daemon (notifies on change) +flow claude watch --interval N # Poll every N seconds (default: 60) +flow claude watch --stop # Stop the daemon +flow claude watch --status # Show daemon state and last check result ``` --- @@ -24,23 +28,33 @@ flow claude doctor --fix # Alias for check --fix | C1 | Settings parity | Each key in `~/.claude/settings.json` `.env` block has a matching `export KEY=VALUE` in zshrc | WARN | | C2 | Hook health | `~/.claude/hooks/post-compact-reinject.sh` exists, is executable, passes `shellcheck` | ERROR | | C3 | Memory index drift | `.md` file count in each `memory/` dir vs entry count in its `MEMORY.md` | WARN | -| C4 | CLAUDE.md length | `~/.claude/CLAUDE.md` line count ≀ 100 (project rule: trim before adding) | WARN | +| C4 | CLAUDE.md length | >100 lines β†’ WARN (approaching 180-line limit); >180 lines β†’ ERROR (hard limit exceeded) | WARN/ERROR | | C5 | Shell env parity | `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` exported in current shell (proxy for env reaching Electron apps) | INFO | | C6 | Output token limit | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` set in settings.json or zshrc and value > 8192 | WARN | +| C7 | Per-project CLAUDE.md | Scans `$FLOW_CLAUDE_PROJECTS_ROOT` (depth 4); warns on >180 lines or stale version refs | WARN | +| C8 | Orphaned memory dirs | Decodes each memory slug back to a filesystem path; warns if the project no longer exists | WARN | +| C9 | Rules drift | Every `~/.claude/rules/*.md` stem must appear in main `~/.claude/CLAUDE.md` | WARN | +| C10 | Missing hook files | Parses `settings.json` hook commands; errors on absent absolute-path scripts | ERROR | +| C11 | Plugin health | Checks `~/.claude/plugins/*/plugin.json` exists and is valid JSON (skips `cache/`) | WARN | --- ## Output Format -``` +```text flow claude check -βœ“ Settings parity AUTOCOMPACT=65 matches in settings.json + zshrc -βœ— Hook health post-compact-reinject.sh: shellcheck failed (line 12) -⚠ Memory index drift ~/.claude/projects/-Users-dt--config/memory/: 8 files, 6 MEMORY.md entries -⚠ CLAUDE.md length 148 lines β€” exceeds 100-line rule (trim before adding) -β„Ή Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 exported in current session -⚠ Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS not set β€” default 8192 cap may truncate responses (run --fix to set 32000) +βœ“ Settings parity AUTOCOMPACT=65 matches in settings.json + zshrc +βœ— Hook health post-compact-reinject.sh: shellcheck failed (line 12) +⚠ Memory index drift ~/.claude/projects/-Users-dt--config/memory/: 8 files, 6 MEMORY.md entries +⚠ CLAUDE.md length 148 lines β€” approaching 180-line hard limit (trim before adding) +β„Ή Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 exported in current session +⚠ Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS not set β€” default 8192 cap may truncate responses (run --fix to set 32000) +⚠ Per-project CLAUDE.md ~/projects/my-app/CLAUDE.md: 205 lines β€” exceeds 180-line limit +⚠ Orphaned memory dirs slug 'users-dt-projects-old-app': /Users/dt/projects/old-app not found +⚠ Rules drift ~/.claude/rules/my-rule.md not referenced in ~/.claude/CLAUDE.md +βœ— Missing hook files settings.json references missing script: /Users/dt/.claude/hooks/on-start.sh +⚠ Plugin health ~/.claude/plugins/myplugin: plugin.json missing or invalid JSON ``` --- @@ -53,7 +67,7 @@ flow claude check |-------|------------------| | C1 | Updates the `export KEY=VALUE` line in zshrc to match the value in settings.json | | C6 | Appends `export CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000` if missing, or updates the value if ≀ 8192 | -| C2, C3, C4, C5 | Reported only β€” no auto-repair | +| C2, C3, C4, C5, C7–C11 | Reported only β€” no auto-repair | **Why settings.json is canonical:** It's what Claude Code reads directly. zshrc exports are a belt-and-suspenders fallback for [issue #63186](https://github.com/anthropics/claude-code/issues/63186) where the env block is silently ignored in some launch paths. `--fix` brings zshrc into alignment with settings.json, never the reverse. @@ -83,12 +97,22 @@ flow claude check || echo "Environment issues detected" **C3 β€” Memory index drift:** `MEMORY.md` is the index Claude reads to decide which memory files to load. If files exist that aren't indexed, they're invisible to Claude. If MEMORY.md lists files that don't exist, the index rots and misleads. -**C4 β€” CLAUDE.md length:** The global rule caps `~/.claude/CLAUDE.md` at 100 lines. Beyond that it starts consuming context on every turn. Every line over 100 is paying a per-turn tax. +**C4 β€” CLAUDE.md length:** The global rule caps `~/.claude/CLAUDE.md` at 180 lines (hard limit) with a warning at 100 lines (approaching limit). Beyond 100 lines it starts consuming meaningful context on every turn β€” every line over that threshold is a per-turn tax. **C5 β€” Shell env parity:** Claude Code Electron apps inherit env from the shell that launched them, not from `settings.json`. This check verifies that `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` is actually in the shell's env β€” a proxy for "will Electron-launched Claude see my settings?" **C6 β€” Output token limit:** Claude Code defaults to 8192 output tokens per response. Long file writes, comprehensive diffs, or detailed analysis can exceed this, interrupting the session with `API Error: Claude's response exceeded the 8192 output token maximum`. Setting `CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000` raises the ceiling. +**C7 β€” Per-project CLAUDE.md:** Project-level `CLAUDE.md` files load into context for every session in that project. An oversized or stale project CLAUDE.md has the same per-turn cost as a bloated global one. C7 scans `$FLOW_CLAUDE_PROJECTS_ROOT` (up to depth 4) and flags any project CLAUDE.md that exceeds 180 lines or contains stale version references. + +**C8 β€” Orphaned memory dirs:** The memory system accumulates slug-encoded project entries under `~/.claude/projects/`. If the underlying project directory no longer exists, the slug is dead weight β€” context space spent on a project that's gone. C8 decodes each slug back to a filesystem path and warns when the path is missing. + +**C9 β€” Rules drift:** `~/.claude/rules/*.md` files are only active when referenced from the global `CLAUDE.md`. A rule file that isn't listed there is silently ignored β€” meaning new rules never take effect without this link. C9 checks that every stem in `rules/` appears in `CLAUDE.md`. + +**C10 β€” Missing hook files:** `settings.json` can declare hook scripts via absolute paths. If those scripts are absent on disk, Claude Code silently skips them β€” missing hooks that were expected to fire. C10 parses the hook declarations and errors if any referenced script doesn't exist. + +**C11 β€” Plugin health:** Plugins require a valid `plugin.json` manifest to be loaded. An invalid or missing manifest means the plugin is silently ignored. C11 validates each installed plugin's manifest before a session starts (skipping the `cache/` subdirectory). + --- ## Manual Fix for C2 (Hook Health) @@ -125,12 +149,40 @@ Recommended value: `32000`. Maximum for Sonnet 4.6: ~64000. --- +## Watch Daemon + +`flow claude watch` runs `flow claude check` on a schedule in the background and sends +a desktop notification (via `terminal-notifier`) when health state changes. + +```bash +flow claude watch # Start daemon (default: 60s interval) +flow claude watch --interval 30 # Poll every 30 seconds +flow claude watch --stop # Stop the daemon +flow claude watch --status # Print daemon PID, uptime, and last check result +``` + +**State files:** + +| File | Purpose | +|------|---------| +| `~/.flow/claude-watch.pid` | Daemon PID (absent when stopped) | +| `~/.flow/claude-health-state.json` | Last check result + severity | + +The daemon only notifies on state **transitions** (healthy β†’ degraded, or degraded β†’ +healthy), not on every poll. This avoids notification spam when the environment is stable. + +**Requires:** `terminal-notifier` (`brew install terminal-notifier`). The daemon starts +but skips desktop notifications if `terminal-notifier` is absent. + +--- + ## Dependencies | Tool | Required by | Behavior if absent | |------|-------------|-------------------| -| `jq` | C1, C6 (settings.json parsing) | Skips JSON checks, reports "jq required" | +| `jq` | C1, C6, C10 (JSON parsing) | Skips JSON checks, reports "jq required" | | `shellcheck` | C2 (hook validation) | Reports existence + executable only, skips lint | +| `terminal-notifier` | watch daemon | Daemon runs; desktop notifications silently skipped | --- @@ -143,5 +195,5 @@ Recommended value: `32000`. Maximum for Sonnet 4.6: ~64000. --- **Last Updated:** 2026-06-19 -**Version:** v7.12.0 +**Version:** v7.13.0 **Status:** Implemented in `commands/claude.zsh` diff --git a/docs/guides/TESTING.md b/docs/guides/TESTING.md index 0174f6286..1847ea2c2 100644 --- a/docs/guides/TESTING.md +++ b/docs/guides/TESTING.md @@ -23,7 +23,7 @@ flow-cli uses a **shared test framework** (`tests/test-framework.zsh`) with comp | Metric | Count | |--------|-------| -| Test files | 218 | +| Test files | 219 | | Test suites (run-all.sh) | 67 total β€” 66 passed, 1 skipped, 0 failed | | Test functions | 12,000+ | | Expected skips | 1 (`e2e-em-dispatcher` β€” needs configured IMAP account) | @@ -408,4 +408,4 @@ When adding new functionality: **Established:** v5.0.0 (2026-01-11) **Overhauled:** v7.4.0 (2026-02-16) β€” shared framework, mock registry, dogfood scanner -**Test Count:** 218 test files, 12000+ assertions, 66/66 suites passing +**Test Count:** 219 test files, 12000+ assertions, 67/67 suites passing diff --git a/docs/reference/MASTER-DISPATCHER-GUIDE.md b/docs/reference/MASTER-DISPATCHER-GUIDE.md index 03d2efd84..0b6fa36a2 100644 --- a/docs/reference/MASTER-DISPATCHER-GUIDE.md +++ b/docs/reference/MASTER-DISPATCHER-GUIDE.md @@ -3465,6 +3465,6 @@ at stats --- -**Version:** v7.12.0 +**Version:** v7.13.0 **Last Updated:** 2026-06-19 **Total:** 14 dispatchers + at bridge + claude command fully documented diff --git a/docs/specs/PROPOSAL-flow-claude-c7-watch.md b/docs/specs/PROPOSAL-flow-claude-c7-watch.md new file mode 100644 index 000000000..0b170e6a4 --- /dev/null +++ b/docs/specs/PROPOSAL-flow-claude-c7-watch.md @@ -0,0 +1,142 @@ +# PROPOSAL: flow claude C7 + --watch + +**Date:** 2026-06-19 +**Context:** Extensions to `flow claude check` (shipped in v7.12.0) + +--- + +## Feature 1: C7 β€” Per-project CLAUDE.md audit + +### What + +C4 already checks `~/.claude/CLAUDE.md` (global, length only). +C7 scans **all project-level CLAUDE.md files** under `~/projects/**` for three +degradation signals: + +| Sub-check | Signal | Severity | +|-----------|--------|----------| +| C7a Line count | > 180 lines (global rule = 180; project files creep too) | WARN | +| C7b Version drift | Hardcoded `vX.Y.Z` strings that don't match the project's current version | WARN | +| C7c Last-updated staleness | File not touched in > 90 days while the project has recent git activity | INFO | + +### Scope + +- Discovery: `find ~/projects -maxdepth 3 -name "CLAUDE.md"` (skip `.git/`) +- ~40 files currently; scan is fast (wc/grep, no external tools) +- injectable via `FLOW_CLAUDE_PROJECTS_ROOT` for tests +- Default: skip dirs matching `/.git/` and `/node_modules/` +- Version check: only fires if the CLAUDE.md is inside a git repo and + `git describe --tags --abbrev=0` succeeds; compares against any `vX.Y.Z` + pattern in the file + +### Output + +``` +β„Ή C7 Project CLAUDE.md scanned 40 files +βœ“ C7 Project CLAUDE.md 38 clean +⚠ C7 Project CLAUDE.md 2 issues: + craft/CLAUDE.md: 203 lines (> 180) + flow-cli/CLAUDE.md: version ref v7.11.0 (current: v7.12.0) +``` + +### Complexity: Medium +- Pure ZSH + grep/wc β€” no new deps +- Needs a `_flow_find_project_claude_mds` helper (reusable) +- Version extraction from CLAUDE.md: grep `v[0-9]+\.[0-9]+\.[0-9]+` patterns +- Test: inject `FLOW_CLAUDE_PROJECTS_ROOT` pointing to a fixture dir + +--- + +## Feature 2: `flow claude watch` + +### What + +Background daemon that runs the full C1–C7 suite on a timer, notifies on state +change (pass β†’ warn/error or vice versa), and stores last-known state so the +next `flow claude check` can show a diff. + +### Design + +``` +flow claude watch [--interval N] # start watcher (default: 30 min) +flow claude watch --stop # kill watcher +flow claude watch --status # show watcher PID + last check result +``` + +State file: `~/.flow/claude-health-state.json` + +```json +{ + "last_check": "2026-06-19T17:30:00Z", + "result": "warn", + "checks": { + "C1": "pass", "C2": "pass", "C3": "warn", + "C4": "pass", "C5": "pass", "C6": "pass", "C7": "pass" + } +} +``` + +Notification (macOS via `terminal-notifier`): + +``` +terminal-notifier \ + -title "flow claude" \ + -subtitle "Health degraded" \ + -message "C3 Memory index drift β€” testproj: 3 files, 2 entries" \ + -sound default +``` + +Notification only fires on **state change** (passβ†’warn, warnβ†’error, anyβ†’pass). +Silent poll otherwise β€” no notification spam. + +Daemon lifecycle: +- PID stored in `~/.flow/claude-watch.pid` +- `flow claude watch` checks for existing PID before starting +- Watcher runs `_flow_claude_check` in a subshell; output goes to + `~/.flow/claude-watch.log` (last 100 lines kept via tail) +- On `--stop`: `kill $(cat ~/.flow/claude-watch.pid)` + +### Complexity: Medium-high +- ZSH background process management (`&`, `disown`) +- PID file lifecycle (stale PID detection via `kill -0`) +- `terminal-notifier` dep (already installed; graceful fallback to `print`) +- No new external deps on Linux CI (guard with `command -v terminal-notifier`) +- Tests: mock `_flow_claude_check` exit codes, assert state file transitions + +--- + +## Implementation Plan + +### Quick Wins (< 30 min) +1. **C7a line count only** β€” 10-line addition to `_flow_claude_check`; reuses + `wc -l` pattern from C4; injectable root via env var; single test case + +### Medium Effort (2–3 hrs each) +- [ ] **C7 full** (a+b+c) β€” discovery helper, version extraction, staleness check, + injectable fixture, 6–8 tests +- [ ] **`flow claude watch` core** β€” background loop, PID file, state JSON, + notifier integration, `--stop`/`--status` subcommands, 4–5 tests + +### Long-term (future) +- [ ] `flow claude watch --on-fix` β€” auto-run `--fix` when state degrades on + fixable checks (C1, C6) +- [ ] `precmd` hook variant β€” check on every new shell prompt (1-second cached + check, badge in prompt if degraded) +- [ ] `flow doctor` integration β€” expose C7 + watch status in the full doctor + output + +## Recommended Next Step + +β†’ **C7a first** (line count across all project CLAUDE.md files) β€” 30-min win, +ships in the next patch, gives immediate value and proves the discovery helper +before adding version/staleness checks. Then C7b+c as a follow-on. Watch is +a separate worktree feature. + +--- + +**Scope decision needed before starting:** + +1. C7: scope to `~/projects/**` only, or also scan `~/.claude/projects/*/` memory + CLAUDE.md files? (Memory ones rarely have version refs β€” probably skip.) +2. Watch interval: 30 min default, or configurable only (no default)? +3. Watch notifier: notification-center only, or also `osascript` speech fallback? diff --git a/docs/specs/SPEC-flow-claude-v2.md b/docs/specs/SPEC-flow-claude-v2.md new file mode 100644 index 000000000..e9d8e6df8 --- /dev/null +++ b/docs/specs/SPEC-flow-claude-v2.md @@ -0,0 +1,357 @@ +# SPEC: flow claude v2 β€” Extended Checks + Watch + +**Version target:** v7.13.0 +**Branch:** `feature/flow-claude-v2` from `dev` +**Date:** 2026-06-19 + +--- + +## Summary + +Extends `flow claude check` with four new checks (C4 two-tier, C7–C10) and adds +`flow claude watch` as a background health daemon with `terminal-notifier` alerts. +Ships as a single PR. + +--- + +## Check Changes + +### C4 (existing): CLAUDE.md length β€” fix threshold + +**Current:** warns at `> 100` lines +**New:** two-tier + +| Threshold | Severity | Message | +|-----------|----------|---------| +| > 100 | WARN | `$n lines β€” approaching 180-line limit (trim before adding)` | +| > 180 | ERROR | `$n lines β€” exceeds 180-line hard limit (see ~/.claude/rules/claude-md-length.md)` | + +File: `commands/claude.zsh` in `_flow_claude_check`, C4 block. + +--- + +## New Checks + +### C7: Per-project CLAUDE.md audit + +**What:** Scans all project-level CLAUDE.md files under `~/projects/**` for: + +| Sub-check | Signal | Severity | +|-----------|--------|----------| +| C7a | Line count > 180 | WARN | +| C7b | `vX.Y.Z` string in file doesn't match `git describe --tags --abbrev=0` | WARN | + +**Discovery:** +```zsh +find "${FLOW_CLAUDE_PROJECTS_ROOT:-$HOME/projects}" \ + -maxdepth 4 -name "CLAUDE.md" \ + -not -path "*/.git/*" \ + -not -path "*/node_modules/*" +``` + +**Version check guard:** only fires if `git -C "$proj_dir" describe --tags --abbrev=0 2>/dev/null` succeeds. Skip silently if no tags. + +**Version extraction:** `grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' "$file"` β€” compares each match against the git tag. If any match differs, it's drift. + +**Injectable env:** `FLOW_CLAUDE_PROJECTS_ROOT` for tests. + +**Output example:** +``` +⚠ C7 Project CLAUDE.md 3 issues (40 files scanned): + flow-cli/CLAUDE.md: 380 lines (> 180) + craft/CLAUDE.md: 203 lines (> 180) + flow-cli/CLAUDE.md: version ref v7.11.0 (current: v7.12.0) +βœ“ C7 Project CLAUDE.md 37 clean +``` + +--- + +### C8: Orphaned memory dirs + +**What:** Checks `~/.claude/projects/` for dirs whose corresponding project path +no longer exists on disk. The dir name is a path slug (e.g., +`-Users-dt-projects-dev-tools-flow-cli`) β€” decode by replacing `-` with `/` +(leading `-` β†’ `/`). + +**Decode logic:** +```zsh +slug="${dir##*/}" # e.g. -Users-dt-projects-dev-tools-flow-cli +path="/${slug//-//}" # /Users/dt/projects/dev-tools/flow-cli +path="${path// //}" # collapse any double-slash artifacts +``` + +Flag if `[[ ! -d "$path" ]]`. + +**Severity:** WARN (stale data, not broken behavior) + +**Output example:** +``` +⚠ C8 Orphaned memory 1 stale dir: + -Users-dt-old-project (path /Users/dt/old-project not found) +βœ“ C8 Orphaned memory 12 dirs all valid +``` + +--- + +### C9: Rules drift + +**What:** For each `.md` file in `~/.claude/rules/`, checks whether its stem +(filename without `.md`) appears anywhere in `~/.claude/CLAUDE.md`. + +**Logic:** +```zsh +for rule_file in "$claude_home/rules"/*.md(N); do + stem="${rule_file:t:r}" # e.g. doc-update-currency-check + if ! grep -qF "$stem" "$claude_home/CLAUDE.md" 2>/dev/null; then + unreferenced+=("$stem") + fi +done +``` + +**Severity:** WARN + +**Output example:** +``` +⚠ C9 Rules drift 1 unreferenced rule: + my-old-rule (not mentioned in CLAUDE.md) +βœ“ C9 Rules drift 14 rules all referenced +``` + +--- + +### C10: Missing hook files + +**What:** Reads `hooks` array from `~/.claude/settings.json` via `jq`, checks +each hook's `command` field for a file path β€” if the path is a script (starts +with `/` or `~`), verify it exists. + +**jq extraction:** +```zsh +jq -r '(.hooks // {}) | to_entries[] | .value[] | .command' "$settings_json" 2>/dev/null +``` + +Flag paths that start with `/` or `~` and don't exist as files. + +**Severity:** ERROR (missing hook = silent breakage) + +**Requires:** `jq` (same guard as C1) + +**Output example:** +``` +βœ— C10 Hook files 1 missing: + /Users/dt/.claude/hooks/my-hook.sh (defined in settings.json, not found) +βœ“ C10 Hook files 5 hooks all present +``` + +--- + +### C11: Plugin health + +**What:** For each directory under `~/.claude/plugins/` (excluding `cache/`), +check that `plugin.json` exists and is valid JSON. + +```zsh +for plugin_dir in "$claude_home/plugins"/*(N/); do + [[ "${plugin_dir:t}" == "cache" ]] && continue + pjson="$plugin_dir/plugin.json" + if [[ ! -f "$pjson" ]]; then + broken+=("${plugin_dir:t}: missing plugin.json") + elif ! jq empty "$pjson" 2>/dev/null; then + broken+=("${plugin_dir:t}: invalid JSON in plugin.json") + fi +done +``` + +**Severity:** WARN + +--- + +## `flow claude watch` + +### Subcommand routing + +```zsh +case "$subcmd" in + watch) _flow_claude_watch "$@" ;; +``` + +### Interface + +``` +flow claude watch # start watcher (30-min default) +flow claude watch --interval N # start with N-minute interval +flow claude watch --stop # kill watcher +flow claude watch --status # PID + last check result + time since last run +``` + +### State file: `~/.flow/claude-health-state.json` + +```json +{ + "pid": 12345, + "interval": 1800, + "last_check": "2026-06-19T17:30:00Z", + "result": "warn", + "checks": { + "C1": "pass", "C2": "pass", "C3": "warn", + "C4": "pass", "C5": "info", "C6": "pass", + "C7": "warn", "C8": "pass", "C9": "pass", + "C10": "pass", "C11": "pass" + } +} +``` + +### Daemon lifecycle + +```zsh +_flow_claude_watch_start() { + local interval=$(( ${1:-30} * 60 )) + local pid_file="$HOME/.flow/claude-watch.pid" + local state_file="$HOME/.flow/claude-health-state.json" + local log_file="$HOME/.flow/claude-watch.log" + + # Stale PID check + if [[ -f "$pid_file" ]]; then + local old_pid=$(< "$pid_file") + if kill -0 "$old_pid" 2>/dev/null; then + _flow_log_warning "watch already running (PID $old_pid) β€” use --stop first" + return 1 + fi + rm -f "$pid_file" + fi + + # Launch background loop + ( + print $$ > "$pid_file" + while true; do + _flow_claude_watch_run_check "$state_file" >> "$log_file" 2>&1 + tail -c 50000 "$log_file" > "${log_file}.tmp" && mv "${log_file}.tmp" "$log_file" + sleep "$interval" + done + ) & + disown $! + _flow_log_success "watch started (PID $!, interval ${1:-30}m)" +} +``` + +### Notification logic + +Only notify on **state change at WARN or ERROR level**. C5 (`info`) never triggers. + +```zsh +_flow_claude_watch_notify() { + local prev_result="$1" # pass/warn/error + local new_result="$2" + local summary="$3" + + [[ "$prev_result" == "$new_result" ]] && return # no change, silent + + # Only notify if new or old state is warn/error (ignore info↔pass) + if [[ "$new_result" == "pass" && "$prev_result" == "pass" ]]; then return; fi + + local title="flow claude" + local subtitle message + if [[ "$new_result" == "pass" ]]; then + subtitle="Health restored" + message="All checks passing" + elif [[ "$new_result" == "error" ]]; then + subtitle="Health degraded β€” ERROR" + message="$summary" + else + subtitle="Health warning" + message="$summary" + fi + + if command -v terminal-notifier &>/dev/null; then + terminal-notifier -title "$title" -subtitle "$subtitle" \ + -message "$message" -sound default + fi + # Silent fallback on Linux (no terminal-notifier) +} +``` + +### `--status` output + +``` +● flow claude watch running (PID 12345, interval 30m) + Last check: 4 min ago β€” WARN + C3 Memory index drift, C7 Project CLAUDE.md (2 issues) +``` + +If not running: +``` +β—‹ flow claude watch not running + Last check: 2026-06-19 17:30 β€” WARN (watcher was stopped) +``` + +--- + +## Help text additions + +``` + flow claude watch Start background health watcher (30-min default) + flow claude watch --stop Stop watcher + flow claude watch --status Show watcher status + last result + flow claude watch --interval N Set poll interval in minutes + +Checks: + C7 Project CLAUDE.md per-project line count + version drift + C8 Orphaned memory ~/.claude/projects/ dirs for deleted projects + C9 Rules drift ~/.claude/rules/*.md files not cited in CLAUDE.md + C10 Hook files hooks in settings.json pointing to missing scripts + C11 Plugin health ~/.claude/plugins/ dirs missing valid plugin.json +``` + +--- + +## Files to change + +| File | Change | +|------|--------| +| `commands/claude.zsh` | C4 two-tier, C7–C11, `watch` subcommand + helpers | +| `completions/_flow_claude` | add `watch`, `--interval`, `--stop`, `--status` | +| `man/man1/flow.1` | document new subcommand + checks | +| `tests/test-flow-claude.zsh` | new test cases for C4 two-tier, C7–C11, watch | + +--- + +## Test cases (new) + +| Test | What | +|------|------| +| C4: 95 lines β†’ pass | under both thresholds | +| C4: 150 lines β†’ warn | over 100, under 180 | +| C4: 200 lines β†’ error | over 180 | +| C7: fixture with 200-line CLAUDE.md β†’ warn | line count breach | +| C7: fixture with stale version tag β†’ warn | version drift (mock git describe) | +| C7: no git tags β†’ skip version check | no false positive | +| C8: slug decodes to missing dir β†’ warn | orphan detection | +| C8: slug decodes to valid dir β†’ pass | no false positive | +| C9: rule stem missing from CLAUDE.md β†’ warn | unreferenced rule | +| C9: all rules referenced β†’ pass | clean | +| C10: hook path missing β†’ error | missing file | +| C10: no hooks in settings β†’ pass | skip gracefully | +| C11: plugin missing plugin.json β†’ warn | broken plugin | +| C11: plugin has invalid JSON β†’ warn | parse error | +| watch --stop: kills PID, removes pid file | lifecycle | +| watch --status: not running β†’ clean message | no crash | +| watch notify: warnβ†’pass fires notifier | state change | +| watch notify: passβ†’pass silent | no spam | + +--- + +## Implementation order + +1. C4 two-tier fix (minimal, safe) +2. C8 orphaned memory (pure ZSH, no new deps) +3. C9 rules drift (pure ZSH) +4. C10 missing hooks (needs jq, same guard as C1) +5. C11 plugin health (needs jq) +6. C7 per-project scan (needs `_flow_find_project_claude_mds` helper + git calls) +7. `flow claude watch` daemon (background process management) +8. Tests for all above +9. Help text + man page + completions + +--- + +**Status:** SPEC ONLY β€” no implementation diff --git a/docs/tutorials/49-flow-claude-check.md b/docs/tutorials/49-flow-claude-check.md index afb41afaa..9c5615946 100644 --- a/docs/tutorials/49-flow-claude-check.md +++ b/docs/tutorials/49-flow-claude-check.md @@ -8,17 +8,17 @@ tags: # Tutorial 49: Diagnosing Claude Code Environment with `flow claude check` -> **What you'll learn:** how to catch the six most common Claude Code configuration -> problems before they silently derail a session β€” and how to auto-fix the ones -> that are safe to fix. +> **What you'll learn:** how to catch eleven Claude Code configuration problems before +> they silently derail a session β€” and how to auto-fix the ones that are safe to fix. +> Includes the `flow claude watch` daemon for continuous background monitoring. > -> **Time:** ~10 minutes | **Level:** Beginner | **v7.12.0** +> **Time:** ~15 minutes | **Level:** Beginner | **v7.13.0** **Why this matters:** Claude Code configuration lives in two places (`settings.json` -and your shell), and they drift. Hook files break. Memory indexes go stale. The -default 8192-token output cap truncates long responses mid-run. None of these fail -loudly β€” they just produce confusing behavior. `flow claude check` finds them all in -one pass. +and your shell), and they drift. Hook files break. Memory indexes go stale. Project +CLAUDE.md files balloon. The default 8192-token output cap truncates long responses +mid-run. None of these fail loudly β€” they just produce confusing behavior. +`flow claude check` finds them all in one pass. --- @@ -40,9 +40,10 @@ flow claude check --help ## What You'll Learn 1. Run `flow claude check` and read the report -2. Understand each of the six checks (C1–C6) +2. Understand all eleven checks (C1–C11) 3. Use `--fix` to auto-repair safe mismatches 4. Know which failures require manual intervention +5. Run `flow claude watch` for continuous background monitoring --- @@ -52,15 +53,20 @@ flow claude check --help flow claude check ``` -You'll see a six-line report. Each line is one check: - -``` -βœ“ Settings parity AUTOCOMPACT=65 matches in settings.json + zshrc -βœ— Hook health post-compact-reinject.sh: shellcheck failed (line 12) -⚠ Memory index drift ~/.claude/projects/-Users-dt--config/memory/: 8 files, 6 MEMORY.md entries -⚠ CLAUDE.md length 148 lines β€” exceeds 100-line rule (trim before adding) -β„Ή Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 exported in current session -⚠ Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS not set β€” default 8192 cap may truncate responses (run --fix to set 32000) +You'll see an eleven-line report. Each line is one check: + +```text +βœ“ Settings parity AUTOCOMPACT=65 matches in settings.json + zshrc +βœ— Hook health post-compact-reinject.sh: shellcheck failed (line 12) +⚠ Memory index drift ~/.claude/projects/-Users-dt--config/memory/: 8 files, 6 MEMORY.md entries +⚠ CLAUDE.md length 148 lines β€” approaching 180-line hard limit (trim before adding) +β„Ή Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 exported in current session +⚠ Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS not set β€” default 8192 cap may truncate responses +⚠ Per-project CLAUDE.md ~/projects/my-app/CLAUDE.md: 205 lines β€” exceeds 180-line limit +⚠ Orphaned memory dirs slug 'users-dt-projects-old-app': /Users/dt/projects/old-app not found +⚠ Rules drift ~/.claude/rules/my-rule.md not referenced in ~/.claude/CLAUDE.md +βœ— Missing hook files settings.json references missing: /Users/dt/.claude/hooks/on-start.sh +⚠ Plugin health ~/.claude/plugins/myplugin: plugin.json missing or invalid JSON ``` **Reading the symbols:** @@ -74,7 +80,7 @@ You'll see a six-line report. Each line is one check: --- -## Step 2: Understand the Six Checks +## Step 2: Understand All Eleven Checks ### C1 β€” Settings Parity (WARN) @@ -88,7 +94,7 @@ both locations agree. **Example failure:** -``` +```text ⚠ Settings parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: settings.json=65, zshrc=missing ``` @@ -103,7 +109,7 @@ drops your context. **Example failure:** -``` +```text βœ— Hook health post-compact-reinject.sh: not executable ``` @@ -116,19 +122,23 @@ index rots. **Example failure:** -``` +```text ⚠ Memory index drift memory/: 8 .md files, 6 MEMORY.md entries (2 unindexed) ``` -### C4 β€” CLAUDE.md Length (WARN) +### C4 β€” CLAUDE.md Length (WARN / ERROR) -The global rule caps `~/.claude/CLAUDE.md` at 100 lines. Every line loads into -context on every turn β€” so a bloated CLAUDE.md is a per-turn tax you pay forever. +The global rule sets two thresholds for `~/.claude/CLAUDE.md`: -**Example failure:** +- **>100 lines β†’ WARN** β€” "approaching 180-line hard limit". Start trimming. +- **>180 lines β†’ ERROR** β€” hard limit exceeded. Every line loads into context on every + turn, so a bloated CLAUDE.md is a per-turn tax you pay forever. -``` -⚠ CLAUDE.md length 148 lines β€” exceeds 100-line rule (trim before adding) +**Example failures:** + +```text +⚠ CLAUDE.md length 148 lines β€” approaching 180-line hard limit (trim before adding) +βœ— CLAUDE.md length 195 lines β€” exceeds 180-line hard limit ``` ### C5 β€” Shell Env Parity (INFO) @@ -146,7 +156,7 @@ Claude Code defaults to 8192 output tokens per response. When a task requires a response β€” writing a large file, generating a comprehensive diff, explaining a complex system β€” it hits this cap mid-output and errors with: -``` +```text API Error: Claude's response exceeded the 8192 output token maximum. To configure this behavior, set the CLAUDE_CODE_MAX_OUTPUT_TOKENS environment variable. ``` @@ -156,10 +166,77 @@ and that its value is greater than 8192. **Example failure:** -``` +```text ⚠ Output token limit CLAUDE_CODE_MAX_OUTPUT_TOKENS not set β€” default 8192 cap may truncate responses ``` +### C7 β€” Per-project CLAUDE.md (WARN) + +Project-level `CLAUDE.md` files load into context for every session in that project β€” +same per-turn cost as the global one. C7 scans `$FLOW_CLAUDE_PROJECTS_ROOT` (up to +depth 4) and flags any project CLAUDE.md that exceeds 180 lines or contains stale +version strings that no longer match the running flow-cli version. + +**Example failure:** + +```text +⚠ Per-project CLAUDE.md ~/projects/my-app/CLAUDE.md: 205 lines β€” exceeds 180-line limit +``` + +### C8 β€” Orphaned Memory Dirs (WARN) + +The memory system stores per-project history under slug-encoded paths in +`~/.claude/projects/`. If a project directory was deleted or moved, its slug is dead +weight β€” context space spent loading history for a project that's gone. + +C8 decodes each slug back to a filesystem path (reversing the `-` β†’ `/` encoding) and +warns when the decoded path doesn't exist on disk. + +**Example failure:** + +```text +⚠ Orphaned memory dirs slug 'users-dt-projects-old-app': /Users/dt/projects/old-app not found +``` + +### C9 β€” Rules Drift (WARN) + +`~/.claude/rules/*.md` files are only active when referenced from `~/.claude/CLAUDE.md`. +A rule file that isn't listed there is silently ignored β€” meaning the rule never takes +effect even though the file exists. + +C9 checks that every stem in `rules/` appears in `CLAUDE.md`. + +**Example failure:** + +```text +⚠ Rules drift ~/.claude/rules/my-new-rule.md not referenced in ~/.claude/CLAUDE.md +``` + +### C10 β€” Missing Hook Files (ERROR) + +`settings.json` can declare hook scripts via absolute paths. If those scripts don't +exist on disk, Claude Code silently skips them. C10 parses the hook declarations from +`settings.json` and errors for any referenced script that is absent. + +**Example failure:** + +```text +βœ— Missing hook files settings.json references missing: /Users/dt/.claude/hooks/on-start.sh +``` + +### C11 β€” Plugin Health (WARN) + +Plugins require a valid `plugin.json` manifest to load. An invalid or missing manifest +means the plugin is silently skipped. C11 checks each directory under +`~/.claude/plugins/` (excluding the `cache/` subdirectory) for a readable, valid-JSON +`plugin.json`. + +**Example failure:** + +```text +⚠ Plugin health ~/.claude/plugins/myplugin: plugin.json missing or invalid JSON +``` + --- ## Step 3: Auto-Fix What's Safe @@ -182,13 +259,18 @@ only), hook scripts, MEMORY.md, or CLAUDE.md. **Example output after `--fix`:** -``` -βœ“ Settings parity Fixed: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE aligned in zshrc -βœ— Hook health post-compact-reinject.sh: not executable (manual fix needed) -⚠ Memory index drift memory/: 8 .md files, 6 MEMORY.md entries (2 unindexed) -⚠ CLAUDE.md length 148 lines β€” exceeds 100-line rule -β„Ή Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 exported -βœ“ Output token limit Fixed: CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000 added to zshrc +```text +βœ“ Settings parity Fixed: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE aligned in zshrc +βœ— Hook health post-compact-reinject.sh: not executable (manual fix needed) +⚠ Memory index drift memory/: 8 .md files, 6 MEMORY.md entries (2 unindexed) +⚠ CLAUDE.md length 148 lines β€” approaching 180-line hard limit +β„Ή Shell env parity CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 exported +βœ“ Output token limit Fixed: CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000 added to zshrc +⚠ Per-project CLAUDE.md ~/projects/my-app/CLAUDE.md: 205 lines (manual fix needed) +⚠ Orphaned memory dirs slug 'users-dt-projects-old-app' orphaned (manual cleanup) +⚠ Rules drift my-new-rule.md not referenced (add to CLAUDE.md manually) +βœ— Missing hook files /Users/dt/.claude/hooks/on-start.sh missing (manual fix) +⚠ Plugin health myplugin: plugin.json invalid (manual fix needed) ``` After `--fix`, reload your shell: `source ~/.config/zsh/.zshrc` @@ -243,7 +325,42 @@ Recommended value: `32000`. Maximum for Sonnet 4.6: ~64000. --- -## Step 5: Use the Exit Code in Scripts +## Step 5: Monitor Continuously with the Watch Daemon + +Instead of running `flow claude check` manually, you can run it as a background daemon +that watches your environment and sends a desktop notification when health state +changes. + +```bash +# Start the daemon (polls every 60 seconds by default) +flow claude watch + +# Check if it's running + see last result +flow claude watch --status + +# Adjust poll interval (e.g., every 2 minutes) +flow claude watch --interval 120 + +# Stop the daemon +flow claude watch --stop +``` + +**How notifications work:** The daemon only notifies on *state transitions* β€” when the +environment goes from healthy to degraded, or degraded back to healthy. It won't +spam you with a notification every 60 seconds when nothing changes. + +**Requires** `terminal-notifier` for desktop notifications: + +```bash +brew install terminal-notifier +``` + +If `terminal-notifier` isn't installed, the daemon still runs and checks β€” it just +skips the desktop notification step. + +--- + +## Step 6: Use the Exit Code in Scripts `flow claude check` exits with a meaningful status code: @@ -268,7 +385,7 @@ flow claude check; if [[ $? -eq 1 ]]; then echo "⚠ Fix hook before proceeding" ## Quick Reference ```bash -# Run all checks +# Run all 11 checks flow claude check # Run + auto-repair C1 and C6 @@ -280,6 +397,12 @@ flow claude doctor --fix # Reload shell after --fix source ~/.config/zsh/.zshrc + +# Watch daemon +flow claude watch # Start (60s interval) +flow claude watch --interval 30 # Custom interval +flow claude watch --status # Check daemon state +flow claude watch --stop # Stop daemon ``` --- @@ -294,4 +417,4 @@ source ~/.config/zsh/.zshrc --- **Last Updated:** 2026-06-19 -**Version:** v7.12.0 +**Version:** v7.13.0 diff --git a/flow.plugin.zsh b/flow.plugin.zsh index 0b19ff045..29b82274c 100644 --- a/flow.plugin.zsh +++ b/flow.plugin.zsh @@ -186,7 +186,7 @@ _flow_plugin_init # Export loaded marker export FLOW_PLUGIN_LOADED=1 -export FLOW_VERSION="7.12.0" +export FLOW_VERSION="7.13.0" # Register exit hook for plugin cleanup add-zsh-hook zshexit _flow_plugin_cleanup diff --git a/man/man1/agenda.1 b/man/man1/agenda.1 index a20a96cde..e472fa3d4 100644 --- a/man/man1/agenda.1 +++ b/man/man1/agenda.1 @@ -1,6 +1,6 @@ .\" Man page for the agenda command (forward-looking schedule view) .\" Updated: June 2026 -.TH AGENDA 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH AGENDA 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME agenda \- forward-looking schedule across all projects .SH SYNOPSIS diff --git a/man/man1/at.1 b/man/man1/at.1 index 88a00c6c4..9e7938e51 100644 --- a/man/man1/at.1 +++ b/man/man1/at.1 @@ -1,6 +1,6 @@ .\" Man page for at dispatcher (Atlas bridge) .\" Generated: June 2026 -.TH AT 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH AT 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME at \- Atlas project intelligence bridge (optional integration) .SH SYNOPSIS diff --git a/man/man1/cc.1 b/man/man1/cc.1 index a42f74e19..b8ba7636a 100644 --- a/man/man1/cc.1 +++ b/man/man1/cc.1 @@ -1,6 +1,6 @@ .\" Man page for cc dispatcher (Claude Code launcher) .\" Generated: June 2026 -.TH CC 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH CC 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME cc \- Claude Code launcher and dispatcher .SH SYNOPSIS diff --git a/man/man1/dash.1 b/man/man1/dash.1 index 72ed269db..5086ed4ad 100644 --- a/man/man1/dash.1 +++ b/man/man1/dash.1 @@ -1,6 +1,6 @@ .\" Man page for the dash command (project dashboard) .\" Updated: June 2026 -.TH DASH 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH DASH 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME dash \- ADHD-friendly project dashboard .SH SYNOPSIS diff --git a/man/man1/dots.1 b/man/man1/dots.1 index ce85dbdfd..4b1e79820 100644 --- a/man/man1/dots.1 +++ b/man/man1/dots.1 @@ -1,6 +1,6 @@ .\" Man page for dots dispatcher (Dotfile Management) .\" Generated: June 2026 -.TH DOTS 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH DOTS 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME dots \- Dotfile management dispatcher (chezmoi wrapper) .SH SYNOPSIS diff --git a/man/man1/em.1 b/man/man1/em.1 index 12d534501..609e1464a 100644 --- a/man/man1/em.1 +++ b/man/man1/em.1 @@ -1,6 +1,6 @@ .\" Man page for em dispatcher (Email / himalaya) .\" Generated: June 2026 -.TH EM 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH EM 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME em \- Email dispatcher (himalaya wrapper) .SH SYNOPSIS diff --git a/man/man1/flow-claude.1 b/man/man1/flow-claude.1 index 53f3e1a04..83cf00e18 100644 --- a/man/man1/flow-claude.1 +++ b/man/man1/flow-claude.1 @@ -1,4 +1,4 @@ -.TH FLOW-CLAUDE 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH FLOW-CLAUDE 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME flow-claude \- Claude Code environment health checker .SH SYNOPSIS diff --git a/man/man1/flow.1 b/man/man1/flow.1 index 339f13b1e..64d5d71f6 100644 --- a/man/man1/flow.1 +++ b/man/man1/flow.1 @@ -1,6 +1,6 @@ .\" Man page for flow command .\" Updated: June 2026 -.TH FLOW 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH FLOW 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME flow \- ADHD-friendly workflow CLI for developers .SH SYNOPSIS diff --git a/man/man1/g.1 b/man/man1/g.1 index 7031e7613..f250f91ee 100644 --- a/man/man1/g.1 +++ b/man/man1/g.1 @@ -1,6 +1,6 @@ .\" Man page for g dispatcher (Git workflows) .\" Updated: June 2026 -.TH G 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH G 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME g \- Git commands dispatcher .SH SYNOPSIS diff --git a/man/man1/mcp.1 b/man/man1/mcp.1 index 51f0f4cc2..fc534eaf9 100644 --- a/man/man1/mcp.1 +++ b/man/man1/mcp.1 @@ -1,6 +1,6 @@ .\" Man page for mcp dispatcher (MCP server management) .\" Updated: June 2026 -.TH MCP 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH MCP 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME mcp \- MCP server management dispatcher .SH SYNOPSIS diff --git a/man/man1/morning.1 b/man/man1/morning.1 index 5d31f2f10..2a015da94 100644 --- a/man/man1/morning.1 +++ b/man/man1/morning.1 @@ -1,6 +1,6 @@ .\" Man page for the morning command (daily startup routine) .\" Updated: June 2026 -.TH MORNING 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH MORNING 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME morning \- ADHD-friendly daily startup routine .SH SYNOPSIS diff --git a/man/man1/prompt.1 b/man/man1/prompt.1 index 85bb2a4e9..c518cf98b 100644 --- a/man/man1/prompt.1 +++ b/man/man1/prompt.1 @@ -1,6 +1,6 @@ .\" Man page for prompt dispatcher (Prompt Engine Switcher) .\" Generated: June 2026 -.TH PROMPT 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH PROMPT 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME prompt \- Prompt engine switcher .SH SYNOPSIS diff --git a/man/man1/qu.1 b/man/man1/qu.1 index 127be15bd..3662d8fa5 100644 --- a/man/man1/qu.1 +++ b/man/man1/qu.1 @@ -1,6 +1,6 @@ .\" Man page for qu dispatcher (Quarto publishing) .\" Updated: June 2026 -.TH QU 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH QU 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME qu \- Quarto publishing dispatcher .SH SYNOPSIS diff --git a/man/man1/r.1 b/man/man1/r.1 index 8a6f98617..585c53f46 100644 --- a/man/man1/r.1 +++ b/man/man1/r.1 @@ -1,6 +1,6 @@ .\" Man page for r dispatcher (R package development) .\" Updated: June 2026 -.TH R 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH R 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME r \- R package development dispatcher .SH SYNOPSIS diff --git a/man/man1/sec.1 b/man/man1/sec.1 index cbb46f097..ed4c5f4e7 100644 --- a/man/man1/sec.1 +++ b/man/man1/sec.1 @@ -1,6 +1,6 @@ .\" Man page for sec dispatcher (Secret Management) .\" Generated: June 2026 -.TH SEC 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH SEC 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME sec \- Secret management dispatcher (Keychain and Bitwarden) .SH SYNOPSIS diff --git a/man/man1/teach.1 b/man/man1/teach.1 index f8a40e978..76e457dc3 100644 --- a/man/man1/teach.1 +++ b/man/man1/teach.1 @@ -1,6 +1,6 @@ .\" Man page for teach dispatcher (Teaching Workflow) .\" Generated: June 2026 -.TH TEACH 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH TEACH 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME teach \- Teaching workflow dispatcher (Scholar integration) .SH SYNOPSIS diff --git a/man/man1/tm.1 b/man/man1/tm.1 index 8b01eaf5f..3dc6e4a4e 100644 --- a/man/man1/tm.1 +++ b/man/man1/tm.1 @@ -1,6 +1,6 @@ .\" Man page for tm dispatcher (Terminal Manager) .\" Generated: June 2026 -.TH TM 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH TM 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME tm \- Terminal manager dispatcher .SH SYNOPSIS diff --git a/man/man1/today.1 b/man/man1/today.1 index 65e283340..3da6560c5 100644 --- a/man/man1/today.1 +++ b/man/man1/today.1 @@ -1,6 +1,6 @@ .\" Man page for the today command (quick daily status) .\" Updated: June 2026 -.TH TODAY 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH TODAY 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME today \- quick daily status .SH SYNOPSIS diff --git a/man/man1/tok.1 b/man/man1/tok.1 index c49ef6d94..706c2fd4d 100644 --- a/man/man1/tok.1 +++ b/man/man1/tok.1 @@ -1,6 +1,6 @@ .\" Man page for tok dispatcher (Token Management) .\" Generated: June 2026 -.TH TOK 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH TOK 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME tok \- Token lifecycle management dispatcher .SH SYNOPSIS diff --git a/man/man1/v.1 b/man/man1/v.1 index 5e8bfd133..636f55827 100644 --- a/man/man1/v.1 +++ b/man/man1/v.1 @@ -1,6 +1,6 @@ .\" Man page for v dispatcher (Vibe / Workflow Automation) .\" Generated: June 2026 -.TH V 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH V 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME v \- Workflow automation dispatcher (vibe coding mode) .SH SYNOPSIS diff --git a/man/man1/week.1 b/man/man1/week.1 index a4dbe85ab..506f16305 100644 --- a/man/man1/week.1 +++ b/man/man1/week.1 @@ -1,6 +1,6 @@ .\" Man page for the week command (weekly review helper) .\" Updated: June 2026 -.TH WEEK 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH WEEK 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME week \- weekly review helper .SH SYNOPSIS diff --git a/man/man1/wt.1 b/man/man1/wt.1 index b1c241da6..09a3cb6e4 100644 --- a/man/man1/wt.1 +++ b/man/man1/wt.1 @@ -1,6 +1,6 @@ .\" Man page for wt dispatcher (Git Worktree Management) .\" Generated: June 2026 -.TH WT 1 "June 2026" "flow-cli 7.12.0" "User Commands" +.TH WT 1 "June 2026" "flow-cli 7.13.0" "User Commands" .SH NAME wt \- Git worktree management dispatcher .SH SYNOPSIS diff --git a/package.json b/package.json index cd8835e30..0dd180872 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flow-cli", - "version": "7.12.0", + "version": "7.13.0", "description": "ADHD-optimized ZSH workflow plugin", "private": true, "scripts": { diff --git a/tests/test-flow-claude.zsh b/tests/test-flow-claude.zsh index a2d2e8e99..d4e6241c5 100755 --- a/tests/test-flow-claude.zsh +++ b/tests/test-flow-claude.zsh @@ -1,5 +1,5 @@ #!/usr/bin/env zsh -# tests/test-flow-claude.zsh β€” Tests for flow claude check (C1-C6) +# tests/test-flow-claude.zsh β€” Tests for flow claude check (C1-C11) + watch SCRIPT_DIR="${0:A:h}" PROJECT_ROOT="${SCRIPT_DIR:h}" @@ -19,13 +19,19 @@ _tc_setup() { _TC_TMP=$(mktemp -d) export FLOW_CLAUDE_HOME="$_TC_TMP/claude" export FLOW_CLAUDE_ZSHRC="$_TC_TMP/zshrc" - mkdir -p "$FLOW_CLAUDE_HOME/hooks" "$FLOW_CLAUDE_HOME/projects" + export FLOW_CLAUDE_PROJECTS_ROOT="$_TC_TMP/projects" + mkdir -p "$FLOW_CLAUDE_HOME/hooks" "$FLOW_CLAUDE_HOME/projects" "$FLOW_CLAUDE_PROJECTS_ROOT" print 'export CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000' > "$FLOW_CLAUDE_ZSHRC" } _tc_teardown() { [[ -n "${_TC_TMP:-}" ]] && rm -rf "$_TC_TMP" - unset FLOW_CLAUDE_HOME FLOW_CLAUDE_ZSHRC _TC_TMP + unset FLOW_CLAUDE_HOME FLOW_CLAUDE_ZSHRC FLOW_CLAUDE_PROJECTS_ROOT _TC_TMP +} + +_tc_make_hook() { + local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" + print '#!/bin/bash' > "$hook" && chmod +x "$hook" } # ── C1: Settings parity ─────────────────────────────────────────────────── @@ -33,8 +39,7 @@ _tc_teardown() { test_case "C1: passes when settings.json has no env block" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? if command -v jq &>/dev/null; then @@ -48,8 +53,7 @@ _tc_teardown test_case "C1: warns when key missing from zshrc" _tc_setup print '{"env":{"MY_TEST_VAR":"99"}}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash\necho ok' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? if command -v jq &>/dev/null; then @@ -64,8 +68,7 @@ test_case "C1: passes when key matches zshrc" _tc_setup print '{"env":{"MY_TEST_VAR":"99"}}' > "$FLOW_CLAUDE_HOME/settings.json" print 'export MY_TEST_VAR=99' >> "$FLOW_CLAUDE_ZSHRC" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash\necho ok' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? if command -v jq &>/dev/null; then @@ -79,8 +82,7 @@ _tc_teardown test_case "C1: --fix appends missing key to zshrc" _tc_setup print '{"env":{"FIX_VAR":"42"}}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash\necho ok' > "$hook" && chmod +x "$hook" +_tc_make_hook _flow_claude_check --fix 2>&1 if command -v jq &>/dev/null; then assert_contains "$(cat "$FLOW_CLAUDE_ZSHRC")" "FIX_VAR=42" "--fix appended key" @@ -93,8 +95,7 @@ test_case "C1: --fix updates existing mismatched value" _tc_setup print '{"env":{"FIX_VAR":"new"}}' > "$FLOW_CLAUDE_HOME/settings.json" print 'export FIX_VAR=old' > "$FLOW_CLAUDE_ZSHRC" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash\necho ok' > "$hook" && chmod +x "$hook" +_tc_make_hook _flow_claude_check --fix 2>&1 if command -v jq &>/dev/null; then assert_contains "$(cat "$FLOW_CLAUDE_ZSHRC")" "FIX_VAR=new" "--fix updated value" @@ -129,8 +130,7 @@ _tc_teardown test_case "C2: passes when hook exists and is executable" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash\necho ok' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? assert_contains "$out" "Hook health" "C2 output present" @@ -142,11 +142,13 @@ _tc_teardown test_case "C3: passes when file count matches MEMORY.md entries" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" -local mem="$FLOW_CLAUDE_HOME/projects/testproj/memory" +_tc_make_hook +# Slug must decode to an existing path so C8 doesn't fire (/${slug//-//} = _TC_TMP) +local proj_slug="${_TC_TMP:1}" # strip leading / +proj_slug="${proj_slug//\//-}" # / β†’ - +local mem="$FLOW_CLAUDE_HOME/projects/$proj_slug/memory" mkdir -p "$mem" -print -- '- [a](a.md) β€” x\n- [b](b.md) β€” y' > "$mem/MEMORY.md" +printf -- '- [a](a.md) β€” x\n- [b](b.md) β€” y\n' > "$mem/MEMORY.md" print 'a' > "$mem/a.md" print 'b' > "$mem/b.md" local out rc @@ -158,11 +160,12 @@ _tc_teardown test_case "C3: warns when file count exceeds MEMORY.md entries" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" -local mem="$FLOW_CLAUDE_HOME/projects/testproj/memory" +_tc_make_hook +local proj_slug="${_TC_TMP:1}" +proj_slug="${proj_slug//\//-}" +local mem="$FLOW_CLAUDE_HOME/projects/$proj_slug/memory" mkdir -p "$mem" -print -- '- [a](a.md) β€” x' > "$mem/MEMORY.md" +printf -- '- [a](a.md) β€” x\n' > "$mem/MEMORY.md" print 'a' > "$mem/a.md" print 'b' > "$mem/b.md" # extra file not in index local out rc @@ -171,30 +174,39 @@ assert_contains "$out" "drift" "C3 warns on drift" [[ $rc -ge 1 ]] && test_pass "non-zero on drift" || test_fail "non-zero on drift" "rc=$rc" _tc_teardown -# ── C4: CLAUDE.md length ────────────────────────────────────────────────── +# ── C4: CLAUDE.md length (two-tier) ────────────────────────────────────── test_case "C4: passes when CLAUDE.md <= 100 lines" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" -printf '%s\n' {1..50} > "$FLOW_CLAUDE_HOME/CLAUDE.md" +_tc_make_hook +printf '%s\n' {1..95} > "$FLOW_CLAUDE_HOME/CLAUDE.md" local out rc out=$(_flow_claude_check 2>&1); rc=$? assert_contains "$out" "CLAUDE.md length" "C4 runs" [[ $rc -eq 0 ]] && test_pass "exit 0 under limit" || test_fail "exit 0 under limit" "rc=$rc" _tc_teardown -test_case "C4: warns when CLAUDE.md > 100 lines" +test_case "C4: warns when CLAUDE.md > 100 lines (approaching limit)" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" -printf '%s\n' {1..101} > "$FLOW_CLAUDE_HOME/CLAUDE.md" +_tc_make_hook +printf '%s\n' {1..150} > "$FLOW_CLAUDE_HOME/CLAUDE.md" local out rc out=$(_flow_claude_check 2>&1); rc=$? -assert_contains "$out" "exceeds 100-line rule" "C4 warns on overflow" -[[ $rc -ge 1 ]] && test_pass "non-zero on overflow" || test_fail "non-zero on overflow" "rc=$rc" +assert_contains "$out" "approaching 180-line limit" "C4 warns on approaching limit" +[[ $rc -eq 2 ]] && test_pass "exit 2 on WARN" || test_fail "exit 2 on WARN" "rc=$rc" +_tc_teardown + +test_case "C4: errors when CLAUDE.md > 180 lines" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +printf '%s\n' {1..200} > "$FLOW_CLAUDE_HOME/CLAUDE.md" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "exceeds 180-line hard limit" "C4 errors on overflow" +[[ $rc -eq 1 ]] && test_pass "exit 1 on ERROR" || test_fail "exit 1 on ERROR" "rc=$rc" _tc_teardown # ── C5: Shell env parity ────────────────────────────────────────────────── @@ -202,8 +214,7 @@ _tc_teardown test_case "C5: reports when CLAUDE_AUTOCOMPACT_PCT_OVERRIDE is set" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook export CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65 local out out=$(_flow_claude_check 2>&1) @@ -214,8 +225,7 @@ _tc_teardown test_case "C5: reports when CLAUDE_AUTOCOMPACT_PCT_OVERRIDE is unset" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook unset CLAUDE_AUTOCOMPACT_PCT_OVERRIDE local out out=$(_flow_claude_check 2>&1) @@ -228,8 +238,7 @@ test_case "C6: warns when CLAUDE_CODE_MAX_OUTPUT_TOKENS not set" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" > "$FLOW_CLAUDE_ZSHRC" # clear default token so C6 warns -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? assert_contains "$out" "Output token limit" "C6 output present" @@ -241,8 +250,7 @@ test_case "C6: passes when CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000 in zshrc" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" print 'export CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000' > "$FLOW_CLAUDE_ZSHRC" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? assert_contains "$out" "Output token limit" "C6 output present" @@ -254,8 +262,7 @@ test_case "C6: warns when value is at default 8192" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" print 'export CLAUDE_CODE_MAX_OUTPUT_TOKENS=8192' > "$FLOW_CLAUDE_ZSHRC" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook local out rc out=$(_flow_claude_check 2>&1); rc=$? assert_contains "$out" "8192" "C6 shows value" @@ -266,14 +273,292 @@ _tc_teardown test_case "C6: --fix appends CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000 to zshrc" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook _flow_claude_check --fix 2>&1 local zshrc_content zshrc_content=$(cat "$FLOW_CLAUDE_ZSHRC") assert_contains "$zshrc_content" "CLAUDE_CODE_MAX_OUTPUT_TOKENS=32000" "C6 --fix writes to zshrc" _tc_teardown +# ── C7: Per-project CLAUDE.md audit ─────────────────────────────────────── + +test_case "C7: passes when no projects root exists" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +export FLOW_CLAUDE_PROJECTS_ROOT="$_TC_TMP/no-such-dir" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "Project CLAUDE.md" "C7 output present" +assert_contains "$out" "not found" "C7 skips gracefully" +[[ $rc -eq 0 ]] && test_pass "exit 0 when no projects" || test_fail "exit 0 when no projects" "rc=$rc" +_tc_teardown + +test_case "C7a: warns when project CLAUDE.md > 180 lines" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local proj="$FLOW_CLAUDE_PROJECTS_ROOT/myproject" +mkdir -p "$proj" +printf '%s\n' {1..200} > "$proj/CLAUDE.md" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "200 lines" "C7a reports line count" +assert_contains "$out" "> 180" "C7a flags threshold" +[[ $rc -ge 2 ]] && test_pass "non-zero on C7a issue" || test_fail "non-zero on C7a issue" "rc=$rc" +_tc_teardown + +test_case "C7a: passes when project CLAUDE.md <= 180 lines" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local proj="$FLOW_CLAUDE_PROJECTS_ROOT/myproject" +mkdir -p "$proj" +printf '%s\n' {1..100} > "$proj/CLAUDE.md" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "all clean" "C7 all clean" +[[ $rc -eq 0 ]] && test_pass "exit 0 on clean" || test_fail "exit 0 on clean" "rc=$rc" +_tc_teardown + +test_case "C7b: warns on version drift when git tag differs" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local proj="$FLOW_CLAUDE_PROJECTS_ROOT/myproject" +mkdir -p "$proj" +printf 'Current Version: v1.0.0\n' > "$proj/CLAUDE.md" +# Mock git to return a different tag +git() { + if [[ "$*" == *"describe"* ]]; then + print "v2.0.0" + return 0 + fi + command git "$@" +} +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +unfunction git 2>/dev/null +assert_contains "$out" "version ref v1.0.0" "C7b reports version drift" +assert_contains "$out" "current: v2.0.0" "C7b shows current tag" +[[ $rc -ge 2 ]] && test_pass "non-zero on version drift" || test_fail "non-zero on version drift" "rc=$rc" +_tc_teardown + +test_case "C7b: passes when no git tags exist" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local proj="$FLOW_CLAUDE_PROJECTS_ROOT/myproject" +mkdir -p "$proj" +printf 'Current Version: v1.0.0\n' > "$proj/CLAUDE.md" +# Mock git to return no tags (non-zero exit) +git() { + if [[ "$*" == *"describe"* ]]; then + return 1 + fi + command git "$@" +} +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +unfunction git 2>/dev/null +assert_contains "$out" "all clean" "C7b skips when no tags" +[[ $rc -eq 0 ]] && test_pass "exit 0 β€” no false positive on missing tags" || test_fail "exit 0" "rc=$rc" +_tc_teardown + +# ── C8: Orphaned memory dirs ─────────────────────────────────────────────── + +test_case "C8: warns when memory dir decodes to missing path" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +# Create a slug that decodes to a nonexistent path +local fake_slug="-tmp-no-such-project-xyzzy" +mkdir -p "$FLOW_CLAUDE_HOME/projects/$fake_slug" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "Orphaned memory" "C8 output present" +assert_contains "$out" "stale" "C8 reports stale dir" +[[ $rc -ge 2 ]] && test_pass "non-zero on orphaned dir" || test_fail "non-zero on orphaned dir" "rc=$rc" +_tc_teardown + +test_case "C8: passes when all memory dirs decode to valid paths" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +# Create a slug that decodes to an existing path (tmp dir itself) +local tmp_slug="${_TC_TMP/\//}" # strip leading / +tmp_slug="${tmp_slug//\//-}" # replace / with - +tmp_slug="-$tmp_slug" # add leading - to represent leading / +mkdir -p "$FLOW_CLAUDE_HOME/projects/$tmp_slug" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "Orphaned memory" "C8 output present" +assert_contains "$out" "valid" "C8 passes for valid dir" +[[ $rc -eq 0 ]] && test_pass "exit 0 on valid dirs" || test_fail "exit 0 on valid dirs" "rc=$rc" +_tc_teardown + +# ── C9: Rules drift ──────────────────────────────────────────────────────── + +test_case "C9: warns when rule file not cited in CLAUDE.md" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +mkdir -p "$FLOW_CLAUDE_HOME/rules" +print 'some rule content' > "$FLOW_CLAUDE_HOME/rules/my-uncited-rule.md" +# CLAUDE.md does not mention the rule stem +print 'This is CLAUDE.md with no rule references.' > "$FLOW_CLAUDE_HOME/CLAUDE.md" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "Rules drift" "C9 output present" +assert_contains "$out" "my-uncited-rule" "C9 reports unreferenced rule" +[[ $rc -ge 2 ]] && test_pass "non-zero on rules drift" || test_fail "non-zero on rules drift" "rc=$rc" +_tc_teardown + +test_case "C9: passes when all rules are referenced in CLAUDE.md" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +mkdir -p "$FLOW_CLAUDE_HOME/rules" +print 'some rule content' > "$FLOW_CLAUDE_HOME/rules/my-cited-rule.md" +# CLAUDE.md mentions the rule stem +print 'See my-cited-rule for details.' > "$FLOW_CLAUDE_HOME/CLAUDE.md" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "Rules drift" "C9 output present" +assert_contains "$out" "referenced" "C9 passes on all referenced" +[[ $rc -eq 0 ]] && test_pass "exit 0 on all referenced" || test_fail "exit 0 on all referenced" "rc=$rc" +_tc_teardown + +test_case "C9: skips gracefully when no rules dir" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +# No rules dir created +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +assert_contains "$out" "Rules drift" "C9 output present" +assert_contains "$out" "skipped" "C9 skips without rules dir" +[[ $rc -eq 0 ]] && test_pass "exit 0 without rules dir" || test_fail "exit 0 without rules dir" "rc=$rc" +_tc_teardown + +# ── C10: Missing hook files ──────────────────────────────────────────────── + +test_case "C10: passes when settings.json has no hooks" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + assert_contains "$out" "Hook files" "C10 output present" + assert_contains "$out" "present" "C10 passes with no hooks" + [[ $rc -eq 0 ]] && test_pass "exit 0 with no hooks" || test_fail "exit 0 with no hooks" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + +test_case "C10: passes when all hook paths exist" +_tc_setup +local hook_script="$_TC_TMP/myhook.sh" +print '#!/bin/bash\necho ok' > "$hook_script" && chmod +x "$hook_script" +print "{\"hooks\":{\"PreToolUse\":[{\"command\":\"$hook_script\"}]}}" > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + assert_contains "$out" "Hook files" "C10 output present" + assert_contains "$out" "present" "C10 passes for existing hook" + [[ $rc -eq 0 ]] && test_pass "exit 0 when hook exists" || test_fail "exit 0 when hook exists" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + +test_case "C10: errors when hook path does not exist" +_tc_setup +local missing_hook="$_TC_TMP/nonexistent-hook.sh" +print "{\"hooks\":{\"PreToolUse\":[{\"command\":\"$missing_hook\"}]}}" > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + assert_contains "$out" "Hook files" "C10 output present" + assert_contains "$out" "missing" "C10 errors on missing hook" + [[ $rc -eq 1 ]] && test_pass "exit 1 on missing hook" || test_fail "exit 1 on missing hook" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + +# ── C11: Plugin health ───────────────────────────────────────────────────── + +test_case "C11: passes when all plugins have valid plugin.json" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +mkdir -p "$FLOW_CLAUDE_HOME/plugins/myplugin" +print '{"name":"myplugin","version":"1.0.0"}' > "$FLOW_CLAUDE_HOME/plugins/myplugin/plugin.json" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + assert_contains "$out" "Plugin health" "C11 output present" + assert_contains "$out" "healthy" "C11 passes for valid plugin" + [[ $rc -eq 0 ]] && test_pass "exit 0 for valid plugin" || test_fail "exit 0 for valid plugin" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + +test_case "C11: warns when plugin is missing plugin.json" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +mkdir -p "$FLOW_CLAUDE_HOME/plugins/broken-plugin" +# No plugin.json created +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + assert_contains "$out" "Plugin health" "C11 output present" + assert_contains "$out" "missing plugin.json" "C11 warns on missing file" + [[ $rc -ge 2 ]] && test_pass "non-zero on missing plugin.json" || test_fail "non-zero on missing plugin.json" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + +test_case "C11: warns when plugin.json is invalid JSON" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +mkdir -p "$FLOW_CLAUDE_HOME/plugins/bad-json-plugin" +print 'not valid json {{{' > "$FLOW_CLAUDE_HOME/plugins/bad-json-plugin/plugin.json" +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + assert_contains "$out" "Plugin health" "C11 output present" + assert_contains "$out" "invalid JSON" "C11 warns on invalid JSON" + [[ $rc -ge 2 ]] && test_pass "non-zero on invalid JSON" || test_fail "non-zero on invalid JSON" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + +test_case "C11: skips cache/ subdirectory" +_tc_setup +print '{}' > "$FLOW_CLAUDE_HOME/settings.json" +_tc_make_hook +mkdir -p "$FLOW_CLAUDE_HOME/plugins/cache/some-cached-plugin" +# No plugin.json in cache subdir β€” should be ignored +local out rc +out=$(_flow_claude_check 2>&1); rc=$? +if command -v jq &>/dev/null; then + [[ $rc -eq 0 ]] && test_pass "exit 0 β€” cache/ dir skipped" || test_fail "exit 0 β€” cache/ dir skipped" "rc=$rc" +else + test_skip "jq not installed" +fi +_tc_teardown + # ── Exit codes ───────────────────────────────────────────────────────────── test_case "exit 1: ERROR wins over WARN" @@ -289,8 +574,7 @@ _tc_teardown test_case "exit 2: WARN only" _tc_setup print '{}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook printf '%s\n' {1..150} > "$FLOW_CLAUDE_HOME/CLAUDE.md" local out rc out=$(_flow_claude_check 2>&1); rc=$? @@ -302,8 +586,7 @@ _tc_teardown test_case "C1: degrades gracefully when jq not installed" _tc_setup print '{"env":{"FOO":"bar"}}' > "$FLOW_CLAUDE_HOME/settings.json" -local hook="$FLOW_CLAUDE_HOME/hooks/post-compact-reinject.sh" -print '#!/bin/bash' > "$hook" && chmod +x "$hook" +_tc_make_hook # Hide jq via PATH override in subshell local orig_path="$PATH" export PATH="/usr/bin:/bin" @@ -313,4 +596,107 @@ export PATH="$orig_path" assert_contains "$out" "Settings parity" "C1 output present without jq" _tc_teardown +# ── Watch daemon ─────────────────────────────────────────────────────────── + +test_case "watch --status: shows not-running when no pid file" +local flow_dir +flow_dir=$(mktemp -d) +local pid_file="$flow_dir/claude-watch.pid" +local state_file="$flow_dir/claude-health-state.json" +# No pid file β€” status should say not running without crashing +local orig_home="$HOME" +HOME="$flow_dir" +local out rc +out=$(_flow_claude_watch_status 2>&1); rc=$? +HOME="$orig_home" +rm -rf "$flow_dir" +assert_contains "$out" "not running" "status shows not running" +[[ $rc -eq 0 ]] && test_pass "exit 0 on status when not running" || test_fail "exit 0 on status" "rc=$rc" + +test_case "watch --stop: no-ops cleanly when watcher not running" +local flow_dir saved_home +flow_dir=$(mktemp -d) +saved_home="$HOME" +HOME="$flow_dir" +local out rc +out=$(_flow_claude_watch_stop 2>&1); rc=$? +HOME="$saved_home" +rm -rf "$flow_dir" +[[ $rc -eq 0 ]] && test_pass "exit 0 on stop when not running" || test_fail "exit 0 on stop" "rc=$rc" + +test_case "watch --stop: kills process and removes pid file" +local flow_dir saved_home +flow_dir=$(mktemp -d) +mkdir -p "$flow_dir/.flow" +# Start a background sleep and write its PID where _flow_claude_watch_stop expects it +sleep 999 & +local fake_pid=$! +print "$fake_pid" > "$flow_dir/.flow/claude-watch.pid" +saved_home="$HOME" +HOME="$flow_dir" +local out rc +out=$(_flow_claude_watch_stop 2>&1); rc=$? +HOME="$saved_home" +assert_not_contains "$out" "not running" "stop reports success" +[[ ! -f "$flow_dir/.flow/claude-watch.pid" ]] && test_pass "pid file removed after stop" || test_fail "pid file removed after stop" +kill "$fake_pid" 2>/dev/null || true +rm -rf "$flow_dir" + +test_case "watch notify: warnβ†’pass fires notifier" +# Use a real executable in PATH β€” command -v doesn't find shell functions +local notif_dir called_file saved_path +notif_dir=$(mktemp -d) +called_file="$notif_dir/notif-args.txt" +print "#!/bin/sh" > "$notif_dir/terminal-notifier" +print "echo \"\$@\" >> \"$called_file\"" >> "$notif_dir/terminal-notifier" +chmod +x "$notif_dir/terminal-notifier" +saved_path="$PATH" +export PATH="$notif_dir:$PATH" +_flow_claude_watch_notify "warn" "pass" "All clear" +export PATH="$saved_path" +if [[ -f "$called_file" ]]; then + local notif_args + notif_args=$(cat "$called_file") + assert_contains "$notif_args" "restored" "notifier output shows restored on warnβ†’pass" + test_pass "notifier called on state change" +else + test_fail "notifier should have been called on warnβ†’pass (args file missing)" +fi +rm -rf "$notif_dir" + +test_case "watch notify: passβ†’pass is silent" +local notif_dir saved_path +notif_dir=$(mktemp -d) +print "#!/bin/sh" > "$notif_dir/terminal-notifier" +print "touch \"$notif_dir/was-called\"" >> "$notif_dir/terminal-notifier" +chmod +x "$notif_dir/terminal-notifier" +saved_path="$PATH" +export PATH="$notif_dir:$PATH" +_flow_claude_watch_notify "pass" "pass" "still passing" +export PATH="$saved_path" +[[ ! -f "$notif_dir/was-called" ]] && test_pass "no notification on passβ†’pass" || test_fail "no notification on passβ†’pass" "notifier was called" +rm -rf "$notif_dir" + +test_case "watch run_check: writes state file with result" +_tc_setup +local flow_dir +flow_dir=$(mktemp -d) +local state_file="$flow_dir/state.json" +# Mock _flow_claude_check to return WARN (rc=2) +_flow_claude_check() { return 2 } +local orig_home="$HOME" +HOME="$flow_dir" +_flow_claude_watch_run_check "$state_file" 2>/dev/null +HOME="$orig_home" +unfunction _flow_claude_check 2>/dev/null +if command -v jq &>/dev/null && [[ -f "$state_file" ]]; then + local result + result=$(jq -r '.result' "$state_file" 2>/dev/null) + [[ "$result" == "warn" ]] && test_pass "state file has result=warn" || test_fail "state file has result=warn" "got: $result" +else + test_skip "jq not installed or state file missing" +fi +rm -rf "$flow_dir" +_tc_teardown + test_suite_end