Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 54bd533

Browse files
chernistryclaude
andcommitted
feat: Claude Code Routines integration (rt-001, rt-002) + version 1.7.5
Add Routine trigger source for receiving webhook payloads from Claude Code Routines with GitHub context extraction. Add Routine offload adapter with /fire API support, cost tracking, and three-tier routing (local/Cloudflare/Routine). Add MCP scenario discovery tools (list_scenarios, get_scenario_detail). Include 3 scenario templates: pr-review-comprehensive, nightly-maintenance, deploy-verification. 30 new tests, all passing. Vulture clean. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent bac7ed8 commit 54bd533

11 files changed

Lines changed: 656 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "bernstein"
7-
version = "1.7.4"
7+
version = "1.7.5"
88
description = "Declarative agent orchestration for engineering teams"
99
readme = "README.md"
1010
requires-python = ">=3.12"
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Claude Code Routine adapter — offloads tasks to Anthropic cloud via /fire API."""
2+
3+
from __future__ import annotations
4+
5+
import time
6+
from dataclasses import dataclass, field
7+
from typing import Any
8+
9+
# Routine API constants
10+
ROUTINE_API_BASE = "https://api.anthropic.com/v1/claude_code/routines"
11+
ROUTINE_BETA_HEADER = "experimental-cc-routine-2026-04-01"
12+
ROUTINE_API_VERSION = "2023-06-01"
13+
14+
15+
@dataclass(frozen=True)
16+
class RoutineTriggerInfo:
17+
"""Connection info for a pre-configured Routine."""
18+
19+
trigger_id: str
20+
token: str
21+
role: str = ""
22+
description: str = ""
23+
24+
25+
@dataclass
26+
class RoutineFireResult:
27+
"""Result of firing a Routine via the /fire API."""
28+
29+
session_id: str
30+
session_url: str
31+
fired_at: float = field(default_factory=time.time)
32+
33+
34+
@dataclass
35+
class RoutineAdapterConfig:
36+
"""Configuration for the Claude Code Routine adapter."""
37+
38+
enabled: bool = False
39+
routine_triggers: dict[str, RoutineTriggerInfo] = field(default_factory=dict)
40+
default_trigger_id: str = ""
41+
default_trigger_token: str = ""
42+
poll_interval_seconds: int = 30
43+
max_wait_minutes: int = 60
44+
branch_prefix: str = "claude/bernstein-"
45+
max_daily_fires: int = 20
46+
47+
48+
@dataclass
49+
class RoutineCostTracker:
50+
"""Track daily Routine fires to prevent runaway billing."""
51+
52+
daily_fires: int = 0
53+
max_daily_fires: int = 20
54+
_day_start: float = field(default_factory=time.time)
55+
56+
def check_budget(self) -> bool:
57+
"""Return True if within daily fire limit."""
58+
now = time.time()
59+
if now - self._day_start > 86400:
60+
self.daily_fires = 0
61+
self._day_start = now
62+
return self.daily_fires < self.max_daily_fires
63+
64+
def record_fire(self) -> None:
65+
"""Record a fire event."""
66+
now = time.time()
67+
if now - self._day_start > 86400:
68+
self.daily_fires = 0
69+
self._day_start = now
70+
self.daily_fires += 1
71+
72+
73+
def build_fire_payload(
74+
*,
75+
goal: str,
76+
role: str,
77+
task_id: str = "",
78+
repo: str = "",
79+
base_branch: str = "main",
80+
context_files: list[str] | None = None,
81+
test_command: str = "",
82+
) -> dict[str, str]:
83+
"""Build the /fire API request payload.
84+
85+
The `text` field is appended to the Routine's saved prompt
86+
as a one-shot user turn.
87+
"""
88+
parts = [
89+
"## Bernstein Task Assignment\n",
90+
f"**Goal**: {goal}",
91+
f"**Role**: {role}",
92+
]
93+
if task_id:
94+
parts.append(f"**Task ID**: {task_id}")
95+
if repo:
96+
parts.append(f"\n### Repository: {repo}")
97+
parts.append(f"Base branch: {base_branch}")
98+
if context_files:
99+
parts.append(f"Related files: {', '.join(context_files[:10])}")
100+
if test_command:
101+
parts.append(f"\n### Verification\nRun before pushing: `{test_command}`")
102+
103+
parts.append(f"\nWork on branch `claude/bernstein-{task_id or role}`")
104+
105+
return {"text": "\n".join(parts)}
106+
107+
108+
def build_fire_headers(token: str) -> dict[str, str]:
109+
"""Build HTTP headers for the /fire API call."""
110+
return {
111+
"Authorization": f"Bearer {token}",
112+
"anthropic-beta": ROUTINE_BETA_HEADER,
113+
"anthropic-version": ROUTINE_API_VERSION,
114+
"Content-Type": "application/json",
115+
}
116+
117+
118+
def build_fire_url(trigger_id: str) -> str:
119+
"""Build the /fire endpoint URL for a given trigger ID."""
120+
return f"{ROUTINE_API_BASE}/{trigger_id}/fire"
121+
122+
123+
def parse_fire_response(data: dict[str, Any]) -> RoutineFireResult:
124+
"""Parse the /fire API response into a RoutineFireResult."""
125+
return RoutineFireResult(
126+
session_id=str(data.get("claude_code_session_id", "")),
127+
session_url=str(data.get("claude_code_session_url", "")),
128+
)
129+
130+
131+
def select_trigger(
132+
config: RoutineAdapterConfig,
133+
role: str,
134+
) -> tuple[str, str]:
135+
"""Select the appropriate trigger ID and token for a given role.
136+
137+
Returns (trigger_id, token) tuple.
138+
Falls back to default trigger if no role-specific one is configured.
139+
"""
140+
if role in config.routine_triggers:
141+
trigger = config.routine_triggers[role]
142+
return trigger.trigger_id, trigger.token
143+
return config.default_trigger_id, config.default_trigger_token
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Claude Code Routine trigger source — normalizes Routine webhook payloads."""
2+
3+
from __future__ import annotations
4+
5+
import time
6+
from typing import Any
7+
8+
from bernstein.core.tasks.models import TriggerEvent
9+
10+
11+
def normalize_routine_webhook(
12+
_headers: dict[str, str],
13+
payload: dict[str, Any],
14+
) -> TriggerEvent:
15+
"""Normalize a Claude Code Routine webhook payload into a TriggerEvent.
16+
17+
Called when a Routine session POSTs to Bernstein's webhook endpoint.
18+
The Routine typically includes GitHub context (PR number, branch, etc.)
19+
and optionally a scenario_id to invoke.
20+
"""
21+
# Extract GitHub context if present
22+
github_ctx = payload.get("github", {})
23+
scenario_id = payload.get("scenario_id", "")
24+
goal = payload.get("goal", "")
25+
26+
metadata: dict[str, Any] = {
27+
"source_type": "routine",
28+
"routine_session_id": payload.get("session_id", ""),
29+
"routine_session_url": payload.get("session_url", ""),
30+
}
31+
32+
if github_ctx:
33+
metadata["github_event"] = github_ctx.get("event_type", "")
34+
metadata["github_repo"] = github_ctx.get("repo", "")
35+
metadata["github_pr_number"] = github_ctx.get("pr_number")
36+
metadata["github_ref"] = github_ctx.get("ref", "")
37+
metadata["github_author"] = github_ctx.get("author", "")
38+
39+
if scenario_id:
40+
metadata["scenario_id"] = scenario_id
41+
42+
message = goal or payload.get("text", "") or str(payload.get("message", ""))
43+
44+
return TriggerEvent(
45+
source="routine",
46+
timestamp=time.time(),
47+
raw_payload=payload,
48+
message=message[:500],
49+
metadata=metadata,
50+
)
51+
52+
53+
def extract_github_context(payload: dict[str, Any]) -> dict[str, Any]:
54+
"""Extract structured GitHub context from a Routine webhook payload.
55+
56+
Returns a dict with pr_number, pr_title, repo, ref, author, labels,
57+
changed_files — useful for task decomposition.
58+
"""
59+
gh = payload.get("github", {})
60+
return {
61+
"event_type": gh.get("event_type", ""),
62+
"repo": gh.get("repo", ""),
63+
"ref": gh.get("ref", ""),
64+
"pr_number": gh.get("pr_number"),
65+
"pr_title": gh.get("pr_title", ""),
66+
"pr_body": gh.get("pr_body", ""),
67+
"issue_number": gh.get("issue_number"),
68+
"issue_title": gh.get("issue_title", ""),
69+
"author": gh.get("author", ""),
70+
"labels": gh.get("labels", []),
71+
"changed_files": gh.get("changed_files", []),
72+
}

src/bernstein/mcp/routine_tools.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""MCP tools for Claude Code Routine integration."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from bernstein.core.planning.scenario_library import (
9+
load_scenario_library,
10+
)
11+
12+
# Default scenario directory
13+
_SCENARIOS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "templates" / "scenarios"
14+
15+
16+
def list_scenarios(scenarios_dir: Path | None = None) -> list[dict[str, Any]]:
17+
"""List all available Bernstein scenarios.
18+
19+
Returns a list of scenario summaries with id, name, description,
20+
tags, task_count, and roles.
21+
"""
22+
root = scenarios_dir or _SCENARIOS_DIR
23+
library = load_scenario_library(root)
24+
return [
25+
{
26+
"id": recipe.scenario_id,
27+
"name": recipe.name,
28+
"description": recipe.description,
29+
"tags": list(recipe.tags),
30+
"task_count": len(recipe.tasks),
31+
"roles": sorted(set(t.role for t in recipe.tasks)),
32+
"version": recipe.version,
33+
}
34+
for recipe in library.scenarios.values()
35+
]
36+
37+
38+
def get_scenario_detail(scenario_id: str, scenarios_dir: Path | None = None) -> dict[str, Any] | None:
39+
"""Get detailed information about a specific scenario.
40+
41+
Returns full scenario with task breakdown, or None if not found.
42+
"""
43+
root = scenarios_dir or _SCENARIOS_DIR
44+
library = load_scenario_library(root)
45+
recipe = library.get(scenario_id)
46+
if recipe is None:
47+
return None
48+
return {
49+
"id": recipe.scenario_id,
50+
"name": recipe.name,
51+
"description": recipe.description,
52+
"tags": list(recipe.tags),
53+
"version": recipe.version,
54+
"tasks": [
55+
{
56+
"title": t.title,
57+
"description": t.description,
58+
"role": t.role,
59+
"priority": t.priority,
60+
"scope": t.scope,
61+
"complexity": t.complexity,
62+
}
63+
for t in recipe.tasks
64+
],
65+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
id: deploy-verification
2+
name: Deploy Verification
3+
description: Post-deploy smoke tests and regression detection
4+
tags: [deploy, verify, ci, api]
5+
version: "1.0"
6+
tasks:
7+
- title: API smoke tests
8+
description: Run critical API endpoint smoke tests against the deployed environment
9+
role: qa
10+
priority: 0
11+
scope: medium
12+
complexity: moderate
13+
- title: Error log scan
14+
description: Scan error logs for new exceptions or regressions since deploy
15+
role: devops
16+
priority: 0
17+
scope: small
18+
complexity: simple
19+
- title: Performance baseline check
20+
description: Compare response times and resource usage against pre-deploy baseline
21+
role: backend
22+
priority: 1
23+
scope: small
24+
complexity: simple
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
id: nightly-maintenance
2+
name: Nightly Maintenance
3+
description: Automated nightly maintenance — dependency updates, stale branch cleanup, lint fixes
4+
tags: [maintenance, ci, cleanup, schedule]
5+
version: "1.0"
6+
tasks:
7+
- title: Dependency security audit
8+
description: Check for known vulnerabilities in dependencies, update patch versions
9+
role: security
10+
priority: 0
11+
scope: medium
12+
complexity: moderate
13+
- title: Lint and format fixes
14+
description: Run linters and formatters, fix auto-fixable issues, commit results
15+
role: backend
16+
priority: 1
17+
scope: small
18+
complexity: simple
19+
- title: Stale branch cleanup
20+
description: Identify and delete merged or stale branches older than 30 days
21+
role: devops
22+
priority: 2
23+
scope: small
24+
complexity: simple
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
id: pr-review-comprehensive
2+
name: Comprehensive PR Review
3+
description: Multi-role parallel PR review with security, performance, style, and test coverage checks
4+
tags: [review, ci, quality, github]
5+
version: "1.0"
6+
tasks:
7+
- title: Security review
8+
description: Review PR for security vulnerabilities, injection risks, auth issues, and sensitive data exposure
9+
role: security
10+
priority: 0
11+
scope: medium
12+
complexity: moderate
13+
- title: Performance review
14+
description: Review PR for performance regressions, N+1 queries, unnecessary allocations, and scalability concerns
15+
role: backend
16+
priority: 1
17+
scope: medium
18+
complexity: moderate
19+
- title: Style and standards review
20+
description: Review PR for code style, naming conventions, documentation, and adherence to project standards
21+
role: reviewer
22+
priority: 1
23+
scope: small
24+
complexity: simple
25+
- title: Test coverage analysis
26+
description: Analyze test coverage for changed code, identify untested paths, suggest missing test cases
27+
role: qa
28+
priority: 2
29+
scope: small
30+
complexity: simple

0 commit comments

Comments
 (0)