Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions .claude/skills/check/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: check
description: Verify the repo is green by running scripts/check.sh — the same full health check CI runs (swift test + coverage gate, sanitizers, xcodegen drift, app build, swift-format/swiftlint/periphery/prettier/markdownlint/shellcheck/shfmt, site deployability, ruff + pytest over evals/). Use before claiming a change builds, passes, or is ready to commit/PR. Bakes in the macOS-only guard so a Linux/web sandbox flags "verify on a Mac" instead of fabricating a green result; there, scripts/check.sh --portable runs the platform-independent subset (docs/site/scripts/workflows).
description: Verify the repo is green by running scripts/check.sh — the same full health check CI runs (swift test + coverage gate, sanitizers, xcodegen drift, app build, swift-format/swiftlint/periphery/prettier/markdownlint/shellcheck/shfmt, site deployability, settled-decision invariants, ruff + pytest over evals/). Its read-only checks report together, so a red run names every failure at the bottom rather than stopping at the first. Use before claiming a change builds, passes, or is ready to commit/PR. Bakes in the macOS-only guard so a Linux/web sandbox flags "verify on a Mac" instead of fabricating a green result; there, scripts/check.sh --portable runs the platform-independent subset (docs/site/scripts/workflows).
---

# check — is this green?
Expand All @@ -27,19 +27,20 @@ What you CAN run there is the portable subset:
scripts/check.sh --portable
```

It runs the repo-integrity guards (dependencies, sound catalog, site, shell
portability) then
actionlint / zizmor / prettier / xmllint / markdownlint / shellcheck / shfmt /
ruff (lint + format check) / pytest over `evals/` / `release.test.sh` (plus `swift-format lint` and `swiftlint lint` if Linux
builds are on `PATH` — under the default web network policy they are not).
That fully verifies docs, site, scripts, eval, and workflow changes. It is **not**
"green" in the CI sense: the entire Swift side is skipped, and the closing
line says so. For Swift changes, push and watch `check.yml` instead — its
`compile` job reports a broken test build in ~2 minutes, and `format-patch`
It runs the repo-integrity guards — dependencies, sound catalog, site, shell
portability, and settled decisions — then actionlint, zizmor, prettier, xmllint,
markdownlint, shellcheck, shfmt, ruff (lint + format check), pytest over
`evals/`, and `release.test.sh`. `swift-format lint` and `swiftlint lint` join
them when Linux builds are on `PATH`; under the default web network policy they
are not.

That fully verifies docs, site, scripts, eval, and workflow changes. It is
**not** "green" in the CI sense: the entire Swift side is skipped, and the
closing line says so. For Swift changes, push and watch `check.yml` instead —
its `compile` job reports a broken test build in ~2 minutes, and `format-patch`
publishes the exact `swift-format` reflow as an artifact so you don't have to
reproduce it by hand. In
Claude Code on the web, the `SessionStart` hook installs the portable
linters automatically.
reproduce it by hand. In Claude Code on the web, the `SessionStart` hook
installs the portable linters automatically.

Quick preflight:

Expand All @@ -62,6 +63,11 @@ Everything source-only runs first, then everything that builds. That ordering is
deliberate: reversed, a compile error means the cheap checks are
never reached and their findings arrive on the next 11-minute run instead.

The read-only checks (steps 1–5, plus 12) also don't stop each other: each one
runs, failures are tallied, and the run ends with a single `error: N check(s)
failed:` list naming all of them. So one pending `swift-format` reflow no longer
hides every lint finding behind it — expect to fix a batch, not a queue.

1. repo-integrity guards: no external SPM dependencies; sound-catalog
integrity (every `SoundPackCatalog` voice has both cue files, no orphans, no
duplicate or reserved ids); and site integrity (`scripts/check-site.sh` —
Expand All @@ -70,7 +76,15 @@ never reached and their findings arrive on the next 11-minute run instead.
no unreferenced assets). All run in `--portable` too
2. shell portability (`scripts/check-portability.sh`): GNU-only idioms in
`scripts/*.sh` and `.claude/hooks/*.sh`, which run on BSD userland (Mac, CI)
as well as GNU (Linux sandbox). `--portable` too
as well as GNU (Linux sandbox). Then settled decisions
(`scripts/check-invariants.sh`): the grep-decidable subset of AGENTS.md's
[Settled decisions](../../../AGENTS.md#settled-decisions--dont-reintroduce-these)
table — `AVAudioEngine` capture, a streaming or on-device path, a client-side
cleanup pass, `LSUIElement`, a keystroke-typing injector, the production
Keychain in tests. Each rule is pinned to its table row and its
`project-guardrails` bullet, so editing the prose without revisiting the rule
fails here rather than leaving a gate that enforces a reversed decision. Both
`--portable` too
3. `swift-format lint --strict`, then `swiftlint lint --strict` (warnings are
failures) — both source-only
4. actionlint / zizmor (workflow security) / prettier / xmllint / markdownlint /
Expand Down Expand Up @@ -102,6 +116,9 @@ never reached and their findings arrive on the next 11-minute run instead.
- **Any non-zero exit** → not green. Report the failing step and its output
verbatim; do not soften ("mostly passes") or claim success. Fix, then re-run
the _full_ `check.sh` — a single-file `swift test --filter` is not green.
A red run reports every independent failure it found, so read the closing
`error: N check(s) failed:` list and fix all of them before re-running —
stopping at the first one wastes the aggregation.
- A `note: <tool> not installed; skipping` line means coverage of that check is
_missing_, not satisfied. On a dev Mac, run `scripts/bootstrap.sh` to install
the toolchain rather than accepting skips.
Expand Down
12 changes: 12 additions & 0 deletions .claude/skills/project-guardrails/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ These are settled decisions. Don't reintroduce them; if a task seems to require
one, stop and ask the user first. This is the fast "don't" list; AGENTS.md's
"Settled decisions" table is the fuller reference and the source of truth.

Many of these are also enforced mechanically — `scripts/check-invariants.sh`
(run by `check.sh`, including `--portable`) greps for the constructs that give
each one away, so reintroducing one fails the health check rather than depending
on this list being read. Those rules also pin a verbatim slice of the bullet
they come from in this file, so rewording or deleting one fails the gate until
someone decides whether the rule survives the edit — edit these entries
knowing that, and fix the anchor in the same change. Treat that as a backstop, not the boundary: the rules
it can't express are still here, still binding, and the reasons in this file are
what let you tell an intended exception from a mistake. Never silence a finding
with `// invariant-ok:` to get a build green — that marker is for a line that is
genuinely correct, and reaching for it means it's time to stop and ask.

## Audio

- **No `AVAudioEngine` / `installTap` capture path.** `MicCapture` uses
Expand Down
22 changes: 20 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ App/Blurt/
Shared/ UITestIdentifiers.swift — compiled into BOTH app and UI-test targets
BlurtUITests/ XCUITest bundle (see Tests)
Tests/BlurtEngineTests/ Swift Testing suites; Stubs/ holds the seam doubles
scripts/ check.sh, check-site.sh, bootstrap.sh, dev-build.sh, uitest.sh, leaks.sh,
release*.sh
scripts/ check.sh, check-site.sh, check-portability.sh, check-invariants.sh,
bootstrap.sh, dev-build.sh, uitest.sh, leaks.sh, release*.sh
hand-run maintainer tools — no automated caller, invoked by a
human, so "nothing references it" here does NOT mean dead code:
serve-site.sh (preview site/ locally), screenshot.swift +
Expand Down Expand Up @@ -327,6 +327,24 @@ In Claude Code on the web, a `SessionStart` hook installs the portable linters a
Each was tried the other way and reverted. If a task seems to require one, stop and ask first.
(`.claude/skills/project-guardrails` is the compressed version of this list.)

`scripts/check-invariants.sh` mechanizes the subset of this table a regex can decide, so those
entries fail `check.sh` rather than relying on a reviewer's memory: `AVAudioEngine`/`installTap`
capture, a streaming or on-device path, a client-side cleanup pass (`StylerProtocol`, an LLM
Gateway/LeMUR client), `config.language_code`, `config.prompt`, a keystroke-typing injector,
`LSUIElement`, a `KeyboardShortcuts` import, a self-replacing updater, the production Keychain
under `Tests/`, and `@available(*, deprecated)` shims. The SPM-dependency row has its own guard in
`check.sh` (it parses `project.yml`'s `packages:` block), and the `.pbxproj` row is covered by the
xcodegen drift check plus a Claude `PreToolUse` hook. The rest — the filler-word clause, the
context-widening rule, the `/Applications` install path — stay prose, because no pattern separates
them from correct code that reads the same. Add a rule when you find one that can.

**Editing a mechanized row breaks the build on purpose.** Each of those rules pins a verbatim slice
of its row here (and of its bullet in the guardrails skill), so deleting or rewording one fails
`check-invariants.sh --self-test`. That is the intended behaviour, not an obstacle to route around:
a rule that outlives its row keeps firing and keeps citing this table while enforcing something the
project has already reversed. Reworded the row? Update the anchor. Reversed the decision? Delete the
rule along with the row.

| Don't | Because |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Add an external SPM dependency to the engine | Dependency-free by rule (biggest supply-chain risk); a `check.sh` guard fails on `.package(` in `Package.swift` or a `url:`/`github:` package in `project.yml`. Extend `BlurtEngine` instead. |
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ globs go in one invocation so `hook-lib.sh` and `release-lib.sh` are in the inpu
portable subset. Load it before claiming a change builds or passes.
- **`project-guardrails`** — the compressed "don't do this" list. `AGENTS.md`'s
[Settled decisions](./AGENTS.md#settled-decisions--dont-reintroduce-these) table is the fuller
reference and the source of truth; keep the two in agreement when you change either.
reference and the source of truth; keep the two in agreement when you change either. For the
twelve decisions `scripts/check-invariants.sh` mechanizes, that agreement is no longer on your
honour: each rule pins a verbatim slice of its table row _and_ its skill bullet, so deleting or
rewording either fails the gate's `--self-test` until someone decides whether the rule survives
the edit. The rest of the list is still yours to keep in sync.
- **`release`** — the ship pipeline (build → sign → notarize → staple → DMG → GitHub). User-invoked
only: it has real, hard-to-undo side effects.
- **`launch-metrics`** — read-only launch KPI snapshot from the GitHub API.
Expand Down
9 changes: 1 addition & 8 deletions Sources/BlurtEngine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,4 @@ Two ways out, neither taken yet: drop it from the public product (its own target

## Invariants — don't break these

Each of these was tried the other way and reverted; the longer stories are in [AGENTS.md](../../AGENTS.md) and the source comments:

- **No external SPM dependencies in the engine.** Foundation/Security/AVFoundation only.
- **No streaming STT, no local models, no client-side LLM cleanup pass.** One dictation request per utterance is the architecture; the cleanup rewrite is server-side (the request's `llm` block), and transcription steering belongs in `ConversationContext`.
- **No `AVAudioEngine`/`installTap` capture path.** Fresh `AVAudioRecorder` per session, resolved at record time.
- **Paste is always clipboard-based** (save → write → ⌘V → settle → restore), with the copied-to-clipboard degradation for lost targets.
- **No English-pinning or filler-word clauses**, no reviving `config.prompt`, and no widening the conversation context past the recent dictations and the prior chunk — the app name, window title, field label and selected text stay on the machine. Key terms go on `config.word_boost`, never back into the context turns.
- **Actors own state** (`DictationSession`, `KeyInjector`, `MicCapture`); the stateless API client stays a `Sendable` struct. Keep new code Swift 6 strict-concurrency clean.
Each was tried the other way and reverted, and they bind engine code as much as the app's. The list is deliberately not repeated here: it lives once, in [AGENTS.md's Settled decisions](../../AGENTS.md#settled-decisions--dont-reintroduce-these) table, alongside the engine conventions those rules rest on (dependency-free by rule; actors own state; the stateless API client stays a `Sendable` struct; new code Swift 6 strict-concurrency clean). `scripts/check-invariants.sh` mechanizes the subset a regex can decide and fails `check.sh` on them, so a good number are enforced rather than remembered — and it pins each rule to the table row it came from, which only works while there is one row to pin to.
Loading
Loading