@tech-leads-club/harness-toolkit 0.10.6 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/bin/tlc-cli.ts +60 -1
- package/capabilities/catalog.json +18 -1
- package/dist/compact-before.mjs +79 -78
- package/dist/doctor.mjs +83 -82
- package/dist/init-project.mjs +86 -85
- package/dist/install-runtime.mjs +80 -79
- package/dist/lessons-cli.mjs +82 -81
- package/dist/new-provider.mjs +126 -0
- package/dist/obs-cli.mjs +83 -82
- package/dist/prompt-submit.mjs +79 -78
- package/dist/refresh-model-prices.mjs +80 -79
- package/dist/response-after.mjs +80 -79
- package/dist/run.mjs +79 -78
- package/dist/session-end.mjs +85 -84
- package/dist/session-start.mjs +87 -86
- package/dist/shim.mjs +81 -80
- package/dist/ship-gate.mjs +79 -78
- package/dist/stop.mjs +86 -85
- package/dist/subagent-start.mjs +79 -78
- package/dist/subagent-stop.mjs +80 -79
- package/dist/support.mjs +84 -83
- package/dist/tlc-cli.mjs +225 -97
- package/dist/tool-after.mjs +80 -79
- package/dist/tool-before.mjs +82 -81
- package/dist/tool-failure.mjs +79 -78
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/architecture.md +16 -3
- package/docs/concepts.md +11 -0
- package/docs/log.md +4 -0
- package/docs/providers/claude-code.md +42 -21
- package/docs/providers/cursor.md +23 -7
- package/docs/troubleshooting.md +1 -0
- package/package.json +1 -1
- package/schema.json +12 -0
- package/skills/harness-init/references/capabilities.md +1 -0
- package/src/contracts/capabilities.ts +8 -1
- package/src/contracts/decision.ts +2 -1
- package/src/core/core.facade.ts +6 -0
- package/src/core/floor/floor.catalog.ts +5 -0
- package/src/core/floor/floor.paths.ts +17 -1
- package/src/core/floor/floor.policy-surface.ts +26 -9
- package/src/core/floor/floor.service.ts +37 -5
- package/src/core/policy/policy.defaults.ts +3 -0
- package/src/core/policy/policy.loader.ts +1 -0
- package/src/core/policy/policy.types.ts +4 -0
- package/src/core/secret-scan/secret-scan.service.ts +96 -0
- package/src/core/secret-scan/secret-scan.store.ts +56 -0
- package/src/entrypoints/run.ts +3 -1
- package/src/entrypoints/tool-after.ts +20 -0
- package/src/entrypoints/tool-before.ts +1 -0
- package/src/providers/claude/claude.capabilities.ts +3 -1
- package/src/providers/claude/claude.inbound.ts +37 -22
- package/src/providers/claude/claude.outbound.ts +4 -0
- package/src/providers/claude/claude.wiring.ts +4 -0
- package/src/providers/claude/index.ts +2 -1
- package/src/providers/cursor/cursor.capabilities.ts +3 -1
- package/src/providers/cursor/cursor.inbound.ts +1 -1
- package/src/providers/cursor/cursor.outbound.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +9 -1
- package/src/providers/cursor/index.ts +2 -1
- package/src/providers/provider.degrade.ts +19 -0
- package/src/providers/provider.port.ts +1 -0
- package/tools/new-provider.ts +251 -0
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<p align="center">
|
|
40
40
|
Hooks fire on the editor's own events. The harness answers each one with a decision — <b>allow</b>, <b>ask</b>,
|
|
41
41
|
<b>deny</b>, or text injected into the turn — and writes a record of what it decided and why.
|
|
42
|
-
<b>
|
|
42
|
+
<b>8 floor rules</b> no configuration can reach, <b>3 always-on checks</b>, and <b>24 rails</b> you choose.
|
|
43
43
|
</p>
|
|
44
44
|
|
|
45
45
|
- **[Everything it validates](#everything-it-validates)** — the whole list, one row per check
|
|
@@ -90,11 +90,11 @@ Three tiers, and which tier a check is in decides whether you can turn it off.
|
|
|
90
90
|
|
|
91
91
|
| Tier | Count | Configurable | Runs |
|
|
92
92
|
|------|-------|--------------|------|
|
|
93
|
-
| [Floor](#tier-1--the-floor-no-configuration-reaches-it) |
|
|
93
|
+
| [Floor](#tier-1--the-floor-no-configuration-reaches-it) | 8 rules | Never | Before any policy is loaded, on every tool call, shell command and read |
|
|
94
94
|
| [Always on](#tier-2--always-on-no-switch) | 3 checks | Never | After the floor, on every acting event |
|
|
95
|
-
| [Rails](#tier-3--the-rails-you-choose) |
|
|
95
|
+
| [Rails](#tier-3--the-rails-you-choose) | 25 capabilities | Each one, individually | Where the table says |
|
|
96
96
|
|
|
97
|
-
Nothing else runs. If a message on your screen is not from one of the thirty-
|
|
97
|
+
Nothing else runs. If a message on your screen is not from one of the thirty-six rows below, it is not the
|
|
98
98
|
harness.
|
|
99
99
|
|
|
100
100
|
### Tier 1 — the floor, no configuration reaches it
|
|
@@ -113,6 +113,7 @@ prints `rule=<name>`, and the name is the first column here.
|
|
|
113
113
|
| `machine-control` | `shutdown`, `reboot`, `halt`, `poweroff` | — |
|
|
114
114
|
| `unprovable-execution` | a program fetched over the network and handed to a shell — piped, process-substituted, or inside a shell's `-c`/`eval` substitution. The gate cannot read what would run | a fetch with no shell downstream, and a shell fed a local file the gate can read |
|
|
115
115
|
| `policy-surface-write` | every route an agent has to harness policy and state — a shell redirect, an interpreter, a heredoc program, or a write tool — in the project and under the runtime home, plus the mutating `tlc harness` subcommands from inside a session | reading them with a proven reader (`cat`, `head`, `grep`, `jq`, `ls`, `stat`, `test`), and `tlc harness handoff` for the handoff state |
|
|
116
|
+
| `wiring-tamper` | a shell redirect, in-place edit, or delete into a registered provider's wiring target — the document its own editor reads to register the harness's hooks — and a direct `Edit`/`Write`/`MultiEdit` tool call against the same path — overwriting it silences every hook the harness has for that host | reading the same path with a proven reader or a read tool |
|
|
116
117
|
|
|
117
118
|
<!-- /generated -->
|
|
118
119
|
|
|
@@ -167,6 +168,7 @@ trade-off when you ran the init wizard. `configPath` is the key in `.tlc/harness
|
|
|
167
168
|
| **Plan gate (declared scope vs diff)**<br>`planGate.enabled` · off | Checks the files the turn changed against the scope it declared, and against any stated deviation. | `response.after`<br>`stop` | `block-stop` | tlc harness handoff — plan_paths, plan_at and plan_deviations |
|
|
168
169
|
| **Observation mode (measure a rail with its rule off)**<br>`observe.enabled` · off | Runs a rail's checker while that rail is not enforcing, and records the reading without acting on it. | `stop`<br>`session.end` | `record` | tlc harness obs report — the observation readings, held apart from the refusal counters so those stay honest |
|
|
169
170
|
| **Operator rules (your trigger, your proof)**<br>`rules.enabled` · off | Reads the rules you declare in markdown and enforces them: on this trigger, this must have happened. | `tool.before`<br>`stop` | `deny` | tlc harness doctor — each active rule, its tier, and any proof kind never observed here |
|
|
171
|
+
| **Secret redaction (tool/shell output)**<br>`secrets.redactOutput` · **on** | Scans a tool's or shell command's output for a secret-shaped signature or high-entropy span and masks it. | `tool.after` | `context` | the masked placeholder itself, in the tool output — `[REDACTED:<kind>:<hash>]` |
|
|
170
172
|
|
|
171
173
|
<!-- /generated -->
|
|
172
174
|
|
package/bin/tlc-cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "../src/platform/paths.ts";
|
|
31
31
|
import { type Row, render, type Screen, type Section } from "../src/platform/screen.ts";
|
|
32
32
|
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
33
|
+
import { scaffold } from "../tools/new-provider.ts";
|
|
33
34
|
|
|
34
35
|
export class UsageError extends Error {}
|
|
35
36
|
|
|
@@ -861,6 +862,43 @@ export function setGateCommand(root: string, field: GateField, argv: string[], i
|
|
|
861
862
|
return `grind.${field}Command = ${JSON.stringify(argv)}`;
|
|
862
863
|
}
|
|
863
864
|
|
|
865
|
+
const REGISTRY_TYPE_IMPORT = 'import type { ProviderPort } from "./provider.port.ts";\n';
|
|
866
|
+
const REGISTRY_ARRAY = /export const providers: ProviderPort\[\] = \[([^\]]*)\];/;
|
|
867
|
+
|
|
868
|
+
// invariant: an append, never a reorder — the new entry always lands last in `providers`, so every existing provider's detection-order position is unchanged.
|
|
869
|
+
export function appendProviderToRegistry(text: string, name: string): string {
|
|
870
|
+
if (!text.includes(REGISTRY_TYPE_IMPORT)) {
|
|
871
|
+
throw new Error("provider.registry.ts: could not find the ProviderPort type-import anchor line");
|
|
872
|
+
}
|
|
873
|
+
const withImport = text.replace(
|
|
874
|
+
REGISTRY_TYPE_IMPORT,
|
|
875
|
+
`import { ${name}Provider } from "./${name}/index.ts";\n${REGISTRY_TYPE_IMPORT}`,
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
const match = REGISTRY_ARRAY.exec(withImport);
|
|
879
|
+
if (!match) {
|
|
880
|
+
throw new Error("provider.registry.ts: could not find the providers array literal");
|
|
881
|
+
}
|
|
882
|
+
const existing = (match[1] ?? "").trim();
|
|
883
|
+
const appended = existing.length > 0 ? `${existing}, ${name}Provider` : `${name}Provider`;
|
|
884
|
+
return withImport.replace(REGISTRY_ARRAY, `export const providers: ProviderPort[] = [${appended}];`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* The full `tlc harness new-provider` flow: T5's scaffold, then a real import appended to
|
|
889
|
+
* provider.registry.ts — in that order, so a refused scaffold never touches the registry.
|
|
890
|
+
*/
|
|
891
|
+
export function runNewProvider(name: string, root: string): { ok: true } | { ok: false; reason: string } {
|
|
892
|
+
const scaffolded = scaffold(name, root);
|
|
893
|
+
if (!scaffolded.ok) {
|
|
894
|
+
return scaffolded;
|
|
895
|
+
}
|
|
896
|
+
const registryPath = join(root, "src", "providers", "provider.registry.ts");
|
|
897
|
+
const current = readFileSync(registryPath, "utf8");
|
|
898
|
+
writeFileSync(registryPath, appendProviderToRegistry(current, name), "utf8");
|
|
899
|
+
return { ok: true };
|
|
900
|
+
}
|
|
901
|
+
|
|
864
902
|
export function helpScreen(): Screen {
|
|
865
903
|
return {
|
|
866
904
|
title: "tlc harness",
|
|
@@ -1167,6 +1205,7 @@ export type Action =
|
|
|
1167
1205
|
| { kind: "prices-lookup"; modelId: string; provider: string }
|
|
1168
1206
|
| { kind: "entry"; entry: string; args: string[] }
|
|
1169
1207
|
| { kind: "install"; args: string[] }
|
|
1208
|
+
| { kind: "new-provider"; name: string }
|
|
1170
1209
|
| { kind: "unknown"; cmd: string };
|
|
1171
1210
|
|
|
1172
1211
|
export function route(args: string[]): Action {
|
|
@@ -1295,6 +1334,13 @@ export function route(args: string[]): Action {
|
|
|
1295
1334
|
return { kind: "entry", entry: "init-project", args: args.slice(1) };
|
|
1296
1335
|
case "install":
|
|
1297
1336
|
return { kind: "install", args: args.slice(1) };
|
|
1337
|
+
case "new-provider": {
|
|
1338
|
+
const name = args[1];
|
|
1339
|
+
if (!name) {
|
|
1340
|
+
throw new UsageError("usage: tlc harness new-provider <name>");
|
|
1341
|
+
}
|
|
1342
|
+
return { kind: "new-provider", name };
|
|
1343
|
+
}
|
|
1298
1344
|
// why: the exit has to be as easy to find as the entrance. An operator who cannot get the harness off their
|
|
1299
1345
|
// machine without hand-editing settings.json will not try it on a second one
|
|
1300
1346
|
// ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
@@ -1332,7 +1378,8 @@ export const TEST_ENV_IMPORT = ["--import", "./tools/test-env.mjs"];
|
|
|
1332
1378
|
* commit is one somebody looks at. Lowering it is the point; raising it needs an argument in the diff
|
|
1333
1379
|
* ([/decisions/ad-102.md](/decisions/ad-102.md)).
|
|
1334
1380
|
*/
|
|
1335
|
-
|
|
1381
|
+
// why: raised from 76 to 80 — knip cannot trace render-provider-docs.ts's dynamic import() of each provider's inbound module, so EVENT_KIND_BY_HOOK (both adapters) and Claude's PRE_TOOL_USE_FAN_OUT/POST_TOOL_USE_FAN_OUT report as unused though the drift gate depends on them.
|
|
1382
|
+
export const KNIP_EXPORTS_CEILING = 80;
|
|
1336
1383
|
|
|
1337
1384
|
export function buildTestSteps(): TestStep[] {
|
|
1338
1385
|
return [
|
|
@@ -1378,6 +1425,7 @@ export function buildTestSteps(): TestStep[] {
|
|
|
1378
1425
|
// for an unrelated reason ([/decisions/ad-081.md](/decisions/ad-081.md)).
|
|
1379
1426
|
{ label: "check-manifest", bin: "node", args: ["tools/dev/check-manifest.ts"] },
|
|
1380
1427
|
{ label: "capabilities in sync", bin: "node", args: ["tools/dev/render-capabilities.ts", "--check"] },
|
|
1428
|
+
{ label: "provider docs in sync", bin: "node", args: ["tools/dev/render-provider-docs.ts", "--check"] },
|
|
1381
1429
|
{ label: "changelog in sync", bin: "node", args: ["tools/dev/render-changelog.ts", "--check"] },
|
|
1382
1430
|
// why: the OKF bundle's log is a reserved file that cannot be retired, and hand-maintaining it drifted to 19
|
|
1383
1431
|
// of 66 records before anyone noticed ([/decisions/ad-067.md](/decisions/ad-067.md)).
|
|
@@ -1815,6 +1863,17 @@ async function main(argv: string[]): Promise<void> {
|
|
|
1815
1863
|
case "install":
|
|
1816
1864
|
runInstall(action.args, root);
|
|
1817
1865
|
break;
|
|
1866
|
+
case "new-provider": {
|
|
1867
|
+
const result = runNewProvider(action.name, process.cwd());
|
|
1868
|
+
if (!result.ok) {
|
|
1869
|
+
console.error(`new-provider: refusing — ${result.reason}`);
|
|
1870
|
+
process.exit(1);
|
|
1871
|
+
}
|
|
1872
|
+
console.log(
|
|
1873
|
+
`new-provider: scaffolded src/providers/${action.name}/, docs/providers/${action.name}.md, and appended ${action.name}Provider to provider.registry.ts`,
|
|
1874
|
+
);
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1818
1877
|
case "entry":
|
|
1819
1878
|
runEntry(action.entry, json ? [...action.args, JSON_FLAG] : action.args, root);
|
|
1820
1879
|
break;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"catalogVersion":
|
|
2
|
+
"catalogVersion": 15,
|
|
3
3
|
"capabilities": [
|
|
4
4
|
{
|
|
5
5
|
"id": "grind",
|
|
@@ -430,6 +430,23 @@
|
|
|
430
430
|
"verdict": "deny",
|
|
431
431
|
"inspect": "tlc harness doctor \u2014 each active rule, its tier, and any proof kind never observed here",
|
|
432
432
|
"asks": []
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"id": "secretRedaction",
|
|
436
|
+
"configPath": "secrets.redactOutput",
|
|
437
|
+
"title": "Secret redaction (tool/shell output)",
|
|
438
|
+
"summary": "Scans a tool's or shell command's output for a secret-shaped signature or high-entropy span and masks it.",
|
|
439
|
+
"benefit": "Masks an AWS access key, GitHub/Slack/Stripe token, PEM private-key block, JWT, or an unlabelled high-entropy string with a deterministic placeholder \u2014 the same value always yields the same placeholder within a session \u2014 instead of blocking the command that produced it.",
|
|
440
|
+
"tradeOff": "A synchronous regex plus Shannon-entropy pass on every tool/shell output; a high-diversity non-secret string can be masked as a false positive, and an unlabelled low-entropy secret can be missed. On a host with no rewrite mechanism for the firing event, the mask degrades to a context warning instead \u2014 the model is told a secret was masked but the raw value already reached it.",
|
|
441
|
+
"defaultOn": true,
|
|
442
|
+
"sinceCatalogVersion": 15,
|
|
443
|
+
"fires": [
|
|
444
|
+
"tool.after"
|
|
445
|
+
],
|
|
446
|
+
"verdict": "context",
|
|
447
|
+
"inspect": "the masked placeholder itself, in the tool output \u2014 `[REDACTED:<kind>:<hash>]`",
|
|
448
|
+
"asks": [],
|
|
449
|
+
"recommend": "on"
|
|
433
450
|
}
|
|
434
451
|
]
|
|
435
452
|
}
|