@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,79 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-043 — A standing rule is pinned, not ranked, and the budget says what it dropped"
4
+ description: "The operator's own rule was written, stored, correct and never delivered. Three causes, each invisible on its own: an authored lesson carried `priority: 0.8` on a 70..100 scale, so it contributed 0.004 where a seed contributes 0.45; the char budget fits about two blocks while `maxInjectSession` promises five, silently; and ranking is the wrong mechanism for an instruction the operator did not want ranked."
5
+ tags: [decision, lessons, ranking, visibility]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-043 — A standing rule is pinned, not ranked
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **`priority: 0.8` on an authored lesson becomes `80`.** Every other producer uses 70..100 and `relevanceScore`
17
+ divides by 200. The old value was written as if priority were the 0..1 scale `confidence` uses.
18
+ 2. **A lesson can be pinned.** `tlc harness lessons add --pin` sets `pinned: true`; pinned lessons are placed before
19
+ every scored lesson, in store order.
20
+ 3. **Pinning changes order, not eligibility.** Staleness, the validity window, mode filtering and the char budget
21
+ all still bind on a pinned lesson.
22
+ 4. **`selectLessons` returns `omitted`**, and the injected block names it when it is non-zero.
23
+ 5. **The note is silent when nothing was dropped.**
24
+ 6. **`lessons list` marks a pinned lesson `PINNED`.**
25
+
26
+ ## Why, and how the three causes hid each other
27
+
28
+ The intent was one rule: never declare work done without an end-to-end run pasted into the reply. It was written
29
+ to the global tier, stored correctly, and `lessons list` showed it active. It never reached a turn.
30
+
31
+ Running `selectLessons` against another repository and printing what came out gave the answer in one line — the
32
+ rule was absent and two core seeds were present. Then three separate causes had to be peeled apart:
33
+
34
+ **The priority scale.** `buildAuthoredLesson` set `priority: 0.8`. `relevanceScore` adds `priority / 200`, so an
35
+ authored lesson contributed **0.004** where `core:gate:stagnation` contributes 0.5. Combined with confidence 0.8
36
+ against 1, and no decay on core, every authored lesson sat at the bottom of every ranking. Shipped in AD-035 and
37
+ invisible for as long as nobody looked at an ordered list — the observed scores, 0.203 and 0.305 against 0.700,
38
+ match the arithmetic exactly.
39
+
40
+ **The silent budget.** `maxInjectSession` defaults to 5, `maxCharsSession` to 900, and a rendered block is four
41
+ lines. Two fit. So the count promised five, delivered two, and nothing anywhere said so. That is the same defect
42
+ family as a config value nothing reads, pointed the other way: a config value that reads as a promise the
43
+ mechanism cannot keep.
44
+
45
+ **Ranking as the wrong mechanism.** Fixing the scale was not enough, and the correct response was not to keep
46
+ raising numbers until the rule won. Ranking exists for lessons the harness *inferred* from failures — it weighs
47
+ recurrence, decay and gate match, all of which are estimates. An instruction the operator wrote deliberately has
48
+ no recurrence to accumulate and is not an estimate. Making it compete on score was a category error.
49
+
50
+ ## Why pinning is bounded rather than absolute
51
+
52
+ A pinned lesson still obeys the char budget. The alternative — pinned content bypassing the budget — would let a
53
+ handful of pins consume the whole session context and make the budget a suggestion.
54
+
55
+ The worst case is therefore an operator pinning more than fits, and the omission note is what makes that visible
56
+ instead of silent. Staleness and validity still apply, because a pinned rule that names a renamed file is exactly
57
+ as wrong as an unpinned one.
58
+
59
+ ## Trade-offs
60
+
61
+ **A pinned lesson displaces a higher-scoring one.** That is the point, and it is the operator's explicit choice.
62
+
63
+ **Nothing caps how many lessons can be pinned.** A cap would need a number nobody can justify. The budget already
64
+ bounds delivery, and `lessons list` shows every `PINNED` row.
65
+
66
+ **The priority fix changes the ranking of every authored lesson already on disk.** They move up, which is what the
67
+ 0..100 scale always meant.
68
+
69
+ **The omission note costs a line whenever the budget binds.** With the default 900-char budget and any real store,
70
+ that is most turns — which is honest: it says the budget is the binding constraint, and raising `maxCharsSession`
71
+ is the operator's call.
72
+
73
+ ## Not decided here
74
+
75
+ - **Raising the default `maxCharsSession`.** It is per-project config, and a default that spends more context by
76
+ itself is a decision for whoever installs the harness.
77
+ - **`lessons pin <id>` / `unpin <id>`.** Today pinning is set when the lesson is written; rewriting the same
78
+ instruction updates it in place, which covers the case.
79
+ - **Whether core seeds should ever be pinnable.** They ship immutable, so there is nowhere to record it.
@@ -0,0 +1,61 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-044 — Only an injection a gate could grade can be unproven"
4
+ description: "The effectiveness reading counted session-start injections, which nothing ever grades — a lesson whose gate is `any` is not even eligible on a retry. So a healthy pinned standing rule reported `unproven` forever and `doctor` warned about it on every run, in every repository on the machine. A separate `gradeableCount` is incremented where the pending credit is written."
5
+ tags: [decision, lessons, measurement, visibility]
6
+ timestamp: "2026-08-04"
7
+ ---
8
+
9
+ # AD-044 — Only an injection a gate could grade can be unproven
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-04
13
+
14
+ ## Decision
15
+
16
+ 1. **`gradeableCount` counts injections a later gate run can grade** — incremented where the pending credit is
17
+ written, so the counter and the credit cannot disagree.
18
+ 2. **`unproven` means `gradeableCount > 0` and nothing graded yet.** It no longer reads `injectedCount`.
19
+ 3. **A lesson injected only at session start reads `session-only`**, rendered as
20
+ `session-only (injected 12x, never for a gate)`. It is not a warning.
21
+ 4. **`doctor` says "1 lesson has been injected … nothing shows it helped"** with real agreement, instead of
22
+ "1 lesson have been injected … nothing shows it help".
23
+
24
+ ## Why
25
+
26
+ `injectedCount` is incremented for every injection, in both modes. Only the retry path writes a pending credit,
27
+ because only there is there a gate whose next run can decide. So a session-start injection can never be graded.
28
+
29
+ The two facts combine badly for exactly the case AD-043 just made possible. A pinned standing rule is written
30
+ with gate `any`; `allowedForMode` on a retry requires `failedGate === gate`, so an `any` lesson is **never
31
+ eligible on a retry**. It is injected at every session start, `injectedCount` climbs, `gradeableCount` stays at
32
+ zero, and the old reading called that `unproven` — forever, in every repository on the machine, as a `doctor`
33
+ warning.
34
+
35
+ That is an alarm firing on a healthy install, which AD-034 named as its own defect family: a warning the reader
36
+ learns to skip takes the next real one with it. The measurement was also simply wrong — the lesson is not
37
+ unproven, it is unprovable by this mechanism, and those are different claims.
38
+
39
+ ## How it was found
40
+
41
+ By running `tlc harness doctor` from a second repository after pinning the rule, and reading the output. The
42
+ grammar defect was in the same line.
43
+
44
+ ## Trade-offs
45
+
46
+ **A third reading to explain.** `not-injected`, `session-only`, `unproven`, `helped`, `neutral`. The alternative
47
+ is one reading that is wrong for the most common case.
48
+
49
+ **`session-only` is not actionable.** Deliberately: it states that this lesson's usefulness cannot be measured by
50
+ the current mechanism, which is information, not a task.
51
+
52
+ **Records written before this field read `gradeableCount: 0`**, so an already-graded lesson keeps its `helped` or
53
+ `neutral` reading and an ungraded one drops from `unproven` to `session-only` until it is next injected for a
54
+ gate. No migration; the reading self-corrects.
55
+
56
+ ## Not decided here
57
+
58
+ - **Grading session-start injections.** It would need a notion of "the session went well", which the harness does
59
+ not have. Recorded in AD-039 and still true.
60
+ - **Making an `any` lesson eligible on a retry.** It would be boosted by gates it was never about, which is the
61
+ reason AD-035 chose a neutral gate in the first place.
@@ -0,0 +1,94 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-045 — A gate verdict is reused when the content hash of its inputs did not change"
4
+ description: "A read-only question in a repository with uncommitted work re-ran the whole suite, because the trigger read the state of the tree rather than what the turn did. The verdict is now keyed on a content hash of the command and the files, which is the monorepo-tooling rule: same inputs, replay the result. Making a reused failure advisory instead of blocking was tried and rejected — the suite showed it silences the stagnation rail."
5
+ tags: [decision, gate, cost, caching]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-045 — A gate verdict is reused when its inputs did not change
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **`computeInputsHash` keys a verdict** on the gate command plus each file's repository-relative path, size and
17
+ **contents**.
18
+ 2. **A matching hash reuses the verdict** and the command does not execute.
19
+ 3. **Contents, not mtime.** A `touch`, a branch switch restoring identical bytes, or a clock that moved all change
20
+ mtime without changing anything the gate reads.
21
+ 4. **An unreadable input or a cap overrun is incomplete, and incomplete is never a hit.** Unknown means run.
22
+ 5. **An incomplete hash is not recorded either.** It hashes over the subset it could read, which collides with the
23
+ hash of that subset alone.
24
+ 6. **The gate name is compared as well as the hash**, in a function with its own test, because end to end the
25
+ check is unfalsifiable while the command is part of the key.
26
+ 7. **A reused verdict changes nothing but execution.** It still blocks on failure, still advances the fingerprint,
27
+ still grades lessons.
28
+ 8. **`gate.outcome` carries `reused`,** and the session report counts reused verdicts in their own column and adds
29
+ no time for them.
30
+
31
+ ## Why
32
+
33
+ `stop.ts` decided whether to run the gates from `listChangedRepoFiles` — `git diff HEAD`, `--cached`,
34
+ `ls-files --others`. That is the state of the tree. With any uncommitted work present, **every** turn re-ran the
35
+ suite, including turns that changed nothing.
36
+
37
+ The report: a colleague asked "check the tasks and status" in a repository mid-feature. The agent answered, the
38
+ stop hook ran the suite, returned `BLOCKED: test failed`, and the agent abandoned the question to fix tests nobody
39
+ had asked about. The same mechanism is the likely cause of the earlier "more than thirty minutes running jest"
40
+ report, which was diagnosed as a slow machine and measured as hook latency. It was not the machine, and that
41
+ earlier conclusion is wrong.
42
+
43
+ The 2026 stop-hook pattern acts only when code was modified, detected from the turn's own tool calls. The
44
+ monorepo-tooling rule keys a task result on a content hash of its inputs and replays on a match. This takes the
45
+ second: a hash covers a turn that wrote nothing *and* a turn that wrote through a shell command, which
46
+ transcript-scanning does not.
47
+
48
+ ## The divergence that stays
49
+
50
+ The published guidance is that the hook should detect and decide, not perform the validation — keep it under ten
51
+ seconds and let the agent run the suite when it acts on the block reason.
52
+
53
+ This harness does the opposite deliberately. If the agent executes the verification, it can skip it or misreport
54
+ it, and the whole thesis is that the verdict is not the model's to give. The cost of that choice is what the
55
+ colleague paid; the cache is how it stops being paid for questions rather than for work.
56
+
57
+ ## What was tried and rejected
58
+
59
+ A reused **failure** was going to be advisory — surfaced as context rather than blocking — on the reasoning that
60
+ blocking on a failure the turn did not cause hijacks the operator's question.
61
+
62
+ The suite refused it. Four existing tests failed, and the reason is structural: with the advisory in place a
63
+ reused failure never reaches `failGate`, so `trackFingerprint` never runs, so the **stagnation rail stops firing
64
+ exactly in the case it exists for** — an agent that changes nothing and stops again. An agent could escape a grind
65
+ loop by doing nothing.
66
+
67
+ The discriminator that would make it safe is "was the operator asking, or was the agent told to fix this" — and
68
+ the stop payload does not carry it. `handoff.last_gate_result` and the loop counter are both proxies that break on
69
+ one provider or the other. So the advisory is not shipped, and the cost fix stands on its own: the suite does not
70
+ re-run, which is the part that was expensive.
71
+
72
+ No published source discusses replaying a cached **failure** at all; every example demonstrates a cache hit on a
73
+ passing task. That gap is real and this decision does not close it.
74
+
75
+ ## Trade-offs
76
+
77
+ **Hashing costs a read per changed file per stop.** Bounded at 400 files and 12 MB; over either, the result is
78
+ incomplete and the gate runs. A hash of a few files is orders of magnitude cheaper than the command it replaces.
79
+
80
+ **A verdict can be stale in a way the hash cannot see.** An external input — a database, a network service, an
81
+ environment variable, the clock — can change without any file changing. Turborepo has the same exposure and
82
+ handles it by declaring inputs; here the changed-file set is the declaration, and a gate depending on the world
83
+ outside it will reuse a verdict that no longer holds.
84
+
85
+ **One artifact per repository, not per gate.** `readLastGate` returns the most recent artifact of any gate, so a
86
+ lint run overwrites the test verdict and the next test stop re-runs. Correct but weaker than it could be; a
87
+ per-gate artifact is a bigger change to a file other things read.
88
+
89
+ ## Not decided here
90
+
91
+ - **Advisory for a reused failure.** Above. It needs a signal the harness does not have.
92
+ - **A per-gate artifact**, which would let lint and test both stay cached across a turn.
93
+ - **Declaring extra inputs** — a lock file, a config file, an environment variable — so a gate that depends on more
94
+ than the changed files can say so.
@@ -0,0 +1,111 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-046 — The runtime path is an artifact, and update never touches what it does not own"
4
+ description: "`tlc harness update` failed for a colleague because the harness itself had dirtied the checkout: update rebuilt `dist/` in place, and Bun and esbuild produce different bytes for the same source. The message then named the wrong cause, offered a remedy that runs the command that just failed, and told the operator to `git reset --hard` a path that on a contributor's machine is a symlink to their own repository. Ownership now decides what update may write."
5
+ tags: [decision, update, install, safety]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-046 — The runtime path is an artifact
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **`classifyRuntimePath` names what the runtime path is**: `managed` (a checkout the installer created),
17
+ `linked` (a symlink to someone's clone), `unmanaged` (no `.git`), `absent`.
18
+ 2. **The symlink test comes first**, decided by the path and never by its contents. A linked clone has a `.git`
19
+ too, so testing for that first would classify a contributor's repository as the harness's own.
20
+ 3. **A managed artifact is moved with `git reset --hard`**, not a fast-forward merge. It is the harness's own, so a
21
+ local change in it is not the operator's work and not a conflict for them to resolve.
22
+ 4. **A linked path receives no git command at all** — not fetch, not merge, not reset. Update refreshes only what
23
+ is machine-local (CLI link, skill link, provider hooks) and exits zero.
24
+ 5. **`update` builds only when a bundle is missing.** `dist/` is committed and the gate keeps it matching `src/`,
25
+ so the pulled revision already carries the right bundles.
26
+ 6. **Every message that named a remedy the harness does not perform is deleted.**
27
+ 7. **`doctor` reports the path kind** as one `ok` row for both supported kinds.
28
+
29
+ ## Why
30
+
31
+ Five defects, each measured.
32
+
33
+ **The harness created the dirt.** `update` ran `bin/tlc-build`, which rebuilds `dist/` inside the checkout.
34
+ `tlc-build` uses Bun when present and esbuild otherwise. Measured on the same source:
35
+
36
+ ```
37
+ bun 1.3.14 → 223,390 bytes
38
+ esbuild 0.28.1 → 228,018 bytes
39
+ ```
40
+
41
+ Any toolchain differing from the one that produced the committed bundles leaves the checkout permanently dirty, so
42
+ **every later update fails to fast-forward**. That is every user, not an unlucky one.
43
+
44
+ **The diagnosis was wrong.** Git reported uncommitted local modifications; the message said "the runtime checkout
45
+ has commits that upstream does not". Two different situations, and the operator was sent looking for commits that
46
+ did not exist.
47
+
48
+ **One of the two remedies did not exist.** "Re-run the installer, which replaces the checkout" — `install.sh` runs
49
+ `git -C "$DEST" pull --ff-only`, the command that had just failed. It replaces nothing.
50
+
51
+ **The safety judgement was inverted.** It warned that `reset --hard` "throws work away". What it would have
52
+ discarded was build output the harness generated.
53
+
54
+ **And the remedy was destructive.** `install.sh` links `$DEST` to the clone it was run from, so on a contributor's
55
+ machine `~/.tlc/harness` **is a symlink to their working repository**. Verified on the author's machine while
56
+ diagnosing this. The printed `git reset --hard` would have destroyed uncommitted work. The colleague who reported
57
+ it was safe only because he had installed with the curl one-liner — by install method, not by design.
58
+
59
+ ## The standard this adopts
60
+
61
+ The named smell is conflating a **package artifact** — replaceable, nobody edits it — with a **working tree**, which
62
+ assumes a human resolves conflicts. The published fixes, in order of strength: do not track generated files in the
63
+ install path; revert them before pulling; or replace the pull with a versioned release download and an atomic swap,
64
+ which is what rustup, nvm and deno do.
65
+
66
+ This change adopts the principle those all rest on — **the install path is an artifact the tool owns, and the
67
+ updater never touches anything it does not own** — without the release machinery. `git` stays the transport; what
68
+ changes is that the checkout is now genuinely disposable, because nothing is written into it that upstream does not
69
+ already ship.
70
+
71
+ ## What the demo and CI each caught, after the tests were green
72
+
73
+ **The demo, before pushing.** `resolveHarnessRoot` calls `realpathSync`, and the first version classified that
74
+ resolved path — so a linked clone read as `managed` and `update` ran `git fetch` inside a contributor's repository.
75
+ Sixteen unit tests passed through it, because they called the classifier directly with the path they meant. Only
76
+ driving the real command against a real linked install showed it. Classification now takes the **configured** home.
77
+
78
+ **macOS CI, after pushing.** The fix for that came with a second condition: treat "the path resolves elsewhere" as
79
+ linked too, to catch a symlinked ancestor — justified in a comment with macOS as the example. macOS is what refuted
80
+ it. `/var` is a symlink to `/private/var`, so every path under the system temp directory resolves elsewhere, and a
81
+ **managed** checkout classified as `linked`: updates would have stopped silently on the platform the reporter uses.
82
+ Only the last hop decides now, which is the one thing `install.sh` actually creates, and a regression test asserts
83
+ that a managed checkout under a symlinked ancestor stays managed.
84
+
85
+ Both belong here because the shape repeats: a guess about the filesystem, argued from an example, and only a real
86
+ platform can settle it.
87
+
88
+ ## Trade-offs
89
+
90
+ **A hard reset discards local commits at a managed runtime path silently.** That is the point: it is not a place to
91
+ develop. Anyone developing there has a `linked` install, which is never touched.
92
+
93
+ **No rollback.** A versioned-directory layout would make reverting a symlink flip. Deferred below.
94
+
95
+ **`state/` and `config.json` still live inside the artifact.** They are gitignored, so a reset cannot remove them —
96
+ asserted by a test rather than trusted, because the whole model rests on it.
97
+
98
+ **Building only when a bundle is missing trusts the committed `dist/`.** If upstream ever shipped a stale bundle, a
99
+ Node-fallback user would run stale code until a bundle went missing. `check-dist-fresh` in the gate is what makes
100
+ that impossible by construction, and it is a repository invariant rather than a runtime one.
101
+
102
+ ## Not decided here
103
+
104
+ - **Versioned artifact directories with an atomic swap** (`~/.tlc/versions/<rev>`, `harness → versions/…`). The full
105
+ form of the standard, and it buys rollback. It needs release artifacts in CI, checksum verification, junctions on
106
+ Windows, and a migration for every existing install.
107
+ - **Moving `state/` and `config.json` out of the artifact.** The right end state — user data should not live inside
108
+ a disposable directory — but it is a data migration, and every moved file is a chance to lose an operator's global
109
+ lessons or obs history. It gets its own change, with a verified migration.
110
+ - **Making the build deterministic** by pinning one bundler. It would let `dist/` be rebuilt anywhere without dirt,
111
+ but pinning Bun breaks the esbuild fallback that exists for machines without Bun ([/decisions/ad-012.md](/decisions/ad-012.md)).
@@ -0,0 +1,96 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-047 — An instruction is not an affordance, and a refusal names the route that works"
4
+ description: "The bootstrap told the agent to read `.tlc/harness/state/handoff.json`, a path the floor guards, and the obvious command — `test -f … && head -c 2000 …` — was refused with advice about writing policy. Three layers disagreed: the instruction asked for something the runtime denies, and the denial could not be planned around. The instruction now names `tlc harness handoff`, `test` and `[` join the proven readers, and each refusal carries the remedy for its own case."
5
+ tags: [decision, floor, affordance, agent-experience]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-047 — An instruction is not an affordance
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **`tlc harness handoff [--json]`** is the sanctioned way to read handoff state. Nothing has to reach into the
17
+ protected path.
18
+ 2. **Every instruction naming the handoff names the command**, not the file — session start, session end, and the
19
+ operator bootstrap. The path still appears, as *where state lands*, never as the route.
20
+ 3. **`test` and `[` join the proven readers.** They evaluate a predicate and produce an exit code; neither can write
21
+ a file, which makes them strictly safer than `echo`, already on the list.
22
+ 4. **A policy-surface refusal carries the remedy for its own case.** A read refusal says how to read; a write
23
+ refusal says who may write.
24
+ 5. **Allowlisting a verb does not allowlist a redirect.** `test -f x > config.json` is still denied by the redirect
25
+ rule.
26
+
27
+ ## Why
28
+
29
+ A colleague's agent was told, by the harness, to read the handoff. It ran the natural command:
30
+
31
+ ```
32
+ test -f /…/.tlc/harness/state/handoff.json && head -c 2000 …
33
+ ```
34
+
35
+ `head` is a proven reader. `test` was not on the list, so the segment was denied — and the denial ended with the
36
+ sentence every policy-surface refusal ended with:
37
+
38
+ > Set a gate command with `tlc harness gate test-command` or `gate lint-command`, and run policy changes from your
39
+ > own terminal rather than from inside this session.
40
+
41
+ That is advice about *writing* policy, handed to an agent trying to *read* a file the harness had just pointed it
42
+ at. The operator's report was exact: "ele diz pra usar o handoff, mas tem comandos que não são permitidos — aí
43
+ confunde o agente."
44
+
45
+ Three layers had drifted apart: the instruction text, the set of permitted verbs, and the refusal.
46
+
47
+ ## What the research changed
48
+
49
+ The 2026 literature on agent tool use names both halves of this.
50
+
51
+ The authorization work frames the general rule as **existence is not permission** — a tool being callable is not the
52
+ same as it being authorized, and prompt text is not a boundary. The inverse is the case here: **an instruction is
53
+ not an affordance.** Telling a model to do something is not the same as granting it, and the fix is to stop exposing
54
+ a route the runtime will refuse.
55
+
56
+ The recommended handling of the refusal itself is to *make the deny legible*: return structured, self-describing
57
+ refusals the model can plan around, rather than opaque ones. Work on tool documentation
58
+ (OpaqueToolsBench / ToolObserver) reaches the same place from the documentation side — descriptions are
59
+ load-bearing infrastructure and must cover failure modes, not only the happy path, because a model cannot predict
60
+ what a stateful environment will do.
61
+
62
+ Without that research this would have been fixed by adding `test` to the allowlist and stopping. The allowlist gap
63
+ is the smallest of the three defects; the refusal giving unrelated advice, and the harness pointing at a path it
64
+ guards, are the ones that confused the agent.
65
+
66
+ ## Why `test` was missing, and why adding it is not a loosening
67
+
68
+ The allowlist is deliberately an allowlist: the ways a shell can write are unbounded — `python3 -c`, `perl -pi`,
69
+ `ex`, any interpreter — so enumerating writers guarantees a hole, and enumerating readers puts an unknown verb on
70
+ the deny side without anyone predicting it. `awk` and `sort` are excluded on purpose, because `awk '{print > f}'`
71
+ and `sort -o f` write a file the head verb never reveals.
72
+
73
+ `test` and `[` have no such form. There is no flag, no redirect of their own, no output beyond an exit code. Their
74
+ absence was an incomplete list rather than a decision.
75
+
76
+ ## Trade-offs
77
+
78
+ **Two remedies to keep true instead of one.** A branch that denies without passing a remedy falls back to the write
79
+ advice, which is right for the write cases and wrong for a read case somebody adds later. The sensor covers the
80
+ distinction in both directions.
81
+
82
+ **`tlc harness handoff` is another surface to keep honest.** It reads the same file the hooks read, through the
83
+ facade, so it cannot report something different from what a hook sees.
84
+
85
+ **The bootstrap line is longer.** It now carries the command and the path, because the path is still where state
86
+ lands and an operator looking for the file should find it.
87
+
88
+ ## Not decided here
89
+
90
+ - **Whether the floor should refuse `tlc harness handoff` mid-session.** It reads and writes nothing, so it is not
91
+ in `MUTATING_SUBCOMMANDS`.
92
+ - **Auditing every other instruction the harness emits against what the floor permits.** This one was found by a
93
+ report. A mechanical check — every path named in injected prose is one the floor allows a proven reader on —
94
+ would catch the class, and is not built.
95
+ - **`--json` shape stability.** The report is a projection over the handoff file, and the handoff schema is
96
+ versioned; the projection is not.
@@ -0,0 +1,85 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-048 — A broken updater cannot deliver its own fix, so the installer is the recovery route"
4
+ description: "AD-046 fixed `tlc harness update`, and the fix could not reach anyone who needed it: the new code lives in the revision the broken updater cannot fetch. A colleague updated, got the old message from his stale CLI, and typed `--force`, which was accepted in silence and did nothing. The installer now recovers a managed checkout with a hard reset — the one route that does not run through the installed CLI — and `update` says what `--force` is not."
5
+ tags: [decision, update, install, delivery]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-048 — A broken updater cannot deliver its own fix
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **`install.sh` and `install.ps1` recover a managed checkout** with `fetch` + `reset --hard origin/main`, rather
17
+ than `pull --ff-only`.
18
+ 2. **Neither installer runs git against a linked runtime.** `[[ -L "$DEST" ]]` on Unix and `LinkType` on Windows are
19
+ checked *before* the `.git` test, because `[[ -d "$DEST/.git" ]]` follows a symlink and would have matched a
20
+ contributor's clone.
21
+ 3. **`update` validates its flags.** `--force` is answered with what it is not and what to do instead; any other
22
+ unknown flag is named.
23
+ 4. **The README and the diagnose runbook name the one-liner as the recovery**, because it is the only route
24
+ independent of the installed CLI.
25
+
26
+ ## Why
27
+
28
+ AD-046 was correct and undeliverable.
29
+
30
+ `tlc harness update` runs from the installed runtime. The fix for `update` is in a revision that `update` has to
31
+ fetch and fast-forward to — and the bug being fixed is precisely that the fast-forward aborts. So every install that
32
+ had ever rebuilt `dist/` with a different bundler was stuck at its current revision, and the new code could not
33
+ arrive.
34
+
35
+ The report shows it exactly. A colleague ran `tlc harness update --force` and got:
36
+
37
+ ```
38
+ Updating 297b623..b453d8b
39
+ error: Your local changes to the following files would be overwritten by merge:
40
+ dist/… (17 bundles)
41
+ update: fast-forward failed (origin/main) — the runtime checkout has commits that upstream does not.
42
+ or re-run the installer from the README, which replaces the checkout
43
+ ```
44
+
45
+ Three separate things are visible in that output:
46
+
47
+ - **The message is one this repository no longer contains.** `ffFailureMessage` was deleted in AD-046. He was
48
+ running the old CLI, which is the proof that the fix had not landed and could not.
49
+ - **`--force` was swallowed.** `route` filtered flags for every other command and not for `update`, so a flag that
50
+ does not exist was accepted without a word. He had every reason to read the unchanged failure as a refusal to
51
+ force.
52
+ - **The remedy it offered still did not work.** "Re-run the installer, which replaces the checkout" —
53
+ `install.sh` ran `git pull --ff-only`, the same command that had just aborted. AD-046 deleted that sentence from
54
+ the CLI and left the installer as it was, so the sentence stopped being printed without becoming true.
55
+
56
+ ## What this says about the shape of the change
57
+
58
+ A self-updating tool needs a recovery path that does not run through the thing being updated. `curl | bash` is that
59
+ path here: it is fetched fresh from upstream every time, so it carries the current logic regardless of what is
60
+ installed. That makes the installer, not the CLI, the place where "get me to a known-good state" has to work.
61
+
62
+ The same reasoning is why the deferred versioned-artifact layout in AD-046 matters more than it looked: with
63
+ `~/.tlc/versions/<rev>` and a symlink swap, recovery is a link flip and there is nothing to reconcile. This change
64
+ does not build that; it makes the route that exists actually work.
65
+
66
+ ## Trade-offs
67
+
68
+ **The installer now discards local changes at a managed runtime path.** That is the ownership model of AD-046
69
+ applied where recovery happens. `config.json` and `state/` are gitignored, so a reset cannot reach them — asserted
70
+ by a test that writes both and checks they survive, because the whole recovery rests on it.
71
+
72
+ **Existing installs need one manual step.** There is no way around it: the code that would do it for them cannot be
73
+ delivered by the mechanism that is broken. The step is the documented install one-liner.
74
+
75
+ **`--force` is refused rather than implemented.** A managed runtime is already hard-reset, and a linked clone must
76
+ never be written to, so there is nothing left for a force flag to mean. Refusing it with that explanation is more
77
+ honest than accepting it as a synonym.
78
+
79
+ ## Not decided here
80
+
81
+ - **Versioned artifact directories with an atomic swap.** Still the full form, still deferred, and this decision is
82
+ the second argument for it.
83
+ - **A check that the CLI and the installer agree.** They now implement the same ownership rule in TypeScript and in
84
+ bash and PowerShell, three times. Nothing enforces that they stay in agreement.
85
+ - **Telling an operator their runtime is behind.** `update --check` reports it, and nothing surfaces it unprompted.
@@ -0,0 +1,66 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-049 — An empty synced file says which of four reasons made it empty"
4
+ description: "An operator reported that the harness never updates its lessons file. It was being rewritten every session with the same sentence — \"No active project lessons yet.\" — because lessons were switched off. That sentence covered four different situations: off, on-but-nothing-repeated, candidates-not-promoted, and active-but-withheld. Each now names itself."
5
+ tags: [decision, lessons, visibility]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-049 — An empty synced file says which of four reasons made it empty
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **`emptySyncReason` names the reason** the synced markdown has no lessons in it, from the store and the config.
17
+ 2. **Four distinct reasons**, asserted distinct by a test: the capability is off; on with an empty store; candidates
18
+ recorded but none promoted; active but withheld.
19
+ 3. **The switched-off reason names the config field and the route to change it** — the init skill, not the file.
20
+ 4. **`renderLessonsMarkdown` takes the whole config** rather than one number from it, because the reason needs
21
+ `enabled` and `promoteHitCount` too.
22
+ 5. **The docs name `.specs/LESSONS.md` as belonging to the `/sdd` skill**, so the two files stop being compared.
23
+
24
+ ## Why
25
+
26
+ The report: "uma coisa que eu percebi é que ele não tá atualizando nunca esse arquivo, mas atualiza o `.specs` do
27
+ tlc." Followed by "mandei um init all default".
28
+
29
+ Both halves are informative.
30
+
31
+ `intelligence.lessons.enabled` defaults to **false**, and an all-defaults init leaves it off. With
32
+ `syncRulesFile` on, `gardenAndPersistLessons` rewrote the file at every session end — with the same empty text,
33
+ forever. From the outside that is indistinguishable from a file nothing writes, which is exactly how it was
34
+ reported.
35
+
36
+ And the sentence could not have told him anything, because it was the same sentence for four situations. Reaching a
37
+ non-empty file requires all of: the capability on, the **same** gate failure repeating inside one session
38
+ (`hits >= 2`), and the resulting candidate promoted across `promoteHitCount` distinct sessions — `rankLessonsForSync`
39
+ filters on `status === "active"`. Three hurdles, one message.
40
+
41
+ The other half of the report is a naming collision the harness does not own. `.specs/LESSONS.md` is the `/sdd`
42
+ skill's own lessons layer. Seeing one update and the other not is reasonable evidence that the second is broken,
43
+ and the answer is documentation rather than code: the harness must not couple to another tool's files, and AD-035
44
+ already refused to read a project's documentation at all.
45
+
46
+ ## Trade-offs
47
+
48
+ **Four strings to keep true.** A fifth state — say, every lesson quarantined — falls through to the original
49
+ sentence. The test asserts the four are distinct, not that the set is complete.
50
+
51
+ **The reason is computed on every sync.** It reads the store that was just gardened and does no extra I/O.
52
+
53
+ **It says "ask the agent to run the harness-init skill" rather than naming a command.** There is no CLI to toggle a
54
+ capability, because `tlc harness` mutations are the operator's from a terminal ([/decisions/ad-022.md](/decisions/ad-022.md)) and the init skill is the
55
+ sanctioned interactive route. A line pointing at a command that does not exist would be the AD-047 defect again.
56
+
57
+ ## Not decided here
58
+
59
+ - **Whether the file should be written at all when lessons are off.** It is, and it says so. Not writing it would
60
+ leave `syncRulesFile: true` silently doing nothing, which is the same defect one layer down.
61
+ - **Surfacing the off state in `doctor`.** `checkLessonHealth` is silent when the capability is off, deliberately —
62
+ a row about a disabled feature is noise on every healthy run ([/decisions/ad-034.md](/decisions/ad-034.md)). The synced file is the place an operator
63
+ who enabled `syncRulesFile` is already looking.
64
+ - **Whether `promoteHitCount: 2` is the right default** for a capability whose first visible output needs two
65
+ sessions. It is the recurrence threshold AD-038 argued for; the interaction with time-to-first-output is not
66
+ measured.