@adia-ai/adia-ui-forge 0.8.59 → 0.8.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -0
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/references/contracts/a2ui-mcp-surface.md +1 -1
- package/scripts/lint-rules.generated.mjs +2079 -346
- package/skills/a2ui-maintenance/references/pipeline-overview.md +24 -0
- package/skills/gen-ui-review/scripts/gen-review-decompose.mjs +27 -34
- package/skills/gen-ui-review/scripts/overflow-detect.generated.mjs +101 -0
- package/skills/package-release/references/cut-procedure.md +49 -5
- package/skills/package-release/references/recovery-paths.md +20 -0
- package/skills/package-release/scripts/bump.mjs +36 -6
- package/skills/package-release/scripts/release-pack.mjs +424 -45
- package/skills/primitive-authoring/references/yaml-contract.md +106 -9
|
@@ -71,6 +71,11 @@ import process from 'node:process';
|
|
|
71
71
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
72
72
|
import { resolvePackageChangelog, PACKAGE_ROSTER } from './package-paths.mjs';
|
|
73
73
|
import { GATE_ROSTER } from './gate-roster.mjs';
|
|
74
|
+
// gh#3361: Step 5's releaseFiles derives its PINNED_REFS-covered entries
|
|
75
|
+
// from bump.mjs's own tables (below, pinnedRefFiles()) instead of
|
|
76
|
+
// hand-duplicating them — bump.mjs guards its own main()/selftest() behind
|
|
77
|
+
// an entry-point check specifically so this import is side-effect-free.
|
|
78
|
+
import { PINNED_REFS, REPO_PINNED_REFS } from './bump.mjs';
|
|
74
79
|
|
|
75
80
|
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
76
81
|
const REPO = process.cwd();
|
|
@@ -118,6 +123,7 @@ function parseArgs(argv) {
|
|
|
118
123
|
host: process.env.ADIA_DEPLOY_HOST || DEFAULT_DEPLOY_HOST,
|
|
119
124
|
scope: process.env.ADIA_NPM_SCOPE || DEFAULT_NPM_SCOPE,
|
|
120
125
|
dry: false, yes: false, push: false, publish: false, skipGates: false, skipSite: false,
|
|
126
|
+
fromStep10: false,
|
|
121
127
|
};
|
|
122
128
|
for (let i = 0; i < argv.length; i++) {
|
|
123
129
|
const k = argv[i];
|
|
@@ -148,6 +154,15 @@ function parseArgs(argv) {
|
|
|
148
154
|
else if (k === '--go') { args.yes = true; args.push = true; args.publish = true; }
|
|
149
155
|
else if (k === '--skip-gates') args.skipGates = true;
|
|
150
156
|
else if (k === '--skip-site') args.skipSite = true;
|
|
157
|
+
// gh#3342: resume a handoff that already tagged/pushed/published — a
|
|
158
|
+
// straight re-run of `--mode handoff` re-gates (Step 3 never runs in
|
|
159
|
+
// handoff mode, but Step 6 would re-tag at HEAD, which may no longer be
|
|
160
|
+
// the tagged commit if anything merged since) and Step 9 would
|
|
161
|
+
// re-dispatch publishes that already succeeded (idempotent, but a
|
|
162
|
+
// needless wait). `--from-step10` skips straight to Step 10 (GH
|
|
163
|
+
// releases + site deploy) — the exact "resume from Step 10 only" shape
|
|
164
|
+
// the v0.8.59 cut needed by hand (a one-off script, never checked in).
|
|
165
|
+
else if (k === '--from-step10') args.fromStep10 = true;
|
|
151
166
|
else if (k === '-h' || k === '--help') { help(); process.exit(0); }
|
|
152
167
|
}
|
|
153
168
|
if (!args.version || !args.date || !args.previous || !args.mode) {
|
|
@@ -235,6 +250,10 @@ function parseArgs(argv) {
|
|
|
235
250
|
console.error(`error: GH notes file not found or not a regular file: ${args.ghNotesFile}`);
|
|
236
251
|
process.exit(2);
|
|
237
252
|
}
|
|
253
|
+
} else if (args.fromStep10) {
|
|
254
|
+
console.error('error: --from-step10 only applies to --mode handoff (it resumes handoff\'s own');
|
|
255
|
+
console.error(' tag→publish→deploy sequence at its last step) — got --mode ' + args.mode);
|
|
256
|
+
process.exit(2);
|
|
238
257
|
}
|
|
239
258
|
return args;
|
|
240
259
|
}
|
|
@@ -259,8 +278,13 @@ Flags: --go (single-authorization: the operator's cut-start go covers the
|
|
|
259
278
|
--push (auto-confirm checkpoint 2 — push tags)
|
|
260
279
|
--publish (auto-confirm checkpoint 3 — dispatch publishes)
|
|
261
280
|
--skip-site (skip EXE deploy)
|
|
281
|
+
--from-step10 (--mode handoff only: resume at Step 10 — GH releases +
|
|
282
|
+
site deploy — skipping the pre-flight gates and re-tagging;
|
|
283
|
+
use once Steps 1-9 are independently confirmed done. See
|
|
284
|
+
recovery-paths.md §Scenario 9.)
|
|
262
285
|
Instance: --host <deploy-host> (default ${DEFAULT_DEPLOY_HOST}; or $ADIA_DEPLOY_HOST)
|
|
263
|
-
--scope <@org> (npm scope; default ${DEFAULT_NPM_SCOPE}; or $ADIA_NPM_SCOPE)
|
|
286
|
+
--scope <@org> (npm scope; default ${DEFAULT_NPM_SCOPE}; or $ADIA_NPM_SCOPE)
|
|
287
|
+
$REGISTRY_POLL_MINUTES (Step 9 registry-poll window; default 30)`);
|
|
264
288
|
}
|
|
265
289
|
|
|
266
290
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
@@ -740,6 +764,27 @@ async function step3PreFlight(args) {
|
|
|
740
764
|
return;
|
|
741
765
|
}
|
|
742
766
|
console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
|
|
767
|
+
|
|
768
|
+
// gh#3342: gate 4 (test:unit:serial) runs package-root `*.test.js` files
|
|
769
|
+
// for llm/agent/persona against their BUILT tsc output — agent.test.js's
|
|
770
|
+
// own docstring: "run against the BUILT output ... npm run build
|
|
771
|
+
// -w @adia-ai/agent first"; persona.test.js imports './index.js' directly;
|
|
772
|
+
// llm/core carries a dedicated dist-check.test.js that asserts the emitted
|
|
773
|
+
// artifacts exist and explicitly does NOT build them itself ("callers
|
|
774
|
+
// should run npm run build first"). All three packages' emitted .js/.d.ts
|
|
775
|
+
// are gitignored (packages/llm/agent/.gitignore: "Generated by npm run
|
|
776
|
+
// build ... not tracked in git"), so a fresh cut clone has none of them.
|
|
777
|
+
// cut-procedure.md §3.0's harvest preamble names `npm run build
|
|
778
|
+
// -w @adia-ai/llm` but that whole block is CONDITIONAL on source content
|
|
779
|
+
// having changed in the window, and never named agent/persona at all — a
|
|
780
|
+
// cut with no §3.0 trigger skipped the build outright and gate 4 failed
|
|
781
|
+
// on missing dist (the v0.8.59 cut). Unlike §3.0's regen outputs, these
|
|
782
|
+
// are plain build artifacts, not content-conditional — build them
|
|
783
|
+
// unconditionally, every cut, before gate 4 ever runs. Runs before the
|
|
784
|
+
// `--dry` early-return below so a dry preview shows it too.
|
|
785
|
+
console.log('\n-- Precondition: tsc build (llm, agent, persona — gate 4 needs their built output) --');
|
|
786
|
+
sh('npm run build -w @adia-ai/llm -w @adia-ai/agent -w @adia-ai/persona', args);
|
|
787
|
+
|
|
743
788
|
if (args.dry) {
|
|
744
789
|
step3PreFlightDry(args);
|
|
745
790
|
return;
|
|
@@ -973,6 +1018,31 @@ function step4PromoteAndBump(args) {
|
|
|
973
1018
|
console.log(' ✓ Hermes/Pi plugin manifests fresh after regeneration');
|
|
974
1019
|
}
|
|
975
1020
|
|
|
1021
|
+
// Step 4d.8 — rebuild the dist CSS+JS bundles (gh#3342): bump.mjs's
|
|
1022
|
+
// web-components PINNED_REFS entry moves icons-cdn.js's hand-typed
|
|
1023
|
+
// PACKAGE_VERSION literal every cut (gh#3240/invariant 9), and that
|
|
1024
|
+
// literal is embedded verbatim in three committed dist bundles
|
|
1025
|
+
// (web-components.min.js, everything.min.js, chat-shell.min.js) —
|
|
1026
|
+
// same "derived artifact embeds the lockstep version" class as 4d.5-4d.7
|
|
1027
|
+
// above. Gate 9 (check:js-bundles-fresh) runs pre-bump in Step 3 and only
|
|
1028
|
+
// proves freshness against the PREVIOUS version; relying on the
|
|
1029
|
+
// derived-resync bot to rebuild post-merge left gate 9 red in the
|
|
1030
|
+
// handoff pre-flight (AGENTS.md: committed dist/ bundles are PR-blocking,
|
|
1031
|
+
// Class C — never a derived-resync-only artifact). Rebuild here, under
|
|
1032
|
+
// the same npm-ci layout Step 1 already asserted for gate 9, and verify:
|
|
1033
|
+
console.log('\n Step 4d.8 — rebuild dist CSS+JS bundles (icons-cdn.js pin embeds the lockstep bump):');
|
|
1034
|
+
sh('npm run build:bundles', args);
|
|
1035
|
+
if (args.dry) {
|
|
1036
|
+
console.log(' [dry] npm run check:css-bundles-fresh && npm run check:js-bundles-fresh (post-rebuild proof)');
|
|
1037
|
+
} else if (!shOk('npm run check:css-bundles-fresh') || !shOk('npm run check:js-bundles-fresh')) {
|
|
1038
|
+
console.error('\nERROR: dist bundles still stale after rebuild — build:bundles did not produce');
|
|
1039
|
+
console.error(' output matching check:css-bundles-fresh/check:js-bundles-fresh\'s expectations.');
|
|
1040
|
+
console.error(' Run both by hand to diagnose before re-running the cut.');
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
} else {
|
|
1043
|
+
console.log(' ✓ dist CSS+JS bundles fresh after rebuild');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
976
1046
|
// Step 4e — generate docs/ops/releases/vX.Y.Z.md (gh#399: neither this step nor
|
|
977
1047
|
// its staging into the release commit existed before — check-cut-hygiene's
|
|
978
1048
|
// "notes present" claim (Step 4g, below) went unverified until
|
|
@@ -1002,6 +1072,27 @@ function step4PromoteAndBump(args) {
|
|
|
1002
1072
|
}
|
|
1003
1073
|
}
|
|
1004
1074
|
|
|
1075
|
+
// gh#3361: derive Step 5's PINNED_REFS-covered allowlist entries directly
|
|
1076
|
+
// from bump.mjs's own tables, instead of hand-listing the same paths a
|
|
1077
|
+
// second time — the drift class this closes structurally (gh#1198, gh#1899,
|
|
1078
|
+
// gh#1954, gh#2473, gh#3342): every one of those was bump.mjs gaining a new
|
|
1079
|
+
// pinned file that release-pack.mjs's Step 5 didn't independently know
|
|
1080
|
+
// about. A package-keyed entry becomes `<pkgDir>/<file>`; a repo-level entry
|
|
1081
|
+
// (REPO_PINNED_REFS, e.g. the root README's "Current version" claim) is
|
|
1082
|
+
// already repo-root-relative. Pure — the selftest below asserts the
|
|
1083
|
+
// derivation against a fixture table, and separately that it agrees with
|
|
1084
|
+
// the REAL imported PINNED_REFS/REPO_PINNED_REFS for every entry currently
|
|
1085
|
+
// known (so an entry added to bump.mjs without a corresponding
|
|
1086
|
+
// release-pack.mjs edit still shows up here for free).
|
|
1087
|
+
function pinnedRefFiles(pinnedRefs, repoPinnedRefs) {
|
|
1088
|
+
const files = [];
|
|
1089
|
+
for (const [pkgDir, cfgs] of Object.entries(pinnedRefs)) {
|
|
1090
|
+
for (const cfg of cfgs) files.push(`${pkgDir}/${cfg.file}`);
|
|
1091
|
+
}
|
|
1092
|
+
for (const cfg of repoPinnedRefs) files.push(cfg.file);
|
|
1093
|
+
return files;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1005
1096
|
// Pure so the selftest can lock the parse without a real git tree (gh#2473,
|
|
1006
1097
|
// same "pure so selftest can lock the transform" shape as bump.mjs's
|
|
1007
1098
|
// bumpPinnedRef). Takes `git status --porcelain` output, returns the tracked
|
|
@@ -1069,10 +1160,10 @@ function step5Commit(args) {
|
|
|
1069
1160
|
sh('git reset HEAD >/dev/null 2>&1 || true', args);
|
|
1070
1161
|
const releaseFiles = [
|
|
1071
1162
|
'package-lock.json',
|
|
1072
|
-
// Root README's "Current version" claim
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1163
|
+
// Root README's "Current version" claim (gate 18, check-cut-hygiene
|
|
1164
|
+
// doc-currency, exact-matches it against the cut version) comes from
|
|
1165
|
+
// pinnedRefFiles(REPO_PINNED_REFS) below — bump.mjs's own REPO_PINNED_REFS
|
|
1166
|
+
// table names this exact file (gh#3361).
|
|
1076
1167
|
// Root CHANGELOG — the gh#453 promotion machinery renames its
|
|
1077
1168
|
// [Unreleased] heading at Step 4, but this roster predated it, so the
|
|
1078
1169
|
// v0.8.20 cut promoted the heading and then shipped WITHOUT it (the
|
|
@@ -1097,6 +1188,17 @@ function step5Commit(args) {
|
|
|
1097
1188
|
'packages/genui/adia-catalog/adia.agent.json',
|
|
1098
1189
|
'packages/genui/adia-catalog/adia.shells.json',
|
|
1099
1190
|
'packages/genui/adia-catalog/catalog-data.js',
|
|
1191
|
+
// gh#3342/gh#3361: packages/web-components/core/icons-cdn.js (bump.mjs's
|
|
1192
|
+
// PACKAGE_VERSION pin, fifth instance of this drift class — gh#1198,
|
|
1193
|
+
// gh#1899, gh#1954, gh#2473) comes from pinnedRefFiles(PINNED_REFS)
|
|
1194
|
+
// below, not a hand-typed entry — bump.mjs's own PINNED_REFS table names
|
|
1195
|
+
// it, so it can't drift out of this list again.
|
|
1196
|
+
// gh#3342: Step 4d.8 rebuilds the dist CSS+JS bundles post-bump (the
|
|
1197
|
+
// icons-cdn.js PACKAGE_VERSION pin above is embedded in three of them) —
|
|
1198
|
+
// directory pathspecs, `git add` stages whatever the rebuild actually
|
|
1199
|
+
// changed, same shape as the chunk-corpus/skills-glob entries below.
|
|
1200
|
+
'packages/web-components/dist',
|
|
1201
|
+
'packages/web-modules/dist',
|
|
1100
1202
|
// gh#1361: Step 4d.5b regenerates the catalog tier index post-bump (it
|
|
1101
1203
|
// derives from the catalog derive-genui-catalog.mjs just refreshed) and,
|
|
1102
1204
|
// when that regen goes stale against the chunk harvester's recorded
|
|
@@ -1147,19 +1249,25 @@ function step5Commit(args) {
|
|
|
1147
1249
|
// Codex `agents/openai.yaml` glob above is.
|
|
1148
1250
|
files.push(`${dir}/plugin.yaml`, `${dir}/__init__.py`, `${dir}/prompts/*.md`);
|
|
1149
1251
|
}
|
|
1150
|
-
// gh#
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
|
|
1157
|
-
// (below) started catching it.
|
|
1158
|
-
if (name === 'adia-ui-factory') files.push(`${dir}/.mcp.json`, `${dir}/hermes-mcp.yaml`, `${dir}/README.md`);
|
|
1252
|
+
// gh#1954: Step 4d.7 also regenerates the factory's Hermes/Pi
|
|
1253
|
+
// hermes-mcp.yaml fragment post-bump — not PINNED_REFS-covered (it's
|
|
1254
|
+
// a manifest-generator output, not a version-pin literal), so it
|
|
1255
|
+
// stays hand-listed here. factory's `.mcp.json` + `README.md`
|
|
1256
|
+
// generation-MCP pins (gh#1198, gh#2473 — same invariant-8 scope) come
|
|
1257
|
+
// from pinnedRefFiles(PINNED_REFS) below instead (gh#3361).
|
|
1258
|
+
if (name === 'adia-ui-factory') files.push(`${dir}/hermes-mcp.yaml`);
|
|
1159
1259
|
return files;
|
|
1160
1260
|
}),
|
|
1261
|
+
// gh#3361: every PINNED_REFS/REPO_PINNED_REFS-covered file, derived
|
|
1262
|
+
// straight from bump.mjs's own tables — see pinnedRefFiles()'s header
|
|
1263
|
+
// comment for what this closes. Deduped below against entries this list
|
|
1264
|
+
// already carries for OTHER reasons (e.g. every roster package's own
|
|
1265
|
+
// package.json is already staged above; web-modules' peer-pin entry
|
|
1266
|
+
// targets that same package.json).
|
|
1267
|
+
...pinnedRefFiles(PINNED_REFS, REPO_PINNED_REFS),
|
|
1161
1268
|
];
|
|
1162
|
-
|
|
1269
|
+
const dedupedReleaseFiles = [...new Set(releaseFiles)];
|
|
1270
|
+
sh(`git add ${dedupedReleaseFiles.join(' ')}`, args);
|
|
1163
1271
|
sh('git diff --cached --stat | tail -3', args);
|
|
1164
1272
|
|
|
1165
1273
|
// Step 5.6 — unstaged-tracked-files guard (gh#2473). Numbered 5.6, not
|
|
@@ -1317,6 +1425,77 @@ function registrySnapshot(args) {
|
|
|
1317
1425
|
return `Registry state BEFORE publish (target = ${args.version}):\n${lines.join('\n')}`;
|
|
1318
1426
|
}
|
|
1319
1427
|
|
|
1428
|
+
async function sleepMs(ms) {
|
|
1429
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Registry truth for the Step 9 poll — which packages (plus, as the sentinel
|
|
1433
|
+
// entry 'dist-tags.latest', the tag itself) have NOT yet converged on the
|
|
1434
|
+
// target version. [] means fully converged.
|
|
1435
|
+
function registryStalePackages(args, scope, version) {
|
|
1436
|
+
const stale = [];
|
|
1437
|
+
for (const pkg of PACKAGES) {
|
|
1438
|
+
let v;
|
|
1439
|
+
try { v = execSync(`npm view "${scope}/${pkg}" version`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }
|
|
1440
|
+
catch { v = null; }
|
|
1441
|
+
if (v !== version) stale.push(pkg);
|
|
1442
|
+
}
|
|
1443
|
+
let latest;
|
|
1444
|
+
try { latest = execSync(`npm view ${scope}/web-components dist-tags.latest`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }
|
|
1445
|
+
catch { latest = null; }
|
|
1446
|
+
if (latest !== version) stale.push('dist-tags.latest');
|
|
1447
|
+
return stale;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// gh#3342: npm now stages a large publish ASYNCHRONOUSLY — the tarball can
|
|
1451
|
+
// take up to ~25 minutes to actually become visible on `npm view`, well past
|
|
1452
|
+
// this poll's former 10-minute bound (the v0.8.59 cut hit exactly this). A
|
|
1453
|
+
// re-dispatch of the publish workflow while a package is in that state fails
|
|
1454
|
+
// its own `npm publish` step with npm error E409 ("Cannot publish over
|
|
1455
|
+
// previously staged version") — that error IS the staged-not-lost signal
|
|
1456
|
+
// (npm rejected the duplicate BECAUSE the real publish already landed
|
|
1457
|
+
// server-side), never a real failure. Check the package's latest
|
|
1458
|
+
// publish-<pkg>.yml run: a FAILED conclusion whose log matches the E409
|
|
1459
|
+
// signature confirms staged-not-lost; anything else (no failed run, or a
|
|
1460
|
+
// failure that isn't E409) is a genuine miss. Never throws — a gh query
|
|
1461
|
+
// failure just means "not confirmed E409", the conservative default so a
|
|
1462
|
+
// genuine miss is never masked.
|
|
1463
|
+
function publishRunFailedWithE409(pkg) {
|
|
1464
|
+
try {
|
|
1465
|
+
const runId = execSync(
|
|
1466
|
+
`gh run list --workflow=publish-${pkg}.yml --limit 1 --json databaseId,conclusion -q '.[0] | select(.conclusion=="failure") | .databaseId'`,
|
|
1467
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
|
|
1468
|
+
).trim();
|
|
1469
|
+
if (!runId) return false;
|
|
1470
|
+
const log = execSync(`gh run view ${runId} --log-failed`, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1471
|
+
return /E409|previously staged/i.test(log);
|
|
1472
|
+
} catch {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Poll the registry every 15s up to `minutes`, logging progress the same way
|
|
1478
|
+
// the original bash loop did. Returns the still-stale list (empty = fully
|
|
1479
|
+
// converged) — never throws; the caller decides what a non-empty result means.
|
|
1480
|
+
async function pollRegistry(args, scope, version, minutes, label) {
|
|
1481
|
+
if (args.dry) {
|
|
1482
|
+
console.log(` [dry] ${label} poll skipped (up to ${minutes}min, every 15s)`);
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
const checks = Math.max(1, Math.round((minutes * 60) / 15));
|
|
1486
|
+
let stale = PACKAGES.length ? [...PACKAGES, 'dist-tags.latest'] : [];
|
|
1487
|
+
for (let i = 1; i <= checks; i++) {
|
|
1488
|
+
stale = registryStalePackages(args, scope, version);
|
|
1489
|
+
if (stale.length === 0) {
|
|
1490
|
+
console.log(` ✓ ${label} confirms all ${PACKAGES.length} + latest, after ${i} check(s)`);
|
|
1491
|
+
return [];
|
|
1492
|
+
}
|
|
1493
|
+
if (i < checks) await sleepMs(15000);
|
|
1494
|
+
}
|
|
1495
|
+
console.log(` ${label} did not converge after ${minutes}min — still stale: ${stale.join(', ')}`);
|
|
1496
|
+
return stale;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1320
1499
|
async function step9Publish(args) {
|
|
1321
1500
|
console.log('\n=== Step 9 — Verify-triggered + re-dispatch misses ===');
|
|
1322
1501
|
const evidence = registrySnapshot(args);
|
|
@@ -1334,39 +1513,105 @@ async function step9Publish(args) {
|
|
|
1334
1513
|
// Poll the REGISTRY itself instead — the skill's own doctrine (SKILL.md:
|
|
1335
1514
|
// "the registry is the verify target, never a workflow's green check")
|
|
1336
1515
|
// applies here as much as it does to the final check.
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1516
|
+
//
|
|
1517
|
+
// gh#3342: the poll window moved from a flat 10 minutes to
|
|
1518
|
+
// REGISTRY_POLL_MINUTES (default 30) — npm stages a large publish
|
|
1519
|
+
// ASYNCHRONOUSLY and the tarball can take up to ~25 minutes to become
|
|
1520
|
+
// visible on `npm view`, past the old bound (the v0.8.59 cut hit this and
|
|
1521
|
+
// exited 1 with every publish run actually green).
|
|
1522
|
+
const pollMinutes = Number(process.env.REGISTRY_POLL_MINUTES) || 30;
|
|
1523
|
+
console.log(`\n Waiting for the registry to confirm all ${PACKAGES.length} packages at ${args.version} (polling every 15s, up to ${pollMinutes}min)...`);
|
|
1524
|
+
let stale = await pollRegistry(args, args.scope, args.version, pollMinutes, 'registry');
|
|
1525
|
+
|
|
1526
|
+
// gh#3342: a still-stale package here may be genuinely missing, OR it may
|
|
1527
|
+
// be staged-but-not-yet-visible — distinguish via its latest publish run.
|
|
1528
|
+
// A run that failed specifically on npm's E409 ("Cannot publish over
|
|
1529
|
+
// previously staged version") proves the package IS staged: npm rejected
|
|
1530
|
+
// the (redundant) publish attempt BECAUSE the real one already landed
|
|
1531
|
+
// server-side. That is the staged-not-lost signal this ticket exists to
|
|
1532
|
+
// handle — never a hard failure, and never itself grounds to re-dispatch
|
|
1533
|
+
// again (a THIRD publish attempt would only 409 again). Give
|
|
1534
|
+
// E409-confirmed packages one more extended, isolated poll; a package
|
|
1535
|
+
// with no E409 signal is a genuine miss and skips straight to the
|
|
1536
|
+
// fail-closed report below.
|
|
1537
|
+
if (stale.length > 0 && !args.dry) {
|
|
1538
|
+
const staleePkgs = stale.filter((p) => p !== 'dist-tags.latest');
|
|
1539
|
+
const e409 = staleePkgs.filter((pkg) => publishRunFailedWithE409(pkg));
|
|
1540
|
+
if (e409.length > 0) {
|
|
1541
|
+
console.log(`\n ${e409.length} stale package(s) confirmed E409 (staged, not lost) — extending the poll another ${pollMinutes}min: ${e409.join(', ')}`);
|
|
1542
|
+
console.log(' (a genuinely missing package would show no failed run, or a failure that is not E409 — those are NOT extended)');
|
|
1543
|
+
// The fail-closed block below re-queries the registry fresh regardless
|
|
1544
|
+
// of this poll's own result — this extended wait exists to let a
|
|
1545
|
+
// genuinely staged package converge before that final read, not to
|
|
1546
|
+
// compute the verdict itself.
|
|
1547
|
+
await pollRegistry(args, args.scope, args.version, pollMinutes, 'extended registry poll');
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1353
1551
|
// Fail-closed: the registry is the verify target (SKILL.md) — a missing or
|
|
1354
1552
|
// stale package aborts the cut here, before GH releases + site deploy. This
|
|
1355
|
-
// is now a final confirmation of the poll above, not the first real read.
|
|
1553
|
+
// is now a final confirmation of the poll(s) above, not the first real read.
|
|
1356
1554
|
console.log(`\n Post-publish registry verification, all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
`
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
`
|
|
1368
|
-
|
|
1369
|
-
|
|
1555
|
+
if (args.dry) {
|
|
1556
|
+
console.log(' [dry] registry re-verify skipped');
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
const final = registryStalePackages(args, args.scope, args.version);
|
|
1560
|
+
for (const pkg of PACKAGES) {
|
|
1561
|
+
console.log(` ${pkg}: ${final.includes(pkg) ? 'MISSING' : args.version}`);
|
|
1562
|
+
}
|
|
1563
|
+
console.log(` latest: ${final.includes('dist-tags.latest') ? 'MISSING' : args.version}`);
|
|
1564
|
+
if (final.length > 0) {
|
|
1565
|
+
console.error(`ERROR: registry does not confirm ${args.version} across all ${PACKAGES.length} packages — fix before GH releases/deploy`);
|
|
1566
|
+
console.error(` still stale: ${final.join(', ')}`);
|
|
1567
|
+
process.exit(1);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Precondition for `--from-step10` (critic finding on PR #3364): jumping
|
|
1572
|
+
// straight to Step 10 without checking the release tags exist is a silent
|
|
1573
|
+
// trap — `gh release create <tag>` on a tag that doesn't exist yet mints a
|
|
1574
|
+
// NEW lightweight tag at whatever HEAD happens to be, not the real tagged
|
|
1575
|
+
// release commit. Refuse the resume outright unless every tag Step 6 would
|
|
1576
|
+
// have created (umbrella + all per-package) already exists on origin and
|
|
1577
|
+
// points at HEAD.
|
|
1578
|
+
function assertReleaseTagsAtHead(args) {
|
|
1579
|
+
console.log('\n Precondition — verifying release tags already exist and point at HEAD before resuming at Step 10:');
|
|
1580
|
+
const headSha = shQuiet('git rev-parse HEAD', args).trim();
|
|
1581
|
+
const tags = [`v${args.version}`, ...PACKAGES.map((p) => `${p}-v${args.version}`)];
|
|
1582
|
+
if (args.dry) {
|
|
1583
|
+
console.log(` [dry] git fetch origin ${tags.map((t) => `tag ${t}`).join(' ')}`);
|
|
1584
|
+
console.log(` [dry] verify each of ${tags.length} tags resolves to HEAD (${headSha || '<dry HEAD>'})`);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
sh(`git fetch origin ${tags.map((t) => `tag ${t}`).join(' ')}`, args);
|
|
1588
|
+
const missing = [];
|
|
1589
|
+
const mismatched = [];
|
|
1590
|
+
for (const tag of tags) {
|
|
1591
|
+
let tagSha;
|
|
1592
|
+
try {
|
|
1593
|
+
tagSha = execSync(`git rev-parse "${tag}^{commit}"`, { cwd: REPO, encoding: 'utf8' }).trim();
|
|
1594
|
+
} catch {
|
|
1595
|
+
missing.push(tag);
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
if (tagSha !== headSha) mismatched.push(`${tag} -> ${tagSha}`);
|
|
1599
|
+
}
|
|
1600
|
+
if (missing.length > 0 || mismatched.length > 0) {
|
|
1601
|
+
console.error('\nERROR: --from-step10 refuses to resume — the release tags this handoff needs are not all present at HEAD.');
|
|
1602
|
+
if (missing.length > 0) {
|
|
1603
|
+
console.error(` Missing entirely (Step 6 creates these, Step 8 pushes them to origin): ${missing.join(', ')}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (mismatched.length > 0) {
|
|
1606
|
+
console.error(` Point somewhere other than HEAD (${headSha}): ${mismatched.join(', ')}`);
|
|
1607
|
+
}
|
|
1608
|
+
console.error(' Resuming Step 10 here would let `gh release create` mint NEW lightweight tags at the');
|
|
1609
|
+
console.error(' current HEAD instead of using the real tagged release commit — silently wrong.');
|
|
1610
|
+
console.error(' Fix: confirm the original handoff run actually reached Step 8 (tags pushed to origin)');
|
|
1611
|
+
console.error(' and that HEAD has not moved since, or re-run the full handoff instead of --from-step10.');
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
console.log(` ✓ all ${tags.length} release tags exist on origin and point at HEAD (${headSha})`);
|
|
1370
1615
|
}
|
|
1371
1616
|
|
|
1372
1617
|
// ── Step 10 — GH releases + site deploy ──────────────────────────
|
|
@@ -1519,6 +1764,22 @@ async function main() {
|
|
|
1519
1764
|
console.error(` node ${repoCopy} <same args>\n`);
|
|
1520
1765
|
}
|
|
1521
1766
|
}
|
|
1767
|
+
// gh#3342: --from-step10 resumes a handoff that already tagged, pushed,
|
|
1768
|
+
// and published — skip straight to Step 10 (GH releases + site deploy)
|
|
1769
|
+
// rather than a plain re-run, which would re-run the full pre-flight gate
|
|
1770
|
+
// roster (Step 3, ~15min) AND re-tag at HEAD (Step 6, wrong if anything
|
|
1771
|
+
// merged since the original tag). Every step this skips is either
|
|
1772
|
+
// idempotent-but-wasteful (3, 9) or actively wrong to repeat (6, if HEAD
|
|
1773
|
+
// moved) — a real operational need the v0.8.59 cut hit and worked around
|
|
1774
|
+
// with a one-off, never-checked-in script.
|
|
1775
|
+
if (args.mode === 'handoff' && args.fromStep10) {
|
|
1776
|
+
console.log('\n[resume --from-step10] skipping Steps 1/3/4/5/6/7/8/9 (already tagged/pushed/published) — jumping straight to Step 10.');
|
|
1777
|
+
assertReleaseTagsAtHead(args);
|
|
1778
|
+
await step10GhAndSite(args);
|
|
1779
|
+
rl.close();
|
|
1780
|
+
console.log('\n✓ release-pack complete (resumed at Step 10). Verify against the npm registry + GH releases, then author the release notes.');
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1522
1783
|
// gh#765: an aborted Step-4 run leaves bumped versions + promoted
|
|
1523
1784
|
// CHANGELOGs; detect BEFORE pre-flight so gate 24 validates at the right
|
|
1524
1785
|
// version and Step 4 skips the already-done promote/stub/bump.
|
|
@@ -1652,6 +1913,50 @@ async function selftest() {
|
|
|
1652
1913
|
}
|
|
1653
1914
|
}
|
|
1654
1915
|
|
|
1916
|
+
// gh#3361 — pinnedRefFiles() must derive `<pkgDir>/<file>` for a
|
|
1917
|
+
// package-keyed entry and the bare `file` for a repo-level entry, from a
|
|
1918
|
+
// FIXTURE table (independent of bump.mjs's real, evolving content).
|
|
1919
|
+
{
|
|
1920
|
+
const fixturePinned = {
|
|
1921
|
+
'packages/plugins/fake-plugin': [{ file: '.mcp.json' }, { file: 'README.md' }],
|
|
1922
|
+
'packages/fake-pkg': [{ file: 'core/pinned.js' }],
|
|
1923
|
+
};
|
|
1924
|
+
const fixtureRepoPinned = [{ file: 'README.md' }];
|
|
1925
|
+
const got = pinnedRefFiles(fixturePinned, fixtureRepoPinned);
|
|
1926
|
+
const want = [
|
|
1927
|
+
'packages/plugins/fake-plugin/.mcp.json',
|
|
1928
|
+
'packages/plugins/fake-plugin/README.md',
|
|
1929
|
+
'packages/fake-pkg/core/pinned.js',
|
|
1930
|
+
'README.md',
|
|
1931
|
+
];
|
|
1932
|
+
if (JSON.stringify(got) !== JSON.stringify(want)) {
|
|
1933
|
+
console.error(`selftest FAIL: pinnedRefFiles() expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`);
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
// gh#3361 — the structural guarantee this ticket exists for: every path
|
|
1938
|
+
// bump.mjs's REAL, imported PINNED_REFS/REPO_PINNED_REFS tables name is
|
|
1939
|
+
// present in Step 5's assembled releaseFiles (the `--mode cut --dry`
|
|
1940
|
+
// subprocess run below), proven by literally re-deriving the expected
|
|
1941
|
+
// set from the SAME imported tables the subprocess itself reads — a
|
|
1942
|
+
// future PINNED_REFS entry with no matching releaseFiles line (the
|
|
1943
|
+
// gh#1198/1899/1954/2473/3342 drift shape) fails HERE, not three cuts
|
|
1944
|
+
// later at Step 5.6.
|
|
1945
|
+
{
|
|
1946
|
+
const expectedPinned = pinnedRefFiles(PINNED_REFS, REPO_PINNED_REFS);
|
|
1947
|
+
if (expectedPinned.length === 0) {
|
|
1948
|
+
console.error('selftest FAIL: bump.mjs\'s real PINNED_REFS/REPO_PINNED_REFS produced zero derived paths — the import is broken');
|
|
1949
|
+
process.exit(1);
|
|
1950
|
+
}
|
|
1951
|
+
// The fixture set itself must never silently reduce to nothing new —
|
|
1952
|
+
// proves gh#3342's icons-cdn.js entry specifically survived the
|
|
1953
|
+
// gh#3361 refactor (it's the exact file that motivated this ticket).
|
|
1954
|
+
if (!expectedPinned.includes('packages/web-components/core/icons-cdn.js')) {
|
|
1955
|
+
console.error(`selftest FAIL: pinnedRefFiles(PINNED_REFS, REPO_PINNED_REFS) is missing packages/web-components/core/icons-cdn.js — got ${JSON.stringify(expectedPinned)}`);
|
|
1956
|
+
process.exit(1);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1655
1960
|
// gh#2473 — Step 5.6's guard: unstagedTrackedLeftovers() must find a
|
|
1656
1961
|
// plain modified/deleted tracked file, unquote a C-quoted path
|
|
1657
1962
|
// (spaces), and report the NEW side of a rename/copy — all from a
|
|
@@ -1715,6 +2020,11 @@ async function selftest() {
|
|
|
1715
2020
|
`${GATE_ROSTER.length}-gate roster`,
|
|
1716
2021
|
`✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`,
|
|
1717
2022
|
'STOPPING per invariant 3',
|
|
2023
|
+
// gh#3342 — gate 4 needs llm/agent/persona's BUILT (tsc) output, not
|
|
2024
|
+
// just conditionally regenerated content; the precondition must run
|
|
2025
|
+
// even in a --dry preview, before the gate roster itself.
|
|
2026
|
+
'Precondition: tsc build (llm, agent, persona',
|
|
2027
|
+
'[dry] npm run build -w @adia-ai/llm -w @adia-ai/agent -w @adia-ai/persona',
|
|
1718
2028
|
// gh#358 — the release branch must actually be created, not just documented.
|
|
1719
2029
|
'git checkout -b release/v9.9.9',
|
|
1720
2030
|
// gh#1197 — Step 5's staging pathspec must carry gate 28's committed
|
|
@@ -1752,6 +2062,19 @@ async function selftest() {
|
|
|
1752
2062
|
// README target), or it rides unstaged/uncommitted past the release
|
|
1753
2063
|
// commit the same way .mcp.json itself did before gh#1198.
|
|
1754
2064
|
'packages/plugins/adia-ui-factory/README.md',
|
|
2065
|
+
// gh#3342 — fifth instance of the same allowlist-drift class
|
|
2066
|
+
// (gh#1198, gh#1899, gh#1954, gh#2473): bump.mjs's web-components
|
|
2067
|
+
// PINNED_REFS entry moves icons-cdn.js's PACKAGE_VERSION literal
|
|
2068
|
+
// every cut; the v0.8.59 cut hit Step 5.6's guard on this exact file.
|
|
2069
|
+
'packages/web-components/core/icons-cdn.js',
|
|
2070
|
+
// gh#3342 — Step 4d.8 rebuilds the dist bundles post-bump (the
|
|
2071
|
+
// icons-cdn.js pin above is embedded in three of them) and Step 5
|
|
2072
|
+
// must stage the result, or gate 9 (check:js-bundles-fresh) fails in
|
|
2073
|
+
// the handoff pre-flight the way the v0.8.59 cut's did.
|
|
2074
|
+
'Step 4d.8 — rebuild dist CSS+JS bundles',
|
|
2075
|
+
'[dry] npm run build:bundles',
|
|
2076
|
+
'packages/web-components/dist',
|
|
2077
|
+
'packages/web-modules/dist',
|
|
1755
2078
|
// gh#2473 — Step 5.6's unstaged-tracked-files guard must actually run
|
|
1756
2079
|
// (as a no-op) on a clean --dry plan. Its real leftover-detection
|
|
1757
2080
|
// logic is unit-tested directly above via unstagedTrackedLeftovers();
|
|
@@ -1908,6 +2231,62 @@ async function selftest() {
|
|
|
1908
2231
|
process.exit(1);
|
|
1909
2232
|
}
|
|
1910
2233
|
|
|
2234
|
+
// gh#3364 critic finding — `--from-step10` must (a) only apply to
|
|
2235
|
+
// --mode handoff, at parse time, and (b) refuse to resume without first
|
|
2236
|
+
// verifying the release tags already exist and point at HEAD (otherwise
|
|
2237
|
+
// `gh release create` on a tag that was never made mints a NEW
|
|
2238
|
+
// lightweight tag at whatever HEAD happens to be — silently wrong).
|
|
2239
|
+
let fromStep10ModeFailed = false;
|
|
2240
|
+
let fromStep10ModeOut = '';
|
|
2241
|
+
try {
|
|
2242
|
+
execSync(
|
|
2243
|
+
`node "${scriptPath}" --mode cut --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 --from-step10 --dry`,
|
|
2244
|
+
{ cwd: REPO, encoding: 'utf8' },
|
|
2245
|
+
);
|
|
2246
|
+
} catch (e) {
|
|
2247
|
+
fromStep10ModeFailed = true;
|
|
2248
|
+
fromStep10ModeOut = (e.stdout || '') + (e.stderr || '');
|
|
2249
|
+
}
|
|
2250
|
+
if (!fromStep10ModeFailed || !fromStep10ModeOut.includes('--from-step10 only applies to --mode handoff')) {
|
|
2251
|
+
console.error('selftest FAIL: --from-step10 with a non-handoff mode must hard-reject at parse time');
|
|
2252
|
+
process.exit(1);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
let resumeOut;
|
|
2256
|
+
try {
|
|
2257
|
+
resumeOut = execSync(
|
|
2258
|
+
`node "${scriptPath}" --mode handoff --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 ` +
|
|
2259
|
+
`--gh-notes-file "${notesFile}" --from-step10 --skip-site --dry`,
|
|
2260
|
+
{ cwd: REPO, encoding: 'utf8' },
|
|
2261
|
+
);
|
|
2262
|
+
} catch (e) {
|
|
2263
|
+
console.error('selftest FAIL: --mode handoff --from-step10 --dry plan assembly exited non-zero');
|
|
2264
|
+
console.error((e.stdout || '') + (e.stderr || ''));
|
|
2265
|
+
process.exit(1);
|
|
2266
|
+
}
|
|
2267
|
+
const resumeNeed = [
|
|
2268
|
+
'[resume --from-step10] skipping Steps 1/3/4/5/6/7/8/9',
|
|
2269
|
+
'Precondition — verifying release tags already exist and point at HEAD',
|
|
2270
|
+
'[dry] git fetch origin',
|
|
2271
|
+
'=== Step 10 — GH releases + Site deploy',
|
|
2272
|
+
];
|
|
2273
|
+
for (const marker of resumeNeed) {
|
|
2274
|
+
if (!resumeOut.includes(marker)) {
|
|
2275
|
+
console.error(`selftest FAIL: expected marker not found in --from-step10 dry plan: "${marker}"`);
|
|
2276
|
+
process.exit(1);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
// "=== Step 1" is a prefix of "=== Step 10" — every marker below must
|
|
2280
|
+
// carry enough of its own header to avoid a false positive against the
|
|
2281
|
+
// Step 10 banner this resume path is SUPPOSED to reach.
|
|
2282
|
+
const resumeMustSkip = ['=== Step 1 — Re-baseline', '=== Step 3 — Pre-flight', '=== Step 6 — Tag at HEAD', '=== Step 7 — F-N1', '=== Step 8 — Push', '=== Step 9 — Verify-triggered'];
|
|
2283
|
+
for (const marker of resumeMustSkip) {
|
|
2284
|
+
if (resumeOut.includes(marker)) {
|
|
2285
|
+
console.error(`selftest FAIL: --from-step10 must skip Steps 1-9 — found "${marker}" in the dry plan`);
|
|
2286
|
+
process.exit(1);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
1911
2290
|
console.log('selftest OK');
|
|
1912
2291
|
} finally {
|
|
1913
2292
|
fs.rmSync(tmp, { recursive: true, force: true });
|