The surveyor's plumb line: checks that things are true — and calls in repairs when they aren't.
Plumb is a small, dependency-free TypeScript package that turns failed deployment checks into agent-drafted repair proposals. You supply a shell command that reports success or failure. Plumb logs the result and, on failure, invokes a guardrailed AI agent in its own clean clone to diagnose the problem and open a PR or issue. The deploy checkout and production data remain outside the agent's working boundary.
Born inside OpenTherapist, where its first live firing found two real deployment bugs.
npm install @kontourai/plumb
PLUMB_CONFIG=/path/to/deployment/plumb.config npx @kontourai/plumb runWith a local dependency, the same CLI is available directly:
plumb run
plumb escalate database-refresh /var/log/my-app/database-refresh.logOnce config loads, plumb run always exits zero. A failed CHECKS_CMD
triggers escalation as a side effect; suppression guards or agent outcomes do
not turn the scheduled check runner into a failing job. plumb run does exit
non-zero if config loading itself fails (missing PLUMB_CONFIG or a missing
required key) — that is a setup error, not a checks result.
At the end of a deployment, attach the deployed commit:
plumb run --context deploy="$DEPLOY_SHA"The optional context is a single key=value pair. It tags the run's log header
and is otherwise inert unless the key is deploy and the value is a 7–64 digit
hex Git SHA. Deploy SHAs are normalized to lowercase. A malformed context emits
a one-line warning and is ignored without changing plumb run's zero exit.
A deploy failure tells the maintenance agent which commit just landed. When
plumb has seen a different deploy SHA previously, it also supplies the verified
Git range and diffstat from the clean agent clone.
Start from plumb.config.example. It is a bash key/value
file, so quoting, comments, $HOME expansion, and existing shell expressions
continue to work. Plumb loads it by sourcing it in bash.
Your plumb.config belongs with the deployment, not inside the plumb
package or repository. Point PLUMB_CONFIG at that file in the environment or
in a systemd Environment= line:
export PLUMB_CONFIG="$HOME/infra/my-app/plumb.config"The required keys are:
REPO_URL: repository cloned for the maintenance agent.CHECKS_CMD: shell command that exits non-zero when any hard check fails.AGENT_WORKDIR: dedicated agent clone, never the deployment checkout.AGENT_CMD: headless agent command.LOG_FILE: append-only checks log; escalation output uses the corresponding-escalations.logfile.CONTEXT_DOCS: optional docs the agent should read first.
Escalation guards are tunable through the config or environment:
PLUMB_ESCALATE_COOLDOWN_SECSdefaults to21600(6 hours) per context.PLUMB_ESCALATE_DAILY_CAPdefaults to8across all contexts.PLUMB_STATE_DIRdefaults to astatedirectory besideLOG_FILE.
Deploy-context runs keep the latest valid SHA in PLUMB_STATE_DIR/deploy-sha.
This is only a one-value pointer for enriching the next deploy failure; it is
not a run-history store.
plumb escalate <context> <log> is a standalone primitive. Any cron job,
timer, or deployment step can route its own failure evidence through the same
per-context lock, cooldown, global daily cap, clean clone, and guardrailed
agent. Give each failure mode a distinct context name so unrelated jobs do not
share cooldown state:
plumb escalate database-refresh /var/log/my-app/database-refresh.log
plumb escalate search-reindex /var/log/my-app/search-reindex.logSee examples/job-with-escalation.sh for a
complete two-step cron-job pattern. If you'd rather run plumb on a timer
than from cron, systemd/plumb-checks.service
and systemd/plumb-checks.timer are ready-made
units.
import { escalate, loadConfig, run } from "@kontourai/plumb";
const config = loadConfig(process.env.PLUMB_CONFIG);
run(config, { context: `deploy=${process.env.DEPLOY_SHA}` });
escalate("custom-job", "/var/log/custom-job.log", config);- Checks may be written in any language. They should print useful evidence and exit non-zero if any hard check fails.
plumb runappends the check output and escalates failures. An optional--context key=valuetags the log header;deploy=<sha>also carries deploy provenance into escalation.- The escalator resets a dedicated clone to
origin/main, gives the last 4000 log bytes to the configured agent, and enforces lock, cooldown, and daily-cap guards before expensive work. A new deploy SHA gets its own cooldown signature while remaining subject to the same base-context lock and global daily cap. - The agent prompt forbids production-data writes, commits to main, force-pushes, and secret-file changes. Confident fixes become unmerged PRs; uncertain or environmental diagnoses become issues.
A pager tells you at 7am that something broke. Plumb's agent has already read the logs, bisected the cause, and opened a PR with a failing-case explanation — or an issue explaining why it didn't act. You review diffs over coffee instead of tailing logs in a bathrobe. The guardrails (own clone, no-main, no-prod-data, PR-only) are designed so a wrong diagnosis costs you a closed PR. Today those guardrails are prompt-level instructions to the agent; hard enforcement — a post-run boundary audit and a sandboxed agent option — is tracked in the backlog and should land before you point plumb at anything you cannot afford to babysit.