Repository navigation
feat(gateway): F2 breadcrumb backlog cleanup (config family, contract gate, watcher guardrail) - #86
Conversation
…t gate, no-regression guard Closes the three residuals #80 logged (spec 2026-06-22_f2-breadcrumb-backlog-cleanup): - Item 3 (config consistency): surface the whole restart_* family (restart_loop_threshold/window_secs/initiated_ttl_secs) in the config defaults dict with doc comments, and add the missing restart_initiated_ttl_secs config→env bridge. Extracted the inline agent-config bridge into _bridge_agent_config_to_env + _AGENT_CONFIG_ENV_BRIDGE (single-sourced, unit-testable) — config UNCONDITIONALLY wins over a pre-set env var, matching the shipped siblings (PR NousResearch#18413). - Item 2 (contract drift): a # F2 BREADCRUMB CONTRACT block as the single documented source of truth + an always-on frozen-literal conformance test over a key vector (the skill has a mirror test in hermes-home so drift in EITHER repo reddens its own CI — no cross-repo skip-no-op). - I-1 no-regression: source-guard test asserting the shipped #80 detection bodies are unchanged; this PR adds config/tests/docs only — zero behavior change to the breadcrumb path. Tests: 53 cascade (9 new) + 246 blast-radius green; bridge, config-wins, conformance (dirname + hash skew), and source-guard all RED-proven. Skill side (watcher guardrail + mirror conformance) lands in hermes-home. Spec reviewed: 2x Opus APPROVE-WITH-CHANGES (precedence corrected from ground-truth, conformance gate de-no-op'd).
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
9 |
unresolved-attribute |
2 |
not-subscriptable |
2 |
First entries
tests/gateway/test_restart_cascade.py:1066: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["restart_loop_window_secs"]` on object of type `list[str]`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
tests/gateway/test_restart_cascade.py:1065: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["restart_loop_threshold"]` on object of type `list[str]`
run_agent.py:2940: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/gateway/test_restart_cascade.py:1067: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
tests/gateway/test_restart_cascade.py:1065: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["restart_loop_threshold"]` on object of type `list[Unknown]`
tests/gateway/test_restart_cascade.py:1066: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["restart_loop_window_secs"]` on object of type `str`
tests/gateway/test_restart_cascade.py:1066: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["restart_loop_window_secs"]` on object of type `list[Unknown]`
tests/gateway/test_restart_cascade.py:1067: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["restart_initiated_ttl_secs"]` on object of type `str`
tests/gateway/test_restart_cascade.py:1067: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["restart_initiated_ttl_secs"]` on object of type `list[str]`
tests/gateway/test_restart_cascade.py:1065: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["restart_loop_threshold"]` on object of type `str`
tests/gateway/test_restart_cascade.py:1067: [not-subscriptable] not-subscriptable: Cannot subscript object of type `int` with no `__getitem__` method
tests/gateway/test_restart_cascade.py:1067: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["restart_initiated_ttl_secs"]` on object of type `list[Unknown]`
✅ Fixed issues (1):
| Rule | Count |
|---|---|
invalid-assignment |
1 |
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
Unchanged: 5922 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
|
| Filename | Overview |
|---|---|
| gateway/run.py | Extracts inline agent-config→env bridge into _AGENT_CONFIG_ENV_BRIDGE dict + _bridge_agent_config_to_env function; behavior-neutral for all pre-existing knobs. |
| hermes_cli/config.py | Adds restart_loop_threshold, restart_loop_window_secs, restart_initiated_ttl_secs to DEFAULT_CONFIG with doc comments; values match live helper fallbacks. |
| tests/gateway/test_restart_cascade.py | Adds 9 new tests covering bridge map correctness, TTL clamping, precedence, absent-key passthrough, non-dict no-op, frozen-contract conformance, and a source-guard for the shipped #80 detection path. |
Reviews (2): Last reviewed commit: "test: address Greptile review — drop dup..." | Re-trigger Greptile
…t_cd88e043) Rows from t_f4ef102c's CSV (row index #i). Each fix has a test that fails on base fbfcf03/a290767e19 and passes here. - #47 kanban_survivor: a submodule gitfile (.git/modules/...) whose target is missing was refused as a "dead linked worktree stub". Now only a target under .git/worktrees/ counts. - #64 whatsapp adapter: the npm-install remedy interpolated bridge_dir and the npm path unquoted. Both are now shlex.quote'd. - #86 /kanban dashboard: dashboard_link (reads config.yaml) ran on the event loop. Now asyncio.to_thread. - #12/#63 e2e _owns_transcript_driver: an exception in the interrupt-phase turn printed {"interrupted": false} with rc 0. It is now captured and returned as rc 4 with the error. The marker path in RUN_TOOL is shlex.quote'd. - #125/#135 sdlc-review SKILL.md and KANBAN_GUIDANCE: the no-delegation path now names batch_id "n/a: <reason>", which the coverage gate accepts (asserted). - #10 test_review_policy_none_skips_every_card patched configured_review_policy and then asserted the patch. It now drives the real resolver through _board_home_kanban_cfg. - #28 test_async_hint_is_bounded...: release.set() ran after asyncio.run, so the default-executor shutdown waited the full 5 s. Call time: 5.06 s -> 0.20 s. - #62 test_runtime_tree_is_checked_against_a_real_git_checkout: `git init` without -b collided when init.defaultBranch=upstream. Now `-b main`. - #82 no-network-on-loop gate: added requests.patch/delete and httpx.put/patch/delete to the sinks. This surfaced gateway/run.py stop and _stop_impl reaching requests.delete (camofox close at shutdown). They are baselined as pre-existing; the walker names one sink per coroutine, so they replace the old `-> requests.get` keys for the same two coroutines. - #87 no-agent-construction gate: the alias walker handled ast.Assign only. It now handles AnnAssign too (`store: X = self.session_store`). - Cluster A (16 rows here, 33 fleet-wide): tests/ci/test_no_new_source_proxy_asserts.py is a ratchet. It flags any test function that calls getsource and asserts with in/not in. The 116 functions in 84 files that match today are frozen in source_proxy_baseline.json. A new one fails, and so does a stale key. Opt out on the def line with `# noqa: source-proxy <reason>`.
…t_cd88e043) Rows from t_f4ef102c's CSV (row index #i). Each fix has a test that fails on base fbfcf03/a290767e19 and passes here. - #47 kanban_survivor: a submodule gitfile (.git/modules/...) whose target is missing was refused as a "dead linked worktree stub". Now only a target under .git/worktrees/ counts. - #64 whatsapp adapter: the npm-install remedy interpolated bridge_dir and the npm path unquoted. Both are now shlex.quote'd. - #86 /kanban dashboard: dashboard_link (reads config.yaml) ran on the event loop. Now asyncio.to_thread. - #12/#63 e2e _owns_transcript_driver: an exception in the interrupt-phase turn printed {"interrupted": false} with rc 0. It is now captured and returned as rc 4 with the error. The marker path in RUN_TOOL is shlex.quote'd. - #125/#135 sdlc-review SKILL.md and KANBAN_GUIDANCE: the no-delegation path now names batch_id "n/a: <reason>", which the coverage gate accepts (asserted). - #10 test_review_policy_none_skips_every_card patched configured_review_policy and then asserted the patch. It now drives the real resolver through _board_home_kanban_cfg. - #28 test_async_hint_is_bounded...: release.set() ran after asyncio.run, so the default-executor shutdown waited the full 5 s. Call time: 5.06 s -> 0.20 s. - #62 test_runtime_tree_is_checked_against_a_real_git_checkout: `git init` without -b collided when init.defaultBranch=upstream. Now `-b main`. - #82 no-network-on-loop gate: added requests.patch/delete and httpx.put/patch/delete to the sinks. This surfaced gateway/run.py stop and _stop_impl reaching requests.delete (camofox close at shutdown). They are baselined as pre-existing; the walker names one sink per coroutine, so they replace the old `-> requests.get` keys for the same two coroutines. - #87 no-agent-construction gate: the alias walker handled ast.Assign only. It now handles AnnAssign too (`store: X = self.session_store`). - Cluster A (16 rows here, 33 fleet-wide): tests/ci/test_no_new_source_proxy_asserts.py is a ratchet. It flags any test function that calls getsource and asserts with in/not in. The 116 functions in 84 files that match today are frozen in source_proxy_baseline.json. A new one fails, and so does a stale key. Opt out on the def line with `# noqa: source-proxy <reason>`.
…t_cd88e043) (#1414) Rows from t_f4ef102c's CSV (row index #i). Each fix has a test that fails on base fbfcf03/a290767e19 and passes here. - #47 kanban_survivor: a submodule gitfile (.git/modules/...) whose target is missing was refused as a "dead linked worktree stub". Now only a target under .git/worktrees/ counts. - #64 whatsapp adapter: the npm-install remedy interpolated bridge_dir and the npm path unquoted. Both are now shlex.quote'd. - #86 /kanban dashboard: dashboard_link (reads config.yaml) ran on the event loop. Now asyncio.to_thread. - #12/#63 e2e _owns_transcript_driver: an exception in the interrupt-phase turn printed {"interrupted": false} with rc 0. It is now captured and returned as rc 4 with the error. The marker path in RUN_TOOL is shlex.quote'd. - #125/#135 sdlc-review SKILL.md and KANBAN_GUIDANCE: the no-delegation path now names batch_id "n/a: <reason>", which the coverage gate accepts (asserted). - #10 test_review_policy_none_skips_every_card patched configured_review_policy and then asserted the patch. It now drives the real resolver through _board_home_kanban_cfg. - #28 test_async_hint_is_bounded...: release.set() ran after asyncio.run, so the default-executor shutdown waited the full 5 s. Call time: 5.06 s -> 0.20 s. - #62 test_runtime_tree_is_checked_against_a_real_git_checkout: `git init` without -b collided when init.defaultBranch=upstream. Now `-b main`. - #82 no-network-on-loop gate: added requests.patch/delete and httpx.put/patch/delete to the sinks. This surfaced gateway/run.py stop and _stop_impl reaching requests.delete (camofox close at shutdown). They are baselined as pre-existing; the walker names one sink per coroutine, so they replace the old `-> requests.get` keys for the same two coroutines. - #87 no-agent-construction gate: the alias walker handled ast.Assign only. It now handles AnnAssign too (`store: X = self.session_store`). - Cluster A (16 rows here, 33 fleet-wide): tests/ci/test_no_new_source_proxy_asserts.py is a ratchet. It flags any test function that calls getsource and asserts with in/not in. The 116 functions in 84 files that match today are frozen in source_proxy_baseline.json. A new one fails, and so does a stale key. Opt out on the def line with `# noqa: source-proxy <reason>`. Co-authored-by: ang-fleet-workers[bot] <333956806+ang-fleet-workers[bot]@users.noreply.github.com> Co-authored-by: ang-fleet-interactive[bot] <333962442+ang-fleet-interactive[bot]@users.noreply.github.com>
Summary
Closes the three backlog residuals logged by #80 (the F2 authoritative breadcrumb). Spec:
2026-06-22_f2-breadcrumb-backlog-cleanup.md(2× Opus spec review + 2× Opus diff review, both APPROVE).Zero behavior change to the shipped #80 detection path — this is config + tests + docs, guarded by a source-guard test.
Item 3 —
restart_*config family consistencyDEFAULT_CONFIG["agent"]with doc comments:restart_loop_threshold: 3,restart_loop_window_secs: 300,restart_initiated_ttl_secs: 600(== the live helper fallbacks).restart_initiated_ttl_secsconfig→env bridge._bridge_agent_config_to_env+_AGENT_CONFIG_ENV_BRIDGE(single-sourced, unit-testable). Behavior-neutral for all 8 existing knobs (a reviewer executed the equivalence proof over 8 input cases). config.yaml unconditionally wins over a pre-set env var, matching the shipped siblings (PR fix(auxiliary): strip trailing /v1 for OpenCode Zen/Go Anthropic bases NousResearch/hermes-agent#18413).restart_loop_threshold/window_secswere already bridged but never inDEFAULT_CONFIG— they only worked if hand-added; now properly defaulted. The helpers already defaulted to 3/300 when unset.Item 2 — breadcrumb contract drift insurance
# F2 BREADCRUMB CONTRACTblock ingateway/run.pyas the single documented source of truth.safe-restart.py, inANG-Ventures/hermes-home) has a mirror test asserting the same literals — so drift in either repo reddens that repo's own CI, with no cross-repo skip-no-op.Item 1 — watcher guardrail (skill side, lands in hermes-home)
safe-restart.py). It can't write one (post-quiescence + wrong-boot), so it reads-only. + a SKILL.md "always use safe-restart.py" contract.Testing
🤖 Generated with Apollo (Hermes agent)