From 917f0794d566b3d09d2f99af686c5a36eeb7fd4d Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 4 Aug 2026 15:06:05 +0700 Subject: [PATCH] docs(lanes): document per-feature state and archive (B) --- ARCHITECTURE.md | 1 + docs/API.md | 75 +++++++++++++++++++ docs/CLI.md | 4 + docs/LANES.md | 47 ++++++++++++ .../plans/2026-08-03-shipyard-parity-lanes.md | 4 +- 5 files changed, 129 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 00b8a31..6973ba1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -386,6 +386,7 @@ graph TD | `lib/secrets.js` | (A2) Reads `~/.ccam/secrets.env` — machine-level database/Redis credentials, deliberately outside any repository. Parsed with `lane-profile.js`'s literal `KEY=VALUE` reader, never sourced. Falls back to local defaults (with a one-time warning) when the file is absent; refuses to load a file readable by group or world rather than trusting it. Never returned by any route | | `lib/lane-env.js` | (A2) `seedEnv` copies a repo's real `.env` into a lane on first boot (or `--force`) and rewrites the declared `ENV_REWRITE` keys (`DATABASE_URL`/`REDIS_URL`/`UPLOAD_DIR`) in place, byte-identical otherwise. A `--force` refresh preserves `ENV_PRESERVE` keys (e.g. `JWT_SECRET`) from the lane's own existing file — swapping in the source's secret would 401 a running lane until reboot. Falls back to `.env.example` with a warning when the source is missing. Refuses on an adopted lane: that file is the user's real config | | `lib/lane-services.js` | (A2) `ensureDatabase`/`dropDatabase` call the profile's `db-create`/`db-drop` hooks — CCAM stays stack-agnostic on purpose. A state-dir marker file tracks whether a slot's database was already created, since a plain `createdb` can't be re-run safely and CCAM can't assume the hook is idempotent; this is also how `upLane` knows to seed only a freshly-created database. `dropDatabase` asserts the lane is `managed` and that the name being dropped is one this lane's own slot actually derives (itself or its `_test` sibling) before spawning anything | +| `lib/lane-features.js` | (B) Per-feature state and archive. `activateFeature` archives the lane's current active feature (if different) and restores the target's saved stage onto the live `lanes` row — the row stays the one live view every other reader already uses. `canonicalizeSlug` is a DELIBERATELY separate rule from `worktree.js:slugify` (drops a leading `feat/`, keeps `[A-Za-z0-9._-]`, does not lowercase) — the two must never be conflated. `clearLane` (`lib/lanes.js`) archives the active feature (if any) before resetting; a lane that never activated one is unaffected | | `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `ccam lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive | ### API Documentation diff --git a/docs/API.md b/docs/API.md index d3b03d2..91e6834 100644 --- a/docs/API.md +++ b/docs/API.md @@ -397,6 +397,81 @@ resolved path is confined to the lane's log directory after `realpath`, so a name from the request can never escape it; anything else is `404 ENOLOG`. A lane with no slot returns `{"available": false}`. + +#### Lane features + +```http +GET /api/lanes/:id/features +``` + +List every feature this lane has activated, archived or live: + +```json +{ + "features": [ + { "slug": "auth-redesign", "active": true, "title": "Auth redesign (v2)", "stage": "implement", "status": "running", "created_at": 1722702012 }, + { "slug": "migration", "active": false, "title": null, "stage": "done", "status": "idle", "created_at": 1722700000 }, + ] +} +``` + +Each feature carries a canonicalized `slug` (drops leading `feat/`, `/` → `-`, preserves case, keeps `[A-Za-z0-9._-]` only — **not the same rule as `worktree.js:slugify`**, which lowercases). `active` is `true` for the currently live feature. `title` is the display name (human-chosen via `ccam feature activate --title`; omitted/`null` if never set or identical to slug). `stage`, `status`, `notes` reflect the **saved** state when this feature was last archived; `active:true` shows the **live** lane's current stage instead. + +```http +GET /api/lanes/:id/features/:slug +``` + +Show one feature's saved pipeline: + +```json +{ + "slug": "auth-redesign", + "active": true, + "title": "Auth redesign (v2)", + "stage": "implement", + "status": "running", + "notes": "Testing with OAuth...", + "created_at": 1722702012 +} +``` + +Works on both archived and active features. If the slug has never been activated, returns `404 ENOFEAT`. + +```http +POST /api/lanes/:id/features/activate +{ "slug": "auth-redesign", "title": "Auth redesign (v2)" } +``` + +Activate a feature by slug. Request body: +- `slug` (required, string) — the canonicalized slug (or raw slug; the route canonicalizes it before lookup) +- `title` (optional, string) — human-friendly name to save with this feature + +Response: + +```json +{ + "slug": "auth-redesign", + "active": true, + "title": "Auth redesign (v2)", + "stage": "implement", + "status": "running", + "created_at": 1722702012, + "archivedPrevious": { "slug": "migration", "stage": "done" } +} +``` + +Behavior: +- If the target slug has been activated before, restores its saved stage/status/notes onto the live lane +- If the slug is new, creates a fresh feature row with empty stage/status/notes +- If a different feature is currently active, archives it first (copies live stage/status/notes to its row) — echoed in `archivedPrevious` +- Returns **200** on success; **409 ESTALE** if another request changed the lane between read and write + +Status codes: +- **200** — feature activated +- **409** — the lane's stage changed concurrently (rare with single-session lanes) +- **400** — missing/invalid request body + + ### Locks Cross-lane named locks (`server/lib/named-lock.js`) — the OTHER axis from a diff --git a/docs/CLI.md b/docs/CLI.md index 63557e5..759745f 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -230,6 +230,10 @@ A lane is a durable unit of parallel agent work — one working directory, many | `ccam lanes profile check []` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `` defaults to the current directory (not a lane id) | | `ccam lanes reset\|remove\|purge [--force] [--keep-db] --yes` | Show preflight facts, then perform a destructive action. Refuses without `--yes`; `--force` is required when commits are unpushed; `--keep-db` (`reset` only) skips dropping/recreating a data-isolated lane's database | | `ccam stage [--evidence ] [--note ] [--result pass\|fail]` | Declare the lane's current pipeline stage. Called by a skill at each phase boundary | +| `ccam feature list []` | List every feature this lane has activated, archived or live | +| `ccam feature activate [--title text] []` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first | +| `ccam feature show []` | Show one feature's saved pipeline — works on an archived one too | + **Runtime** — the lane's own application stack, as opposed to `start`/`stop`, which drive its Claude run. Two lifecycles, one lane id. Each requires the repository to declare a profile at `/.ccam/profile/` ([contract](LANES.md#lane-runtime-running-a-lanes-own-stack)); without one they report that nothing is configured rather than failing. diff --git a/docs/LANES.md b/docs/LANES.md index 626eff0..e1a74ab 100644 --- a/docs/LANES.md +++ b/docs/LANES.md @@ -543,6 +543,53 @@ ccam lanes add --cwd /path/to/ml-repo --title "Training run #1" --pipeline ml-tr (The pipeline is selected at creation time. Lanes created without `--pipeline` default to the `default` pipeline.) + +## Per-feature state and archive + +A lane can carry many features (identified by slug) across its lifetime. Each feature has its own saved pipeline state (stage, status, notes) that persists separately from the live lane — the one every session and the Workspace console observe. + +**Why:** `clearLane` used to erase the entire lane on each new feature. Now a lane can accumulate many features' saved states, letting you switch between them (via `ccam feature activate`) and keep their pipeline histories separate. This matters for proof galleries, parallel task batches, and any flow where one lane runs many intentional phases. + +### The opt-in model + +Nothing changes for a lane that never calls `ccam feature activate` — `clearLane` still erases the live stage and status rows exactly as before. Only when you opt in to features does a lane start archiving. At that point: +- `clearLane` archives the **current** active feature (if any) before clearing the live lane row — its saved stage goes into the archive +- A lane that has never activated a feature is unaffected by this change and works exactly as it did before feature support + +### Slug canonicalization + +Every feature is identified by a canonicalized slug. The rule is: +1. Drop a leading `feat/` prefix if present +2. Replace `/` and whitespace with `-` +3. Keep `[A-Za-z0-9._-]` only +4. **Do NOT lowercase** — slugs preserve case + +This is a **deliberately different rule** from `worktree.js:slugify`'s branch-name slugification (which lowercases). The two must never be conflated. Every endpoint and CLI command echoes back the canonicalized form so the caller knows the exact slug that was stored. + +### Activate semantics + +`ccam feature activate ` (or the API's `POST /api/lanes/:id/features/activate`): +1. Archives the current active feature (if any and if different from the target slug) — copies its live stage/status/notes to the archive +2. Restores the target slug's saved pipeline onto the live lane row — so switching back to a past feature resumes its pipeline exactly where it left off +3. Creates a fresh feature row for a never-seen slug (empty stage/status/notes) + +This means activating an archived feature twice resumes the same pipeline both times. + +### The Workspace feature picker + +The Workspace page's feature picker (read-only) shows every archived and active feature for the lane. Selecting one displays its saved pipeline. **It never changes the live lane** — a read-only view matching the standing rule that "the console never writes a lane's stage". Changing the active feature requires the CLI or the API route, not a UI button. + +### CLI commands + +```bash +ccam feature list [] # List every feature (archived or live) +ccam feature activate [--title text] [] # Switch to a feature (echoes canonicalized slug) +ccam feature show [] # Show one feature's saved pipeline +``` + +Omit `` to address the lane owning the current directory. + + ## Stage detection Besides the stage a skill explicitly declares with `ccam stage`, the dashboard diff --git a/docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md b/docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md index f93c259..08b75aa 100644 --- a/docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md +++ b/docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md @@ -23,7 +23,7 @@ stronger is a separate design (§ Future). | **A1** | Slots, ports, profile hooks, detached lifecycle | — | ✅ **done** 2026-08-03 | | **A2** | Data isolation: `.env`, database, Redis index | A1 | ✅ **done** 2026-08-03 | | **A3** | Stack detection + profile scaffolding | A2 | ✅ **done** 2026-08-04 | -| **B** | Per-feature state + archive | — | planned | +| **B** | Per-feature state + archive | — | ✅ **done** 2026-08-04 | | **C** | Proof gallery | B | planned | | **D** | Cross-lane named locks | — | ✅ **done** 2026-08-04 | | **E** | `ship-feature` skill + QC agents | A2·B·C·D | planned | @@ -346,7 +346,7 @@ check fires instead of silently degrading. ``` A1 ✅ ──▶ A2 ✅ ──▶ A3 ✅ │ -B ──▶ C ─────────────┼──▶ E ──▶ F +B ✅ ──▶ C ─────────────┼──▶ E ──▶ F D ✅ ────────────────────┘ ```