@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.
- package/bin/tlc-cli.ts +20 -5
- package/dist/compact-before.mjs +86 -8
- package/dist/doctor.mjs +116 -10
- package/dist/init-project.mjs +13 -7
- package/dist/lessons-cli.mjs +86 -8
- package/dist/obs-cli.mjs +86 -8
- package/dist/prompt-submit.mjs +86 -8
- package/dist/response-after.mjs +86 -8
- package/dist/run.mjs +86 -8
- package/dist/session-end.mjs +86 -8
- package/dist/session-start.mjs +86 -8
- package/dist/shim.mjs +6976 -15
- package/dist/stop.mjs +86 -8
- package/dist/subagent-start.mjs +86 -8
- package/dist/subagent-stop.mjs +86 -8
- package/dist/support.mjs +86 -8
- package/dist/tlc-cli.mjs +106 -12
- package/dist/tool-after.mjs +86 -8
- package/dist/tool-before.mjs +86 -8
- package/dist/tool-failure.mjs +86 -8
- package/docs/log.md +5 -0
- package/package.json +3 -2
- package/src/core/core.facade.ts +11 -0
- package/src/core/index.ts +2 -0
- package/src/core/release/release.version.ts +147 -0
- package/src/core/shim/shim.precedence.ts +72 -0
- package/src/core/skill/skill.link.ts +81 -0
- package/src/entrypoints/shim.ts +41 -9
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/paths.ts +27 -0
- package/tools/doctor.ts +50 -3
- package/tools/init-project.ts +27 -7
- package/CHANGELOG.md +0 -95
- package/docs/decisions/ad-001.md +0 -32
- package/docs/decisions/ad-002.md +0 -51
- package/docs/decisions/ad-003.md +0 -30
- package/docs/decisions/ad-004.md +0 -37
- package/docs/decisions/ad-005.md +0 -36
- package/docs/decisions/ad-006.md +0 -49
- package/docs/decisions/ad-007.md +0 -36
- package/docs/decisions/ad-008.md +0 -54
- package/docs/decisions/ad-009.md +0 -61
- package/docs/decisions/ad-010.md +0 -45
- package/docs/decisions/ad-011.md +0 -59
- package/docs/decisions/ad-012.md +0 -71
- package/docs/decisions/ad-013.md +0 -87
- package/docs/decisions/ad-014.md +0 -56
- package/docs/decisions/ad-015.md +0 -33
- package/docs/decisions/ad-016.md +0 -98
- package/docs/decisions/ad-017.md +0 -65
- package/docs/decisions/ad-018.md +0 -77
- package/docs/decisions/ad-019.md +0 -75
- package/docs/decisions/ad-020.md +0 -88
- package/docs/decisions/ad-021.md +0 -57
- package/docs/decisions/ad-022.md +0 -120
- package/docs/decisions/ad-023.md +0 -87
- package/docs/decisions/ad-024.md +0 -85
- package/docs/decisions/ad-025.md +0 -92
- package/docs/decisions/ad-026.md +0 -131
- package/docs/decisions/ad-027.md +0 -90
- package/docs/decisions/ad-028.md +0 -90
- package/docs/decisions/ad-029.md +0 -73
- package/docs/decisions/ad-030.md +0 -97
- package/docs/decisions/ad-031.md +0 -94
- package/docs/decisions/ad-032.md +0 -83
- package/docs/decisions/ad-033.md +0 -89
- package/docs/decisions/ad-034.md +0 -86
- package/docs/decisions/ad-035.md +0 -86
- package/docs/decisions/ad-036.md +0 -68
- package/docs/decisions/ad-037.md +0 -47
- package/docs/decisions/ad-038.md +0 -52
- package/docs/decisions/ad-039.md +0 -69
- package/docs/decisions/ad-040.md +0 -89
- package/docs/decisions/ad-041.md +0 -98
- package/docs/decisions/ad-042.md +0 -82
- package/docs/decisions/ad-043.md +0 -79
- package/docs/decisions/ad-044.md +0 -61
- package/docs/decisions/ad-045.md +0 -94
- package/docs/decisions/ad-046.md +0 -111
- package/docs/decisions/ad-047.md +0 -96
- package/docs/decisions/ad-048.md +0 -85
- package/docs/decisions/ad-049.md +0 -66
- package/docs/decisions/ad-050.md +0 -94
- package/docs/decisions/ad-051.md +0 -69
- package/docs/decisions/ad-052.md +0 -69
- package/docs/decisions/ad-053.md +0 -78
- package/docs/decisions/ad-054.md +0 -98
- package/docs/decisions/ad-055.md +0 -74
- package/docs/decisions/ad-056.md +0 -85
- package/docs/decisions/ad-057.md +0 -68
- package/docs/decisions/ad-058.md +0 -97
- package/docs/decisions/ad-059.md +0 -82
- package/docs/decisions/ad-060.md +0 -75
- package/docs/decisions/ad-061.md +0 -68
- package/docs/decisions/ad-062.md +0 -72
- package/docs/decisions/ad-063.md +0 -84
- package/docs/decisions/ad-064.md +0 -79
- package/docs/decisions/ad-065.md +0 -81
- package/docs/decisions/ad-066.md +0 -111
- package/docs/decisions/ad-067.md +0 -64
- package/docs/decisions/ad-068.md +0 -79
- package/docs/decisions/ad-069.md +0 -74
- package/docs/decisions/ad-070.md +0 -86
- package/docs/decisions/ad-071.md +0 -93
- package/docs/decisions/ad-072.md +0 -82
- package/docs/decisions/ad-073.md +0 -102
- package/docs/decisions/ad-074.md +0 -91
- package/docs/decisions/ad-075.md +0 -79
- package/docs/decisions/ad-076.md +0 -102
- package/docs/decisions/ad-077.md +0 -94
- package/docs/decisions/ad-078.md +0 -84
- package/docs/decisions/ad-079.md +0 -73
- package/docs/decisions/ad-080.md +0 -86
- package/docs/decisions/ad-081.md +0 -70
- package/docs/decisions/ad-082.md +0 -79
- package/docs/decisions/ad-083.md +0 -88
- package/docs/decisions/index.md +0 -111
- package/tools/test-env.mjs +0 -28
- package/tools/test-env.names.d.mts +0 -1
- 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 {
|
|
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
|
-
|
|
1289
|
-
|
|
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 },
|
package/dist/compact-before.mjs
CHANGED
|
@@ -276,20 +276,27 @@ function readJson(path) {
|
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
-
|
|
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 <
|
|
290
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
284
291
|
try {
|
|
285
|
-
|
|
292
|
+
openLock(lockPath);
|
|
286
293
|
acquired = true;
|
|
287
294
|
break;
|
|
288
295
|
} catch (error) {
|
|
289
|
-
if (
|
|
296
|
+
if (!isContention(error)) {
|
|
290
297
|
throw error;
|
|
291
298
|
}
|
|
292
|
-
await
|
|
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
|
-
|
|
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 <
|
|
312
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
303
313
|
try {
|
|
304
|
-
|
|
314
|
+
openLock(lockPath);
|
|
305
315
|
acquired = true;
|
|
306
316
|
break;
|
|
307
317
|
} catch (error) {
|
|
308
|
-
if (
|
|
318
|
+
if (!isContention(error)) {
|
|
309
319
|
throw error;
|
|
310
320
|
}
|
|
311
|
-
await
|
|
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,
|
package/dist/init-project.mjs
CHANGED
|
@@ -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
|
|
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
|
|
868
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
997
|
+
PROJECT_SHIMS,
|
|
998
|
+
GITIGNORE_STATE
|
|
993
999
|
};
|
package/dist/lessons-cli.mjs
CHANGED
|
@@ -268,20 +268,27 @@ function readJson(path) {
|
|
|
268
268
|
return null;
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
|
|
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 <
|
|
282
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
276
283
|
try {
|
|
277
|
-
|
|
284
|
+
openLock(lockPath);
|
|
278
285
|
acquired = true;
|
|
279
286
|
break;
|
|
280
287
|
} catch (error) {
|
|
281
|
-
if (
|
|
288
|
+
if (!isContention(error)) {
|
|
282
289
|
throw error;
|
|
283
290
|
}
|
|
284
|
-
await
|
|
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,
|