Skip to content

Custom Operator Tools (`[[tool]]` extension points)

Status: Open — design proposal (Phase 2, Agent Orchestrator Research Program)

Different teams want different operator-side actions: “run my repo’s lint script against the agent’s worktree”, “open my org’s issue tracker URL for the active issue”, “snapshot the agent’s data dir before a risky test”, “slack the team that the agent finished”. Today the only way to express these is to wrap jackin’ in shell aliases or external scripts.

multicode addresses this with a [[tool]] array: each entry is a hotkey that runs a command (or prompts for input and runs a command) when pressed in the TUI. Reserved keys (q, a, d, s) are off-limits; everything else is operator-defined.

  • It’s the difference between jackin’ being a fixed operator surface and an extensible one. The latter is what teams need to standardize on jackin’ instead of building custom CLI wrappers.
  • It pairs cleanly with agent runtime status and tag protocol: the captured tags become substitution arguments for the tool command, so a tool can be as simple as “open the issue URL the agent declared”.
  • It uses the same handler-resolution machinery as the operator handler system — different config surface, same primitives.

Sources:

  • No dedicated README section
  • Config — config.toml [[tool]] array (the example only shows one tool — bash on b — but the schema is the same)
  • Source — tui/src/app.rs (tool firing, reserved-key validation)
[[tool]]
type = "exec"
name = "Run lint"
key = "l"
exec = "bun run lint"
[[tool]]
type = "prompt"
name = "Slack the team"
key = "k"
prompt = "What did the agent ship?"

multicode has two type values:

  • exec — runs the command verbatim (shell-words parsed). The TUI invokes the tool with cwd = workspace dir. Output displayed in a modal.
  • prompt — opens an input modal asking the operator’s question, then runs an associated command with the operator’s answer interpolated. Times out at 300 seconds.

Reserved keys are documented; collisions error at config load.

Same shape, with two extensions: argument substitution from captured agent tags, and per-role scoping so a Rust role gets cargo shortcuts and a JS role gets bun shortcuts.

~/.config/jackin/config.toml
[[tool]]
key = "l"
name = "Run lint"
exec = "cargo clippy"
# Optional scope filter — only show this tool when the workspace is
# loaded against a matching role. Same selector grammar as
# [docker.mounts] scope.
scope = "the-architect"
[[tool]]
key = "i"
name = "Open emitted issue"
exec = "open {issue}"
# {issue} substitutes the agent's most-recent <jackin:issue> URL.
# Available substitutions: {repo}, {issue}, {pr}, {link}, {workspace},
# {instance}, {workdir}.
[[tool]]
key = "n"
name = "Note for ops log"
type = "prompt"
prompt = "What happened?"
exec = "echo {input} >> ~/jackin-ops.log"

type defaults to "exec". scope defaults to no filter (tool always visible).

jackin’s existing reserved keystrokes (the navigation set in the console, plus quit/exile/eject) are off-limits. Tool config validation errors at config load if a custom tool collides with a reserved key.

When the tool fires, jackin’ resolves {X} placeholders against the selected workspace’s runtime state:

  • {instance} — container name
  • {workspace} — workspace name
  • {workdir} — primary mount destination inside the container
  • {host_workdir} — the materialized host path (worktree if isolated)
  • {repo}, {issue}, {pr}, {link} — most-recent value from the tag protocol index
  • {input} — only for type = "prompt"; the operator’s response

Missing placeholders error at fire time with a helpful message (“no <jackin:issue> recorded for this instance”); they don’t silently expand to empty.

Tools run on the operator host, not inside the container. They get the operator’s environment plus a few injected variables matching the substitution set. If a team needs to run inside the container, they can docker exec from the tool command — explicit and intentional.

  • [[tool]] array at the operator-config level.
  • type = "exec" and type = "prompt" (default exec).
  • scope filter on role selector.
  • Substitution for {instance}, {workspace}, {workdir}, {host_workdir}, {input}, plus tag-protocol values once that ships.
  • Reserved-key collision detection at config load.
  • 300-second prompt timeout (matches multicode).
  • Tool firing routes through the operator handler system for command resolution and output rendering.
  • Per-workspace tool overrides. Operator-config-level only in V1.
  • Streaming output for long-running tools. V1 captures and displays on completion, like multicode.
  • Tool authoring helpers (operator-facing CLI for “scaffold a tool config from this command”). Defer until friction surfaces.
  • Tool emission of <jackin:*> tags (so a tool can update the per-instance tag index). Interesting; defer.
  • Tool composability (one tool invokes another). Out of scope.
  • Where does the tool’s stdout go? A modal that overlays the console matches multicode but interrupts flow. A scrolling log panel is friendlier but requires the console resource panel infrastructure. Recommended default: modal for V1, log panel later.
  • Do tools work outside the console? A jackin tool run <key> CLI command would be useful for scripting. Recommended: yes, V1.1; V1 ships console-only.
  • Substitution scope when no agent is loaded. If the operator picks a workspace but no agent has run yet, {instance} and {workdir} are unset. Recommended: fire-time error, never silent empty.
  • New module (e.g. src/console/tools.rs) — tool config + substitution + firing
  • src/config/mod.rs[[tool]] parsing
  • src/console/manager/state.rs — keybinding registration and modal rendering
  • The handler system module — output rendering routes through there