Skip to content

Releases: qwwiwi/agent-jobs

v0.4.0

Choose a tag to compare

@qwwiwi qwwiwi released this 18 Oct 23:31
5c54932

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) and doctor procs (report only) once per local day inside the daemon.
  • Codex runner (DEVIATIONS #5): gpt-* models or runner: codex run on codex exec; read-only kinds only unless codex_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/skills rating.
  • Autopilot: acceptance_cmd, risky-first review checklist, secret redaction in reports and logs, ## Progress section in the report contract, brief size check (max_brief_bytes, 32 KB).
  • Skills for jobs (E7 job side, DEVIATIONS #16): skill_roots catalogs, core skills per kind linked read-only into the worktree, skill_list, agent-jobs skills, stats by skill.
  • Configurable concurrency: max_running per agent (#33).
  • Clean-clone acceptance (#35): scripts/acceptance-clean-clone.sh replays 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 in prune.

Upgrade notes

  • Rebuild and reinstall the binary (go build -ldflags "-X main.version=v0.4.0" ... or scripts/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.db before the first start anyway.
  • max_running: integer 1..8, default 2 when absent (same behaviour as before). A value outside 1..8, null or 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_doctor and agent-jobs status may show the file's value before the daemon picks it up.
  • New features are opt-in: heartbeat.enabled false, http_api.enabled false, codex_write_allowed false, skill_roots empty.
  • 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_running 8.
  • 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

Choose a tag to compare

@qwwiwi qwwiwi released this 18 Oct 01:29

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 by scripts/check.sh and the pre-push hook: refuses commits not yet on origin/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-sdk v1.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_cancel over the existing Manager; job_result reads report.md only as a regular file (no FIFO, symlink or directory), capped at 256 KB.
  • Each job's mcp.json now names the job-role server instead of an empty stub.
  • Store columns complexity, runner, owner_waits, resumed_from, schedule_id, billing (tolerant migrations).
  • tmux_socket config: 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 against agent-jobs as a command and against changing AGENT_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 a sweep_incomplete marker and the daemon fails the job with «processes survived the job», which the job-done line repeats.
  • agent-jobs skill (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.sh installs 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 (backup SKILL.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 own skills/ 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 (config cards_dir), front matter repo: / 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: network host/path (host case ignored, path case kept, scheme/user/port/.git ignored) 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.md state 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 gets memory_search and memory_write only, 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), and token_env is dropped from the job's environment. A job's note carries tag agent-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 a decision. The job role refuses to start without AGENT_JOBS_JOB. The brain client follows no redirect and sends the token only to the configured endpoint; task_update reports «fields updated, but the status change … failed»; send_message cannot override from. New package internal/brain: Brain interface, HTTP Client (one short session per call, bounded by timeout_seconds), Disabled, and the test Fake.
  • Config block brain: recall_url, memory_url, tasks_url, swarm_url (https, plain http only on loopback), token_file (mode 600) or token_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 (path field or "<word>: <path>", vault-relative .md only); 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 its job_notes mark.
  • 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 hook examples/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; --kill ends 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's stderr.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 uses renameatx_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 and doctor procs --kill signal 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; config log_archive_dir (default <state>/archive) and log_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 print retention off: no marker in <root>; the default <state>/archive gets 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 tool job_stats summarize them (success rate, median and p90 cost and duration, top refusal rules, skills by uses and success ...
Read more

v0.1.0 — Agent Jobs

Choose a tag to compare

@qwwiwi qwwiwi released this 17 Oct 17:32

Added

  • SQLite job queue (jobs table) 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) and agent-jobs tick; launchd template with KeepAlive.
  • Launch exactly per the reference via a detached agent-jobs run-wrapper in 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_commit file.
  • 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 init writes 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=…", dev otherwise).
  • 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) and scripts/install-linux.sh (idempotent build/install/enable/restart, --uninstall, no sudo, current user only; stamps the binary with git describe --tags --always --dirty).
  • Tests for the unit template and the install script (fake systemctl, sandboxed HOME), including systemd-analyze --user verify of 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 in docs/REFERENCE.ru.md), public roadmap (docs/PLAN.md, docs/PLAN.ru.md), docs/DECISIONS.md, DEVIATIONS #11 (systemd user service on Linux), diagrams in docs/images/, MIT LICENSE.

Changed

  • Model table: review/fix/general light → sonnet, normal → opus, hard → fable; research light/normal → sonnet, hard → opus.
  • Reference timings fixed: daemon --interval removed (tick every minute), cleanup_delay_minutes removed (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 flock around tick and cancel; atomic claim with a launch token; a tick only finishes jobs it owns.
  • Claim before spawn; the wrapper registers in wrapper.json before running claude and holds wrapper.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.
  • --uninstall keeps 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-enabled agent-jobs@ units.
  • Installer: env file parsed (KEY=VALUE only), never sourced; umask 077 scoped to the env-file write; installer lock (flock -n); unit written via tmp + rename; warning on a non-default XDG_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 -P and similar). Shell detection is case-insensitive and covers csh/tcsh/ash/mksh/fish; busybox is a wrapper, nix-shell --run is parsed like sh -c.
  • gh is checked against a read-only allowlist; gh flags before the subcommand other than -R/--repo/--hostname are rejected; gh auth status is not allowed.
  • git commit -C/-c, --reuse-message, --reedit-message, --fixup, --squash and abbreviated long options are rejected; -t/--template files are checked like -F; pipefail state is tracked; commit message and identity checks parse real arguments; pgrep -f is blocked only when its pattern matches its own loop.
  • File tools (Write/Edit/MultiEdit/NotebookEdit) cannot write inside .git or 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.