coding-guardrails 0.7.2__tar.gz → 0.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. coding_guardrails-0.7.4/CLAUDE.md +174 -0
  2. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/PKG-INFO +1 -1
  3. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/pyproject.toml +1 -1
  4. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/handler.py +14 -7
  5. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/loop_detection.py +6 -2
  6. coding_guardrails-0.7.2/CLAUDE.md +0 -189
  7. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/.github/workflows/ci.yaml +0 -0
  8. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/.gitignore +0 -0
  9. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/Dockerfile +0 -0
  10. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/LICENSE +0 -0
  11. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/README.md +0 -0
  12. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/aider.yaml +0 -0
  13. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/claude-code.yaml +0 -0
  14. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/guardrail-config.yaml +0 -0
  15. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/opencode.json +0 -0
  16. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/configs/pi.yaml +0 -0
  17. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docker-compose.yaml +0 -0
  18. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/agents.md +0 -0
  19. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/architecture.md +0 -0
  20. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/models.md +0 -0
  21. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/docs/rules.md +0 -0
  22. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/__init__.py +0 -0
  23. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/__main__.py +0 -0
  24. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/cli.py +0 -0
  25. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/config.py +0 -0
  26. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/eval.py +0 -0
  27. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/middleware.py +0 -0
  28. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/__init__.py +0 -0
  29. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/profiles.py +0 -0
  30. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/models/registry.py +0 -0
  31. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/__init__.py +0 -0
  32. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/client.py +0 -0
  33. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/layer1.py +0 -0
  34. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/proxy/server.py +0 -0
  35. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/__init__.py +0 -0
  36. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/base.py +0 -0
  37. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/commands.py +0 -0
  38. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/network.py +0 -0
  39. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/path_safety.py +0 -0
  40. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/prerequisites.py +0 -0
  41. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/secrets.py +0 -0
  42. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/sensitive_files.py +0 -0
  43. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/sequencing.py +0 -0
  44. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/session_budget.py +0 -0
  45. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/thoroughness.py +0 -0
  46. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/tool_resolution.py +0 -0
  47. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/src/coding_guardrails/rules/value_fidelity.py +0 -0
  48. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/__init__.py +0 -0
  49. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/integration/__init__.py +0 -0
  50. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/integration/test_proxy.py +0 -0
  51. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/__init__.py +0 -0
  52. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_commands.py +0 -0
  53. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_config.py +0 -0
  54. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_loop_detection.py +0 -0
  55. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_middleware.py +0 -0
  56. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_network.py +0 -0
  57. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_path_safety.py +0 -0
  58. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_prerequisites.py +0 -0
  59. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_secrets.py +0 -0
  60. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_sensitive_files.py +0 -0
  61. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_sequencing.py +0 -0
  62. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_session_budget.py +0 -0
  63. {coding_guardrails-0.7.2 → coding_guardrails-0.7.4}/tests/unit/test_tool_resolution.py +0 -0
@@ -0,0 +1,174 @@
1
+ # Coding Guardrails
2
+
3
+ An LLM proxy with safety guardrails, built on [Forge](https://github.com/antoinezambelli/forge) (vendored at `.vendors/forge/`).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ source .venv/bin/activate
9
+ pytest tests/unit/ -q # 233 tests, ~0.1s
10
+ uv pip install -e ".[dev]" # refresh editable install
11
+ ```
12
+
13
+ ## Architecture
14
+
15
+ Two-layer proxy between agent and LLM:
16
+
17
+ ```
18
+ Agent → :8081 (our proxy)
19
+ ├── Layer 1: Forge (rescue, validate, retry, thinking capture)
20
+ ├── Layer 2: Coding Guardrails (11 rules)
21
+ └── → :8080 (llama-server / LLM backend)
22
+ ```
23
+
24
+ ### Request Flow
25
+
26
+ 1. Agent sends OpenAI-compatible request to `:8081`
27
+ 2. **Preprocessing**: empty user messages fixed, stale assistant text stripped
28
+ 3. **Tool enforcement**: for real coding agents (bash/read/edit/write tools), inject guidance
29
+ 4. **Layer 1 (Forge)**: run inference with rescue + retry + thinking capture
30
+ 5. **Layer 2 (Guardrails)**: check tool calls against 11 rules
31
+ 6. **Response**: blocked calls return text nudge to agent; allowed calls pass through
32
+
33
+ ### Key Modules
34
+
35
+ | File | Purpose |
36
+ |------|---------|
37
+ | `proxy/handler.py` | Request pipeline: preprocessing → L1 → L2 → response |
38
+ | `proxy/layer1.py` | Instrumented Forge wrapper with thinking capture |
39
+ | `proxy/client.py` | `SafeLlamafileClient` — thinking tokens, max_tokens |
40
+ | `proxy/server.py` | Asyncio HTTP server, `/v1/chat/completions` |
41
+ | `middleware.py` | Composes all rules, `check()` / `record()` API |
42
+ | `cli.py` | `coding-guardrails serve` CLI |
43
+ | `rules/` | 11 guardrail rules |
44
+
45
+ ### Guardrail Rules
46
+
47
+ | Rule | Action | Purpose |
48
+ |------|--------|---------|
49
+ | `prerequisites` | block | Read-before-edit |
50
+ | `path_safety` | block | Path traversal |
51
+ | `command_safety` | block | Destructive commands |
52
+ | `network` | block | Network access |
53
+ | `sensitive_files` | block | Protect sensitive files |
54
+ | `secrets` | block/mask | Secret detection |
55
+ | `loop_detection` | nudge→block | Repeated identical calls (3 nudge, 5 block) |
56
+ | `session_budget` | nudge | File/command budgets |
57
+ | `thoroughness` | nudge | Premature terminal submission |
58
+ | `sequencing` | nudge | Test-after-change |
59
+ | `tool_resolution` | nudge | Empty/error results |
60
+
61
+ ## Production Rules
62
+
63
+ ### Block Response Design
64
+
65
+ When Layer 2 blocks a tool call, the proxy returns a **text response** (not an empty tool call).
66
+
67
+ **Why**: Returning an empty tool call with the blocked tool name confuses agents — they see a tool call, execute it with empty args, get nothing, and retry the same call. Returning text makes the nudge visible to the agent and the model, providing a clear escape path.
68
+
69
+ **Loop detection blocks specifically** tell the model: "If the task is done, call respond() with your final answer. Otherwise try a completely different approach." This gives the model an explicit exit instead of vague guidance.
70
+
71
+ ### Tool Enforcement
72
+
73
+ Only injected for **real coding agents** (requests that include bash/read/edit/write tools). The enforcement says:
74
+
75
+ > "Respond by calling tools. If the task is complete, call respond(). If unsure, call bash with 'echo ready'."
76
+
77
+ **Never** say "NEVER respond with plain text" — the model needs to finish tasks. The `respond()` tool is the proper terminal action.
78
+
79
+ ### For eval/workflow requests (no coding tools)
80
+
81
+ No enforcement, no injection, no modification. The proxy is a transparent pass-through.
82
+
83
+ ### Thinking Capture & Injection
84
+
85
+ Layer 1 captures thinking tokens from the model's response. On retry (failed validation), the thinking is injected into the retry nudge so the model doesn't re-think from scratch.
86
+
87
+ ### Nudges vs Blocks
88
+
89
+ - **Nudge**: advisory — the call proceeds, message is logged. Only visible if the agent's runner supports nudge injection (Forge does for its own evals). For streaming agents (Pi), nudges are silently logged.
90
+ - **Block**: hard stop — the call is NOT passed through. The proxy returns a text response to the agent explaining why. This is the only reliable way to change model behavior mid-conversation.
91
+
92
+ ## Running the Proxy
93
+
94
+ ### tmux Sessions
95
+
96
+ | Session | Purpose | Port |
97
+ |---------|---------|------|
98
+ | `llm-server` | `llama-server` with model | :8080 |
99
+ | `guardrails` | `coding-guardrails serve` | :8081 |
100
+
101
+ ```bash
102
+ # Start proxy
103
+ source .venv/bin/activate
104
+ coding-guardrails serve \
105
+ --backend-url http://localhost:8080 \
106
+ --model Qwen3.5-9B-UD-Q4_K_XL \
107
+ --port 8081 -v
108
+ ```
109
+
110
+ ### Client Quirks
111
+
112
+ - `SafeLlamafileClient` needs `gguf_path` (stem = model name): use `/tmp/Qwen3.5-9B-UD-Q4_K_XL.gguf`
113
+ - `recommended_sampling=False` — model not in Forge's registry
114
+
115
+ ## Testing
116
+
117
+ ```bash
118
+ pytest tests/unit/ -q # All 233 tests
119
+ pytest tests/unit/ -q -k "loop" # Specific rule
120
+ ```
121
+
122
+ All 233 tests must pass before committing.
123
+
124
+ ## Eval
125
+
126
+ ```bash
127
+ # Full Forge 30-scenario benchmark (proxy mode)
128
+ python eval/scripts/run_forge_eval.py --mode proxy --runs 5
129
+
130
+ # Direct vs proxy comparison
131
+ python eval/scripts/run_forge_eval.py --mode both --runs 5
132
+
133
+ # Specific scenarios
134
+ python eval/scripts/run_forge_eval.py --mode proxy --scenario data_gap_recovery_extended
135
+
136
+ # Layer 2 guardrails
137
+ python eval/scripts/run_layer2_eval.py
138
+ ```
139
+
140
+ Results go to `eval/runs/<timestamp>/` (gitignored).
141
+
142
+ **Best result: 93% (140/150)** on Forge 30-scenario eval, +9pp over Forge's 84% baseline.
143
+
144
+ ## Development Guidelines
145
+
146
+ - **Do NOT hack Forge source** — extend via public API, subclassing, wrapping
147
+ - All 233 unit tests must pass
148
+ - No hardcoded scenario-specific logic
149
+ - Block responses must return **text**, not empty tool calls
150
+ - Enforcement prompts must mention `respond()` as the exit tool
151
+ - No injection/modification for non-coding-agent requests
152
+
153
+ ## Versioning & Releases
154
+
155
+ SemVer: `MAJOR.MINOR.PATCH`
156
+
157
+ - **PATCH**: Bug fixes, no new features
158
+ - **MINOR**: New features, new rules, backward-compatible
159
+ - **MAJOR**: Breaking API changes
160
+
161
+ ### Release Process
162
+
163
+ 1. Update `version` in `pyproject.toml`
164
+ 2. Update README/CLAUDE.md if needed
165
+ 3. `pytest tests/unit/ -q` — all 233 must pass
166
+ 4. `git commit -m "vX.Y.Z: description" && git push`
167
+ 5. `git tag vX.Y.Z && git push origin vX.Y.Z`
168
+ 6. GitHub Actions: test → build → verify CLI → publish to PyPI
169
+
170
+ ### CI Pipeline
171
+
172
+ - Push to `main`: tests + lint
173
+ - Tag `v*`: build → verify → publish to PyPI (trusted publishing, no tokens)
174
+ - Refresh local: `uv pip install -e ".[dev]" && coding-guardrails --version`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-guardrails
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: Safe, reliable local coding agent proxy. Forge (rescue, retry, thinking capture) + 11 composable guardrail rules. 93% on Forge eval (Qwen3.5-9B).
5
5
  Project-URL: Homepage, https://github.com/stawils/coding-guardrails
6
6
  Project-URL: Repository, https://github.com/stawils/coding-guardrails
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coding-guardrails"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
8
  description = "Safe, reliable local coding agent proxy. Forge (rescue, retry, thinking capture) + 11 composable guardrail rules. 93% on Forge eval (Qwen3.5-9B)."
9
9
  requires-python = ">=3.12"
10
10
  license = "MIT"
@@ -221,8 +221,9 @@ async def handle_chat_completions(
221
221
 
222
222
  if _REAL_AGENT_TOOLS & tool_names_lower:
223
223
  enforcement = (
224
- "CRITICAL: You MUST always respond by calling bash, read, edit, or write. "
225
- "Never respond with plain text. "
224
+ "IMPORTANT: Respond by calling tools (bash, read, edit, write). "
225
+ "If you have completed the task and need to give a final answer, "
226
+ "call the respond tool. "
226
227
  "If unsure what to do, call bash with 'echo ready'."
227
228
  )
228
229
  if openai_messages:
@@ -313,11 +314,15 @@ async def handle_chat_completions(
313
314
  other_calls = [tc for tc in tool_calls if tc.tool != RESPOND_TOOL_NAME]
314
315
 
315
316
  if respond_calls and not other_calls:
317
+ # Convert respond() to text — most agents (Pi, Cline, etc.)
318
+ # don't have a respond tool. The model is saying "I'm done."
319
+ msg = respond_calls[0].args.get("message", respond_calls[0].args.get("answer", ""))
316
320
  attempts_tag = f"[%d attempt%s]" % (attempts, "s" if attempts != 1 else "") if attempts > 1 else ""
317
- logger.info("✅ Layer 1 done %s (%s, respond: %s)",
318
- attempts_tag, _fmt_elapsed(elapsed_l1),
319
- _short(respond_calls[0].args.get("message", respond_calls[0].args.get("answer", "")), 60))
320
- return tool_calls_to_openai(respond_calls, model=model_name)
321
+ logger.info("✅ Layer 1 done %s (%s, respond → text: %s)",
322
+ attempts_tag, _fmt_elapsed(elapsed_l1), _short(msg, 60))
323
+ if is_stream:
324
+ return text_to_sse_events(msg, model=model_name)
325
+ return text_response_to_openai(msg, model=model_name)
321
326
 
322
327
  if not other_calls:
323
328
  logger.info("⚠️ No actionable tool calls")
@@ -375,7 +380,9 @@ async def handle_chat_completions(
375
380
 
376
381
  if is_stream:
377
382
  return text_to_sse_events(nudge_text, model=model_name)
378
- return _make_block_response(block.tool, nudge_text, model=model_name)
383
+ # Return as text so the agent sees the nudge directly.
384
+ # Empty-args tool-call blocks confuse agents into retrying.
385
+ return text_response_to_openai(nudge_text, model=model_name)
379
386
 
380
387
  # All clear
381
388
  logger.info("✅ Request PASSED (%s)", _fmt_elapsed(elapsed_l2))
@@ -78,7 +78,9 @@ class LoopDetectionRule:
78
78
  return RuleResult.block(
79
79
  call.tool,
80
80
  nudge=f"You've called {call.tool} with the same arguments "
81
- f"{count + 1} times. This isn't working — try a different approach.",
81
+ f"{count + 1} times. This isn't working. "
82
+ f"If the task is done, call respond() with your final answer. "
83
+ f"Otherwise, try a completely different approach.",
82
84
  reason=f"loop detected: {call.tool} repeated {count + 1}x",
83
85
  )
84
86
 
@@ -86,7 +88,9 @@ class LoopDetectionRule:
86
88
  return RuleResult.nudge(
87
89
  call.tool,
88
90
  message=f"You've tried {call.tool} {count + 1} times with "
89
- "the same arguments. Consider trying a different approach.",
91
+ "the same arguments. If the task is complete, "
92
+ "call respond() with your answer. "
93
+ "Otherwise try a different approach.",
90
94
  )
91
95
 
92
96
  # ── Check 2: Stagnation (cycling same few tools, different args) ──
@@ -1,189 +0,0 @@
1
- # Coding Guardrails
2
-
3
- An LLM proxy with safety guardrails, built on [Forge](https://github.com/antoinezambelli/forge) (vendored at `.vendors/forge/`).
4
-
5
- ## Quick Start
6
-
7
- ```bash
8
- # Activate venv
9
- source .venv/bin/activate
10
-
11
- # Run tests
12
- pytest tests/unit/ -q # 233 tests, ~0.1s
13
-
14
- # Install editable
15
- pip install -e .
16
- ```
17
-
18
- ## Architecture
19
-
20
- Two-layer proxy sitting between agent and LLM:
21
-
22
- ```
23
- Agent → :8081 (our proxy)
24
- ├── Layer 1: Forge (rescue, validate, retry, thinking capture)
25
- ├── Layer 2: Coding Guardrails (11 rules)
26
- └── → :8080 (llama-server / LLM backend)
27
- ```
28
-
29
- ### Key Modules
30
-
31
- | File | Purpose |
32
- |------|---------|
33
- | `src/coding_guardrails/proxy/handler.py` | Request pipeline: preprocessing → L1 (Forge) → L2 (guardrails) → response |
34
- | `src/coding_guardrails/proxy/layer1.py` | Instrumented wrapper around Forge's `run_inference()` with thinking capture |
35
- | `src/coding_guardrails/proxy/client.py` | `SafeLlamafileClient` — thinking token capture, max_tokens forwarding |
36
- | `src/coding_guardrails/proxy/server.py` | Asyncio HTTP server, OpenAI-compatible `/v1/chat/completions` |
37
- | `src/coding_guardrails/middleware.py` | Composes all guardrail rules, provides `check()` / `record()` API |
38
- | `src/coding_guardrails/cli.py` | `coding-guardrails serve` CLI with `--backend-url`, `--model`, `--port`, `--log-file` |
39
- | `src/coding_guardrails/rules/` | 11 guardrail rules (see below) |
40
-
41
- ### Guardrail Rules (Layer 2)
42
-
43
- | Rule | Action | What it does |
44
- |------|--------|-------------|
45
- | `prerequisites` | block | Enforce read-before-edit |
46
- | `path_safety` | block | Path traversal blocking |
47
- | `command_safety` | block | Destructive command blocking |
48
- | `network` | block | Network access control |
49
- | `sensitive_files` | block | Protect sensitive files |
50
- | `secrets` | block/mask | Secret detection |
51
- | `loop_detection` | nudge/block | Stagnation detection (≤2 unique tools over 14+ calls) |
52
- | `session_budget` | nudge | File/command/read budget limits |
53
- | `thoroughness` | block | Premature terminal submission (low tool exploration ratio) |
54
- | `sequencing` | nudge | Test-after-change suggestions |
55
- | `tool_resolution` | nudge | Empty/error result handling |
56
-
57
- ## Running the Proxy
58
-
59
- ### tmux Sessions
60
-
61
- Four tmux sessions are used:
62
-
63
- | Session | Purpose | Port |
64
- |---------|---------|------|
65
- | `llm-server` | `llama-server` with Qwen3.5-9B-UD-Q4_K_XL | :8080 |
66
- | `guardrails` | `coding-guardrails serve` proxy | :8081 |
67
- | `skills-master` | General workspace | — |
68
- | `pi` | Pi coding agent | — |
69
-
70
- ```bash
71
- # Start llama-server (in tmux: llm-server)
72
- llama-server -m /path/to/Qwen3.5-9B-UD-Q4_K_XL.gguf --port 8080 --host 0.0.0.0
73
-
74
- # Start proxy (in tmux: guardrails)
75
- source .venv/bin/activate
76
- coding-guardrails serve --backend-url http://localhost:8080 --model Qwen3.5-9B-UD-Q4_K_XL --port 8081 -v
77
- ```
78
-
79
- ### Important Client Quirks
80
-
81
- - `SafeLlamafileClient` needs `gguf_path` for model identity (stem = model name), not `model` — file doesn't need to exist: use `/tmp/Qwen3.5-9B-UD-Q4_K_XL.gguf`
82
- - `recommended_sampling=False` required — Qwen3.5-9B-UD-Q4_K_XL not in Forge's MODEL_SAMPLING_DEFAULTS registry
83
-
84
- ## Testing
85
-
86
- ```bash
87
- source .venv/bin/activate
88
- pytest tests/unit/ -q # All 233 unit tests
89
- pytest tests/unit/ -q -k "loop" # Specific rule tests
90
- ```
91
-
92
- All 233 tests must pass before committing.
93
-
94
- ## Eval
95
-
96
- ### Forge 30-Scenario Eval
97
-
98
- ```bash
99
- source .venv/bin/activate
100
-
101
- # Proxy mode (5 runs × 30 scenarios = 150 calls)
102
- python eval/scripts/run_forge_eval.py --mode proxy --runs 5
103
-
104
- # Direct mode (bypass proxy, LLM only)
105
- python eval/scripts/run_forge_eval.py --mode direct --runs 5
106
-
107
- # Both modes (direct vs proxy comparison)
108
- python eval/scripts/run_forge_eval.py --mode both --runs 5
109
-
110
- # Specific scenarios
111
- python eval/scripts/run_forge_eval.py --mode proxy --scenario data_gap_recovery_extended argument_transformation
112
-
113
- # Verbose (shows tool calls, thinking, results)
114
- python eval/scripts/run_forge_eval.py --mode proxy --runs 1 -v
115
- ```
116
-
117
- ### Run Folder Structure
118
-
119
- Each run creates `eval/runs/<timestamp>/`:
120
-
121
- ```
122
- eval/runs/2026-05-31_204247Z/
123
- ├── run.json # Run metadata (mode, model, timestamps)
124
- ├── eval.log # Forge's verbose trace (tee'd from stdout)
125
- ├── proxy.log # Proxy's logging module output
126
- ├── proxy.jsonl # Per-scenario JSON results
127
- ├── proxy_summary.txt # Human-readable summary table
128
- ├── comparison.md # Direct vs proxy comparison (both mode)
129
- └── direct.jsonl # Direct mode results (both mode)
130
- ```
131
-
132
- ### Layer 2 Guardrails Eval
133
-
134
- ```bash
135
- python eval/scripts/run_layer2_eval.py # 17 JSON scenarios
136
- ```
137
-
138
- ### Compare Direct vs Proxy
139
-
140
- ```bash
141
- python eval/scripts/compare.py eval/runs/<timestamp>/direct.jsonl eval/runs/<timestamp>/proxy.jsonl
142
- ```
143
-
144
- ### Benchmark Results
145
-
146
- Latest run: `eval/runs/2026-05-31_204247Z/` — **93% accuracy** (140/150), 100% completion.
147
- Forge's published benchmark: ~84% accuracy. Our proxy: +9pp with zero regressions.
148
-
149
- ## Development Guidelines
150
-
151
- - **Do NOT hack Forge source** — extend via public API, subclassing, or wrapping only
152
- - Changes must survive upstream Forge updates (vendored at `.vendors/forge/`)
153
- - All 233 unit tests must pass
154
- - No hardcoded scenario-specific logic — guardrails must be general purpose
155
- - Use `edit` for targeted changes, `write` for new files or complete rewrites
156
- - Keep the proxy clean: no injection/annotation of messages for non-coding-agent requests
157
-
158
- ## Versioning & Releases
159
-
160
- Uses [SemVer](https://semver.org/): `MAJOR.MINOR.PATCH`
161
-
162
- - **PATCH** (0.7.1): Bug fixes, no new features or API changes
163
- - **MINOR** (0.8.0): New features, new rules, backward-compatible
164
- - **MAJOR** (1.0.0): Breaking API changes
165
-
166
- ### Release Process
167
-
168
- 1. Update `version` in `pyproject.toml`
169
- 2. Update README if needed (eval results, new rules, new features)
170
- 3. Update CLAUDE.md if architecture changed
171
- 4. Ensure all 233 tests pass: `pytest tests/unit/ -q`
172
- 5. Commit: `git commit -m "vX.Y.Z: description"`
173
- 6. Push: `git push`
174
- 7. Tag: `git tag vX.Y.Z && git push origin vX.Y.Z`
175
- 8. GitHub Actions auto-builds, tests, and publishes to PyPI
176
-
177
- ### CI Pipeline
178
-
179
- - On every push to `main`: runs tests + lint
180
- - On tag `v*`: builds wheel, verifies CLI (`coding-guardrails --version`), publishes to PyPI
181
- - PyPI uses trusted publishing (OIDC), no API tokens needed
182
-
183
- ### Refresh Local Install
184
-
185
- ```bash
186
- source .venv/bin/activate
187
- uv pip install -e ".[dev]"
188
- coding-guardrails --version # verify
189
- ```