IW-423: Configured terminal workspace manager for the dashboard #444

Merged
mph merged 36 commits from IW-423 into main 2026-08-24 09:50:43 +00:00
Owner

Closes IW-423.

Summary

The dashboard is a long-lived daemon with no invoking terminal, so it could not use
iw's existing rule for picking a terminal workspace manager — and worse, it inherits
HERDR_ENV at spawn time from whatever terminal started it, a value that can be days
old and describe a pane that no longer exists. Its create-worktree button therefore
always drove tmux, whatever the user actually runs.

This branch gives the pure selection rule a second input — a server-scoped configured
default — and gives the dashboard its own binder that supplies that input safely, while
the CLI keeps resolving purely from the environment.

Changes

Selection rule and config (core/)

  • ServerConfig gains an optional workspaceBackend field, read only by the dashboard.
  • TerminalWorkspaceBackend.select becomes a two-input pure function (env, configuredName)
    with the decided precedence: recognised IW_WORKSPACE_BACKEND > HERDR_ENV=1 >
    configured default > tmux. A typo in either source is an error, not a silent fallback,
    and a source is parsed only when the precedence reaches it.
  • WorkspaceBackend is extracted to its own file with a pure resolve; its lazy val selected passes None, so the CLI ignores the setting entirely.
  • Constants.ServerConfigKeys.WorkspaceBackend names the config key, so the error
    message and the field cannot drift apart silently.

Dashboard binder and de-tmuxing sweep (dashboard/)

  • ServerWorkspace resolves the backend for the daemon, filtering its environment down
    to IW_WORKSPACE_BACKEND only — HERDR_ENV never reaches the rule from a server process.
  • The worktree-creation path moves off TmuxAdapter.createSession onto the same
    select/opsFor composition the CLI uses.
  • Full de-tmuxing: WorktreeCreationError.TmuxError becomes TerminalWorkspaceError;
    WorktreeCreationResult's tmuxSessionName/tmuxAttachCommand become
    workspaceName/attachCommand; attach and failure hints render from ops.attachHint
    and ops.unavailableHint instead of hand-written tmux strings.
  • A config error breaks only the backend decision — the port and host list from the same
    file are unaffected.

Docs

  • docs/server-config.md documents the new key and states plainly that it steers the
    dashboard only, not iw start/open/rm.
  • docs/api/ServerTypes.md, docs/api/Constants.md and llms.txt refreshed for the
    changed modules.

Accepted limitation

A worktree created from the dashboard as a herdr workspace, then opened later by iw open
from a plain terminal, gets a second workspace in tmux. This follows from decision 3 (the
setting is server-scoped) and is documented; the workaround is IW_WORKSPACE_BACKEND=herdr
in the shell rc.

Testing

  • ./mill core.test — 186 pass
  • ./mill dashboard.test — 241 pass (whole unit tier)
  • ./iw ./test e2e — 188 BATS tests pass, 0 failures
  • Phase 2 closes with a case-insensitive tmux grep across dashboard/jvm/ — the sweep
    is verified, not asserted.

Both phase PRs (#434, #435) passed code review with 0 critical findings. One warning found
in Phase 2 review was fixed: an inline onclick handler in CreationSuccessView that this
phase's change made reachable with attacker-influenced text, replaced by a data-copy-text
attribute.

Lessons triaged

20 entries triaged before this PR; 0 pending. Filed as iw-cli issues #436-#443 and kanon
notes #183, #184. Two promotions landed in this branch (WorkspaceBackendTest guard
comment; the config-key constant in Phase 2), one was rejected, one retained, one deleted.

Issue #441 is a security finding raised by the Phase 2 review — the dashboard creation
handler anchors filesystem paths on a client-supplied projectPath with no allow-list
check. The behaviour predates this branch, which only changed the callee of one of the
three operations sharing that anchor.

Release notes

project-management/issues/IW-423/release-notes.md (Czech). Note: VERSION is not bumped
by this branch — the change is additive, so the version line was left out of the notes
until the release number is decided.

Review

Start with project-management/issues/IW-423/review-packet.md — it names the entry points,
the precedence and component diagrams, and the renamed-identifier table for the sweep.

Closes IW-423. ## Summary The dashboard is a long-lived daemon with no invoking terminal, so it could not use `iw`'s existing rule for picking a terminal workspace manager — and worse, it inherits `HERDR_ENV` at spawn time from whatever terminal started it, a value that can be days old and describe a pane that no longer exists. Its create-worktree button therefore always drove tmux, whatever the user actually runs. This branch gives the pure selection rule a second input — a server-scoped configured default — and gives the dashboard its own binder that supplies that input safely, while the CLI keeps resolving purely from the environment. ## Changes **Selection rule and config (`core/`)** - `ServerConfig` gains an optional `workspaceBackend` field, read only by the dashboard. - `TerminalWorkspaceBackend.select` becomes a two-input pure function `(env, configuredName)` with the decided precedence: recognised `IW_WORKSPACE_BACKEND` > `HERDR_ENV=1` > configured default > tmux. A typo in either source is an error, not a silent fallback, and a source is parsed only when the precedence reaches it. - `WorkspaceBackend` is extracted to its own file with a pure `resolve`; its `lazy val selected` passes `None`, so the CLI ignores the setting entirely. - `Constants.ServerConfigKeys.WorkspaceBackend` names the config key, so the error message and the field cannot drift apart silently. **Dashboard binder and de-tmuxing sweep (`dashboard/`)** - `ServerWorkspace` resolves the backend for the daemon, filtering its environment down to `IW_WORKSPACE_BACKEND` only — `HERDR_ENV` never reaches the rule from a server process. - The worktree-creation path moves off `TmuxAdapter.createSession` onto the same `select`/`opsFor` composition the CLI uses. - Full de-tmuxing: `WorktreeCreationError.TmuxError` becomes `TerminalWorkspaceError`; `WorktreeCreationResult`'s `tmuxSessionName`/`tmuxAttachCommand` become `workspaceName`/`attachCommand`; attach and failure hints render from `ops.attachHint` and `ops.unavailableHint` instead of hand-written tmux strings. - A config error breaks only the backend decision — the port and host list from the same file are unaffected. **Docs** - `docs/server-config.md` documents the new key and states plainly that it steers the dashboard only, not `iw start`/`open`/`rm`. - `docs/api/ServerTypes.md`, `docs/api/Constants.md` and `llms.txt` refreshed for the changed modules. ## Accepted limitation A worktree created from the dashboard as a herdr workspace, then opened later by `iw open` from a plain terminal, gets a second workspace in tmux. This follows from decision 3 (the setting is server-scoped) and is documented; the workaround is `IW_WORKSPACE_BACKEND=herdr` in the shell rc. ## Testing - `./mill core.test` — 186 pass - `./mill dashboard.test` — 241 pass (whole unit tier) - `./iw ./test e2e` — 188 BATS tests pass, 0 failures - Phase 2 closes with a case-insensitive `tmux` grep across `dashboard/jvm/` — the sweep is verified, not asserted. Both phase PRs (#434, #435) passed code review with 0 critical findings. One warning found in Phase 2 review was fixed: an inline `onclick` handler in `CreationSuccessView` that this phase's change made reachable with attacker-influenced text, replaced by a `data-copy-text` attribute. ## Lessons triaged 20 entries triaged before this PR; 0 pending. Filed as iw-cli issues #436-#443 and kanon notes #183, #184. Two promotions landed in this branch (`WorkspaceBackendTest` guard comment; the config-key constant in Phase 2), one was rejected, one retained, one deleted. Issue #441 is a **security finding** raised by the Phase 2 review — the dashboard creation handler anchors filesystem paths on a client-supplied `projectPath` with no allow-list check. The behaviour predates this branch, which only changed the callee of one of the three operations sharing that anchor. ## Release notes `project-management/issues/IW-423/release-notes.md` (Czech). Note: `VERSION` is not bumped by this branch — the change is additive, so the version line was left out of the notes until the release number is decided. ## Review Start with `project-management/issues/IW-423/review-packet.md` — it names the entry points, the precedence and component diagrams, and the renamed-identifier table for the sweep.
mph added 34 commits 2026-08-23 20:01:36 +00:00
Configured terminal workspace manager for the dashboard

Artifacts:
- analysis.md

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w2RExuf1LXh5SvN3FJVhK
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w2RExuf1LXh5SvN3FJVhK
Resolved:
- analysis.md: Precedence of the configured default → environment detection
  outranks it (IW_WORKSPACE_BACKEND > HERDR_ENV > config > tmux)
- analysis.md: The daemon's environment → trust only IW_WORKSPACE_BACKEND,
  never the inherited HERDR_ENV
- analysis.md: Where the setting lives → server-scoped on ServerConfig; the
  dashboard reads it, the CLI ignores it and keeps env-only detection
- analysis.md: De-tmuxing sweep → full (TerminalWorkspaceError, renamed result
  fields, hints rendered from ops.attachHint)

The scoping decision removed the CLI config read, so the estimate drops from
7-11h to 6-9h and the phase split becomes clean by module: Phase 1 core/,
Phase 2 dashboard/.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w2RExuf1LXh5SvN3FJVhK
Configured terminal workspace manager for the dashboard

Artifacts:
- tasks.md
- review-state.json (tasks_ready)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w2RExuf1LXh5SvN3FJVhK
Three findings from the task-index run:
- --needs-attention is a presence flag, so "--needs-attention false" sets it true
- wf-create-tasks appends an implement action wf-create-analysis already wrote
- Mill daemon crashed intermittently between back-to-back ./iw calls

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w2RExuf1LXh5SvN3FJVhK
Issue: IW-423
Phase: 1 - Selection rule + config field

This commit marks the checkpoint for phase 1 context generation.
Use wf-implement IW-423 to return to this point.

🤖 Generated with Claude Code
Issue: IW-423

- --checkpoint takes phase:sha; the command text's phase:context_sha=sha
  double-writes the key
- review-state update preserves git_sha rather than refreshing it

🤖 Generated with Claude Code
Issue: IW-423
Phase: 1 - Selection rule + config field

A source is parsed only when the precedence reaches it. Decided by Michal
on 2026-08-23; the deciding cell is pinned by the test table.

🤖 Generated with Claude Code
Selection rule + config field

Artifacts:
- phase-01-tasks.md

🤖 Generated with Claude Code
LESSONS entries 11-12.
Issue: IW-423
Phase: 2 - Dashboard binder + de-tmuxing sweep

This commit marks the checkpoint for phase 2 context generation.
Use wf-implement IW-423 to return to this point.

🤖 Generated with Claude Code
Record the checkpoint-replace and stale git_sha traps in LESSONS.
Decided with Michal: the dashboard binder calls WorkspaceBackend.resolve and
nothing else from the shared rule, so resolve gains the production caller
phase-01-context.md promised it.

The binder needed the backend by name for one suggestion string ("check that
tmux is running"). Rather than carry a name across three layers - or change
resolve's signature one PR after it merged - the sentence moves onto the port:
TerminalWorkspaceOps gains unavailableHint, a fourth hint beside attachHint,
focusHint and leaveHint, where each implementation writes its own text. That
also produces better copy: herdr is a server, not a multiplexer.

Context changes:
- ServerWorkspace holds trustedEnvironment, workspaceFor, configuredBackend,
  forRequest. backendFor, opsFor and backendName are gone; forRequest returns
  TerminalWorkspaceOps rather than an Either.
- toUserFriendly takes (error, issueId, attachHint, unavailableHint).
- "The two core changes" documents unavailableHint and its four implementations
  alongside the existing Constants key change.
- Testing strategy, files list, acceptance criteria and estimate note follow.

Rejected alternatives are recorded in the settled-decision section: accepting a
resolve with no caller, backend-neutral wording, and returning (backend, ops).

🤖 Generated with Claude Code
Dashboard binder + de-tmuxing sweep

127 tagged tasks across nine groups, ordered for TDD: setup, the two core
changes, the pure renames, the binder, the wiring, the docs, the generated-docs
refresh, then the verification gate.

One ordering correction the context's "Approach" does not make, recorded in a
"Compile constraints" block rather than applied silently: the three signature
additions (WorktreeCreationService's attachHint, toUserFriendly's two
parameters) cannot compile before the binder exists, because CaskServer has no
honest source for the values it must pass. Group 3 therefore holds the pure
renames only, which keeps the sweep separable from the binder as the context
intends.

Also settled in this commit:
- The test package for ServerWorkspaceTest.scala is iw.dashboard.test. The
  directory has 7 packages across 65 files, so there was no convention to
  follow, only one to pick.
- Fixed a defect in the context's "Testing Strategy" friendly-helper snippet:
  its last two arguments were swapped against the declared signature and would
  not have compiled, and its unavailableHint fixture was a bare name where the
  call site renders a clause.

Artifacts:
- phase-02-tasks.md

🤖 Generated with Claude Code
Dashboard binder + de-tmuxing sweep
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 7s
CI / compile (pull_request) Failing after 2m37s
CI / test (pull_request) Has been skipped
CI / dashboard-build (pull_request) Has been skipped
e756b5f2e7
- ServerWorkspace resolves the configured backend per creation request via WorkspaceBackend.resolve
- CaskServer no longer names TmuxAdapter and touches only create/attachHint/unavailableHint on the port
- TerminalWorkspaceOps gains unavailableHint with all four implementations pinned by tests
- TmuxError becomes TerminalWorkspaceError and WorktreeCreationResult drops its tmux-named fields
- attachCommand is built from attachHint(sessionName) instead of a hand-written string
- Copy button no longer interpolates the command into a JavaScript string literal
- docs/server-config.md documents workspaceBackend and the generated API docs are refreshed
chore(IW-423): update review-state for phase 02
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 8s
CI / lint (pull_request) Successful in 9s
CI / compile (pull_request) Successful in 1m31s
CI / dashboard-build (pull_request) Successful in 1m18s
CI / test (pull_request) Successful in 11m22s
88059f9879
chore(IW-423): record CI Bloop timeout and set phase 02 awaiting review
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 7s
CI / compile (pull_request) Successful in 1m21s
CI / dashboard-build (pull_request) Successful in 58s
CI / test (pull_request) Successful in 10m6s
80ce673ae0
CI run 486 is green. The earlier compile failure was a Bloop build-server
timeout with no compile error in the log, recorded as LESSONS entry 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEMXsdxXN2NJxJWQjM6eYt
Reviewed-on: #435
workflow: Triage lessons for IW-423
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 8s
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Has been cancelled
CI / dashboard-build (pull_request) Has been cancelled
CI / compile (pull_request) Has been cancelled
a8e10d20b0
Promoted:
- review-state CLI flag parsing and update semantics (entries 1, 2, 4, 5, 13, 14) -> iw-cli #436
- phase CI gate: Forgejo 409 resume, wall-clock budget (entries 10, 12) -> iw-cli #437
- shared server-config path helper (entry 6) -> iw-cli #438
- help flag must never execute a command (entry 11) -> iw-cli #439
- docs/api and llms.txt staleness (entry 17) -> iw-cli #440
- dashboard creation handler anchors on client-supplied projectPath (entry 19) -> iw-cli #441
- WorktreeCreationService parameter restatement (entry 18) -> iw-cli #442
- CI compile job fails on Bloop server timeout (entries 15 CI half, 20) -> iw-cli #443
- dev-docs command text for the review-state flags (entries 1, 2, 4, 13) -> kanon note #183
- phase gate must run the commit gate's format check (entry 9) -> kanon note #184
- structural guard coverage claim (entry 7) -> core/test/WorkspaceBackendTest.scala
- workspaceBackend config key constant (entry 8) -> fixed at source in Phase 2

Rejected:
- pre-push hook wording (entry 15): scala-cli output is already printed; hedging
  the banner would blunt the common case, which is a real compile error

Retained:
- closing-sweep grep allow-list (entry 16): explains this issue only

Deleted:
- Mill daemon crash: one unreproduced observation, discarded per its own proposed home

🤖 Generated with Claude Code
chore(IW-423): update review-state
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 8s
CI / compile (pull_request) Successful in 1m27s
CI / dashboard-build (pull_request) Successful in 1m10s
CI / test (pull_request) Successful in 13m4s
bd4b15f5de
mph merged commit 21bcadf20c into main 2026-08-24 09:50:43 +00:00
mph deleted branch IW-423 2026-08-24 09:50:43 +00:00
Sign in to join this conversation.
No description provided.