Releases: BackTrackCo/tenjin-agent
Release list
v0.1.0-alpha.19
Minor Changes
-
ddf34f7: Separate automatic router limits from mandatory manual payment consent, check Base USDC balance before signing, repair Ultravioleta discovery, and retire the obsolete payment skill.
maxAutoSpendandsessionBudgetlimit automatic router purchases only. Zero blocks positive automatic spending; dailynoneremoves the automatic daily ceiling. A missing daily limit defaults to $5 both before and after install. Manualtenjin payignores both configured limits and always requires consent for the quoted transaction, interactively or through--yesafter explicit user approval. It is never an autonomous workaround for a router refusal. An optional--max-price, creator/destination restrictions, supported payment terms and the balance check still apply.Legacy
bazaarPayandconfirmkeys are ignored after upgrade, including oldfalse/alwaysvalues. Doctor/status warn; install and refresh remove and report them while preserving current router/limit settings and unrelated fields.config setrejects the retired keys. No hand edit is required to keep routing operational.Manual and automatic payments share one ledger and duplicate guard. Manual payments remain in total reporting without consuming automatic budget headroom. Legacy exposure without mode/counter metadata conservatively counts as automatic until the existing window expires.
Missing Bazaar metadata or an unlisted endpoint is an ordinary direct-payment outcome. Only unavailable/incomplete lookups or exact-listing term differences require invocation-scoped
--ignore-warnings;--yessupplies payment consent only. The selected signer's balance is read before signing, with one bounded retry on an unreadable result. Confirmed insufficient funds or persistent read failure refuses and releases the reservation. Router calls retain their advertised-price checks and wire protocol; policy refusals now say “Blocked by spending policy.”Do not roll back to a config reader that treats zero as unlimited or ignores
none; preserve the operator's explicit automatic limits when reverting behavior.
Patch Changes
- 1f6100f: The pre-call router hook no longer blocks a WebSearch for the free library docs
lookup: the search runs as written, Context7 docs for the same query are fetched
alongside it, and when found they are added above the search's own results
(nothing changes when none are found). Paid lookups are redirected exactly as
before, andtenjin install --refreshgives the after-call hook the 15 second
timeout that wait needs. A provider's own error message, such as the docs
lookup's "no library matched" 404, now reaches the agent from
mcp__x402__requestinstead of a bare status telling it to retry. - de8c3c0: The pre-call router hook no longer redirects WebSearch or WebFetch twice in a
row to the same kind of lookup. When themcp__x402__requestlookup it
redirected to does not come back fulfilled (it failed, stopped to ask, or was
never called), or was a free lookup, whose success nothing verifies, that agent's next call matched to the same kind runs as it is,
while other lookups, and the call after that, are routed as usual; the main
agent and each subagent keep their own record. The redirect's reason tells the
agent it can make its own call again if the lookup does not cover it. - 2f41cb2: Keep
tenjin updaterefreshes at user scope when they run from the home directory, avoiding an unintended project-scoped x402 MCP registration. Recognize symlinked home paths, preserve existing project-only registrations and explicit project installs, and document cleanup for the accidental alpha.18 entry.
v0.1.0-alpha.18
Patch Changes
- df153c2: Eight fixes to paid lookups. A paid body is now always delivered: one that fails
the decision's success rule comes backunverifiedwith a caveat naming the
rule it missed, where it used to be refused after the money had moved. The
requesttool now sends its result once instead of twice, and declares
anthropic/maxResultSizeCharsof 200,000 so Claude Code keeps a full page read
inline; a larger result is saved by Claude Code itself and the model is handed
the path. A 633,016-character page read used to go past the tool-output limit,
so the model never saw what it paid for. The pre-call hook now denies the main
agent's WebFetch or WebSearch only when the paid lookup would run without
approval; over the cap or past the budget, the free call runs and the
after-call offer still applies, where a deny used to leave the page neither
fetched nor bought. The prompt hook follows the same rule: its hint to call
requestis shown only when that call would run without approval, where it
used to send the model to aneeds_approvalstop while its free tools would
have answered. Both, and a subagent's offer, also need the wallet to cover the
price: once the policy allows the spend, the hook reads the wallet's USDC
balance with onebalanceOfagainstrpcUrl, inside the hook's existing time
budget, and a balance below the price leaves the free call to run. A fresh
install's empty wallet used to get its WebFetch denied and then a refused
payment. A balance that cannot be read leaves the policy to decide, as before.
tenjin paywaits on the paid request for the seller's
advertisedmaxTimeoutSeconds, capped at 120 s and never shorter than
--timeout, instead of cutting off a signed payment at the 10 s default; the
unpaid probe keeps--timeout. The Bazaar lane now looks a resource up
itself: it asks each registry's/discovery/searchfor this URL under the live
payTo, and falls back to thepayTo-filtered list where a registry has no
such search. CDP's list ignorespayToand returns the same first page of
about 17,000 listings, so any endpoint nodiscoversweep had stored was
refused as unlisted. And the lane's refusals and the tenjin-pay skill no longer
name the shelvedtenjin discover. - 54e64c5: A router packet no longer carries rows the harness writes into the user's turn. A
background task or subagent finishing (origin.kind: "task-notification") and a local
command's output (<local-command-stdout>,<local-command-stderr>) arrive as
type: "user"rows withoutisMeta, so they were read as the user's words: they travelled
inhistory, and one could becomecurrent, even withrouter.context turn. They are now
skipped like harness meta rows. A<command-name>row, the command the user typed, stays. - 0e7a557:
tenjin uninstallleaves anx402MCP server it did not write. Before removing the
registration it reads the scope's own file the wayinstalldoes, and when thex402
entry launches something other thantenjin mcp, or the file cannot be read, it removes
nothing and says why instead of runningclaude mcp remove x402. An entry that is the
router's, or no entry at all, is removed as before.
v0.1.0-alpha.17
Patch Changes
-
2a95507:
tenjin hook <name>for a hook arm this binary does not know now exits 0 with
nothing on stdout, the same "no opinion" every handler gives on a bad event.
It used to exit 2 as a usage error, which Claude Code reads as a blocking hook
failure: a settings file written by a newertenjin install(a new arm, or a
source build ahead of the npm release) then failed every call of the matched
tool until the binary caught up. -
46fc65f: The router has an off switch and a context setting that every router hook and the
requesttool honour.tenjin config set router.enabled falsestops it on this
machine, andtenjin config set --project router.enabled falsestops it in one
repository through a committed.tenjin/config.json;--project --localwrites
a personal.tenjin/config.local.jsonbeside it instead, which belongs in
.gitignore. Off means the hooks send nothing and therequesttool answers
needs_inputnaming the key, paying nothing.router.context turnsends the
current turn with no prior messages. Every project file from the working
directory up to the git root applies (in a git worktree, the main checkout's
too), and each can only turn the router off or narrow what it sends.tenjin configshows
both keys with the file they came from, andtenjin doctorwarns when the hooks
are wired but the router is off in the directory it runs from. -
0be992c: Router hooks are now safe inside subagents. WebSearch and WebFetch are routed
before each call as before, and a fitting paid lookup still redirects the call
tomcp__x402__request; but a subagent is redirected only when it is known to
havemcp__x402__request(its owntools:, or the built-ingeneral-purpose,
ExploreandPlanagents) and your spend policy would pay without asking, and
it is routed on its own task rather than the parent's last message. When a free
call clearly fails (blocked, a server error, an empty page, a search with no
links, a network error; never a 404 or 410), the router is asked once and may
offer a paid lookup. A newPreToolUsehook onAgent|Taskappends a fitting
offer to the task a subagent is handed. Every line the router adds opens with
Tenjin router (installed by the user):and names the call
mcp__x402__request.tenjin doctornames custom agents whosetools:leave
the paid tool out. Existing installs keep working as they are;
tenjin install --refresh(whichtenjin updateruns) adds the new hooks. -
dea376f: Every router packet is masked before it leaves: the prompt, the prior messages,
the literal URLs and the pending search or URL go through onesealstep that
applies the publish scan's key, PEM, BIP-39 seed-phrase and URL credential rules
(a credential query parameter, a long token-shaped path segment). Harness meta
rows no longer travel. A nativeWebSearchorWebFetchwhose search or URL
carries a credential, or whose URL is local or private, now runs natively with
no router call. The mask also covers a quoted"password": "..."value,
Authorization: Basic,curl -u user:pass, a 40-hex node key in a URL path, a
64-hex key without0x(whole or split 32+32), a Solana secret key (base58 or
the keygen byte array), and a checksum-valid recovery phrase in any case, with
commas, quotes or line breaks between its words.The
requesttool refuses a live 402 above the price the routing decision
quoted, on every pay lane, before anything is signed. A provider or a stale catalog can no longer
charge more than it advertised;maxAutoSpendandsessionBudgetstill cap
every payment.
v0.1.0-alpha.16
Minor Changes
-
720f868: Grade a finding against the transcript it actually landed in, including a
subagent's own.Every arm now records the subagent it fired inside. The harness stamps
agent_idon a hook input that fires inside a subagent and leaves it off in the
main session, whilesession_idstays the parent's either way — so until now a
row written inside a child pointed only at a parent transcript that holds no
word of what the child did.events,injections,searchesand
pairing_closesall gain anagent_idcolumn in one step, and the prompt,
failure, pass, edit, research, dispatch and subagent arms all stamp it. The
subagent arm records the child the finding was relayed TO, which is the file the
verdict has to come out of; the close on a pairing records the worker that made
it, and counts for nothing — the promotion toverifiedstill asks for two
independent SESSIONS, because two subagents of one conversation are one laptop
in one checkout.Schema version 2, and tenjin-agent#247 is what it is. Anything after this
takes version 3. A fresh file is CREATED at the current shape and steps nothing;
a file that already exists is altered in place by the first open of either the
CLI or a hook, keeping every row. The step also BACKFILLSevents.agent_idfrom
thedata.agentIdthat tenjin-agent#242 has been writing since 2026-08-28, so
the fortnight of rows in between are still the worker's rather than the lead's.
ALTER TABLE ADD COLUMNis not idempotent, so the version is re-read inside the
BEGIN IMMEDIATEand the create and migrate branches are exclusive — pinned by
a dozen hook processes racing one version 1 file, and by a version 1 hook core
writing to a version 2 file, which is the ordinary state of a machine that
upgraded the CLI without re-runningtenjin install.One identity, parsed once. The prelude has a single reader,
identityOf,
answering the session and the agent together; an id that is not[A-Za-z0-9_-]
of 1–128 characters is refused rather than stripped, because it is also a
transcript filename and stripping a separator out of one id spells another id
exactly.NULLis the main session everywhere and never "unknown", with the one
place it becomes the''asession_statekey segment needs spelled out as
agentKey— soedited::<path>,edits::<path>andreplayed::<head>are
byte-for-byte the keys the lead's rows already sit under. The importance score
reads theeventscolumn instead of a JSON field, which is what makes "this
child was shown a finding" and "this child then fixed something" the same worker
rather than two.An id the harness stamps but this build cannot use is not the lead. It fails
the bound, so it names no transcript and no partition; filing the fire under the
main session instead would hand a child's search, edit or close to its parent,
and the score would then read that work as the lead's. Every arm drops such a
fire whole — no lookup, no event row, no decision row — rather than inventing a
reason bucket for a row that should not exist.tenjin gradereads that file. A row with an agent id is judged against
<session>/subagents/agent-<id>.jsonl, never the parent's. A relayed finding has
no anchor row in any transcript — the child is handed it as its opening context
and nothing records it — so it is judged from the child's first tool call onward,
by the same evidence rules everything else gets. It also leaves no injected text
on disk, so its span evidence comes from the piece's title alone, which usually
means a relayed finding is judged on the strong evidence (an explicit read, or
the URL) or not at all.unobservednarrows to what it always meant: nothing to read and nothing
that ever will be. Every subagent injection used to land there unconditionally,
which closed the whole handoff as never-seen; now only a relayed row with no
agent id recorded does — rows written before this version, or by an arm that
could read none off its input.--explainnames the agent and the file that
answered. -
79b8862: Codex runs the same loop.
tenjin installon a machine with Codex writes sevencommandentries to~/.codex/hooks.json(or$CODEX_HOME/hooks.json), all through the shim, beside the Claude Code set;doctorreports them as configured and observed in the ledger;uninstallremoves only the entries it wrote. The daemon serves/hook/codexwith an adapter built on payloads captured from codex-cli 0.153.4: root prompt lookups, per-child identity throughagent_id, multi-fileapply_patchedits marked in one fire, and the capture ask delivered as a Stopdecision: block.Shared boundary corrections that a second harness forced: the tool an arm reads is canonical (
command,paths,task,query,url) rather than a vendor argument bag; a stored session is namespaced by harness (claude:<id>,codex:<id>), soloop.dbstate from before this release does not join new sessions;tenjin searchstamps the thread it ran inside (CODEX_THREAD_ID) asagent_id, and the capture ask names a CLI miss only to the actor that opened it. A Codex shell result carries no exit status, so its completion stays unknown (an error marker is still a failure) and no pairing closes on it; the spawn tool's task is opaque on the wire, so no dispatch handoff runs for Codex. -
00b382a: Take a publish back from the CLI.
tenjin delete <postId>removes one of your own
pieces through the owner-scoped soft-delete atDELETE /api/posts/<id>, and
tenjin edit <postId> --status draft|publishedis the reversible half:draft
unpublishes without losing the id or the body,publishedputs a draft up. Both
reuseedit's signing path, and the MCP server exposes them astenjin_delete
(annotated destructive) and astatusargument ontenjin_edit.deleteconfirms in every mode, and never readspublish.mode. The mode is
consent to publish, not consent to destroy, sofull-autoasks here exactly as
reviewdoes. At a terminal it asksy/Ninline; anywhere else, including under
an agent or the MCP server, it refuses withNEEDS_CONFIRMATION(exit 3) carrying
the title, status, url, the--yescommand that confirms, and the reversible
--status draftalternative, which is the same exit-3 channelpublishandedit
already use.--statusis an ordinary change flag by contrast: it diffs, it prunes
when it already matches, and it rides the ordinarypublish.modegate.Every successful
tenjin publishnow prints the exact undo commands with the real
post id, and carries them on the--jsonenvelope asdata.undo, so an agent
reporting a publish hands over a real command instead of guessing one. The printed
removal command carries no--yes: it starts the undo, so each surface's own
confirmation still runs, and--yesappears only in a refusal payload's
confirmCommand, which answers a question the user has already been shown.A server refusal after the confirmation is the new
DELETE_FAILED(exit 4), whose
message says the piece is still live. No refused delete leaves a write credential
behind: the owner-scoped read that the preview is built from signs with a
read-scoped session, and only an actual approval mintsread+write, so both a
headless refusal and a declined prompt end with nothing on disk that a later write
could reuse. An approved prompt pays one extra in-memory signature for that, with
no second keystore prompt and no extra round trip.tenjin deleteis never
allowlisted: no mode carries it, and it is not delegable to a subagent.Promoting a draft settles what the draft publish deferred.
edit --status publishedre-scans the stored body at the block tier (a draft made on the web
desk was never scanned locally), claims the searches a
publish --draft --search-idparked on the draft (the ids ride the same PUT and
their local loops close), and writes the same-body dedup marker, so the next
publish of the promoted body dedups instead of creating a duplicate. The
server-returned post id is shape-checked as a uuid before it can reach the undo
line or the delete confirm payload. -
9656c04: The failure arm fires behind a fixed allowlist of command heads; every search names its arm;
publish --key(tenjin-agent#212, PR A).The allowlist is fixed.
gitis no longer a head the arm fires behind: every record it opened had come fromgit show … | grep ENOENTover source that merely mentions an errno.node,deno,pythonandpython3count only when their first argument is a file or their own test runner (node --test,deno test), sopython3 -c,node -eand a piped stdin never do;python3 -m pytestandpython -m unittestare read aspytestandunittest, the module being the program.tenjin searchnames itself too. The hook arms already sendtriggeron each/api/searchbody; this release adds the manual half, sotenjin searchand the MCPsearchtool over it sendcliexplicitly rather than relying on the server's default. The arms that send one areresearch,dispatch,promptandfailure. Telemetry only; a shelf that predates the field recordscli.tenjin publish --key <kind=value>(repeatable, up to 32; the MCP publish tool'skey) sends exact-match keys on the post body —fingerprint,package_version,command_head,repo— bounded before the wallet signs. Keys go out unverified;verifiedis the shelf's own claim about a key. A shelf withKNOWLEDGE_KEYSoff refuses a keyed body as ...
Bench-1 vendored toolchain: vitest 3.2.4 (node24, darwin-arm64)
Not a product release. This tag exists only to host one build artifact for the
Bench-1 benchmark harness (evals/benchmark/), so that the artifact does not
live in git history.
vitest-3.2.4-node24-darwin-arm64.tar.gz is the deterministic pnpm install of
the Bench-1 live task fixture's pinned lockfile: 781 files, darwin-arm64
natives, node ABI 137, vitest 3.2.4, pnpm 11.11.0. Every live-task
fixture_hash in the benchmark manifests folds this archive's digest in, so
these exact bytes are the pin. A rebuild produces different bytes and re-pins
every manifest.
The committed record beside the fixture,
evals/benchmark/fixtures/live/vendor/vitest-3.2.4-node24-darwin-arm64.json,
states the digest:
sha256:a1c55230908e6ad737ac20f06381c23fdc6a2f080488809992dad751ae5317b3
evals/benchmark/vendor.py uses the local file when it is there and fetches
this asset when it is not, verifying that digest before anything reads the
bytes. A mismatch is a refusal, never a warning.
Rebuild recipe: evals/benchmark/scripts/vendor-vitest.sh.
v0.1.0-alpha.15
Minor Changes
-
18385e4: Add a working native Hermes Agent integration.
tenjin install --harness hermes
now installs the Tenjin skills, adds a conservative MCP entry, and enables a
stdlib-only Hermes plugin that checks Tenjin beforeweb_search, attaches a hit
to that tool's result, and surfaces unresolved searches through
transform_llm_outputfor publish-back.Hermes reuses the same generated, bounded, fail-open retrieval/store/nag scripts
as Claude Code instead of carrying a second payment-facing implementation. The
installer honors an absoluteHERMES_HOME, embeds absolute executable paths,
preserves unsupported or user-owned YAML byte-for-byte, never overrides
plugins.disabled, keeps automatic detection inert until explicit activation,
and adds a warn-level doctor check. It adds noTENJIN_HARNESSpolicy selector
and does not copy or couple wallet state.Hook consent and plugin activation are two separate decisions.
--no-hooksand
--search-hooks offwithhold the Hermes scripts, plugin, and activation exactly
as they withhold Claude'ssettings.jsonentries; themcp_servers.tenjinentry
is a server registration, so it is still written. Where the stored
hooks.searchModeis what holds the plugin back, install andtenjin doctorsay
tenjin config set hooks.searchMode autorather than an install command that
cannot move the blocker. Withholding a write is not an uninstall, so install
reports it asskippedand names any enabled plugin an earlier run left behind.Re-pointing the MCP entry (an nvm switch, a pnpm-vs-npm global) rewrites the
managed block in place, leaving one marker comment and any neighbouring comments
untouched.tenjin doctortolerates a relativeHERMES_HOMEset for some other
tool, reports a baked MCP command that no longer exists as stale rather than
green, and shares the installer's classifier so its fix cannot point into a
conflict it did not predict. -
ba09366: Close a batch of open search loops in one call, and let the WebSearch hook say
which requests are its own.The hook now leads its
User-Agentwithtenjin-websearch-hook/<version>
instead of the CLI product, so a query that rode along with a web search is
separable from a question an agent deliberately looked up. Tenjin attributes a
request to the firstUser-Agentproduct, so that position is the whole
mechanism: it lands asclient_nameon the search row, and the marketplace's
/trending questions tier filters on that exact name rather than showing readers
queries nobody vetted. A deliberatetenjin searchis unchanged and still
identifies astenjin-cli. The caller handoff still composes behind whichever
product leads.tenjin installrewrites the hook scripts.tenjin outcome --search-idnow repeats, reporting one status against every id
named, and--all-opencloses this session's open hook loops. Both report per
id in the JSON envelope, and both refuse the whole batch before sending anything
if one target's id or status could not be right. A batch that closed some and
failed others says exactly which.The sweep is per session because the loop is: a session's open loops are its
own, and one that ends leaves its unpublished debt to decay rather than handing
it to whichever session stops next. The session comes fromTENJIN_SESSION_ID,
thenCLAUDE_CODE_SESSION_ID, the same resolversearchstamps entries with,
and an entry no harness could attribute stays in scope everywhere rather than
nowhere. There is no machine-wide sweep.--all-openreportsregeneratedand nothing else, and sweeps MISSes only. The
other statuses are claims about what a specific search did for the agent, and a
blanketusedover queries nobody examined one by one is attribution the
marketplace would be right to trust and wrong to believe. A hook search Tenjin
answered is left alone for the same reason one step further in: that is the
search where a piece may have been bought and read, andregeneratedwould
overwrite the only positive attribution the loop collects. Deliberate searches
are never swept. Both kinds are counted and named in the output, so a blanket
close cannot look more complete than it is.A batch stops at the first rate limit or transport failure and reports the rest
untouched, rather than spending an agent's outcome budget on requests that will
fail the same way: an open loop is the safe state, and the Stop hook raises it
again.The Stop hook's end-of-turn reminder now names ONE close command for the whole
batch of hook misses instead of a per-id one. Seventeen open loops meant
seventeenoutcomecalls, which is enough friction that the honest close stops
happening at all. -
01daf8b: Search now speaks schemaVersion 3 to
POST /api/searchwith
view: "decision".POST /api/agent/searchis a deprecated alias that answers
410 after one deprecation window (BackTrackCo/tenjin#137), so nothing in the CLI
still calls it: nottenjin search, not the MCPtenjin_searchtool, and not
the WebSearch/dispatch hook scriptstenjin installwrites.The v3 request is not the v2 request with a new path.
questionbecomes the
documentedquery,viewis named explicitly rather than left to the server
default, andfreshWithin/maxPrice/appliesTomove under a nestedfilters
object. That nesting matters: the v3 route STRIPS an unknown top-level key into
a non-fatalwarningsarray instead of rejecting it, so a top-levelmaxPrice
would have run the search unfiltered with no error anywhere.The response envelope changed with it.
decision+candidates[]became
matched+items[], and the MISSbrowse[]tail is gone — the decision view
draws no fallback shelf, so a miss is an empty result plus ahintpointing at
GET /api/articles.--jsoncarries the server's envelope verbatim, so an
agent parsingdecisionorcandidatesmust move tomatchedanditems. The
local search store keeps its CANDIDATES/MISS vocabulary, becausetenjin outcomebranches on it and older entries carry it; it is derived from whether
anything matched rather than read off a field that no longer exists.tenjin doctor's search-contract probe looks for/api/searchin the
deployment's OpenAPI paths. A deploy advertising only the alias now warns, which
is the case the probe exists for. -
e91f3eb: Two new harness hooks, so an agent looks before it researches rather than after.
A SessionStart primer.
installnow writes~/.tenjin/hooks/tenjin-sessionstart.mjs
and registers it forstartup,clearandcompact. It prints one paragraph:
what Tenjin is, the entry gate (public, durable, costly to reproduce), the
instruction to state the question in one line and search first, the reminder to
name Tenjin when enumerating research sources for a subagent, and what to skip.
It makes no network call and reads no state. Nothing else may join it — no update
line, no publish nudge, no open-loop summary — because the measured bottleneck is
the retrieval reflex, and a paragraph that grows stops being read.tenjin config set hooks.sessionPrimer offsilences it at run time with no re-install.A research-dispatch hook.
~/.tenjin/hooks/tenjin-dispatch.mjsfires on
PreToolUseforAgent|Task, the two names one subagent dispatch goes by across
Claude Code versions. The WebSearch hook only ever saw a question the agent had
already decided to ask the web; the expensive research is the work it delegates to
a subagent, and this rides along with that. It sends the dispatch's description
plus at most 400 characters of its prompt — a privacy bound, not a display one —
and on a CANDIDATES decision mentions at most two tested answers in the WebSearch
hook's own format, in the parent's context only, since the tool input is already
formed by then. It shares that hook's whole boundary: nopermissionDecision, a
2s fetch budget under the harness's 5s kill, and a silent exit 0 on every failure.
hooks.searchModegoverns it, sooffandremindbehave as they do for web
searches, and the disclosure at install time now names the subagent prompt and its
400-character bound.Two bounds on a fan-out. The same question is asked once per session, because
a fan-out dispatches near-identical prompts and the answer is already in the
store, and a session gets at most 10 dispatch lookups however wide it fans out, so
a ten-way research turn cannot put the fetch budget in front of every subagent.
Nothing fires on aWebFetch.Dispatches record into the CLI's own
searches.jsonunder a newdispatch-hook
source, so a HIT still attributes a later purchase andbuy <resourceId>still
resolves the read URL. They are never nag material: the Stop hook's strong arm
stayscli-only and its weak arm stayswebsearch-hook-only, an entry from any
other source is skipped unnagged rather than promoted, andoutcome --lastskips
them for the same reason it already skipped web-search entries. Because nothing
ever closes one, they also hold at most 15 of the store's 50 slots: a demand entry
is telemetry, and the store's other two jobs are resolving a payable read URL and
finding the last deliberate search, so a wide fan-out must not be able to drain
either.installdiscloses both hooks and what leaves the machine,uninstallremoves
both scripts and both entries, and the wiring stays idempotent: each script owns...
v0.1.0-alpha.14
Minor Changes
-
6b960e1: Let the agent that runs the CLI travel in the same
User-Agentfield, behind the
tenjin-cliproduct: exportTENJIN_CALLER_USER_AGENTwhen you launch it.Composition happens in one place (
composeUserAgentinlib/client-meta.ts) and
is idempotent: the caller value is decomposed into products, any copy of our own
product or comment is dropped from it, and the field is rebuilt from the package
identity. A retry, a nested helper, or an agent re-exporting the env it received
therefore cannot mint a secondtenjin-clitoken, and the caller's own products
survive byte for byte in their original order. The write still runs through the
Headers API, so a call-specificUser-Agentin any casing cannot erase the
composed field or add a second one.The handoff accepts a product sequence and nothing else, which is what keeps a
user, wallet, session, hostname, or machine identifier structurally out of it. A
value that is not printable ASCII, not a bare product sequence, or long enough to
push the composed field past the 512 characters the server accepts is omitted
whole: the CLI identity travels alone rather than as a truncated token that would
read as a different client. It is self-reported telemetry, never trusted policy
input, and no signed header set changes: the payment and RFC 9421 signatures
cover the same material they did before.The generated WebSearch hook script carries the identity too. It is the CLI's
highest-volume request path and it imports nothing, so it had been sending Node's
defaultUser-Agent: nodeand every hook-driven search was landing as a
synthetic client namednodethat was in fact this CLI. It now composes the same
field, from constants interpolated out oflib/client-meta.tsat generation time
and the caller handoff read at run time, with a test that runs the shipped bytes
and the real composer over the same inputs so the two cannot drift. Because the
version is baked when the script is written, an installed hook keeps reporting
the version that wrote it untiltenjin installis re-run:tenjin update
replaces the binary and leaves hook scripts alone. -
77244fd: Publish-back loop DX (tenjin-agent#161, #162), plus a skill diet.
The Stop hook's weak arm now fires at most once per session. A research fan-out
mints new searchIds every turn, so per-searchId dedupe never rate-limited it and
the batch read as harness debug output.hooks.stopNaggainsdeliberate-only,
which drops that arm and keeps the deliberate-search one, so silencing the noise
is no longer the cliffoffis. Whatever the hook raises now leads with the
resolved publish.mode, which an agent otherwise had to runtenjin config get
mid-publish to discover. That line resolves the mode in the CLI's own order,
global config then a project.tenjin.jsonfound by walking up from the session's
cwd thenTENJIN_PUBLISH_MODE, so the hook,publish, anddoctoragree on what
the next publish in that directory will actually run under. That walk stops where
the CLI's stops: at the repo root, never above$HOME, and skipping a
.tenjin.jsonowned by another user. A projectfull-autoconservatively reads
asauto.tenjin publish --search-id <id>re-links a loop something else already closed,
so a MISS closed asregeneratedwhile the answer was still being written can
still be attributed to the piece that answered it. A repeat publish against a
loop an earlier publish already closed now says so instead of reporting a fresh
close it did not make.tenjin publish --search-id <id>now sends that id to the server, which accepts
and documents it as the attribution for the MISS the piece answers. It never
reached the wire before, so every publish that named a search still went out
unlinked, the thing #161 is actually about. The id is validated at the command
edge against the shape the server declares rather than the CLI's looser uuid, so
a bad one costs a message instead of a 400 collected after the wallet signature.
A--draftpublish sends no attribution, matching the local ledger, which has
always treated a draft as answering nobody.Installing Tenjin is the consent for auto-publishing
Every install settles
publish.modeatautounless told otherwise, and the
first install, headless included, writes the two mode-gated rules
Bash(tenjin publish:*)andBash(tenjin edit:*)alongside the nine free ones.
The terminal says it in plain words, two lines and a link: what the agent will now
do, how many rules are allowed, and the one command that turns it off. The
complete disclosure (both rule strings, unattended keystore access, the read+write
session credential minted on disk, publishing the contents of any local file the
agent can read, the--base-urland--yescaveats, all three undos) is
unchanged in docs/agent-permissions.md, intenjin doctor --json, and in this
command's own--jsonenvelope.
edittravels withpublishbecause it runs the identical consent gate, touches
only posts the wallet already owns, spends nothing, and creates no new public
content; an auto mode that can publish a post unattended but cannot fix that
post's price is the asymmetry the mode exists to remove.tenjin config set publish.modesyncs the pair at the moment the mode changes.
Loosening to auto or full-auto asks once at a real terminal, naming both rules,
and writes on yes; no terminal,--json, or a decline writes nothing and points
attenjin install. Tightening back toreviewretracts both unprompted through
a retraction-only pass that never appends, so it retracts on any machine
carrying them, including one whose free tier no longer matches what this release
ships.uninstallreclaims both. The bare CLI, with no install ever run, still
defaults toreview.tenjin installandtenjin config setnow preserve~/.claude/settings.json's
file mode, so achmod 600on a file holding anenvblock survives a write.Every install line that reports the allowlist says what a
reviewrun took back,
and names the file it took it from. The retraction runs above the guards that
decline a write, so a run can retract and then skip, and two skip lines described
the file as untouched: "unchanged" under--no-allow-free-verbs, and "not wired
(Claude Code only)" under another--harness, which read as "your Claude settings
were left alone" on the run that had just deleted two rules from them.Moving to
reviewretracts on every install path. The retraction sat below the
--no-allow-free-verbsand--harnessguards, so a run that declined the free-verb
WRITE also silently declined the REVOCATION:install --publish-mode review --no-allow-free-verbswrotemode: reviewand left both rules allowed. It also
returned before the additive pass, so one review-install retracted the pair,
claimed a free tier it had not written, and stranded a legacy rule for a second
run to clear. It now runs first and falls through: one run retracts, wires the
tier, and sweeps legacy rules, and the summary stops describingpublishand
editas "commands that no longer exist".tenjin doctorresolvespublish.modefrom the project.tenjin.jsonlike
config getandpublishdo. It read the global file and env only, so inside a
repo pinned toreviewunder a globalautoit reported the machine as needing a
grant the next publish there would never use.The install prompts are shorter. The permissions question is two sentences and a
link rather than a tier inventory, no prompt recites aBash(...)rule an
operator has not met yet, the search-hook question drops its "(Escape skips,
registering nothing)" hint, and the summary no longer promises that "your harness
still shows each command for approval", which the same mode writes a rule to
remove.tenjin search --jsonno longer writes the publish-back hint to stderr. It went
straight to the stream rather than through the human rendering, so the flag whose
help promises to "suppress human stderr rendering" left ~260 bytes of prose beside
every MISS envelope.tenjin uninstallstops contradicting itself. Its help and itskeptlist both
claimed nothing under~/.tenjinis touched, while the same run correctly deleted
~/.tenjin/hooks/*.mjsand listed them underscripts. Deleting is right, so the
two sentences now state it: wallet, config, library and search history kept, the
generated hook scripts removed.tenjin install --dry-runreports the permission rules a real run would write,
including the mode-gated grant with its disclosure and undos, in the same envelope
fields flaggedplanned. An operator dry-running to learn whetherpublishand
editwould be granted was previously told only "unchanged (dry run)".Both CLI skills are restructured around what an agent has to decide, with the
detail one hop away. tenjin-search stops re-explaining tenjin-publish: the
after-a-MISS section was a second copy of the mode table, the exit-3 render rule
and the consent contract, which is one contract stated twice and a standing
invitation to drift, and it now delegates in three sentences. tenjin-publish
leads with the consent mode as a table, replaces two detector catalogs with the
triage that actually helps (which warnings are ordinary in technical writing,
...
v0.1.0-alpha.13
Minor Changes
-
a38f843: Self-update from the CLI, and tell agents when to use it.
tenjin updateinstalls the newest version npm offers this build, pinned to the
exact version the registry names;--checkonly reports. The target is the
newest of the build's own channel tag andlatest, because which tag a publish
lands on is a property of the release pipeline rather than of the version
number:alphasat on 0.1.0-alpha.7 while 0.1.0-alpha.8 through .11 shipped on
latest, and a channel-only lookup would tell every alpha user they were
current. The daily check resolves the same way, so the two cannot disagree.The manager that owns the install performs it: npm, pnpm, and bun are each
driven with their own global-add command, since the PATH race that makes a
second install dangerous comes from cross-manager writes rather than from asking
the owner to do what it already owns. What cannot be driven is refused with the
instruction that is correct there instead of writing a global you never had: a
source checkout updates by git, an npx run has nothing installed to replace, a
project-local copy updates where it is declared, and yarn is refused because
yarn global addexists only in yarn 1.--checkanswers from all of them.The daily check now reaches agents, not just humans. Alongside the dim stderr
line a TTY gets, a newer version appears asupdateAvailable(withcurrent
andlatest) on the JSON envelope and on the generated hook scripts' output, so
the agent driving a command can runtenjin updateitself at a moment it picks.
Nothing installs on its own: a CLI that starts a fresh process per invocation
has no deferred-activation window to hide a binary swap in, and a silent version
swap reshapes output exactly where nobody is watching. Newupdate.modeconfig
key,nudge(default, all three surfaces) oroff(none, and no request to
npm). The field is read from the check's cache, so it costs no request and no
delay on the command carrying it.Every install runs as a binary or
node <script>and never through a shell,
which is also the only form win32 can spawn, bounded by a 5 minute budget, from
the home directory, with the manager's output sanitized before it reaches a
terminal.tenjin updateis documented as never-allowlisted for agents: it
replaces the binary the agent then runs.
Patch Changes
- dcc6b5a: tenjin-publish gains the harvest ask (tenjin-agent#145 item 2, redesigned):
"anything from our recent work worth publishing?" now routes into the skill
explicitly, with a sweep procedure — grade what is visible in the
conversation against the sell rubric, offer survivors once as one batch, a no
is final and never re-asked, and never dig through transcripts or archives
the user did not hand over. This replaces the weekly scheduled-harvest design
outright: an OS-level cron written during onboarding has no precedent and
reads as intrusive, out-of-session runs create a session-archive dependency,
and per-session nudge telemetry shows unprompted reminders do not convert.
In-context, user-invoked, zero scheduling. One trigger clause added to the
always-loaded description; the procedure lives in the body.
v0.1.0-alpha.12
Patch Changes
-
a402916: Post outcome reports to
/api/searches/:id/outcomes, the path the server now
documents after BackTrackCo/tenjin#616 dropped the/agentprefix. The contract
fixture and the live drift pin move with the client, so the scheduled
contract-drift run goes green again.No fallback: tenjin serves the old
/api/agent/searches/:id/outcomesspelling as
a real alias onto the same handler for one deprecation window, so both spellings
answer identically today and the pinned path is the one that survives the window. -
c315e09: Widen the supply/demand triggers (tenjin-agent#145 item 1) by restructuring
both skill descriptions rather than appending to them, holding length at
parity with the previous wording. tenjin-search now leads with both search
moments: before regenerating expensive research, and before empirically
probing a third-party library or platform's undocumented behavior; the skip
list narrows from "the thing in front of you" to "your own code" and the two
skip sentences merge into one. tenjin-publish reorganizes its triggers into
three routes in (user ask, after-a-MISS, unprompted work worth selling), the
unprompted route covering substantial research and empirical proofs the docs
don't state. Two bundled fixes surfaced by the eval work: the requires-CLI
sentence becomes an explicit stand-down ("does not apply and must not fire,
not even to route the question"), taking the pre-existing no-CLI defer red
from 1/4 on main's wording to 2/4 in two independent samples; and preflight's
mirror-drift check now only gates runs that install the vendored tenjin
mirror, so non-mirror skill evals no longer need --no-preflight during #88.
Clean-room results: trigger eval extended 20 to 22 cases (one per side of the
new probe boundary) and scores 22/22 under the final wording, in-sample as
with the previous tuning. The two defer queries still firing both carry an
explicit user directive to use Tenjin while this skill is the only Tenjin
skill installed in the eval room; that residual is documented on the PR
rather than fought with more description weight.
v0.1.0-alpha.11
Minor Changes
-
62c2c25: Card-fund the wallet from the terminal.
tenjin fund [amountUsd]signs a SIWX
proof with the wallet's own key, asks the Tenjin backend to mint a Coinbase
Onramp checkout URL for that same address, prints it, opens it in the default
browser, and polls the Base USDC balance until the purchase lands (--no-open
and--no-waitopt out). The link is single-use, expires in about five
minutes, is bound to this machine's network, and requires a Coinbase account
to complete; onlyhttps://pay.coinbase.comURLs are ever opened. A matching
tenjin_fundMCP tool mints the link and returns it for the agent to hand to
the human (no browser open, no poll; minting moves no money and the payment
itself happens on Coinbase's authenticated page).sendstays off MCP.The link goes to stderr the moment it is minted, on every surface, because the
stdout envelope that also carries it is written only after the poll and the link
does not live that long. Opening a browser and polling are interactive
behaviours: both default off when stdout is not a TTY, so a piped,--json, or
MCP run returns as soon as the link exists.pollStatuson the envelope
distinguishesskipped,unavailable,timed-outandarrivedinstead of
collapsing three of them intofunded: false.tenjin fundis never
allowlisted for Bash: a prefix rule would clear--base-urlwith it, which the
MCP tool's amount-only input does not. -
5e1148e: Remove the candidate pen. Implements #133.
A publish decision is made once. Parking a draft "for later" turned that into a
backlog nobody returned to, and the reminder that would have resurfaced it is the
repeat-nag class this CLI stopped emitting — so rather than warn about the pen for
a release, it is gone now, in the same release as the skills that stopped teaching
it.Removed:
tenjin candidate add,list, anddrop; thetenjin publish --candidatepath; the local candidate store; thetenjin_candidateMCP tool;
and theBash(tenjin candidate list:*)line from the recommended free-verb
allowlist, which is now eight rules rather than nine. If an earlier version wrote
that rule into your~/.claude/settings.json, the nexttenjin installremoves
it and says so: a grant for a command that no longer exists is bloat we created,
so clearing it is our job rather than yours.tenjin uninstallreclaims it too.
Nothing else in that file is touched — rules you added yourself, other keys, and
their order all survive.The Stop hook's open-loop reminder and the MISS
publishBackhint keep the shape
they took when parking was deprecated: publish it back, or close the loop with
tenjin outcome --search-id <id> --status regenerated. Nothing is saved to come
back to, and the outcome report is what marks the search resolved so the reminder
never raises it again.Anything already in
~/.tenjin/candidates/is left exactly where it is. It is
your content, so nothing deletes it andtenjin uninstallwill not either — but
nothing reads it any more, so a draft you want is a file to open at
~/.tenjin/candidates/<id>/draft.mdand publish withtenjin publish <file.md>.
Thecandidateresolution value stays parseable in the search ledger so an older
searches.jsonstill loads; nothing writes it now. -
5cc75d2: Make the MISS → publish loop closable on the path agents actually take. Until now
onlytenjin publish --candidate <id>could name the search it answered, so the
bare file publish that both the Stop hook and the auto-mode skill prescribe left
the search open in the local store and the reminder kept firing at work that was
already on the marketplace.tenjin publish <file.md> --search-id <id>now closes
it, and prefills the searched question into the answer card'squestionsAnswered
when neither--questionnor the draft's frontmatter names one — that phrasing is
what the next searcher sends, so it is the right fallback. The prefill is dropped
rather than truncated when the stored question runs past the card's 200-character
item bound, because a prefill nobody asked for must not fail a publish that was
otherwise fine.--search-idwith--candidateis a usage error (the candidate
already carries its own searchId), a malformed id refuses before any wallet touch,
and an id the local store does not know still publishes and says so. What happened
is reported in both registers: a stderr line for a human, and a
search: { id, closed, prefill }field on the JSON receipt, because--json
suppresses the stderr notes and an agent that named a search could otherwise not
tell a closed loop from an open one, nor learn that its question was too long to
become a card entry.closedreports the OUTCOME of the local write rather than
the intent to make it: the store update is best-effort and never throws, so a
lock it could not take comes back asclosed: falseand a line naming the
command that closes the loop by hand.A
--draftnow parks privately on BOTH paths and closes nothing. It leaves the
named search open, and — this is a behavior change — it also leaves a
--candidatedraft parked instead of clearing it. A draft answered nobody, so it
is not the publish that retires the pen entry; the later real publish is. The
receipt reportscleared: falsewith nowarning, which is how a deliberate hold
reads against a clear that failed.Every surface that hands you the closing command now hands you a complete one. A
MISS'spublishBackpayload emittedtenjin publish <file.md> --jsonbeside a
parkline that already carried the id, so the arm the agent was most likely to
copy was the one that closed nothing; both arms, and the stderr line beside them,
now name the searchId.The Stop hook's open-loop reminder is reworded to match. It named a flag that did
not exist (tenjin publish, searchId <id>) and it carried no safety qualifier, so
it nagged just as hard on a question about private infrastructure as on a durable
public finding. It now emits the real command and conditions the publish arm on
the finding being public, reusable and rights-clean, with parking as the other
arm. The batched line for WebSearch-hook misses gets the same command syntax.The reminder is now scoped to the session that opened the loop. The search
ledger is machine-global, so the Stop hook was raising whichever open MISSes it
found at whatever session happened to stop next, including a sibling session's
work that this one never did and cannot close. A stored search now carries an
optionalsessionId, and the Stop hook parses thesession_idits turn-end
payload already carries and skips entries stamped with a different one — without
marking them nagged, so the session that owns a loop still gets its single
reminder. The scoping is deliberately one-directional: an entry nothing could
attribute is still raised in every session, so a loop can never go invisible in
all of them at once, and a payload that is malformed or names no session falls
back to exactly the old machine-global behavior.Both recorders stamp. The WebSearch hook reads the
session_idthe harness puts
on its stdin, and a deliberatetenjin searchreads the same value from
CLAUDE_CODE_SESSION_ID, which Claude Code exports to Bash tool subprocesses, so
a CLI search and a hook search in one session carry the same stamp.
TENJIN_SESSION_IDoverrides it for anyone wiring this up by hand. On a harness
that exports neither, a search records no session rather than guessing one, and
falls back to being raised everywhere.tenjin publish --excerptsets the public preview. The excerpt is what every
non-buyer reads — the 402 preview, the directory feed, search results, RSS, the
social card — and the server only derives it from the body's leading prose when
the client sends none. Frontmatterexcerpt:already reached the wire; the flag
did not exist, so the one-shot path an agent actually uses had no way to say what
a stranger sees, and a piece whose answer sat too high leaked its verdict for
free.--excerptwins over frontmatter, both are refused over the server's
500-character bound rather than truncated (a silently cut preview is a different
preview), and the refusal now happens at the command's edge instead of inside the
request builder, so it costs a message rather than a keystore unlock.Every free-text field that ships is stripped of control bytes, escape sequences
and bidi overrides. None of it is necessarily typed by the person publishing: a
card question can be prefilled from a stored search, and the title, excerpt,
tags and every card field can arrive over MCP from an agent that read them off a
fetched page.trim()removes neither a CSI sequence nor a right-to-left
override, so without this a payload rides into the marketplace and renders in
every future reader's terminal. The strip lives in the two request builders that
publishandeditshare, so it covers both commands and both MCP tools by
construction rather than by each flag remembering, and it runs before the length
bounds, since the stripped text is what the bound has to describe. The post BODY
is deliberately left alone: that is the author's own markdown, and rewriting it
is a content change nobody asked for. Ordinary unicode, including emoji ZWJ
sequences, is untouched.The MCP
tenjin_publishtool now forwardssearchIdandexcerptto the
command core. The tool advertised both, because the input schema...