@adia-ai/adia-ui-forge 0.8.51 → 0.8.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.51",
3
+ "version": "0.8.52",
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-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.51",
3
+ "version": "0.8.52",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — 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-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog — adia-ui-kit-forge
2
2
 
3
+ ## [0.8.52] — 2026-08-25
4
+
5
+ ### Added
6
+ - **`cross-harness-authoring-standards` (renamed from `writing-harness-install-instructions`, which had only ever existed as uncommitted worktree content) + new sibling skill `cross-harness-compatibility-standards` (gh#1975, PR #2003).** The rename is byte-faithful aside from the three self-referential mentions (frontmatter name, H1, one in-body reference). The new skill is the upstream engineering standard for actually building a capability that works across Claude Code, Codex, Pi, and Hermes — distinct from the renamed sibling's downstream consumer-facing install-docs concern — extracted from ADR-0091 and the four PRs (#1997-#2000) that built and shipped it: the skill-vs-thin-adapter classification rule, verify-before-coding per harness, one authored skill body with every runtime as a thin consumer, one contract grammar (record-lint family) instead of a schema per runtime, and naming distribution channels distinctly. Codex and Hermes derived manifests regenerated for both skills; README/`AGENTS.md` skill-roster and count updated.
7
+
8
+ ### Changed
9
+ - **`package-release`'s pre-cut roster runs in three phases instead of one serial for-loop (gh#2006, follow-up from gh#1987's audit)** — `release-pack.mjs`'s `step3PreFlight()` now runs gate 4 (`test:unit:serial`) solo, the eval-health chain (gates 16 → 27 → 28 — a real CSS-before-JS-shaped file-write ordering dependency: gate 28 reads whichever `evals/mcp/runs/` directory sorts lexically last, so gate 27's free-form run must land uninterrupted) strictly in order, and every other gate in a concurrency-capped pool (`PREFLIGHT_CONCURRENCY`, default 4) — all concurrent with the chain. Cuts real wall-clock with zero coverage loss: no gate dropped, none renumbered, none silently skipped. `--dry` previews are unchanged (still the original flat serial walk, byte-for-byte, so `selftest`'s exact-output assertions keep passing).
10
+ - **All agent seats pinned to `sonnet` + `xhigh` reasoning effort, replacing the prior fable/opus ceiling-ladder pins (PR #1999).** Operator's direct standing instruction, applied across `a2ui-maintenance-agent`, `a2ui-planner-agent`, `demo-audit-agent`, `framework-checker`, `framework-planner-agent`, `gen-ui-review-agent`, `package-release-agent`, `primitive-authoring-agent`. See root `CHANGELOG.md` for the cross-plugin note.
11
+
12
+ ### Fixed
13
+ - **`__init__.py`'s Hermes manifest generator's `register_skill` call passed a plain string where the runtime requires a `pathlib.Path` (calls `path.exists()`) — wrapped, discovered incidentally while building `adia-ui-factory`'s Hermes `delegate_task` command (PR #2000).**
14
+
15
+ ### Maintenance
16
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
17
+ - **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
18
+
3
19
  ## [0.8.51] — 2026-08-24
4
20
 
5
21
  ### Added
package/README.md CHANGED
@@ -8,7 +8,7 @@ Re-engineered 2026-07 for the Fable 5 harness (design:
8
8
  `.claude/docs/specs/plugin-estate-v2.md`): one skill per job, explicit
9
9
  invocation dials, references for depth, mechanical rules in hooks, thin agents.
10
10
 
11
- ## Skills (9)
11
+ ## Skills (11)
12
12
 
13
13
  | Skill | Species | Job |
14
14
  |---|---|---|
@@ -21,6 +21,8 @@ invocation dials, references for depth, mechanical rules in hooks, thin agents.
21
21
  | `site-deployment` | procedural | exe.dev service ops — site deploys, VM diagnosis, secret rotation |
22
22
  | `site-docs-authoring` | procedural | author/review `site/pages/` docs pages against the shared skeleton |
23
23
  | `ssr-compatibility` | knowledge | answers SSR failure-shape questions (linkedom/Astro consumers) |
24
+ | `cross-harness-authoring-standards` | procedural | author consumer-facing multi-harness install instructions |
25
+ | `cross-harness-compatibility-standards` | procedural | engineering standard for building a capability that genuinely works across Claude/Codex/Pi/Hermes (ADR-0091) |
24
26
 
25
27
  ## Agents (8, thin shells)
26
28
 
package/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """adia-ui-kit-forge — generated by scripts/build/harness-manifests.mjs. Do not edit."""
2
2
  import os
3
+ from pathlib import Path
3
4
 
4
5
  _HERE = os.path.dirname(os.path.abspath(__file__))
5
6
 
@@ -7,46 +8,56 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
7
8
  def register(ctx):
8
9
  ctx.register_skill(
9
10
  name="a2ui-maintenance",
10
- path=os.path.join(_HERE, "skills", "a2ui-maintenance", "SKILL.md"),
11
+ path=Path(os.path.join(_HERE, "skills", "a2ui-maintenance", "SKILL.md")),
11
12
  description="Maintains the A2UI pipeline (packages/gen-ui/a2ui/): the chunk corpus, compose strategies (zettel, chunk-zettel, free-form, monolithic), retrieval, validator, calibration, evals, the a2ui MCP server. Use to author/harvest/fix chunks, tune STRONG_MATCH or zettel thresholds, validate an A2UI document, diagnose an eval gap/regression or lift a semantic fail, change MCP tools (generate_ui, compose_from_chunks, check_anti_patterns, refine_composition), scan anti-patterns, run pipeline ops, or when a contract can't express a shape. NOT for app screens (screen-composition), runtime gen-UI features (gen-ui-wiring), primitive authoring (primitive-authoring), or gallery scoring (gen-ui-review).",
12
13
  )
14
+ ctx.register_skill(
15
+ name="cross-harness-authoring-standards",
16
+ path=Path(os.path.join(_HERE, "skills", "cross-harness-authoring-standards", "SKILL.md")),
17
+ description="Authors copy-paste install instructions for an adia-ui plugin (or any Claude Code plugin) across multiple agent harnesses — Claude Code, Codex, Pi, Hermes. Use when asked to \"write a Slack post for installing our plugins\", \"how do devs on Codex/Pi/Hermes install this\", \"update the multi-harness install docs\", or after a harness-distribution change (a new .codex-plugin/, plugin.yaml, or package.json \"pi\" field) needs its install instructions to catch up. NOT for the manifest/build mechanics themselves (package-release's Codex-manifest step, gh#1888/gh#1954) — this skill only writes the consumer-facing instructions once those exist.",
18
+ )
19
+ ctx.register_skill(
20
+ name="cross-harness-compatibility-standards",
21
+ path=Path(os.path.join(_HERE, "skills", "cross-harness-compatibility-standards", "SKILL.md")),
22
+ description="The engineering/architecture standard for actually BUILDING a Claude agent-family (or any multi-runtime capability) so it genuinely works across Claude Code, Codex, Pi, and Hermes — not the consumer-facing install docs once it already works (that's cross-harness-authoring-standards). Use when deciding whether a capability earns a new user-facing skill or should stay a thin runtime-specific agent/seat/extension, before writing a Pi extension or Hermes command and unsure whether the target API actually exists, defining a structured handoff record two or more runtimes must exchange, or naming a new plugin distribution/marketplace channel alongside an existing one. NOT for writing the resulting per-harness install commands (cross-harness-authoring-standards owns that, once the capability already works).",
23
+ )
13
24
  ctx.register_skill(
14
25
  name="demo-audit",
15
- path=os.path.join(_HERE, "skills", "demo-audit", "SKILL.md"),
26
+ path=Path(os.path.join(_HERE, "skills", "demo-audit", "SKILL.md")),
16
27
  description="Seven-mode QA sweep of the monorepo's demo/app surfaces: visual probe, app-shell QA, attr-quote typos, native-primitive leak, admin-shell composition, card anatomy, plus an aggregated token/contrast/lifecycle drift battery (`npm run dogfood:status`). Use for \"run a dogfood sweep\", \"find broken demos\", \"audit native primitive leaks\". NOT for gen-UI gallery scoring (gen-ui-review) or authoring primitives (primitive-authoring).",
17
28
  )
18
29
  ctx.register_skill(
19
30
  name="gen-ui-review",
20
- path=os.path.join(_HERE, "skills", "gen-ui-review", "SKILL.md"),
31
+ path=Path(os.path.join(_HERE, "skills", "gen-ui-review", "SKILL.md")),
21
32
  description="Closed-loop Gen UI gallery quality review — per cycle: derive ideal specs, decompose the rendered canvas, score the gap, root-cause, emit fix plans until every prompt clears the exit gate. Use for /gen-review, \"score the gallery\", \"review gen-ui outputs\". NOT for corpus fixes (a2ui-maintenance), primitive authoring (primitive-authoring), or a hand-authored demo/app-shell QA sweep (demo-audit).",
22
33
  )
23
34
  ctx.register_skill(
24
35
  name="llm-client-maintenance",
25
- path=os.path.join(_HERE, "skills", "llm-client-maintenance", "SKILL.md"),
36
+ path=Path(os.path.join(_HERE, "skills", "llm-client-maintenance", "SKILL.md")),
26
37
  description="Maintains @adia-ai/llm (packages/llm/core/): provider adapters (anthropic/openai/ gemini), the shared SSE parser, model registry, chat()/streamChat() facade, createAdapter() bridge. Use when adding or fixing a provider adapter, debugging streaming bugs (StreamChunk, no terminal `done` chunk), raw `stopReason`/usage mapping, buildRequest() or passthrough proxy dispatch (browser 401s, API key in browser), detectProvider/MODELS registry changes, or the stub adapter. NOT for wiring the client into an app (llm-wiring, adia-ui-factory plugin).",
27
38
  )
28
39
  ctx.register_skill(
29
40
  name="package-release",
30
- path=os.path.join(_HERE, "skills", "package-release", "SKILL.md"),
41
+ path=Path(os.path.join(_HERE, "skills", "package-release", "SKILL.md")),
31
42
  description="Release engineering for the @adia-ai lockstep monorepo. Use to cut and ship a release, promote [Unreleased] CHANGELOG entries, tag and push lockstep packages to origin, publish a 10-package release (roster: scripts/package-paths.mjs), publish a single plugin independently of the lockstep set (Class B), batch-push piled-up release commits, recover a cut that landed wrong or whose publish workflows didn't fire, fix a check:lockstep bump failure or an F-N1 CHANGELOG warning, verify release gates without cutting anything, or author release notes/a MIGRATION GUIDE section. NOT for consumer-app migration sweeps (app-migration) or exe.dev VM ops (site-deployment).",
32
43
  )
33
44
  ctx.register_skill(
34
45
  name="primitive-authoring",
35
- path=os.path.join(_HERE, "skills", "primitive-authoring", "SKILL.md"),
46
+ path=Path(os.path.join(_HERE, "skills", "primitive-authoring", "SKILL.md")),
36
47
  description="Author or modify AdiaUI framework source inside the monorepo — primitives (packages/web-components), shells/composites (packages/web-modules), yaml SoTs, demos. Use to add a new primitive, fix a prop/slot/attribute/CSS variant, update a yaml, build or fix a shell (chat-shell, admin-shell, editor-shell — sidebar/pane/bespoke-tier composition), promote repeated inline content into a shared module, audit a component's four-axis contract/token usage/lifecycle for drift, or author a demo or examples.html. NOT for app screens (screen-composition), A2UI internals (a2ui-maintenance), @adia-ai/llm internals (llm-client-maintenance), or site/pages docs (site-docs-authoring).",
37
48
  )
38
49
  ctx.register_skill(
39
50
  name="site-deployment",
40
- path=os.path.join(_HERE, "skills", "site-deployment", "SKILL.md"),
51
+ path=Path(os.path.join(_HERE, "skills", "site-deployment", "SKILL.md")),
41
52
  description="Deploys and operates the AdiaUI site + services on exe.dev VMs — pushing a `site-v*` tag through the hardened rsync --delete deploy to ui-kit.exe.xyz (dry-run delete summary reviewed before the real deploy job runs), diagnosis (\"Port 8000 unbound\", a 502, a stale/404ing build behind npm after the last lockstep cut), rolling back a broken deploy, VM provisioning, secret rotation. Use for \"deploy to exe.dev\", \"push a site-v* tag\", \"the site is 502ing / looks stale\", \"roll back the last deploy\", \"restart/diagnose the exe service\", \"rotate keys on the VM\". NOT for cutting the release itself (package-release).",
42
53
  )
43
54
  ctx.register_skill(
44
55
  name="site-docs-authoring",
45
- path=os.path.join(_HERE, "skills", "site-docs-authoring", "SKILL.md"),
56
+ path=Path(os.path.join(_HERE, "skills", "site-docs-authoring", "SKILL.md")),
46
57
  description="Review or author pages under site/pages/{architecture,getting-started, guides,patterns,reference}/ — the docs site (count the pages on disk; it grows). Use when asked to add or edit a getting-started/architecture/guides/patterns/reference page, review a site docs page for consistency, fix a callout that reads as plain text, or explain why an inline-code chip or a demo gallery looks broken. NOT for a component's own .examples.html demo (primitive-authoring) or any pure-primitive-composition training-harvest page (governed by composition-and-examples.md's no-style-block rule; owner: a2ui-maintenance).",
47
58
  )
48
59
  ctx.register_skill(
49
60
  name="ssr-compatibility",
50
- path=os.path.join(_HERE, "skills", "ssr-compatibility", "SKILL.md"),
61
+ path=Path(os.path.join(_HERE, "skills", "ssr-compatibility", "SKILL.md")),
51
62
  description="Answers why an AdiaUI component crashes, drops content, renders wrong, or mutates a byte-identical subtree under SSR (linkedom/Astro) — the known failure shapes, what's fixed vs open, how to prove a fix under the linkedom shim gate. Use for \"does this work under SSR\", why a component crashes on attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/ `instanceof Node` under a DOM shim, why table-ui/chart-ui/select-ui or a container CE renders empty or drops nested children server-rendered, whether getBoundingClientRect() is safe in connectedCallback, whether a custom render()/connected() path adopts-in-place or rebuilds a server-rendered subtree that already matches (zero-subtree-mutation / AC-004a-shaped asks), whether a querySelector-guard-before-innerHTML component is SSR-safe, or whether a shim can be deleted after a fix ships. ANSWERS only. NOT for a fix (primitive-authoring) or host/hydration wiring (host-wiring, adia-ui-factory).",
52
63
  )
@@ -12,10 +12,12 @@ description: |
12
12
  tools: Read, Grep, Glob, Edit, Write, Bash
13
13
  skills:
14
14
  - a2ui-maintenance
15
- # Explicit pin (gh#618): never `inherit` — a Fable/planning caller would
16
- # silently run this seat's pipeline-tuning and eval-diagnosis work on the
17
- # caller's tier. Coding/execution seat opus.
18
- model: opus
15
+ # Explicit pin (gh#618): never `inherit` — a caller on a cheaper tier would
16
+ # silently run this seat's pipeline-tuning and eval-diagnosis work on its
17
+ # own tier instead. Operator's explicit standing instruction for this seat
18
+ # family: sonnet + xhigh.
19
+ model: sonnet
20
+ effort: xhigh
19
21
  ---
20
22
 
21
23
  The a2ui-maintenance-agent works the pipeline to the preloaded `a2ui-maintenance` procedure:
@@ -15,11 +15,11 @@ skills:
15
15
  - break-down-problem
16
16
  - doc-writing-rules
17
17
  - a2ui-maintenance
18
- # Planning & architecture row (agent-writing-rules' seat ladder): fable + high, never
19
- # below fable. Same reasoning as framework-planner-agent a catalog-cohesion decision
20
- # sets the ceiling on what a2ui-maintenance-agent builds against it.
21
- model: fable
22
- effort: high
18
+ # Explicit pin: never `inherit` a catalog-cohesion decision sets the
19
+ # ceiling on what a2ui-maintenance-agent builds against it. Operator's
20
+ # explicit standing instruction for this seat family: sonnet + xhigh.
21
+ model: sonnet
22
+ effort: xhigh
23
23
  ---
24
24
 
25
25
  The a2ui-planner-agent decomposes one named A2UI catalog-system change via the preloaded
@@ -16,10 +16,10 @@ tools: Read, Grep, Glob, Bash
16
16
  skills:
17
17
  - demo-audit
18
18
  # Explicit pin (gh#618, tier corrected gh#1045): a review/critic seat's
19
- # verdict must not depend on the caller's model tier — never `inherit`,
20
- # never below fable (the ceiling ladder's Review row).
21
- model: fable
22
- effort: high
19
+ # verdict must not depend on the caller's model tier — never `inherit`.
20
+ # Operator's explicit standing instruction for this seat family: sonnet + xhigh.
21
+ model: sonnet
22
+ effort: xhigh
23
23
  ---
24
24
 
25
25
  The demo-audit-agent runs the preloaded dogfood sweep procedure and returns
@@ -13,11 +13,11 @@ description: |
13
13
  tools: Read, Grep, Glob, Bash
14
14
  skills:
15
15
  - primitive-authoring
16
- # Review / hard-bug analysis row (agent-writing-rules' seat ladder), pinned explicitly —
17
- # never `inherit`, same reasoning as demo-audit-agent's own explicit pin (gh#618): a
18
- # critic's verdict must not depend on the caller's tier.
19
- model: fable
20
- effort: high
16
+ # Explicit pin (gh#618) never `inherit`, same reasoning as demo-audit-agent's
17
+ # own explicit pin: a critic's verdict must not depend on the caller's tier.
18
+ # Operator's explicit standing instruction for this seat family: sonnet + xhigh.
19
+ model: sonnet
20
+ effort: xhigh
21
21
  ---
22
22
 
23
23
  The framework-checker sweeps the named scope (the whole primitive corpus, or a
@@ -14,12 +14,13 @@ tools: Read, Grep, Glob, Write, Edit
14
14
  skills:
15
15
  - break-down-problem
16
16
  - doc-writing-rules
17
- # Planning & architecture row (agent-writing-rules' seat ladder, ratified 2026-07-12):
18
- # fable + high, never below fable, effort range high-xhigh. A decomposition seat's
19
- # output sets the ceiling on everything primitive-authoring-agent builds downstream from
20
- # it; a cheap tier would make load-bearing architectural calls with no independent check.
21
- model: fable
22
- effort: high
17
+ # Explicit pin: never `inherit` a decomposition seat's output sets the
18
+ # ceiling on everything primitive-authoring-agent builds downstream from it;
19
+ # a cheap tier would make load-bearing architectural calls with no
20
+ # independent check. Operator's explicit standing instruction for this seat
21
+ # family: sonnet + xhigh.
22
+ model: sonnet
23
+ effort: xhigh
23
24
  ---
24
25
 
25
26
  The framework-planner-agent decomposes one named framework change via the preloaded
@@ -17,10 +17,10 @@ tools: Read, Grep, Glob, Bash
17
17
  skills:
18
18
  - gen-ui-review
19
19
  # Explicit pin (gh#618, tier corrected gh#1045): a review/critic seat's
20
- # verdict must not depend on the caller's model tier — never `inherit`,
21
- # never below fable (the ceiling ladder's Review row).
22
- model: fable
23
- effort: high
20
+ # verdict must not depend on the caller's model tier — never `inherit`.
21
+ # Operator's explicit standing instruction for this seat family: sonnet + xhigh.
22
+ model: sonnet
23
+ effort: xhigh
24
24
  ---
25
25
 
26
26
  The gen-ui-review-agent runs the preloaded gen-ui-review cycle and returns
@@ -14,8 +14,10 @@ skills:
14
14
  - package-release
15
15
  # Explicit pin (gh#618): never `inherit` — this seat runs UNATTENDED cuts,
16
16
  # where a cheap caller tier would make live gate-failure judgment calls
17
- # with no operator watching. Execution seat with real judgment → opus.
18
- model: opus
17
+ # with no operator watching. Operator's explicit standing instruction for
18
+ # this seat family: sonnet + xhigh.
19
+ model: sonnet
20
+ effort: xhigh
19
21
  ---
20
22
 
21
23
  The package-release-agent runs the preloaded `package-release` procedure under its
@@ -11,6 +11,7 @@ tools: Read, Grep, Glob, Edit, Write, Bash
11
11
  skills:
12
12
  - primitive-authoring
13
13
  model: sonnet
14
+ effort: xhigh
14
15
  ---
15
16
 
16
17
  The primitive-authoring-agent builds framework source to the preloaded `primitive-authoring`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.51",
3
+ "version": "0.8.52",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — 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",
package/plugin.yaml CHANGED
@@ -1,4 +1,4 @@
1
1
  name: "adia-ui-kit-forge"
2
- version: "0.8.51"
2
+ version: "0.8.52"
3
3
  description: "Maintain the adia-ui (@adia-ai) framework itself — 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-ui-kit-factory (the consumer/app-author plugin)."
4
4
  manifest_version: 1
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: cross-harness-authoring-standards
3
+ description: >-
4
+ Authors copy-paste install instructions for an adia-ui plugin (or any
5
+ Claude Code plugin) across multiple agent harnesses — Claude Code, Codex,
6
+ Pi, Hermes. Use when asked to "write a Slack post for installing our
7
+ plugins", "how do devs on Codex/Pi/Hermes install this", "update the
8
+ multi-harness install docs", or after a harness-distribution change
9
+ (a new .codex-plugin/, plugin.yaml, or package.json "pi" field) needs its
10
+ install instructions to catch up. NOT for the manifest/build mechanics
11
+ themselves (package-release's Codex-manifest step, gh#1888/gh#1954) — this
12
+ skill only writes the consumer-facing instructions once those exist.
13
+ disable-model-invocation: false
14
+ user-invocable: true
15
+ ---
16
+
17
+ # cross-harness-authoring-standards — one command per harness, zero invented syntax
18
+
19
+ Every harness's install path is a claim someone will paste verbatim into a terminal. A
20
+ plausible-looking command that was never verified against this repo's actual shipped
21
+ manifests is worse than no instructions — it fails silently or errors somewhere the
22
+ author never sees. The whole discipline is: **read the real generated file before
23
+ describing what it does.**
24
+
25
+ ## The verification-before-writing gate `[gate]`
26
+
27
+ Before writing one line of install instructions for a harness, verify against the strongest
28
+ source actually reachable — never infer a harness's install surface from another harness's
29
+ shape, and never invent a CLI subcommand you have not seen documented or confirmed:
30
+
31
+ - **Strongest: the target harness's own agent, live.** If a Codex/Pi/Hermes agent is
32
+ reachable (a peer session, a cross-harness message), have it check or correct the draft
33
+ against its own real CLI/config surface directly — it knows its own tool's commands with
34
+ more authority than any manifest file a human read once and wrote down. A harness section
35
+ corrected this way outranks a manifest-file read on the same point; update the draft to
36
+ match, don't average the two.
37
+ - **Fallback: the repo's own shipped manifest for that harness** (below), when no live
38
+ agent is reachable.
39
+
40
+ | Harness | What to read before writing anything |
41
+ | --- | --- |
42
+ | Claude Code | The plugin's own `README.md` "Install" section + `.claude-plugin/marketplace.json` (or the published npm marketplace manifest) |
43
+ | Codex | `.codex-plugin/plugin.json` + `codex.interface.json` + the repo's own `AGENTS.md` "Non-Claude harnesses" section (gh#1888 — the canonical degradation-table citation) |
44
+ | Hermes | `plugin.yaml` + `__init__.py` at the package root, and whether an `hermes-mcp.yaml` (or equivalent) fragment exists — Hermes MCP is user-level config, not a plugin field, in every verified case so far |
45
+ | Pi | `package.json`'s `"pi"` field + `pi-package` keyword, and `prompts/*.md` if present |
46
+
47
+ If a harness's manifest file doesn't exist yet in the repo, that harness has **no verified
48
+ install path** — say so plainly (`cross-harness-authoring-standards` never fabricates a
49
+ "probably works like X" command) and route the gap to the maintainer skill/issue that owns
50
+ harness-manifest generation, rather than guessing syntax.
51
+
52
+ ## The four-axis degradation table
53
+
54
+ Every harness other than Claude Code lacks at least one of: commands, agent seats, hooks,
55
+ native MCP. State the gap plainly per harness, in the instructions themselves — a dev who
56
+ installs expecting the full surface and silently gets less is worse served than one told
57
+ up front:
58
+
59
+ - **Commands** → Codex/Pi/Hermes have no slash-command layer in the verified cases; a
60
+ command degrades to invoking its underlying skill directly (Codex, Hermes) or, where the
61
+ harness's own prompt-template layer supports genuine commands (Pi's `prompts/*.md`),
62
+ upgrades instead of degrading — check per harness, don't assume degradation is universal.
63
+ - **Agent seats** → do not exist outside Claude Code. Never write "then invoke
64
+ `<agent-name>`" for Codex/Pi/Hermes; give the entry-point *skill* that agent dispatches
65
+ internally instead (see "The common starting prompt" below).
66
+ - **Hooks** → Codex/Pi/Hermes have no hook runtime in the verified cases; state "no
67
+ hooks surface" rather than omitting the topic (an omission reads as "works the same").
68
+ - **MCP** → the highest-risk gap. Verify per harness: does it read `.mcp.json` unmodified
69
+ (Codex — verified), read it via a third-party adapter the dev must separately install
70
+ (Pi's `pi-mcp-adapter` — verified 2026-08-24), or require hand-merging a generated
71
+ fragment into the harness's own user-level config (Hermes — verified, no plugin-level MCP
72
+ field exists)? If the plugin's whole value proposition is its MCP server, say that
73
+ explicitly before promising support — a silent no-op MCP install is the worst failure
74
+ mode this skill exists to prevent.
75
+
76
+ ## The common starting prompt
77
+
78
+ A "getting started" prompt that opens with an agent name breaks on every harness but
79
+ Claude Code. Give two forms:
80
+
81
+ - **Claude Code**: name the coordinating agent directly (`Use <agent-name> to build
82
+ [...]`).
83
+ - **Everywhere else**: name the entry-point *skill* that agent dispatches internally —
84
+ same routing, no agent-seat dependency. Confirm the skill is genuinely the entry point
85
+ (its own description says "run FIRST" / "cold-start router") rather than picking an
86
+ arbitrary skill from the roster.
87
+
88
+ ```
89
+ Bad (breaks on Codex/Pi/Hermes — no agent seat exists):
90
+ Use ui-architect to build a settings page.
91
+
92
+ Good (Claude Code):
93
+ Use ui-architect to build a settings page. Check pattern-catalog for an
94
+ existing pattern before composing from scratch.
95
+
96
+ Good (Codex/Pi/Hermes — same routing, no agent dependency):
97
+ Use the app-planning skill to orient a new settings page. Check
98
+ pattern-catalog for an existing pattern before composing from scratch,
99
+ then use screen-composition to build it.
100
+ ```
101
+
102
+ ## Structure
103
+
104
+ One block per harness, each self-contained (a reader only cares about their own harness):
105
+ install command(s) → any manual wiring step (config file edit, separate adapter install) →
106
+ the degradation summary (what's full-strength, what's reduced, what's absent) for that
107
+ harness specifically. Never a single combined command block trying to cover two harnesses —
108
+ the reader copy-pastes the whole block for their tool and nothing else.
109
+
110
+ ## Failure branches
111
+
112
+ A harness with no verified manifest in the repo → report the gap, do not draft syntax for
113
+ it. A degradation claim that contradicts what a just-read manifest file actually shows
114
+ (e.g. writing "no MCP" for a harness whose `.mcp.json` passthrough is confirmed working) →
115
+ re-read the source file; the manifest is authoritative over any prior draft, including this
116
+ skill's own past output for an earlier plugin version.
117
+
118
+ Done when every harness section cites the specific file it was verified against (in prose,
119
+ not a footnote) and no command exists in the output that wasn't read from a real shipped
120
+ artifact.
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "Cross Harness Authoring Standards"
3
+ short_description: "Authors copy-paste install instructions for an adia-ui plugin (or any Claude Code plugin) across multiple agent harnesses — Claude Code, Codex, Pi, Hermes."
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: cross-harness-compatibility-standards
3
+ description: >-
4
+ The engineering/architecture standard for actually BUILDING a Claude
5
+ agent-family (or any multi-runtime capability) so it genuinely works
6
+ across Claude Code, Codex, Pi, and Hermes — not the consumer-facing
7
+ install docs once it already works (that's
8
+ cross-harness-authoring-standards). Use when deciding whether a
9
+ capability earns a new user-facing skill or should stay a thin
10
+ runtime-specific agent/seat/extension, before writing a Pi extension or
11
+ Hermes command and unsure whether the target API actually exists,
12
+ defining a structured handoff record two or more runtimes must exchange,
13
+ or naming a new plugin distribution/marketplace channel alongside an
14
+ existing one. NOT for writing the resulting per-harness install commands
15
+ (cross-harness-authoring-standards owns that, once the capability
16
+ already works).
17
+ disable-model-invocation: false
18
+ user-invocable: true
19
+ ---
20
+
21
+ # cross-harness-compatibility-standards — build it portable, don't just document it
22
+
23
+ `cross-harness-authoring-standards` covers the downstream concern: writing the
24
+ consumer-facing install instructions once a capability already works across
25
+ harnesses. This skill covers the upstream concern — the engineering standard
26
+ for actually **building** that capability so it works on Claude Code, Codex,
27
+ Pi, and Hermes in the first place. If you're drafting a "how to install"
28
+ block, you want the other skill; if you're deciding whether something needs
29
+ a new skill, writing a Pi/Hermes adapter, or designing a structured record
30
+ two runtimes must exchange, you're in the right place.
31
+
32
+ This standard is not invented doctrine — it is extracted from
33
+ [ADR-0091](../../../../../docs/ops/adr/adr-0091-ui-architect-cross-harness-portability.md)
34
+ and its companion
35
+ [PRD](../../../../../docs/ops/prd/prd-ui-architect-cross-harness-portability.md)/
36
+ [SPEC](../../../../../docs/ops/spec/spec-ui-architect-cross-harness-portability.md)/
37
+ [LLD](../../../../../docs/ops/lld/lld-ui-architect-cross-harness-portability.md),
38
+ ratified and built across four merged PRs (#1997 docs, #1998 skill + contracts
39
+ + Codex marketplace, #1999 agent pinning, #2000 Pi extension + Hermes
40
+ command) that made `ui-architect` portable. Every rule below cites the
41
+ decision or requirement it comes from — re-read the source before extending
42
+ this standard, don't paraphrase from this skill alone.
43
+
44
+ ## Rule 1 — skill vs. thin adapter: earn the promotion `[gate]`
45
+
46
+ Substantive reusable behavior belongs in a portable skill body; a
47
+ runtime-specific agent/seat/extension stays **only if** it provides one of:
48
+ restricted tools, isolated context, parallelism, multi-skill preload, or a
49
+ distinct model/effort configuration (ADR-0091 D1). Delete an agent, or fold
50
+ it into its skill, if it provides none of those structural properties. Never
51
+ convert every internal seat into a user-facing skill merely because
52
+ portability is required — a capability is user-invocable only when a user
53
+ has a sensible reason to request it directly, not because promoting it was
54
+ convenient.
55
+
56
+ Apply the rule by checking each existing seat against it, on the record, not
57
+ by assertion:
58
+
59
+ - `ui-architect` was the one seat of four in `adia-ui-factory` missing its
60
+ skill twin — it earned `skills/ui-architect/SKILL.md` because the
61
+ Gear-selection/wave/generator-≠-reviewer/four-lap-cap/failure-routing
62
+ procedure is exactly the "substantive reusable behavior" class the rule
63
+ routes to a skill, and Codex/Pi/Hermes users are the concrete "sensible
64
+ reason to request it directly." The agent seat was **retained, not
65
+ replaced**: no Write/Edit (restricted tools), dispatches into isolated
66
+ per-screen contexts (isolated context + parallelism), pinned `fable`/
67
+ `high` never-`inherit` (distinct model/effort) — independently sufficient
68
+ to keep it.
69
+ - `app-planning-agent`, `screen-composition-agent`, `surface-qa-agent`
70
+ already each preloaded a same-named user-invocable skill before this
71
+ change touched anything — the rule was already satisfied; nothing about
72
+ their shape changed beyond the same Claude-adapter slimming (Rule 3)
73
+ applied to all four seats equally.
74
+ - `wave-coordination` stayed `user-invocable: false`, Claude-only,
75
+ unpromoted — team-pattern *mechanics* for one runtime's dispatch model,
76
+ not a capability a Codex/Pi/Hermes user has a sensible reason to request
77
+ directly. Failing the rule's own admission test on purpose is a legitimate
78
+ outcome, not an oversight to fix later.
79
+
80
+ ## Rule 2 — verify before coding, per harness, every time `[gate]`
81
+
82
+ Never assume a target harness's API, extension mechanism, or delegation
83
+ surface from documentation alone. Check the actually-installed version
84
+ live — read its real source, run its real CLI — before writing one line of
85
+ adapter code against it. This is the single most load-bearing lesson from
86
+ how `ui-architect`'s Pi and Hermes adapters were actually built (PR #2000):
87
+
88
+ - **Pi** was verified present by reading the installed
89
+ `@earendil-works/pi-coding-agent` package directly: its own shipped
90
+ `examples/extensions/subagent/` proved a real subprocess-based
91
+ subagent-dispatch pattern (`registerCommand` + `child_process` spawn of a
92
+ nested `pi --mode json -p --no-session`), and `package.json`'s
93
+ `pi.extensions` field was confirmed real and documented — not inferred by
94
+ analogy to Claude's Agent tool. Once verified, the extension was
95
+ **live-tested end to end**: a real `pi` session ran `/ui-architect`
96
+ against a live model, dispatched all three isolated passes, wrote a real
97
+ file, took a real screenshot, and returned a clean `VerifyProof`.
98
+ - **Hermes** was verified sufficient by reading the installed Hermes
99
+ Agent's own source — `PluginContext.register_command` + `dispatch_tool`,
100
+ and `tools/delegate_tool.py`'s `delegate_task(goal, context, role, ...)` —
101
+ confirmed as real, current APIs before the generator surgery (Rule 3)
102
+ was written against them. The generated command was built behind its own
103
+ `--selftest` fixture (a real `py_compile` check plus a fake `delegate_task`
104
+ double driving the actual generated logic against the real linters)
105
+ *before* wiring it into the real plugin tree, then verified live via
106
+ `hermes plugins doctor --ci`. A full interactive dispatch was blocked by
107
+ the build sandbox refusing to mutate Hermes's global plugin-enable
108
+ state — that limitation was **noted honestly in the README rather than
109
+ worked around or silently dropped**.
110
+
111
+ If a required capability is genuinely absent, or materially different from
112
+ what the docs claimed (SPEC REQ-011/012's capability-boundary discipline):
113
+ stop that adapter at an explicit, clearly-labeled capability boundary and
114
+ record four things — the API checked, the observed mismatch, the smallest
115
+ viable fallback, and the remaining work. Never simulate success with an
116
+ unverified manifest field or a dead adapter file that looks like it works
117
+ but was never actually exercised. One of the two outcomes (working adapter
118
+ + passing native smoke test, or a capability-boundary record with all four
119
+ fields) must exist — never neither, never both.
120
+
121
+ ## Rule 3 — one authored copy, many thin consumers
122
+
123
+ The portable behavior lives in exactly **one** skill body. Every runtime's
124
+ own adapter mechanism *consumes* that one body — it never gets a second,
125
+ hand-copied prose duplicate:
126
+
127
+ - **Claude** preloads it (`skills:` frontmatter on the owning agent file).
128
+ - **Codex** auto-derives its manifest sibling
129
+ (`skills/<name>/agents/openai.yaml`) from the same skill frontmatter via
130
+ this repo's own generator (`scripts/build/codex-manifests.mjs`) — no
131
+ manual authoring, ever.
132
+ - **Pi**'s extension reads the skill file *at runtime* rather than copying
133
+ its prose into the extension source (`ui-architect`'s
134
+ `pi/ui-architect.extension.ts` loads agent descriptions from the
135
+ plugin-local skill directly).
136
+ - **Hermes**'s command derives from the same source
137
+ (`scripts/build/harness-manifests.mjs`'s generated `__init__.py`
138
+ registers the skill; the generated `/ui-architect` command runs the same
139
+ planning/composition/QA loop through `delegate_task`, gated by the same
140
+ contracts).
141
+
142
+ A change to the workflow means editing one file, not four. If you find
143
+ yourself pasting a skill's prose into an agent file, an extension, or a
144
+ generator template instead of citing or reading it, stop — that's the
145
+ duplication this rule exists to prevent.
146
+
147
+ ## Rule 4 — one contract grammar, not a schema per runtime
148
+
149
+ When two or more runtimes need to exchange a structured handoff record,
150
+ define it once as a plain strict-prose-record shape validated by a small
151
+ stdlib CLI linter — never introduce JSON Schema or another
152
+ schema-validation library into every runtime's own toolchain to do the same
153
+ job (ADR-0091 D3). This repo's own `record-lint` family is the reusable
154
+ grammar: `<script> <file>|-|selftest`, exit 0 clean / 1 on findings, with
155
+ `UNMEASURED — <reason>` as a legal per-slot value (silent omission is not
156
+ legal). `OrientationRecord` (`scripts/record-lint`), `VerifyProof`
157
+ (`scripts/verify-proof-lint`), and `BuildResult`
158
+ (`scripts/build-result-lint`) — all three in `adia-ui-factory`'s
159
+ `scripts/` — are structurally identical siblings, not three different
160
+ formats doing the same job. Every runtime adapter that produces or consumes
161
+ one of these contracts calls the same linter script; a second,
162
+ runtime-specific parsing or validation implementation for the same contract
163
+ is a defect, not a stylistic choice.
164
+
165
+ Before minting a new contract shape, check whether an existing one already
166
+ covers it under a different name — reuse beats a fourth near-duplicate
167
+ grammar.
168
+
169
+ ## Rule 5 — name distribution channels distinctly
170
+
171
+ When the same plugin ships **both** an in-repo/access-gated channel and a
172
+ public/npm-sourced channel for the same runtime, name them distinctly. A
173
+ collision — both channels sharing one marketplace name — makes
174
+ `<plugin>@<marketplace-name>` resolution ambiguous for a consumer trying to
175
+ install from a specific channel.
176
+
177
+ The confirmed, working precedent is Claude's own two-channel split: the
178
+ repo-root, access-gated `.claude-plugin/marketplace.json` is named
179
+ `adia-ui-kit`; the public, npm-sourced
180
+ `packages/plugins/adia-plugins/marketplace.json` is named `adia-plugins`
181
+ (ADR-0091 D2's own table) — two distinct names, two distinct reachability
182
+ tiers, no ambiguity for a consumer running `/plugin install
183
+ <plugin>@<marketplace-name>`.
184
+
185
+ ADR-0091 D2 designed the Codex side to mirror that same split (a
186
+ repo-root/local-source channel alongside the existing
187
+ `packages/plugins/adia-plugins/.agents/plugins/marketplace.json` npm
188
+ sibling) — but the LLD (§C7) flagged the new manifest's `name` field as an
189
+ explicit, unverified, build-time question: does Codex key off `name` for
190
+ `<plugin>@<marketplace-name>` resolution the way Claude does? **Verify this
191
+ against the actually-shipped manifests before citing the Codex side as a
192
+ settled precedent** — Rule 2's verify-before-coding discipline applies to
193
+ this rule's own worked example, not only to Pi/Hermes adapters. Diff the
194
+ `name` field of both Codex marketplace manifests against each other before
195
+ assuming they follow the Claude-side pattern.
196
+
197
+ ## Failure branches
198
+
199
+ A capability promoted to a user-facing skill with none of Rule 1's five
200
+ qualifying properties on its remaining agent seat → fold the seat into the
201
+ skill or delete it; don't keep a Write/Edit-free, non-isolated, no-model-pin
202
+ shell around "for consistency." An adapter written against a harness's
203
+ documented API with no live verification → stop, verify against the
204
+ installed runtime first (Rule 2); a passing test against a mocked or
205
+ assumed API is not verification. A second hand-copied prose block for a
206
+ skill's workflow anywhere outside its one `SKILL.md` → delete the copy,
207
+ point the consumer at the source (Rule 3). A new structured record
208
+ introduced as JSON Schema, or as a shape that already has a linter under a
209
+ different name → stop, extend `record-lint`'s family instead or reuse the
210
+ existing contract (Rule 4). A new distribution manifest reusing an existing
211
+ sibling's marketplace name → rename before shipping (Rule 5).
212
+
213
+ Done when: every new or promoted seat cites the specific Rule 1 property
214
+ that keeps it (or is deleted/folded); every harness adapter names the API
215
+ it verified live and how (Rule 2); the skill body has exactly one authored
216
+ copy with every consumer citing or reading it, never duplicating it (Rule
217
+ 3); every cross-runtime contract has exactly one shape and one stdlib
218
+ linter (Rule 4); and every distribution channel's marketplace name is
219
+ diffed against its siblings and confirmed distinct (Rule 5).
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "Cross Harness Compatibility Standards"
3
+ short_description: "The engineering/architecture standard for actually BUILDING a Claude agent-family (or any multi-runtime capability) so it genuinely works across Claude Code, Codex, Pi, and Hermes — not the consumer-facing install docs once it already works (that's cross-harness-authoring-standards)."
@@ -89,6 +89,8 @@ When only source *hashes* move and chunk content does not, `check:embeddings-fre
89
89
 
90
90
  ### 3.1 The full roster — every gate runs; a subset = pre-flight failure
91
91
 
92
+ **Execution model (gh#2006): three phases, not one serial walk.** `step3PreFlight()` runs gate 4 solo first (see its own note below), then gates 16 → 27 → 28 strictly in order (the eval-health write-then-read dependency — gate 28 reads whichever `evals/mcp/runs/` directory sorts lexically LAST, so nothing else may write there between 27 and 28), concurrently with a bounded pool running every other gate at once (`PREFLIGHT_CONCURRENCY`, default 4 — override for a dedicated/idle host). Every gate still resolves the same command, still fails the whole pre-flight on a red result, and still reports its own number — only the WALL-CLOCK schedule changed, never the roster below or its numbering. `--dry` previews stay the original flat serial walk unchanged.
93
+
92
94
  ```bash
93
95
  node scripts/build/components.mjs --verify # 1 yaml ↔ sidecar ↔ .d.ts
94
96
  npm run verify:traits # 2 trait coverage
@@ -60,7 +60,8 @@
60
60
  // block BEFORE the prompt (H5) — never a bare y/N — whether it waits for input
61
61
  // (manual flags) or auto-confirms (--go).
62
62
 
63
- import { execSync } from 'node:child_process';
63
+ import { execSync, exec as execCb } from 'node:child_process';
64
+ import { promisify } from 'node:util';
64
65
  import { createInterface } from 'node:readline/promises';
65
66
  import { stdin, stdout } from 'node:process';
66
67
  import fs from 'node:fs';
@@ -347,6 +348,15 @@ function shCapture(cmd, args) {
347
348
  }
348
349
  }
349
350
 
351
+ // Async, CAPTURING exec — the concurrent pre-flight phases (gh#2006) can't
352
+ // use sh()'s stdio:'inherit' (N processes' live output interleaved is
353
+ // unreadable), so every gate's stdout+stderr is captured and printed as one
354
+ // block the moment that gate finishes instead. maxBuffer generous (64 MB) —
355
+ // a truncated capture on a real failure (tsc, vitest) would read as a false
356
+ // pass/silent-gap the same class of defect shFailure's own doc-comment warns
357
+ // against.
358
+ const execAsync = promisify(execCb);
359
+
350
360
  // ── Half-cut detection (gh#765) ──────────────────────────────────
351
361
  // An aborted Step-4 run (the v0.8.29 cut aborted three times at Step 4e)
352
362
  // leaves a half-cut tree: package versions bumped to the cut version +
@@ -428,53 +438,245 @@ function step1ReBaseline(args) {
428
438
  console.log(' (empty — origin not ahead)');
429
439
  }
430
440
 
431
- // ── Step 3 — Pre-flight gates ────────────────────────────────────
432
- function step3PreFlight(args) {
433
- if (args.skipGates) {
434
- console.log('\n=== Step 3 Pre-flight (SKIPPED via --skip-gates; danger!) ===');
441
+ // ── Step 3 — Pre-flight gates (gh#2006) ───────────────────────────
442
+ //
443
+ // gh#1987's audit found the real "feels slow" lever isn't roster size (all
444
+ // 32 gates are justified defense-in-depth `main`'s branch protection only
445
+ // hard-requires the browser-scheme probe, so the `npm run check` name-
446
+ // duplicates in this roster are real re-checks, not dead weight) — it's
447
+ // that step3PreFlight ran every gate one at a time. Most of the 32 are
448
+ // independent, read-only checks with no shared mutable state; this splits
449
+ // them into three phases instead of one flat serial for-loop. Two real
450
+ // sequencing requirements survived the audit — everything else runs
451
+ // concurrently:
452
+ //
453
+ // Phase 1 (solo) — gate 4 (test:unit:serial) alone, no sibling gate
454
+ // process running at the same time. Not a proven requirement — the
455
+ // roster's own "serial on purpose" note is about vitest's OWN
456
+ // --no-file-parallelism, not about co-running with sibling PROCESSES —
457
+ // but a deliberately conservative one: gate 4 is the roster's single
458
+ // heaviest gate, and this repo's own host-audit F3 remedy
459
+ // (scripts/dev/worker-cap.mjs, gh#1792) exists precisely because
460
+ // unbounded concurrent lanes on a shared multi-session host turn a gate
461
+ // that's clean in isolation red under contention. Revisit with real
462
+ // measurement before loosening this.
463
+ // Phase 2 (concurrent) — two workstreams launched together and awaited
464
+ // together:
465
+ // (a) a bounded-concurrency pool of every remaining gate EXCEPT the
466
+ // eval-health chain below — each independently read-only
467
+ // (--verify/--fix-gated/dry-diff, checked directly against every
468
+ // script in the roster) or, for gate 17 (dogfood:status), writing
469
+ // only to a gate-private path (qa/findings/dogfood-tracker.md)
470
+ // no other gate reads or writes;
471
+ // (b) the eval-health chain — gates 16 → 27 → 28 — run strictly
472
+ // serially relative to EACH OTHER (still concurrent with (a),
473
+ // since neither touches (a)'s files). This is the roster's one
474
+ // CSS-before-JS-shaped file-write ordering dependency: gate 27
475
+ // (`eval:diff --engine free-form`) writes a fresh
476
+ // evals/mcp/runs/<ISO>/ directory; gate 28
477
+ // (write-eval-health.mjs) reads whichever directory under
478
+ // evals/mcp/runs/ SORTS LEXICALLY LAST — not specifically the
479
+ // latest free-form run — and expects free-form.json there. Gate
480
+ // 16 (`eval:diff --engine zettel`) writes its OWN fresh directory
481
+ // under that same root; if 16 and 27 ran concurrently and 16
482
+ // happened to finish after 27, gate 28 would silently pick 16's
483
+ // directory (no free-form.json) and commit
484
+ // evals/health/<version>.json with `engines.free-form: null` — a
485
+ // real, silent coverage loss the roster's fixed serial order
486
+ // prevents today only by construction. Keeping 16 before 27
487
+ // before 28, uninterrupted by any other evals/mcp/runs/ writer,
488
+ // preserves that guarantee.
489
+ //
490
+ // Fail-fast BETWEEN phases (a Phase 1 failure skips Phase 2 outright — no
491
+ // point spending Phase 2's wall-clock once the roster's known-flakiest gate
492
+ // is already red); fail-TOGETHER within Phase 2 (every Phase 2 gate runs to
493
+ // completion regardless of a sibling's failure, so one red pre-flight run
494
+ // reports every failing gate at once instead of the old one-at-a-time
495
+ // discovery) — the explicit fail-fast-vs-fail-together decision gh#2006's
496
+ // acceptance criteria calls for. Within the eval-health chain specifically,
497
+ // a failure DOES stop the chain (16 failing skips 27 and 28; 27 failing
498
+ // skips 28) — that trio has a real dependency relationship the rest of
499
+ // Phase 2 doesn't, so it keeps today's strict fail-fast semantics.
500
+ //
501
+ // versionArg/targetVersionArg/resume-mode branching is unchanged — every
502
+ // gate resolves its actual command through the same resolveGateCommand()
503
+ // logic step3PreFlight always applied, now factored out so all three
504
+ // phases share it verbatim.
505
+
506
+ const SOLO_GATE_NUMBER = 4; // test:unit:serial — see the phase-model note above.
507
+ const EVAL_HEALTH_CHAIN_NUMBERS = [16, 27, 28]; // eval:diff zettel → eval:diff free-form → write-eval-health.mjs
508
+
509
+ // Concurrency cap for Phase 2's pool — a fixed, deliberately conservative
510
+ // constant rather than the host's full core count. Most Phase-2 gates are
511
+ // lightweight verify/diff scripts, but a few (typecheck, the bundle
512
+ // verifiers, corpus verify) are real tsc/esbuild/Node processes, and
513
+ // worker-cap.mjs's own host-audit F3 finding is that unbounded concurrent
514
+ // lanes on a shared multi-session host reintroduce exactly the contention
515
+ // class this ticket must not reintroduce. Override via PREFLIGHT_CONCURRENCY
516
+ // for a dedicated/idle host.
517
+ const PREFLIGHT_CONCURRENCY = Number(process.env.PREFLIGHT_CONCURRENCY) || 4;
518
+
519
+ // Resolves one gate's actual command string — the same versionArg/
520
+ // targetVersionArg/resume branching step3PreFlight has always applied.
521
+ // Returns null for a versionArg gate deferred by an in-flight resume (Step
522
+ // 4g re-runs it once the notes it needs actually exist).
523
+ function resolveGateCommand(g, args) {
524
+ // versionArg gates take a MODE-DEPENDENT version (both directions bit the
525
+ // first 0.8.8 run): cut modes run pre-flight PRE-BUMP, where the README
526
+ // "Current version" claim still equals the PREVIOUS version (the bump
527
+ // moves it in Step 4; Step 4g then re-proves at the cut version) — while
528
+ // handoff runs POST-MERGE, where the claim already equals the CUT version
529
+ // and validating at previous fails by construction. A RESUMED half-cut run
530
+ // (gh#765) is post-bump too: the earlier aborted run already moved every
531
+ // reference to the cut version, so validating at previous false-fails
532
+ // gate 24 exactly like handoff would.
533
+ const hygieneVersion = (args.mode === 'handoff' || args.resume) ? args.version : args.previous;
534
+ // A RESUMED run's versionArg gates (cut-hygiene) demand artifacts Step 4
535
+ // has not recreated yet — docs/ops/releases/vX.Y.Z.md exists only after 4e,
536
+ // which runs AFTER this pre-flight. Step 4g re-runs these same gates at
537
+ // the cut version once the notes exist, so skipping them here loses no
538
+ // coverage — running them here false-fails by construction (the v0.8.32
539
+ // cut hit exactly this: 23/24 green, gate 24 dead on the missing notes).
540
+ if (g.versionArg && args.resume) return null;
541
+ if (g.versionArg) return `${g.cmd} --version ${hygieneVersion}`;
542
+ // targetVersionArg (gh#1135, REQ-06) is a distinct flag from versionArg:
543
+ // write-eval-health.mjs NAMES a new file after the cut's target version
544
+ // from the very start — there is no pre/post-bump claim to reconcile the
545
+ // way gate 24's README-currency check has, so it always takes
546
+ // `args.version` verbatim regardless of mode or resume.
547
+ if (g.targetVersionArg) return `${g.cmd} --version ${args.version}`;
548
+ return g.cmd;
549
+ }
550
+
551
+ // Runs ONE gate to completion, capturing rather than streaming its output —
552
+ // see the file-header note above for why. Never throws: a failed gate is a
553
+ // normal, expected outcome the caller decides how to act on, exactly like
554
+ // shOk()'s own contract.
555
+ async function runGateCaptured(g, args) {
556
+ const cmd = resolveGateCommand(g, args);
557
+ if (cmd === null) {
558
+ return { n: g.n, cmd: g.cmd, what: g.what, ok: true, skipped: true,
559
+ note: '[resume] deferred to Step 4g (notes not regenerated yet)' };
560
+ }
561
+ const start = Date.now();
562
+ try {
563
+ const { stdout, stderr } = await execAsync(cmd, { cwd: REPO, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
564
+ return { n: g.n, cmd, what: g.what, ok: true, stdout, stderr, ms: Date.now() - start };
565
+ } catch (e) {
566
+ return {
567
+ n: g.n, cmd, what: g.what, ok: false, ms: Date.now() - start,
568
+ status: e.code ?? (e.signal ? `signal ${e.signal}` : '?'),
569
+ stdout: e.stdout, stderr: e.stderr,
570
+ };
571
+ }
572
+ }
573
+
574
+ // Prints one captured gate's result in the same shape a serial gate would
575
+ // have streamed live, so a scan-back-through-the-log habit built on the old
576
+ // stdio:'inherit' output still works.
577
+ function printGateResult(r) {
578
+ if (r.skipped) {
579
+ console.log(`\n ${r.n}/${GATE_ROSTER.length}. ${r.cmd} # ${r.what} — ${r.note}`);
435
580
  return;
436
581
  }
437
- console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster gate-roster.mjs) ===`);
582
+ console.log(`\n ${r.n}/${GATE_ROSTER.length}. ${r.cmd} # ${r.what} (${(r.ms / 1000).toFixed(1)}s)`);
583
+ const out = [r.stdout, r.stderr].map((s) => (s || '').toString().trim()).filter(Boolean).join('\n');
584
+ if (out) console.log(out.split('\n').map((l) => ` ${l}`).join('\n'));
585
+ if (!r.ok) {
586
+ console.error(` ✗ FAILED (${r.n}/${GATE_ROSTER.length}): ${r.cmd} (exit ${r.status})`);
587
+ }
588
+ }
589
+
590
+ // Runs a fixed array of gates strictly one after another, in array order —
591
+ // the eval-health chain (16 → 27 → 28), where the ordering IS the
592
+ // correctness requirement (see the phase-model note above), never a
593
+ // scheduling nicety. A failure stops the chain — the gates after it have a
594
+ // real dependency on what it would have produced, unlike the pool below.
595
+ async function runGateChain(gates, args) {
596
+ const results = [];
597
+ for (const g of gates) {
598
+ const r = await runGateCaptured(g, args);
599
+ printGateResult(r);
600
+ results.push(r);
601
+ if (!r.ok) break;
602
+ }
603
+ return results;
604
+ }
605
+
606
+ // Runs a list of gates concurrently, capped at `limit` in flight at once —
607
+ // a minimal pool, not a dependency graph: every gate here is fully
608
+ // independent, so order WITHIN the pool carries no meaning, only the cap
609
+ // does. Every gate runs to completion regardless of a sibling's failure
610
+ // (fail-together within the pool — see the file-header note above).
611
+ async function runGatePool(gates, args, limit) {
612
+ const queue = [...gates];
613
+ const results = [];
614
+ async function worker() {
615
+ while (queue.length) {
616
+ const g = queue.shift();
617
+ const r = await runGateCaptured(g, args);
618
+ printGateResult(r);
619
+ results.push(r);
620
+ }
621
+ }
622
+ await Promise.all(Array.from({ length: Math.min(limit, gates.length) }, worker));
623
+ return results;
624
+ }
625
+
626
+ // DRY preview stays the original flat serial walk, byte-for-byte — no real
627
+ // process ever spawns in dry mode, so there is no concurrency win to chase,
628
+ // and selftest()'s exact-output assertions (below) only ever run `--dry`.
629
+ function step3PreFlightDry(args) {
438
630
  for (const g of GATE_ROSTER) {
439
- // versionArg gates take a MODE-DEPENDENT version (both directions bit
440
- // the first 0.8.8 run): cut modes run pre-flight PRE-BUMP, where the
441
- // README "Current version" claim still equals the PREVIOUS version (the
442
- // bump moves it in Step 4; Step 4g then re-proves at the cut version) —
443
- // while handoff runs POST-MERGE, where the claim already equals the CUT
444
- // version and validating at previous fails by construction. A RESUMED
445
- // half-cut run (gh#765) is post-bump too: the earlier aborted run already
446
- // moved every reference to the cut version, so validating at previous
447
- // false-fails gate 24 exactly like handoff would.
448
- const hygieneVersion = (args.mode === 'handoff' || args.resume) ? args.version : args.previous;
449
- // A RESUMED run's versionArg gates (cut-hygiene) demand artifacts Step 4
450
- // has not recreated yet — docs/ops/releases/vX.Y.Z.md exists only after 4e,
451
- // which runs AFTER this pre-flight. Step 4g re-runs these same gates at
452
- // the cut version once the notes exist, so skipping them here loses no
453
- // coverage — running them here false-fails by construction (the v0.8.32
454
- // cut hit exactly this: 23/24 green, gate 24 dead on the missing notes).
455
- if (g.versionArg && args.resume) {
631
+ const cmd = resolveGateCommand(g, args);
632
+ if (cmd === null) {
456
633
  console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${g.cmd} # ${g.what} — [resume] deferred to Step 4g (notes not regenerated yet)`);
457
634
  continue;
458
635
  }
459
- // targetVersionArg (gh#1135, REQ-06) is a distinct flag from versionArg:
460
- // write-eval-health.mjs NAMES a new file after the cut's target version
461
- // from the very start — there is no pre/post-bump claim to reconcile
462
- // the way gate 24's README-currency check has, so it always takes
463
- // `args.version` verbatim regardless of mode or resume.
464
- const cmd = g.versionArg
465
- ? `${g.cmd} --version ${hygieneVersion}`
466
- : g.targetVersionArg
467
- ? `${g.cmd} --version ${args.version}`
468
- : g.cmd;
469
636
  console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${cmd} # ${g.what}`);
470
- try {
471
- sh(cmd, args, { stdio: 'inherit' });
472
- } catch (e) {
473
- console.error(`\n ✗ FAILED (${g.n}/${GATE_ROSTER.length}): ${cmd}`);
474
- console.error(' Pre-flight aborted. Fix the gate failure before proceeding.');
475
- console.error(' See references/gates-catalog.md for failure-mode → recovery.');
476
- process.exit(1);
477
- }
637
+ sh(cmd, args, { stdio: 'inherit' }); // dry: logs `[dry] cmd`, never executes
638
+ }
639
+ console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
640
+ }
641
+
642
+ async function step3PreFlight(args) {
643
+ if (args.skipGates) {
644
+ console.log('\n=== Step 3 — Pre-flight (SKIPPED via --skip-gates; danger!) ===');
645
+ return;
646
+ }
647
+ console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
648
+ if (args.dry) {
649
+ step3PreFlightDry(args);
650
+ return;
651
+ }
652
+
653
+ const soloGate = GATE_ROSTER.find((g) => g.n === SOLO_GATE_NUMBER);
654
+ const chainGates = EVAL_HEALTH_CHAIN_NUMBERS.map((n) => GATE_ROSTER.find((g) => g.n === n));
655
+ const chainNumbers = new Set(EVAL_HEALTH_CHAIN_NUMBERS);
656
+ const poolGates = GATE_ROSTER.filter((g) => g.n !== SOLO_GATE_NUMBER && !chainNumbers.has(g.n));
657
+
658
+ console.log(`\n-- Phase 1/2: gate ${SOLO_GATE_NUMBER} solo (test:unit:serial) --`);
659
+ const soloResult = await runGateCaptured(soloGate, args);
660
+ printGateResult(soloResult);
661
+ if (!soloResult.ok) {
662
+ console.error('\n Pre-flight aborted at Phase 1 — fix the gate failure before proceeding.');
663
+ console.error(' See references/gates-catalog.md for failure-mode → recovery.');
664
+ process.exit(1);
665
+ }
666
+
667
+ console.log(`\n-- Phase 2/2: ${poolGates.length} gates (concurrency ${PREFLIGHT_CONCURRENCY}) + eval-health chain (gates ${EVAL_HEALTH_CHAIN_NUMBERS.join(' → ')}) --`);
668
+ const [poolResults, chainResults] = await Promise.all([
669
+ runGatePool(poolGates, args, PREFLIGHT_CONCURRENCY),
670
+ runGateChain(chainGates, args),
671
+ ]);
672
+
673
+ const allResults = [soloResult, ...poolResults, ...chainResults].sort((a, b) => a.n - b.n);
674
+ const failed = allResults.filter((r) => !r.ok);
675
+ if (failed.length > 0) {
676
+ console.error(`\n ✗ ${failed.length}/${GATE_ROSTER.length} gate(s) FAILED: ${failed.map((r) => r.n).join(', ')}`);
677
+ console.error(' Pre-flight aborted. Fix the gate failure(s) before proceeding.');
678
+ console.error(' See references/gates-catalog.md for failure-mode → recovery.');
679
+ process.exit(1);
478
680
  }
479
681
  console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
480
682
  }
@@ -1129,7 +1331,7 @@ async function main() {
1129
1331
  process.exit(2);
1130
1332
  }
1131
1333
  step1ReBaseline(args);
1132
- step3PreFlight(args);
1334
+ await step3PreFlight(args);
1133
1335
  step4PromoteAndBump(args);
1134
1336
  step5Commit(args);
1135
1337
  // Invariant 3 (operator ruling 2026-07-12): release commits land via PR,
@@ -143,6 +143,41 @@ pixel-diff/visual-eval gate, not unit tests or review alone. Source:
143
143
  [ADR-0081](../../../../../../docs/ops/adr/adr-0081-chart-2-0-foundations-attribute-grammar.md)
144
144
  Amendment (2026-08-21).
145
145
 
146
+ ## Canonical breakpoint scale (adr-0089)
147
+
148
+ [verified 2026-08-25, gh#1984] `core/responsive.js`'s `BREAKPOINTS` export is
149
+ the single canonical reference for any device-class threshold, whether the
150
+ query mechanism is `@media` (the five sanctioned viewport primitives —
151
+ `grid-ui`, `col-ui`, `row-ui`, `block-ui`, `text-ui` — plus top-layer/popover
152
+ positioning with no ancestor box to query) or `@container` (everything else,
153
+ per `adr-0088` — `spec-breakpoint-convention` REQ-004 forbids importing
154
+ `core/responsive.js` itself outside those cases). This table MUST match
155
+ `core/responsive.js`'s `BREAKPOINTS` export byte-for-byte — a gap here was
156
+ exactly the drift `spec-breakpoint-convention` REQ-007 named and closed:
157
+
158
+ | Name | Min-width |
159
+ | --- | --- |
160
+ | `xs` | `0` |
161
+ | `sm` | `480px` |
162
+ | `md` | `768px` |
163
+ | `lg` | `1024px` |
164
+ | `xl` | `1280px` |
165
+
166
+ A `@container`/`@media` rule can't read a CSS custom property at parse time
167
+ (forbidden-pattern #2 in [token-contract.md](token-contract.md)), so these
168
+ values are cited as raw literals in component CSS, never tokenized — but
169
+ every such literal MUST carry a preceding comment naming the rung it
170
+ implements (device-class threshold) or the ergonomic reason for the number
171
+ (a component-intrinsic threshold, exempt from this table entirely — `nav-ui`'s
172
+ 96px icon-rail floor, `table-toolbar-ui`'s compaction stages, `chart-ui`'s
173
+ 200px legend-hide are correctly exempt and are NOT migration candidates).
174
+ `packages/web-components/core/breakpoint-observer.js` is the shared
175
+ `ResizeObserver`-to-attribute helper for a component that needs
176
+ JS-observable state (relocating a node, switching a positioning strategy)
177
+ rather than a pure CSS layout switch — see its own module doc for the
178
+ boolean-vs-named-value reflection modes. Full requirements:
179
+ [spec-breakpoint-convention](../../../../../../docs/ops/spec/spec-breakpoint-convention.md).
180
+
146
181
  ## Numeric props — `null` over sentinels
147
182
 
148
183
  Indeterminate, unknown, or "not yet set" numeric state uses `null`, not `-1` or `Infinity`: