Phase 2: A workspace has a display label #483

Merged
mph merged 6 commits from IW-476-phase-02 into IW-476 2026-08-31 07:48:08 +00:00
Owner

Phase 2 of IW-476. A worktree now carries two names, each with one job.

  • The addressing name — WorktreePath.sessionName, <project>-<key>. Unchanged. It is the tmux session name, and under herdr it is the string LiveHerdrOps derives the checkout path from.
  • The display label — WorktreePath.workspaceLabel, key.value. Written once when the workspace is created, and read back by nothing.

TerminalWorkspaceOps.create carries both. Under herdr the label reaches herdr worktree open --label, so the sidebar row reads IW-476 rather than iw-cli-IW-476, and an effort row reads mail-capture rather than iw-cli-mail-capture. Under tmux the label is accepted and discarded — tmux has no display name distinct from its session name.

This is the phase the user sees. Phase 1 changed no sidebar text.

Changes

  • core/model/WorktreePath.scala — workspaceLabel as a derived method, beside directoryName and sessionName. No third case-class parameter, no branch on WorktreeKey.
  • core/commands/CommandEnv.scala — TerminalWorkspaceOps.create widened to (name, label, workDir), with scaladoc stating which name addresses, which displays, and that no implementation may read a label back. No default on label, so the compiler names every call site.
  • The three implementations (UnavailableWorkspaceOps, LiveTmuxOps, LiveHerdrOps) and the test fake follow. LiveHerdrOps.create forwards label — not name — to HerdrAdapter.openWorktree.
  • Call sites Start.scala, Open.scala, CaskServer.scala, plus two compile-forced test call sites.
  • docs/api/WorktreeTypes.md — documents workspaceLabel and fixes a stale issueId: IssueId signature that predates this issue (the code has taken key: WorktreeKey since effort worktrees landed).

⚠️ This phase ships manually unverified

No automated tier reaches herdr. All E2E BATS suites force IW_WORKSPACE_BACKEND=tmux, and no seam was extracted — Michal's recorded decision in analysis.md (RESOLVED: How is the herdr behaviour actually verified?).

Two manual checklists are outstanding, and together they gate the final PR to main — not this PR. Phase 1's and Phase 2's, both recorded in implementation-log.md under Deferred manual verification. They run in one live-herdr session; the iw start items go first because they create the worktrees Phase 1's iw open / iw rm items need.

Accepted risks, stated plainly

The label rule is proved one layer above the code that sends it. FakeTerminalWorkspaceOps.CreateCall.label records what the command handed the port; LiveCommandEnv.scala:728, which actually sends it to herdr, is covered by nothing. A change that passed name there would keep every automated tier green.

Two label conventions will coexist in one sidebar. A worktree created from the browser keeps <project>-<key>; one created by iw start gets <key>. Both resolve by path, so this is cosmetic. The dashboard's own worktree-creation path is deliberately out of scope and tracked as IW-477; the call site carries a comment saying so.

Deferring the manual check has a cost. The label rule and the docs are both written before anyone has seen the sidebar. If herdr does not nest worktrees under a project row the way analysis.md records, both need a follow-up change and the published skills need a second rebuild.

Testing

TDD in two moves, so the assertions could be proved RED honestly. The port widening landed first with every call site passing sessionName twice — compiling and green, but proving nothing. The .label assertions then went in and were confirmed RED (obtained testproject-IWLE-400 / testproject-IWLE-123 where IWLE-400 / IWLE-123 was expected), and only then did the call sites pass the real label.

  • core/test/WorktreePathTest.scala — 3 tests: the label for an issue key, for an effort key, and that it differs from sessionName and directoryName.
  • StartHarnessTest / OpenHarnessTest — the only automated proof of the issue's first acceptance criterion.
  • core.test 186/186, dashboard.test 241/241, ./iw ./test e2e exit 0 with 190 passing. No BATS file modified.

Code review

0 critical, 2 warnings, 5 suggestions across six review skills — review-phase-02-20260830-113230.md.

Both warnings described one shape: name and label always travel together but pass as two loose same-typed positional strings, so a transposed call compiles silently and would misbehave differently per backend (tmux reads name, herdr reads label). Fixed at the three call sites with named arguments, which removes the risk without changing the port signature the phase context specifies. The fuller WorkspaceIdentity(name, label) bundling is LESSONS.md entry 14, proposed for a new tracker issue.

The security reviewer confirmed the change is security-neutral: the label reaches a discrete argv element with no shell on the path, IssueId/EffortSlug parsing forbids a leading - so it cannot be read as a flag, and it reaches no HTML rendering path.

Phase 2 of IW-476. A worktree now carries two names, each with one job. - **The addressing name** — `WorktreePath.sessionName`, `<project>-<key>`. Unchanged. It is the tmux session name, and under herdr it is the string `LiveHerdrOps` derives the checkout path from. - **The display label** — `WorktreePath.workspaceLabel`, `key.value`. Written once when the workspace is created, and read back by nothing. `TerminalWorkspaceOps.create` carries both. Under herdr the label reaches `herdr worktree open --label`, so the sidebar row reads `IW-476` rather than `iw-cli-IW-476`, and an effort row reads `mail-capture` rather than `iw-cli-mail-capture`. Under tmux the label is accepted and discarded — tmux has no display name distinct from its session name. This is the phase the user sees. Phase 1 changed no sidebar text. ## Changes - `core/model/WorktreePath.scala` — `workspaceLabel` as a derived method, beside `directoryName` and `sessionName`. No third case-class parameter, no branch on `WorktreeKey`. - `core/commands/CommandEnv.scala` — `TerminalWorkspaceOps.create` widened to `(name, label, workDir)`, with scaladoc stating which name addresses, which displays, and that no implementation may read a label back. No default on `label`, so the compiler names every call site. - The three implementations (`UnavailableWorkspaceOps`, `LiveTmuxOps`, `LiveHerdrOps`) and the test fake follow. `LiveHerdrOps.create` forwards `label` — not `name` — to `HerdrAdapter.openWorktree`. - Call sites `Start.scala`, `Open.scala`, `CaskServer.scala`, plus two compile-forced test call sites. - `docs/api/WorktreeTypes.md` — documents `workspaceLabel` and fixes a stale `issueId: IssueId` signature that predates this issue (the code has taken `key: WorktreeKey` since effort worktrees landed). ## ⚠️ This phase ships manually unverified **No automated tier reaches herdr.** All E2E BATS suites force `IW_WORKSPACE_BACKEND=tmux`, and no seam was extracted — Michal's recorded decision in `analysis.md` (*RESOLVED: How is the herdr behaviour actually verified?*). **Two manual checklists are outstanding, and together they gate the final PR to main — not this PR.** Phase 1's and Phase 2's, both recorded in `implementation-log.md` under *Deferred manual verification*. They run in one live-herdr session; the `iw start` items go first because they create the worktrees Phase 1's `iw open` / `iw rm` items need. ### Accepted risks, stated plainly **The label rule is proved one layer above the code that sends it.** `FakeTerminalWorkspaceOps.CreateCall.label` records what the command handed the port; `LiveCommandEnv.scala:728`, which actually sends it to herdr, is covered by nothing. A change that passed `name` there would keep every automated tier green. **Two label conventions will coexist in one sidebar.** A worktree created from the browser keeps `<project>-<key>`; one created by `iw start` gets `<key>`. Both resolve by path, so this is cosmetic. The dashboard's own worktree-creation path is deliberately out of scope and tracked as **IW-477**; the call site carries a comment saying so. **Deferring the manual check has a cost.** The label rule and the docs are both written before anyone has seen the sidebar. If herdr does not nest worktrees under a project row the way `analysis.md` records, both need a follow-up change and the published skills need a second rebuild. ## Testing TDD in two moves, so the assertions could be proved RED honestly. The port widening landed first with every call site passing `sessionName` twice — compiling and green, but proving nothing. The `.label` assertions then went in and were confirmed RED (obtained `testproject-IWLE-400` / `testproject-IWLE-123` where `IWLE-400` / `IWLE-123` was expected), and only then did the call sites pass the real label. - `core/test/WorktreePathTest.scala` — 3 tests: the label for an issue key, for an effort key, and that it differs from `sessionName` and `directoryName`. - `StartHarnessTest` / `OpenHarnessTest` — the only automated proof of the issue's first acceptance criterion. - `core.test` 186/186, `dashboard.test` 241/241, `./iw ./test e2e` exit 0 with 190 passing. No BATS file modified. ## Code review 0 critical, 2 warnings, 5 suggestions across six review skills — `review-phase-02-20260830-113230.md`. Both warnings described one shape: `name` and `label` always travel together but pass as two loose same-typed positional strings, so a transposed call compiles silently and would misbehave differently per backend (tmux reads `name`, herdr reads `label`). Fixed at the three call sites with named arguments, which removes the risk without changing the port signature the phase context specifies. The fuller `WorkspaceIdentity(name, label)` bundling is `LESSONS.md` entry 14, proposed for a new tracker issue. The security reviewer confirmed the change is security-neutral: the label reaches a discrete argv element with no shell on the path, `IssueId`/`EffortSlug` parsing forbids a leading `-` so it cannot be read as a flag, and it reaches no HTML rendering path.
Phase 2: A workspace has a display label
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 9s
CI / lint (pull_request) Successful in 11s
CI / compile (pull_request) Successful in 1m38s
CI / dashboard-build (pull_request) Successful in 1m46s
CI / test (pull_request) Successful in 11m19s
2ec9d276f4
A worktree now carries two names, each with one job. sessionName addresses it —
the tmux session name, and the string LiveHerdrOps derives the checkout path
from. workspaceLabel is what a person reads.

- WorktreePath.workspaceLabel is key.value, a derived method beside
  directoryName and sessionName. No third case-class parameter and no branch
  on WorktreeKey.
- TerminalWorkspaceOps.create carries both names. Its scaladoc states which
  addresses, which displays, and that no implementation may read a label back:
  a label the user renames must not change what any operation resolves. There
  is no default on label, so the compiler names every call site.
- LiveHerdrOps.create forwards the label — not the name — to
  HerdrAdapter.openWorktree, so the herdr sidebar row reads IW-476 rather than
  iw-cli-IW-476, and an effort row reads mail-capture rather than
  iw-cli-mail-capture. LiveTmuxOps accepts the label and discards it; tmux has
  no display name apart from its session name.
- CaskServer passes the session name as both, deliberately and with a comment.
  The dashboard's own worktree-creation path is out of scope, tracked as
  IW-477, so two label conventions will coexist in one sidebar. Both resolve by
  path, so the difference is cosmetic.
- docs/api/WorktreeTypes.md documents workspaceLabel and corrects a stale
  issueId: IssueId signature that predates this issue — the code has taken
  key: WorktreeKey since effort worktrees landed.

The widening took two moves so the assertions could be proved RED honestly:
every call site first passed sessionName twice, which compiles and stays green
while proving nothing, then the .label assertions went in and failed, and only
then did the call sites pass the real label.

Code review: 6 skills, 0 critical, 2 warnings. Both described one shape — name
and label always travel together but pass as two loose same-typed positional
strings, and a transposed call would misbehave differently per backend. The
three call sites now pass named arguments, which removes the risk without
changing the signature the phase context specifies. The fuller
WorkspaceIdentity bundling is LESSONS.md entry 14, for a new tracker issue.

The manual herdr checklist is deferred to the final PR by Michal's decision, so
this phase ships manually unverified. No automated tier reaches herdr, and the
label rule is proved one layer above LiveCommandEnv.scala:728, which is the
code that actually sends it.

Issue: IW-476

🤖 Generated with Claude Code
chore(IW-476): update review-state for phase 02
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Failing after 4s
CI / lint (pull_request) Successful in 7s
CI / compile (pull_request) Successful in 1m20s
CI / dashboard-build (pull_request) Successful in 1m10s
CI / test (pull_request) Successful in 10m40s
3915100ffd
ci: re-trigger checks after a scalafmt runner crash
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 1m20s
CI / dashboard-build (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 10m12s
45bca50071
The format job on 3915100f exited 139 — 'ScalaNative: Unrecoverable
NullPointerException' and a segfault in scala-cli fmt --check, after it had
scanned zero of 512 files. It did not report an unformatted file. The same job
passed on 2ec9d276 minutes earlier on the same image, and scala-cli fmt --check
passes locally across all 512 files, so this was a runner crash rather than a
formatting fault.

3915100f changes review-state.json only and contains no Scala.

Issue: IW-476
workflow(IW-476): record lessons 15 and 16, repair the phase 2 card
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 9s
CI / lint (pull_request) Successful in 10s
CI / compile (pull_request) Successful in 1m49s
CI / dashboard-build (pull_request) Successful in 1m15s
CI / test (pull_request) Successful in 12m24s
4c034e3c4a
Lesson 15: phase-pr pushes its review-state commit after reading CI, so its
exit 0 described 2ec9d276 while the pull request head had moved to 3915100f,
whose format job failed.

Lesson 16: that format job exited 139 after scanning 0 of 512 files. A
formatter returning SIGSEGV crashed rather than rejecting the code.

ci-gate re-read CI for the current head and stamped ci_gated_sha 45bca500
without pushing, which is the repair that does not repeat the pattern.

Issue: IW-476
chore(IW-476): stamp the CI gate for the phase 2 head
All checks were successful
CI / contract (pull_request) Has been skipped
CI / compile (pull_request) Successful in 1m23s
CI / format (pull_request) Successful in 9s
CI / lint (pull_request) Successful in 9s
CI / dashboard-build (pull_request) Successful in 1m1s
CI / test (pull_request) Successful in 10m16s
8468bbaa15
ci-gate re-read CI for 4c034e3c and recorded it as the gated commit, replacing
the stale 2ec9d276 that lesson 15 describes.

This commit moves the head again, so ci_gated_sha is deliberately left one
commit behind rather than re-gated. phase-merge re-reads CI whenever the gated
sha and the head differ, which is the conservative direction.

Issue: IW-476
mph merged commit 6c7a410d62 into IW-476 2026-08-31 07:48:08 +00:00
mph deleted branch IW-476-phase-02 2026-08-31 07:48:08 +00:00
Sign in to join this conversation.
No description provided.