@tech-leads-club/harness-toolkit 0.4.1 → 0.4.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/README.md +49 -11
- package/bin/tlc-cli.ts +45 -1
- package/dist/compact-before.mjs +73 -73
- package/dist/doctor.mjs +74 -74
- package/dist/help-topic.mjs +1 -1
- package/dist/init-project.mjs +78 -78
- package/dist/install-runtime.mjs +73 -73
- package/dist/lessons-cli.mjs +75 -75
- package/dist/obs-cli.mjs +73 -73
- package/dist/price-lookup.mjs +2 -2
- package/dist/prompt-submit.mjs +73 -73
- package/dist/refresh-model-prices.mjs +73 -73
- package/dist/response-after.mjs +73 -73
- package/dist/run.mjs +73 -73
- package/dist/session-end.mjs +78 -78
- package/dist/session-start.mjs +80 -80
- package/dist/shim.mjs +72 -72
- package/dist/stop.mjs +83 -83
- package/dist/subagent-start.mjs +73 -73
- package/dist/subagent-stop.mjs +74 -74
- package/dist/support.mjs +78 -78
- package/dist/tlc-cli.mjs +93 -93
- package/dist/tool-after.mjs +73 -73
- package/dist/tool-before.mjs +73 -73
- package/dist/tool-failure.mjs +73 -73
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/concepts.md +11 -0
- package/docs/log.md +1 -0
- package/package.json +2 -1
- package/skills/harness-init/SKILL.md +34 -2
- package/src/core/core.facade.ts +9 -1
- package/src/core/policy/policy.integrity.ts +13 -1
- package/src/platform/paths.ts +42 -1
- package/tools/doctor.ts +15 -5
- package/src/core/floor/index.ts +0 -1
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@ To give one project its own rules, open it and say **"setup harness"** to the ag
|
|
|
65
65
|
- [Tier 1 — the floor](#tier-1--the-floor-no-configuration-reaches-it)
|
|
66
66
|
- [Tier 2 — always on, no switch](#tier-2--always-on-no-switch)
|
|
67
67
|
- [Tier 3 — the rails you choose](#tier-3--the-rails-you-choose)
|
|
68
|
+
- [One rail takes a rule you write](#one-rail-takes-a-rule-you-write)
|
|
68
69
|
3. [How to see any of it](#how-to-see-any-of-it)
|
|
69
70
|
4. [How to explain a decision](#how-to-explain-a-decision)
|
|
70
71
|
5. [Providers](#providers)
|
|
@@ -180,6 +181,46 @@ Each rail's full benefit and trade-off — the long form, as the init wizard rea
|
|
|
180
181
|
the agent surfaces and what earns an interruption. It switches no gate on and weakens no verification —
|
|
181
182
|
the evidence bar is identical at all three ([`docs/decisions/ad-025.md`](docs/decisions/ad-025.md)).
|
|
182
183
|
|
|
184
|
+
### One rail takes a rule you write
|
|
185
|
+
|
|
186
|
+
Every other capability is a switch. **Operator rules** is a switch plus a file, because the trigger and the
|
|
187
|
+
proof are yours: *on this trigger, this must have happened, otherwise do that.* No pull request without a
|
|
188
|
+
review, for instance:
|
|
189
|
+
|
|
190
|
+
```markdown
|
|
191
|
+
---
|
|
192
|
+
on: pr-open
|
|
193
|
+
require:
|
|
194
|
+
- subagent(the-jury) since HEAD
|
|
195
|
+
otherwise: deny
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
Convene the jury on this branch. Checklist: docs/review-checklist.md
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Save it as one markdown file per rule in `.tlc/harness/rules/` for this repository, or in the runtime home's
|
|
202
|
+
`rules/` for every repository on this machine, and set `rules.enabled: true`. The proof has to be something
|
|
203
|
+
the harness observed — a subagent that ran, a command that completed, a gate that passed, a file that changed
|
|
204
|
+
— so the agent cannot write one: that store is under the project state directory, which the floor refuses it.
|
|
205
|
+
|
|
206
|
+
`tlc harness doctor` lists every rule it loaded, and says so when the switch is on and no file was found.
|
|
207
|
+
The full grammar — six triggers, four proof kinds, four verdicts and how each lands at the stop — is
|
|
208
|
+
`tlc harness help rules`.
|
|
209
|
+
|
|
210
|
+
**Why not the editor's own rules?** Because a rule file there is context, not a gate: activation is a mode
|
|
211
|
+
you pick, always-apply defaults to off, and one of the modes leaves it to the model to decide whether to load
|
|
212
|
+
the rule at all. This is not a replacement for that file — it is the layer underneath it. The editor's rule
|
|
213
|
+
says what the agent should do; this one says what must be provably true before it may.
|
|
214
|
+
|
|
215
|
+
The fair comparison is with the editors' **hooks**, which are deterministic too. Three things separate them
|
|
216
|
+
from this, taken from their own reference rather than from opinion: there is no prerequisite mechanism and no
|
|
217
|
+
state store, so "X requires Y to have happened" is custom logic you write per event; the state you would keep
|
|
218
|
+
lives in an ordinary repository path the agent can write, while a proof here lives where the floor refuses it;
|
|
219
|
+
and one host's stop hook cannot refuse completion at all, so the same requirement has to mean something
|
|
220
|
+
different on each — which is what the capability table and the degrade step already do. A hook is a program
|
|
221
|
+
per host per repository, fail-open unless it opts out. A rule is five lines of frontmatter, two tiers, both
|
|
222
|
+
hosts ([`docs/decisions/ad-100.md`](docs/decisions/ad-100.md)).
|
|
223
|
+
|
|
183
224
|
## How to see any of it
|
|
184
225
|
|
|
185
226
|
Every row above names a command in its last column. These are those commands.
|
|
@@ -276,21 +317,18 @@ each provider it finds, because a provider only reads its own.
|
|
|
276
317
|
|
|
277
318
|
1. Materialises the runtime at `~/.tlc/harness` from the installed package
|
|
278
319
|
2. Creates `config.json` from `config.example.json` when missing
|
|
279
|
-
3. Links
|
|
280
|
-
4.
|
|
320
|
+
3. Links `tlc` into `~/.local/bin`, and says so if that directory is not on `PATH`
|
|
321
|
+
4. Links the init skill into each detected provider's `skills/harness-init`
|
|
322
|
+
5. Wires user-level hooks for every provider it detects installed, in that provider's resolved config
|
|
281
323
|
directory
|
|
282
324
|
|
|
283
|
-
The `tlc` command itself comes from npm, not from this step: `npm i -g` generates the shim for the platform it
|
|
284
|
-
runs on, and `npm link` does the same from a clone. If `tlc` is not found after installing, the shim is in the
|
|
285
|
-
`bin` directory of the Node version npm installed under — which is not on `PATH` when a version manager later
|
|
286
|
-
switches versions. A link solves it for good:
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
ln -s ~/.tlc/harness/bin/tlc ~/.local/bin/tlc
|
|
290
|
-
```
|
|
291
|
-
|
|
292
325
|
Update with `tlc harness update`, which bumps the package and re-materialises the runtime.
|
|
293
326
|
|
|
327
|
+
Every release is published from CI with a short-lived OIDC credential — no token is stored in the repository — and
|
|
328
|
+
carries a provenance attestation you can check on npmjs.com. What gates it is the test suite on four platforms plus
|
|
329
|
+
an install of the packed tarball into a clean container, driven as a real command
|
|
330
|
+
([`docs/decisions/ad-102.md`](docs/decisions/ad-102.md)).
|
|
331
|
+
|
|
294
332
|
Overrides: `TLC_HOME`, `TLC_REPO_URL`, `TLC_BIN_DIR`.
|
|
295
333
|
|
|
296
334
|
### Running a development copy
|
package/bin/tlc-cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { linkDir, linkFile, seedConfig } from "../src/platform/links.ts";
|
|
|
16
16
|
import {
|
|
17
17
|
EXECUTABLE_EXTENSIONS,
|
|
18
18
|
executableOnPath,
|
|
19
|
+
findProjectRoot,
|
|
19
20
|
flagsDir,
|
|
20
21
|
isOnPath,
|
|
21
22
|
launcherBinDir,
|
|
@@ -35,8 +36,17 @@ export class UsageError extends Error {}
|
|
|
35
36
|
// single door into core and the two cannot drift apart.
|
|
36
37
|
type Posture = ReturnType<typeof coreFacade.policy.resolveProjectPosture>;
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* invariant: an explicit `TLC_PROJECT_DIR` still wins — the hooks set it from the host's own payload, which knows
|
|
41
|
+
* the workspace better than a directory walk can. Everything else discovers the project the way `git` does
|
|
42
|
+
* ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
43
|
+
*/
|
|
38
44
|
export function resolveProjectRoot(): string {
|
|
39
|
-
|
|
45
|
+
const declared = process.env.TLC_PROJECT_DIR;
|
|
46
|
+
if (declared) {
|
|
47
|
+
return declared;
|
|
48
|
+
}
|
|
49
|
+
return findProjectRoot(process.cwd()) ?? process.cwd();
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
export function modeFilePath(root: string): string {
|
|
@@ -390,9 +400,18 @@ export function acceptPolicy(root: string, paths: string[], interactive: boolean
|
|
|
390
400
|
const notHere = requested.filter((path) => !blocked.includes(path));
|
|
391
401
|
const outcome = coreFacade.policy.acceptPolicySources(root, requested);
|
|
392
402
|
if (outcome.kind === "not-a-source") {
|
|
403
|
+
/**
|
|
404
|
+
* hazard: this listed the sources and never said which project it had resolved. Run from a home directory,
|
|
405
|
+
* `projectConfigPath(root)` *is* the machine config path — so the list showed the same file twice, none of the
|
|
406
|
+
* repository's own paths, and no hint that the root was wrong. An operator read it as a defect in the product
|
|
407
|
+
* and lost the afternoon to it ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
408
|
+
*
|
|
409
|
+
* invariant: the success path already names the project. The failure path is the one that needed it.
|
|
410
|
+
*/
|
|
393
411
|
throw new UsageError(
|
|
394
412
|
[
|
|
395
413
|
`not a policy source: ${outcome.paths.join(", ")}`,
|
|
414
|
+
`project: ${root} — pass TLC_PROJECT_DIR or run this from the repository whose session is blocked`,
|
|
396
415
|
"The sources the loader reads are:",
|
|
397
416
|
...outcome.sources.map((source) => ` ${source}`),
|
|
398
417
|
].join("\n"),
|
|
@@ -1277,6 +1296,15 @@ export type TestStep = { label: string; bin: string; args: string[] };
|
|
|
1277
1296
|
// against the real repository — green from a shell, red from inside a hook.
|
|
1278
1297
|
export const TEST_ENV_IMPORT = ["--import", "./tools/test-env.mjs"];
|
|
1279
1298
|
|
|
1299
|
+
/**
|
|
1300
|
+
* The number of unused exports this repository carries today.
|
|
1301
|
+
*
|
|
1302
|
+
* why a number in code and not a snapshot file: it is a debt, and a debt that has to be edited down in a reviewed
|
|
1303
|
+
* commit is one somebody looks at. Lowering it is the point; raising it needs an argument in the diff
|
|
1304
|
+
* ([/decisions/ad-102.md](/decisions/ad-102.md)).
|
|
1305
|
+
*/
|
|
1306
|
+
export const KNIP_EXPORTS_CEILING = 76;
|
|
1307
|
+
|
|
1280
1308
|
export function buildTestSteps(): TestStep[] {
|
|
1281
1309
|
return [
|
|
1282
1310
|
// why: `--error-on-warnings`. A warn-level rule does not change biome's exit code, so three fixable warnings
|
|
@@ -1288,6 +1316,22 @@ export function buildTestSteps(): TestStep[] {
|
|
|
1288
1316
|
{ label: "tsc --noEmit", bin: "npx", args: ["tsc", "--noEmit"] },
|
|
1289
1317
|
{ label: "src suite", bin: "node", args: [...TEST_ENV_IMPORT, "--test", "src/**/__test__/*.test.ts"] },
|
|
1290
1318
|
{ label: "tools suite", bin: "node", args: [...TEST_ENV_IMPORT, "--test", "tools/__test__/*.test.ts"] },
|
|
1319
|
+
/**
|
|
1320
|
+
* why two knip steps and not one: `files` and `dependencies` are already at zero, so they block. `exports` has a
|
|
1321
|
+
* backlog of legitimately-exported-for-tests symbols, and a step that reports without failing is a signal that
|
|
1322
|
+
* never fires — so it blocks on *growth* instead, which is the published way to adopt this without a sweep
|
|
1323
|
+
* ([/decisions/ad-102.md](/decisions/ad-102.md)).
|
|
1324
|
+
*
|
|
1325
|
+
* hazard: `observe` was exported, wired into the facade and called by nothing, so no proof could ever exist and
|
|
1326
|
+
* every operator rule denied for ever. 113 tests passed. This is the check that sees that class
|
|
1327
|
+
* ([/decisions/ad-100.md](/decisions/ad-100.md)).
|
|
1328
|
+
*/
|
|
1329
|
+
{ label: "knip: dead files and dependencies", bin: "npx", args: ["knip", "--files", "--dependencies"] },
|
|
1330
|
+
{
|
|
1331
|
+
label: "knip: unused exports do not grow",
|
|
1332
|
+
bin: "npx",
|
|
1333
|
+
args: ["knip", "--exports", "--max-issues", String(KNIP_EXPORTS_CEILING)],
|
|
1334
|
+
},
|
|
1291
1335
|
{ label: "check-boundaries", bin: "node", args: ["tools/dev/check-boundaries.ts"] },
|
|
1292
1336
|
// why: `--error-on-warnings` above cannot see a rule that was suppressed rather than fixed, and biome accepts
|
|
1293
1337
|
// any text after the colon. This is what makes the reason a reason ([/decisions/ad-051.md](/decisions/ad-051.md)).
|