First suite of the corpus audit. All 160 Swift blocks were compiled as probes at
the iOS 18 floor and every API claim checked against the 27.2 SDK interfaces and
swiftinterfaces. 35 findings across the 8 files: 21 defects (wrong as written),
4 stale (teaching a fix for a problem the SDK has since solved), 10 sub-floor.
Every file had at least one.
The worst was silent. assume-isolated.md recommends testing MainActor code with a
nonisolated `@Test` calling `MainActor.assumeIsolated`; that compiles clean and
traps at runtime, because Swift Testing does not run nonisolated synchronous tests
on the main thread. It takes down the whole test process with SIGTRAP, and no
compiler can warn about it — which is the argument for a harness that runs what it
checks rather than only compiling it.
Elsewhere of that class: Pattern 9's copy-paste SwiftData template returns
non-Sendable models across an actor boundary and cannot compile for any conforming
type; four ✅ blocks fail under `-swift-version 6` for the same missing `@MainActor`;
a gotcha table teaches that `case failed(Error)` is a compile error when `Error` has
been Sendable since Swift 5.5; and a documented debug env var is absent from every
runtime Apple ships.
Two failure modes worth naming, because neither is reachable by compiling code
blocks alone. Content that teaches a FIX for a problem the SDK already solved —
Core Data's `perform` already takes a sendable block, so the ❌ does not fail and
the "fix" is a no-op. And content that teaches a DIAGNOSTIC that no longer exists.
Gotcha tables and decision trees need the same scrutiny as the code.
One claim was wrong in the same direction in three files at once: that an async
function resumes on the caller's actor. True only under
NonisolatedNonsendingByDefault, which defaults to NO, so the auditor could report
a false CRITICAL. Qualified at every site, and in the agent source rather than in
its generated output — the inlined sub-skill is built from that source by
build-inlined-auditors, so editing the output alone would have tripped the drift
gate and been overwritten.
The suite is substantially correct elsewhere: the @concurrent and actor machinery,
the Sendable rules, the OS27 ProgressManager section and the crash signature table
all check out, several of them character-for-character.
Verified: 518/518 unit tests, pre-deploy --static clean, all six generators current,
and all 8 files recorded in the out-of-repo ledger (corpus 44 -> 52 verified).
The warn tier's last 8 findings were all real values sitting in fixtures: three
Apple OS binary UUIDs (libsystem_kernel.dylib, /usr/bin/yes, dyld) from a real
cpuprofile trace, and a display UUID captured from `devicectl device appResize
set`. None was sensitive, and the obvious response was to exempt them — which
would have been the wrong one. An exemption is permanent: it lives in the scanner
forever, and each one narrows what the gate can still catch.
The values were also inert. cmd_resize_test.go asserts the parsed "Actual size"
field and never reads the display id; the cpuprofile assertions need only the
fixture and the expectation to agree.
So the fixtures carry synthetic UUIDs now, and the scanner's own test assembles
its unpatterned control values from parts rather than writing literals. That last
part matters because the test file is shipped content like any other — a literal
there is a value the gate has to carry forever. Same move e6f267ce made for the
deny-list values: build it, don't ship it.
The tier is therefore empty with no exemptions, so the next warning is real.
Measured with `node scripts/leak-scan.ts` over 1978 shipped files: 16 -> 8 -> 0.
The 8 in the middle were real but not identifying — one OS-binary UUID per
architecture, plus a display id that devicectl regenerates on every boot
(measured across three boots of one simulator).
cmd_resize_test.go's comment claimed a verbatim capture above a value that is
now synthetic; corrected.
Verified: 518/518 unit tests, xcprof/xcsym/xcui go suites, leak scan 0/0.
tools/xcprof/testdata/toc.xml and network-toc.xml carried the maintainer's
IOPlatformUUID in the trace's <device> node, next to model="Mac Studio" and
os-version="26.5 (25F71)". Verified against `ioreg -d2 -c IOPlatformExpertDevice`,
which returns the same value. It entered in 2a82c80a and was re-added in 427d4922.
Replaced with the RFC 4122 canonical placeholder, which the scanner already
recognizes. No code parses that node's uuid — the toc tests read duration and family
data — so the change is inert, confirmed by the xcprof suite and the unit suite.
The warn tier is what surfaced this, so the tier stays populated rather than being
silenced. Two changes reduce it on merit instead: the 4C4C44 prefix joins the
recognized placeholder shapes (ASCII "LLD"; a real UUID carrying those three bytes
has probability ~1/16.7M), and the removed value is simply gone. Tier 16 -> 8, and
the remainder is four OS-binary identifiers plus one captured devicectl session id.
Verified: 518/518 unit tests, xcprof go tests, leak scan 0 errors over 1978 files.
Two P3 guards in the content gate could not do the job they were written for.
`pre-commit-axiom.sh` captured the scanner's output and then filtered it to
`^ ✗|error(s)`. When the scanner crashed rather than reported, that filter
discarded its entire message and the hook still asserted "private data in shipped
content" — a confident diagnosis the run had produced no evidence for. The hook now
branches on whether a `✗` line exists: findings are printed and the failure says so,
and their absence means the scanner failed, which gets its own wording and the raw
output. Verified in a sandbox against a scanner that throws and one that reports.
`always-on-footprint.test.ts` asserted the absence of a block scalar against
`String(matter(file).data.description).trim()`. gray-matter resolves `description: |`
to its content, so that assertion could only fire on a description whose literal text
was `|` — the byte count the test sums changes for exactly the case it was meant to
catch. The shape is now asserted against the frontmatter region of the file text, and
fires on `|` and `>-` while leaving a quoted "|" alone.
Also drops an SC2181 in the same hook. pre-deploy check 11 shellchecks only the
plugin's own hooks/ directory, so scripts/git-hooks/ — this gate's machinery — has
never been linted.
Verified: 518/518 unit tests, 14/14 installer tests, shellcheck clean, and both hook
branches exercised end to end.
The test-suite workflow's `paths` filter listed nine prefixes while its two steps
read considerably more. `node scripts/leak-scan.ts` walks every SURFACES entry
plus ROOT_FILES — `.cursor-plugin`, `.agents`, `axiom-codex`, `axiom-cursor`,
`axiom-mcp` and the root release files among them — and the unit suite covers
`axiom-pi` and `axiom-mcp`. A PR that added a home path to README.md or a real
UUID to `axiom-codex` therefore started no job at all, which is exactly the case
this workflow exists to backstop on a fresh clone or a `--no-verify` commit. The
filter now lists all twenty entries per event, and a test derives the coverage
requirement from the scanner's own SURFACES and ROOT_FILES so the two cannot
drift apart again.
`install-git-hooks.sh` grew the hook by one line on every run: the strip removed
the block but left the separator blank line on both sides of it, and the insert
added one back. The block now contributes no leading blank and the strip consumes
the trailing one, so a re-install leaves the file byte-identical.
Verified: 518/518 unit tests, 14/14 installer tests, leak scan 0 errors across
1978 shipped files.
Five defects, all in the gate added on 2026-09-15, found by two independent review
passes and then traced to root causes.
The installer appended its block to .git/hooks/pre-commit, so a foreign block that
terminates the script left the whole gate unreachable while the installer reported
success. This repo's own hook is that case: the beads block carries
`if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi`. It now strips any existing block
and re-inserts after the shebang, which also MOVES a block an older revision left
in the wrong place — the live hook was reordered by running it.
SURFACES replaced the tracked `axiom-mcp` entry with `axiom-mcp/dist`, leaving 38
tracked files — src/, package.json, and the npm-published README and LICENSE — in
no surface. Both entries are needed; both are present now, and the scan sees 1,977
files against 1,939 before.
Both warn-tier suppressions were dead. The issue-id guard tested m[0], which
carries the `issue_id": "` prefix, against an anchored id pattern: it could never
match, so it warned on the ACME-*/ASC-* placeholders it exists to suppress. The
UUID allowlist missed the RFC 4122 example and every doubled-character shape.
Warnings fall from 89 to 16, and a test now asserts a suppressed value stays quiet
— the one thing the suite never checked.
Check 12s counted its warnings and printed none while telling the reader to eyeball
findings it never showed. They go through warn() now, which the Phase 1 block
renders.
The CLI derived its root from new URL(import.meta.url).pathname, which keeps
percent-encoding: from a checkout path containing a space every surface walked to
nothing and the guard then blocked every commit, naming the wrong cause. Verified
in a worktree at such a path — old idiom exit 1, new idiom exit 0. A lint keeps the
lossy idiom from returning anywhere in scripts/.
.github/workflows/test-suite.yml runs the unit suite and the content scan on pull
requests and on main. Neither had ever run off a maintainer's machine. It
deliberately omits `npm run test`: that gate's first check shells out to
`claude plugin validate` and treats the CLI being absent as a failure.
Verified: 515/515 unit tests, Phase 1 static PASSED, leak scan 0 errors / 16
warnings over 1,977 files. The remaining 16 are real dylib and profile UUIDs in
tools/*/testdata, listed in the summary output for a decision on allowlisting.
The Harness Support intro said Codex has "no per-prompt routing hook in the
same form", three lines above a table that marks per-prompt routing supported
on Codex. The clause predated the port of the hooks to the Codex variant: the
generated axiom-codex/hooks/hooks.json binds UserPromptSubmit with no matcher
(Codex rejects one on that event), and user-prompt-submit.py returns
hookSpecificOutput.additionalContext, the context-injecting shape a routing
nudge needs.
Removed the clause so the prose agrees with the table and with what ships. The
page already states the real caveat elsewhere — Codex needs
features.hooks = true, and a skills-only install gets no hooks at all.
Verified: docs build clean, npm run test:unit 498/498, Phase 1 static PASSED.
Asking for the staged blob of an untracked-but-published file is an expected miss;
printing 'exists on disk, but not in the index' forty times inside check 12s buries
the result line.
Sixteen findings across two reviews, all in the guard and the fixes it prompted.
The guard:
- The pre-commit step resolved `tsx` through `npx`, and tsx is not a dependency:
on a fresh clone or offline, the download failure took the failure branch and
blocked the commit with "private data in shipped content" — a wrong and alarming
diagnosis. It runs under plain node now (Node 24 strips types), like every other
TypeScript entry point here.
- A failing run printed `tail -20`, which is trailing warnings plus the summary —
the `✗ path:line` naming the offender scrolled off. It greps for the error lines.
- Files were listed from the index and read from the working tree, so staging a
leaking revision and restoring the clean file on disk passed the scan. It reads
the staged blob, and a staged-then-deleted file no longer throws ENOENT.
- Several tracked, shipping surfaces were never scanned: `.cursor-plugin/`,
`.agents/`, the whole `axiom-pi/` package, the submission Markdown at the root,
and `package-lock.json`. The npm bundle needed the opposite treatment —
axiom-mcp/package.json publishes `dist/`, which is gitignored except two JSON
files, so that surface is read from disk.
- CHANGELOG.md was in ROOT_FILES but is gitignored, so the entry could only fire on
the fallback path: the scan reported clean while the file still named the app.
Root files are read from disk now, and that is exactly what it found — a third
mention the earlier cleanup missed, and which the close note on Axiom-1z8m
wrongly claimed was fixed. Corrected here.
- The uuid rule was uppercase-only (Claude Code session paths are lowercase), the
tracker rule's exclusion list sat where it could not apply (checked in code now),
and printable runs in binaries were collected at 16+ characters, long enough to
miss short values.
- A scan that finds almost nothing now reports a broken scan instead of clean
(floor on the file count, in both the CLI and check 12s).
- The hook itself was untracked, so the guard vanished on a fresh clone. It lives
in scripts/git-hooks/pre-commit-axiom.sh and is spliced in by
scripts/install-git-hooks.sh, idempotently, leaving the beads block alone.
The fixtures the earlier round was supposed to have cleaned:
- The two xccrashpoint copies of the crash still carried the framework's full codec
inventory and its build UUIDs, and the copy that was edited kept the same names
in the path column. Both files and that column are neutralised.
- The real crash instant survived in the two fixture file names while the header
said 2026-01-01; the names now match the header, and nothing asserts them.
- crash_text_test.go pins that the dedup-symbol frame resolves its image, so the
case cannot pass while exercising the unknown-image path.
Scan: 0 errors, 89 warnings across 1937 shipped files. Unit 498/0; xcsym and xcui
pass, including the anonymizer fixed-point test against the edited fixtures.
My earlier rebuild (the privacy commit) dropped the stripping the committed
binaries had: they went from 3.9-8.8 MB to 9.0-9.9 MB. The Makefiles now pass
-ldflags "-s -w", which is what the hand-built binaries had before a Makefile
existed for all four tools. Sizes are 6.0-6.6 MB now — three of the four are
smaller than the versions they replace, and xclog is larger than its 3.9 MB
hand-built predecessor, which used an older toolchain. All four Go modules pass.
Groundwork for scrubbing the repository's history: a test that asserts on private
values puts those values back into the tree, where any history rewrite has to
touch them. The engine now takes its rules as a parameter, and the tests exercise
it with their own synthetic set. The shipped list gets a structural test instead —
rule ids and the five error classes, not values.
Axiom-1z8m. The identifier class leaked four separate times — skill examples, the
xcsym/xcui fixtures, a Cursor hook fixture carrying a real session path and
session UUID, and all four bundled binaries embedding the maintainer's source
layout — and no gate saw any of it, because the pre-commit hooks match file names
and directories only.
scripts/leak-scan.ts scans what ships, where "ships" means tracked: the plugin,
the generated Codex/Cursor variants, the MCP bundle, docs, and the tool and
script trees that carry fixtures. Files are read as UTF-8; anything with a NUL in
its first block is scanned through its printable runs, which is how a source path
embedded in a binary gets caught by the same rules as prose.
Rules, each with a test: project names, personal home paths (including Claude
Code's encoded -Users-<name>- form), session temp paths, and timestamp-shaped
build stamps are errors; unpatterned UUIDs and quoted tracker ids warn, and
obviously synthetic values (AAAA…, 1A2B…, ACME-*, /Users/you, REDACTED) are left
alone so the warning tier stays worth reading. One allow entry exists, for the
Cursor fixture, whose whole purpose is to be shaped like a real session path.
Wiring: check 12s runs inside pre-deploy, and the pre-commit hook runs the scan on
every commit regardless of staged paths — the class has come through tools/,
fixtures and binaries, so staged-path gating cannot cover it. tools/ also joins
the hook's full-validation trigger set. Proof it can fail: planting "ExampleApp" in
README.md makes the scan exit 1 with the file and line, and reverting restores it.
It also found two real leaks on first run: the xccrashpoint fixtures still carried
the real app version and crash timestamps (the earlier fix covered only the
apple_crash fixture), and two public CHANGELOG entries named the app while
describing past scrubs.
The restored sentence — "This agent writes code; it has no /axiom:audit form" — was
invisible while it lived in the frontmatter description, which every emitter
strips. In the body it is emitted, the Codex build annotates it as unresolvable,
and check 12o flags the resulting pointer. Not a check bug: Codex ships no
commands, so a bare /axiom:audit reference has no target to resolve. Reworded to
keep the hand-off ("use the iap-auditor agent, which /axiom:audit iap invokes")
and drop the dangling reference. Phase 1 clean.
- always-on-footprint.test.ts computed the expected skill-listing size with a
regex that mirrored the reader's own inline rule, so it agreed with the reader
even when both were wrong: a quoted description kept its quotes, a
continuation line was dropped, and a `description:` with an indented
continuation failed with a misleading message. It now parses with gray-matter,
as the sibling listing test does for the same reason.
- crash_text_test.go's dedup case named its frame image "SomeApp" while the
images slice it is passed holds "App", so UUID lookup missed and the case
exercised the unknown-image path instead of the one it documents.
- cmd_triage_test.go's comment still promised "huge user count" over data that
had become users:9 events:12. The contrast is restored with invented values
(1200/4500) rather than the real figures.
Axiom-2fa kept the first sentence and moved the <example> blocks, but dropped
everything else the description carried, in all 42 agents — the closing prose
that said what the agent covers and, in three cases, who should get the prompt
instead:
- iap-implementation lost "This agent writes code; it has no /axiom:audit form.
To review existing IAP code instead, use /axiom:audit iap (the iap-auditor
agent)." The implementation agent has Write/Edit and the shipped trigger had
no exclusion, so review-shaped prompts named the same vocabulary.
- triage-analyzer lost "For a single crash file (.ips, MetricKit, .crash,
.xccrashpoint), use the crash-analyzer agent instead."
- swiftui-architecture-auditor lost "Complements (but is distinct from)
performance and navigation audits."
My own verification missed it because it compared bodies, and this content was
in the frontmatter. Each agent's dropped prose now lives in a `## Scope`
section, and a check confirms every tail sentence exists in its file.
Two router descriptions lost capabilities in the same rewrite and the restore
pass missed them: axiom-build's test-crash trigger ("a crash log needs
diagnosing, a test run crashes") — the capability ships in SKILL.md and
xcode-debugging.md, and axiom-testing routes it here — and axiom-shipping's
"age ratings", which the suite documents in four places and the deterministic
hook matches verbatim.
Found by an independent security review of the repo, not by a gate — the
leak-prevention hooks are filename-scoped and cannot see content classes.
- scripts/cursor/fixtures/cursor-3.17.8-hook-payloads.json embedded a real
session path (which encodes the maintainer's home directory) and a real
session UUID in seven fields.
- The committed .crash fixture kept the real app's Version and build stamp
(1.0.0 / 1000000000), the crash's Date/Time and Launch Time with the
device's UTC offset, and the full binary inventory of a framework whose name
the anonymizer deliberately hides (wavpack, ogg, FLAC, opus, vorbis, lame,
mpc, mpg123, sndfile, tta-cpp, Lottie) with their build UUIDs. The
anonymizer has no rule for any of those keys, which is why they survived;
editing them does not touch the fixture's fixed-point test because nothing
rewrites them.
- scripts/migrate-skill-namespace.sh hardcoded /Users/you/Projects/Axiom,
publishing the layout and making the script unusable anywhere else.
- Four shipped skill files used the maintainer's identity where the corpus
uses placeholders: /Users/you/... in sandbox-and-file-access.md,
~/charles-personal.p12 and "Apple Distribution: Charles Personal (ABC123)"
in code-signing.md, and an unpatterned device UDID in xctrace-ref.md.
- testflight-triage.md published measured production triage figures and a real
app-internal symbol (16 of 17 signatures, crashHandlerSymbol); the lesson
survives without the numbers or the symbol name.
- The three tool Makefiles built without -trimpath, so the maintainer's source
path was embedded in every shipped binary, and xclog had no Makefile at all —
its binary was hand-built with the absolute path in its debug info. All four
tools now build with -trimpath; strings over the four shipped binaries
reports zero /Users/you occurrences, down from xclog=1 and the three
others at their previous values.
Every skill writes `description: Use when …` inline; every agent writes a `|`
block scalar. The reader handles both, but the test guarded only the block form —
so the shape the skills actually use was untested, and an inline-blind reader
scores 27 descriptions at a handful of chars and reports the listing as free.
That silent-zero class bit three times on 2026-09-15: run.py located the hook via
HERE.parent.parent and printed 0/17 recall with 0% false positives after a
directory move; an ad-hoc crash-triage check read only block scalars and reported
zero hits against 27 inline descriptions; and this test could not have caught
either.
The expectation is computed from the files rather than from the reader, with a
floor on the sample count so the guard cannot become vacuous, and an assertion
that a future block-scalar skill description extends the test instead of silently
passing it.
Claude Code's always-on cost was 54,966 chars, 39,359 of it agent descriptions —
the same 42 agents Cursor and Codex carry for ~6,100, because both emitters
already truncate each description to its first sentence. The <example> dialogues
exist to teach triggering; the first sentence already carries the trigger, and
the dialogues now live in the body, which loads only when the agent runs.
Measured before landing — 169 prompts (each agent's own <example> user lines,
labelled with the agent that example names) classified against all 42
descriptions, two runs per condition:
full text 169/169, 169/169 (the examples contain their own answer, so
this level is an artifact, not a baseline)
first sentence 168/169, 166/169
The three deviations are two-way calls between plausible agents —
spm-conflict-resolver over build-fixer for "No such module after I updated
packages", test-debugger over build-fixer for "tests passed yesterday but now
fail", security-privacy-scanner over grdb-performance-auditor for "scan for SQL
injection in GRDB code". No cross-domain misroutes. The corpus cannot rule out a
larger effect on other model classes, and it is built from the removed examples,
so it is the hardest available test for the truncated form, not a field result.
Why the risk is bounded: the surface that changes is discretionary selection.
The skills carry 102 "Launch `<agent>` agent" directives covering all 42 agents,
and commands another 33, so designed flows choose their agent from skill text
either way — untouched here.
Always-on: claude-code 54,966 -> 22,059 chars (~18,322 -> ~7,353 tokens, -60%).
Ceiling ratcheted 56,000 -> 24,000.
Gates that had to move, with reasons:
- always-on-footprint.test.ts asserted the 6.4x Cursor/Claude Code gap. The gap
was the defect, so it now asserts near-parity; re-opening it fails the test.
- audit-parity's advertised-area and advertised-command parsers read the
frontmatter description only. The `Explicit command:` hints now live in the
body, so both scan the whole file — a ghost command promised in the body is
exactly as broken as one promised in the frontmatter, which is the check's
purpose. Two tests encoded the old frontmatter-only contract; they now assert
the new one, plus a new case proving a body-promised ghost is caught.
- Four agents (energy, memory, swift-performance, swiftui-performance) had lost
the vocabulary their audit area checks for; it is back in the trigger
sentence, which also routes on those words now. foundation-models and
spritekit gained theirs as well.
- `/axiom:audit all` carries "(Claude Code only)": Codex has no commands and the
emitter cannot map `all` to an area.
Note: 250 chars is Axiom's own emitter budget, not a platform cap. The Agent
Skills spec allows 1024; Claude Code documents 1536 for description +
when_to_use; Codex enforces 1024 and shortens long descriptions for its listing.
The xcsym and xcui test fixtures carried data from a real project: the app
name in sample crash reports, a live Sentry issue ID (APP-3V) with its real
impact counts, and a dated incident reference in a comment. Axiom is a public
repository; a private app's crash telemetry has no business in it.
Placeholders only — "SomeApp [14250]", "APP-3V", users:9 events:12, and the
incident comment drops the app name. Every execution path is unchanged: the
anonymizer's bracket/pid handling, the header-line extraction, the dedup
symbol classification, and the triage noise-flagging all still exercise the
same shapes. The impact numbers in normalized_test.go moved with the assertion
that checks them.
Note: this removes the data from the current tree, not from git history.
The assertion's expectation re-implemented the loader's own parse rule, so it
compared the module against a copy of itself and could not catch the loader
mis-reading a shape it accepts: a quoted description keeps its quotes and a plain
multi-line scalar loses its continuation, and both passed. gray-matter (already a
devDependency) returns the frontmatter's meaning instead. The manifest-vs-disk
guard for this file remains the sibling test, which fails when the listing comes
from claude-code.json because axiom-tools is deliberately absent there.
An independent review of the previous commit found that seven of the twelve
rewrites removed the only description surface for content their own suites still
ship — the same defect the rewrite existed to repair, reintroduced elsewhere.
- axiom-integration had dropped "timers" and "reminders"; timer-patterns.md and
eventkit.md own both, and axiom-performance routes four timer prompts here.
- axiom-swiftui had dropped "performance" and "architecture" to stop colliding
with other routers, but swiftui-performance.md and architecture.md are its own
children and its own text says "try axiom-swiftui first" for slow UI. Scoped
instead of dropped: "view-level performance, feature architecture".
- axiom-media had narrowed CarPlay to metadata; carplay-hig.md,
carplay-templates-ref.md and carplay-navigation-ref.md own app design,
entitlements, templates and navigation, and axiom-design routes that work here
by name.
- axiom-build had claimed TestFlight crash triage, which axiom-shipping owns
(its own table forwards "beta tester reported a crash" to that suite); it now
claims the local crash log it actually analyses.
- axiom-design restores auth-flow structure (app-composition.md owns the state
machine); axiom-shipping restores App Store Connect automation alongside the
accurate Xcode Cloud claim (asc-mcp.md).
- axiom-apple-docs loses its trailing mechanism clause, which told the model what
the skill does rather than when to load it.
- axiom-performance's platform anchor is "Apple-platform", not an enumeration:
its children carry watchOS and tvOS memory, energy and MetricKit content, and
"battery drains" is the canonical watchOS question.
Measured with the same description-only classification over 59 real and 25
synthetic prompts, before and after the whole change: SHOULD recall 16/17 ->
17/17, pointer rows routed 1/24 -> 0/24, false fires 2 rows (one unique prompt,
"Do we have a pop-triage skill?", which axiom-tools' own text claims to answer).
axiom-swiftui's share of picks falls 40% -> 35%; the deeper reduction the first
pass measured came from orphaning its two children and does not survive contact
with the suite's actual contents.
The test is named "carries the frontmatter description verbatim" but asserted a
hardcoded prefix of one description, so it failed on every edit to that text
without testing its own claim. It now reads axiom-shipping's SKILL.md and
requires exact equality with the shipped listing entry — the same guard against
the manifest-vs-disk defect it was written for, and immune to description edits.
Nine defects in the 27 router descriptions, from the Axiom-65q.10 measurement of
the one invocation mechanism that had never been tested — the model's own choice
from the description list. Every harness is affected, including MCP, where
descriptions are the only lever.
- Crash reports had no description surface at all. /axiom:analyze-crash,
/axiom:triage, xcsym and the crash-analyzer and triage-analyzer agents ship, but
nothing said so: axiom-build now names crash logs and TestFlight crashes,
axiom-shipping names TestFlight/Sentry corpus triage.
- axiom-apple-docs fired on nothing. Its trigger was a superset of every other
router's and its Covers list duplicated five suites, so a maximal trigger with
no discriminating content was easy to ignore. It now triggers on needing
Apple's own documentation rather than on any Apple question.
- Four routers named no platform, so axiom-data's trigger literally covered "set
up a Postgres migration". iOS/macOS/visionOS anchors added to axiom-data,
axiom-networking, axiom-performance and axiom-concurrency.
- axiom-swiftui claimed "performance" and "architecture", colliding with two other
routers; it absorbed 40% of picks. Both claims dropped.
- axiom-media claimed the bare phrase "Now Playing" — also a screen name — so
layout work on a screen called Now Playing routed to the wrong suite. Scoped to
lock-screen and CarPlay metadata.
- axiom-design is scoped to visual and interaction decisions for an Apple app, and
no longer claims authentication flows.
- axiom-swift is narrowed off "any Swift edit".
- axiom-integration's 13-noun keyword list became the condition it stands for,
which is what skill-descriptions.md asks for.
- Xcode Cloud configuration claimed by axiom-shipping.
Listing text: 5,429 -> 5,317 chars. axiom-media (273) and axiom-shipping (252)
were past the 250-char point where the Codex variant truncates mid-sentence; both
now ship whole.
The page scoped MatchType to iOS/macOS/watchOS/visionOS 27 and excluded tvOS.
DataDetection carries @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0,
visionOS 26.0, *): all five platforms since 26. Only the SwiftUI
.dataDetection(_:options:) modifier this bullet sits beside is 27-only, and only
on iOS/watchOS/visionOS.
Xcode 27.0 released as build 27A266a — the same build the release-candidate sweep
probed, with the iOS SDK at 24A430 — so wording that called it "the 27.0 RC" now
reads as pre-release in guidance describing what shipped.
Restamped only where RC stood in for the release ("verified on the 27.0 RC", "as
of the Xcode 27.0 RC SDK", "still gone in the Xcode 27.0 RC"). Beta-by-beta
history and every build string are kept: both stay accurate for readers on older
Xcodes.
Three lines needed more than a rename, after re-checking each against the 27.0
SDK. The Foundation Models adapters' "no replacement" claim drops its "(beta 1)"
provenance — Adapter carries deprecated 26.4 / obsoleted 27.0 with no renamed: or
message: hint, and LanguageModelExecutor ships as the pivot. The xcode-mcp tool-set
sentence names the release instead of a bare version sitting next to "beta 6".
Tap-to-pay keeps its provenance hedge, now scoped to the shipped SDK.
A setup failure (or a hard kill) would otherwise leave axiom-parity-probe-<pid>.swift
in the shared temp root — the junk class this suite exists to guard against, and
now harmless to the detector but still litter.
Corrects two things the earlier commits in this area got wrong, and closes the
false positive they left open.
CrashReportExtension visionOS availability. e5f1018b dropped the visionOS claim
on the strength of developer.apple.com symbol-page badges; the SDK contradicts
them. Against the installed Xcode 27 SDK,
'xcrun --sdk xros swiftc -typecheck -target arm64e-apple-xros27.0' compiles
clean, xros26.0 reports "only available in visionOS 27.0 or newer" (a version
gate, not an exclusion), while tvOS and watchOS report "unavailable" outright
and Mac Catalyst has no module at all. The .swiftinterface carries
@available(iOS 27.0, macOS 27.0, *) with @available(tvOS, unavailable) and
@available(watchOS, unavailable), and no visionOS clause. Apple's pages disagree
with one another, so the SDK leads: skill text, the version-support row and the
docs page are restored with the reasoning inline.
Temp-root guard. Three ways the guard added by ba04043b could still misjudge a
project:
- a TMPDIR-less process got /tmp from tempfile.gettempdir(), leaving the macOS
per-user scratch root unneutralized and the original false positive alive for
any launcher that scrubs the environment — roots now also come from the
filesystem on darwin (containers and their T/ dirs);
- a relative TMPDIR resolved against the detector's own cwd — the project being
judged — turning a real Apple project into a "temp root" and silently
disabling Axiom; only absolute values are accepted;
- the scan-root guard ran before the repo-root exemption, so a repo rooted at a
temp root (devcontainer or CI exporting TMPDIR to the workspace, or a clone in
/tmp) read as non-Apple; the exemption now wins.
Mirrored into axiom-pi/src/session.ts with the same three tests; the parity
matrix gained a repo-rooted-at-a-temp-root case and TMPDIR control.
Verified: detector 54/54, axiom-pi 69/69 + typecheck, npm test PASS, test:unit
exit 0, check:cursor clean, docs build clean.
axiom-pi/src/session.ts is a hand-maintained port of project_detect.py and had
the same defect the Python fix just closed: a marker in the shared temp root made
every cwd beneath it read as an Apple project, so Pi injected Axiom context in
non-Apple workspaces. Checked against the exported isAppleProject before the
change: temp dir true, temp root true.
- port the temp-root neutralization (TEMP_ROOTS Record + computed realpath set,
guard in the upward walk, temp root as a vacuous scan root)
- mirror the three regression tests from project_detect_test.py
- add a cross-implementation parity gate: one fixture matrix (plain dir, marker
at cwd, in an ancestor, above a git root; .swiftpm home; visible .swiftpm
package; project inside the temp root; plain temp dir; temp root itself;
missing path; oversized tree) run through both implementations, failing on any
mismatch. It rides the axiom-pi suite, which the full pre-deploy runs.
Both cursor adapter contract tests failed on main: they assert that a temp
workspace is not an Apple project, and the detector said it was. Root cause is
not the tests and not the adapter — the upward walk found an Apple marker sitting
in the shared temp root itself ($TMPDIR/plan-test.swift, left by an unrelated
tool), so every cwd beneath $TMPDIR inherited it. Same class as GH #52's
~/.swiftpm: tool state, not project evidence.
- neutralize the temp root in the upward marker walk ($TMPDIR plus /tmp,
/var/tmp, /private/tmp, abspath and realpath forms)
- treat the temp root as a vacuous scan root, so containment there never falls
through to a whole-tree scan
- revert the failing tests to green with three new detector tests: a marker in
the temp root is not evidence, the temp root is not a project, and a project
that genuinely lives inside a temp dir is STILL detected (over-correction guard)
Regenerated axiom-codex/ and axiom-cursor/ mirrors.
Verified against Apple's docs:
- visionOS is unavailable (the framework, CrashReporterExtension, and
CrashedProcess pages all list it), so drop the 'also in the visionOS 27 SDK'
claim
- add the Mac Catalyst exclusion and the iOS-apps-on-Apple-silicon-Mac case;
Part 1 lists Catalyst for MetricKit, so the omission read as Catalyst-safe
- add an Extension Setup subsection: the com.apple.crash-reporter.extension
extension point, the child-bundle-ID rule, and the Xcode template
- mirror the availability in the Version Support table and the docs page
Regenerated axiom-codex/ and axiom-cursor/ mirrors.
AGENTS.md is now a symlink to CLAUDE.md: harnesses whose context loader reads
file bytes without expanding @ imports (the pi loader vendored in axiom-pi is
one) would otherwise load the 4-line pointer and never see CLAUDE.md.
AGENTS.md becomes a pointer (@CLAUDE.md) for omp/Codex/Cursor; both files
are now gitignored local dev state, so the public repo ships no agent
instructions. Removes the stale bd sync / bd dolt push workflow text that
only the tracked copy carried.
Corrects the size-class guidance shipped in 27.0.0, verified on physical
devices, and adds two smaller pieces of guidance.
- Size classes follow resizable iPhone windows (iPhone Mirroring,
iPhone-only apps on iPad). Axiom had said they stay .compact. Popover
adaptation and injected-.regular guidance are corrected to match.
- Liquid Glass auditor Pattern 8: custom bars pinned over scrolling content
should use safeAreaBar(edge:).
- iPhone Duo: Split View size-class row and custom-grid fold guidance.
- Cursor variant and MCP bundle regenerated.
Gates: test:full both phases, version parity across all 9 files, router
integrity 417/0, VitePress build. Two independent content reviews applied.
Behavioral smoke tests on iphone-duo, layout, and presentations.
Since June the skills said an iPhone app "stays .compact at any width" in
iPhone Mirroring and as an iPhone-only app on iPad. They also said
popovers stay sheets there, and that injecting .regular merely fails to
produce a sidebar. All three were wrong.
- A resizable iPhone window keeps the .phone idiom, but its size classes
follow the window.
- A popover presents as a popover once both size classes are regular. A
wide but short window is still vertically compact.
- Injecting .regular hides a sidebar-adaptable TabView's tabs in a narrow
window. Regular width alone doesn't guarantee a visible sidebar either;
isTabViewSidebarAvailable (iOS 27) is the signal.
Measured:
- iOS 27.0 simulator appResize session: compact to regular between 655
and 700 pt.
- iPhone Mirroring on an iPhone 16 Pro Max: 440x956 compact, then 1144x845
regular.
- iPad Pro 12.9-inch with Windowed Apps, iPhone-only app: regular at
683 pt, compact at 477 and 375 pt. The sidebar starts collapsed behind a
toggle at 683 pt. Under Full Screen Apps the app runs in the fixed
compatibility box.
The iPhone Duo size-class table in layout.md also gains the Split View
row.
- The size-class table gains a row for one half of Split View, marked "not
stated": neither Apple's talks nor the design guidelines give its size
class. A new red flag warns against treating the inner display as wide.
- The fold section gains custom grids, from talk 111463: keep outer
margins and widen the gap at the fold, and prefer an even column count
when a division region exists. The gap lands on the fold only when the
grid is centered and the fold is vertical. Locating the fold uses the
reserved-region query, marked 27.1.
A custom bar pinned over a List or ScrollView with .overlay or a ZStack
doesn't inset the safe area, so the last rows can't scroll clear of it.
.safeAreaInset fixes the inset, but rows under the bar stay sharp.
safeAreaBar(edge:) (iOS 26) insets the content and extends the scroll edge
effect.
Pattern 8 finds these bars and skips floating buttons, toasts, content
that already reserves room, and pre-26 fallbacks. Ordinary actions go to a
.bottomBar toolbar item and mini players to .tabViewBottomAccessory. On
iOS 26 the bar drops its own background, since a .bar material paints a
flat band over the edge effect. UIKit gets
UIScrollEdgeElementContainerInteraction.
26-ref's safeAreaBar note is corrected: it is not "safeAreaInset with blur".
Verified on the iOS 27.0 simulator: .overlay left the last row under the
bar, safeAreaInset left rows under it sharp, and safeAreaBar did neither.
Axiom leaves beta for the OS 27 cycle.
- iPhone Duo hub in axiom-swiftui, with fold, pose, and camera guidance
folded into layout, presentation, adaptive-layout, and camera skills
(276 skills, up from 275)
- Every 27-cycle claim re-verified against the Xcode 27.0 RC, plus 27.0
SDK coverage for AVFoundation, PhotoKit, ScreenCaptureKit, Background
Assets, XCUITest VoiceOver, and TextKit
- UIDesignRequiresCompatibility is ignored for 27-SDK builds on OS 27;
the Liquid Glass skill and auditor are corrected to match
- iOS 26 navigation bar subtitles in SwiftUI and UIKit, with hook routing
- Authoring metadata footers removed from skill files
- Cursor variant and MCP bundle regenerated
Gates: test:full both phases, version parity across all 9 files, unit
suite 488/488, hook tests 144, MCP tests 181, cross-refs 333 files / 27
suites, router integrity, VitePress dead-link validated.
Skill files are loaded into model context, so every line costs tokens on
every read. History, Last Updated, Created, Skill Type, numeric Version,
authoring Status, and Tested footers told the model nothing it could act
on, and git already records them. They are removed from every skill that
carried one (skills touched by other commits in this release were cleaned
in those commits).
Long platform lists ("iOS 26+, iPadOS 26+, macOS Tahoe+, …") now use the
same compact tags as cycle markers, such as "OS26, not tvOS", with no loss
of information.
Prompts about navigation subtitles and the Liquid Glass compatibility key
reached no skill, or the wrong one.
- navigationSubtitle and the subtitle and large-title toolbar placements
route to axiom-swiftui; UINavigationItem subtitle properties and
subtitleView route to axiom-uikit. A bare "subtitle view" needs iOS or
navigation context, so video-caption prompts don't match.
- Caption and subtitle styling for media no longer captures navigation
bar subtitle styling.
- The macOS navigationSubtitle rule needs Mac, Catalyst, window, or
title-bar context.
- UIDesignRequiresCompatibility, and plain-language requests to opt out of
the new OS 26/27 design, route to axiom-design; generic "compatibility
mode" questions from other platforms do not.
144 hook tests pass, and every mutant of the new rules is killed.
UIDesignRequiresCompatibility does not keep the old design everywhere.
Measured on simulators, the key is ignored when an app is built with the
27 SDK and runs on OS 27. Apps built with the 26 SDK stay in compatibility
mode on OS 27, and 26.x honors the key for any SDK. The skill now gives
that SDK-by-OS matrix, its consequences, and a migration path, instead of
treating the key as a durable opt-out.
The liquid-glass-auditor was recommending APIs that don't exist or don't
apply:
- Glass is applied with glassEffect (glassBackgroundEffect is visionOS
only).
- A Spacer between toolbar items is a compile error; use ToolbarSpacer.
- Glass belongs on floating surfaces, not content.
- It now finds the compatibility key in Info.plist and flags builds that
depend on it.
- It recognizes existing glass adoption, including glass button styles
and search tabs, rather than recommending it again.
health-check dispatches the auditor on glassEffect, GlassEffectContainer,
the blur and visual-effect views, and the Info.plist key.
HIG and wellbeing skills refer to iOS 18, not a nonexistent iOS 25, as
the pre-Liquid Glass release.
A reviewer noted Axiom offered nothing on largeSubtitleView, and it had no
coverage of iOS 26 navigation subtitles at all.
SwiftUI (toolbars Pattern 14): navigationSubtitle, the .title, .subtitle,
.largeTitle, and .largeSubtitle toolbar placements, and a principal-item
fallback for earlier releases and compatibility mode.
UIKit (uikit-modernization): the UINavigationItem subtitle family, in
string, attributed, and custom-view forms for the inline and large title,
with the precedence between them.
Behavior was measured on iPhone simulators running iOS 26.5 and 27.0:
- largeSubtitleTextAttributes has no effect on the item, the bar, or the
appearance proxy; style with largeAttributedSubtitle instead.
- Large subtitle views hide when the bar collapses.
- A UIButton subtitle view is centered unless its content alignment and
insets are changed.
- Custom views win their slot even when empty.
- Subtitles don't render under UIDesignRequiresCompatibility, so the
fallback is chosen by the SDK the app is built with.
The swiftui-26 reference points to the new pattern; its WebPage
availability note is re-verified against the 27.0 RC, and it gains an
iPhone Duo concentric-corners pointer.
APIs that shipped in the 27.0 SDK but had no Axiom coverage, each checked
against the RC SDK and the published snippets compiled.
- AVFoundation: async session activation and the deactivation and
resumption notifications that replace interruption types; throwing
AVAudioEngine and node APIs (connectNode, playAudio, installAudioTap)
that replace calls that trapped on misuse; realtime-safe render blocks
are ObjC-only.
- PhotoKit: Apple Reference Image viewer, the persistent change history
observer, asset keyword/rating/caption editing, the root folder
collection, synced-only identifier mapping, and background resource
upload configuration.
- ScreenCaptureKit: clip buffering, the recording editor, new error codes,
and a per-platform availability table now that it ships beyond macOS.
The Presenter Overlay delegate methods use their shipped names
(outputVideoEffectDidStart/Stop), and the iOS screen-capture sample
compiles under Swift 6.
- macOS 27 denies another team's app or app group container without a
prompt; the sandbox skill explains the failure and the fixes.
- Background Assets: the languageChange content request (a build break
for exhaustive switches), localized file reads, and async exclusive
control that must be used on both the app and extension side.
- XCUITest drives VoiceOver and asserts spoken output
(XCUIDevice.shared.voiceOverService).
- TextKit: per-edge block borders, hit-testing transformed text, and
UITextChecker grammar checking.
- Xcode 27 silently ignores -ld_classic and rejects -ld64; Clang module
names must be unique per dependency scan.
- Media router rows for 27 camera controls and iPhone Duo camera direction.