@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/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
|
-
|
|
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 <
|
|
308
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
291
309
|
try {
|
|
292
|
-
|
|
310
|
+
openLock(lockPath);
|
|
293
311
|
acquired = true;
|
|
294
312
|
break;
|
|
295
313
|
} catch (error) {
|
|
296
|
-
if (
|
|
314
|
+
if (!isContention(error)) {
|
|
297
315
|
throw error;
|
|
298
316
|
}
|
|
299
|
-
await
|
|
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
|
-
|
|
7797
|
-
|
|
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 }
|
package/dist/tool-after.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/tool-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/tool-failure.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/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.
|
|
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
|
-
"
|
|
33
|
+
"!tools/test-env*",
|
|
34
|
+
"!docs/decisions",
|
|
34
35
|
"config.example.json",
|
|
35
36
|
"model-aliases.json",
|
|
36
37
|
"model-prices.cursor.json",
|
package/src/core/core.facade.ts
CHANGED
|
@@ -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";
|