Skip to content

v6.7 Tranche 1: kanban_complete verification gates (#28, #62, #64) - #43104

Closed
jarvis-stark-ops wants to merge 6 commits into
NousResearch:mainfrom
1Team-Engineering:kaipo/v6.7-completion-gates
Closed

v6.7 Tranche 1: kanban_complete verification gates (#28, #62, #64)#43104
jarvis-stark-ops wants to merge 6 commits into
NousResearch:mainfrom
1Team-Engineering:kaipo/v6.7-completion-gates

Conversation

@jarvis-stark-ops

Copy link
Copy Markdown

Summary

Adds three pre-write-txn gates in complete_task that catch the failure modes the 2026-06-09 v6.7 build chain demonstrated. See #61 for the full case study — 9 tasks reported done in ~10 minutes with zero real deliverables.

Pattern mirrors the existing _verify_created_cards / HallucinatedCardsError flow at hermes_cli/kanban_db.py:3607. State unchanged on rejection; worker can retry after fixing the issue.

Gates

  • verify_runtime_floor (closes #64) — per-role floor on completed_at - started_at. build roles 5min, review roles 90s, orchestration 0. Catches Tony's 20s reviews and Friday's 59s "implementation".
  • verify_workspace_diff (closes #62) — non-review workers on dir/worktree workspaces that claim implementation work must produce a non-empty git diff against the tracking base. Catches Friday's empty branch + fabricated summary.
  • verify_no_stray_artifacts (closes #28) — rejects *evidence*, commit-hash*, triage/*, tmp-*, and tracked files with no extension and no shebang. Catches the agent-dashboard PR Terminal tool #1 "all prior block evidence files" failure mode.

Opt-outs

  • metadata.x_fast_justified → bypasses runtime floor
  • metadata.x_no_code → bypasses workspace-diff
  • metadata.x_stray_ok → bypasses hygiene gate

Opt-outs recorded on the completed event for audit.

Test plan

  • 28 new tests in tests/cli/test_kanban_completion_gates.py — each pin pins the exact 2026-06-09 failure modes plus clean-path passes and opt-outs
  • Wider regression: pytest tests/cli/ tests/tools/ -k 'kanban or complete or task' → 258 passed / 0 failed
  • Validation loop: spawn a fresh swarm chain with intentional empty-diff/stray-file/fast-completion attempts; verify each is rejected at kanban_complete time

Why this PR alone (Tranche 1)

v6.7 has 11 total issues. This PR addresses the three that would have rejected the 2026-06-09 build chain on the first attempt. Tranche 2 covers #29/#30/#31 (reviewer-field validation + auto-spawn integrative review + reviewer SOULs in hermes-jarvis). Tranche 3: #32/#33/#34/#63/#65 (doc-drift, gh-auth propagation, respawn-guarded exemption, PR-existence verification, auth-claim verification).

🤖 Generated with Claude Code

Jarvis and others added 6 commits June 6, 2026 06:47
…callers

Two tool schemas advertised `required: []` (or just `["mode"]`) but
enforced additional requirements in the Python handler. Anthropic-tuned
models like Claude read the prose "REQUIRED PARAMETERS: …" hints and
emit the fields. xAI grok-4.3 reads the JSON Schema literally and omits
both/all of them, then loops on the Python validator's `tool_error`.

This blocked autonomous orchestration profiles (e.g. JARVIS in the
1Team-Engineering hermes-jarvis fleet) running on grok from completing
or patching anything.

Fix: express the real constraints in the schema using JSON Schema
2020-12's `anyOf` / `oneOf`. Both branches use only `required` so the
constraint is honored by every conformant validator.

- kanban_complete: anyOf summary or result (matches handler line 543)
- patch:           oneOf mode=replace (path+old_string+new_string)
                        | mode=patch  (patch)
                   (matches handler in _handle_patch)

Schema validation tested with jsonschema 4.25.1 (Draft 2020-12):
- kanban_complete: 5/5 cases match expected validity
- patch:           6/6 cases match expected validity

Handlers unchanged — they still tolerate both shapes for CLI legacy
callers that bypass the schema layer (e.g. `hermes kanban complete
--result "..."`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
schema: express conditional-required constraints for grok-style tool callers
…de (#8)

Closes #7.

Problem
The kanban dispatcher loop in `_kanban_dispatcher_watcher` can silently stop
cycling while the gateway process stays alive — observed twice in one day
(2026-06-07). Cause is unclear (possibly hung `dispatch_once()`, sqlite lock,
or event-loop livelock). Without instrumentation we can't tell a dead loop
from an idle gateway.

This unblocks investigation of #5 (HTTP 429 → delayed retry) and #6 (provider
auth crashes → skip-not-stall), both of which were blocked on "is the
dispatcher even running?" being answerable.

Solution
Write a heartbeat JSON to `$HERMES_HOME/state/dispatcher_health.json` at the
end of every dispatcher cycle (success AND exception paths AND cancellation).

Schema (v1, stable contract):
  schema_version: 1
  last_cycle_ts: float        # unix seconds, end of cycle
  last_cycle_iso: str         # UTC ISO 8601 with Z suffix
  cycle_started_at: float
  cycle_duration_seconds: float
  interval_seconds: float     # configured cadence (default 60)
  cycles_since_start: int     # monotonic; first cycle = 1, 0 means "never wrote"
  any_spawned_this_cycle: bool
  spawned_total_this_cycle: int   # count across all boards
  ready_pending: bool         # ready queue non-empty
  consecutive_bad_ticks: int  # mirrors the existing HEALTH_WINDOW counter
  gateway_pid: int
  cycle_error: str | null     # exception text if cycle errored

Detection rule for monitors:
  stall = time.time() - last_cycle_ts > 2 × interval_seconds
  dead  = last_cycle_ts > 5 minutes ago AND gateway PID still alive

Implementation
- New method `GatewayRunner._write_dispatcher_heartbeat` (gateway/run.py).
  Uses existing `atomic_json_write` for crash-safe writes.
- Called from `_kanban_dispatcher_watcher` at end of every iteration.
- Heartbeat-write failures are wrapped in try/except so they can NEVER
  kill the dispatcher (heartbeat is a diagnostic, not a dependency).
- Cancellation path also writes a final heartbeat with cycle_error="cancelled"
  before re-raising — so monitors can distinguish clean shutdown from crash.
- Locals (`cycles_since_start`, `any_spawned`, etc.) initialized at top of
  the loop body BEFORE any try block so they're defined for the heartbeat
  call even if zombie-reap or main tick throws.

Tests (5/5 passing)
- Schema-v1 contract pinned (all 13 keys, types, ISO Z suffix)
- Cycle-error path recorded correctly
- Two writes overwrite (not append)
- First-cycle-is-1 contract (monitors treat 0 as "never wrote")
- Auto-creates `state/` dir if missing

Follow-up (separate issues)
- Extend `hermes gateway status` to read this file and show stall age
- Schema v2: add `gateway_started_at` for uptime computation without ps
- Optional Prometheus textfile exposition for node_exporter setups

Co-authored-by: Jarvis <jarvis@Kaipos-Mac-mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ot just runtime (#9)

Closes #6.

Problem
Worker startup calls `resolve_runtime_provider` to acquire credentials for
the primary provider. If that raises AuthError (xAI OAuth token expired,
Anthropic logged out, Codex revoked), the worker crashes before AIAgent's
runtime fallback loop ever gets a chance — even though the user has
explicitly configured a fallback chain for exactly this case.

Observed in the v6.6 incident 2026-06-07: xAI OAuth token went missing
mid-session and every subsequent worker crashed at startup despite having
`fallback_providers: [openai-codex/gpt-5.5, xai-oauth/grok-4.3]` configured.

Solution
New helper `_resolve_runtime_with_fallback` wraps the primary-resolution
call. On AuthError, iterates the configured fallback chain (read once from
`get_fallback_chain(cfg)`) until one succeeds. If all fail, re-raises the
LAST AuthError so cli.py's exit handling can surface it.

Three safety bounds preserved (informed by code-review):

1. **Explicit CLI pin** — `hermes -z --model X --provider Y ...` should NOT
   silently downgrade. When `model` OR `provider` was a non-empty CLI arg,
   the helper re-raises primary AuthError verbatim, no fallback attempt.

2. **Rate-limit AuthError on primary** — falling through to other providers
   would burn their quota in milliseconds (the "quota amplification"
   footgun). Detected via existing `is_rate_limited_auth_error()` —
   re-raise immediately; existing rate-limit handling (cli.py exit 75)
   gets the task requeued.

3. **Remaining-chain handoff to AIAgent** — when fallback lands on chain
   entry [N], AIAgent's runtime fallback loop should only see entries
   AFTER N (not the dead primary, not the entry we just used). The helper
   now returns `(runtime, effective_model, landed_at_index, remaining_chain)`
   and the caller passes `remaining` to AIAgent's `fallback_model`.

Implementation
- `hermes_cli/oneshot.py:33-110` — new helper (testable at module level).
- `hermes_cli/oneshot.py:439-460` — call site updated; reads chain once,
  detects explicit_pin from CLI args, passes remaining_chain to AIAgent.
- AIAgent receives the correctly-sliced chain via `fallback_model=_fb`,
  preserving existing runtime-fallback semantics for mid-conversation
  failures.

Tests (9/9 passing) — tests/cli/test_oneshot_runtime_fallback.py
- primary succeeds → no fallback attempted, full chain preserved for AIAgent
- primary fails, first fallback succeeds → effective_model advances,
  remaining_chain sliced correctly
- two failures → third succeeds, slicing correct
- all fail → LAST AuthError propagates (not primary's)
- empty chain → primary error verbatim
- fallback without model → effective_model preserved
- explicit_pin=True → no fallback, primary error verbatim
- rate-limit AuthError → no fallback, primary error verbatim
- same provider in chain → no infinite loop, advances to next entry

Code-review pre-merge: reviewer caught silent-downgrade regression, stale
chain handoff, and quota-amplification footgun. All three addressed.

Follow-up (separate issues, not blocking)
- Consider applying the same pattern to `gateway/run.py:_resolve_runtime_agent_kwargs`
  and `cli.py:4881-4914` for a consistent worker-startup contract across surfaces.
- Optional: emit a metric/heartbeat counter when fallback fires so we can
  detect "constantly failing primary" silently.

Co-authored-by: Jarvis <jarvis@Kaipos-Mac-mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rs (#10)

Closes #5.

Problem
The non-quiet `hermes -p <profile> chat -q "..."` path (the actual invocation
pattern Marvel team kanban workers use, e.g. `chat -q "work kanban task X"`)
never applied the kanban EX_TEMPFAIL exit-code mapping. A rate-limited worker
exited 0 by virtue of `cli.chat()` returning cleanly. The dispatcher's reap
classifier then treated rc=0 as a "protocol violation" (worker exited cleanly
without calling `kanban_complete` or `kanban_block`) and auto-blocked the task.

This was the v6.6 incident root cause. Tony+Tchalla re-reviews (t_77ac35a7,
t_a30a88db) hit Codex 429, exited 0, dispatcher auto-blocked, chain stalled
2 hours until quota reset — even though `KANBAN_RATE_LIMIT_EXIT_CODE = 75`
and the mapping at cli.py existed (just only on the QUIET single-query path).

Solution
1. Extract the exit-code mapping into `_worker_exit_code_from_result(result)`
   at module level. Returns 0 / 1 / 75 per these rules:
     - None or non-dict or success → 0
     - Failure outside a kanban worker → 1
     - Failure inside a worker with failure_reason ∈ {rate_limit, billing} → 75
     - Any other failure inside a worker → 1
2. Refactor the quiet path (~line 16172) to call the helper instead of inline.
3. Make `cli.chat()` stash its run_conversation result on `self._last_run_result`
   so the non-quiet caller can inspect failure metadata after chat() returns
   only the response string. Reset to None at __init__ AND at start of every
   turn — invariant doesn't depend on early-return order (caught by code review).
4. Wire the non-quiet path (~line 16197) to call the helper after chat() returns.
5. Exception path inside chat() synthesizes `{failed:True, error:..., completed:False}`
   so single-query callers still apply mapping (rate-limit branch doesn't fire
   because no failure_reason — correctly falls through to exit 1).

Tests (9/9 passing) — tests/cli/test_worker_exit_code_from_result.py
- None result → 0
- Non-dict result → 0
- Success result → 0
- Failure outside kanban (no HERMES_KANBAN_TASK env) → 1, regardless of reason
- Rate-limit inside kanban → KANBAN_RATE_LIMIT_EXIT_CODE (75)
- Billing inside kanban → 75 (same recovery story)
- Other failures inside kanban → 1
- Missing failure_reason field inside kanban → 1 (defensive)
- Rate-limit without HERMES_KANBAN_TASK env → 1 (human CLI gets generic exit)

Combined with #6 (worker-startup fallback) and #7 (dispatcher heartbeat),
both already merged, the worker-startup → dispatcher-detect → next-retry
loop is now operationally robust:
- #7 — silent stalls detectable via heartbeat JSON
- #6 — primary provider auth crash falls through to fallback chain at startup
- #5 (this) — rate-limit failures exit 75 so dispatcher requeues without
  burning the retry counter

Code-review pre-merge: reviewer caught a stale-stash bug (previous turn's
`_last_run_result` leaking into a downstream consumer if chat() takes an
early return path). Fixed by initializing the stash to None in __init__
AND resetting at the top of each chat() turn — invariant pinned.

Follow-up (separate issues, not blocking)
- `_print_exit_summary()` shows "Resume this session with:" even on rate-limit
  failure in the non-quiet path. Pre-existing; not introduced by this PR.
- Non-quiet branch doesn't check `HERMES_KANBAN_GOAL_MODE` env var (only
  quiet path runs `_run_kanban_goal_loop_q`). If a goal_mode worker ever
  spawns via the non-quiet path, the goal loop silently skips. Pre-existing.

Co-authored-by: Jarvis <jarvis@Kaipos-Mac-mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three pre-write-txn gates that fire before complete_task transitions a
task to done. Mirrors the existing _verify_created_cards /
HallucinatedCardsError pattern: any violation is recorded as an audit event
and raised, so worker state is unchanged and the worker can retry after
fixing the underlying issue.

Closes hermes-jarvis#28 (repo hygiene gate)
Closes hermes-jarvis#62 (workspace-diff verification)
Closes hermes-jarvis#64 (per-role runtime floor)
Context: hermes-jarvis#61 (bootstrap-paradox case study)

## The three gates

1. verify_runtime_floor — per-role floor on completed_at - started_at.
   build roles 5min, review roles 90s, orchestration roles 0.
   Catches Tony's 20-second "approve" verdicts and Friday's 59-second
   "implemented 7 dispatcher gates" claims.

2. verify_workspace_diff — when a non-review worker on a dir/worktree
   workspace claims to have produced code, git diff against the tracking
   base must show actual changes. Catches Friday's "Wave A gates
   implemented" with an empty diff on the branch.

3. verify_no_stray_artifacts — rejects untracked or tracked artifacts
   matching patterns the swarm has historically committed by accident:
   *evidence*, commit-hash*, triage/*, tmp-*, and tracked files with
   no extension and no shebang (the "all prior block evidence files"
   failure mode from agent-dashboard PR #1).

## Opt-outs

Workers may bypass individual gates via per-call metadata keys:
  x_fast_justified  → allow_below_floor
  x_no_code         → allow_no_code
  x_stray_ok        → allow_stray

Opt-outs are recorded as part of the completed event for audit.

## Tests

28 new tests cover the exact 2026-06-09 failure modes (Tony 20s,
Friday 59s + empty diff, PR-1 "all prior block evidence files") plus
clean-path passes and opt-outs. 258 passed / 0 failed in the wider
kanban+complete+task test suite — zero regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jarvis-stark-ops

Copy link
Copy Markdown
Author

Wrong target — re-opening against 1Team-Engineering/hermes-agent.

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