-
Notifications
You must be signed in to change notification settings - Fork 0
fix: v1.4 bugfixes + 5-line status layout + TUI live preview #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
60bc5f0
ffeeb2c
768a5e0
6cf685d
4b55e02
e5cd4ad
45bcd75
49c15e8
a105f74
e84859e
31c394c
4718ab1
16e645b
f0b07b5
8b0ba0b
b7f3e54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| name: TUI Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - "tui/**" | ||
| - ".github/workflows/tui-tests.yml" | ||
| pull_request: | ||
| paths: | ||
| - "tui/**" | ||
| - ".github/workflows/tui-tests.yml" | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| python-version: ["3.11", "3.12", "3.13"] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v4 | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: tui | ||
| run: uv pip install --system -e ".[dev]" | ||
|
|
||
| - name: Run TUI tests | ||
| working-directory: tui | ||
| run: python -m pytest tests/ -v | ||
|
|
||
| - name: Upload snapshot diffs on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: snapshot-diffs-py${{ matrix.python-version }} | ||
| path: tui/tests/**/snapshot_tests_output/ | ||
| retention-days: 7 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,63 @@ | ||
| # Philosophy | ||
|
|
||
| hookwise was born from watching Claude Code do amazing things -- and occasionally terrifying things. | ||
| hookwise is a platform for guarding, coaching, and observing Claude Code sessions. | ||
|
|
||
| ## Why hookwise Exists | ||
|
|
||
| I built hookwise because I kept waking up to disasters. | ||
| Claude Code is extraordinary. It can refactor an entire module, set up infrastructure, write tests -- all while you are reading the previous diff. That power is exactly why it needs guard rails, observability, and coaching. | ||
|
|
||
| Not "my server is down" disasters -- "my AI rewrote the build system while I was making coffee" disasters. Claude Code is extraordinary. It can refactor an entire module, set up infrastructure, write tests -- all while you are reading the previous diff. That power is exactly why it needs guard rails. | ||
| The existing hooks system is powerful but raw. You write bash scripts, scatter them across your config, and hope they cover the right events. There is no testing, no sharing, no analytics, and no way to know if your guards actually work until something slips through. | ||
|
|
||
| The existing hooks system is powerful but raw. You write bash scripts, scatter them across your config, and hope they cover the right events. There is no testing, no sharing, and no way to know if your guards actually work until something slips through. | ||
| hookwise replaces all of that with a platform: declarative guards, analytics that tell you where your time actually goes, coaching that nudges you when you have been in tooling mode for 90 minutes, feeds that surface context in your status line, recipes that let you share configurations, and a TUI that ties it all together. | ||
|
|
||
| **hookwise is one YAML file that replaces all of that.** Declarative guards that read like firewall rules. Analytics that tell you where your time actually goes. Coaching that nudges you when you have been in "tooling mode" for 90 minutes and forgot your original goal. | ||
| ## Core Principles | ||
|
|
||
| ## Design Principle | ||
| ### 1. Platform, Not Just Config | ||
|
|
||
| The design principle is simple: **your AI should never be the reason you cannot sleep.** Everything in hookwise serves that principle -- guards protect, context enriches, side effects observe. If any part of hookwise itself errors, it fails open. hookwise must never be the thing that breaks your flow. | ||
| hookwise started as a YAML config layer for guard rails. It is now a platform with a SQLite analytics engine, a cache bus, coaching state, a feed daemon, a TUI, and a recipe system. YAML remains the right format for declaring guards, coaching rules, and feed settings -- the things that should be human-readable, git-tracked, and declarative. But YAML is not the whole platform. | ||
|
|
||
| > *"Guard rails should be boring. The exciting part is what you build when you are not worried about what your AI is doing."* | ||
| The platform needs a data layer, not just a config layer. Recipes need a standard way to persist structured data. Analytics needs a durable store. Coaching needs memory across sessions. Without a standard data layer, every recipe author invents their own storage, every new feature adds another ad hoc file, and the platform fragments. | ||
|
|
||
| ## Fail-Open | ||
| hookwise should provide a shared data layer that any subsystem or recipe can use. The right tool for that layer -- SQLite, a managed database, purpose-built files -- is an engineering question, not a philosophical one. | ||
|
|
||
| If any part of hookwise itself errors, it fails open. hookwise must never accidentally block a tool call due to internal errors. Any unhandled exception anywhere in the dispatch pipeline results in `exit 0`. | ||
| ### 2. Degrade Gracefully, Always Warn | ||
|
|
||
| If any part of hookwise errors, the session continues. hookwise must never accidentally block a tool call due to internal errors. Any unhandled exception in the dispatch pipeline results in `exit 0`. | ||
|
|
||
| But graceful degradation must not mean silent degradation. The v1.3 retro showed that "fail-open" was interpreted as "fail-silent" -- 5 of 7 launch bugs shipped unnoticed because errors were swallowed without any signal to the user. Every degradation must produce a visible diagnostic: a status line warning, a cache bus key, a log entry. The user should always know when something is running in a reduced state. | ||
|
|
||
| **Degrade gracefully AND always warn the user.** | ||
|
|
||
| Graceful degradation is a design practice, not a philosophical argument against infrastructure. A database server that crashes should not take down your Claude Code session -- but the possibility of a crash is not a reason to avoid running a database server. The question is whether the infrastructure solves a real problem and whether hookwise handles its failure modes correctly. | ||
|
|
||
| ### 3. Infrastructure Is Normal | ||
|
|
||
| hookwise is a developer tool platform. Developer tool platforms have databases, background daemons, and managed processes. This is normal and expected. | ||
|
|
||
| hookwise already runs a background daemon for feeds, manages PID files, bundles better-sqlite3 as a native addon, and ships a Python TUI package. The bar for adding infrastructure should be: "Does this solve a real problem for users?" Not: "Is this zero-config?" and not: "Could this possibly go down?" | ||
|
|
||
| What matters is that infrastructure is invisible to the user when it works, and diagnostic when it does not. | ||
|
|
||
| ### 4. Guards Should Be Boring; the Platform Should Not | ||
|
|
||
| Guard rails should be declarative, predictable, and unexciting. You write YAML rules that read like firewall policies. First block wins. No magic. The exciting part is what you build when you are not worried about what your AI is doing. | ||
|
|
||
| This principle applies specifically to the guard system. Other parts of hookwise -- analytics dashboards, coaching nudges, feed-backed status lines, the TUI, recipe ecosystems -- can and should be rich, interactive, and engaging. The platform grows; guards stay boring. | ||
|
|
||
| ### 5. Recipes Need a Data Contract | ||
|
|
||
| A recipe is a shareable hookwise configuration: guards, coaching rules, feed producers, status line segments. Today, a recipe is a directory with `hooks.yaml`. That is enough for declarative configuration but not enough for recipes that need to persist data. | ||
|
|
||
| A recipe that tracks code review patterns needs a table. A recipe that scores commit quality needs a rolling window of scores. A recipe that detects dependency drift needs a snapshot of the last lockfile. Without a standard data layer, each recipe author builds their own persistence -- different formats, different locations, different cleanup strategies. | ||
|
|
||
| hookwise should define a data contract for recipes: a standard way to create tables, read/write rows, and query data. The underlying engine is an implementation detail. The contract is what makes recipes portable and composable. | ||
|
|
||
| ### 6. Composition Over Extension | ||
|
|
||
| New capabilities should be composable from existing subsystems. A new feed producer is a function. A new guard is a YAML rule. A new recipe is a directory with `hooks.yaml` and optionally a data schema. A new status line segment is a pure function of cache and config. | ||
|
|
||
| When composition is not enough and a new architectural layer is needed, that is a signal to design it carefully -- not a signal to avoid it. | ||
|
|
||
| --- | ||
|
|
||
| ← [Back to Home](/) | ||
| <- [Back to Home](/) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| #!/usr/bin/env bash | ||
| # hooks/agent-tracker.sh | ||
| # | ||
| # Claude Code hook for SubagentStart / SubagentStop events. | ||
| # Reads JSON from stdin, maintains state at ~/.hookwise/cache/active-agents.json. | ||
| # Always exits 0 (fail-open). | ||
| set -uo pipefail | ||
|
|
||
| STATE_DIR="${HOOKWISE_STATE_DIR:-$HOME/.hookwise}" | ||
| CACHE_DIR="$STATE_DIR/cache" | ||
| STATE_FILE="$CACHE_DIR/active-agents.json" | ||
| STALE_SECONDS=600 # 10 minutes | ||
|
|
||
| mkdir -p "$CACHE_DIR" | ||
|
|
||
| # Read stdin JSON | ||
| INPUT=$(cat) | ||
|
|
||
| # Extract event type from CLAUDE_CODE_HOOK_EVENT_NAME env var | ||
| EVENT="${CLAUDE_CODE_HOOK_EVENT_NAME:-}" | ||
|
|
||
| # Parse fields from stdin JSON | ||
| agent_id=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('agent_id',''))" 2>/dev/null || echo "") | ||
| session_id=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('session_id',''))" 2>/dev/null || echo "") | ||
| worktree=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || echo "") | ||
| team_name=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('team_name',''))" 2>/dev/null || echo "") | ||
| strategy=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('strategy',''))" 2>/dev/null || echo "") | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consolidate JSON parsing into a single Python invocation. Four separate ♻️ Proposed consolidation # Parse fields from stdin JSON
-agent_id=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('agent_id',''))" 2>/dev/null || echo "")
-worktree=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || echo "")
-team_name=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('team_name',''))" 2>/dev/null || echo "")
-strategy=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('strategy',''))" 2>/dev/null || echo "")
+read -r agent_id worktree team_name strategy < <(echo "$INPUT" | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+print(d.get('agent_id', ''))
+print(d.get('cwd', ''))
+print(d.get('team_name', ''))
+print(d.get('strategy', ''))
+" 2>/dev/null | tr '\n' ' ' || echo " ")Alternatively, use a here-string with tab-separated output for cleaner parsing: # Parse all fields in one python3 call
eval "$(echo "$INPUT" | python3 -c "
import sys, json, shlex
d = json.load(sys.stdin)
print(f'agent_id={shlex.quote(d.get(\"agent_id\", \"\"))}')
print(f'worktree={shlex.quote(d.get(\"cwd\", \"\"))}')
print(f'team_name={shlex.quote(d.get(\"team_name\", \"\"))}')
print(f'strategy={shlex.quote(d.get(\"strategy\", \"\"))}')
" 2>/dev/null)" || { agent_id=""; worktree=""; team_name=""; strategy=""; }🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion. The four There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a follow-up GitHub issue to track the consolidation so it doesn't get lost? ✏️ Learnings added
🧠 Learnings used |
||
|
|
||
| if [ -z "$agent_id" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Derive a short name from worktree basename or truncated agent_id | ||
| if [ -n "$worktree" ]; then | ||
| name=$(basename "$worktree") | ||
| else | ||
| name="${agent_id:0:12}" | ||
| fi | ||
|
|
||
| NOW=$(date +%s) | ||
|
|
||
| # Read existing state (or start fresh) | ||
| if [ -f "$STATE_FILE" ]; then | ||
| CURRENT=$(cat "$STATE_FILE" 2>/dev/null || echo '{"agents":[],"team_name":"","strategy":""}') | ||
| else | ||
| CURRENT='{"agents":[],"team_name":"","strategy":""}' | ||
| fi | ||
|
|
||
| # Use python3 for JSON manipulation (available on macOS) | ||
| UPDATED=$(python3 -c " | ||
| import json, sys | ||
|
|
||
| current = json.loads('''$CURRENT''') | ||
| agents = current.get('agents', []) | ||
| event = '$EVENT' | ||
| agent_id = '$agent_id' | ||
| name = '$name' | ||
| now = $NOW | ||
| stale = $STALE_SECONDS | ||
| team_name = '$team_name' or current.get('team_name', '') | ||
| strategy = '$strategy' or current.get('strategy', '') | ||
|
|
||
| # Clean stale entries (older than 10 minutes) | ||
| agents = [a for a in agents if (now - a.get('started_at', now)) < stale] | ||
|
|
||
| if event == 'SubagentStart': | ||
| # Remove existing entry with same agent_id (if re-started) | ||
| agents = [a for a in agents if a.get('agent_id') != agent_id] | ||
| agents.append({ | ||
| 'agent_id': agent_id, | ||
| 'name': name, | ||
| 'status': 'working', | ||
| 'started_at': now | ||
| }) | ||
| elif event == 'SubagentStop': | ||
| for a in agents: | ||
| if a.get('agent_id') == agent_id: | ||
| a['status'] = 'done' | ||
| a['stopped_at'] = now | ||
| break | ||
|
|
||
| result = { | ||
| 'agents': agents, | ||
| 'team_name': team_name, | ||
| 'strategy': strategy, | ||
| 'updated_at': now | ||
| } | ||
| print(json.dumps(result, indent=2)) | ||
| " 2>/dev/null) || exit 0 | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Atomic write via temp file + rename | ||
| TMPFILE="$CACHE_DIR/.active-agents.tmp.$$" | ||
| echo "$UPDATED" > "$TMPFILE" | ||
| mv "$TMPFILE" "$STATE_FILE" | ||
|
|
||
| exit 0 | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
LGTM!
The workflow is well-structured with appropriate Python version matrix,
uvfor fast dependency installation, and helpful artifact upload for debugging snapshot failures.Consider adding a job-level timeout and
workflow_dispatchfor manual triggering:♻️ Optional enhancement
jobs: test: runs-on: ubuntu-latest + timeout-minutes: 15 strategy: + fail-fast: false matrix: python-version: ["3.11", "3.12", "3.13"]And optionally:
on: push: branches: [main] paths: - "tui/**" - ".github/workflows/tui-tests.yml" pull_request: paths: - "tui/**" - ".github/workflows/tui-tests.yml" + workflow_dispatch:🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestions! Will consider adding
workflow_dispatchandtimeout-minutesin a follow-up. Low priority for now.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vishnujayvel, sounds good! No rush — the workflow is solid as-is for the initial snapshot testing setup. Feel free to ping me when you revisit it in a follow-up PR.