@adia-ai/adia-ui-forge 0.8.59 → 0.8.60
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -0
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/references/contracts/a2ui-mcp-surface.md +1 -1
- package/scripts/lint-rules.generated.mjs +2079 -346
- package/skills/a2ui-maintenance/references/pipeline-overview.md +24 -0
- package/skills/gen-ui-review/scripts/gen-review-decompose.mjs +27 -34
- package/skills/gen-ui-review/scripts/overflow-detect.generated.mjs +101 -0
- package/skills/package-release/references/cut-procedure.md +49 -5
- package/skills/package-release/references/recovery-paths.md +20 -0
- package/skills/package-release/scripts/bump.mjs +36 -6
- package/skills/package-release/scripts/release-pack.mjs +424 -45
- package/skills/primitive-authoring/references/yaml-contract.md +106 -9
|
@@ -193,6 +193,30 @@ 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` (10000). `auto` inherits
|
|
206
|
+
through the free-form picker call the moment it escalates that far -
|
|
207
|
+
`monolithic-thinking` itself is never reachable via `auto`'s own
|
|
208
|
+
escalation ladder. Every other LLM call site (`generate-pro.js`'s four
|
|
209
|
+
branches, zettel's locator/modifier/synthesizer, the shared
|
|
210
|
+
`validate-and-repair.js` repair loop) stays thinking-off deliberately,
|
|
211
|
+
a narrow first pass pending real eval numbers - do not assume a new
|
|
212
|
+
engine inherits thinking by proximity to one that has it.
|
|
213
|
+
`StubLLMAdapter` accepts and records `thinking`/`thinkingBudget` on
|
|
214
|
+
its own `calls` log for strategy-level test assertions.
|
|
215
|
+
`eval-diff.mjs` gained `--mode` (`instant`/`pro`/`thinking`), scoped
|
|
216
|
+
to `--engine mcp` only, so `--engine mcp --mode thinking` exercises
|
|
217
|
+
the opted-in path independently of the router's own default. See
|
|
218
|
+
`docs/ops/lld/lld-0033-strategy-thinking-opt-in.md` for the full
|
|
219
|
+
decision record and the conductor's D1-D5 rulings.
|
|
196
220
|
|
|
197
221
|
## Test + run commands (all verified in root package.json)
|
|
198
222
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
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
|
-
|
|
331
|
-
gh#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
+
}
|