@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-028 — A resolved failure is kept, a session attests to itself, and provider neutrality becomes a proof"
|
|
4
|
+
description: "The harness deleted the pairing of a failure with what resolved it at the exact moment the gate went green. A reviewer asked to trust agent-written code had nothing to read, while the harness held every input. And the central promise of provider neutrality was asserted by goldens covering one decision kind out of seven."
|
|
5
|
+
tags: [decision, stagnation, attestation, providers, recovery]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-028 — Keep what resolved it, attest to what ran, prove the neutrality
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **When a gate passes after failing, the files changed between those states are recorded against the failure's
|
|
17
|
+
fingerprint.** Captured immediately before the fingerprint record is cleared — the one moment both halves exist.
|
|
18
|
+
2. **On recurrence the follow-up cites that history in one line, in the past tense, saying it is not a list to
|
|
19
|
+
edit.** A test asserts no imperative survives in the wording.
|
|
20
|
+
3. **The store is bounded** at 200 resolutions and 8 files each, pruning the oldest, because it is read on the
|
|
21
|
+
failure path while an operator waits.
|
|
22
|
+
4. **Scope is the repository.** A resolution from elsewhere is not offered.
|
|
23
|
+
5. **A session appends a hash-chained attestation**: policy fingerprint, whether policy diverged mid-session, the
|
|
24
|
+
rails that were active, refusals by rule, and gate outcomes. `tlc harness attest [--json]` reads and verifies
|
|
25
|
+
it, exiting non-zero on a broken chain.
|
|
26
|
+
6. **Verification reports the index at which the chain broke**, and an absent file is an empty valid chain rather
|
|
27
|
+
than a broken one.
|
|
28
|
+
7. **The attestation states only what the harness observed.** No claim of correctness, of review, or of human
|
|
29
|
+
approval.
|
|
30
|
+
8. **Provider neutrality is asserted across the full matrix** — every event kind × every decision kind × every
|
|
31
|
+
registered provider — driven from the registry so a new provider is covered without editing the test.
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
**The harness destroyed its most valuable output at the moment it became true.** It computes an identity for a
|
|
36
|
+
failure and counts repeats to detect stagnation; on success it called `clearFingerprint`, which wiped the record.
|
|
37
|
+
The pairing of *that failure* with *what resolved it* is the one thing nothing else can assemble, because nothing
|
|
38
|
+
else holds a failure fingerprint and the diff in the same process. Products that "learn" from failures store prose;
|
|
39
|
+
this stores evidence.
|
|
40
|
+
|
|
41
|
+
It also addresses the failure mode that matters most in long work: an agent that commits a wrong intermediate state
|
|
42
|
+
deep in a trajectory, where the error propagates because nothing detects or reverts it. A repeated fingerprint is
|
|
43
|
+
precisely that situation announcing itself, and the harness now has something to say about it beyond "you are
|
|
44
|
+
stuck".
|
|
45
|
+
|
|
46
|
+
**The wording is the whole safeguard.** AD-021 recorded the harm of sending an agent to edit healthy code because a
|
|
47
|
+
gate command was malformed; AD-024 recorded the same harm arriving through the diff, and established that a plan
|
|
48
|
+
names files from evidence and never from proximity. A previous resolution is evidence — but the same list phrased
|
|
49
|
+
as an instruction would be that harm arriving through a third door. Hence past tense, hence the explicit "not a
|
|
50
|
+
list to edit", hence a test that fails on an imperative.
|
|
51
|
+
|
|
52
|
+
**A reviewer had nothing to read.** Everything needed to trust an agent-produced change was already in the
|
|
53
|
+
harness — which policy was in force, whether it moved mid-session, every refusal with its rule, every gate
|
|
54
|
+
outcome — and none of it left the machine in a form anyone could check. Governance treated as prompting produces
|
|
55
|
+
no artifact; governance treated as record-keeping does.
|
|
56
|
+
|
|
57
|
+
**And the neutrality claim was thinner than it looked.** `provider.contract.test.ts` asserted the port shape and
|
|
58
|
+
that render survived `abstain`. Six of the seven decision kinds and eighteen of the nineteen event kinds went
|
|
59
|
+
unexercised per provider. A provider that throws on one of those combinations breaks a real turn in production, and
|
|
60
|
+
the test suite would have said nothing.
|
|
61
|
+
|
|
62
|
+
## Trade-offs
|
|
63
|
+
|
|
64
|
+
Hash chaining is not signing, and the record says so by omission: it detects a rewritten or removed middle record,
|
|
65
|
+
which is what an on-disk ledger is exposed to, and it does not prove authorship. A key would prove more and would
|
|
66
|
+
mean key management, a different problem with its own failure modes. If authorship becomes the requirement, that is
|
|
67
|
+
a separate decision.
|
|
68
|
+
|
|
69
|
+
Repository-scoped resolution history is narrower than it could be. The global spool could carry these across
|
|
70
|
+
repositories, and a fix that worked in another codebase is exactly the kind of advice that reads plausible and is
|
|
71
|
+
wrong. Narrow first.
|
|
72
|
+
|
|
73
|
+
The resolution cache pairs a fingerprint with *everything that changed*, which on a wide turn includes files that
|
|
74
|
+
had nothing to do with the fix. The file cap bounds the noise and the wording refuses to assert causation — it says
|
|
75
|
+
what changed, not what fixed it. That is the honest limit of what the data supports.
|
|
76
|
+
|
|
77
|
+
The conformance matrix runs 266 combinations and will grow with every event kind and provider. It is cheap because
|
|
78
|
+
render is pure, and the test asserts the number of combinations it ran: a loop over an empty registry passes
|
|
79
|
+
silently, which is the way this kind of test rots.
|
|
80
|
+
|
|
81
|
+
## Not decided here
|
|
82
|
+
|
|
83
|
+
- **Signed attestation** and any notion of identity. Hash chaining is the tamper evidence; authorship is not
|
|
84
|
+
claimed.
|
|
85
|
+
- **Cross-repository resolution history.** The mechanism exists in the global spool; the advice is dangerous enough
|
|
86
|
+
to want data first.
|
|
87
|
+
- **Whether the attestation should be emitted on stop rather than session end.** Session end has the complete
|
|
88
|
+
rollup; a crash before it leaves no record, which is a gap this accepts.
|
|
89
|
+
- **Asserting that two providers produce byte-identical output.** They cannot — each speaks its own dialect. The
|
|
90
|
+
matrix asserts the decision survives and its meaning is preserved, which is the property that matters.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-029 — A capability the init skill cannot correctly initialise is not shipped"
|
|
4
|
+
description: "Observation mode shipped with one observable rail, a catalog prompt that named none of them, and silent inertness for any other name — so the init skill would present it, collect a value that does nothing, and report success. The observable set now has one source, the prompt names it, and doctor refuses to be silent about a name with no checker."
|
|
5
|
+
tags: [decision, capability, init, observability, doctor]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-029 — A capability the init skill cannot correctly initialise is not shipped
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`OBSERVABLE_RAILS` is the single source of what can be observed**, read by the classifier, by `doctor`, and by
|
|
17
|
+
the capability catalog's own prompt.
|
|
18
|
+
2. **A name enters that list in the same change that adds its `shouldObserve` call site.** A name listed without a
|
|
19
|
+
call site would advertise an observation that never happens.
|
|
20
|
+
3. **`shouldObserve` refuses a rail it cannot observe**, even when the operator listed it, so the function's
|
|
21
|
+
contract matches what the harness can actually do.
|
|
22
|
+
4. **`doctor` reports an unobservable rail by name**, alongside the set that is observable. Observation enabled with
|
|
23
|
+
an empty list is also a warning, because that is the shape a "yes" with no follow-up produces.
|
|
24
|
+
5. **The catalog's `asks` names the valid values and states the consequence of a wrong one**, so the init skill
|
|
25
|
+
collects something real rather than inventing a plausible name.
|
|
26
|
+
6. **Both are warnings, not failures.** A misconfigured measurement rail is a config fault to fix, not a broken
|
|
27
|
+
install, and failing here would block the command an operator runs to find out what is wrong.
|
|
28
|
+
|
|
29
|
+
## Why
|
|
30
|
+
|
|
31
|
+
Observation mode shipped one revision ago with three properties that only make sense separately. Exactly one rail
|
|
32
|
+
has a checker wired to it (`comments`). The catalog asked the operator for "rails (rail names to observe)" without
|
|
33
|
+
naming any. And a name with no checker was **silently inert** — inertness was deliberate, for forward compatibility,
|
|
34
|
+
and the silence was not thought about at all.
|
|
35
|
+
|
|
36
|
+
Put together, the failure is specific and bad: the init skill presents the capability, the operator says yes, a
|
|
37
|
+
plausible name like `plan-gate` gets written, and nothing is ever recorded. The operator then reads that silence as
|
|
38
|
+
*the property always holds*, which is the worst available misreading of a rail whose entire purpose is to tell them
|
|
39
|
+
whether a rule is needed. A measurement that reads as a clean result when it never ran is worse than no measurement.
|
|
40
|
+
|
|
41
|
+
This is the AD-020 family again — a config value nothing reads — and it arrived through a route the previous
|
|
42
|
+
instances did not: not a field with no producer, but a field whose *values* have no producer. The check that caught
|
|
43
|
+
it was the question "can the init skill actually initialise this?", which is worth asking of every capability rather
|
|
44
|
+
than of this one.
|
|
45
|
+
|
|
46
|
+
The general rule this establishes: **a capability is not finished when its code works. It is finished when the
|
|
47
|
+
surface that offers it can offer it correctly.** The init skill, `doctor` and the catalog prompt are part of the
|
|
48
|
+
capability, not documentation of it.
|
|
49
|
+
|
|
50
|
+
## Trade-offs
|
|
51
|
+
|
|
52
|
+
Forward-compatible inertness is kept — an unknown name does not throw, so a config written for a future version of
|
|
53
|
+
the harness still loads. What changed is that inertness is now reported. Refusing to load would be the other
|
|
54
|
+
defensible choice and it would break the forward compatibility this project has elsewhere; reporting keeps both.
|
|
55
|
+
|
|
56
|
+
`OBSERVABLE_RAILS` is a hand-maintained list, which is a small duplication against the call sites. Deriving it
|
|
57
|
+
mechanically would mean either a registry the call sites enrol in — more machinery than one entry justifies — or
|
|
58
|
+
scanning source at runtime, which the latency budget forbids. The invariant is stated in a comment and asserted by a
|
|
59
|
+
test that pins the list, so adding a rail without its call site fails.
|
|
60
|
+
|
|
61
|
+
One rail is a thin start, and naming it explicitly makes that thinness visible in the catalog rather than implied.
|
|
62
|
+
That is the point: an operator reading "observable today: comments" knows the shape of what they are buying, and an
|
|
63
|
+
operator reading "rail names to observe" does not.
|
|
64
|
+
|
|
65
|
+
## Not decided here
|
|
66
|
+
|
|
67
|
+
- **Observing the plan gate or the ship gate.** Both have deterministic checkers and would be genuine additions.
|
|
68
|
+
Each needs its own call site and its own entry in the list, in a change that adds both together.
|
|
69
|
+
- **Deriving the observable set from the call sites mechanically.** Worth revisiting if the list reaches a size
|
|
70
|
+
where hand-maintenance is a real risk.
|
|
71
|
+
- **Whether the init skill should refuse to write an unobservable name.** Today it can write one and `doctor` will
|
|
72
|
+
say so. Validating at write time is stricter and would need `init` to validate fields generally, which it does not
|
|
73
|
+
do for any other value.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-030 — Clearing a policy divergence is one operator command behind four independent locks, and a refusal never points an agent at a door the floor holds shut"
|
|
4
|
+
description: "The integrity check locked two live sessions out because its recovery path named no command, pointed the agent at commands the floor refuses, and did not exist as a side-effect-free operation. `tlc harness policy accept` is that operation, kept out of an agent's reach by four locks that fail independently, recorded rather than erased — and a sweep now fails the build when an agent-facing refusal instructs a command the agent cannot run."
|
|
5
|
+
tags: [decision, policy, integrity, security, cli]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-030 — One operator command, four locks, and a refusal that knows its audience
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **`tlc harness policy` lists every source that changed out of band during a live session and changes nothing.**
|
|
17
|
+
2. **`tlc harness policy accept <path>...` updates the baseline entry for exactly the named paths**, in every live
|
|
18
|
+
session's baseline.
|
|
19
|
+
3. **Four independent locks sit between that command and an agent**, no one of them load-bearing alone:
|
|
20
|
+
- the floor refuses `tlc harness policy` from inside any agent session, with no config switch;
|
|
21
|
+
- the command refuses without an interactive terminal, so a script cannot reach it;
|
|
22
|
+
- the operator names each path, so accepting is an act rather than a keystroke and its blast radius is exactly
|
|
23
|
+
what was typed;
|
|
24
|
+
- acceptance is per source, so every other divergence keeps blocking.
|
|
25
|
+
4. **No blanket permission is expressible.** The accepted hash is the hash at that moment; a later change to the
|
|
26
|
+
same file diverges again. There is deliberately no way to stop watching a source.
|
|
27
|
+
5. **A path the loader never reads is refused**, naming the sources that exist.
|
|
28
|
+
6. **`reason` is the agent's half of a refusal and `userNote` is the operator's.** An agent-facing refusal states
|
|
29
|
+
what happened and says to report it; the command that clears it lives in the operator's half.
|
|
30
|
+
7. **A sweep over every refusal producer fails the build** when a `reason` instructs a mutating harness subcommand.
|
|
31
|
+
8. **`doctor` reports a divergence, naming the paths and the command**, and is silent when there is none.
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
The integrity check has now blocked two live sessions: its own author, and a colleague's agent after an operator
|
|
36
|
+
edited `config.json` from their terminal. The check behaved correctly both times — a mid-session policy change with
|
|
37
|
+
no harness command behind it is precisely what it exists to catch. The recovery path is what failed, in four
|
|
38
|
+
measurable ways.
|
|
39
|
+
|
|
40
|
+
**It named no command.** The refusal said "the harness commands re-record the baseline when they write". Only
|
|
41
|
+
`grind`, `pause|resume`, `mode` and `gate` do. `status` and `doctor` do not — and `status` is exactly what a
|
|
42
|
+
reasonable reader tries first, because it sounds like the command that reports state. The blocked agent tried it,
|
|
43
|
+
learned nothing, and stopped.
|
|
44
|
+
|
|
45
|
+
**It pointed the agent at a door the floor holds shut.** Every command that would have worked is in
|
|
46
|
+
`MUTATING_SUBCOMMANDS`, refused from inside a session by design. So the instruction was not merely unhelpful; it was
|
|
47
|
+
addressed to the one party that cannot act on it. This is the third instance of that defect: the policy-surface
|
|
48
|
+
guard had it and was fixed under AD-026, the integrity check had it here, and the sweep built for this decision
|
|
49
|
+
found a third in the `paired` shell ask on its first run. Three instances is not carelessness about wording — it is
|
|
50
|
+
a missing invariant, which is why the sweep exists.
|
|
51
|
+
|
|
52
|
+
**No side-effect-free operation existed.** Every route that repairs a baseline also writes something: `mode` writes
|
|
53
|
+
a state file that then outranks config, `grind` writes a flag that outranks it too. An operator fixing a lockout
|
|
54
|
+
would silently acquire a latent surprise for the next time they edited config.
|
|
55
|
+
|
|
56
|
+
**And `doctor` was silent.** The one command an operator runs to find out what is wrong said nothing about the thing
|
|
57
|
+
blocking every acting tool call.
|
|
58
|
+
|
|
59
|
+
The design question that matters is not "add an accept command" but **"what keeps an accept command out of reach of
|
|
60
|
+
the thing it defends against?"** A command that clears a tampering signal is the first thing an attacker wants, and
|
|
61
|
+
one lock is one bug away from nothing. Hence four, at different layers, testable separately: a floor rule that no
|
|
62
|
+
config can weaken, a TTY requirement that stops scripts, an explicit naming that cannot happen by accident, and
|
|
63
|
+
per-source scope that limits what a single accept can bless.
|
|
64
|
+
|
|
65
|
+
## Trade-offs
|
|
66
|
+
|
|
67
|
+
**Accept is recorded, not silent.** Without that it would be a memory hole: the rail's own history could be erased
|
|
68
|
+
by the command meant to resolve it. Recorded, it is an operator decision a later reviewer can see, which is the
|
|
69
|
+
difference between a sanctioned override and a bypass.
|
|
70
|
+
|
|
71
|
+
**Naming the path instead of prompting.** A y/N prompt is the obvious alternative and it is worse twice over: a
|
|
72
|
+
prompt is a keystroke, and keystrokes become reflex — the finding AD-026 acted on — and a prompt cannot be tested
|
|
73
|
+
without a pty, on a rail whose failure mode is silence. Typing the path is slower on purpose.
|
|
74
|
+
|
|
75
|
+
**Per-source acceptance required new machinery.** `refreshPolicyBaselines` rewrites the whole fingerprint, which is
|
|
76
|
+
right for a sanctioned mutation that changed one thing and wrong here: it would bless every other pending change
|
|
77
|
+
alongside the one named. `acceptPolicySources` writes one entry at a time and leaves the rest untouched.
|
|
78
|
+
|
|
79
|
+
**The sweep is a line-scoped scan, not an AST walk.** It reads the `reason:` property and the continuation lines of
|
|
80
|
+
its array literal, which is the shape every refusal in this codebase uses, and skips comments. It asserts how many
|
|
81
|
+
reason strings it scanned, because a scan that parses nothing passes forever. A refusal built in some other shape
|
|
82
|
+
would escape it — the test says so rather than implying completeness.
|
|
83
|
+
|
|
84
|
+
**Social engineering is not solved.** An agent cannot run accept, and nothing stops it from writing a message that
|
|
85
|
+
persuades an operator to. The mitigation is that accept prints the path, so the operator sees what they are being
|
|
86
|
+
asked to bless. Stated here rather than pretended away.
|
|
87
|
+
|
|
88
|
+
## Not decided here
|
|
89
|
+
|
|
90
|
+
- **Whether a read should ever refresh a baseline.** It should not, and this decision does not revisit it: a
|
|
91
|
+
tampering signal cleared as a side effect of looking is not a signal.
|
|
92
|
+
- **Recording the acceptance in a dedicated ledger.** The attestation carries `policyDiverged` for the session; a
|
|
93
|
+
per-acceptance audit row would be finer and needs its own decision about retention.
|
|
94
|
+
- **Refusing to load a config whose divergence was never accepted.** The current design blocks acting tool calls and
|
|
95
|
+
allows reads ([/decisions/ad-025.md](/decisions/ad-025.md)), which is what lets a blocked agent explain the situation. Failing closed would remove that.
|
|
96
|
+
- **Extending the sweep to `userNote`, `text` and `detail` strings.** Only `reason` is agent-facing today. The others
|
|
97
|
+
reach the operator, where naming a command is correct.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-031 — The decisions are the changelog, a breaking change carries its own instruction, and looking never changes anything"
|
|
4
|
+
description: "Update did the mechanical work well and could not say what changed: no version identity, no mapping from a revision to the decisions that landed, and no channel for a breaking change at the moment of impact — three shipped in one week and only doctor mentioned them afterwards. The revision is the version, a migration note lives in the decision that caused it, and `update --check` fetches without merging."
|
|
5
|
+
tags: [decision, release, update, changelog, cli]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-031 — The decisions are the changelog
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **The runtime's git revision and commit date are the version.** `tlc harness version` reports them, plus the
|
|
17
|
+
revision this project last saw.
|
|
18
|
+
2. **A breaking change carries a `migration` note in the frontmatter of the decision that caused it.** Optional;
|
|
19
|
+
validated as non-empty when present.
|
|
20
|
+
3. **`migration` marks a decision as needing operator action.** Amended by
|
|
21
|
+
[/decisions/ad-034.md](/decisions/ad-034.md): a note belongs only where `doctor` cannot detect the condition for
|
|
22
|
+
the operator. Five of the first six notes repeated a doctor row and were removed.
|
|
23
|
+
4. **`tlc harness update --check` fetches and never merges**, reporting the pending commit count and the decisions
|
|
24
|
+
that would land, with the ones needing action first and their notes shown.
|
|
25
|
+
5. **After an update, decisions added between the seen revision and the new one are announced once.** A project with
|
|
26
|
+
no marker records where it stands and announces nothing.
|
|
27
|
+
6. **A fast-forward failure names both routes out** — reset to upstream, or re-run the installer — gives the command
|
|
28
|
+
for the first, and runs neither.
|
|
29
|
+
7. **There is no hand-written changelog and no semantic version.**
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
`update` already did the mechanical work well: fetch, fast-forward, relink the CLI and skill, rewrite hooks, rebuild
|
|
34
|
+
`dist/`, announce newly available capabilities, run `doctor`. One command ending in a health check. What it could not
|
|
35
|
+
do was tell the operator what changed, for reasons that compound.
|
|
36
|
+
|
|
37
|
+
**There was no version identity.** `package.json` has said `0.1.0` since the first commit — which is exactly what a
|
|
38
|
+
hand-maintained number does — with no tags and no releases, and `tlc harness version` answered `unknown: version`.
|
|
39
|
+
Without an identity, "what changed since mine?" is not a hard question, it is an unanswerable one.
|
|
40
|
+
|
|
41
|
+
**The substance of a changelog already existed and was unreachable.** Thirty decision records carry why, the
|
|
42
|
+
trade-offs and what was refused, which is more than most changelogs contain. What was missing was the mapping from
|
|
43
|
+
*I updated* to *these landed*. Writing a `CHANGELOG.md` would have restated them and then drifted from them; reading
|
|
44
|
+
them needs no second source to maintain.
|
|
45
|
+
|
|
46
|
+
**A breaking change had no channel at the moment of impact, and three shipped in one week.** AD-025 removed a
|
|
47
|
+
posture's second spelling, so a config carrying it falls back silently at load. AD-029 made an unobservable rail name
|
|
48
|
+
reportable, which means a config naming one had been recording nothing. AD-027 is the sharpest: ordering evidence
|
|
49
|
+
against the code can newly **block a stop that previously passed**. `doctor` reports all three afterwards — that is
|
|
50
|
+
the safety net working — but the operator was standing in front of `update` when the change happened, and it said
|
|
51
|
+
nothing.
|
|
52
|
+
|
|
53
|
+
The mechanism was already in the codebase and pointed at the wrong subject. `announceNewCapabilities` reads a
|
|
54
|
+
per-project seen marker, lists only what is new *to that project*, prints benefit and trade-off, and never repeats.
|
|
55
|
+
That is a changelog delivered at the moment of impact. Applying the same shape to decisions is the whole feature.
|
|
56
|
+
|
|
57
|
+
## Trade-offs
|
|
58
|
+
|
|
59
|
+
**No semantic version.** A version number is a promise about compatibility, and AD-003 refuses back-compat shims —
|
|
60
|
+
so the promise cannot be kept and stating it would be a lie with a dot in it. The revision is honest, moves with the
|
|
61
|
+
thing it names, and is what `update` already manipulates. Tags become worth adding when the compatibility promise
|
|
62
|
+
becomes real.
|
|
63
|
+
|
|
64
|
+
**`migration` is optional, which means it can be forgotten.** Requiring it on every decision would produce "no
|
|
65
|
+
migration needed" on the large majority and train a reader to skip the field. The bundle check catches the halfway
|
|
66
|
+
state — present and empty — because a decision announced as needing action that then shows nothing is worse than one
|
|
67
|
+
that never claimed it. Forgetting it entirely is still possible, and `doctor` remains the net.
|
|
68
|
+
|
|
69
|
+
**A first update announces nothing.** A project with no seen marker could be shown every decision ever written, and
|
|
70
|
+
thirty entries is indistinguishable from no message. It records where it stands instead. The cost is that an operator
|
|
71
|
+
installing today learns nothing about the past from this rail — which is what `docs/decisions/index.md` is for.
|
|
72
|
+
|
|
73
|
+
**The digest reads decisions added in a range**, so a decision *amended* after landing is not re-announced. That is
|
|
74
|
+
deliberate: an amendment is usually a wording fix, and re-announcing on every edit is how the capability digest would
|
|
75
|
+
have become noise too.
|
|
76
|
+
|
|
77
|
+
**The three retroactive notes were written after the fact**, by the author of the changes they describe. That is the
|
|
78
|
+
weakest part of this: they are as good as the memory of the week they cover. Notes written at the time of the change
|
|
79
|
+
are the standard from here.
|
|
80
|
+
|
|
81
|
+
**Amended the day after by AD-034.** Five of the six notes then in the tree said, in substance, "run
|
|
82
|
+
`tlc harness doctor`" — which `update` runs automatically three lines later, and which reports whether the condition
|
|
83
|
+
applies at all. The mechanism was right and the content was noise, so `migration` now means specifically "something
|
|
84
|
+
doctor cannot detect for you".
|
|
85
|
+
|
|
86
|
+
## Not decided here
|
|
87
|
+
|
|
88
|
+
- **Tags and releases.** Worth adding when there is a compatibility promise to attach to them.
|
|
89
|
+
- **Announcing on session start rather than on update.** The information belongs where the change happens, and a
|
|
90
|
+
hook that reports it every session is a cost on every session.
|
|
91
|
+
- **Checking for updates in the background.** A scheduled phone-home is a permanent cost for information the operator
|
|
92
|
+
can ask for in one command.
|
|
93
|
+
- **Migration notes that the harness applies.** `config.json` is a protected surface and policy is the operator's
|
|
94
|
+
([/decisions/ad-022.md](/decisions/ad-022.md)). A note says what to run; running it is not the harness's call.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-032 — A hook is healthy when it can run, not when a marker string is present"
|
|
4
|
+
description: "A colleague's session was blocked by a preToolUse hook whose command was a bare executable, so Node read the hook payload as a program. The harness could not have written that command — but its health check could not have caught it either, because it decided wiring health by looking for one string. Health is now per event: our launcher named, the script present, a handler after it."
|
|
5
|
+
tags: [decision, providers, wiring, doctor, diagnosis]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-032 — A hook is healthy when it can run
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **Wiring health is checked per declared event**: an entry must name the expected launcher, that file must exist,
|
|
17
|
+
and a handler token must follow it.
|
|
18
|
+
2. **Only the events this provider declares are inspected**, and within them only entries whose command names our
|
|
19
|
+
launcher. A hook belonging to another tool is never reported.
|
|
20
|
+
3. **A declared event with no harness entry is a problem**, which is the case marker presence cannot see.
|
|
21
|
+
4. **Marker presence keeps its job unchanged** — deciding whether the file is ours, and therefore whether `update`
|
|
22
|
+
may overwrite it. Two questions, two checks.
|
|
23
|
+
5. **`doctor` names the failing event and the reason**, bounded to three with a count of the rest.
|
|
24
|
+
6. **Command tokens are parsed respecting quotes**, because the writer quotes a path containing spaces and a checker
|
|
25
|
+
that mis-tokenises reports a healthy wiring as broken.
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
29
|
+
A colleague's Cursor session stopped working. The `preToolUse` hook crashed with a raw Node stack trace and the hook
|
|
30
|
+
payload echoed back — conversation id, workspace path, email, and the command they were about to run. The entry was
|
|
31
|
+
`failClosed: true`, so the crash did not merely log: it blocked the tool.
|
|
32
|
+
|
|
33
|
+
The signature was `node:internal/main/eval_stdin`, which occurs only when `node` is invoked with **no script** and
|
|
34
|
+
stdin is a pipe: Node reads the payload as a program, and the first `:` in the JSON is a syntax error.
|
|
35
|
+
|
|
36
|
+
Two things were worth separating. **The harness could not have written that command.** `cursorWiring` builds `args`
|
|
37
|
+
as `[launcherPath, handler]`, so every rendered command carries three tokens; even an empty launcher path renders
|
|
38
|
+
`node tool-before`, which fails as a missing module and not as an eval. `tlc-exec.mjs` is ruled out too — every run
|
|
39
|
+
branch carries non-empty args, and a missing entry exits 2 with usage. Verified by rendering, not by reading.
|
|
40
|
+
|
|
41
|
+
**But the harness could not have caught it either, and that part is ours.** `isCursorWired` decides health by
|
|
42
|
+
checking whether the file *contains* the string `tlc-exec.mjs`. That is a good proxy for "is this file ours", which
|
|
43
|
+
is the right question when deciding whether `update` may overwrite it. It is the wrong question for "do these hooks
|
|
44
|
+
work": a file with the marker in one entry and a broken command in another reported `wired`, and `doctor` said the
|
|
45
|
+
provider was healthy. Nothing checked that the executable resolves, that the script exists, or that every event the
|
|
46
|
+
harness needs has an entry at all. A hook whose script moved in a reinstall reported healthy until it fired.
|
|
47
|
+
|
|
48
|
+
And when `doctor` did notice, it said `detected but not wired — run: tlc harness update` and nothing else. An
|
|
49
|
+
operator learned that something was wrong, which is one step above silence.
|
|
50
|
+
|
|
51
|
+
## Trade-offs
|
|
52
|
+
|
|
53
|
+
**The check resolves paths; it does not run hooks.** Running a hook to prove it runs would run it, with whatever side
|
|
54
|
+
effects that implies on a `preToolUse`. Naming the launcher, checking the file exists and requiring a handler is what
|
|
55
|
+
can be established without acting — and it is exactly the set of things that produced this incident.
|
|
56
|
+
|
|
57
|
+
**Foreign hooks are ignored, which means a foreign hook can still break a session.** That is deliberate: reporting
|
|
58
|
+
another tool's entry would train an operator to skip the check, and the check would then miss ours. The harness
|
|
59
|
+
reports on what it wrote.
|
|
60
|
+
|
|
61
|
+
**A fresh install with no wiring produces one problem per declared event**, nineteen of them, which is why the
|
|
62
|
+
formatted detail is bounded to three plus a count. A doctor line that is a wall is a line nobody reads.
|
|
63
|
+
|
|
64
|
+
**Quote-aware tokenising was found by a test, not by review.** Splitting on whitespace broke a launcher path
|
|
65
|
+
containing spaces — a macOS path under "Application Support" is an ordinary shape — and a checker that
|
|
66
|
+
reports a healthy wiring as broken is the failure mode that gets a check switched off. The writer already quoted for
|
|
67
|
+
this reason; the reader now matches it.
|
|
68
|
+
|
|
69
|
+
**One pre-existing test asserted the weak rule** — marker present, one entry out of nineteen, expect `wired` — and it
|
|
70
|
+
had to be inverted rather than deleted. It now asserts the incident's shape.
|
|
71
|
+
|
|
72
|
+
## Not decided here
|
|
73
|
+
|
|
74
|
+
- **Where the broken hook came from.** The file is not the harness's output, so either it was hand-edited, written by
|
|
75
|
+
a much older revision, or written by another tool. Answering it needs the operator's `hooks.json`, and the fix does
|
|
76
|
+
not depend on the answer.
|
|
77
|
+
- **Whether `update` should overwrite a file that is ours but broken.** Today it refuses when the marker is absent and
|
|
78
|
+
rewrites when present, which already covers the broken-but-ours case. A file with the marker and a foreign entry
|
|
79
|
+
alongside is the one shape where overwriting loses something, and it stays refused.
|
|
80
|
+
- **Checking the Claude settings shape more deeply.** Its status already comes from a merge comparison against the
|
|
81
|
+
desired entries, so a changed command is caught. Only the replace-strategy path used the weak check.
|
|
82
|
+
- **Rewriting a hook's crash output.** The stack trace and echoed payload come from Node running someone else's
|
|
83
|
+
malformed command. The harness cannot format an error it did not produce.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-033 — The gate says what it costs, appendFiles stops promising what it cannot deliver, and a dead capability leaves"
|
|
4
|
+
description: "An operator reported thirty minutes on a small feature. The cause was not his machine: appendFiles auto advertised narrowing the gate to changed files and could deliver it for neither of his commands, so a full lint and a full suite ran on every one of three attempts. Doctor now names that, measures the interpreter start instead of asserting it, the report shows time per gate, and the format capability nothing ever read is gone."
|
|
5
|
+
tags: [decision, grind, cost, doctor, observability]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-033 — The gate says what it costs
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **A package-manager script does not receive appended files.** `npm`, `yarn`, `pnpm` and `bun run` invoke somebody
|
|
17
|
+
else's script, and what that script does with a path is not something this process can know.
|
|
18
|
+
2. **A command that already carries a glob does not receive appended files.** It walks the glob regardless, so
|
|
19
|
+
appending narrows nothing.
|
|
20
|
+
3. **A transparent prefix resolves through to the real tool.** `npx`, `bunx` and `dlx` run the tool named next, and
|
|
21
|
+
that tool decides whether a path narrows — so `npx jest <file>` still narrows.
|
|
22
|
+
4. **`appendFilesVerdict` returns a reason**, and `doctor` reports a `gate scope` warning naming the command, the
|
|
23
|
+
reason, and `maxLoops`.
|
|
24
|
+
5. **`doctor` measures the interpreter's cold start** — three samples, median — and reports no number when the
|
|
25
|
+
measurement fails. The hardcoded millisecond claim is gone.
|
|
26
|
+
6. **The session report shows runs, total and worst run per gate**, and states that the cost is paid once per attempt.
|
|
27
|
+
7. **`format.enabled` and `format.command` are removed** from the policy type, the defaults, the loader, the catalog
|
|
28
|
+
and the docs.
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
An operator reported the harness as "extremely slow" — thirty minutes on a three-model feature, "still running jest,
|
|
33
|
+
format and whatever". Three findings, none of them his machine.
|
|
34
|
+
|
|
35
|
+
**The harness was not running format.** `format` was capability number one in the catalog, presented to every operator
|
|
36
|
+
by the init skill, declared in `Policy`, merged by the loader, and read by no entrypoint. Fifth instance in one week
|
|
37
|
+
of a field advertised and never consumed, and the worst placed of them. AD-003 removes what nothing reads rather than
|
|
38
|
+
honouring it late, and implementing it now — a command after every Write — would ship exactly the failure mode being
|
|
39
|
+
reported.
|
|
40
|
+
|
|
41
|
+
**The cost was a promise `appendFiles` could not keep.** His configuration, verbatim: `appendFiles: "auto"`,
|
|
42
|
+
`lintCommand: ["npx","eslint","src/**/*.ts","test/**/*.ts","--no-fix"]`, `testCommand: ["npm","test"]`,
|
|
43
|
+
`maxLoops: 3`. The lint command carries its own globs, so appending changed files narrows nothing and the whole tree
|
|
44
|
+
is linted on every stop. The test command is a package-manager script, so whether appended paths reach the runner
|
|
45
|
+
depends on the manager and its version — and `shouldAppendFiles(["npm","test"], "auto")` returned **true**, verified
|
|
46
|
+
by running it.
|
|
47
|
+
|
|
48
|
+
The codebase already knew this. `RESOLUTION_FAILURE_PATTERNS` contains `missing script:`, which is npm's message —
|
|
49
|
+
so npm was recognised as a script runner for the purpose of classifying a failure, and not for the purpose of
|
|
50
|
+
deciding whether to append arguments to it. The recipe-runner exception exists for precisely this reason and had the
|
|
51
|
+
wrong membership.
|
|
52
|
+
|
|
53
|
+
**And the one number `doctor` offered about speed was prose.** `BUN_COST_NOTE` asserted "hook cost ~1 ms with Bun vs
|
|
54
|
+
~27 ms with Node" on every machine and had measured it on none. An operator with a slow install had nothing to
|
|
55
|
+
measure and nothing to point at, which is how "the harness is slow" becomes unfalsifiable. It measures now: 2 ms on
|
|
56
|
+
the machine this decision was written on.
|
|
57
|
+
|
|
58
|
+
The data to answer his question was already recorded — `gate.outcome` carries `duration_ms` — and surfaced nowhere.
|
|
59
|
+
|
|
60
|
+
## Trade-offs
|
|
61
|
+
|
|
62
|
+
**Not appending is the conservative direction.** The command runs as the operator wrote it, which is what they can
|
|
63
|
+
reason about. Appending is what produced the surprise. `always` remains for an operator who knows their runner
|
|
64
|
+
forwards, and `never` remains for one who wants the full run.
|
|
65
|
+
|
|
66
|
+
**`bun test` narrows and `bun run test` does not**, which is a distinction inside one executable. It is real — the
|
|
67
|
+
first is bun's own runner, the second is a script — and encoding it means a special case. The alternative was to
|
|
68
|
+
refuse narrowing for all of `bun`, which would penalise the shape that works.
|
|
69
|
+
|
|
70
|
+
**The glob rule is a character match**, so a literal `[` in a filename would read as a glob and stop narrowing. The
|
|
71
|
+
failure direction is the safe one: the command runs as written.
|
|
72
|
+
|
|
73
|
+
**The measured figure is the interpreter's start, not the whole hook.** Every entrypoint has side effects — an
|
|
74
|
+
unrecognised payload writes an obs record into the operator's repository — so a full round trip cannot be measured
|
|
75
|
+
without acting. The label says which it is, because reporting a part as the whole is the overclaim this replaces.
|
|
76
|
+
|
|
77
|
+
**Removing `format` breaks nothing and deletes an option some operator may have enabled**, believing it worked. It
|
|
78
|
+
never did. A stale block in a config still loads and is ignored, so there is nothing to do about it and — per
|
|
79
|
+
[/decisions/ad-034.md](/decisions/ad-034.md) — nothing worth telling an operator to do.
|
|
80
|
+
|
|
81
|
+
## Not decided here
|
|
82
|
+
|
|
83
|
+
- **Implementing format-on-edit.** It would be a new capability with a measured per-Write cost, decided on its own
|
|
84
|
+
evidence rather than inherited from a field that never ran.
|
|
85
|
+
- **Warning about `maxLoops` itself.** Three or five is a considered choice; multiplying a cheap gate is fine and
|
|
86
|
+
multiplying an expensive one is what the `gate scope` row is for.
|
|
87
|
+
- **Detecting a slow gate command and suggesting a faster one.** The harness would be guessing at somebody's test
|
|
88
|
+
setup. It reports the time and the multiplication; the choice is theirs.
|
|
89
|
+
- **Measuring a full hook round trip.** Needs an entrypoint with no side effects, which none of them is today.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: "AD-034 — A warning that fires on a healthy install is not a warning, and the author reads the operator's output before anyone else does"
|
|
4
|
+
description: "A real update run put NEEDS YOUR ACTION on two changes that did not apply, both saying run doctor — which update ran automatically three lines later — with an escaped quote leaking into the terminal, above nine warnings for capabilities nobody had enabled, under a line claiming all checks passed. Every piece had a passing test. A migration note now means something doctor cannot detect, unenabled capabilities are inventory, and the author runs the operator-facing commands and reads them before shipping."
|
|
5
|
+
tags: [decision, process, doctor, release, noise]
|
|
6
|
+
timestamp: "2026-08-04"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# AD-034 — A warning that fires on a healthy install is not a warning
|
|
10
|
+
|
|
11
|
+
- **status**: active
|
|
12
|
+
- **date**: 2026-08-04
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **A `migration` note exists only where `doctor` cannot detect the condition for the operator.** Five of the six
|
|
17
|
+
notes then in the tree said, in substance, "run `tlc harness doctor`" — which `update` runs automatically. They are
|
|
18
|
+
removed. AD-027 keeps its note because a stop blocked by evidence ordering is invisible to every check.
|
|
19
|
+
2. **The digest leads with the note and trails the decision id.** A decision's title states the author's reasoning;
|
|
20
|
+
the operator's situation is what belongs on the first line.
|
|
21
|
+
3. **The heading appears only when a note exists**, and says what a note now means. With nothing to do, the digest
|
|
22
|
+
states what landed and points at the doctor run below rather than asking for it.
|
|
23
|
+
4. **A frontmatter value is unescaped before display.** An escaped quote reached a terminal as a literal backslash.
|
|
24
|
+
5. **A capability that is merely not enabled is one `ok` inventory row**, not one warning each.
|
|
25
|
+
6. **The doctor summary counts failures and warnings separately** and never says everything passed while printing a
|
|
26
|
+
warning.
|
|
27
|
+
7. **Before asking anyone else to test, the author runs every operator-facing command and reads the output.** Unit
|
|
28
|
+
tests assert pieces; the defects here were all in the composition and in the reading.
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
An operator ran `tlc harness update` and the output contained four defects, each of which had a passing test.
|
|
33
|
+
|
|
34
|
+
`NEEDS YOUR ACTION (2)` — and neither applied to him. Three lines below, `doctor` reported his Cursor wiring as
|
|
35
|
+
`wired` and his config carried no `format` block. Both notes said "run `tlc harness doctor`", which the update then
|
|
36
|
+
did automatically. One of them rendered `\"detected but not wired\"` with the backslashes visible, because the value
|
|
37
|
+
was written with escaped quotes inside a quoted string and only the outer pair was stripped.
|
|
38
|
+
|
|
39
|
+
Below that, nine warnings for capabilities he had never enabled — and buried among them the three rows that mattered:
|
|
40
|
+
a policy baseline changed out of band, and two gate commands running in full on every attempt. Then, under all of it,
|
|
41
|
+
`doctor: all checks passed`.
|
|
42
|
+
|
|
43
|
+
Every one of those pieces was individually correct and individually tested. The escaping test did not exist because
|
|
44
|
+
the value looked fine in the source. The digest test asserted the heading appeared, which it did. The capability
|
|
45
|
+
warning test asserted a warning was produced, which it was. What no test covered, and no test naturally would, is
|
|
46
|
+
**what the whole thing reads like to the person it is for**.
|
|
47
|
+
|
|
48
|
+
The through-line is one idea, arriving for the fourth time this week. AD-026 removed an approval prompt that fired
|
|
49
|
+
often enough to become a keystroke. AD-029 removed a silence an operator would misread as a clean result. AD-032
|
|
50
|
+
removed a health check that passed on a broken install. This removes an alarm that fires on a healthy one. In every
|
|
51
|
+
case the mechanism worked and the signal was worthless, because **a signal that appears when nothing is wrong cannot
|
|
52
|
+
carry the case where something is.**
|
|
53
|
+
|
|
54
|
+
And the process failure is mine and worth writing down plainly: the harness was handed to two colleagues today and
|
|
55
|
+
broke in front of both. The first was a hook that could not run; the second was thirty minutes of gate time nobody
|
|
56
|
+
could see. Both were found by them, not by me, and both were visible in output I had never once read end to end. The
|
|
57
|
+
tests were not the problem. Not looking was.
|
|
58
|
+
|
|
59
|
+
## Trade-offs
|
|
60
|
+
|
|
61
|
+
**Removing five notes loses real information for an operator who never runs `doctor`.** That is accepted: `update`
|
|
62
|
+
runs `doctor` at the end, so the only way to miss it is to ignore the output — and a reader who ignores the output is
|
|
63
|
+
exactly the reader an extra alarm does not reach.
|
|
64
|
+
|
|
65
|
+
**One inventory row is less discoverable than nine warnings.** Deliberately. `update` still prints each capability
|
|
66
|
+
with its benefit and trade-off in full, which is the moment an operator is choosing; `doctor` is the moment they are
|
|
67
|
+
diagnosing, and a list of things they decided not to enable is not a diagnosis.
|
|
68
|
+
|
|
69
|
+
**`migration` now depends on a judgement — can doctor see this?** — which cannot be checked mechanically. The bundle
|
|
70
|
+
check still catches a present-but-empty note. The judgement is stated here so the next note is written against it
|
|
71
|
+
rather than against a habit.
|
|
72
|
+
|
|
73
|
+
**Reading the output by hand does not scale and is not a test.** It found four defects that four suites did not, so
|
|
74
|
+
it earns its place as a step rather than as a substitute. The narrower part is testable and now tested: the digest's
|
|
75
|
+
shape, the unescaping, the inventory row, the summary wording.
|
|
76
|
+
|
|
77
|
+
## Not decided here
|
|
78
|
+
|
|
79
|
+
- **A golden test over the whole `update` output.** It would have caught all four, and it would also fail on every
|
|
80
|
+
wording change, which is how a golden becomes a rubber stamp. The composition is checked by reading; the pieces by
|
|
81
|
+
assertions.
|
|
82
|
+
- **Whether `doctor` should have a quiet mode.** With the wall gone the output is short enough that it has not earned
|
|
83
|
+
a flag.
|
|
84
|
+
- **Re-announcing a decision whose note is added later.** The digest reads decisions added in a range, so a note
|
|
85
|
+
written after the fact is not delivered. Notes belong in the change that causes them, which is the standard this
|
|
86
|
+
decision is a correction toward.
|