feat(outcome): batch close, and a WebSearch hook that names itself - #177
feat(outcome): batch close, and a WebSearch hook that names itself#177vraspar wants to merge 4 commits into
Conversation
Closing seventeen open hook loops took seventeen `tenjin outcome` calls in one session, and every hook query reached the marketplace as `tenjin-cli`, indistinguishable from a question an agent chose to look up. - The WebSearch hook leads its User-Agent with `tenjin-websearch-hook/<version>` instead of the CLI product. The server attributes on the first product, so that position is the mechanism that reaches `lookups.client_name`; tenjin's /trending questions tier filters on the name. Hook script v18. - `tenjin outcome --search-id` repeats, and `--all-open` closes every open websearch-hook loop in any session. Per-id results in the JSON envelope, and the whole batch is refused before any request if one status is incoherent. - `--all-open` reports `regenerated` only; deliberate searches are left open and counted in the output. - The Stop hook's weak-arm nag names one batch close command, not one per id. 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.
A1igator
left a comment
There was a problem hiding this comment.
Review: two blockers on what --all-open sweeps, the rest is close
Reviewed against main at 0c92f5b, verified at 5adcabb. Findings below were run against a checkout of the head, not read off the diff: probes are named where they exist.
What's solid:
- The cross-repo claim in the body checks out on the tenjin side, verified rather than taken on the word:
resolveClientProductreturns the firstUser-Agentproduct and falls back toX-Tenjin-Clientonly when that is absent, invalid, or a generic runtime (lib/client-product.ts:53-63), andtenjin-websearch-hookis absent fromKNOWN_CLIENT_PRODUCTSso it folds tootherexactly as described. - Negative control on the rename: no tenjin route gates behavior on the literal
tenjin-cli. A grep acrosslib/andapp/finds it only inclient-product.ts's vocabulary, a schema comment, and agent docs, so the new product cannot change server behavior, only attribution. - The wire-identity test asserting the field off a real socket rather than off the generated string, and pinning the name as a literal on the side the contract can drift from.
--all-openrefusing the four non-regeneratedstatuses before anything is sent, with all four pinned.
Major
-
[data-integrity]
--all-opencloses hook searches that Tenjin answered, reportingregeneratedfor them: the sweep filters onsourceand unresolved only, with no test ondecision(outcome.ts:249-255), but the WebSearch hook recordsCANDIDATESentries under that same source, before the emit (hook-scripts.ts:610 and :652-656). Probed on the head: seed one hookMISSand one hookCANDIDATESentry, run--all-open --status regenerated, and two POSTs go out, one of them for theCANDIDATESid. That entry is the one where the agent was shownTenjin lists a paid answer titled "..."and may have bought and read it, soregeneratedthere is the same unexamined claim the PR refuses to letusedbe, and it overwrites the only positive-attribution signal the loop exists to collect. The Stop hook's own weak batch isMISS-only (hook-scripts.ts:872), so the line saying "N web search(es) this session had no Tenjin answer" now names a command that reaches past what it describes. Fix: addstored.decision === 'MISS'to theresolveAllOpenfilter so the two agree, and count the rest the waydeliberateLeftOpenis counted, so the sweep still cannot look more complete than it is. The batch tests seedMISSthroughout (outcome.test.ts:525-540), so a case seeding aCANDIDATEShook entry is what would have caught this. -
[agent-usability] the sweep is machine-global while the nag that recommends it is session-scoped:
ownedByThisSessionexists so a session is never nagged about a sibling's loops, and it skips a foreign entry unnagged precisely so the owning session still gets its reminder (hook-scripts.ts:811-822);sessionIdwas added to a machine-global ledger for exactly that (search-store.ts:66-84).resolveAllOpenapplies no session filter, so whichever session stops first sendsregeneratedfor a sibling session's still-live loops and silently removes its reminder and its publish prompt. Probed: two entries stampedsession-Aandsession-B, one--all-opencall, two POSTs. The weak line reads "N web search(es) this session had no Tenjin answer" and then names the machine-wide command, andskills/tenjin-search/SKILL.mdsays only "every open loop the WebSearch hook recorded", so nothing in the agent-facing copy says the blast radius is the machine. Fix: scope--all-opento the current session by default, using the resolver search.ts:233 already has (TENJIN_SESSION_ID, thenCLAUDE_CODE_SESSION_ID), keeping unstamped entries in scope the way the Stop hook does, and put an explicit flag on the wider sweep. If the wide sweep is the intended default instead, say "on this machine, in any session" in the skill and the Stop line, and treat it as a decision below.
Minor
-
[hygiene] the pre-flight covers the status but not the ids, so a typo sends half the batch: outcome.ts:81-85 says "Every target before any request", but the id shape is only checked inside
postOutcomes(agent-api.ts:455-459), which runs per id inside the send loop. Probed:--search-id <valid> --search-id not-a-uuidsends one POST, then fails the whole call asAPI_UNREACHABLE(exit 1) with "Retry the failed ids", where the real class isUSAGE(exit 2) and no retry will ever help. Fix: validate every id against the same regex inresolveTargets, next to the coherence loop, and derive the thrown code from the failures so an all-USAGEbatch still exits 2. -
[agent-usability] a partial batch names no ids in the human rendering: the per-id results ride
details(outcome.ts:134-143), and the renderer emits onlydetails.findings, deliberately: "Every other details shape stays machine-only" (output.ts:245-253). Without--jsonthe caller gets "Reported regenerated for 2 of 3 searches; 1 failed" plus "Retry the failed ids with --search-id " and no ids to retry, and thehumanLinesbuilt just above, including thedeliberateLeftOpenline, are discarded by the throw. Fix: put the failed ids in the message or infix. -
[security] the sweep has no bound and no early stop, against a 60/min budget: the store holds up to 50 entries (
MAX_ENTRIES, search-store.ts:15), the loop is sequential with the default--timeoutat 10000ms (cli.ts:22), and nothing stops it early. Tenjin'sOUTCOME_RATE_LIMITis 60/min keyed on client IP, and its comment says the looser window exists so outcome reporting "must never spend the lookup budget and self-429 an agent mid-task"; one sweep can spend 50 of those 60, and two sessions sweeping in the same minute on one IP 429 each other. During an outage this is the command the Stop hook now injects into every session's context, and it will sit for up to ~8 minutes at turn end before reporting. Server-side batching across searchIds is correctly out of scope here (the route is per search id), so the cheap fix is to stop the sweep on the firstRATE_LIMITEDor transport failure and report the remainder as untouched: an unclosed loop is the safe state, and the Stop hook will raise it again.
Nits (3), none blocking
-
[hygiene]
--resourcereaches a search the caller never named: outcome.ts:74-80 refuses a resource only whentargets.length > 1, so--all-open --resource <uuid>with exactly one open hook loop attaches the resource to whatever the sweep happened to find, and with zero open loops it is ignored in silence. Refusing--resourceunder--all-openoutright is the simpler rule. -
[hygiene] the "status name first" comment is now false for
--all-open: outcome.ts:67-68 claims an unknown status still fails as an unknown status, butresolveTargetsnow runs first andresolveAllOpenreadsargs.statusbefore the vocabulary is validated (outcome.ts:237), so--all-open --status bogusreports the--all-openrestriction. Harmless, since the message quotes the value back, but the comment says otherwise. -
[hygiene]
patchunderstates the changeset: this adds a CLI flag, a repeatable flag, a new MCP input field, and changes the wire identity of the highest-volume request path. The comparable User-Agent change (#126) shipped as a Minor, perCHANGELOG.mdunder 0.1.0-alpha.14.
Verified, not issues
- The permanent-column effect of the rename is exactly what the body claims and nothing more:
canonicalClientProductfolds an unlisted name toother, the swept rawclient_namekeepstenjin-websearch-hook, and no request-path branch reads the product. Worth an issue rather than a PR-body paragraph, since it puts a step change in a permanent metric nobody is watching for. composeUserAgent'sproductseam is not a new injection surface: the caller handoff is still totally rejected unless every token is a bare product, andOWN_PRODUCT_NAMESnow drops a handed-back hook identity in either direction, pinned in both suites.idsOfdedupes before the send loop, so a repeated--search-idis one request, and the flattened single-result fields keep the olddata.searchIdenvelope.- The single-target failure path still throws the original error with its original code, so nothing that reads
outcome's error code today changes. HOOK_SCRIPT_VERSIONis bumped to 18, so an installed script is rewritten rather than left sending the old identity.- skill-drift is green on this head, so the
skills/tenjin-search/SKILL.mdedit is mirrored.
Decisions for the owner: whether --all-open is a per-session or a per-machine sweep (Major 2). Everything else follows from that answer: if per-machine is intended, the copy in the Stop line, SKILL.md, and command-reference.md needs to say so, since all three currently read as session-local.
Heads up on a rebase, not a review finding: #158 touches five of this PR's files (README.md, docs/command-reference.md, skills/tenjin-search/SKILL.md, src/cli.ts, src/mcp/server.ts), #150 touches src/lib/client-meta.ts and src/lib/hook-scripts.ts directly, and #113 touches src/lib/hook-scripts.ts and its test. Whichever lands second eats the conflict.
Verdict: comments-only, two Majors. Both are about what --all-open reaches rather than about the mechanism, and both are a filter away. Ran locally against the head checkout: pnpm install --frozen-lockfile, plus three throwaway probes on runOutcome (hook CANDIDATES sweep, cross-session sweep, malformed id in a batch) with a negative control that a resolved entry is not swept. CI is green on this head, and the tenjin-side claims were checked against that repo rather than taken from the body.
|
Two operator dispositions on the review's open ends:
|
…front Review fixes from PR 177. - `--all-open` sweeps MISS entries only. The hook records CANDIDATES under the same source, and those are the searches where a priced answer was shown and may have been bought, so `regenerated` there would overwrite the one positive attribution the loop collects. Answered entries are counted and named, like deliberate ones, and the sweep now matches the Stop hook's MISS-only batch. - Every --search-id is validated before the first request, so a typo no longer sends the ids ahead of it, and a batch whose failures agree keeps their code: an all-USAGE batch exits 2 rather than reporting an outage. - The partial-failure fix line names the ids to retry; without --json the envelope's per-id results are not rendered. - A batch stops at the first rate limit or dead network and reports the rest untouched. A non-2xx is not a halt: it covers a 400 about one id. - `--resource` is refused under --all-open outright. - Changeset patch -> minor: new flags, a new MCP input field, and a changed wire identity on the highest-volume request path. 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.
`pnpm sync:skill` output, nothing hand-written. The canonical page changed upstream (agent search now matches a piece's own body/title/excerpt rather than the answer card, and the card is what makes a piece a candidate at all), so the committed mirror went stale on its own. Not caused by this PR: a clean origin/main checkout drifts identically, and this branch never touched skills/tenjin/SKILL.md. It rides here because the drift check runs on any PR touching skills/**, and the workflow's own remedy for a red PR check is `pnpm sync:skill` locally, then 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.
A1igator
left a comment
There was a problem hiding this comment.
Review round 2: both Majors addressed, one new item outside the code
Delta only, 5adcabb7..6aeed4b3 read as one round. Verified at 6aeed4b.
Resolved since round 1
- [closed] Major 1,
--all-openclosing answered hook searches: the sweep now filtersdecision === 'MISS'and counts the rest asansweredLeftOpen(outcome.ts:325-331), with a human line, a JSON field, and a test that pins one POST, the answered id absent from the wire, and its store entry left unresolved. The flag help, the MCP description, the reference table, and the skill all say MISS now, so the Stop hook's line no longer names a command reaching past what it describes. - [closed] Minor 1, ids validated mid-batch:
assertReportableIdruns in the pre-flight loop (outcome.ts:249-254) againstUUID_RE, which is exactly whatpostOutcomesenforces (SEARCH_ID_RE = UUID_RE, agent-api.ts:448), so nothing can pass here and fail inside the loop.failureCoderestores exit 2 for an all-USAGEbatch, and the test asserts zero requests. - [closed] Minor 2, a partial batch naming no ids to a human: the ids ride
fix, which is the one detail shape the human renderer prints (output.ts:118-119), bounded at five with a pointer to the envelope past that. - [closed] Minor 3, an unbounded sweep: it halts on
RATE_LIMITEDorNETWORK_ERRORand marks the remainderuntouchedwithout resolving them locally (outcome.ts:64-69). Both codes are reachable on this route, which is what makes the set real rather than decorative:httpRequestreturnsok: truefor any status sopostOutcomes' 429 branch is live, and network and timeout both map toNETWORK_ERROR(http.ts:388-391), which is the turn-end stall case. ExcludingAPI_UNREACHABLEis the right call and the 400 test pins it. - [closed] all three nits:
--resourcerefused wheneverallOpenis set, with a test covering the one-loop case that used to slip; the ordering comment rewritten to describe the order that now runs; the changeset bumped tominor.
Still open
- [agent-usability] the machine-wide scope is stated in three surfaces, absent from the two an agent reads, and guarded by none (round 1, Major 2): keeping the sweep machine-wide is yours to choose and I am not re-arguing it. What is missing is the half that makes it a decision rather than an accident. "In any session" is stated in
--help, in the MCPallOpendescription, and incommand-reference.md. It is absent fromskills/tenjin-search/SKILL.md:119("closes every unanswered loop the WebSearch hook recorded"), and the Stop hook line moved the wrong way:close every open hook loop in one callbecameclose **them** in one call(hook-scripts.ts:806), where "them" is the at-most-three MISSes the same line introduces as "this session". That line is the only place an agent reads this at turn end. The docstring at the sweep also lost the qualifier it had at5adcabb: outcome.ts:297 now reads "Every open loop the WebSearch hook recorded and Tenjin could not answer", where it used to end "in any session". And no test seeds asessionIdanywhere inoutcome.test.ts, so a future filter narrowing this to the current session breaks nothing. Fix: put "in any session, on this machine" back at outcome.ts:297 and in the skill, and add one test seeding two entries under differentsessionIds that asserts both are swept.ownedByThisSession(hook-scripts.ts:811-822) is the invariant on the other side, so the test is what keeps the two from silently converging.
New
- [hygiene] the cross-repo contract the rename is justified by was closed unmerged, and the code still asserts it: tenjin#703 was closed on 2026-08-17 by owner decision, "hiding the entire websearch-hook origin is broader than wanted", with the replacement moving to ingest classification against tenjin#704 and tenjin-agent#179. The PR body still describes it as an open draft that "filters that string out of the /trending questions tier", and client-meta.ts:14-20 still carries it as a load-bearing invariant: "CROSS-REPO CONTRACT, tenjin's /trending questions tier drops this exact name, so renaming it here silently puts unvetted web-search queries back in front of readers". No such filter exists or is planned in that shape, so that comment now promises a safety property the marketplace does not have, and the literal pinned in
client-meta.test.tsand the wire-identity test is pinned against a counterpart nobody will write. The rename itself still earns its place, since separating ridealong from deliberate traffic inclient_nameis what any classifier needs, ingest-time ones included, so this is a claims fix and not a rollback: restate the comment as attribution separation consumed by whatever classifies hook demand, cite tenjin#704 instead of #703, and correct the body. Round 1 verified those claims against tenjin and they were true then; they stopped being true the same day.
Not re-raised
The HALTING_FAILURES comment attributes API_UNREACHABLE to apiFailure, where a 400 or 500 actually gets it from the status !== 202 branch below it, since apiFailure only fires on transport. The conclusion the comment draws is right and the behavior is right, so this is a one-word correction, not a finding.
Decisions for the owner: whether to ship the rename with tenjin#703 dead. It is defensible on its own and the ingest work needs the same separation, but the operator's KNOWN_CLIENT_PRODUCTS item matters more now rather than less: with no filter consuming the name, the permanent client column still steps down to other the day this ships, and nothing in this delta or a linked issue covers that yet.
Verdict: comments-only. Both Majors from round 1 are addressed and the three Minors and three nits are closed, verified against the head rather than the diff: SEARCH_ID_RE is literally UUID_RE, fix is the shape the human renderer prints, and both halting codes are reachable on this route. One round-1 item stays open in narrowed form (state the chosen scope where an agent reads it, and guard it with a test), and one new item is about claims rather than code.
Owner decision on the review's Major 2. The hit/miss loop is per session by design: a session's open loops are its own, one that ends leaves its unpublished debt to decay, and no session closes another's. There is no machine-wide sweep, behind a flag or otherwise. The session resolver moves to lib/session.ts and is shared rather than copied, so the Stop hook's reminder, the stamp `search` writes, and the sweep agree on what "this session" means: TENJIN_SESSION_ID, then CLAUDE_CODE_SESSION_ID, with an unstamped entry in scope everywhere rather than nowhere, exactly as ownedByThisSession treats it. Copy follows the behavior: the skill, the command reference, the flag help, the MCP tool description and the Stop hook's batch line all say this session's. 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.
|
Round closed. Everything below is on the branch; head is 2b28a56, CI and skill-drift green. Major 1 — Major 2 — machine-global sweep vs session-scoped nag. Owner decision, implemented in 2b28a56: scoped to the current session, with no machine-wide flag. The rationale is that the whole hit/miss loop is per session — a session's open loops are its own, a session that ends leaves its unpublished debt to decay, and no session closes another's. The resolver moved to Minor 1 — id shape checked mid-send. Fixed in bb7b6ae; every id is validated before the first request. Deviation: Minor 2 — partial batch named no ids. Fixed; the failed ids ride the Minor 3 — unbounded sweep against a 60/min budget. Fixed; the batch stops at the first failure and reports the remainder Nits. All three: One commit is not feature scope. 6aeed4b regenerates |
A1igator
left a comment
There was a problem hiding this comment.
Review round 3: the scope reopen is closed, both deviations hold up
Delta only, 6aeed4b3..2b28a56. Verified at 2b28a56. Final round from me on the code.
Resolved since round 2
-
[closed] the sweep's scope is now decided, stated, and guarded:
resolveAllOpenfilters onownedByThisSession(s.sessionId, readSessionId(deps.env))(outcome.ts:336-341), and the resolver plus the ownership rule live in one module (session.ts:14, :30) thatsearchnow imports rather than copies. The intent is stated at the sweep itself ("THIS SESSION ONLY, and there is no flag for the wider sweep", outcome.ts:306) and in all five agent-facing surfaces, including the Stop line, which now says "close this session's in one call" against a batch it introduces as this session's. Three tests cover the shape that matters: a sibling's stamped entry is never swept while an unstamped one is, theCLAUDE_CODE_SESSION_IDfallback resolves, and a harness that names no session sweeps everything. The unstamped-entry direction matches the Stop hook's, so no loop can become unreachable in every session at once, and precedence between the two env vars stays covered at the resolver's original call site insearch.test.ts. -
[closed] deviation 1,
UUID_REoverSEARCH_ID_WIRE_REfor the preflight: correct, and for a second reason worth recording. The fixture agrees with the argument,/api/searches/{id}/outcomesdeclares its path parameter{"type":"string","format":"uuid"}with nopattern, whileSEARCH_ID_WIRE_REpins RFC version and variant nibbles for thePOST /api/postsbody (posts-api.ts:65-73); ran both against the placeholder id and it passesUUID_REand fails the wire regex, exactly as stated. The second reason:postOutcomesalready enforcedSEARCH_ID_RE, which is literallyUUID_RE(agent-api.ts:448), so hoisting that same regex moves a check earlier without changing which ids the command accepts. Reaching for the stricter one here would have been a behavior change smuggled inside a fix. -
[closed] deviation 2, the halt set: sound and pinned from both sides. A 400 about one id must not abandon the batch, and a rate limit or a dead network must, which is what the two tests assert. Only the attribution in the comment is off, and it does not change the conclusion:
httpRequestreturnsok: truefor any status, soapiFailurefires on transport only, and a 400'sAPI_UNREACHABLEcomes from thestatus !== 202branch below it rather than fromapiFailure. Excluding the code is right either way.
New
- [agent-usability] the MCP description promises session scoping the MCP process may not be able to resolve:
deps.envdefaults toprocess.envand the tool description now reads "Close this session's open WebSearch-hook MISSes" (server.ts:135). The docstring is careful thatCLAUDE_CODE_SESSION_IDis "what Claude Code exports to Bash subprocesses", and an MCP server is a different process class; I did not verify whether the harness exports it there, so this is a question rather than a defect. If it does not,allOpenover MCP resolves no session and falls back to the global sweep, which is the safe direction by design but is not what the description says. Cheap fix either way: have the description name the fallback, "this session's, or every open hook MISS when the harness names no session", so the tool text is true on both paths. Worth a one-line check against a live MCP session before release.
Nits (2), none blocking
- [hygiene]
lib/session.tshas no colocated test: the repo convention isfoo.test.tsbesidefoo.ts, and the behavior is covered at both call sites (search.test.tsfor precedence,outcome.test.tsfor the three scope cases), so this is coverage-by-caller for a module that now has two callers plus a mirrored copy in the hook. - [hygiene] "the sweep covers exactly the set the nag names" (outcome.ts:311-313): within the session the sweep is a superset, since the nag additionally bounds by the open-loop recency window, by three entries per batch, and by once per session. The superset is the right direction and nothing misbehaves; "the same set the nag is drawn from" would be the accurate phrasing.
Verified, not issues
- The two
ownedByThisSessionimplementations differ on an empty-string stamp, and it is unreachable: the hook'ssessionIdOfrejects a zero-lengthsession_idandreadSessionIdtrims toundefined, so no writer can produce one. - Counting is computed from the session-scoped set, so
deliberateLeftOpenandansweredLeftOpendescribe this session rather than the machine, and the empty-sweep line says "in this session". - The sweep still has no age filter, which is deliberate here: your own stale open loop is yours to close even after the nag stopped raising it.
Closed as decisions
Two round-2 items are unchanged in this delta and are with the operator rather than with you: the tenjin#703 claims still standing in client-meta.ts and the PR body, and the KNOWN_CLIENT_PRODUCTS follow-up. Not re-argued here.
Verdict: comments-only, and every code finding from rounds 1 through 3 is closed. The scope re-cut is the version I would have asked for: one resolver, one ownership rule, shared with the hook, stated where it is enforced, and tested in the three directions that could drift. What remains is one description-accuracy question on the MCP path and two items that were never yours to close.
Scope
Partial progress on #167, which asks for multiple
--search-idvalues on bothpublishandoutcome. This PR does theoutcomehalf only; thepublishmulti-id form (extra ids closing as published-back) and server-side array support on the endpoints stay out of scope.Three loop-noise fixes: a dogfooding session closed seventeen open hook loops one
tenjin outcomecall at a time, and every one of those hook queries reached the marketplace labelledtenjin-cli, so unvetted web-search text was indistinguishable from a question an agent deliberately looked up.src/lib/hook-scripts.tsnow leads the hook'sUser-Agentwithtenjin-websearch-hook/<version>instead of the CLI product; deliberatetenjin searchstill identifies astenjin-cli. Hook script version 17 to 18, sotenjin installrewrites the scripts.tenjin outcomecloses a batch.--search-idrepeats (one status for all ids), and--all-opencloses every open websearch-hook loop insearches.json, in any session. Per-id results in the JSON envelope; a partial failure names what closed and what did not (src/commands/outcome.ts).tenjin outcome --all-open --status regeneratedonce for the whole batch. The strong (deliberate-search) arm and the publish path stay per id.The client-name contract
The name is
tenjin-websearch-hook, exactly. It travels as the firstUser-Agentproduct, which is the only mechanism that reacheslookups.client_name: tenjin'sresolveClientProduct()reads the first product token and falls back toX-Tenjin-Clientonly when the User-Agent is absent, invalid, or a genericnode/undiciruntime, so a header alongside a real User-Agent would be ignored.lib/search/telemetry.tswrites that value to bothlookups.client_nameandsearch_queries.client_name.The sibling tenjin branch
vraspar/questions-hook-origin-filter(tenjin#703, draft) filters that string out of the /trending questions tier, matching case-insensitively. It is inert until this ships.Residual risk, stated rather than solved: the string lives in two independent copies with nothing mechanical tying them across repos. This side pins the literal deliberately, in
client-meta.test.tsand in the wire-identity test that reads the header off a real socket; tenjin holds its ownWEBSEARCH_HOOK_CLIENT_NAME. Each repo's tests only cover its own half, so a rename on either side fails silently — nothing errors, the questions tier just quietly leaks again. Both constants carry a comment naming the contract.tenjin-websearch-hookis deliberately not added toKNOWN_CLIENT_PRODUCTS, socanonicalClientProduct()folds it tootherin tenjin's permanent client columns. That list extends from names operators observe in the swept space, which cannot happen until this ships; it is a one-line follow-up there, not part of either PR. The swept rawclient_namethe questions filter reads is unaffected.Why
--all-openis regenerated-onlyThe other four statuses claim what a specific search did for the agent. A blanket
usedover queries nobody read piece by piece is attribution the marketplace would be right to trust and wrong to believe, so any other status is a USAGE refusal before anything is sent. "Nothing here answered it, I wrote it myself" is the one report that is honest about an unexamined ridealong query.--all-opennever touches a deliberatetenjin search: those are left open, counted, and named in the output ("N deliberate search(es) left open"), so a blanket close cannot look more complete than it is. It is also mutually exclusive with--search-idand--last.The skills mirror commit is environmental, not feature scope
One commit here regenerates
skills/tenjin/SKILL.md, the vendored byte-for-byte mirror oftenjin.blog/skills.md. The canonical page was rewritten upstream when the embeddings search change reached it (agent search now matches a piece's own body, title and excerpt rather than the answer card), so the committed mirror went stale on its own. It ispnpm sync:skilloutput with no manual edits, and it reproduces identically on a cleanorigin/maincheckout, which is how I confirmed this PR did not cause it. It rides here because the drift check runs on any PR touchingskills/**, and the workflow's own remedy for a red PR check is to run the sync locally and commit. Read it as housekeeping; the feature diff is everything else.Testing
pnpm typecheck,pnpm lint,pnpm format:check,pnpm build: green.pnpm test: 2162 passed, 10 skipped, 2 failed. The two failures aredoctor.test.ts > names the rule on auto/on full-auto, which fail identically on a cleanorigin/mainworktree on this machine (they read the developer's own~/.claude/skillsstate); nothing else fails.composeUserAgent), the batch and--all-openpaths including every refusal, the partial-failure report, and the flag wiring at the dispatcher.pack-smokedeliberately not run locally: it rewrites the real~/.claude/skillson this machine. CI covers it.🤖 Generated with Claude Code