Skip to content

Jobs and configuration ​

This page describes the components that are neither agents, skills, hooks, scripts nor adapters: the background jobs, an agent workflow, the configuration files and the test queries of the skills. Most of them reach the user through a copy that the user makes once. harness install copies only Project.toml of this page. harness render prints a template of this page with the values of the profile.

The sections follow the purpose of the files:

  • the launch agents of macOS, in launchagents/, and their systemd --user units for Linux, in systemd/;
  • the agent workflow agent-workflows/repo-drift.js;
  • the settings template, the example profile and the example model tables;
  • the Julia environment of the scripts, Project.toml;
  • the linter configuration: fatou.toml and the files of ast-grep/;
  • the leak check, .gitleaks.toml;
  • the test queries of the skills, tests/skill-triggers/.

claude-autocommit, claude-autocommit-test.jl and claude-autocommit.plist ​

A macOS launch agent is a job that launchd, the service manager of macOS, runs for one user: on a schedule, at login, or as a server that it keeps alive. Each job is a plist file in ~/Library/LaunchAgents/. The plists in launchagents/ are templates: their label starts with the profile key launchd_prefix, and their paths hold {home}. The harness has no verb that installs them. harness render launchagents/<name> prints the rendered plist.

claude-autocommit is a bash script that commits the plan files and the project memories of ~/.claude/, which must be a git repository. It commits plans/ and projects/*/memory/ only, and pushes the local main to the branch of origin that CLAUDE_AUTOCOMMIT_BRANCH names, main when the variable is unset. The plist and the unit set it from the profile key autocommit_branch: main on the machine whose memory is the template, a branch of its own on each other machine. It stages each path by name and commits with a pathspec, so a file that another session staged stays staged. It skips a file that changed in the last 5 minutes, and refuses a rename or more than 20 deletions.

claude-autocommit [--dry-run] [--no-push] [--help]

claude-autocommit.plist runs the installed copy ~/.local/bin/claude-autocommit at load and every 15 minutes. Edit the script here, then copy it there again.

The script runs on macOS and on Linux, under bash 3.2 too. A lock directory stops a second run. On macOS the lock is ~/Library/Logs/claude-autocommit/lock, beside the job's log. On Linux it is $XDG_STATE_HOME/claude-autocommit/lock, or ~/.local/state/claude-autocommit/lock when XDG_STATE_HOME is unset or empty. claude-autocommit-test.jl runs the script on a fixture repository with a bare remote, on the system it runs on, and checks the branch that receives the push.

julia-update.plist ​

A launch agent that keeps the Julia installation and the Kaimon server current. It runs daily at 04:30, and not at login. The job runs ~/.local/bin/julia-update.jl, the installed copy of scripts/julia-update.jl, with the juliaup launcher ~/.juliaup/bin/julia. The scripts describes what the script does.

The job runs the installed copy and not the copy in ~/Research, because a session can write ~/Research and the job runs outside the sandbox. It sets JULIA_PKG_USE_CLI_GIT=true, so Pkg uses the git program, and a PATH for that git. It runs at a low priority, so it does not compete with an interactive Julia session. Its output goes to ~/Library/Logs/julia-update/. On Linux, the script runs as the systemd --user unit julia-update.service, whose output goes to the journal: journalctl --user -u julia-update.service.

The label is <launchd_prefix>.julia-update. Render the plist with harness render launchagents/julia-update.plist. harness leaks renders it with the example profile.

kaimon.plist ​

A launch agent that runs the Kaimon MCP server. Kaimon gives an agent a Julia session and tools for Julia code. The job starts at login, and launchd restarts it when it exits. It runs ~/.julia/bin/kaimon --headless --port 2828.

The working directory is ~/Research. Do not remove it: Kaimon reads its tool policy from .kaimon/tools.json in its working directory, and without it Kaimon offers every default tool. A restart waits at least 60 s, so a second server that cannot use the port does not restart many times a second. JULIA_APPS_JULIA_CMD names the juliaup launcher, so the job does not depend on one Julia release. KAIMON_QDRANT_MANAGED=off stops the background index.

OpenCode and oh-my-pi connect to this server at http://127.0.0.1:2828/. The update job julia-update.plist restarts it when its Julia release is deleted. Render the plist with harness render launchagents/kaimon.plist.

kaimon.service, julia-update.service, julia-update.timer, claude-autocommit.service and claude-autocommit.timer ​

The systemd --user units in systemd/ run the three jobs of the plists on Linux. Like the plists, they are templates of the profile: harness render systemd/<name> prints a rendered unit, and the harness has no verb that installs them. Copy each rendered unit into ~/.config/systemd/user/, run systemctl --user daemon-reload, and enable the server and the two timers:

systemctl --user enable --now kaimon.service julia-update.timer claude-autocommit.timer

Each unit describes the same job as its plist:

unitplisthow it runs
kaimon.servicekaimon.plistat login (WantedBy=default.target), restarted on exit after 60 s (Restart=always, RestartSec=60), in ~/Research
julia-update.service, julia-update.timerjulia-update.plistdaily at 04:30 (OnCalendar), not at login
claude-autocommit.service, claude-autocommit.timerclaude-autocommit.plistwhen the timer starts (OnActiveSec=0), then 15 minutes after each run (OnUnitActiveSec)

The program and its arguments, the environment and the PATH are those of the plist. Both timers set Persistent=true, so a calendar start that the machine missed while it slept runs when the timer starts again. The units are --user units only. Their output goes to the journal: journalctl --user -u <name>. julia-update.jl restarts the server as kaimon.service.

The jobs run outside the sandbox, so their PATH holds no directory that a session can write: each directory is one that the settings template keeps out of a session's writes, because no allowWrite entry covers it or a denyWrite entry names it, or is one of /usr/bin, /bin, /usr/sbin, /sbin, /usr/local/bin and /opt/homebrew/bin. harness test renders each unit and its plist with the example profile and checks all of this, through units.py of the harness command. CI renders each unit on ubuntu-latest and runs systemd-analyze verify --user on it. harness leaks renders them with the example profile.

repo-drift.js ​

A Workflow script of Claude Code that checks whether the repositories of the tree still hold the canonical copies of githooks/. agent-workflows/README.md explains the directory and its rules for a workflow script.

Call it with absolute paths of repositories below a Research/ directory:

Workflow({scriptPath: '<home>/Research/Harness/agent-workflows/repo-drift.js',
          args: ['<home>/Research/Packages/<name>', ...]})

The first stage runs one agent per repository, on a small model. It compares the two git hooks, the CI, dependabot, Documenter and TagBot files, and codecov.yml at the repository root with Harness/githooks/. It also reports core.hooksPath and whether .gitignore and CHANGELOG.md exist. A second stage reads each file that differs again, and confirms or overturns the finding. The result names each repository that returned nothing.

The script only reads. Check its result against githooks/verify-workflows.jl. To correct a drift, run harness workflows or harness githooks.

settings.proposal.json ​

The settings template, the policy source of all three frontends. It holds the Claude Code settings with placeholders such as {home}, {org} and {protected_dir}, which the profile fills in.

The template has the sections permissions, hooks, sandbox and modelSettings, and other keys of settings.json. These components read it:

  • harness install merges permissions, hooks, sandbox and modelSettings into ~/.claude/settings.json. It keeps every other key of that file, and does not install the other keys of the template;
  • harness settings surface, compare, twins and domains measure and check it;
  • harness permissions writes the OpenCode permission block from its ask and deny rules;
  • harness install writes the bash.patterns and the path list of oh-my-pi from it;
  • harness leaks renders it with the example profile.

Edit the template, never the installed settings.json, and commit it when you install it. Typical use gives the steps. The security model says which of its mechanisms is a boundary and which only prevents accidents.

profile.toml ​

The example of the private profile, examples/profile.toml. The profile holds every value that names a person, an institution or a machine, for example the home directory, the GitHub accounts and the strings that must never be in the repository. A template names a key as {key}.

Copy the file to ~/.config/research-harness/profile.toml, and write your values into the copy. harness --profile F and $RESEARCH_HARNESS_PROFILE name another path. The example uses the home /home/example.

The harness uses the example too. harness leaks renders every template with it, and the test cases use it as their dummy profile. So it must name every key that a template uses. Adapting the profile names each key and the verbs that read it.

models.toml ​

The example of the model tables, examples/models.toml. An agent or a skill names a tier in its model:: large, medium or small. The model tables map each tier to a model of each frontend.

[claude] gives the Claude Code model of each tier. OpenCode and oh-my-pi share every other table, so that their agents run on the same models: the model of each tier, a model for one agent, the variants, the reasoning effort, the councils, which copy an agent onto other models, and the context limits. [opencode] and [omp] hold the same sub-tables, and each of their keys replaces the shared key for that frontend alone. A table that names a tier by an old name, opus, sonnet or haiku, stops harness install with exit 2.

Copy the file to models.toml beside the profile. harness --models F and $RESEARCH_HARNESS_MODELS name another path. Only harness install reads it. The test cases use the example as it is. Adapting the profile describes each table.

Project.toml ​

The Julia environment of the scripts of the harness. It names the six packages that the scripts load beyond the standard library, with their [compat] bounds: ExplicitImports, JSON, JuliaFormatter, JuliaSyntax, TestEnv and YAML.

harness install --apply copies the file to ~/.local/share/research-harness/julia/, or to $RESEARCH_HARNESS_JULIA, and instantiates it there. Each script that loads one of the packages puts that environment on its load path. No Manifest.toml is committed.

The scripts never use this file in place. A session can edit the checkout, and the update job runs outside the sandbox, so it must install only what the installed copy names. Dependencies says what each package does.

fatou.toml ​

The global configuration of fatou, a linter for Julia code. fatou reads $XDG_CONFIG_HOME/fatou/fatou.toml, and does not look above the root of a git repository. Copy the file by hand, and copy it again after a change:

bash
cp ~/Research/Harness/fatou.toml ~/.config/fatou/fatou.toml

The file turns off the rule unused-import. That rule does not follow include, so in a package whose imports are in the module file, almost every finding is false. ExplicitImports answers that question instead.

The installed pre-commit hook runs fatou lint with this configuration, and prints its findings as advice. scripts/mutate.jl writes a configuration of its own and does not read this file.

research-sgconfig.yml ​

The configuration of ast-grep, a tool that searches code by its syntax tree, for the whole research tree. Copy it by hand to ~/Research/sgconfig.yml. ast-grep looks for sgconfig.yml in each parent directory, so the file applies to every repository of the tree.

Julia is not a language that ast-grep knows. The file registers julia as a custom language for .jl files, with the parser library Harness/ast-grep/julia.dylib, which build-julia.sh builds. It also names Harness/ast-grep/rules as the directory of the rules. The paths are relative to ~/Research, so the checkout must be at ~/Research/Harness.

build-julia.sh ​

A bash script that builds the Julia parser library of ast-grep. It fetches the tree-sitter-julia grammar at a pinned commit into a temporary directory. Then it compiles the grammar with cc into julia.dylib beside the script, and deletes the temporary directory.

bash
ast-grep/build-julia.sh

Run it once on each machine, and again after a change of the pinned commit. julia.dylib is ignored by git, so it is never committed. research-sgconfig.yml names the library.

rules/parse-error.yml ​

A rule of ast-grep for Julia code. It marks each place where tree-sitter-julia could not parse the code: a node of the kind ERROR. Its severity is info.

The rule tells you where a search with a pattern can miss code, because the syntax tree there is not correct. research-sgconfig.yml names ast-grep/rules/ as the directory of the rules, so ast-grep uses the rule in every repository of the tree.

.gitleaks.toml ​

The configuration of gitleaks, the tool that finds secrets in text. The pre-push hook of this repository and the leaks job of CI read it.

It keeps the default rules of gitleaks, and adds the rule home-path: a path below /Users/ or /home/ that names a user. The rule allows only the two example homes of the repository, /Users/me and /home/example. Every call of gitleaks passes --ignore-gitleaks-allow, so a gitleaks:allow comment has no effect.

Fix a false positive in the text. A new allowlist entry needs the approval of the user and its own line in CHANGELOG.md. harness leaks finds the private strings of the profile, which gitleaks does not know. Development describes both checks.

tests/skill-triggers ​

The queries of the triggering test of the skills, one TOML file for each skill of skills/, tests/skill-triggers/<skill>.toml. harness skill-triggers reads them.

A skill that the model can load has load, 10 queries that must load it, and near, 5 queries that must not. Each near entry names the skill that the query belongs to, or none. A skill with disable-model-invocation: true has one near query and no load. A query must not start with /, and must not occur twice in one file. A near entry must not name the skill of its own file. Another count or form exits 2.

fixtures/ holds recorded stream-json output of Claude Code sessions, with the private content removed, and some streams written by hand. The cases of harness test read them to check how a session's output is judged.

Run the test after you change the description of a skill. Typical use shows the commands.