@tech-leads-club/harness-toolkit 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/LICENSE +84 -0
- package/NOTICE +9 -0
- package/README.md +480 -0
- package/bin/tlc +10 -0
- package/bin/tlc-build +80 -0
- package/bin/tlc-cli.ts +1509 -0
- package/bin/tlc-exec +10 -0
- package/bin/tlc-exec.cmd +4 -0
- package/bin/tlc-exec.d.mts +60 -0
- package/bin/tlc-exec.mjs +248 -0
- package/bin/tlc.cmd +4 -0
- package/bin/tlc.mjs +7 -0
- package/bin/write-user-hooks.d.mts +40 -0
- package/bin/write-user-hooks.mjs +123 -0
- package/capabilities/catalog.json +418 -0
- package/config.example.json +51 -0
- package/dist/compact-before.mjs +7839 -0
- package/dist/doctor.mjs +8263 -0
- package/dist/help-topic.mjs +82 -0
- package/dist/init-project.mjs +993 -0
- package/dist/install-runtime.mjs +969 -0
- package/dist/lessons-cli.mjs +7258 -0
- package/dist/obs-cli.mjs +7025 -0
- package/dist/price-lookup.mjs +221 -0
- package/dist/prompt-submit.mjs +7860 -0
- package/dist/refresh-model-prices.mjs +158 -0
- package/dist/response-after.mjs +7879 -0
- package/dist/run.mjs +7830 -0
- package/dist/session-end.mjs +7937 -0
- package/dist/session-start.mjs +8056 -0
- package/dist/shim.mjs +52 -0
- package/dist/stop.mjs +8494 -0
- package/dist/subagent-start.mjs +7861 -0
- package/dist/subagent-stop.mjs +7851 -0
- package/dist/support.mjs +7045 -0
- package/dist/tlc-cli.mjs +8063 -0
- package/dist/tool-after.mjs +8113 -0
- package/dist/tool-before.mjs +7985 -0
- package/dist/tool-failure.mjs +7850 -0
- package/dist/uninstall-runtime.mjs +1307 -0
- package/docs/architecture.md +270 -0
- package/docs/concepts.md +562 -0
- package/docs/coverage.md +55 -0
- package/docs/decisions/ad-001.md +32 -0
- package/docs/decisions/ad-002.md +51 -0
- package/docs/decisions/ad-003.md +30 -0
- package/docs/decisions/ad-004.md +37 -0
- package/docs/decisions/ad-005.md +36 -0
- package/docs/decisions/ad-006.md +49 -0
- package/docs/decisions/ad-007.md +36 -0
- package/docs/decisions/ad-008.md +54 -0
- package/docs/decisions/ad-009.md +61 -0
- package/docs/decisions/ad-010.md +45 -0
- package/docs/decisions/ad-011.md +59 -0
- package/docs/decisions/ad-012.md +71 -0
- package/docs/decisions/ad-013.md +87 -0
- package/docs/decisions/ad-014.md +56 -0
- package/docs/decisions/ad-015.md +33 -0
- package/docs/decisions/ad-016.md +98 -0
- package/docs/decisions/ad-017.md +65 -0
- package/docs/decisions/ad-018.md +77 -0
- package/docs/decisions/ad-019.md +75 -0
- package/docs/decisions/ad-020.md +88 -0
- package/docs/decisions/ad-021.md +57 -0
- package/docs/decisions/ad-022.md +120 -0
- package/docs/decisions/ad-023.md +87 -0
- package/docs/decisions/ad-024.md +85 -0
- package/docs/decisions/ad-025.md +92 -0
- package/docs/decisions/ad-026.md +131 -0
- package/docs/decisions/ad-027.md +90 -0
- package/docs/decisions/ad-028.md +90 -0
- package/docs/decisions/ad-029.md +73 -0
- package/docs/decisions/ad-030.md +97 -0
- package/docs/decisions/ad-031.md +94 -0
- package/docs/decisions/ad-032.md +83 -0
- package/docs/decisions/ad-033.md +89 -0
- package/docs/decisions/ad-034.md +86 -0
- package/docs/decisions/ad-035.md +86 -0
- package/docs/decisions/ad-036.md +68 -0
- package/docs/decisions/ad-037.md +47 -0
- package/docs/decisions/ad-038.md +52 -0
- package/docs/decisions/ad-039.md +69 -0
- package/docs/decisions/ad-040.md +89 -0
- package/docs/decisions/ad-041.md +98 -0
- package/docs/decisions/ad-042.md +82 -0
- package/docs/decisions/ad-043.md +79 -0
- package/docs/decisions/ad-044.md +61 -0
- package/docs/decisions/ad-045.md +94 -0
- package/docs/decisions/ad-046.md +111 -0
- package/docs/decisions/ad-047.md +96 -0
- package/docs/decisions/ad-048.md +85 -0
- package/docs/decisions/ad-049.md +66 -0
- package/docs/decisions/ad-050.md +94 -0
- package/docs/decisions/ad-051.md +69 -0
- package/docs/decisions/ad-052.md +69 -0
- package/docs/decisions/ad-053.md +78 -0
- package/docs/decisions/ad-054.md +98 -0
- package/docs/decisions/ad-055.md +74 -0
- package/docs/decisions/ad-056.md +85 -0
- package/docs/decisions/ad-057.md +68 -0
- package/docs/decisions/ad-058.md +97 -0
- package/docs/decisions/ad-059.md +82 -0
- package/docs/decisions/ad-060.md +75 -0
- package/docs/decisions/ad-061.md +68 -0
- package/docs/decisions/ad-062.md +72 -0
- package/docs/decisions/ad-063.md +84 -0
- package/docs/decisions/ad-064.md +79 -0
- package/docs/decisions/ad-065.md +81 -0
- package/docs/decisions/ad-066.md +111 -0
- package/docs/decisions/ad-067.md +64 -0
- package/docs/decisions/ad-068.md +79 -0
- package/docs/decisions/ad-069.md +74 -0
- package/docs/decisions/ad-070.md +86 -0
- package/docs/decisions/ad-071.md +93 -0
- package/docs/decisions/ad-072.md +82 -0
- package/docs/decisions/ad-073.md +102 -0
- package/docs/decisions/ad-074.md +91 -0
- package/docs/decisions/ad-075.md +79 -0
- package/docs/decisions/ad-076.md +102 -0
- package/docs/decisions/ad-077.md +94 -0
- package/docs/decisions/ad-078.md +84 -0
- package/docs/decisions/ad-079.md +73 -0
- package/docs/decisions/ad-080.md +86 -0
- package/docs/decisions/ad-081.md +70 -0
- package/docs/decisions/ad-082.md +79 -0
- package/docs/decisions/ad-083.md +88 -0
- package/docs/decisions/index.md +111 -0
- package/docs/diagnose.md +248 -0
- package/docs/index.md +52 -0
- package/docs/init.md +101 -0
- package/docs/lessons.md +294 -0
- package/docs/log.md +139 -0
- package/docs/measure.md +178 -0
- package/docs/providers/claude-code.md +118 -0
- package/docs/providers/cursor.md +110 -0
- package/docs/providers/index.md +71 -0
- package/docs/troubleshooting.md +138 -0
- package/model-aliases.json +12 -0
- package/model-prices.cursor.json +410 -0
- package/model-prices.json +1 -0
- package/package.json +57 -0
- package/skills/harness-init/SKILL.md +241 -0
- package/skills/harness-init/evals/trigger_evals.json +86 -0
- package/skills/harness-init/references/capabilities.md +113 -0
- package/src/contracts/capabilities.ts +30 -0
- package/src/contracts/decision.ts +21 -0
- package/src/contracts/effort.ts +15 -0
- package/src/contracts/harness-event.ts +87 -0
- package/src/contracts/index.ts +6 -0
- package/src/contracts/policy-defaults.ts +14 -0
- package/src/contracts/wiring.ts +20 -0
- package/src/core/attest/attest.service.ts +0 -0
- package/src/core/capability/capability.service.ts +62 -0
- package/src/core/capability/capability.store.ts +51 -0
- package/src/core/capability/capability.types.ts +56 -0
- package/src/core/comment-policy/comment-policy.service.ts +307 -0
- package/src/core/comment-policy/comment-policy.types.ts +6 -0
- package/src/core/comment-policy/comment-resolvability.ts +134 -0
- package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
- package/src/core/comment-policy/comment-syntax.store.ts +65 -0
- package/src/core/comment-policy/comment-syntax.types.ts +24 -0
- package/src/core/core.facade.ts +461 -0
- package/src/core/duplication/duplication.service.ts +219 -0
- package/src/core/floor/floor.catalog.ts +54 -0
- package/src/core/floor/floor.paths.ts +90 -0
- package/src/core/floor/floor.policy-surface.ts +311 -0
- package/src/core/floor/floor.service.ts +306 -0
- package/src/core/floor/floor.tokenize.ts +179 -0
- package/src/core/floor/floor.verb.ts +28 -0
- package/src/core/floor/index.ts +1 -0
- package/src/core/gate/gate.artifact.ts +157 -0
- package/src/core/gate/gate.command.ts +130 -0
- package/src/core/gate/gate.findings.ts +201 -0
- package/src/core/gate/gate.inputs.ts +0 -0
- package/src/core/gate/gate.lock.ts +283 -0
- package/src/core/gate/gate.service.ts +28 -0
- package/src/core/gate/gate.types.ts +67 -0
- package/src/core/handoff/handoff.service.ts +43 -0
- package/src/core/handoff/handoff.store.ts +59 -0
- package/src/core/handoff/handoff.types.ts +89 -0
- package/src/core/index.ts +52 -0
- package/src/core/integrity/state-seal.ts +81 -0
- package/src/core/lesson/lesson.authored.ts +96 -0
- package/src/core/lesson/lesson.credit.ts +51 -0
- package/src/core/lesson/lesson.garden.ts +283 -0
- package/src/core/lesson/lesson.link.ts +93 -0
- package/src/core/lesson/lesson.score.ts +61 -0
- package/src/core/lesson/lesson.select.ts +209 -0
- package/src/core/lesson/lesson.service.ts +110 -0
- package/src/core/lesson/lesson.store.ts +317 -0
- package/src/core/lesson/lesson.sync.ts +70 -0
- package/src/core/lesson/lesson.types.ts +92 -0
- package/src/core/lesson/lesson.validity.ts +44 -0
- package/src/core/observability/observability.report.ts +326 -0
- package/src/core/observability/observability.service.ts +328 -0
- package/src/core/observability/observability.store.ts +269 -0
- package/src/core/observability/observability.types.ts +193 -0
- package/src/core/observability/observability.why.ts +206 -0
- package/src/core/observe/observe.service.ts +76 -0
- package/src/core/plan/plan.detect.ts +47 -0
- package/src/core/plan/plan.service.ts +81 -0
- package/src/core/plan/plan.types.ts +25 -0
- package/src/core/policy/policy.defaults.ts +103 -0
- package/src/core/policy/policy.guard.ts +34 -0
- package/src/core/policy/policy.integrity.ts +238 -0
- package/src/core/policy/policy.loader.ts +118 -0
- package/src/core/policy/policy.operator.ts +82 -0
- package/src/core/policy/policy.posture.ts +79 -0
- package/src/core/policy/policy.rails.ts +45 -0
- package/src/core/policy/policy.types.ts +157 -0
- package/src/core/presence/presence.service.ts +111 -0
- package/src/core/presence/presence.store.ts +59 -0
- package/src/core/presence/presence.types.ts +9 -0
- package/src/core/release/release.decisions.ts +147 -0
- package/src/core/release/release.seen.ts +38 -0
- package/src/core/shell-policy/shell-policy.service.ts +215 -0
- package/src/core/shell-policy/shell-policy.stall.ts +57 -0
- package/src/core/shell-policy/shell-policy.types.ts +26 -0
- package/src/core/ship/ship.ledger.ts +76 -0
- package/src/core/ship/ship.service.ts +137 -0
- package/src/core/ship/ship.types.ts +19 -0
- package/src/core/stagnation/stagnation.resolution.ts +87 -0
- package/src/core/stagnation/stagnation.service.ts +20 -0
- package/src/core/stagnation/stagnation.store.ts +49 -0
- package/src/core/stagnation/stagnation.types.ts +12 -0
- package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
- package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
- package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
- package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
- package/src/core/supply-chain/supply-chain.service.ts +217 -0
- package/src/core/turn/turn.activity.ts +94 -0
- package/src/core/turn/turn.autopilot.ts +131 -0
- package/src/core/turn/turn.failure-signals.ts +193 -0
- package/src/core/turn/turn.loop-counter.ts +71 -0
- package/src/core/turn/turn.types.ts +14 -0
- package/src/core/untrusted/untrusted.detect.ts +53 -0
- package/src/core/untrusted/untrusted.recall.ts +87 -0
- package/src/core/untrusted/untrusted.service.ts +129 -0
- package/src/core/untrusted/untrusted.store.ts +75 -0
- package/src/core/untrusted/untrusted.types.ts +42 -0
- package/src/entrypoints/compact-before.ts +14 -0
- package/src/entrypoints/prompt-submit.ts +25 -0
- package/src/entrypoints/response-after.ts +60 -0
- package/src/entrypoints/run.ts +179 -0
- package/src/entrypoints/session-end.ts +58 -0
- package/src/entrypoints/session-start.ts +172 -0
- package/src/entrypoints/shim.ts +46 -0
- package/src/entrypoints/stop.ts +876 -0
- package/src/entrypoints/subagent-start.ts +16 -0
- package/src/entrypoints/subagent-stop.ts +32 -0
- package/src/entrypoints/support.ts +182 -0
- package/src/entrypoints/tool-after.ts +111 -0
- package/src/entrypoints/tool-before.ts +186 -0
- package/src/entrypoints/tool-failure.ts +26 -0
- package/src/platform/backoff.ts +49 -0
- package/src/platform/cli-output.ts +30 -0
- package/src/platform/env-scope.ts +22 -0
- package/src/platform/fs-atomic.ts +142 -0
- package/src/platform/fs-jsonl.ts +24 -0
- package/src/platform/git.ts +187 -0
- package/src/platform/paths.ts +80 -0
- package/src/platform/pricing.ts +229 -0
- package/src/platform/process.ts +64 -0
- package/src/platform/sanitize.ts +23 -0
- package/src/platform/screen.ts +46 -0
- package/src/platform/style.ts +103 -0
- package/src/providers/claude/claude.capabilities.ts +25 -0
- package/src/providers/claude/claude.detect.ts +14 -0
- package/src/providers/claude/claude.inbound.ts +252 -0
- package/src/providers/claude/claude.lessons-view.ts +33 -0
- package/src/providers/claude/claude.outbound.ts +65 -0
- package/src/providers/claude/claude.policy-defaults.ts +9 -0
- package/src/providers/claude/claude.transcript.ts +62 -0
- package/src/providers/claude/claude.wiring.ts +245 -0
- package/src/providers/claude/index.ts +17 -0
- package/src/providers/cursor/cursor.capabilities.ts +28 -0
- package/src/providers/cursor/cursor.detect.ts +14 -0
- package/src/providers/cursor/cursor.inbound.ts +203 -0
- package/src/providers/cursor/cursor.lessons-view.ts +30 -0
- package/src/providers/cursor/cursor.outbound.ts +43 -0
- package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +214 -0
- package/src/providers/cursor/index.ts +17 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider.degrade.ts +152 -0
- package/src/providers/provider.port.ts +20 -0
- package/src/providers/provider.registry.ts +28 -0
- package/tools/doctor.ts +631 -0
- package/tools/help-topic.ts +74 -0
- package/tools/init-project.ts +277 -0
- package/tools/install-runtime.ts +164 -0
- package/tools/lessons-cli.ts +474 -0
- package/tools/obs-cli.ts +175 -0
- package/tools/price-lookup.ts +57 -0
- package/tools/refresh-model-prices.ts +210 -0
- package/tools/test-env.mjs +28 -0
- package/tools/test-env.names.d.mts +1 -0
- package/tools/test-env.names.mjs +14 -0
- package/tools/uninstall-runtime.ts +463 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-init
|
|
3
|
+
description: Mandatory interactive wizard to connect a repo to the global harness (writes .tlc/harness/config.json + shim hooks via tlc-exec, for whichever of Cursor / Claude Code are installed). Use when the user says setup harness, init harness, enable harness, ligar o harness, bootstrap harness, configure harness for this repo, or wants lessons/grind/shipGate policy chosen with trade-offs. Do NOT use for status (tlc harness status), expert Q&A (tlc harness help), grind/mode toggles alone, or explaining metrics (send those to tlc harness help).
|
|
4
|
+
license: CC-BY-4.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: Felipe Rodrigues
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Harness init (mandatory project wizard)
|
|
11
|
+
|
|
12
|
+
Connect the **global** harness runtime to **this** repo, for whichever providers are actually installed
|
|
13
|
+
(Cursor, Claude Code, or both). The skill is required for setup; **each capability is optional** and must
|
|
14
|
+
be chosen with benefit / trade-off / default.
|
|
15
|
+
|
|
16
|
+
Help SoT is CLI docs (`tlc harness help <topic>`), not this skill. For Q&A only, stop and send the user to
|
|
17
|
+
`tlc harness help`.
|
|
18
|
+
|
|
19
|
+
## Instructions
|
|
20
|
+
|
|
21
|
+
### Step 1: Preconditions
|
|
22
|
+
|
|
23
|
+
1. Run `tlc harness doctor` and `tlc harness help init`.
|
|
24
|
+
2. If global runtime checks FAIL (Node 24+, `dist/`, `tlc-exec`), stop and fix the global install
|
|
25
|
+
(`tlc harness build`, PATH link) before project policy.
|
|
26
|
+
3. Detect which providers are installed. **Resolve the directories, do not assume them**: Claude Code
|
|
27
|
+
honours `CLAUDE_CONFIG_DIR` and Cursor honours `CURSOR_CONFIG_DIR`, so a relocated config is common
|
|
28
|
+
and wiring the default path would write a file the agent never reads. `tlc harness doctor` prints the
|
|
29
|
+
resolved target for each provider — read it from there rather than guessing. Tell the user which
|
|
30
|
+
provider(s) will be wired. Never assume Cursor: a Claude-only machine has no Cursor directory at all.
|
|
31
|
+
4. If `.tlc/harness/config.json` already exists, show it and ask: **overwrite**, **merge**, or **abort**.
|
|
32
|
+
|
|
33
|
+
### Step 1b: Hook runtime (ask once, never block)
|
|
34
|
+
|
|
35
|
+
Hooks fire on every tool call, shell command, file read and stop — dozens per agent turn — so the runtime
|
|
36
|
+
that launches them dominates their cost, regardless of which provider is in use.
|
|
37
|
+
|
|
38
|
+
Check whether **Bun** is on `PATH`.
|
|
39
|
+
|
|
40
|
+
**Bun present** → say so in one line and move on.
|
|
41
|
+
|
|
42
|
+
**Bun absent** → present it as a trade-off, not a warning, with the measured numbers:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Hook runtime: Node (Bun not found)
|
|
46
|
+
Bun runs each hook in ~1ms; Node needs ~27ms.
|
|
47
|
+
At ~30 hooks per turn that is ~30ms vs ~810ms of startup.
|
|
48
|
+
Install: curl -fsSL https://bun.sh/install | bash
|
|
49
|
+
Node works fine — just slower. Continue on Node?
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Rules:
|
|
53
|
+
|
|
54
|
+
- **Never block.** Node is fully supported. The question is "continue?", not "install first".
|
|
55
|
+
- **Never ask twice.** Record the answer in the project's harness state; on later runs skip this step.
|
|
56
|
+
`doctor` keeps reporting it as a non-failing `WARN`.
|
|
57
|
+
- **Always give the number.** "~1ms vs ~27ms" lets the user decide; "faster" gets ignored.
|
|
58
|
+
- If the user installs Bun mid-wizard, re-check before continuing rather than trusting the earlier probe.
|
|
59
|
+
|
|
60
|
+
### Step 1c: State the floor once (no question attached)
|
|
61
|
+
|
|
62
|
+
Before offering any choice, tell the user what holds regardless of configuration. Read the
|
|
63
|
+
**Not configurable** table in `references/capabilities.md` and state it in two or three lines. This is not
|
|
64
|
+
a question and nothing here is optional; skipping it means their first denial looks like a defect.
|
|
65
|
+
|
|
66
|
+
### Step 2: Discovery (never skip)
|
|
67
|
+
|
|
68
|
+
Read `references/capabilities.md` **now** (full menu + lessons automation table).
|
|
69
|
+
|
|
70
|
+
Work **one capability at a time** from that catalog. For each: name → benefit → trade-off → default → ask
|
|
71
|
+
yes/no → if yes, collect values. Stay stack-agnostic.
|
|
72
|
+
|
|
73
|
+
When presenting **Subagent allowlist** (capability 6), also present **Block parent Fast** (capability 6b /
|
|
74
|
+
`subagents.blockParentFast`, default **off**) from `references/capabilities.md` in the same pass: benefit →
|
|
75
|
+
trade-off → default → ask yes/no. Write `blockParentFast: true` only if the user accepts.
|
|
76
|
+
|
|
77
|
+
If the user accepts the allowlist, **collect the model slugs from them**. The harness ships no catalogue, and
|
|
78
|
+
`enforceAllowlist: true` with an empty `allowedModels` enforces nothing — `doctor` reports it as a fault. Do not
|
|
79
|
+
propose slugs: a suggested list is a shipped default one conversation later, and the one that used to be shipped is
|
|
80
|
+
exactly what went stale ([/decisions/ad-053.md](/decisions/ad-053.md)). Tell them the values their provider accepts
|
|
81
|
+
are in its own model picker, that a variant suffix such as `-thinking-high` needs its own entry, and that `inherit`
|
|
82
|
+
is a value the list may contain. If they accept the capability but have no list ready, write
|
|
83
|
+
`enforceAllowlist: false` and say why.
|
|
84
|
+
|
|
85
|
+
Do **not** enable anything the user did not accept. Do **not** invent stack commands.
|
|
86
|
+
|
|
87
|
+
### Step 2b: Comment gate — four choices, ask explicitly
|
|
88
|
+
|
|
89
|
+
Agents narrate. Prose in an instruction file does not stop it, so this is the deterministic gate.
|
|
90
|
+
It compares added lines against the commit the turn started from, so comments already committed are
|
|
91
|
+
never flagged — and a turn that commits its own work cannot move the base past its own lines.
|
|
92
|
+
|
|
93
|
+
Present all four and let the user pick:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
off — no comment gate. Anything the agent writes lands.
|
|
97
|
+
declared — an added comment must declare a reason: why: / hazard: / invariant:
|
|
98
|
+
Narration is blocked; a real hazard can be recorded without interrupting you.
|
|
99
|
+
Cost: a marker prefix no other codebase uses.
|
|
100
|
+
resolvable — declared, plus the question a marker cannot answer: can a reader who was not
|
|
101
|
+
in this session check the comment? Blocks change narration ("this used to"),
|
|
102
|
+
citations only the session saw ("(decision 3)", "per the plan"), PR vantage
|
|
103
|
+
("this PR"), and comments arguing their own correctness.
|
|
104
|
+
Cost: a phrase-level rule, so it refuses some sentences that were fine.
|
|
105
|
+
strict — no agent-added comments at all. If one is warranted the agent says so in
|
|
106
|
+
its reply and you write it. Cost: interrupts more; zero invented convention.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Write `comments.enabled: false` for **off**, or `comments.enabled: true` with
|
|
110
|
+
`comments.mode: "declared" | "resolvable" | "strict"`. Default is off, like every other capability.
|
|
111
|
+
|
|
112
|
+
In every mode a `/** */` comment attached to a declaration is judged differently: it does not need a
|
|
113
|
+
marker, but it must say something its identifier does not already say. A floating `/** */` inside a
|
|
114
|
+
function body is not attached to anything, so it counts as an ordinary comment.
|
|
115
|
+
|
|
116
|
+
Tool directives (`biome-ignore`, `@ts-`, `noqa`, `shellcheck`, shebang) are exempt in every mode.
|
|
117
|
+
|
|
118
|
+
### Step 2c: Idle-turn gate (capability 17 — offer it, default off)
|
|
119
|
+
|
|
120
|
+
The mode lines tell the agent not to interrupt for reversible work, but that is prose and prose is
|
|
121
|
+
probabilistic. This is the deterministic half.
|
|
122
|
+
|
|
123
|
+
It blocks the stop when a turn ends with open handoff work, **zero recorded tool calls and zero file
|
|
124
|
+
changes**. It counts events the harness already recorded — it never reads or judges the agent's reply,
|
|
125
|
+
so there is no wording that satisfies it. Works on both providers, since both fire tool hooks.
|
|
126
|
+
|
|
127
|
+
Ask yes/no. Default off. Turn it off for conversational repos where answering without touching files
|
|
128
|
+
is a normal turn.
|
|
129
|
+
|
|
130
|
+
### Step 2d: Docs staleness gate (capability 18 — offer it, default off)
|
|
131
|
+
|
|
132
|
+
Code changes pass lint, tests and the ship gate; the documents describing them pass nothing. This gate runs
|
|
133
|
+
**the repository's own** staleness tool on stop, the same way grind runs its lint and test. The harness does
|
|
134
|
+
not infer staleness from paths: mapping directories to documentation was measured reporting on 82–100% of
|
|
135
|
+
commits, which detects nothing.
|
|
136
|
+
|
|
137
|
+
So the question is which tool the repository has, not which globs to write. Look for evidence before asking:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
drift.lock, .drift/ → drift (AST anchors; command is usually: drift check)
|
|
141
|
+
openapi.yaml + oasdiff in CI → oasdiff (usually: oasdiff changelog <base> <head> --fail-on-diff)
|
|
142
|
+
sgconfig.yml, ast-grep rules → ast-grep (usually: ast-grep scan)
|
|
143
|
+
a docs script in package.json → that script
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Ask, in this order:
|
|
147
|
+
|
|
148
|
+
1. Is there such a tool? If the repository has none, **say so and leave the capability off.** A gate with
|
|
149
|
+
nothing to run is not worth configuring, and inventing a command is worse than no gate.
|
|
150
|
+
2. If there is one, ask for the exact argv array. Never guess flags.
|
|
151
|
+
3. `warn` or `deny`. Default `warn`; reserve `deny` for a surface where a stale document is actively harmful.
|
|
152
|
+
|
|
153
|
+
The tool owns its own escape hatch — `drift` requires an explicit confirmation that the document was read
|
|
154
|
+
before re-stamping — so the harness adds no skip token of its own.
|
|
155
|
+
|
|
156
|
+
### Step 3: Confirm
|
|
157
|
+
|
|
158
|
+
Show the **full proposed** `.tlc/harness/config.json` in a fenced block (English keys/strings only). Ask
|
|
159
|
+
explicit confirmation before writing.
|
|
160
|
+
|
|
161
|
+
### Step 4: Write
|
|
162
|
+
|
|
163
|
+
Only after confirmation:
|
|
164
|
+
|
|
165
|
+
1. Prefer: `echo '<policy-json>' | tlc harness init --write --stdin-json`
|
|
166
|
+
2. If that fails: write `.tlc/harness/config.json` yourself; `tlc harness init --minimal` only as last
|
|
167
|
+
resort for hooks/gitignore, then re-apply the agreed policy.
|
|
168
|
+
3. Shim hooks must call `$HOME/.tlc/harness/bin/tlc-exec shim <handler>` (never `bun run …/src/*.ts`
|
|
169
|
+
directly, and never a bare `harness-exec` from the predecessor layout).
|
|
170
|
+
4. `tlc harness init` writes project shim hooks only for providers detected present, using the config
|
|
171
|
+
directory each provider actually resolves to — the defaults are `~/.cursor` and `~/.claude`, but
|
|
172
|
+
`CURSOR_CONFIG_DIR` and `CLAUDE_CONFIG_DIR` override them and a relocated config is common. Take the
|
|
173
|
+
resolved paths from `tlc harness doctor` rather than assuming either default.
|
|
174
|
+
5. Ensure `.gitignore` covers `.tlc/harness/state/`.
|
|
175
|
+
6. If `intelligence.lessons.enabled` is true and `syncRulesFile` is not `never`, run `tlc harness lessons
|
|
176
|
+
sync-rules` once after write. Under the default `auto` the durable view is written where the provider does not
|
|
177
|
+
deliver hook context, so do not ask the user to choose a transport — ask only whether they want the file at all.
|
|
178
|
+
7. Run `tlc harness status` and `tlc harness doctor`.
|
|
179
|
+
8. Tell the user: reload/restart the provider session if hooks were new; next agent turn should set
|
|
180
|
+
`TLC_ACTIVE`; day-to-day help is `tlc harness help`.
|
|
181
|
+
|
|
182
|
+
**Never** create `<repo>/.cursor/commands/` or an equivalent project-local commands directory for harness
|
|
183
|
+
slash commands — keep provider-native commands global.
|
|
184
|
+
|
|
185
|
+
### Step 5: Explain automation (if lessons or grind enabled)
|
|
186
|
+
|
|
187
|
+
State clearly what runs **without further prompts**:
|
|
188
|
+
|
|
189
|
+
- Grind/ship/comments/intelligence rails fire on **stop** / **sessionStart** via hooks, on every wired
|
|
190
|
+
provider.
|
|
191
|
+
- Ship gate (when enabled) reacts **only** to an explicit `HARNESS_SHIP_CLAIM: …` line — free-English
|
|
192
|
+
"done/shipped" is ignored. Prefer `runtimePathExcludes` (defaults include `.tlc/`, `.cursor/`, `.claude/`)
|
|
193
|
+
over pausing gates forever.
|
|
194
|
+
- Lessons (when enabled): auto-record on **stagnation**, auto-inject on start/retry, auto-garden on
|
|
195
|
+
sessionEnd; the provider-native durable view (Cursor's `.cursor/rules/harness-lessons.mdc` or Claude's
|
|
196
|
+
`CLAUDE.md` import line) syncs only if configured.
|
|
197
|
+
- Enabling capabilities is never automatic — only what the user opted into during discovery.
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
### Example 1: New repo, Cursor only
|
|
202
|
+
|
|
203
|
+
User: "setup harness here"
|
|
204
|
+
Actions: doctor → detect Cursor installed, Claude Code not → read capabilities.md → one-by-one opt-in →
|
|
205
|
+
confirm JSON → `tlc harness init --write --stdin-json` → status/doctor
|
|
206
|
+
Result: policy + Cursor shim; features only where user said yes
|
|
207
|
+
|
|
208
|
+
### Example 2: Repo already used with Claude Code, enabling lessons
|
|
209
|
+
|
|
210
|
+
User: "ligar lessons no harness deste repo"
|
|
211
|
+
Actions: show current `.tlc/harness/config.json` → explain auto record/inject/garden + `syncRulesFile`
|
|
212
|
+
trade-off → merge `intelligence.lessons` → confirm → write → `tlc harness lessons sync-rules` if sync on →
|
|
213
|
+
doctor
|
|
214
|
+
Result: lessons enabled; no unrelated feature flips; only Claude's `CLAUDE.md` import line is touched,
|
|
215
|
+
since Cursor is not installed on this machine
|
|
216
|
+
|
|
217
|
+
### Example 3: Wrong skill
|
|
218
|
+
|
|
219
|
+
User: "how do I see harness metrics?"
|
|
220
|
+
Actions: do **not** run init; tell them `tlc harness help measure`
|
|
221
|
+
Result: no files written
|
|
222
|
+
|
|
223
|
+
## Troubleshooting
|
|
224
|
+
|
|
225
|
+
### Error: tlc not found
|
|
226
|
+
|
|
227
|
+
Cause: CLI not on PATH.
|
|
228
|
+
Solution: `ln -sfn ~/.tlc/harness/bin/tlc ~/.local/bin/tlc`
|
|
229
|
+
|
|
230
|
+
### Error: doctor fails Node / dist
|
|
231
|
+
|
|
232
|
+
Cause: neither Bun nor Node 24+ is available, or the bundles are missing.
|
|
233
|
+
Solution: either is enough — `curl -fsSL https://bun.sh/install | bash`, or Node 24 LTS / 26 Current from
|
|
234
|
+
nodejs.org. Bun needs no build step; on Node run `tlc harness build`. Reload the editor session afterwards.
|
|
235
|
+
Doctor reports an old Node as `OK` when Bun covers it, so check the `hook runtime` line too.
|
|
236
|
+
|
|
237
|
+
### Error: doctor fails global runtime
|
|
238
|
+
|
|
239
|
+
Cause: incomplete `~/.tlc/harness` or hooks still pointing at a predecessor path (`~/.cursor/agent-harness`,
|
|
240
|
+
`harness-exec`).
|
|
241
|
+
Solution: fix global hooks to `tlc-exec` under `~/.tlc/harness`; rebuild dist.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "setup harness",
|
|
4
|
+
"should_trigger": true,
|
|
5
|
+
"phrase": "setup harness",
|
|
6
|
+
"note": "the canonical opening phrase named in the description"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"query": "setup harness aqui neste repo, quero escolher as regras",
|
|
10
|
+
"should_trigger": true,
|
|
11
|
+
"phrase": "setup harness",
|
|
12
|
+
"note": "same intent in Portuguese, with the policy choice made explicit"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "init harness for this project",
|
|
16
|
+
"should_trigger": true,
|
|
17
|
+
"phrase": "init harness",
|
|
18
|
+
"note": "init is the CLI verb; the wizard owns the interactive path"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"query": "enable harness on this repository",
|
|
22
|
+
"should_trigger": true,
|
|
23
|
+
"phrase": "enable harness",
|
|
24
|
+
"note": "enable reads as first-time connection, not as a single toggle"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "ligar o harness neste repositório",
|
|
28
|
+
"should_trigger": true,
|
|
29
|
+
"phrase": "ligar o harness",
|
|
30
|
+
"note": "Portuguese trigger; the skill is bilingual by description"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"query": "bootstrap harness",
|
|
34
|
+
"should_trigger": true,
|
|
35
|
+
"phrase": "bootstrap harness",
|
|
36
|
+
"note": "bootstrap implies writing project policy plus shim hooks"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"query": "configure harness for this repo",
|
|
40
|
+
"should_trigger": true,
|
|
41
|
+
"phrase": "configure harness for this repo",
|
|
42
|
+
"note": "configure plus repo scope is the wizard, not the help topic"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"query": "quero definir a política de lessons e grind com os trade-offs explicados",
|
|
46
|
+
"should_trigger": true,
|
|
47
|
+
"phrase": "policy",
|
|
48
|
+
"note": "policy chosen with trade-offs is a listed trigger even without the word harness"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"query": "I want the shipGate policy for this project, walk me through the trade-offs",
|
|
52
|
+
"should_trigger": true,
|
|
53
|
+
"phrase": "policy",
|
|
54
|
+
"note": "shipGate policy with trade-offs routes to discovery, capability by capability"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"query": "qual o status do harness agora?",
|
|
58
|
+
"should_trigger": false,
|
|
59
|
+
"note": "status is a non-goal — answer with tlc harness status, never run the wizard"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"query": "how do I see harness metrics?",
|
|
63
|
+
"should_trigger": false,
|
|
64
|
+
"note": "explaining metrics is a non-goal — send this to tlc harness help measure"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"query": "what does the ship gate actually do?",
|
|
68
|
+
"should_trigger": false,
|
|
69
|
+
"note": "expert Q&A is a non-goal — the help topics are the source of truth, not the wizard"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"query": "turn grind off",
|
|
73
|
+
"should_trigger": false,
|
|
74
|
+
"note": "a grind toggle alone is a non-goal — tlc harness grind off, no discovery pass"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"query": "mude o mode para paired",
|
|
78
|
+
"should_trigger": false,
|
|
79
|
+
"note": "a mode toggle alone is a non-goal — tlc harness mode paired"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"query": "run the tests and fix whatever fails",
|
|
83
|
+
"should_trigger": false,
|
|
84
|
+
"note": "ordinary engineering work; nothing about connecting a repo to the harness"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Capability catalog (load when running discovery)
|
|
2
|
+
|
|
3
|
+
Read this file during the Discovery step of harness-init. Present **one capability at a time**: name →
|
|
4
|
+
benefit → trade-off → default → ask yes/no → if yes, collect values.
|
|
5
|
+
|
|
6
|
+
Stay **stack-agnostic** (never assume Biome, Vitest, npm, Bun, pytest) and **provider-agnostic** (never
|
|
7
|
+
assume Cursor — check what Step 1 detected).
|
|
8
|
+
|
|
9
|
+
## Always ask once (before capabilities)
|
|
10
|
+
|
|
11
|
+
- `projectName` (optional string)
|
|
12
|
+
- `codePaths` (dirs that count as code for grind)
|
|
13
|
+
- starting `mode`: `paired` | `solo` | `focus` (default `solo`) — how much the agent surfaces; verification is
|
|
14
|
+
identical at all three. No other value is accepted
|
|
15
|
+
|
|
16
|
+
## Capabilities (all optional)
|
|
17
|
+
|
|
18
|
+
<!-- generated:capabilities -->
|
|
19
|
+
|
|
20
|
+
| # | Capability | Key | Default | Benefit | Trade-off | Extra asks if yes |
|
|
21
|
+
|---|------------|-----|---------|---------|-----------|-------------------|
|
|
22
|
+
| 1 | Grind (lint/test on stop) | `grind.enabled` | off | Re-checks lint/test after each completed turn and follow-ups until gates pass. | Uses turns; flaky commands thrash the agent. The gate's cost is paid once per attempt, up to maxLoops — a four-minute suite with maxLoops 5 can spend twenty minutes on tests alone. tlc harness obs report shows the runs and the total. | `lintCommand`, `testCommand`, `maxLoops` |
|
|
23
|
+
| 2 | Ship gate | `shipGate.enabled` | off | Blocks false done after an explicit HARNESS_SHIP_CLAIM when evidence is missing. | Needs a real evidenceDir workflow; free-English done is ignored. | `evidenceDir`, `runtimePathPrefixes`, `runtimePathExcludes`, `evidenceMaxAgeHours`, `claimWindowMinutes` |
|
|
24
|
+
| 3 | Empty-diff anti-ship | `shipGate.emptyDiffAntiShip` | off | Blocks a ship claim when the working tree has zero changes. | Annoys when a zero-diff claim is intentionally correct. | `requires shipGate enabled` |
|
|
25
|
+
| 4 | Comment gate (agent-added comments) | `comments.enabled` | off | Blocks the stop when this turn added comment lines, so narration never lands. Diff-scoped: comments you already committed are never flagged. | Three modes. declared lets the agent keep a comment by writing why:/hazard:/invariant:; resolvable adds the question the marker cannot answer — can a reader who was not in the session check this? — and catches change narration, dead plan or decision citations, and comments arguing with a reviewer; strict accepts none and asks you to write it instead. Each is stricter and interrupts more. | `mode: declared \| resolvable \| strict` |
|
|
26
|
+
| 5 | Duplication gate (agent-added copies) | `duplication.enabled` | off | Blocks the stop when the turn wrote six or more lines the project already has, naming both sites. Diff-scoped: what was already duplicated is not counted. | Reads every tracked file on stop, bounded at 2000 files and 8 MB, and says when a bound was reached. Comments, dependency declarations and pure data — object literals, type bodies, export lists — are excluded, so it reports duplicated logic and misses duplicated shape. | `minRun: lines a run must reach before two copies count (default 6)` |
|
|
27
|
+
| 6 | Supply-chain gate (dependencies this turn added) | `supplyChain.enabled` | off | Blocks the stop when a dependency added this turn is not recorded in a lockfile, or is specified as latest/*/no version. A dependency added in a turn runs on every later turn, in CI, and on every machine that installs the project. | Recognises a manifest by filename from one table, so an ecosystem the table does not carry is not covered. For a JSON manifest the declared dependency sections decide, so it reads the manifest as it stands — without that, a rename or a scripts entry reads as a dependency. It does not check advisories, licences or typosquats: those need the network on every stop. | — |
|
|
28
|
+
| 7 | Subagent allowlist | `subagents.enforceAllowlist` | off | Restricts Task/subagent models to a list you write, and blocks *-fast shapes by default. | The harness ships no model list, so switching this on without filling allowedModels enforces nothing and doctor reports it as a fault. You maintain the list as providers add models — including a value for `inherit` if you want subagents to take the parent's model. | `allowedModels`, `requireModel`, `blockMode` |
|
|
29
|
+
| 8 | Block parent Fast mode for Task spawns | `subagents.blockParentFast` | off | Denies Task/subagentStart while the parent chat is in Fast mode (sticky from hooks), closing the gap where Task slugs omit *-fast. | Needs parent model hooks (sessionStart/obs/stop); false denials if you intentionally run Fast parent with workers. | recommend **on** |
|
|
30
|
+
| 9 | Shell stall detection | `shell.stallDetection` | off | Blocks repeating the exact same shell command too many times. | Can false-positive on intentional retries. | `stallRepeatThreshold (default 3)` |
|
|
31
|
+
| 10 | Catastrophic shell ask | `shell.catastrophicAsk` | **on** | Asks before destructive shell commands (rm -rf, drop db, force push, …). | Extra prompts on risky commands. | recommend **on** |
|
|
32
|
+
| 11 | Lessons | `intelligence.lessons.enabled` | off | Records compact lessons on gate stagnation and reinjects them ranked under a char budget. A lesson can name the path or symbol that makes it true and is withheld once that stops resolving, can carry an end date, and is graded helped or neutral by the next run of the gate it was injected for. Three tiers: shipped core, a global tier read by every product on this machine, and this project's own. How the lessons reach the model is decided by the provider rather than configured: where a host does not deliver context returned from its session-start hook, a durable rules file is written instead. | Uses context tokens; not a second brain / chat memory. The grading is correlational, not causal — a gate passing after a lesson was injected does not prove the lesson caused it. Nothing is promoted between products automatically, so carrying a lesson to another product is an operator command. On a host that needs the durable view, a file is written into the repo (`.cursor/rules/harness-lessons.mdc`) and asked to be included on every request; `syncRulesFile: never` declines it. | `maxInjectSession`, `maxCharsSession`, `maxInjectRetry`, `maxCharsRetry`, `promoteHitCount`, `decayLambda`, `projectBoost`, `syncRulesFile`, `gardenOnSessionEnd`; recommend **on** |
|
|
33
|
+
| 12 | Budget continue | `intelligence.budgetContinue` | off | Pushes the agent to keep working under context pressure instead of wrapping up early. | Can delay clean stops. | `budgetContinueAfterLoops` |
|
|
34
|
+
| 13 | Gap feedback | `intelligence.gapFeedback` | **on** | Injects PREVIOUS_GAPS on gate failure so retries fix listed items. | Longer follow-ups. | — |
|
|
35
|
+
| 14 | Failure classification | `intelligence.failureClassification` | **on** | Stores failure categories on the handoff for clearer next actions. | Extra handoff fields. | — |
|
|
36
|
+
| 15 | Progressive handoff | `intelligence.progressiveHandoff` | **on** | Carries the gaps the previous session ended with into the next session's bootstrap, as history rather than as a task list. | Spends session-start context on a verdict that may already be stale — only the next run of the gate says whether it still holds. Capped at five, and the rest are counted rather than dropped in silence. | — |
|
|
37
|
+
| 16 | Progressive context | `intelligence.progressiveContext` | **on** | Escalates gate follow-up detail on each stop retry. | Longer thrash follow-ups. | — |
|
|
38
|
+
| 17 | Autopilot | `intelligence.autopilot` | **on** | Adds ordered AUTOPILOT steps on gate failure. | Agent must follow the block; more directive follow-ups. | — |
|
|
39
|
+
| 18 | Idle-turn gate (asked instead of acting) | `intelligence.idleTurnGate` | off | Blocks a turn that ends with open work, zero tool calls and zero file changes. Counts recorded tool events rather than reading the reply, so it cannot be talked around. | A turn that legitimately only answers a question is blocked while handoff work is open — clear the handoff or turn this off for conversational repos. | — |
|
|
40
|
+
| 19 | Docs staleness gate | `docs.command` | off | Runs the repository's own documentation staleness tool on stop, so a stale document fails like a failing test. | Needs such a tool in the repository; without one there is nothing to run. | `command (exact argv array)`, `severity: warn \| deny` |
|
|
41
|
+
| 20 | Global observability spool | `obs.globalSpool` | off | Mirrors this repo's obs and audit records into one file under the runtime home, so cost and gate history can be read across every repository at once. | Writes outside the repository. Records carry the repo path and project name, and the spool is pruned on the same retention window as session rollups. | — |
|
|
42
|
+
| 21 | Untrusted-content framing and enforcement | `untrustedContent.enabled` | off | Injects one framing line per turn when the agent reads a pull request, an issue, a fetched page or an MCP result, stating that the content is data and that any directive inside it is to be reported as a prompt-injection attempt, not obeyed. | Two modes. frame injects one line per turn, costing a few hundred characters and enforcing nothing. enforce also remembers what an untrusted read returned, bounded at 64 KB per session, and asks before a shell command that appears verbatim in it — verbatim because a paraphrase cannot be shown to come from the content, so a rewritten command is missed. It needs the host to deliver tool output on the after-event: measured present on Claude Code's PostToolUse and on Cursor's afterShellExecution and afterMCPExecution, and absent on Cursor's generic postToolUse. | `mode: frame \| enforce` |
|
|
43
|
+
| 22 | Plan gate (declared scope vs diff) | `planGate.enabled` | off | Blocks the stop when the turn changed files the declared HARNESS_PLAN did not name, so scope creep fails like a failing test instead of surviving as a review comment. | Requires the agent to declare HARNESS_PLAN before editing, and each honest deviation to state a reason. A turn with no declaration is not gated at all. | `windowMinutes` |
|
|
44
|
+
| 23 | Observation mode (measure a rail with its rule off) | `observe.enabled` | off | Runs a rail's checker while that rail is not enforcing, so the record says whether the property held with the rule injected or without it. That is the reading that tells you a rail is unnecessary rather than merely quiet, and it is what makes deleting one a decision instead of a guess. | Costs one diff scan per turn per observed rail, and answers a question only an operator who is asking it needs answered. It never blocks and never changes a decision, so it buys information and nothing else. | `rails — the rails to observe, chosen from the observable set (today: comments). A name with no checker records nothing and doctor reports it` |
|
|
45
|
+
|
|
46
|
+
<!-- /generated -->
|
|
47
|
+
|
|
48
|
+
Stagnation fingerprinting is always on when grind gates fail (no separate toggle) — mention when discussing
|
|
49
|
+
grind.
|
|
50
|
+
|
|
51
|
+
## Lessons subsection (capability 9)
|
|
52
|
+
|
|
53
|
+
If the user enables lessons, explain what runs automatically:
|
|
54
|
+
|
|
55
|
+
| Event | What happens |
|
|
56
|
+
|-------|--------------|
|
|
57
|
+
| Gate stagnation (same fingerprint ≥ 2) | Upsert `candidate` lesson in `.tlc/harness/state/lessons.json`, recording the session key |
|
|
58
|
+
| Stop retry / sessionStart | Inject ranked lessons under char budget, skipping any that are stale or out of window |
|
|
59
|
+
| Next run of the same gate | Grade the lessons injected for it: passed → `helped`, failed → `neutral` |
|
|
60
|
+
| sessionEnd | Promote / decay / quarantine when `gardenOnSessionEnd`, mark or clear staleness, prune expired |
|
|
61
|
+
| `syncRulesFile` | Rewrite the provider-native durable view — Cursor's `.cursor/rules/harness-lessons.mdc`, Claude's `CLAUDE.md` import line. `auto` writes it where the provider does not deliver context returned from its session-start hook, which today is Cursor |
|
|
62
|
+
|
|
63
|
+
Ask for lessons knobs (offer defaults):
|
|
64
|
+
|
|
65
|
+
- `maxInjectSession` (5), `maxCharsSession` (900)
|
|
66
|
+
- `maxInjectRetry` (8), `maxCharsRetry` (1400)
|
|
67
|
+
- `promoteHitCount` (2) — counted in **distinct sessions**, not raw recurrences
|
|
68
|
+
- `decayLambda` (0.02), `projectBoost` (1.5) — the boost favours this project over the global tier
|
|
69
|
+
- `syncRulesFile` (recommend **auto**, the default — the provider decides. Cursor drops `additional_context`
|
|
70
|
+
returned from `sessionStart`, acknowledged by Cursor as a race with the composer handle, so `auto` writes the
|
|
71
|
+
rules file there and not on Claude Code. Use `never` if the user does not want a file in `.cursor/rules/`, and
|
|
72
|
+
`always` on Claude Code to keep a `CLAUDE.md` pointer that survives a restart)
|
|
73
|
+
- `gardenOnSessionEnd` (recommend true)
|
|
74
|
+
|
|
75
|
+
**Nothing to configure for the tiers.** The global tier lives at `<runtime home>/state/lessons.json` and is read
|
|
76
|
+
automatically; it is written only when the operator passes `--global` or runs `lessons promote`. There is no
|
|
77
|
+
setting that makes promotion automatic, and that is deliberate — a lesson mined from one product's gate names that
|
|
78
|
+
product's tooling ([/decisions/ad-040.md](/decisions/ad-040.md)).
|
|
79
|
+
|
|
80
|
+
If the user asks how to record something they just learned, the commands are:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
tlc harness lessons add "<what to do differently>" [--gate <name>] [--ref path[:symbol]] [--until <iso>] [--global]
|
|
84
|
+
tlc harness lessons promote <id>
|
|
85
|
+
tlc harness lessons list
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Mention `--ref` when the lesson is about a file or a symbol: it is what makes the lesson retire itself instead of
|
|
89
|
+
outliving what it was about. Mention `--global` when the lesson would be true in a different repository.
|
|
90
|
+
|
|
91
|
+
Point deep docs to: `tlc harness help lessons` (load only if the user asks how decay/ranking works).
|
|
92
|
+
|
|
93
|
+
## Not configurable — state this once, before discovery
|
|
94
|
+
|
|
95
|
+
A floor tier runs ahead of every setting and reads no configuration, so nothing below can switch it off.
|
|
96
|
+
Tell the user plainly, because the first denial otherwise looks like a bug:
|
|
97
|
+
|
|
98
|
+
| Rule | Denies |
|
|
99
|
+
|------|--------|
|
|
100
|
+
| `outside-project-destruction` | A destructive command whose target resolves outside the project and outside the OS temp directory |
|
|
101
|
+
| `unprovable-destruction` | A destructive verb whose target cannot be resolved — a variable, a substitution, or a command built at runtime |
|
|
102
|
+
| `secret-access` | Reading a credential path into the transcript: `.env`, `~/.ssh`, `~/.aws`, `*.pem`, and similar. `.env.example` and friends are not secrets |
|
|
103
|
+
| `history-rewrite` | `git push --force`. `--force-with-lease` is allowed, since it refuses when the remote moved |
|
|
104
|
+
| `machine-control` | `shutdown`, `reboot`, `halt`, `poweroff` |
|
|
105
|
+
|
|
106
|
+
Harness policy and state are not agent-writable either: a gate an agent can edit is not a gate.
|
|
107
|
+
|
|
108
|
+
## Runtime note (tell user once)
|
|
109
|
+
|
|
110
|
+
Hooks call `~/.tlc/harness/bin/tlc-exec` — Bun-first when Bun is on PATH (~1 ms/hook), Node 24+ + `dist/`
|
|
111
|
+
otherwise (~27 ms/hook; see Step 1b). After global code changes: `tlc harness build`. Day-to-day use does
|
|
112
|
+
**not** require Bun. Install path is only `~/.tlc/harness` — never `~/.cursor/harness` or
|
|
113
|
+
`~/.cursor/agent-harness`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HarnessEventKind } from "./harness-event.ts";
|
|
2
|
+
|
|
3
|
+
export type ProviderCapabilities = {
|
|
4
|
+
enforcesHooks: boolean;
|
|
5
|
+
askSupportedOn: HarnessEventKind[];
|
|
6
|
+
sessionEnv: boolean;
|
|
7
|
+
nativeLoopCounter: boolean;
|
|
8
|
+
dedicatedShellEvent: boolean;
|
|
9
|
+
toolInputRewrite: boolean;
|
|
10
|
+
toolOutputRewrite: boolean;
|
|
11
|
+
contextAtToolBefore: boolean;
|
|
12
|
+
contextAtToolAfter: boolean;
|
|
13
|
+
contextAtStop: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether context returned from the session-start hook reaches the model. A host can accept the field, log it as
|
|
16
|
+
* merged, and still drop it, so this is not the same question as whether the field exists. A host declaring false
|
|
17
|
+
* needs the durable provider view to carry lessons; the adapter that declares it cites the evidence
|
|
18
|
+
* ([/decisions/ad-050.md](/decisions/ad-050.md)).
|
|
19
|
+
*/
|
|
20
|
+
sessionStartContextReliable: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether an after-event delivers what the tool returned. Measured as per-event rather than per-host: one host
|
|
23
|
+
* supplies it on its shell and MCP after-events and not on its generic tool after-event, so a rail that needs
|
|
24
|
+
* it asks the capability and then still checks the field ([/decisions/ad-077.md](/decisions/ad-077.md)).
|
|
25
|
+
*/
|
|
26
|
+
toolOutputAtAfter: boolean;
|
|
27
|
+
usageInPayload: boolean;
|
|
28
|
+
effortSignal: boolean;
|
|
29
|
+
thoughtEvent: boolean;
|
|
30
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* why: `rule` is the machine-readable form of a thing the codebase already wanted — the floor hand-writes
|
|
3
|
+
* `rule=policy-surface-write` into its reason prose, and anything wanting to attribute a decision had to parse
|
|
4
|
+
* English. Optional, because a decision is valid without one; a consumer that wants attribution and finds none
|
|
5
|
+
* reports it as unattributed rather than guessing.
|
|
6
|
+
*/
|
|
7
|
+
export type Decision =
|
|
8
|
+
| { kind: "abstain" }
|
|
9
|
+
| { kind: "allow" }
|
|
10
|
+
| { kind: "deny"; reason: string; userNote?: string; rule: string }
|
|
11
|
+
| { kind: "ask"; reason: string; userNote?: string; rule: string }
|
|
12
|
+
| { kind: "context"; text: string; env?: Record<string, string> }
|
|
13
|
+
| { kind: "continue"; text: string }
|
|
14
|
+
| { kind: "rewriteInput"; input: Record<string, unknown>; reason: string };
|
|
15
|
+
|
|
16
|
+
export type Rendered = {
|
|
17
|
+
// invariant: null means write nothing; a provider whose abstain is a literal "{}" sets it explicitly.
|
|
18
|
+
stdout: string | null;
|
|
19
|
+
// invariant: always 0 — exit code is never used as a policy channel.
|
|
20
|
+
exitCode: number;
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// why: never an enum — erasableSyntaxOnly forbids it.
|
|
2
|
+
export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
3
|
+
export type EffortLevel = (typeof EFFORT_LEVELS)[number];
|
|
4
|
+
|
|
5
|
+
export function effortOrdinal(level: EffortLevel): number {
|
|
6
|
+
return EFFORT_LEVELS.indexOf(level);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function compareEffort(a: EffortLevel, b: EffortLevel): number {
|
|
10
|
+
return effortOrdinal(a) - effortOrdinal(b);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isEffortLevel(value: unknown): value is EffortLevel {
|
|
14
|
+
return typeof value === "string" && (EFFORT_LEVELS as readonly string[]).includes(value);
|
|
15
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { EffortLevel } from "./effort.ts";
|
|
2
|
+
|
|
3
|
+
export type HarnessEventKind =
|
|
4
|
+
| "session.start"
|
|
5
|
+
| "session.end"
|
|
6
|
+
| "prompt.submit"
|
|
7
|
+
| "tool.before"
|
|
8
|
+
| "tool.after"
|
|
9
|
+
| "tool.failure"
|
|
10
|
+
| "shell.before"
|
|
11
|
+
| "shell.after"
|
|
12
|
+
| "mcp.before"
|
|
13
|
+
| "mcp.after"
|
|
14
|
+
| "read.before"
|
|
15
|
+
| "edit.after"
|
|
16
|
+
| "subagent.start"
|
|
17
|
+
| "subagent.stop"
|
|
18
|
+
| "stop"
|
|
19
|
+
| "compact.before"
|
|
20
|
+
| "response.after"
|
|
21
|
+
| "thought.after";
|
|
22
|
+
|
|
23
|
+
export const HARNESS_EVENT_KINDS: readonly HarnessEventKind[] = [
|
|
24
|
+
"session.start",
|
|
25
|
+
"session.end",
|
|
26
|
+
"prompt.submit",
|
|
27
|
+
"tool.before",
|
|
28
|
+
"tool.after",
|
|
29
|
+
"tool.failure",
|
|
30
|
+
"shell.before",
|
|
31
|
+
"shell.after",
|
|
32
|
+
"mcp.before",
|
|
33
|
+
"mcp.after",
|
|
34
|
+
"read.before",
|
|
35
|
+
"edit.after",
|
|
36
|
+
"subagent.start",
|
|
37
|
+
"subagent.stop",
|
|
38
|
+
"stop",
|
|
39
|
+
"compact.before",
|
|
40
|
+
"response.after",
|
|
41
|
+
"thought.after",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export type HarnessUsage = {
|
|
45
|
+
inputTokens?: number;
|
|
46
|
+
outputTokens?: number;
|
|
47
|
+
cachedInputTokens?: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type HarnessEvent = {
|
|
51
|
+
provider: string;
|
|
52
|
+
event: HarnessEventKind;
|
|
53
|
+
sessionKey: string;
|
|
54
|
+
projectDir: string;
|
|
55
|
+
// hazard: the `spawn*` pair describes the child of a spawn; the unprefixed fields describe the
|
|
56
|
+
// running agent. Conflating them clobbers sticky parent state.
|
|
57
|
+
model?: string;
|
|
58
|
+
spawnModel?: string;
|
|
59
|
+
effort?: EffortLevel;
|
|
60
|
+
text?: string;
|
|
61
|
+
toolName?: string;
|
|
62
|
+
toolInput?: Record<string, unknown>;
|
|
63
|
+
command?: string;
|
|
64
|
+
filePath?: string;
|
|
65
|
+
subagentType?: string;
|
|
66
|
+
spawnSubagentType?: string;
|
|
67
|
+
status?: "completed" | "aborted" | "error";
|
|
68
|
+
loopCount?: number;
|
|
69
|
+
/** Provider's own permission posture, when it exposes one. Absent means unknown, not permissive. */
|
|
70
|
+
permissionMode?: string;
|
|
71
|
+
contextUsagePercent?: number;
|
|
72
|
+
transcriptPath?: string;
|
|
73
|
+
usage?: HarnessUsage;
|
|
74
|
+
/**
|
|
75
|
+
* What the tool returned, when the host delivers it.
|
|
76
|
+
*
|
|
77
|
+
* why: measured across 69,034 real records before this existed. Every host that delivers it uses its own field
|
|
78
|
+
* name and its own type — three names and two types across the two adapters — so the translation belongs to
|
|
79
|
+
* each adapter and this is the one shape core reads ([/decisions/ad-077.md](/decisions/ad-077.md)).
|
|
80
|
+
*
|
|
81
|
+
* invariant: absent is absent. Two of the after-events measured carry nothing on 21,167 records, so a reader
|
|
82
|
+
* that assumed presence would be blind on the majority of one host's traffic.
|
|
83
|
+
*/
|
|
84
|
+
toolOutput?: string;
|
|
85
|
+
/** Adapter-only escape hatch — core must not read this. */
|
|
86
|
+
raw: Record<string, unknown>;
|
|
87
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EffortLevel } from "./effort.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hazard: this carried `allowedModels`, and an empty project list fell back to it — so a spawn could be refused by
|
|
5
|
+
* a list the operator never wrote, which had already gone stale. `blockedPatterns` stays because it is the opposite
|
|
6
|
+
* mechanism: it is concatenated with the project's rather than replacing it, and what it carries is the `-fast`
|
|
7
|
+
* denial the rail exists for ([/decisions/ad-053.md](/decisions/ad-053.md)).
|
|
8
|
+
*/
|
|
9
|
+
export type ProviderPolicyDefaults = {
|
|
10
|
+
blockedPatterns: string[];
|
|
11
|
+
minEffort: EffortLevel | null;
|
|
12
|
+
/** Tool names whose results carry content from outside the repository. */
|
|
13
|
+
untrustedTools: string[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type RuntimePaths = {
|
|
2
|
+
launcherPath: string;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type WiringEntry = {
|
|
6
|
+
hookEvent: string;
|
|
7
|
+
handler: string;
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
timeoutSeconds: number;
|
|
11
|
+
failClosed?: boolean;
|
|
12
|
+
matcher?: string;
|
|
13
|
+
loopLimit?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ProviderWiring = {
|
|
17
|
+
target: string;
|
|
18
|
+
strategy: "replace" | "merge";
|
|
19
|
+
entries: WiringEntry[];
|
|
20
|
+
};
|
|
Binary file
|