Commit Graph

37 Commits

Author SHA1 Message Date
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 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 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.
2026-09-09 15:32:09 +07:00
nntrivi2001 85187dfc78 feat(config): create a new project's memory file from Settings
The Memory tab could only show this machine's user memory and the
dashboard's own project file. Setting up a project you had not run
through the dashboard meant opening a terminal — `PUT /file`'s
"project" scope always resolves against the server's cwd, on purpose.

Add one narrow route per tool that writes into the directory you name:
CLAUDE.md for Claude Code, AGENTS.md for Codex, both through the same
`project-memory.js`. Being the only write path that honours a
caller-supplied directory, it pays for that:

  - the directory must be absolute and already exist — nothing is
    created for you;
  - the filename comes from the route, never from the body;
  - an existing file, symlink included, is a 409 — this can only
    create, never overwrite;
  - it sits behind the loopback-Origin guard /api/run uses, so a page
    in the user's browser cannot plant agent instructions in a project
    directory. That guard moves to lib/same-origin.js to be shared.

The modal takes the dashboard's known project dirs as suggestions on a
free-text input rather than a closed list: a project it has never seen
is exactly the case this exists for.

Mutation-checked: dropping the origin guard, and swapping lstat for
stat, each fail a test.
2026-09-09 01:45:00 +07:00
nntrivi2001 c34931c3f5 fix(cc-config): .claude.json sits BESIDE ~/.claude by default, not inside it
The previous commit routed the read through getClaudeHome(), which appends
/.claude when CLAUDE_HOME is unset — pointing the dashboard at
~/.claude/.claude.json, a file the CLI never writes by default. Verified
against the real binary: with CLAUDE_CONFIG_DIR unset the CLI writes
$HOME/.claude.json; with it set, .claude.json sits flat inside that dir.
Branch on CLAUDE_HOME accordingly.

The test now drives a fake claude that writes where the real one does and
asserts getClaudeJsonPath() names that exact file, under both branches —
the previous test only restated the implementation.
2026-09-09 10:00:05 +07:00
nntrivi2001 6d27c115d8 fix(cc-config): MCP scope was inverted — expose user/local, not user/project
readMcpServers only ever parses ~/.claude.json (top-level = user scope,
projects[<root>] = the panel's "projectScoped" bucket, which the CLI itself
calls `local`). The previous user/project scope choice was the exact
inverse: `--scope project` writes .mcp.json, which readMcpServers never
reads (a successful add would vanish), and every "projectScoped" row was
actually `local`, so Remove sent a scope the CLI could never match.

- cc-mcp-cli.js: MCP_SCOPES -> ["user", "local"]; drop the local
  mutationQueue/serialize in favor of cc-plugin-cli.js's exported one, so a
  plugin install and an MCP add can't spawn `claude` concurrently; drop the
  String(v) coercion in env/header flags (a wrong-typed value is now a 400,
  not a written "[object Object]"); assertEmbeddedValue allows empty strings
  (a legitimate empty stdio arg was previously a 400).
- cc-plugin-cli.js: export `serialize` for reuse.
- CcConfig.tsx: CcMcpScope narrowed to user/local; the projectScoped bucket's
  McpCard now sends scope="local"; rows sourced from
  ~/.claude/settings.json (no CLI scope addresses them) no longer render a
  Remove button. Cleaned up i18n keys my own earlier change made dead
  (explain.mcp.addCmd/remove/removeButton) and gave the scope <select> its
  own short option labels instead of reusing full-sentence section headings.
2026-09-09 09:36:52 +07:00
nntrivi2001 fd4edaac98 feat(cc-config): add and remove MCP servers through the claude CLI
Mutation goes through `claude mcp add|remove`, never a direct edit of
~/.claude.json — the running CLI rewrites that file concurrently, so a
dashboard write would race it. Reads stay on readMcpServers.

The plan specified a --url flag; the real CLI (v2.1.266) has none —
commandOrUrl is positional, and --header must trail it or the variadic
flag swallows the url. Built against the verified shape.
2026-09-09 09:15:53 +07:00
nntrivi2001 acff059e92 fix(codex-config): close whole-branch review defects in the Codex panel
Plugins/marketplaces GET routes were silently empty because codex-cli.js
never unwrapped the CLI's {installed,available}/{marketplaces} objects
into arrays; backup writes bypassed the symlink-escape guard entirely;
readFileSafe was stricter than every other Codex reader, showing a
symlinked skill/rule in the list and then refusing to open it;
marketplaceAdd only accepted http(s) URLs, rejecting owner/repo sources
the CLI itself takes. Also: docs drift, a dead i18n key, and a scope-cast
hazard in the shared Editor/ConfirmDelete modals.

Fixes, one per finding: unwrap installed/marketplaces in codex-cli.js so
every list route keeps returning a bare list; guard the backup path in
codex-mutate.js with the same assertNotSymlinkEscape the write path
already used (Claude's cc-mutate.js has no such guard on its primary
write path either, so left untouched there); relax readFileSafe's
containment to lexical-only, matching the other four Codex readers,
while leaving the write side's realpath check untouched; reuse
cc-plugin-cli's assertSource for marketplaceAdd instead of a third
validator (ssh remains out of scope, no credential story for it yet).

Server: 1557/1557 (was 1548). Client: tsc -b clean, 469/469 tests.
2026-09-09 01:17:06 +07:00
nntrivi2001 8015730adf feat(codex): create and customize Codex config from /settings
Adds the Codex config panel (skills/agents/rules/memory/config editing,
mcp/plugins/marketplaces add-remove via the codex CLI) as the last piece
of parity with the Claude Code settings tab, plus the codex_config_changed
WebSocket type the client was missing, and docs for the new
/api/codex-config surface.
2026-09-08 21:09:28 +07:00
nntrivi2001 11dcc04ac4 fix(cc-config): one account is one profile, and rename it in place
Four defects, all from one design mistake: a profile's display name and its
filename were the same string.

- Saving the logged-in account made a SECOND profile of an account another
  profile already held. Both then reported themselves active, so there was
  nothing to switch between. `POST /profiles` now refuses `EDUPLICATE` and
  names the profile that already holds the account. Re-saving into the
  account's own profile stays allowed - that is the token-refresh path.
- Worse, an accidental duplicate could not be cleaned up: two profiles of one
  account are both active, and the delete guard refused any active profile, so
  neither was ever deletable. The guard now blocks only the LAST saved copy of
  the live account, which is the actual credential-loss condition it exists
  for. The trash button had to follow, or the server would allow what the UI
  hid.
- A name could not contain spaces or diacritics, because it had to be
  filename-safe. A profile now carries `name` (internal identifier, still the
  filename, still what every route takes) and `label` (what the user reads and
  edits: 1-60 free-form characters, control characters rejected, stored
  trimmed, unique case-insensitively). A label never touches a path, which is
  what makes "Tài khoản chính" a legal name.
- Naming happened up front in a "save current account as" field. That field is
  gone. The server derives the identifier, the save button lives on the live
  account's card and appears only while it is unsaved, and the label is edited
  by clicking it (Enter saves, Escape cancels).

Renaming rewrites the label in place and deliberately does NOT move `name`, so
it cannot half-fail and leave a profile unreachable. Profiles written before
labels existed report `label` equal to their `name`, so nothing needs migrating.

Adds `PATCH /api/cc-config/profiles`; `POST` now takes an optional `name`.

Verified: server 1431/1431 (11 new), client 459/459, tsc clean. Also exercised
end to end against a COPY of the real profile store on this machine: the
duplicate deleted, the last copy still refused, a Vietnamese label accepted,
and the stored access/refresh tokens and 0600 mode intact after a rename.
2026-09-07 16:29:11 +07:00
nntrivi2001 4bc77ded17 feat(cc-config): switch between Claude Code accounts, keeping the workspace
Adds a "Switch profile" button left of Backups on /settings?tool=claude-code.
A *profile* is one Anthropic account - one quota - saved under a name, so the
local `claude` CLI can be pointed at a different account without disturbing
anything else.

A switch swaps exactly two identity artifacts and nothing more: `claudeAiOauth`
inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside
the `.claude.json` config file(s). Those files are key-merged in place, and only
where they already exist, so `projects`/`numStartups`/everything else survives.
Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and
settings are never read or written here - that is what makes a switch keep the
whole workspace and change only the account. `mcpOAuth` stays put too: those are
the machine's MCP server tokens, not account identity.

Profiles are one JSON file each under `<CLAUDE_HOME>/agenthub-profiles/` (dir
0700, files 0600, atomic temp+rename) and hold live OAuth tokens, so no route
ever returns token material - `GET /profiles` answers identity metadata only.

The dashboard cannot log an account in; only `claude auth login` mints tokens.
Adding a profile is therefore always "log in from a terminal, then snapshot what
the CLI wrote". `activateProfile` re-snapshots the outgoing account first so
tokens Claude Code refreshed since its last save are not lost, and refuses
(`EUNSAVED`) to switch away from an account that was never saved - that would
discard the only copy of its credentials. A switch applies to new sessions only,
since a running session rewrites the config on exit.

Routes: GET/POST/DELETE /api/cc-config/profiles, POST /profiles/activate.

Verified: server 1420/1420 (14 new, incl. token-free responses and 0600/0700
permissions), client 459/459.
2026-09-07 15:13:38 +07:00
nntrivi2001 17d56d0e01 refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
2026-09-07 12:09:15 +07:00
nntrivi2001 677cdf007b feat(cc-config): list a plugin's skills, agents and commands by name
A plugin card showed "7 skills, 5 agents" and nothing more — the counts
were the whole story, so finding out WHICH skills a plugin ships meant
opening its install directory by hand. The contribution chips are now
buttons: clicking one expands a list naming each artifact with its
description, one surface open at a time. Commands render with their
slash. Hooks stay a plain chip — a plugin's hooks are matcher/command
pairs declared inline in plugin.json, not named artifacts.

`contributes.items` carries the four surfaces server-side; the existing
counts keep their type and are now derived from those lists, so a chip
reading "3 commands" can never sit beside a list holding two.

That also fixes a real blind spot. Each surface was read only from the
conventional install subdirectory, but plugin.json may declare its own
paths — this repo's own `ccam` lists three files under
plugins/ccam/commands/ and keeps commands/ empty, so it reported zero
commands. Both sources are now read and de-duplicated. A declared path
resolving outside the plugin's install directory is dropped: a manifest
is third-party data and must not turn the dashboard into an arbitrary
file reader.

Also pins the Refresh button's width: both labels share one grid cell so
the button is always as wide as "Refreshing…" / "Đang làm mới…" and no
longer resizes the toolbar when the state flips. Measuring the longest
real string beats a min-width that would be wrong in the next language.

Verified: npm run test:server (1370 tests, +6 for the new discovery),
npm run test:client (457, +3 for the expand behaviour), tsc --noEmit,
prettier and check-headers clean. Docs synced: docs/API.md, openapi.yaml
(new CcConfigPluginItem schema), ARCHITECTURE.md, docs/PLUGINS.md.
2026-09-04 11:21:12 +07:00
nntrivi2001 ce5eadba89 fix(run): keep the tmux pane exactly as wide as xterm
Scrolling a lane console repainted history with stray characters glued to
the left edge of blank lines. Reproduced against a real tmux attach: a pane
WIDER than xterm makes every over-long row autowrap its tail onto the next
row's left margin, and tmux — believing that row unchanged — never repaints
over it. One column of difference leaves one stray character per wrapped
line, two columns leave two; matched widths render clean.

TerminalView only reported its size on WS open and on the container's
ResizeObserver, so nothing corrected:
- cols moving with no container resize (webfont load, page zoom, scrollbar)
- tmux's `window-size latest` letting a second attach client (another tab,
  `ccam lanes shell`) resize the window out from under this one

Size is now reported from xterm's own onResize, re-asserted when the
terminal regains focus, and never reported as zero — the console collapses
with CSS, and fitting a 0x0 container would resize the real pane to nothing.

client 447/447, tsc clean.
2026-08-28 10:07:12 +07:00
nntrivi2001 b31da64d4f fix(docs): correct lane start action API and sandbox/approval enum values 2026-08-27 10:56:40 +07:00
nntrivi2001 24de68cfe1 feat(lanes): badge codex lanes and document the lane tool 2026-08-27 10:55:27 +07:00
nntrivi2001 20d85d3450 feat(workflows): pick a lane in the pipeline pass-rate panel
The panel aggregated every lane at once, which answers "does this stage get
proved across the project" but never "does THIS lane prove it". A lane picker
now narrows the same aggregation through ?lane=<id> on
GET /api/lanes/pipeline-stats, counting that lane plus its own archived
features — dropping the features would hide most of a lane's history the moment
it starts its next one.

The response carries a `lanes` array that the filter never narrows: it
populates the picker, so it has to keep offering every other lane even while
the counts show one. For the same reason the picker renders above the empty
branch rather than inside the populated one — narrowing to a lane that has
recorded nothing would otherwise be a one-way trip, with no control left to
select anything else. A lane filter that is not a positive integer is rejected
with 400 EBADLANE rather than silently aggregating everything.
2026-08-26 11:19:39 +07:00
nntrivi2001 348c645ef3 feat(workflows): pipeline pass rates and skill/plugin usage
Restores the five analytics sections that turned out to be worth keeping
(patterns, model delegation, error propagation, session complexity, compaction
impact) and reworks what the page opens on.

The lane panel no longer mirrors the Workspace's live lane strip. It now
answers the question the strip cannot: how often each pipeline node actually
passes. GET /api/lanes/pipeline-stats aggregates every stage record of every
lane AND every archived feature — a lane's own `stages` only holds its current
feature, so without the archive the history resets each time a lane starts new
work. Evidence-backed passes are counted apart from bare claims, the same line
the pipeline map draws between `done` and `passed-no-evidence`.

Beside it, GET /api/workflows/skill-usage reports which skills and plugins the
sessions actually reach for. Only PreToolUse rows are counted: every call also
emits a PostToolUse, and counting both would double every number. A plugin's
total includes calls to the MCP tools it ships, so one used purely through MCP
does not read as unused.

Still dropped from issue #6: the agent collaboration network and the subagent
effectiveness scorecards.
2026-08-26 10:32:13 +07:00
nntrivi2001 341197aac0 feat(settings): host Claude config as a tab in Settings
Settings becomes a tabbed shell (SettingsHub) with one tab per AI tool:
Dashboard (existing settings), Claude Code (CcConfig), plus Codex and
Cursor placeholders. Active tab lives in the ?tool= query param, and the
old /cc-config route redirects to /settings?tool=claude-code. The
standalone sidebar entry is dropped.

Adding a tool is one entry in TOOL_TABS plus its i18n label.
2026-08-26 09:01:00 +07:00
nntrivi2001 008ebf1c5c feat(cc-config): manage plugins, marketplaces, hooks and settings from the dashboard
The Claude Config page was read-only for these four surfaces. It can now:

- register a marketplace from a pasted GitHub `owner/repo`, http(s) URL or
  absolute path, and install / enable / disable / update / uninstall plugins,
  all delegated to the `claude plugin …` CLI so its registry stays consistent;
- edit hooks and settings.json keys in the user, project and project-local
  layers.

Server:

- `server/lib/cc-plugin-cli.js` wraps the CLI with `execFile` and an explicit
  argv array — never a shell string. Every argument is validated before the
  spawn (shape allowlist per kind, no control characters, no leading `-`, no
  `..`), `CLAUDE_CONFIG_DIR` is pinned to the dashboard's CLAUDE_HOME, stdin is
  closed so a prompt errors instead of hanging, and mutating calls are
  serialized in-process.
- `cc-mutate.patchSettings` merges individual dotted keys into one settings
  layer. It re-reads the file unredacted, so keys the UI never displayed —
  including secret-valued ones — survive byte-for-byte. A payload carrying the
  `<redacted>` placeholder is refused (EREDACTED), as are `__proto__` /
  `prototype` / `constructor` segments; a settings file that does not parse is
  reported (EBADJSON) rather than clobbered. Backup first, then atomic write.
- Six additive routes on /api/cc-config; a non-zero CLI exit is a 422 carrying
  the CLI's own message, a missing binary a 503.

Client: install/add bars, per-card plugin and marketplace actions behind a
two-click confirm, and edit modes for hooks and settings that send only the
keys that changed. New i18n keys in en + vi; the explainer banners that said
these surfaces were read-only now describe what the writes actually do.

Tests: 61 new server tests (CLI argv construction and validation, the settings
key-merge, and the six routes against a fake `claude` on PATH).
2026-08-25 15:58:03 +07:00
nntrivi2001 8ce20e8163 feat(dashboard): merge Analytics into Dashboard, remove Kanban Board
Closes issue #5 (Dashboard | Update menu & page):
- Analytics content (heatmap, sparkline, token/cost/workflow/productivity
  charts) moves into a new "Analytics" tab on the Dashboard page, alongside
  the existing Monitor/Health tabs. The standalone Analytics page, its
  /analytics route, and its sidebar menu entry are removed.
- The Kanban Board page, its /kanban route, and its sidebar menu entry are
  removed entirely (page content deleted, not just hidden).
- Updated ARCHITECTURE.md, README.md, client/README.md, docs/API.md, and
  the affected client tests/snapshots to match.
2026-08-25 11:45:39 +07:00
nntrivi2001 b1bca07c47 feat(sagi): spawn a real claude session for Sagi's Ask handoff
Unmatched Ask questions now adopt a dedicated "Sagi" lane (POST
/api/lanes/ensure, cwd defaults to $HOME) and spawn a tmux-backed claude
run there instead of just prefilling the Run page composer.
2026-08-24 09:39:28 +07:00
nntrivi2001 ce2797b01f feat(run): make bypass permissions selectable in a lane pane's shift+tab cycle
`claude` only offers bypass in its shift+tab permission cycle when started
with `--allow-dangerously-skip-permissions`, which the pane argv never passed.
A dashboard-started run was therefore stuck cycling plan/auto/manual/
accept-edits, while the same session attached from a real terminal could
reach bypass.

The flag makes bypass SELECTABLE, not enabled — `claude --help`: "Enable
bypassing all permission checks as an option, without it being enabled by
default." The starting mode is still whatever `--permission-mode` says
(`acceptEdits` by default) and `--dangerously-skip-permissions`, which would
actually turn it on, is still never passed. Entering bypass remains an
explicit human shift+tab in the pane, or an explicit
`permissionMode: "bypassPermissions"` on POST /api/run.
2026-08-20 08:32:43 +07:00
nntrivi2001 c7e934b4b4 fix(run): stop the terminal wheel from typing arrow keys, re-attach dropped sockets
Three separate reasons the Workspace terminal looked unscrollable:

- xterm falls back, on an alt-screen buffer whose program has not enabled
  mouse tracking, to translating each wheel notch into a cursor-key press.
  Inside a `claude` pane that reads as arrow up/down, so scrolling walked the
  prompt history. A custom wheel handler returning false suppresses only that
  emulation branch; real mouse reports come from a separate listener xterm
  registers when the program does request wheel events.
- TerminalView never reconnected, so a server restart left the pane frozen on
  its last painted frame — still live-looking, but swallowing every keystroke
  and mouse report. It now re-attaches 1.5s after an unexpected close and
  stops only on the server's `exit` frame; tmux keeps the session, so the
  re-attach repaints in full.
- tmux repaints the whole pane, so xterm's own scrollback is always empty and
  its viewport scrollbar renders as a groove with no thumb that cannot move.
  Hidden, since scrolling there goes through mouse reports, not the DOM.
2026-08-19 12:02:01 +07:00
nntrivi2001 c25008ab19 fix(run): stop Start/Resume from silently no-oping on an idle lane session
spawnRun adopted any existing `ccam-lane-<id>` tmux session without looking
at it, so a Resume issued while the session sat at a bare shell prompt (left
by `ccam lanes shell`, or by a `claude` that had already exited) dropped the
whole argv: no `--resume` ran, no initial prompt was typed, and the API still
answered 200. The lane's DB `run_id` is not set in that case, so the ERUNLIVE
guard in the start route never saw it either.

Reuse the pane instead of erroring: when the session exists and
`#{pane_current_command}` is a shell, type the argv into that pane and record
the run. A pane running a program (a live `claude`, an editor, a build) is
still adopted untouched, so attaching shows what is running rather than typing
over it. `sendCommand` POSIX single-quotes every argument and uses
`send-keys -l`, the only place in tmux.js that composes a command line.
2026-08-18 10:50:28 +07:00
nntrivi2001 9c3331c843 docs: fix Task 12 Round 1 accuracy findings
- Fix POST /api/run param: change 'prompt' to 'initialPrompt' (Finding 1)
- Fix POST /api/run response shape: show actual fields from publicRun() (Finding 2)
- Delete stale 'Running and releasing lanes' section with old POST /api/lanes/:id/start (Finding 3)
- Delete stale POST /api/lanes/:id/message section — endpoint now returns 400 EUNSUPPORTED (Finding 4)

All changes verified against actual code:
- server/routes/run.js:285 uses body.initialPrompt
- server/lib/pty-run.js:144-156 publicRun() returns {id, laneId, status, cwd, model, ...}
- server/routes/lanes.js:1134 message action returns 400 error
2026-08-12 15:48:10 +07:00
nntrivi2001 31af7aefbf docs: update Run feature docs for the tmux+PTY terminal (was stream-json)
- README.md: replace stream-json bullet with tmux+PTY description
- ARCHITECTURE.md: update /run Workspace page section to reflect PTY transport, xterm.js, and ccam lanes shell
- docs/API.md: replace stream-json endpoint docs with /api/run/*+/ws-pty/:runId PTY endpoint docs
- docs/LANES.md: add "Attaching a real terminal to a lane" section for ccam lanes shell; update lane "start" action docs and /run Workspace description
- Remove stale references: run_stream, run_input_ack, conversation/headless modes, RunConsole components
2026-08-12 15:35:57 +07:00
nntrivi2001 7357070fb9 chore: remove unused desktop app, cloud deployment infra, and monitoring stack
Deletes desktop/ (Electron wrapper), deployments/ (Helm/Kustomize/
Terraform/CI for cloud deploy), and monitoring/ (Prometheus + Grafana
stack) along with DESKTOP.md, DEPLOYMENT.md, docker-compose.full.yml,
their npm scripts, and every dangling reference across README,
ARCHITECTURE, INSTALL, SETUP, docs/, and the repeated per-file
MODULE_GUIDE "Observability" boilerplate comment. The GET /api/metrics
endpoint itself is untouched — it's the dashboard's own route, not
part of the removed monitoring stack.
2026-08-11 12:16:54 +07:00
nntrivi2001 8fcef5a10b feat(lanes): let Add Lane choose the pipeline template
Creation is the only point the UI could ever set a lane's template, and it
never offered the choice — so every lane added from "+ Add lane" was born
on `default` and rendered an 8-node map for a 16-node workflow, with no
screen able to change it afterwards. That is the defect that made the
ship-feature template unreachable from the browser.

The modal now shows a *Pipeline template* select fed by
`GET /api/lanes/pipelines`, labelled with each template's node count so the
consequence of the choice is visible. A failed fetch degrades to a `default`
option rather than blocking lane creation.

`pipeline` was already accepted by `POST /api/lanes` but silently dropped by
`/ensure` and `/worktree`, which build their own createLane payloads; both
now pass it through, and both map `EBADPIPELINE` to 400 like `EBADCWD`.
2026-08-07 09:44:48 +07:00
nntrivi2001 67edda77eb feat(lanes): make the pipeline map track a skill's real progress
A lane's pipeline map only ever moved when a skill remembered to call
`ccam stage`, and the ship-feature template shipped with no detection rules
at all — so a lane driven by Superpowers skills sat at whatever stage it
last declared, and the `gates` node was never declared by anything.

Detection (`detect` rules on each node) now covers the Superpowers skill
invocations and the `ccam`/`gh` commands the ship-feature-lane skill
actually runs. It stays a safety net, not the mechanism: forward-only,
never `done`, never overriding a declaration. Two rules were deliberately
left out — `git diff` on `review` (this repo's own tests record it pinning
a lane at `review` on a real session) and anything on `merged`/`done`.

Stage vocabulary grows to 50 names over the same 16 nodes, following
Shipyard's PHASES shape: sub-states like `migration-collision`,
`e2e-scoped` and `gate-blocked` say WHY a lane sits on a node without the
map growing a node per reason. Every alias has a source — the skill
declares it, `default.json` uses it, or Shipyard's PHASES lists it.

Two silent failures fixed along the way:

- `lane.stages` is keyed by the raw declared string, so a stage declared
  under an alias lost its `--evidence` and rendered amber instead of
  green. `stageRecords` resolves each key onto its node.
- `ccam stage <typo>` stored fine and then rendered nowhere. It now warns
  on stderr while still exiting 0.

`ccam lanes pipeline` closes the gap that made all of this invisible: a
lane could only be assigned a template at creation, and no screen in the
web UI offers the choice, so every lane added from "+ Add lane" was stuck
on `default`'s 8 nodes. An unknown template id is now refused rather than
silently falling back to `default` on read.

Also merges the repo's own `ship-feature` skill into the Superpowers
workflow: it delegates planning/TDD/review/verification instead of
restating them, and declares a stage at each phase.
2026-08-07 09:34:20 +07:00
nntrivi2001 5d5ea32db6 docs(lanes): document proof gallery (C) 2026-08-04 16:41:56 +07:00
nntrivi2001 fcc7a8f2f3 fix(lanes): stop archive/activate from clobbering feature title and links
archiveActiveFeature was overwriting a feature's own title with the
LIVE lane's title on every clear/switch — a --title set on activation
silently disappeared. activateFeature restored stage/status/gate/CI/
stages/notes onto the live lane when switching back to a past feature,
but never links, so they vanished on reactivation. Both are fixed, with
a regression test for each (full activate/archive/reactivate round trip
for links).

Also fixes server/routes/lanes.js: the doc comment explaining GET
/:id/git's rationale had been left sitting above the newly-inserted
/:id/features routes instead of its own route.

docs/API.md's Lane features section described fields and behavior that
don't exist in the real routes (an "active" boolean, a POST response
containing "archivedPrevious", a "409 ESTALE" concurrency response) —
rewritten to match the actual request/response shapes exactly.
2026-08-04 15:17:14 +07:00
nntrivi2001 917f0794d5 docs(lanes): document per-feature state and archive (B) 2026-08-04 15:06:05 +07:00
nntrivi2001 465dca35e5 fix(docs): correct fabricated API/behavior claims in the locks docs
docs/API.md's Locks section described a timeoutMs request param, a
408-timeout response, and field names (acquiredAt/acquiredMs) that
don't exist anywhere in the actual routes.js — the server is single-shot
and never blocks; timing out is a CLI-only concept. Also fixes the
section being spliced into the middle of the pre-existing Sessions
heading and its content.

docs/LANES.md said the owner file stores an epoch in milliseconds
(it's seconds), that the CLI polls every ~1s (it's ~2s), that the lane
card shows a "waiting" state (no such server-side concept exists, only
who currently holds), and included a fabricated "manually transfer a
lock's holder identity" procedure that also contradicted the
never-touch-the-lock-directory etiquette rule stated right above it.
2026-08-04 14:02:40 +07:00
nntrivi2001 6c7554dbdc docs(locks): document cross-lane named locks (D) 2026-08-04 11:41:54 +07:00
nntrivi2001 4d9a385c5e feat(lanes): lane runtime UI, docs, and env additions (A1+A2)
Client-side rendering for the per-lane runtime facts (slot, ports,
database, Redis index, service liveness) added in the server-side
A1/A2 work, plus the doc updates (README, CLAUDE.md, docs/API.md,
client/server READMEs) describing the new profile.env keys, hook
environment contract, and REST endpoints.
2026-08-04 10:04:58 +07:00
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00