LearnNewsExamplesServices
Frontmatter
id17969
title`detachItem` leaves a railed item `autoHidden`, contradicting ADR 0029''s stated mutual exclusion
stateClosed
labels
bugaiarchitecturecore
assigneesneo-opus-grace
createdAtAug 31, 2026, 9:15 PM
updatedAtSep 1, 2026, 2:02 AM
githubUrlhttps://github.com/neomjs/neo/issues/17969
authorneo-opus-grace
commentsCount2
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 1, 2026, 2:02 AM

detachItem leaves a railed item autoHidden, contradicting ADR 0029's stated mutual exclusion

Closed Backlog/active-chunk-19 bugaiarchitecturecore
neo-opus-grace
neo-opus-grace commented on Aug 31, 2026, 9:15 PM

Context

Found while building the pop-out header action (#17947), whose AC inherited the ADR's claim as a premise. The AC could not be witnessed, so the premise was checked against the code rather than restated. #17947 ships without that clause and asserts nothing in either direction; this ticket owns the gap.

Anchors verified at Engine dev@f189cc6ffa.

The Problem

ADR 0029 §2.7's state table (learn/agentos/decisions/0029-docking-design.md:324) states the rule as normative:

detached — Mutually exclusive with autoHidden: auto-hide is an in-workspace affordance; detachItem on a railed item clears autoHidden in the same commit, and setItemAutoHidden(true) rejects detached items.

Neither half is implemented.

Operations.detachItem (src/dashboard/dock/model/Operations.mjs) delegates to Document.detachFromTabs, which touches node structure only:

static detachFromTabs(document, itemId) {
    let tabsId = Document.findContainingTabsId(document, itemId);

    if (!tabsId) return;

    let node = document.nodes[tabsId];

    node.items = node.items.filter(id => id !== itemId);

    if (node.activeItemId === itemId) {
        node.activeItemId = node.items[0] ?? null
    }
}

No item field is read or written. The only site in the model that clears autoHidden is setItemPinned under pinned === true.

The validator does not catch it either: Document.mjs rejects only pinned === true && autoHidden === true, never detached && autoHidden. So a detached item sits in the catalog carrying autoHidden: true, and the document is valid.

The second half — setItemAutoHidden(true) rejecting a detached item — is also absent: that guard rejects non-booleans and pinned items only. Nothing consults detachment.

Why it matters beyond a stale doc line

autoHidden is committed, serialized item state (ADR 0029 line 87), and it is what the projection keys on to rail an item (collectAutoHiddenItems / createEdgeRail). ADR 0029 line 348 says a closing detached window reintegrates "with the committed pinned / autoHidden values its catalog record carries" — so an item torn out of a rail and then returned comes home railed, which is defensible. But the same stale flag is live the whole time it is detached: a perspective captured while the item lives in its own window records it as auto-hidden (line 347 captures committed item state), and any consumer reading the catalog to decide rail membership sees a railed item that is not in the workspace at all.

The divergence sits in the shared detach commit, so it reaches every entry point equally: the drag terminal, interaction/KeyboardCommands#detachItem, and the #17947 header action.

The Fork (needs a decision before code)

The ADR and the model disagree; exactly one of them is wrong, and this ticket must not guess.

  • A — implement the ADR. detachItem clears autoHidden in the same commit; setItemAutoHidden gains a detached-item refusal. This changes the committed grammar of every existing tear-out, so it needs the reintegration story re-read against line 348: an item torn from a rail would return unrailed, which may be the intended behaviour or may be a regression for rail users.
  • B — amend the ADR. Detachment and auto-hide are orthogonal: autoHidden is where the item lives when docked, and preserving it across a detach round-trip is what makes line 348's "returns with its committed values" meaningful. Then §2.7's mutual-exclusion row and the setItemAutoHidden clause are the error.

B has a real argument and the current code is closer to it, but the ADR is the accepted authority and only an ADR-amending decision may overturn it — which is why this is a fork and not an implementation ticket.

Acceptance Criteria

  • A decision is recorded: either ADR 0029 §2.7's detached row is amended (option B) or the model implements it (option A). The ADR and the model agree afterwards; a passing test against undecided behaviour does not close this.
  • If A: detachItem clears autoHidden in the same commit, and setItemAutoHidden(true) refuses an item that is in no tabs node. Both witnessed by unit arms that fail against today's detachFromTabs.
  • If A: the reintegration path is re-checked against ADR 0029 line 348 and the perspective-capture row (line 347), with the resulting behaviour stated in the ADR rather than left to the reader.
  • If B: ADR 0029 line 324 is amended to state the orthogonality and the reason, and line 348's round-trip guarantee is cross-referenced from it.
  • Either way, the divergence cannot silently return: whichever invariant survives is asserted in DockOperations/DockZoneModel unit coverage.

Out of Scope

  • The pop-out header action itself (#17947) — it inherits whatever this decides, along with the drag terminal and the keyboard twin.
  • Rail projection, reveal-overlay behaviour, and the pin control's own autoHidden clearing (landed and correct).

Related

Authority: ADR 0029 §2.7 (learn/agentos/decisions/0029-docking-design.md). Surfaced by: #17947. Parent family: #13158. ADR origin: #14423.

Retrieval Hint: "detachItem autoHidden mutual exclusion ADR 0029 §2.7 railed item detached"

Origin Session ID: dfa1b252-76f5-4a0a-81ab-98b9a5228703

— Grace (Opus 5, Claude Code) 🖖