@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,94 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-050 — Lesson transport is a provider capability, not an operator preference"
4
+ description: "A question about duplicated lessons found the opposite of duplication: on the host that drops context returned from its session-start hook, the durable rules file was the only route to the model, and it was off by default. Transport becomes a declared capability, the advisory stops being rendered into a field that host ignores, and the cost report names which of the two is paid."
5
+ tags: [decision, lessons, providers, capabilities, observability]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-050 — Lesson transport is a provider capability, not an operator preference
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **`sessionStartContextReliable` is declared per provider** and states whether context returned from the
17
+ session-start hook reaches the model. It is not the same question as whether the field exists.
18
+ 2. **`contextAtStop` is declared per provider** and read by `degrade`, so a `context` decision raised on `stop`
19
+ abstains where the host has no field to carry it.
20
+ 3. **`intelligence.lessons.syncRulesFile` becomes `auto` | `always` | `never`, defaulting to `auto`.** Under `auto`
21
+ the durable view is written where the host does not deliver hook context, and withheld where it does.
22
+ 4. **The legacy boolean is coerced** — `true` reads as `always`, `false` as `never` — and `lessons list` names the
23
+ coercion once, next to the value it produced.
24
+ 5. **`durableViewVerdict` takes the capability as data** and returns a reason, following `appendFilesVerdict`. Core
25
+ does not import a provider to decide it.
26
+ 6. **The durable view is written at session start as well as session end**, from `allLessons` rather than from the
27
+ project store, filtered by `appliesHere` as well as `isInjectable`.
28
+ 7. **The cost report says which transport is paid.** `durable_chars` is measured from the file that was written and
29
+ `hook_context_reliable` records what the provider declares.
30
+ 8. **`session.start` keeps carrying context on every provider**, because the drop is a race rather than a refusal.
31
+
32
+ ## Why
33
+
34
+ The question was whether it is correct to hold the same lessons in `.cursor/rules/harness-lessons.mdc` and in
35
+ `.tlc/harness/lessons.md`. Measuring it found the reverse of duplication.
36
+
37
+ Cursor's own staff answered it, on the `sessionStart` `additional_context` bug report (forum thread 158452,
38
+ 2026-04-20): *"This is a bug on our side. `additional_context` from `sessionStart` gets dropped due to a timing issue
39
+ between when the hook runs and when the composer handle is created."* `env` from the same payload arrives, because
40
+ it follows a different code path. It was reported again against 3.14.7 on 2026-08-02 (thread 167274), and no
41
+ changelog entry fixes it.
42
+
43
+ So on that host the hook route is unreliable and the rules file is the durable one — which AD-011 item 4 had already
44
+ reasoned, without a citation. What nobody had noticed is that `syncRulesFile` defaulted to `false`. An all-defaults
45
+ init with lessons switched on therefore delivered lessons by **no** route at all, which is consistent with the
46
+ report that opened AD-049. One boolean was deciding transport for two hosts with different capabilities: on the one
47
+ that delivers hook context it was a convenience, and on the one that does not it was the only route.
48
+
49
+ Two further defects came out of reading the schemas rather than assuming them.
50
+
51
+ Cursor's `stop` output schema carries `followup_message` and nothing else, and `cursorRender` maps every `context`
52
+ decision to `additional_context`. The docs gate's non-blocking advisory was therefore rendered into a field that
53
+ host ignores. The test covering it asserted the text on a Cursor stop event, which is how it passed. `degrade`
54
+ already abstained a `context` decision a provider could not carry — it only knew about `tool.before` and
55
+ `tool.after`. Claude Code's `Stop` *does* accept `hookSpecificOutput.additionalContext` for feedback that continues
56
+ the turn, so the advisory survives where it can be read.
57
+
58
+ And the durable view was rendering from `readProjectLessons`, so it carried one of the three tiers AD-040
59
+ introduced. On the host where that file is the only route, every shipped core lesson and every global lesson
60
+ reached nothing.
61
+
62
+ ## Trade-offs
63
+
64
+ **`auto` changes behaviour for an existing Cursor project.** A file appears in `.cursor/rules/` that was not there
65
+ before. That is the fix, and `never` is the way to decline it — which is why the mode has three values rather than
66
+ being inferred outright.
67
+
68
+ **A file written into someone's repo.** `.cursor/rules/` is tracked by default in this project, on the reasoning
69
+ that lessons are worth sharing with a team. In another repo that is the operator's call, and `never` is how they
70
+ make it.
71
+
72
+ **Two transports pay twice when the race is won.** Emitting at `session.start` on a host that usually drops the text
73
+ means that when it does arrive, the same lessons arrive twice on that turn. The alternative is to stop emitting and
74
+ lose the cases that work; the emission is bounded by `maxCharsSession`, so the cost of the overlap is bounded too.
75
+
76
+ **`sessionStartContextReliable` is a claim about someone else's bug.** It will be wrong the day Cursor fixes the
77
+ race, and nothing here will notice. The adapter cites the thread and the version so the claim can be re-checked
78
+ rather than inherited.
79
+
80
+ **`lessons list` carries a migration line.** It retires itself once the config is updated, and until then it is one
81
+ line on a command an operator runs deliberately. `doctor` was the other candidate and was rejected: a row about a
82
+ coerced field on every healthy run is the AD-034 defect.
83
+
84
+ ## Not decided here
85
+
86
+ - **Whether the `alwaysApply` file's real token cost can be reported.** The harness cannot see the assembled prompt.
87
+ It reports the characters it wrote and states that the host is *asked* to include them on every request.
88
+ - **Whether the advisory should reach the operator by another route on a host that cannot carry it.** It is in the
89
+ gate artifact, which is where `tlc harness` reads it. A handoff blocker for a non-blocking advisory was not
90
+ written.
91
+ - **Whether the emission should stop once the Cursor race is fixed.** That is a capability flip, not a code change,
92
+ and the flip needs evidence that does not exist yet.
93
+ - **Whether Claude Code should get the durable pointer by default.** `always` writes it. Under `auto` it does not,
94
+ because the hook delivers and the pointer would be a second copy of a working route.
@@ -0,0 +1,69 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-051 — A warning fails the gate, and a suppression states what breaks without it"
4
+ description: "Three fixable warnings sat in this repository across several green gates, because a warn-level biome rule does not change the exit code. The gate now runs biome with --error-on-warnings, and a new checker fails it on any suppression whose reason is not a reason. Escalating every rule group to error was measured first and rejected."
5
+ tags: [decision, gate, lint, quality]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-051 — A warning fails the gate, and a suppression states what breaks without it
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **The gate runs `biome check --error-on-warnings`.** A warn-level diagnostic fails it like an error.
17
+ 2. **`linter.rules` stays on the `recommended` preset.** Group-level `error` severity was measured and rejected.
18
+ 3. **`tools/check-suppressions.ts` is a gate step.** A `biome-ignore`, `@ts-ignore` or `@ts-expect-error` must carry
19
+ a reason opening with `why:`, `hazard:` or `invariant:` — the words this project already requires of any comment
20
+ that stays — followed by more than four words.
21
+ 4. **`@ts-nocheck` fails regardless of its reason**, because it silences a whole file rather than a diagnostic.
22
+ 5. **The three warnings are fixed rather than suppressed**, and one of them was a real defect.
23
+
24
+ ## Why
25
+
26
+ `npx biome check .` exited 0 while printing "Found 2 warnings. Found 1 info." Warn-level diagnostics do not change
27
+ biome's exit code unless `--error-on-warnings` is passed, so the gate was green and the output said otherwise. Three
28
+ findings had survived that way long enough that nobody knew when they arrived.
29
+
30
+ One was not cosmetic. `linkedRuntimeMessage(dest, target)` accepted `dest` and never used it, so the message read
31
+ "the runtime path is a link to a working clone" without naming the path — while both sibling messages,
32
+ `unmanagedRuntimeMessage` and `resetFailureMessage`, name theirs. An operator with more than one runtime could not
33
+ tell which link was meant. `noUnusedFunctionParameters` had been pointing at it in the warning list.
34
+
35
+ The other two were `useIndexOf` (a `findIndex` with an equality predicate) and `useOptionalChain`.
36
+
37
+ **Group-level severity was tried and measured.** Setting every group to `error` in `biome.json` produced **3763**
38
+ findings, because a group severity enables that group's non-recommended rules as well. Among them: `noBarrelFile`
39
+ and `noReExportAll`, which forbid precisely the core facade this architecture is built on ([/decisions/ad-004.md](/decisions/ad-004.md)), and
40
+ `noNodejsModules` in a Node CLI. Adopting that would have meant a config full of exclusions — the opposite of
41
+ removing clutter — so the curated preset stays and the flag does the enforcing.
42
+
43
+ **`--error-on-warnings` cannot see a rule that was suppressed instead of fixed.** Biome 2 requires text after the
44
+ colon, so `biome-ignore lint/complexity/useIndexOf: needed` parses and says nothing. That is exactly the escape a
45
+ stricter exit code creates pressure to use, which is why the checker ships in the same change rather than later.
46
+
47
+ ## Trade-offs
48
+
49
+ **A prose rule enforced by a regex.** The checker cannot tell a real reason from four words that look like one. It
50
+ raises the cost of a thoughtless suppression; it does not make one impossible.
51
+
52
+ **`__test__` is scanned like everything else.** Excluding it would be the route around the rule. What keeps the
53
+ comment-policy fixtures quiet is `isInComment`, which tracks the opening quote character rather than counting
54
+ quotes — the first version counted, and reported three findings that were all string literals inside this project's
55
+ own tests. A checker whose findings are noise is a checker somebody switches off, so it was calibrated against a
56
+ deliberately injected suppression before being wired in.
57
+
58
+ **Four words is arbitrary.** It was chosen because `why: needed` and `hazard: unavoidable` both pass a
59
+ non-empty test and neither says what breaks.
60
+
61
+ ## Not decided here
62
+
63
+ - **Whether biome's own unused-suppression diagnostic should also fail.** It is part of the recommended preset and
64
+ now fails through `--error-on-warnings`, so no separate decision was needed.
65
+ - **Whether individual non-recommended rules are worth enabling one at a time.** `useConsistentTypeDefinitions`
66
+ (8 findings) would force `interface` on a codebase that uses `type` throughout; `noMagicNumbers` accounts for most
67
+ of the 3763. Neither was adopted and neither was excluded by name — the preset simply does not include them.
68
+ - **Whether the same reason discipline should apply to `.mjs` and shell files.** The checker scans the tracked
69
+ TypeScript surface, which is where suppressions have appeared.
@@ -0,0 +1,69 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-052 — The repository moves to the org and the runtime paths do not"
4
+ description: "The project moves to github.com/tech-leads-club/harness-toolkit. Only the identity layer changes — package name, repository URLs, installer source. The CLI, the runtime home and the project policy path stay exactly as AD-002 set them, because renaming those would re-initialise every install for no gain."
5
+ tags: [decision, naming, install, delivery]
6
+ timestamp: "2026-08-05"
7
+ ---
8
+
9
+ # AD-052 — The repository moves to the org and the runtime paths do not
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-05
13
+
14
+ ## Decision
15
+
16
+ 1. **The repository is `github.com/tech-leads-club/harness-toolkit`**, superseding that one clause of
17
+ [/decisions/ad-002.md](/decisions/ad-002.md). The package is `harness-toolkit`.
18
+ 2. **Nothing else in AD-002 changes.** The CLI is still `tlc` with a `harness` subcommand group, the runtime home is
19
+ still `~/.tlc/harness/`, and the project source of truth is still `.tlc/harness/config.json`.
20
+ 3. **`felipfr/tlc-agent-harness` is frozen as it stands**, not archived and not amended.
21
+ 4. **While the repository is private to the org, the documented install fetches the script through `gh`**, because
22
+ `raw.githubusercontent.com` is unauthenticated and cannot read it. The public one-liner is documented alongside,
23
+ labelled for when it applies.
24
+ 5. **A clone that fails names the credential it needs** — `gh auth login`, `gh auth setup-git`, org membership —
25
+ rather than passing git's own error through.
26
+
27
+ ## Why
28
+
29
+ **The paths stay because renaming them costs users and buys nothing.** `TLC` is Tech Leads Club, so `tlc` and
30
+ `.tlc/` read as *more* accurate under the org than they did under one person's account. Changing them would move
31
+ `~/.tlc/harness`, `TLC_HOME`, `.tlc/harness/config.json` — which is also the literal string the floor's
32
+ `policy-surface-write` rule matches — and every already-initialised project would need a manual re-init. Two
33
+ colleagues are mid-onboarding on the current layout.
34
+
35
+ **The private phase is an install problem, not a code problem.** `install.sh` clones over HTTPS and works against a
36
+ private repository as soon as git has a credential; what does not work is the *bootstrap*, because the one-liner
37
+ fetches the script from `raw.githubusercontent.com` unauthenticated. `gh api …/contents/install.sh` carries the
38
+ credential and reaches the same script, so both phases run identical code and going public later requires no change
39
+ at all.
40
+
41
+ **The old repository is left alone by choice.** It keeps its history, and it keeps receiving `tlc harness update`
42
+ from every install that predates this move — silently, from a repository that has stopped moving. That is a known
43
+ consequence: the affected installs have to re-run the installer against the new repository, and being told is the
44
+ only mechanism, because a stale install cannot learn about a move on its own. This is the same shape as
45
+ [/decisions/ad-048.md](/decisions/ad-048.md), one level up.
46
+
47
+ ## Trade-offs
48
+
49
+ **Two install routes in the docs at once.** One of them does not work today and the other will not be needed later,
50
+ and each is labelled with which. The alternative — documenting only the working one — means the public one-liner has
51
+ to be reinstated in a later commit that nobody will remember to make.
52
+
53
+ **`gh api … | base64 -d | bash` is not memorable.** It is also the only unattended route into a private repository
54
+ that does not ask anyone to paste a token into a shell.
55
+
56
+ **A frozen repository under a personal account still answers `git fetch`.** Anyone who never re-installs stays on
57
+ the last revision it received, and the harness has no way to tell them so.
58
+
59
+ ## Not decided here
60
+
61
+ - **Publishing to npm.** It would remove `curl | bash`, the clone, the private-repository problem and the committed
62
+ `dist/` in one move — and it replaces the update mechanism that AD-046 and AD-048 are built on, so it is its own
63
+ decision rather than part of a move.
64
+ - **When the repository becomes public.** The docs carry both routes so the switch is a visibility change and
65
+ nothing else.
66
+ - **Renaming the local working directory.** It is the operator's filesystem, and a runtime linked to it would follow
67
+ the rename only if the link is re-pointed.
68
+ - **`projectName` in this repository's own `.tlc/harness/config.json`.** It still reads `tlc-agent-harness`. The
69
+ file is on the policy surface, so it is the operator's edit from a terminal ([/decisions/ad-022.md](/decisions/ad-022.md)).
@@ -0,0 +1,78 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-053 — The harness ships no model allowlist, and a list that names nothing enforces nothing"
4
+ description: "An agent read `enforceAllowlist: true` with `allowedModels: []` and concluded that every model was blocked, then offered to switch the rail off. The list it was actually being refused by was shipped by the adapter and already stale. No adapter ships one now, an empty list enforces nothing, doctor reports that combination, and the refusal names the key it came from."
5
+ tags: [decision, subagents, policy, defaults]
6
+ timestamp: "2026-08-06"
7
+ ---
8
+
9
+ # AD-053 — The harness ships no model allowlist, and a list that names nothing enforces nothing
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-06
13
+
14
+ ## Decision
15
+
16
+ 1. **`ProviderPolicyDefaults` no longer carries `allowedModels`**, and no adapter declares one. The effective
17
+ allowlist is exactly what the project configured.
18
+ 2. **An empty or absent list enforces nothing.** `enforceAllowlist` with no entries does not deny.
19
+ 3. **`doctor` reports that combination as a fault**, naming `subagents.allowedModels`, and is silent when the rail
20
+ is off or the list has entries.
21
+ 4. **The refusal names the key the list came from**, and says `inherit` is a value that list may contain when
22
+ `inherit` is what was refused.
23
+ 5. **`blockedPatterns` keeps its shipped `-fast` entries.** It is the opposite mechanism and stays.
24
+
25
+ ## Why
26
+
27
+ The report came from another project, where an agent read:
28
+
29
+ ```json
30
+ "subagents": { "enforceAllowlist": true, "requireModel": false, "allowedModels": [] }
31
+ ```
32
+
33
+ and concluded that every model fell out, `inherit` and a `-thinking-high` variant included. It offered to set
34
+ `enforceAllowlist: false`.
35
+
36
+ The conclusion was wrong and the remedy would have switched off a rail. `effectiveAllowedModels` fell back to
37
+ `provider.policyDefaults().allowedModels` whenever the project's list was empty, so the spawn was refused by a
38
+ **shipped** list — three model slugs on Claude Code, five on Cursor — that appears nowhere in the project. The
39
+ requested model genuinely was not on it, because that list had already gone stale.
40
+
41
+ That a careful reader got it exactly backwards is the finding, not a footnote. The refusal said `Use one of: <list>`
42
+ and named no source. An operator who opens their own config, sees `[]`, and is told a spawn was refused by an
43
+ allowlist can only conclude that empty means none.
44
+
45
+ **No shipped list at all** is the operator's decision, and it removes the cause rather than the symptom: a list the
46
+ harness invents is a list that goes stale, and enforcing one nobody wrote is what made the message unreadable.
47
+
48
+ **An empty list cannot deny**, because it says nothing about what is permitted. With the shipped fallback gone, the
49
+ old code would have refused every spawn from a rule listing nothing — the behaviour the reader mistook for a bug,
50
+ where they would have been right.
51
+
52
+ **`inherit` is not a model name.** It means the parent's model, and before this the only occurrences of the string
53
+ in this repository were `stdio: "inherit"`. It is now an ordinary value an operator may put on the list, and the
54
+ refusal says so instead of answering with slugs.
55
+
56
+ ## Trade-offs
57
+
58
+ **A project that relied on the shipped list loses it.** `enforceAllowlist: true` with no list now permits every
59
+ model instead of the three or five the adapter named. `doctor` fails on exactly that state, which is the only
60
+ signal available — a spawn cannot warn without denying, and denying is what this removes.
61
+
62
+ **`-fast` protection is unaffected, and the asymmetry is deliberate.** `effectiveBlockedPatterns` concatenates the
63
+ project's patterns with the adapter's rather than replacing them, so the `-fast` denial survives an empty project
64
+ config. A blocklist that ships entries adds protection; an allowlist that ships entries invents a policy.
65
+
66
+ **`doctor` gains a `fail` row that some projects will hit immediately.** It is a fault: a rail declared on and
67
+ enforcing nothing is a claim of protection that is not being given.
68
+
69
+ ## Not decided here
70
+
71
+ - **Resolving `inherit` to the recorded parent model and judging that.** More precise than treating it as a listable
72
+ value, and it is inference the operator did not ask for. `blockParentFast` already covers the case that makes
73
+ `inherit` risky.
74
+ - **Whether the init skill should offer a starting list.** It asks; it does not propose slugs, because proposing
75
+ them is shipping a default one conversation later.
76
+ - **Whether `requireModel` should default on.** Unchanged by this decision.
77
+ - **Prefix or wildcard matching for model families.** `isModelAllowlisted` still matches exactly or on `prefix[`,
78
+ so `-thinking-high` needs its own entry. Widening the match is a separate question from who owns the list.
@@ -0,0 +1,98 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-054 — npm is the distribution, and a merged release PR is the only thing that publishes"
4
+ description: "Distribution moves from a git clone of the repository to a published npm package. Release Please computes the version from Conventional Commits and opens a release PR; merging it tags, releases and publishes through npm trusted publishing, with no stored token and no bot commit on main. The runtime path does not move."
5
+ tags: [decision, release, npm, distribution, ci]
6
+ timestamp: "2026-08-07"
7
+ ---
8
+
9
+ # AD-054 — npm is the distribution, and a merged release PR is the only thing that publishes
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-07
13
+
14
+ ## Decision
15
+
16
+ 1. **The product is published as `@tech-leads-club/harness-toolkit`.** The unscoped name was already taken on
17
+ npm by an unrelated package; the scope is the one `@tech-leads-club/agent-skills` already uses.
18
+ 2. **Release Please v4 computes the version** from Conventional Commits, which this repository already writes,
19
+ and opens a release PR. It writes no changelog — `skip-changelog` is set, and
20
+ [/decisions/ad-055.md](/decisions/ad-055.md) says where the changelog comes from instead.
21
+ 3. **Merging that PR is the release.** It tags, creates the GitHub Release, and only then does the publish job
22
+ run. An ordinary push to `main` publishes nothing. *(The clause making that merge a human approval is
23
+ superseded by [/decisions/ad-057.md](/decisions/ad-057.md): the bot merges it.)*
24
+ 4. **Publishing uses npm trusted publishing (OIDC).** No npm token exists anywhere — the workflow requests a
25
+ short-lived credential with `id-token: write`, and provenance is generated automatically from a public
26
+ repository.
27
+ 5. **The publish job rebuilds `dist/` and runs the full gate against the tag** before publishing.
28
+ 6. **No bot commits to `main`.** The one generated file a release touches is corrected inside the release PR.
29
+ 7. **The runtime path does not move.** `~/.tlc/harness` stays what hooks point at, and what holds `config.json`,
30
+ `flags/` and `state/` ([/decisions/ad-002.md](/decisions/ad-002.md),
31
+ [/decisions/ad-052.md](/decisions/ad-052.md)).
32
+
33
+ ## Why
34
+
35
+ **A git clone is not a distribution channel, and this repository has the bug reports to prove it.** Two of the
36
+ last five fixes were to the updater: a checkout that dirtied itself on rebuild and broke every later update
37
+ ([/decisions/ad-046.md](/decisions/ad-046.md)), and an updater unable to deliver its own fix
38
+ ([/decisions/ad-048.md](/decisions/ad-048.md)). Both are properties of running `git` against a user's machine.
39
+ A registry has solved fetch, integrity and rollback already, and neither of those failures has an analogue in
40
+ `npm install`.
41
+
42
+ **Trusted publishing rather than a token, because the deadline is real.** npm is restricting tokens that bypass
43
+ 2FA — account changes in August 2026, direct publishing in January 2027 — so a token-based pipeline would need
44
+ replacing within months of being built. OIDC also produces the provenance attestation for free, which is the
45
+ artifact that says this tarball was built from this commit by this workflow.
46
+
47
+ **A release PR rather than publish-on-merge.** semantic-release publishes on every merge to `main` with nothing
48
+ in between. Release Please's PR is where the version bump and the rendered changelog are assembled before either
49
+ reaches `main`, and it costs nothing to author because the commit convention is already in place. It was also
50
+ where a human could approve; [/decisions/ad-057.md](/decisions/ad-057.md) removed that and the bot merges it,
51
+ which leaves the PR doing the assembly job it was chosen for.
52
+
53
+ **Changesets was the other candidate and lost on shape.** Its strength is monorepos and its cost is a third
54
+ artifact per PR — a changeset file, alongside the commit message and the decision record. This is one package
55
+ with a decision-record habit already. The pause it sells is a pause this repository already takes.
56
+
57
+ **A breaking change stays in `0.x`.** Release Please bumps a breaking change straight to `1.0.0` by default,
58
+ even from `0.1.0`, so a single `feat!:` would have declared this product stable on its first release.
59
+ `bump-minor-pre-major` turns that into a minor bump. Going to `1.0.0` becomes a decision someone makes, which
60
+ is the only way it can mean anything.
61
+
62
+ **Version numbers are now a promise, and the number stays below 1.0.0.**
63
+ [/decisions/ad-003.md](/decisions/ad-003.md) declined backward compatibility and declined a version for exactly
64
+ that reason. A registry requires one. `0.x` is the honest encoding of that stance: it is a version without a
65
+ compatibility promise, which is what AD-003 said this product offers.
66
+
67
+ ## Trade-offs
68
+
69
+ **Publishing makes the source public, whatever the repository's visibility.** The tarball is world-readable, so
70
+ the decision to publish is the decision to open the code. Provenance additionally requires a public repository,
71
+ so publishing from a private one silently drops the attestation — the one thing the pipeline exists to produce.
72
+
73
+ **The licence does not become permissive.** PolyForm Noncommercial 1.0.0 is a valid SPDX identifier and npm
74
+ carries it, but a package on a public registry will be installed by people who never read it.
75
+
76
+ **Two things must be done by hand, once, and the pipeline cannot do them.** A trusted publisher must be
77
+ configured on npmjs.com naming the organisation, the repository and `release.yml`, and the repository must be
78
+ public for provenance. A configuration created after 20 May 2026 must also state explicitly which operations it
79
+ permits.
80
+
81
+ **The version becomes a thing that can be wrong.** A `fix:` commit that changes behaviour ships as a patch and
82
+ says compatibility held when it did not. The gate does not check this and nothing here proposes that it could.
83
+
84
+ **`dist/` is now built twice** — committed for the Node fallback path and rebuilt in the publish job. The
85
+ duplication is deliberate: `check-dist-fresh` proves the committed bundles match `src/`, and a release is the one
86
+ moment where building again costs nothing and removes the question.
87
+
88
+ ## Not decided here
89
+
90
+ - **How an existing install migrates.** Two things were measured as wrong for the npm route: `doctor` reported
91
+ `not a git checkout` as a failure, and the runtime cache was written inside the package directory npm replaces
92
+ on update. Both are settled in [/decisions/ad-056.md](/decisions/ad-056.md).
93
+ - **Whether to publish a library entry point.** Nothing is exported for programmatic use. The package is a CLI
94
+ and a runtime; `src/` ships because Bun executes it directly, not as an API.
95
+ - **Prereleases and dist-tags.** No `next` channel, no beta flow. One tag, `latest`.
96
+ - **Signing beyond provenance.** Provenance attests the build; it is not a signature over the contents by a key
97
+ this project holds, and key management stays out of scope for the same reason
98
+ [/decisions/ad-028.md](/decisions/ad-028.md) declined it for attestations.
@@ -0,0 +1,74 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-055 — The changelog is rendered from the decision records, and git says which release each landed in"
4
+ description: "CHANGELOG.md is generated from docs/decisions/ rather than from commit messages, and the mapping from decision to version is read out of git rather than recorded anywhere. Release Please writes no changelog. The gate fails when the file drifts."
5
+ tags: [decision, release, changelog, docs]
6
+ timestamp: "2026-08-07"
7
+ ---
8
+
9
+ # AD-055 — The changelog is rendered from the decision records, and git says which release each landed in
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-07
13
+
14
+ ## Decision
15
+
16
+ 1. **`CHANGELOG.md` is generated** by `tools/render-changelog.ts` from `docs/decisions/ad-*.md`. It is not
17
+ edited by hand and Release Please does not write it (`skip-changelog: true`).
18
+ 2. **Which release a decision landed in is read from git** — the commit that added the file, and the tag whose
19
+ range contains it. Nothing records it, so nothing can disagree about it.
20
+ 3. **A `migration:` note becomes a "Needs your action" line.** Everything else is a title.
21
+ 4. **`node tools/render-changelog.ts --check` is a gate step**, so the file cannot drift from the records.
22
+ 5. **`--release <version>` renames the pending section**, and the release workflow runs it inside the release
23
+ PR so the merged commit already carries the final document.
24
+
25
+ ## Why
26
+
27
+ [/decisions/ad-031.md](/decisions/ad-031.md) already decided that the substance of a changelog exists here as
28
+ decision records — each carrying why, the trade-offs and what was refused — and that a second index would be a
29
+ thing to maintain and a thing that can go stale. Publishing to a registry does not change that; it only adds a
30
+ requirement that the document exist as a file.
31
+
32
+ **Commit messages describe the commit; decision records describe the decision.** Generating from commits would
33
+ produce a list where `fix(lessons): the migration line names the file the old boolean is actually in` is an
34
+ entry, which tells a reader nothing they can act on. The decision record for the same change says what changed
35
+ and what it costs.
36
+
37
+ **The decision-to-version mapping is already in git.** Writing a version into each record's frontmatter would be
38
+ the second index AD-031 refused, and it would be hand-maintained at exactly the moment — cutting a release —
39
+ when nobody is looking at it. Walking the tag ranges answers the same question from the thing that cannot lie.
40
+
41
+ **`--release` exists because the release PR bumps the version before the tag is created.** Without it, the first
42
+ push after every release would fail its own `--check`: the file on disk would say `Unreleased` while a
43
+ regeneration, now seeing the tag, would say `v0.2.0`. A test asserts the two renderings are byte-identical, so
44
+ this stays true rather than being a claim in a comment.
45
+
46
+ **And `--check` accepts two renderings, because the merge commit is a real window.** `ci.yml` and `release.yml`
47
+ both fire on a push to `main` and run in parallel, so on the commit that merges a release PR the gate reads a
48
+ document naming `v0.2.0` while the tag is still being created in the other workflow. A plain comparison would
49
+ fail there on every release, on a file that is already correct. The tolerance is exactly one state and closes on
50
+ its own: the pending rendering is accepted only while no tag of that name exists, so it cannot hold a stale file.
51
+ A mutant that removes the closing condition kills a test.
52
+
53
+ ## Trade-offs
54
+
55
+ **A change with no decision record is invisible in the changelog.** That is the intent — a change worth telling
56
+ users about is worth a record — but it means a bug fix that warranted no record leaves no trace here. The commit
57
+ history remains the complete account; the GitHub Release carries Release Please's own commit-derived notes for
58
+ anyone who wants them.
59
+
60
+ **The first release lists every decision at once.** Fifty-odd entries under one version, which AD-031 argues is
61
+ the same as no message. It is accepted for exactly one release: subsequent ones carry the handful that landed.
62
+
63
+ **Generating from git makes the tool require git.** It shells out to `git log` and `git tag`, so it runs in a
64
+ checkout and not from an unpacked tarball. It is a build-time tool and never runs in a hook, so the latency rule
65
+ that governs the runtime does not reach it.
66
+
67
+ ## Not decided here
68
+
69
+ - **Grouping entries by kind** (feature, fix, security). Decision records carry tags and none of them is a
70
+ release-note category; inventing one would be a second taxonomy.
71
+ - **Linking each entry to its pull request.** The record links to the code it decided about, which is the more
72
+ durable pointer.
73
+ - **Rendering the changelog for other products.** The harness deliberately reads no project's documentation
74
+ convention ([/decisions/ad-035.md](/decisions/ad-035.md)); this tool is this repository's, not a feature.
@@ -0,0 +1,85 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-056 — The package delivers the runtime, and the runtime path stays where hooks already point"
4
+ description: "npm becomes the install route. The published package is a delivery vehicle: `tlc harness install` materialises the code at ~/.tlc/harness, which keeps config.json, state/ and flags/ out of a directory npm replaces. Update bumps the package and re-materialises; no git command runs against an npm-delivered runtime."
5
+ tags: [decision, install, update, npm, runtime]
6
+ timestamp: "2026-08-07"
7
+ ---
8
+
9
+ # AD-056 — The package delivers the runtime, and the runtime path stays where hooks already point
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-07
13
+
14
+ ## Decision
15
+
16
+ 1. **`install.sh` installs the package globally and then runs `tlc harness install`.** The clone route stays,
17
+ and the installer falls back to it — with a line saying so — whenever npm cannot deliver.
18
+ 2. **`tlc harness install` copies a declared payload** from wherever the package sits into `~/.tlc/harness`.
19
+ Each entry is replaced wholesale, so a file deleted upstream does not survive. Nothing outside the payload
20
+ is touched, which is what keeps `config.json`, `state/` and `flags/` the operator's.
21
+ 3. **The runtime path does not move** ([/decisions/ad-002.md](/decisions/ad-002.md),
22
+ [/decisions/ad-052.md](/decisions/ad-052.md)). Hooks keep naming `~/.tlc/harness/bin/tlc-exec.mjs`, so the
23
+ hot path never resolves anything.
24
+ 4. **An npm-installed launcher defers to that runtime** when one is installed there, and runs from the package
25
+ only while it is not — the package has to be able to run itself in order to install anything.
26
+ 5. **`runtimePathKind` gains `npm`**, told apart by a marker the installer writes rather than guessed from the
27
+ contents. `doctor` reports it as `ok`; `update` bumps the package and re-materialises, and runs no git
28
+ command against it, for the same reason it runs none against a linked clone.
29
+ 6. **The runtime cache is never written into a packaged copy**, and an unwritable home degrades to probing
30
+ rather than throwing.
31
+
32
+ ## Why
33
+
34
+ **A directory npm owns cannot hold state.** `npm install -g` replaces the package directory, so the global
35
+ lesson tier, the cross-repository spool and the runtime cache would be deleted by an ordinary update. This was
36
+ measured, not predicted: the first packed tarball wrote `runtime-cache.json` inside the package on its first
37
+ run, and `doctor` reported `FAIL runtime ownership — not a git checkout` on a perfectly healthy install.
38
+
39
+ **Materialising beats splitting the paths.** The alternative was to teach the runtime that code and data live in
40
+ different places — a change to `runtimeHome()`, the floor's protected paths, the lesson store, the spool and the
41
+ policy loader, all on the hot path, on every machine that already has this installed. Copying four megabytes
42
+ once per update buys the same correctness and touches one new file. The conflation of "where the code is" and
43
+ "where the data is" was only ever safe because the runtime was a checkout the harness owned; the package
44
+ restores that property instead of removing the assumption.
45
+
46
+ **The marker, rather than inference.** A directory with no `.git` was classified `unmanaged`, which `doctor`
47
+ calls a failure and `update` answers with "re-install with the one-liner". A directory can be many things and
48
+ only whatever created it knows which, so the installer says so in a file.
49
+
50
+ **The installer falls back rather than failing.** Before the first publish the package genuinely does not
51
+ exist, and an installer that breaks because a future route is not ready yet is broken by its own roadmap. The
52
+ fallback prints which route it took, so nothing is silent. This was caught by the existing test that pipes the
53
+ installer the way the README documents.
54
+
55
+ ## Trade-offs
56
+
57
+ **The code exists twice.** Roughly four megabytes in global `node_modules` and again under the runtime home.
58
+ The duplication buys a stable path for hooks and a directory npm will not delete, and disk is the cheapest
59
+ thing being traded here.
60
+
61
+ **`~/.tlc/harness` can be stale relative to the installed package.** `npm i -g` alone does not update the
62
+ runtime; `tlc harness install` does, and `tlc harness update` runs both. An operator who bumps the package by
63
+ hand and stops there keeps running the previous runtime, and nothing currently reports that gap.
64
+
65
+ **The npm-installed CLI is not the code that runs.** `tlc` resolves to the package's shim, which defers to the
66
+ materialised runtime. That is the same relationship the git route has — the CLI runs whatever is at the runtime
67
+ home — but it is surprising the first time.
68
+
69
+ **One env var of coupling.** `TLC_ORIGIN` carries where the launcher physically lives, because the resolved home
70
+ is no longer that. `TLC_HOME_FROM_ENV` distinguishes an operator's choice from the launcher's own resolution,
71
+ and it is derived once by the outermost launcher and inherited after that: deriving it twice made the nested
72
+ invocation read the parent's `TLC_HOME` as a choice and install the runtime on top of itself. Measured against
73
+ the packed tarball, and now asserted.
74
+
75
+ ## Not decided here
76
+
77
+ - **Reporting a runtime older than the installed package.** `doctor` could compare the two `package.json`
78
+ versions and say so. Worth doing; it is a check, not a distribution decision.
79
+ - **Removing the git route.** It stays for contributors and for anyone already on it. Whether it is eventually
80
+ retired depends on how the npm route behaves in the field.
81
+ - **A postinstall script.** It would remove the second command, and `--ignore-scripts` is common and postinstall
82
+ scripts are exactly the supply-chain surface this project should not add to.
83
+ - **Windows.** `install.ps1` is unchanged and still clones. The CLI half works — `bin/tlc.mjs` is why the
84
+ published entry points are `.mjs` rather than the shell wrappers — but the installer is not converted and
85
+ neither is CI's coverage of it.
@@ -0,0 +1,68 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-057 — The release PR is a mechanism, not a gate, and the bot merges it"
4
+ description: "Publishing is unattended. Release Please still opens a release PR, because that is where the version bump and the rendered changelog are assembled, and the release bot merges it as soon as they are. Supersedes the human-approval clause of AD-054."
5
+ tags: [decision, release, ci, npm]
6
+ timestamp: "2026-08-07"
7
+ ---
8
+
9
+ # AD-057 — The release PR is a mechanism, not a gate, and the bot merges it
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-07
13
+
14
+ ## Decision
15
+
16
+ 1. **The release is unattended.** A merge to `main` carrying a `feat:` or `fix:` reaches npm with no human
17
+ step. This supersedes item 3 of [/decisions/ad-054.md](/decisions/ad-054.md), which made merging the release
18
+ PR an approval.
19
+ 2. **The release PR stays**, because it is where the version bump and the changelog rendered at the pending
20
+ version are assembled. A `merge` job merges it once the `changelog` job has committed into it.
21
+ 3. **`--squash --auto` first, direct merge as the fallback.** Auto-merge waits for the required checks; a
22
+ repository with none, or with auto-merge disabled, gets the direct merge. Which one happened is printed.
23
+ 4. **The publish job carries `environment: publish`.** Empty, it changes nothing. Given required reviewers it
24
+ becomes an approval again, without touching any code.
25
+
26
+ ## Why
27
+
28
+ **The operator asked for it, having been told the cost, and restated it.** That is the whole of the reason,
29
+ and recording it as anything else would be dressing up someone else's call as an engineering finding. The
30
+ sibling repository `agent-skills` already publishes this way, and one product in an organisation releasing by
31
+ hand while the other does not is a difference nobody chose.
32
+
33
+ **Merging beats replacing the PR flow.** The alternative was to publish directly from `main` the way
34
+ `agent-skills` does — compute the version, commit the bump, tag and publish in one run. That discards the
35
+ changelog-in-PR mechanism ([/decisions/ad-055.md](/decisions/ad-055.md)) and puts the bot's bump commit on
36
+ `main` from a workflow rather than through a reviewed branch. Merging a PR the bot opened costs one job.
37
+
38
+ **It terminates, and that is a property rather than a hope.** The merge is a push to `main`, which runs the
39
+ workflow again; that run finds the merged PR, creates the tag and the release, and the publish job runs off it.
40
+ A third run finds no releasable commit — the changelog commit is `docs(...)`, which is not a releasable type —
41
+ so no PR is opened, the merge job is skipped, and nothing re-triggers.
42
+
43
+ ## Trade-offs
44
+
45
+ **A bad release reaches other people's machines with nobody having looked.** This runtime installs itself into
46
+ editors and writes their hook files. Two defences remain and neither is review: the full gate runs against the
47
+ tag inside the publish job before `npm publish`, and CI runs on the release PR before the merge — which works
48
+ only because the App token opens it, since a PR opened with `GITHUB_TOKEN` triggers no workflows.
49
+
50
+ **The fallback merges without waiting for checks.** `--auto` needs required checks configured on `main`; with
51
+ none, there is nothing to wait for and the direct merge is what happens. Configuring CI as a required check on
52
+ `main` is what turns that branch off, and it is a repository setting rather than a change here.
53
+
54
+ **Recovering a failed publish is manual.** If the gate fails inside the publish job the tag and the GitHub
55
+ Release already exist and npm does not. Re-running the whole workflow does not help — `release_created` is
56
+ false on a second pass — so the recovery is "Re-run failed jobs", which preserves the upstream outputs.
57
+
58
+ **The version is now decided entirely by commit messages.** A `fix:` that changes behaviour ships as a patch
59
+ and says compatibility held when it did not. Nothing checks this, and with the human step gone there is no
60
+ longer a moment where someone would notice.
61
+
62
+ ## Not decided here
63
+
64
+ - **Required checks on `main`.** A repository setting that would make the auto-merge path the real one. Worth
65
+ doing; not a code change.
66
+ - **Reviewers on the `publish` environment.** The hook is in place and deliberately empty.
67
+ - **A cool-off or a `next` dist-tag.** One channel, `latest`, published immediately
68
+ ([/decisions/ad-054.md](/decisions/ad-054.md)).