Complete LangGraph migration epic (#169)#172
Open
chimerakang wants to merge 234 commits into
Open
Conversation
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>
Related to GitHub #119 completion
- 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>
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>
6 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
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.gobecause 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 logby 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
d4e328bAdd Hermes snapshot schema and runtime types2817e38Add Hermes state update reducersf1b2ea9Write Hermes runtime snapshots at step boundariesae5e30eslice 1: unify error classifiers behind engine/errorclass2473faeslice 2: persist Hermes failure-pause as durable interrupt + orphan sweep4fd7b37slice 3a + 3b: route all engine writes through CommitRuntimeStep1031f42slice 3c: read paths derive TaskState from latest snapshot69fdc51slice 3d: snapshot becomes sole authority for task statusβ phase — interrupt + resume
5d92856β1: cold-restart resume for Hermes failure pausee2284ffβ2: persist pause decision into snapshot for cold-restart resume57f932aβ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.Planb4508bfγ3a: minimal ExecutorNode for snapshot-driven sub-task dispatchc5d5591γ4: ApprovalNode + ExecutorNode failure-pause routingd850941γ3c: StrictReviewNode for per-sub-task block/retry routing9bff48dγ5: ReviewerNode for task-level review routingafde881γ6: Walker-driven RunViaGraph entry point + node adaptersHand-coded edge cases (issue body §1-7)
0112511📊 Performance Monitoring & Analytics #4: ReplanSetupNode + Replan-aware PlannerNode (partial vs full retry)4fae532📊 Performance Monitoring & Analytics #4 follow-up: wire replan loop into Walker via shared coordinatordc241ee🔍 AI Agent Transparency & Decision Logging #2: Walker panic recovery commits terminal snapshotb83aab8🎛️ Web Dashboard Integration #1, 💾 Data Persistence Layer (SQLite) #7: walking-agent state on the snapshotf5ec010🤖 Multi-Agent Coordination System #3: snapshot.Interrupt is the canonical pause signal in Walker1a273b2🚀 Deployment & DevOps Improvements #6: route telegram.classifyError through errorclass + document boundaryCut-over scaffolding (#171 partial)
8031e17Cut over plan_execute.Run() to RunViaGraph one task class at a time #171 Class A: gate direct-chat plan/execute on RunViaGraphResult (feature-flagged off)4cd63bcCut over plan_execute.Run() to RunViaGraph one task class at a time #171 Class B: StartViaGraph + graph-native Hermes pause resume (feature-flagged off)What this delivers
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.
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 throughStateUpdatereducers.All 7 hand-coded edge cases resolved — see commits above for individual decisions documented in each commit message.
Graph-side runtime is production-ready but cut-over is gated:
ALICE_DIRECT_PLAN_VIA_GRAPH=1→ Class A (direct chat) uses WalkerALICE_HERMES_START_VIA_GRAPH=1→ Class B (Hermes async) uses WalkerRun()/Start()are unchangedCut-over follow-up tracked in Cut over plan_execute.Run() to RunViaGraph one task class at a time #171.
Test plan
go build ./...cleango test ./...all packages greenPlanExecuteEngine.run()(separate PR after both bakes)🤖 Generated with Claude Code