@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/tlc-cli.mjs CHANGED
@@ -4,17 +4,16 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
  // bin/tlc-cli.ts
5
5
  import { spawnSync } from "node:child_process";
6
6
  import {
7
- existsSync as existsSync25,
8
- lstatSync,
9
- mkdirSync as mkdirSync16,
7
+ existsSync as existsSync26,
8
+ lstatSync as lstatSync2,
9
+ mkdirSync as mkdirSync17,
10
10
  readdirSync as readdirSync6,
11
11
  readFileSync as readFileSync27,
12
12
  realpathSync,
13
- rmSync as rmSync4,
13
+ rmSync as rmSync5,
14
14
  writeFileSync as writeFileSync15
15
15
  } from "node:fs";
16
- import { homedir as homedir3 } from "node:os";
17
- import { delimiter, join as join26 } from "node:path";
16
+ import { delimiter, join as join27 } from "node:path";
18
17
 
19
18
  // src/core/attest/attest.service.ts
20
19
  import { createHash } from "node:crypto";
@@ -85,6 +84,17 @@ function bootDir(root) {
85
84
  function policyBaselineDir(root) {
86
85
  return join(projectStateDir(root), "policy-baseline");
87
86
  }
87
+ function claudeConfigDir() {
88
+ const custom = process.env.CLAUDE_CONFIG_DIR?.trim();
89
+ return custom && custom.length > 0 ? custom : join(homedir(), ".claude");
90
+ }
91
+ function cursorConfigDir() {
92
+ const custom = process.env.CURSOR_CONFIG_DIR?.trim();
93
+ return custom && custom.length > 0 ? custom : join(homedir(), ".cursor");
94
+ }
95
+ function providerConfigDirs() {
96
+ return [cursorConfigDir(), claudeConfigDir()];
97
+ }
88
98
 
89
99
  // src/core/attest/attest.service.ts
90
100
  var CHAIN_ROOT = "genesis";
@@ -283,20 +293,27 @@ function readJson(path) {
283
293
  return null;
284
294
  }
285
295
  }
286
- async function withFileLock(lockPath, fn) {
296
+ function isContention(error) {
297
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
298
+ }
299
+ async function withFileLock(lockPath, fn, options = {}) {
300
+ const {
301
+ openLock = (path) => closeSync(openSync(path, "wx")),
302
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
303
+ lockAttempts = 200
304
+ } = options;
287
305
  mkdirSync2(dirname2(lockPath), { recursive: true });
288
- const attempts = 200;
289
306
  let acquired = false;
290
- for (let attempt = 0;attempt < attempts; attempt++) {
307
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
291
308
  try {
292
- closeSync(openSync(lockPath, "wx"));
309
+ openLock(lockPath);
293
310
  acquired = true;
294
311
  break;
295
312
  } catch (error) {
296
- if (errorCode(error) !== "EEXIST") {
313
+ if (!isContention(error)) {
297
314
  throw error;
298
315
  }
299
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
316
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
300
317
  }
301
318
  }
302
319
  if (!acquired) {
@@ -311,14 +328,14 @@ async function withFileLock(lockPath, fn) {
311
328
  }
312
329
  }
313
330
  async function updateJsonAtomic(path, mutator, options) {
314
- const { lockPath, afterWrite, ...atomicOptions } = options;
331
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
315
332
  return withFileLock(lockPath, async () => {
316
333
  const current = readJson(path);
317
334
  const next = mutator(current);
318
335
  await writeJsonAtomic(path, next, atomicOptions);
319
336
  afterWrite?.(path);
320
337
  return next;
321
- });
338
+ }, { openLock, lockSleep, lockAttempts });
322
339
  }
323
340
 
324
341
  // src/core/capability/capability.store.ts
@@ -5266,6 +5283,62 @@ function release(root, provider, session) {
5266
5283
  deletePresenceRecord(root, provider, session);
5267
5284
  }
5268
5285
 
5286
+ // src/core/pricing/pricing.freshness.ts
5287
+ var DEFAULT_TTL_DAYS = 7;
5288
+ var MS_PER_DAY = 86400000;
5289
+ function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
5290
+ if (meta === null) {
5291
+ return { state: "absent" };
5292
+ }
5293
+ const stamp = meta.refreshedAt;
5294
+ if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
5295
+ return { state: "undated" };
5296
+ }
5297
+ const ageMs = now.getTime() - Date.parse(stamp);
5298
+ const ageDays = Math.max(0, ageMs / MS_PER_DAY);
5299
+ return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
5300
+ }
5301
+ function shouldRefetch(state) {
5302
+ return state.state === "absent" || state.state === "undated" || state.state === "stale";
5303
+ }
5304
+ function freshnessMessage(state, catalogue) {
5305
+ switch (state.state) {
5306
+ case "absent":
5307
+ return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
5308
+ case "undated":
5309
+ return `${catalogue}: present but carries no date — it will be refetched`;
5310
+ case "fresh":
5311
+ return `${catalogue}: ${describeAge(state.ageDays)} old`;
5312
+ default:
5313
+ return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
5314
+ }
5315
+ }
5316
+ var MIN_RETAINED_RATIO = 0.5;
5317
+ function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
5318
+ if (incomingCount === 0) {
5319
+ return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
5320
+ }
5321
+ if (existingCount === 0) {
5322
+ return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
5323
+ }
5324
+ if (incomingCount >= existingCount) {
5325
+ return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
5326
+ }
5327
+ const retained = incomingCount / existingCount;
5328
+ return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
5329
+ replace: false,
5330
+ reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
5331
+ };
5332
+ }
5333
+ function describeAge(ageDays) {
5334
+ if (ageDays < 1) {
5335
+ const hours = Math.max(1, Math.round(ageDays * 24));
5336
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
5337
+ }
5338
+ const days = Math.round(ageDays);
5339
+ return `${days} day${days === 1 ? "" : "s"}`;
5340
+ }
5341
+
5269
5342
  // src/core/release/release.decisions.ts
5270
5343
  import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
5271
5344
  import { join as join18 } from "node:path";
@@ -5569,6 +5642,69 @@ function evaluateShellCommand(args) {
5569
5642
  return { kind: "allow" };
5570
5643
  }
5571
5644
 
5645
+ // src/core/shim/shim.precedence.ts
5646
+ var LAUNCHER = "tlc-exec";
5647
+ function invocationText(entry) {
5648
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5649
+ }
5650
+ function coversHandler(settings, handler) {
5651
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5652
+ for (const matcher of matchers) {
5653
+ for (const entry of matcher.hooks ?? []) {
5654
+ const text = invocationText(entry);
5655
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5656
+ return true;
5657
+ }
5658
+ }
5659
+ }
5660
+ }
5661
+ return false;
5662
+ }
5663
+ function decideShim(userSettings, handler) {
5664
+ if (userSettings === null) {
5665
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5666
+ }
5667
+ return coversHandler(userSettings, handler) ? {
5668
+ run: false,
5669
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5670
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5671
+ }
5672
+
5673
+ // src/core/skill/skill.link.ts
5674
+ var SKILL_NAME = "harness-init";
5675
+ function skillLinks(runtimeHome2, providerDirs, present) {
5676
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5677
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5678
+ providerDir,
5679
+ source,
5680
+ target: `${providerDir}/skills/${SKILL_NAME}`
5681
+ }));
5682
+ }
5683
+ function linkHealth(target, runtimeHome2, probe) {
5684
+ const resolved = probe.linkTarget(target);
5685
+ if (resolved === null) {
5686
+ return { state: "absent", target };
5687
+ }
5688
+ if (!probe.exists(resolved)) {
5689
+ return { state: "dangling", target, resolved };
5690
+ }
5691
+ const resolveHome = probe.realpath ?? ((path) => path);
5692
+ const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
5693
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5694
+ }
5695
+ function linkHealthMessage(health) {
5696
+ switch (health.state) {
5697
+ case "ok":
5698
+ return `linked → ${health.resolved}`;
5699
+ case "dangling":
5700
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5701
+ case "outside-runtime":
5702
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5703
+ default:
5704
+ return "not linked — the provider cannot see the init skill";
5705
+ }
5706
+ }
5707
+
5572
5708
  // src/core/stagnation/stagnation.resolution.ts
5573
5709
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5574
5710
  import { join as join21 } from "node:path";
@@ -6696,6 +6832,21 @@ var coreFacade = {
6696
6832
  readHandoffFile,
6697
6833
  readForeignSlices
6698
6834
  },
6835
+ shim: {
6836
+ coversHandler,
6837
+ decideShim
6838
+ },
6839
+ pricing: {
6840
+ freshness,
6841
+ freshnessMessage,
6842
+ mayReplace,
6843
+ shouldRefetch
6844
+ },
6845
+ skill: {
6846
+ linkHealth,
6847
+ linkHealthMessage,
6848
+ skillLinks
6849
+ },
6699
6850
  lesson: {
6700
6851
  projectLessonsInjectable,
6701
6852
  recordLessonFromFailure,
@@ -6902,6 +7053,43 @@ function unknownFlags(args) {
6902
7053
  return args.filter((arg) => arg.startsWith("--"));
6903
7054
  }
6904
7055
 
7056
+ // src/platform/links.ts
7057
+ import { copyFileSync, existsSync as existsSync25, lstatSync, mkdirSync as mkdirSync16, rmSync as rmSync4, symlinkSync } from "node:fs";
7058
+ import { dirname as dirname7, join as join26 } from "node:path";
7059
+ var LINK_TYPE = "junction";
7060
+ function linkDir(source, target) {
7061
+ let replaced = false;
7062
+ if (isLink(target)) {
7063
+ rmSync4(target, { recursive: true, force: true });
7064
+ replaced = true;
7065
+ } else if (existsSync25(target)) {
7066
+ return {
7067
+ kind: "refused",
7068
+ target,
7069
+ reason: `${target} exists and is not a link — move it aside and re-run`
7070
+ };
7071
+ }
7072
+ mkdirSync16(dirname7(target), { recursive: true });
7073
+ symlinkSync(source, target, LINK_TYPE);
7074
+ return { kind: replaced ? "relinked" : "linked", target, source };
7075
+ }
7076
+ function isLink(path) {
7077
+ try {
7078
+ return lstatSync(path).isSymbolicLink();
7079
+ } catch {
7080
+ return false;
7081
+ }
7082
+ }
7083
+ function seedConfig(dest) {
7084
+ const path = join26(dest, "config.json");
7085
+ const example = join26(dest, "config.example.json");
7086
+ if (existsSync25(path) || !existsSync25(example)) {
7087
+ return { seeded: false, path };
7088
+ }
7089
+ copyFileSync(example, path);
7090
+ return { seeded: true, path };
7091
+ }
7092
+
6905
7093
  // bin/tlc-cli.ts
6906
7094
  class UsageError extends Error {
6907
7095
  }
@@ -6909,22 +7097,22 @@ function resolveProjectRoot() {
6909
7097
  return process.env.TLC_PROJECT_DIR ?? process.cwd();
6910
7098
  }
6911
7099
  function modeFilePath(root) {
6912
- return join26(projectStateDir(root), "harness-mode");
7100
+ return join27(projectStateDir(root), "harness-mode");
6913
7101
  }
6914
7102
  function grindFlagPath(root) {
6915
- return join26(flagsDir(root), "grind-on");
7103
+ return join27(flagsDir(root), "grind-on");
6916
7104
  }
6917
7105
  function skipFlagPath(root) {
6918
- return join26(flagsDir(root), "skip-verify");
7106
+ return join27(flagsDir(root), "skip-verify");
6919
7107
  }
6920
7108
  function focusFlagPath(root) {
6921
- return join26(flagsDir(root), "focus");
7109
+ return join27(flagsDir(root), "focus");
6922
7110
  }
6923
7111
  function pairedFlagPath(root) {
6924
- return join26(flagsDir(root), "paired");
7112
+ return join27(flagsDir(root), "paired");
6925
7113
  }
6926
7114
  function ensureFlagsDir(root) {
6927
- mkdirSync16(flagsDir(root), { recursive: true });
7115
+ mkdirSync17(flagsDir(root), { recursive: true });
6928
7116
  }
6929
7117
  function readMode(root) {
6930
7118
  return coreFacade.policy.loadPolicy(root).mode;
@@ -6933,7 +7121,7 @@ function grindOn(root) {
6933
7121
  return coreFacade.policy.loadPolicy(root).grind.enabled;
6934
7122
  }
6935
7123
  function gatesPaused(root) {
6936
- return existsSync25(skipFlagPath(root));
7124
+ return existsSync26(skipFlagPath(root));
6937
7125
  }
6938
7126
  function acceptedModes() {
6939
7127
  return coreFacade.policy.OPERATOR_MODES.join(" | ");
@@ -6995,8 +7183,8 @@ function setGrind(root, on) {
6995
7183
  coreFacade.policy.refreshPolicyBaselines(root);
6996
7184
  return "grind ON — stop hook will lint/test and auto-retry on failure";
6997
7185
  }
6998
- if (existsSync25(path)) {
6999
- rmSync4(path);
7186
+ if (existsSync26(path)) {
7187
+ rmSync5(path);
7000
7188
  }
7001
7189
  coreFacade.policy.refreshPolicyBaselines(root);
7002
7190
  return "grind OFF — no auto fix loops";
@@ -7009,8 +7197,8 @@ function setPaused(root, on) {
7009
7197
  coreFacade.policy.refreshPolicyBaselines(root);
7010
7198
  return "gates PAUSED — stop checks disabled until `tlc harness resume`";
7011
7199
  }
7012
- if (existsSync25(path)) {
7013
- rmSync4(path);
7200
+ if (existsSync26(path)) {
7201
+ rmSync5(path);
7014
7202
  }
7015
7203
  coreFacade.policy.refreshPolicyBaselines(root);
7016
7204
  return "gates ACTIVE again";
@@ -7232,30 +7420,30 @@ function classifyRuntimePath(dest, probe) {
7232
7420
  if (!probe.exists(dest)) {
7233
7421
  return "absent";
7234
7422
  }
7235
- if (probe.exists(join26(dest, ".git"))) {
7423
+ if (probe.exists(join27(dest, ".git"))) {
7236
7424
  return "managed";
7237
7425
  }
7238
- return probe.exists(join26(dest, NPM_MARKER)) ? "npm" : "unmanaged";
7426
+ return probe.exists(join27(dest, NPM_MARKER)) ? "npm" : "unmanaged";
7239
7427
  }
7240
7428
  function runtimePathKind(dest) {
7241
7429
  return classifyRuntimePath(dest, {
7242
7430
  isSymlink: (path) => {
7243
7431
  try {
7244
- return lstatSync(path).isSymbolicLink();
7432
+ return lstatSync2(path).isSymbolicLink();
7245
7433
  } catch {
7246
7434
  return false;
7247
7435
  }
7248
7436
  },
7249
- exists: existsSync25
7437
+ exists: existsSync26
7250
7438
  });
7251
7439
  }
7252
7440
  function missingBundles(dest) {
7253
- const entrypoints = join26(dest, "src", "entrypoints");
7254
- if (!existsSync25(entrypoints)) {
7441
+ const entrypoints = join27(dest, "src", "entrypoints");
7442
+ if (!existsSync26(entrypoints)) {
7255
7443
  return [];
7256
7444
  }
7257
7445
  const expected = readdirSync6(entrypoints).filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")).map((name) => `${name.slice(0, -3)}.mjs`);
7258
- return expected.filter((bundle) => !existsSync25(join26(dest, "dist", bundle)));
7446
+ return expected.filter((bundle) => !existsSync26(join27(dest, "dist", bundle)));
7259
7447
  }
7260
7448
  function linkedRuntimeMessage(dest, target) {
7261
7449
  return [
@@ -7303,7 +7491,7 @@ function resetFailureMessage(dest, mergeRef, gitOutput) {
7303
7491
  `);
7304
7492
  }
7305
7493
  function runtimeRevision(dest) {
7306
- if (!existsSync25(join26(dest, ".git"))) {
7494
+ if (!existsSync26(join27(dest, ".git"))) {
7307
7495
  return { revision: null, date: null };
7308
7496
  }
7309
7497
  const read = (args) => {
@@ -7346,7 +7534,7 @@ function versionText(root, style = PLAIN) {
7346
7534
  return render(versionScreen(root), style);
7347
7535
  }
7348
7536
  function pendingUpdate(dest, mergeRef) {
7349
- if (!existsSync25(join26(dest, ".git"))) {
7537
+ if (!existsSync26(join27(dest, ".git"))) {
7350
7538
  return { ok: false, reason: "the runtime path is not a git checkout", commits: 0, decisions: [] };
7351
7539
  }
7352
7540
  const fetch = spawnSync("git", ["-C", dest, "fetch", "origin"], { stdio: "inherit", env: process.env });
@@ -7397,17 +7585,17 @@ var GATE_FIELDS = {
7397
7585
  "test-command": "test",
7398
7586
  "lint-command": "lint"
7399
7587
  };
7400
- function resolveExecutable(name, env = process.env, platform = process.platform) {
7401
- const extensions = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
7402
- const candidates = (base) => [base, ...extensions.map((ext) => `${base}${ext}`)];
7588
+ var EXECUTABLE_EXTENSIONS = ["", ".exe", ".cmd", ".bat", ".ps1"];
7589
+ function resolveExecutable(name, env = process.env) {
7590
+ const candidates = (base) => EXECUTABLE_EXTENSIONS.map((ext) => `${base}${ext}`);
7403
7591
  if (name.includes("/") || name.includes("\\")) {
7404
- return candidates(name).find((candidate) => existsSync25(candidate)) ?? null;
7592
+ return candidates(name).find((candidate) => existsSync26(candidate)) ?? null;
7405
7593
  }
7406
7594
  for (const dir of (env.PATH ?? "").split(delimiter)) {
7407
7595
  if (!dir) {
7408
7596
  continue;
7409
7597
  }
7410
- const found = candidates(join26(dir, name)).find((candidate) => existsSync25(candidate));
7598
+ const found = candidates(join27(dir, name)).find((candidate) => existsSync26(candidate));
7411
7599
  if (found) {
7412
7600
  return found;
7413
7601
  }
@@ -7426,11 +7614,11 @@ function setGateCommand(root, field, argv, interactive) {
7426
7614
  throw new UsageError(`\`${binary}\` was not found on PATH, and a gate command that cannot run is a config fault ([/decisions/ad-021.md](/decisions/ad-021.md)).`);
7427
7615
  }
7428
7616
  const path = projectConfigPath(root);
7429
- const parsed = existsSync25(path) ? JSON.parse(readFileSync27(path, "utf8")) : {};
7617
+ const parsed = existsSync26(path) ? JSON.parse(readFileSync27(path, "utf8")) : {};
7430
7618
  const grind = { ...parsed.grind ?? {} };
7431
7619
  grind[field === "test" ? "testCommand" : "lintCommand"] = argv;
7432
7620
  parsed.grind = grind;
7433
- mkdirSync16(join26(root, ".tlc", "harness"), { recursive: true });
7621
+ mkdirSync17(join27(root, ".tlc", "harness"), { recursive: true });
7434
7622
  writeFileSync15(path, `${JSON.stringify(parsed, null, 2)}
7435
7623
  `, "utf8");
7436
7624
  coreFacade.policy.refreshPolicyBaselines(root);
@@ -7486,23 +7674,25 @@ function helpText(style = PLAIN) {
7486
7674
  }
7487
7675
  function pricesHelpScreen() {
7488
7676
  return {
7489
- title: "price catalogs",
7677
+ title: "prices",
7490
7678
  sections: [
7491
7679
  {
7492
- lines: ` tlc harness prices refresh [all|cursor|litellm]
7680
+ lines: ` tlc harness prices refresh [all|cursor|litellm] [--if-stale]
7493
7681
  tlc harness prices lookup <model-id>
7494
7682
 
7495
- refresh / refresh all Cursor catalog + LiteLLM fallback
7496
- refresh cursor model-prices.cursor.json (tracked)
7497
- refresh litellm model-prices.litellm.json (local)
7683
+ refresh / refresh all both planes of model-prices.json
7684
+ refresh cursor the provider's own rates
7685
+ refresh litellm the vendors' list prices
7686
+ --if-stale fetch only past the 7-day TTL
7498
7687
  lookup <model-id> catalog key, pool, USD for 1M in + 1M out
7499
7688
 
7500
- Resolution: overrides Cursor LiteLLM null
7689
+ Catalogue: <runtime home>/model-prices.json fetched per machine, never versioned
7690
+ Overrides: <runtime home>/model-prices.local.json — yours, hand-written
7501
7691
  Documentation: tlc harness help prices`.split(`
7502
7692
  `)
7503
7693
  }
7504
7694
  ],
7505
- footer: "resolution: local overrides → the provider's own catalog LiteLLM → null"
7695
+ footer: "resolution: your overrides → the asking provider's planethe vendor plane → null"
7506
7696
  };
7507
7697
  }
7508
7698
  function pricesHelpText(style = PLAIN) {
@@ -7516,11 +7706,37 @@ function resolveHarnessRoot() {
7516
7706
  return home;
7517
7707
  }
7518
7708
  }
7709
+ function wireRuntime(dest, home) {
7710
+ const lines = [];
7711
+ const seeded = seedConfig(dest);
7712
+ if (seeded.seeded) {
7713
+ lines.push(`config seeded → ${seeded.path}`);
7714
+ }
7715
+ if (!existsSync26(join27(dest, "skills", "harness-init"))) {
7716
+ return { lines, missingSkill: true };
7717
+ }
7718
+ const links = coreFacade.skill.skillLinks(dest, providerConfigDirs(), existsSync26);
7719
+ if (links.length === 0) {
7720
+ lines.push("no provider config dir found — skill not linked");
7721
+ }
7722
+ for (const link of links) {
7723
+ const outcome = linkDir(link.source, link.target);
7724
+ lines.push(outcome.kind === "refused" ? `skill not linked — ${outcome.reason}` : `skill → ${outcome.target}`);
7725
+ }
7726
+ const hooks = spawnSync(process.execPath, [join27(dest, "bin", "write-user-hooks.mjs")], {
7727
+ stdio: "inherit",
7728
+ env: { ...process.env, TLC_HOME: home }
7729
+ });
7730
+ if ((hooks.status ?? 1) !== 0) {
7731
+ lines.push("hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
7732
+ }
7733
+ return { lines, missingSkill: false };
7734
+ }
7519
7735
  function execBinPath() {
7520
- return join26(resolveHarnessRoot(), "bin", "tlc-exec");
7736
+ return join27(resolveHarnessRoot(), "bin", "tlc-exec.mjs");
7521
7737
  }
7522
7738
  function buildBinPath() {
7523
- return join26(resolveHarnessRoot(), "bin", "tlc-build");
7739
+ return join27(resolveHarnessRoot(), "bin", "tlc-build.mjs");
7524
7740
  }
7525
7741
  function route(args) {
7526
7742
  const cmd = (args[0] ?? "status").toLowerCase();
@@ -7726,7 +7942,7 @@ function runUpdate(root) {
7726
7942
  const revisionBefore = runtimeRevision(dest).revision;
7727
7943
  const home = runtimeHome();
7728
7944
  console.log(`update: runtime → ${dest}`);
7729
- if (!existsSync25(join26(dest, "bin", "tlc-exec.mjs"))) {
7945
+ if (!existsSync26(join27(dest, "bin", "tlc-exec.mjs"))) {
7730
7946
  console.error(`update: missing install at ${home}`);
7731
7947
  console.error(`update: install once with \`npm i -g ${NPM_PACKAGE}\`, then \`tlc harness install\`, then retry.`);
7732
7948
  process.exit(1);
@@ -7738,13 +7954,16 @@ function runUpdate(root) {
7738
7954
  const bump = spawnSync("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
7739
7955
  stdio: "inherit",
7740
7956
  env: process.env,
7741
- shell: process.platform === "win32"
7957
+ shell: true
7742
7958
  });
7743
7959
  if ((bump.status ?? 1) !== 0) {
7744
7960
  console.error(npmUpdateFailureMessage());
7745
7961
  process.exit(bump.status ?? 1);
7746
7962
  }
7747
- const sync = spawnSync(execBinPath(), ["install-runtime"], { stdio: "inherit", env: process.env });
7963
+ const sync = spawnSync(process.execPath, [execBinPath(), "install-runtime"], {
7964
+ stdio: "inherit",
7965
+ env: process.env
7966
+ });
7748
7967
  if ((sync.status ?? 1) !== 0) {
7749
7968
  process.exit(sync.status ?? 1);
7750
7969
  }
@@ -7771,52 +7990,32 @@ function runUpdate(root) {
7771
7990
  const after = runtimeRevision(dest).revision;
7772
7991
  console.log(revisionBefore === after ? `update: runtime already at ${after ?? "unknown"} — nothing to move` : `update: runtime ${revisionBefore ?? "unknown"} → ${after ?? "unknown"}`);
7773
7992
  }
7774
- const binDir = process.env.TLC_BIN_DIR || join26(homedir3(), ".local", "bin");
7775
- mkdirSync16(binDir, { recursive: true });
7776
- mkdirSync16(join26(home, "..", "skills"), { recursive: true });
7777
- if (process.platform === "win32") {
7778
- const installPs1 = join26(dest, "install.ps1");
7779
- const r = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", installPs1], {
7780
- stdio: "inherit",
7781
- env: { ...process.env, TLC_HOME: home },
7782
- cwd: dest
7783
- });
7784
- if ((r.status ?? 1) !== 0) {
7785
- process.exit(r.status ?? 1);
7786
- }
7787
- } else {
7788
- const tlcBin = join26(dest, "bin", "tlc");
7789
- const skillSrc = join26(dest, "skills", "harness-init");
7790
- const skillDest = join26(home, "..", "skills", "harness-init");
7791
- spawnSync("ln", ["-sfn", tlcBin, join26(binDir, "tlc")], { stdio: "inherit" });
7792
- if (!existsSync25(skillSrc)) {
7793
- console.error(`update: missing skill at ${skillSrc}`);
7794
- process.exit(1);
7795
- }
7796
- spawnSync("ln", ["-sfn", skillSrc, skillDest], { stdio: "inherit" });
7797
- console.log(`update: skill → ${skillDest}`);
7798
- const hooks = spawnSync(process.execPath, [join26(dest, "bin", "write-user-hooks.mjs")], {
7799
- stdio: "inherit",
7800
- env: { ...process.env, TLC_HOME: home }
7801
- });
7802
- if ((hooks.status ?? 1) !== 0) {
7803
- console.log("update: hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
7804
- }
7993
+ const wired = wireRuntime(dest, home);
7994
+ for (const line of wired.lines) {
7995
+ console.log(`update: ${line}`);
7996
+ }
7997
+ if (wired.missingSkill) {
7998
+ console.error(`update: missing skill at ${join27(dest, "skills", "harness-init")}`);
7999
+ process.exit(1);
7805
8000
  }
7806
8001
  const missing = missingBundles(dest);
7807
8002
  if (missing.length === 0) {
7808
8003
  console.log("update: dist/ complete — no rebuild, so the runtime path stays clean");
7809
- } else if (existsSync25(buildBinPath())) {
8004
+ } else if (existsSync26(buildBinPath())) {
7810
8005
  console.log(`update: ${missing.length} bundle(s) missing — building`);
7811
- const build = spawnSync(buildBinPath(), [], { stdio: "inherit", env: process.env });
8006
+ const build = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
7812
8007
  if ((build.status ?? 1) !== 0) {
7813
8008
  console.log(`update: build failed — ${missing.length} bundle(s) still missing from dist/`);
7814
8009
  }
7815
8010
  }
7816
8011
  announceNewCapabilities(root, dest);
7817
8012
  announceLandedDecisions(root, dest, revisionBefore);
8013
+ spawnSync(process.execPath, [execBinPath(), "refresh-model-prices", "all", "--if-stale"], {
8014
+ stdio: "inherit",
8015
+ env: { ...process.env, TLC_PROJECT_DIR: root }
8016
+ });
7818
8017
  console.log("update: running doctor…");
7819
- const doctor = spawnSync(execBinPath(), ["doctor"], {
8018
+ const doctor = spawnSync(process.execPath, [execBinPath(), "doctor"], {
7820
8019
  stdio: "inherit",
7821
8020
  env: { ...process.env, TLC_PROJECT_DIR: root }
7822
8021
  });
@@ -7824,7 +8023,7 @@ function runUpdate(root) {
7824
8023
  process.exit(doctor.status ?? 0);
7825
8024
  }
7826
8025
  function runEntry(entry, toolArgs, root) {
7827
- const r = spawnSync(execBinPath(), [entry, ...toolArgs], {
8026
+ const r = spawnSync(process.execPath, [execBinPath(), entry, ...toolArgs], {
7828
8027
  stdio: "inherit",
7829
8028
  env: { ...process.env, TLC_PROJECT_DIR: root }
7830
8029
  });
@@ -7919,7 +8118,7 @@ function main(argv) {
7919
8118
  console.log(helpText(createStyle()));
7920
8119
  break;
7921
8120
  case "build": {
7922
- const r = spawnSync(buildBinPath(), [], { stdio: "inherit", env: process.env });
8121
+ const r = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
7923
8122
  process.exit(r.status ?? 1);
7924
8123
  break;
7925
8124
  }
@@ -8001,6 +8200,7 @@ if (__require.main == __require.module) {
8001
8200
  main(process.argv.slice(2));
8002
8201
  }
8003
8202
  export {
8203
+ wireRuntime,
8004
8204
  versionText,
8005
8205
  versionScreen,
8006
8206
  versionJson,