@adia-ai/adia-ui-forge 0.8.4 → 0.8.6

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 (72) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +27 -2
  3. package/README.md +13 -4
  4. package/agents/a2ui-builder.corpus.json +35 -0
  5. package/agents/a2ui-builder.md +37 -0
  6. package/agents/component-author.corpus.json +40 -0
  7. package/agents/component-author.md +10 -1
  8. package/agents/framework-reviewer.corpus.json +35 -0
  9. package/agents/framework-reviewer.md +42 -0
  10. package/agents/release-builder.corpus.json +39 -0
  11. package/agents/release-builder.md +39 -0
  12. package/agents/routing-corpus.json +38 -38
  13. package/bin/release-pretag-docs-gate +226 -0
  14. package/commands/release.md +5 -3
  15. package/hooks/hooks.json +10 -0
  16. package/package.json +1 -1
  17. package/skills/adia-a2ui/SKILL.md +37 -8
  18. package/skills/adia-a2ui/evals/routing-corpus.json +36 -29
  19. package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
  20. package/skills/adia-a2ui/references/zettel-calibration.md +5 -5
  21. package/skills/adia-author/SKILL.md +16 -8
  22. package/skills/adia-author/evals/routing-corpus.json +39 -218
  23. package/skills/adia-author/references/anti-patterns.md +7 -7
  24. package/skills/adia-author/references/api-contract.md +9 -9
  25. package/skills/adia-author/references/authoring-cycle.md +25 -6
  26. package/skills/adia-author/references/code-style.md +4 -5
  27. package/skills/adia-author/references/lifecycle-patterns.md +8 -8
  28. package/skills/adia-author/references/llm-bridge.md +7 -0
  29. package/skills/adia-author/references/token-contract.md +5 -5
  30. package/skills/adia-author/references/worked-example.md +34 -18
  31. package/skills/adia-author/references/yaml-contract.md +1 -1
  32. package/skills/adia-deploy/SKILL.md +71 -12
  33. package/skills/adia-deploy/evals/routing-corpus.json +37 -0
  34. package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
  35. package/skills/adia-dogfood/SKILL.md +71 -11
  36. package/skills/adia-dogfood/evals/routing-corpus.json +34 -0
  37. package/skills/adia-dogfood/references/admin-shell-anatomy.md +1 -1
  38. package/skills/adia-gen-review/SKILL.md +73 -7
  39. package/skills/adia-gen-review/evals/routing-corpus.json +34 -0
  40. package/skills/adia-gen-review/scripts/gen-review-decompose.mjs +5 -0
  41. package/skills/adia-llm-internals/SKILL.md +10 -6
  42. package/skills/adia-llm-internals/evals/routing-corpus.json +37 -0
  43. package/skills/adia-llm-internals/references/bridge-facade.md +2 -2
  44. package/skills/adia-llm-internals/references/streaming-sse.md +1 -1
  45. package/skills/adia-release/SKILL.md +43 -13
  46. package/skills/adia-release/evals/evals.json +24 -0
  47. package/skills/adia-release/evals/routing-corpus.json +35 -0
  48. package/skills/adia-release/references/changelog-discipline.md +15 -9
  49. package/skills/adia-release/references/cut-procedure.md +78 -24
  50. package/skills/adia-release/references/gates-catalog.md +3 -3
  51. package/skills/adia-release/references/independent-package-release.md +1 -1
  52. package/skills/adia-release/references/recovery-paths.md +4 -2
  53. package/skills/adia-release/scripts/bump.mjs +32 -1
  54. package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
  55. package/skills/adia-release/scripts/gate-roster.mjs +71 -0
  56. package/skills/adia-release/scripts/insert-stub.mjs +36 -2
  57. package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
  58. package/skills/adia-release/scripts/release-pack.mjs +315 -56
  59. package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
  60. package/skills/adia-site-docs/SKILL.md +6 -1
  61. package/skills/adia-site-docs/evals/routing-corpus.json +35 -173
  62. package/skills/adia-ssr/SKILL.md +128 -0
  63. package/skills/adia-ssr/evals/audit-report.md +63 -0
  64. package/skills/adia-ssr/evals/routing-corpus.json +34 -0
  65. package/skills/adia-ssr/references/consumer-workarounds.md +81 -0
  66. package/skills/adia-ssr/references/failure-shapes.md +164 -0
  67. package/skills/adia-ssr/references/guard-patterns.md +183 -0
  68. package/skills/adia-ssr/references/status-ledger.md +33 -0
  69. package/skills/adia-ssr/references/test-without-linkedom.md +129 -0
  70. package/agents/a2ui-engineer.md +0 -26
  71. package/agents/framework-verifier.md +0 -26
  72. package/agents/release-engineer.md +0 -26
@@ -4,8 +4,11 @@
4
4
  // Chains the 5 CLI helpers (bump · promote-unreleased · insert-stub ·
5
5
  // tag-lockstep · dispatch-publish) plus the non-helper steps (lockfile ·
6
6
  // pre-flight gates · git commit · F-N1 · push · GH releases · site
7
- // deploy) into one walked sequence with 3 operator checkpoints (before
8
- // tag, before push, before publish) per SKILL.md operator checkpoints.
7
+ // deploy) into one walked sequence with 4 confirmation boundaries (before
8
+ // tag, before push, before publish, before deploy dispatch). Under the
9
+ // single-authorization model (SKILL.md §Authorization, operator ruling
10
+ // 2026-07-17) `--go` auto-confirms all of them — the evidence blocks still
11
+ // print as a log; the granular flags below remain for cautious manual runs.
9
12
  //
10
13
  // MODES (two-phase since 2026-07-16 — invariant 3: release commits land via
11
14
  // PR, so the cut modes stop after the release commit; handoff runs the
@@ -13,7 +16,13 @@
13
16
  // --mode cut (Mode 1 — peer pre-staged [Unreleased] or pure stub; STOPS at the release commit)
14
17
  // --mode from-scratch (Mode 2 — promote [Unreleased] → [VERSION]; STOPS at the release commit)
15
18
  // --mode handoff (Mode 3 — the second phase: tag at post-merge main → publish → deploy)
16
- // --mode batch (Mode 4 — multiple merged cuts; pass --version-list)
19
+ //
20
+ // `--mode batch` is NOT implemented here — a batch push tags each version at
21
+ // its OWN release-merge SHA and enforces oldest-first publish ordering, which
22
+ // this single-version orchestrator has no model for. Passing it hard-errors
23
+ // with a pointer to the manual procedure: references/recovery-paths.md
24
+ // §Scenario 2 (Batch push). Silently falling through to the single-version
25
+ // path was the H2 audit finding this replaced.
17
26
  //
18
27
  // REQUIRED:
19
28
  // --version 0.6.22
@@ -25,26 +34,42 @@
25
34
  // --gh-notes-file <path> : GH release notes body (one per cycle)
26
35
  // --substantive "<one-line>" : for the stub block xref
27
36
  // --xref "<changelog-anchor>" : for the stub block
28
- // --substantive-packages a,b,c : packages with [Unreleased] to promote
37
+ // --substantive-packages a,b,c : packages with [Unreleased] to promote (cut AND from-scratch)
29
38
  // --stub-packages a,b,c : packages getting the lockstep stub
30
39
  //
31
40
  // FLAGS:
41
+ // --go : single-authorization — the operator's cut-start go
42
+ // covers the whole cycle; equals --yes --push --publish.
43
+ // THE standard invocation for an operator-initiated
44
+ // release. An F-N1 warn still refuses auto-confirm at
45
+ // the push boundary (a gate failure, not ceremony).
32
46
  // --dry : preview all commands, no mutation, no prompts
33
- // --yes : skip the 3 operator checkpoints (CI / scripted)
47
+ // --yes : auto-confirm the tag boundary ONLY. Tag creation is
48
+ // the least destructive of the mutating boundaries
49
+ // (moving/deleting a local-only tag is free); push and
50
+ // publish each need their own flag — --yes does not
51
+ // cover them (H5/defect-2: a single --yes used to
52
+ // flatten every boundary).
53
+ // --push : auto-confirm the push-tags boundary
54
+ // --publish : auto-confirm the publish + deploy-dispatch boundaries
34
55
  // --skip-gates : (DEBUG ONLY) skip pre-flight (don't ship anything!)
35
56
  // --skip-site : skip the EXE deploy step (rare; for offline cycles)
36
57
  //
37
58
  // Part of the adia-release skill. Mechanizes the cut procedure from
38
- // references/cut-procedure.md while preserving operator judgment at
39
- // the 3 mutating-checkpoint boundaries.
59
+ // references/cut-procedure.md. Every confirmation boundary prints its evidence
60
+ // block BEFORE the prompt (H5) — never a bare y/N — whether it waits for input
61
+ // (manual flags) or auto-confirms (--go).
40
62
 
41
63
  import { execSync } from 'node:child_process';
42
64
  import { createInterface } from 'node:readline/promises';
43
65
  import { stdin, stdout } from 'node:process';
44
66
  import fs from 'node:fs';
67
+ import os from 'node:os';
45
68
  import path from 'node:path';
46
69
  import process from 'node:process';
47
70
  import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
71
+ import { resolvePackageChangelog } from './package-paths.mjs';
72
+ import { GATE_ROSTER } from './gate-roster.mjs';
48
73
 
49
74
  const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
50
75
  const REPO = process.cwd();
@@ -70,7 +95,7 @@ function parseArgs(argv) {
70
95
  versionList: null,
71
96
  host: process.env.ADIA_DEPLOY_HOST || DEFAULT_DEPLOY_HOST,
72
97
  scope: process.env.ADIA_NPM_SCOPE || DEFAULT_NPM_SCOPE,
73
- dry: false, yes: false, skipGates: false, skipSite: false,
98
+ dry: false, yes: false, push: false, publish: false, skipGates: false, skipSite: false,
74
99
  };
75
100
  for (let i = 0; i < argv.length; i++) {
76
101
  const k = argv[i];
@@ -89,6 +114,16 @@ function parseArgs(argv) {
89
114
  else if (k === '--scope') args.scope = argv[++i];
90
115
  else if (k === '--dry') args.dry = true;
91
116
  else if (k === '--yes') args.yes = true;
117
+ else if (k === '--push') args.push = true;
118
+ else if (k === '--publish') args.publish = true;
119
+ // --go: the single-authorization model (operator ruling 2026-07-17,
120
+ // replacing the 4-checkpoint ceremony after the v0.8.5 cut spent ~40
121
+ // minutes on approval relays that produced zero safety). The operator's
122
+ // cut-start instruction covers the WHOLE cycle; the run stops only on
123
+ // gate failures. Equivalent to --yes --push --publish; the F-N1
124
+ // error hard-stop and the warn-blocks-auto-confirm rule still apply —
125
+ // those are gates, not approvals, and no flag skips them.
126
+ else if (k === '--go') { args.yes = true; args.push = true; args.publish = true; }
92
127
  else if (k === '--skip-gates') args.skipGates = true;
93
128
  else if (k === '--skip-site') args.skipSite = true;
94
129
  else if (k === '-h' || k === '--help') { help(); process.exit(0); }
@@ -98,8 +133,17 @@ function parseArgs(argv) {
98
133
  console.error(' see --help');
99
134
  process.exit(2);
100
135
  }
101
- if (!['cut', 'from-scratch', 'handoff', 'batch'].includes(args.mode)) {
102
- console.error(`error: --mode must be cut|from-scratch|handoff|batch (got: ${args.mode})`);
136
+ if (args.mode === 'batch') {
137
+ console.error('error: --mode batch is not implemented — a batch push tags each version at');
138
+ console.error(' its OWN release-merge SHA and enforces oldest-first publish ordering,');
139
+ console.error(' which this single-version orchestrator has no model for. Silently');
140
+ console.error(' falling through to the single-version path was the H2 defect this');
141
+ console.error(' replaced. Follow the manual procedure instead:');
142
+ console.error(' references/recovery-paths.md §Scenario 2 — Batch push');
143
+ process.exit(2);
144
+ }
145
+ if (!['cut', 'from-scratch', 'handoff'].includes(args.mode)) {
146
+ console.error(`error: --mode must be cut|from-scratch|handoff (got: ${args.mode})`);
103
147
  process.exit(2);
104
148
  }
105
149
  return args;
@@ -116,15 +160,31 @@ function help() {
116
160
  --substantive-packages web-components,web-modules,a2ui/corpus \\
117
161
  --stub-packages llm,a2ui/compose,a2ui/mcp,a2ui/retrieval,a2ui/runtime,a2ui/validator
118
162
 
119
- Flags: --dry (preview all) --yes (skip prompts) --skip-site (skip EXE deploy)
163
+ Flags: --go (single-authorization: the operator's cut-start go covers the
164
+ whole cycle — auto-confirms ALL checkpoints; stops only on
165
+ gate failures. THE DEFAULT for an operator-initiated release.)
166
+ --dry (preview all) --yes (auto-confirm checkpoint 1 ONLY — tag)
167
+ --push (auto-confirm checkpoint 2 — push tags)
168
+ --publish (auto-confirm checkpoint 3 — dispatch publishes)
169
+ --skip-site (skip EXE deploy)
120
170
  Instance: --host <deploy-host> (default ${DEFAULT_DEPLOY_HOST}; or $ADIA_DEPLOY_HOST)
121
171
  --scope <@org> (npm scope; default ${DEFAULT_NPM_SCOPE}; or $ADIA_NPM_SCOPE)`);
122
172
  }
123
173
 
124
174
  const rl = createInterface({ input: stdin, output: stdout });
125
- async function checkpoint(label, args) {
126
- if (args.yes) {
127
- console.log(`\n[checkpoint] ${label} auto-confirmed (--yes)`);
175
+ // Every checkpoint prints its evidence block BEFORE the y/N prompt (H5)
176
+ // `evidence` is a pre-formatted string (tag list, F-N1 results, registry
177
+ // snapshot); `autoConfirmed` is the SPECIFIC flag that covers THIS checkpoint
178
+ // (--yes for checkpoint 1 only; --push / --publish for 2 and 3 — H5/defect-2:
179
+ // a single --yes used to flatten all three).
180
+ async function checkpoint(label, evidence, autoConfirmed) {
181
+ if (evidence) {
182
+ console.log(`\n--- evidence for: ${label} ---`);
183
+ console.log(evidence);
184
+ console.log('---');
185
+ }
186
+ if (autoConfirmed) {
187
+ console.log(`\n[checkpoint] ${label} — auto-confirmed`);
128
188
  return;
129
189
  }
130
190
  const answer = await rl.question(`\n[CHECKPOINT] ${label} — proceed? [y/N]: `);
@@ -151,6 +211,24 @@ function shQuiet(cmd, args) {
151
211
  return execSync(cmd, { cwd: REPO, encoding: 'utf8' });
152
212
  }
153
213
 
214
+ // Runs a command and CAPTURES its combined output instead of streaming it —
215
+ // used where the output itself becomes evidence in a later checkpoint's
216
+ // evidence block (H5), rather than just scrolling past in the terminal.
217
+ // Never throws: F-N1 legitimately exits non-zero on cosmetic warns; the
218
+ // caller decides what a non-zero code means.
219
+ function shCapture(cmd, args) {
220
+ if (args.dry) {
221
+ console.log(` [dry] ${cmd}`);
222
+ return { code: 0, output: '[dry] (not run)' };
223
+ }
224
+ try {
225
+ const output = execSync(cmd, { cwd: REPO, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
226
+ return { code: 0, output };
227
+ } catch (e) {
228
+ return { code: e.status ?? 1, output: (e.stdout || '') + (e.stderr || '') };
229
+ }
230
+ }
231
+
154
232
  // ── Step 1 — Re-baseline ──────────────────────────────────────────
155
233
  function step1ReBaseline(args) {
156
234
  console.log('\n=== Step 1 — Re-baseline ===');
@@ -173,32 +251,40 @@ function step3PreFlight(args) {
173
251
  console.log('\n=== Step 3 — Pre-flight (SKIPPED via --skip-gates; danger!) ===');
174
252
  return;
175
253
  }
176
- console.log('\n=== Step 3 — Pre-flight gates ===');
177
- const gates = [
178
- 'node scripts/build/components.mjs --verify',
179
- 'npm run verify:traits',
180
- 'npm run check:lockstep',
181
- 'npm run test:unit',
182
- 'npm run typecheck',
183
- 'npm run check:demo-shells',
184
- 'npm run check:lightningcss-build',
185
- 'npm run smoke:engines',
186
- 'npm run smoke:register-engine',
187
- 'npm run check:links',
188
- 'npm run check:embeddings-fresh',
189
- 'npm run verify:corpus',
190
- ];
191
- for (const g of gates) {
192
- console.log(`\n ${g}`);
254
+ console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
255
+ for (const g of GATE_ROSTER) {
256
+ console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${g.cmd} # ${g.what}`);
193
257
  try {
194
- sh(g, args, { stdio: 'inherit' });
258
+ sh(g.cmd, args, { stdio: 'inherit' });
195
259
  } catch (e) {
196
- console.error(`\n ✗ FAILED: ${g}`);
260
+ console.error(`\n ✗ FAILED (${g.n}/${GATE_ROSTER.length}): ${g.cmd}`);
197
261
  console.error(' Pre-flight aborted. Fix the gate failure before proceeding.');
198
262
  console.error(' See references/gates-catalog.md for failure-mode → recovery.');
199
263
  process.exit(1);
200
264
  }
201
265
  }
266
+ console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
267
+ }
268
+
269
+ // Read a package's CHANGELOG at repo HEAD (working tree) and report whether a
270
+ // `## [Unreleased]` heading still carries non-empty content — i.e. it rode
271
+ // through un-promoted. Defect 1 (H1-adjacent, audit seed): `--mode cut` never
272
+ // called promote-unreleased.mjs at all, so a peer's hand-authored
273
+ // `## [Unreleased]` section shipped in the tarball unpromoted (bit the
274
+ // v0.8.4 cut — the plugin packages needed hand-promotion because nothing
275
+ // caught it). Returns null when clean, or a short description when dirty.
276
+ function unpromotedUnreleased(repo, pkg) {
277
+ const p = resolvePackageChangelog(repo, pkg);
278
+ if (!fs.existsSync(p)) return null;
279
+ const txt = fs.readFileSync(p, 'utf8');
280
+ const m = txt.match(/^## \[Unreleased\][^\n]*\n([\s\S]*?)(?=^## \[|$(?![\s\S]))/m);
281
+ if (!m) return null;
282
+ // Strip subsection headers (### Added, ### Fixed, …) and blank lines — what's
283
+ // left is real content, not just empty scaffolding.
284
+ const body = m[1].replace(/^###.*$/gm, '').trim();
285
+ if (body.length === 0) return null;
286
+ const firstLine = body.split('\n')[0].slice(0, 100);
287
+ return `${pkg}: "${firstLine}${firstLine.length === 100 ? '…' : ''}"`;
202
288
  }
203
289
 
204
290
  // ── Step 4 — Promote + bump + lockfile ───────────────────────────
@@ -209,7 +295,12 @@ function step4PromoteAndBump(args) {
209
295
  }
210
296
  console.log('\n=== Step 4 — Promote + Bump + Lockfile ===');
211
297
 
212
- if (args.mode === 'from-scratch' && args.substantivePackages) {
298
+ // Promotion runs for BOTH cut and from-scratch now a peer's pre-staged
299
+ // [Unreleased] content needs promoting in either mode; only the CALLER
300
+ // (whether a peer already promoted vs. this cycle authoring fresh) differs,
301
+ // not whether promotion is needed. `--substantive-packages` is the same flag
302
+ // in both modes.
303
+ if (args.substantivePackages) {
213
304
  const cmd = `node ${SCRIPT_DIR}/promote-unreleased.mjs --version ${args.version} --date ${args.date} --packages ${args.substantivePackages.join(',')}`;
214
305
  sh(cmd, args);
215
306
  }
@@ -219,9 +310,35 @@ function step4PromoteAndBump(args) {
219
310
  sh(cmd, args);
220
311
  }
221
312
 
313
+ // Loud guard: after promotion, NO package in the lockstep set (including
314
+ // the 2 plugin packages, which required hand-promotion at v0.8.4) may still
315
+ // carry a non-empty [Unreleased] — that content would ride through the
316
+ // bump unpromoted, undocumented in the tarball's own CHANGELOG.
317
+ if (!args.dry) {
318
+ const dirty = PACKAGES.map((p) => unpromotedUnreleased(REPO, p)).filter(Boolean);
319
+ if (dirty.length > 0) {
320
+ console.error(`\nERROR: ${dirty.length} package(s) still have non-empty [Unreleased] content after promotion:`);
321
+ for (const d of dirty) console.error(` ✗ ${d}`);
322
+ console.error('\n Add the missing package(s) to --substantive-packages (or --stub-packages if');
323
+ console.error(' they are pure ride-alongs) and re-run. See changelog-discipline.md §Detecting');
324
+ console.error(' source-changing packages.');
325
+ process.exit(1);
326
+ }
327
+ console.log(` ✓ no unpromoted [Unreleased] content across all ${PACKAGES.length} packages`);
328
+ }
329
+
222
330
  sh(`node ${SCRIPT_DIR}/bump.mjs --from ${args.previous} --to ${args.version}`, args);
223
331
  sh('npm install --package-lock-only --no-audit --no-fund', args);
224
332
  sh('npm run check:lockstep', args);
333
+
334
+ // Step 4f — pre-tag F-N1 coverage with auto-fix, run with the AUTHORITATIVE
335
+ // matcher (check-release.mjs itself, not the drifted 9-package
336
+ // check-changelog-coverage.mjs). Appends verified Maintenance bullets for
337
+ // any changed-but-unmentioned directory so the post-tag F-N1 (Step 7) is a
338
+ // formality. The v0.8.5 cut lacked this and paid 3 enrichment PRs + 3
339
+ // delete/retag rounds discovering the same gaps only after tagging.
340
+ console.log('\n Step 4f — pre-tag coverage (+auto-fix) with the authoritative F-N1 matcher:');
341
+ sh(`node scripts/release/check-release.mjs --pending-version ${args.version} --fix`, args);
225
342
  }
226
343
 
227
344
  // ── Step 5 — Commit ──────────────────────────────────────────────
@@ -262,23 +379,45 @@ function step5Commit(args) {
262
379
  sh(`git commit -F ${args.commitMessageFile}`, args);
263
380
  }
264
381
 
265
- // ── Step 6 — Tag ─────────────────────────────────────────────────
382
+ // ── Step 6 — Tag ← operator checkpoint 1 (before tagging) ────────
266
383
  async function step6Tag(args) {
267
384
  console.log('\n=== Step 6 — Tag at HEAD ===');
268
- await checkpoint('Ready to create 12 tags (umbrella + 11 per-package)', args);
385
+ const tagList = [`v${args.version}`, ...PACKAGES.map((p) => `${p}-v${args.version}`)];
386
+ const evidence = `Planned tags (${tagList.length} — umbrella + ${PACKAGES.length} per-package):\n` +
387
+ tagList.map((t) => ` ${t}`).join('\n');
388
+ // --yes covers ONLY this checkpoint (defect-2/H5) — moving/deleting a
389
+ // local-only tag is free, so this is the least destructive of the three.
390
+ await checkpoint('Create tags at HEAD — before tagging', evidence, args.yes);
269
391
  sh(`node ${SCRIPT_DIR}/tag-lockstep.mjs --version ${args.version}`, args);
392
+ console.log(` ✓ created ${tagList.length} tags`);
270
393
  }
271
394
 
272
- // ── Step 7 — F-N1 ────────────────────────────────────────────────
395
+ // ── Step 7 — F-N1 ─────────────────────────────────────────────────
396
+ // Captures output (rather than streaming it) so checkpoint 2 can show the
397
+ // per-package F-N1 results as evidence BEFORE the push prompt (H5) — the F-N1
398
+ // output used to scroll past in Step 7 with no connection to Step 8's prompt.
273
399
  function step7Fn1(args) {
274
400
  console.log('\n=== Step 7 — F-N1 release trip-wire ===');
275
- try {
276
- sh('node scripts/release/check-release.mjs --all-pending', args);
277
- } catch (e) {
278
- // F-N1 sometimes exits non-zero on cosmetic warns; show the output and let the operator decide
279
- console.warn('\n F-N1 returned non-zero review the warns above.');
280
- console.warn(' See references/changelog-discipline.md § F-N1 diff-coverage enrichment.');
401
+ const { code, output } = shCapture('node scripts/release/check-release.mjs --all-pending', args);
402
+ console.log(output);
403
+ // CodeRabbit (PR #289): a non-zero exit here (without --strict, which this
404
+ // call doesn't pass) is ALWAYS a real `error` finding check-release.mjs
405
+ // only sets a non-zero code for `error` severity; cosmetic `warn`s leave it
406
+ // 0. Treating non-zero as "just review the warns" let --push auto-confirm
407
+ // straight past a known trip-wire failure. Fail closed: hard-stop HERE,
408
+ // before Step 8 exists at all — no flag can skip this.
409
+ if (code !== 0 && !args.dry) {
410
+ console.error('\n ✗ F-N1 reported an ERROR (not a cosmetic warn) — release-pack stops here.');
411
+ console.error(' Fix the finding above, land it as a new commit through the PR flow (the');
412
+ console.error(' release commit is already merged — see changelog-discipline.md §F-N1');
413
+ console.error(' diff-coverage enrichment), delete + re-create the tags, re-run.');
414
+ process.exit(1);
281
415
  }
416
+ // Cosmetic warns (exit 0, but the output carries `[warn]` lines) are real
417
+ // signal an operator should read before pushing — never let --push silently
418
+ // wave them through; only the interactive prompt may proceed past a warn.
419
+ const hasWarn = /\[warn\]/.test(output);
420
+ return { output, hasWarn };
282
421
  }
283
422
 
284
423
  // ── Step 8 — Push ────────────────────────────────────────────────
@@ -296,7 +435,7 @@ function step7Fn1(args) {
296
435
  // orchestrated cut does NOT rely on push-on-tag firing. The one-at-a-time push
297
436
  // is defensive (a cut aborted after Step 8 still has its publishes triggered)
298
437
  // AND keeps the standalone tag-lockstep.mjs hint honest.
299
- async function step8Push(args) {
438
+ async function step8Push(args, fn1) {
300
439
  console.log('\n=== Step 8 — Push tags (per-tag, NOT batched) ===');
301
440
  // Invariant 3: main reached this state via a merged release PR — never a
302
441
  // direct push from here. Assert we're tagging the pushed history.
@@ -305,14 +444,22 @@ async function step8Push(args) {
305
444
  console.error(`ERROR: HEAD is ${ahead} commit(s) ahead of origin/main — release commits land via PR (invariant 3); merge first, then tag at post-merge main.`);
306
445
  process.exit(1);
307
446
  }
308
- await checkpoint('Ready to push 12 tags (one-at-a-time)', args);
309
447
  const perPkgTags = PACKAGES.map((p) => `${p}-v${args.version}`);
448
+ const evidence = `F-N1 results (Step 7):\n${fn1?.output || '(none captured)'}\n` +
449
+ `Tags to push (${perPkgTags.length + 1} — one push per tag, umbrella last):\n` +
450
+ [...perPkgTags, `v${args.version}`].map((t) => ` ${t}`).join('\n');
451
+ // --push is its OWN flag — NOT covered by --yes (defect-2/H5). CodeRabbit
452
+ // (PR #289): a cosmetic F-N1 warn must still force the interactive prompt —
453
+ // --push may NOT auto-confirm past ANY F-N1 finding, only a fully clean
454
+ // run. (A real F-N1 error never reaches here — step7Fn1 hard-exits first.)
455
+ const autoConfirm = args.push && !fn1?.hasWarn;
456
+ await checkpoint('Push tags — before pushing', evidence, autoConfirm);
310
457
  for (const t of perPkgTags) {
311
458
  sh(`git push origin ${t}`, args);
312
459
  }
313
460
  // Umbrella tag last — triggers no workflow (convention only).
314
461
  sh(`git push origin v${args.version}`, args);
315
- console.log(`\n ${perPkgTags.length} per-package tags + 1 umbrella pushed individually.`);
462
+ console.log(`\n ✓ pushed ${perPkgTags.length} per-package tags + 1 umbrella tag`);
316
463
  console.log(' NOTE: a single `git push origin <all tags>` can skip ALL publish-on-tag');
317
464
  console.log(' triggers (the GitHub batch-push skip). Step 9 dispatches via');
318
465
  console.log(' workflow_dispatch + verifies each triggered — see recovery-paths.md §Scenario 7.');
@@ -324,9 +471,30 @@ async function step8Push(args) {
324
471
  // exist for the <pkg>-vX.Y.Z tags and re-dispatches ONLY the misses — closing the
325
472
  // batch-tag-push skip without double-publishing the ones that already fired.
326
473
  // Idempotent: if all 11 triggered (or none did), it does the right thing.
474
+ // Snapshot the CURRENT registry state — the "ordering evidence" for
475
+ // checkpoint 3 (H5): what's live right now, before anything is dispatched,
476
+ // so the operator can confirm publish order/target against reality rather
477
+ // than a bare y/N.
478
+ function registrySnapshot(args) {
479
+ if (args.dry) return '[dry] (registry snapshot skipped)';
480
+ const lines = [];
481
+ for (const pkg of PACKAGES) {
482
+ let v;
483
+ try { v = execSync(`npm view "${args.scope}/${pkg}" version`, { encoding: 'utf8' }).trim(); }
484
+ catch { v = 'MISSING/E404'; }
485
+ lines.push(` ${pkg}: ${v}${v === args.version ? ' (already at target — idempotent re-dispatch)' : ''}`);
486
+ }
487
+ let latest = 'unknown';
488
+ try { latest = execSync(`npm view ${args.scope}/web-components dist-tags.latest`, { encoding: 'utf8' }).trim(); } catch { /* offline */ }
489
+ lines.push(` dist-tags.latest: ${latest}`);
490
+ return `Registry state BEFORE publish (target = ${args.version}):\n${lines.join('\n')}`;
491
+ }
492
+
327
493
  async function step9Publish(args) {
328
494
  console.log('\n=== Step 9 — Verify-triggered + re-dispatch misses ===');
329
- await checkpoint('Ready to verify the 11 publish triggers + re-dispatch any misses', args);
495
+ const evidence = registrySnapshot(args);
496
+ // --publish is its OWN flag — NOT covered by --yes (defect-2/H5).
497
+ await checkpoint('Dispatch publishes — before publishing', evidence, args.publish);
330
498
  // Give GH a moment to index the runs created by the per-tag push in Step 8.
331
499
  if (!args.dry) sh('sleep 6', args);
332
500
  sh(`node ${SCRIPT_DIR}/dispatch-publish.mjs --version ${args.version} --scope ${args.scope} --verify-triggered`, args);
@@ -335,7 +503,7 @@ async function step9Publish(args) {
335
503
  console.log(' ✓ publishes settled');
336
504
  // Fail-closed: the registry is the verify target (SKILL.md) — a missing or
337
505
  // stale package aborts the cut here, before GH releases + site deploy.
338
- console.log(`\n Verify all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
506
+ console.log(`\n Post-publish registry verification, all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
339
507
  sh(
340
508
  `fail=0; ` +
341
509
  `for pkg in ${PACKAGES.join(' ')}; do ` +
@@ -369,8 +537,24 @@ async function step10GhAndSite(args) {
369
537
  console.log(' --skip-site set; skipping EXE deploy');
370
538
  return;
371
539
  }
372
- sh('npm run build:site', args);
373
- sh(`rsync -az --delete dist/ ${args.host}:/srv/adia-ui/dist/`, args);
540
+ // H1 (forge-campaign gh#268 audit): this used to `npm run build:site` +
541
+ // raw `rsync -az --delete` straight to the host — bypassing deploy-site.yml
542
+ // entirely (its build → pre-flight verify → snapshot → rsync → post-deploy
543
+ // verify → auto-rollback pipeline), even though cut-procedure.md §Step 10
544
+ // already documented the workflow-dispatch path as the correct one. A raw
545
+ // local rsync has no snapshot to roll back to on a bad deploy. Dispatch the
546
+ // real pipeline instead — gated behind its own confirm (deploy is
547
+ // outward-facing exactly like publish; --publish covers it, or an
548
+ // interactive confirm if not given).
549
+ const evidence = `Dispatching "Deploy site (${args.host})" (.github/workflows/deploy-site.yml) —\n` +
550
+ `builds from main, runs pre-flight verify → snapshot → hardened rsync →\n` +
551
+ `post-deploy verify → auto-rollback. NOT a raw rsync (the bypass this replaced).`;
552
+ await checkpoint('Deploy site — before dispatching deploy-site.yml', evidence, args.publish);
553
+ sh(`gh workflow run "Deploy site (${args.host})" --repo adiahealth/gen-ui-kit --ref main`, args);
554
+ console.log(' Waiting for the deploy workflow to settle...');
555
+ if (!args.dry) sh('sleep 10', args);
556
+ sh(`until [ "$(gh run list --workflow=deploy-site.yml --limit 1 --json status -q '.[0].status')" = "completed" ]; do sleep 5; done`, args);
557
+ console.log(' ✓ deploy-site.yml settled');
374
558
  // Verify a deployed CONTENT FILE, never an SPA route — the docs site
375
559
  // returns HTTP 200 + a blank shell for ANY unmatched route (v0.7.4 trap).
376
560
  sh(`curl -s -o /dev/null -w "EXE host.css: HTTP %{http_code}\\n" https://${args.host}/packages/web-components/styles/host.css`, args);
@@ -405,16 +589,91 @@ async function main() {
405
589
  return;
406
590
  }
407
591
  await step6Tag(args);
408
- step7Fn1(args);
409
- await step8Push(args);
592
+ const fn1 = step7Fn1(args);
593
+ await step8Push(args, fn1);
410
594
  await step9Publish(args);
411
595
  await step10GhAndSite(args);
412
596
  rl.close();
413
597
  console.log('\n✓ release-pack complete. Verify against the npm registry + GH releases, then author the release notes.');
414
598
  }
415
599
 
416
- main().catch((e) => {
417
- console.error(e);
600
+ // Plan-assembly selftest — spawns THIS script as a real subprocess with
601
+ // --mode cut --dry (fully mutation-free per every step's own dry-guard) and
602
+ // asserts the assembled plan carries the expected markers: the full
603
+ // 17-gate roster (H3 — a subset run must be impossible without editing
604
+ // gate-roster.mjs), and the invariant-3 stop-before-PR message (cut mode
605
+ // must never proceed to tag/push/publish on its own). A fixture temp dir
606
+ // supplies the operator-authored content files (--commit-message-file /
607
+ // --gh-notes-file) so nothing here depends on real repo state.
608
+ function selftest() {
609
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'release-pack-selftest-'));
610
+ try {
611
+ const commitFile = path.join(tmp, 'commit.txt');
612
+ const notesFile = path.join(tmp, 'notes.md');
613
+ fs.writeFileSync(commitFile, 'selftest fixture commit message\n');
614
+ fs.writeFileSync(notesFile, 'selftest fixture notes\n');
615
+
616
+ const scriptPath = fileURLToPathCompat(import.meta.url);
617
+ let out;
618
+ try {
619
+ out = execSync(
620
+ `node "${scriptPath}" --mode cut --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 ` +
621
+ `--commit-message-file "${commitFile}" --gh-notes-file "${notesFile}" ` +
622
+ `--substantive-packages web-components --stub-packages llm --dry --yes`,
623
+ { cwd: REPO, encoding: 'utf8' },
624
+ );
625
+ } catch (e) {
626
+ console.error('selftest FAIL: --mode cut --dry plan assembly exited non-zero');
627
+ console.error(e.stdout || e.message);
628
+ process.exit(1);
629
+ }
630
+
631
+ const need = [
632
+ `${GATE_ROSTER.length}-gate roster`,
633
+ `✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`,
634
+ 'STOPPING per invariant 3',
635
+ ];
636
+ for (const marker of need) {
637
+ if (!out.includes(marker)) {
638
+ console.error(`selftest FAIL: expected marker not found in plan output: "${marker}"`);
639
+ process.exit(1);
640
+ }
641
+ }
642
+
643
+ // The batch-mode hard-reject (H2) must fire BEFORE any of the above runs.
644
+ let batchOut = '';
645
+ let batchFailed = false;
646
+ try {
647
+ execSync(`node "${scriptPath}" --mode batch --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8`, { cwd: REPO, encoding: 'utf8' });
648
+ } catch (e) {
649
+ batchFailed = true;
650
+ batchOut = (e.stdout || '') + (e.stderr || '');
651
+ }
652
+ if (!batchFailed || !batchOut.includes('Scenario 2')) {
653
+ console.error('selftest FAIL: --mode batch must hard-reject with a pointer to recovery-paths.md §Scenario 2');
654
+ process.exit(1);
655
+ }
656
+
657
+ console.log('selftest OK');
658
+ } finally {
659
+ fs.rmSync(tmp, { recursive: true, force: true });
660
+ }
661
+ }
662
+
663
+ // import.meta.url → filesystem path (works the same on the platforms this
664
+ // skill runs on; avoids pulling in node:url's fileURLToPath just for this).
665
+ function fileURLToPathCompat(url) {
666
+ return new URL(url).pathname;
667
+ }
668
+
669
+ const topArgv = process.argv.slice(2);
670
+ if (topArgv[0] === 'selftest') {
418
671
  rl.close();
419
- process.exit(1);
420
- });
672
+ selftest();
673
+ } else {
674
+ main().catch((e) => {
675
+ console.error(e);
676
+ rl.close();
677
+ process.exit(1);
678
+ });
679
+ }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // tag-lockstep.mjs — create 10 lockstep tags (umbrella vX.Y.Z + 9
3
- // per-package <pkg>-vX.Y.Z) at HEAD or at a specified SHA.
2
+ // tag-lockstep.mjs — create 12 lockstep tags (umbrella vX.Y.Z + 11
3
+ // per-package <pkg>-vX.Y.Z, including the 2 plugin packages that joined
4
+ // the lockstep 2026-07-15) at HEAD or at a specified SHA.
4
5
  //
5
6
  // Usage:
6
7
  // node tag-lockstep.mjs --version 0.6.22
@@ -114,9 +115,30 @@ function main() {
114
115
  }
115
116
  }
116
117
 
117
- try {
118
- main();
119
- } catch (e) {
120
- console.error(`error: ${e.message}`);
121
- process.exit(1);
118
+ function selftest() {
119
+ const tags = buildTagList('1.2.3');
120
+ if (tags.length !== 12) {
121
+ console.error(`selftest FAIL: expected 12 tags (umbrella + 11 per-package), got ${tags.length}`); process.exit(1);
122
+ }
123
+ if (tags[0] !== 'v1.2.3') {
124
+ console.error('selftest FAIL: umbrella tag must be first'); process.exit(1);
125
+ }
126
+ for (const expected of ['web-components-v1.2.3', 'adia-ui-factory-v1.2.3', 'adia-ui-forge-v1.2.3']) {
127
+ if (!tags.includes(expected)) {
128
+ console.error(`selftest FAIL: expected tag '${expected}' missing from buildTagList output`); process.exit(1);
129
+ }
130
+ }
131
+ console.log('selftest OK');
132
+ }
133
+
134
+ const topArgv = process.argv.slice(2);
135
+ if (topArgv[0] === 'selftest') {
136
+ selftest();
137
+ } else {
138
+ try {
139
+ main();
140
+ } catch (e) {
141
+ console.error(`error: ${e.message}`);
142
+ process.exit(1);
143
+ }
122
144
  }
@@ -35,7 +35,7 @@ skill routes to it and enforces it, it does not restate it.
35
35
  | Task shape | Do |
36
36
  | --- | --- |
37
37
  | New page in an owned category | Read the convention doc's template section; copy the shared skeleton from a sibling page in the same category (narrative archetype vs. reference/gallery archetype). |
38
- | Callout / "meta content" block | `<alert-ui variant="warning\|info\|...">` with `<div slot="content">` (never `<span>` — the transpiler only treats `p`/`div`/`ul`/`ol` as prose; a `<span>` silently reorders mixed inline content on regen) for rich text — never a bare `data-*` attribute. A whole multi-heading section aimed at a different reader (not a short callout) is NOT `alert-ui`'s job either (wrong shape)currently there's no working distinguishing treatment for this case beyond the heading text itself; see the convention doc's rule 2 for what's been tried and why it doesn't render. |
38
+ | Callout / "meta content" block | `<alert-ui variant="warning\|info\|...">` with `<div slot="content">` (never `<span>` — the transpiler only treats `p`/`div`/`ul`/`ol` as prose; a `<span>` silently reorders mixed inline content on regen) for rich text — never a bare `data-*` attribute. A whole multi-heading section aimed at a different reader (not a short callout) is NOT `alert-ui`'s job either wrong shape — the convention doc's rule 2 carries the working treatment (eyebrow `<tag-ui>` title-row, TKT-0011). |
39
39
  | Tamed admin-shell demo | Add `class="demo-frame"` to the shell instance + a page-local `<style>` block setting only `--demo-frame-height` (and any genuinely page-specific extra). That's `site/site.css`'s `.demo-frame` utility already covering `position`/`border`/`overflow`/`.demo-body` padding. |
40
40
  | A surface needs to read as "its own distinct object" against the page background (a chip, a pill) | Reuse `--a-canvas-well-strong` (`packages/web-components/styles/colors/semantics/core.css`) — verified ≥3:1 (WCAG 2.2 SC 1.4.11) in both schemes. `--a-canvas-well` alone is for a subtly-sunken panel; a new candidate token always runs through `color:color-verify` first. |
41
41
  | "Is this page consistent?" / review request | Check the page against the convention doc's rules 1–4 by name; a finding names the specific rule violated, not just "this looks off." |
@@ -63,6 +63,11 @@ A check that cannot run (missing script, no network for a build step) is a
63
63
  named blocker in the report — flag it and stop; never mark the page done on
64
64
  an assumed pass.
65
65
 
66
+ The fresh-context critic for an authored page is the `framework-reviewer`
67
+ agent (its dogfood visual probe covers rendered site surfaces) — the author
68
+ never certifies their own page's rendered result; the mechanical checks
69
+ above plus that read-only pass together are the review.
70
+
66
71
  Done when the touched page(s) pass the checks above and, for a reviewed
67
72
  page, every finding cites the specific rule in `site-pages-authoring.md` it
68
73
  violates.