@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/tlc-cli.mjs
CHANGED
|
@@ -4,17 +4,16 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
// bin/tlc-cli.ts
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import {
|
|
7
|
-
existsSync as
|
|
8
|
-
lstatSync,
|
|
9
|
-
mkdirSync as
|
|
7
|
+
existsSync as existsSync26,
|
|
8
|
+
lstatSync as lstatSync2,
|
|
9
|
+
mkdirSync as mkdirSync17,
|
|
10
10
|
readdirSync as readdirSync6,
|
|
11
11
|
readFileSync as readFileSync27,
|
|
12
12
|
realpathSync,
|
|
13
|
-
rmSync as
|
|
13
|
+
rmSync as rmSync5,
|
|
14
14
|
writeFileSync as writeFileSync15
|
|
15
15
|
} from "node:fs";
|
|
16
|
-
import {
|
|
17
|
-
import { delimiter, join as join26 } from "node:path";
|
|
16
|
+
import { delimiter, join as join27 } from "node:path";
|
|
18
17
|
|
|
19
18
|
// src/core/attest/attest.service.ts
|
|
20
19
|
import { createHash } from "node:crypto";
|
|
@@ -85,6 +84,17 @@ function bootDir(root) {
|
|
|
85
84
|
function policyBaselineDir(root) {
|
|
86
85
|
return join(projectStateDir(root), "policy-baseline");
|
|
87
86
|
}
|
|
87
|
+
function claudeConfigDir() {
|
|
88
|
+
const custom = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
89
|
+
return custom && custom.length > 0 ? custom : join(homedir(), ".claude");
|
|
90
|
+
}
|
|
91
|
+
function cursorConfigDir() {
|
|
92
|
+
const custom = process.env.CURSOR_CONFIG_DIR?.trim();
|
|
93
|
+
return custom && custom.length > 0 ? custom : join(homedir(), ".cursor");
|
|
94
|
+
}
|
|
95
|
+
function providerConfigDirs() {
|
|
96
|
+
return [cursorConfigDir(), claudeConfigDir()];
|
|
97
|
+
}
|
|
88
98
|
|
|
89
99
|
// src/core/attest/attest.service.ts
|
|
90
100
|
var CHAIN_ROOT = "genesis";
|
|
@@ -283,20 +293,27 @@ function readJson(path) {
|
|
|
283
293
|
return null;
|
|
284
294
|
}
|
|
285
295
|
}
|
|
286
|
-
|
|
296
|
+
function isContention(error) {
|
|
297
|
+
return errorCode(error) === "EEXIST" || isRetryableFsError(error);
|
|
298
|
+
}
|
|
299
|
+
async function withFileLock(lockPath, fn, options = {}) {
|
|
300
|
+
const {
|
|
301
|
+
openLock = (path) => closeSync(openSync(path, "wx")),
|
|
302
|
+
lockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
303
|
+
lockAttempts = 200
|
|
304
|
+
} = options;
|
|
287
305
|
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
288
|
-
const attempts = 200;
|
|
289
306
|
let acquired = false;
|
|
290
|
-
for (let attempt = 0;attempt <
|
|
307
|
+
for (let attempt = 0;attempt < lockAttempts; attempt++) {
|
|
291
308
|
try {
|
|
292
|
-
|
|
309
|
+
openLock(lockPath);
|
|
293
310
|
acquired = true;
|
|
294
311
|
break;
|
|
295
312
|
} catch (error) {
|
|
296
|
-
if (
|
|
313
|
+
if (!isContention(error)) {
|
|
297
314
|
throw error;
|
|
298
315
|
}
|
|
299
|
-
await
|
|
316
|
+
await lockSleep(nextDelay({ attempt, baseMs: 10, capMs: 200 }));
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
if (!acquired) {
|
|
@@ -311,14 +328,14 @@ async function withFileLock(lockPath, fn) {
|
|
|
311
328
|
}
|
|
312
329
|
}
|
|
313
330
|
async function updateJsonAtomic(path, mutator, options) {
|
|
314
|
-
const { lockPath, afterWrite, ...atomicOptions } = options;
|
|
331
|
+
const { lockPath, afterWrite, openLock, lockSleep, lockAttempts, ...atomicOptions } = options;
|
|
315
332
|
return withFileLock(lockPath, async () => {
|
|
316
333
|
const current = readJson(path);
|
|
317
334
|
const next = mutator(current);
|
|
318
335
|
await writeJsonAtomic(path, next, atomicOptions);
|
|
319
336
|
afterWrite?.(path);
|
|
320
337
|
return next;
|
|
321
|
-
});
|
|
338
|
+
}, { openLock, lockSleep, lockAttempts });
|
|
322
339
|
}
|
|
323
340
|
|
|
324
341
|
// src/core/capability/capability.store.ts
|
|
@@ -5266,6 +5283,62 @@ function release(root, provider, session) {
|
|
|
5266
5283
|
deletePresenceRecord(root, provider, session);
|
|
5267
5284
|
}
|
|
5268
5285
|
|
|
5286
|
+
// src/core/pricing/pricing.freshness.ts
|
|
5287
|
+
var DEFAULT_TTL_DAYS = 7;
|
|
5288
|
+
var MS_PER_DAY = 86400000;
|
|
5289
|
+
function freshness(meta, now, ttlDays = DEFAULT_TTL_DAYS) {
|
|
5290
|
+
if (meta === null) {
|
|
5291
|
+
return { state: "absent" };
|
|
5292
|
+
}
|
|
5293
|
+
const stamp = meta.refreshedAt;
|
|
5294
|
+
if (stamp === undefined || Number.isNaN(Date.parse(stamp))) {
|
|
5295
|
+
return { state: "undated" };
|
|
5296
|
+
}
|
|
5297
|
+
const ageMs = now.getTime() - Date.parse(stamp);
|
|
5298
|
+
const ageDays = Math.max(0, ageMs / MS_PER_DAY);
|
|
5299
|
+
return ageDays > ttlDays ? { state: "stale", ageDays, refreshedAt: stamp } : { state: "fresh", ageDays, refreshedAt: stamp };
|
|
5300
|
+
}
|
|
5301
|
+
function shouldRefetch(state) {
|
|
5302
|
+
return state.state === "absent" || state.state === "undated" || state.state === "stale";
|
|
5303
|
+
}
|
|
5304
|
+
function freshnessMessage(state, catalogue) {
|
|
5305
|
+
switch (state.state) {
|
|
5306
|
+
case "absent":
|
|
5307
|
+
return `${catalogue}: not on this machine — run \`tlc harness prices refresh\``;
|
|
5308
|
+
case "undated":
|
|
5309
|
+
return `${catalogue}: present but carries no date — it will be refetched`;
|
|
5310
|
+
case "fresh":
|
|
5311
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old`;
|
|
5312
|
+
default:
|
|
5313
|
+
return `${catalogue}: ${describeAge(state.ageDays)} old — run \`tlc harness prices refresh\``;
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
var MIN_RETAINED_RATIO = 0.5;
|
|
5317
|
+
function mayReplace(existingCount, incomingCount, minRatio = MIN_RETAINED_RATIO) {
|
|
5318
|
+
if (incomingCount === 0) {
|
|
5319
|
+
return { replace: false, reason: "parsed no entries at all — the upstream format has changed" };
|
|
5320
|
+
}
|
|
5321
|
+
if (existingCount === 0) {
|
|
5322
|
+
return { replace: true, reason: `first catalogue, ${incomingCount} entries` };
|
|
5323
|
+
}
|
|
5324
|
+
if (incomingCount >= existingCount) {
|
|
5325
|
+
return { replace: true, reason: `${existingCount} → ${incomingCount} entries` };
|
|
5326
|
+
}
|
|
5327
|
+
const retained = incomingCount / existingCount;
|
|
5328
|
+
return retained >= minRatio ? { replace: true, reason: `${existingCount} → ${incomingCount} entries` } : {
|
|
5329
|
+
replace: false,
|
|
5330
|
+
reason: `would drop from ${existingCount} to ${incomingCount} entries, keeping the existing catalogue — the upstream format has probably changed`
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5333
|
+
function describeAge(ageDays) {
|
|
5334
|
+
if (ageDays < 1) {
|
|
5335
|
+
const hours = Math.max(1, Math.round(ageDays * 24));
|
|
5336
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
5337
|
+
}
|
|
5338
|
+
const days = Math.round(ageDays);
|
|
5339
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5269
5342
|
// src/core/release/release.decisions.ts
|
|
5270
5343
|
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "node:fs";
|
|
5271
5344
|
import { join as join18 } from "node:path";
|
|
@@ -5569,6 +5642,69 @@ function evaluateShellCommand(args) {
|
|
|
5569
5642
|
return { kind: "allow" };
|
|
5570
5643
|
}
|
|
5571
5644
|
|
|
5645
|
+
// src/core/shim/shim.precedence.ts
|
|
5646
|
+
var LAUNCHER = "tlc-exec";
|
|
5647
|
+
function invocationText(entry) {
|
|
5648
|
+
return [entry.command ?? "", ...entry.args ?? []].join(" ");
|
|
5649
|
+
}
|
|
5650
|
+
function coversHandler(settings, handler) {
|
|
5651
|
+
for (const matchers of Object.values(settings.hooks ?? {})) {
|
|
5652
|
+
for (const matcher of matchers) {
|
|
5653
|
+
for (const entry of matcher.hooks ?? []) {
|
|
5654
|
+
const text = invocationText(entry);
|
|
5655
|
+
if (text.includes(LAUNCHER) && new RegExp(`(^|\\s)${handler}(\\s|$)`).test(text)) {
|
|
5656
|
+
return true;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
return false;
|
|
5662
|
+
}
|
|
5663
|
+
function decideShim(userSettings, handler) {
|
|
5664
|
+
if (userSettings === null) {
|
|
5665
|
+
return { run: true, reason: "no user-level settings — this shim is the only hook for this event" };
|
|
5666
|
+
}
|
|
5667
|
+
return coversHandler(userSettings, handler) ? {
|
|
5668
|
+
run: false,
|
|
5669
|
+
reason: `a user-level hook already runs ${handler} — standing down to avoid a second run`
|
|
5670
|
+
} : { run: true, reason: `no user-level hook runs ${handler}` };
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5673
|
+
// src/core/skill/skill.link.ts
|
|
5674
|
+
var SKILL_NAME = "harness-init";
|
|
5675
|
+
function skillLinks(runtimeHome2, providerDirs, present) {
|
|
5676
|
+
const source = `${runtimeHome2}/skills/${SKILL_NAME}`;
|
|
5677
|
+
return providerDirs.filter((dir) => present(dir)).map((providerDir) => ({
|
|
5678
|
+
providerDir,
|
|
5679
|
+
source,
|
|
5680
|
+
target: `${providerDir}/skills/${SKILL_NAME}`
|
|
5681
|
+
}));
|
|
5682
|
+
}
|
|
5683
|
+
function linkHealth(target, runtimeHome2, probe) {
|
|
5684
|
+
const resolved = probe.linkTarget(target);
|
|
5685
|
+
if (resolved === null) {
|
|
5686
|
+
return { state: "absent", target };
|
|
5687
|
+
}
|
|
5688
|
+
if (!probe.exists(resolved)) {
|
|
5689
|
+
return { state: "dangling", target, resolved };
|
|
5690
|
+
}
|
|
5691
|
+
const resolveHome = probe.realpath ?? ((path) => path);
|
|
5692
|
+
const home = resolveHome(runtimeHome2).replace(/\/+$/, "");
|
|
5693
|
+
return resolved === home || resolved.startsWith(`${home}/`) ? { state: "ok", target, resolved } : { state: "outside-runtime", target, resolved };
|
|
5694
|
+
}
|
|
5695
|
+
function linkHealthMessage(health) {
|
|
5696
|
+
switch (health.state) {
|
|
5697
|
+
case "ok":
|
|
5698
|
+
return `linked → ${health.resolved}`;
|
|
5699
|
+
case "dangling":
|
|
5700
|
+
return `points at ${health.resolved}, which does not exist — re-run \`tlc harness install\``;
|
|
5701
|
+
case "outside-runtime":
|
|
5702
|
+
return `points at ${health.resolved}, outside the runtime — it will break when that path goes`;
|
|
5703
|
+
default:
|
|
5704
|
+
return "not linked — the provider cannot see the init skill";
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5572
5708
|
// src/core/stagnation/stagnation.resolution.ts
|
|
5573
5709
|
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5574
5710
|
import { join as join21 } from "node:path";
|
|
@@ -6696,6 +6832,21 @@ var coreFacade = {
|
|
|
6696
6832
|
readHandoffFile,
|
|
6697
6833
|
readForeignSlices
|
|
6698
6834
|
},
|
|
6835
|
+
shim: {
|
|
6836
|
+
coversHandler,
|
|
6837
|
+
decideShim
|
|
6838
|
+
},
|
|
6839
|
+
pricing: {
|
|
6840
|
+
freshness,
|
|
6841
|
+
freshnessMessage,
|
|
6842
|
+
mayReplace,
|
|
6843
|
+
shouldRefetch
|
|
6844
|
+
},
|
|
6845
|
+
skill: {
|
|
6846
|
+
linkHealth,
|
|
6847
|
+
linkHealthMessage,
|
|
6848
|
+
skillLinks
|
|
6849
|
+
},
|
|
6699
6850
|
lesson: {
|
|
6700
6851
|
projectLessonsInjectable,
|
|
6701
6852
|
recordLessonFromFailure,
|
|
@@ -6902,6 +7053,43 @@ function unknownFlags(args) {
|
|
|
6902
7053
|
return args.filter((arg) => arg.startsWith("--"));
|
|
6903
7054
|
}
|
|
6904
7055
|
|
|
7056
|
+
// src/platform/links.ts
|
|
7057
|
+
import { copyFileSync, existsSync as existsSync25, lstatSync, mkdirSync as mkdirSync16, rmSync as rmSync4, symlinkSync } from "node:fs";
|
|
7058
|
+
import { dirname as dirname7, join as join26 } from "node:path";
|
|
7059
|
+
var LINK_TYPE = "junction";
|
|
7060
|
+
function linkDir(source, target) {
|
|
7061
|
+
let replaced = false;
|
|
7062
|
+
if (isLink(target)) {
|
|
7063
|
+
rmSync4(target, { recursive: true, force: true });
|
|
7064
|
+
replaced = true;
|
|
7065
|
+
} else if (existsSync25(target)) {
|
|
7066
|
+
return {
|
|
7067
|
+
kind: "refused",
|
|
7068
|
+
target,
|
|
7069
|
+
reason: `${target} exists and is not a link — move it aside and re-run`
|
|
7070
|
+
};
|
|
7071
|
+
}
|
|
7072
|
+
mkdirSync16(dirname7(target), { recursive: true });
|
|
7073
|
+
symlinkSync(source, target, LINK_TYPE);
|
|
7074
|
+
return { kind: replaced ? "relinked" : "linked", target, source };
|
|
7075
|
+
}
|
|
7076
|
+
function isLink(path) {
|
|
7077
|
+
try {
|
|
7078
|
+
return lstatSync(path).isSymbolicLink();
|
|
7079
|
+
} catch {
|
|
7080
|
+
return false;
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
7083
|
+
function seedConfig(dest) {
|
|
7084
|
+
const path = join26(dest, "config.json");
|
|
7085
|
+
const example = join26(dest, "config.example.json");
|
|
7086
|
+
if (existsSync25(path) || !existsSync25(example)) {
|
|
7087
|
+
return { seeded: false, path };
|
|
7088
|
+
}
|
|
7089
|
+
copyFileSync(example, path);
|
|
7090
|
+
return { seeded: true, path };
|
|
7091
|
+
}
|
|
7092
|
+
|
|
6905
7093
|
// bin/tlc-cli.ts
|
|
6906
7094
|
class UsageError extends Error {
|
|
6907
7095
|
}
|
|
@@ -6909,22 +7097,22 @@ function resolveProjectRoot() {
|
|
|
6909
7097
|
return process.env.TLC_PROJECT_DIR ?? process.cwd();
|
|
6910
7098
|
}
|
|
6911
7099
|
function modeFilePath(root) {
|
|
6912
|
-
return
|
|
7100
|
+
return join27(projectStateDir(root), "harness-mode");
|
|
6913
7101
|
}
|
|
6914
7102
|
function grindFlagPath(root) {
|
|
6915
|
-
return
|
|
7103
|
+
return join27(flagsDir(root), "grind-on");
|
|
6916
7104
|
}
|
|
6917
7105
|
function skipFlagPath(root) {
|
|
6918
|
-
return
|
|
7106
|
+
return join27(flagsDir(root), "skip-verify");
|
|
6919
7107
|
}
|
|
6920
7108
|
function focusFlagPath(root) {
|
|
6921
|
-
return
|
|
7109
|
+
return join27(flagsDir(root), "focus");
|
|
6922
7110
|
}
|
|
6923
7111
|
function pairedFlagPath(root) {
|
|
6924
|
-
return
|
|
7112
|
+
return join27(flagsDir(root), "paired");
|
|
6925
7113
|
}
|
|
6926
7114
|
function ensureFlagsDir(root) {
|
|
6927
|
-
|
|
7115
|
+
mkdirSync17(flagsDir(root), { recursive: true });
|
|
6928
7116
|
}
|
|
6929
7117
|
function readMode(root) {
|
|
6930
7118
|
return coreFacade.policy.loadPolicy(root).mode;
|
|
@@ -6933,7 +7121,7 @@ function grindOn(root) {
|
|
|
6933
7121
|
return coreFacade.policy.loadPolicy(root).grind.enabled;
|
|
6934
7122
|
}
|
|
6935
7123
|
function gatesPaused(root) {
|
|
6936
|
-
return
|
|
7124
|
+
return existsSync26(skipFlagPath(root));
|
|
6937
7125
|
}
|
|
6938
7126
|
function acceptedModes() {
|
|
6939
7127
|
return coreFacade.policy.OPERATOR_MODES.join(" | ");
|
|
@@ -6995,8 +7183,8 @@ function setGrind(root, on) {
|
|
|
6995
7183
|
coreFacade.policy.refreshPolicyBaselines(root);
|
|
6996
7184
|
return "grind ON — stop hook will lint/test and auto-retry on failure";
|
|
6997
7185
|
}
|
|
6998
|
-
if (
|
|
6999
|
-
|
|
7186
|
+
if (existsSync26(path)) {
|
|
7187
|
+
rmSync5(path);
|
|
7000
7188
|
}
|
|
7001
7189
|
coreFacade.policy.refreshPolicyBaselines(root);
|
|
7002
7190
|
return "grind OFF — no auto fix loops";
|
|
@@ -7009,8 +7197,8 @@ function setPaused(root, on) {
|
|
|
7009
7197
|
coreFacade.policy.refreshPolicyBaselines(root);
|
|
7010
7198
|
return "gates PAUSED — stop checks disabled until `tlc harness resume`";
|
|
7011
7199
|
}
|
|
7012
|
-
if (
|
|
7013
|
-
|
|
7200
|
+
if (existsSync26(path)) {
|
|
7201
|
+
rmSync5(path);
|
|
7014
7202
|
}
|
|
7015
7203
|
coreFacade.policy.refreshPolicyBaselines(root);
|
|
7016
7204
|
return "gates ACTIVE again";
|
|
@@ -7232,30 +7420,30 @@ function classifyRuntimePath(dest, probe) {
|
|
|
7232
7420
|
if (!probe.exists(dest)) {
|
|
7233
7421
|
return "absent";
|
|
7234
7422
|
}
|
|
7235
|
-
if (probe.exists(
|
|
7423
|
+
if (probe.exists(join27(dest, ".git"))) {
|
|
7236
7424
|
return "managed";
|
|
7237
7425
|
}
|
|
7238
|
-
return probe.exists(
|
|
7426
|
+
return probe.exists(join27(dest, NPM_MARKER)) ? "npm" : "unmanaged";
|
|
7239
7427
|
}
|
|
7240
7428
|
function runtimePathKind(dest) {
|
|
7241
7429
|
return classifyRuntimePath(dest, {
|
|
7242
7430
|
isSymlink: (path) => {
|
|
7243
7431
|
try {
|
|
7244
|
-
return
|
|
7432
|
+
return lstatSync2(path).isSymbolicLink();
|
|
7245
7433
|
} catch {
|
|
7246
7434
|
return false;
|
|
7247
7435
|
}
|
|
7248
7436
|
},
|
|
7249
|
-
exists:
|
|
7437
|
+
exists: existsSync26
|
|
7250
7438
|
});
|
|
7251
7439
|
}
|
|
7252
7440
|
function missingBundles(dest) {
|
|
7253
|
-
const entrypoints =
|
|
7254
|
-
if (!
|
|
7441
|
+
const entrypoints = join27(dest, "src", "entrypoints");
|
|
7442
|
+
if (!existsSync26(entrypoints)) {
|
|
7255
7443
|
return [];
|
|
7256
7444
|
}
|
|
7257
7445
|
const expected = readdirSync6(entrypoints).filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")).map((name) => `${name.slice(0, -3)}.mjs`);
|
|
7258
|
-
return expected.filter((bundle) => !
|
|
7446
|
+
return expected.filter((bundle) => !existsSync26(join27(dest, "dist", bundle)));
|
|
7259
7447
|
}
|
|
7260
7448
|
function linkedRuntimeMessage(dest, target) {
|
|
7261
7449
|
return [
|
|
@@ -7303,7 +7491,7 @@ function resetFailureMessage(dest, mergeRef, gitOutput) {
|
|
|
7303
7491
|
`);
|
|
7304
7492
|
}
|
|
7305
7493
|
function runtimeRevision(dest) {
|
|
7306
|
-
if (!
|
|
7494
|
+
if (!existsSync26(join27(dest, ".git"))) {
|
|
7307
7495
|
return { revision: null, date: null };
|
|
7308
7496
|
}
|
|
7309
7497
|
const read = (args) => {
|
|
@@ -7346,7 +7534,7 @@ function versionText(root, style = PLAIN) {
|
|
|
7346
7534
|
return render(versionScreen(root), style);
|
|
7347
7535
|
}
|
|
7348
7536
|
function pendingUpdate(dest, mergeRef) {
|
|
7349
|
-
if (!
|
|
7537
|
+
if (!existsSync26(join27(dest, ".git"))) {
|
|
7350
7538
|
return { ok: false, reason: "the runtime path is not a git checkout", commits: 0, decisions: [] };
|
|
7351
7539
|
}
|
|
7352
7540
|
const fetch = spawnSync("git", ["-C", dest, "fetch", "origin"], { stdio: "inherit", env: process.env });
|
|
@@ -7397,17 +7585,17 @@ var GATE_FIELDS = {
|
|
|
7397
7585
|
"test-command": "test",
|
|
7398
7586
|
"lint-command": "lint"
|
|
7399
7587
|
};
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
const candidates = (base) =>
|
|
7588
|
+
var EXECUTABLE_EXTENSIONS = ["", ".exe", ".cmd", ".bat", ".ps1"];
|
|
7589
|
+
function resolveExecutable(name, env = process.env) {
|
|
7590
|
+
const candidates = (base) => EXECUTABLE_EXTENSIONS.map((ext) => `${base}${ext}`);
|
|
7403
7591
|
if (name.includes("/") || name.includes("\\")) {
|
|
7404
|
-
return candidates(name).find((candidate) =>
|
|
7592
|
+
return candidates(name).find((candidate) => existsSync26(candidate)) ?? null;
|
|
7405
7593
|
}
|
|
7406
7594
|
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
7407
7595
|
if (!dir) {
|
|
7408
7596
|
continue;
|
|
7409
7597
|
}
|
|
7410
|
-
const found = candidates(
|
|
7598
|
+
const found = candidates(join27(dir, name)).find((candidate) => existsSync26(candidate));
|
|
7411
7599
|
if (found) {
|
|
7412
7600
|
return found;
|
|
7413
7601
|
}
|
|
@@ -7426,11 +7614,11 @@ function setGateCommand(root, field, argv, interactive) {
|
|
|
7426
7614
|
throw new UsageError(`\`${binary}\` was not found on PATH, and a gate command that cannot run is a config fault ([/decisions/ad-021.md](/decisions/ad-021.md)).`);
|
|
7427
7615
|
}
|
|
7428
7616
|
const path = projectConfigPath(root);
|
|
7429
|
-
const parsed =
|
|
7617
|
+
const parsed = existsSync26(path) ? JSON.parse(readFileSync27(path, "utf8")) : {};
|
|
7430
7618
|
const grind = { ...parsed.grind ?? {} };
|
|
7431
7619
|
grind[field === "test" ? "testCommand" : "lintCommand"] = argv;
|
|
7432
7620
|
parsed.grind = grind;
|
|
7433
|
-
|
|
7621
|
+
mkdirSync17(join27(root, ".tlc", "harness"), { recursive: true });
|
|
7434
7622
|
writeFileSync15(path, `${JSON.stringify(parsed, null, 2)}
|
|
7435
7623
|
`, "utf8");
|
|
7436
7624
|
coreFacade.policy.refreshPolicyBaselines(root);
|
|
@@ -7486,23 +7674,25 @@ function helpText(style = PLAIN) {
|
|
|
7486
7674
|
}
|
|
7487
7675
|
function pricesHelpScreen() {
|
|
7488
7676
|
return {
|
|
7489
|
-
title: "
|
|
7677
|
+
title: "prices",
|
|
7490
7678
|
sections: [
|
|
7491
7679
|
{
|
|
7492
|
-
lines: ` tlc harness prices refresh [all|cursor|litellm]
|
|
7680
|
+
lines: ` tlc harness prices refresh [all|cursor|litellm] [--if-stale]
|
|
7493
7681
|
tlc harness prices lookup <model-id>
|
|
7494
7682
|
|
|
7495
|
-
refresh / refresh all
|
|
7496
|
-
refresh cursor
|
|
7497
|
-
refresh litellm
|
|
7683
|
+
refresh / refresh all both planes of model-prices.json
|
|
7684
|
+
refresh cursor the provider's own rates
|
|
7685
|
+
refresh litellm the vendors' list prices
|
|
7686
|
+
--if-stale fetch only past the 7-day TTL
|
|
7498
7687
|
lookup <model-id> catalog key, pool, USD for 1M in + 1M out
|
|
7499
7688
|
|
|
7500
|
-
|
|
7689
|
+
Catalogue: <runtime home>/model-prices.json — fetched per machine, never versioned
|
|
7690
|
+
Overrides: <runtime home>/model-prices.local.json — yours, hand-written
|
|
7501
7691
|
Documentation: tlc harness help prices`.split(`
|
|
7502
7692
|
`)
|
|
7503
7693
|
}
|
|
7504
7694
|
],
|
|
7505
|
-
footer: "resolution:
|
|
7695
|
+
footer: "resolution: your overrides → the asking provider's plane → the vendor plane → null"
|
|
7506
7696
|
};
|
|
7507
7697
|
}
|
|
7508
7698
|
function pricesHelpText(style = PLAIN) {
|
|
@@ -7516,11 +7706,37 @@ function resolveHarnessRoot() {
|
|
|
7516
7706
|
return home;
|
|
7517
7707
|
}
|
|
7518
7708
|
}
|
|
7709
|
+
function wireRuntime(dest, home) {
|
|
7710
|
+
const lines = [];
|
|
7711
|
+
const seeded = seedConfig(dest);
|
|
7712
|
+
if (seeded.seeded) {
|
|
7713
|
+
lines.push(`config seeded → ${seeded.path}`);
|
|
7714
|
+
}
|
|
7715
|
+
if (!existsSync26(join27(dest, "skills", "harness-init"))) {
|
|
7716
|
+
return { lines, missingSkill: true };
|
|
7717
|
+
}
|
|
7718
|
+
const links = coreFacade.skill.skillLinks(dest, providerConfigDirs(), existsSync26);
|
|
7719
|
+
if (links.length === 0) {
|
|
7720
|
+
lines.push("no provider config dir found — skill not linked");
|
|
7721
|
+
}
|
|
7722
|
+
for (const link of links) {
|
|
7723
|
+
const outcome = linkDir(link.source, link.target);
|
|
7724
|
+
lines.push(outcome.kind === "refused" ? `skill not linked — ${outcome.reason}` : `skill → ${outcome.target}`);
|
|
7725
|
+
}
|
|
7726
|
+
const hooks = spawnSync(process.execPath, [join27(dest, "bin", "write-user-hooks.mjs")], {
|
|
7727
|
+
stdio: "inherit",
|
|
7728
|
+
env: { ...process.env, TLC_HOME: home }
|
|
7729
|
+
});
|
|
7730
|
+
if ((hooks.status ?? 1) !== 0) {
|
|
7731
|
+
lines.push("hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
|
|
7732
|
+
}
|
|
7733
|
+
return { lines, missingSkill: false };
|
|
7734
|
+
}
|
|
7519
7735
|
function execBinPath() {
|
|
7520
|
-
return
|
|
7736
|
+
return join27(resolveHarnessRoot(), "bin", "tlc-exec.mjs");
|
|
7521
7737
|
}
|
|
7522
7738
|
function buildBinPath() {
|
|
7523
|
-
return
|
|
7739
|
+
return join27(resolveHarnessRoot(), "bin", "tlc-build.mjs");
|
|
7524
7740
|
}
|
|
7525
7741
|
function route(args) {
|
|
7526
7742
|
const cmd = (args[0] ?? "status").toLowerCase();
|
|
@@ -7726,7 +7942,7 @@ function runUpdate(root) {
|
|
|
7726
7942
|
const revisionBefore = runtimeRevision(dest).revision;
|
|
7727
7943
|
const home = runtimeHome();
|
|
7728
7944
|
console.log(`update: runtime → ${dest}`);
|
|
7729
|
-
if (!
|
|
7945
|
+
if (!existsSync26(join27(dest, "bin", "tlc-exec.mjs"))) {
|
|
7730
7946
|
console.error(`update: missing install at ${home}`);
|
|
7731
7947
|
console.error(`update: install once with \`npm i -g ${NPM_PACKAGE}\`, then \`tlc harness install\`, then retry.`);
|
|
7732
7948
|
process.exit(1);
|
|
@@ -7738,13 +7954,16 @@ function runUpdate(root) {
|
|
|
7738
7954
|
const bump = spawnSync("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
|
|
7739
7955
|
stdio: "inherit",
|
|
7740
7956
|
env: process.env,
|
|
7741
|
-
shell:
|
|
7957
|
+
shell: true
|
|
7742
7958
|
});
|
|
7743
7959
|
if ((bump.status ?? 1) !== 0) {
|
|
7744
7960
|
console.error(npmUpdateFailureMessage());
|
|
7745
7961
|
process.exit(bump.status ?? 1);
|
|
7746
7962
|
}
|
|
7747
|
-
const sync = spawnSync(execBinPath(),
|
|
7963
|
+
const sync = spawnSync(process.execPath, [execBinPath(), "install-runtime"], {
|
|
7964
|
+
stdio: "inherit",
|
|
7965
|
+
env: process.env
|
|
7966
|
+
});
|
|
7748
7967
|
if ((sync.status ?? 1) !== 0) {
|
|
7749
7968
|
process.exit(sync.status ?? 1);
|
|
7750
7969
|
}
|
|
@@ -7771,52 +7990,32 @@ function runUpdate(root) {
|
|
|
7771
7990
|
const after = runtimeRevision(dest).revision;
|
|
7772
7991
|
console.log(revisionBefore === after ? `update: runtime already at ${after ?? "unknown"} — nothing to move` : `update: runtime ${revisionBefore ?? "unknown"} → ${after ?? "unknown"}`);
|
|
7773
7992
|
}
|
|
7774
|
-
const
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
env: { ...process.env, TLC_HOME: home },
|
|
7782
|
-
cwd: dest
|
|
7783
|
-
});
|
|
7784
|
-
if ((r.status ?? 1) !== 0) {
|
|
7785
|
-
process.exit(r.status ?? 1);
|
|
7786
|
-
}
|
|
7787
|
-
} else {
|
|
7788
|
-
const tlcBin = join26(dest, "bin", "tlc");
|
|
7789
|
-
const skillSrc = join26(dest, "skills", "harness-init");
|
|
7790
|
-
const skillDest = join26(home, "..", "skills", "harness-init");
|
|
7791
|
-
spawnSync("ln", ["-sfn", tlcBin, join26(binDir, "tlc")], { stdio: "inherit" });
|
|
7792
|
-
if (!existsSync25(skillSrc)) {
|
|
7793
|
-
console.error(`update: missing skill at ${skillSrc}`);
|
|
7794
|
-
process.exit(1);
|
|
7795
|
-
}
|
|
7796
|
-
spawnSync("ln", ["-sfn", skillSrc, skillDest], { stdio: "inherit" });
|
|
7797
|
-
console.log(`update: skill → ${skillDest}`);
|
|
7798
|
-
const hooks = spawnSync(process.execPath, [join26(dest, "bin", "write-user-hooks.mjs")], {
|
|
7799
|
-
stdio: "inherit",
|
|
7800
|
-
env: { ...process.env, TLC_HOME: home }
|
|
7801
|
-
});
|
|
7802
|
-
if ((hooks.status ?? 1) !== 0) {
|
|
7803
|
-
console.log("update: hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
|
|
7804
|
-
}
|
|
7993
|
+
const wired = wireRuntime(dest, home);
|
|
7994
|
+
for (const line of wired.lines) {
|
|
7995
|
+
console.log(`update: ${line}`);
|
|
7996
|
+
}
|
|
7997
|
+
if (wired.missingSkill) {
|
|
7998
|
+
console.error(`update: missing skill at ${join27(dest, "skills", "harness-init")}`);
|
|
7999
|
+
process.exit(1);
|
|
7805
8000
|
}
|
|
7806
8001
|
const missing = missingBundles(dest);
|
|
7807
8002
|
if (missing.length === 0) {
|
|
7808
8003
|
console.log("update: dist/ complete — no rebuild, so the runtime path stays clean");
|
|
7809
|
-
} else if (
|
|
8004
|
+
} else if (existsSync26(buildBinPath())) {
|
|
7810
8005
|
console.log(`update: ${missing.length} bundle(s) missing — building`);
|
|
7811
|
-
const build = spawnSync(buildBinPath()
|
|
8006
|
+
const build = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
|
|
7812
8007
|
if ((build.status ?? 1) !== 0) {
|
|
7813
8008
|
console.log(`update: build failed — ${missing.length} bundle(s) still missing from dist/`);
|
|
7814
8009
|
}
|
|
7815
8010
|
}
|
|
7816
8011
|
announceNewCapabilities(root, dest);
|
|
7817
8012
|
announceLandedDecisions(root, dest, revisionBefore);
|
|
8013
|
+
spawnSync(process.execPath, [execBinPath(), "refresh-model-prices", "all", "--if-stale"], {
|
|
8014
|
+
stdio: "inherit",
|
|
8015
|
+
env: { ...process.env, TLC_PROJECT_DIR: root }
|
|
8016
|
+
});
|
|
7818
8017
|
console.log("update: running doctor…");
|
|
7819
|
-
const doctor = spawnSync(execBinPath(),
|
|
8018
|
+
const doctor = spawnSync(process.execPath, [execBinPath(), "doctor"], {
|
|
7820
8019
|
stdio: "inherit",
|
|
7821
8020
|
env: { ...process.env, TLC_PROJECT_DIR: root }
|
|
7822
8021
|
});
|
|
@@ -7824,7 +8023,7 @@ function runUpdate(root) {
|
|
|
7824
8023
|
process.exit(doctor.status ?? 0);
|
|
7825
8024
|
}
|
|
7826
8025
|
function runEntry(entry, toolArgs, root) {
|
|
7827
|
-
const r = spawnSync(execBinPath(),
|
|
8026
|
+
const r = spawnSync(process.execPath, [execBinPath(), entry, ...toolArgs], {
|
|
7828
8027
|
stdio: "inherit",
|
|
7829
8028
|
env: { ...process.env, TLC_PROJECT_DIR: root }
|
|
7830
8029
|
});
|
|
@@ -7919,7 +8118,7 @@ function main(argv) {
|
|
|
7919
8118
|
console.log(helpText(createStyle()));
|
|
7920
8119
|
break;
|
|
7921
8120
|
case "build": {
|
|
7922
|
-
const r = spawnSync(buildBinPath()
|
|
8121
|
+
const r = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
|
|
7923
8122
|
process.exit(r.status ?? 1);
|
|
7924
8123
|
break;
|
|
7925
8124
|
}
|
|
@@ -8001,6 +8200,7 @@ if (__require.main == __require.module) {
|
|
|
8001
8200
|
main(process.argv.slice(2));
|
|
8002
8201
|
}
|
|
8003
8202
|
export {
|
|
8203
|
+
wireRuntime,
|
|
8004
8204
|
versionText,
|
|
8005
8205
|
versionScreen,
|
|
8006
8206
|
versionJson,
|