Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
0323ec9
fix(tools): defensive ENOENT handling for deleted cwd in terminal/cod…
sahilm-ti May 25, 2026
4f22fc6
fix(kanban): stop self-deleting worker scratch workspace; defer to di…
sahilm-ti May 25, 2026
ac5d841
feat(kanban): add human_review status + review/approve/reject tools a…
sahilm-ti May 25, 2026
96b6050
kanban: pre-flight skill validation to prevent worker crash-loops (#7)
sahilm-ti May 25, 2026
9fe1605
fix(kanban-dashboard): add human_review column to BOARD_COLUMNS (#8)
sahilm-ti May 25, 2026
4bcb31e
fix(skills): resolve name collisions deterministically (local > exter…
sahilm-ti May 25, 2026
26989c1
infra(kanban): default workspace_kind=worktree for hermes-agent tasks…
sahilm-ti May 25, 2026
9233739
fix(setup): stop seeding bundled skills into per-profile dirs by defa…
sahilm-ti May 25, 2026
014d99f
fix(kanban): inject git identity on worker spawn + add sahil@nousrese…
sahilm-ti May 25, 2026
e27cb68
fix(tests): align cleanup-workspace tests with deferred-GC semantics
May 25, 2026
8810362
kanban-worker prompt: replace kanban_block(review-required:) with kan…
sahilm-ti May 25, 2026
4e1cbe9
design: per-workspace venv isolation for kanban worktrees (#18)
sahilm-ti May 25, 2026
74035f8
fix(auth): stop blocklisting GH_TOKEN / GITHUB_TOKEN (#12)
sahilm-ti May 25, 2026
5b252de
fix(kanban): reject_task accepts running-from-review + active_pr guar…
sahilm-ti May 25, 2026
02bb8a3
kanban: flip default worker git identity sahilm-ti -> sahilm-ai (#20)
sahilm-ai May 25, 2026
c4bac94
feat(kanban): per-assignee crash-loop circuit breaker + quarantine (#19)
sahilm-ti May 25, 2026
3a75453
fix(skills_sync): honor .no-bundled-skills marker for DESCRIPTION.md …
sahilm-ti May 26, 2026
b05fd41
fix(kanban): worker-spawned child tasks inherit parents' notification…
sahilm-ti May 26, 2026
5d2845d
feat(kanban): post-approve merger agent (#23)
sahilm-ti May 26, 2026
7fa102a
fix(skill): restore load-bearing negations in post-approve-merger SKI…
sahilm-ti May 26, 2026
d198086
fix(kanban): expand ~ in external_dirs against real user home, not $H…
sahilm-ti May 26, 2026
f3f09a7
feat(kanban): heartbeat-aware stuck-worker detection in dispatcher (#26)
sahilm-ti May 26, 2026
1adf776
feat(kanban): pin worker git identity at workspace init + pre-commit …
sahilm-ai May 27, 2026
a2a50e9
fix(kanban): extend active_pr respawn guard to honor `unblocked` even…
sahilm-ti May 28, 2026
b23f118
feat(memory): procedural-content gate + bypass flag + tests + skill d…
sahilm-ai May 28, 2026
beecb45
fix(gateway/telegram): eliminate duplicate row labels in pipe-table r…
sahilm-ti May 28, 2026
ba78838
fix(memory-gate): narrow SQL regex + fix vacuous test + harden bool c…
sahilm-ai May 28, 2026
a4fd2d6
feat(sdlc-review): regime-B completion audit — lint non-PR deliverabl…
sahilm-ai May 28, 2026
b9cac98
feat(gateway): auto-restart on code changes via CodeWatcher (#31)
sahilm-ti May 28, 2026
3cc98a2
test(memory): add bullet-shape bypass regression tests to test_memory…
sahilm-ti May 28, 2026
1a39f45
fix(gateway): propagate reply_to_message_id + reply_to_text through /…
sahilm-ti May 28, 2026
addcf80
feat(gateway): auto-pull ~/.hermes from origin/main via HermesHomePul…
sahilm-ai May 29, 2026
a3a25ce
fix(kanban): include merge_requested/reclaimed as active_pr resume si…
sahilm-ti May 29, 2026
097ce44
fix(worker-discipline): execute_code gate + heartbeat enforcement + m…
sahilm-ai May 29, 2026
12c2b59
fix(kanban): pin TERMINAL_CWD to workspace + warn on hermes-config br…
sahilm-ai May 29, 2026
491ba90
fix(memory_tool): add missing `import re` — NameError in _detect_proc…
sahilm-ai May 29, 2026
6265b3e
fix(test): prevent test_kanban_core_functionality self-SIGTERM via he…
sahilm-ai Jun 3, 2026
beac99e
fix(tests): make kanban dispatch tests hermetic + fix default_assigne…
sahilm-ai Jun 3, 2026
272e646
fix(mcp): heartbeat activity callback during long synchronous MCP/hiv…
sahilm-ti Jun 3, 2026
078d08e
fix(gateway): alert operator when hermes-home auto-pull is wedged by …
sahilm-ti Jun 3, 2026
281de20
fix(update): track fork + merge upstream in, never hard-reset to orig…
sahilm-ai Jun 9, 2026
326a450
fix(gateway): launchd domain detection (gui vs user) — restore superv…
sahilm-ai Jun 9, 2026
ba2a0eb
test(rebase-fallout): repair red test (1) shard after fork-sync (#1, …
sahilm-ai Jun 15, 2026
4f39b96
fix(banner): count "commits behind" against the fork (myfork/main), n…
sahilm-ai Jun 15, 2026
d627867
feat(kanban): add `merging` status + `skip_merge` flag to the approve…
sahilm-ti Jun 18, 2026
8e86a64
fix(kanban): propagate stuck_after_seconds_default + crash_breaker pa…
sahilm-ai Jun 22, 2026
33d2113
test(rebase-fallout): fix 3 test failures introduced by upstream changes
sahilm-ai Jun 22, 2026
fd5eac7
feat(kanban): add kanban.disable_pr_respawn_guard config knob
sahilm-ai Jun 25, 2026
46c0e5e
fix(test): pin launchd domain in refresh test to survive Aqua CI runners
sahilm-ai Jun 25, 2026
7be385c
fix(tools): support CPython 3.14 WorkerContext in DaemonThreadPoolExe…
sahilm-ai Jul 3, 2026
bf20cb5
fix(kanban): let PR tasks respawn for review iteration
sahilm-ai Jul 8, 2026
75734d3
fix(kanban): reject unresolved worktree tasks at create time
sahilm-ai Jul 9, 2026
887c5ef
feat(kanban): expose archive to orchestrators
sahilm-ai Jul 13, 2026
d9c55dd
feat(kanban): expose archive to orchestrators (#56)
sahilm-ai Jul 13, 2026
6fe4800
fix(kanban): bind merger to current review handoff
sahilm-ai Jul 13, 2026
b86174b
test(kanban): run artifact cleanup through reaper
sahilm-ai Jul 13, 2026
fb35312
test(kanban): cover review-claimed rejection tool path (#60)
sahilm-ai Jul 15, 2026
bec68fc
fix(kanban): restore completion audit dispatch (#61)
sahilm-ai Jul 15, 2026
dfd8869
fix(gitignore): ignore worker identity hooks (#62)
sahilm-ai Jul 17, 2026
612c40c
fix(kanban): recover missed goal finalizers (#63)
sahilm-ai Jul 21, 2026
43b16b3
test: repair platform-specific suite assumptions
sahilm-ai Jul 22, 2026
d175339
test(gateway): normalize macOS temp media paths
sahilm-ai Jul 22, 2026
20e2249
fix: stabilize environment snapshots and notifier migration
sahilm-ai Jul 23, 2026
12b35c9
fix: claim legacy notifier subscriptions at startup
sahilm-ai Jul 23, 2026
e1cb294
test: relax flaky stream timeout assertion
sahilm-ai Jul 23, 2026
12fb9d5
test: stabilize gateway cleanup event-loop regression
sahilm-ai Jul 23, 2026
d84ce4f
fix: preserve blocked kanban cards from auto decomposition
sahilm-ai Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ models-dev-upstream/
.codex/
.cursor/
.gemini/
# Generated local Kanban worker Git-identity guard.
.hermes-hooks/
.zed/
.mcp.json
opencode.json
Expand Down
336 changes: 336 additions & 0 deletions DESIGN.md

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,13 +926,17 @@ def _read_claude_code_credentials_from_keychain() -> Optional[Dict[str, Any]]:
logger.debug("Keychain: no entry found for 'Claude Code-credentials'")
return None

raw = result.stdout.strip()
raw = result.stdout
if not isinstance(raw, str):
logger.debug("Keychain: credentials payload was non-text (%s)", type(raw).__name__)
return None
raw = raw.strip()
if not raw:
return None

try:
data = json.loads(raw)
except json.JSONDecodeError:
except (json.JSONDecodeError, TypeError):
logger.debug("Keychain: credentials payload is not valid JSON")
return None

Expand Down
35 changes: 23 additions & 12 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,19 @@ def _strip_yaml_frontmatter(content: str) -> str:
"files outside it unless the task explicitly asks.\n"
"3. **Heartbeat on long operations.** Call `kanban_heartbeat(note=...)` "
"every few minutes during long subprocesses (training, encoding, crawling). "
"Skip heartbeats for short tasks. **If your task may run longer than 1 hour, "
"you MUST call `kanban_heartbeat` at least once an hour** — the dispatcher "
"reclaims tasks running past `kanban.dispatch_stale_timeout_seconds` "
"(default 4 hours) when no heartbeat has arrived in the last hour. A "
"reclaim re-queues the task as `ready` without penalty (no failure counter "
"tick), but you lose your current run's progress.\n"
"Skip heartbeats for short tasks. Heartbeat cadence has TWO distinct windows:\n"
" - **Initial window (no heartbeat yet on this run):** you have up to "
"`kanban.dispatch_stale_timeout_seconds` (default 4 hours) before the "
"stale-claim reaper reclaims the task — but send your first heartbeat as "
"soon as the work is meaningfully underway, not at the last minute.\n"
" - **Steady state (after your first heartbeat on this run):** you MUST "
"call `kanban_heartbeat` at least every `kanban.stuck_after_seconds_default` "
"(default 15 min). A worker that has heartbeated at least once but then goes "
"silent for >`kanban.stuck_after_seconds_default` is killed and re-queued "
"without a failure penalty, but progress since the last heartbeat is lost. "
"If your task legitimately needs longer between heartbeats (e.g. a training "
"epoch), set `metadata.stuck_after_seconds` on the task or mark it "
"`metadata.no_heartbeat_required=true` to opt out of stuck detection.\n"
"4. **Block on genuine ambiguity.** If you need a human decision you cannot "
"infer (missing credentials, UX choice, paywalled source, peer output you "
"need first), call `kanban_block(reason=\"...\")` and stop. Don't guess. "
Expand All @@ -233,13 +240,17 @@ def _strip_yaml_frontmatter(content: str) -> str:
"workers read both via their own `kanban_show`. Never put secrets / "
"tokens / raw PII in either field — run rows are durable forever. "
"Exception: if your output is a code change that needs human review "
"before counting as merged/done (most coding tasks), drop the "
"structured metadata (changed_files / tests_run / diff_path) into a "
"before counting as merged/done (most coding tasks), open a PR, drop "
"the structured metadata (changed_files / tests_run / pr_url) into a "
"`kanban_comment` first, then end with "
"`kanban_block(reason=\"review-required: <one-line summary>\")` so a "
"reviewer can approve+unblock or request changes. Reviewing-then-"
"completing is more honest than auto-completing work that still needs "
"eyes on it.\n"
"`kanban_review(reason=\"PR <url>, AC: <one-line>\")`. That hands "
"the card to the `sdlc-review` auto-reviewer, which runs mechanical "
"checks and on a clean pass promotes to `human_review` for Sahil to "
"merge and approve (the auto-reviewer does NOT merge — Sahil merges "
"every PR himself). The old `kanban_block(reason=\"review-required: "
"...\")` pattern is forbidden — use `kanban_review` for PR handoffs and "
"reserve `kanban_block` for genuine blockers (missing creds, paywalled "
"source, needs-smoke-test, etc.).\n"
"6. **If follow-up work appears, create it; don't do it.** Use "
"`kanban_create(title=..., assignee=<right-profile>, parents=[your-task-id])` "
"to spawn a child task for the appropriate specialist profile instead of "
Expand Down
57 changes: 56 additions & 1 deletion cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16020,7 +16020,7 @@ def _block(reason: str) -> None:
except Exception:
pass

_run_loop(
loop_result = _run_loop(
task_id=task_id,
goal_text=goal_text,
run_turn=_run_turn,
Expand All @@ -16030,6 +16030,61 @@ def _block(reason: str) -> None:
first_response=first_response or "",
log=lambda m: logger.info("%s", m),
)
if not isinstance(loop_result, dict):
return
if loop_result.get("outcome") != "finalization_recovery":
return

recovery_contract = str(loop_result.get("recovery_contract") or "").strip()
recovery_reason = str(loop_result.get("reason") or "").strip()
run_id_raw = (_os.environ.get("HERMES_KANBAN_RUN_ID") or "").strip()
expected_run_id = int(run_id_raw) if run_id_raw.isdigit() else None
c = _kb.connect()
try:
saved = _kb.record_finalization_recovery(
c,
task_id,
reason=recovery_reason,
contract=recovery_contract,
expected_run_id=expected_run_id,
)
if saved:
logger.info(
"kanban goal loop: recorded finalization recovery contract for "
"task %s (run_id=%s)",
task_id,
expected_run_id,
)
return
task_after = _kb.get_task(c, task_id)
if task_after is not None and task_after.status in {
"done",
"blocked",
"review",
"human_review",
"triage",
"todo",
"scheduled",
}:
logger.info(
"kanban goal loop: finalization recovery contract became a "
"no-op for task %s (already in %s)",
task_id,
task_after.status,
)
else:
logger.info(
"kanban goal loop: skipped finalization recovery contract for "
"task %s due to ownership/state drift (status=%s, run_id=%s)",
task_id,
task_after.status if task_after is not None else None,
expected_run_id,
)
finally:
try:
c.close()
except Exception:
pass


def main(
Expand Down
Loading
Loading