@peezy.tech/jaeger
v0.1.4
Published
Prompt-first workflows across native agent harnesses
Maintainers
Readme
Jaeger
Jaeger is a prompt-first workflow runner for coordinating native coding-agent harnesses. A workflow is an ordinary JavaScript or TypeScript file: normal control flow connects structured agent outputs while Jaeger owns routing, concurrency, durable run state, and inspection.
The npm distribution is named @peezy.tech/jaeger; the repository is
peezy-tech/jaeger, and the installed command is jaeger. This project is not
affiliated with the
CNCF Jaeger distributed tracing project.
Jaeger's source is MIT licensed. Provider SDKs, native harnesses, accounts, and
services remain subject to their own terms; see
THIRD_PARTY_NOTICES.md.
Jaeger does not wrap capabilities the harness already provides. Native harnesses continue to own commands, edits, tools, skills, MCP servers, hooks, and provider behavior.
Jaeger can also reconcile the environment around those native harnesses. This is
separate from workflow execution: jaeger env composes instruction files and
installs provider-native skills, Codex/Claude plugins, Pi packages, and
configuration from a declarative, reversible environment.
Instruction composition is provided by the standalone
mdcsp package and
consumed through its library API; Jaeger retains ownership of environment
targets, native assets, state, backups, and reconciliation.
export const meta = {
name: "mixed-review",
phases: [
{ title: "Review", detail: "Two independent reviews" },
{ title: "Synthesize", detail: "Reconcile their findings" },
],
}
phase("Review")
const reviews = await parallel([
() => agent(`${reviewPrompt}\nDo not modify files.`, {
harness: "codex",
effort: "low",
schema: FINDINGS,
}),
() => agent(`${reviewPrompt}\nDo not modify files.`, {
harness: "claude",
effort: "low",
schema: FINDINGS,
}),
])
return agent(`Reconcile these reviews: ${JSON.stringify(reviews)}`, {
harness: "codex",
schema: VERDICT,
})Current capabilities
.jsand.tsworkflow files with top-levelawaitandreturnagent,parallel,phase,log,inputs, and immutabletriggerglobals- persistent Codex app-server threads, Claude Agent SDK sessions, and Pi RPC sessions
- host-configured harness surfaces over any shipped protocol driver
- user-controlled session resume plus active-turn steer and interrupt
- model, effort, service-tier, profile, working-directory, schema, label, and timeout selection per agent
- JSON Schema structured-output validation
- pinned source and inputs with durable completed-call replay
- fail-closed uncertainty for a call that started but did not durably complete
- a persistent same-user local backend with transient, independently supervised per-run and per-session-turn workers
- named remote runtimes over existing OpenSSH host profiles, with no Jaeger TCP listener, tunnel, key store, or credential protocol
- a Windows controller-only CLI that sends locally authored workflows to Linux runtimes through Windows OpenSSH
- persistent-backend schedules with immutable revisions, idempotent occurrences, explicit activation, overlap and misfire policy, and fail-closed pausing
- operator-configured post-checkpoint lifecycle hooks with stable event IDs, durable at-least-once delivery, bounded direct commands, and independent failure state
- blocking or detached CLI attachment with compact inspect, wait, list, resume, and stop controls
- a bare-command system overview with harness versions, backend health, active workflows and sessions, schedules, and environment state
- one packaged, optional
jaeger-workflowsauthoring and operations skill - declarative Codex, Claude, and Pi environments with drift detection, backups, and reversible instruction, skill, plugin/package, and config installation
The compiler requires one export const meta declaration and otherwise expects a small,
prompt-heavy coordinator. Imports and other exports are intentionally unsupported.
Develop and verify
Linux runtime hosts require Node.js 22.13 or newer, pnpm for development, cgroup v2, and an available user systemd manager. Jaeger runs coordinators and every provider call in transient user scopes so lifecycle control remains authoritative across CLI exits, backend restarts, owner crashes, and new-session descendants. Windows 10 and Windows 11 are supported as controller-only clients with Node.js and the built-in OpenSSH client; execution still belongs to a prepared Linux runtime.
For a fresh machine, give an agent the raw GitHub URL for AGENT_INSTALL.md. That runbook detects the controller/runtime role, installs an exact source artifact per-user, preserves existing Jaeger configuration, exposes the packaged skill, and proves the installed path with a provider-free workflow.
Registry consumers can install the exact release into a user-owned prefix:
Jaeger 0.1.0 used the unscoped package name jaeger-workflows. Uninstall that
package from the same prefix before installing the scoped package so npm can
replace its jaeger executable shim.
npm uninstall --global --prefix "$HOME/.local" jaeger-workflows
npm install --global --prefix "$HOME/.local" @peezy.tech/[email protected]
export PATH="$HOME/.local/bin:$PATH"
hash -r
jaeger backend install
jaeger backend status
jaeger doctor$InstallRoot = Join-Path $env:LOCALAPPDATA "Jaeger"
npm uninstall --global --prefix $InstallRoot jaeger-workflows
npm install --global --prefix $InstallRoot @peezy.tech/[email protected]
$env:Path = "$InstallRoot;$env:Path"
jaeger --helpPersist $HOME/.local/bin on Linux or %LOCALAPPDATA%\Jaeger on Windows in
the user's PATH if it is not already present.
pnpm install --frozen-lockfile
pnpm verifyThe verifier starts from clean build output, checks and tests the project, validates every example and the skill, packs the npm artifact, installs that tarball into a temporary project, and exercises the installed binary. It does not publish or globally install anything.
Run jaeger without arguments for the human-readable operational overview.
The display is colorized in a terminal and stays useful when a subsystem is
unavailable by reporting degraded sections as warnings. Agents and scripts can
read the same snapshot without parsing terminal output:
jaeger status --json
jaeger --helpFor individual commands:
pnpm build
pnpm jaeger backend install
pnpm jaeger doctor
pnpm jaeger validate examples/mixed-review.js
pnpm jaeger run examples/mixed-review.js \
--input examples/mixed-review-input.jsonAcceptance and failure diagnostics are written to stderr. Machine-readable
command results are written to stdout; durable phase and agent progress is
available through targeted inspect --events calls. If a blocking run fails,
stderr also includes a one-line
jaeger run summary: {...} record with the durable run ID, uncertainty boundary,
and exact inspect command.
Harness environments
Jaeger environments use mdcsp's canonical configuration home and profile list:
$MDCSP_HOME, then $XDG_CONFIG_HOME/mdcsp, then ~/.config/mdcsp. A profile
is an environment's instruction authority, so mdcsp render workstation and
jaeger env apply workstation select the same ordered snippets. The minimal
layout is:
~/.config/mdcsp/
├── profiles/
│ ├── workstation.toml
│ ├── workstation-claude.toml
│ └── workstation-pi.toml
├── snippets/
│ ├── common.md
│ ├── codex.md
│ ├── claude.md
│ └── pi.md
└── jaeger/
└── workstation/
├── environment.toml
├── harnesses.json
├── skills/
└── configs/profiles/workstation.toml is an ordinary mdcsp profile:
version = 1
name = "workstation"
snippets = ["common", "codex"]The jaeger/<name>/environment.toml sidecar is optional. It adds native
provider assets and can map Claude or Pi to other mdcsp profiles without owning
Markdown composition itself. Sources are relative to the sidecar and must
remain inside its directory:
version = 1
name = "workstation"
harness_config = "harnesses.json"
[providers.codex]
skills = ["skills/jaeger-workflows"]
plugins = ["github@openai-curated"]
[[providers.codex.configs]]
source = "configs/tool.toml"
target = "$CODEX_HOME/tool.toml"
[providers.claude]
profile = "workstation-claude"
[providers.pi]
profile = "workstation-pi"
skills = ["skills/jaeger-workflows"]
packages = ["npm:@acme/[email protected]"]
[[providers.pi.configs]]
source = "configs/pi-settings.json"
target = "$PI_CODING_AGENT_DIR/settings.json"Without a sidecar, Jaeger manages only Codex instructions from the profile with
the environment's name. Explicit --file paths continue to accept the previous
standalone Jaeger environment-manifest format for controlled migrations, but
the default discovery path is exclusively mdcsp profiles/.
To migrate an existing Jaeger environment, copy its Markdown snippets into the
mdcsp snippets/ directory, express their order in profiles/<name>.toml, and
move only the remaining skills/plugins/packages/config entries into the
optional sidecar. Keep the old environment directory until both
mdcsp explain <name> and jaeger env inspect <name> show the intended plan;
then jaeger env apply <name> safely replaces the previously managed output
and records the profile as the new manifest authority.
Codex instructions target $CODEX_HOME/AGENTS.md (normally
~/.codex/AGENTS.md); Claude instructions target
$CLAUDE_CONFIG_DIR/CLAUDE.md (normally ~/.claude/CLAUDE.md); Pi instructions
target $PI_CODING_AGENT_DIR/AGENTS.md (normally ~/.pi/agent/AGENTS.md).
Skills are copied into each provider's native skills/ directory. Codex and
Claude plugins use
explicit plugin@marketplace selectors and are installed or removed through
the provider's native plugin manager (codex plugin or claude plugin), so
Jaeger does not create a second plugin loader. Pi packages retain their native
source identity and are reconciled through pi install, pi list, and
pi remove; packages that predate the environment are never removed by it. An
npm:, git:, HTTPS, or SSH source is stored verbatim; a local ./ source is
resolved inside the environment directory before it is installed. An
optional top-level
harness_config = "harnesses.json" installs
Jaeger's custom harness registry at the standard Jaeger config path.
Instruction snippets are Markdown with optional TOML front matter:
+++
description = "Only use this block when Jaeger is available"
requires = ["jaeger"]
excludes = ["some-incompatible-command"]
+++
## Jaeger
Use Jaeger for durable workflow execution.Jaeger calls the mdcsp library directly and uses its profile loader, snippet
directory, ordering, conditions, digest, and generated header. It does not
shell out to the executable or maintain a parallel instruction home. Jaeger's
private apply/uninstall state and backups remain under
$XDG_STATE_HOME/jaeger/environment because mdcsp owns no lifecycle state.
Inspect and reconcile an environment with:
jaeger env list
jaeger env inspect workstation
jaeger env diff workstation
jaeger env apply workstation
jaeger env check workstation
jaeger env status
jaeger env uninstall workstationapply refuses to replace unmanaged targets or overwrite locally modified
managed targets. Use --force only after reviewing inspect or diff. The
first forced adoption preserves existing content under Jaeger's private state
root. Switching environments restores targets no longer managed by the new
environment; uninstall restores all preserved originals and removes targets
that Jaeger created. State is stored under
$XDG_STATE_HOME/jaeger/environment (falling back to
~/.local/state/jaeger/environment). check and diff exit with status 1 when
the requested environment is not the active, byte-current environment.
Use --file, --config-root, and --state-root for explicit local testing or
non-default layouts. These commands do not require the workflow backend to be
running.
Persistent local backend
On Linux, the installed CLI resolves the persistent local backend by default.
On Windows, it resolves the configured default SSH runtime. Operational
commands—run, inspect, wait, stop, resume, list, doctor, and every
session action—cross one versioned Unix-socket protocol. The backend owns
submission, run discovery, recovery, session control, and artifact reads. CLI
users normally need only a run ID; they do not need to preserve a state path or
stay in the directory where a run began.
jaeger backend install writes and enables a user unit named
jaeger-backend.service. By default it listens at
$XDG_RUNTIME_DIR/jaeger/backend.sock and stores runs under
$XDG_STATE_HOME/jaeger/runs (falling back to ~/.local/state/jaeger/runs).
Installation persists the selected socket, state root, entrypoint, optional
harness, lifecycle-hook, and runtime-module configurations, stable backend instance identity,
random service generation, and absolute systemctl launcher in
$XDG_CONFIG_HOME/jaeger/backend.json (falling back to
~/.config/jaeger/backend.json), so a custom installation remains the target of
later plain CLI commands. JAEGER_SOCKET can temporarily redirect a client.
JAEGER_STATE_DIR selects the root when installing or directly starting a
backend; it does not retarget an already-running service. The socket, state
root, and their runtime parents are user-only. The profile itself must be a
user-owned, owner-only regular file. Profile, socket, state, and user-unit paths
must have real, non-replaceable ancestor chains owned by root or the current
user; the CLI fails closed instead of following writable or symlinked routing
authority.
jaeger backend status
jaeger backend restartThe service and workers have separate lifetimes. Restarting the control plane does not terminate an active workflow or continued session turn. On startup, the backend reconciles its atomic submission index, publishes any durably claimed staged run, relaunches only safe pending or interrupted workflows, and launches only session turns that were queued but never claimed by a worker. It never replays a claimed session turn or a workflow at an uncertain agent boundary. A client disconnect stops waiting, not execution.
If the configured socket is unavailable, the CLI fails with an actionable
error; it never silently starts the same submission under another authority.
--runtime embedded or JAEGER_RUNTIME=embedded is an explicit development and
legacy escape hatch. Embedded mode still uses durable state and transient
workers, but has no persistent control plane. --state-dir remains an advanced
same-host override for embedded compatibility. The persistent service owns one
configured state root and rejects per-command alternate roots so every accepted
run remains discoverable and recoverable.
The installed unit pins Node, the CLI entrypoint, and the resolved systemd-run
and systemctl launchers used by both the service and later lifecycle commands.
Resolved service and harness paths must be owned by root or the current user and
must not be writable by other users or a shared group; ancestor directories are
checked under the same rule. Each accepted run also pins
absolute harness launcher paths instead of resolving codex, claude, pi, or a
custom command later from a mutable PATH. Rerun
jaeger backend install after moving or replacing the installation so the unit
and running service use the intended build. Reinstallation preserves an existing
harness profile unless another --harness-config is supplied. Installation
serializes installers with a generation-fenced process lease and durably records
a pending profile before changing systemd. The replacement service can bind both
the old and new socket during handoff, but rejects every operation except ping
until its exact generation is atomically committed in the routing profile. CLI
requests carry the committed generation, so a crash or stale client fails closed
instead of submitting into a different state root. A later install continues the
durable pending intent; a failed pre-commit update restores the prior unit,
profiles, and exact persistent/runtime enabled plus active lifecycle state. After
commit, the temporary compatibility socket is retired. Before changing systemd,
installation also verifies that any existing unit is the exact current-user
regular unit at Jaeger's unit path with a stable active/inactive and supported
enabled state. A foreign, masked, symlinked, transient-state, or unsupported
same-named unit is refused instead of shadowed or approximately restored.
Provider launchers inherit the user systemd manager's environment plus a
normalized absolute PATH captured at installation, so credentials that exist
only in an interactive shell must be imported into that manager or owned by the
provider's normal on-disk configuration.
Remote runtimes over SSH
A named remote runtime is the same persistent local backend viewed through an
SSH transport. The remote host still listens only on its owner-only Unix socket.
For each CLI operation, Jaeger opens one noninteractive SSH channel, runs the
hidden jaeger __rpc-stdio bridge, and forwards one versioned request through
that bridge to the remote socket:
local jaeger -> OpenSSH Host profile -> remote jaeger stdio bridge
-> remote backend.sock -> remote transient workersInstall and verify Jaeger on the target host first. Configure its keys, user, hostname, port, jump hosts, host-key policy, and optional connection sharing in the normal OpenSSH configuration. Jaeger never reads or stores those settings. Register the already-working SSH destination and optionally make it the persistent default:
jaeger runtime add build --ssh build-host --default
jaeger runtime list --json
jaeger runtime doctor build
jaeger doctorRegistration performs a protocol and doctor handshake, captures the remote
backend's stable instance ID, and writes a strict owner-only registry at
$XDG_CONFIG_HOME/jaeger/runtimes.toml (falling back to
~/.config/jaeger/runtimes.toml) on Linux, or an inherited-user-ACL registry at
%APPDATA%\Jaeger\runtimes.toml on Windows. The ID binds the name to one remote
state authority. Changing a backend's state root creates a new ID and requires
an explicit runtime add --force; ordinary reinstalls against the same state
root preserve it.
Windows installations are deliberately controller-only. They do not provide
backend install, embedded execution, or local systemd workers. The CLI uses
the built-in ssh.exe, including %USERPROFILE%\.ssh\config, to reach the
selected Linux runtime. jaeger runtime default NAME persists a target so bare
jaeger and ordinary operational commands route there without an environment
variable. Local workflow and input paths use Windows semantics; every target
--cwd and target harness path remains an absolute POSIX path.
Runtime entries contain only an OpenSSH destination, a constrained remote Jaeger executable name or absolute path, the pinned instance ID, and a connect timeout. They cannot contain private-key paths, passwords, arbitrary SSH arguments, or shell fragments. The client keeps normal host-key verification, uses batch mode without a TTY, disables agent and port forwarding for the transport command, and fails closed on unexpected stdout or identity mismatch.
Launch local workflow source against an existing directory on the remote host:
jaeger --runtime build run ./workflows/review.js \
--input ./review-input.json \
--cwd /srv/checkouts/project \
--detachThe workflow and input files are read and validated locally, then their contents
are transmitted and pinned in the remote run. --cwd is required, must be an
absolute path on the target, and is resolved and identity-pinned by the remote
backend. Jaeger does not copy or synchronize a checkout. Git, deployment,
rsync, shared storage, and other workspace preparation remain separate
operator concerns.
Remote responses include both the ordinary runId and a target-qualified
runRef. Exact action commands retain the target without another flag:
jaeger inspect build:20260724120000-0123456789 --summary --json
jaeger wait build:20260724120000-0123456789
jaeger stop build:20260724120000-0123456789JAEGER_RUNTIME=build or --runtime build also selects the target for run
discovery, schedules, hooks, doctor, and session operations. A qualified run
reference overrides the environment default, but conflicts with an explicit
different --runtime. Physical cwd, run, scratch, and transcript paths in a
remote response are target-local diagnostics.
Remote schedule manifests are read locally, but their cwd must likewise be an
absolute target path. Source and inputs are pinned in the remote schedule
revision. Backend installation and restart remain deliberately host-local
administration; perform them in an interactive SSH shell instead of through the
workflow transport.
An SSH failure never falls back to the local backend. A disconnected wait
stops only client attachment. Run submission, session continuation, schedule
application, and manual schedule triggering retain their existing idempotency
IDs across ambiguous transport failure, so retry the exact target-qualified
command rather than creating replacement work blindly.
Lifecycle hooks
Lifecycle hooks observe Jaeger-owned durable state after it is recorded. They are global operator configuration, not authored workflow commands, and are separate from provider-native hooks. A hook cannot intercept tools or influence workflow decisions.
version = 1
[[hooks]]
name = "hq-markdown"
events = [
"run.accepted",
"phase.changed",
"agent.completed",
"run.terminal",
"schedule.changed",
"schedule.occurrence",
]
command = ["node", "/home/user/.local/libexec/jaeger-hq-markdown.mjs"]
timeout_ms = 5000Validate and install the private, user-owned configuration explicitly:
jaeger hooks validate ~/.config/jaeger/hooks.toml
jaeger backend install --hooks-config ~/.config/jaeger/hooks.toml
jaeger hooks status --json
jaeger hooks history --limit 50 --jsonEach direct command receives one bounded normalized JSON event on standard input. Prompts, transcripts, arbitrary results, agent outputs, and provider metadata are excluded. A zero exit status acknowledges delivery. Nonzero exits, timeouts, output-limit violations, and backend restarts retain retryable state with the same event ID and capped backoff. Hook failure never changes a run or makes a completed workflow uncertain. Embedded mode validates configuration but does not dispatch hooks.
Runtime modules
Runtime modules are trusted JavaScript loaded into the persistent backend process. They are machine-owned composition, not workflow input or a provider plugin system. A normal ESM project can import arbitrary packages and export a small module list:
import { telegramModule } from "./telegram.mjs";
export default {
version: 1,
modules: [telegramModule({
tokenFile: new URL("./secrets/telegram-token", import.meta.url),
})],
};Each module registers durable lifecycle-event consumers and background services. Its runtime context provides run/session inspection, forked session queries, namespaced JSON storage, and logging. Module code shares the backend PID and trust boundary. Handler error containment is reliability handling, not a security sandbox.
Modules can be copied from a source registry into the one operator-owned
runtime project. Like source-component registries, items remain readable and
editable after installation. Their dependency requirements are reconciled into
one root package.json, one package-manager lockfile, and one node_modules;
module directories are not independent package installations.
Inspect or install the built-in catalog without executing module code:
jaeger modules view telegram
jaeger modules add telegram voice-spike --dry-run
jaeger modules add telegram
jaeger modules list
jaeger modules diff telegrammodules add accepts a local manifest, HTTPS manifest or catalog reference,
and OWNER/REPOSITORY/ITEM#REF. GitHub refs resolve to a commit before files
are fetched. Multi-item additions perform one dependency installation with
package lifecycle scripts disabled. modules.lock.json records source and file
hashes plus dependency ownership. modules remove refuses locally edited
modules and, because runtime configurations are arbitrary ESM, requires
--force whenever jaeger.runtime.mjs exists after the operator removes the
module from that composition.
Installing source does not activate it. Review and configure the files, then cross the existing explicit execution boundary:
jaeger modules validate ~/.config/jaeger/runtime/jaeger.runtime.mjs
jaeger backend install \
--runtime-config ~/.config/jaeger/runtime/jaeger.runtime.mjs
jaeger modules status --jsonConfiguration loads on backend start. Reinstall or restart after edits.
jaeger backend install --clear-runtime-config removes a broken composition.
The source schemas are under schemas/, the catalog is
jaeger.registry.json, and reference
items are under examples/runtime-modules/.
Run lifecycle
Every run is backend-owned and may outlive the invoking shell or parent-agent
turn. Without --detach, the CLI waits for the terminal result. With
--detach, it returns immediately after durable acceptance:
jaeger run examples/mixed-review.js \
--input examples/mixed-review-input.json \
--detach--detach changes only client attachment; it does not select a backend or make
the run more durable. Preserve the returned runId and action commands rather
than continuously polling or ingesting full transcripts.
{
"schemaVersion": 1,
"backend": { "name": "local", "kind": "local-service" },
"runId": "20260720194512-0123456789",
"submissionId": "142c8ce2-51cb-448d-8f83-f9fbe3ad2d60",
"status": "running",
"scriptPath": "/workspace/examples/mixed-review.js",
"runDir": "/home/user/.local/state/jaeger/runs/20260720194512-0123456789",
"inspect": "jaeger inspect 20260720194512-0123456789 --summary --json",
"wait": "jaeger wait 20260720194512-0123456789",
"stop": "jaeger stop 20260720194512-0123456789"
}The CLI creates and prints the submissionId before contacting the backend.
The service atomically binds that ID to one normalized request and one staged
run before publication. If transport fails after possible acceptance, retry the
same command with --submission-id ID, or resolve it without creating work:
jaeger submission 142c8ce2-51cb-448d-8f83-f9fbe3ad2d60Reusing an ID with different source, inputs, workspace, harness definitions, or runtime policy is rejected as an idempotency conflict.
Operate the run through the CLI:
jaeger inspect 20260720194512-0123456789 --summary --json
jaeger wait 20260720194512-0123456789
jaeger stop 20260720194512-0123456789
jaeger list --jsonEvery launched agent step also has a durable Jaeger session. List them by run, then use the exact command returned for the session's current state:
jaeger session list 20260720194512-0123456789 --json
jaeger session inspect 20260720194512-0123456789 session-0123456789abcdef --json
jaeger session resume 20260720194512-0123456789 session-0123456789abcdef --message -
jaeger session steer 20260720194512-0123456789 session-0123456789abcdef --message -
jaeger session interrupt 20260720194512-0123456789 session-0123456789abcdef
jaeger session query 20260720194512-0123456789 session-0123456789abcdef \
--message -resume starts another turn in an idle provider session. steer and
interrupt address the active turn owned by a running workflow or resume
command. A session selector may be the Jaeger session ID, workflow step ID, or
native provider session ID. Codex applies steering to its active app-server
turn. Claude Code's native SDK has interrupt but no equivalent append-to-active-
turn operation, so Jaeger steering interrupts the current Claude response and
immediately continues with the steering message in the same persisted session.
Pi maps steering and interrupt directly to its RPC commands and waits for
agent_settled, after automatic retries, compaction retries, and queued
continuations have finished.
The persistent backend archives idle Codex threads after a run completes
successfully, so workflow workers do not accumulate in the default Codex app
thread list. Failed, interrupted, stopped, uncertain, active, or pinned threads
remain visible. Jaeger also retains a parent thread when archiving it would
cascade into a descendant that Jaeger does not own. Session list and inspect
responses expose the resulting threadArchive state. A later Jaeger resume or
fork transparently unarchives the provider thread first; the durable Jaeger run,
session record, outputs, and transcripts are never removed.
Under the persistent backend, each resume is first recorded as a durable turn
job and then executed by its own transient systemd worker. The CLI prints the
turn ID before submission. Use --detach to return after worker acceptance,
--request-id TURN_ID to retry the same ambiguous submission, and the returned
commands to inspect or wait:
jaeger session turn inspect 20260720194512-0123456789 turn-0123456789abcdef
jaeger session turn wait 20260720194512-0123456789 turn-0123456789abcdefA backend or client exit never aborts that worker. The worker durably claims the
turn before provider interaction and writes its result before making the
session idle. If it disappears after the claim without a result, Jaeger records
the turn as uncertain and never relaunches it, avoiding duplicated provider
effects. If two distinct IDs contend for the same next turn, exactly one is
accepted; the loser is durably rejected rather than left as a phantom queued
job and has no provider effects.
session query is separate from resume: it forks the latest native provider
session and asks the question in a read-only child conversation. The workflow
session's turn count, provider ID, and result remain unchanged. Query
submissions have their own durable request IDs, transient workers, inspect/wait
commands, and uncertainty boundary. A later question creates a fresh fork so it
sees the latest parent state. Pi queries require the parent session to be idle
because Pi can only fork a session after its durable session file exists.
Start with a summary and request detailed events or transcripts only for the
current decision. wait exits when the run reaches a terminal state. stop
empties each OS-owned provider cgroup before it returns, including descendants
that created a new process session. If a worker had started, its possible
external effects remain uncertain.
Standing schedules
A schedule is persistent-backend-owned standing authority to create ordinary Jaeger runs at matching times. Embedded mode deliberately rejects schedule operations because an embedded process cannot own future time. Cron is the first trigger type; it is not a second workflow runtime.
Schedule manifests are ordinary TOML files. Relative workflow, input, and working-directory paths resolve from the manifest directory:
version = 1
name = "weekday_triage"
workflow = "./workflows/triage.ts"
cwd = "."
input = "./inputs/triage.json"
max_concurrency = 2
[trigger]
type = "cron"
expression = "0 9 * * 1-5"
timezone = "America/New_York"
[policy]
overlap = "forbid"
misfire = "skip"
max_catch_up = 1
pause_on_uncertain = true
pause_after_failures = 3Jaeger accepts five-field minute, hour, day-of-month, month, and day-of-week expressions with lists, ranges, and steps. Timezones are explicit IANA names. When day-of-month and day-of-week are both restricted, either may match, as in traditional cron. Every matching real UTC minute is evaluated in the requested timezone, including both repeated local minutes during a fall-back transition.
Validate the file, apply an immutable disabled revision, then explicitly grant standing authority:
jaeger schedule validate ./weekday-triage.toml
jaeger schedule apply ./weekday-triage.toml
jaeger schedule enable weekday_triageschedule apply --activate combines the last two operations when immediate
activation is intentional. Applying a changed launch template creates a new
disabled revision. An identical application is idempotent and preserves the
existing revision and activation state.
Operate schedules and occurrence history through the same backend:
jaeger schedule list --json
jaeger schedule inspect weekday_triage --json
jaeger schedule history weekday_triage --limit 20 --json
jaeger schedule trigger weekday_triage --json
jaeger schedule disable weekday_triage
jaeger schedule remove weekday_triageManual triggers use the same immutable revision and occurrence admission path.
The CLI prints a request ID before submission; retry an ambiguous request with
the same --request-id. Removal requires the schedule to be disabled and leaves
its revision and occurrence history as a tombstone. Reapplying the same name
creates a new disabled revision while retaining that history.
Each occurrence is durably claimed before submission and uses a deterministic
submission key, so service restarts and concurrent recovery cannot create two
authoritative runs for one firing. A scheduled run receives immutable metadata
through the coordinator's trigger global:
export const meta = { name: "scheduled triage" }
log({
schedule: trigger.scheduleId,
revision: trigger.revision,
scheduledFor: trigger.scheduledFor,
})
return await agent("Triage the current repository state.", { harness: "codex" })Ordinary launches expose trigger as undefined. Scheduled source, inputs,
harness definitions, workspace identity, cron expression, timezone, and policy
are pinned in the schedule revision; every occurrence then gets a normal run
record, run ID, checkpoint journal, sessions, and inspection commands.
The default overlap policy is forbid. A cron firing that overlaps an active
occurrence is recorded as skipped; an explicit manual trigger reports the
conflict. misfire = "skip" ignores times missed while the backend was down,
latest admits only the latest match, and catch-up admits at most
max_catch_up matches, capped at 24 within a seven-day scan window.
Jaeger never automatically retries an uncertain occurrence. Uncertainty or an
interrupted run pauses the schedule immediately. Repeated failed or stopped
runs pause it at pause_after_failures. Reconcile the affected run, then use
schedule enable to grant standing authority again. Disabling or removing a
schedule never stops an already admitted run.
Exact resume and uncertainty
Workflow source, inputs, harness definitions, runtime ABI, workspace identity,
and execution boundary are pinned when a run starts. A safely interrupted run
can replay completed agent results and continue unfinished work in the same
workspace and execution boundary. Use the exact resume command reported by
inspect; the backend reopens its pinned snapshot and does not require the
original workflow file to remain present.
For the local backend, workspace identity includes the real path plus filesystem device and inode. Replacing a directory at the same path therefore fails before execution rather than being mistaken for the continuing workspace.
jaeger resume 20260720194512-0123456789Editing the workflow or changing inputs creates a new run. Resume does not reconstruct or verify filesystem and external effects from completed workers. If that state was rolled back, replaced, or changed outside the run, abandon the old run and start a new one.
An agent step that started but did not durably complete is uncertain. It may
already have produced side effects, so Jaeger fails closed: it does not retry the
step and does not offer a resume command. Reconcile the possible external effects
before deciding whether to start a new run.
Workflow API
inputs
The pinned JSON value read from --input FILE or stdin with --input -.
phase(name) and log(value)
Record replay-safe human-readable progress without changing workflow dataflow.
parallel(tasks)
Run zero-argument functions concurrently and return their results in input order. Jaeger's run-level concurrency policy bounds launched agent work.
agent(prompt, options)
Each call creates a durable native provider session and runs its first turn. Supported authored options are:
| Option | Meaning |
| --- | --- |
| harness | Required: built-in codex, claude, or pi, or a configured custom name |
| model | Native model identifier passed to the harness |
| effort | Native effort value passed to the harness |
| serviceTier | Codex service-tier override |
| profile | Codex configuration profile |
| cwd | Working directory, resolved relative to the run working directory |
| schema | JSON Schema for the final structured output |
| label | Human-readable label included in the durable step identity |
| timeoutMs | Positive timeout up to 24 hours |
Per-agent permission and session-persistence controls are deliberately absent.
Sessions are always persisted and workers always have full authority. Legacy
access, fresh, and claude-code compatibility forms are invalid rather than
silently ignored.
Custom harness surfaces
Jaeger ships three protocol drivers: codex-app-server, claude-agent-sdk, and
pi-rpc.
Operators may register additional workflow-facing harness names that select one
of those drivers and a launcher executable. The local backend reads user configuration from
$XDG_CONFIG_HOME/jaeger/harnesses.json or ~/.config/jaeger/harnesses.json.
Use jaeger backend install --harness-config FILE to pin another same-host
configuration into the service, or use --harness-config FILE explicitly with
run or doctor.
{
"version": 1,
"harnesses": {
"company-claude": {
"driver": "claude-agent-sdk",
"command": "/opt/company/bin/claude-gateway",
"description": "Claude Code through the company gateway"
}
}
}The custom name is then an ordinary workflow choice:
return agent("Inspect the current change.", {
harness: "company-claude",
effort: "high",
})Built-in names are reserved. Configuration supplies one executable path, not a shell command or argument string; wrapper launchers may own environment setup, credentials, gateway selection, and default model mapping. Keep credentials out of the Jaeger configuration itself.
New runs pin the resolved harness definitions in their immutable run record.
Detached workers and later session resume operations therefore retain the
same driver and launcher without re-reading mutable user configuration. The
launcher binary and its external environment remain host-owned dependencies,
just as they are for the built-in codex, claude, and pi commands.
Unavailable built-ins are omitted from a new run's pinned registry, so adding
Pi support does not make Pi a mandatory dependency for Codex- or Claude-only
hosts. An unavailable explicitly configured custom launcher still blocks
admission.
Native session boundary
Codex is integrated only through the
Codex app-server protocol over
codex app-server --stdio; Jaeger does not retain a codex exec fallback. Each
active turn gets a contained app-server
transport process while the non-ephemeral Codex thread remains provider-
persisted and resumable. Claude Code surfaces are integrated through the official
streaming Claude Agent SDK
with session persistence
enabled; there is no claude --print fallback. Custom surfaces reuse one of
these transports rather than weakening the provider session contract. Pi
surfaces use strict-LF JSONL over pi --mode rpc, require Pi 0.80.4 or newer,
persist sessions below the Jaeger run, and wait for agent_settled; there is no
text/print compatibility fallback. Pi has no native output-schema switch, so
Jaeger adds the schema to the prompt and still parses and validates the final
JSON itself. All
surfaces load their native commands, tools, skills, MCP servers, hooks, and
settings with full permissions.
The workflow checkpoint and provider session are related but distinct durable
objects. Steering an in-flight workflow step can change that step's eventual
checkpointed result. Turns started later with jaeger session resume remain in
the provider conversation and Jaeger's session record, but do not rewrite the
already completed workflow result or replay journal.
Authority and isolation
Starting a workflow is the authorization event for the entire automation. Every worker receives its native harness's full non-interactive technical authority. Jaeger does not pause for per-command or per-edit approval.
Prompts such as “review without editing” are useful behavioral constraints, but they do not reduce technical authority. Do not describe them as a sandbox or a permission guarantee.
Containment belongs around the whole Jaeger process. When a workflow must not reach the full host, run Jaeger and all of its workers inside a constrained workspace, sandbox, container, or VM selected by the operator. Jaeger's default boundary is the host and working directory from which it was launched; Jaeger does not claim that boundary is isolated.
The per-call systemd scopes are lifecycle containment, not a security sandbox. They let Jaeger discover, stop, and prove the absence of ordinary provider descendants after failures. A full-authority worker on the default host boundary is still trusted and may be technically capable of changing its own host process controls. Use the outer runtime boundary for adversarial isolation.
Coordinator replay contract
Coordinator code may compute with pinned inputs and checkpointed agent outputs
using ordinary deterministic JavaScript. Outside Jaeger primitives, it must not
perform filesystem, shell, network, clock, randomness, or environment-dependent
operations. Put external effects in agent() calls so Jaeger can attribute and
checkpoint them. phase() and log() are safe because Jaeger owns their journal
and replay behavior.
The validator also rejects Promise continuation methods, timing races,
locale-sensitive methods, reflective or dynamically synthesized string member
access, and mutation of captured state from async or parallel callbacks. Use
await, return values from parallel tasks, combine those results afterward, and
coerce dynamic array indexes with Number(...) when necessary. These constraints
keep replay order independent of provider completion timing and host locale.
Only a truncated final coordinator checkpoint with no unfinished agent can be
repaired, and repair occurs while holding the exclusive run lease. A complete
agent.started without one durable completion remains uncertain and is never
discarded or retried.
Each service-owned run stores its immutable record, pinned workflow source,
journal, session records, control mailboxes, and harness scratch data below
$XDG_STATE_HOME/jaeger/runs/<run-id>/ by default. Version 4 run records also
pin the runtime ABI, backend kind, filesystem-backed workspace identity, and
submission key. The state root also contains a private atomic submission index;
continued provider turns add immutable request, owner, and result records under
the run directory. Persistent schedule state lives under the same backend-owned
root in .schedules/, with private state, revision, and occurrence records.
Lifecycle hook events and delivery attempts live separately in .hooks/; they
are observer state and are never interpreted as workflow checkpoints.
Embedded compatibility mode defaults to .jaeger/runs/<run-id>/. Provider-
native conversation history is owned by Codex, Claude Code, or Pi. Jaeger state and
transcripts may contain prompts, tool activity, and outputs; treat the entire
state root as sensitive local data.
Packaged skill
The npm artifact contains exactly one skill at
skills/jaeger-workflows/. Installing the package makes that directory available
inside the package, but deliberately does not copy or link it into a harness's
personal skill directory. Load or install it explicitly according to the parent
harness's skill conventions.
See examples/mixed-review.js, examples/review-fix-loop.js, and examples/cross-harness-smoke.js for complete workflow files. The accepted product boundary is documented in docs/design/jaeger-workflows.md.
