IW-476: The herdr sidebar names a worktree by its key, and a rename sticks #498
No reviewers
Labels
No labels
agent-ready
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
needs-human
needs-input
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli!498
Loading…
Reference in a new issue
No description provided.
Delete branch "IW-476"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #476.
The herdr sidebar now names a worktree by its key, and renaming a workspace in herdr no longer breaks the commands that address it.
What changed
Phase 1 — the checkout path is the only identity. A herdr workspace is resolved by the worktree directory it holds, and by nothing else.
HerdrLookuplosesbyLabel,resolve's label fallback andhas's label disjunct;LiveHerdrOps.candidatePathstops filtering onos.exists;Open.scalaswapscurrentName.containsforisCurrent, andcurrentNameleaves the port entirely. A label is now write-only, so a rename cannot misleadiw open,iw rm, or a hook-driven command.Phase 2 — a workspace has a display label. A worktree carries two names with one job each.
WorktreePath.sessionNameaddresses it, unchanged.WorktreePath.workspaceLabeldisplays it, and is written once and read back by nothing.TerminalWorkspaceOps.createcarries both, so a herdr sidebar row readsIW-476rather thaniw-cli-IW-476, and an effort row readsmail-capturerather thaniw-cli-mail-capture. Under tmux the label is accepted and discarded — tmux has no display name distinct from its session name.Phase 1 had to precede Phase 2: shortening the label first, while
Open.scalastill testedcurrentName.contains(sessionName), would compare a short label against a long session name.Option.containsis equality, not substring, soiw openinside the target worktree would have re-focused itself instead of printing "Already in session" — the regression this issue exists to prevent.⚠️ This PR ships manually unverified — the gate is open
No automated tier reaches herdr. Every E2E BATS suite forces
IW_WORKSPACE_BACKEND=tmux, and no seam was extracted. That was a recorded decision, not an oversight (analysis.md, RESOLVED: How is the herdr behaviour actually verified?).The implementation log states that both phases' manual checklists must be run before this PR is opened. They have not been. Michal chose to open the PR now and verify before merge. Both checklists run in one live-herdr session; the
iw startitems go first because they create the worktrees theiw open/iw rmitems need.Under
IW_WORKSPACE_BACKEND=herdrwith a live server:Phase 2 items — run these first
iw starton a fresh issue key. The sidebar row readsIW-nnn, not<project>-IW-nnn.iw start --effort <slug>on a fresh effort. Its row reads<slug>.repoRoot.last), so an effort slug reads unambiguously in place —iw-cli><slug>. If the sidebar does not nest,WorktreePath.workspaceLabelanddocs/api/WorktreeTypes.mdboth need a follow-up change, and the published skills need a second rebuild.iw openon a worktree whose workspace does not exist yet creates it with the short label too — the secondcreatecall site.iw openfinds it,iw rmcloses it.Phase 1 items
iw openfrom inside the target worktree prints "Already in session" rather than re-focusing.iw openfrom outside focuses the right workspace.iw rmcloses the workspace it names and not the one the user sits in.runCommandstill resolves.iw startcollides correctly on an existing key — the call the removed label route used to serve.iw openon an existing worktree prints the "cannot ask herdr about workspace" warning twice, and on a missing worktree prints it once. Both are expected; confirm they are warnings and not failures.iw rmfrom inside a hand-made herdr workspace carrying noworktreeobject. Added by the phase-01 security review:resolvecannot reach that shape by path, soisCurrentandexistsboth answerfalseandiw rmtears the directory down under a user sitting in it, silently. The context accepts the cost but describes it asfocus/close/runCommandfailing, which is milder than whatiw rmactually does. Observe it once and report.Accepted risks, stated plainly
The label rule is proved one layer above the code that sends it.
FakeTerminalWorkspaceOps.CreateCall.labelrecords what the command handed the port.LiveCommandEnv.scala:728, which actually sends it to herdr, is covered by nothing. A change that passednamethere 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 byiw startgets<key>. Both resolve by path, so this is cosmetic. The dashboard's own worktree-creation path is deliberately out of scope and tracked as #477; the call site carries a comment saying so.A herdr workspace with no
worktreeobject is now unreachable. iw never creates that shape; only herdr's own UI does. Keeping the label disjunct would not have saved the case past Phase 2 anyway, since a shortened label no longer equals the session name.resolvereturnsLeftwhenrepoRootOf(os.pwd)gives no candidate path —iwrun outside a git repository. A clear failure beats a label match naming a workspace on a different directory.Testing
TDD in two moves per phase, so the assertions could be proved RED honestly. In Phase 2 the port widening landed first with every call site passing
sessionNametwice — compiling and green, but proving nothing. The.labelassertions then went in and were confirmed RED (obtainedtestproject-IWLE-400whereIWLE-400was expected), and only then did the call sites pass the real label.core/test/WorktreePathTest.scala— the label for an issue key, for an effort key, and that it differs fromsessionNameanddirectoryName.HerdrLookupTest,WorkspaceBackendTest,OpenHarnessTest,StartHarnessTest— resolution semantics and the label reaching the port.core.test186/186,dashboard.test241/241. E2E: 190 passing, tmux only.Both phases passed code review — Phase 1 with 0 critical, Phase 2 with 0 critical and 2 warnings. Both warnings describe one shape:
nameandlabeltravel as two loose same-typed positional strings, socreate(label, name, workDir)compiles silently. Mitigated here by named arguments at all three call sites; the type-level fix is tracked on #429.Lessons triaged
Eight pending entries were walked with Michal before this PR. Two promotions landed in this branch (
docs/testing.md, and a fix to thebuild-iw-cli-skillsmarker), two issues were filed (#491, #497), two were rejected as already tracked (#337, #429), and one was deleted. Triage also found and resolved two duplicate pairs: #471 was closed into #337, and #489 — which this triage itself opened — was closed into #468.Artifacts
project-management/issues/IW-476/review-packet.mdproject-management/issues/IW-476/release-notes.md(Czech)project-management/issues/IW-476/implementation-log.mdproject-management/issues/IW-476/LESSONS.mdIssue: IW-476 Phase: 1 - The checkout path is the only identity This commit marks the checkpoint for phase 1 context generation. Use wf-implement IW-476 to return to this point. 🤖 Generated with Claude CodeThe checkout path is the only identity Artifacts: - phase-01-tasks.md 🤖 Generated with Claude CodeA herdr workspace had two identities: the checkout path of the worktree it holds, and its display label. It now has one — the checkout path. - HerdrLookup.byLabel, resolve's label fallback and has's label disjunct are deleted. This is reachable code removed deliberately, not a dead-code sweep: has is a disjunction rather than a fallthrough, and resolve's fallback ran whenever candidatePath returned None. - currentName is removed from the TerminalWorkspaceOps port and its four implementations. It declared one signature with two meanings — tmux returned an addressing name every port method accepts, herdr returned a display label none accepts. Open.scala asks isCurrent instead. - LiveHerdrOps.candidatePath derives a path instead of looking one up, so iw start can ask whether a workspace already holds a directory it is about to create. - has loses its now-unused label parameter, so the deleted disjunct cannot return without a signature change. Taken against the phase context's deferral, on Michal's decision after two reviewers raised it. Code review: 6 skills, 0 critical, 5 warnings — all fixed. Three were comments the diff left actively false; one was a real coverage gap for has, now pinned by a test proved RED. The manual herdr checklist is deferred to the final PR by Michal's decision, so this phase ships manually unverified. No automated tier reaches the herdr path, and OpenHarnessTest stays green by construction rather than as evidence. Issue: IW-476 🤖 Generated with Claude CodeIssue: IW-476 Phase: 2 - A workspace has a display label This commit marks the checkpoint for phase 2 context generation. Use wf-implement IW-476 to return to this point. 🤖 Generated with Claude CodeA workspace has a display label Artifacts: - phase-02-tasks.md 🤖 Generated with Claude CodeA 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 CodeThe packet leads with the combined manual herdr checklist, still unchecked, as an open gate on the PR to main. The notes state in Czech that herdr behaviour is verified by hand, not by any automated tier. 🤖 Generated with Claude CodeThe Phase 1 and Phase 2 checklists were not run. Michal waived the gate and authorised the merge with CI green. On merge the herdr behaviour of this change has been exercised by nothing - no automated tier reaches herdr, and the manual check did not happen. Recorded so the gap is on the record. 🤖 Generated with Claude Code