watch
watch recursively watches a repository and runs a
static-only check on every relevant change, live, instead of
one shot after the agent stops.
What it does
Run paranoid watch [path] while an agent (or you)
is editing a repository. It resolves a base ref once, at
startup, then watches the tree for changes to test files, test
configuration, CI workflow files, and dependency manifests. On
a debounced batch of changes it runs test integrity plus DEP005
against that fixed base and the current worktree, and prints
the result.
$ paranoid watch
watching /home/you/project from base 7db5cfbeaacf (Ctrl-C to stop)
14:32:07 write internal/calc/calc_test.go
14:32:08 1 finding score 90 verdict pass (84ms)
paranoid dev
repo: /home/you/project
base: 7db5cfbeaacf
head: worktree (uncommitted changes included)
score: 90/100 verdict: pass
test-integrity (1 finding)
TI004 high internal/calc/calc_test.go:6
A skip was added to an existing test.
evidence: TestAdd now calls a skip method
fix: Remove the skip or explain why the test cannot run.
checks run: test-integrity/config, test-integrity/go, test-integrity/javascript, test-integrity/python, dependencies/consistency
...
Every line of output starts with a clock time. A relevant
filesystem event gets one compact line as it happens
(write, create, remove,
or rename, then the path relative to the watched
root). Once a debounced run finishes, one status line always
prints (score, verdict, finding count, run time); when the run
actually found something, the full report follows it, the same
text format verify prints.
What counts as a relevant change
watch reuses the exact same file classification
verify already uses, from
internal/analyze/testintegrity/common and
internal/analyze/deps, so "what is a test file" or
"what is a manifest" is never a second, drifting definition:
- A Go, Python, or JS/TS test file.
- A test or CI configuration file (pytest, jest/vitest config, a Makefile, a
.github/workflows/*.ymlfile). - A dependency manifest or lockfile (
package.json,go.mod,requirements*.txt, and so on).
Anything else, a source file, documentation, an asset, does not
trigger a run. The watch is recursive: a newly created
directory is added to it automatically, and a removed directory
is dropped from it. .git/, node_modules/,
and common build directories (dist,
build, vendor, __pycache__,
and similar) are never descended into.
The static-only scope
Every debounced run is test integrity plus DEP005 (dependency
consistency) only. It never contacts a dependency registry
(the same skip verify --offline gives DEP001 to
DEP004) and never runs the clean-room sandbox. Claims,
error-handling, api-surface, and safety analysis do not run
either. The report is always honest about this: every stage
that does not run is listed in checks_skipped with
the reason, never silently missing.
This is a deliberate scope, not a shortcut applied silently: a
live watch needs an answer in well under a second, and a
container run or a network round trip cannot deliver that. Run
paranoid verify for the full pipeline, including
the sandbox and claims checking, once you are done.
Base and head
The base ref is resolved exactly once, when watch
starts: --base if given, otherwise the same
default verify uses (merge-base with the default
branch, else HEAD~1). Every run for the life of
that one watch process compares against that same
base, even if you commit or switch branches while it is
running. There is no --head flag: the head is
always the current worktree, since that is the entire point of
a live watch.
Notifications
--notify sends a desktop notification whenever a
run finds something: osascript on darwin,
notify-send on linux, when either is on
PATH. On any other platform, or when the binary is
not available, notification is silently skipped; it never fails
the run.
Performance target
From a settled change (the debounce window closing) to a
printed report, under 1 second on a 100-file diff. This matches
the shape of verify's own target (under 2 seconds
without the sandbox on a 100-file diff): watch mode's pipeline
is a strict subset of that same work, so it stays well inside
it.
Lifecycle
Stop with Ctrl-C (SIGINT) or SIGTERM. Shutdown is always clean: the filesystem watcher is closed, and an in-flight check either finishes on its own or is canceled through its context, never left running after the process exits. A single worker runs one check at a time; a change that arrives while a check is still running is coalesced into exactly one more run once it finishes, never a growing queue of pending runs.
Flags
| Flag | Default | Meaning |
|---|---|---|
| --base string | merge-base with the default branch, else HEAD~1 | State to compare the worktree against, resolved once at startup |
| --notify | off | Send a desktop notification when a run finds something |