@adia-ai/adia-ui-forge 0.8.8 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-forge",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
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,16 @@
1
1
  # Changelog — adia-forge
2
2
 
3
+ ## [0.8.9] — 2026-07-19
4
+
5
+ ### Fixed
6
+ - **`release-pack.mjs` now actually creates `release/vX.Y.Z`** before staging — `step5Commit()` previously committed on whatever branch was checked out (the v0.8.8 cut landed its release commit on local `main`, caught and recovered by hand). Idempotent (checkout if the branch exists — a resumed run — create with `-b` otherwise); `step1ReBaseline()` now asserts the starting branch is `main` or the release branch itself for cut modes. Guarded: checking out an *existing* release branch on a dirty tree now fails with a real diagnostic (which files, what to do) instead of an opaque git error.
7
+ - **Step 9's settle-wait polls the registry itself**, not one proxy workflow — the old code waited for `publish-a2ui-validator.yml` alone as a stand-in for all 11 publishes; a straggler on any other package made the immediately-following fail-closed registry check abort on a pure timing false alarm (hit live on the v0.8.8 cut). Now polls all 11 packages + `latest` directly, every 15s up to ~10-18min, per the skill's own doctrine ("the registry is the verify target, never a workflow's green check"). The existing fail-closed check remains as a final confirmation.
8
+ - **`pr-bridge.mjs` refuses to merge a DRAFT PR** — a real incident: it attempted to merge an author-marked draft, which GitHub's API rejects, and the old code (no `isDraft` check anywhere) crashed uncaught. `bridgeDecision()` now treats `isDraft` as an unconditional stop; the `gh pr merge` call and the top-level `main().catch` are both wrapped so no rejection reason — draft or otherwise — can crash invisibly under the script's own documented `grep -E "pr-bridge" | tail -N` usage pattern.
9
+
10
+ ### Maintenance
11
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
12
+ - **`skills/` touched in this release window** (2 file(s), e.g. `scripts/pr-bridge.mjs`) — carried by the entries above.
13
+
3
14
  ## [0.8.8] — 2026-07-19
4
15
 
5
16
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
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",
@@ -124,6 +124,20 @@ export function bridgeDecision(state) {
124
124
  if (!PASS.has(concl)) reasons.push(`check '${c.name}' landed ${concl}`);
125
125
  else gatingLanded++;
126
126
  }
127
+ // gh#(pr-bridge draft guard): a draft PR is the AUTHOR's own "not ready"
128
+ // signal — GitHub's API refuses to merge one anyway (GraphQL: "Pull
129
+ // Request is still a draft"), and the old code had no isDraft check, so
130
+ // bridgeDecision said MERGE, the gh pr merge call threw, and the whole
131
+ // script crashed uncaught. A caller piping through `grep -E "pr-bridge" |
132
+ // tail -N` (this script's own documented usage pattern) sees only the
133
+ // last N lines BEFORE the crash and no error — reads as quiet success.
134
+ // Unconditional stop (review nit: placed after the per-check loop above,
135
+ // not literally first — but every reasons.push() here is unconditional and
136
+ // the "stop" decision below fires on ANY non-empty reasons list, so a
137
+ // draft overrides an otherwise-perfect merge regardless of ordering):
138
+ // never attempt the merge call on a draft, no matter how green everything
139
+ // else is.
140
+ if (state.isDraft) reasons.push('PR is a DRAFT — the author has not marked it ready for review; never auto-merge a draft');
127
141
  if (state.reviewDecision === 'CHANGES_REQUESTED') reasons.push('a review requests changes');
128
142
  if (state.unresolvedThreads > 0) reasons.push(`${state.unresolvedThreads} unresolved review thread(s) — admin merge cannot bypass these`);
129
143
  if (state.mergeable === 'CONFLICTING') reasons.push('PR is CONFLICTING with the base branch');
@@ -144,7 +158,7 @@ export function bridgeDecision(state) {
144
158
  // ── GitHub reads ──────────────────────────────────────────────────
145
159
  function readPrState(prNumber) {
146
160
  const view = JSON.parse(sh(
147
- `gh pr view ${prNumber} --json state,mergeable,reviewDecision,statusCheckRollup,headRefName,baseRefName`,
161
+ `gh pr view ${prNumber} --json state,isDraft,mergeable,reviewDecision,statusCheckRollup,headRefName,baseRefName`,
148
162
  ));
149
163
  const checks = (view.statusCheckRollup || []).map((c) => ({
150
164
  name: c.name || c.context || '?',
@@ -156,6 +170,7 @@ function readPrState(prNumber) {
156
170
  )).data.repository.pullRequest.reviewThreads.nodes;
157
171
  return {
158
172
  state: view.state,
173
+ isDraft: view.isDraft,
159
174
  headRefName: view.headRefName,
160
175
  baseRefName: view.baseRefName,
161
176
  checks,
@@ -240,7 +255,19 @@ async function main() {
240
255
  }
241
256
  if (d.action === 'merge') {
242
257
  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`);
258
+ // Defense in depth: the isDraft check above should catch this, but
259
+ // GitHub's own merge call is the actual authority — never let its
260
+ // rejection crash uncaught (a caller filtering output for the
261
+ // "pr-bridge" prefix, this script's own documented usage, would see
262
+ // the run go quiet mid-merge with no clear failure, easy to misread
263
+ // as success). Report loudly and exit 1 instead of throwing.
264
+ try {
265
+ sh(`gh pr merge ${prNumber} --admin --merge --delete-branch`);
266
+ } catch (e) {
267
+ console.error(`\n[pr-bridge] STOP — gh pr merge rejected PR #${prNumber}: ${e.message || e}`);
268
+ console.error(' Nothing else mutated by this run; investigate and merge manually if appropriate.');
269
+ process.exit(1);
270
+ }
244
271
  break;
245
272
  }
246
273
  if (Date.now() > deadline) {
@@ -269,10 +296,12 @@ async function main() {
269
296
  // ── Selftest — the predicate is the load-bearing part ─────────────
270
297
  function selftest() {
271
298
  const ck = (name, status, conclusion) => ({ name, status, conclusion });
272
- const base = { unresolvedThreads: 0, reviewDecision: 'REVIEW_REQUIRED', mergeable: 'MERGEABLE' };
299
+ const base = { unresolvedThreads: 0, reviewDecision: 'REVIEW_REQUIRED', mergeable: 'MERGEABLE', isDraft: false };
273
300
  const cases = [
274
301
  ['all green → merge',
275
302
  { ...base, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS'), ck('CodeRabbit', '', 'SUCCESS')] }, 'merge'],
303
+ ['DRAFT PR with all-green checks → stop, never attempt gh pr merge (real incident: PR #362)',
304
+ { ...base, isDraft: true, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS'), ck('CodeRabbit', '', 'SUCCESS')] }, 'stop'],
276
305
  ['fail-soft failure is advisory → merge',
277
306
  { ...base, checks: [ck('Static checks + MCP smoke', 'COMPLETED', 'SUCCESS'), ck('Audit health (fail-soft)', 'COMPLETED', 'FAILURE')] }, 'merge'],
278
307
  ['EMPTY check set → wait, never blind-merge (reviewer finding #1)',
@@ -313,4 +342,12 @@ function selftest() {
313
342
 
314
343
  const argv = process.argv.slice(2);
315
344
  if (argv[0] === 'selftest') selftest();
316
- else main().catch((e) => { console.error(e.message || e); process.exit(1); });
345
+ else main().catch((e) => {
346
+ // Reviewer finding (PR #364): every OTHER error path (readPrState()'s gh
347
+ // pr view/graphql calls on every poll, git push/gh pr create, the
348
+ // re-baseline sequence) throws up to this top-level handler, which had NO
349
+ // "[pr-bridge]" prefix — invisible under the exact `grep -E "pr-bridge" |
350
+ // tail -N` filter this PR exists to fix. Same class, different call site.
351
+ console.error(`[pr-bridge] STOP — ${e.message || e}`);
352
+ process.exit(1);
353
+ });
@@ -229,6 +229,21 @@ function shCapture(cmd, args) {
229
229
  // ── Step 1 — Re-baseline ──────────────────────────────────────────
230
230
  function step1ReBaseline(args) {
231
231
  console.log('\n=== Step 1 — Re-baseline ===');
232
+ // cut-procedure.md §Step 1 requires `git branch --show-current` == main
233
+ // before a cut starts — cutting on any other branch pushes a stale main ref
234
+ // while tags point at the wrong tip (recovery-paths.md §Scenario 8). The doc
235
+ // asserted this; nothing here ever checked it (gh#358). The one legitimate
236
+ // exception is resuming this SAME cut after step5Commit already created and
237
+ // checked out release/vX.Y.Z — allow only that exact branch name back in.
238
+ if ((args.mode === 'cut' || args.mode === 'from-scratch') && !args.dry) {
239
+ const branch = shQuiet('git branch --show-current', args).trim();
240
+ const releaseBranch = `release/v${args.version}`;
241
+ if (branch !== 'main' && branch !== releaseBranch) {
242
+ console.error(`ERROR: on branch '${branch}' — cut modes start from 'main' (or resume on '${releaseBranch}'). Switch branches before proceeding.`);
243
+ process.exit(1);
244
+ }
245
+ console.log(` branch: ${branch} (ok)`);
246
+ }
232
247
  console.log('git status --short:');
233
248
  sh('git status --short', args);
234
249
  console.log('\norigin/main..HEAD (unpushed):');
@@ -362,6 +377,30 @@ function step5Commit(args) {
362
377
  return;
363
378
  }
364
379
  console.log('\n=== Step 5 — Stage + Commit ===');
380
+ // cut-procedure.md §Step 5: "Branch FIRST, then stage" — this step never
381
+ // actually created release/vX.Y.Z, so the commit landed on whatever branch
382
+ // was checked out (main, on the v0.8.8 cut — gh#358). Idempotent: if the
383
+ // branch already exists (a resumed run), check it out rather than failing;
384
+ // step1ReBaseline's branch assertion is what stops a genuinely wrong branch
385
+ // from reaching this point.
386
+ const releaseBranch = `release/v${args.version}`;
387
+ const currentBranch = args.dry ? null : shQuiet('git branch --show-current', args).trim();
388
+ if (currentBranch !== releaseBranch) {
389
+ const exists = !args.dry && shQuiet(`git branch --list ${releaseBranch}`, args).trim().length > 0;
390
+ // Reviewer finding (PR #359): checking out an EXISTING release branch (a
391
+ // resumed run) on a dirty tree fails with a raw, opaque git error — Step 4
392
+ // (promote/bump/lockfile) runs immediately before this and dirties exactly
393
+ // the files a release branch differs from main in. A fresh `-b` create
394
+ // from clean main can't hit this, so the check only guards the resume path.
395
+ if (exists && !args.dry) {
396
+ const dirty = shQuiet('git status --porcelain', args).trim();
397
+ if (dirty) {
398
+ console.error(`ERROR: '${releaseBranch}' already exists, but the working tree is dirty — checking out would fail or silently carry these changes onto the resumed branch:\n${dirty}\n Commit, stash, or discard first, then re-run.`);
399
+ process.exit(1);
400
+ }
401
+ }
402
+ sh(`git checkout ${exists ? '' : '-b '}${releaseBranch}`, args);
403
+ }
365
404
  sh('git reset HEAD >/dev/null 2>&1 || true', args);
366
405
  const releaseFiles = [
367
406
  'package-lock.json',
@@ -512,11 +551,34 @@ async function step9Publish(args) {
512
551
  // Give GH a moment to index the runs created by the per-tag push in Step 8.
513
552
  if (!args.dry) sh('sleep 6', args);
514
553
  sh(`node ${SCRIPT_DIR}/dispatch-publish.mjs --version ${args.version} --scope ${args.scope} --verify-triggered`, args);
515
- console.log('\n Waiting for publishes to settle...');
516
- sh(`until [ "$(gh run list --workflow=publish-a2ui-validator.yml --limit 1 --json status -q '.[0].status')" = "completed" ]; do sleep 5; done`, args);
517
- console.log(' publishes settled');
554
+ // gh#354: this used to poll ONE workflow (publish-a2ui-validator.yml) as a
555
+ // sample-of-one proxy for all 11 publishes settling a straggler on any
556
+ // OTHER package's workflow made the fail-closed check below abort on a pure
557
+ // timing false alarm (hit live on the v0.8.8 cut: a2ui-retrieval published
558
+ // successfully ~1 minute after validator did, after the old proxy had
559
+ // already declared "settled" and the registry read caught it mid-flight).
560
+ // Poll the REGISTRY itself instead — the skill's own doctrine (SKILL.md:
561
+ // "the registry is the verify target, never a workflow's green check")
562
+ // applies here as much as it does to the final check.
563
+ console.log(`\n Waiting for the registry to confirm all ${PACKAGES.length} packages at ${args.version} (polling every 15s, up to 10min)...`);
564
+ sh(
565
+ `for i in $(seq 1 40); do ` +
566
+ `fail=0; ` +
567
+ `for pkg in ${PACKAGES.join(' ')}; do ` +
568
+ `v="$(npm view "${args.scope}/$pkg" version 2>/dev/null || true)"; ` +
569
+ `[ "$v" = "${args.version}" ] || fail=1; ` +
570
+ `done; ` +
571
+ `latest="$(npm view ${args.scope}/web-components dist-tags.latest 2>/dev/null || true)"; ` +
572
+ `[ "$latest" = "${args.version}" ] || fail=1; ` +
573
+ `[ "$fail" = 0 ] && { echo " ✓ registry confirms all ${PACKAGES.length} + latest, after $i check(s)"; exit 0; }; ` +
574
+ `sleep 15; ` +
575
+ `done; ` +
576
+ `echo " registry did not converge after 10min — the fail-closed check below will report exactly which package(s) are still stale"`,
577
+ args,
578
+ );
518
579
  // Fail-closed: the registry is the verify target (SKILL.md) — a missing or
519
- // stale package aborts the cut here, before GH releases + site deploy.
580
+ // stale package aborts the cut here, before GH releases + site deploy. This
581
+ // is now a final confirmation of the poll above, not the first real read.
520
582
  console.log(`\n Post-publish registry verification, all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
521
583
  sh(
522
584
  `fail=0; ` +
@@ -648,6 +710,8 @@ function selftest() {
648
710
  `${GATE_ROSTER.length}-gate roster`,
649
711
  `✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`,
650
712
  'STOPPING per invariant 3',
713
+ // gh#358 — the release branch must actually be created, not just documented.
714
+ 'git checkout -b release/v9.9.9',
651
715
  ];
652
716
  for (const marker of need) {
653
717
  if (!out.includes(marker)) {