@tech-leads-club/harness-toolkit 0.2.1
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/CHANGELOG.md +95 -0
- package/LICENSE +84 -0
- package/NOTICE +9 -0
- package/README.md +480 -0
- package/bin/tlc +10 -0
- package/bin/tlc-build +80 -0
- package/bin/tlc-cli.ts +1509 -0
- package/bin/tlc-exec +10 -0
- package/bin/tlc-exec.cmd +4 -0
- package/bin/tlc-exec.d.mts +60 -0
- package/bin/tlc-exec.mjs +248 -0
- package/bin/tlc.cmd +4 -0
- package/bin/tlc.mjs +7 -0
- package/bin/write-user-hooks.d.mts +40 -0
- package/bin/write-user-hooks.mjs +123 -0
- package/capabilities/catalog.json +418 -0
- package/config.example.json +51 -0
- package/dist/compact-before.mjs +7839 -0
- package/dist/doctor.mjs +8263 -0
- package/dist/help-topic.mjs +82 -0
- package/dist/init-project.mjs +993 -0
- package/dist/install-runtime.mjs +969 -0
- package/dist/lessons-cli.mjs +7258 -0
- package/dist/obs-cli.mjs +7025 -0
- package/dist/price-lookup.mjs +221 -0
- package/dist/prompt-submit.mjs +7860 -0
- package/dist/refresh-model-prices.mjs +158 -0
- package/dist/response-after.mjs +7879 -0
- package/dist/run.mjs +7830 -0
- package/dist/session-end.mjs +7937 -0
- package/dist/session-start.mjs +8056 -0
- package/dist/shim.mjs +52 -0
- package/dist/stop.mjs +8494 -0
- package/dist/subagent-start.mjs +7861 -0
- package/dist/subagent-stop.mjs +7851 -0
- package/dist/support.mjs +7045 -0
- package/dist/tlc-cli.mjs +8063 -0
- package/dist/tool-after.mjs +8113 -0
- package/dist/tool-before.mjs +7985 -0
- package/dist/tool-failure.mjs +7850 -0
- package/dist/uninstall-runtime.mjs +1307 -0
- package/docs/architecture.md +270 -0
- package/docs/concepts.md +562 -0
- package/docs/coverage.md +55 -0
- package/docs/decisions/ad-001.md +32 -0
- package/docs/decisions/ad-002.md +51 -0
- package/docs/decisions/ad-003.md +30 -0
- package/docs/decisions/ad-004.md +37 -0
- package/docs/decisions/ad-005.md +36 -0
- package/docs/decisions/ad-006.md +49 -0
- package/docs/decisions/ad-007.md +36 -0
- package/docs/decisions/ad-008.md +54 -0
- package/docs/decisions/ad-009.md +61 -0
- package/docs/decisions/ad-010.md +45 -0
- package/docs/decisions/ad-011.md +59 -0
- package/docs/decisions/ad-012.md +71 -0
- package/docs/decisions/ad-013.md +87 -0
- package/docs/decisions/ad-014.md +56 -0
- package/docs/decisions/ad-015.md +33 -0
- package/docs/decisions/ad-016.md +98 -0
- package/docs/decisions/ad-017.md +65 -0
- package/docs/decisions/ad-018.md +77 -0
- package/docs/decisions/ad-019.md +75 -0
- package/docs/decisions/ad-020.md +88 -0
- package/docs/decisions/ad-021.md +57 -0
- package/docs/decisions/ad-022.md +120 -0
- package/docs/decisions/ad-023.md +87 -0
- package/docs/decisions/ad-024.md +85 -0
- package/docs/decisions/ad-025.md +92 -0
- package/docs/decisions/ad-026.md +131 -0
- package/docs/decisions/ad-027.md +90 -0
- package/docs/decisions/ad-028.md +90 -0
- package/docs/decisions/ad-029.md +73 -0
- package/docs/decisions/ad-030.md +97 -0
- package/docs/decisions/ad-031.md +94 -0
- package/docs/decisions/ad-032.md +83 -0
- package/docs/decisions/ad-033.md +89 -0
- package/docs/decisions/ad-034.md +86 -0
- package/docs/decisions/ad-035.md +86 -0
- package/docs/decisions/ad-036.md +68 -0
- package/docs/decisions/ad-037.md +47 -0
- package/docs/decisions/ad-038.md +52 -0
- package/docs/decisions/ad-039.md +69 -0
- package/docs/decisions/ad-040.md +89 -0
- package/docs/decisions/ad-041.md +98 -0
- package/docs/decisions/ad-042.md +82 -0
- package/docs/decisions/ad-043.md +79 -0
- package/docs/decisions/ad-044.md +61 -0
- package/docs/decisions/ad-045.md +94 -0
- package/docs/decisions/ad-046.md +111 -0
- package/docs/decisions/ad-047.md +96 -0
- package/docs/decisions/ad-048.md +85 -0
- package/docs/decisions/ad-049.md +66 -0
- package/docs/decisions/ad-050.md +94 -0
- package/docs/decisions/ad-051.md +69 -0
- package/docs/decisions/ad-052.md +69 -0
- package/docs/decisions/ad-053.md +78 -0
- package/docs/decisions/ad-054.md +98 -0
- package/docs/decisions/ad-055.md +74 -0
- package/docs/decisions/ad-056.md +85 -0
- package/docs/decisions/ad-057.md +68 -0
- package/docs/decisions/ad-058.md +97 -0
- package/docs/decisions/ad-059.md +82 -0
- package/docs/decisions/ad-060.md +75 -0
- package/docs/decisions/ad-061.md +68 -0
- package/docs/decisions/ad-062.md +72 -0
- package/docs/decisions/ad-063.md +84 -0
- package/docs/decisions/ad-064.md +79 -0
- package/docs/decisions/ad-065.md +81 -0
- package/docs/decisions/ad-066.md +111 -0
- package/docs/decisions/ad-067.md +64 -0
- package/docs/decisions/ad-068.md +79 -0
- package/docs/decisions/ad-069.md +74 -0
- package/docs/decisions/ad-070.md +86 -0
- package/docs/decisions/ad-071.md +93 -0
- package/docs/decisions/ad-072.md +82 -0
- package/docs/decisions/ad-073.md +102 -0
- package/docs/decisions/ad-074.md +91 -0
- package/docs/decisions/ad-075.md +79 -0
- package/docs/decisions/ad-076.md +102 -0
- package/docs/decisions/ad-077.md +94 -0
- package/docs/decisions/ad-078.md +84 -0
- package/docs/decisions/ad-079.md +73 -0
- package/docs/decisions/ad-080.md +86 -0
- package/docs/decisions/ad-081.md +70 -0
- package/docs/decisions/ad-082.md +79 -0
- package/docs/decisions/ad-083.md +88 -0
- package/docs/decisions/index.md +111 -0
- package/docs/diagnose.md +248 -0
- package/docs/index.md +52 -0
- package/docs/init.md +101 -0
- package/docs/lessons.md +294 -0
- package/docs/log.md +139 -0
- package/docs/measure.md +178 -0
- package/docs/providers/claude-code.md +118 -0
- package/docs/providers/cursor.md +110 -0
- package/docs/providers/index.md +71 -0
- package/docs/troubleshooting.md +138 -0
- package/model-aliases.json +12 -0
- package/model-prices.cursor.json +410 -0
- package/model-prices.json +1 -0
- package/package.json +57 -0
- package/skills/harness-init/SKILL.md +241 -0
- package/skills/harness-init/evals/trigger_evals.json +86 -0
- package/skills/harness-init/references/capabilities.md +113 -0
- package/src/contracts/capabilities.ts +30 -0
- package/src/contracts/decision.ts +21 -0
- package/src/contracts/effort.ts +15 -0
- package/src/contracts/harness-event.ts +87 -0
- package/src/contracts/index.ts +6 -0
- package/src/contracts/policy-defaults.ts +14 -0
- package/src/contracts/wiring.ts +20 -0
- package/src/core/attest/attest.service.ts +0 -0
- package/src/core/capability/capability.service.ts +62 -0
- package/src/core/capability/capability.store.ts +51 -0
- package/src/core/capability/capability.types.ts +56 -0
- package/src/core/comment-policy/comment-policy.service.ts +307 -0
- package/src/core/comment-policy/comment-policy.types.ts +6 -0
- package/src/core/comment-policy/comment-resolvability.ts +134 -0
- package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
- package/src/core/comment-policy/comment-syntax.store.ts +65 -0
- package/src/core/comment-policy/comment-syntax.types.ts +24 -0
- package/src/core/core.facade.ts +461 -0
- package/src/core/duplication/duplication.service.ts +219 -0
- package/src/core/floor/floor.catalog.ts +54 -0
- package/src/core/floor/floor.paths.ts +90 -0
- package/src/core/floor/floor.policy-surface.ts +311 -0
- package/src/core/floor/floor.service.ts +306 -0
- package/src/core/floor/floor.tokenize.ts +179 -0
- package/src/core/floor/floor.verb.ts +28 -0
- package/src/core/floor/index.ts +1 -0
- package/src/core/gate/gate.artifact.ts +157 -0
- package/src/core/gate/gate.command.ts +130 -0
- package/src/core/gate/gate.findings.ts +201 -0
- package/src/core/gate/gate.inputs.ts +0 -0
- package/src/core/gate/gate.lock.ts +283 -0
- package/src/core/gate/gate.service.ts +28 -0
- package/src/core/gate/gate.types.ts +67 -0
- package/src/core/handoff/handoff.service.ts +43 -0
- package/src/core/handoff/handoff.store.ts +59 -0
- package/src/core/handoff/handoff.types.ts +89 -0
- package/src/core/index.ts +52 -0
- package/src/core/integrity/state-seal.ts +81 -0
- package/src/core/lesson/lesson.authored.ts +96 -0
- package/src/core/lesson/lesson.credit.ts +51 -0
- package/src/core/lesson/lesson.garden.ts +283 -0
- package/src/core/lesson/lesson.link.ts +93 -0
- package/src/core/lesson/lesson.score.ts +61 -0
- package/src/core/lesson/lesson.select.ts +209 -0
- package/src/core/lesson/lesson.service.ts +110 -0
- package/src/core/lesson/lesson.store.ts +317 -0
- package/src/core/lesson/lesson.sync.ts +70 -0
- package/src/core/lesson/lesson.types.ts +92 -0
- package/src/core/lesson/lesson.validity.ts +44 -0
- package/src/core/observability/observability.report.ts +326 -0
- package/src/core/observability/observability.service.ts +328 -0
- package/src/core/observability/observability.store.ts +269 -0
- package/src/core/observability/observability.types.ts +193 -0
- package/src/core/observability/observability.why.ts +206 -0
- package/src/core/observe/observe.service.ts +76 -0
- package/src/core/plan/plan.detect.ts +47 -0
- package/src/core/plan/plan.service.ts +81 -0
- package/src/core/plan/plan.types.ts +25 -0
- package/src/core/policy/policy.defaults.ts +103 -0
- package/src/core/policy/policy.guard.ts +34 -0
- package/src/core/policy/policy.integrity.ts +238 -0
- package/src/core/policy/policy.loader.ts +118 -0
- package/src/core/policy/policy.operator.ts +82 -0
- package/src/core/policy/policy.posture.ts +79 -0
- package/src/core/policy/policy.rails.ts +45 -0
- package/src/core/policy/policy.types.ts +157 -0
- package/src/core/presence/presence.service.ts +111 -0
- package/src/core/presence/presence.store.ts +59 -0
- package/src/core/presence/presence.types.ts +9 -0
- package/src/core/release/release.decisions.ts +147 -0
- package/src/core/release/release.seen.ts +38 -0
- package/src/core/shell-policy/shell-policy.service.ts +215 -0
- package/src/core/shell-policy/shell-policy.stall.ts +57 -0
- package/src/core/shell-policy/shell-policy.types.ts +26 -0
- package/src/core/ship/ship.ledger.ts +76 -0
- package/src/core/ship/ship.service.ts +137 -0
- package/src/core/ship/ship.types.ts +19 -0
- package/src/core/stagnation/stagnation.resolution.ts +87 -0
- package/src/core/stagnation/stagnation.service.ts +20 -0
- package/src/core/stagnation/stagnation.store.ts +49 -0
- package/src/core/stagnation/stagnation.types.ts +12 -0
- package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
- package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
- package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
- package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
- package/src/core/supply-chain/supply-chain.service.ts +217 -0
- package/src/core/turn/turn.activity.ts +94 -0
- package/src/core/turn/turn.autopilot.ts +131 -0
- package/src/core/turn/turn.failure-signals.ts +193 -0
- package/src/core/turn/turn.loop-counter.ts +71 -0
- package/src/core/turn/turn.types.ts +14 -0
- package/src/core/untrusted/untrusted.detect.ts +53 -0
- package/src/core/untrusted/untrusted.recall.ts +87 -0
- package/src/core/untrusted/untrusted.service.ts +129 -0
- package/src/core/untrusted/untrusted.store.ts +75 -0
- package/src/core/untrusted/untrusted.types.ts +42 -0
- package/src/entrypoints/compact-before.ts +14 -0
- package/src/entrypoints/prompt-submit.ts +25 -0
- package/src/entrypoints/response-after.ts +60 -0
- package/src/entrypoints/run.ts +179 -0
- package/src/entrypoints/session-end.ts +58 -0
- package/src/entrypoints/session-start.ts +172 -0
- package/src/entrypoints/shim.ts +46 -0
- package/src/entrypoints/stop.ts +876 -0
- package/src/entrypoints/subagent-start.ts +16 -0
- package/src/entrypoints/subagent-stop.ts +32 -0
- package/src/entrypoints/support.ts +182 -0
- package/src/entrypoints/tool-after.ts +111 -0
- package/src/entrypoints/tool-before.ts +186 -0
- package/src/entrypoints/tool-failure.ts +26 -0
- package/src/platform/backoff.ts +49 -0
- package/src/platform/cli-output.ts +30 -0
- package/src/platform/env-scope.ts +22 -0
- package/src/platform/fs-atomic.ts +142 -0
- package/src/platform/fs-jsonl.ts +24 -0
- package/src/platform/git.ts +187 -0
- package/src/platform/paths.ts +80 -0
- package/src/platform/pricing.ts +229 -0
- package/src/platform/process.ts +64 -0
- package/src/platform/sanitize.ts +23 -0
- package/src/platform/screen.ts +46 -0
- package/src/platform/style.ts +103 -0
- package/src/providers/claude/claude.capabilities.ts +25 -0
- package/src/providers/claude/claude.detect.ts +14 -0
- package/src/providers/claude/claude.inbound.ts +252 -0
- package/src/providers/claude/claude.lessons-view.ts +33 -0
- package/src/providers/claude/claude.outbound.ts +65 -0
- package/src/providers/claude/claude.policy-defaults.ts +9 -0
- package/src/providers/claude/claude.transcript.ts +62 -0
- package/src/providers/claude/claude.wiring.ts +245 -0
- package/src/providers/claude/index.ts +17 -0
- package/src/providers/cursor/cursor.capabilities.ts +28 -0
- package/src/providers/cursor/cursor.detect.ts +14 -0
- package/src/providers/cursor/cursor.inbound.ts +203 -0
- package/src/providers/cursor/cursor.lessons-view.ts +30 -0
- package/src/providers/cursor/cursor.outbound.ts +43 -0
- package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +214 -0
- package/src/providers/cursor/index.ts +17 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider.degrade.ts +152 -0
- package/src/providers/provider.port.ts +20 -0
- package/src/providers/provider.registry.ts +28 -0
- package/tools/doctor.ts +631 -0
- package/tools/help-topic.ts +74 -0
- package/tools/init-project.ts +277 -0
- package/tools/install-runtime.ts +164 -0
- package/tools/lessons-cli.ts +474 -0
- package/tools/obs-cli.ts +175 -0
- package/tools/price-lookup.ts +57 -0
- package/tools/refresh-model-prices.ts +210 -0
- package/tools/test-env.mjs +28 -0
- package/tools/test-env.names.d.mts +1 -0
- package/tools/test-env.names.mjs +14 -0
- package/tools/uninstall-runtime.ts +463 -0
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Concept
|
|
3
|
+
title: "Concepts"
|
|
4
|
+
description: "The operator-facing concepts behind the harness: operator posture, grind, pause/resume, shipGate, subagent allowlist, comment policy, duplication, catastrophic shell, shell stall, the intelligence rails, observability planes, and cost estimates."
|
|
5
|
+
tags: [concepts, policy, posture, grind, shipgate, observability]
|
|
6
|
+
timestamp: "2026-07-29"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Concepts
|
|
10
|
+
|
|
11
|
+
## operator posture
|
|
12
|
+
|
|
13
|
+
`mode`, or `tlc harness mode <paired|solo|focus>`. It sets how much the agent surfaces and what earns an
|
|
14
|
+
interruption — nothing else. Verification is identical at all three: the same evidence bar, the same gates, the
|
|
15
|
+
same done-criteria ([/decisions/ad-025.md](/decisions/ad-025.md)).
|
|
16
|
+
|
|
17
|
+
- **paired** — explains as it goes, and asks before any sizable move. A shell command that reaches the network, or
|
|
18
|
+
that can overwrite or remove a path that already exists, is asked about before it runs — so this posture is
|
|
19
|
+
enforced rather than merely stated
|
|
20
|
+
- **solo** (default) — works on its own. Three things reach you: an irreversible or destructive action, a real
|
|
21
|
+
dead-end after exhausting sources, and ambiguity that changes the outcome
|
|
22
|
+
- **focus** — only a destructive action or a real dead-end reaches you. Ambiguity is the agent's to settle,
|
|
23
|
+
taking the most reasonable reading and stating the assumption in one line
|
|
24
|
+
|
|
25
|
+
**Every posture also carries a deadline, not just a threshold.** An unclear goal belongs in the agent's first
|
|
26
|
+
actions; once the work is under way it takes the most reasonable reading and states the assumption instead of
|
|
27
|
+
asking, because a late question is measurably worse than a decision. `focus` admits exactly one early question —
|
|
28
|
+
a goal it cannot read before starting — so it means *ask early or not at all*, never simply never ask
|
|
29
|
+
([/decisions/ad-026.md](/decisions/ad-026.md)).
|
|
30
|
+
|
|
31
|
+
Three tiers ask, and each names a different risk: a command that **leaves the machine**, one that **can overwrite
|
|
32
|
+
or remove** an existing path, and one that **changes who can reach** a path. That last one — `chmod`, `chown` — is
|
|
33
|
+
asked about even though it loses no data, because it is the only change that appears in no diff. An append is the
|
|
34
|
+
one shape that does not ask: content survives it, and asking about it would train you to clear the prompt without
|
|
35
|
+
reading, which is how the action that mattered gets waved through. `cp`, `mv` and `tee` stay in the asking tier,
|
|
36
|
+
because each can overwrite a destination and the harness cannot know whether that destination exists.
|
|
37
|
+
|
|
38
|
+
Precedence: the `harness-mode` state file, then a posture flag file, then `mode` in the config, then the default.
|
|
39
|
+
Any other value is refused rather than absorbed — `tlc harness status` and `tlc harness doctor` name the rejected
|
|
40
|
+
word and the posture running in its place.
|
|
41
|
+
|
|
42
|
+
Posture never switches a gate or a capability on. `focus` used to force grind on, which meant a surfacing
|
|
43
|
+
preference silently overrode a capability with its own switch and its own trade-off.
|
|
44
|
+
|
|
45
|
+
## grind
|
|
46
|
+
|
|
47
|
+
`grind.enabled`. After each completed agent turn, run configured lint/test against **relevant** changed files:
|
|
48
|
+
|
|
49
|
+
- **lint** — only when files under `codePaths` changed
|
|
50
|
+
- **test** — when test files or `codePaths` files changed. Policy-only / non-code changes do **not** trigger the
|
|
51
|
+
test gate. Posture does not narrow this: the change that most needs testing is the one with no test file in
|
|
52
|
+
the diff ([/decisions/ad-025.md](/decisions/ad-025.md))
|
|
53
|
+
|
|
54
|
+
`grind.appendFiles` decides whether the changed files are appended to the lint/test argv. `auto` (default) appends
|
|
55
|
+
them, and refuses in three cases where appending cannot narrow the run:
|
|
56
|
+
|
|
57
|
+
| Shape | Why it does not narrow |
|
|
58
|
+
|-------|------------------------|
|
|
59
|
+
| a recipe runner — `just`, `make`, `task`, `mise`, `rake` | takes a target name; the first path reads as a second target |
|
|
60
|
+
| a package-manager script — `npm test`, `yarn test`, `bun run test` | the argument goes to somebody else's script, and whether it reaches the runner is not something the harness can know |
|
|
61
|
+
| a command that already globs — `eslint "src/**/*.ts"` | it walks the glob regardless, so appending widens rather than narrows |
|
|
62
|
+
|
|
63
|
+
`npx`, `bunx` and `dlx` are transparent: the tool named next is what decides, so `npx jest <file>` still narrows.
|
|
64
|
+
`always` and `never` override all of it.
|
|
65
|
+
|
|
66
|
+
**When `auto` cannot narrow, the gate runs in full on every attempt — up to `maxLoops`.** A four-minute suite with
|
|
67
|
+
three attempts is twelve minutes of tests, and that is the shape behind most reports of the harness being slow.
|
|
68
|
+
`tlc harness doctor` names any command in that state and says why; `tlc harness obs report` shows the runs and the
|
|
69
|
+
total ([/decisions/ad-033.md](/decisions/ad-033.md)).
|
|
70
|
+
|
|
71
|
+
Lint/test runs are serialized with a lock in the project state directory. A neighbour session holding it does not
|
|
72
|
+
block the turn: a recorded verdict whose inputs hash matches is reused and the lock is never taken, otherwise the
|
|
73
|
+
turn waits a bounded share of the stop hook's timeout, and if that expires the gate **defers** — the turn ends,
|
|
74
|
+
the handoff records `skipped`, and the holder is named ([/decisions/ad-073.md](/decisions/ad-073.md)).
|
|
75
|
+
|
|
76
|
+
A lock is reclaimed when it is older than 30 minutes, when it cannot be read, or when its owning process is gone
|
|
77
|
+
— the last one only on the host that wrote it, since a pid means nothing on another machine
|
|
78
|
+
([/decisions/ad-024.md](/decisions/ad-024.md)).
|
|
79
|
+
|
|
80
|
+
Each lint/test invocation writes `.tlc/harness/state/last-gate.json` (`harness.gate.v1`) with exit code,
|
|
81
|
+
command, files, `outputTail`, and `findings`. Follow-up gaps and stagnation fingerprints use that artifact.
|
|
82
|
+
Optional: the child may write findings to the path in `HARNESS_GATE_REPORT` (JSON
|
|
83
|
+
`{ "findings": [{ "summary": "..." }] }`).
|
|
84
|
+
|
|
85
|
+
On failure, send a follow-up so the agent fixes (loop, capped). Identical failure fingerprints trigger a
|
|
86
|
+
stagnation follow-up. Trade-off: catches breakage early; burns turns if gates are flaky.
|
|
87
|
+
|
|
88
|
+
A gate whose command never ran — exit 127, or a runner that could not resolve the target — is reported as
|
|
89
|
+
`config`, not `verification`. The distinction matters: the verification follow-up tells the agent to fix the
|
|
90
|
+
findings without deleting tests, which on a malformed command sends it to edit healthy code.
|
|
91
|
+
|
|
92
|
+
## pause / resume
|
|
93
|
+
|
|
94
|
+
`tlc harness pause` disables stop checks (grind + ship challenge). Use when exploring or mid-refactor.
|
|
95
|
+
`tlc harness resume` turns them back on.
|
|
96
|
+
|
|
97
|
+
Run both from your own terminal. Inside an agent session they are denied by the floor rule
|
|
98
|
+
`policy-surface-write`: policy is the operator's to change, and a stop check the agent can switch off is not a
|
|
99
|
+
stop check ([/decisions/ad-022.md](/decisions/ad-022.md)).
|
|
100
|
+
|
|
101
|
+
## gate commands
|
|
102
|
+
|
|
103
|
+
`tlc harness gate test-command <cmd> [args...]` and `tlc harness gate lint-command <cmd> [args...]` set
|
|
104
|
+
`grind.testCommand` and `grind.lintCommand` in the project policy. This is the only supported way to change
|
|
105
|
+
those fields — editing `config.json` by hand is fine for you as the operator, but no agent route reaches it.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
tlc harness gate test-command node --test 'src/**/__test__/*.test.ts'
|
|
109
|
+
tlc harness gate lint-command npx biome check .
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each refuses without writing when the argv is empty, when the first element does not resolve on `PATH` (a gate
|
|
113
|
+
command that cannot run is a config fault, AD-021), or when stdin is not a terminal.
|
|
114
|
+
|
|
115
|
+
## policy integrity
|
|
116
|
+
|
|
117
|
+
Every source the policy loader reads — the project config, the runtime config, `harness-mode` and the flag
|
|
118
|
+
files — is hashed when a session starts. If one changes during that session without a `tlc harness` command
|
|
119
|
+
behind it, the next tool call is refused and the changed path is named. The check has no config switch, for
|
|
120
|
+
the same reason the floor does not: a detector the detected change can disable is not a detector.
|
|
121
|
+
|
|
122
|
+
Editing the config between sessions never triggers it. Baselines are per session, so concurrent sessions do
|
|
123
|
+
not interfere, and every `tlc harness` mutation re-records them.
|
|
124
|
+
|
|
125
|
+
## shipGate
|
|
126
|
+
|
|
127
|
+
`shipGate.enabled`. Ship challenges fire **only** after an explicit protocol line in the agent response:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
HARNESS_SHIP_CLAIM: <one-line summary>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Prose without that marker does not count as a ship claim.
|
|
134
|
+
|
|
135
|
+
When a claim is recent (`claimWindowMinutes`, default 10), changed files touch `runtimePathPrefixes` after
|
|
136
|
+
`runtimePathExcludes`, and there is no recent PASS under `evidenceDir/*/90-verdict.txt`, stop follows up
|
|
137
|
+
with BLOCKED.
|
|
138
|
+
|
|
139
|
+
Outcomes append to `.tlc/harness/state/ship-ledger.jsonl` (`claim` / `challenge` / `pass`), each row tagged
|
|
140
|
+
with the resolved `provider`.
|
|
141
|
+
|
|
142
|
+
Default excludes: `.tlc/`, `.cursor/`, `.claude/`, `**/node_modules/`, `**/.git/`.
|
|
143
|
+
|
|
144
|
+
## emptyDiffAntiShip
|
|
145
|
+
|
|
146
|
+
`shipGate.emptyDiffAntiShip`. When enabled, a recent `HARNESS_SHIP_CLAIM` with zero changed files is blocked. Omit the claim line when an
|
|
147
|
+
empty diff is intentional.
|
|
148
|
+
|
|
149
|
+
## subagent allowlist
|
|
150
|
+
|
|
151
|
+
`subagents.enforceAllowlist`. Task/subagent models must be on `subagents.allowedModels`, which is **yours** — no
|
|
152
|
+
provider ships a catalogue, because one the harness invents goes stale and then refuses a spawn by a list that
|
|
153
|
+
appears nowhere in the project ([/decisions/ad-053.md](/decisions/ad-053.md)). An empty list enforces nothing and
|
|
154
|
+
`doctor` reports the combination as a fault. `*-fast`-shaped models are blocked separately, by patterns that are
|
|
155
|
+
added to yours rather than replacing them. `inherit` is a value the list may contain. Trade-off: cost/quality
|
|
156
|
+
control; you update the list when a provider adds models you want.
|
|
157
|
+
|
|
158
|
+
## Block parent Fast
|
|
159
|
+
|
|
160
|
+
`subagents.blockParentFast` (default off) denies a Task/subagent spawn while the sticky parent model is a
|
|
161
|
+
"fast" variant, even when the spawn's own `model` string looks allowlisted. See
|
|
162
|
+
[/decisions/ad-001.md](/decisions/ad-001.md).
|
|
163
|
+
|
|
164
|
+
## comment policy
|
|
165
|
+
|
|
166
|
+
`comments.enabled`, with `comments.mode` of `declared` or `strict`. Blocks the stop when the turn added
|
|
167
|
+
comment lines, so narration never lands. Diff-scoped against the sha the turn started from, not `HEAD`:
|
|
168
|
+
a turn that commits its own work moves `HEAD` past the very lines being judged, which is how the gate
|
|
169
|
+
missed every comment in a committing turn ([/decisions/ad-058.md](/decisions/ad-058.md)). Comments already
|
|
170
|
+
committed before the turn are never flagged. `declared` keeps a comment that states `why:`, `hazard:` or `invariant:`; `resolvable` is `declared` plus the
|
|
171
|
+
question a marker cannot answer — can a reader at HEAD, with no transcript of the session, resolve every
|
|
172
|
+
reference and check every claim? It refuses change narration, citations only the session could see, pull-request
|
|
173
|
+
vantage, comments arguing their own correctness, and control-flow narration, and asks for a restatement rather
|
|
174
|
+
than a deletion ([/decisions/ad-070.md](/decisions/ad-070.md)); `strict` accepts none and
|
|
175
|
+
asks the operator to write it. Tool directives (`biome-ignore`, `@ts-`, `noqa`, `shellcheck`, shebang) are
|
|
176
|
+
exempt in both modes.
|
|
177
|
+
|
|
178
|
+
## supply chain
|
|
179
|
+
|
|
180
|
+
`supplyChain.enabled`, off by default. Blocks the stop when this turn added a dependency and left one of two
|
|
181
|
+
things undone: the paired lockfile did not move, or the specifier names no version — `latest`, `*`, `x`, or blank.
|
|
182
|
+
Diff-scoped against the sha the turn started from, so a manifest already unlocked before the turn is not this
|
|
183
|
+
turn's to answer for.
|
|
184
|
+
|
|
185
|
+
A manifest is recognised by filename from one table that pairs each with its lockfile, and accepts any lockfile
|
|
186
|
+
the ecosystem uses — a project on pnpm has locked as firmly as one on npm. A filename the table does not carry
|
|
187
|
+
produces no findings.
|
|
188
|
+
|
|
189
|
+
For a JSON manifest the declared dependency sections decide what counts, so the manifest is read as it stands.
|
|
190
|
+
Without that step a rename reads as a dependency: calibrated against this repository's own history, the textual
|
|
191
|
+
shape alone reported `"name": "harness-toolkit"` from a rename commit, and would report every `scripts` entry the
|
|
192
|
+
same way.
|
|
193
|
+
|
|
194
|
+
It does not check advisories, licences or typosquats. Each needs the network on every stop, and `npm audit` is
|
|
195
|
+
already a gate command an operator can configure.
|
|
196
|
+
|
|
197
|
+
## duplication
|
|
198
|
+
|
|
199
|
+
`duplication.enabled`, off by default, with `duplication.minRun` (default 6). Blocks the stop when this turn
|
|
200
|
+
added a run of that many lines or more that already exists somewhere else in the project, naming both sites.
|
|
201
|
+
Diff-scoped against the sha the turn started from, like the comment gate: a run that was already duplicated
|
|
202
|
+
before the turn is not this turn's to answer for.
|
|
203
|
+
|
|
204
|
+
Three things are excluded, each because measuring said so. **Comments**, so two identical licence headers are
|
|
205
|
+
not a duplicated implementation. **Dependency declarations** — `import`, `require`, `use`, `#include` and their
|
|
206
|
+
siblings — because they are identical in every file that needs the same thing. **Pure data**: a run has to carry
|
|
207
|
+
operations, a call, an assignment, a branch or a return, in the majority of its lines, so a repeated object
|
|
208
|
+
literal, type body or export list does not count. Repeated shape is what those are for.
|
|
209
|
+
|
|
210
|
+
The comparison ignores indentation and a trailing comma, and nothing else. Renaming an identifier makes it a
|
|
211
|
+
different run on purpose — a rule that matched through renames would report every similarly shaped function.
|
|
212
|
+
|
|
213
|
+
It reads every tracked file on stop, bounded at 2000 files and 8 MB, and says when a bound was reached: a scan
|
|
214
|
+
that silently covered half a project reads as a clean answer.
|
|
215
|
+
|
|
216
|
+
## docs staleness gate
|
|
217
|
+
|
|
218
|
+
`docs.command`, optional and off by default. It is the repository's own staleness tool — `drift check`,
|
|
219
|
+
`oasdiff`, `ast-grep scan`, or a script the repo already has — run on stop through the same lock, artifact and
|
|
220
|
+
failure path as the lint and test gates.
|
|
221
|
+
|
|
222
|
+
`docs.severity` is `warn` or `deny`. `warn` injects the tool's output and lets the turn end; `deny` blocks and
|
|
223
|
+
goes through the standard gate failure path, which brings stagnation fingerprinting and progressive follow-up
|
|
224
|
+
with it.
|
|
225
|
+
|
|
226
|
+
The harness does not infer staleness from paths. A source-glob to docs-glob map was measured reporting on
|
|
227
|
+
82–100% of commits, which detects nothing, so a repository without a real tool gets no gate rather than a
|
|
228
|
+
noisy one. The tool also owns its own escape hatch, so there is no harness-level skip token.
|
|
229
|
+
|
|
230
|
+
## catastrophic shell
|
|
231
|
+
|
|
232
|
+
`shell.catastrophicAsk`. The shell-before hook asks before commands that can destroy data outside the workspace. Happy-path allows
|
|
233
|
+
are not signal events.
|
|
234
|
+
|
|
235
|
+
## shell stall
|
|
236
|
+
|
|
237
|
+
`shell.stallDetection`. When enabled, repeating the same shell command N times (`stallRepeatThreshold`) is denied with a
|
|
238
|
+
change-approach follow-up. Trade-off: stops loops; can block intentional retries.
|
|
239
|
+
|
|
240
|
+
## intelligence (rails)
|
|
241
|
+
|
|
242
|
+
| Key | Effect |
|
|
243
|
+
|------|--------|
|
|
244
|
+
| `intelligence.gapFeedback` | Gate fails include structured PREVIOUS_GAPS + NEXT suggestion |
|
|
245
|
+
| `intelligence.failureClassification` | Handoff stores category (verification, ship-evidence, stagnation, …) |
|
|
246
|
+
| `intelligence.progressiveHandoff` | sessionStart reads the gaps the previous session ended with back out of the handoff, capped at five and counting the rest. Phrased as history: only the next run of the gate says whether they still hold |
|
|
247
|
+
| `intelligence.progressiveContext` | Each stop retry escalates context (merge prior gaps, more gate output, stronger "don't repeat") |
|
|
248
|
+
| `intelligence.autopilot` | Runtime emits ordered AUTOPILOT steps + NEXT_ACTION (not LLM-invented plan) |
|
|
249
|
+
| `intelligence.lessons.enabled` | Durable gate lessons with decay/promote; inject at sessionStart + stop retry (see [/lessons.md](/lessons.md)) |
|
|
250
|
+
| `intelligence.budgetContinue` | Under loop/context pressure **and** unfinished handoff work, follow-up says keep working — do not summarize |
|
|
251
|
+
| `intelligence.idleTurnGate` | Blocks a turn that ends with open handoff work, zero recorded tool calls and zero file changes. It counts events the harness recorded rather than reading the reply, so no wording satisfies it |
|
|
252
|
+
|
|
253
|
+
## plan gate
|
|
254
|
+
|
|
255
|
+
`planGate.enabled` (off by default), with `planGate.windowMinutes` (default 120). The turn declares the paths
|
|
256
|
+
it intends to touch through a protocol line, exactly as the ship gate works — free-form prose about plans is
|
|
257
|
+
ignored:
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
HARNESS_PLAN: src/core/plan/**, src/entrypoints/stop.ts
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Declared paths use the same matcher as `shipGate.runtimePathExcludes`, so there is one pattern syntax to
|
|
264
|
+
learn, globs included. On stop, any changed file that no declared path covers and no accepted deviation
|
|
265
|
+
justifies blocks with BLOCKED / TRIED / NEED, naming those paths. A deviation is accepted only with a stated
|
|
266
|
+
reason:
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
HARNESS_PLAN_DEVIATION: src/x.ts — the call site moved with the type
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Naming the path alone is refused, since that would make the gate a formality satisfied by restating the file
|
|
273
|
+
just touched. Deviations accumulate for the plan's window, so one can be justified in a later message than
|
|
274
|
+
the one that declared the plan. The gate runs **before** the ship gate: a turn whose scope is invalid
|
|
275
|
+
produced evidence for the wrong change.
|
|
276
|
+
|
|
277
|
+
A turn that declares no plan is not gated at all, so the rail costs nothing until the agent opts in. That is
|
|
278
|
+
also its limit — it depends on the declaration being made.
|
|
279
|
+
|
|
280
|
+
## untrusted-content framing
|
|
281
|
+
|
|
282
|
+
`untrustedContent.enabled` (off by default), with `untrustedContent.extraTools` and
|
|
283
|
+
`untrustedContent.extraCommandPatterns`. The floor governs what the agent executes; this governs what it
|
|
284
|
+
reads. When a turn takes in content from outside the repository, one framing message states that the content
|
|
285
|
+
is data and that any directive inside it is to be reported as a prompt-injection attempt, never obeyed.
|
|
286
|
+
|
|
287
|
+
`untrustedContent.mode` chooses how far it goes. **`frame`** is the default and is the paragraph above: one
|
|
288
|
+
message, no refusal. **`enforce`** adds the question framing cannot ask — did this command come from that
|
|
289
|
+
content — and answers it verbatim ([/decisions/ad-077.md](/decisions/ad-077.md)).
|
|
290
|
+
|
|
291
|
+
In `enforce`, what an untrusted read returned is remembered for the session, bounded at 64 KB with the oldest
|
|
292
|
+
dropped first, whitespace collapsed and nothing else rewritten. When a shell command about to run appears
|
|
293
|
+
verbatim in it, the decision is `ask`, naming the source. Verbatim because a paraphrase cannot be shown to come
|
|
294
|
+
from the content, so an agent that rewrites a command before running it is missed on purpose — the alternative
|
|
295
|
+
guesses, and a rail that guesses asks about every command in every turn that read anything.
|
|
296
|
+
|
|
297
|
+
It needs the host to deliver what the tool returned. That is a capability, `toolOutputAtAfter`, because presence
|
|
298
|
+
is per-event rather than per-host: measured across 69,034 real records, two of the after-events carry nothing on
|
|
299
|
+
21,167 of them. A rail that assumed presence would be blind on the majority of one host's traffic and would not
|
|
300
|
+
know.
|
|
301
|
+
|
|
302
|
+
What covers the damaging tail either way is the floor, and provenance never mattered to it: running a program
|
|
303
|
+
fetched from the network, reading a credential, destroying outside the project, rewriting history, controlling
|
|
304
|
+
the machine and writing policy are all refused before any policy is read, whoever suggested them. An injected
|
|
305
|
+
`curl … | bash` is refused for being unreadable code, not for being injected — which holds without recognising
|
|
306
|
+
the attack at all.
|
|
307
|
+
|
|
308
|
+
Detection is a declared list, never inferred from output: every MCP result (the server is not this
|
|
309
|
+
repository), a tool whose name the provider declares as untrusted (`WebFetch` / `WebSearch` on Claude Code,
|
|
310
|
+
`Fetch` / `WebSearch` on Cursor), and a shell command whose **segment starts with** `gh pr view|diff|list`,
|
|
311
|
+
`gh issue view|list`, `gh api`, `curl` or `wget`. A source nobody listed is not covered.
|
|
312
|
+
|
|
313
|
+
Matching is anchored at the start of a command segment (split on `|`, `||`, `&&`, `;` and newline) rather
|
|
314
|
+
than a substring search, so naming a pattern inside a quoted argument, a `grep` search or a heredoc is not a
|
|
315
|
+
read. That distinction was not academic: this document names the patterns, and writing it tripped the rail
|
|
316
|
+
when the match was a substring.
|
|
317
|
+
|
|
318
|
+
Injected at most once per turn, keyed on a marker cleared at the prompt boundary, so it cannot spend the
|
|
319
|
+
context budget it exists to protect. When the provider cannot carry context on that event the decision
|
|
320
|
+
abstains rather than rendering into a field the provider ignores.
|
|
321
|
+
|
|
322
|
+
## global observability spool
|
|
323
|
+
|
|
324
|
+
`obs.globalSpool` (off by default). Every record already written under the project state directory is also
|
|
325
|
+
appended to one file under the runtime home, wrapped with the repository path and project name, so cost and
|
|
326
|
+
gate history can be read across every repository at once.
|
|
327
|
+
|
|
328
|
+
Writing outside the repository is the one thing an operator cannot undo by editing project policy, which is
|
|
329
|
+
why it is opt-in. Redaction is inherited rather than reimplemented — records are redacted before the store
|
|
330
|
+
sees them. Writes are best-effort: an unwritable runtime home degrades to project-only recording without
|
|
331
|
+
changing the decision returned to the provider. The spool is pruned on the same retention window as session
|
|
332
|
+
rollups, and `tlc harness obs prune` reports how many records it dropped.
|
|
333
|
+
|
|
334
|
+
## observability planes
|
|
335
|
+
|
|
336
|
+
| Plane | File | Default |
|
|
337
|
+
|-------|------|---------|
|
|
338
|
+
| Signal | `.tlc/harness/state/obs.jsonl` | ON — lifecycle, fails, denials, gates, cost alerts |
|
|
339
|
+
| Debug | `.tlc/harness/state/debug.jsonl` | OFF — happy-path tool/shell noise |
|
|
340
|
+
| Audit | `.tlc/harness/state/audit.jsonl` | ON — verbose per-event record, restored per [/decisions/ad-016.md](/decisions/ad-016.md) item 7 |
|
|
341
|
+
|
|
342
|
+
Which plane an event lands on is fixed by its kind. What a project can tune is the `obs` block:
|
|
343
|
+
|
|
344
|
+
| Key | Effect |
|
|
345
|
+
|-----|--------|
|
|
346
|
+
| `obs.globalSpool` | Mirror every record into the cross-repository spool (see above) |
|
|
347
|
+
| `obs.includePayloads` | Keep tool payloads in `attrs` instead of stripping them |
|
|
348
|
+
| `obs.maxAttrChars` | Truncation budget for `attrs` on every recorded event |
|
|
349
|
+
| `obs.sessionCostAlertUsd` | Threshold for the session cost alert; `null` disables it |
|
|
350
|
+
| `obs.retentionDays` | Window used by `tlc harness obs prune`, for rollups and the spool |
|
|
351
|
+
|
|
352
|
+
`debugEnabled` is deliberately **not** a project field: every event that resolves to debug level is emitted
|
|
353
|
+
with the audit configuration, which forces debug on so the audit trail persists
|
|
354
|
+
([/decisions/ad-016.md](/decisions/ad-016.md) item 7). There would be nothing for a project to switch.
|
|
355
|
+
|
|
356
|
+
An `"observability": { … }` block is not read at all — it never was. It was removed rather than honoured,
|
|
357
|
+
per [/decisions/ad-003.md](/decisions/ad-003.md). Full detail: [/measure.md](/measure.md).
|
|
358
|
+
|
|
359
|
+
### interruption rate
|
|
360
|
+
|
|
361
|
+
Every shell decision is recorded with the permission it produced, the active posture and the rule responsible —
|
|
362
|
+
`shell-posture-paired`, `shell-catastrophic` or `shell-stall`. An allow resolves to debug level and is dropped by
|
|
363
|
+
default, so only the interruptions reach disk. The session report shows them attributed by rule, because "seven
|
|
364
|
+
interruptions" names no switch while "six from the posture, one from the catastrophic rule" does. That is how you
|
|
365
|
+
calibrate the posture from your own sessions instead of trusting a threshold someone else chose
|
|
366
|
+
([/decisions/ad-026.md](/decisions/ad-026.md)).
|
|
367
|
+
|
|
368
|
+
What this is **not**: the harness records the decisions it made. It never learns your answer, and it cannot know
|
|
369
|
+
whether a question it did not ask would have helped. So it reports a rate and its attribution — never a precision,
|
|
370
|
+
a recall, or an accuracy of asking. Floor denials are outside it too: `rm -rf /` never reaches a policy layer.
|
|
371
|
+
|
|
372
|
+
## cost estimates
|
|
373
|
+
|
|
374
|
+
USD estimates use on-disk catalogs, resolved provider-first: local overrides → this provider's own catalog
|
|
375
|
+
→ LiteLLM → `null`.
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
tlc harness prices refresh
|
|
379
|
+
tlc harness prices refresh cursor
|
|
380
|
+
tlc harness prices refresh litellm
|
|
381
|
+
tlc harness prices lookup <model-id> [provider]
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Details: `tlc harness help prices` (or [/measure.md](/measure.md)).
|
|
385
|
+
|
|
386
|
+
## capability catalog
|
|
387
|
+
|
|
388
|
+
Optional features are chosen during the harness-init wizard (see [/init.md](/init.md)) and stored per
|
|
389
|
+
project. `tlc harness doctor` WARNs without failing for off/default opt-ins. Enable via harness-init or by
|
|
390
|
+
editing `.tlc/harness/config.json` — never auto-enabled.
|
|
391
|
+
|
|
392
|
+
## observation mode
|
|
393
|
+
|
|
394
|
+
`observe.enabled` plus `observe.rails`. Runs a rail's checker while that rail is **not** enforcing, and records
|
|
395
|
+
the reading without touching the turn.
|
|
396
|
+
|
|
397
|
+
It exists to answer the one question a firing rate cannot: *was the rule ever needed?* A rail that never fires
|
|
398
|
+
while its prose is injected is either working or unnecessary, and the count alone cannot tell you which. Run the
|
|
399
|
+
checker with the prose absent and the two separate: if the property holds anyway, the model was already doing it
|
|
400
|
+
and the rule is paying for injected context and returning nothing.
|
|
401
|
+
|
|
402
|
+
| Reading | What it means |
|
|
403
|
+
|---------|---------------|
|
|
404
|
+
| `held-without-prose` | The model does this on its own. The rule is a candidate for deletion. |
|
|
405
|
+
| `held-with-prose` | Ambiguous by construction — this is why observation runs with enforcement off. |
|
|
406
|
+
| `violated-without-prose` | The rule is doing real work. Keep it. |
|
|
407
|
+
| `violated-with-prose` | The prose is not working. Move the rule to a gate, or accept the rate. |
|
|
408
|
+
|
|
409
|
+
Observation never returns a decision and never blocks — a measurement that can change what it measures is not a
|
|
410
|
+
measurement. An enforcing rail is not observed, because it already records through its own path and counting it
|
|
411
|
+
twice would double the readings. Records land under their own obs kind rather than sharing the refusal kind, so
|
|
412
|
+
the denial counters stay honest ([/decisions/ad-027.md](/decisions/ad-027.md)).
|
|
413
|
+
|
|
414
|
+
Only `comments` is observable today. A name with no checker records nothing, and `doctor` says so by name rather
|
|
415
|
+
than leaving you to read the silence as "the property always holds" — which is the worst available misreading of a
|
|
416
|
+
measurement rail ([/decisions/ad-029.md](/decisions/ad-029.md)).
|
|
417
|
+
|
|
418
|
+
This is possible because the checker and the instruction are separate things here. In a system where the rule *is*
|
|
419
|
+
the mechanism there is nothing to hold apart, and the only alternative — running the same task repeatedly with and
|
|
420
|
+
without the rule — needs task repetition that real work does not offer.
|
|
421
|
+
|
|
422
|
+
## resolution history
|
|
423
|
+
|
|
424
|
+
When a gate passes after having failed, the harness records the files that changed between those two states against
|
|
425
|
+
the failure's fingerprint. If that exact failure returns, the follow-up carries one line naming them.
|
|
426
|
+
|
|
427
|
+
It is offered as **history, never as instruction**: past tense, and explicit that it is not a list to edit. A
|
|
428
|
+
previous resolution is evidence, and evidence is what a plan may name — but the same list phrased as an order would
|
|
429
|
+
send an agent to edit files that may be irrelevant this time, which is the harm AD-021 and AD-024 each removed
|
|
430
|
+
through a different door ([/decisions/ad-028.md](/decisions/ad-028.md)).
|
|
431
|
+
|
|
432
|
+
Bounded at 200 resolutions and 8 files each, pruning the oldest, because the store is read on the failure path.
|
|
433
|
+
Scoped to this repository: a fix that worked in another codebase is exactly the kind of advice that reads plausible
|
|
434
|
+
and is wrong.
|
|
435
|
+
|
|
436
|
+
## attestation
|
|
437
|
+
|
|
438
|
+
`tlc harness attest [--json]`. Every session appends one hash-chained record: which policy was in force, whether it
|
|
439
|
+
changed mid-session without a harness command, which rails were active, refusals by rule, and gate outcomes.
|
|
440
|
+
|
|
441
|
+
That is the artifact a reviewer needs in order to trust agent-written code, and it is the part governance-as-prompting
|
|
442
|
+
never produces. Verification reports the index at which the chain broke, so a tampered record sends you to one line
|
|
443
|
+
rather than to the whole file. A missing file is an empty valid chain, not a broken one.
|
|
444
|
+
|
|
445
|
+
Two things it deliberately does **not** claim. It is chained, not signed — that detects a rewritten or removed
|
|
446
|
+
record and does not prove authorship, because a key would mean key management. And every field is something the
|
|
447
|
+
harness observed: there is no assertion that the code is correct, that anyone reviewed it, or that a human approved
|
|
448
|
+
anything. An attestation implying those would be worse than none, because a reviewer would stop looking
|
|
449
|
+
([/decisions/ad-028.md](/decisions/ad-028.md)).
|
|
450
|
+
|
|
451
|
+
## accepting a policy edit you made
|
|
452
|
+
|
|
453
|
+
If you edit `config.json`, a flag file or the mode file while a session is live, the next acting tool call in that
|
|
454
|
+
session is refused and the changed path is named. That is the integrity check working: a mid-session policy change
|
|
455
|
+
with no harness command behind it is what it exists to catch.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
tlc harness policy # list what changed, change nothing
|
|
459
|
+
tlc harness policy accept <path>... # accept exactly those paths
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Four things keep that second command out of an agent's reach, and no single one carries the weight
|
|
463
|
+
([/decisions/ad-030.md](/decisions/ad-030.md)):
|
|
464
|
+
|
|
465
|
+
- the floor refuses `tlc harness policy` from inside any agent session, with no config switch
|
|
466
|
+
- it refuses without an interactive terminal, so a script cannot reach it either
|
|
467
|
+
- you name each path, so accepting is an act rather than a keystroke
|
|
468
|
+
- acceptance is per source, so anything you leave out keeps blocking
|
|
469
|
+
|
|
470
|
+
Accepting records the hash as it is now. A later change to the same file diverges again — there is deliberately no
|
|
471
|
+
way to say "stop watching this". And the acceptance is recorded rather than erased, so a reviewer reading the
|
|
472
|
+
session's attestation sees that policy moved and was accepted, instead of seeing nothing.
|
|
473
|
+
|
|
474
|
+
`status` and `doctor` never clear a divergence as a side effect of looking at it. `doctor` reports one when it
|
|
475
|
+
exists, naming the paths and the command.
|
|
476
|
+
|
|
477
|
+
## updating
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
tlc harness version # which revision you run, and what this project last saw
|
|
481
|
+
tlc harness update --check # what an update would pull. Fetches; never merges
|
|
482
|
+
tlc harness update # pull, relink, rebuild, announce what landed, then doctor
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
There is no changelog file and no version number, on purpose. The version is the runtime's git revision and its
|
|
486
|
+
date, because that is what `update` actually moves — a hand-maintained number drifts, and this one said `0.1.0` for
|
|
487
|
+
the project's whole life. A semantic version is a promise about compatibility that AD-003 declines to make.
|
|
488
|
+
|
|
489
|
+
The changelog is `docs/decisions/`. A decision carries a `migration` note **only when `doctor` cannot detect the
|
|
490
|
+
condition for you** — `update` runs `doctor` at the end, so a note that says "run doctor" is noise, and an alarm that
|
|
491
|
+
fires on every update is one you learn to scroll past ([/decisions/ad-034.md](/decisions/ad-034.md)). Today exactly one
|
|
492
|
+
decision carries a note: the ship gate's evidence ordering, which shows up as a blocked stop and nothing can see in
|
|
493
|
+
advance. A project updating for the first time records where it stands
|
|
494
|
+
and announces nothing, because thirty entries at once is the same as no message
|
|
495
|
+
([/decisions/ad-031.md](/decisions/ad-031.md)).
|
|
496
|
+
|
|
497
|
+
`doctor` is the net underneath: a posture that fell back, an observed rail with no checker, a policy that changed out
|
|
498
|
+
of band. If a migration note was forgotten, that is where it surfaces.
|
|
499
|
+
|
|
500
|
+
If the fast-forward fails, the runtime checkout has commits upstream does not. The message names both ways out —
|
|
501
|
+
reset to upstream, or re-run the installer — and runs neither, because the first one throws work away.
|
|
502
|
+
|
|
503
|
+
## wiring health
|
|
504
|
+
|
|
505
|
+
`tlc harness doctor` checks each provider's hooks, and for the replace-strategy target it checks them **per event**:
|
|
506
|
+
our launcher named in the command, that file present on disk, and a handler after it. A declared event with no
|
|
507
|
+
harness entry is reported too — that is the case a marker cannot see.
|
|
508
|
+
|
|
509
|
+
The marker keeps its own job, unchanged: it answers *is this file ours*, which is what decides whether `update` may
|
|
510
|
+
overwrite it. Whether the hooks work is a different question, and conflating the two is what let a hook that could
|
|
511
|
+
not run report as healthy ([/decisions/ad-032.md](/decisions/ad-032.md)).
|
|
512
|
+
|
|
513
|
+
A hook belonging to another tool in the same file is never reported. That is deliberate — flagging someone else's
|
|
514
|
+
entry would train you to skip the check, and then it would miss ours.
|
|
515
|
+
|
|
516
|
+
When something is wrong the detail names the event and the reason, bounded to three with a count of the rest:
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
WARN cursor wiring — detected but not wired — preToolUse: no handler after the
|
|
520
|
+
script: `node /path/tlc-exec.mjs` — run: tlc harness update (~/.cursor/hooks.json)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## writing a lesson yourself
|
|
524
|
+
|
|
525
|
+
The store used to have one producer: the same gate failing twice. So anything learned another way — a review, an
|
|
526
|
+
incident, a pattern you noticed across several changes — could not enter the one channel built to carry it back to the
|
|
527
|
+
next turn.
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
tlc harness lessons add "Grep for a producer before calling a new field done." --tokens producer,dead
|
|
531
|
+
tlc harness lessons add "Re-run the suite after the last edit." --gate test --avoid "citing a stale verdict"
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
It is recorded as `source: manual` and active immediately — a candidate exists because the automatic producer is
|
|
535
|
+
guessing from gate output, and an author is not. The id is a hash of the instruction, so rewriting the same lesson
|
|
536
|
+
updates it instead of adding a near-duplicate. A lesson written from inside an agent session says so in its category,
|
|
537
|
+
so you can tell the two apart in `tlc harness lessons list`.
|
|
538
|
+
|
|
539
|
+
**Say what makes it true, and it retires itself.** `--ref path[:symbol]` names the thing the lesson is about; when
|
|
540
|
+
that stops resolving the lesson is withheld instead of sending the next turn after a file that no longer exists.
|
|
541
|
+
`--until <iso>` gives it an end date. Both are optional and a lesson about conduct needs neither.
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
tlc harness lessons add "Run tools/check-dist-fresh.ts before the commit, never chained with &&." \
|
|
545
|
+
--ref tools/check-dist-fresh.ts --gate test
|
|
546
|
+
tlc harness lessons add "Pin the formatter until the toolchain moves." --until 2026-12-01T00:00:00Z
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Decide who should read it.** A lesson about this repository stays here; one about engineering belongs to every
|
|
550
|
+
product you work in. `--global` writes it to the machine tier, and `lessons promote <id>` moves an existing project
|
|
551
|
+
lesson up. Nothing is promoted automatically — a lesson mined from one product's gate names that product's tooling
|
|
552
|
+
([/decisions/ad-040.md](/decisions/ad-040.md)).
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
tlc harness lessons add "Run the gate itself, never an approximation of its steps." --global
|
|
556
|
+
tlc harness lessons promote project:test:9f2c1a
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**The harness never reads your documentation to find lessons.** No decision-record convention, no directory, no file
|
|
560
|
+
format — it runs in many products, and one project's filing habits are not a feature of the tool. If you want your own
|
|
561
|
+
ADRs or postmortems to produce lessons, that is a script in your repository calling this command
|
|
562
|
+
([/decisions/ad-035.md](/decisions/ad-035.md)).
|
package/docs/coverage.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Concept
|
|
3
|
+
title: "What this covers, and what it does not"
|
|
4
|
+
description: "A self-assessment of the harness against a published agentic-risk taxonomy: which of its own rules answer each risk, and what each one still leaves open. Control names are generated from the catalogs and checked by the gate."
|
|
5
|
+
tags: [coverage, security, self-assessment]
|
|
6
|
+
timestamp: "2026-08-17"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# What this covers, and what it does not
|
|
10
|
+
|
|
11
|
+
**This is a self-assessment, not an audit.** Nobody external has verified it. It is published because a list with
|
|
12
|
+
its gaps in it is more useful than a badge, and because this project refuses claims without evidence in the code
|
|
13
|
+
it governs — the same standard applies to its own README ([/decisions/ad-079.md](/decisions/ad-079.md)).
|
|
14
|
+
|
|
15
|
+
Taxonomy: OWASP Top 10 for Agentic Applications, 2026 list (ASI01–ASI10). A revision to that list makes this page stale and nothing here will notice; the
|
|
16
|
+
date above is how you tell.
|
|
17
|
+
|
|
18
|
+
**4 covered · 5 partial · 1 not applicable.**
|
|
19
|
+
|
|
20
|
+
Every name in the **Controls** column is a floor rule or a capability id, generated from
|
|
21
|
+
`src/core/floor/floor.catalog.ts` and `capabilities/catalog.json` and checked by the gate. A control that is
|
|
22
|
+
renamed or removed fails the build rather than leaving a claim standing here. The **What it leaves open** column is
|
|
23
|
+
judgement and is written by hand.
|
|
24
|
+
|
|
25
|
+
| Risk | State | Controls | What it leaves open |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| **ASI01** Agent goal hijack | partial | `untrustedContent`, `unprovable-execution`, `secret-access` | a command rewritten before it runs is not traced to the content it came from, and only shell actions are checked. The damaging tail is refused by the floor whoever suggested it |
|
|
28
|
+
| **ASI02** Tool misuse and exploitation | covered | `outside-project-destruction`, `unprovable-destruction`, `unprovable-execution`, `machine-control`, `catastrophicShell`, `shellStall` | — |
|
|
29
|
+
| **ASI03** Identity and privilege abuse | partial | `subagents`, `blockParentFast`, `policy-surface-write` | the substrate is one developer's editor session, so identity is the session. There is no per-agent credential to bind a decision to |
|
|
30
|
+
| **ASI04** Agentic supply chain | partial | `supplyChain` | a manifest changed without its lockfile, and a version nobody pinned. Advisories, licences and typosquats need the network on every stop and are left to a gate command |
|
|
31
|
+
| **ASI05** Unexpected code execution | covered | `unprovable-execution`, `unprovable-destruction`, `policy-surface-write` | — |
|
|
32
|
+
| **ASI06** Memory and context poisoning | partial | `policy-surface-write`, `lessons` | the handoff and the project lesson store are sealed on write and withheld when a write the harness did not make is detected. The global lesson store is written by other repositories' sessions and cannot be sealed per project |
|
|
33
|
+
| **ASI07** Insecure inter-agent communication | n/a | `subagents` | subagents are spawned by the host and exchange no messages the harness sits between. There is no wire to secure; what is governed is the spawn |
|
|
34
|
+
| **ASI08** Cascading agent failures | covered | `budgetContinue`, `shellStall`, `idleTurnGate`, `failureClassification`, `grind` | — |
|
|
35
|
+
| **ASI09** Human-agent trust exploitation | covered | `shipGate`, `emptyDiffAntiShip`, `planGate`, `comments` | — |
|
|
36
|
+
| **ASI10** Rogue agents | partial | `observe`, `subagents`, `idleTurnGate` | every decision is recorded and hash-chained, and a stalled or idle agent is caught. There is no behavioural baseline, so an agent acting plausibly but wrongly is not flagged |
|
|
37
|
+
|
|
38
|
+
## How to read "covered"
|
|
39
|
+
|
|
40
|
+
It means every mechanism this project has for that risk is in place and enforced before any policy is read, or is
|
|
41
|
+
a rail an operator can switch on. It does not mean the risk is eliminated. Prompt-level safety is a request to a
|
|
42
|
+
stochastic system, so what is claimed here is only ever about what happens in deterministic code *after* the model
|
|
43
|
+
decides ([/decisions/ad-016.md](/decisions/ad-016.md)).
|
|
44
|
+
|
|
45
|
+
## How to read "partial"
|
|
46
|
+
|
|
47
|
+
The row's limit says what is missing, in the terms an operator would notice. Three of the four partials are
|
|
48
|
+
partial for the same reason: the harness sits at one developer's editor, so it sees actions rather than identities,
|
|
49
|
+
and it sees what a turn wrote rather than what a turn meant.
|
|
50
|
+
|
|
51
|
+
## See also
|
|
52
|
+
|
|
53
|
+
- [/concepts.md](/concepts.md) — every rail from the operator's side
|
|
54
|
+
- [/troubleshooting.md](/troubleshooting.md) — from a refusal on screen back to the rule
|
|
55
|
+
- [/decisions/index.md](/decisions/index.md) — why each rule exists
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-001 — Optional parent-Fast sticky deny for Task spawns"
|
|
4
|
+
description: "subagents.blockParentFast denies Task/subagent spawns while the sticky parent model is Fast, closing a gap where Cursor Fast appears on the parent but not on the Task model string."
|
|
5
|
+
tags: [decision, subagents, policy, cursor]
|
|
6
|
+
timestamp: "2026-07-27"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-001 — Optional parent-Fast sticky deny for Task spawns
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-07-27
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
`subagents.blockParentFast` (default `false`) denies Task / `subagentStart` when sticky parent state is
|
|
17
|
+
Fast; bracket/`model_params` Fast is always treated as blocked in policy helpers.
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
Cursor Fast often appears on the parent (`*-fast` / `model_params.fast=true`) while Task `model` stays
|
|
22
|
+
allowlisted without `-fast`. Without this rail, a Fast parent chat can still spawn full-price subagents.
|
|
23
|
+
|
|
24
|
+
## Applies to
|
|
25
|
+
|
|
26
|
+
- agent-harness hooks
|
|
27
|
+
- harness-init capability 6b (see [/init.md](/init.md))
|
|
28
|
+
|
|
29
|
+
## See also
|
|
30
|
+
|
|
31
|
+
- [/decisions/ad-011.md](/decisions/ad-011.md) — provider model catalogs and blocked patterns
|
|
32
|
+
- [/decisions/ad-016.md](/decisions/ad-016.md) — item 2 fixes the missing writer for this state
|