|
| 1 | +# BETA_READINESS_REPORT.md — claude-code-247 v1.0.0-beta.0 |
| 2 | + |
| 3 | +> Synthesis of the four-phase beta-readiness milestone (M18-P0..P4) |
| 4 | +> following the v1.0.0-alpha.1 release. Goal of M18: stop adding new |
| 5 | +> features; move the product from "alpha harness" to "beta-ready |
| 6 | +> live-ops" by hardening auth, validators, daemon, webhooks, and |
| 7 | +> running a clean second end-to-end on the sacrificial test repo. |
| 8 | +
|
| 9 | +## TL;DR |
| 10 | + |
| 11 | +- **All four directive phases (P0/P1/P2/P3) are committed, pushed, |
| 12 | + and tested.** |
| 13 | +- **Second real E2E (P4) ran clean** on `CTlanston/auto-evo-playground` |
| 14 | + using the local-first auth path — see |
| 15 | + [REAL_E2E_REPORT_M18_P4.md](REAL_E2E_REPORT_M18_P4.md). |
| 16 | +- **Anthropic API spend dropped from ~$1.50 (M17/α1) → $0.00 (M18-P4)** |
| 17 | + for the same shape of task. |
| 18 | +- **No fake green** — Gemini correctly returned `NEEDS_HUMAN` because |
| 19 | + the evidence package omits the textual diff body. System routed to |
| 20 | + `waiting_for_approval` as designed. Two real findings filed for |
| 21 | + follow-up. |
| 22 | +- **Recommendation: tag v1.0.0-beta.0** on `claude247/v1`. |
| 23 | + |
| 24 | +## Phase-by-phase |
| 25 | + |
| 26 | +| Phase | Title | Commit | Status | |
| 27 | +|---|---|---|---| |
| 28 | +| M18-P0 | Subscription/local auth — `worker_mode` + no silent API fallback | `334ed46` | ✓ pushed | |
| 29 | +| M18-P1 | Real OpenAI validator + mock-cannot-silently-auto-merge | `9dacd5d` | ✓ pushed | |
| 30 | +| M18-P2 | launchd hardening — `doctor_launchd.sh` + extended `doctor` fields + plist tests | `712a639` | ✓ pushed | |
| 31 | +| M18-P3 | Real GitHub webhook live test through ngrok + explicit `handle_ping` | `5170197` | ✓ pushed | |
| 32 | +| M18-P4 | Second real E2E proving reduced API spend + cleaner auto-merge path | (this report, no code change) | ✓ validated live | |
| 33 | + |
| 34 | +### P0 — Subscription/local auth (no silent API fallback) |
| 35 | + |
| 36 | +**The problem (from M17 live discovery):** `auth_mode` label was |
| 37 | +hardcoded `"local_claude_code"` even when the worker actually used |
| 38 | +`ANTHROPIC_API_KEY` from env (silent paid fallback). That made |
| 39 | +"reduced API spend" claims unverifiable. |
| 40 | + |
| 41 | +**What was done:** |
| 42 | +- `runner/auth.py` rewritten: `resolve_worker_mode()`, |
| 43 | + `effective_env(worker_mode=...)`, `ensure_usable(mode)`, |
| 44 | + `allow_api_fallback()`. |
| 45 | +- `local_claude_code` mode now **strips** `ANTHROPIC_API_KEY` and |
| 46 | + `ANTHROPIC_BASE_URL` from the worker subprocess env. |
| 47 | +- `anthropic_api` mode keeps them but only if the key is present; |
| 48 | + `ensure_usable` returns a clear error otherwise. |
| 49 | +- `runner/claude_cli.py::invoke` honestly labels the mode based on |
| 50 | + the env actually given to the subprocess, not what the caller hoped. |
| 51 | +- `config/default.yaml` adds: |
| 52 | + - `auth.worker_mode: local_claude_code` (default) |
| 53 | + - `auth.allow_api_fallback: false` |
| 54 | + - `auth.require_explicit_api_fallback: true` |
| 55 | + - `auth.show_cost_warnings: true` |
| 56 | +- `gateway/doctor.py::check_auth_mode` surfaces the resolved mode + |
| 57 | + whether `ANTHROPIC_API_KEY` would have been picked up if not for |
| 58 | + the strip. |
| 59 | +- Tests: [tests/unit/test_auth_mode_no_silent_fallback.py](tests/unit/test_auth_mode_no_silent_fallback.py) |
| 60 | + — 12 tests covering default/explicit modes, env strip, key |
| 61 | + presence, and legacy `anthropic_api_fallback` still requiring |
| 62 | + approval. |
| 63 | + |
| 64 | +**P4 evidence:** worker subprocess invoked `claude --print` with no |
| 65 | +Anthropic key in env; PR landed without burning API credit. |
| 66 | + |
| 67 | +### P1 — Real OpenAI validator + mock-cannot-pass-auto-merge |
| 68 | + |
| 69 | +**The problem:** previously, when the OpenAI key was absent the |
| 70 | +adapter silently returned `validator: "openai-mock"` with verdict |
| 71 | +PASS, which (under sufficient validator agreement) was enough to |
| 72 | +satisfy auto-merge. That broke the "no fake green" principle. |
| 73 | + |
| 74 | +**What was done:** |
| 75 | +- `validator/openai_judge.py`: real REST adapter via httpx, falls back |
| 76 | + to mock only when the key is absent — and the mock now labels itself |
| 77 | + `openai-mock` so the policy layer can see it. |
| 78 | +- `validator/validation_policy.py::validate`: |
| 79 | + - Refuses `PASS` for auto-merge if any result is labeled `*-mock` |
| 80 | + unless `validators.allow_mock_validators_for_auto_merge: true`. |
| 81 | + - Per-validator `validators.<kind>.require_real_for_auto_merge: true` |
| 82 | + forces approval routing even if the global flag is permissive. |
| 83 | +- `config/default.yaml`: |
| 84 | + - `validators.allow_mock_validators_for_auto_merge: false` (default) |
| 85 | + - `validators.gemini.require_real_for_auto_merge: true` |
| 86 | + - `validators.openai.require_real_for_auto_merge: true` |
| 87 | + |
| 88 | +**P4 evidence:** P4 reached `NEEDS_HUMAN`, not `PASS`, so the gate |
| 89 | +itself didn't *fire* — but the upstream behavior (real Gemini call |
| 90 | +with `auth_mode: gemini_api`; OpenAI ran as `openai-mock` because |
| 91 | +the key wasn't in the env-loader scope — Finding 2) confirmed the |
| 92 | +plumbing. Unit tests `tests/unit/test_validation_policy_mock_gate.py` |
| 93 | +and `tests/unit/test_openai_judge_real_or_mock.py` cover the gate |
| 94 | +behavior. |
| 95 | + |
| 96 | +### P2 — launchd hardening |
| 97 | + |
| 98 | +**The problem:** `scripts/install_launchd.sh` rendered plists, but |
| 99 | +nothing inspected the loaded state at runtime, and `doctor` only said |
| 100 | +"none of com.claude247.* services loaded" without saying which one |
| 101 | +crashed or where to look. |
| 102 | + |
| 103 | +**What was done:** |
| 104 | +- `scripts/doctor_launchd.sh` (new, ~70 lines): per-service report — |
| 105 | + loaded state from `launchctl list`, exit status, last 20 log lines, |
| 106 | + dashboard `/healthz` reachability. |
| 107 | +- `gateway/doctor.py::check_launchd`: surfaces each service's loaded |
| 108 | + state in `claude247 doctor` output. |
| 109 | +- All four plist templates already tested in |
| 110 | + [tests/unit/test_launchd_plist_generation.py](tests/unit/test_launchd_plist_generation.py) |
| 111 | + — 9 tests: existence, valid XML, dispatcher `StartInterval=30`, |
| 112 | + backup `StartCalendarInterval`, dashboard `KeepAlive=true`, |
| 113 | + install/uninstall script consistency, doctor_launchd.sh executable. |
| 114 | + |
| 115 | +**P4 evidence:** no launchd services were loaded during P4 (deliberate |
| 116 | +— the run was foreground via `claude247 dispatcher --once`); doctor |
| 117 | +correctly flagged `none of com.claude247.* services loaded`. |
| 118 | + |
| 119 | +### P3 — Live GitHub webhook through ngrok |
| 120 | + |
| 121 | +**The problem:** unit tests covered HMAC signature + handler dispatch, |
| 122 | +but the dashboard endpoint had never received a real GitHub |
| 123 | +delivery, so a tunnel/header/firewall regression could go unnoticed. |
| 124 | + |
| 125 | +**What was done:** |
| 126 | +- ngrok tunnel exposed `127.0.0.1:8423` → public URL. |
| 127 | +- Created webhook id `630064720` on `CTlanston/auto-evo-playground` |
| 128 | + subscribed to `pull_request`, `check_run`, `check_suite`, `ping`. |
| 129 | +- Real deliveries: 2 × ping (auto + forced via `/pings` API), 1 × |
| 130 | + `pull_request opened` (PR #52), 7 × `check_run` (4 created + 3 |
| 131 | + completed). All 200 OK, all logged to DB `logs` table, all source |
| 132 | + IPs in GitHub's published webhook ranges (140.82.115.x). |
| 133 | +- Code change: added explicit `handle_ping` in `orchestrator/webhooks.py` |
| 134 | + + `HANDLERS["ping"] = handle_ping` so ping events show up as |
| 135 | + `handled` with `zen` + `hook_id` surfaced, not as `ignored`. |
| 136 | +- Tests: `tests/unit/test_webhooks.py::test_ping_event_is_handled_explicitly` |
| 137 | + + `tests/integration/test_webhooks_route.py::test_webhook_ping_event_handled`. |
| 138 | +- Full report: [WEBHOOK_LIVE_REPORT.md](WEBHOOK_LIVE_REPORT.md). |
| 139 | + |
| 140 | +**Cleanup performed:** webhook 630064720 deleted; PR #52 closed + |
| 141 | +branch deleted; ngrok and dashboard processes killed; webhook_secret |
| 142 | +stripped from `~/.claude-code-247/config.yaml`; tmp files removed. |
| 143 | + |
| 144 | +### P4 — Second real E2E proving reduced API spend + cleaner auto-merge |
| 145 | + |
| 146 | +**The problem:** M17 (α1) PR #51 landed on auto-evo-playground but |
| 147 | +burned ~$1.50 of Anthropic API credit because of the silent fallback |
| 148 | +bug. The whole point of P0/P1 was to make this no longer happen. |
| 149 | + |
| 150 | +**What was done:** queued the `normalize_whitespace` task on |
| 151 | +auto-evo-playground; ran `claude247 dispatcher --once`; observed the |
| 152 | +full pipeline. See [REAL_E2E_REPORT_M18_P4.md](REAL_E2E_REPORT_M18_P4.md). |
| 153 | + |
| 154 | +**Key results:** |
| 155 | +- Worker used local `claude` CLI (subscription). $0.00 Anthropic spend. |
| 156 | +- Real Gemini judge ran (`auth_mode: gemini_api`). |
| 157 | +- Worker output was *correct*: `normalize_whitespace(text)` → |
| 158 | + `" ".join(text.split())`, plus 6 unit tests covering the spec |
| 159 | + cases. 85 pytest assertions passed (was 79 + 6 new). |
| 160 | +- PR opened as draft (#53), commit `e7537d3`, branch pushed. |
| 161 | +- Gemini returned `NEEDS_HUMAN` — *honestly* — because the evidence |
| 162 | + package only contains a diff *summary*, not the diff *body* |
| 163 | + (Finding 1). |
| 164 | +- Merge policy routed to `WAITING_APPROVAL`. Auto-merge gate held. |
| 165 | +- PR #53 cleanup: closed + branch deleted after report. |
| 166 | + |
| 167 | +## Findings — beta-readiness backlog (NOT blockers for tagging) |
| 168 | + |
| 169 | +These were surfaced by the P4 live run. They are real and worth |
| 170 | +filing, but they are not regressions and do not break any documented |
| 171 | +contract. |
| 172 | + |
| 173 | +| ID | Finding | Severity | Source | |
| 174 | +|---|---|---|---| |
| 175 | +| BR-001 | `JudgeInput` includes `diff_summary.md` (stat) but not the textual diff body. Real validators correctly refuse to verify byte-identical preservation without seeing the body. | medium — caps real-validator PASS rate | P4 §Finding 1 | |
| 176 | +| BR-002 | `env_loader.load()` only reads `~/.claude-code-247/.env`; the project-local `.env` is ignored. So `OPENAI_API_KEY` in CWD/.env runs as mock even though the key exists. | low — config UX, no security risk | P4 §Finding 2 | |
| 177 | +| BR-003 | Dispatcher summary reports `worker_exit: 3` even when role artifacts are complete and downstream pipeline produced a clean PR. Misleading. | low — observability, not a runtime bug | P4 §Finding 3 | |
| 178 | + |
| 179 | +## What's intentionally not in scope for beta |
| 180 | + |
| 181 | +- Multi-machine HA. Single-Mac is by design. |
| 182 | +- Cross-org auth (the project is local-first; one user, one machine). |
| 183 | +- Docker runner outside dev mode. The local backend covers the |
| 184 | + product's stated 24/7 single-Mac scope. |
| 185 | +- A dashboard with users/auth — dashboard is bound to `127.0.0.1` |
| 186 | + and that's deliberate. |
| 187 | + |
| 188 | +## Test posture |
| 189 | + |
| 190 | +``` |
| 191 | +$ .venv/bin/python -m pytest -q --no-cov |
| 192 | +``` |
| 193 | + |
| 194 | +All M18 work landed with tests-first; the new files include: |
| 195 | + |
| 196 | +- `tests/unit/test_auth_mode_no_silent_fallback.py` (12 tests) |
| 197 | +- `tests/unit/test_validation_policy_mock_gate.py` |
| 198 | +- `tests/unit/test_openai_judge_real_or_mock.py` |
| 199 | +- `tests/unit/test_launchd_plist_generation.py` (9 tests) |
| 200 | +- `tests/unit/test_webhooks.py::test_ping_event_is_handled_explicitly` |
| 201 | +- `tests/integration/test_webhooks_route.py::test_webhook_ping_event_handled` |
| 202 | + |
| 203 | +## Recommendation |
| 204 | + |
| 205 | +Tag **v1.0.0-beta.0** on `claude247/v1` at the current HEAD. The |
| 206 | +three findings above should be filed as follow-up tickets but do not |
| 207 | +hold the tag. |
| 208 | + |
| 209 | +> "Beta-ready" here means: the documented product works |
| 210 | +> end-to-end on a real GitHub repository, the auth path is honest |
| 211 | +> about what it spends, validators are honest about what they ran |
| 212 | +> with, the daemon path is inspectable, and live webhook delivery has |
| 213 | +> been observed. It does **not** mean every backlog item is closed. |
0 commit comments