@tech-leads-club/harness-toolkit 0.3.0 → 0.3.2
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 +46 -11
- package/bin/tlc-build.mjs +35 -6
- package/config.example.json +1 -1
- package/dist/chunks/compact-before-1e4qg1qt.mjs +1185 -0
- package/dist/chunks/compact-before-2hpbfxm5.mjs +5782 -0
- package/dist/chunks/compact-before-49j320yp.mjs +1283 -0
- package/dist/chunks/compact-before-4jrq0sqs.mjs +61 -0
- package/dist/chunks/compact-before-6w8n1vh1.mjs +186 -0
- package/dist/chunks/compact-before-7sdmwswh.mjs +52 -0
- package/dist/chunks/compact-before-beqpmqrm.mjs +187 -0
- package/dist/chunks/compact-before-j9y4jgn4.mjs +845 -0
- package/dist/chunks/compact-before-pk86tqx2.mjs +118 -0
- package/dist/chunks/compact-before-pkqk5v29.mjs +137 -0
- package/dist/chunks/compact-before-w1293m4n.mjs +315 -0
- package/dist/chunks/compact-before-wnnds45y.mjs +26 -0
- package/dist/chunks/compact-before-wt2c3nh4.mjs +551 -0
- package/dist/compact-before.mjs +13 -7961
- package/dist/doctor.mjs +33 -8480
- package/dist/help-topic.mjs +6 -14
- package/dist/init-project.mjs +36 -793
- package/dist/install-runtime.mjs +18 -1039
- package/dist/lessons-cli.mjs +25 -7043
- package/dist/obs-cli.mjs +27 -7037
- package/dist/price-lookup.mjs +11 -201
- package/dist/prompt-submit.mjs +14 -7974
- package/dist/refresh-model-prices.mjs +26 -7061
- package/dist/response-after.mjs +11 -7961
- package/dist/run.mjs +10 -7960
- package/dist/session-end.mjs +14 -8029
- package/dist/session-start.mjs +20 -8075
- package/dist/shim.mjs +18 -7024
- package/dist/stop.mjs +29 -8042
- package/dist/subagent-start.mjs +13 -7983
- package/dist/subagent-stop.mjs +11 -7961
- package/dist/support.mjs +21 -7168
- package/dist/tlc-cli.mjs +65 -8200
- package/dist/tool-after.mjs +20 -8176
- package/dist/tool-before.mjs +15 -7990
- package/dist/tool-failure.mjs +14 -7961
- package/dist/uninstall-runtime.mjs +61 -1008
- package/docs/log.md +4 -0
- package/package.json +2 -2
- package/src/core/release/release.version.ts +10 -4
- package/tools/install-runtime.ts +21 -1
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSON_FLAG,
|
|
3
|
+
emitJson,
|
|
4
|
+
takeJsonFlag,
|
|
5
|
+
unknownFlags
|
|
6
|
+
} from "./compact-before-wnnds45y.mjs";
|
|
7
|
+
import {
|
|
8
|
+
coreFacade
|
|
9
|
+
} from "./compact-before-2hpbfxm5.mjs";
|
|
10
|
+
import {
|
|
11
|
+
PLAIN,
|
|
12
|
+
createStyle,
|
|
13
|
+
render
|
|
14
|
+
} from "./compact-before-49j320yp.mjs";
|
|
15
|
+
import {
|
|
16
|
+
__require,
|
|
17
|
+
flagsDir,
|
|
18
|
+
projectConfigPath,
|
|
19
|
+
projectStateDir,
|
|
20
|
+
providerConfigDirs,
|
|
21
|
+
runtimeHome
|
|
22
|
+
} from "./compact-before-4jrq0sqs.mjs";
|
|
23
|
+
|
|
24
|
+
// bin/tlc-cli.ts
|
|
25
|
+
import { spawnSync } from "node:child_process";
|
|
26
|
+
import {
|
|
27
|
+
existsSync as existsSync2,
|
|
28
|
+
lstatSync as lstatSync2,
|
|
29
|
+
mkdirSync as mkdirSync2,
|
|
30
|
+
readdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
realpathSync,
|
|
33
|
+
rmSync as rmSync2,
|
|
34
|
+
writeFileSync
|
|
35
|
+
} from "node:fs";
|
|
36
|
+
import { delimiter, join as join2 } from "node:path";
|
|
37
|
+
|
|
38
|
+
// src/platform/links.ts
|
|
39
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
40
|
+
import { dirname, join } from "node:path";
|
|
41
|
+
var LINK_TYPE = "junction";
|
|
42
|
+
function linkDir(source, target) {
|
|
43
|
+
let replaced = false;
|
|
44
|
+
if (isLink(target)) {
|
|
45
|
+
rmSync(target, { recursive: true, force: true });
|
|
46
|
+
replaced = true;
|
|
47
|
+
} else if (existsSync(target)) {
|
|
48
|
+
return {
|
|
49
|
+
kind: "refused",
|
|
50
|
+
target,
|
|
51
|
+
reason: `${target} exists and is not a link — move it aside and re-run`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
55
|
+
symlinkSync(source, target, LINK_TYPE);
|
|
56
|
+
return { kind: replaced ? "relinked" : "linked", target, source };
|
|
57
|
+
}
|
|
58
|
+
function isLink(path) {
|
|
59
|
+
try {
|
|
60
|
+
return lstatSync(path).isSymbolicLink();
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function seedConfig(dest) {
|
|
66
|
+
const path = join(dest, "config.json");
|
|
67
|
+
const example = join(dest, "config.example.json");
|
|
68
|
+
if (existsSync(path) || !existsSync(example)) {
|
|
69
|
+
return { seeded: false, path };
|
|
70
|
+
}
|
|
71
|
+
copyFileSync(example, path);
|
|
72
|
+
return { seeded: true, path };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// bin/tlc-cli.ts
|
|
76
|
+
class UsageError extends Error {
|
|
77
|
+
}
|
|
78
|
+
function resolveProjectRoot() {
|
|
79
|
+
return process.env.TLC_PROJECT_DIR ?? process.cwd();
|
|
80
|
+
}
|
|
81
|
+
function modeFilePath(root) {
|
|
82
|
+
return join2(projectStateDir(root), "harness-mode");
|
|
83
|
+
}
|
|
84
|
+
function grindFlagPath(root) {
|
|
85
|
+
return join2(flagsDir(root), "grind-on");
|
|
86
|
+
}
|
|
87
|
+
function skipFlagPath(root) {
|
|
88
|
+
return join2(flagsDir(root), "skip-verify");
|
|
89
|
+
}
|
|
90
|
+
function focusFlagPath(root) {
|
|
91
|
+
return join2(flagsDir(root), "focus");
|
|
92
|
+
}
|
|
93
|
+
function pairedFlagPath(root) {
|
|
94
|
+
return join2(flagsDir(root), "paired");
|
|
95
|
+
}
|
|
96
|
+
function ensureFlagsDir(root) {
|
|
97
|
+
mkdirSync2(flagsDir(root), { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
function readMode(root) {
|
|
100
|
+
return coreFacade.policy.loadPolicy(root).mode;
|
|
101
|
+
}
|
|
102
|
+
function grindOn(root) {
|
|
103
|
+
return coreFacade.policy.loadPolicy(root).grind.enabled;
|
|
104
|
+
}
|
|
105
|
+
function gatesPaused(root) {
|
|
106
|
+
return existsSync2(skipFlagPath(root));
|
|
107
|
+
}
|
|
108
|
+
function acceptedModes() {
|
|
109
|
+
return coreFacade.policy.OPERATOR_MODES.join(" | ");
|
|
110
|
+
}
|
|
111
|
+
function statusScreen(root) {
|
|
112
|
+
const report = statusJson(root);
|
|
113
|
+
const origin = report.modeInvalid === undefined ? `from ${report.modeOrigin}` : `${report.modeOrigin} — \`${report.modeInvalid}\` is not a posture; accepted: ${acceptedModes()}`;
|
|
114
|
+
return {
|
|
115
|
+
title: "harness status",
|
|
116
|
+
summary: [root],
|
|
117
|
+
sections: [
|
|
118
|
+
{
|
|
119
|
+
rows: [
|
|
120
|
+
{ label: "mode", value: `${report.mode} [${origin}]`, level: "info" },
|
|
121
|
+
{
|
|
122
|
+
label: "grind",
|
|
123
|
+
value: report.grind ? "ON — stop hook re-runs lint/tests and auto-retries on fail" : "OFF — no auto fix loops",
|
|
124
|
+
level: report.grind ? "ok" : "info"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
label: "gates",
|
|
128
|
+
value: report.gatesPaused ? "PAUSED — stop checks disabled" : "active",
|
|
129
|
+
level: report.gatesPaused ? "warn" : "ok"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
title: "Postures",
|
|
135
|
+
lines: [
|
|
136
|
+
"paired explains as it goes, and asks before any sizable move",
|
|
137
|
+
"solo works on its own; a destructive action, a dead-end or real ambiguity reaches you",
|
|
138
|
+
"focus only a destructive action or a dead-end reaches you; it settles ambiguity itself"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
footer: "verification is identical at all three postures · tlc harness why · tlc harness doctor"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function statusText(root, style = PLAIN) {
|
|
146
|
+
return render(statusScreen(root), style);
|
|
147
|
+
}
|
|
148
|
+
function statusJson(root) {
|
|
149
|
+
const policy = coreFacade.policy.loadPolicy(root);
|
|
150
|
+
const posture = coreFacade.policy.resolveProjectPosture(root);
|
|
151
|
+
return {
|
|
152
|
+
root,
|
|
153
|
+
mode: posture.mode,
|
|
154
|
+
modeOrigin: posture.origin,
|
|
155
|
+
...posture.invalid === undefined ? {} : { modeInvalid: posture.invalid },
|
|
156
|
+
grind: policy.grind.enabled,
|
|
157
|
+
gatesPaused: gatesPaused(root)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function setGrind(root, on) {
|
|
161
|
+
ensureFlagsDir(root);
|
|
162
|
+
const path = grindFlagPath(root);
|
|
163
|
+
if (on) {
|
|
164
|
+
writeFileSync(path, "");
|
|
165
|
+
coreFacade.policy.refreshPolicyBaselines(root);
|
|
166
|
+
return "grind ON — stop hook will lint/test and auto-retry on failure";
|
|
167
|
+
}
|
|
168
|
+
if (existsSync2(path)) {
|
|
169
|
+
rmSync2(path);
|
|
170
|
+
}
|
|
171
|
+
coreFacade.policy.refreshPolicyBaselines(root);
|
|
172
|
+
return "grind OFF — no auto fix loops";
|
|
173
|
+
}
|
|
174
|
+
function setPaused(root, on) {
|
|
175
|
+
ensureFlagsDir(root);
|
|
176
|
+
const path = skipFlagPath(root);
|
|
177
|
+
if (on) {
|
|
178
|
+
writeFileSync(path, "");
|
|
179
|
+
coreFacade.policy.refreshPolicyBaselines(root);
|
|
180
|
+
return "gates PAUSED — stop checks disabled until `tlc harness resume`";
|
|
181
|
+
}
|
|
182
|
+
if (existsSync2(path)) {
|
|
183
|
+
rmSync2(path);
|
|
184
|
+
}
|
|
185
|
+
coreFacade.policy.refreshPolicyBaselines(root);
|
|
186
|
+
return "gates ACTIVE again";
|
|
187
|
+
}
|
|
188
|
+
var MODE_CONFIRMATION = {
|
|
189
|
+
paired: "mode paired — explains as it goes, and asks before any sizable move",
|
|
190
|
+
solo: "mode solo — a destructive action, a dead-end or real ambiguity reaches you",
|
|
191
|
+
focus: "mode focus — only a destructive action or a dead-end reaches you; ambiguity is settled for you"
|
|
192
|
+
};
|
|
193
|
+
function setMode(root, raw) {
|
|
194
|
+
const mode = raw.toLowerCase();
|
|
195
|
+
if (!coreFacade.policy.isOperatorMode(mode)) {
|
|
196
|
+
throw new UsageError(`mode must be: ${acceptedModes()}`);
|
|
197
|
+
}
|
|
198
|
+
ensureFlagsDir(root);
|
|
199
|
+
writeFileSync(modeFilePath(root), `${mode}
|
|
200
|
+
`);
|
|
201
|
+
coreFacade.policy.refreshPolicyBaselines(root);
|
|
202
|
+
return MODE_CONFIRMATION[mode];
|
|
203
|
+
}
|
|
204
|
+
function handoffJson(root) {
|
|
205
|
+
const file = coreFacade.handoff.readHandoffFile(root);
|
|
206
|
+
const providers = {};
|
|
207
|
+
for (const provider of Object.keys(file.by_provider)) {
|
|
208
|
+
providers[provider] = coreFacade.handoff.readHandoff(root, provider);
|
|
209
|
+
}
|
|
210
|
+
return { root, providers };
|
|
211
|
+
}
|
|
212
|
+
function handoffScreen(report) {
|
|
213
|
+
const names = Object.keys(report.providers).sort();
|
|
214
|
+
if (names.length === 0) {
|
|
215
|
+
return {
|
|
216
|
+
title: "handoff",
|
|
217
|
+
summary: [report.root],
|
|
218
|
+
sections: [{ lines: ["nothing recorded yet — this is a fresh start, not a missing file"] }]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const sections = [];
|
|
222
|
+
for (const name of names) {
|
|
223
|
+
const slice = report.providers[name];
|
|
224
|
+
if (!slice) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const rows = [];
|
|
228
|
+
for (const [label, value, level] of [
|
|
229
|
+
["blockers", slice.blockers, "warn"],
|
|
230
|
+
["next", slice.next_action, "info"],
|
|
231
|
+
["last gate", slice.last_gate_result, slice.last_gate_result === "pass" ? "ok" : "warn"],
|
|
232
|
+
["last failure", slice.last_failure_category, "fail"]
|
|
233
|
+
]) {
|
|
234
|
+
if (value) {
|
|
235
|
+
rows.push({ label, value: String(value), level });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const [label, list] of [
|
|
239
|
+
["in progress", slice.in_progress],
|
|
240
|
+
["pending", slice.pending],
|
|
241
|
+
["gaps", slice.previous_gaps?.map((gap) => gap.summary)]
|
|
242
|
+
]) {
|
|
243
|
+
if (list && list.length > 0) {
|
|
244
|
+
rows.push({ label, value: list.slice(0, 6).join(" | ") });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
sections.push({ title: `${name} (updated ${slice.updated_at})`, rows });
|
|
248
|
+
}
|
|
249
|
+
return { title: "handoff", summary: [report.root], sections };
|
|
250
|
+
}
|
|
251
|
+
function handoffText(report, style = PLAIN) {
|
|
252
|
+
return render(handoffScreen(report), style);
|
|
253
|
+
}
|
|
254
|
+
function attestScreen(root) {
|
|
255
|
+
const records = coreFacade.attest.readAttestations(root);
|
|
256
|
+
const verdict = coreFacade.attest.verifyChain(records);
|
|
257
|
+
const head = verdict.ok ? { label: "chain", value: `attestation chain OK — ${verdict.length} session(s)`, level: "ok" } : {
|
|
258
|
+
label: "chain",
|
|
259
|
+
value: `attestation chain BROKEN at record ${verdict.brokenAt} (${verdict.reason})`,
|
|
260
|
+
level: "fail"
|
|
261
|
+
};
|
|
262
|
+
if (records.length === 0) {
|
|
263
|
+
return {
|
|
264
|
+
title: "attestation",
|
|
265
|
+
summary: [root],
|
|
266
|
+
sections: [{ rows: [head] }, { lines: ["no sessions recorded yet"] }]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const sections = [{ rows: [head] }];
|
|
270
|
+
for (const record of records.slice(-10).reverse()) {
|
|
271
|
+
const rules = Object.entries(record.decisionsByRule).map(([rule, count]) => `${rule}=${count}`).join(" ");
|
|
272
|
+
sections.push({
|
|
273
|
+
title: `${record.ts} ${record.provider}/${record.session}`,
|
|
274
|
+
rows: [
|
|
275
|
+
{
|
|
276
|
+
label: "policy",
|
|
277
|
+
value: `${record.policyFingerprint}${record.policyDiverged ? " (DIVERGED mid-session)" : ""}`,
|
|
278
|
+
level: record.policyDiverged ? "warn" : "ok"
|
|
279
|
+
},
|
|
280
|
+
{ label: "rails", value: record.railsActive.join(", ") || "none" },
|
|
281
|
+
{
|
|
282
|
+
label: "gates",
|
|
283
|
+
value: `${record.gates.pass} pass / ${record.gates.fail} fail${rules ? ` | ${rules}` : ""}`
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
title: "attestation",
|
|
290
|
+
summary: [root],
|
|
291
|
+
sections,
|
|
292
|
+
footer: "chained, not signed — it detects a rewritten record and proves nothing about authorship"
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function attestText(root, style = PLAIN) {
|
|
296
|
+
return render(attestScreen(root), style);
|
|
297
|
+
}
|
|
298
|
+
function attestJson(root) {
|
|
299
|
+
const records = coreFacade.attest.readAttestations(root);
|
|
300
|
+
const verdict = coreFacade.attest.verifyChain(records);
|
|
301
|
+
return verdict.ok ? { ok: true, sessions: verdict.length, records } : { ok: false, brokenAt: verdict.brokenAt, reason: verdict.reason, sessions: records.length, records };
|
|
302
|
+
}
|
|
303
|
+
function acceptPolicy(root, paths, interactive) {
|
|
304
|
+
if (!interactive) {
|
|
305
|
+
throw new UsageError("tlc harness policy accept needs an interactive terminal — clearing a policy divergence is the operator's call, not a script's.");
|
|
306
|
+
}
|
|
307
|
+
const requested = paths.includes("--all") ? coreFacade.policy.allDivergedPaths(root) : paths;
|
|
308
|
+
if (paths.includes("--all") && requested.length === 0) {
|
|
309
|
+
return `nothing to accept — no policy source diverged in ${root}`;
|
|
310
|
+
}
|
|
311
|
+
if (requested.length === 0) {
|
|
312
|
+
throw new UsageError([
|
|
313
|
+
"usage: tlc harness policy accept <path> [path...]",
|
|
314
|
+
" tlc harness policy accept --all accept everything `tlc harness policy` lists here"
|
|
315
|
+
].join(`
|
|
316
|
+
`));
|
|
317
|
+
}
|
|
318
|
+
const blocked = coreFacade.policy.allDivergedPaths(root);
|
|
319
|
+
const notHere = requested.filter((path) => !blocked.includes(path));
|
|
320
|
+
const outcome = coreFacade.policy.acceptPolicySources(root, requested);
|
|
321
|
+
if (outcome.kind === "not-a-source") {
|
|
322
|
+
throw new UsageError([
|
|
323
|
+
`not a policy source: ${outcome.paths.join(", ")}`,
|
|
324
|
+
"The sources the loader reads are:",
|
|
325
|
+
...outcome.sources.map((source) => ` ${source}`)
|
|
326
|
+
].join(`
|
|
327
|
+
`));
|
|
328
|
+
}
|
|
329
|
+
if (outcome.kind === "nothing-to-accept") {
|
|
330
|
+
return [
|
|
331
|
+
`nothing to accept — ${root} has no recorded session baseline.`,
|
|
332
|
+
"Acceptance is written per project. Run this from the repository whose session is blocked:",
|
|
333
|
+
` cd <that repo> && tlc harness policy accept ${requested.join(" ")}`
|
|
334
|
+
].join(`
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
const lines = [
|
|
338
|
+
`accepted: ${outcome.paths.join(", ")}`,
|
|
339
|
+
` for sessions in ${root} — acceptance is per project, not machine-wide`
|
|
340
|
+
];
|
|
341
|
+
if (notHere.length > 0) {
|
|
342
|
+
lines.push(` note: ${notHere.join(", ")} was not diverging here. If a session elsewhere is blocked, run this in that repository too.`);
|
|
343
|
+
}
|
|
344
|
+
return lines.join(`
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
function policyScreen(root) {
|
|
348
|
+
const diverged = coreFacade.policy.allDivergedPaths(root);
|
|
349
|
+
if (diverged.length === 0) {
|
|
350
|
+
return {
|
|
351
|
+
title: "policy baseline",
|
|
352
|
+
sections: [
|
|
353
|
+
{
|
|
354
|
+
rows: [
|
|
355
|
+
{
|
|
356
|
+
label: "baseline",
|
|
357
|
+
value: "matches — nothing changed out of band during any live session",
|
|
358
|
+
level: "ok"
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
title: "policy baseline",
|
|
367
|
+
summary: [`policy changed out of band during a live session (${diverged.length})`],
|
|
368
|
+
sections: [
|
|
369
|
+
{ rows: diverged.map((path) => ({ label: "changed", value: path, level: "warn" })) },
|
|
370
|
+
{
|
|
371
|
+
title: "If that was you, accept it from your own terminal with",
|
|
372
|
+
lines: [`tlc harness policy accept ${diverged.join(" ")}`, "", "or: tlc harness policy accept --all"]
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
footer: "accepting is per path, so anything you leave out keeps blocking"
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function policyText(root, style = PLAIN) {
|
|
379
|
+
return render(policyScreen(root), style);
|
|
380
|
+
}
|
|
381
|
+
function policyJson(root) {
|
|
382
|
+
const diverged = coreFacade.policy.allDivergedPaths(root);
|
|
383
|
+
return { diverged, ok: diverged.length === 0 };
|
|
384
|
+
}
|
|
385
|
+
function upstreamRef(dest) {
|
|
386
|
+
const read = (args) => {
|
|
387
|
+
const r = spawnSync("git", ["-C", dest, ...args], { encoding: "utf8", env: process.env });
|
|
388
|
+
return (r.status ?? 1) === 0 ? (r.stdout ?? "").trim() : "";
|
|
389
|
+
};
|
|
390
|
+
const tracked = read(["rev-parse", "--abbrev-ref", "@{u}"]);
|
|
391
|
+
if (tracked !== "") {
|
|
392
|
+
return tracked;
|
|
393
|
+
}
|
|
394
|
+
return `origin/${read(["rev-parse", "--abbrev-ref", "HEAD"]) || "main"}`;
|
|
395
|
+
}
|
|
396
|
+
var NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
|
|
397
|
+
var NPM_MARKER = "installed-from-npm";
|
|
398
|
+
function classifyRuntimePath(dest, probe) {
|
|
399
|
+
if (probe.isSymlink(dest)) {
|
|
400
|
+
return "linked";
|
|
401
|
+
}
|
|
402
|
+
if (!probe.exists(dest)) {
|
|
403
|
+
return "absent";
|
|
404
|
+
}
|
|
405
|
+
if (probe.exists(join2(dest, ".git"))) {
|
|
406
|
+
return "managed";
|
|
407
|
+
}
|
|
408
|
+
return probe.exists(join2(dest, NPM_MARKER)) ? "npm" : "unmanaged";
|
|
409
|
+
}
|
|
410
|
+
function runtimePathKind(dest) {
|
|
411
|
+
return classifyRuntimePath(dest, {
|
|
412
|
+
isSymlink: (path) => {
|
|
413
|
+
try {
|
|
414
|
+
return lstatSync2(path).isSymbolicLink();
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
exists: existsSync2
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function missingBundles(dest) {
|
|
423
|
+
const entrypoints = join2(dest, "src", "entrypoints");
|
|
424
|
+
if (!existsSync2(entrypoints)) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
const expected = readdirSync(entrypoints).filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")).map((name) => `${name.slice(0, -3)}.mjs`);
|
|
428
|
+
return expected.filter((bundle) => !existsSync2(join2(dest, "dist", bundle)));
|
|
429
|
+
}
|
|
430
|
+
function linkedRuntimeMessage(dest, target) {
|
|
431
|
+
return [
|
|
432
|
+
`update: ${dest} is a link to a working clone${target ? ` → ${target}` : ""}.`,
|
|
433
|
+
"Nothing in it is touched by this command — updating that clone is your own `git pull`.",
|
|
434
|
+
"Refreshing the machine-local parts only: CLI link, init skill, provider hooks."
|
|
435
|
+
].join(`
|
|
436
|
+
`);
|
|
437
|
+
}
|
|
438
|
+
function fetchFailureMessage(dest) {
|
|
439
|
+
return [
|
|
440
|
+
`update: git fetch failed in ${dest}.`,
|
|
441
|
+
` The published package needs no clone: npm i -g ${NPM_PACKAGE}@latest, then \`tlc harness install\`.`,
|
|
442
|
+
" If this runtime predates the move to tech-leads-club/harness-toolkit, it is still pointing at the old",
|
|
443
|
+
" repository, and that install is what the package replaces.",
|
|
444
|
+
" For a private fork, this needs a GitHub credential: `gh auth login`, then `gh auth setup-git`."
|
|
445
|
+
].join(`
|
|
446
|
+
`);
|
|
447
|
+
}
|
|
448
|
+
function unmanagedRuntimeMessage(dest) {
|
|
449
|
+
return [
|
|
450
|
+
`update: ${dest} is not a git checkout, so there is nothing to pull.`,
|
|
451
|
+
`Install the package to get a runtime update can move: npm i -g ${NPM_PACKAGE}@latest, then \`tlc harness install\`.`
|
|
452
|
+
].join(`
|
|
453
|
+
`);
|
|
454
|
+
}
|
|
455
|
+
function npmUpdateFailureMessage() {
|
|
456
|
+
return [
|
|
457
|
+
`update: npm could not install ${NPM_PACKAGE}@latest.`,
|
|
458
|
+
" permissions — a global prefix owned by root needs sudo, or an npm prefix you own:",
|
|
459
|
+
" npm config set prefix ~/.local",
|
|
460
|
+
" not found — the package is published; check the network and any registry proxy in ~/.npmrc",
|
|
461
|
+
" offline — nothing was changed; the runtime you have still works."
|
|
462
|
+
].join(`
|
|
463
|
+
`);
|
|
464
|
+
}
|
|
465
|
+
function resetFailureMessage(dest, mergeRef, gitOutput) {
|
|
466
|
+
return [
|
|
467
|
+
`update: could not move the runtime to ${mergeRef}.`,
|
|
468
|
+
` path: ${dest} (managed checkout)`,
|
|
469
|
+
gitOutput.trim() ? ` git: ${gitOutput.trim().split(`
|
|
470
|
+
`).slice(-3).join(" / ")}` : "",
|
|
471
|
+
`Nothing was changed. If this persists, install the package instead: npm i -g ${NPM_PACKAGE}@latest, then \`tlc harness install\`.`
|
|
472
|
+
].filter(Boolean).join(`
|
|
473
|
+
`);
|
|
474
|
+
}
|
|
475
|
+
function runtimeRevision(dest) {
|
|
476
|
+
if (!existsSync2(join2(dest, ".git"))) {
|
|
477
|
+
return { revision: null, date: null };
|
|
478
|
+
}
|
|
479
|
+
const read = (args) => {
|
|
480
|
+
const r = spawnSync("git", ["-C", dest, ...args], { encoding: "utf8", env: process.env });
|
|
481
|
+
const out = (r.stdout ?? "").trim();
|
|
482
|
+
return (r.status ?? 1) === 0 && out !== "" ? out : null;
|
|
483
|
+
};
|
|
484
|
+
return { revision: read(["rev-parse", "--short", "HEAD"]), date: read(["log", "-1", "--format=%cs"]) };
|
|
485
|
+
}
|
|
486
|
+
function versionJson(root) {
|
|
487
|
+
const dest = resolveHarnessRoot();
|
|
488
|
+
const { revision, date } = runtimeRevision(dest);
|
|
489
|
+
return {
|
|
490
|
+
runtime: dest,
|
|
491
|
+
revision,
|
|
492
|
+
date,
|
|
493
|
+
seenRevision: coreFacade.release.readReleaseSeen(root)?.revision ?? null
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function versionScreen(root) {
|
|
497
|
+
const report = versionJson(root);
|
|
498
|
+
const rows = report.revision === null ? [
|
|
499
|
+
{ label: "runtime", value: report.runtime },
|
|
500
|
+
{
|
|
501
|
+
label: "revision",
|
|
502
|
+
value: "unknown — the runtime path is not a git checkout, so `update` cannot pull either",
|
|
503
|
+
level: "warn"
|
|
504
|
+
}
|
|
505
|
+
] : [
|
|
506
|
+
{ label: "runtime", value: report.runtime },
|
|
507
|
+
{ label: "revision", value: `${report.revision} (${report.date ?? "date unknown"})`, level: "ok" },
|
|
508
|
+
{
|
|
509
|
+
label: "project last saw",
|
|
510
|
+
value: report.seenRevision ?? "nothing yet — the next update will announce what landed"
|
|
511
|
+
}
|
|
512
|
+
];
|
|
513
|
+
return { title: "harness version", sections: [{ rows }] };
|
|
514
|
+
}
|
|
515
|
+
function versionText(root, style = PLAIN) {
|
|
516
|
+
return render(versionScreen(root), style);
|
|
517
|
+
}
|
|
518
|
+
function pendingUpdate(dest, mergeRef) {
|
|
519
|
+
if (!existsSync2(join2(dest, ".git"))) {
|
|
520
|
+
return { ok: false, reason: "the runtime path is not a git checkout", commits: 0, decisions: [] };
|
|
521
|
+
}
|
|
522
|
+
const fetch = spawnSync("git", ["-C", dest, "fetch", "origin"], { stdio: "inherit", env: process.env });
|
|
523
|
+
if ((fetch.status ?? 1) !== 0) {
|
|
524
|
+
return { ok: false, reason: "git fetch failed", commits: 0, decisions: [] };
|
|
525
|
+
}
|
|
526
|
+
const count = spawnSync("git", ["-C", dest, "rev-list", "--count", `HEAD..${mergeRef}`], {
|
|
527
|
+
encoding: "utf8",
|
|
528
|
+
env: process.env
|
|
529
|
+
});
|
|
530
|
+
const commits = Number.parseInt((count.stdout ?? "0").trim(), 10) || 0;
|
|
531
|
+
const added = spawnSync("git", ["-C", dest, "diff", "--name-only", "--diff-filter=A", `HEAD..${mergeRef}`, "--", "docs/decisions"], { encoding: "utf8", env: process.env });
|
|
532
|
+
const files = (added.stdout ?? "").split(`
|
|
533
|
+
`).map((line) => line.trim().split("/").pop() ?? "").filter(Boolean);
|
|
534
|
+
return { ok: true, commits, decisions: coreFacade.release.readDecisions(dest, files) };
|
|
535
|
+
}
|
|
536
|
+
function pendingScreen(report) {
|
|
537
|
+
if (!report.ok) {
|
|
538
|
+
return {
|
|
539
|
+
title: "update --check",
|
|
540
|
+
sections: [
|
|
541
|
+
{
|
|
542
|
+
rows: [{ label: "status", value: `${report.reason} — nothing to compare against`, level: "warn" }]
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (report.commits === 0) {
|
|
548
|
+
return {
|
|
549
|
+
title: "update --check",
|
|
550
|
+
sections: [
|
|
551
|
+
{ rows: [{ label: "status", value: "the runtime is current — nothing to pull", level: "ok" }] }
|
|
552
|
+
]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const digest = coreFacade.release.formatDecisionDigest(report.decisions);
|
|
556
|
+
return {
|
|
557
|
+
title: "update --check",
|
|
558
|
+
summary: [`${report.commits} commit(s) would be pulled`, "Nothing has changed yet."],
|
|
559
|
+
sections: [{ lines: digest === "" ? ["no decisions landed in that range"] : digest.split(`
|
|
560
|
+
`) }]
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function pendingText(report, style = PLAIN) {
|
|
564
|
+
return render(pendingScreen(report), style);
|
|
565
|
+
}
|
|
566
|
+
var GATE_FIELDS = {
|
|
567
|
+
"test-command": "test",
|
|
568
|
+
"lint-command": "lint"
|
|
569
|
+
};
|
|
570
|
+
var EXECUTABLE_EXTENSIONS = ["", ".exe", ".cmd", ".bat", ".ps1"];
|
|
571
|
+
function resolveExecutable(name, env = process.env) {
|
|
572
|
+
const candidates = (base) => EXECUTABLE_EXTENSIONS.map((ext) => `${base}${ext}`);
|
|
573
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
574
|
+
return candidates(name).find((candidate) => existsSync2(candidate)) ?? null;
|
|
575
|
+
}
|
|
576
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
577
|
+
if (!dir) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const found = candidates(join2(dir, name)).find((candidate) => existsSync2(candidate));
|
|
581
|
+
if (found) {
|
|
582
|
+
return found;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
function setGateCommand(root, field, argv, interactive) {
|
|
588
|
+
if (argv.length === 0) {
|
|
589
|
+
throw new UsageError(`usage: tlc harness gate ${field}-command <command> [args...]`);
|
|
590
|
+
}
|
|
591
|
+
if (!interactive) {
|
|
592
|
+
throw new UsageError(`tlc harness gate ${field}-command needs an interactive terminal — harness policy is the operator's to set, not a script's.`);
|
|
593
|
+
}
|
|
594
|
+
const binary = argv[0];
|
|
595
|
+
if (resolveExecutable(binary) === null) {
|
|
596
|
+
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)).`);
|
|
597
|
+
}
|
|
598
|
+
const path = projectConfigPath(root);
|
|
599
|
+
const parsed = existsSync2(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
600
|
+
const grind = { ...parsed.grind ?? {} };
|
|
601
|
+
grind[field === "test" ? "testCommand" : "lintCommand"] = argv;
|
|
602
|
+
parsed.grind = grind;
|
|
603
|
+
mkdirSync2(join2(root, ".tlc", "harness"), { recursive: true });
|
|
604
|
+
writeFileSync(path, `${JSON.stringify(parsed, null, 2)}
|
|
605
|
+
`, "utf8");
|
|
606
|
+
coreFacade.policy.refreshPolicyBaselines(root);
|
|
607
|
+
return `grind.${field}Command = ${JSON.stringify(argv)}`;
|
|
608
|
+
}
|
|
609
|
+
function helpScreen() {
|
|
610
|
+
return {
|
|
611
|
+
title: "tlc harness",
|
|
612
|
+
sections: [
|
|
613
|
+
{
|
|
614
|
+
lines: `Requires Node.js 24+ (Active LTS 24 or Current 26).
|
|
615
|
+
|
|
616
|
+
Read commands accept --json: status, doctor, obs, lessons, prices lookup, attest, policy.
|
|
617
|
+
|
|
618
|
+
QUICK
|
|
619
|
+
tlc harness status mode / grind / gates
|
|
620
|
+
tlc harness version runtime revision, and what this project last saw
|
|
621
|
+
tlc harness update --check what an update would pull, without pulling it
|
|
622
|
+
tlc harness update pull runtime + refresh skill/CLI, then doctor
|
|
623
|
+
tlc harness doctor health checklist
|
|
624
|
+
tlc harness why [n] the last n decisions this tool made, with the rule behind each
|
|
625
|
+
tlc harness install put the runtime in place from the installed npm package
|
|
626
|
+
tlc harness uninstall print what would be undone; --yes applies it, --purge includes state
|
|
627
|
+
tlc harness build compile dist/ for Node
|
|
628
|
+
tlc harness test run the full local gate
|
|
629
|
+
tlc harness help <topic> documentation
|
|
630
|
+
|
|
631
|
+
TOPICS
|
|
632
|
+
architecture | concepts | lessons | measure | prices | diagnose | init
|
|
633
|
+
|
|
634
|
+
CONTROL
|
|
635
|
+
tlc harness grind [on|off] tlc harness pause | resume tlc harness mode solo|paired|focus
|
|
636
|
+
tlc harness gate test-command <cmd> [args...] tlc harness gate lint-command <cmd> [args...]
|
|
637
|
+
tlc harness attest tamper-evident record of what each session ran under
|
|
638
|
+
tlc harness policy show a policy that changed out of band; accept <path> to clear it
|
|
639
|
+
|
|
640
|
+
MEASURE
|
|
641
|
+
tlc harness obs live|events|report|prune
|
|
642
|
+
tlc harness prices refresh [all|cursor|litellm]
|
|
643
|
+
tlc harness prices lookup <model-id>
|
|
644
|
+
tlc harness lessons list|show|garden|sync-rules
|
|
645
|
+
|
|
646
|
+
PROJECT
|
|
647
|
+
tlc harness init --minimal | tlc harness init --write --stdin-json`.split(`
|
|
648
|
+
`)
|
|
649
|
+
}
|
|
650
|
+
],
|
|
651
|
+
footer: "tlc harness help <topic> for a document · tlc harness why to see what it decided"
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function helpText(style = PLAIN) {
|
|
655
|
+
return render(helpScreen(), style);
|
|
656
|
+
}
|
|
657
|
+
function pricesHelpScreen() {
|
|
658
|
+
return {
|
|
659
|
+
title: "prices",
|
|
660
|
+
sections: [
|
|
661
|
+
{
|
|
662
|
+
lines: ` tlc harness prices refresh [all|cursor|litellm] [--if-stale]
|
|
663
|
+
tlc harness prices lookup <model-id>
|
|
664
|
+
|
|
665
|
+
refresh / refresh all both planes of model-prices.json
|
|
666
|
+
refresh cursor the provider's own rates
|
|
667
|
+
refresh litellm the vendors' list prices
|
|
668
|
+
--if-stale fetch only past the 7-day TTL
|
|
669
|
+
lookup <model-id> catalog key, pool, USD for 1M in + 1M out
|
|
670
|
+
|
|
671
|
+
Catalogue: <runtime home>/model-prices.json — fetched per machine, never versioned
|
|
672
|
+
Overrides: <runtime home>/model-prices.local.json — yours, hand-written
|
|
673
|
+
Documentation: tlc harness help prices`.split(`
|
|
674
|
+
`)
|
|
675
|
+
}
|
|
676
|
+
],
|
|
677
|
+
footer: "resolution: your overrides → the asking provider's plane → the vendor plane → null"
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function pricesHelpText(style = PLAIN) {
|
|
681
|
+
return render(pricesHelpScreen(), style);
|
|
682
|
+
}
|
|
683
|
+
function resolveHarnessRoot() {
|
|
684
|
+
const home = runtimeHome();
|
|
685
|
+
try {
|
|
686
|
+
return realpathSync(home);
|
|
687
|
+
} catch {
|
|
688
|
+
return home;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function wireRuntime(dest, home) {
|
|
692
|
+
const lines = [];
|
|
693
|
+
const seeded = seedConfig(dest);
|
|
694
|
+
if (seeded.seeded) {
|
|
695
|
+
lines.push(`config seeded → ${seeded.path}`);
|
|
696
|
+
}
|
|
697
|
+
if (!existsSync2(join2(dest, "skills", "harness-init"))) {
|
|
698
|
+
return { lines, missingSkill: true };
|
|
699
|
+
}
|
|
700
|
+
const links = coreFacade.skill.skillLinks(dest, providerConfigDirs(), existsSync2);
|
|
701
|
+
if (links.length === 0) {
|
|
702
|
+
lines.push("no provider config dir found — skill not linked");
|
|
703
|
+
}
|
|
704
|
+
for (const link of links) {
|
|
705
|
+
const outcome = linkDir(link.source, link.target);
|
|
706
|
+
lines.push(outcome.kind === "refused" ? `skill not linked — ${outcome.reason}` : `skill → ${outcome.target}`);
|
|
707
|
+
}
|
|
708
|
+
const hooks = spawnSync(process.execPath, [join2(dest, "bin", "write-user-hooks.mjs")], {
|
|
709
|
+
stdio: "inherit",
|
|
710
|
+
env: { ...process.env, TLC_HOME: home }
|
|
711
|
+
});
|
|
712
|
+
if ((hooks.status ?? 1) !== 0) {
|
|
713
|
+
lines.push("hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
|
|
714
|
+
}
|
|
715
|
+
return { lines, missingSkill: false };
|
|
716
|
+
}
|
|
717
|
+
function execBinPath() {
|
|
718
|
+
return join2(resolveHarnessRoot(), "bin", "tlc-exec.mjs");
|
|
719
|
+
}
|
|
720
|
+
function buildBinPath() {
|
|
721
|
+
return join2(resolveHarnessRoot(), "bin", "tlc-build.mjs");
|
|
722
|
+
}
|
|
723
|
+
function route(args) {
|
|
724
|
+
const cmd = (args[0] ?? "status").toLowerCase();
|
|
725
|
+
switch (cmd) {
|
|
726
|
+
case "status":
|
|
727
|
+
case "st":
|
|
728
|
+
case "s":
|
|
729
|
+
return { kind: "status" };
|
|
730
|
+
case "build":
|
|
731
|
+
case "rebuild":
|
|
732
|
+
return { kind: "build" };
|
|
733
|
+
case "update":
|
|
734
|
+
case "upgrade": {
|
|
735
|
+
const flags = args.slice(1);
|
|
736
|
+
if (flags.includes("--check")) {
|
|
737
|
+
return { kind: "update-check" };
|
|
738
|
+
}
|
|
739
|
+
const leftover = unknownFlags(flags);
|
|
740
|
+
if (leftover.length > 0) {
|
|
741
|
+
throw new UsageError(leftover[0] === "--force" ? `update takes no --force: a managed runtime is already reset to upstream, and a linked clone is never written to. If update cannot move it, install the package instead: npm i -g ${NPM_PACKAGE}@latest.` : `unknown flag: ${leftover[0]}
|
|
742
|
+
usage: tlc harness update [--check]`);
|
|
743
|
+
}
|
|
744
|
+
return { kind: "update" };
|
|
745
|
+
}
|
|
746
|
+
case "version":
|
|
747
|
+
case "--version":
|
|
748
|
+
return { kind: "version" };
|
|
749
|
+
case "test":
|
|
750
|
+
return { kind: "test" };
|
|
751
|
+
case "grind":
|
|
752
|
+
case "g": {
|
|
753
|
+
const arg = (args[1] ?? "on").toLowerCase();
|
|
754
|
+
if (arg === "on" || arg === "1" || arg === "true") {
|
|
755
|
+
return { kind: "grind", on: true };
|
|
756
|
+
}
|
|
757
|
+
if (arg === "off" || arg === "0" || arg === "false") {
|
|
758
|
+
return { kind: "grind", on: false };
|
|
759
|
+
}
|
|
760
|
+
throw new UsageError("usage: tlc harness grind [on|off]");
|
|
761
|
+
}
|
|
762
|
+
case "pause":
|
|
763
|
+
case "p":
|
|
764
|
+
return { kind: "pause" };
|
|
765
|
+
case "resume":
|
|
766
|
+
case "r":
|
|
767
|
+
return { kind: "resume" };
|
|
768
|
+
case "mode":
|
|
769
|
+
case "m": {
|
|
770
|
+
const modeArg = args[1];
|
|
771
|
+
if (!modeArg) {
|
|
772
|
+
throw new UsageError("usage: tlc harness mode <solo|paired|focus>");
|
|
773
|
+
}
|
|
774
|
+
return { kind: "mode", value: modeArg };
|
|
775
|
+
}
|
|
776
|
+
case "attest":
|
|
777
|
+
return { kind: "attest" };
|
|
778
|
+
case "handoff":
|
|
779
|
+
return { kind: "handoff" };
|
|
780
|
+
case "policy": {
|
|
781
|
+
const sub = (args[1] ?? "").toLowerCase();
|
|
782
|
+
if (!sub) {
|
|
783
|
+
return { kind: "policy", accept: [] };
|
|
784
|
+
}
|
|
785
|
+
if (sub !== "accept") {
|
|
786
|
+
throw new UsageError("usage: tlc harness policy [accept <path> [path...]]");
|
|
787
|
+
}
|
|
788
|
+
return { kind: "policy", accept: args.slice(2) };
|
|
789
|
+
}
|
|
790
|
+
case "gate": {
|
|
791
|
+
const field = GATE_FIELDS[(args[1] ?? "").toLowerCase()];
|
|
792
|
+
if (!field) {
|
|
793
|
+
throw new UsageError("usage: tlc harness gate <test-command|lint-command> <command> [args...]");
|
|
794
|
+
}
|
|
795
|
+
return { kind: "gate", field, argv: args.slice(2) };
|
|
796
|
+
}
|
|
797
|
+
case "prices": {
|
|
798
|
+
const sub = (args[1] ?? "").toLowerCase();
|
|
799
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
|
|
800
|
+
return { kind: "prices-help" };
|
|
801
|
+
}
|
|
802
|
+
if (sub === "refresh") {
|
|
803
|
+
return { kind: "prices-refresh", scope: args[2] ?? "all" };
|
|
804
|
+
}
|
|
805
|
+
if (sub === "lookup" || sub === "get") {
|
|
806
|
+
const modelId = args[2];
|
|
807
|
+
if (!modelId) {
|
|
808
|
+
throw new UsageError(`usage: tlc harness prices lookup <model-id>
|
|
809
|
+
detail: tlc harness help prices`);
|
|
810
|
+
}
|
|
811
|
+
return { kind: "prices-lookup", modelId };
|
|
812
|
+
}
|
|
813
|
+
throw new UsageError(`usage: tlc harness prices refresh [all|cursor|litellm] | tlc harness prices lookup <model>
|
|
814
|
+
detail: tlc harness help prices`);
|
|
815
|
+
}
|
|
816
|
+
case "obs":
|
|
817
|
+
case "o":
|
|
818
|
+
return { kind: "entry", entry: "obs-cli", args: args.slice(1) };
|
|
819
|
+
case "doctor":
|
|
820
|
+
case "doc":
|
|
821
|
+
return { kind: "entry", entry: "doctor", args: args.slice(1) };
|
|
822
|
+
case "lessons":
|
|
823
|
+
case "lesson":
|
|
824
|
+
return { kind: "entry", entry: "lessons-cli", args: args.slice(1) };
|
|
825
|
+
case "init":
|
|
826
|
+
return { kind: "entry", entry: "init-project", args: args.slice(1) };
|
|
827
|
+
case "install":
|
|
828
|
+
return { kind: "entry", entry: "install-runtime", args: args.slice(1) };
|
|
829
|
+
case "uninstall":
|
|
830
|
+
return { kind: "entry", entry: "uninstall-runtime", args: args.slice(1) };
|
|
831
|
+
case "why":
|
|
832
|
+
return { kind: "entry", entry: "obs-cli", args: ["why", ...args.slice(1)] };
|
|
833
|
+
case "help":
|
|
834
|
+
case "-h":
|
|
835
|
+
case "--help": {
|
|
836
|
+
const topic = args[1];
|
|
837
|
+
if (!topic) {
|
|
838
|
+
return { kind: "help" };
|
|
839
|
+
}
|
|
840
|
+
return { kind: "entry", entry: "help-topic", args: [topic] };
|
|
841
|
+
}
|
|
842
|
+
default:
|
|
843
|
+
return { kind: "unknown", cmd };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
var TEST_ENV_IMPORT = ["--import", "./tools/test-env.mjs"];
|
|
847
|
+
function buildTestSteps() {
|
|
848
|
+
return [
|
|
849
|
+
{ label: "biome check", bin: "npx", args: ["biome", "check", "--error-on-warnings"] },
|
|
850
|
+
{ label: "tsc --noEmit", bin: "npx", args: ["tsc", "--noEmit"] },
|
|
851
|
+
{ label: "src suite", bin: "node", args: [...TEST_ENV_IMPORT, "--test", "src/**/__test__/*.test.ts"] },
|
|
852
|
+
{ label: "tools suite", bin: "node", args: [...TEST_ENV_IMPORT, "--test", "tools/__test__/*.test.ts"] },
|
|
853
|
+
{ label: "check-boundaries", bin: "node", args: ["tools/dev/check-boundaries.ts"] },
|
|
854
|
+
{ label: "check-suppressions", bin: "node", args: ["tools/dev/check-suppressions.ts"] },
|
|
855
|
+
{ label: "check-wiring", bin: "node", args: ["tools/dev/check-wiring.ts"] },
|
|
856
|
+
{ label: "check-docs-bundle", bin: "node", args: ["tools/dev/check-docs-bundle.ts"] },
|
|
857
|
+
{ label: "check-decisions", bin: "node", args: ["tools/dev/check-decisions.ts"] },
|
|
858
|
+
{ label: "check-screens", bin: "node", args: ["tools/dev/check-screens.ts"] },
|
|
859
|
+
{ label: "check-obs-contract", bin: "node", args: ["tools/dev/check-obs-contract.ts"] },
|
|
860
|
+
{ label: "check-manifest", bin: "node", args: ["tools/dev/check-manifest.ts"] },
|
|
861
|
+
{ label: "capabilities in sync", bin: "node", args: ["tools/dev/render-capabilities.ts", "--check"] },
|
|
862
|
+
{ label: "changelog in sync", bin: "node", args: ["tools/dev/render-changelog.ts", "--check"] },
|
|
863
|
+
{ label: "log in sync", bin: "node", args: ["tools/dev/render-log.ts", "--check"] },
|
|
864
|
+
{ label: "coverage in sync", bin: "node", args: ["tools/dev/render-coverage.ts", "--check"] }
|
|
865
|
+
];
|
|
866
|
+
}
|
|
867
|
+
function runTestSteps(steps, cwd, spawner = (bin, spawnArgs, spawnCwd) => spawnSync(bin, spawnArgs, { cwd: spawnCwd, stdio: "inherit" })) {
|
|
868
|
+
for (const step of steps) {
|
|
869
|
+
console.log(`tlc harness test: running ${step.label}`);
|
|
870
|
+
const result = spawner(step.bin, step.args, cwd);
|
|
871
|
+
const status = result.status ?? 1;
|
|
872
|
+
if (status !== 0) {
|
|
873
|
+
console.error(`tlc harness test: FAILED at "${step.label}" (exit ${status})`);
|
|
874
|
+
return status;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
console.log("tlc harness test: all steps passed");
|
|
878
|
+
return 0;
|
|
879
|
+
}
|
|
880
|
+
function announceNewCapabilities(root, runtimeRoot) {
|
|
881
|
+
const catalog = coreFacade.capability.loadCatalog(runtimeRoot);
|
|
882
|
+
const policy = coreFacade.capability.readProjectPolicyRaw(root);
|
|
883
|
+
if (!catalog || !policy) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const seen = coreFacade.capability.readRuntimeSeen(root);
|
|
887
|
+
const fresh = coreFacade.capability.listNewlyAnnounceable(policy, catalog, seen.catalogVersion);
|
|
888
|
+
if (fresh.length === 0) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
console.log("");
|
|
892
|
+
console.log(coreFacade.capability.formatCapabilityDigest(fresh));
|
|
893
|
+
console.log("");
|
|
894
|
+
coreFacade.capability.writeRuntimeSeen(root, catalog.catalogVersion);
|
|
895
|
+
}
|
|
896
|
+
function announceLandedDecisions(root, dest, before) {
|
|
897
|
+
const now = runtimeRevision(dest).revision;
|
|
898
|
+
if (now === null) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const seen = coreFacade.release.readReleaseSeen(root)?.revision ?? before;
|
|
902
|
+
if (seen === null || seen === now) {
|
|
903
|
+
coreFacade.release.writeReleaseSeen(root, now);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const added = spawnSync("git", ["-C", dest, "diff", "--name-only", "--diff-filter=A", `${seen}..${now}`, "--", "docs/decisions"], { encoding: "utf8", env: process.env });
|
|
907
|
+
if ((added.status ?? 1) !== 0) {
|
|
908
|
+
console.log(`update: cannot list what landed since ${seen} — that revision is no longer in the checkout`);
|
|
909
|
+
coreFacade.release.writeReleaseSeen(root, now);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const files = (added.stdout ?? "").split(`
|
|
913
|
+
`).map((line) => line.trim().split("/").pop() ?? "").filter(Boolean);
|
|
914
|
+
const digest = coreFacade.release.formatDecisionDigest(coreFacade.release.readDecisions(dest, files));
|
|
915
|
+
if (digest !== "") {
|
|
916
|
+
console.log("");
|
|
917
|
+
console.log(digest);
|
|
918
|
+
console.log("");
|
|
919
|
+
}
|
|
920
|
+
coreFacade.release.writeReleaseSeen(root, now);
|
|
921
|
+
}
|
|
922
|
+
function runUpdate(root) {
|
|
923
|
+
const dest = resolveHarnessRoot();
|
|
924
|
+
const revisionBefore = runtimeRevision(dest).revision;
|
|
925
|
+
const home = runtimeHome();
|
|
926
|
+
console.log(`update: runtime → ${dest}`);
|
|
927
|
+
if (!existsSync2(join2(dest, "bin", "tlc-exec.mjs"))) {
|
|
928
|
+
console.error(`update: missing install at ${home}`);
|
|
929
|
+
console.error(`update: install once with \`npm i -g ${NPM_PACKAGE}\`, then \`tlc harness install\`, then retry.`);
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
const kind = runtimePathKind(home);
|
|
933
|
+
if (kind === "linked") {
|
|
934
|
+
console.log(linkedRuntimeMessage(home, dest === home ? null : dest));
|
|
935
|
+
} else if (kind === "npm") {
|
|
936
|
+
const bump = spawnSync("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
|
|
937
|
+
stdio: "inherit",
|
|
938
|
+
env: process.env,
|
|
939
|
+
shell: true
|
|
940
|
+
});
|
|
941
|
+
if ((bump.status ?? 1) !== 0) {
|
|
942
|
+
console.error(npmUpdateFailureMessage());
|
|
943
|
+
process.exit(bump.status ?? 1);
|
|
944
|
+
}
|
|
945
|
+
const sync = spawnSync(process.execPath, [execBinPath(), "install-runtime"], {
|
|
946
|
+
stdio: "inherit",
|
|
947
|
+
env: process.env
|
|
948
|
+
});
|
|
949
|
+
if ((sync.status ?? 1) !== 0) {
|
|
950
|
+
process.exit(sync.status ?? 1);
|
|
951
|
+
}
|
|
952
|
+
} else if (kind === "unmanaged") {
|
|
953
|
+
console.log(unmanagedRuntimeMessage(dest));
|
|
954
|
+
} else {
|
|
955
|
+
const fetch = spawnSync("git", ["-C", dest, "fetch", "origin"], {
|
|
956
|
+
stdio: "inherit",
|
|
957
|
+
env: process.env
|
|
958
|
+
});
|
|
959
|
+
if ((fetch.status ?? 1) !== 0) {
|
|
960
|
+
console.error(fetchFailureMessage(dest));
|
|
961
|
+
process.exit(fetch.status ?? 1);
|
|
962
|
+
}
|
|
963
|
+
const mergeRef = upstreamRef(dest);
|
|
964
|
+
const reset = spawnSync("git", ["-C", dest, "reset", "--hard", mergeRef], {
|
|
965
|
+
encoding: "utf8",
|
|
966
|
+
env: process.env
|
|
967
|
+
});
|
|
968
|
+
if ((reset.status ?? 1) !== 0) {
|
|
969
|
+
console.error(resetFailureMessage(dest, mergeRef, `${reset.stderr ?? ""}${reset.stdout ?? ""}`));
|
|
970
|
+
process.exit(reset.status ?? 1);
|
|
971
|
+
}
|
|
972
|
+
const after = runtimeRevision(dest).revision;
|
|
973
|
+
console.log(revisionBefore === after ? `update: runtime already at ${after ?? "unknown"} — nothing to move` : `update: runtime ${revisionBefore ?? "unknown"} → ${after ?? "unknown"}`);
|
|
974
|
+
}
|
|
975
|
+
const wired = wireRuntime(dest, home);
|
|
976
|
+
for (const line of wired.lines) {
|
|
977
|
+
console.log(`update: ${line}`);
|
|
978
|
+
}
|
|
979
|
+
if (wired.missingSkill) {
|
|
980
|
+
console.error(`update: missing skill at ${join2(dest, "skills", "harness-init")}`);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
const missing = missingBundles(dest);
|
|
984
|
+
if (missing.length === 0) {
|
|
985
|
+
console.log("update: dist/ complete — no rebuild, so the runtime path stays clean");
|
|
986
|
+
} else if (existsSync2(buildBinPath())) {
|
|
987
|
+
console.log(`update: ${missing.length} bundle(s) missing — building`);
|
|
988
|
+
const build = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
|
|
989
|
+
if ((build.status ?? 1) !== 0) {
|
|
990
|
+
console.log(`update: build failed — ${missing.length} bundle(s) still missing from dist/`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
announceNewCapabilities(root, dest);
|
|
994
|
+
announceLandedDecisions(root, dest, revisionBefore);
|
|
995
|
+
spawnSync(process.execPath, [execBinPath(), "refresh-model-prices", "all", "--if-stale"], {
|
|
996
|
+
stdio: "inherit",
|
|
997
|
+
env: { ...process.env, TLC_PROJECT_DIR: root }
|
|
998
|
+
});
|
|
999
|
+
console.log("update: running doctor…");
|
|
1000
|
+
const doctor = spawnSync(process.execPath, [execBinPath(), "doctor"], {
|
|
1001
|
+
stdio: "inherit",
|
|
1002
|
+
env: { ...process.env, TLC_PROJECT_DIR: root }
|
|
1003
|
+
});
|
|
1004
|
+
console.log("update: ok — reload if hooks/skill should refresh");
|
|
1005
|
+
process.exit(doctor.status ?? 0);
|
|
1006
|
+
}
|
|
1007
|
+
function runEntry(entry, toolArgs, root) {
|
|
1008
|
+
const r = spawnSync(process.execPath, [execBinPath(), entry, ...toolArgs], {
|
|
1009
|
+
stdio: "inherit",
|
|
1010
|
+
env: { ...process.env, TLC_PROJECT_DIR: root }
|
|
1011
|
+
});
|
|
1012
|
+
process.exit(r.status ?? 1);
|
|
1013
|
+
}
|
|
1014
|
+
function main(argv) {
|
|
1015
|
+
const root = resolveProjectRoot();
|
|
1016
|
+
const group = (argv[0] ?? "").toLowerCase();
|
|
1017
|
+
if (group !== "harness") {
|
|
1018
|
+
console.error(`unknown: ${argv[0] ?? ""}`);
|
|
1019
|
+
console.error("usage: tlc harness <status|doctor|help|grind|pause|resume|mode|obs|prices|lessons|init|update|test|build>");
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
const { json, rest: args } = takeJsonFlag(argv.slice(1));
|
|
1023
|
+
let action;
|
|
1024
|
+
try {
|
|
1025
|
+
action = route(args);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error instanceof UsageError) {
|
|
1028
|
+
console.error(error.message);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
switch (action.kind) {
|
|
1034
|
+
case "status": {
|
|
1035
|
+
const leftover = unknownFlags(args.slice(1));
|
|
1036
|
+
if (leftover.length > 0) {
|
|
1037
|
+
console.error(`unknown flag: ${leftover[0]}`);
|
|
1038
|
+
console.error("usage: tlc harness status [--json]");
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
if (json) {
|
|
1042
|
+
emitJson(statusJson(root));
|
|
1043
|
+
} else {
|
|
1044
|
+
console.log(statusText(root, createStyle()));
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
case "handoff": {
|
|
1049
|
+
const leftover = unknownFlags(args.slice(1));
|
|
1050
|
+
if (leftover.length > 0) {
|
|
1051
|
+
console.error(`unknown flag: ${leftover[0]}`);
|
|
1052
|
+
console.error("usage: tlc harness handoff [--json]");
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
const report = handoffJson(root);
|
|
1056
|
+
if (json) {
|
|
1057
|
+
emitJson(report);
|
|
1058
|
+
} else {
|
|
1059
|
+
console.log(handoffText(report));
|
|
1060
|
+
}
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
case "attest": {
|
|
1064
|
+
const leftover = unknownFlags(args.slice(1));
|
|
1065
|
+
if (leftover.length > 0) {
|
|
1066
|
+
console.error(`unknown flag: ${leftover[0]}`);
|
|
1067
|
+
console.error("usage: tlc harness attest [--json]");
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
const report = attestJson(root);
|
|
1071
|
+
if (json) {
|
|
1072
|
+
emitJson(report);
|
|
1073
|
+
} else {
|
|
1074
|
+
console.log(attestText(root, createStyle()));
|
|
1075
|
+
}
|
|
1076
|
+
process.exit(report.ok ? 0 : 1);
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case "policy": {
|
|
1080
|
+
if (action.accept.length === 0 && !args.includes("accept")) {
|
|
1081
|
+
if (json) {
|
|
1082
|
+
emitJson(policyJson(root));
|
|
1083
|
+
} else {
|
|
1084
|
+
console.log(policyText(root, createStyle()));
|
|
1085
|
+
}
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
console.log(acceptPolicy(root, action.accept, Boolean(process.stdin.isTTY)));
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (error instanceof UsageError) {
|
|
1092
|
+
console.error(error.message);
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case "help":
|
|
1100
|
+
console.log(helpText(createStyle()));
|
|
1101
|
+
break;
|
|
1102
|
+
case "build": {
|
|
1103
|
+
const r = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
|
|
1104
|
+
process.exit(r.status ?? 1);
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "version":
|
|
1108
|
+
if (json) {
|
|
1109
|
+
emitJson(versionJson(root));
|
|
1110
|
+
} else {
|
|
1111
|
+
console.log(versionText(root, createStyle()));
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
case "update-check": {
|
|
1115
|
+
const dest = resolveHarnessRoot();
|
|
1116
|
+
const report = pendingUpdate(dest, upstreamRef(dest));
|
|
1117
|
+
if (json) {
|
|
1118
|
+
emitJson(report);
|
|
1119
|
+
} else {
|
|
1120
|
+
console.log(pendingText(report, createStyle()));
|
|
1121
|
+
}
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
case "update":
|
|
1125
|
+
runUpdate(root);
|
|
1126
|
+
break;
|
|
1127
|
+
case "test": {
|
|
1128
|
+
const status = runTestSteps(buildTestSteps(), process.cwd());
|
|
1129
|
+
process.exit(status);
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
case "grind":
|
|
1133
|
+
console.log(setGrind(root, action.on));
|
|
1134
|
+
break;
|
|
1135
|
+
case "pause":
|
|
1136
|
+
console.log(setPaused(root, true));
|
|
1137
|
+
break;
|
|
1138
|
+
case "resume":
|
|
1139
|
+
console.log(setPaused(root, false));
|
|
1140
|
+
break;
|
|
1141
|
+
case "mode":
|
|
1142
|
+
try {
|
|
1143
|
+
console.log(setMode(root, action.value));
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (error instanceof UsageError) {
|
|
1146
|
+
console.error(error.message);
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
break;
|
|
1152
|
+
case "gate":
|
|
1153
|
+
try {
|
|
1154
|
+
console.log(setGateCommand(root, action.field, action.argv, process.stdin.isTTY === true));
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
if (error instanceof UsageError) {
|
|
1157
|
+
console.error(error.message);
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
break;
|
|
1163
|
+
case "prices-help":
|
|
1164
|
+
console.log(pricesHelpText(createStyle()));
|
|
1165
|
+
break;
|
|
1166
|
+
case "prices-refresh":
|
|
1167
|
+
runEntry("refresh-model-prices", [action.scope], root);
|
|
1168
|
+
break;
|
|
1169
|
+
case "prices-lookup":
|
|
1170
|
+
runEntry("price-lookup", json ? [action.modelId, JSON_FLAG] : [action.modelId], root);
|
|
1171
|
+
break;
|
|
1172
|
+
case "entry":
|
|
1173
|
+
runEntry(action.entry, json ? [...action.args, JSON_FLAG] : action.args, root);
|
|
1174
|
+
break;
|
|
1175
|
+
case "unknown":
|
|
1176
|
+
console.error(`unknown: ${action.cmd}`);
|
|
1177
|
+
console.log(helpText(createStyle()));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (__require.main == __require.module) {
|
|
1182
|
+
main(process.argv.slice(2));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export { linkDir, UsageError, resolveProjectRoot, modeFilePath, grindFlagPath, skipFlagPath, focusFlagPath, pairedFlagPath, ensureFlagsDir, readMode, grindOn, gatesPaused, acceptedModes, statusScreen, statusText, statusJson, setGrind, setPaused, setMode, handoffJson, handoffScreen, handoffText, attestScreen, attestText, attestJson, acceptPolicy, policyScreen, policyText, policyJson, upstreamRef, NPM_PACKAGE, NPM_MARKER, classifyRuntimePath, runtimePathKind, missingBundles, linkedRuntimeMessage, fetchFailureMessage, unmanagedRuntimeMessage, npmUpdateFailureMessage, resetFailureMessage, runtimeRevision, versionJson, versionScreen, versionText, pendingUpdate, pendingScreen, pendingText, resolveExecutable, setGateCommand, helpScreen, helpText, pricesHelpScreen, pricesHelpText, resolveHarnessRoot, wireRuntime, execBinPath, buildBinPath, route, TEST_ENV_IMPORT, buildTestSteps, runTestSteps };
|