@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
package/bin/tlc-exec ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SOURCE="${BASH_SOURCE[0]:-$0}"
4
+ while [[ -L "$SOURCE" ]]; do
5
+ DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
6
+ SOURCE="$(readlink "$SOURCE")"
7
+ [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
8
+ done
9
+ BIN_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
10
+ exec node "$BIN_DIR/tlc-exec.mjs" "$@"
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ setlocal
3
+ node "%~dp0tlc-exec.mjs" %*
4
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,60 @@
1
+ export const MIN_NODE_MAJOR: number;
2
+
3
+ export function conventionalHarnessHome(home?: string): string;
4
+
5
+ export function isPackagedCopy(candidate: string): boolean;
6
+
7
+ export type HarnessHomeDeps = {
8
+ realpath: (path: string) => string;
9
+ home: () => string;
10
+ exists?: (path: string) => boolean;
11
+ };
12
+
13
+ export function resolveHarnessHome(
14
+ binDir: string,
15
+ env?: Record<string, string | undefined>,
16
+ invoked?: string,
17
+ deps?: HarnessHomeDeps,
18
+ ): string;
19
+
20
+ export function bunExecutableName(platform?: string): string;
21
+
22
+ export function findBunOnPath(
23
+ env?: Record<string, string | undefined>,
24
+ platform?: string,
25
+ ): string | null;
26
+
27
+ export function runtimeCachePath(harnessHome: string): string;
28
+
29
+ export type RuntimeCache = { bunPath: string | null; checkedAt: string };
30
+
31
+ export function readRuntimeCache(harnessHome: string): RuntimeCache | null;
32
+
33
+ export function writeRuntimeCache(harnessHome: string, bunPath: string | null): RuntimeCache;
34
+
35
+ export function resolveBunPath(
36
+ harnessHome: string,
37
+ env?: Record<string, string | undefined>,
38
+ platform?: string,
39
+ ): string | null;
40
+
41
+ export function entrySourceCandidates(harnessHome: string, entry: string): string[];
42
+
43
+ export function resolveEntrySource(harnessHome: string, entry: string): string | null;
44
+
45
+ export type RuntimeDecisionInput = {
46
+ harnessHome: string;
47
+ entry: string;
48
+ bunPath: string | null;
49
+ nodeMajor: number;
50
+ distExists: boolean;
51
+ srcPath: string | null;
52
+ };
53
+
54
+ export type RuntimeDecision =
55
+ | { kind: "run"; command: string; args: string[] }
56
+ | { kind: "error"; status: number; message: string };
57
+
58
+ export function decideRuntime(input: RuntimeDecisionInput): RuntimeDecision;
59
+
60
+ export function main(argv?: string[]): void;
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { delimiter, dirname, join } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ export const MIN_NODE_MAJOR = 24;
9
+
10
+ export function conventionalHarnessHome(home = homedir()) {
11
+ return join(home, ".tlc", "harness");
12
+ }
13
+
14
+ function samePath(left, right, resolve) {
15
+ try {
16
+ return resolve(left) === resolve(right);
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ // hazard: ESM resolves import.meta.url to the realpath, and the bash wrappers walk readlink before
23
+ // invoking, so both binDir and argv[1] can name the checkout rather than the install path. Anything derived
24
+ // from this value is written into hook files and compared by doctor, so the checkout leaking in here made
25
+ // generated shims point at a directory that only exists on the machine that ran init.
26
+ // invariant: the conventional path wins only when it resolves to the same runtime — verified, never assumed,
27
+ // so a deliberately relocated install is left alone.
28
+ /**
29
+ * why: an npm-installed copy lives under a directory npm replaces wholesale on update, so anything the runtime
30
+ * writes there is deleted by the next `npm i -g` — measured on the packed tarball, which put `runtime-cache.json`
31
+ * inside the package on its first run, where the global lesson tier and the cross-repository spool would follow.
32
+ *
33
+ * invariant: the installed runtime under the conventional home wins, and the npm copy is only the delivery
34
+ * vehicle plus the CLI shim that reaches it. Hooks already name the conventional path directly, so the hot path
35
+ * never asks this question.
36
+ */
37
+ export function isPackagedCopy(candidate) {
38
+ return candidate.split(/[/\\]/).includes("node_modules");
39
+ }
40
+
41
+ export function resolveHarnessHome(
42
+ binDir,
43
+ env = process.env,
44
+ invoked = process.argv[1],
45
+ deps = { realpath: realpathSync, home: homedir, exists: existsSync },
46
+ ) {
47
+ const fromEnv = env.TLC_HOME?.trim();
48
+ if (fromEnv) {
49
+ return fromEnv;
50
+ }
51
+ const candidate = invoked?.endsWith("tlc-exec.mjs") ? join(dirname(invoked), "..") : join(binDir, "..");
52
+ const conventional = conventionalHarnessHome(deps.home());
53
+ if (conventional !== candidate && samePath(conventional, candidate, deps.realpath)) {
54
+ return conventional;
55
+ }
56
+ // invariant: only when that home actually holds a runtime. Before `tlc harness install` has ever run there is
57
+ // nothing there, and the package has to be able to run itself in order to put it there.
58
+ const exists = deps.exists ?? existsSync;
59
+ if (isPackagedCopy(candidate) && exists(join(conventional, "bin", "tlc-exec.mjs"))) {
60
+ return conventional;
61
+ }
62
+ return candidate;
63
+ }
64
+
65
+ export function bunExecutableName(platform = process.platform) {
66
+ return platform === "win32" ? "bun.exe" : "bun";
67
+ }
68
+
69
+ export function findBunOnPath(env = process.env, platform = process.platform) {
70
+ const pathValue = env.PATH ?? "";
71
+ const bunName = bunExecutableName(platform);
72
+ for (const dir of pathValue.split(delimiter)) {
73
+ if (!dir) {
74
+ continue;
75
+ }
76
+ const candidate = join(dir, bunName);
77
+ if (existsSync(candidate)) {
78
+ return candidate;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ export function runtimeCachePath(harnessHome) {
85
+ return join(harnessHome, "state", "runtime-cache.json");
86
+ }
87
+
88
+ export function readRuntimeCache(harnessHome) {
89
+ const cachePath = runtimeCachePath(harnessHome);
90
+ if (!existsSync(cachePath)) {
91
+ return null;
92
+ }
93
+ try {
94
+ const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
95
+ if (parsed && typeof parsed === "object" && "bunPath" in parsed) {
96
+ return parsed;
97
+ }
98
+ return null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * hazard: this used to write unconditionally. On the first run of an npm-installed copy the home is still the
106
+ * package, so it dropped a cache file into global `node_modules` — harmless on a prefix you own, and `EACCES`
107
+ * on one installed with sudo, which would crash the bootstrap before it could install anything. The cache is
108
+ * derived, so failing to write it costs one PATH scan per invocation and nothing else.
109
+ */
110
+ export function writeRuntimeCache(harnessHome, bunPath) {
111
+ const record = { bunPath, checkedAt: new Date().toISOString() };
112
+ if (isPackagedCopy(harnessHome)) {
113
+ return record;
114
+ }
115
+ try {
116
+ const cachePath = runtimeCachePath(harnessHome);
117
+ mkdirSync(dirname(cachePath), { recursive: true });
118
+ writeFileSync(cachePath, `${JSON.stringify(record)}\n`);
119
+ } catch {
120
+ // why: an unwritable runtime home degrades to probing every time, which is correct and merely slower.
121
+ }
122
+ return record;
123
+ }
124
+
125
+ export function resolveBunPath(harnessHome, env = process.env, platform = process.platform) {
126
+ const cached = readRuntimeCache(harnessHome);
127
+ if (cached) {
128
+ return cached.bunPath;
129
+ }
130
+ const found = findBunOnPath(env, platform);
131
+ writeRuntimeCache(harnessHome, found);
132
+ return found;
133
+ }
134
+
135
+ export function entrySourceCandidates(harnessHome, entry) {
136
+ return [
137
+ entry === "tlc-cli" ? join(harnessHome, "bin", "tlc-cli.ts") : null,
138
+ join(harnessHome, "src", "entrypoints", `${entry}.ts`),
139
+ join(harnessHome, "src", `${entry}.ts`),
140
+ join(harnessHome, "tools", `${entry}.ts`),
141
+ ].filter((candidate) => candidate !== null);
142
+ }
143
+
144
+ export function resolveEntrySource(harnessHome, entry) {
145
+ for (const candidate of entrySourceCandidates(harnessHome, entry)) {
146
+ if (existsSync(candidate)) {
147
+ return candidate;
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+
153
+ export function decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExists, srcPath }) {
154
+ const distPath = join(harnessHome, "dist", `${entry}.mjs`);
155
+ if (bunPath && srcPath && distExists) {
156
+ return { kind: "run", command: bunPath, args: ["run", srcPath] };
157
+ }
158
+ if (nodeMajor >= MIN_NODE_MAJOR && distExists) {
159
+ return { kind: "run", command: process.execPath, args: [distPath] };
160
+ }
161
+ if (bunPath && srcPath) {
162
+ return { kind: "run", command: bunPath, args: ["run", srcPath] };
163
+ }
164
+ if (nodeMajor >= MIN_NODE_MAJOR) {
165
+ return {
166
+ kind: "error",
167
+ status: 1,
168
+ message: [
169
+ `tlc: Node ${process.version} found, but dist/${entry}.mjs is missing.`,
170
+ ` Run: ${join(harnessHome, "bin", "tlc-build")}`,
171
+ ].join("\n"),
172
+ };
173
+ }
174
+ if (nodeMajor > 0 && nodeMajor < MIN_NODE_MAJOR) {
175
+ return {
176
+ kind: "error",
177
+ status: 1,
178
+ message: [
179
+ `tlc: no supported hook runtime (Node ${process.version}, Bun not found).`,
180
+ " Either install Bun: curl -fsSL https://bun.sh/install | bash",
181
+ ` or Node >= ${MIN_NODE_MAJOR}: https://nodejs.org/`,
182
+ " Then reload the editor session. Until then this hook does nothing.",
183
+ ].join("\n"),
184
+ };
185
+ }
186
+ return {
187
+ kind: "error",
188
+ status: 127,
189
+ message: [
190
+ `tlc: need Node.js ${MIN_NODE_MAJOR}+ with dist/, or Bun as optional fallback.`,
191
+ " Install: https://nodejs.org/ (prefer 24 LTS or 26 Current)",
192
+ ].join("\n"),
193
+ };
194
+ }
195
+
196
+ function run(harnessHome, command, commandArgs, origin = harnessHome) {
197
+ const result = spawnSync(command, commandArgs, {
198
+ stdio: "inherit",
199
+ // why: `TLC_ORIGIN` is where this copy physically lives, which is not `TLC_HOME` once an npm-installed shim
200
+ // is driving the runtime installed under the conventional path. `tlc harness install` needs the former as
201
+ // its source and the latter as its destination, and nothing else in the runtime reads it.
202
+ env: {
203
+ ...process.env,
204
+ TLC_HOME: harnessHome,
205
+ TLC_ORIGIN: origin,
206
+ // hazard: derived once, by the outermost launcher, and inherited after that. `tlc harness install` reaches
207
+ // the tool through the CLI, so the launcher runs twice — and the second one saw the `TLC_HOME` the first
208
+ // one had just set, concluded the operator had chosen it, and installed the runtime on top of itself.
209
+ // Measured against the packed tarball: "runtime already at <package> — nothing to copy".
210
+ TLC_HOME_FROM_ENV: process.env.TLC_HOME_FROM_ENV ?? (process.env.TLC_HOME?.trim() ? "1" : "0"),
211
+ },
212
+ shell: false,
213
+ });
214
+ if (result.error) {
215
+ console.error(`tlc: failed to start ${command}: ${result.error.message}`);
216
+ process.exit(127);
217
+ }
218
+ process.exit(result.status ?? 1);
219
+ }
220
+
221
+ export function main(argv = process.argv) {
222
+ const binDir = dirname(fileURLToPath(import.meta.url));
223
+ const harnessHome = resolveHarnessHome(binDir);
224
+
225
+ const entry = argv[2];
226
+ if (!entry) {
227
+ console.error("usage: tlc-exec <entry> [args...]");
228
+ console.error(" entry: session-start | tool-before | stop | doctor | ...");
229
+ process.exit(2);
230
+ }
231
+ const args = argv.slice(3);
232
+
233
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
234
+ const distExists = existsSync(join(harnessHome, "dist", `${entry}.mjs`));
235
+ const srcPath = resolveEntrySource(harnessHome, entry);
236
+ const bunPath = resolveBunPath(harnessHome);
237
+
238
+ const decision = decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExists, srcPath });
239
+ if (decision.kind === "error") {
240
+ console.error(decision.message);
241
+ process.exit(decision.status);
242
+ }
243
+ run(harnessHome, decision.command, [...decision.args, ...args], join(binDir, ".."));
244
+ }
245
+
246
+ if (import.meta.main) {
247
+ main();
248
+ }
package/bin/tlc.cmd ADDED
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ setlocal
3
+ node "%~dp0tlc-exec.mjs" tlc-cli %*
4
+ exit /b %ERRORLEVEL%
package/bin/tlc.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // why: npm's bin shim invokes its target with node on every platform, so the bash wrapper `bin/tlc` cannot be
3
+ // the published entry — a shell script behind a generated .cmd is the classic broken global install on Windows.
4
+ // The two wrappers stay for the git-clone route, which puts them on PATH directly.
5
+ import { main } from "./tlc-exec.mjs";
6
+
7
+ main([process.argv[0] ?? "node", process.argv[1] ?? "tlc", "tlc-cli", ...process.argv.slice(2)]);
@@ -0,0 +1,40 @@
1
+ import type { ProviderWiring, WiringEntry } from "../src/contracts/index.ts";
2
+
3
+ export type CursorHookDef = {
4
+ command: string;
5
+ timeout: number;
6
+ failClosed?: true;
7
+ matcher?: string;
8
+ loop_limit?: number;
9
+ };
10
+
11
+ export type CursorHooksDocument = {
12
+ version: 1;
13
+ hooks: Record<string, CursorHookDef[]>;
14
+ };
15
+
16
+ export function renderCursorHooksDocument(entries: readonly WiringEntry[]): CursorHooksDocument;
17
+
18
+ export function isCursorWired(targetPath: string): boolean;
19
+
20
+ export type ApplyOptions = { force?: boolean };
21
+
22
+ export type CursorApplyResult =
23
+ | { status: "written"; target: string }
24
+ | { status: "unchanged"; target: string }
25
+ | { status: "refused"; target: string; reason: string };
26
+
27
+ export function applyCursorWiring(wiring: ProviderWiring, options?: ApplyOptions): CursorApplyResult;
28
+
29
+ export type ClaudeApplyResult =
30
+ | { status: "merged"; target: string }
31
+ | { status: "unchanged"; target: string }
32
+ | { status: "failed"; target: string; reason: string };
33
+
34
+ export type ApplyResult = CursorApplyResult | ClaudeApplyResult;
35
+
36
+ export function applyProviderWiring(wiring: ProviderWiring, options?: ApplyOptions): ApplyResult;
37
+
38
+ export function isProviderHomePresent(wiring: ProviderWiring): boolean;
39
+
40
+ export function main(): void;
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { applyClaudeWiring } from "../src/providers/claude/claude.wiring.ts";
6
+ import { providers } from "../src/providers/index.ts";
7
+
8
+ const CURSOR_MARKER = "tlc-exec.mjs";
9
+
10
+ function quoteIfNeeded(token) {
11
+ return token.includes(" ") ? `"${token}"` : token;
12
+ }
13
+
14
+ function commandStringFor(entry) {
15
+ return [entry.command, ...entry.args].map(quoteIfNeeded).join(" ");
16
+ }
17
+
18
+ export function renderCursorHooksDocument(entries) {
19
+ const hooks = {};
20
+ for (const entry of entries) {
21
+ const rendered = { command: commandStringFor(entry), timeout: entry.timeoutSeconds };
22
+ if (entry.failClosed) {
23
+ rendered.failClosed = true;
24
+ }
25
+ if (entry.matcher !== undefined) {
26
+ rendered.matcher = entry.matcher;
27
+ }
28
+ if (entry.loopLimit !== undefined) {
29
+ rendered.loop_limit = entry.loopLimit;
30
+ }
31
+ hooks[entry.hookEvent] = [...(hooks[entry.hookEvent] ?? []), rendered];
32
+ }
33
+ return { version: 1, hooks };
34
+ }
35
+
36
+ export function isCursorWired(targetPath) {
37
+ return existsSync(targetPath) && readFileSync(targetPath, "utf8").includes(CURSOR_MARKER);
38
+ }
39
+
40
+ export function applyCursorWiring(wiring, { force = false } = {}) {
41
+ const targetPath = wiring.target;
42
+ const document = renderCursorHooksDocument(wiring.entries);
43
+ const rendered = `${JSON.stringify(document, null, 2)}\n`;
44
+
45
+ if (existsSync(targetPath) && !force) {
46
+ if (isCursorWired(targetPath)) {
47
+ return { status: "unchanged", target: targetPath };
48
+ }
49
+ return {
50
+ status: "refused",
51
+ target: targetPath,
52
+ reason: `${targetPath} exists without harness entries — rerun with --force to overwrite, or merge manually.`,
53
+ };
54
+ }
55
+
56
+ mkdirSync(dirname(targetPath), { recursive: true });
57
+ writeFileSync(targetPath, rendered);
58
+ return { status: "written", target: targetPath };
59
+ }
60
+
61
+ export function applyProviderWiring(wiring, { force = false } = {}) {
62
+ if (wiring.strategy === "replace") {
63
+ return applyCursorWiring(wiring, { force });
64
+ }
65
+ const result = applyClaudeWiring(wiring.target, wiring.entries);
66
+ if (!result.ok) {
67
+ return { status: "failed", target: wiring.target, reason: result.error };
68
+ }
69
+ return { status: result.changed ? "merged" : "unchanged", target: wiring.target };
70
+ }
71
+
72
+ export function isProviderHomePresent(wiring) {
73
+ return existsSync(dirname(wiring.target));
74
+ }
75
+
76
+ function report(result) {
77
+ switch (result.status) {
78
+ case "written":
79
+ console.log(`hooks: wrote ${result.target}`);
80
+ return true;
81
+ case "merged":
82
+ console.log(`hooks: merged ${result.target}`);
83
+ return true;
84
+ case "unchanged":
85
+ console.log(`hooks: unchanged (${result.target})`);
86
+ return true;
87
+ case "refused":
88
+ console.error(`hooks: ${result.reason}`);
89
+ return false;
90
+ case "failed":
91
+ console.error(`hooks: failed to update ${result.target}: ${result.reason}`);
92
+ return false;
93
+ default:
94
+ return false;
95
+ }
96
+ }
97
+
98
+ export function main() {
99
+ const binDir = dirname(fileURLToPath(import.meta.url));
100
+ // hazard: ESM resolves import.meta.url to the realpath, so deriving the launcher from it bakes
101
+ // the checkout location into every hook. TLC_HOME is the install path and survives a move.
102
+ const harnessHome = process.env.TLC_HOME?.trim() || join(binDir, "..");
103
+ const launcherPath = join(harnessHome, "bin", "tlc-exec.mjs");
104
+ const force = process.argv.includes("--force");
105
+ let anyFailed = false;
106
+
107
+ for (const provider of providers) {
108
+ const wiring = provider.wiring({ launcherPath });
109
+ if (!isProviderHomePresent(wiring)) {
110
+ console.log(`hooks: ${provider.name} not installed — skipping (${dirname(wiring.target)} not found)`);
111
+ continue;
112
+ }
113
+ if (!report(applyProviderWiring(wiring, { force }))) {
114
+ anyFailed = true;
115
+ }
116
+ }
117
+
118
+ process.exitCode = anyFailed ? 1 : 0;
119
+ }
120
+
121
+ if (import.meta.main) {
122
+ main();
123
+ }