Capability-set targeting + Claude Code headless provider + resilience fixes

Capability targeting (new feature):
- loop --capability <SET|ALL> scopes which capability library the loop reasons
  against. data/capability_sets/<name>.json (same schema as capabilities.json)
  holds one-off targeted products; drop a file in and pass its name, no code
  changes. First set: agentminder (Broadcom AgentMinder, agentic-AI identity).
- capabilities.set_active_set() swaps the active library; keyword pre-filter is
  bypassed for small targeted sets so every signal is evaluated. New
  'capability-sets' command lists them; run banner shows the active set.

LLM provider — the '429' was a disguised policy block:
- Anthropic rejects Claude Code OAuth tokens on /v1/messages for non-Claude-Code
  traffic with a fake rate_limit_error (no anthropic-ratelimit-* headers). New
  'claude-cli' provider runs inference through the sanctioned headless path
  (claude -p --append-system-prompt --model), now the default provider.
- OAuth keychain token auto-refresh via headless claude when expired.

Resilience:
- STATE_DIR is overridable (macOS endpoint-security locked output/state via
  com.apple.macl); load_memory tolerates PermissionError; exa usage counter
  tolerates malformed files; run.py handles Ctrl-C cleanly (no traceback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 09:00:02 -04:00
parent c2854ba163
commit fefda7481f
9 changed files with 234 additions and 26 deletions

View File

@@ -6,9 +6,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = ROOT / "data"
OUTPUT_DIR = ROOT / "output"
STATE_DIR = OUTPUT_DIR / "state"
BRIEFS_DIR = OUTPUT_DIR / "briefs"
def _load_dotenv() -> None:
@@ -27,6 +24,15 @@ def _load_dotenv() -> None:
_load_dotenv()
# Paths (after dotenv so OUTPUT_DIR/STATE_DIR overrides in .env apply).
# STATE_DIR is overridable because macOS TCC/endpoint-security software can
# permanently lock a directory tree (com.apple.macl xattr) — pointing at a
# fresh dir is the recovery path (state is regenerable). Relative values are
# resolved against the sales-agent root.
OUTPUT_DIR = ROOT / os.environ.get("OUTPUT_DIR", "output")
STATE_DIR = (ROOT / os.environ.get("STATE_DIR", "")) if os.environ.get("STATE_DIR") else OUTPUT_DIR / "state"
BRIEFS_DIR = OUTPUT_DIR / "briefs"
# --- LLM ---
# Provider abstraction: openai | gemini | anthropic. If LLM_PROVIDER is unset we
# auto-detect from whichever key is present (OpenAI-compatible first). There is NO
@@ -66,6 +72,16 @@ LLM_MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-5")
# keychain ("Claude Code-credentials"), read at runtime so refreshes are picked up.
ANTHROPIC_OAUTH_TOKEN = os.environ.get("ANTHROPIC_OAUTH_TOKEN", "").strip()
ANTHROPIC_USE_KEYCHAIN = os.environ.get("ANTHROPIC_USE_KEYCHAIN", "1").strip() == "1"
# When the keychain token is expired, run a headless `claude` command to make
# Claude Code refresh it, then re-read — the loop self-heals with no human step.
ANTHROPIC_AUTOREFRESH = os.environ.get("ANTHROPIC_AUTOREFRESH", "1").strip() == "1"
# "claude-cli" provider: run inference through Claude Code's official headless
# mode (`claude -p`). This is the sanctioned way to use a Claude subscription
# programmatically — direct API calls with the Claude Code OAuth token are
# policy-blocked (they return a fake 429 "Error"). Requires `claude` on PATH.
CLAUDE_CLI_MODEL = os.environ.get("CLAUDE_CLI_MODEL", "sonnet").strip()
CLAUDE_CLI_TIMEOUT = int(os.environ.get("CLAUDE_CLI_TIMEOUT", "300"))
# Effort level for reasoning models (Sonnet 5 etc.): low | medium | high | max.
ANTHROPIC_EFFORT = os.environ.get("ANTHROPIC_EFFORT", "low").strip()
@@ -74,14 +90,14 @@ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
_provider = os.environ.get("LLM_PROVIDER", "").strip().lower()
if not _provider:
if ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY:
if ANTHROPIC_API_KEY:
_provider = "anthropic"
elif OPENAI_API_KEY:
_provider = "openai"
elif GEMINI_API_KEY:
_provider = "gemini"
else:
_provider = "none"
_provider = "claude-cli" # subscription via Claude Code headless mode
# Selecting a provider whose key is missing => no usable provider.
# ("anthropic" is exempt here: the token may come from the keychain at runtime.)
if _provider == "openai" and not OPENAI_API_KEY:
@@ -100,6 +116,8 @@ LLM_CONFIGURED = LLM_PROVIDER != "none"
def llm_label() -> str:
if LLM_PROVIDER == "none":
return "NONE (no LLM provider configured)"
if LLM_PROVIDER == "claude-cli":
return f"Claude Code headless ({CLAUDE_CLI_MODEL}, subscription)"
if LLM_PROVIDER == "openai":
host = OPENAI_BASE_URL.split("//")[-1].split("/")[0]
return f"OpenAI-compatible ({OPENAI_MODEL} @ {host})"