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

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 (120) hide show
  1. package/bin/tlc-cli.ts +20 -5
  2. package/dist/compact-before.mjs +86 -8
  3. package/dist/doctor.mjs +116 -10
  4. package/dist/init-project.mjs +13 -7
  5. package/dist/lessons-cli.mjs +86 -8
  6. package/dist/obs-cli.mjs +86 -8
  7. package/dist/prompt-submit.mjs +86 -8
  8. package/dist/response-after.mjs +86 -8
  9. package/dist/run.mjs +86 -8
  10. package/dist/session-end.mjs +86 -8
  11. package/dist/session-start.mjs +86 -8
  12. package/dist/shim.mjs +6976 -15
  13. package/dist/stop.mjs +86 -8
  14. package/dist/subagent-start.mjs +86 -8
  15. package/dist/subagent-stop.mjs +86 -8
  16. package/dist/support.mjs +86 -8
  17. package/dist/tlc-cli.mjs +106 -12
  18. package/dist/tool-after.mjs +86 -8
  19. package/dist/tool-before.mjs +86 -8
  20. package/dist/tool-failure.mjs +86 -8
  21. package/docs/log.md +5 -0
  22. package/package.json +3 -2
  23. package/src/core/core.facade.ts +11 -0
  24. package/src/core/index.ts +2 -0
  25. package/src/core/release/release.version.ts +147 -0
  26. package/src/core/shim/shim.precedence.ts +72 -0
  27. package/src/core/skill/skill.link.ts +81 -0
  28. package/src/entrypoints/shim.ts +41 -9
  29. package/src/platform/fs-atomic.ts +61 -23
  30. package/src/platform/paths.ts +27 -0
  31. package/tools/doctor.ts +50 -3
  32. package/tools/init-project.ts +27 -7
  33. package/CHANGELOG.md +0 -95
  34. package/docs/decisions/ad-001.md +0 -32
  35. package/docs/decisions/ad-002.md +0 -51
  36. package/docs/decisions/ad-003.md +0 -30
  37. package/docs/decisions/ad-004.md +0 -37
  38. package/docs/decisions/ad-005.md +0 -36
  39. package/docs/decisions/ad-006.md +0 -49
  40. package/docs/decisions/ad-007.md +0 -36
  41. package/docs/decisions/ad-008.md +0 -54
  42. package/docs/decisions/ad-009.md +0 -61
  43. package/docs/decisions/ad-010.md +0 -45
  44. package/docs/decisions/ad-011.md +0 -59
  45. package/docs/decisions/ad-012.md +0 -71
  46. package/docs/decisions/ad-013.md +0 -87
  47. package/docs/decisions/ad-014.md +0 -56
  48. package/docs/decisions/ad-015.md +0 -33
  49. package/docs/decisions/ad-016.md +0 -98
  50. package/docs/decisions/ad-017.md +0 -65
  51. package/docs/decisions/ad-018.md +0 -77
  52. package/docs/decisions/ad-019.md +0 -75
  53. package/docs/decisions/ad-020.md +0 -88
  54. package/docs/decisions/ad-021.md +0 -57
  55. package/docs/decisions/ad-022.md +0 -120
  56. package/docs/decisions/ad-023.md +0 -87
  57. package/docs/decisions/ad-024.md +0 -85
  58. package/docs/decisions/ad-025.md +0 -92
  59. package/docs/decisions/ad-026.md +0 -131
  60. package/docs/decisions/ad-027.md +0 -90
  61. package/docs/decisions/ad-028.md +0 -90
  62. package/docs/decisions/ad-029.md +0 -73
  63. package/docs/decisions/ad-030.md +0 -97
  64. package/docs/decisions/ad-031.md +0 -94
  65. package/docs/decisions/ad-032.md +0 -83
  66. package/docs/decisions/ad-033.md +0 -89
  67. package/docs/decisions/ad-034.md +0 -86
  68. package/docs/decisions/ad-035.md +0 -86
  69. package/docs/decisions/ad-036.md +0 -68
  70. package/docs/decisions/ad-037.md +0 -47
  71. package/docs/decisions/ad-038.md +0 -52
  72. package/docs/decisions/ad-039.md +0 -69
  73. package/docs/decisions/ad-040.md +0 -89
  74. package/docs/decisions/ad-041.md +0 -98
  75. package/docs/decisions/ad-042.md +0 -82
  76. package/docs/decisions/ad-043.md +0 -79
  77. package/docs/decisions/ad-044.md +0 -61
  78. package/docs/decisions/ad-045.md +0 -94
  79. package/docs/decisions/ad-046.md +0 -111
  80. package/docs/decisions/ad-047.md +0 -96
  81. package/docs/decisions/ad-048.md +0 -85
  82. package/docs/decisions/ad-049.md +0 -66
  83. package/docs/decisions/ad-050.md +0 -94
  84. package/docs/decisions/ad-051.md +0 -69
  85. package/docs/decisions/ad-052.md +0 -69
  86. package/docs/decisions/ad-053.md +0 -78
  87. package/docs/decisions/ad-054.md +0 -98
  88. package/docs/decisions/ad-055.md +0 -74
  89. package/docs/decisions/ad-056.md +0 -85
  90. package/docs/decisions/ad-057.md +0 -68
  91. package/docs/decisions/ad-058.md +0 -97
  92. package/docs/decisions/ad-059.md +0 -82
  93. package/docs/decisions/ad-060.md +0 -75
  94. package/docs/decisions/ad-061.md +0 -68
  95. package/docs/decisions/ad-062.md +0 -72
  96. package/docs/decisions/ad-063.md +0 -84
  97. package/docs/decisions/ad-064.md +0 -79
  98. package/docs/decisions/ad-065.md +0 -81
  99. package/docs/decisions/ad-066.md +0 -111
  100. package/docs/decisions/ad-067.md +0 -64
  101. package/docs/decisions/ad-068.md +0 -79
  102. package/docs/decisions/ad-069.md +0 -74
  103. package/docs/decisions/ad-070.md +0 -86
  104. package/docs/decisions/ad-071.md +0 -93
  105. package/docs/decisions/ad-072.md +0 -82
  106. package/docs/decisions/ad-073.md +0 -102
  107. package/docs/decisions/ad-074.md +0 -91
  108. package/docs/decisions/ad-075.md +0 -79
  109. package/docs/decisions/ad-076.md +0 -102
  110. package/docs/decisions/ad-077.md +0 -94
  111. package/docs/decisions/ad-078.md +0 -84
  112. package/docs/decisions/ad-079.md +0 -73
  113. package/docs/decisions/ad-080.md +0 -86
  114. package/docs/decisions/ad-081.md +0 -70
  115. package/docs/decisions/ad-082.md +0 -79
  116. package/docs/decisions/ad-083.md +0 -88
  117. package/docs/decisions/index.md +0 -111
  118. package/tools/test-env.mjs +0 -28
  119. package/tools/test-env.names.d.mts +0 -1
  120. package/tools/test-env.names.mjs +0 -14
package/bin/tlc-cli.ts CHANGED
@@ -13,7 +13,13 @@ import { homedir } from "node:os";
13
13
  import { delimiter, join } from "node:path";
14
14
  import { coreFacade } from "../src/core/index.ts";
15
15
  import { emitJson, JSON_FLAG, takeJsonFlag, unknownFlags } from "../src/platform/cli-output.ts";
16
- import { flagsDir, projectConfigPath, projectStateDir, runtimeHome } from "../src/platform/paths.ts";
16
+ import {
17
+ flagsDir,
18
+ projectConfigPath,
19
+ projectStateDir,
20
+ providerConfigDirs,
21
+ runtimeHome,
22
+ } from "../src/platform/paths.ts";
17
23
  import { type Row, render, type Screen, type Section } from "../src/platform/screen.ts";
18
24
  import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
19
25
 
@@ -1264,7 +1270,6 @@ function runUpdate(root: string): never {
1264
1270
 
1265
1271
  const binDir = process.env.TLC_BIN_DIR || join(homedir(), ".local", "bin");
1266
1272
  mkdirSync(binDir, { recursive: true });
1267
- mkdirSync(join(home, "..", "skills"), { recursive: true });
1268
1273
 
1269
1274
  if (process.platform === "win32") {
1270
1275
  const installPs1 = join(dest, "install.ps1");
@@ -1279,14 +1284,24 @@ function runUpdate(root: string): never {
1279
1284
  } else {
1280
1285
  const tlcBin = join(dest, "bin", "tlc");
1281
1286
  const skillSrc = join(dest, "skills", "harness-init");
1282
- const skillDest = join(home, "..", "skills", "harness-init");
1283
1287
  spawnSync("ln", ["-sfn", tlcBin, join(binDir, "tlc")], { stdio: "inherit" });
1284
1288
  if (!existsSync(skillSrc)) {
1285
1289
  console.error(`update: missing skill at ${skillSrc}`);
1286
1290
  process.exit(1);
1287
1291
  }
1288
- spawnSync("ln", ["-sfn", skillSrc, skillDest], { stdio: "inherit" });
1289
- console.log(`update: skill ${skillDest}`);
1292
+ // hazard: this linked to `<home>/../skills/harness-init` a directory no provider reads, and which did not
1293
+ // exist at all on the machine where this was found. `install.sh` had it right all along, linking into every
1294
+ // provider config directory that exists, because each provider only reads its own. The resolution is now one
1295
+ // function both use ([/decisions/ad-095.md](/decisions/ad-095.md)).
1296
+ const links = coreFacade.skill.skillLinks(dest, providerConfigDirs(), existsSync);
1297
+ if (links.length === 0) {
1298
+ console.log("update: no provider config dir found — skill not linked");
1299
+ }
1300
+ for (const link of links) {
1301
+ mkdirSync(join(link.providerDir, "skills"), { recursive: true });
1302
+ spawnSync("ln", ["-sfn", link.source, link.target], { stdio: "inherit" });
1303
+ console.log(`update: skill → ${link.target}`);
1304
+ }
1290
1305
  const hooks = spawnSync(process.execPath, [join(dest, "bin", "write-user-hooks.mjs")], {
1291
1306
  stdio: "inherit",
1292
1307
  env: { ...process.env, TLC_HOME: home },
@@ -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
@@ -5569,6 +5576,68 @@ function evaluateShellCommand(args) {
5569
5576
  return { kind: "allow" };
5570
5577
  }
5571
5578
 
5579
+ // src/core/shim/shim.precedence.ts
5580
+ var LAUNCHER = "tlc-exec";
5581
+ function invocationText(entry) {
5582
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5583
+ }
5584
+ function coversHandler(settings, handler) {
5585
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5586
+ for (const matcher of matchers) {
5587
+ for (const entry of matcher.hooks ?? []) {
5588
+ const text = invocationText(entry);
5589
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5590
+ return true;
5591
+ }
5592
+ }
5593
+ }
5594
+ }
5595
+ return false;
5596
+ }
5597
+ function decideShim(userSettings, handler) {
5598
+ if (userSettings === null) {
5599
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5600
+ }
5601
+ return coversHandler(userSettings, handler) ? {
5602
+ run: false,
5603
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5604
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5605
+ }
5606
+
5607
+ // src/core/skill/skill.link.ts
5608
+ var SKILL_NAME = "harness-init";
5609
+ function skillLinks(runtimeHome2, providerDirs, present) {
5610
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5611
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5612
+ providerDir,
5613
+ source,
5614
+ target: `${providerDir}/skills/${SKILL_NAME}`
5615
+ }));
5616
+ }
5617
+ function linkHealth(target, runtimeHome2, probe) {
5618
+ const resolved = probe.linkTarget(target);
5619
+ if (resolved === null) {
5620
+ return { state: "absent", target };
5621
+ }
5622
+ if (!probe.exists(resolved)) {
5623
+ return { state: "dangling", target, resolved };
5624
+ }
5625
+ const home = runtimeHome2.replace(/\/+$/, "");
5626
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5627
+ }
5628
+ function linkHealthMessage(health) {
5629
+ switch (health.state) {
5630
+ case "ok":
5631
+ return `linked → ${health.resolved}`;
5632
+ case "dangling":
5633
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5634
+ case "outside-runtime":
5635
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5636
+ default:
5637
+ return "not linked — the provider cannot see the init skill";
5638
+ }
5639
+ }
5640
+
5572
5641
  // src/core/stagnation/stagnation.resolution.ts
5573
5642
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5574
5643
  import { join as join21 } from "node:path";
@@ -6696,6 +6765,15 @@ var coreFacade = {
6696
6765
  readHandoffFile,
6697
6766
  readForeignSlices
6698
6767
  },
6768
+ shim: {
6769
+ coversHandler,
6770
+ decideShim
6771
+ },
6772
+ skill: {
6773
+ linkHealth,
6774
+ linkHealthMessage,
6775
+ skillLinks
6776
+ },
6699
6777
  lesson: {
6700
6778
  projectLessonsInjectable,
6701
6779
  recordLessonFromFailure,
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 existsSync29, lstatSync as lstatSync2, readFileSync as readFileSync31, 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, dirname as dirname9, join as join30 } 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
@@ -5581,6 +5591,68 @@ function evaluateShellCommand(args) {
5581
5591
  return { kind: "allow" };
5582
5592
  }
5583
5593
 
5594
+ // src/core/shim/shim.precedence.ts
5595
+ var LAUNCHER = "tlc-exec";
5596
+ function invocationText(entry) {
5597
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5598
+ }
5599
+ function coversHandler(settings, handler) {
5600
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5601
+ for (const matcher of matchers) {
5602
+ for (const entry of matcher.hooks ?? []) {
5603
+ const text = invocationText(entry);
5604
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5605
+ return true;
5606
+ }
5607
+ }
5608
+ }
5609
+ }
5610
+ return false;
5611
+ }
5612
+ function decideShim(userSettings, handler) {
5613
+ if (userSettings === null) {
5614
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5615
+ }
5616
+ return coversHandler(userSettings, handler) ? {
5617
+ run: false,
5618
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5619
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5620
+ }
5621
+
5622
+ // src/core/skill/skill.link.ts
5623
+ var SKILL_NAME = "harness-init";
5624
+ function skillLinks(runtimeHome2, providerDirs, present) {
5625
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5626
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5627
+ providerDir,
5628
+ source,
5629
+ target: `${providerDir}/skills/${SKILL_NAME}`
5630
+ }));
5631
+ }
5632
+ function linkHealth(target, runtimeHome2, probe) {
5633
+ const resolved = probe.linkTarget(target);
5634
+ if (resolved === null) {
5635
+ return { state: "absent", target };
5636
+ }
5637
+ if (!probe.exists(resolved)) {
5638
+ return { state: "dangling", target, resolved };
5639
+ }
5640
+ const home = runtimeHome2.replace(/\/+$/, "");
5641
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5642
+ }
5643
+ function linkHealthMessage(health) {
5644
+ switch (health.state) {
5645
+ case "ok":
5646
+ return `linked → ${health.resolved}`;
5647
+ case "dangling":
5648
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5649
+ case "outside-runtime":
5650
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5651
+ default:
5652
+ return "not linked — the provider cannot see the init skill";
5653
+ }
5654
+ }
5655
+
5584
5656
  // src/core/stagnation/stagnation.resolution.ts
5585
5657
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5586
5658
  import { join as join21 } from "node:path";
@@ -6708,6 +6780,15 @@ var coreFacade = {
6708
6780
  readHandoffFile,
6709
6781
  readForeignSlices
6710
6782
  },
6783
+ shim: {
6784
+ coversHandler,
6785
+ decideShim
6786
+ },
6787
+ skill: {
6788
+ linkHealth,
6789
+ linkHealthMessage,
6790
+ skillLinks
6791
+ },
6711
6792
  lesson: {
6712
6793
  projectLessonsInjectable,
6713
6794
  recordLessonFromFailure,
@@ -7893,6 +7974,29 @@ function runtimeOwnershipCheck(home) {
7893
7974
  detail: detail[kind]
7894
7975
  };
7895
7976
  }
7977
+ function checkSkillLinks(home, providerDirs = providerConfigDirs(), probe = {
7978
+ linkTarget: (path) => {
7979
+ try {
7980
+ return realpathSync4(path);
7981
+ } catch {
7982
+ try {
7983
+ return readlinkSync(path);
7984
+ } catch {
7985
+ return null;
7986
+ }
7987
+ }
7988
+ },
7989
+ exists: existsSync29
7990
+ }) {
7991
+ return providerDirs.filter((dir) => existsSync29(dir)).map((dir) => {
7992
+ const health = coreFacade.skill.linkHealth(join30(dir, "skills", "harness-init"), home, probe);
7993
+ return {
7994
+ level: health.state === "ok" ? "ok" : "fail",
7995
+ name: `init skill (${basename5(dir)})`,
7996
+ detail: coreFacade.skill.linkHealthMessage(health)
7997
+ };
7998
+ });
7999
+ }
7896
8000
  function checkRuntimePaths(home, platform) {
7897
8001
  const launcher = join30(home, "bin", "tlc-exec.mjs");
7898
8002
  const distSample = join30(home, "dist", "stop.mjs");
@@ -8171,6 +8275,7 @@ function runChecks(ctx) {
8171
8275
  return [
8172
8276
  ...checkNodeVersion(ctx.nodeVersion, ctx.bunPath),
8173
8277
  ...checkRuntimePaths(ctx.runtimeHome, ctx.platform),
8278
+ ...checkSkillLinks(ctx.runtimeHome),
8174
8279
  checkHookRuntime(ctx.runtimeHome, ctx.bunPath),
8175
8280
  ...checkProviders(ctx.registry, ctx.runtimeHome),
8176
8281
  ...checkProjectPolicy(ctx.root),
@@ -8251,6 +8356,7 @@ export {
8251
8356
  formatReport,
8252
8357
  exitCodeFor,
8253
8358
  checkSubagentAllowlist,
8359
+ checkSkillLinks,
8254
8360
  checkRuntimePaths,
8255
8361
  checkProviders,
8256
8362
  checkProjectPolicy,
@@ -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";
@@ -858,19 +858,23 @@ function claudeShimEntries(launcher) {
858
858
  ...spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}
859
859
  }));
860
860
  }
861
- var GITIGNORE_LINE = ".tlc/harness/state/";
861
+ var PROJECT_SHIMS = [join4(".cursor", "hooks.json"), join4(".claude", "settings.json")];
862
+ var GITIGNORE_STATE = ".tlc/harness/state/";
863
+ function gitignoreEntries() {
864
+ return [GITIGNORE_STATE, ...PROJECT_SHIMS.map((path) => path.split(sep).join("/"))];
865
+ }
862
866
  function mergeGitignore(root) {
863
867
  const path = join4(root, ".gitignore");
864
868
  const existing = existsSync3(path) ? readFileSync3(path, "utf8") : "";
865
869
  const lines = existing.split(`
866
870
  `);
867
- const alreadyPresent = lines.includes(GITIGNORE_LINE);
868
- if (alreadyPresent) {
871
+ const missing = gitignoreEntries().filter((entry) => !lines.includes(entry));
872
+ if (missing.length === 0) {
869
873
  return { text: existing.endsWith(`
870
874
  `) || existing === "" ? existing : `${existing}
871
875
  `, changed: false };
872
876
  }
873
- lines.push(GITIGNORE_LINE);
877
+ lines.push(...missing);
874
878
  const withoutTrailingBlank = lines.filter((line, index, all) => line.length > 0 || index < all.length - 1);
875
879
  return { text: `${withoutTrailingBlank.join(`
876
880
  `).replace(/\n+$/, "")}
@@ -901,7 +905,7 @@ function buildPlan(root, flags, stdinText, presence) {
901
905
  policy,
902
906
  cursorHooksDocument: presence.cursor ? renderCursorHooksDocument(cursorShimEntries(launcher)) : null,
903
907
  claudeHooksPreview: presence.claude ? claudeShimEntries(launcher) : null,
904
- gitignoreLine: GITIGNORE_LINE
908
+ gitignoreEntries: gitignoreEntries()
905
909
  };
906
910
  }
907
911
  function applyPlan(root, flags, presence, stdinText) {
@@ -983,11 +987,13 @@ export {
983
987
  mergeGitignore,
984
988
  main,
985
989
  launcherPath,
990
+ gitignoreEntries,
986
991
  detectProviders,
987
992
  cursorShimEntries,
988
993
  claudeShimEntries,
989
994
  buildPlan,
990
995
  applyPlan,
991
996
  UsageError,
992
- GITIGNORE_LINE
997
+ PROJECT_SHIMS,
998
+ GITIGNORE_STATE
993
999
  };
@@ -268,20 +268,27 @@ function readJson(path) {
268
268
  return null;
269
269
  }
270
270
  }
271
- async function withFileLock(lockPath, fn) {
271
+ function isContention(error) {
272
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
273
+ }
274
+ async function withFileLock(lockPath, fn, options = {}) {
275
+ const {
276
+ openLock = (path) => closeSync(openSync(path, "wx")),
277
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
278
+ lockAttempts = 200
279
+ } = options;
272
280
  mkdirSync2(dirname2(lockPath), { recursive: true });
273
- const attempts = 200;
274
281
  let acquired = false;
275
- for (let attempt = 0;attempt < attempts; attempt++) {
282
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
276
283
  try {
277
- closeSync(openSync(lockPath, "wx"));
284
+ openLock(lockPath);
278
285
  acquired = true;
279
286
  break;
280
287
  } catch (error) {
281
- if (errorCode(error) !== "EEXIST") {
288
+ if (!isContention(error)) {
282
289
  throw error;
283
290
  }
284
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
291
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
285
292
  }
286
293
  }
287
294
  if (!acquired) {
@@ -296,14 +303,14 @@ async function withFileLock(lockPath, fn) {
296
303
  }
297
304
  }
298
305
  async function updateJsonAtomic(path, mutator, options) {
299
- const { lockPath, afterWrite, ...atomicOptions } = options;
306
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
300
307
  return withFileLock(lockPath, async () => {
301
308
  const current = readJson(path);
302
309
  const next = mutator(current);
303
310
  await writeJsonAtomic(path, next, atomicOptions);
304
311
  afterWrite?.(path);
305
312
  return next;
306
- });
313
+ }, { openLock, lockSleep, lockAttempts });
307
314
  }
308
315
 
309
316
  // src/core/capability/capability.store.ts
@@ -5554,6 +5561,68 @@ function evaluateShellCommand(args) {
5554
5561
  return { kind: "allow" };
5555
5562
  }
5556
5563
 
5564
+ // src/core/shim/shim.precedence.ts
5565
+ var LAUNCHER = "tlc-exec";
5566
+ function invocationText(entry) {
5567
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5568
+ }
5569
+ function coversHandler(settings, handler) {
5570
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5571
+ for (const matcher of matchers) {
5572
+ for (const entry of matcher.hooks ?? []) {
5573
+ const text = invocationText(entry);
5574
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5575
+ return true;
5576
+ }
5577
+ }
5578
+ }
5579
+ }
5580
+ return false;
5581
+ }
5582
+ function decideShim(userSettings, handler) {
5583
+ if (userSettings === null) {
5584
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5585
+ }
5586
+ return coversHandler(userSettings, handler) ? {
5587
+ run: false,
5588
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5589
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5590
+ }
5591
+
5592
+ // src/core/skill/skill.link.ts
5593
+ var SKILL_NAME = "harness-init";
5594
+ function skillLinks(runtimeHome2, providerDirs, present) {
5595
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5596
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5597
+ providerDir,
5598
+ source,
5599
+ target: `${providerDir}/skills/${SKILL_NAME}`
5600
+ }));
5601
+ }
5602
+ function linkHealth(target, runtimeHome2, probe) {
5603
+ const resolved = probe.linkTarget(target);
5604
+ if (resolved === null) {
5605
+ return { state: "absent", target };
5606
+ }
5607
+ if (!probe.exists(resolved)) {
5608
+ return { state: "dangling", target, resolved };
5609
+ }
5610
+ const home = runtimeHome2.replace(/\/+$/, "");
5611
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5612
+ }
5613
+ function linkHealthMessage(health) {
5614
+ switch (health.state) {
5615
+ case "ok":
5616
+ return `linked → ${health.resolved}`;
5617
+ case "dangling":
5618
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5619
+ case "outside-runtime":
5620
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5621
+ default:
5622
+ return "not linked — the provider cannot see the init skill";
5623
+ }
5624
+ }
5625
+
5557
5626
  // src/core/stagnation/stagnation.resolution.ts
5558
5627
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5559
5628
  import { join as join21 } from "node:path";
@@ -6681,6 +6750,15 @@ var coreFacade = {
6681
6750
  readHandoffFile,
6682
6751
  readForeignSlices
6683
6752
  },
6753
+ shim: {
6754
+ coversHandler,
6755
+ decideShim
6756
+ },
6757
+ skill: {
6758
+ linkHealth,
6759
+ linkHealthMessage,
6760
+ skillLinks
6761
+ },
6684
6762
  lesson: {
6685
6763
  projectLessonsInjectable,
6686
6764
  recordLessonFromFailure,