reasoned. A file recording measured evidence is itself evidence that decays —
put a dated-not-invariant note beside every number, because improving the suite
silently invalidates the figures.
<!-- SKILL-INDEX-START -->
[snappy-watchtower Index]|root: ~/.claude/skills/snappy-watchtower|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
# snappy-watchtower (agent loader)
Standing error monitors, and the probes that lie. Read `SKILL.md` for the full
reference; this is the short form a lane needs in context.
## API module
```typescript
import { watchtowerMonitors, tscWatchCommand, controlledAbsenceProbe,
servingHeadProof, oneTestFile, isStaleWorktreeCopy } from "../snappy-watchtower/api.ts";
```
Or CLI (contract only -- this hand is a library, `verbs: {}`):
```bash
npx tsx ~/.claude/skills/snappy-watchtower/api.ts contract
```
## API functions
| Function | Purpose |
|----------|---------|
| `watchtowerMonitors(trees?)` | The monitor specs to arm at session start, one per tree |
| `tscWatchCommand(tree)` | The `tsc --watch` command for one tree, crash signatures included in the filter |
| `controlledAbsenceProbe({...})` | An absence claim wrapped in its positive control (rule 1) |
| `servingHeadProof(commit, port?)` | The command that proves an install by the serving head, not by an exit code (rule 5) |
| `oneTestFile({...})` | Run one test file without the full-suite worker swarm |
| `isStaleWorktreeCopy(testPath)` | True when a path points at a stale runtime copy beside the live one |
## Arm first, build second
Two persistent `tsc --watch` monitors, one per tree (app tree + Convex twin).
Grep alternation must include the crash signatures, not just `error TS` — a
filter that only matches the happy path is silent through a crashloop, and
silence is indistinguishable from health.
## The six rules that catch the most
1. **Every absence claim carries a positive control.** Grep for something you
know is there first. If that comes back empty, the probe is broken and its
"absence" proves nothing.
2. **Read the LIST of failing test names, never a count.** `node --test`
double-prints `✖` and its `failing tests:` header matches `^✖ `. Vitest's
default reporter prints failures only — use `--reporter=verbose`.
3. **A guarantee with N properties needs N breaks.** A green is a control only
when it is a property that SHOULD hold under that specific damage. The
control count per break is zero or one — never the green count.
4. **Assert the absence of the row, the request, the effect** — not the presence
of a word.
5. **Verify installs by the serving head**, never the runner's exit code.
6. **zsh does not word-split unquoted variables**, and backticks inside a
double-quoted string are command substitution.
7. **QUOTE `--include` globs.** `grep --include=*.ts` dies with zsh's
"no matches found" before grep ever runs — the shell expands it. Write
`--include='*.ts'`. An unquoted one looks exactly like a clean absence.
8. **`${PIPESTATUS[0]}` is a bashism; zsh uses `$pipestatus[1]`.** In zsh it
expands EMPTY, so `echo "EXIT=${PIPESTATUS[0]}"` after a pipe prints nothing
and a failed command reads as unreported rather than failed.
## Mark which grade every claim is
measured-and-dated > derived-from-a-measurement > measured-then-recalled >
reasoned. A file recording measured evidence is itself evidence that decays —
put a dated-not-invariant note beside every number, because improving the suite
silently invalidates the figures.
<!-- SKILL-INDEX-START -->
[snappy-watchtower Index]|root: ~/.claude/skills/snappy-watchtower|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
## Used by
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
This skill declares no executable verbs. Its `instruction-only` label is intentional.
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
SKILL.md
snappy-watchtower — standing error monitors, and the probes that lie#
Every entry below is a real failure with a date. Nothing here is a
hypothetical, and nothing here is advice — each line is a trap that cost real
Skills whose description overlaps this one enough that a reader could pick the
wrong door. Each row is that skill's own first sentence about itself, so the
choice is made on its words, not on a summary written here.
Skill
Reach for it instead when
snappy-agent-host
Run the REAL Claude Code, Codex, and Gemini CLIs through ACP via the skills MCP, with durable…
snappy-artifact-loop
Build published Artifacts as I/O devices where the AGENT is the backend, not as static output…
snappy-ax
Drive any Mac app through the Accessibility tree (AXUIElement) the way the shipping tools act…
snappy-blog
Interview-driven blog post generation for the Snappy website (snappy.ai/blog).
snappy-content
Interview-driven content production methodology, the writing engine for every Snappy channel…
snappy-docs
THE DEFAULT for writing to Notion -- the Snappy stack's Notion primitive over the REST API (a…
snappy-dom-cartographer
Master DOM mapping agent for the Snappy swarm.
snappy-faces
Draw Snappy work objects as their channel-faithful UI faces.
snappy-jcode
Dispatch GPT 5.6 (Luna/Sol) agents as sandboxed lane workers via the local jcode CLI, on this…
snappy-os-operator
Operate SnappyOS like a pro through product doors only: governed connector reads, staged writ…
snappy-review-pages
Local HTML review pages with provenance and a copy-back feedback loop.
snappy-session-close
Close a working session in two verbs: RECONCILE the agent-facing docs of a repo set (CLAUDE.m…
snappy-telegram
Telegram Bot API channel for Snappy: direct calls to api.telegram.org (no Xano middleware) to…
snappy-voice-control
Voice control on macOS, extracted from two shipping open-source agents (fazm by mediar-ai; Ag…
snappy-xano-mcp
THE EXISTING, DEPLOYED Snappy MCP server: the Cloudflare Worker exposing the Snappy Xano API…
---
name: snappy-watchtower
instruction-only: true
description: "Standing error monitors and the probes that lie: arm live typecheck, build, and test monitors at session start, plus the dated list of git, Vitest, install, deploy, twin-tree, and probe traps where a green result meant nothing, each with the exact command that avoids it. Use when Robert says: /snappy-watchtower, \"set up the monitor with the feedback from errors\", \"the monitor is helping, keep the live loop going\", \"200 doesn't mean an agent system is working\", \"is it working\", \"is it actually working now\", \"it says green but it is broken\", \"keep the building/typechecking over there\", \"explain your monitor to the other team\". Triggers on: watchtower, monitor, typecheck watch, green but broken, probe trap, standing monitor. NOT the overnight orchestration itself (see snappy-nightshift). NOT app-level dogfooding (see snappy-resident)."
---
# snappy-watchtower — standing error monitors, and the probes that lie
Every entry below is a real failure with a date. Nothing here is a
hypothetical, and nothing here is advice — each line is a trap that cost real
time, plus the exact command that avoids it.
## 1. Arm the monitors at session start
A typecheck you run once is a photograph. A typecheck that watches is a smoke
alarm. Arm these FIRST, before any building — a silent exit means re-arm.
```
Monitor({
command: "cd <TREE>/apps/snappy-os/web && npx tsc --noEmit --watch --preserveWatchOutput 2>&1 | grep -E --line-buffered 'error TS|Found [1-9][0-9]* error|command not found|ENOENT'",
description: "web tsc errors, live (<which tree>)",
persistent: true, timeout_ms: 3600000,
})
```
Arm it once per tree. This repo has TWO (the app tree and the Convex twin), and
an error in the untracked one is invisible until a deploy fails.
**Why the grep alternation is wide.** `grep 'error TS'` alone is silent when
`tsc` itself dies — and silence is indistinguishable from "no errors". The
`command not found|ENOENT` arms exist so a monitor that stops working SAYS SO.
Ask before arming any monitor: *if this process crashed right now, would my
filter emit anything?* If not, widen it.
**`--preserveWatchOutput` is load-bearing.** Without it `tsc --watch` clears the
screen each cycle with an ANSI escape, and the escape can eat the error line
before grep sees it.
## 2. Probes lie in BOTH directions — always carry a positive control
The rule: **grep for something you know IS there first.** If that comes back
empty, your probe is broken and its "absence" proves nothing.
```bash
grep -c "<thing you know exists>" "$FILE" # must be > 0, or the probe is lying
grep -c "<thing that must be gone>" "$FILE" # only now is 0 meaningful
```
Measured 2026-08-23: a bundle grep returned "clean" for a string that was
present, because zsh had mangled the pattern variable. The positive control is
what caught it. Applies to every absence claim — deployed bundles, installed
runtimes, config files.
## 3. Read the LIST of failing tests, never the count
Four wrong numbers in one session, 2026-08-30, all from counting:
- `node --test` prints `✖` **twice** per failure, so `grep -c '^✖ '` doubles it.
- It also prints a literal `failing tests:` header that **matches `^✖ `**.
- Its `✔` line prints once, so green counts are trustworthy and red counts are
not — and you cannot tell which without working out why they disagree.
- **Vitest's default reporter prints failing names only.** There is no green
list to read, which pushes you toward *deriving* "how many passed" by
subtraction. Use `--reporter=verbose` to get both.
The honest unit is never "N passed". It is: **which single test had something
to lose, and did it lose it.**
## 4. Break-proof discipline
- **A guarantee with N properties needs N breaks.** One break proves one thing.
- **A green counts as a control ONLY when it is a property that should hold
under that specific damage.** Four greens and zero controls is the shape of a
receipt that looks stronger than it is.
- **The control count per break is zero or one — never the green count.**
Measured across two independent suites: green counts ranged 4–7, control
counts were 0 or 1 every time.
- **Controls are free for a classifier, constructed for a gate.** A classifier
routes between two paths, so each direction is the other's control. A gate
stands on one shared path — only the *permissive* break isolates anything;
the restrictive break is a sledgehammer that reds its own would-be control.
- **A guard that cannot fire and a test that cannot fail are the same defect.**
Break it and watch it go red, or you have not tested it.
- **Assert the ABSENCE** — of the row, the request, the effect — not the
presence of a word.
### Four grades of claim, descending reliability
1. measured **and dated**
2. derived from a measurement (the arithmetic is right; it cannot tell you
*which*)
3. measured once, then **recalled** — has the provenance of a measurement and
the reliability of a memory
4. reasoned
A file must make clear which grade each claim is, or grade 4 reads like grade 1
forever. **Any file recording measured evidence is itself evidence that
decays** — put a dated-not-invariant warning beside every number, because
improving the suite silently invalidates the figures.
## 5. Shell traps (zsh)
- **`git commit --only` REFUSES a path git does not track yet** — "pathspec did
not match any file(s) known to git", exit 1, nothing committed. A new file
needs `git add <path>` (or `git add -N`) FIRST, then `--only` works normally.
Costs one failed commit every time a landing includes a new module.
- **`timeout` does not exist on macOS.** `timeout 20 npx tsc …` dies with
`command not found` and the exit code belongs to the shell, not your command.
Use `curl -m`, a Monitor, or background-plus-kill instead.
- **zsh does NOT word-split unquoted variables.** `git commit --only $PATHS`
passes ONE argument containing spaces. Spell paths out or quote each.
- **Backticks inside a double-quoted string are command substitution.** A
commit message with `` `conversation` `` in it makes zsh try to *run*
`conversation`. Use a heredoc or single quotes for prose containing backticks.
- **`set -euo pipefail` + `curl -m`**: a timeout is a non-zero exit that kills
the script. `out="$(curl -sN -m 6 "$url" || true)"` absorbs it.
- **`grep -q -m1` inside a pipeline under `pipefail`** fails the whole pipeline
when grep exits early and the upstream gets SIGPIPE. Read the whole window
into a variable first, then grep the variable.
## 6. Test-runner traps
- **The wrong `--root` reports `ReferenceError: document is not defined`.** That
looks exactly like a code failure and is a config miss — the jsdom environment
from the project's vitest config was never loaded. Run from the project root,
or pass an absolute `--root`.
- **Vitest globs into `.build/` worktrees.** Stale install-worktrees and
head-check worktrees hold COPIES of every test file, so one file can run three
times and the two stale copies assert the old truth. If a test you just fixed
still fails, check whether the failing path starts with `.build/`.
- **Never run the full vitest suite on this machine** — orphaned workers swamp
it. Run one file, or `node --test --import tsx <file>`.
- **`npx tsx -e "…"` cannot use top-level `await`** — esbuild emits CJS and
fails with *"Top-level await is currently not supported with the cjs output
format"*. Dynamic `import()` of a project module therefore dies in a one-liner.
Write the probe to a scratchpad `.ts` file and wrap the body in an
`async function main(){…} void main();`, or the whole probe is unrunnable.
- **A mutation test is the only proof a test can fail.** Break the guard, run,
read the failing NAMES, restore, then `diff -q` against a pre-mutation copy to
prove the restore was byte-exact. Restoring by hand-editing is how a mutation
ships.
## 7. Install and deploy truth
- **Verify by the SERVING HEAD, never the runner's exit code.** An installer
can exit 0 having laid down nothing. `curl /deploy-truth` and read
`serving_head`; confirm with `git merge-base --is-ancestor <commit> <head>`.
- **Four install lanes, by what changed:** `state/**` only →
`install-state-fast.sh` (~40s); web only → `install-web-fast.sh` (~60s);
runtime reseal → `install-runtime-fast.sh`; anything native → full
`npm run app:install`.
- **The mixed-artifact guard is a feature.** `install-state-fast.sh` verifies
the destination matches the source and REFUSES if a file changed mid-sync.
Seen 2026-08-30 when a parallel lane edited a state file. Re-run in a quiet
window; never force past it.
- **A stream probe with a byte bound has a cliff.** Reading the first 4000 bytes
of an SSE endpoint missed a `healthz` event at byte 3633 behind a large frame.
Bound the probe by the CLOCK (`curl -m 6`), not by bytes.
## 8. Twin-tree Convex protocol
- Edit in BOTH trees. Mirror with **absolute paths on both sides** — a compound
`cd` has silently mirrored into the wrong tree. Prove with `diff -q`.
- Deploy ONLY `npx convex dev --once` from the paired repo's
`apps/snappy-os/web`. **Never `npx convex deploy`** — a stray deploy from the
app tree forks the schema.
## Files
- `AGENTS.md` — the loader a subagent reads.
- `api.ts` — the monitor commands and the verify helpers, typed.
## 9. Traps found on 2026-08-30, after this file was first written
- **`echo "EXIT=$?"` after a pipe reports the LAST stage's status**, not the
command you care about. `git commit ... | tail -4; echo "EXIT=$?"` printed 0
over a failed commit. Put the `echo` immediately after the bare command, or
check `${PIPESTATUS[0]}`.
- **A stale zero-byte `.git/index.lock`** left by an interrupted parallel lane
makes every later commit fail with a message about an editor. Before removing
it, prove no git process is live (`pgrep -f "git (commit|add|merge|rebase)"`)
— deleting the lock under a running git corrupts the index.
- **A one-shot `tsc --noEmit` while two `tsc --watch` monitors run takes
minutes.** That is the monitors doing their job. Let the monitor answer
instead of racing it; run targeted tests for the specific change.
- **`grep` is aliased to `ugrep` on this machine.** Pattern semantics differ
from GNU grep in places. Another reason every absence claim needs its
positive control.
- **A minified bundle does not preserve your source's property order.** Probing
for `'"Settings","title":"Settings"'` returned 0 while the code was present;
`aria-label":"Settings"` found it. The positive control is what exposed the
false negative — exactly the mechanism §2 exists for.
- **Find the entry bundle from `index.html`, never by globbing `assets/`.**
Several `index-*.js` files coexist after repeated installs; grepping "the
bundle" can read a stale one and answer about code nobody serves.
- **The 900-line cap blocks GROWTH, and it is right to.** When it refuses a
defensive line, ask whether the line was needed at all. It refused
`text-decoration: none` on a chip whose only child is an `aria-hidden` SVG —
there was no text node to underline.
- **`install-web-fast.sh` warns about uncommitted `state/` files and continues.**
Read the warning: it is telling you the web swap happened while daemon code
in the tree is NOT in the installed runtime. Web-only changes are fine; a
`state/` change needs its own lane.
## Near neighbours
Skills whose description overlaps this one enough that a reader could pick the
wrong door. Each row is that skill's own first sentence about itself, so the
choice is made on its words, not on a summary written here.
| Skill | Reach for it instead when |
|---|---|
| `snappy-agent-host` | Run the REAL Claude Code, Codex, and Gemini CLIs through ACP via the skills MCP, with durable… |
| `snappy-artifact-loop` | Build published Artifacts as I/O devices where the AGENT is the backend, not as static output… |
| `snappy-ax` | Drive any Mac app through the Accessibility tree (AXUIElement) the way the shipping tools act… |
| `snappy-blog` | Interview-driven blog post generation for the Snappy website (snappy.ai/blog). |
| `snappy-content` | Interview-driven content production methodology, the writing engine for every Snappy channel… |
| `snappy-docs` | THE DEFAULT for writing to Notion -- the Snappy stack's Notion primitive over the REST API (a… |
| `snappy-dom-cartographer` | Master DOM mapping agent for the Snappy swarm. |
| `snappy-faces` | Draw Snappy work objects as their channel-faithful UI faces. |
| `snappy-jcode` | Dispatch GPT 5.6 (Luna/Sol) agents as sandboxed lane workers via the local jcode CLI, on this… |
| `snappy-os-operator` | Operate SnappyOS like a pro through product doors only: governed connector reads, staged writ… |
| `snappy-review-pages` | Local HTML review pages with provenance and a copy-back feedback loop. |
| `snappy-session-close` | Close a working session in two verbs: RECONCILE the agent-facing docs of a repo set (CLAUDE.m… |
| `snappy-telegram` | Telegram Bot API channel for Snappy: direct calls to api.telegram.org (no Xano middleware) to… |
| `snappy-voice-control` | Voice control on macOS, extracted from two shipping open-source agents (fazm by mediar-ai; Ag… |
| `snappy-xano-mcp` | THE EXISTING, DEPLOYED Snappy MCP server: the Cloudflare Worker exposing the Snappy Xano API… |
api.ts
import { annotationsForClass } from "../snappy-settings/tool-annotations.ts";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
import { serviceUrl } from "../snappy-settings/hand-resources.ts";
/**
* snappy-watchtower/api.ts — the typed interface.
*
* Two jobs: hand back the exact monitor commands to arm, and provide the
* verify helpers whose absence caused the failures SKILL.md records.
*
* Every default here is a measurement, not a preference. Where a number
* appears, it is dated — see the staleness rule in SKILL.md §4.
*/
export interface TreeSpec {
/** Repo root, absolute. Relative paths have mirrored into the wrong tree. */
readonly root: string;
/** Which tree this is, for the notification text. */
readonly label: string;
}
/** The two trees this repo actually has. An error in the untracked one is
* invisible until a deploy fails. */
export const TREES: readonly TreeSpec[] = [
{ root: "/Users/robertboulos/Projects/snappy-os-app", label: "app tree" },
{ root: "/Users/robertboulos/Projects/snappy-os-app-convex", label: "convex twin tree" },
];
/**
* The signatures a live typecheck monitor must match.
*
* `error TS` alone is SILENT when tsc itself dies, and silence reads exactly
* like "no errors". The crash arms exist so a monitor that stops working says
* so. Widen this rather than narrow it: extra noise beats a missed crashloop.
*/
export const TSC_WATCH_SIGNATURES = [
"error TS",
"Found [1-9][0-9]* error",
"command not found",
"ENOENT",
] as const;
/**
* The command to hand to Monitor for one tree.
*
* `--preserveWatchOutput` is load-bearing, not cosmetic: without it tsc clears
* the screen each cycle with an ANSI escape that can eat the error line before
* grep sees it. `--line-buffered` is likewise required — without it grep holds
* matches in its buffer and events arrive in clumps or not at all.
*/
export function tscWatchCommand(tree: TreeSpec): string {
const pattern = TSC_WATCH_SIGNATURES.join("|");
return `cd ${tree.root}/apps/snappy-os/web && npx tsc --noEmit --watch --preserveWatchOutput 2>&1 | grep -E --line-buffered '${pattern}'`;
}
export interface MonitorSpec {
readonly command: string;
readonly description: string;
readonly persistent: true;
readonly timeout_ms: number;
}
/** Arm one of these per tree, at session start, BEFORE building anything. */
export function watchtowerMonitors(trees: readonly TreeSpec[] = TREES): readonly MonitorSpec[] {
return trees.map((tree) => ({
command: tscWatchCommand(tree),
description: `web tsc errors, live (${tree.label})`,
persistent: true as const,
timeout_ms: 3_600_000,
}));
}
/**
* A grep whose absence claim is worth believing.
*
* The whole point is `knownPresent`: grep for something you KNOW is in the file
* first. If that returns zero, the probe is broken and the "absence" of
* `mustBeAbsent` proves nothing. Measured 2026-08-23: a bundle probe reported
* clean for a string that was present, because zsh had mangled the pattern.
*/
export function controlledAbsenceProbe(args: {
readonly file: string;
readonly knownPresent: string;
readonly mustBeAbsent: string;
}): string {
return [
`control=$(grep -c ${shellQuote(args.knownPresent)} ${shellQuote(args.file)} || true)`,
`if [ "$control" -eq 0 ]; then echo "PROBE BROKEN: the positive control matched nothing; this file's absence claim is worthless"; exit 2; fi`,
`hits=$(grep -c ${shellQuote(args.mustBeAbsent)} ${shellQuote(args.file)} || true)`,
`echo "control=$control absent_target_hits=$hits"`,
].join("\n");
}
/**
* Is this commit actually being SERVED?
*
* An installer can exit 0 having laid down nothing, so the exit code is not the
* answer — the serving head is. Read it off the running daemon, then prove
* ancestry rather than eyeballing two short hashes.
*/
export function servingHeadProof(commit: string, base = serviceUrl("snappy-os-app")): string {
// THE ADDRESS COMES FROM THE ONE REGISTRY ⟨lane mini-reads, 2026-09-09⟩. The
// port was this function's own default, so a moved daemon produced a proof
// line that probed the wrong place and reported NOT SERVED.
return [
`head=$(curl -s -m 8 ${base}/deploy-truth | sed -n 's/.*"serving_head":"\\([^"]*\\)".*/\\1/p')`,
`echo "serving_head=$head"`,
`git merge-base --is-ancestor ${shellQuote(commit)} "$head" && echo "SERVED: ${commit} is an ancestor of $head" || echo "NOT SERVED: ${commit} is not in the serving head"`,
].join("\n");
}
/**
* Run ONE test file. Never the full vitest suite on this machine — orphaned
* workers swamp it.
*
* `--reporter=verbose` is not optional when you intend to reason about which
* tests passed: the default reporter prints FAILING names only, which is what
* pushes an agent into deriving "how many passed" by subtraction. Subtraction
* cannot tell you WHICH, and which is the whole question.
*
* Pass an absolute `projectRoot`. A wrong `--root` reports
* `ReferenceError: document is not defined`, which looks exactly like a code
* failure and is a config miss — the project's jsdom environment never loaded.
*/
export function oneTestFile(args: {
readonly projectRoot: string;
readonly file: string;
readonly runner?: "vitest" | "node";
}): string {
if (args.runner === "node") {
return `node --test --import tsx ${shellQuote(args.file)}`;
}
return `cd ${args.projectRoot} && npx vitest run ${shellQuote(args.file)} --reporter=verbose`;
}
/** Test paths under `.build/` are STALE COPIES living in install worktrees and
* head-check worktrees. One file can run three times, and the two copies
* assert the old truth. If a test you just fixed still fails, check this
* before touching the code again. */
export function isStaleWorktreeCopy(testPath: string): boolean {
return testPath.includes("/.build/");
}
/** Shell-quote for zsh. zsh does NOT word-split unquoted variables, so an
* unquoted path list arrives as ONE argument containing spaces — this is the
* trap that broke `git commit --only $PATHS`. Single quotes also stop
* backticks in prose from being run as commands. */
export function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
/** WHAT THIS HAND ANSWERS: NOTHING, AND IT SAYS SO ⟨lane CONTRACTS, 2026-09-07⟩.
* This file is a library other skills import — it has no CLI dispatch, so it
* implements no verb a person or an AI could call. Declaring `verbs: {}` is
* the honest reading: the census counts it as a skill with a contract and zero
* callable verbs, rather than as a hand whose contract could not be read,
* which is a different fact and would send someone looking for a bug. */
/** THE HOST-FACING FACTS ⟨lane CONTRACTS N–Z, 2026-09-09⟩. `class` is the
* closed effect set snappy-tool-design rule 18 grades; `annotations` are
* DERIVED from it by the ONE derivation in
* `snappy-settings/tool-annotations.ts`, never written per verb, so a class
* and its published hints cannot disagree; `refusals` projects the ONE closed
* table in `snappy-settings/refusal-codes.ts`; `requires` is exactly the
* credential keys this file's own executable reads name, and nothing else. */
export const HAND_CONTRACT = {
skill: "snappy-watchtower",
description: "Standing error monitors and the probes that lie: arm live typecheck, build, and test monitors at session start, plus the dated list of git, Vitest, install, deploy, twin-tree, and probe traps where a green result meant nothing, each with the exact command that avoids it. Use when Robert says: /snappy-watchtower, \\\"set up the monitor with the feedback from errors\\\", \\\"the monitor is helping, keep the live loop going\\\", \\\"200 doesn't mean an agent system is working\\\", \\\"is it working\\\", \\\"is it actually working now\\\", \\\"it says green but it is broken\\\", \\\"keep the building/typechecking over there\\\", \\\"explain your monitor to the other team\\\". Triggers on: watchtower, monitor, typecheck watch, green but broken, probe trap, standing monitor. NOT the overnight orchestration itself (see snappy-nightshift). NOT app-level dogfooding (see snappy-resident).",
managed: false,
requires: [] as string[],
refusals: refusalTable("unknown_verb"),
verbs: {
},
} as const;
if (process.argv[1] !== undefined && process.argv[2] === "contract") {
try {
const { realpathSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
} catch { /* not invoked directly */ }
}
import { annotationsForClass } from "../snappy-settings/tool-annotations.ts";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
import { serviceUrl } from "../snappy-settings/hand-resources.ts";
/**
* snappy-watchtower/api.ts — the typed interface.
*
* Two jobs: hand back the exact monitor commands to arm, and provide the
* verify helpers whose absence caused the failures SKILL.md records.
*
* Every default here is a measurement, not a preference. Where a number
* appears, it is dated — see the staleness rule in SKILL.md §4.
*/
export interface TreeSpec {
/** Repo root, absolute. Relative paths have mirrored into the wrong tree. */
readonly root: string;
/** Which tree this is, for the notification text. */
readonly label: string;
}
/** The two trees this repo actually has. An error in the untracked one is
* invisible until a deploy fails. */
export const TREES: readonly TreeSpec[] = [
{ root: "/Users/robertboulos/Projects/snappy-os-app", label: "app tree" },
{ root: "/Users/robertboulos/Projects/snappy-os-app-convex", label: "convex twin tree" },
];
/**
* The signatures a live typecheck monitor must match.
*
* `error TS` alone is SILENT when tsc itself dies, and silence reads exactly
* like "no errors". The crash arms exist so a monitor that stops working says
* so. Widen this rather than narrow it: extra noise beats a missed crashloop.
*/
export const TSC_WATCH_SIGNATURES = [
"error TS",
"Found [1-9][0-9]* error",
"command not found",
"ENOENT",
] as const;
/**
* The command to hand to Monitor for one tree.
*
* `--preserveWatchOutput` is load-bearing, not cosmetic: without it tsc clears
* the screen each cycle with an ANSI escape that can eat the error line before
* grep sees it. `--line-buffered` is likewise required — without it grep holds
* matches in its buffer and events arrive in clumps or not at all.
*/
export function tscWatchCommand(tree: TreeSpec): string {
const pattern = TSC_WATCH_SIGNATURES.join("|");
return `cd ${tree.root}/apps/snappy-os/web && npx tsc --noEmit --watch --preserveWatchOutput 2>&1 | grep -E --line-buffered '${pattern}'`;
}
export interface MonitorSpec {
readonly command: string;
readonly description: string;
readonly persistent: true;
readonly timeout_ms: number;
}
/** Arm one of these per tree, at session start, BEFORE building anything. */
export function watchtowerMonitors(trees: readonly TreeSpec[] = TREES): readonly MonitorSpec[] {
return trees.map((tree) => ({
command: tscWatchCommand(tree),
description: `web tsc errors, live (${tree.label})`,
persistent: true as const,
timeout_ms: 3_600_000,
}));
}
/**
* A grep whose absence claim is worth believing.
*
* The whole point is `knownPresent`: grep for something you KNOW is in the file
* first. If that returns zero, the probe is broken and the "absence" of
* `mustBeAbsent` proves nothing. Measured 2026-08-23: a bundle probe reported
* clean for a string that was present, because zsh had mangled the pattern.
*/
export function controlledAbsenceProbe(args: {
readonly file: string;
readonly knownPresent: string;
readonly mustBeAbsent: string;
}): string {
return [
`control=$(grep -c ${shellQuote(args.knownPresent)} ${shellQuote(args.file)} || true)`,
`if [ "$control" -eq 0 ]; then echo "PROBE BROKEN: the positive control matched nothing; this file's absence claim is worthless"; exit 2; fi`,
`hits=$(grep -c ${shellQuote(args.mustBeAbsent)} ${shellQuote(args.file)} || true)`,
`echo "control=$control absent_target_hits=$hits"`,
].join("\n");
}
/**
* Is this commit actually being SERVED?
*
* An installer can exit 0 having laid down nothing, so the exit code is not the
* answer — the serving head is. Read it off the running daemon, then prove
* ancestry rather than eyeballing two short hashes.
*/
export function servingHeadProof(commit: string, base = serviceUrl("snappy-os-app")): string {
// THE ADDRESS COMES FROM THE ONE REGISTRY ⟨lane mini-reads, 2026-09-09⟩. The
// port was this function's own default, so a moved daemon produced a proof
// line that probed the wrong place and reported NOT SERVED.
return [
`head=$(curl -s -m 8 ${base}/deploy-truth | sed -n 's/.*"serving_head":"\\([^"]*\\)".*/\\1/p')`,
`echo "serving_head=$head"`,
`git merge-base --is-ancestor ${shellQuote(commit)} "$head" && echo "SERVED: ${commit} is an ancestor of $head" || echo "NOT SERVED: ${commit} is not in the serving head"`,
].join("\n");
}
/**
* Run ONE test file. Never the full vitest suite on this machine — orphaned
* workers swamp it.
*
* `--reporter=verbose` is not optional when you intend to reason about which
* tests passed: the default reporter prints FAILING names only, which is what
* pushes an agent into deriving "how many passed" by subtraction. Subtraction
* cannot tell you WHICH, and which is the whole question.
*
* Pass an absolute `projectRoot`. A wrong `--root` reports
* `ReferenceError: document is not defined`, which looks exactly like a code
* failure and is a config miss — the project's jsdom environment never loaded.
*/
export function oneTestFile(args: {
readonly projectRoot: string;
readonly file: string;
readonly runner?: "vitest" | "node";
}): string {
if (args.runner === "node") {
return `node --test --import tsx ${shellQuote(args.file)}`;
}
return `cd ${args.projectRoot} && npx vitest run ${shellQuote(args.file)} --reporter=verbose`;
}
/** Test paths under `.build/` are STALE COPIES living in install worktrees and
* head-check worktrees. One file can run three times, and the two copies
* assert the old truth. If a test you just fixed still fails, check this
* before touching the code again. */
export function isStaleWorktreeCopy(testPath: string): boolean {
return testPath.includes("/.build/");
}
/** Shell-quote for zsh. zsh does NOT word-split unquoted variables, so an
* unquoted path list arrives as ONE argument containing spaces — this is the
* trap that broke `git commit --only $PATHS`. Single quotes also stop
* backticks in prose from being run as commands. */
export function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
/** WHAT THIS HAND ANSWERS: NOTHING, AND IT SAYS SO ⟨lane CONTRACTS, 2026-09-07⟩.
* This file is a library other skills import — it has no CLI dispatch, so it
* implements no verb a person or an AI could call. Declaring `verbs: {}` is
* the honest reading: the census counts it as a skill with a contract and zero
* callable verbs, rather than as a hand whose contract could not be read,
* which is a different fact and would send someone looking for a bug. */
/** THE HOST-FACING FACTS ⟨lane CONTRACTS N–Z, 2026-09-09⟩. `class` is the
* closed effect set snappy-tool-design rule 18 grades; `annotations` are
* DERIVED from it by the ONE derivation in
* `snappy-settings/tool-annotations.ts`, never written per verb, so a class
* and its published hints cannot disagree; `refusals` projects the ONE closed
* table in `snappy-settings/refusal-codes.ts`; `requires` is exactly the
* credential keys this file's own executable reads name, and nothing else. */
export const HAND_CONTRACT = {
skill: "snappy-watchtower",
description: "Standing error monitors and the probes that lie: arm live typecheck, build, and test monitors at session start, plus the dated list of git, Vitest, install, deploy, twin-tree, and probe traps where a green result meant nothing, each with the exact command that avoids it. Use when Robert says: /snappy-watchtower, \\\"set up the monitor with the feedback from errors\\\", \\\"the monitor is helping, keep the live loop going\\\", \\\"200 doesn't mean an agent system is working\\\", \\\"is it working\\\", \\\"is it actually working now\\\", \\\"it says green but it is broken\\\", \\\"keep the building/typechecking over there\\\", \\\"explain your monitor to the other team\\\". Triggers on: watchtower, monitor, typecheck watch, green but broken, probe trap, standing monitor. NOT the overnight orchestration itself (see snappy-nightshift). NOT app-level dogfooding (see snappy-resident).",
managed: false,
requires: [] as string[],
refusals: refusalTable("unknown_verb"),
verbs: {
},
} as const;
if (process.argv[1] !== undefined && process.argv[2] === "contract") {
try {
const { realpathSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
} catch { /* not invoked directly */ }
}
refusals.test.ts
/**
* COVERAGE FOR SNAPPY-WATCHTOWER'S DECLARED REFUSAL CODES
* (snappy-tool-design rule 33: "refusal codes form one closed table and each
* row has coverage").
*
* Two things are graded here, and the second is the one that matters. The
* first is that the hand's table is a PROJECTION of the collection's one
* closed table in snappy-settings/refusal-codes.ts — same object, not a copy
* that can drift. The second is that every declared code is GROUNDED: the
* evidence that justified declaring it is re-checked here, because a refusal
* code with no path that emits it is a branch the reader waits for and never
* sees, and a table of those passes a lint while teaching a lie.
*
* The code list is spelled out rather than read from the contract: a test that
* iterates the thing it grades passes for an empty table.
*/
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES } from "../snappy-settings/refusal-codes.ts";
const SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "api.ts"), "utf8");
/** Every refusal code snappy-watchtower declares. */
const DECLARED = [
"unknown_verb",
] as const;
test("snappy-watchtower declares exactly these refusal codes", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals).sort(), [...DECLARED].sort());
});
test("every declared code is the SAME row as the one closed table's, never a copy", () => {
const table = HAND_CONTRACT.refusals as Record<string, unknown>;
for (const code of DECLARED) {
assert.equal(table[code], REFUSAL_CODES[code], `${code} is not the shared row`);
}
});
test("unknown_verb is grounded: the contract closes the verb set, so a word outside it is refusable", () => {
assert.ok(Object.keys(HAND_CONTRACT.verbs).length >= 0);
assert.ok(!Object.keys(HAND_CONTRACT.verbs).includes("no-such-verb"));
});
/**
* COVERAGE FOR SNAPPY-WATCHTOWER'S DECLARED REFUSAL CODES
* (snappy-tool-design rule 33: "refusal codes form one closed table and each
* row has coverage").
*
* Two things are graded here, and the second is the one that matters. The
* first is that the hand's table is a PROJECTION of the collection's one
* closed table in snappy-settings/refusal-codes.ts — same object, not a copy
* that can drift. The second is that every declared code is GROUNDED: the
* evidence that justified declaring it is re-checked here, because a refusal
* code with no path that emits it is a branch the reader waits for and never
* sees, and a table of those passes a lint while teaching a lie.
*
* The code list is spelled out rather than read from the contract: a test that
* iterates the thing it grades passes for an empty table.
*/
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES } from "../snappy-settings/refusal-codes.ts";
const SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "api.ts"), "utf8");
/** Every refusal code snappy-watchtower declares. */
const DECLARED = [
"unknown_verb",
] as const;
test("snappy-watchtower declares exactly these refusal codes", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals).sort(), [...DECLARED].sort());
});
test("every declared code is the SAME row as the one closed table's, never a copy", () => {
const table = HAND_CONTRACT.refusals as Record<string, unknown>;
for (const code of DECLARED) {
assert.equal(table[code], REFUSAL_CODES[code], `${code} is not the shared row`);
}
});
test("unknown_verb is grounded: the contract closes the verb set, so a word outside it is refusable", () => {
assert.ok(Object.keys(HAND_CONTRACT.verbs).length >= 0);
assert.ok(!Object.keys(HAND_CONTRACT.verbs).includes("no-such-verb"));
});