@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/obs-cli.mjs CHANGED
@@ -272,20 +272,27 @@ function readJson(path) {
272
272
  return null;
273
273
  }
274
274
  }
275
- async function withFileLock(lockPath, fn) {
275
+ function isContention(error) {
276
+ return errorCode(error) === "EEXIST" || isRetryableFsError(error);
277
+ }
278
+ async function withFileLock(lockPath, fn, options = {}) {
279
+ const {
280
+ openLock = (path) => closeSync(openSync(path, "wx")),
281
+ lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
282
+ lockAttempts = 200
283
+ } = options;
276
284
  mkdirSync2(dirname2(lockPath), { recursive: true });
277
- const attempts = 200;
278
285
  let acquired = false;
279
- for (let attempt = 0;attempt < attempts; attempt++) {
286
+ for (let attempt = 0;attempt < lockAttempts; attempt++) {
280
287
  try {
281
- closeSync(openSync(lockPath, "wx"));
288
+ openLock(lockPath);
282
289
  acquired = true;
283
290
  break;
284
291
  } catch (error) {
285
- if (errorCode(error) !== "EEXIST") {
292
+ if (!isContention(error)) {
286
293
  throw error;
287
294
  }
288
- await new Promise((resolve) => setTimeout(resolve, nextDelay({ attempt, baseMs: 10, capMs: 200 })));
295
+ await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
289
296
  }
290
297
  }
291
298
  if (!acquired) {
@@ -300,14 +307,14 @@ async function withFileLock(lockPath, fn) {
300
307
  }
301
308
  }
302
309
  async function updateJsonAtomic(path, mutator, options) {
303
- const { lockPath, afterWrite, ...atomicOptions } = options;
310
+ const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
304
311
  return withFileLock(lockPath, async () => {
305
312
  const current = readJson(path);
306
313
  const next = mutator(current);
307
314
  await writeJsonAtomic(path, next, atomicOptions);
308
315
  afterWrite?.(path);
309
316
  return next;
310
- });
317
+ }, { openLock, lockSleep, lockAttempts });
311
318
  }
312
319
 
313
320
  // src/core/capability/capability.store.ts
@@ -5558,6 +5565,68 @@ function evaluateShellCommand(args) {
5558
5565
  return { kind: "allow" };
5559
5566
  }
5560
5567
 
5568
+ // src/core/shim/shim.precedence.ts
5569
+ var LAUNCHER = "tlc-exec";
5570
+ function invocationText(entry) {
5571
+ return [entry.command ?? "", ...entry.args ?? []].join(" ");
5572
+ }
5573
+ function coversHandler(settings, handler) {
5574
+ for (const matchers of Object.values(settings.hooks ?? {})) {
5575
+ for (const matcher of matchers) {
5576
+ for (const entry of matcher.hooks ?? []) {
5577
+ const text = invocationText(entry);
5578
+ if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
5579
+ return true;
5580
+ }
5581
+ }
5582
+ }
5583
+ }
5584
+ return false;
5585
+ }
5586
+ function decideShim(userSettings, handler) {
5587
+ if (userSettings === null) {
5588
+ return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
5589
+ }
5590
+ return coversHandler(userSettings, handler) ? {
5591
+ run: false,
5592
+ reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
5593
+ } : { run: true, reason: `no user-level hook runs ${handler}` };
5594
+ }
5595
+
5596
+ // src/core/skill/skill.link.ts
5597
+ var SKILL_NAME = "harness-init";
5598
+ function skillLinks(runtimeHome2, providerDirs, present) {
5599
+ const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
5600
+ return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
5601
+ providerDir,
5602
+ source,
5603
+ target: `${providerDir}/skills/${SKILL_NAME}`
5604
+ }));
5605
+ }
5606
+ function linkHealth(target, runtimeHome2, probe) {
5607
+ const resolved = probe.linkTarget(target);
5608
+ if (resolved === null) {
5609
+ return { state: "absent", target };
5610
+ }
5611
+ if (!probe.exists(resolved)) {
5612
+ return { state: "dangling", target, resolved };
5613
+ }
5614
+ const home = runtimeHome2.replace(/\/+$/, "");
5615
+ return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
5616
+ }
5617
+ function linkHealthMessage(health) {
5618
+ switch (health.state) {
5619
+ case "ok":
5620
+ return `linked → ${health.resolved}`;
5621
+ case "dangling":
5622
+ return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
5623
+ case "outside-runtime":
5624
+ return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
5625
+ default:
5626
+ return "not linked — the provider cannot see the init skill";
5627
+ }
5628
+ }
5629
+
5561
5630
  // src/core/stagnation/stagnation.resolution.ts
5562
5631
  import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
5563
5632
  import { join as join21 } from "node:path";
@@ -6685,6 +6754,15 @@ var coreFacade = {
6685
6754
  readHandoffFile,
6686
6755
  readForeignSlices
6687
6756
  },
6757
+ shim: {
6758
+ coversHandler,
6759
+ decideShim
6760
+ },
6761
+ skill: {
6762
+ linkHealth,
6763
+ linkHealthMessage,
6764
+ skillLinks
6765
+ },
6688
6766
  lesson: {
6689
6767
  projectLessonsInjectable,
6690
6768
  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/dist/run.mjs CHANGED
@@ -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,