The third and last capability the ledger recorded as downgraded, replaced by a
static preamble in each agent file. Cursor's subagentStart response accepts
additional_context and its query carries subagent_type, which is the only field
the canonical hook reads, so the port is a field rename.
This is additive, not a replacement. The generated preamble carries an agent's
own declared skills; the hook carries the general skill-awareness text gated by
agent type. Both now ship.
Verified as far as a free Cursor plan allows: the hook registers, fires, and
delivers a payload carrying subagent_type, task, and subagent_model — confirmed
against a live Cursor 3.17.8 delegation, which fires subagentStart three times
before refusing to start the subagent with "Named models unavailable. Free
plans can only use Auto." What a free plan cannot show is whether the returned
context reaches a subagent that never starts. The docs and the submission
matrix say so rather than implying full verification. If delivery does not
work, the preamble still carries the declared skills, so the failure mode is
the status quo rather than a regression.
The context guard needed widening: skill awareness is legitimately
multi-paragraph, where the router and crash hints are single-line. Newline and
tab are now permitted, every other control character is still rejected, and the
bound is separate at 8 KiB.
The second capability recorded as "omitted" without rationale. Cursor supports
it: PreToolUseRequestQuery carries tool_name and tool_input, and
PreToolUseRequestResponse accepts additional_context, so the canonical
PreToolUse(Read) crash routing maps directly onto preToolUse with a Read
matcher. Verified against a live Cursor 3.17.8 session — asked to read a
Sample.ips, the model reported being advised to use the axiom_xcsym_crash MCP
tool, and the read was not gated.
The canonical hints name the bare `xcsym` binary, which a Cursor install cannot
run — the plugin routes xcsym through MCP. Shipping them verbatim would have
told the model to run something that is not there, which is the likeliest
reason this looked unportable. The adapter maps the four invocation forms onto
the MCP tool and fails closed if any backticked `xcsym` survives: no hint is
strictly better than a wrong one.
No `permission` field is emitted, so the plugin never gates a read, consistent
with the documented position that no Axiom hook is a permission boundary. The
hint interpolates the file path, so the adapter bounds its length and rejects
control characters before it reaches the model's turn.
Contract disposition moves from "omitted" to "preToolUse.additional_context".
The capability ledger recorded UserPromptSubmit as "omitted" with no stated
rationale, which dropped Axiom's core routing-enforcement mechanism and left
skill activation in Cursor resting entirely on description-based discovery.
Cursor supports the capability.
Its beforeSubmitPrompt event carries the prompt text and its response accepts
additional_context — the same mechanism user-prompt-submit.py already emits
through hookSpecificOutput.additionalContext. Verified against a live Cursor
3.17.8 session, not inferred: an injected marker token came back at the head of
the model's reply, and with this adapter mode in place the prompt "My SwiftUI
list scrolls badly on device" is routed to axiom-swiftui, which the model then
opens before answering.
The adapter gains a prompt-submit mode that translates the payload, runs the
canonical router as a child, and returns its guidance as additional_context.
The router child now ships with the distribution.
Two details worth noting:
_workspace_root centralises the fact that Cursor omits cwd on several events
but always sends workspace_roots. Children run their own project gate against
the directory they start in, so the router child is now started in the
workspace rather than the plugin directory. session_start uses the same helper.
Child output is not forwarded blindly. The router emits a fixed template naming
skills from its own table and never prompt text, so the adapter requires that
shape, bounds the length, and rejects control characters before the string
reaches the model's turn.
Also stops the hooks writing __pycache__ into the plugin directory Cursor
manages, via sys.dont_write_bytecode and -B on the child spawn.
transformCommand derives the emitted path from the filename (`axiom-<stem>.md`)
but the emitted `name:` from frontmatter (`axiom-<name>`), so a command whose
frontmatter name differs from its filename would ship with the two disagreeing.
Check 12r would then fail with its generic blanket message, naming neither the
command nor the cause.
Skills and agents already assert name-matches-filename in the same loader;
commands did not. They do now, with an error that names the file and the
offending value. No current command diverges.
Verified against payloads captured from a live Cursor 3.17.8 agent turn. Every
existing adapter test built its own payload, so they all agreed with the
adapter's assumptions rather than with Cursor.
postToolUse(Write) carries no cwd. _validated_post_write_context required it,
raised "unsafe write cwd", and failed open, so the Swift guardrails never ran
in Cursor at all. cwd now falls back to workspace_roots[0], which every
payload does carry; all existing path validation still applies to it.
duration arrives as a float (11554.364 ms). The int-only check dropped it, so
duration_ms never reached the hints child and no duration hint could fire.
Both int and float are accepted and coerced.
Three assumptions were checked and are correct, and are now pinned so they
cannot rot: tool_output is a JSON-encoded string, the Shell variant is keyed
exitCode/output, and workspace_roots is always present.
real-payload.test.ts runs against the generated distribution rather than the
source tree, because the guardrails child is only co-located with the adapter
in the shipped output. The fixture records the captured shape with identity
fields redacted.
Also documents two support boundaries found while verifying: Cursor Free
refuses subagent delegation, so the plugin's agents register but cannot run
there, and plugin hooks load behind Cursor's enable_cc_plugin_import gate,
which clears them silently when off. Both reach the install guide and the
submission runbook, whose smoke matrix could not otherwise be completed on a
free account.
transformAgent computed releasedBackground before applying the
CURSOR_FORCED_FOREGROUND exclusion for the emitted frontmatter, but after it
for the capability report. The two disagreed. It is masked today because the
only forced-foreground agent is writable and so is never background-eligible,
but the moment that set includes a read-only agent the shipped frontmatter
would claim is_background: true while the report and pre-deploy assert
foreground. The exclusion is now applied once, before rendering.
rewriteCursorHostClaims strips Claude-host capability claims ("xclog is on
PATH as a bare command, just run it") from canonical prose. Each pattern is
pinned to current canonical wording, so rewording upstream silently stops the
rewrite and ships Cursor users instructions to run a binary the plugin does
not install. An output-side token gate cannot catch this: once the wording
changes the old string is gone and the new one passes through unmatched. The
rewrites are now a table with per-pattern hit tracking, and a render asserts
every pattern matched something. A dead pattern fails the build. All seven
fire on the current corpus.
post_write validated the payload, including reading up to 1 MiB, before
checking the .swift extension, so a large non-Swift write paid that cost and
could report "write file too large". The extension is checked first; a missing
or non-string path still falls through and keeps its diagnostic.
shipped-runtime.test.ts covers a gap: cursorRuntime applies blind string
substitutions over Python source, and every hook test staged the pre-transform
bytes, so the files users actually run were never executed. It compiles all
four shipped runtime scripts and checks the renamed environment variable
agrees between the adapter that sets it and the hints script that reads it.
The reviewed contract froze the Axiom inventory in three places: exact
router/agent/command name lists and counts in contract.ts, the agent class
distribution in report.ts, and both again in pre-deploy check 12r. Because
build:cursor is wired into `npm run build`, 12r into `npm test`, and
build-cursor into set-version.js, adding any skill, agent, or command broke
the build, test, and release paths until all three fixtures were hand-edited.
Adding one well-formed agent reproduced it: "Cursor agent inventory differs
from reviewed contract".
This is the drift trap build-codex.ts:412 already documents fixing, and that
set-version.js avoids by counting from directory scans.
Inventories and class counts are now derived. What stays hand-reviewed is
policy: the forced-foreground set, read-only and allowed tool sets, allowed
agent fields, hook dispositions, and hook event/entry counts. Check 12r
derives expected names from the canonical tree, and its per-agent hook warning
and advisory maps now come from CURSOR_AGENT_ADVISORIES instead of a third copy.
assertCursorCapabilityInventory validates shape rather than membership, so it
still fails closed on empty, duplicate, and non-slug names.
report.ts gains a real invariant in place of the 30/1/11 literals: a writable
background agent is downgraded to foreground by the authority rule whether or
not anyone decided so, and must therefore appear in the reviewed
forced-foreground set. Stale entries fail too, and the message names both
sides and the remedy.
source-fidelity.test.ts keeps the removal honest. Deriving inventories means
generation, check:cursor, and every other Cursor test read the same loader and
agree with each other, so a loader defect that silently drops an agent would be
invisible to CI — 12r catches it, but the Cursor workflow does not run
pre-deploy. It compares loadCursorSource against an independent filesystem and
manifest scan.
Generated output is byte-identical: this changes validation, not what ships.
production-triage.md used real APP-3V / APP-7B issue IDs in its sample Sentry
payloads and named a specific project in guidance ("the ExampleApp lesson was..."),
and localization-research-ref.md shipped a branded glossary. Axiom is not written
for any one app, and its examples should not carry another project's namespace.
Sample IDs are now ACME-*, consistent with the organizations/acme URL already in
the payload. The triage lesson keeps its substance without the attribution.
No behavioural change: the xcsym noise-rule contract parity check (12n) still
passes, so the noise classes and standing notes are untouched.
layout-ref.md stated that lazy stacks "recycle them when off-screen" with no
version qualifier. That is true on iOS 27 and false on iOS 26 — inside the
supported floor — so a developer choosing LazyVStack for an unbounded feed on
26 believed memory was bounded when it was not.
Measured, one binary, same device, runtime the only variable (800 rows x 100 KB):
- 26.5 LazyVStack: 800 of 800 alive at the bottom, +95 MB; List is no better at
+88 MB; onDisappear fires for 786 rows while zero are freed; 8s idle frees none
- 27.0 LazyVStack: 50 of 800 alive, +1.5 MB
- Not SDK-gated: a binary linked against the 26.5 SDK evicts identically on 27,
so shipped apps change behaviour on OS update with no way to opt out
- 27 rebuilds rows on scroll-back, so per-row @State silently resets
One rule covers both cycles, both halves load-bearing: lift per-row state above
the row, and bound whatever holds the weight. Bounded-NSCache fix verified at
+6.5 MB against the same scroll.
memory-debugging.md gains "Measure Peak, Not Resting" — task_vm_info,
ledger_phys_footprint_peak, os_proc_available_memory headroom, which number each
termination is judged against, that the peak never resets (take a delta), and
that a zero headroom reading is ambiguous per <os/proc.h>. Its decision tree no
longer routes "memory grows while scrolling" to "Memory leak (fix retention)".
swiftui-performance-analyzer gains rule 11 (heavy payload in per-row state under
a lazy container) and a Phase 3 question on state that must survive a scroll-away.
docs/reference/swiftui-layout-ref.md also gains the two required Reference-page
sections it was missing (When to Use, Example Prompts) plus Documentation Scope.
Published Swift extracted from the markdown and compiled with
swiftc -emit-sil -swift-version 6 at ios26.0 and ios27.0.
Ships List section index and system-spacing coverage. Axiom had zero coverage of
the section index — sectionIndexLabel appeared nowhere in the suite — and no
answer to "what spacing value should I use", while its own layout references
taught 30 hardcoded literals as canonical.
Adds sectionIndexLabel, listSectionIndexVisibility and listSectionMargins with
compile-proved availability (including the macOS/tvOS asymmetry), the HIG rule
against pairing an index with trailing row controls, and the system-spacing rule
grounded in the SDK's own optional-length signatures.
Also removes sub-floor content from the SwiftUI layout references — an
iOS 13-16 safe-area fallback and 23 dead availability markers — and ships the
in-flight sfsymbols CLI documentation.
Counts unchanged: 273 skills, 42 agents, 17 commands.
Axiom had zero coverage of the List section index — a suite-wide grep for
sectionIndexLabel returned nothing — and no guidance on what spacing or margin
value to use, while axiom-swiftui's layout references taught 30 hardcoded
literals as canonical.
axiom-swiftui (skills/26-ref.md)
Section Index and Section Margins. Research surfaced a third API that neither
the report nor the issue knew about: listSectionIndexVisibility(_:), named by
Apple's own doc comment on sectionIndexLabel. Two behavioral facts come from
the SDK doc comments and appear nowhere in the web docs — the index is visible
by default once any section carries a label, and an empty labeled section with
a hidden header is how you show a letter that has no content. Also the i18n
trap: a naive first.uppercased() collapses Japanese, Korean, Chinese, Greek
and Cyrillic into one bucket, where UILocalizedIndexedCollation is correct.
axiom-design (skills/hig.md)
The system-spacing rule. Apple's HIG publishes no iOS spacing scale, and the
SDK encodes that: padding and safeAreaPadding default the length to nil,
meaning system-determined. contentMargins, listRowInsets and listSectionMargins
type it CGFloat? but give it no default, so the argument is mandatory while a
number is not — .contentMargins(.horizontal) does not compile.
Plus the section-index design rule: never pair an index with trailing row
controls, and navigationLinkIndicatorVisibility(.hidden) as the way to keep
push navigation regardless.
Version floor
layout-ref.md and containers-ref.md are now free of sub-floor content. An
iOS 13-16 manual safe-area fallback section is deleted outright, 23 dead
availability markers removed, and the hardcoded spacing literals reduced to
only the deliberate ones — spacing: 0, a photo grid's tight gutters, and
containerRelativeFrame's required argument.
Prompt router
Routes "index strip" and "alphabet scrubber" plus the three API tokens, with
the English phrases behind the non_ios gate because Android's SectionIndexer
is literally this feature. Adds a meta gate so prompts about Axiom's own
internals stop routing to Axiom skills. Measured against the published hook
across 372 prompts: 0 regressions, 5 new routes, false positives unchanged
at zero.
Also ships the in-flight sfsymbols CLI documentation in axiom-design
(skills/sf-symbols.md, sf-symbols-ref.md): binary discovery, per-symbol and
per-rendering-mode availability via --json, deployment-target filtering, export,
and the trap that a zero-result search still exits 0.
Every API claim verified against iPhoneOS27.0.sdk; every published example
compile-tested with swiftc at ios26.0.
Ships status-bar-for-screenshots coverage in the axiom-tools device-control
reference: the devicectl `statusBar` family and its `screenshot` preset, the
override store it shares with `simctl status_bar`, and the silent exit-0
failures that make a stale status bar reach a shipped screenshot.
Counts unchanged: 273 skills, 42 agents, 17 commands.
Axiom had no coverage of preparing a status bar for App Store screenshots. The
tool map also listed `status_bar` as a simctl sim-only feature, which stopped
being the whole story: devicectl owns a `device simulate statusBar` family whose
`preset screenshot` sets Apple's classic 9:41 / full-bars / 100% look in one
command, and both tools write the same override store.
Every claim was verified against a booted iPhone 17 simulator on Xcode 27.0 /
CoreDevice 642.9.1 — readbacks, exit codes, and a visual capture — rather than
from documentation, which is what surfaced the traps below.
- `override` merges into existing state, and several values silently fail while
still exiting 0: `--operatorName` will not overwrite an existing carrier name,
and `--dataNetwork wifi` stores `5G` if any `--cellularMode` is in the same
call. Guidance is now clear-first, then assert on the readback — never on the
exit code. The silent-failure table carries a "changes the capture?" column so
a reader can tell which rows actually alter pixels.
- The `screenshot` preset writes the literal carrier string `Carrier` — the same
placeholder screenshot-validator flags as a defect — and nothing can overwrite
it afterward. Both sides now cross-reference: the skill says to skip the preset
on carrier-rendering devices, and the agent explains where a flagged `Carrier`
came from and how to re-capture.
- The two tools' flag vocabularies are not interchangeable (`draining` vs
`discharging`, `5G` vs `5g`); cross-spellings hard-fail. Documented side by side
with the observed exit codes.
- devicectl's `--cellular-strength` help advertises 1-5; a simulator rejects 5.
- `dataNetworkType` in `--json-output` comes back backtick-wrapped for values that
are not valid Swift identifiers (`5G`, `4G`, `1x`, `5GUC`). Strip before parsing.
- devicectl's subcommand set follows a machine-wide CoreDevice, not xcode-select,
so a version cannot be inferred from the selected Xcode. Documented as a
capability probe the reader runs instead of a version claim.
Hotfix release. xcui assert/wait/dialog/voiceover failed to parse any
accessibility tree containing a non-string AXValue (slider, page control,
toggle) — a defect present in every prior release, reported against beta.44.
Reported from ExampleApp: `xcui assert` died with "cannot unmarshal number into Go
struct field AXElement.AXValue of type string".
AXValue is not consistently a string — a slider reports 0.5, a page control an
integer, a toggle a bool. Typed as *string, encoding/json failed the WHOLE
document, so a single numeric value anywhere on screen took down every command
that reads the accessibility tree: assert, wait, dialog, and voiceover together.
Measured on a stock Settings screen: 258 null, 10 string, 5 number — not an
exotic control.
Pre-existing, not a regression: axtree.go was last touched in aefd0240 and every
release since has shipped it. What is new is that it was found.
New AXText type accepts string, number, bool, or null and keeps the literal text
(0.5, 3, true) — which is what a user comparing against `--value 0.5` would type.
Applied to every text-ish field, not just the reported one: the cost of guessing
wrong about any of them is total parse failure, and there is no upside to
strictness when xcui compares them as text either way.
Also fixed a nil-ordering bug introduced while converting findByID — an empty
--id would have matched every element lacking an identifier.
9 regression tests: all six JSON shapes for AXValue, every text field carrying a
number, and numeric-identifier lookup.
Derived output only — regenerated from the source changes in this series via
`npm run build:codex`, `pnpm run build:bundle`, and `make install` in tools/xcui.
The Codex tree is the largest delta: it now carries three previously excluded
suites, translated agent/command invokes, policy blocks in every agent-skill's
openai.yaml, and no longer carries the frontmatter key Codex's own validator
rejects. Verified against that validator directly — "Plugin validation passed",
exit 0, zero errors, which is the first time this build has passed it.
install.md gains a "Controlling When Axiom Activates" section. Axiom shipped zero
guidance on this, so users were reaching for Claude Code's per-skill
skillOverrides — which its documentation states plainly does not apply:
"Plugin skills are not affected by skillOverrides. Manage those through /plugin
instead." The supported levers are the whole-plugin toggle and the fact that
every /axiom:* command is user-invoked only.
codex-install.md's differences table advertised the abandoned blanket-router
policy as a feature — "175 specialized (Codex has native routing)" — and claimed
"Agents: Not supported in Codex plugins", which was never true: all 42 ship as
skills. Corrected to real counts, with the 31 withheld files accounted for
(30 inlined auditor procedures Codex receives as first-class skills instead, plus
getting-started).
Four new checks, each closing a class where both halves looked locally correct
and only the built artifact showed the break.
- 11z runs Codex's OWN validate_plugin.py against axiom-codex, from the copy
Codex extracts to ~/.codex/skills/.system/. 12p asserts one rule transcribed by
hand, which catches the rule we know about; running the validator catches the
next one. Skips cleanly when Codex or PyYAML is absent.
- 12o resolves every pointer in the EMITTED Codex tree — sub-skill paths, bare
agent names, and /axiom: commands. Source cross-references were valid and the
exclusion list was valid; only the emitted tree showed 41 dangling pointers.
Check the artifact, not the inputs.
- 12p asserts Codex invocation policy in BOTH directions: agent-skills carry
policy.allow_implicit_invocation: false with a $-named default_prompt, routers
stay discoverable, no emitted skill carries the frontmatter key Codex's
validator rejects, and no description was truncated at a file-extension period.
- 12q asserts all 17 commands and the ask.md template are user-invoked only.
Also corrected 12n's comment, which justified inlining crash-triage safety text
by citing the axiom-shipping Codex exclusion that no longer exists. Axiom-dtq
treated that symptom; the root cause was the inherited exclusion. The inlining
stays — safety text is worth carrying at the point of use.
Three suites (axiom-tools, axiom-shipping, axiom-apple-docs) were excluded from
the Codex build. That was not a decision — the original list (2b5d6adb) was a
blanket "Router skills — Codex has native progressive disclosure, so these are
unnecessary". The premise was abandoned and the list curated, but these three
survived: two still annotated "Codex fit not yet assessed", and axiom-tools
carried a reason written when it was named axiom-using-axiom and held ONLY
discipline text. It then absorbed six sub-skills, five of them harness-neutral
tool references, all inheriting an exclusion that predated them.
Measured cost: 41 dangling "see X" pointers plus ~94 name citations across ~40
emitted Codex skills, aimed at files the build never wrote.
- CODEX_EXCLUDED_SUITES is now empty; per-FILE exclusion replaces it, holding one
entry — getting-started.md, which drives AskUserQuestion and cites 15 /axiom:
commands, and has zero inbound references.
- dropExcludedSubSkillRows strips a router's table row for a withheld sub-skill,
so per-file exclusion cannot recreate the defect it exists to avoid.
- rewriteInlineAuditorBlockForCodex translates auditor file paths to Codex skill
names; that block previously told Codex users to read files this build skips.
- rewriteAgentInvokesForCodex fixes the layer under that: routers said "Launch
`build-fixer` agent or `/axiom:fix-build`" while `axiom-fix-build` — the thing
that exists there — appeared zero times. Neither agents nor /axiom: commands
exist on Codex, and the mapping is not derivable (spm-conflict-resolver →
axiom-resolve-spm). Applied to routers, agent-skills, and sub-skills, since a
verbatim copy reintroduces it. Two commands have no Codex equivalent at all
(console, compare-traces) and are labelled Claude Code-only rather than
redirected somewhere that does a different job.
- agentToSkillName moved into the shared module so the pre-deploy gate derives
the agent-skill set instead of guessing from names — a name heuristic
misclassifies both ways (axiom-health and axiom-testing are routers that read
verb-first; axiom-swift-simplifier is an agent-skill that does not).
- The count test now derives the agent total from the agents directory; the
hardcoded 41/68 had drifted from a real 42/69 and passed anyway by being
self-consistent.
15 of 17 commands carried `disable-model-invocation: true`; ask.md and status.md
did not, so Claude could invoke both unprompted.
/axiom:ask is the worst case. It is 7.5 KB listing all 26 routers, and it exists
as the MANUAL escape hatch for when auto-routing misses — so making it
auto-invocable loaded the entire routing table to redo routing the routers had
already done, and put it in competition with the routers it backstops.
/axiom:status ran shell checks nobody asked for.
ask.md is generated from scripts/templates/ask.md.template by set-version.js, so
the template is fixed too: patching only the output would have regressed on the
next version bump, which is plausibly how this was lost in the first place.
Added pre-deploy gate 12q, which asserts every commands/*.md AND the template
carry the key.
Repoints guidance from bare `axe tap` to `xcui tap` so the SimulatorKit handling
comes along automatically, and routes breakpoint testing at the new
`xcui resize sweep` instead of a hand-rolled devicectl loop.
- The axiom-tools router's resize row now points at the sweep; the raw
appResize section is kept for driving it by hand.
- resize-auditor's final validation step is one command rather than
start/set/kill plus a separate assert — which is what made that step
agent-executable.
- The docs agent page still described the old two-tool flow; found by preflight's
companion-docs drift check and fixed in the same pass.
Guarded against a regression this repoint introduced: xcui is on PATH
automatically ONLY on Claude Code. Codex, Pi, and MCP installs ship skills with
no binaries, so guidance telling those users to run `xcui tap` pointed at
something they do not have, where the old `axe tap` worked. xcui-ref even
asserted "xcui is on PATH as a bare command". The invocation section is now
harness-aware, and the guidance says `command -v xcui` first with the bare-`axe`
fallback spelled out.
`killall -9 Simulator` was prescribed as the stuck-simulator nuclear option in
six shipped places. Xcode 27 ships Contents/Applications/DeviceHub.app and no
Simulator.app at any path, so on a 27-only Mac the command kills nothing while
an agent reports the fix as done. Verified: `find` for Simulator.app under
Xcode-beta.app returns nothing; the live processes are DeviceHub (27) and
Simulator (26.6).
Now `killall -9 Simulator DeviceHub`, verified against the PROCESS rather than
`$?`. This second half came out of behavioural testing: killall exits 0 when
EITHER name matched, so on a machine carrying both Xcodes a 0 can mean "killed
Simulator, DeviceHub still running". Every site now confirms with
`pgrep -l Simulator DeviceHub`.
Also corrected, all verified against the installed toolchains:
- Four GUI paths now branch on Xcode version instead of naming only the pre-27
surface (Devices and Simulators, the Simulator I/O menu). Previously only
watch-device-diag did this.
- device-control-ref gains the bundle path / process name / bundle id table per
Xcode version — the facts the killall fix depends on.
- CarPlay: the Simulator menu path AND `defaults write com.apple.iphonesimulator
CarPlayExtraOptions` are Xcode 26-only. The key appears nowhere in the 27
toolchain, and on 27 that `defaults write` FAILS SILENTLY — it validates no key
names, so it exits 0 writing into the domain of an app the toolchain no longer
ships. That silence is why the recipe keeps propagating from stale notes.
- CarPlay Simulator requires a physical iPhone over USB and cannot drive a booted
simulator (otool: links MobileDevice, not CoreSimulator; its own strings say
so). Load-bearing now that the Simulator path is dead on 27 — it is the only
CarPlay route, so a reader could otherwise plan a device-free session that
cannot work.
- axe-ref dropped "AXe 1.7.1 has no fix yet" and the universal "bare axe fails";
both were false at AXe 1.8.0. Guidance is now conditional on what `xcui doctor`
observed.
Two capabilities that had no scriptable path before.
VoiceOver toggle — closes a documented v1 gap. `a11y set --toggle voiceover`
maps to `devicectl device settings voiceover`, the mechanism that did not exist
when the toggle was omitted. It is the only toggle that leaves the
simctl/defaults world, since simctl has no VoiceOver setter. `a11y reset` now
disables it too: every other toggle tears down through simctl, so a VoiceOver
left running would have been the worst leak of the set — a screen reader that
survives the run changes what every later run sees.
differentiate-without-color and bold-text stay unsupported; `devicectl device
settings` covers only appearance, audio, biometrics, reset, and voiceover.
resize sweep — drives breakpoints and checks each in one JSON envelope, wrapping
the three tedious parts of `devicectl device appResize`:
- Session lifecycle. `appResize start` holds the session only while it runs, so
it is backgrounded and reaped on every exit path, including SIGINT/SIGTERM. A
bare defer covered only normal returns; a harness signalling just this PID
would have stranded the app on the Resizable display.
- Actual vs requested size, read from the field devicectl labels "Actual" rather
than inferred from info appResize's preferredSize, which names the REQUEST. A
device that clamps (an iPhone 17 turned 1100x500 into 1100x550) is exactly the
case a sweep exists to catch, and reading the proxy would report a clean pass
for a width never exercised. Readiness waits for BOTH the info query and the
geometry flush — two unordered processes, and keying off one alone reported
honored:false for sizes the device honored.
- Post-resize flakiness: the automation session can time out once while the
display transitions, so each assertion retries before failing.
--screenshot-dir writes a PNG per breakpoint from the Resizable display. An
id-presence assertion catches "the element vanished", but resizing breaks layouts
by overlap and truncation, which only a picture shows.
--strict fails a clamped step. Without it a fully clamped sweep still exited 0
when assertions passed, so an exit-code-only consumer read "all breakpoints
validated" for breakpoints the device never produced.
Three CoreDeviceErrors are separated because each needs a different fix: 1001
(device has no Resizable App Management), 24001 (no foreground app to move),
24004 (no session — not terminal; the startup poll retries through it).
The output buffer is mutex-guarded: os/exec writes the child's output from its
own goroutine while the parser reads it, which -race catches.
Adds twelve passthrough verbs (tap, slider, type, swipe, drag, touch, gesture,
button, key, key-sequence, key-combo, screenshot) that forward argv to AXe
verbatim — same flags, same output, same exit code. xcui parses none of AXe's
flags, so they stay flag-compatible as AXe evolves.
The point is that input goes through runAxe, which owns the SimulatorKit
fallback. Guidance that says "run `xcui tap`" cannot drift out of that handling
the way "run `axe tap`, and also remember a DEVELOPER_DIR prefix when doctor
tells you to" can.
- --udid is injected when omitted; hasFlag handles both --udid X and --udid=X and
does not false-match a longer flag sharing the prefix.
- AXe's exit code is propagated via errors.As on an ExitCode() interface rather
than *exec.ExitError concretely — broader, and constructible in a test without
spawning a process. A passthrough that swallows the failure code is not
transparent.
- Only a LEADING --help is a help request. Scanning all of argv hijacked a
literal one (`xcui type -- --help`) and swallowed the error, so `xcui tap
--help` exited 0 printing nothing when AXe was absent.
- Dispatched after xcui's own verbs so they always win a name collision with a
future AXe subcommand; a test asserts the two sets stay disjoint.
- describe-ui / list-simulators / init are not forwarded (xcui owns those jobs);
stream-video / record-video stay bare because they outlive any request timeout.
- doctor now reports AXe verbs xcui does not forward, so the static allowlist
cannot silently lag the tool it fronts. Anchored to the exact two-space indent
of the SUBCOMMANDS block: a looser pattern was terminal-width dependent and
matched a wrapped description word while missing a real verb.
The DEVELOPER_DIR override was predictive: stat the selected Xcode for
SimulatorKit.framework at the legacy path and pre-emptively pin an older Xcode
for every AXe call. Correct when written (2026-07-06), wrong by now — AXe 1.8.0
finds the framework at its Xcode 27 home unaided, so the override fired when
nothing needed fixing, silently downgraded the toolchain AXe ran under, and made
`xcui doctor` assert that bare `axe` fails when it demonstrably does not. It had
no path that ever un-set itself.
Now reactive: run AXe bare, and only on an observed SimulatorKit load failure
find a fallback DEVELOPER_DIR and retry. It retires itself when upstream fixes
things, with no version table to maintain.
- Record the override only AFTER the retry is observed to work.
axeFallbackDeveloperDir only stats a path, so a candidate can carry the legacy
directory and still fail to run AXe. Recording first pinned a known-bad
toolchain for the whole process and made doctor report "no fallback was found"
when one was found and tried.
- Memoize only conclusive outcomes, so an unrelated failure (no booted sim, bad
args) leaves the question open rather than pinning a verdict off noise.
- currentDeveloperDir() honours $DEVELOPER_DIR before xcode-select, matching
Apple's precedence and giving callers an escape hatch they did not have.
- doctor's note now reports what xcui observed rather than asserting third-party
behaviour, and only appears when the override actually engaged.
- isSimulatorKitLoadError tracks AXe 1.8.0's rewording ("could not load simulator
support" replaced "Failed to load essential private frameworks"); only the
shared "does not exist" tail kept the detector alive across that change.
- Added an axeExec seam: on a healthy machine the reactive path is unreachable
end-to-end, and an untested retry is how the last workaround rotted unnoticed.
A stale zero-byte gate lock sat untracked in the repo root and blocked
`set-version.js --tag`, which refuses on any unrelated working-tree change.
Beads artifacts are never committed (`.beads/` and `.beads-credential-key`
are already ignored); this one was missed.
- axiom-xcode-mcp rewritten for the Xcode 27 headless MCP server (mcp-server),
reference regenerated from a live tools/list capture (20 -> 54 tools)
- devicectl appResize documented; the "resize is GUI-only" claim corrected in
five files, making resize-auditor's final validation step scriptable
- Manifest routing description updated: it still advertised the nonexistent
RunTests tool and had no headless trigger, so Layer-1 routing could not
surface the rewritten content
Axiom asserted "GUI only — no public window-resize API in XCUITest, xcui,
simctl, or devicectl". `devicectl device appResize` is exactly that API.
Verified end to end on an iOS 27 simulator against Xcode 27 beta 5: a purpose-
built SwiftUI probe was driven 500x800 -> 900x600 -> 1100x500 from the CLI with
the accessibility tree asserted at each step, confirming children re-center.
- Document start/set/observe + `info appResize`, --preferred-size, --corner-radius
- Note actual size can differ from requested (1100x500 yields 1100x550) — read
it back rather than trusting the request
- Correct the four files carrying the GUI-only claim; resize-auditor's final
validation step is now scriptable instead of a human handoff
- Add `devicectl device settings voiceover` to the verified matrix — one of the
three toggles xcui v1 omitted for lack of a mechanism (Axiom-h8m)
- Reframe device-control-ref from uptime to capability-and-privilege: the CLI
tools run unprivileged, which is what matters in CI
The suite's premise — "mcpbridge requires a running Xcode with a project
open" — is false on Xcode 27. `xcrun mcp-server` runs the tool service with
Xcode.app closed. Verified against 27A5237l by driving raw JSON-RPC over
mcpbridge with no Xcode process running.
- Split the running-Xcode requirement into an OS26 paragraph and an OS27 one;
clients still register `xcrun mcpbridge` (transport), mcp-server is the service
- Add the headless lifecycle (enable/start/open/status/stop/disable) and the
sudo approval model (approve/allow-folder/deny/clear-permissions)
- Document the top headless hazard: an unapproved agent blocks forever on an
XcodeService dialog while initialize succeeds and status reports running:true
- Regenerate xcode-mcp-ref from a live tools/list capture: 20 -> 54 tools across
12 categories, params from inputSchema, returns from outputSchema, 15 notes
- Replace tabIdentifier (absent from all 54) with workspaceIdentifier, which is
required in practice despite appearing in no tool's `required` list
- Remove XcodeListWindows, XcodeListNavigatorIssues, XcodeGetCurrentFile, RunTests
- Add the workspace-management family the headless bootstrap depends on
- Keep DocumentationSearch: it is workspace-gated, not dead (53 tools without a
workspace open, 54 with; the server advertises listChanged:true)
- Retire the XcodeMCPWrapper proxy advice — structuredContent is fixed on 27
- New Pattern 1 subsection: Xcode 27 unifies Group/Section/ForEach under
ContentBuilder (a ViewBuilder typealias) with TupleContent conditional
conformances; Scene/Tab/Table keep dedicated builders
- Two-tier availability, compile-measured: mixed Section/Group/ForEach nest
7.2s (Xcode 26) -> 1.7s at any deployment target on Xcode 27; homogeneous
11-level Group collapses fully only at iOS 27 targets (1.8s -> 0.15s)
- TN3211 source-incompatibility shapes and fixes folded; WWDC 2026-269 and
/technotes/tn3211 added to Resources
- Companion docs page gains matching When to Use bullet, example prompts,
and an Xcode 27 Features section
- axiom-uikit hook now routes resize/resizable/foldable prompts, so
resize-auditor reaches the suite that owns it (2 of 5 canonical prompts
routed before; 5 of 5 now)
- skill-annotations.json regenerated after 2.5 months of drift: 106 previously
unannotated skills gain category faceting and the 2x tags search boost
- generate-annotations wired into build:bundle so coverage cannot drift again
- MCP SDK >=1.30 transport errors are logged instead of exiting silently
Counts unchanged: 27 suites, 273 skills, 42 agents, 17 commands.
skill-annotations.json supplies category and tags to the search bundle, where
tags carry a 2x BM25 boost. It had not been regenerated since 2026-05-28, so
106 of 300 skills had no entry — the Foundation Models adapters/evaluations
families, the CoreML trio, watchos-a11y, and every sub-skill added since.
Affected skills still loaded and were findable by body text; they ranked lower
and could not be filtered by category.
Three properties compounded: the generator had no npm script, CI step, or hook;
it is additive-only, so it never reported what was missing or failed a build;
and its last run was bundled incidentally into an unrelated feature commit.
- generate-annotations now runs inside build:bundle, so coverage cannot drift
- it writes only when content changes, so a standalone run does not report a
stale bundle over identical bytes (staleness.ts only forgives git-clean skew)
- adds a non-destructive `annotations` script for regenerating without a bundle
Fixing that exposed a second defect. The bundler excludes generated
inline-auditor sub-skills — MCP ships each auditor as an agent, so bundling the
inlined copy double-counts — but the generator had no such exclusion and
produced 30 entries for files the bundle drops. The marker was a bare literal
duplicated across consumers behind "keep in sync" comments. It is now one
exported predicate, isGeneratedSubSkill, shared by bundler and generator, named
to match the existing root-side definition.
Result: 299 of 300 bundled skills annotated, 0 orphans, generator idempotent.
Also handles MCP SDK >=1.30 transport errors, which route to onerror and then
close; unhandled, the server exited silently with nothing in the log.
Known gaps, filed not fixed: dev-loader.ts still lacks the exclusion, so dev
mode serves ~30 skills production drops (Axiom-71h); the marker is still two
copies rather than one, across a package boundary (Axiom-ris).
The axiom-uikit branch of user-prompt-submit.py contained no `resiz` token,
so resize-auditor's own canonical example prompts did not reach the suite that
owns it — 2 of 5 routed. "Audit my app for screen resizing support" matched
nothing; "Is my app ready for resizable windows on iOS 27?" went to
axiom-apple-docs.
The token was already present in the swiftui, testing, and games branches; only
the owning suite was missed. Adds bidirectional resize proximity matching plus
foldable and folding-device phrasings, so word order does not matter. Uses
`apps?\b` rather than bare `app` so "Apple" and "happy" do not trigger it.
All 5 canonical prompts now route. New test_uikit_resize covers them, with
negatives for "resize an image" and "create a folder" guarding over-match.
Xcode 27 beta 5 (27A5237l) sweep plus a camera-reliability repair.
- HealthKit: beta 5 ships the earliestAuthorizedSampleDate doc comment;
the dictionary is sparse and a floor may only clamp forward
- CoreLocation: LiveConfiguration.maritime (OS27)
- CryptoKit: Insecure.UnauthenticatedAES / UnauthenticatedChaCha20 (OS27)
- WebKit: allowsJSHandleCreationInPageWorld; 9999 sentinels resolved to 27.0
- StoreKit: three standing re-checks verified unchanged, stamps refreshed
- Xcode MCP: ExecuteSnippet renamed to RunCodeSnippet; 53-tool surface scoped
- Camera: error-channel + controller-dealloc diagnostics, iOS 18 notification
migration across four files, .systemStress corrected to .batteryStress
Swift 6.4 probe was a no-op this beta: mapKeyedValues, stdlib FilePath, and
BorrowingSequence all still absent.
Counts unchanged: 273 skills, 42 agents, 17 commands.
Three defects and two new failure modes, all verified against the iOS 27 SDK.
An invented symbol was shipping. Both camera skills named .systemStress as an
OS27 system-pressure factor. The SDK defines exactly six factors and that is
not one of them — the real symbol is .batteryStress. The surrounding prose was
accurate; only the identifier was wrong. It survived every gate because it
lived in a markdown table row rather than a swift fence, and the
compile-the-published-text discipline only extracts fenced blocks.
The auditor would have failed correct code. camera-auditor searched only the
legacy notification constant names, which are not substrings of the current
ones — so an app correctly observing AVCaptureSession.runtimeErrorNotification
was reported as missing a runtime-error observer. It now searches both.
Ten uses of the iOS 18-deprecated .AVCaptureSessionWasInterrupted family
across four files migrated to the current AVCaptureSession.<name>Notification
forms, each mapping read from NS_SWIFT_NAME in the SDK header.
New in camera-capture-diag:
- Step 5, Turn On the Error Channel. A clean console is not evidence of
health: AVCaptureSession reports runtime failures through NotificationCenter,
not by throwing, and a runtime error leaves the session stopped without
restarting itself.
- Pattern 18, Controller Deallocated. A controller that falls out of scope
tears down the session and the preview goes black with no error, no
exception, no log.
- DiscoverySession named as an explicit non-fix for a nil device: it queries
the same enumeration, so nil merely becomes [].