diff --git a/AGENTS.md b/AGENTS.md index 7ad289cc..126004bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,42 +11,11 @@ - Update docs whenever workflow or architecture changes — follow `skills/update-project-docs/` automatically at the end of every change-set (README + VN/CN/KO mirrors, ARCHITECTURE, wiki + i18n + cache bump, server/client READMEs, docs/*). - Every applicable source file you create or update (`.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css`) must start with the authorship header: a truthful file overview plus the exact line `@author Nguyễn Ngọc Trí Vĩ `. See `skills/file-headers/` and `.claude/rules/file-headers.md`; verify with `bash skills/file-headers/scripts/check-headers.sh`. -## Declare your lane stage as you work +## Lane stage -This repository is watched by AgentHub, its own dashboard. A **lane** is keyed by -working directory, and the pipeline map only moves forward when the agent doing -the work declares its stage: - -```bash -agenthub stage --tool codex --evidence "" -``` - -- Call it at every real transition — starting to plan, starting to implement, - running tests, opening the PR — not once at the end. -- Stage names, `default` template: `intake → plan → implement → tests → review - → gate → ship → done` (the 16-node `ship-feature` template accepts these as - aliases too; `agenthub stage` warns on stderr and names the valid nodes if a stage - matches none). -- **`--tool codex` matters.** One directory can back both a `claude` lane and a - `codex` lane, and without a tool the command fails rather than guess (it names - both candidates). Inside a pane the dashboard started you need nothing — that - pane carries `AGENTHUB_LANE_ID`, which outranks everything but `--lane`. -- If `agenthub stage` reports that no lane owns the directory, this checkout was - never adopted: skip the stage calls and carry on. Stage reporting is - reporting, never control flow — a failed `agenthub stage` never justifies skipping - a step of the actual work. - -The dashboard also *infers* your stage from your tool calls (it reads Codex's -own rollout journals — see `docs/LANES.md#stage-detection`), and on a codex lane -that inference does move the pipeline map forward on its own. It still never -counts as **evidence**: a node it moved through stays amber and never renders -green. Only your own declaration does that — which is why the call above is -worth making even when the map already looks right. - -If you hold a stage for a while with no evidence recorded, the dashboard may -type a one-line `[agenthub]` reminder straight into this pane asking you to run -`agenthub stage ... --evidence "..."`. That is the dashboard, not the user — treat -it as the same instruction as this section and just make the real call. +AgentHub's `codex` lanes carry no pipeline: the dashboard shows no stage for +them and never infers or advances one, so there is no `agenthub stage` call to +make while you work here as Codex. ## Where to work - `server/` for API/routes/data processing. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b39936d1..840c764c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -370,7 +370,7 @@ graph TD | `lib/lane-services.js` | (A2) `ensureDatabase`/`dropDatabase` call the profile's `db-create`/`db-drop` hooks — AgentHub stays stack-agnostic on purpose. A state-dir marker file tracks whether a slot's database was already created, since a plain `createdb` can't be re-run safely and AgentHub can't assume the hook is idempotent; this is also how `upLane` knows to seed only a freshly-created database. `dropDatabase` asserts the lane is `managed` and that the name being dropped is one this lane's own slot actually derives (itself or its `_test` sibling) before spawning anything | | `lib/lane-features.js` | (B) Per-feature state and archive. `activateFeature` archives the lane's current active feature (if different) and restores the target's saved stage onto the live `lanes` row — the row stays the one live view every other reader already uses. `canonicalizeSlug` is a DELIBERATELY separate rule from `worktree.js:slugify` (drops a leading `feat/`, keeps `[A-Za-z0-9._-]`, does not lowercase) — the two must never be conflated. `clearLane` (`lib/lanes.js`) archives the active feature (if any) before resetting; a lane that never activated one is unaffected | | `lib/git-diff.js` | (#34/#35) Reads a lane's code changes as structured hunks for the Workspace Changes panel — `work` (HEAD vs the working tree, untracked files included), `branch` (`...HEAD`) or `commit` (one sha), at a caller-chosen `context` (default 3, clamped to 5000, which is how the viewer expands a diff towards the whole file). Also the four reads behind History, the whole-file view, quick-open and the file explorer: `listCommits`, `readFileAt` (the `work` revision is realpath-confined to the lane; anything else goes through `git show`), `listPaths` and `listTree` (both `git ls-files`, so `.gitignore` is honoured without a skip-list). Read-only, argv-only through `worktree.js:git()`. An untracked file's patch is SYNTHESIZED from the file itself rather than fetched with `git diff --no-index`, which exits non-zero on a difference and would read as a failure. Three caps (300 files, 200 KB per file, 1 MB per full read) keep a lane mid-build from shipping megabytes of patch into a browser; a cap that bit sets `truncated` | -| `lib/git-ops.js` | (#34) The git WRITES a lane offers: `fetch`, `pull`, `push`, `stage`, `unstage`, `discard`, `stage-hunk`/`discard-hunk` (one hunk, matched by its `@@` line against a fresh diff; `discard-hunk` needs `confirm:true`), `commit` (everything, or `only:"staged"`), `switch`, `branch-create` (never switches) and `branch-delete` (`-d`, refusing `main`/`master`/`development`/`develop`, the current branch and one another worktree holds), `publish` (`remote add origin -- ` + `push -u origin HEAD`, only when the repo has NO remote and a commit; the one write whose destination comes from the request, so the URL is allowlisted by `validateRemoteUrl` — `https://` without userinfo, `ssh://`, scp-like `user@host:path` — and `origin` is removed again when the push fails), and `merge`/`resolve`/`merge-commit`/`merge-abort` (issue #45: a named branch into a clean repo-root lane, conflicts resolved per file, abort behind `confirm:true`). A caller-supplied path is validated against `git status` before it reaches an argv, so only a path git already reports as changed is reachable; `discard` — the one irreversible action — additionally requires `confirm: true`. No rebase, reset or stash, because a dashboard button is the wrong place to start an operation whose recovery needs a terminal (`reset` stays behind worktree.js's three-check destroy guard). `pull` is ALWAYS `--ff-only` and `switch` refuses a dirty tree, so neither can invent a merge commit or a conflict underneath a running agent; `push` pushes `HEAD`, never a branch name from the request. Network actions carry a 60s timeout | +| `lib/git-ops.js` | (#34) The git WRITES a lane offers: `fetch`, `pull`, `push`, `stage`, `unstage`, `discard`, `stage-hunk`/`discard-hunk` (one hunk, matched by its `@@` line against a fresh diff — or one `block`, a run of +/- lines inside it, the others folded into context by `onlyBlock` in `git-diff.js` (#50); `discard-hunk` needs `confirm:true`), `commit` (everything, or `only:"staged"`), `switch`, `branch-create` (never switches) and `branch-delete` (`-d`, refusing `main`/`master`/`development`/`develop`, the current branch and one another worktree holds), `publish` (`remote add origin -- ` + `push -u origin HEAD`, only when the repo has NO remote and a commit; the one write whose destination comes from the request, so the URL is allowlisted by `validateRemoteUrl` — `https://` without userinfo, `ssh://`, scp-like `user@host:path` — and `origin` is removed again when the push fails), and `merge`/`resolve`/`merge-commit`/`merge-abort` (issue #45: a named branch into a clean repo-root lane, conflicts resolved per file, abort behind `confirm:true`). A caller-supplied path is validated against `git status` before it reaches an argv, so only a path git already reports as changed is reachable; `discard` — the one irreversible action — additionally requires `confirm: true`. No rebase, reset or stash, because a dashboard button is the wrong place to start an operation whose recovery needs a terminal (`reset` stays behind worktree.js's three-check destroy guard). `pull` is ALWAYS `--ff-only` and `switch` refuses a dirty tree, so neither can invent a merge commit or a conflict underneath a running agent; `push` pushes `HEAD`, never a branch name from the request. Network actions carry a 60s timeout | | `lib/proof.js` | (C) Proof gallery: lists/serves/deletes QC screenshots grouped by feature slug (Task B's `lane_features`) and phase, under `/.playwright-mcp/proof///`. Every path is resolved, realpath'd, and containment-checked before touching `fs` — the entire security surface. `ensureProofLink` ports Shipyard's `ensure_proof_link` (converge a stray clone-root `proof/` onto the canonical dir); never auto-invoked, exposed only as `agenthub lanes proof-link` | | `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `agenthub lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive | @@ -1512,56 +1512,39 @@ changes. --- -## Codex Lane Ingestion (no hooks) +## Codex Lane Status (no hooks) A `codex` lane has no hook stream at all — Codex does not implement Claude Code's hook protocol — so `touchLaneFromHook` (the only caller of `stage-detect.detect()`) can never see one, and is deliberately pinned to `"claude"` so a Claude hook cannot touch a codex lane sharing the same -directory. +directory. Codex lanes carry **no pipeline**: nothing detects, advances or +nudges their stage, and the Workspace hides the stage chip, time-on-stage, +pipeline/feature pickers and PipelineMap for them. -`server/lib/codex-ingest.js` is the codex-side source. Codex appends a rollout -journal per thread at +`server/lib/codex-ingest.js` only mirrors a codex lane's `status`. Codex appends +a rollout journal per thread at `$CODEX_HOME/sessions///
/rollout--.jsonl` (`$CODEX_HOME` defaults to `~/.codex`); the first line is a `session_meta` -payload carrying the thread's `cwd`, and each tool call is a `custom_tool_call` -or `function_call` payload. One pass: +payload carrying the thread's `cwd`. One pass: 1. `rolloutFiles()` walks `sessions/` (depth ≤ 4), newest mtime first, capped at 40 files. 2. `rolloutCwd()` reads the thread's cwd from the file's first line (memoized — - it is written once, at thread creation) and matches it to a `codex` lane by + it is written once, at thread creation; the cache is pruned to the current + scan once it exceeds 4 × the 40-file cap) and matches it to a `codex` lane by deepest path-boundary prefix. -3. Only bytes appended since the last pass are read. On the FIRST sight of a - file only its recent 64 KiB 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. `completeLines()` does the framing **on the - buffer, never on a decoded string** (a tail offset can land inside a - multi-byte UTF-8 sequence, where the decoder's U+FFFD would throw the byte - math off): a tail read's leading fragment is dropped, the stored offset - advances only past the last newline, and a trailing fragment is re-read whole - next pass. A read is capped at 4 MiB, and the two tracking maps are pruned to - the current scan once they exceed 4 × the 40-file cap. -4. `eventsFromPayload()` maps each payload onto the hook-shaped - `{tool_name, tool_input}` the matcher already understands: shell calls - (`exec`/`exec_command`/`shell`/`local_shell`) → `Bash`, an embedded - `apply_patch` heredoc → one `Write`/`Edit` per touched file, `update_plan` → - `TaskCreate`, `spawn_agent` → `Agent`. Everything else is ignored. Newlines - inside a patch heredoc arrive escaped (it rides inside a JS string literal), - so they are unescaped before the file list is scanned. -5. The last matching event of the batch goes through the same - `recordDetection()` as a hook — same forward-only, declared-wins, - amber-never-green rules — and the same pass mirrors the lane's `status` - between `idle` and `running` from the journal's mtime, under the identical - guard hook ingestion uses (no `run_id`, only from `idle`/`running`). Nothing - else is written: not `stage`, not `notes`, not `slot`/`ports`. +3. The lane is `running` while that journal's mtime is under two minutes old, + `idle` once older **or in the future** (a clock stepped back would otherwise + read as live), under the identical guard hook ingestion uses (no `run_id`, + only from `idle`/`running`). Nothing else is written: not `stage`, not + `notes`, not `slot`/`ports`. `startCodexPoll(broadcastLane)` runs it on an unref'd 15 s interval from `server/index.js`, tunable via `DASHBOARD_CODEX_POLL_MS` (`0` disables). Every step is fail-safe: an absent `~/.codex`, a malformed journal, a lane deleted mid-pass or a busy SQLite is swallowed locally, and a pass never throws into the -event loop. No schema change, no new route, no new WebSocket message type — the -existing `lane_update` broadcast carries it. +event loop. The existing `lane_update` broadcast carries the change. Codex 0.149 also mirrors threads into version-suffixed SQLite files (`state_5.sqlite`, `thread_history_1.sqlite`), but every thread row still points @@ -1569,45 +1552,6 @@ at a live rollout file, so reading the JSONL avoids coupling to a schema the next Codex release renames. `rolloutFiles()` is the single function to re-point if that changes. -A written detection then **also advances the lane's declared `stage`** — -`setStage(lane.id, { stage: nodeId, keepDetection: true })`, the only place in -AgentHub where an inference moves the blue `current` ring. It is gated on -`recordDetection` having written, so it inherits forward-only/declared-wins/TTL -unchanged; it passes no `evidence`, so the node renders `passed-no-evidence` -(amber) and never `done`; and `keepDetection` is the sole exception to -`setStage`'s clearing of the detection columns, so the card still shows which -command moved it rather than looking like an agent's own claim. No template's -last node carries `detect` rules, so a lane can never be auto-declared `done`. - -### Codex evidence reliability (`server/lib/codex-nudge.js`) - -Auto-advance moves the ring but can never supply evidence; only the session's -own `agenthub stage --evidence` call can, so two mechanisms make that more likely -without weakening declared-wins/inferred-never-`done`. (Opening-prompt priming -used to be a third: `LaneConsolePane.tsx` pre-filled the prompt box from the -lane's pipeline — the driving slash command on a Claude lane, a plain-text -stage checklist on a codex one. It is gone: whatever sits in that box is sent -as a real prompt when the run starts, so the box now stays empty and the -prompt is the user's alone.) - -- **`AGENTS.md` stage block** (`server/lib/lane-agents.js`): on a `codex` lane, - `agenthub lanes agents install` writes a marker-delimited - `` section into `/AGENTS.md` instead of - copying the two agent templates into `.claude/agents/`, which Codex never - reads. Appended to an existing file (user prose preserved), and a reinstall - replaces exactly that block. -- **Missing-evidence nudge** (`sweepCodexNudges`, an unref'd 60 s interval from - `server/index.js`, tunable via `DASHBOARD_CODEX_NUDGE_MS`, `0` disables): - when a codex lane's current stage has been held for at least two minutes with - `stages[stage].evidence` still null and hasn't already been nudged for that - exact stage, it types a one-line `[agenthub] ... run: agenthub stage ... --evidence - "..."` reminder into the lane's live tmux pane via `tmux.sendText` (skipped if - the pane isn't running an interactive program — `pty-run.js`'s - `SHELL_COMMANDS` — since Enter there would execute the reminder as a shell - command instead of submitting it as a chat turn). It never writes `stage`, - `stages` or `detected_stage` — any resulting evidence is still something the - codex session declared itself, through the real `agenthub stage` call. - --- ## Agent Extension Layer diff --git a/CLAUDE.md b/CLAUDE.md index bbe871b0..6f1bdc2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,13 +1,13 @@ # Claude Code Working Guide -_Last synced: v1.8.6 (2026-09-24) + four fixes found by the #37 evaluation, shipped without #37 itself: tmux targets are EXACT (`=name` for has-/kill-/attach-session, `=name:` for pane and option targets, in `server/lib/tmux.js`, `pty-attach.js` and `scripts/cc-update-all-helper.js`) because a bare `-t agenthub-lane-1` resolves by prefix to `agenthub-lane-12` once lane 1 is gone; `getRun`/`killRun` in `server/lib/pty-run.js` refuse any id that is not `agenthub-lane-`, so `DELETE /api/run/main` can no longer kill a user's own tmux session; `POST /api/lanes/:id/router/apply` reports `restart_required` only when the pane's status is `running` (it was always true, since `getRun` returns a `gone` handle); and a lane's `needs_action` is cleared only by the session that raised it (new nullable `lanes.needs_action_session`, `server/routes/hooks.js`) + macOS support, code-audited and untested on a Mac (#22): `server/index.js` deletes a DENYLIST of parent-session variables at startup (`PARENT_SESSION_VARS`: CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_MESSAGING_*, CLAUDE_PLUGIN_ROOT, AI_AGENT, …, plus GIT_EDITOR only when it is Claude Code's `true`; user config such as CLAUDE_CONFIG_DIR/CLAUDE_CODE_USE_BEDROCK/ANTHROPIC_* passes through), because a server started from a Claude Code session handed them through tmux to every lane pane; the `~/.local/bin/agenthub` launcher falling back to `node` on PATH when its pinned node is gone (`linkCli` in `scripts/plugin-bootstrap.js`); `liveServers` ignoring a discovery entry whose `startedAt` predates this boot, so a recycled pid is never a live dashboard; lane cwds stored by real path (`canonicalCwd` in `server/lib/lanes.js`, used by `createLane`, `POST /api/lanes/ensure` and the liveness reaper — `/tmp` becomes `/private/tmp`, a not-yet-existing path through its deepest existing ancestor, existing rows untouched); an `__setExecImpl` seam in `server/lib/session-liveness.js`; and the `INSTALL.md` § macOS section with the Mac checklist + issue #34's empty-repo half — `publish` as a git write (`server/lib/git-ops.js`: `remote add origin -- ` + `push -u origin HEAD`, only in a repo with a commit and NO remote, URL allowlisted by `validateRemoteUrl` to https-without-userinfo/ssh:///scp-like, `origin` removed again on a failed push; `ENOCOMMITS`/`EREMOTEEXISTS`/`EBADURL`), an unborn repo being readable (`gitFacts`/`statusCounts` in `server/lib/worktree.js`: `head: null`, new `unborn`, branch via `symbolic-ref`), `POST /api/lanes/worktree` answering `400 EEMPTYREPO` instead of leaving a `failed` row, and `LaneGitBar.tsx` gaining **Publish…** (no remote), **Publish branch** (remote but no upstream) and a commit-first hint (unborn) while `AddLaneModal.tsx` steers an empty repo to Repo mode + a dashboard fetch/pull/push that needs a git LOGIN asking for one in the git bar (issue #45: `409 EGITAUTH` with `auth`, resent with `credentials`; AgentHub's own shared store `server/lib/git-credentials.js` + helper `server/lib/git-credential-helper.js`, `/api/git-credentials`, Settings → **Git credentials** = `client/src/pages/GitCredentialsConfig.tsx`) + MERGE and conflict resolution from the dashboard (issue #45 part B: `merge`/`resolve`/`merge-commit`/`merge-abort` in `server/lib/git-ops.js`, `gitFacts.merging`/`conflicts`, the git bar's **Merge into current** and diverged-upstream **Merge**, the Changes panel's merge banner and **Merge conflicts** group, and the per-block conflict view `client/src/components/lanes/LaneConflictView.tsx` over the pure parser `client/src/lib/conflicts.ts` — keep mine/theirs/both per block, saved through the whole-file editor's `PUT /git/file` + `resolve done`, no new API) + drag-and-drop of an image onto the lane terminal (`TerminalView.tsx` uploads to `POST /api/run/drop-image` in `server/routes/run.js` — raw body, png/jpeg/gif/webp only, random name under `/agenthub-drops-/`, never a name from the request — then `term.paste()`s the returned path, which is how `claude` attaches it) + Claude Opus 5.5 getting its own pricing row (`claude-opus-5-5%` at 4/20/0.2/5/8, fast 8/40 — `claude-opus-5%` had been pricing it as Opus 5) + AUTOMATIC pricing (`server/lib/pricing-sync.js`, started in `startBackgroundServices`: a model family used in `token_usage` with no `%` row of its own is INSERTED from LiteLLM's public price list, never updated, off with `AGENTHUB_PRICING_SYNC=0`) + the Changes panel at **Full screen** moving the whole Files view (finder + expand-all + tree, one `browsePane`) into a left SIDEBAR and dropping the Files tab and in-place editing (`full` prop on `LaneChangesPanel` from `Workspace.tsx`; a whole file opens straight into the editor, Save/Cancel enabled only when dirty, Cancel reverts) and height-locking the page (`viewportLocked` also true at full, else the panel shrank to content with no live run) + the overview splitting into **Staged Changes** / **Changes** groups in the `work` range + a changed tree row tinted whole (`treeMark` tone carries a `bg-status-*/10`) + the Files tree listing untracked-not-ignored files and carrying each entry's git status (`treeStatus`/`treeEntries`/`TREE_LS` in `server/lib/git-diff.js`: file `status` = porcelain code, folder `changed: true`, from ONE `laneStatus` per tree read; `treeMark` in `LaneChangesPanel.tsx` paints M/U/A/D/R) + split panes on ONE lane (every Workspace pane shows the strip's selected lane, each in its own tmux session: `slot` 0–3 on `POST /api/lanes/:id/start`, `agenthub-lane-` for slot 0 = `run_id`, `agenthub-lane--` for 1–3 — `runIdForLane(id, slot)`/`MAX_PANE_SLOT` in `server/lib/pty-run.js`, the `-[1-3]` suffix accepted by `pty-attach.js` and `websocket.js`; `stop`/`reset`/`remove` kill every slot via `laneRunIds`; a pane's Active runs hides what the other panes show; no per-pane lane picker) + the server suite running on a PRIVATE tmux server (`npm run test:server` = `scripts/test-server.js`: drops `TMUX`, sets `TMUX_TMPDIR` to a temp dir, kills that server after — suites spawn and kill real `agenthub-lane-` sessions with ids from their own DB, and on the default server that killed the user's real lane panes, including the one running the tests) + the whole-file EDITOR (`PUT /api/lanes/:id/git/file`, `server/lib/lane-file-write.js`: the read path's guards plus existing-text-file-only and a `baseSha` stale check → `409 ESTALE`; `readFileAt` returns that `sha`) + the per-hunk controls moving into an editor-style review bar under each hunk's last changed line (`HunkBar` in `LaneDiffView.tsx`: ^/v, `n of N`, Undo, Keep) + **Unstage all** on the ONE commit/Stage-all row + Changes sizes Narrow (30%) / Medium (50/50) / Full screen, no 70/30 + the lane card's needs-you banner gone (the strip tile keeps it) + per-HUNK Stage/Undo in the Changes panel's Review and single-file views (`stage-hunk`/`discard-hunk` in `server/lib/git-ops.js`: the hunk is named by its `@@` line and re-matched against a FRESH `git diff -U HEAD` by `hunkPatch` in `git-diff.js`, then `git apply --cached` / `git apply -R` from a temp file — `409 EHUNKGONE` when it moved, `400 EUNTRACKED` on an untracked file; `discard-hunk` is the SECOND irreversible git action and carries the same `confirm: true` gate and two-click UI; the controls reach every `HunkHeader` through a context in `LaneDiffView.tsx`), each Review file header sticky with its own stage/unstage/discard (`FileActions`, shared with the overview row) + the Changes panel gaining a FULL-width step (`CHANGES_SIZES` in `Workspace.tsx`: narrow → 50/50 → 70/30 → full → closed; at full the `lane-detail` section gets `hidden` — CSS, so the one console render site stays mounted), **Stage all** plus the staged-only commit box ABOVE the overview list and sticky (as is the Files finder), and **Expand all folders** being ONE `GET /api/lanes/:id/git/tree-all` (`listTreeAll` in `server/lib/git-diff.js`: one `ls-files`, every level shaped like `listTree`'s) instead of a request — and a `git ls-files` — per folder + the lane's git section MOVING from the lane card to the top of the Changes panel (`client/src/components/lanes/LaneGitHeader.tsx`: `useLaneGitFacts`, the working-copy line, `LaneGitFilesPanel` and `LaneGitBar`, sharing ONE facts read with `LaneChangesPanel`, which refreshes it after every stage/unstage/discard/commit while a git bar action re-reads the diff; `onNewLane` comes from `Workspace.tsx`, and `LaneCard` has no git row and no `onNewLaneFromRepo` any more) and the git bar losing its commit UI — the overview's staged-only box is the ONE commit path, the server's `commit` without `only` is unchanged; the git line and its icon-only actions are ONE row in one card, the panel toolbar ONE wrapping row, and `LaneCard` hides itself (`empty:hidden`) when it has nothing to show + the run form's `/` and `@` autocomplete being REAL again (`PromptEditor` in `client/src/components/run/RunSetup.tsx` — the `2f39f4e` rewrite had left a bare textarea: `/` offers the run cwd's project + user commands and skills, via `api.ccConfig.commands|skills(scope, cwd)`, plus the CLI built-ins, claude lanes only; `@` offers `GET /api/run/files`; Enter picks while the list is open and sends otherwise) + the console's **Active runs** listing only the CURRENT lane's runs and external sessions (`LaneConsolePane.tsx`: runs by `laneId`, sessions by the longest path-boundary lane cwd, `owningLaneId`) + v1.8.3 (2026-09-23) + `GET /api/settings/info` reporting `server.memory.heapLimit` (V8's `heap_size_limit`), which the Dashboard's V8 Heap bar and Health Score now divide by instead of `heapTotal` + every `claude plugin install|update -y` (`cc-plugin-cli.js`, `skill-plugins.js`, both detached update helpers, `/agenthub-update`) retrying WITHOUT `-y` when an older Claude Code answers `unknown option '-y'` + `LaneDiffView.tsx` WINDOWING a diff above `WINDOW_THRESHOLD` (500) items at a fixed `ROW_PX` row height (`visibleRange`, spacers, a `ch` min-width off the longest line; at or below the threshold the DOM is unchanged) and clamping its mirrored split scroll (`clampScroll`/`follow`) + a git action that git itself refuses (a numeric exec exit status) answering 409 `EGIT` instead of 500 with a number for a code (`gitErrorCode` in `server/routes/lanes.js`), and one `parseNumstat` (`server/lib/git-diff.js`) shared by the Changes panel and `gitFacts.files` + the Dashboard heatmap ramp mixing `--accent` into `--surface-0` so it reads on every theme + the lane git surface being HARDENED (`server/lib/git-diff.js`, `server/lib/git-ops.js`, `server/lib/worktree.js`: every lane git call runs with `GIT_LITERAL_PATHSPECS=1` and `-z` porcelain, is scoped to the LANE's cwd — a subdir lane sees and acts on only its own paths, `gitFacts.files` included — and reads through `lanePath` at every rev; any file whose basename is `settings.local.json`, the lane router key file, is refused as `ESECRET`/403 on disk and at every rev; `commit` refuses with `ESTAGEDOUTSIDE` while anything outside the lane is staged, checked with `--no-renames`; `push` without a resolvable remote is `ENOREMOTE`; git writes serialize on the repo's absolute git dir, not the lane id) + the xterm terminal following the selected DARK theme (`client/src/lib/terminalTheme.ts`, live via `term.options.theme`) and chart colours using structural tokens + the Add-lane modal asking `POST /api/lanes/ensure` with `exact: true` (#42) — without it a directory NESTED inside an existing lane resolves to that parent lane with `created: false`, so the modal closed on the parent and no new card ever appeared; `exact` counts only a lane on that very directory, and a `created: false` answer now names the lane that owns the folder instead of closing. The CLI, the run console and Sagi keep the parent lookup + Settings → **Appearance** (`client/src/components/settings/AppearanceSettings.tsx`: mode Dark/Light/**System** — `prefers-color-scheme`, followed live — plus one NAMED theme remembered per mode: dark Default/Darcula/Apple Dark, light Default/Apple Light/IntelliJ Light/Canvas). A theme is a CSS-variable override block in `client/src/index.css` keyed by `data-theme` on `` (`.dark[data-theme=…]` / `[data-mode="light"][data-theme=…]`, `data-mode` is set on `` too), and `--tint-base`/light `--syn-plain` are now `var(--surface-3)`/`var(--fg-primary)` so no theme restates them; `hooks/useTheme.ts` is a `useSyncExternalStore` reader over `localStorage` (`theme` = `dark|light|system`, `agenthub.theme.dark|light`) that caches NOTHING in the module, so the Sidebar and Settings can never disagree — do not reintroduce per-instance `useState` there; an inline ` + + diff --git a/bin/agenthub.js b/bin/agenthub.js index 5777d5f2..e6bca569 100755 --- a/bin/agenthub.js +++ b/bin/agenthub.js @@ -1463,16 +1463,24 @@ async function cmdStart(flags) { } /** - * Installs/removes a systemd `--user` service so the dashboard survives - * reboots without anything (Claude Code included) needing to start it first. - * Linux/systemd only; other platforms are told to keep using `agenthub start`. + * Installs/removes a systemd `--user` service (Linux) or a LaunchAgent + * (macOS) so the dashboard survives reboots without anything (Claude Code + * included) needing to start it first. Elsewhere, `agenthub start` by hand. */ async function cmdService(sub, flags) { - const svc = require(path.join(REPO_ROOT, "scripts", "systemd-service.js")); + const svc = require(path.join(REPO_ROOT, "scripts", "service-manager.js")); if (!svc.systemctlAvailable()) { - console.error(c.red("✖ systemd --user is not available on this machine.")); console.error( - c.dim(" Auto-start on boot needs Linux + systemd. Use `agenthub start` manually instead.") + c.red( + `✖ ${svc.kind === "launchd" ? "launchd (a GUI login session)" : "systemd --user"} is not available here.` + ) + ); + console.error( + c.dim( + svc.kind === "launchd" + ? " A LaunchAgent needs the user logged in at the Mac itself, not only over SSH. Use `agenthub start` meanwhile." + : " Auto-start on boot needs Linux + systemd. Use `agenthub start` manually instead." + ) ); process.exit(1); } @@ -1493,24 +1501,27 @@ async function cmdService(sub, flags) { description: "AgentHub dashboard server", workingDir: REPO_ROOT, execStart: `${process.execPath} ${path.join(REPO_ROOT, "server", "index.js")}`, + argv: [process.execPath, path.join(REPO_ROOT, "server", "index.js")], env, }); if (result === "foreign") { console.error( - c.red( - `✖ ~/.config/systemd/user/${name}.service already exists and wasn't written by agenthub.` - ) + c.red(`✖ ${svc.unitPath(name)} already exists and wasn't written by agenthub.`) ); console.error(c.dim(" Remove or rename it, then re-run `agenthub service install`.")); process.exit(1); } if (result === "failed") { - console.error(c.red("✖ Failed to write/enable the systemd unit.")); + console.error(c.red(`✖ Failed to write/enable the ${svc.label}.`)); process.exit(1); } console.log( - `${c.green("✔")} systemd --user service '${name}' ${result === "unchanged" ? "already up to date" : "installed"} and enabled.` + `${c.green("✔")} ${svc.label} '${name}' ${result === "unchanged" ? "already up to date" : "installed"} and enabled.` ); + if (svc.kind === "launchd") { + console.log(c.dim(` It starts at every login. Server log: ${svc.logPath()}`)); + return; + } const lingered = svc.enableLinger(); if (lingered) { console.log( @@ -1531,20 +1542,24 @@ async function cmdService(sub, flags) { if (sub === "uninstall") { const result = svc.uninstallUserService(name); if (result === "foreign") { - console.error( - c.red( - `✖ ~/.config/systemd/user/${name}.service wasn't written by agenthub — not touching it.` - ) - ); + console.error(c.red(`✖ ${svc.unitPath(name)} wasn't written by agenthub — not touching it.`)); process.exit(1); } console.log( - `${c.green("✔")} systemd service ${result === "absent" ? "was not installed" : "removed"}.` + `${c.green("✔")} ${svc.label} ${result === "absent" ? "was not installed" : "removed"}.` ); return; } if (sub === "status") { - spawn("systemctl", ["--user", "status", `${name}.service`, "--no-pager"], { stdio: "inherit" }); + if (svc.kind === "launchd") { + spawn("launchctl", ["print", `gui/${process.getuid()}/io.smartgift.${name}`], { + stdio: "inherit", + }); + } else { + spawn("systemctl", ["--user", "status", `${name}.service`, "--no-pager"], { + stdio: "inherit", + }); + } return; } console.error(c.red("✖ Usage: agenthub service install|uninstall|status [--port N]")); diff --git a/client/src/components/lanes/LaneCard.tsx b/client/src/components/lanes/LaneCard.tsx index 00ff1caf..c7f0f76c 100644 --- a/client/src/components/lanes/LaneCard.tsx +++ b/client/src/components/lanes/LaneCard.tsx @@ -363,19 +363,22 @@ export function LaneToolsMenu({ lane }: { lane: Lane }) { role="menu" className="absolute right-0 z-10 mt-1 min-w-44 overflow-hidden rounded-md border border-border-light bg-surface-0 py-1 shadow-lg shadow-black/40" > - + {/* Claude-only: a codex lane has no agents to install. */} + {lane.tool !== "codex" && ( + + )} {lane.tool !== "codex" && (