Second review round on the contained-group composition:
- Ownership scanned only `insert` rows, so an external bundle could set a
built-in row's id as a child through `{ id: own, config: [...] }` and the
Loader re-parented that row under the bundle while `originOf` still named
the built-in layer. The rows an id-targeted patch sets as a group's config
now count as the layer's own: an external bundle that sets an id another
layer owns is left out whole, a built-in layer that takes another layer's
id fails loud, and a layer restating its own child is not a repeat.
- `recompose` read the committed profile before awaiting the update, so a
watcher firing while a bundle was being enabled composed the tree before
the enable and applied it after; the plugin vanished while `layers` kept
listing it. Recompositions now queue, each reading what the previous one
committed, and a rejection stays its own caller's outcome.
- A patch that replaced a built-in target's config removed the wrapper the
bundle had put there, yet later inserts still targeted the detached
wrapper and vanished. The composition forgets the wrapper on such an
override and creates a new one under the same id.
- Generated wrapper ids bypassed the duplicate check, so a bundle declaring
a row spelled like a wrapper produced two nodes with one id. Declared and
generated ids now go through one registration.
- Failure records outlived their rows when a bundle update dropped a failed
row without unmounting the group, or when a pending row sat in a plain
nested group whose id the record named. A contained group now drops the
records of rows its configuration no longer names after each update, and
the audit records every row under the contained group that isolates it.
- `applyEntryPatches` assigned override values by reference, so applying a
patch list carried a later insert's rows back into the patch object; the
vendored include now clones object-valued overrides as it does inserts.
`applyEntryPatches` pushed a patch's `insert` rows into the entry list by
reference, so a later patch that inserted into a group an earlier patch had
inserted mutated the patch's own object. With bundle rows now inserted into
their group by a separate patch, the Loader's rollback of a rejected update
re-applied the previous include config over those mutated objects and hit
`duplicate loader entry id`; the rollback failed and the rejection reason
named it instead of the row that broke. Inserted rows are now cloned before
they join the list, as the function's detachment contract already stated;
the vendored change is logged and pinned by an application-twice test.
The publish guide still told bundle authors that mounted row ids carry the
package prefix and that `!!js` gates should compare names, a scheme this
stack withdrew: ids stay as declared, and a shared id or a repeated one
leaves the bundle out with the reason. It now says so, and names
`dsh.title` and `dsh.plugins` with a pointer to the manifest package that
declares every key. The cordis catalog follows app-boot's changed exports.
`stageOf` parsed the stage out of the Loader's `failed to <stage> loader
entry` message. The vendored wrapper is now an exported `EntryUpdateError`
carrying its `stage`, logged as a local modification, and the contained
group reads the field. `isJsDisabled` wrapped `isJsExpr` behind a literal
`disabled === true` comparison that already excluded expression nodes, so
it goes. The post-boot rejection guard, its documentation, and the
external-bundle module doc now say what they do: the process keeps running
with nothing stopped or attributed to a plugin, and a contained group lets
a failing row's siblings mount rather than failing the bundle as a whole.
The probe's child was a script string that wrote its report to stdout, and
the parent parsed the whole stream as one JSON value: a plugin that merely
printed a line at import could not be probed, and a package that kept a
timer alive after import ran until the timeout. The parent asserted the
parsed value as the report, and the cache read checked only `format`, so a
file holding nothing but `{ "format": 2 }` came back as a complete record.
The child is now its own module, `probe-child.ts`, run through tsx under a
source launch and bundled as `lib/probe-child.js`, and it sends one report
over an IPC channel; stdout is not read at all, and the child is killed as
soon as the report arrived. `parseChildReport` and `parseProbeRecord`
validate the report and the cached record field by field, so an
unrecognized report is a rejection and an unrecognized record is probed
again. `PluginProbe.ok` now states what it is: the main export imported and
cordis is not a second copy; `kind` and `addable[].ok` decide what can be
enabled or added.
`ProfileRuntime.recompose({ reloadBundles: true })` replaced its profile
before the Loader had accepted the update, so a rejected recomposition left
`current`, `layers`, and `originOf` describing a stack that never mounted.
The user patch watchers took a second path through the launcher's
`composeLive`, which wrote the new conflicts into the failure registry
before the update and kept composing from the runtime's stale profile.
Conflict records shared the registry with row failures under a fabricated
entry id and a group id that meant three different things, so the plugin
inventory attributed a conflict to the layer that won the id, and a bundle
disabled after failing kept its import and apply records forever.
The runtime now holds one committed composition — profile, stack ownership,
conflicts — and publishes a candidate only after `entry.update` resolves;
`originOf` reads the committed owners instead of recomputing them. The
watchers call `recompose` through a `reapply` callback, so every live change
goes through the one entry point. Conflicts live on the runtime with their
messages and never enter `pluginFailures`, whose records name rows that
reached the Loader; the inventory lists them under the layer that lost. A
contained group clears its rows' records when it unmounts.
`composeExternalLayer` inserted every id-targeted patch after all inserts,
so a bundle that replaced a group's config and then appended to it lost the
appended row under the `runtime` stage; two inserts into the same built-in
group produced two wrapper groups with one id, which the Loader rejected as
a duplicate and the boot failed; and an id a bundle inserted twice was
indexed once, escaped the ownership check, and failed the boot the same way.
The composition now inserts the bundle's group empty and emits the bundle's
patches in the order written, re-targeting root inserts into the group and
gathering every insert into one built-in group in one wrapper. A repeated id
is a conflict of the bundle with itself: the bundle is left out whole and
recorded like any other conflict, and a built-in layer that repeats an id
fails loud. `composeProfileStack` renders each contained layer once and
returns the owner of every id with the patches and conflicts, and one walker
in `patch-rows.ts` replaces the five recursive scans of inserted rows.
Archiving low-future-value Agent Notes (#3548) stripped the inbound
citations to three now-archived notes from the ui-primitives README, and
this branch rewrote the same "Controls and icons" opener so the new
component catalog owns the export enumeration. The resolution keeps this
branch's opener and drops the archived-note citation master removed, in
both languages; the pairing record is re-recorded from the merged pair.
The track's radius is half its height, so its corners consume the whole
side and the global `superellipse(1.5)` squares the ends off — beside a
thumb that already opts out at `border-radius: 50%`, leaving a round knob
in a squared-off slot. Pair `corner-shape: round` with the radius, the way
every full-round radius in the repository does.
The corner-shape stylesheet spec recognizes 50%, 100%, and radii at or
above 99px, so a radius that is full-round only relative to its own box
passes it; the comment records that gap at the rule it affects.
The `idle` addition left two maintainer-facing statements behind: the
README's limitations entry still listed four supported StateDot states,
and TerminalBlock's run-state JSDoc still counted the union as four while
correctly saying three of them are reachable there.
Pill's header comment still sent every capsule without `onClick` to Tag,
which contradicts both the catalog this pull request corrected and
TerminalBlock's own static read-only Pill two files away. Size separates
the two controls as much as interactivity does, and the comment now says
so. The function JSDoc and the static-mode tests stay as they are: they
describe supported behavior.
`unloading` marks a teardown the Host is running, which an async disposer
can hold for a while, so the inventory's dot animates it like `loading`
instead of showing the static grey `idle` mark that means no activity.
Correct four documentation facts against the shipped code: StateDot's
render JSDoc still promised four states, the catalog claimed four Tooltip
sides where TooltipSide has three, the Agent Note's geometry audit counted
three matching rules where two match, and the migrated-site and shipped-
appearance counts had drifted.
Record two things the first pass left out: TerminalBlock renders its exit
status as a static Pill, which the Tag/Pill catalog entry now accounts for
by size rather than by interactivity alone, and the disabled inventory tag
changed fill token, which joins the visual-change list.
Pin Tag's eight tones with a stylesheet spec, for the same reason StateDot
has one: CSS Modules resolve to class-name maps in component suites, so a
missing or wrong tone rule passes every render assertion.
- Stop treating three archived records as current authority: bounded-llm
citation now points at the llm-retry README, the semantic-composer
proposal cites the runtime-owned child guard as history it builds on,
and the code-runtime comment states the deferred rationale locally.
- Fix the stale "dsh family stays restricted" claims left without their
rationale citation: the check-workspace-constraints comment, the
release publish comment, and the kept npm-release-sequences note now
record that the family published publicly on 2026-08-13 (87f8e6a728).
- The rejected client-settings-locale-theme body keeps its pristine
proposal prose with only the Status line and the two link paths whose
targets this PR archived; the link remaps are required for
verify-md-links and pairing, and every prose byte is untouched.
Run the dsh-archive-agent-notes audit over every active Agent Note on
current master, judging each record by whether its rationale still guides
work rather than by size or age.
- Archive 453 implemented bilingual triplets (417,882 English words):
completed UI chrome, narrow adapters, closed bug fixes, implementation
walkthroughs whose package READMEs, docs pages, generators, or successor
notes now carry the useful behavior, and 51 records fully superseded by
a later active note. Keep 201 implemented notes whose ownership rules,
negative guarantees, durable or wire semantics, security rules,
reintroduction conditions, or still-tempting rejected alternatives
remain useful.
- Reject 7 proposals whose premise is gone or whose work shipped in
amended form under other records; delete 2 rejected notes that no
longer prevent a plausible mistake.
- Retarget every remaining inbound link to the archived path, and repair
active prose that named an archived record as the owner of a live fact:
parenthetical citations drop, ownership sentences redirect to the
README, docs page, or active note that states the fact, and history
citations say so. Chinese files link the English archived path because
the pairing gate treats the frozen tree as outside the bilingual corpus.
- Seal 1,359 new frozen artifacts; existing seals are unchanged and
outbound links from archived notes are neither inspected nor repaired.
- Regenerate docs/config-catalog.md after the hook-bridge comment edits
shifted two source line numbers.
Client feature plugins never import one another's components, so
ui-primitives is their only channel for sharing one. Nothing told an
author to look there first, and the package offered nothing to look
at: its README named six source files against forty-plus exports.
Add Tag (read-only capsule badge, eight tones) and Switch (36x20
toggle with a required accessible name), extend StateDotState with
idle for a tracked subject with no activity, and move ui-agent-preset,
ui-settings-plugins, and ui-settings-plugin-inventory onto them.
The plugin inventory's conditional tag referenced
--dsw-alias-state-warning-primary, which does not exist, so both
themes had been painting its hardcoded #b45309 fallback.
State the reuse rule as the first step of the new-component checklist,
and give the README a component catalog so the rule has something to
check. The rule is guidance, not a gate: a package may still write its
own control, and the Agent Note records the five that stay local.
The Agent Note rules keep the `# Agent Note: ` title prefix and the
`Status:` line in English verbatim on both language sides. 51 Chinese
counterparts carried a full-width colon (`# Agent Note:`), an annotated
prefix (`# Agent Note(agent 决策记录):`), or a translated status token
(`状态:已实现`, `状态:implemented`, `状态:已实施`, `状态:proposed`), which the
archive verifier rejects on entry to the frozen tree. Normalize the tokens
and re-record the bilingual sidecars; no body text changes.