ReviewOS

also looking at this

craft-native/craft

Brief agents on the environment a fresh checkout cannot infer

#319
Merged glennmichael123 wants to merge fix/agents-environment-briefing into main
4 files +126 -0

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

.github/workflows/ci.ymlmodified+3-0
Changes to .github/workflows/ci.yml
@@ -294,6 +294,9 @@ jobs:
294294 - name: Test dependency-bot job selection
295295 run: bun test scripts/buddy-schedule.test.ts scripts/buddy-update.test.ts
296296
297 - name: Test agent environment briefing
298 run: bun test scripts/agents-guidelines.test.ts
299
297300 - name: Test merged SBOM inventory
298301 run: bun test scripts/merge-sbom.test.ts
299302
AGENTS.mdmodified+41-0
Changes to AGENTS.md
@@ -4,6 +4,43 @@
44
55A lightweight, high-performance cross-platform application framework built with Zig. It creates native desktop apps (macOS, Linux, Windows), mobile apps (iOS, Android), and menubar/system tray apps using web technologies. It includes a library of native UI components, advanced GPU rendering (Vulkan/Metal/Direct3D), WebSocket support, a JavaScript bridge, system integration (notifications, clipboard, file dialogs), and a TypeScript SDK (`craft-native`) for building apps without writing Zig directly. Reproducible binary-size and startup benchmarks live in [`benchmarks/`](./benchmarks).
66
7## Environment
8
9These are the things a fresh checkout cannot tell you, each of which has already cost a separately diagnosed build failure. None is a code defect.
10
11**`zig build` needs the pinned toolchain.** The `zig` on your PATH is almost certainly the wrong snapshot, and the build refuses it by design. Point `CRAFT_ZIG` at the executable `pantry.lock` pins, or activate the pinned Pantry toolchain. `CRAFT_ALLOW_UNPINNED_ZIG=1` exists only for deliberately testing another snapshot.
12
13**`zig build` also needs three sibling checkouts outside the repository.** `packages/zig/build.zig.zon` resolves them by relative path — `../../../../Libraries/zig-js` and likewise `zig-regex` and `zig-gc` — so they must sit four levels above `packages/zig`, beside the repository's own parent. CI provides them through `.github/actions/first-party-zig-deps`, which clones each at the SHA in `pins.env`.
14
15Two traps follow from that. The pins are **not branch tips**, so `git fetch` and `git pull` will not reach them; fetch the commit directly (`git fetch --depth 1 origin <sha>`), which is what the action does and why it says so. And a checkout that merely *exists* is not enough: a local clone at a different revision fails to compile against the pinned Zig with errors that look like Zig bugs.
16
17**A container that installs Zig still cannot build** until both of the above are satisfied. If you cannot satisfy them, say so and let CI's `zig-core` job be the signal rather than fighting it.
18
19**`bun run test` is the Zig suite, not the SDK suite.** The SDK suite is `bun run test:sdk`. A bare `bun test` at the repository root is worse than either: `bunfig.toml` picks up vendored fixtures under `pantry/`, so it reports failures that have nothing to do with your change. Inside a package, plain `bun test` is correct.
20
21**Not every Zig test runs on every runner.** Zig collects tests only from files its lazy analysis reaches, and `src/ios.zig` is skipped off Darwin. The whole of `bridge_error.zig` and every `bridge_mobile_*.zig` module is therefore invisible to the Linux leg of `zig-core`. For iOS bridge work, read the **macos-latest** leg; a green Linux leg means almost nothing.
22
23**A new `scripts/*.test.ts` file does not run until `ci.yml` names it.** The lint job invokes those tests file by file, so a test added to `scripts/` and nowhere else is silently never executed. Add the invocation in the same change.
24
25**`main` is not branch-protected.** There are no required status checks, so a red pull request *can* be merged. Never merge one.
26
27## Verification
28
29Before pushing, run what your machine can actually run:
30
31```sh
32bun run test:sdk # the SDK suite
33bun run typecheck
34bun run verify:packages # needs the package dists built first
35bunx --bun pickier . # never eslint; 0 errors is the bar
36```
37
38`verify:packages` fails on a fresh checkout until the public packages are built, which is why CI runs it after the builds rather than before.
39
40A pull request touching `packages/ios/**`, `packages/android/**` or the mobile Zig bridges also boots a real simulator or emulator on a long job. When that job is red for a reason unrelated to your change, say so on the pull request instead of thrashing on it.
41
42`mobile-e2e.yml` installs with `--frozen-lockfile` while `ci.yml` does not, so a dependency edit without a regenerated `bun.lock` passes one workflow and fails the other.
43
744## Linting
845
946- Use **pickier** for linting — never use eslint directly
@@ -21,6 +58,7 @@ A lightweight, high-performance cross-platform application framework built with
2158- **buddy-bot** handles dependency updates — not renovatebot
2259- **better-dx** provides shared dev tooling as peer dependencies — do not install its peers (e.g., `typescript`, `pickier`, `bun-plugin-dtsx`) separately if `better-dx` is already in `package.json`
2360- If `better-dx` is in `package.json`, ensure `bunfig.toml` includes `linker = "hoisted"`
61- Do not run `bun audit fix` unreviewed. It is willing to *downgrade* a package to fall below an advisory window rather than upgrade past it, and it reports that as a fix.
2462
2563## Distribution
2664
@@ -28,7 +66,10 @@ A lightweight, high-performance cross-platform application framework built with
2866- The `release.yml` workflow publishes to pantry on every tag via `home-lang/pantry/packages/action@main` with `publish: 'zig'`.
2967- Tests / monorepo dev loop: set `CRAFT_BIN=/absolute/path/to/craft` to override the PATH lookup. The SDK's `AppConfig.craftPath` field accepts the same kind of override at app-config time.
3068- Adding new lookup paths or fallbacks to `binary-resolver.ts` defeats the pantry contract — don't.
69- Release archives are built natively, one runner per platform. They are not cross-compiled, so a full release cannot be rehearsed on one machine.
3170
3271## Commits
3372
3473- Use conventional commit messages (e.g., `fix:`, `feat:`, `chore:`)
74- Keep commits small and focused: 3–5 files each
75- Do not add agent attribution trailers to commits or pull requests
scripts/agents-guidelines.test.tsadded+37-0
Changes to scripts/agents-guidelines.test.ts
@@ -0,0 +1,37 @@
1import { expect, test } from 'bun:test'
2import { readFileSync } from 'node:fs'
3import { join } from 'node:path'
4import { checkAgentsGuidelines } from './agents-guidelines'
5
6const root = join(import.meta.dir, '..')
7const read = (name: string): string => readFileSync(join(root, name), 'utf8')
8
9const rules = 'Use pickier, never eslint. buddy-bot handles updates. Use conventional commit messages.'
10const briefed = [
11 'Set CRAFT_ZIG to the executable pantry.lock pins.',
12 'Zig needs the sibling Libraries/zig-js checkout at the SHA in pins.env.',
13 'The SDK suite is bun run test:sdk.',
14].join('\n')
15
16test('the repository AGENTS.md briefs the environment and keeps the house rules', () => {
17 expect(() => checkAgentsGuidelines(read('AGENTS.md'), read('CLAUDE.md'))).not.toThrow()
18})
19
20test('rejects an AGENTS.md that only repeats CLAUDE.md', () => {
21 expect(() => checkAgentsGuidelines(`# Codex Guidelines\n\n${rules}`, `# Claude Code Guidelines\n\n${rules}`))
22 .toThrow('repeats CLAUDE.md')
23})
24
25test('names the briefing an AGENTS.md leaves out', () => {
26 expect(() => checkAgentsGuidelines(`# Codex\n\n${rules}\n${briefed.replace('pantry.lock', 'the lockfile')}`, `# Claude\n\n${rules}`))
27 .toThrow('pinned Zig toolchain')
28 expect(() => checkAgentsGuidelines(`# Codex\n\n${rules}\n${briefed.replace('pins.env', 'a pin file')}`, `# Claude\n\n${rules}`))
29 .toThrow('sibling first-party checkouts')
30 expect(() => checkAgentsGuidelines(`# Codex\n\n${rules}\n${briefed.replace('bun run test:sdk', 'bun test')}`, `# Claude\n\n${rules}`))
31 .toThrow('which suite each test script runs')
32})
33
34test('refuses to let a rewrite drop a shared house rule', () => {
35 expect(() => checkAgentsGuidelines(`# Codex\n\n${rules.replace('buddy-bot', 'the bot')}\n${briefed}`, `# Claude\n\n${rules}`))
36 .toThrow('buddy-bot')
37})
scripts/agents-guidelines.tsadded+45-0
Changes to scripts/agents-guidelines.ts
@@ -0,0 +1,45 @@
1/**
2 * Keeps `AGENTS.md` a real briefing rather than a copy of `CLAUDE.md`.
3 *
4 * `AGENTS.md` is the only thing a coding agent working from a fresh checkout
5 * reads before it runs a command. It was byte-identical to `CLAUDE.md` below
6 * its title, so it carried the house rules and none of the environment facts
7 * that a machine without this repository's history cannot discover.
8 *
9 * Every topic below cost a real, separately diagnosed build failure here, in
10 * this order: an unpinned `zig` on PATH, the absent sibling checkouts, and a
11 * `bun test` that ran the wrong suite. None of them are code defects and none
12 * are visible from the tree, which is exactly why they belong in writing.
13 */
14
15/** Facts an agent cannot derive from the tree, each with the words that prove it was written down. */
16const briefings: { topic: string, needles: string[] }[] = [
17 { topic: 'the pinned Zig toolchain', needles: ['CRAFT_ZIG', 'pantry.lock'] },
18 { topic: 'the sibling first-party checkouts', needles: ['Libraries/zig-js', 'pins.env'] },
19 { topic: 'which suite each test script runs', needles: ['bun run test:sdk'] },
20]
21
22/** House rules that must survive any rewrite, since the copy is where they came from. */
23const houseRules = ['pickier', 'buddy-bot', 'conventional commit']
24
25/** The text below the first heading, which is the only line the two files are meant to differ on. */
26function body(text: string): string {
27 return text.split('\n').slice(1).join('\n').trim()
28}
29
30export function checkAgentsGuidelines(agents: string, claude: string): void {
31 if (body(agents) === body(claude))
32 throw new Error('AGENTS.md repeats CLAUDE.md below its title, so it briefs none of the environment a fresh checkout needs')
33
34 for (const { topic, needles } of briefings) {
35 const missing = needles.filter(needle => !agents.includes(needle))
36 if (missing.length)
37 throw new Error(`AGENTS.md does not brief ${topic}; expected to find ${missing.join(' and ')}`)
38 }
39
40 const lowered = agents.toLowerCase()
41 for (const rule of houseRules) {
42 if (!lowered.includes(rule))
43 throw new Error(`AGENTS.md dropped the shared house rule: ${rule}`)
44 }
45}