Repository navigation
Releases: qwwiwi/agent-jobs
Releases · qwwiwi/agent-jobs
Release list
v0.4.0
agent-jobs v0.4.0 (2026-10-18)
Full list: CHANGELOG.md, section [0.4.0]. Compare: v0.3.0...v0.4.0
Highlights
- Schedules, dead-man checks, intentions,
notify_owner(Wave B): cron schedules that start jobs, checks that alert when an expected mark is missing, the agent's own goals, and a rate-limited message to the owner through the agent's channel. - Daily automation:
prune(with the log archive) anddoctor procs(report only) once per local day inside the daemon. - Codex runner (DEVIATIONS #5):
gpt-*models orrunner: codexrun oncodex exec; read-only kinds only unlesscodex_write_allowed: true. - Heartbeat (Wave D, off by default): a Go gate plus one cheap model decision whether to wake the agent for its goals; only a fixed
[jobs:heartbeat] wake <id>line reaches the session. - Owner-waits Stop hook, read-only HTTP API for a jobs dashboard (off by default, loopback or unix socket, bearer token),
/v1/skillsrating. - Autopilot:
acceptance_cmd, risky-first review checklist, secret redaction in reports and logs,## Progresssection in the report contract, brief size check (max_brief_bytes, 32 KB). - Skills for jobs (E7 job side, DEVIATIONS #16):
skill_rootscatalogs, core skills per kind linked read-only into the worktree,skill_list,agent-jobs skills, stats by skill. - Configurable concurrency:
max_runningper agent (#33). - Clean-clone acceptance (#35):
scripts/acceptance-clean-clone.shreplays build, check, install, daemon and one job from a fresh clone; see README «Release checklist». - Security: job tool allowlist in the guard (#29), state-dir read restriction for jobs, write protection for skill catalogs and links, PEM redaction fixes (#26), bare-redirection parsing.
- Fixes: scope cgroup lookup via the system manager when the user manager is not under
user.slice(#32); fork subagents off for claude jobs; per-job lifecycle lock inprune.
Upgrade notes
- Rebuild and reinstall the binary (
go build -ldflags "-X main.version=v0.4.0" ...orscripts/install-linux.sh), then restart the daemon and reconnect MCP sessions so every process runs 0.4.0. - New tables (schedules, checks, intentions, owner_notifications, heartbeats, ...) are created on open; column additions are additive migrations. Back up
jobs.dbbefore the first start anyway. max_running: integer 1..8, default 2 when absent (same behaviour as before). A value outside 1..8,nullor a non-integer is a config error at load, never clamped: the daemon refuses to start. It is read at start, so a change needs a daemon restart;system_doctorandagent-jobs statusmay show the file's value before the daemon picks it up.- New features are opt-in:
heartbeat.enabledfalse,http_api.enabledfalse,codex_write_allowedfalse,skill_rootsempty. - Claude jobs now get
CLAUDE_CODE_FORK_SUBAGENT=0; jobs may call only the allowlisted tools (an MCP tool outside the job set is refused).
Known limits
- Ceilings are rejected, never clamped: turns 200, budget $50, time 180 min,
max_running8. - Codex jobs: no turn or budget caps (timeout only, cost recorded as unknown), the guard hook does not run inside codex, resume of a codex job is refused, codex jobs get no skills.
- Dead-man alerts are at-least-once: a crash between typing and storing may type an alert twice.
- Without a working user bus or a trusted root-owned
systemctl, a scope sweep may not find the cgroup and the stop stays pending (no guessing of cgroup paths). - Heartbeat decision text is untrusted model output; it is stored, never typed into the session.
- Cgroup containment depends on systemd user scopes (Linux); on macOS jobs run without a scope.
v0.3.0
No 0.2.0 was ever tagged: everything that was described as 0.2.0 is released here in 0.3.0.
Added
scripts/check-commit-messages.sh, run byscripts/check.shand the pre-push hook: refuses commits not yet onorigin/main(or the last 20 when it is unknown) whose message contains Cyrillic, naming each one; bash 3.2 and POSIX grep.- MCP server
agent-jobs mcp --role orchestrator|job --agent A(stdio,github.com/modelcontextprotocol/go-sdkv1.8.0) with a tool registry of two audiences; the job role is an allowlist, and the server itself refuses hidden tools and filters the tool list. - Orchestrator tools
job_start,job_status,job_result,job_list,job_cancelover the existing Manager;job_resultreadsreport.mdonly as a regular file (no FIFO, symlink or directory), capped at 256 KB. - Each job's
mcp.jsonnow names the job-role server instead of an empty stub. - Store columns
complexity,runner,owner_waits,resumed_from,schedule_id,billing(tolerant migrations). tmux_socketconfig: notify on a named tmux socket (tmux -L).- Nested-job prevention: job marker
AGENT_JOBS_JOB; the binary refuses control commands under the marker, under a wrapper ancestor, in a wrapper's session or in a running job's session; guard rules againstagent-jobsas a command and against changingAGENT_JOBS_*(incl.mapfile/readarray,printf -v). - Session sweep: after claude exits the wrapper ends every other process of the job's session before writing
exit_code. Signals go through a pidfd opened before the membership check, so a reused pid is never signalled; without pidfd support nothing is signalled. If processes survive or the sweep fails, the wrapper writes asweep_incompletemarker and the daemon fails the job with «processes survived the job», which the job-done line repeats. agent-jobsskill (skills/agent-jobs/SKILL.md): the MCP job tools, the mandatory model-or-complexity, the routing rule (do it yourself, a subagent, or a job), the subagent cap and how to read a result.scripts/install-linux.shinstalls it per agent (--skills-dir DIR, default${CLAUDE_CONFIG_DIR:-~/.claude}/skills;--no-skill). It records the installed hash (.installed-sha256) and updates the skill only while the destination still matches it; a locally edited skill is left with a notice, replaced only with--force-skill(backupSKILL.md.bak.<time>). Ownership is recorded per instance (~/.config/agent-jobs/<instance>.skill, canonical path): uninstall goes by that record, keeps a skill another instance recorded, and keeps an edited one. A skills dir that is the repository's ownskills/is refused.- Jobs may use subagents: at most three per job, inside the job's own limits. The brief carries a «Subagents» section; the guard hook holds the cap with a monotonic counter in the job's store row (new column
subagents) and logs each call in<state>/subagents/<id>.log; a shortened log refuses further subagents (DEVIATIONS #12). - Job profile:
agent-jobs profile --agent A --from FILE... [--force] [--dry-run]generates the owner-rules file from the sections of the agent's own rules marked<!-- agent-jobs:profile -->…<!-- /agent-jobs:profile -->; over 8 KB is refused and nothing is written. - Project cards:
<state>/cards/<name>.md(configcards_dir), front matterrepo:/remote:, ≤ 4 KB each; cards matching a job's repository are attached to its brief (≤ 8 KB for the rendered section, headings included). Remotes match as identities: networkhost/path(host case ignored, path case kept, scheme/user/port/.gitignored) or a local absolute path (relative remotes resolved against the repository), so a relative local remote never matches a network one.agent-jobs card new --repo PATH [--name N]writes one from the template,agent-jobs card list [--repo PATH]shows them. Example card:cards/example-agent-jobs.md. - The orchestrator MCP instructions and
prompts/orchestrator.mdstate the routing rule and the subagent cap. - Shared-brain tools (DEVIATIONS #2, #3):
memory_search,memory_write,memory_already_exists,task_create,task_list,task_update,task_get,send_message,inbox, with the reference's names, backed by the team's shared brain (gbrain) over MCP streamable HTTP. The job role getsmemory_searchandmemory_writeonly, through the daemon: the job-role MCP server reads no token and calls the daemon's brain proxy on<state>/brain.sock(mode 600, running jobs only), andtoken_envis dropped from the job's environment. A job's note carries tagagent-jobs-job:<id>, author<agent>/job:<id>and a marker first line (caller tags with that prefix are dropped in both roles); a job cannot write adecision. The job role refuses to start withoutAGENT_JOBS_JOB. The brain client follows no redirect and sends the token only to the configured endpoint;task_updatereports «fields updated, but the status change … failed»;send_messagecannot overridefrom. New packageinternal/brain:Braininterface, HTTPClient(one short session per call, bounded bytimeout_seconds),Disabled, and the testFake. - Config block
brain:recall_url,memory_url,tasks_url,swarm_url(https, plain http only on loopback),token_file(mode 600) ortoken_env(one is required, never the token itself),agent,timeout_seconds,already_exists_min_score. Without the block the tools answer «brain not configured» and nothing else changes; a malformed block is a config error. - Memory notes in the brief: when a job is prepared the daemon asks shared memory for up to six notes closest to its title and brief and adds a «Memory notes» section after the project cards (snippets quoted line by line, marked as data). An error or no answer within 5 s never blocks the job: the section then says memory was unreachable, and lookups are skipped for a minute after an unanswered one. Notes written by jobs (recorded paths in the new table
job_notes, or the marker line) are left out and counted. - Job note tracking fails closed: the note path is taken from the brain's write answer (
pathfield or"<word>: <path>", vault-relative.mdonly); a job write whose path cannot be extracted or recorded returns an error and the daemon refuses further job writes until it restarts. An orchestrator rewrite (created/updated) of a job's note clears itsjob_notesmark. - Docs:
docs/WORKFLOW.md(Russian:docs/WORKFLOW.ru.md), how the main session works as an orchestrator: routing between itself, subagents and jobs, the pipeline, review policy, reading results, known limits; optional reminder hookexamples/hooks/prefer-jobs-reminder.sh. agent-jobs prune [--dry-run] [--json]: folders of jobs finished more than 48 hours ago keep the records (report.md, patches,base_commit,diffstat.txt) and small top-level text files (≤ 1 MiB); subdirectories, binaries, large files and symlinks (removed as links, never followed) go. Repo jobs whose cleanup has not run are skipped; a worktree kept for a human is left alone. Runs under the lifecycle lock.agent-jobs doctor procs [--kill] [--json]: finds processes carrying a finished job's marker, markerless leftovers in a finished job's session (leader gone, the session's first member too), both only when started while the job ran in the current boot; leaders (a marked process that daemonized into its own session, or a marked process under such a leader or in a live session that is not the job's; a running row whose wrapper died is matched by marker only, never by session); running rows with a dead wrapper pid; and suspects (preview servers, browsers, wait loops whose session leader is gone). Report only by default;--killends the job ghosts (never leaders or suspects) through a pidfd after re-checking start time and session.- Orchestrator MCP tools
system_doctor(disk, queue, processes, hooks),system_logs(daemon log or a job'sstderr.log),activity_report(jobs over a window),trash_move/trash_restore(reversible deletion by rename into<state>/trash/<id>/,renameat2(RENAME_NOREPLACE)on a re-checked parent descriptor). None of them is open to jobs. - Config
trash_roots(no default: trash moves nothing until it is set). Always protected: the agent-jobs home (every agent), owner rules, the agent-jobs binary,~/.claude, an absolute$CLAUDE_CONFIG_DIR,~/.claude.json, any.git. On macOS trash usesrenameatx_np(RENAME_EXCL), an fstat device/inode re-check of the parent descriptor and case-folded.git/protected-path checks; without pidfds (macOS) the sweep anddoctor procs --killsignal nothing. - Log archive: prune moves a cleaned job's records into
<archive>/<YYYY-MM>/<id>/(stream.jsonl.gz,stderr.log.gz,prompt.md,report.md,diffstat.txt,meta.json) instead of deleting the stream; configlog_archive_dir(default<state>/archive) andlog_archive_days(default 365, 0 = forever, enforced by prune only in a root the archive created, marked by.agent-jobs-archive; an existing folder without the marker is never expired or chmodded and prune/archive printretention off: no marker in <root>; the default<state>/archivegets a missing marker back); the archive is 0700 and a symlinked archive path is refused; when the archive cannot be written, the stream, stderr and prompt stay and prune still cleans the rest of the folder.agent-jobs archive [--dry-run] [--json]shows or does the move alone. - Per-job stats: table
job_stats, one row per finished job written when it ends (tool calls by tool, skills used, guard refusals by rule, subagents, report present, result subtype, cost, duration, repository basename).agent-jobs stats [--since D] [--by kind|model|skill] [--json] [--backfill [--all]]and the orchestrator MCP tooljob_statssummarize them (success rate, median and p90 cost and duration, top refusal rules, skills by uses and success ...
v0.1.0 — Agent Jobs
Added
- SQLite job queue (
jobstable) with statuses queued/running/succeeded/failed/timeout/cancelled; only queued/running are mutable. - Limits: defaults 80 turns / $10 / 45 min, ceilings 200 / $50 / 180 – over-ceiling requests rejected, never clamped. At most 2 running jobs per agent.
agent-jobs daemon(tick every minute) andagent-jobs tick; launchd template with KeepAlive.- Launch exactly per the reference via a detached
agent-jobs run-wrapperin its own process group; exit_code file; a daemon restart neither kills nor loses jobs. - Per-job git worktree (detached at base) or empty work dir;
base_commitfile. - settings.json: «nothing outside» deny rules, web tools only for research, PreToolUse guard hook (
agent-jobs guard). - prompt.md: preamble + owner rules (≤ 8 KB, required) + task + kind checklist + report contract.
- report.md extracted from stream.jsonl; exit 0 without a report = failed.
- One-line tmux notify
[jobs:job-done] …(disabled unless configured); undelivered lines stay pending and are retried. - Cleanup 30 min after notify: diffstat.txt, commits.patch, staged.patch, uncommitted.patch, untracked.tar,
refs/agent-jobs/jobs/<id>, then removal of only that job's worktree; restore tested. prompts/orchestrator.md– main-session duties quoted verbatim from the reference (read the report, «Not done / uncertain» first, check the diff, publish only after; cleanup/patches);agent-jobs initwrites it to<state>/orchestrator.md.- CLI: init, start, status, list, result, cancel,
version(also--version; the version is set at build time with-ldflags "-X main.version=…",devotherwise). - Linux support: systemd user unit template
deploy/systemd/agent-jobs@.service(instance = agent,KillMode=process,Restart=on-failure, env file$XDG_CONFIG_HOME/agent-jobs/<agent>.env) andscripts/install-linux.sh(idempotent build/install/enable/restart,--uninstall, no sudo, current user only; stamps the binary withgit describe --tags --always --dirty). - Tests for the unit template and the install script (fake systemctl, sandboxed HOME), including
systemd-analyze --user verifyof the template when available. - Local checks:
scripts/check.sh,.githooks/pre-push. - Docs: English README with a Russian version (
README.ru.md), English translation of the reference design (docs/REFERENCE.md, original indocs/REFERENCE.ru.md), public roadmap (docs/PLAN.md,docs/PLAN.ru.md),docs/DECISIONS.md, DEVIATIONS #11 (systemd user service on Linux), diagrams indocs/images/, MITLICENSE.
Changed
- Model table: review/fix/general light → sonnet, normal → opus, hard →
fable; research light/normal → sonnet, hard → opus. - Reference timings fixed: daemon
--intervalremoved (tick every minute),cleanup_delay_minutesremoved (cleanup 30 min after notify); a config that still sets it is rejected. - Preamble wording 1:1 with the reference – no «unless the brief names a path» exception.
Fixed
- Per-agent
flockaround tick and cancel; atomic claim with a launch token; a tick only finishes jobs it owns. - Claim before spawn; the wrapper registers in
wrapper.jsonbefore running claude and holdswrapper.lock; the next tick adopts an orphan wrapper; a claim whose wrapper never registered fails loudly after 1 minute. - stream.jsonl is read after exit_code is seen.
- Cleanup no longer touches the index, and verifies worktree ownership (worktree.json + porcelain listing + owner mark in the admin dir) before touching anything.
--uninstallkeeps the binary while job wrappers started from it run (it is their guard hook; exit 1 with «N jobs still running»); «other instances» also counts active-but-not-enabledagent-jobs@units.- Installer: env file parsed (
KEY=VALUEonly), never sourced;umask 077scoped to the env-file write; installer lock (flock -n); unit written via tmp + rename; warning on a non-defaultXDG_CONFIG_HOME.
Security
- Job-done line is printable-only (control/format characters dropped, whitespace → space, title ≤ 120 runes), so a title cannot type keystrokes into the session.
- Guard parses Bash commands and blocks git push/tag and gh release/api/pr/issue writes in any spelling (global options, quotes, paths, env prefixes, wrappers, substitutions,
sh -c,eval,-R, aliases). - Guard fails closed: a program not positively classified as data-only is rejected when a git/gh or shell word is among its arguments (closes awk
system(),vim -c '!…',docker exec,python3 -W ignore -c,rg --pre,man -Pand similar). Shell detection is case-insensitive and covers csh/tcsh/ash/mksh/fish;busyboxis a wrapper,nix-shell --runis parsed likesh -c. - gh is checked against a read-only allowlist; gh flags before the subcommand other than
-R/--repo/--hostnameare rejected;gh auth statusis not allowed. git commit -C/-c,--reuse-message,--reedit-message,--fixup,--squashand abbreviated long options are rejected;-t/--templatefiles are checked like-F; pipefail state is tracked; commit message and identity checks parse real arguments;pgrep -fis blocked only when its pattern matches its own loop.- File tools (Write/Edit/MultiEdit/NotebookEdit) cannot write inside
.gitor the job's own git dirs (symlinks resolved,..rejected). - Wrapper starts claude with a scrubbed git environment (job-only global config with the job identity, no system config, hooks/fsmonitor/ssh/credential helper/pager/editor overridden); the daemon's own git calls use the same overrides, and cleanup makes no patch when the repository config names a filter, diff command or textconv.