@adia-ai/adia-ui-forge 0.8.7 → 0.8.8
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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/skills/adia-release/SKILL.md +1 -1
- package/skills/adia-release/references/cut-procedure.md +16 -4
- package/skills/adia-release/references/gates-catalog.md +9 -14
- package/skills/adia-release/scripts/bump.mjs +143 -22
- package/skills/adia-release/scripts/dispatch-publish.mjs +3 -14
- package/skills/adia-release/scripts/gate-roster.mjs +9 -1
- package/skills/adia-release/scripts/package-paths.mjs +38 -0
- package/skills/adia-release/scripts/pr-bridge.mjs +316 -0
- package/skills/adia-release/scripts/release-pack.mjs +39 -23
- package/skills/adia-release/scripts/tag-lockstep.mjs +4 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adia-forge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kim",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog — adia-forge
|
|
2
2
|
|
|
3
|
+
## [0.8.8] — 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`pr-bridge.mjs`** (adia-release `skills/adia-release/scripts/`): the cut's two-phase PR bridge, mechanized — pushes the release branch, opens/reuses the PR, polls CI, and admin-merges ONLY on all-non-fail-soft-checks-green + zero unresolved review threads + no changes-requested + MERGEABLE; anything else waits or stops with full evidence (`--dry` previews the decision). Independently reviewed and hardened pre-ship: never merges on an empty gating-check set ("nothing failed" ≠ "something passed"); operator-supplied args go through `execFileSync` arg arrays (no shell); the merge-landing proof never skips silently. 14-case selftest; registered in CI's Plugin bin selftests; battle-tested on 8 real merges before first release use. cut-procedure §5.7 documents both the mechanized and manual forms.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **`package-paths.mjs` is now the single source of the 11-package roster** (`PACKAGE_ROSTER` + `PACKAGE_NAMES`/`PACKAGE_DIRS`): the lockstep list was re-declared in 8 scripts (bump, tag-lockstep, dispatch-publish, release-pack, and the repo-side check-release/check-lockstep/check-release-docs/generate-release-notes), each with its own projection — the same list-drift class the gate-roster incident proved out. Skill-side home is deliberate: cache-executed skill scripts can't import repo modules; repo scripts import the skill module by its stable in-repo path.
|
|
10
|
+
- **`gate-roster.mjs` grew gate 18** (`check-cut-hygiene --version X.Y.Z`, via the new `versionArg` field release-pack substitutes) and **gate 4 now runs `test:unit:serial`** (`vitest --no-file-parallelism` — gates-catalog §Cat 8's documented source of truth IS now the gate). References updated same-change: cut-procedure §3.1 (18-gate table), gates-catalog Cat 1/7/8/9.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`bump.mjs` moves every cross-file version reference in the same pass**: the factory `.mcp.json` `@adia-ai/a2ui-mcp@X.Y.Z` pin (invariant 8 — hand-fixed on BOTH the 0.8.6 and 0.8.7 cuts; `check:lockstep` now guards it too) and the root README "Current version" claim (`REPO_PINNED_REFS` — gate 18 exact-matches it, so a hand-set value fails the next cut by construction). All pins pre-flight-validated alongside `validateAllAtFrom`, so a doomed bump writes NOTHING (reviewer finding: the mid-loop check fired only after two files were already written).
|
|
14
|
+
- **adia-release docs**: SKILL.md Mechanization + cut-procedure §5.7/§3.1 + gates-catalog reflect pr-bridge, the 18-gate roster, the umbrella tag's `info` classification (the repo-side `check-release.mjs` fix that killed the Step-7 false-stop which forced manual handoffs on 0.8.6/0.8.7), and the deleted superseded `check-changelog-coverage` gate.
|
|
15
|
+
|
|
16
|
+
### Maintenance
|
|
17
|
+
- **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
|
|
18
|
+
|
|
3
19
|
## [0.8.7] — 2026-07-19
|
|
4
20
|
|
|
5
21
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/adia-ui-forge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adia-ui",
|
|
@@ -95,7 +95,7 @@ assumed pass, or "published" without the actual `npm view` output.
|
|
|
95
95
|
|
|
96
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 of the 11 packages 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 17-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), `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 only missing runs; `--after` enforces batch ordering — dispatch output states what WAS done, never a to-do phrased as if it hadn't fired). 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` (12 tags, `--delete` to move), `dispatch-publish.mjs` (publish workflows; `--verify-triggered` re-dispatches only missing runs; `--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
|
|
|
@@ -78,13 +78,13 @@ npm run build:bundles # dist CSS+JS bundles
|
|
|
78
78
|
```bash
|
|
79
79
|
node scripts/build/components.mjs --verify # 1 yaml ↔ sidecar ↔ .d.ts
|
|
80
80
|
npm run verify:traits # 2 trait coverage
|
|
81
|
-
npm run check:lockstep # 3 version coherence
|
|
82
|
-
npm run test:unit
|
|
81
|
+
npm run check:lockstep # 3 version coherence (+ factory .mcp.json a2ui-mcp pin, invariant 8)
|
|
82
|
+
npm run test:unit:serial # 4 vitest, serial — the source of truth (§Cat 8; parallel flakes under load)
|
|
83
83
|
npm run typecheck # 5 tsc --noEmit
|
|
84
84
|
npm run check:demo-shells # 6 demo imports cover composes:
|
|
85
85
|
npm run check:lightningcss-build # 7 CSS minifies
|
|
86
86
|
npm run check:css-bundles-fresh # 8 dist CSS matches source
|
|
87
|
-
npm run check:js-bundles-fresh # 9 dist JS matches source
|
|
87
|
+
npm run check:js-bundles-fresh # 9 dist JS matches source + semantic content (everything-import, ≥100 -ui tags, icons-manifest ×2)
|
|
88
88
|
npm run smoke:engines # 10 gen-UI engines
|
|
89
89
|
npm run smoke:register-engine # 11 register-engine 11/11
|
|
90
90
|
npm run verify:corpus # 12 corpus 0 errors
|
|
@@ -93,6 +93,7 @@ npm run check:embeddings-fresh # 14 embeddings vs chunk index
|
|
|
93
93
|
npm run check:links # 15 intra-repo links
|
|
94
94
|
npm run eval:diff -- --engine zettel # 16 eval floors
|
|
95
95
|
npm run dogfood:status # 17 P0/P1 dogfood floor
|
|
96
|
+
node scripts/release/check-cut-hygiene.mjs --version <mode-dependent> # 18 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)
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
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.
|
|
@@ -157,7 +158,18 @@ Drift here means a regen output was left out of the allowlist — stage it and r
|
|
|
157
158
|
|
|
158
159
|
## §Step 5.7 — Release PR: push the branch, merge, re-baseline
|
|
159
160
|
|
|
160
|
-
The release commit reaches `main` through the standard PR flow
|
|
161
|
+
The release commit reaches `main` through the standard PR flow. Mechanized
|
|
162
|
+
form (preferred — `pr-bridge.mjs` pushes, opens the PR, polls, merges ONLY
|
|
163
|
+
when every non-fail-soft check is green AND zero review threads are
|
|
164
|
+
unresolved AND no review requests changes; any other state stops with the
|
|
165
|
+
evidence, never force-merges):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/pr-bridge.mjs" \
|
|
169
|
+
--branch "release/vX.Y.Z" --title "release: vX.Y.Z lockstep" --body-file <path>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Manual form (the same flow the bridge mechanizes):
|
|
161
173
|
|
|
162
174
|
```bash
|
|
163
175
|
git -C "$REPO" push -u origin "release/vX.Y.Z"
|
|
@@ -25,12 +25,9 @@ Row layout per gate: **What** · **Typical failure** · **Recovery**.
|
|
|
25
25
|
- **Cosmetic failure:** "diff `packages/<pkg>/components/` touched but CHANGELOG `[X.Y.Z]` doesn't mention 'components'" — the entry names the component (`table-ui`) but not the literal path keyword. Regex miss, change IS documented.
|
|
26
26
|
- **Real failure:** a touched directory has NO matching CHANGELOG entry at all — the cycle missed documenting a change.
|
|
27
27
|
- **Recovery:** cosmetic → add the path keyword to a relevant entry (`table.yaml` → `components/table/table.yaml`); real → author the missing entry. Either way: land it as a new commit through the PR flow (the release commit is already merged — `--amend` is not possible post-invariant-3), delete + re-create the tags at the new post-merge SHA, re-run. Full discipline: [`changelog-discipline.md`](changelog-discipline.md) §F-N1 enrichment.
|
|
28
|
-
- The umbrella tag
|
|
28
|
+
- The umbrella tag `vX.Y.Z` classifies **`info`** (since 2026-07-19 — it used to score `error`, which false-stopped release-pack's Step 7 on every cut); only per-package findings need action.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **What:** every published package has a `[vX.Y.Z]` CHANGELOG block for the current `package.json` version.
|
|
33
|
-
- **Recovery:** add the block (stub if genuinely no source change — but classify by the diff first, see [`changelog-discipline.md`](changelog-discipline.md) §Detecting source-changing packages).
|
|
30
|
+
(The former `check:changelog-coverage` gate was deleted 2026-07-19 — its drifted 9-package matcher could report "clean" where the authoritative gate fails. Pre-tag coverage is `check-release.mjs --pending-version X.Y.Z [--fix]`, Step 4f.)
|
|
34
31
|
|
|
35
32
|
---
|
|
36
33
|
|
|
@@ -141,14 +138,7 @@ The two `audit:*` probes are soft-fail pre-cut (exit-1 only on criticals); findi
|
|
|
141
138
|
|
|
142
139
|
- **What:** the `dist/` CDN bundles (`web-components.min.{css,js}`, `everything.min.js`, per-shell `*.min.js`, `icons-manifest.js`) match source. Stale bundles mean jsdelivr/unpkg serve yesterday's build. **Blocking in CI** (2026-06-08).
|
|
143
140
|
- **Recovery:** `npm run build -w @adia-ai/llm` **FIRST** (its `index.js` is a gitignored tsc artifact `build:bundle-js` resolves — a fresh worktree can't bundle without it), then `npm run build:bundles` (or `build:bundle-css` / `build:bundle-js` individually). Stage both `dist/` trees.
|
|
144
|
-
- **
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
grep "import '@adia-ai/web-components'" packages/web-modules/everything.js # must be present
|
|
148
|
-
grep -oE '"[a-z][a-z-]*-ui"' packages/web-modules/dist/everything.min.js | sort -u | wc -l # ≥100
|
|
149
|
-
node -e "const m=require('./packages/web-components/dist/icons-manifest.js'); \
|
|
150
|
-
console.log('regular:', m.default.regular.length, '| cdn:', m.default.cdn)" # non-empty + cdn key
|
|
151
|
-
```
|
|
141
|
+
- **Semantic content checks — mechanized 2026-07-19** (formerly manual spot-checks; the v0.6.29–31 cycles shipped a shells-only `everything.min.js` that hard-crashed CDN consumers): `bundle-js.mjs` now enforces, on every build AND `--verify` run, that (1) `everything.js` still imports `@adia-ai/web-components`, (2) `everything.min.js` carries ≥100 distinct `-ui` tags, and (3) `icons-manifest.js` exists populated in BOTH dist trees. A fresh-but-semantically-wrong bundle fails the gate itself — no separate manual step remains.
|
|
152
142
|
|
|
153
143
|
### `npm run audit:chunk-reconcile`
|
|
154
144
|
|
|
@@ -162,7 +152,7 @@ The two `audit:*` probes are soft-fail pre-cut (exit-1 only on criticals); findi
|
|
|
162
152
|
|
|
163
153
|
- **What:** the vitest suite (~1000+ tests).
|
|
164
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.
|
|
165
|
-
- **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. **
|
|
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.
|
|
166
156
|
|
|
167
157
|
### `npm run eval:diff -- --engine zettel`
|
|
168
158
|
|
|
@@ -179,6 +169,11 @@ Siblings: `typecheck` (`tsc --noEmit`) · `smoke:engines` (gen-UI engines + retr
|
|
|
179
169
|
|
|
180
170
|
## §Category 9 — Misc release safety
|
|
181
171
|
|
|
172
|
+
### `node scripts/release/check-cut-hygiene.mjs --version X.Y.Z`
|
|
173
|
+
|
|
174
|
+
- **What:** roster gate 18 (wired 2026-07-19; release-pack fills `--version` — the roster's one `versionArg` entry). The objective half of the v0.7.13-retro hygiene checker: published README CDN pins / "Current version" claims must not sit below the cut's minor (`doc-currency`, error-level); merged release branches + extra worktrees print as `limbo` warns (advisory without `--strict`). The judgment half of §4e — content *currency* of what the READMEs say — stays yours.
|
|
175
|
+
- **Recovery:** update the stale README pin/claim, stage it into the release commit. A `limbo` warn routes to branch/worktree cleanup, never blocks the cut.
|
|
176
|
+
|
|
182
177
|
### `npm run dogfood:status`
|
|
183
178
|
|
|
184
179
|
- **What:** aggregator over the component dogfood audits; classifies findings P0–P3 and regenerates the tracker at `qa/findings/dogfood-tracker.md`. Exit-1 when P0+P1 > 0; P2/P3 are advisory.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// bump.mjs — bump
|
|
2
|
+
// bump.mjs — bump the 11 lockstep @adia-ai/* package.json from --from to --to.
|
|
3
3
|
//
|
|
4
4
|
// Idempotent. Stops on the first mismatch (a package not at --from) so
|
|
5
5
|
// you can investigate before any mutation lands.
|
|
@@ -20,32 +20,55 @@
|
|
|
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
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'packages/a2ui/compose',
|
|
29
|
-
'packages/a2ui/corpus',
|
|
30
|
-
'packages/a2ui/mcp',
|
|
31
|
-
'packages/a2ui/retrieval',
|
|
32
|
-
'packages/a2ui/runtime',
|
|
33
|
-
'packages/a2ui/validator',
|
|
34
|
-
// Plugins joined the lockstep 2026-07-15 (see check-lockstep.mjs's POLICY
|
|
35
|
-
// note). Each also carries a .claude-plugin/plugin.json whose version MUST
|
|
36
|
-
// move with package.json — plugin.json is the /plugin-update cache key
|
|
37
|
-
// (verify:plugins gates the sync).
|
|
38
|
-
'packages/plugins/adia-ui-factory',
|
|
39
|
-
'packages/plugins/adia-ui-forge',
|
|
40
|
-
];
|
|
25
|
+
// Single-sourced roster (H3, package-paths.mjs). Each plugin also carries a
|
|
26
|
+
// .claude-plugin/plugin.json whose version MUST move with package.json —
|
|
27
|
+
// plugin.json is the /plugin-update cache key (verify:plugins gates the sync).
|
|
28
|
+
const PACKAGES = PACKAGE_DIRS;
|
|
41
29
|
|
|
42
30
|
// Sibling manifests bumped in the same pass as package.json (path relative to
|
|
43
|
-
// the package dir)
|
|
44
|
-
const SIBLING_MANIFESTS =
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
// the package dir) — derived from the roster's plugin flag.
|
|
32
|
+
const SIBLING_MANIFESTS = Object.fromEntries(
|
|
33
|
+
PACKAGE_ROSTER.filter((p) => p.plugin).map((p) => [p.dir, '.claude-plugin/plugin.json']),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Cross-package EXACT pins bumped in the same pass — a different shape from a
|
|
37
|
+
// package.json "version" field, so SIBLING_MANIFESTS' regex can't reach them.
|
|
38
|
+
// The factory plugin's .mcp.json pins @adia-ai/a2ui-mcp to the exact lockstep
|
|
39
|
+
// version (SKILL invariant 8 — the consumer never floats). bump.mjs left this
|
|
40
|
+
// untouched through the 0.8.6 + 0.8.7 cuts, forcing a manual edit + amend each
|
|
41
|
+
// time; check-lockstep's mcp-pin guard now catches a drift, this moves it.
|
|
42
|
+
const PINNED_REFS = {
|
|
43
|
+
'packages/plugins/adia-ui-factory': {
|
|
44
|
+
file: '.mcp.json',
|
|
45
|
+
label: '@adia-ai/a2ui-mcp pin',
|
|
46
|
+
// `@adia-ai/a2ui-mcp@<from>` (not a prefix of a longer version) → `@<to>`
|
|
47
|
+
pattern: (from) => new RegExp(`(@adia-ai/a2ui-mcp@)${from.replace(/\./g, '\\.')}(?![\\d.])`),
|
|
48
|
+
replace: (to) => `$1${to}`,
|
|
49
|
+
},
|
|
47
50
|
};
|
|
48
51
|
|
|
52
|
+
// Repo-level version references (path relative to REPO ROOT, not a package) —
|
|
53
|
+
// same mechanism, different anchor. The root README's "Current version" claim
|
|
54
|
+
// is gate 18's EXACT-match check (check-cut-hygiene doc-currency): hand-set it
|
|
55
|
+
// once and the next cut fails the gate — so the bump moves it, like the
|
|
56
|
+
// .mcp.json pin (H2 follow-through, 2026-07-19: found 0.7.13 claimed while
|
|
57
|
+
// 0.8.7 was live — a full minor of README drift shipped to npm).
|
|
58
|
+
const REPO_PINNED_REFS = [
|
|
59
|
+
{
|
|
60
|
+
file: 'README.md',
|
|
61
|
+
label: '"Current version" claim',
|
|
62
|
+
pattern: (from) => new RegExp(`([Cc]urrent version[^\\d]*v?)${from.replace(/\./g, '\\.')}(?![\\d.])`),
|
|
63
|
+
replace: (to) => `$1${to}`,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Pure so the selftest can lock the transform without touching a real file.
|
|
68
|
+
function bumpPinnedRef(text, cfg, from, to) {
|
|
69
|
+
return text.replace(cfg.pattern(from), cfg.replace(to));
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
function parseArgs(argv) {
|
|
50
73
|
const args = { from: null, to: null, dry: false, repo: process.cwd() };
|
|
51
74
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -84,6 +107,52 @@ function loadPackageJsons(repo) {
|
|
|
84
107
|
return result;
|
|
85
108
|
}
|
|
86
109
|
|
|
110
|
+
// Pre-flight the cross-package pins the same way validateAllAtFrom pre-flights
|
|
111
|
+
// versions: a pin not at --from means invariant-8 drift, and catching it HERE —
|
|
112
|
+
// before bumpAll writes anything — keeps the failure mutation-free. (Reviewer
|
|
113
|
+
// finding on the first PINNED_REFS cut: the mid-loop pin check fired only after
|
|
114
|
+
// factory's package.json + plugin.json were already written, leaving a
|
|
115
|
+
// half-bumped tree whose re-run then failed validateAllAtFrom.)
|
|
116
|
+
function loadPinnedRefs(repo) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const [pkg, cfg] of Object.entries(PINNED_REFS)) {
|
|
119
|
+
const p = path.join(repo, pkg, cfg.file);
|
|
120
|
+
if (!fs.existsSync(p)) {
|
|
121
|
+
console.error(`error: missing ${p}`);
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
out.push({ pkg, cfg, path: p, txt: fs.readFileSync(p, 'utf8') });
|
|
125
|
+
}
|
|
126
|
+
for (const cfg of REPO_PINNED_REFS) {
|
|
127
|
+
const p = path.join(repo, cfg.file);
|
|
128
|
+
if (!fs.existsSync(p)) {
|
|
129
|
+
console.error(`error: missing ${p}`);
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
out.push({ pkg: '(repo)', cfg, path: p, txt: fs.readFileSync(p, 'utf8') });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Repo-level pins aren't tied to a package iteration in bumpAll — apply them
|
|
138
|
+
// after it, from the same pre-validated set (validatePinsAtFrom guarantees the
|
|
139
|
+
// pattern matches, so this cannot fail mid-write).
|
|
140
|
+
function applyRepoPins(pins, from, to, dry) {
|
|
141
|
+
for (const x of pins.filter((x) => x.pkg === '(repo)')) {
|
|
142
|
+
const out = bumpPinnedRef(x.txt, x.cfg, from, to);
|
|
143
|
+
if (!dry) fs.writeFileSync(x.path, out);
|
|
144
|
+
console.log(` ${dry ? '[dry] ' : ''}bumped ${x.cfg.file} ${x.cfg.label}: ${from} → ${to}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validatePinsAtFrom(pins, from) {
|
|
149
|
+
const bad = pins.filter((x) => !x.cfg.pattern(from).test(x.txt));
|
|
150
|
+
for (const x of bad) {
|
|
151
|
+
console.error(`error: ${x.pkg}/${x.cfg.file} — ${x.cfg.label} '@${from}' not found (invariant-8 drift? fix the pin first: \`npm run check:lockstep:fix\`). Nothing written.`);
|
|
152
|
+
}
|
|
153
|
+
return bad.length === 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
87
156
|
function validateAllAtFrom(packages, from) {
|
|
88
157
|
const wrong = packages.filter((p) => p.version !== from);
|
|
89
158
|
if (wrong.length > 0) {
|
|
@@ -121,6 +190,18 @@ function bumpAll(packages, from, to, dry) {
|
|
|
121
190
|
if (!dry) fs.writeFileSync(sibPath, sibOut);
|
|
122
191
|
console.log(` ${dry ? '[dry] ' : ''}bumped ${p.pkg}/${sibling}: ${from} → ${to}`);
|
|
123
192
|
}
|
|
193
|
+
const pin = PINNED_REFS[p.pkg];
|
|
194
|
+
if (pin) {
|
|
195
|
+
const pinPath = path.join(path.dirname(p.path), pin.file);
|
|
196
|
+
const pinTxt = fs.readFileSync(pinPath, 'utf8');
|
|
197
|
+
const pinOut = bumpPinnedRef(pinTxt, pin, from, to);
|
|
198
|
+
if (pinOut === pinTxt) {
|
|
199
|
+
console.error(`error: ${p.pkg}/${pin.file} — ${pin.label} '@${from}' not found (invariant 8 drift? set the pin to @${from} and re-run, or use \`npm run check:lockstep:fix\`)`);
|
|
200
|
+
return -1;
|
|
201
|
+
}
|
|
202
|
+
if (!dry) fs.writeFileSync(pinPath, pinOut);
|
|
203
|
+
console.log(` ${dry ? '[dry] ' : ''}bumped ${p.pkg}/${pin.file} ${pin.label}: @${from} → @${to}`);
|
|
204
|
+
}
|
|
124
205
|
}
|
|
125
206
|
return bumped;
|
|
126
207
|
}
|
|
@@ -128,8 +209,11 @@ function bumpAll(packages, from, to, dry) {
|
|
|
128
209
|
function main() {
|
|
129
210
|
const args = parseArgs(process.argv.slice(2));
|
|
130
211
|
const packages = loadPackageJsons(args.repo);
|
|
212
|
+
const pins = loadPinnedRefs(args.repo);
|
|
131
213
|
if (!validateAllAtFrom(packages, args.from)) process.exit(1);
|
|
214
|
+
if (!validatePinsAtFrom(pins, args.from)) process.exit(1);
|
|
132
215
|
const bumped = bumpAll(packages, args.from, args.to, args.dry);
|
|
216
|
+
if (bumped >= 0) applyRepoPins(pins, args.from, args.to, args.dry);
|
|
133
217
|
if (bumped < 0) process.exit(3);
|
|
134
218
|
console.log(`\n[bump] ${bumped}/${PACKAGES.length} ${args.dry ? '(dry — no files written)' : 'bumped'}`);
|
|
135
219
|
if (!args.dry) {
|
|
@@ -166,6 +250,43 @@ function selftest() {
|
|
|
166
250
|
if (bumpAll(corrupted, '0.8.4', '0.8.5', true) !== -1) {
|
|
167
251
|
console.error('selftest FAIL: corrupted package.json (no version match) not caught'); process.exit(1);
|
|
168
252
|
}
|
|
253
|
+
|
|
254
|
+
// PINNED_REFS — the .mcp.json a2ui-mcp pin (invariant 8) moves in lockstep.
|
|
255
|
+
const pinCfg = PINNED_REFS['packages/plugins/adia-ui-factory'];
|
|
256
|
+
const pinBefore = '{"args":["-y","@adia-ai/a2ui-mcp@0.8.6"]}';
|
|
257
|
+
const pinAfter = bumpPinnedRef(pinBefore, pinCfg, '0.8.6', '0.8.7');
|
|
258
|
+
if (!pinAfter.includes('@adia-ai/a2ui-mcp@0.8.7') || pinAfter.includes('@0.8.6')) {
|
|
259
|
+
console.error('selftest FAIL: PINNED_REFS did not bump the .mcp.json a2ui-mcp pin'); process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
// Must NOT prefix-match a longer version (0.8.6 inside 0.8.60).
|
|
262
|
+
if (bumpPinnedRef('@adia-ai/a2ui-mcp@0.8.60', pinCfg, '0.8.6', '0.8.7') !== '@adia-ai/a2ui-mcp@0.8.60') {
|
|
263
|
+
console.error('selftest FAIL: PINNED_REFS pattern prefix-matched a longer version'); process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// validatePinsAtFrom — the mutation-free pre-flight (reviewer finding: the
|
|
267
|
+
// mid-loop pin check fired only after two files were already written).
|
|
268
|
+
const goodPin = [{ pkg: 'packages/plugins/adia-ui-factory', cfg: pinCfg, path: '/nonexistent/.mcp.json', txt: pinBefore }];
|
|
269
|
+
if (!validatePinsAtFrom(goodPin, '0.8.6')) {
|
|
270
|
+
console.error('selftest FAIL: validatePinsAtFrom rejected a pin at --from'); process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
if (validatePinsAtFrom(goodPin, '0.8.5')) {
|
|
273
|
+
console.error('selftest FAIL: validatePinsAtFrom accepted a drifted pin (should pre-flight-fail before any write)'); process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// REPO_PINNED_REFS — the root README "Current version" claim moves per-cut
|
|
277
|
+
// (gate 18's doc-currency check is EXACT-match, so a static value fails the
|
|
278
|
+
// next cut by construction).
|
|
279
|
+
const readmeCfg = REPO_PINNED_REFS[0];
|
|
280
|
+
const claimBefore = 'lockstep. Current version: **`v0.8.7`** — see CHANGELOG';
|
|
281
|
+
const claimAfter = bumpPinnedRef(claimBefore, readmeCfg, '0.8.7', '0.8.8');
|
|
282
|
+
if (!claimAfter.includes('v0.8.8') || claimAfter.includes('0.8.7')) {
|
|
283
|
+
console.error('selftest FAIL: REPO_PINNED_REFS did not move the Current-version claim'); process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
// Must not touch an already-moved or unrelated version string.
|
|
286
|
+
if (bumpPinnedRef('Current version: v0.8.70', readmeCfg, '0.8.7', '0.8.8') !== 'Current version: v0.8.70') {
|
|
287
|
+
console.error('selftest FAIL: Current-version pattern prefix-matched a longer version'); process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
169
290
|
console.log('selftest OK');
|
|
170
291
|
}
|
|
171
292
|
|
|
@@ -20,25 +20,14 @@
|
|
|
20
20
|
import { execSync } 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
24
|
|
|
24
25
|
// Instance data — fork-configurable. The npm scope the 11 packages publish under.
|
|
25
26
|
// Default preserves @adia-ai behavior; override via --scope or $ADIA_NPM_SCOPE.
|
|
26
27
|
const DEFAULT_SCOPE = '@adia-ai';
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'web-modules',
|
|
31
|
-
'llm',
|
|
32
|
-
'a2ui-runtime',
|
|
33
|
-
'a2ui-compose',
|
|
34
|
-
'a2ui-corpus',
|
|
35
|
-
'a2ui-mcp',
|
|
36
|
-
'a2ui-retrieval',
|
|
37
|
-
'a2ui-validator',
|
|
38
|
-
// Plugins joined the lockstep 2026-07-15 (check-lockstep.mjs POLICY note).
|
|
39
|
-
'adia-ui-factory',
|
|
40
|
-
'adia-ui-forge',
|
|
41
|
-
];
|
|
29
|
+
// Single-sourced roster (H3, package-paths.mjs); order = dispatch order.
|
|
30
|
+
const PACKAGES = PACKAGE_NAMES;
|
|
42
31
|
|
|
43
32
|
function parseArgs(argv) {
|
|
44
33
|
const args = {
|
|
@@ -23,7 +23,10 @@ export const GATE_ROSTER = [
|
|
|
23
23
|
{ n: 1, cmd: 'node scripts/build/components.mjs --verify', what: 'yaml ↔ sidecar ↔ .d.ts' },
|
|
24
24
|
{ n: 2, cmd: 'npm run verify:traits', what: 'trait coverage' },
|
|
25
25
|
{ n: 3, cmd: 'npm run check:lockstep', what: 'version coherence' },
|
|
26
|
-
|
|
26
|
+
// Serial on purpose: heavy web-modules suites flake under full parallelism
|
|
27
|
+
// ("drain loop exceeded 100 iterations") — gates-catalog §Cat 8 names the
|
|
28
|
+
// sequential run as the source of truth; the gate now IS that run (H5).
|
|
29
|
+
{ n: 4, cmd: 'npm run test:unit:serial', what: 'vitest (serial — the source of truth)' },
|
|
27
30
|
{ n: 5, cmd: 'npm run typecheck', what: 'tsc --noEmit' },
|
|
28
31
|
{ n: 6, cmd: 'npm run check:demo-shells', what: 'demo imports cover composes:' },
|
|
29
32
|
{ n: 7, cmd: 'npm run check:lightningcss-build', what: 'CSS minifies' },
|
|
@@ -37,6 +40,11 @@ export const GATE_ROSTER = [
|
|
|
37
40
|
{ n: 15, cmd: 'npm run check:links', what: 'intra-repo links' },
|
|
38
41
|
{ n: 16, cmd: 'npm run eval:diff -- --engine zettel', what: 'eval floors' },
|
|
39
42
|
{ n: 17, cmd: 'npm run dogfood:status', what: 'P0/P1 dogfood floor' },
|
|
43
|
+
// versionArg: the runner appends ` --version X.Y.Z` (release-pack knows the
|
|
44
|
+
// cut version; the roster stays a static list). Objective half of the
|
|
45
|
+
// v0.7.13-retro hygiene checker — README CDN pins / "Current version"
|
|
46
|
+
// claims vs the cut version; branch/worktree limbo prints as warns (H2).
|
|
47
|
+
{ n: 18, cmd: 'node scripts/release/check-cut-hygiene.mjs', what: 'README CDN-pin + version currency', versionArg: true },
|
|
40
48
|
];
|
|
41
49
|
|
|
42
50
|
// -- CLI ------------------------------------------------------------------
|
|
@@ -23,6 +23,44 @@
|
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
|
|
26
|
+
// ─── THE package roster — single source of truth (H3, 2026-07-19) ─────────
|
|
27
|
+
//
|
|
28
|
+
// The 11-package lockstep list was re-declared in ~8 files (bump, tag-lockstep,
|
|
29
|
+
// dispatch-publish, release-pack, check-release, check-lockstep,
|
|
30
|
+
// check-release-docs, generate-release-notes), each carrying its own
|
|
31
|
+
// name/path projection — adding the 12th package meant 8 hand-edits, and the
|
|
32
|
+
// gate-roster.mjs incident (release-pack silently running 12 of 17 gates)
|
|
33
|
+
// proved what list-drift does. Same fix, same pattern: ONE list here, every
|
|
34
|
+
// consumer projects from it.
|
|
35
|
+
//
|
|
36
|
+
// This module lives in the SKILL scripts dir deliberately: skill scripts must
|
|
37
|
+
// work from the installed plugin cache (CLAUDE_PLUGIN_ROOT), where repo
|
|
38
|
+
// modules aren't importable — but repo scripts (scripts/release/*) always run
|
|
39
|
+
// in-repo and CAN import this by its stable in-repo path. The asymmetry
|
|
40
|
+
// decides the home.
|
|
41
|
+
//
|
|
42
|
+
// Order is the canonical publish/tag order (release-pack's historical order);
|
|
43
|
+
// consumers needing a different ordering (check-lockstep sorts by path for
|
|
44
|
+
// stable output) re-sort locally.
|
|
45
|
+
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: 'a2ui-runtime', dir: 'packages/a2ui/runtime' },
|
|
50
|
+
{ name: 'a2ui-compose', dir: 'packages/a2ui/compose' },
|
|
51
|
+
{ name: 'a2ui-corpus', dir: 'packages/a2ui/corpus' },
|
|
52
|
+
{ name: 'a2ui-mcp', dir: 'packages/a2ui/mcp' },
|
|
53
|
+
{ name: 'a2ui-retrieval', dir: 'packages/a2ui/retrieval' },
|
|
54
|
+
{ name: 'a2ui-validator', dir: 'packages/a2ui/validator' },
|
|
55
|
+
// Plugins joined the lockstep 2026-07-15 (check-lockstep.mjs POLICY note);
|
|
56
|
+
// each carries a .claude-plugin/plugin.json that bumps with package.json.
|
|
57
|
+
{ name: 'adia-ui-factory', dir: 'packages/plugins/adia-ui-factory', plugin: true },
|
|
58
|
+
{ name: 'adia-ui-forge', dir: 'packages/plugins/adia-ui-forge', plugin: true },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const PACKAGE_NAMES = PACKAGE_ROSTER.map((p) => p.name);
|
|
62
|
+
export const PACKAGE_DIRS = PACKAGE_ROSTER.map((p) => p.dir);
|
|
63
|
+
|
|
26
64
|
// Resolve a package arg to its on-disk directory under packages/, accepting
|
|
27
65
|
// BOTH the name form (`a2ui-corpus`) and the path form (`a2ui/corpus`).
|
|
28
66
|
// Returns the resolved relative directory (e.g. `a2ui/corpus`); does not
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pr-bridge.mjs — babysit a release PR from push to merged main (the cut's
|
|
3
|
+
// two-phase bridge, mechanized).
|
|
4
|
+
//
|
|
5
|
+
// The cut modes of release-pack.mjs stop at the release commit (invariant 3:
|
|
6
|
+
// release commits land via PR, never a direct push to main). The bridge from
|
|
7
|
+
// there to `--mode handoff` — push the branch, open the PR, wait out CI,
|
|
8
|
+
// merge, re-baseline main — was the release cycle's last hand-carried leg
|
|
9
|
+
// (the operator babysat it on every cut). This script mechanizes EXACTLY the
|
|
10
|
+
// documented house flow (cut-procedure.md §5.7), no policy expansion:
|
|
11
|
+
//
|
|
12
|
+
// MERGES only when ALL of:
|
|
13
|
+
// - every non-fail-soft check has landed SUCCESS (jobs whose name carries
|
|
14
|
+
// the repo's "(fail-soft)" marker are advisory — reported, never gating,
|
|
15
|
+
// matching §5.7's "required checks (fail-soft jobs excluded)")
|
|
16
|
+
// - zero unresolved review threads (admin merge bypasses required review
|
|
17
|
+
// but NOT unresolved threads — repo-verified behavior)
|
|
18
|
+
// - no CHANGES_REQUESTED review decision
|
|
19
|
+
// - GitHub reports the PR MERGEABLE
|
|
20
|
+
// WAITS while anything is still pending/queued/UNKNOWN (up to --timeout).
|
|
21
|
+
// STOPS (exit 1, full evidence, no mutation) on anything else: a failed
|
|
22
|
+
// check, an unresolved thread, changes requested, a merge conflict, or
|
|
23
|
+
// timeout. It never force-merges past a red or contested state — a stop
|
|
24
|
+
// here is a human's turn, by design.
|
|
25
|
+
//
|
|
26
|
+
// Usage:
|
|
27
|
+
// node pr-bridge.mjs --branch release/v0.8.8 \
|
|
28
|
+
// --title "release: v0.8.8 lockstep" --body-file /tmp/pr-body.md
|
|
29
|
+
// node pr-bridge.mjs --branch <existing-pr-branch> # reuses the open PR
|
|
30
|
+
// node pr-bridge.mjs selftest
|
|
31
|
+
//
|
|
32
|
+
// Flags:
|
|
33
|
+
// --poll-interval <s> seconds between polls (default 30)
|
|
34
|
+
// --timeout <s> max seconds to wait for pending checks (default 2400)
|
|
35
|
+
// --dry report the current decision for the PR and exit;
|
|
36
|
+
// no push, no create, no merge
|
|
37
|
+
//
|
|
38
|
+
// Exit: 0 merged + main re-baselined · 1 stopped (evidence printed) · 2 bad args.
|
|
39
|
+
//
|
|
40
|
+
// Part of the adia-release skill. Registered in CI's "Plugin bin selftests".
|
|
41
|
+
|
|
42
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import process from 'node:process';
|
|
45
|
+
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
46
|
+
|
|
47
|
+
const REPO = process.cwd();
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const args = {
|
|
51
|
+
branch: null, title: null, bodyFile: null,
|
|
52
|
+
pollInterval: 30, timeout: 2400, dry: false,
|
|
53
|
+
};
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const k = argv[i];
|
|
56
|
+
if (k === '--branch') args.branch = argv[++i];
|
|
57
|
+
else if (k === '--title') args.title = argv[++i];
|
|
58
|
+
else if (k === '--body-file') args.bodyFile = argv[++i];
|
|
59
|
+
else if (k === '--poll-interval') args.pollInterval = Number(argv[++i]);
|
|
60
|
+
else if (k === '--timeout') args.timeout = Number(argv[++i]);
|
|
61
|
+
else if (k === '--dry') args.dry = true;
|
|
62
|
+
else if (k === '-h' || k === '--help') { help(); process.exit(0); }
|
|
63
|
+
}
|
|
64
|
+
if (!args.branch) {
|
|
65
|
+
console.error('error: --branch is required (see --help)');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isFinite(args.pollInterval) || args.pollInterval < 5 ||
|
|
69
|
+
!Number.isFinite(args.timeout) || args.timeout < args.pollInterval) {
|
|
70
|
+
console.error('error: --poll-interval must be ≥5s and --timeout ≥ --poll-interval');
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
return args;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function help() {
|
|
77
|
+
console.log(`Usage:
|
|
78
|
+
node pr-bridge.mjs --branch release/vX.Y.Z --title "release: vX.Y.Z lockstep" --body-file <path>
|
|
79
|
+
node pr-bridge.mjs --branch <branch-with-open-pr> # reuse the PR
|
|
80
|
+
node pr-bridge.mjs --branch <branch> --dry # decision only
|
|
81
|
+
Flags: --poll-interval <s=30> --timeout <s=2400> --dry`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sh(cmd) {
|
|
85
|
+
return execSync(cmd, { cwd: REPO, encoding: 'utf8' }).trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// No-shell variant for every call that carries an operator-supplied value
|
|
89
|
+
// (branch, title, body path) — reviewer finding #2: string interpolation left
|
|
90
|
+
// `$(…)`/backticks live inside double quotes and broke on metachar branch
|
|
91
|
+
// names. execFileSync + arg array = no shell, no expansion, no quoting bugs.
|
|
92
|
+
function shFile(cmd, argv) {
|
|
93
|
+
return execFileSync(cmd, argv, { cwd: REPO, encoding: 'utf8' }).trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── The decision predicate — pure, selftest-locked ────────────────
|
|
97
|
+
// state: {
|
|
98
|
+
// checks: [{ name, status, conclusion }] normalized check runs + contexts
|
|
99
|
+
// unresolvedThreads: number
|
|
100
|
+
// reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | null
|
|
101
|
+
// mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN'
|
|
102
|
+
// }
|
|
103
|
+
// → { action: 'merge' | 'wait' | 'stop', reasons: [string] }
|
|
104
|
+
//
|
|
105
|
+
// Gating checks = every check whose name does NOT carry the "(fail-soft)"
|
|
106
|
+
// marker (the repo's own advisory-job convention). NEUTRAL and SKIPPED count
|
|
107
|
+
// as pass; anything not landed counts as pending; every other conclusion is
|
|
108
|
+
// a failure. CHANGES_REQUESTED, unresolved threads, and CONFLICTING always
|
|
109
|
+
// stop — no flag overrides them.
|
|
110
|
+
export const FAIL_SOFT_RE = /fail-soft/i;
|
|
111
|
+
const PASS = new Set(['SUCCESS', 'NEUTRAL', 'SKIPPED']);
|
|
112
|
+
const PENDING = new Set(['PENDING', 'QUEUED', 'IN_PROGRESS', 'WAITING', 'REQUESTED', 'EXPECTED', '']);
|
|
113
|
+
|
|
114
|
+
export function bridgeDecision(state) {
|
|
115
|
+
const reasons = [];
|
|
116
|
+
let pending = false;
|
|
117
|
+
let gatingLanded = 0;
|
|
118
|
+
|
|
119
|
+
for (const c of state.checks) {
|
|
120
|
+
if (FAIL_SOFT_RE.test(c.name)) continue; // advisory by convention
|
|
121
|
+
const landed = (c.status || '').toUpperCase() === 'COMPLETED' || c.conclusion;
|
|
122
|
+
const concl = (c.conclusion || c.status || '').toUpperCase();
|
|
123
|
+
if (!landed || PENDING.has(concl)) { pending = true; continue; }
|
|
124
|
+
if (!PASS.has(concl)) reasons.push(`check '${c.name}' landed ${concl}`);
|
|
125
|
+
else gatingLanded++;
|
|
126
|
+
}
|
|
127
|
+
if (state.reviewDecision === 'CHANGES_REQUESTED') reasons.push('a review requests changes');
|
|
128
|
+
if (state.unresolvedThreads > 0) reasons.push(`${state.unresolvedThreads} unresolved review thread(s) — admin merge cannot bypass these`);
|
|
129
|
+
if (state.mergeable === 'CONFLICTING') reasons.push('PR is CONFLICTING with the base branch');
|
|
130
|
+
|
|
131
|
+
if (reasons.length > 0) return { action: 'stop', reasons };
|
|
132
|
+
if (pending || state.mergeable === 'UNKNOWN') return { action: 'wait', reasons: ['checks or mergeability still pending'] };
|
|
133
|
+
// Never merge on an empty gating set (reviewer finding #1): "nothing failed"
|
|
134
|
+
// must not pass for "something passed" — a fresh PR's rollup can be briefly
|
|
135
|
+
// empty (or carry only advisory fail-soft jobs) while CI spins up, and this
|
|
136
|
+
// window would otherwise admin-merge with ZERO verified checks. WAIT (not
|
|
137
|
+
// stop): the checks appear within moments, or the timeout reports it.
|
|
138
|
+
if (gatingLanded === 0) {
|
|
139
|
+
return { action: 'wait', reasons: ['no gating check has landed SUCCESS yet — refusing to merge on an empty check set'] };
|
|
140
|
+
}
|
|
141
|
+
return { action: 'merge', reasons: [] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── GitHub reads ──────────────────────────────────────────────────
|
|
145
|
+
function readPrState(prNumber) {
|
|
146
|
+
const view = JSON.parse(sh(
|
|
147
|
+
`gh pr view ${prNumber} --json state,mergeable,reviewDecision,statusCheckRollup,headRefName,baseRefName`,
|
|
148
|
+
));
|
|
149
|
+
const checks = (view.statusCheckRollup || []).map((c) => ({
|
|
150
|
+
name: c.name || c.context || '?',
|
|
151
|
+
status: c.status || c.state || '',
|
|
152
|
+
conclusion: c.conclusion || (c.state === 'SUCCESS' ? 'SUCCESS' : c.state === 'FAILURE' || c.state === 'ERROR' ? 'FAILURE' : ''),
|
|
153
|
+
}));
|
|
154
|
+
const threads = JSON.parse(sh(
|
|
155
|
+
`gh api graphql -f query='query($n:Int!){repository(owner:"${OWNER}",name:"${NAME}"){pullRequest(number:$n){reviewThreads(first:100){nodes{isResolved path line}}}}}' -F n=${prNumber}`,
|
|
156
|
+
)).data.repository.pullRequest.reviewThreads.nodes;
|
|
157
|
+
return {
|
|
158
|
+
state: view.state,
|
|
159
|
+
headRefName: view.headRefName,
|
|
160
|
+
baseRefName: view.baseRefName,
|
|
161
|
+
checks,
|
|
162
|
+
unresolvedThreads: threads.filter((t) => !t.isResolved).length,
|
|
163
|
+
unresolvedList: threads.filter((t) => !t.isResolved).map((t) => `${t.path}:${t.line}`),
|
|
164
|
+
reviewDecision: view.reviewDecision || null,
|
|
165
|
+
mergeable: view.mergeable || 'UNKNOWN',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function printEvidence(st) {
|
|
170
|
+
console.log(` checks (${st.checks.length}):`);
|
|
171
|
+
for (const c of st.checks) {
|
|
172
|
+
const soft = FAIL_SOFT_RE.test(c.name) ? ' [advisory]' : '';
|
|
173
|
+
console.log(` ${c.name}: ${c.status} ${c.conclusion}${soft}`);
|
|
174
|
+
}
|
|
175
|
+
console.log(` unresolved threads: ${st.unresolvedThreads}${st.unresolvedThreads ? ' — ' + st.unresolvedList.join(', ') : ''}`);
|
|
176
|
+
console.log(` reviewDecision: ${st.reviewDecision || '(none)'} · mergeable: ${st.mergeable}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let OWNER = 'adiahealth';
|
|
180
|
+
let NAME = 'gen-ui-kit';
|
|
181
|
+
|
|
182
|
+
// ── Main flow ─────────────────────────────────────────────────────
|
|
183
|
+
async function main() {
|
|
184
|
+
const args = parseArgs(process.argv.slice(2));
|
|
185
|
+
assertMonorepoRoot(REPO);
|
|
186
|
+
const slug = JSON.parse(sh('gh repo view --json nameWithOwner')).nameWithOwner;
|
|
187
|
+
[OWNER, NAME] = slug.split('/');
|
|
188
|
+
|
|
189
|
+
// Locate or create the PR for the branch.
|
|
190
|
+
const existing = JSON.parse(
|
|
191
|
+
shFile('gh', ['pr', 'list', '--head', args.branch, '--state', 'open', '--json', 'number,title']));
|
|
192
|
+
let prNumber;
|
|
193
|
+
if (existing.length > 0) {
|
|
194
|
+
prNumber = existing[0].number;
|
|
195
|
+
console.log(`[pr-bridge] reusing open PR #${prNumber} for ${args.branch} ("${existing[0].title}")`);
|
|
196
|
+
} else if (args.dry) {
|
|
197
|
+
console.error(`[pr-bridge] --dry: no open PR for ${args.branch}; nothing to report`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
} else {
|
|
200
|
+
if (!args.title || !args.bodyFile) {
|
|
201
|
+
console.error('error: no open PR for this branch — --title and --body-file are required to create one');
|
|
202
|
+
process.exit(2);
|
|
203
|
+
}
|
|
204
|
+
if (!fs.existsSync(args.bodyFile)) {
|
|
205
|
+
console.error(`error: body file not found: ${args.bodyFile}`);
|
|
206
|
+
process.exit(2);
|
|
207
|
+
}
|
|
208
|
+
// Push the branch (idempotent if already pushed), then create.
|
|
209
|
+
shFile('git', ['push', '-u', 'origin', args.branch]);
|
|
210
|
+
const url = shFile('gh', ['pr', 'create', '--base', 'main', '--head', args.branch, '--title', args.title, '--body-file', args.bodyFile]);
|
|
211
|
+
prNumber = Number(url.split('/').pop());
|
|
212
|
+
console.log(`[pr-bridge] opened PR #${prNumber}: ${url}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Poll until the predicate resolves.
|
|
216
|
+
const deadline = Date.now() + args.timeout * 1000;
|
|
217
|
+
for (;;) {
|
|
218
|
+
const st = readPrState(prNumber);
|
|
219
|
+
if (st.state === 'MERGED') {
|
|
220
|
+
console.log(`[pr-bridge] PR #${prNumber} is already merged`);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (st.state !== 'OPEN') {
|
|
224
|
+
console.error(`[pr-bridge] STOP — PR #${prNumber} is ${st.state}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const d = bridgeDecision(st);
|
|
228
|
+
console.log(`\n[pr-bridge] ${new Date().toISOString()} — decision: ${d.action.toUpperCase()}`);
|
|
229
|
+
printEvidence(st);
|
|
230
|
+
|
|
231
|
+
if (args.dry) {
|
|
232
|
+
console.log(`[pr-bridge] --dry: would ${d.action}${d.reasons.length ? ` (${d.reasons.join('; ')})` : ''}`);
|
|
233
|
+
process.exit(d.action === 'stop' ? 1 : 0);
|
|
234
|
+
}
|
|
235
|
+
if (d.action === 'stop') {
|
|
236
|
+
console.error(`\n[pr-bridge] STOP — not merging PR #${prNumber}:`);
|
|
237
|
+
for (const r of d.reasons) console.error(` ✗ ${r}`);
|
|
238
|
+
console.error(' This state needs a human (or a fix pushed to the branch); pr-bridge never merges past it.');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
if (d.action === 'merge') {
|
|
242
|
+
console.log(`\n[pr-bridge] all gates green — merging PR #${prNumber} (admin merge per house flow §5.7)`);
|
|
243
|
+
sh(`gh pr merge ${prNumber} --admin --merge --delete-branch`);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
if (Date.now() > deadline) {
|
|
247
|
+
console.error(`\n[pr-bridge] STOP — timeout after ${args.timeout}s with checks still pending. Re-run to resume waiting.`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
await new Promise((r) => setTimeout(r, args.pollInterval * 1000));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Re-baseline main and prove the merge landed.
|
|
254
|
+
sh('git checkout main');
|
|
255
|
+
sh('git pull origin main');
|
|
256
|
+
const mergedSha = JSON.parse(sh(`gh pr view ${prNumber} --json mergeCommit`)).mergeCommit?.oid;
|
|
257
|
+
if (mergedSha) {
|
|
258
|
+
sh(`git merge-base --is-ancestor ${mergedSha} HEAD`); // throws if not
|
|
259
|
+
console.log(`[pr-bridge] main re-baselined at ${sh('git rev-parse --short HEAD')} (merge ${mergedSha.slice(0, 9)} confirmed in history)`);
|
|
260
|
+
} else {
|
|
261
|
+
// Reviewer finding #4: never skip the landing proof SILENTLY.
|
|
262
|
+
console.log(`[pr-bridge] note: GitHub has not populated mergeCommit yet — landing proof skipped (main was pulled; verify with \`git log\` if it matters)`);
|
|
263
|
+
}
|
|
264
|
+
// Local branch cleanup (remote side handled by --delete-branch).
|
|
265
|
+
try { shFile('git', ['branch', '-D', args.branch]); } catch { /* already gone */ }
|
|
266
|
+
console.log(`[pr-bridge] done — PR #${prNumber} merged, main current. Resume with --mode handoff.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Selftest — the predicate is the load-bearing part ─────────────
|
|
270
|
+
function selftest() {
|
|
271
|
+
const ck = (name, status, conclusion) => ({ name, status, conclusion });
|
|
272
|
+
const base = { unresolvedThreads: 0, reviewDecision: 'REVIEW_REQUIRED', mergeable: 'MERGEABLE' };
|
|
273
|
+
const cases = [
|
|
274
|
+
['all green → merge',
|
|
275
|
+
{ ...base, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS'), ck('CodeRabbit', '', 'SUCCESS')] }, 'merge'],
|
|
276
|
+
['fail-soft failure is advisory → merge',
|
|
277
|
+
{ ...base, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS'), ck('Audit health (fail-soft)', 'COMPLETED', 'FAILURE')] }, 'merge'],
|
|
278
|
+
['EMPTY check set → wait, never blind-merge (reviewer finding #1)',
|
|
279
|
+
{ ...base, checks: [] }, 'wait'],
|
|
280
|
+
['fail-soft-only set → wait, zero gating checks verified (reviewer finding #1)',
|
|
281
|
+
{ ...base, checks: [ck('Audit health (fail-soft)', 'COMPLETED', 'SUCCESS')] }, 'wait'],
|
|
282
|
+
['gating check failed → stop',
|
|
283
|
+
{ ...base, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'FAILURE')] }, 'stop'],
|
|
284
|
+
['gating check pending → wait',
|
|
285
|
+
{ ...base, checks: [ck('Static checks + MCP smoke', 'IN_PROGRESS', '')] }, 'wait'],
|
|
286
|
+
['status-context pending → wait',
|
|
287
|
+
{ ...base, checks: [ck('CodeRabbit', 'PENDING', '')] }, 'wait'],
|
|
288
|
+
['unresolved thread → stop even when green',
|
|
289
|
+
{ ...base, unresolvedThreads: 2, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS')] }, 'stop'],
|
|
290
|
+
['changes requested → stop even when green',
|
|
291
|
+
{ ...base, reviewDecision: 'CHANGES_REQUESTED', checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS')] }, 'stop'],
|
|
292
|
+
['conflicting → stop',
|
|
293
|
+
{ ...base, mergeable: 'CONFLICTING', checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS')] }, 'stop'],
|
|
294
|
+
['mergeable UNKNOWN with green checks → wait (never merge blind)',
|
|
295
|
+
{ ...base, mergeable: 'UNKNOWN', checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS')] }, 'wait'],
|
|
296
|
+
['SKIPPED + NEUTRAL count as pass → merge',
|
|
297
|
+
{ ...base, checks: [ck('a', 'COMPLETED', 'SKIPPED'), ck('b', 'COMPLETED', 'NEUTRAL')] }, 'merge'],
|
|
298
|
+
['CANCELLED is a failure → stop',
|
|
299
|
+
{ ...base, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'CANCELLED')] }, 'stop'],
|
|
300
|
+
['failure + pending → stop wins (report now, not after the wait)',
|
|
301
|
+
{ ...base, checks: [ck('a', 'COMPLETED', 'FAILURE'), ck('b', 'IN_PROGRESS', '')] }, 'stop'],
|
|
302
|
+
];
|
|
303
|
+
let failed = 0;
|
|
304
|
+
for (const [label, state, want] of cases) {
|
|
305
|
+
const got = bridgeDecision(state).action;
|
|
306
|
+
const ok = got === want;
|
|
307
|
+
if (!ok) failed++;
|
|
308
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${label}: expected ${want}, got ${got}`);
|
|
309
|
+
}
|
|
310
|
+
console.log(failed === 0 ? '\nselftest OK' : `\nselftest: ${failed} failure(s)`);
|
|
311
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const argv = process.argv.slice(2);
|
|
315
|
+
if (argv[0] === 'selftest') selftest();
|
|
316
|
+
else main().catch((e) => { console.error(e.message || e); process.exit(1); });
|
|
@@ -68,7 +68,7 @@ import os from 'node:os';
|
|
|
68
68
|
import path from 'node:path';
|
|
69
69
|
import process from 'node:process';
|
|
70
70
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
71
|
-
import { resolvePackageChangelog } from './package-paths.mjs';
|
|
71
|
+
import { resolvePackageChangelog, PACKAGE_NAMES, PACKAGE_ROSTER } from './package-paths.mjs';
|
|
72
72
|
import { GATE_ROSTER } from './gate-roster.mjs';
|
|
73
73
|
|
|
74
74
|
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -80,11 +80,8 @@ const REPO = process.cwd();
|
|
|
80
80
|
const DEFAULT_DEPLOY_HOST = 'ui-kit.exe.xyz';
|
|
81
81
|
const DEFAULT_NPM_SCOPE = '@adia-ai';
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'a2ui-validator',
|
|
86
|
-
// Plugins joined the lockstep 2026-07-15 (check-lockstep.mjs POLICY note).
|
|
87
|
-
'adia-ui-factory', 'adia-ui-forge'];
|
|
83
|
+
// Single-sourced roster (H3, package-paths.mjs); order = publish order.
|
|
84
|
+
const PACKAGES = PACKAGE_NAMES;
|
|
88
85
|
|
|
89
86
|
function parseArgs(argv) {
|
|
90
87
|
const args = {
|
|
@@ -253,11 +250,19 @@ function step3PreFlight(args) {
|
|
|
253
250
|
}
|
|
254
251
|
console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
|
|
255
252
|
for (const g of GATE_ROSTER) {
|
|
256
|
-
|
|
253
|
+
// versionArg gates take a MODE-DEPENDENT version (both directions bit
|
|
254
|
+
// the first 0.8.8 run): cut modes run pre-flight PRE-BUMP, where the
|
|
255
|
+
// README "Current version" claim still equals the PREVIOUS version (the
|
|
256
|
+
// bump moves it in Step 4; Step 4g then re-proves at the cut version) —
|
|
257
|
+
// while handoff runs POST-MERGE, where the claim already equals the CUT
|
|
258
|
+
// version and validating at previous fails by construction.
|
|
259
|
+
const hygieneVersion = args.mode === 'handoff' ? args.version : args.previous;
|
|
260
|
+
const cmd = g.versionArg ? `${g.cmd} --version ${hygieneVersion}` : g.cmd;
|
|
261
|
+
console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${cmd} # ${g.what}`);
|
|
257
262
|
try {
|
|
258
|
-
sh(
|
|
263
|
+
sh(cmd, args, { stdio: 'inherit' });
|
|
259
264
|
} catch (e) {
|
|
260
|
-
console.error(`\n ✗ FAILED (${g.n}/${GATE_ROSTER.length}): ${
|
|
265
|
+
console.error(`\n ✗ FAILED (${g.n}/${GATE_ROSTER.length}): ${cmd}`);
|
|
261
266
|
console.error(' Pre-flight aborted. Fix the gate failure before proceeding.');
|
|
262
267
|
console.error(' See references/gates-catalog.md for failure-mode → recovery.');
|
|
263
268
|
process.exit(1);
|
|
@@ -339,6 +344,15 @@ function step4PromoteAndBump(args) {
|
|
|
339
344
|
// delete/retag rounds discovering the same gaps only after tagging.
|
|
340
345
|
console.log('\n Step 4f — pre-tag coverage (+auto-fix) with the authoritative F-N1 matcher:');
|
|
341
346
|
sh(`node scripts/release/check-release.mjs --pending-version ${args.version} --fix`, args);
|
|
347
|
+
|
|
348
|
+
// Step 4g — re-run the versionArg gates at the CUT version, post-bump: the
|
|
349
|
+
// pre-flight ran them against the previous version (the README claim can't
|
|
350
|
+
// equal the cut version before bump.mjs moves it); this run proves the bump
|
|
351
|
+
// moved every cross-file reference to the new version.
|
|
352
|
+
console.log('\n Step 4g — cut-hygiene at the cut version (post-bump proof):');
|
|
353
|
+
for (const g of GATE_ROSTER.filter((g) => g.versionArg)) {
|
|
354
|
+
sh(`${g.cmd} --version ${args.version}`, args);
|
|
355
|
+
}
|
|
342
356
|
}
|
|
343
357
|
|
|
344
358
|
// ── Step 5 — Commit ──────────────────────────────────────────────
|
|
@@ -351,18 +365,18 @@ function step5Commit(args) {
|
|
|
351
365
|
sh('git reset HEAD >/dev/null 2>&1 || true', args);
|
|
352
366
|
const releaseFiles = [
|
|
353
367
|
'package-lock.json',
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
368
|
+
// Root README's "Current version" claim — bump.mjs moves it per-cut
|
|
369
|
+
// (REPO_PINNED_REFS); gate 18 (check-cut-hygiene doc-currency) exact-matches
|
|
370
|
+
// it against the cut version, so it must ride the release commit.
|
|
371
|
+
'README.md',
|
|
372
|
+
...PACKAGE_ROSTER.flatMap(({ name, dir, plugin }) => {
|
|
373
|
+
// Roster-driven (H3): each package stages its manifest + CHANGELOG;
|
|
374
|
+
// plugins also stage .claude-plugin/plugin.json (the /plugin-update
|
|
375
|
+
// cache key, moved by bump.mjs); factory additionally pins a2ui-mcp in
|
|
376
|
+
// .mcp.json (SKILL invariant 8 — bump.mjs moves the pin too).
|
|
363
377
|
const files = [`${dir}/package.json`, `${dir}/CHANGELOG.md`];
|
|
364
|
-
if (
|
|
365
|
-
if (
|
|
378
|
+
if (plugin) files.push(`${dir}/.claude-plugin/plugin.json`);
|
|
379
|
+
if (name === 'adia-ui-factory') files.push(`${dir}/.mcp.json`);
|
|
366
380
|
return files;
|
|
367
381
|
}),
|
|
368
382
|
];
|
|
@@ -579,9 +593,11 @@ async function main() {
|
|
|
579
593
|
console.log(`
|
|
580
594
|
✓ release commit ready — STOPPING per invariant 3 (release commits land via PR).
|
|
581
595
|
|
|
582
|
-
Next (
|
|
583
|
-
|
|
584
|
-
|
|
596
|
+
Next — mechanized bridge (pushes, opens the PR, waits out CI, merges only
|
|
597
|
+
green + zero unresolved threads, re-baselines main; stops on anything else):
|
|
598
|
+
node ${SCRIPT_DIR}/pr-bridge.mjs --branch release/v${args.version} \\
|
|
599
|
+
--title "release: v${args.version} lockstep" --body-file <path>
|
|
600
|
+
(or the manual flow: git push -u origin release/v${args.version} && gh pr create … && merge)
|
|
585
601
|
Then resume from post-merge main:
|
|
586
602
|
git checkout main && git pull origin main
|
|
587
603
|
node release-pack.mjs --mode handoff --version ${args.version} --date ${args.date} \\
|
|
@@ -15,22 +15,11 @@
|
|
|
15
15
|
import { execSync } from 'node:child_process';
|
|
16
16
|
import process from 'node:process';
|
|
17
17
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
18
|
+
import { PACKAGE_NAMES } from './package-paths.mjs';
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'llm',
|
|
23
|
-
'a2ui-runtime',
|
|
24
|
-
'a2ui-compose',
|
|
25
|
-
'a2ui-corpus',
|
|
26
|
-
'a2ui-mcp',
|
|
27
|
-
'a2ui-retrieval',
|
|
28
|
-
'a2ui-validator',
|
|
29
|
-
// Plugins joined the lockstep 2026-07-15 (check-lockstep.mjs POLICY note);
|
|
30
|
-
// their `adia-ui-*-v*` tags trigger publish-adia-ui-{factory,forge}.yml.
|
|
31
|
-
'adia-ui-factory',
|
|
32
|
-
'adia-ui-forge',
|
|
33
|
-
];
|
|
20
|
+
// The roster is single-sourced in package-paths.mjs (H3) — the `adia-ui-*-v*`
|
|
21
|
+
// tags trigger publish-adia-ui-{factory,forge}.yml like every other package.
|
|
22
|
+
const PER_PACKAGE_NAMES = PACKAGE_NAMES;
|
|
34
23
|
|
|
35
24
|
function parseArgs(argv) {
|
|
36
25
|
const args = { version: null, at: null, deleteMode: false, dry: false, repo: process.cwd() };
|