@tech-leads-club/harness-toolkit 0.3.6 → 0.4.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.
Files changed (73) hide show
  1. package/README.md +44 -8
  2. package/bin/tlc-cli.ts +40 -1
  3. package/bin/tlc-exec.d.mts +1 -0
  4. package/bin/tlc-exec.mjs +47 -2
  5. package/capabilities/catalog.json +47 -30
  6. package/dist/compact-before.mjs +84 -78
  7. package/dist/doctor.mjs +87 -81
  8. package/dist/help-topic.mjs +6 -5
  9. package/dist/init-project.mjs +147 -9
  10. package/dist/install-runtime.mjs +85 -79
  11. package/dist/lessons-cli.mjs +87 -81
  12. package/dist/obs-cli.mjs +83 -77
  13. package/dist/price-lookup.mjs +2 -2
  14. package/dist/prompt-submit.mjs +84 -78
  15. package/dist/refresh-model-prices.mjs +85 -79
  16. package/dist/response-after.mjs +84 -78
  17. package/dist/run.mjs +84 -78
  18. package/dist/session-end.mjs +90 -84
  19. package/dist/session-start.mjs +92 -86
  20. package/dist/shim.mjs +82 -76
  21. package/dist/stop.mjs +90 -84
  22. package/dist/subagent-start.mjs +84 -78
  23. package/dist/subagent-stop.mjs +85 -79
  24. package/dist/support.mjs +88 -82
  25. package/dist/tlc-cli.mjs +104 -98
  26. package/dist/tool-after.mjs +84 -78
  27. package/dist/tool-before.mjs +84 -78
  28. package/dist/tool-failure.mjs +84 -78
  29. package/dist/uninstall-runtime.mjs +4 -4
  30. package/docs/architecture.md +1 -0
  31. package/docs/concepts.md +86 -0
  32. package/docs/diagnose.md +20 -0
  33. package/docs/init.md +10 -2
  34. package/docs/lessons.md +12 -0
  35. package/docs/log.md +5 -0
  36. package/package.json +1 -1
  37. package/skills/harness-init/references/capabilities.md +54 -0
  38. package/src/core/core.facade.ts +54 -0
  39. package/src/core/floor/floor.paths.ts +2 -2
  40. package/src/core/floor/floor.policy-surface.ts +6 -1
  41. package/src/core/lesson/lesson.select.ts +30 -7
  42. package/src/core/policy/policy.defaults.ts +3 -0
  43. package/src/core/policy/policy.integrity.ts +2 -2
  44. package/src/core/policy/policy.loader.ts +14 -3
  45. package/src/core/policy/policy.shadow.ts +97 -0
  46. package/src/core/policy/policy.types.ts +8 -0
  47. package/src/core/release/release.decisions.ts +3 -13
  48. package/src/core/rules/rules.decide.ts +123 -0
  49. package/src/core/rules/rules.observe.ts +76 -0
  50. package/src/core/rules/rules.parse.ts +142 -0
  51. package/src/core/rules/rules.proof.ts +130 -0
  52. package/src/core/rules/rules.service.ts +141 -0
  53. package/src/core/rules/rules.store.ts +77 -0
  54. package/src/core/rules/rules.trigger.ts +101 -0
  55. package/src/core/rules/rules.types.ts +64 -0
  56. package/src/entrypoints/shim.ts +9 -1
  57. package/src/entrypoints/stop.ts +75 -1
  58. package/src/entrypoints/subagent-stop.ts +9 -1
  59. package/src/entrypoints/support.ts +32 -0
  60. package/src/entrypoints/tool-after.ts +7 -2
  61. package/src/entrypoints/tool-before.ts +44 -3
  62. package/src/platform/frontmatter.ts +142 -0
  63. package/src/platform/links.ts +32 -0
  64. package/src/platform/paths.ts +58 -4
  65. package/src/platform/pricing.ts +3 -3
  66. package/src/platform/screen.ts +62 -3
  67. package/tools/doctor.ts +162 -2
  68. package/tools/help-topic.ts +39 -23
  69. package/tools/init-project.ts +51 -6
  70. package/tools/install-runtime.ts +23 -2
  71. package/tools/lessons-cli.ts +4 -1
  72. package/tools/refresh-model-prices.ts +2 -2
  73. package/tools/uninstall-runtime.ts +11 -3
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>7 floor rules</b> no configuration can reach, <b>3 always-on checks</b>, and <b>23 rails</b> you choose.
42
+ <b>7 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
@@ -91,9 +91,9 @@ Three tiers, and which tier a check is in decides whether you can turn it off.
91
91
  |------|-------|--------------|------|
92
92
  | [Floor](#tier-1--the-floor-no-configuration-reaches-it) | 7 rules | Never | Before any policy is loaded, on every tool call, shell command and read |
93
93
  | [Always on](#tier-2--always-on-no-switch) | 3 checks | Never | After the floor, on every acting event |
94
- | [Rails](#tier-3--the-rails-you-choose) | 23 capabilities | Each one, individually | Where the table says |
94
+ | [Rails](#tier-3--the-rails-you-choose) | 24 capabilities | Each one, individually | Where the table says |
95
95
 
96
- Nothing else runs. If a message on your screen is not from one of the thirty-three rows below, it is not the
96
+ Nothing else runs. If a message on your screen is not from one of the thirty-four rows below, it is not the
97
97
  harness.
98
98
 
99
99
  ### Tier 1 — the floor, no configuration reaches it
@@ -135,7 +135,7 @@ otherwise switch off.
135
135
 
136
136
  ### Tier 3 — the rails you choose
137
137
 
138
- All 23 are off unless the **default** column says `on`, and each was presented with its benefit and its
138
+ All 24 are off unless the **default** column says `on`, and each was presented with its benefit and its
139
139
  trade-off when you ran the init wizard. `configPath` is the key in `.tlc/harness/config.json`.
140
140
 
141
141
  <!-- generated:validates -->
@@ -165,6 +165,7 @@ trade-off when you ran the init wizard. `configPath` is the key in `.tlc/harness
165
165
  | **Untrusted-content framing and enforcement**<br>`untrustedContent.enabled` · off | Frames outside content as data, and in enforce mode asks before a command that appears verbatim in it. | `tool.after` | `context` | tlc harness obs report — one framing injection per turn, with the characters it cost |
166
166
  | **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 |
167
167
  | **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 |
168
+ | **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 |
168
169
 
169
170
  <!-- /generated -->
170
171
 
@@ -275,15 +276,50 @@ each provider it finds, because a provider only reads its own.
275
276
 
276
277
  1. Materialises the runtime at `~/.tlc/harness` from the installed package
277
278
  2. Creates `config.json` from `config.example.json` when missing
278
- 3. Adds `tlc` to `~/.local/bin`
279
- 4. Links the init skill into each detected provider's `skills/harness-init`
280
- 5. Wires user-level hooks for every provider it detects installed, in that provider's resolved config
279
+ 3. Links the init skill into each detected provider's `skills/harness-init`
280
+ 4. Wires user-level hooks for every provider it detects installed, in that provider's resolved config
281
281
  directory
282
282
 
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
+
283
292
  Update with `tlc harness update`, which bumps the package and re-materialises the runtime.
284
293
 
285
294
  Overrides: `TLC_HOME`, `TLC_REPO_URL`, `TLC_BIN_DIR`.
286
295
 
296
+ ### Running a development copy
297
+
298
+ The runtime that answers a hook is whichever one `TLC_HOME` names, and it wins over the location of the launcher
299
+ that was invoked. So a checkout can be exercised without touching the machine's install:
300
+
301
+ ```bash
302
+ # one command against the checkout
303
+ TLC_HOME=/path/to/clone tlc harness doctor
304
+
305
+ # a whole editor session on the checkout — everything else on the machine stays on the installed copy
306
+ export TLC_HOME=/path/to/clone
307
+ cursor .
308
+ ```
309
+
310
+ Two things worth knowing before doing that:
311
+
312
+ - **A checkout serves its hooks from source.** With Bun present the launcher runs `src/` directly, so a file saved
313
+ half-edited is live on the next hook — in that session only. Nothing needs building first, and nothing protects
314
+ you from a syntax error either.
315
+ - **Do not install a clone with `--link` as the machine's runtime** unless you accept that your working tree *is*
316
+ what every session on the machine runs. That is the contributor route, and it is why `doctor` reports it
317
+ distinctly (`runtime ownership — link to a working clone`).
318
+
319
+ Neither of those applies to the ordinary route: `tlc harness test` runs the whole gate hermetically, and the
320
+ entrypoints can be driven with real hook payloads from a scratch directory, so most work needs no live session at
321
+ all.
322
+
287
323
  Provider config directories are resolved, not assumed: `CLAUDE_CONFIG_DIR` and `CURSOR_CONFIG_DIR` are
288
324
  honoured when set, so a relocated config is wired correctly. `tlc harness doctor` prints the resolved
289
325
  target for each provider.
@@ -415,7 +451,7 @@ See `tlc harness help architecture` or [`docs/architecture.md`](docs/architectur
415
451
  | `tlc harness lessons list` | Lessons across the three tiers, with staleness and effectiveness |
416
452
  | `tlc harness lessons add "…" [--ref path:symbol] [--global] [--pin]` | Write a lesson; `--ref` retires it when that stops resolving, `--pin` puts it ahead of ranking |
417
453
  | `tlc harness policy` / `policy accept <path>` | List a mid-session policy change; accept exactly those paths |
418
- | `tlc harness init --minimal` | Project stub |
454
+ | `tlc harness init --minimal` | A project config that decides nothing; an existing one is kept |
419
455
 
420
456
  ## Connect a project
421
457
 
package/bin/tlc-cli.ts CHANGED
@@ -12,9 +12,11 @@ import {
12
12
  import { delimiter, join } from "node:path";
13
13
  import { coreFacade } from "../src/core/index.ts";
14
14
  import { emitJson, JSON_FLAG, takeJsonFlag, unknownFlags } from "../src/platform/cli-output.ts";
15
- import { linkDir, seedConfig } from "../src/platform/links.ts";
15
+ import { linkDir, linkFile, seedConfig } from "../src/platform/links.ts";
16
16
  import {
17
17
  flagsDir,
18
+ isOnPath,
19
+ launcherBinDir,
18
20
  projectConfigPath,
19
21
  projectStateDir,
20
22
  providerConfigDirs,
@@ -977,6 +979,41 @@ export function npmRootFailureMessage(home: string): string {
977
979
  *
978
980
  * invariant: one function, no platform branch, and the launcher on PATH is npm's business.
979
981
  */
982
+ /**
983
+ * The `tlc` command on `PATH`.
984
+ *
985
+ * hazard: install never created this. `uninstall` removed it, `doctor` failed without it, and the README claimed
986
+ * install added it — three halves of a thing that did not exist. The command came from npm's own shim instead,
987
+ * which lives in the `bin` directory of whichever Node version npm ran under and leaves `PATH` the moment a
988
+ * version manager switches. Measured on an operator's machine: a successful install followed immediately by
989
+ * `tlc: command not found` ([/decisions/ad-101.md](/decisions/ad-101.md)).
990
+ *
991
+ * invariant: never fatal. A link is a convenience — npm's shim is still there — so a refusal is reported and the
992
+ * install continues.
993
+ *
994
+ * why the `PATH` check is separate from the link: a link nobody can reach is worse than none, because `doctor`
995
+ * then reports it healthy while the command still does not exist.
996
+ */
997
+ export function launcherLines(dest: string): string[] {
998
+ const dir = launcherBinDir();
999
+ const source = join(dest, "bin", "tlc");
1000
+ // hazard: `symlinkSync` happily creates a link to a path that is not there, and `existsSync` on a dangling link
1001
+ // is false — so a broken launcher would report as linked and `doctor` would say no `tlc` on PATH with a healthy
1002
+ // install beside it. Found by the test for this function ([/decisions/ad-101.md](/decisions/ad-101.md)).
1003
+ if (!existsSync(source)) {
1004
+ return [`tlc not linked — ${source} is missing from the runtime`];
1005
+ }
1006
+ const outcome = linkFile(source, join(dir, "tlc"));
1007
+ if (outcome.kind === "refused") {
1008
+ return [`tlc not linked — ${outcome.reason}`];
1009
+ }
1010
+ const lines = [`tlc → ${outcome.target}`];
1011
+ if (!isOnPath(dir)) {
1012
+ lines.push(`${dir} is not on PATH — add it, or use the shim npm installed`);
1013
+ }
1014
+ return lines;
1015
+ }
1016
+
980
1017
  export function wireRuntime(dest: string, home: string): { lines: string[]; missingSkill: boolean } {
981
1018
  const lines: string[] = [];
982
1019
  const seeded = seedConfig(dest);
@@ -999,6 +1036,8 @@ export function wireRuntime(dest: string, home: string): { lines: string[]; miss
999
1036
  );
1000
1037
  }
1001
1038
 
1039
+ lines.push(...launcherLines(dest));
1040
+
1002
1041
  const hooks = spawnSync(process.execPath, [join(dest, "bin", "write-user-hooks.mjs")], {
1003
1042
  stdio: "inherit",
1004
1043
  env: { ...process.env, TLC_HOME: home },
@@ -58,3 +58,4 @@ export type RuntimeDecision =
58
58
  export function decideRuntime(input: RuntimeDecisionInput): RuntimeDecision;
59
59
 
60
60
  export function main(argv?: string[]): void;
61
+ export const HOOK_ENTRIES: Set<string>;
package/bin/tlc-exec.mjs CHANGED
@@ -153,6 +153,44 @@ export function resolveEntrySource(harnessHome, entry) {
153
153
  return null;
154
154
  }
155
155
 
156
+ /**
157
+ * The entries that answer a host hook, as opposed to the ones an operator runs.
158
+ *
159
+ * why the distinction: a hook that cannot run must not stand between the agent and its tools, and a command that
160
+ * cannot run must fail loudly or the operator and CI lose the signal. Same launcher, opposite duties
161
+ * ([/decisions/ad-101.md](/decisions/ad-101.md)).
162
+ *
163
+ * invariant: this list is checked against `src/entrypoints/` by the gate, so an entrypoint added later cannot be
164
+ * left out of it — a hook missing from here fails closed, which is the direction that blocks a working machine.
165
+ */
166
+ export const HOOK_ENTRIES = new Set([
167
+ "compact-before",
168
+ "prompt-submit",
169
+ "response-after",
170
+ "session-end",
171
+ "session-start",
172
+ "stop",
173
+ "subagent-start",
174
+ "subagent-stop",
175
+ "tool-after",
176
+ "tool-before",
177
+ "tool-failure",
178
+ ]);
179
+
180
+ /**
181
+ * No opinion, in the shape both hosts read as "carry on".
182
+ *
183
+ * why `{}` rather than nothing: measured against both providers' own renderers, an abstain is `{}` on stdout for
184
+ * Cursor and empty for Claude, and `{}` satisfies both — Claude parses it as a verdict with no decision. A broken
185
+ * runtime used to emit nothing at all with exit 1, which Claude treats as a non-blocking error and Cursor's
186
+ * contract does not describe. A harness that cannot run was protecting nothing, so it must not be the thing that
187
+ * stops the turn ([/decisions/ad-101.md](/decisions/ad-101.md)).
188
+ */
189
+ function carryOn() {
190
+ process.stdout.write("{}");
191
+ process.exit(0);
192
+ }
193
+
156
194
  export function decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExists, srcPath }) {
157
195
  const distPath = join(harnessHome, "dist", `${entry}.mjs`);
158
196
  if (bunPath && srcPath && distExists) {
@@ -196,7 +234,7 @@ export function decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExis
196
234
  };
197
235
  }
198
236
 
199
- function run(harnessHome, command, commandArgs, origin = harnessHome) {
237
+ function run(harnessHome, command, commandArgs, origin = harnessHome, entry = "") {
200
238
  const result = spawnSync(command, commandArgs, {
201
239
  stdio: "inherit",
202
240
  // why: `TLC_ORIGIN` is where this copy physically lives, which is not `TLC_HOME` once an npm-installed shim
@@ -216,6 +254,9 @@ function run(harnessHome, command, commandArgs, origin = harnessHome) {
216
254
  });
217
255
  if (result.error) {
218
256
  console.error(`tlc: failed to start ${command}: ${result.error.message}`);
257
+ if (HOOK_ENTRIES.has(entry)) {
258
+ carryOn();
259
+ }
219
260
  process.exit(127);
220
261
  }
221
262
  process.exit(result.status ?? 1);
@@ -240,10 +281,14 @@ export function main(argv = process.argv) {
240
281
 
241
282
  const decision = decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExists, srcPath });
242
283
  if (decision.kind === "error") {
284
+ // invariant: the diagnosis still reaches stderr either way. Failing open is not failing silently.
243
285
  console.error(decision.message);
286
+ if (HOOK_ENTRIES.has(entry)) {
287
+ carryOn();
288
+ }
244
289
  process.exit(decision.status);
245
290
  }
246
- run(harnessHome, decision.command, [...decision.args, ...args], join(binDir, ".."));
291
+ run(harnessHome, decision.command, [...decision.args, ...args], join(binDir, ".."), entry);
247
292
  }
248
293
 
249
294
  if (import.meta.main) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "catalogVersion": 13,
2
+ "catalogVersion": 14,
3
3
  "capabilities": [
4
4
  {
5
5
  "id": "grind",
@@ -7,14 +7,14 @@
7
7
  "title": "Grind (lint/test on stop)",
8
8
  "summary": "Runs your lint and test commands against the files the turn changed, and sends the agent back until they pass.",
9
9
  "benefit": "Re-checks lint/test after each completed turn and follow-ups until gates pass.",
10
- "tradeOff": "Uses turns; flaky commands thrash the agent. The gate's cost is paid once per attempt, up to maxLoops a four-minute suite with maxLoops 5 can spend twenty minutes on tests alone. tlc harness obs report shows the runs and the total.",
10
+ "tradeOff": "Uses turns; flaky commands thrash the agent. The gate's cost is paid once per attempt, up to maxLoops \u2014 a four-minute suite with maxLoops 5 can spend twenty minutes on tests alone. tlc harness obs report shows the runs and the total.",
11
11
  "defaultOn": false,
12
12
  "sinceCatalogVersion": 1,
13
13
  "fires": [
14
14
  "stop"
15
15
  ],
16
16
  "verdict": "follow-up",
17
- "inspect": "tlc harness obs report runs, wall-clock and total; the last verdict is in the project state directory as last-gate.json",
17
+ "inspect": "tlc harness obs report \u2014 runs, wall-clock and total; the last verdict is in the project state directory as last-gate.json",
18
18
  "asks": [
19
19
  "lintCommand",
20
20
  "testCommand",
@@ -56,7 +56,7 @@
56
56
  "stop"
57
57
  ],
58
58
  "verdict": "block-stop",
59
- "inspect": "the ship ledger in the project state directory the challenge row names the empty diff",
59
+ "inspect": "the ship ledger in the project state directory \u2014 the challenge row names the empty diff",
60
60
  "asks": [
61
61
  "requires shipGate enabled"
62
62
  ]
@@ -67,14 +67,14 @@
67
67
  "title": "Comment gate (agent-added comments)",
68
68
  "summary": "Checks the comment lines this turn added against the commit the turn started from: by reason, by resolvability, or none.",
69
69
  "benefit": "Blocks the stop when this turn added comment lines, so narration never lands. Diff-scoped: comments you already committed are never flagged.",
70
- "tradeOff": "Three modes. declared lets the agent keep a comment by writing why:/hazard:/invariant:; resolvable adds the question the marker cannot answer can a reader who was not in the session check this? and catches change narration, dead plan or decision citations, and comments arguing with a reviewer; strict accepts none and asks you to write it instead. Each is stricter and interrupts more.",
70
+ "tradeOff": "Three modes. declared lets the agent keep a comment by writing why:/hazard:/invariant:; resolvable adds the question the marker cannot answer \u2014 can a reader who was not in the session check this? \u2014 and catches change narration, dead plan or decision citations, and comments arguing with a reviewer; strict accepts none and asks you to write it instead. Each is stricter and interrupts more.",
71
71
  "defaultOn": false,
72
72
  "sinceCatalogVersion": 1,
73
73
  "fires": [
74
74
  "stop"
75
75
  ],
76
76
  "verdict": "block-stop",
77
- "inspect": "tlc harness obs report the comments gate appears among the gate outcomes",
77
+ "inspect": "tlc harness obs report \u2014 the comments gate appears among the gate outcomes",
78
78
  "asks": [
79
79
  "mode: declared | resolvable | strict"
80
80
  ]
@@ -85,14 +85,14 @@
85
85
  "title": "Duplication gate (agent-added copies)",
86
86
  "summary": "Checks whether the runs of code this turn added already exist somewhere else in the project.",
87
87
  "benefit": "Blocks the stop when the turn wrote six or more lines the project already has, naming both sites. Diff-scoped: what was already duplicated is not counted.",
88
- "tradeOff": "Reads every tracked file on stop, bounded at 2000 files and 8 MB, and says when a bound was reached. Comments, dependency declarations and pure data object literals, type bodies, export lists are excluded, so it reports duplicated logic and misses duplicated shape.",
88
+ "tradeOff": "Reads every tracked file on stop, bounded at 2000 files and 8 MB, and says when a bound was reached. Comments, dependency declarations and pure data \u2014 object literals, type bodies, export lists \u2014 are excluded, so it reports duplicated logic and misses duplicated shape.",
89
89
  "defaultOn": false,
90
90
  "sinceCatalogVersion": 11,
91
91
  "fires": [
92
92
  "stop"
93
93
  ],
94
94
  "verdict": "block-stop",
95
- "inspect": "tlc harness obs report the duplication gate appears among the gate outcomes",
95
+ "inspect": "tlc harness obs report \u2014 the duplication gate appears among the gate outcomes",
96
96
  "asks": [
97
97
  "minRun: lines a run must reach before two copies count (default 6)"
98
98
  ]
@@ -103,14 +103,14 @@
103
103
  "title": "Supply-chain gate (dependencies this turn added)",
104
104
  "summary": "Checks what this turn added to the dependency graph: a manifest that moved without its lock, or an unpinned version.",
105
105
  "benefit": "Blocks the stop when a dependency added this turn is not recorded in a lockfile, or is specified as latest/*/no version. A dependency added in a turn runs on every later turn, in CI, and on every machine that installs the project.",
106
- "tradeOff": "Recognises a manifest by filename from one table, so an ecosystem the table does not carry is not covered. For a JSON manifest the declared dependency sections decide, so it reads the manifest as it stands without that, a rename or a scripts entry reads as a dependency. It does not check advisories, licences or typosquats: those need the network on every stop.",
106
+ "tradeOff": "Recognises a manifest by filename from one table, so an ecosystem the table does not carry is not covered. For a JSON manifest the declared dependency sections decide, so it reads the manifest as it stands \u2014 without that, a rename or a scripts entry reads as a dependency. It does not check advisories, licences or typosquats: those need the network on every stop.",
107
107
  "defaultOn": false,
108
108
  "sinceCatalogVersion": 12,
109
109
  "fires": [
110
110
  "stop"
111
111
  ],
112
112
  "verdict": "block-stop",
113
- "inspect": "tlc harness obs report the supply-chain gate appears among the gate outcomes",
113
+ "inspect": "tlc harness obs report \u2014 the supply-chain gate appears among the gate outcomes",
114
114
  "asks": []
115
115
  },
116
116
  {
@@ -119,7 +119,7 @@
119
119
  "title": "Subagent allowlist",
120
120
  "summary": "Checks a subagent's model against the list you wrote, and against the blocked *-fast shapes.",
121
121
  "benefit": "Restricts Task/subagent models to a list you write, and blocks *-fast shapes by default.",
122
- "tradeOff": "The harness ships no model list, so switching this on without filling allowedModels enforces nothing and doctor reports it as a fault. You maintain the list as providers add models including a value for `inherit` if you want subagents to take the parent's model.",
122
+ "tradeOff": "The harness ships no model list, so switching this on without filling allowedModels enforces nothing and doctor reports it as a fault. You maintain the list as providers add models \u2014 including a value for `inherit` if you want subagents to take the parent's model.",
123
123
  "defaultOn": false,
124
124
  "sinceCatalogVersion": 1,
125
125
  "fires": [
@@ -127,7 +127,7 @@
127
127
  "subagent.start"
128
128
  ],
129
129
  "verdict": "deny",
130
- "inspect": "tlc harness obs report refusals attributed by rule; the denial text names subagents.allowedModels and lists what is permitted",
130
+ "inspect": "tlc harness obs report \u2014 refusals attributed by rule; the denial text names subagents.allowedModels and lists what is permitted",
131
131
  "asks": [
132
132
  "allowedModels",
133
133
  "requireModel",
@@ -148,7 +148,7 @@
148
148
  "subagent.start"
149
149
  ],
150
150
  "verdict": "deny",
151
- "inspect": "tlc harness obs report refusals by rule; tlc harness status shows the sticky parent model it read",
151
+ "inspect": "tlc harness obs report \u2014 refusals by rule; tlc harness status shows the sticky parent model it read",
152
152
  "recommend": "on"
153
153
  },
154
154
  {
@@ -164,7 +164,7 @@
164
164
  "shell.before"
165
165
  ],
166
166
  "verdict": "deny",
167
- "inspect": "tlc harness obs report interruptions attributed to the shell-stall rule",
167
+ "inspect": "tlc harness obs report \u2014 interruptions attributed to the shell-stall rule",
168
168
  "asks": [
169
169
  "stallRepeatThreshold (default 3)"
170
170
  ]
@@ -174,7 +174,7 @@
174
174
  "configPath": "shell.catastrophicAsk",
175
175
  "title": "Catastrophic shell ask",
176
176
  "summary": "Checks a shell command for destruction that reaches outside the workspace.",
177
- "benefit": "Asks before destructive shell commands (rm -rf, drop db, force push, ).",
177
+ "benefit": "Asks before destructive shell commands (rm -rf, drop db, force push, \u2026).",
178
178
  "tradeOff": "Extra prompts on risky commands.",
179
179
  "defaultOn": true,
180
180
  "sinceCatalogVersion": 1,
@@ -182,7 +182,7 @@
182
182
  "shell.before"
183
183
  ],
184
184
  "verdict": "ask",
185
- "inspect": "tlc harness obs report interruptions attributed to the shell-catastrophic rule",
185
+ "inspect": "tlc harness obs report \u2014 interruptions attributed to the shell-catastrophic rule",
186
186
  "recommend": "on"
187
187
  },
188
188
  {
@@ -191,7 +191,7 @@
191
191
  "title": "Lessons",
192
192
  "summary": "Records what a repeated gate failure taught, ranks it, and injects it into the next session and retry.",
193
193
  "benefit": "Records compact lessons on gate stagnation and reinjects them ranked under a char budget. A lesson can name the path or symbol that makes it true and is withheld once that stops resolving, can carry an end date, and is graded helped or neutral by the next run of the gate it was injected for. Three tiers: shipped core, a global tier read by every product on this machine, and this project's own. How the lessons reach the model is decided by the provider rather than configured: where a host does not deliver context returned from its session-start hook, a durable rules file is written instead.",
194
- "tradeOff": "Uses context tokens; not a second brain / chat memory. The grading is correlational, not causal a gate passing after a lesson was injected does not prove the lesson caused it. Nothing is promoted between products automatically, so carrying a lesson to another product is an operator command. On a host that needs the durable view, a file is written into the repo (`.cursor/rules/harness-lessons.mdc`) and asked to be included on every request; `syncRulesFile: never` declines it.",
194
+ "tradeOff": "Uses context tokens; not a second brain / chat memory. The grading is correlational, not causal \u2014 a gate passing after a lesson was injected does not prove the lesson caused it. Nothing is promoted between products automatically, so carrying a lesson to another product is an operator command. On a host that needs the durable view, a file is written into the repo (`.cursor/rules/harness-lessons.mdc`) and asked to be included on every request; `syncRulesFile: never` declines it.",
195
195
  "defaultOn": false,
196
196
  "sinceCatalogVersion": 1,
197
197
  "fires": [
@@ -200,7 +200,7 @@
200
200
  "session.end"
201
201
  ],
202
202
  "verdict": "context",
203
- "inspect": "tlc harness lessons list every tier with staleness and effectiveness; obs report shows the characters each injection cost",
203
+ "inspect": "tlc harness lessons list \u2014 every tier with staleness and effectiveness; obs report shows the characters each injection cost",
204
204
  "asks": [
205
205
  "maxInjectSession",
206
206
  "maxCharsSession",
@@ -227,7 +227,7 @@
227
227
  "stop"
228
228
  ],
229
229
  "verdict": "follow-up",
230
- "inspect": "tlc harness handoff the follow-up fires only with unfinished work recorded there",
230
+ "inspect": "tlc harness handoff \u2014 the follow-up fires only with unfinished work recorded there",
231
231
  "asks": [
232
232
  "budgetContinueAfterLoops"
233
233
  ]
@@ -245,7 +245,7 @@
245
245
  "stop"
246
246
  ],
247
247
  "verdict": "follow-up",
248
- "inspect": "tlc harness handoff the gaps it injects are the ones stored as previous_gaps"
248
+ "inspect": "tlc harness handoff \u2014 the gaps it injects are the ones stored as previous_gaps"
249
249
  },
250
250
  {
251
251
  "id": "failureClassification",
@@ -260,7 +260,7 @@
260
260
  "stop"
261
261
  ],
262
262
  "verdict": "record",
263
- "inspect": "tlc harness handoff last_failure_category"
263
+ "inspect": "tlc harness handoff \u2014 last_failure_category"
264
264
  },
265
265
  {
266
266
  "id": "progressiveHandoff",
@@ -268,14 +268,14 @@
268
268
  "title": "Progressive handoff",
269
269
  "summary": "Reads the gaps the previous session ended with back out at the next session's start.",
270
270
  "benefit": "Carries the gaps the previous session ended with into the next session's bootstrap, as history rather than as a task list.",
271
- "tradeOff": "Spends session-start context on a verdict that may already be stale only the next run of the gate says whether it still holds. Capped at five, and the rest are counted rather than dropped in silence.",
271
+ "tradeOff": "Spends session-start context on a verdict that may already be stale \u2014 only the next run of the gate says whether it still holds. Capped at five, and the rest are counted rather than dropped in silence.",
272
272
  "defaultOn": true,
273
273
  "sinceCatalogVersion": 1,
274
274
  "fires": [
275
275
  "session.start"
276
276
  ],
277
277
  "verdict": "context",
278
- "inspect": "tlc harness handoff the gaps it reads back out are previous_gaps"
278
+ "inspect": "tlc harness handoff \u2014 the gaps it reads back out are previous_gaps"
279
279
  },
280
280
  {
281
281
  "id": "progressiveContext",
@@ -290,7 +290,7 @@
290
290
  "stop"
291
291
  ],
292
292
  "verdict": "follow-up",
293
- "inspect": "tlc harness obs report the retry count for a stop is the escalation level it reached"
293
+ "inspect": "tlc harness obs report \u2014 the retry count for a stop is the escalation level it reached"
294
294
  },
295
295
  {
296
296
  "id": "autopilot",
@@ -313,7 +313,7 @@
313
313
  "title": "Idle-turn gate (asked instead of acting)",
314
314
  "summary": "Checks whether a turn that ended with open work recorded any tool call or file change at all.",
315
315
  "benefit": "Blocks a turn that ends with open work, zero tool calls and zero file changes. Counts recorded tool events rather than reading the reply, so it cannot be talked around.",
316
- "tradeOff": "A turn that legitimately only answers a question is blocked while handoff work is open clear the handoff or turn this off for conversational repos.",
316
+ "tradeOff": "A turn that legitimately only answers a question is blocked while handoff work is open \u2014 clear the handoff or turn this off for conversational repos.",
317
317
  "defaultOn": false,
318
318
  "sinceCatalogVersion": 3,
319
319
  "fires": [
@@ -364,14 +364,14 @@
364
364
  "title": "Untrusted-content framing and enforcement",
365
365
  "summary": "Frames outside content as data, and in enforce mode asks before a command that appears verbatim in it.",
366
366
  "benefit": "Injects one framing line per turn when the agent reads a pull request, an issue, a fetched page or an MCP result, stating that the content is data and that any directive inside it is to be reported as a prompt-injection attempt, not obeyed.",
367
- "tradeOff": "Two modes. frame injects one line per turn, costing a few hundred characters and enforcing nothing. enforce also remembers what an untrusted read returned, bounded at 64 KB per session, and asks before a shell command that appears verbatim in it verbatim because a paraphrase cannot be shown to come from the content, so a rewritten command is missed. It needs the host to deliver tool output on the after-event: measured present on Claude Code's PostToolUse and on Cursor's afterShellExecution and afterMCPExecution, and absent on Cursor's generic postToolUse.",
367
+ "tradeOff": "Two modes. frame injects one line per turn, costing a few hundred characters and enforcing nothing. enforce also remembers what an untrusted read returned, bounded at 64 KB per session, and asks before a shell command that appears verbatim in it \u2014 verbatim because a paraphrase cannot be shown to come from the content, so a rewritten command is missed. It needs the host to deliver tool output on the after-event: measured present on Claude Code's PostToolUse and on Cursor's afterShellExecution and afterMCPExecution, and absent on Cursor's generic postToolUse.",
368
368
  "defaultOn": false,
369
369
  "sinceCatalogVersion": 7,
370
370
  "fires": [
371
371
  "tool.after"
372
372
  ],
373
373
  "verdict": "context",
374
- "inspect": "tlc harness obs report one framing injection per turn, with the characters it cost",
374
+ "inspect": "tlc harness obs report \u2014 one framing injection per turn, with the characters it cost",
375
375
  "asks": [
376
376
  "mode: frame | enforce"
377
377
  ]
@@ -390,7 +390,7 @@
390
390
  "stop"
391
391
  ],
392
392
  "verdict": "block-stop",
393
- "inspect": "tlc harness handoff plan_paths, plan_at and plan_deviations",
393
+ "inspect": "tlc harness handoff \u2014 plan_paths, plan_at and plan_deviations",
394
394
  "asks": [
395
395
  "windowMinutes"
396
396
  ]
@@ -409,10 +409,27 @@
409
409
  "session.end"
410
410
  ],
411
411
  "verdict": "record",
412
- "inspect": "tlc harness obs report the observation readings, held apart from the refusal counters so those stay honest",
412
+ "inspect": "tlc harness obs report \u2014 the observation readings, held apart from the refusal counters so those stay honest",
413
413
  "asks": [
414
- "rails the rails to observe, chosen from the observable set (today: comments). A name with no checker records nothing and doctor reports it"
414
+ "rails \u2014 the rails to observe, chosen from the observable set (today: comments). A name with no checker records nothing and doctor reports it"
415
415
  ]
416
+ },
417
+ {
418
+ "id": "operatorRules",
419
+ "configPath": "rules.enabled",
420
+ "title": "Operator rules (your trigger, your proof)",
421
+ "summary": "Reads the rules you declare in markdown and enforces them: on this trigger, this must have happened.",
422
+ "benefit": "Turns a standing instruction into a gate. A rule names when it applies, what the harness must have observed, and what to do when it has not \u2014 so 'no pull request without a review' stops depending on the model remembering it.",
423
+ "tradeOff": "The proof must be something the harness observed: a subagent that ran, a command that completed, a gate that passed, a file that changed. It cannot judge whether the review was good, and a pattern trigger is policy rather than containment \u2014 a script written to disk and run later, or a pull request opened in a browser, escape it.",
424
+ "defaultOn": false,
425
+ "sinceCatalogVersion": 14,
426
+ "fires": [
427
+ "tool.before",
428
+ "stop"
429
+ ],
430
+ "verdict": "deny",
431
+ "inspect": "tlc harness doctor \u2014 each active rule, its tier, and any proof kind never observed here",
432
+ "asks": []
416
433
  }
417
434
  ]
418
435
  }