@tech-leads-club/harness-toolkit 0.2.1 → 0.3.0

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 (142) hide show
  1. package/README.md +22 -26
  2. package/bin/tlc-build.mjs +93 -0
  3. package/bin/tlc-cli.ts +117 -58
  4. package/bin/tlc-exec.mjs +16 -13
  5. package/dist/compact-before.mjs +151 -15
  6. package/dist/doctor.mjs +288 -45
  7. package/dist/help-topic.mjs +0 -0
  8. package/dist/init-project.mjs +15 -14
  9. package/dist/install-runtime.mjs +100 -17
  10. package/dist/lessons-cli.mjs +152 -8
  11. package/dist/obs-cli.mjs +149 -8
  12. package/dist/price-lookup.mjs +45 -23
  13. package/dist/prompt-submit.mjs +151 -15
  14. package/dist/refresh-model-prices.mjs +7190 -46
  15. package/dist/response-after.mjs +151 -15
  16. package/dist/run.mjs +151 -15
  17. package/dist/session-end.mjs +151 -15
  18. package/dist/session-start.mjs +151 -15
  19. package/dist/shim.mjs +7040 -16
  20. package/dist/stop.mjs +151 -15
  21. package/dist/subagent-start.mjs +151 -15
  22. package/dist/subagent-stop.mjs +151 -15
  23. package/dist/support.mjs +149 -8
  24. package/dist/tlc-cli.mjs +289 -89
  25. package/dist/tool-after.mjs +196 -38
  26. package/dist/tool-before.mjs +151 -15
  27. package/dist/tool-failure.mjs +151 -15
  28. package/dist/uninstall-runtime.mjs +9 -10
  29. package/docs/log.md +7 -0
  30. package/docs/measure.md +35 -31
  31. package/package.json +6 -5
  32. package/src/core/core.facade.ts +18 -0
  33. package/src/core/index.ts +3 -0
  34. package/src/core/pricing/pricing.freshness.ts +118 -0
  35. package/src/core/release/release.version.ts +147 -0
  36. package/src/core/shim/shim.precedence.ts +72 -0
  37. package/src/core/skill/skill.link.ts +92 -0
  38. package/src/entrypoints/shim.ts +49 -11
  39. package/src/platform/fs-atomic.ts +61 -23
  40. package/src/platform/links.ts +73 -0
  41. package/src/platform/paths.ts +27 -0
  42. package/src/platform/pricing.ts +139 -31
  43. package/src/providers/cursor/cursor.wiring.ts +11 -8
  44. package/tools/doctor.ts +127 -10
  45. package/tools/init-project.ts +34 -14
  46. package/tools/install-runtime.ts +89 -6
  47. package/tools/refresh-model-prices.ts +242 -75
  48. package/tools/uninstall-runtime.ts +23 -19
  49. package/CHANGELOG.md +0 -95
  50. package/bin/tlc-build +0 -80
  51. package/bin/tlc-exec +0 -10
  52. package/bin/tlc-exec.cmd +0 -4
  53. package/docs/decisions/ad-001.md +0 -32
  54. package/docs/decisions/ad-002.md +0 -51
  55. package/docs/decisions/ad-003.md +0 -30
  56. package/docs/decisions/ad-004.md +0 -37
  57. package/docs/decisions/ad-005.md +0 -36
  58. package/docs/decisions/ad-006.md +0 -49
  59. package/docs/decisions/ad-007.md +0 -36
  60. package/docs/decisions/ad-008.md +0 -54
  61. package/docs/decisions/ad-009.md +0 -61
  62. package/docs/decisions/ad-010.md +0 -45
  63. package/docs/decisions/ad-011.md +0 -59
  64. package/docs/decisions/ad-012.md +0 -71
  65. package/docs/decisions/ad-013.md +0 -87
  66. package/docs/decisions/ad-014.md +0 -56
  67. package/docs/decisions/ad-015.md +0 -33
  68. package/docs/decisions/ad-016.md +0 -98
  69. package/docs/decisions/ad-017.md +0 -65
  70. package/docs/decisions/ad-018.md +0 -77
  71. package/docs/decisions/ad-019.md +0 -75
  72. package/docs/decisions/ad-020.md +0 -88
  73. package/docs/decisions/ad-021.md +0 -57
  74. package/docs/decisions/ad-022.md +0 -120
  75. package/docs/decisions/ad-023.md +0 -87
  76. package/docs/decisions/ad-024.md +0 -85
  77. package/docs/decisions/ad-025.md +0 -92
  78. package/docs/decisions/ad-026.md +0 -131
  79. package/docs/decisions/ad-027.md +0 -90
  80. package/docs/decisions/ad-028.md +0 -90
  81. package/docs/decisions/ad-029.md +0 -73
  82. package/docs/decisions/ad-030.md +0 -97
  83. package/docs/decisions/ad-031.md +0 -94
  84. package/docs/decisions/ad-032.md +0 -83
  85. package/docs/decisions/ad-033.md +0 -89
  86. package/docs/decisions/ad-034.md +0 -86
  87. package/docs/decisions/ad-035.md +0 -86
  88. package/docs/decisions/ad-036.md +0 -68
  89. package/docs/decisions/ad-037.md +0 -47
  90. package/docs/decisions/ad-038.md +0 -52
  91. package/docs/decisions/ad-039.md +0 -69
  92. package/docs/decisions/ad-040.md +0 -89
  93. package/docs/decisions/ad-041.md +0 -98
  94. package/docs/decisions/ad-042.md +0 -82
  95. package/docs/decisions/ad-043.md +0 -79
  96. package/docs/decisions/ad-044.md +0 -61
  97. package/docs/decisions/ad-045.md +0 -94
  98. package/docs/decisions/ad-046.md +0 -111
  99. package/docs/decisions/ad-047.md +0 -96
  100. package/docs/decisions/ad-048.md +0 -85
  101. package/docs/decisions/ad-049.md +0 -66
  102. package/docs/decisions/ad-050.md +0 -94
  103. package/docs/decisions/ad-051.md +0 -69
  104. package/docs/decisions/ad-052.md +0 -69
  105. package/docs/decisions/ad-053.md +0 -78
  106. package/docs/decisions/ad-054.md +0 -98
  107. package/docs/decisions/ad-055.md +0 -74
  108. package/docs/decisions/ad-056.md +0 -85
  109. package/docs/decisions/ad-057.md +0 -68
  110. package/docs/decisions/ad-058.md +0 -97
  111. package/docs/decisions/ad-059.md +0 -82
  112. package/docs/decisions/ad-060.md +0 -75
  113. package/docs/decisions/ad-061.md +0 -68
  114. package/docs/decisions/ad-062.md +0 -72
  115. package/docs/decisions/ad-063.md +0 -84
  116. package/docs/decisions/ad-064.md +0 -79
  117. package/docs/decisions/ad-065.md +0 -81
  118. package/docs/decisions/ad-066.md +0 -111
  119. package/docs/decisions/ad-067.md +0 -64
  120. package/docs/decisions/ad-068.md +0 -79
  121. package/docs/decisions/ad-069.md +0 -74
  122. package/docs/decisions/ad-070.md +0 -86
  123. package/docs/decisions/ad-071.md +0 -93
  124. package/docs/decisions/ad-072.md +0 -82
  125. package/docs/decisions/ad-073.md +0 -102
  126. package/docs/decisions/ad-074.md +0 -91
  127. package/docs/decisions/ad-075.md +0 -79
  128. package/docs/decisions/ad-076.md +0 -102
  129. package/docs/decisions/ad-077.md +0 -94
  130. package/docs/decisions/ad-078.md +0 -84
  131. package/docs/decisions/ad-079.md +0 -73
  132. package/docs/decisions/ad-080.md +0 -86
  133. package/docs/decisions/ad-081.md +0 -70
  134. package/docs/decisions/ad-082.md +0 -79
  135. package/docs/decisions/ad-083.md +0 -88
  136. package/docs/decisions/index.md +0 -111
  137. package/model-aliases.json +0 -12
  138. package/model-prices.cursor.json +0 -410
  139. package/model-prices.json +0 -1
  140. package/tools/test-env.mjs +0 -28
  141. package/tools/test-env.names.d.mts +0 -1
  142. package/tools/test-env.names.mjs +0 -14
package/dist/doctor.mjs CHANGED
@@ -3,9 +3,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // tools/doctor.ts
5
5
  import { spawnSync } from "node:child_process";
6
- import { existsSync as existsSync29, lstatSync as lstatSync2, readFileSync as readFileSync31, readlinkSync } from "node:fs";
6
+ import { existsSync as existsSync30, lstatSync as lstatSync2, readFileSync as readFileSync32, readlinkSync, realpathSync as realpathSync4 } from "node:fs";
7
7
  import { homedir as homedir3, platform as osPlatform } from "node:os";
8
- import { dirname as dirname9, join as join30 } from "node:path";
8
+ import { basename as basename5, delimiter as delimiter3, dirname as dirname9, join as join31 } from "node:path";
9
9
 
10
10
  // bin/tlc-cli.ts
11
11
  import {
@@ -97,6 +97,9 @@ function cursorConfigDir() {
97
97
  const custom = process.env.CURSOR_CONFIG_DIR?.trim();
98
98
  return custom && custom.length > 0 ? custom : join(homedir(), ".cursor");
99
99
  }
100
+ function providerConfigDirs() {
101
+ return [cursorConfigDir(), claudeConfigDir()];
102
+ }
100
103
 
101
104
  // src/core/attest/attest.service.ts
102
105
  var CHAIN_ROOT = "genesis";
@@ -295,20 +298,27 @@ function readJson(path) {
295
298
  return null;
296
299
  }
297
300
  }
298
- async function withFileLock(lockPath, fn) {
301
+ function isContention(error) {
302
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
303
+ }
304
+ async function withFileLock(lockPath, fn, options = {}) {
305
+ const {
306
+ openLock = (path) => closeSync(openSync(path, "wx")),
307
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
308
+ lockAttempts = 200
309
+ } = options;
299
310
  mkdirSync2(dirname2(lockPath), { recursive: true });
300
- const attempts = 200;
301
311
  let acquired = false;
302
- for (let attempt = 0;attempt < attempts; attempt++) {
312
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
303
313
  try {
304
- closeSync(openSync(lockPath, "wx"));
314
+ openLock(lockPath);
305
315
  acquired = true;
306
316
  break;
307
317
  } catch (error) {
308
- if (errorCode(error) !== "EEXIST") {
318
+ if (!isContention(error)) {
309
319
  throw error;
310
320
  }
311
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
321
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
312
322
  }
313
323
  }
314
324
  if (!acquired) {
@@ -323,14 +333,14 @@ async function withFileLock(lockPath, fn) {
323
333
  }
324
334
  }
325
335
  async function updateJsonAtomic(path, mutator, options) {
326
- const { lockPath, afterWrite, ...atomicOptions } = options;
336
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
327
337
  return withFileLock(lockPath, async () => {
328
338
  const current = readJson(path);
329
339
  const next = mutator(current);
330
340
  await writeJsonAtomic(path, next, atomicOptions);
331
341
  afterWrite?.(path);
332
342
  return next;
333
- });
343
+ }, { openLock, lockSleep, lockAttempts });
334
344
  }
335
345
 
336
346
  // src/core/capability/capability.store.ts
@@ -5278,6 +5288,62 @@ function release(root, provider, session) {
5278
5288
  deletePresenceRecord(root, provider, session);
5279
5289
  }
5280
5290
 
5291
+ // src/core/pricing/pricing.freshness.ts
5292
+ var DEFAULT_TTL_DAYS = 7;
5293
+ var MS_PER_DAY = 86400000;
5294
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5295
+ if (meta === null) {
5296
+ return { state: "absent" };
5297
+ }
5298
+ const stamp = meta.refreshedAt;
5299
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5300
+ return { state: "undated" };
5301
+ }
5302
+ const ageMs = now.getTime() - Date.parse(stamp);
5303
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5304
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5305
+ }
5306
+ function shouldRefetch(state) {
5307
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5308
+ }
5309
+ function freshnessMessage(state, catalogue) {
5310
+ switch (state.state) {
5311
+ case "absent":
5312
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5313
+ case "undated":
5314
+ return `${catalogue}: present but carries no date — it will be refetched`;
5315
+ case "fresh":
5316
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5317
+ default:
5318
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5319
+ }
5320
+ }
5321
+ var MIN_RETAINED_RATIO = 0.5;
5322
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5323
+ if (incomingCount === 0) {
5324
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5325
+ }
5326
+ if (existingCount === 0) {
5327
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5328
+ }
5329
+ if (incomingCount >= existingCount) {
5330
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5331
+ }
5332
+ const retained = incomingCount / existingCount;
5333
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5334
+ replace: false,
5335
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5336
+ };
5337
+ }
5338
+ function describeAge(ageDays) {
5339
+ if (ageDays < 1) {
5340
+ const hours = Math.max(1, Math.round(ageDays * 24));
5341
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5342
+ }
5343
+ const days = Math.round(ageDays);
5344
+ return `${days} day${days === 1 ? "" : "s"}`;
5345
+ }
5346
+
5281
5347
  // src/core/release/release.decisions.ts
5282
5348
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5283
5349
  import { join as join18 } from "node:path";
@@ -5581,6 +5647,69 @@ function evaluateShellCommand(args) {
5581
5647
  return { kind: "allow" };
5582
5648
  }
5583
5649
 
5650
+ // src/core/shim/shim.precedence.ts
5651
+ var LAUNCHER = "tlc-exec";
5652
+ function invocationText(entry) {
5653
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5654
+ }
5655
+ function coversHandler(settings, handler) {
5656
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5657
+ for (const matcher of matchers) {
5658
+ for (const entry of matcher.hooks ?? []) {
5659
+ const text = invocationText(entry);
5660
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5661
+ return true;
5662
+ }
5663
+ }
5664
+ }
5665
+ }
5666
+ return false;
5667
+ }
5668
+ function decideShim(userSettings, handler) {
5669
+ if (userSettings === null) {
5670
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5671
+ }
5672
+ return coversHandler(userSettings, handler) ? {
5673
+ run: false,
5674
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5675
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5676
+ }
5677
+
5678
+ // src/core/skill/skill.link.ts
5679
+ var SKILL_NAME = "harness-init";
5680
+ function skillLinks(runtimeHome2, providerDirs, present) {
5681
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5682
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5683
+ providerDir,
5684
+ source,
5685
+ target: `${providerDir}/skills/${SKILL_NAME}`
5686
+ }));
5687
+ }
5688
+ function linkHealth(target, runtimeHome2, probe) {
5689
+ const resolved = probe.linkTarget(target);
5690
+ if (resolved === null) {
5691
+ return { state: "absent", target };
5692
+ }
5693
+ if (!probe.exists(resolved)) {
5694
+ return { state: "dangling", target, resolved };
5695
+ }
5696
+ const resolveHome = probe.realpath ?? ((path) => path);
5697
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5698
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5699
+ }
5700
+ function linkHealthMessage(health) {
5701
+ switch (health.state) {
5702
+ case "ok":
5703
+ return `linked → ${health.resolved}`;
5704
+ case "dangling":
5705
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5706
+ case "outside-runtime":
5707
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5708
+ default:
5709
+ return "not linked — the provider cannot see the init skill";
5710
+ }
5711
+ }
5712
+
5584
5713
  // src/core/stagnation/stagnation.resolution.ts
5585
5714
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5586
5715
  import { join as join21 } from "node:path";
@@ -6708,6 +6837,21 @@ var coreFacade = {
6708
6837
  readHandoffFile,
6709
6838
  readForeignSlices
6710
6839
  },
6840
+ shim: {
6841
+ coversHandler,
6842
+ decideShim
6843
+ },
6844
+ pricing: {
6845
+ freshness,
6846
+ freshnessMessage,
6847
+ mayReplace,
6848
+ shouldRefetch
6849
+ },
6850
+ skill: {
6851
+ linkHealth,
6852
+ linkHealthMessage,
6853
+ skillLinks
6854
+ },
6711
6855
  lesson: {
6712
6856
  projectLessonsInjectable,
6713
6857
  recordLessonFromFailure,
@@ -6912,6 +7056,7 @@ function writeStdout(text) {
6912
7056
  }
6913
7057
 
6914
7058
  // bin/tlc-cli.ts
7059
+ var NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
6915
7060
  var NPM_MARKER = "installed-from-npm";
6916
7061
  function classifyRuntimePath(dest, probe) {
6917
7062
  if (probe.isSymlink(dest)) {
@@ -6945,19 +7090,17 @@ import { delimiter as delimiter2, dirname as dirname7, join as join27 } from "no
6945
7090
  function isPackagedCopy(candidate) {
6946
7091
  return candidate.split(/[/\\]/).includes("node_modules");
6947
7092
  }
6948
- function bunExecutableName(platform = process.platform) {
6949
- return platform === "win32" ? "bun.exe" : "bun";
6950
- }
6951
- function findBunOnPath(env = process.env, platform = process.platform) {
6952
- const pathValue = env.PATH ?? "";
6953
- const bunName = bunExecutableName(platform);
6954
- for (const dir of pathValue.split(delimiter2)) {
7093
+ var BUN_EXECUTABLE_NAMES = ["bun", "bun.exe"];
7094
+ function findBunOnPath(env = process.env) {
7095
+ for (const dir of (env.PATH ?? "").split(delimiter2)) {
6955
7096
  if (!dir) {
6956
7097
  continue;
6957
7098
  }
6958
- const candidate = join27(dir, bunName);
6959
- if (existsSync26(candidate)) {
6960
- return candidate;
7099
+ for (const name of BUN_EXECUTABLE_NAMES) {
7100
+ const candidate = join27(dir, name);
7101
+ if (existsSync26(candidate)) {
7102
+ return candidate;
7103
+ }
6961
7104
  }
6962
7105
  }
6963
7106
  return null;
@@ -7733,14 +7876,9 @@ var ENTRY_SPECS2 = [
7733
7876
  { hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
7734
7877
  { hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
7735
7878
  ];
7736
- function commandFor(runtime) {
7737
- if (process.platform === "win32") {
7738
- return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
7739
- }
7740
- return { command: "node", argsPrefix: [runtime.launcherPath] };
7741
- }
7742
7879
  function cursorWiring(runtime) {
7743
- const { command, argsPrefix } = commandFor(runtime);
7880
+ const command = "node";
7881
+ const argsPrefix = [runtime.launcherPath];
7744
7882
  const entries = ENTRY_SPECS2.map((spec) => ({
7745
7883
  hookEvent: spec.hookEvent,
7746
7884
  handler: spec.handler,
@@ -7832,6 +7970,50 @@ function isCursorWired(targetPath) {
7832
7970
  }
7833
7971
  if (false) {}
7834
7972
 
7973
+ // src/platform/pricing.ts
7974
+ import { existsSync as existsSync29, readFileSync as readFileSync31 } from "node:fs";
7975
+ import { join as join30 } from "node:path";
7976
+ function cataloguePath() {
7977
+ return join30(runtimeHome(), "model-prices.json");
7978
+ }
7979
+ var cache = new Map;
7980
+ function readCatalogue(path) {
7981
+ if (!existsSync29(path)) {
7982
+ cache.delete(path);
7983
+ return null;
7984
+ }
7985
+ let text;
7986
+ try {
7987
+ text = readFileSync31(path, "utf8");
7988
+ } catch {
7989
+ cache.delete(path);
7990
+ return null;
7991
+ }
7992
+ const hit = cache.get(path);
7993
+ if (hit && hit.text === text) {
7994
+ return hit.value;
7995
+ }
7996
+ let parsed;
7997
+ try {
7998
+ parsed = JSON.parse(text);
7999
+ } catch {
8000
+ cache.delete(path);
8001
+ return null;
8002
+ }
8003
+ cache.set(path, { text, value: parsed });
8004
+ return parsed;
8005
+ }
8006
+ function loadCatalogue() {
8007
+ return readCatalogue(cataloguePath()) ?? {};
8008
+ }
8009
+ function planeMeta() {
8010
+ return loadCatalogue()._meta?.planes ?? {};
8011
+ }
8012
+ function catalogueMeta() {
8013
+ const parsed = readCatalogue(cataloguePath());
8014
+ return parsed === null ? null : parsed._meta ?? {};
8015
+ }
8016
+
7835
8017
  // tools/doctor.ts
7836
8018
  function plural(count, word) {
7837
8019
  return `${count} ${word}${count === 1 ? "" : "s"}`;
@@ -7893,24 +8075,80 @@ function runtimeOwnershipCheck(home) {
7893
8075
  detail: detail[kind]
7894
8076
  };
7895
8077
  }
8078
+ function checkSkillLinks(home, providerDirs = providerConfigDirs(), probe = {
8079
+ linkTarget: (path) => {
8080
+ try {
8081
+ return realpathSync4(path);
8082
+ } catch {
8083
+ try {
8084
+ return readlinkSync(path);
8085
+ } catch {
8086
+ return null;
8087
+ }
8088
+ }
8089
+ },
8090
+ exists: existsSync30,
8091
+ realpath: (path) => {
8092
+ try {
8093
+ return realpathSync4(path);
8094
+ } catch {
8095
+ return path;
8096
+ }
8097
+ }
8098
+ }) {
8099
+ return providerDirs.filter((dir) => existsSync30(dir)).map((dir) => {
8100
+ const health = coreFacade.skill.linkHealth(join31(dir, "skills", "harness-init"), home, probe);
8101
+ return {
8102
+ level: health.state === "ok" ? "ok" : "fail",
8103
+ name: `init skill (${basename5(dir)})`,
8104
+ detail: coreFacade.skill.linkHealthMessage(health)
8105
+ };
8106
+ });
8107
+ }
8108
+ function checkPrices(now = new Date, read = { meta: catalogueMeta, planes: planeMeta }) {
8109
+ const state = coreFacade.pricing.freshness(read.meta(), now);
8110
+ const planes = read.planes();
8111
+ const named = Object.entries(planes).map(([plane, meta]) => `${plane} ${meta.count ?? 0}`).join(", ");
8112
+ return [
8113
+ {
8114
+ level: state.state === "fresh" ? "ok" : "warn",
8115
+ name: "prices",
8116
+ detail: state.state === "fresh" ? `${coreFacade.pricing.freshnessMessage(state, "catalogue")}${named ? ` (${named})` : ""}` : coreFacade.pricing.freshnessMessage(state, "catalogue")
8117
+ }
8118
+ ];
8119
+ }
8120
+ function resolveOnPath(command, env = process.env, exists = existsSync30) {
8121
+ for (const dir of (env.PATH ?? "").split(delimiter3)) {
8122
+ if (!dir) {
8123
+ continue;
8124
+ }
8125
+ for (const name of [command, `${command}.cmd`, `${command}.exe`, `${command}.ps1`]) {
8126
+ const candidate = join31(dir, name);
8127
+ if (exists(candidate)) {
8128
+ return candidate;
8129
+ }
8130
+ }
8131
+ }
8132
+ return null;
8133
+ }
7896
8134
  function checkRuntimePaths(home, platform) {
7897
- const launcher = join30(home, "bin", "tlc-exec.mjs");
7898
- const distSample = join30(home, "dist", "stop.mjs");
7899
- const cliLink = join30(homedir3(), ".local", "bin", platform === "win32" ? "tlc.cmd" : "tlc");
8135
+ const launcher = join31(home, "bin", "tlc-exec.mjs");
8136
+ const distSample = join31(home, "dist", "stop.mjs");
8137
+ const onPath = resolveOnPath("tlc");
7900
8138
  return [
7901
8139
  { level: "ok", name: "platform", detail: platform },
7902
- { level: existsSync29(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
8140
+ { level: existsSync30(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
7903
8141
  runtimeOwnershipCheck(home),
7904
8142
  {
7905
- level: existsSync29(distSample) ? "ok" : "fail",
8143
+ level: existsSync30(distSample) ? "ok" : "fail",
7906
8144
  name: "dist bundles",
7907
- detail: existsSync29(distSample) ? join30(home, "dist") : "missing — run: tlc harness build"
8145
+ detail: existsSync30(distSample) ? join31(home, "dist") : "missing — run: tlc harness build"
7908
8146
  },
7909
- { level: existsSync29(launcher) ? "ok" : "fail", name: "portable launcher", detail: launcher },
8147
+ { level: existsSync30(launcher) ? "ok" : "fail", name: "portable launcher", detail: launcher },
7910
8148
  {
7911
- level: existsSync29(cliLink) || existsSync29(join30(home, "bin", platform === "win32" ? "tlc.cmd" : "tlc")) ? "ok" : "fail",
8149
+ level: onPath === null ? "fail" : "ok",
7912
8150
  name: "CLI on PATH",
7913
- detail: cliLink
8151
+ detail: onPath ?? `no \`tlc\` on PATH — npm i -g ${NPM_PACKAGE}, or \`npm link\` from a clone`
7914
8152
  }
7915
8153
  ];
7916
8154
  }
@@ -7929,15 +8167,15 @@ function wiringProblems(wiring) {
7929
8167
  if (wiring.strategy !== "replace") {
7930
8168
  return [];
7931
8169
  }
7932
- const text = existsSync29(wiring.target) ? readFileSync31(wiring.target, "utf8") : null;
7933
- return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) }, existsSync29);
8170
+ const text = existsSync30(wiring.target) ? readFileSync32(wiring.target, "utf8") : null;
8171
+ return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) }, existsSync30);
7934
8172
  }
7935
8173
  function launcherPathOf(wiring) {
7936
8174
  const first = wiring.entries[0];
7937
8175
  return first?.args.find((arg) => arg.endsWith(".mjs")) ?? "";
7938
8176
  }
7939
8177
  function providerWiringStatus(wiring) {
7940
- if (!existsSync29(dirname9(wiring.target))) {
8178
+ if (!existsSync30(dirname9(wiring.target))) {
7941
8179
  return "not-installed";
7942
8180
  }
7943
8181
  if (wiring.strategy === "replace") {
@@ -7946,12 +8184,12 @@ function providerWiringStatus(wiring) {
7946
8184
  }
7947
8185
  return wiringProblems(wiring).length === 0 ? "wired" : "detected-but-unwired";
7948
8186
  }
7949
- const existingText = existsSync29(wiring.target) ? readFileSync31(wiring.target, "utf8") : null;
8187
+ const existingText = existsSync30(wiring.target) ? readFileSync32(wiring.target, "utf8") : null;
7950
8188
  const result = mergeClaudeSettings(existingText, wiring.entries);
7951
8189
  return result.ok && !result.changed ? "wired" : "detected-but-unwired";
7952
8190
  }
7953
8191
  function checkProviders(registry, home) {
7954
- const launcherPath = join30(home, "bin", "tlc-exec.mjs");
8192
+ const launcherPath = join31(home, "bin", "tlc-exec.mjs");
7955
8193
  return registry.map((provider) => {
7956
8194
  const wiring = provider.wiring({ launcherPath });
7957
8195
  const status = providerWiringStatus(wiring);
@@ -8135,12 +8373,12 @@ function checkProjectPolicy(root) {
8135
8373
  {
8136
8374
  level: "ok",
8137
8375
  name: "project policy",
8138
- detail: existsSync29(configPath) ? configPath : "missing — run: tlc harness init"
8376
+ detail: existsSync30(configPath) ? configPath : "missing — run: tlc harness init"
8139
8377
  },
8140
8378
  {
8141
8379
  level: "ok",
8142
8380
  name: "state dir",
8143
- detail: existsSync29(stateDir) ? stateDir : `${stateDir} (created on first session)`
8381
+ detail: existsSync30(stateDir) ? stateDir : `${stateDir} (created on first session)`
8144
8382
  },
8145
8383
  checkPosture(root),
8146
8384
  ...checkObservedRails(root),
@@ -8151,8 +8389,8 @@ function checkProjectPolicy(root) {
8151
8389
  ];
8152
8390
  }
8153
8391
  function checkGlobalCommands(home) {
8154
- const globalCommands = join30(home, ".cursor", "commands");
8155
- if (!existsSync29(globalCommands)) {
8392
+ const globalCommands = join31(home, ".cursor", "commands");
8393
+ if (!existsSync30(globalCommands)) {
8156
8394
  return {
8157
8395
  level: "ok",
8158
8396
  name: "global commands dir",
@@ -8171,10 +8409,12 @@ function runChecks(ctx) {
8171
8409
  return [
8172
8410
  ...checkNodeVersion(ctx.nodeVersion, ctx.bunPath),
8173
8411
  ...checkRuntimePaths(ctx.runtimeHome, ctx.platform),
8412
+ ...checkSkillLinks(ctx.runtimeHome),
8174
8413
  checkHookRuntime(ctx.runtimeHome, ctx.bunPath),
8175
8414
  ...checkProviders(ctx.registry, ctx.runtimeHome),
8176
8415
  ...checkProjectPolicy(ctx.root),
8177
8416
  ...checkCapabilities(ctx.root, ctx.runtimeHome),
8417
+ ...checkPrices(),
8178
8418
  checkGlobalCommands(ctx.home)
8179
8419
  ];
8180
8420
  }
@@ -8244,6 +8484,7 @@ export {
8244
8484
  toReport,
8245
8485
  runtimeOwnershipCheck,
8246
8486
  runChecks,
8487
+ resolveOnPath,
8247
8488
  providerWiringStatus,
8248
8489
  plural,
8249
8490
  medianMs,
@@ -8251,9 +8492,11 @@ export {
8251
8492
  formatReport,
8252
8493
  exitCodeFor,
8253
8494
  checkSubagentAllowlist,
8495
+ checkSkillLinks,
8254
8496
  checkRuntimePaths,
8255
8497
  checkProviders,
8256
8498
  checkProjectPolicy,
8499
+ checkPrices,
8257
8500
  checkNodeVersion,
8258
8501
  checkLessonHealth,
8259
8502
  checkId,
File without changes
@@ -3,7 +3,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // tools/init-project.ts
5
5
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
6
- import { dirname as dirname3, join as join4 } from "node:path";
6
+ import { dirname as dirname3, join as join4, sep } from "node:path";
7
7
 
8
8
  // bin/write-user-hooks.mjs
9
9
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -811,12 +811,7 @@ function usageText(style = PLAIN) {
811
811
  function launcherPath(home = runtimeHome()) {
812
812
  return join4(home, "bin", "tlc-exec.mjs");
813
813
  }
814
- function shimCommand(platform = process.platform) {
815
- if (platform === "win32") {
816
- return { command: "cmd", argsPrefix: ["/c", "node"] };
817
- }
818
- return { command: "node", argsPrefix: [] };
819
- }
814
+ var SHIM_COMMAND = { command: "node", argsPrefix: [] };
820
815
  var CURSOR_SHIM_SPECS = [
821
816
  { hookEvent: "sessionStart", handler: "session-start", timeoutSeconds: 10 },
822
817
  { hookEvent: "sessionEnd", handler: "session-end", timeoutSeconds: 10 },
@@ -837,7 +832,7 @@ var CLAUDE_SHIM_SPECS = [
837
832
  { hookEvent: "MessageDisplay", handler: "response-after", timeoutSeconds: 5 }
838
833
  ];
839
834
  function cursorShimEntries(launcher) {
840
- const { command, argsPrefix } = shimCommand();
835
+ const { command, argsPrefix } = SHIM_COMMAND;
841
836
  return CURSOR_SHIM_SPECS.map((spec) => ({
842
837
  hookEvent: spec.hookEvent,
843
838
  handler: spec.handler,
@@ -858,19 +853,23 @@ function claudeShimEntries(launcher) {
858
853
  ...spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}
859
854
  }));
860
855
  }
861
- var GITIGNORE_LINE = ".tlc/harness/state/";
856
+ var PROJECT_SHIMS = [join4(".cursor", "hooks.json"), join4(".claude", "settings.json")];
857
+ var GITIGNORE_STATE = ".tlc/harness/state/";
858
+ function gitignoreEntries() {
859
+ return [GITIGNORE_STATE, ...PROJECT_SHIMS.map((path) => path.split(sep).join("/"))];
860
+ }
862
861
  function mergeGitignore(root) {
863
862
  const path = join4(root, ".gitignore");
864
863
  const existing = existsSync3(path) ? readFileSync3(path, "utf8") : "";
865
864
  const lines = existing.split(`
866
865
  `);
867
- const alreadyPresent = lines.includes(GITIGNORE_LINE);
868
- if (alreadyPresent) {
866
+ const missing = gitignoreEntries().filter((entry) => !lines.includes(entry));
867
+ if (missing.length === 0) {
869
868
  return { text: existing.endsWith(`
870
869
  `) || existing === "" ? existing : `${existing}
871
870
  `, changed: false };
872
871
  }
873
- lines.push(GITIGNORE_LINE);
872
+ lines.push(...missing);
874
873
  const withoutTrailingBlank = lines.filter((line, index, all) => line.length > 0 || index < all.length - 1);
875
874
  return { text: `${withoutTrailingBlank.join(`
876
875
  `).replace(/\n+$/, "")}
@@ -901,7 +900,7 @@ function buildPlan(root, flags, stdinText, presence) {
901
900
  policy,
902
901
  cursorHooksDocument: presence.cursor ? renderCursorHooksDocument(cursorShimEntries(launcher)) : null,
903
902
  claudeHooksPreview: presence.claude ? claudeShimEntries(launcher) : null,
904
- gitignoreLine: GITIGNORE_LINE
903
+ gitignoreEntries: gitignoreEntries()
905
904
  };
906
905
  }
907
906
  function applyPlan(root, flags, presence, stdinText) {
@@ -983,11 +982,13 @@ export {
983
982
  mergeGitignore,
984
983
  main,
985
984
  launcherPath,
985
+ gitignoreEntries,
986
986
  detectProviders,
987
987
  cursorShimEntries,
988
988
  claudeShimEntries,
989
989
  buildPlan,
990
990
  applyPlan,
991
991
  UsageError,
992
- GITIGNORE_LINE
992
+ PROJECT_SHIMS,
993
+ GITIGNORE_STATE
993
994
  };