@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,74 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { runtimeHome } from "../src/platform/paths.ts";
4
+
5
+ const docsDir = join(runtimeHome(), "docs");
6
+ const topic = (process.argv[2] ?? "").toLowerCase();
7
+
8
+ const TOPICS: Record<string, string> = {
9
+ architecture: "architecture.md",
10
+ concepts: "concepts.md",
11
+ measure: "measure.md",
12
+ metrics: "measure.md",
13
+ prices: "measure.md",
14
+ price: "measure.md",
15
+ cost: "measure.md",
16
+ costs: "measure.md",
17
+ diagnose: "diagnose.md",
18
+ doctor: "diagnose.md",
19
+ debug: "diagnose.md",
20
+ init: "init.md",
21
+ setup: "init.md",
22
+ lessons: "lessons.md",
23
+ lesson: "lessons.md",
24
+ };
25
+
26
+ function printIndex(): void {
27
+ console.log(`tlc harness help
28
+
29
+ TOPICS
30
+ tlc harness help architecture
31
+ tlc harness help concepts
32
+ tlc harness help lessons
33
+ tlc harness help measure
34
+ tlc harness help prices
35
+ tlc harness help diagnose
36
+ tlc harness help init
37
+
38
+ PRICES
39
+ tlc harness prices refresh [all|cursor|litellm]
40
+ tlc harness prices lookup <model-id>
41
+
42
+ ALSO
43
+ tlc harness status | doctor | grind | mode | obs | lessons
44
+ `);
45
+ }
46
+
47
+ if (!topic || topic === "help" || topic === "-h" || topic === "--help") {
48
+ printIndex();
49
+ process.exit(0);
50
+ }
51
+
52
+ const file = TOPICS[topic];
53
+ if (!file) {
54
+ console.error(`unknown topic: ${topic}`);
55
+ printIndex();
56
+ process.exit(1);
57
+ }
58
+
59
+ const path = join(docsDir, file);
60
+ if (!existsSync(path)) {
61
+ console.error(`missing doc: ${path}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ let body = readFileSync(path, "utf8");
66
+ if (topic === "prices" || topic === "price" || topic === "cost" || topic === "costs") {
67
+ const marker = "## Prices";
68
+ const idx = body.indexOf(marker);
69
+ if (idx >= 0) {
70
+ body = `# Prices\n\n${body.slice(idx)}`;
71
+ }
72
+ }
73
+
74
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
@@ -0,0 +1,277 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { applyCursorWiring, renderCursorHooksDocument } from "../bin/write-user-hooks.mjs";
4
+ import type { WiringEntry } from "../src/contracts/index.ts";
5
+ import { DEFAULTS } from "../src/core/policy/policy.defaults.ts";
6
+ import { claudeConfigDir, cursorConfigDir, projectConfigPath, runtimeHome } from "../src/platform/paths.ts";
7
+ import { render, type Screen } from "../src/platform/screen.ts";
8
+ import { PLAIN, type Style } from "../src/platform/style.ts";
9
+ import { applyClaudeWiring } from "../src/providers/claude/claude.wiring.ts";
10
+
11
+ export class UsageError extends Error {}
12
+
13
+ export type InitFlags = {
14
+ dryRun: boolean;
15
+ write: boolean;
16
+ minimal: boolean;
17
+ stdinJson: boolean;
18
+ force: boolean;
19
+ };
20
+
21
+ export function parseFlags(args: readonly string[]): InitFlags {
22
+ return {
23
+ dryRun: args.includes("--dry-run"),
24
+ write: args.includes("--write") || args.includes("--minimal"),
25
+ minimal: args.includes("--minimal"),
26
+ stdinJson: args.includes("--stdin-json"),
27
+ force: args.includes("--force"),
28
+ };
29
+ }
30
+
31
+ export function usageScreen(): Screen {
32
+ return {
33
+ title: "harness init",
34
+ sections: [
35
+ {
36
+ lines: ` tlc harness init --dry-run
37
+ tlc harness init --write [--stdin-json] [--force]
38
+ tlc harness init --minimal
39
+
40
+ --minimal writes a safe agnostic stub (grind/ship off). Prefer the harness-init skill for full discovery.`.split(
41
+ "\n",
42
+ ),
43
+ },
44
+ ],
45
+ };
46
+ }
47
+
48
+ // why: plain by default and never given a style here — the only caller throws it as a UsageError, and an error
49
+ // message is printed on a path that may be redirected.
50
+ export function usageText(style: Style = PLAIN): string {
51
+ return render(usageScreen(), style);
52
+ }
53
+
54
+ export function launcherPath(home = runtimeHome()): string {
55
+ return join(home, "bin", "tlc-exec.mjs");
56
+ }
57
+
58
+ function shimCommand(platform = process.platform): { command: string; argsPrefix: string[] } {
59
+ if (platform === "win32") {
60
+ return { command: "cmd", argsPrefix: ["/c", "node"] };
61
+ }
62
+ return { command: "node", argsPrefix: [] };
63
+ }
64
+
65
+ type ShimSpec = {
66
+ hookEvent: string;
67
+ handler: string;
68
+ timeoutSeconds: number;
69
+ loopLimit?: number;
70
+ matcher?: string;
71
+ };
72
+
73
+ const CURSOR_SHIM_SPECS: readonly ShimSpec[] = [
74
+ { hookEvent: "sessionStart", handler: "session-start", timeoutSeconds: 10 },
75
+ { hookEvent: "sessionEnd", handler: "session-end", timeoutSeconds: 10 },
76
+ { hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 10 },
77
+ { hookEvent: "beforeShellExecution", handler: "tool-before", timeoutSeconds: 10 },
78
+ { hookEvent: "beforeMCPExecution", handler: "tool-before", timeoutSeconds: 10 },
79
+ { hookEvent: "beforeReadFile", handler: "tool-before", timeoutSeconds: 5 },
80
+ { hookEvent: "subagentStart", handler: "subagent-start", timeoutSeconds: 5 },
81
+ { hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
82
+ { hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" },
83
+ ];
84
+
85
+ const CLAUDE_SHIM_SPECS: readonly ShimSpec[] = [
86
+ { hookEvent: "SessionStart", handler: "session-start", timeoutSeconds: 10 },
87
+ { hookEvent: "SessionEnd", handler: "session-end", timeoutSeconds: 10 },
88
+ { hookEvent: "PreToolUse", handler: "tool-before", timeoutSeconds: 10 },
89
+ { hookEvent: "SubagentStart", handler: "subagent-start", timeoutSeconds: 5 },
90
+ { hookEvent: "Stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
91
+ { hookEvent: "MessageDisplay", handler: "response-after", timeoutSeconds: 5 },
92
+ ];
93
+
94
+ export function cursorShimEntries(launcher: string): WiringEntry[] {
95
+ const { command, argsPrefix } = shimCommand();
96
+ return CURSOR_SHIM_SPECS.map((spec) => ({
97
+ hookEvent: spec.hookEvent,
98
+ handler: spec.handler,
99
+ command,
100
+ args: [...argsPrefix, launcher, "shim", spec.handler],
101
+ timeoutSeconds: spec.timeoutSeconds,
102
+ ...(spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}),
103
+ ...(spec.matcher !== undefined ? { matcher: spec.matcher } : {}),
104
+ }));
105
+ }
106
+
107
+ export function claudeShimEntries(launcher: string): WiringEntry[] {
108
+ return CLAUDE_SHIM_SPECS.map((spec) => ({
109
+ hookEvent: spec.hookEvent,
110
+ handler: spec.handler,
111
+ command: "node",
112
+ args: [launcher, "shim", spec.handler],
113
+ timeoutSeconds: spec.timeoutSeconds,
114
+ ...(spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}),
115
+ }));
116
+ }
117
+
118
+ export const GITIGNORE_LINE = ".tlc/harness/state/";
119
+
120
+ export function mergeGitignore(root: string): { text: string; changed: boolean } {
121
+ const path = join(root, ".gitignore");
122
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
123
+ const lines = existing.split("\n");
124
+ const alreadyPresent = lines.includes(GITIGNORE_LINE);
125
+ if (alreadyPresent) {
126
+ return { text: existing.endsWith("\n") || existing === "" ? existing : `${existing}\n`, changed: false };
127
+ }
128
+ lines.push(GITIGNORE_LINE);
129
+ const withoutTrailingBlank = lines.filter((line, index, all) => line.length > 0 || index < all.length - 1);
130
+ return { text: `${withoutTrailingBlank.join("\n").replace(/\n+$/, "")}\n`, changed: true };
131
+ }
132
+
133
+ export function resolvePolicy(root: string, flags: InitFlags, stdinText: string | null): unknown {
134
+ if (flags.stdinJson && !flags.minimal) {
135
+ if (!stdinText || stdinText.trim() === "") {
136
+ throw new Error("stdin-json: empty stdin");
137
+ }
138
+ return JSON.parse(stdinText);
139
+ }
140
+ if (!flags.minimal && !flags.stdinJson && existsSync(projectConfigPath(root))) {
141
+ return JSON.parse(readFileSync(projectConfigPath(root), "utf8"));
142
+ }
143
+ return DEFAULTS;
144
+ }
145
+
146
+ export type ProviderPresence = { cursor: boolean; claude: boolean };
147
+
148
+ export function detectProviders(dirs: { cursor?: string; claude?: string } = {}): ProviderPresence {
149
+ return {
150
+ cursor: existsSync(dirs.cursor ?? cursorConfigDir()),
151
+ claude: existsSync(dirs.claude ?? claudeConfigDir()),
152
+ };
153
+ }
154
+
155
+ export type InitPlan = {
156
+ policy: unknown;
157
+ cursorHooksDocument: unknown | null;
158
+ claudeHooksPreview: WiringEntry[] | null;
159
+ gitignoreLine: string;
160
+ };
161
+
162
+ export function buildPlan(
163
+ root: string,
164
+ flags: InitFlags,
165
+ stdinText: string | null,
166
+ presence: ProviderPresence,
167
+ ): InitPlan {
168
+ const policy = resolvePolicy(root, flags, stdinText);
169
+ const launcher = launcherPath();
170
+ return {
171
+ policy,
172
+ cursorHooksDocument: presence.cursor ? renderCursorHooksDocument(cursorShimEntries(launcher)) : null,
173
+ claudeHooksPreview: presence.claude ? claudeShimEntries(launcher) : null,
174
+ gitignoreLine: GITIGNORE_LINE,
175
+ };
176
+ }
177
+
178
+ export type ApplyOutcome = {
179
+ configPath: string;
180
+ cursor: { skipped: true } | { skipped: false; status: string; target: string };
181
+ claude: { skipped: true } | { skipped: false; status: string; target: string };
182
+ };
183
+
184
+ export function applyPlan(
185
+ root: string,
186
+ flags: InitFlags,
187
+ presence: ProviderPresence,
188
+ stdinText: string | null,
189
+ ): ApplyOutcome {
190
+ const policy = resolvePolicy(root, flags, stdinText);
191
+ const configPath = projectConfigPath(root);
192
+ mkdirSync(dirname(configPath), { recursive: true });
193
+ writeFileSync(configPath, `${JSON.stringify(policy, null, 2)}\n`);
194
+
195
+ const launcher = launcherPath();
196
+
197
+ const cursor = presence.cursor
198
+ ? (() => {
199
+ const result = applyCursorWiring(
200
+ {
201
+ target: join(root, ".cursor", "hooks.json"),
202
+ strategy: "replace",
203
+ entries: cursorShimEntries(launcher),
204
+ },
205
+ { force: flags.force },
206
+ );
207
+ return { skipped: false as const, status: result.status, target: result.target };
208
+ })()
209
+ : { skipped: true as const };
210
+
211
+ const claude = presence.claude
212
+ ? (() => {
213
+ const result = applyClaudeWiring(join(root, ".claude", "settings.json"), claudeShimEntries(launcher));
214
+ return {
215
+ skipped: false as const,
216
+ status: result.ok ? (result.changed ? "written" : "unchanged") : "failed",
217
+ target: join(root, ".claude", "settings.json"),
218
+ };
219
+ })()
220
+ : { skipped: true as const };
221
+
222
+ const gitignore = mergeGitignore(root);
223
+ writeFileSync(join(root, ".gitignore"), gitignore.text);
224
+
225
+ return { configPath, cursor, claude };
226
+ }
227
+
228
+ async function readStdin(): Promise<string> {
229
+ const chunks: Buffer[] = [];
230
+ for await (const chunk of process.stdin) {
231
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
232
+ }
233
+ return Buffer.concat(chunks).toString("utf8").trim();
234
+ }
235
+
236
+ export async function main(argv: string[]): Promise<void> {
237
+ const root = process.env.TLC_PROJECT_DIR ?? process.cwd();
238
+ const flags = parseFlags(argv);
239
+
240
+ if (!flags.dryRun && !flags.write) {
241
+ throw new UsageError(usageText());
242
+ }
243
+
244
+ const stdinText = flags.stdinJson ? await readStdin() : null;
245
+ const presence = detectProviders();
246
+
247
+ if (flags.dryRun) {
248
+ console.log(JSON.stringify(buildPlan(root, flags, stdinText, presence), null, 2));
249
+ return;
250
+ }
251
+
252
+ const outcome = applyPlan(root, flags, presence, stdinText);
253
+ console.log(`wrote ${outcome.configPath}`);
254
+ if (outcome.cursor.skipped) {
255
+ console.log("init: cursor not installed — skipped project hooks.json");
256
+ } else {
257
+ console.log(`hooks: ${outcome.cursor.status} ${outcome.cursor.target}`);
258
+ }
259
+ if (outcome.claude.skipped) {
260
+ console.log("init: claude not installed — skipped project settings.json");
261
+ } else {
262
+ console.log(`hooks: ${outcome.claude.status} ${outcome.claude.target}`);
263
+ }
264
+ console.log("updated .gitignore harness entries");
265
+ }
266
+
267
+ if (import.meta.main) {
268
+ try {
269
+ await main(process.argv.slice(2));
270
+ } catch (error) {
271
+ if (error instanceof UsageError) {
272
+ console.error(error.message);
273
+ process.exit(1);
274
+ }
275
+ throw error;
276
+ }
277
+ }
@@ -0,0 +1,164 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, relative, resolve, sep } from "node:path";
3
+ import { NPM_MARKER, NPM_PACKAGE } from "../bin/tlc-cli.ts";
4
+ import { conventionalRuntimeHome, runtimeHome, runtimeHomeWasChosen } from "../src/platform/paths.ts";
5
+ import { type Row, render, type Screen } from "../src/platform/screen.ts";
6
+ import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
7
+
8
+ /**
9
+ * why: an npm-installed copy lives under a directory npm replaces wholesale, so the runtime cannot keep its state
10
+ * there — measured on the packed tarball, which wrote `runtime-cache.json` inside the package on its first run.
11
+ * The package is the delivery vehicle; the runtime that hooks name stays at the conventional home, and this is
12
+ * what puts the code there ([/decisions/ad-056.md](/decisions/ad-056.md)).
13
+ *
14
+ * invariant: an entry here is replaced wholesale, so a file deleted upstream does not survive the update. Nothing
15
+ * outside this list is touched, which is what keeps `config.json`, `state/` and `flags/` the operator's.
16
+ */
17
+ export const RUNTIME_PAYLOAD = [
18
+ "bin",
19
+ "capabilities",
20
+ "dist",
21
+ "docs",
22
+ "skills",
23
+ "src",
24
+ "tools",
25
+ "config.example.json",
26
+ "model-aliases.json",
27
+ "model-prices.cursor.json",
28
+ "model-prices.json",
29
+ "package.json",
30
+ ] as const;
31
+
32
+ /** Never copied and never removed. The reason the split exists. */
33
+ export const OPERATOR_OWNED = ["config.json", "state", "flags"] as const;
34
+
35
+ /**
36
+ * Inside a payload entry and still not shipped.
37
+ *
38
+ * why: `tools/dev` holds the checks that validate *this* repository — its module boundaries, its screen contract,
39
+ * its decision records. A user's install has none of that to validate, and with Bun present the launcher resolves
40
+ * an entry straight from source, so copying them would put runnable repo-only commands on their machine. The
41
+ * clone route is different on purpose: a checkout is the repository, and a contributor needs them
42
+ * ([/decisions/ad-068.md](/decisions/ad-068.md)).
43
+ */
44
+ export const NOT_SHIPPED = [join("tools", "dev"), join("tools", "__test__")] as const;
45
+
46
+ export function isShipped(relativePath: string): boolean {
47
+ const normalised = relativePath.split(sep).join("/");
48
+ return !NOT_SHIPPED.some((excluded) => {
49
+ const prefix = excluded.split(sep).join("/");
50
+ return normalised === prefix || normalised.startsWith(`${prefix}/`);
51
+ });
52
+ }
53
+
54
+ export type InstallReport = {
55
+ kind: "copied" | "in-place";
56
+ source: string;
57
+ dest: string;
58
+ entries: string[];
59
+ missing: string[];
60
+ };
61
+
62
+ /** The physical location of the copy that launched us, which is not the home once an npm shim is driving one. */
63
+ export function originRoot(env: NodeJS.ProcessEnv = process.env): string {
64
+ const declared = env.TLC_ORIGIN?.trim();
65
+ if (declared && declared.length > 0) {
66
+ return resolve(declared);
67
+ }
68
+ // hazard: `runtimeHome()` reads `process.env` and would ignore the env passed in, which made this untestable
69
+ // and would have read the wrong home for any caller that supplies one.
70
+ const home = env.TLC_HOME?.trim();
71
+ return home && home.length > 0 ? resolve(home) : conventionalRuntimeHome();
72
+ }
73
+
74
+ export function installRuntime(source: string, dest: string): InstallReport {
75
+ if (resolve(source) === resolve(dest)) {
76
+ // why: the git route already has the code at the destination. Copying a directory onto itself is the one
77
+ // input that turns a sync into data loss.
78
+ return { kind: "in-place", source, dest, entries: [], missing: [] };
79
+ }
80
+ mkdirSync(dest, { recursive: true });
81
+ const entries: string[] = [];
82
+ const missing: string[] = [];
83
+ for (const entry of RUNTIME_PAYLOAD) {
84
+ const from = join(source, entry);
85
+ if (!existsSync(from)) {
86
+ missing.push(entry);
87
+ continue;
88
+ }
89
+ const to = join(dest, entry);
90
+ rmSync(to, { recursive: true, force: true });
91
+ cpSync(from, to, {
92
+ recursive: true,
93
+ filter: (src) => isShipped(relative(source, src)),
94
+ });
95
+ entries.push(entry);
96
+ }
97
+ // why: a directory with no `.git` used to classify as `unmanaged`, which doctor reports as a failure and update
98
+ // answers with "re-install with the one-liner". The marker is how the thing that created this directory says
99
+ // what it is, rather than leaving every later reader to guess from the contents.
100
+ writeFileSync(
101
+ join(dest, NPM_MARKER),
102
+ `Installed by \`tlc harness install\` from ${source}.\nUpdate with: npm i -g ${NPM_PACKAGE}@latest && tlc harness install\n`,
103
+ "utf8",
104
+ );
105
+ const config = join(dest, "config.json");
106
+ const example = join(dest, "config.example.json");
107
+ if (!existsSync(config) && existsSync(example)) {
108
+ writeFileSync(config, readFileSync(example, "utf8"), "utf8");
109
+ }
110
+ return { kind: "copied", source, dest, entries, missing };
111
+ }
112
+
113
+ export function installScreen(report: InstallReport): Screen {
114
+ if (report.kind === "in-place") {
115
+ return {
116
+ title: "harness install",
117
+ sections: [
118
+ { rows: [{ label: "runtime", value: `already at ${report.dest} — nothing to copy`, level: "ok" }] },
119
+ ],
120
+ };
121
+ }
122
+ const rows: Row[] = [
123
+ { label: "installed", value: `${report.entries.length} path(s) → ${report.dest}`, level: "ok" },
124
+ { label: "from", value: report.source },
125
+ ];
126
+ if (report.missing.length > 0) {
127
+ // why: a payload entry absent from the source is a packaging fault, not a passing install. `tools/` was
128
+ // missing from the published `files` list the first time this ran.
129
+ rows.push({
130
+ label: "packaging",
131
+ value: `MISSING from the source: ${report.missing.join(", ")}`,
132
+ level: "fail",
133
+ });
134
+ }
135
+ return { title: "harness install", sections: [{ rows }] };
136
+ }
137
+
138
+ export function installReportText(report: InstallReport, style: Style = PLAIN): string {
139
+ return render(installScreen(report), style);
140
+ }
141
+
142
+ /**
143
+ * hazard: not the resolved home. On the very first npm run nothing is installed yet, so the resolved home *is*
144
+ * the package — and asking it where to install would answer "here", which is the one place that cannot hold
145
+ * state. The conventional path is the answer unless the operator named one themselves.
146
+ */
147
+ export function installDest(env: NodeJS.ProcessEnv = process.env): string {
148
+ // hazard: the destination cannot be carried on `TLC_HOME`. Setting that makes the launcher resolve the runtime
149
+ // to a directory that does not exist yet on a first install, and it then refuses for want of the bundles it was
150
+ // being asked to put there. A separate variable keeps resolution and destination independent.
151
+ const explicit = env.TLC_INSTALL_DEST?.trim();
152
+ if (explicit && explicit.length > 0) {
153
+ return resolve(explicit);
154
+ }
155
+ return runtimeHomeWasChosen(env) ? runtimeHome(env) : conventionalRuntimeHome();
156
+ }
157
+
158
+ if (import.meta.main) {
159
+ const source = originRoot();
160
+ const dest = installDest();
161
+ const report = installRuntime(source, dest);
162
+ console.log(installReportText(report, createStyle()));
163
+ process.exit(report.missing.length > 0 ? 1 : 0);
164
+ }