From c7dfbc9b5b997091edc93b59d49cb9004012bbb9 Mon Sep 17 00:00:00 2001 From: ArthurBernard Date: Sat, 4 Jul 2026 16:07:48 +0200 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20sync=20claude=20config=20=E2=80=94?= =?UTF-8?q?=20hooks=20canon,=20session-model=20policy,=20live-trading=20pe?= =?UTF-8?q?rmission=20guardrails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/pr_decision_guard.py | 36 +++++++++++------------------- .claude/hooks/session_start.py | 9 +++----- .claude/settings.json | 10 +++++++++ .gitignore | 6 +++++ CLAUDE.md | 13 ++++++----- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/.claude/hooks/pr_decision_guard.py b/.claude/hooks/pr_decision_guard.py index 788fc303..7875eff1 100755 --- a/.claude/hooks/pr_decision_guard.py +++ b/.claude/hooks/pr_decision_guard.py @@ -1,18 +1,16 @@ #!/usr/bin/env python3 """PreToolUse(Bash) safety net for `gh pr create`. -If the branch changes structural code under / (excluding tests and -the reference-only legacy/ tree) but adds no entry to the decision journal -(), ask the user to confirm before the PR is opened. This is only a -net — the real capture is the `/finish-task` "décision" step; here we just make -the omission visible. +If the branch changes structural code under / (excluding tests) but +adds no entry to the decision journal (), ask the user to confirm +before the PR is opened. This is only a net — the real capture is the +`/finish-task` decision step; here we just make the omission visible. Emits a PreToolUse "ask" decision (human confirms / overrides) rather than a hard deny, so a genuinely decision-free PR isn't deterministically blocked. No-op for any command other than `gh pr create`, or if the project doesn't declare both a `package_dir` and a `decisions` path in .claude/workflow.json. """ - from __future__ import annotations import json @@ -52,10 +50,7 @@ def main() -> None: try: changed = subprocess.run( ["git", "diff", f"{base}...HEAD", "--name-only"], - cwd=ROOT, - capture_output=True, - text=True, - timeout=10, + cwd=ROOT, capture_output=True, text=True, timeout=10, ).stdout.split() except Exception: _allow() @@ -65,7 +60,6 @@ def is_structural(path: str) -> bool: return ( path.startswith(f"{pkg}/") and "/tests/" not in path - and not path.startswith(f"{pkg}/legacy/") and not name.startswith("test_") ) @@ -73,21 +67,17 @@ def is_structural(path: str) -> bool: if touched_code and decisions not in changed: reason = ( f"This branch changes structural code under {pkg}/ but adds no entry " - f"to {decisions}. Capture the *why* (the /finish-task 'décision' step) " + f"to {decisions}. Capture the *why* (the /finish-task decision step) " f"before opening the PR — or confirm this PR genuinely needs no " f"decision entry." ) - print( - json.dumps( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "ask", - "permissionDecisionReason": reason, - } - } - ) - ) + print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "ask", + "permissionDecisionReason": reason, + } + })) sys.exit(0) _allow() diff --git a/.claude/hooks/session_start.py b/.claude/hooks/session_start.py index 85c6bbde..749117d7 100755 --- a/.claude/hooks/session_start.py +++ b/.claude/hooks/session_start.py @@ -6,7 +6,6 @@ Claude Code. Reads paths from .claude/workflow.json; degrades gracefully if the descriptor or any target file is missing (prints what it can, never errors out). """ - from __future__ import annotations import json @@ -29,10 +28,7 @@ def _branch() -> str: try: out = subprocess.run( ["git", "branch", "--show-current"], - cwd=ROOT, - capture_output=True, - text=True, - timeout=5, + cwd=ROOT, capture_output=True, text=True, timeout=5, ) return out.stdout.strip() or "(detached)" except Exception: @@ -41,7 +37,8 @@ def _branch() -> str: def main() -> None: cfg = _cfg() - lines = [f"trading_bot workflow — branch: {_branch()}"] + label = cfg.get("package_dir", ROOT.name) + lines = [f"{label} workflow — branch: {_branch()}"] roadmap_rel = cfg.get("roadmap", "doc/dev/07-roadmap.md") roadmap = ROOT / roadmap_rel diff --git a/.claude/settings.json b/.claude/settings.json index b0197317..a0eacaac 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,4 +1,14 @@ { + "permissions": { + "deny": [ + "Read(./.env)", + "Read(./.env.*)" + ], + "ask": [ + "Bash(pytest -m network:*)", + "Bash(python -m pytest -m network:*)" + ] + }, "hooks": { "SessionStart": [ { diff --git a/.gitignore b/.gitignore index 78416479..a216fe24 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,9 @@ strategies/* !strategies/README.md !strategies/example/ !strategies/another_example/ + +# Claude Code workflow — local-only state (shared workflow.json / settings.json +# and hooks ARE committed; per-machine overrides are not) +.claude/settings.local.json +.claude/scheduled_tasks.lock +.claude/worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md index 132e98b9..9457bcbc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,8 @@ mypy trading_bot/ ## Common conventions + + Shared across my repos, mirrored from `~/.claude/CLAUDE.md` (the single source of truth — if they ever disagree, the global file wins). Restated here so the repo stays self-contained: @@ -72,8 +74,9 @@ stays self-contained: `Co-Authored-By` trailers** (personal repo). - **One PR = one concern**, small and disposable — a big plan ships as several small atomic PRs, never one catch-all branch. -- **Model: `opus`, always** — interactive sessions and every spawned subagent; a plan - leaf's `complexity` is effort/ordering only and never downgrades the model. +- **Model: the session model, always** — interactive sessions and every spawned + subagent inherit it (set in `~/.claude/settings.json`); a plan leaf's + `complexity` is effort/ordering only and never selects or downgrades the model. - **Before every commit** — `python -m pytest` and `ruff check trading_bot/` must pass (run under the `trading_bot_env` pyenv-virtualenv, auto-activated by `.python-version`). @@ -97,9 +100,9 @@ viable. The loop: `/plan` (decompose into a `doc/dev/plans//` tree — single leaf for a trivial task, a global `00-plan.md` + leaves otherwise — and open the **plan PR** onto `develop`) → -`/execute-leaf next` (cut the leaf branch, **spawn an agent at the model -derived from the leaf's `complexity`**, which implements + tests + **verifies on -real data**) → +`/execute-leaf next` (cut the leaf branch, **spawn an agent — session +model, effort derived from the leaf's `complexity`** — which implements + tests + +**verifies on real data**) → `/finish-task` (tests, ADR, CHANGELOG, leaf PR, archive the leaf, tick the global checklist) → … per leaf … → last leaf removes the roadmap line → `/release`. From edd25d6d07db2eb6d9054cb27307b9ecd2d7c296 Mon Sep 17 00:00:00 2001 From: ArthurBernard Date: Sat, 4 Jul 2026 16:32:39 +0200 Subject: [PATCH 2/2] docs: tiered execution model policy (judgement on session model, leaves tiered by complexity with escalation) --- CLAUDE.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9457bcbc..5aaeed3c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,9 +74,11 @@ stays self-contained: `Co-Authored-By` trailers** (personal repo). - **One PR = one concern**, small and disposable — a big plan ships as several small atomic PRs, never one catch-all branch. -- **Model: the session model, always** — interactive sessions and every spawned - subagent inherit it (set in `~/.claude/settings.json`); a plan leaf's - `complexity` is effort/ordering only and never selects or downgrades the model. +- **Model: session model for judgement, tiered execution** — sessions, + orchestration and the judgement skills run on the session model (set in + `~/.claude/settings.json`); plan-leaf execution runs at the tier derived from + the leaf's `complexity` (`low→haiku / medium→sonnet / high→session model`), + escalating one tier on failed tests/verification. - **Before every commit** — `python -m pytest` and `ruff check trading_bot/` must pass (run under the `trading_bot_env` pyenv-virtualenv, auto-activated by `.python-version`). @@ -100,9 +102,9 @@ viable. The loop: `/plan` (decompose into a `doc/dev/plans//` tree — single leaf for a trivial task, a global `00-plan.md` + leaves otherwise — and open the **plan PR** onto `develop`) → -`/execute-leaf next` (cut the leaf branch, **spawn an agent — session -model, effort derived from the leaf's `complexity`** — which implements + tests + -**verifies on real data**) → +`/execute-leaf next` (cut the leaf branch, **spawn an agent at the tier +derived from the leaf's `complexity`, escalating a tier on failure**, which +implements + tests + **verifies on real data**) → `/finish-task` (tests, ADR, CHANGELOG, leaf PR, archive the leaf, tick the global checklist) → … per leaf … → last leaf removes the roadmap line → `/release`.