Skip to content

Task automation

Discoverable, non-interactive commands for routine repository work.

Updated View as MarkdownView on Github

A coding agent should not have to read a CI workflow or hunt through package manifests to learn how to check a change. Every covered repository publishes one command interface for routine work and documents it in AGENTS.md.

Keep one repository command interface

Use the tool the repository already uses. The standard does not require just, make, or another wrapper merely to give an agent shorter commands.

  • Use native package or build commands when they already provide clear repository-level entry points.
  • Use a root task runner when one outcome spans packages, languages, or two or more ordered commands.
  • Put branching, loops, and platform handling in a checked-in script. Keep the public task entry point short.
  • Make CI call the same entry points that people and agents run locally.
  • Let provider-specific commands or skills point to the repository task. They must not become a second implementation.

These are all valid interfaces:

  • tradovate/servers uses direct sbt commands.
  • NT-NinjaTrader/ninja-web-trader uses root and package-level pnpm scripts.
  • NT-NinjaTrader/innolab-ada uses a root Justfile to compose Cargo and infrastructure checks.

Tell the agent which interface is authoritative:

AGENTS.mdmarkdown
## Commands

Use root `pnpm` scripts for repository-wide work. Use `pnpm --filter <package>`
for one workspace package. Scripts under `scripts/ci/` implement multi-step checks;
call them through `pnpm` instead of invoking them directly.

Publish an exact command contract

The Commands section of the root AGENTS.md lists every applicable outcome below. Give the exact command and a result the reader can observe. If an outcome does not apply, say why instead of silently omitting it.

Outcome Requirement
Discover commands List the available repository tasks without opening an interactive interface.
Build Produce the normal local artifact or compile every supported target.
Format Apply the repository formatter. Name this as a file-changing command.
Check formatting Check formatting without changing tracked files.
Lint or static analysis Run the standard language and repository linters.
Type-check Check types without requiring a production build when the stack supports it.
Check edited files Format and inspect named files after an agent edits them.
Check staged files Run the fast, non-mutating checks required before a commit.
Focused tests Select one test, package, module, or affected area.
Full tests Run the complete local test suite.
Generate and check Regenerate checked-in output and fail when it leaves an unexpected diff. Use only when the repository has generated files.
Full local verification Compose every local check required before handoff.
Clean build output Remove declared generated build output. This command is optional.

The command map belongs in AGENTS.md, even when the runner can list its own tasks:

AGENTS.mdmarkdown
## Commands

Run commands from the repository root.

| Outcome | Command | Done when |
| --- | --- | --- |
| List tasks | `just --list` | The repository recipes print and the command exits zero. |
| Build | `just build` | Every workspace package compiles. |
| Check formatting | `just fmt-check` | The command exits zero and leaves no diff. |
| Focused tests | `just test-package <package>` | Tests for the named package pass. |
| Full tests | `just test-all` | Both local test lanes pass. |
| Full local verification | `just verify` | Formatting, lint, and all local tests pass. |
| Type-check | Not applicable | Rust type-checking is part of `just check`. |

Test the command interface

Write automated tests for every new or changed canonical command before other people or agents use it. Exercise the public entry point, not only the script or function behind it.

The tests cover:

  • documented arguments and selectors;
  • a successful exit;
  • a failed child process and its preserved nonzero exit code;
  • missing or unsupported arguments;
  • documented output;
  • file changes for format, generate, and clean commands.

Use the test framework that fits the interface:

  • Test Bash commands with Bats.
  • Test TypeScript commands with Vitest or Jest.
  • Test Python commands with pytest.
  • Test Rust commands with the Rust test harness.
  • Test task-runner recipes through Bats or the repository’s standard test framework.

The example keeps the public Bash interface small and replaces pnpm with a controlled executable during the contract test:

These tests prove the wrapper contract. They do not prove that the real build, lint, or test command works. CI also runs each canonical command against the repository before the command ships for wider use.

Run checks before commits and pushes

Every covered repository uses a Git hook manager with checked-in configuration and installs its hooks during setup. Use pre-commit, Husky, or the tool already used by the repository. Configure both boundaries:

  • The pre-commit hook runs fast, non-mutating checks against staged files.
  • The pre-push hook runs full local verification.

Hooks call the canonical repository commands. Do not copy formatter, linter, or test logic into hook files. CI repeats the same checks because a missing local hook must not weaken a merge gate.

These configurations make the two boundaries explicit:

Document installation and behavior in AGENTS.md:

AGENTS.mdmarkdown
Local Git hooks:

- `pnpm install` installs the checked-in Husky hooks.
- Before each commit, `scripts/task check-staged` checks staged files without changing them.
- Before each push, `scripts/task verify` runs full local verification.
- Do not bypass either hook with `--no-verify`. Fix the failure.

Test the hook configuration before shipping it. The tests prove that setup installs both hooks, each hook calls the documented command, and a failed command blocks the Git operation.

Provide focused and full paths

A repository needs both. Running the whole monorepo for every edit wastes time. Running only one test before handoff misses failures elsewhere.

Focused commands accept the smallest useful selector for the stack:

  • a test file or class;
  • a package or module;
  • a workspace filter;
  • a changed-file set for formatting or linting.

The full verification command runs every local merge gate that applies to the repository. It may call other public tasks, but it must not hide a different implementation of those checks.

Document the selection syntax, including its easy-to-miss details:

AGENTS.mdmarkdown
Focused verification:

- One Web package: `pnpm --filter <package> lint`
- One test file: `pnpm --filter <package> test <path>`
- One Scala module: `sbt "project <module>" test`
- One Scala test class: `sbt "testOnly *<TestClassName>"`

Do not insert `--` before the Vitest path. Run the full local check with
`pnpm verify` before handoff.

Make commands safe for automation

Every canonical command:

  • runs from the repository root;
  • finishes without a prompt, watcher, editor, or graphical interface;
  • returns zero only when the requested outcome succeeds;
  • preserves a nonzero exit code from a failed child command;
  • prints the failed check and enough context to rerun it;
  • behaves the same in a terminal, a sandbox, and CI;
  • leaves tracked files unchanged unless its name says it formats or generates files.

Report skipped and blocked checks honestly. SKIP means the check does not apply to the selected change. A missing tool, credential, service, or network route is BLOCKED and returns nonzero.

Verification outputtext
PASS format-check
PASS lint
SKIP codegen: no schema files changed
BLOCKED integration-test: local MySQL is not ready

Verification failed. Start MySQL with `just services-start`, then run `just verify`.

Do not make the caller parse prose to learn whether verification passed. A summary is useful, but the process exit code remains the contract.

Keep destructive work out of verification

Build, test, lint, and verification commands do not deploy, publish, apply infrastructure, run migrations against shared systems, or delete source files. Put those operations behind separate commands and the approval rules in AGENTS.md.

An optional clean command removes only named build output and caches owned by the repository. It does not remove untracked source, local environment files, credentials, or data outside the repository.

AGENTS.mdmarkdown
Cleanup:

- `just clean-build` removes `target/` and `.cache/test/` only.
- It does not remove `.env`, untracked files, containers, volumes, or databases.
- Stop before any broader cleanup. Do not substitute `git clean`.

Reuse the commands in CI

CI invokes the same full verification entry point. It may split tests into documented shards, but each shard uses the same underlying task and selection syntax as local work.

Some gates need GitHub or a managed service. List them as CI-only checks in AGENTS.md. Do not claim the local verification command ran them, and do not add a fake local command that always skips them.

.github/workflows/verify.ymlyaml
- name: Run repository verification
  run: just verify

When a local task changes, update its CI caller and AGENTS.md in the same pull request.

Give every approved agent the same task

Claude Code, Codex, Cursor, and OpenCode use the same repository commands. Do not maintain one test command per provider. Their non-interactive launch syntax differs, but the task does not.

Do not add a permission-bypass flag merely to make an unattended run pass. Configure the agent’s approved permissions and let a denied action fail. When broader permissions are justified, run the agent in an approved sbx sandbox instead of removing host protections.

Check files after an agent edits them

Coding agents have lifecycle hooks that can run a repository command immediately after an edit. Configure every approved agent to call the same checked-in adapter. The adapter reads the provider’s event, finds the edited files, and calls the canonical edited-file command.

Keep this hook quick and file-scoped:

  • Run the formatter on the edited file.
  • Run file-scoped lint and static analysis, including complexity and likely-bug checks configured by the repository.
  • Print concise failures so the agent can correct the edit in the same turn.
  • Leave full tests and full verification to the normal task and pre-push paths.

Immediate feedback saves model turns and tokens. The agent sees a formatting, complexity, or correctness failure while the edit is fresh instead of finding it at the end and reading the file again.

The adapter requires Bash, Git, and jq. It accepts the provider name as its only argument and reads the hook event from standard input:

scripts/hooks/check-agent-editbash
#!/usr/bin/env bash
set -euo pipefail

provider="${1:-}"
payload="$(cat)"

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
repo_root="$(git -C "$script_dir" rev-parse --show-toplevel)"
paths=()

read_json() {
  local filter="$1"
  local error_message="$2"
  local value

  if ! value="$(jq --exit-status --raw-output "$filter" <<<"$payload")"; then
    printf '%s\n' "$error_message" >&2
    exit 2
  fi
  printf '%s' "$value"
}

add_patch_paths() {
  local patch="$1"
  local line

  while IFS= read -r line; do
    case "$line" in
      "*** Add File: "*) paths+=("${line#"*** Add File: "}") ;;
      "*** Update File: "*) paths+=("${line#"*** Update File: "}") ;;
      "*** Delete File: "*) paths+=("${line#"*** Delete File: "}") ;;
      "*** Move to: "*) paths+=("${line#"*** Move to: "}") ;;
    esac
  done <<<"$patch"
}

case "$provider" in
  claude)
    paths+=("$(read_json '.tool_input.file_path | strings | select(length > 0)' 'Claude Code hook event has no tool_input.file_path.')")
    ;;
  cursor)
    paths+=("$(read_json '.file_path | strings | select(length > 0)' 'Cursor hook event has no file_path.')")
    ;;
  opencode)
    tool_name="$(read_json '.tool_name | strings | select(length > 0)' 'OpenCode hook event has no tool_name.')"
    case "$tool_name" in
      edit | write)
        paths+=("$(read_json '.tool_input.filePath | strings | select(length > 0)' 'OpenCode edit event has no tool_input.filePath.')")
        ;;
      apply_patch)
        patch="$(read_json '.tool_input.patchText | strings | select(length > 0)' 'OpenCode apply_patch event has no tool_input.patchText.')"
        add_patch_paths "$patch"
        ;;
    esac
    ;;
  codex)
    patch="$(read_json '.tool_input.command | strings | select(length > 0)' 'Codex hook event has no tool_input.command.')"
    add_patch_paths "$patch"
    ;;
  *)
    printf 'Unsupported provider: %s\n' "$provider" >&2
    exit 64
    ;;
esac

checked_paths=()
for path in "${paths[@]}"; do
  if [[ "$path" == /* ]]; then
    candidate="$path"
  else
    candidate="$repo_root/$path"
  fi

  if ! path_dir="$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)"; then
    continue
  fi
  candidate="$path_dir/$(basename "$candidate")"

  if [[ "$candidate" != "$repo_root/"* ]]; then
    printf 'Edited path is outside the worktree: %s\n' "$path" >&2
    exit 65
  fi
  if [[ -L "$candidate" ]]; then
    printf 'Edited path is outside the worktree or is a symlink: %s\n' "$path" >&2
    exit 65
  fi
  if [[ ! -f "$candidate" ]]; then
    continue
  fi

  relative_path="${candidate#"$repo_root/"}"
  duplicate=false
  for checked_path in "${checked_paths[@]}"; do
    if [[ "$checked_path" == "$relative_path" ]]; then
      duplicate=true
      break
    fi
  done
  if [[ "$duplicate" == false ]]; then
    checked_paths+=("$relative_path")
  fi
done

if [[ ${#checked_paths[@]} -eq 0 ]]; then
  exit 0
fi

task="${CHECK_AGENT_EDIT_TASK:-$repo_root/scripts/task}"
cd "$repo_root"
if "$task" check-files -- "${checked_paths[@]}"; then
  exit 0
else
  task_status=$?
  printf 'Edited-file checks failed with exit %d. Fix the reported issues before continuing.\n' "$task_status" >&2
  exit 2
fi

Claude Code, Codex, and Cursor send the hook event as JSON on standard input. The OpenCode plugin converts its tool event to the same input. The adapter handles each provider’s schema, refuses paths outside the worktree, ignores deleted files, and calls a command such as scripts/task check-files -- <path...>.

The examples/task-automation folder contains the executable adapter, Bats tests, and sanitized events captured from controlled edits by all four approved agents. A smoke test compares the code block above with the executable file. If one changes without the other, the test fails.

Tell agents what the hook does and how to respond:

AGENTS.mdmarkdown
Agent edit hooks:

- After an edit, the repository formats and checks the files changed by the agent.
- Treat hook output as part of the edit result. Fix each reported issue before continuing.
- Do not run full verification after every edit. Run `scripts/task verify` before handoff.

Test the adapter with recorded events from all four providers. Cover multiple edited files, renamed and deleted files, paths with spaces, files outside the worktree, formatter changes, and linter failures. Agent hooks improve the editing loop. They do not replace Git hooks or CI.

References

Navigation

Type to search…

↑↓ navigate↵ selectEsc close