feat: per-change-block hunk actions (#50), codex lanes without a pipeline, macOS LaunchAgent

- git (#50): stage-hunk/discard-hunk take an optional `block` — one run of
  consecutive +/- lines inside an @@ hunk. `onlyBlock` (server/lib/git-diff.js)
  cuts git's fresh hunk down to it, folding the other blocks into the side
  being checked; LaneDiffView hangs one review bar per block (unified + split)
  and its arrows walk blocks.
- git exclude: entries AgentHub writes for a lane go to the COMMON git dir with
  the lane's path prefix (server/lib/git-exclude.js, used by router-apply and
  lane-mcp) — a linked worktree's own info/exclude is never read by git, and a
  subdir lane has no .git. MCP sync no longer fails with ENOTDIR on a worktree
  lane or creates a stray .git in a non-repo lane.
- codex lanes carry no pipeline: codex-ingest mirrors only idle/running,
  codex-nudge is removed, agents install writes nothing for codex, the
  Workspace hides stage/pipeline/feature pickers/PipelineMap for them, and
  every server start clears leftover codex stage data and the old AGENTS.md
  stage block.
- Changes panel: a whole file opens for reading and enters the editor on a
  click on its text (caret at the clicked line), not automatically.
- macOS: LaunchAgent start-at-login (scripts/launchd-service.js, picked by
  scripts/service-manager.js).
- Adds the AgentHub introduction slides and their image-embedding script.
This commit is contained in:
2026-09-25 17:30:28 +07:00
parent 3f6e903c22
commit 952e8584f0
48 changed files with 3336 additions and 1579 deletions
+47 -116
View File
@@ -96,7 +96,7 @@ Both modes' path field has a **Browse** button next to it, opening a small folde
A lane is driven by a tool: **Claude** (default) or **Codex**, chosen in the "+ Add lane" modal and immutable once the lane exists. A lane's tool determines which CLI is spawned when a run starts.
- **`claude` (default)** — `claude` binary is invoked; the session's hooks, messages, and tool calls flow through the dashboard. Claude lanes render Sessions, Analytics, cost, and stage-detection badges; liveness is tracked by the Claude Code session.
- **`codex` (experimental)** — `codex` binary is invoked instead. The Run Setup panel shows fields for model, sandbox, and approval mode instead of the Claude session picker, and offers "Resume last Codex run" instead of picking a session. Resume uses `codex resume --last`. Codex lanes render stage-detection badges too, fed from Codex's own rollout journals rather than hooks (see "Stage detection"), but no Sessions/Analytics/cost. (The console pre-fills nothing into the prompt box, for either tool.)
- **`codex` (experimental)** — `codex` binary is invoked instead. The Run Setup panel shows fields for model, sandbox, and approval mode instead of the Claude session picker, and offers "Resume last Codex run" instead of picking a session. Resume uses `codex resume --last`. Codex lanes carry **no pipeline**: no stage chip, no time-on-stage, no pipeline or feature picker and no PipelineMap, and nothing infers or advances their stage. They also get no Sessions/Analytics/cost. (The console pre-fills nothing into the prompt box, for either tool.)
### How Codex arguments map
@@ -116,39 +116,17 @@ Codex does not emit Claude Code hooks (sessions, agent events, tool-execution tr
- **Sessions** and **Analytics** tabs — no session history or aggregated statistics
- **Cost** surface — no per-run or per-model pricing data
**These are not bugs; they reflect Codex's architecture.** The stage you declare via `agenthub stage` (or that the driving agent calls itself) still works; liveness is tracked by the lane's tmux pane (if the stack is up) and `dashboard_runs` table (recording every invocation). Cost tracking for Codex runs would require parsing Codex's own logs, a separate design not yet implemented.
**These are not bugs; they reflect Codex's architecture.** Liveness is tracked by the lane's tmux pane (if the stack is up) and `dashboard_runs` table (recording every invocation). Cost tracking for Codex runs would require parsing Codex's own logs, a separate design not yet implemented.
**Stage detection is not in that list.** It does work for a codex lane, from
Codex's own on-disk rollout journals rather than from hooks, and unlike a claude
lane a written detection there also **advances the lane's declared `stage`** —
see "Stage detection" below. What a codex lane cannot get for free is
*evidence*: an auto-advanced node renders `passed-no-evidence` (amber), never
`done`, because only the session itself can say what it actually finished. The
run console never pre-fills a Claude slash command into a codex pane (Codex
would read `/agenthub:ship-feature-lane` as prose), so a codex session declares
evidence only when something tells it to. Three things do:
- **An opening prompt checklist.** The Run Setup panel pre-fills a codex pane's
first prompt with a plain-text list of `agenthub stage <stage> --tool codex`
calls to make, one per node of the lane's own pipeline template (built from
`Lane.pipeline_nodes`, so it always matches whatever template is in use) —
the same up-front priming a Claude lane gets from its pre-filled
`/agenthub:ship-feature-lane` skill command, just as plain text since Codex has
no slash commands. See `LaneConsolePane.tsx`'s `promptPrefill`.
- **An `AGENTS.md` block.** `agenthub lanes agents install` on a codex lane writes
a marker-delimited `<!-- agenthub:stage:start -->` section into
`<lane cwd>/AGENTS.md` — the one instruction channel Codex actually reads,
since it never loads `.claude/agents/`. Appended, never overwriting your own
prose, and a reinstall replaces exactly that block. (On a `claude` lane the
same command still copies the two agent templates instead.)
- **A missing-evidence nudge.** `server/lib/codex-nudge.js` sweeps every 60s
(`DASHBOARD_CODEX_NUDGE_MS` to change, `0` to disable) for a codex lane whose
current stage has been held for at least two minutes with no evidence
recorded, and types a one-line `[agenthub] ... run: agenthub stage ... --evidence
"..."` reminder straight into that lane's live tmux pane (`tmux.sendText`,
skipped if the pane is at a bare shell prompt). It never writes `stage`,
`stages` or `detected_stage` itself — the evidence that results is a real
declaration the session made.
**Codex lanes carry no pipeline.** Nothing infers their stage, nothing types
a stage reminder into their pane, and `agenthub lanes agents install` installs
nothing on them (`installed: []`). The one thing AgentHub reads from Codex is
its running/idle status — see "Stage detection" for the journal poll that
mirrors it. Every server start also wipes what older builds left behind on a
codex lane — its stage columns, stage log (`lane_stage_events`) and feature
records (`lane_features`) — and takes the old
`<!-- agenthub:stage:start -->` block back out of its `AGENTS.md` (deleting
the file if nothing else is in it).
## Destructive lane actions
@@ -652,13 +630,18 @@ only one that answers "show me the code" rather than "show me the change"):
one green with `U` (`A` added, `D` deleted and struck through, `R`
renamed), and a folder holding any of them gets an amber dot. The colours
come with the tree read, so **Refresh** re-reads them.
Opening a file entry shows the whole file (`/git/file`). Its **Edit** button
(any range but a commit) turns that view into a text editor: **Save** (or
Opening a file entry shows the whole file (`/git/file`) for reading, at every
panel width. **Clicking its text** (or Enter on it; any range but a commit)
turns that view into a text editor with the caret on the clicked line and
the scroll position kept — a click that only ends a drag-selection does not,
so copying never drops you into the editor. **Cancel** returns to reading.
From a diff, which has no whole text to click, an **Edit** button does the
same once the file has loaded. **Save** (or
Ctrl/Cmd+S) is `PUT /api/lanes/:id/git/file`, which writes only over the
exact version that was opened — a file the agent changed meanwhile answers
`409 ESTALE` and the draft stays for a retry — and keeps the file's CRLF
line endings and final newline. A file the editor cannot take (binary, or
over the 1 MB view cap) says so where the Edit button would be.
over the 1 MB view cap) says so in the file's header.
- **Overview** — every changed file with its `+N −M`. Polls every 15s, because
it is the cheap shape and the one you leave open while an agent works. Each
row carries its own **stage** / **unstage** / **discard** buttons.
@@ -670,14 +653,18 @@ only one that answers "show me the code" rather than "show me the change"):
a file that appears later is open by default; a lane switch clears it.
Each file's header stays **pinned** at the top while its diff scrolls past,
and carries the same stage / unstage / discard buttons as its overview row.
On a file with unstaged changes every hunk gets an editor-style **review
bar**, hung just under its last changed line and pinned to the visible
right edge: `^`/`v` to walk the hunks (each brings the next/previous `@@`
line to the top of the ONE scroller, below the sticky file header — a
On a file with unstaged changes every **change block** — each run of
consecutive `+`/`-` lines, not each `@@` hunk: git merges edits up to six
lines apart into one hunk, and one bar for all of them acted on every edit
at once (issue #50) — gets an editor-style **review bar**, hung just under
its last changed line and pinned to the visible right edge: `^`/`v` to walk
the blocks (each brings the next/previous block's first line to the top of
the ONE scroller, below the sticky file header — a
review diff has no scroll box of its own unless it is large enough to
window, above 500 rows), `n of N`, **Undo**
(`discard-hunk` — asks twice, like discard) and **Keep** (`stage-hunk`).
The single-file view offers the same bar.
(`discard-hunk` — asks twice, like discard) and **Keep** (`stage-hunk`),
both sent with the hunk's `@@` line plus the block's index, so they touch
that block alone. The single-file view offers the same bar.
- **One file** — clicking a file's path in any list opens just that file.
**All files** returns to the list it was opened from — the same mode, at the
same scroll offset — because reading a change-set is a walk down a long list
@@ -857,7 +844,7 @@ one feature's work reads `implement → tests → implement → tests` as the se
edits, runs the suite, and edits again — and when any backwards move opened a
run, a single feature was recorded as five or six of them. A real pass ends
where the session says it ends: a backwards move only starts a new run when it
is declared (`agenthub stage`, or a codex lane's ingest-driven declaration). A
is declared (`agenthub stage`). A
detection that moves backwards continues the run in flight. Reaching the last
node still closes the run for both — work after `done` is new work by anyone's
reading. On this repo's own lane the rule took a 26-run history down to 5, which
@@ -1164,72 +1151,23 @@ also **infers** a stage from the tool events every hook already delivers
un-instrumented sessions that never call `agenthub stage` — it fills in a lane's
pipeline map without requiring every skill to be rewritten.
Two event sources feed the identical matcher, one per tool:
| Lane `tool` | Source | Cadence |
| --- | --- | --- |
| `claude` | hooks — `touchLaneFromHook` in `server/routes/hooks.js` | every hook, live |
| `codex` | Codex's own rollout journals on disk — `server/lib/codex-ingest.js` | poll, `DASHBOARD_CODEX_POLL_MS` (default 15000; `0` disables) |
The matcher is fed by hooks — `touchLaneFromHook` in `server/routes/hooks.js`,
on every hook, live — so it only ever sees `claude` lanes.
Codex has no hook mechanism at all, so `touchLaneFromHook` is pinned to
`"claude"` — a hook must never touch a `codex` lane sharing the same directory.
`codex-ingest.js` is the codex-side equivalent: it tails
`$CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl` (`$CODEX_HOME` defaults
to `~/.codex`), reads the thread's `cwd` from the `session_meta` on each file's
first line, and maps Codex's tool calls onto the same `{tool_name, tool_input}`
shape a Claude hook delivers:
| Codex tool call | Mapped to |
| --- | --- |
| `exec` / `exec_command` / `shell` / `local_shell` | `Bash` with the extracted `cmd` |
| any of the above carrying an `apply_patch` heredoc | one `Write` (for `*** Add File:`) or `Edit` (`Update`/`Delete`) per touched file |
| `update_plan` | `TaskCreate` |
| `spawn_agent` | `Agent` with the task name as `subagent_type` |
| anything else (messages, reasoning, web search, tool OUTPUT) | ignored |
Everything past that point is shared: the same templates, the same
forward-only/declared-wins rules, the same amber-never-green boundary. Three
codex-only details:
- **A written detection also advances the lane's declared `stage`.** This is
the one place in AgentHub where an inference moves the blue `current` ring, and
it exists because nothing else can: a codex session has no hook and rarely
calls `agenthub stage` on its own, so without it the card sits at `idle` forever
while the amber badge is the only thing that ever moves. It is gated on
`recordDetection` having *written*, so it inherits that function's
forward-only, declared-wins and TTL guards unchanged — a detection behind the
declared stage still advances nothing. The `setStage` call passes **no
evidence**, so every node it moves through renders `passed-no-evidence`
(amber), never `done`: a detection sees a tool call, never its outcome. And
it passes `keepDetection: true`, the sole exception to `setStage`'s clearing
of the detection columns — here the detection is the *reason* for the
declaration, so the card keeps showing which command caused it rather than
looking like a claim the agent made. The last node of every template carries
no `detect` rules, so this can never auto-declare a lane `done`.
- Only bytes appended since the last pass are read. On the first sight of a
journal only its recent tail is read, and nothing at all when that file is
already idle — replaying a thread that finished days ago would paint stages
for work nobody is doing. "Idle" means the journal's mtime is more than two
minutes old **or in the future**: a file that claims to be from the future
(clock stepped back, NTP correction, a restore that preserved timestamps)
makes a raw `now - mtime` negative, which would otherwise read as "written
milliseconds ago" and replay a finished thread as live work.
- The same pass mirrors the lane's `status` between `idle` and `running` from
the journal's mtime, under the identical guard hook ingestion uses (only a
lane with no `run_id`, and only from `idle`/`running` — never from
`provisioning` or `failed`). It writes nothing else: not `stage`, not
`notes`, not `slot`/`ports`.
**A codex lane's *evidence* is a separate matter.** Auto-advance moves the ring
but can never supply evidence, so every node a codex lane passes through stays
amber until the session itself runs `agenthub stage <node> --evidence "..."`. AgentHub
cannot inject an instruction into a CLI it merely spawns, so an `AGENTS.md` in
the working copy is the only lever — `agenthub lanes agents install` writes the
marker-delimited block for you (see "Installing the QC/gate agents"). Note that
from inside a codex session `agenthub stage` needs to know which lane it is: a pane
the dashboard started carries `AGENTHUB_LANE_ID`, and anywhere else `--tool codex`
disambiguates (see "Which lane a CLI command addresses" below).
Codex lanes carry no pipeline, so nothing detects or advances their stage.
`server/lib/codex-ingest.js` only mirrors a codex lane's `status` between
`idle` and `running`: every `DASHBOARD_CODEX_POLL_MS` (default 15000; `0`
disables) it lists `$CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl`
(`$CODEX_HOME` defaults to `~/.codex`), reads each thread's `cwd` from the
`session_meta` on the file's first line, and calls the owning lane `running`
while that journal's mtime is under two minutes old — and `idle` once it is
older **or in the future** (a clock stepped back makes a raw `now - mtime`
negative, which would otherwise read as live). It uses the identical guard hook
ingestion uses (only a lane with no `run_id`, and only from `idle`/`running` —
never from `provisioning` or `failed`) and writes nothing else: not `stage`,
not `notes`, not `slot`/`ports`.
### The evidence boundary
@@ -1796,16 +1734,9 @@ agenthub lanes agents install
Writes both agent definitions (from the repo's own `agents/`, the same files the plugin ships) into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `agenthub lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a AgentHub upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
**On a `codex` lane the same command installs something else.** Codex never
reads `.claude/agents/`, so copying agent definitions there would be dead
files. Instead it writes the stage-declaration block into
`<lane>/AGENTS.md` — the project doc Codex *does* load from the working
directory — delimited by `<!-- agenthub:stage:start -->` / `<!-- agenthub:stage:end -->`
so a reinstall replaces exactly that block and nothing else. The file is
appended to when it already exists (your own prose is preserved) and created
when it does not; `installed` comes back as `["AGENTS.md"]`. This is the lever
that gets a codex session to supply the *evidence* auto-advance cannot — see
"Stage detection".
**On a `codex` lane the same command installs nothing** (`installed: []`, and
the Tools menu hides it): Codex never reads `.claude/agents/`, and codex lanes
carry no pipeline to drive.
**Credentials are not embedded.** Unlike Shipyard's original per-lane agent generation, these templates carry no QA account credentials — this repo has no seeded-QA-account system yet. `qc-local` degrades gracefully: if it hits a login page with no credentials block present, it reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile` rather than guessing.