@adia-ai/adia-ui-forge 0.8.34 → 0.8.35

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 (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +12 -4
  4. package/agents/a2ui-builder.md +12 -8
  5. package/agents/component-author.md +17 -12
  6. package/agents/framework-reviewer.md +18 -12
  7. package/agents/release-builder.md +15 -10
  8. package/commands/deploy.md +2 -0
  9. package/commands/dogfood.md +2 -0
  10. package/commands/gen-review.md +2 -0
  11. package/commands/release.md +3 -1
  12. package/package.json +4 -1
  13. package/scripts/forge-lint +38 -1
  14. package/scripts/release-pretag-docs-gate +45 -3
  15. package/skills/adia-a2ui/SKILL.md +2 -2
  16. package/skills/adia-author/SKILL.md +1 -1
  17. package/skills/adia-author/references/anti-patterns.md +1 -0
  18. package/skills/adia-author/references/code-style.md +3 -3
  19. package/skills/adia-author/references/worked-example.md +3 -3
  20. package/skills/adia-author/references/yaml-contract.md +42 -0
  21. package/skills/adia-deploy/SKILL.md +10 -11
  22. package/skills/adia-dogfood/SKILL.md +5 -4
  23. package/skills/adia-dogfood/references/admin-shell-anatomy.md +5 -2
  24. package/skills/adia-dogfood/references/app-shell-pitfalls.md +10 -3
  25. package/skills/adia-dogfood/scripts/analyze.mjs +2 -0
  26. package/skills/adia-gen-review/SKILL.md +10 -3
  27. package/skills/adia-gen-review/references/loop-protocol.md +15 -8
  28. package/skills/adia-gen-review/references/rubric-score.md +5 -2
  29. package/skills/adia-gen-review/scripts/gen-review-status.mjs +8 -6
  30. package/skills/adia-llm-internals/SKILL.md +1 -1
  31. package/skills/adia-release/SKILL.md +2 -2
  32. package/skills/adia-release/references/cut-procedure.md +4 -0
  33. package/skills/adia-release/references/gates-catalog.md +3 -1
  34. package/skills/adia-release/references/independent-package-release.md +2 -2
  35. package/skills/adia-release/scripts/bump.mjs +7 -3
  36. package/skills/adia-release/scripts/dispatch-publish.mjs +5 -2
  37. package/skills/adia-release/scripts/gate-roster.mjs +29 -0
  38. package/skills/adia-release/scripts/package-paths.mjs +41 -13
  39. package/skills/adia-release/scripts/pr-bridge.mjs +36 -2
  40. package/skills/adia-release/scripts/promote-unreleased.mjs +27 -7
  41. package/skills/adia-release/scripts/release-pack.mjs +24 -11
  42. package/skills/adia-release/scripts/tag-lockstep.mjs +10 -8
  43. package/skills/adia-site-docs/SKILL.md +8 -6
  44. package/skills/adia-site-docs/intent.md +1 -1
  45. package/skills/adia-ssr/SKILL.md +1 -1
  46. package/skills/adia-ssr/references/failure-shapes.md +8 -5
  47. package/skills/adia-ssr/references/guard-patterns.md +1 -1
  48. package/skills/adia-site-docs/evals/audit-report.md +0 -30
  49. package/skills/adia-ssr/evals/audit-report.md +0 -63
@@ -3,8 +3,11 @@
3
3
  Script: `scripts/dev/audit-shell-composition.mjs` (repo-local), npm gates
4
4
  `audit:shell-composition{,:strict,:all}`. Static AST walk over
5
5
  `apps/**/*.html`, `playgrounds/**/*.html`, `catalog/page-shells/**/*.html` —
6
- no browser needed; pre-commit fast. The script mechanically checks a subset
7
- (the findings table below); this 13-part list is the full review standard.
6
+ no browser needed; pre-commit fast. The script's own `PARTS` array is the
7
+ mechanical census it has grown past this list (count it in
8
+ `scripts/dev/audit-shell-composition.mjs`, never from a hand-typed number
9
+ here); this numbered list is the human review standard, and the findings
10
+ table below samples common symptoms rather than the script's full roster.
8
11
  Canonical source: the monorepo's `site/index.html` — the markup that renders
9
12
  the live admin-dashboard example.
10
13
 
@@ -20,10 +20,17 @@ may stall — wait 30s, re-run.
20
20
  | 4 | Top-level `await` without async setup wrap | `[setup-failed]` console error | `export default async function setup(host) { … }` |
21
21
  | 5 | Vite import-analysis 500 on dynamic import | `[network-4xx] 500` for `./<name>.contents.js` | add `/* @vite-ignore */` to the dynamic import |
22
22
  | 6 | icon-ui not imported despite `<icon-ui>` / `[icon=…]` / icon-rendering composites | `[icon-ui-missing]` | `import "/packages/web-components/components/icon/icon.js"` |
23
+ | 7 | `<admin-page-body>` emitted without its `<admin-page>` ancestor (gh#981) | *(no audit-app-shells.mjs tag — apps/-only script, doesn't sweep this surface)* | wrap in `<admin-page>` — `admin-page > admin-page-body { flex:1; … }` (`admin-shell.bespoke.css:144`) is a direct-child selector; without that literal parent, `admin-page-body` falls back to UA `display:inline` |
23
24
 
24
- Secondary signals: `[collapsed-element]` (registered but 0px tall),
25
- `[network-4xx]` (typoed stylesheet href, missing contents.html, stale
26
- `import.meta.url`).
25
+ Secondary signals: `[collapsed-element]` (registered but 0px tall
26
+ `audit-app-shells.mjs`'s own threshold is <4px, so a shallower-but-still-broken
27
+ collapse won't trip it), `[network-4xx]` (typoed stylesheet href, missing
28
+ contents.html, stale `import.meta.url`). Row 7's specific case (gh#981, in
29
+ `site/site.js`'s router, not swept by this script at all): the 150px isn't
30
+ `admin-page-body`'s own height — it's a *replaced child* (an `<iframe>`)
31
+ whose `height:100%` can't resolve, so it falls back to the browser's
32
+ intrinsic default (300×150). The observable is a body rendering ~150px tall
33
+ with no console error, regardless of real content height.
27
34
 
28
35
  ## Composite → internal primitives (undiscoverable from the markup)
29
36
 
@@ -571,6 +571,8 @@ function renderReport(results, base) {
571
571
  lines.push('4. **Synonym-attr / synonym-slot drift** — markers documented in `.claude/docs/conventions/attribute-api-migration.md`.');
572
572
  lines.push('5. **Alert flex-row** — `alert-ui` with multiple bare `<text-ui>` children (need `<col-ui slot="content">` wrap).');
573
573
  lines.push('6. **Console** — every `console.error` and `console.warn` during page load + 800ms settling.');
574
+ lines.push('7. **Missing component CSS** — a rendered `*-ui` element with no resolved `--{prefix}-*` custom property, meaning its per-component stylesheet never loaded (controls render unstyled).');
575
+ lines.push('8. **Unstyled popover** — an OPEN `[popover]` with transparent background AND zero padding (transparent-host components like `feed-ui` allowlisted).');
574
576
  lines.push('');
575
577
 
576
578
  return lines.join('\n');
@@ -35,8 +35,15 @@ directives are findings.
35
35
  3. Zero P1 cosmetic findings (overflow entries auto-count as P1) — checked
36
36
  mechanically.
37
37
  4. Zero RENDER_FAILURE prompts; cycle `status: COMPLETE` — checked
38
- mechanically (plus the human-QA block, ≥4/5).
39
- 5. Human QA: 5 sampled prompts, `passCount 4`.
38
+ mechanically.
39
+ 5. **Human QA is per-sweep, not per-cycle** (spec-factory-dx-ws6-measurement.md
40
+ REQ-11, gh#1137 — retired from this exit gate 2026-08-12; the per-cycle
41
+ `humanQA` ledger field never held: null in 12 of 13 rows, and the one
42
+ populated row still said "gate pending"). The 5-sampled-prompts,
43
+ `passCount ≥ 4` bar is unchanged, but it now feeds `qa/dx/`'s R1 sweep
44
+ record (D5's induced-failure sample + D8's gallery term) instead of
45
+ blocking a single cycle's own exit condition. `gen-review-status.mjs
46
+ --check-exit` no longer reads `humanQA` off the ledger row.
40
47
 
41
48
  ## The Gen-Review Scorecard — the per-prompt output contract
42
49
 
@@ -182,7 +189,7 @@ map + FAILS/WORKS transpilation pairs:
182
189
  | New primitive / native tag in canvas | `TAG_TO_COMPONENT` in the decompose script (`Native*` prefix for natives); coverage-audit must go green |
183
190
  | New root-cause code | rubric-score.md table + `scores.schema.json` enum + `schemaVersion` bump |
184
191
  | New safe attribute | `ATTR_ALLOWLIST` — static enums only, never freeform-text attrs |
185
- | Threshold recalibration | rubric-score.md §Thresholds (pass-flip sweep) + the exit gate above |
192
+ | Threshold recalibration | rubric-score.md §Thresholds (pass-flip sweep) + the exit gate above + `EXCELLENCE_THRESHOLD` at `scripts/gen-review-status.mjs:31` — the only mechanical enforcer; skip it and `--check-exit` silently keeps the old floor |
186
193
 
187
194
  ## Posture
188
195
 
@@ -161,12 +161,16 @@ never get fix plans. Reads ONLY the decomposed file.
161
161
  skill does not perform the edits.
162
162
  3. **Regenerate**: `npm run gallery:generate`; confirm 0 console
163
163
  errors/warnings in the canvas output.
164
- 4. **Human QA gaterequired for COMPLETE.** Operator reviews 5 random
165
- PASSING prompts against: (a) serves the user's task? (b) right primary
166
- primitive? (c) would ship unchanged? Record
167
- `humanQA.{sampledPrompts,passCount,failCount}`. `failCount 2` cycle
168
- stays `OPEN`: the thresholds are miscalibrated recalibrate
169
- rubric-score.md §Thresholds against the human judgments.
164
+ 4. **Human QA sampleper-sweep, not per-cycle** (retired from this
165
+ cycle's own COMPLETE gate 2026-08-12, spec-factory-dx-ws6-measurement.md
166
+ REQ-11, gh#1137). Operator reviews 5 random PASSING prompts against:
167
+ (a) serves the user's task? (b) right primary primitive? (c) would ship
168
+ unchanged? Record the result in the next `qa/dx/` sweep record's
169
+ `humanQA.{sampledPrompts,pass,fail}` (`node scripts/qa/dx-status.mjs`),
170
+ not on this cycle's ledger row. `failCount ≥ 2` still means the
171
+ thresholds are miscalibrated — recalibrate rubric-score.md §Thresholds
172
+ against the human judgments — it just no longer blocks THIS cycle's own
173
+ `status: COMPLETE`.
170
174
  5. **Schema gate** (must exit 0 before touching the ledger):
171
175
 
172
176
  ```bash
@@ -175,8 +179,11 @@ never get fix plans. Reads ONLY the decomposed file.
175
179
 
176
180
  6. **Update ledger** (`review/cycle-ledger.json`): `cycleNumber`,
177
181
  `completedAt`, `engine`, `status`, `aggregate`
178
- (passingCount/failingCount/renderFailureCount/meanScore/Δ), `humanQA`.
179
- Remove the `cycle-{N}.lock` sentinel.
182
+ (passingCount/failingCount/renderFailureCount/meanScore/Δ). `humanQA` is
183
+ RETIRED from this per-cycle row (step 4) — do not populate it here; a
184
+ stray value is harmless (ignored) but the field's home is now the
185
+ per-sweep `qa/dx/sweeps/*.json` record. Remove the `cycle-{N}.lock`
186
+ sentinel.
180
187
  7. **Exit condition**:
181
188
 
182
189
  ```bash
@@ -22,8 +22,11 @@ Max score 105 (D1–D5 at 20 each = 100; D6 mechanical = 0 or +5).
22
22
  **Recalibrating** (when human QA contradicts mechanical scores across 2+
23
23
  cycles): sweep candidate threshold values and read the pass-flips at each step
24
24
  against the human verdicts — never pick a round number by feel. A threshold
25
- change must also update the exit gate in SKILL.md and the schema comment, and
26
- new cause codes / dimensions bump `schemaVersion` in
25
+ change must also update the exit gate in SKILL.md AND the
26
+ `EXCELLENCE_THRESHOLD` constant at `scripts/gen-review-status.mjs:31` that
27
+ constant is the only mechanical enforcer (`--check-exit` reads nothing else;
28
+ the schema carries no threshold), so missing it leaves the old floor silently
29
+ enforced. New cause codes / dimensions bump `schemaVersion` in
27
30
  [scores.schema.json](scores.schema.json).
28
31
 
29
32
  ---
@@ -17,9 +17,14 @@
17
17
  *
18
18
  * Exit-condition (from scores.schema.json + SKILL.md §ExitCondition):
19
19
  * - Every prompt PASSING (p1Count = 0 AND rubricScore.score ≥ 92)
20
- * - humanQA.passCount ≥ 4 (of 5 sampled)
21
20
  * - No RENDER_FAILURE prompts
22
21
  * - Cycle status = COMPLETE (not OPEN or INTERRUPTED)
22
+ *
23
+ * humanQA is RETIRED from this per-cycle exit condition (spec-factory-dx-
24
+ * ws6-measurement.md REQ-11, gh#1137, 2026-08-12): the per-cycle ledger
25
+ * field never held (null in 12/13 rows), so it moved to a per-sweep
26
+ * obligation — `qa/dx/sweeps/*.json`'s `humanQA` field (dx-status.mjs)
27
+ * reads a 5-prompt sample per sweep instead of per cycle.
23
28
  */
24
29
 
25
30
  import { readFileSync, existsSync } from 'node:fs';
@@ -85,11 +90,8 @@ function checkExitCondition() {
85
90
  reasons.push(`Cycle ${latest.cycleNumber} status is "${latest.status}" (need COMPLETE)`);
86
91
  }
87
92
 
88
- if (!latest.humanQA) {
89
- reasons.push('Human QA gate not completed (humanQA block missing)');
90
- } else if (latest.humanQA.passCount < 4) {
91
- reasons.push(`Human QA pass count ${latest.humanQA.passCount}/5 (need ≥ 4)`);
92
- }
93
+ // humanQA is RETIRED from this per-cycle check (REQ-11, gh#1137) — it's a
94
+ // per-sweep obligation now (qa/dx/sweeps/*.json), not a per-cycle gate.
93
95
 
94
96
  const agg = latest.aggregate;
95
97
  if (agg?.failingCount > 0) {
@@ -25,7 +25,7 @@ Consumers depend on: the `StreamChunk` union, `ChatResult` (`text` / `usage` / `
25
25
 
26
26
  Three invariants override any cleanup instinct:
27
27
 
28
- 1. **NEVER normalize `stopReason`.** Providers emit `end` / `stop` / `max_tokens` / `length` / `MAX_TOKENS` / `tool_use`; the downstream truncation detector reads the raw value. Collapsing to `end` hides truncation — a defect, not a cleanup.
28
+ 1. **Never collapse `stopReason` truncation values — and never invent a NEW normalization.** The one sanctioned mapping is OpenAI's own `finish_reason === 'stop'` → `end` (`openai.ts` `parseResponse`; adapter-contract.md §stopReason documents it as correct); everything else propagates raw. Providers emit `end` / `stop` / `max_tokens` / `length` / `MAX_TOKENS` / `tool_use`; the downstream truncation detector reads the raw value, so collapsing `max_tokens`/`length`/`MAX_TOKENS` to `end` hides truncation — a defect, not a cleanup.
29
29
  2. **`buildRequest()` is the single source of upstream shape** for direct AND passthrough-proxy mode; the dispatcher swaps only the URL. Never fork it per proxy flavor.
30
30
  3. **No real API key reaches the browser on a production host.** The same-origin passthrough proxy injects the key server-side; the sentinel-key + one-shot-warning path in `createAdapter()` must survive any refactor.
31
31
 
@@ -93,9 +93,9 @@ assumed pass, or "published" without the actual `npm view` output.
93
93
 
94
94
  ## Mechanization (bundled `scripts/`, stdlib Node, all support `--dry`)
95
95
 
96
- `release-pack.mjs` walks the cycle in two phases per invariant 3 — `--mode cut` / `--mode from-scratch` promote `[Unreleased]` (BOTH modes now — a peer's hand-authored section must be promoted whichever mode cut it, and a loud guard blocks the bump if any roster package still carries non-empty `[Unreleased]` content after promotion), run Step 4f (pre-tag coverage `--fix` with the authoritative F-N1 matcher), and stop at the release commit (PR → merge); `--mode handoff` tags/publishes/deploys from post-merge main. **`--go` is the standard invocation for an operator-initiated release** — the single authorization covers the cycle; evidence blocks print as a log; granular `--yes`/`--push`/`--publish` remain for cautious manual runs (`--push` still refuses to auto-fire past any F-N1 finding). `--mode batch` is not implemented — it hard-rejects with a pointer to [`recovery-paths.md`](references/recovery-paths.md) §Scenario 2 rather than silently misbehaving; a real batch tags each version at its own release-merge SHA, which this single-version orchestrator has no model for. Pre-flight (Step 3) runs the full 24-gate roster from the single shared list in `scripts/gate-roster.mjs` — the same list [`cut-procedure.md`](references/cut-procedure.md) §3.1 transcribes, so a subset run requires editing that one file, not two.
96
+ `release-pack.mjs` walks the cycle in two phases per invariant 3 — `--mode cut` / `--mode from-scratch` promote `[Unreleased]` (BOTH modes now — a peer's hand-authored section must be promoted whichever mode cut it, and a loud guard blocks the bump if any roster package still carries non-empty `[Unreleased]` content after promotion), run Step 4f (pre-tag coverage `--fix` with the authoritative F-N1 matcher), and stop at the release commit (PR → merge); `--mode handoff` tags/publishes/deploys from post-merge main. **`--go` is the standard invocation for an operator-initiated release** — the single authorization covers the cycle; evidence blocks print as a log; granular `--yes`/`--push`/`--publish` remain for cautious manual runs (`--push` still refuses to auto-fire past any F-N1 finding). `--mode batch` is not implemented — it hard-rejects with a pointer to [`recovery-paths.md`](references/recovery-paths.md) §Scenario 2 rather than silently misbehaving; a real batch tags each version at its own release-merge SHA, which this single-version orchestrator has no model for. Pre-flight (Step 3) runs the full 28-gate roster from the single shared list in `scripts/gate-roster.mjs` — the same list [`cut-procedure.md`](references/cut-procedure.md) §3.1 transcribes, so a subset run requires editing that one file, not two.
97
97
 
98
- The pieces are callable alone: `bump.mjs` (version fields + the factory `.mcp.json` a2ui-mcp pin, invariant 8), `promote-unreleased.mjs` / `insert-stub.mjs` (CHANGELOG blocks), `tag-lockstep.mjs` (12 tags, `--delete` to move), `dispatch-publish.mjs` (publish workflows; `--verify-triggered` re-dispatches missing AND dead — cancelled/failed/timed-out — runs, each gated behind an npm-registry check so an already-published version is never re-dispatched (gh#763); `--after` enforces batch ordering — dispatch output states what WAS done, never a to-do phrased as if it hadn't fired), and `pr-bridge.mjs` (the two-phase bridge: pushes the release branch, opens the PR, waits out CI, and merges ONLY on all-non-fail-soft-checks-green + zero unresolved threads + no changes-requested; any other state stops with evidence — §5.7 mechanized, `--dry` to preview the decision). All fail fast outside a monorepo root (`assert-monorepo-root.mjs`); fork-configurable via `--host` / `--scope`. All 6 CLI scripts (plus `scripts/release/check-release.mjs`) have a `selftest` mode wired into CI's Plugin bin selftests step; the 3 helper modules (`assert-monorepo-root`, `gate-roster`, `package-paths`) are exercised through the CLIs' selftests, not their own.
98
+ The pieces are callable alone: `bump.mjs` (version fields + the factory `.mcp.json` a2ui-mcp pin, invariant 8), `promote-unreleased.mjs` / `insert-stub.mjs` (CHANGELOG blocks), `tag-lockstep.mjs` (umbrella + one tag per roster package — 14 today, `--delete` to move), `dispatch-publish.mjs` (publish workflows; `--verify-triggered` re-dispatches missing AND dead — cancelled/failed/timed-out — runs, each gated behind an npm-registry check so an already-published version is never re-dispatched (gh#763); `--after` enforces batch ordering — dispatch output states what WAS done, never a to-do phrased as if it hadn't fired), and `pr-bridge.mjs` (the two-phase bridge: pushes the release branch, opens the PR, waits out CI, and merges ONLY on all-non-fail-soft-checks-green + zero unresolved threads + no changes-requested; any other state stops with evidence — §5.7 mechanized, `--dry` to preview the decision). All fail fast outside a monorepo root (`assert-monorepo-root.mjs`); fork-configurable via `--host` / `--scope`. All 6 CLI scripts (plus `scripts/release/check-release.mjs`) have a `selftest` mode wired into CI's Plugin bin selftests step; the 3 helper modules (`assert-monorepo-root`, `gate-roster`, `package-paths`) are exercised through the CLIs' selftests, not their own.
99
99
 
100
100
  Site deploy (Step 10 of the handoff) dispatches `deploy-site.yml` (its own pre-flight verify → snapshot → hardened rsync → post-deploy verify → auto-rollback), never a raw local `rsync` — H1 (forge-campaign gh#268 audit): the script had drifted into bypassing that pipeline even though this doc already taught the workflow-dispatch path. The dispatch rides the cycle's single authorization (`--go`, or `--publish` on a manual run); the workflow's own GitHub environment gate is the human stop for the deploy itself.
101
101
 
@@ -113,6 +113,10 @@ npm run check:token-semantics-sync # 21 adia-tokens generated refs v
113
113
  npm run check:demo-routes # 22 demo surfaces routed + patterns indexed
114
114
  npm run check:brand-assets # 23 brand mark token-driven, not baked raster
115
115
  node scripts/release/check-cut-hygiene.mjs --version <mode-dependent> # 24 README CDN-pin + version currency — CUT modes pre-flight at the PREVIOUS version (the README claim is exact-match and only moves at the Step-4 bump; Step 4g re-proves at the cut version), HANDOFF pre-flights at the CUT version (post-merge, the claim already moved)
116
+ python3 packages/plugins/adia-ui-factory/scripts/adia-scaffold selftest # 25 scaffold specifiers resolve in packed @adia-ai/web-components + @adia-ai/web-modules tarballs (gh#1132)
117
+ node scripts/release/check-dx-sweep-freshness.mjs # 26 DX sweep fresh + not regression-RED (gh#1137 REQ-05 — FLAGGED, needs adia-release confirmation; see the script's own header)
118
+ npm run eval:diff -- --engine free-form # 27 fresh free-form eval run (evals/health input, gh#1135)
119
+ 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)
116
120
  ```
117
121
 
118
122
  Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source, re-run the narrowest gate, then re-run the full sequence. The canonical miss: a cut that ran 7 of the gates shipped a stale-embeddings defect that surfaced a day later and cost a tag-move recovery.
@@ -153,6 +153,8 @@ The two `audit:*` probes are soft-fail pre-cut (exit-1 only on criticals); findi
153
153
  - **What:** the vitest suite (~1000+ tests).
154
154
  - **Stale-test failure:** the test asserts a behavior a peer deliberately changed (CHANGELOG-documented) without updating the assertion. Tell it apart by reading assertion vs CHANGELOG vs source: if the code matches the CHANGELOG's described behavior, the test is stale — update the assertion (see [`recovery-paths.md`](recovery-paths.md) §Scenario 5). Real regression → fix the regression.
155
155
  - **Parallel-contention flake:** heavy web-modules composite suites can fail under full parallelism on a loaded machine (`signals: drain loop exceeded 100 iterations`). Load-dependent, not a code defect. Sequential is the source of truth, and **the roster gate now runs it directly** (`npm run test:unit:serial` = `vitest run --no-file-parallelism`, gate 4 since 2026-07-19) — a parallel `test:unit` flake outside the roster still isn't a blocker, and don't raise the drain guard.
156
+ - **Exit 143 (SIGTERM):** the vitest process was KILLED (resource pressure / a stray terminator), not a test failure — re-run the gate directly (`npx vitest run --no-file-parallelism`) before diagnosing anything; a clean re-run means transient, proceed (v0.8.34 handoff hit this).
157
+ - **First-time-at-cut failures are REAL (v0.8.34):** this serial suite runs tests PR CI never does — `exit-gate.corpus` (dialect-catalog conformance over real harvested chunks) surfaces yaml-schema drift (e.g. an array prop missing `items.type`, the command/combobox `DynamicStringList` mis-map) only HERE, potentially weeks after the yaml edit merged green. Treat such a failure as a genuine latent defect to root-cause at the yaml SoT, never as release-blocking noise. (Structural fix — promoting the corpus batch into PR CI — tracked as a follow-up.)
156
158
 
157
159
  ### `npm run eval:diff -- --engine zettel`
158
160
 
@@ -202,7 +204,7 @@ npm run check:demo-shells
202
204
 
203
205
  Add `verify:corpus` + `check:embeddings-fresh` if chunks were touched; `check:lightningcss-build` if CSS was touched; F-N1 if unpushed release tags exist.
204
206
 
205
- **Full pre-cut sweep** — the 24-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full — a subset run is impossible without editing that file). ~90s wall time.
207
+ **Full pre-cut sweep** — the 28-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full — a subset run is impossible without editing that file). ~90s wall time.
206
208
 
207
209
  **Omnibus** — `npm run check` invokes everything. Heavy; use when re-baselining a stale checkout.
208
210
 
@@ -39,11 +39,11 @@ Substrate fixes (release scripts / lockfile / CHANGELOG), landed once as a norma
39
39
  4. **Tag at the published commit** — `git tag <pkg>-vX.Y.Z` per package; versions differ; no umbrella. Dry-run the trip-wire over the tags (`node scripts/release/check-release.mjs <tags>`) before pushing. If the shared working tree is on a peer's branch, push tags from a throwaway worktree on `origin/main`.
40
40
  5. **Push the tags** → the per-package workflows run `npm ci → verify:plugins → npm publish --access public`.
41
41
  6. **Verify against npm, not the workflow:** `npm view @adia-ai/<pkg> version` returns the new independent version for each package. Watch runs (`gh run watch <id> --exit-status`), but the registry is the source of truth.
42
- 7. **Marketplace cut-over** (if a plugin marketplace repo exists): point the manifest's per-plugin `source` at npm (`{ "source": "npm", "package": "@adia-ai/<pkg>" }`), `git rm -r` vendored copies, validate the JSON, commit, push. Consumers then `/plugin marketplace add <org>/<repo>` `/plugin install <pkg>@<repo>`.
42
+ 7. **Marketplace cut-over** per ADR-0045 the distribution channel is the npm-sourced manifest package `@adia-ai/adia-plugins` (no public git marketplace repo that shape was considered and rejected). Bump/publish the manifest package so its `marketplace.json` entries reference the new plugin versions; consumers add the marketplace from npm, never from a git repo.
43
43
 
44
44
  ## §Verify target
45
45
 
46
- The published package(s) on the **npm registry** at their independent versions, plus (if a marketplace exists) its manifest referencing npm. NOT "all 11 + the demo site" — that's class A.
46
+ The published package(s) on the **npm registry** at their independent versions, plus the npm-published `@adia-ai/adia-plugins` manifest referencing the new versions (ADR-0045). NOT "all 11 + the demo site" — that's class A.
47
47
 
48
48
  ## §Gotchas (from the first independent cut, factory v0.2.2 + forge v0.1.2)
49
49
 
@@ -20,17 +20,21 @@
20
20
  import fs from 'node:fs';
21
21
  import path from 'node:path';
22
22
  import process from 'node:process';
23
- import { PACKAGE_DIRS, PACKAGE_ROSTER } from './package-paths.mjs';
23
+ import { PACKAGE_ROSTER } from './package-paths.mjs';
24
24
 
25
25
  // Single-sourced roster (H3, package-paths.mjs). Each plugin also carries a
26
26
  // .claude-plugin/plugin.json whose version MUST move with package.json —
27
27
  // plugin.json is the /plugin-update cache key (verify:plugins gates the sync).
28
- const PACKAGES = PACKAGE_DIRS;
28
+ // Filtered to `lockstep !== false`: a class-B package (gh#1133's
29
+ // adia-plugins) bumps on its own independent version line, never via this
30
+ // lockstep --from/--to sweep. See package-paths.mjs's `lockstep` field note.
31
+ const LOCKSTEP_ROSTER = PACKAGE_ROSTER.filter((p) => p.lockstep !== false);
32
+ const PACKAGES = LOCKSTEP_ROSTER.map((p) => p.dir);
29
33
 
30
34
  // Sibling manifests bumped in the same pass as package.json (path relative to
31
35
  // the package dir) — derived from the roster's plugin flag.
32
36
  const SIBLING_MANIFESTS = Object.fromEntries(
33
- PACKAGE_ROSTER.filter((p) => p.plugin).map((p) => [p.dir, '.claude-plugin/plugin.json']),
37
+ LOCKSTEP_ROSTER.filter((p) => p.plugin).map((p) => [p.dir, '.claude-plugin/plugin.json']),
34
38
  );
35
39
 
36
40
  // Cross-package EXACT pins bumped in the same pass — a different shape from a
@@ -20,14 +20,17 @@
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 { PACKAGE_NAMES } from './package-paths.mjs';
23
+ import { PACKAGE_ROSTER } from './package-paths.mjs';
24
24
 
25
25
  // Instance data — fork-configurable. The npm scope the lockstep roster publishes under.
26
26
  // Default preserves @adia-ai behavior; override via --scope or $ADIA_NPM_SCOPE.
27
27
  const DEFAULT_SCOPE = '@adia-ai';
28
28
 
29
29
  // Single-sourced roster (H3, package-paths.mjs); order = dispatch order.
30
- const PACKAGES = PACKAGE_NAMES;
30
+ // Filtered to `lockstep !== false`: a class-B package (gh#1133's
31
+ // adia-plugins) publishes via its own manual cut (independent-package-
32
+ // release.md), never this lockstep dispatch loop.
33
+ const PACKAGES = PACKAGE_ROSTER.filter((p) => p.lockstep !== false).map((p) => p.name);
31
34
 
32
35
  function parseArgs(argv) {
33
36
  const args = {
@@ -64,6 +64,35 @@ export const GATE_ROSTER = [
64
64
  // v0.7.13-retro hygiene checker — README CDN pins / "Current version"
65
65
  // claims vs the cut version; branch/worktree limbo prints as warns (H2).
66
66
  { n: 24, cmd: 'node scripts/release/check-cut-hygiene.mjs', what: 'README CDN-pin + version currency', versionArg: true },
67
+ // gh#1132 (factory-dx-ws1-boot SPEC, REQ-04): npm-packs BOTH
68
+ // @adia-ai/web-components and @adia-ai/web-modules and resolves every
69
+ // specifier the scaffold templates emit against the extracted tarballs'
70
+ // real exports maps — the same blast radius gh#296's
71
+ // exports-wildcard-resolution.mjs gate covers, applied to the factory
72
+ // plugin's own templates instead of a consumer's hand-written import.
73
+ { n: 25, cmd: 'python3 packages/plugins/adia-ui-factory/scripts/adia-scaffold selftest', what: 'scaffold specifiers resolve in packed tarballs' },
74
+ // gh#1137 (factory-dx-ws6-measurement SPEC, REQ-05) — FLAGGED FOR
75
+ // CONFIRMATION: this entry was authored by the WS-6 build without a live
76
+ // adia-release sign-off (no operator was available mid-build to confirm
77
+ // the exact freshness window / regression semantics). Asserts a
78
+ // qa/dx/sweeps/ record exists dated within this cut's window AND its
79
+ // verdict is not a regression-RED (a plain floors<=1 baseline RED does
80
+ // NOT block a cut — only a floor that DROPPED since the prior sweep
81
+ // does). CONFIRMED by adia-release on the first live cut (v0.8.35,
82
+ // 2026-08-13): semantics hold, with one correction — the previous-cut
83
+ // comparison excludes the in-flight target version (self-reference fired
84
+ // on the handoff re-run, since the promoted heading was already on main).
85
+ { n: 26, cmd: 'node scripts/release/check-dx-sweep-freshness.mjs', targetVersionArg: true, what: 'DX sweep fresh + not regression-RED' },
86
+ // gh#1135 (WS-4 SPEC REQ-06): a fresh free-form eval run feeds the health
87
+ // record gate 28 writes — without it, "latest" under evals/mcp/runs/ could
88
+ // be a stale leftover from an earlier ad-hoc run, not this cut's own state.
89
+ { n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (evals/health input)' },
90
+ // REQ-06: commits evals/health/<version>.json — the version this record
91
+ // names is the CUT's target version from the start, never the
92
+ // pre/post-bump `hygieneVersion` gate 24 needs (a claim-validation
93
+ // concern this is not) — hence `targetVersionArg` rather than
94
+ // `versionArg`, a distinct flag release-pack.mjs's step3PreFlight reads.
95
+ { 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 },
67
96
  ];
68
97
 
69
98
  // -- CLI ------------------------------------------------------------------
@@ -42,24 +42,52 @@ import path from 'node:path';
42
42
  // Order is the canonical publish/tag order (release-pack's historical order);
43
43
  // consumers needing a different ordering (check-lockstep sorts by path for
44
44
  // stable output) re-sort locally.
45
+ //
46
+ // `lockstep` (added gh#1133, onboarding the first class-B member since the
47
+ // 2026-07-15 policy emptied that class — independent-package-release.md):
48
+ // `true` (or omitted — every pre-2026-08 entry defaults true below) means the
49
+ // package rides the coordinated cut — version-match/caret gate
50
+ // (check-lockstep.mjs), the umbrella tag, the --pending-version sweep, bump.mjs's
51
+ // --from/--to pass, the dispatch-publish loop, and generate-release-notes.mjs's
52
+ // notes. `false` means class B: its own version line, its own manual tag +
53
+ // publish (independent-package-release.md), NEVER the umbrella tag or a
54
+ // lockstep bump/dispatch/notes pass. The ONE exception: the F-N1 release
55
+ // trip-wire (check-release.mjs's per-tag `checkTag` lookup) reads the FULL
56
+ // roster unfiltered — every pushed tag, lockstep or not, still needs its
57
+ // package.json/CHANGELOG validated, so it must recognize a class-B tag too.
58
+ // Every lockstep-scoped consumer must filter `p.lockstep !== false` itself
59
+ // (this module intentionally does NOT pre-filter PACKAGE_NAMES/PACKAGE_DIRS —
60
+ // they stay full-roster projections so a consumer's own filter is visible at
61
+ // its point of use, not hidden upstream).
45
62
  export const PACKAGE_ROSTER = [
46
- { name: 'web-components', dir: 'packages/web-components' },
47
- { name: 'web-modules', dir: 'packages/web-modules' },
48
- { name: 'llm', dir: 'packages/llm' },
49
- { name: 'agent', dir: 'packages/agent' },
50
- { name: 'persona', dir: 'packages/persona' },
51
- { name: 'a2ui-runtime', dir: 'packages/a2ui/runtime' },
52
- { name: 'a2ui-compose', dir: 'packages/a2ui/compose' },
53
- { name: 'a2ui-corpus', dir: 'packages/a2ui/corpus' },
54
- { name: 'a2ui-mcp', dir: 'packages/a2ui/mcp' },
55
- { name: 'a2ui-retrieval', dir: 'packages/a2ui/retrieval' },
56
- { name: 'a2ui-validator', dir: 'packages/a2ui/validator' },
63
+ { name: 'web-components', dir: 'packages/web-components', lockstep: true },
64
+ { name: 'web-modules', dir: 'packages/web-modules', lockstep: true },
65
+ { name: 'llm', dir: 'packages/llm', lockstep: true },
66
+ { name: 'agent', dir: 'packages/agent', lockstep: true },
67
+ { name: 'persona', dir: 'packages/persona', lockstep: true },
68
+ { name: 'a2ui-runtime', dir: 'packages/a2ui/runtime', lockstep: true },
69
+ { name: 'a2ui-compose', dir: 'packages/a2ui/compose', lockstep: true },
70
+ { name: 'a2ui-corpus', dir: 'packages/a2ui/corpus', lockstep: true },
71
+ { name: 'a2ui-mcp', dir: 'packages/a2ui/mcp', lockstep: true },
72
+ { name: 'a2ui-retrieval', dir: 'packages/a2ui/retrieval', lockstep: true },
73
+ { name: 'a2ui-validator', dir: 'packages/a2ui/validator', lockstep: true },
57
74
  // Plugins joined the lockstep 2026-07-15 (check-lockstep.mjs POLICY note);
58
75
  // each carries a .claude-plugin/plugin.json that bumps with package.json.
59
- { name: 'adia-ui-factory', dir: 'packages/plugins/adia-ui-factory', plugin: true },
60
- { name: 'adia-ui-forge', dir: 'packages/plugins/adia-ui-forge', plugin: true },
76
+ { name: 'adia-ui-factory', dir: 'packages/plugins/adia-ui-factory', lockstep: true, plugin: true },
77
+ { name: 'adia-ui-forge', dir: 'packages/plugins/adia-ui-forge', lockstep: true, plugin: true },
78
+ // adia-plugins is class B (gh#1133, ADR-0045/spec-factory-dx-ws2): the npm-
79
+ // sourced marketplace manifest package. No lockstep content (a roster of
80
+ // npm sources, no code) and REQ-03's unpinned sources mean it republishes
81
+ // only on its own roster change — cuts independently, own version line,
82
+ // never the umbrella tag. Not a Claude Code plugin itself (no `plugin.json`
83
+ // sidecar), so no `plugin: true`.
84
+ { name: 'adia-plugins', dir: 'packages/plugins/adia-plugins', lockstep: false },
61
85
  ];
62
86
 
87
+ // Full-roster projections — every registered package, lockstep or not. A
88
+ // lockstep-scoped consumer (bump, tag-lockstep, dispatch-publish, the
89
+ // version-match gate, the notes generator, the docs gate) must filter
90
+ // `.lockstep !== false` itself; see the `lockstep` field note above.
63
91
  export const PACKAGE_NAMES = PACKAGE_ROSTER.map((p) => p.name);
64
92
  export const PACKAGE_DIRS = PACKAGE_ROSTER.map((p) => p.dir);
65
93
 
@@ -81,8 +81,29 @@ function help() {
81
81
  Flags: --poll-interval <s=30> --timeout <s=2400> --dry`);
82
82
  }
83
83
 
84
+ // Transient-network failure signatures (v0.8.34 cut: three bridge runs died
85
+ // on momentary TLS-interception blips — `x509: certificate signed by unknown
86
+ // authority` — that a 20s retry sailed past). Deliberately narrow: anything
87
+ // NOT matching rethrows immediately so real failures stay loud.
88
+ const TRANSIENT_NET = /tls:|x509:|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|Could not resolve host|connection reset|TLS handshake/i;
89
+ const NET_RETRIES = 3;
90
+ const NET_BACKOFF_MS = 20_000;
91
+
92
+ function withNetRetry(fn, label) {
93
+ for (let attempt = 1; ; attempt++) {
94
+ try { return fn(); }
95
+ catch (e) {
96
+ const msg = `${e.message || ''}\n${e.stderr || ''}\n${e.stdout || ''}`;
97
+ if (attempt >= NET_RETRIES || !TRANSIENT_NET.test(msg)) throw e;
98
+ console.error(`[pr-bridge] transient network failure on ${label} (attempt ${attempt}/${NET_RETRIES}) — retrying in ${NET_BACKOFF_MS / 1000}s`);
99
+ execFileSync('sleep', [String(NET_BACKOFF_MS / 1000)]);
100
+ }
101
+ }
102
+ }
103
+
84
104
  function sh(cmd) {
85
- return execSync(cmd, { cwd: REPO, encoding: 'utf8' }).trim();
105
+ return withNetRetry(
106
+ () => execSync(cmd, { cwd: REPO, encoding: 'utf8' }).trim(), cmd.slice(0, 40));
86
107
  }
87
108
 
88
109
  // No-shell variant for every call that carries an operator-supplied value
@@ -90,7 +111,9 @@ function sh(cmd) {
90
111
  // `$(…)`/backticks live inside double quotes and broke on metachar branch
91
112
  // names. execFileSync + arg array = no shell, no expansion, no quoting bugs.
92
113
  function shFile(cmd, argv) {
93
- return execFileSync(cmd, argv, { cwd: REPO, encoding: 'utf8' }).trim();
114
+ return withNetRetry(
115
+ () => execFileSync(cmd, argv, { cwd: REPO, encoding: 'utf8' }).trim(),
116
+ `${cmd} ${argv[0] ?? ''}`);
94
117
  }
95
118
 
96
119
  // ── The decision predicate — pure, selftest-locked ────────────────
@@ -208,6 +231,17 @@ async function main() {
208
231
  if (existing.length > 0) {
209
232
  prNumber = existing[0].number;
210
233
  console.log(`[pr-bridge] reusing open PR #${prNumber} for ${args.branch} ("${existing[0].title}")`);
234
+ // Push any LOCAL-AHEAD commits before judging CI (v0.8.34 cut: a fix
235
+ // committed locally but never pushed left the bridge judging the STALE
236
+ // failed run — permanently, since the remote never got the fix). Only
237
+ // meaningful when the branch exists in this checkout; a bridge run from
238
+ // a checkout without the branch (handoff-only) skips silently.
239
+ if (!args.dry) {
240
+ try {
241
+ shFile('git', ['rev-parse', '--verify', `refs/heads/${args.branch}`]);
242
+ shFile('git', ['push', 'origin', args.branch]);
243
+ } catch { /* no local branch — nothing to push */ }
244
+ }
211
245
  } else if (args.dry) {
212
246
  console.error(`[pr-bridge] --dry: no open PR for ${args.branch}; nothing to report`);
213
247
  process.exit(1);
@@ -3,8 +3,10 @@
3
3
  // in N package CHANGELOGs. The special target `root` is the repo-root
4
4
  // CHANGELOG.md — same swap, no package resolution.
5
5
  //
6
- // Idempotent. Stops if any target already has the [vX.Y.Z] heading
7
- // (means you've already run this; would create a duplicate block).
6
+ // Idempotent including across PARTIAL runs: a target already promoted to
7
+ // exactly [vX.Y.Z] with no [Unreleased] remaining counts as done (skip).
8
+ // Stops only if a target carries BOTH headings (its [Unreleased] content
9
+ // would silently miss the release) — merge those by hand.
8
10
  //
9
11
  // Usage:
10
12
  // node promote-unreleased.mjs --version 0.6.22 --date 2026-05-22 \
@@ -17,7 +19,7 @@
17
19
  //
18
20
  // Exits:
19
21
  // 0 — all targets promoted (or --dry succeeded)
20
- // 1 — at least one target missing [Unreleased] OR already has [vX.Y.Z]
22
+ // 1 — a target missing [Unreleased], or carrying BOTH [Unreleased] and [vX.Y.Z]
21
23
  // 2 — bad args / file I/O error
22
24
 
23
25
  import fs from 'node:fs';
@@ -67,7 +69,17 @@ function promote(repo, pkg, version, date, dry) {
67
69
  const txt = fs.readFileSync(p, 'utf8');
68
70
  const newHeading = `## [${version}] — ${date}`;
69
71
  if (txt.includes(newHeading)) {
70
- console.error(` ERROR ${pkg} already has ${newHeading} (already promoted)`);
72
+ // TRUE idempotency (v0.8.34 cut, gh#976): a partial Step-4 failure
73
+ // leaves some targets promoted; re-running must treat "already carries
74
+ // exactly this heading AND no [Unreleased] remains" as the DONE state,
75
+ // not an error — the error branch forced a hand-driven recovery.
76
+ // Having BOTH headings is still an error: the [Unreleased] content
77
+ // would silently miss the release.
78
+ if (!txt.includes('## [Unreleased]')) {
79
+ console.log(` ok ${pkg} — already promoted to ${newHeading} (idempotent skip)`);
80
+ return 'promoted';
81
+ }
82
+ console.error(` ERROR ${pkg} — has BOTH ${newHeading} and [Unreleased]: merge the [Unreleased] block into the promoted section by hand`);
71
83
  return 'already-promoted';
72
84
  }
73
85
  if (!txt.includes('## [Unreleased]')) {
@@ -111,10 +123,18 @@ function selftest() {
111
123
  if (out.includes('## [Unreleased]')) { console.error('selftest FAIL: [Unreleased] heading should have been renamed, not left behind'); process.exit(1); }
112
124
  if (!out.includes('### Fixed\n- x')) { console.error('selftest FAIL: content under the heading must survive the rename'); process.exit(1); }
113
125
 
114
- // Idempotency: re-running against an already-promoted target must fail
115
- // loudly (a silent no-op or a duplicate block would both be wrong).
126
+ // Idempotency across partial runs (v0.8.34 cut): re-running against a
127
+ // fully-promoted target is the DONE state 'promoted', file untouched.
128
+ const beforeRerun = fs.readFileSync(clPath, 'utf8');
116
129
  const r2 = promote(tmp, 'web-components', '0.2.0', '2026-02-02', false);
117
- if (r2 !== 'already-promoted') { console.error(`selftest FAIL: expected 'already-promoted' on re-run, got '${r2}'`); process.exit(1); }
130
+ if (r2 !== 'promoted') { console.error(`selftest FAIL: expected idempotent 'promoted' on re-run, got '${r2}'`); process.exit(1); }
131
+ if (fs.readFileSync(clPath, 'utf8') !== beforeRerun) { console.error('selftest FAIL: idempotent re-run must not modify the file'); process.exit(1); }
132
+
133
+ // BOTH headings present = the one loud stop left: the [Unreleased]
134
+ // content would silently miss the release.
135
+ fs.writeFileSync(clPath, '# C\n\n## [Unreleased]\n\n### Fixed\n- late fix\n\n## [0.2.0] — 2026-02-02\n\n- x\n');
136
+ const r2b = promote(tmp, 'web-components', '0.2.0', '2026-02-02', false);
137
+ if (r2b !== 'already-promoted') { console.error(`selftest FAIL: expected 'already-promoted' for BOTH-headings case, got '${r2b}'`); process.exit(1); }
118
138
 
119
139
  // `root` target resolves to <repo>/CHANGELOG.md, no package resolution.
120
140
  const rootPath = path.join(tmp, 'CHANGELOG.md');