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
1 change: 1 addition & 0 deletions .claude/skills/termlens
67 changes: 67 additions & 0 deletions .github/scripts/check-skill-snippets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# check-skill-snippets.sh — compile every Rust block in skills/termlens/SKILL.md
# against the crate in this checkout.
#
# The skill is what a coding agent copies from, so a snippet that no longer
# compiles teaches the wrong API to everyone who installed it. Every fenced
# block tagged exactly ```rust must be a complete integration test that
# compiles in a consumer package owning a binary called `myapp` — the shape
# the recipes are written for. Blocks tagged ```rust,ignore are fragments and
# are skipped; ```toml, ```sh and ```text are never Rust.
#
# Compiles, does not run: the stub `myapp` here is `fn main() {}`, and the
# recipes' runtime claims are exercised against a real application when the
# skill is written, not on every push.
#
# Usage: check-skill-snippets.sh [SKILL.md]
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
skill="${1:-$root/skills/termlens/SKILL.md}"
[ -f "$skill" ] || { echo "::error::$skill does not exist"; exit 1; }

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
consumer="$tmp/consumer"
mkdir -p "$consumer/src" "$consumer/tests"

cat > "$consumer/Cargo.toml" <<EOF
[package]
name = "skill-consumer"
version = "0.0.0"
edition = "2021"
publish = false

[[bin]]
name = "myapp"
path = "src/main.rs"

[dev-dependencies]
termlens = { path = "$root/crates/termlens" }
insta = "1"

[workspace]
EOF
echo 'fn main() {}' > "$consumer/src/main.rs"

# One file per block, and an index from file to the line in SKILL.md where
# the block opens, so a compile error points back at the prose.
awk -v dir="$consumer/tests" -v index_file="$tmp/index" '
/^```rust$/ { n++; f = sprintf("%s/snippet_%02d.rs", dir, n); printf "snippet_%02d.rs %s:%d\n", n, FILENAME, NR + 1 >> index_file; inblock = 1; next }
/^```/ && inblock { inblock = 0; close(f); next }
inblock { print > f }
END { print n + 0 }
' "$skill" > "$tmp/count"
count="$(cat "$tmp/count")"
if [ "$count" -eq 0 ]; then
echo "::error::no \`\`\`rust blocks found in $skill"
exit 1
fi

if ! (cd "$consumer" && cargo check --quiet --tests); then
echo "::error::a Rust block in $skill no longer compiles against crates/termlens"
echo "snippet files map to the skill as follows:"
cat "$tmp/index"
exit 1
fi
echo "check-skill-snippets: $count Rust blocks in ${skill#"$root"/} compile against crates/termlens"
18 changes: 17 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,26 @@ jobs:

# Single stable job name for branch protection: require this one check and
# matrix/job changes never break the required-checks configuration.
# The skill for coding agents (skills/termlens/SKILL.md) is what an agent
# copies from, so a recipe that no longer compiles teaches the wrong API to
# everyone who installed it. Every Rust block in it is compiled against the
# crate in this checkout, inside a consumer that owns a `myapp` binary.
skill:
name: skill
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- run: .github/scripts/check-skill-snippets.sh

required-green:
name: required-green
if: always()
needs: [fmt, clippy, test, features, msrv, docs, deny, zizmor, gates-listed]
needs: [fmt, clippy, test, features, msrv, docs, deny, zizmor, gates-listed, skill]
runs-on: ubuntu-latest
steps:
- name: Verify every needed job succeeded
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ listed under a **Changed** or **Removed** heading.

## [Unreleased]

### Added

- **A skill for AI coding agents.** `skills/termlens/SKILL.md` teaches an
agent how to test a terminal program with termlens without the mistakes
agents make on their own: no `sleep`, `snapshot_after` for whole-screen
snapshots, the 2x2 geometry floor, `bin!` for hermetic spawns, the two
coordinate orders, `wait_frame` only for applications that emit
synchronized updates (stock ratatui does not), and four copy-paste recipes
— a CLI snapshot, a ratatui navigation flow, overriding defaults, targeted
cell and style assertions. Every Rust block in it is compiled against the
crate in CI, and the README shows the one-line install for Claude Code.

## [0.9.0] - 2026-09-05

### Added
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ cargo insta review # inspect and accept/reject each diff
- `docs/DESIGN.md` — the architecture in four layers, wait semantics, and the
snapshot format spec. **Read this before touching `wait.rs`, `terminal.rs`,
or the emulator.**
- `skills/termlens/` — the skill for AI coding agents that write tests
*with* termlens (`AGENTS.md` briefs agents working *on* it). Every Rust
block in `SKILL.md` is compiled against the crate by
`.github/scripts/check-skill-snippets.sh`; run it after editing the file.
`.claude/skills/termlens` links to it so Claude Code finds it here.
- `.github/` — CI, commit policy enforcement, release automation.

## 3. Testing policy
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ starts from, so it has a name: `termlens::bin!("myapp")` spawns
five-second deadline, and builder calls after the name override any of it —
`termlens::bin!("myapp", size(120, 40), env("NO_COLOR", "1"))?`.

### AI-Assisted Testing (Claude Code / Cursor / Agents)

Coding agents write terminal tests badly in predictable ways: a `sleep`
where a wait belongs, a snapshot taken mid-repaint, a `1x1` terminal, a
`(row, col)` handed to a method that wants `(col, row)`. The skill at
[`skills/termlens/SKILL.md`](skills/termlens/SKILL.md) is the counter to
each: the model, the rules that keep a PTY test from flaking, the API in one
page, and four copy-paste recipes — a CLI snapshot, a ratatui navigation
flow, overriding defaults, targeted cell and style assertions. Every Rust
block in it is compiled against the crate in CI, so it cannot drift from the
API. Install it for Claude Code with one command:

```sh
mkdir -p ~/.claude/skills/termlens && curl -sSL https://raw.githubusercontent.com/vyncint/termlens/main/skills/termlens/SKILL.md -o ~/.claude/skills/termlens/SKILL.md
```

Other agents take the same file: add it to a Cursor rule or reference it
from `.github/copilot-instructions.md`. Inside this repository Claude Code
finds it without installing anything, through `.claude/skills/termlens`.

## What it is (and is not)

- **Not** an expect-style stream matcher — [rexpect] and [expectrl] already
Expand Down
Loading