Skip to content

Complete LangGraph migration epic (#169)#172

Open
chimerakang wants to merge 234 commits into
mainfrom
exp/hermes-vscode-hook
Open

Complete LangGraph migration epic (#169)#172
chimerakang wants to merge 234 commits into
mainfrom
exp/hermes-vscode-hook

Conversation

@chimerakang

Copy link
Copy Markdown
Owner

Scope

Closes #169. Lands the full LangGraph-style snapshot-driven runtime for Hermes plus all 7 hand-coded edge cases the issue body called out. Does NOT close #171 — Class A migration is included behind a feature flag, Class B is partially scaffolded (StartViaGraph + pause-resume) but not on by default.

⚠️ Branch hygiene note for reviewers

This is a long-lived experimental branch (exp/hermes-vscode-hook) that accumulated 197 commits since main. 26 commits are in-scope for this PR (listed below). The other ~80 commits are unrelated work (i18n, dashboard, #170 sticky-session fix, IssueOps FSM, prototype removal, VS Code hook, etc) that happen to ride along — most of those have already shipped via their own PRs and are essentially no-ops vs main; the rest are small follow-ups that could not be cleanly cherry-picked over.

Cherry-picking just the 26 #169 commits onto a fresh branch is impractical: the first conflict appears in internal/app/hermes/store.go because the 80 intervening commits also touch that file. Resolving the cascade by hand would re-do most of the work.

Recommendation for review: filter git log by commit-message prefix (#169 slice, #169 γ, #169 β, #169 #N) to see the actual scope. The diff is large but each in-scope commit is self-contained with its own tests.

In-scope commits (26)

Foundation

  • d4e328b Add Hermes snapshot schema and runtime types
  • 2817e38 Add Hermes state update reducers
  • f1b2ea9 Write Hermes runtime snapshots at step boundaries
  • ae5e30e slice 1: unify error classifiers behind engine/errorclass
  • 2473fae slice 2: persist Hermes failure-pause as durable interrupt + orphan sweep
  • 4fd7b37 slice 3a + 3b: route all engine writes through CommitRuntimeStep
  • 1031f42 slice 3c: read paths derive TaskState from latest snapshot
  • 69fdc51 slice 3d: snapshot becomes sole authority for task status

β phase — interrupt + resume

  • 5d92856 β1: cold-restart resume for Hermes failure pause
  • e2284ff β2: persist pause decision into snapshot for cold-restart resume
  • 57f932a β3+β5+β6: zombie-sweep fix, live persist, /resume, pause reminder

γ phase — graph nodes

  • 2a39a03 γ1: graph runtime skeleton (Walker + Registry + Node interface)
  • 0eaecf6 γ2: PlannerNode wraps PlannerSession.Plan
  • b4508bf γ3a: minimal ExecutorNode for snapshot-driven sub-task dispatch
  • c5d5591 γ4: ApprovalNode + ExecutorNode failure-pause routing
  • d850941 γ3c: StrictReviewNode for per-sub-task block/retry routing
  • 9bff48d γ5: ReviewerNode for task-level review routing
  • afde881 γ6: Walker-driven RunViaGraph entry point + node adapters

Hand-coded edge cases (issue body §1-7)

Cut-over scaffolding (#171 partial)

What this delivers

  1. Snapshot is now the canonical source of truth for task state: status, plan, accumulated, interrupts, replan context, walking-agent session contract — all live on the snapshot, not in process-local engine fields. Survives restart cleanly.

  2. Walker dispatches 6 nodes (planner / replan_setup / executor / strict_review / reviewer / approval) plus a terminal sentinel. Each Node is a pure function over HermesState → NodeOutput; side effects flow through StateUpdate reducers.

  3. All 7 hand-coded edge cases resolved — see commits above for individual decisions documented in each commit message.

  4. Graph-side runtime is production-ready but cut-over is gated:

    • ALICE_DIRECT_PLAN_VIA_GRAPH=1 → Class A (direct chat) uses Walker
    • ALICE_HERMES_START_VIA_GRAPH=1 → Class B (Hermes async) uses Walker
    • Both default off; legacy Run() / Start() are unchanged
  5. Cut-over follow-up tracked in Cut over plan_execute.Run() to RunViaGraph one task class at a time #171.

Test plan

  • go build ./... clean
  • go test ./... all packages green
  • Walker integration tests cover planner → executor → reviewer → terminal
  • Replan loop integration test covers block-review → replan_setup → second attempt → pass
  • Walking-agent integration test covers two sub-tasks with session reuse
  • Panic recovery test covers node panic → terminal commit + recorded error
  • ctx-cancel test covers commit interrupt before exit
  • Class A wrapper test covers RunViaGraphResult success path
  • Production bake — Class A flag-on cycle (1 week, post-merge)
  • Production bake — Class B flag-on cycle (1 week, after Class A)
  • Delete legacy PlanExecuteEngine.run() (separate PR after both bakes)

🤖 Generated with Claude Code

chimerakang and others added 30 commits April 22, 2026 19:43
Phase 1 of the experiment branch: classify Claude Code prompts via the
Complexity Gate heuristic and persist the decision without intervening.
Empty hook response keeps VS Code and terminal sessions unchanged while
we gather data to validate classifier accuracy before enabling block mode.

- Add ClassifyComplexity heuristic with trivial/moderate/complex tiers
- Add prompt_classifications table and RecordPromptClassification
- Add /api/hooks/user-prompt-submit endpoint
- Add scripts/hooks/user-prompt-submit.sh forwarder with fail-open behavior
  and vscode/terminal source detection

Hook registration lives in .claude/settings.local.json (gitignored) so the
experiment only affects the current operator's Alice project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 1 observation showed nearly all VS Code prompts landed in the
moderate-default bucket because the heuristic only matched English shell
verbs and the ide_opened_file payload inflated prompt length. Replay of
the first nine real samples now resolves every record to an explanatory
rule instead of the unhelpful default.

- Strip <ide_opened_file>, <system-reminder>, <command-*> and
  <local-command-*> tags before scoring so injected editor noise does
  not dominate the length signal
- Add Chinese moderate verbs (研究/分析/檢查/確認/處理/修正/修復/調整/...)
  and complex verbs (重構/重寫/整合/重新設計/遷移/...)
- Add short Chinese shell-like verbs (查一下/重啟/啟動/...)
- Recognise bare issue references (#61, #61) as moderate so task
  entry points are not mistaken for noise
- Reorder precedence: complex > moderate > trivial so a prompt
  containing both "檢查" and "查一下" resolves to moderate, not trivial

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tightens Planner behaviour so simple goals return a single "execute
directly" sub-task while complex goals decompose into 3-7 tasks with
granularity checks.

- Planner system prompt rewritten to require direct-execution mode for
  simple goals (commit & push, add file & test) and prefer
  underdecomposition over overdecomposition
- Add validateGranularity: reject plans with >8 tasks whose leading verb
  repeats for more than half the entries (classic per-item
  decomposition smell — "stage file A", "stage file B", ...)
- Retry once with feedback on granularity violations before failing
- Coordinator hard-caps plans at 15 sub-tasks and marks the task failed
  if the Planner still exceeds the limit after retries
- Executor rules now document tool_hints as advisory; executor may pick
  a different tool when hints do not match the current file state

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replays the stored prompt_classifications snippets against the current
classifier and prints the updated decision for each record. Used to
validate heuristic changes without waiting for new VS Code samples.

Not wired into the bot; invoke with:
  sqlite3 data/alice.db "SELECT id, prompt_snippet FROM prompt_classifications ORDER BY id;" > /tmp/samples.txt
  go run ./cmd/reclassify

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-mode feature flag controls whether complex classifications block
the VS Code / terminal prompt:

  off     (default) — Phase 1 observe-only behaviour, no blocking.
  dryrun            — log wouldBlock=true without sending block to Claude
                      Code; useful for watching real decisions accumulate
                      before committing to hard block.
  on                — return {"decision":"block"} for complex prompts
                      with a reason pointing users to /hermes in Telegram.

Only complex (not moderate) classifications ever block; trivial stays
untouched in every mode. ALICE_VSCODE_BLOCK_REASON overrides the default
Chinese reason string.

Log line now includes mode + block/dryrun flags so dashboard queries can
compute wouldBlock accuracy before operators flip the mode to "on".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rks (#105)

Every helper in internal/app/hermes/github.go invoked gh via
exec.CommandContext without setting Dir, so gh always resolved the
repository from the bot's cwd (Alice). A chat configured for another
project issued /hermes #239 and got "gh issue view 239: exit status 1"
because Alice has no such issue.

- Add ghCommand helper that honours a projectDir argument; empty string
  keeps the old cwd-inheriting behaviour for backward compatibility
- Thread projectDir through FetchIssue, PostComment, SyncChecklist,
  ApplyLabel, CloseIssue, WritePlanToIssue, PostCommentEvent
- Coordinator forwards c.cfg.ProjectDir on every gh call
- Telegram /hermes #N entry passes the chat's agent.ProjectDir()

Verified gh resolves the correct repo when run from the target
project directory and fails fast when the directory has no git remote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Record #5 of prompt_classifications captured the first prompt of the
#61 work session — a real user question ("請幫我研究...") prefixed by
an <ide_opened_file> tag. Under the old classifier it was misclassified
as complex via the long-prompt rule because the wrapper inflated length.

Lock the correct post-upgrade behaviour: strip the tag, recognise "研究"
as a moderate verb, classify as moderate-verb:研究. Any future change
that breaks ide_noise stripping or reorders the moderate precedence
will surface here.

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

Pull all security-related code out of internal/app/security.go into a
dedicated subpackage so issue #61's token detection engine has a clean
home. SecurityConfig/Event/Manager, PII detection, rate limiting, IP
filtering, HTTP middleware, and crypto now live under
internal/app/security/. Callers use `security.Global()` and the
stuttered types (e.g. `security.SecurityEvent`) — renaming to drop the
stutter is left for a follow-up.

Persistence and WebSocket broadcast are injected via
security.OnPersistEvent / OnBroadcastEvent callbacks in main.go to
avoid a reverse dependency on package app.

Non-security config structs that happened to live in security.go
(MultimediaConfig, RenderingConfig, ModelRoutingConfig, ModelRoute,
GetDefaultModelRoutes) move to internal/app/config_types.go.

No behavioral changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Natural-language messages classified as complex by the Complexity
Gate heuristic (#103) now start Hermes automatically — no /hermes
command required. Moderate and trivial messages keep flowing
through the existing model-routing path so lightweight questions
are not hijacked into Planner-Executor orchestration.

- Add HermesConfig.AutoRouteComplex (default false) so the feature
  is opt-in per operator
- TG handleMessage runs ClassifyComplexity after the /hermes-mode
  check; on complex + non-continuation, start a Hermes task for the
  current chat's project_dir
- Skip classification on isContinuationMessage matches (pronoun /
  short follow-up) so conversational context stays on the routing
  path and does not spin up a new Hermes task per reply
- User-visible notification quotes the matched rule so routing
  decisions are auditable in chat without tailing logs

Operator enables via config.json: "hermes": {"auto_route_complex": true}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Natural-language shortcuts like "請處理 #239" or "fix #61" are the most
common way the operator kicks off substantive issue work, but they used
to land in moderate because 處理 / fix are on the moderate verb list.
With Complexity Gate auto-routing enabled, that meant Hermes never took
these tasks automatically even though they are exactly the decomposable
work Planner-Executor was built for.

- Add actionVerbs subset (處理 / 修 / 實作 / 完成 / fix / implement /
  build / create / ship / do) — moderate verbs that imply doing work
  rather than merely looking
- New precedence rule: any actionVerbs match combined with an issue
  reference (#123 or #123) promotes to complex with matched rule
  "action-verb+issue-ref:<verb>"
- Research / check / analyze + issue-ref intentionally stays moderate
  so "研究一下 #61 的現況" does not hijack a read-only query into
  Hermes orchestration

Coverage: six new test cases across halfwidth/fullwidth issue refs and
both Chinese / English action verbs, plus two negative cases for the
research+issue and check+issue non-trigger paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Task Summary Report feature landed in the #102 commit but nothing
called AddUsage or TaskSummary.GenerateSummary, so operators saw only
the subtask completion message and no token breakdown after running
/hermes. Close the gap so "hermes.summary.enabled: true" in config
actually produces output.

- store: add model_usages TEXT column plus ALTER TABLE migration, and
  propagate the field through CreateTask, scanTask, scanRowsInto and
  every SELECT so existing tasks keep loading cleanly
- store: add AddModelUsage(taskID, model, input, output) that merges
  per-model call count and input/output tokens atomically; NoopStore
  gets a matching stub
- planner: Plan and Compress now return (inputTokens, outputTokens)
  separately instead of a combined total so callers can record an
  accurate per-model ModelUsage
- coordinator: call AddModelUsage(PlannerModel, ...) after Plan and
  Compress, and AddModelUsage(ExecutorModel, ...) after every Execute
  — AddTokenUsage is unchanged, the budget still tracks the total
- progress: OnDone renders TaskSummary.GenerateSummary() in minimal
  form when state.ModelUsages is non-empty. Presence of usage data
  is the enabled signal (Coordinator only populates it when the
  operator has opted in), so the reporter needs no extra config wiring

Existing tasks started before this commit have an empty model_usages
blob and render the pre-patch completion message; only newly-created
tasks see the token breakdown.

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

When auto-routing fired on prompts like "處理 #250" the Coordinator only
saw the seven-character user message — Planner had no context about the
issue and reasonably produced a research-only plan (Read / git log /
report) instead of implementation sub-tasks (Edit / Write / Bash).

Detect rules starting with "action-verb+issue-ref:", parse the issue
number, and route through startHermesFromIssue so the same FetchIssue
+ BuildGoalFromIssue path used by /hermes #N feeds the Planner the
title, body, and unchecked checklist. Other complex triggers (refactor,
implement new module, long prompts) still go through startHermesTask
with the original text.

- ParseIssueNumber lifts the issue regex into an exported helper that
  normalises full-width digits (#250) and rejects non-numeric noise
- Auto-route falls through to startHermesTask whenever the issue
  number cannot be parsed, so a malformed reference never blocks the
  request

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Budget-exceeded events used to terminate the task immediately, posting a
"budget exceeded" GitHub comment and dropping any unfinished sub-tasks.
This commit lets callers supply a confirmation channel so the user can
acknowledge the warning and let the task continue from the next sub-task
with a refreshed wallclock window.

- CoordinatorConfig.ContinueCh is an optional chan struct{}; when set
  and the budget warning fires, the coordinator blocks on the channel
  with ContinueTimeout (default 15 min) before falling back to the
  legacy fail-and-comment path
- On confirmation the wallclock budget StartedAt is rewritten so the
  remaining sub-tasks get a fresh window; total token cap is unchanged
- TaskStateStore.ResetBudgetStartedAt persists the new start time so a
  crash mid-resume reloads the correct budget; SQLiteTaskStore reads
  the budget JSON, mutates StartedAt, and writes back inside one
  execWithRetry callback. NoopTaskStore gets the matching stub
- Channel-less callers keep the previous behaviour unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add ListIssues / FormatIssueList so the Telegram bot can present a
prioritised view of open GitHub issues, ready to feed into a
"/issues" or "/work-on-next" command later.

- ListIssues(ctx, projectDir, limit) calls `gh issue list --json
  number,title,labels,milestone,updatedAt` in projectDir so cross-repo
  chats hit the right remote (same fix as #105). Default limit 15
- Per-issue Priority score: bug +10, blocked -5, priority labels
  +8/+4, has-milestone +5, updated within 72h +3 — tuned to surface
  recent bugs first while still respecting milestone scope
- Insertion sort on Priority desc, then UpdatedAt desc
- FormatIssueList renders Telegram-friendly Markdown with a coloured
  indicator (🔴 ≥13 / 🟠 ≥8 / 🟡 ≥3 / ⚪ otherwise), the labels and
  milestone, and a usage hint pointing at /hermes #N

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-world samples showed auto-route was triggering Hermes on prompts
that were clearly state inquiries — "請問 #225 子項目處理完畢了嗎",
"請檢視 #225 還有哪些子項目要處理", "但剛剛 #246 都處理過了,為何還在開發中?" —
because they all matched action-verb+issue-ref. Same problem hit common
substrings: "do" caught "do you", 修 caught 修改/修飾, 做 caught 做的/做了.

- Drop single-char action verbs (修, 做, do) so they no longer fire
  on substring matches like 修改 / 做的 / "do you"
- Add statusQueryPatterns (22 markers including 完畢 / 過了 / 哪些 /
  那些 / 子項目 / 還有 / 為何 / 請問 / 檢視 / ?/?): when any of
  these appear alongside an action verb + issue ref, the upgrade is
  suppressed and the prompt stays moderate — auto-route no longer
  spins up Hermes for "is X done?" type messages
- Tighten long-prompt rule: > 200 chars upgrades to complex only when
  the message also contains an issue ref. Operator pasting a long
  Hermes summary back at us no longer triggers a fresh task
- New cases cover all the failure modes (cn status: 完畢/那些/過了/?,
  EN: "do you", 修改/做的) plus a positive case for long+issue-ref

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…heavy work to Sonnet

Three corrections to make Hermes actually finish substantive work
instead of producing pretty-but-empty completion reports.

A. Wallclock budget default 600s → 3600s
   Real implementation tasks (multi-file edits + go build + go test +
   commit) routinely needed > 10 minutes. Operators are reporting
   tasks "interrupted by token problem" but the data shows token use
   stays under 14% — the actual cap they were hitting was the 10-min
   wallclock. Set max_wallclock_seconds: 0 in config.json to disable
   the cap entirely

B. Planner gets an IMPLEMENTATION IMPERATIVE rule
   Audit of done tasks revealed Planner was generating verification-
   only plans (Read + git log + Update Issue) for goals that clearly
   asked for code changes — Issue #250's seven sub-tasks were all
   verification, no Edit/Write. New rule in plannerSystemPrompt: when
   the goal contains 修/實作/修正/實現/refactor/fix/implement/build/
   create/redesign, the plan MUST include at least one Edit/Write/
   file_patch/Bash sub-task. Verification-only plans are explicitly
   permitted only for "verify"/"check status"/"audit" style goals.
   Issue checklists are treated as the implementation list, not as
   items to silently downgrade to "verify X was done"

C. Per-sub-task model dispatch (Haiku → Sonnet for heavy work)
   Add CoordinatorConfig.HeavyExecutorModel + a second ExecutorSession
   inside Coordinator. pickExecutor classifies each sub-task and
   routes Edit/Write/MultiEdit/file_patch tool hints, or descriptions
   containing refactor/implement/重構/實作/修正, to the heavy executor.
   Read/Bash/Grep stays on Haiku. AddModelUsage records the model
   actually used so the #102 token summary shows the Opus/Sonnet/
   Haiku split honestly. NewCoordinator gains an execFnHeavy
   parameter; nil keeps the legacy single-model behaviour. Default
   wired to ModelRouting.SmartModel (Sonnet) so existing config gets
   the upgrade for free; set hermes.heavy_executor_model equal to
   hermes.executor_model to opt out

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operators reported the completion message showed only the token usage
panel — the running "accumulated" text scrolled past and they could not
tell what each sub-task actually achieved. Output also bled English
labels ("calls", "tokens", "Total:", "Goal:") even though the bot is
configured for Traditional Chinese.

Conclusion reporting:
- OnDone now renders a "📋 子任務結果" block enumerating each sub-task
  with its Executor-supplied Result line. Status icon (✅ done /
  ❌ failed / ⏸️ pending / ⏭️ skipped) makes failures obvious without
  reading the full text. Long results truncate to 240 runes
- Replaces the previous reliance on Accumulated, which is mid-task
  narration shaped for the Planner's compression step, not for users

Language alignment:
- BuildExecutorPrompt template translated end-to-end
  (Goal / Accumulated summary / Current sub-task / Suggested tools →
  目標 / 累積進度 / 當前子任務 / 建議工具) and prefixed with an
  explicit "語言要求:所有回應必須使用繁體中文" line so the cold-start
  Executor inherits the project's zh-TW default
- executor_rules.md gains a top-priority "語言要求(最高優先)"
  section that overrides every later rule, plus a structured result
  format (✅ 完成:... / ❌ 失敗:...) so the new OnDone listing reads
  cleanly
- TaskSummary minimal/detailed label translation: "1 calls, 12,450
  tokens" → "1 次呼叫,12,450 tokens"; "Total:" → "合計:";
  "in:/out:/total:" → "輸入:/輸出:/合計:"; "Total: 8m 47s" → "合計:耗時 8m 47s"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ASKS.md

Operators noticed Planner kept generating sub-tasks like "Read
docs/MASTER_TASKS.md to locate task #250" instead of querying GitHub
directly. MASTER_TASKS.md is a snapshot produced by /task-sync — it
lags GitHub by minutes to days, so plans built off it work from stale
state (closed issues marked open, missing recent comments, etc.).

Add an ISSUE INFORMATION SOURCE rule to the Planner system prompt:

- For any question about an issue's body / status / comments / checklist,
  call gh issue view N --json ... or gh issue list ... directly. List
  three concrete invocations the Planner should reach for first
- Explicitly forbid planning a "Read MASTER_TASKS.md to find issue X"
  sub-task when the goal is about a single issue
- Carve out an exception for true cross-task / portfolio queries where
  the local doc is more concise than running many gh calls
- When the goal already starts with [GitHub #N] <title>\\n\\n... the
  issue body was inlined by the auto-route fetch path; no gh issue
  view call is needed and the plan should work straight off the goal
  text already provided

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two latency wins for Hermes:

C. Pre-Planner skip (~10s + 300 Opus tokens saved per simple goal)
   - New ClassifyGoal in hermes/goal_classifier.go: short non-implementation
     goals without [GitHub #N] inlining are flagged GoalSimple
   - Coordinator.run synthesises a single "Execute the goal directly"
     SubTask and bypasses planner.Plan when the goal is simple
   - Implementation verbs (重構/實作/refactor/implement/...) and the
     [GitHub #N] prefix from the auto-route fetcher both keep the Planner
     in the loop because those are the cases decomposition actually helps
   - Six-case unit test pinning the boundary

B-light. Parallel read-only sub-task execution
   - GatherReadOnlyBatch returns up to MaxParallelReadOnly (4) contiguous
     indices when each sub-task's ToolHints is a non-empty subset of
     {Read, Grep, Glob, WebFetch, WebSearch, NotebookRead}. Bash is
     deliberately excluded — even an "innocent" bash call can shell out
     to git commit / npm install
   - runBatch dispatches goroutines for batches > 1, single inline path
     unchanged. State is cloned per goroutine (CurrentIdx override) so
     prompts render correctly. Token / model usage updates flow through
     SQLite's serialised execWithRetry
   - Post-processing (UpdateSubTask final, accumulated append, GitHub
     sync, per-subtask comment) runs in original idx order after the
     batch — keeps event ordering deterministic in TG and on the issue
   - Write-capable sub-tasks (Edit/Write/Bash/...) keep their existing
     fully-sequential path — no file conflict surface to manage

Realistic speedup is workload-dependent: investigation-heavy plans with
several Read/Grep steps see ~1.5–2x; tight Edit→Bash→Edit chains see no
change because the batch resolves to a single index every iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/ghermes)

Implements Issue #106: support Codex CLI as a parallel AI backend alongside Claude.

- CodexClient wraps `codex exec --json`, normalizes JSONL events to CLIResponse
- MultiBackendClient dispatcher routes claude*/gpt*/o3*/o4* prefix-matched models
  to the right backend; returns hard error when required backend is unconfigured
- New `/gfast` `/gsmart` `/gdeep` commands mirror the Claude tier structure
- New `/ghermes` runs Hermes on the Codex tier (separate from `/hermes`)
- Hermes config gains CodexPlannerModel/CodexExecutorModel/CodexHeavyExecutorModel
- Reuses multimedia.openai_api_key as fallback when OPENAI_API_KEY unset
- Pricing table with longest-prefix match covers gpt-5.5/5.5-pro/5.4/5.3-codex
- CallPlan injects no-tool-execution prompt guard (codex has no --max-turns)
- Cross-backend session safety: GPT tier commands ClearSession() on switch
- i18n: zh-TW + en for tier-switch messages and codex-unavailable warnings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ream errors

Two issues uncovered while testing /ghermes:

1. gpt-5.5-pro is API-key-only — Codex CLI rejects it for any ChatGPT-account
   auth (Plus/Pro/Enterprise alike), regardless of subscription tier. Switch
   the default codex_deep_model to gpt-5.5, the strongest model available to
   ChatGPT-account Codex users.

2. CodexClient ignored "error" and "turn.failed" stream events, so when codex
   refused a model the response surfaced as empty text, leaving Hermes with
   "last output: " and no actionable message. Capture both events into
   streamErrMsg and treat them as fatal even when the codex process exits 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-Planner skip (simple-goal heuristic, e.g. "好") bypasses the LLM but the
coordinator still called AddModelUsage(PlannerModel, 0, 0), polluting the task
summary with phantom rows like "Opus: 1 次呼叫, 0 tokens".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- formatToolUpdate: handle codex's "command_execution" tool the same way as
  Claude's Bash, showing the actual command in the progress preview instead
  of the literal "🔧 command_execution" string.
- getModelTag: add labels for gpt-5.5 / gpt-5.5 Pro / gpt-5.4 / gpt-5.4 mini /
  gpt-5.3 codex, plus a generic gpt-/o3-/o4- fallback. Previously every codex
  model fell through to "🤖 [Default]".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add backfillDecisionLogsToUnified, idempotent batch (500/page) mirror.
  Skips when count(tasks where id LIKE 'decision:%') >= count(decision_logs).
- Wire into initTables right after migrateUnifiedTaskTables so historical
  Agent-path records show up in the unified Dashboard alongside new traffic.
- Remove dead hermes/stats_query.go (StatsQuerier had no callers and a
  stub TODO; UnifiedTaskStore already covers /hermes-stats use cases).

Code-review follow-up to the engine refactor batch (#110-#114, #119).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chimerakang and others added 6 commits May 9, 2026 13:48
Verification-only sub-tasks (work already done in a prior session,
nothing to diff) were getting trapped in the strict-review retry loop:

  1. Executor returned a "PARTIAL" prefixed result describing what was
     verified
  2. Reviewer signed off (verdict=pass, overall_score=85, feedback
     "appears satisfied"), added incomplete_traceability as a procedural
     caveat (no diff to inspect)
  3. ReviewDecisionFromStrictTags ignored the verdict + score, saw the
     tag in the default block list, demoted to verdict=block
  4. StrictReviewNode retried until budget exhausted, then marked the
     sub-task Skipped — even though the reviewer explicitly said it
     was satisfied
  5. IssueOps refused to tick the GitHub checklist because 0/N
     sub-tasks landed Done

Fix: ReviewDecisionFromStrictTags now treats matched tags as advisory
when the reviewer's holistic judgment is positive
(verdict == VerdictPass AND overall_score >= SubTaskScoreFailingThreshold,
i.e. 80). The tag list is still recorded on the decision for telemetry,
but does not flip the verdict to block.

Behavior preserved:
  - verdict=partial / fail still hard-blocks on matching tags
  - verdict=pass with low overall_score (LLM hedging) still hard-blocks
  - non-strict mode unchanged

Tests:
  - High-score pass + incomplete_traceability tag → allow (was block)
  - Low-score pass + missing_validation tag → still block
  - Verdict=partial + high score + scope_creep → still block

Refs #171.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three display fixes the operator was missing:

1. Drill-in shows what each sub-task actually did, not just hops
   /api/hermes/snapshots now includes latest_plan (per-sub-task
   description, status, result truncated to 4KB, attempts, tokens,
   reviewer retry feedback truncated to 1.5KB) and accumulated text
   (truncated to 8KB). The dashboard renders three tabs:
     - 子任務 — per-sub-task cards with status badge, description,
       result excerpt (expandable to full), retry feedback callout
       when present
     - Walker hops — the existing transition timeline
     - Accumulated — the final accumulated transcript
   This closes the gap where operators had to tail Telegram to see
   what each sub-task produced or why the strict reviewer blocked.

2. Long goals wrap instead of truncating
   Removed max-w-md + truncate from the row goal cell. Now uses
   whitespace-pre-wrap break-words so the full goal is visible,
   meta info wraps below. Both the Runtime page panel and the
   Hermes Tasks history page got the same treatment.

3. GitHub issue numbers are clickable links
   ActiveTaskRef gains a github_url field. The web handlers cache
   git remote get-url per project_dir per request via
   enrichTasksWithGithubURL so a 100-row list runs at most one
   git command per unique project. Frontend renders #N as an
   external link with ↗ marker; falls back to plain text when
   github_url is empty (no remote / lookup failure).

Also rebuilt the Docker dashboard image so the recently-added
"Hermes 任務" sidebar entry actually appears in the served bundle —
the previous frontend rebuild only updated web/ on disk; the
nginx container had baked the older bundle into its image.

Refs #171.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chimerakang and others added 23 commits May 10, 2026 01:31
Lifts the manual SQL effectiveness analysis (success rate, failure
reasons, source-node distribution, per-phase token averages, hop-count
distribution) into the dashboard so #171 / #173 bake observation
becomes self-serve instead of "ask claude to query the DB".

Backend:
  - GET /api/hermes/stats?days=N (default 14, capped 90) — read-only
  - SQL-aggregated: totals + per-day done/failed/interrupted, failure
    reasons (terminal commit metadata.reason on failed tasks),
    source_node distribution across all snapshots in window (proves
    graph nodes are firing — executor / reviewer / strict_review /
    planner / approval), per-phase token sums + averages from done
    tasks' final state_json phase_usages, per-task hop-count buckets

Frontend:
  - HermesStatsPanel mounted on top of HermesTasks page
  - 5 summary cards (total / done-failed / success-rate with
    color-coded threshold / interrupted / generated-at)
  - Stacked daily bar chart (done/failed/interrupted)
  - Horizontal source-node bar chart with per-node colors so the
    graph runtime's behaviour is at-a-glance verifiable
  - Failure-reasons table sorted by frequency
  - Phase token table (calls / avg-in / avg-out / sum-in)
  - Hop-distribution histogram showing the bimodal "quick exit" vs
    "normal flow" vs "lots of replans" pattern
  - Window selector (7d / 14d / 30d / 90d) + manual refresh

Refs #171 #173.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two display fixes:

1. i18n compliance — hermes_tasks namespace added to zh-TW.json + en.json
   covering page title/subtitle, status filter labels, drill-in tab
   labels, sub-task card section labels, panel interrupt UX strings,
   stats panel labels (cards / chart titles / tables / axis labels /
   tooltips). HermesStatsPanel, HermesTasksPanel and HermesTasks page
   now route every visible string through t(); the previous
   t(key, fallback) calls (which leaked the zh-TW string into en.json
   automatically when the key was missing) are removed in favour of
   explicit dual-language keys

2. Scrollable long-text blocks — drill-in result / retry-feedback /
   accumulated previews + the row-header goal block had no max-height
   so a single 50-line goal pushed the resolve buttons off-screen.
   Each prose block now caps at a viewport-relative max-height
   (40-60vh for results, 32-40 for goals) with overflow-y-auto so
   long content scrolls inside its own block instead of growing the
   page

Refs #171.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tive (#171)

Bug: clicking the "Interrupted" chip made every other chip — including
"Done" — read 0, because chip counts were computed from
\`tasks.reduce\` over the currently-loaded list. The list only contains
tasks matching the active filter, so non-matching buckets always saw
zero. The user reported "每次 done 的數字都會變動" — clicking around the
filter showed wildly different totals for the same underlying data.

Fix: chip counts come from /api/hermes/stats (independent of which
status filter is selected) instead of from the loaded subset. The
stats response was extended to break out \`executing\` and \`planning\`
buckets (previously folded into \`other\`) so all five non-"All" chips
now have their own canonical totals.

\`All\` chip uses \`stats.totals.total\` instead of \`tasks.length\` so it
also stays stable across filter changes.

Refresh button now reloads both the list and the counts; previously it
only reloaded the list.

Refs #171.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The placeholder "顯示 X / Y(捲頁尚未實作)" finally goes away. Replaces the
notice with a real "Load more" button that pulls the next page using
the existing /api/hermes/tasks ?offset= parameter (backend already
supported it, just no UI hooked up).

Behaviour:
  - PAGE_SIZE = 100 (matches the backend's default cap)
  - load(): first page, replaces task list. Triggered on filter change
    or manual refresh
  - loadMore(): appends next page using current task count as offset.
    Note: this gives a deterministic page boundary as long as no new
    tasks land between requests; for an admin-facing history view that
    trade-off is fine
  - Button hidden when tasks.length >= total (the whole result set is
    loaded)
  - i18n keys hermes_tasks.pagination_status + pagination_load_more
    added to zh-TW + en

Refs #171.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update Opus standard pricing to $5/$25 per MTok (was $15/$75, stale Opus 4.1
rate) and add inert data-layer plumbing for Opus Fast Mode (research preview):
opus_fast pricing tier, ModelPricing entry, Hermes planner/reviewer toggles,
and Summary CostRates registration. Runtime stays on standard tier — flip
will land once Claude Code CLI exposes a headless fast-mode flag. See #178.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opus 4.8 defaults the CLI --effort to "high" on every surface. In the
planning phase that makes the model over-reason and narrate a prose summary
("Plan emitted successfully…") instead of firing the emit_plan MCP tool, so
the JSON parse fails after all retries (observed on /hermes #374).

Planning only fills the emit_plan schema, so cap it via a configurable
HermesConfig.planner_effort (default "medium"), applied through a
process-wide SetPlannerEffort mirroring the SetForcePromptCaching5m pattern.
CallPlan appends --effort unless the level is "off"/"none" (inherit the
model's adaptive default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Planner relied on an MCP emit_plan tool: the model had to choose to fire
a tool_use block, which Alice then parsed. Opus 4.8 frequently narrates a
prose summary instead ("Plan emitted successfully…"/"I've decomposed #N
into…"), leaving the parser with no tool call and failing after retries —
reproduced live on /hermes #115 and #374.

Switch CallPlan to `--json-schema`, which constrains the output to a
{sub_tasks: [...]} object at decode time. The validated plan lands in the
result event's structured_output regardless of any prose the model also
emits. Verified end-to-end against the real #115 prompt: the model still
narrated prose, but the 9-task plan was captured every run.

- structured_output is now the primary plan source; the emit_plan tool_use
  path is kept as a secondary fallback.
- Drop the temp Python MCP server (writePlannerEmitPlanMCPConfig + script);
  planning stays hermetic via --strict-mcp-config with no --mcp-config and
  --tools "".
- Add TestCallPlanPrefersStructuredOutputOverProse; update the argv assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the --json-schema switch: the planner prompts still told the
model to "Call the emit_plan tool" / "Output ONLY the emit_plan tool call",
but --json-schema removed that tool (output now goes through StructuredOutput).
Instructing Opus 4.8 to call a nonexistent tool made it narrate "Plan
emitted successfully" instead of producing a plan — the exact failure still
seen on /hermes #115 and #374.

Two changes:

1. Tool-agnostic instructions everywhere the planner is prompted:
   - plannerSystemPrompt, planner_rules.md, and the embedded defaultPlannerRules
     now ask for the sub_tasks structured output and explicitly say a "Plan
     emitted" prose summary is NOT a plan.
2. Planner retries run fresh, not resumed:
   - A JSON-parse/quality/empty retry now re-sends the full self-contained
     prompt with NO --resume. Resuming the turn where 4.8 already narrated
     anchored it to the same prose and cascaded all retries to failure
     (attempt 2 "Plan emitted…", attempt 3 "No response requested.").
   - Retry feedback messages no longer reference the removed emit_plan tool.

Verified end-to-end against the real #115 prompt with production args:
6/6 runs produced a valid structured plan.

Tests: rewrite TestPlannerSessionResumesAcrossJSONRetries ->
TestPlannerSessionRunsFreshOnJSONRetry; update prompt_builder expectations
from emit_plan to structured output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alvage

Two remaining causes of "planner JSON parse failed" on Opus 4.8 with large
(resumed-task) prompts:

1. planner_rules.md told the model "你只負責規劃,不執行任何工具操作" (you only
   plan, do not perform ANY tool operations). --json-schema is implemented as
   a StructuredOutput *tool*, so that line actively suppressed the very call
   that emits the plan — the model obeyed by narrating prose instead. Reworded
   to forbid only file/command tools (Read/Edit/Bash) while requiring the
   sub_tasks structured output.

2. --json-schema is still an optional tool the model can skip, so no prompt can
   guarantee it on every large prompt. Added salvagePlanFromProse: when CallPlan
   captures neither structured output nor an emit_plan tool_use but did get a
   prose plan, it runs one focused low-effort --json-schema pass that restructures
   that prose into sub_tasks. Verified on the real #115 failure text ("I've
   emitted the re-plan … 12 sub-tasks …") → recovered all 12 tasks.

Net effect: the prompt fix makes narration rare; the salvage makes it harmless.
Reproduction at ~16KB resumed prompt: 8/8 captured a structured plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the salvage fallback added in 221ac2a. The failure mode is Claude/CLI
reporting it "emitted" structured output while the structured_output payload
never reaches the caller, so re-running the salvage through --json-schema can
hit the same dead end. Instead:

- extractPlannerJSONText first tries to pull a JSON array straight out of the
  narrated prose (no extra model call when the plan is already there as text).
- The conversion pass now asks for a LITERAL JSON array in the text response
  (no --json-schema), re-supplying the original planner request so required
  fields like checklist_item_ids are preserved.
- hermes_plan_bridge prefers resp.TextContent (which CallPlan rewrites with the
  structured/salvaged plan) over the earlier streamed prose, so the recovered
  plan actually reaches the Planner parser.

Adds hermes_plan_bridge_test plus planner/client coverage. Build + full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion

Replace the hardcoded score-70 cutoff in retry-target selection with the
shared appengine.SubTaskScoreFailingThreshold constant (80), so the retry UI
and the reviewer sign-off gate agree on what "failing" means. Add a NOT EXISTS
guard so a sub-task is selected from its newest review row only, and fall back
to that latest score instead of rejecting reviews that lack older sub-task
score rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ibility

Replace bufio.Scanner with bufio.Reader.ReadBytes in the codex client,
codex session watcher, and enhanced CLI client. Single stream-json/JSONL
lines can exceed Scanner's 4MB token cap, causing bufio.ErrTooLong: the
reader stopped draining stdout, the OS pipe filled, and the child process
blocked on write until timeout — freezing Hermes sub-tasks. The session
watcher additionally never advanced its offset past oversized lines, so it
re-scanned forever and grew alice.log past 450MB. ReadBytes has no per-line
size cap and lets the watcher skip partial trailing lines safely.

Surface real gh failure reasons: ghOutput now embeds a trimmed stderr
snippet in the error, add ghOutputWithRetry for transient blips, and route
not-found detection through isGHNotFoundErr. Runner logs a one-line stderr
snippet on command failure instead of a bare exit status.

Make telegram getUpdates 409 Conflict visible: a competing poller on the
same bot token was silently swallowed, making the bot look dead. Log the
error_code/description and back off 3s instead of tight-looping.

Stream JSON decode in ParseReviewResult so trailing prose after the JSON
object no longer breaks parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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.

Cut over plan_execute.Run() to RunViaGraph one task class at a time LangGraph migration: hand-coded edge cases in plan_execute.go to resolve

1 participant