Skip to content

chore: release v0.1.2 - #3

Merged
cheneeheng merged 9 commits into
mainfrom
chore/prep-v0-1-2
Aug 7, 2026
Merged

chore: release v0.1.2#3
cheneeheng merged 9 commits into
mainfrom
chore/prep-v0-1-2

Conversation

@cheneeheng

Copy link
Copy Markdown
Owner

What

Release v0.1.2. Adds per-tier reasoning effort, moves the specialist and investigator tiers to
claude-sonnet-5, and fixes six defects — two of which disabled the investigator entirely.

Why

The pipeline had not been run end to end against the real API since v0.1.1. Doing so surfaced
failures that no test could catch, because each came from a library or platform default rather
than from this project's logic:

  • uvicorn --reload — the command this repo documented everywhere — selects a SelectorEventLoop
    on Windows, which cannot spawn a subprocess. The Agent SDK drives the Claude Code CLI as one, so
    every investigation failed with Failed to start Claude Code: and an empty message. Nothing
    else in the pipeline spawns a process, so the failure was invisible until the sub-agent ran.
  • pydantic-settings reads ANTHROPIC_API_KEY from .env and never writes os.environ. The
    spawned CLI inherits os.environ, so the investigator alone ran unauthenticated.
  • Setting a reasoning effort turns on adaptive thinking, and the API refuses a forced tool call
    while thinking is on — so with_structured_output's default function_calling silently lost
    its guarantee and specialists died on a ValidationError.
  • LangGraph's with_allowlist merges into the existing allowlist, and the default is the
    sentinel True, so merging is a no-op. An earlier fix on this branch appeared to work only
    because it was verified under LANGGRAPH_STRICT_MSGPACK=true.

How

  • Effort: GATE_MODEL_<TIER>_EFFORT beside each GATE_MODEL_<TIER>. effort_for() withholds
    it from any model id containing haiku, keyed on the configured id rather than the tier, and
    the investigator's value reaches both its SDK tool loop and its summarizing turn.
  • Structured output: json_schema on effort-carrying tiers, function_calling elsewhere.
  • Checkpoints: use_state_allowlist (new graph/checkpoint.py) constructs the serializer
    with CHECKPOINT_ALLOWLIST instead of merging into a permissive default.
  • Investigator: preflights the event loop and names the cause; receives credentials via
    ClaudeAgentOptions.env. --reload removed from every documented command.
  • API: a review reads as running until its first checkpoint, instead of 404ing the UI out of
    the page on the first review after a start.
  • Docs: guide renumbered to HT-/OP- prefixes with index.md as the entry point, a
    glossary, and four fixes from an audit against the source.

Testing

  • uv run pytest --cov — 113 passed, 100% statement and branch coverage of src/gate.
  • uv run mypy src — clean under strict. uv run ruff check . — clean.
  • The checkpoint allowlist was verified against a real checkpoints.db: 13 deserialization
    warnings before, none after, with HumanDecision and InvestigationReport arriving as models.
  • The event-loop failure was reproduced directly: ProactorEventLoop spawns, SelectorEventLoop
    raises NotImplementedError with an empty message.

Not verified against the live API: the default suite makes no API calls, so the json_schema
structured-output path and the effort/temperature combination were checked only where the request
is built. uv run python examples/demo.py 003_auth_change with a key is the end-to-end check.

Checklist

  • One version everywhere — pyproject.toml is the only manifest, at 0.1.2
  • CHANGELOG [0.1.2] section written
  • README and CLAUDE.md updated
  • CI gates green locally

Generated with Claude Code by CEH

An audit of docs/guide against the user-operator-guide standard found the
structure sound but turned up four content gaps and two structural deviations.

Content:
- POLICY_COLLECTION (config.py, default "policies") was documented nowhere.
  Added to the configuration reference and .env.example, noting that it
  carries no GATE_ prefix and that changing it needs a re-ingest.
- The "invalid sample PR id" FixtureError added with the path-traversal fix
  landed after the guide was written. The runbook now tables all five 422
  messages, and corrects two: the id pattern allows hyphens, and an invalid
  meta.json is a JSON parse failure only (a missing pr_id or title falls back
  to the directory name). Broadened that section's heading accordingly.
- The "Review not found." and "Connection issue: …" banners were missing from
  the reviewer troubleshooting table, along with the invalid-id banner.
- README had no link to the guide at all.

Structure:
- 00-index.md -> index.md, the entry point the standard expects.
- Added a glossary; the domain jargon (risk gate, specialist, reconcile,
  escalation, checkpoint) was assumed rather than defined.
- Renamed the how-to and operations pages to HT-01..04 and OP-01..05 so each
  folder numbers from 01, and dropped the numeric prefixes from the root
  pages. Headings, cross-links, and the index follow.

Everything else in the guide was checked against source and is accurate: all
GATE_* defaults, the seven enums, the seven endpoints and their status codes,
the six-rule reconcile ladder, the log event table, the UI labels, and the
demo script's beats.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
…et 5

Adds GATE_MODEL_EFFORT (low|medium|high|xhigh|max, default high) as a
closed-set Literal so an invalid value fails at startup rather than at call
time, mid-review.

Effort is withheld from any model id containing "haiku": Haiku has no effort
control and the API rejects the parameter. The test is on the configured id,
not on which factory was called, so a Haiku model under any GATE_MODEL_*
setting is covered and pointing a tier at Sonnet opts it in.

One consequence worth calling out: langchain-anthropic turns on adaptive
thinking when reasoning_effort is set on a model declaring xhigh support, and
passes temperature through unchanged. Since a non-default temperature is not
accepted alongside thinking, _chat now sends no temperature when effort is
applied and keeps temperature=0 on the tiers that take none. Specialist
wording is therefore no longer reproducible run to run; routing is unaffected,
as reconcile is plain code. See DECISION_LOG entry 14.

Default specialist and investigator model moves to claude-sonnet-5. Its
PRICE_TABLE rate is carried over from claude-sonnet-4-6 and is unverified —
it equals the existing fallback, so the displayed estimate is unchanged.

The Agent SDK's ClaudeAgentOptions also accepts an effort field; the
investigator's tool loop is left untouched, as the request covered the chat
model.

Not verified against the live Anthropic API: the default suite makes no API
calls, so the effort payload was checked only where the request is built.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
…SDK loop

Replaces the single GATE_MODEL_EFFORT with one setting per tier, so each
GATE_MODEL_<TIER> now has a GATE_MODEL_<TIER>_EFFORT beside it:

  GATE_MODEL_CLASSIFIER      / _EFFORT
  GATE_MODEL_SPECIALIST      / _EFFORT
  GATE_MODEL_INVESTIGATOR    / _EFFORT

The Haiku rule is unchanged and still keyed on the configured id, not the
tier: effort_for() returns None for any id containing "haiku", so a tier
pointed at Haiku is covered wherever it appears and one pointed at Sonnet opts
in. This is why the classifier's effort is inert at its Haiku default.

The investigator's effort now also reaches the Agent SDK tool loop as
ClaudeAgentOptions.effort, not just the final summarizing turn. effort_for()
became public so both call sites resolve it the same way; ClaudeAgentOptions
exposes no temperature, so the adaptive-thinking caveat from entry 14 applies
only to the chat-model path.

DECISION_LOG entry 14 amended: its per-tier and SDK-scope decisions are
superseded here by explicit instruction. The temperature handling stands and
remains unverified against the live API.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
Setting an effort broke the specialists:

  ValidationError: 1 validation error for _FindingList
  findings
    Input should be a valid list [type=list_type,
    input_value='{"findings": []}', input_type=str]

An effort turns on adaptive thinking, and the Anthropic API rejects a
tool_choice that forces tool use while thinking is on. langchain-anthropic
therefore drops the forcing (chat_models.py:2025 and
_get_llm_for_structured_output_when_thinking_is_enabled), and
with_structured_output's default function_calling method silently loses its
guarantee -- the model answered in prose, and the parser fed the raw JSON text
into the schema's only field.

structured_call now picks the method from the effort: json_schema, Claude's
dedicated structured output, which thinking does not restrict, when an effort
is set; function_calling otherwise. Tiers without an effort -- Haiku, where
effort_for() returns None -- are unchanged.

Not verified against the live API: the default suite makes no API calls, so
this was checked only where the method is selected. See DECISION_LOG entry 15
for the two rejected alternatives.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
Every checkpoint read logged a deprecation warning per domain type:

  Deserializing unregistered type gate.graph.state.Finding from checkpoint.
  This will be blocked in a future version.

LangGraph's msgpack deserializer reconstructs only allowlisted types. Ours are
permitted-with-a-warning under today's default and the library states they
will be refused outright later -- at which point checkpointed state comes back
as bare dicts instead of models.

Both checkpointers now declare them through the library's own API,
saver.with_allowlist(CHECKPOINT_ALLOWLIST), in api/app.py and examples/demo.py.
CHECKPOINT_ALLOWLIST sits at the bottom of graph/state.py and names each model
and StrEnum explicitly rather than deriving them from vars(): an allowlist is a
security boundary and is worth reading at a glance. A new test fails if the two
drift apart, so a type added to state.py is caught at test time rather than at
runtime.

compile_graph now takes BaseCheckpointSaver[Any]; with_allowlist returns a
shallow clone typed as the base class, and the graph never needed the concrete
SQLite saver.

Verified with LANGGRAPH_STRICT_MSGPACK=true, which enables the future behavior
now: an allowlisted saver round-trips a populated ReviewState with its nested
findings and enums intact, while an unwrapped one returns a plain dict.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
…rst-review race

Three defects, one of them mine from 900a920.

1. The allowlist never took effect. BaseCheckpointSaver.with_allowlist MERGES
   into the existing allowlist, and the default is the sentinel True, so
   JsonPlusSerializer.with_msgpack_allowlist returns self unchanged and the
   warnings continued. It only appeared to work because the verification ran
   under LANGGRAPH_STRICT_MSGPACK=true, where the base is None and the merge
   does happen. The allowlist has to be given to the serializer at
   construction, which is what the new graph/checkpoint.py does. Confirmed
   against a real checkpoints.db: thirteen warnings before, none after, with
   HumanDecision and InvestigationReport arriving as models.

   The new test asserts both halves -- that a round trip warns nothing, and
   that an undeclared type is refused. Only the second would have caught the
   no-op.

2. The investigator ran unauthenticated. The SDK spawns the Claude Code CLI,
   which inherits os.environ; pydantic-settings reads ANTHROPIC_API_KEY from
   .env and never writes it there. Every other component takes the key as an
   argument, so this failed in exactly one place. Now passed through
   ClaudeAgentOptions.env.

3. The first review after a server start died with "Review not found.". A
   review is registered before its background run writes a checkpoint, and the
   UI polls as soon as POST returns; get_review 404'd, and the page stops
   polling on 404. Later reviews raced better because the first one pays for
   the lazy imports. A thread the app knows about now reads as running until
   its checkpoint exists.

CLAUDE.md records the with_allowlist trap and the os.environ gap, both of
which are invisible until something downstream breaks.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
…he sub-agent

The reported failure was:

  event='investigation_error' error='Failed to start Claude Code: '

with nothing after the colon, 17ms after the human decision. That empty string
is the whole diagnosis: Windows' SelectorEventLoop raises a bare
NotImplementedError for subprocess creation, and the SDK interpolates its empty
message into its own wrapper. Reproduced directly:

  ProactorEventLoop    spawn OK
  SelectorEventLoop    NotImplementedError str=''

uvicorn picks that loop whenever use_subprocess is set -- which --reload does --
even on Windows, where it is precisely the loop that cannot spawn subprocesses
(uvicorn/loops/asyncio.py). So `uvicorn ... --reload`, the command this repo
documented everywhere, disabled the investigator and nothing else: classify,
risk, and the specialists never spawn anything, so the pipeline looked healthy
right up to the point the sub-agent ran.

--reload is now gone from the README, CLAUDE.md, and the install, configure, and
routine-operations pages, each with a note on why. run_investigation preflights
the loop and returns an inconclusive report naming the cause, so the same
mistake reads as a sentence rather than an empty error. The preflight sits with
the existing no-snapshot check: still no raising into the graph.

This supersedes the ClaudeAgentOptions.env fix in b1ab652 as the explanation for
the reported failure. That change was still correct on its own terms -- the key
genuinely never reached the subprocess -- but it was not what broke this.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
Ships per-tier reasoning effort (GATE_MODEL_<TIER>_EFFORT), moves the
specialist and investigator tiers to claude-sonnet-5, and fixes the defects
that surfaced from running the pipeline for real — two of which disabled the
investigator outright: uvicorn --reload selects an event loop that cannot spawn
the Claude Code CLI, and the CLI never received the API key because
pydantic-settings reads .env without touching os.environ.

- Bump: PATCH — the release is dominated by fixes; the new effort settings are
  additive and default to the model's existing behaviour, so nothing that
  worked before behaves differently unless a tier is retuned.
- Manifests: pyproject.toml 0.1.1 -> 0.1.2 (the only manifest this project
  ships).
- Docs: CHANGELOG [0.1.2] section written; README updated (Documentation
  section, GATE_MODEL_*_EFFORT, the --reload warning); CLAUDE.md updated (the
  with_allowlist trap, the os.environ credential gap, the --reload rule, and
  the start command); the user/operator guide renumbered to HT-/OP- prefixes
  with a glossary and four audit fixes.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
@cheneeheng

Copy link
Copy Markdown
Owner Author

Reopening to re-fire CI (no workflow run was created for this PR).

@cheneeheng cheneeheng closed this Aug 6, 2026
@cheneeheng cheneeheng reopened this Aug 6, 2026
CI failed the 100% coverage gate on both matrix jobs at 99.80%, with a single
missed line:

  src/gate/agents/investigator.py  156  1  50  1  99%  254

Line 254 is the isinstance(loop, SelectorEventLoop) check inside
_loop_can_spawn_subprocess, which sits behind an early return for
sys.platform != "win32". On Windows the harness tests reach it incidentally; on
the Linux runner nothing can. Coverage was host-dependent, so the suite read
100% locally and 99.80% in CI -- the gap only surfaced once Actions started
running again.

The sibling test already pins the POSIX direction by faking the platform, so
this makes the pair symmetric: monkeypatch sys.platform to "win32" and stub
asyncio.get_running_loop with a Mock(spec=asyncio.SelectorEventLoop), and the
win32 branch executes regardless of the host. Rejected `# pragma: no cover` and
a coverage exclusion -- both hide a real branch of a documented platform
workaround rather than testing it.

Test-only change; src/ is untouched. 114 passed at 100% statement and branch
coverage, ruff check and format clean.

Generated with [Claude Code](https://claude.com/claude-code) by CEH
@cheneeheng
cheneeheng merged commit d4c9bd2 into main Aug 7, 2026
2 checks passed
@cheneeheng
cheneeheng deleted the chore/prep-v0-1-2 branch August 7, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant