@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.
Files changed (300) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +84 -0
  3. package/NOTICE +9 -0
  4. package/README.md +480 -0
  5. package/bin/tlc +10 -0
  6. package/bin/tlc-build +80 -0
  7. package/bin/tlc-cli.ts +1509 -0
  8. package/bin/tlc-exec +10 -0
  9. package/bin/tlc-exec.cmd +4 -0
  10. package/bin/tlc-exec.d.mts +60 -0
  11. package/bin/tlc-exec.mjs +248 -0
  12. package/bin/tlc.cmd +4 -0
  13. package/bin/tlc.mjs +7 -0
  14. package/bin/write-user-hooks.d.mts +40 -0
  15. package/bin/write-user-hooks.mjs +123 -0
  16. package/capabilities/catalog.json +418 -0
  17. package/config.example.json +51 -0
  18. package/dist/compact-before.mjs +7839 -0
  19. package/dist/doctor.mjs +8263 -0
  20. package/dist/help-topic.mjs +82 -0
  21. package/dist/init-project.mjs +993 -0
  22. package/dist/install-runtime.mjs +969 -0
  23. package/dist/lessons-cli.mjs +7258 -0
  24. package/dist/obs-cli.mjs +7025 -0
  25. package/dist/price-lookup.mjs +221 -0
  26. package/dist/prompt-submit.mjs +7860 -0
  27. package/dist/refresh-model-prices.mjs +158 -0
  28. package/dist/response-after.mjs +7879 -0
  29. package/dist/run.mjs +7830 -0
  30. package/dist/session-end.mjs +7937 -0
  31. package/dist/session-start.mjs +8056 -0
  32. package/dist/shim.mjs +52 -0
  33. package/dist/stop.mjs +8494 -0
  34. package/dist/subagent-start.mjs +7861 -0
  35. package/dist/subagent-stop.mjs +7851 -0
  36. package/dist/support.mjs +7045 -0
  37. package/dist/tlc-cli.mjs +8063 -0
  38. package/dist/tool-after.mjs +8113 -0
  39. package/dist/tool-before.mjs +7985 -0
  40. package/dist/tool-failure.mjs +7850 -0
  41. package/dist/uninstall-runtime.mjs +1307 -0
  42. package/docs/architecture.md +270 -0
  43. package/docs/concepts.md +562 -0
  44. package/docs/coverage.md +55 -0
  45. package/docs/decisions/ad-001.md +32 -0
  46. package/docs/decisions/ad-002.md +51 -0
  47. package/docs/decisions/ad-003.md +30 -0
  48. package/docs/decisions/ad-004.md +37 -0
  49. package/docs/decisions/ad-005.md +36 -0
  50. package/docs/decisions/ad-006.md +49 -0
  51. package/docs/decisions/ad-007.md +36 -0
  52. package/docs/decisions/ad-008.md +54 -0
  53. package/docs/decisions/ad-009.md +61 -0
  54. package/docs/decisions/ad-010.md +45 -0
  55. package/docs/decisions/ad-011.md +59 -0
  56. package/docs/decisions/ad-012.md +71 -0
  57. package/docs/decisions/ad-013.md +87 -0
  58. package/docs/decisions/ad-014.md +56 -0
  59. package/docs/decisions/ad-015.md +33 -0
  60. package/docs/decisions/ad-016.md +98 -0
  61. package/docs/decisions/ad-017.md +65 -0
  62. package/docs/decisions/ad-018.md +77 -0
  63. package/docs/decisions/ad-019.md +75 -0
  64. package/docs/decisions/ad-020.md +88 -0
  65. package/docs/decisions/ad-021.md +57 -0
  66. package/docs/decisions/ad-022.md +120 -0
  67. package/docs/decisions/ad-023.md +87 -0
  68. package/docs/decisions/ad-024.md +85 -0
  69. package/docs/decisions/ad-025.md +92 -0
  70. package/docs/decisions/ad-026.md +131 -0
  71. package/docs/decisions/ad-027.md +90 -0
  72. package/docs/decisions/ad-028.md +90 -0
  73. package/docs/decisions/ad-029.md +73 -0
  74. package/docs/decisions/ad-030.md +97 -0
  75. package/docs/decisions/ad-031.md +94 -0
  76. package/docs/decisions/ad-032.md +83 -0
  77. package/docs/decisions/ad-033.md +89 -0
  78. package/docs/decisions/ad-034.md +86 -0
  79. package/docs/decisions/ad-035.md +86 -0
  80. package/docs/decisions/ad-036.md +68 -0
  81. package/docs/decisions/ad-037.md +47 -0
  82. package/docs/decisions/ad-038.md +52 -0
  83. package/docs/decisions/ad-039.md +69 -0
  84. package/docs/decisions/ad-040.md +89 -0
  85. package/docs/decisions/ad-041.md +98 -0
  86. package/docs/decisions/ad-042.md +82 -0
  87. package/docs/decisions/ad-043.md +79 -0
  88. package/docs/decisions/ad-044.md +61 -0
  89. package/docs/decisions/ad-045.md +94 -0
  90. package/docs/decisions/ad-046.md +111 -0
  91. package/docs/decisions/ad-047.md +96 -0
  92. package/docs/decisions/ad-048.md +85 -0
  93. package/docs/decisions/ad-049.md +66 -0
  94. package/docs/decisions/ad-050.md +94 -0
  95. package/docs/decisions/ad-051.md +69 -0
  96. package/docs/decisions/ad-052.md +69 -0
  97. package/docs/decisions/ad-053.md +78 -0
  98. package/docs/decisions/ad-054.md +98 -0
  99. package/docs/decisions/ad-055.md +74 -0
  100. package/docs/decisions/ad-056.md +85 -0
  101. package/docs/decisions/ad-057.md +68 -0
  102. package/docs/decisions/ad-058.md +97 -0
  103. package/docs/decisions/ad-059.md +82 -0
  104. package/docs/decisions/ad-060.md +75 -0
  105. package/docs/decisions/ad-061.md +68 -0
  106. package/docs/decisions/ad-062.md +72 -0
  107. package/docs/decisions/ad-063.md +84 -0
  108. package/docs/decisions/ad-064.md +79 -0
  109. package/docs/decisions/ad-065.md +81 -0
  110. package/docs/decisions/ad-066.md +111 -0
  111. package/docs/decisions/ad-067.md +64 -0
  112. package/docs/decisions/ad-068.md +79 -0
  113. package/docs/decisions/ad-069.md +74 -0
  114. package/docs/decisions/ad-070.md +86 -0
  115. package/docs/decisions/ad-071.md +93 -0
  116. package/docs/decisions/ad-072.md +82 -0
  117. package/docs/decisions/ad-073.md +102 -0
  118. package/docs/decisions/ad-074.md +91 -0
  119. package/docs/decisions/ad-075.md +79 -0
  120. package/docs/decisions/ad-076.md +102 -0
  121. package/docs/decisions/ad-077.md +94 -0
  122. package/docs/decisions/ad-078.md +84 -0
  123. package/docs/decisions/ad-079.md +73 -0
  124. package/docs/decisions/ad-080.md +86 -0
  125. package/docs/decisions/ad-081.md +70 -0
  126. package/docs/decisions/ad-082.md +79 -0
  127. package/docs/decisions/ad-083.md +88 -0
  128. package/docs/decisions/index.md +111 -0
  129. package/docs/diagnose.md +248 -0
  130. package/docs/index.md +52 -0
  131. package/docs/init.md +101 -0
  132. package/docs/lessons.md +294 -0
  133. package/docs/log.md +139 -0
  134. package/docs/measure.md +178 -0
  135. package/docs/providers/claude-code.md +118 -0
  136. package/docs/providers/cursor.md +110 -0
  137. package/docs/providers/index.md +71 -0
  138. package/docs/troubleshooting.md +138 -0
  139. package/model-aliases.json +12 -0
  140. package/model-prices.cursor.json +410 -0
  141. package/model-prices.json +1 -0
  142. package/package.json +57 -0
  143. package/skills/harness-init/SKILL.md +241 -0
  144. package/skills/harness-init/evals/trigger_evals.json +86 -0
  145. package/skills/harness-init/references/capabilities.md +113 -0
  146. package/src/contracts/capabilities.ts +30 -0
  147. package/src/contracts/decision.ts +21 -0
  148. package/src/contracts/effort.ts +15 -0
  149. package/src/contracts/harness-event.ts +87 -0
  150. package/src/contracts/index.ts +6 -0
  151. package/src/contracts/policy-defaults.ts +14 -0
  152. package/src/contracts/wiring.ts +20 -0
  153. package/src/core/attest/attest.service.ts +0 -0
  154. package/src/core/capability/capability.service.ts +62 -0
  155. package/src/core/capability/capability.store.ts +51 -0
  156. package/src/core/capability/capability.types.ts +56 -0
  157. package/src/core/comment-policy/comment-policy.service.ts +307 -0
  158. package/src/core/comment-policy/comment-policy.types.ts +6 -0
  159. package/src/core/comment-policy/comment-resolvability.ts +134 -0
  160. package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
  161. package/src/core/comment-policy/comment-syntax.store.ts +65 -0
  162. package/src/core/comment-policy/comment-syntax.types.ts +24 -0
  163. package/src/core/core.facade.ts +461 -0
  164. package/src/core/duplication/duplication.service.ts +219 -0
  165. package/src/core/floor/floor.catalog.ts +54 -0
  166. package/src/core/floor/floor.paths.ts +90 -0
  167. package/src/core/floor/floor.policy-surface.ts +311 -0
  168. package/src/core/floor/floor.service.ts +306 -0
  169. package/src/core/floor/floor.tokenize.ts +179 -0
  170. package/src/core/floor/floor.verb.ts +28 -0
  171. package/src/core/floor/index.ts +1 -0
  172. package/src/core/gate/gate.artifact.ts +157 -0
  173. package/src/core/gate/gate.command.ts +130 -0
  174. package/src/core/gate/gate.findings.ts +201 -0
  175. package/src/core/gate/gate.inputs.ts +0 -0
  176. package/src/core/gate/gate.lock.ts +283 -0
  177. package/src/core/gate/gate.service.ts +28 -0
  178. package/src/core/gate/gate.types.ts +67 -0
  179. package/src/core/handoff/handoff.service.ts +43 -0
  180. package/src/core/handoff/handoff.store.ts +59 -0
  181. package/src/core/handoff/handoff.types.ts +89 -0
  182. package/src/core/index.ts +52 -0
  183. package/src/core/integrity/state-seal.ts +81 -0
  184. package/src/core/lesson/lesson.authored.ts +96 -0
  185. package/src/core/lesson/lesson.credit.ts +51 -0
  186. package/src/core/lesson/lesson.garden.ts +283 -0
  187. package/src/core/lesson/lesson.link.ts +93 -0
  188. package/src/core/lesson/lesson.score.ts +61 -0
  189. package/src/core/lesson/lesson.select.ts +209 -0
  190. package/src/core/lesson/lesson.service.ts +110 -0
  191. package/src/core/lesson/lesson.store.ts +317 -0
  192. package/src/core/lesson/lesson.sync.ts +70 -0
  193. package/src/core/lesson/lesson.types.ts +92 -0
  194. package/src/core/lesson/lesson.validity.ts +44 -0
  195. package/src/core/observability/observability.report.ts +326 -0
  196. package/src/core/observability/observability.service.ts +328 -0
  197. package/src/core/observability/observability.store.ts +269 -0
  198. package/src/core/observability/observability.types.ts +193 -0
  199. package/src/core/observability/observability.why.ts +206 -0
  200. package/src/core/observe/observe.service.ts +76 -0
  201. package/src/core/plan/plan.detect.ts +47 -0
  202. package/src/core/plan/plan.service.ts +81 -0
  203. package/src/core/plan/plan.types.ts +25 -0
  204. package/src/core/policy/policy.defaults.ts +103 -0
  205. package/src/core/policy/policy.guard.ts +34 -0
  206. package/src/core/policy/policy.integrity.ts +238 -0
  207. package/src/core/policy/policy.loader.ts +118 -0
  208. package/src/core/policy/policy.operator.ts +82 -0
  209. package/src/core/policy/policy.posture.ts +79 -0
  210. package/src/core/policy/policy.rails.ts +45 -0
  211. package/src/core/policy/policy.types.ts +157 -0
  212. package/src/core/presence/presence.service.ts +111 -0
  213. package/src/core/presence/presence.store.ts +59 -0
  214. package/src/core/presence/presence.types.ts +9 -0
  215. package/src/core/release/release.decisions.ts +147 -0
  216. package/src/core/release/release.seen.ts +38 -0
  217. package/src/core/shell-policy/shell-policy.service.ts +215 -0
  218. package/src/core/shell-policy/shell-policy.stall.ts +57 -0
  219. package/src/core/shell-policy/shell-policy.types.ts +26 -0
  220. package/src/core/ship/ship.ledger.ts +76 -0
  221. package/src/core/ship/ship.service.ts +137 -0
  222. package/src/core/ship/ship.types.ts +19 -0
  223. package/src/core/stagnation/stagnation.resolution.ts +87 -0
  224. package/src/core/stagnation/stagnation.service.ts +20 -0
  225. package/src/core/stagnation/stagnation.store.ts +49 -0
  226. package/src/core/stagnation/stagnation.types.ts +12 -0
  227. package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
  228. package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
  229. package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
  230. package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
  231. package/src/core/supply-chain/supply-chain.service.ts +217 -0
  232. package/src/core/turn/turn.activity.ts +94 -0
  233. package/src/core/turn/turn.autopilot.ts +131 -0
  234. package/src/core/turn/turn.failure-signals.ts +193 -0
  235. package/src/core/turn/turn.loop-counter.ts +71 -0
  236. package/src/core/turn/turn.types.ts +14 -0
  237. package/src/core/untrusted/untrusted.detect.ts +53 -0
  238. package/src/core/untrusted/untrusted.recall.ts +87 -0
  239. package/src/core/untrusted/untrusted.service.ts +129 -0
  240. package/src/core/untrusted/untrusted.store.ts +75 -0
  241. package/src/core/untrusted/untrusted.types.ts +42 -0
  242. package/src/entrypoints/compact-before.ts +14 -0
  243. package/src/entrypoints/prompt-submit.ts +25 -0
  244. package/src/entrypoints/response-after.ts +60 -0
  245. package/src/entrypoints/run.ts +179 -0
  246. package/src/entrypoints/session-end.ts +58 -0
  247. package/src/entrypoints/session-start.ts +172 -0
  248. package/src/entrypoints/shim.ts +46 -0
  249. package/src/entrypoints/stop.ts +876 -0
  250. package/src/entrypoints/subagent-start.ts +16 -0
  251. package/src/entrypoints/subagent-stop.ts +32 -0
  252. package/src/entrypoints/support.ts +182 -0
  253. package/src/entrypoints/tool-after.ts +111 -0
  254. package/src/entrypoints/tool-before.ts +186 -0
  255. package/src/entrypoints/tool-failure.ts +26 -0
  256. package/src/platform/backoff.ts +49 -0
  257. package/src/platform/cli-output.ts +30 -0
  258. package/src/platform/env-scope.ts +22 -0
  259. package/src/platform/fs-atomic.ts +142 -0
  260. package/src/platform/fs-jsonl.ts +24 -0
  261. package/src/platform/git.ts +187 -0
  262. package/src/platform/paths.ts +80 -0
  263. package/src/platform/pricing.ts +229 -0
  264. package/src/platform/process.ts +64 -0
  265. package/src/platform/sanitize.ts +23 -0
  266. package/src/platform/screen.ts +46 -0
  267. package/src/platform/style.ts +103 -0
  268. package/src/providers/claude/claude.capabilities.ts +25 -0
  269. package/src/providers/claude/claude.detect.ts +14 -0
  270. package/src/providers/claude/claude.inbound.ts +252 -0
  271. package/src/providers/claude/claude.lessons-view.ts +33 -0
  272. package/src/providers/claude/claude.outbound.ts +65 -0
  273. package/src/providers/claude/claude.policy-defaults.ts +9 -0
  274. package/src/providers/claude/claude.transcript.ts +62 -0
  275. package/src/providers/claude/claude.wiring.ts +245 -0
  276. package/src/providers/claude/index.ts +17 -0
  277. package/src/providers/cursor/cursor.capabilities.ts +28 -0
  278. package/src/providers/cursor/cursor.detect.ts +14 -0
  279. package/src/providers/cursor/cursor.inbound.ts +203 -0
  280. package/src/providers/cursor/cursor.lessons-view.ts +30 -0
  281. package/src/providers/cursor/cursor.outbound.ts +43 -0
  282. package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
  283. package/src/providers/cursor/cursor.wiring.ts +214 -0
  284. package/src/providers/cursor/index.ts +17 -0
  285. package/src/providers/index.ts +9 -0
  286. package/src/providers/provider.degrade.ts +152 -0
  287. package/src/providers/provider.port.ts +20 -0
  288. package/src/providers/provider.registry.ts +28 -0
  289. package/tools/doctor.ts +631 -0
  290. package/tools/help-topic.ts +74 -0
  291. package/tools/init-project.ts +277 -0
  292. package/tools/install-runtime.ts +164 -0
  293. package/tools/lessons-cli.ts +474 -0
  294. package/tools/obs-cli.ts +175 -0
  295. package/tools/price-lookup.ts +57 -0
  296. package/tools/refresh-model-prices.ts +210 -0
  297. package/tools/test-env.mjs +28 -0
  298. package/tools/test-env.names.d.mts +1 -0
  299. package/tools/test-env.names.mjs +14 -0
  300. package/tools/uninstall-runtime.ts +463 -0
@@ -0,0 +1,562 @@
1
+ ---
2
+ type: Concept
3
+ title: "Concepts"
4
+ description: "The operator-facing concepts behind the harness: operator posture, grind, pause/resume, shipGate, subagent allowlist, comment policy, duplication, catastrophic shell, shell stall, the intelligence rails, observability planes, and cost estimates."
5
+ tags: [concepts, policy, posture, grind, shipgate, observability]
6
+ timestamp: "2026-07-29"
7
+ ---
8
+
9
+ # Concepts
10
+
11
+ ## operator posture
12
+
13
+ `mode`, or `tlc harness mode <paired|solo|focus>`. It sets how much the agent surfaces and what earns an
14
+ interruption — nothing else. Verification is identical at all three: the same evidence bar, the same gates, the
15
+ same done-criteria ([/decisions/ad-025.md](/decisions/ad-025.md)).
16
+
17
+ - **paired** — explains as it goes, and asks before any sizable move. A shell command that reaches the network, or
18
+ that can overwrite or remove a path that already exists, is asked about before it runs — so this posture is
19
+ enforced rather than merely stated
20
+ - **solo** (default) — works on its own. Three things reach you: an irreversible or destructive action, a real
21
+ dead-end after exhausting sources, and ambiguity that changes the outcome
22
+ - **focus** — only a destructive action or a real dead-end reaches you. Ambiguity is the agent's to settle,
23
+ taking the most reasonable reading and stating the assumption in one line
24
+
25
+ **Every posture also carries a deadline, not just a threshold.** An unclear goal belongs in the agent's first
26
+ actions; once the work is under way it takes the most reasonable reading and states the assumption instead of
27
+ asking, because a late question is measurably worse than a decision. `focus` admits exactly one early question —
28
+ a goal it cannot read before starting — so it means *ask early or not at all*, never simply never ask
29
+ ([/decisions/ad-026.md](/decisions/ad-026.md)).
30
+
31
+ Three tiers ask, and each names a different risk: a command that **leaves the machine**, one that **can overwrite
32
+ or remove** an existing path, and one that **changes who can reach** a path. That last one — `chmod`, `chown` — is
33
+ asked about even though it loses no data, because it is the only change that appears in no diff. An append is the
34
+ one shape that does not ask: content survives it, and asking about it would train you to clear the prompt without
35
+ reading, which is how the action that mattered gets waved through. `cp`, `mv` and `tee` stay in the asking tier,
36
+ because each can overwrite a destination and the harness cannot know whether that destination exists.
37
+
38
+ Precedence: the `harness-mode` state file, then a posture flag file, then `mode` in the config, then the default.
39
+ Any other value is refused rather than absorbed — `tlc harness status` and `tlc harness doctor` name the rejected
40
+ word and the posture running in its place.
41
+
42
+ Posture never switches a gate or a capability on. `focus` used to force grind on, which meant a surfacing
43
+ preference silently overrode a capability with its own switch and its own trade-off.
44
+
45
+ ## grind
46
+
47
+ `grind.enabled`. After each completed agent turn, run configured lint/test against **relevant** changed files:
48
+
49
+ - **lint** — only when files under `codePaths` changed
50
+ - **test** — when test files or `codePaths` files changed. Policy-only / non-code changes do **not** trigger the
51
+ test gate. Posture does not narrow this: the change that most needs testing is the one with no test file in
52
+ the diff ([/decisions/ad-025.md](/decisions/ad-025.md))
53
+
54
+ `grind.appendFiles` decides whether the changed files are appended to the lint/test argv. `auto` (default) appends
55
+ them, and refuses in three cases where appending cannot narrow the run:
56
+
57
+ | Shape | Why it does not narrow |
58
+ |-------|------------------------|
59
+ | a recipe runner — `just`, `make`, `task`, `mise`, `rake` | takes a target name; the first path reads as a second target |
60
+ | a package-manager script — `npm test`, `yarn test`, `bun run test` | the argument goes to somebody else's script, and whether it reaches the runner is not something the harness can know |
61
+ | a command that already globs — `eslint "src/**/*.ts"` | it walks the glob regardless, so appending widens rather than narrows |
62
+
63
+ `npx`, `bunx` and `dlx` are transparent: the tool named next is what decides, so `npx jest <file>` still narrows.
64
+ `always` and `never` override all of it.
65
+
66
+ **When `auto` cannot narrow, the gate runs in full on every attempt — up to `maxLoops`.** A four-minute suite with
67
+ three attempts is twelve minutes of tests, and that is the shape behind most reports of the harness being slow.
68
+ `tlc harness doctor` names any command in that state and says why; `tlc harness obs report` shows the runs and the
69
+ total ([/decisions/ad-033.md](/decisions/ad-033.md)).
70
+
71
+ Lint/test runs are serialized with a lock in the project state directory. A neighbour session holding it does not
72
+ block the turn: a recorded verdict whose inputs hash matches is reused and the lock is never taken, otherwise the
73
+ turn waits a bounded share of the stop hook's timeout, and if that expires the gate **defers** — the turn ends,
74
+ the handoff records `skipped`, and the holder is named ([/decisions/ad-073.md](/decisions/ad-073.md)).
75
+
76
+ A lock is reclaimed when it is older than 30 minutes, when it cannot be read, or when its owning process is gone
77
+ — the last one only on the host that wrote it, since a pid means nothing on another machine
78
+ ([/decisions/ad-024.md](/decisions/ad-024.md)).
79
+
80
+ Each lint/test invocation writes `.tlc/harness/state/last-gate.json` (`harness.gate.v1`) with exit code,
81
+ command, files, `outputTail`, and `findings`. Follow-up gaps and stagnation fingerprints use that artifact.
82
+ Optional: the child may write findings to the path in `HARNESS_GATE_REPORT` (JSON
83
+ `{ "findings": [{ "summary": "..." }] }`).
84
+
85
+ On failure, send a follow-up so the agent fixes (loop, capped). Identical failure fingerprints trigger a
86
+ stagnation follow-up. Trade-off: catches breakage early; burns turns if gates are flaky.
87
+
88
+ A gate whose command never ran — exit 127, or a runner that could not resolve the target — is reported as
89
+ `config`, not `verification`. The distinction matters: the verification follow-up tells the agent to fix the
90
+ findings without deleting tests, which on a malformed command sends it to edit healthy code.
91
+
92
+ ## pause / resume
93
+
94
+ `tlc harness pause` disables stop checks (grind + ship challenge). Use when exploring or mid-refactor.
95
+ `tlc harness resume` turns them back on.
96
+
97
+ Run both from your own terminal. Inside an agent session they are denied by the floor rule
98
+ `policy-surface-write`: policy is the operator's to change, and a stop check the agent can switch off is not a
99
+ stop check ([/decisions/ad-022.md](/decisions/ad-022.md)).
100
+
101
+ ## gate commands
102
+
103
+ `tlc harness gate test-command <cmd> [args...]` and `tlc harness gate lint-command <cmd> [args...]` set
104
+ `grind.testCommand` and `grind.lintCommand` in the project policy. This is the only supported way to change
105
+ those fields — editing `config.json` by hand is fine for you as the operator, but no agent route reaches it.
106
+
107
+ ```bash
108
+ tlc harness gate test-command node --test 'src/**/__test__/*.test.ts'
109
+ tlc harness gate lint-command npx biome check .
110
+ ```
111
+
112
+ Each refuses without writing when the argv is empty, when the first element does not resolve on `PATH` (a gate
113
+ command that cannot run is a config fault, AD-021), or when stdin is not a terminal.
114
+
115
+ ## policy integrity
116
+
117
+ Every source the policy loader reads — the project config, the runtime config, `harness-mode` and the flag
118
+ files — is hashed when a session starts. If one changes during that session without a `tlc harness` command
119
+ behind it, the next tool call is refused and the changed path is named. The check has no config switch, for
120
+ the same reason the floor does not: a detector the detected change can disable is not a detector.
121
+
122
+ Editing the config between sessions never triggers it. Baselines are per session, so concurrent sessions do
123
+ not interfere, and every `tlc harness` mutation re-records them.
124
+
125
+ ## shipGate
126
+
127
+ `shipGate.enabled`. Ship challenges fire **only** after an explicit protocol line in the agent response:
128
+
129
+ ```text
130
+ HARNESS_SHIP_CLAIM: <one-line summary>
131
+ ```
132
+
133
+ Prose without that marker does not count as a ship claim.
134
+
135
+ When a claim is recent (`claimWindowMinutes`, default 10), changed files touch `runtimePathPrefixes` after
136
+ `runtimePathExcludes`, and there is no recent PASS under `evidenceDir/*/90-verdict.txt`, stop follows up
137
+ with BLOCKED.
138
+
139
+ Outcomes append to `.tlc/harness/state/ship-ledger.jsonl` (`claim` / `challenge` / `pass`), each row tagged
140
+ with the resolved `provider`.
141
+
142
+ Default excludes: `.tlc/`, `.cursor/`, `.claude/`, `**/node_modules/`, `**/.git/`.
143
+
144
+ ## emptyDiffAntiShip
145
+
146
+ `shipGate.emptyDiffAntiShip`. When enabled, a recent `HARNESS_SHIP_CLAIM` with zero changed files is blocked. Omit the claim line when an
147
+ empty diff is intentional.
148
+
149
+ ## subagent allowlist
150
+
151
+ `subagents.enforceAllowlist`. Task/subagent models must be on `subagents.allowedModels`, which is **yours** — no
152
+ provider ships a catalogue, because one the harness invents goes stale and then refuses a spawn by a list that
153
+ appears nowhere in the project ([/decisions/ad-053.md](/decisions/ad-053.md)). An empty list enforces nothing and
154
+ `doctor` reports the combination as a fault. `*-fast`-shaped models are blocked separately, by patterns that are
155
+ added to yours rather than replacing them. `inherit` is a value the list may contain. Trade-off: cost/quality
156
+ control; you update the list when a provider adds models you want.
157
+
158
+ ## Block parent Fast
159
+
160
+ `subagents.blockParentFast` (default off) denies a Task/subagent spawn while the sticky parent model is a
161
+ "fast" variant, even when the spawn's own `model` string looks allowlisted. See
162
+ [/decisions/ad-001.md](/decisions/ad-001.md).
163
+
164
+ ## comment policy
165
+
166
+ `comments.enabled`, with `comments.mode` of `declared` or `strict`. Blocks the stop when the turn added
167
+ comment lines, so narration never lands. Diff-scoped against the sha the turn started from, not `HEAD`:
168
+ a turn that commits its own work moves `HEAD` past the very lines being judged, which is how the gate
169
+ missed every comment in a committing turn ([/decisions/ad-058.md](/decisions/ad-058.md)). Comments already
170
+ committed before the turn are never flagged. `declared` keeps a comment that states `why:`, `hazard:` or `invariant:`; `resolvable` is `declared` plus the
171
+ question a marker cannot answer — can a reader at HEAD, with no transcript of the session, resolve every
172
+ reference and check every claim? It refuses change narration, citations only the session could see, pull-request
173
+ vantage, comments arguing their own correctness, and control-flow narration, and asks for a restatement rather
174
+ than a deletion ([/decisions/ad-070.md](/decisions/ad-070.md)); `strict` accepts none and
175
+ asks the operator to write it. Tool directives (`biome-ignore`, `@ts-`, `noqa`, `shellcheck`, shebang) are
176
+ exempt in both modes.
177
+
178
+ ## supply chain
179
+
180
+ `supplyChain.enabled`, off by default. Blocks the stop when this turn added a dependency and left one of two
181
+ things undone: the paired lockfile did not move, or the specifier names no version — `latest`, `*`, `x`, or blank.
182
+ Diff-scoped against the sha the turn started from, so a manifest already unlocked before the turn is not this
183
+ turn's to answer for.
184
+
185
+ A manifest is recognised by filename from one table that pairs each with its lockfile, and accepts any lockfile
186
+ the ecosystem uses — a project on pnpm has locked as firmly as one on npm. A filename the table does not carry
187
+ produces no findings.
188
+
189
+ For a JSON manifest the declared dependency sections decide what counts, so the manifest is read as it stands.
190
+ Without that step a rename reads as a dependency: calibrated against this repository's own history, the textual
191
+ shape alone reported `"name": "harness-toolkit"` from a rename commit, and would report every `scripts` entry the
192
+ same way.
193
+
194
+ It does not check advisories, licences or typosquats. Each needs the network on every stop, and `npm audit` is
195
+ already a gate command an operator can configure.
196
+
197
+ ## duplication
198
+
199
+ `duplication.enabled`, off by default, with `duplication.minRun` (default 6). Blocks the stop when this turn
200
+ added a run of that many lines or more that already exists somewhere else in the project, naming both sites.
201
+ Diff-scoped against the sha the turn started from, like the comment gate: a run that was already duplicated
202
+ before the turn is not this turn's to answer for.
203
+
204
+ Three things are excluded, each because measuring said so. **Comments**, so two identical licence headers are
205
+ not a duplicated implementation. **Dependency declarations** — `import`, `require`, `use`, `#include` and their
206
+ siblings — because they are identical in every file that needs the same thing. **Pure data**: a run has to carry
207
+ operations, a call, an assignment, a branch or a return, in the majority of its lines, so a repeated object
208
+ literal, type body or export list does not count. Repeated shape is what those are for.
209
+
210
+ The comparison ignores indentation and a trailing comma, and nothing else. Renaming an identifier makes it a
211
+ different run on purpose — a rule that matched through renames would report every similarly shaped function.
212
+
213
+ It reads every tracked file on stop, bounded at 2000 files and 8 MB, and says when a bound was reached: a scan
214
+ that silently covered half a project reads as a clean answer.
215
+
216
+ ## docs staleness gate
217
+
218
+ `docs.command`, optional and off by default. It is the repository's own staleness tool — `drift check`,
219
+ `oasdiff`, `ast-grep scan`, or a script the repo already has — run on stop through the same lock, artifact and
220
+ failure path as the lint and test gates.
221
+
222
+ `docs.severity` is `warn` or `deny`. `warn` injects the tool's output and lets the turn end; `deny` blocks and
223
+ goes through the standard gate failure path, which brings stagnation fingerprinting and progressive follow-up
224
+ with it.
225
+
226
+ The harness does not infer staleness from paths. A source-glob to docs-glob map was measured reporting on
227
+ 82–100% of commits, which detects nothing, so a repository without a real tool gets no gate rather than a
228
+ noisy one. The tool also owns its own escape hatch, so there is no harness-level skip token.
229
+
230
+ ## catastrophic shell
231
+
232
+ `shell.catastrophicAsk`. The shell-before hook asks before commands that can destroy data outside the workspace. Happy-path allows
233
+ are not signal events.
234
+
235
+ ## shell stall
236
+
237
+ `shell.stallDetection`. When enabled, repeating the same shell command N times (`stallRepeatThreshold`) is denied with a
238
+ change-approach follow-up. Trade-off: stops loops; can block intentional retries.
239
+
240
+ ## intelligence (rails)
241
+
242
+ | Key | Effect |
243
+ |------|--------|
244
+ | `intelligence.gapFeedback` | Gate fails include structured PREVIOUS_GAPS + NEXT suggestion |
245
+ | `intelligence.failureClassification` | Handoff stores category (verification, ship-evidence, stagnation, …) |
246
+ | `intelligence.progressiveHandoff` | sessionStart reads the gaps the previous session ended with back out of the handoff, capped at five and counting the rest. Phrased as history: only the next run of the gate says whether they still hold |
247
+ | `intelligence.progressiveContext` | Each stop retry escalates context (merge prior gaps, more gate output, stronger "don't repeat") |
248
+ | `intelligence.autopilot` | Runtime emits ordered AUTOPILOT steps + NEXT_ACTION (not LLM-invented plan) |
249
+ | `intelligence.lessons.enabled` | Durable gate lessons with decay/promote; inject at sessionStart + stop retry (see [/lessons.md](/lessons.md)) |
250
+ | `intelligence.budgetContinue` | Under loop/context pressure **and** unfinished handoff work, follow-up says keep working — do not summarize |
251
+ | `intelligence.idleTurnGate` | Blocks a turn that ends with open handoff work, zero recorded tool calls and zero file changes. It counts events the harness recorded rather than reading the reply, so no wording satisfies it |
252
+
253
+ ## plan gate
254
+
255
+ `planGate.enabled` (off by default), with `planGate.windowMinutes` (default 120). The turn declares the paths
256
+ it intends to touch through a protocol line, exactly as the ship gate works — free-form prose about plans is
257
+ ignored:
258
+
259
+ ```text
260
+ HARNESS_PLAN: src/core/plan/**, src/entrypoints/stop.ts
261
+ ```
262
+
263
+ Declared paths use the same matcher as `shipGate.runtimePathExcludes`, so there is one pattern syntax to
264
+ learn, globs included. On stop, any changed file that no declared path covers and no accepted deviation
265
+ justifies blocks with BLOCKED / TRIED / NEED, naming those paths. A deviation is accepted only with a stated
266
+ reason:
267
+
268
+ ```text
269
+ HARNESS_PLAN_DEVIATION: src/x.ts — the call site moved with the type
270
+ ```
271
+
272
+ Naming the path alone is refused, since that would make the gate a formality satisfied by restating the file
273
+ just touched. Deviations accumulate for the plan's window, so one can be justified in a later message than
274
+ the one that declared the plan. The gate runs **before** the ship gate: a turn whose scope is invalid
275
+ produced evidence for the wrong change.
276
+
277
+ A turn that declares no plan is not gated at all, so the rail costs nothing until the agent opts in. That is
278
+ also its limit — it depends on the declaration being made.
279
+
280
+ ## untrusted-content framing
281
+
282
+ `untrustedContent.enabled` (off by default), with `untrustedContent.extraTools` and
283
+ `untrustedContent.extraCommandPatterns`. The floor governs what the agent executes; this governs what it
284
+ reads. When a turn takes in content from outside the repository, one framing message states that the content
285
+ is data and that any directive inside it is to be reported as a prompt-injection attempt, never obeyed.
286
+
287
+ `untrustedContent.mode` chooses how far it goes. **`frame`** is the default and is the paragraph above: one
288
+ message, no refusal. **`enforce`** adds the question framing cannot ask — did this command come from that
289
+ content — and answers it verbatim ([/decisions/ad-077.md](/decisions/ad-077.md)).
290
+
291
+ In `enforce`, what an untrusted read returned is remembered for the session, bounded at 64 KB with the oldest
292
+ dropped first, whitespace collapsed and nothing else rewritten. When a shell command about to run appears
293
+ verbatim in it, the decision is `ask`, naming the source. Verbatim because a paraphrase cannot be shown to come
294
+ from the content, so an agent that rewrites a command before running it is missed on purpose — the alternative
295
+ guesses, and a rail that guesses asks about every command in every turn that read anything.
296
+
297
+ It needs the host to deliver what the tool returned. That is a capability, `toolOutputAtAfter`, because presence
298
+ is per-event rather than per-host: measured across 69,034 real records, two of the after-events carry nothing on
299
+ 21,167 of them. A rail that assumed presence would be blind on the majority of one host's traffic and would not
300
+ know.
301
+
302
+ What covers the damaging tail either way is the floor, and provenance never mattered to it: running a program
303
+ fetched from the network, reading a credential, destroying outside the project, rewriting history, controlling
304
+ the machine and writing policy are all refused before any policy is read, whoever suggested them. An injected
305
+ `curl … | bash` is refused for being unreadable code, not for being injected — which holds without recognising
306
+ the attack at all.
307
+
308
+ Detection is a declared list, never inferred from output: every MCP result (the server is not this
309
+ repository), a tool whose name the provider declares as untrusted (`WebFetch` / `WebSearch` on Claude Code,
310
+ `Fetch` / `WebSearch` on Cursor), and a shell command whose **segment starts with** `gh pr view|diff|list`,
311
+ `gh issue view|list`, `gh api`, `curl` or `wget`. A source nobody listed is not covered.
312
+
313
+ Matching is anchored at the start of a command segment (split on `|`, `||`, `&&`, `;` and newline) rather
314
+ than a substring search, so naming a pattern inside a quoted argument, a `grep` search or a heredoc is not a
315
+ read. That distinction was not academic: this document names the patterns, and writing it tripped the rail
316
+ when the match was a substring.
317
+
318
+ Injected at most once per turn, keyed on a marker cleared at the prompt boundary, so it cannot spend the
319
+ context budget it exists to protect. When the provider cannot carry context on that event the decision
320
+ abstains rather than rendering into a field the provider ignores.
321
+
322
+ ## global observability spool
323
+
324
+ `obs.globalSpool` (off by default). Every record already written under the project state directory is also
325
+ appended to one file under the runtime home, wrapped with the repository path and project name, so cost and
326
+ gate history can be read across every repository at once.
327
+
328
+ Writing outside the repository is the one thing an operator cannot undo by editing project policy, which is
329
+ why it is opt-in. Redaction is inherited rather than reimplemented — records are redacted before the store
330
+ sees them. Writes are best-effort: an unwritable runtime home degrades to project-only recording without
331
+ changing the decision returned to the provider. The spool is pruned on the same retention window as session
332
+ rollups, and `tlc harness obs prune` reports how many records it dropped.
333
+
334
+ ## observability planes
335
+
336
+ | Plane | File | Default |
337
+ |-------|------|---------|
338
+ | Signal | `.tlc/harness/state/obs.jsonl` | ON — lifecycle, fails, denials, gates, cost alerts |
339
+ | Debug | `.tlc/harness/state/debug.jsonl` | OFF — happy-path tool/shell noise |
340
+ | Audit | `.tlc/harness/state/audit.jsonl` | ON — verbose per-event record, restored per [/decisions/ad-016.md](/decisions/ad-016.md) item 7 |
341
+
342
+ Which plane an event lands on is fixed by its kind. What a project can tune is the `obs` block:
343
+
344
+ | Key | Effect |
345
+ |-----|--------|
346
+ | `obs.globalSpool` | Mirror every record into the cross-repository spool (see above) |
347
+ | `obs.includePayloads` | Keep tool payloads in `attrs` instead of stripping them |
348
+ | `obs.maxAttrChars` | Truncation budget for `attrs` on every recorded event |
349
+ | `obs.sessionCostAlertUsd` | Threshold for the session cost alert; `null` disables it |
350
+ | `obs.retentionDays` | Window used by `tlc harness obs prune`, for rollups and the spool |
351
+
352
+ `debugEnabled` is deliberately **not** a project field: every event that resolves to debug level is emitted
353
+ with the audit configuration, which forces debug on so the audit trail persists
354
+ ([/decisions/ad-016.md](/decisions/ad-016.md) item 7). There would be nothing for a project to switch.
355
+
356
+ An `"observability": { … }` block is not read at all — it never was. It was removed rather than honoured,
357
+ per [/decisions/ad-003.md](/decisions/ad-003.md). Full detail: [/measure.md](/measure.md).
358
+
359
+ ### interruption rate
360
+
361
+ Every shell decision is recorded with the permission it produced, the active posture and the rule responsible —
362
+ `shell-posture-paired`, `shell-catastrophic` or `shell-stall`. An allow resolves to debug level and is dropped by
363
+ default, so only the interruptions reach disk. The session report shows them attributed by rule, because "seven
364
+ interruptions" names no switch while "six from the posture, one from the catastrophic rule" does. That is how you
365
+ calibrate the posture from your own sessions instead of trusting a threshold someone else chose
366
+ ([/decisions/ad-026.md](/decisions/ad-026.md)).
367
+
368
+ What this is **not**: the harness records the decisions it made. It never learns your answer, and it cannot know
369
+ whether a question it did not ask would have helped. So it reports a rate and its attribution — never a precision,
370
+ a recall, or an accuracy of asking. Floor denials are outside it too: `rm -rf /` never reaches a policy layer.
371
+
372
+ ## cost estimates
373
+
374
+ USD estimates use on-disk catalogs, resolved provider-first: local overrides → this provider's own catalog
375
+ → LiteLLM → `null`.
376
+
377
+ ```bash
378
+ tlc harness prices refresh
379
+ tlc harness prices refresh cursor
380
+ tlc harness prices refresh litellm
381
+ tlc harness prices lookup <model-id> [provider]
382
+ ```
383
+
384
+ Details: `tlc harness help prices` (or [/measure.md](/measure.md)).
385
+
386
+ ## capability catalog
387
+
388
+ Optional features are chosen during the harness-init wizard (see [/init.md](/init.md)) and stored per
389
+ project. `tlc harness doctor` WARNs without failing for off/default opt-ins. Enable via harness-init or by
390
+ editing `.tlc/harness/config.json` — never auto-enabled.
391
+
392
+ ## observation mode
393
+
394
+ `observe.enabled` plus `observe.rails`. Runs a rail's checker while that rail is **not** enforcing, and records
395
+ the reading without touching the turn.
396
+
397
+ It exists to answer the one question a firing rate cannot: *was the rule ever needed?* A rail that never fires
398
+ while its prose is injected is either working or unnecessary, and the count alone cannot tell you which. Run the
399
+ checker with the prose absent and the two separate: if the property holds anyway, the model was already doing it
400
+ and the rule is paying for injected context and returning nothing.
401
+
402
+ | Reading | What it means |
403
+ |---------|---------------|
404
+ | `held-without-prose` | The model does this on its own. The rule is a candidate for deletion. |
405
+ | `held-with-prose` | Ambiguous by construction — this is why observation runs with enforcement off. |
406
+ | `violated-without-prose` | The rule is doing real work. Keep it. |
407
+ | `violated-with-prose` | The prose is not working. Move the rule to a gate, or accept the rate. |
408
+
409
+ Observation never returns a decision and never blocks — a measurement that can change what it measures is not a
410
+ measurement. An enforcing rail is not observed, because it already records through its own path and counting it
411
+ twice would double the readings. Records land under their own obs kind rather than sharing the refusal kind, so
412
+ the denial counters stay honest ([/decisions/ad-027.md](/decisions/ad-027.md)).
413
+
414
+ Only `comments` is observable today. A name with no checker records nothing, and `doctor` says so by name rather
415
+ than leaving you to read the silence as "the property always holds" — which is the worst available misreading of a
416
+ measurement rail ([/decisions/ad-029.md](/decisions/ad-029.md)).
417
+
418
+ This is possible because the checker and the instruction are separate things here. In a system where the rule *is*
419
+ the mechanism there is nothing to hold apart, and the only alternative — running the same task repeatedly with and
420
+ without the rule — needs task repetition that real work does not offer.
421
+
422
+ ## resolution history
423
+
424
+ When a gate passes after having failed, the harness records the files that changed between those two states against
425
+ the failure's fingerprint. If that exact failure returns, the follow-up carries one line naming them.
426
+
427
+ It is offered as **history, never as instruction**: past tense, and explicit that it is not a list to edit. A
428
+ previous resolution is evidence, and evidence is what a plan may name — but the same list phrased as an order would
429
+ send an agent to edit files that may be irrelevant this time, which is the harm AD-021 and AD-024 each removed
430
+ through a different door ([/decisions/ad-028.md](/decisions/ad-028.md)).
431
+
432
+ Bounded at 200 resolutions and 8 files each, pruning the oldest, because the store is read on the failure path.
433
+ Scoped to this repository: a fix that worked in another codebase is exactly the kind of advice that reads plausible
434
+ and is wrong.
435
+
436
+ ## attestation
437
+
438
+ `tlc harness attest [--json]`. Every session appends one hash-chained record: which policy was in force, whether it
439
+ changed mid-session without a harness command, which rails were active, refusals by rule, and gate outcomes.
440
+
441
+ That is the artifact a reviewer needs in order to trust agent-written code, and it is the part governance-as-prompting
442
+ never produces. Verification reports the index at which the chain broke, so a tampered record sends you to one line
443
+ rather than to the whole file. A missing file is an empty valid chain, not a broken one.
444
+
445
+ Two things it deliberately does **not** claim. It is chained, not signed — that detects a rewritten or removed
446
+ record and does not prove authorship, because a key would mean key management. And every field is something the
447
+ harness observed: there is no assertion that the code is correct, that anyone reviewed it, or that a human approved
448
+ anything. An attestation implying those would be worse than none, because a reviewer would stop looking
449
+ ([/decisions/ad-028.md](/decisions/ad-028.md)).
450
+
451
+ ## accepting a policy edit you made
452
+
453
+ If you edit `config.json`, a flag file or the mode file while a session is live, the next acting tool call in that
454
+ session is refused and the changed path is named. That is the integrity check working: a mid-session policy change
455
+ with no harness command behind it is what it exists to catch.
456
+
457
+ ```bash
458
+ tlc harness policy # list what changed, change nothing
459
+ tlc harness policy accept <path>... # accept exactly those paths
460
+ ```
461
+
462
+ Four things keep that second command out of an agent's reach, and no single one carries the weight
463
+ ([/decisions/ad-030.md](/decisions/ad-030.md)):
464
+
465
+ - the floor refuses `tlc harness policy` from inside any agent session, with no config switch
466
+ - it refuses without an interactive terminal, so a script cannot reach it either
467
+ - you name each path, so accepting is an act rather than a keystroke
468
+ - acceptance is per source, so anything you leave out keeps blocking
469
+
470
+ Accepting records the hash as it is now. A later change to the same file diverges again — there is deliberately no
471
+ way to say "stop watching this". And the acceptance is recorded rather than erased, so a reviewer reading the
472
+ session's attestation sees that policy moved and was accepted, instead of seeing nothing.
473
+
474
+ `status` and `doctor` never clear a divergence as a side effect of looking at it. `doctor` reports one when it
475
+ exists, naming the paths and the command.
476
+
477
+ ## updating
478
+
479
+ ```bash
480
+ tlc harness version # which revision you run, and what this project last saw
481
+ tlc harness update --check # what an update would pull. Fetches; never merges
482
+ tlc harness update # pull, relink, rebuild, announce what landed, then doctor
483
+ ```
484
+
485
+ There is no changelog file and no version number, on purpose. The version is the runtime's git revision and its
486
+ date, because that is what `update` actually moves — a hand-maintained number drifts, and this one said `0.1.0` for
487
+ the project's whole life. A semantic version is a promise about compatibility that AD-003 declines to make.
488
+
489
+ The changelog is `docs/decisions/`. A decision carries a `migration` note **only when `doctor` cannot detect the
490
+ condition for you** — `update` runs `doctor` at the end, so a note that says "run doctor" is noise, and an alarm that
491
+ fires on every update is one you learn to scroll past ([/decisions/ad-034.md](/decisions/ad-034.md)). Today exactly one
492
+ decision carries a note: the ship gate's evidence ordering, which shows up as a blocked stop and nothing can see in
493
+ advance. A project updating for the first time records where it stands
494
+ and announces nothing, because thirty entries at once is the same as no message
495
+ ([/decisions/ad-031.md](/decisions/ad-031.md)).
496
+
497
+ `doctor` is the net underneath: a posture that fell back, an observed rail with no checker, a policy that changed out
498
+ of band. If a migration note was forgotten, that is where it surfaces.
499
+
500
+ If the fast-forward fails, the runtime checkout has commits upstream does not. The message names both ways out —
501
+ reset to upstream, or re-run the installer — and runs neither, because the first one throws work away.
502
+
503
+ ## wiring health
504
+
505
+ `tlc harness doctor` checks each provider's hooks, and for the replace-strategy target it checks them **per event**:
506
+ our launcher named in the command, that file present on disk, and a handler after it. A declared event with no
507
+ harness entry is reported too — that is the case a marker cannot see.
508
+
509
+ The marker keeps its own job, unchanged: it answers *is this file ours*, which is what decides whether `update` may
510
+ overwrite it. Whether the hooks work is a different question, and conflating the two is what let a hook that could
511
+ not run report as healthy ([/decisions/ad-032.md](/decisions/ad-032.md)).
512
+
513
+ A hook belonging to another tool in the same file is never reported. That is deliberate — flagging someone else's
514
+ entry would train you to skip the check, and then it would miss ours.
515
+
516
+ When something is wrong the detail names the event and the reason, bounded to three with a count of the rest:
517
+
518
+ ```
519
+ WARN cursor wiring — detected but not wired — preToolUse: no handler after the
520
+ script: `node /path/tlc-exec.mjs` — run: tlc harness update (~/.cursor/hooks.json)
521
+ ```
522
+
523
+ ## writing a lesson yourself
524
+
525
+ The store used to have one producer: the same gate failing twice. So anything learned another way — a review, an
526
+ incident, a pattern you noticed across several changes — could not enter the one channel built to carry it back to the
527
+ next turn.
528
+
529
+ ```bash
530
+ tlc harness lessons add "Grep for a producer before calling a new field done." --tokens producer,dead
531
+ tlc harness lessons add "Re-run the suite after the last edit." --gate test --avoid "citing a stale verdict"
532
+ ```
533
+
534
+ It is recorded as `source: manual` and active immediately — a candidate exists because the automatic producer is
535
+ guessing from gate output, and an author is not. The id is a hash of the instruction, so rewriting the same lesson
536
+ updates it instead of adding a near-duplicate. A lesson written from inside an agent session says so in its category,
537
+ so you can tell the two apart in `tlc harness lessons list`.
538
+
539
+ **Say what makes it true, and it retires itself.** `--ref path[:symbol]` names the thing the lesson is about; when
540
+ that stops resolving the lesson is withheld instead of sending the next turn after a file that no longer exists.
541
+ `--until <iso>` gives it an end date. Both are optional and a lesson about conduct needs neither.
542
+
543
+ ```bash
544
+ tlc harness lessons add "Run tools/check-dist-fresh.ts before the commit, never chained with &&." \
545
+ --ref tools/check-dist-fresh.ts --gate test
546
+ tlc harness lessons add "Pin the formatter until the toolchain moves." --until 2026-12-01T00:00:00Z
547
+ ```
548
+
549
+ **Decide who should read it.** A lesson about this repository stays here; one about engineering belongs to every
550
+ product you work in. `--global` writes it to the machine tier, and `lessons promote <id>` moves an existing project
551
+ lesson up. Nothing is promoted automatically — a lesson mined from one product's gate names that product's tooling
552
+ ([/decisions/ad-040.md](/decisions/ad-040.md)).
553
+
554
+ ```bash
555
+ tlc harness lessons add "Run the gate itself, never an approximation of its steps." --global
556
+ tlc harness lessons promote project:test:9f2c1a
557
+ ```
558
+
559
+ **The harness never reads your documentation to find lessons.** No decision-record convention, no directory, no file
560
+ format — it runs in many products, and one project's filing habits are not a feature of the tool. If you want your own
561
+ ADRs or postmortems to produce lessons, that is a script in your repository calling this command
562
+ ([/decisions/ad-035.md](/decisions/ad-035.md)).
@@ -0,0 +1,55 @@
1
+ ---
2
+ type: Concept
3
+ title: "What this covers, and what it does not"
4
+ description: "A self-assessment of the harness against a published agentic-risk taxonomy: which of its own rules answer each risk, and what each one still leaves open. Control names are generated from the catalogs and checked by the gate."
5
+ tags: [coverage, security, self-assessment]
6
+ timestamp: "2026-08-17"
7
+ ---
8
+
9
+ # What this covers, and what it does not
10
+
11
+ **This is a self-assessment, not an audit.** Nobody external has verified it. It is published because a list with
12
+ its gaps in it is more useful than a badge, and because this project refuses claims without evidence in the code
13
+ it governs — the same standard applies to its own README ([/decisions/ad-079.md](/decisions/ad-079.md)).
14
+
15
+ Taxonomy: OWASP Top 10 for Agentic Applications, 2026 list (ASI01–ASI10). A revision to that list makes this page stale and nothing here will notice; the
16
+ date above is how you tell.
17
+
18
+ **4 covered · 5 partial · 1 not applicable.**
19
+
20
+ Every name in the **Controls** column is a floor rule or a capability id, generated from
21
+ `src/core/floor/floor.catalog.ts` and `capabilities/catalog.json` and checked by the gate. A control that is
22
+ renamed or removed fails the build rather than leaving a claim standing here. The **What it leaves open** column is
23
+ judgement and is written by hand.
24
+
25
+ | Risk | State | Controls | What it leaves open |
26
+ |---|---|---|---|
27
+ | **ASI01** Agent goal hijack | partial | `untrustedContent`, `unprovable-execution`, `secret-access` | a command rewritten before it runs is not traced to the content it came from, and only shell actions are checked. The damaging tail is refused by the floor whoever suggested it |
28
+ | **ASI02** Tool misuse and exploitation | covered | `outside-project-destruction`, `unprovable-destruction`, `unprovable-execution`, `machine-control`, `catastrophicShell`, `shellStall` | — |
29
+ | **ASI03** Identity and privilege abuse | partial | `subagents`, `blockParentFast`, `policy-surface-write` | the substrate is one developer's editor session, so identity is the session. There is no per-agent credential to bind a decision to |
30
+ | **ASI04** Agentic supply chain | partial | `supplyChain` | a manifest changed without its lockfile, and a version nobody pinned. Advisories, licences and typosquats need the network on every stop and are left to a gate command |
31
+ | **ASI05** Unexpected code execution | covered | `unprovable-execution`, `unprovable-destruction`, `policy-surface-write` | — |
32
+ | **ASI06** Memory and context poisoning | partial | `policy-surface-write`, `lessons` | the handoff and the project lesson store are sealed on write and withheld when a write the harness did not make is detected. The global lesson store is written by other repositories' sessions and cannot be sealed per project |
33
+ | **ASI07** Insecure inter-agent communication | n/a | `subagents` | subagents are spawned by the host and exchange no messages the harness sits between. There is no wire to secure; what is governed is the spawn |
34
+ | **ASI08** Cascading agent failures | covered | `budgetContinue`, `shellStall`, `idleTurnGate`, `failureClassification`, `grind` | — |
35
+ | **ASI09** Human-agent trust exploitation | covered | `shipGate`, `emptyDiffAntiShip`, `planGate`, `comments` | — |
36
+ | **ASI10** Rogue agents | partial | `observe`, `subagents`, `idleTurnGate` | every decision is recorded and hash-chained, and a stalled or idle agent is caught. There is no behavioural baseline, so an agent acting plausibly but wrongly is not flagged |
37
+
38
+ ## How to read "covered"
39
+
40
+ It means every mechanism this project has for that risk is in place and enforced before any policy is read, or is
41
+ a rail an operator can switch on. It does not mean the risk is eliminated. Prompt-level safety is a request to a
42
+ stochastic system, so what is claimed here is only ever about what happens in deterministic code *after* the model
43
+ decides ([/decisions/ad-016.md](/decisions/ad-016.md)).
44
+
45
+ ## How to read "partial"
46
+
47
+ The row's limit says what is missing, in the terms an operator would notice. Three of the four partials are
48
+ partial for the same reason: the harness sits at one developer's editor, so it sees actions rather than identities,
49
+ and it sees what a turn wrote rather than what a turn meant.
50
+
51
+ ## See also
52
+
53
+ - [/concepts.md](/concepts.md) — every rail from the operator's side
54
+ - [/troubleshooting.md](/troubleshooting.md) — from a refusal on screen back to the rule
55
+ - [/decisions/index.md](/decisions/index.md) — why each rule exists
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-001 — Optional parent-Fast sticky deny for Task spawns"
4
+ description: "subagents.blockParentFast denies Task/subagent spawns while the sticky parent model is Fast, closing a gap where Cursor Fast appears on the parent but not on the Task model string."
5
+ tags: [decision, subagents, policy, cursor]
6
+ timestamp: "2026-07-27"
7
+ ---
8
+
9
+ # AD-001 — Optional parent-Fast sticky deny for Task spawns
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-27
13
+
14
+ ## Decision
15
+
16
+ `subagents.blockParentFast` (default `false`) denies Task / `subagentStart` when sticky parent state is
17
+ Fast; bracket/`model_params` Fast is always treated as blocked in policy helpers.
18
+
19
+ ## Why
20
+
21
+ Cursor Fast often appears on the parent (`*-fast` / `model_params.fast=true`) while Task `model` stays
22
+ allowlisted without `-fast`. Without this rail, a Fast parent chat can still spawn full-price subagents.
23
+
24
+ ## Applies to
25
+
26
+ - agent-harness hooks
27
+ - harness-init capability 6b (see [/init.md](/init.md))
28
+
29
+ ## See also
30
+
31
+ - [/decisions/ad-011.md](/decisions/ad-011.md) — provider model catalogs and blocked patterns
32
+ - [/decisions/ad-016.md](/decisions/ad-016.md) — item 2 fixes the missing writer for this state