Commit Graph

348 Commits

Author SHA1 Message Date
nntrivi2001 27923db4e0 refactor(lanes): compact the lane strip tile and the lane-detail header
The Workspace lane panel spent four rows saying what fits in two.

Strip tile: the stage badges move onto the id row and the progress bar
and percent go away — the detail panel's pipeline map right below shows
the same progress with the stage names attached.

Lane detail: the pipeline picker, stage badges, feature picker, router
select and the remove button merge into the single header row instead of
a second row under it, and the pipeline's name is no longer interpolated
into `laneHeader` — the picker beside it already says it. The git block
collapses from four lines to one and drops the cwd, which the header
above it already spells out.

The router select is relabelled Model, moves out of LaneCard into that
header row (`LaneRouterRow` is now exported), and reports its apply
outcome as a ✓/⚠ marker with the message in a tooltip rather than a
wrapped sentence that pushes every row below it down.

Drops the tracker / dev QC / CI wait badge row and everything feeding it
(`useLaneIntegrations`, `api.lanes.integration`, the `integrations.*`
strings): two of the three toggles have no consumer at all, and a badge
reporting a toggle nothing acts on costs a row of height to say nothing.
`agenthub lanes integration` and the API route are untouched.
2026-09-17 09:41:03 +07:00
nntrivi2001 5ab674f134 chore(release): bump version to 1.6.4 v1.6.4 2026-09-16 11:28:18 +07:00
nntrivi2001 00a2aedb09 feat(ai-router): test an endpoint before saving it, and re-point a live lane
Two gaps left by the AI Router feature: a profile could be saved without ever
being reachable, and a lane's router could only be chosen at creation.

**POST /api/ai-routers/test** answers the question saving cannot. It sends one
Anthropic-style message capped at a single output token to `<baseUrl>/v1/messages`
with a 15s timeout — the wire shape a claude lane's eight env keys point at, so
what is tested is what will actually run. A router that answers is a 200 here
whatever it replies: `{ ok, status, message }` carries the router's own words,
and `status` is null when nothing answered at all. Only bad input is a 4xx.

The stored key is only ever sent to the endpoint it was stored against. A blank
`apiKey` falls back to the key of the profile named by `name`, but only while
the submitted `baseUrl` still equals the saved one — so testing a retyped
endpoint needs a retyped key (`EBADKEY`). Without that rule this route would be
a way to make the server POST a saved API key to any URL a browser names.

Network failures report `error.cause`, not `error.message`: node's fetch reports
every transport failure as the same useless "fetch failed" and hides the part
the user can act on (ECONNREFUSED, ENOTFOUND, a TLS error) one level down.

**The lane card carries an AI Router select.** The Add-lane modal was the only
place a router was chosen, and a lane outlives its creation. Switching goes
through `POST /api/lanes/:id/router/apply` — never a lane PATCH — because the
DB column and the lane's own config on disk may only move together. A failed
apply puts the select back rather than leaving the card claiming a router the
lane did not get. Hidden when `native` is the only profile: a select with one
option is a control that cannot do anything.

Adds `Lane.router_profile` to the client type (the payload already carried it).
2026-09-16 11:26:00 +07:00
nntrivi2001 9128352d1b fix(pricing): price Claude Opus 5, correct Sonnet 5, drop retired models
Claude Opus 5 had no pricing row at all, so every token it burned was costed
at $0 — the pattern list stopped at the 4.x Opus family and nothing matched
`claude-opus-5`. Add it (5/25, fast mode 10/50, same shape as 4.8).

Sonnet 5's launch discount expired 2026-08-31 and the discounted rate became
the published standard, so the row's 3/15 standard rate started overcharging
the day after. Fold 2/10 into DEFAULT_PRICING and empty DEFAULT_INTRO_PRICING;
no model ships a promo today.

Fable/Mythos 5.1 fell through to the `claude-fable-5%` row, which caches at
$1/MTok against 5.1's $0.25 — give them their own rows. That overlap is only
resolvable if the lookup prefers the more specific pattern, and `matchPricing`
took `LIMIT 1` with no ORDER BY, i.e. whichever row was inserted first. Order
by pattern length descending, which is what docs/DATABASE.md already claimed.
(The nested REPLACE(model_pattern, '%', '%') it dropped was a no-op.)

Retire Claude 3.x, Opus 4/4.1 and Sonnet 4 from the defaults. The startup
top-up only ever ADDS missing rows, so removing them from DEFAULT_PRICING
would leave every existing install untouched — prune them once, guarded by
PRAGMA user_version (0 -> 1) so a model deliberately re-added in Settings
survives later restarts. The same one-shot migration corrects the existing
Sonnet 5 row. Only rates are deleted; token_usage is never touched, and
Reset Defaults still restores the full current set.
2026-09-15 15:17:55 +07:00
nntrivi2001 fb3796f064 chore(release): bump version to 1.6.3 v1.6.3 2026-09-15 14:55:23 +07:00
nntrivi2001 c3f8dec104 refactor(lanes): drop the tick box and radio dots from the Add-lane modal
Both controls already render as cards that light up when selected — the same
selected/unselected treatment LaneStripCard uses — so the tick box and the two
radio dots were a second indicator of a state the card was already showing.

Neither input is removed, only hidden (`sr-only`): they are still what a
keyboard toggles and what a screen reader announces, so the cards take a
`focus-within` ring in exchange for the focus ring that went with them.

The router radios needed one thing the checkbox did not. "AI Router" is
disabled until the profile list lands, and the greyed-out dot was the only
thing showing that; hidden, it would have read as a card you can click that
does nothing — the dead control the existing comment warns about. So disabled
is now its own branch: muted text, reduced opacity, no hover, not-allowed
cursor.

Layout follows: no flex row with a gap when there is one child left, and the
two equal-width router cards centre their label rather than leaving it hard
against an empty left edge.

Visual only. No behaviour, API or schema change.
2026-09-15 14:53:00 +07:00
nntrivi2001 6e685ce1c3 chore(release): bump version to 1.6.2 v1.6.2 2026-09-15 11:01:07 +07:00
nntrivi2001 29a5da3e4e fix(bootstrap): stop a retired plugin build from downgrading the dashboard
`claude plugin update` unpacks the new version into a new cache directory and
says "Restart to apply changes". A Claude Code process that was already running
does not: it keeps loading the OLD directory, hooks and all.

Every session such a process opened then ran the OLD plugin-bootstrap, which saw
a state.json naming a different root, concluded the server had to be replaced,
repointed agenthub.service at itself and restarted the dashboard onto the
version the user had just upgraded away from. The dashboard reported the old
version and offered the update again — once per session, forever, while
installed_plugins.json and the cache both said the new version was installed.
A /agenthub-update from that session "succeeded" and changed nothing.

- `supersededBy()` reads Claude Code's installed_plugins.json. When a SIBLING
  cache directory is the recorded install, the bootstrap logs one line and
  returns "superseded" — no deps, no unit, no server. `--force` does not
  override it: the self-update chain forces from the NEW directory, which is
  never superseded, so the only --force refused is a deliberate downgrade.
  It cannot hand over either — the process it would replace is the one loading
  it — so the log line says to restart Claude Code.
- Matched by sibling directory, not by the <plugin>@<marketplace> key: correct
  without parsing a cache path or trusting a name. A phantom entry (installPath
  that no longer exists) is ignored rather than wedging the bootstrap shut.

Also adds `pruneRetiredPluginRoots()`, since the same updates leave a full copy
of the repo — node_modules and all — per release; twelve of them measured 1.7 GB
on one machine. It runs after writeState, so a bad prune cannot cost the record
of a good bootstrap, and it protects four independent readings of "in use":
this build, every installPath Claude Code records, the directory
agenthub.service names (`unitRoot`, foreign unit included), and any directory
still named in the user's settings.json hooks — that last one is the un-restarted
process above, seen from the other side. Plus the 2 newest survivors as a
rollback cushion, and nothing at all outside ~/.claude/plugins/cache/.
2026-09-15 10:30:00 +07:00
nntrivi2001 713e9fe58b chore(release): bump version to 1.6.1 v1.6.1 2026-09-14 16:33:46 +07:00
nntrivi2001 13bc7154d6 fix(updates): keep the update modal's footer buttons on one line
The footer wrapped every label mid-word ("Check / now", "Copy / command")
because nothing stopped the text breaking and the modal was too narrow to
hold four buttons.

Three changes, smallest first:

  - `whitespace-nowrap` on the footer container. `white-space` inherits, so
    one class keeps every label on its own line without touching each button.
  - `flex-wrap` alongside it, so a translation that still outgrows the modal
    breaks the ROW rather than the words.
  - `max-w-lg` -> `max-w-2xl` on the dialog.

Those alone were not enough in Vietnamese: the button's restarting label is
a full sentence ("Đã build xong — đang khởi động lại bảng điều khiển...",
52 chars), which put the row over the width on its own. That sentence is
already shown in the status banner directly above the footer, so the button
now uses a new short `restartingShort` key and the banner keeps the full
one unchanged.
2026-09-14 16:32:15 +07:00
nntrivi2001 e895c0e8eb chore(release): bump version to 1.6.0 v1.6.0 2026-09-14 16:07:05 +07:00
nntrivi2001 0a86dfb8c5 feat(settings): add AI Router profiles and attach one per lane
A new **AI Router** tab in Settings, next to Codex, stores named router
endpoints (9Router, OpenRouter, a self-hosted gateway) as profiles: a base
URL, an API key, a default model and an optional per-tier override. The tab
only stores them; a lane CHOOSES one in the Add-lane modal, via a Native /
AI Router radio, and that choice is written into the lane's own config.

The two CLIs are configured differently, and the difference is theirs:

  - a claude lane gets eight `env` keys merged into its own
    `.claude/settings.local.json` (`.local` because the value holds a key);
  - a codex lane gets `$CODEX_HOME/<name>.config.toml` — the layer
    `codex -p <name>` loads — because Codex has NO project-level config.
    Its base `config.toml` is never touched.

Native is a built-in profile rather than a code branch, so "go back to
native" is the same write path. It blanks the two connection variables
instead of removing them (an absent key at project scope lets a USER-level
router apply) and removes the model names instead of writing them (writing
them would override the user's own global model choice).

Key material never leaves the server: profiles live one file each under
`<CLAUDE_HOME>/agenthub-routers/` (0700/0600), `GET /api/ai-routers`
reports only `hasKey`, and there is deliberately no route returning one
profile in full.

Three things this had to get right, each found by review and verified
against the real tools rather than assumed:

  - `wire_api = "chat"` is rejected outright by codex-cli 0.149 ("no longer
    supported"), so every codex lane on a router would have failed to start.
    The layer writes "responses", and writes `env_key` only when the profile
    actually has a key — naming an unset variable is itself a startup error.
  - The key must never be TYPED into a pane. `tmux.sendCommand` used
    `send-keys` with a `KEY='…' cmd` prefix, which the dashboard streams to
    every attached browser and bash appends to history; it now takes over the
    idle pane with `respawn-pane -k -e`, passing env through argv the way
    `newSession` always has. Verified on tmux 3.4: multi-element argv arrives
    intact and the pane renders no trace of the value.
  - Attaching COPIES a profile, so editing or deleting one in Settings has to
    reach the lanes already using it. An edit re-applies it (a rotated key
    would otherwise never reach the lane still holding the revoked one); a
    delete resets those lanes to native and removes the Codex layer file.
    Both report per-lane failures, which the tab surfaces.

`.claude/settings.local.json` is added to the lane's git exclude, for both
native and router lanes: `agenthub lanes reset` runs `git clean -fd` without
`-x`, so an un-excluded settings file is deleted on reset — and untracked
meant the key was committable.

DB: `lanes.router_profile`, nullable, additive. Written only by
`setRouterProfile` beside the disk write, never through `PATCHABLE`.
2026-09-14 16:04:57 +07:00
nntrivi2001 fc7b58f15b chore(release): bump version to 1.5.10 v1.5.10 2026-09-14 13:10:21 +07:00
nntrivi2001 c4025cd2f7 style(lanes): render the create-readme option as a selectable card
Matches LaneStripCard's selected/unselected treatment and moves the hint
inside the card. Drops the "codex has no slash commands" clause from the
hint — the option is already hidden for a codex lane.
2026-09-14 13:10:16 +07:00
nntrivi2001 ee1e07bb5e fix(update-check): resolve the release URL for a relative plugin source
This project's marketplace entry is `"source": "./"` — the plugin IS the
marketplace repo — but pluginMarketplaceUrl only accepted a URL string or
`{url}`, so it returned null and every plugin-cache install answered
`no_remote`: no install of this plugin could ever see a published release.

A relative source now falls back to the manifest's own `repository`. An
`owner/repo` shorthand still returns null — it names a different repo, so
borrowing the marketplace's URL would be a confidently wrong answer.
2026-09-14 13:10:16 +07:00
nntrivi2001 91af3dca86 chore(release): bump version to 1.5.9 v1.5.9 2026-09-14 11:22:11 +07:00
nntrivi2001 baa355bee4 feat(lanes): offer a create/update readme run when adding a lane
An optional checkbox in the Add-lane modal starts the new lane's terminal
run right after creation with `/sg-all:sg-md-writing` as its first message,
so the lane opens already writing or refreshing its CLAUDE.md/AGENTS.md set.
Claude lanes only — codex has no slash commands.
2026-09-14 11:20:37 +07:00
nntrivi2001 b7e81af0b6 chore(release): bump version to 1.5.8 v1.5.8 2026-09-11 16:48:00 +07:00
nntrivi2001 c90dd7b147 fix(bootstrap): self-heal a systemd unit left on a retired plugin root
The residue an interrupted update leaves behind is invisible: the server
runs, every doctor row reads OK, and the unit quietly still names a cache
directory Claude Code deleted during the update. Nothing surfaces it until
a reboot, when the dashboard does not come back and nothing recorded why.

- fastPathOk now rejects a stale unit, so the next SessionStart repairs it
  instead of short-circuiting. `unitIsStale` is pure fs on purpose: that
  path runs on every session and may not spawn a process.
- Repairing the unit is not restarting the server. A current, healthy
  server keeps running and the corrected unit takes effect at the next
  start (`pendingSystemdRestart = systemdOwned && needsServer`), and the
  detached-spawn fallback is guarded by needsServer so a stale unit alone
  cannot start a second server onto a bound port.
- systemdOwned now also requires the unit to be ACTIVE. Installed-but-
  stopped owns nothing: skipping the stop there and asking systemd to start
  would bind an occupied port and crash-loop under Restart=always.
- /agenthub-doctor gains a systemd row naming the consequence ("will NOT
  come back after a reboot") and the fix. WARN, never FAIL — the dashboard
  in front of you works, and having no unit at all is a normal setup.

The fast-path test pins the regression against an otherwise fully healthy
install, live server included; without a live server it would have passed
against the unfixed code either way.
2026-09-11 16:45:47 +07:00
nntrivi2001 49a041b1dc chore(release): bump version to 1.5.7 v1.5.7 2026-09-11 16:32:50 +07:00
nntrivi2001 de8d1079e0 fix(bootstrap): never stop the dashboard from inside its own systemd cgroup
A plugin self-update took the dashboard down and left it down. The chain
ran correctly up to the point where the bootstrap SIGTERMs the server it is
replacing — and that server was systemd-managed, with the bootstrap running
as its descendant, i.e. inside its cgroup. Two failures fired at once:

  * the server exits 0, and `Restart=on-failure` reads a clean exit as
    "meant to stop", so systemd did not bring it back;
  * `KillMode=control-group` reaped the bootstrap itself, mid-run, before it
    could repoint the unit at the new plugin root or write state.json.

Evidence from the incident: bootstrap.log ends at "stopped the server
started from the previous plugin version" with no systemd line after it,
.bootstrap.lock survived with a dead pid, state.json still named the
previous version, and the unit's ExecStart still pointed at the old cache
dir. The same truncation is visible on three earlier updates — those only
recovered because a later SessionStart bootstrapped again.

systemd-service.js now splits install into writeUserService (repoints the
unit, starts nothing) and restartUserService (--no-block). The bootstrap
skips the manual stop whenever a unit of ours owns the server, and defers
the restart past writeState and releaseLock, so being killed by its own
request costs nothing. The unit is Restart=always: a graceful SIGTERM from
anything must no longer read as "leave it stopped".

installUserService keeps its old semantics for `agenthub service install`,
which runs from a user's shell and is not in the cgroup.
2026-09-11 16:30:10 +07:00
nntrivi2001 0335e0e6bc chore(release): bump version to 1.5.6 v1.5.6 2026-09-11 15:59:33 +07:00
nntrivi2001 18614f378d fix(updates): give a plugin-cache install a real message, not null
`agenthub update-check` prints `message` as its headline, so every
plugin-cache install saw the literal "null" where the verdict belongs — and
that is the one install kind with no commit count to fall back on, so the
line carried nothing at all.

The plugin branch of getUpdatesStatus compares versions exactly like the
checkout branch does; it now builds the same four sentences from what it
already computed (newer release / newest / no local version to compare /
release lookup failed).

The nogit regression test asserted `message === null` as a proxy for "did
not fall through to the not-a-git-clone payload" — it now asserts that on
the message's content instead, which is what it always meant.
2026-09-11 15:54:03 +07:00
nntrivi2001 63ed90ba05 docs(plugins): stop promising an MCP server the plugin no longer ships
c7dbaf2 dropped it. The doctor row list also claimed an 'MCP build
freshness' check that plugin-doctor.js does not emit.
2026-09-11 15:39:24 +07:00
nntrivi2001 3ed88062d6 chore(release): bump version to 1.5.5 v1.5.5 2026-09-11 15:35:35 +07:00
nntrivi2001 27313d4413 fix(run): refuse a run with ETMUXMISSING when the host has no tmux
Starting a run on a machine without tmux surfaced `spawnSync tmux ENOENT`
as a 400 — blaming the request for a missing host package, naming no fix,
and reaching the user as a raw errno in the console pane.

The guard goes in spawnRun, which both spawn paths (POST /api/run and
POST /api/lanes/:id/start) already route through, so one check covers
them; the message carries the install command. Both routes map the code to
503: the request was fine, the dependency is not installed. The client
already renders `error.message` from the body verbatim, so no UI change.

Also: README's node advice said "stay on 20/22/24"; 1712 server + 488
client tests are green on v26.8.1, so only 25 is actually the bad one.
2026-09-11 15:32:24 +07:00
nntrivi2001 d4b949a6f4 docs(deployment): list git and tmux beside node in the prerequisites
The only prerequisites block that still said node+npm alone.
2026-09-11 15:22:11 +07:00
nntrivi2001 8930865376 test(cli): cover the doctor's tmux line
The row was added without a check here, so a regression that dropped it
would only surface when someone's Start Run failed silently again.
2026-09-11 15:20:30 +07:00
nntrivi2001 29858ddccb docs: document the system prerequisites, and report tmux in both doctors
The READMEs never said tmux was needed, so a fresh install only found out
when Start Run failed with nothing explaining why. Both doctors were silent
about it too.

- README/README.vi: a prerequisites table (node, npm, git, tmux, claude,
  codex, gh, systemd) and per-OS tmux install + verify steps; the feature
  list is regrouped into observe/drive/configure and now covers analytics,
  alerts, import, remote sources, the config explorer and Update all.
- INSTALL: Step 0 for the system packages, plus a troubleshooting entry
  separating "tmux missing" from "the server has a different PATH".
- SETUP/docs/README: the same troubleshooting entry and a pointer to Step 0.
- plugin-doctor: a tmux row, WARN and never FAIL - the run console and
  `lanes shell` break without it, nothing else does. Reuses tmux.js's own
  isTmuxAvailable().
- agenthub doctor: asks GET /api/run/tmux instead, because the PATH that
  decides whether a run starts is the server's, not the caller's shell's.
2026-09-11 10:44:10 +07:00
nntrivi2001 7000a9c5ce chore(release): bump version to 1.5.4 v1.5.4 2026-09-10 23:21:45 +07:00
nntrivi2001 99e718ad91 fix(plugin): build the UI with devDependencies so a self-update ships its own bundle
After the 1.5.2 -> 1.5.3 self-update the server ran 1.5.3 while the dashboard
still served the 1.5.2 UI. The client build had failed, and `runtime/client-dist`
still held the previous version's bundle, so nothing looked broken — the only
signal was the Settings page's "UI build v1.5.2 (rebuild client to match)".

Cause: the whole client build toolchain (vite, typescript,
@vitejs/plugin-react, tailwindcss, @types/*) is a devDependency, and npm reads
NODE_ENV=production as --omit=dev (`NODE_ENV=production npm config get omit` ->
`dev`). The dashboard server runs with NODE_ENV=production, and every bootstrap
descended from it inherits that: the plugin-update chain, and any SessionStart
fired by a `claude` running in a tmux pane the server started. So `npm ci`
dropped vite, `tsc -b` died on missing type declarations, and buildClient
returned "failed" with the stale bundle left in place.

`--include=dev` on both install paths — a CLI flag outranks the env, which is
the point. Verified by running buildClient with NODE_ENV=production against the
installed plugin root: previously "failed", now "built", and the served bundle
carries 1.5.3.

Also: plugin-update-helper writes its verdict BEFORE the bootstrap handover as
well as after. That step retires the process tree the helper hangs off, and the
observed run reached the new server with no verdict file at all — the dashboard
reported `last_plugin_update: null` for an update that had actually succeeded.
2026-09-10 23:17:33 +07:00
nntrivi2001 67c948bbe2 chore(release): bump version to 1.5.3 v1.5.3 2026-09-10 17:09:07 +07:00
nntrivi2001 8b8582f231 fix(runs): persist tmux-backed runs so a resumed session shows its own info
`dashboard_runs.mode` is NOT NULL and `recordRun` swallows insert errors by
design, so pty-run.js passing `mode: null` meant NO tmux-backed run was ever
persisted — the newest row in a live database dated from before pty-run.js
replaced run-spawner.js. `publicRun` reads cwd, model, prompt preview,
started_at and session_id off that row and the handle itself is recomputed
from tmux, so the runs modal rendered a live row with no cwd, no model, no
prompt and a start date of Jan 1 1970. The same missing row is why a resumed
session kept its separate `external` badge: the client dedupes an external
session against the session ids carried by run rows, and no run row ever
carried one.

- pty-run.js records `mode: "interactive"` — every run here is one pane.
- dashboard-runs.js still never fails a live run on a db error, but warns
  instead of swallowing silently; that silence hid this for six weeks.
- RunHistory renders `startedAt: 0` as "-" rather than dating a row with no
  known start time to the epoch.

Also closes the other half of the same lie: adopting a pane that is already
running an agent now throws ERUNLIVE (409 from both the lane start action and
POST /api/run) when the request carried a prompt or a resumeSessionId. That
argv cannot be typed over a live agent, so answering 200 reported a resume
that never spawned. A start asking for nothing but a pane still adopts.
2026-09-10 17:07:23 +07:00
nntrivi2001 b868a2fa85 chore(release): bump version to 1.5.2 v1.5.2 2026-09-10 16:47:16 +07:00
nntrivi2001 30bc6debde fix(updates): check releases for a plugin install with no .git
An archive-sourced marketplace entry ("source": "url" — this project's own)
makes Claude Code unpack the plugin rather than clone it, so the cache dir
has no .git. getUpdatesStatus probed for .git first and returned "not a git
clone; check for updates manually", skipping the entire release layer:
update_available stayed false however many releases were published, and
plugin_marketplace/plugin_name were never set, so POST /api/updates/apply's
plugin branch could never fire (it answered 409, claiming nothing newer was
published). The bug reads as "up to date", so it stayed invisible for as
long as the install happened to be current.

The plugin branch now runs BEFORE the .git probe and answers without git at
all: the repository URL comes from the marketplace manifest on disk
(known_marketplaces.json -> .claude-plugin/marketplace.json -> source.url,
null rather than a guessed host for a github owner/repo shorthand), and
package.json is the only local comparison signal. A failed release lookup
is reported as release_check, never as "nothing newer".

UI/CLI: the "not a git clone" note is gated on install_kind !== "plugin", so
the Update now button is offered where the dashboard can actually check and
apply.
2026-09-10 16:46:06 +07:00
nntrivi2001 d4052ed235 chore(release): bump version to 1.5.1
openapi.yaml embeds info.version and the Settings screen snapshot pins the UI build string, so both are regenerated in the same commit - repo-links.test.js and the snapshot test each fail otherwise.
v1.5.1
2026-09-10 16:16:47 +07:00
nntrivi2001 aca35dd4cd feat(cc-config): update every plugin and marketplace at once, then reload the open sessions
The plugins tab could already update one plugin or one marketplace at a time. It gains an install-wide batch and, separately, the reload step that actually makes an update take effect.

Update all runs in a DETACHED worker rather than the request handler, for a structural reason: the batch ends with the plugin the dashboard ITSELF runs from, and that step's plugin-bootstrap.js --force SIGTERMs this very server. An in-process loop would die partway through with the remaining plugins unrun and nothing left to report. Order is marketplaces (one call, no name = all) -> every other installed plugin -> AgentHub last -> one reload. Each transition is journalled to cc-update-all.json in the runtime dir, outside the plugin cache the update replaces, so whichever server is alive serves progress from it; the POST's response is only an acknowledgement.

readUpdateAll DERIVES an abandoned status for a running journal older than an hour, because a detached worker killed by a reboot leaves nobody to write the terminal entry and the button would stay wedged forever. isRunning gates a concurrent batch: two claude plugin update chains over one config dir race.

Reload is its own capability (server/lib/cc-reload.js, POST /api/cc-config/reload). /reload-plugins and /reload-skills are scoped BY CLAUDE CODE to the session that runs them, so nothing can reload a session from outside - typing into a session already running is the only real mechanism, and that is what this does, over tmux send-keys into the dashboard's live lane panes. It spawns NO claude process on purpose: a one-shot claude -p "/reload-plugins" would reload only its own short-lived session, exit, and leave a junk session in the dashboard's history - a headless run gets a full sessions row and there is no filter anywhere to suppress it. Reusing live panes means no session is created at all. Panes at a shell prompt are skipped (the text would run as a shell command) and only claude lanes are addressed, since Codex has no such command.

Update-all reloads once at the end; updating a single plugin passes reload:true and reloads immediately, and never after a failed action.

Fixes a real pre-existing bug on the way: pluginUpdate omitted -y while runClaude closes stdin, and the CLI requires it whenever stdin is not a TTY - so the per-plugin Update button failed for any plugin whose marketplace declares an install command. Also completes CcConfigChangedPayload's action union, which claimed write|delete while nine other values were already being emitted.

Verified: server 1683/1683, client 486/486 (27 new tests), build, prettier, file-header audit, openapi.yaml regenerated (117 paths). Update all was NOT executed against this machine - it would update 7 real plugins and restart the running dashboard.
2026-09-10 16:05:07 +07:00
nntrivi2001 cc85e35439 feat(updates): detect the newest release and self-apply it, including plugin installs
Update detection was a commit count against the default branch, which says nothing about whether a *version* shipped and is pure noise on the plugin cache (a depth-1 clone, where every distance is a graft artefact). It now reads the forge's own releases API and compares the published tag to this install's package.json version, falling back to the commit count only when no release can be read — with the reason in release_check, so "up to date" never silently means "we could not look".

Two states a commit count cannot express: dev_build (ahead of the newest release, missing nothing from it) now reads "Dev build - N ahead" instead of a false "Up to date"; and on a shallow clone every commits_*_release field is null and dev_build is never asserted, because the numbers would be meaningless.

POST /api/updates/apply gains a second mode, chosen by install kind and never by the request body. A checkout still fast-forwards onto the release tag, rebuilds and self-restarts. A ~/.claude/plugins/cache/ install is refused by applyUpdate (that directory is Claude Code's to replace) and handed instead to a detached chain: claude plugin marketplace update -> claude plugin update -y -> plugin-bootstrap.js --force from the NEW cache dir, which retires this server and starts its replacement. Both write the HTTP response before anything stops the process.

The chain records every outcome to plugin-update.json in the runtime dir, outside the cache the update replaces, so the next server reports it as last_plugin_update: a failed chain is otherwise indistinguishable from a slow one, and the tab would poll a server that never returns. /reload-plugins is deliberately not in the chain - it is REPL-scoped to the session that runs it, so no background process can perform it, and plugin-bootstrap derives PLUGIN_ROOT from its own __dirname, which is what makes it unnecessary for the dashboard.

Also rebrands the sidebar subtitle, splash and page title to SmartGift Solution, and corrects three ARCHITECTURE.md statements that claimed /apply did not exist.

Verified: server 1683/1683, client 486/486, build, prettier, file-header audit, openapi.yaml regenerated.
2026-09-10 16:03:07 +07:00
nntrivi2001 eb63569324 Merge pull request 'chore: remove unused surfaces (VS Code extension, statusline, landing assets, Docker)' (#14) from chore/remove-unused-surfaces into main 2026-09-10 07:12:08 +00:00
nntrivi2001 a146284087 Merge pull request 'fix: make every plugin in the marketplace actually installable, and repoint the repo-rename leftovers' (#13) from fix/plugin-install-and-repo-rename into main 2026-09-10 07:11:45 +00:00
nntrivi2001 61cc37ba32 chore: remove unused surfaces (VS Code extension, statusline, landing assets, Docker)
The repo shipped five install/publish surfaces that nothing in the project builds, tests or documents as supported any more. Each one carried its own stale docs, and the Docker/Podman path additionally described a Dockerfile whose COPY of statusline/ would have broken the build the moment that directory went away.

Removed: vscode-extension/ (16 files, marketplace extension for an ID we do not own); statusline/ (3 files, standalone CLI statusline); the repo-root landing/wiki PWA assets (sw.js, manifest.json, robots.txt, sitemap.xml, og-image.svg, favicon.svg, fonts/ - 17 files for pages that no longer exist); .superpowers/ (4 files of scratch brainstorm state, already covered by .gitignore:55); and the container publish path (Dockerfile, docker-compose.yml, .dockerignore, 5 Makefile targets, 2 npm scripts).

Kept deliberately - these are about the USER's containers, not ours: scripts/install-hooks.js container detection and AGENTHUB_ALLOW_CONTAINER_HOOKS (issue #193), server/lib/lane-detect.js docker-compose service detection, server/data/profile-templates/postgres-compose/, server/lib/stage-detect.js 'docker push' detection, the host.docker.internal Prometheus notes, the Config Explorer's reads of the user's own ~/.claude/statusline.py, and .devcontainer/ (its docker-in-docker feature is dropped since it existed only to build the removed Dockerfile).

Docs follow the code: ARCHITECTURE.md loses the VS Code, statusline and container sections plus two dead TOC anchors, and its PWA section is rewritten from three PWAs to one; INSTALL.md, SETUP.md, docs/DEPLOYMENT.md, server/README.md, client/README.md, docs/README.md, docs/HOOKS.md and docs/DATABASE.md drop the same. The Makefile target count in ARCHITECTURE.md is corrected 25 -> 17 (it was already stale).
2026-09-10 13:44:28 +07:00
nntrivi2001 e972267a42 fix(plugins): make the 10 focused plugins installable, finish the AgentHub rename
The ten `agenthub-*` plugins were listed by the marketplace but could never be
installed — two schema mistakes present since the entries were first authored
(57dc915), both invisible until you actually run `claude plugin install`:

- marketplace entries used `"path": "plugins/<name>"`; the CLI only accepts
  `"source"`, so every one failed with
  `This plugin's marketplace entry is invalid: source: Invalid input`
- each `plugin.json` carried npm's `"repository": { "type", "url" }` object;
  the schema wants a plain string, so the next attempt failed with
  `Validation errors: repository: Invalid input`

Verified by installing all 11 entries into a throwaway CLAUDE_CONFIG_DIR:
1/11 before, 11/11 after. `plugins-marketplace.test.js` was asserting the
broken shape, which is why CI stayed green — it now asserts `source` and
rejects `path`.

Marketplace renamed `claude-code-agent-monitor-plugins` -> `agenthub`, so the
install line is `agenthub@agenthub`. ARCHITECTURE.md documented a marketplace
name (`smartgift-claude-code-monitor`) that never existed in any revision.

The documented `/plugin marketplace add Smartgift-AI/Claude-Code-Monitor` also
never worked: the `owner/repo` shorthand resolves to github.com only, and this
repo is self-hosted on Gitea. Confirmed the CLI rejects `host/owner/repo` and
ignores GH_HOST / GITHUB_HOST / CLAUDE_CODE_MARKETPLACE_URL, so the full
https:// clone URL is the only supported form. Same GitHub-habit class fixed
elsewhere: raw.githubusercontent.com icon URLs (404, and the repo ships
favicon.svg not favicon.ico — the client had the same dead /favicon.ico),
a smartgift.github.io docs link (404), and Gitea `/blob/<ref>/` paths that
must be `/src/branch/<ref>/`.

Repository renamed to Smartgift-AI/AgentHub throughout. Gitea 301-redirects
the old name, so these were stale rather than broken; `docs/superpowers/`
keeps the old name because those are dated decision records.

Adds `repo-links.test.js` to guard both classes (it runs under the existing
pre-commit gate) plus an openapi.yaml drift check — the committed spec had
silently fallen a minor version behind package.json. `generate-openapi-yaml.js`
now exports `render()` so the test compares against one definition of the dump
options instead of a second copy.
2026-09-10 11:03:14 +07:00
nntrivi2001 876c6de6d2 fix(lanes): count every pipeline run, not one per lane
Lane Pipeline Pass Rates read `lanes.stages`, which is a snapshot of the
pass in flight: re-entering a node overwrites its record, and moving
backwards deletes every record ahead of the new stage. The report treated
one lane row as one run, so shipping ten features through a lane still
counted as one, and no node could ever exceed 1/1. History survived only
via `lane_features`, which is written when a feature is activated — a
path most users never take. The panel was not wrong, it was structurally
incapable of counting.

Add `lane_stage_events`, an append-only log written by every declaration
and every accepted detection, and aggregate from that. A run is one pass
through the template: the log starts a new one when the lane moves
backwards or leaves the template's last node. Existing snapshots are
seeded as one run each on first boot, so upgrading does not read as
"never run". Templates a lane is configured with are still listed at
zero — a pipeline going unused has to be visible as such.

Count inferred reaches too, in their own column and their own colour.
Detection stays out of the pass rate ("inference is never evidence"), but
reporting a stage the hooks demonstrably saw as "never reached" is what
made the panel look broken to anyone who works without running the
pipeline commands.

Also reject a stage naming no node of the lane's pipeline (400 EBADSTAGE,
listing the ones that exist) instead of storing it with a warning nobody
reads. A real lane had accumulated five such keys, one of them `test` on
a ship-feature lane whose CI node is `gates` — which is why that row read
never-reached while the stage had in fact been declared. Aliases still
resolve, so shorthand is unaffected.
v1.5.0
2026-09-09 15:32:09 +07:00
nntrivi2001 3549000fa9 chore(release): bump version to 1.5.0
Minor, not patch: this cycle added the Codex config panel, made Claude
Code's MCP surface editable, and added the project-memory route both
panels use — new surfaces, no breaking change.
2026-09-09 14:46:02 +07:00
nntrivi2001 2942ced3ec style(config): give Codex the same tab bar as Claude Code, drop the Cursor tab
The Codex tab strip wrapped onto a second row while Claude Code's stayed
one scrollable row, so two panels that are meant to be twins read as
different components. Both now render the same ConfigTabBar — the
scroller lifted out of CcConfig, with the fade edges and chevrons that
only appear when there is somewhere to scroll. The deferral note in
CodexConfig that predicted this is gone with it.

The bar also carries role="tablist"/"tab" and aria-selected now: Codex
had them, Claude Code did not, and one of the two had to win.

Cursor's placeholder tab goes away with the machinery that served only
it — the optional Panel, the "soon" badge, the coming-soon card, and
their four i18n keys.

Element.scrollBy and friends move to test-setup: jsdom has none of them,
and the tab bar now calls them from both pages.
2026-09-09 14:44:25 +07:00
nntrivi2001 742e23e418 Merge branch 'feat/claude-settings-editable' into main
Makes the Claude Code MCP surface editable from the dashboard (add and
remove via the claude CLI), corrects which .claude.json the dashboard
reads so it matches the one the CLI writes, and adds the project-memory
route both panels use to create a new project's CLAUDE.md / AGENTS.md.
2026-09-09 13:56:02 +07:00
nntrivi2001 d7c1a10724 Merge branch 'fix/config-path-containment' into main
Closes two pre-existing holes in the Claude Code writer: a symlink at
(or above) a write target escaped the config root, and PUT/DELETE
/file honoured a caller-supplied ?cwd=. Both now go through the shared
guard in config-write.js.
2026-09-09 13:55:57 +07:00
nntrivi2001 e63bbf825e Merge branch 'feat/codex-config-panel' into main
Codex config panel in /settings, at full parity with the Claude Code
panel: skills, agents, rules, AGENTS.md memory and config.toml are
readable and editable, with MCP servers, plugins and marketplaces
driven through the codex CLI.
2026-09-09 13:55:53 +07:00
nntrivi2001 2ade30f670 fix(cc-config): close the symlink write escape and the caller-chosen write target
The Claude writer's containment was lexical only: a symlink at
~/.claude/<type>/<name>, or at any ancestor of it, is a string inside the
root while the write lands wherever it points. The backup destination had
the same exposure, and delete copies through the link before unlinking it.

PUT/DELETE /file also honoured ?cwd=, so a project-scope write could be
aimed at any directory on disk. PATCH /settings already refused that, with
the reasoning in a comment; the two other write routes never adopted it.

The guard moves into config-write.js so both writers run one copy.
2026-09-09 08:30:45 +07:00
nntrivi2001 5720e8d5f8 docs(plans): record the Codex config panel and Claude settings plans 2026-09-09 08:22:17 +07:00