@tech-leads-club/harness-toolkit 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tlc-build.mjs +15 -29
- package/bin/tlc-cli.ts +63 -2
- package/dist/compact-before.mjs +7961 -13
- package/dist/doctor.mjs +8480 -33
- package/dist/help-topic.mjs +14 -6
- package/dist/init-project.mjs +793 -36
- package/dist/install-runtime.mjs +7268 -17
- package/dist/lessons-cli.mjs +7044 -26
- package/dist/obs-cli.mjs +7037 -27
- package/dist/price-lookup.mjs +201 -11
- package/dist/prompt-submit.mjs +7975 -15
- package/dist/refresh-model-prices.mjs +7060 -25
- package/dist/response-after.mjs +7962 -12
- package/dist/run.mjs +7960 -10
- package/dist/session-end.mjs +8030 -15
- package/dist/session-start.mjs +8076 -21
- package/dist/shim.mjs +7024 -18
- package/dist/stop.mjs +8042 -29
- package/dist/subagent-start.mjs +7983 -13
- package/dist/subagent-stop.mjs +7962 -12
- package/dist/support.mjs +7168 -21
- package/dist/tlc-cli.mjs +8235 -65
- package/dist/tool-after.mjs +8177 -21
- package/dist/tool-before.mjs +7991 -16
- package/dist/tool-failure.mjs +7962 -15
- package/dist/uninstall-runtime.mjs +1008 -61
- package/docs/log.md +1 -1
- package/package.json +1 -1
- package/dist/chunks/compact-before-1e4qg1qt.mjs +0 -1185
- package/dist/chunks/compact-before-2hpbfxm5.mjs +0 -5782
- package/dist/chunks/compact-before-49j320yp.mjs +0 -1283
- package/dist/chunks/compact-before-4jrq0sqs.mjs +0 -61
- package/dist/chunks/compact-before-6w8n1vh1.mjs +0 -186
- package/dist/chunks/compact-before-7sdmwswh.mjs +0 -52
- package/dist/chunks/compact-before-beqpmqrm.mjs +0 -187
- package/dist/chunks/compact-before-j9y4jgn4.mjs +0 -845
- package/dist/chunks/compact-before-pk86tqx2.mjs +0 -118
- package/dist/chunks/compact-before-pkqk5v29.mjs +0 -137
- package/dist/chunks/compact-before-w1293m4n.mjs +0 -315
- package/dist/chunks/compact-before-wnnds45y.mjs +0 -26
- package/dist/chunks/compact-before-wt2c3nh4.mjs +0 -551
|
@@ -1,551 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isCursorWired
|
|
3
|
-
} from "./compact-before-7sdmwswh.mjs";
|
|
4
|
-
import {
|
|
5
|
-
NPM_PACKAGE,
|
|
6
|
-
runtimePathKind
|
|
7
|
-
} from "./compact-before-1e4qg1qt.mjs";
|
|
8
|
-
import {
|
|
9
|
-
emitJson,
|
|
10
|
-
takeJsonFlag
|
|
11
|
-
} from "./compact-before-wnnds45y.mjs";
|
|
12
|
-
import {
|
|
13
|
-
catalogueMeta,
|
|
14
|
-
planeMeta
|
|
15
|
-
} from "./compact-before-beqpmqrm.mjs";
|
|
16
|
-
import {
|
|
17
|
-
providers
|
|
18
|
-
} from "./compact-before-j9y4jgn4.mjs";
|
|
19
|
-
import {
|
|
20
|
-
cursorWiringProblems,
|
|
21
|
-
formatWiringProblems,
|
|
22
|
-
mergeClaudeSettings
|
|
23
|
-
} from "./compact-before-w1293m4n.mjs";
|
|
24
|
-
import {
|
|
25
|
-
coreFacade
|
|
26
|
-
} from "./compact-before-2hpbfxm5.mjs";
|
|
27
|
-
import {
|
|
28
|
-
PLAIN,
|
|
29
|
-
SYMBOLS,
|
|
30
|
-
createStyle
|
|
31
|
-
} from "./compact-before-49j320yp.mjs";
|
|
32
|
-
import {
|
|
33
|
-
__require,
|
|
34
|
-
projectConfigPath,
|
|
35
|
-
projectStateDir,
|
|
36
|
-
providerConfigDirs,
|
|
37
|
-
runtimeHome
|
|
38
|
-
} from "./compact-before-4jrq0sqs.mjs";
|
|
39
|
-
|
|
40
|
-
// tools/doctor.ts
|
|
41
|
-
import { spawnSync } from "node:child_process";
|
|
42
|
-
import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
43
|
-
import { homedir, platform as osPlatform } from "node:os";
|
|
44
|
-
import { basename, delimiter as delimiter2, dirname as dirname2, join as join2 } from "node:path";
|
|
45
|
-
|
|
46
|
-
// bin/tlc-exec.mjs
|
|
47
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
48
|
-
import { delimiter, dirname, join } from "node:path";
|
|
49
|
-
function isPackagedCopy(candidate) {
|
|
50
|
-
return candidate.split(/[/\\]/).includes("node_modules");
|
|
51
|
-
}
|
|
52
|
-
var BUN_EXECUTABLE_NAMES = ["bun", "bun.exe"];
|
|
53
|
-
function findBunOnPath(env = process.env) {
|
|
54
|
-
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
55
|
-
if (!dir) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
for (const name of BUN_EXECUTABLE_NAMES) {
|
|
59
|
-
const candidate = join(dir, name);
|
|
60
|
-
if (existsSync(candidate)) {
|
|
61
|
-
return candidate;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
function runtimeCachePath(harnessHome) {
|
|
68
|
-
return join(harnessHome, "state", "runtime-cache.json");
|
|
69
|
-
}
|
|
70
|
-
function writeRuntimeCache(harnessHome, bunPath) {
|
|
71
|
-
const record = { bunPath, checkedAt: new Date().toISOString() };
|
|
72
|
-
if (isPackagedCopy(harnessHome)) {
|
|
73
|
-
return record;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
const cachePath = runtimeCachePath(harnessHome);
|
|
77
|
-
mkdirSync(dirname(cachePath), { recursive: true });
|
|
78
|
-
writeFileSync(cachePath, `${JSON.stringify(record)}
|
|
79
|
-
`);
|
|
80
|
-
} catch {}
|
|
81
|
-
return record;
|
|
82
|
-
}
|
|
83
|
-
if (false) {}
|
|
84
|
-
|
|
85
|
-
// tools/doctor.ts
|
|
86
|
-
function plural(count, word) {
|
|
87
|
-
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
88
|
-
}
|
|
89
|
-
var MIN_NODE = 24;
|
|
90
|
-
function medianMs(samples) {
|
|
91
|
-
if (samples.length === 0) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
95
|
-
const middle = Math.floor(sorted.length / 2);
|
|
96
|
-
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
97
|
-
}
|
|
98
|
-
function measureRuntimeStart(args) {
|
|
99
|
-
const spawn = args.spawn ?? ((command, argv) => ({
|
|
100
|
-
ok: (spawnSync(command, argv, { stdio: "ignore" }).status ?? 1) === 0
|
|
101
|
-
}));
|
|
102
|
-
const now = args.now ?? (() => Date.now());
|
|
103
|
-
const durations = [];
|
|
104
|
-
for (let i = 0;i < (args.samples ?? 3); i += 1) {
|
|
105
|
-
const started = now();
|
|
106
|
-
if (!spawn(args.command, args.args).ok) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
durations.push(now() - started);
|
|
110
|
-
}
|
|
111
|
-
return medianMs(durations);
|
|
112
|
-
}
|
|
113
|
-
function checkNodeVersion(nodeVersion, bunPath = null) {
|
|
114
|
-
const nodeMajor = Number.parseInt(nodeVersion.replace(/^v/, "").split(".")[0] ?? "0", 10);
|
|
115
|
-
const checks = [
|
|
116
|
-
{
|
|
117
|
-
level: nodeMajor >= MIN_NODE || bunPath !== null ? "ok" : "fail",
|
|
118
|
-
name: "Node.js runtime",
|
|
119
|
-
detail: nodeMajor >= MIN_NODE ? `${nodeVersion} (>= ${MIN_NODE})` : bunPath !== null ? `${nodeVersion} — below ${MIN_NODE}, covered by Bun at ${bunPath}` : `${nodeVersion} — no runtime for hooks. Install Bun (curl -fsSL https://bun.sh/install | bash) or Node ${MIN_NODE}+ (nodejs.org), then reload the editor.`
|
|
120
|
-
}
|
|
121
|
-
];
|
|
122
|
-
if (nodeMajor === 25) {
|
|
123
|
-
checks.push({
|
|
124
|
-
level: "warn",
|
|
125
|
-
name: "Node.js line",
|
|
126
|
-
detail: "Node 25 is EOL — prefer 24 LTS or 26 Current"
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return checks;
|
|
130
|
-
}
|
|
131
|
-
function runtimeOwnershipCheck(home) {
|
|
132
|
-
const kind = runtimePathKind(home);
|
|
133
|
-
const detail = {
|
|
134
|
-
managed: "managed checkout — `tlc harness update` moves it to upstream and owns its contents",
|
|
135
|
-
linked: "link to a working clone — update never writes here; pull that clone yourself",
|
|
136
|
-
npm: "installed from npm — `tlc harness update` bumps the package and re-materialises this directory",
|
|
137
|
-
unmanaged: "not a git checkout — update cannot pull; install the package and run `tlc harness install`",
|
|
138
|
-
absent: "missing — install the package, then run `tlc harness install`"
|
|
139
|
-
};
|
|
140
|
-
return {
|
|
141
|
-
level: kind === "managed" || kind === "linked" || kind === "npm" ? "ok" : "fail",
|
|
142
|
-
name: "runtime ownership",
|
|
143
|
-
detail: detail[kind]
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function checkSkillLinks(home, providerDirs = providerConfigDirs(), probe = {
|
|
147
|
-
linkTarget: (path) => {
|
|
148
|
-
try {
|
|
149
|
-
return realpathSync2(path);
|
|
150
|
-
} catch {
|
|
151
|
-
try {
|
|
152
|
-
return readlinkSync(path);
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
exists: existsSync2,
|
|
159
|
-
realpath: (path) => {
|
|
160
|
-
try {
|
|
161
|
-
return realpathSync2(path);
|
|
162
|
-
} catch {
|
|
163
|
-
return path;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}) {
|
|
167
|
-
return providerDirs.filter((dir) => existsSync2(dir)).map((dir) => {
|
|
168
|
-
const health = coreFacade.skill.linkHealth(join2(dir, "skills", "harness-init"), home, probe);
|
|
169
|
-
return {
|
|
170
|
-
level: health.state === "ok" ? "ok" : "fail",
|
|
171
|
-
name: `init skill (${basename(dir)})`,
|
|
172
|
-
detail: coreFacade.skill.linkHealthMessage(health)
|
|
173
|
-
};
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
function checkPrices(now = new Date, read = { meta: catalogueMeta, planes: planeMeta }) {
|
|
177
|
-
const state = coreFacade.pricing.freshness(read.meta(), now);
|
|
178
|
-
const planes = read.planes();
|
|
179
|
-
const named = Object.entries(planes).map(([plane, meta]) => `${plane} ${meta.count ?? 0}`).join(", ");
|
|
180
|
-
return [
|
|
181
|
-
{
|
|
182
|
-
level: state.state === "fresh" ? "ok" : "warn",
|
|
183
|
-
name: "prices",
|
|
184
|
-
detail: state.state === "fresh" ? `${coreFacade.pricing.freshnessMessage(state, "catalogue")}${named ? ` (${named})` : ""}` : coreFacade.pricing.freshnessMessage(state, "catalogue")
|
|
185
|
-
}
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
|
-
function resolveOnPath(command, env = process.env, exists = existsSync2) {
|
|
189
|
-
for (const dir of (env.PATH ?? "").split(delimiter2)) {
|
|
190
|
-
if (!dir) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
for (const name of [command, `${command}.cmd`, `${command}.exe`, `${command}.ps1`]) {
|
|
194
|
-
const candidate = join2(dir, name);
|
|
195
|
-
if (exists(candidate)) {
|
|
196
|
-
return candidate;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
function checkRuntimePaths(home, platform) {
|
|
203
|
-
const launcher = join2(home, "bin", "tlc-exec.mjs");
|
|
204
|
-
const distSample = join2(home, "dist", "stop.mjs");
|
|
205
|
-
const onPath = resolveOnPath("tlc");
|
|
206
|
-
return [
|
|
207
|
-
{ level: "ok", name: "platform", detail: platform },
|
|
208
|
-
{ level: existsSync2(launcher) ? "ok" : "fail", name: "global runtime", detail: home },
|
|
209
|
-
runtimeOwnershipCheck(home),
|
|
210
|
-
{
|
|
211
|
-
level: existsSync2(distSample) ? "ok" : "fail",
|
|
212
|
-
name: "dist bundles",
|
|
213
|
-
detail: existsSync2(distSample) ? join2(home, "dist") : "missing — run: tlc harness build"
|
|
214
|
-
},
|
|
215
|
-
{ level: existsSync2(launcher) ? "ok" : "fail", name: "portable launcher", detail: launcher },
|
|
216
|
-
{
|
|
217
|
-
level: onPath === null ? "fail" : "ok",
|
|
218
|
-
name: "CLI on PATH",
|
|
219
|
-
detail: onPath ?? `no \`tlc\` on PATH — npm i -g ${NPM_PACKAGE}, or \`npm link\` from a clone`
|
|
220
|
-
}
|
|
221
|
-
];
|
|
222
|
-
}
|
|
223
|
-
function checkHookRuntime(_home, bunPath, measure = measureRuntimeStart) {
|
|
224
|
-
const runtime = bunPath ?? process.execPath;
|
|
225
|
-
const label = bunPath ? `Bun (${bunPath})` : `Node + dist/ (${process.version})`;
|
|
226
|
-
const ms = measure({ command: runtime, args: ["-e", ""] });
|
|
227
|
-
const timing = ms === null ? " — interpreter start could not be measured on this machine" : ` — interpreter start measured at ${ms} ms here, paid once per hook`;
|
|
228
|
-
return bunPath ? { level: "ok", name: "hook runtime", detail: `${label}${timing}` } : {
|
|
229
|
-
level: "warn",
|
|
230
|
-
name: "hook runtime",
|
|
231
|
-
detail: `${label}${timing}. Bun runs the source directly and starts faster — install: https://bun.sh`
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
function wiringProblems(wiring) {
|
|
235
|
-
if (wiring.strategy !== "replace") {
|
|
236
|
-
return [];
|
|
237
|
-
}
|
|
238
|
-
const text = existsSync2(wiring.target) ? readFileSync2(wiring.target, "utf8") : null;
|
|
239
|
-
return cursorWiringProblems(text, { launcherPath: launcherPathOf(wiring) }, existsSync2);
|
|
240
|
-
}
|
|
241
|
-
function launcherPathOf(wiring) {
|
|
242
|
-
const first = wiring.entries[0];
|
|
243
|
-
return first?.args.find((arg) => arg.endsWith(".mjs")) ?? "";
|
|
244
|
-
}
|
|
245
|
-
function providerWiringStatus(wiring) {
|
|
246
|
-
if (!existsSync2(dirname2(wiring.target))) {
|
|
247
|
-
return "not-installed";
|
|
248
|
-
}
|
|
249
|
-
if (wiring.strategy === "replace") {
|
|
250
|
-
if (!isCursorWired(wiring.target)) {
|
|
251
|
-
return "detected-but-unwired";
|
|
252
|
-
}
|
|
253
|
-
return wiringProblems(wiring).length === 0 ? "wired" : "detected-but-unwired";
|
|
254
|
-
}
|
|
255
|
-
const existingText = existsSync2(wiring.target) ? readFileSync2(wiring.target, "utf8") : null;
|
|
256
|
-
const result = mergeClaudeSettings(existingText, wiring.entries);
|
|
257
|
-
return result.ok && !result.changed ? "wired" : "detected-but-unwired";
|
|
258
|
-
}
|
|
259
|
-
function checkProviders(registry, home) {
|
|
260
|
-
const launcherPath = join2(home, "bin", "tlc-exec.mjs");
|
|
261
|
-
return registry.map((provider) => {
|
|
262
|
-
const wiring = provider.wiring({ launcherPath });
|
|
263
|
-
const status = providerWiringStatus(wiring);
|
|
264
|
-
if (status === "not-installed") {
|
|
265
|
-
return { level: "ok", name: `${provider.name} wiring`, detail: "not installed" };
|
|
266
|
-
}
|
|
267
|
-
if (status === "wired") {
|
|
268
|
-
return { level: "ok", name: `${provider.name} wiring`, detail: `wired (${wiring.target})` };
|
|
269
|
-
}
|
|
270
|
-
const problems = wiringProblems(wiring);
|
|
271
|
-
const why = problems.length > 0 ? ` — ${formatWiringProblems(problems)}` : "";
|
|
272
|
-
return {
|
|
273
|
-
level: "warn",
|
|
274
|
-
name: `${provider.name} wiring`,
|
|
275
|
-
detail: `detected but not wired${why} — run: tlc harness update (${wiring.target})`
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
function checkCapabilities(root, runtimeRoot) {
|
|
280
|
-
const catalog = coreFacade.capability.loadCatalog(runtimeRoot);
|
|
281
|
-
const policy = coreFacade.capability.readProjectPolicyRaw(root);
|
|
282
|
-
if (!catalog || !policy) {
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
const available = coreFacade.capability.listAvailableNotEnabled(policy, catalog);
|
|
286
|
-
if (available.length === 0) {
|
|
287
|
-
return [];
|
|
288
|
-
}
|
|
289
|
-
return [
|
|
290
|
-
{
|
|
291
|
-
level: "ok",
|
|
292
|
-
name: "capabilities",
|
|
293
|
-
detail: coreFacade.capability.formatAvailableInventory(available)
|
|
294
|
-
}
|
|
295
|
-
];
|
|
296
|
-
}
|
|
297
|
-
function checkPosture(root) {
|
|
298
|
-
const posture = coreFacade.policy.resolveProjectPosture(root);
|
|
299
|
-
if (posture.origin !== "fallback") {
|
|
300
|
-
return { level: "ok", name: "operator posture", detail: `${posture.mode} (from ${posture.origin})` };
|
|
301
|
-
}
|
|
302
|
-
return {
|
|
303
|
-
level: "warn",
|
|
304
|
-
name: "operator posture",
|
|
305
|
-
detail: `\`${posture.invalid}\` is not a posture — running as ${posture.mode}. Accepted: ${coreFacade.policy.OPERATOR_MODES.join(" | ")}. Fix \`mode\` in ${projectConfigPath(root)}, or run: tlc harness mode <${coreFacade.policy.OPERATOR_MODES.join("|")}>`
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
function checkObservedRails(root) {
|
|
309
|
-
const policy = coreFacade.policy.loadPolicy(root);
|
|
310
|
-
if (!policy.observe.enabled) {
|
|
311
|
-
return [];
|
|
312
|
-
}
|
|
313
|
-
const unusable = coreFacade.observe.unobservableRails(policy.observe.rails);
|
|
314
|
-
const observable = coreFacade.observe.OBSERVABLE_RAILS.join(" | ");
|
|
315
|
-
if (policy.observe.rails.length === 0) {
|
|
316
|
-
return [
|
|
317
|
-
{
|
|
318
|
-
level: "warn",
|
|
319
|
-
name: "observed rails",
|
|
320
|
-
detail: `observation is on with no rails listed, so nothing is measured. Set \`observe.rails\` to one or more of: ${observable}`
|
|
321
|
-
}
|
|
322
|
-
];
|
|
323
|
-
}
|
|
324
|
-
if (unusable.length === 0) {
|
|
325
|
-
return [{ level: "ok", name: "observed rails", detail: policy.observe.rails.join(", ") }];
|
|
326
|
-
}
|
|
327
|
-
return [
|
|
328
|
-
{
|
|
329
|
-
level: "warn",
|
|
330
|
-
name: "observed rails",
|
|
331
|
-
detail: `no checker exists for ${unusable.map((rail) => `\`${rail}\``).join(", ")}, so nothing is recorded for them. Observable today: ${observable}`
|
|
332
|
-
}
|
|
333
|
-
];
|
|
334
|
-
}
|
|
335
|
-
function checkSubagentAllowlist(root) {
|
|
336
|
-
const { subagents } = coreFacade.policy.loadPolicy(root);
|
|
337
|
-
if (!subagents.enforceAllowlist) {
|
|
338
|
-
return [];
|
|
339
|
-
}
|
|
340
|
-
const configured = subagents.allowedModels;
|
|
341
|
-
const entries = Array.isArray(configured) ? configured.length : Object.values(configured ?? {}).reduce((total, list) => total + list.length, 0);
|
|
342
|
-
if (entries > 0) {
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
-
return [
|
|
346
|
-
{
|
|
347
|
-
level: "fail",
|
|
348
|
-
name: "subagent allowlist",
|
|
349
|
-
detail: "enforceAllowlist is on and subagents.allowedModels is empty, so it permits every model. The harness ships no list — add the model slugs you allow, or set enforceAllowlist to false."
|
|
350
|
-
}
|
|
351
|
-
];
|
|
352
|
-
}
|
|
353
|
-
function checkLessonHealth(root) {
|
|
354
|
-
const policy = coreFacade.policy.loadPolicy(root);
|
|
355
|
-
if (!policy.intelligence.lessons.enabled) {
|
|
356
|
-
return [];
|
|
357
|
-
}
|
|
358
|
-
const now = new Date;
|
|
359
|
-
const writable = [...coreFacade.lesson.readProjectLessons(root), ...coreFacade.lesson.readGlobalLessons()];
|
|
360
|
-
if (writable.length === 0) {
|
|
361
|
-
return [];
|
|
362
|
-
}
|
|
363
|
-
const stale = writable.filter((lesson) => coreFacade.lesson.isStaleLesson(lesson));
|
|
364
|
-
const outOfWindow = writable.filter((lesson) => coreFacade.lesson.validityReason(lesson, now) !== "active");
|
|
365
|
-
const unproven = writable.filter((lesson) => coreFacade.lesson.lessonEffectiveness(lesson) === "unproven");
|
|
366
|
-
const checks = [];
|
|
367
|
-
if (stale.length > 0) {
|
|
368
|
-
checks.push({
|
|
369
|
-
level: "warn",
|
|
370
|
-
name: "stale lessons",
|
|
371
|
-
detail: `${plural(stale.length, "lesson")} name a path or symbol that no longer resolves, so ${stale.length === 1 ? "it is" : "they are"} withheld: ${stale.map((lesson) => lesson.id).join(", ")}. Run: tlc harness lessons list`
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
if (outOfWindow.length > 0) {
|
|
375
|
-
checks.push({
|
|
376
|
-
level: "warn",
|
|
377
|
-
name: "lessons out of window",
|
|
378
|
-
detail: `${plural(outOfWindow.length, "lesson")} fall outside their validity window: ${outOfWindow.map((lesson) => lesson.id).join(", ")}. Run: tlc harness lessons garden`
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
if (unproven.length > 0) {
|
|
382
|
-
checks.push({
|
|
383
|
-
level: "warn",
|
|
384
|
-
name: "unproven lessons",
|
|
385
|
-
detail: unproven.length === 1 ? "1 lesson has been injected for a gate and never graded, so nothing shows it helped" : `${unproven.length} lessons have been injected for a gate and never graded, so nothing shows they helped`
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
if (checks.length === 0) {
|
|
389
|
-
return [
|
|
390
|
-
{
|
|
391
|
-
level: "ok",
|
|
392
|
-
name: "lesson health",
|
|
393
|
-
detail: `${plural(writable.length, "lesson")} across the writable tiers, none stale, none out of window`
|
|
394
|
-
}
|
|
395
|
-
];
|
|
396
|
-
}
|
|
397
|
-
return checks;
|
|
398
|
-
}
|
|
399
|
-
function checkPolicyDivergence(root) {
|
|
400
|
-
const diverged = coreFacade.policy.allDivergedPaths(root);
|
|
401
|
-
if (diverged.length === 0) {
|
|
402
|
-
return [];
|
|
403
|
-
}
|
|
404
|
-
return [
|
|
405
|
-
{
|
|
406
|
-
level: "warn",
|
|
407
|
-
name: "policy baseline",
|
|
408
|
-
detail: `changed out of band during a live session: ${diverged.join(", ")}. If that was you: tlc harness policy accept ${diverged.join(" ")}`
|
|
409
|
-
}
|
|
410
|
-
];
|
|
411
|
-
}
|
|
412
|
-
function checkGateScope(root) {
|
|
413
|
-
const policy = coreFacade.policy.loadPolicy(root);
|
|
414
|
-
if (!policy.grind.enabled) {
|
|
415
|
-
return [];
|
|
416
|
-
}
|
|
417
|
-
const checks = [];
|
|
418
|
-
for (const [label, command] of [
|
|
419
|
-
["lintCommand", policy.grind.lintCommand],
|
|
420
|
-
["testCommand", policy.grind.testCommand]
|
|
421
|
-
]) {
|
|
422
|
-
if (!command || command.length === 0) {
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
const verdict = coreFacade.gate.appendFilesVerdict(command, policy.grind.appendFiles);
|
|
426
|
-
if (verdict.appends || policy.grind.appendFiles === "never") {
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
checks.push({
|
|
430
|
-
level: "warn",
|
|
431
|
-
name: `gate scope (${label})`,
|
|
432
|
-
detail: `runs in full on every attempt, up to maxLoops ${policy.grind.maxLoops}, because ${verdict.reason}. Scope the command itself, or accept the cost knowingly.`
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
return checks;
|
|
436
|
-
}
|
|
437
|
-
function checkProjectPolicy(root) {
|
|
438
|
-
const configPath = projectConfigPath(root);
|
|
439
|
-
const stateDir = projectStateDir(root);
|
|
440
|
-
return [
|
|
441
|
-
{
|
|
442
|
-
level: "ok",
|
|
443
|
-
name: "project policy",
|
|
444
|
-
detail: existsSync2(configPath) ? configPath : "missing — run: tlc harness init"
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
level: "ok",
|
|
448
|
-
name: "state dir",
|
|
449
|
-
detail: existsSync2(stateDir) ? stateDir : `${stateDir} (created on first session)`
|
|
450
|
-
},
|
|
451
|
-
checkPosture(root),
|
|
452
|
-
...checkObservedRails(root),
|
|
453
|
-
...checkLessonHealth(root),
|
|
454
|
-
...checkSubagentAllowlist(root),
|
|
455
|
-
...checkPolicyDivergence(root),
|
|
456
|
-
...checkGateScope(root)
|
|
457
|
-
];
|
|
458
|
-
}
|
|
459
|
-
function checkGlobalCommands(home) {
|
|
460
|
-
const globalCommands = join2(home, ".cursor", "commands");
|
|
461
|
-
if (!existsSync2(globalCommands)) {
|
|
462
|
-
return {
|
|
463
|
-
level: "ok",
|
|
464
|
-
name: "global commands dir",
|
|
465
|
-
detail: "optional — ~/.cursor/commands for slash commands"
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
try {
|
|
469
|
-
const st = lstatSync(globalCommands);
|
|
470
|
-
const detail = st.isSymbolicLink() ? `${globalCommands} → ${readlinkSync(globalCommands)}` : globalCommands;
|
|
471
|
-
return { level: "ok", name: "global commands dir", detail };
|
|
472
|
-
} catch {
|
|
473
|
-
return { level: "ok", name: "global commands dir", detail: globalCommands };
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
function runChecks(ctx) {
|
|
477
|
-
return [
|
|
478
|
-
...checkNodeVersion(ctx.nodeVersion, ctx.bunPath),
|
|
479
|
-
...checkRuntimePaths(ctx.runtimeHome, ctx.platform),
|
|
480
|
-
...checkSkillLinks(ctx.runtimeHome),
|
|
481
|
-
checkHookRuntime(ctx.runtimeHome, ctx.bunPath),
|
|
482
|
-
...checkProviders(ctx.registry, ctx.runtimeHome),
|
|
483
|
-
...checkProjectPolicy(ctx.root),
|
|
484
|
-
...checkCapabilities(ctx.root, ctx.runtimeHome),
|
|
485
|
-
...checkPrices(),
|
|
486
|
-
checkGlobalCommands(ctx.home)
|
|
487
|
-
];
|
|
488
|
-
}
|
|
489
|
-
function exitCodeFor(checks) {
|
|
490
|
-
return checks.some((c) => c.level === "fail") ? 1 : 0;
|
|
491
|
-
}
|
|
492
|
-
function checkId(name) {
|
|
493
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
494
|
-
}
|
|
495
|
-
function toReport(checks) {
|
|
496
|
-
const statuses = { ok: "OK", warn: "WARN", fail: "FAIL" };
|
|
497
|
-
return {
|
|
498
|
-
ok: exitCodeFor(checks) === 0,
|
|
499
|
-
failed: checks.filter((check) => check.level === "fail").length,
|
|
500
|
-
warned: checks.filter((check) => check.level === "warn").length,
|
|
501
|
-
checks: checks.map((check) => ({
|
|
502
|
-
id: checkId(check.name),
|
|
503
|
-
name: check.name,
|
|
504
|
-
status: statuses[check.level],
|
|
505
|
-
detail: check.detail
|
|
506
|
-
}))
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
function formatReport(checks, style = PLAIN) {
|
|
510
|
-
const marks = { ok: "OK ", warn: "WARN", fail: "FAIL" };
|
|
511
|
-
const paint = { ok: "success", warn: "warning", fail: "error" };
|
|
512
|
-
const lines = checks.map((c) => `${style.paint(paint[c.level], marks[c.level])} ${style.paint("textMuted", c.name)} ${style.dim("—")} ${c.detail}`);
|
|
513
|
-
const failed = checks.filter((c) => c.level === "fail").length;
|
|
514
|
-
const warned = checks.filter((c) => c.level === "warn").length;
|
|
515
|
-
lines.push("");
|
|
516
|
-
if (failed > 0) {
|
|
517
|
-
lines.push(style.paint("error", `${SYMBOLS.cross} doctor: ${plural(failed, "failure")}${warned > 0 ? `, ${plural(warned, "warning")}` : ""}`));
|
|
518
|
-
} else if (warned > 0) {
|
|
519
|
-
lines.push(style.paint("warning", `${SYMBOLS.warning} doctor: no failures, ${plural(warned, "warning")} to read above`));
|
|
520
|
-
} else {
|
|
521
|
-
lines.push(style.paint("success", `${SYMBOLS.check} doctor: all checks passed`));
|
|
522
|
-
}
|
|
523
|
-
return lines.join(`
|
|
524
|
-
`);
|
|
525
|
-
}
|
|
526
|
-
function realContext() {
|
|
527
|
-
const home = runtimeHome();
|
|
528
|
-
const bunPath = findBunOnPath();
|
|
529
|
-
writeRuntimeCache(home, bunPath);
|
|
530
|
-
return {
|
|
531
|
-
root: process.env.TLC_PROJECT_DIR ?? process.cwd(),
|
|
532
|
-
home: homedir(),
|
|
533
|
-
runtimeHome: home,
|
|
534
|
-
platform: osPlatform(),
|
|
535
|
-
nodeVersion: process.version,
|
|
536
|
-
bunPath,
|
|
537
|
-
registry: providers
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
if (__require.main == __require.module) {
|
|
541
|
-
const { json } = takeJsonFlag(process.argv.slice(2));
|
|
542
|
-
const checks = runChecks(realContext());
|
|
543
|
-
if (json) {
|
|
544
|
-
emitJson(toReport(checks));
|
|
545
|
-
} else {
|
|
546
|
-
console.log(formatReport(checks, createStyle()));
|
|
547
|
-
}
|
|
548
|
-
process.exit(exitCodeFor(checks));
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
export { plural, medianMs, measureRuntimeStart, checkNodeVersion, runtimeOwnershipCheck, checkSkillLinks, checkPrices, resolveOnPath, checkRuntimePaths, checkHookRuntime, wiringProblems, providerWiringStatus, checkProviders, checkCapabilities, checkSubagentAllowlist, checkLessonHealth, checkProjectPolicy, checkGlobalCommands, runChecks, exitCodeFor, checkId, toReport, formatReport };
|