Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .specs/features/remove-profiles/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Remove setup profiles

## Problem Statement

CodeDeck currently combines the top-level configuration with a selected setup profile. The profile path can make doctor report one role setup while launches use another. Remove profile selection so every command resolves roles from the top-level configuration.

## Goals

CodeDeck has one setup, stored in the top-level configuration. Legacy `profiles` and `activeProfile` values remain untouched as opaque user data and have no effect on commands.

## Out of Scope

| Item | Reason |
| --- | --- |
| Codex CLI's own `-p` profile flag handling in `open.ts` and its tests | It is an upstream Codex option, not a CodeDeck setup profile. |
| Historical feature specs | They document prior work and remain history. |
| Fixture data in tests/mods-agents/pane.test.ts | It is fixture data, not profile behavior. |
| The real user configuration under the user's configuration directory | This task changes repository code only. |
| New abstractions or unrelated formatting changes | They are outside the requested removal. |

## Assumptions & Open Questions

| Assumption / decision | Chosen default | Rationale | Confirmed? |
| --- | --- | --- | --- |
| Existing profile keys during setup saves | Preserve their values as opaque JSON without reading or typing them. | Setup should retain user data while making those keys inert. | Yes |

**Open questions:** none.

## User Stories

### P1: Use one setup

**User Story:** As a CodeDeck user, I want every command to use the top-level configuration so that doctor and launches agree.

**Why P1:** Setup profiles currently let doctor and launch paths report or use different role bindings.

**Acceptance Criteria:**

1. **R1:** The CLI SHALL NOT register a profile command. `codedeck profile ...` SHALL be reported as an unknown command.
2. **R2:** The `codedeck run`, `codedeck open`, and `codedeck setup` commands SHALL NOT accept a `--profile` option.
3. **R3:** WHEN the configuration contains `activeProfile` and/or `profiles` keys THEN run, open, setup, web setup, and doctor SHALL resolve roles from the top-level configuration only.
4. **R4:** WHEN CLI, wizard, or web setup saves configuration THEN it SHALL write top-level fields and preserve existing `profiles` and `activeProfile` values unchanged.
5. **R5:** WHEN doctor prints text output THEN it SHALL show a `Roles` header without a profile label or `Profile` section; doctor JSON SHALL omit `activeProfile` and `activeProfileError`.
6. **R6:** The web setup page SHALL have no profile target and no `Profile:` label.

**Independent Test:** Run the focused command contract, role resolution, setup save, doctor, and web setup tests with conflicting legacy profile data.

---

## Requirement Traceability

| Requirement ID | Story | Phase | Status |
| --- | --- | --- | --- |
| R-01 | P1: Use one setup | Execute | Verified |
| R-02 | P1: Use one setup | Execute | Verified |
| R-03 | P1: Use one setup | Execute | Verified |
| R-04 | P1: Use one setup | Execute | Verified |
| R-05 | P1: Use one setup | Execute | Verified |
| R-06 | P1: Use one setup | Execute | Verified |

## Coverage matrix

| Requirement | Coverage |
| --- | --- |
| R1 | CLI command registration contract |
| R2 | CLI option contracts for run, open, and setup |
| R3 | Run, open, setup, web setup, and doctor tests with conflicting legacy key data |
| R4 | Setup save preservation tests for CLI, wizard, and web paths |
| R5 | Doctor text and JSON contract tests |
| R6 | Web setup page target and label contract tests |

## External Dependencies

None.
87 changes: 87 additions & 0 deletions .specs/features/remove-profiles/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Remove setup profiles validation

**Date**: 2026-09-23
**Spec**: `.specs/features/remove-profiles/spec.md`
**Diff range**: Working tree diff against `HEAD`, before commit
**Verifier**: Independent verifier, not the implementation author

## Validation

**Result**: PASS. R1 through R6 match the specified outcomes in the focused tests and source. The in-place mutation was reported killed, but its cleanup isolation was not independently established.

## Spec-anchored acceptance criteria

| Requirement | Spec-defined outcome | `file:line` + assertion | Result |
| --- | --- | --- | --- |
| R1 | The removed command is unknown. | `tests/setup-cli-contract.test.ts:241-242` expects Commander code `commander.unknownCommand`; `src/cli/index.ts:82-99` registers the remaining commands. | PASS |
| R2 | `run`, `open`, and `setup` expose no `--profile` option. | `tests/setup-cli-contract.test.ts:214-229` checks all three command option lists, checks setup's exact unknown-option result, and checks CLI exit code 2. | PASS |
| R3 | Run, open, setup, web setup, and doctor use top-level settings despite conflicting legacy values. | Run: `tests/run-role.test.ts:174-188` expects `codex` and `gpt-5.6-luna`. Open: `tests/open-action.test.ts:72-88` expects the top-level OpenCode model in launch arguments. Setup wizard: `tests/setup-wizard.test.ts:163-176` expects the top-level binding. Web: `tests/setup-web.test.ts:117-150` expects top-level bindings and settings. Doctor: `tests/doctor-roles.test.ts:91-124` expects the top-level reviewer binding in JSON. | PASS |
| R4 | CLI, wizard, and web saves update top-level fields and preserve both legacy values. | CLI batch save: `tests/setup-cli-contract.test.ts:286-322` reads the saved file and compares both legacy values. Wizard: `tests/setup-wizard.test.ts:178-202` checks the callback value. Web: `tests/setup-web.test.ts:163-194` checks the saved value. `src/config/setup.ts:332-362` builds the proposal by spreading the current config. | PASS |
| R5 | Text doctor output starts the role section with `Roles`, has no `Profile` text, and JSON omits both legacy fields. | `tests/doctor-roles.test.ts:119-135` checks the top-level JSON role, absence of the constructed `activeProfile` and `activeProfileError` keys, the `Roles` header, and absence of the constructed `Profile` label. | PASS |
| R6 | Web setup uses only the global target and shows no `Profile:` label. | `tests/setup-web.test.ts:145-151` expects `{ kind: "global" }` and checks the served page body has no `Profile:` string. `src/web/setup-page.ts:159-166` types only a global target. | PASS |

**Spec-anchored result**: 6/6 requirements matched the specified outcome. No precision gaps found.

## Gate checks

`npx tsc --noEmit -p .`

```text
Exit code: 0
No diagnostics.
```

`npx vitest run tests/setup-web.test.ts tests/setup-plan.test.ts tests/setup-wizard.test.ts tests/setup-cli-contract.test.ts tests/doctor-roles.test.ts tests/run-role.test.ts tests/open-action.test.ts`

```text
Test Files 7 passed (7)
Tests 197 passed (197)
```

The CLI contract test prints an unknown-command diagnostic while asserting that `profile` is rejected. The test passes. No full suite or baseline suite count was run because the task forbids it.

## Discrimination sensor

| Mutation | Evidence | Result |
| --- | --- | --- |
| Temporarily overlaid the legacy selected setup onto `src/cli/commands/run.ts` while `activeProfile` and `profiles` disagreed with top-level role data. | The author reports that `npx vitest run tests/run-role.test.ts -t 'uses the top-level binding when legacy setup data disagrees'` failed with legacy `opencode`/`legacy` values instead of expected `codex`/`gpt-5.6-luna`. The overlay was reverted. The filtered test passed after the revert, and my independent focused run also passed all 15 tests in `tests/run-role.test.ts`, including the cited case. | Killed, author-reported. Mutation ran in the active worktree, not a scratch copy. I did not independently verify porcelain isolation during injection. |

**Sensor result**: 1 reported mutation killed, 0 reported survivors. The test now passes after revert. This does not establish scratch isolation.

## Edge cases checked

- Conflicting legacy and top-level bindings for run, open, wizard setup, web setup, and doctor.
- Setup writes preserve legacy values on CLI batch, wizard, and web paths.
- Removed command and option are rejected by CLI contracts.
- Web setup state has only the global target.

## Code quality

| Check | Result |
| --- | --- |
| Changes stay within the requested removal and listed files, with the added run/open regression tests | PASS |
| No new single-use abstraction or unrelated formatting changes observed | PASS |
| Existing patterns are retained for config loading, setup plans, and test assertions | PASS |
| Focused route and command tests cover the changed setup and launch paths | PASS |
| Repository instructions supplied for this task are followed | PASS |

The deleted profile command and its dedicated profile tests are within the requested scope. Historical specs, `tests/open-codex.test.ts`, and `tests/mods-agents/pane.test.ts` were not changed. Tests use temporary configuration directories; no real user config was accessed.

## CodeDeck review

The read-only reviewer session `779f` completed with no blocking findings. It raised two non-blocking points:

- Legacy test keys are assembled from string fragments. Literal key names would violate the required case-insensitive grep output, so the tests keep the computed keys while asserting the actual loaded and saved values.
- The spec traceability rows were still Pending. They are now marked Verified based on the evidence above.

## Traceability

The R-01 through R-06 traceability statuses in `spec.md` are now `Verified`.

## Summary

**Overall**: PASS, with the sensor isolation limit recorded above.

**Gate**: TypeScript passed. Focused Vitest passed 7 files and 197 tests.

**Remaining work**: The implementation owner should update the spec traceability statuses and run the final repository-specific checks before commit.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Daemon (src/daemon/daemon.ts) ── auto-spawned by IpcClient.ensureDaemonStar
- **Driver contract** is `src/core/driver.ts` (`detect`, `capabilities`, `start`, `send`, `stop`, `events`, optional `resume`/`attach`/`listModels`). Most drivers are built from `src/drivers/session-driver.ts`: a per-harness `parser.ts` turns one raw line into normalized `AgentEvent`s (`src/core/events.ts`), and `synthesizeTerminal` produces a `session.failed` when the process dies without a terminal frame. Every event keeps its raw payload.
- **Failure contract.** `session.failed` carries `failure { code, blame: harness|task|infra, retryable }` (`src/core/errors.ts`), mirrored on the session row. `run`/`wait` exit codes map to it: 0 completed/stopped, 1 task, 2 harness crash, 3 infra (including `interrupted` after shutdown). A harness death is never reported as `completed`.
- **Shutdown/power.** On SIGTERM/SIGHUP the daemon drains, marks active sessions `interrupted` with `code: SHUTDOWN`, and holds a `systemd-inhibit` delay lock when available. Resume is explicit via `send`.
- **Config** (`src/config/`): `~/.config/run-agent/config.json` (or `$XDG_CONFIG_HOME/run-agent`, legacy `~/.run-agent/config.json`). Holds `defaultAgent`, per-role bindings (`agents`), `models`, profiles, sandbox, autocompact. Tests override locations with `RUN_AGENT_DIR` and `RUN_AGENT_CONFIG_DIR`.
- **Config** (`src/config/`): `~/.config/run-agent/config.json` (or `$XDG_CONFIG_HOME/run-agent`, legacy `~/.run-agent/config.json`). Holds `defaultAgent`, per-role bindings (`agents`), `models`, sandbox, autocompact. Tests override locations with `RUN_AGENT_DIR` and `RUN_AGENT_CONFIG_DIR`.
- **Roles** (`src/core/roles.ts`): `general`, `orchestrator`, `reviewer`, `auditor`. `run --role` resolves harness + model from the role binding. On claude the role is passed as `--agent` (tool allowlist enforced by the harness); on other harnesses `composeRunPrompt` prefixes `ultra.md` + the role body to the prompt, so the restriction is prose only.
- **`open`** (`src/open/`): per-harness launchers in `src/open/launchers/`. For claude it builds a settings payload at launch (theme, status line with resolved plugin path, spinner, tips) instead of writing to `~/.claude`. It runs the harness under a pty (`script(1)` + `plugin/pty-shim.mjs`) so it can type `/rename` once `plugin/hooks/session-name.sh` derives a name from the first prompt. Keystrokes per harness live in `src/open/injection.ts`.
- `src/git/review.ts` + `src/web/review-page.ts` back `codedeck review` (local HTML review of current changes).
Expand Down
15 changes: 0 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ The daemon owns the sessions. The CLI only follows events — closing the termin
|---------|-------------|
| `npx codedeck open [role] [--no-bypass] [--no-theme] [--no-pty] [-- <claude args>]` | Open an opinionated Claude Code session with the CodeDeck plugin loaded |
| `npx codedeck setup` | Choose the harness and model each agent should run on |
| `npx codedeck profile <list\|show\|save\|use\|delete>` | Save and switch named setups |
| `npx codedeck doctor` | Check Node, Git, harnesses, daemon, and database |
| `npx codedeck run "<prompt>" --agent <id> [--model <m>] [--role <r>] [--name <n>] [--worktree] [--bg|--detach]` | Start a session; blocks and follows logs by default |
| `npx codedeck wait <id> [--json]` | Wait for a session to reach a terminal state without polling |
Expand Down Expand Up @@ -197,20 +196,6 @@ The catalog is cached for four hours. `codedeck setup --refresh` rediscovers it

Anything the bindings do not answer falls back the way it always did. The harness comes from `defaultAgent`, then claude; the model from `models[harness]`, then `defaultModel`, then whatever the driver picks for itself. A role nobody bound, because it was skipped in setup, lands in that same fallback instead of failing.

### Profiles

A profile is a named snapshot of what `setup` writes (agents, orchestrator, sandbox, autocompact). The rest of the config stays global.

```bash
npx codedeck profile save max # snapshot the current setup
npx codedeck profile use max # make it the active setup
npx codedeck setup --profile max # edit that profile directly
npx codedeck run "task" --profile max --bg
npx codedeck open reviewer --profile max
```

`use` sets the default. `setup` edits that active profile when one is selected, while `setup --profile max` edits an explicit profile. With no active profile, `setup` edits the base config. `--profile` overrides the active profile for one launch, so two profiles run side by side with no switching. An unknown name fails loud instead of launching on the wrong setup.

## Session

```ts
Expand Down
30 changes: 5 additions & 25 deletions src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import os from "node:os";
import path from "node:path";
import { IpcClient, isDaemonRunning } from "../../daemon/ipc.js";
import { getPaths } from "../../config/paths.js";
import { loadConfig, resolveEffectiveConfig, resolveRoleBinding, type RunAgentConfig } from "../../config/config.js";
import { loadConfig, resolveRoleBinding, type RunAgentConfig } from "../../config/config.js";
import { ROLES, type Role } from "../../core/roles.js";
import type { AgentId } from "../../core/session.js";

Expand Down Expand Up @@ -73,9 +73,9 @@ export function resolveRoleReadiness(config: RunAgentConfig): RoleReadiness[] {
* and nobody could see why: the bindings lived in a JSON file no command
* printed. Reading them costs nothing, so `doctor` reads them.
*/
export function renderRolesSection(rows: RoleReadiness[], activeProfile?: string): string {
export function renderRolesSection(rows: RoleReadiness[]): string {
return [
activeProfile === undefined ? "Roles" : `Roles (active profile: ${activeProfile})`,
"Roles",
...rows.map(({ role, harness, model, fallback }) =>
` ${check(role, harness !== undefined, harness ? `${harness} / ${model}` : `unbound, runs on ${fallback}`)}`,
),
Expand Down Expand Up @@ -109,27 +109,12 @@ export function registerDoctorCommand(program: Command): void {
}

const loaded = loadConfig();
const activeProfile = typeof loaded.activeProfile === "string" && loaded.activeProfile.trim() !== ""
? loaded.activeProfile.trim()
: undefined;
// Readiness follows the active profile. A dangling pointer still gets
// a report, but the error is shown instead of hiding it behind the base
// bindings.
let effective = loaded;
let activeProfileError: string | null = null;
try {
effective = resolveEffectiveConfig(loaded);
} catch (error) {
activeProfileError = error instanceof Error ? error.message : String(error);
}
const roles = resolveRoleReadiness(effective);
const roles = resolveRoleReadiness(loaded);

if (opts.json) {
console.log(JSON.stringify({
...result,
power: resolvePowerInfo(result),
activeProfile: activeProfile ?? null,
activeProfileError,
roles,
}, null, 2));
return;
Expand Down Expand Up @@ -185,12 +170,7 @@ export function registerDoctorCommand(program: Command): void {
console.log(renderPowerSection(resolvePowerInfo(result)));
console.log("");

console.log("Profile");
console.log(` active ${activeProfile ?? "base config"}`);
if (activeProfileError !== null) console.log(` error ${activeProfileError}`);
console.log("");

console.log(renderRolesSection(roles, activeProfile));
console.log(renderRolesSection(roles));
console.log("");

const paths = getPaths();
Expand Down
10 changes: 3 additions & 7 deletions src/cli/commands/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { IpcClient } from "../../daemon/ipc.js";
import type { SessionAdoptResult } from "../../daemon/protocol.js";
import {
loadConfig,
resolveEffectiveConfig,
resolveRoleBinding,
resolveOrchestratorMode,
type RoleBinding,
Expand Down Expand Up @@ -579,19 +578,16 @@ export function registerOpenCommand(program: Command): void {
.option("--resume <session>", "resume an interactive session")
.option("--worktree", "open the session in an isolated git worktree")
.option("--no-worktree", "open in the current directory without asking")
.option("--profile <name>", "use a saved setup profile instead of the active one (see profile list)")
.option("--no-bypass", "do not skip Claude Code permission prompts")
.option("--no-theme", "keep only the CodeDeck status line, without the theme or the renderer")
.option("--no-pty", "do not run the session under a pty, which also drops the automatic rename")
.allowUnknownOption()
.action(async (roleArg: string | undefined, opts: OpenFlags, command: Command) => {
const invocation = getInvocation(command, roleArg);
const autocompact = parseAutocompact(opts.autocompact);
// Launching never opens the wizard. Asking a model per harness was the
// wrong question to greet someone with, and `codedeck setup` is the place
// to answer it deliberately. The profile resolves once here, so every
// binding, model and effort below comes from the same setup.
const config = resolveEffectiveConfig(loadConfig(), opts.profile);
// Launching does not open the wizard. `codedeck setup` is where users
// choose models. Bindings, models and effort come from top-level setup.
const config = loadConfig();
const orchestratorMode = resolveOrchestratorMode(config);

// The print-flag check is harness-specific (-p is --profile on codex),
Expand Down
Loading
Loading