agentleFS
Sign inSign up

thlibo

3rg0n/thlibo/CLAUDE.md

Guidance for AI assistants (Claude Code, Codex, etc.) working in this repo. Humans: the README is your starting point; this file is for agents that need architectural context in a single shot. v0.11.6 (current). Single binary shipped (thlibo); inference runs in a separate sidecar, inferd (its own repo, github.com/3rg0n/inferd), which thlibo install probe-or-installs. thlibo install is zero-touch on all three OSes (incl. Windows arm64): it copies inferd's backends/ libs beside the daemon, pins the latest stable (non--rc) inferd, runs the…

CLAUDE.md9 starsChanged 57 days ago
# CLAUDE.md

Guidance for AI assistants (Claude Code, Codex, etc.) working in this
repo. Humans: the README is your starting point; this file is for
agents that need architectural context in a single shot.

## Status

v0.11.6 (current). Single binary shipped (`thlibo`); inference runs in
a separate sidecar, **`inferd`** (its own repo, github.com/3rg0n/inferd),
which `thlibo install` probe-or-installs. `thlibo install` is zero-touch
on all three OSes (incl. Windows arm64): it copies inferd's `backends/`
libs beside the daemon, pins the latest *stable* (non-`-rc`) inferd, runs
the per-user installer (LaunchAgent / systemd-user / Startup-shortcut),
and probes the daemon for readiness before reporting success
(fresh-install fixes, #47). On Linux it warns when there is no systemd
user session, because that is the one case it cannot fix and must not
hide: `install.Install` swallows `systemctl --user` errors by design, so
the unit lands on disk, nothing starts it, and fail-open then makes every
hook a silent permanent passthrough (#117). `thlibo upgrade` rename-then-replaces its own
binary so it works while running (#52). Four AI clients: Claude Code
hooks (Bash + PowerShell + Read + Write/Edit); Codex PostToolUse hook
(canonical `[features] hooks` flag + `/hooks` trust reminder, #57);
Cursor IDE `preToolUse` hooks (Shell command rewrite + Read file_path
rewrite; bash-wrapped on Windows, invalid-JSON-escape tolerant, #59/#60/
#62); GitHub Copilot CLI hooks (`preToolUse` `modifiedArgs` command
rewrite, fail-closed-safe + `postToolUse` `modifiedResult` output
compression, fail-open; own `~/.copilot/hooks/thlibo.json`) — the same
hook file is auto-discovered by **VS Code Copilot** (1.111+, Insiders),
whose Claude-Code wire format the scripts detect and match. Full test +
scanner CI on linux/macOS/Windows, signed releases via Sigstore keyless,
CycloneDX SBOM.

A whole JSON / YAML / TOML document now passes through untouched
(v0.11.6, #129): `compress` is the router's general fallback and its
summary shape replaced the config with a description of itself. See
invariant #4 for where that gate sits and why it must stay after
`MatchFastPath`.

**Python is no longer needed on the compression path** (v0.11.3). ADR
0015 moved PDF text extraction to native Go and ADR 0016 ported
`cordon-filter`, closing both of ADR 0010's carve-outs. The single
remaining Python processor is `pdf-to-md`, reached only for ADR 0009's
scanned-page rasterization — so the prereq now applies to scanned PDFs
alone, not to ordinary tool output.

> History: through v0.5.x thlibo shipped a second binary, `thlibod`,
> that spawned llamafile directly. ADR 0005 extracted all inference
> into `inferd`; ADR 0006 made thlibo fail open during the inferd
> bootstrap window. If you see `thlibod`, llamafile, or `thlibo pull`
> referenced as live, that's stale — they're gone.

Authoritative sources when they disagree:

1. `THREAT_MODEL.md` — security posture + threat decisions.
2. `docs/adr/*.md` — cross-cutting architectural choices.
3. This file — drift happens; fix it when you see it.

## What this project is

A single binary plus PreToolUse hooks that compresses
AI-coding-assistant tool output, backed by a locally-hosted Gemma 4
E4B model served by a sidecar:

- **`thlibo`** — CLI + middleware (the whole repo). Subcommands:
  `rewrite`, `exec`, `compress`, `case`, `shorthand`, `install`,
  `uninstall`, `upgrade`, `config`, `version` (see `cmd/thlibo/main.go`
  for the authoritative switch). Scans `~/.thlibo/processors/`, routes
  tool output to the right processor (script or prompt), and — for
  prompt processors — posts fully-formed requests to inferd. Knows
  only about routing; never about model loading or inference
  mechanics.
- **`inferd`** — inference sidecar, separate project. Loads the model
  once, stays warm, serves the length-prefixed v2 wire over a per-user
  socket. thlibo talks to it through `internal/inferd` — thlibo's own
  codec implemented against inferd's `protocol-v2.md` (no dependency on
  inferd's reference client). If inferd is unreachable, the middleware
  fails open (passthrough) per ADR 0006.

## Architectural invariants (load-bearing — do not blur)

1. **Middleware has zero knowledge of model loading or inference
   mechanics.** It speaks only inferd's wire protocol via
   `internal/inferd` (thlibo's owned codec). (Inference invariants —
   single warm model, fixed concurrency, offline-only generation — now
   live in the `inferd` repo, not here.)
2. **Fallback to original output on any error path.** The middleware
   must never break the AI client. Script non-zero exit, inferd
   unreachable, parse failure, timeout → pass through the original
   bytes. Every hook script exits 0 on error. (ADR 0006 — fail open.)
   **A processor's stdout IS the tool output, so an error message
   written there doesn't report the failure — it replaces the
   document.** A processor that cannot do its job exits **non-zero**
   with diagnostics on **stderr**; that is the only way to reach the
   fallback, since exit 0 with a short body looks like success to the
   dispatcher and the original bytes are then lost. Exit 0 with partial
   output is legitimate only when that output is genuinely more useful
   than the input (see `pdf-to-md`'s pdfplumber path, which keeps the
   metadata/outline markdown it already extracted).
   **Fail open means fail *fast*:** every inference round-trip is
   bounded in `inferd.Client.Post` (15s, `$THLIBO_INFERD_TIMEOUT`), never
   at the call sites, because an unbounded wait on a wedged daemon is a
   hang, not a fallback (ADR 0012).
3. **Short-circuit before doing any work.** Input under
   `middleware.MinBytesForRouting` (2000 bytes) passes through without
   scanning processors or calling inferd.
4. **Fast-path before routing.** Each processor's `match` regex is
   checked before inferd is asked to route — a regex hit dispatches
   immediately, no routing call. Because `match` is then the *only*
   evidence used, precedence among hits is load-bearing (ADR 0014):
   rooted format signatures (`^%PDF-`) beat line shapes found anywhere,
   script/native beats prompt within a tier, tiebreak is stable
   alphabetical, and line-shape filters are refused entirely on input
   that sniffs binary. Rootedness is derived from the pattern in
   `validate()`, never declared — don't add a priority field.

   Two local gates sit between the fast path and the routing call, and
   both exist because reaching the router is not free — the bytes leave
   the process for a decision that can be made here. `BinaryLooking`
   refuses containers (#97). `StructuredDocument` refuses a whole
   JSON/YAML/TOML document (#129): `compress` is the router's declared
   general fallback and its mandatory output is a group-by-signature
   summary, so a config file routed there came back as a description of
   itself with the original bytes discarded — and an agent that reads a
   config that way then edits it corrupts the file. **Order is
   load-bearing: both run *after* `MatchFastPath`,** because `har-filter`
   and `ndjson-filter` legitimately take JSON and a whole-document parse
   must not shadow a filter whose `match` already fired.

   The risk in `StructuredDocument` runs one way. A false negative
   changes nothing — the input reaches the router as before. A false
   positive makes thlibo a silent no-op for that input, the #106 failure
   class, so each detector demands positive evidence of config shape.
   That is why YAML needs *nesting* (a run of `LEVEL: message` log lines
   parses as a mapping) and why TOML rejects bare-word values (`compress`'s
   own `sig=`/`level=` output is otherwise `key=value`-shaped, which the
   first draft of the guard misclassified). `structured_test.go` asserts
   no false positive across every fixture in `internal/processors/testdata/`.
5. **Thinking mode is owned by the processor prompt, not inferd.**
   Gemma 4's `<|channel>thought` block is stripped by the
   `internal/processors` thinking filter (`thinking.go`) before output
   reaches the AI client.
6. **All hook scripts are SHA-stamped and survive reinstall.** User
   edits are preserved; new versions land at `<path>.new` on
   conflict.

## Processors

Live in `~/.thlibo/processors/<name>/`. Two kinds:

- `processor.yaml` → **script processor**. `entry` is a plain
  filename (`.py` → python3, `.sh` → bash, `.exe`/`.bin` → direct).
  stdin in, stdout out, non-zero exit = fallback. Entry is
  fingerprinted (size/mtime/mode) at load and re-verified at
  dispatch — TOCTOU guard.
- `processor.md` → **prompt processor**. YAML frontmatter is config
  (`temperature`, `max_tokens`, `match`, `thinking`, etc.); the
  markdown body is the system prompt, sent to inferd verbatim.
- **Routing fields (ADR 0013).** `route_hint` is the short "when should
  I be picked?" line sent to the routing model — `description` is for
  humans and runs long, and shipping it cost ~2,200 tokens of prompt per
  routing call. `routable: false` keeps a processor out of the model's
  candidate set entirely (set on `shorthand`, which rewrites prose in
  place, and `cordon-filter`, which is hardwired); it stays reachable by
  fast-path match, explicit chain, and hardwired dispatch. A processor
  with a `match` regex is excluded by default, since `MatchFastPath`
  answers before the router is consulted.
- Both present → yaml wins for type, md body is the system prompt.
- Neither → folder ignored.

Built-ins are embedded via `go:embed` under `processors/` (see
`processors/embed.go`): `compress`, `casefolder`, `shorthand` (prompt
processors) plus the deterministic native-Go filters `git-filter`,
`npm-filter`, `cargo-filter`, `pytest-filter`, `ndjson-filter`,
`stacktrace-filter`, `lint-filter`, `trivy-filter`, `go-test-filter`,
`har-filter`, `mhtml-filter` (ADR 0010), `pdf-filter` (ADR 0015),
`cordon-filter` (ADR 0016) and the one remaining Python script filter,
`pdf-to-md`. A user processor of the same name overrides a built-in; the
registry emits a `ShadowWarning` at load time so it's visible.

**`cordon-filter` is the one native filter that reaches the network.** It
scores log windows by k-NN distance over inferd embeddings, so it is
registered via `RegisterNativeCtx` (`NativeCtxFilter`, taking a context)
rather than `RegisterNative`. Every other filter must stay ctx-free — that
signature is what states at the type level that a filter does not do I/O.
Its round-trip goes through `inferd.EmbedClient`, a *second* protocol in
`internal/inferd`: line-delimited JSON on its own socket, sharing only the
dialers with the length-prefixed generation wire. Two bounds apply, and
both matter: `EmbedClient` caps each batch, and `CORDON_TIMEOUT` (30s)
caps the whole filter — `RunNativeCtx` cannot interrupt a running filter,
so without the outer bound a wedged daemon would hang the hook rather
than fall back (ADR 0012). Parity with the retired Python lives in
`cordon_parity_test.go`, captured from live `run.py` and **not
regenerable** — a change there is a behaviour change, not a test fix.

**Two processors claim `^%PDF-`, on purpose.** `pdf-filter` is the primary
path — native Go over the vendored parser in `internal/pdf/`, four tiers
(`/StructTreeRoot` → native text → geometry tables → scanned placeholder),
~99× faster than the Python path with less text mangling. `pdf-to-md` is
retained because ADR 0009's OCR flow needs page *rasterization*, which is
not text extraction and which no pure-Go library in our license posture
provides. ADR 0014's tier-1 tiebreak (native beats script within rooted
signatures) decides the fast path; don't "fix" the collision by deleting
one. See ADR 0015 and `internal/pdf/VENDOR.md`.

`internal/pdf/` is **vendored, patched, and fuzzed** — it parses untrusted
input, so treat it accordingly. Every local change carries a `thlibo:`
comment for re-sync. Fuzzing found three *hangs* (lexer non-advance on a
stray delimiter, a trusted xref entry count, a self-referential page tree);
under invariant #2 a hang is strictly worse than a panic, since `RunNative`
recovers a panic and passes the original bytes through but nothing recovers a
blocked hook. If you add a walk over file-controlled structure here, bound
it and add a fuzz seed.

**Unbounded recursion is worse still, and no scanner or fuzzer will tell
you.** A Go stack overflow is a `runtime.throw`, not a panic: the runtime
grows the goroutine stack to 1 GB and then kills the *process*, so
`RunNativeCtx`'s `defer recover()` — thlibo's fail-open net — cannot catch
it, and the hook dies with tool output unwritten. That was THREAT_MODEL #29
(unbounded `parseArray`/`parseDict`, reached from a ~2.6 MB file against the
64 MiB the middleware accepts), now bounded by `maxObjectDepth`. Two
non-obvious consequences: mutual recursion needs the guard on **both**
functions (`parseArray` ↔ `parseDict` via `parseFromToken`), and the bound
needs a **direct unit test** — see the fuzz note below for why the fuzzer
won't cover it.

The three targets run **nightly**, not per-PR (`.github/workflows/fuzz.yml`,
one matrix leg each, 10 min apiece, corpus cached so coverage compounds).
`go test` replays the committed seeds in `testdata/fuzz/` on every run, which
guards the known regressions — but finding a *new* hang is the nightly's job,
so a parser change is not "fuzz-clean" because CI went green. Soak it first:
`gh workflow run fuzz.yml -f fuzzminutes=30` (bare minutes — the job's own
`timeout-minutes` is computed from it, so the two can't drift out of sync and
cancel the run), or locally
`go test ./internal/pdf/ -run '^$' -fuzz '^FuzzOpenBytes$' -fuzztime 60s`.
A nightly failure uploads the minimised input as a `crashers-<Target>`
artifact; commit it under `testdata/fuzz/<Target>/` and it becomes a
permanent regression test that needs no fuzzing to reproduce.

**A green fuzz run does not mean "no stack overflow."** Measured against a
target rigged to throw on demand: `go test -fuzz` printed `PASS` and exited
**0** while its workers were being killed by stack overflow — the only
symptom was the exec rate dropping from ~550k/sec to ~35/sec, which nothing
asserts on. The worker dies before it can report, and the parent treats that
as a lost worker rather than a finding. So a regression of `maxObjectDepth`
would sail through both the nightly and a 30-minute soak. Recursion bounds
are covered by `TestObjectNestingIsBounded` in the ordinary suite, and that
test asserts on `Parser` directly rather than through the `mustTerminate`
helper — the helper runs its body in a goroutine, where neither a throw nor
a panic can fail the case.

## Talking to inferd

thlibo is a *client* of inferd; it does not own the model or the
inference mechanics. But it **does** own its wire codec — implemented
directly against inferd's `protocol-v2.md` (length-prefixed `0x01`/
`0x02` framing, in-band `wire_version`, the unified generation socket),
not via inferd's reference Go client. That's a deliberate decoupling
(ADR-level: thlibo's release no longer waits on inferd's client
cadence). The surface is `internal/inferd`:

- `protocol.go` — wire types (`Request`/`Message`/`Result`/
  `ResponseFormat`/`Tool`) + the length-prefixed frame reader/writer.
- `client.go` — `Post(ctx, Request) (Result, error)`: dial, stream,
  collapse to text + tool calls; fail-open on connect/parse failure.
- `addr.go` — socket resolution per `protocol-v2.md` §1.1
  (`\\.\pipe\inferd` / `inferd.sock`, XDG→$HOME→/tmp).
- `dial_unix.go` / `dial_windows.go` — UDS / named-pipe dialers
  (no TCP — inferd binds no network listener, ADR 0022).

If you need to change how thlibo *reaches* or *frames* inference,
that's here; if you need to change inference behaviour (model,
sampling, concurrency, queueing), that's the inferd repo. If inferd
bumps `wire_version`, the daemon fails the request loudly and this is
where you update.

The middleware sends prompt-processor work to inferd as a
fully-formed request and gets compressed text back. The router uses
`response_format` (JSON-Schema) to constrain routing output. On any
failure to reach or parse, it fails open (ADR 0006).

`Registry.RoutableNames()` is the single source of the router's candidate
set, and three places must agree on it: the prompt's processor list, the
schema `enum`, and `parseRouteResult`'s validation. The third is
security-relevant, not just tidiness — a backend that ignores
`response_format` can emit any name, so validating against the full
registry would let the model select `shorthand` for tool output (ADR
0013). If you add a candidate-filtering rule, change it in
`RouterEligible` and all three follow.

## Adapters

- **`internal/adapters/claudecode/`** — PreToolUse hooks for Bash,
  PowerShell, Read, and Write/Edit tools. Settings merger. /caselog
  skill. **Every matcher gets one hook script chosen by host, never by
  matcher name** — `.ps1` on Windows, `.sh` elsewhere. The Bash and
  PowerShell tools both carry the command in `tool_input.command` and the
  hook reads only that field, so the script's language is independent of
  the tool's shell. Registering the `.sh` under the Bash matcher on
  Windows was #127: a bare script path in `command` is resolved through
  the `.sh` file association, and on a Git-for-Windows box that is
  `git-bash.exe` — a *GUI terminal launcher*, not an interpreter. The
  hook opened a window and never fed the tool; fail-open hid it, so that
  hook had never worked on Windows. The mirror case is real too — off
  Windows the PowerShell binary is `pwsh` and `-ExecutionPolicy` is
  Windows-only, so a `.ps1` must never be registered there.
  `addPreToolUseHook` therefore takes **every** marker in a hook's family
  (both the `.sh` and `.ps1` names), not just the one being written:
  markers identify a hook by *file*, so matching only the new name would
  leave the stale entry firing beside it. `runtimeIsWindows` is a `var`
  so both host paths are tested on every CI leg.
- **`internal/adapters/codex/`** — PostToolUse hook using
  `decision: block` + `reason` to substitute the tool result. Codex accepts
  hooks in **two** representations and warns when one config layer holds
  both, so `InstallHook` detects which the layer already uses and matches
  it: inline `[[hooks.PostToolUse]]` in `config.toml` by default (git-ai
  and taco write inline), or `hooks.json` when that is where the layer's
  other hooks live. Writing inline unconditionally is what caused #170 in
  mirror — and detection must **exclude `[hooks.state]`**, since Codex
  records per-hook trust there keyed by the *defining file*, so a
  hooks.json-only layer grows a `[hooks.state.'…/hooks.json:…']` table in
  `config.toml` as soon as the user trusts one. Count that as an inline
  hook and the detection reports "inline" for exactly the layer it exists
  to find. `[features] hooks = true` goes in `config.toml` either way.
  **The hook script is chosen by host** — `.ps1` on Windows, `.sh`
  elsewhere (`HookFileName`, #126) — for #127's reason: a bare `.sh` path
  in a `command` resolves through the `.sh` file association, which on a
  Git-for-Windows box is `git-bash.exe`, a GUI terminal launcher. The
  `.ps1` is registered wrapped in `powershell -NoProfile -ExecutionPolicy
  Bypass -File`, so `isThliboCommand` must match the *script name* inside
  the command, and it checks **both** markers: an upgrade that matched only
  the new name would append the `.ps1` beside a still-firing `.sh` (#128).
  So `MergeConfigTOMLHook` **rewrites** a stale `command =` line in place
  instead of appending, and it scopes the match to `command =` assignments
  — a whole-file substring match would read the inert
  `[hooks.state.'…thlibo-rewrite-codex.sh:…']` trust record as an installed
  hook and make install a silent no-op. **Delivery on Windows is broken
  upstream** (openai/codex#38850): Codex never fires a trusted PostToolUse
  for its shell results there, so the installer warns rather than let a
  clean install read as "compression active". Nothing here can fix it.
- **`internal/adapters/cursor/`** — `preToolUse` hooks (Shell +
  Read) using `updated_input` to rewrite the command / `file_path`.
  Non-destructive `~/.cursor/hooks.json` merge; bash-wraps the command
  on Windows (no `.sh` file association); tolerates invalid JSON
  escapes in the envelope. Cannot substitute MCP output (Cursor limit).
- **`internal/adapters/copilot/`** — GitHub Copilot CLI hooks in
  `~/.copilot/hooks/thlibo.json` (Copilot reads every `*.json`; each
  tool owns its file, so no merge — thlibo writes/deletes its own).
  Two events, both fail-safe: `preToolUse` rewrites the shell command
  via `modifiedArgs` (fail-**closed** host → the hook only ever
  `"allow"`s, never denies), and `postToolUse` replaces verbose tool
  output via `modifiedResult` (fail-open) by piping through
  `thlibo compress`. A double-compression guard skips output whose
  command was already `exec --`-wrapped by preToolUse. Ships native
  `.sh` + `.ps1` per event (config carries both `bash`/`powershell`),
  so Windows runs the PowerShell variant directly — no bash-wrapping.
  **Dual-host:** VS Code Copilot (1.111+, Insiders) also reads
  `~/.copilot/hooks/`, so the same file works there — but VS Code uses
  the Claude-Code envelope (`tool_input` / `hookSpecificOutput`.
  `updatedInput`; observe-only postToolUse). The hook scripts detect the
  envelope (CLI `toolArgs` vs `tool_input`) and reply in kind; on VS
  Code, compression rides the preToolUse wrap since its postToolUse
  can't replace output. `--copilot` covers both; no `--vscode` flag.

**Every `.ps1` hook must set UTF-8 on all three encodings, and this is a
correctness requirement, not tidiness (#134).** PowerShell 5.1 — the
`powershell` the hooks are registered under — defaults each one to a
non-UTF-8 code page while the envelope is UTF-8: `[Console]::InputEncoding`
decodes stdin (the OEM page, IBM437 on a default box),
`[Console]::OutputEncoding` decodes a **child process's stdout**, and
`$OutputEncoding` encodes what the hook **pipes to** a child (ASCII). Missing
any of the three corrupts a hook that carries non-ASCII, and three of the six
hooks rewrite tool *input* — so the corrupted command, path, or file content
is the one that runs or lands on disk. Measured before the fix:
`git log --grep=café` reached the Bash tool as `git log --grep=caf└⌐`, a Read
path with an accent failed `Test-Path` and the hook became a silent no-op, and
`Naïve résumé façade` was written to disk as `naive r??sum?? fa??ade`.
Fail-open cannot help — a corrupted-but-valid command is indistinguishable
from a correct one. So read stdin through an explicit UTF-8 `StreamReader`
over `[Console]::OpenStandardInput()`, never `[Console]::In`, and set both
encoding variables. Keep the `try`/`catch` around the
`[Console]::OutputEncoding` assignment: it calls `SetConsoleOutputCP`, which
throws with no console attached. `encoding_test.go` in each adapter asserts
the lines are present in the embedded bytes, because a dropped line is silent
on any test input that happens to be ASCII.

## Build, test, scan

```
go build ./...                 # build all
go build -ldflags "-X github.com/3rg0n/thlibo/internal/version.Tag=v0.X.Y" -o thlibo ./cmd/thlibo
go test ./...                  # full suite
go test ./internal/middleware/... -run TokenSavings   # the savings benchmark
go test ./internal/processors/ -run '^$' -bench . -benchmem   # filter perf
go vet ./...                   # required before commit
staticcheck ./...              # required — blocks CI
gosec ./...                    # required — blocks CI
```

The version tag is injected via `-ldflags -X …/internal/version.Tag`;
an un-injected build reports `dev` and skips the background
update-check.

**Two filters carry a performance ceiling as a test, and one of them is a
safety property.** `internal/processors/bench_test.go` benchmarks
`pdf-filter` and cordon's k-NN pass, plus `-run StaysUnderBudget` ceiling
tests that run with the ordinary suite. For cordon that ceiling is
**availability, not speed**: the scoring pass is O(n²) in the window count,
and if it degrades far enough to stop finishing inside `CORDON_TIMEOUT`, the
deadline fires and the filter returns the input verbatim — which is the
*correct* documented behaviour (invariant #2), so every other test still
passes while cordon has silently become a permanent no-op. That is the #106
failure mode encoded as a test. Both budgets are sized to catch a change in
*complexity*, not constant factor; a flaky perf test gets deleted rather
than investigated, so use `benchstat` across revisions for anything
narrower. The published "~99×" is a one-off hand measurement against Python
(ADR 0015) — these are Go-only and cannot re-derive it; they only catch this
path regressing.

## When adding code

- Repo layout: `cmd/thlibo` (the only binary), `internal/*`
  (adapters, casefile, config, execpolicy, inferd, install, logx,
  middleware, processors, promptsan, router, shellcmd, shorthand,
  telemetry, update, version), `processors/` for embedded built-ins,
  `skills/` for Claude Code skill definitions.
- **Optional OTel emission (ADR 0011):** `internal/telemetry` emits
  content-free metrics + events, off by default
  (`THLIBO_ENABLE_TELEMETRY`), configured via standard `OTEL_*` env,
  drained to an operator collector. The middleware `decide()` returns a
  `telemetry.Invocation` alongside its output; emitting subcommands
  (exec/compress/case) `defer Pipeline.Shutdown` to force-flush within a
  2s bound. Fails open (never blocks the client); never emits tool
  output/prompts/commands/paths; user processor names redact to
  `"custom"`.
- New user-facing features: add a scanner annotation if one fires
  (gosec / semgrep / staticcheck all block CI). Keep `#nosec` and
  `nosemgrep` reasons short but honest.
- New subcommands: wire into `cmd/thlibo/main.go` switch, update the
  usage string, and exclude from the update-check short-circuit
  only if the subcommand should NOT trigger a background update
  fetch (like `version`).
- `.plan/thlibo-spec.md` is the original v0.1/v0.2 design doc — useful
  history, but the ADRs (`docs/adr/`) outrank it for anything the
  inferd extraction touched. When an ADR and this file disagree, the
  ADR wins — and update this file.

## Two Claude sessions?

When two Claude Code sessions share this repo (Windows + macOS QA
pairing, etc.), treat GitHub Issues as the source of truth and
always `git fetch origin && git rebase origin/main` before every
local commit. Reference issues by `Fixes #N` / `Refs #N` in commit
messages so the timeline stays legible. If you see a commit you
didn't make against a file you're mid-edit on, stop and ask before
pushing.

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.