@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/dist/tlc-cli.mjs CHANGED
@@ -85,6 +85,17 @@ function bootDir(root) {
85
85
  function policyBaselineDir(root) {
86
86
  return join(projectStateDir(root), "policy-baseline");
87
87
  }
88
+ function claudeConfigDir() {
89
+ const custom = process.env.CLAUDE_CONFIG_DIR?.trim();
90
+ return custom && custom.length > 0 ? custom : join(homedir(), ".claude");
91
+ }
92
+ function cursorConfigDir() {
93
+ const custom = process.env.CURSOR_CONFIG_DIR?.trim();
94
+ return custom && custom.length > 0 ? custom : join(homedir(), ".cursor");
95
+ }
96
+ function providerConfigDirs() {
97
+ return [cursorConfigDir(), claudeConfigDir()];
98
+ }
88
99
 
89
100
  // src/core/attest/attest.service.ts
90
101
  var CHAIN_ROOT = "genesis";
@@ -283,20 +294,27 @@ function readJson(path) {
283
294
  return null;
284
295
  }
285
296
  }
286
- async function withFileLock(lockPath, fn) {
297
+ function isContention(error) {
298
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
299
+ }
300
+ async function withFileLock(lockPath, fn, options = {}) {
301
+ const {
302
+ openLock = (path) => closeSync(openSync(path, "wx")),
303
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
304
+ lockAttempts = 200
305
+ } = options;
287
306
  mkdirSync2(dirname2(lockPath), { recursive: true });
288
- const attempts = 200;
289
307
  let acquired = false;
290
- for (let attempt = 0;attempt < attempts; attempt++) {
308
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
291
309
  try {
292
- closeSync(openSync(lockPath, "wx"));
310
+ openLock(lockPath);
293
311
  acquired = true;
294
312
  break;
295
313
  } catch (error) {
296
- if (errorCode(error) !== "EEXIST") {
314
+ if (!isContention(error)) {
297
315
  throw error;
298
316
  }
299
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
317
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
300
318
  }
301
319
  }
302
320
  if (!acquired) {
@@ -311,14 +329,14 @@ async function withFileLock(lockPath, fn) {
311
329
  }
312
330
  }
313
331
  async function updateJsonAtomic(path, mutator, options) {
314
- const { lockPath, afterWrite, ...atomicOptions } = options;
332
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
315
333
  return withFileLock(lockPath, async () => {
316
334
  const current = readJson(path);
317
335
  const next = mutator(current);
318
336
  await writeJsonAtomic(path, next, atomicOptions);
319
337
  afterWrite?.(path);
320
338
  return next;
321
- });
339
+ }, { openLock, lockSleep, lockAttempts });
322
340
  }
323
341
 
324
342
  // src/core/capability/capability.store.ts
@@ -5569,6 +5587,68 @@ function evaluateShellCommand(args) {
5569
5587
  return { kind: "allow" };
5570
5588
  }
5571
5589
 
5590
+ // src/core/shim/shim.precedence.ts
5591
+ var LAUNCHER = "tlc-exec";
5592
+ function invocationText(entry) {
5593
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5594
+ }
5595
+ function coversHandler(settings, handler) {
5596
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5597
+ for (const matcher of matchers) {
5598
+ for (const entry of matcher.hooks ?? []) {
5599
+ const text = invocationText(entry);
5600
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5601
+ return true;
5602
+ }
5603
+ }
5604
+ }
5605
+ }
5606
+ return false;
5607
+ }
5608
+ function decideShim(userSettings, handler) {
5609
+ if (userSettings === null) {
5610
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5611
+ }
5612
+ return coversHandler(userSettings, handler) ? {
5613
+ run: false,
5614
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5615
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5616
+ }
5617
+
5618
+ // src/core/skill/skill.link.ts
5619
+ var SKILL_NAME = "harness-init";
5620
+ function skillLinks(runtimeHome2, providerDirs, present) {
5621
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5622
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5623
+ providerDir,
5624
+ source,
5625
+ target: `${providerDir}/skills/${SKILL_NAME}`
5626
+ }));
5627
+ }
5628
+ function linkHealth(target, runtimeHome2, probe) {
5629
+ const resolved = probe.linkTarget(target);
5630
+ if (resolved === null) {
5631
+ return { state: "absent", target };
5632
+ }
5633
+ if (!probe.exists(resolved)) {
5634
+ return { state: "dangling", target, resolved };
5635
+ }
5636
+ const home = runtimeHome2.replace(/\/+$/, "");
5637
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5638
+ }
5639
+ function linkHealthMessage(health) {
5640
+ switch (health.state) {
5641
+ case "ok":
5642
+ return `linked → ${health.resolved}`;
5643
+ case "dangling":
5644
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5645
+ case "outside-runtime":
5646
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5647
+ default:
5648
+ return "not linked — the provider cannot see the init skill";
5649
+ }
5650
+ }
5651
+
5572
5652
  // src/core/stagnation/stagnation.resolution.ts
5573
5653
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5574
5654
  import { join as join21 } from "node:path";
@@ -6696,6 +6776,15 @@ var coreFacade = {
6696
6776
  readHandoffFile,
6697
6777
  readForeignSlices
6698
6778
  },
6779
+ shim: {
6780
+ coversHandler,
6781
+ decideShim
6782
+ },
6783
+ skill: {
6784
+ linkHealth,
6785
+ linkHealthMessage,
6786
+ skillLinks
6787
+ },
6699
6788
  lesson: {
6700
6789
  projectLessonsInjectable,
6701
6790
  recordLessonFromFailure,
@@ -7773,7 +7862,6 @@ function runUpdate(root) {
7773
7862
  }
7774
7863
  const binDir = process.env.TLC_BIN_DIR || join26(homedir3(), ".local", "bin");
7775
7864
  mkdirSync16(binDir, { recursive: true });
7776
- mkdirSync16(join26(home, "..", "skills"), { recursive: true });
7777
7865
  if (process.platform === "win32") {
7778
7866
  const installPs1 = join26(dest, "install.ps1");
7779
7867
  const r = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", installPs1], {
@@ -7787,14 +7875,20 @@ function runUpdate(root) {
7787
7875
  } else {
7788
7876
  const tlcBin = join26(dest, "bin", "tlc");
7789
7877
  const skillSrc = join26(dest, "skills", "harness-init");
7790
- const skillDest = join26(home, "..", "skills", "harness-init");
7791
7878
  spawnSync("ln", ["-sfn", tlcBin, join26(binDir, "tlc")], { stdio: "inherit" });
7792
7879
  if (!existsSync25(skillSrc)) {
7793
7880
  console.error(`update: missing skill at ${skillSrc}`);
7794
7881
  process.exit(1);
7795
7882
  }
7796
- spawnSync("ln", ["-sfn", skillSrc, skillDest], { stdio: "inherit" });
7797
- console.log(`update: skill ${skillDest}`);
7883
+ const links = coreFacade.skill.skillLinks(dest, providerConfigDirs(), existsSync25);
7884
+ if (links.length === 0) {
7885
+ console.log("update: no provider config dir found — skill not linked");
7886
+ }
7887
+ for (const link of links) {
7888
+ mkdirSync16(join26(link.providerDir, "skills"), { recursive: true });
7889
+ spawnSync("ln", ["-sfn", link.source, link.target], { stdio: "inherit" });
7890
+ console.log(`update: skill → ${link.target}`);
7891
+ }
7798
7892
  const hooks = spawnSync(process.execPath, [join26(dest, "bin", "write-user-hooks.mjs")], {
7799
7893
  stdio: "inherit",
7800
7894
  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,
@@ -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,
@@ -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/docs/log.md CHANGED
@@ -18,6 +18,11 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
18
18
  - **AD-081** — The manifest npm publishes is checked here, because the release runner was the only thing reading it ([/decisions/ad-081.md](/decisions/ad-081.md))
19
19
  - **AD-082** — npm is the documented install, and the first version cannot come from CI ([/decisions/ad-082.md](/decisions/ad-082.md))
20
20
  - **AD-083** — Going public switched the branch ruleset on, and an unattended release needs the App to bypass it ([/decisions/ad-083.md](/decisions/ad-083.md))
21
+ - **AD-084** — The rule about unpinned dependencies is applied to this repository too, and the release gate stops being a copy ([/decisions/ad-084.md](/decisions/ad-084.md))
22
+ - **AD-085** — The flake was a test measuring a file the whole machine writes to ([/decisions/ad-085.md](/decisions/ad-085.md))
23
+ - **AD-086** — The write lock read the wrong error code on Windows, in a module that already listed the right ones ([/decisions/ad-086.md](/decisions/ad-086.md))
24
+ - **AD-087** — How the release works, and the six wrong shapes it took first ([/decisions/ad-087.md](/decisions/ad-087.md))
25
+ - **AD-095** — Four defects about where things are written, and one of them made every hook run twice ([/decisions/ad-095.md](/decisions/ad-095.md))
21
26
 
22
27
  ## 2026-08-17
23
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -30,7 +30,8 @@
30
30
  "!src/**/__test__",
31
31
  "!tools/__test__",
32
32
  "!tools/dev",
33
- "CHANGELOG.md",
33
+ "!tools/test-env*",
34
+ "!docs/decisions",
34
35
  "config.example.json",
35
36
  "model-aliases.json",
36
37
  "model-prices.cursor.json",
@@ -140,6 +140,7 @@ import {
140
140
  import { readReleaseSeen, writeReleaseSeen } from "./release/release.seen.ts";
141
141
  import { evaluateShellCommand } from "./shell-policy/shell-policy.service.ts";
142
142
  import { clearShellStall } from "./shell-policy/shell-policy.stall.ts";
143
+ import { coversHandler, decideShim } from "./shim/shim.precedence.ts";
143
144
  import { appendShipLedger, hasRecentEvidence, newestChangeMs, readShipLedger } from "./ship/ship.ledger.ts";
144
145
  import {
145
146
  detectShipClaim,
@@ -148,6 +149,7 @@ import {
148
149
  recentShipClaimActive,
149
150
  touchesRuntime,
150
151
  } from "./ship/ship.service.ts";
152
+ import { linkHealth, linkHealthMessage, skillLinks } from "./skill/skill.link.ts";
151
153
  import {
152
154
  recordResolution,
153
155
  resolutionFor,
@@ -271,6 +273,15 @@ export const coreFacade = {
271
273
  readHandoffFile,
272
274
  readForeignSlices,
273
275
  },
276
+ shim: {
277
+ coversHandler,
278
+ decideShim,
279
+ },
280
+ skill: {
281
+ linkHealth,
282
+ linkHealthMessage,
283
+ skillLinks,
284
+ },
274
285
  lesson: {
275
286
  projectLessonsInjectable,
276
287
  recordLessonFromFailure,
package/src/core/index.ts CHANGED
@@ -45,7 +45,9 @@ export type {
45
45
  } from "./policy/policy.types.ts";
46
46
  export type { PresenceRecord } from "./presence/presence.types.ts";
47
47
  export type { ShellEffectClass } from "./shell-policy/shell-policy.types.ts";
48
+ export type { ProviderSettings } from "./shim/shim.precedence.ts";
48
49
  export type { ShipClaim, ShipClaimKind, ShipLedgerEvent, ShipLedgerRow } from "./ship/ship.types.ts";
50
+ export type { LinkHealth, SkillLink } from "./skill/skill.link.ts";
49
51
  export type { FingerprintEntry, FingerprintStore } from "./stagnation/stagnation.types.ts";
50
52
  export type { ModelParam, ParentModelSnapshot } from "./subagent-policy/subagent-policy.types.ts";
51
53
  export type { AutopilotPlan } from "./turn/turn.autopilot.ts";