@adia-ai/adia-ui-forge 0.8.10 → 0.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/bin/release-pretag-docs-gate +51 -2
- package/package.json +1 -1
- package/skills/adia-author/SKILL.md +2 -0
- package/skills/adia-author/references/authoring-cycle.md +5 -1
- package/skills/adia-release/references/changelog-discipline.md +1 -0
- package/skills/adia-release/references/cut-procedure.md +28 -0
- package/skills/adia-release/scripts/release-pack.mjs +33 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adia-forge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
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,17 @@
|
|
|
1
1
|
# Changelog — adia-forge
|
|
2
2
|
|
|
3
|
+
## [0.8.11] — 2026-07-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **`adia-author`: dist-bundle regen joins the structural-gate sequence** (`skills/adia-author/SKILL.md`, `references/authoring-cycle.md`) — a new/changed component CSS `@import` or barrel export drifts `dist/` bundles, and the skill's verify sequence never said so; gh#390's PR failed CI's `check:css-bundles-fresh`/`check:js-bundles-fresh` exactly this way. The gate sequence and the barrel-registration step now both name `build:bundle-css`/`build:bundle-js` + the two freshness checks.
|
|
7
|
+
- **`adia-release`: `release-pack.mjs --mode handoff` validates `--gh-notes-file` at parse time** — the flag was only checked at Step 10 (GH releases), AFTER tags and npm publish were irreversible; the v0.8.10 cut died there live. Selftest case added.
|
|
8
|
+
- **`adia-release`: mid-Step-4 resume path documented** (`references/cut-procedure.md` §4-resume) — once any CHANGELOG promotion lands, re-invoking the orchestrator hard-errors on the idempotency guard ("already has ## [X.Y.Z]") forever; the doc now names the standalone-pieces sequence (bump → lockfile → lockstep → 4f → 4e → Step 5/5.7) instead of leaving the operator to reverse-engineer it mid-cut.
|
|
9
|
+
- **`adia-release`: stub-package ownership rule** (`references/changelog-discipline.md` §Stubs) — `insert-stub.mjs` inserts, never promotes; a hand-written `[Unreleased]` in a `--stub-packages` target survives as an orphaned second block and fails the loud guard (v0.8.10 lost a cycle to this across 6 packages). Rule: the tool owns stub packages; hand-written content means the package belongs in `--substantive-packages`.
|
|
10
|
+
- **`release-pretag-docs-gate`: a `cd <path> && …` command prefix now resolves the gate against THAT checkout, not the session cwd** (`bin/release-pretag-docs-gate`) — a session pinned to a stale worktree (own copy of the gate script, branch without the release's CHANGELOG sections) false-FAILed the v0.8.10 handoff even though the command explicitly cd'd to the clean primary checkout. Selftest fixtures added for the prefix resolution.
|
|
11
|
+
|
|
12
|
+
### Maintenance
|
|
13
|
+
- **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
|
|
14
|
+
|
|
3
15
|
## [0.8.10] — 2026-07-20
|
|
4
16
|
|
|
5
17
|
### Added
|
|
@@ -89,6 +89,33 @@ def classify(command):
|
|
|
89
89
|
return None
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
CD_PREFIX = re.compile(r"""^\s*cd\s+(?:--\s+)?(['"]?)([^'";&|\n]+)\1\s*(?:&&|;)""")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolve_start_dir(command, cwd):
|
|
96
|
+
"""A command that opens with `cd <path> && …` targets THAT checkout, not
|
|
97
|
+
the session's cwd. Live false FAIL (v0.8.10 handoff, 2026-07-20): the
|
|
98
|
+
session sat pinned in a stale worktree (its own copy of the gate script +
|
|
99
|
+
a branch with no [0.8.10] CHANGELOG sections) while the command cd'd to
|
|
100
|
+
the primary checkout on post-merge main — the gate validated the stale
|
|
101
|
+
worktree and denied a genuinely clean release. Prefer the cd target when
|
|
102
|
+
it is a real directory; otherwise fall back to the event cwd."""
|
|
103
|
+
m = CD_PREFIX.match(command or "")
|
|
104
|
+
if m:
|
|
105
|
+
# Mirror the shell: ~ expands only when the operand is UNQUOTED —
|
|
106
|
+
# `cd "~"` targets a literal ./~ (and typically fails), so resolving
|
|
107
|
+
# $HOME here would let a quoted-tilde push bypass the gate whenever
|
|
108
|
+
# $HOME lacks the gate script (review finding, PR #394).
|
|
109
|
+
target = m.group(2).strip()
|
|
110
|
+
if not m.group(1):
|
|
111
|
+
target = os.path.expanduser(target)
|
|
112
|
+
if not os.path.isabs(target) and cwd:
|
|
113
|
+
target = os.path.join(cwd, target)
|
|
114
|
+
if os.path.isdir(target):
|
|
115
|
+
return target
|
|
116
|
+
return cwd
|
|
117
|
+
|
|
118
|
+
|
|
92
119
|
def find_repo_gate(start):
|
|
93
120
|
"""Walk up from `start` to the git root looking for the gate script —
|
|
94
121
|
a release command launched from a subdirectory must not bypass the
|
|
@@ -125,10 +152,11 @@ def hook_mode():
|
|
|
125
152
|
sys.exit(0)
|
|
126
153
|
if event.get("tool_name") != "Bash":
|
|
127
154
|
sys.exit(0)
|
|
128
|
-
|
|
155
|
+
command = (event.get("tool_input") or {}).get("command", "")
|
|
156
|
+
version = classify(command)
|
|
129
157
|
if not version:
|
|
130
158
|
sys.exit(0)
|
|
131
|
-
root, gate = find_repo_gate(event.get("cwd"))
|
|
159
|
+
root, gate = find_repo_gate(resolve_start_dir(command, event.get("cwd")))
|
|
132
160
|
if not gate:
|
|
133
161
|
sys.exit(0) # consumer repo — nothing to enforce here
|
|
134
162
|
if version == AMBIGUOUS:
|
|
@@ -214,6 +242,27 @@ def selftest():
|
|
|
214
242
|
if gate is not None:
|
|
215
243
|
print("selftest FAIL: consumer repo must yield no gate", file=sys.stderr)
|
|
216
244
|
sys.exit(1)
|
|
245
|
+
# resolve_start_dir: a `cd <path> && …` prefix must win over the session
|
|
246
|
+
# cwd (the v0.8.10 pinned-worktree false FAIL); anything else falls back.
|
|
247
|
+
with tempfile.TemporaryDirectory() as td:
|
|
248
|
+
real = os.path.join(td, "checkout")
|
|
249
|
+
os.makedirs(real)
|
|
250
|
+
cases_dir = [
|
|
251
|
+
(f"cd {real} && node release-pack.mjs --mode handoff --version 0.9.0", "/elsewhere", real),
|
|
252
|
+
(f'cd "{real}" && git push origin v0.9.0', "/elsewhere", real),
|
|
253
|
+
(f"cd {td}/missing && node release-pack.mjs --mode handoff --version 0.9.0", "/elsewhere", "/elsewhere"),
|
|
254
|
+
("node release-pack.mjs --mode handoff --version 0.9.0", "/elsewhere", "/elsewhere"),
|
|
255
|
+
("", "/elsewhere", "/elsewhere"),
|
|
256
|
+
# Quoted tilde: bash cd's to a literal ./~, NOT $HOME — the gate
|
|
257
|
+
# must not resolve $HOME either (quoted-tilde bypass, PR #394).
|
|
258
|
+
('cd "~" && git push origin v0.9.0', "/elsewhere", "/elsewhere"),
|
|
259
|
+
]
|
|
260
|
+
for cmd, cwd, want in cases_dir:
|
|
261
|
+
got = resolve_start_dir(cmd, cwd)
|
|
262
|
+
if got != want:
|
|
263
|
+
print(f"selftest FAIL: resolve_start_dir({cmd!r}, {cwd!r}) = {got!r}, want {want!r}", file=sys.stderr)
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
|
|
217
266
|
print("selftest OK")
|
|
218
267
|
|
|
219
268
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/adia-ui-forge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
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",
|
|
@@ -101,6 +101,8 @@ npm run smoke:engines # green
|
|
|
101
101
|
node scripts/dev/audit-native-primitive-leak.mjs # 0 critical leaks
|
|
102
102
|
node scripts/dev/audit-shell-composition.mjs # 0 critical defects
|
|
103
103
|
node scripts/dev/audit-template-child-conflict.mjs # 0 critical (gh#284 shape — non-null template + slots.default)
|
|
104
|
+
npm run build:bundle-css && npm run build:bundle-js # regenerate dist bundles — any component CSS/JS change drifts them
|
|
105
|
+
npm run check:css-bundles-fresh && npm run check:js-bundles-fresh # both must be clean (CI gates; gh#390's PR failed here)
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
A failed gate is the artifact: fix at the source, re-run the narrowest gate,
|
|
@@ -200,7 +200,11 @@ node scripts/build/components.mjs --verify # "clean — N files up-to-date"
|
|
|
200
200
|
|
|
201
201
|
A primitive also needs BOTH barrel registrations — the CSS `@import` in
|
|
202
202
|
`styles/components.css` AND the JS `export` in `components/index.js`
|
|
203
|
-
(`scripts/audit/check-components-js-barrel.mjs` gates the second).
|
|
203
|
+
(`scripts/audit/check-components-js-barrel.mjs` gates the second). Either
|
|
204
|
+
registration drifts the built `dist/` bundles — regenerate them in the same
|
|
205
|
+
change (`npm run build:bundle-css && npm run build:bundle-js`) and commit the
|
|
206
|
+
`dist/` updates, or CI's `check:{css,js}-bundles-fresh` gates fail on the PR
|
|
207
|
+
(gh#390's PR shipped without this and failed exactly there).
|
|
204
208
|
|
|
205
209
|
## Step 5 — Run the project's verification gates
|
|
206
210
|
|
|
@@ -92,6 +92,7 @@ The inserted block:
|
|
|
92
92
|
|
|
93
93
|
- **Make the PATCH-cut asymmetry visible** when an entry mentions a dependency: version bumps to `X.Y.Z` while internal ranges hold at `^X.Y.0` — spell it as `dependencies["@adia-ai/<x>"]: ^X.Y.0 (covers X.Y.Z)` so it doesn't read as a bug.
|
|
94
94
|
- **Stale stubs are an F-N1 hazard.** A "no source changes" stub on a package that DID change earns a warn; the fix is authoring (§Authoring), not stubbing.
|
|
95
|
+
- **The tool owns stub packages — never hand-write their `[Unreleased]`.** `insert-stub.mjs` INSERTS a fresh dated block at the top; it does not promote or replace existing content. A hand-authored `[Unreleased]` stub in a `--stub-packages` target survives the insert as a second, orphaned block below it, and the orchestrator's loud guard then fails the whole run ("N packages still have non-empty [Unreleased]") — the v0.8.10 cut lost a full cycle to exactly this across 6 packages. Ride-along packages get NO hand-written entry at cut time: leave them untouched and list them in `--stub-packages`. (A package that deserves hand-written content isn't a stub — it belongs in `--substantive-packages`.)
|
|
95
96
|
|
|
96
97
|
## §F-N1 diff-coverage — mechanized at cut time (Step 4f)
|
|
97
98
|
|
|
@@ -127,6 +127,34 @@ tag-creation/tag-push/publish-dispatch commands until it passes, and
|
|
|
127
127
|
shipped tarballs with unpromoted `[Unreleased]` CHANGELOG headers because
|
|
128
128
|
this class of check ran only after tagging).
|
|
129
129
|
|
|
130
|
+
**4-resume. Resuming a cut that died mid-Step-4** (v0.8.10 hit this live):
|
|
131
|
+
`release-pack.mjs` always re-runs the FULL sequence from Step 1, and
|
|
132
|
+
`promote-unreleased.mjs` hard-errors on already-promoted packages ("already
|
|
133
|
+
has ## [X.Y.Z]") — so once ANY promotion has landed, re-invoking the
|
|
134
|
+
orchestrator can never get past Step 4. That error is the idempotency guard
|
|
135
|
+
working, not a corrupted state. Do NOT hand-edit CHANGELOGs back to
|
|
136
|
+
`[Unreleased]` to appease it. Instead, verify the promotion state with the
|
|
137
|
+
authoritative gate — `node scripts/release/check-release-docs.mjs --version
|
|
138
|
+
X.Y.Z` covers every lockstep package (including the nested `a2ui/*` and
|
|
139
|
+
`plugins/*` paths a shallow `packages/*` glob misses) and fails on any
|
|
140
|
+
leftover `[Unreleased]` content or missing `[X.Y.Z]` heading; it will still
|
|
141
|
+
flag the not-yet-generated `docs/releases/vX.Y.Z.md`, which 4e below creates.
|
|
142
|
+
Then run the remaining steps as the standalone pieces the Mechanization
|
|
143
|
+
section already names, in this order:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
node "${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z # 4b
|
|
147
|
+
npm install --package-lock-only --no-audit --no-fund # 4c
|
|
148
|
+
npm run check:lockstep # 4d
|
|
149
|
+
node scripts/release/check-release.mjs --pending-version X.Y.Z --fix # 4f
|
|
150
|
+
node scripts/release/generate-release-notes.mjs --version X.Y.Z --write # 4e
|
|
151
|
+
node scripts/release/check-release-docs.mjs --version X.Y.Z # 4e gate
|
|
152
|
+
node scripts/release/check-cut-hygiene.mjs --version X.Y.Z # 4g — post-bump proof
|
|
153
|
+
# then Step 5 by hand (branch, stage, commit) — Step 5.5's freshness
|
|
154
|
+
# trip-wire runs on the staged set exactly as on a normal cut, don't skip it —
|
|
155
|
+
# and Step 5.7 via pr-bridge.mjs
|
|
156
|
+
```
|
|
157
|
+
|
|
130
158
|
## §Step 5 — Stage and commit (on a release branch)
|
|
131
159
|
|
|
132
160
|
The release commit lands via PR, never a direct push to `main` (repo
|
|
@@ -143,11 +143,29 @@ function parseArgs(argv) {
|
|
|
143
143
|
console.error(`error: --mode must be cut|from-scratch|handoff (got: ${args.mode})`);
|
|
144
144
|
process.exit(2);
|
|
145
145
|
}
|
|
146
|
+
// Handoff needs the GH-notes body at Step 10 — validate at PARSE time, not
|
|
147
|
+
// there: on the v0.8.10 cut the missing flag surfaced only after tags and
|
|
148
|
+
// npm publish were already irreversible (Step 10 is the LAST step). An arg
|
|
149
|
+
// error must never cost half a shipped release.
|
|
150
|
+
if (args.mode === 'handoff') {
|
|
151
|
+
if (!args.ghNotesFile) {
|
|
152
|
+
console.error('error: --mode handoff requires --gh-notes-file (Step 10 creates the 11 GH');
|
|
153
|
+
console.error(' releases from it; tags/publish would land first, then the run would');
|
|
154
|
+
console.error(' die on the missing flag — v0.8.10 hit exactly this).');
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
if (!args.dry && !(fs.existsSync(args.ghNotesFile) && fs.statSync(args.ghNotesFile).isFile())) {
|
|
158
|
+
console.error(`error: GH notes file not found or not a regular file: ${args.ghNotesFile}`);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
146
162
|
return args;
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
function help() {
|
|
150
166
|
console.log(`Usage:
|
|
167
|
+
node release-pack.mjs --mode handoff --version X.Y.Z --date YYYY-MM-DD \\
|
|
168
|
+
--previous-version X.Y.Z-1 --gh-notes-file /tmp/release-v$X.Y.Z.md # REQUIRED for handoff
|
|
151
169
|
node release-pack.mjs --mode cut --version X.Y.Z --date YYYY-MM-DD \\
|
|
152
170
|
--previous-version X.Y.Z-1 \\
|
|
153
171
|
--commit-message-file /tmp/v$X.Y.Z-commit.txt \\
|
|
@@ -734,6 +752,21 @@ function selftest() {
|
|
|
734
752
|
process.exit(1);
|
|
735
753
|
}
|
|
736
754
|
|
|
755
|
+
// Handoff without --gh-notes-file must die at PARSE time (v0.8.10: the old
|
|
756
|
+
// Step-10-only check fired after tags+publish were irreversible).
|
|
757
|
+
let handoffFailed = false;
|
|
758
|
+
let handoffOut = '';
|
|
759
|
+
try {
|
|
760
|
+
execSync(`node "${scriptPath}" --mode handoff --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 --dry`, { cwd: REPO, encoding: 'utf8' });
|
|
761
|
+
} catch (e) {
|
|
762
|
+
handoffFailed = true;
|
|
763
|
+
handoffOut = (e.stdout || '') + (e.stderr || '');
|
|
764
|
+
}
|
|
765
|
+
if (!handoffFailed || !handoffOut.includes('--gh-notes-file')) {
|
|
766
|
+
console.error('selftest FAIL: --mode handoff without --gh-notes-file must hard-reject at parse time');
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
|
|
737
770
|
console.log('selftest OK');
|
|
738
771
|
} finally {
|
|
739
772
|
fs.rmSync(tmp, { recursive: true, force: true });
|