@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/stop.mjs
CHANGED
|
@@ -280,20 +280,27 @@ function readJson(path) {
|
|
|
280
280
|
return null;
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
function isContention(error) {
|
|
284
|
+
return errorCode(error) === "EEXIST" || isRetryableFsError(error);
|
|
285
|
+
}
|
|
286
|
+
async function withFileLock(lockPath, fn, options = {}) {
|
|
287
|
+
const {
|
|
288
|
+
openLock = (path) => closeSync(openSync(path, "wx")),
|
|
289
|
+
lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
290
|
+
lockAttempts = 200
|
|
291
|
+
} = options;
|
|
284
292
|
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
285
|
-
const attempts = 200;
|
|
286
293
|
let acquired = false;
|
|
287
|
-
for (let attempt = 0;attempt <
|
|
294
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
288
295
|
try {
|
|
289
|
-
|
|
296
|
+
openLock(lockPath);
|
|
290
297
|
acquired = true;
|
|
291
298
|
break;
|
|
292
299
|
} catch (error) {
|
|
293
|
-
if (
|
|
300
|
+
if (!isContention(error)) {
|
|
294
301
|
throw error;
|
|
295
302
|
}
|
|
296
|
-
await
|
|
303
|
+
await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
|
|
297
304
|
}
|
|
298
305
|
}
|
|
299
306
|
if (!acquired) {
|
|
@@ -308,14 +315,14 @@ async function withFileLock(lockPath, fn) {
|
|
|
308
315
|
}
|
|
309
316
|
}
|
|
310
317
|
async function updateJsonAtomic(path, mutator, options) {
|
|
311
|
-
const { lockPath, afterWrite, ...atomicOptions } = options;
|
|
318
|
+
const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
|
|
312
319
|
return withFileLock(lockPath, async () => {
|
|
313
320
|
const current = readJson(path);
|
|
314
321
|
const next = mutator(current);
|
|
315
322
|
await writeJsonAtomic(path, next, atomicOptions);
|
|
316
323
|
afterWrite?.(path);
|
|
317
324
|
return next;
|
|
318
|
-
});
|
|
325
|
+
}, { openLock, lockSleep, lockAttempts });
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
// src/core/capability/capability.store.ts
|
|
@@ -5328,6 +5335,62 @@ function release(root, provider, session) {
|
|
|
5328
5335
|
deletePresenceRecord(root, provider, session);
|
|
5329
5336
|
}
|
|
5330
5337
|
|
|
5338
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5339
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5340
|
+
var MS_PER_DAY = 86400000;
|
|
5341
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5342
|
+
if (meta === null) {
|
|
5343
|
+
return { state: "absent" };
|
|
5344
|
+
}
|
|
5345
|
+
const stamp = meta.refreshedAt;
|
|
5346
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5347
|
+
return { state: "undated" };
|
|
5348
|
+
}
|
|
5349
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5350
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5351
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5352
|
+
}
|
|
5353
|
+
function shouldRefetch(state) {
|
|
5354
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5355
|
+
}
|
|
5356
|
+
function freshnessMessage(state, catalogue) {
|
|
5357
|
+
switch (state.state) {
|
|
5358
|
+
case "absent":
|
|
5359
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5360
|
+
case "undated":
|
|
5361
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5362
|
+
case "fresh":
|
|
5363
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5364
|
+
default:
|
|
5365
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5369
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5370
|
+
if (incomingCount === 0) {
|
|
5371
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5372
|
+
}
|
|
5373
|
+
if (existingCount === 0) {
|
|
5374
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5375
|
+
}
|
|
5376
|
+
if (incomingCount >= existingCount) {
|
|
5377
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5378
|
+
}
|
|
5379
|
+
const retained = incomingCount / existingCount;
|
|
5380
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5381
|
+
replace: false,
|
|
5382
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5383
|
+
};
|
|
5384
|
+
}
|
|
5385
|
+
function describeAge(ageDays) {
|
|
5386
|
+
if (ageDays < 1) {
|
|
5387
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5388
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5389
|
+
}
|
|
5390
|
+
const days = Math.round(ageDays);
|
|
5391
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5331
5394
|
// src/core/release/release.decisions.ts
|
|
5332
5395
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5333
5396
|
import { join as join18 } from "node:path";
|
|
@@ -5631,6 +5694,69 @@ function evaluateShellCommand(args) {
|
|
|
5631
5694
|
return { kind: "allow" };
|
|
5632
5695
|
}
|
|
5633
5696
|
|
|
5697
|
+
// src/core/shim/shim.precedence.ts
|
|
5698
|
+
var LAUNCHER = "tlc-exec";
|
|
5699
|
+
function invocationText(entry) {
|
|
5700
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5701
|
+
}
|
|
5702
|
+
function coversHandler(settings, handler) {
|
|
5703
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5704
|
+
for (const matcher of matchers) {
|
|
5705
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5706
|
+
const text = invocationText(entry);
|
|
5707
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5708
|
+
return true;
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
return false;
|
|
5714
|
+
}
|
|
5715
|
+
function decideShim(userSettings, handler) {
|
|
5716
|
+
if (userSettings === null) {
|
|
5717
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5718
|
+
}
|
|
5719
|
+
return coversHandler(userSettings, handler) ? {
|
|
5720
|
+
run: false,
|
|
5721
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5722
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
// src/core/skill/skill.link.ts
|
|
5726
|
+
var SKILL_NAME = "harness-init";
|
|
5727
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5728
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5729
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5730
|
+
providerDir,
|
|
5731
|
+
source,
|
|
5732
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5733
|
+
}));
|
|
5734
|
+
}
|
|
5735
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5736
|
+
const resolved = probe.linkTarget(target);
|
|
5737
|
+
if (resolved === null) {
|
|
5738
|
+
return { state: "absent", target };
|
|
5739
|
+
}
|
|
5740
|
+
if (!probe.exists(resolved)) {
|
|
5741
|
+
return { state: "dangling", target, resolved };
|
|
5742
|
+
}
|
|
5743
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5744
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5745
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5746
|
+
}
|
|
5747
|
+
function linkHealthMessage(health) {
|
|
5748
|
+
switch (health.state) {
|
|
5749
|
+
case "ok":
|
|
5750
|
+
return `linked → ${health.resolved}`;
|
|
5751
|
+
case "dangling":
|
|
5752
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5753
|
+
case "outside-runtime":
|
|
5754
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5755
|
+
default:
|
|
5756
|
+
return "not linked — the provider cannot see the init skill";
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5634
5760
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5635
5761
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5636
5762
|
import { join as join21 } from "node:path";
|
|
@@ -6758,6 +6884,21 @@ var coreFacade = {
|
|
|
6758
6884
|
readHandoffFile,
|
|
6759
6885
|
readForeignSlices
|
|
6760
6886
|
},
|
|
6887
|
+
shim: {
|
|
6888
|
+
coversHandler,
|
|
6889
|
+
decideShim
|
|
6890
|
+
},
|
|
6891
|
+
pricing: {
|
|
6892
|
+
freshness,
|
|
6893
|
+
freshnessMessage,
|
|
6894
|
+
mayReplace,
|
|
6895
|
+
shouldRefetch
|
|
6896
|
+
},
|
|
6897
|
+
skill: {
|
|
6898
|
+
linkHealth,
|
|
6899
|
+
linkHealthMessage,
|
|
6900
|
+
skillLinks
|
|
6901
|
+
},
|
|
6761
6902
|
lesson: {
|
|
6762
6903
|
projectLessonsInjectable,
|
|
6763
6904
|
recordLessonFromFailure,
|
|
@@ -7703,14 +7844,9 @@ var ENTRY_SPECS2 = [
|
|
|
7703
7844
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7704
7845
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7705
7846
|
];
|
|
7706
|
-
function commandFor(runtime) {
|
|
7707
|
-
if (process.platform === "win32") {
|
|
7708
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7709
|
-
}
|
|
7710
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7711
|
-
}
|
|
7712
7847
|
function cursorWiring(runtime) {
|
|
7713
|
-
const
|
|
7848
|
+
const command = "node";
|
|
7849
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7714
7850
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7715
7851
|
hookEvent: spec.hookEvent,
|
|
7716
7852
|
handler: spec.handler,
|
package/dist/subagent-start.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
|
|
@@ -5266,6 +5273,62 @@ function release(root, provider, session) {
|
|
|
5266
5273
|
deletePresenceRecord(root, provider, session);
|
|
5267
5274
|
}
|
|
5268
5275
|
|
|
5276
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5277
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5278
|
+
var MS_PER_DAY = 86400000;
|
|
5279
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5280
|
+
if (meta === null) {
|
|
5281
|
+
return { state: "absent" };
|
|
5282
|
+
}
|
|
5283
|
+
const stamp = meta.refreshedAt;
|
|
5284
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5285
|
+
return { state: "undated" };
|
|
5286
|
+
}
|
|
5287
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5288
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5289
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5290
|
+
}
|
|
5291
|
+
function shouldRefetch(state) {
|
|
5292
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5293
|
+
}
|
|
5294
|
+
function freshnessMessage(state, catalogue) {
|
|
5295
|
+
switch (state.state) {
|
|
5296
|
+
case "absent":
|
|
5297
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5298
|
+
case "undated":
|
|
5299
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5300
|
+
case "fresh":
|
|
5301
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5302
|
+
default:
|
|
5303
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5307
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5308
|
+
if (incomingCount === 0) {
|
|
5309
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5310
|
+
}
|
|
5311
|
+
if (existingCount === 0) {
|
|
5312
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5313
|
+
}
|
|
5314
|
+
if (incomingCount >= existingCount) {
|
|
5315
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5316
|
+
}
|
|
5317
|
+
const retained = incomingCount / existingCount;
|
|
5318
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5319
|
+
replace: false,
|
|
5320
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
function describeAge(ageDays) {
|
|
5324
|
+
if (ageDays < 1) {
|
|
5325
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5326
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5327
|
+
}
|
|
5328
|
+
const days = Math.round(ageDays);
|
|
5329
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5269
5332
|
// src/core/release/release.decisions.ts
|
|
5270
5333
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5271
5334
|
import { join as join18 } from "node:path";
|
|
@@ -5569,6 +5632,69 @@ function evaluateShellCommand(args) {
|
|
|
5569
5632
|
return { kind: "allow" };
|
|
5570
5633
|
}
|
|
5571
5634
|
|
|
5635
|
+
// src/core/shim/shim.precedence.ts
|
|
5636
|
+
var LAUNCHER = "tlc-exec";
|
|
5637
|
+
function invocationText(entry) {
|
|
5638
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5639
|
+
}
|
|
5640
|
+
function coversHandler(settings, handler) {
|
|
5641
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5642
|
+
for (const matcher of matchers) {
|
|
5643
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5644
|
+
const text = invocationText(entry);
|
|
5645
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5646
|
+
return true;
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
return false;
|
|
5652
|
+
}
|
|
5653
|
+
function decideShim(userSettings, handler) {
|
|
5654
|
+
if (userSettings === null) {
|
|
5655
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5656
|
+
}
|
|
5657
|
+
return coversHandler(userSettings, handler) ? {
|
|
5658
|
+
run: false,
|
|
5659
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5660
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
// src/core/skill/skill.link.ts
|
|
5664
|
+
var SKILL_NAME = "harness-init";
|
|
5665
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5666
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5667
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5668
|
+
providerDir,
|
|
5669
|
+
source,
|
|
5670
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5671
|
+
}));
|
|
5672
|
+
}
|
|
5673
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5674
|
+
const resolved = probe.linkTarget(target);
|
|
5675
|
+
if (resolved === null) {
|
|
5676
|
+
return { state: "absent", target };
|
|
5677
|
+
}
|
|
5678
|
+
if (!probe.exists(resolved)) {
|
|
5679
|
+
return { state: "dangling", target, resolved };
|
|
5680
|
+
}
|
|
5681
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5682
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5683
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5684
|
+
}
|
|
5685
|
+
function linkHealthMessage(health) {
|
|
5686
|
+
switch (health.state) {
|
|
5687
|
+
case "ok":
|
|
5688
|
+
return `linked → ${health.resolved}`;
|
|
5689
|
+
case "dangling":
|
|
5690
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5691
|
+
case "outside-runtime":
|
|
5692
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5693
|
+
default:
|
|
5694
|
+
return "not linked — the provider cannot see the init skill";
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5572
5698
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5573
5699
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5574
5700
|
import { join as join21 } from "node:path";
|
|
@@ -6696,6 +6822,21 @@ var coreFacade = {
|
|
|
6696
6822
|
readHandoffFile,
|
|
6697
6823
|
readForeignSlices
|
|
6698
6824
|
},
|
|
6825
|
+
shim: {
|
|
6826
|
+
coversHandler,
|
|
6827
|
+
decideShim
|
|
6828
|
+
},
|
|
6829
|
+
pricing: {
|
|
6830
|
+
freshness,
|
|
6831
|
+
freshnessMessage,
|
|
6832
|
+
mayReplace,
|
|
6833
|
+
shouldRefetch
|
|
6834
|
+
},
|
|
6835
|
+
skill: {
|
|
6836
|
+
linkHealth,
|
|
6837
|
+
linkHealthMessage,
|
|
6838
|
+
skillLinks
|
|
6839
|
+
},
|
|
6699
6840
|
lesson: {
|
|
6700
6841
|
projectLessonsInjectable,
|
|
6701
6842
|
recordLessonFromFailure,
|
|
@@ -7641,14 +7782,9 @@ var ENTRY_SPECS2 = [
|
|
|
7641
7782
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7642
7783
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7643
7784
|
];
|
|
7644
|
-
function commandFor(runtime) {
|
|
7645
|
-
if (process.platform === "win32") {
|
|
7646
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7647
|
-
}
|
|
7648
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7649
|
-
}
|
|
7650
7785
|
function cursorWiring(runtime) {
|
|
7651
|
-
const
|
|
7786
|
+
const command = "node";
|
|
7787
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7652
7788
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7653
7789
|
hookEvent: spec.hookEvent,
|
|
7654
7790
|
handler: spec.handler,
|
package/dist/subagent-stop.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
|
|
@@ -5266,6 +5273,62 @@ function release(root, provider, session) {
|
|
|
5266
5273
|
deletePresenceRecord(root, provider, session);
|
|
5267
5274
|
}
|
|
5268
5275
|
|
|
5276
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5277
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5278
|
+
var MS_PER_DAY = 86400000;
|
|
5279
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5280
|
+
if (meta === null) {
|
|
5281
|
+
return { state: "absent" };
|
|
5282
|
+
}
|
|
5283
|
+
const stamp = meta.refreshedAt;
|
|
5284
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5285
|
+
return { state: "undated" };
|
|
5286
|
+
}
|
|
5287
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5288
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5289
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5290
|
+
}
|
|
5291
|
+
function shouldRefetch(state) {
|
|
5292
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5293
|
+
}
|
|
5294
|
+
function freshnessMessage(state, catalogue) {
|
|
5295
|
+
switch (state.state) {
|
|
5296
|
+
case "absent":
|
|
5297
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5298
|
+
case "undated":
|
|
5299
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5300
|
+
case "fresh":
|
|
5301
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5302
|
+
default:
|
|
5303
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5307
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5308
|
+
if (incomingCount === 0) {
|
|
5309
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5310
|
+
}
|
|
5311
|
+
if (existingCount === 0) {
|
|
5312
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5313
|
+
}
|
|
5314
|
+
if (incomingCount >= existingCount) {
|
|
5315
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5316
|
+
}
|
|
5317
|
+
const retained = incomingCount / existingCount;
|
|
5318
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5319
|
+
replace: false,
|
|
5320
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
function describeAge(ageDays) {
|
|
5324
|
+
if (ageDays < 1) {
|
|
5325
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5326
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5327
|
+
}
|
|
5328
|
+
const days = Math.round(ageDays);
|
|
5329
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5269
5332
|
// src/core/release/release.decisions.ts
|
|
5270
5333
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5271
5334
|
import { join as join18 } from "node:path";
|
|
@@ -5569,6 +5632,69 @@ function evaluateShellCommand(args) {
|
|
|
5569
5632
|
return { kind: "allow" };
|
|
5570
5633
|
}
|
|
5571
5634
|
|
|
5635
|
+
// src/core/shim/shim.precedence.ts
|
|
5636
|
+
var LAUNCHER = "tlc-exec";
|
|
5637
|
+
function invocationText(entry) {
|
|
5638
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5639
|
+
}
|
|
5640
|
+
function coversHandler(settings, handler) {
|
|
5641
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5642
|
+
for (const matcher of matchers) {
|
|
5643
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5644
|
+
const text = invocationText(entry);
|
|
5645
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5646
|
+
return true;
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
return false;
|
|
5652
|
+
}
|
|
5653
|
+
function decideShim(userSettings, handler) {
|
|
5654
|
+
if (userSettings === null) {
|
|
5655
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5656
|
+
}
|
|
5657
|
+
return coversHandler(userSettings, handler) ? {
|
|
5658
|
+
run: false,
|
|
5659
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5660
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
// src/core/skill/skill.link.ts
|
|
5664
|
+
var SKILL_NAME = "harness-init";
|
|
5665
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5666
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5667
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5668
|
+
providerDir,
|
|
5669
|
+
source,
|
|
5670
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5671
|
+
}));
|
|
5672
|
+
}
|
|
5673
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5674
|
+
const resolved = probe.linkTarget(target);
|
|
5675
|
+
if (resolved === null) {
|
|
5676
|
+
return { state: "absent", target };
|
|
5677
|
+
}
|
|
5678
|
+
if (!probe.exists(resolved)) {
|
|
5679
|
+
return { state: "dangling", target, resolved };
|
|
5680
|
+
}
|
|
5681
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5682
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5683
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5684
|
+
}
|
|
5685
|
+
function linkHealthMessage(health) {
|
|
5686
|
+
switch (health.state) {
|
|
5687
|
+
case "ok":
|
|
5688
|
+
return `linked → ${health.resolved}`;
|
|
5689
|
+
case "dangling":
|
|
5690
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5691
|
+
case "outside-runtime":
|
|
5692
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5693
|
+
default:
|
|
5694
|
+
return "not linked — the provider cannot see the init skill";
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5572
5698
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5573
5699
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5574
5700
|
import { join as join21 } from "node:path";
|
|
@@ -6696,6 +6822,21 @@ var coreFacade = {
|
|
|
6696
6822
|
readHandoffFile,
|
|
6697
6823
|
readForeignSlices
|
|
6698
6824
|
},
|
|
6825
|
+
shim: {
|
|
6826
|
+
coversHandler,
|
|
6827
|
+
decideShim
|
|
6828
|
+
},
|
|
6829
|
+
pricing: {
|
|
6830
|
+
freshness,
|
|
6831
|
+
freshnessMessage,
|
|
6832
|
+
mayReplace,
|
|
6833
|
+
shouldRefetch
|
|
6834
|
+
},
|
|
6835
|
+
skill: {
|
|
6836
|
+
linkHealth,
|
|
6837
|
+
linkHealthMessage,
|
|
6838
|
+
skillLinks
|
|
6839
|
+
},
|
|
6699
6840
|
lesson: {
|
|
6700
6841
|
projectLessonsInjectable,
|
|
6701
6842
|
recordLessonFromFailure,
|
|
@@ -7641,14 +7782,9 @@ var ENTRY_SPECS2 = [
|
|
|
7641
7782
|
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
7642
7783
|
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
7643
7784
|
];
|
|
7644
|
-
function commandFor(runtime) {
|
|
7645
|
-
if (process.platform === "win32") {
|
|
7646
|
-
return { command: "cmd", argsPrefix: ["/c", "node", runtime.launcherPath] };
|
|
7647
|
-
}
|
|
7648
|
-
return { command: "node", argsPrefix: [runtime.launcherPath] };
|
|
7649
|
-
}
|
|
7650
7785
|
function cursorWiring(runtime) {
|
|
7651
|
-
const
|
|
7786
|
+
const command = "node";
|
|
7787
|
+
const argsPrefix = [runtime.launcherPath];
|
|
7652
7788
|
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
7653
7789
|
hookEvent: spec.hookEvent,
|
|
7654
7790
|
handler: spec.handler,
|