461 Commits

Author SHA1 Message Date
nntrivi2001 dab01b7f2b fix(plugin): install the client build's deps with npm ci
npm install reuses whatever the cache and an earlier run left behind and still
reports success, so a truncated cache entry produced a node_modules that was
missing typescript's lib/*.d.ts — the build then died with TS2318, every global
type undefined. npm ci starts from an empty tree and verifies each tarball
against the lockfile's integrity hash. Falls back to install when the copied
client has no usable lockfile.
2026-09-08 15:43:20 +07:00
nntrivi2001 bede2b5813 style(workflows): stack the lane pipeline and skill usage panels
Both panels carry wide tables that were fighting for half a row on lg
screens. Each gets its own full-width row instead.
1.4.6
2026-09-08 11:27:43 +07:00
nntrivi2001 fe45644b31 feat(workflows): drop the patterns list and model delegation flow
Neither panel drove a decision: the detected-pattern ranking restated what
the orchestration DAG already shows, and the delegation flow's main→subagent
model pairing is visible in the per-session drill-in. Removes both components
and their i18n blocks, promotes the error-propagation map to full width, and
renumbers the remaining sections 4-7.
2026-09-08 11:10:32 +07:00
nntrivi2001 47a8726e27 chore(client): drop dead UI code
Nothing imports SessionCard, CardSkeleton, shortModel or agentPillLabel —
the Sessions page renders its own list, formatModelName replaced shortModel,
and the UI labels agents through agentOriginLabel. Removes the symbols, their
Public surface entries, and the ARCHITECTURE mention of SessionCard.
2026-09-08 10:54:10 +07:00
nntrivi2001 2eb28f41c3 fix(ui): title the workspace tab "Workspace", not the raw i18n key
DocumentTitle mapped /workspace to nav:run, a key that does not exist, so
i18next rendered the key itself and the browser tab read "run". Point it at
nav:workspace, which both locales already ship, and add a test asserting each
top-level route resolves to a translated label.
2026-09-08 10:52:49 +07:00
nntrivi2001 c7dbaf2d9f refactor: finish the AgentHub rename and drop the MCP server
Two leftovers from the rebrand, done together because they touch the same
docs and manifests.

**Data directory.** `agent-dashboard` could not simply be renamed in the
source: it names a real directory holding the live SQLite database. Startup
now moves it once, via `migrateLegacyDataDir()` in `server/lib/claude-home.js`
— a same-filesystem rename, so it is instant even at multiple gigabytes and
cannot leave two divergent copies. `getDataDir()` stays PURE and resolves to
the legacy path until the move happens; the first version of this put the
rename inside the getter, which meant `npm run test:server` relocated the
developer's real database as a side effect. The call sits behind
`require.main === module` and ahead of the router requires, because those pull
in db.js, which opens SQLite at module load. A failed rename is silent and the
old directory keeps being used: an unfashionable directory name beats a blank
database beside an unreachable one. Same treatment for the port-discovery file
(`.agent-dashboard.json` → `.agenthub.json`, old one dropped on write).

Not renamed on purpose: the Sagi localStorage keys (renaming silently resets
everyone's prefs) and the PWA manifest `id` (changing it orphans an installed
app).

**MCP server.** 4 tool calls in 46 days across 577,132 events, against 4,556
lines of TypeScript, a 2 MB committed build artifact, and a pre-commit guard
that had already blocked commits twice. Deleted: `mcp/`, the maintainer skills
and `mcp-reviewer` agent, the build check, `plugins/agenthub/.mcp.json`,
`docs/MCP.md`, 12 npm scripts, 5 Makefile targets, and every doc section
describing it.

Kept, because they are unrelated things sharing the name: the Config
Explorer's MCP tab and its API, per-lane Playwright MCP, the agenthub-config
plugin's MCP audit, and the historical `mcp__plugin_agenthub_*` events that
analytics must keep attributing.

Verified: server 1432/1432, client 459/459, tsc clean, prettier clean. Both
directions of the migration proven — a sandbox-HOME test run leaves the legacy
directory untouched, and a real boot moves it with its contents intact.
2026-09-08 09:48:40 +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 ae9b3dbcd5 fix(cc-watcher): stop burning 65k inotify watches on the plugin cache
`plugins` sat in WATCH_SUBDIRS, so it got fs.watch({recursive:true}).
On Linux that's a userland tree walk costing ONE inotify watch per
directory, and ~/.claude/plugins is the plugin cache: full git checkouts
with node_modules, 44k+ directories. The dashboard process held 65,464
watches against the default fs.inotify.max_user_watches of 65536,
leaving none for systemd/IBus — IBus exited on boot and Unikey died
with it.

Config Explorer only reads two top-level files out of that tree
(installed_plugins.json, known_marketplaces.json), so watch plugins/
flat: 1 watch instead of 44k. The small config subdirs stay recursive.

Measured on the running service: 65,464 -> 96 watches, and touching
installed_plugins.json still broadcasts cc_config_changed.
2026-09-07 08:32:19 +07:00
nntrivi2001 3223c8edac feat(lanes): run ONE pipeline node, from the terminal or a slash command
A pipeline was all-or-nothing: `ccam ship-full` / `ship-quick` hand the whole
thing to a skill, and a node's own `command` — added so the map would stop
hiding the driver behind its detect regexes — was display-only. Re-running just
the CI gate, or redoing the brainstorm, meant clicking copy on the node and
pasting the string into a session by hand.

`ccam lanes node [<id>] [extra…] [--print]` runs it, in the lane's own cwd. A
slash-command node spawns `claude` with it as the prompt (same shape as
`ccam ship-full`); anything else is spawned directly. No id lists every node
and its command, which is also what the two error paths print.

The node is matched by id OR alias, the same way `ccam stage` matches one:
`default` calls the step `implement` and `ship-feature` calls it `implementing`,
and one command name must not depend on which template the lane happens to be
on. That needs the template's `aliases`, which the lane payload's
`pipeline_nodes` does not carry — so the two are merged, template for the
definition, lane for the `current` marker.

Both shipped templates now fill EVERY node's `command`. The seven that were
empty are the nodes whose only work IS the declaration — a terminal `done`, a
gate another agent declares — so they get `ccam stage <id>`, which keeps all 16
(and all 8) runnable instead of leaving holes a caller has to know about.

Slash commands for the same thing, because a session should not shell out to a
nested `claude`: `/intake`, `/plan`, `/implement`, `/tests`, `/e2e`,
`/e2e-passed`, `/review`, `/qc-plan`, `/qc`, `/gate`, `/publishing`, `/pr-open`,
`/reported`, `/watching-pr`, `/merged`, `/done`, plus `/ship-full` and
`/ship-quick`. Each resolves its node through `ccam lanes node <id> --print` and
runs the result IN the current session — invoking the slash command itself when
that is what the node names, otherwise running the shell argv. Resolving at call
time rather than baking the string into the file is what stops the 18 command
files from drifting when a template changes.

Two limits are deliberate:
  - The command is split on whitespace and spawned WITHOUT a shell. A template
    is data — anyone can drop a file into DASHBOARD_PIPELINES_DIR — so its
    `command` must never be interpreted as shell. A node needing a pipe names a
    script that contains one.
  - Running a node does not declare its stage. A command executing is not proof
    the node passed, same boundary as the console never writing a lane's stage.

Verified: npm run test:server (1403, +3 covering alias matching, full command
coverage of both templates, and every command file being listed in the
manifest), npm run test:client (459), prettier and check-headers clean. Docs:
docs/LANES.md § Running ONE node, docs/PLUGINS.md commands table, README +
README.vi CLI blocks, CLAUDE.md marker.
2026-09-04 15:14:53 +07:00
nntrivi2001 58774f211a fix(sagi): anchor mood animations to the mascot, and let the bubble follow it
The seven per-mood `transform-origin: 640px 720px` rules were written in
mascot.svg's 1280-unit space but applied to the root <svg>, where CSS
resolves a length against the 68px CSS box. The pivot landed ~9x the
element away, so `scale(1.035)` ("gentle breathing") swung the whole
mascot ~20px up-left every 4s - and the speech bubble, positioned from
the untransformed box, stayed put while Sagi visibly moved.

- One `transform-origin: 50% 92%` on `.sagi-avatar` (feet pivot); a
  percentage so it cannot be misresolved against either reference box.
- Eye-blink and sparkle origins rebased onto `transform-box: view-box`,
  whose 0,0 is the viewBox min (253.5 106) - the eyelids were closing
  off-target and the sparkle twinkled around a point outside itself.
- `sagi-rock` returns to 0deg; it used to park `thinking` at a
  permanent -6deg tilt.
- `.sagi-flyout` gets the avatar button's exact left/top easing so the
  bubble glides with the snap instead of teleporting, and the bubble no
  longer unmounts mid-drag (the panel still does - dragging a 320px
  panel around is jank).
- Reduced motion now also covers `.sagi-bubble-enter` and both
  transitions; dropped a `~ *` selector that never matched.
2026-09-04 14:40:45 +07:00
nntrivi2001 6cb7f4f843 feat(plugin): install the pipelines' skill plugins on SessionStart
The ship-feature pipelines drive `/superpowers:*` commands as their per-node
drivers, so a fresh install had a dependency it was only told about — the
`/ccam-doctor` WARN row and a line in the README.

`server/lib/skill-plugins.js` now carries the whole thing, shared by the
doctor (reports) and the bootstrap (installs) so the two cannot disagree
about what is missing. The bootstrap installs at the very end of its DETACHED
worker — the install clones from the network and nothing in the 10 s
SessionStart hook may wait on it — retrying once behind a `marketplace add`,
logging a failure with the manual command, and never throwing: the dashboard,
hooks, CLI and MCP all work without these plugins. The outcome is recorded as
`state.skillPluginsSettled`, which `fastPathOk()` now also requires, so the
check runs exactly once per install and `/ccam-update` is the retry.
`CCAM_AUTO_INSTALL_PLUGINS=0|false|off` opts out.

Reporting is derived, installing is allowlisted. Requirements come from the
loaded templates, which is right for a report; installing from that same
source would not be, because a template is a file anyone can drop into
`DASHBOARD_PIPELINES_DIR` — it would turn "edit a JSON file" into "run this
vendor's install command on the next session". Auto-install consults
`AUTO_INSTALLABLE`, a constant in code (`superpowers` today), and reports
every other requirement as skipped.

Tests inject a `run` seam so no case reaches a real `claude plugin install`,
and cover the refusal explicitly.
2026-09-04 14:03:12 +07:00
nntrivi2001 413b3f40b2 feat(doctor): report skill plugins the pipelines name but nothing provides
The ship-feature pipelines have always depended on the `superpowers`
plugin — skills/ship-feature/SKILL.md calls its skills by name, and the
node detect rules match `Skill` events named `brainstorming`,
`writing-plans`, `test-driven-development` — but no README, INSTALL,
manifest or SKILL.md said so, and /ccam-doctor did not check. Install the
`ccam` plugin without it and `/ccam:ship-feature-lane` runs partway, then
fails at the first skill call with no warning beforehand.

/ccam-doctor now derives the requirement from the loaded pipeline
templates rather than a hardcoded list: every `/plugin:skill` a template
names, as its own `skill` or as one node's `command`, has to come from an
installed and enabled plugin. Editing a template cannot leave the check
behind, and a custom template in DASHBOARD_PIPELINES_DIR naming some
other plugin is covered for free.

Three deliberate calls:
  - WARN, never FAIL. The dashboard, hooks, CLI and MCP all work without
    it; failing a doctor run over it would cry wolf.
  - A plugin present but explicitly disabled counts as missing — Claude
    Code will not resolve its skills either way.
  - The skills sitting in ~/.claude/skills/ count as satisfied. That is a
    legitimate hand-copied setup, and calling it broken is how a warning
    row teaches its reader to ignore it.

Docs state the prerequisite where someone installing would look: README
and INSTALL requirements, plus docs/PLUGINS.md for how the row decides.
The install line is `superpowers@claude-plugins-official`, verified
present in that marketplace's manifest.

Verified: npm run test:server (1379, +6 covering all three calls above
plus the partially-satisfied case), npm run test:client (459), prettier
and check-headers clean. `node scripts/plugin-doctor.js` here reports
`OK Skill plugins superpowers available`; the WARN branches were checked
against a temp CLAUDE_HOME.
2026-09-04 11:44:53 +07:00
nntrivi2001 6df69d8811 feat(lanes): give every pipeline node its own driver command
A pipeline template named ONE command for the whole run (`skill`), so the
only way to know what actually moves a given node was to read that node's
`detect` regexes. Each node now carries its own `command`: a slash command
(`/superpowers:writing-plans`) or a shell one (`ccam lanes hook ci-gate`),
whichever its detect rules already watch for.

The values are derived from each node's own detect rules, not invented —
every `ccam …` string was checked against bin/ccam.js and every
`/superpowers:*` against the installed plugin. Nodes reaching which is a
declaration rather than an action (`done`, `merged`, `reported`,
`e2e-feature-passed`, `qc-plan`) are deliberately left empty.

`pipelines.js` normalizes a missing or non-string value to "", so a
custom template written before this field keeps loading rather than
rendering "undefined". `nodeStates()` and GET /api/lanes/pipeline-stats
pass it through. Never executed by the dashboard — display text, the same
contract `skill` already had.

Surfaced where each view has room for it: the Workflows pass-rate panel
shows it under the node label and copies it on click; PipelineMap folds
it into the node tooltip, because a 16-node pipeline has to fit across
one card and there is no width for a second line.

`command?: string` is optional in the client type only to spare four
unrelated test fixtures — the server always sends the key, and the UI has
to branch on the empty string regardless.

Verified: npm run test:server (1373, +3), npm run test:client (459, +2),
tsc --noEmit, prettier and check-headers clean. docs/LANES.md § Custom
template format documents the field. openapi.yaml needs no change: the
lane payload and pipeline-stats were never schematized there.
2026-09-04 11:43:43 +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 965c62c5cf fix(workspace): stop the console remounting, and the idle polling re-renders
The Workspace page rendered `ConsoleArea` from two sites, picked by
`currentLane`. Every flip of that value unmounted the whole console
subtree and mounted it again at the other site: the terminal died, the
run history went with it, and `LaneConsolePane`'s lane-switch effect
wiped a half-typed prompt. It flipped on every page load (lanes arrive,
`refreshLanes` selects `lanes[0]`), on a lane being removed, and on any
refetch that re-picked the selection.

`lane-detail` is now always rendered and holds the only `ConsoleArea`;
the header and info block are the conditional parts. Same rule the
RunConsole already followed one level down — collapse with CSS, never by
unmounting — now enforced above it too.

Second half: every poller fed `setState` a brand-new object graph each
tick, re-rendering the lane strip, the card and the console area several
times a minute on a completely idle dashboard. `keepIfSame` keeps the
previous reference when the payload is value-identical, applied to the
5 s run/session poll, the lane list and counts, the `lane_update` WS
branch, and LaneCard's git/runtime/locks pollers.

Verified: npm run test:client (44 files / 454 tests), tsc --noEmit
clean, check-headers.sh clean. The Run screen snapshot was regenerated —
the diff is the added `<section>` wrapper and one indent level, no
content change.
2026-09-04 10:48:22 +07:00
nntrivi2001 e109d2804c feat(lanes): auto-advance a codex lane's stage from its rollout detection
A codex session emits no hooks and rarely calls `ccam stage` on its own, so
its card sat at `idle` forever while the amber "detected" badge was the only
thing that ever moved. `codex-ingest.js` now advances the declared `stage`
from its own written detection — the one place in CCAM where an inference
moves the blue `current` ring, and only for `codex` lanes.

It stays honest: gated on `recordDetection` having written (so forward-only,
declared-wins and the detection TTL are inherited unchanged), passes NO
evidence (the node renders `passed-no-evidence` amber, never `done`), and no
template's last node carries detect rules, so a lane can never be
auto-declared `done`. `setStage` grows a `keepDetection` option — its sole
caller — so the detection columns survive the advance and the card keeps
showing which command moved it instead of looking like an agent's own claim.

Two levers then get the evidence auto-advance cannot supply:

- `codex-nudge.js` now nudges on a stage held 2+ min with no evidence,
  instead of on a detection sitting ahead of the declared stage — after
  auto-advance the old condition can never fire, since the two now match.
- `ccam lanes agents install` on a codex lane writes the marker-delimited
  `<!-- ccam:stage:start -->` block into the lane's own AGENTS.md — the one
  instruction channel Codex reads — instead of copying agent templates into
  `.claude/agents/`, which it never loads. Appended, user prose preserved,
  reinstall replaces exactly that block.

Verified with a real `codex exec` run: stage moved to `watching-pr` with
`evidence: null` and the detection signal intact. 1364 server tests pass.
2026-09-03 16:51:02 +07:00
nntrivi2001 8a812a174b fix(lanes): keep stage detection honest across rollbacks and feature switches
Three defects let a lane's pipeline map claim progress that was not this
pass's work, all of them on the "going back" path a new request takes:

- `setStage` cleared the detection columns on a real transition but never
  pruned the `stages` records AHEAD of the new stage. A lane that ran through
  `done` and was then declared back at `plan` rendered `plan` as `current`
  while `tests`/`review`/`done` still read `done`, carrying the previous
  pass's evidence, and contradicting `progress` (computed from the declared
  index alone). A backwards declaration now drops every record it can prove
  is ahead; a key matching no node is unplaceable and left alone.
- `activateFeature` rewrote the live lane row for the incoming feature but
  carried the outgoing one's `detected_*` across. A never-seen slug starts at
  `stage='idle'`/`stages={}`, exactly the shape `inferredDisplayStage` renders
  from detection, so the new feature opened painted with the old one's
  progress — and forward-only rejected every real detection for it until the
  carried-over one aged past DETECTION_TTL_MS. Now cleared, like every other
  real transition; deliberately not archived, since detection is live-only.
- `codex-ingest` classified a rollout journal by raw `now - mtime`, which goes
  negative for a future mtime (clock stepped back, NTP correction, a restore
  that preserved timestamps) and read as "written milliseconds ago". A
  finished thread's journal could replay as live work; a file claiming to be
  from the future is now idle.

Also fixes `check-headers.sh`, which audited via `find` and so reported local
gitignored runtime state (a lane's own `.ccam/profile/hooks/*.sh`) that is not
this repo's source and can never be made compliant here. It now lists files
through git (tracked + untracked-but-not-ignored), so a file an agent just
wrote is still caught before it is staged while ignored state is not.

Tests: 1359 pass (4 new). Header audit exits 0.
2026-09-03 15:29:08 +07:00
nntrivi2001 13b2bc4ab6 fix(service): forward PATH into the ccam systemd --user unit
systemd --user units start with systemd's own minimal PATH, not the
installer's shell PATH, so the dashboard couldn't find claude/codex
(e.g. installed under ~/.local/bin) when spawning a lane from a
service-managed server. Forward process.env.PATH into the unit's
environment from both ccam service install and the plugin bootstrap's
auto-install path.
2026-09-03 11:29:24 +07:00
nntrivi2001 8711917d44 fix(lanes): render undeclared lanes at their detected stage, quiet tmux polling noise
A lane whose sessions never call `ccam stage` had no declaration at all, so
its pipeline map stayed 100% pending even though detection was tracking real
progress. inferredDisplayStage renders such a lane AT its last detection
instead (display only — the DB's declared `stage` and detection-as-evidence
rules are untouched). Also stops tmux's own "no session" stderr noise from
spamming the journal on every poll.
2026-09-03 11:28:06 +07:00
nntrivi2001 b27e679eac docs: sync README.vi.md and INSTALL.md with the systemd auto-start section
README.md already documented ccam service install/status/uninstall; the
Vietnamese mirror and the full install guide had gone stale.
2026-08-28 16:48:06 +07:00
nntrivi2001 1cb7ac0a4b feat(install): auto-start the dashboard on boot via systemd, no Claude Code needed
Adds ccam service install/uninstall/status for checkout installs and wires
the same systemd --user unit into plugin-bootstrap.js, so plugin-installed
colleagues get boot persistence too instead of needing a Claude Code
session to (re)start the server after every reboot.
2026-08-28 16:15:45 +07:00
nntrivi2001 8bd74a57e6 feat(lanes): prime and nudge codex lanes into declaring their own stage
A codex lane's declared stage has always depended entirely on the session
reading AGENTS.md and calling `ccam stage` on its own — no hook, no wrapper,
no enforcement. Two additions make that more reliable without touching the
declared-wins / inferred-never-done invariants:

- LaneConsolePane pre-fills a codex pane's opening prompt with a plain-text
  stage-declaration checklist built from the lane's own pipeline_nodes —
  the same up-front priming a Claude lane gets from its pre-filled
  /ccam:ship-feature-lane skill command, just as prose since Codex has no
  slash commands.
- server/lib/codex-nudge.js sweeps every 60s and types a one-line reminder
  into a lane's live tmux pane (tmux.sendText, a new literal-text primitive
  alongside sendCommand) when its fresh detected_stage has sat undeclared
  for over two minutes. It never writes stage/detected_stage itself — any
  resulting declaration is still the codex session's own real ccam stage
  call, just server-prompted instead of relying on memory alone.

server 1354/1354, client 449/449, tsc clean.
2026-08-28 15:59:22 +07:00
nntrivi2001 5c297c6483 fix(run): pin the lane terminal font so Vietnamese glyphs never fall back
xterm left fontFamily unset, so it used the browser/OS default
(courier-new/monospace). That face is missing Vietnamese precomposed
diacritics on many systems, so the browser silently substitutes those
glyphs from another face with a different advance width — corrupting
the cell even under the canvas renderer, which only stops a glyph from
shoving its neighbours, not from overflowing its own cell. Pin the
terminal to the app's own bundled JetBrains Mono and import its
vietnamese subset (previously only latin was loaded).
2026-08-28 13:18:12 +07:00
nntrivi2001 52fbd852af feat(sagi): flesh out mood animations and add avatar interaction motion
Gives watching/thinking real animations (previously static), adds
character to happy/worried/stuck moods, and adds mount-in/press/error-pulse
motion to the avatar button - all gated by prefers-reduced-motion.

fix(test): pin NODE_ENV=production in client-dist-override.test.js

The suite exercises server/index.js's production-only static-serving
gate but never set NODE_ENV itself, so it silently depended on the
ambient shell's NODE_ENV - failing whenever that wasn't "production".
2026-08-28 11:17:27 +07:00
nntrivi2001 cc4e71c127 fix(run): draw the lane console through the canvas renderer
Scrolling a console left stray characters glued to the left edge of blank
lines. Copying an affected line yields clean text, so the buffer was always
correct — only the painting drifted, which no CSS can correct.

xterm's default DOM renderer aligns each row with a single `letter-spacing`
value. That holds only while every glyph comes from one monospace font, and
TerminalView passed no fontFamily, so xterm used its default
`courier-new, courier, monospace`: Vietnamese diacritics and CJK fall
through to a fallback face whose advance width differs, glyphs shove their
neighbours along, and the row's tail spills onto the row below.

The canvas renderer draws every cell at a fixed position, so a wide glyph
can no longer displace its neighbours. Loaded best-effort — a browser that
cannot hand back a 2D context keeps the DOM renderer rather than losing the
terminal entirely.

Ruled out first, each by replaying the real byte stream into xterm.js and
diffing against `tmux capture-pane`: wrapped Vietnamese at the margin, mixed
CJK, wheel scrolling through copy-mode, and a live 131->95->131 resize all
render byte-identically at matched widths.

client 448/448, tsc clean.
2026-08-28 10:44:17 +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 82263a0ab5 fix(lanes): revive dead detect rules and stop alias-blind detection badges
Two defects that made the 16-node ship-feature pipeline look like stage
detection was broken:

- stage-detect matched each rule against a JOIN of the tool input's
  identifying fields. An Agent event really carries description + prompt +
  subagent_type, so `^Explore$` (intake) and `^Plan$` (plan) could never
  fire and both nodes shipped inert — the map jumped straight to
  `implementing`. Rules are now tested one field at a time, which also
  stops a rule matching across the seam between two unrelated fields.

- `detectionLeadsDeclaration` (bin/ccam.js) and `detectionLeads`
  (LaneStripCard.tsx) located the declaration with `indexOf(lane.stage)`.
  Most of what the skill declares is an ALIAS (`tests`, `booting`, `ship`,
  `gate-blocked`), which is not a node id, so indexOf answered -1 and every
  detection — however far behind — was badged as leading. Both now use the
  node the server already resolved it onto (`state === "current"`).

server 1346/1346, client 447/447.
2026-08-28 10:05:24 +07:00
nntrivi2001 fd7f57929c fix(lanes): auto-detect and report stage for codex lanes (issue 11)
A lane on the 16-node `ship-feature` template showed an untouched pipeline
map and never declared a stage. Four independent causes, fixed at their
shared choke points:

1. Codex emits no hooks, so `touchLaneFromHook` — the only caller of
   `stage-detect.detect()`, and correctly pinned to `claude` — could never
   see a `codex` lane. New `server/lib/codex-ingest.js` tails Codex's own
   rollout journals (`$CODEX_HOME/sessions/**/rollout-*.jsonl`), maps its
   tool calls onto the hook-shaped `{tool_name, tool_input}` the matcher
   already understands, and feeds the existing detect path plus an
   idle/running status mirror under the same guard hooks use. Polled every
   `DASHBOARD_CODEX_POLL_MS` ms (default 15000, `0` disables).
2. `resolveLaneArg` matched on cwd alone, so with a claude and a codex lane
   at one directory (the composite `idx_lanes_cwd_tool`) a `ccam stage`
   call could drive the sibling lane's card. It now resolves by
   `--lane` → `CCAM_LANE_ID` → `--tool`/harness marker → owning lane, and
   errors naming both candidates rather than picking one. Every pane the
   dashboard starts carries `CCAM_LANE_ID` (`tmux new-session -e`, or an
   assignment prefix when typing into a live shell pane).
3. `recordDetection`'s declared-wins veto had no exit: a lane left at the
   pipeline's last node rejected every detection for the rest of its life.
   A declaration on the LAST node no longer vetoes — the pipeline is over
   there, so a later detection is new work. Nothing else is relaxed, and
   detection still never writes `stage` nor renders `done`.
4. The run console pre-filled the pipeline's Claude slash command into
   codex panes, which Codex reads as prose — so the skill that would have
   called `ccam stage` never ran. Claude lanes only now.

A codex lane's *declared* stage still depends on the session calling
`ccam stage`; `AGENTS.md` is the only lever CCAM has, so it now says so.

Verified: server 1345/1345, client 441/441, and end-to-end against a real
codex lane row (detected_stage/detected_signal set, declared stage
untouched, no node rendered done).
2026-08-27 16:38:23 +07:00
nntrivi2001 b1734a492f feat(run): tool-aware model picker and run panel labels for codex lanes
Model dropdown now shows curated Claude vs Codex models per lane's tool,
and the run panel title/subtitle/binary-missing text switch between
"Chạy Claude"/"Chạy Codex" accordingly.
2026-08-27 13:33:55 +07:00
nntrivi2001 ccf2fdb8eb feat(lanes): let claude and codex lanes coexist at the same cwd
Repo/Adopt mode (POST /api/lanes/ensure) treated lanes.cwd as globally
unique, so adopting a path already driven by one CLI silently ignored a
different tool choice. Uniqueness moves to the composite (cwd, tool):
one directory can now back a claude lane and a codex lane side by side.

- server/db.js: drop the column-level UNIQUE on cwd, migrate existing
  DBs via table rebuild, add idx_lanes_cwd_tool.
- resolveLaneByCwd gains an optional tool filter; hook ingestion always
  pins to "claude" since Codex has no hook mechanism here, so a hook
  event never touches a codex lane sharing the path.
- POST /ensure resolves the existing owner by (cwd, tool) instead of
  cwd alone, so a different tool creates a sibling lane.
- LaneConsolePane's cwd-only lane lookups now also match tool, so a
  shared cwd doesn't attach a run to the wrong sibling lane.

Worktree mode is unaffected (already allocates a unique directory per
lane). docs/LANES.md and CLAUDE.md updated to describe the new
composite uniqueness.
2026-08-27 13:06:08 +07:00
nntrivi2001 f8fba0a51d docs: sync CLAUDE.md and READMEs with codex lanes and the tmux run surface
Add the codex-lane facts and doc-sync marker to CLAUDE.md, document the
per-lane tool picker in README/README.vi, and bring server/README.md and
client/README.md's API and routing tables up to date with the tmux-backed
run endpoints and the /workspace route.
2026-08-27 10:59:18 +07:00
nntrivi2001 d81b229f79 refactor(run): drop unused CODEX_SESSION_RE constant 2026-08-27 10:58:01 +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 0d53603d9e fix(run): update file headers to reflect per-tool setup, add resume-last test 2026-08-27 10:54:16 +07:00
nntrivi2001 abf3181843 feat(run): per-tool run setup fields and binary probe 2026-08-27 10:53:03 +07:00
nntrivi2001 3891f64f04 docs(lanes): update AddLaneModal description to include tool selection 2026-08-27 10:51:41 +07:00
nntrivi2001 810c28cdf6 feat(lanes): pick Claude Code or Codex when adding a lane 2026-08-27 10:50:25 +07:00
nntrivi2001 02050ac0ad docs(client): add new exports to module catalogs 2026-08-27 10:49:08 +07:00
nntrivi2001 13b188d54f feat(client): carry the lane tool through run fixtures 2026-08-27 10:47:45 +07:00
nntrivi2001 a1b9accb3a feat(client): type the lane tool and codex run args 2026-08-27 10:45:38 +07:00
nntrivi2001 2206bdbd2c fix: update run.js header to reflect claude and codex support 2026-08-27 10:43:18 +07:00
nntrivi2001 ce830094f0 feat(api): accept a lane tool on create, start and binary probe 2026-08-27 11:00:13 +07:00
nntrivi2001 e8070f04d1 feat(run): spawn codex in a lane's tmux pane 2026-08-27 10:39:00 +07:00
nntrivi2001 a66331f008 feat(lanes): record which CLI tool drives a lane 2026-08-27 11:00:13 +07:00
nntrivi2001 97839b1f97 docs: spec and plan for codex lanes 2026-08-27 10:36:46 +07:00
nntrivi2001 a75e90dea5 feat(workflows): rename Workspace route from /run to /workspace, capitalize pipeline node labels
Titlecase all default/ship-feature pipeline node labels for consistent display.
2026-08-26 11:32:03 +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