@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,90 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-028 — A resolved failure is kept, a session attests to itself, and provider neutrality becomes a proof"
4
+ description: "The harness deleted the pairing of a failure with what resolved it at the exact moment the gate went green. A reviewer asked to trust agent-written code had nothing to read, while the harness held every input. And the central promise of provider neutrality was asserted by goldens covering one decision kind out of seven."
5
+ tags: [decision, stagnation, attestation, providers, recovery]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-028 — Keep what resolved it, attest to what ran, prove the neutrality
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **When a gate passes after failing, the files changed between those states are recorded against the failure's
17
+ fingerprint.** Captured immediately before the fingerprint record is cleared — the one moment both halves exist.
18
+ 2. **On recurrence the follow-up cites that history in one line, in the past tense, saying it is not a list to
19
+ edit.** A test asserts no imperative survives in the wording.
20
+ 3. **The store is bounded** at 200 resolutions and 8 files each, pruning the oldest, because it is read on the
21
+ failure path while an operator waits.
22
+ 4. **Scope is the repository.** A resolution from elsewhere is not offered.
23
+ 5. **A session appends a hash-chained attestation**: policy fingerprint, whether policy diverged mid-session, the
24
+ rails that were active, refusals by rule, and gate outcomes. `tlc harness attest [--json]` reads and verifies
25
+ it, exiting non-zero on a broken chain.
26
+ 6. **Verification reports the index at which the chain broke**, and an absent file is an empty valid chain rather
27
+ than a broken one.
28
+ 7. **The attestation states only what the harness observed.** No claim of correctness, of review, or of human
29
+ approval.
30
+ 8. **Provider neutrality is asserted across the full matrix** — every event kind × every decision kind × every
31
+ registered provider — driven from the registry so a new provider is covered without editing the test.
32
+
33
+ ## Why
34
+
35
+ **The harness destroyed its most valuable output at the moment it became true.** It computes an identity for a
36
+ failure and counts repeats to detect stagnation; on success it called `clearFingerprint`, which wiped the record.
37
+ The pairing of *that failure* with *what resolved it* is the one thing nothing else can assemble, because nothing
38
+ else holds a failure fingerprint and the diff in the same process. Products that "learn" from failures store prose;
39
+ this stores evidence.
40
+
41
+ It also addresses the failure mode that matters most in long work: an agent that commits a wrong intermediate state
42
+ deep in a trajectory, where the error propagates because nothing detects or reverts it. A repeated fingerprint is
43
+ precisely that situation announcing itself, and the harness now has something to say about it beyond "you are
44
+ stuck".
45
+
46
+ **The wording is the whole safeguard.** AD-021 recorded the harm of sending an agent to edit healthy code because a
47
+ gate command was malformed; AD-024 recorded the same harm arriving through the diff, and established that a plan
48
+ names files from evidence and never from proximity. A previous resolution is evidence — but the same list phrased
49
+ as an instruction would be that harm arriving through a third door. Hence past tense, hence the explicit "not a
50
+ list to edit", hence a test that fails on an imperative.
51
+
52
+ **A reviewer had nothing to read.** Everything needed to trust an agent-produced change was already in the
53
+ harness — which policy was in force, whether it moved mid-session, every refusal with its rule, every gate
54
+ outcome — and none of it left the machine in a form anyone could check. Governance treated as prompting produces
55
+ no artifact; governance treated as record-keeping does.
56
+
57
+ **And the neutrality claim was thinner than it looked.** `provider.contract.test.ts` asserted the port shape and
58
+ that render survived `abstain`. Six of the seven decision kinds and eighteen of the nineteen event kinds went
59
+ unexercised per provider. A provider that throws on one of those combinations breaks a real turn in production, and
60
+ the test suite would have said nothing.
61
+
62
+ ## Trade-offs
63
+
64
+ Hash chaining is not signing, and the record says so by omission: it detects a rewritten or removed middle record,
65
+ which is what an on-disk ledger is exposed to, and it does not prove authorship. A key would prove more and would
66
+ mean key management, a different problem with its own failure modes. If authorship becomes the requirement, that is
67
+ a separate decision.
68
+
69
+ Repository-scoped resolution history is narrower than it could be. The global spool could carry these across
70
+ repositories, and a fix that worked in another codebase is exactly the kind of advice that reads plausible and is
71
+ wrong. Narrow first.
72
+
73
+ The resolution cache pairs a fingerprint with *everything that changed*, which on a wide turn includes files that
74
+ had nothing to do with the fix. The file cap bounds the noise and the wording refuses to assert causation — it says
75
+ what changed, not what fixed it. That is the honest limit of what the data supports.
76
+
77
+ The conformance matrix runs 266 combinations and will grow with every event kind and provider. It is cheap because
78
+ render is pure, and the test asserts the number of combinations it ran: a loop over an empty registry passes
79
+ silently, which is the way this kind of test rots.
80
+
81
+ ## Not decided here
82
+
83
+ - **Signed attestation** and any notion of identity. Hash chaining is the tamper evidence; authorship is not
84
+ claimed.
85
+ - **Cross-repository resolution history.** The mechanism exists in the global spool; the advice is dangerous enough
86
+ to want data first.
87
+ - **Whether the attestation should be emitted on stop rather than session end.** Session end has the complete
88
+ rollup; a crash before it leaves no record, which is a gap this accepts.
89
+ - **Asserting that two providers produce byte-identical output.** They cannot — each speaks its own dialect. The
90
+ matrix asserts the decision survives and its meaning is preserved, which is the property that matters.
@@ -0,0 +1,73 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-029 — A capability the init skill cannot correctly initialise is not shipped"
4
+ description: "Observation mode shipped with one observable rail, a catalog prompt that named none of them, and silent inertness for any other name — so the init skill would present it, collect a value that does nothing, and report success. The observable set now has one source, the prompt names it, and doctor refuses to be silent about a name with no checker."
5
+ tags: [decision, capability, init, observability, doctor]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-029 — A capability the init skill cannot correctly initialise is not shipped
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **`OBSERVABLE_RAILS` is the single source of what can be observed**, read by the classifier, by `doctor`, and by
17
+ the capability catalog's own prompt.
18
+ 2. **A name enters that list in the same change that adds its `shouldObserve` call site.** A name listed without a
19
+ call site would advertise an observation that never happens.
20
+ 3. **`shouldObserve` refuses a rail it cannot observe**, even when the operator listed it, so the function's
21
+ contract matches what the harness can actually do.
22
+ 4. **`doctor` reports an unobservable rail by name**, alongside the set that is observable. Observation enabled with
23
+ an empty list is also a warning, because that is the shape a "yes" with no follow-up produces.
24
+ 5. **The catalog's `asks` names the valid values and states the consequence of a wrong one**, so the init skill
25
+ collects something real rather than inventing a plausible name.
26
+ 6. **Both are warnings, not failures.** A misconfigured measurement rail is a config fault to fix, not a broken
27
+ install, and failing here would block the command an operator runs to find out what is wrong.
28
+
29
+ ## Why
30
+
31
+ Observation mode shipped one revision ago with three properties that only make sense separately. Exactly one rail
32
+ has a checker wired to it (`comments`). The catalog asked the operator for "rails (rail names to observe)" without
33
+ naming any. And a name with no checker was **silently inert** — inertness was deliberate, for forward compatibility,
34
+ and the silence was not thought about at all.
35
+
36
+ Put together, the failure is specific and bad: the init skill presents the capability, the operator says yes, a
37
+ plausible name like `plan-gate` gets written, and nothing is ever recorded. The operator then reads that silence as
38
+ *the property always holds*, which is the worst available misreading of a rail whose entire purpose is to tell them
39
+ whether a rule is needed. A measurement that reads as a clean result when it never ran is worse than no measurement.
40
+
41
+ This is the AD-020 family again — a config value nothing reads — and it arrived through a route the previous
42
+ instances did not: not a field with no producer, but a field whose *values* have no producer. The check that caught
43
+ it was the question "can the init skill actually initialise this?", which is worth asking of every capability rather
44
+ than of this one.
45
+
46
+ The general rule this establishes: **a capability is not finished when its code works. It is finished when the
47
+ surface that offers it can offer it correctly.** The init skill, `doctor` and the catalog prompt are part of the
48
+ capability, not documentation of it.
49
+
50
+ ## Trade-offs
51
+
52
+ Forward-compatible inertness is kept — an unknown name does not throw, so a config written for a future version of
53
+ the harness still loads. What changed is that inertness is now reported. Refusing to load would be the other
54
+ defensible choice and it would break the forward compatibility this project has elsewhere; reporting keeps both.
55
+
56
+ `OBSERVABLE_RAILS` is a hand-maintained list, which is a small duplication against the call sites. Deriving it
57
+ mechanically would mean either a registry the call sites enrol in — more machinery than one entry justifies — or
58
+ scanning source at runtime, which the latency budget forbids. The invariant is stated in a comment and asserted by a
59
+ test that pins the list, so adding a rail without its call site fails.
60
+
61
+ One rail is a thin start, and naming it explicitly makes that thinness visible in the catalog rather than implied.
62
+ That is the point: an operator reading "observable today: comments" knows the shape of what they are buying, and an
63
+ operator reading "rail names to observe" does not.
64
+
65
+ ## Not decided here
66
+
67
+ - **Observing the plan gate or the ship gate.** Both have deterministic checkers and would be genuine additions.
68
+ Each needs its own call site and its own entry in the list, in a change that adds both together.
69
+ - **Deriving the observable set from the call sites mechanically.** Worth revisiting if the list reaches a size
70
+ where hand-maintenance is a real risk.
71
+ - **Whether the init skill should refuse to write an unobservable name.** Today it can write one and `doctor` will
72
+ say so. Validating at write time is stricter and would need `init` to validate fields generally, which it does not
73
+ do for any other value.
@@ -0,0 +1,97 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-030 — Clearing a policy divergence is one operator command behind four independent locks, and a refusal never points an agent at a door the floor holds shut"
4
+ description: "The integrity check locked two live sessions out because its recovery path named no command, pointed the agent at commands the floor refuses, and did not exist as a side-effect-free operation. `tlc harness policy accept` is that operation, kept out of an agent's reach by four locks that fail independently, recorded rather than erased — and a sweep now fails the build when an agent-facing refusal instructs a command the agent cannot run."
5
+ tags: [decision, policy, integrity, security, cli]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-030 — One operator command, four locks, and a refusal that knows its audience
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **`tlc harness policy` lists every source that changed out of band during a live session and changes nothing.**
17
+ 2. **`tlc harness policy accept <path>...` updates the baseline entry for exactly the named paths**, in every live
18
+ session's baseline.
19
+ 3. **Four independent locks sit between that command and an agent**, no one of them load-bearing alone:
20
+ - the floor refuses `tlc harness policy` from inside any agent session, with no config switch;
21
+ - the command refuses without an interactive terminal, so a script cannot reach it;
22
+ - the operator names each path, so accepting is an act rather than a keystroke and its blast radius is exactly
23
+ what was typed;
24
+ - acceptance is per source, so every other divergence keeps blocking.
25
+ 4. **No blanket permission is expressible.** The accepted hash is the hash at that moment; a later change to the
26
+ same file diverges again. There is deliberately no way to stop watching a source.
27
+ 5. **A path the loader never reads is refused**, naming the sources that exist.
28
+ 6. **`reason` is the agent's half of a refusal and `userNote` is the operator's.** An agent-facing refusal states
29
+ what happened and says to report it; the command that clears it lives in the operator's half.
30
+ 7. **A sweep over every refusal producer fails the build** when a `reason` instructs a mutating harness subcommand.
31
+ 8. **`doctor` reports a divergence, naming the paths and the command**, and is silent when there is none.
32
+
33
+ ## Why
34
+
35
+ The integrity check has now blocked two live sessions: its own author, and a colleague's agent after an operator
36
+ edited `config.json` from their terminal. The check behaved correctly both times — a mid-session policy change with
37
+ no harness command behind it is precisely what it exists to catch. The recovery path is what failed, in four
38
+ measurable ways.
39
+
40
+ **It named no command.** The refusal said "the harness commands re-record the baseline when they write". Only
41
+ `grind`, `pause|resume`, `mode` and `gate` do. `status` and `doctor` do not — and `status` is exactly what a
42
+ reasonable reader tries first, because it sounds like the command that reports state. The blocked agent tried it,
43
+ learned nothing, and stopped.
44
+
45
+ **It pointed the agent at a door the floor holds shut.** Every command that would have worked is in
46
+ `MUTATING_SUBCOMMANDS`, refused from inside a session by design. So the instruction was not merely unhelpful; it was
47
+ addressed to the one party that cannot act on it. This is the third instance of that defect: the policy-surface
48
+ guard had it and was fixed under AD-026, the integrity check had it here, and the sweep built for this decision
49
+ found a third in the `paired` shell ask on its first run. Three instances is not carelessness about wording — it is
50
+ a missing invariant, which is why the sweep exists.
51
+
52
+ **No side-effect-free operation existed.** Every route that repairs a baseline also writes something: `mode` writes
53
+ a state file that then outranks config, `grind` writes a flag that outranks it too. An operator fixing a lockout
54
+ would silently acquire a latent surprise for the next time they edited config.
55
+
56
+ **And `doctor` was silent.** The one command an operator runs to find out what is wrong said nothing about the thing
57
+ blocking every acting tool call.
58
+
59
+ The design question that matters is not "add an accept command" but **"what keeps an accept command out of reach of
60
+ the thing it defends against?"** A command that clears a tampering signal is the first thing an attacker wants, and
61
+ one lock is one bug away from nothing. Hence four, at different layers, testable separately: a floor rule that no
62
+ config can weaken, a TTY requirement that stops scripts, an explicit naming that cannot happen by accident, and
63
+ per-source scope that limits what a single accept can bless.
64
+
65
+ ## Trade-offs
66
+
67
+ **Accept is recorded, not silent.** Without that it would be a memory hole: the rail's own history could be erased
68
+ by the command meant to resolve it. Recorded, it is an operator decision a later reviewer can see, which is the
69
+ difference between a sanctioned override and a bypass.
70
+
71
+ **Naming the path instead of prompting.** A y/N prompt is the obvious alternative and it is worse twice over: a
72
+ prompt is a keystroke, and keystrokes become reflex — the finding AD-026 acted on — and a prompt cannot be tested
73
+ without a pty, on a rail whose failure mode is silence. Typing the path is slower on purpose.
74
+
75
+ **Per-source acceptance required new machinery.** `refreshPolicyBaselines` rewrites the whole fingerprint, which is
76
+ right for a sanctioned mutation that changed one thing and wrong here: it would bless every other pending change
77
+ alongside the one named. `acceptPolicySources` writes one entry at a time and leaves the rest untouched.
78
+
79
+ **The sweep is a line-scoped scan, not an AST walk.** It reads the `reason:` property and the continuation lines of
80
+ its array literal, which is the shape every refusal in this codebase uses, and skips comments. It asserts how many
81
+ reason strings it scanned, because a scan that parses nothing passes forever. A refusal built in some other shape
82
+ would escape it — the test says so rather than implying completeness.
83
+
84
+ **Social engineering is not solved.** An agent cannot run accept, and nothing stops it from writing a message that
85
+ persuades an operator to. The mitigation is that accept prints the path, so the operator sees what they are being
86
+ asked to bless. Stated here rather than pretended away.
87
+
88
+ ## Not decided here
89
+
90
+ - **Whether a read should ever refresh a baseline.** It should not, and this decision does not revisit it: a
91
+ tampering signal cleared as a side effect of looking is not a signal.
92
+ - **Recording the acceptance in a dedicated ledger.** The attestation carries `policyDiverged` for the session; a
93
+ per-acceptance audit row would be finer and needs its own decision about retention.
94
+ - **Refusing to load a config whose divergence was never accepted.** The current design blocks acting tool calls and
95
+ allows reads ([/decisions/ad-025.md](/decisions/ad-025.md)), which is what lets a blocked agent explain the situation. Failing closed would remove that.
96
+ - **Extending the sweep to `userNote`, `text` and `detail` strings.** Only `reason` is agent-facing today. The others
97
+ reach the operator, where naming a command is correct.
@@ -0,0 +1,94 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-031 — The decisions are the changelog, a breaking change carries its own instruction, and looking never changes anything"
4
+ description: "Update did the mechanical work well and could not say what changed: no version identity, no mapping from a revision to the decisions that landed, and no channel for a breaking change at the moment of impact — three shipped in one week and only doctor mentioned them afterwards. The revision is the version, a migration note lives in the decision that caused it, and `update --check` fetches without merging."
5
+ tags: [decision, release, update, changelog, cli]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-031 — The decisions are the changelog
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **The runtime's git revision and commit date are the version.** `tlc harness version` reports them, plus the
17
+ revision this project last saw.
18
+ 2. **A breaking change carries a `migration` note in the frontmatter of the decision that caused it.** Optional;
19
+ validated as non-empty when present.
20
+ 3. **`migration` marks a decision as needing operator action.** Amended by
21
+ [/decisions/ad-034.md](/decisions/ad-034.md): a note belongs only where `doctor` cannot detect the condition for
22
+ the operator. Five of the first six notes repeated a doctor row and were removed.
23
+ 4. **`tlc harness update --check` fetches and never merges**, reporting the pending commit count and the decisions
24
+ that would land, with the ones needing action first and their notes shown.
25
+ 5. **After an update, decisions added between the seen revision and the new one are announced once.** A project with
26
+ no marker records where it stands and announces nothing.
27
+ 6. **A fast-forward failure names both routes out** — reset to upstream, or re-run the installer — gives the command
28
+ for the first, and runs neither.
29
+ 7. **There is no hand-written changelog and no semantic version.**
30
+
31
+ ## Why
32
+
33
+ `update` already did the mechanical work well: fetch, fast-forward, relink the CLI and skill, rewrite hooks, rebuild
34
+ `dist/`, announce newly available capabilities, run `doctor`. One command ending in a health check. What it could not
35
+ do was tell the operator what changed, for reasons that compound.
36
+
37
+ **There was no version identity.** `package.json` has said `0.1.0` since the first commit — which is exactly what a
38
+ hand-maintained number does — with no tags and no releases, and `tlc harness version` answered `unknown: version`.
39
+ Without an identity, "what changed since mine?" is not a hard question, it is an unanswerable one.
40
+
41
+ **The substance of a changelog already existed and was unreachable.** Thirty decision records carry why, the
42
+ trade-offs and what was refused, which is more than most changelogs contain. What was missing was the mapping from
43
+ *I updated* to *these landed*. Writing a `CHANGELOG.md` would have restated them and then drifted from them; reading
44
+ them needs no second source to maintain.
45
+
46
+ **A breaking change had no channel at the moment of impact, and three shipped in one week.** AD-025 removed a
47
+ posture's second spelling, so a config carrying it falls back silently at load. AD-029 made an unobservable rail name
48
+ reportable, which means a config naming one had been recording nothing. AD-027 is the sharpest: ordering evidence
49
+ against the code can newly **block a stop that previously passed**. `doctor` reports all three afterwards — that is
50
+ the safety net working — but the operator was standing in front of `update` when the change happened, and it said
51
+ nothing.
52
+
53
+ The mechanism was already in the codebase and pointed at the wrong subject. `announceNewCapabilities` reads a
54
+ per-project seen marker, lists only what is new *to that project*, prints benefit and trade-off, and never repeats.
55
+ That is a changelog delivered at the moment of impact. Applying the same shape to decisions is the whole feature.
56
+
57
+ ## Trade-offs
58
+
59
+ **No semantic version.** A version number is a promise about compatibility, and AD-003 refuses back-compat shims —
60
+ so the promise cannot be kept and stating it would be a lie with a dot in it. The revision is honest, moves with the
61
+ thing it names, and is what `update` already manipulates. Tags become worth adding when the compatibility promise
62
+ becomes real.
63
+
64
+ **`migration` is optional, which means it can be forgotten.** Requiring it on every decision would produce "no
65
+ migration needed" on the large majority and train a reader to skip the field. The bundle check catches the halfway
66
+ state — present and empty — because a decision announced as needing action that then shows nothing is worse than one
67
+ that never claimed it. Forgetting it entirely is still possible, and `doctor` remains the net.
68
+
69
+ **A first update announces nothing.** A project with no seen marker could be shown every decision ever written, and
70
+ thirty entries is indistinguishable from no message. It records where it stands instead. The cost is that an operator
71
+ installing today learns nothing about the past from this rail — which is what `docs/decisions/index.md` is for.
72
+
73
+ **The digest reads decisions added in a range**, so a decision *amended* after landing is not re-announced. That is
74
+ deliberate: an amendment is usually a wording fix, and re-announcing on every edit is how the capability digest would
75
+ have become noise too.
76
+
77
+ **The three retroactive notes were written after the fact**, by the author of the changes they describe. That is the
78
+ weakest part of this: they are as good as the memory of the week they cover. Notes written at the time of the change
79
+ are the standard from here.
80
+
81
+ **Amended the day after by AD-034.** Five of the six notes then in the tree said, in substance, "run
82
+ `tlc harness doctor`" — which `update` runs automatically three lines later, and which reports whether the condition
83
+ applies at all. The mechanism was right and the content was noise, so `migration` now means specifically "something
84
+ doctor cannot detect for you".
85
+
86
+ ## Not decided here
87
+
88
+ - **Tags and releases.** Worth adding when there is a compatibility promise to attach to them.
89
+ - **Announcing on session start rather than on update.** The information belongs where the change happens, and a
90
+ hook that reports it every session is a cost on every session.
91
+ - **Checking for updates in the background.** A scheduled phone-home is a permanent cost for information the operator
92
+ can ask for in one command.
93
+ - **Migration notes that the harness applies.** `config.json` is a protected surface and policy is the operator's
94
+ ([/decisions/ad-022.md](/decisions/ad-022.md)). A note says what to run; running it is not the harness's call.
@@ -0,0 +1,83 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-032 — A hook is healthy when it can run, not when a marker string is present"
4
+ description: "A colleague's session was blocked by a preToolUse hook whose command was a bare executable, so Node read the hook payload as a program. The harness could not have written that command — but its health check could not have caught it either, because it decided wiring health by looking for one string. Health is now per event: our launcher named, the script present, a handler after it."
5
+ tags: [decision, providers, wiring, doctor, diagnosis]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-032 — A hook is healthy when it can run
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **Wiring health is checked per declared event**: an entry must name the expected launcher, that file must exist,
17
+ and a handler token must follow it.
18
+ 2. **Only the events this provider declares are inspected**, and within them only entries whose command names our
19
+ launcher. A hook belonging to another tool is never reported.
20
+ 3. **A declared event with no harness entry is a problem**, which is the case marker presence cannot see.
21
+ 4. **Marker presence keeps its job unchanged** — deciding whether the file is ours, and therefore whether `update`
22
+ may overwrite it. Two questions, two checks.
23
+ 5. **`doctor` names the failing event and the reason**, bounded to three with a count of the rest.
24
+ 6. **Command tokens are parsed respecting quotes**, because the writer quotes a path containing spaces and a checker
25
+ that mis-tokenises reports a healthy wiring as broken.
26
+
27
+ ## Why
28
+
29
+ A colleague's Cursor session stopped working. The `preToolUse` hook crashed with a raw Node stack trace and the hook
30
+ payload echoed back — conversation id, workspace path, email, and the command they were about to run. The entry was
31
+ `failClosed: true`, so the crash did not merely log: it blocked the tool.
32
+
33
+ The signature was `node:internal/main/eval_stdin`, which occurs only when `node` is invoked with **no script** and
34
+ stdin is a pipe: Node reads the payload as a program, and the first `:` in the JSON is a syntax error.
35
+
36
+ Two things were worth separating. **The harness could not have written that command.** `cursorWiring` builds `args`
37
+ as `[launcherPath, handler]`, so every rendered command carries three tokens; even an empty launcher path renders
38
+ `node tool-before`, which fails as a missing module and not as an eval. `tlc-exec.mjs` is ruled out too — every run
39
+ branch carries non-empty args, and a missing entry exits 2 with usage. Verified by rendering, not by reading.
40
+
41
+ **But the harness could not have caught it either, and that part is ours.** `isCursorWired` decides health by
42
+ checking whether the file *contains* the string `tlc-exec.mjs`. That is a good proxy for "is this file ours", which
43
+ is the right question when deciding whether `update` may overwrite it. It is the wrong question for "do these hooks
44
+ work": a file with the marker in one entry and a broken command in another reported `wired`, and `doctor` said the
45
+ provider was healthy. Nothing checked that the executable resolves, that the script exists, or that every event the
46
+ harness needs has an entry at all. A hook whose script moved in a reinstall reported healthy until it fired.
47
+
48
+ And when `doctor` did notice, it said `detected but not wired — run: tlc harness update` and nothing else. An
49
+ operator learned that something was wrong, which is one step above silence.
50
+
51
+ ## Trade-offs
52
+
53
+ **The check resolves paths; it does not run hooks.** Running a hook to prove it runs would run it, with whatever side
54
+ effects that implies on a `preToolUse`. Naming the launcher, checking the file exists and requiring a handler is what
55
+ can be established without acting — and it is exactly the set of things that produced this incident.
56
+
57
+ **Foreign hooks are ignored, which means a foreign hook can still break a session.** That is deliberate: reporting
58
+ another tool's entry would train an operator to skip the check, and the check would then miss ours. The harness
59
+ reports on what it wrote.
60
+
61
+ **A fresh install with no wiring produces one problem per declared event**, nineteen of them, which is why the
62
+ formatted detail is bounded to three plus a count. A doctor line that is a wall is a line nobody reads.
63
+
64
+ **Quote-aware tokenising was found by a test, not by review.** Splitting on whitespace broke a launcher path
65
+ containing spaces — a macOS path under "Application Support" is an ordinary shape — and a checker that
66
+ reports a healthy wiring as broken is the failure mode that gets a check switched off. The writer already quoted for
67
+ this reason; the reader now matches it.
68
+
69
+ **One pre-existing test asserted the weak rule** — marker present, one entry out of nineteen, expect `wired` — and it
70
+ had to be inverted rather than deleted. It now asserts the incident's shape.
71
+
72
+ ## Not decided here
73
+
74
+ - **Where the broken hook came from.** The file is not the harness's output, so either it was hand-edited, written by
75
+ a much older revision, or written by another tool. Answering it needs the operator's `hooks.json`, and the fix does
76
+ not depend on the answer.
77
+ - **Whether `update` should overwrite a file that is ours but broken.** Today it refuses when the marker is absent and
78
+ rewrites when present, which already covers the broken-but-ours case. A file with the marker and a foreign entry
79
+ alongside is the one shape where overwriting loses something, and it stays refused.
80
+ - **Checking the Claude settings shape more deeply.** Its status already comes from a merge comparison against the
81
+ desired entries, so a changed command is caught. Only the replace-strategy path used the weak check.
82
+ - **Rewriting a hook's crash output.** The stack trace and echoed payload come from Node running someone else's
83
+ malformed command. The harness cannot format an error it did not produce.
@@ -0,0 +1,89 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-033 — The gate says what it costs, appendFiles stops promising what it cannot deliver, and a dead capability leaves"
4
+ description: "An operator reported thirty minutes on a small feature. The cause was not his machine: appendFiles auto advertised narrowing the gate to changed files and could deliver it for neither of his commands, so a full lint and a full suite ran on every one of three attempts. Doctor now names that, measures the interpreter start instead of asserting it, the report shows time per gate, and the format capability nothing ever read is gone."
5
+ tags: [decision, grind, cost, doctor, observability]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-033 — The gate says what it costs
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **A package-manager script does not receive appended files.** `npm`, `yarn`, `pnpm` and `bun run` invoke somebody
17
+ else's script, and what that script does with a path is not something this process can know.
18
+ 2. **A command that already carries a glob does not receive appended files.** It walks the glob regardless, so
19
+ appending narrows nothing.
20
+ 3. **A transparent prefix resolves through to the real tool.** `npx`, `bunx` and `dlx` run the tool named next, and
21
+ that tool decides whether a path narrows — so `npx jest <file>` still narrows.
22
+ 4. **`appendFilesVerdict` returns a reason**, and `doctor` reports a `gate scope` warning naming the command, the
23
+ reason, and `maxLoops`.
24
+ 5. **`doctor` measures the interpreter's cold start** — three samples, median — and reports no number when the
25
+ measurement fails. The hardcoded millisecond claim is gone.
26
+ 6. **The session report shows runs, total and worst run per gate**, and states that the cost is paid once per attempt.
27
+ 7. **`format.enabled` and `format.command` are removed** from the policy type, the defaults, the loader, the catalog
28
+ and the docs.
29
+
30
+ ## Why
31
+
32
+ An operator reported the harness as "extremely slow" — thirty minutes on a three-model feature, "still running jest,
33
+ format and whatever". Three findings, none of them his machine.
34
+
35
+ **The harness was not running format.** `format` was capability number one in the catalog, presented to every operator
36
+ by the init skill, declared in `Policy`, merged by the loader, and read by no entrypoint. Fifth instance in one week
37
+ of a field advertised and never consumed, and the worst placed of them. AD-003 removes what nothing reads rather than
38
+ honouring it late, and implementing it now — a command after every Write — would ship exactly the failure mode being
39
+ reported.
40
+
41
+ **The cost was a promise `appendFiles` could not keep.** His configuration, verbatim: `appendFiles: "auto"`,
42
+ `lintCommand: ["npx","eslint","src/**/*.ts","test/**/*.ts","--no-fix"]`, `testCommand: ["npm","test"]`,
43
+ `maxLoops: 3`. The lint command carries its own globs, so appending changed files narrows nothing and the whole tree
44
+ is linted on every stop. The test command is a package-manager script, so whether appended paths reach the runner
45
+ depends on the manager and its version — and `shouldAppendFiles(["npm","test"], "auto")` returned **true**, verified
46
+ by running it.
47
+
48
+ The codebase already knew this. `RESOLUTION_FAILURE_PATTERNS` contains `missing script:`, which is npm's message —
49
+ so npm was recognised as a script runner for the purpose of classifying a failure, and not for the purpose of
50
+ deciding whether to append arguments to it. The recipe-runner exception exists for precisely this reason and had the
51
+ wrong membership.
52
+
53
+ **And the one number `doctor` offered about speed was prose.** `BUN_COST_NOTE` asserted "hook cost ~1 ms with Bun vs
54
+ ~27 ms with Node" on every machine and had measured it on none. An operator with a slow install had nothing to
55
+ measure and nothing to point at, which is how "the harness is slow" becomes unfalsifiable. It measures now: 2 ms on
56
+ the machine this decision was written on.
57
+
58
+ The data to answer his question was already recorded — `gate.outcome` carries `duration_ms` — and surfaced nowhere.
59
+
60
+ ## Trade-offs
61
+
62
+ **Not appending is the conservative direction.** The command runs as the operator wrote it, which is what they can
63
+ reason about. Appending is what produced the surprise. `always` remains for an operator who knows their runner
64
+ forwards, and `never` remains for one who wants the full run.
65
+
66
+ **`bun test` narrows and `bun run test` does not**, which is a distinction inside one executable. It is real — the
67
+ first is bun's own runner, the second is a script — and encoding it means a special case. The alternative was to
68
+ refuse narrowing for all of `bun`, which would penalise the shape that works.
69
+
70
+ **The glob rule is a character match**, so a literal `[` in a filename would read as a glob and stop narrowing. The
71
+ failure direction is the safe one: the command runs as written.
72
+
73
+ **The measured figure is the interpreter's start, not the whole hook.** Every entrypoint has side effects — an
74
+ unrecognised payload writes an obs record into the operator's repository — so a full round trip cannot be measured
75
+ without acting. The label says which it is, because reporting a part as the whole is the overclaim this replaces.
76
+
77
+ **Removing `format` breaks nothing and deletes an option some operator may have enabled**, believing it worked. It
78
+ never did. A stale block in a config still loads and is ignored, so there is nothing to do about it and — per
79
+ [/decisions/ad-034.md](/decisions/ad-034.md) — nothing worth telling an operator to do.
80
+
81
+ ## Not decided here
82
+
83
+ - **Implementing format-on-edit.** It would be a new capability with a measured per-Write cost, decided on its own
84
+ evidence rather than inherited from a field that never ran.
85
+ - **Warning about `maxLoops` itself.** Three or five is a considered choice; multiplying a cheap gate is fine and
86
+ multiplying an expensive one is what the `gate scope` row is for.
87
+ - **Detecting a slow gate command and suggesting a faster one.** The harness would be guessing at somebody's test
88
+ setup. It reports the time and the multiplication; the choice is theirs.
89
+ - **Measuring a full hook round trip.** Needs an entrypoint with no side effects, which none of them is today.
@@ -0,0 +1,86 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-034 — A warning that fires on a healthy install is not a warning, and the author reads the operator's output before anyone else does"
4
+ description: "A real update run put NEEDS YOUR ACTION on two changes that did not apply, both saying run doctor — which update ran automatically three lines later — with an escaped quote leaking into the terminal, above nine warnings for capabilities nobody had enabled, under a line claiming all checks passed. Every piece had a passing test. A migration note now means something doctor cannot detect, unenabled capabilities are inventory, and the author runs the operator-facing commands and reads them before shipping."
5
+ tags: [decision, process, doctor, release, noise]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-034 — A warning that fires on a healthy install is not a warning
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **A `migration` note exists only where `doctor` cannot detect the condition for the operator.** Five of the six
17
+ notes then in the tree said, in substance, "run `tlc harness doctor`" — which `update` runs automatically. They are
18
+ removed. AD-027 keeps its note because a stop blocked by evidence ordering is invisible to every check.
19
+ 2. **The digest leads with the note and trails the decision id.** A decision's title states the author's reasoning;
20
+ the operator's situation is what belongs on the first line.
21
+ 3. **The heading appears only when a note exists**, and says what a note now means. With nothing to do, the digest
22
+ states what landed and points at the doctor run below rather than asking for it.
23
+ 4. **A frontmatter value is unescaped before display.** An escaped quote reached a terminal as a literal backslash.
24
+ 5. **A capability that is merely not enabled is one `ok` inventory row**, not one warning each.
25
+ 6. **The doctor summary counts failures and warnings separately** and never says everything passed while printing a
26
+ warning.
27
+ 7. **Before asking anyone else to test, the author runs every operator-facing command and reads the output.** Unit
28
+ tests assert pieces; the defects here were all in the composition and in the reading.
29
+
30
+ ## Why
31
+
32
+ An operator ran `tlc harness update` and the output contained four defects, each of which had a passing test.
33
+
34
+ `NEEDS YOUR ACTION (2)` — and neither applied to him. Three lines below, `doctor` reported his Cursor wiring as
35
+ `wired` and his config carried no `format` block. Both notes said "run `tlc harness doctor`", which the update then
36
+ did automatically. One of them rendered `\"detected but not wired\"` with the backslashes visible, because the value
37
+ was written with escaped quotes inside a quoted string and only the outer pair was stripped.
38
+
39
+ Below that, nine warnings for capabilities he had never enabled — and buried among them the three rows that mattered:
40
+ a policy baseline changed out of band, and two gate commands running in full on every attempt. Then, under all of it,
41
+ `doctor: all checks passed`.
42
+
43
+ Every one of those pieces was individually correct and individually tested. The escaping test did not exist because
44
+ the value looked fine in the source. The digest test asserted the heading appeared, which it did. The capability
45
+ warning test asserted a warning was produced, which it was. What no test covered, and no test naturally would, is
46
+ **what the whole thing reads like to the person it is for**.
47
+
48
+ The through-line is one idea, arriving for the fourth time this week. AD-026 removed an approval prompt that fired
49
+ often enough to become a keystroke. AD-029 removed a silence an operator would misread as a clean result. AD-032
50
+ removed a health check that passed on a broken install. This removes an alarm that fires on a healthy one. In every
51
+ case the mechanism worked and the signal was worthless, because **a signal that appears when nothing is wrong cannot
52
+ carry the case where something is.**
53
+
54
+ And the process failure is mine and worth writing down plainly: the harness was handed to two colleagues today and
55
+ broke in front of both. The first was a hook that could not run; the second was thirty minutes of gate time nobody
56
+ could see. Both were found by them, not by me, and both were visible in output I had never once read end to end. The
57
+ tests were not the problem. Not looking was.
58
+
59
+ ## Trade-offs
60
+
61
+ **Removing five notes loses real information for an operator who never runs `doctor`.** That is accepted: `update`
62
+ runs `doctor` at the end, so the only way to miss it is to ignore the output — and a reader who ignores the output is
63
+ exactly the reader an extra alarm does not reach.
64
+
65
+ **One inventory row is less discoverable than nine warnings.** Deliberately. `update` still prints each capability
66
+ with its benefit and trade-off in full, which is the moment an operator is choosing; `doctor` is the moment they are
67
+ diagnosing, and a list of things they decided not to enable is not a diagnosis.
68
+
69
+ **`migration` now depends on a judgement — can doctor see this?** — which cannot be checked mechanically. The bundle
70
+ check still catches a present-but-empty note. The judgement is stated here so the next note is written against it
71
+ rather than against a habit.
72
+
73
+ **Reading the output by hand does not scale and is not a test.** It found four defects that four suites did not, so
74
+ it earns its place as a step rather than as a substitute. The narrower part is testable and now tested: the digest's
75
+ shape, the unescaping, the inventory row, the summary wording.
76
+
77
+ ## Not decided here
78
+
79
+ - **A golden test over the whole `update` output.** It would have caught all four, and it would also fail on every
80
+ wording change, which is how a golden becomes a rubber stamp. The composition is checked by reading; the pieces by
81
+ assertions.
82
+ - **Whether `doctor` should have a quiet mode.** With the wall gone the output is short enough that it has not earned
83
+ a flag.
84
+ - **Re-announcing a decision whose note is added later.** The digest reads decisions added in a range, so a note
85
+ written after the fact is not delivered. Notes belong in the change that causes them, which is the standard this
86
+ decision is a correction toward.