Repository navigation
Releases
0.7.0 - 2026-11-11
Release Notes
Added
--mcp name=url, atinitandrun, 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 inrun.jsonaslaunch.mcpServers;run --no-mcpdrops the pinned ones. fx, OpenCode, Qwen Code and--harness acpget them;initandrunrefuse them with Pi or dsh.
Changed
- After
verifyor a heavy job ends, Tenaz also stops the processes it
left running in a session of their own (Node'sdetached: 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 notgit, works inside the project, and was
adopted bylaunchd,initorsystemd --useror 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 detachednpx vite previewbuilt up 175 servers, about 2.5 GB, until the engine ran
out of memory. --harness dshturns 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 dshgives dsh the user's Playwright and npm caches
(PLAYWRIGHT_BROWSERS_PATH,npm_config_cache), so browsers and
packages it installs are whereverify, which runs with the user's
ownHOME, finds them. The protocol now says thatverifyruns with
thatHOME. In the 0.6.0 raycaster run a browser installed into
dsh'sHOMEfailed the first task's firstverify, andnpm 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
suggeststimeout 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 | shInstall prebuilt binaries via Homebrew
brew install aartiles/tap/tenazDownload 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/tenazYou 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
Release Notes
Added
-
--harness acpdrives any other ACP agent:--acp-binnames the
command andtenaz run --acp-argsits 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 inTENAZ_AGENT_SESSION, so a script can
configure the agent. Use it instead of pointing--fx-binat such a
script. The agent keeps the user'sHOME. -
--harness dshdrives DeepSeek Harness over ACP (dsh --profile acp,
--dsh-binnames it), withHOMEandDSH_HOMEof its own under
.tenaz/private/dsh-homeand 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 passespastReasoningTokens(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=0brings 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'sgreporlswhosepathis new. A builder that
read the checks withsed -nandgrepand 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 (alate_textevent in the journal) instead of dropped.
DeepSeek Harness sends its answer so: the preflight's tool probe failed
withtools=falsethough 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 | shInstall prebuilt binaries via Homebrew
brew install aartiles/tap/tenazDownload 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/tenazYou 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
Release Notes
Added
--harness opencodeand--harness qwendrive OpenCode and Qwen Code
over ACP, as fx is (--opencode-binand--qwen-binname their
executables). Each runs in aHOMEof 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 thatHOME.
Qwen Code's memory and chat recording are off, and the context files
it reads from itsHOMEare removed before each session. Neither
agent takes a steering message.
tenaz doctorand the setup wizard find both onPATH. See ADR 0019.- A
generate_3dmedia tool:--meshes <base URL> --mesh-model <model>
(atinitorrun, pinned inrun.jsonaslaunch.meshesUrland
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 doctorhas an
optional3dline 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=truesends 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 asrefused-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 --resumeon 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.jsonlet it go on.- A replan that sets a blocked task back to
todowhile 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 theamendsor route around the task. Before, Tenaz set
the task back toblockedwhen 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
kindfirst (readoredit) and by the name only when the kind says
nothing about files. See ADR 0021. - After a
generate_3dor--imagesjob, Tenaz frees the media model
through the server'sPOST /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
(bashwith 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.toolMinutescounts from
then, so a call the agent sends neither a permission request nor
in_progressfor 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 totodo, depending
on that card, instead of dropping it. Tenaz used to reject that backlog
("amendsT001.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 | shInstall prebuilt binaries via Homebrew
brew install aartiles/tap/tenazDownload 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/tenazYou 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
Release Notes
Added
--model-forworks with--harness pi: each session's Pi is launched
with its role's model, and the run'smodels.jsonlists every model of
the map, keeping the settings your~/.pi/agent/models.jsonhas for
each. A role's model without--context-window-fortakes the window
that file lists for it, and its inputs unless--visionis given.
--swap-modelsworks with Pi too. See ADR 0011.tenaz init --swap-modelskeeps one model of a--model-formap
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 everytenaz runstart 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-modelsand
--no-swap-modelschange it; it is kept aslaunch.swapModelsin
run.json,status --jsonhasswap_models, andtenaz statussays
the models are loaded one at a time. When swapping, the
journal'sleaserecords name the model of eachloadandunload,
and record a model the engine dropped by itself asevicted. 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>, or500alone when there is no message. The message
of an SSE body is that of its firsterrorchunk. Pi puts the raw body
after the status instead. It shows in the session'sprovider-error
watchdog event andimage_refusal, the journal'simage-continuation
refusal, the image continuation note andtenaz 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,
orcargo 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
leaserecords end withreadywhenever 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 runnow
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-enginedoes 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'spreflightkeeps the engine's
errors inengine_errors. The log andtenaz statusshow 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.logonly with fx: Pi keeps no log
of it. See ADR 0016. - The preflight line of the log and of
tenaz statusshows the image
check (vision=trueorvision=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-engineruns past a failed preflight, astenaz 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 statusstarts with
settled=falsewhen 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\nor\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 | shInstall prebuilt binaries via Homebrew
brew install aartiles/tap/tenazDownload 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/tenazYou 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
Release Notes
Added
-
Before a session opens, Tenaz estimates its first request: the prompt
at 3 bytes per token, plusharnessReserveTokens(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 startsprompt 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, withdiffLimitBytes(40000)
still the most. A reviewer on a 65536 window atcontextBudget0.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
diffShareloads with the default. See ADR 0012. -
tenaz init --model-for ROLE=IDgives 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=Nsets that model's window (default: the
run's). The map is kept inrun.jsonunderconfig.roleModels;
tenaz statusnames the roles it covers and gives the builder's context
budget, andstatus --jsonhasrole_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 thecontextBudgetshare of that model's window; the
session's journal record hasmodel. 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, andtenaz runstops 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 runstops and names the others. -
A builder whose task needs work too heavy for a session (a render, a
long build) writes.tenaz/jobs/<TASK>.jsonwith 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 mostjobMinutes(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 havejobsPerAttempt(3) job files; one past them is removed unread
and the attempt is judged. The journal getsjob-startedand
job-finishedrecords. 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 inrun.jsonacross 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=0turns 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=0get no such note. See ADR 0010. -
A builder whose task cannot pass without changing a check of a done task
writes.tenaz/conflicts/<TASK>.mdnaming those checks and stops. Tenaz
blocks the task at once, without more attempts or a split, journals a
conflictrecord, and starts a replan that is shown the conflict. The
replan may add a card withamends: [<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
sandboxAllowWriteis noted: a line in
orchestrator.log,outside_writesand anoutside-writewatchdog
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 runsaysModel <id> is not being served at <url>, with why
loading it failed, instead ofthe 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.
- A session whose context budget is already under the cap a memory
- On macOS the sandbox lets the agent and
verifywrite 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./blendercall 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 ofverifyoutput three
times.run.jsonkeeps that failure ascurrent.verify_failure
(attempt,text). - A builder whose previous attempt failed
verifyword for word as an
earlier one is told so. Ifverifyreports 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 saysexit 1reads the same while real work goes on. - The release builds the macOS archive on GitHub's
macos-15runner
instead ofmacos-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:T021ddmay correct.tenaz/notes/T021d.mdand
.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 whyverify
never saw it.run.jsonkeeps the paths ascurrent.reverteduntil
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--resumewhen 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
ContextCapacityExceededbefore any reply) blocks the run at once, with
a reason that startsprompt 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 toengineRetriestimes and then blo...
0.2.0 - 2026-10-28
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 demosets 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'sbashhas 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) runsverifyon 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. -
verifyruns in a process group of its own, and Tenaz kills the group
when it ends or times out. A check that startednpm run devand 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. -
verifyruns withNO_COLOR=1as well asCI=1.CIturns 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
environmentverifyruns in. -
tenaz watchshows Pi sessions. Pi's stream keeps Pi's own records,
whichwatchdropped (or showed raw, marked?, atdebug); it now
translates them as the Pi adapter does. -
The setup wizard names the provider as
tenaz initwith flags does. For
an engine address that does not answer yet, it pinnedlocal; the flags
pinned the provider usual on that port (llamacppon 8080,ollamaon
11434, ...). Both now take that one, andlocalonly for another port.
The provider is part ofmodelinrun.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 | shInstall prebuilt binaries via Homebrew
brew install aartiles/tap/tenazDownload 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/tenazYou 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
Release Notes
Changed
tenaz --versionandrunner_versioninrun.jsonname the commit a
binary was built from a git checkout of (tenaz 0.1.0 (816e1d8)). Three
benchmark runs of different code all recordedtenaz 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## Overallin
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 infoand 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 | shInstall prebuilt binaries via Homebrew
brew install aartiles/tap/tenazDownload 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
Release Notes
Added
- The README opens with a GIF of a scripted
tenaz demorun, recorded
with VHS fromdocs/demo/demo.tape. tenazbinary withinit,run,status,finishandparams.--helpdescribes the workflow with a non-interactive example;run --help
lists the exit codes andstatus --helpwhat each phase means.tenaz runexits 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 --jsonprints 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 aState:line.tenaz runholds a lock on the project while it works: a second run on
the same project is refused, andstatustells a working run (running)
from one that was killed or failed (interrupted).tenaz guideprints an Agent Skill for delegating goals to Tenaz from
another agent.- The agent skill moved to
skills/tenaz/SKILL.md, sonpx skills add aartiles/tenazinstalls it. It now also covers installing and setting
up Tenaz:tenaz doctorand its fixes, what to ask the user to do
rather than doing it, andtenaz demo. tenaz demoshows a whole run in about a minute with no model and no
agent: the scripted harness builds a small goal in a newtenaz-demo
folder. It replaces the three commands the README gave for that, and
tenaz doctoroffers it while something is still missing.tenaz doctorchecks 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 initwithout--model, in a terminal, finds the
coding agents onPATH, the engines on their usual ports with their
models, context windows and vision, and ComfyUI, then prints the
equivalent command line.--interactiveforces it with a piped stdin. run.jsonpins how to launch the run (launch: engine API and
start/stop commands, proxy, harness executable, ComfyUI and workflows,
media timeout), sotenaz runneeds no flags; a flag given torun
replaces the pinned value and is saved.runandstatusdefault to
the current directory.- On a repository that already has commits,
initworks 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 inrun.json(origin).--in-placekeeps
the old behaviour;--worktreeand--namechoose the directory and
branch. The setup wizard asks which,runandstatussay 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.gitignorelines Tenaz added);
the run branch keeps the full history,--remove-worktreeremoves 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,runandstatussay how to move
it (mv .agent .tenaz, the.gitignorelines,--refresh-templates). - Tenaz keeps all its files under
.tenaz/: the brief is
.tenaz/brief.md(it wasBRIEF.md), the shared rules are
.tenaz/protocol.md(they were the project'sAGENTS.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 loadAGENTS.md, soinitworks in a project
that has its ownAGENTS.md, and sessions may not edit the project's
AGENTS.mdorCLAUDE.md. A run started withBRIEF.mdat the root is
not run:runandstatussay how to move it (git mv,
--refresh-templates). existingProjectparameter.initsets it when the repository already
has commits:verifyis 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,initasks the engine at--base-urlwhich 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 exposinggenerate_image. - The README and
docs/COMPATIBILITY.mdname MLX'smlx_lm.serverand
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 putstenazin~/.local/binand onPATH, and
a Homebrew formula inaartiles/homebrew-tap. requests.jsonlrows carry the engine's owntimingsper request
(server: prefill and decode milliseconds, cached tokens, and draft
counts when the engine reports them), andstatusprints the draft
acceptance.tenaz watchfollows 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) anddebug(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 aftercapacityRecoverSessionssessions 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 doctorlists the image engines it finds with the flags that use them.
TENAZ_NO_PROBEkeeps both from looking at the usual ports, for tests
and scripted setups.- A builder session that answers its prompt while
./verify --taskruns 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--allbut 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
contextSteerTurnsturns (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
verifyis 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 --statsinceinit) 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 minimalverify, then the task cards (ADR 0003). Each
session gets only its step's part ofbootstrap.md, steps 2 and 3 get
the spec and map in their prompt, andmaxBootstrapAttemptsapplies to
each step. A step whose files are already rig...