Skip to content

feat(gateway): F2 breadcrumb backlog cleanup (config family, contract gate, watcher guardrail) - #86

Merged
Kyzcreig merged 2 commits into
mainfrom
fix/f2-breadcrumb-backlog
Jun 22, 2026
Merged

Kyzcreig merged 2 commits into
mainfrom
fix/f2-breadcrumb-backlog

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

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 consistency

  • Surface the whole family in DEFAULT_CONFIG["agent"] with doc comments: restart_loop_threshold: 3, restart_loop_window_secs: 300, restart_initiated_ttl_secs: 600 (== the live helper fallbacks).
  • Add the missing restart_initiated_ttl_secs config→env bridge.
  • Refactor: extracted the inline agent-config bridge block into _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).
  • Ride-along (not a behavior change): restart_loop_threshold/window_secs were already bridged but never in DEFAULT_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

  • A # F2 BREADCRUMB CONTRACT block in gateway/run.py as the single documented source of truth.
  • An always-on frozen-literal conformance test over a key vector (empty/4KB/unicode/slash). The skill (safe-restart.py, in ANG-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)

  • The watcher does a boot-matched breadcrumb check at kickstart and logs loudly when no current-boot crumb exists (a direct-watcher invocation that bypassed 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

  • 53 cascade tests (9 new) + 246 blast-radius (cascade + status + session-hygiene + config) green; ruff clean.
  • RED-proven: bridge wire-up, config-wins precedence, conformance gate (dirname + hash skew), source-guard, watcher boot-match (presence-only would break it).
  • Skill side: 59/59 green (mirror conformance + boot-matched guardrail RED-proven).

🤖 Generated with Apollo (Hermes agent)

…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).
@Kyzcreig
Kyzcreig enabled auto-merge (squash) June 22, 2026 11:16
@github-actions

github-actions Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

🔎 Lint report: fix/f2-breadcrumb-backlog vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11345 on HEAD, 11328 on base (🆕 +17)

🆕 New issues (13):

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.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR closes three backlog items from the F2 authoritative-breadcrumb work (#80): it refactors the inline agent-config→env bridge into a single-sourced _AGENT_CONFIG_ENV_BRIDGE dict + _bridge_agent_config_to_env function, surfaces the full restart_* config family in DEFAULT_CONFIG, and adds a frozen-literal conformance gate for the cross-repo breadcrumb contract. There are no behavior changes to the shipped F2 detection path.

  • Config bridge refactor (gateway/run.py): The 8-key inline block is replaced by _bridge_agent_config_to_env. The new function is unit-testable and the mapping is single-sourced; the new restart_initiated_ttl_secs key and its env-var bridge are added alongside the existing knobs.
  • DEFAULT_CONFIG additions (hermes_cli/config.py): restart_loop_threshold: 3, restart_loop_window_secs: 300, and restart_initiated_ttl_secs: 600 are now documented defaults that match the live helper fallbacks.
  • Test coverage (test_restart_cascade.py): Nine new tests covering bridge-map membership, precedence semantics, absent-key passthrough, clamp bounds, non-dict defensiveness, frozen-contract conformance, and a source-guard for the fix(gateway): authoritative restart-initiator breadcrumb for F2 breaker #80 detection path.

Confidence Score: 5/5

Safe to merge — config/docs/tests addition with a provably behavior-neutral refactor of the agent-config bridge.

The bridge refactor is equivalent to the old inline block for every input shape, and 9 new tests RED-prove the key invariants. The DEFAULT_CONFIG additions surface values that were already the effective runtime defaults.

No files require special attention.

Important Files Changed

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

Comment thread tests/gateway/test_restart_cascade.py
Comment thread tests/gateway/test_restart_cascade.py
@Kyzcreig
Kyzcreig merged commit 3166de5 into main Jun 22, 2026
37 checks passed
@Kyzcreig
Kyzcreig deleted the fix/f2-breadcrumb-backlog branch June 22, 2026 11:35
@Kyzcreig
Kyzcreig restored the fix/f2-breadcrumb-backlog branch September 21, 2026 10:32
Kyzcreig pushed a commit that referenced this pull request Sep 28, 2026
…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>`.
Kyzcreig pushed a commit that referenced this pull request Sep 28, 2026
…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>`.
Kyzcreig pushed a commit that referenced this pull request Sep 28, 2026
…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>
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