diff --git a/feature-requests/configurable-board-path/01-scope.md b/feature-requests/configurable-board-path/01-scope.md
new file mode 100644
index 0000000000..4b264b5102
--- /dev/null
+++ b/feature-requests/configurable-board-path/01-scope.md
@@ -0,0 +1,76 @@
+# Scope
+
+## Objective
+
+Implement a low-drift configurable board-path feature that lets Kanban load and persist board state from an explicit custom file path while preserving current default behavior.
+
+## Source request
+
+GitHub discussion `#228`:
+- “when executing the kanban command then we should have the option to set the customised board path so that we can load the project specific board and track the board.json in git.”
+
+## Locked goals
+
+- support a custom board file path
+- preserve current default path behavior
+- keep workspace identity based on repo path and existing workspace index
+- keep current task/session/worktree behavior unchanged
+- keep CLI/task seams compatible
+
+## Recommended v1 scope
+
+- add one canonical board-path resolver
+- add one supported configuration source for board-path override
+- refactor workspace-state reads/writes to use that resolver
+- add validation and tests for default and override behavior
+- document path semantics and limitations
+
+## Non-goals
+
+- no workspace-state storage redesign
+- no full relocation of all workspace files unless evidence forces it
+- no new external API
+- no board import/export workflow
+- no Prompt Forge integration work in this branch
+- no changes under `src/cline-sdk/`
+- no broad UI redesign
+
+## Decision gates
+
+### Gate 1: config surface
+
+Chosen:
+- persisted project runtime config as the canonical source
+
+Deferred:
+- launch-time CLI flag
+
+Reason:
+- lower drift than threading a launch-only override through every boot path
+- aligns with existing per-project config pattern
+
+### Gate 2: storage split policy
+
+Chosen:
+- `board.json` only in v1
+
+Deferred:
+- relocating `sessions.json` and `meta.json`
+
+Reason:
+- matches the actual feature request
+- lowest drift
+- avoids turning a file-path feature into a broader persistence migration
+
+Accepted tradeoff:
+- `statePath` no longer fully implies board location
+- API/doc language must become more precise
+
+## Success criteria
+
+1. Kanban still works unchanged with no custom configuration.
+2. A configured custom board file path is used for board read/write.
+3. `sessions.json` and `meta.json` behavior remains deterministic.
+4. Malformed custom board files fail loudly and clearly.
+5. CLI/runtime/project flows continue to resolve the same workspace identity.
+6. Tests cover default, override, and failure cases.
diff --git a/feature-requests/configurable-board-path/02-ticket-index.md b/feature-requests/configurable-board-path/02-ticket-index.md
new file mode 100644
index 0000000000..781453c74d
--- /dev/null
+++ b/feature-requests/configurable-board-path/02-ticket-index.md
@@ -0,0 +1,162 @@
+# Ticket Index
+
+## Summary
+
+This branch is a brownfield persistence change. Most work belongs in `src/state/workspace-state.ts`, config plumbing, and workspace-state integration tests.
+
+## Tickets
+
+### T-00 Feature folder scaffolding
+- Model: `gpt-5.4-mini low`
+- Scope:
+ - feature docs
+ - ticket index
+ - orchestration prompt
+ - Cavekit kit/validation/tracking artifacts
+- Status:
+ - done
+
+### T-01 Existing behavior audit
+- Model: `gpt-5.4-mini medium`
+- Scope:
+ - enumerate every current assumption that board path equals `/board.json`
+ - identify all code paths that read or write board state
+ - identify docs/tests that pin current path semantics
+- Primary files:
+ - `src/state/workspace-state.ts`
+ - `src/server/workspace-registry.ts`
+ - `src/trpc/projects-api.ts`
+ - `src/trpc/workspace-api.ts`
+ - `test/integration/workspace-state.integration.test.ts`
+ - `docs/architecture.md`
+- Acceptance:
+ - drift map written into tracking
+ - no assumed seams left undocumented
+
+### T-02 Board-path config source
+- Model: `gpt-5.4-mini medium`
+- Scope:
+ - add canonical board-path override source
+ - prefer project runtime config for v1
+ - if project config chosen, add typed field and normalization
+- Primary files:
+ - `src/config/runtime-config.ts`
+ - `src/core/api-contract.ts` if config contract changes require it
+ - related config tests
+- Acceptance:
+ - default config unchanged
+ - override can be read deterministically
+ - invalid/empty override rejected or normalized clearly
+
+### T-03 Storage resolver extraction
+- Model: `gpt-5.4-mini medium`
+- Scope:
+ - create one exported resolver for:
+ - `statePath`
+ - `boardPath`
+ - `sessionsPath`
+ - `metaPath`
+ - preserve current defaults
+- Primary files:
+ - `src/state/workspace-state.ts`
+- Acceptance:
+ - no direct hardcoded board-path joins remain outside resolver
+ - default resolver outputs match current behavior
+
+### T-04 Workspace-state persistence refactor
+- Model: `gpt-5.4-medium`
+- Scope:
+ - update load/save/mutate/read helpers to use resolved board path
+ - keep locks and revision semantics correct
+ - keep sessions/meta behavior explicit
+- Primary files:
+ - `src/state/workspace-state.ts`
+- Acceptance:
+ - default path still works
+ - configured board path works
+ - conflict behavior unchanged
+ - malformed board file errors still identify actual file path
+
+### T-05 Runtime/read-side contract audit
+- Model: `gpt-5.4-mini low`
+- Scope:
+ - decide whether `RuntimeWorkspaceStateResponse` needs `boardPath`
+ - update runtime/docs/tests if `statePath` would become misleading
+- Primary files:
+ - `src/core/api-contract.ts`
+ - `src/trpc/workspace-api.ts`
+ - `src/server/runtime-state-hub.ts`
+ - `web-ui` tests if affected
+- Acceptance:
+ - runtime contract is semantically honest
+ - no product code depends on false `statePath => board.json` assumption
+
+### T-06 CLI and operator surface
+- Model: `gpt-5.4-mini medium`
+- Scope:
+ - if needed, add a minimal CLI/configuration surface to set or inspect board-path override
+ - keep scope narrow; avoid general config overhaul
+- Primary files:
+ - `src/cli.ts`
+ - `src/commands/task.ts`
+ - config command files if existing surface is reused
+- Acceptance:
+ - operator can configure feature without manual file surgery
+ - no regression to normal launch/task commands
+
+### T-07 Integration and regression test wave
+- Model: `gpt-5.3-codex medium`
+- Scope:
+ - add or update tests for:
+ - default path
+ - custom path
+ - malformed custom board file
+ - concurrent workspace creation unaffected
+ - stale write/conflict unaffected
+- Primary files:
+ - `test/integration/workspace-state.integration.test.ts`
+ - config tests
+ - CLI tests if new flag/surface added
+- Acceptance:
+ - path feature validated end-to-end
+ - no silent fallback to old path when override configured
+
+### T-08 Docs update
+- Model: `gpt-5.4-mini low`
+- Scope:
+ - update human docs for board-path semantics
+ - note feature-request motivation and limitations
+- Primary files:
+ - `README.md`
+ - `docs/architecture.md`
+ - maybe `docs/README.md`
+- Acceptance:
+ - docs match actual resolver behavior
+ - no stale `statePath` implications remain
+
+### T-09 Final audit
+- Model: `gpt-5.4-mini low`
+- Scope:
+ - verify narrow branch scope
+ - verify no hidden persistence redesign slipped in
+ - close tracking/final validation
+- Deliverables:
+ - `09-final-validation.md`
+
+## Dependency order
+
+1. `T-01`
+2. `T-02`
+3. `T-03`
+4. `T-04`
+5. `T-05`
+6. `T-06`
+7. `T-07`
+8. `T-08`
+9. `T-09`
+
+Reason:
+- config source and resolver must settle first
+- persistence refactor depends on both
+- contract/CLI/docs should follow actual storage behavior
+- tests validate the final integrated shape
diff --git a/feature-requests/configurable-board-path/03-orchestration-prompt.md b/feature-requests/configurable-board-path/03-orchestration-prompt.md
new file mode 100644
index 0000000000..46086dfda8
--- /dev/null
+++ b/feature-requests/configurable-board-path/03-orchestration-prompt.md
@@ -0,0 +1,66 @@
+# Multi-Agent Orchestration Prompt
+
+Branch:
+- `fork/feature-request/configurable-board-path`
+
+Objective:
+- implement a low-drift configurable board-path feature for Kanban
+
+Locked constraints:
+- no SDK changes
+- no changes under `src/cline-sdk/`
+- preserve default behavior
+- no invented APIs
+- do not widen into workspace-state redesign
+
+Feature intent:
+- allow Kanban to load and persist board state from a custom board file path
+- support tracking project-specific board state in git
+
+Required brownfield facts:
+- board path is currently hardcoded in `src/state/workspace-state.ts`
+- workspace identity remains `repoPath <-> workspaceId`
+- current persistence model is directory-centric
+- request `#228` asks for custom board path, not a new storage engine
+
+Decision gates before code lands:
+1. choose canonical override source
+2. choose whether override applies only to `board.json` or to all workspace state files
+
+Recommended decisions:
+1. project runtime config as canonical source
+2. `board.json` only in v1
+
+Delegation map:
+- `gpt-5.4-mini low`
+ - audits
+ - docs
+ - contract honesty pass
+ - final validation
+- `gpt-5.4-mini medium`
+ - config plumbing
+ - resolver extraction
+ - CLI/config surface
+- `gpt-5.4-medium`
+ - workspace-state persistence refactor
+- `gpt-5.3-codex medium`
+ - integrated regression test wave
+
+Validation gates:
+1. resolver gate
+ - one canonical resolver owns board path semantics
+2. persistence gate
+ - load/save/mutate respect override and preserve revision rules
+3. compatibility gate
+ - default path behavior unchanged
+4. failure gate
+ - malformed custom board file fails loudly with actual file path
+5. scope gate
+ - no hidden relocation of sessions/meta unless explicitly approved
+
+Worker output requirements:
+- files changed
+- tests added/updated
+- assumptions
+- open risks
+- whether change widened scope beyond plan
diff --git a/feature-requests/configurable-board-path/04-context-index.md b/feature-requests/configurable-board-path/04-context-index.md
new file mode 100644
index 0000000000..cddace2d7f
--- /dev/null
+++ b/feature-requests/configurable-board-path/04-context-index.md
@@ -0,0 +1,45 @@
+# Context Index
+
+## Entry points
+
+### Feature intent
+- feature request `#228`
+- Phase2 recommendation docs in `/lump/apps/kanban-integration-idea/docs/Phase2/`
+
+### Primary code ownership
+- `src/state/workspace-state.ts`
+
+### Supporting areas
+- `src/config/runtime-config.ts`
+- `src/trpc/workspace-api.ts`
+- `src/trpc/projects-api.ts`
+- `src/server/workspace-registry.ts`
+- `src/core/api-contract.ts`
+- `src/cli.ts`
+- `test/integration/workspace-state.integration.test.ts`
+- `docs/architecture.md`
+
+## Traversal order
+
+1. `05-refs-existing-behavior.md`
+2. `06-kit-configurable-board-path.md`
+3. `02-ticket-index.md`
+4. `07-validation.md`
+5. `08-tracking.md`
+
+## Context edges
+
+### Existing behavior refs -> kit
+- current hardcoded board path
+- directory-centric persistence model
+- repo-path-based workspace identity
+- runtime contract exposing `statePath`
+
+### Kit -> tickets
+- every ticket maps to one or more kit requirements
+
+### Tickets -> validation
+- each implementation ticket has one or more validation gates
+
+### Validation -> tracking
+- `08-tracking.md` records pass/fail/open risk for each gate
diff --git a/feature-requests/configurable-board-path/05-refs-existing-behavior.md b/feature-requests/configurable-board-path/05-refs-existing-behavior.md
new file mode 100644
index 0000000000..bbbe1a97a1
--- /dev/null
+++ b/feature-requests/configurable-board-path/05-refs-existing-behavior.md
@@ -0,0 +1,65 @@
+# Refs: Existing Behavior
+
+## Confirmed current behavior
+
+### Board path is hardcoded
+
+- `BOARD_FILENAME = "board.json"` in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L28)
+- `getWorkspaceBoardPath(workspaceId)` returns `/board.json` in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L181)
+
+### Workspace state is directory-centric
+
+- workspace root is `~/.cline/kanban/workspaces` via [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L169)
+- workspace dir is `/` via [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L177)
+- sessions/meta are siblings of board file via:
+ - [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L185)
+ - [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L189)
+
+### Load/save/mutate all assume default board path
+
+- `loadWorkspaceState()` in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L640)
+- `saveWorkspaceState()` in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L648)
+- `mutateWorkspaceState()` in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L701)
+
+### Workspace identity is repo-based
+
+- git-root canonicalization in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L503)
+- workspace index maps `repoPath <-> workspaceId` in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L39) and [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L446)
+- `loadWorkspaceContext()` returns `repoPath`, `workspaceId`, `statePath`, and git info in [workspace-state.ts](/lump/apps/kanban/src/state/workspace-state.ts#L551)
+
+### Runtime/server bootstrap depends on repo path, not board path
+
+- registry bootstraps from `loadWorkspaceContext(deps.cwd)` in [workspace-registry.ts](/lump/apps/kanban/src/server/workspace-registry.ts#L188)
+- state snapshots reload by repo path in [workspace-registry.ts](/lump/apps/kanban/src/server/workspace-registry.ts#L315)
+- project add/remove logic depends on repo path and workspace id in `projects-api`
+
+### Runtime contract exposes `statePath`, not `boardPath`
+
+- `RuntimeWorkspaceStateResponse` includes `statePath` in [api-contract.ts](/lump/apps/kanban/src/core/api-contract.ts#L296)
+- workspace API returns that state shape in [workspace-api.ts](/lump/apps/kanban/src/trpc/workspace-api.ts)
+
+### Existing tests pin current layout
+
+- workspace-state integration tests write malformed board files at `join(context.statePath, "board.json")` in [workspace-state.integration.test.ts](/lump/apps/kanban/test/integration/workspace-state.integration.test.ts#L273)
+- same suite pins `sessions.json` and index layout in:
+ - [workspace-state.integration.test.ts](/lump/apps/kanban/test/integration/workspace-state.integration.test.ts#L319)
+ - [workspace-state.integration.test.ts](/lump/apps/kanban/test/integration/workspace-state.integration.test.ts#L351)
+
+## External requirement signal
+
+GitHub discussion `#228` says:
+- custom board path should be set when executing `kanban`
+- goal is project-specific board tracking in git
+
+Source:
+- https://github.com/cline/kanban/discussions/228
+
+## Brownfield implications
+
+1. This feature is not just a UI tweak.
+2. The primary blast radius is persistence resolution in `workspace-state.ts`.
+3. A v1 that moves only `board.json` introduces split storage.
+4. Split storage is acceptable only if:
+ - resolver semantics are explicit
+ - runtime contract is not misleading
+ - tests cover custom-path failure modes
diff --git a/feature-requests/configurable-board-path/06-kit-configurable-board-path.md b/feature-requests/configurable-board-path/06-kit-configurable-board-path.md
new file mode 100644
index 0000000000..a3eb54e7a5
--- /dev/null
+++ b/feature-requests/configurable-board-path/06-kit-configurable-board-path.md
@@ -0,0 +1,112 @@
+# Kit: Configurable Board Path
+
+## Purpose
+
+Let Kanban use a custom board file path while preserving current default behavior and keeping workspace identity stable.
+
+## R-01 Default compatibility
+
+Kanban must behave exactly as it does today when no board-path override is configured.
+
+Acceptance:
+- default path remains `/board.json`
+- existing projects continue loading without migration
+- no task/session/worktree behavior changes
+
+## R-02 Canonical resolver
+
+Board path semantics must be owned by one canonical resolver in the state layer.
+
+Acceptance:
+- no duplicated board-path join logic remains
+- resolver returns deterministic paths from workspace context + config
+- resolver is used by read, write, and mutation flows
+
+## R-03 Stable workspace identity
+
+Custom board path must not redefine how Kanban identifies a workspace.
+
+Acceptance:
+- workspace identity remains tied to repo path and workspace index
+- workspace id generation and collision handling remain unchanged
+- task commands still resolve workspace by project repo path
+
+## R-04 Narrow persistence change
+
+v1 must keep the change narrow.
+
+Acceptance:
+- if only `board.json` is moved, that policy is explicit
+- `sessions.json` and `meta.json` stay deterministic and documented
+- no hidden full persistence relocation happens
+
+## R-05 Honest runtime contract
+
+Runtime responses and docs must not imply false storage semantics.
+
+Acceptance:
+- if `statePath` no longer implies board location, either:
+ - runtime also exposes `boardPath` when a non-default override is active, or
+ - docs/tests clearly state `statePath` is workspace metadata directory only
+
+## R-06 Configurability
+
+The board-path override must come from a supported, typed source.
+
+Acceptance:
+- override source is validated and normalized
+- empty or invalid values fail clearly
+- operator does not need to patch internal files by hand
+
+## R-07 Loud failure behavior
+
+Malformed custom board files must fail loudly with actual file-location context.
+
+Acceptance:
+- error includes offending path
+- no silent fallback to default board file when override is configured
+- malformed data still gets schema validation
+
+## R-08 Test coverage
+
+The feature must be covered by integration tests and any necessary config/CLI tests.
+
+Acceptance:
+- default path test
+- custom path test
+- malformed custom board file test
+- conflict/revision behavior unchanged
+- concurrent workspace creation/index behavior unchanged
+
+## R-09 Low drift
+
+The branch must not widen into unrelated persistence, runtime, or UI redesign.
+
+Acceptance:
+- no new external API
+- no broad CLI redesign
+- no changes under `src/cline-sdk/`
+- only minimal docs/runtime contract changes
+
+## Locked decisions
+
+### D-01 Canonical override source
+
+Chosen:
+- persisted project config
+
+Deferred:
+- launch-time CLI flag
+
+### D-02 Board-only vs full-state relocation
+
+Chosen:
+- board-only override in v1
+
+Deferred:
+- board + sessions + meta relocation
+
+Reason:
+- directly matches request `#228`
+- minimizes blast radius
+- avoids hidden migration complexity
diff --git a/feature-requests/configurable-board-path/07-validation.md b/feature-requests/configurable-board-path/07-validation.md
new file mode 100644
index 0000000000..7236cfc58a
--- /dev/null
+++ b/feature-requests/configurable-board-path/07-validation.md
@@ -0,0 +1,115 @@
+# Validation
+
+## Gate V-01 Resolver gate
+
+Requirement links:
+- `R-01`
+- `R-02`
+
+Pass when:
+- canonical resolver exists
+- default resolver output matches current layout
+- all board read/write paths use resolver
+
+Evidence:
+- [`src/state/workspace-state.ts`](/lump/apps/kanban/src/state/workspace-state.ts)
+- `test/integration/workspace-state.integration.test.ts`
+
+Status:
+- pass
+
+## Gate V-02 Config gate
+
+Requirement links:
+- `R-06`
+
+Pass when:
+- one supported override source is implemented
+- invalid/empty overrides fail clearly
+- default config remains unchanged
+
+Evidence:
+- [`src/config/runtime-config.ts`](/lump/apps/kanban/src/config/runtime-config.ts)
+- [`src/core/api-contract.ts`](/lump/apps/kanban/src/core/api-contract.ts)
+- `test/runtime/config/runtime-config.test.ts`
+- `test/runtime/api-validation.test.ts`
+
+Status:
+- pass
+
+## Gate V-03 Persistence gate
+
+Requirement links:
+- `R-03`
+- `R-04`
+- `R-07`
+
+Pass when:
+- custom board path is used for load/save/mutate
+- workspace identity remains repo-based
+- malformed custom board files fail loudly
+- no silent fallback occurs
+
+Evidence:
+- [`src/state/workspace-state.ts`](/lump/apps/kanban/src/state/workspace-state.ts)
+- [`src/trpc/runtime-api.ts`](/lump/apps/kanban/src/trpc/runtime-api.ts)
+- `test/integration/workspace-state.integration.test.ts`
+
+Status:
+- pass
+
+## Gate V-04 Compatibility gate
+
+Requirement links:
+- `R-01`
+- `R-08`
+
+Pass when:
+- stale write/conflict behavior unchanged
+- concurrent workspace index behavior unchanged
+- normal default-path workflows still pass
+
+Evidence:
+- `pnpm vitest run test/runtime/config/runtime-config.test.ts test/integration/workspace-state.integration.test.ts test/runtime/api-validation.test.ts`
+- `cd web-ui && npx vitest run src/runtime/use-runtime-config.test.tsx src/runtime/use-runtime-project-config.test.tsx src/runtime/native-agent.test.ts src/hooks/use-git-actions.test.tsx src/hooks/use-home-agent-session.test.tsx src/hooks/use-runtime-settings-cline-controller.test.tsx src/hooks/use-startup-onboarding.test.tsx`
+
+Status:
+- pass
+
+## Gate V-05 Contract honesty gate
+
+Requirement links:
+- `R-05`
+
+Pass when:
+- runtime/docs/tests do not imply `statePath === board location` if no longer true
+
+Evidence:
+- [`src/core/api-contract.ts`](/lump/apps/kanban/src/core/api-contract.ts)
+- [`src/terminal/agent-registry.ts`](/lump/apps/kanban/src/terminal/agent-registry.ts)
+- [`web-ui/src/components/runtime-settings-dialog.tsx`](/lump/apps/kanban/web-ui/src/components/runtime-settings-dialog.tsx)
+
+Status:
+- pass
+
+## Gate V-06 Scope gate
+
+Requirement links:
+- `R-09`
+
+Pass when:
+- no SDK changes
+- no hidden storage-engine redesign
+- no unrelated runtime/UI work
+
+Evidence:
+- no changes under `src/cline-sdk/`
+- diff limited to config, state, runtime, tests, and runtime settings UI
+- final audit found and fixed non-atomic rollback in `runtime-api.ts`
+
+Status:
+- pass
+
+## Known limitation
+
+- `test/runtime/trpc/runtime-api.test.ts` still does not execute in this repo because of an existing `@clinebot/core` export-condition failure at suite load time. The new rollback path is covered by typecheck and code audit, but not by executable Vitest coverage in the current environment.
diff --git a/feature-requests/configurable-board-path/08-tracking.md b/feature-requests/configurable-board-path/08-tracking.md
new file mode 100644
index 0000000000..3a62459a43
--- /dev/null
+++ b/feature-requests/configurable-board-path/08-tracking.md
@@ -0,0 +1,67 @@
+# Tracking
+
+## Status
+
+- branch: `fork/feature-request/configurable-board-path`
+- phase: implementation complete
+- implementation: in progress review / validation closed for current scope
+
+## Requirement tracking
+
+| Requirement | Status | Notes |
+| --- | --- | --- |
+| `R-01` default compatibility | done | default fallback remains `/board.json` |
+| `R-02` canonical resolver | done | board path joins centralized in `workspace-state.ts` |
+| `R-03` stable workspace identity | done | workspace index semantics unchanged |
+| `R-04` narrow persistence change | done | only board file relocates; sessions/meta remain in state dir |
+| `R-05` honest runtime contract | done | runtime responses expose `boardPath` only when a non-default override is active |
+| `R-06` configurability | done | persisted project runtime config is canonical source |
+| `R-07` loud failure behavior | done | malformed custom board path fails loudly; no silent fallback |
+| `R-08` test coverage | done | config, validation, workspace-state, and focused web tests updated |
+| `R-09` low drift | done | no SDK changes and no broad persistence redesign |
+
+## Decision log
+
+### Accepted
+
+- use brownfield, low-drift approach
+- preserve default behavior
+- no SDK changes
+- persisted project runtime config as canonical override source
+- board-only override in v1
+- runtime response exposes `boardPath` only when a non-default override is active
+
+### Deferred
+
+- explicit CLI setter surface in v1
+
+## Risks
+
+1. Split storage can make `statePath` semantically misleading.
+2. `runtime-api` suite execution is still blocked by existing `@clinebot/core` export conditions in this repo.
+3. Full-state relocation would widen scope sharply and should be rejected unless evidence demands it.
+
+## Implemented
+
+1. Added project-level `boardPath` to runtime config and runtime contract.
+2. Added canonical storage resolver and board-path migration helper.
+3. Refactored board load/save/mutate flows to use resolved board path.
+4. Added runtime save rollback so failed board relocation restores prior config.
+5. Added settings UI field for project board file path.
+6. Added config, validation, workspace-state, and focused web test coverage.
+
+## Validation summary
+
+1. `pnpm vitest run test/runtime/config/runtime-config.test.ts test/integration/workspace-state.integration.test.ts test/runtime/api-validation.test.ts`
+ - pass
+2. `npm run typecheck`
+ - pass
+3. `cd web-ui && npm run typecheck`
+ - pass
+4. `cd web-ui && npx vitest run src/runtime/use-runtime-config.test.tsx src/runtime/use-runtime-project-config.test.tsx src/runtime/native-agent.test.ts src/hooks/use-git-actions.test.tsx src/hooks/use-home-agent-session.test.tsx src/hooks/use-runtime-settings-cline-controller.test.tsx src/hooks/use-startup-onboarding.test.tsx`
+ - pass
+
+## Remaining work
+
+1. optional full-suite execution once the repo-wide `@clinebot/core` test-import issue is resolved
+2. final audit / commit only when requested
diff --git a/feature-requests/configurable-board-path/README.md b/feature-requests/configurable-board-path/README.md
new file mode 100644
index 0000000000..0687ab7872
--- /dev/null
+++ b/feature-requests/configurable-board-path/README.md
@@ -0,0 +1,33 @@
+# `fork/feature-request/configurable-board-path`
+
+Feature folder for the configurable board-path fork branch.
+
+Branch:
+- `fork/feature-request/configurable-board-path`
+
+Problem:
+- Kanban currently hardcodes board persistence to `~/.cline/kanban/workspaces//board.json`.
+- Feature request [`#228`](https://github.com/cline/kanban/discussions/228) asks for a custom board path so project-specific board state can be tracked in git.
+
+Planning stance:
+- brownfield only
+- no SDK changes
+- no invented APIs
+- default behavior must remain intact
+
+Recommended v1:
+- make `board.json` path configurable
+- do not redesign workspace identity
+- do not relocate `sessions.json` or `meta.json` in v1 unless required by evidence
+
+Read in this order:
+1. `05-refs-existing-behavior.md`
+2. `06-kit-configurable-board-path.md`
+3. `02-ticket-index.md`
+4. `07-validation.md`
+5. `08-tracking.md`
+
+Current status:
+- implementation complete for planned v1 scope
+- validation complete for executable targeted suites
+- one repo-level test limitation remains: `test/runtime/trpc/runtime-api.test.ts` is blocked by the existing `@clinebot/core` export-condition issue
diff --git a/src/config/runtime-config.ts b/src/config/runtime-config.ts
index 215228cd51..fd505175da 100644
--- a/src/config/runtime-config.ts
+++ b/src/config/runtime-config.ts
@@ -21,6 +21,7 @@ interface RuntimeGlobalConfigFileShape {
interface RuntimeProjectConfigFileShape {
shortcuts?: RuntimeProjectShortcut[];
+ boardPath?: string | null;
}
export interface RuntimeConfigState {
@@ -31,6 +32,7 @@ export interface RuntimeConfigState {
agentAutonomousModeEnabled: boolean;
readyForReviewNotificationsEnabled: boolean;
shortcuts: RuntimeProjectShortcut[];
+ boardPath: string | null;
commitPromptTemplate: string;
openPrPromptTemplate: string;
commitPromptTemplateDefault: string;
@@ -43,6 +45,7 @@ export interface RuntimeConfigUpdateInput {
agentAutonomousModeEnabled?: boolean;
readyForReviewNotificationsEnabled?: boolean;
shortcuts?: RuntimeProjectShortcut[];
+ boardPath?: string | null;
commitPromptTemplate?: string;
openPrPromptTemplate?: string;
}
@@ -193,6 +196,28 @@ function normalizeShortcutLabel(value: unknown): string | null {
return normalized.length > 0 ? normalized : null;
}
+function readProjectBoardPath(value: unknown): string | null {
+ if (typeof value !== "string") {
+ return null;
+ }
+ const normalized = value.trim();
+ return normalized.length > 0 ? normalized : null;
+}
+
+function normalizeProjectBoardPathInput(value: string | null | undefined): string | null | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (value === null) {
+ return null;
+ }
+ const normalized = value.trim();
+ if (normalized.length === 0) {
+ throw new Error("Board path cannot be empty.");
+ }
+ return normalized;
+}
+
function hasOwnKey(value: T | null, key: keyof T): boolean {
if (!value) {
return false;
@@ -284,6 +309,7 @@ function toRuntimeConfigState({
DEFAULT_READY_FOR_REVIEW_NOTIFICATIONS_ENABLED,
),
shortcuts: normalizeShortcuts(projectConfig?.shortcuts),
+ boardPath: readProjectBoardPath(projectConfig?.boardPath),
commitPromptTemplate: normalizePromptTemplate(globalConfig?.commitPromptTemplate, DEFAULT_COMMIT_PROMPT_TEMPLATE),
openPrPromptTemplate: normalizePromptTemplate(
globalConfig?.openPrPromptTemplate,
@@ -382,16 +408,17 @@ async function writeRuntimeGlobalConfigFile(
async function writeRuntimeProjectConfigFile(
configPath: string | null,
- config: { shortcuts: RuntimeProjectShortcut[] },
+ config: { shortcuts: RuntimeProjectShortcut[]; boardPath?: string | null },
): Promise {
const normalizedShortcuts = normalizeShortcuts(config.shortcuts);
+ const normalizedBoardPath = normalizeProjectBoardPathInput(config.boardPath) ?? null;
if (!configPath) {
- if (normalizedShortcuts.length > 0) {
- throw new Error("Cannot save project shortcuts without a selected project.");
+ if (normalizedShortcuts.length > 0 || normalizedBoardPath !== null) {
+ throw new Error("Cannot save project-specific settings without a selected project.");
}
return;
}
- if (normalizedShortcuts.length === 0) {
+ if (normalizedShortcuts.length === 0 && normalizedBoardPath === null) {
await rm(configPath, { force: true });
try {
await rm(dirname(configPath));
@@ -403,7 +430,8 @@ async function writeRuntimeProjectConfigFile(
await lockedFileSystem.writeJsonFileAtomic(
configPath,
{
- shortcuts: normalizedShortcuts,
+ ...(normalizedShortcuts.length > 0 ? { shortcuts: normalizedShortcuts } : {}),
+ ...(normalizedBoardPath !== null ? { boardPath: normalizedBoardPath } : {}),
} satisfies RuntimeProjectConfigFileShape,
{
lock: null,
@@ -454,6 +482,7 @@ function createRuntimeConfigStateFromValues(input: {
agentAutonomousModeEnabled: boolean;
readyForReviewNotificationsEnabled: boolean;
shortcuts: RuntimeProjectShortcut[];
+ boardPath: string | null;
commitPromptTemplate: string;
openPrPromptTemplate: string;
}): RuntimeConfigState {
@@ -471,6 +500,7 @@ function createRuntimeConfigStateFromValues(input: {
DEFAULT_READY_FOR_REVIEW_NOTIFICATIONS_ENABLED,
),
shortcuts: normalizeShortcuts(input.shortcuts),
+ boardPath: normalizeProjectBoardPathInput(input.boardPath) ?? null,
commitPromptTemplate: normalizePromptTemplate(input.commitPromptTemplate, DEFAULT_COMMIT_PROMPT_TEMPLATE),
openPrPromptTemplate: normalizePromptTemplate(input.openPrPromptTemplate, DEFAULT_OPEN_PR_PROMPT_TEMPLATE),
commitPromptTemplateDefault: DEFAULT_COMMIT_PROMPT_TEMPLATE,
@@ -487,6 +517,7 @@ export function toGlobalRuntimeConfigState(current: RuntimeConfigState): Runtime
agentAutonomousModeEnabled: current.agentAutonomousModeEnabled,
readyForReviewNotificationsEnabled: current.readyForReviewNotificationsEnabled,
shortcuts: [],
+ boardPath: null,
commitPromptTemplate: current.commitPromptTemplate,
openPrPromptTemplate: current.openPrPromptTemplate,
});
@@ -522,6 +553,7 @@ export async function saveRuntimeConfig(
agentAutonomousModeEnabled: boolean;
readyForReviewNotificationsEnabled: boolean;
shortcuts: RuntimeProjectShortcut[];
+ boardPath: string | null;
commitPromptTemplate: string;
openPrPromptTemplate: string;
},
@@ -536,7 +568,10 @@ export async function saveRuntimeConfig(
commitPromptTemplate: config.commitPromptTemplate,
openPrPromptTemplate: config.openPrPromptTemplate,
});
- await writeRuntimeProjectConfigFile(projectConfigPath, { shortcuts: config.shortcuts });
+ await writeRuntimeProjectConfigFile(projectConfigPath, {
+ shortcuts: config.shortcuts,
+ boardPath: config.boardPath,
+ });
return createRuntimeConfigStateFromValues({
globalConfigPath,
projectConfigPath,
@@ -545,6 +580,7 @@ export async function saveRuntimeConfig(
agentAutonomousModeEnabled: config.agentAutonomousModeEnabled,
readyForReviewNotificationsEnabled: config.readyForReviewNotificationsEnabled,
shortcuts: config.shortcuts,
+ boardPath: config.boardPath,
commitPromptTemplate: config.commitPromptTemplate,
openPrPromptTemplate: config.openPrPromptTemplate,
});
@@ -555,8 +591,12 @@ export async function updateRuntimeConfig(cwd: string, updates: RuntimeConfigUpd
const { globalConfigPath, projectConfigPath } = resolveRuntimeConfigPaths(cwd);
return await lockedFileSystem.withLocks(getRuntimeConfigLockRequests(cwd), async () => {
const current = await loadRuntimeConfigLocked(cwd);
- if (projectConfigPath === null && normalizeShortcuts(updates.shortcuts).length > 0) {
- throw new Error("Cannot save project shortcuts without a selected project.");
+ const normalizedBoardPathUpdate = normalizeProjectBoardPathInput(updates.boardPath);
+ if (
+ projectConfigPath === null &&
+ (normalizeShortcuts(updates.shortcuts).length > 0 || normalizedBoardPathUpdate !== undefined)
+ ) {
+ throw new Error("Cannot save project-specific settings without a selected project.");
}
const nextConfig = {
selectedAgentId: updates.selectedAgentId ?? current.selectedAgentId,
@@ -566,6 +606,10 @@ export async function updateRuntimeConfig(cwd: string, updates: RuntimeConfigUpd
readyForReviewNotificationsEnabled:
updates.readyForReviewNotificationsEnabled ?? current.readyForReviewNotificationsEnabled,
shortcuts: projectConfigPath ? (updates.shortcuts ?? current.shortcuts) : current.shortcuts,
+ boardPath:
+ projectConfigPath && normalizedBoardPathUpdate !== undefined
+ ? normalizedBoardPathUpdate
+ : current.boardPath,
commitPromptTemplate: updates.commitPromptTemplate ?? current.commitPromptTemplate,
openPrPromptTemplate: updates.openPrPromptTemplate ?? current.openPrPromptTemplate,
};
@@ -575,6 +619,7 @@ export async function updateRuntimeConfig(cwd: string, updates: RuntimeConfigUpd
nextConfig.selectedShortcutLabel !== current.selectedShortcutLabel ||
nextConfig.agentAutonomousModeEnabled !== current.agentAutonomousModeEnabled ||
nextConfig.readyForReviewNotificationsEnabled !== current.readyForReviewNotificationsEnabled ||
+ nextConfig.boardPath !== current.boardPath ||
nextConfig.commitPromptTemplate !== current.commitPromptTemplate ||
nextConfig.openPrPromptTemplate !== current.openPrPromptTemplate ||
!areRuntimeProjectShortcutsEqual(nextConfig.shortcuts, current.shortcuts);
@@ -593,6 +638,7 @@ export async function updateRuntimeConfig(cwd: string, updates: RuntimeConfigUpd
});
await writeRuntimeProjectConfigFile(projectConfigPath, {
shortcuts: nextConfig.shortcuts,
+ boardPath: nextConfig.boardPath,
});
return createRuntimeConfigStateFromValues({
globalConfigPath,
@@ -602,6 +648,7 @@ export async function updateRuntimeConfig(cwd: string, updates: RuntimeConfigUpd
agentAutonomousModeEnabled: nextConfig.agentAutonomousModeEnabled,
readyForReviewNotificationsEnabled: nextConfig.readyForReviewNotificationsEnabled,
shortcuts: nextConfig.shortcuts,
+ boardPath: nextConfig.boardPath,
commitPromptTemplate: nextConfig.commitPromptTemplate,
openPrPromptTemplate: nextConfig.openPrPromptTemplate,
});
@@ -631,6 +678,7 @@ export async function updateGlobalRuntimeConfig(
readyForReviewNotificationsEnabled:
updates.readyForReviewNotificationsEnabled ?? current.readyForReviewNotificationsEnabled,
shortcuts: current.shortcuts,
+ boardPath: current.boardPath,
commitPromptTemplate: updates.commitPromptTemplate ?? current.commitPromptTemplate,
openPrPromptTemplate: updates.openPrPromptTemplate ?? current.openPrPromptTemplate,
};
@@ -664,6 +712,7 @@ export async function updateGlobalRuntimeConfig(
agentAutonomousModeEnabled: nextConfig.agentAutonomousModeEnabled,
readyForReviewNotificationsEnabled: nextConfig.readyForReviewNotificationsEnabled,
shortcuts: nextConfig.shortcuts,
+ boardPath: nextConfig.boardPath,
commitPromptTemplate: nextConfig.commitPromptTemplate,
openPrPromptTemplate: nextConfig.openPrPromptTemplate,
});
diff --git a/src/core/api-contract.ts b/src/core/api-contract.ts
index ac2a2dfaec..ab759b777f 100644
--- a/src/core/api-contract.ts
+++ b/src/core/api-contract.ts
@@ -294,6 +294,7 @@ export type RuntimeTaskSessionSummary = z.infer {
+ const runtimeConfig = await loadRuntimeConfig(repoPath);
+ return resolveWorkspaceStoragePathsForOverride(repoPath, workspaceId, runtimeConfig.boardPath);
}
function getWorkspaceIndexLockRequest(): LockRequest {
@@ -292,28 +307,44 @@ function parseWorkspaceStateSavePayload(payload: RuntimeWorkspaceStateSaveReques
return parsed.data;
}
-async function readWorkspaceBoard(workspaceId: string): Promise {
- const boardPath = getWorkspaceBoardPath(workspaceId);
- const rawBoard = await readJsonFile(boardPath);
+async function readWorkspaceBoard(storagePaths: WorkspaceStoragePaths): Promise {
+ const rawBoard = await readJsonFile(storagePaths.boardPath);
return updateTaskDependencies(
- parsePersistedStateFile(boardPath, BOARD_FILENAME, rawBoard, runtimeBoardDataSchema, createEmptyBoard()),
+ parsePersistedStateFile(
+ storagePaths.boardPath,
+ BOARD_FILENAME,
+ rawBoard,
+ runtimeBoardDataSchema,
+ createEmptyBoard(),
+ ),
);
}
export async function loadWorkspaceBoardById(workspaceId: string): Promise {
- return await readWorkspaceBoard(workspaceId);
+ const context = await loadWorkspaceContextById(workspaceId);
+ if (!context) {
+ throw new Error(`Unknown workspace ID: ${workspaceId}`);
+ }
+ const storagePaths = await resolveWorkspaceStoragePaths(context.repoPath, context.workspaceId);
+ return await readWorkspaceBoard(storagePaths);
}
-async function readWorkspaceSessions(workspaceId: string): Promise> {
- const sessionsPath = getWorkspaceSessionsPath(workspaceId);
- const rawSessions = await readJsonFile(sessionsPath);
- return parsePersistedStateFile(sessionsPath, SESSIONS_FILENAME, rawSessions, workspaceSessionsSchema, {});
+async function readWorkspaceSessions(
+ storagePaths: WorkspaceStoragePaths,
+): Promise> {
+ const rawSessions = await readJsonFile(storagePaths.sessionsPath);
+ return parsePersistedStateFile(
+ storagePaths.sessionsPath,
+ SESSIONS_FILENAME,
+ rawSessions,
+ workspaceSessionsSchema,
+ {},
+ );
}
-async function readWorkspaceMeta(workspaceId: string): Promise {
- const metaPath = getWorkspaceMetaPath(workspaceId);
- const rawMeta = await readJsonFile(metaPath);
- return parsePersistedStateFile(metaPath, META_FILENAME, rawMeta, workspaceStateMetaSchema, {
+async function readWorkspaceMeta(storagePaths: WorkspaceStoragePaths): Promise {
+ const rawMeta = await readJsonFile(storagePaths.metaPath);
+ return parsePersistedStateFile(storagePaths.metaPath, META_FILENAME, rawMeta, workspaceStateMetaSchema, {
revision: 0,
updatedAt: 0,
});
@@ -524,13 +555,16 @@ async function resolveWorkspacePath(cwd: string): Promise {
function toWorkspaceStateResponse(
context: RuntimeWorkspaceContext,
+ storagePaths: WorkspaceStoragePaths,
board: RuntimeBoardData,
sessions: Record,
revision: number,
): RuntimeWorkspaceStateResponse {
+ const defaultBoardPath = join(storagePaths.statePath, BOARD_FILENAME);
return {
repoPath: context.repoPath,
- statePath: context.statePath,
+ statePath: storagePaths.statePath,
+ ...(storagePaths.boardPath !== defaultBoardPath ? { boardPath: storagePaths.boardPath } : {}),
git: context.git,
board,
sessions,
@@ -639,10 +673,11 @@ export async function removeWorkspaceStateFiles(workspaceId: string): Promise {
const context = await loadWorkspaceContext(cwd);
- const board = await readWorkspaceBoard(context.workspaceId);
- const sessions = await readWorkspaceSessions(context.workspaceId);
- const meta = await readWorkspaceMeta(context.workspaceId);
- return toWorkspaceStateResponse(context, board, sessions, meta.revision);
+ const storagePaths = await resolveWorkspaceStoragePaths(context.repoPath, context.workspaceId);
+ const board = await readWorkspaceBoard(storagePaths);
+ const sessions = await readWorkspaceSessions(storagePaths);
+ const meta = await readWorkspaceMeta(storagePaths);
+ return toWorkspaceStateResponse(context, storagePaths, board, sessions, meta.revision);
}
export async function saveWorkspaceState(
@@ -652,8 +687,8 @@ export async function saveWorkspaceState(
const parsedPayload = parseWorkspaceStateSavePayload(payload);
const context = await loadWorkspaceContext(cwd);
return await lockedFileSystem.withLock(getWorkspaceDirectoryLockRequest(context.workspaceId), async () => {
- const metaPath = getWorkspaceMetaPath(context.workspaceId);
- const currentMeta = await readWorkspaceMeta(context.workspaceId);
+ const storagePaths = await resolveWorkspaceStoragePaths(context.repoPath, context.workspaceId);
+ const currentMeta = await readWorkspaceMeta(storagePaths);
const expectedRevision = parsedPayload.expectedRevision;
if (
typeof expectedRevision === "number" &&
@@ -671,17 +706,17 @@ export async function saveWorkspaceState(
updatedAt: Date.now(),
};
- await lockedFileSystem.writeJsonFileAtomic(getWorkspaceBoardPath(context.workspaceId), board, {
+ await lockedFileSystem.writeJsonFileAtomic(storagePaths.boardPath, board, {
lock: null,
});
- await lockedFileSystem.writeJsonFileAtomic(getWorkspaceSessionsPath(context.workspaceId), sessions, {
+ await lockedFileSystem.writeJsonFileAtomic(storagePaths.sessionsPath, sessions, {
lock: null,
});
- await lockedFileSystem.writeJsonFileAtomic(metaPath, nextMeta, {
+ await lockedFileSystem.writeJsonFileAtomic(storagePaths.metaPath, nextMeta, {
lock: null,
});
- return toWorkspaceStateResponse(context, board, sessions, nextRevision);
+ return toWorkspaceStateResponse(context, storagePaths, board, sessions, nextRevision);
});
}
@@ -704,10 +739,17 @@ export async function mutateWorkspaceState(
): Promise> {
const context = await loadWorkspaceContext(cwd);
return await lockedFileSystem.withLock(getWorkspaceDirectoryLockRequest(context.workspaceId), async () => {
- const currentBoard = await readWorkspaceBoard(context.workspaceId);
- const currentSessions = await readWorkspaceSessions(context.workspaceId);
- const currentMeta = await readWorkspaceMeta(context.workspaceId);
- const currentState = toWorkspaceStateResponse(context, currentBoard, currentSessions, currentMeta.revision);
+ const storagePaths = await resolveWorkspaceStoragePaths(context.repoPath, context.workspaceId);
+ const currentBoard = await readWorkspaceBoard(storagePaths);
+ const currentSessions = await readWorkspaceSessions(storagePaths);
+ const currentMeta = await readWorkspaceMeta(storagePaths);
+ const currentState = toWorkspaceStateResponse(
+ context,
+ storagePaths,
+ currentBoard,
+ currentSessions,
+ currentMeta.revision,
+ );
const mutation = mutate(currentState);
if (mutation.save === false) {
@@ -726,20 +768,75 @@ export async function mutateWorkspaceState(
updatedAt: Date.now(),
};
- await lockedFileSystem.writeJsonFileAtomic(getWorkspaceBoardPath(context.workspaceId), nextBoard, {
+ await lockedFileSystem.writeJsonFileAtomic(storagePaths.boardPath, nextBoard, {
lock: null,
});
- await lockedFileSystem.writeJsonFileAtomic(getWorkspaceSessionsPath(context.workspaceId), nextSessions, {
+ await lockedFileSystem.writeJsonFileAtomic(storagePaths.sessionsPath, nextSessions, {
lock: null,
});
- await lockedFileSystem.writeJsonFileAtomic(getWorkspaceMetaPath(context.workspaceId), nextMeta, {
+ await lockedFileSystem.writeJsonFileAtomic(storagePaths.metaPath, nextMeta, {
lock: null,
});
return {
value: mutation.value,
- state: toWorkspaceStateResponse(context, nextBoard, nextSessions, nextRevision),
+ state: toWorkspaceStateResponse(context, storagePaths, nextBoard, nextSessions, nextRevision),
saved: true,
};
});
}
+
+export async function reconfigureWorkspaceBoardPath(
+ cwd: string,
+ previousBoardPathOverride: string | null,
+ nextBoardPathOverride: string | null,
+): Promise {
+ const context = await loadWorkspaceContext(cwd);
+ const previousPaths = resolveWorkspaceStoragePathsForOverride(
+ context.repoPath,
+ context.workspaceId,
+ previousBoardPathOverride,
+ );
+ const nextPaths = resolveWorkspaceStoragePathsForOverride(
+ context.repoPath,
+ context.workspaceId,
+ nextBoardPathOverride,
+ );
+ if (previousPaths.boardPath === nextPaths.boardPath) {
+ return;
+ }
+
+ await lockedFileSystem.withLock(getWorkspaceDirectoryLockRequest(context.workspaceId), async () => {
+ const previousBoardRaw = await readFile(previousPaths.boardPath, "utf8").catch((error: unknown) => {
+ if (isNodeErrorWithCode(error, "ENOENT")) {
+ return null;
+ }
+ throw error;
+ });
+ if (previousBoardRaw === null) {
+ return;
+ }
+
+ const nextBoardRaw = await readFile(nextPaths.boardPath, "utf8").catch((error: unknown) => {
+ if (isNodeErrorWithCode(error, "ENOENT")) {
+ return null;
+ }
+ throw error;
+ });
+ if (nextBoardRaw !== null) {
+ if (nextBoardRaw === previousBoardRaw) {
+ await rm(previousPaths.boardPath, { force: true });
+ return;
+ }
+ throw new Error(
+ `Could not move board file to ${nextPaths.boardPath} because a different file already exists there.`,
+ );
+ }
+
+ await mkdir(dirname(nextPaths.boardPath), { recursive: true });
+ await lockedFileSystem.writeTextFileAtomic(nextPaths.boardPath, previousBoardRaw, {
+ lock: null,
+ });
+ await rm(previousPaths.boardPath, { force: true });
+ });
+}
diff --git a/src/terminal/agent-registry.ts b/src/terminal/agent-registry.ts
index 4775a128b6..8a35c4897a 100644
--- a/src/terminal/agent-registry.ts
+++ b/src/terminal/agent-registry.ts
@@ -119,6 +119,7 @@ export function buildRuntimeConfigResponse(
detectedCommands,
agents,
shortcuts: runtimeConfig.shortcuts,
+ boardPath: runtimeConfig.boardPath,
clineProviderSettings,
commitPromptTemplate: runtimeConfig.commitPromptTemplate,
openPrPromptTemplate: runtimeConfig.openPrPromptTemplate,
diff --git a/src/trpc/runtime-api.ts b/src/trpc/runtime-api.ts
index b6ee43b270..8a8016f9d1 100644
--- a/src/trpc/runtime-api.ts
+++ b/src/trpc/runtime-api.ts
@@ -13,7 +13,7 @@ import { createClineProviderService } from "../cline-sdk/cline-provider-service"
import { isClineClearSlashCommand } from "../cline-sdk/cline-slash-commands";
import type { ClineTaskSessionService } from "../cline-sdk/cline-task-session-service";
import type { RuntimeConfigState } from "../config/runtime-config";
-import { updateGlobalRuntimeConfig, updateRuntimeConfig } from "../config/runtime-config";
+import { saveRuntimeConfig, updateGlobalRuntimeConfig, updateRuntimeConfig } from "../config/runtime-config";
import type { RuntimeCommandRunResponse } from "../core/api-contract";
import {
parseClineAccountSwitchRequest,
@@ -40,6 +40,7 @@ import {
import { isHomeAgentSessionId } from "../core/home-agent-session";
import { resolveTaskTitle } from "../core/task-title.js";
import { openInBrowser } from "../server/browser";
+import { reconfigureWorkspaceBoardPath } from "../state/workspace-state";
import { buildRuntimeConfigResponse, resolveAgentCommand } from "../terminal/agent-registry";
import type { TerminalSessionManager } from "../terminal/session-manager";
import { resolveTaskCwd } from "../workspace/task-worktree";
@@ -122,7 +123,29 @@ export function createRuntimeApi(deps: CreateRuntimeApiDependencies): RuntimeTrp
const parsed = parseRuntimeConfigSaveRequest(input);
let nextRuntimeConfig: RuntimeConfigState;
if (workspaceScope) {
+ const currentRuntimeConfig = await deps.loadScopedRuntimeConfig(workspaceScope);
nextRuntimeConfig = await updateRuntimeConfig(workspaceScope.workspacePath, parsed);
+ if (parsed.boardPath !== undefined && currentRuntimeConfig.boardPath !== nextRuntimeConfig.boardPath) {
+ try {
+ await reconfigureWorkspaceBoardPath(
+ workspaceScope.workspacePath,
+ currentRuntimeConfig.boardPath,
+ nextRuntimeConfig.boardPath,
+ );
+ } catch (error) {
+ await saveRuntimeConfig(workspaceScope.workspacePath, {
+ selectedAgentId: currentRuntimeConfig.selectedAgentId,
+ selectedShortcutLabel: currentRuntimeConfig.selectedShortcutLabel,
+ agentAutonomousModeEnabled: currentRuntimeConfig.agentAutonomousModeEnabled,
+ readyForReviewNotificationsEnabled: currentRuntimeConfig.readyForReviewNotificationsEnabled,
+ shortcuts: currentRuntimeConfig.shortcuts,
+ boardPath: currentRuntimeConfig.boardPath,
+ commitPromptTemplate: currentRuntimeConfig.commitPromptTemplate,
+ openPrPromptTemplate: currentRuntimeConfig.openPrPromptTemplate,
+ });
+ throw error;
+ }
+ }
} else {
const activeRuntimeConfig = deps.getActiveRuntimeConfig?.();
if (!activeRuntimeConfig) {
diff --git a/test/integration/workspace-state.integration.test.ts b/test/integration/workspace-state.integration.test.ts
index 6e169b8c0d..f7acdf64b8 100644
--- a/test/integration/workspace-state.integration.test.ts
+++ b/test/integration/workspace-state.integration.test.ts
@@ -1,9 +1,9 @@
import { spawnSync } from "node:child_process";
-import { mkdirSync, writeFileSync } from "node:fs";
+import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
-
+import { updateRuntimeConfig } from "../../src/config/runtime-config";
import type { RuntimeBoardData, RuntimeTaskSessionSummary } from "../../src/core/api-contract";
import type { WorkspaceStateConflictError } from "../../src/state/workspace-state";
import {
@@ -262,6 +262,64 @@ describe.sequential("workspace-state integration", () => {
});
});
+ it("loads and saves board state from a configured custom board path", async () => {
+ await withTemporaryHome(async () => {
+ const { path: sandboxRoot, cleanup } = createTempDir("kanban-custom-board-path-");
+ try {
+ const workspacePath = join(sandboxRoot, "project-custom-board");
+ mkdirSync(workspacePath, { recursive: true });
+ initGitRepository(workspacePath);
+
+ await updateRuntimeConfig(workspacePath, {
+ boardPath: ".kanban/board.json",
+ });
+
+ await saveWorkspaceState(workspacePath, {
+ board: createBoard("Custom path task"),
+ sessions: {},
+ expectedRevision: 0,
+ });
+
+ const context = await loadWorkspaceContext(workspacePath);
+ const customBoardPath = join(workspacePath, ".kanban", "board.json");
+ expect(existsSync(customBoardPath)).toBe(true);
+ expect(existsSync(join(context.statePath, "board.json"))).toBe(false);
+
+ const loaded = await loadWorkspaceState(workspacePath);
+ expect(loaded.board.columns[0]?.cards[0]?.prompt).toBe("Custom path task");
+ expect(loaded.statePath).toBe(context.statePath);
+ expect(loaded.boardPath).toBe(customBoardPath);
+ } finally {
+ cleanup();
+ }
+ });
+ });
+
+ it("omits boardPath from workspace state when the default board location is in use", async () => {
+ await withTemporaryHome(async () => {
+ const { path: sandboxRoot, cleanup } = createTempDir("kanban-default-board-path-");
+ try {
+ const workspacePath = join(sandboxRoot, "project-default-board");
+ mkdirSync(workspacePath, { recursive: true });
+ initGitRepository(workspacePath);
+
+ const saved = await saveWorkspaceState(workspacePath, {
+ board: createBoard("Default path task"),
+ sessions: {},
+ expectedRevision: 0,
+ });
+
+ const loaded = await loadWorkspaceState(workspacePath);
+ expect(saved.revision).toBe(1);
+ expect(loaded.board.columns[0]?.cards[0]?.prompt).toBe("Default path task");
+ expect(loaded.statePath).toBeTruthy();
+ expect(loaded).not.toHaveProperty("boardPath");
+ } finally {
+ cleanup();
+ }
+ });
+ });
+
it("fails loudly when persisted board data is malformed", async () => {
await withTemporaryHome(async () => {
const { path: sandboxRoot, cleanup } = createTempDir("kanban-malformed-board-");
@@ -308,6 +366,40 @@ describe.sequential("workspace-state integration", () => {
});
});
+ it("fails loudly when a configured custom board file is malformed", async () => {
+ await withTemporaryHome(async () => {
+ const { path: sandboxRoot, cleanup } = createTempDir("kanban-malformed-custom-board-");
+ try {
+ const workspacePath = join(sandboxRoot, "project-bad-custom-board");
+ mkdirSync(workspacePath, { recursive: true });
+ initGitRepository(workspacePath);
+
+ await updateRuntimeConfig(workspacePath, {
+ boardPath: ".kanban/board.json",
+ });
+
+ const customBoardDir = join(workspacePath, ".kanban");
+ mkdirSync(customBoardDir, { recursive: true });
+ writeFileSync(
+ join(customBoardDir, "board.json"),
+ JSON.stringify(
+ {
+ columns: [{ id: "backlog", title: "Backlog", cards: [{ prompt: "Missing ID" }] }],
+ },
+ null,
+ 2,
+ ),
+ "utf8",
+ );
+
+ await expect(loadWorkspaceState(workspacePath)).rejects.toThrow(join(customBoardDir, "board.json"));
+ await expect(loadWorkspaceState(workspacePath)).rejects.toThrow(/id|baseRef/);
+ } finally {
+ cleanup();
+ }
+ });
+ });
+
it("fails loudly when persisted sessions include unknown states", async () => {
await withTemporaryHome(async () => {
const { path: sandboxRoot, cleanup } = createTempDir("kanban-malformed-sessions-");
diff --git a/test/runtime/api-validation.test.ts b/test/runtime/api-validation.test.ts
index 72373e0958..d6cc008b55 100644
--- a/test/runtime/api-validation.test.ts
+++ b/test/runtime/api-validation.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
parseHookIngestRequest,
+ parseRuntimeConfigSaveRequest,
parseTaskSessionStartRequest,
parseWorkspaceFileSearchRequest,
} from "../../src/core/api-validation";
@@ -89,3 +90,31 @@ describe("parseTaskSessionStartRequest", () => {
});
});
});
+
+describe("parseRuntimeConfigSaveRequest", () => {
+ it("accepts nullable board path overrides", () => {
+ expect(
+ parseRuntimeConfigSaveRequest({
+ boardPath: ".kanban/board.json",
+ }),
+ ).toEqual({
+ boardPath: ".kanban/board.json",
+ });
+
+ expect(
+ parseRuntimeConfigSaveRequest({
+ boardPath: null,
+ }),
+ ).toEqual({
+ boardPath: null,
+ });
+ });
+
+ it("rejects empty board path overrides", () => {
+ expect(() => {
+ parseRuntimeConfigSaveRequest({
+ boardPath: " ",
+ });
+ }).toThrow();
+ });
+});
diff --git a/test/runtime/config/runtime-config.test.ts b/test/runtime/config/runtime-config.test.ts
index 884b383d73..56a95b96ee 100644
--- a/test/runtime/config/runtime-config.test.ts
+++ b/test/runtime/config/runtime-config.test.ts
@@ -292,6 +292,7 @@ describe.sequential("runtime-config auto agent selection", () => {
agentAutonomousModeEnabled: true,
readyForReviewNotificationsEnabled: true,
shortcuts: [],
+ boardPath: null,
commitPromptTemplate: current.commitPromptTemplateDefault,
openPrPromptTemplate: current.openPrPromptTemplateDefault,
});
@@ -337,6 +338,7 @@ describe.sequential("runtime-config auto agent selection", () => {
agentAutonomousModeEnabled: true,
readyForReviewNotificationsEnabled: true,
shortcuts: [],
+ boardPath: null,
commitPromptTemplate: current.commitPromptTemplateDefault,
openPrPromptTemplate: current.openPrPromptTemplateDefault,
});
@@ -364,6 +366,7 @@ describe.sequential("runtime-config auto agent selection", () => {
agentAutonomousModeEnabled: true,
readyForReviewNotificationsEnabled: true,
shortcuts: [{ label: "Ship", command: "npm run ship", icon: "rocket" }],
+ boardPath: null,
commitPromptTemplate: current.commitPromptTemplateDefault,
openPrPromptTemplate: current.openPrPromptTemplateDefault,
});
@@ -413,6 +416,41 @@ describe.sequential("runtime-config auto agent selection", () => {
}
});
+ it("persists project board path without requiring shortcuts", async () => {
+ const { path: tempHome, cleanup: cleanupHome } = createTempDir("kanban-home-runtime-config-board-path-");
+ const { path: tempProject, cleanup: cleanupProject } = createTempDir("kanban-project-runtime-config-board-path-");
+
+ try {
+ await withTemporaryEnv({ home: tempHome }, async () => {
+ const updated = await updateRuntimeConfig(tempProject, {
+ boardPath: ".kanban/board.json",
+ });
+ expect(updated.boardPath).toBe(".kanban/board.json");
+
+ const projectPayload = JSON.parse(
+ readFileSync(join(tempProject, ".cline", "kanban", "config.json"), "utf8"),
+ ) as {
+ boardPath?: string;
+ shortcuts?: unknown;
+ };
+ expect(projectPayload.boardPath).toBe(".kanban/board.json");
+ expect(projectPayload.shortcuts).toBeUndefined();
+
+ const reloaded = await loadRuntimeConfig(tempProject);
+ expect(reloaded.boardPath).toBe(".kanban/board.json");
+
+ const cleared = await updateRuntimeConfig(tempProject, {
+ boardPath: null,
+ });
+ expect(cleared.boardPath).toBeNull();
+ expect(existsSync(join(tempProject, ".cline", "kanban", "config.json"))).toBe(false);
+ });
+ } finally {
+ cleanupProject();
+ cleanupHome();
+ }
+ });
+
it("persists autonomous mode when disabled", async () => {
const { path: tempHome, cleanup: cleanupHome } = createTempDir("kanban-home-runtime-config-autonomous-disabled-");
const { path: tempProject, cleanup: cleanupProject } = createTempDir(
diff --git a/test/runtime/terminal/agent-registry.test.ts b/test/runtime/terminal/agent-registry.test.ts
index 0c54bd147b..a2d8340334 100644
--- a/test/runtime/terminal/agent-registry.test.ts
+++ b/test/runtime/terminal/agent-registry.test.ts
@@ -24,6 +24,7 @@ function createRuntimeConfigState(overrides: Partial = {}):
agentAutonomousModeEnabled: true,
readyForReviewNotificationsEnabled: true,
shortcuts: [],
+ boardPath: null,
commitPromptTemplate: "commit",
openPrPromptTemplate: "pr",
commitPromptTemplateDefault: "commit",
diff --git a/test/runtime/trpc/runtime-api.test.ts b/test/runtime/trpc/runtime-api.test.ts
index 9a4cd6ea2d..cb46774717 100644
--- a/test/runtime/trpc/runtime-api.test.ts
+++ b/test/runtime/trpc/runtime-api.test.ts
@@ -55,6 +55,16 @@ const browserMocks = vi.hoisted(() => ({
openInBrowser: vi.fn(),
}));
+const runtimeConfigMocks = vi.hoisted(() => ({
+ saveRuntimeConfig: vi.fn(),
+ updateGlobalRuntimeConfig: vi.fn(),
+ updateRuntimeConfig: vi.fn(),
+}));
+
+const workspaceStateMocks = vi.hoisted(() => ({
+ reconfigureWorkspaceBoardPath: vi.fn(),
+}));
+
vi.mock("../../../src/terminal/agent-registry.js", () => ({
resolveAgentCommand: agentRegistryMocks.resolveAgentCommand,
buildRuntimeConfigResponse: agentRegistryMocks.buildRuntimeConfigResponse,
@@ -121,6 +131,16 @@ vi.mock("../../../src/server/browser.js", () => ({
openInBrowser: browserMocks.openInBrowser,
}));
+vi.mock("../../../src/config/runtime-config.js", () => ({
+ saveRuntimeConfig: runtimeConfigMocks.saveRuntimeConfig,
+ updateGlobalRuntimeConfig: runtimeConfigMocks.updateGlobalRuntimeConfig,
+ updateRuntimeConfig: runtimeConfigMocks.updateRuntimeConfig,
+}));
+
+vi.mock("../../../src/state/workspace-state.js", () => ({
+ reconfigureWorkspaceBoardPath: workspaceStateMocks.reconfigureWorkspaceBoardPath,
+}));
+
import { createRuntimeApi } from "../../../src/trpc/runtime-api";
function createSummary(overrides: Partial = {}): RuntimeTaskSessionSummary {
@@ -150,6 +170,7 @@ function createRuntimeConfigState(): RuntimeConfigState {
agentAutonomousModeEnabled: true,
readyForReviewNotificationsEnabled: true,
shortcuts: [],
+ boardPath: null,
commitPromptTemplate: "commit",
openPrPromptTemplate: "pr",
commitPromptTemplateDefault: "commit",
@@ -251,6 +272,10 @@ describe("createRuntimeApi startTaskSession", () => {
llmsModelMocks.getAllProviders.mockReset();
llmsModelMocks.getModelsForProvider.mockReset();
browserMocks.openInBrowser.mockReset();
+ runtimeConfigMocks.saveRuntimeConfig.mockReset();
+ runtimeConfigMocks.updateGlobalRuntimeConfig.mockReset();
+ runtimeConfigMocks.updateRuntimeConfig.mockReset();
+ workspaceStateMocks.reconfigureWorkspaceBoardPath.mockReset();
agentRegistryMocks.resolveAgentCommand.mockReturnValue({
agentId: "claude",
@@ -327,6 +352,16 @@ describe("createRuntimeApi startTaskSession", () => {
}),
});
setSelectedProviderSettings(null);
+ runtimeConfigMocks.updateGlobalRuntimeConfig.mockImplementation(async (current, updates) => ({
+ ...current,
+ ...updates,
+ }));
+ runtimeConfigMocks.updateRuntimeConfig.mockImplementation(async (_cwd, updates) => ({
+ ...createRuntimeConfigState(),
+ ...updates,
+ }));
+ runtimeConfigMocks.saveRuntimeConfig.mockImplementation(async (_cwd, config) => config);
+ workspaceStateMocks.reconfigureWorkspaceBoardPath.mockResolvedValue(undefined);
llmsModelMocks.getAllProviders.mockResolvedValue([
{
id: "cline",
@@ -2693,3 +2728,79 @@ describe("createRuntimeApi getFeaturebaseToken", () => {
expect(oauthMocks.getValidClineCredentials).toHaveBeenCalledTimes(1);
});
});
+
+describe("createRuntimeApi saveConfig", () => {
+ it("rolls back the full workspace config when board-path reconfiguration fails", async () => {
+ const currentConfig = createRuntimeConfigState();
+ const nextShortcuts = [{ label: "Ship", command: "npm run ship", icon: "rocket" }];
+ const nextConfig = {
+ ...currentConfig,
+ selectedAgentId: "codex" as const,
+ shortcuts: nextShortcuts,
+ boardPath: ".kanban/board.json",
+ commitPromptTemplate: "next commit",
+ openPrPromptTemplate: "next pr",
+ };
+ const setActiveRuntimeConfig = vi.fn();
+ runtimeConfigMocks.updateRuntimeConfig.mockResolvedValue(nextConfig);
+ workspaceStateMocks.reconfigureWorkspaceBoardPath.mockRejectedValue(new Error("Destination already exists."));
+ agentRegistryMocks.buildRuntimeConfigResponse.mockImplementation((config) => config);
+
+ const api = createRuntimeApi({
+ getActiveWorkspaceId: () => "workspace-1",
+ getActiveRuntimeConfig: () => currentConfig,
+ loadScopedRuntimeConfig: async () => currentConfig,
+ setActiveRuntimeConfig,
+ getScopedTerminalManager: async () => ({}) as never,
+ getScopedClineTaskSessionService: async () => createClineTaskSessionServiceMock() as never,
+ resolveInteractiveShellCommand: () => ({ binary: "bash", args: [] }),
+ runCommand: async () => ({
+ exitCode: 0,
+ stdout: "",
+ stderr: "",
+ combinedOutput: "",
+ durationMs: 0,
+ }),
+ });
+
+ await expect(
+ api.saveConfig(
+ {
+ workspaceId: "workspace-1",
+ workspacePath: "/tmp/repo",
+ },
+ {
+ selectedAgentId: "codex",
+ shortcuts: nextShortcuts,
+ boardPath: ".kanban/board.json",
+ commitPromptTemplate: "next commit",
+ openPrPromptTemplate: "next pr",
+ },
+ ),
+ ).rejects.toThrow("Destination already exists.");
+
+ expect(runtimeConfigMocks.updateRuntimeConfig).toHaveBeenCalledWith("/tmp/repo", {
+ selectedAgentId: "codex",
+ shortcuts: nextShortcuts,
+ boardPath: ".kanban/board.json",
+ commitPromptTemplate: "next commit",
+ openPrPromptTemplate: "next pr",
+ });
+ expect(workspaceStateMocks.reconfigureWorkspaceBoardPath).toHaveBeenCalledWith(
+ "/tmp/repo",
+ null,
+ ".kanban/board.json",
+ );
+ expect(runtimeConfigMocks.saveRuntimeConfig).toHaveBeenCalledWith("/tmp/repo", {
+ selectedAgentId: currentConfig.selectedAgentId,
+ selectedShortcutLabel: currentConfig.selectedShortcutLabel,
+ agentAutonomousModeEnabled: currentConfig.agentAutonomousModeEnabled,
+ readyForReviewNotificationsEnabled: currentConfig.readyForReviewNotificationsEnabled,
+ shortcuts: currentConfig.shortcuts,
+ boardPath: currentConfig.boardPath,
+ commitPromptTemplate: currentConfig.commitPromptTemplate,
+ openPrPromptTemplate: currentConfig.openPrPromptTemplate,
+ });
+ expect(setActiveRuntimeConfig).not.toHaveBeenCalled();
+ });
+});
diff --git a/web-ui/src/components/runtime-settings-dialog.tsx b/web-ui/src/components/runtime-settings-dialog.tsx
index 314e80c682..5858079000 100644
--- a/web-ui/src/components/runtime-settings-dialog.tsx
+++ b/web-ui/src/components/runtime-settings-dialog.tsx
@@ -373,6 +373,7 @@ export function RuntimeSettingsDialog({
const [draftThemeId, setDraftThemeId] = useState(readStoredThemeId);
const [notificationPermission, setNotificationPermission] = useState("unsupported");
const [shortcuts, setShortcuts] = useState([]);
+ const [boardPath, setBoardPath] = useState("");
const [commitPromptTemplate, setCommitPromptTemplate] = useState("");
const [openPrPromptTemplate, setOpenPrPromptTemplate] = useState("");
const [selectedPromptVariant, setSelectedPromptVariant] = useState("commit");
@@ -443,6 +444,7 @@ export function RuntimeSettingsDialog({
const initialAgentAutonomousModeEnabled = config?.agentAutonomousModeEnabled ?? true;
const initialReadyForReviewNotificationsEnabled = config?.readyForReviewNotificationsEnabled ?? true;
const initialShortcuts = config?.shortcuts ?? [];
+ const initialBoardPath = config?.boardPath ?? "";
const initialCommitPromptTemplate = config?.commitPromptTemplate ?? "";
const initialOpenPrPromptTemplate = config?.openPrPromptTemplate ?? "";
const clineSettings = useRuntimeSettingsClineController({
@@ -482,6 +484,9 @@ export function RuntimeSettingsDialog({
if (!areRuntimeProjectShortcutsEqual(shortcuts, initialShortcuts)) {
return true;
}
+ if (boardPath.trim() !== initialBoardPath.trim()) {
+ return true;
+ }
if (
normalizeTemplateForComparison(commitPromptTemplate) !==
normalizeTemplateForComparison(initialCommitPromptTemplate)
@@ -494,12 +499,14 @@ export function RuntimeSettingsDialog({
);
}, [
agentAutonomousModeEnabled,
+ boardPath,
clineMcpSettings.hasUnsavedChanges,
clineSettings.hasUnsavedChanges,
commitPromptTemplate,
config,
draftThemeId,
initialAgentAutonomousModeEnabled,
+ initialBoardPath,
initialCommitPromptTemplate,
initialOpenPrPromptTemplate,
initialReadyForReviewNotificationsEnabled,
@@ -520,11 +527,13 @@ export function RuntimeSettingsDialog({
setAgentAutonomousModeEnabled(config?.agentAutonomousModeEnabled ?? true);
setReadyForReviewNotificationsEnabled(config?.readyForReviewNotificationsEnabled ?? true);
setShortcuts(config?.shortcuts ?? []);
+ setBoardPath(config?.boardPath ?? "");
setCommitPromptTemplate(config?.commitPromptTemplate ?? "");
setOpenPrPromptTemplate(config?.openPrPromptTemplate ?? "");
setSaveError(null);
}, [
config?.agentAutonomousModeEnabled,
+ config?.boardPath,
config?.commitPromptTemplate,
config?.openPrPromptTemplate,
config?.readyForReviewNotificationsEnabled,
@@ -702,6 +711,7 @@ export function RuntimeSettingsDialog({
agentAutonomousModeEnabled,
readyForReviewNotificationsEnabled,
shortcuts,
+ boardPath: boardPath.trim().length > 0 ? boardPath.trim() : null,
commitPromptTemplate,
openPrPromptTemplate,
});
@@ -1066,6 +1076,37 @@ export function RuntimeSettingsDialog({
: "/.cline/kanban/config.json"}
{config?.projectConfigPath ? : null}
+
+
+
+ Board file path
+
+
+
+
{
+ setBoardPath(event.target.value);
+ }}
+ disabled={controlsDisabled}
+ placeholder="board.json or .kanban/board.json"
+ className="w-full rounded-md border border-border bg-surface-2 px-3 py-2 text-[13px] text-text-primary outline-none transition focus:border-border-focus"
+ />
+
+ Relative paths resolve from the project root. Leave empty to keep the default board at{" "}
+ <statePath>/board.json.
+
+
= {}): Ru
},
],
shortcuts: [],
+ boardPath: null,
clineProviderSettings: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
diff --git a/web-ui/src/hooks/use-runtime-settings-cline-controller.test.tsx b/web-ui/src/hooks/use-runtime-settings-cline-controller.test.tsx
index 52f9c8b006..d25e032bb0 100644
--- a/web-ui/src/hooks/use-runtime-settings-cline-controller.test.tsx
+++ b/web-ui/src/hooks/use-runtime-settings-cline-controller.test.tsx
@@ -82,6 +82,7 @@ function createRuntimeConfigResponse(
},
],
shortcuts: [],
+ boardPath: null,
clineProviderSettings: {
providerId: "cline",
modelId: "claude-sonnet-4-6",
diff --git a/web-ui/src/hooks/use-startup-onboarding.test.tsx b/web-ui/src/hooks/use-startup-onboarding.test.tsx
index 3f88430ac7..b3b1fef259 100644
--- a/web-ui/src/hooks/use-startup-onboarding.test.tsx
+++ b/web-ui/src/hooks/use-startup-onboarding.test.tsx
@@ -36,6 +36,7 @@ function createRuntimeConfigResponse(selectedAgentId: RuntimeConfigResponse["sel
},
],
shortcuts: [],
+ boardPath: null,
clineProviderSettings: {
providerId: null,
modelId: null,
diff --git a/web-ui/src/runtime/native-agent.test.ts b/web-ui/src/runtime/native-agent.test.ts
index 59671565c1..ec2a1c4230 100644
--- a/web-ui/src/runtime/native-agent.test.ts
+++ b/web-ui/src/runtime/native-agent.test.ts
@@ -44,6 +44,7 @@ function createRuntimeConfigResponse(
},
],
shortcuts: [],
+ boardPath: null,
clineProviderSettings: {
providerId: "cline",
modelId: "sonnet",
diff --git a/web-ui/src/runtime/runtime-config-query.ts b/web-ui/src/runtime/runtime-config-query.ts
index 6e413c090e..5c4853f9e2 100644
--- a/web-ui/src/runtime/runtime-config-query.ts
+++ b/web-ui/src/runtime/runtime-config-query.ts
@@ -43,6 +43,7 @@ export async function saveRuntimeConfig(
selectedShortcutLabel?: string | null;
agentAutonomousModeEnabled?: boolean;
shortcuts?: RuntimeProjectShortcut[];
+ boardPath?: string | null;
readyForReviewNotificationsEnabled?: boolean;
commitPromptTemplate?: string;
openPrPromptTemplate?: string;
diff --git a/web-ui/src/runtime/use-runtime-config.test.tsx b/web-ui/src/runtime/use-runtime-config.test.tsx
index 58f043d526..5ca9405a9f 100644
--- a/web-ui/src/runtime/use-runtime-config.test.tsx
+++ b/web-ui/src/runtime/use-runtime-config.test.tsx
@@ -45,6 +45,7 @@ function createRuntimeConfigResponse(selectedAgentId: RuntimeConfigResponse["sel
},
],
shortcuts: [],
+ boardPath: null,
clineProviderSettings: {
providerId: null,
modelId: null,
diff --git a/web-ui/src/runtime/use-runtime-config.ts b/web-ui/src/runtime/use-runtime-config.ts
index 0f8d222004..da7e1b468a 100644
--- a/web-ui/src/runtime/use-runtime-config.ts
+++ b/web-ui/src/runtime/use-runtime-config.ts
@@ -14,6 +14,7 @@ export interface UseRuntimeConfigResult {
selectedShortcutLabel?: string | null;
agentAutonomousModeEnabled?: boolean;
shortcuts?: RuntimeProjectShortcut[];
+ boardPath?: string | null;
readyForReviewNotificationsEnabled?: boolean;
commitPromptTemplate?: string;
openPrPromptTemplate?: string;
@@ -81,6 +82,7 @@ export function useRuntimeConfig(
selectedShortcutLabel?: string | null;
agentAutonomousModeEnabled?: boolean;
shortcuts?: RuntimeProjectShortcut[];
+ boardPath?: string | null;
readyForReviewNotificationsEnabled?: boolean;
commitPromptTemplate?: string;
openPrPromptTemplate?: string;
diff --git a/web-ui/src/runtime/use-runtime-project-config.test.tsx b/web-ui/src/runtime/use-runtime-project-config.test.tsx
index 293e19b0ba..73a9556816 100644
--- a/web-ui/src/runtime/use-runtime-project-config.test.tsx
+++ b/web-ui/src/runtime/use-runtime-project-config.test.tsx
@@ -55,6 +55,7 @@ function createRuntimeConfigResponse(
},
],
shortcuts,
+ boardPath: null,
clineProviderSettings: {
providerId: null,
modelId: null,