Skip to content

Releases

0.7.0 - 2026-11-11

Choose a tag to compare

@github-actions github-actions released this 11 Nov 08:20
3eb5334

Release Notes

Added

  • --mcp name=url, at init and run, offers the agent an MCP server of
    your own over HTTP, such as Blender Lab's server for a running Blender
    (ADR 0032). It is pinned in run.json as launch.mcpServers; run --no-mcp drops the pinned ones. fx, OpenCode, Qwen Code and --harness acp get them; init and run refuse them with Pi or dsh.

Changed

  • After verify or a heavy job ends, Tenaz also stops the processes it
    left running in a session of their own (Node's detached: true,
    setsid), which killing its process group missed. Such a process was
    born while it ran, leads a process group of its own, has no terminal,
    runs as the same user, is not git, works inside the project, and was
    adopted by launchd, init or systemd --user or has a parent still
    in the command's group (ADR 0031; see its known gap in
    docs/COMPATIBILITY.md). In a raycaster run, a check's detached npx vite preview built up 175 servers, about 2.5 GB, until the engine ran
    out of memory.
  • --harness dsh turns off dsh's own automatic compaction, so a session
    ends at Tenaz's context budget instead of summarizing its history once
    dsh's count reaches about 49k tokens of a 131k window. That count
    includes reasoning the proxy does not send (ADR 0030). In a 7-hour
    raycaster run with 0.6.0, 18 summaries and the requests after them
    took about 51 minutes.
  • If dsh still asks to summarize its conversation (the setting above did
    not apply), the engine proxy refuses the request, as it does Qwen
    Code's, and the session ends there as one out of context instead of
    the summary counting as a turn. In the 0.6.0 raycaster run seven
    summaries counted toward one session's 40-turn limit, which stopped it
    at "60/40" with 53 real turns and its checks passing.
  • --harness dsh gives dsh the user's Playwright and npm caches
    (PLAYWRIGHT_BROWSERS_PATH, npm_config_cache), so browsers and
    packages it installs are where verify, which runs with the user's
    own HOME, finds them. The protocol now says that verify runs with
    that HOME. In the 0.6.0 raycaster run a browser installed into
    dsh's HOME failed the first task's first verify, and npm install
    ran from an empty cache (354 s).
  • The protocol no longer tells sessions to use timeout 120 <command>,
    which macOS does not have: most sessions of a macOS run lost a turn to
    "command not found". It now says to set the shell tool's own time limit,
    and to time out a program a check starts in the check's own language.
  • The orchestrator's message after it stops a long tool call no longer
    suggests timeout 60 <command>; it says to run the call again with
    the shell tool's own time limit.
  • A check tests its requirement in the state the spec describes: one
    about the first screen is checked before any user action. The
    protocol, which every role reads, now says so. A check that changes
    such a state first is MAJOR for the reviewer; when a card asks for
    one, the builder follows the spec. In two runs a check of a game's
    first frame opened a door first, and only the final review caught it.

Security

  • rmcp, the MCP library behind the media server, goes from 3.5.1 to 3.9.0
    for RUSTSEC-2026-0461: an unbounded tool cache in its HTTP server, which
    only this machine can reach (the server listens on 127.0.0.1). The
    upgrade also clears RUSTSEC-2026-0457 (the OAuth client) and
    RUSTSEC-2026-0462 (the HTTP client), which Tenaz does not ship; only its
    tests use the client.

Install tenaz 0.7.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aartiles/tenaz/releases/download/v0.7.0/tenaz-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install aartiles/tap/tenaz

Download tenaz 0.7.0

File Platform Checksum
tenaz-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
tenaz-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo aartiles/tenaz

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>

0.6.0 - 2026-11-08

Choose a tag to compare

@github-actions github-actions released this 08 Nov 21:54
fcc7494

Release Notes

Added

  • --harness acp drives any other ACP agent: --acp-bin names the
    command and tenaz run --acp-args its arguments. Before each session
    Tenaz writes .tenaz/private/acp-session.json (the proxy's URL, the
    session's model and limits, the reasoning effort, the media MCP
    servers) and names it in TENAZ_AGENT_SESSION, so a script can
    configure the agent. Use it instead of pointing --fx-bin at such a
    script. The agent keeps the user's HOME.

  • --harness dsh drives DeepSeek Harness over ACP (dsh --profile acp,
    --dsh-bin names it), with HOME and DSH_HOME of its own under
    .tenaz/private/dsh-home and a patch Tenaz writes for each session's
    model that points it at the engine proxy. Two runs finished with
    Qwen3.8-Flash-Next on Sushi, the longer one a 19-hour game brief. It
    does not ask before tool calls (see
    Known gaps).

Changed

  • The engine proxy now keeps the reasoning of earlier assistant messages
    until it passes pastReasoningTokens (8192), then removes all but the
    newest one's in one step, instead of removing it every turn. Removing
    it every turn broke the engine's prefix cache on each request: in a
    replay of the 2026-11-08 raycaster run the cache rises from 85% to 90%
    and the extra prefill falls from 31 to 10 minutes, for up to 8k more
    tokens in the window. --set pastReasoningTokens=0 brings back the
    old behaviour. See ADR 0028.
  • The idle rule counts a shell command that names a file not read or
    named before as a read, as it counts a read tool's, and so a tool such
    as Pi's grep or ls whose path is new. A builder that
    read the checks with sed -n and grep and ran probe scripts was
    stopped as idle four times in the 2026-11-08 raycaster run, two of
    them T019's attempts. Reading the same files again through the shell,
    or running the same probe again, still counts as idle. See ADR 0029.

Fixed

  • An ACP agent's message text that arrives after its model request has
    ended is kept (a late_text event in the journal) instead of dropped.
    DeepSeek Harness sends its answer so: the preflight's tool probe failed
    with tools=false though the model ran the tool and answered, and a
    review answer would have been lost the same way.

Install tenaz 0.6.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aartiles/tenaz/releases/download/v0.6.0/tenaz-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install aartiles/tap/tenaz

Download tenaz 0.6.0

File Platform Checksum
tenaz-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
tenaz-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo aartiles/tenaz

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>

0.5.0 - 2026-11-07

Choose a tag to compare

@github-actions github-actions released this 07 Nov 11:50
f8d7668

Release Notes

Added

  • --harness opencode and --harness qwen drive OpenCode and Qwen Code
    over ACP, as fx is (--opencode-bin and --qwen-bin name their
    executables). Each runs in a HOME of its own under .tenaz/private/
    with a configuration Tenaz writes for each session's model: the engine
    proxy, the model's window, asking before every edit and command, and
    the media MCP server; the pinned skills are linked into that HOME.
    Qwen Code's memory and chat recording are off, and the context files
    it reads from its HOME are removed before each session. Neither
    agent takes a steering message.
    tenaz doctor and the setup wizard find both on PATH. See ADR 0019.
  • A generate_3d media tool: --meshes <base URL> --mesh-model <model>
    (at init or run, pinned in run.json as launch.meshesUrl and
    launch.meshModel) let the agent turn a PNG or JPEG inside the project
    into a GLB inside the project through mlx-serve's
    POST /v1/3d/generations (Idol, TRELLIS.2). Each call takes the lease
    like an image and is limited by --media-timeout. tenaz doctor has an
    optional 3d line that offers a 3D model an engine lists. See ADR 0020.

Changed

  • The engine proxy now forwards only the last assistant message's
    reasoning: a harness that sends all of it back filled the window with
    the model's earlier reasoning. --set keepPastReasoning=true sends it all. See ADR 0023.
  • When an ACP agent reports an edit as a diff, Tenaz records the edited
    file's path as the call's result, not the agent's raw output. Qwen Code
    sends the whole file before and after each edit in the diff, so every edit to
    a large file raised a false big-output steer.
  • With --harness qwen, the engine proxy refuses Qwen Code's own request
    to summarize its conversation, and Tenaz ends the session there as one
    out of context; a fresh session continues. Before, each summary took
    minutes, showed as an empty reply, and the session was stopped right
    after it anyway. See ADR 0022.
  • A reviewer stopped (for example at its turn budget) after writing its
    review now says so in the log, as in
    turn budget exceeded (38/25) after writing its review; its verdict (CHANGES) is used.
    Before, it read only as a failed session, though its verdict was taken.

Fixed

  • A session the engine held past a lowered context budget, with no
    engine error, now raises the budget at once to the headroom share of its
    peak (0.8 by default, the share a lowering uses), and
    lifts a ceiling under that. Before, sessions that needed more than the
    cap were cut one after another while Tenaz waited for a raise, and a
    run could block. See ADR 0026.
  • A request the engine refuses for what is in it (400, 413, 415 or 422,
    other than an image, a prompt too long or the engine's memory) is no
    longer retried as it was. A fresh session continues the work with a
    note that quotes the refusal and says to read that input another way;
    the journal records it as refused-continuation. Before, Tenaz
    retried the same session six times with growing waits and then
    blocked the run as "engine unavailable"; it still does once the
    session's continuations are spent (ADR 0025).
  • A video file an agent reads reaches the engine as a note saying to
    read it as frames. Before, Qwen Code sent the whole file, Sushi
    refused each request, and the run blocked as "engine unavailable"
    (ADR 0024).
  • tenaz run --resume on a run blocked with no task ready gets one
    more replan. Before, the run had already used its one replan for
    that, so it blocked again at once, and only an edit of
    .tenaz/run.json let it go on.
  • A replan that sets a blocked task back to todo while no card still to
    build amends a check of its conflict gets one more prompt before it
    ends, once per session, saying that Tenaz will block the task again.
    It may then add the amends or route around the task. Before, Tenaz set
    the task back to blocked when the replan ended and said nothing, and
    the run found no task ready and started another replan, or blocked.
    The log says when Tenaz asks and when it blocks the task again (ADR
    0018).
  • The watchdog sees Qwen Code's reads and writes (ReadFile,
    WriteFile, Edit), so it no longer stops a Qwen Code session that is
    reading new files as idle, and the journal counts its writes. For an
    ACP agent (fx, OpenCode, Qwen Code) it now goes by the tool call's
    kind first (read or edit) and by the name only when the kind says
    nothing about files. See ADR 0021.
  • After a generate_3d or --images job, Tenaz frees the media model
    through the server's POST /v1/unload-model, as for ComfyUI. mlx-serve
    keeps a 3D or image model loaded after its job; in the first real 3D run
    TRELLIS.2 stayed resident for hours next to the LLM and the Mac swapped.
    This is done also when the server is the LLM's own. A server without
    that route is left as it is. A failed media unload is journaled
    (media:<engine> failed unload: …) and no longer fails the job, which has
    already written its file.
  • An ACP agent's tool call is reported with the input it runs with.
    OpenCode and Qwen Code announce a call before its input is complete
    (bash with only its folder, a read with no file) and send the input
    once it runs; Tenaz took the first version, so different calls looked
    the same and the loop check stopped sessions that were working. A
    pending call is now reported when it is about to run: at its permission
    request, when it starts, or when it ends. toolMinutes counts from
    then, so a call the agent sends neither a permission request nor
    in_progress for is not timed. A context stop held for a reply's tool
    calls now also waits for a call that starts after another one ended.
    A permission request names the call as its start does, not by the
    title the agent shows people.
  • A replan that routes a blocked task's work through a card that amends
    its conflict's checks may set the blocked task back to todo, depending
    on that card, instead of dropping it. Tenaz used to reject that backlog
    ("amends T001.runs, which no file in .tenaz/conflicts/ names") and end
    the run BLOCKED. The reopened task starts with no attempts and is not
    built while a card that amends its checks is still to build. A status a
    planner may not set is now put right before its cards are judged. See
    ADR 0018.

Install tenaz 0.5.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aartiles/tenaz/releases/download/v0.5.0/tenaz-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install aartiles/tap/tenaz

Download tenaz 0.5.0

File Platform Checksum
tenaz-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
tenaz-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo aartiles/tenaz

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>

0.4.0 - 2026-11-02

Choose a tag to compare

@github-actions github-actions released this 02 Nov 22:03

Release Notes

Added

  • --model-for works with --harness pi: each session's Pi is launched
    with its role's model, and the run's models.json lists every model of
    the map, keeping the settings your ~/.pi/agent/models.json has for
    each. A role's model without --context-window-for takes the window
    that file lists for it, and its inputs unless --vision is given.
    --swap-models works with Pi too. See ADR 0011.
  • tenaz init --swap-models keeps one model of a --model-for map
    loaded at a time, for models that do not fit in memory together: before
    each session Tenaz unloads the others and loads that session's model,
    so each review costs two loads. At every tenaz run start it loads
    each model once, alone, and stops the run if one will not load. Needs
    --engine load-api (Sushi, mlx-serve). tenaz run --swap-models and
    --no-swap-models change it; it is kept as launch.swapModels in
    run.json, status --json has swap_models, and tenaz status says
    the models are loaded one at a time. When swapping, the
    journal's lease records name the model of each load and unload,
    and record a model the engine dropped by itself as evicted. See ADR
    0015.

Changed

  • With fx, an engine error that came with an error status now starts
    with that status, as Pi's errors do, then the engine's message:
    503: <message>, or 500 alone when there is no message. The message
    of an SSE body is that of its first error chunk. Pi puts the raw body
    after the status instead. It shows in the session's provider-error
    watchdog event and image_refusal, the journal's image-continuation
    refusal, the image continuation note and tenaz watch.
  • The agent skill no longer offers to pipe the install script into sh:
    it gives Homebrew, a release archive with its checksum and attestation,
    or cargo install, and points to the README for the rest. skills.sh's
    Snyk audit flagged the script's URL as an unverifiable dependency.
  • The journal's lease records end with ready whenever the lease loads
    the LLMs back: also at the start of a run, after an engine error, and
    after a failed reload or unload, not only after a media or heavy job.
  • A context budget held at the ceiling a failed raise set, for twice the
    sessions a raise waits for then (8 to 32 with the default
    capacityRecoverSessions), is raised past it, halfway to the largest
    configured budget, and the log says so; an overflow sets the ceiling
    again. The ceiling used to last the whole run: in the Digger below the
    Mine run of 2026-10-30 it was set at 58419 while memory was short, and
    the budget stayed there for hours after the pressure was gone, with
    sessions reaching 77257 tokens unharmed. See ADR 0014.

Fixed

  • When the engine cannot take the preflight's requests, tenaz run now
    stops with the engine's error and says it is the engine's. It used to
    report no reasoning, no cached prefix and a failed tool call, and advise
    another model. This covers an engine out of memory (mlx-serve answers
    503), one that answers 502, 503 or 504, and one not reached.
    --allow-weak-engine does not accept such a preflight, so the next run
    measures it again. For an engine out of memory it says to free memory
    or reload the model, as the run's retries do. Other engine errors, such
    as a refused image or a 400 or 500 not about memory, are still left to
    the checks and the flag. run.json's preflight keeps the engine's
    errors in engine_errors. The log and tenaz status show such a
    preflight as not judged, with the count of errors, instead of its
    checks, and the log names the first error. A failed preflight points
    to .tenaz/private/preflight.fx-acp.log only with fx: Pi keeps no log
    of it. See ADR 0016.
  • The preflight line of the log and of tenaz status shows the image
    check (vision=true or vision=false) for a vision model. A preflight
    that failed only on the image used to read like a pass.
  • The log's preflight line shows (accepted weak) when
    --allow-weak-engine runs past a failed preflight, as tenaz status
    does. The log used to be written before the run took the flag into
    account.
  • The preflight line of the log and of tenaz status starts with
    settled=false when the preflight's session did not finish. Such a
    preflight fails, but its line used to read like a pass.
  • The engine proxy ends a streamed event at a blank line written with
    \r\n\r\n or \n\r\n, as well as \n\n. With an engine that framed
    its events that way, the proxy used to report an error inside the
    stream only when the stream ended. When the request offered a tool to
    repair, the whole reply reached the harness only then, not repaired,
    and when its first event held arguments of a call to that tool, the
    rest of the reply was dropped.

Install tenaz 0.4.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aartiles/tenaz/releases/download/v0.4.0/tenaz-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install aartiles/tap/tenaz

Download tenaz 0.4.0

File Platform Checksum
tenaz-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
tenaz-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo aartiles/tenaz

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>

0.3.0 - 2026-10-31

Choose a tag to compare

@github-actions github-actions released this 31 Oct 14:12

Release Notes

Added

  • Before a session opens, Tenaz estimates its first request: the prompt
    at 3 bytes per token, plus harnessReserveTokens (default 8192) for the
    harness's own prompt and tools. Over the budget of the role's model,
    the run blocks with a reason that starts prompt too large:, names the
    role, the task if any, the model and its window, and says what to
    change; no session runs. A card's own token budget and the engine's
    capacity cap do not count here. Over the Digger 3D run of 2026-10-29
    the estimate was 2.2k to 4.5k tokens high, never low. See ADR 0012.

  • diffShare (default 0.25) sizes the diff a reviewer is shown by its
    own model's context budget, under the engine's capacity cap when there
    is one, counted at 3 bytes per token, with diffLimitBytes (40000)
    still the most. A reviewer on a 65536 window at contextBudget 0.65
    gets about 32 KB; one on 131072 gets 40 KB, as before. In the Digger 3D
    run of 2026-10-29 the reviewer's first request was 30245 of its 42598
    tokens before it read a file, half of it the diff. A run.json without
    diffShare loads with the default. See ADR 0012.

  • tenaz init --model-for ROLE=ID gives a role another model than
    --model, for example a reviewer of another model family, served by the
    same engine. It is repeatable, one role each, and
    --context-window-for ROLE=N sets that model's window (default: the
    run's). The map is kept in run.json under config.roleModels;
    tenaz status names the roles it covers and gives the builder's context
    budget, and status --json has role_models. An unknown or repeated
    role, the run's own model, and a window for a role with no model of its
    own are refused, and so are one model given different windows for two
    roles and a map with --harness pi.

    fx is launched with each session's model, and the session's context
    budget is the contextBudget share of that model's window; the
    session's journal record has model. Planners size cards for the
    builder's model. After the engine runs out of memory, the lowered budget
    is raised toward the largest model's budget. See ADR 0011.

  • With --model-for, every model of the run must be loaded before the
    first session. Tenaz loads those that are not, as it does the run's
    model, and tenaz run stops and names any it could not load. Around an
    image job or a heavy job the lease frees every model and loads them all
    back; a generic engine's stop and start commands run once for all of
    them. A generic engine that serves only some of the run's models is not
    started a second time: tenaz run stops and names the others.

  • A builder whose task needs work too heavy for a session (a render, a
    long build) writes .tenaz/jobs/<TASK>.json with the command, its time
    limit and why, and stops. Tenaz runs the command between sessions of the
    same attempt, with the engine's request gate closed and the LLM unloaded
    where the engine can, for at most jobMinutes (60); a fresh session
    then gets its exit code, duration and the end of its log, in
    .tenaz/private/jobs/. The job does not cost an attempt. An attempt
    may have jobsPerAttempt (3) job files; one past them is removed unread
    and the attempt is judged. The journal gets job-started and
    job-finished records. A file that is not valid runs nothing and the
    next session is told why. A stop during the job pauses the run, and on
    resume the job runs again before any session; a report no session has
    read yet is kept in run.json across a pause. A crash costs the
    attempt: the job file it left is removed unrun. After a job the LLM
    serves again before the next session; one that does not come back
    blocks the run as "engine unavailable". jobMinutes=0 turns jobs off:
    a job file is removed unread and the attempt is judged. In the Lotion
    Pump run a 25-minute Blender render next to the model's engine failed
    for memory, and so did the engine; the operator had to run it with the
    run blocked. See ADR 0010.

  • A builder session rerun after the engine ran out of memory is told why
    and asked not to leave heavy work running in the background, but to
    declare it as a job. Other roles, other engine errors and runs with
    jobMinutes=0 get no such note. See ADR 0010.

  • A builder whose task cannot pass without changing a check of a done task
    writes .tenaz/conflicts/<TASK>.md naming those checks and stops. Tenaz
    blocks the task at once, without more attempts or a split, journals a
    conflict record, and starts a replan that is shown the conflict. The
    replan may add a card with amends: [<check IDs>], naming only checks a
    conflict names; its builder may change those checks and no others, and
    its reviewer judges each change against the spec. A conflict with the
    task's own checks, or with a task that is not done, does not stand: the
    attempt fails as before and the next builder is told why. A builder that
    declared a conflict is not asked for a passing check, and a conflict
    belongs to the attempt that declares it: the file is removed when an
    attempt starts. In the Lotion
    Pump run the builders of T016b measured that their card and a check of
    T016a could not both pass, and no session was allowed to change the
    check. The builder prompt no longer promises a replan for any other
    task that cannot be done. See ADR 0009.

  • Without the sandbox, a write tool that names a file outside the project,
    the temporary directories, on macOS the per-user cache, and
    sandboxAllowWrite is noted: a line in
    orchestrator.log, outside_writes and an outside-write watchdog
    event in the session's journal record, and one steer telling the session
    its files belong in the project. A bootstrap step that is not accepted
    says where the files went. The write is not stopped, and shell commands
    are not checked. See ADR 0008.

Changed

  • These apply to every run, with or without --model-for:
    • A session whose context budget is already under the cap a memory
      failure set (a card that asks for a small budget, for example) no
      longer counts toward raising the cap: it did not test it.
    • After an image job or a heavy job, a generic engine's start command
      is skipped when the engine still serves the model.
    • When the run's model is not served before the first session,
      tenaz run says Model <id> is not being served at <url>, with why
      loading it failed, instead of the LLM could not be loaded: ...
      (Model <id> is not being served (<error>) when the engine does not
      answer, as before).
    • When freeing the LLM for an image or a heavy job fails and the engine
      still answers without it, Tenaz loads it back at once (a generic
      engine's start command runs) instead of leaving it to the engine
      retry.
  • On macOS the sandbox lets the agent and verify write the per-user cache
    (getconf DARWIN_USER_CACHE_DIR), as they could already write the
    temporary directory beside it. Metal keeps its compiled shaders there:
    without it every program that draws with Metal compiles them again on
    each start, and in a Blender benchmark each ./blender call took about
    10 s longer (a small render took 11 s instead of 0.9 s).
  • In the failure history a split is shown, an attempt whose verify
    failure repeats the one before word for word says so and names the
    attempt that has the text, instead of repeating it. In the Lotion Pump
    run the split of T016b read the same 1.5 KB of verify output three
    times. run.json keeps that failure as current.verify_failure
    (attempt, text).
  • A builder whose previous attempt failed verify word for word as an
    earlier one is told so. If verify reports a measurement, nothing done
    since moved it: change the approach; or, when the card cannot pass
    without changing a check of a done task, declare the conflict (see
    "Added"); or say in the notes why the card cannot be done, and stop. In
    the Lotion Pump run the three attempts of T016b each left the scene at
    its starting numbers. The task is not escalated any sooner: a check
    that only says exit 1 reads the same while real work goes on.
  • The release builds the macOS archive on GitHub's macos-15 runner
    instead of macos-14, whose retirement GitHub set for 2026-11-02. The
    binary's minimum macOS should not change (11.0, Rust's default for
    Apple silicon).

Fixed

  • The builder of a split task may write the notes of the tasks it was
    split from: T021dd may correct .tenaz/notes/T021d.md and
    .tenaz/notes/T021.md. Tenaz reverted those edits, so a card the split
    wrote to fix a parent's stale handover could never pass. See ADR 0013.
  • A builder session after one whose writes Tenaz reverted is told which
    paths were reverted, why, and where a builder may write. Only the
    bootstrap was told before, so the builders of a task could write the
    same forbidden file on every attempt without learning why verify
    never saw it. run.json keeps the paths as current.reverted until
    the task ends.
  • A session paused with Ctrl-C keeps its continuation until a session
    that continues it ends. A rerun after an engine error or a lowered
    context budget is told it continues the paused work, and so is the
    session after --resume when the run blocks because the harness refused
    its prompt, or the engine or capacity retries ran out. Tenaz used to
    clear the continuation before the first session, so these started the
    work afresh with the paused session's changes in the tree.
  • A prompt the harness refuses as larger than the model's window (fx's
    ContextCapacityExceeded before any reply) blocks the run at once, with
    a reason that starts prompt too large:, names the session, its role,
    its model, the model's window and the prompt's size, and says what to
    change. The same error after a reply is rerun as before. Tenaz used to
    send the same prompt again up to engineRetries times and then blo...
Read more

0.2.0 - 2026-10-28

Choose a tag to compare

@github-actions github-actions released this 28 Oct 13:20

Release Notes

Added

  • capacityRetryWaitSec (10 by default): the pause before a session is
    rerun after the engine ran out of memory and Tenaz lowered the context
    budget. It was a fixed 10 seconds. A stop (Ctrl-C) during the pause now
    pauses the run at once; before, the pause ran out and the next session
    started first. tenaz demo sets it to 0, so the demo ends about 10
    seconds sooner.

  • A time limit per tool call, toolMinutes (10 by default, 0 turns it
    off). A call that runs over it, such as a script in an endless loop, is
    stopped and the same session is told which call and what to do; a second
    one ends the session. Pi's bash has no default timeout, and a hung call
    held a session for 21 minutes. See ADR 0004.

  • Release archives come with a GitHub build provenance attestation, which
    shows that an archive was built from this repository by the release
    workflow. Check one with
    gh attestation verify <archive> --repo aartiles/tenaz.

Changed

  • The sessions that write task cards (the bootstrap's task-cards step,
    split and replan) are told the builders' context budget in tokens, with
    a rule: what a builder reads and writes for a card fits in about two
    thirds of it, at roughly 4 characters per token, and a card over that is
    split before it is written. The prompts only said "context-limited
    sessions": in the Digger 3D run of 2026-10-27 a card that needed eight
    modules read was split twice, and builders ended past the budget.

Fixed

  • A run paused during the review of an attempt that passed verify (or
    stopped there because the engine gave out) runs verify on that
    attempt again when it resumes and, if it still passes, reviews it
    again. It counted the attempt and built the task again, and a pause in
    the review of the last allowed attempt split the task. The Digger 3D run
    of 2026-10-27 was paused this way during the review of T012ba's attempt
    2.

  • A session stopped at its second context compaction is continued by a
    fresh session, like one stopped at its context budget, and no longer
    counts as a failed attempt. Both mean the context ran out; in the Digger
    3D run of 2026-10-27 the compaction stop charged T012ba an attempt.
    An attempt that uses up its continuations this way is split, as one
    stopped at its budget is (contextSplit).

  • A session stopped at its context budget keeps the work of the reply that
    crossed it. The stop came at the reply's end, before its tool calls ran,
    so a model that wrote a whole file in that reply lost it: three files in
    the Digger 3D run of 2026-10-27, and each continuation wrote it again
    into the same stop. The reply's tool calls now run first, and the session
    stops before its next reply.

  • The engine proxy no longer garbles a character that the network splits
    across two chunks of a streamed reply. It decoded each chunk on its own,
    so an accented letter or emoji cut in half reached the harness as �
    in every repaired stream. It now decodes only whole events.

  • A request the harness cancels because Tenaz stopped the turn no longer
    counts as an engine error. Pi reports it as one ("This operation was
    aborted"), so a session aborted at its second compaction was rerun as an
    engine failure: the attempt did not count and an engine retry was spent.

  • verify runs in a process group of its own, and Tenaz kills the group
    when it ends or times out. A check that started npm run dev and failed
    left Vite running on its fixed port, so the next check failed on the port
    (with --strictPort), and a leftover process that kept verify's output
    open made Tenaz wait for it.

  • verify runs with NO_COLOR=1 as well as CI=1. CI turns colours
    on in some tools (Vite), so a check that read their output failed under
    Tenaz and passed in the builder's shell. Escape codes are also left out of
    failure messages and output tails. The protocol tells builders the
    environment verify runs in.

  • tenaz watch shows Pi sessions. Pi's stream keeps Pi's own records,
    which watch dropped (or showed raw, marked ?, at debug); it now
    translates them as the Pi adapter does.

  • The setup wizard names the provider as tenaz init with flags does. For
    an engine address that does not answer yet, it pinned local; the flags
    pinned the provider usual on that port (llamacpp on 8080, ollama on
    11434, ...). Both now take that one, and local only for another port.
    The provider is part of model in run.json (provider/id), and the
    wizard's advice on the context window now names that engine (Ollama's
    context setting for an address on 11434).

Install tenaz 0.2.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aartiles/tenaz/releases/download/v0.2.0/tenaz-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install aartiles/tap/tenaz

Download tenaz 0.2.0

File Platform Checksum
tenaz-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
tenaz-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo aartiles/tenaz

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation>

0.1.1 - 2026-10-27

Choose a tag to compare

@github-actions github-actions released this 27 Oct 09:19

Release Notes

Changed

  • tenaz --version and runner_version in run.json name the commit a
    binary was built from a git checkout of (tenaz 0.1.0 (816e1d8)). Three
    benchmark runs of different code all recorded tenaz 0.1.0. A build
    from a source archive, without git, shows the version alone.
  • The final reviewer first judges the product as a whole against what the
    brief says it is for and what it says to avoid, under ## Overall in
    FINAL.md, and asks for changes when it misses that even if every
    requirement passes. In the explainer run each motion requirement passed
    and the review accepted a video that read like the slides the brief
    ruled out.
  • A builder or bootstrap session stopped at its turn budget is continued
    by a fresh session, like one stopped at its context budget, instead of
    failing the attempt: its changes are kept and the new session is told to
    finish from them. In the explainer run three builders ran out of turns
    while working, and each cost its task an attempt.
  • The package description (shown by brew info and on the release) says
    what Tenaz does now: it drives a local model through a coding agent
    until the goal passes its checks.

Install tenaz 0.1.1

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aartiles/tenaz/releases/download/v0.1.1/tenaz-installer.sh | sh

Install prebuilt binaries via Homebrew

brew install aartiles/tap/tenaz

Download tenaz 0.1.1

File Platform Checksum
tenaz-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
tenaz-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum

0.1.0 - 2026-10-27

Choose a tag to compare

@github-actions github-actions released this 27 Oct 07:07

Release Notes

Added

  • The README opens with a GIF of a scripted tenaz demo run, recorded
    with VHS from docs/demo/demo.tape.
  • tenaz binary with init, run, status, finish and params.
  • --help describes the workflow with a non-interactive example; run --help
    lists the exit codes and status --help what each phase means.
  • tenaz run exits with 0 when the goal is met, 3 when it completes with
    gaps, 4 when it is blocked and 5 when it pauses; 1 is an error and 2 a
    usage error.
  • tenaz status --json prints the status as one JSON object (phase,
    state, pid, blocked_reason, last_error, task counts, current task,
    session in progress, origin); the text status gains a State: line.
  • tenaz run holds a lock on the project while it works: a second run on
    the same project is refused, and status tells a working run (running)
    from one that was killed or failed (interrupted).
  • tenaz guide prints an Agent Skill for delegating goals to Tenaz from
    another agent.
  • The agent skill moved to skills/tenaz/SKILL.md, so npx skills add aartiles/tenaz installs it. It now also covers installing and setting
    up Tenaz: tenaz doctor and its fixes, what to ask the user to do
    rather than doing it, and tenaz demo.
  • tenaz demo shows a whole run in about a minute with no model and no
    agent: the scripted harness builds a small goal in a new tenaz-demo
    folder. It replaces the three commands the README gave for that, and
    tenaz doctor offers it while something is still missing.
  • tenaz doctor checks git, a coding agent, an engine and a model with
    enough context, and says how to install or fix what is missing (the
    install commands for Pi and fx, LM Studio or Ollama, the context setting
    of each engine). It exits with 0 when a real run can start and 1
    otherwise. The setup wizard gives the same advice when it finds no git,
    no agent, no engine or too small a context.
  • Setup wizard: tenaz init without --model, in a terminal, finds the
    coding agents on PATH, the engines on their usual ports with their
    models, context windows and vision, and ComfyUI, then prints the
    equivalent command line. --interactive forces it with a piped stdin.
  • run.json pins how to launch the run (launch: engine API and
    start/stop commands, proxy, harness executable, ComfyUI and workflows,
    media timeout), so tenaz run needs no flags; a flag given to run
    replaces the pinned value and is saved. run and status default to
    the current directory.
  • On a repository that already has commits, init works on a new branch
    tenaz/<goal> in a worktree next to the repository instead of on the
    current branch, refuses a tree with uncommitted changes, and records the
    starting branch and commit in run.json (origin). --in-place keeps
    the old behaviour; --worktree and --name choose the directory and
    branch. The setup wizard asks which, run and status say where the
    run works, and both point to the worktree from the original checkout.
    A directory inside a repository that tracks nothing in it, such as an
    ignored directory with seed files, is a new project rather than refused.
  • tenaz finish: a branch in the original repository with the run's
    result as one commit on the starting commit, without .tenaz/ (nor,
    for older layouts, the root files and .gitignore lines Tenaz added);
    the run branch keeps the full history, --remove-worktree removes the
    worktree. Nothing is pushed.
  • The run's state lives in .tenaz/ (it was .agent/). A project still
    in the old layout is not run: init, run and status say how to move
    it (mv .agent .tenaz, the .gitignore lines, --refresh-templates).
  • Tenaz keeps all its files under .tenaz/: the brief is
    .tenaz/brief.md (it was BRIEF.md), the shared rules are
    .tenaz/protocol.md (they were the project's AGENTS.md) and the ignore
    rules are in .tenaz/.gitignore (they were appended to the project's
    .gitignore). The protocol now starts every session's prompt instead of
    relying on the agent to load AGENTS.md, so init works in a project
    that has its own AGENTS.md, and sessions may not edit the project's
    AGENTS.md or CLAUDE.md. A run started with BRIEF.md at the root is
    not run: run and status say how to move it (git mv,
    --refresh-templates).
  • existingProject parameter. init sets it when the repository already
    has commits: verify is then .tenaz/verify, a wrapper around the
    project's own tests, new checks go into the project's test suite, and
    bootstrap plans only the change instead of a skeleton.
  • Without --engine, init asks the engine at --base-url which API it
    speaks (Sushi or generic) and names the provider after it.
  • Orchestrator: bootstrap, build, verify, review,
    split or block, replan and final review, with pause and resume.
  • Lease manager keeping one engine resident at a time.
  • OpenAI-compatible engine proxy that holds requests while engines swap.
  • Harness adapters for fx over ACP and Pi over its RPC mode, plus a scripted
    fake harness.
  • Engine adapters for Sushi and generic OpenAI-compatible engines; ComfyUI
    media adapter; MCP server exposing generate_image.
  • The README and docs/COMPATIBILITY.md name MLX's mlx_lm.server and
    TensorFold as engines to use with --engine generic, untested.
  • CI on macOS and Linux and a tagged release workflow.
  • Releases are built with dist and come with an install script
    (curl ... | sh) that puts tenaz in ~/.local/bin and on PATH, and
    a Homebrew formula in aartiles/homebrew-tap.
  • requests.jsonl rows carry the engine's own timings per request
    (server: prefill and decode milliseconds, cached tokens, and draft
    counts when the engine reports them), and status prints the draft
    acceptance.
  • tenaz watch follows a run from its files as it works, at four levels:
    summary (the run log), actions (each tool call and reply on one line,
    with context, output, thinking and decode speed), full (whole replies,
    the start of tool results) and debug (the harness's log too). --once
    prints the current session and exits.
  • A context budget lowered because the engine ran out of memory is raised
    halfway back after capacityRecoverSessions sessions in a row (4 by
    default; 0 keeps it lowered) that the engine held without critical memory
    pressure. A raise the engine overflows again lowers it as before and
    doubles the sessions the next raise waits for, up to four times as many;
    a raise the engine held starts that count again.
  • --images <base URL> --image-model <id> gives the agent its image tool
    through an OpenAI Images API (POST /v1/images/generations), as MLX
    servers, LocalAI and Atomic Chat serve it, with no workflow to export.
    When that API is on the LLM's own server, the LLM stays loaded while the
    image renders (ADR 0002).
  • tenaz init's wizard offers an image tool when an engine on the usual
    ports, Atomic Chat on 1337 or mlx-serve on 11234, lists an image model
    (the LLM's own engine first), before it looks for ComfyUI. tenaz doctor lists the image engines it finds with the flags that use them.
    TENAZ_NO_PROBE keeps both from looking at the usual ports, for tests
    and scripted setups.
  • A builder session that answers its prompt while ./verify --task runs no
    passing check of its task gets one more prompt asking it to add them,
    before the attempt is judged. In the Wild West run 32 of 197 attempts
    passed --all but failed only for lack of the task's own check.
  • A raised context budget never goes above the headroom share of what the
    engine said it could hold when an earlier raise overflowed it. Raising
    past it only overflowed again: in the Wild West run the engine held
    86016 tokens, yet the budget was raised above 68812 six times and lowered
    back each time, and a low reading under passing memory pressure then
    kept the budget at 38215 for seven hours while raises waited 64 sessions.
  • A session is steered to wrap up when, at the pace its context grew over
    the last requests, it would reach the context stop within
    contextSteerTurns turns (10 by default; 0 steers only at the budget), so
    a slow-to-react model can still write its handoff before it is stopped.
  • An attempt that uses up its context continuations and still fails
    verify is split at once instead of retried against the same budget
    (contextSplit, on by default), when the split depth allows.
  • A builder's briefing shows only the spec requirements its card requires,
    with a pointer to the whole spec (citedSpec, on by default; a card that
    names none the spec has gets the whole spec). A session that continues
    stopped work no longer gets the repository map in its prompt.
  • A session steered for going idle is asked to write what it already knows
    (the files and facts the change depends on, and its plan) in its task's
    notes before making the change, and the next attempt's briefing includes
    those notes.
  • Reading something not read before counts as progress for the idle rule,
    like a file change; repeated reads of the same thing still count as idle.
  • With fx, each session's reasoning text is kept in
    .tenaz/private/<label>.thinking.md, with a line after each model
    request. Before, only its length was kept.
  • A bootstrap session after a failed one is told which files the earlier
    sessions left (git diff --stat since init) and asked to continue from
    them instead of starting over. Before, it got only the list of problems,
    and explored and wrote the spec again.
  • The bootstrap runs in three steps, one session each: the spec and map,
    then a minimal verify, then the task cards (ADR 0003). Each
    session gets only its step's part of bootstrap.md, steps 2 and 3 get
    the spec and map in their prompt, and maxBootstrapAttempts applies to
    each step. A step whose files are already rig...
Read more