@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
@@ -276,20 +276,27 @@ function readJson(path) {
276
276
  return null;
277
277
  }
278
278
  }
279
- async function withFileLock(lockPath, fn) {
279
+ function isContention(error) {
280
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
281
+ }
282
+ async function withFileLock(lockPath, fn, options = {}) {
283
+ const {
284
+ openLock = (path) => closeSync(openSync(path, "wx")),
285
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
286
+ lockAttempts = 200
287
+ } = options;
280
288
  mkdirSync2(dirname2(lockPath), { recursive: true });
281
- const attempts = 200;
282
289
  let acquired = false;
283
- for (let attempt = 0;attempt < attempts; attempt++) {
290
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
284
291
  try {
285
- closeSync(openSync(lockPath, "wx"));
292
+ openLock(lockPath);
286
293
  acquired = true;
287
294
  break;
288
295
  } catch (error) {
289
- if (errorCode(error) !== "EEXIST") {
296
+ if (!isContention(error)) {
290
297
  throw error;
291
298
  }
292
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
299
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
293
300
  }
294
301
  }
295
302
  if (!acquired) {
@@ -304,14 +311,14 @@ async function withFileLock(lockPath, fn) {
304
311
  }
305
312
  }
306
313
  async function updateJsonAtomic(path, mutator, options) {
307
- const { lockPath, afterWrite, ...atomicOptions } = options;
314
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
308
315
  return withFileLock(lockPath, async () => {
309
316
  const current = readJson(path);
310
317
  const next = mutator(current);
311
318
  await writeJsonAtomic(path, next, atomicOptions);
312
319
  afterWrite?.(path);
313
320
  return next;
314
- });
321
+ }, { openLock, lockSleep, lockAttempts });
315
322
  }
316
323
 
317
324
  // src/core/capability/capability.store.ts
@@ -5266,6 +5273,62 @@ function release(root, provider, session) {
5266
5273
  deletePresenceRecord(root, provider, session);
5267
5274
  }
5268
5275
 
5276
+ // src/core/pricing/pricing.freshness.ts
5277
+ var DEFAULT_TTL_DAYS = 7;
5278
+ var MS_PER_DAY = 86400000;
5279
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5280
+ if (meta === null) {
5281
+ return { state: "absent" };
5282
+ }
5283
+ const stamp = meta.refreshedAt;
5284
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5285
+ return { state: "undated" };
5286
+ }
5287
+ const ageMs = now.getTime() - Date.parse(stamp);
5288
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5289
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5290
+ }
5291
+ function shouldRefetch(state) {
5292
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5293
+ }
5294
+ function freshnessMessage(state, catalogue) {
5295
+ switch (state.state) {
5296
+ case "absent":
5297
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5298
+ case "undated":
5299
+ return `${catalogue}: present but carries no date — it will be refetched`;
5300
+ case "fresh":
5301
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5302
+ default:
5303
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5304
+ }
5305
+ }
5306
+ var MIN_RETAINED_RATIO = 0.5;
5307
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5308
+ if (incomingCount === 0) {
5309
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5310
+ }
5311
+ if (existingCount === 0) {
5312
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5313
+ }
5314
+ if (incomingCount >= existingCount) {
5315
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5316
+ }
5317
+ const retained = incomingCount / existingCount;
5318
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5319
+ replace: false,
5320
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5321
+ };
5322
+ }
5323
+ function describeAge(ageDays) {
5324
+ if (ageDays < 1) {
5325
+ const hours = Math.max(1, Math.round(ageDays * 24));
5326
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5327
+ }
5328
+ const days = Math.round(ageDays);
5329
+ return `${days} day${days === 1 ? "" : "s"}`;
5330
+ }
5331
+
5269
5332
  // src/core/release/release.decisions.ts
5270
5333
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5271
5334
  import { join as join18 } from "node:path";
@@ -5569,6 +5632,69 @@ function evaluateShellCommand(args) {
5569
5632
  return { kind: "allow" };
5570
5633
  }
5571
5634
 
5635
+ // src/core/shim/shim.precedence.ts
5636
+ var LAUNCHER = "tlc-exec";
5637
+ function invocationText(entry) {
5638
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5639
+ }
5640
+ function coversHandler(settings, handler) {
5641
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5642
+ for (const matcher of matchers) {
5643
+ for (const entry of matcher.hooks ?? []) {
5644
+ const text = invocationText(entry);
5645
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5646
+ return true;
5647
+ }
5648
+ }
5649
+ }
5650
+ }
5651
+ return false;
5652
+ }
5653
+ function decideShim(userSettings, handler) {
5654
+ if (userSettings === null) {
5655
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5656
+ }
5657
+ return coversHandler(userSettings, handler) ? {
5658
+ run: false,
5659
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5660
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5661
+ }
5662
+
5663
+ // src/core/skill/skill.link.ts
5664
+ var SKILL_NAME = "harness-init";
5665
+ function skillLinks(runtimeHome2, providerDirs, present) {
5666
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5667
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5668
+ providerDir,
5669
+ source,
5670
+ target: `${providerDir}/skills/${SKILL_NAME}`
5671
+ }));
5672
+ }
5673
+ function linkHealth(target, runtimeHome2, probe) {
5674
+ const resolved = probe.linkTarget(target);
5675
+ if (resolved === null) {
5676
+ return { state: "absent", target };
5677
+ }
5678
+ if (!probe.exists(resolved)) {
5679
+ return { state: "dangling", target, resolved };
5680
+ }
5681
+ const resolveHome = probe.realpath ?? ((path) => path);
5682
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5683
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5684
+ }
5685
+ function linkHealthMessage(health) {
5686
+ switch (health.state) {
5687
+ case "ok":
5688
+ return `linked → ${health.resolved}`;
5689
+ case "dangling":
5690
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5691
+ case "outside-runtime":
5692
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5693
+ default:
5694
+ return "not linked — the provider cannot see the init skill";
5695
+ }
5696
+ }
5697
+
5572
5698
  // src/core/stagnation/stagnation.resolution.ts
5573
5699
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5574
5700
  import { join as join21 } from "node:path";
@@ -6696,6 +6822,21 @@ var coreFacade = {
6696
6822
  readHandoffFile,
6697
6823
  readForeignSlices
6698
6824
  },
6825
+ shim: {
6826
+ coversHandler,
6827
+ decideShim
6828
+ },
6829
+ pricing: {
6830
+ freshness,
6831
+ freshnessMessage,
6832
+ mayReplace,
6833
+ shouldRefetch
6834
+ },
6835
+ skill: {
6836
+ linkHealth,
6837
+ linkHealthMessage,
6838
+ skillLinks
6839
+ },
6699
6840
  lesson: {
6700
6841
  projectLessonsInjectable,
6701
6842
  recordLessonFromFailure,
@@ -6880,6 +7021,11 @@ var coreFacade = {
6880
7021
  // src/platform/pricing.ts
6881
7022
  import { existsSync as existsSync25, readFileSync as readFileSync27 } from "node:fs";
6882
7023
  import { join as join26 } from "node:path";
7024
+ var FALLBACK_PLANE = "litellm";
7025
+ var MODEL_ALIASES = {
7026
+ "cursor-grok-4.5": "grok-4.5",
7027
+ auto: "auto-cost"
7028
+ };
6883
7029
  var VENDOR_TO_NEUTRAL_POOL = {
6884
7030
  cursor_models: "provider_native",
6885
7031
  anthropic_models: "provider_native",
@@ -6890,16 +7036,6 @@ var VENDOR_TO_NEUTRAL_POOL = {
6890
7036
  function mapPoolToNeutral(pool) {
6891
7037
  return VENDOR_TO_NEUTRAL_POOL[pool];
6892
7038
  }
6893
- function readJsonFile2(path) {
6894
- if (!existsSync25(path)) {
6895
- return null;
6896
- }
6897
- try {
6898
- return JSON.parse(readFileSync27(path, "utf8"));
6899
- } catch {
6900
- return null;
6901
- }
6902
- }
6903
7039
  function stripMeta(table) {
6904
7040
  if (!table) {
6905
7041
  return {};
@@ -6908,7 +7044,7 @@ function stripMeta(table) {
6908
7044
  return rest;
6909
7045
  }
6910
7046
  function slugifyModelName(name) {
6911
- return name.trim().toLowerCase().replace(/[[\]]/g, "").replace(/\(.*?\)/g, "").replace(/[^a-z0-9.+]+/g, "-").replace(/^-+|-+$/g, "");
7047
+ return name.trim().toLowerCase().replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[[\]]/g, "").replace(/[^a-z0-9.+]+/g, "-").replace(/^-+|-+$/g, "");
6912
7048
  }
6913
7049
  function candidatesFor(model, aliases) {
6914
7050
  const trimmed = model.trim();
@@ -6945,28 +7081,55 @@ function fuzzyFind(table, needle) {
6945
7081
  }
6946
7082
  return;
6947
7083
  }
6948
- function overridesPath() {
7084
+ function cataloguePath() {
6949
7085
  return join26(runtimeHome(), "model-prices.json");
6950
7086
  }
6951
- function providerNativePath(provider) {
6952
- return join26(runtimeHome(), `model-prices.${provider}.json`);
7087
+ function overridesPath() {
7088
+ return join26(runtimeHome(), "model-prices.local.json");
7089
+ }
7090
+ var cache = new Map;
7091
+ function readCatalogue(path) {
7092
+ if (!existsSync25(path)) {
7093
+ cache.delete(path);
7094
+ return null;
7095
+ }
7096
+ let text;
7097
+ try {
7098
+ text = readFileSync27(path, "utf8");
7099
+ } catch {
7100
+ cache.delete(path);
7101
+ return null;
7102
+ }
7103
+ const hit = cache.get(path);
7104
+ if (hit && hit.text === text) {
7105
+ return hit.value;
7106
+ }
7107
+ let parsed;
7108
+ try {
7109
+ parsed = JSON.parse(text);
7110
+ } catch {
7111
+ cache.delete(path);
7112
+ return null;
7113
+ }
7114
+ cache.set(path, { text, value: parsed });
7115
+ return parsed;
6953
7116
  }
6954
- function litellmPath() {
6955
- return join26(runtimeHome(), "model-prices.litellm.json");
7117
+ function loadCatalogue() {
7118
+ return readCatalogue(cataloguePath()) ?? {};
6956
7119
  }
6957
- function aliasesPath() {
6958
- return join26(runtimeHome(), "model-aliases.json");
7120
+ function planeFor(catalogue, plane) {
7121
+ return stripMeta(catalogue.planes?.[plane] ?? null);
6959
7122
  }
6960
7123
  function resolveModelPrice(provider, model) {
6961
7124
  const trimmed = model.trim();
6962
7125
  if (!trimmed) {
6963
7126
  return;
6964
7127
  }
6965
- const overrides = stripMeta(readJsonFile2(overridesPath()));
6966
- const native = stripMeta(readJsonFile2(providerNativePath(provider)));
6967
- const litellm = stripMeta(readJsonFile2(litellmPath()));
6968
- const aliases = readJsonFile2(aliasesPath()) ?? {};
6969
- const candidates = candidatesFor(trimmed, aliases);
7128
+ const catalogue = loadCatalogue();
7129
+ const overrides = stripMeta(readCatalogue(overridesPath()));
7130
+ const native = provider ? planeFor(catalogue, provider) : {};
7131
+ const litellm = planeFor(catalogue, FALLBACK_PLANE);
7132
+ const candidates = candidatesFor(trimmed, MODEL_ALIASES);
6970
7133
  for (const id of candidates) {
6971
7134
  const entry = overrides[id];
6972
7135
  if (entry) {
@@ -7831,14 +7994,9 @@ var ENTRY_SPECS2 = [
7831
7994
  { hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
7832
7995
  { hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
7833
7996
  ];
7834
- function commandFor(runtime) {
7835
- if (process.platform === "win32") {
7836
- return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
7837
- }
7838
- return { command: "node", argsPrefix: [runtime.launcherPath] };
7839
- }
7840
7997
  function cursorWiring(runtime) {
7841
- const { command, argsPrefix } = commandFor(runtime);
7998
+ const command = "node";
7999
+ const argsPrefix = [runtime.launcherPath];
7842
8000
  const entries = ENTRY_SPECS2.map((spec) => ({
7843
8001
  hookEvent: spec.hookEvent,
7844
8002
  handler: spec.handler,
@@ -276,20 +276,27 @@ function readJson(path) {
276
276
  return null;
277
277
  }
278
278
  }
279
- async function withFileLock(lockPath, fn) {
279
+ function isContention(error) {
280
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
281
+ }
282
+ async function withFileLock(lockPath, fn, options = {}) {
283
+ const {
284
+ openLock = (path) => closeSync(openSync(path, "wx")),
285
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
286
+ lockAttempts = 200
287
+ } = options;
280
288
  mkdirSync2(dirname2(lockPath), { recursive: true });
281
- const attempts = 200;
282
289
  let acquired = false;
283
- for (let attempt = 0;attempt < attempts; attempt++) {
290
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
284
291
  try {
285
- closeSync(openSync(lockPath, "wx"));
292
+ openLock(lockPath);
286
293
  acquired = true;
287
294
  break;
288
295
  } catch (error) {
289
- if (errorCode(error) !== "EEXIST") {
296
+ if (!isContention(error)) {
290
297
  throw error;
291
298
  }
292
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
299
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
293
300
  }
294
301
  }
295
302
  if (!acquired) {
@@ -304,14 +311,14 @@ async function withFileLock(lockPath, fn) {
304
311
  }
305
312
  }
306
313
  async function updateJsonAtomic(path, mutator, options) {
307
- const { lockPath, afterWrite, ...atomicOptions } = options;
314
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
308
315
  return withFileLock(lockPath, async () => {
309
316
  const current = readJson(path);
310
317
  const next = mutator(current);
311
318
  await writeJsonAtomic(path, next, atomicOptions);
312
319
  afterWrite?.(path);
313
320
  return next;
314
- });
321
+ }, { openLock, lockSleep, lockAttempts });
315
322
  }
316
323
 
317
324
  // src/core/capability/capability.store.ts
@@ -5266,6 +5273,62 @@ function release(root, provider, session) {
5266
5273
  deletePresenceRecord(root, provider, session);
5267
5274
  }
5268
5275
 
5276
+ // src/core/pricing/pricing.freshness.ts
5277
+ var DEFAULT_TTL_DAYS = 7;
5278
+ var MS_PER_DAY = 86400000;
5279
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5280
+ if (meta === null) {
5281
+ return { state: "absent" };
5282
+ }
5283
+ const stamp = meta.refreshedAt;
5284
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5285
+ return { state: "undated" };
5286
+ }
5287
+ const ageMs = now.getTime() - Date.parse(stamp);
5288
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5289
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5290
+ }
5291
+ function shouldRefetch(state) {
5292
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5293
+ }
5294
+ function freshnessMessage(state, catalogue) {
5295
+ switch (state.state) {
5296
+ case "absent":
5297
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5298
+ case "undated":
5299
+ return `${catalogue}: present but carries no date — it will be refetched`;
5300
+ case "fresh":
5301
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5302
+ default:
5303
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5304
+ }
5305
+ }
5306
+ var MIN_RETAINED_RATIO = 0.5;
5307
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5308
+ if (incomingCount === 0) {
5309
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5310
+ }
5311
+ if (existingCount === 0) {
5312
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5313
+ }
5314
+ if (incomingCount >= existingCount) {
5315
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5316
+ }
5317
+ const retained = incomingCount / existingCount;
5318
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5319
+ replace: false,
5320
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5321
+ };
5322
+ }
5323
+ function describeAge(ageDays) {
5324
+ if (ageDays < 1) {
5325
+ const hours = Math.max(1, Math.round(ageDays * 24));
5326
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5327
+ }
5328
+ const days = Math.round(ageDays);
5329
+ return `${days} day${days === 1 ? "" : "s"}`;
5330
+ }
5331
+
5269
5332
  // src/core/release/release.decisions.ts
5270
5333
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5271
5334
  import { join as join18 } from "node:path";
@@ -5569,6 +5632,69 @@ function evaluateShellCommand(args) {
5569
5632
  return { kind: "allow" };
5570
5633
  }
5571
5634
 
5635
+ // src/core/shim/shim.precedence.ts
5636
+ var LAUNCHER = "tlc-exec";
5637
+ function invocationText(entry) {
5638
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5639
+ }
5640
+ function coversHandler(settings, handler) {
5641
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5642
+ for (const matcher of matchers) {
5643
+ for (const entry of matcher.hooks ?? []) {
5644
+ const text = invocationText(entry);
5645
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5646
+ return true;
5647
+ }
5648
+ }
5649
+ }
5650
+ }
5651
+ return false;
5652
+ }
5653
+ function decideShim(userSettings, handler) {
5654
+ if (userSettings === null) {
5655
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5656
+ }
5657
+ return coversHandler(userSettings, handler) ? {
5658
+ run: false,
5659
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5660
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5661
+ }
5662
+
5663
+ // src/core/skill/skill.link.ts
5664
+ var SKILL_NAME = "harness-init";
5665
+ function skillLinks(runtimeHome2, providerDirs, present) {
5666
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5667
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5668
+ providerDir,
5669
+ source,
5670
+ target: `${providerDir}/skills/${SKILL_NAME}`
5671
+ }));
5672
+ }
5673
+ function linkHealth(target, runtimeHome2, probe) {
5674
+ const resolved = probe.linkTarget(target);
5675
+ if (resolved === null) {
5676
+ return { state: "absent", target };
5677
+ }
5678
+ if (!probe.exists(resolved)) {
5679
+ return { state: "dangling", target, resolved };
5680
+ }
5681
+ const resolveHome = probe.realpath ?? ((path) => path);
5682
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5683
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5684
+ }
5685
+ function linkHealthMessage(health) {
5686
+ switch (health.state) {
5687
+ case "ok":
5688
+ return `linked → ${health.resolved}`;
5689
+ case "dangling":
5690
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5691
+ case "outside-runtime":
5692
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5693
+ default:
5694
+ return "not linked — the provider cannot see the init skill";
5695
+ }
5696
+ }
5697
+
5572
5698
  // src/core/stagnation/stagnation.resolution.ts
5573
5699
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5574
5700
  import { join as join21 } from "node:path";
@@ -6696,6 +6822,21 @@ var coreFacade = {
6696
6822
  readHandoffFile,
6697
6823
  readForeignSlices
6698
6824
  },
6825
+ shim: {
6826
+ coversHandler,
6827
+ decideShim
6828
+ },
6829
+ pricing: {
6830
+ freshness,
6831
+ freshnessMessage,
6832
+ mayReplace,
6833
+ shouldRefetch
6834
+ },
6835
+ skill: {
6836
+ linkHealth,
6837
+ linkHealthMessage,
6838
+ skillLinks
6839
+ },
6699
6840
  lesson: {
6700
6841
  projectLessonsInjectable,
6701
6842
  recordLessonFromFailure,
@@ -7641,14 +7782,9 @@ var ENTRY_SPECS2 = [
7641
7782
  { hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
7642
7783
  { hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
7643
7784
  ];
7644
- function commandFor(runtime) {
7645
- if (process.platform === "win32") {
7646
- return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
7647
- }
7648
- return { command: "node", argsPrefix: [runtime.launcherPath] };
7649
- }
7650
7785
  function cursorWiring(runtime) {
7651
- const { command, argsPrefix } = commandFor(runtime);
7786
+ const command = "node";
7787
+ const argsPrefix = [runtime.launcherPath];
7652
7788
  const entries = ENTRY_SPECS2.map((spec) => ({
7653
7789
  hookEvent: spec.hookEvent,
7654
7790
  handler: spec.handler,