Commit Graph

86 Commits

Author SHA1 Message Date
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 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 ed418d5696 fix(codex): treat a literal null from the CLI as an empty list, not a 500 2026-09-09 01:29:17 +07:00
nntrivi2001 42db57ee18 fix(codex-config): stop rejecting env values that start with "-"
--env's value is the RHS of one KEY=VALUE argv element, never its own
argv position, so a leading "-" can never be parsed as a flag — unlike
the key, which stays validated as an identifier. assertArg's leading-dash
guard was reused for the value too, refusing ordinary values like "-1".
Split it into assertEnvValue (no leading-dash check) for values only.
2026-09-09 01:22:12 +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 4d4449866b fix(codex): let plugin remove take the same spec plugin add and list use 2026-09-08 21:37:41 +07:00
nntrivi2001 aca8fd02ee fix(codex): stop 502ing empty CLI output, GET /file no longer mkdirs or leaks paths 2026-09-08 20:15:12 +07:00
nntrivi2001 29ed86d672 fix(codex): close symlink escape on GET /file, pin project writes to server cwd 2026-09-08 20:07:57 +07:00
nntrivi2001 58623f37c0 fix(codex): close parent-dir symlink escape in the containment check 2026-09-08 17:43:09 +07:00
nntrivi2001 f3a2f41ed7 fix(codex): close symlink write escape and validate listBackups scope/type 2026-09-08 17:34:10 +07:00
nntrivi2001 c9541dc509 feat(codex): write Codex artifacts with backup and atomic rename 2026-09-08 17:22:31 +07:00
nntrivi2001 dcd65f891c fix(codex): support mcp add --url form, guard specs/URLs, cover --env 2026-09-08 17:15:24 +07:00
nntrivi2001 ba79610697 feat(codex): wrap the codex mcp/plugin CLI in argv-safe calls 2026-09-08 17:06:44 +07:00
nntrivi2001 b0788eea80 fix(codex): backslash parity in the TOML closing-quote scan
Round 2's escape check rejected a quote whenever a single backslash
preceded it, so an escaped backslash before a real closing quote (e.g.
path = "path\\" # c) was misread as an escaped quote and the field was
dropped. Now counts the run of backslashes and only treats an ODD run as
escaping the quote. Folded the old two-branch quote handling into one
scan (it already finds the terminator correctly with or without a
trailing comment) and decode \\ -> \ in the extracted value, since the
scan already knows which backslash run is real.
2026-09-08 16:57:02 +07:00
nntrivi2001 236e94760f fix(codex): escape-aware closing-quote scan, honest symlink coverage
readTomlMeta's trailing-comment stripper found the first quote character
regardless of escaping, so a description containing \"escaped quotes\"
before a real comment got silently truncated instead of dropped. The scan
now skips a quote preceded by a backslash.

The "never walks outside" containment test passed by construction (row.file
is always built off a fixed root) and would not fail if the isUnder guard
were removed. Replaced it with two tests that actually pin the regression
risk: a skill dir and an agent .toml symlinked in from outside CODEX_HOME,
asserting the row is discovered and its content read through the link.
isUnder stays as a defensive check (matches cc-discovery.js), now commented
as such.
2026-09-08 16:48:45 +07:00
nntrivi2001 98b1921ca6 fix(codex): keep commented TOML fields, follow symlinked config surfaces
readTomlMeta was dropping any field with a trailing `# comment` (common in
real agents/*.toml). readSkills/readAgents/readRules used bare
isDirectory()/isFile(), which miss symlinked entries per cc-discovery.js's
own documented Dirent quirk — reused its isDirLike/isFileLike instead
(exporting isDirLike from cc-discovery.js, which wasn't previously public).
Also swapped the "never walks outside" test for one that actually exercises
a symlink escape, and dropped the unnecessary isUnder re-export.
2026-09-08 16:41:46 +07:00
nntrivi2001 7618766861 feat(codex): read the Codex config surfaces 2026-09-08 16:32:06 +07:00
nntrivi2001 57849d2f75 refactor(codex): give the CODEX_HOME lookup its own module 2026-09-08 16:24:17 +07:00
nntrivi2001 aef7ec130b refactor(config): extract the safe-write primitives cc-mutate guards with 2026-09-08 16:17:48 +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 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 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 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 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 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 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 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 d81b229f79 refactor(run): drop unused CODEX_SESSION_RE constant 2026-08-27 10:58:01 +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 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 8c89e73ebd Merge remote-tracking branch 'origin/main' into feat/pipeline-driven-ship-commands
# Conflicts:
#	server/__tests__/lanes-cli.test.js
2026-08-25 11:17:33 +07:00
nntrivi2001 0343e706a7 fix(server): revert unnecessary gh.js env change from Task 4 2026-08-25 09:38:09 +07:00
nntrivi2001 a2fcd9bafc feat(cli): add ccam lanes ci status 2026-08-25 09:33:26 +07:00
nntrivi2001 8c76ce708e feat(server): derive CI state from a single gh run snapshot 2026-08-25 09:21:25 +07:00
nntrivi2001 62789ffae7 feat(server): add gh execFile wrapper for CI status 2026-08-25 09:04:28 +07:00
nntrivi2001 2103dde889 feat(lanes): drive runs from the pipeline template's own skill
A pipeline template may now declare the slash command that drives it
(`skill`), so the choice of pipeline is the choice of workflow rather
than just a picture on the dashboard:

- `ccam ship-full "<req>"` runs the 16-node `ship-feature` template's
  skill (`/ccam:ship-feature-lane`) — through gates, QC and the senior
  gate to a published PR, watched until a human merges it.
- `ccam ship-quick "<req>"` runs the 8-node `default` template's skill
  (`/ccam:ship-feature`) — in-repo feature work that stops at the gate.

Each command puts the lane on its own template before spawning, so the
dashboard renders the right node count from the first stage instead of
after the skill self-corrects. `--print` shows the command, switches
nothing and runs nothing. The prompt reaches `claude` as one argv
element, never through a shell.

The same field feeds the Workspace run console, which pre-fills its
prompt with the lane's driving skill and never overwrites a prompt the
user has already edited.
2026-08-24 16:34:55 +07:00
nntrivi2001 d279af85d5 fix(lanes): expire stale stage detections, cover raw test runs in ship-feature gates
withDetected() rendered lane.detected_stage forever once written — the TTL in
recordDetection only stops future writes from advancing past a stale floor, it
never clears the row, so a lane gone quiet kept painting amber nodes from
activity that was no longer current. Also add the shared test-runner regex to
the ship-feature pipeline's `gates` node: Stage 1b's independent-test subagent
runs the repo's test command directly, not through `ccam`, and had no
detection fallback if `ccam stage tests` was skipped.
2026-08-24 10:43:43 +07:00
nntrivi2001 510f015fdf feat(plugin): ship the agents from a single root agents/ directory
Move .claude/agents/ and server/data/agent-templates/ship-feature-lane/
into one agents/ at the repo root — which IS the plugin root, so all five
agents now ship with the ccam plugin as ccam:<name>, the same way skills
already do.

The two pipeline agents were byte-identical copies in both places; that
duplication is gone. `ccam lanes agents install` now reads them straight
from agents/, so the per-lane copy in <lane>/.claude/agents/ still gives
the bare `subagent_type: qc-local` the ship-feature-lane skill prefers.
Skill and docs record both names: bare inside a lane, ccam: as fallback.
2026-08-21 17:30:04 +07:00
nntrivi2001 b7c573f8d9 feat(plugin): ship the repo's skills as ccam plugin skills
The repo root already IS the `ccam` plugin root (`"source": "./"`), so the
conventional `skills/` dir at the root is the plugin's skills dir — while
`.claude/skills/` only loaded for a session whose cwd was inside this
checkout. That is useless for a lane working in another repo, which is why
`ccam skills install` existed at all.

Move all seven skills to `skills/`: they now reach every plugin user as
`/ccam:<name>` (bare `/<name>` as an alias). `ccam skills install` stays as
the fallback for a plain checkout without the plugin, sourcing from the new
path.

Agents deliberately do NOT move. A plugin agent is addressed as
`ccam:qc-local`, which would break the `subagent_type: qc-local` references
in ship-feature-lane; `ccam lanes agents install` keeps writing them into the
lane's own `.claude/agents/`, the highest-priority agent source. MCP was
already plugin-shipped via `plugins/ccam/.mcp.json`.

Mechanical fallout: ~115 source files carry a doc-comment pointer to
`.claude/skills/file-headers/`, repointed to `skills/file-headers/`. The
generic Claude Code config-surface examples in openapi/cc-config (`.claude/
skills/code-reviewer`) describe any user's layout and are left alone.

Also fixes the two file-headers scripts, whose `ROOT=` climbed four levels
from `.claude/skills/file-headers/scripts/` and would have escaped the repo
from the new depth — it scanned sibling projects until corrected to three.

npm run test:server (1160 pass), test:client (422 pass), mcp:typecheck,
mcp:build + mcp:check-build all green.
2026-08-21 17:17:33 +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 aaa67da394 fix(run): turn on tmux mouse mode on every attach, not just at session creation
Sessions created before the mouse-on fix landed (or before the server
restarted to pick it up) never got the set-option call, so their pane
still had no drag/wheel scroll. Moving it into pty-attach's attach()
makes it idempotent per-connection instead of once-at-birth.
2026-08-19 11:32:23 +07:00