@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,56 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-014 — Claude Code hook payload field paths, pinned"
4
+ description: "The design specified Claude's event names and capability booleans but never its JSON field paths; this decision pins them so a wrong field path cannot silently pass a green test suite."
5
+ tags: [decision, claude, contracts, inbound]
6
+ timestamp: "2026-07-29"
7
+ ---
8
+
9
+ # AD-014 — Claude Code hook payload field paths, pinned
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-29
13
+
14
+ ## Why this exists
15
+
16
+ The design specified Claude's event *names* and capability *booleans* but never the JSON field paths.
17
+ Cursor's adapter could be checked against captured goldens from the predecessor handlers; Claude has no
18
+ such upstream, so a wrong field path produces a passing test suite and a silently broken adapter. This
19
+ pins the field paths before later phases build on top.
20
+
21
+ ## Field paths
22
+
23
+ | Field | Where | Source |
24
+ | --- | --- | --- |
25
+ | `session_id` | top level, all events | documented |
26
+ | `hook_event_name` | top level, PascalCase | documented |
27
+ | `cwd` | top level | documented |
28
+ | `transcript_path` | top level | documented |
29
+ | `permission_mode` | top level (`default\|plan\|acceptEdits\|auto\|dontAsk\|bypassPermissions`) | documented |
30
+ | `effort.level` | top level object, `low\|medium\|high\|xhigh\|max` | documented |
31
+ | `prompt_id` | top level, absent until first input | documented |
32
+ | `agent_id`, `agent_type` | top level, subagent events only | documented |
33
+ | `tool_name`, `tool_input`, `tool_use_id` | top level, tool events | documented |
34
+ | `tool_input.command` | Bash | documented (official example) |
35
+ | `tool_input.file_path` | Edit / Write / Read | **inferred** from the tool schema, not quoted in the hooks reference — verify against a real payload when one is available |
36
+ | loop counter | **does not exist** on `Stop` | documented by absence; this is why Claude has no native loop counter (see [/providers/claude-code.md](/providers/claude-code.md)) |
37
+
38
+ ## Consequence
39
+
40
+ `claude.inbound.ts` reads exactly these paths. Any future divergence is a payload change, not a design
41
+ choice, and must be caught by re-reading the reference rather than by guessing.
42
+
43
+ ## Applies to
44
+
45
+ - `src/providers/claude/claude.inbound.ts`
46
+ - its fixtures
47
+ - Claude entrypoints
48
+
49
+ ## See also
50
+
51
+ - [/providers/claude-code.md](/providers/claude-code.md)
52
+
53
+ # Citations
54
+
55
+ 1. Claude Code hooks reference (Anthropic documentation) — field paths pinned above were cross-checked
56
+ against the documented hook payload shape at the time this decision was recorded.
@@ -0,0 +1,33 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-015 — Wiring handler names are the entrypoint filenames"
4
+ description: "The handler name a provider's wiring registers is the src/entrypoints/<name>.ts filename, so both providers' wiring descriptors reference exactly the same eleven handler names."
5
+ tags: [decision, entrypoints, providers, wiring]
6
+ timestamp: "2026-07-29"
7
+ ---
8
+
9
+ # AD-015 — Wiring handler names are the entrypoint filenames
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-29
13
+
14
+ ## Decision
15
+
16
+ The handler name a provider's wiring registers is the `src/entrypoints/<name>.ts` filename —
17
+ `session-start`, `session-end`, `prompt-submit`, `tool-before`, `tool-after`, `tool-failure`,
18
+ `subagent-start`, `subagent-stop`, `stop`, `compact-before`, `response-after`. Both providers' wiring
19
+ descriptors already use these names.
20
+
21
+ ## Why
22
+
23
+ Building the second provider adapter required picking a naming scheme with no spec anchor. Making the
24
+ entrypoint filename the single source of truth removes the guess for every wiring descriptor that follows.
25
+
26
+ ## Applies to
27
+
28
+ - `cursor.wiring.ts`, `claude.wiring.ts`
29
+ - all `src/entrypoints/*.ts` files
30
+
31
+ ## See also
32
+
33
+ - [/providers/cursor.md](/providers/cursor.md), [/providers/claude-code.md](/providers/claude-code.md) — the wiring tables built on this naming
@@ -0,0 +1,98 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-016 — Field semantics, state writers, and the core export surface"
4
+ description: "Spawn fields are named apart from the running agent's, parent-model state has a writer, core exports what entrypoints need, ObsKind covers the adapter boundary, blockMode is honoured, and shell audit records are signal-level."
5
+ tags: [decision, entrypoints, contracts, observability, correctness]
6
+ timestamp: "2026-07-29"
7
+ ---
8
+
9
+ # AD-016 — Field semantics, state writers, and the core export surface
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-29
13
+
14
+ ## Why this exists
15
+
16
+ Seven decisions about contracts that only settle where the layers meet: what a field name promises, who
17
+ writes a piece of state, what core exports, and which records are durable.
18
+
19
+ ## 1. `spawn*` names the child of a spawn; unprefixed fields name the running agent
20
+
21
+ Cursor's inbound adapter reads `raw.subagent_type` (the *calling* agent's identity). Claude's inbound
22
+ adapter reads `tool_input.subagent_type` (the *target of a Task spawn*). Same field, inverted semantics.
23
+ `model` had the same clash: on `subagent.start`/`subagent.stop` it means the child being spawned, everywhere
24
+ else the parent's current model. This reproduced as AD-001's `blockParentFast` state being clobbered by
25
+ every subsequent event.
26
+
27
+ Fix — split the fields in `contracts/harness-event.ts`:
28
+
29
+ | Field | Meaning | Cursor | Claude |
30
+ | --- | --- | --- | --- |
31
+ | `subagentType` | the **calling** agent's own identity | `raw.subagent_type` | top-level `agent_type` |
32
+ | `spawnSubagentType` | the **target** of a Task spawn | `tool_input.subagent_type` | `tool_input.subagent_type` |
33
+ | `model` | the current / parent model | `raw.model` | `raw.model` |
34
+ | `spawnModel` | the child model being spawned | `raw.subagent_model` | `tool_input.model` |
35
+
36
+ **Needs verification against a real payload:** AD-014 records `agent_type` as "subagent events only",
37
+ quoting the reference's "present only in subagent hooks". The useful reading is "present whenever a hook
38
+ fires *inside* a subagent's context", which would make it the caller identity on an ordinary `PreToolUse`.
39
+ If that reading is wrong, Claude has no caller-identity signal at tool time and the read-only-subagent rail
40
+ is Cursor-only — a genuine parity gap to be documented rather than hidden.
41
+
42
+ ## 2. Parent-model state is written wherever an event carries the running agent's model
43
+
44
+ `upsertParentModelState` was built early and consumed by `evaluateSubagentSpawn`, but no task ever called
45
+ it — `blockParentFast` was dead on the write side for several phases. It is now wired in `run.ts`.
46
+
47
+ ## 3. Core must export what entrypoints legitimately need
48
+
49
+ Several constants and helpers (a stagnation follow-up string, default observability config, lessons
50
+ rendering helpers, a gate-lock timeout error) were being duplicated by entrypoints because the core facade
51
+ curated them out. Duplication guarantees drift. They are now exported from core and the copies deleted.
52
+
53
+ ## 4. `ObsKind` needs adapter-boundary members
54
+
55
+ `adapter.unrecognized`, `adapter.error` and `adapter.ambiguous` fire before a provider or session is
56
+ resolved. An earlier version bypassed the observability recorder and appended JSONL directly rather than
57
+ widen a core type from outside core — right instinct, wrong resting place. The union is widened inside core
58
+ instead.
59
+
60
+ ## 5. `subagents.blockMode` is honoured by the spawn decision
61
+
62
+ A documented option must change behaviour: `deny` refuses the spawn, `ask` escalates it. An option the
63
+ service ignores is removed rather than documented.
64
+
65
+ ## 6. `impeccableHook` is out of scope
66
+
67
+ Relaying a third-party script's verdict is the script author's concern, not the harness's. No port,
68
+ timeout or contract for it exists.
69
+
70
+ ## 7. Shell audit records must be signal-level, not debug-level
71
+
72
+ `SIGNAL_KINDS` excluded `shell.end` outright, so "shell commands are audited" silently persisted nothing
73
+ unless `debugEnabled` was on. An audit trail that requires opting in is not an audit trail — `shell.end` is
74
+ now signal-level whenever the shell permission was not a plain allow (see
75
+ [/measure.md](/measure.md#observability-planes)).
76
+
77
+ ## Also
78
+
79
+ `HarnessEvent` had no generic usage field, so Cursor's `usageInPayload: true` went unread — only Claude's
80
+ transcript path fed cost. `usage?: { inputTokens?, outputTokens?, … }` was added, populated by whichever
81
+ provider reports it.
82
+
83
+ `sandbox` is documented for Cursor only; an earlier version read `raw.sandbox` defensively for Claude with
84
+ no confirmation it exists — verify or drop from the Claude path.
85
+
86
+ ## Applies to
87
+
88
+ - `contracts/harness-event.ts`
89
+ - both `*.inbound.ts` adapters
90
+ - `core/observability`, `core/subagent-policy`, `core/gate`, `core/lesson`, `core/stagnation`
91
+ - `core/index.ts` (the facade)
92
+ - entrypoints (`run`, `support`, `tool-before`, `tool-after`)
93
+
94
+ ## See also
95
+
96
+ - [/decisions/ad-001.md](/decisions/ad-001.md) — the rail item 2 restores the writer for
97
+ - [/decisions/ad-009.md](/decisions/ad-009.md) — the earlier round of amendments this one follows
98
+ - [/providers/index.md](/providers/index.md) — `subagentType` vs `spawnSubagentType` in the event mapping tables
@@ -0,0 +1,65 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-017 — The docs gate delegates to the project's tool, and the catalog is the only source of capability metadata"
4
+ description: "Path mapping was measured reporting on 82-100% of commits and removed; the docs gate runs the repository's own staleness tool through the grind path; the capability catalog generates the wizard menu and the architecture rails table."
5
+ tags: [decision, docs, capabilities, gates]
6
+ timestamp: "2026-07-30"
7
+ ---
8
+
9
+ # AD-017 — The docs gate delegates to the project's tool, and the catalog is the only source of capability metadata
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-30
13
+
14
+ ## Decision
15
+
16
+ `docs.command` runs the repository's own documentation staleness tool on stop, through the same lock, artifact
17
+ writer and failure path as the lint and test gates. `docs.severity` is `warn` or `deny`, defaulting to `warn`.
18
+ A repository without such a tool gets no gate.
19
+
20
+ `capabilities/catalog.json` is the only place a capability is described. `tools/render-capabilities.ts`
21
+ generates the wizard's menu and the architecture rails table into marked regions, and `--check` is part of the
22
+ gate.
23
+
24
+ ## Why
25
+
26
+ Code changes pass lint, tests and the ship gate; the documents describing them pass nothing.
27
+
28
+ The first implementation inferred staleness from a source-glob to docs-glob map. It was replayed over real
29
+ history with its own evaluator and reported on 100% of applicable commits in one repository and 82–91% in
30
+ another, at every granularity tried — any markdown, co-located markdown, excluding test paths, a specific
31
+ artifact. The denominator is wrong: nearly every commit touches an area and few change documented behaviour.
32
+ A gate that reports almost always detects nothing, so it was removed rather than kept as a fallback.
33
+
34
+ The tools that do discriminate declare rather than infer. `drift` anchors a document to a file or symbol and
35
+ hashes a normalized AST through tree-sitter. `oasdiff` compares generated specs and classifies changes by
36
+ severity. `ast-grep` matches structure.
37
+
38
+ Building an equivalent inside the harness was rejected on availability, not taste. Regex over source is the
39
+ failure mode this project has already removed twice. The TypeScript compiler API is not guaranteed at runtime,
40
+ since the installer runs no package install and a clean runtime has no `node_modules`. `Bun.Transpiler.scan()`
41
+ is precise and free but Bun-only, which would make a gate's verdict depend on which runtime launched the hook.
42
+
43
+ That leaves delegation, which is what `grind` already does: the harness runs the project's lint and test rather
44
+ than implementing either. The docs gate is the same shape, so it inherits stagnation fingerprinting, gap
45
+ feedback and progressive follow-up for free, and adds no second gate mechanism.
46
+
47
+ ## Consequences
48
+
49
+ - Prose a catalog entry cannot express stays outside the markers: the floor table, the always-ask section, the
50
+ lessons subsection, and operator mode, which is posture rather than a capability.
51
+ - Adding a capability is one edit to the catalog plus running the generator.
52
+ - No skip token of our own: the tool owns its escape hatch, and `drift` already hardened that path by
53
+ requiring explicit confirmation that the document was read before re-stamping.
54
+ - A repository with no such tool is told so and the capability stays off.
55
+
56
+ ## Applies to
57
+
58
+ - `src/entrypoints/stop.ts`, `src/core/policy/`
59
+ - `capabilities/catalog.json`, `tools/render-capabilities.ts`
60
+ - `skills/harness-init/`
61
+
62
+ ## See also
63
+
64
+ - [/decisions/ad-013.md](/decisions/ad-013.md) — the documentation bundle this gate protects
65
+ - [/decisions/index.md](/decisions/index.md)
@@ -0,0 +1,77 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-018 — Three rails adopted from an external review, each off by default and declared rather than inferred"
4
+ description: "The plan gate, the untrusted-content framing and the global observability spool were adopted after validating each candidate against this codebase; two candidates were refuted and one turned out to be an active defect. All three are opt-in, and both detectors match declared inputs instead of inferring from content."
5
+ tags: [decision, gates, capabilities, security, observability]
6
+ timestamp: "2026-07-30"
7
+ ---
8
+
9
+ # AD-018 — Three rails adopted from an external review, each off by default and declared rather than inferred
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-30
13
+
14
+ ## Decision
15
+
16
+ Three capabilities were added, all off by default: `planGate.enabled` (declared scope versus the diff),
17
+ `untrustedContent.enabled` (framing for content read from outside the repository) and `obs.globalSpool`
18
+ (cross-repository record spool).
19
+
20
+ Both new detectors match **declared inputs**, never inferred ones. The plan gate reads a `HARNESS_PLAN:`
21
+ protocol line and ignores prose about plans. The untrusted-content rail matches a fixed list of tool names,
22
+ every MCP result, and command patterns anchored at the start of a command segment.
23
+
24
+ ## Why
25
+
26
+ The candidates came from reading another agent-facing product. Each was validated against this codebase
27
+ before being accepted, and the record matters more than the list:
28
+
29
+ | Candidate | Outcome |
30
+ | --- | --- |
31
+ | Reclaim a stale grind lock | **Defect found here** — `describeHolder` ignored mtime and the stop short-circuited on it, so the steal path was unreachable |
32
+ | Typed handoff phase machine | Refuted — the routing machine already exists in `last_gate_result`, `fingerprint_hits`, `last_failure_category` and `next_action` |
33
+ | Persist state as it happens | Refuted — `stop.ts` already patches the handoff in every branch |
34
+ | Plan versus diff conformance | Adopted |
35
+ | Untrusted-content framing | Adopted |
36
+ | Cross-repo observability | Adopted |
37
+ | Ship evidence tied to a reproduction | Cut — depends on a `HARNESS_REPRO:` discipline nobody practises, and re-executing agent-authored argv is the riskiest surface proposed even behind the floor |
38
+
39
+ Adopting on reputation would have shipped two mechanisms this product already had and missed the defect.
40
+
41
+ **Off by default** is not timidity. `obs.globalSpool` writes outside the repository, which is the one thing
42
+ an operator cannot undo by editing project policy. The plan gate changes what "done" means for a turn. A
43
+ capability that changes either belongs in the wizard, presented with its trade-off, rather than arriving
44
+ with an update.
45
+
46
+ **Declared over inferred** is the lesson AD-017 already paid for with the docs gate: a check that fires on
47
+ almost everything detects nothing. The untrusted rail proved it again on its first production run — the
48
+ patterns were matched as substrings, so writing the documentation that names them tripped the rail on its
49
+ own prose. Anchoring at the start of a command segment fixed it, and the false positive is now a test.
50
+
51
+ **The plan gate's limit is stated, not hidden.** A turn that declares no plan is not gated. That is the same
52
+ weakness that disqualified the reproduction gate; the difference is that the plan gate executes nothing, so
53
+ the cost of the weakness is friction rather than a privilege-escalation path.
54
+
55
+ ## Consequences
56
+
57
+ - `Policy` gained `planGate`, `untrustedContent` and `obs`; the loader merges each like every other section.
58
+ - `ProviderPolicyDefaults` gained `untrustedTools`, so a new adapter cannot forget to declare which of its
59
+ own tools carry outside content — the provider contract test fails if it does.
60
+ - The handoff carries `plan_paths`, `plan_at`, `plan_snippet` and `plan_deviations`.
61
+ - The plan gate runs before the ship gate: a turn whose scope is invalid produced evidence for the wrong
62
+ change.
63
+ - A deviation without a stated reason is refused, otherwise the gate is a formality satisfied by restating
64
+ the file just touched.
65
+ - Catalog version 8, twenty-one capabilities, generated regions rewritten from the catalog per AD-017.
66
+
67
+ ## Applies to
68
+
69
+ - `src/core/plan/`, `src/core/untrusted/`, `src/core/observability/`
70
+ - `src/entrypoints/stop.ts`, `src/entrypoints/response-after.ts`, `src/entrypoints/tool-after.ts`
71
+ - `capabilities/catalog.json`, `docs/concepts.md`, `docs/measure.md`
72
+
73
+ ## See also
74
+
75
+ - [/decisions/ad-017.md](/decisions/ad-017.md) — declared over inferred, and the catalog as the only source
76
+ - [/decisions/ad-019.md](/decisions/ad-019.md) — the identity bugs found while verifying these rails
77
+ - [/concepts.md](/concepts.md)
@@ -0,0 +1,75 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-019 — A resource is identified by what it resolves to, and a declared capability must be read where it matters"
4
+ description: "Three defects of the same shape: a lock body that parses is not a holder, a launcher path string is not the launcher file, and a capability every adapter declares was consulted by nothing. Each let a gate report a state that was not true."
5
+ tags: [decision, gates, capabilities, providers]
6
+ timestamp: "2026-07-30"
7
+ ---
8
+
9
+ # AD-019 — A resource is identified by what it resolves to, and a declared capability must be read where it matters
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-30
13
+
14
+ ## Decision
15
+
16
+ Three comparisons were corrected, all of the same shape — the check used a proxy for the thing instead of the
17
+ thing:
18
+
19
+ 1. **A lock is a holder only if its body names one.** `readLockBody` returning non-null is not enough:
20
+ `JSON.parse` succeeds on `"a string"`, `42`, `{}` and `{"provider":"p"}`. `isUsableLockBody` checks for
21
+ `provider`, `session` and `pid`. A lock that names no holder is reclaimed after a five-second grace
22
+ window, alongside the existing age-based steal.
23
+ 2. **A launcher is the file, not the path string.** Wiring comparison canonicalises any argument naming the
24
+ launcher before deciding whether it changed.
25
+ 3. **`contextAtToolBefore` / `contextAtToolAfter` are now read.** `degrade` abstains when a `context`
26
+ decision lands on an event the provider cannot carry.
27
+
28
+ ## Why
29
+
30
+ Each of the three let a gate report something untrue, which is worse than a gate that is absent.
31
+
32
+ The lock case cost **120 seconds per stop, indefinitely**. `describeHolder` returned null for an unreadable
33
+ body, so the stop fell through to `withGateLock`, which reclaimed only by mtime — a recent corrupt file was
34
+ never stale, so the loop ran to the deadline and abstained with an `adapter.error`. The escape was deleting
35
+ the file by hand. It was first written off as safe degradation; that was wrong. An unreadable file blocking
36
+ every stop for two minutes is a defect, and the grace window has to outlast `tryAcquire`'s create-then-write
37
+ gap by orders of magnitude without approaching the stale threshold — five seconds is roughly four orders
38
+ above the gap and twenty-four times below the wait it replaces.
39
+
40
+ The launcher case made `doctor` report "detected but not wired" for a provider whose eleven hooks were wired
41
+ and firing, on every from-a-clone install, because `settings.json` holds the symlinked install path while the
42
+ runtime resolves the checkout behind it. `bin/tlc-exec.mjs` already documents this hazard and reads `argv[1]`
43
+ to preserve the caller's path — but `bin/tlc` walks `readlink` before invoking it, so the protection never
44
+ applied to a CLI reached through `~/.local/bin/tlc`. Rather than unpick that resolution, the comparison was
45
+ made canonical, which also stops `update` from rewriting a `settings.json` that was already correct.
46
+
47
+ The capability case was found by verifying an acceptance criterion that assumed a degrade path existed. Both
48
+ adapters declared the field and nothing consulted it, so a `context` returned on an unsupported event was
49
+ rendered into a field the provider ignores and lost silently, while the caller believed it was delivered.
50
+ Abstaining is the honest degradation: `context` is informative, so unlike `ask` there is nothing to escalate
51
+ to.
52
+
53
+ ## Consequences
54
+
55
+ - `describeHolder` returns null for a stale lock and for one whose body names no holder, so a caller that
56
+ short-circuits on it falls through to the reclaim path instead of reporting a dead owner.
57
+ - `withGateLock` accepts `unreadableGraceMs`; `GATE_LOCK_UNREADABLE_GRACE_MS` is 5 000 ms.
58
+ - Wiring equality is canonical, so a symlinked install is recognised and not rewritten. Only strings naming
59
+ the launcher are resolved: a foreign hook's own script path is untouched.
60
+ - The untrusted-content rail keeps its own capability check in addition to `degrade`, because reaching
61
+ `degrade` would already have consumed the once-per-turn marker and cost the whole turn its framing.
62
+ - A test that passed for the wrong reason was found the same way: it hardcoded `WebFetch` while
63
+ `providers[0]` is Cursor, whose untrusted list does not contain it. The tool name now comes from the
64
+ provider's own list.
65
+
66
+ ## Applies to
67
+
68
+ - `src/core/gate/gate.lock.ts`
69
+ - `src/providers/claude/claude.wiring.ts`, `src/providers/provider.degrade.ts`
70
+ - `src/entrypoints/tool-after.ts`, `tools/doctor.ts`
71
+
72
+ ## See also
73
+
74
+ - [/decisions/ad-018.md](/decisions/ad-018.md) — the rails whose verification surfaced these
75
+ - [/diagnose.md](/diagnose.md) — operator-facing symptoms of all three
@@ -0,0 +1,88 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-020 — One resolution for the install path, one source for posture, and a config that only advertises what it reads"
4
+ description: "The wrappers collapsed the install symlink, so generated shims named the checkout; status derived posture separately from the loader and disagreed with it; the observability config section was read by nothing. Each is fixed at the root, and a gate refuses any operator home path in a tracked file."
5
+ tags: [decision, runtime, cli, observability, security]
6
+ timestamp: "2026-07-30"
7
+ ---
8
+
9
+ # AD-020 — One resolution for the install path, one source for posture, and a config that only advertises what it reads
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-30
13
+
14
+ ## Decision
15
+
16
+ 1. **`resolveHarnessHome` returns the conventional install path when `realpath` proves it names the same
17
+ runtime as the invoked launcher.** An explicit `TLC_HOME` still wins; a relocated install is untouched.
18
+ 2. **`status` reads `loadPolicy`.** It reports the posture a hook resolves and, under `--json` and in the text
19
+ form, where the mode came from (`config`, `file` or `flag`).
20
+ 3. **`Policy.obs` carries `globalSpool`, `includePayloads`, `maxAttrChars`, `sessionCostAlertUsd` and
21
+ `retentionDays`.** The `observability` section is removed, not honoured. `debugEnabled` is deliberately
22
+ absent.
23
+ 4. **A tracked file may not contain an operator's home path**, enforced by a test over `git ls-files` with a
24
+ per-line `leak-gate-allow` marker for fixtures.
25
+ 5. **Project shim hooks stay untracked**, and that is a decision rather than a deferral — see below.
26
+
27
+ ## Why
28
+
29
+ **One resolution.** Both bash wrappers walk `readlink` before invoking the launcher, so `argv[1]` and `binDir`
30
+ name the checkout. `resolveHarnessHome` documented that exact hazard and still returned the collapsed path.
31
+ Everything derived from it is written to disk or compared: `init` wrote shim hooks pointing at a checkout that
32
+ exists only on the machine that ran it, and `doctor` compared a checkout-shaped path against the install-shaped
33
+ one recorded in the provider's config, reporting healthy wiring as broken. Preferring the conventional path
34
+ only when `realpath` agrees fixes the source instead of each symptom, and refuses to guess: a deliberately
35
+ relocated install still wins.
36
+
37
+ **One source for posture.** `status` recomputed mode and grind from flag files and defaulted to `solo`. This
38
+ repository, whose policy sets `heads-down`, therefore reported `mode: solo, grind: OFF` while every hook
39
+ resolved focus with grind forced on. A second derivation of the same fact is the whole reason they could
40
+ disagree. Reporting the origin matters as much as the value: the question after a surprising posture is always
41
+ *why*, and a leftover flag is the usual answer.
42
+
43
+ An earlier reading of this — that `tlc harness grind on` and `mode` were inert — was **wrong**, and is recorded
44
+ as refuted: `policy.loader.ts` has always read those flags. Only the reporting was broken.
45
+
46
+ **A config that only advertises what it reads.** The `observability` block sat in the example config and in
47
+ this repository's policy, was documented in `concepts.md` with an instruction to set `debugEnabled: true`, and
48
+ was read by nothing — the planes came from a module constant. Values happened to match the defaults, so it
49
+ never diverged visibly; it simply promised configuration that did not exist. Honouring the key would have been
50
+ back-compat this project refuses ([/decisions/ad-003.md](/decisions/ad-003.md)), so the fields that have a real effect moved to `obs` and the key was
51
+ deleted.
52
+
53
+ `debugEnabled` was implemented, then removed before shipping: every event that resolves to debug level is
54
+ emitted with the audit configuration, which forces debug on for the audit trail (AD-016 item 7). Exposing a
55
+ field with nothing to switch would have repeated the mistake being corrected. A mutation test confirmed the
56
+ same danger elsewhere — `retentionDays` initially reached the runtime with no test covering it, and the
57
+ mutation that ignored it survived until one was added.
58
+
59
+ **The leak gate.** This product installs with a shell one-liner and tracks its own bundles, so a generated file
60
+ is exactly where an account name slips in unnoticed. A denylist of one name would pass on every other machine,
61
+ so the rule is the shape of the path. An audit before adding it found the tree clean.
62
+
63
+ ## Consequences
64
+
65
+ - `bin/tlc-exec.d.mts` declares the four-argument resolver; the `.mjs` and its declarations are separate files
66
+ and both must move together.
67
+ - `StatusReport` gained `modeOrigin`. Any consumer of `status --json` sees one more field.
68
+ - `tlc harness obs prune` reads retention from policy, so two projects on one machine can differ.
69
+ - The example config and this repository's policy no longer carry `observability`; a project that copied it
70
+ loses nothing, because nothing read it.
71
+ - Fixtures that need a realistic account name carry `leak-gate-allow` on that line.
72
+
73
+ **Project shims are not version-controllable, by decision.** A portable form needs shell expansion, and `args`
74
+ arrays carry none — so every hook of every provider would need `sh -c` (or `cmd /c` on Windows), paying
75
+ permanent per-hook latency for a file `tlc harness init` regenerates in under a second. They are ignored and
76
+ documented instead.
77
+
78
+ ## Applies to
79
+
80
+ - `bin/tlc-exec.mjs`, `bin/tlc-exec.d.mts`, `bin/tlc-cli.ts`
81
+ - `src/core/policy/`, `src/entrypoints/support.ts`, `tools/obs-cli.ts`
82
+ - `config.example.json`, `.gitignore`
83
+
84
+ ## See also
85
+
86
+ - [/decisions/ad-019.md](/decisions/ad-019.md) — the same identity-versus-proxy mistake in three other places
87
+ - [/decisions/ad-003.md](/decisions/ad-003.md) — no backward compatibility
88
+ - [/diagnose.md](/diagnose.md), [/concepts.md](/concepts.md)
@@ -0,0 +1,57 @@
1
+ ---
2
+ type: Decision
3
+ title: "AD-021 — A gate command that never resolved is a config fault, and a recipe runner does not receive file arguments"
4
+ description: "The grind gate appended changed files to every lint/test argv. Against a recipe runner that reads the first path as a second target, the gate failed on code it never executed, and the failure was classified as verification — a follow-up that instructs the agent to fix findings without deleting tests, aimed at healthy code."
5
+ tags: [decision, grind, gate, config]
6
+ timestamp: "2026-07-30"
7
+ ---
8
+
9
+ # AD-021 — A gate command that never resolved is a config fault, and a recipe runner does not receive file arguments
10
+
11
+ - **status**: active
12
+ - **date**: 2026-07-30
13
+
14
+ ## Decision
15
+
16
+ 1. **`grind.appendFiles` decides whether changed files reach the lint/test argv**, with modes `auto`
17
+ (default), `always` and `never`. `auto` appends, except to a recipe runner.
18
+ 2. **A recipe runner never receives file arguments.** The set is `just`, `make`, `task`, `mise` and `rake`,
19
+ matched on the executable name after stripping the directory and a Windows `.exe`/`.cmd`/`.bat` suffix.
20
+ 3. **A gate whose command never resolved is classified `config`, not `verification`.** The signal is exit
21
+ 127, or a runner message that names an unresolved target: `does not contain recipe`, `No rule to make
22
+ target`, `unknown recipe`, `Missing script:`, `task "…" does not exist`, `Don't know how to build task`.
23
+
24
+ ## Why
25
+
26
+ A project configured `testCommand` as a recipe runner invocation. The gate ran
27
+ `just <recipe> <path>.test.ts`; `just` read the path as a second recipe and exited 1 in 53ms with
28
+ `justfile does not contain recipe`. Nothing about the code caused it, and no edit to the code could clear it.
29
+
30
+ The second half is the damaging one. `classifyGateFailure` maps the `test` gate to `verification`, whose
31
+ follow-up reads *"Fix the test findings without suppressions or deleted tests"*, reinforced by a lesson that
32
+ forbids deleting or skipping tests. Pointed at a malformed command, that text is an instruction to go edit a
33
+ test file that passes — in the observed case a file being edited concurrently by another session, on another
34
+ branch. The harness was steering an agent into damaging healthy work, and the loop counter plus stagnation
35
+ detection kept it there.
36
+
37
+ `config` already existed as a category, with the suggestion *"Check .tlc/harness/config.json commands/paths;
38
+ run harness doctor."* The fix routes to it rather than inventing a concept.
39
+
40
+ ## Trade-offs
41
+
42
+ Detection is a closed set of runner names and messages, not inference. A runner outside the set that also
43
+ rejects file arguments still needs `appendFiles: "never"`; that is deliberate, because guessing at argv
44
+ semantics is how the original bug reached a user. The message patterns are specific strings rather than a
45
+ generic `command not found`, so a test whose own output mentions a missing command is not misfiled — exit 127
46
+ covers the real missing-binary case.
47
+
48
+ `auto` changes behaviour for an existing config that pairs a recipe runner with a file-scoped expectation:
49
+ the gate now runs the recipe as written, over the whole suite, instead of failing. That is the intended
50
+ correction.
51
+
52
+ ## Not decided here
53
+
54
+ The gate scopes files from `listChangedRepoFiles`, which reads the whole dirty worktree. When two sessions
55
+ share a checkout, one session's gate sees the other's edits. The observed incident surfaced that, and it is
56
+ not addressed: there is no per-session record of edited paths to intersect against, and adding one is a
57
+ separate change to the tool-after path.