{
  "markdown": "# tlc-agent-harness\n\nSteers Cursor and Claude Code agents with **gates → follow-up → handoff → policy**.\n\nOn stop (and related hooks), the runtime can re-check work, require verified ship claims, persist handoff\nstate, and constrain subagent model choice — the same steering logic, driven through a provider-neutral\ncore and one anti-corruption-layer adapter per provider.\n\n## Start here\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/felipfr/tlc-agent-harness/main/install.sh | bash\n```\n\nThen restart Cursor or Claude Code. That is the whole setup — the installer finds which of the two you\nhave and wires only those, and the harness works in every repository right away with a safe baseline.\n\nTo give one project its own rules, open it and say **\"setup harness\"** to the agent, or run\n`tlc harness init --minimal`. To check anything, run `tlc harness doctor`.\n\n## Table of contents\n\n1. [Start here](#start-here)\n2. [Why it exists](#why-it-exists)\n3. [Providers](#providers)\n4. [Requirements](#requirements)\n5. [Install](#install)\n6. [Update](#update)\n7. [Quick start](#quick-start)\n8. [How it works](#how-it-works)\n9. [Commands](#commands)\n10. [Connect a project](#connect-a-project)\n11. [Paths and shared state](#paths-and-shared-state)\n12. [Ship claims](#ship-claims)\n13. [Price catalogs](#price-catalogs)\n14. [Windows](#windows)\n15. [Troubleshooting](#troubleshooting)\n16. [Documentation](#documentation)\n17. [Contributing](#contributing)\n18. [License](#license)\n\n## Why it exists\n\n| Goal | Mechanism |\n|------|-----------|\n| Hold a line no setting can cross | Floor tier, evaluated before any config is read |\n| Catch breakage early | Optional grind (lint/test) on stop |\n| Block false ship claims | `HARNESS_SHIP_CLAIM` + evidence |\n| Keep narration out of the diff | Comment gate on added lines, by declared reason |\n| Survive context loss | Handoff + lessons on disk |\n| Control cost/quality | Subagent model allowlist (per provider) |\n| Measure what happened | Observability + cost catalogs, tagged by provider |\n| See cost across every repo | Optional global observability spool under the runtime home |\n| Fail scope creep like a test | Plan gate: `HARNESS_PLAN` vs the diff, deviations need a stated reason |\n| Never obey content read from outside | Untrusted-content framing, once per turn |\n| Let CI and agents read the output | `--json` on every read command |\n\n### The floor\n\nFive rules read no configuration at all, so nothing in a config file and no edit by an agent can clear\nthem. Every denial names its rule.\n\n| Rule | Denies |\n|------|--------|\n| `outside-project-destruction` | A destructive command whose target resolves outside the repo and outside the OS temp directory |\n| `unprovable-destruction` | A destructive verb whose target is a variable, a substitution, or built at runtime |\n| `secret-access` | A read that would pull `.env`, `~/.ssh`, `~/.aws`, `*.pem` or similar into the transcript |\n| `history-rewrite` | `git push --force`. `--force-with-lease` is allowed, since it refuses when the remote moved |\n| `machine-control` | `shutdown`, `reboot`, `halt`, `poweroff` |\n| `policy-surface-write` | Any shell route to `.tlc/harness/config.json`, `flags/` or `state/` — a redirect, an interpreter, a heredoc program — plus the same paths under the runtime home `~/.tlc/harness`, and `tlc harness pause \\| resume \\| grind \\| mode \\| init \\| gate` from inside an agent session |\n\nHarness policy and state are not agent-writable, through a tool or a shell. Reading them stays allowed: a\nproven reader (`cat`, `grep`, `jq`, `test`, `git show`) on those paths passes, and anything not proven to only\nread does not — and the refusal names the way through, including `tlc harness handoff`. Policy changes are the operator's, from a terminal outside the agent session:\n\n```bash\ntlc harness gate test-command node --test 'src/**/__test__/*.test.ts'\ntlc harness gate lint-command npx biome check .\n```\n\nThe harness also hashes every policy source at session start. If one changes mid-session without a\n`tlc harness` command, the next tool call is refused and the change reported — the layer that covers what\nshell parsing cannot see.\n\nEverything else is opt-in: 21 capabilities, each presented with benefit, trade-off and default by the init\nskill. Full list in [`docs/architecture.md`](docs/architecture.md).\n\nRuntime: `~/.tlc/harness`.\nProject policy: `<repo>/.tlc/harness/config.json`.\n\n## Providers\n\nBoth providers share one runtime, one project policy file, and one on-disk state directory. Core steering\nlogic never imports a provider adapter and never branches on a provider's name — see\n[`docs/architecture.md`](docs/architecture.md) and [`docs/providers/index.md`](docs/providers/index.md).\n\n| Provider | Detected by | User-level wiring | Docs |\n|----------|-------------|--------------------|------|\n| **Cursor** | `CURSOR_CONFIG_DIR`, else `~/.cursor` | `<resolved>/hooks.json` (replaced) | [`docs/providers/cursor.md`](docs/providers/cursor.md) |\n| **Claude Code** | `CLAUDE_CONFIG_DIR`, else `~/.claude` | `<resolved>/settings.json` `hooks` block (merged) | [`docs/providers/claude-code.md`](docs/providers/claude-code.md) |\n\nThe installer and `tlc harness init` detect which of these are present and wire only those — neither\nassumes Cursor.\n\n## Requirements\n\n| Dependency | Notes |\n|------------|--------|\n| **Bun** *or* **Node.js 24+** | Either one is enough. Bun runs every hook directly with no build step (~1 ms/hook); Node needs 24 LTS or 26 and the shipped `dist/` (~27 ms/hook). With neither, the installer stops and names both fixes |\n| **git** | Installer clone/update |\n| **esbuild** (only for the Node path) | Needed once to recompile `dist/`; the published `dist/` already works |\n\n| Environment | Installer |\n|-------------|-----------|\n| Linux / macOS / WSL | `install.sh` |\n| Windows | `install.ps1` (see [Windows](#windows)) |\n\n## Install\n\n**Linux / macOS / WSL**\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/felipfr/tlc-agent-harness/main/install.sh | bash\n```\n\n**Windows (PowerShell)**\n\n```powershell\nirm https://raw.githubusercontent.com/felipfr/tlc-agent-harness/main/install.ps1 | iex\n```\n\nInstall target: `~/.tlc/harness` (runtime). The init skill is linked into the skills directory of\neach provider it finds, because a provider only reads its own.\n\nThe installer:\n\n1. Clones or updates the runtime at `~/.tlc/harness`\n2. Creates `config.json` from `config.example.json` when missing\n3. Adds `tlc` to `~/.local/bin`\n4. Links the init skill into each detected provider's `skills/harness-init`\n5. Wires user-level hooks for every provider it detects installed, in that provider's resolved config\n   directory\n\nOverrides: `TLC_HOME`, `TLC_REPO_URL`, `TLC_BIN_DIR`.\n\nProvider config directories are resolved, not assumed: `CLAUDE_CONFIG_DIR` and `CURSOR_CONFIG_DIR` are\nhonoured when set, so a relocated config is wired correctly. `tlc harness doctor` prints the resolved\ntarget for each provider.\n\nRestart or reload the provider session after install.\n\n**From a git clone** (same installers; then build `dist/`):\n\n```bash\ngit clone https://github.com/felipfr/tlc-agent-harness.git\ncd tlc-agent-harness\n./install.sh\n./bin/tlc-build\n```\n\n```powershell\ngit clone https://github.com/felipfr/tlc-agent-harness.git\ncd tlc-agent-harness\n.\\install.ps1\n.\\bin\\tlc-build\n```\n\n## Update\n\n```bash\ntlc harness update\n```\n\nMoves the runtime to upstream, refreshes CLI + init skill + provider wiring, then runs doctor.\nReload/restart the provider session afterward if hooks or the init skill should refresh.\n\n**The runtime path is an artifact the harness owns**, and update never touches anything else\n([AD-046](docs/decisions/ad-046.md)):\n\n| `tlc harness doctor` says | What update writes |\n| --- | --- |\n| `managed checkout` | moves it to upstream with a hard reset. Do not develop there — a local change is discarded |\n| `link to a working clone` | nothing in the clone. That is a contributor install; you pull it yourself |\n\n`dist/` is rebuilt only when a bundle is missing. Rebuilding a complete `dist/` is what used to dirty the checkout\nand break every later update, because Bun and esbuild emit different bytes for the same source.\n\n**If `update` aborts on `dist/`, re-run the install one-liner once.** A stuck install cannot deliver its own fix —\nthe fix lives in the revision `update` has to fetch — so the one-liner, which is fetched fresh from upstream, is\nthe recovery route. It hard-resets a managed checkout and leaves `config.json`, `state/` and any linked clone\nuntouched ([AD-048](docs/decisions/ad-048.md)). There is no `--force`.\n\nAfter a successful pull, prints a short digest of **optional catalog capabilities this project has not\nenabled yet** (benefit + trade-off + how to enable). Nothing is auto-enabled — use the harness-init skill or\nedit `.tlc/harness/config.json`.\n\n`tlc harness doctor` emits non-blocking `WARN:` lines for the same off/missing opt-ins (and for default-on\nfeatures you explicitly set to `false`).\n\n## Quick start\n\n```bash\ntlc harness doctor\ntlc harness help\ntlc harness status\n```\n\nHealthy install checklist:\n\n- Bun on PATH, or Node 24+ for the `dist/` fallback path\n- `~/.tlc/harness` present with `dist/*.mjs`\n- At least one provider's user-level hooks invoke `tlc-exec`\n- `tlc` on PATH (open a new shell if needed)\n\n## How it works\n\n```mermaid\ngraph LR\n    EV[\"provider event<br/><i>Cursor or Claude Code hook</i>\"]\n    HK[\"user-level hook file\"]\n    LX[\"bin/tlc-exec.mjs &lt;handler&gt;<br/><i>Bun first, Node + dist fallback</i>\"]\n    EP[\"src/entrypoints/&lt;handler&gt;.ts<br/><i>core + resolved adapter</i>\"]\n    OUT[\"follow-up · handoff · observability<br/><i>under project policy</i>\"]\n    EV --> HK --> LX --> EP --> OUT\n```\n\n| Layer | Location |\n|-------|----------|\n| Runtime | `~/.tlc/harness` |\n| Cursor user hooks | `<cursor config>/hooks.json` |\n| Claude Code user hooks | `<claude config>/settings.json` (`hooks` block) |\n| Project policy | `<repo>/.tlc/harness/config.json` |\n| Project shim (per provider) | `<repo>/.cursor/hooks.json`, `<repo>/.claude/settings.json` |\n\nEntrypoint: `bin/tlc-exec.mjs`.\nWrappers: `bin/tlc`, `bin/tlc-exec` (Unix); `bin/tlc.cmd`, `bin/tlc-exec.cmd` (Windows).\n\nSee `tlc harness help architecture` or [`docs/architecture.md`](docs/architecture.md).\n\n## Commands\n\n| Command | Purpose |\n|---------|---------|\n| `tlc harness status` | Mode, grind, gates |\n| `--json` on any read command | Machine-readable output: `status`, `doctor`, `obs`, `lessons`, `prices lookup` |\n| `tlc harness update` | Pull runtime + refresh skill/CLI/wiring + doctor |\n| `tlc harness doctor` | Health checklist |\n| `tlc harness help [topic]` | Docs |\n| `tlc harness build` | Compile `dist/` for the Node fallback path |\n| `tlc harness test` | Run the full local gate |\n| `tlc harness grind [on\\|off]` | Lint/test follow-ups on stop |\n| `tlc harness pause` / `resume` | Disable / enable stop checks |\n| `tlc harness mode solo\\|paired\\|focus` | Operator posture |\n| `tlc harness attest` | Tamper-evident record of what each session ran under |\n| `tlc harness handoff` | Handoff state between turns and sessions — the sanctioned reader |\n| `tlc harness obs live` / `obs report` | Signal / session rollup |\n| `tlc harness prices refresh` / `lookup` | Cost catalogs |\n| `tlc harness lessons list` | Lessons across the three tiers, with staleness and effectiveness |\n| `tlc harness lessons add \"…\" [--ref path:symbol] [--global] [--pin]` | Write a lesson; `--ref` retires it when that stops resolving, `--pin` puts it ahead of ranking |\n| `tlc harness init --minimal` | Project stub |\n\n## Connect a project\n\n1. Open the repository in Cursor and/or Claude Code.\n2. Run `tlc harness init --minimal`, or ask the agent to run the harness-init skill.\n3. Confirm `.tlc/harness/config.json` and the shim hooks for whichever provider(s) you use.\n4. Run `tlc harness doctor` from the project root.\n\nDetails: `tlc harness help init` or [`docs/init.md`](docs/init.md).\n\n## Paths and shared state\n\nBoth providers read and write the **same** project state — there is one `.tlc/harness/state/`, not one per\nprovider. Records inside it (signal, debug, audit) carry a `provider` field per event.\n\n| Path | Contents |\n|------|----------|\n| `~/.tlc/harness` | Runtime |\n| `~/.tlc/harness/state/lessons.json` | Global lesson tier — this machine, every product ([AD-040](docs/decisions/ad-040.md)) |\n| `<cursor config>/hooks.json` | Cursor user hooks (if Cursor installed) |\n| `<claude config>/settings.json` | Claude Code user hooks, `hooks` block (if Claude Code installed) |\n| `<provider config>/skills/harness-init` | Init skill, linked per detected provider from runtime `skills/harness-init` |\n| `<repo>/.tlc/harness/config.json` | Project policy (tracked) |\n| `<repo>/.tlc/harness/state/` | Handoff, obs, audit, project-tier `lessons.json`, ship ledger (gitignored) |\n\nDo not use `~/.tlc/harness` for anything other than the installed runtime — see\n[`docs/decisions/ad-002.md`](docs/decisions/ad-002.md) for why the layout is namespaced this way.\n\n## Ship claims\n\nProtocol line (free-form \"done/shipped\" is ignored):\n\n```text\nHARNESS_SHIP_CLAIM: <one-line summary>\n```\n\nWhen `shipGate` is enabled and runtime paths changed, cite recent PASS under `evidenceDir`.\nSee `tlc harness help concepts` or [`docs/concepts.md`](docs/concepts.md).\n\n## Price catalogs\n\n```bash\ntlc harness prices refresh\ntlc harness prices refresh cursor\ntlc harness prices refresh litellm\ntlc harness prices lookup <model-id> [provider]\n```\n\nSee `tlc harness help prices` or [`docs/measure.md`](docs/measure.md).\n\n## Windows\n\nPath resolution goes through `os.homedir()` only, hooks use exec form, filenames are sanitized,\natomic writes retry, and the CLI ships a `.cmd` shim alongside directory junctions\n([`docs/decisions/ad-006.md`](docs/decisions/ad-006.md)).\n\nCI runs the full suite and the `dist/` build on `windows-latest` on every push.\n\nOutside CI coverage: `install.ps1`, and hooks firing inside a Cursor or Claude Code session on Windows.\n\n## Troubleshooting\n\n| Symptom | Action |\n|---------|--------|\n| `tlc: command not found` | New shell; ensure `~/.local/bin` on PATH; re-run install |\n| Hooks never fire | Reload/restart the provider session; check the provider's own hook log; confirm `tlc-exec` |\n| Missing `dist/` | `tlc harness build` |\n| Cost `null` | `tlc harness help prices` |\n| Project doctor FAILs | Expected until project policy exists |\n\nSee `tlc harness help diagnose` or [`docs/diagnose.md`](docs/diagnose.md).\n\n## Documentation\n\nFull OKF v0.1 documentation bundle: [`docs/index.md`](docs/index.md).\n\n## Contributing\n\n[`CONTRIBUTING.md`](./CONTRIBUTING.md) · [`SECURITY.md`](./SECURITY.md)\n\n## License\n\n**PolyForm Noncommercial 1.0.0** — [`LICENSE`](./LICENSE), [`NOTICE`](./NOTICE).\n\n| Allowed | Requires separate license |\n|---------|---------------------------|\n| Noncommercial use, change, distribute with attribution | Commercial use |\n| Keep `Required Notice` + license terms | Dropping attribution |\n",
  "bytes": 15017,
  "sha": "1700c89d1990ae50b77cfc3e5b3783f37c037e7f077ac982c023df71635e8282",
  "repo_slug": "felipfr/tlc-agent-harness",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_felipfr_tlc_agent_harness_docs_index_md_6e538df1/readme"
}