docs(lanes): document per-feature state and archive (B)

This commit is contained in:
2026-08-04 15:06:05 +07:00
parent 3a83e849cf
commit 917f0794d5
5 changed files with 129 additions and 2 deletions
+1
View File
@@ -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
+75
View File
@@ -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
+4
View File
@@ -230,6 +230,10 @@ A lane is a durable unit of parallel agent work — one working directory, many
| `ccam lanes profile check [<path>]` | Validate a profile — parses, every referenced hook exists and is executable, no leftover `TODO:`, declared ports free. `<path>` defaults to the current directory (not a lane id) |
| `ccam lanes reset\|remove\|purge <id> [--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 <stage> [--evidence <text>] [--note <text>] [--result pass\|fail]` | Declare the lane's current pipeline stage. Called by a skill at each phase boundary |
| `ccam feature list [<id>]` | List every feature this lane has activated, archived or live |
| `ccam feature activate <slug> [--title text] [<id>]` | Switch to a feature by slug (echoes the canonicalized slug), archiving the current one first |
| `ccam feature show <slug> [<id>]` | 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 `<repo>/.ccam/profile/` ([contract](LANES.md#lane-runtime-running-a-lanes-own-stack)); without one they report that nothing is configured rather than failing.
+47
View File
@@ -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 <slug>` (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 [<id>] # List every feature (archived or live)
ccam feature activate <slug> [--title text] [<id>] # Switch to a feature (echoes canonicalized slug)
ccam feature show <slug> [<id>] # Show one feature's saved pipeline
```
Omit `<id>` to address the lane owning the current directory.
## Stage detection
Besides the stage a skill explicitly declares with `ccam stage`, the dashboard
@@ -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 ✅ ────────────────────┘
```