Commit Graph

61 Commits

Author SHA1 Message Date
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
nntrivi2001 8aa22cd255 fix(run): enable tmux mouse mode so the run terminal's scrollbar actually drags
Session panes had a scrollbar track but no way to drag it — wheel/drag
events passed straight to the running program instead of entering tmux's
copy-mode scrollback. xterm.js already forwards mouse-tracking escapes
once tmux advertises them, so turning mouse mode on per-session is enough.
2026-08-19 10:05:18 +07:00
nntrivi2001 8e49d2b300 feat(updates): auto-apply and self-restart on new dashboard versions
Adds POST /api/updates/apply (pull, rebuild, restart) plus an "Update now"
button and hourly auto-check in the UI, for checkouts that are safely
fast-forwardable. Reintroduces self-restart (previously removed in edc25ca
for cross-environment reliability concerns) per explicit user request.
2026-08-18 16:04:27 +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 18a42873b2 fix(mcp): fall back to lane cwd when source_repo is null
Adopted lanes have source_repo = null; lane-mcp.js passed it raw to
readSourceMcpServers and threw ENOMCPCONFIG. Match the existing fallback
pattern in lane-env.js/lane-profile.js.
2026-08-13 15:36:36 +07:00
nntrivi2001 7e2bb6225f fix(run): forward text-frame keystrokes to pty and fix ws-pty upgrade dispatch
xterm.js's onData hands the browser a plain string, and WebSocket.send(string)
always emits a TEXT frame — pty-attach.js only forwarded BINARY frames to
pty.write(), so every keystroke was silently dropped as an unparseable JSON
control message. Now any non-control text frame reaches the pty.

Deeper root cause of the terminal never accepting input at all: the /ws
WebSocketServer used the {server, path} shorthand, whose own internal
upgrade listener calls handleUpgrade() for every upgrade on the shared
http.Server and aborts with 400 on a path mismatch — killing /ws-pty/*
upgrades before the PTY server's own listener ever ran. Switched /ws to
noServer + a manual path-checked dispatch, matching /ws-pty's pattern.
2026-08-13 11:14:18 +07:00
nntrivi2001 774ee48f19 fix(run): switch node-pty to @lydell/node-pty for the plugin's --ignore-scripts install path
node-pty ships prebuilt binaries for darwin/win32 only — on Linux it
needs a native build via its install script. The plugin's
--ignore-scripts install (scripts/plugin-bootstrap.js, deliberately
skipped to avoid requiring a build toolchain) silently left node-pty
unusable: server/lib/pty-attach.js's require() threw
"Cannot find module './prebuilds/linux-x64//pty.node'" the moment a
terminal was attached, leaving TerminalView permanently blank with no
visible error.

@lydell/node-pty is an API-compatible fork that ships each platform's
binary as a regular optionalDependency instead of a postinstall build
step, so a plain --ignore-scripts install resolves a working native
binding on Linux with no compiler needed. Verified by installing with
the exact `npm install --omit=dev --ignore-scripts` invocation the
plugin bootstrap uses and confirming require() succeeds.
2026-08-13 08:54:42 +07:00
nntrivi2001 b951f64321 fix(run): plumb the recorded prompt back into live runs, fix lane routing when the cwd doesn't match the selected lane
- pty-run.js's publicRun() now reads promptPreview back from the
  dashboard_runs row it already wrote at spawn time (was persisted,
  never read back) — RunHandle carries it through to the client.
- Workspace.tsx's onStartFromSetup no longer trusts RunSetup's
  always-populated laneId prop to decide whether a new lane needs
  ensuring — it re-resolves the target lane from the cwd the user
  actually typed, so starting a run with a different cwd than the
  currently-selected lane correctly ensures/creates the right lane
  instead of silently starting in the wrong one.

Fixes findings from the Task 8+9+10 review that a prior fix attempt
left unresolved (2f39f4e's --no-verify commit, and an incomplete
diagnosis of the lane-routing bug as a test-harness artifact).
2026-08-12 13:18:04 +07:00
nntrivi2001 2f39f4ec98 feat(run): wire Workspace to TerminalView, delete the stream-json Run feature
Combines three tasks that couldn't land as separate commits: the
pre-commit hook's full test run crashes on any intermediate state
where Workspace.tsx still imports the files being deleted, so the
deletion (old RunConsole/useRunStream/run-spawner/stream-json-parser),
the RunSetup/RunHistory type adjustments, and this file's own
TerminalView wiring had to be staged together and committed as one
hook-passable unit.

- Delete RunConsole.tsx, useRunStream.ts, server/lib/run-spawner.js,
  server/lib/stream-json-parser.js and their tests (Task 8).
- Adjust RunSetup.tsx/RunHistory.tsx to the tmux-backed RunHandle/
  RunStartArgs/DashboardRunHistoryItem shapes, remove mode selection
  UI (Task 9).
- Swap Workspace.tsx's chat-bubble run console for TerminalView
  (xterm.js over /ws-pty/:runId), drop the stream-json envelope
  plumbing, update Start/Resume to the new RunStartArgs payload.
  Create onStartFromSetup handler to work with RunSetup's new callback
  shape. Remove mode state and related plumbing. Remove send/followUp
  state (no longer using old RunConsole chat interface).
- Add promptPlaceholderTerminal i18n key to support RunSetup's new
  placeholder text (Task 10).
- Update Workspace.test.tsx to mock TerminalView component.
- Regenerate screens.snapshot.test.tsx snapshot (only Workspace run
  panel changes: terminal container instead of chat bubbles).
2026-08-12 11:58:38 +07:00
nntrivi2001 872c698132 feat(run): add /ws-pty/:runId PTY transport bridging WS to tmux attach 2026-08-12 10:13:14 +07:00
nntrivi2001 56744b360d feat(run): add tmux-backed run lifecycle (spawn/kill/list computed from tmux state) 2026-08-12 09:38:25 +07:00
nntrivi2001 1dd18fe98c feat(run): add tmux command wrapper with an injectable exec seam 2026-08-12 09:34:18 +07:00
nntrivi2001 dfea1a99d6 fix(tests): scrub GIT_* env vars leaking from the pre-commit hook into git-fixture tests
server/lib/update-check.js's execGit() and two test helpers
(lanes-cli.test.js, update-check.test.js) shelled out to git with an
explicit `cwd` but no `env` override. A parent git hook process (this
repo's own .husky/pre-commit, which runs `npm run test:server`) sets
GIT_DIR/GIT_INDEX_FILE in its own environment; those leak to every
child process and take precedence over `cwd` for repo discovery, so
every git command these tests ran against their throwaway tmp repos
was silently redirected at the real repo running the hook instead —
reproduced firsthand as four foreign "init"/"fixture" commits
overwriting a worktree branch mid pre-commit run. Fixes it the same
way server/lib/worktree.js already documented and did for its own git
calls: strip the GIT_* vars before exec.
2026-08-12 09:24:03 +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 78f6e1be8e feat(lanes): Add Lane repo/worktree mode toggle, manual branch, folder browse
- Repo mode adopts a directory as-is via /lanes/ensure (no worktree, no
  branch fields) - the right choice for a main repo you want stage
  detection on. Worktree mode (default) keeps the existing provisioning
  flow but now requires a manually-typed branch name instead of deriving
  one from the title.
- POST /lanes/worktree accepts an optional `branch`, validated via
  `git check-ref-format --branch`; omitting it preserves the CLI's
  existing auto-derived-branch behavior.
- New GET /lanes/browse lists a directory's immediate subdirectories,
  backing a small folder-browse modal on both path fields - browsers
  cannot expose an absolute path from a native picker, so this is
  server-backed instead, consistent with the tool's local-first model.
2026-08-06 16:03:49 +07:00
nntrivi2001 e7ef7bcef9 feat(lanes): extract skills-install lib, add POST /api/skills/install (F4) 2026-08-06 09:26:43 +07:00
nntrivi2001 5e620bd8ab feat(lanes): add ccam lanes gc — orphan MCP reap + log capping (E, F3c)
Ports the two pieces of Shipyard's lane-gc.sh that match CCAM's actual
architecture: kill Playwright MCP processes reparented to pid 1 (owning
session died), cap hook logs over 10MB back to their last 2MB in place.
Drops auto-removing stale worktrees by age (conflicts with the
never-automatic-destroy rule), state archiving, and scratch-debris
sweep (different storage architecture / files CCAM doesn't generate) —
see docs/superpowers/specs/2026-08-05-lane-gc-design.md.
2026-08-05 17:44:46 +07:00
nntrivi2001 8fc7a32490 feat(lanes): add isIntegrationEnabled toggle check (F3a) 2026-08-05 16:12:58 +07:00
nntrivi2001 2731b7c8ce feat(lanes): add lane-mcp sync core (F1) 2026-08-05 15:20:25 +07:00
nntrivi2001 d2327408fb fix(lanes): mergeSync commits a fully rerere-auto-resolved merge instead of rethrowing (E2)
Root cause: the catch block only fell through to the auto-commit path when
`unmergedFiles().length && MERGE_HEAD exists` — but a merge rerere resolved
completely has ZERO unmerged files (git already staged the resolution), so
that guard was always false and the raw git error was rethrown instead.
Found via an audit against the Shipyard source this was ported from.

Fixed by checking MERGE_HEAD first (unconditionally — its absence means the
merge never started, a real failure), then branching on whether any files
are still unmerged. Added a real rerere fixture test (teach a resolution,
recreate the identical conflict, confirm mergeSync auto-commits) — the
existing test suite had no coverage for this path.
2026-08-05 14:39:02 +07:00
nntrivi2001 4134b3d307 feat(lanes): add lane-agents install core (E3) 2026-08-05 12:37:40 +07:00
nntrivi2001 d12e9410e3 feat(lanes): add lane-sync core — check/merge/continue (E2) 2026-08-05 10:36:33 +07:00
nntrivi2001 a4246528fe feat(lanes): add MIGRATIONS_DIR + GENERATED_MERGE_PATHS profile declarations (E2) 2026-08-05 10:30:41 +07:00
nntrivi2001 31f984c750 feat(lanes): add --qc boot flag + QC_BOOT_ENV for deterministic QC stacks (E1) 2026-08-04 17:51:03 +07:00
nntrivi2001 e6942db7fe feat(lanes): add proof gallery core (list/file/delete/link) (C) 2026-08-04 15:55:04 +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