Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ Follow the same constraints as `CLAUDE.md`. The short version:
- Update `docs/internal/progress.txt` when finishing implementation steps.
- Update `docs/internal/lessons.md` when a mistake reveals a durable rule.

## Phase 16 Commands (Session Observability & Loop Control)

- `bach sessions list [--project] [--runtime] [--state] [--json]` — discover running/idle/ended sessions across all runtimes
- `bach sessions adopt <session> <task> [--force]` — link a discovered session ID to a Bach task artifact
- `bach sessions resume <session>` — resume a session in iTerm (prints fallback command if iTerm unavailable)
- `bach sessions watch <task-or-session>` — run the foreground session observer (polls transcript, calls judge, applies status via observer authority)
- `bach hooks install claude-code [--project-dir PATH]` — idempotent Claude Code hook install (SessionEnd + Stop + PostToolUse → `bach internal hook-event` / `bach internal session-stop`)
- `bach hooks install codex [--codex-home PATH]` — idempotent Codex hook install (sessionStop → `bach internal hook-event`)
- `bach hooks status [--project-dir PATH]` — show which runtimes have Bach hooks installed
- `bach afk run <task>` — headless multi-turn task runner; claims `afk_queue` → `executing`, drives turns, never sets `done`

<!-- gitnexus:start -->
# GitNexus — Code Intelligence

Expand Down
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ Bach is a CLI-first personal agentic development session launcher that turns dai
- `uv run bach task add` - Create a Mode 1 task artifact
- `uv run bach task launch <artifact>` - Launch a task session

### Phase 16 — Session Observability & Loop Control
- `uv run bach sessions list [--project] [--runtime] [--state] [--json]` - List discovered runtime sessions joined with Bach tasks
- `uv run bach sessions adopt <session> <task> [--force]` - Link a discovered session to a task artifact
- `uv run bach sessions resume <session>` - Resume a session via iTerm (fallback command printed on failure)
- `uv run bach sessions watch <task-or-session>` - Run the foreground session watcher (observer mode)
- `uv run bach hooks install claude-code [--project-dir PATH]` - Idempotent install of Claude Code hooks (SessionEnd/Stop/PostToolUse)
- `uv run bach hooks install codex [--codex-home PATH]` - Idempotent install of Codex hooks (sessionStop)
- `uv run bach hooks status [--project-dir PATH]` - Report which runtimes have Bach hooks installed
- `uv run bach afk run <task>` - Run a task headlessly (AFK mode) from `afk_queue`

## File References
- `docs/internal/DISCOVERY.md` - real goal and scope
- `docs/internal/PRD.md` - requirements
Expand Down
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,78 @@ every prompt; drag a folder from Finder into any path prompt.
event auto-sets the task status to `paused` and appends an audit log entry.
- **Fallback safety** — if `osascript` fails, Bach prints the exact shell
command to run manually.
- **Session radar & observer** — discover live/idle/ended agent sessions,
link them to tasks, and let the observer automatically suggest or apply
status transitions based on transcript analysis (see below).
- **AFK loop control** — headless multi-turn task runner with loop-gate
verification and judge-driven status management.

---

## Session radar & observer

Bach can discover running and ended Claude Code and Codex sessions across
your machine, join them with your task artifacts, and let an observer
process apply status transitions automatically.

```bash
# See all current sessions with task linkage
bach sessions list

# Filter by state
bach sessions list --state running

# Link an orphan session to a task artifact
bach sessions adopt <session-prefix> t47

# Resume a session in iTerm
bach sessions resume <session-prefix>

# Run the foreground observer for a task
bach sessions watch t47
```

Example output from `bach sessions list`:

| Session ID | Runtime | State | Task | Project | Preview |
|--------------|--------------|---------|------|---------|--------------------------|
| `a3f9c2e1...` | claude-code | running | t47 | myapp | "refactoring the auth…" |
| `b81d04f7...` | codex | idle | t12 | backend | "added retry logic" |
| `cc4e9101...` | claude-code | ended ||||

### Observer authority

The observer watches a session transcript, calls the LLM judge on a
schedule, and may apply status transitions with **conservative authority**:

| Allowed move | Trigger |
|-----------------------|---------------------------------------------|
| `executing → qa` | Judge confidence ≥ threshold, task complete |
| `* → hitl_queue` | Judge says `needs_human=true` |

The observer **never** sets `done`, never moves backward, and never
touches pre-execution stages (`inbox`, `prd`, `kanban_issues`).
All observer actions are appended to the artifact log.

Set `observer_moves: false` in `~/.bach/config.yaml` to make the
observer log-only (suggestions only, no writes).

### Hook installation

Wire up Bach hooks for automatic session tracking:

```bash
# Claude Code: installs SessionEnd + Stop + PostToolUse hooks
bach hooks install claude-code

# Codex: installs sessionStop hook in ~/.codex/hooks.json
bach hooks install codex

# Check what's installed
bach hooks status
```

See `docs/how-to/wire-up-session-hook.md` for full setup instructions.

---

Expand Down
143 changes: 143 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,146 @@ Reference: `reference/stable-task-ids.md`.
- **Phase 11** — complete (Pocock kanban `/board` view; sticky third view alongside today/all; tN-only refs in board; oldest-first sort; new `src/bach/repl/board.py` module). ADR-009. 322 tests.
- **Phase 12** — in progress. macOS TCC EPERM robustness in the daily-index walker (`_skipped_projects` tracker, `_record_skip`, `get_skipped_projects`). New `/workflow` REPL command (Pocock cheatsheet + Obsidian deep-link). Stub `_format_skip_banner` pending UX implementation. ADR-010. 354 tests.
- **Phase 13** — in progress. Two new pre-PRD workflow stages: `research` (read/gather) + `prototype` (throwaway spike). Wired across the full stack: `VALID_STATUSES` + enum validator, `SUGGESTED_NEXT_STEPS`, `ThemePalette` color fields (7 themes), board columns (7 → 9 WIP), narrow-terminal threshold (90 → 116), `_count_statuses` canonical order, `/research` + `/prototype` REPL handlers, `WORKFLOW_STAGES` (9 → 11). ADR-011. 354 tests.
- **Phase 16** — complete (session observability + loop control). ADR-015, ADR-016. 812+ tests. See below for component architecture.

## Phase 16 — Session Observability and Loop Control Components

Phase 16 introduces a passive observation plane and an active continuation
plane that operate alongside the existing hook-based tracking from Phase 8.
The two planes share the `status_service` write path but are subject to
different authority rules.

### Observation plane

```text
Claude/Codex session transcript (.jsonl)
-> runtimes/session_discovery.py discover_sessions()
-> DiscoveredSession (runtime, session_id, transcript_path,
project_dir, liveness, last_activity)

-> runtimes/transcript.py follow_events() / read_tail_events() / parse_line()
-> SessionEvent (kind, timestamp, text[truncated], raw_kind)
-> Never raises; unrecognized records → kind=unknown

-> services/judge_service.py judge_session()
-> Bounded prompt window (last 50 events, ≤ 8 000 chars)
-> Fixed JSON schema: {summary, suggested_status, needs_human, confidence}
-> Returns JudgeVerdict | None (None = no-op)

-> services/session_watcher.py watch_session()
-> Foreground run-and-exit loop (no threads, no asyncio)
-> follow_events + drain spool on each poll cycle
-> Judge fires ≤ once per judge_interval_seconds, always on stop/end
-> Applies verdict via set_task_status(source="observer")
-> Exits when end event seen OR stop spool event received
```

The observer writes status via `source="observer"`, which is subject to
a hard transition matrix in `status_service.set_task_status`:

```
executing → qa (hand-off)
{executing, qa, afk_queue} → hitl_queue (escalation)
```

Any other observer-source write raises `InvalidStatusError`.

### IPC: event spool

Hook commands are fast-exit subprocesses; the session watcher is a
long-running foreground process. They communicate via a per-session JSONL
spool:

```text
~/.bach/sessions/<session_id>.events.jsonl

bach internal hook-event → append_hook_event(session_id, payload)
bach internal session-stop → spool stop event; evaluate_stop() handles gate
session_watcher poll cycle → read_spool_events(session_id, from_offset=N)
```

The spool is write-once append; the watcher tracks a byte offset to avoid
re-reading events.

### Session discovery

`runtimes/session_discovery.py` is the ONLY module that knows on-disk
session layout for both runtimes:

- **Claude Code:** `~/.claude/projects/<slug>/<uuid>.jsonl`
Slug is the project absolute path with `/` → `-`.
- **Codex:** `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl`
Enriched from `~/.codex/session_index.jsonl` when present.

Liveness is classified as `running` / `idle` / `ended` by comparing
the transcript's mtime against injected thresholds (from `BachConfig`).
A `process_probe` callable (default: `ps -axo pid,command`) is injected
so tests are fully hermetic.

`services/sessions_service.py` joins `DiscoveredSession` records with
Bach task artifacts (`SessionRow`) and exposes `list_sessions`,
`adopt_session`, `resume_session`.

### Continuation plane

```text
claude internal hook Stop
-> bach internal session-stop (reads JSON from stdin)
-> loop_gate.evaluate_stop(session_id, sessions_dir, store, …)
-> sidecar lookup → artifact frontmatter
-> loop_gate.enabled check (opt-in — default: never gate)
-> Mode 1 check: post_grill.readiness + next_action
-> cap check: continuation_count >= max_continuations → hitl_queue
-> run verify_command (shell, cwd=project, timeout)
-> pass → exit 0; fail → print block message to stderr + exit 2

bach afk run <task>
-> cli/afk_cmd.py
-> afk_runner.run_afk_task(artifact_path, …)
-> claim: afk_queue → executing, source="afk"
-> turn loop (≤ afk_max_turns, ≤ afk_time_budget_minutes):
run turn → gate? → qa + stop
judge needs_human? → hitl_queue + stop
else → continue
-> bounds exhausted → hitl_queue
-> NEVER sets done
```

The AFK runner uses `source="afk"` (human-authority path in
`status_service`) but self-restricts to `{executing, qa, hitl_queue}`
via a code-layer guard inside `afk_runner._set_status`.

### New CLI surfaces (Phase 16)

| Command | File | Description |
|---|---|---|
| `bach sessions list` | `cli/sessions.py` | Table of live/recent sessions |
| `bach sessions adopt <session> <task>` | `cli/sessions.py` | Link session to artifact |
| `bach sessions resume <session>` | `cli/sessions.py` | Re-enter a session |
| `bach sessions watch <task>` | `cli/sessions.py` | Foreground watcher |
| `bach hooks install <runtime>` | `cli/hooks_cmd.py` | Idempotent hook installer |
| `bach hooks status` | `cli/hooks_cmd.py` | Show installed hooks |
| `bach internal hook-event` | `cli/internal_sessions.py` | Fast spool append |
| `bach internal session-watch <artifact>` | `cli/internal_sessions.py` | Launch watcher |
| `bach internal session-stop` | `cli/internal_sessions.py` | Gate check for Stop hook |
| `bach afk run <task>` | `cli/afk_cmd.py` | Headless multi-turn driver |

### New config fields (Phase 16)

All fields live in `~/.bach/config.yaml` and are loaded by
`config/settings.py` with lenient coercion (invalid value → WARNING +
default):

| Field | Default | Description |
|---|---|---|
| `watch_on_launch` | `false` | Spawn watcher automatically on task launch |
| `observer_moves` | `true` | `false` → judge is log-only, never writes status |
| `judge_confidence_threshold` | `0.7` | Minimum confidence for observer status writes |
| `judge_interval_seconds` | `300` | Min seconds between mid-session judge calls |
| `liveness_running_seconds` | `60` | Transcript mtime age threshold for "running" |
| `liveness_idle_minutes` | `5` | Process-alive + stale threshold for "idle" |
| `afk_max_turns` | `10` | Hard turn cap for AFK runner |
| `afk_time_budget_minutes` | `60` | Hard time cap for AFK runner |

How-to: `how-to/wire-up-session-hook.md`. ADRs: `adr/015-session-observability-codex-parity.md`,
`adr/016-loop-control.md`.
Loading
Loading