@adia-ai/adia-ui-forge 0.8.59 → 0.8.61

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.
Files changed (26) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +68 -0
  4. package/package.json +1 -1
  5. package/plugin.yaml +1 -1
  6. package/references/contracts/a2ui-mcp-surface.md +1 -1
  7. package/scripts/lint-rules.generated.mjs +2418 -359
  8. package/skills/a2ui-maintenance/references/eval-diagnostics.md +2 -2
  9. package/skills/a2ui-maintenance/references/pipeline-overview.md +26 -0
  10. package/skills/a2ui-maintenance/references/strategy-engines.md +12 -0
  11. package/skills/demo-audit/references/admin-shell-anatomy.md +18 -12
  12. package/skills/gen-ui-review/scripts/gen-review-decompose.mjs +27 -34
  13. package/skills/gen-ui-review/scripts/overflow-detect.generated.mjs +101 -0
  14. package/skills/package-release/references/cut-procedure.md +49 -5
  15. package/skills/package-release/references/recovery-paths.md +20 -0
  16. package/skills/package-release/scripts/bump.mjs +36 -6
  17. package/skills/package-release/scripts/dispatch-publish.mjs +8 -0
  18. package/skills/package-release/scripts/gate-roster.mjs +4 -4
  19. package/skills/package-release/scripts/release-pack.mjs +485 -68
  20. package/skills/package-release/scripts/stale-copy-warning.mjs +215 -0
  21. package/skills/package-release/scripts/tag-lockstep.mjs +8 -0
  22. package/skills/primitive-authoring/references/INDEX.md +1 -0
  23. package/skills/primitive-authoring/references/examples-structure.md +52 -0
  24. package/skills/primitive-authoring/references/shell-patterns.md +12 -11
  25. package/skills/primitive-authoring/references/token-contract.md +2 -2
  26. package/skills/primitive-authoring/references/yaml-contract.md +106 -9
@@ -46,7 +46,7 @@ fluctuate).
46
46
  ## Phase 2 — fixes per bucket
47
47
 
48
48
  - **A. Holdout alignment** — map each intent's `expected_chunk` to the actual
49
- top-1; update `packages/gen-ui/engine/corpus/evals/holdout-compose-from-chunks.jsonl`.
49
+ top-1; update `packages/gen-ui/engine/evals/corpus/holdout-compose-from-chunks.jsonl`.
50
50
  - **E. Measurement traps** — PascalCase→kebab (`AgentTrace` → `agent-trace`,
51
51
  not `agenttrace`); substring false positives (`pane` vs `panel`,
52
52
  `textarea-ui` contains `text-ui` — word boundaries); case sensitivity (`/i`).
@@ -82,7 +82,7 @@ the SKILL.md floors hold.
82
82
  The two `check:*-eval-regression` scripts own the floor numbers — read the source
83
83
  before quoting a number elsewhere; SKILL.md only mirrors them and can drift (it
84
84
  once silently regressed to `cov≥40` before the mechanical gate existed). Zettel's
85
- floors are a committed file, `evals/health/zettel-floor.json` (gh#1391)
85
+ floors are a committed file, `packages/gen-ui/engine/evals/health/zettel-floor.json` (gh#1391);
86
86
  `scripts/release/check-zettel-eval-regression.mjs` loads it at runtime and refuses
87
87
  to run without it, so re-baselining is a JSON diff, not a source edit. Free-form's
88
88
  floors are still `ALERT_FLOOR`/`HARD_FLOOR` constants in
@@ -193,6 +193,32 @@ for any constant or decision lives in git and PR descriptions
193
193
  SoT); ADR-0097 rules a fourth, `traits`, but that part is decided-not-
194
194
  yet-shipped (gh#2513) — see `primitive-authoring/references/
195
195
  yaml-contract.md` §Synthesized universal props for the full contract.
196
+ 14. **Provider "extended thinking" is a strategy-level opt-in, not a
197
+ global default** (gh#3516, LLD-0033). PR #3511 (gh#3477) made
198
+ `{ thinking, thinkingBudget }` reachable through
199
+ `AdiaUILLMBridge.complete()`/`stream()`, default off; two call sites
200
+ opt in for real, each behind its own fixed-constant budget rather than
201
+ a caller-threaded option: `generate-thinking.js`'s own generate call
202
+ (`THINKING_BUDGET_MONOLITHIC_THINKING`) and `free-form-composer/
203
+ index.js`'s ingredient-picker call, both its primary pick and its own
204
+ paraphrase-retry (`THINKING_BUDGET_FREE_FORM`), both starting at the
205
+ bridge's own `DEFAULT_THINKING_BUDGET`, lowered to 4000 (gh#3516 slice
206
+ 3, conductor ruling 2026-09-08) after an n=5 measurement at 10000
207
+ showed no measurable quality gain with real cost. `auto` inherits
208
+ through the free-form picker call the moment it escalates that far -
209
+ `monolithic-thinking` itself is never reachable via `auto`'s own
210
+ escalation ladder. Every other LLM call site (`generate-pro.js`'s four
211
+ branches, zettel's locator/modifier/synthesizer, the shared
212
+ `validate-and-repair.js` repair loop) stays thinking-off deliberately,
213
+ a narrow first pass pending real eval numbers - do not assume a new
214
+ engine inherits thinking by proximity to one that has it.
215
+ `StubLLMAdapter` accepts and records `thinking`/`thinkingBudget` on
216
+ its own `calls` log for strategy-level test assertions.
217
+ `eval-diff.mjs` gained `--mode` (`instant`/`pro`/`thinking`), scoped
218
+ to `--engine mcp` only, so `--engine mcp --mode thinking` exercises
219
+ the opted-in path independently of the router's own default. See
220
+ `docs/ops/lld/lld-0033-strategy-thinking-opt-in.md` for the full
221
+ decision record and the conductor's D1-D5 rulings.
196
222
 
197
223
  ## Test + run commands (all verified in root package.json)
198
224
 
@@ -86,6 +86,18 @@ shows the results); `--strict` enforcement is deferred to TKT-0010
86
86
  (27% of the corpus currently fails, mostly a schema-generation gap
87
87
  around `data-*`/`span` attributes rather than corpus-content defects).
88
88
 
89
+ ## Provider thinking opt-in kill switch (gh#3516, LLD-3516 slice 1)
90
+
91
+ `monolithic-thinking` and `free-form-composer` opt into provider thinking at
92
+ a fixed strategy-level budget (`THINKING_BUDGET_MONOLITHIC_THINKING`,
93
+ `THINKING_BUDGET_FREE_FORM`, both 4000 as of gh#3516 slice 3) via the shared
94
+ `compose/shared/thinking-opt-in.js` helper. `ADIA_THINKING_OPT_IN=0` drops
95
+ the `thinking`/`thinkingBudget` fields at both call sites entirely (not
96
+ `thinking: false`, the adapter never sees the keys), giving the
97
+ thinking-off baseline a runtime lever instead of a code revert. Unset (or
98
+ `"1"`) keeps current behavior. This is an operations knob, not a caller
99
+ option: no consumer threads it through `generateUI()`.
100
+
89
101
  ## Pitfalls
90
102
 
91
103
  - **`STRONG_MATCH_THRESHOLD` was raised 22 → 40 post-incident.** Lowering it
@@ -17,7 +17,7 @@ the live admin-dashboard example.
17
17
  `<select-ui avatar="…" value="…" variant="ghost">` context switcher —
18
18
  NOT `<menu-ui>` (legacy pattern) — ★ commonly mis-implemented
19
19
  4. Sidebar nav wrap around `<nav-ui>` — `<section>` (per the examples),
20
- `<section-ui>` (card-style chrome), or `<admin-scroll>` all accepted;
20
+ `<section-ui>` (card-style chrome), or `<page-scroll>` all accepted;
21
21
  a bare `<nav-ui>` direct child overflows long lists
22
22
  5. Sidebar `<admin-statusbar slot="footer">` with `<select-ui avatar="…">`
23
23
  (user menu) — ★ commonly missing
@@ -28,17 +28,23 @@ the live admin-dashboard example.
28
28
  ★ spacer + actions commonly missing
29
29
  8. `[data-actions]` contains `<popover-ui>` + `<theme-panel slot="content">`
30
30
  (there is no `<theme-picker-ui>`)
31
- 9. `<admin-scroll>` wrapping optional `<aside data-subnav hidden>` +
32
- `<router-ui>` (or `<admin-page>` directly for non-routed)
33
- **[deprecated 2026-09-01, ADR-0098]** `admin-scroll` is a wholesale
34
- rename to `page-scroll` (both modes carried over); `admin-scroll` stays
35
- as a working compat alias for the deprecation window only.
31
+ 9. `<page-scroll>` wrapping optional `<aside data-subnav hidden>` +
32
+ `<router-ui>` (or `<page-ui band>` directly for non-routed)
33
+ **[deleted, ADR-0098 / gh#3745]** `admin-scroll` was a wholesale
34
+ rename to `page-scroll` (both modes carried over); its one-release
35
+ deprecation window has closed and the module is deleted (gh#3745).
36
+ The `admin-page` family (`admin-page`/`admin-page-header`/
37
+ `admin-page-body`) is deleted outright per ADR-0098, no compat alias.
38
+ `page-scroll` wrapping `page-ui[band]` is the sole successor for the
39
+ routed and non-routed case alike.
36
40
  10. `<admin-page>` with `<admin-page-header>` + `<admin-page-body>`
37
- **[deprecated 2026-09-01, ADR-0098]** The `admin-page` family is
38
- retired deprecate-then-delete in favor of `page-ui[band]` — `page-ui`
39
- is now the one canonical page-chrome primitive; this anatomy still
40
- describes the pre-migration shape for auditing legacy surfaces during
41
- the deprecation window.
41
+ **[deleted, ADR-0098]** The `admin-page` family is deleted outright,
42
+ no compat alias and no open deprecation window, in favor of
43
+ `page-ui[band]`; `page-ui` is now the one canonical page-chrome
44
+ primitive (item 9 above shows the live composition: `page-scroll`
45
+ wrapping `page-ui[band]`). This item is kept only so this anatomy
46
+ still recognizes the retired shape when auditing legacy surfaces
47
+ that predate the migration; never author it in new surfaces.
42
48
  11. `<admin-statusbar>` at content footer (version strip) — ★ commonly missing
43
49
  12. Second `<admin-sidebar slot="trailing">` (inspector rail, hidden by
44
50
  default) — strongly recommended
@@ -64,7 +70,7 @@ the live admin-dashboard example.
64
70
  | content topbar missing `[data-spacer]` / `[data-actions]` | warning — part 7 |
65
71
  | content missing trailing `<admin-statusbar>` | warning — part 11 |
66
72
  | sidebar topbar contains only plain text | warning — part 3 context switcher |
67
- | `<admin-scroll>` missing around `<admin-page>` | critical part 9 (the enforcing script `audit-shell-composition.mjs` tiers this critical: without the scroll+page wrapper the content renders flush with the topbar, no margins) **[deprecated 2026-09-01, ADR-0098]** `admin-scroll`/`admin-page` describe the pre-migration shape; the successor is `page-scroll` wrapping `page-ui[band]` |
73
+ | `<page-scroll>` missing around `<page-ui band>` | critical, part 9 (the enforcing script `audit-shell-composition.mjs` tiers this critical: without the scroll+chrome wrapper the content renders flush with the topbar, no margins). **[deleted, ADR-0098 / gh#3745]** `admin-scroll` and the `admin-page` family are both deleted outright, no compat alias; the successor is `page-scroll` wrapping `page-ui[band]` |
68
74
 
69
75
  ## Opt-out contract
70
76
 
@@ -37,6 +37,16 @@ import { chromium } from 'playwright';
37
37
  import { mkdir, writeFile, readFile } from 'node:fs/promises';
38
38
  import { existsSync } from 'node:fs';
39
39
  import { join } from 'node:path';
40
+ // gh#3616: the shared overflow predicate, vendored (not import()able:
41
+ // this script ships inside a published plugin package with no apps/ tree
42
+ // in its files allowlist, see this repo's scripts/build/
43
+ // overflow-detect-vendor.mjs for the derive-and-vendor build that keeps
44
+ // this copy provably identical to apps/genui/app/_shared/overflow-detect.js).
45
+ // Imported as an ordinary function and passed BY REFERENCE into
46
+ // elementHandle.evaluate() below, which stringifies it into the browser's
47
+ // page context, same mechanism scripts/qa/gen-review-decompose.mjs's own
48
+ // (non-vendored, import()-able) copy uses.
49
+ import { OVERFLOW_TAGS, detectOverflow } from './overflow-detect.generated.mjs';
40
50
 
41
51
  // The script lives inside the plugin but runs against the monorepo. All
42
52
  // monorepo paths are resolved from the working directory (the monorepo root),
@@ -259,7 +269,7 @@ for (const group of gallery.groups) {
259
269
  for (const engineKey of Object.keys(prompt.engines ?? {})) {
260
270
  const engineData = prompt.engines[engineKey];
261
271
  if (!engineData || engineData.dryRun) continue;
262
- allPrompts.push({ group: group.slug, prompt: prompt.slug, engineKey, engineData });
272
+ allPrompts.push({ group: group.slug, prompt: prompt.slug, label: prompt.label, engineKey, engineData });
263
273
  }
264
274
  }
265
275
  }
@@ -321,7 +331,7 @@ const results = [];
321
331
  let renderFailureCount = 0;
322
332
  let idx = 0;
323
333
 
324
- for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
334
+ for (const { group, prompt: promptSlug, label, engineKey, engineData } of allPrompts) {
325
335
  idx++;
326
336
  const slug = `${group}-${promptSlug}-${engineKey}`;
327
337
  process.stdout.write(` [${idx}/${allPrompts.length}] ${slug.padEnd(50)} `);
@@ -331,7 +341,10 @@ for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
331
341
  // Each has a .gallery-canvas-wrap containing a canvas-ui.
332
342
  // We identify by matching the group anchor + prompt h3 text.
333
343
 
334
- const promptLabel = promptSlug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
344
+ // gh#3249: use the gallery's real prompt.label (what the h3 actually renders), not a
345
+ // title-cased guess derived from the slug -- the two diverge whenever the authored label
346
+ // isn't the slug's mechanical title-case ("search-filters" vs "Search with Filters").
347
+ const promptLabel = label;
335
348
 
336
349
  // Scroll to the prompt section and wait for canvas settle
337
350
  await page.evaluate(({ groupSlug, label }) => {
@@ -407,41 +420,21 @@ for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
407
420
  // canvas. Runs only when the canvas rendered (renderFailure = false).
408
421
  // Results land in decomposed.json as `overflowElements` — a non-empty array
409
422
  // is treated as P1 in Phase 4 regardless of the Phase 3 structural score.
410
-
411
- const OVERFLOW_TAGS = new Set([
412
- 'text-ui', 'stat-ui', 'badge-ui', 'field-ui', 'button-ui',
413
- 'label', 'span', 'p', 'h1', 'h2', 'h3', 'h4',
414
- ]);
423
+ //
424
+ // gh#3616: the predicate itself (both the TEXT-truncation and LAYOUT-
425
+ // overflow checks, plus every documented exclusion: hscroll boundary,
426
+ // <svg> subtree) now lives in overflow-detect.generated.mjs (vendored
427
+ // from apps/genui/app/_shared/overflow-detect.js), passed straight into
428
+ // elementHandle.evaluate() below rather than re-implemented here, so
429
+ // this plugin sweep and the in-repo QA script/live artifact pane provably
430
+ // run the same code. `canvasWrapEl` is the exact same `.gallery-canvas-
431
+ // wrap` element the screenshot/render-failure check above already
432
+ // resolved, so there's no need to re-locate it by groupSlug/label.
415
433
 
416
434
  let overflowElements = [];
417
435
 
418
436
  if (!renderFailure) {
419
- overflowElements = await page.evaluate(({ groupSlug, label, overflowTags }) => {
420
- const section = document.getElementById(`group-${groupSlug}`);
421
- if (!section) return [];
422
- const h3s = [...section.querySelectorAll('.gallery-prompt-heading')];
423
- const h3 = h3s.find(h => h.textContent.trim().toLowerCase() === label.toLowerCase());
424
- const wrap = h3?.closest('.gallery-prompt')?.querySelector('.gallery-canvas-wrap');
425
- if (!wrap) return [];
426
-
427
- const found = [];
428
- function walk(el) {
429
- const tag = el.tagName?.toLowerCase() ?? '';
430
- if (overflowTags.includes(tag)) {
431
- const style = getComputedStyle(el);
432
- const hasHidden = style.overflow === 'hidden' || style.overflowX === 'hidden';
433
- if (hasHidden && el.scrollWidth > el.clientWidth + 2) {
434
- found.push({ tag, clippedWidth: true });
435
- }
436
- if (hasHidden && el.scrollHeight > el.clientHeight + 2) {
437
- found.push({ tag, clippedHeight: true });
438
- }
439
- }
440
- for (const child of el.children) walk(child);
441
- }
442
- walk(wrap);
443
- return found;
444
- }, { groupSlug: group, label: promptLabel, overflowTags: [...OVERFLOW_TAGS] });
437
+ overflowElements = await canvasWrapEl.evaluate(detectOverflow, OVERFLOW_TAGS);
445
438
  }
446
439
 
447
440
  // ── Primitive lookup + sanitize ──────────────────────────────────────────
@@ -0,0 +1,101 @@
1
+ // GENERATED, do not hand-edit. Source: apps/genui/app/_shared/overflow-detect.js
2
+ // Rebuild: node scripts/build/overflow-detect-vendor.mjs
3
+ // Freshness gate: node scripts/build/overflow-detect-vendor.mjs --verify (npm run check:overflow-detect-vendor-fresh)
4
+ //
5
+ // gh#3616: vendored so the gen-ui-review plugin's own decompose script (a
6
+ // published package with no apps/ tree in its files allowlist) runs the
7
+ // EXACT SAME overflow/clip predicate as the in-repo QA script and the live
8
+ // artifact pane, instead of a hand-kept inline copy that can silently drift
9
+ // (the gap gh#3616 closed, see that ticket for the pre-fix divergence).
10
+
11
+ /**
12
+ * Tags the text-truncation check inspects for their own `overflow:hidden`
13
+ * clipping a2ui-root/canvas-ui render. Every other tag is still walked for
14
+ * the layout-overflow check below, which applies to any tag.
15
+ */
16
+ export const OVERFLOW_TAGS = [
17
+ 'text-ui', 'stat-ui', 'badge-ui', 'field-ui', 'button-ui',
18
+ 'label', 'span', 'p', 'h1', 'h2', 'h3', 'h4',
19
+ ];
20
+
21
+ /**
22
+ * Walks a rendered container and reports overflow/clip findings.
23
+ *
24
+ * Two independent clipping mechanisms (gh#3248):
25
+ * - TEXT truncation: an element's own `overflow:hidden` clips its own
26
+ * scrollWidth/scrollHeight (e.g. a Text node with no line-clamp room).
27
+ * Checked only for `overflowTags` elements.
28
+ * - LAYOUT overflow: an element's whole box lays out past the
29
+ * container's edge, whether or not anything in its ancestor chain has
30
+ * `overflow:hidden` (e.g. two sibling `[stretch]` buttons in a Row
31
+ * each claiming full row width, one landing off-canvas). Checked for
32
+ * every visible tag, horizontal only. Vertical overflow is never
33
+ * flagged since the container is expected to scroll vertically (the
34
+ * gallery's own `.gallery-canvas-wrap` is deliberately
35
+ * `overflow-y: auto`), and a container never scrolls sideways so a
36
+ * right-edge slice is always wrong.
37
+ *
38
+ * Both checks skip content inside a genuine horizontal-scroll boundary
39
+ * (table-ui's own scroll wrapper, swiper-ui, tabs-ui's overflow-x strip):
40
+ * content past the visible edge there is intentional, scrolled-to
41
+ * content, not a layout bug. Also skipped: content inside an `<svg>`
42
+ * subtree (icon-ui's glyph markup, chart-ui's rendered marks), since SVG
43
+ * coordinate-space geometry doesn't map onto DOM layout-box geometry the
44
+ * same way, confirmed via a live false positive on chart-ui's own
45
+ * sparkline. `<foreignObject>` re-enters normal HTML flow content inside
46
+ * that `<svg>` subtree (SVG spec), so the svg-exclusion resets for its
47
+ * own children rather than staying latched: a real HTML element nested
48
+ * there gets checked again.
49
+ *
50
+ * `container` itself is excluded from the horizontal-scroll-boundary test:
51
+ * per the CSS Overflow spec, pairing `overflow-y: auto` with the default
52
+ * `overflow-x: visible` computes overflow-x to `auto` too. Treating that
53
+ * as a real scroll boundary would mark every one of the container's own
54
+ * children as inside-hscroll and silently disable the layout-overflow
55
+ * check for the entire canvas, the opposite of what it exists to catch.
56
+ *
57
+ * @param {Element} container the rendered surface's outer boundary
58
+ * (gen-review-decompose.mjs's `.gallery-canvas-wrap`; the live artifact
59
+ * pane's own equivalent container).
60
+ * @param {string[]} overflowTags see OVERFLOW_TAGS; always pass this
61
+ * explicitly (see the module-doc note on why there's no default value).
62
+ * @returns {Array<{tag: string, clippedWidth?: true, clippedHeight?: true, pushedPastContainer?: true}>}
63
+ */
64
+ export function detectOverflow(container, overflowTags) {
65
+ const found = [];
66
+ const containerRect = container.getBoundingClientRect();
67
+ const TOLERANCE = 2; // matches the scrollWidth/clientWidth +2 slack below
68
+
69
+ function walk(el, insideHScroll, isContainer, insideSvg) {
70
+ const tag = el.tagName?.toLowerCase() ?? '';
71
+ const style = getComputedStyle(el);
72
+
73
+ if (overflowTags.includes(tag)) {
74
+ const hasHidden = style.overflow === 'hidden' || style.overflowX === 'hidden';
75
+ if (hasHidden && el.scrollWidth > el.clientWidth + TOLERANCE) {
76
+ found.push({ tag, clippedWidth: true });
77
+ }
78
+ if (hasHidden && el.scrollHeight > el.clientHeight + TOLERANCE) {
79
+ found.push({ tag, clippedHeight: true });
80
+ }
81
+ }
82
+
83
+ if (!insideHScroll && !insideSvg) {
84
+ const rect = el.getBoundingClientRect();
85
+ if (rect.width > 0 && rect.height > 0) {
86
+ if (rect.right > containerRect.right + TOLERANCE || rect.left < containerRect.left - TOLERANCE) {
87
+ found.push({ tag, pushedPastContainer: true });
88
+ }
89
+ }
90
+ }
91
+
92
+ const isHScrollRoot = !isContainer
93
+ && (style.overflowX === 'auto' || style.overflowX === 'scroll')
94
+ && el.scrollWidth > el.clientWidth + TOLERANCE;
95
+ const nowInsideSvg = tag === 'foreignobject' ? false : (insideSvg || tag === 'svg');
96
+ for (const child of el.children) walk(child, insideHScroll || isHScrollRoot, false, nowInsideSvg);
97
+ }
98
+
99
+ walk(container, false, true, false);
100
+ return found;
101
+ }
@@ -105,6 +105,24 @@ When only source *hashes* move and chunk content does not, `check:embeddings-fre
105
105
 
106
106
  ### 3.1 The full roster — every gate runs; a subset = pre-flight failure
107
107
 
108
+ **Precondition — tsc build for llm, agent, persona (gh#3342).** Gate 4
109
+ (`test:unit:serial`) runs each package's root `*.test.js` files against its
110
+ BUILT output, not its `src/*.ts`: `persona.test.js` imports `./index.js`
111
+ directly, `agent.test.js`'s own docstring says "run against the BUILT
112
+ output ... `npm run build -w @adia-ai/agent` first", and `llm/core` carries
113
+ a dedicated `dist-check.test.js` that asserts the emitted artifacts exist
114
+ and explicitly does not build them itself. All three packages' emitted
115
+ `.js`/`.d.ts` are gitignored, so a fresh cut clone has none of them until
116
+ something builds them — unlike §3.0's regen outputs, this is a plain build
117
+ artifact, not a content-conditional regen, so `release-pack.mjs`'s
118
+ `step3PreFlight()` now runs `npm run build -w @adia-ai/llm -w @adia-ai/agent
119
+ -w @adia-ai/persona` unconditionally, every cut, immediately before gate 4
120
+ (mechanized fix — a manual cut should run the same command first). The
121
+ v0.8.59 cut hit this: §3.0's `npm run build -w @adia-ai/llm` line only
122
+ fires when its source-content trigger list matches, and never named
123
+ agent/persona at all, so a cut with no matching trigger reached gate 4 with
124
+ stale or absent dist and failed on it.
125
+
108
126
  **Execution model (gh#2006): three phases, not one serial walk.** `step3PreFlight()` runs gate 4 solo first (see its own note below), then gates 16 → 27 → 28 strictly in order (the eval-health write-then-read dependency — gate 28 reads whichever `evals/mcp/runs/` directory sorts lexically LAST, so nothing else may write there between 27 and 28), concurrently with a bounded pool running every other gate at once (`PREFLIGHT_CONCURRENCY`, default 4 — override for a dedicated/idle host). Every gate still resolves the same command, still fails the whole pre-flight on a red result, and still reports its own number — only the WALL-CLOCK schedule changed, never the roster below or its numbering. `--dry` previews stay the original flat serial walk unchanged.
109
127
 
110
128
  ```bash
@@ -176,6 +194,11 @@ node scripts/release/assemble-changelog-fragments.mjs # writes, deletes c
176
194
  node scripts/release/assemble-changelog-fragments.mjs --verify # must print PASS afterward
177
195
  ```
178
196
 
197
+ Since gh#3638 a fragment may also be named `changes/gh-<issue>.md` (the form a builder can use
198
+ before a PR number exists). Assembly handles both: an issue-named fragment is credited to the PR
199
+ that merged it, read off that merge commit's own `(#<pr>)` subject, and falls back to the issue
200
+ number when no such commit is found. Nothing changes in this step's commands.
201
+
179
202
  Routing rule (`scripts/release/assemble-changelog-fragments.mjs`'s own header, full detail
180
203
  there): a fragment's first line is `- <kind>: <sentence>` (kind in fix|feature|chore|docs,
181
204
  unchanged from `changelog_fragments.py`'s schema) or, this repo's own addition, `- <kind>
@@ -327,11 +350,20 @@ Drift here means a regen output was left out of the allowlist — stage it and r
327
350
  fails the cut if any tracked file is still modified-in-the-worktree —
328
351
  proof the allowlist covered everything bump.mjs / cut-hygiene touched this
329
352
  cut, not just what §Step 5.5's three named freshness gates happen to check.
330
- This is the fourth time the allowlist itself went stale (gh#1198, gh#1899,
331
- gh#1954, gh#2473 — most recently the factory README's generation-MCP pin
332
- prose), so the guard is generic rather than another named file. A manual
333
- cut should run the equivalent check by hand: `git status --porcelain`
334
- after staging must be empty of `M`/`D` lines.
353
+ The allowlist itself went stale five times (gh#1198, gh#1899, gh#1954,
354
+ gh#2473, gh#3342 — most recently `icons-cdn.js`'s PACKAGE_VERSION pin), so
355
+ this guard is generic rather than another named file it stays the
356
+ fallback for anything below. **The PINNED_REFS-covered subset of the
357
+ allowlist can no longer drift this way at all** (gh#3361): `release-pack.mjs`
358
+ now derives those specific entries straight from `bump.mjs`'s own
359
+ `PINNED_REFS`/`REPO_PINNED_REFS` tables (`pinnedRefFiles()`) instead of
360
+ hand-listing the same paths a second time — a new pinned file in `bump.mjs`
361
+ is automatically a new allowlist entry, no second edit needed. Everything
362
+ NOT PINNED_REFS-covered (roster `package.json`/`CHANGELOG.md`, and the Step
363
+ 4d.5-4d.8 derived catalog/manifest/dist outputs) is still hand-listed and
364
+ still relies on this guard as the safety net. A manual cut should run the
365
+ equivalent check by hand: `git status --porcelain` after staging must be
366
+ empty of `M`/`D` lines.
335
367
 
336
368
  ## §Step 5.7 — Release PR: push the branch, merge, re-baseline
337
369
 
@@ -454,6 +486,18 @@ npm view @adia-ai/web-components dist-tags.latest # must equal X.Y.Z
454
486
 
455
487
  Zero workflows fired after a tag push → [`recovery-paths.md`](recovery-paths.md) §Scenario 7.
456
488
 
489
+ **npm's async staged-publish path (gh#3342):** a large tarball can take up to
490
+ ~25 minutes to become visible on `npm view` after npm accepts it —
491
+ `release-pack.mjs`'s own Step 9 poll accounts for this (`REGISTRY_POLL_MINUTES`,
492
+ default 30, up from the 10-minute window the v0.8.59 cut exceeded with every
493
+ publish run green). A re-dispatch attempted while a package is in that state
494
+ fails its `npm publish` step with `npm error code E409` ("Cannot publish over
495
+ previously staged version") — that is the staged-not-lost signal, never a real
496
+ failure; release-pack's Step 9 detects it (the failing run's own log) and polls
497
+ longer instead of hard-failing. See [`recovery-paths.md`](recovery-paths.md)
498
+ §Scenario 9 for manual recovery, including resuming at Step 10 only
499
+ (`--from-step10`) without re-running the pre-flight or re-tagging.
500
+
457
501
  ## §Step 10 — GH releases + site deploy dispatch
458
502
 
459
503
  ```bash
@@ -97,6 +97,26 @@ For a batch, preserve npm-latest ordering (`--after <prev>`). Verify against the
97
97
 
98
98
  ---
99
99
 
100
+ ## §Scenario 9 — Step 9 registry poll times out with every publish run green (npm E409)
101
+
102
+ **Shape:** `release-pack.mjs --mode handoff` exits 1 at Step 9, but `gh run list --workflow=publish-<pkg>.yml` shows every run `success` and `npm view <scope>/<pkg> version` eventually returns the target version — it just took longer than the poll waited. A large tarball can take up to ~25 minutes to become visible on `npm view` after npm accepts it (asynchronous staged publish); the v0.8.59 cut's poll window was 10 minutes.
103
+
104
+ **A re-dispatch made while a package is in this state fails its own `npm publish` step with `npm error code E409` / "Cannot publish over previously staged version".** That error is not a real failure — npm rejected the duplicate publish precisely because the real one already landed server-side. It is the staged-not-lost signal, never grounds to re-dispatch a third time.
105
+
106
+ **Resolution:**
107
+
108
+ - `release-pack.mjs`'s Step 9 (gh#3342) already extends its own poll to `REGISTRY_POLL_MINUTES` (default 30, override via env) and, for any package still stale after that, checks its latest `publish-<pkg>.yml` run for the E409 signature before deciding — an E409-confirmed package gets one more extended, isolated poll instead of an immediate hard-fail. Nothing to do by hand in that case; let it finish.
109
+ - If Step 9 already exited 1 and you've confirmed by hand (registry + `gh run list`) that every package is actually published, don't re-run the full handoff — it would re-run the ~15min pre-flight (Step 3) and re-tag at HEAD (Step 6, wrong if anything merged since the original tag). Resume from Step 10 only:
110
+
111
+ ```bash
112
+ node "<plugin-root>/skills/package-release/scripts/release-pack.mjs" \
113
+ --mode handoff --version X.Y.Z --date YYYY-MM-DD --previous-version X.Y.Z-1 \
114
+ --gh-notes-file <path> --from-step10
115
+ ```
116
+
117
+ `--from-step10` skips Steps 1/3/4/5/6/7/8/9 entirely and runs only Step 10 (GH releases + site deploy) — the exact shape a hand-rolled one-off script (`/tmp/cut-0859-step10.sh`, never checked in) worked around live on the v0.8.59 cut. Before jumping, it hard-verifies the umbrella tag AND every per-package tag (`vX.Y.Z`, `<pkg>-vX.Y.Z`) already exist on origin and resolve to HEAD — without that check, `gh release create` on a tag that was never actually made would silently mint a NEW lightweight tag at whatever HEAD happens to be, attaching the release notes to the wrong commit. A missing or mismatched tag refuses the resume outright, naming which tag and why (either Step 8 never pushed it, or HEAD moved since); re-tag/push for real, or checkout the tagged commit, before retrying.
118
+ - **Never** re-dispatch a package a third time once it shows E409 — a third attempt only 409s again. Wait for the registry; it always converges.
119
+
100
120
  ## §Decision flowchart
101
121
 
102
122
  ```text
@@ -22,6 +22,7 @@
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import process from 'node:process';
25
+ import { fileURLToPath } from 'node:url';
25
26
  import { PACKAGE_ROSTER } from './package-paths.mjs';
26
27
 
27
28
  // Single-sourced roster (H3, package-paths.mjs). Each plugin also carries a
@@ -60,7 +61,14 @@ const SIBLING_MANIFESTS = Object.fromEntries(
60
61
  // `gen-ui-mcp`) was narrowed back by the shim-deletion follow-up PR, matching
61
62
  // lockstep-checks.mjs's MCP_PIN_RE — a retired name in the pin must now fail
62
63
  // loudly instead of being bumped along.
63
- const A2UI_MCP_PIN = {
64
+ // gh#3361: exported so release-pack.mjs's Step 5 can derive its
65
+ // PINNED_REFS-covered allowlist entries from this table directly instead of
66
+ // hand-duplicating the file list — the drift class this table itself was
67
+ // born to fix (gh#1198/1899/1954/2473/3342, all "bump.mjs moved a pin,
68
+ // release-pack.mjs's own list didn't know about it") can no longer recur
69
+ // for anything PINNED_REFS already covers: a new entry here is now a new
70
+ // entry there, structurally, not two edits.
71
+ export const A2UI_MCP_PIN = {
64
72
  label: 'generation-MCP pin (@adia-ai/mcp)',
65
73
  // `@adia-ai/mcp@<from>` (not a prefix of a longer version) → `@<to>`
66
74
  // Escape EVERY regex metacharacter in `from`, not just dots: a prerelease or
@@ -71,7 +79,7 @@ const A2UI_MCP_PIN = {
71
79
  new RegExp(`(@adia-ai/mcp@)${from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\d.])`),
72
80
  replace: (to) => `$1${to}`,
73
81
  };
74
- const PINNED_REFS = {
82
+ export const PINNED_REFS = {
75
83
  'packages/plugins/adia-ui-factory': [
76
84
  { ...A2UI_MCP_PIN, file: '.mcp.json' },
77
85
  { ...A2UI_MCP_PIN, file: 'README.md', label: 'generation-MCP pin (README prose)' },
@@ -113,7 +121,7 @@ const PINNED_REFS = {
113
121
  // once and the next cut fails the gate — so the bump moves it, like the
114
122
  // .mcp.json pin (H2 follow-through, 2026-07-19: found 0.7.13 claimed while
115
123
  // 0.8.7 was live — a full minor of README drift shipped to npm).
116
- const REPO_PINNED_REFS = [
124
+ export const REPO_PINNED_REFS = [
117
125
  {
118
126
  file: 'README.md',
119
127
  label: '"Current version" claim',
@@ -407,6 +415,28 @@ function selftest() {
407
415
  console.log('selftest OK');
408
416
  }
409
417
 
410
- const topArgv = process.argv.slice(2);
411
- if (topArgv[0] === 'selftest') selftest();
412
- else main();
418
+ // gh#3361: this module is now imported for its PINNED_REFS/REPO_PINNED_REFS
419
+ // exports (release-pack.mjs's Step 5), not only run as a CLI — a bare
420
+ // `topArgv[0] === 'selftest'` / `else main()` at module scope would fire
421
+ // `main()` (which calls `parseArgs`, which prints "error: --from/--to
422
+ // required" and `process.exit(2)`s) the moment ANYTHING imports this file,
423
+ // killing the importer's own process. Realpath-safe (a naive
424
+ // `argv[1] === fileURLToPath(import.meta.url)` reads false through a
425
+ // symlink, same pitfall scripts/lib/is-entry-point.mjs documents) so this
426
+ // stays correct if bump.mjs is ever ELM'd behind one; inlined rather than
427
+ // importing that top-level helper, keeping this skill's own scripts/
428
+ // self-sufficient.
429
+ function isEntryPoint() {
430
+ if (!process.argv[1]) return false;
431
+ try {
432
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
433
+ } catch {
434
+ return false;
435
+ }
436
+ }
437
+
438
+ if (isEntryPoint()) {
439
+ const topArgv = process.argv.slice(2);
440
+ if (topArgv[0] === 'selftest') selftest();
441
+ else main();
442
+ }
@@ -20,6 +20,7 @@
20
20
  import { execFileSync } from 'node:child_process';
21
21
  import process from 'node:process';
22
22
  import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
23
+ import { warnIfStaleCopy } from './stale-copy-warning.mjs';
23
24
  import { PACKAGE_ROSTER } from './package-paths.mjs';
24
25
 
25
26
  // Instance data — fork-configurable. The npm scope the lockstep roster publishes under.
@@ -288,6 +289,13 @@ async function main() {
288
289
  const args = parseArgs(process.argv.slice(2));
289
290
  // Fail-fast guard: refuse to run npm/gh against a non-monorepo directory.
290
291
  assertMonorepoRoot(process.cwd());
292
+ // Stale-plugin-cache self-check (ticket #3943 item 2), same primitive
293
+ // release-pack.mjs's own top-level check now calls.
294
+ warnIfStaleCopy(
295
+ import.meta.url,
296
+ process.cwd(),
297
+ 'packages/plugins/adia-ui-forge/skills/package-release/scripts/dispatch-publish.mjs',
298
+ );
291
299
  if (args.after) checkAfter(args.after, args.scope);
292
300
 
293
301
  // Recovery mode — verify each publish workflow SUCCEEDED for its pushed tag
@@ -103,15 +103,15 @@ export const GATE_ROSTER = [
103
103
  // on the handoff re-run, since the promoted heading was already on main).
104
104
  { n: 26, cmd: 'node scripts/release/check-dx-sweep-freshness.mjs', targetVersionArg: true, what: 'DX sweep fresh + not regression-RED' },
105
105
  // gh#1135 (WS-4 SPEC REQ-06): a fresh free-form eval run feeds the health
106
- // record gate 28 writes without it, "latest" under evals/mcp/runs/ could
106
+ // record gate 28 writes: without it, "latest" under packages/gen-ui/engine/evals/mcp/runs/ could
107
107
  // be a stale leftover from an earlier ad-hoc run, not this cut's own state.
108
- { n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (evals/health input)' },
109
- // REQ-06: commits evals/health/<version>.json the version this record
108
+ { n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (packages/gen-ui/engine/evals/health input)' },
109
+ // REQ-06: commits packages/gen-ui/engine/evals/health/<version>.json; the version this record
110
110
  // names is the CUT's target version from the start, never the
111
111
  // pre/post-bump `hygieneVersion` gate 24 needs (a claim-validation
112
112
  // concern this is not) — hence `targetVersionArg` rather than
113
113
  // `versionArg`, a distinct flag release-pack.mjs's step3PreFlight reads.
114
- { n: 28, cmd: 'node scripts/release/write-eval-health.mjs', what: 'evals/health/<version>.json committed, AC-01/AC-02 run for real', targetVersionArg: true },
114
+ { n: 28, cmd: 'node scripts/release/write-eval-health.mjs', what: 'packages/gen-ui/engine/evals/health/<version>.json committed, AC-01/AC-02 run for real', targetVersionArg: true },
115
115
  // ADR-0048 (gh#1192) estate-split latch. Between P1 (mint @adia-ai/gen-ui)
116
116
  // and P5 (shims + roster) the tree is correct in-repo but deliberately
117
117
  // NOT publishable: the old package dirs are `private: true` stubs the