docs(lanes): document per-feature state and archive (B)
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 ✅ ────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user