Tom McKenzie 1 mês atrás
pai
commit
e3fee65e4a
7 arquivos alterados com 168 adições e 20 exclusões
  1. 1 1
      .gitconfig
  2. 35 1
      .rc.d/aliases.sh
  3. 69 0
      .rc.d/ssh-agent.sh
  4. 29 5
      .tmux.conf
  5. 11 12
      CLAUDE.md
  6. 11 0
      config/ghostty/config
  7. 12 1
      config/zed/keymap.json

+ 1 - 1
.gitconfig

@@ -46,7 +46,7 @@
 	# use delta if installed, else fall back to less (don't break git without delta)
 	pager = command -v delta >/dev/null 2>&1 && delta || less -R
 [push]
-	default = upstream
+	default = simple
 [status]
 	submoduleSummary = true
 [merge "npm-merge-driver"]

+ 35 - 1
.rc.d/aliases.sh

@@ -92,7 +92,41 @@ alias pbjsons="pbpaste | jq -sRr '@json'"
 alias vscode-server="docker run -it --init -p 3000:3000 -v "$(pwd):/home/workspace:cached" gitpod/openvscode-server"
 
 alias cc='claude --dangerously-skip-permissions --system-prompt "."'
-alias ccw='claude --dangerously-skip-permissions --system-prompt "." --tmux -w'
+
+# cct — boot cc inside tmux. Already in tmux: a new window in the CURRENT
+#       session. Outside: attach/create the session for this dir (tt's naming)
+#       and boot cc there.
+cct() {
+  local cmd='claude --dangerously-skip-permissions --system-prompt "."'
+  # tmux runs the command via sh -c, so re-quote each arg to survive splitting
+  local a
+  for a in "$@"; do cmd="$cmd $(printf '%q' "$a")"; done
+  if [ -n "$TMUX" ]; then
+    tmux new-window -c "$PWD" "$cmd"
+    return
+  fi
+  local name
+  name="$(basename "$(dirname "$PWD")")_$(basename "$PWD")"
+  if tmux has-session -t "=$name" 2>/dev/null; then
+    tmux new-window -t "=$name" -c "$PWD" "$cmd"
+    tmux attach-session -t "=$name"
+  else
+    tmux new-session -s "$name" -c "$PWD" "$cmd"
+  fi
+}
+
+# ccw — cc in a fresh git worktree. Outside tmux, let claude build its own
+#       session (--tmux). Inside tmux, DON'T pass --tmux: it creates a separate
+#       session and switch-clients you into it, yanking you out of whatever you
+#       were working in. Give it a window in the current session instead.
+ccw() {
+  local cmd='claude --dangerously-skip-permissions --system-prompt "." -w'
+  if [ -n "$TMUX" ]; then
+    tmux new-window -c "$PWD" "$cmd${*:+ $*}"
+  else
+    claude --dangerously-skip-permissions --system-prompt "." --tmux -w "$@"
+  fi
+}
 
 # gw — create a git worktree under .claude/worktrees/<name>, copy in the
 #      .worktreeinclude (gitignored) files, cd into it, and print its path.

+ 69 - 0
.rc.d/ssh-agent.sh

@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# SSH agent forwarding status.
+#
+# Sourced by both bash and zsh (see .bashrc / .zshrc), so keep this POSIX-ish.
+#
+# `ssh-agent-status` prints a one-line summary and can be run by hand any time.
+# It also runs automatically when you land on an interactive SSH login, so a
+# missing forward is obvious immediately rather than at the first git push.
+#
+# A missing forward is dim rather than red: it is usually deliberate, and the
+# line is there to tell you which mode you are in, not to report a fault.
+#
+# A missing forward is almost always a client-side omission: the machine you
+# connected FROM needs `ForwardAgent yes` for this host in its ~/.ssh/config.
+# Fix it there, not here.
+
+ssh-agent-status() {
+  local reset='\033[0m' dim='\033[2m' grey='\033[90m'
+  local green='\033[0;32m' yellow='\033[0;33m' red='\033[0;31m'
+  # Not `status`: that is a read-only builtin in zsh and assigning to it
+  # aborts the function on every zsh login.
+  local keys rc count
+
+  if [ -z "$SSH_AUTH_SOCK" ]; then
+    printf "${dim}${grey}- ssh agent not forwarded (SSH_AUTH_SOCK is unset)${reset}\n"
+    return 1
+  fi
+
+  # Keep the assignment and the status check separate: `local keys=$(...)`
+  # would return the status of `local`, not of ssh-add.
+  keys=$(ssh-add -l 2>/dev/null)
+  rc=$?
+
+  case $rc in
+    0)
+      # Counted in-shell rather than piping through wc and tr. This runs on
+      # every SSH login, and those are two processes spawned to count at most a
+      # handful of lines.
+      count=0
+      while IFS= read -r _; do count=$((count + 1)); done <<EOF
+$keys
+EOF
+      printf "${green}+ ssh agent forwarded${reset} ${dim}(%s keys)${reset}\n" "$count"
+      ;;
+    1)
+      printf "${yellow}! ssh agent forwarded but holds no keys${reset}\n"
+      return 1
+      ;;
+    *)
+      printf "${red}x ssh agent socket is dead${reset} ${dim}(%s)${reset}\n" "$SSH_AUTH_SOCK"
+      return 1
+      ;;
+  esac
+}
+
+# Report on interactive SSH logins only. .zshrc sources .rc.d for
+# non-interactive shells too, so without this guard the banner would be written
+# into the stream used by scp, rsync and git-over-ssh and corrupt them.
+#
+# Local shells never reach the function at all, so this costs a local terminal
+# nothing: $- and SSH_CONNECTION are both shell builtins.
+case $- in
+  *i*)
+    if [ -n "$SSH_CONNECTION" ] && [ -t 1 ]; then
+      ssh-agent-status || true
+    fi
+    ;;
+esac

+ 29 - 5
.tmux.conf

@@ -17,14 +17,19 @@ set -g status-left ' '
 # prefix-highlight (native #{client_prefix} — no plugin needed) + copy/sync indicators.
 # Indicators prepend to status-right so [+] [d] stay flush-right (mouse math intact).
 set -g status-right '#{?client_prefix,#[bg=colour3#,fg=colour235#,bold] ^B #[default] ,}#{?#{==:#{pane_mode},copy-mode},#[bg=colour2#,fg=colour235#,bold] COPY #[default] ,}#{?pane_synchronized,#[bg=colour1#,fg=colour15#,bold] SYNC #[default] ,} [+] [d] '
-# Tab label = the app's pane title (Claude Code sets a good one), trimmed to 24;
+# Tab label = the app's pane title (Claude Code sets a good one);
 # falls back to the running command for bare shells (where pane_title == hostname).
 # #{E:...} forces the stored format in @wtitle to expand. Replaces #W to dodge the
 # window-name pollution (apps leak junk like "2.1.156" into the window NAME).
-set -g @wtitle '#{?#{||:#{==:#{pane_title},},#{==:#{pane_title},#{host}}},#{pane_current_command},#{=/24/…:pane_title}}'
+# @wtitle = full (feeds Ghostty title), @wtab = truncated (feeds status-bar tabs).
+set -g @wtitle '#{?#{||:#{==:#{pane_title},},#{==:#{pane_title},#{host}}},#{pane_current_command},#{pane_title}}'
+set -g @wtab '#{?#{||:#{==:#{pane_title},},#{==:#{pane_title},#{host}}},#{pane_current_command},#{=/24/…:pane_title}}'
+# Forward the active pane's title to the outer terminal (Ghostty tab/window title)
+set -g set-titles on
+set -g set-titles-string '#{E:@wtitle}'
 # Tab: index:(NpZ) title — pane count + zoom flag prefixed in parens, only when split.
-setw -g window-status-format ' #I:#{?#{>:#{window_panes},1},(#{window_panes}p#{?window_zoomed_flag,Z,}) ,}#{E:@wtitle} '
-setw -g window-status-current-format ' #I:#{?#{>:#{window_panes},1},(#{window_panes}p#{?window_zoomed_flag,Z,}) ,}#{E:@wtitle} '
+setw -g window-status-format ' #I:#{?#{>:#{window_panes},1},(#{window_panes}p#{?window_zoomed_flag,Z,}) ,}#{E:@wtab} '
+setw -g window-status-current-format ' #I:#{?#{>:#{window_panes},1},(#{window_panes}p#{?window_zoomed_flag,Z,}) ,}#{E:@wtab} '
 setw -g window-status-current-style 'bg=colour4 fg=colour15 bold'
 set -g pane-border-status top
 
@@ -58,7 +63,26 @@ bind w display-menu -T "Windows" \
 
 bind-key -n M-w new-window
 
+# Navigation hierarchy (no prefix), shared with Zed where possible:
+#   Shift+arrow       -> tabs/windows (flat, left/right only)
+#   Ctrl+Shift+arrow  -> panes (the 2D grid). Same chord in Zed.
+# Modified arrows arrive as legacy CSI 1;6 sequences, so tmux parses C-S-Left
+# natively -- no user-keys translation needed. Never use Cmd here: Ghostty
+# owns it and never forwards it to tmux.
+# -r lets you hold/repeat within repeat-time.
+bind-key -n -r S-Left  previous-window
+bind-key -n -r S-Right next-window
+
+bind-key -n -r C-S-Left  select-pane -L
+bind-key -n -r C-S-Right select-pane -R
+bind-key -n -r C-S-Up    select-pane -U
+bind-key -n -r C-S-Down  select-pane -D
+
 # Fork Claude Code session into a new window
 bind R source-file ~/.tmux.conf \; display "Reloaded"
 
-bind f run-shell 'T="Fork ($(date "+%a %H:%M"))"; tmux new-window -a -n "$T" "claude --dangerously-skip-permissions --continue --fork-session"; tmux select-pane -T "$T"'
+bind f run-shell 'T="Fork ($(date "+%a %H:%M"))"; tmux new-window -a -n "$T" "claude --dangerously-skip-permissions --resume --fork-session"; tmux select-pane -T "$T"'
+
+# >>> tmux-claude-status >>>
+source-file /Users/tom/.claude/tmux-claude-status.conf
+# <<< tmux-claude-status <<<

+ 11 - 12
CLAUDE.md

@@ -29,10 +29,17 @@ Use me for: classification, drafting, summarization, extraction.
 Do NOT use me for: routing, retries, deterministic transforms.
 If code can answer, code answers.
 
-## Rule 6 — Token budgets are not advisory
-Per-task: 8,000 tokens. Per-session: 200,000 tokens.
-If approaching budget, summarize and start fresh.
-Surface the breach. Do not silently overrun.
+## Rule 6 — Spend in proportion to stakes
+Trivial task, trivial spend. Deep spends must earn their keep: say what they bought.
+Delegate bulk reading to subagents. Conclusions, not dumps.
+Flag large or unusual spend as it happens, with why.
+Stated limits are ceilings, not suggestions.
+
+Delegation: set the model on every subagent call. Unset inherits the host tier and bills grunt work at it.
+Opus for implementation, design, review, and anything with judgment in it.
+Sonnet for basic research — locating code, gathering facts, bulk reading, summarising.
+Pick the subagent by job: Explore to find code, Plan to design, claude for the rest.
+Never general-purpose — it inherits the host to do work a named agent does cheaper.
 
 ## Rule 7 — Surface conflicts, don't average them
 If two patterns contradict, pick one (more recent / more tested).
@@ -60,11 +67,3 @@ If you genuinely think a convention is harmful, surface it. Don't fork silently.
 "Completed" is wrong if anything was skipped silently.
 "Tests pass" is wrong if any were skipped.
 Default to surfacing uncertainty, not hiding it.
-
-## Addendum
-
-For a second opinion or adversarial review of a plan or diff, shell out to Codex:
-
-```bash
-git diff HEAD | codex exec --model gpt-5.6-sol --sandbox read-only - | tee /tmp/codex-$(date -Iseconds).md
-```

+ 11 - 0
config/ghostty/config

@@ -30,4 +30,15 @@ quick-terminal-position = bottom
 quick-terminal-autohide = false
 quick-terminal-animation-duration = 0.1
 # copy-on-select = false
+
 keybind = shift+enter=text:\x1b\r
+
+# Navigation hierarchy: cmd+alt = tabs. Panes/splits are ctrl+shift+arrow,
+# deliberately left unbound here so they pass straight through to tmux
+# (which owns splits inside the terminal). Same chord works in Zed.
+keybind = cmd+alt+left=previous_tab
+keybind = cmd+alt+right=next_tab
+# Drop Ghostty's default goto_split:up/down -- orphaned leftovers, since
+# cmd+alt+left/right above already claimed this modifier for tabs.
+keybind = cmd+alt+up=unbind
+keybind = cmd+alt+down=unbind

+ 12 - 1
config/zed/keymap.json

@@ -12,7 +12,18 @@
       "cmd-p": "command_palette::Toggle",
       "cmd-shift-p": "project_symbols::Toggle",
       "alt-cmd-o": "workspace::Open",
-      "cmd-alt-n": ["task::Spawn", { "task_name": "agent terminal" }]
+      "cmd-alt-n": ["task::Spawn", { "task_name": "agent terminal" }],
+
+      // Navigation hierarchy, kept parallel with Ghostty + tmux:
+      //   cmd-alt-arrow    -> tabs (flat, left/right only)  [same as Ghostty]
+      //   ctrl-shift-arrow -> panes/splits (the 2D grid)    [same as tmux]
+      // Tabs can't share tmux's shift-arrow here because that's text selection.
+      "cmd-alt-left": "pane::ActivatePreviousItem",
+      "cmd-alt-right": "pane::ActivateNextItem",
+      "ctrl-shift-left": "workspace::ActivatePaneLeft",
+      "ctrl-shift-right": "workspace::ActivatePaneRight",
+      "ctrl-shift-up": "workspace::ActivatePaneUp",
+      "ctrl-shift-down": "workspace::ActivatePaneDown"
     }
   },
   {