@tech-leads-club/harness-toolkit 0.2.1 → 0.3.0
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/README.md +22 -26
- package/bin/tlc-build.mjs +93 -0
- package/bin/tlc-cli.ts +117 -58
- package/bin/tlc-exec.mjs +16 -13
- package/dist/compact-before.mjs +151 -15
- package/dist/doctor.mjs +288 -45
- package/dist/help-topic.mjs +0 -0
- package/dist/init-project.mjs +15 -14
- package/dist/install-runtime.mjs +100 -17
- package/dist/lessons-cli.mjs +152 -8
- package/dist/obs-cli.mjs +149 -8
- package/dist/price-lookup.mjs +45 -23
- package/dist/prompt-submit.mjs +151 -15
- package/dist/refresh-model-prices.mjs +7190 -46
- package/dist/response-after.mjs +151 -15
- package/dist/run.mjs +151 -15
- package/dist/session-end.mjs +151 -15
- package/dist/session-start.mjs +151 -15
- package/dist/shim.mjs +7040 -16
- package/dist/stop.mjs +151 -15
- package/dist/subagent-start.mjs +151 -15
- package/dist/subagent-stop.mjs +151 -15
- package/dist/support.mjs +149 -8
- package/dist/tlc-cli.mjs +289 -89
- package/dist/tool-after.mjs +196 -38
- package/dist/tool-before.mjs +151 -15
- package/dist/tool-failure.mjs +151 -15
- package/dist/uninstall-runtime.mjs +9 -10
- package/docs/log.md +7 -0
- package/docs/measure.md +35 -31
- package/package.json +6 -5
- package/src/core/core.facade.ts +18 -0
- package/src/core/index.ts +3 -0
- package/src/core/pricing/pricing.freshness.ts +118 -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 +92 -0
- package/src/entrypoints/shim.ts +49 -11
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/links.ts +73 -0
- package/src/platform/paths.ts +27 -0
- package/src/platform/pricing.ts +139 -31
- package/src/providers/cursor/cursor.wiring.ts +11 -8
- package/tools/doctor.ts +127 -10
- package/tools/init-project.ts +34 -14
- package/tools/install-runtime.ts +89 -6
- package/tools/refresh-model-prices.ts +242 -75
- package/tools/uninstall-runtime.ts +23 -19
- package/CHANGELOG.md +0 -95
- package/bin/tlc-build +0 -80
- package/bin/tlc-exec +0 -10
- package/bin/tlc-exec.cmd +0 -4
- 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/model-aliases.json +0 -12
- package/model-prices.cursor.json +0 -410
- package/model-prices.json +0 -1
- 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/install-runtime.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// tools/install-runtime.ts
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2, writeFileSync } from "node:fs";
|
|
7
|
+
import { join as join3, relative, resolve, sep } from "node:path";
|
|
7
8
|
|
|
8
9
|
// src/platform/paths.ts
|
|
9
10
|
import { homedir } from "node:os";
|
|
@@ -849,6 +850,34 @@ var TOOL_KINDS = new Set([
|
|
|
849
850
|
"file.edit",
|
|
850
851
|
"file.read"
|
|
851
852
|
]);
|
|
853
|
+
// src/platform/links.ts
|
|
854
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
855
|
+
import { dirname, join as join2 } from "node:path";
|
|
856
|
+
var LINK_TYPE = "junction";
|
|
857
|
+
function linkDir(source, target) {
|
|
858
|
+
let replaced = false;
|
|
859
|
+
if (isLink(target)) {
|
|
860
|
+
rmSync(target, { recursive: true, force: true });
|
|
861
|
+
replaced = true;
|
|
862
|
+
} else if (existsSync(target)) {
|
|
863
|
+
return {
|
|
864
|
+
kind: "refused",
|
|
865
|
+
target,
|
|
866
|
+
reason: `${target} exists and is not a link — move it aside and re-run`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
870
|
+
symlinkSync(source, target, LINK_TYPE);
|
|
871
|
+
return { kind: replaced ? "relinked" : "linked", target, source };
|
|
872
|
+
}
|
|
873
|
+
function isLink(path) {
|
|
874
|
+
try {
|
|
875
|
+
return lstatSync(path).isSymbolicLink();
|
|
876
|
+
} catch {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
852
881
|
// bin/tlc-cli.ts
|
|
853
882
|
var NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
|
|
854
883
|
var NPM_MARKER = "installed-from-npm";
|
|
@@ -864,13 +893,10 @@ var RUNTIME_PAYLOAD = [
|
|
|
864
893
|
"src",
|
|
865
894
|
"tools",
|
|
866
895
|
"config.example.json",
|
|
867
|
-
"model-aliases.json",
|
|
868
|
-
"model-prices.cursor.json",
|
|
869
|
-
"model-prices.json",
|
|
870
896
|
"package.json"
|
|
871
897
|
];
|
|
872
898
|
var OPERATOR_OWNED = ["config.json", "state", "flags"];
|
|
873
|
-
var NOT_SHIPPED = [
|
|
899
|
+
var NOT_SHIPPED = [join3("tools", "dev"), join3("tools", "__test__")];
|
|
874
900
|
function isShipped(relativePath) {
|
|
875
901
|
const normalised = relativePath.split(sep).join("/");
|
|
876
902
|
return !NOT_SHIPPED.some((excluded) => {
|
|
@@ -890,34 +916,75 @@ function installRuntime(source, dest) {
|
|
|
890
916
|
if (resolve(source) === resolve(dest)) {
|
|
891
917
|
return { kind: "in-place", source, dest, entries: [], missing: [] };
|
|
892
918
|
}
|
|
893
|
-
|
|
919
|
+
mkdirSync2(dest, { recursive: true });
|
|
894
920
|
const entries = [];
|
|
895
921
|
const missing = [];
|
|
896
922
|
for (const entry of RUNTIME_PAYLOAD) {
|
|
897
|
-
const from =
|
|
898
|
-
if (!
|
|
923
|
+
const from = join3(source, entry);
|
|
924
|
+
if (!existsSync2(from)) {
|
|
899
925
|
missing.push(entry);
|
|
900
926
|
continue;
|
|
901
927
|
}
|
|
902
|
-
const to =
|
|
903
|
-
|
|
928
|
+
const to = join3(dest, entry);
|
|
929
|
+
rmSync2(to, { recursive: true, force: true });
|
|
904
930
|
cpSync(from, to, {
|
|
905
931
|
recursive: true,
|
|
906
932
|
filter: (src) => isShipped(relative(source, src))
|
|
907
933
|
});
|
|
908
934
|
entries.push(entry);
|
|
909
935
|
}
|
|
910
|
-
writeFileSync(
|
|
936
|
+
writeFileSync(join3(dest, NPM_MARKER), `Installed by \`tlc harness install\` from ${source}.
|
|
911
937
|
Update with: npm i -g ${NPM_PACKAGE}@latest && tlc harness install
|
|
912
938
|
`, "utf8");
|
|
913
|
-
const config =
|
|
914
|
-
const example =
|
|
915
|
-
if (!
|
|
939
|
+
const config = join3(dest, "config.json");
|
|
940
|
+
const example = join3(dest, "config.example.json");
|
|
941
|
+
if (!existsSync2(config) && existsSync2(example)) {
|
|
916
942
|
writeFileSync(config, readFileSync(example, "utf8"), "utf8");
|
|
917
943
|
}
|
|
918
944
|
return { kind: "copied", source, dest, entries, missing };
|
|
919
945
|
}
|
|
946
|
+
function linkRuntime(source, dest) {
|
|
947
|
+
if (resolve(source) === resolve(dest)) {
|
|
948
|
+
return { kind: "in-place", source, dest, entries: [], missing: [] };
|
|
949
|
+
}
|
|
950
|
+
const outcome = linkDir(resolve(source), dest);
|
|
951
|
+
if (outcome.kind === "refused") {
|
|
952
|
+
return { kind: "refused", source, dest, entries: [], missing: [], reason: outcome.reason };
|
|
953
|
+
}
|
|
954
|
+
const missing = RUNTIME_PAYLOAD.filter((entry) => !existsSync2(join3(dest, entry)));
|
|
955
|
+
return { kind: outcome.kind === "relinked" ? "relinked" : "linked", source, dest, entries: [], missing };
|
|
956
|
+
}
|
|
920
957
|
function installScreen(report) {
|
|
958
|
+
if (report.kind === "refused") {
|
|
959
|
+
return {
|
|
960
|
+
title: "harness install",
|
|
961
|
+
sections: [{ rows: [{ label: "refused", value: report.reason ?? "", level: "fail" }] }]
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (report.kind === "linked" || report.kind === "relinked") {
|
|
965
|
+
return {
|
|
966
|
+
title: "harness install",
|
|
967
|
+
sections: [
|
|
968
|
+
{
|
|
969
|
+
rows: [
|
|
970
|
+
{
|
|
971
|
+
label: report.kind === "linked" ? "linked" : "relinked",
|
|
972
|
+
value: `${report.dest} → ${report.source}`,
|
|
973
|
+
level: "ok"
|
|
974
|
+
},
|
|
975
|
+
...report.missing.length > 0 ? [
|
|
976
|
+
{
|
|
977
|
+
label: "incomplete",
|
|
978
|
+
value: `the checkout has no ${report.missing.join(", ")} — run the build`,
|
|
979
|
+
level: "fail"
|
|
980
|
+
}
|
|
981
|
+
] : []
|
|
982
|
+
]
|
|
983
|
+
}
|
|
984
|
+
],
|
|
985
|
+
footer: "an edit in the checkout is live in the next hook · `npm link` puts `tlc` on PATH"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
921
988
|
if (report.kind === "in-place") {
|
|
922
989
|
return {
|
|
923
990
|
title: "harness install",
|
|
@@ -949,20 +1016,36 @@ function installDest(env = process.env) {
|
|
|
949
1016
|
}
|
|
950
1017
|
return runtimeHomeWasChosen(env) ? runtimeHome(env) : conventionalRuntimeHome();
|
|
951
1018
|
}
|
|
1019
|
+
function fetchPrices(dest, spawn = spawnSync) {
|
|
1020
|
+
const result = spawn(process.execPath, [join3(dest, "bin", "tlc-exec.mjs"), "refresh-model-prices"], {
|
|
1021
|
+
stdio: "inherit",
|
|
1022
|
+
env: { ...process.env, TLC_HOME: dest }
|
|
1023
|
+
});
|
|
1024
|
+
if ((result.status ?? 1) !== 0) {
|
|
1025
|
+
console.log("install: prices not fetched — cost estimates stay empty until `tlc harness prices refresh`");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
952
1028
|
if (__require.main == __require.module) {
|
|
953
|
-
const
|
|
1029
|
+
const link = process.argv.includes("--link");
|
|
1030
|
+
const source = link ? process.cwd() : originRoot();
|
|
954
1031
|
const dest = installDest();
|
|
955
|
-
const report = installRuntime(source, dest);
|
|
1032
|
+
const report = link ? linkRuntime(source, dest) : installRuntime(source, dest);
|
|
956
1033
|
console.log(installReportText(report, createStyle()));
|
|
1034
|
+
if (report.kind === "refused") {
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
fetchPrices(dest);
|
|
957
1038
|
process.exit(report.missing.length > 0 ? 1 : 0);
|
|
958
1039
|
}
|
|
959
1040
|
export {
|
|
960
1041
|
originRoot,
|
|
1042
|
+
linkRuntime,
|
|
961
1043
|
isShipped,
|
|
962
1044
|
installScreen,
|
|
963
1045
|
installRuntime,
|
|
964
1046
|
installReportText,
|
|
965
1047
|
installDest,
|
|
1048
|
+
fetchPrices,
|
|
966
1049
|
RUNTIME_PAYLOAD,
|
|
967
1050
|
OPERATOR_OWNED,
|
|
968
1051
|
NOT_SHIPPED
|
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
|
|
@@ -5251,6 +5258,62 @@ function release(root, provider, session) {
|
|
|
5251
5258
|
deletePresenceRecord(root, provider, session);
|
|
5252
5259
|
}
|
|
5253
5260
|
|
|
5261
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5262
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5263
|
+
var MS_PER_DAY = 86400000;
|
|
5264
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5265
|
+
if (meta === null) {
|
|
5266
|
+
return { state: "absent" };
|
|
5267
|
+
}
|
|
5268
|
+
const stamp = meta.refreshedAt;
|
|
5269
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5270
|
+
return { state: "undated" };
|
|
5271
|
+
}
|
|
5272
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5273
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5274
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5275
|
+
}
|
|
5276
|
+
function shouldRefetch(state) {
|
|
5277
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5278
|
+
}
|
|
5279
|
+
function freshnessMessage(state, catalogue) {
|
|
5280
|
+
switch (state.state) {
|
|
5281
|
+
case "absent":
|
|
5282
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5283
|
+
case "undated":
|
|
5284
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5285
|
+
case "fresh":
|
|
5286
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5287
|
+
default:
|
|
5288
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5292
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5293
|
+
if (incomingCount === 0) {
|
|
5294
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5295
|
+
}
|
|
5296
|
+
if (existingCount === 0) {
|
|
5297
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5298
|
+
}
|
|
5299
|
+
if (incomingCount >= existingCount) {
|
|
5300
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5301
|
+
}
|
|
5302
|
+
const retained = incomingCount / existingCount;
|
|
5303
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5304
|
+
replace: false,
|
|
5305
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
function describeAge(ageDays) {
|
|
5309
|
+
if (ageDays < 1) {
|
|
5310
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5311
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5312
|
+
}
|
|
5313
|
+
const days = Math.round(ageDays);
|
|
5314
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5315
|
+
}
|
|
5316
|
+
|
|
5254
5317
|
// src/core/release/release.decisions.ts
|
|
5255
5318
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5256
5319
|
import { join as join18 } from "node:path";
|
|
@@ -5554,6 +5617,69 @@ function evaluateShellCommand(args) {
|
|
|
5554
5617
|
return { kind: "allow" };
|
|
5555
5618
|
}
|
|
5556
5619
|
|
|
5620
|
+
// src/core/shim/shim.precedence.ts
|
|
5621
|
+
var LAUNCHER = "tlc-exec";
|
|
5622
|
+
function invocationText(entry) {
|
|
5623
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5624
|
+
}
|
|
5625
|
+
function coversHandler(settings, handler) {
|
|
5626
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5627
|
+
for (const matcher of matchers) {
|
|
5628
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5629
|
+
const text = invocationText(entry);
|
|
5630
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5631
|
+
return true;
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
return false;
|
|
5637
|
+
}
|
|
5638
|
+
function decideShim(userSettings, handler) {
|
|
5639
|
+
if (userSettings === null) {
|
|
5640
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5641
|
+
}
|
|
5642
|
+
return coversHandler(userSettings, handler) ? {
|
|
5643
|
+
run: false,
|
|
5644
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5645
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
// src/core/skill/skill.link.ts
|
|
5649
|
+
var SKILL_NAME = "harness-init";
|
|
5650
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5651
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5652
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5653
|
+
providerDir,
|
|
5654
|
+
source,
|
|
5655
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5656
|
+
}));
|
|
5657
|
+
}
|
|
5658
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5659
|
+
const resolved = probe.linkTarget(target);
|
|
5660
|
+
if (resolved === null) {
|
|
5661
|
+
return { state: "absent", target };
|
|
5662
|
+
}
|
|
5663
|
+
if (!probe.exists(resolved)) {
|
|
5664
|
+
return { state: "dangling", target, resolved };
|
|
5665
|
+
}
|
|
5666
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5667
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5668
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5669
|
+
}
|
|
5670
|
+
function linkHealthMessage(health) {
|
|
5671
|
+
switch (health.state) {
|
|
5672
|
+
case "ok":
|
|
5673
|
+
return `linked → ${health.resolved}`;
|
|
5674
|
+
case "dangling":
|
|
5675
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5676
|
+
case "outside-runtime":
|
|
5677
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5678
|
+
default:
|
|
5679
|
+
return "not linked — the provider cannot see the init skill";
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5557
5683
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5558
5684
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5559
5685
|
import { join as join21 } from "node:path";
|
|
@@ -6681,6 +6807,21 @@ var coreFacade = {
|
|
|
6681
6807
|
readHandoffFile,
|
|
6682
6808
|
readForeignSlices
|
|
6683
6809
|
},
|
|
6810
|
+
shim: {
|
|
6811
|
+
coversHandler,
|
|
6812
|
+
decideShim
|
|
6813
|
+
},
|
|
6814
|
+
pricing: {
|
|
6815
|
+
freshness,
|
|
6816
|
+
freshnessMessage,
|
|
6817
|
+
mayReplace,
|
|
6818
|
+
shouldRefetch
|
|
6819
|
+
},
|
|
6820
|
+
skill: {
|
|
6821
|
+
linkHealth,
|
|
6822
|
+
linkHealthMessage,
|
|
6823
|
+
skillLinks
|
|
6824
|
+
},
|
|
6684
6825
|
lesson: {
|
|
6685
6826
|
projectLessonsInjectable,
|
|
6686
6827
|
recordLessonFromFailure,
|
|
@@ -6894,6 +7035,9 @@ var NO_HUMAN_MODES = new Set(["bypassPermissions", "dontAsk"]);
|
|
|
6894
7035
|
// bin/write-user-hooks.mjs
|
|
6895
7036
|
if (false) {}
|
|
6896
7037
|
|
|
7038
|
+
// src/platform/pricing.ts
|
|
7039
|
+
var cache = new Map;
|
|
7040
|
+
|
|
6897
7041
|
// tools/doctor.ts
|
|
6898
7042
|
function plural(count, word) {
|
|
6899
7043
|
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
package/dist/obs-cli.mjs
CHANGED
|
@@ -272,20 +272,27 @@ function readJson(path) {
|
|
|
272
272
|
return null;
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
-
|
|
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 <
|
|
286
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
280
287
|
try {
|
|
281
|
-
|
|
288
|
+
openLock(lockPath);
|
|
282
289
|
acquired = true;
|
|
283
290
|
break;
|
|
284
291
|
} catch (error) {
|
|
285
|
-
if (
|
|
292
|
+
if (!isContention(error)) {
|
|
286
293
|
throw error;
|
|
287
294
|
}
|
|
288
|
-
await
|
|
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
|
|
@@ -5255,6 +5262,62 @@ function release(root, provider, session) {
|
|
|
5255
5262
|
deletePresenceRecord(root, provider, session);
|
|
5256
5263
|
}
|
|
5257
5264
|
|
|
5265
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5266
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5267
|
+
var MS_PER_DAY = 86400000;
|
|
5268
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5269
|
+
if (meta === null) {
|
|
5270
|
+
return { state: "absent" };
|
|
5271
|
+
}
|
|
5272
|
+
const stamp = meta.refreshedAt;
|
|
5273
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5274
|
+
return { state: "undated" };
|
|
5275
|
+
}
|
|
5276
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5277
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5278
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5279
|
+
}
|
|
5280
|
+
function shouldRefetch(state) {
|
|
5281
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5282
|
+
}
|
|
5283
|
+
function freshnessMessage(state, catalogue) {
|
|
5284
|
+
switch (state.state) {
|
|
5285
|
+
case "absent":
|
|
5286
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5287
|
+
case "undated":
|
|
5288
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5289
|
+
case "fresh":
|
|
5290
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5291
|
+
default:
|
|
5292
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5296
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5297
|
+
if (incomingCount === 0) {
|
|
5298
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5299
|
+
}
|
|
5300
|
+
if (existingCount === 0) {
|
|
5301
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5302
|
+
}
|
|
5303
|
+
if (incomingCount >= existingCount) {
|
|
5304
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5305
|
+
}
|
|
5306
|
+
const retained = incomingCount / existingCount;
|
|
5307
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5308
|
+
replace: false,
|
|
5309
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
function describeAge(ageDays) {
|
|
5313
|
+
if (ageDays < 1) {
|
|
5314
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5315
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5316
|
+
}
|
|
5317
|
+
const days = Math.round(ageDays);
|
|
5318
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5258
5321
|
// src/core/release/release.decisions.ts
|
|
5259
5322
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5260
5323
|
import { join as join18 } from "node:path";
|
|
@@ -5558,6 +5621,69 @@ function evaluateShellCommand(args) {
|
|
|
5558
5621
|
return { kind: "allow" };
|
|
5559
5622
|
}
|
|
5560
5623
|
|
|
5624
|
+
// src/core/shim/shim.precedence.ts
|
|
5625
|
+
var LAUNCHER = "tlc-exec";
|
|
5626
|
+
function invocationText(entry) {
|
|
5627
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5628
|
+
}
|
|
5629
|
+
function coversHandler(settings, handler) {
|
|
5630
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5631
|
+
for (const matcher of matchers) {
|
|
5632
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5633
|
+
const text = invocationText(entry);
|
|
5634
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5635
|
+
return true;
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
return false;
|
|
5641
|
+
}
|
|
5642
|
+
function decideShim(userSettings, handler) {
|
|
5643
|
+
if (userSettings === null) {
|
|
5644
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5645
|
+
}
|
|
5646
|
+
return coversHandler(userSettings, handler) ? {
|
|
5647
|
+
run: false,
|
|
5648
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5649
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5650
|
+
}
|
|
5651
|
+
|
|
5652
|
+
// src/core/skill/skill.link.ts
|
|
5653
|
+
var SKILL_NAME = "harness-init";
|
|
5654
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5655
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5656
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5657
|
+
providerDir,
|
|
5658
|
+
source,
|
|
5659
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5660
|
+
}));
|
|
5661
|
+
}
|
|
5662
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5663
|
+
const resolved = probe.linkTarget(target);
|
|
5664
|
+
if (resolved === null) {
|
|
5665
|
+
return { state: "absent", target };
|
|
5666
|
+
}
|
|
5667
|
+
if (!probe.exists(resolved)) {
|
|
5668
|
+
return { state: "dangling", target, resolved };
|
|
5669
|
+
}
|
|
5670
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5671
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5672
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5673
|
+
}
|
|
5674
|
+
function linkHealthMessage(health) {
|
|
5675
|
+
switch (health.state) {
|
|
5676
|
+
case "ok":
|
|
5677
|
+
return `linked → ${health.resolved}`;
|
|
5678
|
+
case "dangling":
|
|
5679
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5680
|
+
case "outside-runtime":
|
|
5681
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5682
|
+
default:
|
|
5683
|
+
return "not linked — the provider cannot see the init skill";
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5561
5687
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5562
5688
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5563
5689
|
import { join as join21 } from "node:path";
|
|
@@ -6685,6 +6811,21 @@ var coreFacade = {
|
|
|
6685
6811
|
readHandoffFile,
|
|
6686
6812
|
readForeignSlices
|
|
6687
6813
|
},
|
|
6814
|
+
shim: {
|
|
6815
|
+
coversHandler,
|
|
6816
|
+
decideShim
|
|
6817
|
+
},
|
|
6818
|
+
pricing: {
|
|
6819
|
+
freshness,
|
|
6820
|
+
freshnessMessage,
|
|
6821
|
+
mayReplace,
|
|
6822
|
+
shouldRefetch
|
|
6823
|
+
},
|
|
6824
|
+
skill: {
|
|
6825
|
+
linkHealth,
|
|
6826
|
+
linkHealthMessage,
|
|
6827
|
+
skillLinks
|
|
6828
|
+
},
|
|
6688
6829
|
lesson: {
|
|
6689
6830
|
projectLessonsInjectable,
|
|
6690
6831
|
recordLessonFromFailure,
|