IW-476: The herdr sidebar names a worktree by its key, and a rename sticks #498

Merged
mph merged 38 commits from IW-476 into main 2026-09-01 20:00:58 +00:00
Owner

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. HerdrLookup loses byLabel, resolve's label fallback and has's label disjunct; LiveHerdrOps.candidatePath stops filtering on os.exists; Open.scala swaps currentName.contains for isCurrent, and currentName leaves the port entirely. A label is now write-only, so a rename cannot mislead iw 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.sessionName addresses it, unchanged. WorktreePath.workspaceLabel displays it, and is written once and read back by nothing. TerminalWorkspaceOps.create carries both, so a 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. 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.scala still tested currentName.contains(sessionName), would compare a short label against a long session name. Option.contains is equality, not substring, so iw open inside 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 start items go first because they create the worktrees the iw open / iw rm items need.

Under IW_WORKSPACE_BACKEND=herdr with a live server:

Phase 2 items — run these first

  • iw start on a fresh issue key. The sidebar row reads IW-nnn, not <project>-IW-nnn.
  • iw start --effort <slug> on a fresh effort. Its row reads <slug>.
  • The parent row both workspaces nest under names the project (repoRoot.last), so an effort slug reads unambiguously in place — iw-cli > <slug>. If the sidebar does not nest, WorktreePath.workspaceLabel and docs/api/WorktreeTypes.md both need a follow-up change, and the published skills need a second rebuild.
  • iw open on a worktree whose workspace does not exist yet creates it with the short label too — the second create call site.
  • A worktree created before this change keeps its long label and still resolves: iw open finds it, iw rm closes it.

Phase 1 items

  • iw open from inside the target worktree prints "Already in session" rather than re-focusing.
  • iw open from outside focuses the right workspace.
  • iw rm closes the workspace it names and not the one the user sits in.
  • A hook-driven runCommand still resolves.
  • iw start collides correctly on an existing key — the call the removed label route used to serve.
  • With the herdr server stopped, iw open on 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 rm from inside a hand-made herdr workspace carrying no worktree object. Added by the phase-01 security review: resolve cannot reach that shape by path, so isCurrent and exists both answer false and iw rm tears the directory down under a user sitting in it, silently. The context accepts the cost but describes it as focus/close/runCommand failing, which is milder than what iw rm actually 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.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 #477; the call site carries a comment saying so.

A herdr workspace with no worktree object 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.

resolve returns Left when repoRootOf(os.pwd) gives no candidate path — iw run 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 sessionName twice — compiling and green, but proving nothing. The .label assertions then went in and were confirmed RED (obtained testproject-IWLE-400 where IWLE-400 was 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 from sessionName and directoryName.
  • HerdrLookupTest, WorkspaceBackendTest, OpenHarnessTest, StartHarnessTest — resolution semantics and the label reaching the port.
  • Unit: core.test 186/186, dashboard.test 241/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: name and label travel as two loose same-typed positional strings, so create(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 the build-iw-cli-skills marker), 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.md
  • project-management/issues/IW-476/release-notes.md (Czech)
  • project-management/issues/IW-476/implementation-log.md
  • project-management/issues/IW-476/LESSONS.md
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. `HerdrLookup` loses `byLabel`, `resolve`'s label fallback and `has`'s label disjunct; `LiveHerdrOps.candidatePath` stops filtering on `os.exists`; `Open.scala` swaps `currentName.contains` for `isCurrent`, and `currentName` leaves the port entirely. A label is now write-only, so a rename cannot mislead `iw 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.sessionName` addresses it, unchanged. `WorktreePath.workspaceLabel` displays it, and is written once and read back by nothing. `TerminalWorkspaceOps.create` carries both, so a 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`. 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.scala` still tested `currentName.contains(sessionName)`, would compare a short label against a long session name. `Option.contains` is equality, not substring, so `iw open` inside 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 start` items go first because they create the worktrees the `iw open` / `iw rm` items need. Under `IW_WORKSPACE_BACKEND=herdr` with a live server: **Phase 2 items — run these first** - [ ] `iw start` on a fresh issue key. The sidebar row reads `IW-nnn`, not `<project>-IW-nnn`. - [ ] `iw start --effort <slug>` on a fresh effort. Its row reads `<slug>`. - [ ] The parent row both workspaces nest under names the project (`repoRoot.last`), so an effort slug reads unambiguously in place — `iw-cli` > `<slug>`. **If the sidebar does not nest, `WorktreePath.workspaceLabel` and `docs/api/WorktreeTypes.md` both need a follow-up change, and the published skills need a second rebuild.** - [ ] `iw open` on a worktree whose workspace does not exist yet creates it with the short label too — the second `create` call site. - [ ] A worktree created before this change keeps its long label and still resolves: `iw open` finds it, `iw rm` closes it. **Phase 1 items** - [ ] `iw open` from inside the target worktree prints "Already in session" rather than re-focusing. - [ ] `iw open` from outside focuses the right workspace. - [ ] `iw rm` closes the workspace it names and not the one the user sits in. - [ ] A hook-driven `runCommand` still resolves. - [ ] `iw start` collides correctly on an existing key — the call the removed label route used to serve. - [ ] With the herdr server stopped, `iw open` on 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 rm` from inside a hand-made herdr workspace carrying no `worktree` object. Added by the phase-01 security review: `resolve` cannot reach that shape by path, so `isCurrent` and `exists` both answer `false` and `iw rm` tears the directory down under a user sitting in it, silently. The context accepts the cost but describes it as `focus`/`close`/`runCommand` failing, which is milder than what `iw rm` actually 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.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 **#477**; the call site carries a comment saying so. **A herdr workspace with no `worktree` object 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. **`resolve` returns `Left` when `repoRootOf(os.pwd)` gives no candidate path** — `iw` run 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 `sessionName` twice — compiling and green, but proving nothing. The `.label` assertions then went in and were confirmed RED (obtained `testproject-IWLE-400` where `IWLE-400` was 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 from `sessionName` and `directoryName`. - `HerdrLookupTest`, `WorkspaceBackendTest`, `OpenHarnessTest`, `StartHarnessTest` — resolution semantics and the label reaching the port. - Unit: `core.test` 186/186, `dashboard.test` 241/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: `name` and `label` travel as two loose same-typed positional strings, so `create(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 the `build-iw-cli-skills` marker), 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.md` - `project-management/issues/IW-476/release-notes.md` (Czech) - `project-management/issues/IW-476/implementation-log.md` - `project-management/issues/IW-476/LESSONS.md`
mph added 35 commits 2026-09-01 15:35:39 +00:00
The herdr sidebar names a worktree by its key, and a rename sticks

Artifacts:
- analysis.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgMgGpKBedGbRhakDQdBfK
The Mill failure was a crashed daemon, not broken code: `./mill shutdown`
then `./mill core.compile` succeeds. Risk 1 now records the recovery
instead of naming the build a blocker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgMgGpKBedGbRhakDQdBfK
Resolved:
- analysis.md: Delete the label route in HerdrLookup -> Option A, remove
  byLabel, resolve's fallback and has's label test; internal representation,
  no consumer to keep compatible
- analysis.md: Keep or drop currentName -> Option A, drop it; its contract
  cannot be honoured uniformly across tmux and herdr
- analysis.md: Effort slugs lose the project -> Option A, key.value for both
  kinds; herdr nests the worktree under its parent, confirmed by eye
- analysis.md: Dashboard create path label -> Option B, leave it; the
  orchestrator is dissolved by IW-477

Still open:
- analysis.md: How is the herdr behaviour actually verified?

Filed IW-477 for the duplicated worktree orchestration between the CLI and
the dashboard. LESSONS.md records the supporting facts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgMgGpKBedGbRhakDQdBfK
Resolved:
- analysis.md: How is the herdr behaviour actually verified? -> Option A,
  manual verification against a live herdr server, recorded in the
  implementation log. No seam extracted, no contract case added.

All five clarifications are now resolved. Conditional "if CLARIFY n says"
branches throughout the document are collapsed to the decisions taken, so
task generation does not inherit stale alternatives. Estimates tighten to
6-9.5h (label routes removed is now in scope; dashboard threading is out):
Phase 1 2.5-4h, Phase 2 3.5-5.5h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgMgGpKBedGbRhakDQdBfK
The herdr sidebar names a worktree by its key, and a rename sticks

Artifacts:
- tasks.md (2 phases, 6-9.5h)
- review-state.json (tasks_ready)

Phase 1 is kept under the 3h floor deliberately: it isolates the resolution
semantics change, which affects every herdr operation including `iw rm` and
has no automated coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgMgGpKBedGbRhakDQdBfK
Issue: 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 Code
The checkout path is the only identity

Artifacts:
- phase-01-tasks.md

🤖 Generated with Claude Code
A 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 Code
chore(IW-476): register phase 1 code review artifact
All checks were successful
CI / contract (pull_request) Has been skipped
CI / compile (pull_request) Successful in 1m40s
CI / format (pull_request) Successful in 9s
CI / lint (pull_request) Successful in 9s
CI / dashboard-build (pull_request) Successful in 1m28s
CI / test (pull_request) Successful in 10m33s
470f4afc9c
chore(IW-476): update review-state for phase 01
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 11s
CI / lint (pull_request) Successful in 12s
CI / compile (pull_request) Successful in 1m27s
CI / dashboard-build (pull_request) Successful in 1m5s
CI / test (pull_request) Successful in 10m0s
e2a16b25ee
Reviewed-on: #480
Issue: 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 Code
A workspace has a display label

Artifacts:
- phase-02-tasks.md

🤖 Generated with Claude Code
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
Reviewed-on: #483
The 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 Code
workflow: Triage lessons for IW-476
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 10s
CI / lint (pull_request) Successful in 11s
CI / compile (pull_request) Successful in 1m21s
CI / dashboard-build (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
5db38b702d
Promoted:
- Mill daemon failure -> docs/testing.md. Rewritten: copy out/mill-daemon/stderr
  before restarting, since Mill truncates it on the next daemon start. The
  original "--no-daemon is the remedy" framing was not promoted; an upstream
  search found four Mill issues sharing that message with four different causes
  and none matching our trigger.
- A single red run is not evidence -> docs/testing.md, citing #468.
- build-iw-cli-skills marker -> SKILL.md and CLAUDE.md, fixed rather than
  documented. The marker recorded a commit while the generator reads the working
  tree, so a built-then-discarded edit left the staged output stale forever. It
  is now .input-manifest, a sha256 per input file actually read.

Filed:
- #491 phase-pr pushes a commit after reading CI, so its green can describe a
  stale head.
- #497 Hooks and CI read a crashed tool as a finding, and print --no-verify as
  the remedy. Third sighting; IW-422 held its two for exactly this recurrence.

Rejected as already tracked:
- phase-commit --items comma splitting: #337. Found filed twice; #471 closed as
  a duplicate and its content folded into #337.
- Two same-typed strings that travel together: #429, with the new create() call
  site and the missing label opaque type added there.

Deleted:
- Phase 2's Setup-step rg command breadth. Nothing worth keeping.

Also closed #489, which this triage opened before finding #468 already covered
it; its additions were folded into #468.

🤖 Generated with Claude Code
The guidance committed in 5db38b70 named out/mill-daemon/stderr. That file is
truncated on every daemon start and holds only the JVM startup notice. The
daemon's log is out/mill-daemon/server.log, which is append-only across
restarts.

Following the rule when the daemon crashed again found both the error in the
rule and the cause of the crash. server.log shows "client interrupted while
server was executing command" followed by a ujson parse failure in
MillRpcWireTransport, repeating across daemon pids on 2026-08-29 and again on
2026-09-01. A client leaves mid-command, the daemon reads a partial message as
the next request and exits; the client reports "Worker wire broken".

That explains why ./mill shutdown appeared not to work — each fresh daemon is
re-killed within seconds — and why --no-daemon does, since it has no long-lived
RPC wire. What interrupts the client is not established.

🤖 Generated with Claude Code
chore(IW-476): update review-state to all_complete
All checks were successful
CI / contract (pull_request) Has been skipped
CI / compile (pull_request) Successful in 1m31s
CI / format (pull_request) Successful in 10s
CI / lint (pull_request) Successful in 10s
CI / dashboard-build (pull_request) Successful in 1m0s
CI / test (pull_request) Successful in 11m0s
b3355f638d
chore(IW-476): record that the manual herdr gate was waived
All checks were successful
CI / contract (pull_request) Has been skipped
CI / compile (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 10s
CI / lint (pull_request) Successful in 9s
CI / dashboard-build (pull_request) Successful in 1m26s
CI / test (pull_request) Successful in 11m45s
ae88d1de84
The 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
mph merged commit 420912f461 into main 2026-09-01 20:00:58 +00:00
Sign in to join this conversation.
No description provided.