Imported from inactdev/inspector (
AGENTS.md). Install upstream withnpx skills add inactdev/inspector. Copyright stays with the author.
Project agent memory
This file is the project's committed home for project-intrinsic agent knowledge: build, test, release, architecture, and sharp-edge notes that should travel with the code.
- Read SPEC.md before touching anything - it is the design of record and short.
- This repo's own check command is
script/check(required-tools preflight, gofmt, vet, build, test, then inspector-gate's own shell tests), wired up via.inspector.json- inspector dogfoods itself. Tests run under-race, which needs cgo and a C compiler locally (CI's ubuntu-latest already has one) and costs about a second of suite time; keep it, the code captures subprocess output concurrently.-raceis unsupported on some platforms (32-bit x86, linux/arm, freebsd/arm) - useCGO_ENABLED=0 go test ./...there instead ofscript/check. Also needs Docker forinternal/container's own tests, most ofinternal/inspector's, and the end-to-end ones incmd/inspector- they skip cleanly (requireDockerin each package's tests) rather than fail when no usable runtime is present, the same toleranceTestLoadConfig_UnreadableFilealready extends to a root-run suite. - Two similarly-named paths, different jobs:
.inspector.json(tracked, project config - the check command, the image it runs in, network opt-in, andprotectedPaths) vs.inspector/(gitignored, one JSON report per local run, notes only, never authority). internal/inspector.Runrefuses on a dirty working tree, because a result is only honest when it's bound to a commit that actually matches what's on disk. It excludes.inspector/itself from that dirty check via a git pathspec (repo.go,WorkingTreeStatus) - without that exclusion, a repo that never gitignores.inspector/would go permanently dirty, and therefore permanently refused, after its first run. Don't drop that exclusion.- Exit codes are a deliberate split, not pass/fail:
0/1/2are reserved for verdicts, and anything that is not a verdict attempt at all gets64. The list itself is owned by README's Use section and the const block incmd/inspector/main.go- read it there, don't restate it. The invariant every caller that shells out to inspector depends on: never treat2as red, and never confuse64with a verdict. (inspector-gate is not one of those callers - it reads the commit status, never an exit code.) - This is issue #1 of the roadmap in
gh issue list(orgh-axi issue listunder firstmate) - the fixer (#2) and the installer (#5) are deliberately out of scope here and still open. Issue #13 (run the check command in a container) is built, ininternal/container, and the gate workflow (#4) is built too - both are described below. - A green or red local run (issue #3) also has to land as a GitHub commit status (
internal/inspector/status.go, wired fromcmd/inspector/main.go) before the CLI is allowed to exit as that verdict - see SPEC.md section 7. The status context is the stable stringinspector, whichinspector-gate(#4) keys on; the token comes fromGITHUB_TOKEN. A missing token, an unresolvableoriginremote, or GitHub rejecting the post all fail loudly and exit2, even when the local check itself passed - a printed "green" is never proof the status reached GitHub. ARefusedoutcome posts nothing on purpose: absence already reads as failing, same as an unreachable API. - The check command runs inside a container (
internal/container, driven bycheck.go), not on the host: bind-mounted to the repo root atcontainer.WorkspaceDirand nothing else, network-denied by default (network: truein.inspector.jsonopts in).container.EnsureAvailablerefuses loudly, before ever attempting a run, when Docker is missing or its daemon is unreachable - never fall back to running the check command on the host, that's the one thing this exists to prevent.RunCheck(check.go) distinguishes docker itself failing to start the container (an infrastructure failure, surfaced as a Go error -container.Cmd.Startedreads back the--cidfileDocker writes at container creation, before the command inside it ever runs) from the check command's own exit, signal, or timeout (a normalCheckResult, unchanged shape). Killing the container on timeout (docker kill, not a local process-group kill - that older mechanism died with the directshexecution it protected) rides the kernel's own guarantee that killing a PID namespace's init process takes every process it forked down with it - stronger than theSetpgidgroup-kill it replaced, and the reasonTestRunCheck_TimeoutKillsChildProcessesstill passes. Whether that kill actually worked is read fromcontainer.Cmd.KillError, never inferred from whatRunreturned -exec.Cmd.WaitdropsCancel's error whenever thedocker runclient itself exited badly, which is precisely the case where the container is still running - and a refused kill stays aTimedOutverdict with docker's own message on stderr and inOutput, never an infrastructure error that would cost the run its report. It also setsCheckResult.KillFailed, which is reporting only (nothing classifies on it) and exists sorun.go's refusal never tells someone the check was killed when docker says it wasn't. - A container's own init process (PID 1) is immune to a signal sent to it from within its own PID namespace, even
SIGKILL- real Linux kernel behavior (pid_namespaces(7)), not a bug. A check command'skill -9 $$is therefore a no-op when that check command is the container's top-level process; a signal delivered from outside the namespace (docker kill, the OOM killer) still works normally. Tests that need a real signal-killed check command fork a child to kill instead (sh -c 'kill -9 $$' && true, matching README's ownnpm test && npm run lintshape) - seeTestRunCheck_CompoundCommandSurvivingKilledChild. - Inspector's own
.inspector.jsondeclares"image": "cimg/go:1.22", becausescript/checkneeds more than a Go toolchain: gcc and CGO_ENABLED=1 for-race, plus the shell tools.github/scripts/required-tools.sh'sREQUIRED_TOOLSnames, for.github/scripts/inspector-gate_test.sh-jqamong them being a real runtime dependency of the shipped gate script, not just its tests.golang:1.22was the earlier choice and carries nojq, so the gate tests failed inside the container while passing on CI's host.required-tools.shis the single source of truth for that list -script/check's preflight andinspector-gate_test.sh's own drift test (which re-runs itself with PATH restricted to exactly that list, catching a tool the list forgot rather than a reviewer having to notice) both read it, so it can't drift between the two the way the hand-duplicated version did the first time (trwas missing). The one thing that drift test cannot prove is its own scaffolding -mktemp,ln,sedandrmrun in the outer, unrestricted-PATH copy, so they are listed outright instead of being verified. The container is network-denied by default, so nothing can be installed at check time - any replacement image has to carry the whole list already. .github/workflows/inspector-gate.yml(issue #4) runs onpull_request_targetwith noactions/checkoutat all, ever (SPEC.md section 8) - so it can't run.github/scripts/inspector-gate.shas a file on disk. Instead it fetches that script's own content from the BASE BRANCH TIP (base.ref, not the payload'sbase.sha) via the Contents API andsources it, the same trust boundary and same ref used for reading.inspector.json; the tip is deliberate, so a tightenedprotectedPathsapplies to already-open pull requests and the script comes from where GitHub loads the workflow file from. Never add a checkout. Anon: statustrigger was tried, to re-answer question 1 when inspector's status lands after thesynchronizerun that reported its absence, and was REMOVED: a status-triggered run's check attaches to the default branch's tip, never the pull request's head, so it never did the one thing it was for, and a status event cannot be filtered by context, so any third party's status re-ran the gate and landed failures on the default branch. Don't reintroduce it - inspector#18 (inspector owns the push, so a green status exists before the pull request is opened) is the intended fix; until then the check can stand stale-red until a human re-runs the workflow.- inspector-gate's protected-path list is
.github/**and.inspector.json(hardcoded in the script,ALWAYS_PROTECTED), plus the base branch's.inspector.jsonprotectedPathsentries - nothing else. A project's own check command is deliberately NOT derived from itscheckfield: that was tried (auto-protect whencheckreads as a bare path) and reverted after landing three review findings in the same spot, because telling a path from a command line by looking at the string never holds up in general - don't reintroduce it. This repo's own.inspector.jsonlistsscript/checkunderprotectedPathsexplicitly for exactly that reason. Every uncertainty in reading.inspector.jsonfails CLOSED except a genuine 404; don't relax that, a gate protecting less than it claims is worse than one that refuses. The script's pure decision functions are unit tested byinspector-gate_test.sh, wired intoscript/check; the GitHub API orchestration inmain()is not testable outside Actions and is documented as such rather than mocked.
Maintaining this file
Keep this file for knowledge useful to almost every future agent session in this project. Do not repeat what the codebase already shows; point to the authoritative file or command instead. Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve this bar for all agents and keep entries concise.