Skip to content

feat(run): add hermit run — Superintendent loop owned by HERMIT's own process - #189

Merged
ytnobody merged 1 commit into
developfrom
hermit/ytnobody/issue-181
Jul 28, 2026
Merged

feat(run): add hermit run — Superintendent loop owned by HERMIT's own process#189
ytnobody merged 1 commit into
developfrom
hermit/ytnobody/issue-181

Conversation

@ytnobody

@ytnobody ytnobody commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #181. Adds hermit run, a long-lived process (the same shape hermit serve already is for the MCP server) that owns an internal ticker and drives the Superintendent cycle by launching claude -p non-interactively once per tick, waiting [agent].loop_interval after each pass finishes before starting the next one. This is the third design attempt at "the loop shouldn't require a human to keep a Claude Code session open forever":

# Approach Outcome
#147 Superintendent as a background subagent Exhausted the session's subagent-spawn cap over long unattended runs (#171)
#172 Inline in the invoking context, no subagent Current; stable but requires the session to stay open
#181 (this PR) Loop lives outside any Claude Code session, in HERMIT's own process

No subagent spawning is introduced anywhere in this design — hermit run never touches the Agent tool at all, so #171's failure mode structurally cannot recur.

What changed

  • internal/runloop (new): the tick loop itself.
    • Sequential by construction — each iteration blocks on Invoke before doing anything else, so two passes can never overlap even if a pass runs long.
    • Graceful shutdown: the shutdown context (wired to SIGINT/SIGTERM) is only ever consulted between passes, never passed into an in-flight Invoke call — a running pass always finishes; the loop stops only after that.
    • Detects .hermit-paused (skips the tick, loop keeps ticking) and .hermit-quit (stops the loop) itself, without relying on the invoked claude -p session's own logic.
    • Tracks consecutive failures and fires a webhook notification (internal/notification) once a configurable threshold is reached.
  • internal/state (new): owns .hermit/superintendent-state.json (atomic read-modify-write via temp file + rename). Both hermit run itself (last_success_tick, consecutive_failures) and the two new MCP tools below write through this package — nothing hand-writes the JSON file directly anymore.
  • internal/mcp: two new tools, get_loop_state and update_loop_state, so the Superintendent cycle reads/writes the three cadence timestamps (pr_comments_since, issue_comments_since, requirements_sweep_since) through an MCP tool instead of writing the JSON file itself.
  • cmd/hermit: the hermit run subcommand, a new [run].failure_notify_threshold config field (default 3), and the actual claude invocation — --dangerously-skip-permissions, optional --model, -p <CLAUDE.md contents> — matching the non-interactive pattern already documented in docs/github-actions.md.
  • README: an "Alternative to Step 2" section introducing hermit run, plus a new "Running HERMIT Continuously" section with systemd/launchd/Windows-service/tmux/Docker guidance. No OS-specific code was added anywhere — this is documentation only, per the Issue's explicit scope boundary.
  • REQUIREMENTS.md: adds REQ-019 for hermit run, and carves out a narrow, explicit exception in REQ-014's non-goals list (which otherwise forbids a Go binary launching Claude Code as a subprocess) — see the REQ-014 update for the reasoning.

Assumptions made (no chat clarification was requested, per HERMIT's Human Input Policy)

  • [run].failure_notify_threshold <= 0 falls back to the default (3), matching this codebase's existing convention for [agent].loop_interval / [agent].max_engineers (there's no way to explicitly disable notification via this field, same limitation those two already have).
  • hermit run reads CLAUDE.md fresh from the project root on every tick (not cached at startup), so edits to CLAUDE.md take effect on the very next tick without restarting hermit run.
  • hermit run refuses to start (fails fast, before entering the loop) if CLAUDE.md is missing from the project root, rather than looping forever invoking a claude -p that would immediately error every tick.
  • /hermit and hermit run are documented as coexisting alternatives, not a replacement — per the Issue's explicit "スコープ外" on retiring the slash command.

Test plan

  • go test ./... passes
  • GOOS=windows GOARCH=amd64 go build ./... succeeds
  • gofmt -l . / go vet ./... clean
  • New tests: internal/runloop/runloop_test.go, internal/state/state_test.go, internal/mcp/req_test.go (TestREQ019_*), cmd/hermit/run_test.go — cover no-overlap, graceful shutdown not interrupting an in-flight pass, .hermit-quit/.hermit-paused detection, state persistence, failure-threshold notification, non-interactive claude argument construction, and CLAUDE.md-missing fail-fast.
  • .claude/settings.json's permissions.allow updated with the two new MCP tools (TestAllRegisteredToolsAreAllowlisted guards this).

🤖 Generated with Claude Code

@ytnobody

Copy link
Copy Markdown
Owner Author

Superintendentによるレビュー(PR #189, Issue #181)

evaluate_risk の判定は HIGH(500行以上、かつ cmd/hermit/main.go が高リスクパス)。自動マージ対象外のため、人間の判断を待ちます。以下、実質的なレビュー内容です。

差分の内容確認

  • 設計方針の遵守: サブエージェント方式(hermitコマンドでのループをバックグラウンドで実行する #147)への回帰を避け、internal/runloop は単純な直列ticker実装。並行呼び出しの余地がない構造で「重複起動しない」受け入れ条件を満たしている
  • シャットダウン処理: shutdown contextはパス開始前とインターバル待機中のみ参照し、実行中のInvoke(=claude -p)には渡さない設計。実行中のパスを中断しないというIssueの受け入れ条件に合致
  • internal/state: .hermit/superintendent-state.jsonをatomicな読み書き(一時ファイル+rename)で所有。get_loop_state/update_loop_stateという新規MCPツール経由でのみアクセスされ、Superintendentサイクルがこのファイルを直接Write toolで書く運用(これまでこのセッションで私が行っていたやり方)から脱却する設計になっている
  • cmd/hermit/main.go: claude -p --dangerously-skip-permissionsでの非対話起動は、既存のdocs/github-actions.mdに記載のパターンを踏襲しており、無人運用の要求と整合する
  • Windows amd64向けGOOS=windows GOARCH=amd64 go buildでの検証をEngineerが実施済みと報告
  • OS固有コード(systemd/launchd/Windowsサービス)は追加されておらず、README案内のみに留まっている(スコープ外の遵守を確認)

特記事項: REQUIREMENTS.md REQ-014の書き換え

本PRは既存のREQ-014(「Go バイナリがClaude Codeをサブプロセスとして起動・管理する『外側から包む』アーキテクチャを行わない」)の非ゴールに、hermit run向けの明示的な例外を追記しています。これはIssue本文で「3回目の再設計」として明言されている設計判断であり、隠蔽せず変更理由・スコープをREQUIREMENTS.md自体に明記して透明化しています。妥当な対応と判断しますが、既存要件の非ゴールを緩める変更である点は人間の確認を推奨します。

.claude/settings.json の変更

新設したget_loop_state/update_loop_stateツールをpermissions.allowに追加しているのみで、既存の許可リストを狭める変更ではありません。

懸念点

推奨

設計判断・実装ともにIssueの要求と過去の失敗(#147, #171)を踏まえた妥当なものです。人間による承認後のマージを推奨します。マージ順序およびREQ-015番号衝突の解消(5件分)をご検討ください。

@ytnobody
ytnobody force-pushed the hermit/ytnobody/issue-181 branch 2 times, most recently from 8d4bbca to f328533 Compare July 28, 2026 02:59
… process (Closes #181)

Adds `hermit run`, a long-lived process (same shape as `hermit serve`)
that owns an internal ticker and launches `claude -p` non-interactively
once per tick, waiting `[agent].loop_interval` after each pass before
starting the next. This removes the requirement that a human keep a
Claude Code session open indefinitely to run /hermit — the third design
attempt at this problem (#147 background-subagent design failed per
but session-bound). No subagent spawning is introduced here at all.

- internal/runloop: the tick loop. Sequential by construction (no
  overlapping passes), graceful SIGINT/SIGTERM handling (an in-flight
  pass always finishes; shutdown is only checked between passes),
  .hermit-paused/.hermit-quit detection, and consecutive-failure
  webhook notification via internal/notification.
- internal/state: owns .hermit/superintendent-state.json (atomic
  read-modify-write). Both `hermit run` (last_success_tick,
  consecutive_failures) and the new get_loop_state/update_loop_state
  MCP tools (pr_comments_since, issue_comments_since,
  requirements_sweep_since) go through this package instead of the
  Superintendent hand-writing the JSON file.
- cmd/hermit: `hermit run` subcommand, [run].failure_notify_threshold
  config, and the claude invocation (--dangerously-skip-permissions,
  optional --model, -p <CLAUDE.md contents>) matching the pattern
  already documented in docs/github-actions.md.
- README: "Alternative to Step 2" and a new "Running HERMIT
  Continuously" section documenting systemd/launchd/Windows
  service/tmux/Docker daemonization — no OS-specific code is added,
  per the Issue's explicit scope boundary.
- REQUIREMENTS.md: adds REQ-015 and carves out an explicit, narrow
  exception in REQ-014's non-goals for this one case.

Verified GOOS=windows GOARCH=amd64 build and go test ./... pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ytnobody
ytnobody force-pushed the hermit/ytnobody/issue-181 branch from f328533 to cc4d750 Compare July 28, 2026 03:04
@ytnobody

Copy link
Copy Markdown
Owner Author

⚠️ HERMIT: HIGH risk detected.
Reasons: [500 or more lines changed cmd/hermit/main.go is in a high-risk path]

@ytnobody
ytnobody merged commit c91b30f into develop Jul 28, 2026
3 checks passed
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.

hermit run: ループを Claude Code セッションの外に出し、HERMIT 自身のプロセスが所有する

1 participant