@adia-ai/adia-ui-forge 0.8.48 → 0.8.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.48",
3
+ "version": "0.8.51",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.48",
3
+ "version": "0.8.51",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog — adia-ui-kit-forge
2
2
 
3
+ ## [0.8.51] — 2026-08-24
4
+
5
+ ### Added
6
+ - Hermes + Pi external-distribution manifests (gh#1954) — `plugin.yaml` +
7
+ `__init__.py` (Hermes) and `prompts/*.md` + a `"pi"` field in
8
+ `package.json` (Pi), derived from `.claude-plugin/plugin.json` +
9
+ `skills/` + `commands/` via `npm run build:harness-manifests`
10
+ (`check:harness-manifests-fresh` gates freshness). Additive: the Claude
11
+ Code plugin surface is unchanged. This plugin ships no `.mcp.json`, so
12
+ neither harness's MCP story applies to it.
13
+
14
+ ### Maintenance
15
+ - **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
16
+
17
+ ## [0.8.50] — 2026-08-23
18
+
19
+ ### Maintenance
20
+ - **Lockstep bump riding the v0.8.50 cut** (`scripts/release/check-lockstep.mjs`); the headline v0.8.50 work ships in @adia-ai/web-components (prose/verse sheet removal gh#1885 / PR #1918, input/search fill-default posture gh#1856 — see `packages/web-components/CHANGELOG.md#0850--2026-08-23`). Package-local changes are listed below.
21
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
22
+ - **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
23
+
24
+ ### Fixed
25
+ - **`skills/package-release` regenerates Codex plugin manifests in Step 4 (gh#1899, PR #1917)** — `release-pack.mjs` + `cut-procedure.md` gain the post-bump `build:codex-manifests` regen so `check:codex-manifests-fresh` stays green through a cut; `a2ui-maintenance` (`mcp-pipeline-ops.md`) and `primitive-authoring` (`api-contract.md`) references refreshed in the same window.
26
+
3
27
  ## [0.8.48] — 2026-08-23
4
28
 
5
29
  ### Added
package/README.md CHANGED
@@ -48,6 +48,26 @@ ship `selftest` modes.
48
48
 
49
49
  `/package-release` · `/site-deployment` · `/demo-audit` · `/gen-ui-review`
50
50
 
51
+ ## Non-Claude harnesses
52
+
53
+ External install beyond Claude Code (AGENTS.md's own "Non-Claude harnesses"
54
+ section carries the shared degradation table this summarizes):
55
+
56
+ - **Codex** (gh#1888): `.codex-plugin/plugin.json` + per-skill
57
+ `agents/openai.yaml`, derived. No manifest key for hooks/commands/agents —
58
+ a command degrades to invoking its skill directly.
59
+ - **Hermes** (gh#1954): `plugin.yaml` + `__init__.py` at this package's
60
+ root — point Hermes's plugin loading at the installed
61
+ `node_modules/@adia-ai/adia-ui-forge` directory. No hooks/agents surface.
62
+ This plugin ships no `.mcp.json`, so there's no MCP story to wire either
63
+ way.
64
+ - **Pi** (gh#1954): discovered via the `pi-package` keyword and the `"pi"`
65
+ field in `package.json`; `prompts/*.md` gives every `/command` — including
66
+ the two consent-gated ones, `/package-release` and `/site-deployment` — a
67
+ genuine Pi prompt template (Pi's prompt layer is human-keystroke-only, the
68
+ same trust boundary those two already sit behind in Claude Code). No
69
+ hooks/agents surface.
70
+
51
71
  ## Contracts
52
72
 
53
73
  `references/contracts/` — the producer side of the MIGRATION GUIDE format and
package/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """adia-ui-kit-forge — generated by scripts/build/harness-manifests.mjs. Do not edit."""
2
+ import os
3
+
4
+ _HERE = os.path.dirname(os.path.abspath(__file__))
5
+
6
+
7
+ def register(ctx):
8
+ ctx.register_skill(
9
+ name="a2ui-maintenance",
10
+ path=os.path.join(_HERE, "skills", "a2ui-maintenance", "SKILL.md"),
11
+ description="Maintains the A2UI pipeline (packages/gen-ui/a2ui/): the chunk corpus, compose strategies (zettel, chunk-zettel, free-form, monolithic), retrieval, validator, calibration, evals, the a2ui MCP server. Use to author/harvest/fix chunks, tune STRONG_MATCH or zettel thresholds, validate an A2UI document, diagnose an eval gap/regression or lift a semantic fail, change MCP tools (generate_ui, compose_from_chunks, check_anti_patterns, refine_composition), scan anti-patterns, run pipeline ops, or when a contract can't express a shape. NOT for app screens (screen-composition), runtime gen-UI features (gen-ui-wiring), primitive authoring (primitive-authoring), or gallery scoring (gen-ui-review).",
12
+ )
13
+ ctx.register_skill(
14
+ name="demo-audit",
15
+ path=os.path.join(_HERE, "skills", "demo-audit", "SKILL.md"),
16
+ description="Seven-mode QA sweep of the monorepo's demo/app surfaces: visual probe, app-shell QA, attr-quote typos, native-primitive leak, admin-shell composition, card anatomy, plus an aggregated token/contrast/lifecycle drift battery (`npm run dogfood:status`). Use for \"run a dogfood sweep\", \"find broken demos\", \"audit native primitive leaks\". NOT for gen-UI gallery scoring (gen-ui-review) or authoring primitives (primitive-authoring).",
17
+ )
18
+ ctx.register_skill(
19
+ name="gen-ui-review",
20
+ path=os.path.join(_HERE, "skills", "gen-ui-review", "SKILL.md"),
21
+ description="Closed-loop Gen UI gallery quality review — per cycle: derive ideal specs, decompose the rendered canvas, score the gap, root-cause, emit fix plans until every prompt clears the exit gate. Use for /gen-review, \"score the gallery\", \"review gen-ui outputs\". NOT for corpus fixes (a2ui-maintenance), primitive authoring (primitive-authoring), or a hand-authored demo/app-shell QA sweep (demo-audit).",
22
+ )
23
+ ctx.register_skill(
24
+ name="llm-client-maintenance",
25
+ path=os.path.join(_HERE, "skills", "llm-client-maintenance", "SKILL.md"),
26
+ description="Maintains @adia-ai/llm (packages/llm/core/): provider adapters (anthropic/openai/ gemini), the shared SSE parser, model registry, chat()/streamChat() facade, createAdapter() bridge. Use when adding or fixing a provider adapter, debugging streaming bugs (StreamChunk, no terminal `done` chunk), raw `stopReason`/usage mapping, buildRequest() or passthrough proxy dispatch (browser 401s, API key in browser), detectProvider/MODELS registry changes, or the stub adapter. NOT for wiring the client into an app (llm-wiring, adia-ui-factory plugin).",
27
+ )
28
+ ctx.register_skill(
29
+ name="package-release",
30
+ path=os.path.join(_HERE, "skills", "package-release", "SKILL.md"),
31
+ description="Release engineering for the @adia-ai lockstep monorepo. Use to cut and ship a release, promote [Unreleased] CHANGELOG entries, tag and push lockstep packages to origin, publish a 10-package release (roster: scripts/package-paths.mjs), publish a single plugin independently of the lockstep set (Class B), batch-push piled-up release commits, recover a cut that landed wrong or whose publish workflows didn't fire, fix a check:lockstep bump failure or an F-N1 CHANGELOG warning, verify release gates without cutting anything, or author release notes/a MIGRATION GUIDE section. NOT for consumer-app migration sweeps (app-migration) or exe.dev VM ops (site-deployment).",
32
+ )
33
+ ctx.register_skill(
34
+ name="primitive-authoring",
35
+ path=os.path.join(_HERE, "skills", "primitive-authoring", "SKILL.md"),
36
+ description="Author or modify AdiaUI framework source inside the monorepo — primitives (packages/web-components), shells/composites (packages/web-modules), yaml SoTs, demos. Use to add a new primitive, fix a prop/slot/attribute/CSS variant, update a yaml, build or fix a shell (chat-shell, admin-shell, editor-shell — sidebar/pane/bespoke-tier composition), promote repeated inline content into a shared module, audit a component's four-axis contract/token usage/lifecycle for drift, or author a demo or examples.html. NOT for app screens (screen-composition), A2UI internals (a2ui-maintenance), @adia-ai/llm internals (llm-client-maintenance), or site/pages docs (site-docs-authoring).",
37
+ )
38
+ ctx.register_skill(
39
+ name="site-deployment",
40
+ path=os.path.join(_HERE, "skills", "site-deployment", "SKILL.md"),
41
+ description="Deploys and operates the AdiaUI site + services on exe.dev VMs — pushing a `site-v*` tag through the hardened rsync --delete deploy to ui-kit.exe.xyz (dry-run delete summary reviewed before the real deploy job runs), diagnosis (\"Port 8000 unbound\", a 502, a stale/404ing build behind npm after the last lockstep cut), rolling back a broken deploy, VM provisioning, secret rotation. Use for \"deploy to exe.dev\", \"push a site-v* tag\", \"the site is 502ing / looks stale\", \"roll back the last deploy\", \"restart/diagnose the exe service\", \"rotate keys on the VM\". NOT for cutting the release itself (package-release).",
42
+ )
43
+ ctx.register_skill(
44
+ name="site-docs-authoring",
45
+ path=os.path.join(_HERE, "skills", "site-docs-authoring", "SKILL.md"),
46
+ description="Review or author pages under site/pages/{architecture,getting-started, guides,patterns,reference}/ — the docs site (count the pages on disk; it grows). Use when asked to add or edit a getting-started/architecture/guides/patterns/reference page, review a site docs page for consistency, fix a callout that reads as plain text, or explain why an inline-code chip or a demo gallery looks broken. NOT for a component's own .examples.html demo (primitive-authoring) or any pure-primitive-composition training-harvest page (governed by composition-and-examples.md's no-style-block rule; owner: a2ui-maintenance).",
47
+ )
48
+ ctx.register_skill(
49
+ name="ssr-compatibility",
50
+ path=os.path.join(_HERE, "skills", "ssr-compatibility", "SKILL.md"),
51
+ description="Answers why an AdiaUI component crashes, drops content, renders wrong, or mutates a byte-identical subtree under SSR (linkedom/Astro) — the known failure shapes, what's fixed vs open, how to prove a fix under the linkedom shim gate. Use for \"does this work under SSR\", why a component crashes on attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/ `instanceof Node` under a DOM shim, why table-ui/chart-ui/select-ui or a container CE renders empty or drops nested children server-rendered, whether getBoundingClientRect() is safe in connectedCallback, whether a custom render()/connected() path adopts-in-place or rebuilds a server-rendered subtree that already matches (zero-subtree-mutation / AC-004a-shaped asks), whether a querySelector-guard-before-innerHTML component is SSR-safe, or whether a shim can be deleted after a fix ships. ANSWERS only. NOT for a fix (primitive-authoring) or host/hydration wiring (host-wiring, adia-ui-factory).",
52
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.48",
3
+ "version": "0.8.51",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
5
5
  "keywords": [
6
6
  "adia-ui",
@@ -9,7 +9,8 @@
9
9
  "web-components",
10
10
  "framework-authoring",
11
11
  "release-engineering",
12
- "maintainer"
12
+ "maintainer",
13
+ "pi-package"
13
14
  ],
14
15
  "license": "MIT",
15
16
  "author": {
@@ -19,15 +20,22 @@
19
20
  "bugs": {
20
21
  "email": "kim.granlund@adia.ai"
21
22
  },
23
+ "pi": {
24
+ "skills": ["./skills"],
25
+ "prompts": ["./prompts"]
26
+ },
22
27
  "files": [
23
28
  ".claude-plugin",
24
29
  ".codex-plugin",
25
30
  "skills",
26
31
  "agents",
27
32
  "commands",
33
+ "prompts",
28
34
  "references",
29
35
  "scripts",
30
36
  "hooks",
37
+ "plugin.yaml",
38
+ "__init__.py",
31
39
  "README.md",
32
40
  "CHANGELOG.md",
33
41
  "codex.interface.json"
package/plugin.yaml ADDED
@@ -0,0 +1,4 @@
1
+ name: "adia-ui-kit-forge"
2
+ version: "0.8.51"
3
+ description: "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin)."
4
+ manifest_version: 1
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: "Run the multi-mode QA sweep (visual probe, static QA, typo, native-primitive leak, shell composition, anatomy) across component demos, apps, playgrounds, and catalog."
3
+ argument-hint: "[mode or surface]"
4
+ ---
5
+
6
+ Run a dogfood sweep. **$ARGUMENTS**
7
+
8
+ Invoke **`demo-audit`**; run the requested modes (all, if unscoped), then
9
+ triage findings per its severity contract. Detection is script-driven; the
10
+ skill owns triage and the auto-fix boundary.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: "Run the closed-loop gen-UI quality review — score gallery outputs against ideal specs, trace root causes, produce the ranked improvement plan."
3
+ argument-hint: "[prompt subset]"
4
+ ---
5
+
6
+ Run a gen-UI review cycle. **$ARGUMENTS**
7
+
8
+ Invoke **`gen-ui-review`** and run its loop: derive ideal → decompose actual
9
+ → score → root-cause → plan. Corpus fixes route to `a2ui-maintenance`.
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: "Cut, tag, publish, and deploy an @adia-ai release (lockstep roster in skills/package-release/scripts/package-paths.mjs — npm libraries + the 2 Claude Code plugins). This invocation IS the authorization for the whole cycle; only gate failures stop it."
3
+ argument-hint: "[version or package]"
4
+ ---
5
+
6
+ Run a release. **$ARGUMENTS**
7
+
8
+ Invoke **`package-release`** and run its cut procedure end-to-end: pre-flight
9
+ gates → cut → tag → publish → deploy → notes. This command's invocation is
10
+ the operator's single authorization for the entire cycle (§Authorization,
11
+ operator ruling 2026-07-17) — run inline, don't re-ask per step; stop only
12
+ on a gate failure. The skill owns the discipline; don't restate it here.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: "Deploy a service or dist to the exe.dev VMs (ui-kit.exe.xyz and siblings), or diagnose a running exe service."
3
+ argument-hint: "[host or service]"
4
+ ---
5
+
6
+ Deploy or diagnose an exe.dev surface. **$ARGUMENTS**
7
+
8
+ Invoke **`site-deployment`** and follow its playbook for the named host; verify
9
+ against the live endpoint before reporting done.
@@ -59,9 +59,13 @@ MCP server restart, re-run `compose_from_chunks` for a fresh `state_id`.
59
59
  When the engine breaks expectations, fire `report_issue` with the most recent
60
60
  `state_id` (reporter: `llm` for agent self-fire, `user` for a human request).
61
61
  The record lands in the engine-internal telemetry store — scratch data for
62
- diagnosis. Anything worth durable tracking (a recurring pattern, a fix
63
- proposal) belongs in a GitHub issue or the PR description of the fixing
64
- change.
62
+ diagnosis. **[corrected 2026-08-23, ADR-0008 amendment]** that store is
63
+ `qa/findings/issues/<issue_id>.json`
64
+ (`packages/gen-ui/engine/compose/strategies/zettel/issue-reporter.js:10,26`,
65
+ `DEFAULT_STORAGE_ROOT`) — not the original `.brain/audit-history/issues/`
66
+ path, which was retired. Anything worth durable tracking (a recurring
67
+ pattern, a fix proposal) belongs in a GitHub issue or the PR description of
68
+ the fixing change.
65
69
 
66
70
  ## Validation checks
67
71
 
@@ -120,6 +120,8 @@ npm run eval:diff -- --engine free-form # 27 fresh free-form
120
120
  node scripts/release/write-eval-health.mjs --version <cut> # 28 evals/health/<version>.json committed, AC-01/AC-02 run for real (gh#1135, WS-4 SPEC REQ-06)
121
121
  node scripts/release/check-estate-split-latch.mjs # 29 no lockstep cut mid estate-split (ADR-0048 / gh#1192) — see the note below
122
122
  npm run check:catalog-tiers # 30 tier-index.json vs committed catalog (gh#1494 — ADR-0069 moved its PR-blocking half to derived-resync; the pre-cut roster re-asserts Class-R freshness before a tag. Gate 13 can't catch this: the harvester hashes tier-index.json as a SOURCE)
123
+ npm run check:codex-manifests-fresh # 31 Codex plugin.json + openai.yaml vs .claude-plugin/plugin.json SoT (gh#1888)
124
+ npm run check:harness-manifests-fresh # 32 Hermes/Pi plugin.yaml + __init__.py + prompts vs .claude-plugin/plugin.json + commands SoT (gh#1954)
123
125
  ```
124
126
 
125
127
  **Gate 29 was the ADR-0048 latch; since P5 it is a permanent invariant.** Between P1 and P5 the repo was correct in-repo but deliberately **not publishable** (old-name stubs marked `private: true` that the roster still mapped, plus dependency edges onto workspace packages no cut published), and a cut in that window would have shipped broken packages that npm cannot unpublish. **P5 cleared it by landing the real thing** — the six stubs became publishable shims and `PACKAGE_ROSTER` gained the three remaining new names, at which point all 9 offending edges resolved and the gate went green on its own. No gate logic was changed.
@@ -151,6 +153,10 @@ Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source,
151
153
 
152
154
  **4d.5b. Regenerate catalog tiers + re-harvest the chunk corpus if it goes stale** (gh#1361, automated — this was a recurring manual rider before): `npm run build:catalog-tiers`, then `npm run check:chunks-fresh`; if that probe goes stale, `npm run harvest:chunks`. `tier-index.json` derives from the same post-bump catalog 4d.5 just refreshed, and the chunk harvester hashes `tier-index.json` as a harvest SOURCE — regenerating tiers without re-harvesting left `check:chunks-fresh` red on the next run, needing a manual rider commit both cuts it happened live: v0.8.39 (`72417beff`) and v0.8.40/CUT-0840-B (`d9ca8b323`, "Ran `npm run build:catalog-tiers` ... That regen staled `check:chunks-fresh` ... so re-harvested"). The re-harvest is CONDITIONAL on the freshness probe, not unconditional — a tier regen that produces a byte-identical index owes no re-harvest. `tier-index.json`, `packages/gen-ui/engine/corpus/manifest.json`, and `packages/gen-ui/engine/corpus/chunks/` all ride the release commit (Step 5 stages them).
153
155
 
156
+ **4d.6. Regenerate the Codex plugin manifests** (gh#1888, gh#1899): `node scripts/build/codex-manifests.mjs`, then `npm run check:codex-manifests-fresh` to confirm. Both plugins' `.codex-plugin/plugin.json` embed `version` from `.claude-plugin/plugin.json`, which the 4b bump just moved — same "derived artifact carries the lockstep version" class as 4d.5's genui catalog. Gate 31 runs pre-bump in Step 3 and only proves freshness against the PREVIOUS version; nothing re-ran the generator post-bump before this line existed — the v0.8.48 release PR (#1897) shipped stale manifests as a result, caught by `check:codex-manifests-fresh` in CI and fixed by hand on the release branch. `packages/plugins/*/.codex-plugin/plugin.json` and `packages/plugins/*/skills/*/agents/openai.yaml` ride the release commit (Step 5 stages them).
157
+
158
+ **4d.7. Regenerate the Hermes/Pi plugin manifests** (gh#1954): `node scripts/build/harness-manifests.mjs`, then `npm run check:harness-manifests-fresh` to confirm. Same hazard as 4d.6, same fix — both plugins' `plugin.yaml` embed `version` from `.claude-plugin/plugin.json`. Gate 32 runs pre-bump in Step 3 and only proves freshness against the PREVIOUS version; run this post-bump every cut, not just when a skill/command changed. `packages/plugins/*/plugin.yaml`, `packages/plugins/*/__init__.py`, `packages/plugins/*/hermes-mcp.yaml` (factory only), and `packages/plugins/*/prompts/*.md` ride the release commit (Step 5 stages them).
159
+
154
160
  **4e. Release docs + team notes** (gated, not optional): review the entry
155
161
  files the release touches (root README/CHANGELOG, per-package READMEs —
156
162
  content currency is YOUR judgment; the gate only proves presence), then
@@ -212,6 +218,8 @@ node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/bump.mjs" --from X.Y.
212
218
  npm install --package-lock-only --no-audit --no-fund # 4c
213
219
  npm run check:lockstep # 4d
214
220
  node scripts/build/derive-genui-catalog.mjs # 4d.5 — catalogId carries the bumped version (gh#617)
221
+ node scripts/build/codex-manifests.mjs # 4d.6 — Codex manifest version carries the bumped version (gh#1899)
222
+ node scripts/build/harness-manifests.mjs # 4d.7 — Hermes/Pi manifest version carries the bumped version (gh#1954)
215
223
  node scripts/release/check-release.mjs --pending-version X.Y.Z --fix # 4f — AFTER the stubs exist
216
224
  node scripts/release/generate-release-notes.mjs --version X.Y.Z --write # 4e
217
225
  node scripts/release/check-release-docs.mjs --version X.Y.Z # 4e gate
@@ -137,6 +137,11 @@ export const GATE_ROSTER = [
137
137
  // hazard the same way a stale dist bundle is a real Claude-install
138
138
  // hazard. Appended, not inserted — same numbering discipline as gate 30.
139
139
  { n: 31, cmd: 'npm run check:codex-manifests-fresh', what: 'Codex plugin.json + openai.yaml vs .claude-plugin/plugin.json SoT' },
140
+ // gh#1954: the Hermes (plugin.yaml + __init__.py + hermes-mcp.yaml) and Pi
141
+ // (prompts/*.md) external-distribution overlays are derived the same way
142
+ // gate 31's Codex tree is — a stale copy is the same real install hazard.
143
+ // Appended, not inserted — same numbering discipline as gate 30/31.
144
+ { n: 32, cmd: 'npm run check:harness-manifests-fresh', what: 'Hermes/Pi plugin.yaml + __init__.py + prompts vs .claude-plugin/plugin.json + commands SoT' },
140
145
  ];
141
146
 
142
147
  // -- CLI ------------------------------------------------------------------
@@ -627,6 +627,29 @@ function step4PromoteAndBump(args) {
627
627
  console.log(' ✓ chunk corpus still fresh after catalog-tiers regen — no re-harvest needed');
628
628
  }
629
629
 
630
+ // Step 4d.6 — regenerate the Codex plugin manifests (gh#1888, gh#1899): both
631
+ // .codex-plugin/plugin.json's `version` and each per-skill agents/openai.yaml
632
+ // derive from .claude-plugin/plugin.json, which the bump above just moved —
633
+ // same "derived artifact embeds the lockstep version" class as 4d.5's genui
634
+ // catalog. Gate 31 (check:codex-manifests-fresh) runs pre-bump in Step 3, so
635
+ // it only proves freshness against the PREVIOUS version; nothing re-ran the
636
+ // generator after the bump until now — the release PR for v0.8.48 (#1897)
637
+ // shipped stale manifests as a result (Static checks + MCP smoke failed on
638
+ // check:codex-manifests-fresh, fixed by hand on the release branch). Step 5
639
+ // stages both plugins' .codex-plugin/plugin.json + skills/*/agents/openai.yaml.
640
+ console.log('\n Step 4d.6 — regenerate Codex plugin manifests (version embeds the lockstep bump):');
641
+ sh('node scripts/build/codex-manifests.mjs', args);
642
+ if (args.dry) {
643
+ console.log(' [dry] npm run check:codex-manifests-fresh (post-regen proof)');
644
+ } else if (!shOk('npm run check:codex-manifests-fresh')) {
645
+ console.error('\nERROR: Codex plugin manifests still stale after regeneration — codex-manifests.mjs');
646
+ console.error(' did not produce output matching check:codex-manifests-fresh\'s expectations.');
647
+ console.error(' Run both by hand to diagnose before re-running the cut.');
648
+ process.exit(1);
649
+ } else {
650
+ console.log(' ✓ Codex plugin manifests fresh after regeneration');
651
+ }
652
+
630
653
  // Step 4e — generate docs/ops/releases/vX.Y.Z.md (gh#399: neither this step nor
631
654
  // its staging into the release commit existed before — check-cut-hygiene's
632
655
  // "notes present" claim (Step 4g, below) went unverified until
@@ -747,7 +770,17 @@ function step5Commit(args) {
747
770
  // generation MCP (@adia-ai/mcp) in .mcp.json (SKILL invariant 8 —
748
771
  // bump.mjs moves the pin too).
749
772
  const files = [`${dir}/package.json`, `${dir}/CHANGELOG.md`];
750
- if (plugin) files.push(`${dir}/.claude-plugin/plugin.json`);
773
+ if (plugin) {
774
+ files.push(`${dir}/.claude-plugin/plugin.json`);
775
+ // gh#1899: Step 4d.6 regenerates both plugins' Codex manifest tree
776
+ // post-bump (version embeds the lockstep bump, same class as the
777
+ // genui catalog outputs above) — stage it here or CI catches the
778
+ // drift the way v0.8.48's release PR (#1897) did. `skills/*/
779
+ // agents/openai.yaml` is a glob: `git add` expands it via the shell,
780
+ // and the per-skill count isn't statically knowable (same shape as
781
+ // the chunk-corpus directory pathspec above).
782
+ files.push(`${dir}/.codex-plugin/plugin.json`, `${dir}/skills/*/agents/openai.yaml`);
783
+ }
751
784
  if (name === 'adia-ui-factory') files.push(`${dir}/.mcp.json`);
752
785
  return files;
753
786
  }),
@@ -1169,6 +1202,17 @@ function selftest() {
1169
1202
  // health record, or it rides untracked past the release commit
1170
1203
  // (v0.8.35/v0.8.36 both did, PR #1196 swept them by hand).
1171
1204
  'evals/health/9.9.9.json',
1205
+ // gh#1899 — Step 4 must regenerate the Codex plugin manifests post-bump
1206
+ // (the v0.8.48 release PR #1897 shipped stale ones without this step).
1207
+ 'Step 4d.6 — regenerate Codex plugin manifests',
1208
+ '[dry] node scripts/build/codex-manifests.mjs',
1209
+ // gh#1899 — Step 5's staging pathspec must carry both plugins' Codex
1210
+ // manifest tree, or it rides untracked/uncommitted past the release
1211
+ // commit the same way the genui catalog outputs did before gh#617.
1212
+ 'packages/plugins/adia-ui-forge/.codex-plugin/plugin.json',
1213
+ 'packages/plugins/adia-ui-forge/skills/*/agents/openai.yaml',
1214
+ 'packages/plugins/adia-ui-factory/.codex-plugin/plugin.json',
1215
+ 'packages/plugins/adia-ui-factory/skills/*/agents/openai.yaml',
1172
1216
  ];
1173
1217
  for (const marker of need) {
1174
1218
  if (!out.includes(marker)) {
@@ -16,7 +16,7 @@ file, including the depth references those entries cross-link.
16
16
  - [api-contract.md](api-contract.md) — how do I declare `static properties` (type, default, reflect, attribute), avoid the `attr:` silent-typo trap, and expose a selection-item's `selected` state (declared reflected prop, never a `data-*` stamp — ADR-0056)?
17
17
  - [yaml-contract.md](yaml-contract.md) — what fields does the `<name>.yaml` SoT take, and how does it feed the generated `.a2ui.json` sidecar?
18
18
  - [token-contract.md](token-contract.md) — is this component's CSS honoring the two-block `@scope` token contract, and why does it misrender under a theme?
19
- - [css-patterns.md](css-patterns.md) — how do I structure a component stylesheet: two-block `@scope`, variants vs modes, token layering?
19
+ - [css-patterns.md](css-patterns.md) — how do I structure a component stylesheet: two-block `@scope`, variants vs modes, token layering, deriving an inset item's border-radius from its rounded container (concentric-corner radius)?
20
20
  - [lifecycle-patterns.md](lifecycle-patterns.md) — how do I keep `connected()`/`disconnected()` symmetric for listeners, timers, observers, and popovers?
21
21
  - [svg-authoring.md](svg-authoring.md) — viewBox/coordinate scaling, stroke-width, text-anchor/dominant-baseline, card-bleed clipping, currentColor vs `var()` inside SVG markup, hit-testing — for `chart-ui`/`qr-code-ui`/`icon-ui`/any new `<svg>`-emitting primitive.
22
22
  - [form-control-sizing.md](form-control-sizing.md) — does a form-control host fill its container or hug its content, and where does the legibility floor live (ADR-0077)?
@@ -119,6 +119,30 @@ shape — see the ADR-0063 conventions list in
119
119
 
120
120
  Source: [ADR-0074](../../../../../../docs/ops/adr/adr-0074-chart-ratio-attribute-grammar.md).
121
121
 
122
+ **[verified 2026-08-23] Failure mode — DOM-ancestry auto-detection is riskier
123
+ than container-query auto-detection.** ADR-0081 Decision 1 (`chart-ui[labels]`,
124
+ a 3-value enum: `""`/`chip`/`outside`) initially mirrored this same
125
+ auto/explicit-override shape, but auto-detected via DOM ancestry — resolving
126
+ unset `labels=""` to `chip` whenever the element sat inside a `section[bleed]`
127
+ or `card-ui[padding="none"]` ancestor — instead of a container query on the
128
+ element's own box. The 2026-08-21 amendment walked that back entirely after it
129
+ silently flipped 2 of 60 fixtures on the Charts visual-eval floor
130
+ (`comp-chart-in-card-n-*`, pre-existing full-bleed compositions), caught only
131
+ by the floor's pixel-diff gate — neither the unit-test suite nor code review
132
+ renders real CSS/layout. Amended decision: unset `labels=""` resolves to
133
+ `outside` unconditionally; `chip` mode activates only via the explicit
134
+ `labels="chip"` attribute — no ancestry detection at all. ADR-0081's Decision
135
+ 2 (a `today` marker attribute) independently rejects clock-derived
136
+ auto-detection for the same reliability reason (cites IDR-0006). Lesson for a
137
+ new auto-snap attribute of this shape: auto-detection is safe when it reads
138
+ the element's *own* rendered geometry (a container query against its own box,
139
+ as `ratio` does); it is risky when it reads *ancestor* DOM state or wall-clock
140
+ time, because neither is guaranteed stable across every composition that
141
+ happens to nest the element — verify any such default against a real
142
+ pixel-diff/visual-eval gate, not unit tests or review alone. Source:
143
+ [ADR-0081](../../../../../../docs/ops/adr/adr-0081-chart-2-0-foundations-attribute-grammar.md)
144
+ Amendment (2026-08-21).
145
+
122
146
  ## Numeric props — `null` over sentinels
123
147
 
124
148
  Indeterminate, unknown, or "not yet set" numeric state uses `null`, not `-1` or `Infinity`:
@@ -99,6 +99,8 @@ Does your [attribute=value] need to change any of:
99
99
 
100
100
  This list is the single source of truth. If your mode isn't here, add it. If adding would feel weird, that's a signal the "mode" is actually a **sibling component** — prefer `code-inline-ui` over `code-ui[inline]` unless the attribute genuinely toggles one surface between two states of the same thing.
101
101
 
102
+ **Orthogonal boolean combinations need every combination defined, not left as an undefined hybrid.** Two independently-toggleable boolean attributes on the same component (e.g. stat-ui's `band` and `bleed`) form an N² space; shipping three of the four combinations and leaving the fourth undefined means a consumer who reaches it gets whatever the cascade happens to produce, not a designed layout. ADR-0083 gave stat-ui's `band bleed` combination a real contract — the band bleeds inline-start/inline-end/block-end via the `--card-inset` negative-extent technique, with `[slot="change"]` overlaid as a z-ordered, `pointer-events: none` chip anchored block-start/inline-end. When you add a second orthogonal boolean to an existing single-boolean variant, audit all four quadrants before shipping — an unstyled hybrid is a defect, not a follow-up.
103
+
102
104
  ## Font-family floor — text-bearing primitives must anchor to a token
103
105
 
104
106
  A primitive that renders text must NOT rely on `font: inherit` / `font-family: inherit` alone. Those carry **no default** — the primitive inherits whatever the host page sets, so a consumer page with a broken or serif `font-family` (a dead token, a missing `--a-font-family`, a serif host document) makes the primitive's labels render in UA serif while token-anchored siblings (`text-ui`) stay correct. A confusing same-page split — the exact bug behind an embedded-app `<segmented-ui>` serif regression (25 primitives shared the flaw).
@@ -218,6 +220,83 @@ Example carve-out:
218
220
 
219
221
  A cursory check might flag `--col-*` as "wrong" because the file is named `layout.css`. It's not wrong — the **scope tag** determines the stem, not the filename.
220
222
 
223
+ **Cross-component token-fallback aliasing.** A new component's tokens can alias a sibling component's existing token ladder as their `var()` fallback instead of minting an independent scale — when the two components share a visual role closely enough that re-theming one should re-theme the other in the same stroke. ADR-0083's stat-ui `[slot="change"]` chip mints six stat-scoped tokens (`--stat-change-*`) that fall back to chart.css's existing `--chart-chip-*` rungs:
224
+
225
+ ```css
226
+ :where(:scope) {
227
+ --stat-change-bg: var(--chart-chip-bg);
228
+ --stat-change-fg: var(--chart-chip-fg);
229
+ }
230
+ ```
231
+
232
+ A consumer who re-themes chart chips re-themes the stat delta chip too, with no separate override required. Reach for this only when the aliasing component is a genuine visual sibling of the aliased one (same role, same page context) — otherwise it's spooky action at a distance when the aliased component's tokens change for an unrelated reason.
233
+
234
+ ## Concentric-corner radius — deriving an item's radius from its container
235
+
236
+ When a rounded container pads a rounded-corner item flush against its own edge — a popover listbox around `[role="option"]` rows, a menu popover around `menu-item-ui` — a flat item-radius token only reads as concentric with the container's own corner at the one padding value it happened to be tuned against. Change the density scale or the radius scale independently (a consumer re-theme, a `--a-density` step) and the two arcs drift apart.
237
+
238
+ **The formula.** Solve for the ITEM's radius; keep the container's padding fixed:
239
+
240
+ ```
241
+ Ir = max(0, Cr − Cp)
242
+ ```
243
+
244
+ - `Cr` — the container's own `border-radius` (an existing fixed radius-scale token, untouched).
245
+ - `Cp` — the container's own padding/inset (an existing fixed spacing-scale token, untouched — this pattern never derives padding).
246
+ - `Ir` — the item's `border-radius`, the only new derived value.
247
+
248
+ **Why solve for `Ir`, not `Cp`.** The tempting reverse direction — hold the item's radius fixed and derive the container's padding from it — needs a second term, `min(Ir, Ih/2)` (`Ih` = item height), to keep the derived padding from going negative once the item is short enough that its own radius would pill-clamp. That's the browser's own `border-radius` clamp (CSS Backgrounds §5.5 — a radius past half an element's shorter side reduces to a pill) reimplemented by hand inside the padding formula. Solving for `Ir` instead gets that clamp for free: `max(0px, calc(Cr - Cp))` alone is correct at every item height, because the browser applies its pill-clamp to the declared `Ir` automatically — no `min()` term needed anywhere in the CSS.
249
+
250
+ **Companion rule — the `min-height` floor is load-bearing, not optional.** The browser's pill-clamp cuts both ways: if the item is shorter than `2 × Ir`, the *effective* rendered radius clamps down even though the *declared* `Ir` is correct — the item silently stops reading as concentric, with no error and no visual-eval regression to catch it (a live-browser-only defect class, the same shape as this file's `display:contents` entry below). Pair every derived radius with a matching height floor:
251
+
252
+ ```css
253
+ min-height: calc(2 * <item-radius-var>);
254
+ ```
255
+
256
+ Ruled (Kim, 2026-08-24): enforce this floor rather than let it silently degrade — items get taller at large radius/density scales as the accepted tradeoff for exact concentricity, not a bug to route around.
257
+
258
+ **Worked example** (`select-ui`'s listbox — the reported surface, gh#1956):
259
+
260
+ ```css
261
+ /* Container — Cr and Cp declared as LOCAL custom properties on the
262
+ popover's own top-layer rule. A top-layer popover usually can't inherit
263
+ the host element's @scope'd component tokens (a different, often
264
+ detached DOM subtree once popover-open) — declaring them locally here
265
+ sidesteps that; a local declaration DOES inherit down to a genuine DOM
266
+ child. */
267
+ select-ui [slot="listbox"] {
268
+ --select-listbox-padding: var(--a-space-1); /* Cp — fixed */
269
+ --select-listbox-radius: var(--a-radius); /* Cr — fixed */
270
+ padding: var(--select-listbox-padding);
271
+ border-radius: var(--select-listbox-radius);
272
+ }
273
+
274
+ /* Item — Ir derived; Cr/Cp inherited from the listbox above because
275
+ [role="option"] is a genuine DOM child of it (an appended option row). */
276
+ select-ui [slot="listbox"] [role="option"] {
277
+ --select-option-radius:
278
+ max(0px, calc(var(--select-listbox-radius) - var(--select-listbox-padding)));
279
+ border-radius: var(--select-option-radius);
280
+ min-height: calc(2 * var(--select-option-radius));
281
+ }
282
+ ```
283
+
284
+ (`packages/web-components/components/select/select.css:330-343` and `:378-386`.)
285
+
286
+ Express `Ir` as a live `calc()`/`max()` referencing the radius/space custom properties — never a baked pixel value — so it holds across every `--a-radius-k` / `--a-density` scale change.
287
+
288
+ **Shipped in** (gh#1956, PR #1959 — the reference implementation for this pattern):
289
+
290
+ - `select.css:330-343,378-386` — `[slot="listbox"]` vs. `[role="option"]`.
291
+ - `combobox.css:209-221,254-264` — `[data-listbox]`.
292
+ - `tags-input.css:166-184,200-209` — `[data-suggestions]`.
293
+ - `menu.css:51-65,90-92` — `[data-menu-popover]` vs. `menu-item-ui`.
294
+ - `context-menu.css:26-36,56-58` — `[data-context-menu-surface]`, which shares `menu-item-ui` rows with `menu.css`.
295
+ - `nav-group.css:328-340,358-365` — `[slot="popover"]` vs. `[role="option"]`. Previously gave option rows the container's own flat radius — a design inconsistency next to every sibling popover's distinct smaller item radius. Deriving `Ir` resolves it with no special-casing: under this direction (unlike the reverse "solve for `Cp`" direction, which floors padding to a hard 0px here and was reverted) the row's radius just comes out smaller than the container's automatically.
296
+ - `calendar-picker.css:87` / `date-range-picker.css:267` — `calendar-grid-ui`'s day cells (gh#1966), a variant shape: the item primitive is a *shared substrate* consumed by two different popover containers with different `Cr`/`Cp`, not a single component owning both container and item, so `Ir` is derived once per consumer (not once in the shared `calendar-grid.css`) via a `--calendar-grid-day-radius` override cascaded down. `date-range-picker`'s popover padding is asymmetric (`--date-range-picker-px` ≠ `-py`) — a single circular radius can't be exactly concentric on both axes, so it derives from `min(px, py)`, the tighter constraint; harmless on the corner (bottom-left, with a preset rail present) that isn't actually flush. Also the first shipped case to pair the derived radius with the `min-height: calc(2 * Ir)` companion floor (`calendar-grid.css:199`), per the ruling above.
297
+
298
+ Audited, not applicable: `command-ui`, `drilldown-ui` — item list sits inside a padded region behind a header, not flush against the container's own rounded corner, or the host carries no radius at all.
299
+
221
300
  ## Slot styling without `::slotted()`
222
301
 
223
302
  AdiaUI is light-DOM. Slotted children are just children. Style them with attribute selectors:
@@ -150,6 +150,23 @@ disconnected() {
150
150
 
151
151
  **Lesson:** a class must declare `disconnected()` exactly once. If you need to add teardown, edit the existing method. Duplicate method names silently shadow — no error, no warning.
152
152
 
153
+ ## Reflection — non-finite Number-typed props don't stringify
154
+
155
+ `reflect()` in `core/element.js` writes a `Number`-typed reflected property's value back onto the DOM attribute by stringifying it. That's correct for finite numbers, but a non-numeric sentinel assigned to a `Number`-typed reflected prop (e.g. `range-total="?"` — table-ui's open/unproven-total marker for cursor/hasMore server paging) used to get silently coerced and re-stringified as the literal attribute `range-total="NaN"`, before any render ran (gh#1895, PR #1907, ADR-0082 amendment).
156
+
157
+ **Fix, framework-wide:** a non-finite `Number`-typed value is left unreflected instead of stringified — gated narrowly on `Number` type + `Number.isFinite()` failing, so no other reflected prop's behavior changes.
158
+
159
+ ```javascript
160
+ // core/element.js reflect() — the narrow gate
161
+ if (type === Number && !Number.isFinite(value)) {
162
+ // leave unreflected — do NOT write "NaN" to the attribute
163
+ } else {
164
+ this.setAttribute(attr, String(value));
165
+ }
166
+ ```
167
+
168
+ **Recognition:** any future non-numeric sentinel authored onto a `Number`-typed reflected prop (a `'?'`, an `'auto'`, an open-ended marker) hits the same wart unless it too fails `Number.isFinite()` before the stringify step. If a prop's contract legitimately mixes numbers with a sentinel string, declare it non-reflecting or widen its declared type — don't rely on the reflect() gate alone as documentation of the contract.
169
+
153
170
  ## `UIFormElement` — `super` discipline
154
171
 
155
172
  Form-participating components inherit `ElementInternals` wiring from `UIFormElement`. That wiring only works if `super.connected()` and `super.disconnected()` are called:
@@ -176,6 +176,8 @@ Either ship as a standalone lockstep cut (per the sibling **package-release** sk
176
176
  | **C. Inside a shell cluster** | Almost never — fails if the element is consumed by _other_ shells. Use only when the element is structurally shell-bound. |
177
177
  | **D. Promote to primitive** | Only if it composes < 3 other primitives. Most controls fail this test. |
178
178
 
179
+ **ADR prose drift — don't trust an ADR's "Today's work" narrative for live paths.** ADR-0012 is the source for the primitives-vs-modules-vs-themes decision above, but its own "Today's work"/outcome-notes prose has drifted out from under its ratified Decision: that prose describes `packages/web-components/patterns/` as the shell/chat/editor/runtime source-clustering directory, but present-day `patterns/` holds 45+ usage-example dirs instead — source moved on to `packages/web-modules/{chat,editor,runtime,shell}/` per the later ADR-0015. The same drift claims `@adia-ai/web-themes` as a sibling workspace package; it never shipped that way — themes live under `packages/web-modules/theme/` as a subpath instead. An ADR's ratified **Decision** stays append-only-safe; its **"Today's work" / outcome-notes prose is not** — later reorganizations can strand it, and a reader following the literal directory claims today would git-mv into the wrong (docs-example) directory. Before executing a cluster-placement call cited from an ADR, verify the directory it names still matches the live tree (see "Path-rebase awareness" below) rather than trusting the ADR's narrative section verbatim.
180
+
179
181
  ## Two rules from observed bugs
180
182
 
181
183
  ### OD-002 — Absorb the standalone-button-next-door