@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,111 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-066 — Uninstall reads the artefact, and the plan is the confirmation"
4
+ description: "The harness merges eleven hook groups into a settings.json that also holds the operator's permissions, sandbox and env, and offered no way out. The established answer is a receipt written at install time; that record would desync on the first npm upgrade or hand-edit, and every artefact this installer creates already names itself."
5
+ tags: [decision, install, safety, operator]
6
+ timestamp: "2026-08-12"
7
+ ---
8
+
9
+ # AD-066 — Uninstall reads the artefact, and the plan is the confirmation
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-12
13
+
14
+ ## Decision
15
+
16
+ 1. **Ownership is read from the artefact, never from a record.** No manifest is written at install time and none
17
+ is required at uninstall time.
18
+ 2. **A shared file is un-merged, never deleted.** `settings.json` keeps every key and every foreign hook group;
19
+ an event left with no groups loses its key rather than keeping an empty array.
20
+ 3. **A symlinked runtime home is unlinked and never followed.**
21
+ 4. **The split install already draws is the split uninstall honours.** `RUNTIME_PAYLOAD` goes, `OPERATOR_OWNED`
22
+ stays, and `--purge` is a separate word that `--yes` never implies.
23
+ 5. **The plan is the confirmation and there is no prompt.** Without `--yes` the command prints the full plan and
24
+ changes nothing.
25
+ 6. **The npm global package and per-project `.tlc/` are reported, not removed.**
26
+
27
+ ## Why
28
+
29
+ **The exit was hand-editing a file most operators should not hand-edit.** `~/.claude/settings.json` carries
30
+ `permissions`, `sandbox`, `env` and `statusLine` next to our hook groups. Reverting the install meant finding
31
+ eleven groups in that file by eye. An install with no exit is one people try on one machine and never a second.
32
+
33
+ **The established pattern is a receipt, and it is the wrong one here.** CMake's `install_manifest.txt` is the
34
+ minimal form; package managers write the same record into a local store, and the schema people converge on
35
+ carries the path, a content hash, a `pre_existed` flag and the artefact type. The sharpest statement of the case
36
+ comes from a project that **rejected** an uninstall command outright: never infer ownership, because a bulk
37
+ command guessing which files are safe to remove will eventually guess wrong, and `--yes` turns that guess into a
38
+ recursive delete.
39
+
40
+ That objection is right about inference and does not describe this installer. Every artefact here names itself —
41
+ a hook group whose command *is* the launcher path, a symlink resolving inside the runtime home, a `hooks.json`
42
+ carrying the marker `isCursorWired` already checks. Matching an exact launcher path is not a heuristic. And a
43
+ receipt would be **strictly worse**, because the install routes are npm, git clone and a symlink to a working
44
+ checkout: three ways for a written record to desync from the disk on the first `npm i -g`, `git reset --hard` or
45
+ hand-edit. `isHarnessGroup` cannot desync — it reads what is there.
46
+
47
+ So the receipt's findings are adopted and its mechanism is not. `pre_existed` becomes "this group does not carry
48
+ the marker". "Skip a modified file" becomes "never follow a link into a checkout". "Remove a directory only when
49
+ empty" becomes the `OPERATOR_OWNED` split, which
50
+ [/decisions/ad-056.md](/decisions/ad-056.md) had already drawn for the installer.
51
+
52
+ **The symlinked home is the reason this is careful rather than a one-liner.** `~/.tlc/harness` is a link to the
53
+ contributor's working clone on every development machine, including the one this was written on. `rm -rf`
54
+ follows it and deletes the repository. `install.sh` refuses to touch a symlinked runtime for exactly this reason
55
+ ([/decisions/ad-046.md](/decisions/ad-046.md)), and the uninstall inherits the refusal: `unlinkSync`, never
56
+ `rmSync` recursive.
57
+
58
+ **No prompt, because the operator reaching for this may have no terminal.** A confirmation prompt needs a TTY and
59
+ assumes an interactive session; the person undoing an install is as likely to be in CI, or in a shell the harness
60
+ itself has made unpleasant. Printing the plan and requiring `--yes` gives the same protection, works headless,
61
+ and leaves the plan in the log of whatever ran it.
62
+
63
+ **The npm package is reported rather than removed** because a global prefix owned by root needs `sudo`, and an
64
+ `npm uninstall` failing halfway through a teardown leaves a worse state than one that never started. Per-project
65
+ `.tlc/` is reported for a different reason: finding them means walking the operator's disk, which is a larger
66
+ risk than the residue.
67
+
68
+ ## Trade-offs
69
+
70
+ **Running it found a defect the tests would not have.** On the machine it was written on,
71
+ `~/.claude/skills/harness-init` pointed at a `/tmp` install deleted weeks earlier. Under a target-based ownership
72
+ rule that dangling link reads as "not ours" and survives every uninstall forever. So the two link families now
73
+ answer to different rules: `tlc` on PATH is a name anybody may own and keeps the target test; `skills/harness-init`
74
+ is a path only this installer writes, so its **location** is the identification. Two rules is worse than one, and
75
+ leaving residue that carries our name is worse than two rules.
76
+
77
+ **A file that does not parse is left alone and reported.** The un-merge cannot preserve what it cannot read, and
78
+ the alternative — rewriting from a template — is precisely the data loss this exists to avoid. The operator gets
79
+ the path and the parse error.
80
+
81
+ **Two CI failures paid for the cross-platform rules, and both were the same mistake.** macOS: `realpathSync`
82
+ throws on a path whose tail is absent, so a dangling link fell back to its literal text and was compared against
83
+ a home that had been resolved — and the OS temp directory there sits under `/var`, a symlink to `/private/var`.
84
+ Windows: ownership was decided with `target.startsWith(`${root}/`)`, and that platform separates with `\`. In
85
+ both cases the harness's own artefact read as somebody else's and survived. One rule now: resolve both sides the
86
+ same way, and compare with `relative` rather than a string prefix. The path API is a parameter, so the win32
87
+ rules are asserted from any machine.
88
+
89
+ **And `install.ps1` does not write what `install.sh` writes.** It resolves the home from `USERPROFILE`, copies
90
+ `tlc.cmd` instead of linking it, and places one skill junction at `~/.tlc/skills/harness-init` rather than one
91
+ per provider directory. Reading the POSIX layout on Windows finds none of it and reports a clean machine, which
92
+ is the worst answer an uninstaller can give. A regular file is now identified by the launcher marker its one
93
+ command line carries, the same way every other artefact identifies itself.
94
+
95
+ **An artefact the operator renamed is invisible.** Ownership read from the artefact means an artefact that no
96
+ longer identifies itself is not ours to remove. This is the honest cost of decision 1, and it is quieter than a
97
+ receipt's failure mode, which is deleting something that has since become somebody else's.
98
+
99
+ **A partly-applied run leaves a mixed state, and that is why every step is idempotent.** A second run resumes
100
+ from wherever the first stopped and reports nothing to do once there is nothing left — including a link left
101
+ dangling by a payload removal that ran before it.
102
+
103
+ ## Not decided here
104
+
105
+ - **Removing the harness from repositories.** `.tlc/` exists in every project it ran in. Searching for them is
106
+ the risk; the plan names the one-line removal instead.
107
+ - **Whether the Windows launcher copy is a bug.** `install.ps1` copies `bin\tlc.cmd` into the bin directory, and
108
+ that file resolves the launcher as `%~dp0tlc-exec.mjs` — relative to itself, where no launcher sits. The
109
+ uninstall removes the copy correctly; whether the install should have linked it is a separate question.
110
+ - **A `tlc harness reinstall`.** The one-liner already does it, and a verb that means "uninstall then install"
111
+ would need to decide what happens when the first half fails.
@@ -0,0 +1,64 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-067 — A reserved file that cannot be retired is rendered, and the gate holds it there"
4
+ description: "docs/log.md is a reserved file of the OKF v0.1 bundle, so it cannot be deleted as a duplicate. Hand-maintained, it had drifted to 19 of 66 decision records. It is now rendered from the same files the changelog reads, with a gate step that fails on drift."
5
+ tags: [decision, docs, gates]
6
+ timestamp: "2026-08-12"
7
+ ---
8
+
9
+ # AD-067 — A reserved file that cannot be retired is rendered, and the gate holds it there
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-12
13
+
14
+ ## Decision
15
+
16
+ 1. **`docs/log.md` is generated**, from the same `docs/decisions/` files the changelog reads.
17
+ 2. **A fourteenth gate step**, `render-log.ts --check`, fails on drift.
18
+ 3. **A record with no `timestamp` is dropped, never filed under a guessed date.**
19
+ 4. **Days newest first, records within a day in id order** — because AD-064 was taken before AD-065, and
20
+ reading them the other way round inverts the reasoning.
21
+
22
+ ## Why
23
+
24
+ **It had drifted to 19 of 66 and nobody noticed.** The log stopped at AD-017 with AD-051 and AD-052 grafted on
25
+ afterwards. Forty-seven records were missing, so a document describing itself as "the chronological record"
26
+ was 71% incomplete — worse than absent, because it reads as complete.
27
+
28
+ **Retiring it was the first answer and it was wrong.** The log duplicates `CHANGELOG.md`, which is already
29
+ rendered from the same records precisely so nobody maintains it by hand
30
+ ([/decisions/ad-055.md](/decisions/ad-055.md)) — so deletion looked obvious. Reading
31
+ `tools/check-docs-bundle.ts` before acting found `RESERVED_FILES = new Set(["index.md", "log.md"])`:
32
+ `log.md` is a reserved file of the OKF v0.1 format this bundle adopted
33
+ ([/decisions/ad-013.md](/decisions/ad-013.md)), with its own validation rule requiring ISO 8601 headings.
34
+ Deleting it would have left the bundle claiming a format it no longer follows.
35
+
36
+ **So the duplication is the constraint, not the defect.** The format requires two views of the same records:
37
+ `log.md` by date, `decisions/index.md` by number, and `CHANGELOG.md` by release. Three hand-maintained views
38
+ is three things to forget. Three rendered views is one source and no memory.
39
+
40
+ **The gate step is what makes it true tomorrow.** `render-capabilities --check` and `render-changelog --check`
41
+ already exist for exactly this, and both have caught staleness in this repository. A generated file with no
42
+ check is a hand-maintained file with extra steps.
43
+
44
+ ## Trade-offs
45
+
46
+ **The log lost its prose.** The old entries carried narrative — what was measured, what was rejected — that a
47
+ title cannot. That narrative is in the decision record itself, which the entry links to, and the alternative
48
+ was 47 entries of narrative invented by whoever noticed the gap.
49
+
50
+ **`DecisionSummary` grew a field.** `timestamp` is now read alongside `title` and `migration`. It is optional,
51
+ so the changelog and the update digest are unaffected, and the reader was already parsing the frontmatter this
52
+ field lives in.
53
+
54
+ **Fourteen gate steps is a lot of steps.** Each one exists because something drifted while nobody was looking,
55
+ and the whole set runs in under thirty seconds. The number is a symptom of the checks being cheap, not of the
56
+ gate being heavy.
57
+
58
+ ## Not decided here
59
+
60
+ - **Whether `CHANGELOG.md` should move into the bundle.** It sits at the repository root because npm and GitHub
61
+ both look for it there, and a bundle-relative link cannot reach it. The log now names it in prose.
62
+ - **Rendering `decisions/index.md` as well.** It is the third view of the same records and the only one still
63
+ hand-maintained. It was complete at 66 of 66 when this was written, so there is no evidence of drift to act
64
+ on yet.
@@ -0,0 +1,79 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-068 — A directory decides what ships, and dist is derived from disk in both directions"
4
+ description: "A hand-maintained array named four development-only checks while ten qualified, so six that validate only this repository were bundled and copied into every install. The exclusion is now a directory, the payload copy honours it, and a bundle whose source is gone is pruned rather than shipped forever."
5
+ tags: [decision, packaging, boundaries, build]
6
+ timestamp: "2026-08-13"
7
+ ---
8
+
9
+ # AD-068 — A directory decides what ships, and dist is derived from disk in both directions
10
+
11
+ - **status**: active
12
+ - **date**: 2026-08-13
13
+
14
+ ## Decision
15
+
16
+ 1. **`tools/dev/` is the declaration.** Every check that validates this repository's own architecture, docs or
17
+ conventions lives there, and the build reads one directory level, so nothing under it can become a bundle.
18
+ 2. **The payload copy honours the same boundary.** `installRuntime` filters `tools/dev` and `tools/__test__`, and
19
+ the published `files` list excludes them.
20
+ 3. **The clone route is deliberately different.** A checkout is the repository; a contributor needs these.
21
+ 4. **A bundle whose source no longer exists is pruned by the build.**
22
+
23
+ ## Why
24
+
25
+ **The array named four and ten qualified.** `bin/tlc-build` carried
26
+ `DEV_ONLY=(check-boundaries check-docs-bundle check-dist-fresh render-capabilities)`. Measured against the
27
+ repository as it stood: `check-wiring`, `check-screens`, `check-obs-contract`, `check-suppressions`,
28
+ `render-changelog`, `render-log` and `eval-skill-triggers` were all bundled into `dist/` and copied into every
29
+ install. Each validates something a user's project does not have — our module boundaries, our screen contract,
30
+ our observability kinds, our decision records.
31
+
32
+ **And the file says why that shape fails, two lines above it:**
33
+
34
+ > Derived from disk, never hardcoded: a fixed list silently stops building a new entrypoint, and the missing
35
+ > bundle only surfaces when a hook fires in production.
36
+
37
+ The principle was stated and then not applied to the exclusion. This is the same shape as every defect
38
+ [/decisions/ad-065.md](/decisions/ad-065.md) collected — a consumer that does not grow when the producer does —
39
+ with the list playing the consumer.
40
+
41
+ **Source ships too, and that was the larger half.** Excluding a bundle is not enough: `RUNTIME_PAYLOAD` copies
42
+ `tools` wholesale and the npm `files` list published it, so the TypeScript source arrived intact. With Bun
43
+ present the launcher resolves an entry straight from source, so `tlc-exec render-log` on a user's machine would
44
+ have rewritten files inside their runtime home.
45
+
46
+ **A directory cannot be forgotten.** A new checker either sits in `tools/dev/` and never ships, or sits in
47
+ `tools/` and does. There is no third state and nothing to remember, which is what separates this from the array
48
+ it replaces.
49
+
50
+ **dist only ever grew.** The build derived what to *compile* from disk and never what to *remove*, so a bundle
51
+ whose source moved stayed on disk, stayed committed, and stayed shipped — invisible to `check-dist-fresh`,
52
+ because an unchanged file produces no diff. Pruning made the move visible: seven bundles left, 31 to 24.
53
+
54
+ ## Trade-offs
55
+
56
+ **Two directories now mean two things by their names.** `tools/` is the product, `tools/dev/` is the repository,
57
+ and the difference is only obvious once you know. The alternative was a marker inside each file, which is a
58
+ convention a reader can violate silently; a path is checked by the build every time.
59
+
60
+ **The tests stay flat in `tools/__test__/`.** They now reach across into `../dev/`, which is slightly worse to
61
+ read than co-location. The gate's glob is flat by design, and a second glob is a second thing to keep in step —
62
+ the exact failure being fixed.
63
+
64
+ **Pruning deletes files the operator did not ask to delete.** It is bounded to `dist/*.mjs`, which the build owns
65
+ entirely and rewrites on every run, and it names each file as it goes.
66
+
67
+ **A contributor's clone still has everything, so the boundary is untested there.** The sensor covers the two
68
+ product routes — the payload copy and the published file list — because those are the ones a user receives.
69
+
70
+ ## Not decided here
71
+
72
+ - **Whether `eval-skill-triggers` should exist.** Nothing invokes it: not the gate, not CI, not a document. It
73
+ has a test, so it is exercised but not used. It moved to `tools/dev/` as the conservative placement; whether it
74
+ earns its place is a separate question.
75
+ - **Whether `docs/` should be filtered too.** The runtime payload copies our decision records so
76
+ `tlc harness update` can tell an operator what changed in the harness. That is the product's own release notes,
77
+ not a convention imposed on anyone, and it stays.
78
+ - **A check that fails when a shipped tool imports from `tools/dev`.** `tsc` catches the broken path today
79
+ because the directories differ; a rename could reintroduce the coupling without a type error.
@@ -0,0 +1,74 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-069 — A decision record declares its shape, and cites by link so a move cannot break it"
4
+ description: "Sixty-seven records, all claiming to be current, with the body shape held only by habit and the citation form 96% consistent. The shape is now checked, the older twenty are a ratchet that may only fall, and a bare parenthesised id fails wherever a link is the convention."
5
+ tags: [decision, docs, gates]
6
+ timestamp: "2026-08-13"
7
+ ---
8
+
9
+ # AD-069 — A decision record declares its shape, and cites by link so a move cannot break it
10
+
11
+ - **status**: active
12
+
13
+ ## Decision
14
+
15
+ 1. **Four headings are required**: `## Decision`, a heading beginning `## Why`, `## Trade-offs`, and
16
+ `## Not decided here`. Bespoke sections between them stay free-form.
17
+ 2. **The older twenty are a ratchet**, not an exemption. The count off the current shape starts at 20 and the
18
+ gate fails if it rises **or** if it falls without the recorded budget following.
19
+ 3. **A record is cited by link**, in documentation, in comments, and in test names.
20
+ 4. **`status` is a closed set**, and it must agree with the folder the record sits in.
21
+ 5. **The rule has no exemption list.** Its own examples are written so they do not match.
22
+
23
+ ## Why
24
+
25
+ **The shape was a boundary, not a habit, and nothing knew it.** Measured: AD-021 through AD-067 carry all four
26
+ headings without exception; AD-001 through AD-020 use `## Applies to` and `## Consequences` instead. The format
27
+ changed once and held for forty-seven records. Held by nothing, though — the next record written under time
28
+ pressure would have dropped `## Trade-offs` and started a second boundary nobody noticed.
29
+
30
+ **`## Trade-offs` and `## Not decided here` are the two that get dropped, and the two that pay.** A record
31
+ without them reads as a decision with no cost and no open edges, which is the reading that makes somebody
32
+ re-litigate it six weeks later. This session alone, `## Not decided here` is where the Windows launcher question,
33
+ the per-record token accounting and the unused `eval-skill-triggers` went — each one a thing a reader would
34
+ otherwise assume was settled.
35
+
36
+ **A bare id is a citation only a reader who already knows can check.** The repository stood at 355 bundle-relative
37
+ links against 15 bare `(AD-NNN)` forms. A link survives a file move, `check-docs-bundle` resolves it, and a
38
+ reader can follow it; the bare form does none of that. Fifteen was small enough to fix in one pass and large
39
+ enough to keep growing — and the pass found twenty more inside the records themselves, which the first
40
+ measurement had missed by only looking at `docs/*.md`.
41
+
42
+ **The ratchet fails downward too.** A budget that only refuses to rise is one nobody ever turns: migrating a
43
+ record improves the number and nothing records that it happened. Failing when the count *falls* makes the
44
+ migration a two-line change — the record and the budget — and keeps the number honest.
45
+
46
+ **Backfilling the twenty would have meant inventing them.** Their trade-offs were either never written or are
47
+ now unknowable. A checker that allow-listed them by number would go stale the first time one was migrated, and
48
+ would hide the twenty-first. The count is the honest form.
49
+
50
+ ## Trade-offs
51
+
52
+ **A ratchet at twenty is a number in a file that has to be maintained.** It is one integer, the gate names the
53
+ new value when it is wrong, and the alternative — twenty invented rationales — is worse in a way that cannot be
54
+ detected later.
55
+
56
+ **The Why rule matches by prefix, so `## Whyever` would pass.** Requiring the bare word would have forced
57
+ `## Why the runtime home had to change` down to `## Why`, which is worse prose in a record whose whole value is
58
+ prose. The looser rule buys better writing at the cost of a hole nobody has a reason to walk through.
59
+
60
+ **None of this reaches anyone using the harness.** It is a convention of this repository, checked by a tool in
61
+ `tools/dev/` that is never packaged ([/decisions/ad-068.md](/decisions/ad-068.md)). It makes the corpus cheaper
62
+ to read for whoever maintains the product, which reaches a user slowly and indirectly, or not at all.
63
+
64
+ **Two of the four `(AD-NNN)` forms this converted were inside test names**, where the linked form is long and
65
+ reads badly in a test report. The consistency was judged worth more than the line length, and the rule covers
66
+ tests precisely because that is where a convention erodes first.
67
+
68
+ ## Not decided here
69
+
70
+ - **Whether the older twenty should be migrated at all.** The ratchet permits it and nothing requires it.
71
+ Whoever has the context for one may lower the number; nobody should do all twenty in one pass.
72
+ - **Whether `## Not decided here` should be required to be non-empty.** A heading with nothing under it
73
+ satisfies the checker today. Judging the content is a semantic question a gate cannot answer.
74
+ - **Applying the citation rule to `.specs/`.** That tree is gitignored and outside the documentation bundle.
@@ -0,0 +1,86 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-070 — A comment has to read for somebody who was not in the session"
4
+ description: "The comment gate asked whether a comment declares a reason. It could not ask whether the reason means anything to a reader at HEAD, which is the comment a model actually writes — change narration, a decision number only the session saw, an argument aimed at a reviewer. A third mode asks that question, and the rule set was cut on measured false positives before it shipped."
5
+ tags: [decision, comment-policy, rails]
6
+ timestamp: "2026-08-13"
7
+ ---
8
+
9
+ # AD-070 — A comment has to read for somebody who was not in the session
10
+
11
+ - **status**: active
12
+
13
+ ## Decision
14
+
15
+ 1. **A third mode, `resolvable`**, off by default and never implied: `declared` plus one further question — can
16
+ a reader at HEAD, with no transcript of the session, resolve every reference and check every claim?
17
+ 2. **Five classes** are refused: change narration, dead citations, review vantage, reviewer-addressed
18
+ justification, and control-flow narration.
19
+ 3. **Prose, never syntax.** Nothing here parses a language.
20
+ 4. **One comment, one finding.** A block that trips three rules reports once, and a comment already refused for
21
+ having no declared reason is not refused twice.
22
+ 5. **The refusal asks for a restatement, not a deletion.**
23
+
24
+ ## Why
25
+
26
+ **The rail could not see the comment a model actually writes.** `declared` accepts anything carrying `why:`,
27
+ `hazard:` or `invariant:`, and `strict` accepts nothing. Neither can tell these apart:
28
+
29
+ ```
30
+ // why: this used to read HEAD, which a committing turn moves past
31
+ // why: without the guard, a bare `*` reads as a block continuation
32
+ ```
33
+
34
+ Both declare a reason. The first is only checkable by somebody who saw the previous version; the second is
35
+ checkable by anybody, forever. The whole difference is vantage, and no marker carries it.
36
+
37
+ **The distinguishing question is a single one**, which is what makes it implementable: *could a reader at HEAD
38
+ resolve every reference and check every claim?* A counterfactual passes because the reader can construct it. A
39
+ past-tense claim about the code fails because the repository no longer holds the evidence.
40
+
41
+ **The restatement is the fix, not deletion.** Almost every unresolvable comment carries a true, durable fact
42
+ wrapped in the session's vantage. `this used to emit one finding per line, so one failing test arrived as three`
43
+ becomes `one finding per matched line makes a single failing test arrive as three` — same fact, no transcript
44
+ required. Telling the model to delete would throw away the reason along with the framing, so the message names
45
+ the restatement and only then permits deletion.
46
+
47
+ **Prose keeps it language-agnostic.** The syntax catalog exists because a delimiter per language is fragile and
48
+ does not scale ([/decisions/ad-058.md](/decisions/ad-058.md)). Resolvability is a property of the sentence, not
49
+ of the language, so a new language costs nothing.
50
+
51
+ ## Trade-offs
52
+
53
+ **One phrase was measured out of the rule set before it shipped.** `no longer` was in the first draft. Run
54
+ against this repository's 1,077 comment blocks it matched four times, and every one described *runtime* state —
55
+ a lock owner that no longer exists, a lesson ref that no longer resolves, a path a future refactor would leave
56
+ behind. Zero described the repository's own history. Four wrong and none right is a phrase that teaches an
57
+ operator to switch the rail off ([/decisions/ad-034.md](/decisions/ad-034.md)), so it went.
58
+
59
+ **This repository would not pass its own new mode.** After that cut, 61 of 1,077 blocks still match — 5.7%, 55
60
+ of them `used to`. Sampling them, they are true positives: each carries a durable fact wrapped in history, and
61
+ each could be restated. The mode is off here, and saying so is more honest than weakening a correct rule until
62
+ the author's own code passes.
63
+
64
+ **Precision was chosen over recall everywhere.** Hedges, version stamps and untranslated fragments are all
65
+ recognisable leakage and none is matched, because none can be matched without also catching prose that is fine.
66
+ The rail catches less than a careful reader would.
67
+
68
+ **A regex is a blunt instrument for a semantic property.** `this is safe` is refused even when the sentence goes
69
+ on to state a real invariant. The refusal names the phrase, so the fix is one edit, and the alternative — a model
70
+ call per comment — is not something a stop hook has the latency budget for
71
+ ([/decisions/ad-012.md](/decisions/ad-012.md)).
72
+
73
+ **A fourth mode is a fourth thing to explain.** The catalog's `asks` now offers three words rather than two, and
74
+ the operator prose the model reads is longer in this mode than in the others. The modes are ordered by strictness
75
+ and each is a superset of the last, which is the only arrangement that keeps three explainable.
76
+
77
+ ## Not decided here
78
+
79
+ - **Whether `resolvable` should become the default.** It would change behaviour for every project that enabled
80
+ `comments`, and a capability changes nothing by default when it arrives.
81
+ - **Applying the same test to commit messages, plans or handoff prose.** The same leakage appears there and the
82
+ detector is not coupled to comments, but each surface needs its own calibration run before it can block.
83
+ - **Whether this repository should adopt the mode on itself.** It would mean restating 61 passages, and doing
84
+ that well needs the context of whoever wrote each one.
85
+ - **Hedges and planning residue.** `probably fine for now` is real leakage. Every pattern for it also matched
86
+ honest uncertainty, so it stays out until one exists that does not.
@@ -0,0 +1,93 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-071 — The turn's added lines are checked against the code the project already has"
4
+ description: "Nothing watched for the agent writing something the codebase already contained. A line-run comparison over the turn's diff answers it without parsing a language, and three exclusions — comments, dependency declarations, pure data — were each added because a calibration run said so. It found a real duplication in this repository on its first honest pass."
5
+ tags: [decision, duplication, rails]
6
+ timestamp: "2026-08-13"
7
+ ---
8
+
9
+ # AD-071 — The turn's added lines are checked against the code the project already has
10
+
11
+ - **status**: active
12
+
13
+ ## Decision
14
+
15
+ 1. **A rail, off by default**, that blocks the stop when this turn added a run of `minRun` lines or more that
16
+ already exists elsewhere in the project, naming both sites.
17
+ 2. **Diff-scoped** against the sha the turn started from, like the comment gate. A run already duplicated before
18
+ the turn is not this turn's to answer for.
19
+ 3. **Lines, never syntax.** Nothing parses a language.
20
+ 4. **Three exclusions**, each added because a calibration run demanded it: comments, dependency declarations, and
21
+ runs that are not operational enough to be logic.
22
+ 5. **Bounded and honest about it**: 2,000 files and 8 MB, and the scan reports when a bound was reached.
23
+
24
+ ## Why
25
+
26
+ **Nothing was watching for the copy.** The harness had rails for narration, for evidence, for shell safety and
27
+ for the plan — and none for the failure an operator meets weeks later as two implementations of the same thing
28
+ drifting apart. The second copy is where the drift starts, and the second copy is exactly what a diff-scoped rail
29
+ can see.
30
+
31
+ **Three calibration runs shaped the rule, and each one changed it.**
32
+
33
+ | Run | Reported | What the top of the list was | What changed |
34
+ | --- | --- | --- | --- |
35
+ | first | 0 at every window | — | the index kept one site per run, so a run compared equal to itself |
36
+ | second | 137 at six lines | every import block in the repository | dependency declarations excluded |
37
+ | third | 116 | re-export lists, config literals, type bodies, fixtures | a run must be operational in the majority of its lines |
38
+ | fourth | 13 | two entrypoints building the same policy input | shipped |
39
+
40
+ The first run is the one worth naming: **zero findings looked like a clean repository and was a defect.** One
41
+ site per run meant the project index pointed at the very lines the turn had added, so every duplication compared
42
+ equal to itself and vanished. A rail that reports nothing is indistinguishable from a rail that has nothing to
43
+ report, which is why the calibration was run at four window lengths rather than one — four zeroes in a row is
44
+ not a property of a codebase.
45
+
46
+ **It found a real one immediately.** `subagent-start.ts` and `tool-before.ts` each assembled the same twelve-field
47
+ input for `evaluateSubagentSpawn`, identical apart from indentation. A new field in `policy.subagents` had to be
48
+ remembered in both — the shape [/decisions/ad-065.md](/decisions/ad-065.md) collected seven times, a consumer
49
+ that stops growing with its producer. It is now one function.
50
+
51
+ **Data is not logic, and repeated shape is what data is for.** A type's fields, a config literal and a re-export
52
+ list all repeat by design. Requiring a call, an assignment, a branch or a return in the majority of a run's lines
53
+ is what separates "these two do the same thing" from "these two describe the same thing", without asking what
54
+ language it is.
55
+
56
+ **Renaming makes it a different run, deliberately.** Normalisation collapses whitespace and a trailing comma —
57
+ the two things a paste changes without changing the code — and nothing else. Matching through renames would
58
+ report every similarly shaped function, which is the noise that ends a rail's life.
59
+
60
+ ## Trade-offs
61
+
62
+ **It reads the repository on every stop.** `git ls-files` plus a read of each file is the honest way to answer
63
+ "does this exist anywhere", and hook latency is a product property ([/decisions/ad-012.md](/decisions/ad-012.md)).
64
+ The bounds are what keep a large repository from paying for it, and the scan says when it stopped early, because
65
+ a partial scan reporting nothing reads exactly like a clean one.
66
+
67
+ **Six lines is a judgement, not a discovery.** At four the matches were punctuation and import blocks; at ten
68
+ almost nothing survived. Six is where a match started being worth reading in *this* repository, and it is a
69
+ config field precisely because another project's answer will differ.
70
+
71
+ **Duplicated logic in tests is reported like any other.** Eleven of the thirteen remaining findings here are test
72
+ setup, and reasonable people call that acceptable. The rail does not know the difference, and inventing a
73
+ test-directory exemption would carve out the place duplication accumulates fastest.
74
+
75
+ **A copy that was renamed is invisible.** The rail catches the paste, not the paraphrase. Catching the paraphrase
76
+ needs a parser per language, which is the cost this design exists to avoid
77
+ ([/decisions/ad-058.md](/decisions/ad-058.md)).
78
+
79
+ **Two exclusions are vocabulary, not grammar.** `import`, `require`, `use`, `#include` and their siblings open a
80
+ dependency declaration in most languages but not in all, and a language that spells it differently will have its
81
+ import blocks reported. The failure is loud and the fix is one word in a list.
82
+
83
+ ## Not decided here
84
+
85
+ - **Whether to run the rail in observation mode first.** The harness supports recording a rail's findings without
86
+ blocking, and this one has a first-run false-positive history that argues for it. It ships off, so an operator
87
+ who wants the reading before the block can already have it.
88
+ - **Sharing the scan with the comment gate.** Both read the turn's added lines and both walk the project; today
89
+ each does its own. Merging them is a latency win and a coupling, and neither has been measured.
90
+ - **A cache keyed on the tree hash.** The index is rebuilt every stop. `git ls-files` plus a bounded read was
91
+ fast enough here to not need one, and a stale cache would report a duplication against code that has moved.
92
+ - **Whether the eleven test findings in this repository should be fixed.** They are real; whether shared setup is
93
+ worth the coupling is a judgement per test file.
@@ -0,0 +1,82 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-072 — A record can leave the corpus, and removing is a change worth recording"
4
+ description: "Seventy-one records, every one active and every one about adding something. A record now has a folder it can leave by, value rather than volume decides what leaves, and the doctrine that makes a removal arguable — a test is not the specification, and a decision record is not authority for current behaviour — is written down."
5
+ tags: [decision, docs, process]
6
+ timestamp: "2026-08-13"
7
+ ---
8
+
9
+ # AD-072 — A record can leave the corpus, and removing is a change worth recording
10
+
11
+ - **status**: active
12
+
13
+ ## Decision
14
+
15
+ 1. **`docs/decisions/archived/` exists**, and `- **status**: archived` goes with it. The checker requires the
16
+ folder and the status to agree.
17
+ 2. **An archived record keeps its row** in `CHANGELOG.md` and `docs/log.md`. Only the decisions index separates
18
+ active from archived.
19
+ 3. **Value decides, never volume.** A record stays active while its alternatives, its ownership boundary, its
20
+ negative guarantee, its security rule, or its condition for reintroduction would still change what somebody
21
+ does. There is no target count and no quota.
22
+ 4. **Two doctrines are written down**: a test that pins behaviour nothing load-bearing depends on is evidence
23
+ *for* removal; and a decision record is not authority for current behaviour either.
24
+
25
+ ## Why
26
+
27
+ **Seventy-one records, seventy-one active, and not one about removing.** The corpus is append-only in both
28
+ senses: nothing leaves, and nothing that arrives is a subtraction. That is not because nothing was removed —
29
+ `format.enabled` went ([/decisions/ad-033.md](/decisions/ad-033.md)), `progressiveHandoff` went — but because a
30
+ removal had no obvious place to be recorded, so it was a footnote inside whatever addition replaced it.
31
+
32
+ **An index that only grows stops being read.** Every record claims to be current, so a reader cannot tell a rule
33
+ that still binds from one whose subject no longer exists. The cost is paid on every read and never recovered.
34
+ [/decisions/ad-067.md](/decisions/ad-067.md) noted the same shape from the other side: fourteen gate steps, each
35
+ added because something drifted, none ever removed. It is fifteen now.
36
+
37
+ **Volume is the wrong criterion, and it is the tempting one.** A rule of "archive anything over N months" or
38
+ "keep the corpus under N records" produces motion without judgement, and the records it removes are the ones
39
+ nobody defended rather than the ones nobody needs. The listed properties are the ones that actually guide a later
40
+ change; a record with none of them has done its work.
41
+
42
+ **An archived record still shipped.** The changelog answers "what landed in this release" and the log answers
43
+ "what was decided when" — neither question changes when a decision stops guiding future work. Only the index
44
+ answers "what binds now", so only the index distinguishes.
45
+
46
+ **Two doctrines, because both were assumed and both are wrong.** A test pinning behaviour is routinely read as
47
+ proof the behaviour is required; it is proof somebody once wrote it down. And this repository has treated its own
48
+ decision records as authority — [/decisions/ad-065.md](/decisions/ad-065.md) assumed the record described the
49
+ code. When code and record disagree, finding out which is wrong is the work, and neither one wins by default.
50
+
51
+ ## Trade-offs
52
+
53
+ **Nothing has been archived yet.** The mechanism exists, the rule is written and a test proves an archived record
54
+ keeps its rows and links correctly, but no real record has moved. Judging seventy-one against the rule is work
55
+ that needs the context of whoever wrote each one, and doing it in one pass to prove the folder works would be the
56
+ quota this decision refuses.
57
+
58
+ **The status and the folder are two facts that must agree**, and the checker enforces it rather than deriving one
59
+ from the other. Deriving would have been fewer moving parts; a record whose frontmatter says `active` while it
60
+ sits in `archived/` is a real mistake somebody will make, and a silent derivation would hide it.
61
+
62
+ **`DecisionSummary` grew a `path` field.** The readers previously rebuilt a record's link from its id, which
63
+ stops working the moment a record lives in a subdirectory. Every caller now carries the real path, and four test
64
+ fixtures had to be updated to match.
65
+
66
+ **A removal doctrine in `CONTRIBUTING.md` is prose, not a gate.** Nothing checks that a candidate was judged
67
+ against it. The mechanical half already exists — `check-wiring` and `check-obs-contract` report what nothing
68
+ reads — and those stay reports rather than failures precisely because the judgement is a person's.
69
+
70
+ **None of this reaches anyone using the harness.** It is this repository's own convention, checked from
71
+ `tools/dev/` and never packaged ([/decisions/ad-068.md](/decisions/ad-068.md)).
72
+
73
+ ## Not decided here
74
+
75
+ - **Which records should be archived.** Deliberately. The rule is written; applying it is a pass somebody makes
76
+ with the context.
77
+ - **Whether an archived record should be frozen against edits.** Git already holds the history, and a hash
78
+ manifest is machinery for a corpus far larger than seventy-one.
79
+ - **Deleting a record.** Archiving is a `git mv` away from reversible; deletion is not, and nothing needs it.
80
+ - **A `class` on each record** — feature, fix, simplification. It would make "how much of this corpus is
81
+ subtraction" answerable mechanically. It is also seventy-one records to classify, and the question can wait
82
+ until there is more than zero to count.